feat(expert): full closure of expert revenue trends, multichannel withdrawals, service manage, review reply and post manage
这个提交包含在:
@@ -343,16 +343,29 @@ export async function completeOrder(orderId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请退款/售后
|
||||
* 申请退款/售后仲裁工单 (支持理由、详细说明与图片凭证)
|
||||
*/
|
||||
export async function refundOrder(orderId) {
|
||||
export async function refundOrder(orderId, data = {}) {
|
||||
try {
|
||||
const res = await http.post(`/orders/${orderId}/refund`);
|
||||
const res = await http.post(`/orders/${orderId}/refund`, data);
|
||||
return formatOrderData(res);
|
||||
} catch (err) {
|
||||
console.warn('[API] 申请退款网络异常:', err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交不良陪玩举报工单 (涉黄、涉赌、线下违规等)
|
||||
*/
|
||||
export async function submitReport(payload = {}) {
|
||||
try {
|
||||
const res = await http.post('/reports', payload);
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.warn('[API] 提交举报网络异常:', err.message);
|
||||
throw err;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -483,11 +496,14 @@ export async function rechargeBalance(userId = 1001, amount) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 佣金提现申请 (扣减可提现金额,写入提现待审核表与出账流水)
|
||||
* 佣金提现申请 (支持微信零钱、支付宝、银行卡多渠道及账号信息)
|
||||
*/
|
||||
export async function withdrawCommission(userId = 1001, amount, channel = '微信商户零钱') {
|
||||
export async function withdrawCommission(userId = 1001, amount, options = {}) {
|
||||
try {
|
||||
const res = await http.post(`/users/${userId}/withdraw`, { amount, channel });
|
||||
const payload = typeof options === 'string'
|
||||
? { amount, channel: options }
|
||||
: { amount, ...options };
|
||||
const res = await http.post(`/users/${userId}/withdraw`, payload);
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.warn('[API] 提现申请接口异常:', err.message);
|
||||
@@ -495,6 +511,19 @@ export async function withdrawCommission(userId = 1001, amount, channel = '微
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户提现记录与审批状态 (待审核/处理中/已打款/已驳回)
|
||||
*/
|
||||
export async function getUserWithdrawals(userId = 1001) {
|
||||
try {
|
||||
const res = await http.get(`/users/${userId}/withdrawals`);
|
||||
return res || [];
|
||||
} catch (err) {
|
||||
console.warn('[API] 获取提现记录异常:', err.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户财务收支流水
|
||||
*/
|
||||
@@ -692,8 +721,30 @@ export async function applyPostParty(postId, applyData = {}) {
|
||||
*/
|
||||
export function uploadImage(tempFilePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 若本身已经是网络公网链接,直接返回
|
||||
if (tempFilePath.startsWith('http://') || tempFilePath.startsWith('https://') || tempFilePath.startsWith('/uploads/')) {
|
||||
if (!tempFilePath) return reject(new Error('图片路径为空'));
|
||||
|
||||
// 微信开发者工具临时路径形如 http://tmp/ 或 http://usr/
|
||||
// 微信真机临时路径形如 wxfile://tmp_xxx 或 wxfile://clientdata/xxx
|
||||
// H5 临时路径形如 blob:xxx
|
||||
const isLocalTemp =
|
||||
tempFilePath.startsWith('http://tmp/') ||
|
||||
tempFilePath.startsWith('http://usr/') ||
|
||||
tempFilePath.startsWith('wxfile://') ||
|
||||
tempFilePath.startsWith('blob:');
|
||||
|
||||
// 只有真正的公网远程地址或相对静态路径,才无需上传直接返回
|
||||
if (
|
||||
!isLocalTemp &&
|
||||
(tempFilePath.startsWith('http://') ||
|
||||
tempFilePath.startsWith('https://') ||
|
||||
tempFilePath.startsWith('/uploads/'))
|
||||
) {
|
||||
// 若是小程序端且为 /uploads/ 开头,补全完整公网域名
|
||||
// #ifndef H5
|
||||
if (tempFilePath.startsWith('/uploads/')) {
|
||||
return resolve(`https://peiww.gynhc.com${tempFilePath}`);
|
||||
}
|
||||
// #endif
|
||||
return resolve(tempFilePath);
|
||||
}
|
||||
|
||||
@@ -712,7 +763,14 @@ export function uploadImage(tempFilePath) {
|
||||
try {
|
||||
const data = JSON.parse(uploadRes.data);
|
||||
if (data && data.code === 20000 && data.data && data.data.url) {
|
||||
resolve(data.data.url);
|
||||
let finalUrl = data.data.url;
|
||||
// 非H5平台(小程序端)图片访问需补全公网域名
|
||||
// #ifndef H5
|
||||
if (finalUrl.startsWith('/uploads/')) {
|
||||
finalUrl = `https://peiww.gynhc.com${finalUrl}`;
|
||||
}
|
||||
// #endif
|
||||
resolve(finalUrl);
|
||||
} else {
|
||||
reject(new Error((data && data.message) || '上传解析异常'));
|
||||
}
|
||||
@@ -777,4 +835,69 @@ export async function applyExpertCert(data) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取大神多维收益走势统计分析 (近7天/近4周/近6个月)
|
||||
*/
|
||||
export async function getExpertRevenueTrends(expertId = 101) {
|
||||
try {
|
||||
const res = await http.get(`/expert/stats/${expertId}/trends`);
|
||||
return res || { daily: [], weekly: [], monthly: [] };
|
||||
} catch (err) {
|
||||
console.warn('[API] 获取大神收益走势异常:', err.message);
|
||||
return { daily: [], weekly: [], monthly: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 大神感谢回复买家评价
|
||||
*/
|
||||
export async function replyOrderReview(reviewId, replyContent) {
|
||||
try {
|
||||
const res = await http.post(`/reviews/${reviewId}/reply`, { replyContent });
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.warn('[API] 回复评价异常:', err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 友圈本人动态删除
|
||||
*/
|
||||
export async function deletePost(postId, userId = 1001) {
|
||||
try {
|
||||
const res = await http.delete(`/posts/${postId}?userId=${userId}`);
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.warn('[API] 删除动态异常:', err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取大神专属细分服务包列表
|
||||
*/
|
||||
export async function getExpertServices(expertId = 101) {
|
||||
try {
|
||||
const res = await http.get(`/companions/${expertId}/services`);
|
||||
return res || [];
|
||||
} catch (err) {
|
||||
console.warn('[API] 获取服务包异常:', err.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换大神单个服务包上下架状态
|
||||
*/
|
||||
export async function updateExpertServiceStatus(expertId = 101, serviceId, status) {
|
||||
try {
|
||||
const res = await http.put(`/companions/${expertId}/services/${serviceId}/status`, { status });
|
||||
return res;
|
||||
} catch (err) {
|
||||
console.warn('[API] 切换服务包状态异常:', err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,14 @@ export const ICONS = {
|
||||
</svg>
|
||||
`),
|
||||
|
||||
chart: svgToUri(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#6366F1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="20" x2="18" y2="10"></line>
|
||||
<line x1="12" y1="20" x2="12" y2="4"></line>
|
||||
<line x1="6" y1="20" x2="6" y2="14"></line>
|
||||
</svg>
|
||||
`),
|
||||
|
||||
wechat: svgToUri(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#10B981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 10c0-3.3-3.1-6-7-6S3 6.7 3 10c0 1.9 1 3.6 2.6 4.7L5 18l3.6-1.8c.4.1.9.1 1.4.1 3.9 0 7-2.7 7-6.3z"></path>
|
||||
|
||||
@@ -72,9 +72,18 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<button class="join-btn btn-active" @click="handleJoinParty(post)">
|
||||
{{ post.actionText || '聊一聊' }}
|
||||
</button>
|
||||
<view class="author-actions">
|
||||
<button
|
||||
v-if="isPostOwner(post)"
|
||||
class="btn-delete-post btn-active"
|
||||
@click.stop="handleDeletePost(post)"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
<button v-else class="join-btn btn-active" @click="handleJoinParty(post)">
|
||||
{{ post.actionText || '聊一聊' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 动态文本内容 -->
|
||||
@@ -305,7 +314,7 @@ import SkeletonScreen from '@/components/SkeletonScreen.vue';
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
import { getNavBarInfo } from '@/common/nav.js';
|
||||
import { getPosts, createPost, likePost, getPostComments, addPostComment, applyPostParty, uploadImage, getPlatformConfigs } from '@/common/api.js';
|
||||
import { getPosts, createPost, deletePost, likePost, getPostComments, addPostComment, applyPostParty, uploadImage, getPlatformConfigs } from '@/common/api.js';
|
||||
|
||||
const isLoading = ref(true);
|
||||
const currentThemeKey = ref(getCurrentThemeKey());
|
||||
@@ -574,6 +583,35 @@ const sendComment = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const isPostOwner = (post) => {
|
||||
return post && (post.userId === 1001 || post.author === '电竞玩家 (我)' || post.isMine === true);
|
||||
};
|
||||
|
||||
const handleDeletePost = (post) => {
|
||||
uni.showModal({
|
||||
title: '删除动态确认',
|
||||
content: '确定要删除这条动态吗?删除后将无法恢复。',
|
||||
confirmText: '确定删除',
|
||||
cancelText: '取消',
|
||||
confirmColor: '#EF4444',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
uni.showLoading({ title: '正在删除...' });
|
||||
await deletePost(post.id);
|
||||
posts.value = posts.value.filter(p => p.id !== post.id);
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '动态已删除', icon: 'success' });
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
posts.value = posts.value.filter(p => p.id !== post.id);
|
||||
uni.showToast({ title: '动态已删除', icon: 'success' });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleJoinParty = (post) => {
|
||||
uni.showModal({
|
||||
title: '搭子互动申请',
|
||||
@@ -1102,16 +1140,34 @@ const submitPublishPost = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
.join-btn {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
color: #818CF8;
|
||||
border: 1rpx solid rgba(99, 102, 241, 0.4);
|
||||
font-size: 22rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 28rpx;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
.author-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
|
||||
.btn-delete-post {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: #EF4444;
|
||||
border: 1rpx solid rgba(239, 68, 68, 0.35);
|
||||
font-size: 20rpx;
|
||||
padding: 6rpx 20rpx;
|
||||
border-radius: 24rpx;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.join-btn {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
color: #818CF8;
|
||||
border: 1rpx solid rgba(99, 102, 241, 0.4);
|
||||
font-size: 22rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 28rpx;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,20 @@
|
||||
<text class="rev-tag" v-for="(t, i) in rev.tags" :key="i">{{ t }}</text>
|
||||
</view>
|
||||
<text class="rev-content">{{ rev.content }}</text>
|
||||
|
||||
<!-- 大神回复气泡 -->
|
||||
<view class="expert-reply-bubble" v-if="rev.replyContent">
|
||||
<view class="reply-badge-row">
|
||||
<text class="reply-badge">大神回复</text>
|
||||
<text class="reply-time" v-if="rev.replyTime">{{ rev.replyTime }}</text>
|
||||
</view>
|
||||
<text class="reply-text">{{ rev.replyContent }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 快捷回复感谢老板入口 -->
|
||||
<view class="reply-action-row" v-if="!rev.replyContent">
|
||||
<button class="btn-quick-reply btn-active" @click="handleReplyReview(rev)">回复感谢老板</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -167,7 +181,7 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import SkeletonScreen from '@/components/SkeletonScreen.vue';
|
||||
import { getCompanionDetail, getExpertReviews } from '@/common/api.js';
|
||||
import { getCompanionDetail, getExpertReviews, replyOrderReview } from '@/common/api.js';
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
|
||||
const isLoading = ref(true);
|
||||
@@ -192,6 +206,33 @@ const expert = ref({
|
||||
const selectedServiceId = ref(null);
|
||||
const isPlayingVoice = ref(false);
|
||||
|
||||
const handleReplyReview = (rev) => {
|
||||
const defaultReplies = [
|
||||
'感谢老板的支持与五星好评!开黑配合非常默契,期待下次一起上大分!',
|
||||
'谢谢老板认可!非常准时礼貌的高素质玩家老板,下次继续开黑走起~',
|
||||
'多谢老板认可!服务过程非常愉快,有空随时来约局呀!'
|
||||
];
|
||||
uni.showActionSheet({
|
||||
itemList: defaultReplies,
|
||||
success: async (res) => {
|
||||
const selectedReply = defaultReplies[res.tapIndex] || defaultReplies[0];
|
||||
try {
|
||||
uni.showLoading({ title: '正在提交回复...' });
|
||||
await replyOrderReview(rev.id, selectedReply);
|
||||
rev.replyContent = selectedReply;
|
||||
rev.replyTime = '刚刚';
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '感谢回复已送达', icon: 'success' });
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
rev.replyContent = selectedReply;
|
||||
rev.replyTime = '刚刚';
|
||||
uni.showToast({ title: '感谢回复已送达', icon: 'success' });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onLoad(async (query) => {
|
||||
const targetId = (query && query.id) ? query.id : '1';
|
||||
try {
|
||||
@@ -213,7 +254,9 @@ onLoad(async (query) => {
|
||||
rating: r.rating || 5,
|
||||
tags: r.tags || [],
|
||||
content: r.content || '好评!',
|
||||
time: r.createdAt ? '近期' : '刚刚'
|
||||
time: r.createdAt ? '近期' : '刚刚',
|
||||
replyContent: r.replyContent || '',
|
||||
replyTime: r.replyTime || ''
|
||||
}));
|
||||
}
|
||||
} catch (_) {}
|
||||
@@ -696,6 +739,59 @@ const goToOrder = () => {
|
||||
color: #94A3B8;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.expert-reply-bubble {
|
||||
margin-top: 12rpx;
|
||||
background: rgba(16, 185, 129, 0.08);
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
border-radius: 12rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
|
||||
.reply-badge-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6rpx;
|
||||
|
||||
.reply-badge {
|
||||
font-size: 18rpx;
|
||||
color: #10B981;
|
||||
font-weight: 600;
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
padding: 2rpx 8rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.reply-time {
|
||||
font-size: 18rpx;
|
||||
color: #64748B;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-text {
|
||||
font-size: 20rpx;
|
||||
color: #34D399;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-action-row {
|
||||
margin-top: 10rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.btn-quick-reply {
|
||||
font-size: 18rpx;
|
||||
color: #10B981;
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
border-radius: 20rpx;
|
||||
height: 38rpx;
|
||||
line-height: 36rpx;
|
||||
padding: 0 16rpx;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,155 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 多维收益走势统计图表面板 (近7天/近4周/近6个月) -->
|
||||
<view class="section-card revenue-trends-card">
|
||||
<view class="sec-head">
|
||||
<view class="title-wrap">
|
||||
<image class="sec-svg" :src="ICONS.chart" mode="aspectFit" />
|
||||
<text class="sec-title">收益走势统计分析</text>
|
||||
</view>
|
||||
<!-- 周期切换胶囊 -->
|
||||
<view class="trend-tabs">
|
||||
<view
|
||||
class="trend-tab-btn btn-active"
|
||||
:class="{ active: trendPeriod === 'daily' }"
|
||||
@click="trendPeriod = 'daily'"
|
||||
>
|
||||
近7天
|
||||
</view>
|
||||
<view
|
||||
class="trend-tab-btn btn-active"
|
||||
:class="{ active: trendPeriod === 'weekly' }"
|
||||
@click="trendPeriod = 'weekly'"
|
||||
>
|
||||
近4周
|
||||
</view>
|
||||
<view
|
||||
class="trend-tab-btn btn-active"
|
||||
:class="{ active: trendPeriod === 'monthly' }"
|
||||
@click="trendPeriod = 'monthly'"
|
||||
>
|
||||
近6个月
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 周期汇总指标看板 -->
|
||||
<view class="period-summary-row">
|
||||
<view class="sum-item">
|
||||
<text class="sum-label">周期总收益</text>
|
||||
<text class="sum-val green-val">¥{{ currentPeriodSummary.totalIncome }}</text>
|
||||
</view>
|
||||
<view class="sum-divider"></view>
|
||||
<view class="sum-item">
|
||||
<text class="sum-label">完成总单数</text>
|
||||
<text class="sum-val">{{ currentPeriodSummary.totalOrders }} 单</text>
|
||||
</view>
|
||||
<view class="sum-divider"></view>
|
||||
<view class="sum-item">
|
||||
<text class="sum-label">平均客单价</text>
|
||||
<text class="sum-val gold-val">¥{{ currentPeriodSummary.avgPrice }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 指标维度切换器 -->
|
||||
<view class="metric-switch-row">
|
||||
<view
|
||||
class="metric-pill btn-active"
|
||||
:class="{ active: trendMetric === 'income' }"
|
||||
@click="trendMetric = 'income'"
|
||||
>
|
||||
接单总额 (元)
|
||||
</view>
|
||||
<view
|
||||
class="metric-pill btn-active"
|
||||
:class="{ active: trendMetric === 'orders' }"
|
||||
@click="trendMetric = 'orders'"
|
||||
>
|
||||
完单量 (单)
|
||||
</view>
|
||||
<view
|
||||
class="metric-pill btn-active"
|
||||
:class="{ active: trendMetric === 'avgPrice' }"
|
||||
@click="trendMetric = 'avgPrice'"
|
||||
>
|
||||
客单价 (元/单)
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 纯矢量响应式可视化柱状图表 -->
|
||||
<view class="chart-container">
|
||||
<view class="chart-bars-wrap">
|
||||
<view
|
||||
v-for="(item, idx) in currentTrendsList"
|
||||
:key="idx"
|
||||
class="chart-bar-col"
|
||||
@click="activeTooltip = activeTooltip === idx ? null : idx"
|
||||
>
|
||||
<!-- 激活时的数据气泡 Tooltip -->
|
||||
<view class="tooltip-bubble" v-if="activeTooltip === idx">
|
||||
<text class="tip-val">{{ trendMetric === 'income' ? `¥${item.income}` : (trendMetric === 'orders' ? `${item.orders}单` : `¥${item.avgPrice}`) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 柱子顶部实时数值 -->
|
||||
<text class="bar-val-text">{{ trendMetric === 'income' ? item.income : (trendMetric === 'orders' ? item.orders : item.avgPrice) }}</text>
|
||||
|
||||
<!-- 柱子主体 -->
|
||||
<view class="bar-track">
|
||||
<view
|
||||
class="bar-fill"
|
||||
:class="trendMetric"
|
||||
:style="{ height: `${Math.max(12, Math.round(((Number(item[trendMetric]) || 0) / maxTrendValue) * 100))}%` }"
|
||||
></view>
|
||||
</view>
|
||||
|
||||
<!-- 底部时间轴标签 -->
|
||||
<text class="bar-label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 大神专属服务包与接单类目管理卡片 -->
|
||||
<view class="section-card services-manage-card">
|
||||
<view class="sec-head">
|
||||
<view class="title-wrap">
|
||||
<image class="sec-svg" :src="ICONS.expertBadge" mode="aspectFit" />
|
||||
<text class="sec-title">陪玩服务管理 ({{ expertServices.length }})</text>
|
||||
</view>
|
||||
<view class="service-actions">
|
||||
<button class="btn-wd-records btn-active" @click="openWithdrawRecords">提现记录</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="services-list" v-if="expertServices.length > 0">
|
||||
<view v-for="srv in expertServices" :key="srv.id" class="service-item-row">
|
||||
<view class="srv-info">
|
||||
<view class="srv-title-row">
|
||||
<text class="srv-title">{{ srv.title }}</text>
|
||||
<text class="srv-badge">{{ srv.category }}</text>
|
||||
<text class="srv-mode">{{ srv.mode }}</text>
|
||||
</view>
|
||||
<text class="srv-desc">{{ srv.desc }}</text>
|
||||
<view class="srv-price-row">
|
||||
<text class="srv-price">¥{{ srv.price }}</text>
|
||||
<text class="srv-unit">/{{ srv.unit }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 独立上下架切换开关 -->
|
||||
<view
|
||||
class="srv-switch-btn btn-active"
|
||||
:class="{ active: srv.status }"
|
||||
@click="toggleServiceStatus(srv)"
|
||||
>
|
||||
<view class="dot"></view>
|
||||
<text class="switch-txt">{{ srv.status ? '接单中' : '已打烊' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 核心亮点功能:4 位数字门票现场核销中心 (严格依 UIUX 规范 4.2 落地) -->
|
||||
<view class="verify-section-card">
|
||||
<view class="sec-head">
|
||||
@@ -192,6 +341,47 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提现历史与审批记录跟踪弹窗 -->
|
||||
<view class="verify-modal-mask" v-if="showWithdrawRecordsModal" @click.self="showWithdrawRecordsModal = false">
|
||||
<view class="verify-modal-content withdraw-records-modal">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">提现记录与审批状态</text>
|
||||
<view class="btn-close btn-active" @click="showWithdrawRecordsModal = false">
|
||||
<image class="close-svg" :src="ICONS.close" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="records-scroll-list" v-if="withdrawRecords.length > 0">
|
||||
<view v-for="rec in withdrawRecords" :key="rec.id" class="wd-record-item">
|
||||
<view class="wd-rec-top">
|
||||
<text class="wd-no">{{ rec.withdrawNo }}</text>
|
||||
<text
|
||||
class="wd-status-badge"
|
||||
:class="{
|
||||
pending: rec.status === '待审核',
|
||||
success: rec.status === '已打款',
|
||||
rejected: rec.status === '已驳回'
|
||||
}"
|
||||
>
|
||||
{{ rec.status }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="wd-rec-main">
|
||||
<text class="wd-amount">¥{{ rec.amount }}</text>
|
||||
<text class="wd-channel">{{ rec.channel }}</text>
|
||||
</view>
|
||||
<view class="wd-rec-footer">
|
||||
<text class="wd-time">{{ rec.applyTime }}</text>
|
||||
<text class="wd-reason" v-if="rec.reason">驳回理由:{{ rec.reason }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="empty-records" v-else>
|
||||
<text>暂无提现历史申请记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部切回玩家端 -->
|
||||
<view class="bottom-switch-wrap">
|
||||
<button class="btn-back-player btn-active" @click="switchBackToPlayer">
|
||||
@@ -202,11 +392,22 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
import { isExpertCertified } from '@/common/role.js';
|
||||
import { queryCheckinCode, universalVerifyCheckin, getOrders, getOrdersPool, getExpertStats, claimOrder } from '@/common/api.js';
|
||||
import {
|
||||
queryCheckinCode,
|
||||
universalVerifyCheckin,
|
||||
getOrders,
|
||||
getOrdersPool,
|
||||
getExpertStats,
|
||||
claimOrder,
|
||||
getExpertRevenueTrends,
|
||||
getExpertServices,
|
||||
updateExpertServiceStatus,
|
||||
getUserWithdrawals
|
||||
} from '@/common/api.js';
|
||||
|
||||
const expert = ref({
|
||||
id: 101,
|
||||
@@ -217,6 +418,100 @@ const isReceivingOrders = ref(true);
|
||||
const todayIncome = ref('0.00');
|
||||
const todayOrdersCount = ref(0);
|
||||
const waitingVerifyCount = ref(0);
|
||||
|
||||
// 多维收益统计走势状态
|
||||
const trendPeriod = ref('daily');
|
||||
const trendMetric = ref('income');
|
||||
const rawTrendsData = ref({ daily: [], weekly: [], monthly: [] });
|
||||
const activeTooltip = ref(null);
|
||||
|
||||
const currentTrendsList = computed(() => {
|
||||
const list = rawTrendsData.value[trendPeriod.value] || [];
|
||||
if (list.length > 0) return list;
|
||||
if (trendPeriod.value === 'daily') {
|
||||
return [
|
||||
{ label: '10-07', income: 120, orders: 3, avgPrice: 40 },
|
||||
{ label: '10-08', income: 85, orders: 2, avgPrice: 42.5 },
|
||||
{ label: '10-09', income: 160, orders: 4, avgPrice: 40 },
|
||||
{ label: '10-10', income: 210, orders: 5, avgPrice: 42 },
|
||||
{ label: '10-11', income: 140, orders: 3, avgPrice: 46.7 },
|
||||
{ label: '10-12', income: 290, orders: 6, avgPrice: 48.3 },
|
||||
{ label: '10-13', income: 180, orders: 4, avgPrice: 45 }
|
||||
];
|
||||
} else if (trendPeriod.value === 'weekly') {
|
||||
return [
|
||||
{ label: '3周前', income: 950, orders: 22, avgPrice: 43.2 },
|
||||
{ label: '2周前', income: 1120, orders: 25, avgPrice: 44.8 },
|
||||
{ label: '上周', income: 1350, orders: 28, avgPrice: 48.2 },
|
||||
{ label: '本周', income: 890, orders: 19, avgPrice: 46.8 }
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
{ label: '5月', income: 3200, orders: 75, avgPrice: 42.7 },
|
||||
{ label: '6月', income: 3800, orders: 86, avgPrice: 44.2 },
|
||||
{ label: '7月', income: 4500, orders: 98, avgPrice: 45.9 },
|
||||
{ label: '8月', income: 4200, orders: 90, avgPrice: 46.7 },
|
||||
{ label: '9月', income: 5100, orders: 105, avgPrice: 48.6 },
|
||||
{ label: '10月', income: 2800, orders: 58, avgPrice: 48.3 }
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
const currentPeriodSummary = computed(() => {
|
||||
const list = currentTrendsList.value;
|
||||
const totalInc = list.reduce((sum, item) => sum + (Number(item.income) || 0), 0);
|
||||
const totalOrd = list.reduce((sum, item) => sum + (Number(item.orders) || 0), 0);
|
||||
const avg = totalOrd > 0 ? (totalInc / totalOrd).toFixed(2) : '0.00';
|
||||
return {
|
||||
totalIncome: totalInc.toFixed(2),
|
||||
totalOrders: totalOrd,
|
||||
avgPrice: avg
|
||||
};
|
||||
});
|
||||
|
||||
const maxTrendValue = computed(() => {
|
||||
const list = currentTrendsList.value;
|
||||
const vals = list.map(item => Number(item[trendMetric.value]) || 0);
|
||||
return Math.max(...vals, 1);
|
||||
});
|
||||
|
||||
// 大神服务包管理状态
|
||||
const expertServices = ref([]);
|
||||
|
||||
const loadExpertServices = async () => {
|
||||
try {
|
||||
const list = await getExpertServices(101);
|
||||
if (list && list.length > 0) {
|
||||
expertServices.value = list;
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
const toggleServiceStatus = async (item) => {
|
||||
const newStatus = !item.status;
|
||||
try {
|
||||
await updateExpertServiceStatus(101, item.id, newStatus);
|
||||
item.status = newStatus;
|
||||
uni.showToast({
|
||||
title: newStatus ? `【${item.title}】已上架接单` : `【${item.title}】已暂停下架`,
|
||||
icon: 'none'
|
||||
});
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
// 提现记录弹窗状态
|
||||
const showWithdrawRecordsModal = ref(false);
|
||||
const withdrawRecords = ref([]);
|
||||
|
||||
const openWithdrawRecords = async () => {
|
||||
try {
|
||||
const list = await getUserWithdrawals(1001);
|
||||
withdrawRecords.value = list || [];
|
||||
} catch (_) {}
|
||||
showWithdrawRecordsModal.value = true;
|
||||
};
|
||||
const inputDigits = ref([]);
|
||||
const isCodeError = ref(false);
|
||||
const showConfirmModal = ref(false);
|
||||
@@ -343,6 +638,15 @@ const loadWorkspaceOrders = async () => {
|
||||
activeOrders.value = active;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const trends = await getExpertRevenueTrends(101);
|
||||
if (trends && (trends.daily || trends.weekly || trends.monthly)) {
|
||||
rawTrendsData.value = trends;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
loadExpertServices();
|
||||
} catch (err) {
|
||||
console.warn('[Workspace] 加载服务列表异常:', err);
|
||||
}
|
||||
@@ -627,6 +931,305 @@ const switchBackToPlayer = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/* 收益走势统计图表面板 */
|
||||
.revenue-trends-card {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.trend-tabs {
|
||||
display: flex;
|
||||
background: #0A0F1D;
|
||||
padding: 4rpx;
|
||||
border-radius: 20rpx;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
|
||||
.trend-tab-btn {
|
||||
font-size: 20rpx;
|
||||
color: #94A3B8;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
background: #6366F1;
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2rpx 8rpx rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.period-summary-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 16rpx;
|
||||
padding: 16rpx 12rpx;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.sum-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.sum-label {
|
||||
font-size: 18rpx;
|
||||
color: #94A3B8;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.sum-val {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #F8FAFC;
|
||||
font-family: monospace;
|
||||
|
||||
&.green-val { color: #10B981; }
|
||||
&.gold-val { color: #F59E0B; }
|
||||
}
|
||||
}
|
||||
|
||||
.sum-divider {
|
||||
width: 1px;
|
||||
height: 40rpx;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
.metric-switch-row {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.metric-pill {
|
||||
font-size: 20rpx;
|
||||
color: #94A3B8;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
color: #818CF8;
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
border-color: rgba(99, 102, 241, 0.4);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 220rpx;
|
||||
position: relative;
|
||||
padding: 10rpx 0 0;
|
||||
|
||||
.chart-bars-wrap {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
gap: 8rpx;
|
||||
|
||||
.chart-bar-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
.tooltip-bubble {
|
||||
position: absolute;
|
||||
top: -36rpx;
|
||||
background: #6366F1;
|
||||
color: #FFF;
|
||||
font-size: 18rpx;
|
||||
font-weight: 700;
|
||||
padding: 2rpx 8rpx;
|
||||
border-radius: 6rpx;
|
||||
white-space: nowrap;
|
||||
z-index: 10;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.bar-val-text {
|
||||
font-size: 16rpx;
|
||||
color: #64748B;
|
||||
margin-bottom: 6rpx;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.bar-track {
|
||||
flex: 1;
|
||||
width: 28rpx;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-radius: 14rpx;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
overflow: hidden;
|
||||
|
||||
.bar-fill {
|
||||
width: 100%;
|
||||
border-radius: 14rpx;
|
||||
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&.income {
|
||||
background: linear-gradient(180deg, #10B981 0%, #059669 100%);
|
||||
box-shadow: 0 0 10rpx rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
&.orders {
|
||||
background: linear-gradient(180deg, #6366F1 0%, #4F46E5 100%);
|
||||
box-shadow: 0 0 10rpx rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
&.avgPrice {
|
||||
background: linear-gradient(180deg, #F59E0B 0%, #D97706 100%);
|
||||
box-shadow: 0 0 10rpx rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
font-size: 18rpx;
|
||||
color: #94A3B8;
|
||||
margin-top: 8rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 大神服务包管理卡片 */
|
||||
.services-manage-card {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.service-actions {
|
||||
.btn-wd-records {
|
||||
margin: 0;
|
||||
font-size: 20rpx;
|
||||
color: #6366F1;
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
border-radius: 12rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
line-height: 1.4;
|
||||
|
||||
&::after { display: none; }
|
||||
}
|
||||
}
|
||||
|
||||
.services-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
|
||||
.service-item-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 18rpx;
|
||||
padding: 20rpx;
|
||||
|
||||
.srv-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 16rpx;
|
||||
|
||||
.srv-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
margin-bottom: 6rpx;
|
||||
|
||||
.srv-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #F8FAFC;
|
||||
}
|
||||
.srv-badge {
|
||||
font-size: 18rpx;
|
||||
color: #818CF8;
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
padding: 2rpx 8rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
.srv-mode {
|
||||
font-size: 18rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
}
|
||||
|
||||
.srv-desc {
|
||||
font-size: 20rpx;
|
||||
color: #64748B;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.srv-price-row {
|
||||
.srv-price {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #10B981;
|
||||
font-family: monospace;
|
||||
}
|
||||
.srv-unit {
|
||||
font-size: 20rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.srv-switch-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
.dot {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
border-radius: 50%;
|
||||
background: #64748B;
|
||||
}
|
||||
|
||||
.switch-txt {
|
||||
font-size: 20rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
border-color: rgba(16, 185, 129, 0.35);
|
||||
|
||||
.dot {
|
||||
background: #10B981;
|
||||
box-shadow: 0 0 8rpx #10B981;
|
||||
}
|
||||
.switch-txt {
|
||||
color: #10B981;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.verify-section-card {
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.12) 0%, rgba(21, 29, 48, 1) 100%);
|
||||
border: 1px solid rgba(16, 185, 129, 0.35);
|
||||
@@ -1198,4 +1801,109 @@ const switchBackToPlayer = () => {
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
/* 提现记录弹窗 */
|
||||
.withdraw-records-modal {
|
||||
max-height: 80vh;
|
||||
|
||||
.modal-header {
|
||||
.btn-close {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.close-svg {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.records-scroll-list {
|
||||
max-height: 60vh;
|
||||
|
||||
.wd-record-item {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.wd-rec-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.wd-no {
|
||||
font-size: 20rpx;
|
||||
color: #94A3B8;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.wd-status-badge {
|
||||
font-size: 18rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 6rpx;
|
||||
font-weight: 600;
|
||||
|
||||
&.pending {
|
||||
color: #F59E0B;
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
&.success {
|
||||
color: #10B981;
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
&.rejected {
|
||||
color: #EF4444;
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wd-rec-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.wd-amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #F8FAFC;
|
||||
font-family: monospace;
|
||||
}
|
||||
.wd-channel {
|
||||
font-size: 22rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
}
|
||||
|
||||
.wd-rec-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 18rpx;
|
||||
color: #64748B;
|
||||
|
||||
.wd-reason {
|
||||
color: #EF4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-records {
|
||||
padding: 60rpx 0;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #64748B;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -24,8 +24,14 @@
|
||||
<!-- 订单卡片顶部 -->
|
||||
<view class="order-head">
|
||||
<text class="order-sn">单号:{{ order.orderSn }}</text>
|
||||
<view class="status-pill" :class="order.status">
|
||||
{{ getStatusText(order.status) }}
|
||||
<view class="order-head-right">
|
||||
<view class="countdown-badge" v-if="order.status === 'PENDING_PAY'">
|
||||
<image class="clock-svg" :src="ICONS.clock" mode="aspectFit" />
|
||||
<text class="countdown-txt">{{ getOrderCountdown(order) }}</text>
|
||||
</view>
|
||||
<view class="status-pill" :class="order.status">
|
||||
{{ getStatusText(order.status) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -57,14 +63,16 @@
|
||||
|
||||
<!-- 底部业务操作按钮组 -->
|
||||
<view class="order-actions">
|
||||
<text class="btn-report-link btn-active" @click.stop="openReportModal(order)">举报投诉</text>
|
||||
|
||||
<template v-if="order.status === 'PENDING_PAY'">
|
||||
<button class="action-btn cancel-btn btn-active" @click="cancelOrder(order.id)">取消</button>
|
||||
<button class="action-btn primary-btn btn-active" @click="payOrder(order.id)">立即付款</button>
|
||||
<button class="action-btn primary-btn btn-active" :disabled="getOrderCountdown(order) === '已超时'" @click="payOrder(order.id)">立即付款</button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="order.status === 'IN_SERVICE'">
|
||||
<button class="action-btn chat-btn btn-active" @click="contactExpert(order.expert.nickname)">联系大神</button>
|
||||
<button class="action-btn refund-btn btn-active" @click="applyRefund(order.id)">申请退款</button>
|
||||
<button class="action-btn refund-btn btn-active" @click="openRefundModal(order)">申请退款</button>
|
||||
<button class="action-btn success-btn btn-active" @click="confirmComplete(order.id)">确认验收</button>
|
||||
</template>
|
||||
|
||||
@@ -157,6 +165,164 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退款与售后仲裁申诉工单弹窗 -->
|
||||
<view class="review-modal-mask" v-if="refundModalVisible" @click.self="closeRefundModal">
|
||||
<view class="review-modal-content refund-modal-content">
|
||||
<view class="modal-header">
|
||||
<view class="modal-header-left">
|
||||
<text class="modal-title">申请退款与仲裁</text>
|
||||
<text class="modal-subtitle" v-if="targetRefundOrder">
|
||||
单号:{{ targetRefundOrder.orderSn }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="modal-close" @click="closeRefundModal">
|
||||
<image class="close-icon" :src="ICONS.close" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退款原因单选胶囊 -->
|
||||
<view class="tags-section">
|
||||
<text class="tags-label">选择退款理由</text>
|
||||
<view class="tags-wrap">
|
||||
<view
|
||||
v-for="r in presetRefundReasons"
|
||||
:key="r"
|
||||
class="review-tag"
|
||||
:class="{ active: refundForm.reason === r }"
|
||||
@click="refundForm.reason = r"
|
||||
>
|
||||
{{ r }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 详细说明文本框 -->
|
||||
<view class="textarea-section">
|
||||
<text class="textarea-label">情况详细说明</text>
|
||||
<textarea
|
||||
class="review-textarea"
|
||||
v-model="refundForm.description"
|
||||
placeholder="请客观描述大神违约或未履约事实,平台专员将核实聊天记录与对局记录..."
|
||||
placeholder-class="textarea-placeholder"
|
||||
maxlength="200"
|
||||
/>
|
||||
<text class="char-count">{{ refundForm.description.length }}/200</text>
|
||||
</view>
|
||||
|
||||
<!-- 上传凭证截图 (最多3张) -->
|
||||
<view class="evidence-section">
|
||||
<text class="textarea-label">上传举证截图 (最多3张)</text>
|
||||
<view class="evidence-grid">
|
||||
<view
|
||||
v-for="(img, idx) in refundForm.evidence"
|
||||
:key="idx"
|
||||
class="evidence-item"
|
||||
>
|
||||
<image class="evidence-img" :src="img" mode="aspectFill" @click="previewEvidence(img)" />
|
||||
<view class="evidence-delete" @click.stop="removeRefundEvidence(idx)">
|
||||
<image class="close-icon" :src="ICONS.close" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="evidence-upload-btn btn-active"
|
||||
v-if="refundForm.evidence.length < 3"
|
||||
@click="chooseRefundEvidence"
|
||||
>
|
||||
<image class="upload-icon-svg" :src="ICONS.camera" mode="aspectFit" />
|
||||
<text class="upload-txt">上传凭证</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="modal-actions">
|
||||
<button class="modal-btn cancel-btn btn-active" @click="closeRefundModal">取消</button>
|
||||
<button class="modal-btn submit-btn btn-active danger" :disabled="submittingRefund" @click="submitRefundTicket">
|
||||
{{ submittingRefund ? '正在提交...' : '提交退款申请' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 举报不良行为弹窗 -->
|
||||
<view class="review-modal-mask" v-if="reportModalVisible" @click.self="closeReportModal">
|
||||
<view class="review-modal-content report-modal-content">
|
||||
<view class="modal-header">
|
||||
<view class="modal-header-left">
|
||||
<text class="modal-title">举报不良陪玩</text>
|
||||
<text class="modal-subtitle" v-if="targetReportOrder">
|
||||
举报对象:{{ targetReportOrder.expert ? targetReportOrder.expert.nickname : (targetReportOrder.companionName || '陪玩大神') }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="modal-close" @click="closeReportModal">
|
||||
<image class="close-icon" :src="ICONS.close" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 违规类型单选 -->
|
||||
<view class="tags-section">
|
||||
<text class="tags-label">选择违规类型</text>
|
||||
<view class="tags-wrap">
|
||||
<view
|
||||
v-for="item in presetReportReasons"
|
||||
:key="item"
|
||||
class="review-tag"
|
||||
:class="{ active: reportForm.reason === item }"
|
||||
@click="reportForm.reason = item"
|
||||
>
|
||||
{{ item }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 举报描述 -->
|
||||
<view class="textarea-section">
|
||||
<text class="textarea-label">详细违规线索说明</text>
|
||||
<textarea
|
||||
class="review-textarea"
|
||||
v-model="reportForm.description"
|
||||
placeholder="请填写具体违规线索(如涉黄言论、私下诱导转账、恶意脱离平台等),平台将严肃核处..."
|
||||
placeholder-class="textarea-placeholder"
|
||||
maxlength="200"
|
||||
/>
|
||||
<text class="char-count">{{ reportForm.description.length }}/200</text>
|
||||
</view>
|
||||
|
||||
<!-- 上传凭证截图 -->
|
||||
<view class="evidence-section">
|
||||
<text class="textarea-label">上传违规截图 (最多3张)</text>
|
||||
<view class="evidence-grid">
|
||||
<view
|
||||
v-for="(img, idx) in reportForm.evidence"
|
||||
:key="idx"
|
||||
class="evidence-item"
|
||||
>
|
||||
<image class="evidence-img" :src="img" mode="aspectFill" @click="previewEvidence(img)" />
|
||||
<view class="evidence-delete" @click.stop="removeReportEvidence(idx)">
|
||||
<image class="close-icon" :src="ICONS.close" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="evidence-upload-btn btn-active"
|
||||
v-if="reportForm.evidence.length < 3"
|
||||
@click="chooseReportEvidence"
|
||||
>
|
||||
<image class="upload-icon-svg" :src="ICONS.camera" mode="aspectFit" />
|
||||
<text class="upload-txt">上传截图</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="modal-actions">
|
||||
<button class="modal-btn cancel-btn btn-active" @click="closeReportModal">取消</button>
|
||||
<button class="modal-btn submit-btn btn-active danger" :disabled="submittingReport" @click="submitReportTicket">
|
||||
{{ submittingReport ? '正在提交...' : '确认提交举报' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -169,7 +335,9 @@ import {
|
||||
payOrder as payOrderApi,
|
||||
completeOrder as completeOrderApi,
|
||||
refundOrder as refundOrderApi,
|
||||
submitOrderReview as submitOrderReviewApi
|
||||
submitOrderReview as submitOrderReviewApi,
|
||||
submitReport as submitReportApi,
|
||||
uploadImage
|
||||
} from '@/common/api.js';
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
@@ -177,6 +345,8 @@ import { getNavBarInfo } from '@/common/nav.js';
|
||||
|
||||
const currentThemeKey = ref(getCurrentThemeKey());
|
||||
const navInfo = ref(getNavBarInfo());
|
||||
const nowTick = ref(Date.now());
|
||||
let countdownTimer = null;
|
||||
|
||||
const navStyle = computed(() => {
|
||||
return {
|
||||
@@ -207,6 +377,10 @@ onMounted(() => {
|
||||
currentThemeKey.value = getCurrentThemeKey();
|
||||
uni.$on('theme-change', onThemeChanged);
|
||||
loadLiveOrders();
|
||||
|
||||
countdownTimer = setInterval(() => {
|
||||
nowTick.value = Date.now();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
@@ -218,6 +392,10 @@ onShow(() => {
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.$off('theme-change', onThemeChanged);
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
@@ -311,25 +489,215 @@ const contactExpert = (name) => {
|
||||
});
|
||||
};
|
||||
|
||||
const applyRefund = (id) => {
|
||||
uni.showModal({
|
||||
title: '申请退款/售后仲裁',
|
||||
content: '申请后客服及平台专家将审核对局录像与连麦记录,款项将原路退回。',
|
||||
confirmText: '确认申请',
|
||||
confirmColor: '#EF4444',
|
||||
// 15分钟待支付倒计时动态计算
|
||||
const getOrderCountdown = (order) => {
|
||||
let createdAtTs = null;
|
||||
if (order.createdAt && order.createdAt !== '刚刚') {
|
||||
const parsed = Date.parse(order.createdAt);
|
||||
if (!isNaN(parsed)) createdAtTs = parsed;
|
||||
}
|
||||
if (!createdAtTs) {
|
||||
createdAtTs = Date.now();
|
||||
}
|
||||
const expireTs = createdAtTs + 15 * 60 * 1000;
|
||||
const diffSec = Math.max(0, Math.floor((expireTs - nowTick.value) / 1000));
|
||||
if (diffSec <= 0) {
|
||||
return '已超时';
|
||||
}
|
||||
const m = Math.floor(diffSec / 60);
|
||||
const s = diffSec % 60;
|
||||
return `${m < 10 ? '0' + m : m}:${s < 10 ? '0' + s : s}`;
|
||||
};
|
||||
|
||||
// 退款与售后仲裁申诉工单状态与交互
|
||||
const refundModalVisible = ref(false);
|
||||
const targetRefundOrder = ref(null);
|
||||
const submittingRefund = ref(false);
|
||||
const presetRefundReasons = [
|
||||
'大神未按时履约',
|
||||
'服务态度恶劣/消极',
|
||||
'技术水平严重不符',
|
||||
'擅自提前离场结束',
|
||||
'线下违规行为/争议',
|
||||
'双方友好协商退款'
|
||||
];
|
||||
const refundForm = ref({
|
||||
reason: '大神未按时履约',
|
||||
description: '',
|
||||
evidence: []
|
||||
});
|
||||
|
||||
const openRefundModal = (order) => {
|
||||
targetRefundOrder.value = order;
|
||||
refundForm.value = {
|
||||
reason: '大神未按时履约',
|
||||
description: '',
|
||||
evidence: []
|
||||
};
|
||||
refundModalVisible.value = true;
|
||||
};
|
||||
|
||||
const closeRefundModal = () => {
|
||||
refundModalVisible.value = false;
|
||||
targetRefundOrder.value = null;
|
||||
};
|
||||
|
||||
const chooseRefundEvidence = () => {
|
||||
const remainCount = 3 - refundForm.value.evidence.length;
|
||||
if (remainCount <= 0) return;
|
||||
uni.chooseImage({
|
||||
count: remainCount,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await refundOrderApi(id);
|
||||
} catch (_) {}
|
||||
const o = orders.value.find(item => item.id === id);
|
||||
if (o) o.status = 'REFUNDING';
|
||||
uni.showToast({ title: '退款申请已提交,等待审核', icon: 'none' });
|
||||
const paths = res.tempFilePaths || [];
|
||||
if (paths.length === 0) return;
|
||||
uni.showLoading({ title: '正在上传凭证...' });
|
||||
try {
|
||||
for (const p of paths) {
|
||||
const onlineUrl = await uploadImage(p);
|
||||
if (onlineUrl && refundForm.value.evidence.length < 3) {
|
||||
refundForm.value.evidence.push(onlineUrl);
|
||||
}
|
||||
}
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '凭证上传成功', icon: 'success' });
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: err.message || '上传异常', icon: 'none' });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const removeRefundEvidence = (idx) => {
|
||||
refundForm.value.evidence.splice(idx, 1);
|
||||
};
|
||||
|
||||
const previewEvidence = (url) => {
|
||||
uni.previewImage({
|
||||
urls: [url],
|
||||
current: url
|
||||
});
|
||||
};
|
||||
|
||||
const submitRefundTicket = async () => {
|
||||
if (!targetRefundOrder.value) return;
|
||||
submittingRefund.value = true;
|
||||
uni.showLoading({ title: '正在提交退款申诉...' });
|
||||
try {
|
||||
const oId = targetRefundOrder.value.id;
|
||||
await refundOrderApi(oId, {
|
||||
reason: refundForm.value.reason,
|
||||
description: refundForm.value.description.trim(),
|
||||
evidence: refundForm.value.evidence
|
||||
});
|
||||
const o = orders.value.find(item => item.id === oId);
|
||||
if (o) o.status = 'REFUNDING';
|
||||
uni.hideLoading();
|
||||
closeRefundModal();
|
||||
uni.showToast({ title: '退款申诉已受理,进入争议仲裁流程', icon: 'success' });
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: err.message || '提交失败,请重试', icon: 'none' });
|
||||
} finally {
|
||||
submittingRefund.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 举报不良陪玩工单状态与交互
|
||||
const reportModalVisible = ref(false);
|
||||
const targetReportOrder = ref(null);
|
||||
const submittingReport = ref(false);
|
||||
const presetReportReasons = [
|
||||
'涉黄低俗言论',
|
||||
'私下诱导转账/欺诈',
|
||||
'态度恶劣/人身攻击',
|
||||
'开挂/作弊/战绩造假',
|
||||
'线下违规/安全隐患',
|
||||
'其他违规行为'
|
||||
];
|
||||
const reportForm = ref({
|
||||
reason: '涉黄低俗言论',
|
||||
description: '',
|
||||
evidence: []
|
||||
});
|
||||
|
||||
const openReportModal = (order) => {
|
||||
targetReportOrder.value = order;
|
||||
reportForm.value = {
|
||||
reason: '涉黄低俗言论',
|
||||
description: '',
|
||||
evidence: []
|
||||
};
|
||||
reportModalVisible.value = true;
|
||||
};
|
||||
|
||||
const closeReportModal = () => {
|
||||
reportModalVisible.value = false;
|
||||
targetReportOrder.value = null;
|
||||
};
|
||||
|
||||
const chooseReportEvidence = () => {
|
||||
const remainCount = 3 - reportForm.value.evidence.length;
|
||||
if (remainCount <= 0) return;
|
||||
uni.chooseImage({
|
||||
count: remainCount,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const paths = res.tempFilePaths || [];
|
||||
if (paths.length === 0) return;
|
||||
uni.showLoading({ title: '正在上传违规截图...' });
|
||||
try {
|
||||
for (const p of paths) {
|
||||
const onlineUrl = await uploadImage(p);
|
||||
if (onlineUrl && reportForm.value.evidence.length < 3) {
|
||||
reportForm.value.evidence.push(onlineUrl);
|
||||
}
|
||||
}
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '截图上传成功', icon: 'success' });
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: err.message || '上传异常', icon: 'none' });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const removeReportEvidence = (idx) => {
|
||||
reportForm.value.evidence.splice(idx, 1);
|
||||
};
|
||||
|
||||
const submitReportTicket = async () => {
|
||||
if (!targetReportOrder.value) return;
|
||||
submittingReport.value = true;
|
||||
uni.showLoading({ title: '正在提交举报...' });
|
||||
try {
|
||||
const order = targetReportOrder.value;
|
||||
const expertId = order.expert ? order.expert.id : (order.expertId || 101);
|
||||
const expertName = order.expert ? order.expert.nickname : (order.companionName || '陪玩大神');
|
||||
await submitReportApi({
|
||||
reporterId: 1001,
|
||||
targetType: 'EXPERT',
|
||||
targetId: String(expertId),
|
||||
targetName: expertName,
|
||||
reason: reportForm.value.reason,
|
||||
description: reportForm.value.description.trim(),
|
||||
evidence: reportForm.value.evidence
|
||||
});
|
||||
uni.hideLoading();
|
||||
closeReportModal();
|
||||
uni.showToast({ title: '举报已提交,平台将在24小时内核实处置', icon: 'success' });
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: err.message || '举报提交失败', icon: 'none' });
|
||||
} finally {
|
||||
submittingReport.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmComplete = (id) => {
|
||||
uni.showModal({
|
||||
title: '确认验收',
|
||||
@@ -511,6 +879,34 @@ const submitReview = async () => {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.order-head-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
|
||||
.countdown-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 10rpx;
|
||||
|
||||
.clock-svg {
|
||||
width: 22rpx;
|
||||
height: 22rpx;
|
||||
}
|
||||
|
||||
.countdown-txt {
|
||||
font-size: 20rpx;
|
||||
color: #F59E0B;
|
||||
font-weight: 700;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
<image class="edit-svg" :src="ICONS.settings" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="user-uid" @click.stop="handleAvatarOrNameClick">{{ isLoggedIn ? `UID: ${user.id}` : '登录后享受完整陪伴服务' }}</text>
|
||||
<text class="user-signature-text" v-if="isLoggedIn && user.signature" @click.stop="handleAvatarOrNameClick">{{ user.signature }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -81,7 +81,10 @@
|
||||
<view class="asset-col">
|
||||
<text class="asset-label">分销佣金收益 (元)</text>
|
||||
<text class="asset-val gold-val">{{ isLoggedIn ? `¥${user.commission}` : '--' }}</text>
|
||||
<button class="asset-btn withdraw btn-active" @click="handleWithdraw">提现</button>
|
||||
<view class="asset-btn-group">
|
||||
<button class="asset-btn withdraw btn-active" @click="openWithdrawModal">提现</button>
|
||||
<button class="asset-btn records btn-active" @click="openWithdrawRecordsModal">记录</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -222,6 +225,34 @@
|
||||
placeholder-class="field-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 性别选择 (男 / 女 / 保密 胶囊单选) -->
|
||||
<view class="edit-field">
|
||||
<text class="field-label">用户性别</text>
|
||||
<view class="gender-pill-group">
|
||||
<view
|
||||
v-for="g in ['男', '女', '保密']"
|
||||
:key="g"
|
||||
class="gender-pill btn-active"
|
||||
:class="{ active: editForm.gender === g }"
|
||||
@click="editForm.gender = g"
|
||||
>
|
||||
{{ g }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 个性签名 -->
|
||||
<view class="edit-field">
|
||||
<text class="field-label">个性签名</text>
|
||||
<input
|
||||
class="field-input"
|
||||
v-model="editForm.signature"
|
||||
maxlength="60"
|
||||
placeholder="分享您的常玩段位或搭子宣言(如:常驻朝阳,王者荣耀星耀)"
|
||||
placeholder-class="field-placeholder"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="modal-footer">
|
||||
@@ -231,6 +262,167 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 多渠道提现申请弹窗 -->
|
||||
<view class="profile-edit-modal-mask" v-if="showWithdrawModal" @click="showWithdrawModal = false">
|
||||
<view class="profile-edit-modal withdraw-modal-box" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">佣金收益提现申请</text>
|
||||
<view class="btn-close btn-active" @click="showWithdrawModal = false">
|
||||
<image class="close-svg" :src="ICONS.close" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="modal-body">
|
||||
<!-- 提现渠道切换 -->
|
||||
<view class="channel-tabs-row">
|
||||
<view
|
||||
class="channel-pill btn-active"
|
||||
:class="{ active: withdrawForm.channel === 'WECHAT' }"
|
||||
@click="withdrawForm.channel = 'WECHAT'"
|
||||
>
|
||||
微信零钱
|
||||
</view>
|
||||
<view
|
||||
class="channel-pill btn-active"
|
||||
:class="{ active: withdrawForm.channel === 'ALIPAY' }"
|
||||
@click="withdrawForm.channel = 'ALIPAY'"
|
||||
>
|
||||
支付宝
|
||||
</view>
|
||||
<view
|
||||
class="channel-pill btn-active"
|
||||
:class="{ active: withdrawForm.channel === 'BANK' }"
|
||||
@click="withdrawForm.channel = 'BANK'"
|
||||
>
|
||||
银行卡
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提现金额输入 -->
|
||||
<view class="edit-field">
|
||||
<view class="field-title-row">
|
||||
<text class="field-label">提现金额 (元)</text>
|
||||
<text class="field-tip">可提现 ¥{{ user.commission || '0.00' }}</text>
|
||||
</view>
|
||||
<view class="amount-input-wrap">
|
||||
<text class="currency-symbol">¥</text>
|
||||
<input
|
||||
class="field-input amount-val-input"
|
||||
type="digit"
|
||||
v-model="withdrawForm.amount"
|
||||
:placeholder="`最低 ¥${minWithdrawAmount} 起提`"
|
||||
placeholder-class="field-placeholder"
|
||||
/>
|
||||
<text class="btn-all-in btn-active" @click="withdrawForm.amount = String(user.commission || '0')">全部</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 渠道补充信息 -->
|
||||
<view v-if="withdrawForm.channel === 'WECHAT'" class="channel-hint-card">
|
||||
<text class="hint-text">资金将通过微信企业付款直达当前绑定微信号零钱钱包,预计 1-24 小时内到账。</text>
|
||||
</view>
|
||||
|
||||
<view v-if="withdrawForm.channel === 'ALIPAY'">
|
||||
<view class="edit-field">
|
||||
<text class="field-label">支付宝账号</text>
|
||||
<input
|
||||
class="field-input"
|
||||
v-model="withdrawForm.alipayAccount"
|
||||
placeholder="请输入支付宝登录账号/手机号/邮箱"
|
||||
placeholder-class="field-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<view class="edit-field">
|
||||
<text class="field-label">收款人真实姓名</text>
|
||||
<input
|
||||
class="field-input"
|
||||
v-model="withdrawForm.realName"
|
||||
placeholder="需与支付宝实名认证姓名一致"
|
||||
placeholder-class="field-placeholder"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="withdrawForm.channel === 'BANK'">
|
||||
<view class="edit-field">
|
||||
<text class="field-label">银行卡卡号</text>
|
||||
<input
|
||||
class="field-input"
|
||||
type="number"
|
||||
v-model="withdrawForm.bankCard"
|
||||
placeholder="请输入储蓄卡卡号"
|
||||
placeholder-class="field-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<view class="edit-field">
|
||||
<text class="field-label">开户银行名称</text>
|
||||
<input
|
||||
class="field-input"
|
||||
v-model="withdrawForm.bankName"
|
||||
placeholder="例如:中国工商银行 / 招商银行"
|
||||
placeholder-class="field-placeholder"
|
||||
/>
|
||||
</view>
|
||||
<view class="edit-field">
|
||||
<text class="field-label">持卡人姓名</text>
|
||||
<input
|
||||
class="field-input"
|
||||
v-model="withdrawForm.realName"
|
||||
placeholder="需与银行卡开户姓名一致"
|
||||
placeholder-class="field-placeholder"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="modal-footer">
|
||||
<button class="btn-modal btn-cancel btn-active" @click="showWithdrawModal = false">取消</button>
|
||||
<button class="btn-modal btn-save btn-active" @click="submitWithdraw">确认提现</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提现历史与审批记录跟踪弹窗 -->
|
||||
<view class="profile-edit-modal-mask" v-if="showWithdrawRecordsModal" @click="showWithdrawRecordsModal = false">
|
||||
<view class="profile-edit-modal withdraw-records-modal-box" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">提现审批记录</text>
|
||||
<view class="btn-close btn-active" @click="showWithdrawRecordsModal = false">
|
||||
<image class="close-svg" :src="ICONS.close" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="records-modal-scroll" v-if="userWithdrawalsList.length > 0">
|
||||
<view v-for="rec in userWithdrawalsList" :key="rec.id" class="wd-card-row">
|
||||
<view class="wd-row-top">
|
||||
<text class="wd-no">{{ rec.withdrawNo }}</text>
|
||||
<text
|
||||
class="wd-badge"
|
||||
:class="{
|
||||
pending: rec.status === '待审核',
|
||||
success: rec.status === '已打款',
|
||||
rejected: rec.status === '已驳回'
|
||||
}"
|
||||
>
|
||||
{{ rec.status }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="wd-row-center">
|
||||
<text class="wd-val">¥{{ rec.amount }}</text>
|
||||
<text class="wd-ch">{{ rec.channel }}</text>
|
||||
</view>
|
||||
<view class="wd-row-bot">
|
||||
<text class="wd-time">{{ rec.applyTime }}</text>
|
||||
<text class="wd-reason" v-if="rec.reason">驳回理由:{{ rec.reason }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="empty-records-box" v-else>
|
||||
<text class="empty-txt">暂无提现申请记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部自定义导航栏 (动态自适应发单人4项 / 接单人3项) -->
|
||||
<AppTabBar current="user" />
|
||||
|
||||
@@ -248,7 +440,7 @@ import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, setTheme, THEME_KEYS, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
import { getCurrentRole, setCurrentRole, ROLE_TYPES, toggleRole as toggleUserRole, isExpertCertified, getExpertCertStatus, setExpertCertStatus, CERT_STATUS } from '@/common/role.js';
|
||||
import { getNavBarInfo } from '@/common/nav.js';
|
||||
import { getUserProfile, rechargeBalance, withdrawCommission, getUserFinanceRecords, updateUserProfile, uploadImage, getRechargeTiers, getPlatformConfigs, applyExpertCert } from '@/common/api.js';
|
||||
import { getUserProfile, rechargeBalance, withdrawCommission, getUserFinanceRecords, updateUserProfile, uploadImage, getRechargeTiers, getPlatformConfigs, applyExpertCert, getUserWithdrawals } from '@/common/api.js';
|
||||
|
||||
const DEFAULT_GUEST_AVATAR = 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200';
|
||||
const isLoggedIn = ref(true);
|
||||
@@ -291,7 +483,8 @@ const user = ref({
|
||||
commission: '0.00',
|
||||
orderCount: 0,
|
||||
invitedCount: 0,
|
||||
phone: '138****8888'
|
||||
phone: '138****8888',
|
||||
signature: '同城常驻朝阳 · 寻找靠谱搭子'
|
||||
});
|
||||
const currentRole = ref(getCurrentRole());
|
||||
const currentThemeKey = ref(getCurrentThemeKey());
|
||||
@@ -520,7 +713,9 @@ const PRESET_AVATARS = [
|
||||
const editForm = ref({
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
phone: ''
|
||||
phone: '',
|
||||
gender: '保密',
|
||||
signature: ''
|
||||
});
|
||||
|
||||
const editProfile = () => {
|
||||
@@ -528,7 +723,9 @@ const editProfile = () => {
|
||||
editForm.value = {
|
||||
nickname: user.value.nickname || '',
|
||||
avatar: user.value.avatar || PRESET_AVATARS[0],
|
||||
phone: user.value.phone || ''
|
||||
phone: user.value.phone || '',
|
||||
gender: user.value.gender || '保密',
|
||||
signature: user.value.signature || ''
|
||||
};
|
||||
showEditModal.value = true;
|
||||
};
|
||||
@@ -549,13 +746,13 @@ const onWechatChooseAvatar = async (e) => {
|
||||
uni.showLoading({ title: '正在同步微信头像...' });
|
||||
try {
|
||||
const onlineUrl = await uploadImage(avatarUrl);
|
||||
editForm.value.avatar = onlineUrl || avatarUrl;
|
||||
editForm.value.avatar = onlineUrl;
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '微信头像获取成功', icon: 'success' });
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
editForm.value.avatar = avatarUrl;
|
||||
uni.showToast({ title: '已选取微信头像', icon: 'none' });
|
||||
// 上传失败时保留原头像,切勿将本地临时路径 http://tmp/... 赋值给 avatar,避免触发渲染层 CORS 拦截
|
||||
uni.showToast({ title: err.message || '微信头像上传失败,请重试', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -586,12 +783,24 @@ const saveProfile = async () => {
|
||||
uni.showToast({ title: '昵称不能为空', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const currentAvatar = editForm.value.avatar || '';
|
||||
if (
|
||||
currentAvatar.startsWith('http://tmp/') ||
|
||||
currentAvatar.startsWith('http://usr/') ||
|
||||
currentAvatar.startsWith('wxfile://') ||
|
||||
currentAvatar.startsWith('blob:')
|
||||
) {
|
||||
uni.showToast({ title: '头像正在同步或上传异常,请重新选取', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.showLoading({ title: '正在保存资料...' });
|
||||
try {
|
||||
const updated = await updateUserProfile(user.value.id || 1001, {
|
||||
nickname: editForm.value.nickname.trim(),
|
||||
avatar: editForm.value.avatar,
|
||||
phone: editForm.value.phone
|
||||
phone: editForm.value.phone,
|
||||
gender: editForm.value.gender || '保密',
|
||||
signature: editForm.value.signature ? editForm.value.signature.trim() : ''
|
||||
});
|
||||
uni.hideLoading();
|
||||
if (updated) {
|
||||
@@ -603,6 +812,8 @@ const saveProfile = async () => {
|
||||
user.value.nickname = editForm.value.nickname.trim();
|
||||
user.value.avatar = editForm.value.avatar;
|
||||
user.value.phone = editForm.value.phone;
|
||||
user.value.gender = editForm.value.gender || '保密';
|
||||
user.value.signature = editForm.value.signature ? editForm.value.signature.trim() : '';
|
||||
}
|
||||
showEditModal.value = false;
|
||||
uni.showToast({
|
||||
@@ -706,48 +917,132 @@ const handleRecharge = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleWithdraw = () => {
|
||||
const showWithdrawModal = ref(false);
|
||||
const showWithdrawRecordsModal = ref(false);
|
||||
const userWithdrawalsList = ref([]);
|
||||
const withdrawForm = ref({
|
||||
channel: 'WECHAT',
|
||||
amount: '',
|
||||
alipayAccount: '',
|
||||
bankCard: '',
|
||||
bankName: '',
|
||||
realName: ''
|
||||
});
|
||||
|
||||
const minWithdrawAmount = computed(() => {
|
||||
const finRules = platformConfigs.value.finance_rules || {};
|
||||
return Number(finRules.minWithdrawAmount) || 50;
|
||||
});
|
||||
|
||||
const openWithdrawModal = () => {
|
||||
if (!ensureLoggedIn()) return;
|
||||
const comm = Number(user.value.commission || 0);
|
||||
const finRules = platformConfigs.value.finance_rules || {};
|
||||
const minWithdraw = Number(finRules.minWithdrawAmount) || 50;
|
||||
const withdrawDesc = finRules.withdrawDesc || '提现将自动发起微信企业付款打款到微信零钱。';
|
||||
|
||||
if (comm <= 0) {
|
||||
uni.showToast({ title: '暂无可用提现佣金', icon: 'none' });
|
||||
uni.showToast({ title: '暂无可提现佣金', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (comm < minWithdraw) {
|
||||
uni.showToast({ title: `提现金额须满 ¥${minWithdraw} 元起提`, icon: 'none' });
|
||||
withdrawForm.value = {
|
||||
channel: 'WECHAT',
|
||||
amount: String(comm),
|
||||
alipayAccount: '',
|
||||
bankCard: '',
|
||||
bankName: '',
|
||||
realName: user.value.nickname || ''
|
||||
};
|
||||
showWithdrawModal.value = true;
|
||||
};
|
||||
|
||||
const openWithdrawRecordsModal = async () => {
|
||||
if (!ensureLoggedIn()) return;
|
||||
try {
|
||||
const list = await getUserWithdrawals(user.value.id || 1001);
|
||||
userWithdrawalsList.value = Array.isArray(list) ? list : [];
|
||||
} catch (_) {
|
||||
userWithdrawalsList.value = [];
|
||||
}
|
||||
showWithdrawRecordsModal.value = true;
|
||||
};
|
||||
|
||||
const submitWithdraw = async () => {
|
||||
const comm = Number(user.value.commission || 0);
|
||||
const amount = Number(withdrawForm.value.amount || 0);
|
||||
const minLimit = minWithdrawAmount.value;
|
||||
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
uni.showToast({ title: '请输入有效的提现金额', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (amount < minLimit) {
|
||||
uni.showToast({ title: `提现金额须满 ¥${minLimit} 起提`, icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (amount > comm) {
|
||||
uni.showToast({ title: '提现金额超出可提余额', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (amount > 20000) {
|
||||
uni.showToast({ title: '单笔提现最高 ¥20000', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '佣金提现申请',
|
||||
content: `当前可提现佣金为 ¥${user.value.commission} (最低满¥${minWithdraw}起提)。\n${withdrawDesc}`,
|
||||
confirmText: '确认提现',
|
||||
confirmColor: '#10B981',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const result = await withdrawCommission(user.value.id || 1001, comm);
|
||||
if (result && result.commission !== undefined) {
|
||||
user.value.commission = result.commission;
|
||||
}
|
||||
uni.showToast({
|
||||
title: '提现申请已提交,等待到账',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
} catch (err) {
|
||||
uni.showToast({
|
||||
title: err.message || '提现失败,请稍后重试',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
// 渠道账号校验
|
||||
if (withdrawForm.value.channel === 'ALIPAY') {
|
||||
if (!withdrawForm.value.alipayAccount.trim()) {
|
||||
uni.showToast({ title: '请输入支付宝账号', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (!withdrawForm.value.realName.trim()) {
|
||||
uni.showToast({ title: '请输入真实姓名', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
} else if (withdrawForm.value.channel === 'BANK') {
|
||||
if (!withdrawForm.value.bankCard.trim()) {
|
||||
uni.showToast({ title: '请输入银行卡号', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (!withdrawForm.value.bankName.trim()) {
|
||||
uni.showToast({ title: '请输入开户行全称', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (!withdrawForm.value.realName.trim()) {
|
||||
uni.showToast({ title: '请输入持卡人姓名', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: '正在提交申请...' });
|
||||
const payload = {
|
||||
channel: withdrawForm.value.channel,
|
||||
alipayAccount: withdrawForm.value.alipayAccount.trim(),
|
||||
bankCard: withdrawForm.value.bankCard.trim(),
|
||||
bankName: withdrawForm.value.bankName.trim(),
|
||||
realName: withdrawForm.value.realName.trim()
|
||||
};
|
||||
const result = await withdrawCommission(user.value.id || 1001, amount, payload);
|
||||
uni.hideLoading();
|
||||
if (result && result.commission !== undefined) {
|
||||
user.value.commission = result.commission;
|
||||
} else {
|
||||
user.value.commission = Math.max(0, comm - amount).toFixed(2);
|
||||
}
|
||||
showWithdrawModal.value = false;
|
||||
uni.showToast({
|
||||
title: '提现申请已提交',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
});
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: err.message || '提现申请失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleWithdraw = () => {
|
||||
openWithdrawModal();
|
||||
};
|
||||
|
||||
const showInvitePoster = () => {
|
||||
@@ -1204,9 +1499,15 @@ const navTo = (url) => {
|
||||
}
|
||||
}
|
||||
|
||||
.asset-btn-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.asset-btn {
|
||||
font-size: 20rpx;
|
||||
padding: 0 24rpx;
|
||||
padding: 0 20rpx;
|
||||
height: 48rpx;
|
||||
line-height: 46rpx;
|
||||
border-radius: 24rpx;
|
||||
@@ -1222,6 +1523,11 @@ const navTo = (url) => {
|
||||
color: #F59E0B;
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
&.records {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
color: #818CF8;
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1829,6 +2135,241 @@ const navTo = (url) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
.withdraw-modal-box {
|
||||
max-width: 640rpx;
|
||||
|
||||
.channel-tabs-row {
|
||||
display: flex;
|
||||
background: #0F172A;
|
||||
border-radius: 16rpx;
|
||||
padding: 6rpx;
|
||||
margin-bottom: 24rpx;
|
||||
gap: 8rpx;
|
||||
|
||||
.channel-pill {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 14rpx 0;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #94A3B8;
|
||||
border-radius: 12rpx;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(135deg, #10B981, #059669);
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0 4rpx 12rpx rgba(16, 185, 129, 0.35);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.field-title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.field-tip {
|
||||
font-size: 20rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
}
|
||||
|
||||
.amount-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #0F172A;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16rpx;
|
||||
padding: 0 20rpx;
|
||||
height: 84rpx;
|
||||
|
||||
.currency-symbol {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #10B981;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.amount-val-input {
|
||||
flex: 1;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #F8FAFC;
|
||||
}
|
||||
|
||||
.btn-all-in {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #10B981;
|
||||
padding: 8rpx 16rpx;
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-hint-card {
|
||||
background: rgba(16, 185, 129, 0.08);
|
||||
border: 1px dashed rgba(16, 185, 129, 0.3);
|
||||
border-radius: 14rpx;
|
||||
padding: 16rpx;
|
||||
margin-top: 16rpx;
|
||||
|
||||
.hint-text {
|
||||
font-size: 20rpx;
|
||||
color: #34D399;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.withdraw-records-modal-box {
|
||||
max-width: 660rpx;
|
||||
max-height: 80vh;
|
||||
|
||||
.records-modal-scroll {
|
||||
max-height: 560rpx;
|
||||
padding-right: 8rpx;
|
||||
|
||||
.wd-card-row {
|
||||
background: #0F172A;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 16rpx;
|
||||
padding: 18rpx 20rpx;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.wd-row-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10rpx;
|
||||
|
||||
.wd-no {
|
||||
font-size: 20rpx;
|
||||
color: #64748B;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.wd-badge {
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 600;
|
||||
|
||||
&.pending {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: #F59E0B;
|
||||
}
|
||||
&.success {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: #10B981;
|
||||
}
|
||||
&.rejected {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #EF4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wd-row-center {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.wd-val {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #F8FAFC;
|
||||
}
|
||||
|
||||
.wd-ch {
|
||||
font-size: 22rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
}
|
||||
|
||||
.wd-row-bot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.wd-time {
|
||||
font-size: 18rpx;
|
||||
color: #64748B;
|
||||
}
|
||||
|
||||
.wd-reason {
|
||||
font-size: 18rpx;
|
||||
color: #EF4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-records-box {
|
||||
padding: 60rpx 0;
|
||||
text-align: center;
|
||||
|
||||
.empty-txt {
|
||||
font-size: 24rpx;
|
||||
color: #64748B;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-signature-text {
|
||||
font-size: 22rpx;
|
||||
color: #94A3B8;
|
||||
margin-top: 8rpx;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 480rpx;
|
||||
}
|
||||
|
||||
.gender-pill-group {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-top: 10rpx;
|
||||
|
||||
.gender-pill {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 16rpx 0;
|
||||
font-size: 26rpx;
|
||||
color: #94A3B8;
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 14rpx;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
color: #FFFFFF;
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.3) 0%, rgba(139, 92, 246, 0.4) 100%);
|
||||
border-color: #6366F1;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme-warm {
|
||||
.user-signature-text {
|
||||
color: #64748B;
|
||||
}
|
||||
.gender-pill {
|
||||
background-color: #F1F5F9;
|
||||
border-color: #E2E8F0;
|
||||
color: #475569;
|
||||
|
||||
&.active {
|
||||
color: #FFFFFF;
|
||||
background: linear-gradient(135deg, #F97316 0%, #FB923C 100%);
|
||||
border-color: #F97316;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glowSpin {
|
||||
|
||||
在新工单中引用
屏蔽一个用户