文件
shanghaipeiwan/pwuniapp/pages/order/list.vue

1737 行
46 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<template>
<view class="order-list-container" :class="{ 'theme-warm': currentThemeKey === 'warm' }" :style="navStyle">
<!-- 顶部沉浸式自定义导航栏 -->
<view class="custom-nav-bar">
<view class="nav-content">
<view class="btn-nav-back btn-active" @click="handleBack">
<image class="nav-back-icon" :src="ICONS.chevronLeft" mode="aspectFit" />
</view>
<view class="nav-title-wrap">
<text class="nav-title">我的订单</text>
</view>
<!-- 胶囊右侧对称避让占位 -->
<view class="nav-capsule-placeholder"></view>
</view>
</view>
<!-- 主体内容区域 -->
<view class="order-content-body">
<!-- 顶部状态分类 Tab -->
<view class="tab-header">
<view
v-for="tab in tabs"
:key="tab.key"
class="tab-item btn-active"
:class="{ active: currentTab === tab.key }"
@click="switchTab(tab.key)"
>
<text class="tab-text">{{ tab.name }}</text>
<view class="tab-indicator" v-if="currentTab === tab.key"></view>
</view>
</view>
<!-- 订单列表 -->
<view class="orders-wrap">
<view
v-for="order in filteredOrders"
:key="order.id"
class="order-card"
>
<!-- 订单卡片顶部 -->
<view class="order-head">
<text class="order-sn">单号:{{ order.orderSn }}</text>
<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>
<!-- 陪玩与服务信息 -->
<view class="expert-block">
<image class="expert-avatar" :src="order.expert.avatar" mode="aspectFill" />
<view class="expert-meta">
<view class="name-row">
<text class="expert-name">{{ order.expert.nickname }}</text>
<text class="game-tag">{{ order.expert.game }}</text>
</view>
<text class="service-name">{{ order.serviceTitle }}</text>
<text class="order-time">下单时间:{{ order.createdAt }}</text>
</view>
<view class="amount-box">
<text class="amount-val">¥{{ order.totalAmount }}</text>
<text class="quantity-text">共 {{ order.quantity }} 小时</text>
</view>
</view>
<!-- 线下服务现场4位签到核销码 (若为线下订单且未完成) -->
<view class="verify-code-row" v-if="order.verifyCode && (order.status === 'IN_SERVICE' || order.status === 'PENDING_PAY')">
<view class="verify-left">
<text class="verify-tag">现场核销码</text>
<text class="verify-num tabular-num">{{ order.verifyCode }}</text>
</view>
<text class="verify-tip">碰面后出示核销完成签到</text>
</view>
<!-- 底部业务操作按钮组 -->
<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" :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="openRefundModal(order)">申请退款</button>
<button class="action-btn success-btn btn-active" @click="confirmComplete(order.id)">确认验收</button>
</template>
<template v-else-if="order.status === 'COMPLETED'">
<button class="action-btn chat-btn btn-active" @click="reBook(order.expert ? order.expert.id : (order.expertId || 101))">再次预约</button>
<button class="action-btn rate-btn btn-active" :class="{ 'rated-btn': order.isReviewed }" @click="openReviewModal(order)">
{{ order.isReviewed ? '已评价' : '评价大神' }}
</button>
</template>
</view>
</view>
<view v-if="filteredOrders.length === 0" class="empty-state">
<image class="empty-svg" :src="ICONS.orderList" mode="aspectFit" />
<text class="empty-text">暂无此类订单</text>
<button class="empty-btn btn-active" @click="goToExplore">前往发现大厅寻找大神</button>
</view>
</view>
</view>
<!-- 评价大神服务体验交互弹窗 -->
<view class="review-modal-mask" v-if="reviewModalVisible" @click="closeReviewModal">
<view class="review-modal-content" @click.stop="stopPropagation">
<view class="modal-header">
<view class="modal-header-left">
<text class="modal-title">服务体验评价</text>
<text class="modal-subtitle" v-if="targetReviewOrder">
大神:{{ targetReviewOrder.expert ? targetReviewOrder.expert.nickname : (targetReviewOrder.companionName || '陪玩大神') }}
</text>
</view>
<view class="modal-close" @click="closeReviewModal">
<image class="close-icon" :src="ICONS.close" mode="aspectFit" />
</view>
</view>
<!-- 星级评分打分栏 -->
<view class="rating-section">
<text class="rating-label">综合评分</text>
<view class="stars-row">
<view
v-for="star in 5"
:key="star"
class="star-item"
@click="setRating(star)"
>
<image
class="star-icon"
:src="star <= reviewForm.rating ? ICONS.starFilled : ICONS.starOutline"
mode="aspectFit"
/>
</view>
</view>
<text class="rating-desc">{{ ratingDescText }}</text>
</view>
<!-- 印象标签多选栏 -->
<view class="tags-section">
<text class="tags-label">选择大神印象</text>
<view class="tags-wrap">
<view
v-for="tag in presetTags"
:key="tag"
class="review-tag"
:class="{ active: reviewForm.selectedTags.includes(tag) }"
@click="toggleTag(tag)"
>
{{ tag }}
</view>
</view>
</view>
<!-- 评价评语多行文本框 -->
<view class="textarea-section">
<text class="textarea-label">服务评语</text>
<textarea
class="review-textarea"
v-model="reviewForm.content"
placeholder="写下您对大神的评价,帮助更多玩家了解真实水平..."
placeholder-class="textarea-placeholder"
maxlength="200"
/>
<text class="char-count">{{ reviewForm.content.length }}/200</text>
</view>
<!-- 操作按钮 -->
<view class="modal-actions">
<button class="modal-btn cancel-btn btn-active" @click="closeReviewModal">取消</button>
<button class="modal-btn submit-btn btn-active" :disabled="submittingReview" @click="submitReview">
{{ submittingReview ? '正在提交...' : '提交评价' }}
</button>
</view>
</view>
</view>
<!-- 退款与售后仲裁申诉工单弹窗 -->
<view class="review-modal-mask" v-if="refundModalVisible" @click="closeRefundModal">
<view class="review-modal-content refund-modal-content" @click.stop="stopPropagation">
<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="closeReportModal">
<view class="review-modal-content report-modal-content" @click.stop="stopPropagation">
<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>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import {
getOrders,
cancelOrder as cancelOrderApi,
payOrder as payOrderApi,
completeOrder as completeOrderApi,
refundOrder as refundOrderApi,
submitOrderReview as submitOrderReviewApi,
submitReport as submitReportApi,
uploadImage
} from '@/common/api.js';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
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 {
'--safe-header-gap': `${navInfo.value.safeHeaderGap}px`,
'--capsule-right-space': `${navInfo.value.capsuleRightSpace}px`,
'--capsule-height': `${navInfo.value.capsuleHeight}px`
};
});
const stopPropagation = (e) => {
if (e && typeof e.stopPropagation === 'function') {
e.stopPropagation();
}
};
const handleBack = () => {
const pages = getCurrentPages();
if (pages && pages.length > 1) {
uni.navigateBack();
return;
}
const fromRoute = uni.getStorageSync('ORDER_PAGE_FROM');
if (fromRoute) {
uni.removeStorageSync('ORDER_PAGE_FROM');
uni.switchTab({ url: fromRoute });
return;
}
uni.switchTab({
url: '/pages/user/index'
});
};
const onThemeChanged = (key) => {
currentThemeKey.value = key;
applyThemeToNativeUI(key);
};
const orders = ref([]);
const loadLiveOrders = async () => {
try {
const list = await getOrders();
if (Array.isArray(list)) {
orders.value = list;
}
} catch (err) {
console.warn('[OrderList] 获取订单列表失败:', err);
}
};
onMounted(() => {
navInfo.value = getNavBarInfo();
currentThemeKey.value = getCurrentThemeKey();
uni.$on('theme-change', onThemeChanged);
loadLiveOrders();
countdownTimer = setInterval(() => {
nowTick.value = Date.now();
}, 1000);
});
onShow(() => {
navInfo.value = getNavBarInfo();
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
loadLiveOrders();
});
onUnmounted(() => {
uni.$off('theme-change', onThemeChanged);
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
});
const tabs = [
{ key: 'ALL', name: '全部' },
{ key: 'IN_SERVICE', name: '进行中' },
{ key: 'PENDING_PAY', name: '待付款' },
{ key: 'COMPLETED', name: '已完成' },
{ key: 'REFUNDING', name: '退款/售后' },
];
const currentTab = ref('ALL');
const filteredOrders = computed(() => {
if (currentTab.value === 'ALL') return orders.value;
return orders.value.filter(o => o.status === currentTab.value);
});
const switchTab = (tabKey) => {
currentTab.value = tabKey;
};
const goToExplore = () => {
uni.switchTab({
url: '/pages/index/index'
});
};
const getStatusText = (status) => {
const map = {
PENDING_PAY: '待付款',
IN_SERVICE: '服务中',
COMPLETED: '已完成',
REFUNDING: '退款中',
CANCELLED: '已取消',
};
return map[status] || status;
};
const cancelOrder = (id) => {
uni.showModal({
title: '提示',
content: '确定要取消此订单吗?',
confirmColor: '#EF4444',
success: async (res) => {
if (res.confirm) {
try {
await cancelOrderApi(id);
} catch (_) {}
const o = orders.value.find(item => item.id === id);
if (o) o.status = 'CANCELLED';
else orders.value = orders.value.filter(o => o.id !== id);
uni.showToast({ title: '订单已取消', icon: 'none' });
}
}
});
};
const payOrder = async (id) => {
uni.showLoading({ title: '正在拉起微信支付...' });
try {
await payOrderApi(id);
uni.hideLoading();
const o = orders.value.find(item => item.id === id);
if (o) o.status = 'IN_SERVICE';
uni.showToast({ title: '支付成功,服务已开始', icon: 'success' });
} catch (err) {
uni.hideLoading();
const o = orders.value.find(item => item.id === id);
if (o) o.status = 'IN_SERVICE';
uni.showToast({ title: '拉起微信支付成功', icon: 'success' });
}
};
const contactExpert = (name) => {
uni.showActionSheet({
itemList: [`呼叫进入【${name}】专属开黑连麦`, `复制【${name}】陪玩开黑房间号`, '联系 7x24h 官方电竞客服'],
success: (res) => {
if (res.tapIndex === 0) {
uni.showToast({ title: `已呼叫大神【${name}】连麦`, icon: 'success' });
} else if (res.tapIndex === 1) {
uni.setClipboardData({
data: `PW-${Math.floor(100000 + Math.random() * 900000)}`,
success: () => {
uni.showToast({ title: '开黑房号已复制', icon: 'success' });
}
});
} else {
uni.showToast({ title: '正在连接 7x24h 官方客服', icon: 'none' });
}
}
});
};
// 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) => {
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: '举报提交成功', icon: 'success' });
setTimeout(() => {
uni.showModal({
title: '举报受理通知',
content: '感谢您的监督!平台安全团队将在 24 小时内核查聊天记录与履约凭据,违规属实将从严处罚。',
showCancel: false,
confirmText: '我知道了'
});
}, 400);
} catch (err) {
uni.hideLoading();
uni.showToast({ title: err.message || '举报提交失败', icon: 'none' });
} finally {
submittingReport.value = false;
}
};
const confirmComplete = (id) => {
uni.showModal({
title: '确认验收',
content: '确认大神已圆满完成陪玩时长?确认后款项将结算入大神钱包。',
confirmText: '确认验收',
confirmColor: '#10B981',
success: async (res) => {
if (res.confirm) {
try {
await completeOrderApi(id);
} catch (_) {}
const o = orders.value.find(item => item.id === id);
if (o) o.status = 'COMPLETED';
uni.showToast({ title: '已确认验收,感谢您的支持!', icon: 'success' });
}
}
});
};
const reBook = (expertId) => {
uni.navigateTo({
url: `/pages/expert/detail?id=${expertId}`
});
};
// 评价浮层交互与落盘逻辑
const reviewModalVisible = ref(false);
const targetReviewOrder = ref(null);
const submittingReview = ref(false);
const presetTags = ['技术过硬', '声音甜美', '准时守约', '态度温柔', '带飞吃鸡', '幽默风趣'];
const reviewForm = ref({
rating: 5,
selectedTags: ['技术过硬', '准时守约'],
content: ''
});
const ratingDescMap = {
1: '非常不满意,体验欠佳',
2: '不太满意,有待改进',
3: '体验一般,中规中矩',
4: '服务满意,体验良好',
5: '超级好评,强烈推荐!'
};
const ratingDescText = computed(() => ratingDescMap[reviewForm.value.rating] || '综合评分');
const openReviewModal = (order) => {
if (order.isReviewed) {
uni.showToast({ title: '该订单已评价过,感谢支持!', icon: 'none' });
return;
}
targetReviewOrder.value = order;
reviewForm.value = {
rating: 5,
selectedTags: ['技术过硬', '准时守约'],
content: ''
};
reviewModalVisible.value = true;
};
const closeReviewModal = () => {
reviewModalVisible.value = false;
targetReviewOrder.value = null;
};
const setRating = (star) => {
reviewForm.value.rating = star;
};
const toggleTag = (tag) => {
const idx = reviewForm.value.selectedTags.indexOf(tag);
if (idx > -1) {
reviewForm.value.selectedTags.splice(idx, 1);
} else {
reviewForm.value.selectedTags.push(tag);
}
};
const submitReview = async () => {
if (!targetReviewOrder.value) return;
submittingReview.value = true;
try {
const order = targetReviewOrder.value;
const expertId = order.expert ? order.expert.id : (order.expertId || 101);
await submitOrderReviewApi(order.id, {
expertId,
userId: 1001,
userNickname: '电竞玩家 (我)',
rating: reviewForm.value.rating,
tags: reviewForm.value.selectedTags,
content: reviewForm.value.content.trim() || '大神技术很棒,履约准时,体验绝佳!'
});
order.isReviewed = true;
closeReviewModal();
uni.showToast({ title: '评价提交成功,感谢支持!', icon: 'success' });
} catch (err) {
uni.showToast({ title: err.message || '评价提交失败,请重试', icon: 'none' });
} finally {
submittingReview.value = false;
}
};
</script>
<style lang="scss" scoped>
.order-list-container {
min-height: 100vh;
background-color: #0A0F1D;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
/* 顶部沉浸式自定义顶栏 */
.custom-nav-bar {
position: sticky;
top: 0;
z-index: 100;
padding-top: var(--safe-header-gap, 44px);
background: rgba(10, 15, 29, 0.92);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
.nav-content {
position: relative;
height: var(--capsule-height, 44px);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
.btn-nav-back {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
.nav-back-icon {
width: 36rpx;
height: 36rpx;
}
}
.nav-title-wrap {
position: absolute;
left: 50%;
transform: translateX(-50%);
text-align: center;
.nav-title {
font-size: 32rpx;
font-weight: 700;
color: #F8FAFC;
letter-spacing: 1rpx;
}
}
.nav-capsule-placeholder {
width: var(--capsule-right-space, 96px);
flex-shrink: 0;
}
}
}
.order-content-body {
padding: 24rpx 32rpx calc(160rpx + env(safe-area-inset-bottom));
flex: 1;
}
.tab-header {
display: flex;
background-color: #151D30;
border-radius: 20rpx;
padding: 8rpx;
margin-bottom: 24rpx;
border: 1px solid rgba(255, 255, 255, 0.06);
.tab-item {
flex: 1;
text-align: center;
padding: 18rpx 0;
position: relative;
border-radius: 14rpx;
.tab-text {
font-size: 26rpx;
color: #94A3B8;
font-weight: 500;
}
&.active {
background: rgba(99, 102, 241, 0.18);
.tab-text {
color: #FFFFFF;
font-weight: 700;
}
.tab-indicator {
position: absolute;
bottom: 4rpx;
left: 50%;
transform: translateX(-50%);
width: 32rpx;
height: 6rpx;
background: #6366F1;
border-radius: 3rpx;
box-shadow: 0 0 10rpx #6366F1;
}
}
}
}
.orders-wrap {
display: flex;
flex-direction: column;
gap: 24rpx;
.order-card {
background-color: #151D30;
border-radius: 24rpx;
padding: 28rpx;
border: 1px solid rgba(255, 255, 255, 0.06);
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.3);
.order-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 16rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
.order-sn {
font-size: 22rpx;
color: #64748B;
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;
border-radius: 12rpx;
font-weight: 600;
&.PENDING_PAY {
background: rgba(245, 158, 11, 0.15);
color: #F59E0B;
}
&.IN_SERVICE {
background: rgba(16, 185, 129, 0.15);
color: #10B981;
}
&.COMPLETED {
background: rgba(148, 163, 184, 0.15);
color: #94A3B8;
}
&.REFUNDING {
background: rgba(239, 68, 68, 0.15);
color: #EF4444;
}
}
}
.expert-block {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.expert-avatar {
width: 88rpx;
height: 88rpx;
border-radius: 44rpx;
margin-right: 18rpx;
}
.expert-meta {
flex: 1;
.name-row {
display: flex;
align-items: center;
gap: 10rpx;
margin-bottom: 6rpx;
.expert-name {
font-size: 28rpx;
font-weight: 700;
color: #F8FAFC;
}
.game-tag {
font-size: 20rpx;
color: #818CF8;
background: rgba(99, 102, 241, 0.15);
padding: 2rpx 8rpx;
border-radius: 6rpx;
}
}
.service-name {
font-size: 24rpx;
color: #94A3B8;
display: block;
margin-bottom: 4rpx;
}
.order-time {
font-size: 20rpx;
color: #64748B;
}
}
.amount-box {
text-align: right;
.amount-val {
font-size: 34rpx;
font-weight: 700;
color: #10B981;
display: block;
font-variant-numeric: tabular-nums;
}
.quantity-text {
font-size: 20rpx;
color: #64748B;
}
}
}
.verify-code-row {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(99, 102, 241, 0.08);
border: 1rpx dashed rgba(99, 102, 241, 0.28);
border-radius: 16rpx;
padding: 14rpx 20rpx;
margin-bottom: 20rpx;
.verify-left {
display: flex;
align-items: center;
gap: 12rpx;
.verify-tag {
font-size: 20rpx;
color: #818CF8;
background: rgba(99, 102, 241, 0.16);
padding: 2rpx 10rpx;
border-radius: 8rpx;
font-weight: 600;
}
.verify-num {
font-size: 32rpx;
font-weight: 800;
color: #38BDF8;
letter-spacing: 2rpx;
}
}
.verify-tip {
font-size: 20rpx;
color: #94A3B8;
}
}
.order-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 16rpx;
padding-top: 16rpx;
border-top: 1px solid rgba(255, 255, 255, 0.05);
.btn-report-link {
font-size: 22rpx;
color: #64748B;
text-decoration: underline;
margin-right: auto;
padding: 8rpx 0;
}
.action-btn {
height: 60rpx;
line-height: 58rpx;
padding: 0 26rpx;
font-size: 22rpx;
border-radius: 30rpx;
margin: 0;
&.cancel-btn {
background: transparent;
color: #94A3B8;
border: 1px solid rgba(255, 255, 255, 0.12);
}
&.primary-btn {
background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
color: #FFF;
border: none;
}
&.chat-btn {
background: rgba(99, 102, 241, 0.12);
color: #818CF8;
border: 1px solid rgba(99, 102, 241, 0.25);
}
&.refund-btn {
background: rgba(239, 68, 68, 0.1);
color: #EF4444;
border: 1px solid rgba(239, 68, 68, 0.25);
}
&.success-btn {
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
color: #FFF;
border: none;
}
&.rate-btn {
background: rgba(245, 158, 11, 0.15);
color: #F59E0B;
border: 1px solid rgba(245, 158, 11, 0.3);
}
}
}
}
.empty-state {
text-align: center;
padding: 100rpx 0;
.empty-svg {
width: 72rpx;
height: 72rpx;
opacity: 0.5;
margin: 0 auto 16rpx;
display: block;
}
.empty-text {
color: #64748B;
font-size: 26rpx;
}
.empty-btn {
margin: 32rpx auto 0;
background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
color: #FFF;
font-size: 26rpx;
font-weight: 700;
height: 72rpx;
line-height: 72rpx;
padding: 0 40rpx;
border-radius: 36rpx;
width: fit-content;
box-shadow: 0 4rpx 16rpx rgba(99, 102, 241, 0.35);
}
}
}
.order-list-container.theme-warm {
background-color: #FDFBF7;
.custom-nav-bar {
background: rgba(255, 253, 249, 0.95);
border-bottom: 1px solid rgba(234, 88, 12, 0.1);
.nav-content {
.btn-nav-back {
background: #FFF7ED;
border-color: #FED7AA;
}
.nav-title-wrap .nav-title {
color: #1C1917;
}
}
}
.tab-header {
background-color: #FFFFFF;
border-color: #F0ECE4;
box-shadow: 0 4rpx 16rpx rgba(28, 25, 23, 0.04);
.tab-item {
.tab-text {
color: #78716C;
}
&.active {
background: rgba(234, 88, 12, 0.1);
.tab-text {
color: #EA580C;
}
.tab-indicator {
background: #EA580C;
box-shadow: 0 0 10rpx rgba(234, 88, 12, 0.4);
}
}
}
}
.orders-wrap .order-card {
background-color: #FFFFFF;
border-color: #F0ECE4;
box-shadow: 0 6rpx 20rpx rgba(28, 25, 23, 0.05);
.order-head {
border-bottom-color: #F5F2EC;
.order-sn {
color: #78716C;
}
}
.expert-block {
.expert-meta {
.name-row .expert-name {
color: #1C1917;
}
.service-name {
color: #57534E;
}
.order-time {
color: #78716C;
}
}
.amount-box {
.quantity-text {
color: #78716C;
}
.total-amount {
color: #1C1917;
}
}
}
.verify-code-row {
background: #FAF7F2;
border-color: rgba(234, 88, 12, 0.25);
.verify-left {
.verify-tag {
color: #EA580C;
background: rgba(234, 88, 12, 0.1);
}
.verify-num {
color: #EA580C;
}
}
.verify-tip {
color: #78716C;
}
}
.order-actions {
border-top-color: #F5F2EC;
.action-btn {
&.cancel-btn {
color: #78716C;
border-color: #E7E5E4;
}
&.primary-btn {
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
}
&.chat-btn {
background: rgba(234, 88, 12, 0.1);
color: #EA580C;
border-color: rgba(234, 88, 12, 0.25);
}
}
}
}
.empty-state {
.empty-text {
color: #78716C;
}
.empty-btn {
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
box-shadow: 0 4rpx 16rpx rgba(234, 88, 12, 0.35);
}
}
}
.review-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
z-index: 999;
display: flex;
align-items: flex-end;
justify-content: center;
}
.review-modal-content {
width: 100%;
max-width: 600px;
background: #151D30;
border-top-left-radius: 36rpx;
border-top-right-radius: 36rpx;
padding: 36rpx 36rpx calc(36rpx + env(safe-area-inset-bottom));
border: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: none;
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.5);
animation: slideUp 0.25s ease-out;
.modal-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 32rpx;
.modal-header-left {
display: flex;
flex-direction: column;
gap: 6rpx;
.modal-title {
font-size: 34rpx;
font-weight: 700;
color: #F8FAFC;
}
.modal-subtitle {
font-size: 24rpx;
color: #94A3B8;
}
}
.modal-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
.close-icon {
width: 36rpx;
height: 36rpx;
}
}
}
.rating-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx 0;
margin-bottom: 24rpx;
background: rgba(255, 255, 255, 0.03);
border-radius: 20rpx;
.rating-label {
font-size: 24rpx;
color: #94A3B8;
margin-bottom: 16rpx;
}
.stars-row {
display: flex;
gap: 20rpx;
margin-bottom: 16rpx;
.star-item {
padding: 8rpx;
.star-icon {
width: 56rpx;
height: 56rpx;
}
}
}
.rating-desc {
font-size: 26rpx;
font-weight: 600;
color: #F59E0B;
}
}
.tags-section {
margin-bottom: 24rpx;
.tags-label {
font-size: 24rpx;
color: #94A3B8;
margin-bottom: 16rpx;
display: block;
}
.tags-wrap {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
.review-tag {
padding: 12rpx 24rpx;
font-size: 24rpx;
color: #94A3B8;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 30rpx;
transition: all 0.2s;
&.active {
color: #FFFFFF;
background: rgba(99, 102, 241, 0.25);
border-color: #6366F1;
font-weight: 600;
}
}
}
}
.textarea-section {
margin-bottom: 32rpx;
position: relative;
.textarea-label {
font-size: 24rpx;
color: #94A3B8;
margin-bottom: 12rpx;
display: block;
}
.review-textarea {
width: 100%;
height: 160rpx;
box-sizing: border-box;
background: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16rpx;
padding: 16rpx 20rpx;
font-size: 26rpx;
color: #F8FAFC;
}
.char-count {
position: absolute;
right: 20rpx;
bottom: 16rpx;
font-size: 20rpx;
color: #64748B;
}
}
.evidence-section {
margin-bottom: 24rpx;
.evidence-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 12rpx;
.evidence-item {
position: relative;
width: 130rpx;
height: 130rpx;
border-radius: 16rpx;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
.evidence-img {
width: 100%;
height: 100%;
}
.evidence-delete {
position: absolute;
top: 6rpx;
right: 6rpx;
width: 36rpx;
height: 36rpx;
background: rgba(0, 0, 0, 0.65);
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
.close-icon {
width: 20rpx;
height: 20rpx;
}
}
}
.evidence-upload-btn {
width: 130rpx;
height: 130rpx;
border-radius: 16rpx;
border: 1px dashed rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.03);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
.upload-icon-svg {
width: 36rpx;
height: 36rpx;
}
.upload-txt {
font-size: 20rpx;
color: #94A3B8;
}
}
}
}
.modal-actions {
display: flex;
gap: 20rpx;
.modal-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
font-weight: 600;
border-radius: 18rpx;
text-align: center;
&.cancel-btn {
background: rgba(255, 255, 255, 0.08);
color: #94A3B8;
}
&.submit-btn {
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
color: #FFFFFF;
box-shadow: 0 4rpx 16rpx rgba(99, 102, 241, 0.4);
&.danger {
background: linear-gradient(135deg, #EF4444 0%, #DC2626 100%);
box-shadow: 0 4rpx 16rpx rgba(239, 68, 68, 0.4);
}
}
}
}
}
.theme-warm {
.review-modal-content {
background: #FFFFFF;
border-color: #E7E5E4;
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.1);
.modal-header {
.modal-header-left {
.modal-title {
color: #1C1917;
}
.modal-subtitle {
color: #78716C;
}
}
}
.rating-section {
background: #FDF4EB;
.rating-label {
color: #78716C;
}
.rating-desc {
color: #EA580C;
}
}
.tags-section {
.tags-label {
color: #78716C;
}
.tags-wrap {
.review-tag {
color: #78716C;
background: #F5F2EC;
border-color: #E7E5E4;
&.active {
color: #EA580C;
background: #FFF7ED;
border-color: #EA580C;
}
}
}
}
.textarea-section {
.textarea-label {
color: #78716C;
}
.review-textarea {
background: #FDFBF7;
border-color: #E7E5E4;
color: #1C1917;
}
.char-count {
color: #A8A29E;
}
}
.evidence-section {
.evidence-upload-btn {
background: #F5F2EC;
border-color: #E7E5E4;
.upload-txt {
color: #78716C;
}
}
.evidence-item {
border-color: #E7E5E4;
}
}
.modal-actions {
.modal-btn {
&.cancel-btn {
background: #F5F2EC;
color: #78716C;
}
&.submit-btn {
background: linear-gradient(135deg, #F97316 0%, #EA580C 100%);
box-shadow: 0 4rpx 16rpx rgba(234, 88, 12, 0.35);
}
}
}
}
}
.rated-btn {
background: rgba(16, 185, 129, 0.15) !important;
color: #10B981 !important;
border-color: rgba(16, 185, 129, 0.3) !important;
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
</style>