1737 行
46 KiB
Vue
1737 行
46 KiB
Vue
<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>
|