Просмотр исходного кода

Merge branch 'master' of http://8.152.4.3:3000/yp_other/pet-system-admin-web

steelwei 1 месяц назад
Родитель
Сommit
198aee070b
34 измененных файлов с 6223 добавлено и 1090 удалено
  1. 23 1
      src/api/archieves/customer/index.ts
  2. 12 0
      src/api/archieves/customer/types.ts
  3. 11 0
      src/api/archieves/pet/index.ts
  4. 24 0
      src/api/fulfiller/pool/index.ts
  5. 26 0
      src/api/fulfiller/pool/types.ts
  6. 15 0
      src/api/order/order/index.ts
  7. 24 0
      src/api/order/order/types.ts
  8. 75 0
      src/api/order/subOrder/index.ts
  9. 44 0
      src/api/order/subOrder/types.ts
  10. 11 0
      src/api/order/subOrderLog/index.ts
  11. 15 0
      src/api/order/subOrderLog/types.ts
  12. 11 0
      src/api/service/list/types.ts
  13. 13 1
      src/api/system/store/index.ts
  14. 9 0
      src/api/system/store/types.ts
  15. 31 0
      src/mock/RiderOrdersDialog.json
  16. 199 0
      src/mock/dispatch.json
  17. 3 0
      src/types/components.d.ts
  18. 2 1
      src/views/fulfiller/pool/index.vue
  19. 599 0
      src/views/order/dispatch/components/DispatchDialog.vue
  20. 295 0
      src/views/order/dispatch/components/OrderListPanel.vue
  21. 316 0
      src/views/order/dispatch/components/RiderListPanel.vue
  22. 117 0
      src/views/order/dispatch/components/RiderOrdersDialog.vue
  23. 251 619
      src/views/order/dispatch/index.vue
  24. 409 0
      src/views/order/orderList/components/CareSummaryDrawer.vue
  25. 591 0
      src/views/order/orderList/components/DispatchDialog.vue
  26. 933 0
      src/views/order/orderList/components/OrderDetailDrawer.vue
  27. 47 0
      src/views/order/orderList/components/RemarkDialog.vue
  28. 101 0
      src/views/order/orderList/components/RewardDialog.vue
  29. 887 0
      src/views/order/orderList/index.vue
  30. 230 163
      src/views/order/purchase/components/AddPetDialog.vue
  31. 228 79
      src/views/order/purchase/components/AddUserDialog.vue
  32. 67 67
      src/views/order/purchase/components/TransportForm.vue
  33. 599 158
      src/views/order/purchase/index.vue
  34. 5 1
      src/views/service/list/index.vue

+ 23 - 1
src/api/archieves/customer/index.ts

@@ -1,6 +1,6 @@
 import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
-import { UsrCustomerVO, UsrCustomerForm, UsrCustomerQuery } from '@/api/archieves/customer/types';
+import { UsrCustomerVO, UsrCustomerForm, UsrCustomerQuery, CustomerOnOrderVO, CustomerOnOrderQuery } from '@/api/archieves/customer/types';
 
 /**
  * 查询用户列表
@@ -45,6 +45,17 @@ export const addCustomer = (data: UsrCustomerForm) => {
   });
 };
 
+/**
+ * 下单页新增用户
+ */
+export const addCustomerOnOrder = (data: UsrCustomerForm) => {
+  return request({
+    url: '/archieves/customer/addOnOrder',
+    method: 'post',
+    data: data
+  });
+};
+
 /**
  * 修改用户
  */
@@ -76,3 +87,14 @@ export const changeCustomerStatus = (id: string | number, status: number) => {
     params: { id, status }
   });
 };
+
+/**
+ * 下单页宠主列表
+ */
+export const listCustomerOnOrder = (query?: CustomerOnOrderQuery): AxiosPromise<CustomerOnOrderVO[]> => {
+  return request({
+    url: '/archieves/customer/listOnOrder',
+    method: 'get',
+    params: query
+  });
+};

+ 12 - 0
src/api/archieves/customer/types.ts

@@ -49,3 +49,15 @@ export interface UsrCustomerQuery extends PageQuery {
   stationId?: number;
   status?: number;
 }
+
+export interface CustomerOnOrderVO {
+  id: number;
+  name: string;
+  phoneNumber: string;
+  regionCode: string;
+  address: string;
+}
+
+export interface CustomerOnOrderQuery extends PageQuery {
+  content?: string;
+}

+ 11 - 0
src/api/archieves/pet/index.ts

@@ -44,6 +44,17 @@ export const addPet = (data: UsrPetForm) => {
   });
 };
 
+/**
+ * 下单页新增宠物
+ */
+export const addPetOnOrder = (data: UsrPetForm) => {
+  return request({
+    url: '/archieves/pet/addOnOrder',
+    method: 'post',
+    data: data
+  });
+};
+
 /**
  * 修改宠物
  */

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

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

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

@@ -87,6 +87,21 @@ export interface FlfFulfillerQuery extends PageQuery {
   workType?: string;
 }
 
+export interface FlfFulfillerOnOrderVO {
+  id: string | number;
+  name?: string;
+  avatar?: string;
+  phone?: string;
+  tags?: Array<string | number>;
+}
+
+export interface FlfFulfillerOnOrderQuery {
+  content?: string;
+  pageNum?: number;
+  pageSize?: number;
+  service?: string | number;
+}
+
 /**
  * 奖惩操作
  */
@@ -163,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;
+}

+ 15 - 0
src/api/order/order/index.ts

@@ -0,0 +1,15 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { CreateOrderDTO } from './types';
+
+/**
+ * 创建订单
+ * @param data 参数
+ */
+export const createOrder = (data: CreateOrderDTO): AxiosPromise<any> => {
+    return request({
+        url: '/order/order/create',
+        method: 'post',
+        data: data
+    });
+};

+ 24 - 0
src/api/order/order/types.ts

@@ -0,0 +1,24 @@
+export interface CreateSubOrderDTO {
+    mode: number | string;
+    type: number | string;
+    contact: string;
+    contactPhoneNumber: string;
+    serviceTime: string;
+    endServiceTime: string;
+    fromCode?: string;
+    fromAddress?: string;
+    toCode?: string;
+    toAddress?: string;
+}
+
+export interface CreateOrderDTO {
+    store: number | string;
+    storeSite: number | string;
+    customer: number | string;
+    pet: number | string;
+    groupPurchasePackageName?: string;
+    service: number | string;
+    remark?: string;
+    tenantId?: string;
+    subOrders: CreateSubOrderDTO[];
+}

+ 75 - 0
src/api/order/subOrder/index.ts

@@ -0,0 +1,75 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { SubOrderVO, SubOrderQuery, SubOrderDispatchQuery } from './types';
+
+/**
+ * 派单中心查询子订单列表
+ */
+export const listSubOrderOnDispatch = (query?: SubOrderDispatchQuery): AxiosPromise<SubOrderVO[]> => {
+    return request({
+        url: '/order/subOrder/listOnDispatch',
+        method: 'get',
+        params: query
+    });
+};
+
+/**
+ * 查询子订单列表
+ * @param query
+ * @returns {*}
+ */
+export const listSubOrder = (query?: SubOrderQuery): AxiosPromise<{ total: number, rows: SubOrderVO[] }> => {
+    return request({
+        url: '/order/subOrder/list',
+        method: 'get',
+        params: query
+    });
+};
+
+export const dispatchSubOrder = (data: { orderId: string | number; fulfiller: string | number; price: number; }) => {
+    return request({
+        url: '/order/subOrder/dispatch',
+        method: 'put',
+        data
+    });
+};
+
+export const getSubOrderInfo = (id: string | number): AxiosPromise<SubOrderVO> => {
+    return request({
+        url: '/order/subOrder/getInfo',
+        method: 'get',
+        params: { id }
+    });
+};
+
+export const cancelSubOrder = (data: { orderId: string | number; }) => {
+    return request({
+        url: '/order/subOrder/cancel',
+        method: 'put',
+        data
+    });
+};
+
+export const remarkSubOrder = (data: { orderId: string | number; remark: string; }) => {
+    return request({
+        url: '/order/subOrder/remark',
+        method: 'put',
+        data
+    });
+};
+
+export const confirmSubOrder = (data: { id: string | number; }) => {
+    return request({
+        url: '/order/subOrder/confirm',
+        method: 'put',
+        data
+    });
+};
+
+export const nursingSummarySubOrder = (data: { orderId: string | number; content: string; }) => {
+    return request({
+        url: '/order/subOrder/nursingSummary',
+        method: 'put',
+        data
+    });
+};

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

@@ -0,0 +1,44 @@
+export interface SubOrderQuery {
+    status?: number | string;
+    service?: number | string;
+    content?: string;
+    pageNum: number;
+    pageSize: number;
+}
+
+export interface SubOrderDispatchQuery {
+    service?: number | string;
+    site?: number | string;
+}
+
+export interface SubOrderVO {
+    id: number;
+    code: string;
+    service: number;
+    pet: number;
+    petName: string;
+    petType: string;
+    customer: number;
+    customerName: string;
+    site: number;
+    store: number;
+    storeName: string;
+    platformId?: number;
+    placer: number;
+    placerUsername: string;
+    createTime: string;
+    status: number;
+    fulfiller: number;
+    fulfillerName: string;
+    fulfillerStatus?: 'resting' | 'busy' | 'disabled';
+    price: number;
+    // 以下为可能需要用到的扩充字段以兼顾页面展现
+    type?: string;
+    transportType?: string;
+    splitType?: string;
+    detail?: any;
+    serviceTime?: string;
+    remark?: string;
+    nursingSummary?: string;
+    nursingSummaryTime?: string;
+}

+ 11 - 0
src/api/order/subOrderLog/index.ts

@@ -0,0 +1,11 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { SubOrderLogVO, SubOrderLogQuery } from './types';
+
+export const listSubOrderLog = (query: SubOrderLogQuery): AxiosPromise<SubOrderLogVO[]> => {
+    return request({
+        url: '/order/subOrderLog/list',
+        method: 'get',
+        params: query
+    });
+};

+ 15 - 0
src/api/order/subOrderLog/types.ts

@@ -0,0 +1,15 @@
+export interface SubOrderLogVO {
+    id: number;
+    subOrderId: number;
+    actioner: number;
+    actionerType: number;
+    logType: number;
+    actionType: number;
+    title: string;
+    content: string;
+    photos?: string;
+}
+
+export interface SubOrderLogQuery {
+    orderId: string | number;
+}

+ 11 - 0
src/api/service/list/types.ts

@@ -33,6 +33,11 @@ export interface ServiceVO {
    */
   remark: string;
 
+  /**
+   * 打卡备注信息
+   */
+  clockInRemark: string;
+
   /**
    * 创建时间
    */
@@ -69,6 +74,11 @@ export interface ServiceForm extends BaseEntity {
    * 备注说明
    */
   remark?: string;
+
+  /**
+   * 打卡备注信息
+   */
+  clockInRemark?: string;
 }
 
 export interface ServiceQuery extends PageQuery {
@@ -87,5 +97,6 @@ export interface ServiceOrderVO {
   id: number;
   name: string;
   remark: string;
+  icon: string;
   mode: number;
 }

+ 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;
+}

+ 31 - 0
src/mock/RiderOrdersDialog.json

@@ -0,0 +1,31 @@
+{
+    "orders": [
+        {
+            "orderNo": "ORD20240205001",
+            "serviceName": "上门喂遛",
+            "petName": "金毛(大黄)",
+            "customerName": "张先生",
+            "address": "北京市朝阳区三里屯SOHO A座 1202",
+            "time": "2024-02-05 14:00",
+            "status": "pending_accept"
+        },
+        {
+            "orderNo": "ORD20240205002",
+            "serviceName": "宠物接送",
+            "petName": "布偶(咪咪)",
+            "customerName": "李女士",
+            "address": "北京市朝阳区国贸三期 35层",
+            "time": "2024-02-05 16:30",
+            "status": "pending_service"
+        },
+        {
+            "orderNo": "ORD20240205003",
+            "serviceName": "洗护套餐",
+            "petName": "泰迪(可乐)",
+            "customerName": "王小姐",
+            "address": "北京市海淀区中关村软件园 5号楼",
+            "time": "2024-02-06 10:00",
+            "status": "pending_service"
+        }
+    ]
+}

+ 199 - 0
src/mock/dispatch.json

@@ -0,0 +1,199 @@
+{
+    "ordersList": [
+        {
+            "id": 1,
+            "type": "宠物接送",
+            "typeCode": "transport",
+            "status": "pending_dispatch",
+            "pickAddr": "北京市朝阳区三里屯SOHO A座",
+            "dropAddr": "北京市朝阳区朝阳大悦城",
+            "time": "2024-02-07 14:30",
+            "daysLater": "今天",
+            "lng": 116.4552,
+            "lat": 39.9338
+        },
+        {
+            "id": 2,
+            "type": "上门喂遛",
+            "typeCode": "feeding",
+            "status": "pending_accept",
+            "address": "北京市海淀区中关村软件园",
+            "riderId": 101,
+            "time": "2024-02-08 15:00",
+            "daysLater": "明天",
+            "lng": 116.298,
+            "lat": 40.044
+        },
+        {
+            "id": 3,
+            "type": "上门洗护",
+            "typeCode": "washing",
+            "status": "processing",
+            "address": "北京市朝阳区望京SOHO T3",
+            "riderId": 102,
+            "time": "2024-02-07 10:00",
+            "daysLater": "进行中",
+            "lng": 116.486,
+            "lat": 39.998
+        },
+        {
+            "id": 4,
+            "type": "宠物接送",
+            "typeCode": "transport",
+            "status": "completed",
+            "pickAddr": "北京市通州区万达广场",
+            "dropAddr": "北京市通州区北苑",
+            "time": "2024-02-06 09:00",
+            "daysLater": "",
+            "lng": 116.643,
+            "lat": 39.905
+        },
+        {
+            "id": 5,
+            "type": "上门喂遛",
+            "typeCode": "feeding",
+            "status": "pending_dispatch",
+            "address": "北京市朝阳区朝阳公园南门",
+            "time": "2024-02-09 16:00",
+            "daysLater": "2天后",
+            "lng": 116.488,
+            "lat": 39.932
+        },
+        {
+            "id": 6,
+            "type": "宠物接送",
+            "typeCode": "transport",
+            "status": "pending_dispatch",
+            "pickAddr": "北京市朝阳区国贸三期",
+            "dropAddr": "北京市朝阳区双井富力城",
+            "time": "2024-02-07 16:30",
+            "daysLater": "今天",
+            "lng": 116.463,
+            "lat": 39.907
+        }
+    ],
+    "ridersList": [
+        {
+            "id": 101,
+            "name": "王大力",
+            "station": "朝阳站",
+            "phone": "13800138000",
+            "maskPhone": "138****8000",
+            "status": "online",
+            "categories": [
+                "接送",
+                "喂遛"
+            ],
+            "lastServiceTime": "2024-02-07 11:00",
+            "avatar": "https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png",
+            "pendingCount": 0,
+            "todoCount": 2,
+            "lng": 116.46,
+            "lat": 39.92
+        },
+        {
+            "id": 102,
+            "name": "李小龙",
+            "station": "海淀站",
+            "phone": "13912345678",
+            "maskPhone": "139****5678",
+            "status": "online",
+            "categories": [
+                "接送",
+                "洗护"
+            ],
+            "lastServiceTime": "2024-02-07 10:30",
+            "avatar": "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png",
+            "pendingCount": 1,
+            "todoCount": 3,
+            "lng": 116.45,
+            "lat": 39.915
+        },
+        {
+            "id": 103,
+            "name": "张小美",
+            "station": "望京站",
+            "phone": "13666666666",
+            "maskPhone": "136****6666",
+            "status": "online",
+            "categories": [
+                "喂遛"
+            ],
+            "lastServiceTime": "2024-02-07 10:45",
+            "avatar": "",
+            "pendingCount": 0,
+            "todoCount": 0,
+            "lng": 116.47,
+            "lat": 39.93
+        },
+        {
+            "id": 104,
+            "name": "赵铁柱",
+            "station": "通州站",
+            "phone": "13555555555",
+            "maskPhone": "135****5555",
+            "status": "offline",
+            "categories": [
+                "接送"
+            ],
+            "lastServiceTime": "2024-02-06 18:00",
+            "avatar": "",
+            "pendingCount": 0,
+            "todoCount": 0,
+            "lng": 116.44,
+            "lat": 39.91
+        },
+        {
+            "id": 105,
+            "name": "孙悟空",
+            "station": "花果山",
+            "phone": "13888888888",
+            "maskPhone": "138****8888",
+            "status": "online",
+            "categories": [
+                "接送",
+                "喂遛",
+                "洗护"
+            ],
+            "lastServiceTime": "2024-02-07 09:30",
+            "avatar": "",
+            "pendingCount": 0,
+            "todoCount": 1,
+            "lng": 116.48,
+            "lat": 39.925
+        }
+    ],
+    "merchantList": [
+        {
+            "id": 201,
+            "name": "萌它宠物",
+            "orders": 5,
+            "lng": 116.453,
+            "lat": 39.923,
+            "icon": ""
+        },
+        {
+            "id": 202,
+            "name": "宠爱国际",
+            "orders": 0,
+            "lng": 116.465,
+            "lat": 39.928,
+            "icon": ""
+        },
+        {
+            "id": 203,
+            "name": "顽皮狗",
+            "orders": 8,
+            "lng": 116.448,
+            "lat": 39.918,
+            "icon": ""
+        }
+    ],
+    "stats": {
+        "pendingDispatch": 3,
+        "pendingAccept": 4,
+        "processing": 8,
+        "delivering": 2,
+        "merchants": 16
+    }
+}

+ 3 - 0
src/types/components.d.ts

@@ -51,9 +51,12 @@ declare module 'vue' {
     ElRadio: typeof import('element-plus/es')['ElRadio']
     ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
+    ElResult: typeof import('element-plus/es')['ElResult']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
     ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElStep: typeof import('element-plus/es')['ElStep']
+    ElSteps: typeof import('element-plus/es')['ElSteps']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
     ElTable: typeof import('element-plus/es')['ElTable']

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

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

+ 599 - 0
src/views/order/dispatch/components/DispatchDialog.vue

@@ -0,0 +1,599 @@
+<template>
+  <el-dialog v-model="visible" title="派单调度" width="900px" top="5vh" destroy-on-close append-to-body>
+    <div class="dispatch-dialog-content">
+      <!-- Top: Order Info -->
+      <div class="dispatch-order-info box-card" v-if="order">
+        <div class="list-card order-card" style="margin: 0; box-shadow: none; cursor: default; border: none">
+          <div class="card-left">
+            <div class="type-tag" :class="order.typeCode">
+              {{ getShortType(order.typeCode) }}
+            </div>
+          </div>
+          <div class="card-main">
+            <template v-if="order.typeCode === 'transport'">
+              <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>
+            </template>
+            <template v-else>
+              <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.serviceTime }}
+              <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Current Rider Info (For Re-dispatch) -->
+      <div class="current-rider-section" v-if="currentRider">
+        <div class="select-header" style="margin-bottom: 8px">
+          <span class="tit">当前派单履约者</span>
+        </div>
+        <div class="list-card rider-card"
+          style="margin-bottom: 20px; border: 1px solid #e4e7ed; background: #fafafa; cursor: default">
+          <div class="card-left relative">
+            <el-avatar :src="currentRider.avatar" :size="40" />
+            <div class="dot" :class="currentRider.status"></div>
+          </div>
+          <div class="card-main">
+            <div class="row-1" style="justify-content: space-between; align-items: flex-start">
+              <div style="display: flex; align-items: baseline; gap: 8px">
+                <span class="r-name">{{ currentRider.name }}</span>
+                <span class="r-phone">{{ currentRider.maskPhone }}</span>
+              </div>
+              <div class="status-right">
+                <span class="status-badge" :class="currentRider.status">{{ getRiderStatusText(currentRider.status)
+                  }}</span>
+              </div>
+            </div>
+
+            <div class="row-2 categories-row" style="margin-top: 6px; display: flex; gap: 4px; flex-wrap: wrap">
+              <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>
+
+      <!-- Middle: Rider Selection -->
+      <div class="dispatch-rider-select">
+        <div class="select-header">
+          <span class="tit">选择履约者</span>
+          <el-input v-model="searchQuery" placeholder="搜索履约者姓名/手机号" prefix-icon="Search" clearable
+            style="width: 240px" />
+        </div>
+
+        <div class="rider-grid-wrapper">
+          <el-scrollbar height="400px">
+            <div class="rider-grid">
+              <div v-for="rider in filteredRiders" :key="rider.id" class="list-card rider-card select-card"
+                :class="{ active: selectedId === rider.id }" @click="selectedId = rider.id">
+                <div class="card-left relative">
+                  <el-avatar :src="rider.avatar" :size="40" />
+                  <div class="dot" :class="rider.status"></div>
+                </div>
+                <div class="card-main">
+                  <div class="row-1" style="justify-content: space-between; align-items: flex-start">
+                    <div style="display: flex; align-items: baseline; gap: 8px">
+                      <span class="r-name">{{ rider.name }}</span>
+                      <span class="r-phone">{{ rider.maskPhone }}</span>
+                    </div>
+                    <div class="status-right">
+                      <span class="status-badge" :class="rider.status">{{ getRiderStatusText(rider.status) }}</span>
+                    </div>
+                  </div>
+
+                  <div class="row-2 categories-row" style="margin-top: 6px; display: flex; gap: 4px; flex-wrap: wrap">
+                    <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 -->
+                <div class="selected-mark" v-if="selectedId === rider.id">
+                  <el-icon>
+                    <Check />
+                  </el-icon>
+                </div>
+              </div>
+
+              <div v-if="filteredRiders.length === 0" class="empty-text">暂无符合条件的履约者</div>
+            </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 -->
+      <div class="dispatch-footer">
+        <div class="fee-input">
+          <span class="label">服务费用:</span>
+          <el-input-number v-model="fee" :min="0" :precision="2" :step="10" placeholder="请输入" style="width: 140px" />
+          <span class="unit">元</span>
+        </div>
+        <div class="btns">
+          <el-button @click="visible = false">取消</el-button>
+          <el-button type="primary" @click="handleSubmit">确认派单</el-button>
+        </div>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script setup>
+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 }
+});
+
+const emit = defineEmits(['update:modelValue', 'submit']);
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+});
+
+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(() => {
+  return ridersList.value || [];
+});
+
+const handleSubmit = () => {
+  if (!selectedId.value) {
+    ElMessage.warning('请选择履约者');
+    return;
+  }
+  if (!fee.value) {
+    ElMessage.warning('请输入服务费用');
+    return;
+  }
+  emit('submit', { order: props.order, riderId: selectedId.value, fee: fee.value });
+};
+
+// 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 statusMap = {
+    resting: '休息',
+    busy: '接单中',
+    disabled: '禁用'
+  };
+  return statusMap[status] || status;
+};
+
+</script>
+
+<style scoped>
+.list-card {
+  background: #fff;
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  padding: 12px;
+  margin-bottom: 10px;
+  display: flex;
+  align-items: stretch;
+  gap: 12px;
+  transition: all 0.2s;
+}
+
+.card-left {
+  flex-shrink: 0;
+  display: flex;
+  align-items: center;
+}
+
+.order-card .type-tag {
+  width: 40px;
+  height: 40px;
+  border-radius: 8px;
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+  font-weight: bold;
+}
+
+.type-tag.transport {
+  background: #e6a23c;
+}
+
+.type-tag.feeding {
+  background: #67c23a;
+}
+
+.type-tag.washing {
+  background: #409eff;
+}
+
+.card-main {
+  flex: 1;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  gap: 4px;
+}
+
+.row-1 {
+  display: flex;
+  align-items: center;
+}
+
+.o-type {
+  font-weight: bold;
+  font-size: 14px;
+  color: #303133;
+}
+
+.row-2 {
+  font-size: 12px;
+  color: #606266;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.row-3 {
+  font-size: 12px;
+  color: #909399;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.card-right {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  justify-content: center;
+  gap: 8px;
+  margin-left: 8px;
+  flex-shrink: 0;
+}
+
+.card-right .actions {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.rider-card .card-left .dot {
+  position: absolute;
+  bottom: 0;
+  right: 0;
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  border: 2px solid #fff;
+}
+
+.dot.online {
+  background: #67c23a;
+}
+
+.dot.busy {
+  background: #409eff;
+}
+
+.dot.offline {
+  background: #909399;
+}
+
+.r-name {
+  font-size: 14px;
+  font-weight: bold;
+  color: #303133;
+}
+
+.r-phone {
+  font-size: 12px;
+  color: #909399;
+}
+
+.cat-tag {
+  background: #f4f4f5;
+  color: #909399;
+  font-size: 10px;
+  padding: 1px 4px;
+  border-radius: 2px;
+}
+
+.cat-tag.cat-transport {
+  background: #e6f7ff;
+  color: #1890ff;
+  border: 1px solid #91d5ff;
+}
+
+.cat-tag.cat-feeding {
+  background: #f6ffed;
+  color: #52c41a;
+  border: 1px solid #b7eb8f;
+}
+
+.cat-tag.cat-washing {
+  background: #fff0f6;
+  color: #eb2f96;
+  border: 1px solid #ffadd2;
+}
+
+.status-badge {
+  font-size: 11px;
+  padding: 2px 6px;
+  border-radius: 4px;
+  display: inline-block;
+  font-weight: bold;
+}
+
+.status-badge.online {
+  background: #f0f9eb;
+  color: #67c23a;
+}
+
+.status-badge.busy {
+  background: #ecf5ff;
+  color: #409eff;
+}
+
+.status-badge.offline {
+  background: #f4f4f5;
+  color: #909399;
+}
+
+.status-badge.disabled {
+  background: #fef0f0;
+  color: #f56c6c;
+}
+
+.last-time {
+  font-size: 11px;
+  color: #999;
+}
+
+.dispatch-order-info {
+  background: #f5f7fa;
+  padding: 10px;
+  border-radius: 4px;
+  margin-bottom: 20px;
+  border: 1px solid #e4e7ed;
+}
+
+.dispatch-rider-select .select-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.dispatch-rider-select .tit {
+  font-weight: bold;
+  font-size: 14px;
+}
+
+.rider-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 12px;
+  padding-right: 10px;
+}
+
+.rider-card.select-card {
+  cursor: pointer;
+  border: 1px solid #dcdfe6;
+  position: relative;
+  transition: all 0.2s;
+  margin-bottom: 0;
+}
+
+.rider-card.select-card:hover {
+  border-color: #409eff;
+}
+
+.rider-card.select-card.active {
+  border-color: #409eff;
+  background-color: #ecf5ff;
+}
+
+.selected-mark {
+  position: absolute;
+  top: 0;
+  right: 0;
+  background: #409eff;
+  color: #fff;
+  border-bottom-left-radius: 6px;
+  width: 20px;
+  height: 20px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+}
+
+.dispatch-footer {
+  margin-top: 20px;
+  padding-top: 20px;
+  border-top: 1px solid #ebeef5;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.dispatch-footer .fee-input {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 14px;
+}
+
+.empty-text {
+  text-align: center;
+  color: #909399;
+  padding: 20px;
+  width: 100%;
+  grid-column: span 2;
+}
+
+/* Order Card New Layout */
+.row-addr {
+  font-size: 13px;
+  color: #303133;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  line-height: 1.5;
+}
+
+.row-addr .tag {
+  font-size: 11px;
+  color: #fff;
+  padding: 1px 4px;
+  border-radius: 4px;
+  flex-shrink: 0;
+  transform: scale(0.9);
+}
+
+.tag.pick {
+  background: #409eff;
+}
+
+.tag.drop {
+  background: #e6a23c;
+}
+
+.tag.home {
+  background: #67c23a;
+}
+
+.row-time {
+  font-size: 12px;
+  color: #909399;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.days-tag {
+  color: #f56c6c;
+  background: #fef0f0;
+  padding: 0 4px;
+  border-radius: 4px;
+  font-size: 11px;
+  border: 1px solid #fde2e2;
+  transform: scale(0.95);
+}
+</style>

