Przeglądaj źródła

地图调度基本完成

Huanyi 1 miesiąc temu
rodzic
commit
d0946bc232

+ 12 - 0
src/api/fulfiller/pool/index.ts

@@ -3,6 +3,7 @@ import { AxiosPromise } from 'axios';
 import {
   FlfFulfillerVO, FlfFulfillerForm, FlfFulfillerQuery,
   FlfFulfillerOnOrderVO, FlfFulfillerOnOrderQuery,
+  FlfFulfillerOnDispatchVO,
   FlfRewardForm, FlfAdjustPointsForm, FlfAdjustBalanceForm,
   FlfPointsLogVO, FlfBalanceLogVO, FlfRewardLogVO
 } from './types';
@@ -29,6 +30,17 @@ export const pageFulfillerOnOrder = (query?: FlfFulfillerOnOrderQuery): AxiosPro
   });
 };
 
+/**
+ * 派单页面查询履约者列表(不分页)
+ */
+export const listFulfillerOnDispatch = (query?: { service?: string | number }): AxiosPromise<FlfFulfillerOnDispatchVO[]> => {
+  return request({
+    url: '/fulfiller/fulfiller/listAllOnDispatch',
+    method: 'get',
+    params: query
+  });
+};
+
 /**
  * 查询履约者详细
  */

+ 12 - 0
src/api/fulfiller/pool/types.ts

@@ -99,6 +99,7 @@ export interface FlfFulfillerOnOrderQuery {
   content?: string;
   pageNum?: number;
   pageSize?: number;
+  service?: string | number;
 }
 
 /**
@@ -177,3 +178,14 @@ export interface FlfRewardLogVO {
   operatorName: string;
   createTime: string;
 }
+
+export interface FlfFulfillerOnDispatchVO {
+  id: string | number;
+  name: string;
+  avatar: string;
+  status: string;
+  phone: string;
+  tags: (string | number)[];
+  serviceTypes: string;
+  nextOrderTime?: string;
+}

+ 12 - 1
src/api/order/subOrder/index.ts

@@ -1,6 +1,17 @@
 import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
-import { SubOrderVO, SubOrderQuery } from './types';
+import { SubOrderVO, SubOrderQuery, SubOrderDispatchQuery } from './types';
+
+/**
+ * 派单中心查询子订单列表
+ */
+export const listSubOrderOnDispatch = (query?: SubOrderDispatchQuery): AxiosPromise<SubOrderVO[]> => {
+    return request({
+        url: '/order/subOrder/listOnDispatch',
+        method: 'get',
+        params: query
+    });
+};
 
 /**
  * 查询子订单列表

+ 5 - 0
src/api/order/subOrder/types.ts

@@ -6,6 +6,11 @@ export interface SubOrderQuery {
     pageSize: number;
 }
 
+export interface SubOrderDispatchQuery {
+    service?: number | string;
+    site?: number | string;
+}
+
 export interface SubOrderVO {
     id: number;
     code: string;

+ 13 - 1
src/api/system/store/index.ts

@@ -1,6 +1,6 @@
 import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
-import { StoreVO, StoreForm, StoreQuery, StoreStatusVO, SysStorePageBo, StoreOrderQuery, StoreOrderVO } from '@/api/system/store/types';
+import { StoreVO, StoreForm, StoreQuery, StoreStatusVO, SysStorePageBo, StoreOrderQuery, StoreOrderVO, StoreDispatchVO } from '@/api/system/store/types';
 
 /**
  * 查询门店管理列表
@@ -85,3 +85,15 @@ export const listStoreOnOrder = (query?: StoreOrderQuery): AxiosPromise<{ total:
   });
 };
 
+/**
+ * 调度时查询门店列表
+ * @param query
+ */
+export const listStoreOnDispatch = (query?: { site?: string | number }): AxiosPromise<StoreDispatchVO[]> => {
+  return request({
+    url: '/system/store/listOnDispatch',
+    method: 'get',
+    params: query
+  });
+};
+

+ 9 - 0
src/api/system/store/types.ts

@@ -258,3 +258,12 @@ export interface StoreOrderVO {
   name: string;
   services: number[];
 }
