文件
shanghaipeiwan/pwuniapp/pages/expert/hall.vue
xcbyy 5538d8ce7e fix(miniapp): 接单大厅分类数量取真实数据,抢单失败不再伪造订单
- 分类 chip 的 12/4/3 写死数量改为按真实订单统计

- 雷达页抢单失败时删除「自动落盘创建订单」兜底(原逻辑会伪造一笔 buyerId=2001 的订单并提示成功),改为如实提示已被抢/状态不可接/未认证

- 抢单使用当前登录身份(userId 由服务端 token 覆盖),成功后刷新列表

- 110 套件/730 用例全绿
2026-10-21 14:55:01 +08:00

1479 行
42 KiB
Vue

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

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

<template>
<view class="hall-container" :class="{ 'theme-warm': currentThemeKey === 'warm' }" :style="navStyle">
<!-- 顶部接单状态栏与打烊开关 -->
<view class="hall-header-card">
<view class="expert-info-left">
<image class="avatar-img" :src="expert.avatar" mode="aspectFill" @error="onAvatarError" />
<view class="text-meta">
<view class="name-row">
<text class="expert-name">{{ expert.nickname }}</text>
<text class="pro-badge" :class="{ offline: !isOnline }">{{ isOnline ? '接单中' : '已打烊' }}</text>
</view>
<text class="rating-sub">评分 {{ expert.rating || '5.0' }} · 接单率 {{ expert.orderRate || '99.2%' }}</text>
</view>
</view>
<view class="status-toggle btn-active" @click="toggleOnlineStatus">
<view class="status-dot" :class="{ active: isOnline }"></view>
<text class="toggle-text">{{ isOnline ? '在线接单' : '打烊休息' }}</text>
</view>
</view>
<!-- 今日派单核心数据卡 -->
<view class="hall-stats-card">
<view class="stat-col">
<text class="stat-num text-green">¥{{ todayStats.income }}</text>
<text class="stat-label">今日已入账</text>
</view>
<view class="stat-divider"></view>
<view class="stat-col">
<text class="stat-num">{{ todayStats.orders }}</text>
<text class="stat-label">已履约单数</text>
</view>
<view class="stat-divider"></view>
<view class="stat-col">
<text class="stat-num text-orange">{{ todayStats.waitingVerify }}</text>
<text class="stat-label">待核销门票</text>
</view>
</view>
<!-- 订单品类筛选器 (横向单行防折行胶囊滑栏) -->
<view class="category-tabs-wrap">
<scroll-view class="category-scroll" scroll-x :show-scrollbar="false">
<view class="tabs-list">
<view
v-for="cat in categories"
:key="cat.key"
class="cat-chip btn-active"
:class="{ active: activeCategory === cat.key }"
@click="activeCategory = cat.key"
>
<image v-if="cat.icon" class="chip-icon-svg" :src="cat.icon" mode="aspectFit" />
<text class="chip-text">{{ cat.name }}</text>
<view class="chip-count" v-if="cat.count">{{ cat.count }}</view>
</view>
</view>
</scroll-view>
</view>
<!-- 派单池订单列表 -->
<view class="dispatch-list">
<view v-if="filteredOrders.length === 0" class="hall-empty">
<text class="hall-empty-title">暂无待接需求单</text>
<text class="hall-empty-desc">开启定位并稍后刷新,附近新单会优先推给你</text>
</view>
<view
v-for="order in filteredOrders"
:key="order.id"
class="dispatch-card card-active"
@click="openOrderDetailModal(order)"
>
<view class="card-top-row">
<view class="tag-group">
<text class="category-tag" :class="order.typeClass">{{ order.category }}</text>
<text class="pay-method-tag">{{ order.payMethod }}</text>
</view>
<view class="reward-box">
<text class="reward-val">¥{{ order.reward }}</text>
</view>
</view>
<text class="order-title">{{ order.title }}</text>
<view class="info-grid">
<view class="info-row">
<image class="row-icon" :src="ICONS.mapPin" mode="aspectFit" />
<text class="row-text">{{ order.location }} (距您 {{ order.distance }})</text>
</view>
<view class="info-row">
<image class="row-icon" :src="ICONS.bolt" mode="aspectFit" />
<text class="row-text">预约时段:{{ order.timeSlot }}</text>
</view>
</view>
<view class="remark-box">
<text class="remark-text">需求备注:{{ order.requirement }}</text>
</view>
<!-- 卡片底部操作栏 -->
<view class="card-action-bar">
<view class="user-inline">
<image class="user-avatar" :src="order.userAvatar" mode="aspectFill" />
<text class="user-name">{{ order.userName }}</text>
</view>
<button class="btn-grab-order btn-active" @click.stop="handleGrabOrder(order)">
立即接单
</button>
</view>
</view>
</view>
<!-- 派单需求全貌看板弹窗 (方案1) -->
<view class="dispatch-detail-mask" v-if="showDetailModal" @click="closeOrderDetailModal">
<view class="dispatch-detail-modal" @click.stop="stopPropagation">
<!-- 弹窗顶栏 -->
<view class="modal-header">
<view class="header-title-box">
<text class="modal-title">派单需求详情</text>
<text class="order-no-sub" v-if="selectedOrder" @click.stop="copyOrderNo">NO.{{ formatShortOrderNo(selectedOrder.id) }}</text>
</view>
<view class="btn-close-modal btn-active" @click="closeOrderDetailModal">
<image class="close-icon-svg" :src="ICONS.close" mode="aspectFit" />
</view>
</view>
<!-- 弹窗滚动主体 -->
<scroll-view scroll-y class="modal-body-scroll" v-if="selectedOrder">
<!-- 核心酬金与品类看板 -->
<view class="detail-reward-card">
<view class="reward-left">
<view class="detail-tag-row">
<text class="category-tag" :class="selectedOrder.typeClass">{{ selectedOrder.category }}</text>
<text class="pay-method-tag">{{ selectedOrder.payMethod }}</text>
</view>
<text class="detail-title">{{ selectedOrder.title }}</text>
</view>
<view class="reward-badge-card">
<text class="badge-label">预计到账</text>
<view class="amount-row">
<text class="curr-mark">¥</text>
<text class="reward-num">{{ selectedOrder.reward }}</text>
</view>
</view>
</view>
<!-- 需求与时段核心参数表 -->
<view class="detail-params-card">
<view class="param-row">
<view class="param-lbl">
<image class="param-icon" :src="ICONS.bolt" mode="aspectFit" />
<text class="lbl-txt">预约时段</text>
</view>
<text class="param-val highlight">{{ selectedOrder.timeSlot }}</text>
</view>
<view class="param-row">
<view class="param-lbl">
<image class="param-icon" :src="ICONS.mapPin" mode="aspectFit" />
<text class="lbl-txt">服务地点</text>
</view>
<view class="param-val-col">
<text class="param-val">{{ selectedOrder.location }}</text>
<text class="distance-sub">距离您当前约 {{ selectedOrder.distance }}</text>
</view>
</view>
<view class="param-row">
<view class="param-lbl">
<image class="param-icon" :src="ICONS.clock" mode="aspectFit" />
<text class="lbl-txt">服务时长</text>
</view>
<text class="param-val">{{ selectedOrder.durationHours || 2 }} 小时</text>
</view>
<view class="param-row">
<view class="param-lbl">
<image class="param-icon" :src="ICONS.wallet" mode="aspectFit" />
<text class="lbl-txt">结算方式</text>
</view>
<text class="param-val">{{ selectedOrder.payMethod }} · 平台资金托管</text>
</view>
</view>
<!-- 客户详细需求备注 -->
<view class="detail-remark-card">
<view class="remark-head">
<image class="remark-icon" :src="ICONS.messageSquare" mode="aspectFit" />
<text class="remark-title">客户需求备注说明</text>
</view>
<view class="remark-content">
<text class="remark-text">{{ selectedOrder.requirement || '按约定准时到场,文明健康社交。' }}</text>
</view>
</view>
<!-- 发单客户老板信息 -->
<view class="detail-client-card">
<image class="client-avatar" :src="selectedOrder.userAvatar" mode="aspectFill" />
<view class="client-info">
<view class="client-name-row">
<text class="client-name">{{ selectedOrder.userName }}</text>
<text class="client-verified-tag">已实名认证买家</text>
</view>
<text class="client-trust-sub">信誉积分 100 · 资金全额托管</text>
</view>
</view>
<!-- 履约保障指引 -->
<view class="detail-safe-notice">
<image class="safe-icon" :src="ICONS.shield" mode="aspectFit" />
<text class="safe-desc">现场碰面后请出示或核验双方 4 位数字核销码进行准时签到。服务全程遵循平台文明社交规范,如遇纠纷平台提供 7x24 小时资金托管与仲裁保障。</text>
</view>
</scroll-view>
<!-- 弹窗底部操作条 -->
<view class="modal-footer">
<button class="btn-modal-cancel btn-active" @click="closeOrderDetailModal">稍后考虑</button>
<button class="btn-modal-grab btn-active" @click="handleGrabOrderFromModal">立即接此单</button>
</view>
</view>
</view>
<!-- 底部自定义导航栏 (接单人端:接单大厅 / 我要接单[5km雷达] / 个人中心) -->
<AppTabBar current="hall" />
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import AppTabBar from '@/components/CustomTabBar.vue';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
import { isExpertCertified } from '@/common/role.js';
import { claimOrder, getOrdersPool, getExpertStats, updateCompanionProfile, getUserProfile } from '@/common/api.js';
import { getUserLocation } from '@/common/location.js';
import { getNavBarInfo } from '@/common/nav.js';
const currentThemeKey = ref(getCurrentThemeKey());
const DEFAULT_EXPERT_AVATAR = 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300';
const getInitialExpert = () => {
let cached = null;
try {
cached = uni.getStorageSync('PW_USER_INFO') || uni.getStorageSync('userInfo');
} catch (_) {}
let uid = (cached && cached.id) || uni.getStorageSync('userId') || 2006;
if (uid == 101) uid = 2006;
return {
id: uid,
nickname: (cached && cached.nickname) || '接单大神',
avatar: (cached && cached.avatar) || DEFAULT_EXPERT_AVATAR,
rating: (cached && cached.rating) || '5.0',
orderRate: (cached && cached.orderRate) || '99.2%'
};
};
const expert = ref(getInitialExpert());
const isOnline = ref(uni.getStorageSync('EXPERT_RECEIVING_STATUS') !== '0');
const activeCategory = ref('all');
const navInfo = ref(getNavBarInfo());
const onAvatarError = () => {
expert.value.avatar = DEFAULT_EXPERT_AVATAR;
};
const navStyle = computed(() => {
return {
'--safe-header-gap': `${navInfo.value.safeHeaderGap}px`,
'--capsule-bottom': `${navInfo.value.capsuleBottom || (navInfo.value.safeHeaderGap + 32)}px`,
'--capsule-below-gap': `${navInfo.value.capsuleBelowGap || (navInfo.value.safeHeaderGap + 42)}px`
};
});
const todayStats = ref({
income: '380.00',
orders: 4,
waitingVerify: 1
});
const onThemeChanged = (newTheme) => {
currentThemeKey.value = newTheme;
applyThemeToNativeUI(newTheme);
};
let isHallLoading = false;
const loadHallData = async () => {
if (isHallLoading) return;
isHallLoading = true;
let uid = uni.getStorageSync('userId') || expert.value.id || 2006;
if (uid == 101) uid = 2006;
expert.value.id = uid;
// 1. 同步最新在线接单状态
isOnline.value = uni.getStorageSync('EXPERT_RECEIVING_STATUS') !== '0';
// 2. 并行动态读取最新资料、经营数据与派单池
let myCoords = { lat: null, lng: null };
try {
const pos = await getUserLocation();
myCoords = { lat: pos.latitude, lng: pos.longitude };
} catch (_) {
// 未授权定位:距离显示「同城」,不影响大厅浏览
}
try {
await Promise.allSettled([
getUserProfile(uid).then((profile) => {
if (profile) {
let safeAvatar = profile.avatar;
if (!safeAvatar || safeAvatar.startsWith('http://tmp') || safeAvatar.includes('__tmp__')) {
safeAvatar = DEFAULT_EXPERT_AVATAR;
}
expert.value = {
...expert.value,
...profile,
id: uid,
nickname: profile.nickname || expert.value.nickname,
avatar: safeAvatar
};
}
}).catch(() => {}),
getExpertStats(uid).then((stats) => {
if (stats) {
todayStats.value = {
income: stats.income !== undefined ? stats.income : '0.00',
orders: stats.orders !== undefined ? stats.orders : 0,
waitingVerify: stats.waitingVerify !== undefined ? stats.waitingVerify : 0
};
}
}).catch(() => {}),
getOrdersPool(myCoords).then((pool) => {
if (Array.isArray(pool)) {
orders.value = pool.map(p => {
let typeClass = 'cat-food';
if (p.category.includes('喝') || p.category.includes('咖啡')) typeClass = 'cat-drink';
if (p.category.includes('唱') || p.category.includes('KTV')) typeClass = 'cat-sing';
if (p.category.includes('游戏') || p.category.includes('电竞') || p.category.includes('开黑')) typeClass = 'cat-game';
return {
...p,
typeClass,
timeSlot: p.timeSlot || '今日内随时可约',
distance: p.distanceText || '同城',
requirement: p.requirement || '按约定准时到场,文明健康社交。'
};
});
}
}).catch(() => {})
]);
} catch (err) {
console.warn('[Hall] 加载接单大厅数据异常:', err);
} finally {
isHallLoading = false;
}
};
onMounted(() => {
navInfo.value = getNavBarInfo();
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
uni.$on('theme-change', onThemeChanged);
loadHallData();
});
onShow(() => {
if (!isExpertCertified()) {
uni.showToast({
title: '非认证大神禁止访问接单大厅',
icon: 'none',
duration: 1500
});
setTimeout(() => {
uni.reLaunch({ url: '/pages/user/index' });
}, 400);
return;
}
navInfo.value = getNavBarInfo();
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
uni.hideTabBar({ animation: false, fail: () => {} });
loadHallData();
});
onUnmounted(() => {
uni.$off('theme-change', onThemeChanged);
});
const countByType = (cls: string) => orders.value.filter((o: any) => o.typeClass === cls).length;
const categories = computed(() => [
{ key: 'all', name: '全部可接', count: orders.value.length, icon: ICONS.gridAll },
{ key: 'food', name: '同城美食', count: countByType('cat-food'), icon: ICONS.utensils },
{ key: 'drink', name: '清吧咖啡', count: countByType('cat-drink'), icon: ICONS.coffee },
{ key: 'sing', name: 'KTV麦霸', count: countByType('cat-sing'), icon: ICONS.micVocal },
{ key: 'game', name: '线上电竞', count: countByType('cat-game'), icon: ICONS.gameCrosshair }
]);
const orders = ref([]);
const filteredOrders = computed(() => {
if (activeCategory.value === 'all') return orders.value;
if (activeCategory.value === 'food') return orders.value.filter(o => o.typeClass === 'cat-food');
if (activeCategory.value === 'drink') return orders.value.filter(o => o.typeClass === 'cat-drink');
if (activeCategory.value === 'sing') return orders.value.filter(o => o.typeClass === 'cat-sing');
if (activeCategory.value === 'game') return orders.value.filter(o => o.typeClass === 'cat-game');
return orders.value;
});
const toggleOnlineStatus = async () => {
isOnline.value = !isOnline.value;
uni.setStorageSync('EXPERT_RECEIVING_STATUS', isOnline.value ? '1' : '0');
try {
await updateCompanionProfile(expert.value.id || 2006, {
status: isOnline.value ? '接单中' : '休息中'
});
} catch (_) {}
uni.showToast({
title: isOnline.value ? '已开启在线接单' : '已设置为打烊休息',
icon: isOnline.value ? 'success' : 'none',
duration: 1500
});
};
const handleGrabOrder = (order) => {
uni.showModal({
title: '确认抢单',
content: `【${order.category}】\n酬金:¥${order.reward}\n预约时段:${order.timeSlot}\n\n确认后立即为您保留此单并发送微信履约通知。`,
confirmText: '立即接单',
confirmColor: '#10B981',
cancelText: '取消',
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '正在锁定订单...' });
try {
await claimOrder(order.id, {
expertId: expert.value.id || 2006,
companionName: expert.value.nickname || '接单大神',
expertAvatar: expert.value.avatar || DEFAULT_EXPERT_AVATAR
});
} catch (err) {
uni.hideLoading();
uni.showToast({
title: (err && err.message) || '接单失败,请稍后重试',
icon: 'none'
});
return;
}
uni.hideLoading();
uni.showToast({
title: '接单成功!请准时履约',
icon: 'success'
});
setTimeout(() => {
uni.navigateTo({
url: '/pages/order/list'
});
}, 600);
}
}
});
};
const showDetailModal = ref(false);
const selectedOrder = ref(null);
const stopPropagation = (e) => {
if (e && typeof e.stopPropagation === 'function') {
e.stopPropagation();
}
};
const openOrderDetailModal = (order) => {
selectedOrder.value = order;
showDetailModal.value = true;
};
const closeOrderDetailModal = () => {
showDetailModal.value = false;
selectedOrder.value = null;
};
const handleGrabOrderFromModal = () => {
if (!selectedOrder.value) return;
const orderToGrab = selectedOrder.value;
closeOrderDetailModal();
handleGrabOrder(orderToGrab);
};
const formatShortOrderNo = (id) => {
if (!id && id !== 0) return '';
const str = String(id).trim();
if (str.includes('_')) {
const parts = str.split('_');
const mainPart = parts[1] || parts[0];
const digits = mainPart.replace(/\D/g, '');
if (digits.length >= 6) {
return digits.slice(-6);
}
}
const digits = str.replace(/\D/g, '');
if (digits.length >= 6) {
return digits.slice(-6);
}
return str;
};
const copyOrderNo = () => {
if (!selectedOrder.value || !selectedOrder.value.id) return;
uni.setClipboardData({
data: String(selectedOrder.value.id),
showToast: false,
success: () => {
uni.showToast({
title: '已复制完整单号',
icon: 'none',
duration: 1500
});
}
});
};
</script>
<style lang="scss">
.hall-container {
min-height: 100vh;
background-color: #0A0F1D;
color: #F8FAFC;
padding-top: var(--capsule-below-gap, calc(var(--safe-header-gap, 36rpx) + 80rpx));
padding-left: 28rpx;
padding-right: 28rpx;
padding-bottom: 24rpx;
box-sizing: border-box;
.hall-header-card {
display: flex;
align-items: center;
justify-content: space-between;
background: #151D30;
border-radius: 28rpx;
padding: 24rpx 28rpx;
border: 1rpx solid rgba(255, 255, 255, 0.08);
margin-top: 0;
margin-bottom: 24rpx;
.expert-info-left {
display: flex;
align-items: center;
gap: 16rpx;
.avatar-img {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
border: 4rpx solid #10B981;
}
.text-meta {
.name-row {
display: flex;
align-items: center;
gap: 12rpx;
.expert-name {
font-size: 30rpx;
font-weight: 800;
color: #FFFFFF;
}
.pro-badge {
font-size: 18rpx;
background: rgba(16, 185, 129, 0.2);
color: #10B981;
padding: 2rpx 10rpx;
border-radius: 8rpx;
font-weight: 700;
&.offline {
background: rgba(148, 163, 184, 0.2);
color: #94A3B8;
}
}
}
.rating-sub {
font-size: 20rpx;
color: #94A3B8;
margin-top: 4rpx;
}
}
}
.status-toggle {
display: flex;
align-items: center;
gap: 8rpx;
background: #0A0F1D;
padding: 12rpx 24rpx;
border-radius: 30rpx;
border: 1rpx solid rgba(255, 255, 255, 0.1);
.status-dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
background: #64748B;
&.active {
background: #10B981;
box-shadow: 0 0 10rpx #10B981;
}
}
.toggle-text {
font-size: 22rpx;
color: #E2E8F0;
font-weight: 600;
}
}
}
.hall-stats-card {
display: flex;
align-items: center;
justify-content: space-around;
background: #151D30;
border-radius: 24rpx;
padding: 24rpx 16rpx;
border: 1rpx solid rgba(255, 255, 255, 0.08);
margin-bottom: 24rpx;
.stat-col {
display: flex;
flex-direction: column;
align-items: center;
.stat-num {
font-size: 32rpx;
font-weight: 800;
color: #FFFFFF;
font-family: monospace;
&.text-green {
color: #10B981;
}
&.text-orange {
color: #F59E0B;
}
}
.stat-label {
font-size: 20rpx;
color: #94A3B8;
margin-top: 6rpx;
}
}
.stat-divider {
width: 1rpx;
height: 48rpx;
background: rgba(255, 255, 255, 0.1);
}
}
.category-tabs-wrap {
margin-bottom: 24rpx;
width: 100%;
.category-scroll {
width: 100%;
white-space: nowrap;
box-sizing: border-box;
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
}
.tabs-list {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 16rpx;
padding: 6rpx 4rpx;
.cat-chip {
display: inline-flex;
flex-direction: row;
align-items: center;
flex-shrink: 0;
white-space: nowrap;
padding: 14rpx 26rpx;
background: #151D30;
border-radius: 32rpx;
border: 1px solid rgba(255, 255, 255, 0.08);
box-sizing: border-box;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
.chip-icon-svg {
width: 28rpx;
height: 28rpx;
margin-right: 10rpx;
flex-shrink: 0;
}
.chip-text {
font-size: 24rpx;
color: #94A3B8;
font-weight: 500;
white-space: nowrap;
line-height: 1;
}
.chip-count {
font-size: 20rpx;
font-weight: 700;
line-height: 1;
padding: 4rpx 12rpx;
border-radius: 20rpx;
margin-left: 10rpx;
background: rgba(255, 255, 255, 0.1);
color: #94A3B8;
flex-shrink: 0;
}
&.active {
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
border-color: #34D399;
box-shadow: 0 4rpx 16rpx rgba(16, 185, 129, 0.35);
.chip-text {
color: #FFFFFF;
font-weight: 700;
}
.chip-count {
background: rgba(255, 255, 255, 0.25);
color: #FFFFFF;
}
}
}
}
}
.dispatch-list {
display: flex;
flex-direction: column;
gap: 20rpx;
.hall-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10rpx;
padding: 60rpx 24rpx;
border-radius: 24rpx;
background: rgba(255, 255, 255, 0.03);
border: 1rpx dashed rgba(255, 255, 255, 0.12);
.hall-empty-title {
font-size: 28rpx;
font-weight: 600;
color: #E2E8F0;
}
.hall-empty-desc {
font-size: 24rpx;
color: #94A3B8;
}
}
.dispatch-card {
background: #151D30;
border-radius: 24rpx;
padding: 24rpx;
border: 1rpx solid rgba(255, 255, 255, 0.08);
.card-top-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
.tag-group {
display: flex;
align-items: center;
gap: 10rpx;
.category-tag {
font-size: 20rpx;
padding: 4rpx 14rpx;
border-radius: 8rpx;
font-weight: 700;
&.cat-food {
background: rgba(245, 158, 11, 0.15);
color: #F59E0B;
}
&.cat-game {
background: rgba(99, 102, 241, 0.15);
color: #818CF8;
}
&.cat-drink {
background: rgba(16, 185, 129, 0.15);
color: #10B981;
}
&.cat-sing {
background: rgba(236, 72, 153, 0.15);
color: #EC4899;
}
}
.pay-method-tag {
font-size: 20rpx;
color: #94A3B8;
background: rgba(255, 255, 255, 0.06);
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
}
.reward-box {
.reward-val {
font-size: 36rpx;
font-weight: 800;
color: #10B981;
font-family: monospace;
}
}
}
.order-title {
font-size: 28rpx;
font-weight: 700;
color: #F8FAFC;
display: block;
margin-bottom: 14rpx;
}
.info-grid {
display: flex;
flex-direction: column;
gap: 8rpx;
margin-bottom: 12rpx;
.info-row {
display: flex;
align-items: center;
gap: 8rpx;
.row-icon {
width: 22rpx;
height: 22rpx;
}
.row-text {
font-size: 22rpx;
color: #94A3B8;
}
}
}
.remark-box {
background: rgba(0, 0, 0, 0.25);
border-radius: 12rpx;
padding: 10rpx 14rpx;
margin-bottom: 18rpx;
.remark-text {
font-size: 22rpx;
color: #CBD5E1;
}
}
.card-action-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 16rpx;
border-top: 1rpx solid rgba(255, 255, 255, 0.06);
.user-inline {
display: flex;
align-items: center;
gap: 10rpx;
.user-avatar {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
}
.user-name {
font-size: 22rpx;
color: #94A3B8;
}
}
.btn-grab-order {
background: linear-gradient(135deg, #10B981, #059669);
color: #FFFFFF;
font-size: 24rpx;
font-weight: 700;
padding: 10rpx 28rpx;
border-radius: 28rpx;
box-shadow: 0 4rpx 12rpx rgba(16, 185, 129, 0.35);
margin: 0;
line-height: 1.5;
}
}
}
}
// 派单需求全貌看板浮层弹窗
.dispatch-detail-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.72);
backdrop-filter: blur(16px);
display: flex;
align-items: flex-end;
justify-content: center;
.dispatch-detail-modal {
width: 100%;
max-width: 500px;
max-height: 85vh;
background: #111827;
border-top-left-radius: 36rpx;
border-top-right-radius: 36rpx;
border: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: none;
display: flex;
flex-direction: column;
box-shadow: 0 -16rpx 48rpx rgba(0, 0, 0, 0.5);
animation: modalSlideUp 0.25s cubic-bezier(0.16, 1, 0.3, 1);
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 32rpx 20rpx;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
.header-title-box {
display: flex;
align-items: center;
gap: 14rpx;
.modal-title {
font-size: 32rpx;
font-weight: 800;
color: #F8FAFC;
}
.order-no-sub {
font-size: 20rpx;
color: #94A3B8;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
padding: 2rpx 12rpx;
border-radius: 8rpx;
font-family: -apple-system, BlinkMacSystemFont, monospace;
font-weight: 600;
letter-spacing: 0.5rpx;
}
}
.btn-close-modal {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.06);
display: flex;
align-items: center;
justify-content: center;
.close-icon-svg {
width: 28rpx;
height: 28rpx;
}
}
}
.modal-body-scroll {
flex: 1;
max-height: calc(85vh - 240rpx);
padding: 24rpx 32rpx;
box-sizing: border-box;
.detail-reward-card {
background: linear-gradient(135deg, rgba(30, 41, 59, 0.9) 0%, rgba(15, 23, 42, 0.95) 100%);
border: 1px solid rgba(16, 185, 129, 0.25);
border-radius: 20rpx;
padding: 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.reward-left {
flex: 1;
margin-right: 20rpx;
.detail-tag-row {
display: flex;
gap: 10rpx;
margin-bottom: 8rpx;
.category-tag {
font-size: 20rpx;
padding: 4rpx 14rpx;
border-radius: 8rpx;
font-weight: 700;
&.cat-food { background: rgba(245, 158, 11, 0.15); color: #F59E0B; }
&.cat-game { background: rgba(99, 102, 241, 0.15); color: #818CF8; }
&.cat-drink { background: rgba(16, 185, 129, 0.15); color: #10B981; }
&.cat-sing { background: rgba(236, 72, 153, 0.15); color: #EC4899; }
}
.pay-method-tag {
font-size: 20rpx;
color: #94A3B8;
background: rgba(255, 255, 255, 0.06);
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
}
.detail-title {
font-size: 30rpx;
font-weight: 700;
color: #F8FAFC;
line-height: 1.4;
}
}
.reward-badge-card {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
padding: 12rpx 20rpx;
background: rgba(16, 185, 129, 0.08);
border: 1px solid rgba(16, 185, 129, 0.25);
border-radius: 16rpx;
flex-shrink: 0;
.badge-label {
font-size: 20rpx;
color: #10B981;
font-weight: 600;
margin-bottom: 6rpx;
letter-spacing: 0.5rpx;
}
.amount-row {
display: flex;
align-items: baseline;
gap: 4rpx;
.curr-mark {
font-size: 28rpx;
font-weight: 800;
color: #10B981;
line-height: 1;
}
.reward-num {
font-size: 50rpx;
font-weight: 900;
color: #10B981;
line-height: 1;
font-family: -apple-system, BlinkMacSystemFont, monospace;
}
}
}
}
.detail-params-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 20rpx;
padding: 16rpx 20rpx;
margin-bottom: 20rpx;
.param-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14rpx 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
&:last-child {
border-bottom: none;
}
.param-lbl {
display: flex;
align-items: center;
gap: 12rpx;
.param-icon {
width: 28rpx;
height: 28rpx;
}
.lbl-txt {
font-size: 24rpx;
color: #94A3B8;
}
}
.param-val {
font-size: 24rpx;
color: #F8FAFC;
font-weight: 600;
&.highlight {
color: #818CF8;
}
}
.param-val-col {
text-align: right;
.param-val {
display: block;
}
.distance-sub {
font-size: 20rpx;
color: #64748B;
}
}
}
}
.detail-remark-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 20rpx;
padding: 20rpx;
margin-bottom: 20rpx;
.remark-head {
display: flex;
align-items: center;
gap: 10rpx;
margin-bottom: 12rpx;
.remark-icon {
width: 26rpx;
height: 26rpx;
}
.remark-title {
font-size: 24rpx;
font-weight: 700;
color: #94A3B8;
}
}
.remark-content {
background: rgba(0, 0, 0, 0.2);
padding: 14rpx 16rpx;
border-radius: 12rpx;
.remark-text {
font-size: 24rpx;
color: #CBD5E1;
line-height: 1.5;
}
}
}
.detail-client-card {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 20rpx;
padding: 18rpx 20rpx;
margin-bottom: 20rpx;
.client-avatar {
width: 76rpx;
height: 76rpx;
border-radius: 50%;
margin-right: 16rpx;
border: 2rpx solid rgba(255, 255, 255, 0.1);
}
.client-info {
flex: 1;
.client-name-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 4rpx;
.client-name {
font-size: 26rpx;
font-weight: 700;
color: #F8FAFC;
}
.client-verified-tag {
font-size: 18rpx;
color: #10B981;
background: rgba(16, 185, 129, 0.12);
padding: 2rpx 8rpx;
border-radius: 6rpx;
font-weight: 600;
}
}
.client-trust-sub {
font-size: 20rpx;
color: #64748B;
}
}
}
.detail-safe-notice {
display: flex;
align-items: flex-start;
gap: 10rpx;
background: rgba(16, 185, 129, 0.06);
border: 1px solid rgba(16, 185, 129, 0.15);
border-radius: 16rpx;
padding: 16rpx 20rpx;
margin-bottom: 20rpx;
.safe-icon {
width: 28rpx;
height: 28rpx;
margin-top: 2rpx;
flex-shrink: 0;
}
.safe-desc {
font-size: 20rpx;
color: #94A3B8;
line-height: 1.5;
}
}
}
.modal-footer {
padding: 20rpx 32rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
border-top: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
gap: 20rpx;
.btn-modal-cancel {
flex: 1;
height: 80rpx;
line-height: 80rpx;
font-size: 26rpx;
font-weight: 600;
color: #94A3B8;
background: rgba(255, 255, 255, 0.06);
border-radius: 40rpx;
border: 1px solid rgba(255, 255, 255, 0.1);
margin: 0;
}
.btn-modal-grab {
flex: 2;
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
font-weight: 700;
color: #FFFFFF;
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
border-radius: 40rpx;
border: none;
box-shadow: 0 8rpx 24rpx rgba(16, 185, 129, 0.35);
margin: 0;
}
}
}
}
@keyframes modalSlideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
/* 浅色暖阳 (Warm Theme) 适配 */
&.theme-warm {
background-color: #FAF7F2;
color: #292524;
.hall-header-card {
background: #FFFFFF;
border-color: #E7E5E4;
.expert-info-left .text-meta .name-row .expert-name {
color: #292524;
}
.status-toggle {
background: #F5F5F4;
border-color: #E7E5E4;
.toggle-text {
color: #44403C;
}
}
}
.hall-stats-card {
background: #FFFFFF;
border-color: #E7E5E4;
.stat-col .stat-num {
color: #292524;
&.text-green { color: #10B981; }
&.text-orange { color: #EA580C; }
}
.stat-divider {
background: #E7E5E4;
}
}
.category-tabs-wrap .tabs-list .cat-chip {
background: #FFFFFF;
border: 1px solid #E7E5E4;
box-shadow: 0 2rpx 10rpx rgba(28, 25, 23, 0.03);
.chip-text {
color: #57534E;
}
.chip-count {
background: #F5F5F4;
color: #78716C;
}
&.active {
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
border-color: #10B981;
box-shadow: 0 4rpx 14rpx rgba(16, 185, 129, 0.3);
.chip-text {
color: #FFFFFF;
font-weight: 700;
}
.chip-count {
background: rgba(255, 255, 255, 0.25);
color: #FFFFFF;
}
}
}
.dispatch-list .dispatch-card {
background: #FFFFFF;
border-color: #E7E5E4;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
.order-title {
color: #292524;
}
.remark-box {
background: #F5F5F4;
.remark-text { color: #44403C; }
}
.card-action-bar {
border-top-color: #F5F5F4;
}
}
.dispatch-detail-mask {
background: rgba(0, 0, 0, 0.45);
.dispatch-detail-modal {
background: #FFFFFF;
border-color: #E7E5E4;
box-shadow: 0 -16rpx 48rpx rgba(28, 25, 23, 0.12);
.modal-header {
border-bottom-color: #F5F5F4;
.header-title-box .modal-title { color: #1C1917; }
.header-title-box .order-no-sub {
color: #78716C;
background: #F5F5F4;
border-color: #E7E5E4;
}
.btn-close-modal { background: #F5F5F4; }
}
.modal-body-scroll {
.detail-reward-card {
background: linear-gradient(135deg, #FFF7ED 0%, #FFEDD5 100%);
border-color: #FED7AA;
.reward-left .detail-title { color: #1C1917; }
.reward-left .detail-tag-row .pay-method-tag { background: #FFFFFF; color: #78716C; }
.reward-badge-card {
background: #FFFFFF;
border-color: rgba(234, 88, 12, 0.25);
box-shadow: 0 4rpx 14rpx rgba(234, 88, 12, 0.1);
.badge-label {
color: #EA580C;
}
.amount-row {
.curr-mark,
.reward-num {
color: #EA580C;
}
}
}
}
.detail-params-card {
background: #FAFAF9;
border-color: #E7E5E4;
.param-row {
border-bottom-color: #F5F5F4;
.param-lbl .lbl-txt { color: #78716C; }
.param-val { color: #1C1917; }
.param-val.highlight { color: #EA580C; }
}
}
.detail-remark-card {
background: #FAFAF9;
border-color: #E7E5E4;
.remark-head .remark-title { color: #78716C; }
.remark-content {
background: #F5F5F4;
.remark-text { color: #44403C; }
}
}
.detail-client-card {
background: #FAFAF9;
border-color: #E7E5E4;
.client-info .client-name-row .client-name { color: #1C1917; }
.client-info .client-trust-sub { color: #78716C; }
}
.detail-safe-notice {
background: #FFF7ED;
border-color: #FED7AA;
.safe-desc { color: #78716C; }
}
}
.modal-footer {
border-top-color: #E7E5E4;
.btn-modal-cancel {
background: #F5F5F4;
border-color: #E7E5E4;
color: #78716C;
}
.btn-modal-grab {
background: linear-gradient(135deg, #EA580C 0%, #C2410C 100%);
box-shadow: 0 8rpx 24rpx rgba(234, 88, 12, 0.3);
}
}
}
}
}
}
</style>