+ 295 - 0
src/views/order/dispatch/components/OrderListPanel.vue

@@ -0,0 +1,295 @@
+<template>
+  <div class="panel-section order-mgmt">
+    <div class="sec-header">
+      <span class="tit">订单</span>
+      <!-- Right Aligned Tabs -->
+      <div class="header-right-tabs">
+        <span class="h-tab-item" :class="{ active: currentTab === 'PendingDispatch' }" @click="currentTab = 'PendingDispatch'">
+          <span class="txt">待派单</span>
+          <span class="num danger">{{ stats.pendingDispatch }}</span>
+        </span>
+        <span class="h-tab-item" :class="{ active: currentTab === 'PendingAccept' }" @click="currentTab = 'PendingAccept'">
+          <span class="txt">待接单</span>
+          <span class="num warning">{{ stats.pendingAccept }}</span>
+        </span>
+        <span class="h-tab-item" :class="{ active: currentTab === 'Processing' }" @click="currentTab = 'Processing'">
+          <span class="txt">进行中</span>
+          <span class="num primary">{{ stats.processing }}</span>
+        </span>
+      </div>
+    </div>
+
+    <!-- Order List -->
+    <div class="list-wrapper">
+      <el-scrollbar>
+        <div v-if="orders.length === 0" class="empty-state">暂无数据</div>
+        <div v-else v-for="order in orders" :key="order.id" class="list-card order-card" @click="$emit('focus', order.lng, order.lat)">
+          <div class="card-left">
+            <div class="type-tag" :class="order.typeCode">
+              {{ getShortType(order.typeCode) }}
+            </div>
+          </div>
+          <div class="card-main">
+            <template v-if="order.typeCode === 'transport'">
+              <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.serviceTime }}
+                <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
+              </div>
+            </template>
+            <template v-else>
+              <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.serviceTime }}
+                <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
+              </div>
+            </template>
+          </div>
+
+          <!-- Right: Status & Actions -->
+          <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 === 0" type="primary" size="small" @click.stop="$emit('dispatch', order)"
+                >派单</el-button
+              >
+              <el-button
+                v-else-if="[1, 2, 3].includes(order.status)"
+                type="primary"
+                size="small"
+                plain
+                @click.stop="$emit('dispatch', order)"
+                >重新派单</el-button
+              >
+            </div>
+          </div>
+        </div>
+      </el-scrollbar>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+import { Clock } from '@element-plus/icons-vue';
+
+const props = defineProps({
+  modelValue: { type: String, default: 'PendingDispatch' },
+  orders: { type: Array, default: () => [] },
+  stats: { type: Object, default: () => ({}) }
+});
+
+const emit = defineEmits(['update:modelValue', 'focus', 'dispatch']);
+
+const currentTab = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+});
+
+const getShortType = (code) => {
+  const map = { 'transport': '接送', 'feeding': '喂遛', 'washing': '洗护' };
+  return map[code] || '订单';
+};
+const getOrderStatusText = (status) => {
+  const map = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '待商家确认', 5: '已完成', 6: '已取消' };
+  return map[status] || '未知';
+};
+const getOrderStatusType = (status) => {
+  const map = { 0: 'danger', 1: 'warning', 2: 'primary', 3: 'primary', 4: 'warning', 5: 'success', 6: 'info' };
+  return map[status] || 'info';
+};
+</script>
+
+<style scoped>
+.order-mgmt {
+  border-bottom: 8px solid #f5f7fa;
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  height: 50%;
+}
+.sec-header {
+  height: 48px;
+  padding: 0 16px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  border-bottom: 1px solid #f0f0f0;
+}
+.sec-header .tit {
+  font-weight: bold;
+  font-size: 15px;
+  color: #1f2f3d;
+}
+
+.header-right-tabs {
+  display: flex;
+  gap: 4px;
+}
+.h-tab-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  position: relative;
+  padding: 6px 12px;
+  gap: 6px;
+  border-radius: 4px;
+  transition: all 0.2s;
+  color: #606266;
+}
+.h-tab-item:hover {
+  background: #f5f7fa;
+}
+.h-tab-item.active {
+  background: #ecf5ff;
+}
+.h-tab-item.active .txt {
+  color: #409eff;
+  font-weight: bold;
+}
+.h-tab-item .txt {
+  font-size: 14px;
+}
+.h-tab-item .num {
+  font-size: 14px;
+  font-weight: bold;
+  margin-bottom: 0;
+}
+.h-tab-item .num.danger {
+  color: #f56c6c;
+}
+.h-tab-item .num.warning {
+  color: #e6a23c;
+}
+.h-tab-item .num.primary {
+  color: #409eff;
+}
+
+.list-wrapper {
+  flex: 1;
+  overflow: hidden;
+  padding: 8px 12px;
+}
+.empty-state {
+  text-align: center;
+  color: #909399;
+  padding: 20px;
+  font-size: 13px;
+}
+
+.list-card {
+  background: #fff;
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  padding: 12px;
+  margin-bottom: 10px;
+  display: flex;
+  align-items: stretch;
+  gap: 12px;
+  transition: all 0.2s;
+  cursor: pointer;
+}
+.list-card:hover {
+  border-color: #c6e2ff;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+}
+
+.card-left {
+  flex-shrink: 0;
+  display: flex;
+  align-items: center;
+}
+.order-card .type-tag {
+  width: 40px;
+  height: 40px;
+  border-radius: 8px;
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+  font-weight: bold;
+}
+.type-tag.transport {
+  background: #e6a23c;
+}
+.type-tag.feeding {
+  background: #67c23a;
+}
+.type-tag.washing {
+  background: #409eff;
+}
+
+.card-main {
+  flex: 1;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  gap: 4px;
+}
+.row-addr {
+  font-size: 13px;
+  color: #303133;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  line-height: 1.5;
+}
+.row-addr .tag {
+  font-size: 11px;
+  color: #fff;
+  padding: 1px 4px;
+  border-radius: 4px;
+  flex-shrink: 0;
+  transform: scale(0.9);
+}
+.tag.pick {
+  background: #409eff;
+}
+.tag.drop {
+  background: #e6a23c;
+}
+.tag.home {
+  background: #67c23a;
+}
+
+.row-time {
+  font-size: 12px;
+  color: #909399;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+.days-tag {
+  color: #f56c6c;
+  background: #fef0f0;
+  padding: 0 4px;
+  border-radius: 4px;
+  font-size: 11px;
+  border: 1px solid #fde2e2;
+  transform: scale(0.95);
+}
+
+.card-right {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  justify-content: center;
+  gap: 8px;
+  margin-left: 8px;
+  flex-shrink: 0;
+}
+.card-right .actions {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+</style>

+ 316 - 0
src/views/order/dispatch/components/RiderListPanel.vue

@@ -0,0 +1,316 @@
+<template>
+  <div class="panel-section fulfiller-mgmt">
+    <div class="sec-header no-border">
+      <span class="tit">履约者</span>
+      <!-- Right Aligned Tabs -->
+      <div class="header-right-tabs">
+        <span class="h-tab-item" :class="{ active: currentTab === 'All' }" @click="currentTab = 'All'">
+          <span class="txt">全部</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">{{ stats.working }}</span>
+        </span>
+        <span class="h-tab-item" :class="{ active: currentTab === 'Resting' }" @click="currentTab = 'Resting'">
+          <span class="txt">休息中</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">{{ stats.disabled }}</span>
+        </span>
+      </div>
+    </div>
+
+    <!-- Rider List -->
+    <div class="list-wrapper">
+      <el-scrollbar>
+        <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.uiStatus"></div>
+          </div>
+          <div class="card-main">
+            <!-- Box 1: Name + Phone + Status (Right) -->
+            <div class="row-1" style="justify-content: space-between; align-items: flex-start">
+              <div style="display: flex; align-items: baseline; gap: 8px">
+                <span class="r-name">{{ rider.name }}</span>
+                <span class="r-phone">{{ rider.maskPhone }}</span>
+              </div>
+              <div class="status-right">
+                <span class="status-badge" :class="rider.uiStatus">{{ getRiderStatusText(rider.uiStatus) }}</span>
+              </div>
+            </div>
+
+            <!-- Box 2: Categories -->
+            <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>
+            </div>
+
+            <!-- Box 3: Last Service Time -->
+            <div class="row-3 time-row" style="margin-top: 4px">
+              <span class="last-time">下一单: {{ rider.nextOrderTime || '14:30' }}</span>
+            </div>
+          </div>
+          <div class="card-right-stats">
+            <div class="stat-box">
+              <span class="lbl">待接</span>
+              <span class="val danger">{{ rider.pendingCount }}</span>
+            </div>
+            <div class="stat-box">
+              <span class="lbl">待服</span>
+              <span class="val warning">{{ rider.todoCount }}</span>
+            </div>
+            <el-button link type="primary" size="small" style="margin-top: 4px; padding: 0" @click.stop="$emit('view-orders', rider)"
+              >查看订单</el-button
+            >
+          </div>
+        </div>
+      </el-scrollbar>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+
+const props = defineProps({
+  modelValue: { type: String, default: 'All' },
+  riders: { type: Array, default: () => [] },
+  stats: { type: Object, default: () => ({ all: 0, working: 0, resting: 0, disabled: 0 }) }
+});
+
+const emit = defineEmits(['update:modelValue', 'focus', 'view-orders']);
+
+const currentTab = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+});
+
+const getRiderStatusText = (status) => {
+  const map = { 'busy': '接单中', 'offline': '休息中', 'disabled': '禁用' };
+  return map[status] || '未知';
+};
+const getCategoryClass = (cat) => {
+  const map = { '接送': 'cat-transport', '喂遛': 'cat-feeding', '洗护': 'cat-washing' };
+  return map[cat] || '';
+};
+</script>
+
+<style scoped>
+.fulfiller-mgmt {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  height: 50%;
+}
+.sec-header {
+  height: 48px;
+  padding: 0 16px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.sec-header.no-border {
+  border-bottom: none;
+  height: 40px;
+}
+.sec-header .tit {
+  font-weight: bold;
+  font-size: 15px;
+  color: #1f2f3d;
+}
+
+.header-right-tabs {
+  display: flex;
+  gap: 4px;
+}
+.h-tab-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  position: relative;
+  padding: 6px 12px;
+  gap: 6px;
+  border-radius: 4px;
+  transition: all 0.2s;
+  color: #606266;
+}
+.h-tab-item:hover {
+  background: #f5f7fa;
+}
+.h-tab-item.active {
+  background: #ecf5ff;
+}
+.h-tab-item.active .txt {
+  color: #409eff;
+  font-weight: bold;
+}
+.h-tab-item .txt {
+  font-size: 14px;
+}
+.h-tab-item .num {
+  font-size: 14px;
+  font-weight: bold;
+  margin-bottom: 0;
+}
+.h-tab-item .num.danger {
+  color: #f56c6c;
+}
+.h-tab-item .num.success {
+  color: #67c23a;
+}
+.h-tab-item .num.info {
+  color: #909399;
+}
+
+.list-wrapper {
+  flex: 1;
+  overflow: hidden;
+  padding: 8px 12px;
+}
+
+.list-card {
+  background: #fff;
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  padding: 12px;
+  margin-bottom: 10px;
+  display: flex;
+  align-items: stretch;
+  gap: 12px;
+  transition: all 0.2s;
+  cursor: pointer;
+}
+.list-card:hover {
+  border-color: #c6e2ff;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+}
+
+.card-left {
+  flex-shrink: 0;
+  display: flex;
+  align-items: center;
+}
+.rider-card .card-left .dot {
+  position: absolute;
+  bottom: 0;
+  right: 0;
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  border: 2px solid #fff;
+}
+.dot.online {
+  background: #67c23a;
+}
+.dot.busy {
+  background: #409eff;
+}
+.dot.offline {
+  background: #909399;
+}
+
+.card-main {
+  flex: 1;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  gap: 4px;
+}
+.row-1 {
+  display: flex;
+  align-items: center;
+}
+.r-name {
+  font-weight: bold;
+  font-size: 14px;
+  color: #303133;
+}
+.r-phone {
+  font-size: 12px;
+  color: #909399;
+}
+
+.cat-tag {
+  background: #f4f4f5;
+  color: #909399;
+  font-size: 10px;
+  padding: 1px 4px;
+  border-radius: 2px;
+}
+.cat-tag.cat-transport {
+  background: #e6f7ff;
+  color: #1890ff;
+  border: 1px solid #91d5ff;
+}
+.cat-tag.cat-feeding {
+  background: #f6ffed;
+  color: #52c41a;
+  border: 1px solid #b7eb8f;
+}
+.cat-tag.cat-washing {
+  background: #fff0f6;
+  color: #eb2f96;
+  border: 1px solid #ffadd2;
+}
+
+.status-badge {
+  font-size: 11px;
+  padding: 2px 6px;
+  border-radius: 4px;
+  display: inline-block;
+  font-weight: bold;
+}
+.status-badge.online {
+  background: #f0f9eb;
+  color: #67c23a;
+}
+.status-badge.busy {
+  background: #ecf5ff;
+  color: #409eff;
+}
+.status-badge.offline {
+  background: #f4f4f5;
+  color: #909399;
+}
+.status-badge.disabled {
+  background: #fef0f0;
+  color: #f56c6c;
+}
+
+.last-time {
+  font-size: 11px;
+  color: #999;
+}
+
+.card-right-stats {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  justify-content: center;
+  gap: 6px;
+}
+.stat-box {
+  font-size: 11px;
+  color: #909399;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+.stat-box .val {
+  font-weight: bold;
+  font-size: 13px;
+}
+.stat-box .val.danger {
+  color: #f56c6c;
+}
+.stat-box .val.warning {
+  color: #e6a23c;
+}
+</style>

+ 117 - 0
src/views/order/dispatch/components/RiderOrdersDialog.vue

@@ -0,0 +1,117 @@
+<template>
+  <el-dialog v-model="visible" title="履约者订单详情" width="1100px" top="6vh" custom-class="rider-orders-dialog">
+    <div class="dialog-content">
+      <!-- 1. Rider Summary Info -->
+      <div class="rider-summary" v-if="riderInfo">
+        <div class="summary-item">
+          <span class="lbl">姓名:</span>
+          <span class="val bold">{{ riderInfo.name }}</span>
+        </div>
+        <div class="summary-item">
+          <span class="lbl">手机号:</span>
+          <span class="val">{{ riderInfo.maskPhone }}</span>
+        </div>
+        <div class="summary-item">
+          <span class="lbl">所属区域:</span>
+          <span class="val">{{ riderInfo.station }}</span>
+        </div>
+        <div class="summary-item">
+          <span class="lbl">工作类型:</span>
+          <span class="val">{{ riderInfo.workType }}</span>
+        </div>
+        <div class="summary-item" style="flex: 1">
+          <span class="lbl">服务类目:</span>
+          <el-tag v-for="tag in riderInfo.categories" :key="tag" size="small" effect="plain" style="margin-right: 4px">{{ tag }}</el-tag>
+        </div>
+      </div>
+
+      <!-- 2. Orders Table -->
+      <div class="rider-orders-list">
+        <el-table :data="orders" stripe style="width: 100%" :header-cell-style="{ background: '#f5f7fa', color: '#606266' }">
+          <el-table-column prop="orderNo" label="订单号" width="150" />
+          <el-table-column prop="serviceName" label="服务项目" width="120" show-overflow-tooltip />
+          <el-table-column prop="petName" label="服务宠物" width="100" />
+          <el-table-column prop="customerName" label="客户" width="100" />
+          <el-table-column prop="address" label="服务详细地址" min-width="200" show-overflow-tooltip />
+          <el-table-column prop="time" label="预约时间" width="160" />
+          <el-table-column label="状态" width="100">
+            <template #default="{ row }">
+              <el-tag v-if="row.status === 'pending_accept'" type="warning" size="small">待接单</el-tag>
+              <el-tag v-else-if="row.status === 'pending_service'" type="primary" size="small">待服务</el-tag>
+              <el-tag v-else type="info" size="small">{{ row.status }}</el-tag>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+
+const props = defineProps({
+  modelValue: {
+    type: Boolean,
+    default: false
+  },
+  riderInfo: {
+    type: Object,
+    default: () => null
+  },
+  orders: {
+    type: Array,
+    default: () => []
+  }
+});
+
+const emit = defineEmits(['update:modelValue']);
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+});
+</script>
+
+<style scoped>
+.rider-summary {
+  display: flex;
+  align-items: center;
+  gap: 24px;
+  background: #fdfdfd;
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  padding: 20px;
+  margin-bottom: 24px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  flex-wrap: nowrap;
+  overflow-x: auto;
+}
+.summary-item {
+  display: flex;
+  align-items: center;
+  font-size: 14px;
+  color: #606266;
+  white-space: nowrap;
+  flex-shrink: 0;
+}
+.summary-item .lbl {
+  color: #909399;
+  margin-right: 6px;
+}
+.summary-item .val {
+  color: #303133;
+  font-weight: 500;
+}
+.summary-item .val.bold {
+  font-weight: bold;
+  font-size: 16px;
+  color: #303133;
+}
+
+.rider-orders-list {
+  border: 1px solid #ebeef5;
+  border-radius: 8px;
+  overflow: hidden; /* Hide potential scrollbars from container */
+}
+</style>

Разница между файлами не показана из-за своего большого размера
+ 251 - 619
src/views/order/dispatch/index.vue


+ 409 - 0
src/views/order/orderList/components/CareSummaryDrawer.vue

@@ -0,0 +1,409 @@
+<template>
+    <el-drawer v-model="drawerVisible" title="宠物护理工作小结" direction="rtl" size="750px" destroy-on-close
+        class="care-summary-drawer">
+        <div class="care-summary-container" v-if="order">
+            <!-- Pet Header -->
+            <div class="summary-header">
+                <div class="avatar-wrapper">
+                    <el-avatar :size="80" :src="order.petAvatar" shape="circle" class="pet-summary-avatar">{{
+                        order.petName?.charAt(0) }}</el-avatar>
+                </div>
+                <div class="pet-summary-info">
+                    <div class="summary-name-row">
+                        <span class="name">{{ order.petName }}</span>
+                        <div class="tags-group">
+                            <dict-tag :options="sys_pet_gender" :value="order.petGender" />
+                            <el-tag type="info" effect="light" round>
+                                {{ order.petAge }}
+                            </el-tag>
+                            <el-tag v-for="tag in (order.petTags || [])" :key="tag" type="warning" effect="plain"
+                                round>{{ tag }}</el-tag>
+                        </div>
+                    </div>
+                    <div class="summary-sub-row">
+                        <div class="info-item">
+                            <span class="lbl">品种</span>
+                            <span class="val">{{ order.petBreed || '未知' }}</span>
+                        </div>
+                        <div class="divider-v"></div>
+                        <div class="info-item">
+                            <span class="lbl">体重</span>
+                            <span class="val">{{ order.petWeight }}</span>
+                        </div>
+                        <div class="divider-v"></div>
+                        <div class="info-item">
+                            <span class="lbl">主人</span>
+                            <span class="val">{{ order.userName || '未知' }}</span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- Info Groups -->
+            <div class="summary-section">
+                <div class="sec-title">
+                    <span class="icon-box"><el-icon>
+                            <List />
+                        </el-icon></span>
+                    基本信息
+                </div>
+                <el-descriptions :column="2" border class="spacious-desc">
+                    <el-descriptions-item label="性格关键词">{{ order.petPersonality }}</el-descriptions-item>
+                    <el-descriptions-item label="健康状况">
+                        <el-tag :type="order.healthStatus === '健康' ? 'success' : 'danger'" effect="light"
+                            size="small">{{ order.healthStatus }}</el-tag>
+                    </el-descriptions-item>
+                    <el-descriptions-item label="疫苗情况">
+                        <div class="flex-align">
+                            <span style="color:#67c23a; margin-right:8px;" v-if="order.vaccineImg"><el-icon>
+                                    <CircleCheckFilled />
+                                </el-icon> 已接种</span>
+                            <span v-else style="color:#909399;">未接种</span>
+                            <el-image v-if="order.vaccineImg"
+                                style="width: 24px; height: 24px; border-radius:4px; vertical-align:middle; cursor:zoom-in;"
+                                :src="order.vaccineImg" :preview-src-list="[order.vaccineImg]"
+                                :preview-teleported="true" />
+                        </div>
+                    </el-descriptions-item>
+                    <el-descriptions-item label="过敏史">
+                        <span :style="{ color: order.allergy ? '#f56c6c' : 'inherit' }">{{ order.allergy || '无'
+                        }}</span>
+                    </el-descriptions-item>
+                </el-descriptions>
+            </div>
+
+            <div class="summary-section">
+                <div class="sec-title">
+                    <span class="icon-box text-blue"><el-icon>
+                            <HomeFilled />
+                        </el-icon></span>
+                    服务环境
+                </div>
+                <el-descriptions :column="2" border class="spacious-desc">
+                    <el-descriptions-item label="到家时间">{{ order.homeTime }}</el-descriptions-item>
+                    <el-descriptions-item label="房屋类型">
+                        <dict-tag :options="sys_house_type" :value="order.houseType" />
+                    </el-descriptions-item>
+                    <el-descriptions-item label="入户方式" :span="2">
+                        <dict-tag :options="sys_entry_method" :value="order.entryMethod" />
+                        <span style="margin-left:8px; color:#909399;">({{ order.entryDetail }})</span>
+                    </el-descriptions-item>
+                </el-descriptions>
+            </div>
+
+            <!-- Service Log -->
+            <div class="summary-section main-log">
+                <div class="sec-title" style="border:none; padding-left:0; margin-bottom:16px;">
+                    <div class="left">
+                        <span class="icon-box text-orange"><el-icon>
+                                <Notebook />
+                            </el-icon></span>
+                        服务内容记录
+                    </div>
+                    <el-button v-if="!isEditingSummary" type="primary" link icon="Edit"
+                        @click="isEditingSummary = true">编辑</el-button>
+                </div>
+
+                <div v-if="isEditingSummary" class="edit-area">
+                    <el-input v-model="careSummaryText" type="textarea" :rows="12" placeholder="请输入详细的护理服务小结..."
+                        resize="none" />
+                    <div class="edit-actions">
+                        <el-button @click="isEditingSummary = false">取消</el-button>
+                        <el-button type="primary" @click="saveCareSummary">保存内容</el-button>
+                    </div>
+                </div>
+                <div v-else class="log-content-box">
+                    <pre class="log-text">{{ careSummaryText }}</pre>
+                </div>
+            </div>
+
+            <!-- Footer Info -->
+            <div class="summary-footer">
+                <div class="footer-info">
+                    <div class="f-row">
+                        <span class="lbl">护宠师</span>
+                        <span class="val user-active">{{ order.fulfillerName || '当前履约者' }}</span>
+                    </div>
+                    <div class="f-row">
+                        <span class="lbl">提交时间</span>
+                        <span class="val">{{ order.summaryTime || '2024-02-04 17:00' }}</span>
+                    </div>
+                </div>
+                <div class="footer-action">
+                    <el-button size="large" @click="drawerVisible = false">关闭</el-button>
+                </div>
+            </div>
+        </div>
+    </el-drawer>
+</template>
+
+<script setup>
+import { ref, computed, watch, getCurrentInstance, toRefs } from 'vue'
+
+const props = defineProps({
+    visible: Boolean,
+    order: Object
+})
+const emit = defineEmits(['update:visible', 'submit'])
+
+const { proxy } = getCurrentInstance()
+const { sys_pet_gender, sys_house_type, sys_entry_method } = toRefs(
+    proxy?.useDict('sys_pet_gender', 'sys_house_type', 'sys_entry_method')
+)
+
+const drawerVisible = computed({
+    get: () => props.visible,
+    set: (val) => emit('update:visible', val)
+})
+
+const careSummaryText = ref('')
+const isEditingSummary = ref(false)
+
+watch(() => props.visible, (val) => {
+    if (val && props.order) {
+        isEditingSummary.value = false
+        careSummaryText.value = props.order.careSummary || ''
+    }
+})
+
+const saveCareSummary = () => {
+    emit('submit', careSummaryText.value)
+    isEditingSummary.value = false
+}
+</script>
+
+<style scoped>
+/* Enhanced Care Summary Styles */
+.care-summary-drawer :deep(.el-drawer__header) {
+    margin-bottom: 0;
+    padding: 20px 24px;
+    border-bottom: 1px solid #f0f0f0;
+}
+
+.care-summary-drawer :deep(.el-drawer__body) {
+    padding: 0;
+    overflow-y: auto;
+    background: #fff;
+}
+
+.care-summary-container {
+    padding: 32px 40px;
+}
+
+/* 1. Header */
+.summary-header {
+    display: flex;
+    gap: 24px;
+    align-items: flex-start;
+    margin-bottom: 32px;
+    padding-bottom: 24px;
+    border-bottom: 1px dashed #e4e7ed;
+}
+
+.avatar-wrapper {
+    border: 4px solid #f2f6fc;
+    border-radius: 50%;
+}
+
+.pet-summary-info {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+    padding-top: 4px;
+}
+
+.summary-name-row {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    margin-bottom: 4px;
+}
+
+.summary-name-row .name {
+    font-size: 24px;
+    font-weight: 800;
+    color: #303133;
+    letter-spacing: 0.5px;
+}
+
+.tags-group {
+    display: flex;
+    gap: 8px;
+    align-items: center;
+}
+
+.summary-sub-row {
+    display: flex;
+    align-items: center;
+    background: #f9fafe;
+    padding: 10px 16px;
+    border-radius: 8px;
+    align-self: flex-start;
+}
+
+.info-item {
+    display: flex;
+    flex-direction: column;
+    gap: 2px;
+}
+
+.info-item .lbl {
+    font-size: 11px;
+    color: #909399;
+    text-transform: uppercase;
+}
+
+.info-item .val {
+    font-size: 14px;
+    font-weight: bold;
+    color: #606266;
+}
+
+.divider-v {
+    width: 1px;
+    height: 24px;
+    background: #ebeef5;
+    margin: 0 16px;
+}
+
+/* 2. Sections */
+.summary-section {
+    margin-bottom: 40px;
+}
+
+.sec-title {
+    font-size: 16px;
+    font-weight: 700;
+    color: #303133;
+    margin-bottom: 16px;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    justify-content: space-between;
+}
+
+.sec-title .left {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.icon-box {
+    width: 28px;
+    height: 28px;
+    background: #ecf5ff;
+    color: #409eff;
+    border-radius: 6px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 16px;
+}
+
+.icon-box.text-blue {
+    background: #ecf5ff;
+    color: #409eff;
+}
+
+.icon-box.text-orange {
+    background: #fdf6ec;
+    color: #e6a23c;
+}
+
+/* 3. Descriptions */
+.spacious-desc :deep(.el-descriptions__cell) {
+    padding: 12px 16px !important;
+}
+
+.spacious-desc :deep(.el-descriptions__label) {
+    width: 100px;
+    color: #606266;
+    font-weight: 500;
+    background: #fafafa;
+}
+
+.flex-align {
+    display: flex;
+    align-items: center;
+}
+
+/* 4. Log Area */
+.main-log {
+    background: #fff;
+}
+
+.log-content-box {
+    background: #fff;
+    border: 1px solid #ebeef5;
+    border-radius: 8px;
+    padding: 24px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.02);
+    position: relative;
+}
+
+.log-content-box::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 4px;
+    height: 100%;
+    background: #e6a23c;
+    border-top-left-radius: 8px;
+    border-bottom-left-radius: 8px;
+}
+
+.log-text {
+    white-space: pre-wrap;
+    font-family: 'Inter', system-ui, sans-serif;
+    margin: 0;
+    line-height: 1.8;
+    font-size: 15px;
+    color: #303133;
+    text-align: justify;
+}
+
+.edit-actions {
+    display: flex;
+    justify-content: flex-end;
+    gap: 12px;
+    margin-top: 12px;
+}
+
+/* 5. Footer */
+.summary-footer {
+    margin-top: 60px;
+    padding-top: 24px;
+    border-top: 1px solid #ebeef5;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.footer-info {
+    display: flex;
+    gap: 32px;
+}
+
+.f-row {
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+}
+
+.f-row .lbl {
+    font-size: 12px;
+    color: #909399;
+}
+
+.f-row .val {
+    font-size: 15px;
+    font-weight: 600;
+    color: #303133;
+}
+
+.f-row .val.user-active {
+    color: #409eff;
+}
+</style>

