- 分类 chip 的 12/4/3 写死数量改为按真实订单统计 - 雷达页抢单失败时删除「自动落盘创建订单」兜底(原逻辑会伪造一笔 buyerId=2001 的订单并提示成功),改为如实提示已被抢/状态不可接/未认证 - 抢单使用当前登录身份(userId 由服务端 token 覆盖),成功后刷新列表 - 110 套件/730 用例全绿
1231 行
33 KiB
Vue
1231 行
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-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 } 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';
|
||
|
||
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);
|
||
});
|
||
|
||
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();
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
uni.$off('theme-change', onThemeChanged);
|
||
});
|
||
|
||
|
||
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;
|
||
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>
|