后端: 身份与归属鉴权收口; 资金安全(结算闸门/支付防篡改/报名费真实扣款/赠金/结算唯一入账/提现打款); 数据泄露与隐私; 登录与密钥安全(默认密钥闸门/登录限流); 可观测性; 启动自检. 微信: 商家转账到零钱提现打款闭环(含回调/查单/失败退回佣金); 订阅消息真实下发闭环与后台模板字段对齐. 后台: 财务/微信支付配置读取失败锁定; 看板/待办/走势去假数据; 打款与测试推送文案修正. 小程序: 请求层错误透传与登录过期提示; 多页失败反馈与乐观回滚; 帮助中心FAQ页面化. 测试: 新增 40+ 回归护栏与真实 MySQL 冒烟(75 套件/599 用例全绿).
1240 行
33 KiB
Vue
1240 行
33 KiB
Vue
<template>
|
||
<view class="radar-page-container" :class="{ 'theme-warm': currentThemeKey === 'warm' }" :style="navStyle">
|
||
<!-- 顶部定位栏与范围调节器 (升级双行通栏结构,彻底解决换行拥挤) -->
|
||
<view class="location-top-bar">
|
||
<!-- 第一行:当前定位与高精GPS状态 -->
|
||
<view class="loc-header-row">
|
||
<view class="loc-left">
|
||
<view class="loc-pin-box">
|
||
<image class="loc-pin-svg" :src="ICONS.mapPin" mode="aspectFit" />
|
||
</view>
|
||
<view class="loc-info">
|
||
<view class="loc-name-row">
|
||
<text class="loc-title">{{ currentLocation.name }}</text>
|
||
<view class="gps-badge">
|
||
<view class="gps-dot"></view>
|
||
<text class="gps-text">GPS定位</text>
|
||
</view>
|
||
</view>
|
||
<text class="loc-address">{{ currentLocation.address }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="relocate-btn btn-active" @click="handleRelocate">
|
||
<image class="relocate-icon" :src="ICONS.locate" mode="aspectFit" />
|
||
<text class="relocate-text">重新定位</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="loc-divider"></view>
|
||
|
||
<!-- 第二行:接单巡航探测半径调节 -->
|
||
<view class="loc-range-row">
|
||
<view class="range-meta">
|
||
<image class="range-icon" :src="ICONS.radarWave" mode="aspectFit" />
|
||
<text class="range-label">接单巡航半径</text>
|
||
<text class="range-hint">推荐 5km 覆盖全城热点</text>
|
||
</view>
|
||
<!-- 距离范围切换选择 (1km / 3km / 5km) -->
|
||
<view class="range-selector">
|
||
<view
|
||
v-for="r in rangeOptions"
|
||
:key="r"
|
||
class="range-pill btn-active"
|
||
:class="{ active: currentRadius === r }"
|
||
@click="changeRadius(r)"
|
||
>
|
||
{{ r }}km
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 核心雷达态势感知舱 (地图定位 + 5km同心圆声波扩散 + 动态扫描光束) -->
|
||
<view class="radar-cockpit-card">
|
||
<view class="cockpit-decor">
|
||
<text class="cockpit-tag">RADAR LBS DISPATCH</text>
|
||
<view class="live-dot-wrap">
|
||
<view class="blinking-dot"></view>
|
||
<text class="live-text">{{ isRadarActive ? '5km 巡航扫描中' : '巡航已挂起' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="radar-visual-stage">
|
||
<!-- 3层辐射同心圆波纹 -->
|
||
<view class="radar-circle circle-1"></view>
|
||
<view class="radar-circle circle-2"></view>
|
||
<view class="radar-circle circle-3"></view>
|
||
<view class="radar-axis-x"></view>
|
||
<view class="radar-axis-y"></view>
|
||
|
||
<!-- 360度旋转扫描光束扇区 -->
|
||
<view class="radar-sweeper" v-if="isRadarActive"></view>
|
||
|
||
<!-- 中心接单人大神定位 -->
|
||
<view class="center-expert-avatar">
|
||
<image class="expert-img" :src="expertAvatar" mode="aspectFill" />
|
||
<view class="pulse-ring"></view>
|
||
</view>
|
||
|
||
<!-- 5km范围内散布的实时发单点 (Markers) -->
|
||
<view
|
||
v-for="order in nearbyOrders"
|
||
:key="order.id"
|
||
class="order-marker btn-active"
|
||
:style="{ top: order.markerTop, left: order.markerLeft }"
|
||
:class="{ selected: selectedOrderId === order.id }"
|
||
@click="selectMarkerOrder(order.id)"
|
||
>
|
||
<view class="marker-pin" :class="order.typeClass">
|
||
<image class="marker-icon" :src="order.typeIcon" mode="aspectFit" />
|
||
</view>
|
||
<view class="marker-tooltip">
|
||
<text class="tip-dist">{{ order.distance }}</text>
|
||
<text class="tip-price">¥{{ order.reward }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 巡航接单全局状态开关卡片 -->
|
||
<view class="cruise-switch-box">
|
||
<view class="switch-meta">
|
||
<text class="switch-title">智能巡航接单 (附近 5km 优先派单)</text>
|
||
<text class="switch-desc">
|
||
{{ isRadarActive ? '已开启高灵敏探测,5km 内新单优先强提示推送' : '已暂停主动寻单,仅接收预约派单' }}
|
||
</text>
|
||
</view>
|
||
<switch
|
||
:checked="isRadarActive"
|
||
@change="toggleRadarSwitch"
|
||
color="#10B981"
|
||
style="transform: scale(0.85);"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 附近 5km 待接需求单列表 -->
|
||
<view class="nearby-section">
|
||
<view class="section-head">
|
||
<view class="title-row">
|
||
<image class="sec-icon" :src="ICONS.radarWave" mode="aspectFit" />
|
||
<text class="sec-title">附近 {{ currentRadius }}km 实时需求单</text>
|
||
<text class="order-count-badge">{{ nearbyOrders.length }} 单可接</text>
|
||
</view>
|
||
<text class="refresh-btn btn-active" @click="refreshNearbyOrders">刷新单池</text>
|
||
</view>
|
||
|
||
<!-- 订单卡片流 -->
|
||
<view class="orders-stream">
|
||
<view
|
||
v-for="item in nearbyOrders"
|
||
:key="item.id"
|
||
class="nearby-card"
|
||
:class="{ highlighted: selectedOrderId === item.id }"
|
||
@click="selectedOrderId = item.id"
|
||
>
|
||
<view class="card-head">
|
||
<view class="type-badge" :class="item.typeClass">
|
||
<image class="type-svg" :src="item.typeIcon" mode="aspectFit" />
|
||
<text class="type-name">{{ item.categoryName }}</text>
|
||
</view>
|
||
<view class="dist-tag">
|
||
<image class="dist-svg" :src="ICONS.mapPin" mode="aspectFit" />
|
||
<text class="dist-num">距您 {{ item.distance }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<text class="card-title">{{ item.title }}</text>
|
||
|
||
<view class="demand-desc-box">
|
||
<text class="demand-text">要求:{{ item.requirement }}</text>
|
||
</view>
|
||
|
||
<view class="card-bottom">
|
||
<view class="buyer-box">
|
||
<image class="buyer-avatar" :src="item.buyerAvatar" mode="aspectFill" />
|
||
<view class="buyer-meta">
|
||
<text class="buyer-name">{{ item.buyerName }}</text>
|
||
<text class="buyer-time">{{ item.timeAgo }}发布</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="action-box">
|
||
<view class="price-wrap">
|
||
<text class="price-val">¥{{ item.reward }}</text>
|
||
<text class="price-unit">/ 预计用时 {{ item.duration }}</text>
|
||
</view>
|
||
<button class="btn-take-order btn-active" @click.stop="handleTakeOrder(item)">
|
||
立即接单
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部自定义导航栏 (接单人端:接单大厅 / 我要接单[5km雷达] / 个人中心) -->
|
||
<AppTabBar current="radar" />
|
||
</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 { getNavBarInfo } from '@/common/nav.js';
|
||
import { getOrdersPool, claimOrder, createOrder } from '@/common/api.js';
|
||
|
||
const navInfo = ref(getNavBarInfo());
|
||
const navStyle = computed(() => ({
|
||
'--safe-header-gap': navInfo.value.safeHeaderGap + 'px',
|
||
'--capsule-right-space': navInfo.value.capsuleRightSpace + 'px',
|
||
'--capsule-height': navInfo.value.capsuleHeight + 'px',
|
||
'--capsule-bottom': (navInfo.value.capsuleBottom || (navInfo.value.safeHeaderGap + 32)) + 'px',
|
||
'--capsule-below-gap': (navInfo.value.capsuleBelowGap || (navInfo.value.safeHeaderGap + 42)) + 'px',
|
||
}));
|
||
|
||
const currentThemeKey = ref(getCurrentThemeKey());
|
||
const expertAvatar = ref('https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300');
|
||
const isRadarActive = ref(true);
|
||
const currentRadius = ref(5);
|
||
const rangeOptions = [1, 3, 5];
|
||
const selectedOrderId = ref(1);
|
||
|
||
const currentLocation = ref({
|
||
name: '朝阳区 · 三里屯太古里北区',
|
||
address: '北京市朝阳区三里屯路19号院 (精度 ±5m)'
|
||
});
|
||
|
||
const onThemeChanged = (newTheme) => {
|
||
currentThemeKey.value = newTheme;
|
||
applyThemeToNativeUI(newTheme);
|
||
};
|
||
|
||
const loadNearbyOrdersFromPool = async () => {
|
||
try {
|
||
const pool = await getOrdersPool();
|
||
if (pool && pool.length > 0) {
|
||
const coords = [
|
||
{ top: '28%', left: '22%', dist: '650m' },
|
||
{ top: '32%', left: '72%', dist: '1.4km' },
|
||
{ top: '68%', left: '30%', dist: '2.2km' },
|
||
{ top: '65%', left: '75%', dist: '3.8km' },
|
||
{ top: '45%', left: '50%', dist: '900m' }
|
||
];
|
||
nearbyOrders.value = pool.slice(0, 5).map((p, idx) => {
|
||
let typeClass = 'type-food';
|
||
let typeIcon = ICONS.utensils;
|
||
if (p.category.includes('喝') || p.category.includes('咖啡')) {
|
||
typeClass = 'type-coffee';
|
||
typeIcon = ICONS.coffee;
|
||
} else if (p.category.includes('唱') || p.category.includes('KTV')) {
|
||
typeClass = 'type-ktv';
|
||
typeIcon = ICONS.micVocal;
|
||
} else if (p.category.includes('游戏') || p.category.includes('电竞')) {
|
||
typeClass = 'type-game';
|
||
typeIcon = ICONS.gameAll;
|
||
}
|
||
const coord = coords[idx % coords.length];
|
||
return {
|
||
id: p.id,
|
||
categoryName: p.category,
|
||
title: p.title,
|
||
requirement: p.requirement || '按约定准时到场,现场出示4位核销码,文明健康社交。',
|
||
reward: p.reward,
|
||
duration: '2小时',
|
||
distance: coord.dist,
|
||
timeAgo: '刚刚',
|
||
buyerName: p.userName || '客户老板',
|
||
buyerAvatar: p.userAvatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=120',
|
||
typeClass,
|
||
typeIcon,
|
||
markerTop: coord.top,
|
||
markerLeft: coord.left
|
||
};
|
||
});
|
||
if (nearbyOrders.value.length > 0) {
|
||
selectedOrderId.value = nearbyOrders.value[0].id;
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.warn('[Radar] 加载雷达单池异常:', err);
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
navInfo.value = getNavBarInfo();
|
||
currentThemeKey.value = getCurrentThemeKey();
|
||
applyThemeToNativeUI(currentThemeKey.value);
|
||
uni.$on('theme-change', onThemeChanged);
|
||
loadNearbyOrdersFromPool();
|
||
});
|
||
|
||
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: () => {} });
|
||
loadNearbyOrdersFromPool();
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
uni.$off('theme-change', onThemeChanged);
|
||
});
|
||
|
||
|
||
const nearbyOrders = ref([
|
||
{
|
||
id: 1,
|
||
categoryName: '同城美食陪吃',
|
||
title: '三里屯太古里 · 铜锅涮肉AA搭子',
|
||
requirement: '吃货随和好相处,现场出示4位核销码,餐费已预订AA',
|
||
reward: 98,
|
||
duration: '2小时',
|
||
distance: '650m',
|
||
timeAgo: '5分钟前',
|
||
buyerName: '北京吃货小王',
|
||
buyerAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=120',
|
||
typeClass: 'type-food',
|
||
typeIcon: ICONS.utensils,
|
||
markerTop: '28%',
|
||
markerLeft: '22%'
|
||
},
|
||
{
|
||
id: 2,
|
||
categoryName: '同城下午茶陪喝',
|
||
title: '工体北路 · 独立手冲咖啡闲聊倾听',
|
||
requirement: '安静舒适咖啡馆,主要是倾听工作解压,老板全买单请客',
|
||
reward: 120,
|
||
duration: '2小时',
|
||
distance: '1.4km',
|
||
timeAgo: '12分钟前',
|
||
buyerName: '晴天小筑',
|
||
buyerAvatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=120',
|
||
typeClass: 'type-coffee',
|
||
typeIcon: ICONS.coffee,
|
||
markerTop: '32%',
|
||
markerLeft: '72%'
|
||
},
|
||
{
|
||
id: 3,
|
||
categoryName: '同城KTV陪唱',
|
||
title: '工体量贩纯K · 麦霸对唱与气氛组',
|
||
requirement: '会唱周杰伦/陈奕迅经典情歌,气氛欢快,拒绝私密',
|
||
reward: 180,
|
||
duration: '2.5小时',
|
||
distance: '2.2km',
|
||
timeAgo: '18分钟前',
|
||
buyerName: '欢唱麦霸哥',
|
||
buyerAvatar: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?w=120',
|
||
typeClass: 'type-ktv',
|
||
typeIcon: ICONS.micVocal,
|
||
markerTop: '68%',
|
||
markerLeft: '30%'
|
||
},
|
||
{
|
||
id: 4,
|
||
categoryName: '线上王者冲分',
|
||
title: '王者荣耀 · 荣耀王者晋级双排急召',
|
||
requirement: '擅长打野/射手节奏位,全程语音开麦交流,保星连胜',
|
||
reward: 70,
|
||
duration: '2局',
|
||
distance: '3.8km',
|
||
timeAgo: '25分钟前',
|
||
buyerName: '摘星少年',
|
||
buyerAvatar: 'https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=120',
|
||
typeClass: 'type-game',
|
||
typeIcon: ICONS.gameAll,
|
||
markerTop: '65%',
|
||
markerLeft: '75%'
|
||
}
|
||
]);
|
||
|
||
const changeRadius = (r) => {
|
||
currentRadius.value = r;
|
||
uni.showToast({
|
||
title: `已更新探测范围至周边 ${r} 公里`,
|
||
icon: 'none'
|
||
});
|
||
};
|
||
|
||
const handleRelocate = () => {
|
||
uni.showLoading({ title: '正在校准GPS...' });
|
||
setTimeout(() => {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: 'GPS定位已校准 (精度 ±3m)',
|
||
icon: 'success'
|
||
});
|
||
}, 400);
|
||
};
|
||
|
||
const toggleRadarSwitch = (e) => {
|
||
isRadarActive.value = e.detail.value;
|
||
uni.showToast({
|
||
title: isRadarActive.value ? '已开启 5km 智能巡航接单' : '已挂起智能巡航',
|
||
icon: isRadarActive.value ? 'success' : 'none'
|
||
});
|
||
};
|
||
|
||
const selectMarkerOrder = (id) => {
|
||
selectedOrderId.value = id;
|
||
};
|
||
|
||
const refreshNearbyOrders = () => {
|
||
uni.showLoading({ title: '雷达刷新中...' });
|
||
setTimeout(() => {
|
||
uni.hideLoading();
|
||
uni.showToast({ title: '已匹配最新 5km 派单池', icon: 'success' });
|
||
}, 500);
|
||
};
|
||
|
||
const handleTakeOrder = (order) => {
|
||
uni.showModal({
|
||
title: '确认抢接附近需求单',
|
||
content: `【${order.categoryName}】\n距离:${order.distance} · 酬金:¥${order.reward}\n需求:${order.title}\n\n接单后系统将生成专属 4 位数字核销码通道。`,
|
||
confirmText: '确认抢单',
|
||
confirmColor: '#10B981',
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
uni.showLoading({ title: '正在锁定订单...' });
|
||
try {
|
||
await claimOrder(order.id, {
|
||
expertId: 101,
|
||
companionName: '我 (接单大神)',
|
||
expertAvatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150'
|
||
});
|
||
} catch (claimErr) {
|
||
// 若为新需求单,尝试自动落盘创建
|
||
try {
|
||
await createOrder({
|
||
buyerId: 2001,
|
||
clientName: order.buyerName || '同城客户',
|
||
expertId: 101,
|
||
companionName: '我 (接单大神)',
|
||
track: order.categoryName,
|
||
serviceTitle: order.title,
|
||
hourlyRate: order.reward,
|
||
durationHours: 2,
|
||
amount: order.reward,
|
||
location: '北京市朝阳区商圈',
|
||
expenseBurden: '费用AA',
|
||
status: '履约中'
|
||
});
|
||
} catch (createErr) {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: (createErr && createErr.message) || (claimErr && claimErr.message) || '抢单失败,请稍后重试',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: '恭喜抢单成功!已锁定订单',
|
||
icon: 'success'
|
||
});
|
||
setTimeout(() => {
|
||
uni.navigateTo({
|
||
url: '/pages/order/list'
|
||
});
|
||
}, 800);
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.radar-page-container {
|
||
min-height: 100vh;
|
||
background-color: #0A0F1D;
|
||
color: #F8FAFC;
|
||
padding-top: var(--capsule-below-gap, calc(var(--safe-header-gap, 40px) + 50px));
|
||
padding-left: 28rpx;
|
||
padding-right: 28rpx;
|
||
padding-bottom: 24rpx;
|
||
box-sizing: border-box;
|
||
|
||
/* 顶部定位条 (双行通栏结构,告别挤压折行) */
|
||
.location-top-bar {
|
||
background: #151D30;
|
||
border-radius: 24rpx;
|
||
padding: 22rpx 24rpx;
|
||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||
margin-top: 0;
|
||
margin-bottom: 24rpx;
|
||
box-sizing: border-box;
|
||
|
||
.loc-header-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
|
||
.loc-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
flex: 1;
|
||
min-width: 0;
|
||
|
||
.loc-pin-box {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
border-radius: 18rpx;
|
||
background: rgba(56, 189, 248, 0.12);
|
||
border: 1rpx solid rgba(56, 189, 248, 0.25);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
|
||
.loc-pin-svg {
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
}
|
||
}
|
||
|
||
.loc-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
|
||
.loc-name-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
flex-wrap: nowrap;
|
||
|
||
.loc-title {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: #FFFFFF;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.gps-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
padding: 4rpx 12rpx;
|
||
border-radius: 20rpx;
|
||
background: rgba(16, 185, 129, 0.15);
|
||
border: 1rpx solid rgba(16, 185, 129, 0.3);
|
||
flex-shrink: 0;
|
||
white-space: nowrap;
|
||
|
||
.gps-dot {
|
||
width: 10rpx;
|
||
height: 10rpx;
|
||
border-radius: 50%;
|
||
background: #10B981;
|
||
box-shadow: 0 0 8rpx #10B981;
|
||
}
|
||
|
||
.gps-text {
|
||
font-size: 18rpx;
|
||
color: #10B981;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
.loc-address {
|
||
font-size: 20rpx;
|
||
color: #94A3B8;
|
||
margin-top: 6rpx;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
display: block;
|
||
}
|
||
}
|
||
}
|
||
|
||
.relocate-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
padding: 8rpx 18rpx;
|
||
background: rgba(16, 185, 129, 0.1);
|
||
border: 1rpx solid rgba(16, 185, 129, 0.25);
|
||
border-radius: 24rpx;
|
||
flex-shrink: 0;
|
||
margin-left: 12rpx;
|
||
transition: all 0.15s ease;
|
||
|
||
.relocate-icon {
|
||
width: 24rpx;
|
||
height: 24rpx;
|
||
}
|
||
|
||
.relocate-text {
|
||
font-size: 20rpx;
|
||
color: #10B981;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
}
|
||
|
||
.loc-divider {
|
||
height: 1rpx;
|
||
background: rgba(255, 255, 255, 0.06);
|
||
margin: 18rpx 0 16rpx;
|
||
}
|
||
|
||
.loc-range-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
|
||
.range-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
|
||
.range-icon {
|
||
width: 28rpx;
|
||
height: 28rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.range-label {
|
||
font-size: 22rpx;
|
||
color: #E2E8F0;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.range-hint {
|
||
font-size: 18rpx;
|
||
color: #64748B;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.range-selector {
|
||
display: flex;
|
||
align-items: center;
|
||
background: #0A0F1D;
|
||
border-radius: 30rpx;
|
||
padding: 4rpx;
|
||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||
|
||
.range-pill {
|
||
font-size: 20rpx;
|
||
color: #94A3B8;
|
||
padding: 6rpx 18rpx;
|
||
border-radius: 24rpx;
|
||
font-weight: 600;
|
||
transition: all 0.2s ease;
|
||
white-space: nowrap;
|
||
|
||
&.active {
|
||
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
|
||
color: #FFFFFF;
|
||
font-weight: 700;
|
||
box-shadow: 0 2rpx 10rpx rgba(16, 185, 129, 0.4);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 雷达感知舱 */
|
||
.radar-cockpit-card {
|
||
background: radial-gradient(circle at center, #132238 0%, #0D1627 100%);
|
||
border-radius: 32rpx;
|
||
padding: 28rpx;
|
||
border: 1rpx solid rgba(16, 185, 129, 0.25);
|
||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.5);
|
||
margin-bottom: 28rpx;
|
||
|
||
.cockpit-decor {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16rpx;
|
||
|
||
.cockpit-tag {
|
||
font-size: 20rpx;
|
||
color: #10B981;
|
||
font-family: monospace;
|
||
letter-spacing: 2rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.live-dot-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
|
||
.blinking-dot {
|
||
width: 14rpx;
|
||
height: 14rpx;
|
||
border-radius: 50%;
|
||
background: #10B981;
|
||
box-shadow: 0 0 10rpx #10B981;
|
||
animation: blink 1.2s infinite;
|
||
}
|
||
|
||
.live-text {
|
||
font-size: 20rpx;
|
||
color: #6EE7B7;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 雷达圆盘视觉区 */
|
||
.radar-visual-stage {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 440rpx;
|
||
margin: 10rpx auto 20rpx auto;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
border-radius: 24rpx;
|
||
background: rgba(10, 15, 29, 0.6);
|
||
|
||
.radar-circle {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
border: 1rpx solid rgba(16, 185, 129, 0.25);
|
||
|
||
&.circle-1 {
|
||
width: 140rpx;
|
||
height: 140rpx;
|
||
}
|
||
&.circle-2 {
|
||
width: 280rpx;
|
||
height: 280rpx;
|
||
}
|
||
&.circle-3 {
|
||
width: 420rpx;
|
||
height: 420rpx;
|
||
border: 1rpx dashed rgba(16, 185, 129, 0.35);
|
||
}
|
||
}
|
||
|
||
.radar-axis-x {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 1rpx;
|
||
background: rgba(16, 185, 129, 0.15);
|
||
}
|
||
|
||
.radar-axis-y {
|
||
position: absolute;
|
||
height: 100%;
|
||
width: 1rpx;
|
||
background: rgba(16, 185, 129, 0.15);
|
||
}
|
||
|
||
/* 360度旋转扫描波束 */
|
||
.radar-sweeper {
|
||
position: absolute;
|
||
width: 420rpx;
|
||
height: 420rpx;
|
||
border-radius: 50%;
|
||
background: conic-gradient(from 0deg, rgba(16, 185, 129, 0.35) 0deg, rgba(16, 185, 129, 0) 60deg);
|
||
animation: radarSpin 3.5s linear infinite;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* 中心接单人头像与脉冲波 */
|
||
.center-expert-avatar {
|
||
position: relative;
|
||
z-index: 10;
|
||
width: 76rpx;
|
||
height: 76rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
.expert-img {
|
||
width: 68rpx;
|
||
height: 68rpx;
|
||
border-radius: 50%;
|
||
border: 4rpx solid #10B981;
|
||
box-shadow: 0 0 16rpx rgba(16, 185, 129, 0.8);
|
||
}
|
||
|
||
.pulse-ring {
|
||
position: absolute;
|
||
width: 100rpx;
|
||
height: 100rpx;
|
||
border-radius: 50%;
|
||
border: 2rpx solid #10B981;
|
||
animation: centerPulse 2s ease-out infinite;
|
||
}
|
||
}
|
||
|
||
/* 周边发单点 Markers */
|
||
.order-marker {
|
||
position: absolute;
|
||
z-index: 15;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
transform: translate(-50%, -50%);
|
||
transition: transform 0.2s ease;
|
||
|
||
.marker-pin {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 3rpx solid #FFFFFF;
|
||
box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.6);
|
||
|
||
.marker-icon {
|
||
width: 28rpx;
|
||
height: 28rpx;
|
||
}
|
||
|
||
&.type-food {
|
||
background: #F59E0B;
|
||
}
|
||
&.type-coffee {
|
||
background: #10B981;
|
||
}
|
||
&.type-ktv {
|
||
background: #EC4899;
|
||
}
|
||
&.type-game {
|
||
background: #6366F1;
|
||
}
|
||
}
|
||
|
||
.marker-tooltip {
|
||
margin-top: 4rpx;
|
||
background: rgba(15, 23, 42, 0.9);
|
||
padding: 2rpx 8rpx;
|
||
border-radius: 8rpx;
|
||
border: 1rpx solid rgba(255, 255, 255, 0.15);
|
||
display: flex;
|
||
gap: 6rpx;
|
||
white-space: nowrap;
|
||
|
||
.tip-dist {
|
||
font-size: 16rpx;
|
||
color: #38BDF8;
|
||
}
|
||
.tip-price {
|
||
font-size: 16rpx;
|
||
color: #10B981;
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
|
||
&.selected {
|
||
transform: translate(-50%, -50%) scale(1.2);
|
||
z-index: 20;
|
||
|
||
.marker-pin {
|
||
border-color: #FACC15;
|
||
box-shadow: 0 0 16rpx #FACC15;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 巡航控制条 */
|
||
.cruise-switch-box {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: rgba(15, 23, 42, 0.7);
|
||
border-radius: 20rpx;
|
||
padding: 16rpx 20rpx;
|
||
border: 1rpx solid rgba(16, 185, 129, 0.2);
|
||
|
||
.switch-meta {
|
||
flex: 1;
|
||
|
||
.switch-title {
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
color: #F8FAFC;
|
||
display: block;
|
||
}
|
||
|
||
.switch-desc {
|
||
font-size: 18rpx;
|
||
color: #94A3B8;
|
||
margin-top: 2rpx;
|
||
display: block;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 附近列表区 */
|
||
.nearby-section {
|
||
.section-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 20rpx;
|
||
|
||
.title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
|
||
.sec-icon {
|
||
width: 34rpx;
|
||
height: 34rpx;
|
||
}
|
||
|
||
.sec-title {
|
||
font-size: 30rpx;
|
||
font-weight: 800;
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
.order-count-badge {
|
||
font-size: 20rpx;
|
||
background: rgba(16, 185, 129, 0.15);
|
||
color: #10B981;
|
||
padding: 2rpx 12rpx;
|
||
border-radius: 12rpx;
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
|
||
.refresh-btn {
|
||
font-size: 22rpx;
|
||
color: #6EE7B7;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.orders-stream {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20rpx;
|
||
|
||
.nearby-card {
|
||
background: #151D30;
|
||
border-radius: 24rpx;
|
||
padding: 24rpx;
|
||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||
transition: all 0.2s ease;
|
||
|
||
&.highlighted {
|
||
border-color: #10B981;
|
||
background: #19253C;
|
||
box-shadow: 0 4rpx 20rpx rgba(16, 185, 129, 0.15);
|
||
}
|
||
|
||
.card-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 14rpx;
|
||
|
||
.type-badge {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
padding: 4rpx 14rpx;
|
||
border-radius: 12rpx;
|
||
|
||
.type-svg {
|
||
width: 24rpx;
|
||
height: 24rpx;
|
||
}
|
||
|
||
.type-name {
|
||
font-size: 20rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
&.type-food {
|
||
background: rgba(245, 158, 11, 0.15);
|
||
color: #F59E0B;
|
||
}
|
||
&.type-coffee {
|
||
background: rgba(16, 185, 129, 0.15);
|
||
color: #10B981;
|
||
}
|
||
&.type-ktv {
|
||
background: rgba(236, 72, 153, 0.15);
|
||
color: #EC4899;
|
||
}
|
||
&.type-game {
|
||
background: rgba(99, 102, 241, 0.15);
|
||
color: #818CF8;
|
||
}
|
||
}
|
||
|
||
.dist-tag {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4rpx;
|
||
|
||
.dist-svg {
|
||
width: 22rpx;
|
||
height: 22rpx;
|
||
}
|
||
|
||
.dist-num {
|
||
font-size: 22rpx;
|
||
color: #38BDF8;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: #F8FAFC;
|
||
display: block;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.demand-desc-box {
|
||
background: rgba(0, 0, 0, 0.25);
|
||
border-radius: 12rpx;
|
||
padding: 10rpx 16rpx;
|
||
margin-bottom: 18rpx;
|
||
|
||
.demand-text {
|
||
font-size: 22rpx;
|
||
color: #94A3B8;
|
||
line-height: 1.4;
|
||
}
|
||
}
|
||
|
||
.card-bottom {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
|
||
.buyer-box {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
|
||
.buyer-avatar {
|
||
width: 52rpx;
|
||
height: 52rpx;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.buyer-meta {
|
||
.buyer-name {
|
||
font-size: 22rpx;
|
||
color: #CBD5E1;
|
||
font-weight: 600;
|
||
display: block;
|
||
}
|
||
|
||
.buyer-time {
|
||
font-size: 18rpx;
|
||
color: #64748B;
|
||
display: block;
|
||
}
|
||
}
|
||
}
|
||
|
||
.action-box {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
|
||
.price-wrap {
|
||
text-align: right;
|
||
|
||
.price-val {
|
||
font-size: 32rpx;
|
||
font-weight: 800;
|
||
color: #10B981;
|
||
font-family: monospace;
|
||
}
|
||
|
||
.price-unit {
|
||
font-size: 18rpx;
|
||
color: #64748B;
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
.btn-take-order {
|
||
background: linear-gradient(135deg, #10B981, #059669);
|
||
color: #FFFFFF;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
padding: 12rpx 28rpx;
|
||
border-radius: 30rpx;
|
||
box-shadow: 0 4rpx 14rpx rgba(16, 185, 129, 0.4);
|
||
margin: 0;
|
||
line-height: 1.4;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@keyframes radarSpin {
|
||
from {
|
||
transform: rotate(0deg);
|
||
}
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
@keyframes centerPulse {
|
||
0% {
|
||
transform: scale(0.8);
|
||
opacity: 0.8;
|
||
}
|
||
100% {
|
||
transform: scale(2.2);
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
@keyframes blink {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.2; }
|
||
}
|
||
|
||
/* 浅色暖阳 (Warm Theme) 适配 */
|
||
&.theme-warm {
|
||
background-color: #FAF7F2;
|
||
color: #292524;
|
||
|
||
.location-top-bar {
|
||
background: #FFFFFF;
|
||
border-color: #E7E5E4;
|
||
box-shadow: 0 4rpx 16rpx rgba(28, 25, 23, 0.04);
|
||
|
||
.loc-header-row {
|
||
.loc-left {
|
||
.loc-pin-box {
|
||
background: rgba(16, 185, 129, 0.08);
|
||
border-color: rgba(16, 185, 129, 0.2);
|
||
}
|
||
.loc-info {
|
||
.loc-name-row .loc-title {
|
||
color: #1C1917;
|
||
}
|
||
.loc-address {
|
||
color: #78716C;
|
||
}
|
||
}
|
||
}
|
||
.relocate-btn {
|
||
background: rgba(16, 185, 129, 0.08);
|
||
border-color: rgba(16, 185, 129, 0.25);
|
||
}
|
||
}
|
||
|
||
.loc-divider {
|
||
background: #F0ECE4;
|
||
}
|
||
|
||
.loc-range-row {
|
||
.range-meta {
|
||
.range-label {
|
||
color: #292524;
|
||
}
|
||
.range-hint {
|
||
color: #A8A29E;
|
||
}
|
||
}
|
||
.range-selector {
|
||
background: #F5F5F4;
|
||
border-color: #E7E5E4;
|
||
|
||
.range-pill {
|
||
color: #78716C;
|
||
&.active {
|
||
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
|
||
color: #FFFFFF;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.radar-cockpit-card {
|
||
background: radial-gradient(circle at center, #F0FDF4 0%, #FFFFFF 100%);
|
||
border-color: rgba(16, 185, 129, 0.35);
|
||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.05);
|
||
|
||
.radar-visual-stage {
|
||
background: rgba(245, 245, 244, 0.7);
|
||
.radar-circle {
|
||
border-color: rgba(16, 185, 129, 0.3);
|
||
}
|
||
}
|
||
|
||
.cruise-switch-box {
|
||
background: #FFFFFF;
|
||
border-color: #E7E5E4;
|
||
.switch-meta .switch-title {
|
||
color: #292524;
|
||
}
|
||
}
|
||
}
|
||
|
||
.nearby-section {
|
||
.section-head .title-row .sec-title {
|
||
color: #292524;
|
||
}
|
||
.orders-stream .nearby-card {
|
||
background: #FFFFFF;
|
||
border-color: #E7E5E4;
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
|
||
|
||
&.highlighted {
|
||
border-color: #10B981;
|
||
background: #F0FDF4;
|
||
}
|
||
|
||
.card-title {
|
||
color: #292524;
|
||
}
|
||
|
||
.demand-desc-box {
|
||
background: #F5F5F4;
|
||
}
|
||
|
||
.card-bottom .buyer-box .buyer-meta .buyer-name {
|
||
color: #44403C;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|