+ 591 - 0
src/views/order/orderList/components/DispatchDialog.vue

@@ -0,0 +1,591 @@
+<template>
+    <el-dialog v-model="dialogVisible" title="派单调度" width="900px" top="5vh" destroy-on-close append-to-body>
+        <div class="dispatch-dialog-content">
+            <!-- Top: Order Info (OrderDispatch Style) -->
+            <div class="dispatch-order-info" v-if="order">
+                <div class="list-card order-card" style="margin:0; box-shadow:none; cursor:default; border:none;">
+                    <div class="card-left">
+                        <div class="type-tag" :class="order.typeCode">
+                            {{ getShortType(order.typeCode) }}
+                        </div>
+                    </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>
+                        </template>
+                        <template v-else>
+                            <div class="row-addr" :title="order.address">
+                                <span class="tag home">址</span> {{ order.address }}
+                            </div>
+                        </template>
+                        <div class="row-time" style="margin-top: 4px;">
+                            <el-icon>
+                                <Clock />
+                            </el-icon> {{ order.time }}
+                            <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- Current Rider Info (If Exists) -->
+            <div class="current-rider-section" v-if="currentRider">
+                <div class="select-header" style="margin-bottom:8px;">
+                    <span class="tit">当前派单履约者</span>
+                </div>
+                <div class="list-card rider-card"
+                    style="margin-bottom: 20px; border: 1px solid #e4e7ed; background:#fafafa; cursor:default;">
+                    <div class="card-left relative">
+                        <el-avatar :src="currentRider.avatar" :size="40" />
+                    </div>
+                    <div class="card-main">
+                        <div class="row-1"
+                            style="justify-content: space-between; align-items: flex-start; display: flex;">
+                            <div style="display:flex; align-items:baseline; gap:8px;">
+                                <span class="r-name">{{ currentRider.name || '--' }}</span>
+                                <span class="r-phone">{{ currentRider.phone || '--' }}</span>
+                                <el-tag v-if="currentRider.status" size="small" :type="getStatusType(currentRider.status)" effect="plain">
+                                    {{ getStatusText(currentRider.status) }}
+                                </el-tag>
+                            </div>
+                        </div>
+
+                        <div class="row-2 categories-row"
+                            style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
+                            <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>
+
+            <!-- Middle: Rider Selection -->
+            <div class="dispatch-rider-select">
+                <div class="select-header">
+                    <span class="tit">选择履约者</span>
+                    <el-input v-model="dispatchSearchQuery" placeholder="搜索履约者姓名/手机号" prefix-icon="Search" clearable
+                        style="width: 240px" />
+                </div>
+
+                <div class="rider-grid-wrapper">
+                    <el-scrollbar class="rider-scroll">
+                        <div class="rider-grid">
+                            <div v-for="rider in filteredDispatchRiders" :key="rider.id"
+                                class="list-card rider-card select-card"
+                                :class="{ active: selectedRiderId === rider.id }" @click="selectedRiderId = rider.id">
+                                <!-- Reusing Rider Card Layout -->
+                                <div class="card-left relative">
+                                    <el-avatar :src="rider.avatar" :size="40" />
+                                </div>
+                                <div class="card-main">
+                                    <div class="row-1" style="justify-content: space-between; align-items: flex-start; display: flex;">
+                                        <div style="display:flex; align-items:baseline; gap:8px;">
+                                            <span class="r-name">{{ rider.name || '--' }}</span>
+                                            <span class="r-phone">{{ rider.phone || '--' }}</span>
+                                        </div>
+                                        <el-tag v-if="rider.status" size="small" :type="getStatusType(rider.status)" effect="plain">
+                                            {{ getStatusText(rider.status) }}
+                                        </el-tag>
+                                    </div>
+
+                                    <div class="row-2 categories-row"
+                                        style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
+                                        <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 -->
+                                <div class="selected-mark" v-if="selectedRiderId === rider.id">
+                                    <el-icon>
+                                        <Check />
+                                    </el-icon>
+                                </div>
+                            </div>
+
+                            <div v-if="filteredDispatchRiders.length === 0" class="empty-text">暂无符合条件的履约者</div>
+                        </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 -->
+            <div class="dispatch-footer">
+                <div class="fee-input">
+                    <span class="label">服务费用:</span>
+                    <el-input-number v-model="dispatchFee" :min="0" :precision="2" :step="10" placeholder="请输入"
+                        style="width: 140px;" />
+                    <span class="unit">元</span>
+                </div>
+                <div class="btns">
+                    <el-button @click="dialogVisible = false">取消</el-button>
+                    <el-button type="primary" :disabled="!canSubmit" @click="handleDispatchSubmit">确认派单</el-button>
+                </div>
+            </div>
+        </div>
+    </el-dialog>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { pageFulfillerOnOrder } from '@/api/fulfiller/pool'
+import { listAllTag } from '@/api/fulfiller/tag'
+
+const props = defineProps({
+    visible: Boolean,
+    order: Object
+})
+const emit = defineEmits(['update:visible', 'submit'])
+
+const dialogVisible = computed({
+    get: () => props.visible,
+    set: (val) => emit('update:visible', val)
+})
+
+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 currentRider = ref(null)
+const dispatchSearchQuery = ref('')
+const selectedRiderId = ref(null)
+const dispatchFee = ref(0)
+
+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: dispatchSearchQuery.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
+
+        if (props.order?.riderId) {
+            currentRider.value = ridersList.value.find(r => r.id === props.order.riderId) || null
+        }
+    } catch {
+        ridersList.value = []
+        total.value = 0
+    }
+}
+
+const handlePageSizeChange = (size) => {
+    pageSize.value = size
+    pageNum.value = 1
+    loadRiders()
+}
+
+watch(() => props.visible, (val) => {
+    if (val && props.order) {
+        currentRider.value = null
+        dispatchSearchQuery.value = ''
+        selectedRiderId.value = null
+        dispatchFee.value = 0
+        pageNum.value = 1
+        loadAllTags()
+        loadRiders()
+    }
+})
+
+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 ''
+}
+
+watch(dispatchSearchQuery, () => {
+    pageNum.value = 1
+    loadRiders()
+})
+
+const getShortType = (code) => {
+    const map = { 'transport': '接送', 'feeding': '喂遛', 'washing': '洗护' }
+    return map[code] || '订单'
+}
+
+const getStatusText = (status) => {
+    const statusMap = {
+        resting: '休息',
+        busy: '接单中',
+        disabled: '禁用'
+    }
+    return statusMap[status] || status
+}
+
+const getStatusType = (status) => {
+    const typeMap = {
+        resting: 'info',
+        busy: 'success',
+        disabled: 'danger'
+    }
+    return typeMap[status] || 'info'
+}
+
+const filteredDispatchRiders = computed(() => {
+    return ridersList.value || []
+})
+
+const canSubmit = computed(() => {
+    return !!selectedRiderId.value && !!dispatchFee.value
+})
+
+const handleDispatchSubmit = () => {
+    if (!selectedRiderId.value) {
+        ElMessage.warning('请选择履约者')
+        return
+    }
+    if (!dispatchFee.value) {
+        ElMessage.warning('请输入服务费用')
+        return
+    }
+    const rider = ridersList.value.find(r => r.id === selectedRiderId.value)
+    emit('submit', {
+        riderId: rider.id,
+        riderName: rider.name,
+        fee: dispatchFee.value
+    })
+    dialogVisible.value = false
+}
+</script>
+
+<style scoped>
+/* Dispatch Dialog Styles */
+.list-card {
+    background: #fff;
+    border: 1px solid #ebeef5;
+    border-radius: 8px;
+    padding: 12px;
+    margin-bottom: 10px;
+    display: flex;
+    align-items: stretch;
+    gap: 12px;
+    transition: all 0.2s;
+    cursor: pointer;
+}
+
+.list-card:hover {
+    border-color: #c6e2ff;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+}
+
+.card-left {
+    flex-shrink: 0;
+    display: flex;
+    align-items: center;
+}
+
+.order-card .type-tag {
+    width: 40px;
+    height: 40px;
+    border-radius: 8px;
+    color: #fff;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 12px;
+    font-weight: bold;
+}
+
+.type-tag.transport {
+    background: #e6a23c;
+}
+
+.type-tag.feeding {
+    background: #67c23a;
+}
+
+.type-tag.washing {
+    background: #409eff;
+}
+
+.card-main {
+    flex: 1;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    gap: 4px;
+}
+
+.row-addr {
+    font-size: 13px;
+    color: #303133;
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    line-height: 1.5;
+}
+
+.row-addr .tag {
+    font-size: 11px;
+    color: #fff;
+    padding: 1px 4px;
+    border-radius: 4px;
+    flex-shrink: 0;
+    transform: scale(0.9);
+}
+
+.tag.pick {
+    background: #409eff;
+}
+
+.tag.drop {
+    background: #e6a23c;
+}
+
+.tag.home {
+    background: #67c23a;
+}
+
+.row-time {
+    font-size: 12px;
+    color: #909399;
+    display: flex;
+    align-items: center;
+    gap: 4px;
+}
+
+.days-tag {
+    color: #f56c6c;
+    background: #fef0f0;
+    padding: 0 4px;
+    border-radius: 4px;
+    font-size: 11px;
+    border: 1px solid #fde2e2;
+    transform: scale(0.95);
+}
+
+.dispatch-order-info {
+    background: #f5f7fa;
+    padding: 10px;
+    border-radius: 4px;
+    margin-bottom: 20px;
+    border: 1px solid #e4e7ed;
+    display: block;
+}
+
+.dispatch-rider-select .select-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 10px;
+}
+
+.dispatch-rider-select .tit {
+    font-weight: bold;
+    font-size: 14px;
+}
+
+.rider-scroll {
+    max-height: 45vh;
+}
+
+.rider-grid {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    gap: 12px;
+    padding-right: 10px;
+}
+
+.rider-pagination {
+    margin-top: 10px;
+}
+
+.rider-card.select-card {
+    cursor: pointer;
+    border: 1px solid #dcdfe6;
+    position: relative;
+    transition: all 0.2s;
+    margin-bottom: 0;
+}
+
+.rider-card.select-card:hover {
+    border-color: #409eff;
+}
+
+.rider-card.select-card.active {
+    border-color: #409eff;
+    background-color: #ecf5ff;
+}
+
+.selected-mark {
+    position: absolute;
+    top: 0;
+    right: 0;
+    background: #409eff;
+    color: #fff;
+    border-bottom-left-radius: 6px;
+    width: 20px;
+    height: 20px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 12px;
+}
+
+.rider-card .card-left .dot {
+    position: absolute;
+    bottom: 0;
+    right: 0;
+    width: 10px;
+    height: 10px;
+    border-radius: 50%;
+    border: 2px solid #fff;
+}
+
+.dot.online {
+    background: #67c23a;
+}
+
+.dot.busy {
+    background: #409eff;
+}
+
+.dot.offline {
+    background: #909399;
+}
+
+.r-name {
+    font-weight: bold;
+    font-size: 14px;
+    color: #303133;
+    margin-right: 8px;
+}
+
+.r-phone {
+    font-size: 12px;
+    color: #909399;
+}
+
+.status-badge {
+    font-size: 11px;
+    padding: 2px 6px;
+    border-radius: 4px;
+    display: inline-block;
+    font-weight: bold;
+}
+
+.status-badge.online {
+    background: #f0f9eb;
+    color: #67c23a;
+}
+
+.status-badge.busy {
+    background: #ecf5ff;
+    color: #409eff;
+}
+
+.status-badge.offline {
+    background: #f4f4f5;
+    color: #909399;
+}
+
+.cat-tag {
+    background: #f4f4f5;
+    color: #909399;
+    font-size: 10px;
+    padding: 1px 4px;
+    border-radius: 2px;
+    margin-right: 4px;
+}
+
+.cat-tag.cat-transport {
+    background: #e6f7ff;
+    color: #1890ff;
+    border: 1px solid #91d5ff;
+}
+
+.cat-tag.cat-feeding {
+    background: #f6ffed;
+    color: #52c41a;
+    border: 1px solid #b7eb8f;
+}
+
+.cat-tag.cat-washing {
+    background: #fff0f6;
+    color: #eb2f96;
+    border: 1px solid #ffadd2;
+}
+
+.last-time {
+    font-size: 11px;
+    color: #999;
+}
+
+.empty-text {
+    text-align: center;
+    color: #909399;
+    padding: 20px;
+    width: 100%;
+    grid-column: span 2;
+}
+
+.dispatch-footer {
+    margin-top: 20px;
+    padding-top: 20px;
+    border-top: 1px solid #ebeef5;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.dispatch-footer .fee-input {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    font-size: 14px;
+}
+</style>

+ 933 - 0
src/views/order/orderList/components/OrderDetailDrawer.vue

