f
This commit is contained in:
12
src/components/EmptyState.vue
Normal file
12
src/components/EmptyState.vue
Normal 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>
|
||||
@@ -15,11 +15,11 @@
|
||||
@blur="onBlurPrice" class="!text-3xl" />
|
||||
</div>
|
||||
<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 class="flex items-center justify-between mt-2">
|
||||
<div>底价成本为<span class="text-orange-500"> {{ baseCost }} </span>元</div>
|
||||
<div>提价成本为<span class="text-orange-500"> {{ raiseCost }} </span>元</div>
|
||||
<div>底价成本为<span class="text-orange-500"> {{ pricingResult.baseCost }} </span>元</div>
|
||||
<div>提价成本为<span class="text-orange-500"> {{ pricingResult.raiseCost }} </span>元</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card m-4">
|
||||
@@ -42,6 +42,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { calculatePromotionPricing, validatePrice } from '@/utils/promotionPricing'
|
||||
|
||||
const props = defineProps({
|
||||
defaultPrice: {
|
||||
type: Number,
|
||||
@@ -57,108 +59,22 @@ const emit = defineEmits(["change"])
|
||||
const show = defineModel("show")
|
||||
const price = ref(null)
|
||||
|
||||
|
||||
watch(show, () => {
|
||||
price.value = defaultPrice.value
|
||||
})
|
||||
|
||||
|
||||
const costPrice = computed(() => {
|
||||
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 pricingResult = computed(() => {
|
||||
return calculatePromotionPricing(price.value, productConfig.value)
|
||||
})
|
||||
|
||||
const rateFormat = (rate) => {
|
||||
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 onConfirm = () => {
|
||||
if (isManualConfirm.value) return
|
||||
const { newPrice, message } = validatePrice(price.value)
|
||||
const { newPrice, message } = validatePrice(price.value, productConfig.value, defaultPrice.value)
|
||||
if (message) {
|
||||
price.value = newPrice
|
||||
showToast({ message });
|
||||
@@ -166,11 +82,10 @@ const onConfirm = () => {
|
||||
emit("change", price.value)
|
||||
show.value = false
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
const onBlurPrice = () => {
|
||||
const { newPrice, message } = validatePrice(price.value)
|
||||
const { newPrice, message } = validatePrice(price.value, productConfig.value, defaultPrice.value)
|
||||
if (message) {
|
||||
isManualConfirm.value = true
|
||||
price.value = newPrice
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<router-view />
|
||||
<van-popup v-model:show="showPopup" round @click-overlay="onClickOverlay">
|
||||
<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 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>
|
||||
</van-popup>
|
||||
@@ -18,12 +19,16 @@ import { useRoute } from 'vue-router'
|
||||
const showPopup = ref(false)
|
||||
const notify = ref([])
|
||||
const currentNotify = ref(null)
|
||||
const pendingNotifyQueue = ref([])
|
||||
const shownNotificationKeys = ref(new Set())
|
||||
const SESSION_KEY = 'qnc_webview_shown_notifications'
|
||||
|
||||
// 获取当前页面路径
|
||||
const route = useRoute()
|
||||
|
||||
// 获取通知数据
|
||||
onMounted(() => {
|
||||
loadShownNotificationKeys()
|
||||
getGlobalNotify()
|
||||
})
|
||||
|
||||
@@ -33,60 +38,128 @@ const getGlobalNotify = async () => {
|
||||
.get()
|
||||
.json()
|
||||
|
||||
if (data.value && !error.value) {
|
||||
if (data.value !== 200) {
|
||||
notify.value = data.value.data.notifications
|
||||
checkNotification() // 在获取数据后检查通知
|
||||
}
|
||||
if (data.value && !error.value && data.value.code === 200 && data.value.data) {
|
||||
notify.value = data.value.data.notifications ?? []
|
||||
checkNotification()
|
||||
}
|
||||
}
|
||||
|
||||
// 判断当前时间是否在通知的时间范围内
|
||||
/** 与后台「展示时间」一致:未配置或起止相同视为「全天」,任意时刻都算在范围内 */
|
||||
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 currentMinutes = now.getHours() * 60 + now.getMinutes()
|
||||
|
||||
// 将 startTime 和 endTime 转换为分钟数
|
||||
const startParts = startTime.split(':').map(Number)
|
||||
const endParts = endTime.split(':').map(Number)
|
||||
const startMinutes = startParts[0] * 60 + startParts[1]
|
||||
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 toMinutes = (t) => {
|
||||
const parts = t.split(':').map(Number)
|
||||
const h = parts[0] ?? 0
|
||||
const m = parts[1] ?? 0
|
||||
return h * 60 + m
|
||||
}
|
||||
const startMinutes = toMinutes(s)
|
||||
const endMinutes = toMinutes(e)
|
||||
|
||||
// 普通情况,直接判断时间是否在范围内
|
||||
if (endMinutes < startMinutes) {
|
||||
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
|
||||
const checkNotification = () => {
|
||||
// 遍历通知数组,找到第一个符合条件的通知
|
||||
for (let notification of notify.value) {
|
||||
// 判断时间是否符合当前时间
|
||||
const isTimeValid = isWithinTimeRange(notification.startTime, notification.endTime)
|
||||
// 判断页面是否符合
|
||||
if (showPopup.value) return
|
||||
|
||||
if (isTimeValid && notification.notificationPage === route.path) {
|
||||
currentNotify.value = notification
|
||||
showPopup.value = true
|
||||
break // 只显示第一个符合的通知
|
||||
const matchedNotifications = []
|
||||
for (let notification of notify.value) {
|
||||
const isTimeValid = isWithinTimeRange(notification.startTime, notification.endTime)
|
||||
const isPageValid = matchesNotificationPage(notification.notificationPage)
|
||||
const isShown = hasShownNotification(notification)
|
||||
|
||||
if (isTimeValid && isPageValid && !isShown) {
|
||||
matchedNotifications.push(notification)
|
||||
}
|
||||
}
|
||||
|
||||
pendingNotifyQueue.value = matchedNotifications
|
||||
showNextNotification()
|
||||
}
|
||||
|
||||
// 监听路由变化
|
||||
watch(() => route.path, () => {
|
||||
checkNotification() // 每次路由变化时重新判断通知
|
||||
checkNotification()
|
||||
})
|
||||
|
||||
// 关闭弹窗
|
||||
const onClosePopup = () => {
|
||||
showNextNotification()
|
||||
}
|
||||
|
||||
const onClickOverlay = () => {
|
||||
showPopup.value = false
|
||||
onClosePopup()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
162
src/utils/promotionPricing.js
Normal file
162
src/utils/promotionPricing.js
Normal 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 }
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<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 class="flex justify-between items-center mb-2">
|
||||
<span class="text-gray-500 text-sm">{{ item.create_time || '-' }}</span>
|
||||
@@ -19,11 +19,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<EmptyState v-if="!loading && !data.list.length" text="暂无佣金记录" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getCommissionList } from '@/api/agent'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
// 颜色配置(根据产品名称映射)
|
||||
const typeColors = {
|
||||
@@ -60,16 +64,11 @@ const getDotColor = (name) => {
|
||||
|
||||
// 加载更多数据
|
||||
const onLoad = async () => {
|
||||
if (!finished.value) {
|
||||
page.value++
|
||||
await getData()
|
||||
}
|
||||
await getData()
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const getData = async () => {
|
||||
if (loading.value || finished.value) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const { data: res, error } = await getCommissionList({
|
||||
@@ -78,37 +77,33 @@ const getData = async () => {
|
||||
})
|
||||
|
||||
if (res.value?.code === 200 && !error.value) {
|
||||
// 首次加载
|
||||
const list = res.value.data.list || []
|
||||
const total = res.value.data.total || 0
|
||||
|
||||
if (page.value === 1) {
|
||||
data.value = res.value.data
|
||||
data.value = { total, list }
|
||||
} else {
|
||||
// 分页加载
|
||||
data.value.list.push(...res.value.data.list)
|
||||
data.value.list.push(...list)
|
||||
}
|
||||
|
||||
// 判断是否加载完成
|
||||
if (data.value.list.length >= res.value.data.total ||
|
||||
res.value.data.list.length < pageSize.value) {
|
||||
if (data.value.list.length >= total ||
|
||||
list.length < pageSize.value) {
|
||||
finished.value = true
|
||||
} else {
|
||||
page.value++
|
||||
}
|
||||
} else {
|
||||
// 接口返回错误或请求失败,停止翻页
|
||||
finished.value = true
|
||||
console.error('获取佣金列表失败:', res.value?.msg || error.value || '未知错误')
|
||||
}
|
||||
} catch (err) {
|
||||
// 捕获异常,停止翻页
|
||||
finished.value = true
|
||||
console.error('获取佣金列表失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化加载
|
||||
onMounted(() => {
|
||||
getData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
@@ -9,7 +11,6 @@ const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
|
||||
async function fetchData() {
|
||||
if (loading.value || finished.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
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) {
|
||||
total.value = data.value.data.total
|
||||
if (data.value.data.list && data.value.data.list.length > 0) {
|
||||
reportList.value.push(...data.value.data.list)
|
||||
page.value += 1
|
||||
if (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
|
||||
} else {
|
||||
page.value++
|
||||
}
|
||||
} else {
|
||||
finished.value = true
|
||||
@@ -39,14 +46,8 @@ async function fetchData() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
|
||||
const onLoad = () => {
|
||||
if (!finished.value) {
|
||||
fetchData()
|
||||
}
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function toDetail(item) {
|
||||
@@ -87,7 +88,7 @@ function statusClass(state) {
|
||||
|
||||
<template>
|
||||
<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)"
|
||||
class="bg-white rounded-lg shadow-sm p-4 mb-4 relative cursor-pointer">
|
||||
<div class="flex flex-col">
|
||||
@@ -100,9 +101,10 @@ function statusClass(state) {
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
</div>
|
||||
|
||||
<van-empty v-if="!loading && reportList.length === 0" description="暂无推广查询记录" />
|
||||
<!-- 空状态 -->
|
||||
<EmptyState v-if="!loading && reportList.length === 0" text="暂无推广查询记录" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</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 class="flex justify-between items-center mb-2">
|
||||
<span class="text-gray-500 text-sm">{{ item.create_time || '-' }}</span>
|
||||
@@ -45,12 +45,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<EmptyState v-if="!loading && !data.list.length" text="暂无返佣记录" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { getRebateList, getUpgradeRebateList } from '@/api/agent'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
// 返佣类型映射配置(推广返佣)
|
||||
const typeConfig = {
|
||||
@@ -162,16 +166,11 @@ const onTabChange = (name) => {
|
||||
|
||||
// 加载更多数据
|
||||
const onLoad = async () => {
|
||||
if (!finished.value) {
|
||||
page.value++
|
||||
await getData()
|
||||
}
|
||||
await getData()
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const getData = async () => {
|
||||
if (loading.value || finished.value) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
@@ -227,6 +226,8 @@ const getData = async () => {
|
||||
// 判断是否加载完成
|
||||
if (data.value.list.length >= total || list.length < pageSize.value) {
|
||||
finished.value = true
|
||||
} else {
|
||||
page.value++
|
||||
}
|
||||
} else {
|
||||
// 接口返回错误或请求失败,停止翻页
|
||||
@@ -241,11 +242,6 @@ const getData = async () => {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化加载
|
||||
onMounted(() => {
|
||||
getData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
@@ -20,11 +20,9 @@ const max = ref(60)
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
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
|
||||
try {
|
||||
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) {
|
||||
total.value = data.value.data.total
|
||||
if (data.value.data.list && data.value.data.list.length > 0) {
|
||||
reportList.value.push(...data.value.data.list)
|
||||
page.value += 1
|
||||
if (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
|
||||
} else {
|
||||
page.value++
|
||||
}
|
||||
} else {
|
||||
// 接口返回错误,停止翻页
|
||||
finished.value = true
|
||||
console.error('获取查询列表失败:', data.value.msg || '未知错误')
|
||||
}
|
||||
} else {
|
||||
// 请求失败或返回错误,停止翻页
|
||||
finished.value = true
|
||||
console.error('获取查询列表失败:', error.value || '请求失败')
|
||||
}
|
||||
} catch (err) {
|
||||
// 捕获异常,停止翻页
|
||||
finished.value = true
|
||||
console.error('获取查询列表失败:', err)
|
||||
} finally {
|
||||
@@ -59,21 +60,9 @@ async function fetchData() {
|
||||
}
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
onMounted(async () => {
|
||||
fetchData()
|
||||
})
|
||||
|
||||
// 下拉触底加载更多
|
||||
// 触底加载更多
|
||||
const onLoad = () => {
|
||||
if (!finished.value) {
|
||||
console.log("finished", finished.value)
|
||||
if (num.value >= max.value) {
|
||||
finished.value = true
|
||||
} else {
|
||||
fetchData()
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function toDetail(item) {
|
||||
@@ -91,7 +80,6 @@ function handleBindSuccess() {
|
||||
page.value = 1
|
||||
finished.value = false
|
||||
loading.value = false
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 状态文字映射
|
||||
@@ -137,13 +125,7 @@ function statusClass(state) {
|
||||
@click="dialogStore.openBindPhone">绑定手机号</button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">为保障用户隐私及数据安全,报告保留{{ appStore.queryRetentionDays || 30 }}天,过期自动清理</div>
|
||||
<div v-if="hasNoRecords"
|
||||
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">
|
||||
<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)"
|
||||
class="bg-white rounded-lg shadow-sm p-4 mb-4 relative cursor-pointer">
|
||||
<div class="flex flex-col">
|
||||
@@ -157,6 +139,14 @@ function statusClass(state) {
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ function toPrivacyPolicy() {
|
||||
|
||||
<!-- 注册按钮(推广链接来源的登录不展示) -->
|
||||
<button v-if="!hideRegister" class="register-btn" @click="goToRegister">
|
||||
注册成为代理
|
||||
成为代理
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -203,8 +203,8 @@
|
||||
<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>
|
||||
</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"
|
||||
@click="toRealNameAuth">
|
||||
<img src="@/assets/images/me/smrz.svg" class="w-8 h-8 object-contain" alt="提现" />
|
||||
@@ -259,6 +259,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<BindPhoneDialog />
|
||||
<RealNameAuthDialog />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -274,6 +275,7 @@ import { useDialogStore } from "@/stores/dialogStore";
|
||||
import useApiFetch from "@/composables/useApiFetch";
|
||||
import { getRevenueInfo } from '@/api/agent';
|
||||
import BindPhoneDialog from "@/components/BindPhoneDialog.vue";
|
||||
import RealNameAuthDialog from "@/components/RealNameAuthDialog.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const agentStore = useAgentStore();
|
||||
|
||||
@@ -93,6 +93,7 @@ import PriceInputPopup from '@/components/PriceInputPopup.vue';
|
||||
import QRcode from '@/components/QRcode.vue';
|
||||
import ReportFeatures from '@/components/ReportFeatures.vue';
|
||||
import { getProductConfig, generateLink } from '@/api/agent';
|
||||
import { calculatePromotionPricing, safeTruncate } from '@/utils/promotionPricing';
|
||||
|
||||
// 导入logo图片
|
||||
import personalDataLogo from '@/assets/images/promote/personal_data_logo.png';
|
||||
@@ -140,62 +141,14 @@ const currentLogo = computed(() => {
|
||||
return logoMap[currentFeature.value] || null;
|
||||
});
|
||||
|
||||
const costPrice = computed(() => {
|
||||
if (!pickerProductConfig.value) return 0.00
|
||||
|
||||
// 新系统:成本价 = 实际底价(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 pricingResult = computed(() => {
|
||||
return calculatePromotionPricing(clientPrice.value, pickerProductConfig.value)
|
||||
});
|
||||
|
||||
const baseCost = computed(() => {
|
||||
if (!pickerProductConfig.value) return "0.00";
|
||||
const actualBasePrice = Number(pickerProductConfig.value.actual_base_price) || 0;
|
||||
return safeTruncate(actualBasePrice);
|
||||
});
|
||||
|
||||
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 costPrice = computed(() => pricingResult.value.costPrice);
|
||||
const baseCost = computed(() => pricingResult.value.baseCost);
|
||||
const raiseCost = computed(() => pricingResult.value.raiseCost);
|
||||
const promotionRevenue = computed(() => pricingResult.value.promotionRevenue);
|
||||
// 获取产品信息
|
||||
const getProductInfo = async () => {
|
||||
if (!currentFeature.value) {
|
||||
@@ -336,7 +289,7 @@ const generatePromotionCode = async () => {
|
||||
// 新系统API:使用 product_id、set_price 和 target_path
|
||||
const { data, error } = await generateLink({
|
||||
product_id: pickerProductConfig.value.product_id,
|
||||
set_price: priceNum,
|
||||
set_price: safeTruncate(priceNum),
|
||||
target_path: targetPath
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import useApiFetch from '@/composables/useApiFetch'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const loading = ref(false)
|
||||
@@ -23,9 +24,6 @@ const inviteListTotal = ref(0)
|
||||
|
||||
// 获取详情数据
|
||||
const fetchDetail = async () => {
|
||||
if (loading.value) return
|
||||
if (finished.value && page.value > 1) return
|
||||
|
||||
loading.value = true
|
||||
const tabType = activeTab.value
|
||||
const { data, error } = await useApiFetch(
|
||||
@@ -151,10 +149,6 @@ const formatNumber = num => {
|
||||
if (!num) return '0.00'
|
||||
return Number(num).toFixed(2)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -236,10 +230,9 @@ onMounted(() => {
|
||||
<van-tabs v-model:active="activeTab" @change="switchTab">
|
||||
<van-tab title="订单列表" name="order">
|
||||
<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 v-if="orderList.length === 0" class="text-center text-gray-500 py-8">暂无订单记录</div>
|
||||
<div v-else v-for="item in orderList" :key="item.order_no"
|
||||
<div v-for="item in orderList" :key="item.order_no"
|
||||
class="order-item mb-3 border-b border-gray-200 pb-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
@@ -260,13 +253,13 @@ onMounted(() => {
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
<EmptyState v-if="!loading && orderList.length === 0" text="暂无订单记录" />
|
||||
</van-tab>
|
||||
<van-tab title="邀请列表" name="invite">
|
||||
<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 v-if="inviteList.length === 0" class="text-center text-gray-500 py-8">暂无邀请记录</div>
|
||||
<div v-else v-for="item in inviteList" :key="item.agent_id"
|
||||
<div v-for="item in inviteList" :key="item.agent_id"
|
||||
class="invite-item mb-3 border-b border-gray-200 pb-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3 flex-1">
|
||||
@@ -281,6 +274,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
<EmptyState v-if="!loading && inviteList.length === 0" text="暂无邀请记录" />
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useAgentStore } from '@/stores/agentStore'
|
||||
import { getSubordinateList } from '@/api/agent'
|
||||
import { useRouter } from 'vue-router'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
const agentStore = useAgentStore()
|
||||
const subordinates = ref([])
|
||||
@@ -15,16 +16,9 @@ const router = useRouter()
|
||||
|
||||
// 加载更多数据
|
||||
const onLoad = async () => {
|
||||
if (!finished.value) {
|
||||
await fetchSubordinates()
|
||||
}
|
||||
await fetchSubordinates()
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
// 初始化时重置状态
|
||||
page.value = 1
|
||||
finished.value = false
|
||||
})
|
||||
// 计算统计数据
|
||||
const statistics = ref({
|
||||
totalSubordinates: 0,
|
||||
@@ -32,8 +26,6 @@ const statistics = ref({
|
||||
|
||||
// 获取下级列表
|
||||
const fetchSubordinates = async () => {
|
||||
if (loading.value || finished.value) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const { data, error } = await getSubordinateList({
|
||||
@@ -131,10 +123,6 @@ const viewDetail = item => {
|
||||
params: { id: item.agent_id || item.id },
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchSubordinates()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -152,7 +140,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<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 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">
|
||||
@@ -199,6 +187,9 @@ onMounted(() => {
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<EmptyState v-if="!loading && !subordinates.length" text="暂无下级代理" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeMount } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useAgentStore } from '@/stores/agentStore'
|
||||
import { getTeamList } from '@/api/agent'
|
||||
import { useRouter } from 'vue-router'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
const agentStore = useAgentStore()
|
||||
const teamMembers = ref([])
|
||||
@@ -16,22 +17,12 @@ const searchMobile = ref('')
|
||||
|
||||
// 加载更多数据
|
||||
const onLoad = async () => {
|
||||
if (!finished.value) {
|
||||
await fetchTeamMembers()
|
||||
}
|
||||
await fetchTeamMembers()
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
// 初始化时重置状态
|
||||
page.value = 1
|
||||
finished.value = false
|
||||
})
|
||||
|
||||
|
||||
// 获取团队列表
|
||||
const fetchTeamMembers = async () => {
|
||||
if (loading.value || finished.value) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
@@ -155,10 +146,6 @@ const viewDetail = item => {
|
||||
params: { id: item.agent_id || item.id },
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchTeamMembers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -176,7 +163,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<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 v-for="(item, index) in teamMembers" :key="item.agent_id || item.id" class="team-member-item">
|
||||
<div class="member-card">
|
||||
@@ -236,6 +223,9 @@ onMounted(() => {
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<EmptyState v-if="!loading && !teamMembers.length" text="暂无团队成员" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,118 +1,96 @@
|
||||
<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="header-content">
|
||||
<h1 class="page-title">调整下级级别</h1>
|
||||
<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 class="rounded-2xl px-5 pt-5 pb-4 mb-3" style="background: var(--van-theme-primary);">
|
||||
<h1 class="text-[17px] font-semibold text-white leading-snug">调整下级级别</h1>
|
||||
<p class="text-[13px] text-white/70 mt-1 leading-normal">将团队中的普通代理升级为黄金代理</p>
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-section">
|
||||
<van-field v-model="searchMobile" placeholder="请输入手机号搜索" clearable @clear="handleClear"
|
||||
@keyup.enter="handleSearch">
|
||||
<template #button>
|
||||
<van-button size="small" type="primary" @click="handleSearch" class="search-btn">搜索</van-button>
|
||||
</template>
|
||||
</van-field>
|
||||
<div class="mb-3">
|
||||
<van-search v-model="searchMobile" placeholder="输入手机号搜索" shape="round" show-action
|
||||
@search="handleSearch" @cancel="handleClear" @clear="handleClear" />
|
||||
</div>
|
||||
|
||||
<!-- 提示 -->
|
||||
<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>
|
||||
|
||||
<!-- 代理列表 -->
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
|
||||
<div class="list-container">
|
||||
<div v-if="teamMembers.length === 0 && !loading" class="empty-state">
|
||||
<van-icon name="user-o" class="empty-icon" />
|
||||
<p class="empty-text">暂无可调整的普通代理</p>
|
||||
<van-list v-model:loading="loading" :finished="finished" :finished-text="teamMembers.length > 0 ? '没有更多了' : ''" @load="onLoad">
|
||||
<div v-for="item in teamMembers" :key="item.agent_id || item.id"
|
||||
class="bg-white rounded-2xl mb-2.5 overflow-hidden">
|
||||
<!-- 顶部:用户信息 -->
|
||||
<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 v-for="(item, index) in teamMembers" :key="item.agent_id || item.id" class="member-card">
|
||||
<div class="member-header">
|
||||
<div class="member-index">{{ index + 1 }}</div>
|
||||
<div class="member-info">
|
||||
<div class="member-mobile">{{ item.mobile || '未绑定手机' }}</div>
|
||||
<div class="member-time">加入时间:{{ formatTime(item.create_time) }}</div>
|
||||
</div>
|
||||
<div class="member-badge">
|
||||
<span class="level-badge level-normal">普通代理</span>
|
||||
</div>
|
||||
<!-- 中间:数据统计 -->
|
||||
<div class="grid grid-cols-3 mx-4 py-2.5"
|
||||
style="border-top: 1px solid var(--van-border-color);">
|
||||
<div class="text-center">
|
||||
<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_queries || 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);">¥{{ 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="stat-label">总查询量</div>
|
||||
<div class="stat-value">{{ formatCount(item.total_queries || 0) }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<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 class="flex justify-end px-4 pt-1 pb-3">
|
||||
<van-button size="small" round :loading="item.upgrading" @click="handleUpgrade(item)"
|
||||
style="background: var(--van-theme-primary); color: white; border: none; padding: 0 14px; font-size: 13px; height: 30px;">
|
||||
升级为黄金代理
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
|
||||
<!-- 确认升级弹窗 -->
|
||||
<van-popup v-model:show="showConfirmDialog" round position="center"
|
||||
:style="{ width: '85%', maxWidth: '400px' }">
|
||||
<div class="confirm-dialog">
|
||||
<div class="confirm-header">
|
||||
<van-icon name="warning-o" class="warning-icon" />
|
||||
<h3 class="confirm-title">确认升级</h3>
|
||||
</div>
|
||||
<div class="confirm-content">
|
||||
<p class="confirm-text">
|
||||
确定要将 <span class="highlight">{{ currentUpgradeItem?.mobile }}</span> 升级为黄金代理吗?
|
||||
</p>
|
||||
<div class="confirm-notice">
|
||||
<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>
|
||||
<!-- 空状态 -->
|
||||
<EmptyState v-if="!loading && !teamMembers.length" text="暂无可调整的普通代理" />
|
||||
|
||||
<!-- 确认弹窗 -->
|
||||
<van-dialog v-model:show="showConfirmDialog" title="确认升级" show-cancel-button
|
||||
:before-close="onBeforeClose" confirm-button-color="var(--van-theme-primary)">
|
||||
<div class="px-6 pb-4 pt-4">
|
||||
<p class="text-[14px] leading-relaxed mb-3" style="color: var(--van-text-color);">
|
||||
确定将 <span class="font-semibold" style="color: var(--van-theme-primary);">{{ currentUpgradeItem?.mobile }}</span> 升级为黄金代理?
|
||||
</p>
|
||||
<div class="text-[12px] leading-loose" style="color: var(--van-text-color-2);">
|
||||
<p>· 操作免费</p>
|
||||
<p>· 升级后不可撤销</p>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</van-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeMount, computed } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useAgentStore } from '@/stores/agentStore'
|
||||
import { getTeamList, upgradeSubordinate } from '@/api/agent'
|
||||
import { showToast, showSuccessToast, showFailToast } from 'vant'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const agentStore = useAgentStore()
|
||||
@@ -128,28 +106,16 @@ const showConfirmDialog = ref(false)
|
||||
const currentUpgradeItem = ref(null)
|
||||
const isUpgrading = ref(false)
|
||||
|
||||
// 检查是否是钻石代理
|
||||
const isDiamondAgent = computed(() => {
|
||||
return isAgent.value && level.value === 3
|
||||
})
|
||||
|
||||
// 加载更多数据
|
||||
const onLoad = async () => {
|
||||
if (!finished.value) {
|
||||
await fetchTeamMembers()
|
||||
}
|
||||
await fetchTeamMembers()
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
page.value = 1
|
||||
finished.value = false
|
||||
})
|
||||
|
||||
// 获取团队列表(仅普通代理)
|
||||
const fetchTeamMembers = async () => {
|
||||
if (loading.value || finished.value) return
|
||||
|
||||
loading.value = true
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: page.value,
|
||||
@@ -164,8 +130,6 @@ const fetchTeamMembers = async () => {
|
||||
if (data.value && !error.value) {
|
||||
if (data.value.code === 200) {
|
||||
let list = data.value.data.list || []
|
||||
|
||||
// 过滤出普通代理(level === 1)
|
||||
list = list.filter(item => {
|
||||
const agentLevel = item.level || (item.level_name === '普通' ? 1 : item.level_name === '黄金' ? 2 : item.level_name === '钻石' ? 3 : 1)
|
||||
return agentLevel === 1
|
||||
@@ -177,7 +141,6 @@ const fetchTeamMembers = async () => {
|
||||
teamMembers.value.push(...list)
|
||||
}
|
||||
|
||||
// 判断是否加载完成
|
||||
if (list.length < pageSize) {
|
||||
finished.value = true
|
||||
} else if (teamMembers.value.length >= data.value.data.total) {
|
||||
@@ -186,27 +149,21 @@ const fetchTeamMembers = async () => {
|
||||
page.value++
|
||||
}
|
||||
} else {
|
||||
// 接口返回错误,停止翻页
|
||||
finished.value = true
|
||||
showFailToast(data.value.msg || '获取团队列表失败')
|
||||
}
|
||||
} else {
|
||||
// 请求失败或返回错误,停止翻页
|
||||
finished.value = true
|
||||
showFailToast('获取团队列表失败')
|
||||
console.error('获取团队列表失败:', error.value || '请求失败')
|
||||
}
|
||||
} catch (err) {
|
||||
// 捕获异常,停止翻页
|
||||
finished.value = true
|
||||
console.error('获取团队列表失败:', err)
|
||||
showFailToast('获取团队列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = () => {
|
||||
finished.value = false
|
||||
page.value = 1
|
||||
@@ -215,14 +172,12 @@ const onRefresh = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 搜索功能
|
||||
const handleSearch = () => {
|
||||
finished.value = false
|
||||
page.value = 1
|
||||
fetchTeamMembers()
|
||||
}
|
||||
|
||||
// 清空搜索
|
||||
const handleClear = () => {
|
||||
searchMobile.value = ''
|
||||
finished.value = false
|
||||
@@ -230,13 +185,19 @@ const handleClear = () => {
|
||||
fetchTeamMembers()
|
||||
}
|
||||
|
||||
// 处理升级
|
||||
const handleUpgrade = (item) => {
|
||||
currentUpgradeItem.value = item
|
||||
showConfirmDialog.value = true
|
||||
}
|
||||
|
||||
// 确认升级
|
||||
const onBeforeClose = async (action) => {
|
||||
if (action === 'confirm') {
|
||||
await confirmUpgrade()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const confirmUpgrade = async () => {
|
||||
if (!currentUpgradeItem.value) return
|
||||
|
||||
@@ -244,7 +205,7 @@ const confirmUpgrade = async () => {
|
||||
try {
|
||||
const { data, error } = await upgradeSubordinate({
|
||||
subordinate_id: currentUpgradeItem.value.agent_id || currentUpgradeItem.value.id,
|
||||
to_level: 2 // 只能升级为黄金代理
|
||||
to_level: 2
|
||||
})
|
||||
|
||||
if (data.value && !error.value) {
|
||||
@@ -252,13 +213,9 @@ const confirmUpgrade = async () => {
|
||||
showSuccessToast('升级成功')
|
||||
showConfirmDialog.value = false
|
||||
currentUpgradeItem.value = null
|
||||
|
||||
// 刷新列表
|
||||
finished.value = false
|
||||
page.value = 1
|
||||
await fetchTeamMembers()
|
||||
|
||||
// 刷新代理状态
|
||||
await agentStore.fetchAgentStatus()
|
||||
} else {
|
||||
showFailToast(data.value.msg || '升级失败')
|
||||
@@ -267,307 +224,31 @@ const confirmUpgrade = async () => {
|
||||
showFailToast('升级失败,请重试')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('升级失败:', err)
|
||||
showFailToast('升级失败,请重试')
|
||||
} finally {
|
||||
isUpgrading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeStr) => {
|
||||
if (!timeStr) return '-'
|
||||
return timeStr.split(' ')[0]
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatNumber = (num) => {
|
||||
if (!num) return '0.00'
|
||||
return Number(num).toFixed(2)
|
||||
}
|
||||
|
||||
// 格式化数字
|
||||
const formatCount = (num) => {
|
||||
if (!num) return '0'
|
||||
return Number(num).toLocaleString()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 检查权限
|
||||
if (!isDiamondAgent.value) {
|
||||
showFailToast('只有钻石代理可以升级下级')
|
||||
router.back()
|
||||
return
|
||||
}
|
||||
fetchTeamMembers()
|
||||
})
|
||||
</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>
|
||||
|
||||
@@ -13,105 +13,123 @@
|
||||
根据相关规定,提现功能需要完成实名认证后才能使用,提现金额将转入您实名认证的账户中。
|
||||
</p>
|
||||
<van-button type="primary" block class="text-white rounded-xl h-10"
|
||||
style="background-color: var(--van-theme-primary);"
|
||||
@click="openRealNameAuth">
|
||||
style="background-color: var(--van-theme-primary);" @click="openRealNameAuth">
|
||||
立即实名认证
|
||||
</van-button>
|
||||
</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">
|
||||
<van-tab title="支付宝" :name="1">
|
||||
<div class="pt-2">
|
||||
<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 class="withdraw-tabs mb-5">
|
||||
<div class="tab-container">
|
||||
<div class="tab-slider" :class="withdrawalType === 2 ? 'tab-slider--right' : ''"></div>
|
||||
<div class="tab-item" :class="{ 'tab-item--active': withdrawalType === 1 }"
|
||||
@click="switchTab(1)">
|
||||
<van-icon name="alipay" class="tab-vant-icon" />
|
||||
<span class="tab-label">支付宝</span>
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="银行卡" :name="2">
|
||||
<div class="pt-2">
|
||||
<div class="flex items-center mb-4">
|
||||
<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 class="tab-item" :class="{ 'tab-item--active': withdrawalType === 2 }"
|
||||
@click="switchTab(2)">
|
||||
<van-icon name="card" class="tab-vant-icon" />
|
||||
<span class="tab-label">银行卡</span>
|
||||
</div>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
</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">
|
||||
<label class="text-sm mb-2 block" style="color: var(--van-text-color);">提现金额</label>
|
||||
@@ -125,8 +143,7 @@
|
||||
</template>
|
||||
<template #right-icon> 元 </template>
|
||||
<template #button>
|
||||
<van-button size="small" type="primary"
|
||||
class="rounded-full px-3 shadow-sm"
|
||||
<van-button size="small" type="primary" class="rounded-full px-3 shadow-sm"
|
||||
style="background-color: var(--van-theme-primary); color: white;"
|
||||
@click="fillMaxAmount">
|
||||
全部提现
|
||||
@@ -137,7 +154,8 @@
|
||||
|
||||
<!-- 金额提示 -->
|
||||
<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>
|
||||
|
||||
|
||||
@@ -209,7 +227,8 @@
|
||||
<div class="border-t pt-2" style="border-color: var(--van-border-color);">
|
||||
<div class="flex justify-between items-center">
|
||||
<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>
|
||||
@@ -234,8 +253,8 @@
|
||||
取消
|
||||
</van-button>
|
||||
<van-button block round class="flex-1 h-11 font-medium shadow-sm"
|
||||
style="background-color: var(--van-theme-primary); color: white;"
|
||||
:loading="isSubmitting" @click="confirmWithdraw">
|
||||
style="background-color: var(--van-theme-primary); color: white;" :loading="isSubmitting"
|
||||
@click="confirmWithdraw">
|
||||
确认提现
|
||||
</van-button>
|
||||
</div>
|
||||
@@ -289,8 +308,7 @@
|
||||
<!-- 进度条(处理中状态) -->
|
||||
<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))`"
|
||||
:track-color="`var(--van-theme-primary-light)`"
|
||||
class="!rounded-full" />
|
||||
:track-color="`var(--van-theme-primary-light)`" class="!rounded-full" />
|
||||
|
||||
<!-- 辅助文案 -->
|
||||
<div class="text-xs space-y-1.5" style="color: var(--van-text-color-2);">
|
||||
@@ -306,8 +324,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<van-button block round size="small"
|
||||
class="mt-4 h-11 font-medium shadow-sm"
|
||||
<van-button block round size="small" class="mt-4 h-11 font-medium shadow-sm"
|
||||
:style="{ backgroundColor: statusButtonColor[status], color: status === 1 ? 'var(--van-theme-primary)' : 'white' }"
|
||||
@click="handlePopupAction">
|
||||
{{
|
||||
@@ -424,6 +441,12 @@ const onWithdrawalTypeChange = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const switchTab = (type) => {
|
||||
if (withdrawalType.value === type) return;
|
||||
withdrawalType.value = type;
|
||||
onWithdrawalTypeChange();
|
||||
};
|
||||
|
||||
const getData = async () => {
|
||||
const { data, error } = await getRevenueInfo();
|
||||
|
||||
@@ -594,6 +617,78 @@ const resetForm = () => {
|
||||
@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 {
|
||||
transition: transform 0.4s cubic-bezier(0.22, 0.61, 0.36, 1),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<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 class="flex justify-between items-center mb-2">
|
||||
<span class="text-gray-500 text-sm">{{
|
||||
@@ -36,11 +36,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<EmptyState v-if="!loading && !data.list.length" text="暂无提现记录" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getWithdrawalList } from '@/api/agent'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
// 新系统状态映射配置:1=待审核,2=审核通过,3=审核拒绝,4=提现中,5=提现成功,6=提现失败
|
||||
const statusConfig = {
|
||||
@@ -140,15 +144,11 @@ const getAmountColor = (status) => {
|
||||
|
||||
// 加载更多数据
|
||||
const onLoad = async () => {
|
||||
if (!finished.value) {
|
||||
await getData();
|
||||
}
|
||||
await getData();
|
||||
};
|
||||
|
||||
// 获取数据
|
||||
const getData = async () => {
|
||||
if (loading.value || finished.value) return
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
const { data: res, error } = await getWithdrawalList({
|
||||
@@ -157,45 +157,35 @@ const getData = async () => {
|
||||
});
|
||||
|
||||
if (res.value?.code === 200 && !error.value) {
|
||||
// 首次加载
|
||||
if (page.value === 1) {
|
||||
data.value = res.value.data;
|
||||
} else {
|
||||
// 分页加载
|
||||
data.value.list.push(...res.value.data.list);
|
||||
}
|
||||
const list = res.value.data.list || []
|
||||
const total = res.value.data.total || 0
|
||||
|
||||
// 更新分页状态
|
||||
page.value++;
|
||||
if (page.value === 1) {
|
||||
data.value = { total, list }
|
||||
} else {
|
||||
data.value.list.push(...list)
|
||||
}
|
||||
|
||||
// 判断是否加载完成
|
||||
if (
|
||||
data.value.list.length >= res.value.data.total ||
|
||||
res.value.data.list.length < pageSize.value
|
||||
data.value.list.length >= total ||
|
||||
list.length < pageSize.value
|
||||
) {
|
||||
finished.value = true;
|
||||
} else {
|
||||
page.value++;
|
||||
}
|
||||
} else {
|
||||
// 接口返回错误或请求失败,停止翻页
|
||||
finished.value = true;
|
||||
console.error('获取提现列表失败:', res.value?.msg || error.value || '未知错误');
|
||||
}
|
||||
} catch (err) {
|
||||
// 捕获异常,停止翻页
|
||||
finished.value = true;
|
||||
console.error('获取提现列表失败:', err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化加载
|
||||
onMounted(async () => {
|
||||
// 重置分页状态
|
||||
page.value = 1;
|
||||
finished.value = false;
|
||||
await getData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -15,8 +15,8 @@ export default defineConfig({
|
||||
strictPort: true, // 如果端口被占用则抛出错误而不是使用下一个可用端口
|
||||
proxy: {
|
||||
"/api/v1": {
|
||||
// target: "http://127.0.0.1:8888", // 本地接口地址
|
||||
target: "https://www.quannengcha.com", // 线上接口地址
|
||||
target: "http://127.0.0.1:8888", // 本地接口地址
|
||||
// target: "https://www.quannengcha.com", // 线上接口地址
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path, // 可选:确保路径不被修改
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user