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": { "privacyDescription": {
"NSLocalNetworkUsageDescription": "需要本地网络进行服务使用", "NSLocalNetworkUsageDescription": "需要本地网络进行服务使用",
"NSPhotoLibraryAddUsageDescription": "需要保存二维码海报" "NSPhotoLibraryAddUsageDescription": "需要保存二维码海报"
} },
"idfa": false
}, },
/* SDK配置 */ /* SDK配置 */
"sdkConfigs": { "sdkConfigs": {
@@ -89,6 +90,18 @@ export default defineManifestConfig({
"spotlight@3x": "static/icons/120x120.png" "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', style: { navigationBarTitleText: '代理中心' } },
{ path: 'pages/agent-manage-agreement', style: { navigationBarTitleText: '代理管理协议', navigationStyle: 'default' } }, { path: 'pages/agent-manage-agreement', style: { navigationBarTitleText: '代理管理协议', navigationStyle: 'default' } },
{ path: 'pages/agent-promote-details', auth: true, style: { navigationBarTitleText: '直推收益明细' } }, { 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-service-agreement', style: { navigationBarTitleText: '信息技术服务合同', navigationStyle: 'default' } },
{ path: 'pages/agent-vip', auth: true, style: { navigationBarTitleText: '代理会员' } }, { path: 'pages/agent-vip', auth: true, style: { navigationBarTitleText: '代理会员' } },
{ path: 'pages/agent-vip-apply', auth: true, style: { navigationBarTitleText: 'VIP申请' } }, { 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 aesEncrypt: typeof import('./utils/crypto.js')['aesEncrypt']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const calculatePromotionPricing: typeof import('./utils/promotionPricing.js')['calculatePromotionPricing']
const chatCrypto: typeof import('./utils/chatCrypto.js')['default'] const chatCrypto: typeof import('./utils/chatCrypto.js')['default']
const chatEncrypt: typeof import('./utils/chatEncrypt.js')['default'] const chatEncrypt: typeof import('./utils/chatEncrypt.js')['default']
const clearAuthStorage: typeof import('./utils/storage')['clearAuthStorage'] const clearAuthStorage: typeof import('./utils/storage')['clearAuthStorage']
@@ -143,6 +144,7 @@ declare global {
const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const resolveUserAvatarUrl: typeof import('./utils/avatarUrl')['resolveUserAvatarUrl'] const resolveUserAvatarUrl: typeof import('./utils/avatarUrl')['resolveUserAvatarUrl']
const resolveWebToUni: typeof import('./composables/uni-router')['resolveWebToUni'] const resolveWebToUni: typeof import('./composables/uni-router')['resolveWebToUni']
const safeTruncate: typeof import('./utils/promotionPricing.js')['safeTruncate']
const setAgentInfo: typeof import('./utils/storage')['setAgentInfo'] const setAgentInfo: typeof import('./utils/storage')['setAgentInfo']
const setAuthSession: typeof import('./utils/storage')['setAuthSession'] const setAuthSession: typeof import('./utils/storage')['setAuthSession']
const setPosterMergePending: typeof import('./utils/posterRenderMergeBridge')['setPosterMergePending'] const setPosterMergePending: typeof import('./utils/posterRenderMergeBridge')['setPosterMergePending']
@@ -401,13 +403,11 @@ declare module 'vue' {
interface GlobalComponents {} interface GlobalComponents {}
interface ComponentCustomProperties { interface ComponentCustomProperties {
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']> 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 aesDecrypt: UnwrapRef<typeof import('./utils/crypto.js')['aesDecrypt']>
readonly aesEncrypt: UnwrapRef<typeof import('./utils/crypto.js')['aesEncrypt']> readonly aesEncrypt: UnwrapRef<typeof import('./utils/crypto.js')['aesEncrypt']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']> readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']> readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly chatCrypto: UnwrapRef<typeof import('./utils/chatCrypto.js')['default']> readonly calculatePromotionPricing: UnwrapRef<typeof import('./utils/promotionPricing.js')['calculatePromotionPricing']>
readonly chatEncrypt: UnwrapRef<typeof import('./utils/chatEncrypt.js')['default']>
readonly clearAuthStorage: UnwrapRef<typeof import('./utils/storage')['clearAuthStorage']> readonly clearAuthStorage: UnwrapRef<typeof import('./utils/storage')['clearAuthStorage']>
readonly clearToken: UnwrapRef<typeof import('./utils/storage')['clearToken']> readonly clearToken: UnwrapRef<typeof import('./utils/storage')['clearToken']>
readonly computed: UnwrapRef<typeof import('vue')['computed']> readonly computed: UnwrapRef<typeof import('vue')['computed']>
@@ -537,6 +537,7 @@ declare module 'vue' {
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']> readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly resolveUserAvatarUrl: UnwrapRef<typeof import('./utils/avatarUrl')['resolveUserAvatarUrl']> readonly resolveUserAvatarUrl: UnwrapRef<typeof import('./utils/avatarUrl')['resolveUserAvatarUrl']>
readonly resolveWebToUni: UnwrapRef<typeof import('./composables/uni-router')['resolveWebToUni']> 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 setAgentInfo: UnwrapRef<typeof import('./utils/storage')['setAgentInfo']>
readonly setAuthSession: UnwrapRef<typeof import('./utils/storage')['setAuthSession']> readonly setAuthSession: UnwrapRef<typeof import('./utils/storage')['setAuthSession']>
readonly setPosterMergePending: UnwrapRef<typeof import('./utils/posterRenderMergeBridge')['setPosterMergePending']> readonly setPosterMergePending: UnwrapRef<typeof import('./utils/posterRenderMergeBridge')['setPosterMergePending']>
@@ -586,7 +587,6 @@ declare module 'vue' {
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']> readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']> readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']> readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useAuthGuard: UnwrapRef<typeof import('./composables/useAuthGuard')['useAuthGuard']>
readonly useAuthStore: UnwrapRef<typeof import('./stores/auth')['useAuthStore']> readonly useAuthStore: UnwrapRef<typeof import('./stores/auth')['useAuthStore']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']> readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']> readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
@@ -684,7 +684,6 @@ declare module 'vue' {
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']> readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']> readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']> 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 useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']> readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useReportWebview: UnwrapRef<typeof import('./composables/useReportWebview')['useReportWebview']> readonly useReportWebview: UnwrapRef<typeof import('./composables/useReportWebview')['useReportWebview']>
@@ -761,6 +760,5 @@ declare module 'vue' {
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']> readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']> readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
readonly writePngBase64ToLocal: UnwrapRef<typeof import('./utils/appLocalFile')['writePngBase64ToLocal']> 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> <script setup>
import { computed, nextTick, onUnmounted, ref } from 'vue' import { computed, nextTick, onUnmounted, ref } from 'vue'
import { useDialogStore } from '@/stores/dialogStore' import { useDialogStore } from '@/stores/dialogStore'
import { useAgentStore } from '@/stores/agentStore'
import { useUserStore } from '@/stores/userStore' import { useUserStore } from '@/stores/userStore'
import { setAuthSession } from '@/utils/storage' import { setAuthSession } from '@/utils/storage'
const emit = defineEmits(['login-success']) const emit = defineEmits(['login-success'])
const dialogStore = useDialogStore() const dialogStore = useDialogStore()
const userStore = useUserStore() const userStore = useUserStore()
const agentStore = useAgentStore()
const phoneNumber = ref('') const phoneNumber = ref('')
const verificationCode = ref('') const verificationCode = ref('')
const password = ref('')
const isPasswordLogin = ref(false)
const isAgreed = ref(false) const isAgreed = ref(false)
const isCountingDown = ref(false) const isCountingDown = ref(false)
const countdown = ref(60) const countdown = ref(60)
@@ -34,12 +34,7 @@ const isPhoneNumberValid = computed(() => {
const canLogin = computed(() => { const canLogin = computed(() => {
if (!isPhoneNumberValid.value) if (!isPhoneNumberValid.value)
return false return false
if (isPasswordLogin.value) { return verificationCode.value.length === 6
return password.value.length >= 6
}
else {
return verificationCode.value.length === 6
}
}) })
async function sendVerificationCode() { async function sendVerificationCode() {
@@ -83,17 +78,9 @@ async function handleLogin() {
showToast({ message: '请输入有效的手机号' }) showToast({ message: '请输入有效的手机号' })
return return
} }
if (isPasswordLogin.value) { if (verificationCode.value.length !== 6) {
if (password.value.length < 6) { showToast({ message: '请输入有效的验证码' })
showToast({ message: '密码长度不能小于6位' }) return
return
}
}
else {
if (verificationCode.value.length !== 6) {
showToast({ message: '请输入有效的验证码' })
return
}
} }
if (!isAgreed.value) { if (!isAgreed.value) {
showToast({ message: '请先同意用户协议' }) showToast({ message: '请先同意用户协议' })
@@ -114,6 +101,7 @@ async function performLogin() {
setAuthSession(data.value.data) setAuthSession(data.value.data)
await userStore.fetchUserInfo() await userStore.fetchUserInfo()
await agentStore.fetchAgentStatus()
showToast({ message: '登录成功' }) showToast({ message: '登录成功' })
closeDialog() closeDialog()
@@ -123,6 +111,9 @@ async function performLogin() {
showToast(data.value.msg) showToast(data.value.msg)
} }
} }
else {
showToast({ message: '登录失败' })
}
} }
finally { finally {
uni.hideLoading() uni.hideLoading()
@@ -133,8 +124,6 @@ function closeDialog() {
dialogStore.closeLogin() dialogStore.closeLogin()
phoneNumber.value = '' phoneNumber.value = ''
verificationCode.value = '' verificationCode.value = ''
password.value = ''
isPasswordLogin.value = false
isAgreed.value = false isAgreed.value = false
isCountingDown.value = false isCountingDown.value = false
countdown.value = 60 countdown.value = 60
@@ -203,7 +192,7 @@ onUnmounted(() => {
/> />
</view> </view>
<view v-if="!isPasswordLogin" class="form-item"> <view class="form-item">
<text class="form-label"> <text class="form-label">
验证码 验证码
</text> </text>
@@ -232,27 +221,6 @@ onUnmounted(() => {
</view> </view>
</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"> <view class="agreement-wrapper">
<wd-checkbox v-model="isAgreed" shape="square" size="18px" /> <wd-checkbox v-model="isAgreed" shape="square" size="18px" />
<text class="agreement-text"> <text class="agreement-text">
@@ -393,8 +361,4 @@ onUnmounted(() => {
letter-spacing: 0.25rem; letter-spacing: 0.25rem;
} }
.switch-login-type {
font-size: 0.875rem;
color: #2563eb;
}
</style> </style>

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
<script setup> <script setup>
definePage({ layout: 'default', auth: true }) definePage({ layout: 'default', auth: true })
import { onReachBottom } from '@dcloudio/uni-app'
// 类型映射配置 // 类型映射配置
const typeConfig = { const typeConfig = {
descendant_promotion: { descendant_promotion: {
@@ -31,6 +32,8 @@ const data = ref({
list: [], list: [],
}) })
const loading = ref(false) const loading = ref(false)
const hasMore = ref(true)
const loadMoreState = ref('loading')
// 类型转中文 // 类型转中文
function typeToChinese(type) { 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 { try {
if (loading.value)
return
if (!reset && !hasMore.value)
return
loading.value = true 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( const { data: res, error } = await useApiFetch(
`/agent/rewards?page=${page.value}&page_size=${pageSize.value}`, `/agent/rewards?page=${page.value}&page_size=${pageSize.value}`,
{ silent: true }, { silent: true },
).get().json() ).get().json()
if (res.value?.code === 200 && !error.value) { 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 { finally {
@@ -66,13 +103,12 @@ async function getData() {
} }
} }
function onPageChange({ value }) {
page.value = value
getData()
}
// 初始化加载 // 初始化加载
onMounted(() => { onMounted(() => {
getData(true)
})
onReachBottom(() => {
getData() getData()
}) })
</script> </script>
@@ -106,14 +142,7 @@ onMounted(() => {
暂无记录 暂无记录
</view> </view>
</view> </view>
<wd-pagination <wd-loadmore :state="loadMoreState" @reload="getData" />
v-model="page"
:total="data.total"
:page-size="pageSize"
show-icon
show-message
@change="onPageChange"
/>
</view> </view>
</template> </template>
@@ -129,6 +158,6 @@ onMounted(() => {
} }
.reward-scroll { .reward-scroll {
height: calc(100vh - 110px); min-height: calc(100vh - 110px);
} }
</style> </style>

View File

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

View File

@@ -2,23 +2,21 @@
import { computed, onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
definePage({ layout: 'default', auth: true }) definePage({ layout: 'default', auth: true })
// 报告类型选项 // 报告类型选项:由后端动态返回
const reportOptions = [ const reportOptions = ref([])
{ text: '小微企业', value: 'companyinfo', id: 2 }, const reportOptionsByValue = computed(() => {
{ text: '贷前风险', value: 'preloanbackgroundcheck', id: 5 }, const map = new Map()
{ text: '个人大数据', value: 'personaldata', id: 27 }, reportOptions.value.forEach((item) => {
{ text: '入职风险', value: 'backgroundcheck', id: 1 }, map.set(item.value, item)
{ text: '家政风险', value: 'homeservice', id: 3 }, })
{ text: '婚恋风险', value: 'marriage', id: 4 }, return map
// { text: "租赁风险", value: "rentalrisk", id: 6 }, })
// { text: "个人风险", value: "riskassessment", id: 7 },
]
// 状态管理 // 状态管理
const showPicker = ref(false) const showPicker = ref(false)
const selectedReport = ref(reportOptions[0]) const selectedReport = ref(null)
const selectedReportText = ref(reportOptions[0].text) const selectedReportText = ref('')
const selectedReportId = ref(reportOptions[0].id) const selectedReportId = ref(null)
const configData = ref({}) const configData = ref({})
const productConfigData = ref({}) const productConfigData = ref({})
@@ -28,6 +26,7 @@ const priceRatioMax = ref(null)
const rangeError = ref(false) const rangeError = ref(false)
const ratioError = ref(false) const ratioError = ref(false)
const increaseError = ref(false) const increaseError = ref(false)
const activeField = ref('')
function showToast(message) { function showToast(message) {
if (!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 selectedReport.value = option
selectedReportText.value = option.text selectedReportText.value = option.label
selectedReportId.value = option.id selectedReportId.value = option.id
showPicker.value = false
// 重置错误状态 // 重置错误状态
rangeError.value = false rangeError.value = false
ratioError.value = false ratioError.value = false
@@ -324,9 +331,53 @@ function closeRangeError() {
rangeError.value = false rangeError.value = false
}, 2000) }, 2000)
} }
function onFieldFocus(field) {
activeField.value = field
}
function onFieldBlur(field) {
if (activeField.value === field)
activeField.value = ''
}
onMounted(() => { 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> </script>
<template> <template>
@@ -342,30 +393,16 @@ onMounted(() => {
</view> </view>
<view class="mb-4"> <view class="mb-4">
<view class="card selector" @click="showPicker = true"> <wd-picker
<view class="selector-label"> v-model="selectedReportId"
📝 选择报告 label="报告类型"
</view> label-width="100px"
<view class="selector-value"> title="选择报告类型"
{{ selectedReportText }} :columns="[reportOptions]"
</view> placeholder="请选择报告类型"
</view> :disabled="!reportOptions.length"
<wd-popup v-model="showPicker" position="bottom" custom-style="padding: 16px;"> @confirm="onConfirmType"
<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>
</view> </view>
<view v-if="selectedReportText" class="space-y-6"> <view v-if="selectedReportText" class="space-y-6">
@@ -421,7 +458,7 @@ onMounted(() => {
</view> </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 class="field-label">
🚀 加价金额 🚀 加价金额
</text> </text>
@@ -433,7 +470,13 @@ onMounted(() => {
class="field-wd-input" class="field-wd-input"
no-border no-border
clearable clearable
@blur="validateDecimal('price_increase_amount')" @focus="onFieldFocus('price_increase_amount')"
@blur="
() => {
onFieldBlur('price_increase_amount')
validateDecimal('price_increase_amount')
}
"
/> />
<text class="field-unit"> <text class="field-unit">
@@ -451,7 +494,7 @@ onMounted(() => {
</view> </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 class="field-label">
💰 最低金额 💰 最低金额
</text> </text>
@@ -463,8 +506,10 @@ onMounted(() => {
class="field-wd-input" class="field-wd-input"
no-border no-border
clearable clearable
@focus="onFieldFocus('price_range_from')"
@blur=" @blur="
() => { () => {
onFieldBlur('price_range_from')
validateDecimal('price_range_from') validateDecimal('price_range_from')
validateRange() validateRange()
} }
@@ -483,7 +528,7 @@ onMounted(() => {
</view> </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 class="field-label">
💰 最高金额 💰 最高金额
</text> </text>
@@ -495,8 +540,10 @@ onMounted(() => {
class="field-wd-input" class="field-wd-input"
no-border no-border
clearable clearable
@focus="onFieldFocus('price_range_to')"
@blur=" @blur="
() => { () => {
onFieldBlur('price_range_to')
validateDecimal('price_range_to') validateDecimal('price_range_to')
validateRange() validateRange()
} }
@@ -515,7 +562,7 @@ onMounted(() => {
</view> </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 class="field-label">
📈 收取比例 📈 收取比例
</text> </text>
@@ -527,7 +574,13 @@ onMounted(() => {
class="field-wd-input" class="field-wd-input"
no-border no-border
clearable clearable
@blur="validateRatio()" @focus="onFieldFocus('price_ratio')"
@blur="
() => {
onFieldBlur('price_ratio')
validateRatio()
}
"
/> />
<text class="field-unit"> <text class="field-unit">
% %
@@ -642,11 +695,11 @@ onMounted(() => {
.field-input-wrap { .field-input-wrap {
display: flex; display: flex;
align-items: center; align-items: center;
background: #f9fafb; background: #f3f4f6;
border-radius: 8px; border-radius: 8px;
padding: 8px 12px; padding: 10px 12px;
transition: all 0.3s ease; transition: all 0.2s ease;
border: 1px solid transparent; border: 1px solid #e5e7eb;
} }
:deep(.field-wd-input .wd-input__inner) { :deep(.field-wd-input .wd-input__inner) {
@@ -661,12 +714,24 @@ onMounted(() => {
font-weight: 500; font-weight: 500;
} }
.custom-field:focus-within .field-input-wrap { ::deep(.field-wd-input.wd-input),
border-color: #bfdbfe; ::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 { .field-error .field-input-wrap {
border-color: #fca5a5; border-color: #fca5a5;
background: #fef2f2; background: #fef2f2;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
} }
</style> </style>

View File

@@ -93,9 +93,11 @@ function toSubordinateList() {
<view class="safe-area-top min-h-screen p-4"> <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-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"> <view class="mb-3 flex items-center justify-between">
<text class="text-lg text-gray-800 font-bold"> <view class="flex items-center">
余额 <text class="text-lg text-gray-800 font-bold">
</text> 余额
</text>
</view>
<text class="text-3xl text-blue-600 font-bold"> <text class="text-3xl text-blue-600 font-bold">
¥ {{ (data?.balance || 0).toFixed(2) }} ¥ {{ (data?.balance || 0).toFixed(2) }}
</text> </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 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"> <view class="mb-4 flex items-center justify-between">
<text class="text-lg text-gray-800 font-bold"> <view class="flex items-center">
直推报告收益 <text class="text-lg text-gray-800 font-bold">
</text> 直推报告收益
</text>
</view>
<view class="text-right"> <view class="text-right">
<text class="text-2xl text-blue-600 font-bold"> <text class="text-2xl text-blue-600 font-bold">
¥ {{ (data?.direct_push?.total_commission || 0).toFixed(2) }} ¥ {{ (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="rounded-xl from-green-50/40 to-cyan-50/30 bg-gradient-to-r p-6 shadow-lg">
<view class="mb-4"> <view class="mb-4">
<text class="text-lg text-gray-800 font-bold"> <text class="text-lg text-gray-800 font-bold">
团队奖励 活跃下级奖励
</text> </text>
</view> </view>
@@ -192,7 +196,7 @@ function toSubordinateList() {
</view> </view>
</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="rounded-lg bg-green-50/60 p-3 backdrop-blur-sm">
<view class="text-sm text-gray-500"> <view class="text-sm text-gray-500">
{{ teamTimeText }}下级推广奖励 {{ teamTimeText }}下级推广奖励
@@ -221,7 +225,7 @@ function toSubordinateList() {
<view class="grid grid-cols-1 gap-3 sm:grid-cols-2"> <view class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<wd-button plain block @click="goToRewardsDetail"> <wd-button plain block @click="goToRewardsDetail">
团队奖励明细 查看奖励明细
</wd-button> </wd-button>
<wd-button type="success" block @click="toSubordinateList"> <wd-button type="success" block @click="toSubordinateList">
查看我的下级 查看我的下级

View File

@@ -1,5 +1,6 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { onReachBottom } from '@dcloudio/uni-app'
definePage({ layout: 'default', auth: true }) definePage({ layout: 'default', auth: true })
const page = ref(1) const page = ref(1)
@@ -7,30 +8,65 @@ const pageSize = ref(10)
const total = ref(0) const total = ref(0)
const reportList = ref([]) const reportList = ref([])
const loading = ref(false) 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 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 }) const { data, error } = await useApiFetch(`query/list?page=${page.value}&page_size=${pageSize.value}`, { silent: true })
.get() .get()
.json() .json()
if (data.value && !error.value) { if (data.value && !error.value) {
if (data.value.code === 200) { if (data.value.code === 200) {
total.value = data.value.data.total 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 loading.value = false
} }
// 初始加载 // 初始加载
onMounted(() => { onMounted(() => {
fetchData() fetchData(true)
}) })
function onPageChange({ value }) { onReachBottom(() => {
page.value = value
fetchData() fetchData()
} })
function toDetail(item) { function toDetail(item) {
if (item.query_state !== 'success') if (item.query_state !== 'success')
@@ -102,20 +138,13 @@ function statusClass(state) {
暂无记录 暂无记录
</view> </view>
</view> </view>
<wd-pagination <wd-loadmore :state="loadMoreState" @reload="fetchData" />
v-model="page"
:total="total"
:page-size="pageSize"
show-icon
show-message
@change="onPageChange"
/>
</view> </view>
</template> </template>
<style scoped> <style scoped>
.history-scroll { .history-scroll {
height: calc(100vh - 120px); min-height: calc(100vh - 120px);
} }
.status-pending { .status-pending {

View File

@@ -60,10 +60,8 @@ function toHistory() {
<template> <template>
<view class="box-border min-h-screen"> <view class="box-border min-h-screen">
<view class="relative p-4"> <view class="relative p-4">
<swiper <swiper class="banner-swiper overflow-hidden rounded-xl" circular autoplay :interval="3000" indicator-dots
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">
indicator-color="rgba(255,255,255,0.5)" indicator-active-color="#ffffff"
>
<swiper-item v-for="(item, index) in banners" :key="index"> <swiper-item v-for="(item, index) in banners" :key="index">
<image :src="item" class="h-full w-full" mode="aspectFill" /> <image :src="item" class="h-full w-full" mode="aspectFill" />
</swiper-item> </swiper-item>
@@ -74,8 +72,7 @@ function toHistory() {
<view class="grid grid-cols-3 gap-3"> <view class="grid grid-cols-3 gap-3">
<view class="flex flex-col items-center justify-center text-center" @click="toPromote"> <view class="flex flex-col items-center justify-center text-center" @click="toPromote">
<view <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" /> <image :src="indexPromoteIcon" class="h-12 w-12" mode="aspectFit" />
</view> </view>
<text class="mt-1 text-center font-bold"> <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="flex flex-col items-center justify-center text-center" @click="toInvitation">
<view <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" /> <image :src="indexInvitationIcon" class="h-12 w-12" mode="aspectFit" />
</view> </view>
<text class="mt-1 text-center font-bold"> <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="flex flex-col items-center justify-center text-center" @click="toHistory">
<view <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" /> <image :src="indexMyReportIcon" class="h-12 w-12" mode="aspectFit" />
</view> </view>
<text class="mt-1 text-center font-bold"> <text class="mt-1 text-center font-bold">
@@ -109,13 +104,11 @@ function toHistory() {
<view class="relative p-4 pt-0"> <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 class="grid grid-cols-2 my-4 gap-4" style="grid-template-rows: repeat(2, 1fr);">
<view <view v-for="(service, index) in services" :key="index"
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="relative min-h-18 flex flex-col rounded-xl px-4 py-2 shadow-lg"
:class="index === 0 ? 'row-span-2' : ''" :class="index === 0 ? 'row-span-2' : ''"
:style="`background: url(${service.bg}) no-repeat; background-size: 100% 100%; background-position: center;`" :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"> <view class="min-h-18 flex items-end">
<!-- <text class="text-base text-gray-700 font-semibold"> <!-- <text class="text-base text-gray-700 font-semibold">
{{ service.title }} {{ 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"> <scroll-view scroll-x class="risk-scroll my-4 px-1 pb-4 pt-2 -mx-1">
<view class="inline-flex gap-2"> <view class="inline-flex gap-2">
<view <view v-for="(service, index) in riskServices" :key="index"
v-for="(service, index) in riskServices" :key="index"
class="relative h-24 w-[107px] flex-shrink-0 rounded-xl shadow-lg" 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;`" :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"> <view class="h-full flex items-end px-2 py-2">
<!-- <text class="text-sm text-gray-700 font-semibold"> <!-- <text class="text-sm text-gray-700 font-semibold">
{{ service.title }} {{ service.title }}
@@ -152,10 +143,8 @@ function toHistory() {
<image :src="honestyBanner" class="block w-full" mode="widthFix" /> <image :src="honestyBanner" class="block w-full" mode="widthFix" />
</view> </view>
<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"
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">
@click="toHistory"
>
<view class="mr-4 h-full flex items-center justify-center"> <view class="mr-4 h-full flex items-center justify-center">
<image class="h-10 w-10" :src="bgIcon" mode="aspectFit" /> <image class="h-10 w-10" :src="bgIcon" mode="aspectFit" />
</view> </view>

View File

@@ -97,7 +97,13 @@ async function performLogin() {
await userStore.fetchUserInfo() await userStore.fetchUserInfo()
await agentStore.fetchAgentStatus() 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 { else {
toast(data.value.msg || '登录失败') toast(data.value.msg || '登录失败')

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { onReachBottom } from '@dcloudio/uni-app'
import { useRoute } from '@/composables/uni-router' import { useRoute } from '@/composables/uni-router'
import useApiFetch from '@/composables/useApiFetch' import useApiFetch from '@/composables/useApiFetch'
@@ -14,15 +15,65 @@ const rewardDetails = ref([])
const userInfo = ref({}) const userInfo = ref({})
const summary = ref({}) const summary = ref({})
const statistics = 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) if (loading.value)
return return
if (!subordinateId.value)
return
if (!reset && !hasMore.value)
return
loading.value = true loading.value = true
if (reset) {
page.value = 1
hasMore.value = true
loadMoreState.value = 'loading'
rewardDetails.value = []
}
const { data, error } = await useApiFetch( 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 }, { silent: true },
) )
.get() .get()
@@ -34,7 +85,7 @@ async function fetchRewardDetails() {
// 更新用户信息 // 更新用户信息
userInfo.value = { userInfo.value = {
createTime: data.value.data.create_time, 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, mobile: data.value.data.mobile,
} }
// 更新汇总数据 // 更新汇总数据
@@ -122,28 +173,57 @@ async function fetchRewardDetails() {
conversionSvipStat.count = stats.descendant_upgrade_svip_count || 0 conversionSvipStat.count = stats.descendant_upgrade_svip_count || 0
} }
} }
rewardDetails.value = []
} }
total.value = data.value.data.total || 0 total.value = data.value.data.total || 0
// 处理列表数据 // 处理列表数据
if (data.value.data.list) { const list = data.value.data.list || []
const list = data.value.data.list rewardDetails.value = reset ? list : mergeUniqueById(rewardDetails.value, list)
rewardDetails.value = list const isLastPage = list.length < pageSize || rewardDetails.value.length >= total.value
} hasMore.value = !isLastPage
else { loadMoreState.value = isLastPage ? 'finished' : 'loading'
rewardDetails.value = [] if (!isLastPage)
} page.value += 1
} }
else {
loadMoreState.value = 'error'
}
}
else {
loadMoreState.value = 'error'
} }
loading.value = false loading.value = false
} }
function onPageChange({ value }) { function resolveSubordinateId(query) {
page.value = value const rawId = query?.id
fetchRewardDetails() ?? 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(() => { 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() fetchRewardDetails()
}) })
@@ -213,7 +293,7 @@ function formatNumber(num) {
{{ userInfo.mobile }} {{ userInfo.mobile }}
</view> </view>
<text class="rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-600 font-medium"> <text class="rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-600 font-medium">
{{ userInfo.level }}代理 {{ getAgentLevelLabel(userInfo.level) }}
</text> </text>
</view> </view>
</view> </view>
@@ -320,16 +400,7 @@ function formatNumber(num) {
加载中... 加载中...
</view> </view>
</view> </view>
<view class="px-4 pb-4"> <wd-loadmore :state="loadMoreState" @reload="fetchRewardDetails" />
<wd-pagination
v-model="page"
:total="total"
:page-size="pageSize"
show-icon
show-message
@change="onPageChange"
/>
</view>
</view> </view>
</view> </view>
</view> </view>

View File

@@ -1,5 +1,6 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { onReachBottom } from '@dcloudio/uni-app'
import useApiFetch from '@/composables/useApiFetch' import useApiFetch from '@/composables/useApiFetch'
definePage({ layout: 'default', auth: true }) definePage({ layout: 'default', auth: true })
@@ -8,34 +9,65 @@ const subordinates = ref([])
const loading = ref(false) const loading = ref(false)
const page = ref(1) const page = ref(1)
const pageSize = 8 const pageSize = 8
const hasMore = ref(true)
const loadMoreState = ref('loading')
// 计算统计数据 // 计算统计数据
const statistics = ref({ const statistics = ref({
totalSubordinates: 0, 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) if (loading.value)
return return
if (!reset && !hasMore.value)
return
loading.value = true 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 }) const { data, error } = await useApiFetch(`/agent/subordinate/list?page=${page.value}&page_size=${pageSize}`, { silent: true })
.get() .get()
.json() .json()
if (data.value && !error.value) { if (data.value && !error.value) {
if (data.value.code === 200) { if (data.value.code === 200) {
statistics.value.totalSubordinates = data.value.data.total 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 loading.value = false
} }
function onPageChange({ value }) {
page.value = value
fetchSubordinates()
}
// 格式化金额 // 格式化金额
function formatNumber(num) { function formatNumber(num) {
if (!num) if (!num)
@@ -45,15 +77,26 @@ function formatNumber(num) {
/** 与后端 /agent/subordinate/list 一致level_name兼容历史 level 字段 */ /** 与后端 /agent/subordinate/list 一致level_name兼容历史 level 字段 */
function getLevelText(item) { 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) { function getLevelClass(level) {
switch (level) { switch (level) {
case 'SVIP': case 'SVIP代理':
return 'bg-purple-100 text-purple-600' return 'bg-purple-100 text-purple-600'
case 'VIP': case 'VIP代理':
return 'bg-blue-100 text-blue-600' return 'bg-blue-100 text-blue-600'
default: default:
return 'bg-gray-100 text-gray-600' return 'bg-gray-100 text-gray-600'
@@ -62,12 +105,22 @@ function getLevelClass(level) {
// 查看详情 // 查看详情
function viewDetail(item) { 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({ uni.navigateTo({
url: `/pages/subordinate-detail?id=${encodeURIComponent(String(item.id))}`, url: `/pages/subordinate-detail?id=${encodeURIComponent(String(parsedId))}`,
}) })
} }
onMounted(() => { onMounted(() => {
fetchSubordinates(true)
})
onReachBottom(() => {
fetchSubordinates() fetchSubordinates()
}) })
</script> </script>
@@ -105,7 +158,7 @@ onMounted(() => {
{{ item.mobile }} {{ item.mobile }}
</view> </view>
<text class="rounded-full px-3 py-1 text-sm font-medium" :class="[getLevelClass(getLevelText(item))]"> <text class="rounded-full px-3 py-1 text-sm font-medium" :class="[getLevelClass(getLevelText(item))]">
{{ getLevelText(item) }}代理 {{ getLevelText(item) }}
</text> </text>
</view> </view>
</view> </view>
@@ -162,16 +215,7 @@ onMounted(() => {
<view v-else-if="!subordinates.length" class="py-4 text-center text-sm text-gray-400"> <view v-else-if="!subordinates.length" class="py-4 text-center text-sm text-gray-400">
暂无下级代理 暂无下级代理
</view> </view>
</view> <wd-loadmore :state="loadMoreState" @reload="fetchSubordinates" />
<view class="px-4 pb-4">
<wd-pagination
v-model="page"
:total="statistics.totalSubordinates"
:page-size="pageSize"
show-icon
show-message
@change="onPageChange"
/>
</view> </view>
</view> </view>
</template> </template>
@@ -183,7 +227,7 @@ onMounted(() => {
} }
.subordinate-scroll { .subordinate-scroll {
height: calc(100vh - 180px); min-height: calc(100vh - 180px);
} }
.subordinate-item { .subordinate-item {

View File

@@ -1,5 +1,6 @@
<script setup> <script setup>
definePage({ layout: 'default', auth: true }) definePage({ layout: 'default', auth: true })
import { onReachBottom } from '@dcloudio/uni-app'
// 状态映射配置 // 状态映射配置
const statusConfig = { const statusConfig = {
1: { 1: {
@@ -38,10 +39,8 @@ const data = ref({
list: [], list: [],
}) })
const loading = ref(false) const loading = ref(false)
const pageCount = computed(() => { const hasMore = ref(true)
const total = Number(data.value.total || 0) const loadMoreState = ref('loading')
return total > 0 ? Math.ceil(total / pageSize.value) : 1
})
// 账户脱敏处理 // 账户脱敏处理
function maskName(name) { 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 { try {
if (loading.value)
return
if (!reset && !hasMore.value)
return
loading.value = true 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( const { data: res, error } = await useApiFetch(
`/agent/withdrawal?page=${page.value}&page_size=${pageSize.value}`, `/agent/withdrawal?page=${page.value}&page_size=${pageSize.value}`,
{ silent: true }, { silent: true },
) )
.get() .get()
.json() .json()
if (res.value?.code === 200 && !error.value) 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 { finally {
loading.value = false loading.value = false
} }
} }
function onPageChange({ value }) {
page.value = value
getData()
}
// 初始化加载 // 初始化加载
onMounted(async () => { onMounted(async () => {
page.value = 1 await getData(true)
await getData() })
onReachBottom(() => {
getData()
}) })
</script> </script>
@@ -173,17 +203,7 @@ onMounted(async () => {
</view> </view>
</view> </view>
<view class="pagination-wrap"> <view class="pagination-wrap">
<wd-pagination <wd-loadmore :state="loadMoreState" @reload="getData" />
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>
</view> </view>
</template> </template>
@@ -204,9 +224,6 @@ onMounted(async () => {
} }
.pagination-wrap { .pagination-wrap {
position: sticky;
bottom: 0;
z-index: 10;
border-top: 1px solid #f1f5f9; border-top: 1px solid #f1f5f9;
background: #fff; background: #fff;
padding: 8px 12px 12px; 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),
}
}