Files
bdrp-app/src/pages/subordinate-detail.vue
2026-07-17 15:58:25 +08:00

410 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { onMounted, ref } from 'vue'
import { onReachBottom } from '@dcloudio/uni-app'
import { useRoute } from '@/composables/uni-router'
import useApiFetch from '@/composables/useApiFetch'
import { ensureCurrentPageAppReviewAllowed } from '@/composables/useAppReviewPresentation'
definePage({ layout: 'default', auth: true })
onShow(() => {
ensureCurrentPageAppReviewAllowed()
})
const route = useRoute()
const loading = ref(false)
const page = ref(1)
const pageSize = 8
const total = ref(0)
const rewardDetails = ref([])
const userInfo = ref({})
const summary = ref({})
const statistics = ref([])
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)] || '普通代理'
}
// 获取收益列表
function appendList(oldList, newList) {
return oldList.concat(newList)
}
async function fetchRewardDetails(reset = false) {
if (loading.value)
return
if (!subordinateId.value)
return
if (!reset && !hasMore.value)
return
loading.value = true
if (reset) {
page.value = 1
hasMore.value = true
loadMoreState.value = 'loading'
rewardDetails.value = []
}
const { data, error } = await useApiFetch(
`/agent/subordinate/contribution/detail?subordinate_id=${subordinateId.value}&page=${page.value}&page_size=${pageSize}`,
{ silent: true },
)
.get()
.json()
if (data.value && !error.value) {
if (data.value.code === 200) {
if (page.value === 1) {
// 更新用户信息
userInfo.value = {
createTime: data.value.data.create_time,
level: data.value.data.level_name || data.value.data.level || '',
mobile: data.value.data.mobile,
}
// 更新汇总数据
summary.value = {
totalReward: data.value.data.total_earnings,
totalContribution: data.value.data.total_contribution,
totalOrders: data.value.data.total_orders,
}
// 设置默认的统计类型
statistics.value = [
{
type: 'descendant_promotion',
amount: 0,
count: 0,
description: '推广奖励',
},
{
type: 'cost',
amount: 0,
count: 0,
description: '成本贡献',
},
{
type: 'pricing',
amount: 0,
count: 0,
description: '定价贡献',
},
{
type: 'descendant_withdraw',
amount: 0,
count: 0,
description: '提现收益',
},
{
type: 'descendant_upgrade_vip',
amount: 0,
count: 0,
description: '转化VIP奖励',
},
{
type: 'descendant_upgrade_svip',
amount: 0,
count: 0,
description: '转化SVIP奖励',
},
]
// 如果有统计数据,更新对应的值
if (data.value.data.stats) {
const stats = data.value.data.stats
// 更新推广奖励
const platformStat = statistics.value.find(s => s.type === 'descendant_promotion')
if (platformStat) {
platformStat.amount = stats.descendant_promotion_amount || 0
platformStat.count = stats.descendant_promotion_count || 0
}
// 更新成本贡献
const costStat = statistics.value.find(s => s.type === 'cost')
if (costStat) {
costStat.amount = stats.cost_amount || 0
costStat.count = stats.cost_count || 0
}
// 更新定价贡献
const pricingStat = statistics.value.find(s => s.type === 'pricing')
if (pricingStat) {
pricingStat.amount = stats.pricing_amount || 0
pricingStat.count = stats.pricing_count || 0
}
// 更新提现收益
const withdrawStat = statistics.value.find(s => s.type === 'descendant_withdraw')
if (withdrawStat) {
withdrawStat.amount = stats.descendant_withdraw_amount || 0
withdrawStat.count = stats.descendant_withdraw_count || 0
}
// 更新转化VIP奖励
const conversionVipStat = statistics.value.find(s => s.type === 'descendant_upgrade_vip')
if (conversionVipStat) {
conversionVipStat.amount = stats.descendant_upgrade_vip_amount || 0
conversionVipStat.count = stats.descendant_upgrade_vip_count || 0
}
// 更新转化SVIP奖励
const conversionSvipStat = statistics.value.find(s => s.type === 'descendant_upgrade_svip')
if (conversionSvipStat) {
conversionSvipStat.amount = stats.descendant_upgrade_svip_amount || 0
conversionSvipStat.count = stats.descendant_upgrade_svip_count || 0
}
}
}
total.value = data.value.data.total || 0
// 处理列表数据
const list = data.value.data.list || []
rewardDetails.value = reset ? list : appendList(rewardDetails.value, list)
const isLastPage = list.length < pageSize || rewardDetails.value.length >= total.value
hasMore.value = !isLastPage
loadMoreState.value = isLastPage ? 'finished' : 'loading'
if (!isLastPage)
page.value += 1
}
else {
loadMoreState.value = 'error'
}
}
else {
loadMoreState.value = 'error'
}
loading.value = false
}
function resolveSubordinateId(query) {
const rawId = query?.id
?? query?.subordinate_id
?? route.params?.id
?? route.query?.id
?? route.params?.subordinate_id
?? route.query?.subordinate_id
const parsedId = Number.parseInt(String(rawId ?? ''), 10)
return Number.isFinite(parsedId) && parsedId > 0 ? parsedId : 0
}
onLoad((query) => {
const parsedId = resolveSubordinateId(query)
subordinateId.value = parsedId
})
onMounted(() => {
const parsedId = subordinateId.value || resolveSubordinateId()
if (!Number.isFinite(parsedId) || parsedId <= 0) {
uni.showToast({ title: '下级参数错误', icon: 'none' })
setTimeout(() => {
uni.navigateBack()
}, 800)
return
}
subordinateId.value = parsedId
fetchRewardDetails(true)
})
onReachBottom(() => {
fetchRewardDetails()
})
// 获取收益类型样式
function getRewardTypeClass(type) {
const typeMap = {
descendant_promotion: 'bg-blue-100 text-blue-600',
cost: 'bg-green-100 text-green-600',
pricing: 'bg-purple-100 text-purple-600',
descendant_withdraw: 'bg-yellow-100 text-yellow-600',
descendant_upgrade_vip: 'bg-red-100 text-red-600',
descendant_upgrade_svip: 'bg-orange-100 text-orange-600',
}
return typeMap[type] || 'bg-gray-100 text-gray-600'
}
// 获取收益类型图标
function getRewardTypeIcon(type) {
const iconMap = {
descendant_promotion: '/static/images/tgjl.svg',
cost: '/static/images/cbgx.svg',
pricing: '/static/images/djgx.svg',
descendant_withdraw: '/static/images/txsy.svg',
descendant_upgrade_vip: '/static/images/zhvipjl.svg',
descendant_upgrade_svip: '/static/images/zhsvipjl.svg',
}
return iconMap[type] || '/static/images/tgjl.svg'
}
// 获取收益类型描述
function getRewardTypeDescription(type) {
const descriptionMap = {
descendant_promotion: '推广奖励',
cost: '成本贡献',
pricing: '定价贡献',
descendant_withdraw: '提现收益',
descendant_upgrade_vip: '转化VIP奖励',
descendant_upgrade_svip: '转化SVIP奖励',
}
return descriptionMap[type] || '未知类型'
}
// 格式化时间
function formatTime(timeStr) {
if (!timeStr)
return '-'
const date = new Date(timeStr)
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
}
// 格式化金额
function formatNumber(num) {
if (!num)
return '0.00'
return Number(num).toFixed(2)
}
</script>
<template>
<view class="reward-detail">
<!-- 用户信息卡片 -->
<view class="p-4">
<view class="mb-4 rounded-xl bg-white p-5 shadow-sm">
<view class="mb-4 flex items-center justify-between">
<view class="flex items-center space-x-3">
<view class="text-xl text-gray-800 font-semibold">
{{ userInfo.mobile }}
</view>
<text class="rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-600 font-medium">
{{ getAgentLevelLabel(userInfo.level) }}
</text>
</view>
</view>
<view class="mb-4 text-sm text-gray-500">
成为下级代理时间{{ formatTime(userInfo.createTime) }}
</view>
<view class="grid grid-cols-3 gap-4">
<view class="text-center">
<view class="mb-1 text-sm text-gray-500">
总推广单量
</view>
<view class="text-xl text-blue-600 font-semibold">
{{ summary.totalOrders }}
</view>
</view>
<view class="text-center">
<view class="mb-1 text-sm text-gray-500">
总收益
</view>
<view class="text-xl text-green-600 font-semibold">
¥{{ formatNumber(summary.totalReward) }}
</view>
</view>
<view class="text-center">
<view class="mb-1 text-sm text-gray-500">
总贡献
</view>
<view class="text-xl text-purple-600 font-semibold">
¥{{ formatNumber(summary.totalContribution) }}
</view>
</view>
</view>
</view>
<!-- 贡献统计卡片 -->
<view class="mb-4 rounded-xl bg-white p-4 shadow-sm">
<view class="mb-3 text-base text-gray-800 font-medium">
贡献统计
</view>
<view class="grid grid-cols-2 gap-3">
<view v-for="item in statistics" :key="item.type" class="flex items-center rounded-lg p-2"
:class="getRewardTypeClass(item.type).split(' ')[0]">
<image :src="getRewardTypeIcon(item.type)" class="mr-2 h-5 w-5 flex-shrink-0" />
<view class="flex-1">
<view class="text-sm font-medium" :class="getRewardTypeClass(item.type).split(' ')[1]">
{{ item.description }}
</view>
<view class="mt-1 flex items-center justify-between">
<view class="text-xs text-gray-500">
{{ item.count }}
</view>
<view class="text-sm font-medium" :class="getRewardTypeClass(item.type).split(' ')[1]">
¥{{ formatNumber(item.amount) }}
</view>
</view>
</view>
</view>
</view>
</view>
<view class="mb-4 rounded-xl bg-white p-4 shadow-sm">
<!-- 贡献记录列表 -->
<view class="text-base text-gray-800 font-medium mb-3">
贡献记录
</view>
<view class="detail-scroll">
<EmptyState v-if="!loading && rewardDetails.length === 0" text="暂无贡献记录" />
<view v-for="(item, index) in rewardDetails" v-else :key="index" class="reward-item">
<view class="mb-3 border-b border-gray-200 pb-3">
<view class="flex items-center justify-between">
<view class="flex items-center space-x-3">
<image :src="getRewardTypeIcon(item.type)" class="h-4 w-4 flex-shrink-0" />
<view>
<view class="text-gray-800 font-sm">
{{ getRewardTypeDescription(item.type) }}
</view>
<view class="text-xs text-gray-500">
{{ formatTime(item.create_time) }}
</view>
</view>
</view>
<view class="text-right">
<view class="text-base font-semibold" :class="getRewardTypeClass(item.type).split(' ')[1]">
¥{{ formatNumber(item.amount) }}
</view>
</view>
</view>
</view>
</view>
<view v-if="loading" class="py-3 text-center text-sm text-gray-400">
加载中...
</view>
</view>
<wd-loadmore v-if="rewardDetails.length > 0" :state="loadMoreState" @reload="fetchRewardDetails" />
</view>
</view>
</view>
</template>
<style scoped>
.reward-detail {
min-height: 100vh;
background-color: #f5f5f5;
}
.reward-item {
transition: transform 0.2s;
}
.reward-item:active {
transform: scale(0.98);
}
.detail-scroll {
min-height: 50vh;
}
</style>