+
+export interface StoreDispatchVO {
+  id: number;
+  name: string;
+  site: number;
+  areaCode: string;
+  longitude: number;
+  latitude: number;
+}

+ 2 - 1
src/views/fulfiller/pool/index.vue

@@ -750,7 +750,8 @@ const getSubTypeName = (type: string) => {
     other: '其他',
     admin_adjust: '后台调整',
     admin_reward: '后台奖励',
-    admin_punish: '后台惩罚'
+    admin_punish: '后台惩罚',
+    order: '订单完成'
   }
   return map[type] || type
 }

+ 100 - 37
src/views/order/dispatch/components/DispatchDialog.vue

@@ -11,20 +11,20 @@
           </div>
           <div class="card-main">
             <template v-if="order.typeCode === 'transport'">
-              <div class="row-addr" :title="order.pickAddr">
-                <span class="tag pick">取</span> {{ order.pickAddr }}
+              <div class="row-addr" :title="order.fromAddress">
+                <span class="tag pick">取</span> {{ order.fromAddress }}
               </div>
-              <div class="row-addr" :title="order.dropAddr">
-                <span class="tag drop">送</span> {{ order.dropAddr }}
+              <div class="row-addr" :title="order.toAddress">
+                <span class="tag drop">送</span> {{ order.toAddress }}
               </div>
             </template>
             <template v-else>
-              <div class="row-addr" :title="order.address"><span class="tag home">址</span> {{ order.address }}</div>
+              <div class="row-addr" :title="order.toAddress"><span class="tag home">址</span> {{ order.toAddress }}</div>
             </template>
             <div class="row-time" style="margin-top: 4px">
               <el-icon>
                 <Clock />
-              </el-icon> {{ order.time }}
+              </el-icon> {{ order.serviceTime }}
               <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
             </div>
           </div>
@@ -55,15 +55,13 @@
             </div>
 
             <div class="row-2 categories-row" style="margin-top: 6px; display: flex; gap: 4px; flex-wrap: wrap">
-              <span v-for="cat in currentRider.categories" :key="cat" class="cat-tag" :class="getCategoryClass(cat)">{{
-                cat
-                }}</span>
+              <el-tag v-for="cat in (currentRider.tags || [])" :key="cat" size="small" :type="getTagType(cat)" effect="plain">{{
+                getTagText(cat)
+                }}</el-tag>
             </div>
 
             <div class="row-3 time-row" style="margin-top: 4px">
-              <span class="last-time">下一单: {{ currentRider.status === 'offline' || currentRider.status === 'disabled' ?
-                '--' :
-                currentRider.lastServiceTime }}</span>
+              <span class="last-time">下一单: {{ currentRider.nextOrderTime || '14:30' }}</span>
             </div>
           </div>
         </div>
@@ -72,7 +70,7 @@
       <!-- Middle: Rider Selection -->
       <div class="dispatch-rider-select">
         <div class="select-header">
-          <span class="tit">选择履约者 (下一单时间由近及远排序)</span>
+          <span class="tit">选择履约者</span>
           <el-input v-model="searchQuery" placeholder="搜索履约者姓名/手机号" prefix-icon="Search" clearable
             style="width: 240px" />
         </div>
@@ -98,14 +96,13 @@
                   </div>
 
                   <div class="row-2 categories-row" style="margin-top: 6px; display: flex; gap: 4px; flex-wrap: wrap">
-                    <span v-for="cat in rider.categories" :key="cat" class="cat-tag" :class="getCategoryClass(cat)">{{
-                      cat
-                      }}</span>
+                    <el-tag v-for="cat in (rider.tags || [])" :key="cat" size="small" :type="getTagType(cat)" effect="plain">{{
+                      getTagText(cat)
+                      }}</el-tag>
                   </div>
 
                   <div class="row-3 time-row" style="margin-top: 4px">
-                    <span class="last-time">下一单: {{ rider.status === 'offline' || rider.status === 'disabled' ? '--' :
-                      rider.lastServiceTime }}</span>
+                    <span class="last-time">下一单: {{ rider.nextOrderTime || '14:30' }}</span>
                   </div>
                 </div>
 
