文件
shanghaipeiwan/pwuniapp/pages/expert/radar.vue
xcbyy 001f81eaca feat(lbs): 大神真实 5km 即时匹配与接单心跳
- companions 新增 latitude/longitude/last_active_at 与 idx_comp_active;切换接单中立即刷新活跃时间

- 新增 /api/companions/heartbeat(鉴权)与 /api/companions/match(真实注册+审核通过+接单中+10分钟心跳,Haversine 5km 过滤与距离排序,不返回精确坐标)

- 工作台/接单大厅/雷达前台每 60s 心跳,离开停止;首页即时匹配改为服务端 LBS 匹配并展示真实距离

- 线上实测:2010 匹配排除自己,仅命中 280m 的 8 号;匿名命中 9 号 10m、8 号 280m;北京 5km 为 0;118 套件/771 用例全绿
2026-10-22 18:44:02 +08:00

1266 行
34 KiB
Vue

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

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

<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-if="nearbyOrders.length === 0" class="pool-empty">
<text class="pool-empty-title">附近 {{ currentRadius }}km 暂无待接需求单</text>
<text class="pool-empty-desc">调整巡航半径或点击右上角刷新试试</text>
</view>
<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, onHide } 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, reverseGeocode } from '@/common/api.js';
import { getUserLocation } from '@/common/location.js';
import { startHeartbeat, stopHeartbeat } from '@/common/expert-heartbeat.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 myCoords = ref({ lat: null, lng: null });
const locating = ref(false);
const currentLocation = ref({
name: '正在定位…',
address: '正在获取当前位置'
});
const onThemeChanged = (newTheme) => {
currentThemeKey.value = newTheme;
applyThemeToNativeUI(newTheme);
};
const markerPosition = (distanceKm, idx) => {
const radius = Math.max(1, Number(currentRadius.value) || 5);
const ratio = Number.isFinite(Number(distanceKm))
? Math.min(0.92, Math.max(0.16, Number(distanceKm) / radius))
: 0.5;
const angle = (Math.PI * 2 * idx) / 5 - Math.PI / 2;
const top = 50 + Math.sin(angle) * ratio * 40;
const left = 50 + Math.cos(angle) * ratio * 40;
return { top: `${top.toFixed(1)}%`, left: `${left.toFixed(1)}%` };
};
const loadNearbyOrdersFromPool = async () => {
try {
const pool = await getOrdersPool({
lat: myCoords.value.lat,
lng: myCoords.value.lng,
radius: currentRadius.value
});
nearbyOrders.value = (pool || []).slice(0, 5).map((p, idx) => {
let typeClass = 'type-food';
let typeIcon = ICONS.utensils;
if (String(p.category || '').includes('喝') || String(p.category || '').includes('咖啡')) {
typeClass = 'type-coffee';
typeIcon = ICONS.coffee;
} else if (String(p.category || '').includes('唱') || String(p.category || '').includes('KTV')) {
typeClass = 'type-ktv';
typeIcon = ICONS.micVocal;
} else if (String(p.category || '').includes('游戏') || String(p.category || '').includes('电竞')) {
typeClass = 'type-game';
typeIcon = ICONS.gameAll;
}
const pos = markerPosition(p.distanceKm, idx);
return {
id: p.id,
categoryName: p.category,
title: p.title,
requirement: p.requirement || '按约定准时到场,现场出示4位核销码,文明健康社交。',
reward: p.reward,
duration: '2小时',
distance: p.distanceText || '同城',
distanceKm: p.distanceKm,
timeAgo: '刚刚',
buyerName: p.userName || '客户老板',
buyerAvatar: p.userAvatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=120',
typeClass,
typeIcon,
markerTop: pos.top,
markerLeft: pos.left
};
});
if (nearbyOrders.value.length > 0) {
selectedOrderId.value = nearbyOrders.value[0].id;
}
} catch (err) {
console.warn('[Radar] 加载雷达单池异常:', err);
uni.showToast({ title: '获取附近需求单失败,请稍后重试', icon: 'none' });
}
};
const refreshLocation = async () => {
if (locating.value) return;
locating.value = true;
try {
const pos = await getUserLocation();
myCoords.value = { lat: pos.latitude, lng: pos.longitude };
const geo = await reverseGeocode(pos.latitude, pos.longitude);
if (geo && geo.name) {
currentLocation.value = { name: geo.name, address: geo.address || '当前位置' };
} else {
currentLocation.value = {
name: '当前位置',
address: `经纬度 ${Number(pos.latitude).toFixed(4)}, ${Number(pos.longitude).toFixed(4)}`
};
}
} catch (err) {
myCoords.value = { lat: null, lng: null };
currentLocation.value = {
name: '未开启定位',
address: '开启定位权限后可查看附近需求单的真实距离'
};
uni.showToast({ title: '未获取到定位,距离将显示为同城', icon: 'none' });
} finally {
locating.value = false;
}
await loadNearbyOrdersFromPool();
};
onMounted(() => {
navInfo.value = getNavBarInfo();
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
uni.$on('theme-change', onThemeChanged);
});
let pollTimer = null;
const stopPolling = () => {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
};
const startPolling = () => {
stopPolling();
if (!isRadarActive.value) return;
pollTimer = setInterval(() => {
if (isRadarActive.value) void loadNearbyOrdersFromPool();
}, 20000);
};
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: () => {} });
refreshLocation();
startPolling();
if (isRadarActive.value) startHeartbeat();
else stopHeartbeat();
});
onHide(() => {
stopPolling();
stopHeartbeat();
});
onUnmounted(() => {
uni.$off('theme-change', onThemeChanged);
stopPolling();
stopHeartbeat();
});
const nearbyOrders = ref([]);
const changeRadius = async (r) => {
currentRadius.value = r;
uni.showToast({
title: `已更新探测范围至周边 ${r} 公里`,
icon: 'none'
});
await loadNearbyOrdersFromPool();
};
const handleRelocate = async () => {
uni.showLoading({ title: '正在重新定位...' });
try {
await refreshLocation();
} catch (_) {}
uni.hideLoading();
uni.showToast({
title: myCoords.value.lat ? '定位已更新' : '定位失败,请检查定位权限',
icon: myCoords.value.lat ? 'success' : 'none'
});
};
const toggleRadarSwitch = (e) => {
isRadarActive.value = e.detail.value;
if (isRadarActive.value) {
startPolling();
startHeartbeat();
} else {
stopPolling();
stopHeartbeat();
}
uni.showToast({
title: isRadarActive.value ? '已开启 5km 智能巡航接单' : '已挂起智能巡航',
icon: isRadarActive.value ? 'success' : 'none'
});
};
const selectMarkerOrder = (id) => {
selectedOrderId.value = id;
};
const refreshNearbyOrders = async () => {
uni.showLoading({ title: '雷达刷新中...' });
await refreshLocation();
uni.hideLoading();
uni.showToast({
title: `已匹配 ${currentRadius.value}km 内最新派单池`,
icon: 'success'
});
};
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 {
const cachedUser = uni.getStorageSync('userInfo') || {};
await claimOrder(order.id, {
expertId: Number(uni.getStorageSync('userId')) || 0,
companionName: cachedUser.nickname || '接单大神',
expertAvatar: cachedUser.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150'
});
} catch (claimErr) {
// 抢单失败必须如实告知(已被抢/不可接/未认证),严禁伪造成功
uni.hideLoading();
uni.showToast({
title: (claimErr && claimErr.message) || '抢单失败,请稍后重试',
icon: 'none'
});
await loadNearbyOrdersFromPool();
return;
}
uni.hideLoading();
uni.showToast({
title: '恭喜抢单成功!已锁定订单',
icon: 'success'
});
await loadNearbyOrdersFromPool();
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;
.pool-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);
.pool-empty-title {
font-size: 28rpx;
font-weight: 600;
color: #E2E8F0;
}
.pool-empty-desc {
font-size: 24rpx;
color: #94A3B8;
}
}
.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>