|
@@ -9,23 +9,34 @@
|
|
|
}}</el-radio-button>
|
|
}}</el-radio-button>
|
|
|
</el-radio-group>
|
|
</el-radio-group>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="filter-right">
|
|
|
|
|
- <el-cascader v-model="regionValue" :options="areaOptions" placeholder="省市区域" clearable
|
|
|
|
|
- style="width: 170px; margin-right: 10px" @change="handleAreaChange" />
|
|
|
|
|
- <el-select v-model="filters.station" placeholder="站点" clearable style="width: 170px">
|
|
|
|
|
- <el-option v-for="item in siteOptions" :key="item.value" :label="item.label" :value="item.value" />
|
|
|
|
|
- </el-select>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <el-cascader
|
|
|
|
|
+ v-model="regionValue"
|
|
|
|
|
+ :options="areaOptions"
|
|
|
|
|
+ placeholder="所属站点"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ style="width: 240px"
|
|
|
|
|
+ @change="handleAreaChange"
|
|
|
|
|
+ />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="main-content">
|
|
<div class="main-content">
|
|
|
<!-- Left: Real Gaode Map Area -->
|
|
<!-- Left: Real Gaode Map Area -->
|
|
|
<div class="map-wrapper">
|
|
<div class="map-wrapper">
|
|
|
<template v-if="checkPermi(['order:dispatch:map'])">
|
|
<template v-if="checkPermi(['order:dispatch:map'])">
|
|
|
- <div id="amap-container" class="map-view"></div>
|
|
|
|
|
|
|
+ <div v-if="!mapLoadError" id="amap-container" class="map-view"></div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 地图加载失败提示 -->
|
|
|
|
|
+ <div v-else class="map-error-state">
|
|
|
|
|
+ <div class="error-content">
|
|
|
|
|
+ <el-icon class="error-icon"><CircleCloseFilled /></el-icon>
|
|
|
|
|
+ <h3 class="error-title">地图加载失败</h3>
|
|
|
|
|
+ <p class="error-desc">请检查系统设置中的高德地图配置或联系管理员处理</p>
|
|
|
|
|
+ <el-button type="primary" plain size="small" @click="retryLoadMap">重试加载</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
<!-- Bottom Left: Map Controls & Stats -->
|
|
<!-- Bottom Left: Map Controls & Stats -->
|
|
|
- <div class="map-controls-panel">
|
|
|
|
|
|
|
+ <div v-if="!mapLoadError" class="map-controls-panel">
|
|
|
<div class="control-group">
|
|
<div class="control-group">
|
|
|
<div class="c-btn" :class="{ active: activeMapFilter === 'all' }" @click="setMapFilter('all')">全部</div>
|
|
<div class="c-btn" :class="{ active: activeMapFilter === 'all' }" @click="setMapFilter('all')">全部</div>
|
|
|
<div class="c-btn red" :class="{ active: activeMapFilter === 'merchants' }"
|
|
<div class="c-btn red" :class="{ active: activeMapFilter === 'merchants' }"
|
|
@@ -75,6 +86,8 @@ import { checkPermi } from "@/utils/permission";
|
|
|
import { ElMessage } from 'element-plus';
|
|
import { ElMessage } from 'element-plus';
|
|
|
import { listAllService } from '@/api/service/list/index'
|
|
import { listAllService } from '@/api/service/list/index'
|
|
|
import { listAreaStation } from '@/api/system/areaStation'
|
|
import { listAreaStation } from '@/api/system/areaStation'
|
|
|
|
|
+import { getMapSetting } from '@/api/system/mapSetting';
|
|
|
|
|
+import { CircleCloseFilled } from '@element-plus/icons-vue';
|
|
|
import { listStoreOnDispatch } from '@/api/system/store/index'
|
|
import { listStoreOnDispatch } from '@/api/system/store/index'
|
|
|
import { listSubOrderOnDispatch, dispatchSubOrder } from '@/api/order/subOrder/index';
|
|
import { listSubOrderOnDispatch, dispatchSubOrder } from '@/api/order/subOrder/index';
|
|
|
import { listFulfillerOnDispatch } from '@/api/fulfiller/pool/index'
|
|
import { listFulfillerOnDispatch } from '@/api/fulfiller/pool/index'
|
|
@@ -235,28 +248,31 @@ watch([() => filters.orderType, () => filters.station], () => {
|
|
|
|
|
|
|
|
const areaStationList = ref([])
|
|
const areaStationList = ref([])
|
|
|
const areaOptions = ref([])
|
|
const areaOptions = ref([])
|
|
|
-const siteOptions = ref([])
|
|
|
|
|
const regionValue = ref([])
|
|
const regionValue = ref([])
|
|
|
|
|
|
|
|
const buildTree = (data, parentId) => {
|
|
const buildTree = (data, parentId) => {
|
|
|
return (data || [])
|
|
return (data || [])
|
|
|
.filter(item => String(item.parentId) === String(parentId))
|
|
.filter(item => String(item.parentId) === String(parentId))
|
|
|
- .map(item => ({
|
|
|
|
|
- value: item.id,
|
|
|
|
|
- label: item.name,
|
|
|
|
|
- children: buildTree(data, item.id)
|
|
|
|
|
- }))
|
|
|
|
|
|
|
+ .map(item => {
|
|
|
|
|
+ const children = buildTree(data, item.id);
|
|
|
|
|
+ const node: any = {
|
|
|
|
|
+ value: item.id,
|
|
|
|
|
+ label: item.name,
|
|
|
|
|
+ // 如果不是站点且没有下级了,则不可选(防止误选区域叶子点)
|
|
|
|
|
+ disabled: Number(item.type) !== 2 && (!children || children.length === 0)
|
|
|
|
|
+ };
|
|
|
|
|
+ if (children && children.length > 0) {
|
|
|
|
|
+ node.children = children;
|
|
|
|
|
+ }
|
|
|
|
|
+ return node;
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const handleAreaChange = (value) => {
|
|
const handleAreaChange = (value) => {
|
|
|
- filters.station = undefined
|
|
|
|
|
if (value && value.length > 0) {
|
|
if (value && value.length > 0) {
|
|
|
- const areaId = value[value.length - 1]
|
|
|
|
|
- siteOptions.value = areaStationList.value
|
|
|
|
|
- .filter(item => Number(item.type) === 2 && String(item.parentId) === String(areaId))
|
|
|
|
|
- .map(item => ({ value: item.id, label: item.name }))
|
|
|
|
|
|
|
+ filters.station = value[value.length - 1];
|
|
|
} else {
|
|
} else {
|
|
|
- siteOptions.value = []
|
|
|
|
|
|
|
+ filters.station = undefined;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -265,37 +281,30 @@ const getAreaStationList = async () => {
|
|
|
const res = await listAreaStation()
|
|
const res = await listAreaStation()
|
|
|
const data = res?.data || res
|
|
const data = res?.data || res
|
|
|
areaStationList.value = Array.isArray(data) ? data : []
|
|
areaStationList.value = Array.isArray(data) ? data : []
|
|
|
- const areaData = areaStationList.value.filter(item => Number(item.type) === 0 || Number(item.type) === 1)
|
|
|
|
|
- areaOptions.value = buildTree(areaData, 0)
|
|
|
|
|
- siteOptions.value = []
|
|
|
|
|
|
|
+ areaOptions.value = buildTree(areaStationList.value, 0)
|
|
|
|
|
|
|
|
|
|
+ // 默认选中第一个站点(如果存在)
|
|
|
const allStations = areaStationList.value.filter(item => Number(item.type) === 2)
|
|
const allStations = areaStationList.value.filter(item => Number(item.type) === 2)
|
|
|
if (allStations.length > 0) {
|
|
if (allStations.length > 0) {
|
|
|
const randomStation = allStations[Math.floor(Math.random() * allStations.length)]
|
|
const randomStation = allStations[Math.floor(Math.random() * allStations.length)]
|
|
|
- const areaId = randomStation.parentId
|
|
|
|
|
-
|
|
|
|
|
const path = []
|
|
const path = []
|
|
|
- let currentId = areaId
|
|
|
|
|
|
|
+ let currentId = randomStation.id
|
|
|
while (currentId && String(currentId) !== '0') {
|
|
while (currentId && String(currentId) !== '0') {
|
|
|
path.unshift(currentId)
|
|
path.unshift(currentId)
|
|
|
- const currentArea = areaStationList.value.find(item => String(item.id) === String(currentId))
|
|
|
|
|
- if (currentArea) {
|
|
|
|
|
- currentId = currentArea.parentId
|
|
|
|
|
|
|
+ const node = areaStationList.value.find(item => String(item.id) === String(currentId))
|
|
|
|
|
+ if (node) {
|
|
|
|
|
+ currentId = node.parentId
|
|
|
} else {
|
|
} else {
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
regionValue.value = path
|
|
regionValue.value = path
|
|
|
- siteOptions.value = areaStationList.value
|
|
|
|
|
- .filter(item => Number(item.type) === 2 && String(item.parentId) === String(areaId))
|
|
|
|
|
- .map(item => ({ value: item.id, label: item.name }))
|
|
|
|
|
filters.station = randomStation.id
|
|
filters.station = randomStation.id
|
|
|
}
|
|
}
|
|
|
- } catch {
|
|
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('getAreaStationList error:', error);
|
|
|
areaStationList.value = []
|
|
areaStationList.value = []
|
|
|
areaOptions.value = []
|
|
areaOptions.value = []
|
|
|
- siteOptions.value = []
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -318,37 +327,65 @@ const handleViewRiderOrders = (rider) => {
|
|
|
|
|
|
|
|
// --- Map Logic ---
|
|
// --- Map Logic ---
|
|
|
let map = null;
|
|
let map = null;
|
|
|
-const amapKey = 'a30e76f457c14b6570925522be37565d';
|
|
|
|
|
-const securityJsCode = '531ae14ec1dff87e552e1ea51e848582';
|
|
|
|
|
|
|
+const mapLoadError = ref(false);
|
|
|
|
|
|
|
|
-const loadAMapScript = () => {
|
|
|
|
|
- // 设置安全密钥
|
|
|
|
|
- window._AMapSecurityConfig = {
|
|
|
|
|
- securityJsCode: securityJsCode,
|
|
|
|
|
- };
|
|
|
|
|
|
|
+/** 动态加载高德地图脚本 */
|
|
|
|
|
+const loadAMapScript = async (): Promise<any> => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getMapSetting(1);
|
|
|
|
|
+ if (res.code !== 200) throw new Error(res.msg);
|
|
|
|
|
+ const { apiKey, apiSecret } = res.data;
|
|
|
|
|
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
|
- if (window.AMap) {
|
|
|
|
|
- resolve(window.AMap);
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ if (!apiKey) {
|
|
|
|
|
+ throw new Error('No Map Key Configured');
|
|
|
}
|
|
}
|
|
|
- const script = document.createElement('script');
|
|
|
|
|
- script.src = `https://webapi.amap.com/maps?v=2.0&key=${amapKey}`;
|
|
|
|
|
- script.onload = () => resolve(window.AMap);
|
|
|
|
|
- script.onerror = reject;
|
|
|
|
|
- document.head.appendChild(script);
|
|
|
|
|
- });
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 设置安全密钥
|
|
|
|
|
+ (window as any)._AMapSecurityConfig = {
|
|
|
|
|
+ securityJsCode: apiSecret,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
|
+ if ((window as any).AMap) {
|
|
|
|
|
+ resolve((window as any).AMap);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const script = document.createElement('script');
|
|
|
|
|
+ script.src = `https://webapi.amap.com/maps?v=2.0&key=${apiKey}`;
|
|
|
|
|
+ script.onload = () => resolve((window as any).AMap);
|
|
|
|
|
+ script.onerror = () => reject(new Error('Script Load Error'));
|
|
|
|
|
+ document.head.appendChild(script);
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Map config fetch error:', error);
|
|
|
|
|
+ return Promise.reject(error);
|
|
|
|
|
+ }
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const initMap = () => {
|
|
|
|
|
- if (!window.AMap) return;
|
|
|
|
|
- map = new AMap.Map('amap-container', {
|
|
|
|
|
- zoom: 14,
|
|
|
|
|
- center: [116.4551, 39.9255], // Chaoyang center
|
|
|
|
|
- mapStyle: 'amap://styles/normal' // 可以更换其他样式
|
|
|
|
|
- });
|
|
|
|
|
|
|
+const initMap = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ mapLoadError.value = false;
|
|
|
|
|
+ await loadAMapScript();
|
|
|
|
|
+
|
|
|
|
|
+ const AMap = (window as any).AMap;
|
|
|
|
|
+ if (!AMap) throw new Error('AMap Init Fail');
|
|
|
|
|
+
|
|
|
|
|
+ map = new AMap.Map('amap-container', {
|
|
|
|
|
+ zoom: 14,
|
|
|
|
|
+ center: [116.4551, 39.9255], // Chaoyang center
|
|
|
|
|
+ mapStyle: 'amap://styles/normal'
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
- refreshMarkers();
|
|
|
|
|
|
|
+ refreshMarkers();
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('initMap failed', err);
|
|
|
|
|
+ mapLoadError.value = true;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 重试加载地图 */
|
|
|
|
|
+const retryLoadMap = () => {
|
|
|
|
|
+ initMap();
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const refreshMarkers = () => {
|
|
const refreshMarkers = () => {
|
|
@@ -465,14 +502,7 @@ onMounted(async () => {
|
|
|
getServiceList()
|
|
getServiceList()
|
|
|
await getAreaStationList()
|
|
await getAreaStationList()
|
|
|
if (checkPermi(['order:dispatch:map'])) {
|
|
if (checkPermi(['order:dispatch:map'])) {
|
|
|
- loadAMapScript()
|
|
|
|
|
- .then(() => {
|
|
|
|
|
- initMap();
|
|
|
|
|
- })
|
|
|
|
|
- .catch((err) => {
|
|
|
|
|
- console.error('Map loading failed', err);
|
|
|
|
|
- ElMessage.error('地图加载失败,请检查网络');
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ initMap();
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -718,6 +748,44 @@ const filteredRiders = computed(() => {
|
|
|
font-family: monospace;
|
|
font-family: monospace;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.map-error-state {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ background-color: #fefefe;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.error-content {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 40px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
|
|
|
|
+ border: 1px solid #fde2e2;
|
|
|
|
|
+ max-width: 320px;
|
|
|
|
|
+
|
|
|
|
|
+ .error-icon {
|
|
|
|
|
+ font-size: 48px;
|
|
|
|
|
+ color: #F56C6C;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .error-title {
|
|
|
|
|
+ margin: 0 0 10px 0;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .error-desc {
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ margin-bottom: 24px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/* Right Panel */
|
|
/* Right Panel */
|
|
|
.right-panel {
|
|
.right-panel {
|
|
|
width: 440px;
|
|
width: 440px;
|