@@ -121,6 +118,12 @@
             </div>
           </el-scrollbar>
         </div>
+
+        <div class="rider-pagination" style="margin-top: 20px;">
+          <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize"
+            :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" :total="total"
+            @current-change="loadRiders" @size-change="handlePageSizeChange" />
+        </div>
       </div>
 
       <!-- Bottom: Fee & Submit -->
@@ -143,12 +146,13 @@
 import { ref, computed, watch } from 'vue';
 import { ElMessage } from 'element-plus';
 import { Check, Clock, Search } from '@element-plus/icons-vue';
+import { pageFulfillerOnOrder } from '@/api/fulfiller/pool';
+import { listAllTag } from '@/api/fulfiller/tag';
 
 const props = defineProps({
   modelValue: { type: Boolean, default: false },
   order: { type: Object, default: () => null },
-  currentRider: { type: Object, default: () => null },
-  ridersList: { type: Array, default: () => [] }
+  currentRider: { type: Object, default: () => null }
 });
 
 const emit = defineEmits(['update:modelValue', 'submit']);
@@ -162,27 +166,75 @@ const searchQuery = ref('');
 const selectedId = ref(null);
 const fee = ref(0);
 
+const ridersList = ref([]);
+const total = ref(0);
+const pageNum = ref(1);
+const pageSize = ref(10);
+
+const allTags = ref([]);
+const tagMap = computed(() => {
+  const map = {};
+  for (const t of (allTags.value || [])) {
+    if (t && t.id !== undefined && t.id !== null) map[t.id] = t;
+  }
+  return map;
+});
+
+const loadAllTags = async () => {
+  if (allTags.value && allTags.value.length > 0) return;
+  try {
+    const res = await listAllTag({ category: 'fulfiller' });
+    allTags.value = res?.data || [];
+  } catch {
+    allTags.value = [];
+  }
+};
+
+const loadRiders = async () => {
+  try {
+    const res = await pageFulfillerOnOrder({
+      content: searchQuery.value || undefined,
+      pageNum: pageNum.value,
+      pageSize: pageSize.value,
+      service: props.order?.service
+    });
+    const list = res?.rows || [];
+    ridersList.value = list.map(r => ({
+      ...r,
+      nextOrderTime: r.nextOrderTime || '14:30' // 使用假数据代替下一单时间
+    }));
+    total.value = res?.total || 0;
+  } catch {
+    ridersList.value = [];
+    total.value = 0;
+  }
+};
+
+const handlePageSizeChange = (size) => {
+  pageSize.value = size;
+  pageNum.value = 1;
+  loadRiders();
+};
+
+watch(searchQuery, () => {
+  pageNum.value = 1;
+  loadRiders();
+});
+
 // Reset form when dialog opens
 watch(visible, (val) => {
   if (val) {
     searchQuery.value = '';
     selectedId.value = null;
     fee.value = 0;
+    pageNum.value = 1;
+    loadAllTags();
+    loadRiders();
   }
 });
 
 const filteredRiders = computed(() => {
-  let result = props.ridersList.filter((r) => r.status === 'online' || r.status === 'busy');
-
-  if (searchQuery.value) {
-    const q = searchQuery.value.toLowerCase();
-    result = result.filter((r) => r.name.includes(q) || r.phone.includes(q));
-  }
-
-  result.sort((a, b) => {
-    return a.lastServiceTime.localeCompare(b.lastServiceTime);
-  });
-  return result;
+  return ridersList.value || [];
 });
 
 const handleSubmit = () => {
@@ -198,18 +250,29 @@ const handleSubmit = () => {
 };
 
 // Helpers
+const getTagText = (tagId) => {
+  const t = tagMap.value?.[tagId];
+  return t?.name || String(tagId);
+};
+const getTagType = (tagId) => {
+  const t = tagMap.value?.[tagId];
+  const type = t?.colorType;
+  if (type === 'success' || type === 'warning' || type === 'danger' || type === 'info') return type;
+  return '';
+};
 const getShortType = (code) => {
   const map = { 'transport': '接送', 'feeding': '喂遛', 'washing': '洗护' };
   return map[code] || '订单';
 };
 const getRiderStatusText = (status) => {
-  const map = { 'online': '接单中', 'busy': '接单中', 'offline': '休息中', 'disabled': '禁用' };
-  return map[status];
-};
-const getCategoryClass = (cat) => {
-  const map = { '接送': 'cat-transport', '喂遛': 'cat-feeding', '洗护': 'cat-washing' };
-  return map[cat] || '';
+  const statusMap = {
+    resting: '休息',
+    busy: '接单中',
+    disabled: '禁用'
+  };
+  return statusMap[status] || status;
 };
+
 </script>
 
 <style scoped>

+ 11 - 11
src/views/order/dispatch/components/OrderListPanel.vue

@@ -31,17 +31,17 @@
           </div>
           <div class="card-main">
             <template v-if="order.typeCode === 'transport'">
-              <div class="row-addr" :title="order.pickAddr"><span class="tag pick">取</span> {{ order.pickAddr }}</div>
-              <div class="row-addr" :title="order.dropAddr"><span class="tag drop">送</span> {{ order.dropAddr }}</div>
+              <div class="row-addr" :title="order.fromAddress"><span class="tag pick">起</span> {{ order.fromAddress }}</div>
+              <div class="row-addr" :title="order.toAddress"><span class="tag drop">终</span> {{ order.toAddress }}</div>
               <div class="row-time">
-                <el-icon><Clock /></el-icon> {{ order.time }}
+                <el-icon><Clock /></el-icon> {{ order.serviceTime }}
                 <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
               </div>
             </template>
             <template v-else>
-              <div class="row-addr" :title="order.address"><span class="tag home">址</span> {{ order.address }}</div>
+              <div class="row-addr" :title="order.toAddress"><span class="tag home">终</span> {{ order.toAddress }}</div>
               <div class="row-time" style="margin-top: 4px">
-                <el-icon><Clock /></el-icon> {{ order.time }}
+                <el-icon><Clock /></el-icon> {{ order.serviceTime }}
                 <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
               </div>
             </template>
@@ -51,11 +51,11 @@
           <div class="card-right">
             <el-tag size="small" :type="getOrderStatusType(order.status)" effect="plain">{{ getOrderStatusText(order.status) }}</el-tag>
             <div class="actions">
-              <el-button v-if="order.status === 'pending_dispatch'" type="primary" size="small" @click.stop="$emit('dispatch', order)"
+              <el-button v-if="order.status === 0" type="primary" size="small" @click.stop="$emit('dispatch', order)"
                 >派单</el-button
               >
               <el-button
-                v-else-if="['pending_accept', 'processing'].includes(order.status)"
+                v-else-if="[1, 2, 3].includes(order.status)"
                 type="primary"
                 size="small"
                 plain
@@ -92,12 +92,12 @@ const getShortType = (code) => {
   return map[code] || '订单';
 };
 const getOrderStatusText = (status) => {
-  const map = { 'pending_dispatch': '待派单', 'pending_accept': '待接单', 'processing': '进行中', 'completed': '已完成' };
-  return map[status];
+  const map = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '待商家确认', 5: '已完成', 6: '已取消' };
+  return map[status] || '未知';
 };
 const getOrderStatusType = (status) => {
-  const map = { 'pending_dispatch': 'danger', 'pending_accept': 'warning', 'processing': 'primary', 'completed': 'success' };
-  return map[status];
+  const map = { 0: 'danger', 1: 'warning', 2: 'primary', 3: 'primary', 4: 'warning', 5: 'success', 6: 'info' };
+  return map[status] || 'info';
 };
 </script>
 

