This commit is contained in:
2026-05-13 14:43:38 +08:00
parent 92efdae9eb
commit c9102f2d51
18 changed files with 679 additions and 838 deletions

View File

@@ -0,0 +1,12 @@
<template>
<div class="flex flex-col items-center justify-center py-16">
<img src="@/assets/images/empty.svg" alt="空状态" class="w-48 h-48 mb-4" />
<p class="text-gray-400 text-base">{{ text }}</p>
</div>
</template>
<script setup>
defineProps({
text: { type: String, default: '暂无数据' }
})
</script>

View File

@@ -15,11 +15,11 @@
@blur="onBlurPrice" class="!text-3xl" /> @blur="onBlurPrice" class="!text-3xl" />
</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"> {{ pricingResult.promotionRevenue }} </span></div>
</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"> {{ baseCost }} </span></div> <div>底价成本为<span class="text-orange-500"> {{ pricingResult.baseCost }} </span></div>
<div>提价成本为<span class="text-orange-500"> {{ raiseCost }} </span></div> <div>提价成本为<span class="text-orange-500"> {{ pricingResult.raiseCost }} </span></div>
</div> </div>
</div> </div>
<div class="card m-4"> <div class="card m-4">
@@ -42,6 +42,8 @@
</template> </template>
<script setup> <script setup>
import { calculatePromotionPricing, validatePrice } from '@/utils/promotionPricing'
const props = defineProps({ const props = defineProps({
defaultPrice: { defaultPrice: {
type: Number, type: Number,
@@ -57,108 +59,22 @@ const emit = defineEmits(["change"])
const show = defineModel("show") const show = defineModel("show")
const price = ref(null) const price = ref(null)
watch(show, () => { watch(show, () => {
price.value = defaultPrice.value price.value = defaultPrice.value
}) })
const pricingResult = computed(() => {
const costPrice = computed(() => { return calculatePromotionPricing(price.value, productConfig.value)
if (!productConfig.value) return 0.00
// 新代理系统:成本价 = 实际底价actual_base_price+ 提价成本
const actualBasePrice = Number(productConfig.value.actual_base_price) || 0;
const priceNum = Number(price.value) || 0;
const priceThreshold = Number(productConfig.value.price_threshold) || 0;
const priceFeeRate = Number(productConfig.value.price_fee_rate) || 0;
// 计算提价成本
let priceCost = 0;
if (priceNum > priceThreshold) {
priceCost = (priceNum - priceThreshold) * priceFeeRate;
}
// 总成本 = 实际底价 + 提价成本
const totalCost = actualBasePrice + priceCost;
return safeTruncate(totalCost);
}) })
const rateFormat = (rate) => { const rateFormat = (rate) => {
return rate * 100 + '%'; return rate * 100 + '%';
} }
const baseCost = computed(() => {
if (!productConfig.value) return "0.00";
const actualBasePrice = Number(productConfig.value.actual_base_price) || 0;
return safeTruncate(actualBasePrice);
})
const raiseCost = computed(() => {
if (!productConfig.value) return "0.00";
const priceNum = Number(price.value) || 0;
const priceThreshold = Number(productConfig.value.price_threshold) || 0;
const priceFeeRate = Number(productConfig.value.price_fee_rate) || 0;
let priceCost = 0;
if (priceNum > priceThreshold) {
priceCost = (priceNum - priceThreshold) * priceFeeRate;
}
return safeTruncate(priceCost);
})
const promotionRevenue = computed(() => {
return safeTruncate(price.value - costPrice.value)
});
// 价格校验与修正逻辑
const validatePrice = (currentPrice) => {
const min = productConfig.value.price_range_min;
const max = productConfig.value.price_range_max;
let newPrice = Number(currentPrice);
let message = '';
// 处理无效输入
if (isNaN(newPrice)) {
newPrice = defaultPrice.value;
return { newPrice, message: '输入无效,请输入价格' };
}
// 处理小数位数(兼容科学计数法)
try {
const priceString = newPrice.toString()
const [_, decimalPart = ""] = priceString.split('.');
console.log(priceString, decimalPart)
// 当小数位数超过2位时处理
if (decimalPart.length > 2) {
newPrice = parseFloat(safeTruncate(newPrice));
message = '价格已自动格式化为两位小数';
}
} catch (e) {
console.error('价格格式化异常:', e);
}
// 范围校验(基于可能格式化后的值)
if (newPrice < min) {
message = `价格不能低于 ${min}`;
newPrice = min;
} else if (newPrice > max) {
message = `价格不能高于 ${max}`;
newPrice = max;
}
console.log(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
const { newPrice, message } = validatePrice(price.value) const { newPrice, message } = validatePrice(price.value, productConfig.value, defaultPrice.value)
if (message) { if (message) {
price.value = newPrice price.value = newPrice
showToast({ message }); showToast({ message });
@@ -166,11 +82,10 @@ const onConfirm = () => {
emit("change", price.value) emit("change", price.value)
show.value = false show.value = false
} }
} }
const onBlurPrice = () => { const onBlurPrice = () => {
const { newPrice, message } = validatePrice(price.value) const { newPrice, message } = validatePrice(price.value, productConfig.value, defaultPrice.value)
if (message) { if (message) {
isManualConfirm.value = true isManualConfirm.value = true
price.value = newPrice price.value = newPrice

View File

@@ -2,9 +2,10 @@
<router-view /> <router-view />
<van-popup v-model:show="showPopup" round @click-overlay="onClickOverlay"> <van-popup v-model:show="showPopup" round @click-overlay="onClickOverlay">
<div class="popup-content text-center p-8"> <div class="popup-content text-center p-8">
<div v-if="currentNotify?.title" class="text-lg font-bold mb-4">{{ currentNotify.title }}</div>
<div v-html="currentNotify?.content"></div> <div v-html="currentNotify?.content"></div>
<div class="flex justify-center"> <div class="flex justify-center">
<van-button type="primary" @click="showPopup = false" class="w-24">关闭</van-button> <van-button type="primary" @click="onClosePopup" class="w-24">关闭</van-button>
</div> </div>
</div> </div>
</van-popup> </van-popup>
@@ -18,12 +19,16 @@ import { useRoute } from 'vue-router'
const showPopup = ref(false) const showPopup = ref(false)
const notify = ref([]) const notify = ref([])
const currentNotify = ref(null) const currentNotify = ref(null)
const pendingNotifyQueue = ref([])
const shownNotificationKeys = ref(new Set())
const SESSION_KEY = 'qnc_webview_shown_notifications'
// 获取当前页面路径 // 获取当前页面路径
const route = useRoute() const route = useRoute()
// 获取通知数据 // 获取通知数据
onMounted(() => { onMounted(() => {
loadShownNotificationKeys()
getGlobalNotify() getGlobalNotify()
}) })
@@ -33,60 +38,128 @@ const getGlobalNotify = async () => {
.get() .get()
.json() .json()
if (data.value && !error.value) { if (data.value && !error.value && data.value.code === 200 && data.value.data) {
if (data.value !== 200) { notify.value = data.value.data.notifications ?? []
notify.value = data.value.data.notifications checkNotification()
checkNotification() // 在获取数据后检查通知
}
} }
} }
// 判断当前时间是否在通知的时间范围内 /** 与后台「展示时间」一致:未配置或起止相同视为「全天」,任意时刻都算在范围内 */
const isWithinTimeRange = (startTime, endTime) => { const isWithinTimeRange = (startTime, endTime) => {
const s = (startTime || '').trim()
const e = (endTime || '').trim()
if (!s && !e) return true
if (s === e) return true
const now = new Date() const now = new Date()
// 获取当前时间的小时和分钟
const currentMinutes = now.getHours() * 60 + now.getMinutes() const currentMinutes = now.getHours() * 60 + now.getMinutes()
const toMinutes = (t) => {
// 将 startTime 和 endTime 转换为分钟数 const parts = t.split(':').map(Number)
const startParts = startTime.split(':').map(Number) const h = parts[0] ?? 0
const endParts = endTime.split(':').map(Number) const m = parts[1] ?? 0
const startMinutes = startParts[0] * 60 + startParts[1] return h * 60 + m
const endMinutes = endParts[0] * 60 + endParts[1]
// 如果 endTime 小于 startTime表示跨越了午夜
if (endMinutes < startMinutes) {
// 判断当前时间是否在 [startTime, 23:59:59] 或 [00:00:00, endTime] 之间
return currentMinutes >= startMinutes || currentMinutes < endMinutes
} }
const startMinutes = toMinutes(s)
const endMinutes = toMinutes(e)
// 普通情况,直接判断时间是否在范围内 if (endMinutes < startMinutes) {
return currentMinutes >= startMinutes || currentMinutes <= endMinutes
}
return currentMinutes >= startMinutes && currentMinutes <= endMinutes return currentMinutes >= startMinutes && currentMinutes <= endMinutes
} }
/** 当前路由是否与通知配置的页面一致 */
const matchesNotificationPage = (page) => {
const p = (page || '').trim()
const cur = route.path || ''
if (p === cur) return true
if (p === '/' && (cur === '/' || cur === '')) return true
if (cur.startsWith('/app/') && p === cur.replace('/app/', '/')) return true
if (p.startsWith('/app/') && cur === p.replace('/app/', '/')) return true
return false
}
const buildNotificationKey = (notification) => {
return [
notification.title ?? '',
notification.content ?? '',
notification.notificationPage ?? '',
notification.startDate ?? '',
notification.endDate ?? '',
notification.startTime ?? '',
notification.endTime ?? ''
].join('|')
}
const loadShownNotificationKeys = () => {
const raw = sessionStorage.getItem(SESSION_KEY)
if (!raw) return
try {
const parsed = JSON.parse(raw)
if (Array.isArray(parsed)) {
shownNotificationKeys.value = new Set(parsed)
}
} catch {
shownNotificationKeys.value = new Set()
}
}
const saveShownNotificationKeys = () => {
sessionStorage.setItem(SESSION_KEY, JSON.stringify([...shownNotificationKeys.value]))
}
const hasShownNotification = (notification) => {
return shownNotificationKeys.value.has(buildNotificationKey(notification))
}
const markNotificationShown = (notification) => {
shownNotificationKeys.value.add(buildNotificationKey(notification))
saveShownNotificationKeys()
}
const showNextNotification = () => {
const next = pendingNotifyQueue.value.shift()
if (!next) {
currentNotify.value = null
showPopup.value = false
return
}
currentNotify.value = next
showPopup.value = true
markNotificationShown(next)
}
// 检查通知并更新showPopup // 检查通知并更新showPopup
const checkNotification = () => { const checkNotification = () => {
// 遍历通知数组,找到第一个符合条件的通知 if (showPopup.value) return
for (let notification of notify.value) {
// 判断时间是否符合当前时间
const isTimeValid = isWithinTimeRange(notification.startTime, notification.endTime)
// 判断页面是否符合
if (isTimeValid && notification.notificationPage === route.path) { const matchedNotifications = []
currentNotify.value = notification for (let notification of notify.value) {
showPopup.value = true const isTimeValid = isWithinTimeRange(notification.startTime, notification.endTime)
break // 只显示第一个符合的通知 const isPageValid = matchesNotificationPage(notification.notificationPage)
const isShown = hasShownNotification(notification)
if (isTimeValid && isPageValid && !isShown) {
matchedNotifications.push(notification)
} }
} }
pendingNotifyQueue.value = matchedNotifications
showNextNotification()
} }
// 监听路由变化 // 监听路由变化
watch(() => route.path, () => { watch(() => route.path, () => {
checkNotification() // 每次路由变化时重新判断通知 checkNotification()
}) })
// 关闭弹窗 // 关闭弹窗
const onClosePopup = () => {
showNextNotification()
}
const onClickOverlay = () => { const onClickOverlay = () => {
showPopup.value = false onClosePopup()
} }
</script> </script>

View File

@@ -0,0 +1,162 @@
/**
* 推广定价计算工具
* 统一管理价格计算逻辑,供 Promote.vue 和 PriceInputPopup.vue 共享
*/
/**
* 安全截断小数位数(四舍五入)
* 用于前端显示和发送给后端的价格格式化
* @param {number} num - 要格式化的数值
* @param {number} decimals - 保留小数位数默认2位
* @returns {string} 格式化后的字符串,如 "12.34"
*/
export function safeTruncate(num, decimals = 2) {
if (Number.isNaN(num) || !Number.isFinite(num)) {
return '0.00'
}
const factor = 10 ** decimals
const scaled = Math.round(num * factor)
return (scaled / factor).toFixed(decimals)
}
/**
* 将元转为分(整数),避免浮点运算精度问题
* @param {number} num - 元为单位的数值
* @returns {number} 分为单位的整数
*/
function toTruncatedCents(num) {
if (Number.isNaN(num) || !Number.isFinite(num)) {
return 0
}
return Math.round(num * 100)
}
/**
* 将分转回元返回固定2位小数字符串
* @param {number} cents - 分为单位的整数
* @returns {string} 元为单位的字符串,如 "12.34"
*/
function centsToFixed(cents) {
return (cents / 100).toFixed(2)
}
/**
* 计算提价成本
* 当客户查询价超过价格阈值时,超出部分按费率收取
* @param {number} price - 客户查询价
* @param {object} config - 产品配置
* @returns {number} 提价成本(元)
*/
function calculateRaiseCost(price, config) {
const priceThreshold = Number(config.price_threshold) || 0
const priceFeeRate = Number(config.price_fee_rate) || 0
if (priceThreshold <= 0 || priceFeeRate <= 0) {
return 0
}
if (price <= priceThreshold) {
return 0
}
return (price - priceThreshold) * priceFeeRate
}
/**
* 计算推广定价(核心函数)
* 所有金额先转为分(整数)再计算,避免浮点精度问题
*
* @param {number|string} priceInput - 客户查询价
* @param {object} config - 产品配置,包含以下字段:
* - actual_base_price: 实际底价(含等级加成)
* - price_threshold: 价格阈值(超过此价格收取提价费用)
* - price_fee_rate: 提价费率0-1之间的小数
* @returns {{ costPrice: string, baseCost: string, raiseCost: string, promotionRevenue: string }}
* 各项价格均为 "xx.xx" 格式的字符串
*/
export function calculatePromotionPricing(priceInput, config) {
if (!config) {
return {
costPrice: '0.00',
baseCost: '0.00',
raiseCost: '0.00',
promotionRevenue: '0.00',
}
}
const price = Number(priceInput)
if (!Number.isFinite(price)) {
return {
costPrice: '0.00',
baseCost: '0.00',
raiseCost: '0.00',
promotionRevenue: '0.00',
}
}
const baseCost = Number(config.actual_base_price) || 0
const raiseCost = calculateRaiseCost(price, config)
const totalCost = baseCost + raiseCost
// 转为分计算,避免浮点精度问题
const priceCents = toTruncatedCents(price)
const baseCostCents = toTruncatedCents(baseCost)
const raiseCostCents = toTruncatedCents(raiseCost)
const totalCostCents = toTruncatedCents(totalCost)
const revenueCents = Math.max(0, priceCents - totalCostCents)
return {
costPrice: centsToFixed(totalCostCents),
baseCost: centsToFixed(baseCostCents),
raiseCost: centsToFixed(raiseCostCents),
promotionRevenue: centsToFixed(revenueCents),
}
}
/**
* 验证价格输入
* @param {number|string} currentPrice - 当前输入的价格
* @param {object} productConfig - 产品配置
* @param {number} defaultPrice - 默认价格(用于无效输入时的回退)
* @returns {{ newPrice: number, message: string }}
*/
export function validatePrice(currentPrice, productConfig, defaultPrice) {
if (!productConfig) {
return { newPrice: Number(defaultPrice || 0), message: '产品配置未就绪,请稍后再试' }
}
const min = Number(productConfig.price_range_min) || 0
const max = Number(productConfig.price_range_max) || Infinity
let newPrice = Number(currentPrice)
let message = ''
// 处理无效输入
if (Number.isNaN(newPrice)) {
newPrice = Number(defaultPrice || 0)
return { newPrice, message: '输入无效,请输入价格' }
}
// 处理小数位数(兼容科学计数法)
try {
const priceString = newPrice.toString()
const [_, decimalPart = ''] = priceString.split('.')
if (decimalPart.length > 2) {
newPrice = Number.parseFloat(safeTruncate(newPrice))
message = '价格已自动格式化为两位小数'
}
} catch (e) {
console.error('价格格式化异常:', e)
}
// 范围校验
if (newPrice < min) {
message = `价格不能低于 ${min}`
newPrice = min
} else if (newPrice > max) {
message = `价格不能高于 ${max}`
newPrice = max
}
return { newPrice, message }
}

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="min-h-screen bg-gray-50"> <div class="min-h-screen bg-gray-50">
<!-- 收益列表 --> <!-- 收益列表 -->
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> <van-list v-model:loading="loading" :finished="finished" :finished-text="data.list.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div v-for="(item, index) in data.list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm"> <div v-for="(item, index) in data.list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm">
<div class="flex justify-between items-center mb-2"> <div class="flex justify-between items-center mb-2">
<span class="text-gray-500 text-sm">{{ item.create_time || '-' }}</span> <span class="text-gray-500 text-sm">{{ item.create_time || '-' }}</span>
@@ -19,11 +19,15 @@
</div> </div>
</div> </div>
</van-list> </van-list>
<!-- 空状态 -->
<EmptyState v-if="!loading && !data.list.length" text="暂无佣金记录" />
</div> </div>
</template> </template>
<script setup> <script setup>
import { getCommissionList } from '@/api/agent' import { getCommissionList } from '@/api/agent'
import EmptyState from '@/components/EmptyState.vue'
// 颜色配置(根据产品名称映射) // 颜色配置(根据产品名称映射)
const typeColors = { const typeColors = {
@@ -60,16 +64,11 @@ const getDotColor = (name) => {
// 加载更多数据 // 加载更多数据
const onLoad = async () => { const onLoad = async () => {
if (!finished.value) { await getData()
page.value++
await getData()
}
} }
// 获取数据 // 获取数据
const getData = async () => { const getData = async () => {
if (loading.value || finished.value) return
try { try {
loading.value = true loading.value = true
const { data: res, error } = await getCommissionList({ const { data: res, error } = await getCommissionList({
@@ -78,37 +77,33 @@ const getData = async () => {
}) })
if (res.value?.code === 200 && !error.value) { if (res.value?.code === 200 && !error.value) {
// 首次加载 const list = res.value.data.list || []
const total = res.value.data.total || 0
if (page.value === 1) { if (page.value === 1) {
data.value = res.value.data data.value = { total, list }
} else { } else {
// 分页加载 data.value.list.push(...list)
data.value.list.push(...res.value.data.list)
} }
// 判断是否加载完成 // 判断是否加载完成
if (data.value.list.length >= res.value.data.total || if (data.value.list.length >= total ||
res.value.data.list.length < pageSize.value) { list.length < pageSize.value) {
finished.value = true finished.value = true
} else {
page.value++
} }
} else { } else {
// 接口返回错误或请求失败,停止翻页
finished.value = true finished.value = true
console.error('获取佣金列表失败:', res.value?.msg || error.value || '未知错误') console.error('获取佣金列表失败:', res.value?.msg || error.value || '未知错误')
} }
} catch (err) { } catch (err) {
// 捕获异常,停止翻页
finished.value = true finished.value = true
console.error('获取佣金列表失败:', err) console.error('获取佣金列表失败:', err)
} finally { } finally {
loading.value = false loading.value = false
} }
} }
// 初始化加载
onMounted(() => {
getData()
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,5 +1,7 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref } from 'vue'
import EmptyState from '@/components/EmptyState.vue'
const router = useRouter() const router = useRouter()
const page = ref(1) const page = ref(1)
const pageSize = ref(10) const pageSize = ref(10)
@@ -9,7 +11,6 @@ const loading = ref(false)
const finished = ref(false) const finished = ref(false)
async function fetchData() { async function fetchData() {
if (loading.value || finished.value) return
loading.value = true loading.value = true
try { try {
const { data, error } = await useApiFetch(`/agent/promotion/query/list?page=${page.value}&page_size=${pageSize.value}`).get().json() const { data, error } = await useApiFetch(`/agent/promotion/query/list?page=${page.value}&page_size=${pageSize.value}`).get().json()
@@ -17,11 +18,17 @@ async function fetchData() {
if (data.value.code === 200) { if (data.value.code === 200) {
total.value = data.value.data.total total.value = data.value.data.total
if (data.value.data.list && data.value.data.list.length > 0) { if (data.value.data.list && data.value.data.list.length > 0) {
reportList.value.push(...data.value.data.list) if (page.value === 1) {
page.value += 1 reportList.value = data.value.data.list
} else {
reportList.value.push(...data.value.data.list)
}
} }
if (reportList.value.length >= total.value) {
if (reportList.value.length >= total.value || (data.value.data.list || []).length < pageSize.value) {
finished.value = true finished.value = true
} else {
page.value++
} }
} else { } else {
finished.value = true finished.value = true
@@ -39,14 +46,8 @@ async function fetchData() {
} }
} }
onMounted(() => {
fetchData()
})
const onLoad = () => { const onLoad = () => {
if (!finished.value) { fetchData()
fetchData()
}
} }
function toDetail(item) { function toDetail(item) {
@@ -87,7 +88,7 @@ function statusClass(state) {
<template> <template>
<div class="flex flex-col gap-4 p-4"> <div class="flex flex-col gap-4 p-4">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> <van-list v-model:loading="loading" :finished="finished" :finished-text="reportList.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div v-for="item in reportList" :key="item.id" @click="toDetail(item)" <div v-for="item in reportList" :key="item.id" @click="toDetail(item)"
class="bg-white rounded-lg shadow-sm p-4 mb-4 relative cursor-pointer"> class="bg-white rounded-lg shadow-sm p-4 mb-4 relative cursor-pointer">
<div class="flex flex-col"> <div class="flex flex-col">
@@ -100,9 +101,10 @@ function statusClass(state) {
</div> </div>
</div> </div>
</van-list> </van-list>
<!-- 空状态 -->
<EmptyState v-if="!loading && reportList.length === 0" text="暂无推广查询记录" />
</div> </div>
<van-empty v-if="!loading && reportList.length === 0" description="暂无推广查询记录" />
</template> </template>
<style scoped> <style scoped>

View File

@@ -7,7 +7,7 @@
</van-tabs> </van-tabs>
<!-- 收益列表 --> <!-- 收益列表 -->
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> <van-list v-model:loading="loading" :finished="finished" :finished-text="data.list.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div v-for="(item, index) in data.list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm"> <div v-for="(item, index) in data.list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm">
<div class="flex justify-between items-center mb-2"> <div class="flex justify-between items-center mb-2">
<span class="text-gray-500 text-sm">{{ item.create_time || '-' }}</span> <span class="text-gray-500 text-sm">{{ item.create_time || '-' }}</span>
@@ -45,12 +45,16 @@
</div> </div>
</div> </div>
</van-list> </van-list>
<!-- 空状态 -->
<EmptyState v-if="!loading && !data.list.length" text="暂无返佣记录" />
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref } from 'vue'
import { getRebateList, getUpgradeRebateList } from '@/api/agent' import { getRebateList, getUpgradeRebateList } from '@/api/agent'
import EmptyState from '@/components/EmptyState.vue'
// 返佣类型映射配置(推广返佣) // 返佣类型映射配置(推广返佣)
const typeConfig = { const typeConfig = {
@@ -162,16 +166,11 @@ const onTabChange = (name) => {
// 加载更多数据 // 加载更多数据
const onLoad = async () => { const onLoad = async () => {
if (!finished.value) { await getData()
page.value++
await getData()
}
} }
// 获取数据 // 获取数据
const getData = async () => { const getData = async () => {
if (loading.value || finished.value) return
try { try {
loading.value = true loading.value = true
@@ -227,6 +226,8 @@ const getData = async () => {
// 判断是否加载完成 // 判断是否加载完成
if (data.value.list.length >= total || list.length < pageSize.value) { if (data.value.list.length >= total || list.length < pageSize.value) {
finished.value = true finished.value = true
} else {
page.value++
} }
} else { } else {
// 接口返回错误或请求失败,停止翻页 // 接口返回错误或请求失败,停止翻页
@@ -241,11 +242,6 @@ const getData = async () => {
loading.value = false loading.value = false
} }
} }
// 初始化加载
onMounted(() => {
getData()
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue' import { ref, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/userStore' import { useUserStore } from '@/stores/userStore'
@@ -20,11 +20,9 @@ const max = ref(60)
const loading = ref(false) const loading = ref(false)
const finished = ref(false) const finished = ref(false)
const showBindNotice = computed(() => isLoggedIn.value && !mobile.value && reportList.value.length > 0) const showBindNotice = computed(() => isLoggedIn.value && !mobile.value && reportList.value.length > 0)
const hasNoRecords = computed(() => reportList.value.length === 0)
// 初始加载数据
async function fetchData() {
if (loading.value || finished.value) return
// 加载数据
async function fetchData() {
loading.value = true loading.value = true
try { try {
const { data, error } = await useApiFetch(`query/list?page=${page.value}&page_size=${pageSize.value}`) const { data, error } = await useApiFetch(`query/list?page=${page.value}&page_size=${pageSize.value}`)
@@ -34,24 +32,27 @@ async function fetchData() {
if (data.value.code === 200) { if (data.value.code === 200) {
total.value = data.value.data.total total.value = data.value.data.total
if (data.value.data.list && data.value.data.list.length > 0) { if (data.value.data.list && data.value.data.list.length > 0) {
reportList.value.push(...data.value.data.list) if (page.value === 1) {
page.value += 1 reportList.value = data.value.data.list
} else {
reportList.value.push(...data.value.data.list)
}
} }
if (reportList.value.length >= total.value) {
if (reportList.value.length >= total.value || (data.value.data.list || []).length < pageSize.value) {
finished.value = true finished.value = true
} else {
page.value++
} }
} else { } else {
// 接口返回错误,停止翻页
finished.value = true finished.value = true
console.error('获取查询列表失败:', data.value.msg || '未知错误') console.error('获取查询列表失败:', data.value.msg || '未知错误')
} }
} else { } else {
// 请求失败或返回错误,停止翻页
finished.value = true finished.value = true
console.error('获取查询列表失败:', error.value || '请求失败') console.error('获取查询列表失败:', error.value || '请求失败')
} }
} catch (err) { } catch (err) {
// 捕获异常,停止翻页
finished.value = true finished.value = true
console.error('获取查询列表失败:', err) console.error('获取查询列表失败:', err)
} finally { } finally {
@@ -59,21 +60,9 @@ async function fetchData() {
} }
} }
// 初始加载 // 触底加载更多
onMounted(async () => {
fetchData()
})
// 下拉触底加载更多
const onLoad = () => { const onLoad = () => {
if (!finished.value) { fetchData()
console.log("finished", finished.value)
if (num.value >= max.value) {
finished.value = true
} else {
fetchData()
}
}
} }
function toDetail(item) { function toDetail(item) {
@@ -91,7 +80,6 @@ function handleBindSuccess() {
page.value = 1 page.value = 1
finished.value = false finished.value = false
loading.value = false loading.value = false
fetchData()
} }
// 状态文字映射 // 状态文字映射
@@ -137,13 +125,7 @@ function statusClass(state) {
@click="dialogStore.openBindPhone">绑定手机号</button> @click="dialogStore.openBindPhone">绑定手机号</button>
</div> </div>
<div class="text-xs text-gray-500">为保障用户隐私及数据安全报告保留{{ appStore.queryRetentionDays || 30 }}过期自动清理</div> <div class="text-xs text-gray-500">为保障用户隐私及数据安全报告保留{{ appStore.queryRetentionDays || 30 }}过期自动清理</div>
<div v-if="hasNoRecords" <van-list v-model:loading="loading" :finished="finished" :finished-text="reportList.length > 0 ? '没有更多了' : ''" @load="onLoad">
class="bg-white rounded-lg shadow-sm p-6 flex flex-col items-center justify-center gap-3">
<div class="text-gray-600 text-sm">暂无历史报告</div>
<button v-if="!isLoggedIn || !mobile" class="px-4 py-2 bg-blue-500 text-white rounded"
@click="toLogin">登录</button>
</div>
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div v-for="item in reportList" :key="item.id" @click="toDetail(item)" <div v-for="item in reportList" :key="item.id" @click="toDetail(item)"
class="bg-white rounded-lg shadow-sm p-4 mb-4 relative cursor-pointer"> class="bg-white rounded-lg shadow-sm p-4 mb-4 relative cursor-pointer">
<div class="flex flex-col"> <div class="flex flex-col">
@@ -157,6 +139,14 @@ function statusClass(state) {
</div> </div>
</van-list> </van-list>
<!-- 空状态 -->
<div v-if="!loading && reportList.length === 0"
class="bg-white rounded-lg shadow-sm p-6 flex flex-col items-center justify-center gap-3">
<div class="text-gray-600 text-sm">暂无历史报告</div>
<button v-if="!isLoggedIn || !mobile" class="px-4 py-2 bg-blue-500 text-white rounded"
@click="toLogin">登录</button>
</div>
</div> </div>
</template> </template>

View File

@@ -202,7 +202,7 @@ function toPrivacyPolicy() {
<!-- 注册按钮推广链接来源的登录不展示 --> <!-- 注册按钮推广链接来源的登录不展示 -->
<button v-if="!hideRegister" class="register-btn" @click="goToRegister"> <button v-if="!hideRegister" class="register-btn" @click="goToRegister">
注册成为代理 成为代理
</button> </button>
</div> </div>
</div> </div>

View File

@@ -203,8 +203,8 @@
<img src="@/assets/images/me/yqmgl.svg" class="w-8 h-8 object-contain" alt="邀请码管理" /> <img src="@/assets/images/me/yqmgl.svg" class="w-8 h-8 object-contain" alt="邀请码管理" />
<span class="text-xs text-gray-700 font-medium text-center">邀请码管理</span> <span class="text-xs text-gray-700 font-medium text-center">邀请码管理</span>
</button> </button>
<!-- 实名认证入口所有代理 --> <!-- 实名认证入口所有代理未实名时显示 -->
<button v-if="isAgent" <button v-if="isAgent && !agentStore.isRealName"
class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-green-50 transition-colors" class="flex flex-col items-center justify-center gap-2 py-4 rounded-lg hover:bg-green-50 transition-colors"
@click="toRealNameAuth"> @click="toRealNameAuth">
<img src="@/assets/images/me/smrz.svg" class="w-8 h-8 object-contain" alt="提现" /> <img src="@/assets/images/me/smrz.svg" class="w-8 h-8 object-contain" alt="提现" />
@@ -259,6 +259,7 @@
</div> </div>
</div> </div>
<BindPhoneDialog /> <BindPhoneDialog />
<RealNameAuthDialog />
</div> </div>
</template> </template>
@@ -274,6 +275,7 @@ import { useDialogStore } from "@/stores/dialogStore";
import useApiFetch from "@/composables/useApiFetch"; import useApiFetch from "@/composables/useApiFetch";
import { getRevenueInfo } from '@/api/agent'; import { getRevenueInfo } from '@/api/agent';
import BindPhoneDialog from "@/components/BindPhoneDialog.vue"; import BindPhoneDialog from "@/components/BindPhoneDialog.vue";
import RealNameAuthDialog from "@/components/RealNameAuthDialog.vue";
const router = useRouter(); const router = useRouter();
const agentStore = useAgentStore(); const agentStore = useAgentStore();

View File

@@ -93,6 +93,7 @@ import PriceInputPopup from '@/components/PriceInputPopup.vue';
import QRcode from '@/components/QRcode.vue'; import QRcode from '@/components/QRcode.vue';
import ReportFeatures from '@/components/ReportFeatures.vue'; import ReportFeatures from '@/components/ReportFeatures.vue';
import { getProductConfig, generateLink } from '@/api/agent'; import { getProductConfig, generateLink } from '@/api/agent';
import { calculatePromotionPricing, safeTruncate } from '@/utils/promotionPricing';
// 导入logo图片 // 导入logo图片
import personalDataLogo from '@/assets/images/promote/personal_data_logo.png'; import personalDataLogo from '@/assets/images/promote/personal_data_logo.png';
@@ -140,62 +141,14 @@ const currentLogo = computed(() => {
return logoMap[currentFeature.value] || null; return logoMap[currentFeature.value] || null;
}); });
const costPrice = computed(() => { const pricingResult = computed(() => {
if (!pickerProductConfig.value) return 0.00 return calculatePromotionPricing(clientPrice.value, pickerProductConfig.value)
// 新系统:成本价 = 实际底价actual_base_price
// actual_base_price = base_price + 等级加成
const actualBasePrice = Number(pickerProductConfig.value.actual_base_price) || 0;
const clientPriceNum = Number(clientPrice.value) || 0;
const priceThreshold = Number(pickerProductConfig.value.price_threshold) || 0;
const priceFeeRate = Number(pickerProductConfig.value.price_fee_rate) || 0;
// 计算提价成本
let priceCost = 0;
if (clientPriceNum > priceThreshold) {
priceCost = (clientPriceNum - priceThreshold) * priceFeeRate;
}
// 总成本 = 实际底价 + 提价成本
const totalCost = actualBasePrice + priceCost;
return safeTruncate(totalCost);
}); });
const baseCost = computed(() => { const costPrice = computed(() => pricingResult.value.costPrice);
if (!pickerProductConfig.value) return "0.00"; const baseCost = computed(() => pricingResult.value.baseCost);
const actualBasePrice = Number(pickerProductConfig.value.actual_base_price) || 0; const raiseCost = computed(() => pricingResult.value.raiseCost);
return safeTruncate(actualBasePrice); const promotionRevenue = computed(() => pricingResult.value.promotionRevenue);
});
const raiseCost = computed(() => {
if (!pickerProductConfig.value) return "0.00";
const clientPriceNum = Number(clientPrice.value) || 0;
const priceThreshold = Number(pickerProductConfig.value.price_threshold) || 0;
const priceFeeRate = Number(pickerProductConfig.value.price_fee_rate) || 0;
let priceCost = 0;
if (clientPriceNum > priceThreshold) {
priceCost = (clientPriceNum - priceThreshold) * priceFeeRate;
}
return safeTruncate(priceCost);
});
const promotionRevenue = computed(() => {
const clientPriceNum = Number(clientPrice.value) || 0;
const costPriceNum = parseFloat(costPrice.value) || 0; // costPrice 返回字符串,需要转换为数字
const revenue = clientPriceNum - costPriceNum;
return safeTruncate(revenue >= 0 ? revenue : 0); // 确保收益不为负数
});
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 getProductInfo = async () => { const getProductInfo = async () => {
if (!currentFeature.value) { if (!currentFeature.value) {
@@ -336,7 +289,7 @@ const generatePromotionCode = async () => {
// 新系统API使用 product_id、set_price 和 target_path // 新系统API使用 product_id、set_price 和 target_path
const { data, error } = await generateLink({ const { data, error } = await generateLink({
product_id: pickerProductConfig.value.product_id, product_id: pickerProductConfig.value.product_id,
set_price: priceNum, set_price: safeTruncate(priceNum),
target_path: targetPath target_path: targetPath
}); });

View File

@@ -1,7 +1,8 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import useApiFetch from '@/composables/useApiFetch' import useApiFetch from '@/composables/useApiFetch'
import EmptyState from '@/components/EmptyState.vue'
const route = useRoute() const route = useRoute()
const loading = ref(false) const loading = ref(false)
@@ -23,9 +24,6 @@ const inviteListTotal = ref(0)
// 获取详情数据 // 获取详情数据
const fetchDetail = async () => { const fetchDetail = async () => {
if (loading.value) return
if (finished.value && page.value > 1) return
loading.value = true loading.value = true
const tabType = activeTab.value const tabType = activeTab.value
const { data, error } = await useApiFetch( const { data, error } = await useApiFetch(
@@ -151,10 +149,6 @@ const formatNumber = num => {
if (!num) return '0.00' if (!num) return '0.00'
return Number(num).toFixed(2) return Number(num).toFixed(2)
} }
onMounted(() => {
fetchDetail()
})
</script> </script>
<template> <template>
@@ -236,10 +230,9 @@ onMounted(() => {
<van-tabs v-model:active="activeTab" @change="switchTab"> <van-tabs v-model:active="activeTab" @change="switchTab">
<van-tab title="订单列表" name="order"> <van-tab title="订单列表" name="order">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh"> <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="fetchDetail"> <van-list v-model:loading="loading" :finished="finished" :finished-text="orderList.length > 0 ? '没有更多了' : ''" @load="fetchDetail">
<div class="p-2"> <div class="p-2">
<div v-if="orderList.length === 0" class="text-center text-gray-500 py-8">暂无订单记录</div> <div v-for="item in orderList" :key="item.order_no"
<div v-else v-for="item in orderList" :key="item.order_no"
class="order-item mb-3 border-b border-gray-200 pb-3"> class="order-item mb-3 border-b border-gray-200 pb-3">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex-1"> <div class="flex-1">
@@ -260,13 +253,13 @@ onMounted(() => {
</div> </div>
</van-list> </van-list>
</van-pull-refresh> </van-pull-refresh>
<EmptyState v-if="!loading && orderList.length === 0" text="暂无订单记录" />
</van-tab> </van-tab>
<van-tab title="邀请列表" name="invite"> <van-tab title="邀请列表" name="invite">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh"> <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="fetchDetail"> <van-list v-model:loading="loading" :finished="finished" :finished-text="inviteList.length > 0 ? '没有更多了' : ''" @load="fetchDetail">
<div class="p-2"> <div class="p-2">
<div v-if="inviteList.length === 0" class="text-center text-gray-500 py-8">暂无邀请记录</div> <div v-for="item in inviteList" :key="item.agent_id"
<div v-else v-for="item in inviteList" :key="item.agent_id"
class="invite-item mb-3 border-b border-gray-200 pb-3"> class="invite-item mb-3 border-b border-gray-200 pb-3">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center space-x-3 flex-1"> <div class="flex items-center space-x-3 flex-1">
@@ -281,6 +274,7 @@ onMounted(() => {
</div> </div>
</van-list> </van-list>
</van-pull-refresh> </van-pull-refresh>
<EmptyState v-if="!loading && inviteList.length === 0" text="暂无邀请记录" />
</van-tab> </van-tab>
</van-tabs> </van-tabs>
</div> </div>

View File

@@ -1,8 +1,9 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref } from 'vue'
import { useAgentStore } from '@/stores/agentStore' import { useAgentStore } from '@/stores/agentStore'
import { getSubordinateList } from '@/api/agent' import { getSubordinateList } from '@/api/agent'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import EmptyState from '@/components/EmptyState.vue'
const agentStore = useAgentStore() const agentStore = useAgentStore()
const subordinates = ref([]) const subordinates = ref([])
@@ -15,16 +16,9 @@ const router = useRouter()
// 加载更多数据 // 加载更多数据
const onLoad = async () => { const onLoad = async () => {
if (!finished.value) { await fetchSubordinates()
await fetchSubordinates()
}
} }
onBeforeMount(() => {
// 初始化时重置状态
page.value = 1
finished.value = false
})
// 计算统计数据 // 计算统计数据
const statistics = ref({ const statistics = ref({
totalSubordinates: 0, totalSubordinates: 0,
@@ -32,8 +26,6 @@ const statistics = ref({
// 获取下级列表 // 获取下级列表
const fetchSubordinates = async () => { const fetchSubordinates = async () => {
if (loading.value || finished.value) return
loading.value = true loading.value = true
try { try {
const { data, error } = await getSubordinateList({ const { data, error } = await getSubordinateList({
@@ -131,10 +123,6 @@ const viewDetail = item => {
params: { id: item.agent_id || item.id }, params: { id: item.agent_id || item.id },
}) })
} }
onMounted(() => {
fetchSubordinates()
})
</script> </script>
<template> <template>
@@ -152,7 +140,7 @@ onMounted(() => {
</div> </div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh"> <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> <van-list v-model:loading="loading" :finished="finished" :finished-text="subordinates.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div class="p-4"> <div class="p-4">
<div v-for="(item, index) in subordinates" :key="item.id" class="subordinate-item"> <div v-for="(item, index) in subordinates" :key="item.id" class="subordinate-item">
<div class="flex flex-col p-5 bg-white rounded-xl shadow-sm mb-4"> <div class="flex flex-col p-5 bg-white rounded-xl shadow-sm mb-4">
@@ -199,6 +187,9 @@ onMounted(() => {
</div> </div>
</van-list> </van-list>
</van-pull-refresh> </van-pull-refresh>
<!-- 空状态 -->
<EmptyState v-if="!loading && !subordinates.length" text="暂无下级代理" />
</div> </div>
</template> </template>

View File

@@ -1,8 +1,9 @@
<script setup> <script setup>
import { ref, onMounted, onBeforeMount } from 'vue' import { ref, onMounted } from 'vue'
import { useAgentStore } from '@/stores/agentStore' import { useAgentStore } from '@/stores/agentStore'
import { getTeamList } from '@/api/agent' import { getTeamList } from '@/api/agent'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import EmptyState from '@/components/EmptyState.vue'
const agentStore = useAgentStore() const agentStore = useAgentStore()
const teamMembers = ref([]) const teamMembers = ref([])
@@ -16,22 +17,12 @@ const searchMobile = ref('')
// 加载更多数据 // 加载更多数据
const onLoad = async () => { const onLoad = async () => {
if (!finished.value) { await fetchTeamMembers()
await fetchTeamMembers()
}
} }
onBeforeMount(() => {
// 初始化时重置状态
page.value = 1
finished.value = false
})
// 获取团队列表 // 获取团队列表
const fetchTeamMembers = async () => { const fetchTeamMembers = async () => {
if (loading.value || finished.value) return
loading.value = true loading.value = true
try { try {
const params = { const params = {
@@ -155,10 +146,6 @@ const viewDetail = item => {
params: { id: item.agent_id || item.id }, params: { id: item.agent_id || item.id },
}) })
} }
onMounted(() => {
fetchTeamMembers()
})
</script> </script>
<template> <template>
@@ -176,7 +163,7 @@ onMounted(() => {
</div> </div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh"> <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> <van-list v-model:loading="loading" :finished="finished" :finished-text="teamMembers.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div class="p-4"> <div class="p-4">
<div v-for="(item, index) in teamMembers" :key="item.agent_id || item.id" class="team-member-item"> <div v-for="(item, index) in teamMembers" :key="item.agent_id || item.id" class="team-member-item">
<div class="member-card"> <div class="member-card">
@@ -236,6 +223,9 @@ onMounted(() => {
</div> </div>
</van-list> </van-list>
</van-pull-refresh> </van-pull-refresh>
<!-- 空状态 -->
<EmptyState v-if="!loading && !teamMembers.length" text="暂无团队成员" />
</div> </div>
</template> </template>

View File

@@ -1,118 +1,96 @@
<template> <template>
<div class="upgrade-subordinate-page"> <div class="px-4 pt-3 pb-6 min-h-screen" style="background-color: var(--van-background);">
<!-- 页面头部 --> <!-- 页面头部 -->
<div class="page-header"> <div class="rounded-2xl px-5 pt-5 pb-4 mb-3" style="background: var(--van-theme-primary);">
<div class="header-content"> <h1 class="text-[17px] font-semibold text-white leading-snug">调整下级级别</h1>
<h1 class="page-title">调整下级级别</h1> <p class="text-[13px] text-white/70 mt-1 leading-normal">将团队中的普通代理升级为黄金代理</p>
<p class="page-desc">钻石代理可以将团队中的普通代理升级为黄金代理</p>
</div>
</div>
<!-- 说明卡片 -->
<div class="info-card">
<div class="info-content">
<van-icon name="info-o" class="info-icon" />
<div class="info-text">
<p class="info-title">调整说明</p>
<ul class="info-list">
<li>仅可升级普通代理为黄金代理</li>
<li>调整操作免费无需支付费用</li>
</ul>
</div>
</div>
</div> </div>
<!-- 搜索框 --> <!-- 搜索框 -->
<div class="search-section"> <div class="mb-3">
<van-field v-model="searchMobile" placeholder="输入手机号搜索" clearable @clear="handleClear" <van-search v-model="searchMobile" placeholder="输入手机号搜索" shape="round" show-action
@keyup.enter="handleSearch"> @search="handleSearch" @cancel="handleClear" @clear="handleClear" />
<template #button> </div>
<van-button size="small" type="primary" @click="handleSearch" class="search-btn">搜索</van-button>
</template> <!-- 提示 -->
</van-field> <div class="flex items-center gap-1.5 px-1 mb-4">
<van-icon name="info-o" class="text-[13px]" style="color: var(--van-text-color-2);" />
<p class="text-[12px] leading-normal" style="color: var(--van-text-color-2);">仅可升级普通代理为黄金代理操作免费且不可撤销</p>
</div> </div>
<!-- 代理列表 --> <!-- 代理列表 -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh"> <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> <van-list v-model:loading="loading" :finished="finished" :finished-text="teamMembers.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div class="list-container"> <div v-for="item in teamMembers" :key="item.agent_id || item.id"
<div v-if="teamMembers.length === 0 && !loading" class="empty-state"> class="bg-white rounded-2xl mb-2.5 overflow-hidden">
<van-icon name="user-o" class="empty-icon" /> <!-- 顶部:用户信息 -->
<p class="empty-text">暂无可调整的普通代理</p> <div class="flex items-center justify-between px-4 pt-4 pb-2.5">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="text-[15px] font-medium truncate"
style="color: var(--van-text-color);">{{ item.mobile || '未绑定手机' }}</span>
<span class="inline-flex items-center px-1.5 py-[1px] rounded text-[11px] font-medium"
style="background: rgba(var(--van-theme-primary-rgb, 64,132,240), 0.08); color: var(--van-theme-primary);">普通代理</span>
</div>
<p class="text-[12px] mt-1" style="color: var(--van-text-color-2);">加入于 {{ formatTime(item.create_time) }}</p>
</div>
</div> </div>
<div v-for="(item, index) in teamMembers" :key="item.agent_id || item.id" class="member-card"> <!-- 中间:数据统计 -->
<div class="member-header"> <div class="grid grid-cols-3 mx-4 py-2.5"
<div class="member-index">{{ index + 1 }}</div> style="border-top: 1px solid var(--van-border-color);">
<div class="member-info"> <div class="text-center">
<div class="member-mobile">{{ item.mobile || '未绑定手机' }}</div> <div class="text-[11px] mb-1" style="color: var(--van-text-color-2);">查询量</div>
<div class="member-time">加入时间{{ formatTime(item.create_time) }}</div> <div class="text-[14px] font-medium" style="color: var(--van-text-color);">{{ formatCount(item.total_queries || 0) }}</div>
</div>
<div class="member-badge">
<span class="level-badge level-normal">普通代理</span>
</div>
</div> </div>
<div class="text-center" style="border-left: 1px solid var(--van-border-color);">
<div class="text-[11px] mb-1" style="color: var(--van-text-color-2);">返佣</div>
<div class="text-[14px] font-medium" style="color: var(--van-text-color);">¥{{ formatNumber(item.total_rebate_amount || 0) }}</div>
</div>
<div class="text-center" style="border-left: 1px solid var(--van-border-color);">
<div class="text-[11px] mb-1" style="color: var(--van-text-color-2);">邀请</div>
<div class="text-[14px] font-medium" style="color: var(--van-text-color);">{{ formatCount(item.total_invites || 0) }}</div>
</div>
</div>
<div class="member-stats"> <!-- 底部:操作 -->
<div class="stat-item"> <div class="flex justify-end px-4 pt-1 pb-3">
<div class="stat-label">总查询量</div> <van-button size="small" round :loading="item.upgrading" @click="handleUpgrade(item)"
<div class="stat-value">{{ formatCount(item.total_queries || 0) }}</div> style="background: var(--van-theme-primary); color: white; border: none; padding: 0 14px; font-size: 13px; height: 30px;">
</div> 升级为黄金代理
<div class="stat-item"> </van-button>
<div class="stat-label">返佣总额</div>
<div class="stat-value">¥{{ formatNumber(item.total_rebate_amount || 0) }}</div>
</div>
<div class="stat-item">
<div class="stat-label">邀请人数</div>
<div class="stat-value">{{ formatCount(item.total_invites || 0) }}</div>
</div>
</div>
<div class="member-action">
<van-button type="primary" size="small" :loading="item.upgrading"
@click="handleUpgrade(item)">
<van-icon name="arrow-up" class="mr-1" />
调整为黄金代理
</van-button>
</div>
</div> </div>
</div> </div>
</van-list> </van-list>
</van-pull-refresh> </van-pull-refresh>
<!-- 确认升级弹窗 --> <!-- 空状态 -->
<van-popup v-model:show="showConfirmDialog" round position="center" <EmptyState v-if="!loading && !teamMembers.length" text="暂无可调整的普通代理" />
:style="{ width: '85%', maxWidth: '400px' }">
<div class="confirm-dialog"> <!-- 确认弹窗 -->
<div class="confirm-header"> <van-dialog v-model:show="showConfirmDialog" title="确认升级" show-cancel-button
<van-icon name="warning-o" class="warning-icon" /> :before-close="onBeforeClose" confirm-button-color="var(--van-theme-primary)">
<h3 class="confirm-title">确认升级</h3> <div class="px-6 pb-4 pt-4">
</div> <p class="text-[14px] leading-relaxed mb-3" style="color: var(--van-text-color);">
<div class="confirm-content"> 确定将 <span class="font-semibold" style="color: var(--van-theme-primary);">{{ currentUpgradeItem?.mobile }}</span> 升级为黄金代理?
<p class="confirm-text"> </p>
确定要将 <span class="highlight">{{ currentUpgradeItem?.mobile }}</span> 升级为黄金代理吗 <div class="text-[12px] leading-loose" style="color: var(--van-text-color-2);">
</p> <p>· 操作免费</p>
<div class="confirm-notice"> <p>· 升级后不可撤销</p>
<p> 升级操作不可撤销</p>
<p> 升级操作免费</p>
</div>
</div>
<div class="confirm-actions">
<van-button plain @click="showConfirmDialog = false">取消</van-button>
<van-button type="primary" :loading="isUpgrading" @click="confirmUpgrade">确认升级</van-button>
</div> </div>
</div> </div>
</van-popup> </van-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onBeforeMount, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { useAgentStore } from '@/stores/agentStore' import { useAgentStore } from '@/stores/agentStore'
import { getTeamList, upgradeSubordinate } from '@/api/agent' import { getTeamList, upgradeSubordinate } from '@/api/agent'
import { showToast, showSuccessToast, showFailToast } from 'vant' import { showToast, showSuccessToast, showFailToast } from 'vant'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import EmptyState from '@/components/EmptyState.vue'
const router = useRouter() const router = useRouter()
const agentStore = useAgentStore() const agentStore = useAgentStore()
@@ -128,28 +106,16 @@ const showConfirmDialog = ref(false)
const currentUpgradeItem = ref(null) const currentUpgradeItem = ref(null)
const isUpgrading = ref(false) const isUpgrading = ref(false)
// 检查是否是钻石代理
const isDiamondAgent = computed(() => { const isDiamondAgent = computed(() => {
return isAgent.value && level.value === 3 return isAgent.value && level.value === 3
}) })
// 加载更多数据
const onLoad = async () => { const onLoad = async () => {
if (!finished.value) { await fetchTeamMembers()
await fetchTeamMembers()
}
} }
onBeforeMount(() => {
page.value = 1
finished.value = false
})
// 获取团队列表(仅普通代理)
const fetchTeamMembers = async () => { const fetchTeamMembers = async () => {
if (loading.value || finished.value) return loading.value = true
loading.value = true
try { try {
const params = { const params = {
page: page.value, page: page.value,
@@ -164,8 +130,6 @@ const fetchTeamMembers = async () => {
if (data.value && !error.value) { if (data.value && !error.value) {
if (data.value.code === 200) { if (data.value.code === 200) {
let list = data.value.data.list || [] let list = data.value.data.list || []
// 过滤出普通代理level === 1
list = list.filter(item => { list = list.filter(item => {
const agentLevel = item.level || (item.level_name === '普通' ? 1 : item.level_name === '黄金' ? 2 : item.level_name === '钻石' ? 3 : 1) const agentLevel = item.level || (item.level_name === '普通' ? 1 : item.level_name === '黄金' ? 2 : item.level_name === '钻石' ? 3 : 1)
return agentLevel === 1 return agentLevel === 1
@@ -177,7 +141,6 @@ const fetchTeamMembers = async () => {
teamMembers.value.push(...list) teamMembers.value.push(...list)
} }
// 判断是否加载完成
if (list.length < pageSize) { if (list.length < pageSize) {
finished.value = true finished.value = true
} else if (teamMembers.value.length >= data.value.data.total) { } else if (teamMembers.value.length >= data.value.data.total) {
@@ -186,27 +149,21 @@ const fetchTeamMembers = async () => {
page.value++ page.value++
} }
} else { } else {
// 接口返回错误,停止翻页
finished.value = true finished.value = true
showFailToast(data.value.msg || '获取团队列表失败') showFailToast(data.value.msg || '获取团队列表失败')
} }
} else { } else {
// 请求失败或返回错误,停止翻页
finished.value = true finished.value = true
showFailToast('获取团队列表失败') showFailToast('获取团队列表失败')
console.error('获取团队列表失败:', error.value || '请求失败')
} }
} catch (err) { } catch (err) {
// 捕获异常,停止翻页
finished.value = true finished.value = true
console.error('获取团队列表失败:', err)
showFailToast('获取团队列表失败') showFailToast('获取团队列表失败')
} finally { } finally {
loading.value = false loading.value = false
} }
} }
// 下拉刷新
const onRefresh = () => { const onRefresh = () => {
finished.value = false finished.value = false
page.value = 1 page.value = 1
@@ -215,14 +172,12 @@ const onRefresh = () => {
}) })
} }
// 搜索功能
const handleSearch = () => { const handleSearch = () => {
finished.value = false finished.value = false
page.value = 1 page.value = 1
fetchTeamMembers() fetchTeamMembers()
} }
// 清空搜索
const handleClear = () => { const handleClear = () => {
searchMobile.value = '' searchMobile.value = ''
finished.value = false finished.value = false
@@ -230,13 +185,19 @@ const handleClear = () => {
fetchTeamMembers() fetchTeamMembers()
} }
// 处理升级
const handleUpgrade = (item) => { const handleUpgrade = (item) => {
currentUpgradeItem.value = item currentUpgradeItem.value = item
showConfirmDialog.value = true showConfirmDialog.value = true
} }
// 确认升级 const onBeforeClose = async (action) => {
if (action === 'confirm') {
await confirmUpgrade()
return false
}
return true
}
const confirmUpgrade = async () => { const confirmUpgrade = async () => {
if (!currentUpgradeItem.value) return if (!currentUpgradeItem.value) return
@@ -244,7 +205,7 @@ const confirmUpgrade = async () => {
try { try {
const { data, error } = await upgradeSubordinate({ const { data, error } = await upgradeSubordinate({
subordinate_id: currentUpgradeItem.value.agent_id || currentUpgradeItem.value.id, subordinate_id: currentUpgradeItem.value.agent_id || currentUpgradeItem.value.id,
to_level: 2 // 只能升级为黄金代理 to_level: 2
}) })
if (data.value && !error.value) { if (data.value && !error.value) {
@@ -252,13 +213,9 @@ const confirmUpgrade = async () => {
showSuccessToast('升级成功') showSuccessToast('升级成功')
showConfirmDialog.value = false showConfirmDialog.value = false
currentUpgradeItem.value = null currentUpgradeItem.value = null
// 刷新列表
finished.value = false finished.value = false
page.value = 1 page.value = 1
await fetchTeamMembers() await fetchTeamMembers()
// 刷新代理状态
await agentStore.fetchAgentStatus() await agentStore.fetchAgentStatus()
} else { } else {
showFailToast(data.value.msg || '升级失败') showFailToast(data.value.msg || '升级失败')
@@ -267,307 +224,31 @@ const confirmUpgrade = async () => {
showFailToast('升级失败,请重试') showFailToast('升级失败,请重试')
} }
} catch (err) { } catch (err) {
console.error('升级失败:', err)
showFailToast('升级失败,请重试') showFailToast('升级失败,请重试')
} finally { } finally {
isUpgrading.value = false isUpgrading.value = false
} }
} }
// 格式化时间
const formatTime = (timeStr) => { const formatTime = (timeStr) => {
if (!timeStr) return '-' if (!timeStr) return '-'
return timeStr.split(' ')[0] return timeStr.split(' ')[0]
} }
// 格式化金额
const formatNumber = (num) => { const formatNumber = (num) => {
if (!num) return '0.00' if (!num) return '0.00'
return Number(num).toFixed(2) return Number(num).toFixed(2)
} }
// 格式化数字
const formatCount = (num) => { const formatCount = (num) => {
if (!num) return '0' if (!num) return '0'
return Number(num).toLocaleString() return Number(num).toLocaleString()
} }
onMounted(() => { onMounted(() => {
// 检查权限
if (!isDiamondAgent.value) { if (!isDiamondAgent.value) {
showFailToast('只有钻石代理可以升级下级') showFailToast('只有钻石代理可以升级下级')
router.back() router.back()
return
} }
fetchTeamMembers()
}) })
</script> </script>
<style scoped>
.upgrade-subordinate-page {
min-height: 100vh;
background-color: #f5f7fa;
padding-bottom: 20px;
}
/* 页面头部 */
.page-header {
background: linear-gradient(120deg, #34c9ad 70%, #64d2ff 100%);
padding: 20px 16px;
color: white;
}
.header-content {
max-width: 100%;
}
.page-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
}
.page-desc {
font-size: 14px;
opacity: 0.9;
margin: 0;
}
/* 说明卡片 */
.info-card {
margin: 16px;
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.info-content {
display: flex;
gap: 12px;
}
.info-icon {
font-size: 24px;
color: #1677ff;
flex-shrink: 0;
margin-top: 2px;
}
.info-text {
flex: 1;
}
.info-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin-bottom: 8px;
}
.info-list {
margin: 0;
padding-left: 20px;
color: #6b7280;
font-size: 14px;
line-height: 1.8;
}
.info-list li {
margin-bottom: 4px;
}
/* 搜索区域 */
.search-section {
margin: 0 16px 16px;
}
.search-btn {
margin-left: 8px;
}
/* 列表容器 */
.list-container {
padding: 0 16px;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #9ca3af;
}
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-text {
font-size: 16px;
margin: 0;
}
/* 成员卡片 */
.member-card {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.member-header {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
}
.member-index {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: #e5e7eb;
color: #4b5563;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
}
.member-info {
flex: 1;
min-width: 0;
}
.member-mobile {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin-bottom: 4px;
}
.member-time {
font-size: 12px;
color: #6b7280;
}
.member-badge {
flex-shrink: 0;
}
.level-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.level-normal {
background: #e5e7eb;
color: #4b5563;
}
.member-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 16px;
padding: 12px;
background: #f9fafb;
border-radius: 8px;
}
.stat-item {
text-align: center;
}
.stat-label {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.stat-value {
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
.member-action {
display: flex;
justify-content: flex-end;
}
.mr-1 {
margin-right: 4px;
}
/* 确认弹窗 */
.confirm-dialog {
padding: 24px;
}
.confirm-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.warning-icon {
font-size: 24px;
color: #f59e0b;
}
.confirm-title {
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin: 0;
}
.confirm-content {
margin-bottom: 24px;
}
.confirm-text {
font-size: 15px;
color: #374151;
line-height: 1.6;
margin-bottom: 16px;
}
.highlight {
color: #1677ff;
font-weight: 600;
}
.confirm-notice {
background: #fef3c7;
border-radius: 8px;
padding: 12px;
font-size: 13px;
color: #92400e;
line-height: 1.6;
}
.confirm-notice p {
margin: 4px 0;
}
.confirm-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.confirm-actions .van-button {
flex: 1;
max-width: 120px;
}
</style>

View File

@@ -13,105 +13,123 @@
根据相关规定提现功能需要完成实名认证后才能使用提现金额将转入您实名认证的账户中 根据相关规定提现功能需要完成实名认证后才能使用提现金额将转入您实名认证的账户中
</p> </p>
<van-button type="primary" block class="text-white rounded-xl h-10" <van-button type="primary" block class="text-white rounded-xl h-10"
style="background-color: var(--van-theme-primary);" style="background-color: var(--van-theme-primary);" @click="openRealNameAuth">
@click="openRealNameAuth">
立即实名认证 立即实名认证
</van-button> </van-button>
</div> </div>
</div> </div>
<div> <div>
<!-- 提现卡片 --> <!-- 提现卡片 -->
<div class="rounded-xl shadow-lg p-6 mb-4" style="background: linear-gradient(135deg, var(--van-theme-primary-light), rgba(255,255,255,0.8));"> <div class="rounded-xl shadow-lg p-6 mb-4"
style="background: linear-gradient(135deg, var(--van-theme-primary-light), rgba(255,255,255,0.8));">
<!-- 提现方式切换 --> <!-- 提现方式切换 -->
<van-tabs v-model:active="withdrawalType" shrink class="mb-4" title-active-color="var(--van-theme-primary)" @change="onWithdrawalTypeChange"> <div class="withdraw-tabs mb-5">
<van-tab title="支付宝" :name="1"> <div class="tab-container">
<div class="pt-2"> <div class="tab-slider" :class="withdrawalType === 2 ? 'tab-slider--right' : ''"></div>
<div class="flex items-center mb-4"> <div class="tab-item" :class="{ 'tab-item--active': withdrawalType === 1 }"
<van-icon name="alipay" class="text-xl mr-2" style="color: #1677FF;" /> @click="switchTab(1)">
<span class="text-base font-medium" style="color: var(--van-text-color);">支付宝提现</span> <van-icon name="alipay" class="tab-vant-icon" />
</div> <span class="tab-label">支付宝</span>
<!-- 支付宝账号 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">支付宝账号</label>
<van-field v-model="alipayAccount" placeholder="请输入支付宝账号"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm"
:rules="[{ required: true, message: ' ' }]">
<template #left-icon>
<van-icon name="phone-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
<small class="text-xs mt-1 block" style="color: var(--van-text-color-2);">可填写支付宝账户绑定的手机号</small>
</div>
<!-- 支付宝实名姓名 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">实名姓名</label>
<van-field v-model="realName" placeholder="请输入支付宝认证姓名"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm" :rules="[
{
required: true,
message: ' ',
validator: (val) =>
/^[\u4e00-\u9fa5]{2,4}$/.test(val),
},
]">
<template #left-icon>
<van-icon name="contact-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
<small class="text-xs mt-1 block" style="color: var(--van-text-color-2);">请填写支付宝账户认证的真实姓名</small>
</div>
</div> </div>
</van-tab> <div class="tab-item" :class="{ 'tab-item--active': withdrawalType === 2 }"
<van-tab title="银行卡" :name="2"> @click="switchTab(2)">
<div class="pt-2"> <van-icon name="card" class="tab-vant-icon" />
<div class="flex items-center mb-4"> <span class="tab-label">银行卡</span>
<van-icon name="balance-list-o" class="text-xl mr-2" style="color: var(--van-theme-primary);" />
<span class="text-base font-medium" style="color: var(--van-text-color);">银行卡提现</span>
</div>
<!-- 银行卡号 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">银行卡号</label>
<van-field v-model="bankCardNo" placeholder="请输入银行卡号" type="number"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm"
:rules="[{ required: true, message: ' ' }]">
<template #left-icon>
<van-icon name="balance-list-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
<small class="text-xs mt-1 block" style="color: var(--van-text-color-2);">请填写与实名一致的开户银行卡号</small>
</div>
<!-- 开户行名称 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">开户行名称</label>
<van-field v-model="bankName" placeholder="请输入开户行名称中国工商银行XX支行"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm"
:rules="[{ required: true, message: ' ' }]">
<template #left-icon>
<van-icon name="shop-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
</div>
<!-- 收款人姓名 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">收款人姓名</label>
<van-field v-model="bankPayeeName" placeholder="请输入银行卡户主姓名"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm" :rules="[
{
required: true,
message: ' ',
validator: (val) =>
/^[\u4e00-\u9fa5]{2,4}$/.test(val),
},
]">
<template #left-icon>
<van-icon name="contact-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
<small class="text-xs mt-1 block" style="color: var(--van-text-color-2);">需与银行卡开户姓名一致</small>
</div>
</div> </div>
</van-tab> </div>
</van-tabs> </div>
<!-- 支付宝表单 -->
<div v-show="withdrawalType === 1" class="tab-content">
<div class="flex items-center mb-4">
<van-icon name="alipay" class="text-xl mr-2" style="color: #1677FF;" />
<span class="text-base font-medium" style="color: var(--van-text-color);">支付宝提现</span>
</div>
<!-- 支付宝账号 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">支付宝账号</label>
<van-field v-model="alipayAccount" placeholder="请输入支付宝账号"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm"
:rules="[{ required: true, message: ' ' }]">
<template #left-icon>
<van-icon name="phone-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
<small class="text-xs mt-1 block"
style="color: var(--van-text-color-2);">可填写支付宝账户绑定的手机号</small>
</div>
<!-- 支付宝实名姓名 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">实名姓名</label>
<van-field v-model="realName" placeholder="请输入支付宝认证姓名"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm" :rules="[
{
required: true,
message: ' ',
validator: (val) =>
/^[\u4e00-\u9fa5]{2,4}$/.test(val),
},
]">
<template #left-icon>
<van-icon name="contact-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
<small class="text-xs mt-1 block"
style="color: var(--van-text-color-2);">请填写支付宝账户认证的真实姓名</small>
</div>
</div>
<!-- 银行卡表单 -->
<div v-show="withdrawalType === 2" class="tab-content">
<div class="flex items-center mb-4">
<van-icon name="card" class="text-xl mr-2"
style="color: var(--van-theme-primary);" />
<span class="text-base font-medium" style="color: var(--van-text-color);">银行卡提现</span>
</div>
<!-- 银行卡号 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">银行卡号</label>
<van-field v-model="bankCardNo" placeholder="请输入银行卡号" type="number"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm"
:rules="[{ required: true, message: ' ' }]">
<template #left-icon>
<van-icon name="balance-list-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
<small class="text-xs mt-1 block"
style="color: var(--van-text-color-2);">请填写与实名一致的开户银行卡号</small>
</div>
<!-- 开户行名称 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">开户行名称</label>
<van-field v-model="bankName" placeholder="请输入开户行名称中国工商银行XX支行"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm"
:rules="[{ required: true, message: ' ' }]">
<template #left-icon>
<van-icon name="shop-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
</div>
<!-- 收款人姓名 -->
<div class="mb-6">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">收款人姓名</label>
<van-field v-model="bankPayeeName" placeholder="请输入银行卡户主姓名"
class="flex items-center rounded-lg bg-white/90 backdrop-blur-sm shadow-sm" :rules="[
{
required: true,
message: ' ',
validator: (val) =>
/^[\u4e00-\u9fa5]{2,4}$/.test(val),
},
]">
<template #left-icon>
<van-icon name="contact-o" style="color: var(--van-text-color-2);" />
</template>
</van-field>
<small class="text-xs mt-1 block"
style="color: var(--van-text-color-2);">需与银行卡开户姓名一致</small>
</div>
</div>
<!-- 提现金额 --> <!-- 提现金额 -->
<div class="mb-4"> <div class="mb-4">
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">提现金额</label> <label class="text-sm mb-2 block" style="color: var(--van-text-color);">提现金额</label>
@@ -125,8 +143,7 @@
</template> </template>
<template #right-icon> </template> <template #right-icon> </template>
<template #button> <template #button>
<van-button size="small" type="primary" <van-button size="small" type="primary" class="rounded-full px-3 shadow-sm"
class="rounded-full px-3 shadow-sm"
style="background-color: var(--van-theme-primary); color: white;" style="background-color: var(--van-theme-primary); color: white;"
@click="fillMaxAmount"> @click="fillMaxAmount">
全部提现 全部提现
@@ -137,7 +154,8 @@
<!-- 金额提示 --> <!-- 金额提示 -->
<div class="text-sm mb-2" style="color: var(--van-text-color);"> <div class="text-sm mb-2" style="color: var(--van-text-color);">
可提现金额<span class="font-semibold" style="color: var(--van-theme-primary);">¥{{ availableAmount }}</span> 可提现金额<span class="font-semibold" style="color: var(--van-theme-primary);">¥{{ availableAmount
}}</span>
</div> </div>
@@ -209,7 +227,8 @@
<div class="border-t pt-2" style="border-color: var(--van-border-color);"> <div class="border-t pt-2" style="border-color: var(--van-border-color);">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="font-medium" style="color: var(--van-text-color);">预估到账</span> <span class="font-medium" style="color: var(--van-text-color);">预估到账</span>
<span class="font-bold text-lg" style="color: var(--van-theme-primary);">¥{{ estimatedActualAmount.toFixed(2) }}</span> <span class="font-bold text-lg" style="color: var(--van-theme-primary);">¥{{
estimatedActualAmount.toFixed(2) }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -234,8 +253,8 @@
取消 取消
</van-button> </van-button>
<van-button block round class="flex-1 h-11 font-medium shadow-sm" <van-button block round class="flex-1 h-11 font-medium shadow-sm"
style="background-color: var(--van-theme-primary); color: white;" style="background-color: var(--van-theme-primary); color: white;" :loading="isSubmitting"
:loading="isSubmitting" @click="confirmWithdraw"> @click="confirmWithdraw">
确认提现 确认提现
</van-button> </van-button>
</div> </div>
@@ -288,9 +307,8 @@
<!-- 进度条处理中状态 --> <!-- 进度条处理中状态 -->
<van-progress v-if="status === 1" :percentage="60" stroke-width="8" <van-progress v-if="status === 1" :percentage="60" stroke-width="8"
:color="`linear-gradient(to right, var(--van-theme-primary), var(--van-theme-primary-light))`" :color="`linear-gradient(to right, var(--van-theme-primary), var(--van-theme-primary-light))`"
:track-color="`var(--van-theme-primary-light)`" :track-color="`var(--van-theme-primary-light)`" class="!rounded-full" />
class="!rounded-full" />
<!-- 辅助文案 --> <!-- 辅助文案 -->
<div class="text-xs space-y-1.5" style="color: var(--van-text-color-2);"> <div class="text-xs space-y-1.5" style="color: var(--van-text-color-2);">
@@ -306,8 +324,7 @@
</div> </div>
<!-- 操作按钮 --> <!-- 操作按钮 -->
<van-button block round size="small" <van-button block round size="small" class="mt-4 h-11 font-medium shadow-sm"
class="mt-4 h-11 font-medium shadow-sm"
:style="{ backgroundColor: statusButtonColor[status], color: status === 1 ? 'var(--van-theme-primary)' : 'white' }" :style="{ backgroundColor: statusButtonColor[status], color: status === 1 ? 'var(--van-theme-primary)' : 'white' }"
@click="handlePopupAction"> @click="handlePopupAction">
{{ {{
@@ -424,6 +441,12 @@ const onWithdrawalTypeChange = () => {
} }
}; };
const switchTab = (type) => {
if (withdrawalType.value === type) return;
withdrawalType.value = type;
onWithdrawalTypeChange();
};
const getData = async () => { const getData = async () => {
const { data, error } = await getRevenueInfo(); const { data, error } = await getRevenueInfo();
@@ -594,6 +617,78 @@ const resetForm = () => {
@apply opacity-60 cursor-not-allowed; @apply opacity-60 cursor-not-allowed;
} }
/* 提现方式 Tab 样式 */
.withdraw-tabs {
display: flex;
}
.tab-container {
position: relative;
display: flex;
width: 100%;
background: rgba(0, 0, 0, 0.04);
border-radius: 10px;
padding: 3px;
gap: 3px;
}
.tab-slider {
position: absolute;
top: 3px;
left: 3px;
width: calc(50% - 4.5px);
height: calc(100% - 6px);
background: #fff;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
transition: transform 0.2s ease;
z-index: 0;
}
.tab-slider--right {
transform: translateX(calc(100% + 3px));
}
.tab-item {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
flex: 1;
padding: 7px 16px;
border-radius: 8px;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
.tab-item--active .tab-label {
color: var(--van-theme-primary);
font-weight: 600;
}
.tab-item:not(.tab-item--active) .tab-label {
color: var(--van-text-color-2);
}
.tab-item:not(.tab-item--active) .tab-vant-icon {
color: var(--van-text-color-3);
}
.tab-item--active .tab-vant-icon {
color: var(--van-theme-primary);
}
.tab-label {
font-size: 14px;
}
.tab-vant-icon {
font-size: 16px;
}
/* 弹窗入场动画 */ /* 弹窗入场动画 */
.van-popup { .van-popup {
transition: transform 0.4s cubic-bezier(0.22, 0.61, 0.36, 1), transition: transform 0.4s cubic-bezier(0.22, 0.61, 0.36, 1),

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="min-h-screen bg-gray-50"> <div class="min-h-screen bg-gray-50">
<!-- 提现记录列表 --> <!-- 提现记录列表 -->
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> <van-list v-model:loading="loading" :finished="finished" :finished-text="data.list.length > 0 ? '没有更多了' : ''" @load="onLoad">
<div v-for="(item, index) in data.list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm"> <div v-for="(item, index) in data.list" :key="index" class="mx-4 my-2 bg-white rounded-lg p-4 shadow-sm">
<div class="flex justify-between items-center mb-2"> <div class="flex justify-between items-center mb-2">
<span class="text-gray-500 text-sm">{{ <span class="text-gray-500 text-sm">{{
@@ -36,11 +36,15 @@
</div> </div>
</div> </div>
</van-list> </van-list>
<!-- 空状态 -->
<EmptyState v-if="!loading && !data.list.length" text="暂无提现记录" />
</div> </div>
</template> </template>
<script setup> <script setup>
import { getWithdrawalList } from '@/api/agent' import { getWithdrawalList } from '@/api/agent'
import EmptyState from '@/components/EmptyState.vue'
// 新系统状态映射配置1=待审核2=审核通过3=审核拒绝4=提现中5=提现成功6=提现失败 // 新系统状态映射配置1=待审核2=审核通过3=审核拒绝4=提现中5=提现成功6=提现失败
const statusConfig = { const statusConfig = {
@@ -140,15 +144,11 @@ const getAmountColor = (status) => {
// 加载更多数据 // 加载更多数据
const onLoad = async () => { const onLoad = async () => {
if (!finished.value) { await getData();
await getData();
}
}; };
// 获取数据 // 获取数据
const getData = async () => { const getData = async () => {
if (loading.value || finished.value) return
try { try {
loading.value = true; loading.value = true;
const { data: res, error } = await getWithdrawalList({ const { data: res, error } = await getWithdrawalList({
@@ -157,45 +157,35 @@ const getData = async () => {
}); });
if (res.value?.code === 200 && !error.value) { if (res.value?.code === 200 && !error.value) {
// 首次加载 const list = res.value.data.list || []
if (page.value === 1) { const total = res.value.data.total || 0
data.value = res.value.data;
} else {
// 分页加载
data.value.list.push(...res.value.data.list);
}
// 更新分页状态 if (page.value === 1) {
page.value++; data.value = { total, list }
} else {
data.value.list.push(...list)
}
// 判断是否加载完成 // 判断是否加载完成
if ( if (
data.value.list.length >= res.value.data.total || data.value.list.length >= total ||
res.value.data.list.length < pageSize.value list.length < pageSize.value
) { ) {
finished.value = true; finished.value = true;
} else {
page.value++;
} }
} else { } else {
// 接口返回错误或请求失败,停止翻页
finished.value = true; finished.value = true;
console.error('获取提现列表失败:', res.value?.msg || error.value || '未知错误'); console.error('获取提现列表失败:', res.value?.msg || error.value || '未知错误');
} }
} catch (err) { } catch (err) {
// 捕获异常,停止翻页
finished.value = true; finished.value = true;
console.error('获取提现列表失败:', err); console.error('获取提现列表失败:', err);
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; };
// 初始化加载
onMounted(async () => {
// 重置分页状态
page.value = 1;
finished.value = false;
await getData();
});
</script> </script>
<style scoped> <style scoped>

View File

@@ -15,8 +15,8 @@ export default defineConfig({
strictPort: true, // 如果端口被占用则抛出错误而不是使用下一个可用端口 strictPort: true, // 如果端口被占用则抛出错误而不是使用下一个可用端口
proxy: { proxy: {
"/api/v1": { "/api/v1": {
// target: "http://127.0.0.1:8888", // 本地接口地址 target: "http://127.0.0.1:8888", // 本地接口地址
target: "https://www.quannengcha.com", // 线上接口地址 // target: "https://www.quannengcha.com", // 线上接口地址
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path, // 可选:确保路径不被修改 rewrite: (path) => path, // 可选:确保路径不被修改
}, },