文件
shanghaipeiwan/pwuniapp/pages/expert/hall.vue

787 行
21 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" />
<view class="text-meta">
<view class="name-row">
<text class="expert-name">{{ expert.nickname }}</text>
<text class="pro-badge">接单中</text>
</view>
<text class="rating-sub">评分 5.0 · 接单率 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-for="order in filteredOrders"
:key="order.id"
class="dispatch-card"
>
<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="handleGrabOrder(order)">
立即接单
</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 { createOrder, getOrdersPool, getExpertStats, claimOrder, updateCompanionProfile } from '@/common/api.js';
import { getNavBarInfo } from '@/common/nav.js';
const currentThemeKey = ref(getCurrentThemeKey());
const expert = ref({
id: 101,
nickname: '接单大神',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300'
});
const isOnline = ref(uni.getStorageSync('EXPERT_RECEIVING_STATUS') !== '0');
const activeCategory = ref('all');
const navInfo = ref(getNavBarInfo());
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);
};
const loadHallData = async () => {
try {
const stats = await getExpertStats(101);
if (stats) {
todayStats.value = stats;
}
const pool = await getOrdersPool();
if (pool && pool.length > 0) {
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: '1.2km内',
requirement: p.requirement || '按约定准时到场,文明健康社交。'
};
});
}
} catch (err) {
console.warn('[Hall] 加载接单大厅数据异常:', err);
}
};
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 categories = ref([
{ key: 'all', name: '全部可接', count: 12, icon: ICONS.gridAll },
{ key: 'food', name: '同城美食', count: 4, icon: ICONS.utensils },
{ key: 'drink', name: '清吧咖啡', count: 3, icon: ICONS.coffee },
{ key: 'sing', name: 'KTV麦霸', count: 2, icon: ICONS.micVocal },
{ key: 'game', name: '线上电竞', count: 3, icon: ICONS.gameCrosshair }
]);
const orders = ref([
{
id: 101,
category: '同城美食陪吃',
typeClass: 'cat-food',
payMethod: 'AA制各付',
reward: 120,
title: '三里屯北京烤鸭探店·寻2位好脾气饭搭子',
location: '朝阳区 · 三里屯SOHO商场',
distance: '1.1km',
timeSlot: '今日 18:30 - 20:30',
requirement: '已提前订座,现场各自出示4位核销码,性格随和开朗。',
userName: '小鹿斑比',
userAvatar: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=100'
},
{
id: 102,
category: '线上电竞开黑',
typeClass: 'cat-game',
payMethod: '老板全包',
reward: 70,
title: '王者荣耀 · 微信区荣耀王者晋级双排',
location: '线上远程连麦',
distance: '全国',
timeSlot: '接单后即刻开始 (2局)',
requirement: '擅长射手或打野位,带飞晋级,开麦清晰沟通不压力。',
userName: '想赢一把好难',
userAvatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100'
},
{
id: 103,
category: '同城下午茶陪喝',
typeClass: 'cat-drink',
payMethod: '房主买单',
reward: 150,
title: '工体精品手冲咖啡馆 · 倾听解压与交流',
location: '朝阳区 · 工体北路',
distance: '1.8km',
timeSlot: '今日 15:00 - 17:00',
requirement: '纯正规咖啡馆,工作压力大想倾诉,求善解人意的倾听导师。',
userName: '风吹过的夏天',
userAvatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=100'
},
{
id: 104,
category: '同城KTV陪唱',
typeClass: 'cat-sing',
payMethod: 'AA分摊',
reward: 200,
title: '量贩纯K包厢麦霸欢唱 · 流行情歌对唱',
location: '朝阳区 · 青年路大悦城店',
distance: '3.2km',
timeSlot: '今日 20:00 - 23:00',
requirement: '爱唱歌、不跑调,会带动气氛,严禁任何违规,正规量贩KTV。',
userName: '麦霸小王子',
userAvatar: 'https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?w=100'
}
]);
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 || 101, {
status: isOnline.value ? '接单中' : '休息中'
});
} catch (_) {}
uni.showToast({
title: isOnline.value ? '已开启在线接单状态' : '已设置为打烊休息',
icon: isOnline.value ? 'success' : 'none'
});
};
const handleGrabOrder = (order) => {
uni.showModal({
title: '确认抢单',
content: `【${order.category}】\n酬金:¥${order.reward}\n预约时段:${order.timeSlot}\n\n确认后立即为您保留此单并发送微信履约通知。`,
confirmText: '立即接单',
confirmColor: '#10B981',
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '正在锁定订单...' });
try {
await createOrder({
buyerId: 2001,
clientName: order.userName || '客户老板',
expertId: 101,
companionName: '我 (接单大神)',
expertAvatar: order.userAvatar || 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=100',
track: order.category || '同城生活',
serviceTitle: order.title,
hourlyRate: order.reward,
durationHours: 2,
amount: order.reward,
location: order.location,
expenseBurden: order.payMethod,
status: '履约中'
});
} catch (_) {}
uni.hideLoading();
uni.showToast({
title: '接单成功!请准时履约',
icon: 'success'
});
setTimeout(() => {
uni.navigateTo({
url: '/pages/order/list'
});
}, 600);
}
}
});
};
</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;
}
}
.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;
.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;
}
}
}
}
/* 浅色暖阳 (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;
}
}
}
}
</style>