+ 11 - 12
src/views/order/dispatch/components/RiderListPanel.vue

@@ -6,19 +6,19 @@
       <div class="header-right-tabs">
         <span class="h-tab-item" :class="{ active: currentTab === 'All' }" @click="currentTab = 'All'">
           <span class="txt">全部</span>
-          <span class="num">12</span>
+          <span class="num">{{ stats.all }}</span>
         </span>
         <span class="h-tab-item" :class="{ active: currentTab === 'Working' }" @click="currentTab = 'Working'">
           <span class="txt">接单中</span>
-          <span class="num success">8</span>
+          <span class="num success">{{ stats.working }}</span>
         </span>
         <span class="h-tab-item" :class="{ active: currentTab === 'Resting' }" @click="currentTab = 'Resting'">
           <span class="txt">休息中</span>
-          <span class="num info">3</span>
+          <span class="num info">{{ stats.resting }}</span>
         </span>
         <span class="h-tab-item" :class="{ active: currentTab === 'Disabled' }" @click="currentTab = 'Disabled'">
           <span class="txt">禁用</span>
-          <span class="num danger">1</span>
+          <span class="num danger">{{ stats.disabled }}</span>
         </span>
       </div>
     </div>
@@ -29,7 +29,7 @@
         <div v-for="rider in riders" :key="rider.id" class="list-card rider-card" @click="$emit('focus', rider.lng, rider.lat)">
           <div class="card-left relative">
             <el-avatar :src="rider.avatar" :size="40" />
