f
This commit is contained in:
@@ -35,6 +35,30 @@ export const getAgentRewards = (params) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我的下级列表
|
||||||
|
* @param {Object} params 查询参数 {page, page_size}
|
||||||
|
*/
|
||||||
|
export const getAgentSubordinateList = (params) => {
|
||||||
|
return request({
|
||||||
|
url: '/agent/subordinate/list',
|
||||||
|
method: 'GET',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下级贡献详情
|
||||||
|
* @param {Object} params 查询参数 {subordinate_id, page, page_size}
|
||||||
|
*/
|
||||||
|
export const getAgentSubordinateContributionDetail = (params) => {
|
||||||
|
return request({
|
||||||
|
url: '/agent/subordinate/contribution/detail',
|
||||||
|
method: 'GET',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取代理状态
|
* 获取代理状态
|
||||||
*/
|
*/
|
||||||
|
|||||||
4
src/auto-imports.d.ts
vendored
4
src/auto-imports.d.ts
vendored
@@ -13,6 +13,7 @@ declare global {
|
|||||||
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 buildPromotionH5Url: typeof import('./utils/promotionH5Url.js')['buildPromotionH5Url']
|
const buildPromotionH5Url: typeof import('./utils/promotionH5Url.js')['buildPromotionH5Url']
|
||||||
|
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 computed: typeof import('vue')['computed']
|
const computed: typeof import('vue')['computed']
|
||||||
@@ -134,6 +135,7 @@ declare global {
|
|||||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||||
|
const safeTruncate: typeof import('./utils/promotionPricing.js')['safeTruncate']
|
||||||
const setMiniPromotionShareFriend: typeof import('./utils/miniPromotionSharePayload.js')['setMiniPromotionShareFriend']
|
const setMiniPromotionShareFriend: typeof import('./utils/miniPromotionSharePayload.js')['setMiniPromotionShareFriend']
|
||||||
const setupRouterGuard: typeof import('./utils/routerGuard.js')['setupRouterGuard']
|
const setupRouterGuard: typeof import('./utils/routerGuard.js')['setupRouterGuard']
|
||||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||||
@@ -358,6 +360,7 @@ declare module 'vue' {
|
|||||||
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 buildPromotionH5Url: UnwrapRef<typeof import('./utils/promotionH5Url.js')['buildPromotionH5Url']>
|
readonly buildPromotionH5Url: UnwrapRef<typeof import('./utils/promotionH5Url.js')['buildPromotionH5Url']>
|
||||||
|
readonly calculatePromotionPricing: UnwrapRef<typeof import('./utils/promotionPricing.js')['calculatePromotionPricing']>
|
||||||
readonly chatCrypto: UnwrapRef<typeof import('./utils/chatCrypto.js')['default']>
|
readonly chatCrypto: UnwrapRef<typeof import('./utils/chatCrypto.js')['default']>
|
||||||
readonly chatEncrypt: UnwrapRef<typeof import('./utils/chatEncrypt.js')['default']>
|
readonly chatEncrypt: UnwrapRef<typeof import('./utils/chatEncrypt.js')['default']>
|
||||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||||
@@ -479,6 +482,7 @@ declare module 'vue' {
|
|||||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||||
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
|
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
|
||||||
|
readonly safeTruncate: UnwrapRef<typeof import('./utils/promotionPricing.js')['safeTruncate']>
|
||||||
readonly setMiniPromotionShareFriend: UnwrapRef<typeof import('./utils/miniPromotionSharePayload.js')['setMiniPromotionShareFriend']>
|
readonly setMiniPromotionShareFriend: UnwrapRef<typeof import('./utils/miniPromotionSharePayload.js')['setMiniPromotionShareFriend']>
|
||||||
readonly setupRouterGuard: UnwrapRef<typeof import('./utils/routerGuard.js')['setupRouterGuard']>
|
readonly setupRouterGuard: UnwrapRef<typeof import('./utils/routerGuard.js')['setupRouterGuard']>
|
||||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||||
|
|||||||
1
src/components.d.ts
vendored
1
src/components.d.ts
vendored
@@ -24,6 +24,7 @@ declare module 'vue' {
|
|||||||
WdIcon: typeof import('wot-design-uni/components/wd-icon/wd-icon.vue')['default']
|
WdIcon: typeof import('wot-design-uni/components/wd-icon/wd-icon.vue')['default']
|
||||||
WdInput: typeof import('wot-design-uni/components/wd-input/wd-input.vue')['default']
|
WdInput: typeof import('wot-design-uni/components/wd-input/wd-input.vue')['default']
|
||||||
WdNavbar: typeof import('wot-design-uni/components/wd-navbar/wd-navbar.vue')['default']
|
WdNavbar: typeof import('wot-design-uni/components/wd-navbar/wd-navbar.vue')['default']
|
||||||
|
WdPagination: typeof import('wot-design-uni/components/wd-pagination/wd-pagination.vue')['default']
|
||||||
WdPicker: typeof import('wot-design-uni/components/wd-picker/wd-picker.vue')['default']
|
WdPicker: typeof import('wot-design-uni/components/wd-picker/wd-picker.vue')['default']
|
||||||
WdPopup: typeof import('wot-design-uni/components/wd-popup/wd-popup.vue')['default']
|
WdPopup: typeof import('wot-design-uni/components/wd-popup/wd-popup.vue')['default']
|
||||||
WdTabbar: typeof import('wot-design-uni/components/wd-tabbar/wd-tabbar.vue')['default']
|
WdTabbar: typeof import('wot-design-uni/components/wd-tabbar/wd-tabbar.vue')['default']
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
|
|
||||||
<div class="border-b border-gray-200">
|
<div class="border-b border-gray-200">
|
||||||
<wd-input v-model="price" type="number" label="¥" label-width="28px" size="large"
|
<wd-input v-model="price" type="number" label="¥" label-width="28px" size="large"
|
||||||
:placeholder="priceRangePlaceholder" @blur="onBlurPrice"
|
:placeholder="priceRangePlaceholder" @blur="onBlurPrice" custom-class="wd-input" />
|
||||||
custom-class="wd-input" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between mt-2">
|
<div class="flex items-center justify-between mt-2">
|
||||||
<div>推广收益为<span class="text-orange-500"> {{ promotionRevenue }} </span>元</div>
|
<div>推广收益为<span class="text-orange-500"> {{ promotionRevenue }} </span>元</div>
|
||||||
@@ -40,6 +39,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, toRefs } from 'vue'
|
import { ref, computed, watch, toRefs } from 'vue'
|
||||||
import { useToast } from 'wot-design-uni'
|
import { useToast } from 'wot-design-uni'
|
||||||
|
import { calculatePromotionPricing, safeTruncate } from '@/utils/promotionPricing.js'
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
defaultPrice: {
|
defaultPrice: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -67,31 +67,12 @@ const priceRangePlaceholder = computed(() => {
|
|||||||
return `${min} - ${max}`
|
return `${min} - ${max}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const costPrice = computed(() => {
|
const pricingResult = computed(() => {
|
||||||
if (!productConfig.value) return 0.00
|
return calculatePromotionPricing(price.value, productConfig.value)
|
||||||
// 平台定价成本
|
|
||||||
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)
|
||||||
});
|
|
||||||
|
|
||||||
// 价格校验与修正逻辑
|
// 价格校验与修正逻辑
|
||||||
const validatePrice = (currentPrice) => {
|
const validatePrice = (currentPrice) => {
|
||||||
@@ -130,15 +111,6 @@ const validatePrice = (currentPrice) => {
|
|||||||
}
|
}
|
||||||
return { newPrice, message };
|
return { newPrice, message };
|
||||||
}
|
}
|
||||||
function safeTruncate(num, decimals = 2) {
|
|
||||||
if (isNaN(num) || !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)
|
||||||
const onConfirm = () => {
|
const onConfirm = () => {
|
||||||
if (isManualConfirm.value) return
|
if (isManualConfirm.value) return
|
||||||
|
|||||||
@@ -118,7 +118,23 @@
|
|||||||
"path": "pages/rewardsDetails",
|
"path": "pages/rewardsDetails",
|
||||||
"type": "page",
|
"type": "page",
|
||||||
"layout": "page",
|
"layout": "page",
|
||||||
"title": "收益明细",
|
"title": "代理奖励收益明细",
|
||||||
|
"agent": true,
|
||||||
|
"auth": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/subordinateDetail",
|
||||||
|
"type": "page",
|
||||||
|
"layout": "page",
|
||||||
|
"title": "下级贡献详情",
|
||||||
|
"agent": true,
|
||||||
|
"auth": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/subordinateList",
|
||||||
|
"type": "page",
|
||||||
|
"layout": "page",
|
||||||
|
"title": "我的下级",
|
||||||
"agent": true,
|
"agent": true,
|
||||||
"auth": true
|
"auth": true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const promoteDateOptions = [
|
|||||||
]
|
]
|
||||||
const selectedPromoteDate = ref('today')
|
const selectedPromoteDate = ref('today')
|
||||||
|
|
||||||
// 团队奖励数据(仅保留:下级推广/下级转化/下级提现)
|
// 活跃下级奖励数据(保留:下级推广/下级转化/下级提现)
|
||||||
const teamDateOptions = [
|
const teamDateOptions = [
|
||||||
{ label: '今日', value: 'today' },
|
{ label: '今日', value: 'today' },
|
||||||
{ label: '近7天', value: 'week' },
|
{ label: '近7天', value: 'week' },
|
||||||
@@ -94,6 +94,12 @@ function goToRewardsDetail() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toSubordinateList() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/subordinateList'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function toWithdraw() {
|
function toWithdraw() {
|
||||||
// 弹出公众号二维码提示提现
|
// 弹出公众号二维码提示提现
|
||||||
showGzhQrcode.value = true
|
showGzhQrcode.value = true
|
||||||
@@ -118,8 +124,6 @@ function toWithdrawDetails() {
|
|||||||
<view class="rounded-xl shadow-lg mb-4 bg-gradient-to-r from-blue-50/70 to-blue-100/50 p-6">
|
<view class="rounded-xl shadow-lg mb-4 bg-gradient-to-r from-blue-50/70 to-blue-100/50 p-6">
|
||||||
<view class="flex justify-between items-center mb-3">
|
<view class="flex justify-between items-center mb-3">
|
||||||
<view class="flex items-center">
|
<view class="flex items-center">
|
||||||
|
|
||||||
|
|
||||||
<text class="text-lg font-bold text-gray-800">余额</text>
|
<text class="text-lg font-bold text-gray-800">余额</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="text-3xl text-blue-600 font-bold">¥ {{ (data?.balance || 0).toFixed(2) }}</text>
|
<text class="text-3xl text-blue-600 font-bold">¥ {{ (data?.balance || 0).toFixed(2) }}</text>
|
||||||
@@ -185,11 +189,11 @@ function toWithdrawDetails() {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 团队奖励(移除活跃下级/新增活跃,仅保留下级推广、转化、提现) -->
|
<!-- 活跃下级奖励(保留下级推广、转化、提现) -->
|
||||||
<view class="rounded-xl shadow-lg bg-gradient-to-r from-green-50/40 to-cyan-50/30 p-6">
|
<view class="rounded-xl shadow-lg bg-gradient-to-r from-green-50/40 to-cyan-50/30 p-6">
|
||||||
<view class="flex justify-between items-center mb-4">
|
<view class="flex justify-between items-center mb-4">
|
||||||
<view class="flex items-center">
|
<view class="flex items-center">
|
||||||
<text class="text-lg font-bold text-gray-800">团队奖励</text>
|
<text class="text-lg font-bold text-gray-800">活跃下级奖励</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -204,7 +208,7 @@ function toWithdrawDetails() {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="grid grid-cols-1 gap-2 mb-6">
|
<view class="grid grid-cols-2 gap-2 mb-6">
|
||||||
<view class="bg-green-50/60 p-3 rounded-lg backdrop-blur-sm">
|
<view class="bg-green-50/60 p-3 rounded-lg backdrop-blur-sm">
|
||||||
<view class="flex items-center text-sm text-gray-500">
|
<view class="flex items-center text-sm text-gray-500">
|
||||||
<text>{{ teamTimeText }}下级推广奖励</text>
|
<text>{{ teamTimeText }}下级推广奖励</text>
|
||||||
@@ -225,9 +229,19 @@ function toWithdrawDetails() {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="flex items-center justify-between text-green-500 text-sm font-semibold" @click="goToRewardsDetail">
|
<view class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
<text>查看奖励明细</text>
|
<view
|
||||||
<text class="text-lg">→</text>
|
class="w-full rounded-full border border-gray-200/80 bg-white/90 py-3 px-4 text-center text-sm text-gray-600 font-medium shadow-sm"
|
||||||
|
@click="goToRewardsDetail"
|
||||||
|
>
|
||||||
|
<text>查看奖励明细</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="w-full rounded-full bg-gradient-to-r from-green-500 to-emerald-500 py-3 px-4 text-center text-sm text-white font-medium shadow-sm"
|
||||||
|
@click="toSubordinateList"
|
||||||
|
>
|
||||||
|
<text>查看我的下级</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -708,12 +708,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') {
|
||||||
toast.error('SVIP会员不能降级到VIP会员')
|
toast.error('SVIP会员不能降级到VIP会员')
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ const getAgentInformation = async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUser = async () => {
|
const getUser = async () => {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<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 font-bold text-white shadow-sm"
|
<view class="flex items-center justify-center rounded-full px-3 py-1 text-xs font-bold text-white shadow-sm"
|
||||||
:class="levelGradient.badge">
|
:class="levelGradient.badge">
|
||||||
{{ levelNames[level] }}
|
{{ levelNames[normalizedLevel] }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
{{ !isLoggedIn ? '点击登录' : (userType === 0 ? '绑定手机号' : maskName(userName)) }}
|
{{ !isLoggedIn ? '点击登录' : (userType === 0 ? '绑定手机号' : maskName(userName)) }}
|
||||||
</view>
|
</view>
|
||||||
<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>
|
||||||
@@ -106,6 +106,17 @@ const isAgent = ref(false)
|
|||||||
const level = ref('normal')
|
const level = ref('normal')
|
||||||
const ExpiryTime = ref('')
|
const ExpiryTime = ref('')
|
||||||
|
|
||||||
|
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 showUpdateProgress = ref(false)
|
const showUpdateProgress = ref(false)
|
||||||
|
|
||||||
@@ -136,6 +147,7 @@ onBeforeMount(() => {
|
|||||||
|
|
||||||
const levelNames = {
|
const levelNames = {
|
||||||
normal: '普通代理',
|
normal: '普通代理',
|
||||||
|
NORMAL: '普通代理',
|
||||||
'': '普通代理',
|
'': '普通代理',
|
||||||
VIP: 'VIP代理',
|
VIP: 'VIP代理',
|
||||||
SVIP: 'SVIP代理',
|
SVIP: 'SVIP代理',
|
||||||
@@ -143,37 +155,41 @@ const levelNames = {
|
|||||||
|
|
||||||
const levelText = {
|
const levelText = {
|
||||||
normal: '基础代理特权',
|
normal: '基础代理特权',
|
||||||
|
NORMAL: '基础代理特权',
|
||||||
'': '基础代理特权',
|
'': '基础代理特权',
|
||||||
VIP: '高级代理特权',
|
VIP: '高级代理特权',
|
||||||
SVIP: '尊享代理特权',
|
SVIP: '尊享代理特权',
|
||||||
}
|
}
|
||||||
|
|
||||||
const isVipOrSvip = computed(() => {
|
const isVipOrSvip = computed(() => {
|
||||||
const l = (level.value || '').toString()
|
const l = normalizedLevel.value
|
||||||
return ['VIP', 'SVIP'].includes(l)
|
return ['VIP', 'SVIP'].includes(l)
|
||||||
})
|
})
|
||||||
|
|
||||||
const levelGradient = computed(() => ({
|
const levelGradient = computed(() => ({
|
||||||
border: {
|
border: {
|
||||||
normal: 'bg-green-300',
|
normal: 'bg-green-300',
|
||||||
|
NORMAL: 'bg-green-300',
|
||||||
'': 'bg-green-300',
|
'': 'bg-green-300',
|
||||||
VIP: 'bg-gradient-to-r from-yellow-400 to-amber-500',
|
VIP: 'bg-gradient-to-r from-yellow-400 to-amber-500',
|
||||||
SVIP: 'bg-gradient-to-r from-purple-400 to-pink-400 shadow-[0_0_15px_rgba(163,51,200,0.2)]',
|
SVIP: 'bg-gradient-to-r from-purple-400 to-pink-400 shadow-[0_0_15px_rgba(163,51,200,0.2)]',
|
||||||
}[level.value],
|
}[normalizedLevel.value],
|
||||||
|
|
||||||
badge: {
|
badge: {
|
||||||
normal: 'bg-green-500',
|
normal: 'bg-green-500',
|
||||||
|
NORMAL: 'bg-green-500',
|
||||||
'': 'bg-green-500',
|
'': 'bg-green-500',
|
||||||
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-green-600',
|
normal: 'text-green-600',
|
||||||
|
NORMAL: 'text-green-600',
|
||||||
'': 'text-green-600',
|
'': 'text-green-600',
|
||||||
VIP: 'text-amber-600',
|
VIP: 'text-amber-600',
|
||||||
SVIP: 'text-purple-600',
|
SVIP: 'text-purple-600',
|
||||||
}[level.value],
|
}[normalizedLevel.value],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function toHistory() {
|
function toHistory() {
|
||||||
@@ -247,8 +263,9 @@ function toBindPhone() {
|
|||||||
const getDefaultAvatar = () => {
|
const getDefaultAvatar = () => {
|
||||||
if (!isAgent.value) return '/static/image/head_shot.webp'
|
if (!isAgent.value) return '/static/image/head_shot.webp'
|
||||||
|
|
||||||
switch (level.value) {
|
switch (normalizedLevel.value) {
|
||||||
case 'normal':
|
case 'normal':
|
||||||
|
case 'NORMAL':
|
||||||
case '':
|
case '':
|
||||||
return '/static/image/shot_nonal.png'
|
return '/static/image/shot_nonal.png'
|
||||||
case 'VIP':
|
case 'VIP':
|
||||||
|
|||||||
@@ -69,6 +69,7 @@
|
|||||||
import { getProductConfig, generatePromotionLink } from '@/apis/agent'
|
import { getProductConfig, generatePromotionLink } from '@/apis/agent'
|
||||||
import { usePromotionShareHandlers } from '@/composables/usePromotionShareHandlers'
|
import { usePromotionShareHandlers } from '@/composables/usePromotionShareHandlers'
|
||||||
import { getAgentTabShareTitle } from '@/utils/runtimeEnv.js'
|
import { getAgentTabShareTitle } from '@/utils/runtimeEnv.js'
|
||||||
|
import { calculatePromotionPricing } from '@/utils/promotionPricing.js'
|
||||||
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 QRcode from '@/components/QRcode.vue'
|
import QRcode from '@/components/QRcode.vue'
|
||||||
@@ -103,46 +104,12 @@ const reportTypes = computed(() => {
|
|||||||
.filter(item => !!item.value)
|
.filter(item => !!item.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算成本价格
|
const pricingResult = computed(() => {
|
||||||
const costPrice = computed(() => {
|
return calculatePromotionPricing(formData.value.clientPrice, pickerProductConfig.value)
|
||||||
if (!pickerProductConfig.value) return '0.00'
|
|
||||||
// 平台定价成本
|
|
||||||
let platformPricing = 0
|
|
||||||
platformPricing += pickerProductConfig.value.cost_price
|
|
||||||
if (formData.value.clientPrice > pickerProductConfig.value.p_pricing_standard) {
|
|
||||||
platformPricing += (formData.value.clientPrice - pickerProductConfig.value.p_pricing_standard) * pickerProductConfig.value.p_overpricing_ratio
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pickerProductConfig.value.a_pricing_standard > platformPricing &&
|
|
||||||
pickerProductConfig.value.a_pricing_end > platformPricing &&
|
|
||||||
pickerProductConfig.value.a_overpricing_ratio > 0) {
|
|
||||||
if (formData.value.clientPrice > pickerProductConfig.value.a_pricing_standard) {
|
|
||||||
if (formData.value.clientPrice > pickerProductConfig.value.a_pricing_end) {
|
|
||||||
platformPricing += (pickerProductConfig.value.a_pricing_end - pickerProductConfig.value.a_pricing_standard) * pickerProductConfig.value.a_overpricing_ratio
|
|
||||||
} else {
|
|
||||||
platformPricing += (formData.value.clientPrice - pickerProductConfig.value.a_pricing_standard) * pickerProductConfig.value.a_overpricing_ratio
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return safeTruncate(platformPricing)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算推广收益
|
const costPrice = computed(() => pricingResult.value.costPrice)
|
||||||
const promotionRevenue = computed(() => {
|
const promotionRevenue = computed(() => pricingResult.value.promotionRevenue)
|
||||||
return safeTruncate(formData.value.clientPrice - costPrice.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 安全截断数字(保留2位小数)
|
|
||||||
function safeTruncate(num, decimals = 2) {
|
|
||||||
if (isNaN(num) || !isFinite(num)) return "0.00"
|
|
||||||
|
|
||||||
const factor = 10 ** decimals
|
|
||||||
const scaled = Math.trunc(num * factor)
|
|
||||||
const truncated = scaled / factor
|
|
||||||
|
|
||||||
return truncated.toFixed(decimals)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成推广码
|
// 生成推广码
|
||||||
const generatePromotionCode = async () => {
|
const generatePromotionCode = async () => {
|
||||||
|
|||||||
@@ -7,14 +7,23 @@
|
|||||||
<view v-for="(item, index) in list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm">
|
<view v-for="(item, index) in list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm">
|
||||||
<view class="flex justify-between items-center mb-2">
|
<view class="flex justify-between items-center mb-2">
|
||||||
<text class="text-gray-500 text-sm">{{ item.create_time || '-' }}</text>
|
<text class="text-gray-500 text-sm">{{ item.create_time || '-' }}</text>
|
||||||
<text class="text-green-500 font-bold">+{{ item.amount.toFixed(2) }}</text>
|
<text class="font-bold" :class="getAmountColor(item)">
|
||||||
|
{{ getAmountPrefix(item) }}{{ getDisplayAmount(item).toFixed(2) }}
|
||||||
|
</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="flex items-center">
|
<view class="flex items-center justify-between">
|
||||||
<text class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
|
<text class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
|
||||||
:class="getReportTypeStyle(item.product_name)">
|
:class="getReportTypeStyle(item.product_name)">
|
||||||
<text class="w-2 h-2 rounded-full mr-1 inline-block" :class="getDotColor(item.product_name)"></text>
|
<text class="w-2 h-2 rounded-full mr-1 inline-block" :class="getDotColor(item.product_name)"></text>
|
||||||
{{ item.product_name }}
|
{{ item.product_name }}
|
||||||
</text>
|
</text>
|
||||||
|
<text class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"
|
||||||
|
:class="getStatusStyle(item)">
|
||||||
|
{{ getStatusText(item) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="toNumber(item.refunded_amount) > 0 && getDisplayAmount(item) > 0" class="mt-2 text-xs text-gray-400">
|
||||||
|
原始 {{ toNumber(item.amount).toFixed(2) }},已退 {{ toNumber(item.refunded_amount).toFixed(2) }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -25,7 +34,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { getAgentCommission } from '@/apis/agent'
|
import { getAgentCommission } from '@/apis/agent'
|
||||||
|
|
||||||
// 颜色配置(根据产品名称映射)
|
// 颜色配置(根据产品名称映射)
|
||||||
@@ -59,6 +68,57 @@ const getDotColor = (name) => {
|
|||||||
return (typeColors[name] || typeColors.default).dot
|
return (typeColors[name] || typeColors.default).dot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toNumber = (value) => {
|
||||||
|
const n = Number(value)
|
||||||
|
return Number.isFinite(n) ? n : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 与 H5 对齐:优先显示 net_amount,缺失时退化为 amount-refunded_amount
|
||||||
|
const getDisplayAmount = (item) => {
|
||||||
|
if (item && item.net_amount !== undefined && item.net_amount !== null)
|
||||||
|
return toNumber(item.net_amount)
|
||||||
|
return toNumber(item?.amount) - toNumber(item?.refunded_amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusNumber = (item) => {
|
||||||
|
return Number(item?.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAmountColor = (item) => {
|
||||||
|
const status = getStatusNumber(item)
|
||||||
|
if (status === 2)
|
||||||
|
return 'text-red-500'
|
||||||
|
if (toNumber(item?.refunded_amount) > 0)
|
||||||
|
return 'text-orange-500'
|
||||||
|
return 'text-green-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAmountPrefix = (item) => {
|
||||||
|
return getStatusNumber(item) === 2 ? '-' : '+'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (item) => {
|
||||||
|
const status = getStatusNumber(item)
|
||||||
|
if (status === 2)
|
||||||
|
return '已退款'
|
||||||
|
if (status === 1)
|
||||||
|
return toNumber(item?.refunded_amount) > 0 ? '冻结中(部分退款)' : '冻结中'
|
||||||
|
if (status === 0)
|
||||||
|
return toNumber(item?.refunded_amount) > 0 ? '已结算(部分退款)' : '已结算'
|
||||||
|
return '未知状态'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusStyle = (item) => {
|
||||||
|
const status = getStatusNumber(item)
|
||||||
|
if (status === 2)
|
||||||
|
return 'bg-red-100 text-red-800'
|
||||||
|
if (status === 1)
|
||||||
|
return toNumber(item?.refunded_amount) > 0 ? 'bg-orange-100 text-orange-800' : 'bg-yellow-100 text-yellow-800'
|
||||||
|
if (status === 0)
|
||||||
|
return toNumber(item?.refunded_amount) > 0 ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'
|
||||||
|
return 'bg-gray-100 text-gray-800'
|
||||||
|
}
|
||||||
|
|
||||||
// 加载更多数据
|
// 加载更多数据
|
||||||
const onLoadMore = async () => {
|
const onLoadMore = async () => {
|
||||||
if (loadMoreStatus.value === 'noMore') return
|
if (loadMoreStatus.value === 'noMore') return
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ defineExpose({
|
|||||||
<route type="page" lang="json">
|
<route type="page" lang="json">
|
||||||
{
|
{
|
||||||
"layout": "page",
|
"layout": "page",
|
||||||
"title": "收益明细",
|
"title": "代理奖励收益明细",
|
||||||
"agent": true,
|
"agent": true,
|
||||||
"auth": true
|
"auth": true
|
||||||
}
|
}
|
||||||
|
|||||||
296
src/pages/subordinateDetail.vue
Normal file
296
src/pages/subordinateDetail.vue
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { getAgentSubordinateContributionDetail } from '@/apis/agent'
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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'
|
||||||
|
return 'NORMAL'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAgentLevelLabel(value) {
|
||||||
|
return {
|
||||||
|
NORMAL: '普通代理',
|
||||||
|
VIP: 'VIP代理',
|
||||||
|
SVIP: 'SVIP代理',
|
||||||
|
}[normalizeAgentLevel(value)] || '普通代理'
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchRewardDetails() {
|
||||||
|
if (loading.value || !subordinateId.value)
|
||||||
|
return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getAgentSubordinateContributionDetail({
|
||||||
|
subordinate_id: subordinateId.value,
|
||||||
|
page: page.value,
|
||||||
|
page_size: pageSize,
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
if (page.value === 1) {
|
||||||
|
userInfo.value = {
|
||||||
|
createTime: res.data.create_time,
|
||||||
|
level: res.data.level_name || '',
|
||||||
|
mobile: res.data.mobile,
|
||||||
|
}
|
||||||
|
summary.value = {
|
||||||
|
totalReward: res.data.total_earnings,
|
||||||
|
totalContribution: res.data.total_contribution,
|
||||||
|
totalOrders: res.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奖励' },
|
||||||
|
]
|
||||||
|
const stats = res.data.stats || {}
|
||||||
|
const map = {
|
||||||
|
descendant_promotion: ['descendant_promotion_amount', 'descendant_promotion_count'],
|
||||||
|
cost: ['cost_amount', 'cost_count'],
|
||||||
|
pricing: ['pricing_amount', 'pricing_count'],
|
||||||
|
descendant_withdraw: ['descendant_withdraw_amount', 'descendant_withdraw_count'],
|
||||||
|
descendant_upgrade_vip: ['descendant_upgrade_vip_amount', 'descendant_upgrade_vip_count'],
|
||||||
|
descendant_upgrade_svip: ['descendant_upgrade_svip_amount', 'descendant_upgrade_svip_count'],
|
||||||
|
}
|
||||||
|
statistics.value.forEach((item) => {
|
||||||
|
const keys = map[item.type]
|
||||||
|
if (!keys)
|
||||||
|
return
|
||||||
|
item.amount = stats[keys[0]] || 0
|
||||||
|
item.count = stats[keys[1]] || 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
total.value = res.data.total || 0
|
||||||
|
rewardDetails.value = res.data.list || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPageChange({ value }) {
|
||||||
|
page.value = value
|
||||||
|
fetchRewardDetails()
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad((query) => {
|
||||||
|
const parsed = Number.parseInt(String(query?.id || query?.subordinate_id || ''), 10)
|
||||||
|
subordinateId.value = Number.isFinite(parsed) && parsed > 0 ? parsed : 0
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!subordinateId.value) {
|
||||||
|
uni.showToast({ title: '下级参数错误', icon: 'none' })
|
||||||
|
setTimeout(() => uni.navigateBack(), 800)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fetchRewardDetails()
|
||||||
|
})
|
||||||
|
</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]"
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
贡献记录
|
||||||
|
</view>
|
||||||
|
<view class="detail-scroll p-4">
|
||||||
|
<view v-if="rewardDetails.length === 0" class="py-8 text-center text-gray-500">
|
||||||
|
暂无贡献记录
|
||||||
|
</view>
|
||||||
|
<view v-for="item in rewardDetails" v-else :key="item.id" class="reward-item">
|
||||||
|
<view class="mb-3 border-b border-gray-200 pb-3">
|
||||||
|
<view class="flex items-center justify-between">
|
||||||
|
<view>
|
||||||
|
<view class="text-gray-800 font-medium">
|
||||||
|
{{ getRewardTypeDescription(item.type) }}
|
||||||
|
</view>
|
||||||
|
<view class="text-xs text-gray-500">
|
||||||
|
{{ formatTime(item.create_time) }}
|
||||||
|
</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>
|
||||||
|
<view class="px-4 pb-4">
|
||||||
|
<wd-pagination
|
||||||
|
v-model="page"
|
||||||
|
:total="total"
|
||||||
|
:page-size="pageSize"
|
||||||
|
show-icon
|
||||||
|
show-message
|
||||||
|
@change="onPageChange"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<route type="page" lang="json">
|
||||||
|
{
|
||||||
|
"layout": "page",
|
||||||
|
"title": "下级贡献详情",
|
||||||
|
"agent": true,
|
||||||
|
"auth": true
|
||||||
|
}
|
||||||
|
</route>
|
||||||
215
src/pages/subordinateList.vue
Normal file
215
src/pages/subordinateList.vue
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { getAgentSubordinateList } from '@/apis/agent'
|
||||||
|
|
||||||
|
const subordinates = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = 8
|
||||||
|
const statistics = ref({
|
||||||
|
totalSubordinates: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
function normalizeLevel(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'
|
||||||
|
return 'NORMAL'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLevelText(item) {
|
||||||
|
const level = normalizeLevel(item?.level_name || item?.level)
|
||||||
|
return {
|
||||||
|
NORMAL: '普通代理',
|
||||||
|
VIP: 'VIP代理',
|
||||||
|
SVIP: 'SVIP代理',
|
||||||
|
}[level] || '普通代理'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLevelClass(levelText) {
|
||||||
|
switch (levelText) {
|
||||||
|
case 'SVIP代理':
|
||||||
|
return 'bg-purple-100 text-purple-600'
|
||||||
|
case 'VIP代理':
|
||||||
|
return 'bg-blue-100 text-blue-600'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-600'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(num) {
|
||||||
|
if (!num)
|
||||||
|
return '0.00'
|
||||||
|
return Number(num).toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSubordinates() {
|
||||||
|
if (loading.value)
|
||||||
|
return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getAgentSubordinateList({
|
||||||
|
page: page.value,
|
||||||
|
page_size: pageSize,
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
statistics.value.totalSubordinates = res.data.total || 0
|
||||||
|
subordinates.value = res.data.list || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPageChange({ value }) {
|
||||||
|
page.value = value
|
||||||
|
fetchSubordinates()
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewDetail(item) {
|
||||||
|
const id = Number.parseInt(String(item?.id ?? ''), 10)
|
||||||
|
if (!Number.isFinite(id) || id <= 0) {
|
||||||
|
uni.showToast({ title: '下级信息异常,无法查看详情', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/subordinateDetail?id=${encodeURIComponent(String(id))}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchSubordinates()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="subordinate-list">
|
||||||
|
<view class="p-4 pb-0">
|
||||||
|
<view class="rounded-xl bg-white p-4 shadow-sm">
|
||||||
|
<view class="flex items-center justify-center">
|
||||||
|
<view class="text-center">
|
||||||
|
<view class="mb-1 text-sm text-gray-500">
|
||||||
|
下级总数
|
||||||
|
</view>
|
||||||
|
<view class="text-2xl text-blue-600 font-semibold">
|
||||||
|
{{ statistics.totalSubordinates }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="subordinate-scroll p-4">
|
||||||
|
<view v-for="(item, index) in subordinates" :key="item.id" class="subordinate-item">
|
||||||
|
<view class="mb-4 flex flex-col 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="h-6 w-6 flex items-center justify-center rounded-full bg-blue-100 text-sm text-blue-600 font-medium">
|
||||||
|
{{ index + 1 }}
|
||||||
|
</view>
|
||||||
|
<view class="text-xl text-gray-800 font-semibold">
|
||||||
|
{{ item.mobile }}
|
||||||
|
</view>
|
||||||
|
<text class="rounded-full px-3 py-1 text-sm font-medium" :class="[getLevelClass(getLevelText(item))]">
|
||||||
|
{{ getLevelText(item) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="mb-5 text-sm text-gray-500">
|
||||||
|
成为下级代理时间:{{ item.create_time }}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="grid grid-cols-3 mb-5 gap-6">
|
||||||
|
<view class="text-center">
|
||||||
|
<view class="mb-1 text-sm text-gray-500">
|
||||||
|
总推广单量
|
||||||
|
</view>
|
||||||
|
<view class="text-xl text-blue-600 font-semibold">
|
||||||
|
{{ item.total_orders }}
|
||||||
|
</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(item.total_earnings) }}
|
||||||
|
</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(item.total_contribution) }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="flex justify-end">
|
||||||
|
<view
|
||||||
|
class="inline-flex items-center rounded-full from-blue-500 to-blue-400 bg-gradient-to-r px-4 py-2 text-sm text-white shadow-sm"
|
||||||
|
@click="viewDetail(item)"
|
||||||
|
>
|
||||||
|
查看详情
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="loading" class="py-4 text-center text-sm text-gray-400">
|
||||||
|
加载中...
|
||||||
|
</view>
|
||||||
|
<view v-else-if="!subordinates.length" class="py-4 text-center text-sm text-gray-400">
|
||||||
|
暂无下级代理
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="px-4 pb-4">
|
||||||
|
<wd-pagination
|
||||||
|
v-model="page"
|
||||||
|
:total="statistics.totalSubordinates"
|
||||||
|
:page-size="pageSize"
|
||||||
|
show-icon
|
||||||
|
show-message
|
||||||
|
@change="onPageChange"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.subordinate-list {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subordinate-scroll {
|
||||||
|
height: calc(100vh - 180px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subordinate-item {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subordinate-item:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<route type="page" lang="json">
|
||||||
|
{
|
||||||
|
"layout": "page",
|
||||||
|
"title": "我的下级",
|
||||||
|
"agent": true,
|
||||||
|
"auth": true
|
||||||
|
}
|
||||||
|
</route>
|
||||||
2
src/uni-pages.d.ts
vendored
2
src/uni-pages.d.ts
vendored
@@ -18,6 +18,8 @@ interface NavigateToOptions {
|
|||||||
"/pages/promote" |
|
"/pages/promote" |
|
||||||
"/pages/promoteDetails" |
|
"/pages/promoteDetails" |
|
||||||
"/pages/rewardsDetails" |
|
"/pages/rewardsDetails" |
|
||||||
|
"/pages/subordinateDetail" |
|
||||||
|
"/pages/subordinateList" |
|
||||||
"/pages/withdrawDetails";
|
"/pages/withdrawDetails";
|
||||||
}
|
}
|
||||||
interface RedirectToOptions extends NavigateToOptions {}
|
interface RedirectToOptions extends NavigateToOptions {}
|
||||||
|
|||||||
46
src/utils/promotionPricing.js
Normal file
46
src/utils/promotionPricing.js
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ const BASE_URL = getApiPrefix()
|
|||||||
const BASE_URL = getApiBaseUrl()
|
const BASE_URL = getApiBaseUrl()
|
||||||
// #endif
|
// #endif
|
||||||
const TEMP_USER_INVALID_CODE = 100012
|
const TEMP_USER_INVALID_CODE = 100012
|
||||||
|
const USER_DISABLED_CODE = 100011
|
||||||
|
const USER_CANCELLED_CODE = 100013
|
||||||
let wxMiniReauthPromise = null
|
let wxMiniReauthPromise = null
|
||||||
|
|
||||||
const clearAuthData = () => {
|
const clearAuthData = () => {
|
||||||
@@ -19,6 +21,13 @@ const clearAuthData = () => {
|
|||||||
uni.removeStorageSync('agentInfo')
|
uni.removeStorageSync('agentInfo')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const forceLogoutToLogin = () => {
|
||||||
|
clearAuthData()
|
||||||
|
uni.reLaunch({
|
||||||
|
url: '/pages/login',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const silentWxMiniReAuth = () => {
|
const silentWxMiniReAuth = () => {
|
||||||
if (wxMiniReauthPromise)
|
if (wxMiniReauthPromise)
|
||||||
return wxMiniReauthPromise
|
return wxMiniReauthPromise
|
||||||
@@ -90,6 +99,11 @@ function request(options) {
|
|||||||
success: (res) => {
|
success: (res) => {
|
||||||
// 响应拦截器逻辑
|
// 响应拦截器逻辑
|
||||||
if (res.statusCode === 200) {
|
if (res.statusCode === 200) {
|
||||||
|
if (res.data?.code === USER_DISABLED_CODE || res.data?.code === USER_CANCELLED_CODE) {
|
||||||
|
forceLogoutToLogin()
|
||||||
|
reject(res.data)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (res.data?.code === TEMP_USER_INVALID_CODE) {
|
if (res.data?.code === TEMP_USER_INVALID_CODE) {
|
||||||
if (!options.__retriedAfterReauth) {
|
if (!options.__retriedAfterReauth) {
|
||||||
silentWxMiniReAuth()
|
silentWxMiniReAuth()
|
||||||
@@ -111,10 +125,12 @@ function request(options) {
|
|||||||
resolve(res.data)
|
resolve(res.data)
|
||||||
}
|
}
|
||||||
else if (res.statusCode === 401) {
|
else if (res.statusCode === 401) {
|
||||||
uni.removeStorageSync('token')
|
forceLogoutToLogin()
|
||||||
uni.redirectTo({
|
reject(res.data)
|
||||||
url: '/pages/login',
|
}
|
||||||
})
|
else if (res.statusCode === 403) {
|
||||||
|
forceLogoutToLogin()
|
||||||
|
reject(res.data)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
uni.showToast({ title: res.data.msg || '请求失败', icon: 'none' })
|
uni.showToast({ title: res.data.msg || '请求失败', icon: 'none' })
|
||||||
|
|||||||
@@ -1,55 +1,60 @@
|
|||||||
import pagesJson from '@/pages.json'
|
import pagesJson from "@/pages.json";
|
||||||
const pagesConfig = pagesJson.pages
|
const pagesConfig = pagesJson.pages;
|
||||||
|
|
||||||
// 检查登录状态
|
// 检查登录状态
|
||||||
function checkIsLoggedIn() {
|
function checkIsLoggedIn() {
|
||||||
return !!uni.getStorageSync('token')
|
return !!uni.getStorageSync("token");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否是代理
|
// 检查是否是代理
|
||||||
function checkIsAgent() {
|
function checkIsAgent() {
|
||||||
const agentInfo = uni.getStorageSync('agentInfo')
|
const agentInfo = uni.getStorageSync("agentInfo");
|
||||||
return agentInfo?.isAgent
|
return agentInfo?.isAgent;
|
||||||
}
|
}
|
||||||
function checkBindMobile() {
|
function checkBindMobile() {
|
||||||
const userInfo = uni.getStorageSync('userInfo')
|
const userInfo = uni.getStorageSync("userInfo");
|
||||||
return userInfo?.mobile.length > 0
|
return userInfo?.mobile.length > 0;
|
||||||
}
|
}
|
||||||
// 添加路由拦截器
|
// 添加路由拦截器
|
||||||
function setupRouterGuard() {
|
function setupRouterGuard() {
|
||||||
const methods = ['navigateTo', 'redirectTo', 'reLaunch', 'switchTab']
|
const methods = ["navigateTo", "redirectTo", "reLaunch", "switchTab"];
|
||||||
|
|
||||||
methods.forEach(method => {
|
methods.forEach((method) => {
|
||||||
uni.addInterceptor(method, {
|
uni.addInterceptor(method, {
|
||||||
invoke(e) {
|
invoke(e) {
|
||||||
const url = e.url.split('?')[0]
|
const url = e.url.split("?")[0];
|
||||||
const currentPageConfig = pagesConfig.find(page => '/' +page.path === url) // 根据路径查找 pages.json 中的配置
|
const currentPageConfig = pagesConfig.find(
|
||||||
|
(page) => "/" + page.path === url,
|
||||||
|
); // 根据路径查找 pages.json 中的配置
|
||||||
// 检查是否需要登录
|
// 检查是否需要登录
|
||||||
if (currentPageConfig?.auth && (!checkIsLoggedIn() || !checkBindMobile())) {
|
if (
|
||||||
|
currentPageConfig?.auth &&
|
||||||
|
(!checkIsLoggedIn() || !checkBindMobile())
|
||||||
|
) {
|
||||||
// 跳转到登录页
|
// 跳转到登录页
|
||||||
uni.redirectTo({
|
uni.redirectTo({
|
||||||
url: '/pages/login'
|
url: "/pages/login",
|
||||||
})
|
});
|
||||||
|
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否需要代理身份
|
// 检查是否需要代理身份
|
||||||
if (currentPageConfig?.agent && !checkIsAgent()) {
|
if (currentPageConfig?.agent && !checkIsAgent()) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: '/pages/invitationAgentApply'
|
url: "/pages/invitationAgentApply",
|
||||||
})
|
});
|
||||||
|
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
},
|
},
|
||||||
fail() {
|
fail() {
|
||||||
return false
|
return false;
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { setupRouterGuard }
|
export { setupRouterGuard };
|
||||||
|
|||||||
Reference in New Issue
Block a user