|
|
@@ -28,11 +28,11 @@
|
|
|
<div class="control-group">
|
|
|
<div class="c-btn" :class="{ active: activeMapFilter === 'all' }" @click="setMapFilter('all')">全部</div>
|
|
|
<div class="c-btn red" :class="{ active: activeMapFilter === 'merchants' }"
|
|
|
- @click="setMapFilter('merchants')">商家(16)</div>
|
|
|
+ @click="setMapFilter('merchants')">商家({{ merchantList.length }})</div>
|
|
|
<div class="c-btn green" :class="{ active: activeMapFilter === 'fulfillers' }"
|
|
|
- @click="setMapFilter('fulfillers')">履约者(12)</div>
|
|
|
+ @click="setMapFilter('fulfillers')">履约者({{ ridersList.length }})</div>
|
|
|
<div class="c-btn blue" :class="{ active: activeMapFilter === 'orders' }" @click="setMapFilter('orders')">
|
|
|
- 订单(5)</div>
|
|
|
+ 订单({{ ordersList.length }})</div>
|
|
|
<div class="c-btn gray">灰色表示离线</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -43,7 +43,7 @@
|
|
|
<OrderListPanel v-model="currentOrderTab" :orders="filteredOrders" :stats="orderStats" @focus="focusMapPoint"
|
|
|
@dispatch="openDispatchDialog" />
|
|
|
|
|
|
- <RiderListPanel v-model="currentRiderTab" :riders="filteredRiders" @focus="focusMapPoint"
|
|
|
+ <RiderListPanel v-model="currentRiderTab" :riders="filteredRiders" :stats="riderStats" @focus="focusMapPoint"
|
|
|
@view-orders="handleViewRiderOrders" />
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -51,7 +51,7 @@
|
|
|
<RiderOrdersDialog v-model="riderOrdersVisible" :riderInfo="currentRiderInfo" :orders="currentRiderOrders" />
|
|
|
|
|
|
<DispatchDialog v-model="dispatchDialogVisible" :order="currentDispatchOrder" :currentRider="currentRider"
|
|
|
- :ridersList="ridersList" @submit="handleDispatchSubmit" />
|
|
|
+ @submit="handleDispatchSubmit" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -60,6 +60,9 @@ import { ref, computed, reactive, onMounted, watch } from 'vue';
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
import { listServiceOnOrder } from '@/api/service/list/index'
|
|
|
import { listOnStore as listAreaStationOnStore } from '@/api/system/areaStation'
|
|
|
+import { listStoreOnDispatch } from '@/api/system/store/index'
|
|
|
+import { listSubOrderOnDispatch, dispatchSubOrder } from '@/api/order/subOrder/index';
|
|
|
+import { listFulfillerOnDispatch } from '@/api/fulfiller/pool/index'
|
|
|
|
|
|
import OrderListPanel from './components/OrderListPanel.vue';
|
|
|
import RiderListPanel from './components/RiderListPanel.vue';
|
|
|
@@ -80,21 +83,114 @@ const currentOrderTab = ref('PendingDispatch');
|
|
|
const currentRiderTab = ref('All');
|
|
|
const activeMapFilter = ref('all');
|
|
|
|
|
|
-const ordersList = ref(dispatchMockData.ordersList);
|
|
|
-const ridersList = ref(dispatchMockData.ridersList);
|
|
|
-const merchantList = ref(dispatchMockData.merchantList);
|
|
|
-const orderStats = reactive(dispatchMockData.stats);
|
|
|
+const ordersList = ref([]);
|
|
|
+const ridersList = ref([]);
|
|
|
+const merchantList = ref([]);
|
|
|
+const orderStats = computed(() => ({
|
|
|
+ pendingDispatch: ordersList.value.filter(o => o.status === 0).length,
|
|
|
+ pendingAccept: ordersList.value.filter(o => o.status === 1).length,
|
|
|
+ processing: ordersList.value.filter(o => [2, 3].includes(o.status)).length
|
|
|
+}));
|
|
|
+
|
|
|
+const riderStats = computed(() => ({
|
|
|
+ all: ridersList.value.length,
|
|
|
+ working: ridersList.value.filter(r => r.uiStatus === 'busy').length,
|
|
|
+ resting: ridersList.value.filter(r => r.uiStatus === 'offline').length,
|
|
|
+ disabled: ridersList.value.filter(r => r.uiStatus === 'disabled').length
|
|
|
+}));
|
|
|
|
|
|
const serviceOnOrderList = ref([])
|
|
|
|
|
|
const getServiceList = () => {
|
|
|
listServiceOnOrder().then(res => {
|
|
|
- serviceOnOrderList.value = res?.data?.data || res?.data || []
|
|
|
+ serviceOnOrderList.value = res?.data || []
|
|
|
}).catch(() => {
|
|
|
serviceOnOrderList.value = []
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+const getOrdersList = () => {
|
|
|
+ listSubOrderOnDispatch({
|
|
|
+ service: filters.orderType !== 'all' ? filters.orderType : undefined,
|
|
|
+ site: filters.station
|
|
|
+ }).then(res => {
|
|
|
+ const list = (res?.data || []).map(item => {
|
|
|
+ const typeName = getServiceName(item.service);
|
|
|
+ const isTransport = item.mode === 1 || item.mode === '1';
|
|
|
+ const typeCode = isTransport ? 'transport' : typeName?.includes('喂') || typeName?.includes('遛') ? 'feeding' : 'washing';
|
|
|
+
|
|
|
+ // 模拟经纬度,如果没有真实的话。这里优先保留原有mock结构的坐标展示逻辑。
|
|
|
+ // 注意:真实场景下后端应返回 lng/lat
|
|
|
+ return {
|
|
|
+ ...item,
|
|
|
+ typeCode,
|
|
|
+ // 这里只是为了演示,如果没有坐标,地图会报错。通常真实数据会有。
|
|
|
+ lng: item.lng || (116.4 + Math.random() * 0.1),
|
|
|
+ lat: item.lat || (39.9 + Math.random() * 0.05)
|
|
|
+ };
|
|
|
+ });
|
|
|
+ ordersList.value = list;
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const getServiceName = (serviceId) => {
|
|
|
+ const item = serviceOnOrderList.value.find((i) => i.id === serviceId);
|
|
|
+ return item ? item.name : '未知服务';
|
|
|
+};
|
|
|
+
|
|
|
+const getRidersList = () => {
|
|
|
+ listFulfillerOnDispatch({
|
|
|
+ service: filters.orderType !== 'all' ? filters.orderType : undefined
|
|
|
+ }).then(res => {
|
|
|
+ ridersList.value = (res.data || []).map(r => ({
|
|
|
+ ...r,
|
|
|
+ // 转换状态:resting:休息, busy:接单中, disabled:禁用
|
|
|
+ // UI 映射:online/busy -> 接单中, offline -> 休息中, disabled -> 禁用
|
|
|
+ uiStatus: r.status === 'busy' ? 'busy' : r.status === 'resting' ? 'offline' : 'disabled',
|
|
|
+ maskPhone: r.phone ? r.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '',
|
|
|
+ // categories 处理:serviceTypes 是 ids, 需要匹配名称
|
|
|
+ categories: (r.serviceTypes || '').split(',').map(id => {
|
|
|
+ const s = serviceOnOrderList.value.find(item => String(item.id) === String(id));
|
|
|
+ return s ? s.name.substring(0, 2) : '服务';
|
|
|
+ }),
|
|
|
+ // 真实的下一单时间
|
|
|
+ nextOrderTime: r.nextOrderTime || '14:30',
|
|
|
+ pendingCount: Math.floor(Math.random() * 3),
|
|
|
+ todoCount: Math.floor(Math.random() * 5),
|
|
|
+ lng: 116.4 + Math.random() * 0.1,
|
|
|
+ lat: 39.9 + Math.random() * 0.05
|
|
|
+ }));
|
|
|
+ refreshMarkers();
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+const getMerchantList = () => {
|
|
|
+ if (!filters.station) {
|
|
|
+ merchantList.value = [];
|
|
|
+ refreshMarkers();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ listStoreOnDispatch({ site: filters.station }).then(res => {
|
|
|
+ merchantList.value = (res?.data || []).map(item => ({
|
|
|
+ ...item,
|
|
|
+ lng: item.longitude || (116.4 + Math.random() * 0.1),
|
|
|
+ lat: item.latitude || (39.9 + Math.random() * 0.05)
|
|
|
+ }));
|
|
|
+ refreshMarkers();
|
|
|
+ }).catch(() => {
|
|
|
+ merchantList.value = [];
|
|
|
+ refreshMarkers();
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+watch([() => filters.orderType, () => filters.station], () => {
|
|
|
+ getOrdersList();
|
|
|
+ if (filters.orderType) getRidersList();
|
|
|
+ getMerchantList();
|
|
|
+});
|
|
|
+
|
|
|
const areaStationList = ref([])
|
|
|
const areaOptions = ref([])
|
|
|
const siteOptions = ref([])
|
|
|
@@ -235,7 +331,7 @@ const refreshMarkers = () => {
|
|
|
<!-- Info -->
|
|
|
<div style="flex: 1; overflow: hidden;">
|
|
|
<div style="font-weight: bold; font-size: 14px; color: #333; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${m.name}</div>
|
|
|
- <div style="font-size: 12px; color: #666;">今日 <span style="color: #F56C6C; font-weight: bold; font-size: 14px;">${m.orders}</span> 单</div>
|
|
|
+ <div style="font-size: 12px; color: #666;">今日 <span style="color: #F56C6C; font-weight: bold; font-size: 14px;">0</span> 单</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<!-- Triangle -->
|
|
|
@@ -253,9 +349,7 @@ const refreshMarkers = () => {
|
|
|
if (filter === 'all' || filter === 'fulfillers') {
|
|
|
ridersList.value.forEach((r) => {
|
|
|
const pt = new BMap.Point(r.lng, r.lat);
|
|
|
- const borderColor = r.status === 'online' ? '#67C23A' : r.status === 'busy' ? '#409EFF' : '#DCDFE6';
|
|
|
- const pendingText =
|
|
|
- r.pendingCount > 0 ? `<span style="color:#67C23A;font-weight:bold;">挂${r.pendingCount}单</span>` : `<span style="color:#999;">挂0单</span>`;
|
|
|
+ const borderColor = r.uiStatus === 'busy' ? '#67C23A' : r.uiStatus === 'offline' ? '#909399' : '#F56C6C';
|
|
|
const avatar = r.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png';
|
|
|
|
|
|
const html = `
|
|
|
@@ -272,7 +366,7 @@ const refreshMarkers = () => {
|
|
|
<!-- Info -->
|
|
|
<div style="flex: 1; overflow: hidden;">
|
|
|
<div style="font-weight: bold; font-size: 13px; color: #333; margin-bottom: 2px;">[履约者]${r.name}</div>
|
|
|
- <div style="font-size: 12px; color: #999;">${pendingText}</div>
|
|
|
+ <div style="font-size: 12px; color: #999;">${r.maskPhone}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<!-- Triangle -->
|
|
|
@@ -292,7 +386,7 @@ const refreshMarkers = () => {
|
|
|
const pt = new BMap.Point(o.lng, o.lat);
|
|
|
const marker = new BMap.Marker(pt);
|
|
|
|
|
|
- const labelContent = `<div style="border:1px solid #409EFF;background:#fff;color:#409EFF;padding:2px 6px;border-radius:4px;font-size:12px;">${o.type}</div>`;
|
|
|
+ const labelContent = `<div style="border:1px solid #409EFF;background:#fff;color:#409EFF;padding:2px 6px;border-radius:4px;font-size:12px;">${getServiceName(o.service)}</div>`;
|
|
|
const label = new BMap.Label(labelContent, { position: pt, offset: new BMap.Size(-20, -35) });
|
|
|
label.setStyle({ border: 'none', background: 'transparent' });
|
|
|
|
|
|
@@ -321,9 +415,11 @@ watch([currentOrderTab, currentRiderTab], () => {
|
|
|
refreshMarkers();
|
|
|
});
|
|
|
|
|
|
-onMounted(() => {
|
|
|
+onMounted(async () => {
|
|
|
getServiceList()
|
|
|
- getAreaStationList()
|
|
|
+ await getAreaStationList()
|
|
|
+ getRidersList()
|
|
|
+ getMerchantList()
|
|
|
loadBMapScript()
|
|
|
.then(() => {
|
|
|
initMap();
|
|
|
@@ -341,37 +437,40 @@ const currentRider = ref(null);
|
|
|
|
|
|
const openDispatchDialog = (order) => {
|
|
|
currentDispatchOrder.value = order;
|
|
|
- if (order.riderId) {
|
|
|
- currentRider.value = ridersList.value.find((r) => r.id === order.riderId) || null;
|
|
|
+ if (order.fulfiller) {
|
|
|
+ currentRider.value = ridersList.value.find((r) => r.id === order.fulfiller) || null;
|
|
|
} else {
|
|
|
currentRider.value = null;
|
|
|
}
|
|
|
dispatchDialogVisible.value = true;
|
|
|
};
|
|
|
|
|
|
-const handleDispatchSubmit = (data) => {
|
|
|
- dispatchDialogVisible.value = false;
|
|
|
- ElMessage.success('派单成功');
|
|
|
-
|
|
|
- if (currentDispatchOrder.value && currentDispatchOrder.value.status === 'pending_dispatch') {
|
|
|
- const idx = ordersList.value.findIndex((o) => o.id === currentDispatchOrder.value.id);
|
|
|
- if (idx !== -1) ordersList.value[idx].status = 'pending_accept';
|
|
|
+const handleDispatchSubmit = async (data) => {
|
|
|
+ try {
|
|
|
+ const priceFen = Math.round(Number(data.fee || 0) * 100);
|
|
|
+ await dispatchSubOrder({
|
|
|
+ orderId: data.order.id,
|
|
|
+ fulfiller: data.riderId,
|
|
|
+ price: priceFen
|
|
|
+ });
|
|
|
+ ElMessage.success('派单成功');
|
|
|
+ dispatchDialogVisible.value = false;
|
|
|
+ getOrdersList(); // 重新加载订单列表
|
|
|
+ } catch (error) {
|
|
|
+ // 错误由请求拦截器处理
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const filteredOrders = computed(() => {
|
|
|
let result = ordersList.value;
|
|
|
const statusMap = {
|
|
|
- 'PendingDispatch': 'pending_dispatch',
|
|
|
- 'PendingAccept': 'pending_accept',
|
|
|
- 'Processing': 'processing'
|
|
|
+ 'PendingDispatch': [0],
|
|
|
+ 'PendingAccept': [1],
|
|
|
+ 'Processing': [2, 3]
|
|
|
};
|
|
|
const targetStatus = statusMap[currentOrderTab.value];
|
|
|
if (targetStatus) {
|
|
|
- result = result.filter((o) => o.status === targetStatus);
|
|
|
- }
|
|
|
- if (filters.orderType !== 'all') {
|
|
|
- result = result.filter((o) => String(o.serviceId || o.service) === String(filters.orderType));
|
|
|
+ result = result.filter((o) => targetStatus.includes(o.status));
|
|
|
}
|
|
|
return result;
|
|
|
});
|
|
|
@@ -379,8 +478,8 @@ const filteredOrders = computed(() => {
|
|
|
const filteredRiders = computed(() => {
|
|
|
if (currentRiderTab.value === 'All') return ridersList.value;
|
|
|
const map = {
|
|
|
- 'Working': ['online', 'busy'],
|
|
|
- 'Resting': ['offline'],
|
|
|
+ 'Working': ['busy'],
|
|
|
+ 'Resting': ['resting'],
|
|
|
'Disabled': ['disabled']
|
|
|
};
|
|
|
const allowed = map[currentRiderTab.value] || [];
|