first commit
This commit is contained in:
16
src/pages/agent-manage-agreement.vue
Normal file
16
src/pages/agent-manage-agreement.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { useReportWebview } from '@/composables/useReportWebview'
|
||||
|
||||
definePage({ layout: false })
|
||||
|
||||
const { buildSitePathUrl } = useReportWebview()
|
||||
const src = ref('')
|
||||
|
||||
onLoad(() => {
|
||||
src.value = buildSitePathUrl('/app/agentManageAgreement')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<web-view :src="src" />
|
||||
</template>
|
||||
239
src/pages/agent-promote-details.vue
Normal file
239
src/pages/agent-promote-details.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<script setup>
|
||||
definePage({ layout: 'default', auth: true })
|
||||
// 颜色配置(根据产品名称映射)
|
||||
const typeColors = {
|
||||
小微企业: { bg: 'bg-blue-100', text: 'text-blue-800', dot: 'bg-blue-500' },
|
||||
入职风险: { bg: 'bg-green-100', text: 'text-green-800', dot: 'bg-green-500' },
|
||||
家政风险: { bg: 'bg-purple-100', text: 'text-purple-800', dot: 'bg-purple-500' },
|
||||
婚恋风险: { bg: 'bg-pink-100', text: 'text-pink-800', dot: 'bg-pink-500' },
|
||||
贷前风险: { bg: 'bg-orange-100', text: 'text-orange-800', dot: 'bg-orange-500' },
|
||||
租赁风险: { bg: 'bg-indigo-100', text: 'text-indigo-800', dot: 'bg-indigo-500' },
|
||||
个人风险: { bg: 'bg-red-100', text: 'text-red-800', dot: 'bg-red-500' },
|
||||
个人大数据: { bg: 'bg-red-100', text: 'text-red-800', dot: 'bg-red-500' },
|
||||
// 默认类型
|
||||
default: { bg: 'bg-gray-100', text: 'text-gray-800', dot: 'bg-gray-500' },
|
||||
}
|
||||
|
||||
// 新增点 4: 定义脱敏函数
|
||||
function desen(value, type) {
|
||||
// 如果值是空,直接返回空字符串,让外层的 || '-' 生效
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
// 根据类型进行不同的替换
|
||||
switch (type) {
|
||||
case 'mobile': // 手机号:保留前3位和后4位,中间4位替换为 ****
|
||||
return value.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2')
|
||||
case 'id_card':// 身份证:保留前6位和后1位,中间替换为 ********
|
||||
if (value.length <= 7)
|
||||
return value
|
||||
return `${value.slice(0, 6)}********${value.slice(-1)}`
|
||||
default:
|
||||
return value // 其他类型不处理
|
||||
}
|
||||
}
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const data = ref({
|
||||
total: 0,
|
||||
list: [],
|
||||
})
|
||||
const loading = ref(false)
|
||||
|
||||
// 获取颜色样式
|
||||
function getReportTypeStyle(name) {
|
||||
const color = typeColors[name] || typeColors.default
|
||||
return `${color.bg} ${color.text}`
|
||||
}
|
||||
|
||||
// 获取小圆点颜色
|
||||
function getDotColor(name) {
|
||||
return (typeColors[name] || typeColors.default).dot
|
||||
}
|
||||
|
||||
// 获取金额颜色
|
||||
function getAmountColor(item) {
|
||||
// 如果净佣金为0或状态为已退款,显示红色
|
||||
if (item.net_amount <= 0 || item.status === 2) {
|
||||
return 'text-red-500'
|
||||
}
|
||||
// 如果有部分退款,显示橙色
|
||||
if (item.refunded_amount > 0) {
|
||||
return 'text-orange-500'
|
||||
}
|
||||
// 正常情况显示绿色
|
||||
return 'text-green-500'
|
||||
}
|
||||
|
||||
// 获取金额前缀(+ 或 -)
|
||||
function getAmountPrefix(item) {
|
||||
if (item.net_amount <= 0 || item.status === 2) {
|
||||
return '-'
|
||||
}
|
||||
return '+'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(item) {
|
||||
if (item.status === 2 || item.net_amount <= 0) {
|
||||
return '已退款'
|
||||
}
|
||||
if (item.status === 1) {
|
||||
// 冻结中
|
||||
if (item.refunded_amount > 0) {
|
||||
return '冻结中(部分退款)'
|
||||
}
|
||||
return '冻结中'
|
||||
}
|
||||
if (item.status === 0) {
|
||||
// 已结算
|
||||
if (item.refunded_amount > 0) {
|
||||
return '已结算(部分退款)'
|
||||
}
|
||||
return '已结算'
|
||||
}
|
||||
return '未知状态'
|
||||
}
|
||||
|
||||
// 获取状态样式
|
||||
function getStatusStyle(item) {
|
||||
if (item.status === 2 || item.net_amount <= 0) {
|
||||
return 'bg-red-100 text-red-800'
|
||||
}
|
||||
if (item.status === 1) {
|
||||
// 冻结中
|
||||
if (item.refunded_amount > 0) {
|
||||
return 'bg-orange-100 text-orange-800'
|
||||
}
|
||||
return 'bg-yellow-100 text-yellow-800'
|
||||
}
|
||||
if (item.status === 0) {
|
||||
// 已结算
|
||||
if (item.refunded_amount > 0) {
|
||||
return 'bg-blue-100 text-blue-800'
|
||||
}
|
||||
return 'bg-green-100 text-green-800'
|
||||
}
|
||||
return 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
async function getData() {
|
||||
try {
|
||||
loading.value = true
|
||||
const { data: res, error } = await useApiFetch(
|
||||
`/agent/commission?page=${page.value}&page_size=${pageSize.value}`,
|
||||
).get().json()
|
||||
|
||||
if (res.value?.code === 200 && !error.value) {
|
||||
data.value = res.value.data
|
||||
}
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onPageChange({ value }) {
|
||||
page.value = value
|
||||
getData()
|
||||
}
|
||||
|
||||
// 初始化加载
|
||||
onMounted(() => {
|
||||
getData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50">
|
||||
<view class="detail-scroll">
|
||||
<view v-for="(item, index) in data.list" :key="index" class="mx-4 my-2 rounded-lg bg-white p-4 shadow-sm">
|
||||
<view class="mb-2 flex items-center justify-between">
|
||||
<!-- 修改点 1: 使用 desen 函数处理 mobile -->
|
||||
<text class="text-sm text-gray-500">
|
||||
{{ desen(item.query_params?.mobile, 'mobile') || '-' }}
|
||||
</text>
|
||||
<text
|
||||
class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium"
|
||||
:class="getReportTypeStyle(item.product_name)"
|
||||
>
|
||||
<text class="mr-1 h-2 w-2 rounded-full" :class="getDotColor(item.product_name)" />
|
||||
{{ item.product_name }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="mb-2 flex items-center justify-between">
|
||||
<text class="text-gray-700 font-medium">
|
||||
直接收益
|
||||
</text>
|
||||
<view class="flex flex-col items-end">
|
||||
<!-- 主金额:显示净佣金 -->
|
||||
<text :class="getAmountColor(item)" class="text-lg font-bold">
|
||||
{{ getAmountPrefix(item) }}{{ (item.net_amount || 0).toFixed(2) }}
|
||||
</text>
|
||||
<!-- 如果有部分退款,显示原始金额和已退金额 -->
|
||||
<text v-if="item.refunded_amount > 0 && item.net_amount > 0" class="mt-1 text-xs text-gray-400">
|
||||
原始 {{ item.amount.toFixed(2) }},已退 {{ item.refunded_amount.toFixed(2) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-sm text-gray-500">
|
||||
{{ desen(item.query_params?.name) || '-' }}
|
||||
</text>
|
||||
<!-- 状态标签 -->
|
||||
<text
|
||||
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
|
||||
:class="getStatusStyle(item)"
|
||||
>
|
||||
{{ getStatusText(item) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="mb-2 flex items-center justify-between">
|
||||
<text class="text-sm text-gray-500">
|
||||
{{ item.create_time || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="mt-2 flex items-center">
|
||||
<text class="text-sm text-gray-500">
|
||||
订单号:
|
||||
</text>
|
||||
<text class="text-sm text-gray-700 font-mono">
|
||||
{{ item.order_id || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading" class="py-4 text-center text-sm text-gray-400">
|
||||
加载中...
|
||||
</view>
|
||||
<view v-else-if="!data.list.length" class="py-4 text-center text-sm text-gray-400">
|
||||
暂无记录
|
||||
</view>
|
||||
</view>
|
||||
<wd-pagination
|
||||
v-model="page"
|
||||
:total="data.total"
|
||||
:page-size="pageSize"
|
||||
show-icon
|
||||
show-message
|
||||
@change="onPageChange"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 列表项入场动画 */
|
||||
.list-enter-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
.detail-scroll {
|
||||
height: calc(100vh - 110px);
|
||||
}
|
||||
</style>
|
||||
133
src/pages/agent-rewards-details.vue
Normal file
133
src/pages/agent-rewards-details.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<script setup>
|
||||
definePage({ layout: 'default', auth: true })
|
||||
// 类型映射配置
|
||||
const typeConfig = {
|
||||
descendant_promotion: {
|
||||
chinese: '下级推广奖励',
|
||||
color: { bg: 'bg-blue-100', text: 'text-blue-800', dot: 'bg-blue-500' },
|
||||
},
|
||||
descendant_upgrade_vip: {
|
||||
chinese: '下级升级VIP奖励',
|
||||
color: { bg: 'bg-green-100', text: 'text-green-800', dot: 'bg-green-500' },
|
||||
},
|
||||
descendant_upgrade_svip: {
|
||||
chinese: '下级升级SVIP奖励',
|
||||
color: { bg: 'bg-purple-100', text: 'text-purple-800', dot: 'bg-purple-500' },
|
||||
},
|
||||
descendant_withdraw: {
|
||||
chinese: '下级提现奖励',
|
||||
color: { bg: 'bg-indigo-100', text: 'text-indigo-800', dot: 'bg-indigo-500' },
|
||||
},
|
||||
default: {
|
||||
chinese: '其他奖励',
|
||||
color: { bg: 'bg-gray-100', text: 'text-gray-800', dot: 'bg-gray-500' },
|
||||
},
|
||||
}
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const data = ref({
|
||||
total: 0,
|
||||
list: [],
|
||||
})
|
||||
const loading = ref(false)
|
||||
|
||||
// 类型转中文
|
||||
function typeToChinese(type) {
|
||||
return typeConfig[type]?.chinese || typeConfig.default.chinese
|
||||
}
|
||||
|
||||
// 获取颜色样式
|
||||
function getReportTypeStyle(type) {
|
||||
const config = typeConfig[type] || typeConfig.default
|
||||
return `${config.color.bg} ${config.color.text}`
|
||||
}
|
||||
|
||||
// 获取小圆点颜色
|
||||
function getDotColor(type) {
|
||||
return typeConfig[type]?.color.dot || typeConfig.default.color.dot
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
async function getData() {
|
||||
try {
|
||||
loading.value = true
|
||||
const { data: res, error } = await useApiFetch(
|
||||
`/agent/rewards?page=${page.value}&page_size=${pageSize.value}`,
|
||||
).get().json()
|
||||
|
||||
if (res.value?.code === 200 && !error.value) {
|
||||
data.value = res.value.data
|
||||
}
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onPageChange({ value }) {
|
||||
page.value = value
|
||||
getData()
|
||||
}
|
||||
|
||||
// 初始化加载
|
||||
onMounted(() => {
|
||||
getData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50">
|
||||
<view class="reward-scroll">
|
||||
<view v-for="(item, index) in data.list" :key="index" class="mx-4 my-2 rounded-lg bg-white p-4 shadow-sm">
|
||||
<view class="mb-2 flex items-center justify-between">
|
||||
<text class="text-sm text-gray-500">
|
||||
{{ item.create_time || '-' }}
|
||||
</text>
|
||||
<text class="text-green-500 font-bold">
|
||||
+{{ item.amount.toFixed(2) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center">
|
||||
<text
|
||||
class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium"
|
||||
:class="getReportTypeStyle(item.type)"
|
||||
>
|
||||
<text class="mr-1 h-2 w-2 rounded-full" :class="getDotColor(item.type)" />
|
||||
{{ typeToChinese(item.type) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading" class="py-4 text-center text-sm text-gray-400">
|
||||
加载中...
|
||||
</view>
|
||||
<view v-else-if="!data.list.length" class="py-4 text-center text-sm text-gray-400">
|
||||
暂无记录
|
||||
</view>
|
||||
</view>
|
||||
<wd-pagination
|
||||
v-model="page"
|
||||
:total="data.total"
|
||||
:page-size="pageSize"
|
||||
show-icon
|
||||
show-message
|
||||
@change="onPageChange"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 保持原有样式不变 */
|
||||
.list-enter-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
.reward-scroll {
|
||||
height: calc(100vh - 110px);
|
||||
}
|
||||
</style>
|
||||
15
src/pages/agent-service-agreement.vue
Normal file
15
src/pages/agent-service-agreement.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { useReportWebview } from '@/composables/useReportWebview'
|
||||
definePage({ layout: false })
|
||||
|
||||
const { buildSitePathUrl } = useReportWebview()
|
||||
const src = ref('')
|
||||
|
||||
onLoad(() => {
|
||||
src.value = buildSitePathUrl('/app/agentSerivceAgreement')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<web-view :src="src" />
|
||||
</template>
|
||||
1099
src/pages/agent-vip-apply.vue
Normal file
1099
src/pages/agent-vip-apply.vue
Normal file
File diff suppressed because it is too large
Load Diff
669
src/pages/agent-vip-config.vue
Normal file
669
src/pages/agent-vip-config.vue
Normal file
@@ -0,0 +1,669 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
definePage({ layout: 'default', auth: true })
|
||||
|
||||
// 报告类型选项
|
||||
const reportOptions = [
|
||||
{ text: '小微企业', value: 'companyinfo', id: 2 },
|
||||
{ text: '贷前风险', value: 'preloanbackgroundcheck', id: 5 },
|
||||
{ text: '个人大数据', value: 'personaldata', id: 27 },
|
||||
{ text: '入职风险', value: 'backgroundcheck', id: 1 },
|
||||
{ text: '家政风险', value: 'homeservice', id: 3 },
|
||||
{ text: '婚恋风险', value: 'marriage', id: 4 },
|
||||
// { text: "租赁风险", value: "rentalrisk", id: 6 },
|
||||
// { text: "个人风险", value: "riskassessment", id: 7 },
|
||||
]
|
||||
|
||||
// 状态管理
|
||||
const showPicker = ref(false)
|
||||
const selectedReport = ref(reportOptions[0])
|
||||
const selectedReportText = ref(reportOptions[0].text)
|
||||
const selectedReportId = ref(reportOptions[0].id)
|
||||
|
||||
const configData = ref({})
|
||||
const productConfigData = ref({})
|
||||
const priceIncreaseMax = ref(null)
|
||||
const priceIncreaseAmountMax = ref(null)
|
||||
const priceRatioMax = ref(null)
|
||||
const rangeError = ref(false)
|
||||
const ratioError = ref(false)
|
||||
const increaseError = ref(false)
|
||||
|
||||
function showToast(message) {
|
||||
if (!message)
|
||||
return
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
|
||||
// 金额输入格式验证:确保最多两位小数
|
||||
function validateDecimal(field) {
|
||||
const value = configData.value[field]
|
||||
if (value === null || value === undefined)
|
||||
return
|
||||
|
||||
const numValue = Number(value)
|
||||
if (isNaN(numValue)) {
|
||||
configData.value[field] = null
|
||||
return
|
||||
}
|
||||
const fixedValue = Number.parseFloat(numValue.toFixed(2))
|
||||
configData.value[field] = fixedValue
|
||||
|
||||
if (field === 'price_increase_amount') {
|
||||
if (fixedValue > priceIncreaseAmountMax.value) {
|
||||
configData.value[field] = priceIncreaseAmountMax.value
|
||||
showToast(`加价金额最大为${priceIncreaseAmountMax.value}元`)
|
||||
increaseError.value = true
|
||||
setTimeout(() => {
|
||||
increaseError.value = false
|
||||
}, 2000)
|
||||
}
|
||||
else {
|
||||
increaseError.value = false
|
||||
}
|
||||
// 当加价金额改变后,重新验证价格区间
|
||||
validateRange()
|
||||
}
|
||||
}
|
||||
|
||||
// 价格区间验证(在 @blur 中调用)
|
||||
function validateRange() {
|
||||
console.log(
|
||||
'configData.value.price_range_from',
|
||||
configData.value.price_range_from,
|
||||
)
|
||||
console.log(
|
||||
'configData.value.price_range_to',
|
||||
configData.value.price_range_to,
|
||||
)
|
||||
|
||||
if (
|
||||
configData.value.price_range_from === null
|
||||
|| configData.value.price_range_to === null
|
||||
) {
|
||||
rangeError.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
isNaN(configData.value.price_range_from)
|
||||
|| isNaN(configData.value.price_range_to)
|
||||
) {
|
||||
return
|
||||
}
|
||||
const additional = configData.value.price_increase_amount || 0
|
||||
const minAllowed = Number.parseFloat(
|
||||
(
|
||||
Number(productConfigData.value.cost_price) + Number(additional)
|
||||
).toFixed(2),
|
||||
) // 使用成本价作为最小值
|
||||
const maxAllowed = productConfigData.value.price_range_max // 使用产品配置中的最大价格作为最大值
|
||||
if (configData.value.price_range_from < minAllowed) {
|
||||
configData.value.price_range_from = minAllowed
|
||||
showToast(`最低金额不能低于成本价 ${minAllowed}元`)
|
||||
rangeError.value = true
|
||||
closeRangeError()
|
||||
|
||||
configData.value.price_range_to = Number.parseFloat(
|
||||
(
|
||||
Number(configData.value.price_range_from)
|
||||
+ Number(priceIncreaseMax.value)
|
||||
).toFixed(2),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (configData.value.price_range_to < configData.value.price_range_from) {
|
||||
showToast('最高金额不能低于最低金额')
|
||||
if (
|
||||
configData.value.price_range_from + priceIncreaseMax.value
|
||||
> maxAllowed
|
||||
) {
|
||||
configData.value.price_range_to = maxAllowed
|
||||
}
|
||||
else {
|
||||
configData.value.price_range_to
|
||||
= configData.value.price_range_from + priceIncreaseMax.value
|
||||
}
|
||||
rangeError.value = true
|
||||
closeRangeError()
|
||||
return
|
||||
}
|
||||
const diff = Number.parseFloat(
|
||||
(
|
||||
configData.value.price_range_to - configData.value.price_range_from
|
||||
).toFixed(2),
|
||||
)
|
||||
if (diff > priceIncreaseMax.value) {
|
||||
showToast(`价格区间最大差值为${priceIncreaseMax.value}元`)
|
||||
configData.value.price_range_to
|
||||
= configData.value.price_range_from + priceIncreaseMax.value
|
||||
closeRangeError()
|
||||
return
|
||||
}
|
||||
|
||||
if (configData.value.price_range_to > maxAllowed) {
|
||||
configData.value.price_range_to = maxAllowed
|
||||
showToast(`最高金额不能超过 ${maxAllowed}元`)
|
||||
closeRangeError()
|
||||
}
|
||||
|
||||
if (!rangeError.value) {
|
||||
rangeError.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 收取比例验证(修改为保留两位小数,不再四舍五入取整)
|
||||
function validateRatio() {
|
||||
const value = configData.value.price_ratio
|
||||
if (value === null || value === undefined)
|
||||
return
|
||||
|
||||
const numValue = Number(value)
|
||||
if (isNaN(numValue)) {
|
||||
configData.value.price_ratio = null
|
||||
ratioError.value = true
|
||||
return
|
||||
}
|
||||
|
||||
if (numValue > priceRatioMax.value) {
|
||||
configData.value.price_ratio = priceRatioMax.value
|
||||
showToast(`收取比例最大为${priceRatioMax.value}%`)
|
||||
ratioError.value = true
|
||||
setTimeout(() => {
|
||||
ratioError.value = false
|
||||
}, 1000)
|
||||
}
|
||||
else if (numValue < 0) {
|
||||
configData.value.price_ratio = 0
|
||||
ratioError.value = true
|
||||
}
|
||||
else {
|
||||
configData.value.price_ratio = Number.parseFloat(numValue.toFixed(2))
|
||||
ratioError.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
async function getConfig() {
|
||||
try {
|
||||
const { data, error } = await useApiFetch(
|
||||
`/agent/membership/user_config?product_id=${selectedReportId.value}`,
|
||||
)
|
||||
.get()
|
||||
.json()
|
||||
|
||||
if (data.value?.code === 200) {
|
||||
const respConfigData = data.value.data.agent_membership_user_config
|
||||
configData.value = {
|
||||
id: respConfigData.product_id,
|
||||
price_range_from: respConfigData.price_range_from || null,
|
||||
price_range_to: respConfigData.price_range_to || null,
|
||||
price_ratio: respConfigData.price_ratio * 100 || null, // 转换为百分比
|
||||
price_increase_amount:
|
||||
respConfigData.price_increase_amount || null,
|
||||
}
|
||||
console.log('configData', configData.value)
|
||||
// const respProductConfigData = data.value.data.product_config
|
||||
productConfigData.value = data.value.data.product_config
|
||||
// 设置动态限制值
|
||||
priceIncreaseMax.value = data.value.data.price_increase_max
|
||||
priceIncreaseAmountMax.value
|
||||
= data.value.data.price_increase_amount
|
||||
priceRatioMax.value = data.value.data.price_ratio * 100
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
showToast('配置加载失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 提交处理
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
if (!finalValidation()) {
|
||||
return
|
||||
}
|
||||
// 前端数据转换
|
||||
const submitData = {
|
||||
product_id: configData.value.id,
|
||||
price_range_from: configData.value.price_range_from || 0,
|
||||
price_range_to: configData.value.price_range_to || 0,
|
||||
price_ratio: (configData.value.price_ratio || 0) / 100, // 转换为小数
|
||||
price_increase_amount: configData.value.price_increase_amount || 0,
|
||||
}
|
||||
console.log('submitData', submitData)
|
||||
const { data, error } = await useApiFetch(
|
||||
'/agent/membership/save_user_config',
|
||||
)
|
||||
.post(submitData)
|
||||
.json()
|
||||
|
||||
if (data.value?.code === 200) {
|
||||
setTimeout(() => {
|
||||
showToast('保存成功')
|
||||
}, 500)
|
||||
getConfig()
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
showToast('保存失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
// 最终验证函数
|
||||
function finalValidation() {
|
||||
// 校验最低金额:不能为空且大于0
|
||||
if (
|
||||
!configData.value.price_range_from
|
||||
|| configData.value.price_range_from <= 0
|
||||
) {
|
||||
showToast('最低金额不能为空')
|
||||
return false
|
||||
}
|
||||
// 校验最高金额:不能为空且大于0
|
||||
if (
|
||||
!configData.value.price_range_to
|
||||
|| configData.value.price_range_to <= 0
|
||||
) {
|
||||
showToast('最高金额不能为空')
|
||||
return false
|
||||
}
|
||||
// 校验收取比例:不能为空且大于0
|
||||
if (!configData.value.price_ratio || configData.value.price_ratio <= 0) {
|
||||
showToast('收取比例不能为空')
|
||||
return false
|
||||
}
|
||||
// 验证最低金额必须小于最高金额
|
||||
if (configData.value.price_range_from >= configData.value.price_range_to) {
|
||||
showToast('最低金额必须小于最高金额')
|
||||
return false
|
||||
}
|
||||
// 验证价格区间差值不能超过最大允许差值
|
||||
const finalDiff = Number.parseFloat(
|
||||
(
|
||||
configData.value.price_range_to - configData.value.price_range_from
|
||||
).toFixed(2),
|
||||
)
|
||||
if (finalDiff > priceIncreaseMax.value) {
|
||||
showToast(`价格区间最大差值为${priceIncreaseMax.value}元`)
|
||||
return false
|
||||
}
|
||||
// 验证最高金额不能超过产品配置中设定的上限
|
||||
if (
|
||||
configData.value.price_range_to
|
||||
> productConfigData.value.price_range_max
|
||||
) {
|
||||
showToast(
|
||||
`最高金额不能超过${productConfigData.value.price_range_max}元`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
// 验证最低金额不能低于成本价+加价金额(加价金额允许为空)
|
||||
const additional = configData.value.price_increase_amount || 0
|
||||
if (
|
||||
configData.value.price_range_from
|
||||
< productConfigData.value.cost_price + additional
|
||||
) {
|
||||
showToast(
|
||||
`最低金额不能低于成本价${productConfigData.value.cost_price + additional
|
||||
}元`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 选择器确认
|
||||
function onSelectReport(option) {
|
||||
selectedReport.value = option
|
||||
selectedReportText.value = option.text
|
||||
selectedReportId.value = option.id
|
||||
showPicker.value = false
|
||||
// 重置错误状态
|
||||
rangeError.value = false
|
||||
ratioError.value = false
|
||||
increaseError.value = false
|
||||
|
||||
getConfig()
|
||||
}
|
||||
function closeRangeError() {
|
||||
setTimeout(() => {
|
||||
rangeError.value = false
|
||||
}, 2000)
|
||||
}
|
||||
onMounted(() => {
|
||||
getConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-3xl min-h-screen p-4">
|
||||
<!-- 标题部分 -->
|
||||
<div class="card mb-4 rounded-lg from-blue-500 to-blue-600 bg-gradient-to-r p-4 text-white shadow-lg">
|
||||
<h1 class="mb-2 text-2xl font-extrabold">
|
||||
专业报告定价配置
|
||||
</h1>
|
||||
<p class="opacity-90">
|
||||
请选择报告类型并设置定价策略,助您实现精准定价
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<view class="card selector" @click="showPicker = true">
|
||||
<view class="selector-label">
|
||||
📝 选择报告
|
||||
</view>
|
||||
<view class="selector-value">
|
||||
{{ selectedReportText }}
|
||||
</view>
|
||||
</view>
|
||||
<wd-popup v-model="showPicker" position="bottom" custom-style="padding: 16px;">
|
||||
<view class="popup-title">
|
||||
选择报告类型
|
||||
</view>
|
||||
<view class="report-list">
|
||||
<view
|
||||
v-for="item in reportOptions"
|
||||
:key="item.value"
|
||||
class="report-item"
|
||||
:class="{ active: selectedReportId === item.id }"
|
||||
@click="onSelectReport(item)"
|
||||
>
|
||||
{{ item.text }}
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedReportText" class="space-y-6">
|
||||
<!-- 配置卡片 -->
|
||||
<div class="card">
|
||||
<!-- 当前报告标题 -->
|
||||
<div class="mb-6 flex items-center">
|
||||
<h2 class="text-xl text-gray-800 font-semibold">
|
||||
{{ selectedReportText }}配置
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- 显示当前产品的基础成本信息 -->
|
||||
<div
|
||||
v-if="productConfigData && productConfigData.cost_price"
|
||||
class="mb-4 border border-gray-200 rounded-lg bg-gray-50 px-4 py-2 shadow-sm"
|
||||
>
|
||||
<div class="text-lg text-gray-700 font-semibold">
|
||||
报告基础配置信息
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-gray-600">
|
||||
<div>
|
||||
基础成本价:<span class="font-medium">{{
|
||||
productConfigData.cost_price
|
||||
}}</span>
|
||||
元
|
||||
</div>
|
||||
<!-- <div>区间起始价:<span class="font-medium">{{ productConfigData.price_range_min }}</span> 元</div> -->
|
||||
<div>
|
||||
最高设定金额上限:<span class="font-medium">{{
|
||||
productConfigData.price_range_max
|
||||
}}</span>
|
||||
元
|
||||
</div>
|
||||
<div>
|
||||
最高设定比例上限:<span class="font-medium">{{
|
||||
priceRatioMax
|
||||
}}</span>
|
||||
%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="section-divider my-6">
|
||||
成本策略配置
|
||||
</div>
|
||||
|
||||
<!-- 加价金额 -->
|
||||
<view class="custom-field" :class="{ 'field-error': increaseError }">
|
||||
<text class="field-label">
|
||||
🚀 加价金额
|
||||
</text>
|
||||
<view class="field-input-wrap">
|
||||
<input
|
||||
v-model.number="configData.price_increase_amount"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
class="field-input"
|
||||
@blur="validateDecimal('price_increase_amount')"
|
||||
>
|
||||
<text class="field-unit">
|
||||
元
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<div class="mt-1 text-xs text-gray-400">
|
||||
提示:最大加价金额为{{ priceIncreaseAmountMax }}元<br>
|
||||
说明:加价金额是在基础成本价上增加的额外费用,决定下级报告的最低定价,您将获得所有输入的金额利润。
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="section-divider my-6">
|
||||
定价策略配置
|
||||
</div>
|
||||
|
||||
<!-- 定价区间最低 -->
|
||||
<view class="custom-field" :class="{ 'field-error': rangeError }">
|
||||
<text class="field-label">
|
||||
💰 最低金额
|
||||
</text>
|
||||
<view class="field-input-wrap">
|
||||
<input
|
||||
v-model.number="configData.price_range_from"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
class="field-input"
|
||||
@blur="
|
||||
() => {
|
||||
validateDecimal('price_range_from')
|
||||
validateRange()
|
||||
}
|
||||
"
|
||||
>
|
||||
<text class="field-unit">
|
||||
元
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<div class="mt-1 text-xs text-gray-400">
|
||||
提示:最低金额不能低于(基础最低
|
||||
{{ productConfigData?.price_range_min || 0 }}元 +
|
||||
加价金额)<br>
|
||||
说明:设定的最低金额为定价区间的起始值,若下级设定的报告金额在区间内,则区间内部分将按比例获得收益。
|
||||
</div>
|
||||
|
||||
<!-- 定价区间最高 -->
|
||||
<view class="custom-field" :class="{ 'field-error': rangeError }">
|
||||
<text class="field-label">
|
||||
💰 最高金额
|
||||
</text>
|
||||
<view class="field-input-wrap">
|
||||
<input
|
||||
v-model.number="configData.price_range_to"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
class="field-input"
|
||||
@blur="
|
||||
() => {
|
||||
validateDecimal('price_range_to')
|
||||
validateRange()
|
||||
}
|
||||
"
|
||||
>
|
||||
<text class="field-unit">
|
||||
元
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<div class="mt-1 text-xs text-gray-400">
|
||||
提示:最高金额不能超过上限({{
|
||||
productConfigData?.price_range_max || 0
|
||||
}}元)和大于最低金额({{ priceIncreaseMax }}元)<br>
|
||||
说明:设定的最高金额为定价区间的结束值,若下级设定的报告金额在区间内,则区间内部分将按比例获得收益。
|
||||
</div>
|
||||
|
||||
<!-- 收取比例 -->
|
||||
<view class="custom-field" :class="{ 'field-error': ratioError }">
|
||||
<text class="field-label">
|
||||
📈 收取比例
|
||||
</text>
|
||||
<view class="field-input-wrap">
|
||||
<input
|
||||
v-model.number="configData.price_ratio"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
class="field-input"
|
||||
@blur="validateRatio()"
|
||||
>
|
||||
<text class="field-unit">
|
||||
%
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<div class="mt-1 text-xs text-gray-400">
|
||||
提示:最大收取比例为{{ priceRatioMax }}%<br>
|
||||
说明:收取比例表示对定价区间内(即报告金额超过最低金额,小于最高金额的部分)的金额,按此比例进行利润分成。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<wd-button
|
||||
type="primary" block
|
||||
class="h-12 rounded-xl from-blue-500 to-blue-600 bg-gradient-to-r text-white shadow-lg hover:from-blue-600 hover:to-blue-700"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存当前报告配置
|
||||
</wd-button>
|
||||
</div>
|
||||
<!-- 未选择提示 -->
|
||||
<div v-else class="py-12 text-center">
|
||||
<text class="mb-4 block text-4xl text-gray-400">
|
||||
⚠️
|
||||
</text>
|
||||
<p class="text-gray-500">
|
||||
请先选择需要配置的报告类型
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selector-label {
|
||||
color: #2563eb;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.selector-value {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.report-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.report-item {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.report-item.active {
|
||||
border-color: #2563eb;
|
||||
color: #2563eb;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.section-divider::before,
|
||||
.section-divider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 30%;
|
||||
height: 1px;
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.section-divider::before {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.section-divider::after {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.custom-field {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #4b5563;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.field-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.field-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.field-unit {
|
||||
margin-left: 8px;
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.custom-field:focus-within .field-input-wrap {
|
||||
border-color: #bfdbfe;
|
||||
}
|
||||
|
||||
.field-error .field-input-wrap {
|
||||
border-color: #fca5a5;
|
||||
background: #fef2f2;
|
||||
}
|
||||
</style>
|
||||
36
src/pages/agent-vip.vue
Normal file
36
src/pages/agent-vip.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup>
|
||||
import { openCustomerService } from '@/composables/useCustomerService'
|
||||
|
||||
definePage({ layout: 'default', auth: true })
|
||||
function toVipApply() {
|
||||
uni.navigateTo({ url: '/pages/agent-vip-apply' })
|
||||
}
|
||||
|
||||
function toService() {
|
||||
openCustomerService()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="relative">
|
||||
<image class="block w-full" src="/static/images/vip_bg.png" mode="widthFix" />
|
||||
<view
|
||||
class="absolute bottom-80 left-[50%] flex flex-col translate-x-[-50%] items-center gap-4"
|
||||
>
|
||||
<view
|
||||
class="rounded-lg from-amber-500 to-amber-600 bg-gradient-to-r px-6 py-2 text-[24px] text-white font-bold shadow-[0_0_15px_rgba(255,255,255,0.3)] transition-transform active:scale-105"
|
||||
@click="toVipApply"
|
||||
>
|
||||
<text>申请VIP代理</text>
|
||||
</view>
|
||||
<view
|
||||
class="rounded-lg from-gray-900 via-black to-gray-900 bg-gradient-to-r px-4 py-2 text-[20px] text-white font-bold shadow-[0_0_15px_rgba(255,255,255,0.3)] transition-transform active:scale-105"
|
||||
@click="toService"
|
||||
>
|
||||
<text>联系客服</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
232
src/pages/agent.vue
Normal file
232
src/pages/agent.vue
Normal file
@@ -0,0 +1,232 @@
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
definePage({ layout: 'home' })
|
||||
|
||||
const agentStore = useAgentStore()
|
||||
const { isAgent } = storeToRefs(agentStore)
|
||||
const data = ref(null)
|
||||
|
||||
const dateRangeMap = {
|
||||
today: 'today',
|
||||
week: 'last7d',
|
||||
month: 'last30d',
|
||||
}
|
||||
|
||||
const dateTextMap = {
|
||||
today: '今日',
|
||||
week: '近7天',
|
||||
month: '近1月',
|
||||
}
|
||||
|
||||
const promoteDateOptions = [
|
||||
{ label: '今日', value: 'today' },
|
||||
{ label: '近7天', value: 'week' },
|
||||
{ label: '近1月', value: 'month' },
|
||||
]
|
||||
const selectedPromoteDate = ref('today')
|
||||
|
||||
const teamDateOptions = [
|
||||
{ label: '今日', value: 'today' },
|
||||
{ label: '近7天', value: 'week' },
|
||||
{ label: '近1月', value: 'month' },
|
||||
]
|
||||
const selectedTeamDate = ref('today')
|
||||
|
||||
const currentPromoteData = computed(() => {
|
||||
const range = dateRangeMap[selectedPromoteDate.value]
|
||||
return data.value?.direct_push?.[range] || { commission: 0, report: 0 }
|
||||
})
|
||||
|
||||
const currentTeamData = computed(() => {
|
||||
const range = dateRangeMap[selectedTeamDate.value]
|
||||
return data.value?.active_reward?.[range] || {
|
||||
sub_promote_reward: 0,
|
||||
sub_upgrade_reward: 0,
|
||||
sub_withdraw_reward: 0,
|
||||
}
|
||||
})
|
||||
|
||||
const promoteTimeText = computed(() => dateTextMap[selectedPromoteDate.value] || '今日')
|
||||
const teamTimeText = computed(() => dateTextMap[selectedTeamDate.value] || '今日')
|
||||
|
||||
async function getData() {
|
||||
try {
|
||||
const { data: res, error } = await useApiFetch('/agent/revenue').get().json()
|
||||
if (res.value?.code === 200 && !error.value) {
|
||||
data.value = res.value.data
|
||||
}
|
||||
}
|
||||
catch {
|
||||
uni.showToast({ title: '网络错误', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
if (isAgent.value) {
|
||||
getData()
|
||||
}
|
||||
})
|
||||
|
||||
function goToPromoteDetail() {
|
||||
uni.navigateTo({ url: '/pages/agent-promote-details' })
|
||||
}
|
||||
|
||||
function goToRewardsDetail() {
|
||||
uni.navigateTo({ url: '/pages/agent-rewards-details' })
|
||||
}
|
||||
|
||||
function toWithdraw() {
|
||||
uni.navigateTo({ url: '/pages/withdraw' })
|
||||
}
|
||||
|
||||
function toWithdrawDetails() {
|
||||
uni.navigateTo({ url: '/pages/withdraw-details' })
|
||||
}
|
||||
|
||||
function toSubordinateList() {
|
||||
uni.navigateTo({ url: '/pages/subordinate-list' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="safe-area-top min-h-screen p-4">
|
||||
<view class="mb-4 rounded-xl from-blue-50/70 to-blue-100/50 bg-gradient-to-r p-6 shadow-lg">
|
||||
<view class="mb-3 flex items-center justify-between">
|
||||
<text class="text-lg text-gray-800 font-bold">
|
||||
余额
|
||||
</text>
|
||||
<text class="text-3xl text-blue-600 font-bold">
|
||||
¥ {{ (data?.balance || 0).toFixed(2) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="mb-2 text-sm text-gray-500">
|
||||
累计收益:¥ {{ (data?.total_earnings || 0).toFixed(2) }}
|
||||
</view>
|
||||
<view class="mb-1 text-sm text-gray-500">
|
||||
待结账金额:¥ {{ (data?.frozen_balance || 0).toFixed(2) }}
|
||||
</view>
|
||||
<view class="mb-6 text-xs text-gray-400">
|
||||
待结账金额将在订单创建24小时后自动结账
|
||||
</view>
|
||||
<view class="grid grid-cols-2 gap-3">
|
||||
<wd-button type="primary" block @click="toWithdraw">
|
||||
提现
|
||||
</wd-button>
|
||||
<wd-button plain block @click="toWithdrawDetails">
|
||||
提现记录
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mb-4 rounded-xl from-blue-50/40 to-cyan-50/50 bg-gradient-to-r p-6 shadow-lg">
|
||||
<view class="mb-4 flex items-center justify-between">
|
||||
<text class="text-lg text-gray-800 font-bold">
|
||||
直推报告收益
|
||||
</text>
|
||||
<view class="text-right">
|
||||
<text class="text-2xl text-blue-600 font-bold">
|
||||
¥ {{ (data?.direct_push?.total_commission || 0).toFixed(2) }}
|
||||
</text>
|
||||
<view class="mt-1 text-sm text-gray-500">
|
||||
有效报告 {{ data?.direct_push?.total_report || 0 }} 份
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="grid grid-cols-3 mb-6 gap-2">
|
||||
<view
|
||||
v-for="item in promoteDateOptions"
|
||||
:key="item.value"
|
||||
class="rounded-full px-4 py-1 text-center text-sm transition-all"
|
||||
:class="selectedPromoteDate === item.value ? 'bg-blue-500 text-white shadow-md' : 'border border-gray-200/50 bg-white/90 text-gray-600'"
|
||||
@click="selectedPromoteDate = item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="grid grid-cols-2 mb-6 gap-4">
|
||||
<view class="rounded-lg bg-blue-50/60 p-3 backdrop-blur-sm">
|
||||
<view class="text-sm text-gray-500">
|
||||
{{ promoteTimeText }}收益
|
||||
</view>
|
||||
<text class="mt-1 text-xl text-blue-600 font-bold">
|
||||
¥ {{ currentPromoteData.commission?.toFixed(2) || '0.00' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="rounded-lg bg-blue-50/60 p-3 backdrop-blur-sm">
|
||||
<view class="text-sm text-gray-500">
|
||||
有效报告
|
||||
</view>
|
||||
<text class="mt-1 text-xl text-blue-600 font-bold">
|
||||
{{ currentPromoteData.report || 0 }} 份
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flex items-center justify-between text-sm text-blue-500 font-semibold" @click="goToPromoteDetail">
|
||||
<text>查看收益明细</text>
|
||||
<text class="text-lg">
|
||||
→
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="rounded-xl from-green-50/40 to-cyan-50/30 bg-gradient-to-r p-6 shadow-lg">
|
||||
<view class="mb-4">
|
||||
<text class="text-lg text-gray-800 font-bold">
|
||||
团队奖励
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="grid grid-cols-3 mb-6 gap-2">
|
||||
<view
|
||||
v-for="item in teamDateOptions"
|
||||
:key="item.value"
|
||||
class="rounded-full px-4 py-1 text-center text-sm transition-all"
|
||||
:class="selectedTeamDate === item.value ? 'bg-green-500 text-white shadow-md' : 'border border-gray-200/50 bg-white/90 text-gray-600'"
|
||||
@click="selectedTeamDate = item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="grid grid-cols-1 mb-6 gap-2">
|
||||
<view class="rounded-lg bg-green-50/60 p-3 backdrop-blur-sm">
|
||||
<view class="text-sm text-gray-500">
|
||||
{{ teamTimeText }}下级推广奖励
|
||||
</view>
|
||||
<text class="mt-1 text-xl text-green-600 font-bold">
|
||||
¥ {{ (currentTeamData.sub_promote_reward || 0).toFixed(2) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="rounded-lg bg-green-50/60 p-3 backdrop-blur-sm">
|
||||
<view class="text-sm text-gray-500">
|
||||
{{ teamTimeText }}下级转化奖励
|
||||
</view>
|
||||
<text class="mt-1 text-xl text-green-600 font-bold">
|
||||
¥ {{ (currentTeamData.sub_upgrade_reward || 0).toFixed(2) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="rounded-lg bg-green-50/60 p-3 backdrop-blur-sm">
|
||||
<view class="text-sm text-gray-500">
|
||||
{{ teamTimeText }}下级提现奖励
|
||||
</view>
|
||||
<text class="mt-1 text-xl text-green-600 font-bold">
|
||||
¥ {{ (currentTeamData.sub_withdraw_reward || 0).toFixed(2) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<wd-button plain block @click="goToRewardsDetail">
|
||||
团队奖励明细
|
||||
</wd-button>
|
||||
<wd-button type="success" block @click="toSubordinateList">
|
||||
查看我的下级
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
18
src/pages/authorization.vue
Normal file
18
src/pages/authorization.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { useReportWebview } from '@/composables/useReportWebview'
|
||||
definePage({ layout: false })
|
||||
|
||||
const { buildSitePathUrl } = useReportWebview()
|
||||
const src = ref('')
|
||||
|
||||
onLoad((query) => {
|
||||
const q = query || {}
|
||||
src.value = buildSitePathUrl('/app/authorization', {
|
||||
id: String(q.id || ''),
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<web-view :src="src" />
|
||||
</template>
|
||||
472
src/pages/cancel-account.vue
Normal file
472
src/pages/cancel-account.vue
Normal file
@@ -0,0 +1,472 @@
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, nextTick, onUnmounted, ref } from 'vue'
|
||||
import AccountCancelAgreement from '@/components/AccountCancelAgreement.vue'
|
||||
import useApiFetch from '@/composables/useApiFetch'
|
||||
import { useAgentStore } from '@/stores/agentStore'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import { clearAuthStorage } from '@/utils/storage'
|
||||
|
||||
definePage({ layout: false, auth: true })
|
||||
|
||||
const userStore = useUserStore()
|
||||
const agentStore = useAgentStore()
|
||||
const { level } = storeToRefs(agentStore)
|
||||
|
||||
const revenueData = ref(null)
|
||||
const loading = ref(true)
|
||||
|
||||
const showBalancePopup = ref(false)
|
||||
const showSmsPopup = ref(false)
|
||||
const cancelAccountCode = ref('')
|
||||
const verificationCodeInputRef = ref(null)
|
||||
|
||||
/** 与 login.vue / LoginDialog.vue 一致的倒计时逻辑 */
|
||||
const isCountingDown = ref(false)
|
||||
const countdown = ref(60)
|
||||
let smsTimer = null
|
||||
|
||||
function toast(title) {
|
||||
uni.showToast({ title, icon: 'none' })
|
||||
}
|
||||
|
||||
function maskName(name) {
|
||||
if (!name || name.length < 11)
|
||||
return name
|
||||
return `${name.substring(0, 3)}****${name.substring(7)}`
|
||||
}
|
||||
|
||||
const isPhoneNumberValid = computed(() => {
|
||||
return /^1[3-9]\d{9}$/.test(String(userStore.mobile || ''))
|
||||
})
|
||||
|
||||
const isAgent = computed(() => agentStore.isAgent)
|
||||
const hasWalletBalance = computed(() => {
|
||||
const b = Number(revenueData.value?.balance ?? 0)
|
||||
const f = Number(revenueData.value?.frozen_balance ?? 0)
|
||||
return b > 0 || f > 0
|
||||
})
|
||||
const showBalanceWarning = computed(() => isAgent.value && hasWalletBalance.value)
|
||||
|
||||
const showVipLevelReminder = computed(() =>
|
||||
isAgent.value && (level.value === 'VIP' || level.value === 'SVIP'),
|
||||
)
|
||||
const vipLevelLabel = computed(() => (level.value === 'SVIP' ? 'SVIP' : 'VIP'))
|
||||
|
||||
const showAnyReminder = computed(() => showBalanceWarning.value || showVipLevelReminder.value)
|
||||
|
||||
const canSubmitCancel = computed(() => cancelAccountCode.value.length === 6)
|
||||
|
||||
onLoad(async () => {
|
||||
await userStore.fetchUserInfo()
|
||||
if (!userStore.mobile) {
|
||||
loading.value = false
|
||||
toast('请先绑定手机号')
|
||||
setTimeout(() => uni.navigateBack(), 1500)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await agentStore.fetchAgentStatus()
|
||||
if (agentStore.isAgent) {
|
||||
const { data, error } = await useApiFetch('/agent/revenue').get().json()
|
||||
if (!error.value && data.value?.code === 200)
|
||||
revenueData.value = data.value.data
|
||||
}
|
||||
}
|
||||
catch {
|
||||
/* ignore */
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
function onExit() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
if (smsTimer) {
|
||||
clearInterval(smsTimer)
|
||||
}
|
||||
isCountingDown.value = true
|
||||
countdown.value = 60
|
||||
smsTimer = setInterval(() => {
|
||||
if (countdown.value > 0) {
|
||||
countdown.value--
|
||||
}
|
||||
else {
|
||||
clearInterval(smsTimer)
|
||||
smsTimer = null
|
||||
isCountingDown.value = false
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function resetCountdown() {
|
||||
if (smsTimer) {
|
||||
clearInterval(smsTimer)
|
||||
smsTimer = null
|
||||
}
|
||||
isCountingDown.value = false
|
||||
countdown.value = 60
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
resetCountdown()
|
||||
})
|
||||
|
||||
function openSmsPopup() {
|
||||
cancelAccountCode.value = ''
|
||||
resetCountdown()
|
||||
showSmsPopup.value = true
|
||||
}
|
||||
|
||||
function closeSmsPopup() {
|
||||
showSmsPopup.value = false
|
||||
cancelAccountCode.value = ''
|
||||
resetCountdown()
|
||||
}
|
||||
|
||||
function onConfirmTap() {
|
||||
if (showBalanceWarning.value) {
|
||||
showBalancePopup.value = true
|
||||
return
|
||||
}
|
||||
openSmsPopup()
|
||||
}
|
||||
|
||||
function onBalanceContinue() {
|
||||
showBalancePopup.value = false
|
||||
openSmsPopup()
|
||||
}
|
||||
|
||||
function onBalanceCancel() {
|
||||
showBalancePopup.value = false
|
||||
}
|
||||
|
||||
/** 与登录页 sendVerificationCode 一致:节流、校验手机号、成功后倒计时并聚焦验证码框 */
|
||||
async function sendVerificationCode() {
|
||||
if (isCountingDown.value || !isPhoneNumberValid.value)
|
||||
return
|
||||
if (!userStore.mobile) {
|
||||
toast('请先绑定手机号')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { data, error } = await useApiFetch('auth/sendSms')
|
||||
.post({ mobile: userStore.mobile, actionType: 'cancelAccount', captchaVerifyParam: '' })
|
||||
.json()
|
||||
if (!error.value && data.value?.code === 200) {
|
||||
toast('获取成功')
|
||||
startCountdown()
|
||||
nextTick(() => {
|
||||
verificationCodeInputRef.value?.focus?.()
|
||||
})
|
||||
}
|
||||
else {
|
||||
toast(data.value?.msg || '发送失败')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
toast('发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
function clearAuthAndGoHome() {
|
||||
clearAuthStorage()
|
||||
userStore.resetUser()
|
||||
agentStore.resetAgent()
|
||||
uni.reLaunch({ url: '/pages/index' })
|
||||
}
|
||||
|
||||
async function submitCancelAccount() {
|
||||
if (!canSubmitCancel.value) {
|
||||
toast('请输入6位验证码')
|
||||
return
|
||||
}
|
||||
const { data, error } = await useApiFetch('/user/cancelOut')
|
||||
.post({ code: cancelAccountCode.value })
|
||||
.json()
|
||||
if (!error.value && data.value?.code === 200) {
|
||||
uni.showToast({ title: '账号已注销', icon: 'success' })
|
||||
closeSmsPopup()
|
||||
clearAuthAndGoHome()
|
||||
}
|
||||
else {
|
||||
toast(data.value?.msg || '注销失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-cancel-root box-border flex flex-col bg-gray-50">
|
||||
<view v-if="loading" class="flex flex-1 items-center justify-center py-20 text-gray-500">
|
||||
加载中...
|
||||
</view>
|
||||
<view v-else class="min-h-0 flex flex-1 flex-col">
|
||||
<view class="min-h-0 flex flex-1 flex-col">
|
||||
<scroll-view
|
||||
scroll-y
|
||||
:show-scrollbar="true"
|
||||
class="box-border min-h-0 flex-1 bg-white"
|
||||
style="flex: 1; height: 0; width: 100%;"
|
||||
>
|
||||
<AccountCancelAgreement />
|
||||
</scroll-view>
|
||||
|
||||
<view
|
||||
v-if="showAnyReminder"
|
||||
class="flex-shrink-0 border-t border-gray-100 bg-gray-50 px-4 py-3 space-y-2"
|
||||
>
|
||||
<view
|
||||
v-if="showBalanceWarning"
|
||||
class="border border-amber-200 rounded-lg bg-amber-50 p-3 text-sm text-amber-900"
|
||||
>
|
||||
<text class="font-medium">
|
||||
钱包提示
|
||||
</text>
|
||||
<text class="mt-1 block leading-relaxed">
|
||||
检测到您为代理且账户仍有余额(¥{{ (revenueData?.balance ?? 0).toFixed(2) }})或待结账金额(¥{{ (revenueData?.frozen_balance ?? 0).toFixed(2) }})。注销后将无法通过本账号提现,请确认已了解风险。
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="showVipLevelReminder"
|
||||
class="border border-violet-200 rounded-lg bg-violet-50 p-3 text-sm text-violet-900"
|
||||
>
|
||||
<text class="font-medium">
|
||||
会员提示
|
||||
</text>
|
||||
<text class="mt-1 block leading-relaxed">
|
||||
您当前为 {{ vipLevelLabel }} 代理会员,注销后该账号下的代理身份与相关权益将按平台规则终止,请确认已了解风险。
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="cancel-footer flex-shrink-0 border-t border-gray-200 bg-white px-4 pt-3">
|
||||
<view class="footer-btns flex gap-3">
|
||||
<wd-button
|
||||
class="footer-btn"
|
||||
hairline
|
||||
plain
|
||||
block
|
||||
type="info"
|
||||
@click="onExit"
|
||||
>
|
||||
退出
|
||||
</wd-button>
|
||||
<wd-button
|
||||
class="footer-btn"
|
||||
type="error"
|
||||
block
|
||||
@click="onConfirmTap"
|
||||
>
|
||||
确认注销
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 有余额时的二次确认(Wot) -->
|
||||
<wd-popup
|
||||
v-model="showBalancePopup"
|
||||
position="center"
|
||||
round
|
||||
:close-on-click-modal="true"
|
||||
custom-style="width: 86%; max-width: 360px;"
|
||||
@close="onBalanceCancel"
|
||||
>
|
||||
<view class="balance-popup p-5">
|
||||
<view class="mb-2 text-center text-base text-gray-900 font-semibold">
|
||||
确认注销
|
||||
</view>
|
||||
<view class="text-center text-sm text-gray-600 leading-relaxed">
|
||||
您的代理账户仍有余额或待结账金额,注销后将无法通过本账号提现,确定继续注销?
|
||||
</view>
|
||||
<view class="mt-5 flex gap-3">
|
||||
<wd-button plain hairline type="info" block class="flex-1" @click="onBalanceCancel">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button type="error" block class="flex-1" @click="onBalanceContinue">
|
||||
继续
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 短信验证(与登录页表单风格一致) -->
|
||||
<wd-popup
|
||||
v-model="showSmsPopup"
|
||||
position="bottom"
|
||||
round
|
||||
:safe-area-inset-bottom="true"
|
||||
:style="{ maxHeight: '85vh' }"
|
||||
:z-index="2000"
|
||||
@close="closeSmsPopup"
|
||||
>
|
||||
<view class="sms-popup">
|
||||
<view class="sms-popup-title">
|
||||
<text class="sms-popup-title-text">
|
||||
验证手机号
|
||||
</text>
|
||||
<wd-icon name="close" class="sms-popup-close" @click="closeSmsPopup" />
|
||||
</view>
|
||||
<view class="sms-popup-body">
|
||||
<text class="sms-popup-desc">
|
||||
将向 {{ maskName(userStore.mobile) }} 发送验证码,请输入短信中的 6 位数字完成注销。
|
||||
</text>
|
||||
|
||||
<view class="sms-form-item">
|
||||
<text class="sms-form-label">
|
||||
验证码
|
||||
</text>
|
||||
<view class="sms-verification-wrap">
|
||||
<wd-input
|
||||
ref="verificationCodeInputRef"
|
||||
v-model="cancelAccountCode"
|
||||
class="sms-verification-input"
|
||||
type="number"
|
||||
placeholder="请输入验证码"
|
||||
maxlength="6"
|
||||
no-border
|
||||
clearable
|
||||
>
|
||||
<template #suffix>
|
||||
<wd-button
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="isCountingDown || !isPhoneNumberValid"
|
||||
@click="sendVerificationCode"
|
||||
>
|
||||
{{ isCountingDown ? `${countdown}s` : '获取验证码' }}
|
||||
</wd-button>
|
||||
</template>
|
||||
</wd-input>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="sms-popup-actions mt-6 flex gap-3">
|
||||
<wd-button plain hairline type="info" block class="flex-1" @click="closeSmsPopup">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button
|
||||
type="error"
|
||||
block
|
||||
class="flex-1"
|
||||
:disabled="!canSubmitCancel"
|
||||
@click="submitCancelAccount"
|
||||
>
|
||||
确认注销
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-cancel-root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cancel-footer {
|
||||
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.06);
|
||||
/* 按钮区与屏幕底边留白,并叠加安全区 */
|
||||
padding-bottom: calc(16px + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.footer-btn {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.balance-popup {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sms-popup {
|
||||
display: flex;
|
||||
max-height: 85vh;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #ffffff 28%);
|
||||
}
|
||||
|
||||
.sms-popup-title {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.sms-popup-title-text {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.sms-popup-close {
|
||||
padding: 4px;
|
||||
font-size: 22px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.sms-popup-body {
|
||||
overflow-y: auto;
|
||||
padding: 16px 16px 24px;
|
||||
padding-bottom: calc(24px + constant(safe-area-inset-bottom));
|
||||
padding-bottom: calc(24px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.sms-popup-desc {
|
||||
display: block;
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.55;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.sms-form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
padding: 0 0.75rem;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.sms-form-label {
|
||||
flex-shrink: 0;
|
||||
margin-right: 1rem;
|
||||
min-width: 4rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.sms-verification-wrap {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sms-verification-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sms-popup-actions {
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
page {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
91
src/pages/help-detail.vue
Normal file
91
src/pages/help-detail.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from '@/composables/uni-router'
|
||||
definePage({ layout: 'default' })
|
||||
|
||||
const route = useRoute()
|
||||
const currentHelp = ref({
|
||||
title: '',
|
||||
image: '',
|
||||
images: null,
|
||||
})
|
||||
|
||||
// 图片路径映射
|
||||
const imageMap = {
|
||||
report_calculation: '/image/help/report-calculation.jpg',
|
||||
report_efficiency: '/image/help/report-efficiency.jpg',
|
||||
report_cost: '/image/help/report-cost.jpg',
|
||||
report_types: '/image/help/report-types.jpg',
|
||||
report_push: '/image/help/report-push.jpg',
|
||||
report_secret: ['/image/help/report-secret-1.jpg', '/image/help/report-secret-2.jpg'],
|
||||
invite_earnings: '/image/help/invite-earnings.jpg',
|
||||
direct_earnings: '/image/help/direct-earnings.jpg',
|
||||
vip_guide: '/image/help/vip-guide.jpg',
|
||||
}
|
||||
|
||||
// 标题映射
|
||||
const titleMap = {
|
||||
report_calculation: '推广报告的收益是如何计算的?',
|
||||
report_efficiency: '报告推广效率飙升指南',
|
||||
report_cost: '推广报告的成本是如何计算的?',
|
||||
report_types: '赤眉有哪些大数据报告类型',
|
||||
report_push: '如何推广报告',
|
||||
report_secret: '报告推广秘籍大公开',
|
||||
invite_earnings: '如何邀请下级成为代理',
|
||||
direct_earnings: '如何成为赤眉代理',
|
||||
vip_guide: '如何成为VIP代理和SVIP代理?',
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const id = route.query.id
|
||||
if (id && titleMap[id]) {
|
||||
currentHelp.value = {
|
||||
title: titleMap[id],
|
||||
image: Array.isArray(imageMap[id]) ? null : imageMap[id],
|
||||
images: Array.isArray(imageMap[id]) ? imageMap[id] : null,
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="help-detail">
|
||||
<view class="help-detail-title">{{ currentHelp.title }}</view>
|
||||
<template v-if="Array.isArray(currentHelp.images)">
|
||||
<image
|
||||
v-for="(image, index) in currentHelp.images"
|
||||
:key="index"
|
||||
:src="image"
|
||||
class="help-image"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</template>
|
||||
<image
|
||||
v-else-if="currentHelp.image"
|
||||
:src="currentHelp.image"
|
||||
class="help-image"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.help-detail {
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
|
||||
.help-detail-title {
|
||||
margin: 0 0 20px;
|
||||
font-size: 22px;
|
||||
color: #323233;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.help-image {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
105
src/pages/help-guide.vue
Normal file
105
src/pages/help-guide.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from '@/composables/uni-router'
|
||||
definePage({ layout: 'default' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const currentStepIndex = ref(0)
|
||||
|
||||
// 引导步骤数据
|
||||
const guideSteps = {
|
||||
report_guide: [
|
||||
{
|
||||
title: '第一步:进入直推报告页面',
|
||||
image: '/image/help/report-step1.jpg',
|
||||
},
|
||||
{
|
||||
title: '第二步:选择报告类型',
|
||||
image: '/image/help/report-step2.jpg',
|
||||
},
|
||||
{
|
||||
title: '第三步:填写推广信息',
|
||||
image: '/image/help/report-step3.jpg',
|
||||
},
|
||||
{
|
||||
title: '第四步:完成推广',
|
||||
image: '/image/help/report-step4.jpg',
|
||||
},
|
||||
],
|
||||
invite_guide: [
|
||||
{
|
||||
title: '第一步:进入邀请页面',
|
||||
image: '/image/help/invite-step1.jpg',
|
||||
},
|
||||
{
|
||||
title: '第二步:获取邀请码',
|
||||
image: '/image/help/invite-step2.jpg',
|
||||
},
|
||||
{
|
||||
title: '第三步:分享邀请链接',
|
||||
image: '/image/help/invite-step3.jpg',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const currentGuide = computed(() => {
|
||||
const id = route.query.id
|
||||
return guideSteps[id] || []
|
||||
})
|
||||
|
||||
const totalSteps = computed(() => currentGuide.value.length)
|
||||
|
||||
const currentStep = computed(() => currentGuide.value[currentStepIndex.value])
|
||||
|
||||
function handleImageClick() {
|
||||
if (currentStepIndex.value < totalSteps.value - 1) {
|
||||
currentStepIndex.value++
|
||||
}
|
||||
else {
|
||||
// 最后一步,返回列表页
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
|
||||
function onClickLeft() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const id = route.query.id
|
||||
if (!guideSteps[id]) {
|
||||
router.back()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="help-guide">
|
||||
<view class="guide-content" @click="handleImageClick">
|
||||
<image :src="currentStep.image" class="guide-image" mode="widthFix" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.help-guide {
|
||||
background-color: #666666;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 46px);
|
||||
|
||||
.guide-content {
|
||||
flex: 1;
|
||||
height: calc(100vh - 46px);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.guide-image {
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
161
src/pages/help.vue
Normal file
161
src/pages/help.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from '@/composables/uni-router'
|
||||
definePage({ layout: 'default' })
|
||||
|
||||
const router = useRouter()
|
||||
const activeTab = ref('report')
|
||||
|
||||
const categories = [
|
||||
{
|
||||
title: '推广报告',
|
||||
name: 'report',
|
||||
items: [
|
||||
{ id: 'report_guide', title: '直推报告页面引导', type: 'guide' },
|
||||
{ id: 'invite_guide', title: '邀请下级页面引导', type: 'guide' },
|
||||
{ id: 'direct_earnings', title: '如何成为赤眉代理' },
|
||||
{ id: 'report_push', title: '如何推广报告' },
|
||||
{ id: 'report_calculation', title: '推广报告的收益是如何计算的?' },
|
||||
{ id: 'report_cost', title: '推广报告的成本是如何计算的?' },
|
||||
{ id: 'report_efficiency', title: '报告推广效率飙升指南' },
|
||||
{ id: 'report_secret', title: '报告推广秘籍大公开' },
|
||||
{ id: 'report_types', title: '赤眉有哪些大数据报告类型' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '邀请下级',
|
||||
name: 'invite',
|
||||
items: [
|
||||
{ id: 'invite_earnings', title: '邀请下级赚取收益' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '其他',
|
||||
name: 'other',
|
||||
items: [
|
||||
{ id: 'vip_guide', title: '如何成为VIP代理和SVIP代理?' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const currentCategory = computed(() => {
|
||||
return categories.find(item => item.name === activeTab.value) || categories[0]
|
||||
})
|
||||
|
||||
function goToDetail(id, type) {
|
||||
if (type === 'guide') {
|
||||
router.push({
|
||||
path: '/help/guide',
|
||||
query: { id },
|
||||
})
|
||||
}
|
||||
else {
|
||||
router.push({
|
||||
path: '/help/detail',
|
||||
query: { id },
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="help-center">
|
||||
<view class="tab-bar">
|
||||
<view
|
||||
v-for="(category, index) in categories"
|
||||
:key="index"
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === category.name }"
|
||||
@click="activeTab = category.name"
|
||||
>
|
||||
{{ category.title }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<wd-cell-group border class="help-list">
|
||||
<wd-cell
|
||||
v-for="item in currentCategory.items"
|
||||
:key="item.id"
|
||||
:title="item.title"
|
||||
clickable
|
||||
class="help-item"
|
||||
@click="goToDetail(item.id, item.type)"
|
||||
>
|
||||
<template v-if="item.type === 'guide'" #label>
|
||||
<text class="guide-tag">
|
||||
引导指南
|
||||
</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.help-center {
|
||||
min-height: 100vh;
|
||||
background-color: #f7f8fa;
|
||||
padding: 12px;
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 4px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
border-radius: 8px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.help-list {
|
||||
margin-top: 12px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
.guide-tag {
|
||||
margin-top: 4px;
|
||||
display: inline-flex;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
background-color: #2563eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.help-detail {
|
||||
padding: 20px;
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
white-space: pre-line;
|
||||
line-height: 1.6;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
140
src/pages/history-query.vue
Normal file
140
src/pages/history-query.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
definePage({ layout: 'default', auth: true })
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const reportList = ref([])
|
||||
const loading = ref(false)
|
||||
// 初始加载数据
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
const { data, error } = await useApiFetch(`query/list?page=${page.value}&page_size=${pageSize.value}`)
|
||||
.get()
|
||||
.json()
|
||||
if (data.value && !error.value) {
|
||||
if (data.value.code === 200) {
|
||||
total.value = data.value.data.total
|
||||
reportList.value = data.value.data.list || []
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
|
||||
function onPageChange({ value }) {
|
||||
page.value = value
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function toDetail(item) {
|
||||
if (item.query_state !== 'success')
|
||||
return
|
||||
uni.navigateTo({
|
||||
url: `/pages/report-result-webview?order_id=${encodeURIComponent(String(item.order_id || ''))}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 状态文字映射
|
||||
function stateText(state) {
|
||||
switch (state) {
|
||||
case 'pending':
|
||||
return '查询中'
|
||||
case 'success':
|
||||
return '查询成功'
|
||||
case 'failed':
|
||||
return '查询失败'
|
||||
case 'refunded':
|
||||
return '已退款'
|
||||
default:
|
||||
return '未知状态'
|
||||
}
|
||||
}
|
||||
|
||||
// 状态颜色映射
|
||||
function statusClass(state) {
|
||||
switch (state) {
|
||||
case 'pending':
|
||||
return 'status-pending'
|
||||
case 'success':
|
||||
return 'status-success'
|
||||
case 'failed':
|
||||
return 'status-failed'
|
||||
case 'refunded':
|
||||
return 'status-refunded'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="flex flex-col gap-4 p-4">
|
||||
<view class="history-scroll">
|
||||
<view
|
||||
v-for="item in reportList" :key="item.id" class="relative mb-4 cursor-pointer rounded-lg bg-white p-4 shadow-sm"
|
||||
@click="toDetail(item)"
|
||||
>
|
||||
<view class="flex flex-col">
|
||||
<view class="mb-1 text-xl text-black">
|
||||
{{ item.product_name }}
|
||||
</view>
|
||||
<view class="text-sm text-[#999999]">
|
||||
{{ item.create_time }}
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="absolute right-0 top-0 rounded-bl-lg rounded-tr-lg px-2 py-[1px] text-sm text-white font-medium"
|
||||
:class="[statusClass(item.query_state)]"
|
||||
>
|
||||
{{ stateText(item.query_state) }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading" class="py-4 text-center text-sm text-gray-400">
|
||||
加载中...
|
||||
</view>
|
||||
<view v-else-if="!reportList.length" class="py-4 text-center text-sm text-gray-400">
|
||||
暂无记录
|
||||
</view>
|
||||
</view>
|
||||
<wd-pagination
|
||||
v-model="page"
|
||||
:total="total"
|
||||
:page-size="pageSize"
|
||||
show-icon
|
||||
show-message
|
||||
@change="onPageChange"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.history-scroll {
|
||||
height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background-color: #1FBE5D;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background-color: #EB3C3C;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-refunded {
|
||||
background-color: #999999;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
179
src/pages/index.vue
Normal file
179
src/pages/index.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<script setup>
|
||||
import bgIcon from '/static/images/bg_icon.png'
|
||||
import bannerImg from '/static/images/index/banner_1.png'
|
||||
import bannerImg2 from '/static/images/index/banner_2.png'
|
||||
import bannerImg3 from '/static/images/index/banner_3.png'
|
||||
import honestyBanner from '/static/images/index/banner_B.png'
|
||||
import companyIcon from '/static/images/index/company_bg.png'
|
||||
import housekeepingRiskIcon from '/static/images/index/housekeeping_risk_bg.png'
|
||||
import loanCheckIcon from '/static/images/index/loan_check_bg.png'
|
||||
import marriageRiskIcon from '/static/images/index/marriage_risk_bg.png'
|
||||
import personalDataIcon from '/static/images/index/personal_data_bg.png'
|
||||
import preLoanRiskIcon from '/static/images/index/preloan_risk_bg.png'
|
||||
import rentalInfoBg from '/static/images/index/rentalinfo_bg.png'
|
||||
import rightIcon from '/static/images/index/right.png'
|
||||
import indexPromoteIcon from '/static/images/index/tgbg.png'
|
||||
import indexMyReportIcon from '/static/images/index/wdbg.png'
|
||||
import indexInvitationIcon from '/static/images/index/yqhy.png'
|
||||
|
||||
definePage({ type: 'home', layout: 'home' })
|
||||
|
||||
const banners = [bannerImg, bannerImg2, bannerImg3]
|
||||
|
||||
const services = [
|
||||
{ title: '个人大数据', name: 'personalData', bg: personalDataIcon },
|
||||
{ title: '婚恋风险', name: 'marriage', bg: marriageRiskIcon },
|
||||
{ title: '入职背调', name: 'backgroundcheck', bg: preLoanRiskIcon },
|
||||
]
|
||||
|
||||
const riskServices = [
|
||||
{ title: '小微', name: 'companyinfo', bg: companyIcon },
|
||||
{ title: '家政', name: 'homeservice', bg: housekeepingRiskIcon },
|
||||
{ title: '贷前', name: 'preloanbackgroundcheck', bg: loanCheckIcon },
|
||||
{ title: '诚信租赁', name: 'rentalinfo', bg: rentalInfoBg },
|
||||
]
|
||||
|
||||
function toInquire(name) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/inquire?feature=${encodeURIComponent(name)}`,
|
||||
})
|
||||
}
|
||||
|
||||
function toInvitation() {
|
||||
uni.navigateTo({ url: '/pages/invitation' })
|
||||
}
|
||||
|
||||
function toPromote() {
|
||||
uni.navigateTo({ url: '/pages/promote' })
|
||||
}
|
||||
|
||||
function toHistory() {
|
||||
uni.navigateTo({ url: '/pages/history-query' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="box-border min-h-screen">
|
||||
<view class="relative p-4">
|
||||
<swiper
|
||||
class="banner-swiper overflow-hidden rounded-xl" circular autoplay :interval="3000" indicator-dots
|
||||
indicator-color="rgba(255,255,255,0.5)" indicator-active-color="#ffffff"
|
||||
>
|
||||
<swiper-item v-for="(item, index) in banners" :key="index">
|
||||
<image :src="item" class="h-full w-full" mode="aspectFill" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
|
||||
<view class="px-6">
|
||||
<view class="grid grid-cols-3 gap-3">
|
||||
<view class="flex flex-col items-center justify-center text-center" @click="toPromote">
|
||||
<view
|
||||
class="box-content h-16 w-16 flex items-center justify-center rounded-full from-white to-blue-100/10 bg-gradient-to-b p-1 shadow-lg"
|
||||
>
|
||||
<image :src="indexPromoteIcon" class="h-12 w-12" mode="aspectFit" />
|
||||
</view>
|
||||
<text class="mt-1 text-center font-bold">
|
||||
推广报告
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex flex-col items-center justify-center text-center" @click="toInvitation">
|
||||
<view
|
||||
class="box-content h-16 w-16 flex items-center justify-center rounded-full from-white to-blue-100/10 bg-gradient-to-b p-1 shadow-lg"
|
||||
>
|
||||
<image :src="indexInvitationIcon" class="h-12 w-12" mode="aspectFit" />
|
||||
</view>
|
||||
<text class="mt-1 text-center font-bold">
|
||||
邀请下级
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flex flex-col items-center justify-center text-center" @click="toHistory">
|
||||
<view
|
||||
class="box-content h-16 w-16 flex items-center justify-center rounded-full from-white to-blue-100/10 bg-gradient-to-b p-1 shadow-lg"
|
||||
>
|
||||
<image :src="indexMyReportIcon" class="h-12 w-12" mode="aspectFit" />
|
||||
</view>
|
||||
<text class="mt-1 text-center font-bold">
|
||||
我的报告
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="relative p-4 pt-0">
|
||||
<view class="grid grid-cols-2 my-4 gap-4" style="grid-template-rows: repeat(2, 1fr);">
|
||||
<view
|
||||
v-for="(service, index) in services" :key="index"
|
||||
class="relative min-h-18 flex flex-col rounded-xl px-4 py-2 shadow-lg"
|
||||
:class="index === 0 ? 'row-span-2' : ''"
|
||||
:style="`background: url(${service.bg}) no-repeat; background-size: 100% 100%; background-position: center;`"
|
||||
@click="toInquire(service.name)"
|
||||
>
|
||||
<view class="min-h-18 flex items-end">
|
||||
<!-- <text class="text-base text-gray-700 font-semibold">
|
||||
{{ service.title }}
|
||||
</text> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-x class="risk-scroll my-4 px-1 pb-4 pt-2 -mx-1">
|
||||
<view class="inline-flex gap-2">
|
||||
<view
|
||||
v-for="(service, index) in riskServices" :key="index"
|
||||
class="relative h-24 w-[107px] flex-shrink-0 rounded-xl shadow-lg"
|
||||
:style="`background: url(${service.bg}) no-repeat; background-size: 100% 100%; background-position: center;`"
|
||||
@click="toInquire(service.name)"
|
||||
>
|
||||
<view class="h-full flex items-end px-2 py-2">
|
||||
<!-- <text class="text-sm text-gray-700 font-semibold">
|
||||
{{ service.title }}
|
||||
</text> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="mb-3 mt-6 flex items-center">
|
||||
<view class="bg-primary h-5 w-1.5 rounded-xl" />
|
||||
<text class="ml-2 text-lg text-gray-800">
|
||||
诚信专栏
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="mt-4 overflow-hidden rounded-xl bg-white shadow-xl">
|
||||
<image :src="honestyBanner" class="block w-full" mode="widthFix" />
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="mt-4 box-border h-14 w-full flex items-center rounded-lg bg-white px-4 text-gray-700 shadow-lg"
|
||||
@click="toHistory"
|
||||
>
|
||||
<view class="mr-4 h-full flex items-center justify-center">
|
||||
<image class="h-10 w-10" :src="bgIcon" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<view class="text-gray-800">
|
||||
我的历史查询记录
|
||||
</view>
|
||||
<view class="text-xs text-gray-500">
|
||||
查询记录有效期为30天
|
||||
</view>
|
||||
</view>
|
||||
<image :src="rightIcon" class="h-6 w-6" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.banner-swiper {
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.risk-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
54
src/pages/inquire.vue
Normal file
54
src/pages/inquire.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import InquireForm from '@/components/InquireForm.vue'
|
||||
|
||||
definePage({ layout: 'default' })
|
||||
|
||||
const feature = ref('')
|
||||
const IMMERSIVE_NAVBAR_ROUTE = 'pages/inquire'
|
||||
|
||||
// 获取产品信息
|
||||
const featureData = ref({})
|
||||
|
||||
onLoad(async (query) => {
|
||||
const q = query || {}
|
||||
feature.value = String(q.feature || '')
|
||||
if (q.out_trade_no) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/report-result-webview?out_trade_no=${encodeURIComponent(String(q.out_trade_no))}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
await getProduct()
|
||||
})
|
||||
|
||||
async function getProduct() {
|
||||
if (!feature.value)
|
||||
return
|
||||
const { data } = await useApiFetch(`/product/en/${feature.value}`)
|
||||
.get()
|
||||
.json()
|
||||
|
||||
if (data.value) {
|
||||
featureData.value = data.value.data
|
||||
}
|
||||
}
|
||||
|
||||
onPageScroll((event) => {
|
||||
uni.$emit('immersive-navbar-change', {
|
||||
route: IMMERSIVE_NAVBAR_ROUTE,
|
||||
solid: event.scrollTop > 120,
|
||||
})
|
||||
})
|
||||
|
||||
onUnload(() => {
|
||||
uni.$emit('immersive-navbar-change', {
|
||||
route: IMMERSIVE_NAVBAR_ROUTE,
|
||||
solid: false,
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<InquireForm type="normal" :feature="feature" :feature-data="featureData" />
|
||||
</template>
|
||||
202
src/pages/invitation-agent-apply.vue
Normal file
202
src/pages/invitation-agent-apply.vue
Normal file
@@ -0,0 +1,202 @@
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
import { aesDecrypt } from '@/utils/crypto'
|
||||
import { getToken, setAuthSession } from '@/utils/storage'
|
||||
|
||||
definePage({ layout: 'default' })
|
||||
|
||||
let intervalId = null
|
||||
const POLL_INTERVAL_MS = 2000
|
||||
|
||||
const showApplyPopup = ref(false)
|
||||
const route = useRoute()
|
||||
|
||||
const store = useAgentStore()
|
||||
const userStore = useUserStore()
|
||||
const { isLoggedIn, mobile } = storeToRefs(userStore)
|
||||
const { status } = storeToRefs(store) // 响应式解构
|
||||
const ancestor = ref('')
|
||||
const isSelf = ref(false)
|
||||
const isApplyPolling = ref(false)
|
||||
|
||||
function stopApplyStatusPolling() {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
intervalId = null
|
||||
}
|
||||
isApplyPolling.value = false
|
||||
}
|
||||
|
||||
function startApplyStatusPolling() {
|
||||
if (intervalId || status.value === 1 || status.value === 2)
|
||||
return
|
||||
isApplyPolling.value = true
|
||||
intervalId = setInterval(async () => {
|
||||
await store.fetchAgentStatus()
|
||||
if (status.value === 1 || status.value === 2)
|
||||
stopApplyStatusPolling()
|
||||
}, POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
async function syncStatusAndUpdatePolling() {
|
||||
await store.fetchAgentStatus()
|
||||
if (status.value === 0 || status.value === 3) {
|
||||
startApplyStatusPolling()
|
||||
}
|
||||
else {
|
||||
stopApplyStatusPolling()
|
||||
}
|
||||
}
|
||||
|
||||
function agentApply() {
|
||||
showApplyPopup.value = true
|
||||
}
|
||||
// 计算显示状态(当isSelf为false时强制显示为3)
|
||||
const displayStatus = computed(() => {
|
||||
// return isSelf.value ? status.value : 3;
|
||||
return status.value
|
||||
})
|
||||
|
||||
// 跳转到首页
|
||||
function goToHome() {
|
||||
stopApplyStatusPolling()
|
||||
uni.reLaunch({ url: '/pages/index' })
|
||||
}
|
||||
onBeforeMount(async () => {
|
||||
const channelKey = import.meta.env.VITE_INVITE_CHANNEL_KEY
|
||||
if (!channelKey) {
|
||||
uni.showToast({ title: '缺少邀请渠道配置', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (route.name === 'invitationAgentApplySelf') {
|
||||
isSelf.value = true
|
||||
}
|
||||
else {
|
||||
const linkIdentifier = route.params.linkIdentifier
|
||||
const decryptDataStr = aesDecrypt(
|
||||
decodeURIComponent(linkIdentifier),
|
||||
channelKey,
|
||||
)
|
||||
const decryptData = JSON.parse(decryptDataStr)
|
||||
ancestor.value = decryptData.mobile
|
||||
}
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
await syncStatusAndUpdatePolling()
|
||||
}
|
||||
})
|
||||
async function submitApplication(formData) {
|
||||
// 提交代理申请的数据
|
||||
const { region, mobile, wechat_id, code } = formData
|
||||
const postData = {
|
||||
region,
|
||||
mobile,
|
||||
wechat_id,
|
||||
code,
|
||||
}
|
||||
|
||||
if (!isSelf.value) {
|
||||
postData.ancestor = ancestor.value
|
||||
}
|
||||
|
||||
const { data, error } = await useApiFetch('/agent/apply')
|
||||
.post(postData)
|
||||
.json()
|
||||
|
||||
if (data.value && !error.value) {
|
||||
if (data.value.code === 200) {
|
||||
showApplyPopup.value = false
|
||||
showToast({ message: '已提交申请' })
|
||||
if (data.value.data.accessToken) {
|
||||
setAuthSession(data.value.data)
|
||||
}
|
||||
if (getToken()) {
|
||||
await syncStatusAndUpdatePolling()
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.warn('申请失败', data.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(status, (nextStatus, prevStatus) => {
|
||||
if (!isApplyPolling.value)
|
||||
return
|
||||
if ((prevStatus === 0 || prevStatus === 3) && nextStatus === 1) {
|
||||
showToast({ message: '审核已通过' })
|
||||
}
|
||||
else if ((prevStatus === 0 || prevStatus === 3) && nextStatus === 2) {
|
||||
showToast({ message: '审核未通过,请重新提交' })
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopApplyStatusPolling()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-[#D1D6FF]">
|
||||
<image class="block w-full" src="/static/images/invitation_agent_apply.png" mode="widthFix" />
|
||||
<!-- 统一状态处理容器 -->
|
||||
<view class="flex flex-col items-center justify-center">
|
||||
<!-- 审核中状态 -->
|
||||
<view v-if="displayStatus === 0" class="text-center">
|
||||
<text class="text-xs text-gray-500">您的申请正在审核中</text>
|
||||
<view class="mt-1 rounded-3xl bg-gray-200 p-1 shadow-xl">
|
||||
<view class="rounded-3xl bg-gray-400 px-8 py-2 text-xl text-white font-bold shadow-lg opacity-90">
|
||||
<text>审核进行中</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 审核通过状态 -->
|
||||
<view v-if="displayStatus === 1" class="text-center">
|
||||
<text class="text-xs text-gray-500">您已成为认证代理方</text>
|
||||
<view class="mt-1 rounded-3xl bg-green-100 p-1 shadow-xl" @click="goToHome">
|
||||
<view
|
||||
class="rounded-3xl from-green-500 to-green-300 bg-gradient-to-t px-8 py-2 text-xl text-white font-bold shadow-lg">
|
||||
<text>进入应用首页</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 审核未通过状态 -->
|
||||
<view v-if="displayStatus === 2" class="text-center">
|
||||
<text class="text-xs text-red-500">审核未通过,请重新提交</text>
|
||||
<view class="mt-1 rounded-3xl bg-red-100 p-1 shadow-xl" @click="agentApply">
|
||||
<view
|
||||
class="rounded-3xl from-red-500 to-red-300 bg-gradient-to-t px-8 py-2 text-xl text-white font-bold shadow-lg">
|
||||
<text>重新提交申请</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 未申请状态(包含邀请状态) -->
|
||||
<view v-if="displayStatus === 3" class="text-center">
|
||||
<text class="text-xs text-gray-500">
|
||||
{{ isSelf ? '立即申请成为代理人' : '邀您注册代理人' }}
|
||||
</text>
|
||||
<view class="mt-1 rounded-3xl bg-gray-100 p-1 shadow-xl" @click="agentApply">
|
||||
<view
|
||||
class="rounded-3xl from-blue-500 to-blue-300 bg-gradient-to-t px-8 py-2 text-xl text-white font-bold shadow-lg">
|
||||
<text>立即成为代理方</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<AgentApplicationForm
|
||||
v-model:show="showApplyPopup"
|
||||
:ancestor="ancestor"
|
||||
:is-self="isSelf"
|
||||
:is-logged-in="isLoggedIn"
|
||||
:user-mobile="mobile"
|
||||
@submit="submitApplication"
|
||||
@close="showApplyPopup = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
67
src/pages/invitation.vue
Normal file
67
src/pages/invitation.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted } from 'vue'
|
||||
import { useAgentStore } from '@/stores/agentStore'
|
||||
import { aesEncrypt } from '@/utils/crypto'
|
||||
|
||||
definePage({ layout: 'default', auth: true })
|
||||
|
||||
const agentStore = useAgentStore()
|
||||
const { mobile, agentID } = storeToRefs(agentStore)
|
||||
|
||||
const showQRcode = ref(false)
|
||||
const linkIdentifier = ref('')
|
||||
|
||||
function encryptIdentifire(id, phone) {
|
||||
const channelKey = import.meta.env.VITE_INVITE_CHANNEL_KEY
|
||||
if (!channelKey) {
|
||||
uni.showToast({ title: '缺少邀请渠道配置', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const linkIdentifierJSON = {
|
||||
agentID: id,
|
||||
mobile: phone,
|
||||
}
|
||||
const linkIdentifierStr = JSON.stringify(linkIdentifierJSON)
|
||||
const encodeData = aesEncrypt(
|
||||
linkIdentifierStr,
|
||||
channelKey,
|
||||
)
|
||||
linkIdentifier.value = encodeURIComponent(encodeData)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
if (!agentID.value)
|
||||
await agentStore.fetchAgentStatus()
|
||||
encryptIdentifire(agentID.value || '', mobile.value || '')
|
||||
}
|
||||
catch (e) {
|
||||
console.error('[invitation] 初始化失败', e)
|
||||
uni.showToast({ title: '加载代理信息失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="relative min-h-screen w-full pb-[calc(3rem+env(safe-area-inset-bottom))]">
|
||||
<image
|
||||
class="block w-full"
|
||||
src="/static/images/invitation.png"
|
||||
mode="widthFix"
|
||||
/>
|
||||
<view
|
||||
class="fixed inset-x-0 bottom-0 z-10 min-h-12 flex items-center justify-center rounded-t-xl from-orange-500 to-orange-300 bg-gradient-to-t px-4 pb-[env(safe-area-inset-bottom)] pt-2 text-base text-white font-bold shadow-xl"
|
||||
@click="showQRcode = true"
|
||||
>
|
||||
<text>立即邀请好友</text>
|
||||
</view>
|
||||
</view>
|
||||
<QRcode
|
||||
v-model:show="showQRcode"
|
||||
mode="invitation"
|
||||
:link-identifier="linkIdentifier"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
297
src/pages/login.vue
Normal file
297
src/pages/login.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<script setup>
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
|
||||
definePage({ layout: false })
|
||||
|
||||
const phoneNumber = ref('')
|
||||
const verificationCode = ref('')
|
||||
const isAgreed = ref(false)
|
||||
const isCountingDown = ref(false)
|
||||
const countdown = ref(60)
|
||||
const redirectUrl = ref('')
|
||||
let timer = null
|
||||
const userStore = useUserStore()
|
||||
const agentStore = useAgentStore()
|
||||
|
||||
function toast(title) {
|
||||
uni.showToast({ title, icon: 'none' })
|
||||
}
|
||||
|
||||
const isPhoneNumberValid = computed(() => {
|
||||
return /^1[3-9]\d{9}$/.test(phoneNumber.value)
|
||||
})
|
||||
|
||||
const canLogin = computed(() => {
|
||||
if (!isPhoneNumberValid.value)
|
||||
return false
|
||||
return verificationCode.value.length === 6
|
||||
})
|
||||
|
||||
async function sendVerificationCode() {
|
||||
if (isCountingDown.value || !isPhoneNumberValid.value)
|
||||
return
|
||||
if (!isPhoneNumberValid.value) {
|
||||
toast('请输入有效的手机号')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { data, error } = await useApiFetch('auth/sendSms')
|
||||
.post({ mobile: phoneNumber.value, actionType: 'login', captchaVerifyParam: '' })
|
||||
.json()
|
||||
if (!error.value && data.value?.code === 200) {
|
||||
toast('获取成功')
|
||||
startCountdown()
|
||||
return
|
||||
}
|
||||
toast(data.value?.msg || '发送失败')
|
||||
}
|
||||
catch {
|
||||
toast('发送失败')
|
||||
}
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
isCountingDown.value = true
|
||||
countdown.value = 60
|
||||
timer = setInterval(() => {
|
||||
if (countdown.value > 0) {
|
||||
countdown.value--
|
||||
}
|
||||
else {
|
||||
clearInterval(timer)
|
||||
isCountingDown.value = false
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
if (!isPhoneNumberValid.value) {
|
||||
toast('请输入有效的手机号')
|
||||
return
|
||||
}
|
||||
if (verificationCode.value.length !== 6) {
|
||||
toast('请输入有效的验证码')
|
||||
return
|
||||
}
|
||||
if (!isAgreed.value) {
|
||||
toast('请先同意用户协议')
|
||||
return
|
||||
}
|
||||
performLogin()
|
||||
}
|
||||
|
||||
// 执行实际的登录逻辑
|
||||
async function performLogin() {
|
||||
const { data, error } = await useApiFetch('/user/mobileCodeLogin')
|
||||
.post({ mobile: phoneNumber.value, code: verificationCode.value })
|
||||
.json()
|
||||
|
||||
if (data.value && !error.value) {
|
||||
if (data.value.code === 200) {
|
||||
uni.setStorageSync('token', data.value.data.accessToken)
|
||||
uni.setStorageSync('refreshAfter', data.value.data.refreshAfter)
|
||||
uni.setStorageSync('accessExpire', data.value.data.accessExpire)
|
||||
|
||||
await userStore.fetchUserInfo()
|
||||
await agentStore.fetchAgentStatus()
|
||||
|
||||
uni.reLaunch({ url: redirectUrl.value || '/pages/index' })
|
||||
}
|
||||
else {
|
||||
toast(data.value.msg || '登录失败')
|
||||
}
|
||||
}
|
||||
else {
|
||||
toast('登录失败')
|
||||
}
|
||||
}
|
||||
function toUserAgreement() {
|
||||
uni.navigateTo({ url: '/pages/user-agreement' })
|
||||
}
|
||||
|
||||
function toPrivacyPolicy() {
|
||||
uni.navigateTo({ url: '/pages/privacy-policy' })
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
})
|
||||
function onClickLeft() {
|
||||
uni.reLaunch({ url: '/pages/index' })
|
||||
}
|
||||
|
||||
onLoad((query) => {
|
||||
const raw = String(query?.redirect || '')
|
||||
if (!raw)
|
||||
return
|
||||
try {
|
||||
redirectUrl.value = decodeURIComponent(raw)
|
||||
}
|
||||
catch {
|
||||
redirectUrl.value = raw
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="login-layout">
|
||||
<wd-navbar title="用户登录" safe-area-inset-top left-arrow placeholder fixed @click-left="onClickLeft" />
|
||||
<view class="login relative z-10 px-4">
|
||||
<view class="mb-8 pt-20 text-left">
|
||||
<view class="flex flex-col items-center">
|
||||
<image class="h-16 w-16 rounded-full shadow" src="/static/images/logo.png" alt="Logo" />
|
||||
<view class="mt-4 text-3xl text-slate-700 font-bold">
|
||||
赤眉
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<view class="login-form">
|
||||
<!-- 手机号输入 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">
|
||||
手机号
|
||||
</text>
|
||||
<wd-input v-model="phoneNumber" class="phone-wd-input" type="number" placeholder="请输入手机号" maxlength="11"
|
||||
no-border clearable />
|
||||
</view>
|
||||
|
||||
<!-- 验证码输入 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">
|
||||
验证码
|
||||
</text>
|
||||
<view class="verification-input-wrapper">
|
||||
<wd-input v-model="verificationCode" class="verification-wd-input" placeholder="请输入验证码" maxlength="6"
|
||||
no-border clearable>
|
||||
<template #suffix>
|
||||
<wd-button size="small" type="primary" plain :disabled="isCountingDown || !isPhoneNumberValid"
|
||||
@click="sendVerificationCode">
|
||||
{{ isCountingDown ? `${countdown}s` : '获取验证码' }}
|
||||
</wd-button>
|
||||
</template>
|
||||
</wd-input>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 协议同意框 -->
|
||||
<view class="agreement-wrapper">
|
||||
<wd-checkbox v-model="isAgreed" shape="square" size="18px" />
|
||||
<text class="agreement-text">
|
||||
我已阅读并同意
|
||||
<text class="agreement-link" @click="toUserAgreement">
|
||||
《用户协议》
|
||||
</text>
|
||||
<text class="agreement-link" @click="toPrivacyPolicy">
|
||||
《隐私政策》
|
||||
</text>
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 提示文字 -->
|
||||
<view class="notice-text">
|
||||
未注册手机号登录后将自动生成账号,并且代表您已阅读并同意
|
||||
</view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<wd-button class="login-wd-btn" block type="primary" :disabled="!canLogin" @click="handleLogin">
|
||||
登 录
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-layout {
|
||||
background: linear-gradient(180deg, #eff6ff 0%, #ffffff 100%);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
/* 登录表单 */
|
||||
.login-form {
|
||||
margin-top: 0.5rem;
|
||||
border-radius: 16px;
|
||||
background-color: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: 0 8px 28px rgba(63, 63, 63, 0.1);
|
||||
padding: 1.5rem 1.25rem;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* 表单项 */
|
||||
.form-item {
|
||||
margin-bottom: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 0.9375rem;
|
||||
color: #111827;
|
||||
margin-bottom: 0;
|
||||
margin-right: 1rem;
|
||||
font-weight: 500;
|
||||
min-width: 4rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.phone-wd-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 验证码输入 */
|
||||
.verification-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.verification-wd-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 协议同意 */
|
||||
.agreement-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.4;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.agreement-link {
|
||||
color: #2563eb;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* 提示文字 */
|
||||
.notice-text {
|
||||
font-size: 0.6875rem;
|
||||
color: #9ca3af;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
/* 登录按钮 */
|
||||
.login-wd-btn {
|
||||
letter-spacing: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
360
src/pages/me.vue
Normal file
360
src/pages/me.vue
Normal file
@@ -0,0 +1,360 @@
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onBeforeMount, ref, watch } from 'vue'
|
||||
import { openCustomerService } from '@/composables/useCustomerService'
|
||||
import { useEnv } from '@/composables/useEnv'
|
||||
import { useAgentStore } from '@/stores/agentStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import headShot from '/static/images/head_shot.webp'
|
||||
|
||||
definePage({ layout: 'home' })
|
||||
|
||||
const agentStore = useAgentStore()
|
||||
const userStore = useUserStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const { isAgent, level, ExpiryTime } = storeToRefs(agentStore)
|
||||
const { userAvatar, isLoggedIn, mobile } = storeToRefs(userStore)
|
||||
const { isWeChat } = useEnv()
|
||||
|
||||
const levelNames = {
|
||||
'normal': '普通代理',
|
||||
'': '普通代理',
|
||||
'VIP': 'VIP代理',
|
||||
'SVIP': 'SVIP代理',
|
||||
}
|
||||
|
||||
const levelText = {
|
||||
'normal': '基础代理特权',
|
||||
'': '基础代理特权',
|
||||
'VIP': '高级代理特权',
|
||||
'SVIP': '尊享代理特权',
|
||||
}
|
||||
|
||||
const levelGradient = computed(() => ({
|
||||
border: {
|
||||
'normal': '',
|
||||
'': '',
|
||||
'VIP': '',
|
||||
'SVIP': '',
|
||||
}[level.value],
|
||||
|
||||
badge: {
|
||||
'normal': 'bg-gradient-to-r from-gray-500 to-gray-600',
|
||||
'': 'bg-gradient-to-r from-gray-500 to-gray-600',
|
||||
'VIP': 'bg-gradient-to-r from-yellow-500 to-amber-600',
|
||||
'SVIP': 'bg-gradient-to-r from-purple-500 to-pink-500',
|
||||
}[level.value],
|
||||
|
||||
text: {
|
||||
'normal': 'text-gray-600',
|
||||
'': 'text-gray-600',
|
||||
'VIP': 'text-amber-600',
|
||||
'SVIP': 'text-purple-600',
|
||||
}[level.value],
|
||||
}))
|
||||
|
||||
function maskName(name) {
|
||||
if (!name || name.length < 11)
|
||||
return name
|
||||
return `${name.substring(0, 3)}****${name.substring(7)}`
|
||||
}
|
||||
|
||||
function toHistory() {
|
||||
uni.navigateTo({ url: '/pages/history-query' })
|
||||
}
|
||||
function toPromote() {
|
||||
uni.navigateTo({ url: '/pages/promote' })
|
||||
}
|
||||
function toInvitation() {
|
||||
uni.navigateTo({ url: '/pages/invitation' })
|
||||
}
|
||||
function toUserAgreement() {
|
||||
uni.navigateTo({ url: '/pages/user-agreement' })
|
||||
}
|
||||
|
||||
function toPrivacyPolicy() {
|
||||
uni.navigateTo({ url: '/pages/privacy-policy' })
|
||||
}
|
||||
|
||||
function redirectToLogin() {
|
||||
uni.navigateTo({ url: '/pages/login' })
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('refreshAfter')
|
||||
uni.removeStorageSync('accessExpire')
|
||||
uni.removeStorageSync('userInfo')
|
||||
uni.removeStorageSync('agentInfo')
|
||||
|
||||
// 重置状态
|
||||
userStore.resetUser()
|
||||
agentStore.resetAgent()
|
||||
|
||||
uni.reLaunch({ url: '/pages/index' })
|
||||
}
|
||||
|
||||
function goCancelAccountPage() {
|
||||
if (!mobile.value) {
|
||||
uni.showToast({ title: '请先绑定手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.navigateTo({ url: '/pages/cancel-account' })
|
||||
}
|
||||
|
||||
function toService() {
|
||||
openCustomerService()
|
||||
}
|
||||
|
||||
function toVipConfig() {
|
||||
uni.navigateTo({ url: '/pages/agent-vip-config' })
|
||||
}
|
||||
|
||||
function toVipRenewal() {
|
||||
uni.navigateTo({ url: '/pages/agent-vip-apply' })
|
||||
}
|
||||
|
||||
function formatExpiryTime(expiryTimeStr) {
|
||||
if (!expiryTimeStr)
|
||||
return '未知'
|
||||
// 假设expiryTimeStr格式是 "YYYY-MM-DD HH:MM:SS"
|
||||
// 只返回日期部分 "YYYY-MM-DD"
|
||||
return expiryTimeStr.split(' ')[0]
|
||||
}
|
||||
|
||||
/** 与 bdrp-mini `/static/image/shot_*.png` 一致,资源放在 `src/static/image/` */
|
||||
function getDefaultAvatar() {
|
||||
if (!isAgent.value)
|
||||
return headShot
|
||||
|
||||
const normalizedLevel = String(level.value || '').toUpperCase()
|
||||
switch (normalizedLevel) {
|
||||
case 'NORMAL':
|
||||
case 'normal':
|
||||
case '':
|
||||
return '/static/image/shot_nonal.png'
|
||||
case 'VIP':
|
||||
return '/static/image/shot_vip.png'
|
||||
case 'SVIP':
|
||||
return '/static/image/shot_svip.png'
|
||||
default:
|
||||
return headShot
|
||||
}
|
||||
}
|
||||
|
||||
/** 用户头像 URL(store 已 resolve);默认图为本地资源,加载失败时回退到 headShot */
|
||||
const avatarDisplay = ref(headShot)
|
||||
|
||||
function syncAvatarDisplay() {
|
||||
avatarDisplay.value = userAvatar.value || getDefaultAvatar()
|
||||
}
|
||||
|
||||
watch([userAvatar, isAgent, level], syncAvatarDisplay, { immediate: true })
|
||||
|
||||
function onAvatarError() {
|
||||
avatarDisplay.value = headShot
|
||||
}
|
||||
|
||||
function showBindPhoneDialog() {
|
||||
dialogStore.openBindPhone()
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
// 获取存储的用户和代理信息
|
||||
const userInfo = uni.getStorageSync('userInfo')
|
||||
if (userInfo) {
|
||||
try {
|
||||
const parsedUserInfo = typeof userInfo === 'string' ? JSON.parse(userInfo) : userInfo
|
||||
userStore.updateUserInfo(parsedUserInfo)
|
||||
}
|
||||
catch (e) {
|
||||
console.error('解析用户信息失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
const agentInfo = uni.getStorageSync('agentInfo')
|
||||
if (agentInfo) {
|
||||
try {
|
||||
const parsedAgentInfo = typeof agentInfo === 'string' ? JSON.parse(agentInfo) : agentInfo
|
||||
agentStore.updateAgentInfo(parsedAgentInfo)
|
||||
}
|
||||
catch (e) {
|
||||
console.error('解析代理信息失败', e)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="box-border min-h-screen">
|
||||
<view class="flex flex-col p-4 space-y-6">
|
||||
<!-- 用户信息卡片 -->
|
||||
<view
|
||||
class="group profile-section relative flex items-center gap-4 rounded-xl bg-white p-6 transition-all hover:shadow-xl"
|
||||
@click="!isLoggedIn ? redirectToLogin() : null">
|
||||
<view class="relative">
|
||||
<!-- 头像容器添加overflow-hidden解决边框问题 -->
|
||||
<view class="overflow-hidden rounded-full p-0.5" :class="levelGradient.border">
|
||||
<image :src="avatarDisplay" mode="aspectFill" alt="User Avatar"
|
||||
class="h-24 w-24 border-4 border-white rounded-full" @error="onAvatarError" />
|
||||
</view>
|
||||
|
||||
<!-- 代理标识 -->
|
||||
<view v-if="isAgent" class="absolute -bottom-2 -right-2">
|
||||
<view class="flex items-center justify-center rounded-full px-3 py-1 text-xs text-white font-bold shadow-sm"
|
||||
:class="levelGradient.badge">
|
||||
{{ levelNames[level] }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="space-y-1">
|
||||
<view class="text-2xl text-gray-800 font-bold">
|
||||
{{
|
||||
!isLoggedIn
|
||||
? "点击登录"
|
||||
: mobile
|
||||
? maskName(mobile)
|
||||
: isWeChat
|
||||
? "微信用户"
|
||||
: "未绑定手机号"
|
||||
}}
|
||||
</view>
|
||||
<!-- 手机号绑定提示 -->
|
||||
<template v-if="isLoggedIn && !mobile">
|
||||
<view class="cursor-pointer text-sm text-blue-500 hover:underline" @click.stop="showBindPhoneDialog">
|
||||
点击绑定手机号码
|
||||
</view>
|
||||
</template>
|
||||
<view v-if="isAgent" class="text-sm font-medium" :class="levelGradient.text">
|
||||
🎖️ {{ levelText[level] }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<VipBanner v-if="isAgent && (level === 'normal' || level === '')" />
|
||||
<!-- 功能菜单 -->
|
||||
<view>
|
||||
<view class="overflow-hidden rounded-xl bg-white shadow-sm">
|
||||
<template v-if="isAgent && ['VIP', 'SVIP'].includes(level)">
|
||||
<view
|
||||
class="w-full flex items-center justify-between border-b border-gray-100 px-6 py-4 transition-colors hover:bg-purple-50"
|
||||
@click="toVipConfig">
|
||||
<view class="flex items-center gap-3">
|
||||
<image src="/static/images/me/dlbgpz.png" class="h-6 w-6" alt="代理报告配置" />
|
||||
<text class="text-purple-700 font-medium">
|
||||
代理报告配置
|
||||
</text>
|
||||
</view>
|
||||
<image src="/static/images/me/right.png" class="h-4 w-4" alt="右箭头" />
|
||||
</view>
|
||||
<view
|
||||
class="w-full flex items-center justify-between border-b border-gray-100 px-6 py-4 transition-colors hover:bg-amber-50"
|
||||
@click="toVipRenewal">
|
||||
<view class="flex items-center gap-3">
|
||||
<image src="/static/images/me/xfhy.png" class="h-6 w-6" alt="代理会员" />
|
||||
<view class="flex flex-col items-start">
|
||||
<text class="text-amber-700 font-medium">
|
||||
续费代理会员
|
||||
</text>
|
||||
<text class="text-xs text-gray-500">
|
||||
有效期至 {{ formatExpiryTime(ExpiryTime) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<image src="/static/images/me/right.png" class="h-4 w-4" alt="右箭头" />
|
||||
</view>
|
||||
</template>
|
||||
<view
|
||||
class="w-full flex items-center justify-between border-b border-gray-100 px-6 py-4 transition-colors hover:bg-blue-50"
|
||||
@click="toPromote">
|
||||
<view class="flex items-center gap-3">
|
||||
<image src="/static/images/index/tgbg.png" class="h-6 w-6" alt="推广报告" />
|
||||
<text class="text-gray-700 font-medium">
|
||||
推广报告
|
||||
</text>
|
||||
</view>
|
||||
<image src="/static/images/me/right.png" class="h-4 w-4" alt="右箭头" />
|
||||
</view>
|
||||
<view
|
||||
class="w-full flex items-center justify-between border-b border-gray-100 px-6 py-4 transition-colors hover:bg-blue-50"
|
||||
@click="toInvitation">
|
||||
<view class="flex items-center gap-3">
|
||||
<image src="/static/images/index/yqhy.png" class="h-6 w-6" alt="邀请下级" />
|
||||
<text class="text-gray-700 font-medium">
|
||||
邀请下级
|
||||
</text>
|
||||
</view>
|
||||
<image src="/static/images/me/right.png" class="h-4 w-4" alt="右箭头" />
|
||||
</view>
|
||||
<view
|
||||
class="w-full flex items-center justify-between border-b border-gray-100 px-6 py-4 transition-colors hover:bg-blue-50"
|
||||
@click="toHistory">
|
||||
<view class="flex items-center gap-3">
|
||||
<image src="/static/images/index/wdbg.png" class="h-6 w-6" alt="我的报告" />
|
||||
<text class="text-gray-700 font-medium">
|
||||
我的报告
|
||||
</text>
|
||||
</view>
|
||||
<image src="/static/images/me/right.png" class="h-4 w-4" alt="右箭头" />
|
||||
</view>
|
||||
<view
|
||||
class="w-full flex items-center justify-between border-b border-gray-100 px-6 py-4 transition-colors hover:bg-blue-50"
|
||||
@click="toUserAgreement">
|
||||
<view class="flex items-center gap-3">
|
||||
<image src="/static/images/me/yhxy.png" class="h-6 w-6" alt="用户协议" />
|
||||
<text class="text-gray-700 font-medium">
|
||||
用户协议
|
||||
</text>
|
||||
</view>
|
||||
<image src="/static/images/me/right.png" class="h-4 w-4" alt="右箭头" />
|
||||
</view>
|
||||
<view
|
||||
class="w-full flex items-center justify-between border-b border-gray-100 px-6 py-4 transition-colors hover:bg-blue-50"
|
||||
@click="toPrivacyPolicy">
|
||||
<view class="flex items-center gap-3">
|
||||
<image src="/static/images/me/yszc.png" class="h-6 w-6" alt="隐私政策" />
|
||||
<text class="text-gray-700 font-medium">
|
||||
隐私政策
|
||||
</text>
|
||||
</view>
|
||||
<image src="/static/images/me/right.png" class="h-4 w-4" alt="右箭头" />
|
||||
</view>
|
||||
<view class="w-full flex items-center justify-between px-6 py-4 transition-colors hover:bg-blue-50"
|
||||
@click="toService">
|
||||
<view class="flex items-center gap-3">
|
||||
<image src="/static/images/me/lxkf.png" class="h-6 w-6" alt="联系客服" />
|
||||
<text class="text-gray-700 font-medium">
|
||||
联系客服
|
||||
</text>
|
||||
</view>
|
||||
<image src="/static/images/me/right.png" class="h-4 w-4" alt="右箭头" />
|
||||
</view>
|
||||
<view v-if="isLoggedIn && mobile"
|
||||
class="w-full flex items-center justify-between border-b border-gray-100 px-6 py-4 transition-colors hover:bg-red-50"
|
||||
@click="goCancelAccountPage">
|
||||
<view class="flex items-center gap-3">
|
||||
<image src="/static/images/me/cancelAccount.svg" class="h-6 w-6" alt="注销账号" />
|
||||
<text class="text-gray-700 font-medium">
|
||||
注销账号
|
||||
</text>
|
||||
</view>
|
||||
<image src="/static/images/me/right.png" class="h-4 w-4" alt="右箭头" />
|
||||
</view>
|
||||
<view v-if="isLoggedIn && !isWeChat"
|
||||
class="w-full flex items-center justify-between px-6 py-4 transition-colors hover:bg-red-50"
|
||||
@click="handleLogout">
|
||||
<view class="flex items-center gap-3">
|
||||
<image src="/static/images/me/tcdl.png" class="h-6 w-6" alt="退出登录" />
|
||||
<text class="text-gray-700 font-medium">
|
||||
退出登录
|
||||
</text>
|
||||
</view>
|
||||
<image src="/static/images/me/right.png" class="h-4 w-4" alt="右箭头" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
210
src/pages/not-found.vue
Normal file
210
src/pages/not-found.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { useSEO } from '@/composables/useSEO'
|
||||
definePage({ layout: false })
|
||||
|
||||
// SEO优化
|
||||
const { updateSEO } = useSEO()
|
||||
|
||||
onMounted(() => {
|
||||
const origin = import.meta.env.VITE_SITE_ORIGIN
|
||||
if (!origin)
|
||||
throw new Error('缺少环境变量: VITE_SITE_ORIGIN')
|
||||
const siteName = import.meta.env.VITE_SEO_SITE_NAME
|
||||
if (!siteName)
|
||||
throw new Error('缺少环境变量: VITE_SEO_SITE_NAME')
|
||||
|
||||
updateSEO({
|
||||
title: `404 - 页面未找到 | ${siteName}`,
|
||||
description: `抱歉,您访问的页面不存在。${siteName}专业大数据风险管控平台,提供大数据风险报告查询、婚姻状况查询、个人信用评估等服务。`,
|
||||
keywords: `404, 页面未找到, ${siteName}, 大数据风险管控`,
|
||||
url: `${origin}/404`,
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="not-found">
|
||||
<div class="not-found-content">
|
||||
<h1>404</h1>
|
||||
<h2>页面未找到</h2>
|
||||
<p>抱歉,您访问的页面不存在或已被移除。</p>
|
||||
<div class="suggestions">
|
||||
<h3>您可以尝试:</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<router-link to="/">
|
||||
返回首页
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/help">
|
||||
查看帮助中心
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/service">
|
||||
联系客服
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<router-link to="/" class="home-link">
|
||||
返回首页
|
||||
</router-link>
|
||||
<router-link to="/help" class="help-link">
|
||||
帮助中心
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.not-found {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.not-found-content {
|
||||
background: white;
|
||||
padding: 60px 40px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.not-found h1 {
|
||||
font-size: 120px;
|
||||
color: #667eea;
|
||||
margin: 0 0 20px 0;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.not-found h2 {
|
||||
font-size: 32px;
|
||||
color: #333;
|
||||
margin: 0 0 20px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.not-found p {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
margin: 30px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.suggestions h3 {
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.suggestions ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.suggestions li {
|
||||
margin: 10px 0;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.suggestions li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.suggestions a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.suggestions a:hover {
|
||||
color: #5a6fd8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 40px;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.home-link, .help-link {
|
||||
display: inline-block;
|
||||
padding: 15px 30px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
border-radius: 50px;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.home-link {
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.home-link:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.help-link {
|
||||
color: #667eea;
|
||||
background: white;
|
||||
border: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.help-link:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.not-found-content {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.not-found h1 {
|
||||
font-size: 80px;
|
||||
}
|
||||
|
||||
.not-found h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.home-link, .help-link {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
549
src/pages/payment-result.vue
Normal file
549
src/pages/payment-result.vue
Normal file
@@ -0,0 +1,549 @@
|
||||
<script setup>
|
||||
import {
|
||||
computed,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
ref,
|
||||
} from 'vue'
|
||||
import { openCustomerService } from '@/composables/useCustomerService'
|
||||
import { useAgentStore } from '@/stores/agentStore'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
|
||||
definePage({ layout: 'default' })
|
||||
|
||||
const orderNo = ref('')
|
||||
const agentStore = useAgentStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 状态变量
|
||||
const isLoading = ref(true)
|
||||
const paymentResult = ref(null)
|
||||
const paymentType = ref('')
|
||||
const paymentStatus = ref('')
|
||||
const isApiError = ref(false)
|
||||
const pollingInterval = ref(null)
|
||||
const pollingCount = ref(0)
|
||||
const maxPollingCount = 30 // 最大轮询次数
|
||||
const baseInterval = 2000 // 基础轮询间隔(2秒)
|
||||
|
||||
// 计算属性
|
||||
const statusText = computed(() => {
|
||||
if (isApiError.value) {
|
||||
return '系统繁忙'
|
||||
}
|
||||
|
||||
switch (paymentStatus.value) {
|
||||
case 'pending':
|
||||
return '正在支付'
|
||||
case 'failed':
|
||||
return '支付失败'
|
||||
case 'closed':
|
||||
return '订单已关闭'
|
||||
default:
|
||||
return '处理中'
|
||||
}
|
||||
})
|
||||
|
||||
const statusMessage = computed(() => {
|
||||
if (isApiError.value) {
|
||||
return '系统正在维护或网络繁忙,请稍后再试,或联系客服确认订单状态。'
|
||||
}
|
||||
|
||||
switch (paymentStatus.value) {
|
||||
case 'pending':
|
||||
return '您的订单正在支付,请稍后'
|
||||
case 'failed':
|
||||
return '支付未成功,您可以返回重新支付,或联系客服确认详情。'
|
||||
case 'closed':
|
||||
return '订单已关闭,如有疑问请联系客服。'
|
||||
default:
|
||||
return '系统正在处理您的订单,如有疑问请联系客服。'
|
||||
}
|
||||
})
|
||||
|
||||
// 状态图标
|
||||
const getStatusIcon = computed(() => {
|
||||
if (isApiError.value) {
|
||||
return '⚠️'
|
||||
}
|
||||
return paymentStatus.value === 'pending' ? '⏳' : '❌'
|
||||
})
|
||||
|
||||
// 状态颜色
|
||||
const getStatusColor = computed(() => {
|
||||
if (isApiError.value) {
|
||||
return '#ff9800' // 橙色警告
|
||||
}
|
||||
return paymentStatus.value === 'pending' ? '#ff976a' : '#ee0a24'
|
||||
})
|
||||
|
||||
// 环形样式类
|
||||
const getRingClass = computed(() => {
|
||||
if (isApiError.value) {
|
||||
return 'api-error-ring'
|
||||
}
|
||||
return {
|
||||
'pending-ring': paymentStatus.value === 'pending',
|
||||
'failed-ring': paymentStatus.value === 'failed',
|
||||
'closed-ring': paymentStatus.value === 'closed',
|
||||
}
|
||||
})
|
||||
|
||||
// 状态文本样式
|
||||
const getStatusTextClass = computed(() => {
|
||||
if (isApiError.value) {
|
||||
return 'text-amber-600'
|
||||
}
|
||||
return {
|
||||
'text-orange-500': paymentStatus.value === 'pending',
|
||||
'text-red-500':
|
||||
paymentStatus.value === 'failed'
|
||||
|| paymentStatus.value === 'closed',
|
||||
}
|
||||
})
|
||||
|
||||
// 消息文本样式
|
||||
const getMessageClass = computed(() => {
|
||||
if (isApiError.value) {
|
||||
return 'text-amber-800'
|
||||
}
|
||||
return {
|
||||
'text-orange-700': paymentStatus.value === 'pending',
|
||||
'text-red-700':
|
||||
paymentStatus.value === 'failed'
|
||||
|| paymentStatus.value === 'closed',
|
||||
}
|
||||
})
|
||||
|
||||
// 计算轮询间隔时间(渐进式增加)
|
||||
const getPollingInterval = computed(() => {
|
||||
// 每5次轮询增加1秒,最大间隔10秒
|
||||
const increment = Math.floor(pollingCount.value / 5)
|
||||
return Math.min(baseInterval + increment * 1000, 10000)
|
||||
})
|
||||
|
||||
// 检查支付状态
|
||||
async function checkPaymentStatus() {
|
||||
if (pollingCount.value >= maxPollingCount) {
|
||||
// 超过最大轮询次数,停止轮询
|
||||
stopPolling()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error } = await useApiFetch(`/pay/check`)
|
||||
.post({
|
||||
order_no: orderNo.value,
|
||||
})
|
||||
.json()
|
||||
|
||||
if (data.value && !error.value) {
|
||||
paymentResult.value = data.value.data
|
||||
paymentType.value = data.value.data.type || ''
|
||||
const newStatus = data.value.data.status || ''
|
||||
|
||||
// 状态发生变化时更新
|
||||
if (paymentStatus.value !== newStatus) {
|
||||
paymentStatus.value = newStatus
|
||||
|
||||
// 对于查询类型,如果状态是已支付或已退款,直接跳转
|
||||
if (
|
||||
paymentType.value === 'query'
|
||||
&& (newStatus === 'paid' || newStatus === 'refunded')
|
||||
) {
|
||||
stopPolling()
|
||||
uni.redirectTo({
|
||||
url: `/pages/report-result-webview?orderNo=${encodeURIComponent(orderNo.value)}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果状态不是 pending,停止轮询
|
||||
if (newStatus !== 'pending') {
|
||||
stopPolling()
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.error('API调用失败:', error.value)
|
||||
// 不要立即停止轮询,继续尝试
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('验证支付状态失败:', err)
|
||||
// 不要立即停止轮询,继续尝试
|
||||
}
|
||||
finally {
|
||||
pollingCount.value++
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 开始轮询
|
||||
function startPolling() {
|
||||
if (pollingInterval.value)
|
||||
return
|
||||
|
||||
pollingCount.value = 0
|
||||
const poll = () => {
|
||||
checkPaymentStatus()
|
||||
if (
|
||||
paymentStatus.value === 'pending'
|
||||
&& pollingCount.value < maxPollingCount
|
||||
) {
|
||||
pollingInterval.value = setTimeout(poll, getPollingInterval.value)
|
||||
}
|
||||
}
|
||||
poll()
|
||||
}
|
||||
|
||||
// 停止轮询
|
||||
function stopPolling() {
|
||||
if (pollingInterval.value) {
|
||||
clearTimeout(pollingInterval.value)
|
||||
pollingInterval.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 在组件挂载前验证支付结果
|
||||
onLoad(async (query) => {
|
||||
const q = query || {}
|
||||
orderNo.value = String(q.out_trade_no || q.orderNo || '')
|
||||
if (!orderNo.value) {
|
||||
uni.reLaunch({ url: '/pages/index' })
|
||||
return
|
||||
}
|
||||
// 首次检查支付状态
|
||||
await checkPaymentStatus()
|
||||
|
||||
// 如果状态是 pending,开始轮询
|
||||
if (paymentStatus.value === 'pending') {
|
||||
startPolling()
|
||||
}
|
||||
})
|
||||
|
||||
// 组件卸载前清理轮询
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling()
|
||||
})
|
||||
|
||||
// 处理导航逻辑
|
||||
function handleNavigation() {
|
||||
if (paymentType.value === 'agent_vip') {
|
||||
// 跳转到代理会员页面
|
||||
uni.switchTab({ url: '/pages/agent' })
|
||||
agentStore.fetchAgentStatus()
|
||||
userStore.fetchUserInfo()
|
||||
}
|
||||
else {
|
||||
// 跳转到查询结果页面
|
||||
uni.redirectTo({
|
||||
url: `/pages/report-result-webview?orderNo=${encodeURIComponent(orderNo.value)}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 返回首页
|
||||
function goHome() {
|
||||
uni.reLaunch({ url: '/pages/index' })
|
||||
}
|
||||
|
||||
// 联系客服
|
||||
function contactService() {
|
||||
openCustomerService()
|
||||
}
|
||||
|
||||
// 暴露方法和数据供父组件或路由调用
|
||||
defineExpose({
|
||||
paymentResult,
|
||||
paymentType,
|
||||
paymentStatus,
|
||||
handleNavigation,
|
||||
stopPolling, // 暴露停止轮询方法
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="payment-result-container flex flex-col items-center p-6">
|
||||
<!-- 加载动画,验证支付结果时显示 -->
|
||||
<div v-if="isLoading" class="w-full">
|
||||
<div class="flex flex-col items-center justify-center py-10">
|
||||
<view class="loading-spinner" />
|
||||
<p class="mt-4 text-lg text-gray-600">
|
||||
正在处理支付结果...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 支付结果展示 -->
|
||||
<div v-else class="w-full">
|
||||
<!-- 支付成功 -->
|
||||
<div v-if="paymentStatus === 'paid'" class="success-result">
|
||||
<div class="success-animation mb-6">
|
||||
<text class="status-icon">
|
||||
✅
|
||||
</text>
|
||||
<div class="success-ring" />
|
||||
</div>
|
||||
|
||||
<h1 class="mb-4 text-center text-2xl text-gray-800 font-bold">
|
||||
支付成功
|
||||
</h1>
|
||||
|
||||
<div class="payment-info mb-6 w-full rounded-lg bg-white p-6 shadow-md">
|
||||
<div class="mb-4 flex justify-between">
|
||||
<span class="text-gray-600">订单编号</span>
|
||||
<span class="text-gray-800">{{ orderNo }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">支付类型</span>
|
||||
<span class="text-gray-800">{{
|
||||
paymentType === "agent_vip"
|
||||
? "代理会员"
|
||||
: "查询服务"
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="paymentType === 'agent_vip'" class="mb-4 text-center text-gray-600">
|
||||
恭喜你成为高级代理会员,享受更多权益
|
||||
</div>
|
||||
|
||||
<div class="action-buttons grid grid-cols-1 gap-4">
|
||||
<wd-button block type="primary" class="rounded-lg" @click="handleNavigation">
|
||||
{{
|
||||
paymentType === "agent_vip"
|
||||
? "查看会员权益"
|
||||
: "查看查询结果"
|
||||
}}
|
||||
</wd-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 退款状态 -->
|
||||
<div v-else-if="paymentStatus === 'refunded'" class="refund-result">
|
||||
<div v-if="paymentType === 'query'" class="success-animation mb-6">
|
||||
<text class="status-icon">
|
||||
✅
|
||||
</text>
|
||||
<div class="success-ring" />
|
||||
</div>
|
||||
<div v-else class="info-animation mb-6">
|
||||
<text class="status-icon">
|
||||
ℹ️
|
||||
</text>
|
||||
<div class="info-ring" />
|
||||
</div>
|
||||
|
||||
<h1 class="mb-4 text-center text-2xl text-gray-800 font-bold">
|
||||
{{ paymentType === "query" ? "已处理" : "订单已退款" }}
|
||||
</h1>
|
||||
|
||||
<div class="payment-info mb-6 w-full rounded-lg bg-white p-6 shadow-md">
|
||||
<div class="mb-4 flex justify-between">
|
||||
<span class="text-gray-600">订单编号</span>
|
||||
<span class="text-gray-800">{{ orderNo }}</span>
|
||||
</div>
|
||||
<div class="mb-4 flex justify-between">
|
||||
<span class="text-gray-600">支付类型</span>
|
||||
<span class="text-gray-800">{{
|
||||
paymentType === "agent_vip"
|
||||
? "代理会员"
|
||||
: "查询服务"
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">订单状态</span>
|
||||
<span class="text-blue-600">已退款</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="paymentType === 'query'" class="action-buttons grid grid-cols-1 gap-4">
|
||||
<wd-button block type="primary" class="rounded-lg" @click="handleNavigation">
|
||||
查看查询结果
|
||||
</wd-button>
|
||||
</div>
|
||||
|
||||
<div v-else class="message-box mb-6 rounded-lg bg-blue-50 p-4">
|
||||
<p class="text-center text-blue-800">
|
||||
您的代理会员费用已退款,如有疑问请联系客服。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="paymentType === 'agent_vip'" class="action-buttons grid grid-cols-1 gap-4">
|
||||
<wd-button block type="primary" class="rounded-lg" @click="contactService">
|
||||
联系客服
|
||||
</wd-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他状态(待支付、失败、关闭) -->
|
||||
<div v-else class="other-result">
|
||||
<div class="info-animation mb-6">
|
||||
<text class="status-icon" :style="{ color: getStatusColor }">
|
||||
{{ getStatusIcon }}
|
||||
</text>
|
||||
<div class="info-ring" :class="getRingClass" />
|
||||
</div>
|
||||
|
||||
<h1 class="mb-4 text-center text-2xl text-gray-800 font-bold">
|
||||
{{ statusText }}
|
||||
</h1>
|
||||
|
||||
<!-- 添加轮询状态提示 -->
|
||||
<div v-if="paymentStatus === 'pending'" class="mb-4 text-center text-gray-500">
|
||||
<p>正在等待支付结果,请稍候...</p>
|
||||
<p class="mt-1 text-sm">
|
||||
已等待
|
||||
{{
|
||||
Math.floor(
|
||||
(pollingCount * getPollingInterval) / 1000,
|
||||
)
|
||||
}}
|
||||
秒
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="payment-info mb-6 w-full rounded-lg bg-white p-6 shadow-md">
|
||||
<div class="mb-4 flex justify-between">
|
||||
<span class="text-gray-600">订单编号</span>
|
||||
<span class="text-gray-800">{{ orderNo }}</span>
|
||||
</div>
|
||||
<div v-if="!isApiError" class="mb-4 flex justify-between">
|
||||
<span class="text-gray-600">支付类型</span>
|
||||
<span class="text-gray-800">{{
|
||||
paymentType === "agent_vip"
|
||||
? "代理会员"
|
||||
: "查询服务"
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">订单状态</span>
|
||||
<span :class="getStatusTextClass">{{
|
||||
statusText
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-box mb-6 rounded-lg bg-blue-50 p-4">
|
||||
<p class="text-center" :class="getMessageClass">
|
||||
{{ statusMessage }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons grid grid-cols-2 gap-4">
|
||||
<wd-button block type="info" class="rounded-lg" @click="goHome">
|
||||
返回首页
|
||||
</wd-button>
|
||||
<wd-button block type="primary" class="rounded-lg" @click="contactService">
|
||||
联系客服
|
||||
</wd-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.payment-result-container {
|
||||
min-height: 80vh;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #dbeafe;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 9999px;
|
||||
animation: spin 0.9s linear infinite;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 56px;
|
||||
line-height: 1;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.success-animation,
|
||||
.info-animation {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
.success-ring,
|
||||
.info-ring,
|
||||
.pending-ring,
|
||||
.failed-ring,
|
||||
.closed-ring,
|
||||
.api-error-ring {
|
||||
position: absolute;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.success-ring {
|
||||
border: 2px solid #07c160;
|
||||
}
|
||||
|
||||
.info-ring {
|
||||
border: 2px solid #1989fa;
|
||||
}
|
||||
|
||||
.pending-ring {
|
||||
border: 2px solid #ff976a;
|
||||
}
|
||||
|
||||
.failed-ring,
|
||||
.closed-ring {
|
||||
border: 2px solid #ee0a24;
|
||||
}
|
||||
|
||||
.api-error-ring {
|
||||
border: 2px solid #ff9800;
|
||||
/* 橙色警告 */
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.95);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.95);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.success-result,
|
||||
.refund-result,
|
||||
.other-result {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
15
src/pages/privacy-policy.vue
Normal file
15
src/pages/privacy-policy.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { useReportWebview } from '@/composables/useReportWebview'
|
||||
definePage({ layout: false })
|
||||
|
||||
const { buildSitePathUrl } = useReportWebview()
|
||||
const src = ref('')
|
||||
|
||||
onLoad(() => {
|
||||
src.value = buildSitePathUrl('/app/privacyPolicy')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<web-view :src="src" />
|
||||
</template>
|
||||
225
src/pages/promote.vue
Normal file
225
src/pages/promote.vue
Normal file
@@ -0,0 +1,225 @@
|
||||
<script setup>
|
||||
import PriceInputPopup from '@/components/PriceInputPopup.vue'
|
||||
import VipBanner from '@/components/VipBanner.vue'
|
||||
|
||||
definePage({ layout: 'default' })
|
||||
|
||||
const showPricePicker = ref(false)
|
||||
const pickerProductConfig = ref(null)
|
||||
const productConfig = ref(null)
|
||||
const linkIdentifier = ref('')
|
||||
const showQRcode = ref(false)
|
||||
const promotionForm = ref(null)
|
||||
const loadingConfig = ref(false)
|
||||
const generating = ref(false)
|
||||
|
||||
const formData = ref({
|
||||
productType: '',
|
||||
clientPrice: null,
|
||||
})
|
||||
|
||||
const availableReportTypes = computed(() => {
|
||||
if (!productConfig.value?.length)
|
||||
return []
|
||||
return productConfig.value
|
||||
.map(item => ({
|
||||
id: item.product_id,
|
||||
label: item.product_name || `产品${item.product_id}`,
|
||||
value: item.product_en || '',
|
||||
}))
|
||||
.filter(item => !!item.value)
|
||||
})
|
||||
|
||||
const costPrice = computed(() => {
|
||||
if (!pickerProductConfig.value)
|
||||
return '0.00'
|
||||
let platformPricing = 0
|
||||
platformPricing += pickerProductConfig.value.cost_price
|
||||
if (formData.value.clientPrice > pickerProductConfig.value.p_pricing_standard) {
|
||||
platformPricing += (formData.value.clientPrice - pickerProductConfig.value.p_pricing_standard) * pickerProductConfig.value.p_overpricing_ratio
|
||||
}
|
||||
|
||||
if (pickerProductConfig.value.a_pricing_standard > platformPricing && pickerProductConfig.value.a_pricing_end > platformPricing && pickerProductConfig.value.a_overpricing_ratio > 0) {
|
||||
if (formData.value.clientPrice > pickerProductConfig.value.a_pricing_standard) {
|
||||
if (formData.value.clientPrice > pickerProductConfig.value.a_pricing_end) {
|
||||
platformPricing += (pickerProductConfig.value.a_pricing_end - pickerProductConfig.value.a_pricing_standard) * pickerProductConfig.value.a_overpricing_ratio
|
||||
}
|
||||
else {
|
||||
platformPricing += (formData.value.clientPrice - pickerProductConfig.value.a_pricing_standard) * pickerProductConfig.value.a_overpricing_ratio
|
||||
}
|
||||
}
|
||||
}
|
||||
return safeTruncate(platformPricing)
|
||||
})
|
||||
|
||||
const promotionRevenue = computed(() => {
|
||||
return safeTruncate(formData.value.clientPrice - costPrice.value)
|
||||
})
|
||||
|
||||
function safeTruncate(num, decimals = 2) {
|
||||
if (Number.isNaN(num) || !Number.isFinite(num))
|
||||
return '0.00'
|
||||
const factor = 10 ** decimals
|
||||
const scaled = Math.trunc(num * factor)
|
||||
return (scaled / factor).toFixed(decimals)
|
||||
}
|
||||
|
||||
function selectProductType(reportTypeValue) {
|
||||
const reportType = availableReportTypes.value.find(item => item.id === reportTypeValue || item.value === reportTypeValue)
|
||||
if (!reportType)
|
||||
return
|
||||
|
||||
formData.value.productType = reportType.value
|
||||
|
||||
const matchedConfig = productConfig.value?.find(item => item.product_id === reportType.id)
|
||||
if (!matchedConfig) {
|
||||
pickerProductConfig.value = null
|
||||
formData.value.clientPrice = null
|
||||
uni.showToast({ title: '该报告暂不可推广', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
pickerProductConfig.value = matchedConfig
|
||||
formData.value.clientPrice = matchedConfig.cost_price
|
||||
}
|
||||
|
||||
function onConfirmType(e) {
|
||||
const nextValue = e?.value?.[0] ?? e?.selectedOptions?.[0]?.value
|
||||
if (nextValue)
|
||||
selectProductType(nextValue)
|
||||
}
|
||||
|
||||
function onPriceChange(price) {
|
||||
formData.value.clientPrice = price
|
||||
}
|
||||
|
||||
function getPricePayload() {
|
||||
return safeTruncate(Number(formData.value.clientPrice))
|
||||
}
|
||||
|
||||
function openPricePicker() {
|
||||
if (!pickerProductConfig.value) {
|
||||
uni.showToast({ title: '请先选择报告类型', icon: 'none' })
|
||||
return
|
||||
}
|
||||
showPricePicker.value = true
|
||||
}
|
||||
|
||||
async function getPromoteConfig() {
|
||||
loadingConfig.value = true
|
||||
try {
|
||||
const { data, error } = await useApiFetch('/agent/product_config').get().json()
|
||||
if (data.value && !error.value && data.value.code === 200) {
|
||||
const list = data.value.data.AgentProductConfig || []
|
||||
productConfig.value = list
|
||||
const availableType = availableReportTypes.value[0]
|
||||
if (availableType) {
|
||||
selectProductType(availableType.value)
|
||||
}
|
||||
else {
|
||||
pickerProductConfig.value = null
|
||||
formData.value.productType = ''
|
||||
formData.value.clientPrice = null
|
||||
uni.showToast({ title: '暂无可推广产品', icon: 'none' })
|
||||
}
|
||||
return
|
||||
}
|
||||
uni.showToast({ title: data.value?.msg || '获取配置失败', icon: 'none' })
|
||||
}
|
||||
catch {
|
||||
uni.showToast({ title: '获取配置失败', icon: 'none' })
|
||||
}
|
||||
finally {
|
||||
loadingConfig.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function generatePromotionCode() {
|
||||
if (generating.value)
|
||||
return
|
||||
try {
|
||||
await promotionForm.value.validate()
|
||||
}
|
||||
catch {
|
||||
return
|
||||
}
|
||||
generating.value = true
|
||||
try {
|
||||
const { data, error } = await useApiFetch('/agent/generating_link')
|
||||
.post({ product: formData.value.productType, price: getPricePayload() })
|
||||
.json()
|
||||
if (data.value && !error.value && data.value.code === 200) {
|
||||
linkIdentifier.value = data.value.data.link_identifier
|
||||
showQRcode.value = true
|
||||
}
|
||||
else {
|
||||
uni.showToast({ title: data.value?.msg || '生成推广码失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
catch {
|
||||
uni.showToast({ title: '生成推广码失败', icon: 'none' })
|
||||
}
|
||||
finally {
|
||||
generating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getPromoteConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="promote min-h-screen p-4">
|
||||
<view class="card mb-4 from-orange-200 to-orange-200/80 !bg-gradient-to-b">
|
||||
<view class="text-lg text-orange-500 font-bold">
|
||||
直推用户查询
|
||||
</view>
|
||||
<view class="mt-1 text-orange-400 font-bold">
|
||||
自定义价格,赚取差价
|
||||
</view>
|
||||
<view class="mt-6 rounded-xl bg-orange-100 px-4 py-2 text-gray-600">
|
||||
在下方 “自定义价格” 处选择报告类型,设置客户查询价(元)即可立即推广。
|
||||
</view>
|
||||
</view>
|
||||
<VipBanner />
|
||||
|
||||
<view class="card mb-4">
|
||||
<view class="mb-2 text-xl font-semibold">
|
||||
生成推广码
|
||||
</view>
|
||||
<wd-form ref="promotionForm" :model="formData">
|
||||
<wd-cell-group border>
|
||||
<wd-picker v-model="formData.productType" label="报告类型" label-width="100px" title="选择报告类型"
|
||||
:columns="[availableReportTypes]" prop="productType" placeholder="请选择报告类型"
|
||||
:rules="[{ required: true, message: '请选择报告类型' }]" @confirm="onConfirmType" />
|
||||
<wd-input v-model="formData.clientPrice" label="客户查询价" label-width="100px" placeholder="请输入价格"
|
||||
prop="clientPrice" clickable readonly suffix-icon="arrow-right"
|
||||
:rules="[{ required: true, message: '请输入客户查询价' }]" @click="openPricePicker" />
|
||||
</wd-cell-group>
|
||||
<view class="my-2 flex items-center justify-between text-sm text-gray-500">
|
||||
<view>
|
||||
推广收益为 <text class="text-orange-500">
|
||||
{{ promotionRevenue }}
|
||||
</text> 元
|
||||
</view>
|
||||
<view>
|
||||
我的成本为 <text class="text-orange-500">
|
||||
{{ costPrice }}
|
||||
</text> 元
|
||||
</view>
|
||||
</view>
|
||||
</wd-form>
|
||||
<wd-button type="primary" block :loading="generating || loadingConfig"
|
||||
:disabled="loadingConfig || !pickerProductConfig" @click="generatePromotionCode">
|
||||
点击立即推广
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<PriceInputPopup v-model:show="showPricePicker" :default-price="formData.clientPrice"
|
||||
:product-config="pickerProductConfig" @change="onPriceChange" />
|
||||
<QRcode v-model:show="showQRcode" :link-identifier="linkIdentifier" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
58
src/pages/promotion-inquire.vue
Normal file
58
src/pages/promotion-inquire.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import InquireForm from '@/components/InquireForm.vue'
|
||||
import LoginDialog from '@/components/LoginDialog.vue'
|
||||
import { useRoute } from '@/composables/uni-router'
|
||||
|
||||
definePage({ layout: 'default' })
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const linkIdentifier = ref('')
|
||||
const feature = ref('')
|
||||
const featureData = ref({})
|
||||
|
||||
onLoad(async (query) => {
|
||||
const q = query || {}
|
||||
if (q.out_trade_no) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/report-result-webview?out_trade_no=${encodeURIComponent(String(q.out_trade_no))}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
await getProduct()
|
||||
})
|
||||
|
||||
async function getProduct() {
|
||||
linkIdentifier.value = route.params.linkIdentifier
|
||||
const { data: agentLinkData, error: agentLinkError } = await useApiFetch(
|
||||
`/agent/link?link_identifier=${linkIdentifier.value}`,
|
||||
)
|
||||
.get()
|
||||
.json()
|
||||
if (agentLinkData.value && !agentLinkError.value) {
|
||||
if (agentLinkData.value.code === 200) {
|
||||
feature.value = agentLinkData.value.data.product_en
|
||||
featureData.value = agentLinkData.value.data
|
||||
// 确保 FLXG0V4B 排在首位
|
||||
if (
|
||||
featureData.value.features
|
||||
&& featureData.value.features.length > 0
|
||||
) {
|
||||
featureData.value.features.sort((a, b) => {
|
||||
if (a.api_id === 'FLXG0V4B')
|
||||
return -1
|
||||
if (b.api_id === 'FLXG0V4B')
|
||||
return 1
|
||||
return 0
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<InquireForm type="promotion" :feature="feature" :link-identifier="linkIdentifier" :feature-data="featureData" />
|
||||
<LoginDialog />
|
||||
</template>
|
||||
17
src/pages/report-example-webview.vue
Normal file
17
src/pages/report-example-webview.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { useReportWebview } from '@/composables/useReportWebview'
|
||||
definePage({ layout: false })
|
||||
|
||||
const { buildReportUrl } = useReportWebview()
|
||||
const src = ref('')
|
||||
|
||||
onLoad((query) => {
|
||||
src.value = buildReportUrl('example', {
|
||||
feature: String(query?.feature || ''),
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<web-view :src="src" />
|
||||
</template>
|
||||
28
src/pages/report-result-webview.vue
Normal file
28
src/pages/report-result-webview.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { useReportWebview } from '@/composables/useReportWebview'
|
||||
import { ensureCurrentPageAccess } from '@/composables/useNavigationAuthGuard'
|
||||
|
||||
definePage({ layout: false })
|
||||
|
||||
const { buildReportUrl } = useReportWebview()
|
||||
const src = ref('')
|
||||
|
||||
onLoad((query) => {
|
||||
const q = query || {}
|
||||
src.value = buildReportUrl('report', {
|
||||
orderNo: String(q.orderNo || ''),
|
||||
order_id: String(q.order_id || ''),
|
||||
out_trade_no: String(q.out_trade_no || ''),
|
||||
feature: String(q.feature || ''),
|
||||
})
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
ensureCurrentPageAccess('redirect')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<web-view :src="src" />
|
||||
</template>
|
||||
16
src/pages/report-share.vue
Normal file
16
src/pages/report-share.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
import { useReportWebview } from '@/composables/useReportWebview'
|
||||
definePage({ layout: false })
|
||||
|
||||
const { buildReportShareUrl } = useReportWebview()
|
||||
const src = ref('')
|
||||
|
||||
onLoad((query = {}) => {
|
||||
const linkIdentifier = String(query?.linkIdentifier || '')
|
||||
src.value = buildReportShareUrl(linkIdentifier)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<web-view :src="src" />
|
||||
</template>
|
||||
353
src/pages/subordinate-detail.vue
Normal file
353
src/pages/subordinate-detail.vue
Normal file
@@ -0,0 +1,353 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from '@/composables/uni-router'
|
||||
import useApiFetch from '@/composables/useApiFetch'
|
||||
definePage({ layout: 'default', auth: true })
|
||||
|
||||
const route = useRoute()
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = 8
|
||||
const total = ref(0)
|
||||
const rewardDetails = ref([])
|
||||
const userInfo = ref({})
|
||||
const summary = ref({})
|
||||
const statistics = ref([])
|
||||
|
||||
// 获取收益列表
|
||||
async function fetchRewardDetails() {
|
||||
if (loading.value)
|
||||
return
|
||||
|
||||
loading.value = true
|
||||
const { data, error } = await useApiFetch(
|
||||
`/agent/subordinate/contribution/detail?subordinate_id=${route.params.id}&page=${page.value}&page_size=${pageSize}`,
|
||||
)
|
||||
.get()
|
||||
.json()
|
||||
|
||||
if (data.value && !error.value) {
|
||||
if (data.value.code === 200) {
|
||||
if (page.value === 1) {
|
||||
// 更新用户信息
|
||||
userInfo.value = {
|
||||
createTime: data.value.data.create_time,
|
||||
level: data.value.data.level_name || '普通',
|
||||
mobile: data.value.data.mobile,
|
||||
}
|
||||
// 更新汇总数据
|
||||
summary.value = {
|
||||
totalReward: data.value.data.total_earnings,
|
||||
totalContribution: data.value.data.total_contribution,
|
||||
totalOrders: data.value.data.total_orders,
|
||||
}
|
||||
// 设置默认的统计类型
|
||||
statistics.value = [
|
||||
{
|
||||
type: 'descendant_promotion',
|
||||
amount: 0,
|
||||
count: 0,
|
||||
description: '推广奖励',
|
||||
},
|
||||
{
|
||||
type: 'cost',
|
||||
amount: 0,
|
||||
count: 0,
|
||||
description: '成本贡献',
|
||||
},
|
||||
{
|
||||
type: 'pricing',
|
||||
amount: 0,
|
||||
count: 0,
|
||||
description: '定价贡献',
|
||||
},
|
||||
{
|
||||
type: 'descendant_withdraw',
|
||||
amount: 0,
|
||||
count: 0,
|
||||
description: '提现收益',
|
||||
},
|
||||
{
|
||||
type: 'descendant_upgrade_vip',
|
||||
amount: 0,
|
||||
count: 0,
|
||||
description: '转化VIP奖励',
|
||||
},
|
||||
{
|
||||
type: 'descendant_upgrade_svip',
|
||||
amount: 0,
|
||||
count: 0,
|
||||
description: '转化SVIP奖励',
|
||||
},
|
||||
]
|
||||
// 如果有统计数据,更新对应的值
|
||||
if (data.value.data.stats) {
|
||||
const stats = data.value.data.stats
|
||||
// 更新推广奖励
|
||||
const platformStat = statistics.value.find(s => s.type === 'descendant_promotion')
|
||||
if (platformStat) {
|
||||
platformStat.amount = stats.descendant_promotion_amount || 0
|
||||
platformStat.count = stats.descendant_promotion_count || 0
|
||||
}
|
||||
// 更新成本贡献
|
||||
const costStat = statistics.value.find(s => s.type === 'cost')
|
||||
if (costStat) {
|
||||
costStat.amount = stats.cost_amount || 0
|
||||
costStat.count = stats.cost_count || 0
|
||||
}
|
||||
// 更新定价贡献
|
||||
const pricingStat = statistics.value.find(s => s.type === 'pricing')
|
||||
if (pricingStat) {
|
||||
pricingStat.amount = stats.pricing_amount || 0
|
||||
pricingStat.count = stats.pricing_count || 0
|
||||
}
|
||||
// 更新提现收益
|
||||
const withdrawStat = statistics.value.find(s => s.type === 'descendant_withdraw')
|
||||
if (withdrawStat) {
|
||||
withdrawStat.amount = stats.descendant_withdraw_amount || 0
|
||||
withdrawStat.count = stats.descendant_withdraw_count || 0
|
||||
}
|
||||
// 更新转化VIP奖励
|
||||
const conversionVipStat = statistics.value.find(s => s.type === 'descendant_upgrade_vip')
|
||||
if (conversionVipStat) {
|
||||
conversionVipStat.amount = stats.descendant_upgrade_vip_amount || 0
|
||||
conversionVipStat.count = stats.descendant_upgrade_vip_count || 0
|
||||
}
|
||||
// 更新转化SVIP奖励
|
||||
const conversionSvipStat = statistics.value.find(s => s.type === 'descendant_upgrade_svip')
|
||||
if (conversionSvipStat) {
|
||||
conversionSvipStat.amount = stats.descendant_upgrade_svip_amount || 0
|
||||
conversionSvipStat.count = stats.descendant_upgrade_svip_count || 0
|
||||
}
|
||||
}
|
||||
rewardDetails.value = []
|
||||
}
|
||||
total.value = data.value.data.total || 0
|
||||
// 处理列表数据
|
||||
if (data.value.data.list) {
|
||||
const list = data.value.data.list
|
||||
rewardDetails.value = list
|
||||
}
|
||||
else {
|
||||
rewardDetails.value = []
|
||||
}
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function onPageChange({ value }) {
|
||||
page.value = value
|
||||
fetchRewardDetails()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRewardDetails()
|
||||
})
|
||||
|
||||
// 获取收益类型样式
|
||||
function getRewardTypeClass(type) {
|
||||
const typeMap = {
|
||||
descendant_promotion: 'bg-blue-100 text-blue-600',
|
||||
cost: 'bg-green-100 text-green-600',
|
||||
pricing: 'bg-purple-100 text-purple-600',
|
||||
descendant_withdraw: 'bg-yellow-100 text-yellow-600',
|
||||
descendant_upgrade_vip: 'bg-red-100 text-red-600',
|
||||
descendant_upgrade_svip: 'bg-orange-100 text-orange-600',
|
||||
}
|
||||
return typeMap[type] || 'bg-gray-100 text-gray-600'
|
||||
}
|
||||
|
||||
// 获取收益类型图标
|
||||
function getRewardTypeIcon(type) {
|
||||
const iconMap = {
|
||||
descendant_promotion: 'gift',
|
||||
cost: 'gold-coin',
|
||||
pricing: 'balance-pay',
|
||||
descendant_withdraw: 'cash-back-record',
|
||||
descendant_upgrade_vip: 'fire',
|
||||
descendant_upgrade_svip: 'fire',
|
||||
}
|
||||
return iconMap[type] || 'balance-o'
|
||||
}
|
||||
|
||||
// 获取收益类型描述
|
||||
function getRewardTypeDescription(type) {
|
||||
const descriptionMap = {
|
||||
descendant_promotion: '推广奖励',
|
||||
cost: '成本贡献',
|
||||
pricing: '定价贡献',
|
||||
descendant_withdraw: '提现收益',
|
||||
descendant_upgrade_vip: '转化VIP奖励',
|
||||
descendant_upgrade_svip: '转化SVIP奖励',
|
||||
}
|
||||
return descriptionMap[type] || '未知类型'
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(timeStr) {
|
||||
if (!timeStr)
|
||||
return '-'
|
||||
const date = new Date(timeStr)
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
function formatNumber(num) {
|
||||
if (!num)
|
||||
return '0.00'
|
||||
return Number(num).toFixed(2)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="reward-detail">
|
||||
<!-- 用户信息卡片 -->
|
||||
<div class="p-4">
|
||||
<div class="mb-4 rounded-xl bg-white p-5 shadow-sm">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="text-xl text-gray-800 font-semibold">
|
||||
{{ userInfo.mobile }}
|
||||
</div>
|
||||
<span class="rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-600 font-medium">
|
||||
{{ userInfo.level }}代理
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 text-sm text-gray-500">
|
||||
成为下级代理时间:{{ formatTime(userInfo.createTime) }}
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="text-center">
|
||||
<div class="mb-1 text-sm text-gray-500">
|
||||
总推广单量
|
||||
</div>
|
||||
<div class="text-xl text-blue-600 font-semibold">
|
||||
{{ summary.totalOrders }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="mb-1 text-sm text-gray-500">
|
||||
总收益
|
||||
</div>
|
||||
<div class="text-xl text-green-600 font-semibold">
|
||||
¥{{ formatNumber(summary.totalReward) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="mb-1 text-sm text-gray-500">
|
||||
总贡献
|
||||
</div>
|
||||
<div class="text-xl text-purple-600 font-semibold">
|
||||
¥{{ formatNumber(summary.totalContribution) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 贡献统计卡片 -->
|
||||
<div class="mb-4 rounded-xl bg-white p-4 shadow-sm">
|
||||
<div class="mb-3 text-base text-gray-800 font-medium">
|
||||
贡献统计
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div
|
||||
v-for="item in statistics"
|
||||
:key="item.type"
|
||||
class="flex items-center rounded-lg p-2"
|
||||
:class="getRewardTypeClass(item.type).split(' ')[0]"
|
||||
>
|
||||
<wd-icon
|
||||
:name="getRewardTypeIcon(item.type)"
|
||||
class="mr-2 text-lg"
|
||||
:class="getRewardTypeClass(item.type).split(' ')[1]"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium" :class="getRewardTypeClass(item.type).split(' ')[1]">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
<div class="mt-1 flex items-center justify-between">
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ item.count }} 次
|
||||
</div>
|
||||
<div class="text-sm font-medium" :class="getRewardTypeClass(item.type).split(' ')[1]">
|
||||
¥{{ formatNumber(item.amount) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 rounded-xl bg-white p-4 shadow-sm">
|
||||
<!-- 贡献记录列表 -->
|
||||
<div class="text-base text-gray-800 font-medium">
|
||||
贡献记录
|
||||
</div>
|
||||
<div class="detail-scroll p-4">
|
||||
<div v-if="rewardDetails.length === 0" class="py-8 text-center text-gray-500">
|
||||
暂无贡献记录
|
||||
</div>
|
||||
<div v-for="item in rewardDetails" v-else :key="item.id" class="reward-item">
|
||||
<div class="mb-3 border-b border-gray-200 pb-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<wd-icon
|
||||
:name="getRewardTypeIcon(item.type)"
|
||||
class="text-lg"
|
||||
:class="getRewardTypeClass(item.type).split(' ')[1]"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-gray-800 font-medium">
|
||||
{{ getRewardTypeDescription(item.type) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
{{ formatTime(item.create_time) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-base font-semibold" :class="getRewardTypeClass(item.type).split(' ')[1]">
|
||||
¥{{ formatNumber(item.amount) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" class="py-3 text-center text-sm text-gray-400">
|
||||
加载中...
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 pb-4">
|
||||
<wd-pagination
|
||||
v-model="page"
|
||||
:total="total"
|
||||
:page-size="pageSize"
|
||||
show-icon
|
||||
show-message
|
||||
@change="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.reward-detail {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.reward-item {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.reward-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.detail-scroll {
|
||||
min-height: 50vh;
|
||||
}
|
||||
</style>
|
||||
198
src/pages/subordinate-list.vue
Normal file
198
src/pages/subordinate-list.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import useApiFetch from '@/composables/useApiFetch'
|
||||
definePage({ layout: 'default', auth: true })
|
||||
|
||||
const subordinates = ref([])
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = 8
|
||||
// 计算统计数据
|
||||
const statistics = ref({
|
||||
totalSubordinates: 0,
|
||||
})
|
||||
|
||||
// 获取下级列表
|
||||
async function fetchSubordinates() {
|
||||
if (loading.value)
|
||||
return
|
||||
|
||||
loading.value = true
|
||||
const { data, error } = await useApiFetch(`/agent/subordinate/list?page=${page.value}&page_size=${pageSize}`)
|
||||
.get()
|
||||
.json()
|
||||
if (data.value && !error.value) {
|
||||
if (data.value.code === 200) {
|
||||
statistics.value.totalSubordinates = data.value.data.total
|
||||
subordinates.value = data.value.data.list || []
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function onPageChange({ value }) {
|
||||
page.value = value
|
||||
fetchSubordinates()
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
function formatNumber(num) {
|
||||
if (!num)
|
||||
return '0.00'
|
||||
return Number(num).toFixed(2)
|
||||
}
|
||||
|
||||
// 获取等级标签样式
|
||||
function getLevelClass(level) {
|
||||
switch (level) {
|
||||
case 'SVIP':
|
||||
return 'bg-purple-100 text-purple-600'
|
||||
case 'VIP':
|
||||
return 'bg-blue-100 text-blue-600'
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-600'
|
||||
}
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
function viewDetail(item) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/subordinate-detail?id=${encodeURIComponent(String(item.id))}`,
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchSubordinates()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="subordinate-list">
|
||||
<!-- 顶部统计卡片 -->
|
||||
<div class="p-4 pb-0">
|
||||
<div class="rounded-xl bg-white p-4 shadow-sm">
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="mb-1 text-sm text-gray-500">
|
||||
下级总数
|
||||
</div>
|
||||
<div class="text-2xl text-blue-600 font-semibold">
|
||||
{{ statistics.totalSubordinates }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="subordinate-scroll p-4">
|
||||
<div v-for="(item, index) in subordinates" :key="item.id" class="subordinate-item">
|
||||
<div class="mb-4 flex flex-col rounded-xl bg-white p-5 shadow-sm">
|
||||
<!-- 顶部信息 -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div
|
||||
class="h-6 w-6 flex items-center justify-center rounded-full bg-blue-100 text-sm text-blue-600 font-medium"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<div class="text-xl text-gray-800 font-semibold">
|
||||
{{ item.mobile }}
|
||||
</div>
|
||||
<span class="rounded-full px-3 py-1 text-sm font-medium" :class="[getLevelClass(item.level)]">
|
||||
{{ item.level ? item.level : '普通' }}代理
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加入时间 -->
|
||||
<div class="mb-5 text-sm text-gray-500">
|
||||
成为下级代理时间:{{ item.create_time }}
|
||||
</div>
|
||||
|
||||
<!-- 数据统计 -->
|
||||
<div class="grid grid-cols-3 mb-5 gap-6">
|
||||
<div class="text-center">
|
||||
<div class="mb-1 text-sm text-gray-500">
|
||||
总推广单量
|
||||
</div>
|
||||
<div class="text-xl text-blue-600 font-semibold">
|
||||
{{ item.total_orders }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="mb-1 text-sm text-gray-500">
|
||||
总收益
|
||||
</div>
|
||||
<div class="text-xl text-green-600 font-semibold">
|
||||
¥{{ formatNumber(item.total_earnings) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="mb-1 text-sm text-gray-500">
|
||||
总贡献
|
||||
</div>
|
||||
<div class="text-xl text-purple-600 font-semibold">
|
||||
¥{{ formatNumber(item.total_contribution) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 查看详情按钮 -->
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
class="inline-flex items-center rounded-full from-blue-500 to-blue-400 bg-gradient-to-r px-4 py-2 text-sm text-white shadow-sm transition-all duration-200 hover:shadow-md"
|
||||
@click="viewDetail(item)"
|
||||
>
|
||||
<wd-icon name="view" custom-class="mr-1.5" />
|
||||
查看详情
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loading" class="py-4 text-center text-sm text-gray-400">
|
||||
加载中...
|
||||
</div>
|
||||
<div v-else-if="!subordinates.length" class="py-4 text-center text-sm text-gray-400">
|
||||
暂无下级代理
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 pb-4">
|
||||
<wd-pagination
|
||||
v-model="page"
|
||||
:total="statistics.totalSubordinates"
|
||||
:page-size="pageSize"
|
||||
show-icon
|
||||
show-message
|
||||
@change="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.subordinate-list {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.subordinate-scroll {
|
||||
height: calc(100vh - 180px);
|
||||
}
|
||||
|
||||
.subordinate-item {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.subordinate-item:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
button {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
</style>
|
||||
15
src/pages/user-agreement.vue
Normal file
15
src/pages/user-agreement.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { useReportWebview } from '@/composables/useReportWebview'
|
||||
definePage({ layout: false })
|
||||
|
||||
const { buildSitePathUrl } = useReportWebview()
|
||||
const src = ref('')
|
||||
|
||||
onLoad(() => {
|
||||
src.value = buildSitePathUrl('/app/userAgreement')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<web-view :src="src" />
|
||||
</template>
|
||||
213
src/pages/withdraw-details.vue
Normal file
213
src/pages/withdraw-details.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<script setup>
|
||||
definePage({ layout: 'default', auth: true })
|
||||
// 状态映射配置
|
||||
const statusConfig = {
|
||||
1: {
|
||||
chinese: '处理中',
|
||||
color: {
|
||||
bg: 'bg-yellow-100',
|
||||
text: 'text-yellow-800',
|
||||
dot: 'bg-yellow-500',
|
||||
amount: 'text-yellow-500',
|
||||
},
|
||||
},
|
||||
2: {
|
||||
chinese: '提现成功',
|
||||
color: {
|
||||
bg: 'bg-green-100',
|
||||
text: 'text-green-800',
|
||||
dot: 'bg-green-500',
|
||||
amount: 'text-green-500',
|
||||
},
|
||||
},
|
||||
3: {
|
||||
chinese: '提现失败',
|
||||
color: {
|
||||
bg: 'bg-red-100',
|
||||
text: 'text-red-800',
|
||||
dot: 'bg-red-500',
|
||||
amount: 'text-red-500',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const data = ref({
|
||||
total: 0,
|
||||
list: [],
|
||||
})
|
||||
const loading = ref(false)
|
||||
const pageCount = computed(() => {
|
||||
const total = Number(data.value.total || 0)
|
||||
return total > 0 ? Math.ceil(total / pageSize.value) : 1
|
||||
})
|
||||
|
||||
// 账户脱敏处理
|
||||
function maskName(name) {
|
||||
if (!name || typeof name !== 'string')
|
||||
return ''
|
||||
if (name.length <= 7)
|
||||
return name
|
||||
return `${name.substring(0, 3)}****${name.substring(7)}`
|
||||
}
|
||||
|
||||
// 银行卡号脱敏处理
|
||||
function maskBankCard(cardNo) {
|
||||
if (!cardNo || typeof cardNo !== 'string')
|
||||
return ''
|
||||
const card = cardNo.replace(/\s/g, '')
|
||||
if (card.length <= 8)
|
||||
return card
|
||||
return `${card.substring(0, 4)} **** **** ${card.substring(card.length - 4)}`
|
||||
}
|
||||
|
||||
// 状态转中文
|
||||
function statusToChinese(status) {
|
||||
return statusConfig[status]?.chinese || '未知状态'
|
||||
}
|
||||
|
||||
// 获取状态样式
|
||||
function getStatusStyle(status) {
|
||||
const config = statusConfig[status] || {}
|
||||
return `${config.color?.bg || 'bg-gray-100'} ${
|
||||
config.color?.text || 'text-gray-800'
|
||||
}`
|
||||
}
|
||||
|
||||
// 获取小圆点颜色
|
||||
function getDotColor(status) {
|
||||
return statusConfig[status]?.color.dot || 'bg-gray-500'
|
||||
}
|
||||
|
||||
// 获取金额颜色
|
||||
function getAmountColor(status) {
|
||||
return statusConfig[status]?.color.amount || 'text-gray-500'
|
||||
}
|
||||
|
||||
// 获取当前页数据
|
||||
async function getData() {
|
||||
try {
|
||||
loading.value = true
|
||||
const { data: res, error } = await useApiFetch(
|
||||
`/agent/withdrawal?page=${page.value}&page_size=${pageSize.value}`,
|
||||
)
|
||||
.get()
|
||||
.json()
|
||||
if (res.value?.code === 200 && !error.value)
|
||||
data.value = res.value.data
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onPageChange({ value }) {
|
||||
page.value = value
|
||||
getData()
|
||||
}
|
||||
|
||||
// 初始化加载
|
||||
onMounted(async () => {
|
||||
page.value = 1
|
||||
await getData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-gray-50">
|
||||
<view class="withdraw-content">
|
||||
<view
|
||||
v-for="(item, index) in data.list"
|
||||
:key="index"
|
||||
class="mx-4 my-2 rounded-lg bg-white p-4 shadow-sm"
|
||||
>
|
||||
<view class="mb-2 flex items-center justify-between">
|
||||
<text class="text-sm text-gray-500">
|
||||
{{
|
||||
item.create_time || "-"
|
||||
}}
|
||||
</text>
|
||||
<text class="font-bold" :class="getAmountColor(item.status)">
|
||||
{{ item.amount.toFixed(2) }}元
|
||||
</text>
|
||||
</view>
|
||||
<view class="mb-2 flex items-center">
|
||||
<text
|
||||
class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium"
|
||||
:class="getStatusStyle(item.status)"
|
||||
>
|
||||
<text
|
||||
class="mr-1 h-2 w-2 rounded-full"
|
||||
:class="getDotColor(item.status)"
|
||||
/>
|
||||
{{ statusToChinese(item.status) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="text-xs text-gray-500">
|
||||
<view v-if="item.withdraw_type === 1 && item.payee_account">
|
||||
收款账户:{{ maskName(item.payee_account) }}
|
||||
</view>
|
||||
<view v-if="item.withdraw_type === 2">
|
||||
<view v-if="item.bank_card_no">
|
||||
银行卡号:{{ maskBankCard(item.bank_card_no) }}
|
||||
</view>
|
||||
<view v-if="item.bank_name">
|
||||
开户支行:{{ item.bank_name }}
|
||||
</view>
|
||||
<view v-if="item.payee_name">
|
||||
收款人:{{ item.payee_name }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="item.remark">
|
||||
备注:{{ item.remark }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading" class="py-4 text-center text-sm text-gray-400">
|
||||
加载中...
|
||||
</view>
|
||||
<view v-else-if="!data.list.length" class="py-10 text-center text-sm text-gray-400">
|
||||
暂无提现记录
|
||||
</view>
|
||||
</view>
|
||||
<view class="pagination-wrap">
|
||||
<wd-pagination
|
||||
v-model="page"
|
||||
:total="data.total"
|
||||
:page-size="pageSize"
|
||||
show-icon
|
||||
show-message
|
||||
@change="onPageChange"
|
||||
/>
|
||||
<text class="mt-1 block text-center text-xs text-gray-400">
|
||||
共 {{ pageCount }} 页
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 保持原有样式不变 */
|
||||
.list-enter-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
.withdraw-content {
|
||||
min-height: calc(100vh - 180px);
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
background: #fff;
|
||||
padding: 8px 12px 12px;
|
||||
}
|
||||
</style>
|
||||
556
src/pages/withdraw.vue
Normal file
556
src/pages/withdraw.vue
Normal file
@@ -0,0 +1,556 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeMount, ref } from 'vue'
|
||||
import RealNameAuthDialog from '@/components/RealNameAuthDialog.vue'
|
||||
|
||||
definePage({ layout: 'default', auth: true })
|
||||
|
||||
const agentStore = useAgentStore()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
function toast(title) {
|
||||
uni.showToast({ title, icon: 'none' })
|
||||
}
|
||||
|
||||
// ========== 提现方式配置开关 ==========
|
||||
// 可以通过环境变量控制
|
||||
// 配置选项(建议其中一种):
|
||||
// - 'alipay'
|
||||
// - 'bankcard'
|
||||
// - 'both'
|
||||
// - 'alipay,bankcard'
|
||||
// - JSON 数组字符串: ["alipay","bankcard"]
|
||||
const WITHDRAW_CONFIG = import.meta.env.VITE_WITHDRAW_METHODS
|
||||
if (!WITHDRAW_CONFIG) {
|
||||
throw new Error('缺少环境变量: VITE_WITHDRAW_METHODS')
|
||||
}
|
||||
// 解析配置:支持字符串和数组格式
|
||||
function getWithdrawMethods() {
|
||||
if (Array.isArray(WITHDRAW_CONFIG))
|
||||
return WITHDRAW_CONFIG
|
||||
if (typeof WITHDRAW_CONFIG === 'string') {
|
||||
// 支持 JSON 数组字符串
|
||||
if (WITHDRAW_CONFIG.trim().startsWith('[')) {
|
||||
const parsed = JSON.parse(WITHDRAW_CONFIG)
|
||||
if (Array.isArray(parsed))
|
||||
return parsed.map(String)
|
||||
}
|
||||
if (WITHDRAW_CONFIG === 'alipay')
|
||||
return ['alipay']
|
||||
if (WITHDRAW_CONFIG === 'bankcard')
|
||||
return ['bankcard']
|
||||
if (WITHDRAW_CONFIG === 'both')
|
||||
return ['alipay', 'bankcard']
|
||||
// 支持逗号分隔的字符串: "alipay,bankcard" 或 "alipay" 或 "bankcard"
|
||||
return WITHDRAW_CONFIG.split(',').map(s => s.trim()).filter(Boolean)
|
||||
}
|
||||
throw new Error(`VITE_WITHDRAW_METHODS 无法解析: ${WITHDRAW_CONFIG}`)
|
||||
}
|
||||
|
||||
const enabledMethods = getWithdrawMethods()
|
||||
const showAlipay = enabledMethods.includes('alipay')
|
||||
const showBankcard = enabledMethods.includes('bankcard')
|
||||
const showTabs = showAlipay && showBankcard // 只有两种都显示时才显示 tabs
|
||||
|
||||
// 标签页管理
|
||||
function getInitialTab() {
|
||||
// 如果只显示一种方式,直接设置为该方式
|
||||
if (showAlipay && !showBankcard)
|
||||
return 'alipay'
|
||||
if (showBankcard && !showAlipay)
|
||||
return 'bankcard'
|
||||
// 如果两种都显示,默认支付宝
|
||||
return 'alipay'
|
||||
}
|
||||
const activeTab = ref(getInitialTab())
|
||||
|
||||
// 状态管理
|
||||
const status = ref(null)
|
||||
const failMsg = ref('')
|
||||
const isSubmitting = ref(false)
|
||||
const showStatusPopup = ref(false)
|
||||
const showTaxConfirmPopup = ref(false)
|
||||
|
||||
// 税务
|
||||
const taxFreeAmount = ref(0)
|
||||
const usedExemptionAmount = ref(0)
|
||||
const remainingExemptionAmount = ref(0)
|
||||
const taxRate = ref(0)
|
||||
|
||||
// 共用数据
|
||||
const amount = ref(0)
|
||||
const availableAmount = ref(null)
|
||||
|
||||
// 计算扣税金额和实际到账金额
|
||||
const taxAmount = computed(() => {
|
||||
if (!amount.value)
|
||||
return 0
|
||||
const withdrawAmount = Number(amount.value)
|
||||
return withdrawAmount * 0.06
|
||||
})
|
||||
|
||||
const actualAmount = computed(() => {
|
||||
if (!amount.value)
|
||||
return 0
|
||||
return Number(amount.value) - taxAmount.value
|
||||
})
|
||||
|
||||
// 样式配置
|
||||
const statusButtonColor = {
|
||||
1: '#dbeafe',
|
||||
2: '#10b981',
|
||||
3: '#ef4444',
|
||||
}
|
||||
|
||||
const statusMessages = {
|
||||
1: activeTab.value === 'alipay' ? '提现申请处理中,请稍后再查询结果' : '提现申请已提交,等待管理员审核',
|
||||
2: '提现成功',
|
||||
3: '提现失败',
|
||||
}
|
||||
|
||||
// 表单数据 - 支付宝
|
||||
const alipayAccount = ref('')
|
||||
const alipayRealName = ref('')
|
||||
|
||||
// 表单数据 - 银行卡
|
||||
const bankCardNo = ref('')
|
||||
const bankName = ref('')
|
||||
const bankCardInfo = ref({
|
||||
bank_card_no: '',
|
||||
bank_name: '',
|
||||
payee_name: '',
|
||||
id_card: '',
|
||||
})
|
||||
|
||||
// 标签页切换
|
||||
function onTabChange(name) {
|
||||
activeTab.value = name
|
||||
if (name === 'bankcard') {
|
||||
loadBankCardInfo()
|
||||
}
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 加载银行卡信息
|
||||
async function loadBankCardInfo() {
|
||||
try {
|
||||
const { data, error } = await useApiFetch('/agent/withdrawal/bank-card/info')
|
||||
.get()
|
||||
.json()
|
||||
if (data.value?.code === 200 && !error.value) {
|
||||
bankCardInfo.value = data.value.data
|
||||
// 自动填充历史银行卡信息
|
||||
if (data.value.data.bank_card_no) {
|
||||
bankCardNo.value = data.value.data.bank_card_no
|
||||
}
|
||||
if (data.value.data.bank_name) {
|
||||
bankName.value = data.value.data.bank_name
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('加载银行卡信息失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化身份证号(显示前6位和后4位)
|
||||
function formatIdCard(idCard) {
|
||||
if (!idCard)
|
||||
return ''
|
||||
if (idCard.length <= 10)
|
||||
return idCard
|
||||
return `${idCard.substring(0, 6)}****${idCard.substring(idCard.length - 4)}`
|
||||
}
|
||||
|
||||
async function getData() {
|
||||
const { data: res, error } = await useApiFetch('/agent/revenue')
|
||||
.get()
|
||||
.json()
|
||||
|
||||
if (res.value?.code === 200 && !error.value) {
|
||||
availableAmount.value = res.value.data.balance
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
getData()
|
||||
getTax()
|
||||
// 根据配置加载对应的数据
|
||||
if (showBankcard && (activeTab.value === 'bankcard' || !showTabs)) {
|
||||
loadBankCardInfo()
|
||||
}
|
||||
})
|
||||
|
||||
function validateForm() {
|
||||
if (activeTab.value === 'alipay') {
|
||||
// 支付宝提现验证
|
||||
if (!alipayRealName.value.trim()) {
|
||||
toast('请输入账户实名姓名')
|
||||
return false
|
||||
}
|
||||
if (!/^[\u4E00-\u9FA5]{2,4}$/.test(alipayRealName.value)) {
|
||||
toast('请输入2-4位中文姓名')
|
||||
return false
|
||||
}
|
||||
if (!alipayAccount.value.trim()) {
|
||||
toast('请输入支付宝账号')
|
||||
return false
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 银行卡提现验证
|
||||
if (!bankCardNo.value.trim()) {
|
||||
toast('请输入银行卡号')
|
||||
return false
|
||||
}
|
||||
const cardNo = String(bankCardNo.value).replace(/\s/g, '')
|
||||
if (!/^\d{16,19}$/.test(cardNo)) {
|
||||
toast('银行卡号格式不正确,请输入16-19位数字')
|
||||
return false
|
||||
}
|
||||
if (!bankName.value.trim()) {
|
||||
toast('请输入开户支行')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const amountNum = Number(amount.value)
|
||||
if (!amount.value || Number.isNaN(amountNum)) {
|
||||
toast('请输入有效金额')
|
||||
return false
|
||||
}
|
||||
|
||||
if (amountNum < 50) {
|
||||
toast('提现金额不能低于50元')
|
||||
return false
|
||||
}
|
||||
|
||||
if (amountNum > availableAmount.value) {
|
||||
toast('超过可提现金额')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 打开实名认证对话框
|
||||
function openRealNameAuth() {
|
||||
dialogStore.openRealNameAuth()
|
||||
}
|
||||
|
||||
// 获取税务
|
||||
async function getTax() {
|
||||
const { data, error } = await useApiFetch('/agent/withdrawal/tax/exemption')
|
||||
.get()
|
||||
.json()
|
||||
if (data.value?.code === 200 && !error.value) {
|
||||
taxFreeAmount.value = data.value.data.total_exemption_amount
|
||||
usedExemptionAmount.value = data.value.data.used_exemption_amount
|
||||
remainingExemptionAmount.value = data.value.data.remaining_exemption_amount
|
||||
taxRate.value = data.value.data.tax_rate
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
// 检查实名认证状态
|
||||
if (!agentStore.isRealName) {
|
||||
toast('请先完成实名认证')
|
||||
openRealNameAuth()
|
||||
return
|
||||
}
|
||||
|
||||
// 先进行表单验证
|
||||
if (!validateForm())
|
||||
return
|
||||
|
||||
// 显示税务确认弹窗
|
||||
showTaxConfirmPopup.value = true
|
||||
}
|
||||
|
||||
// 确认提现
|
||||
async function confirmWithdraw() {
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
let apiUrl, requestData
|
||||
|
||||
if (activeTab.value === 'alipay') {
|
||||
// 支付宝提现
|
||||
apiUrl = '/agent/withdrawal'
|
||||
requestData = {
|
||||
payee_account: alipayAccount.value,
|
||||
amount: amount.value,
|
||||
payee_name: alipayRealName.value,
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 银行卡提现
|
||||
apiUrl = '/agent/withdrawal/bank-card'
|
||||
requestData = {
|
||||
bank_card_no: String(bankCardNo.value).replace(/\s/g, ''),
|
||||
bank_name: bankName.value,
|
||||
amount: amount.value,
|
||||
}
|
||||
}
|
||||
|
||||
const { data } = await useApiFetch(apiUrl)
|
||||
.post(requestData)
|
||||
.json()
|
||||
|
||||
if (data.value?.code === 200) {
|
||||
status.value = data.value.data.status
|
||||
showTaxConfirmPopup.value = false
|
||||
showStatusPopup.value = true
|
||||
if (status.value === 3) {
|
||||
failMsg.value = data.value.data.fail_msg || '提现失败'
|
||||
}
|
||||
getData()
|
||||
}
|
||||
else {
|
||||
toast(data.value?.msg || '提现失败')
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
toast('提现失败,请重试')
|
||||
console.error('提现失败', err)
|
||||
}
|
||||
finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗操作
|
||||
function handlePopupAction() {
|
||||
if (status.value === 3) {
|
||||
showStatusPopup.value = false
|
||||
resetForm()
|
||||
}
|
||||
else {
|
||||
showStatusPopup.value = false
|
||||
if (status.value === 2)
|
||||
resetPage()
|
||||
}
|
||||
}
|
||||
|
||||
// 填充最大金额
|
||||
function fillMaxAmount() {
|
||||
amount.value = availableAmount.value
|
||||
}
|
||||
|
||||
// 重置页面
|
||||
function resetForm() {
|
||||
status.value = null
|
||||
if (activeTab.value === 'alipay') {
|
||||
alipayAccount.value = ''
|
||||
alipayRealName.value = ''
|
||||
}
|
||||
else {
|
||||
// 银行卡提现不清空,保留历史信息
|
||||
// bankCardNo.value = "";
|
||||
// bankName.value = "";
|
||||
}
|
||||
amount.value = ''
|
||||
}
|
||||
|
||||
function resetPage() {
|
||||
resetForm()
|
||||
// 可以跳转到提现记录页面
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="withdraw-page min-h-screen from-blue-50/30 to-gray-50 bg-gradient-to-b p-4">
|
||||
<view v-if="!agentStore.isRealName" class="mb-4 rounded-xl bg-amber-50 p-5 shadow">
|
||||
<view class="mb-2 flex items-center text-amber-700">
|
||||
<text class="mr-2">
|
||||
⚠️
|
||||
</text>
|
||||
<text class="text-lg font-bold">
|
||||
未完成实名认证
|
||||
</text>
|
||||
</view>
|
||||
<text class="mb-4 block text-sm text-amber-700/90">
|
||||
根据相关规定,提现功能需要完成实名认证后才能使用,提现金额将转入您实名认证的账户中。
|
||||
</text>
|
||||
<wd-button block type="warning" @click="openRealNameAuth">
|
||||
立即实名认证
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view class="rounded-xl bg-white p-4 shadow">
|
||||
<view v-if="showTabs" class="mb-4 flex gap-2">
|
||||
<wd-button size="small" :type="activeTab === 'alipay' ? 'primary' : 'info'" @click="onTabChange('alipay')">
|
||||
支付宝提现
|
||||
</wd-button>
|
||||
<wd-button size="small" :type="activeTab === 'bankcard' ? 'primary' : 'info'" @click="onTabChange('bankcard')">
|
||||
银行卡提现
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view v-if="(activeTab === 'alipay' && showAlipay) || (showAlipay && !showBankcard)" class="mb-4 rounded-xl bg-blue-50/60 p-4">
|
||||
<text class="mb-3 block text-base text-blue-700 font-semibold">
|
||||
支付宝提现
|
||||
</text>
|
||||
<wd-input v-model="alipayAccount" label="支付宝账号" clearable placeholder="请输入支付宝账号" />
|
||||
<text class="mb-3 mt-1 block text-xs text-gray-500">
|
||||
可填写支付宝账户绑定的手机号
|
||||
</text>
|
||||
<wd-input v-model="alipayRealName" label="实名姓名" clearable placeholder="请输入支付宝认证姓名" />
|
||||
<text class="mt-1 block text-xs text-gray-500">
|
||||
请填写支付宝账户认证的真实姓名
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view v-if="(activeTab === 'bankcard' && showBankcard) || (showBankcard && !showAlipay)" class="mb-4 rounded-xl bg-blue-50/60 p-4">
|
||||
<text class="mb-3 block text-base text-blue-700 font-semibold">
|
||||
银行卡提现
|
||||
</text>
|
||||
<view v-if="bankCardInfo.payee_name" class="mb-3 rounded-lg bg-blue-100/70 p-3 text-sm text-gray-700">
|
||||
<text class="mb-1 block">
|
||||
姓名:{{ bankCardInfo.payee_name }}
|
||||
</text>
|
||||
<text class="block">
|
||||
身份证号:{{ formatIdCard(bankCardInfo.id_card) }}
|
||||
</text>
|
||||
<text class="mt-2 block text-xs text-amber-600">
|
||||
提示:银行卡信息需与实名认证信息一致
|
||||
</text>
|
||||
</view>
|
||||
<wd-input v-model="bankCardNo" label="银行卡号" clearable type="number" placeholder="请输入银行卡号(16-19位)" />
|
||||
<text class="mb-3 mt-1 block text-xs text-gray-500">
|
||||
请输入16-19位银行卡号
|
||||
</text>
|
||||
<wd-input v-model="bankName" label="开户支行" clearable placeholder="请输入开户支行" />
|
||||
<text class="mt-1 block text-xs text-gray-500">
|
||||
例如:中国工商银行XX支行
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="mb-2 text-sm text-gray-700">
|
||||
提现金额
|
||||
</view>
|
||||
<view class="mb-3 flex items-center gap-2">
|
||||
<wd-input v-model.number="amount" class="flex-1" type="number" placeholder="请输入提现金额" />
|
||||
<wd-button size="small" type="primary" plain @click="fillMaxAmount">
|
||||
全部提现
|
||||
</wd-button>
|
||||
</view>
|
||||
<text class="mb-4 block text-sm text-gray-600">
|
||||
可提现金额:<text class="text-blue-600 font-semibold">
|
||||
¥{{ availableAmount || 0 }}
|
||||
</text>
|
||||
</text>
|
||||
|
||||
<view class="mb-4 rounded-xl bg-blue-50 p-3">
|
||||
<text class="mb-1 block text-sm text-blue-700 font-medium">
|
||||
提现须知
|
||||
</text>
|
||||
<text class="block text-xs text-gray-600">
|
||||
· 每日限提现1次,最低50元
|
||||
</text>
|
||||
<text class="block text-xs text-gray-600">
|
||||
· 提现收取6%税收
|
||||
</text>
|
||||
<text class="block text-xs text-gray-600">
|
||||
· {{ activeTab === 'alipay' ? '到账时间:24小时内' : '到账时间:管理员审核后手动转账' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="mb-4 rounded-xl bg-amber-50 p-3">
|
||||
<text class="mb-1 block text-sm text-amber-700 font-medium">
|
||||
税收说明
|
||||
</text>
|
||||
<text class="block text-xs text-gray-600">
|
||||
根据相关规定,提现时将统一收取6%的税收。
|
||||
</text>
|
||||
<text class="block text-xs text-amber-600">
|
||||
• 税率标准:统一按6%收取
|
||||
</text>
|
||||
<text class="block text-xs text-amber-600">
|
||||
• 适用范围:所有提现金额
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<wd-button block type="primary" :loading="isSubmitting" @click="handleSubmit">
|
||||
立即提现
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view v-if="showTaxConfirmPopup" class="modal-mask">
|
||||
<view class="modal-card">
|
||||
<text class="mb-2 block text-center text-lg font-semibold">
|
||||
提现确认
|
||||
</text>
|
||||
<text class="mb-4 block text-center text-sm text-gray-500">
|
||||
请确认以下提现信息
|
||||
</text>
|
||||
<view class="mb-4 rounded-lg bg-gray-50 p-3 text-sm">
|
||||
<view class="mb-1 flex justify-between">
|
||||
<text>提现金额:</text><text>¥{{ amount }}</text>
|
||||
</view>
|
||||
<view class="mb-1 flex justify-between">
|
||||
<text>税收:</text><text class="text-red-500">
|
||||
-¥{{ taxAmount.toFixed(2) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex justify-between border-t pt-2">
|
||||
<text>实际到账:</text><text class="text-blue-600 font-semibold">
|
||||
¥{{ actualAmount.toFixed(2) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex gap-2">
|
||||
<wd-button plain block @click="showTaxConfirmPopup = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button block type="primary" :loading="isSubmitting" @click="confirmWithdraw">
|
||||
确认提现
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="showStatusPopup" class="modal-mask">
|
||||
<view class="modal-card">
|
||||
<text class="mb-2 block text-center text-3xl">
|
||||
{{ status === 1 ? '⏳' : status === 2 ? '✅' : '❌' }}
|
||||
</text>
|
||||
<text class="mb-1 block text-center text-lg font-semibold" :class="status === 2 ? 'text-green-600' : status === 3 ? 'text-red-600' : 'text-blue-600'">
|
||||
{{ statusMessages[status] }}
|
||||
</text>
|
||||
<text v-if="status === 2" class="mb-2 block text-center text-sm text-gray-600">
|
||||
{{ activeTab === 'alipay' ? `已向 ${alipayAccount} 转账` : '提现申请已通过,管理员将手动转账' }}
|
||||
</text>
|
||||
<text v-if="status === 3" class="mb-2 block text-center text-sm text-red-500">
|
||||
{{ failMsg }}
|
||||
</text>
|
||||
<view v-if="status === 1" class="mb-3 mt-2 h-2 w-full overflow-hidden rounded-full bg-blue-100">
|
||||
<view class="h-full w-3/5 rounded-full bg-blue-500" />
|
||||
</view>
|
||||
<wd-button block :custom-style="`background:${statusButtonColor[status]};color:${status === 1 ? '#2563eb' : '#fff'}`" @click="handlePopupAction">
|
||||
{{ status === 1 ? '知道了' : status === 2 ? '完成' : '重新提现' }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<RealNameAuthDialog />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user