-            <div class="dot" :class="rider.status"></div>
+            <div class="dot" :class="rider.uiStatus"></div>
           </div>
           <div class="card-main">
             <!-- Box 1: Name + Phone + Status (Right) -->
@@ -39,7 +39,7 @@
                 <span class="r-phone">{{ rider.maskPhone }}</span>
               </div>
               <div class="status-right">
-                <span class="status-badge" :class="rider.status">{{ getRiderStatusText(rider.status) }}</span>
+                <span class="status-badge" :class="rider.uiStatus">{{ getRiderStatusText(rider.uiStatus) }}</span>
               </div>
             </div>
 
@@ -50,9 +50,7 @@
 
             <!-- Box 3: Last Service Time -->
             <div class="row-3 time-row" style="margin-top: 4px">
-              <span class="last-time"
-                >下一单: {{ rider.status === 'offline' || rider.status === 'disabled' ? '--' : rider.lastServiceTime }}</span
-              >
+              <span class="last-time">下一单: {{ rider.nextOrderTime || '14:30' }}</span>
             </div>
           </div>
           <div class="card-right-stats">
@@ -79,7 +77,8 @@ import { computed } from 'vue';
 
 const props = defineProps({
   modelValue: { type: String, default: 'All' },
-  riders: { type: Array, default: () => [] }
+  riders: { type: Array, default: () => [] },
+  stats: { type: Object, default: () => ({ all: 0, working: 0, resting: 0, disabled: 0 }) }
 });
 
 const emit = defineEmits(['update:modelValue', 'focus', 'view-orders']);
@@ -90,8 +89,8 @@ const currentTab = computed({
 });
 
 const getRiderStatusText = (status) => {
-  const map = { 'online': '接单中', 'busy': '接单中', 'offline': '休息中', 'disabled': '禁用' };
-  return map[status];
+  const map = { 'busy': '接单中', 'offline': '休息中', 'disabled': '禁用' };
+  return map[status] || '未知';
 };
 const getCategoryClass = (cat) => {
   const map = { '接送': 'cat-transport', '喂遛': 'cat-feeding', '洗护': 'cat-washing' };

+ 135 - 36
src/views/order/dispatch/index.vue

@@ -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] || [];

+ 18 - 5
src/views/order/orderList/components/DispatchDialog.vue

@@ -11,9 +11,11 @@
                     </div>
                     <div class="card-main">
                         <template v-if="order.typeCode === 'transport'">
-                            <div class="row-addr" :title="order.toAddress || order.pickAddr || order.dropAddr">
-                                <span class="tag home">址</span> {{ order.toAddress || order.pickAddr || order.dropAddr
-                                }}
+                            <div class="row-addr" :title="order.pickAddr">
+                                <span class="tag pick">取</span> {{ order.pickAddr }}
+                            </div>
+                            <div class="row-addr" :title="order.dropAddr">
+                                <span class="tag drop">送</span> {{ order.dropAddr }}
                             </div>
                         </template>
                         <template v-else>
@@ -58,6 +60,9 @@
                             <el-tag v-for="cat in (currentRider.tags || [])" :key="cat" size="small"
                                 :type="getTagType(cat)" effect="plain">{{ getTagText(cat) }}</el-tag>
                         </div>
