This commit is contained in:
2026-04-27 14:48:54 +08:00
parent 739e08157b
commit 893a223e0e
28 changed files with 828 additions and 634 deletions

View File

@@ -48,7 +48,8 @@ export default defineManifestConfig({
"privacyDescription": {
"NSLocalNetworkUsageDescription": "需要本地网络进行服务使用",
"NSPhotoLibraryAddUsageDescription": "需要保存二维码海报"
}
},
"idfa": false
},
/* SDK配置 */
"sdkConfigs": {
@@ -89,6 +90,18 @@ export default defineManifestConfig({
"spotlight@3x": "static/icons/120x120.png"
}
}
},
splashscreen: {
androidStyle: "default",
android: {
hdpi: "static/launchImg/android@480x762.png",
xhdpi: "static/launchImg/android@720x1242.png",
xxhdpi: "static/launchImg/android@1080x1882.png"
},
iosStyle: "storyboard",
ios: {
storyboard: "static/launchImg/storyboard.zip"
},
}
},
},

View File

@@ -8,7 +8,7 @@ export default defineUniPages({
{ path: 'pages/agent', style: { navigationBarTitleText: '代理中心' } },
{ path: 'pages/agent-manage-agreement', style: { navigationBarTitleText: '代理管理协议', navigationStyle: 'default' } },
{ path: 'pages/agent-promote-details', auth: true, style: { navigationBarTitleText: '直推收益明细' } },
{ path: 'pages/agent-rewards-details', auth: true, style: { navigationBarTitleText: '代理奖励明细' } },
{ path: 'pages/agent-rewards-details', auth: true, style: { navigationBarTitleText: '代理奖励收益明细' } },
{ path: 'pages/agent-service-agreement', style: { navigationBarTitleText: '信息技术服务合同', navigationStyle: 'default' } },
{ path: 'pages/agent-vip', auth: true, style: { navigationBarTitleText: '代理会员' } },
{ path: 'pages/agent-vip-apply', auth: true, style: { navigationBarTitleText: 'VIP申请' } },

10
src/auto-imports.d.ts vendored
View File

@@ -12,6 +12,7 @@ declare global {
const aesEncrypt: typeof import('./utils/crypto.js')['aesEncrypt']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const calculatePromotionPricing: typeof import('./utils/promotionPricing.js')['calculatePromotionPricing']
const chatCrypto: typeof import('./utils/chatCrypto.js')['default']
const chatEncrypt: typeof import('./utils/chatEncrypt.js')['default']
const clearAuthStorage: typeof import('./utils/storage')['clearAuthStorage']
@@ -143,6 +144,7 @@ declare global {
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const resolveUserAvatarUrl: typeof import('./utils/avatarUrl')['resolveUserAvatarUrl']
const resolveWebToUni: typeof import('./composables/uni-router')['resolveWebToUni']
const safeTruncate: typeof import('./utils/promotionPricing.js')['safeTruncate']
const setAgentInfo: typeof import('./utils/storage')['setAgentInfo']
const setAuthSession: typeof import('./utils/storage')['setAuthSession']
const setPosterMergePending: typeof import('./utils/posterRenderMergeBridge')['setPosterMergePending']
@@ -401,13 +403,11 @@ declare module 'vue' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly ZoomAdapter: UnwrapRef<typeof import('./utils/zoomAdapter.js')['ZoomAdapter']>
readonly aesDecrypt: UnwrapRef<typeof import('./utils/crypto.js')['aesDecrypt']>
readonly aesEncrypt: UnwrapRef<typeof import('./utils/crypto.js')['aesEncrypt']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly chatCrypto: UnwrapRef<typeof import('./utils/chatCrypto.js')['default']>
readonly chatEncrypt: UnwrapRef<typeof import('./utils/chatEncrypt.js')['default']>
readonly calculatePromotionPricing: UnwrapRef<typeof import('./utils/promotionPricing.js')['calculatePromotionPricing']>
readonly clearAuthStorage: UnwrapRef<typeof import('./utils/storage')['clearAuthStorage']>
readonly clearToken: UnwrapRef<typeof import('./utils/storage')['clearToken']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
@@ -537,6 +537,7 @@ declare module 'vue' {
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly resolveUserAvatarUrl: UnwrapRef<typeof import('./utils/avatarUrl')['resolveUserAvatarUrl']>
readonly resolveWebToUni: UnwrapRef<typeof import('./composables/uni-router')['resolveWebToUni']>
readonly safeTruncate: UnwrapRef<typeof import('./utils/promotionPricing.js')['safeTruncate']>
readonly setAgentInfo: UnwrapRef<typeof import('./utils/storage')['setAgentInfo']>
readonly setAuthSession: UnwrapRef<typeof import('./utils/storage')['setAuthSession']>
readonly setPosterMergePending: UnwrapRef<typeof import('./utils/posterRenderMergeBridge')['setPosterMergePending']>
@@ -586,7 +587,6 @@ declare module 'vue' {
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useAuthGuard: UnwrapRef<typeof import('./composables/useAuthGuard')['useAuthGuard']>
readonly useAuthStore: UnwrapRef<typeof import('./stores/auth')['useAuthStore']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
@@ -684,7 +684,6 @@ declare module 'vue' {
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useQuery: UnwrapRef<typeof import('./composables/useQuery')['useQuery']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useReportWebview: UnwrapRef<typeof import('./composables/useReportWebview')['useReportWebview']>
@@ -761,6 +760,5 @@ declare module 'vue' {
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
readonly writePngBase64ToLocal: UnwrapRef<typeof import('./utils/appLocalFile')['writePngBase64ToLocal']>
readonly zoomAdapter: UnwrapRef<typeof import('./utils/zoomAdapter.js')['default']>
}
}

View File

@@ -1,17 +1,17 @@
<script setup>
import { computed, nextTick, onUnmounted, ref } from 'vue'
import { useDialogStore } from '@/stores/dialogStore'
import { useAgentStore } from '@/stores/agentStore'
import { useUserStore } from '@/stores/userStore'
import { setAuthSession } from '@/utils/storage'
const emit = defineEmits(['login-success'])
const dialogStore = useDialogStore()
const userStore = useUserStore()
const agentStore = useAgentStore()
const phoneNumber = ref('')
const verificationCode = ref('')
const password = ref('')
const isPasswordLogin = ref(false)
const isAgreed = ref(false)
const isCountingDown = ref(false)
const countdown = ref(60)
@@ -34,12 +34,7 @@ const isPhoneNumberValid = computed(() => {
const canLogin = computed(() => {
if (!isPhoneNumberValid.value)
return false
if (isPasswordLogin.value) {
return password.value.length >= 6
}
else {
return verificationCode.value.length === 6
}
return verificationCode.value.length === 6
})
async function sendVerificationCode() {
@@ -83,17 +78,9 @@ async function handleLogin() {
showToast({ message: '请输入有效的手机号' })
return
}
if (isPasswordLogin.value) {
if (password.value.length < 6) {
showToast({ message: '密码长度不能小于6位' })
return
}
}
else {
if (verificationCode.value.length !== 6) {
showToast({ message: '请输入有效的验证码' })
return
}
if (verificationCode.value.length !== 6) {
showToast({ message: '请输入有效的验证码' })
return
}
if (!isAgreed.value) {
showToast({ message: '请先同意用户协议' })
@@ -114,6 +101,7 @@ async function performLogin() {
setAuthSession(data.value.data)
await userStore.fetchUserInfo()
await agentStore.fetchAgentStatus()
showToast({ message: '登录成功' })
closeDialog()
@@ -123,6 +111,9 @@ async function performLogin() {
showToast(data.value.msg)
}
}
else {
showToast({ message: '登录失败' })
}
}
finally {
uni.hideLoading()
@@ -133,8 +124,6 @@ function closeDialog() {
dialogStore.closeLogin()
phoneNumber.value = ''
verificationCode.value = ''
password.value = ''
isPasswordLogin.value = false
isAgreed.value = false
isCountingDown.value = false
countdown.value = 60
@@ -203,7 +192,7 @@ onUnmounted(() => {
/>
</view>
<view v-if="!isPasswordLogin" class="form-item">
<view class="form-item">
<text class="form-label">
验证码
</text>
@@ -232,27 +221,6 @@ onUnmounted(() => {
</view>
</view>
<view v-else class="form-item">
<text class="form-label">
密码
</text>
<wd-input
v-model="password"
class="phone-wd-input"
type="text"
show-password
placeholder="请输入密码"
no-border
clearable
/>
</view>
<view class="flex items-center justify-end py-1">
<text class="switch-login-type" @click="isPasswordLogin = !isPasswordLogin">
{{ isPasswordLogin ? '验证码登录' : '密码登录' }}
</text>
</view>
<view class="agreement-wrapper">
<wd-checkbox v-model="isAgreed" shape="square" size="18px" />
<text class="agreement-text">
@@ -393,8 +361,4 @@ onUnmounted(() => {
letter-spacing: 0.25rem;
}
.switch-login-type {
font-size: 0.875rem;
color: #2563eb;
}
</style>

View File

@@ -1,4 +1,6 @@
<script setup>
import { calculatePromotionPricing, safeTruncate } from '@/utils/promotionPricing'
const props = defineProps({
defaultPrice: {
type: Number,
@@ -34,33 +36,12 @@ watch(show, (visible) => {
price.value = Number(defaultPrice.value || 0)
})
const costPrice = computed(() => {
if (!productConfig.value)
return 0.00
// 平台定价成本
let platformPricing = 0
platformPricing += productConfig.value.cost_price
if (price.value > productConfig.value.p_pricing_standard) {
platformPricing += (price.value - productConfig.value.p_pricing_standard) * productConfig.value.p_overpricing_ratio
}
if (productConfig.value.a_pricing_standard > platformPricing && productConfig.value.a_pricing_end > platformPricing && productConfig.value.a_overpricing_ratio > 0) {
if (price.value > productConfig.value.a_pricing_standard) {
if (price.value > productConfig.value.a_pricing_end) {
platformPricing += (productConfig.value.a_pricing_end - productConfig.value.a_pricing_standard) * productConfig.value.a_overpricing_ratio
}
else {
platformPricing += (price.value - productConfig.value.a_pricing_standard) * productConfig.value.a_overpricing_ratio
}
}
}
return safeTruncate(platformPricing)
const pricingResult = computed(() => {
return calculatePromotionPricing(price.value, productConfig.value)
})
const promotionRevenue = computed(() => {
return safeTruncate(price.value - costPrice.value)
})
const costPrice = computed(() => pricingResult.value.costPrice)
const promotionRevenue = computed(() => pricingResult.value.promotionRevenue)
/** APP 端 placeholder 需用原生 style否则字号易偏小 */
const PRICE_PLACEHOLDER_STYLE = 'color:#9ca3af;font-size:40rpx;font-weight:500;'
@@ -107,16 +88,6 @@ function validatePrice(currentPrice) {
}
return { newPrice, message }
}
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)
const truncated = scaled / factor
return truncated.toFixed(decimals)
}
const isManualConfirm = ref(false)
function onConfirm() {
if (!hasProductConfig.value) {

View File

@@ -1,5 +1,6 @@
<script setup>
definePage({ layout: 'default', auth: true })
import { onReachBottom } from '@dcloudio/uni-app'
// 颜色配置(根据产品名称映射)
const typeColors = {
小微企业: { bg: 'bg-blue-100', text: 'text-blue-800', dot: 'bg-blue-500' },
@@ -40,6 +41,8 @@ const data = ref({
list: [],
})
const loading = ref(false)
const hasMore = ref(true)
const loadMoreState = ref('loading')
// 获取颜色样式
function getReportTypeStyle(name) {
@@ -119,16 +122,48 @@ function getStatusStyle(item) {
}
// 获取数据
async function getData() {
function mergeUniqueById(oldList, newList) {
const map = new Map()
const resolveKey = (item, index) => String(item?.id ?? item?.order_id ?? `${item?.create_time || ''}_${item?.status || ''}_${index}`)
oldList.forEach((item, index) => {
map.set(resolveKey(item, index), item)
})
newList.forEach((item, index) => {
map.set(resolveKey(item, oldList.length + index), item)
})
return Array.from(map.values())
}
async function getData(reset = false) {
try {
if (loading.value)
return
if (!reset && !hasMore.value)
return
loading.value = true
if (reset) {
page.value = 1
hasMore.value = true
loadMoreState.value = 'loading'
data.value = { total: 0, list: [] }
}
const { data: res, error } = await useApiFetch(
`/agent/commission?page=${page.value}&page_size=${pageSize.value}`,
{ silent: true },
).get().json()
if (res.value?.code === 200 && !error.value) {
data.value = res.value.data
const incoming = res.value.data.list || []
data.value.total = res.value.data.total || 0
data.value.list = reset ? incoming : mergeUniqueById(data.value.list, incoming)
const isLastPage = incoming.length < pageSize.value || data.value.list.length >= data.value.total
hasMore.value = !isLastPage
loadMoreState.value = isLastPage ? 'finished' : 'loading'
if (!isLastPage)
page.value += 1
}
else {
loadMoreState.value = 'error'
}
}
finally {
@@ -136,13 +171,12 @@ async function getData() {
}
}
function onPageChange({ value }) {
page.value = value
getData()
}
// 初始化加载
onMounted(() => {
getData(true)
})
onReachBottom(() => {
getData()
})
</script>
@@ -212,14 +246,7 @@ onMounted(() => {
暂无记录
</view>
</view>
<wd-pagination
v-model="page"
:total="data.total"
:page-size="pageSize"
show-icon
show-message
@change="onPageChange"
/>
<wd-loadmore :state="loadMoreState" @reload="getData" />
</view>
</template>
@@ -235,6 +262,6 @@ onMounted(() => {
}
.detail-scroll {
height: calc(100vh - 110px);
min-height: calc(100vh - 110px);
}
</style>

View File

@@ -1,5 +1,6 @@
<script setup>
definePage({ layout: 'default', auth: true })
import { onReachBottom } from '@dcloudio/uni-app'
// 类型映射配置
const typeConfig = {
descendant_promotion: {
@@ -31,6 +32,8 @@ const data = ref({
list: [],
})
const loading = ref(false)
const hasMore = ref(true)
const loadMoreState = ref('loading')
// 类型转中文
function typeToChinese(type) {
@@ -49,16 +52,50 @@ function getDotColor(type) {
}
// 获取数据
async function getData() {
function mergeUniqueById(oldList, newList) {
const map = new Map()
const resolveKey = (item, index) => String(item?.id ?? item?.order_id ?? `${item?.create_time || ''}_${item?.type || ''}_${index}`)
oldList.forEach((item, index) => {
map.set(resolveKey(item, index), item)
})
newList.forEach((item, index) => {
map.set(resolveKey(item, oldList.length + index), item)
})
return Array.from(map.values())
}
async function getData(reset = false) {
try {
if (loading.value)
return
if (!reset && !hasMore.value)
return
loading.value = true
if (reset) {
page.value = 1
hasMore.value = true
loadMoreState.value = 'loading'
data.value = { total: 0, list: [] }
}
const { data: res, error } = await useApiFetch(
`/agent/rewards?page=${page.value}&page_size=${pageSize.value}`,
{ silent: true },
).get().json()
if (res.value?.code === 200 && !error.value) {
data.value = res.value.data
const incoming = res.value.data.list || []
data.value.total = res.value.data.total || 0
data.value.list = reset
? incoming
: mergeUniqueById(data.value.list, incoming)
const isLastPage = incoming.length < pageSize.value || data.value.list.length >= data.value.total
hasMore.value = !isLastPage
loadMoreState.value = isLastPage ? 'finished' : 'loading'
if (!isLastPage)
page.value += 1
}
else {
loadMoreState.value = 'error'
}
}
finally {
@@ -66,13 +103,12 @@ async function getData() {
}
}
function onPageChange({ value }) {
page.value = value
getData()
}
// 初始化加载
onMounted(() => {
getData(true)
})
onReachBottom(() => {
getData()
})
</script>
@@ -106,14 +142,7 @@ onMounted(() => {
暂无记录
</view>
</view>
<wd-pagination
v-model="page"
:total="data.total"
:page-size="pageSize"
show-icon
show-message
@change="onPageChange"
/>
<wd-loadmore :state="loadMoreState" @reload="getData" />
</view>
</template>
@@ -129,6 +158,6 @@ onMounted(() => {
}
.reward-scroll {
height: calc(100vh - 110px);
min-height: calc(100vh - 110px);
}
</style>

View File

@@ -132,18 +132,18 @@ const revenueData = computed(() => {
// 计算月总收益
const vipMonthlyTotal
= baseRevenue
+ vipCommissionRevenue
+ vipFloatingRevenue
+ vipConversionRevenue
+ vipExtraRevenue
+ vipCommissionRevenue
+ vipFloatingRevenue
+ vipConversionRevenue
+ vipExtraRevenue
const svipMonthlyTotal
= baseRevenue
+ svipCommissionRevenue
+ svipFloatingRevenue
+ withdrawReward
+ svipConversionRevenue
+ svipExtraRevenue
+ svipCommissionRevenue
+ svipFloatingRevenue
+ withdrawReward
+ svipConversionRevenue
+ svipExtraRevenue
// 计算VIP和SVIP之间的差额
const monthlyDifference = svipMonthlyTotal - vipMonthlyTotal
@@ -239,12 +239,6 @@ function selectType(type) {
// 申请VIP或SVIP
async function applyVip() {
// 如果是VIP想升级到SVIP提示联系客服
if (isVip.value && selectedType.value === 'svip') {
contactService()
return
}
// 如果是SVIP要降级到VIP提示不能降级
if (isSvip.value && selectedType.value === 'vip') {
showToast('SVIP会员不能降级到VIP会员')
@@ -289,24 +283,20 @@ function formatExpiryTime(expiryTimeStr) {
<view class="agent-VIP-apply min-h-screen w-full from-amber-50 via-amber-100 to-amber-50 bg-gradient-to-b pb-24">
<!-- 装饰元素 -->
<view
class="absolute right-0 top-0 h-32 w-32 rounded-bl-full from-amber-300 to-amber-500 bg-gradient-to-br opacity-20"
/>
class="absolute right-0 top-0 h-32 w-32 rounded-bl-full from-amber-300 to-amber-500 bg-gradient-to-br opacity-20" />
<view
class="absolute left-0 top-40 h-16 w-16 rounded-tr-full from-amber-400 to-amber-600 bg-gradient-to-tr opacity-20"
/>
class="absolute left-0 top-40 h-16 w-16 rounded-tr-full from-amber-400 to-amber-600 bg-gradient-to-tr opacity-20" />
<view
class="absolute bottom-60 right-0 h-24 w-24 rounded-tl-full from-amber-300 to-amber-500 bg-gradient-to-bl opacity-20"
/>
class="absolute bottom-60 right-0 h-24 w-24 rounded-tl-full from-amber-300 to-amber-500 bg-gradient-to-bl opacity-20" />
<!-- 顶部标题区域 -->
<view class="header relative px-4 pb-6 pt-8 text-center">
<view
class="absolute left-1/2 h-1 w-24 animate-pulse rounded-full from-amber-300 via-amber-500 to-amber-300 bg-gradient-to-r -top-2 -translate-x-1/2"
/>
<text class="mb-1 text-3xl text-amber-800 font-bold">
class="absolute left-1/2 h-1 w-24 animate-pulse rounded-full from-amber-300 via-amber-500 to-amber-300 bg-gradient-to-r -top-2 -translate-x-1/2" />
<view class="mb-1 text-3xl text-amber-800 font-bold">
{{ isVipOrSvip ? '代理会员续费' : 'VIP代理申请' }}
</text>
<text class="mx-auto mt-2 max-w-xs text-sm text-amber-700">
</view>
<view class="mx-auto mt-2 max-w-xs text-sm text-amber-700">
<template v-if="isVipOrSvip">
您的会员有效期至 {{ formatExpiryTime(ExpiryTime) }}续费后有效期至
{{ renewalExpiryTime }}
@@ -314,20 +304,18 @@ function formatExpiryTime(expiryTimeStr) {
<template v-else>
平台为疯狂推广者定制的赚买计划助您收益<text class="text-red-500 font-bold">翻倍增升</text>
</template>
</text>
</view>
<!-- 装饰性金币图标 -->
<view class="absolute left-4 top-6 transform -rotate-12">
<view
class="h-8 w-8 flex items-center justify-center rounded-full from-yellow-300 to-yellow-500 bg-gradient-to-br shadow-lg"
>
class="h-8 w-8 flex items-center justify-center rounded-full from-yellow-300 to-yellow-500 bg-gradient-to-br shadow-lg">
<text class="text-xs text-white font-bold">¥</text>
</view>
</view>
<view class="absolute right-6 top-10 rotate-12 transform">
<view
class="h-6 w-6 flex items-center justify-center rounded-full from-yellow-400 to-yellow-600 bg-gradient-to-br shadow-lg"
>
class="h-6 w-6 flex items-center justify-center rounded-full from-yellow-400 to-yellow-600 bg-gradient-to-br shadow-lg">
<text class="text-xs text-white font-bold">¥</text>
</view>
</view>
@@ -336,16 +324,14 @@ function formatExpiryTime(expiryTimeStr) {
<!-- 选择代理类型 -->
<view class="card-container mb-8 px-4">
<view class="transform overflow-hidden border border-amber-100 rounded-xl bg-white shadow-lg transition-all">
<text
class="relative overflow-hidden from-amber-500 to-amber-600 bg-gradient-to-r px-4 py-3 text-center text-white font-bold"
>
<text class="relative z-10">选择代理类型</text>
<view
class="relative overflow-hidden from-amber-500 to-amber-600 bg-gradient-to-r px-4 py-3 text-center text-white font-bold">
<view class="relative z-10">选择代理类型</view>
<view class="absolute inset-0 bg-amber-500 opacity-30">
<view
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30"
/>
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30" />
</view>
</text>
</view>
<view class="flex gap-4 p-6">
<view
@@ -354,8 +340,7 @@ function formatExpiryTime(expiryTimeStr) {
selectedType === 'vip'
? 'border-amber-500 bg-amber-50 shadow-md'
: 'border-gray-200 hover:border-amber-300',
]" @click="selectType('vip')"
>
]" @click="selectType('vip')">
<view class="text-xl text-amber-700 font-bold">
VIP代理
</view>
@@ -365,10 +350,8 @@ function formatExpiryTime(expiryTimeStr) {
<view class="mt-2 text-sm text-gray-600">
标准VIP权益
</view>
<view
v-if="selectedType === 'vip'"
class="absolute h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br shadow-md -right-2 -top-2"
>
<view v-if="selectedType === 'vip'"
class="absolute h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br shadow-md -right-2 -top-2">
<wd-icon name="check" custom-style="color:#fff;font-size:14px;" />
</view>
</view>
@@ -379,8 +362,7 @@ function formatExpiryTime(expiryTimeStr) {
selectedType === 'svip'
? 'border-amber-500 bg-amber-50 shadow-md'
: 'border-gray-200 hover:border-amber-300',
]" @click="selectType('svip')"
>
]" @click="selectType('svip')">
<view class="text-xl text-amber-700 font-bold">
SVIP代理
</view>
@@ -390,10 +372,8 @@ function formatExpiryTime(expiryTimeStr) {
<view class="mt-2 text-sm text-gray-600">
超级VIP权益
</view>
<view
v-if="selectedType === 'svip'"
class="absolute h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br shadow-md -right-2 -top-2"
>
<view v-if="selectedType === 'svip'"
class="absolute h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br shadow-md -right-2 -top-2">
<wd-icon name="check" custom-style="color:#fff;font-size:14px;" />
</view>
</view>
@@ -404,26 +384,22 @@ function formatExpiryTime(expiryTimeStr) {
<!-- 六大超值权益 -->
<view class="card-container mb-8 px-4">
<view class="overflow-hidden border border-amber-100 rounded-xl bg-white shadow-lg">
<text
class="relative overflow-hidden from-amber-500 to-amber-600 bg-gradient-to-r px-4 py-3 text-center text-white font-bold"
>
<text class="relative z-10">六大超值权益</text>
<view
class="relative overflow-hidden from-amber-500 to-amber-600 bg-gradient-to-r px-4 py-3 text-center text-white font-bold">
<view class="relative z-10">六大超值权益</view>
<view class="absolute inset-0 bg-amber-500 opacity-30">
<view
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30"
/>
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30" />
</view>
</text>
</view>
<view class="grid grid-cols-2 gap-4 p-4">
<!-- 权益1 -->
<view
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md"
>
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md">
<view class="mb-2 flex items-center text-amber-800 font-bold">
<text
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white"
>1</text>
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white">1</text>
下级贡献收益
</view>
<text class="text-sm text-gray-600">
@@ -433,12 +409,10 @@ function formatExpiryTime(expiryTimeStr) {
<!-- 权益2 -->
<view
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md"
>
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md">
<view class="mb-2 flex items-center text-amber-800 font-bold">
<text
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white"
>2</text>
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white">2</text>
下级提现收益
</view>
<text class="text-sm text-gray-600">
@@ -448,12 +422,10 @@ function formatExpiryTime(expiryTimeStr) {
<!-- 权益3 -->
<view
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md"
>
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md">
<view class="mb-2 flex items-center text-amber-800 font-bold">
<text
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white"
>3</text>
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white">3</text>
转换高额奖励
</view>
<text class="text-sm text-gray-600">
@@ -463,12 +435,10 @@ function formatExpiryTime(expiryTimeStr) {
<!-- 权益4 -->
<view
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md"
>
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md">
<view class="mb-2 flex items-center text-amber-800 font-bold">
<text
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white"
>4</text>
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white">4</text>
下级提现奖励
</view>
<text class="text-sm text-gray-600">
@@ -478,12 +448,10 @@ function formatExpiryTime(expiryTimeStr) {
<!-- 权益6 -->
<view
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md"
>
class="border border-amber-200 rounded-lg from-amber-50 to-amber-100 bg-gradient-to-br p-3 transition-all duration-300 hover:border-amber-300 hover:shadow-md">
<view class="mb-2 flex items-center text-amber-800 font-bold">
<text
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white"
>6</text>
class="mr-2 h-6 w-6 flex items-center justify-center rounded-full from-amber-500 to-amber-600 bg-gradient-to-br text-xs text-white">6</text>
平台专项扶持
</view>
<text class="text-sm text-gray-600">
@@ -497,16 +465,14 @@ function formatExpiryTime(expiryTimeStr) {
<!-- 权益对比表 -->
<view v-if="selectedType" class="card-container mb-8 px-4">
<view class="overflow-hidden border border-amber-100 rounded-xl bg-white shadow-lg">
<text
class="relative overflow-hidden from-amber-500 to-amber-600 bg-gradient-to-r px-4 py-3 text-center text-white font-bold"
>
<text class="relative z-10">{{ selectedType === 'vip' ? 'VIP' : 'SVIP' }}代理权益对比</text>
<view
class="relative overflow-hidden from-amber-500 to-amber-600 bg-gradient-to-r px-4 py-3 text-center text-white font-bold">
<view class="relative z-10">{{ selectedType === 'vip' ? 'VIP' : 'SVIP' }}代理权益对比</view>
<view class="absolute inset-0 bg-amber-500 opacity-30">
<view
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30"
/>
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30" />
</view>
</text>
</view>
<view class="overflow-x-auto p-4">
<table class="w-full border-collapse">
@@ -518,16 +484,12 @@ function formatExpiryTime(expiryTimeStr) {
<th class="border border-amber-200 p-2 text-center text-amber-800">
普通代理
</th>
<th
class="border border-amber-200 p-2 text-center text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'vip' }"
>
<th class="border border-amber-200 p-2 text-center text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'vip' }">
VIP代理
</th>
<th
class="border border-amber-200 p-2 text-center text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'svip' }"
>
<th class="border border-amber-200 p-2 text-center text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'svip' }">
SVIP代理
</th>
</tr>
@@ -540,18 +502,14 @@ function formatExpiryTime(expiryTimeStr) {
<td class="border border-amber-200 p-2 text-center">
普通代理<br>免费
</td>
<td
class="border border-amber-200 p-2 text-center font-bold" :class="{
'text-amber-700 bg-amber-50': selectedType === 'vip',
}"
>
<td class="border border-amber-200 p-2 text-center font-bold" :class="{
'text-amber-700 bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.price }}{{ vipConfig.priceUnit }}
</td>
<td
class="border border-amber-200 p-2 text-center font-bold" :class="{
'text-amber-700 bg-amber-50': selectedType === 'svip',
}"
>
<td class="border border-amber-200 p-2 text-center font-bold" :class="{
'text-amber-700 bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.svipPrice }}{{ vipConfig.priceUnit }}
</td>
</tr>
@@ -562,18 +520,14 @@ function formatExpiryTime(expiryTimeStr) {
<td class="border border-amber-200 p-2 text-center">
1/
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.vipCommission }}/
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.svipCommission }}/
</td>
</tr>
@@ -584,18 +538,14 @@ function formatExpiryTime(expiryTimeStr) {
<td class="border border-amber-200 p-2 text-center">
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
</td>
</tr>
@@ -606,18 +556,14 @@ function formatExpiryTime(expiryTimeStr) {
<td class="border border-amber-200 p-2 text-center">
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
最高{{ vipConfig.vipFloatingRate }}%
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
最高{{ vipConfig.svipFloatingRate }}%
</td>
</tr>
@@ -628,18 +574,14 @@ function formatExpiryTime(expiryTimeStr) {
<td class="border border-amber-200 p-2 text-center">
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.withdrawRatio }}%
</td>
</tr>
@@ -650,18 +592,14 @@ function formatExpiryTime(expiryTimeStr) {
<td class="border border-amber-200 p-2 text-center">
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.vipConversionBonus }}*10
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.svipConversionBonus }}*10
</td>
</tr>
@@ -672,18 +610,14 @@ function formatExpiryTime(expiryTimeStr) {
<td class="border border-amber-200 p-2 text-center">
800/
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.vipWithdrawalLimit }}/
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.svipWithdrawalLimit }}/
</td>
</tr>
@@ -694,18 +628,14 @@ function formatExpiryTime(expiryTimeStr) {
<td class="border border-amber-200 p-2 text-center">
1/
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'vip',
}">
1/
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-bold bg-amber-50': selectedType === 'svip',
}">
2/
</td>
</tr>
@@ -718,16 +648,14 @@ function formatExpiryTime(expiryTimeStr) {
<!-- 收益预估 -->
<view v-if="selectedType" class="card-container mb-8 px-4">
<view class="overflow-hidden border border-amber-100 rounded-xl bg-white shadow-lg">
<text
class="relative overflow-hidden from-amber-500 to-amber-600 bg-gradient-to-r px-4 py-3 text-center text-white font-bold"
>
<text class="relative z-10">收益预估对比</text>
<view
class="relative overflow-hidden from-amber-500 to-amber-600 bg-gradient-to-r px-4 py-3 text-center text-white font-bold">
<view class="relative z-10">收益预估对比</view>
<view class="absolute inset-0 bg-amber-500 opacity-30">
<view
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30"
/>
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30" />
</view>
</text>
</view>
<view class="p-4">
<!-- 顶部收益概览 -->
@@ -772,16 +700,12 @@ function formatExpiryTime(expiryTimeStr) {
<th class="border border-amber-200 p-2 text-left text-amber-800">
收益来源
</th>
<th
class="w-1/3 border border-amber-200 p-2 text-center text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'vip' }"
>
<th class="w-1/3 border border-amber-200 p-2 text-center text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'vip' }">
VIP代理
</th>
<th
class="w-1/3 border border-amber-200 p-2 text-center text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'svip' }"
>
<th class="w-1/3 border border-amber-200 p-2 text-center text-amber-800"
:class="{ 'bg-amber-200': selectedType === 'svip' }">
SVIP代理
</th>
</tr>
@@ -791,18 +715,14 @@ function formatExpiryTime(expiryTimeStr) {
<td class="border border-amber-200 p-2 font-medium">
推广收益()
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
300×50=15,000
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
300×50=15,000
</td>
</tr>
@@ -810,18 +730,14 @@ function formatExpiryTime(expiryTimeStr) {
<td class="border border-amber-200 p-2 font-medium">
下级贡献收益()
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
300×{{ vipConfig.vipCommission }}=360
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
300×{{ vipConfig.svipCommission }}=450
</td>
</tr>
@@ -829,18 +745,14 @@ function formatExpiryTime(expiryTimeStr) {
<td class="border border-amber-200 p-2 font-medium">
下级价格浮动收益()
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
100×100×{{ vipConfig.vipFloatingRate }}%=500
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
200×100×{{ vipConfig.svipFloatingRate }}%=2,000
</td>
</tr>
@@ -848,18 +760,14 @@ function formatExpiryTime(expiryTimeStr) {
<td class="border border-amber-200 p-2 font-medium">
下级提现奖励()
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
-
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
{{ revenueData.withdrawReward }}
</td>
</tr>
@@ -867,18 +775,14 @@ function formatExpiryTime(expiryTimeStr) {
<td class="border border-amber-200 p-2 font-medium">
下级转化奖励()
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
{{ vipConfig.vipConversionBonus }}×2=598
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
{{ vipConfig.svipConversionBonus }}×2=798
</td>
</tr>
@@ -886,18 +790,14 @@ function formatExpiryTime(expiryTimeStr) {
<td class="border border-amber-200 p-2 font-medium">
额外业务收益()
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'vip',
}">
约3,000
</td>
<td
class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}"
>
<td class="border border-amber-200 p-2 text-center" :class="{
'text-amber-700 font-medium bg-amber-50': selectedType === 'svip',
}">
约6,000
</td>
</tr>
@@ -905,18 +805,14 @@ function formatExpiryTime(expiryTimeStr) {
<td class="border border-amber-200 p-3">
月计收益
</td>
<td
class="border border-amber-200 p-3 text-center text-amber-700" :class="{
'bg-amber-50 border-amber-300': selectedType === 'vip',
}"
>
<td class="border border-amber-200 p-3 text-center text-amber-700" :class="{
'bg-amber-50 border-amber-300': selectedType === 'vip',
}">
{{ revenueData.vipMonthly }}
</td>
<td
class="border border-amber-200 p-3 text-center text-red-500" :class="{
'bg-amber-50 border-amber-300': selectedType === 'svip',
}"
>
<td class="border border-amber-200 p-3 text-center text-red-500" :class="{
'bg-amber-50 border-amber-300': selectedType === 'svip',
}">
{{ revenueData.svipMonthly }}
</td>
</tr>
@@ -924,18 +820,14 @@ function formatExpiryTime(expiryTimeStr) {
<td class="border border-amber-200 p-3">
年计收益
</td>
<td
class="border border-amber-200 p-3 text-center text-amber-700" :class="{
'bg-amber-50 border-amber-300': selectedType === 'vip',
}"
>
<td class="border border-amber-200 p-3 text-center text-amber-700" :class="{
'bg-amber-50 border-amber-300': selectedType === 'vip',
}">
{{ revenueData.vipYearly }}
</td>
<td
class="border border-amber-200 p-3 text-center text-red-500" :class="{
'bg-amber-50 border-amber-300': selectedType === 'svip',
}"
>
<td class="border border-amber-200 p-3 text-center text-red-500" :class="{
'bg-amber-50 border-amber-300': selectedType === 'svip',
}">
{{ revenueData.svipYearly }}
</td>
</tr>
@@ -995,8 +887,7 @@ function formatExpiryTime(expiryTimeStr) {
</view>
</view>
<view
class="h-6 w-6 flex flex-shrink-0 items-center justify-center rounded-full bg-red-500 text-white"
>
class="h-6 w-6 flex flex-shrink-0 items-center justify-center rounded-full bg-red-500 text-white">
<view class="transform -translate-y-px">
</view>
@@ -1010,8 +901,7 @@ function formatExpiryTime(expiryTimeStr) {
</view>
</view>
<view
class="h-6 w-6 flex flex-shrink-0 items-center justify-center rounded-full bg-red-500 text-white"
>
class="h-6 w-6 flex flex-shrink-0 items-center justify-center rounded-full bg-red-500 text-white">
<text class="transform -translate-y-px"></text>
</view>
<view class="text-center">
@@ -1036,21 +926,14 @@ function formatExpiryTime(expiryTimeStr) {
<!-- 申请按钮固定在底部 -->
<view
class="fixed bottom-0 left-0 right-0 z-30 from-amber-100 to-transparent bg-gradient-to-t px-4 py-3 backdrop-blur-sm"
>
class="fixed bottom-0 left-0 right-0 z-30 from-amber-100 to-transparent bg-gradient-to-t px-4 py-3 backdrop-blur-sm">
<view class="flex flex-col gap-2">
<wd-button :class="buttonClass" block :disabled="!canPerformAction" @click="applyVip">
<text class="relative z-10">{{ buttonText }}</text>
<view
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30"
/>
class="animate-shimmer absolute left-0 top-0 h-full w-full translate-x-full transform from-transparent via-white to-transparent bg-gradient-to-r opacity-20 -skew-x-30" />
</wd-button>
<wd-button
custom-class="contact-wd-btn"
block
plain
@click="contactService"
>
<wd-button custom-class="contact-wd-btn" block plain @click="contactService">
<view class="flex items-center justify-center text-amber-700 font-medium">
<wd-icon name="service" custom-class="mr-1" />
<text>联系客服咨询</text>

View File

@@ -2,23 +2,21 @@
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 reportOptions = ref([])
const reportOptionsByValue = computed(() => {
const map = new Map()
reportOptions.value.forEach((item) => {
map.set(item.value, item)
})
return map
})
// 状态管理
const showPicker = ref(false)
const selectedReport = ref(reportOptions[0])
const selectedReportText = ref(reportOptions[0].text)
const selectedReportId = ref(reportOptions[0].id)
const selectedReport = ref(null)
const selectedReportText = ref('')
const selectedReportId = ref(null)
const configData = ref({})
const productConfigData = ref({})
@@ -28,6 +26,7 @@ const priceRatioMax = ref(null)
const rangeError = ref(false)
const ratioError = ref(false)
const increaseError = ref(false)
const activeField = ref('')
function showToast(message) {
if (!message)
@@ -307,11 +306,19 @@ function finalValidation() {
}
// 选择器确认
function onSelectReport(option) {
function onConfirmType(e) {
const raw = e?.value
const nextValue = Array.isArray(raw) ? raw[0] : raw
const fromItem = e?.selectedItems
const resolved = nextValue ?? (Array.isArray(fromItem) ? fromItem[0]?.value : fromItem?.value)
if (resolved == null || resolved === '')
return
const option = reportOptionsByValue.value.get(Number(resolved))
if (!option)
return
selectedReport.value = option
selectedReportText.value = option.text
selectedReportText.value = option.label
selectedReportId.value = option.id
showPicker.value = false
// 重置错误状态
rangeError.value = false
ratioError.value = false
@@ -324,9 +331,53 @@ function closeRangeError() {
rangeError.value = false
}, 2000)
}
function onFieldFocus(field) {
activeField.value = field
}
function onFieldBlur(field) {
if (activeField.value === field)
activeField.value = ''
}
onMounted(() => {
getConfig()
loadReportOptions()
})
async function loadReportOptions() {
try {
const { data, error } = await useApiFetch('/agent/product_config', { silent: true }).get().json()
if (error.value || data.value?.code !== 200) {
showToast(data.value?.msg || '加载报告类型失败')
return
}
const list = data.value?.data?.AgentProductConfig || data.value?.data?.agent_product_config || []
reportOptions.value = list
.map(item => ({
label: item.product_name || `报告${item.product_id}`,
value: item.product_id,
id: item.product_id,
}))
.filter(item => item.id != null)
if (!reportOptions.value.length) {
showToast('暂无可配置报告类型')
selectedReport.value = null
selectedReportText.value = ''
selectedReportId.value = null
return
}
const first = reportOptions.value[0]
selectedReport.value = first
selectedReportText.value = first.label
selectedReportId.value = first.id
getConfig()
}
catch {
showToast('加载报告类型失败')
}
}
</script>
<template>
@@ -342,30 +393,16 @@ onMounted(() => {
</view>
<view 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>
<wd-picker
v-model="selectedReportId"
label="报告类型"
label-width="100px"
title="选择报告类型"
:columns="[reportOptions]"
placeholder="请选择报告类型"
:disabled="!reportOptions.length"
@confirm="onConfirmType"
/>
</view>
<view v-if="selectedReportText" class="space-y-6">
@@ -421,7 +458,7 @@ onMounted(() => {
</view>
<!-- 加价金额 -->
<view class="custom-field" :class="{ 'field-error': increaseError }">
<view class="custom-field" :class="{ 'field-error': increaseError, 'field-active': activeField === 'price_increase_amount' }">
<text class="field-label">
🚀 加价金额
</text>
@@ -433,7 +470,13 @@ onMounted(() => {
class="field-wd-input"
no-border
clearable
@blur="validateDecimal('price_increase_amount')"
@focus="onFieldFocus('price_increase_amount')"
@blur="
() => {
onFieldBlur('price_increase_amount')
validateDecimal('price_increase_amount')
}
"
/>
<text class="field-unit">
@@ -451,7 +494,7 @@ onMounted(() => {
</view>
<!-- 定价区间最低 -->
<view class="custom-field" :class="{ 'field-error': rangeError }">
<view class="custom-field" :class="{ 'field-error': rangeError, 'field-active': activeField === 'price_range_from' }">
<text class="field-label">
💰 最低金额
</text>
@@ -463,8 +506,10 @@ onMounted(() => {
class="field-wd-input"
no-border
clearable
@focus="onFieldFocus('price_range_from')"
@blur="
() => {
onFieldBlur('price_range_from')
validateDecimal('price_range_from')
validateRange()
}
@@ -483,7 +528,7 @@ onMounted(() => {
</view>
<!-- 定价区间最高 -->
<view class="custom-field" :class="{ 'field-error': rangeError }">
<view class="custom-field" :class="{ 'field-error': rangeError, 'field-active': activeField === 'price_range_to' }">
<text class="field-label">
💰 最高金额
</text>
@@ -495,8 +540,10 @@ onMounted(() => {
class="field-wd-input"
no-border
clearable
@focus="onFieldFocus('price_range_to')"
@blur="
() => {
onFieldBlur('price_range_to')
validateDecimal('price_range_to')
validateRange()
}
@@ -515,7 +562,7 @@ onMounted(() => {
</view>
<!-- 收取比例 -->
<view class="custom-field" :class="{ 'field-error': ratioError }">
<view class="custom-field" :class="{ 'field-error': ratioError, 'field-active': activeField === 'price_ratio' }">
<text class="field-label">
📈 收取比例
</text>
@@ -527,7 +574,13 @@ onMounted(() => {
class="field-wd-input"
no-border
clearable
@blur="validateRatio()"
@focus="onFieldFocus('price_ratio')"
@blur="
() => {
onFieldBlur('price_ratio')
validateRatio()
}
"
/>
<text class="field-unit">
%
@@ -642,11 +695,11 @@ onMounted(() => {
.field-input-wrap {
display: flex;
align-items: center;
background: #f9fafb;
background: #f3f4f6;
border-radius: 8px;
padding: 8px 12px;
transition: all 0.3s ease;
border: 1px solid transparent;
padding: 10px 12px;
transition: all 0.2s ease;
border: 1px solid #e5e7eb;
}
:deep(.field-wd-input .wd-input__inner) {
@@ -661,12 +714,24 @@ onMounted(() => {
font-weight: 500;
}
.custom-field:focus-within .field-input-wrap {
border-color: #bfdbfe;
::deep(.field-wd-input.wd-input),
::deep(.field-wd-input .wd-input__body),
::deep(.field-wd-input .wd-input__prefix),
::deep(.field-wd-input .wd-input__suffix),
::deep(.field-wd-input .wd-input__value),
::deep(.field-wd-input .wd-input__clear) {
background: transparent !important;
}
.field-active .field-input-wrap {
border-color: #93c5fd;
background: #eff6ff;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
}
.field-error .field-input-wrap {
border-color: #fca5a5;
background: #fef2f2;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
</style>

View File

@@ -93,9 +93,11 @@ function toSubordinateList() {
<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>
<view class="flex items-center">
<text class="text-lg text-gray-800 font-bold">
余额
</text>
</view>
<text class="text-3xl text-blue-600 font-bold">
¥ {{ (data?.balance || 0).toFixed(2) }}
</text>
@@ -121,9 +123,11 @@ function toSubordinateList() {
<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="flex items-center">
<text class="text-lg text-gray-800 font-bold">
直推报告收益
</text>
</view>
<view class="text-right">
<text class="text-2xl text-blue-600 font-bold">
¥ {{ (data?.direct_push?.total_commission || 0).toFixed(2) }}
@@ -176,7 +180,7 @@ function toSubordinateList() {
<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>
@@ -192,7 +196,7 @@ function toSubordinateList() {
</view>
</view>
<view class="grid grid-cols-1 mb-6 gap-2">
<view class="grid grid-cols-2 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 }}下级推广奖励
@@ -221,7 +225,7 @@ function toSubordinateList() {
<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">
查看我的下级

View File

@@ -1,5 +1,6 @@
<script setup>
import { onMounted, ref } from 'vue'
import { onReachBottom } from '@dcloudio/uni-app'
definePage({ layout: 'default', auth: true })
const page = ref(1)
@@ -7,30 +8,65 @@ const pageSize = ref(10)
const total = ref(0)
const reportList = ref([])
const loading = ref(false)
const hasMore = ref(true)
const loadMoreState = ref('loading')
// 初始加载数据
async function fetchData() {
function mergeUniqueById(oldList, newList) {
const map = new Map()
const resolveKey = (item, index) => String(item?.id ?? item?.order_id ?? `${item?.create_time || ''}_${item?.query_state || ''}_${index}`)
oldList.forEach((item, index) => {
map.set(resolveKey(item, index), item)
})
newList.forEach((item, index) => {
map.set(resolveKey(item, oldList.length + index), item)
})
return Array.from(map.values())
}
async function fetchData(reset = false) {
if (loading.value)
return
if (!reset && !hasMore.value)
return
loading.value = true
if (reset) {
page.value = 1
hasMore.value = true
loadMoreState.value = 'loading'
reportList.value = []
}
const { data, error } = await useApiFetch(`query/list?page=${page.value}&page_size=${pageSize.value}`, { silent: true })
.get()
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
total.value = data.value.data.total
reportList.value = data.value.data.list || []
const incoming = data.value.data.list || []
reportList.value = reset ? incoming : mergeUniqueById(reportList.value, incoming)
const isLastPage = incoming.length < pageSize.value || reportList.value.length >= total.value
hasMore.value = !isLastPage
loadMoreState.value = isLastPage ? 'finished' : 'loading'
if (!isLastPage)
page.value += 1
}
else {
loadMoreState.value = 'error'
}
}
else {
loadMoreState.value = 'error'
}
loading.value = false
}
// 初始加载
onMounted(() => {
fetchData()
fetchData(true)
})
function onPageChange({ value }) {
page.value = value
onReachBottom(() => {
fetchData()
}
})
function toDetail(item) {
if (item.query_state !== 'success')
@@ -102,20 +138,13 @@ function statusClass(state) {
暂无记录
</view>
</view>
<wd-pagination
v-model="page"
:total="total"
:page-size="pageSize"
show-icon
show-message
@change="onPageChange"
/>
<wd-loadmore :state="loadMoreState" @reload="fetchData" />
</view>
</template>
<style scoped>
.history-scroll {
height: calc(100vh - 120px);
min-height: calc(100vh - 120px);
}
.status-pending {

View File

@@ -60,10 +60,8 @@ function toHistory() {
<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 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>
@@ -74,8 +72,7 @@ function toHistory() {
<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"
>
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">
@@ -85,8 +82,7 @@ function toHistory() {
<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"
>
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">
@@ -96,8 +92,7 @@ function toHistory() {
<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"
>
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">
@@ -109,13 +104,11 @@ function toHistory() {
<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"
<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)"
>
@click="toInquire(service.name)">
<view class="min-h-18 flex items-end">
<!-- <text class="text-base text-gray-700 font-semibold">
{{ service.title }}
@@ -126,12 +119,10 @@ function toHistory() {
<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"
<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)"
>
@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 }}
@@ -152,10 +143,8 @@ function toHistory() {
<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="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 File

@@ -97,7 +97,13 @@ async function performLogin() {
await userStore.fetchUserInfo()
await agentStore.fetchAgentStatus()
uni.reLaunch({ url: redirectUrl.value || '/pages/index' })
const target = redirectUrl.value || '/pages/index'
if (redirectUrl.value) {
uni.redirectTo({ url: target })
}
else {
uni.reLaunch({ url: target })
}
}
else {
toast(data.value.msg || '登录失败')

View File

@@ -16,8 +16,20 @@ const { isAgent, level, ExpiryTime } = storeToRefs(agentStore)
const { userAvatar, isLoggedIn, mobile } = storeToRefs(userStore)
const isWeChat = ref(false)
const normalizedLevel = computed(() => {
const raw = String(level.value || '').trim()
if (!raw || ['NORMAL', 'NORNAL'].includes(raw.toUpperCase()) || raw === 'normal' || raw === 'nornal')
return 'NORMAL'
if (raw.toUpperCase().includes('SVIP'))
return 'SVIP'
if (raw.toUpperCase().includes('VIP'))
return 'VIP'
return 'NORMAL'
})
const levelNames = {
'normal': '普通代理',
'NORMAL': '普通代理',
'': '普通代理',
'VIP': 'VIP代理',
'SVIP': 'SVIP代理',
@@ -25,6 +37,7 @@ const levelNames = {
const levelText = {
'normal': '基础代理特权',
'NORMAL': '基础代理特权',
'': '基础代理特权',
'VIP': '高级代理特权',
'SVIP': '尊享代理特权',
@@ -33,24 +46,27 @@ const levelText = {
const levelGradient = computed(() => ({
border: {
'normal': '',
'NORMAL': '',
'': '',
'VIP': '',
'SVIP': '',
}[level.value],
}[normalizedLevel.value],
badge: {
'normal': 'bg-gradient-to-r from-gray-500 to-gray-600',
'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],
}[normalizedLevel.value],
text: {
'normal': 'text-gray-600',
'NORMAL': 'text-gray-600',
'': 'text-gray-600',
'VIP': 'text-amber-600',
'SVIP': 'text-purple-600',
}[level.value],
}[normalizedLevel.value],
}))
function maskName(name) {
@@ -122,21 +138,18 @@ function formatExpiryTime(expiryTimeStr) {
return expiryTimeStr.split(' ')[0]
}
/** 与 bdrp-mini `/static/image/shot_*.png` 一致,资源放在 `src/static/image/` */
/** 代理默认头像资源位于 `src/static/images/shot_*.png` */
function getDefaultAvatar() {
if (!isAgent.value)
return headShot
const normalizedLevel = String(level.value || '').toUpperCase()
switch (normalizedLevel) {
switch (normalizedLevel.value) {
case 'NORMAL':
case 'normal':
case '':
return '/static/image/shot_nonal.png'
return '/static/images/shot_nonal.png'
case 'VIP':
return '/static/image/shot_vip.png'
return '/static/images/shot_vip.png'
case 'SVIP':
return '/static/image/shot_svip.png'
return '/static/images/shot_svip.png'
default:
return headShot
}
@@ -203,7 +216,7 @@ onBeforeMount(() => {
<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] }}
{{ levelNames[normalizedLevel] }}
</view>
</view>
</view>
@@ -226,15 +239,15 @@ onBeforeMount(() => {
</view>
</template>
<view v-if="isAgent" class="text-sm font-medium" :class="levelGradient.text">
🎖 {{ levelText[level] }}
🎖 {{ levelText[normalizedLevel] }}
</view>
</view>
</view>
<VipBanner v-if="isAgent && (level === 'normal' || level === '')" />
<VipBanner v-if="isAgent && normalizedLevel === 'NORMAL'" />
<!-- 功能菜单 -->
<view>
<view class="overflow-hidden rounded-xl bg-white shadow-sm">
<template v-if="isAgent && ['VIP', 'SVIP'].includes(level)">
<template v-if="isAgent && ['VIP', 'SVIP'].includes(normalizedLevel)">
<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 File

@@ -26,6 +26,17 @@ const pollingCount = ref(0)
const maxPollingCount = 30 // 最大轮询次数
const baseInterval = 2000 // 基础轮询间隔2秒
function navigateToReportResult() {
const url = `/pages/report-result-webview?orderNo=${encodeURIComponent(orderNo.value)}`
uni.redirectTo({
url,
fail: () => {
// redirectTo 失败时兜底,避免无响应
uni.navigateTo({ url })
},
})
}
// 计算属性
const statusText = computed(() => {
if (isApiError.value) {
@@ -152,9 +163,7 @@ async function checkPaymentStatus() {
&& (newStatus === 'paid' || newStatus === 'refunded')
) {
stopPolling()
uni.redirectTo({
url: `/pages/report-result-webview?orderNo=${encodeURIComponent(orderNo.value)}`,
})
navigateToReportResult()
return
}
@@ -230,16 +239,14 @@ onBeforeUnmount(() => {
// 处理导航逻辑
function handleNavigation() {
if (paymentType.value === 'agent_vip') {
// 跳转到代理会员页面
uni.switchTab({ url: '/pages/agent' })
// 该项目未使用原生 tabBar不能用 switchTab
uni.reLaunch({ url: '/pages/agent' })
agentStore.fetchAgentStatus()
userStore.fetchUserInfo()
}
else {
// 跳转到查询结果页面
uni.redirectTo({
url: `/pages/report-result-webview?orderNo=${encodeURIComponent(orderNo.value)}`,
})
navigateToReportResult()
}
}

View File

@@ -1,6 +1,7 @@
<script setup>
import PriceInputPopup from '@/components/PriceInputPopup.vue'
import VipBanner from '@/components/VipBanner.vue'
import { calculatePromotionPricing, safeTruncate } from '@/utils/promotionPricing'
definePage({ layout: 'default' })
@@ -30,39 +31,12 @@ const availableReportTypes = computed(() => {
.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 pricingResult = computed(() => {
return calculatePromotionPricing(formData.value.clientPrice, pickerProductConfig.value)
})
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)
}
const costPrice = computed(() => pricingResult.value.costPrice)
const promotionRevenue = computed(() => pricingResult.value.promotionRevenue)
function selectProductType(reportTypeValue) {
const reportType = availableReportTypes.value.find(item => item.id === reportTypeValue || item.value === reportTypeValue)

View File

@@ -1,5 +1,6 @@
<script setup>
import { onMounted, ref } from 'vue'
import { onReachBottom } from '@dcloudio/uni-app'
import { useRoute } from '@/composables/uni-router'
import useApiFetch from '@/composables/useApiFetch'
@@ -14,15 +15,65 @@ const rewardDetails = ref([])
const userInfo = ref({})
const summary = ref({})
const statistics = ref([])
const subordinateId = ref(0)
const hasMore = ref(true)
const loadMoreState = ref('loading')
function normalizeAgentLevel(value) {
const raw = String(value || '').trim()
if (!raw)
return 'NORMAL'
const cleaned = raw.replace(/代理$/, '')
if (cleaned === '普通')
return 'NORMAL'
const upper = cleaned.toUpperCase()
if (upper.includes('SVIP'))
return 'SVIP'
if (upper.includes('VIP'))
return 'VIP'
if (upper === 'NORMAL' || upper === 'NORNAL')
return 'NORMAL'
return 'NORMAL'
}
function getAgentLevelLabel(value) {
return {
NORMAL: '普通代理',
VIP: 'VIP代理',
SVIP: 'SVIP代理',
}[normalizeAgentLevel(value)] || '普通代理'
}
// 获取收益列表
async function fetchRewardDetails() {
function mergeUniqueById(oldList, newList) {
const map = new Map()
const resolveKey = (item, index) => String(item?.id ?? item?.order_id ?? `${item?.create_time || ''}_${item?.type || ''}_${index}`)
oldList.forEach((item, index) => {
map.set(resolveKey(item, index), item)
})
newList.forEach((item, index) => {
map.set(resolveKey(item, oldList.length + index), item)
})
return Array.from(map.values())
}
async function fetchRewardDetails(reset = false) {
if (loading.value)
return
if (!subordinateId.value)
return
if (!reset && !hasMore.value)
return
loading.value = true
if (reset) {
page.value = 1
hasMore.value = true
loadMoreState.value = 'loading'
rewardDetails.value = []
}
const { data, error } = await useApiFetch(
`/agent/subordinate/contribution/detail?subordinate_id=${route.params.id}&page=${page.value}&page_size=${pageSize}`,
`/agent/subordinate/contribution/detail?subordinate_id=${subordinateId.value}&page=${page.value}&page_size=${pageSize}`,
{ silent: true },
)
.get()
@@ -34,7 +85,7 @@ async function fetchRewardDetails() {
// 更新用户信息
userInfo.value = {
createTime: data.value.data.create_time,
level: data.value.data.level_name || '普通',
level: data.value.data.level_name || data.value.data.level || '',
mobile: data.value.data.mobile,
}
// 更新汇总数据
@@ -122,28 +173,57 @@ async function fetchRewardDetails() {
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 = []
}
const list = data.value.data.list || []
rewardDetails.value = reset ? list : mergeUniqueById(rewardDetails.value, list)
const isLastPage = list.length < pageSize || rewardDetails.value.length >= total.value
hasMore.value = !isLastPage
loadMoreState.value = isLastPage ? 'finished' : 'loading'
if (!isLastPage)
page.value += 1
}
else {
loadMoreState.value = 'error'
}
}
else {
loadMoreState.value = 'error'
}
loading.value = false
}
function onPageChange({ value }) {
page.value = value
fetchRewardDetails()
function resolveSubordinateId(query) {
const rawId = query?.id
?? query?.subordinate_id
?? route.params?.id
?? route.query?.id
?? route.params?.subordinate_id
?? route.query?.subordinate_id
const parsedId = Number.parseInt(String(rawId ?? ''), 10)
return Number.isFinite(parsedId) && parsedId > 0 ? parsedId : 0
}
onLoad((query) => {
const parsedId = resolveSubordinateId(query)
subordinateId.value = parsedId
})
onMounted(() => {
const parsedId = subordinateId.value || resolveSubordinateId()
if (!Number.isFinite(parsedId) || parsedId <= 0) {
uni.showToast({ title: '下级参数错误', icon: 'none' })
setTimeout(() => {
uni.navigateBack()
}, 800)
return
}
subordinateId.value = parsedId
fetchRewardDetails(true)
})
onReachBottom(() => {
fetchRewardDetails()
})
@@ -213,7 +293,7 @@ function formatNumber(num) {
{{ userInfo.mobile }}
</view>
<text class="rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-600 font-medium">
{{ userInfo.level }}代理
{{ getAgentLevelLabel(userInfo.level) }}
</text>
</view>
</view>
@@ -320,16 +400,7 @@ function formatNumber(num) {
加载中...
</view>
</view>
<view class="px-4 pb-4">
<wd-pagination
v-model="page"
:total="total"
:page-size="pageSize"
show-icon
show-message
@change="onPageChange"
/>
</view>
<wd-loadmore :state="loadMoreState" @reload="fetchRewardDetails" />
</view>
</view>
</view>

View File

@@ -1,5 +1,6 @@
<script setup>
import { onMounted, ref } from 'vue'
import { onReachBottom } from '@dcloudio/uni-app'
import useApiFetch from '@/composables/useApiFetch'
definePage({ layout: 'default', auth: true })
@@ -8,34 +9,65 @@ const subordinates = ref([])
const loading = ref(false)
const page = ref(1)
const pageSize = 8
const hasMore = ref(true)
const loadMoreState = ref('loading')
// 计算统计数据
const statistics = ref({
totalSubordinates: 0,
})
// 获取下级列表
async function fetchSubordinates() {
function mergeUniqueById(oldList, newList) {
const map = new Map()
const resolveKey = (item, index) => String(item?.id ?? item?.subordinate_id ?? item?.user_id ?? `${item?.mobile || ''}_${index}`)
oldList.forEach((item, index) => {
map.set(resolveKey(item, index), item)
})
newList.forEach((item, index) => {
map.set(resolveKey(item, oldList.length + index), item)
})
return Array.from(map.values())
}
async function fetchSubordinates(reset = false) {
if (loading.value)
return
if (!reset && !hasMore.value)
return
loading.value = true
if (reset) {
page.value = 1
hasMore.value = true
loadMoreState.value = 'loading'
subordinates.value = []
}
const { data, error } = await useApiFetch(`/agent/subordinate/list?page=${page.value}&page_size=${pageSize}`, { silent: true })
.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 || []
const currentList = data.value.data.list || []
subordinates.value = reset
? currentList
: mergeUniqueById(subordinates.value, currentList)
const isLastPage = currentList.length < pageSize || subordinates.value.length >= data.value.data.total
hasMore.value = !isLastPage
loadMoreState.value = isLastPage ? 'finished' : 'loading'
if (!isLastPage)
page.value += 1
}
else {
loadMoreState.value = 'error'
}
}
else {
loadMoreState.value = 'error'
}
loading.value = false
}
function onPageChange({ value }) {
page.value = value
fetchSubordinates()
}
// 格式化金额
function formatNumber(num) {
if (!num)
@@ -45,15 +77,26 @@ function formatNumber(num) {
/** 与后端 /agent/subordinate/list 一致level_name兼容历史 level 字段 */
function getLevelText(item) {
return item?.level_name || item?.level || '普通'
const raw = String(item?.level_name || item?.level || '').trim()
if (!raw)
return '普通代理'
const cleaned = raw.replace(/代理$/, '')
if (cleaned === '普通')
return '普通代理'
const upper = cleaned.toUpperCase()
if (upper.includes('SVIP'))
return 'SVIP代理'
if (upper.includes('VIP'))
return 'VIP代理'
return '普通代理'
}
// 获取等级标签样式
function getLevelClass(level) {
switch (level) {
case 'SVIP':
case 'SVIP代理':
return 'bg-purple-100 text-purple-600'
case 'VIP':
case 'VIP代理':
return 'bg-blue-100 text-blue-600'
default:
return 'bg-gray-100 text-gray-600'
@@ -62,12 +105,22 @@ function getLevelClass(level) {
// 查看详情
function viewDetail(item) {
const rawId = item?.id ?? item?.subordinate_id ?? item?.user_id
const parsedId = Number.parseInt(String(rawId ?? ''), 10)
if (!Number.isFinite(parsedId) || parsedId <= 0) {
uni.showToast({ title: '下级信息异常,无法查看详情', icon: 'none' })
return
}
uni.navigateTo({
url: `/pages/subordinate-detail?id=${encodeURIComponent(String(item.id))}`,
url: `/pages/subordinate-detail?id=${encodeURIComponent(String(parsedId))}`,
})
}
onMounted(() => {
fetchSubordinates(true)
})
onReachBottom(() => {
fetchSubordinates()
})
</script>
@@ -105,7 +158,7 @@ onMounted(() => {
{{ item.mobile }}
</view>
<text class="rounded-full px-3 py-1 text-sm font-medium" :class="[getLevelClass(getLevelText(item))]">
{{ getLevelText(item) }}代理
{{ getLevelText(item) }}
</text>
</view>
</view>
@@ -162,16 +215,7 @@ onMounted(() => {
<view v-else-if="!subordinates.length" class="py-4 text-center text-sm text-gray-400">
暂无下级代理
</view>
</view>
<view class="px-4 pb-4">
<wd-pagination
v-model="page"
:total="statistics.totalSubordinates"
:page-size="pageSize"
show-icon
show-message
@change="onPageChange"
/>
<wd-loadmore :state="loadMoreState" @reload="fetchSubordinates" />
</view>
</view>
</template>
@@ -183,7 +227,7 @@ onMounted(() => {
}
.subordinate-scroll {
height: calc(100vh - 180px);
min-height: calc(100vh - 180px);
}
.subordinate-item {

View File

@@ -1,5 +1,6 @@
<script setup>
definePage({ layout: 'default', auth: true })
import { onReachBottom } from '@dcloudio/uni-app'
// 状态映射配置
const statusConfig = {
1: {
@@ -38,10 +39,8 @@ const data = ref({
list: [],
})
const loading = ref(false)
const pageCount = computed(() => {
const total = Number(data.value.total || 0)
return total > 0 ? Math.ceil(total / pageSize.value) : 1
})
const hasMore = ref(true)
const loadMoreState = ref('loading')
// 账户脱敏处理
function maskName(name) {
@@ -86,32 +85,63 @@ function getAmountColor(status) {
}
// 获取当前页数据
async function getData() {
function mergeUniqueById(oldList, newList) {
const map = new Map()
const resolveKey = (item, index) => String(item?.id ?? item?.withdraw_no ?? `${item?.create_time || ''}_${item?.amount || ''}_${index}`)
oldList.forEach((item, index) => {
map.set(resolveKey(item, index), item)
})
newList.forEach((item, index) => {
map.set(resolveKey(item, oldList.length + index), item)
})
return Array.from(map.values())
}
async function getData(reset = false) {
try {
if (loading.value)
return
if (!reset && !hasMore.value)
return
loading.value = true
if (reset) {
page.value = 1
hasMore.value = true
loadMoreState.value = 'loading'
data.value = { total: 0, list: [] }
}
const { data: res, error } = await useApiFetch(
`/agent/withdrawal?page=${page.value}&page_size=${pageSize.value}`,
{ silent: true },
)
.get()
.json()
if (res.value?.code === 200 && !error.value)
data.value = res.value.data
if (res.value?.code === 200 && !error.value) {
const incoming = res.value.data.list || []
data.value.total = res.value.data.total || 0
data.value.list = reset ? incoming : mergeUniqueById(data.value.list, incoming)
const isLastPage = incoming.length < pageSize.value || data.value.list.length >= data.value.total
hasMore.value = !isLastPage
loadMoreState.value = isLastPage ? 'finished' : 'loading'
if (!isLastPage)
page.value += 1
}
else {
loadMoreState.value = 'error'
}
}
finally {
loading.value = false
}
}
function onPageChange({ value }) {
page.value = value
getData()
}
// 初始化加载
onMounted(async () => {
page.value = 1
await getData()
await getData(true)
})
onReachBottom(() => {
getData()
})
</script>
@@ -173,17 +203,7 @@ onMounted(async () => {
</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>
<wd-loadmore :state="loadMoreState" @reload="getData" />
</view>
</view>
</template>
@@ -204,9 +224,6 @@ onMounted(async () => {
}
.pagination-wrap {
position: sticky;
bottom: 0;
z-index: 10;
border-top: 1px solid #f1f5f9;
background: #fff;
padding: 8px 12px 12px;

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24765" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_9" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24743"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="440" height="956"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="launchscreen.png" translatesAutoresizingMaskIntoConstraints="NO" id="eKa-Do-o9M">
<rect key="frame" x="0.0" y="124" width="440" height="764"/>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="IW3-oA-Ytg"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="IW3-oA-Ytg" firstAttribute="bottom" secondItem="eKa-Do-o9M" secondAttribute="bottom" id="C62-2E-pZC"/>
<constraint firstItem="IW3-oA-Ytg" firstAttribute="trailing" secondItem="eKa-Do-o9M" secondAttribute="trailing" id="DqN-c8-YSD"/>
<constraint firstItem="eKa-Do-o9M" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="UM0-sL-jGn"/>
<constraint firstItem="eKa-Do-o9M" firstAttribute="top" secondItem="IW3-oA-Ytg" secondAttribute="top" id="iwy-bq-Ia7"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="52.173913043478265" y="374.33035714285711"/>
</scene>
</scenes>
<resources>
<image name="launchscreen.png" width="596.15997314453125" height="1059.8399658203125"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

View File

@@ -0,0 +1,46 @@
export 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 calculatePlatformOverpricingCost(price, config) {
if (price <= config.p_pricing_standard)
return 0
return (price - config.p_pricing_standard) * config.p_overpricing_ratio
}
function calculateSuperiorOverpricingCost(price, config) {
if (config.a_overpricing_ratio <= 0)
return 0
if (price <= config.a_pricing_standard)
return 0
if (config.a_pricing_end <= config.a_pricing_standard)
return 0
const superiorRangeAmount = Math.min(price, config.a_pricing_end) - config.a_pricing_standard
return Math.max(0, superiorRangeAmount) * config.a_overpricing_ratio
}
export function calculatePromotionPricing(priceInput, config) {
if (!config)
return { costPrice: '0.00', promotionRevenue: '0.00' }
const price = Number(priceInput)
if (!Number.isFinite(price))
return { costPrice: '0.00', promotionRevenue: '0.00' }
const baseCost = Number(config.cost_price) || 0
const platformOverpricingCost = calculatePlatformOverpricingCost(price, config)
const superiorOverpricingCost = calculateSuperiorOverpricingCost(price, config)
const totalCost = baseCost + platformOverpricingCost + superiorOverpricingCost
const revenue = price - totalCost
return {
costPrice: safeTruncate(totalCost),
promotionRevenue: safeTruncate(revenue),
}
}