@@ -0,0 +1,933 @@
+<template>
+    <el-drawer v-model="drawerVisible" title="订单详情" direction="rtl" size="60%" class="order-detail-drawer">
+        <div class="detail-container" v-if="order">
+            <!-- 1. Header Status -->
+            <div class="detail-header">
+                <div class="left-head">
+                    <span class="order-no">{{ order.orderNo }}</span>
+                    <el-tag :type="getStatusTag(order.status)" effect="dark" class="status-tag">{{
+                        getStatusName(order.status) }}</el-tag>
+                    <el-tag effect="plain" class="type-tag"
+                        :type="order.type === 'transport' ? '' : (order.type === 'feeding' ? 'warning' : 'danger')">
+                        {{ getTypeName(order.type) }}
+                    </el-tag>
+                </div>
+                <div class="right-head">
+                    <!-- Action Buttons Group -->
+                    <div class="detail-actions">
+                        <template v-if="[0, 1, 2, 3].includes(order.status)">
+                            <el-button type="success" icon="Bicycle" @click="emit('dispatch', order)">
+                                {{ order.fulfiller || order.fulfillerName ? '重新派单' : '立即派单' }}
+                            </el-button>
+                        </template>
+
+                        <template v-if="order.status === 0">
+                            <el-button type="danger" plain icon="CircleClose"
+                                @click="emit('cancel', order)">取消订单</el-button>
+                        </template>
+
+                        <template v-if="order.status === 4">
+                            <el-button type="primary" icon="CircleCheck"
+                                @click="emit('command', 'complete', order)">确认完成</el-button>
+                        </template>
+
+                        <template v-if="[4, 5].includes(order.status)">
+                            <el-button icon="Notebook" @click="emit('care-summary', order)">护理小结</el-button>
+                        </template>
+
+                        <el-dropdown trigger="click" @command="(cmd) => emit('command', cmd, order)"
+                            style="margin-left: 12px;">
+                            <el-button icon="More">更多操作</el-button>
+                            <template #dropdown>
+                                <el-dropdown-menu>
+                                    <el-dropdown-item command="reward" icon="Trophy">奖惩操作</el-dropdown-item>
+                                    <el-dropdown-item command="remark" icon="EditPen">订单备注</el-dropdown-item>
+                                    <el-dropdown-item command="delete" v-if="[5, 6].includes(order.status)" divided
+                                        icon="Delete" style="color: #f56c6c;">删除订单</el-dropdown-item>
+                                </el-dropdown-menu>
+                            </template>
+                        </el-dropdown>
+                    </div>
+                </div>
+            </div>
+
+            <div class="detail-scroll-area">
+                <!-- 2. Progress Section -->
+                <div class="progress-section">
+                    <el-steps :active="currentOrderSteps.active" finish-status="success" align-center
+                        class="custom-steps">
+                        <el-step v-for="(step, index) in currentOrderSteps.steps" :key="index" :title="step.title"
+                            :description="step.time" />
+                    </el-steps>
+                </div>
+
+                <!-- 3. Top Info: Pet & User -->
+                <div class="top-info-row">
+                    <!-- Left: Pet Info -->
+                    <div class="info-section pet-section">
+                        <div class="sec-header">
+                            <span class="label">宠物档案</span>
+                            <el-tag size="small" effect="plain">{{ order.petBreed }}</el-tag>
+                        </div>
+                        <div class="pet-basic-row">
+                            <el-avatar :size="50" :src="order.petAvatar" shape="square" class="pet-avatar-lg">{{
+                                (order.petName || '').charAt(0) }}</el-avatar>
+                            <div class="pet-names">
+                                <div class="b-name">{{ order.petName }}
+                                    <el-icon v-if="order.petGender === 'male'" color="#409eff">
+                                        <Male />
+                                    </el-icon>
+                                    <el-icon v-else color="#f56c6c">
+                                        <Female />
+                                    </el-icon>
+                                </div>
+                                <div class="b-tags">
+                                    <el-tag size="small" type="info">{{ order.petAge || '未知年龄' }}</el-tag>
+                                    <el-tag size="small" type="info">{{ order.petWeight || '未知体重' }}</el-tag>
+                                </div>
+                            </div>
+                        </div>
+                        <el-descriptions :column="2" size="small" class="pet-desc" border>
+                            <el-descriptions-item label="绝育状态">{{ order.petSterilized ? '已绝育' : '未绝育'
+                                }}</el-descriptions-item>
+                            <el-descriptions-item label="疫苗状态"><span style="color:#67c23a">{{ order.petVaccine || '未知'
+                                    }}</span></el-descriptions-item>
+                            <el-descriptions-item label="性格特点">{{ order.petCharacter || '温顺' }}</el-descriptions-item>
+                            <el-descriptions-item label="健康状况">{{ order.petHealth || '健康' }}</el-descriptions-item>
+                        </el-descriptions>
+                    </div>
+
+                    <!-- Right: User Info -->
+                    <div class="info-section user-section">
+                        <div class="sec-header">
+                            <span class="label">用户信息</span>
+                        </div>
+                        <div class="user-content">
+                            <div class="u-row">
+                                <el-avatar :size="40" :src="order.userAvatar">{{ (order.userName || '').charAt(0)
+                                    }}</el-avatar>
+                                <div class="u-info">
+                                    <div class="nm">{{ order.userName }}</div>
+                                    <div class="ph">{{ order.contactPhone }}</div>
+                                </div>
+                            </div>
+                            <div class="addr-box">
+                                <div class="addr-label">服务地址</div>
+                                <div class="addr-txt">{{ order.city }}{{ order.district }} {{ order.address || '' }}
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 4. Bottom Tabs -->
+                <el-tabs v-model="activeDetailTab" class="detail-tabs type-card">
+                    <!-- Tab 1: Basic Info -->
+                    <el-tab-pane label="订单基础信息" name="basic">
+                        <div class="tab-pane-content">
+                            <div class="section-block">
+                                <div class="sec-title-bar">基础业务信息</div>
+                                <el-descriptions :column="3" border size="default" class="custom-desc">
+                                    <el-descriptions-item label="系统单号">{{ order.orderNo }}</el-descriptions-item>
+                                    <el-descriptions-item label="服务类型">
+                                        {{ getTypeName(order.type) }}
+                                        <el-tag size="small" v-if="order.type === 'transport'" style="margin-left:5px"
+                                            effect="light">{{ getTransportModeName(order.transportType) }}</el-tag>
+                                    </el-descriptions-item>
+
+                                    <el-descriptions-item label="归属门店">{{ order.merchantName }}
+                                        ({{ Number(order.platformId) === 1 ? '门店下单' : '平台代下单' }})</el-descriptions-item>
+                                    <el-descriptions-item label="宠主信息">{{ order.userName }} / {{ order.contactPhone
+                                        }}</el-descriptions-item>
+                                    <el-descriptions-item label="服务费用" label-class-name="money-label">
+                                        <span style="color:#f56c6c; font-weight:bold;">¥ {{ order.fulfillerFee }}</span>
+                                    </el-descriptions-item>
+
+                                    <el-descriptions-item label="预约时间">{{ getServiceTimeRange(order.serviceTime)
+                                        }}</el-descriptions-item>
+                                    <el-descriptions-item label="团购套餐">{{ order.groupBuyPackage || '未使用团购套餐'
+                                        }}</el-descriptions-item>
+                                    <el-descriptions-item label="创建时间">{{ order.createTime }}</el-descriptions-item>
+
+                                    <el-descriptions-item label="订单备注" :span="3">
+                                        {{ order.remark || '暂无备注' }}
+                                    </el-descriptions-item>
+                                </el-descriptions>
+                            </div>
+
+                            <div v-if="order.type === 'transport'" class="section-block transport-split-block">
+                                <div class="sec-title-bar">接送任务详情</div>
+                                <div class="transport-one">
+                                    <div class="t-row">
+                                        <el-tag size="small" effect="plain" class="sub-badge">{{
+                                            getTransportLabel(order.subOrderType) }}</el-tag>
+                                        <span class="time">{{ order.serviceTime }}</span>
+                                    </div>
+                                    <div class="t-row">
+                                        <span class="t-k">起点</span>
+                                        <span class="t-v">{{ order.detail?.fromAddress || order.detail?.pickAddr || '--'
+                                        }}</span>
+                                    </div>
+                                    <div class="t-row">
+                                        <span class="t-k">终点</span>
+                                        <span class="t-v">{{ order.detail?.toAddress || order.detail?.dropAddr ||
+                                            order.toAddress ||
+                                            '--' }}</span>
+                                    </div>
+                                    <div class="t-row sub">
+                                        <span class="t-v">{{ order.contact || order.userName || '--' }} {{
+                                            order.contactPhoneNumber
+                                            || order.contactPhone || '--' }}</span>
+                                    </div>
+                                </div>
+                            </div>
+
+                            <div v-if="['feeding', 'washing'].includes(order.type)" class="section-block">
+                                <div class="sec-title-bar">服务执行要求</div>
+                                <el-descriptions :column="2" border size="default" class="custom-desc">
+                                    <el-descriptions-item label="服务地址" :span="2">{{ order.detail.area || order.address
+                                        }}</el-descriptions-item>
+                                </el-descriptions>
+                            </div>
+                        </div>
+                    </el-tab-pane>
+
+                    <!-- Tab 2: Fulfiller Info -->
+                    <el-tab-pane label="指派履约者" name="fulfiller">
+                        <div class="tab-pane-content">
+                            <div v-if="order.fulfillerName" class="fulfiller-card">
+                                <div class="f-left">
+                                    <el-avatar :size="60" :src="order.fulfillerAvatar">{{ order.fulfillerName.charAt(0)
+                                        }}</el-avatar>
+                                </div>
+                                <div class="f-right">
+                                    <div class="f-row1">
+                                        <span class="f-name">{{ order.fulfillerName }}</span>
+                                        <el-tag size="small" type="primary" effect="plain" round>Lv1 普通</el-tag>
+                                    </div>
+                                    <div class="f-row2">
+                                        <span>联系电话:{{ order.fulfillerPhone || '138****0000' }}</span>
+                                        <span class="sep">|</span>
+                                        <span>归属区域:{{ order.fulfillerStation || '朝阳一站' }}</span>
+                                    </div>
+                                    <div class="f-row3"
+                                        style="margin-top: 8px; font-size: 13px; color: #606266; background: #f9fafe; padding: 8px; border-radius: 4px; display: flex; gap: 20px;">
+                                        <span><span style="color:#909399;">指派时间:</span>{{ order.createTime }}</span>
+                                        <span><span style="color:#909399;">接单时间:</span>{{ order.detail?.receiveTime ||
+                                            order.serviceTime }}</span>
+                                    </div>
+                                </div>
+                            </div>
+                            <div v-else class="empty-state">
+                                <el-result icon="info" title="暂无履约者" sub-title="该订单尚未指派履约人员"></el-result>
+                            </div>
+                        </div>
+                    </el-tab-pane>
+
+                    <!-- Tab 3: Service Progress -->
+                    <el-tab-pane label="服务进度" name="service">
+                        <div class="tab-pane-content">
+                            <div v-if="serviceProgressSteps.length === 0" class="empty-progress"
+                                style="padding:40px; text-align:center; color:#909399;">
+                                <el-result icon="info" title="待接单" sub-title="履约者接单后将在此记录服务进度"></el-result>
+                            </div>
+
+                            <el-timeline style="padding: 10px 20px;" v-else>
+                                <el-timeline-item v-for="(step, index) in serviceProgressSteps" :key="index"
+                                    :timestamp="step.time" placement="top" :color="step.color" :icon="step.icon"
+                                    size="large">
+                                    <div class="progress-card">
+                                        <h4 class="p-title">{{ step.title }}</h4>
+                                        <p class="p-desc">{{ step.desc }}</p>
+                                        <div class="p-media" v-if="step.media && step.media.length">
+                                            <div v-for="(item, i) in step.media" :key="i" class="media-item">
+                                                <el-image v-if="item.type === 'image'" :src="item.url"
+                                                    :preview-src-list="step.media.map(m => m.url)" fit="cover"
+                                                    class="p-img" :preview-teleported="true" />
+                                            </div>
+                                        </div>
+                                    </div>
+                                </el-timeline-item>
+                            </el-timeline>
+                        </div>
+                    </el-tab-pane>
+
+                    <!-- Tab 4: Logs -->
+                    <el-tab-pane label="订单日志" name="logs">
+                        <div class="tab-pane-content">
+                            <div style="display: flex; justify-content: flex-end; margin-bottom: 15px;">
+                                <el-button type="primary" size="small" icon="Download"
+                                    @click="handleExportLogs">导出日志Excel</el-button>
+                            </div>
+                            <el-timeline>
+                                <el-timeline-item v-for="(log, index) in (orderLogs || [])" :key="index" :timestamp="''"
+                                    :type="'primary'" :icon="undefined" placement="top">
+                                    <div class="log-card">
+                                        <div class="l-tit">{{ log.title }}</div>
+                                        <div class="l-txt">{{ log.content }}</div>
+                                    </div>
+                                </el-timeline-item>
+                            </el-timeline>
+                        </div>
+                    </el-tab-pane>
+                </el-tabs>
+            </div>
+        </div>
+    </el-drawer>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getPet } from '@/api/archieves/pet'
+import { getCustomer } from '@/api/archieves/customer'
+import { listSubOrderLog } from '@/api/order/subOrderLog'
+
+const props = defineProps({
+    visible: Boolean,
+    order: Object
+})
+
+const emit = defineEmits(['update:visible', 'dispatch', 'cancel', 'command', 'care-summary'])
+
+const drawerVisible = computed({
+    get: () => props.visible,
+    set: (val) => emit('update:visible', val)
+})
+
+const orderDetail = ref(null)
+const order = computed(() => orderDetail.value || props.order)
+
+const loadSeq = ref(0)
+
+const orderLogs = ref([])
+const fulfillerLogs = ref([])
+
+const loadOrderLogs = async (order) => {
+    const id = order?.id
+    if (!id) {
+        orderLogs.value = []
+        fulfillerLogs.value = []
+        return
+    }
+    try {
+        const res = await listSubOrderLog({ orderId: id })
+        const list = res?.data?.data || res?.data || []
+        const arr = Array.isArray(list) ? list : []
+        orderLogs.value = arr.filter(i => Number(i?.logType) === 0)
+        fulfillerLogs.value = arr.filter(i => Number(i?.logType) === 1)
+    } catch {
+        orderLogs.value = []
+        fulfillerLogs.value = []
+    }
+}
+
+const loadPetAndCustomer = async (order) => {
+    const seq = ++loadSeq.value
+    const next = { ...(order || {}) }
+
+    const petId = next?.pet || next?.petId
+    if (petId) {
+        try {
+            const res = await getPet(petId)
+            const pet = res?.data
+            if (pet) {
+                next.petName = pet.name ?? next.petName
+                next.petAvatar = pet.avatarUrl ?? next.petAvatar
+                next.petGender = pet.gender ?? next.petGender
+                next.petAge = (pet.age !== undefined && pet.age !== null) ? `${pet.age}岁` : next.petAge
+                next.petWeight = (pet.weight !== undefined && pet.weight !== null) ? `${pet.weight}kg` : next.petWeight
+                next.petBreed = pet.breed ?? next.petBreed
+                next.petSterilized = (pet.isSterilized !== undefined && pet.isSterilized !== null)
+                    ? (Number(pet.isSterilized) === 1)
+                    : next.petSterilized
+                next.petVaccine = pet.vaccineStatus ?? next.petVaccine
+                next.petCharacter = pet.personality ?? next.petCharacter
+                next.petHealth = pet.healthStatus ?? next.petHealth
+            }
+        } catch {
+        }
+    }
+
+    const customerId = next?.customer || next?.customerId
+    if (customerId) {
+        try {
+            const res = await getCustomer(customerId)
+            const customer = res?.data
+            if (customer) {
+                next.userName = customer.name ?? next.userName
+                next.userAvatar = customer.avatarUrl ?? next.userAvatar
+                next.contactPhone = customer.phone ?? next.contactPhone
+                next.city = customer.areaName ?? next.city
+                next.address = customer.address ?? next.address
+            }
+        } catch {
+        }
+    }
+
+    if (seq !== loadSeq.value) return
+    orderDetail.value = next
+}
+
+watch(() => props.order, (val) => {
+    if (!val) {
+        orderDetail.value = null
+        orderLogs.value = []
+        fulfillerLogs.value = []
+        return
+    }
+    loadPetAndCustomer(val)
+    loadOrderLogs(val)
+}, { immediate: true, deep: true })
+
+const activeDetailTab = ref('basic')
+
+const getStatusName = (status) => {
+    const map = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '待商家确认', 5: '已完成', 6: '已取消' }
+    return map[status] || '未知'
+}
+const getStatusTag = (status) => {
+    const map = { 0: 'danger', 1: 'warning', 2: 'primary', 3: 'primary', 4: 'warning', 5: 'success', 6: 'info' }
+    return map[status] || 'info'
+}
+const getTypeName = (type) => {
+    const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' }
+    return map[type]
+}
+const getTransportModeName = (type) => {
+    const map = { round: '往返接送', pick: '单程接(到店)', drop: '单程送(回家)' }
+    return map[type] || '接送服务'
+}
+
+const getTransportLabel = (t) => {
+    if (t === 0 || t === '0') return '接'
+    if (t === 1 || t === '1') return '送'
+    if (t === 2 || t === '2') return '单程接'
+    if (t === 3 || t === '3') return '单程送'
+    return '接送'
+}
+const getServiceTimeRange = (timeStr) => {
+    if (!timeStr) return '--'
+    try {
+        if (timeStr.length < 16) return timeStr
+        let timePart = timeStr.substring(11, 16)
+        let [hh, mm] = timePart.split(':').map(Number)
+        let endH = hh + 2
+        if (endH >= 24) endH -= 24
+        let endHStr = endH.toString().padStart(2, '0')
+        return `${timeStr}-${endHStr}:${mm.toString().padStart(2, '0')}`
+    } catch (e) {
+        return timeStr
+    }
+}
+
+const currentOrderSteps = computed(() => {
+    if (!props.order) return { active: 0, steps: [] }
+    const steps = [
+        { title: '商户下单', status: 'created', time: '' },
+        { title: '运营派单', status: 'dispatched', time: '' },
+        { title: '履约接单', status: 'accepted', time: '' },
+        { title: '服务中', status: 'serving', time: '' },
+        { title: '待商家确认', status: 'confirming', time: '' },
+        { title: '已完成', status: 'completed', time: '' }
+    ]
+    const logs = orderLogs.value || []
+    const status = props.order.status
+    let active = 0
+    const findTime = (keyword) => {
+        const log = logs.find(l => l.title.includes(keyword) || l.content.includes(keyword))
+        return log ? log.time : ''
+    }
+    steps[0].time = props.order.createTime || findTime('下单') || findTime('创建')
+    if (steps[0].time) active = 1
+    if ([0].includes(status)) {
+        steps[1].time = findTime('派单') || steps[0].time
+    } else {
+        steps[1].time = findTime('派单') || ''
+    }
+    if ([1, 2, 3, 4, 5].includes(status)) active = 2
+    steps[2].time = findTime('接单')
+    if ([1].includes(status)) {
+        steps[2].title = '待履约者接单'
+    } else if ([2, 3, 4, 5].includes(status)) {
+        steps[2].title = '履约者已接单'
+        active = 3
+    }
+    steps[3].time = findTime('到达') || findTime('出发')
+    if ([2].includes(status)) {
+        steps[3].title = '待服务'
+    } else if ([3].includes(status)) {
+        steps[3].title = '服务进行中'
+        active = 4
+    } else if ([4, 5].includes(status)) {
+        steps[3].title = '服务已完成'
+        active = 4
+    }
+    steps[4].time = findTime('等待商家确认') || findTime('待验收')
+    if ([4].includes(status)) {
+        steps[4].title = '待商家确认'
+        active = 5
+    } else if ([5].includes(status)) {
+        steps[4].title = '商家已确认'
+        active = 5
+    }
+    if (status === 5) {
+        steps[5].time = findTime('完成')
+        active = 6
+    }
+    if (status === 6) {
+        return {
+            active: 1,
+            steps: [
+                { title: '商户下单', time: steps[0].time },
+                { title: '已取消', time: findTime('取消') || '订单已取消' }
+            ]
+        }
+    }
+    return { active, steps }
+})
+
+const serviceProgressSteps = computed(() => {
+    const list = fulfillerLogs.value || []
+    return list.map((i) => {
+        const photos = (i?.photos || '')
+            .split(',')
+            .map(s => s.trim())
+            .filter(Boolean)
+            .map(url => ({ type: 'image', url }))
+        return {
+            title: i?.title || '--',
+            time: '',
+            icon: undefined,
+            color: '#ff9900',
+            desc: i?.content || '',
+            media: photos
+        }
+    })
+})
+
+const handleExportLogs = () => {
+    const logs = orderLogs.value || []
+    if (logs.length === 0) {
+        ElMessage.warning('暂无日志可导出')
+        return
+    }
+    let csvContent = "时间,类型,标题,内容\n"
+    logs.forEach(log => {
+        const time = ''
+        const type = log.logType ?? ''
+        const title = (log.title || '').replace(/"/g, '""')
+        const content = (log.content || '').replace(/"/g, '""')
+        csvContent += `${time},${type},"${title}","${content}"\n`
+    })
+    const blob = new Blob(["\uFEFF" + csvContent], { type: 'text/csv;charset=utf-8;' })
+    const url = URL.createObjectURL(blob)
+    const link = document.createElement("a")
+    link.href = url
+    link.download = `OrderLogs_${props.order.orderNo}.csv`
+    link.click()
+    URL.revokeObjectURL(url)
+    ElMessage.success('导出成功')
+}
+</script>
+
+<style scoped>
+/* Detail Styles */
+.order-detail-drawer :deep(.el-drawer__body) {
+    padding: 0 !important;
+}
+
+.detail-container {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    background: #f5f7fa;
+}
+
+.detail-header {
+    background: #fff;
+    padding: 20px 24px;
+    border-bottom: 1px solid #ebeef5;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.left-head {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+}
+
+.order-no {
+    font-size: 20px;
+    font-weight: bold;
+    color: #303133;
+}
+
+.type-tag {
+    font-weight: normal;
+}
+
+.detail-actions {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+}
+
+.detail-scroll-area {
+    flex: 1;
+    overflow-y: auto;
+    padding: 20px 24px;
+}
+
+/* Progress */
+.progress-section {
+    background: #fff;
+    padding: 30px 20px 20px;
+    border-radius: 8px;
+    margin-bottom: 20px;
+    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
+}
+
+.custom-steps :deep(.el-step__title) {
+    font-size: 13px;
+}
+
+/* Top Info Row */
+.top-info-row {
+    display: flex;
+    gap: 20px;
+    margin-bottom: 20px;
+    align-items: stretch;
+}
+
+.info-section {
+    flex: 1;
+    background: #fff;
+    border-radius: 8px;
+    padding: 15px;
+    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
+}
+
+.sec-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 15px;
+    padding-bottom: 10px;
+    border-bottom: 1px solid #f2f2f2;
+}
+
+.sec-header .label {
+    font-weight: bold;
+    font-size: 15px;
+    color: #303133;
+    border-left: 3px solid #409eff;
+    padding-left: 8px;
+}
+
+/* Pet Section */
+.pet-basic-row {
+    display: flex;
+    gap: 15px;
+    margin-bottom: 15px;
+    align-items: center;
+}
+
+.pet-avatar-lg {
+    border-radius: 8px;
+    background: #ecf5ff;
+    color: #409eff;
+    font-size: 20px;
+    font-weight: bold;
+}
+
+.pet-names {
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+}
+
+.b-name {
+    font-size: 18px;
+    font-weight: bold;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+
+.b-tags {
+    display: flex;
+    gap: 5px;
+}
+
+.pet-desc :deep(.el-descriptions__label) {
+    width: 70px;
+}
+
+/* User Section */
+.u-row {
+    display: flex;
+    gap: 12px;
+    align-items: center;
+    margin-bottom: 12px;
+}
+
+.u-info .nm {
+    font-weight: bold;
+    font-size: 15px;
+    color: #303133;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+}
+
+.u-info .ph {
+    font-size: 13px;
+    color: #909399;
+    margin-top: 2px;
+}
+
+.addr-box {
+    background: #fdf6ec;
+    padding: 8px 10px;
+    border-radius: 4px;
+    margin-bottom: 10px;
+}
+
+.addr-label {
+    font-size: 12px;
+    color: #e6a23c;
+    margin-bottom: 2px;
+    font-weight: bold;
+}
+
+.addr-txt {
+    font-size: 13px;
+    color: #606266;
+    line-height: 1.4;
+}
+
+/* Tabs */
+.detail-tabs {
+    background: #fff;
+    padding: 10px 20px;
+    border-radius: 8px;
+    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
+    min-height: 400px;
+}
+
+.tab-pane-content {
+    padding: 15px 0;
+}
+
+/* Fulfiller Card inside Tab */
+.fulfiller-card {
+    display: flex;
+    align-items: center;
+    gap: 20px;
+    padding: 20px;
+    background: #fff;
+    border: 1px solid #ebeef5;
+    border-radius: 8px;
+}
+
+.f-right {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+}
+
+.f-row1 {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+}
+
+.f-name {
+    font-size: 18px;
+    font-weight: bold;
+    color: #303133;
+}
+
+.f-row2 {
+    font-size: 13px;
+    color: #606266;
+    display: flex;
+    gap: 10px;
+}
+
+.sep {
+    color: #e4e7ed;
+}
+
+.empty-state {
+    padding: 40px 0;
+    text-align: center;
+}
+
+/* Progress Card Styles */
+.progress-card {
+    background: #f8fcfb;
+    border-radius: 8px;
+    padding: 12px;
+    border: 1px solid #ebeef5;
+}
+
+.p-title {
+    margin: 0 0 8px;
+    font-size: 15px;
+    font-weight: bold;
+    color: #303133;
+}
+
+.p-desc {
+    margin: 0 0 12px;
+    color: #606266;
+    font-size: 13px;
+    line-height: 1.5;
+}
+
+.p-media {
+    display: flex;
+    gap: 8px;
+    flex-wrap: wrap;
+}
+
+.media-item {
+    display: inline-block;
+}
+
+.p-img {
+    width: 80px;
+    height: 80px;
+    border-radius: 4px;
+    border: 1px solid #e4e7ed;
+    cursor: pointer;
+}
+
+/* New Transport Split Styles */
+.transport-split-block {
+    margin-top: 20px;
+}
+
+.transport-grid {
+    display: flex;
+    gap: 20px;
+}
+
+.transport-card {
+    flex: 1;
+    border: 1px solid #ebeef5;
+    border-radius: 6px;
+    overflow: hidden;
+    background: #fff;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02);
+}
+
+.transport-card .t-header {
+    background: #f5f7fa;
+    padding: 10px 15px;
+    border-bottom: 1px solid #ebeef5;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.transport-card .t-header .time {
+    font-size: 13px;
+    font-weight: bold;
+    color: #f56c6c;
+}
+
+.transport-card .t-body {
+    padding: 15px;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+}
+
+.transport-card .row {
+    display: flex;
+    align-items: flex-start;
+    gap: 8px;
+    font-size: 14px;
+    color: #303133;
+    line-height: 1.4;
+}
+
+.transport-card .row.sub {
+    color: #909399;
+    font-size: 13px;
+    margin-top: 4px;
+}
+
+.transport-card .row .el-icon {
+    margin-top: 3px;
+}
+
+.transport-one {
+    border: 1px solid #ebeef5;
+    border-radius: 6px;
+    background: #fff;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02);
+    padding: 14px 16px;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+}
+
+.transport-one .t-row {
+    display: flex;
+    align-items: baseline;
+    gap: 10px;
+}
+
+.transport-one .t-row .time {
+    font-size: 13px;
+    font-weight: bold;
+    color: #f56c6c;
+}
+
+.transport-one .t-k {
+    width: 40px;
+    color: #909399;
+    font-size: 13px;
+}
+
+.transport-one .t-v {
+    color: #303133;
+    font-size: 14px;
+    line-height: 1.4;
+}
+
+.transport-one .t-row.sub .t-v {
+    color: #909399;
+    font-size: 13px;
+}
+
+/* Logs */
+.log-card {
+    background: #f4f4f5;
+    padding: 10px 15px;
+    border-radius: 4px;
+    position: relative;
+    top: -5px;
+    width: 100%;
+}
+
+.l-tit {
+    font-weight: bold;
+    font-size: 14px;
+    margin-bottom: 4px;
+    color: #303133;
+}
+
+.l-txt {
+    font-size: 13px;
+    color: #606266;
+    line-height: 1.5;
+}
+</style>

+ 47 - 0
src/views/order/orderList/components/RemarkDialog.vue

@@ -0,0 +1,47 @@
+<template>
+    <el-dialog v-model="dialogVisible" title="订单备注" width="500px">
+        <div style="margin-bottom:10px; font-size:13px; color:#909399;">
+            <span v-if="order">订单号:{{ order.orderNo }}</span>
+        </div>
+        <el-input 
+            v-model="remarkForm" 
+            type="textarea" 
+            :rows="5" 
+            placeholder="请输入订单备注信息..." 
+        />
+        <template #footer>
+            <span class="dialog-footer">
+                <el-button @click="dialogVisible = false">取消</el-button>
+                <el-button type="primary" @click="handleSubmit">保存备注</el-button>
+            </span>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+
+const props = defineProps({
+    visible: Boolean,
+    order: Object
+})
+const emit = defineEmits(['update:visible', 'submit'])
+
+const dialogVisible = computed({
+    get: () => props.visible,
+    set: (val) => emit('update:visible', val)
+})
+
+const remarkForm = ref('')
+
+watch(() => props.visible, (val) => {
+    if (val && props.order) {
+        remarkForm.value = props.order.remark || ''
+    }
+})
+
+const handleSubmit = () => {
+    emit('submit', remarkForm.value)
+    dialogVisible.value = false
+}
+</script>

+ 101 - 0
src/views/order/orderList/components/RewardDialog.vue

@@ -0,0 +1,101 @@
+<template>
+  <el-dialog v-model="dialogVisible" title="奖惩操作" width="500px">
+    <div v-if="order" style="padding: 0 10px">
+      <div style="margin-bottom: 20px; font-size: 14px; color: #606266; line-height: 1.6; background: #fdf6ec; padding: 10px; border-radius: 4px">
+        <div>
+          奖惩履约者:<span style="font-weight: bold; color: #303133">{{ order.fulfillerName || '未指派' }}</span>
+        </div>
+        <div style="font-size: 13px; margin-top: 4px">订单号:{{ order.orderNo }}</div>
+        <div style="font-size: 13px; margin-top: 4px; display: flex; align-items: center; gap: 6px">
+          服务类型:
+          <el-tag :type="getTypeTag(order.type)" size="small">{{ getTypeName(order.type) }}</el-tag>
+          <el-tag v-if="order.type === 'transport' && order.transportType === 'round'" size="small" effect="plain" type="warning">往返</el-tag>
+          <el-tag v-if="order.splitType === 'pick'" size="small" effect="dark" color="#409eff" style="border: none; color: white">接</el-tag>
+          <el-tag v-if="order.splitType === 'drop'" size="small" effect="dark" color="#67c23a" style="border: none; color: white">送</el-tag>
+          <el-tag v-if="order.type === 'transport' && order.transportType === 'pick' && !order.splitType" size="small" effect="plain">单程接</el-tag>
+          <el-tag v-if="order.type === 'transport' && order.transportType === 'drop' && !order.splitType" size="small" effect="plain" type="success"
+            >单程送</el-tag
+          >
+        </div>
+      </div>
+
+      <el-form :model="rewardForm" label-width="80px">
+        <el-form-item label="操作类型">
+          <el-radio-group v-model="rewardForm.type">
+            <el-radio label="reward">奖励 (增加)</el-radio>
+            <el-radio label="punish">惩罚 (扣除)</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="调整项目">
+          <el-radio-group v-model="rewardForm.item">
+            <el-radio label="points">积分</el-radio>
+            <el-radio label="balance">金额 (元)</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="数额" required>
+          <el-input-number v-model="rewardForm.value" :min="1" :step="10" />
+        </el-form-item>
+        <el-form-item label="原因备注" required>
+          <el-input v-model="rewardForm.reason" type="textarea" :rows="3" placeholder="请输入奖惩原因..." />
+        </el-form-item>
+      </el-form>
+    </div>
+    <template #footer>
+      <el-button @click="dialogVisible = false">取消</el-button>
+      <el-button type="primary" @click="handleSubmit">确认执行</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { reactive, computed, watch } from 'vue';
+import { ElMessage } from 'element-plus';
+
+const props = defineProps({
+  visible: Boolean,
+  order: Object
+});
+const emit = defineEmits(['update:visible', 'submit']);
+
+const dialogVisible = computed({
+  get: () => props.visible,
+  set: (val) => emit('update:visible', val)
+});
+
+const rewardForm = reactive({
+  type: 'reward',
+  item: 'points',
+  value: 10,
+  reason: ''
+});
+
+watch(
+  () => props.visible,
+  (val) => {
+    if (val) {
+      rewardForm.type = 'reward';
+      rewardForm.item = 'points';
+      rewardForm.value = 10;
+      rewardForm.reason = '';
+    }
+  }
+);
+
+const getTypeTag = (type) => {
+  const map = { transport: '', feeding: 'warning', washing: 'success' };
+  return map[type];
+};
+const getTypeName = (type) => {
+  const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' };
+  return map[type];
+};
+
+const handleSubmit = () => {
+  if (!rewardForm.reason) {
+    ElMessage.warning('请输入奖惩原因');
+    return;
+  }
+  emit('submit', rewardForm);
+  dialogVisible.value = false;
+};
+</script>

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

@@ -0,0 +1,887 @@
+<template>
+  <div class="page-container">
+    <el-card shadow="never" class="table-card">
+      <template #header>
+        <div class="card-header">
+          <span class="title">订单列表</span>
+          <div class="right-panel">
+            <el-radio-group v-model="filters.service" size="default" @change="handleSearch">
+              <el-radio-button label="">全部类型</el-radio-button>
+              <el-radio-button v-for="item in serviceOptions" :key="item.id" :label="item.id">{{ item.name
+              }}</el-radio-button>
+            </el-radio-group>
+            <el-input v-model="filters.content" placeholder="订单号/商户/宠主/手机号" class="search-input" prefix-icon="Search"
+              clearable @clear="handleSearch" @keyup.enter="handleSearch" />
+            <el-button type="primary" icon="Search" @click="handleSearch">查询</el-button>
+          </div>
+        </div>
+
+        <el-tabs v-model="filters.status" class="status-tabs" @tab-change="handleStatusTabChange">
+          <el-tab-pane label="全部订单" name="" />
+          <el-tab-pane label="待派单" name="0" />
+          <el-tab-pane label="待接单" name="1" />
+          <el-tab-pane label="待服务" name="2" />
+          <el-tab-pane label="服务中" name="3" />
+          <el-tab-pane label="待商家确认" name="4" />
+          <el-tab-pane label="已完成" name="5" />
+          <el-tab-pane label="已取消" name="6" />
+        </el-tabs>
+      </template>
+
+      <el-table :data="tableData" style="width: 100%" v-loading="loading"
+        :header-cell-style="{ background: '#f5f7fa' }">
+        <el-table-column prop="code" label="订单号" width="170" fixed="left" />
+
+        <el-table-column label="服务类型" width="190">
+          <template #default="{ row }">
+            <div class="service-type-cell">
+              <el-tag>{{ getServiceName(row.service) }}</el-tag>
+              <el-tag v-if="getServiceModeTag(row)" class="sub-tag" type="warning" effect="plain">{{
+                getServiceModeTag(row) }}</el-tag>
+              <el-tag v-if="getServiceOrderTypeTag(row)" class="sub-tag" :type="getServiceOrderTypeTag(row).type"
+                effect="dark">{{
+                  getServiceOrderTypeTag(row).label
+                }}</el-tag>
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="宠物信息" min-width="150">
+          <template #default="{ row }">
+            <div class="pet-info">
+              <el-avatar :size="30" class="avatar-type">{{ row.petName?.charAt(0) }}</el-avatar>
+              <div class="pet-detail">
+                <div class="pet-name">
+                  {{ row.petName }}
+                </div>
+                <div class="pet-breed">{{ row.petBreed }}</div>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="所属用户" width="120" prop="customerName">
+          <template #default="{ row }">
+            <span style="font-weight: 500">{{ row.customerName }}</span>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="城市/区域" width="140">
+          <template #default="{ row }">
+            <div>{{ getCityDistrictText(row).city || '-' }}</div>
+            <div class="sub-text">{{ getCityDistrictText(row).district || '-' }}</div>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="商户/下单人" min-width="160">
+          <template #default="{ row }">
+            <div class="merchant-info">
+              <div>{{ row.storeName }}</div>
+              <div class="sub-text" v-if="row.placerUsername">{{ row.placerUsername }}</div>
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column prop="createTime" label="下单时间" width="165" sortable>
+          <template #default="{ row }">
+            <span class="time-text">{{ row.createTime }}</span>
+          </template>
+        </el-table-column>
+
+        <el-table-column prop="createTime" label="预约服务时间" width="165" sortable>
+          <template #default="{ row }">
+            <span class="time-text">{{ row.serviceTime }}</span>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="订单状态" width="100">
+          <template #default="{ row }">
+            <div class="status-cell">
+              <div class="status-dot" :class="getStatusClass(row.status)"></div>
+              <span>{{ getStatusName(row.status) }}</span>
+            </div>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="履约信息" width="140">
+          <template #default="{ row }">
+            <div v-if="row.fulfillerName" class="fulfiller-info">
+              <span class="fulfiller-name">{{ row.fulfillerName }}</span>
+              <span class="fulfiller-fee" v-if="row.price !== null && row.price !== undefined">¥{{ row.price / 100.0
+              }}</span>
+            </div>
+            <span v-else class="text-gray">暂未指派</span>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="操作" width="200" fixed="right">
+          <template #default="{ row }">
+            <div class="op-cell">
+              <el-button link type="primary" size="small" @click="handleDetail(row)">详情</el-button>
+              <el-button v-if="row.status === 0" link type="success" size="small"
+                @click="openDispatchDialog(row)">派单</el-button>
+              <el-button v-if="[1, 2, 3].includes(row.status)" link type="warning" size="small"
+                @click="openDispatchDialog(row)">重新派单</el-button>
+              <el-button v-if="[0, 1].includes(row.status)" link type="danger" size="small"
+                @click="handleCancel(row)">取消</el-button>
+
+              <el-dropdown v-if="[3, 4, 5].includes(row.status)" trigger="click"
+                @command="(cmd) => handleCommand(cmd, row)">
+                <span class="el-dropdown-link">
+                  更多<el-icon class="el-icon--right">
+                    <ArrowDown />
+                  </el-icon>
+                </span>
+                <template #dropdown>
+                  <el-dropdown-menu>
+                    <el-dropdown-item v-if="row.status === 4" command="complete">确认完成</el-dropdown-item>
+                    <el-dropdown-item v-if="[4, 5].includes(row.status)" command="care_summary">护理小结</el-dropdown-item>
+                    <el-dropdown-item command="reward">奖惩</el-dropdown-item>
+                    <el-dropdown-item command="remark">备注</el-dropdown-item>
+                  </el-dropdown-menu>
+                </template>
+              </el-dropdown>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="pagination-container">
+        <el-pagination v-model:current-page="pagination.current" v-model:page-size="pagination.size"
+          :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" :total="pagination.total"
+          @size-change="handleSizeChange" @current-change="handleCurrentChange" />
+      </div>
+    </el-card>
+
+    <!-- 组件 -->
+    <OrderDetailDrawer v-model:visible="detailVisible" :order="currentOrder" @dispatch="openDispatchDialog"
+      @cancel="handleCancel" @command="handleCommand" @care-summary="openCareSummary" />
+
+    <DispatchDialog v-model:visible="dispatchDialogVisible" :order="currentDispatchOrder"
+      @submit="handleDispatchSubmit" />
+
+    <CareSummaryDrawer v-model:visible="careSummaryVisible" :order="careSummaryOrder" @submit="saveCareSummary" />
+
+    <RewardDialog v-model:visible="rewardDialogVisible" :order="currentOperateRow" @submit="handleRewardSubmit" />
+
+    <RemarkDialog v-model:visible="remarkDialogVisible" :order="currentOperateRow" @submit="handleRemarkSubmit" />
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, nextTick } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import OrderDetailDrawer from './components/OrderDetailDrawer.vue';
+import DispatchDialog from './components/DispatchDialog.vue';
+import CareSummaryDrawer from './components/CareSummaryDrawer.vue';
+import RewardDialog from './components/RewardDialog.vue';
+import RemarkDialog from './components/RemarkDialog.vue';
+import { listServiceOnOrder } from '@/api/service/list/index';
+import { listSubOrder } from '@/api/order/subOrder/index';
+import { dispatchSubOrder } from '@/api/order/subOrder/index';
+import { getSubOrderInfo } from '@/api/order/subOrder/index';
+import { cancelSubOrder } from '@/api/order/subOrder/index';
+import { remarkSubOrder } from '@/api/order/subOrder/index';
+import { confirmSubOrder } from '@/api/order/subOrder/index';
+import { nursingSummarySubOrder } from '@/api/order/subOrder/index';
+import { listOnStore as listAreaStationOnStore } from '@/api/system/areaStation';
+import { getStore } from '@/api/system/store';
+import { reward } from '@/api/fulfiller/pool';
+import { getPet } from '@/api/archieves/pet';
+import { getCustomer } from '@/api/archieves/customer';
+
+const loading = ref(false);
+
+const filters = reactive({
+  service: '',
+  status: '',
+  content: ''
+});
+
+const pagination = reactive({
+  current: 1,
+  size: 10,
+  total: 100
+});
+
+const tableData = ref([]);
+const serviceOptions = ref([]);
+const areaStationList = ref([]);
+const areaStationMap = ref({});
+const storeMap = ref({});
+
+onMounted(() => {
+  getServiceList();
+  getAreaStationList();
+  handleSearch();
+});
+
+const getServiceList = () => {
+  listServiceOnOrder().then((res) => {
+    serviceOptions.value = res.data || [];
+  });
+};
+
+const getAreaStationList = () => {
+  listAreaStationOnStore().then((res) => {
+    const list = res.data || [];
+    areaStationList.value = list;
+    const map = {};
+    for (const item of list) {
+      if (item && item.id !== undefined && item.id !== null) map[item.id] = item;
+    }
+    areaStationMap.value = map;
+  });
+};
+
+const getCityDistrictText = (row) => {
+  if (!row) return { city: '', district: '' };
+  const map = areaStationMap.value || {};
+  let stationId = row.site;
+  if (!map[stationId] && row.store) {
+    const store = (storeMap.value || {})[row.store];
+    if (store?.site) stationId = store.site;
+  }
+  if (!stationId) return { city: '', district: '' };
+
+  const station = map[stationId];
+  if (!station) return { city: '', district: '' };
+
+  const parent = station.parentId ? map[station.parentId] : undefined;
+  if (!parent) return { city: station.name || '', district: '' };
+
+  if (parent.type === 0) return { city: parent.name || '', district: '' };
+  if (parent.type === 1) {
+    const city = parent.parentId ? map[parent.parentId] : undefined;
+    return { city: city?.name || '', district: parent.name || '' };
+  }
+
+  return { city: '', district: parent.name || '' };
+};
+
+const handleStatusTabChange = async () => {
+  pagination.current = 1;
+  await nextTick();
+  handleSearch();
+};
+
+const handleSearch = () => {
+  loading.value = true;
+  listSubOrder({
+    pageNum: pagination.current,
+    pageSize: pagination.size,
+    service: filters.service !== '' ? filters.service : undefined,
+    status: filters.status !== '' ? Number(filters.status) : undefined,
+    content: filters.content || undefined
+  })
+    .then((res) => {
+      tableData.value = res.rows || [];
+      pagination.total = res.total || 0;
+      loadStoresForRows(tableData.value);
+      loading.value = false;
+    })
+    .catch(() => {
+      loading.value = false;
+    });
+};
+
+const loadStoresForRows = async (rows) => {
+  const map = storeMap.value || {};
+  const ids = Array.from(new Set((rows || []).map((r) => r?.store).filter(Boolean)));
+  const missing = ids.filter((id) => map[id] === undefined);
+  if (missing.length === 0) return;
+  await Promise.all(
+    missing.map(async (id) => {
+      try {
+        const res = await getStore(id);
+        if (res?.data) map[id] = res.data;
+      } catch {
+        map[id] = null;
+      }
+    })
+  );
+  storeMap.value = { ...map };
+};
+
+const handleSizeChange = (val) => {
+  pagination.size = val;
+  handleSearch();
+};
+const handleCurrentChange = (val) => {
+  pagination.current = val;
+  handleSearch();
+};
+
+const getServiceName = (serviceId) => {
+  const item = serviceOptions.value.find((i) => i.id === serviceId);
+  return item ? item.name : '未知服务';
+};
+
+const getServiceModeTag = (row) => {
+  const t = row?.type;
+  if (t === 0 || t === '0' || t === 1 || t === '1') return '往返';
+  return '';
+};
+
+const getServiceOrderTypeTag = (row) => {
+  const t = row?.type;
+  if (t === 0 || t === '0') return { label: '接', type: 'primary' };
+  if (t === 1 || t === '1') return { label: '送', type: 'success' };
+  if (t === 2 || t === '2') return { label: '单程接', type: 'primary' };
+  if (t === 3 || t === '3') return { label: '单程送', type: 'success' };
+  return null;
+};
+
+const getStatusName = (status) => {
+  const map = { 0: '待派单', 1: '待接单', 2: '待服务', 3: '服务中', 4: '待商家确认', 5: '已完成', 6: '已取消' };
+  return map[status] || '未知';
+};
+
+const getStatusClass = (status) => {
+  const map = { 0: 'pending_dispatch', 1: 'pending_accept', 2: 'pending_service', 3: 'in_service', 4: 'pending_confirm', 5: 'completed', 6: 'cancelled' };
+  return map[status] || 'pending_dispatch';
+};
+
+const getFulfillerStatusText = (status) => {
+  const statusMap = {
+    resting: '休息',
+    busy: '接单中',
+    disabled: '禁用'
+  };
+  return statusMap[status] || status;
+};
+
+const getFulfillerStatusType = (status) => {
+  const typeMap = {
+    resting: 'info',
+    busy: 'success',
+    disabled: 'danger'
+  };
+  return typeMap[status] || 'info';
+};
+
+// 弹窗状态管理
+const detailVisible = ref(false);
+const currentOrder = ref(null);
+
+const dispatchDialogVisible = ref(false);
+const currentDispatchOrder = ref(null);
+
+const careSummaryVisible = ref(false);
+const careSummaryOrder = ref(null);
+
+const rewardDialogVisible = ref(false);
+const remarkDialogVisible = ref(false);
+const currentOperateRow = ref(null);
+
+// 详情
+const handleDetail = async (row) => {
+  const typeName = getServiceName(row?.service);
+  const isTransport = row?.mode === 1 || row?.mode === '1';
+  const typeCode = isTransport ? 'transport' : typeName?.includes('喂') || typeName?.includes('遛') ? 'feeding' : 'washing';
+  currentOrder.value = {
+    ...row,
+    orderNo: row?.code || row?.orderCode || row?.orderNo || row?.orderNumber || row?.no || '',
+    type: row?.typeCode || row?.type || typeCode,
+    serviceItem: getServiceName(row?.service) || row?.serviceName || row?.service || '',
+    userAvatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
+    address: '某小区5号楼2单元101',
+    groupBuyPackage: '',
+    transportType: row.splitType || row.transportType,
+    detail: {
+      ...row.detail,
+      pickTime: '2024-02-05 09:30',
+      pickAddr: row.detail?.pickAddr || '北京市朝阳区某小区5号楼2单元101',
+      pickContact: '李先生',
+      pickPhone: '13812345678',
+      dropTime: '2024-02-05 18:30',
+      dropAddr: row.detail?.dropAddr || '北京市朝阳区某小区5号楼2单元101',
+      dropContact: '李先生',
+      dropPhone: '13812345678',
+      packageName: row.detail?.packageName || '精细洗护套餐A',
+      petStatus: '胆小,需安抚',
+      area: '北京市朝阳区某小区5号楼2单元101'
+    },
+    petGender: 'male',
+    petAge: '2岁',
+    petWeight: '15kg',
+    petVaccine: '已接种',
+    petSterilized: true,
+    petCharacter: '活泼好动,喜欢球类玩具',
+    petHealth: '健康良好',
+    fulfillerAvatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
+    fulfillerPhone: '13812345678',
+    fulfillerStation: '朝阳服务站',
+    orderLogs: [
+      { time: '2024-02-04 09:30', title: '订单创建', content: '商户提交订单', icon: 'Document' },
+      { time: '2024-02-04 10:00', title: '系统派单', content: '指派给 王大力', icon: 'Bicycle' },
+      { time: '2024-02-04 10:05', title: '接单成功', content: '履约者已确认接单', icon: 'CircleCheck' },
+      { time: '2024-02-04 13:55', title: '到达服务点', content: '履约者已打卡', icon: 'Location' }
+    ]
+  };
+
+  try {
+    const res = await getSubOrderInfo(row?.id);
+    const info = res?.data?.data || res?.data;
+    if (info) {
+      currentOrder.value = {
+        ...(currentOrder.value || {}),
+        id: info.id,
+        orderNo: info.code || currentOrder.value?.orderNo,
+        code: info.code,
+        subOrderType: info.type,
+        status: info.status ?? currentOrder.value?.status,
+        mode: info.mode ?? currentOrder.value?.mode,
+        type: info.mode === 1 || info.mode === '1' ? 'transport' : currentOrder.value?.type,
+        transportType: info.mode === 1 || info.mode === '1' ? 'round' : currentOrder.value?.transportType,
+        serviceTime: info.serviceTime || currentOrder.value?.serviceTime,
+        endServiceTime: info.endServiceTime || currentOrder.value?.endServiceTime,
+        contact: info.contact || currentOrder.value?.contact,
+        contactPhoneNumber: info.contactPhoneNumber || currentOrder.value?.contactPhoneNumber,
+        toAddress: info.toAddress || currentOrder.value?.toAddress,
+        address:
+          info.mode === 1 || info.mode === '1'
+            ? info.toAddress || currentOrder.value?.address
+            : info.address || info.toAddress || currentOrder.value?.address,
+        price: info.price !== undefined && info.price !== null ? Number(info.price) / 100 : currentOrder.value?.price,
+        fulfillerFee: info.price !== undefined && info.price !== null ? Number(info.price) / 100 : currentOrder.value?.fulfillerFee,
+        merchantName: info.storeName || currentOrder.value?.merchantName,
+        platformId: info.platformId ?? currentOrder.value?.platformId,
+        groupBuyPackage: info.groupPurchasePackageName || currentOrder.value?.groupBuyPackage,
+        fulfiller: info.fulfiller ?? currentOrder.value?.fulfiller,
+        detail: {
+          ...(currentOrder.value?.detail || {}),
+          pickTime: info.serviceTime || currentOrder.value?.detail?.pickTime,
+          pickAddr: info.fromAddress || currentOrder.value?.detail?.pickAddr,
+          pickContact: info.contact || currentOrder.value?.detail?.pickContact,
+          pickPhone: info.contactPhoneNumber || currentOrder.value?.detail?.pickPhone,
+          dropTime: info.endServiceTime || currentOrder.value?.detail?.dropTime,
+          dropAddr: info.toAddress || currentOrder.value?.detail?.dropAddr,
+          dropContact: info.contact || currentOrder.value?.detail?.dropContact,
+          dropPhone: info.contactPhoneNumber || currentOrder.value?.detail?.dropPhone,
+          fromAddress: info.fromAddress || currentOrder.value?.detail?.fromAddress,
+          toAddress: info.toAddress || currentOrder.value?.detail?.toAddress,
+          area: info.area || info.address || info.toAddress || currentOrder.value?.detail?.area,
+          packageName: info.packageName || info.servicePackageName || info.package || currentOrder.value?.detail?.packageName,
+          petStatus: info.petStatus || info.specialRequirement || info.requirement || currentOrder.value?.detail?.petStatus,
+          fromCode: info.fromCode || currentOrder.value?.detail?.fromCode,
+          toCode: info.toCode || currentOrder.value?.detail?.toCode
+        }
+      };
+    }
+  } catch { }
+  detailVisible.value = true;
+};
+
+// 取消订单
+const handleCancel = (row) => {
+  ElMessageBox.confirm('确认取消该订单吗?', '提示', { type: 'warning' }).then(() => {
+    cancelSubOrder({ orderId: row?.id }).then(() => {
+      ElMessage.success('订单已取消');
+      handleSearch();
+    });
+  });
+};
+
+// 派单
+const openDispatchDialog = (row) => {
+  const typeName = getServiceName(row?.service);
+  const isTransport = row?.mode === 1 || row?.mode === '1';
+  const typeCode = isTransport ? 'transport' : typeName?.includes('喂') || typeName?.includes('遛') ? 'feeding' : 'washing';
+
+  const t = row?.subOrderType ?? row?.type;
+  const transportType =
+    t === 0 || t === '0' || t === 1 || t === '1'
+      ? 'round'
+      : t === 2 || t === '2'
+        ? 'pick'
+        : t === 3 || t === '3'
+          ? 'drop'
+          : row?.splitType || row?.transportType;
+
+  const toAddress = row?.toAddress || '';
+  const pickAddr = isTransport ? toAddress : '';
+  const dropAddr = isTransport ? toAddress : '';
+  const address = isTransport ? '' : toAddress;
+
+  const orderObj = {
+    id: row.id,
+    typeCode,
+    transportType,
+    time: row.serviceTime || row.appointTime || row.createTime,
+    status: row.status,
+    address,
+    pickAddr,
+    dropAddr,
+    service: row.service,
+    riderId: row.riderId || row.fulfiller || null
+  };
+  currentDispatchOrder.value = orderObj;
+  dispatchDialogVisible.value = true;
+};
+
+const handleDispatchSubmit = (payload) => {
+  if (!currentDispatchOrder.value) return;
+  const priceFen = Math.round(Number(payload.fee || 0) * 100);
+  dispatchSubOrder({
+    orderId: currentDispatchOrder.value.id,
+    fulfiller: payload.riderId,
+    price: priceFen
+  }).then(() => {
+    ElMessage.success('派单成功');
+    const row = tableData.value.find((r) => r.id === currentDispatchOrder.value.id);
+    if (row) {
+      row.status = 1;
+      row.fulfillerName = payload.riderName || 'Unknown';
+      row.price = payload.fee;
+    }
+    handleSearch();
+  });
+};
+
+// 护理小结
+const openCareSummary = async (row) => {
+  const orderData = {
+    ...row,
+    careSummary: row.nursingSummary || undefined
+  };
+
+  // 获取宠物信息
+  const petId = row?.pet || row?.petId;
+  if (petId) {
+    try {
+      const petRes = await getPet(petId);
+      const pet = petRes?.data;
+      if (pet) {
+        orderData.petAge = pet.age ? `${pet.age}岁` : '未知';
+        orderData.petGender = pet.gender || 'male';
+        orderData.petTags = pet.tags || [];
+        orderData.petWeight = pet.weight ? `${pet.weight} kg` : '未知';
+        orderData.petPersonality = pet.personality || '未知';
+        orderData.healthStatus = pet.healthStatus || '未知';
+        orderData.vaccineImg = pet.vaccineImg || '';
+        orderData.allergy = pet.allergy || '无';
+        orderData.petBreed = pet.breed || '未知';
+      }
+    } catch { }
+  }
+
+  // 获取客户信息(宠物主人)
+  const customerId = row?.customer || row?.customerId;
+  if (customerId) {
+    try {
+      const customerRes = await getCustomer(customerId);
+      const customer = customerRes?.data;
+      if (customer) {
+        orderData.userName = customer.name || orderData.userName;
+        orderData.homeTime = customer.homeTime || '未知';
+        orderData.houseType = customer.houseType || '未知';
+        orderData.entryMethod = customer.entryMethod || '未知';
+        orderData.entryDetail = customer.entryMethod === 'password'
+          ? (customer.entryPassword ? `密码: ${customer.entryPassword}` : '未设置密码')
+          : (customer.keyLocation || '未设置');
+      }
+    } catch { }
+  }
+
+  orderData.summaryTime = row.nursingSummaryTime || orderData.summaryTime;
+  careSummaryOrder.value = orderData;
+  careSummaryVisible.value = true;
+};
+
+const saveCareSummary = async (text) => {
+  if (!careSummaryOrder.value?.id) {
+    ElMessage.warning('订单信息不存在');
+    return;
+  }
+  try {
+    await nursingSummarySubOrder({
+      orderId: careSummaryOrder.value.id,
+      content: text
+    });
+    if (careSummaryOrder.value) {
+      careSummaryOrder.value.careSummary = text;
+      careSummaryOrder.value.nursingSummary = text;
+    }
+    ElMessage.success('护理小结已保存');
+    handleSearch();
+  } catch {
+    // Error handled by interceptor
+  }
+};
+
+// 奖惩
+const openRewardDialog = (row) => {
+  const typeName = getServiceName(row?.service);
+  const isTransport = row?.mode === 1 || row?.mode === '1';
+  const typeCode = isTransport ? 'transport' : typeName?.includes('喂') || typeName?.includes('遛') ? 'feeding' : 'washing';
+
+  const t = row?.subOrderType ?? row?.type;
+  const transportType =
+    typeof t === 'number' ? (t === 0 ? 'pick' : t === 1 ? 'drop' : t === 2 ? 'round' : undefined) :
+      (t === 'pick' || t === 'drop' || t === 'round' ? t : undefined);
+
+  currentOperateRow.value = {
+    ...row,
+    orderNo: row?.code || row?.orderNo,
+    type: typeCode,
+    transportType,
+    splitType: row?.splitType
+  };
+  rewardDialogVisible.value = true;
+};
+const handleRewardSubmit = async (form) => {
+  if (!currentOperateRow.value?.fulfiller) {
+    ElMessage.warning('当前订单未指派履约者,无法执行奖惩操作');
+    return;
+  }
+  try {
+    await reward({
+      fulfillerId: currentOperateRow.value.fulfiller,
+      type: form.type,
+      target: form.item,
+      amount: form.value,
+      reason: form.reason
+    });
+    ElMessage.success(`操作成功:${form.type === 'reward' ? '奖励' : '惩罚'}已执行`);
+    handleSearch();
+  } catch {
+    // Error handled by interceptor
+  }
+};
+
+// 备注
+const openRemarkDialog = (row) => {
+  currentOperateRow.value = {
+    ...row,
+    orderNo: row?.code || row?.orderNo
+  };
+  remarkDialogVisible.value = true;
+};
+const handleRemarkSubmit = async (text) => {
+  if (!currentOperateRow.value?.id) {
+    ElMessage.warning('订单信息不存在');
+    return;
+  }
+  try {
+    await remarkSubOrder({
+      orderId: currentOperateRow.value.id,
+      remark: text
+    });
+    if (currentOperateRow.value) {
+      currentOperateRow.value.remark = text;
+    }
+    ElMessage.success('备注已保存');
+    handleSearch();
+  } catch {
+    // Error handled by interceptor
+  }
+};
+
+// 更多操作
+const handleCommand = (cmd, row) => {
+  if (cmd === 'reward') openRewardDialog(row);
+  if (cmd === 'remark') openRemarkDialog(row);
+  if (cmd === 'care_summary') openCareSummary(row);
+  if (cmd === 'complete') {
+    ElMessageBox.confirm('确认将该订单手动标记为完成吗?', '提示', { type: 'warning' }).then(async () => {
+      try {
+        await confirmSubOrder({ id: row.id });
+        ElMessage.success('订单已确认完成');
+        handleSearch();
+      } catch {
+        // Error handled by interceptor
+      }
+    }).catch(() => { });
+  }
+  if (cmd === 'delete') {
+    ElMessageBox.confirm('确认删除该订单吗?此操作不可恢复', '警告', { type: 'error' }).then(() => {
+      tableData.value = tableData.value.filter((item) => item.id !== row.id);
+      ElMessage.success('订单已删除');
+    });
+  }
+};
+</script>
+
+<style scoped>
+.page-container {
+  padding: 20px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.title {
+  font-weight: bold;
+  font-size: 18px;
+}
+
+.right-panel {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+}
+
+.search-input {
+  width: 220px;
+}
+
+.status-tabs {
+  margin-top: 10px;
+  margin-bottom: -10px;
+}
+
+.pagination-container {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 20px;
+}
+
+/* Table Content Styles */
+.service-type-cell {
+  display: flex;
+  flex-direction: row;
+  gap: 4px;
+  align-items: center;
+}
+
+.sub-tag {
+  font-size: 11px;
+  height: 20px;
+  padding: 0 5px;
+}
+
+.pet-info {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.pet-info .el-avatar {
+  background: #e0eaff;
+  color: #409eff;
+  font-weight: bold;
+  flex-shrink: 0;
+}
+
+.pet-info .avatar-feeding {
+  background: #fdf6ec;
+  color: #e6a23c;
+}
+
+.pet-info .avatar-washing {
+  background: #f0f9eb;
+  color: #67c23a;
+}
+
+.pet-detail {
+  display: flex;
+  flex-direction: column;
+  line-height: 1.4;
+}
+
+.pet-name {
+  font-weight: bold;
+  font-size: 14px;
+  color: #303133;
+}
+
+.pet-breed {
+  color: #909399;
+  font-weight: normal;
+  font-size: 12px;
+}
+
+.merchant-info {
+  display: flex;
+  flex-direction: column;
+  line-height: 1.4;
+}
+
+.sub-text {
+  font-size: 12px;
+  color: #999;
+}
+
+.text-gray {
+  color: #ccc;
+  font-style: italic;
+}
+
+.time-text {
+  font-size: 13px;
+  color: #606266;
+}
+
+.status-cell {
+  display: flex;
+  align-items: center;
+}
+
+.status-dot {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  margin-right: 6px;
+  background-color: #909399;
+}
+
+.status-dot.pending_dispatch {
+  background-color: #f56c6c;
+  box-shadow: 0 0 4px rgba(245, 108, 108, 0.4);
+}
+
+.status-dot.pending_accept {
+  background-color: #e6a23c;
+}
+
+.status-dot.serving {
+  background-color: #409eff;
+}
+
+.status-dot.pending_confirm {
+  background-color: #bf24e8;
+}
+
+.status-dot.completed {
+  background-color: #67c23a;
+}
+
+.status-dot.cancelled {
+  background-color: #909399;
+}
+
+.fulfiller-info {
+  display: flex;
+  flex-direction: column;
+}
+
+.fulfiller-name {
+  font-weight: 500;
+  color: #333;
+}
+
+.fulfiller-fee {
+  font-size: 12px;
+  color: #e6a23c;
+}
+
+.op-cell {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.el-dropdown-link {
+  cursor: pointer;
+  color: #409eff;
+  font-size: 12px;
+  display: flex;
+  align-items: center;
+  line-height: 1;
+  height: 24px;
+}
+</style>

+ 230 - 163
src/views/order/purchase/components/AddPetDialog.vue

@@ -1,165 +1,143 @@
 <template>
-  <el-dialog :model-value="visible" @update:model-value="$emit('update:visible', $event)" title="宠物档案详情" width="800px" top="10vh" class="pet-profile-dialog">
-    <el-tabs v-model="activePetTab" class="pet-tabs">
+  <el-dialog :model-value="visible" @update:model-value="$emit('update:visible', $event)" title="新增宠物" width="800px" destroy-on-close append-to-body>
+    <el-tabs v-model="activeTab">
       <el-tab-pane label="基本信息" name="basic">
-        <div class="pet-form-content">
-          <!-- Avatar Upload -->
-          <div class="avatar-col">
-            <el-upload
-              class="avatar-uploader"
-              action="#"
-              :show-file-list="false"
-              :auto-upload="false"
-              :on-change="handleAvatarChange"
-            >
-              <img v-if="petForm.avatar" :src="petForm.avatar" class="avatar" />
-              <el-icon v-else class="avatar-uploader-icon" :size="28" color="#8c939d"><Plus /></el-icon>
-            </el-upload>
-            <div style="font-size:12px; color:#999; margin-top:8px; text-align:center">点击上传头像</div>
-          </div>
-
-          <!-- Form Fields -->
-          <el-form :model="petForm" label-width="80px" class="inner-form">
-            <el-row :gutter="20">
-              <el-col :span="12">
-                <el-form-item label="宠物姓名" required>
-                  <el-input v-model="petForm.name" placeholder="请输入" />
-                </el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="所属主人" required>
-                  <el-select :model-value="userId" disabled placeholder="选择主人" style="width:100%">
-                    <el-option v-for="u in userOptions" :key="u.id" :label="u.name" :value="u.id" />
-                  </el-select>
-                </el-form-item>
-              </el-col>
-            </el-row>
-
-            <el-row :gutter="20">
-              <el-col :span="12">
-                <el-form-item label="性别">
-                  <el-radio-group v-model="petForm.gender">
-                    <el-radio label="MM">公</el-radio>
-                    <el-radio label="GG">母</el-radio>
-                  </el-radio-group>
-                </el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="品种">
-                  <el-select v-model="petForm.breed" placeholder="请选择品种" style="width:100%">
-                    <el-option label="金毛" value="金毛" />
-                    <el-option label="布偶" value="布偶" />
-                    <el-option label="边牧" value="边牧" />
-                  </el-select>
-                </el-form-item>
-              </el-col>
-            </el-row>
-
-            <el-row :gutter="20">
-              <el-col :span="12">
-                <el-form-item label="体型">
-                  <el-select v-model="petForm.bodyType" placeholder="选择体型" style="width:100%">
-                    <el-option label="小型" value="small" />
-                    <el-option label="中型" value="medium" />
-                    <el-option label="大型" value="large" />
-                  </el-select>
-                </el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="体重(kg)">
-                  <el-row :gutter="10">
-                    <el-col :span="12"><el-input-number v-model="petForm.weight" :min="0" :step="0.1" :controls="false" style="width:100%" /></el-col>
-                    <el-col :span="12"></el-col>
-                  </el-row>
-                </el-form-item>
-              </el-col>
-            </el-row>
-
-            <el-row :gutter="20">
-              <el-col :span="12">
-                <el-form-item label="年龄(岁)">
-                  <el-input-number v-model="petForm.age" :min="0" style="width:100%" />
-                </el-form-item>
-              </el-col>
-            </el-row>
-
-            <el-form-item label="性格关键词">
-              <el-input v-model="petForm.keywords" placeholder="如:活泼、粘人" />
-            </el-form-item>
-
-            <el-form-item label="萌宠性格">
-              <el-input v-model="petForm.desc" type="textarea" placeholder="详细描述" :rows="2" />
-            </el-form-item>
-
-            <el-form-item label="宠物标签">
-              <el-select v-model="petForm.tags" multiple placeholder="选择标签" style="width:100%">
-                <el-option label="绝育" value="1" />
-                <el-option label="疫苗齐全" value="2" />
-              </el-select>
-            </el-form-item>
-
-          </el-form>
-        </div>
+        <el-form :model="form" label-width="100px">
+          <el-row>
+            <el-col :span="24" style="display: flex; justify-content: center; margin-bottom: 20px">
+              <el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUploadFile">
+                <el-avatar v-if="avatarDisplayUrl" :src="avatarDisplayUrl" :size="80" />
+                <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
+              </el-upload>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="宠物姓名" required><el-input v-model="form.name" /></el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="所属主人" required>
+                <el-select v-model="form.userId" placeholder="选择主人" style="width: 100%" filterable disabled>
+                  <el-option v-for="user in userOptions" :key="user.id" :label="user.name" :value="user.id" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="性别">
+                <el-select v-model="form.gender" placeholder="请选择">
+                  <el-option v-for="dict in sys_pet_gender" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="品种">
+                <el-select v-model="form.breed" placeholder="请选择品种" style="width: 100%" filterable allow-create default-first-option>
+                  <el-option v-for="dict in sys_pet_breed" :key="dict.value" :label="dict.label" :value="dict.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="体型">
+                <el-select v-model="form.size" style="width: 100%">
+                  <el-option v-for="dict in sys_pet_size" :key="dict.value" :label="dict.label" :value="dict.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="体重(kg)"><el-input-number v-model="form.weight" :min="0" :precision="1" style="width: 100%" /></el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="年龄(岁)"><el-input-number v-model="form.age" :min="0" style="width: 100%" /></el-form-item>
+            </el-col>
+            <el-col :span="24">
+              <el-form-item label="性格关键词"><el-input v-model="form.personality" placeholder="如:活泼、粘人" /></el-form-item>
+            </el-col>
+            <el-col :span="24">
+              <el-form-item label="萌宠性格"><el-input v-model="form.cutePersonality" type="textarea" placeholder="详细描述" /></el-form-item>
+            </el-col>
+            <el-col :span="24">
+              <el-form-item label="宠物标签">
+                <el-select v-model="form.tagIds" multiple placeholder="选择标签" style="width: 100%">
+                  <el-option v-for="tag in allPetTags" :key="tag.id" :label="tag.name" :value="tag.id">
+                    <el-tag :type="tag.colorType || 'info'" effect="light" size="small">{{ tag.name }}</el-tag>
+                  </el-option>
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
       </el-tab-pane>
       <el-tab-pane label="家庭信息" name="family">
-        <el-form :model="petForm" label-width="120px">
+        <el-form :model="form" label-width="120px">
           <el-form-item label="新来家庭时间">
-            <el-date-picker v-model="petForm.arrivalTime" type="date" placeholder="选择日期" style="width: 100%" />
+            <el-date-picker v-model="form.arrivalTime" type="date" placeholder="选择日期" style="width: 100%" />
           </el-form-item>
           <el-form-item label="家庭房屋类型">
-            <el-radio-group v-model="petForm.houseType">
-              <el-radio label="stairs">楼梯</el-radio>
-              <el-radio label="elevator">电梯</el-radio>
+            <el-radio-group v-model="form.houseType">
+              <el-radio v-for="dict in sys_house_type" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
             </el-radio-group>
           </el-form-item>
           <el-form-item label="入门方式">
-            <el-radio-group v-model="petForm.entryMethod">
-              <el-radio label="password">密码开门</el-radio>
-              <el-radio label="key">钥匙开门</el-radio>
+            <el-radio-group v-model="form.entryMethod">
+              <el-radio v-for="dict in sys_entry_method" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
             </el-radio-group>
           </el-form-item>
-          <el-form-item label="密码" v-if="petForm.entryMethod === 'password'">
-            <el-input v-model="petForm.entryPassword" placeholder="请输入门锁密码" />
+          <el-form-item label="密码" v-if="form.entryMethod === 'password'">
+            <el-input v-model="form.entryPassword" placeholder="请输入门锁密码" />
           </el-form-item>
-          <el-form-item label="钥匙位置" v-if="petForm.entryMethod === 'key'">
-            <el-input v-model="petForm.keyLocation" placeholder="请输入钥匙存放位置" />
+          <el-form-item label="钥匙位置" v-if="form.entryMethod === 'key'">
+            <el-input v-model="form.keyLocation" placeholder="请输入钥匙存放位置" />
           </el-form-item>
         </el-form>
       </el-tab-pane>
       <el-tab-pane label="健康状况" name="health">
-        <el-form :model="petForm" label-width="120px">
+        <el-form :model="form" label-width="120px">
           <el-form-item label="健康状态">
-            <el-radio-group v-model="petForm.healthStatus">
+            <el-radio-group v-model="form.healthStatus">
               <el-radio label="健康">健康</el-radio>
               <el-radio label="亚健康">亚健康</el-radio>
               <el-radio label="疾病">疾病</el-radio>
             </el-radio-group>
           </el-form-item>
           <el-form-item label="是否有攻击倾向">
-            <el-switch v-model="petForm.aggression" active-text="是" inactive-text="否" />
+            <el-switch v-model="form.aggression" active-text="是" inactive-text="否" :active-value="1" :inactive-value="0" />
           </el-form-item>
           <el-form-item label="疫苗情况">
-            <el-input v-model="petForm.vaccine" type="textarea" placeholder="记录疫苗接种情况" />
+            <el-radio-group v-model="form.vaccineStatus">
+              <el-radio label="无">无</el-radio>
+              <el-radio label="已打1次">已打1次</el-radio>
+              <el-radio label="已打2次">已打2次</el-radio>
+              <el-radio label="已打3次">已打3次</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="疫苗凭证">
+            <el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUploadVaccineCert">
+              <img v-if="vaccineCertDisplayUrl" :src="vaccineCertDisplayUrl" class="avatar" style="width: 100px; height: 100px; object-fit: cover" />
+              <el-icon v-else class="avatar-uploader-icon" style="width: 100px; height: 100px; line-height: 100px"><Plus /></el-icon>
+            </el-upload>
           </el-form-item>
           <el-form-item label="既往病史">
-            <el-input v-model="petForm.medicalHistory" type="textarea" placeholder="如有病史请记录" />
+            <el-input v-model="form.medicalHistory" type="textarea" placeholder="如有病史请记录" />
           </el-form-item>
           <el-form-item label="过敏史">
-            <el-input v-model="petForm.allergies" type="textarea" placeholder="如有过敏源请记录" />
+            <el-input v-model="form.allergies" type="textarea" placeholder="如有过敏源请记录" />
           </el-form-item>
         </el-form>
       </el-tab-pane>
     </el-tabs>
     <template #footer>
-      <el-button @click="$emit('update:visible', false)">取消</el-button>
-      <el-button type="primary" @click="submit">保存</el-button>
+      <span class="dialog-footer">
+        <el-button @click="$emit('update:visible', false)">取消</el-button>
+        <el-button type="primary" :loading="submitLoading" @click="saveData">保存</el-button>
+      </span>
     </template>
   </el-dialog>
 </template>
 
 <script setup>
-import { ref, reactive, watch } from 'vue'
+import { ref, reactive, watch, onMounted, getCurrentInstance, toRefs } from 'vue'
 import { ElMessage } from 'element-plus'
+import { globalHeaders } from '@/utils/request'
+import { addPetOnOrder } from '@/api/archieves/pet'
+import { listAllTag } from '@/api/archieves/tag'
 
 const props = defineProps({
   visible: { type: Boolean, default: false },
@@ -169,64 +147,153 @@ const props = defineProps({
 
 const emit = defineEmits(['update:visible', 'success'])
 
-const activePetTab = ref('basic')
+const { proxy } = getCurrentInstance()
+const { sys_pet_gender, sys_pet_type, sys_pet_size, sys_pet_breed, sys_house_type, sys_entry_method } = toRefs(
+  proxy?.useDict('sys_pet_gender', 'sys_pet_type', 'sys_pet_size', 'sys_pet_breed', 'sys_house_type', 'sys_entry_method')
+)
 
-const petForm = reactive({
-  name: '', breed: '', gender: 'MM', avatar: '',
-  bodyType: 'small', weight: 0, age: 0, keywords: '', desc: '', tags: [],
+const activeTab = ref('basic')
+const submitLoading = ref(false)
+const allPetTags = ref([])
+const avatarDisplayUrl = ref('')
+const vaccineCertDisplayUrl = ref('')
 
-  // Family
-  arrivalTime: '', houseType: 'stairs', entryMethod: 'key', entryPassword: '', keyLocation: '',
-  // Health
-  healthStatus: '健康', aggression: false, vaccine: '', medicalHistory: '', allergies: ''
+const baseUrl = import.meta.env.VITE_APP_BASE_API
+const uploadUrl = baseUrl + '/resource/oss/upload'
+
+const form = reactive({
+  userId: undefined,
+  avatar: undefined,
+  name: '',
+  type: 0,
+  gender: undefined,
+  breed: '',
+  birthday: '',
+  age: 1,
+  weight: 5,
+  size: 'small',
+  isSterilized: 0,
+  arrivalTime: '',
+  houseType: '',
+  entryMethod: '',
+  entryPassword: '',
+  keyLocation: '',
+  personality: '',
+  cutePersonality: '',
+  healthStatus: '健康',
+  aggression: 0,
+  vaccineStatus: '无',
+  vaccineCert: undefined,
+  medicalHistory: '',
+  allergies: '',
+  remark: '',
+  tagIds: []
 })
 
-watch(() => props.visible, (newVal) => {
-  if (newVal) {
-    activePetTab.value = 'basic'
-    Object.assign(petForm, {
-      name: '', breed: '', gender: 'MM', avatar: '',
-      bodyType: 'small', weight: 0, age: 0, keywords: '', desc: '', tags: [],
-      arrivalTime: '', houseType: 'stairs', entryMethod: 'key', entryPassword: '', keyLocation: '',
-      healthStatus: '健康', aggression: false, vaccine: '', medicalHistory: '', allergies: ''
+watch(() => props.visible, (val) => {
+  if (val) {
+    activeTab.value = 'basic'
+    submitLoading.value = false
+    avatarDisplayUrl.value = ''
+    vaccineCertDisplayUrl.value = ''
+    Object.assign(form, {
+      userId: props.userId, avatar: undefined, name: '', type: 0, gender: undefined,
+      breed: '', birthday: '', age: 1, weight: 5, size: 'small', isSterilized: 0,
+      arrivalTime: '', houseType: '', entryMethod: '', entryPassword: '', keyLocation: '',
+      personality: '', cutePersonality: '', healthStatus: '健康', aggression: 0,
+      vaccineStatus: '无', vaccineCert: undefined, medicalHistory: '', allergies: '', remark: '', tagIds: []
     })
   }
 })
 
-const handleAvatarChange = (uploadFile) => {
-  // Mock upload: create local URL
-  petForm.avatar = URL.createObjectURL(uploadFile.raw)
+const loadTags = () => {
+  listAllTag({ category: 'pet', status: 0 }).then((res) => {
+    allPetTags.value = res.data || []
+  })
 }
 
-const submit = () => {
-  if (!petForm.name || !petForm.breed) {
-    ElMessage.warning('请补全宠物必填信息')
-    return
+const handleUploadFile = async (file) => {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  try {
+    const headers = globalHeaders()
+    const res = await fetch(uploadUrl, {
+      method: 'POST',
+      headers: {
+        'Authorization': headers.Authorization,
+        'clientid': headers.clientid
+      },
+      body: formData
+    })
+    const result = await res.json()
+    if (result.code === 200) {
+      form.avatar = result.data.ossId
+      avatarDisplayUrl.value = result.data.url
+    } else {
+      ElMessage.error(result.msg || '头像上传失败')
+    }
+  } catch (e) {
+    ElMessage.error('头像上传失败')
   }
-  const newPet = {
-    id: Date.now(),
-    name: petForm.name,
-    breed: petForm.breed,
-    avatar: petForm.avatar
+}
+
+const handleUploadVaccineCert = async (file) => {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  try {
+    const headers = globalHeaders()
+    const res = await fetch(uploadUrl, {
+      method: 'POST',
+      headers: {
+        'Authorization': headers.Authorization,
+        'clientid': headers.clientid
+      },
+      body: formData
+    })
+    const result = await res.json()
+    if (result.code === 200) {
+      form.vaccineCert = result.data.ossId
+      vaccineCertDisplayUrl.value = result.data.url
+    } else {
+      ElMessage.error(result.msg || '疫苗凭证上传失败')
+    }
+  } catch (e) {
+    ElMessage.error('疫苗凭证上传失败')
   }
-  emit('success', newPet)
-  emit('update:visible', false)
 }
+
+const saveData = () => {
+  if (!form.name) return ElMessage.warning('请输入宠物姓名')
+  if (!form.userId) return ElMessage.warning('请先选择或新增所属主人')
+
+  submitLoading.value = true
+  addPetOnOrder(form).then(res => {
+    emit('success', res.data)
+    emit('update:visible', false)
+  }).finally(() => {
+    submitLoading.value = false
+  })
+}
+
+onMounted(() => {
+  loadTags()
+})
 </script>
 
 <style scoped>
-.pet-form-content { display: flex; gap: 20px; }
-.avatar-col { width: 120px; display: flex; flex-direction: column; align-items: center; padding-top: 10px; }
-.avatar-uploader { display: inline-block; }
-.avatar-uploader:deep(.el-upload) {
-  border: 1px dashed #d9d9d9; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden;
-  transition: var(--el-transition-duration-fast);
-}
-.avatar-uploader:deep(.el-upload:hover) { border-color: var(--el-color-primary); }
 .avatar-uploader-icon {
-  font-size: 28px; color: #8c939d; width: 100px; height: 100px; text-align: center; border: 1px dashed #d9d9d9;
-  border-radius: 50%; display: flex; align-items: center; justify-content: center;
+  font-size: 28px;
+  color: #8c939d;
+  width: 80px;
+  height: 80px;
+  text-align: center;
+  border: 1px dashed #dcdfe6;
+  border-radius: 50%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+.avatar-uploader-icon:hover {
+  border-color: var(--el-color-primary);
 }
-.avatar { width: 100px; height: 100px; display: block; border-radius: 50%; object-fit: cover; }
-.inner-form { flex: 1; }
 </style>

+ 228 - 79
src/views/order/purchase/components/AddUserDialog.vue

@@ -1,164 +1,313 @@
 <template>
-  <el-dialog :model-value="visible" @update:model-value="$emit('update:visible', $event)" title="新增用户" width="700px" destroy-on-close append-to-body class="add-user-dialog">
-    <el-form :model="userForm" label-width="90px" class="user-form">
-      <div style="display: flex; justify-content: center; align-items: center; gap: 20px; margin-bottom: 30px;">
-        <el-upload action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUserAvatarChange">
-          <el-avatar :size="80" :src="userForm.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" style="cursor: pointer; border: 2px solid #e4e7ed;" />
-        </el-upload>
-        <el-button type="primary" link @click="">点击修改头像</el-button>
-      </div>
+  <el-dialog :model-value="visible" @update:model-value="$emit('update:visible', $event)" title="新增用户" width="700px" destroy-on-close append-to-body>
+    <el-form :model="form" label-width="90px" class="user-form">
+      <el-row :gutter="20">
+        <el-col :span="24" style="text-align: center; margin-bottom: 25px;">
+          <el-upload action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUserUploadFile">
+            <el-avatar :size="80" :src="userAvatarDisplayUrl || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" class="upload-avatar" />
+            <div style="margin-top: 8px; font-size: 12px; color: #409EFF;">点击修改头像</div>
+          </el-upload>
+        </el-col>
 
-      <div class="form-section-header">基本资料</div>
-      <el-row :gutter="30">
+        <el-col :span="24"><div class="form-section-header">基本资料</div></el-col>
         <el-col :span="12">
           <el-form-item label="录入来源">
-            <el-select v-model="userForm.source" style="width: 100%" filterable allow-create default-first-option>
-              <el-option label="平台录入" value="平台录入" />
-              <el-option label="萌它宠物连锁录入" value="萌它宠物连锁录入" />
-            </el-select>
+            <PageSelect v-model="form.source"
+              :options="brandList.map(item => ({ value: item.name, label: item.name }))"
+              :total="brandTotal" :pageSize="10" placeholder="请选择所属品牌"
+              @page-change="handleBrandPageChange"
+              @visible-change="handleBrandVisibleChange" />
           </el-form-item>
         </el-col>
         <el-col :span="12">
           <el-form-item label="所属区域">
-            <el-select v-model="userForm.area" style="width: 100%" filterable allow-create default-first-option placeholder="请选择或输入">
-              <el-option label="朝阳区" value="朝阳区" />
-              <el-option label="海淀区" value="海淀区" />
+            <el-cascader v-model="formAreaValue" :options="areaTreeOptions" placeholder="请选择区域"
+              style="width: 100%" clearable @change="handleFormAreaChange" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="所属站点">
+            <el-select v-model="form.stationId" style="width: 100%" filterable placeholder="请选择站点" clearable :disabled="!form.areaId">
+              <el-option v-for="station in formStationList" :key="station.id" :label="station.name" :value="station.id" />
             </el-select>
           </el-form-item>
         </el-col>
         <el-col :span="12">
-          <el-form-item label="姓名" required><el-input v-model="userForm.name" placeholder="请输入姓名" /></el-form-item>
+          <el-form-item label="姓名" required><el-input v-model="form.name" placeholder="请输入姓名" /></el-form-item>
         </el-col>
         <el-col :span="12">
-          <el-form-item label="电话" required><el-input v-model="userForm.phone" placeholder="请输入电话" /></el-form-item>
+          <el-form-item label="电话" required><el-input v-model="form.phone" placeholder="请输入电话" /></el-form-item>
         </el-col>
         <el-col :span="12">
           <el-form-item label="性别">
-            <el-radio-group v-model="userForm.gender">
-              <el-radio label="男">男</el-radio>
-              <el-radio label="女">女</el-radio>
-            </el-radio-group>
+            <el-select v-model="form.gender" placeholder="请选择">
+              <el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+            </el-select>
           </el-form-item>
         </el-col>
-      </el-row>
 
-      <div class="form-section-header">居住信息</div>
-      <el-row :gutter="30">
+        <el-col :span="24"><div class="form-section-header">居住信息</div></el-col>
         <el-col :span="24">
           <el-form-item label="所在地区">
-            <el-cascader v-model="userForm.region" :options="pcaOptions" placeholder="请选择省/市/区" style="width: 100%" />
+            <el-cascader
+              v-model="regionCascaderValue"
+              :options="regionData"
+              placeholder="请选择省/市/区"
+              style="width: 100%"
+              clearable
+            />
           </el-form-item>
         </el-col>
         <el-col :span="24">
-          <el-form-item label="详细住址"><el-input v-model="userForm.detailAddress" placeholder="请输入街道/门牌号" /></el-form-item>
+          <el-form-item label="详细住址"><el-input v-model="form.address" placeholder="请输入街道/门牌号" /></el-form-item>
         </el-col>
         <el-col :span="12">
           <el-form-item label="房屋类型">
-            <el-radio-group v-model="userForm.houseType">
-              <el-radio label="stairs">楼梯</el-radio>
-              <el-radio label="elevator">电梯</el-radio>
+            <el-radio-group v-model="form.houseType">
+              <el-radio v-for="dict in sys_house_type" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
             </el-radio-group>
           </el-form-item>
         </el-col>
         <el-col :span="12">
           <el-form-item label="入门方式">
-            <el-radio-group v-model="userForm.entryMethod">
-              <el-radio label="password">密码开门</el-radio>
-              <el-radio label="key">钥匙开门</el-radio>
+            <el-radio-group v-model="form.entryMethod">
+              <el-radio v-for="dict in sys_entry_method" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
             </el-radio-group>
           </el-form-item>
         </el-col>
-        <el-col :span="12" v-if="userForm.entryMethod === 'password'">
+        <el-col :span="12" v-if="form.entryMethod === 'password'">
           <el-form-item label="开门密码">
-            <el-input v-model="userForm.entryPassword" placeholder="请输入密码" />
+            <el-input v-model="form.entryPassword" placeholder="请输入密码" />
           </el-form-item>
         </el-col>
-        <el-col :span="12" v-if="userForm.entryMethod === 'key'">
+        <el-col :span="12" v-if="form.entryMethod === 'key'">
           <el-form-item label="钥匙位置">
-            <el-input v-model="userForm.keyLocation" placeholder="如:地毯下" />
+            <el-input v-model="form.keyLocation" placeholder="如:地毯下" />
           </el-form-item>
         </el-col>
-      </el-row>
 
-      <div class="form-section-header">其他</div>
-      <el-row :gutter="30">
+        <el-col :span="24"><div class="form-section-header">其他</div></el-col>
         <el-col :span="24">
           <el-form-item label="用户标签">
-            <el-select v-model="userSelectedTagIds" multiple placeholder="选择标签" style="width: 100%">
+            <el-select v-model="selectedTagIds" multiple placeholder="选择标签" style="width: 100%">
               <el-option v-for="tag in allUserTags" :key="tag.id" :label="tag.name" :value="tag.id">
-                <el-tag :type="tag.type" effect="light" size="small">{{ tag.name }}</el-tag>
+                <el-tag :type="tag.colorType || 'info'" effect="light" size="small">{{ tag.name }}</el-tag>
               </el-option>
             </el-select>
           </el-form-item>
         </el-col>
         <el-col :span="24">
-          <el-form-item label="备注说明"><el-input type="textarea" v-model="userForm.remark" rows="3" /></el-form-item>
+          <el-form-item label="备注说明"><el-input type="textarea" v-model="form.remark" rows="3" /></el-form-item>
         </el-col>
       </el-row>
     </el-form>
     <template #footer>
       <div style="text-align: center; margin-top: 20px;">
         <el-button @click="$emit('update:visible', false)" size="large" style="width: 120px;">取消</el-button>
-        <el-button type="primary" @click="submit" size="large" style="width: 120px;">保存</el-button>
+        <el-button type="primary" :loading="submitLoading" @click="saveUser" size="large" style="width: 120px;">保存</el-button>
       </div>
     </template>
   </el-dialog>
 </template>
 
 <script setup>
-import { reactive, ref, watch } from 'vue'
+import { ref, reactive, computed, onMounted, watch, getCurrentInstance, toRefs } from 'vue'
 import { ElMessage } from 'element-plus'
+import { globalHeaders } from '@/utils/request'
+import { addCustomerOnOrder } from '@/api/archieves/customer'
+import { listAllTag } from '@/api/archieves/tag'
+import { listOnStore } from '@/api/system/areaStation'
+import { listOnStore as listBrandOnStore } from '@/api/system/tenant'
+import { regionData } from 'element-china-area-data'
+import PageSelect from '@/components/PageSelect/index.vue'
 
 const props = defineProps({
-  visible: { type: Boolean, default: false },
-  pcaOptions: { type: Array, default: () => [] }
+  visible: { type: Boolean, default: false }
 })
 
 const emit = defineEmits(['update:visible', 'success'])
 
-const userSelectedTagIds = ref([])
-const allUserTags = [
-  { id: 1, name: '优质客户', type: 'success' },
-  { id: 2, name: '潜在流失', type: 'warning' },
-  { id: 3, name: '黑名单', type: 'danger' }
-]
-
-const userForm = reactive({
-  id: null, avatar: '', name: '', phone: '', gender: '男', address: '', detailAddress: '', region: [], remark: '',
-  houseType: 'elevator', entryMethod: 'password', entryPassword: '', keyLocation: '',
-  source: '平台录入', area: ''
+const { proxy } = getCurrentInstance()
+const { sys_user_sex, sys_house_type, sys_entry_method } = toRefs(
+  proxy?.useDict('sys_user_sex', 'sys_house_type', 'sys_entry_method')
+)
+
+const submitLoading = ref(false)
+
+const allNodes = ref([])
+const brandList = ref([])
+const brandTotal = ref(0)
+const allUserTags = ref([])
+
+const formAreaValue = ref([])
+const regionCascaderValue = ref([])
+const selectedTagIds = ref([])
+const userAvatarDisplayUrl = ref('')
+
+const baseUrl = import.meta.env.VITE_APP_BASE_API
+const uploadUrl = baseUrl + '/resource/oss/upload'
+
+const form = reactive({
+  name: '',
+  phone: '',
+  avatar: undefined,
+  gender: undefined,
+  birthday: '',
+  idCard: '',
+  areaId: undefined,
+  stationId: undefined,
+  regionCode: '',
+  region: [],
+  address: '',
+  houseType: '',
+  entryMethod: '',
+  entryPassword: '',
+  keyLocation: '',
+  source: '',
+  emergencyContact: '',
+  emergencyPhone: '',
+  memberLevel: 0,
+  status: 0,
+  remark: '',
+  tagIds: []
 })
 
-watch(() => props.visible, (newVal) => {
-  if (newVal) {
-    userSelectedTagIds.value = []
-    Object.assign(userForm, {
-      id: null, avatar: '', name: '', phone: '', gender: '男', address: '', detailAddress: '', region: [], remark: '',
-      houseType: 'elevator', entryMethod: 'password', entryPassword: '', keyLocation: '',
-      source: '平台录入', area: ''
-    })
+watch(() => props.visible, (val) => {
+  if (val) {
+    resetForm()
   }
 })
 
-const handleUserAvatarChange = (uploadFile) => {
-  userForm.avatar = URL.createObjectURL(uploadFile.raw)
+const resetForm = () => {
+  submitLoading.value = false
+  selectedTagIds.value = []
+  userAvatarDisplayUrl.value = ''
+  formAreaValue.value = []
+  regionCascaderValue.value = []
+  Object.assign(form, {
+    name: '', phone: '', avatar: undefined, gender: undefined, birthday: '', idCard: '',
+    areaId: undefined, stationId: undefined, regionCode: '', region: [], address: '',
+    houseType: '', entryMethod: '', entryPassword: '', keyLocation: '', source: '',
+    emergencyContact: '', emergencyPhone: '', memberLevel: 0, status: 0, remark: '', tagIds: []
+  })
 }
 
-const submit = () => {
-  if (!userForm.name || !userForm.phone) {
-    ElMessage.warning('请补全用户必填信息')
-    return
+const areaTreeOptions = computed(() => {
+  const buildTree = (data, parentId) => {
+    return data
+      .filter(item => String(item.parentId) === String(parentId))
+      .map(item => {
+        const children = buildTree(data, item.id)
+        const node = { value: item.id, label: item.name }
+        if (children.length > 0) node.children = children
+        return node
+      })
   }
-  const newUser = {
-    id: Date.now(),
-    name: userForm.name,
-    phone: userForm.phone
+  const areaData = allNodes.value.filter(n => n.type === 0 || n.type === 1)
+  return buildTree(areaData, 0)
+})
+
+const formStationList = computed(() => {
+  const areaId = form.areaId
+  const stations = allNodes.value.filter(n => n.type === 2)
+  if (areaId) {
+    return stations.filter(s => s.parentId === areaId)
+  }
+  return stations
+})
+
+const handleFormAreaChange = (value) => {
+  form.stationId = undefined
+  if (value && value.length > 0) {
+    form.areaId = value[value.length - 1]
+  } else {
+    form.areaId = undefined
   }
-  emit('success', newUser)
-  emit('update:visible', false)
 }
+
+const getBrandList = async (pageNum = 1) => {
+  const res = await listBrandOnStore({ pageNum, pageSize: 10 })
+  if (res.code === 200) {
+    brandList.value = res.rows || []
+    brandTotal.value = res.total || 0
+  }
+}
+
+const handleBrandPageChange = (page) => {
+  getBrandList(Number(page))
+}
+
+const handleBrandVisibleChange = (visible) => {
+  if (visible) {
+    getBrandList(1)
+  }
+}
+
+const loadTags = () => {
+  listAllTag({ category: 'user', status: 0 }).then((res) => {
+    allUserTags.value = res.data || []
+  })
+}
+
+const loadAreaStation = () => {
+  listOnStore().then((res) => {
+    allNodes.value = res.data || []
+  })
+}
+
+const handleUserUploadFile = async (file) => {
+  const formData = new FormData()
+  formData.append('file', file.raw)
+  try {
+    const headers = globalHeaders()
+    const res = await fetch(uploadUrl, {
+      method: 'POST',
+      headers: {
+        'Authorization': headers.Authorization,
+        'clientid': headers.clientid
+      },
+      body: formData
+    })
+    const result = await res.json()
+    if (result.code === 200) {
+      form.avatar = result.data.ossId
+      userAvatarDisplayUrl.value = result.data.url
+    } else {
+      ElMessage.error(result.msg || '头像上传失败')
+    }
+  } catch (e) {
+    ElMessage.error('头像上传失败')
+  }
+}
+
+const saveUser = () => {
+  if (!form.name) return ElMessage.warning('请输入姓名')
+  if (!form.phone) return ElMessage.warning('请输入电话')
+
+  submitLoading.value = true
+  form.tagIds = selectedTagIds.value
+  if (regionCascaderValue.value && regionCascaderValue.value.length > 0) {
+    form.regionCode = regionCascaderValue.value.join('/')
+  } else {
+    form.regionCode = ''
+  }
+  
+  addCustomerOnOrder(form).then(res => {
+    emit('success', res.data)
+    emit('update:visible', false)
+  }).finally(() => {
+    submitLoading.value = false
+  })
+}
+
+onMounted(() => {
+  loadAreaStation()
+  loadTags()
+})
 </script>
 
 <style scoped>
 .form-section-header { font-weight: bold; margin-bottom: 20px; font-size: 15px; color: #303133; }
+.upload-avatar { cursor: pointer; border: 2px solid #e4e7ed; transition: border-color 0.2s; border-radius: 50%; }
+.upload-avatar:hover { border-color: #409EFF; }
 </style>

+ 67 - 67
src/views/order/purchase/components/TransportForm.vue

@@ -10,77 +10,76 @@
 
     <div class="route-box">
       <!-- 接宠段 -->
-      <div class="route-segment" v-if="['round', 'pick'].includes(transportData.subType)">
-        <div class="seg-badge start">接</div>
-        <div class="seg-content">
-          <el-row :gutter="10" align="middle" class="address-row" style="margin-bottom: 10px;">
-            <el-col :span="2"><div class="addr-label">起点</div></el-col>
-            <el-col :span="6">
-              <el-cascader v-model="transportData.pickStartRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
-            </el-col>
-            <el-col :span="16">
-              <el-input v-model="transportData.pickStartDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
-            </el-col>
-          </el-row>
-          <el-row :gutter="10" align="middle" class="address-row">
-            <el-col :span="2"><div class="addr-label">终点</div></el-col>
-            <el-col :span="6">
-              <el-cascader v-model="transportData.pickEndRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
-            </el-col>
-            <el-col :span="16">
-              <el-input v-model="transportData.pickEndDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
-            </el-col>
-          </el-row>
-          <el-row :gutter="10">
-            <el-col :span="12"><el-input v-model="transportData.pickContact" placeholder="联系人" /></el-col>
-            <el-col :span="12"><el-input v-model="transportData.pickPhone" placeholder="电话" /></el-col>
-          </el-row>
-          <el-row :gutter="10">
-            <el-col :span="24">
-              <el-date-picker v-model="transportData.pickTime" type="datetime" placeholder="选择接宠时间" style="width: 100%" />
-            </el-col>
-          </el-row>
+      <div class="segment-card" v-if="['round', 'pick'].includes(transportData.subType)">
+        <div class="route-segment">
+          <div class="seg-badge start">接</div>
+          <div class="seg-content">
+            <el-row :gutter="10" align="middle" class="address-row" style="margin-bottom: 10px;">
+              <el-col :span="2"><div class="addr-label">起点</div></el-col>
+              <el-col :span="6">
+                <el-cascader v-model="transportData.pickStartRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+              </el-col>
+              <el-col :span="16">
+                <el-input v-model="transportData.pickStartDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
+              </el-col>
+            </el-row>
+            <el-row :gutter="10" align="middle" class="address-row">
+              <el-col :span="2"><div class="addr-label">终点</div></el-col>
+              <el-col :span="6">
+                <el-cascader v-model="transportData.pickEndRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+              </el-col>
+              <el-col :span="16">
+                <el-input v-model="transportData.pickEndDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
+              </el-col>
+            </el-row>
+            <el-row :gutter="10">
+              <el-col :span="12"><el-input v-model="transportData.pickContact" placeholder="联系人" /></el-col>
+              <el-col :span="12"><el-input v-model="transportData.pickPhone" placeholder="电话" /></el-col>
+            </el-row>
+            <el-row :gutter="10">
+              <el-col :span="24">
+                <el-date-picker v-model="transportData.pickTime" type="datetime" placeholder="选择接宠时间" style="width: 100%" />
+              </el-col>
+            </el-row>
+          </div>
         </div>
       </div>
 
-      <!-- 门店中转标识 -->
-      <div class="route-connector">
-        <div class="line"></div>
-        <div class="store-node"><el-icon><Shop /></el-icon> 服务门店</div>
-        <div class="line"></div>
-      </div>
+
 
       <!-- 送回段 -->
-      <div class="route-segment" v-if="['round', 'drop'].includes(transportData.subType)">
-        <div class="seg-badge end">送</div>
-        <div class="seg-content">
-          <el-row :gutter="10" align="middle" class="address-row" style="margin-bottom: 10px;">
-            <el-col :span="2"><div class="addr-label">起点</div></el-col>
-            <el-col :span="6">
-              <el-cascader v-model="transportData.dropStartRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
-            </el-col>
-            <el-col :span="16">
-              <el-input v-model="transportData.dropStartDetail" placeholder="详细地址" prefix-icon="Location" />
-            </el-col>
-          </el-row>
-          <el-row :gutter="10" align="middle" class="address-row">
-            <el-col :span="2"><div class="addr-label">终点</div></el-col>
-            <el-col :span="6">
-              <el-cascader v-model="transportData.dropEndRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
-            </el-col>
-            <el-col :span="16">
-              <el-input v-model="transportData.dropEndDetail" placeholder="详细地址" prefix-icon="Location" />
-            </el-col>
-          </el-row>
-          <el-row :gutter="10">
-            <el-col :span="12"><el-input v-model="transportData.dropContact" placeholder="联系人" /></el-col>
-            <el-col :span="12"><el-input v-model="transportData.dropPhone" placeholder="电话" /></el-col>
-          </el-row>
-          <el-row :gutter="10">
-            <el-col :span="24">
-              <el-date-picker v-model="transportData.dropTime" type="datetime" placeholder="预计送回时间 (可选)" style="width: 100%" />
-            </el-col>
-          </el-row>
+      <div class="segment-card" v-if="['round', 'drop'].includes(transportData.subType)">
+        <div class="route-segment">
+          <div class="seg-badge end">送</div>
+          <div class="seg-content">
+            <el-row :gutter="10" align="middle" class="address-row" style="margin-bottom: 10px;">
+              <el-col :span="2"><div class="addr-label">起点</div></el-col>
+              <el-col :span="6">
+                <el-cascader v-model="transportData.dropStartRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+              </el-col>
+              <el-col :span="16">
+                <el-input v-model="transportData.dropStartDetail" placeholder="详细地址" prefix-icon="Location" />
+              </el-col>
+            </el-row>
+            <el-row :gutter="10" align="middle" class="address-row">
+              <el-col :span="2"><div class="addr-label">终点</div></el-col>
+              <el-col :span="6">
+                <el-cascader v-model="transportData.dropEndRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+              </el-col>
+              <el-col :span="16">
+                <el-input v-model="transportData.dropEndDetail" placeholder="详细地址" prefix-icon="Location" />
+              </el-col>
+            </el-row>
+            <el-row :gutter="10">
+              <el-col :span="12"><el-input v-model="transportData.dropContact" placeholder="联系人" /></el-col>
+              <el-col :span="12"><el-input v-model="transportData.dropPhone" placeholder="电话" /></el-col>
+            </el-row>
+            <el-row :gutter="10">
+              <el-col :span="24">
+                <el-date-picker v-model="transportData.dropTime" type="datetime" placeholder="预计送回时间 (可选)" style="width: 100%" />
+              </el-col>
+            </el-row>
+          </div>
         </div>
       </div>
     </div>
@@ -100,7 +99,8 @@ const emit = defineEmits(['change'])
 
 <style scoped>
 .business-form { padding-top: 5px; }
-.route-box { background: #f9f9f9; padding: 20px; border-radius: 8px; border: 1px solid #EBEEF5; }
+.route-box { display: flex; flex-direction: column; gap: 20px; }
+.segment-card { background: #f9f9f9; padding: 20px; border-radius: 8px; border: 1px solid #EBEEF5; }
 .route-segment { display: flex; gap: 15px; }
 .seg-badge {
   width: 32px; height: 32px; background: #409eff; color: white; border-radius: 8px;

+ 599 - 158
src/views/order/purchase/index.vue

@@ -21,60 +21,49 @@
                         <span>服务门店 (平台代下单)</span>
                       </div>
                     </template>
-                    <PageSelect
-                      v-model="form.merchantId"
-                      placeholder="请选择商户门店"
-                      size="large"
-                      style="width: 100%"
-                      :options="merchantOptions"
-                      :total="storeTotal"
-                      :page-size="5"
-                      @page-change="handleStorePageChange"
-                      @update:modelValue="handleStoreChange"
-                    />
+                    <PageSelect v-model="form.merchantId" placeholder="请选择商户门店" size="large" style="width: 100%"
+                      :options="merchantOptions" :total="storeTotal" :page-size="5" @page-change="handleStorePageChange"
+                      @update:modelValue="handleStoreChange" />
                   </el-form-item>
                 </el-col>
                 <el-col :span="12">
                   <el-form-item>
                     <template #label>
-                      <div style="display:flex; justify-content:space-between; align-items:center; width:100%; height: 24px;">
+                      <div
+                        style="display:flex; justify-content:space-between; align-items:center; width:100%; height: 24px;">
                         <span>宠主用户</span>
-                        <el-button type="primary" plain size="small" @click="openAddUser" icon="Plus" style="margin-left: 15px;">添加用户</el-button>
+                        <el-button type="primary" plain size="small" @click="openAddUser" icon="Plus"
+                          style="margin-left: 15px;">添加用户</el-button>
                       </div>
                     </template>
-                    <PageSelect
-                      v-model="form.userId"
-                      placeholder="搜索姓名/手机号"
-                      size="large"
-                      style="width: 100%"
-                      :options="userSelectOptions"
-                      :total="userSelectOptions.length"
-                      @update:modelValue="handleUserChange"
-                    />
+                    <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>
               </el-row>
 
               <el-form-item label="选择宠物" v-if="form.userId">
                 <div class="pet-select-row">
-                  <div
-                    v-for="p in currentPets"
-                    :key="p.id"
-                    class="pet-card"
-                    :class="{ active: form.petId === p.id }"
-                    @click="form.petId = p.id"
-                  >
-                    <el-avatar :size="48" :src="p.avatar" shape="square" style="border-radius: 6px;">{{ p.name.charAt(0) }}</el-avatar>
+                  <div v-for="p in currentPets" :key="p.id" class="pet-card" :class="{ active: form.petId === p.id }"
+                    @click="form.petId = p.id">
+                    <el-avatar :size="48" :src="p.avatar" shape="square" style="border-radius: 6px;">{{ p.name.charAt(0)
+                    }}</el-avatar>
                     <div class="pet-info">
                       <div class="name">{{ p.name }}</div>
                       <div class="sub">{{ p.breed }}</div>
                     </div>
-                    <div class="check-mark" v-if="form.petId === p.id"><el-icon><Check /></el-icon></div>
+                    <div class="check-mark" v-if="form.petId === p.id"><el-icon>
+                        <Check />
+                      </el-icon></div>
                   </div>
 
                   <!-- Add Button Card (Last Item in Grid) -->
                   <div class="pet-card add-card" @click="openAddPet">
-                    <el-icon :size="24"><Plus /></el-icon>
+                    <el-icon :size="24">
+                      <Plus />
+                    </el-icon>
                     <span style="font-size: 15px; font-weight: bold;">新增宠物</span>
                   </div>
                 </div>
@@ -85,22 +74,30 @@
 
         <!-- 2. 服务类型选择 -->
         <div class="type-selection" v-if="form.merchantId">
-          <div
-            v-for="item in availableServices"
-            :key="item.id"
-            class="type-card"
+          <div v-for="item in availableServices" :key="item.id" class="type-card"
             :class="[getServiceType(item.name), { active: form.serviceId === item.id }]"
-            @click="handleServiceChange(item)"
-          >
-            <div class="icon-box"><el-icon><component :is="getServiceIcon(item.name)" /></el-icon></div>
+            @click="handleServiceChange(item)">
+            <div class="icon-box">
+              <img
+                v-if="item.icon && (item.icon.startsWith('http') || item.icon.startsWith('//') || item.icon.startsWith('/profile'))"
+                :src="item.icon" class="service-icon-img" />
+              <el-icon v-else-if="item.icon">
+                <component :is="item.icon" />
+              </el-icon>
+              <el-icon v-else>
+                <component :is="getServiceIcon(item.name)" />
+              </el-icon>
+            </div>
             <div class="text">
               <div class="type-name">{{ item.name }}</div>
               <div class="type-desc">{{ item.remark }}</div>
             </div>
           </div>
-          <div v-if="availableServices.length === 0" style="grid-column: 1 / -1; color: #909399; text-align: center; padding: 20px;">该门店暂无可选服务</div>
+          <div v-if="availableServices.length === 0"
+            style="grid-column: 1 / -1; color: #909399; text-align: center; padding: 20px;">该门店暂无可选服务</div>
         </div>
-        <div v-else style="color: #909399; margin: 20px 0; padding: 20px; text-align: center; background: #fff; border-radius: 8px;">
+        <div v-else
+          style="color: #909399; margin: 20px 0; padding: 20px; text-align: center; background: #fff; border-radius: 8px;">
           请先在上一步中选择服务门店
         </div>
 
@@ -122,13 +119,16 @@
             <div class="divider"></div>
 
             <!-- A. 宠物接送表单 -->
-            <TransportForm v-show="form.type === 'transport'" :transport-data="form.transport" :pca-options="pcaOptions" @change="calcPrice" />
+            <TransportForm v-show="form.type === 'transport'" :transport-data="form.transport" :pca-options="pcaOptions"
+              @change="calcPrice" />
 
             <!-- B. 上门喂遛表单 -->
-            <FeedingForm v-show="form.type === 'feeding'" :feeding-data="form.feeding" :pca-options="pcaOptions" @change="calcPrice" />
+            <FeedingForm v-show="form.type === 'feeding'" :feeding-data="form.feeding" :pca-options="pcaOptions"
+              @change="calcPrice" />
 
             <!-- C. 上门洗护表单 -->
-            <WashingForm v-show="form.type === 'washing'" :washing-data="form.washing" :pca-options="pcaOptions" @change="calcPrice" />
+            <WashingForm v-show="form.type === 'washing'" :washing-data="form.washing" :pca-options="pcaOptions"
+              @change="calcPrice" />
 
           </div>
         </el-card>
@@ -167,9 +167,12 @@
 
               <!-- 接送预览 -->
               <div v-if="form.type === 'transport'" class="preview-detail">
-                <div>{{ form.transport.subType === 'round' ? '往返接送' : (form.transport.subType === 'pick' ? '单程接' : '单程送') }}</div>
+                <div>{{ form.transport.subType === 'round' ? '往返接送' : (form.transport.subType === 'pick' ? '单程接' :
+                  '单程送') }}
+                </div>
                 <div class="minor">接: {{ form.transport.pickTime ? formatTime(form.transport.pickTime) : '未选时间' }}</div>
-                <div class="minor" v-if="form.transport.subType !== 'pick'">送: {{ form.transport.dropTime ? formatTime(form.transport.dropTime) : '未选' }}</div>
+                <div class="minor" v-if="form.transport.subType !== 'pick'">送: {{ form.transport.dropTime ?
+                  formatTime(form.transport.dropTime) : '未选' }}</div>
               </div>
             </div>
 
@@ -188,7 +191,8 @@
     <!-- Dialogs -->
     <!-- Add User Dialog -->
     <AddUserDialog v-model:visible="userDialogVisible" :pca-options="pcaOptions" @success="handleUserSuccess" />
-    <AddPetDialog v-model:visible="petDialogVisible" :user-id="form.userId" :user-options="userOptions" @success="handlePetSuccess" />
+    <AddPetDialog v-model:visible="petDialogVisible" :user-id="form.userId" :user-options="userOptions"
+      @success="handlePetSuccess" />
 
   </div>
 </template>
@@ -204,22 +208,16 @@ import AddPetDialog from './components/AddPetDialog.vue'
 import PageSelect from '@/components/PageSelect/index.vue'
 import { listStoreOnOrder } from '@/api/system/store'
 import { listServiceOnOrder } from '@/api/service/list'
+import { listCustomerOnOrder } from '@/api/archieves/customer'
+import { listPetByUser } from '@/api/archieves/pet'
 import { regionData as pcaOptions } from 'element-china-area-data'
+import { createOrder } from '@/api/order/order'
 
-// --- Mock Data ---
-const userOptions = ref([
-  { id: 101, name: '张三', phone: '13812345678' },
-  { id: 102, name: '李四', phone: '13987654321' }
-])
-const mockPets = {
-  101: [
-    { id: 1, name: '旺财', breed: '金毛', avatar: '', region: ['北京市', '市辖区', '朝阳区'], address: '三里屯SOHO A座 1001' },
-    { id: 2, name: '咪咪', breed: '布偶', avatar: '', region: ['北京市', '市辖区', '朝阳区'], address: '三里屯SOHO A座 1001' }
-  ],
-  102: [
-    { id: 3, name: '奥利奥', breed: '边牧', avatar: '', region: ['上海市', '市辖区', '浦东新区'], address: '陆家嘴一号院 5-502' }
-  ]
-}
+// --- State ---
+const userOptions = ref([])
+const userTotal = ref(0)
+const userQuery = reactive({ pageNum: 1, pageSize: 5, content: '' })
+const userLoading = ref(false)
 
 const serviceList = [
   { type: 'transport', name: '宠物接送', icon: 'Van', desc: '专车接送 · 全程监护', basePrice: 35 },
@@ -235,8 +233,6 @@ const allPackages = [
   { id: 14, type: 'washing', name: '除菌药浴套餐', price: 0 },
 ]
 
-// --- State ---
-const userLoading = ref(false)
 const currentPets = ref([])
 
 const stores = ref([])
@@ -278,31 +274,39 @@ const form = reactive({
 })
 
 // Address Autofill Watcher
-watch(() => form.petId, (newId) => {
-  if (!newId) return
-  const pet = currentPets.value.find(p => p.id === newId)
-  if (!pet) return
-
+watch([() => form.merchantId, () => form.userId, () => form.petId], () => {
+  const store = stores.value.find(s => s.id === form.merchantId)
   const user = userOptions.value.find(u => u.id === form.userId)
 
-  // Fill Transport
-  form.transport.pickStartRegion = pet.region || []
-  form.transport.pickStartDetail = pet.address || ''
+  const storeRegion = store?.areaCode ? store.areaCode.split(',') : []
+  const storeAddr = store?.address || ''
+
+  const userRegion = user?.regionCode ? user.regionCode.split('/') : []
+  const userAddr = user?.address || ''
+
+  // Fill Transport Pick
+  form.transport.pickStartRegion = userRegion
+  form.transport.pickStartDetail = userAddr
+  form.transport.pickEndRegion = storeRegion
+  form.transport.pickEndDetail = storeAddr
   form.transport.pickContact = user?.name || ''
-  form.transport.pickPhone = user?.phone || ''
+  form.transport.pickPhone = user?.phoneNumber || user?.phone || ''
 
-  form.transport.dropEndRegion = pet.region || []
-  form.transport.dropEndDetail = pet.address || ''
+  // Fill Transport Drop
+  form.transport.dropStartRegion = storeRegion
+  form.transport.dropStartDetail = storeAddr
+  form.transport.dropEndRegion = userRegion
+  form.transport.dropEndDetail = userAddr
   form.transport.dropContact = user?.name || ''
-  form.transport.dropPhone = user?.phone || ''
+  form.transport.dropPhone = user?.phoneNumber || user?.phone || ''
 
-  // Fill Feeding
-  form.feeding.region = pet.region || []
-  form.feeding.addressDetail = pet.address || ''
+  // Fill Feeding (上门服务)
+  form.feeding.region = userRegion
+  form.feeding.addressDetail = userAddr
 
-  // Fill Washing
-  form.washing.region = pet.region || []
-  form.washing.addressDetail = pet.address || ''
+  // Fill Washing (上门服务)
+  form.washing.region = userRegion
+  form.washing.addressDetail = userAddr
 })
 
 // Current Active Data Helper
@@ -326,7 +330,7 @@ const handleStorePageChange = (page) => {
 const handleStoreChange = (val) => {
   const store = stores.value.find(s => s.id === val)
   if (store && store.services) {
-    if(!store.services.includes(form.serviceId)) {
+    if (!store.services.includes(form.serviceId)) {
       form.serviceId = ''
       form.type = ''
       form.mode = undefined
@@ -355,7 +359,8 @@ const getServiceIcon = (name) => {
 const handleServiceChange = (item) => {
   form.serviceId = item.id
   form.mode = item.mode
-  const sysType = getServiceType(item.name)
+  const isRouteMode = item.mode === 1 || item.mode === '1'
+  const sysType = isRouteMode ? 'transport' : getServiceType(item.name)
   form.type = sysType
   calcPrice(sysType)
 }
@@ -376,7 +381,7 @@ const calcPrice = (type) => {
 
   // Always use Base Logic for "Order Value", regardless of package
   if (type === 'transport') {
-    if(data.subType === 'round') {
+    if (data.subType === 'round') {
       data.pickPrice = base
       data.dropPrice = base
     } else if (data.subType === 'pick') {
@@ -397,11 +402,27 @@ const calcPrice = (type) => {
 const userDialogVisible = ref(false)
 const openAddUser = () => { userDialogVisible.value = true }
 const handleUserSuccess = (newUser) => {
-  userOptions.value.push(newUser)
-  form.userId = newUser.id
-  currentPets.value = []
-  form.petId = ''
-  ElMessage.success('用户添加成功并已选中')
+  // 重新获取列表
+  userQuery.pageNum = 1
+  fetchUsers()
+
+  if (newUser && newUser.id) {
+    // 后端如果直接返回了用户信息或主键,尝试将其加入列表中并选中
+    const exists = userOptions.value.find(u => u.id === newUser.id)
+    if (!exists) {
+      userOptions.value.unshift(newUser)
+    }
+    form.userId = newUser.id
+    currentPets.value = []
+    form.petId = ''
+    ElMessage.success('用户添加成功并已自动选中')
+  } else {
+    // 未返回具体信息情况,清空当前选中项让用户重选
+    form.userId = ''
+    currentPets.value = []
+    form.petId = ''
+    ElMessage.success('用户添加成功')
+  }
 }
 
 // Removed mocked pcaOptions since we now use element-china-area-data
@@ -410,10 +431,20 @@ const handleUserSuccess = (newUser) => {
 const petDialogVisible = ref(false)
 const openAddPet = () => { petDialogVisible.value = true }
 const handlePetSuccess = (newPet) => {
-  if(!currentPets.value) currentPets.value = []
-  currentPets.value.push(newPet)
-  form.petId = newPet.id
-  ElMessage.success('宠物添加成功')
+  if (form.userId) {
+    listPetByUser(form.userId).then(res => {
+      currentPets.value = res.data || res.rows || []
+      if (newPet && newPet.id) {
+        form.petId = newPet.id
+        ElMessage.success('宠物添加成功并已自动选中')
+      } else {
+        form.petId = ''
+        ElMessage.success('宠物添加成功')
+      }
+    })
+  } else {
+    ElMessage.success('宠物添加成功')
+  }
 }
 
 
@@ -427,7 +458,7 @@ const availableServices = computed(() => {
   return allServices.value.filter(srv => store.services.includes(srv.id))
 })
 const userSelectOptions = computed(() => {
-  return userOptions.value.map(u => ({ label: `${u.name} - ${u.phone}`, value: u.id }))
+  return userOptions.value.map(u => ({ label: `${u.name} - ${u.phoneNumber || u.phone}`, value: u.id }))
 })
 const selectedMerchantName = computed(() => stores.value.find(m => m.id === form.merchantId)?.name)
 const selectedUserName = computed(() => userOptions.value.find(u => u.id === form.userId)?.name)
@@ -446,40 +477,198 @@ const selectedPkgName = computed(() => {
 
 
 const canSubmit = computed(() => {
-  if(!form.merchantId || !form.userId || !form.petId || !form.serviceId) return false
+  if (!form.merchantId || !form.userId || !form.petId || !form.serviceId) return false
   return true
 })
 
 // --- Methods ---
-const searchUser = (query) => { /* Mock */ }
+const fetchUsers = () => {
+  userLoading.value = true
+  listCustomerOnOrder(userQuery).then(res => {
+    userOptions.value = res.rows || []
+    userTotal.value = res.total || 0
+  }).finally(() => {
+    userLoading.value = false
+  })
+}
+
+const handleUserPageChange = (page) => {
+  userQuery.pageNum = page
+  fetchUsers()
+}
+
+const searchUser = (query) => {
+  if (!query) {
+    userOptions.value = []
+    userTotal.value = 0
+    return
+  }
+  userQuery.content = query || ''
+  userQuery.pageNum = 1
+  fetchUsers()
+}
+
 const handleUserChange = (val) => {
-  currentPets.value = mockPets[val] || []
   form.petId = ''
+  currentPets.value = []
+  if (!val) return
+  listPetByUser(val).then(res => {
+    currentPets.value = res.data || res.rows || []
+  })
 }
 const getStepTitle = (mode, type) => {
   if (mode === 1 || mode === '1') return '填写接送路线与时间'
-  if (mode === 0 || mode === '0') return '选择套餐与服务的细则'
-  // 兼容兜底方案(当后端数据未返回 mode 时)
-  const map = { transport: '填写接送路线与时间', feeding: '选择套餐与服务的细则', washing: '选择套餐与服务的细则' }
-  return map[type] || ''
+  return '选择套餐与服务的细则'
 }
 const getTypeName = (type) => {
   const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' }
   return map[type]
 }
 const formatTime = (time) => {
-  if(!time) return ''
+  if (!time) return ''
   const d = new Date(time)
-  return `${d.getMonth()+1}-${d.getDate()} ${d.getHours()}:${d.getMinutes() < 10 ? '0'+d.getMinutes() : d.getMinutes()}`
+  return `${d.getMonth() + 1}-${d.getDate()} ${d.getHours()}:${d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes()}`
+}
+
+const resetForm = () => {
+  form.merchantId = ''
+  form.userId = ''
+  form.petId = ''
+  form.serviceId = ''
+  form.type = ''
+  form.mode = undefined
+  form.groupBuyPackage = ''
+
+  form.transport = {
+    pkgId: '',
+    price: 0,
+    pickPrice: 35,
+    dropPrice: 35,
+    subType: 'round',
+    pickStartRegion: [], pickStartDetail: '', pickEndRegion: [], pickEndDetail: '', pickContact: '', pickPhone: '', pickTime: '',
+    dropStartRegion: [], dropStartDetail: '', dropEndRegion: [], dropEndDetail: '', dropContact: '', dropPhone: '', dropTime: ''
+  }
+  form.feeding = {
+    pkgId: '', price: 68,
+    appointments: [{ startTime: '', endTime: '' }],
+    region: [], addressDetail: '',
+    count: 1, dates: [], area: '', itemLoc: '', cleanLoc: '', foodAmount: '', other: ''
+  }
+  form.washing = {
+    pkgId: '', price: 88,
+    appointments: [{ startTime: '', endTime: '' }],
+    region: [], addressDetail: '',
+    time: '', petStatus: '', cleanLoc: '', toolLoc: '', other: ''
+  }
+  currentPets.value = []
 }
 
-const handleSubmit = () => {
-  ElMessage.success('下单成功!订单号:ORD20248888')
+const handleSubmit = async () => {
+  try {
+    const storeObj = stores.value.find(s => s.id === form.merchantId)
+    if (!storeObj) {
+      ElMessage.warning('请选择门店')
+      return
+    }
+
+    let subOrders = []
+    const baseMode = form.mode || 0
+
+    // 获取默认客户联系方式
+    const userObj = userOptions.value.find(u => u.id === form.userId)
+    const defaultContact = userObj?.name || ''
+    const defaultPhone = userObj?.phoneNumber || userObj?.phone || ''
+
+    if (form.type === 'transport') {
+      const td = form.transport
+      const createTransportSubOrder = (orderType, time, startRegion, startDetail, endRegion, endDetail, contact, phone) => {
+        return {
+          mode: baseMode,
+          type: orderType,
+          contact: contact || defaultContact,
+          contactPhoneNumber: phone || defaultPhone,
+          serviceTime: time || '',
+          endServiceTime: time || '',
+          fromCode: startRegion && startRegion.length > 0 ? startRegion[startRegion.length - 1] : '',
+          fromAddress: startDetail || '',
+          toCode: endRegion && endRegion.length > 0 ? endRegion[endRegion.length - 1] : '',
+          toAddress: endDetail || ''
+        }
+      }
+
+      // 接送单:往返算两个,接/送分别算一个
+      if (td.subType === 'round' || td.subType === 'pick') {
+        subOrders.push(createTransportSubOrder(
+          td.subType === 'round' ? 0 : 2,
+          td.pickTime, td.pickStartRegion, td.pickStartDetail, td.pickEndRegion, td.pickEndDetail, td.pickContact, td.pickPhone
+        ))
+      }
+
+      if (td.subType === 'round' || td.subType === 'drop') {
+        subOrders.push(createTransportSubOrder(
+          td.subType === 'round' ? 1 : 3,
+          td.dropTime, td.dropStartRegion, td.dropStartDetail, td.dropEndRegion, td.dropEndDetail, td.dropContact, td.dropPhone
+        ))
+      }
+    } else {
+      // 上门喂遛或洗护:一个服务时间一个子订单
+      const hd = form[form.type]
+      let code = hd.region && hd.region.length > 0 ? hd.region[hd.region.length - 1] : ''
+      let address = hd.addressDetail || ''
+
+      const createHomeSubOrder = (startTime, endTime) => {
+        return {
+          mode: baseMode,
+          contact: defaultContact,
+          contactPhoneNumber: defaultPhone,
+          serviceTime: startTime || hd.time || '',
+          endServiceTime: endTime || startTime || hd.time || '',
+          fromCode: code,
+          fromAddress: address,
+          toCode: code,
+          toAddress: address
+        }
+      }
+
+      if (hd.appointments && hd.appointments.length > 0) {
+        hd.appointments.forEach(appt => {
+          subOrders.push(createHomeSubOrder(appt.startTime, appt.endTime))
+        })
+      } else if (hd.time) { // 兼容没有appointments只有time的情况
+        subOrders.push(createHomeSubOrder(hd.time, hd.time))
+      }
+    }
+
+    const payload = {
+      store: form.merchantId,
+      storeSite: storeObj.site,
+      customer: form.userId,
+      pet: form.petId,
+      groupPurchasePackageName: form.groupBuyPackage || '',
+      service: form.serviceId,
+      remark: "", // 表单目前暂无备注字段
+      tenantId: storeObj.tenantId || "",
+      subOrders: subOrders
+    }
+
+    const res = await createOrder(payload)
+    if (res && res.code === 200) {
+      ElMessage.success('下单成功')
+      resetForm()
+    } else {
+      // 如果没有抛异常,走这里
+      ElMessage.success('下单成功')
+      resetForm()
+    }
+  } catch (error) {
+    console.error('Create order error: ', error)
+  }
 }
 
 // Initialize
 onMounted(() => {
   fetchStores()
+  // fetchUsers() // 移除初始加载,改为输入后触发
   listServiceOnOrder().then(res => {
     allServices.value = res.data || []
   })
@@ -487,19 +676,57 @@ onMounted(() => {
 </script>
 
 <style scoped>
-.page-container { padding: 20px; background-color: #f0f2f5; min-height: 100vh; }
-.create-layout { display: flex; gap: 20px; align-items: flex-start; max-width: 1400px; margin: 0 auto; }
+.page-container {
+  padding: 20px;
+  background-color: #f0f2f5;
+  min-height: 100vh;
+}
+
+.create-layout {
+  display: flex;
+  gap: 20px;
+  align-items: flex-start;
+  max-width: 1400px;
+  margin: 0 auto;
+}
 
 /* Left Content */
-.form-container { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 20px; }
+.form-container {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.section-card {
+  border-radius: 8px;
+  border: none;
+}
+
+.card-title {
+  font-size: 16px;
+  font-weight: bold;
+  color: #303133;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
 
-.section-card { border-radius: 8px; border: none; }
-.card-title { font-size: 16px; font-weight: bold; color: #303133; display: flex; align-items: center; gap: 10px; }
 .step-num {
-  background: #e6f7ff; color: #1890ff; width: 28px; height: 28px; border-radius: 50%;
-  text-align: center; line-height: 28px; font-family: Impact, sans-serif;
+  background: #e6f7ff;
+  color: #1890ff;
+  width: 28px;
+  height: 28px;
+  border-radius: 50%;
+  text-align: center;
+  line-height: 28px;
+  font-family: Impact, sans-serif;
+}
+
+.base-form .el-form-item {
+  margin-bottom: 18px;
 }
-.base-form .el-form-item { margin-bottom: 18px; }
 
 /* Pet Selection */
 /* Pet Selection */
@@ -509,6 +736,7 @@ onMounted(() => {
   gap: 15px;
   width: 100%;
 }
+
 .pet-card {
   border: 1px solid #8D9095;
   border-radius: 8px;
@@ -522,16 +750,19 @@ onMounted(() => {
   background: #fff;
   min-height: 70px;
 }
+
 .pet-card:hover {
   border-color: #303133;
   transform: translateY(-2px);
-  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
 }
+
 .pet-card.active {
   border-color: #409eff;
   background-color: #fff;
   box-shadow: 0 0 0 1px #409eff inset;
 }
+
 .check-mark {
   position: absolute;
   right: 0;
@@ -546,8 +777,19 @@ onMounted(() => {
   justify-content: center;
   font-size: 12px;
 }
-.pet-info .name { font-weight: bold; font-size: 15px; color: #303133; margin-bottom: 2px; }
-.pet-info .sub { font-size: 12px; color: #606266; line-height: 1.2; }
+
+.pet-info .name {
+  font-weight: bold;
+  font-size: 15px;
+  color: #303133;
+  margin-bottom: 2px;
+}
+
+.pet-info .sub {
+  font-size: 12px;
+  color: #606266;
+  line-height: 1.2;
+}
 
 .pet-card.add-card {
   border: 1px solid #8D9095;
@@ -561,6 +803,7 @@ onMounted(() => {
   height: auto;
   min-height: 70px;
 }
+
 .pet-card.add-card:hover {
   border-color: #303133;
   color: #303133;
@@ -569,59 +812,257 @@ onMounted(() => {
 }
 
 /* Type Selection */
-.type-selection { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; }
+.type-selection {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 15px;
+}
+
 .type-card {
-  background: white; border-radius: 8px; padding: 20px; cursor: pointer; position: relative;
-  display: flex; align-items: center; gap: 15px; transition: all 0.2s;
-  border: 2px solid transparent; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+  background: white;
+  border-radius: 8px;
+  padding: 20px;
+  cursor: pointer;
+  position: relative;
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  transition: all 0.2s;
+  border: 2px solid transparent;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
 }
-.type-card:hover { transform: translateY(-2px); box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1); }
-.type-card.active { border-color: #409eff; background-color: #f0f9ff; }
+
+.type-card:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
+}
+
+.type-card.active {
+  border-color: #409eff;
+  background-color: #f0f9ff;
+}
+
 .type-card .icon-box {
-  width: 48px; height: 48px; border-radius: 12px; background: #f2f3f5;
-  display: flex; align-items: center; justify-content: center; font-size: 24px; color: #606266;
+  width: 48px;
+  height: 48px;
+  border-radius: 12px;
+  background: #f2f3f5;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 24px;
+  color: #606266;
+}
+
+.type-card.active .icon-box {
+  background: #409eff;
+  color: white;
 }
-.type-card.active .icon-box { background: #409eff; color: white; }
+
 /* Colors */
-.type-card.transport.active .icon-box { background: #409eff; }
-.type-card.transport.active { border-color: #409eff; background-color: #f0f9ff; }
-.type-card.feeding.active .icon-box { background: #e6a23c; }
-.type-card.feeding.active { border-color: #e6a23c; background-color: #fdf6ec; }
-.type-card.washing.active .icon-box { background: #67c23a; }
-.type-card.washing.active { border-color: #67c23a; background-color: #f0f9eb; }
+.type-card.transport.active .icon-box {
+  background: #409eff;
+}
+
+.type-card.transport.active {
+  border-color: #409eff;
+  background-color: #f0f9ff;
+}
 
-.type-name { font-weight: bold; font-size: 16px; color: #303133; margin-bottom: 4px; }
-.type-desc { font-size: 12px; color: #909399; margin-bottom: 4px; }
-.type-price { font-size: 14px; color: #f56c6c; font-weight: bold; }
+.type-card.feeding.active .icon-box {
+  background: #e6a23c;
+}
+
+.type-card.feeding.active {
+  border-color: #e6a23c;
+  background-color: #fdf6ec;
+}
+
+.type-card.washing.active .icon-box {
+  background: #67c23a;
+}
+
+.type-card.washing.active {
+  border-color: #67c23a;
+  background-color: #f0f9eb;
+}
+
+.type-name {
+  font-weight: bold;
+  font-size: 16px;
+  color: #303133;
+  margin-bottom: 4px;
+}
+
+.type-desc {
+  font-size: 12px;
+  color: #909399;
+  margin-bottom: 4px;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+}
+
+.type-price {
+  font-size: 14px;
+  color: #f56c6c;
+  font-weight: bold;
+}
+
+/* Custom Backend Icon Img */
+.service-icon-img {
+  width: 28px;
+  height: 28px;
+  object-fit: cover;
+  border-radius: 4px;
+}
 
 /* Package Selection Grid */
-.form-section-title { font-weight: bold; margin-bottom: 12px; font-size: 14px; }
-.package-selection-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
+.form-section-title {
+  font-weight: bold;
+  margin-bottom: 12px;
+  font-size: 14px;
+}
+
+.package-selection-grid {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 12px;
+  margin-bottom: 20px;
+}
+
 .pkg-select-card {
-  border: 1px solid #dcdfe6; border-radius: 8px; padding: 10px 15px; cursor: pointer; position: relative;
-  background: #fff; transition: all 0.2s; min-height: 56px; display: flex; flex-direction: column; justify-content: center;
+  border: 1px solid #dcdfe6;
+  border-radius: 8px;
+  padding: 10px 15px;
+  cursor: pointer;
+  position: relative;
+  background: #fff;
+  transition: all 0.2s;
+  min-height: 56px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
 }
-.pkg-select-card:hover { border-color: #409eff; }
-.pkg-select-card.active { border-color: #409eff; background-color: #ecf5ff; }
-.pkg-select-card .pkg-name { font-weight: bold; font-size: 14px; color: #303133; }
-.pkg-select-card .pkg-desc { font-size: 12px; color: #909399; margin-top: 2px; }
 
-.divider { height: 1px; background: #EBEEF5; margin: 15px 0; }
+.pkg-select-card:hover {
+  border-color: #409eff;
+}
+
+.pkg-select-card.active {
+  border-color: #409eff;
+  background-color: #ecf5ff;
+}
+
+.pkg-select-card .pkg-name {
+  font-weight: bold;
+  font-size: 14px;
+  color: #303133;
+}
+
+.pkg-select-card .pkg-desc {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 2px;
+}
+
+.divider {
+  height: 1px;
+  background: #EBEEF5;
+  margin: 15px 0;
+}
 
 /* Sidebar */
-.summary-sidebar { width: 320px; flex-shrink: 0; }
-.summary-panel { background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); position: sticky; top: 20px; }
-.summary-header { background: #304156; color: white; padding: 15px 20px; font-weight: bold; font-size: 16px; border-radius: 8px 8px 0 0; }
-.summary-content { padding: 20px; }
-.row { display: flex; justify-content: space-between; margin-bottom: 12px; font-size: 14px; }
-.row .label { color: #909399; }
-.row .value { color: #303133; font-weight: 500; }
-.preview-title { font-weight: bold; margin-bottom: 8px; color: #333; }
-.preview-detail { background: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 13px; margin-bottom: 8px; }
-.preview-detail .minor { color: #999; font-size: 12px; margin-top: 2px; }
-.placeholder { color: #C0C4CC; text-align: center; padding: 20px 0; font-size: 13px; font-style: italic; }
-
-
-.summary-footer { background: #f9f9fc; padding: 15px 20px; border-top: 1px solid #ebeef5; text-align: center; border-radius: 0 0 8px 8px; }
-.submit-btn { width: 100%; font-weight: bold; border-radius: 22px; }
+.summary-sidebar {
+  width: 320px;
+  flex-shrink: 0;
+}
+
+.summary-panel {
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+  position: sticky;
+  top: 20px;
+}
+
+.summary-header {
+  background: #304156;
+  color: white;
+  padding: 15px 20px;
+  font-weight: bold;
+  font-size: 16px;
+  border-radius: 8px 8px 0 0;
+}
+
+.summary-content {
+  padding: 20px;
+}
+
+.row {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 12px;
+  font-size: 14px;
+}
+
+.row .label {
+  color: #909399;
+}
+
+.row .value {
+  color: #303133;
+  font-weight: 500;
+}
+
+.preview-title {
+  font-weight: bold;
+  margin-bottom: 8px;
+  color: #333;
+}
+
+.preview-detail {
+  background: #f8f9fa;
+  padding: 10px;
+  border-radius: 4px;
+  font-size: 13px;
+  margin-bottom: 8px;
+}
+
+.preview-detail .minor {
+  color: #999;
+  font-size: 12px;
+  margin-top: 2px;
+}
+
+.placeholder {
+  color: #C0C4CC;
+  text-align: center;
+  padding: 20px 0;
+  font-size: 13px;
+  font-style: italic;
+}
+
+
+.summary-footer {
+  background: #f9f9fc;
+  padding: 15px 20px;
+  border-top: 1px solid #ebeef5;
+  text-align: center;
+  border-radius: 0 0 8px 8px;
+}
+
+.submit-btn {
+  width: 100%;
+  font-weight: bold;
+  border-radius: 22px;
+}
+</style>
+
+<style>
+/* 全局样式:用于在搜索框无内容时强行隐藏 PageSelect 的下拉弹窗 */
+.hide-search-popper {
+  display: none !important;
+}
 </style>

+ 5 - 1
src/views/service/list/index.vue

@@ -50,7 +50,7 @@
             {{ getModeLabel(scope.row.mode) }}
           </template>
         </el-table-column>
-        <el-table-column label="备注说明" align="center" prop="remark" width="700" />
+        <el-table-column label="备注说明" align="center" prop="remark" width="700" show-overflow-tooltip />
         <el-table-column label="创建时间" align="center" prop="createTime" width="180">
           <template #default="scope">
             <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{m}:{s}') }}</span>
@@ -100,6 +100,9 @@
         <el-form-item label="备注说明" prop="remark">
           <el-input v-model="form.remark" placeholder="请输入备注说明" />
         </el-form-item>
+        <el-form-item label="打卡备注" prop="clockInRemark">
+          <el-input v-model="form.clockInRemark" placeholder="请输入打卡备注信息(将对小程序端展示)" />
+        </el-form-item>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
@@ -147,6 +150,7 @@ const initFormData: ServiceForm = {
   mode: undefined,
   sort: undefined,
   remark: undefined,
+  clockInRemark: undefined,
   tenantId: undefined
 };
 const data = reactive<PageData<ServiceForm, ServiceQuery>>({

Некоторые файлы не были показаны из-за большого количества измененных файлов