+                        <div class="row-3 time-row" style="margin-top: 4px;">
+                            <span class="last-time">下一单: {{ currentRider.nextOrderTime || '14:30' }}</span>
+                        </div>
                     </div>
                 </div>
             </div>
@@ -96,6 +101,9 @@
                                         <el-tag v-for="cat in (rider.tags || [])" :key="cat" size="small"
                                             :type="getTagType(cat)" effect="plain">{{ getTagText(cat) }}</el-tag>
                                     </div>
+                                    <div class="row-3 time-row" style="margin-top: 4px">
+                                        <span class="last-time">下一单: {{ rider.nextOrderTime || '14:30' }}</span>
+                                    </div>
                                 </div>
 
                                 <!-- Selected Check -->
@@ -186,9 +194,14 @@ const loadRiders = async () => {
         const res = await pageFulfillerOnOrder({
             content: dispatchSearchQuery.value || undefined,
             pageNum: pageNum.value,
-            pageSize: pageSize.value
+            pageSize: pageSize.value,
+            service: props.order?.service
         })
-        ridersList.value = res?.rows || []
+        const list = res?.rows || []
+        ridersList.value = list.map(r => ({
+            ...r,
+            nextOrderTime: r.nextOrderTime || '14:30'
+        }))
         total.value = res?.total || 0
 
         if (props.order?.riderId) {

+ 1 - 0
src/views/order/orderList/index.vue

@@ -513,6 +513,7 @@ const openDispatchDialog = (row) => {
     address,
     pickAddr,
     dropAddr,
+    service: row.service,
     riderId: row.riderId || row.fulfiller || null
   };
   currentDispatchOrder.value = orderObj;

+ 19 - 3
src/views/order/purchase/index.vue

@@ -38,6 +38,7 @@
                     </template>
                     <PageSelect v-model="form.userId" placeholder="搜索姓名/手机号" size="large" style="width: 100%"
                       :options="userSelectOptions" :total="userTotal" :page-size="5" :filter-method="searchUser"
+                      :loading="userLoading" :popper-class="!userQuery.content ? 'hide-search-popper' : ''"
                       @page-change="handleUserPageChange" @update:modelValue="handleUserChange" />
                   </el-form-item>
                 </el-col>
@@ -215,7 +216,7 @@ import { createOrder } from '@/api/order/order'
 // --- State ---
 const userOptions = ref([])
 const userTotal = ref(0)
-const userQuery = reactive({ pageNum: 1, pageSize: 5, name: '' })
+const userQuery = reactive({ pageNum: 1, pageSize: 5, content: '' })
 const userLoading = ref(false)
 
 const serviceList = [
@@ -482,9 +483,12 @@ const canSubmit = computed(() => {
 
 // --- Methods ---
 const fetchUsers = () => {
+  userLoading.value = true
   listCustomerOnOrder(userQuery).then(res => {
     userOptions.value = res.rows || []
     userTotal.value = res.total || 0
+  }).finally(() => {
+    userLoading.value = false
   })
 }
 
@@ -494,7 +498,12 @@ const handleUserPageChange = (page) => {
 }
 
 const searchUser = (query) => {
-  userQuery.name = query || ''
+  if (!query) {
+    userOptions.value = []
+    userTotal.value = 0
+    return
+  }
+  userQuery.content = query || ''
   userQuery.pageNum = 1
   fetchUsers()
 }
@@ -659,7 +668,7 @@ const handleSubmit = async () => {
 // Initialize
 onMounted(() => {
   fetchStores()
-  fetchUsers()
+  // fetchUsers() // 移除初始加载,改为输入后触发
   listServiceOnOrder().then(res => {
     allServices.value = res.data || []
   })
@@ -1050,3 +1059,10 @@ onMounted(() => {
   border-radius: 22px;
 }
 </style>
+
+<style>
+/* 全局样式:用于在搜索框无内容时强行隐藏 PageSelect 的下拉弹窗 */
+.hide-search-popper {
+  display: none !important;
+}
+</style>