Huanyi 1 месяц назад
Родитель
Сommit
86bd0b5d5f
100 измененных файлов с 11384 добавлено и 2554 удалено
  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. 22 3
      src/api/fulfiller/pool/index.ts
  5. 14 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. 56 0
      src/api/order/subOrder/index.ts
  9. 53 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. 18 5
      src/api/system/store/index.ts
  13. 10 0
      src/api/system/store/types.ts
  14. 604 508
      src/views/archieves/customer/index.vue
  15. 233 227
      src/views/archieves/pet/index.vue
  16. 256 0
      src/views/order/management-bak/components/CareSummaryDrawer.vue
  17. 942 0
      src/views/order/management-bak/components/OrderDetailDrawer.vue
  18. 52 0
      src/views/order/management-bak/components/RemarkDialog.vue
  19. 101 0
      src/views/order/management-bak/components/RewardDialog.vue
  20. 2572 0
      src/views/order/management-bak/index.vue
  21. 170 179
      src/views/order/management/components/CareSummaryDrawer.vue
  22. 0 0
      src/views/order/management/components/DispatchDialog.vue
  23. 0 0
      src/views/order/management/components/OrderDetailDrawer.vue
  24. 30 35
      src/views/order/management/components/RemarkDialog.vue
  25. 74 76
      src/views/order/management/components/RewardDialog.vue
  26. 236 1019
      src/views/order/management/index.vue
  27. 232 0
      src/views/order/purchase-bak/components/AddPetDialog.vue
  28. 164 0
      src/views/order/purchase-bak/components/AddUserDialog.vue
  29. 86 0
      src/views/order/purchase-bak/components/FeedingForm.vue
  30. 95 0
      src/views/order/purchase-bak/components/TransportForm.vue
  31. 90 0
      src/views/order/purchase-bak/components/WashingForm.vue
  32. 562 0
      src/views/order/purchase-bak/index.vue
  33. 230 163
      src/views/order/purchase/components/AddPetDialog.vue
  34. 228 79
      src/views/order/purchase/components/AddUserDialog.vue
  35. 1 8
      src/views/order/purchase/components/FeedingForm.vue
  36. 74 47
      src/views/order/purchase/components/TransportForm.vue
  37. 1 14
      src/views/order/purchase/components/WashingForm.vue
  38. 680 190
      src/views/order/purchase/index.vue
  39. 25 0
      template/api/archieves/changeLog/index.ts
  40. 10 0
      template/api/archieves/changeLog/types.ts
  41. 100 0
      template/api/archieves/customer/index.ts
  42. 63 0
      template/api/archieves/customer/types.ts
  43. 77 0
      template/api/archieves/pet/index.ts
  44. 71 0
      template/api/archieves/pet/types.ts
  45. 67 0
      template/api/archieves/tag/index.ts
  46. 26 0
      template/api/archieves/tag/types.ts
  47. 62 0
      template/api/demo/demo/index.ts
  48. 90 0
      template/api/demo/demo/types.ts
  49. 62 0
      template/api/demo/tree/index.ts
  50. 80 0
      template/api/demo/tree/types.ts
  51. 55 0
      template/api/fulfiller/audit/index.ts
  52. 42 0
      template/api/fulfiller/audit/types.ts
  53. 150 0
      template/api/fulfiller/pool/index.ts
  54. 179 0
      template/api/fulfiller/pool/types.ts
  55. 67 0
      template/api/fulfiller/tag/index.ts
  56. 36 0
      template/api/fulfiller/tag/types.ts
  57. 113 0
      template/api/login.ts
  58. 11 0
      template/api/menu.ts
  59. 59 0
      template/api/monitor/cache/index.ts
  60. 7 0
      template/api/monitor/cache/types.ts
  61. 36 0
      template/api/monitor/loginInfo/index.ts
  62. 20 0
      template/api/monitor/loginInfo/types.ts
  63. 36 0
      template/api/monitor/online/index.ts
  64. 15 0
      template/api/monitor/online/types.ts
  65. 28 0
      template/api/monitor/operlog/index.ts
  66. 53 0
      template/api/monitor/operlog/types.ts
  67. 15 0
      template/api/order/order/index.ts
  68. 24 0
      template/api/order/order/types.ts
  69. 40 0
      template/api/order/subOrder/index.ts
  70. 36 0
      template/api/order/subOrder/types.ts
  71. 11 0
      template/api/order/subOrderLog/index.ts
  72. 15 0
      template/api/order/subOrderLog/types.ts
  73. 76 0
      template/api/resource/smsConfig/index.ts
  74. 131 0
      template/api/resource/smsConfig/types.ts
  75. 85 0
      template/api/service/list/index.ts
  76. 92 0
      template/api/service/list/types.ts
  77. 11 0
      template/api/service/mode/index.ts
  78. 4 0
      template/api/service/mode/types.ts
  79. 85 0
      template/api/system/areaStation/index.ts
  80. 145 0
      template/api/system/areaStation/types.ts
  81. 80 0
      template/api/system/client/index.ts
  82. 135 0
      template/api/system/client/types.ts
  83. 74 0
      template/api/system/config/index.ts
  84. 23 0
      template/api/system/config/types.ts
  85. 65 0
      template/api/system/dept/index.ts
  86. 60 0
      template/api/system/dept/types.ts
  87. 53 0
      template/api/system/dict/data/index.ts
  88. 26 0
      template/api/system/dict/data/types.ts
  89. 62 0
      template/api/system/dict/type/index.ts
  90. 18 0
      template/api/system/dict/type/types.ts
  91. 78 0
      template/api/system/menu/index.ts
  92. 70 0
      template/api/system/menu/types.ts
  93. 45 0
      template/api/system/notice/index.ts
  94. 26 0
      template/api/system/notice/types.ts
  95. 28 0
      template/api/system/oss/index.ts
  96. 22 0
      template/api/system/oss/types.ts
  97. 60 0
      template/api/system/ossConfig/index.ts
  98. 38 0
      template/api/system/ossConfig/types.ts
  99. 9 0
      template/api/system/platform/index.ts
  100. 5 0
      template/api/system/platform/types.ts

+ 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, CustomerOnOrderQuery, CustomerOnOrderVO } from '@/api/archieves/customer/types';
 
 /**
  * 查询用户列表
@@ -76,3 +76,25 @@ export const changeCustomerStatus = (id: string | number, status: number) => {
     params: { id, status }
   });
 };
+
+/**
+ * 下单页新增用户
+ */
+export const addCustomerOnOrder = (data: UsrCustomerForm) => {
+  return request({
+    url: '/archieves/customer/addOnOrder',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 下单页宠主列表
+ */
+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

@@ -64,3 +64,14 @@ export const delPet = (id: string | number | Array<string | number>) => {
     method: 'delete'
   });
 };
+
+/**
+ * 下单页新增宠物
+ */
+export const addPetOnOrder = (data: UsrPetForm) => {
+  return request({
+    url: '/archieves/pet/addOnOrder',
+    method: 'post',
+    data: data
+  });
+};

+ 22 - 3
src/api/fulfiller/pool/index.ts

@@ -1,9 +1,17 @@
 import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
 import {
-  FlfFulfillerVO, FlfFulfillerForm, FlfFulfillerQuery,
-  FlfRewardForm, FlfAdjustPointsForm, FlfAdjustBalanceForm,
-  FlfPointsLogVO, FlfBalanceLogVO, FlfRewardLogVO
+  FlfFulfillerVO,
+  FlfFulfillerForm,
+  FlfFulfillerQuery,
+  FlfRewardForm,
+  FlfAdjustPointsForm,
+  FlfAdjustBalanceForm,
+  FlfPointsLogVO,
+  FlfBalanceLogVO,
+  FlfRewardLogVO,
+  FlfFulfillerOnOrderQuery,
+  FlfFulfillerOnOrderVO
 } from './types';
 
 /**
@@ -136,3 +144,14 @@ export const listRewardLog = (fulfillerId: string | number, query?: PageQuery):
     params: { fulfillerId, ...query }
   });
 };
+
+/**
+ * 下单派单时查询履约者(分页)
+ */
+export const pageFulfillerOnOrder = (query?: FlfFulfillerOnOrderQuery): AxiosPromise<{ total: number; rows: FlfFulfillerOnOrderVO[] }> => {
+  return request({
+    url: '/fulfiller/fulfiller/pageOnOrder',
+    method: 'get',
+    params: query
+  });
+};

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

@@ -163,3 +163,17 @@ export interface FlfRewardLogVO {
   operatorName: string;
   createTime: 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;
+}

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

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

@@ -0,0 +1,56 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { SubOrderVO, SubOrderQuery, SubOrderListParams, SubOrderListResult } from './types';
+
+/**
+ * 查询子订单列表
+ * @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 function listSubOrderOnMerchant(params: SubOrderListParams): AxiosPromise<SubOrderListResult> {
+    return request({
+        url: '/order/subOrder/listOnMerchant',
+        method: 'get',
+        params
+    });
+}
+
+export const remarkSubOrder = (data: { orderId: string | number; remark: string; }) => {
+    return request({
+        url: '/order/subOrder/remark',
+        method: 'put',
+        data
+    });
+};

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

@@ -0,0 +1,53 @@
+export interface SubOrderQuery {
+    status?: number | string;
+    service?: number | string;
+    content?: string;
+    pageNum: number;
+    pageSize: number;
+}
+
+export interface SubOrderListParams {
+    content?: string;
+    service?: number | string;
+    status?: number | string;
+    pageNum: number;
+    pageSize: number;
+}
+
+export interface SubOrderVO {
+    id: number;
+    code: string;
+    service: number;
+    toAddress: string;
+    mode: number;
+    type: number;
+    pet: number;
+    petName: string;
+    petBreed: 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?: string | 'resting' | 'busy' | 'disabled';
+    price: number;
+    transportType?: string;
+    splitType?: string;
+    detail?: any;
+    serviceTime?: string;
+}
+
+export interface SubOrderListResult {
+    total: number;
+    rows: SubOrderVO[];
+    code: number;
+    msg: 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;
+}

+ 18 - 5
src/api/system/store/index.ts

@@ -1,6 +1,7 @@
 import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
-import { StoreListVO, StoreInfoVO, StoreVO, StoreForm, StoreQuery, StoreStatusVO, SysStorePageBo } from '@/api/system/store/types';
+import { StoreListVO, StoreInfoVO, StoreVO, StoreForm, StoreQuery, StoreStatusVO, SysStorePageBo, StoreOrderVO } from '@/api/system/store/types';
+import { StoreOrderQuery } from '../../../../template/api/system/store/types';
 
 // 查询商户门店列表
 export function listOnMerchantStoreInfo(): AxiosPromise<StoreListVO[]> {
@@ -92,9 +93,21 @@ export const delStore = (id: string | number | Array<string | number>) => {
  * 获取状态列表
  */
 export const listStoreStatus = (): AxiosPromise<StoreStatusVO[]> => {
-    return request({
-        url: '/system/store/listStatus',
-        method: 'get'
-    });
+  return request({
+    url: '/system/store/listStatus',
+    method: 'get'
+  });
 };
 
+/**
+ * 下单时查询门店列表
+ * @param query
+ * @returns {*}
+ */
+export const listStoreOnOrder = (query?: StoreOrderQuery): AxiosPromise<{ total: number; rows: StoreOrderVO[] }> => {
+  return request({
+    url: '/system/store/listOnOrder',
+    method: 'get',
+    params: query
+  });
+};

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

@@ -276,3 +276,13 @@ export interface StoreStatusVO {
   label: string;
   style: string;
 }
+
+export interface StoreOrderQuery extends PageQuery {
+  name?: string;
+}
+
+export interface StoreOrderVO {
+  id: number;
+  name: string;
+  services: number[];
+}

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


+ 233 - 227
src/views/archieves/pet/index.vue

@@ -5,17 +5,17 @@
         <div class="card-header">
           <span class="title">宠物档案</span>
           <div class="header-actions">
-            <el-input v-model="searchKey" placeholder="搜索宠物名/主人" style="width: 200px; margin-right: 10px" clearable />
-            <el-button type="primary" icon="Plus" @click="handleAdd">新增档案</el-button>
+            <el-input v-model="searchKey" placeholder="搜索宠物名/主人" style="width: 200px; margin-right: 10px" clearable @keyup.enter="handleSearch" @clear="handleSearch" />
+            <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['archieves:pet:add']">新增档案</el-button>
           </div>
         </div>
       </template>
 
-      <el-table :data="filteredTableData" style="width: 100%">
+      <el-table :data="tableData" v-loading="loading" style="width: 100%">
         <el-table-column label="宠物信息" width="220">
           <template #default="scope">
             <div style="display: flex; align-items: center">
-              <el-avatar :size="50" :src="scope.row.avatar" style="margin-right: 10px" />
+              <el-avatar :size="50" :src="scope.row.avatarUrl" style="margin-right: 10px" />
               <div>
                 <div style="font-weight: bold">{{ scope.row.name }}</div>
                 <div style="font-size: 12px; color: #999">{{ scope.row.breed }} | {{ scope.row.age }}岁</div>
@@ -25,7 +25,7 @@
         </el-table-column>
         <el-table-column prop="gender" label="性别" width="80" align="center">
           <template #default="scope">
-            <el-tag :type="scope.row.gender === '公' ? '' : 'danger'" effect="plain" size="small">{{ scope.row.gender }}</el-tag>
+            <dict-tag :options="sys_pet_gender" :value="scope.row.gender" />
           </template>
         </el-table-column>
         <el-table-column label="所属主人" width="180">
@@ -36,9 +36,9 @@
         </el-table-column>
         <el-table-column label="标签" min-width="150">
           <template #default="scope">
-            <el-tag v-for="tag in scope.row.tags" :key="tag" :type="getTagType(tag)" effect="light" size="small" style="margin-right: 5px">{{
-              tag
-            }}</el-tag>
+            <el-tag v-for="tag in scope.row.tags" :key="tag.id" :type="tag.colorType || 'info'" effect="light" size="small" style="margin-right: 5px">{{
+                tag.name
+              }}</el-tag>
           </template>
         </el-table-column>
         <el-table-column label="健康状态" width="100" align="center">
@@ -48,27 +48,27 @@
         </el-table-column>
         <el-table-column label="疫苗接种" width="120" align="center">
           <template #default="scope">
-            {{ scope.row.vaccine || '-' }}
+            {{ scope.row.vaccineStatus || '-' }}
           </template>
         </el-table-column>
         <el-table-column label="操作" width="200" align="center">
           <template #default="scope">
-            <el-button link type="primary" @click="handleDetail(scope.row)">详情</el-button>
-            <el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
-            <el-button link type="primary" @click="handleRemark(scope.row)">备注</el-button>
-            <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
+            <el-button link type="primary" @click="handleDetail(scope.row)" v-hasPermi="['archieves:pet:query']">详情</el-button>
+            <el-button link type="primary" @click="handleEdit(scope.row)" v-hasPermi="['archieves:pet:edit']">编辑</el-button>
+            <el-button link type="primary" @click="handleRemark(scope.row)" v-hasPermi="['archieves:pet:edit']">备注</el-button>
+            <el-button link type="danger" @click="handleDelete(scope.row)" v-hasPermi="['archieves:pet:remove']">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
       <div class="pagination-container">
         <el-pagination
-          v-model:current-page="currentPage"
-          v-model:page-size="pageSize"
+          v-model:current-page="queryParams.pageNum"
+          v-model:page-size="queryParams.pageSize"
           :page-sizes="[10, 20, 50, 100]"
           layout="total, sizes, prev, pager, next, jumper"
           :total="total"
-          @size-change="handleSizeChange"
-          @current-change="handleCurrentChange"
+          @size-change="getList"
+          @current-change="getList"
         />
       </div>
     </el-card>
@@ -80,7 +80,7 @@
             <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="form.avatar" :src="form.avatar" :size="80" />
+                  <el-avatar v-if="avatarDisplayUrl" :src="avatarDisplayUrl" :size="80" />
                   <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
                 </el-upload>
               </el-col>
@@ -89,32 +89,29 @@
               </el-col>
               <el-col :span="12">
                 <el-form-item label="所属主人" required>
-                  <el-select v-model="form.ownerId" placeholder="选择主人" style="width: 100%" filterable>
+                  <el-select v-model="form.userId" placeholder="选择主人" style="width: 100%" filterable>
                     <el-option v-for="user in userList" :key="user.id" :label="user.name + ' - ' + user.phone" :value="user.id" />
                   </el-select>
                 </el-form-item>
               </el-col>
               <el-col :span="12">
                 <el-form-item label="性别">
-                  <el-radio-group v-model="form.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_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="breed in petBreeds" :key="breed" :label="breed" :value="breed" />
+                    <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 label="小型" value="small" />
-                    <el-option label="中型" value="medium" />
-                    <el-option label="大型" value="large" />
+                    <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>
@@ -132,9 +129,9 @@
               </el-col>
               <el-col :span="24">
                 <el-form-item label="宠物标签">
-                  <el-select v-model="form.tags" multiple placeholder="选择标签" style="width: 100%">
-                    <el-option v-for="tag in allPetTags" :key="tag.name" :label="tag.name" :value="tag.name">
-                      <el-tag :type="tag.type" effect="light" size="small">{{ tag.name }}</el-tag>
+                  <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>
@@ -149,14 +146,12 @@
             </el-form-item>
             <el-form-item label="家庭房屋类型">
               <el-radio-group v-model="form.houseType">
-                <el-radio label="stairs">楼梯</el-radio>
-                <el-radio label="elevator">电梯</el-radio>
+                <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="form.entryMethod">
-                <el-radio label="password">密码开门</el-radio>
-                <el-radio label="key">钥匙开门</el-radio>
+                <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="form.entryMethod === 'password'">
@@ -180,7 +175,7 @@
               <el-switch v-model="form.aggression" active-text="是" inactive-text="否" />
             </el-form-item>
             <el-form-item label="疫苗情况">
-              <el-radio-group v-model="form.vaccine">
+              <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>
@@ -189,7 +184,7 @@
             </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="form.vaccineCert" :src="form.vaccineCert" class="avatar" style="width: 100px; height: 100px; object-fit: cover" />
+                <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>
@@ -205,7 +200,7 @@
       <template #footer>
         <span class="dialog-footer">
           <el-button @click="dialogVisible = false">取消</el-button>
-          <el-button type="primary" @click="saveData">保存</el-button>
+          <el-button type="primary" :loading="submitLoading" @click="saveData">保存</el-button>
         </span>
       </template>
     </el-dialog>
@@ -213,23 +208,21 @@
     <!-- Pet Profile Drawer -->
     <el-drawer v-model="drawerVisible" title="宠物档案详情" size="60%" destroy-on-close>
       <div class="profile-header">
-        <el-avatar :size="80" :src="currentPet.avatar" />
+        <el-avatar :size="80" :src="currentPet.avatarUrl" />
         <div class="profile-basic">
           <div class="name-row">
             <span class="name">{{ currentPet.name }}</span>
-            <el-tag size="small" :type="currentPet.gender === '公' ? '' : 'danger'" effect="dark" style="margin-left: 10px">
-              {{ currentPet.gender }}
-            </el-tag>
+            <dict-tag :options="sys_pet_gender" :value="currentPet.gender" />
             <el-tag size="small" effect="plain" type="info" style="margin-left: 5px">{{ currentPet.age }}岁</el-tag>
           </div>
           <div class="tags-row" style="margin-top: 8px">
-            <el-tag v-for="tag in currentPet.tags" :key="tag" :type="getTagType(tag)" effect="light" size="small" style="margin-right: 5px">
-              {{ tag }}
+            <el-tag v-for="tag in currentPet.tags" :key="tag.id" :type="tag.colorType || 'info'" effect="light" size="small" style="margin-right: 5px">
+              {{ tag.name }}
             </el-tag>
           </div>
         </div>
         <div style="margin-left: auto">
-          <el-button type="primary" size="small" plain @click="handleRemark(currentPet)">添加备注</el-button>
+          <el-button type="primary" size="small" plain @click="handleRemark(currentPet)" v-hasPermi="['archieves:pet:edit']">添加备注</el-button>
         </div>
       </div>
 
@@ -239,7 +232,7 @@
           <el-descriptions :column="2" border>
             <el-descriptions-item label="品种">{{ currentPet.breed }}</el-descriptions-item>
             <el-descriptions-item label="体型">
-              {{ currentPet.size === 'small' ? '小型' : currentPet.size === 'medium' ? '中型' : '大型' }}
+              <dict-tag :options="sys_pet_size" :value="currentPet.size" />
             </el-descriptions-item>
             <el-descriptions-item label="体重">{{ currentPet.weight }} kg</el-descriptions-item>
             <el-descriptions-item label="所属主人">{{ currentPet.ownerName }} ({{ currentPet.ownerPhone }})</el-descriptions-item>
@@ -251,10 +244,10 @@
           <el-descriptions :column="2" border>
             <el-descriptions-item label="到家时间">{{ currentPet.arrivalTime || '-' }}</el-descriptions-item>
             <el-descriptions-item label="房屋类型">
-              {{ currentPet.houseType === 'stairs' ? '楼梯' : '电梯' }}
+              <dict-tag :options="sys_house_type" :value="currentPet.houseType" />
             </el-descriptions-item>
             <el-descriptions-item label="入门方式">
-              {{ currentPet.entryMethod === 'password' ? '密码开门' : '钥匙开门' }}
+              <dict-tag :options="sys_entry_method" :value="currentPet.entryMethod" />
             </el-descriptions-item>
             <el-descriptions-item label="开门详情">
               {{ currentPet.entryMethod === 'password' ? currentPet.entryPassword : currentPet.keyLocation }}
@@ -270,12 +263,12 @@
               <el-tag :type="currentPet.aggression ? 'danger' : 'success'" size="small">{{ currentPet.aggression ? '有' : '无' }}</el-tag>
             </el-descriptions-item>
             <el-descriptions-item label="疫苗情况" :span="2">
-              <div>{{ currentPet.vaccine || '-' }}</div>
-              <div v-if="currentPet.vaccineCert" style="margin-top: 10px">
+              <div>{{ currentPet.vaccineStatus || '-' }}</div>
+              <div v-if="currentPet.vaccineCertUrl" style="margin-top: 10px">
                 <el-image
                   style="width: 100px; height: 100px; border-radius: 4px"
-                  :src="currentPet.vaccineCert"
-                  :preview-src-list="[currentPet.vaccineCert]"
+                  :src="currentPet.vaccineCertUrl"
+                  :preview-src-list="[currentPet.vaccineCertUrl]"
                   fit="cover"
                 />
               </div>
@@ -301,9 +294,9 @@
 
         <el-tab-pane label="备注日志" name="logs">
           <el-timeline style="margin-top: 10px; padding-left: 5px">
-            <el-timeline-item v-for="(activity, index) in mockLogs" :key="index" :timestamp="activity.timestamp" :type="activity.type">
-              {{ activity.content }}
-              <div style="font-size: 12px; color: #999; margin-top: 4px">操作人: {{ activity.operator }}</div>
+            <el-timeline-item v-for="(log, index) in changeLogs" :key="index" :timestamp="log.createTime" type="primary">
+              [{{ log.logType }}] {{ log.content }}
+              <div style="font-size: 12px; color: #999; margin-top: 4px">操作人: {{ log.operatorName }}</div>
             </el-timeline-item>
           </el-timeline>
         </el-tab-pane>
@@ -324,20 +317,31 @@
 </template>
 
 <script setup>
-import { ref, reactive, computed } from 'vue';
+import { ref, reactive, onMounted, getCurrentInstance, toRefs } from 'vue';
+import { globalHeaders } from '@/utils/request';
 import { ElMessage, ElMessageBox } from 'element-plus';
+import { listPet, getPet, addPet, updatePet, delPet } from '@/api/archieves/pet';
+import { listAllTag } from '@/api/archieves/tag';
+import { listAllCustomer } from '@/api/archieves/customer';
+import { listAllChangeLog } from '@/api/archieves/changeLog';
+
+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 loading = ref(false);
+const submitLoading = ref(false);
+const total = ref(0);
+const tableData = ref([]);
+
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  keyword: ''
+});
 
 const searchKey = ref('');
-const currentPage = ref(1);
-const pageSize = ref(10);
-const total = ref(100);
-
-const handleSizeChange = (val) => {
-  console.log(`每页 ${val} 条`);
-};
-const handleCurrentChange = (val) => {
-  console.log(`当前页: ${val}`);
-};
 
 const dialogVisible = ref(false);
 const drawerVisible = ref(false);
@@ -347,165 +351,125 @@ const activeTab = ref('basic');
 const detailActiveTab = ref('info');
 const currentPet = ref({});
 
+const allPetTags = ref([]);
+const userList = ref([]);
+const changeLogs = ref([]);
+
 const mockOrders = ref([
   { orderNo: 'DD20231001001', service: '上门喂养 (标准版)', time: '2023-10-01 10:00', amount: '88.00', status: 'completed' },
   { orderNo: 'DD20230915002', service: '深度洗护套餐', time: '2023-09-15 14:00', amount: '158.00', status: 'completed' }
 ]);
 
-const mockLogs = ref([
-  { content: '因宠物胆小,建议安排资深服务师', timestamp: '2023-10-01 09:00', operator: '客服小美', type: 'warning' },
-  { content: '更新了疫苗接种记录', timestamp: '2023-09-10 11:30', operator: '管理员', type: 'primary' },
-  { content: '创建宠物档案', timestamp: '2023-01-01 10:00', operator: '系统', type: 'info' }
-]);
-
 const remarkForm = reactive({ content: '' });
+const avatarDisplayUrl = ref('');
+const vaccineCertDisplayUrl = ref('');
 
-// Mock Users for selection
-const userList = [
-  { id: 101, name: '张先生', phone: '13800138000' },
-  { id: 102, name: '李小姐', phone: '13900139000' }
-];
-
-const petBreeds = [
-  '金毛',
-  '拉布拉多',
-  '柴犬',
-  '柯基',
-  '哈士奇',
-  '阿拉斯加',
-  '萨摩耶',
-  '边境牧羊犬',
-  '德国牧羊犬',
-  '贵宾犬/泰迪',
-  '比熊',
-  '博美',
-  '雪纳瑞',
-  '法斗',
-  '中华田园犬',
-  '英短',
-  '美短',
-  '布偶猫',
-  '加菲猫',
-  '暹罗猫',
-  '波斯猫',
-  '缅因猫',
-  '中华田园猫'
-];
-
-const allPetTags = [
-  { name: '易过敏', type: 'danger' },
-  { name: '胆小', type: 'warning' },
-  { name: '攻击性', type: 'info' },
-  { name: '粘人', type: 'success' }
-];
-
-const getTagType = (name) => {
-  const tag = allPetTags.find((t) => t.name === name);
-  return tag ? tag.type : 'info';
-};
-
-const tableData = ref([
-  {
-    id: 1,
-    avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
-    name: '旺财',
-    gender: '公',
-    breed: '金毛',
-    age: 3,
-    size: 'large',
-    weight: 30,
-    ownerId: 101,
-    ownerName: '张先生',
-    ownerPhone: '13800138000',
-    tags: ['易过敏', '胆小'],
-    healthStatus: '健康',
-    personality: '活泼',
-    cutePersonality: '超级粘人,喜欢玩球',
-    arrivalTime: '2023-01-01',
-    houseType: 'elevator',
-    entryMethod: 'password',
-    entryPassword: '456',
-    aggression: false,
-    vaccine: '已打3次',
-    vaccineCert: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg',
-    medicalHistory: '',
-    allergies: '海鲜'
-  }
-]);
-
-const filteredTableData = computed(() => {
-  return tableData.value.filter((item) => !searchKey.value || item.name.includes(searchKey.value) || item.ownerName.includes(searchKey.value));
-});
 
 const form = reactive({
-  id: null,
-  avatar: '',
+  id: undefined,
+  userId: undefined,
+  avatar: undefined,
   name: '',
-  gender: '公',
+  type: 0,
+  gender: undefined,
   breed: '',
+  birthday: '',
   age: 1,
-  size: 'small',
   weight: 5,
-  ownerId: null,
-  personality: '',
-  cutePersonality: '',
-  tags: [],
-
+  size: 'small',
+  isSterilized: 0,
   arrivalTime: '',
-  houseType: 'stairs',
-  entryMethod: 'key',
+  houseType: '',
+  entryMethod: '',
   entryPassword: '',
   keyLocation: '',
-
+  personality: '',
+  cutePersonality: '',
   healthStatus: '健康',
-  aggression: false,
-  vaccine: '无',
-  vaccineCert: '',
+  aggression: 0,
+  vaccineStatus: '无',
+  vaccineCert: undefined,
   medicalHistory: '',
-  allergies: ''
+  allergies: '',
+  remark: '',
+  tagIds: []
 });
 
+const getList = () => {
+  loading.value = true;
+  queryParams.keyword = searchKey.value;
+  listPet(queryParams).then((res) => {
+    tableData.value = res.rows;
+    total.value = res.total;
+  }).finally(() => {
+    loading.value = false;
+  });
+};
+
+const handleSearch = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const loadTags = () => {
+  listAllTag({ category: 'pet', status: 0 }).then((res) => {
+    allPetTags.value = res.data || [];
+  });
+};
+
+const loadUsers = () => {
+  listAllCustomer({ status: 0 }).then((res) => {
+    userList.value = res.data || [];
+  });
+};
+
 const handleAdd = () => {
   isEdit.value = false;
   activeTab.value = 'basic';
   Object.assign(form, {
-    id: null,
-    avatar: '',
-    name: '',
-    gender: '公',
-    breed: '',
-    age: 1,
-    size: 'small',
-    weight: 5,
-    ownerId: null,
-    personality: '',
-    cutePersonality: '',
-    tags: [],
-    arrivalTime: '',
-    houseType: 'stairs',
-    entryMethod: 'key',
-    entryPassword: '',
-    keyLocation: '',
-    healthStatus: '健康',
-    aggression: false,
-    vaccine: '无',
-    vaccineCert: '',
-    medicalHistory: '',
-    allergies: ''
+    id: undefined, 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: []
   });
+  avatarDisplayUrl.value = '';
+  vaccineCertDisplayUrl.value = '';
   dialogVisible.value = true;
 };
 
 const handleEdit = (row) => {
   isEdit.value = true;
   activeTab.value = 'basic';
-  Object.assign(form, row);
-  dialogVisible.value = true;
+  getPet(row.id).then((res) => {
+    const data = res.data;
+    Object.assign(form, {
+      id: data.id, userId: data.userId, avatar: data.avatar, name: data.name, type: data.type,
+      gender: data.gender, breed: data.breed, birthday: data.birthday, age: data.age,
+      weight: data.weight, size: data.size, isSterilized: data.isSterilized,
+      arrivalTime: data.arrivalTime, houseType: data.houseType, entryMethod: data.entryMethod,
+      entryPassword: data.entryPassword, keyLocation: data.keyLocation,
+      personality: data.personality, cutePersonality: data.cutePersonality,
+      healthStatus: data.healthStatus, aggression: data.aggression,
+      vaccineStatus: data.vaccineStatus, vaccineCert: data.vaccineCert,
+      medicalHistory: data.medicalHistory, allergies: data.allergies, remark: data.remark,
+      tagIds: data.tags ? data.tags.map(t => t.id) : []
+    });
+    avatarDisplayUrl.value = data.avatarUrl || '';
+    vaccineCertDisplayUrl.value = data.vaccineCertUrl || '';
+    dialogVisible.value = true;
+  });
 };
 
 const handleDetail = (row) => {
-  currentPet.value = { ...row };
-  drawerVisible.value = true;
+  getPet(row.id).then((res) => {
+    currentPet.value = res.data;
+    detailActiveTab.value = 'info';
+    listAllChangeLog(row.id, 'pet').then((logRes) => {
+      changeLogs.value = logRes.data || [];
+    });
+    drawerVisible.value = true;
+  });
 };
 
 const handleRemark = (row) => {
@@ -516,59 +480,101 @@ const handleRemark = (row) => {
 
 const saveRemark = () => {
   if (!remarkForm.content) return ElMessage.warning('请输入内容');
-  mockLogs.value.unshift({
-    content: remarkForm.content,
-    timestamp: new Date().toLocaleString(),
-    operator: '当前用户',
-    type: 'primary'
+  const data = { id: currentPet.value.id, remark: remarkForm.content };
+  updatePet(data).then(() => {
+    ElMessage.success('备注添加成功');
+    remarkDialogVisible.value = false;
+    getList();
+    // 刷新 drawer 中的变更日志
+    if (drawerVisible.value) {
+      listAllChangeLog(currentPet.value.id, 'pet').then((logRes) => {
+        changeLogs.value = logRes.data || [];
+      });
+    }
   });
-  ElMessage.success('备注添加成功');
-  remarkDialogVisible.value = false;
 };
 
 const handleDelete = (row) => {
   ElMessageBox.confirm('确认删除该宠物档案吗?', '提示', { type: 'warning' }).then(() => {
-    tableData.value = tableData.value.filter((item) => item.id !== row.id);
-    ElMessage.success('删除成功');
+    delPet(row.id).then(() => {
+      ElMessage.success('删除成功');
+      getList();
+    });
   });
 };
 
-const handleUploadFile = (file) => {
-  form.avatar = URL.createObjectURL(file.raw);
+const baseUrl = import.meta.env.VITE_APP_BASE_API;
+const uploadUrl = baseUrl + '/resource/oss/upload';
+
+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 handleUploadVaccineCert = (file) => {
-  form.vaccineCert = URL.createObjectURL(file.raw);
+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('疫苗凭证上传失败');
+  }
 };
 
 const saveData = () => {
   if (!form.name) return ElMessage.warning('请输入宠物姓名');
-  if (!form.ownerId) return ElMessage.warning('请选择所属主人');
-
-  const owner = userList.find((u) => u.id === form.ownerId);
-  const saveData = {
-    ...form,
-    ownerName: owner ? owner.name : 'Unknown',
-    ownerPhone: owner ? owner.phone : ''
-  };
-
-  if (isEdit.value) {
-    const idx = tableData.value.findIndex((item) => item.id === form.id);
-    if (idx !== -1) Object.assign(tableData.value[idx], saveData);
-  } else {
-    tableData.value.push({
-      id: Date.now(),
-      ...saveData
-    });
-  }
-  ElMessage.success('保存成功');
-  dialogVisible.value = false;
+  if (!form.userId) return ElMessage.warning('请选择所属主人');
+  submitLoading.value = true;
+  const api = isEdit.value ? updatePet(form) : addPet(form);
+  api.then(() => {
+    ElMessage.success('保存成功');
+    dialogVisible.value = false;
+    getList();
+  }).finally(() => {
+    submitLoading.value = false;
+  });
 };
-</script>
 
-<script>
-// Separate setup logic needed for function declarations not hoisted? No, script setup handles it.
-// Additional functions
+onMounted(() => {
+  getList();
+  loadTags();
+  loadUsers();
+});
 </script>
 
 <style scoped>

+ 256 - 0
src/views/order/management-bak/components/CareSummaryDrawer.vue

@@ -0,0 +1,256 @@
+<template>
+  <el-drawer
+    :model-value="visible"
+    @update:model-value="updateVisible"
+    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">
+              <el-tag :type="order.petGender==='male'?'':'danger'" effect="light" round>
+                <el-icon><component :is="order.petGender==='male'?'Male':'Female'" /></el-icon>
+                {{ 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="房屋类型">{{ order.houseType }}</el-descriptions-item>
+          <el-descriptions-item label="入户方式" :span="2">
+            <span style="font-weight:bold;">{{ order.entryMethod }}</span>
+            <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="updateVisible(false)">关闭</el-button>
+        </div>
+      </div>
+    </div>
+  </el-drawer>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+
+const props = defineProps({
+  visible: Boolean,
+  order: Object
+})
+
+const emit = defineEmits(['update:visible', 'success'])
+
+const isEditingSummary = ref(false)
+const careSummaryText = ref('')
+
+watch(() => props.visible, (val) => {
+  if (val && props.order) {
+    isEditingSummary.value = false
+    if (!props.order.careSummary) {
+      careSummaryText.value = `1. 精神/身体状态:${props.order.petName}精神状态良好,愿意互动。
+2. 进食/饮水:食欲正常,饮水适当,已清洗碗具。
+3. 排泄情况:排便正常,颜色形状正常,已清理。
+4. 卫生情况:猫砂盆/地面已清理干净,无异味。
+5. 互动情况:陪玩了20分钟,${props.order.petName}很开心。
+6. 特殊情况/备注:无特殊异常。`
+    } else {
+      careSummaryText.value = props.order.careSummary
+    }
+  }
+})
+
+const updateVisible = (val) => {
+  emit('update:visible', val)
+}
+
+const saveCareSummary = () => {
+  if (props.order) {
+    props.order.careSummary = careSummaryText.value
+    if (!props.order.summaryTime) {
+      props.order.summaryTime = '2024-02-04 17:00'
+    }
+    ElMessage.success('护理小结已保存')
+    isEditingSummary.value = false
+    emit('success', careSummaryText.value)
+  }
+}
+</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>

+ 942 - 0
src/views/order/management-bak/components/OrderDetailDrawer.vue

@@ -0,0 +1,942 @@
+<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].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 === 3">
+                            <el-button type="primary" icon="CircleCheck"
+                                @click="emit('command', 'complete', order)">确认完成</el-button>
+                        </template>
+
+                        <template v-if="[3, 4].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, 4].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: '已取消' }
+    return map[status] || '未知'
+}
+const getStatusTag = (status) => {
+    const map = { 0: 'danger', 1: 'warning', 2: 'primary', 3: 'warning', 4: 'success', 5: '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].includes(status)) active = 2
+    steps[2].time = findTime('接单')
+    if ([1].includes(status)) {
+        steps[2].title = '待履约者接单'
+    } else if ([2, 3, 4].includes(status)) {
+        steps[2].title = '履约者已接单'
+        active = 3
+    }
+    steps[3].time = findTime('到达') || findTime('出发')
+    if ([2].includes(status)) {
+        steps[3].title = '服务进行中'
+    } else if ([3, 4].includes(status)) {
+        steps[3].title = '服务已完成'
+        active = 4
+    }
+    steps[4].time = findTime('等待商家确认') || findTime('待验收')
+    if ([3].includes(status)) {
+        steps[4].title = '待商家确认'
+    } else if ([4].includes(status)) {
+        steps[4].title = '商家已确认'
+        active = 5
+    }
+    if (status === 4) {
+        steps[5].time = findTime('完成')
+        active = 6
+    }
+    if (status === 5) {
+        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;
+}
+
+.section-block {
+    margin-bottom: 20px;
+}
+
+.sec-title-bar {
+    font-weight: bold;
+    font-size: 14px;
+    color: #303133;
+    margin-bottom: 12px;
+    padding-left: 8px;
+    border-left: 3px solid #409eff;
+}
+</style>

+ 52 - 0
src/views/order/management-bak/components/RemarkDialog.vue

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

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

@@ -0,0 +1,101 @@
+<template>
+  <el-dialog :model-value="visible" @update:model-value="updateVisible" title="奖惩操作" width="500px">
+    <div v-if="data" 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;">{{ data.fulfillerName || '未指派' }}</span></div>
+        <div style="font-size: 13px; margin-top: 4px;">订单号:{{ data.orderNo }}</div>
+        <div style="font-size: 13px; margin-top: 4px; display:flex; align-items:center; gap:6px;">
+          服务类型:
+          <el-tag :type="getTypeTag(data.type)" size="small">{{ getTypeName(data.type) }}</el-tag>
+          <el-tag v-if="data.type === 'transport' && data.transportType === 'round'" size="small" effect="plain" type="warning">往返</el-tag>
+          <el-tag v-if="data.splitType === 'pick'" size="small" effect="dark" color="#409eff" style="border:none; color:white;">接</el-tag>
+          <el-tag v-if="data.splitType === 'drop'" size="small" effect="dark" color="#67c23a" style="border:none; color:white;">送</el-tag>
+          <el-tag v-if="data.type === 'transport' && data.transportType === 'pick' && !data.splitType" size="small" effect="plain">单程接</el-tag>
+          <el-tag v-if="data.type === 'transport' && data.transportType === 'drop' && !data.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="amount">金额 (元)</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="updateVisible(false)">取消</el-button>
+      <el-button type="primary" @click="handleSubmit">确认执行</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { reactive, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+
+const props = defineProps({
+  visible: Boolean,
+  data: Object
+})
+
+const emit = defineEmits(['update:visible', 'success'])
+
+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 updateVisible = (val) => {
+  emit('update:visible', val)
+}
+
+const handleSubmit = () => {
+  if (!rewardForm.reason) {
+    ElMessage.warning('请输入奖惩原因')
+    return
+  }
+  ElMessage.success(`操作成功:${rewardForm.type === 'reward' ? '奖励' : '惩罚'}已执行`)
+  updateVisible(false)
+  emit('success')
+}
+</script>

+ 2572 - 0
src/views/order/management-bak/index.vue

@@ -0,0 +1,2572 @@
+<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.orderType" size="default" @change="handleSearch">
+              <el-radio-button label="">全部类型</el-radio-button>
+              <el-radio-button v-for="item in serviceTypeList" :key="item.id" :label="item.id">{{ item.name
+              }}</el-radio-button>
+            </el-radio-group>
+            <el-input v-model="filters.keyword" 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-click="handleSearch">
+          <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-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" :src="row.petAvatar" :class="'avatar-' + row.type">{{ row.petName?.charAt(0)
+              }}</el-avatar>
+              <div class="pet-detail">
+                <div class="pet-name">
+                  {{ row.petName }}
+                  <span class="pet-breed">{{ row.petBreed }}</span>
+                </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="serviceTime" 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="'status-' + 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).toFixed(2) }}</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].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="[2, 3, 4].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 === 3" command="complete">确认完成</el-dropdown-item>
+                    <el-dropdown-item v-if="[3, 4].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>
+
+    <!-- 订单详情侧滑栏 -->
+    <el-drawer v-model="detailVisible" title="订单详情" direction="rtl" size="60%" class="order-detail-drawer">
+      <div class="detail-container" v-if="currentOrder">
+        <!-- 1. Header Status -->
+        <!-- 1. Header Status -->
+        <div class="detail-header">
+          <div class="left-head">
+            <span class="order-no">{{ currentOrder.orderNo }}</span>
+            <el-tag :type="getStatusTag(currentOrder.status)" effect="dark" class="status-tag">{{
+              getStatusName(currentOrder.status) }}</el-tag>
+            <el-tag effect="plain" class="type-tag"
+              :type="currentOrder.type === 'transport' ? '' : (currentOrder.type === 'feeding' ? 'warning' : 'danger')">
+              {{ getTypeName(currentOrder.type) }}
+            </el-tag>
+          </div>
+          <div class="right-head">
+            <!-- Action Buttons Group -->
+            <div class="detail-actions">
+              <template v-if="currentOrder.status === 0">
+                <el-button type="danger" plain icon="CircleClose" @click="handleCancel(currentOrder)">取消订单</el-button>
+              </template>
+
+              <template v-if="currentOrder.status === 3">
+                <el-button type="primary" icon="CircleCheck"
+                  @click="handleCommand('complete', currentOrder)">确认完成</el-button>
+              </template>
+
+              <template v-if="[3, 4].includes(currentOrder.status)">
+                <el-button icon="Notebook" @click="openCareSummary(currentOrder)">护理小结</el-button>
+              </template>
+
+              <el-dropdown trigger="click" @command="(cmd) => handleCommand(cmd, currentOrder)"
+                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, 4].includes(currentOrder.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 (Enhanced) -->
+            <div class="info-section pet-section">
+              <div class="sec-header">
+                <span class="label">宠物档案</span>
+                <el-tag size="small" effect="plain">{{ currentOrder.petBreed }}</el-tag>
+              </div>
+              <div class="pet-basic-row">
+                <el-avatar :size="50" :src="currentOrder.petAvatar" shape="square" class="pet-avatar-lg">{{
+                  (currentOrder.petName || '').charAt(0) }}</el-avatar>
+                <div class="pet-names">
+                  <div class="b-name">{{ currentOrder.petName }}
+                    <el-icon v-if="currentOrder.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">{{ currentOrder.petAge || '未知年龄' }}</el-tag>
+                    <el-tag size="small" type="info">{{ currentOrder.petWeight || '未知体重' }}</el-tag>
+                  </div>
+                </div>
+              </div>
+              <el-descriptions :column="2" size="small" class="pet-desc" border>
+                <el-descriptions-item label="绝育状态">{{ currentOrder.petSterilized ? '已绝育' : '未绝育'
+                }}</el-descriptions-item>
+                <el-descriptions-item label="疫苗状态"><span style="color:#67c23a">{{ currentOrder.petVaccine || '未知'
+                }}</span></el-descriptions-item>
+                <el-descriptions-item label="性格特点">{{ currentOrder.petCharacter || '温顺' }}</el-descriptions-item>
+                <el-descriptions-item label="健康状况">{{ currentOrder.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="currentOrder.userAvatar">{{ (currentOrder.userName || '').charAt(0)
+                  }}</el-avatar>
+                  <div class="u-info">
+                    <div class="nm">{{ currentOrder.userName }}</div>
+                    <div class="ph">{{ currentOrder.contactPhone }}</div>
+                  </div>
+                </div>
+                <div class="addr-box">
+                  <div class="addr-label">服务地址</div>
+                  <div class="addr-txt">{{ currentOrder.city }}{{ currentOrder.district }} {{ currentOrder.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">
+                <!-- A. General Order Info -->
+                <div class="section-block">
+                  <div class="sec-title-bar">基础业务信息</div>
+                  <el-descriptions :column="3" border size="default" class="custom-desc">
+                    <el-descriptions-item label="系统单号">{{ currentOrder.orderNo }}</el-descriptions-item>
+                    <el-descriptions-item label="服务类型">
+                      {{ getTypeName(currentOrder.type) }}
+                      <el-tag size="small" v-if="currentOrder.type === 'transport'" style="margin-left:5px"
+                        effect="light">{{
+                          getTransportModeName(currentOrder.transportType) }}</el-tag>
+                    </el-descriptions-item>
+
+                    <el-descriptions-item label="归属门店">{{ currentOrder.merchantName }} (平台代下单)</el-descriptions-item>
+                    <el-descriptions-item label="宠主信息">{{ currentOrder.userName }} / {{ currentOrder.contactPhone
+                    }}</el-descriptions-item>
+                    <el-descriptions-item label="服务费用" label-class-name="money-label">
+                      <span style="color:#f56c6c; font-weight:bold;">¥ {{ currentOrder.fulfillerFee }}</span>
+                    </el-descriptions-item>
+
+                    <el-descriptions-item label="预约时间">{{ getServiceTimeRange(currentOrder.serviceTime)
+                    }}</el-descriptions-item>
+                    <el-descriptions-item label="团购套餐">{{ currentOrder.groupBuyPackage || '未使用团购套餐'
+                    }}</el-descriptions-item>
+                    <el-descriptions-item label="创建时间">{{ currentOrder.createTime }}</el-descriptions-item>
+
+                    <el-descriptions-item label="订单备注" :span="3">
+                      {{ currentOrder.remark || '暂无备注' }}
+                    </el-descriptions-item>
+                  </el-descriptions>
+                </div>
+
+                <!-- B. Transport Specifics (Single Display) -->
+                <div v-if="currentOrder.type === 'transport'" class="section-block transport-split-block">
+                  <div class="sec-title-bar">接送任务详情</div>
+                  <div class="transport-grid">
+                    <!-- Pick Up Info (Only if pick or round) -->
+                    <div class="transport-card pick-card" v-if="['round', 'pick'].includes(currentOrder.transportType)">
+                      <div class="t-header">
+                        <div class="left-badges">
+                          <el-tag :type="currentOrder.type === 'feeding' ? 'warning' : ''" effect="dark">{{
+                            getTypeName(currentOrder.type) }}</el-tag>
+                          <el-tag size="small" effect="plain" class="sub-badge">接</el-tag>
+                        </div>
+                        <span class="time">{{ currentOrder.detail.pickTime || currentOrder.serviceTime }}</span>
+                      </div>
+                      <div class="t-body">
+                        <div class="row"><el-icon>
+                            <Location />
+                          </el-icon> <span class="addr">{{ currentOrder.detail.pickAddr }}</span></div>
+                        <div class="row sub"><el-icon>
+                            <User />
+                          </el-icon> {{ currentOrder.detail.pickContact || currentOrder.userName }} <el-icon>
+                            <Phone />
+                          </el-icon> {{ currentOrder.detail.pickPhone || currentOrder.contactPhone }}</div>
+                      </div>
+                    </div>
+                    <!-- Drop Off Info (Only if drop or round) -->
+                    <div class="transport-card drop-card" v-if="['round', 'drop'].includes(currentOrder.transportType)">
+                      <div class="t-header">
+                        <div class="left-badges">
+                          <el-tag :type="currentOrder.type === 'feeding' ? 'warning' : 'success'" effect="dark">{{
+                            getTypeName(currentOrder.type) }}</el-tag>
+                          <el-tag size="small" effect="plain" class="sub-badge">送</el-tag>
+                        </div>
+                        <span class="time">{{ currentOrder.detail.dropTime || '待定' }}</span>
+                      </div>
+                      <div class="t-body">
+                        <div class="row"><el-icon>
+                            <Location />
+                          </el-icon> <span class="addr">{{ currentOrder.detail.dropAddr }}</span></div>
+                        <div class="row sub"><el-icon>
+                            <User />
+                          </el-icon> {{ currentOrder.detail.dropContact || currentOrder.userName }} <el-icon>
+                            <Phone />
+                          </el-icon> {{ currentOrder.detail.dropPhone || currentOrder.contactPhone }}</div>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+
+                <!-- C. Other Service Specifics -->
+                <div v-if="['feeding', 'washing'].includes(currentOrder.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">{{ currentOrder.detail.area || currentOrder.address
+                    }}</el-descriptions-item>
+                    <el-descriptions-item label="服务套餐">{{ currentOrder.detail.packageName }}</el-descriptions-item>
+                    <el-descriptions-item label="特殊要求">{{ currentOrder.detail.petStatus || '无' }}</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="currentOrder.fulfillerName" class="fulfiller-card">
+                  <div class="f-left">
+                    <el-avatar :size="60" :src="currentOrder.fulfillerAvatar">{{ currentOrder.fulfillerName.charAt(0)
+                    }}</el-avatar>
+                  </div>
+                  <div class="f-right">
+                    <div class="f-row1">
+                      <span class="f-name">{{ currentOrder.fulfillerName }}</span>
+                      <el-tag size="small" type="primary" effect="plain" round>Lv1 普通</el-tag>
+                    </div>
+                    <div class="f-row2">
+                      <span>联系电话:{{ currentOrder.fulfillerPhone || '138****0000' }}</span>
+                      <span class="sep">|</span>
+                      <span>归属区域:{{ currentOrder.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>{{ currentOrder.createTime }}</span>
+                      <span><span style="color:#909399;">接单时间:</span>{{ currentOrder.detail?.receiveTime ||
+                        currentOrder.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">
+                <!-- Empty State for Pending Accept -->
+                <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 (currentOrder.orderLogs || [])" :key="index"
+                    :timestamp="log.time" :type="log.type || 'primary'" :icon="log.icon" placement="top">
+                    <div class="log-card">
+                      <div class="l-tit">{{ log.title }}</div>
+                      <div class="l-txt">{{ log.content }}</div>
+                    </div>
+                  </el-timeline-item>
+
+                  <!-- Fallback Legacy Mock -->
+                  <el-timeline-item
+                    v-if="(!currentOrder.orderLogs || currentOrder.orderLogs.length === 0) && currentOrder.timeline"
+                    v-for="(log, idx) in currentOrder.timeline" :key="'old-' + idx" :timestamp="log.time"
+                    :type="log.type">
+                    {{ log.content }}
+                  </el-timeline-item>
+                </el-timeline>
+              </div>
+            </el-tab-pane>
+          </el-tabs>
+
+        </div>
+      </div>
+    </el-drawer>
+
+    <!-- 派单弹窗 (Enhanced) -->
+    <el-dialog v-model="dispatchDialogVisible" 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="currentDispatchOrder">
+          <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="currentDispatchOrder.typeCode">
+                {{ getShortType(currentDispatchOrder.typeCode) }}
+              </div>
+            </div>
+            <div class="card-main">
+              <template v-if="currentDispatchOrder.typeCode === 'transport'">
+                <div class="row-addr" v-if="['round', 'pick'].includes(currentDispatchOrder.transportType)"
+                  :title="currentDispatchOrder.pickAddr">
+                  <span class="tag pick">取</span> {{ currentDispatchOrder.pickAddr }}
+                </div>
+                <div class="row-addr" v-if="['round', 'drop'].includes(currentDispatchOrder.transportType)"
+                  :title="currentDispatchOrder.dropAddr">
+                  <span class="tag drop">送</span> {{ currentDispatchOrder.dropAddr }}
+                </div>
+              </template>
+              <template v-else>
+                <div class="row-addr" :title="currentDispatchOrder.address">
+                  <span class="tag home">址</span> {{ currentDispatchOrder.address }}
+                </div>
+              </template>
+              <div class="row-time" style="margin-top: 4px;">
+                <el-icon>
+                  <Clock />
+                </el-icon> {{ currentDispatchOrder.time }}
+                <span class="days-tag" v-if="currentDispatchOrder.daysLater">{{ currentDispatchOrder.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 class="dot" :class="currentRider.status"></div>
+            </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.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;">
+                <span v-for="cat in currentRider.categories" :key="cat" class="cat-tag"
+                  :class="getCategoryClass(cat)">{{
+                    cat }}</span>
+              </div>
+
+              <div class="row-3 time-row" style="margin-top: 4px;">
+                <span class="last-time">下一单: {{ (currentRider.status === 'offline' || currentRider.status ===
+                  'disabled') ?
+                  '--' : currentRider.lastServiceTime }}</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 height="400px">
+              <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 class="dot" :class="rider.status"></div>
+                  </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.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;">
+                      <span v-for="cat in rider.categories" :key="cat" class="cat-tag" :class="getCategoryClass(cat)">{{
+                        cat
+                      }}</span>
+                    </div>
+
+                    <div class="row-3 time-row" style="margin-top: 4px;">
+                      <span class="last-time">下一单: {{ (rider.status === 'offline' || rider.status === 'disabled') ? '--'
+                        :
+                        rider.lastServiceTime }}</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>
+
+        <!-- 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="dispatchDialogVisible = false">取消</el-button>
+            <el-button type="primary" @click="handleDispatchSubmit">确认派单</el-button>
+          </div>
+        </div>
+      </div>
+    </el-dialog>
+
+    <!-- 护理小结侧滑栏 -->
+    <CareSummaryDrawer v-model:visible="careSummaryVisible" :order="careSummaryOrder"
+      @success="handleCareSummarySuccess" />
+
+    <!-- 奖惩弹窗 -->
+    <RewardDialog v-model:visible="rewardDialogVisible" :data="currentOperateRow" />
+
+    <!-- 备注弹窗 -->
+    <RemarkDialog v-model:visible="remarkDialogVisible" :data="currentOperateRow" />
+
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import OrderDetailDrawer from './components/OrderDetailDrawer.vue'
+import RewardDialog from './components/RewardDialog.vue'
+import RemarkDialog from './components/RemarkDialog.vue'
+import CareSummaryDrawer from './components/CareSummaryDrawer.vue'
+import { listServiceOnOrder } from '@/api/service/list'
+import { listSubOrderOnMerchant } from '@/api/order/subOrder/index'
+import { listOnStore } from '@/api/system/areaStation'
+
+const loading = ref(false)
+const detailVisible = ref(false)
+const dispatchVisible = ref(false)
+const currentOrder = ref(null)
+const serviceTypeList = ref([])
+const areaStationList = ref([])
+
+const filters = reactive({
+  orderType: '',
+  status: '',
+  keyword: ''
+})
+
+const pagination = reactive({
+  current: 1,
+  size: 10,
+  total: 100
+})
+
+const dispatchForm = reactive({
+  orderId: null,
+  fulfillerId: null,
+  fee: 0,
+  remark: ''
+})
+
+// Mocks
+const fulfillerOptions = ref([
+  { id: 101, name: '王大力', distance: '1.2km' },
+  { id: 102, name: '张小美', distance: '3.5km' },
+  { id: 103, name: '李建国', distance: '0.8km' }
+])
+
+const tableData = ref([])
+
+const mockData = [
+  // 宠物接送 - 待派单 (往返)
+  {
+    id: 1,
+    orderNo: 'ORD202402048801',
+    type: 'transport',
+    transportType: 'round', // 往返
+    petName: '旺财',
+    petBreed: '金毛',
+    petAvatar: '',
+    userName: '李先生',
+    city: '北京市',
+    district: '朝阳区',
+    createTime: '2024-02-04 09:30',
+    merchantName: '爱宠生活馆 (三里屯店)',
+    contactPhone: '13812345678',
+    serviceTime: '2024-02-05 10:00',
+    status: 0,
+    fulfillerName: '',
+    fulfillerFee: 0,
+    detail: {
+      pickAddr: '朝阳区三里屯SOHO B座',
+      pickContact: '张先生',
+      pickPhone: '138xxxx',
+      pickTime: '10:00',
+      dropAddr: '朝阳区某某宠物医院',
+      dropContact: '前台',
+      dropPhone: '010-xxxx',
+    },
+    timeline: [{ time: '2024-02-04 09:30', content: '商户下单成功', type: 'primary' }]
+  },
+  // 宠物接送 - 待接单 (单程接)
+  {
+    id: 2,
+    orderNo: 'ORD202402048802',
+    type: 'transport',
+    transportType: 'pick', // 单程接
+    petName: 'Bella',
+    petBreed: '拉布拉多',
+    petAvatar: '',
+    userName: '赵女士',
+    city: '北京市',
+    district: '海淀区',
+    createTime: '2024-02-04 10:00',
+    merchantName: '爱宠生活馆 (国贸店)',
+    contactPhone: '13911112222',
+    serviceTime: '2024-02-05 14:00',
+    status: 1,
+    fulfillerName: '王大力',
+    fulfillerFee: 35.00,
+    detail: {
+      pickAddr: '海淀区万柳书院',
+      pickContact: '李女士',
+      pickPhone: '139xxxx',
+      pickTime: '14:00',
+      dropAddr: '',
+    },
+    timeline: [
+      { time: '2024-02-04 10:00', content: '下单成功', type: 'info' },
+      { time: '2024-02-04 10:30', content: '已派单给 王大力', type: 'primary' }
+    ]
+  },
+  // 宠物接送 - 服务中 (单程送)
+  {
+    id: 3,
+    orderNo: 'ORD202402048803',
+    type: 'transport',
+    transportType: 'drop', // 单程送
+    petName: 'Cookie',
+    petBreed: '柯基',
+    petAvatar: '',
+    userName: '王先生',
+    city: '北京市',
+    district: '朝阳区',
+    createTime: '2024-02-04 12:00',
+    merchantName: '爱宠生活馆 (中关村店)',
+    contactPhone: '13612345678',
+    serviceTime: '2024-02-04 18:00',
+    status: 2,
+    fulfillerName: '张小美',
+    fulfillerFee: 40.00,
+    detail: {
+      pickAddr: '', // 单程送不需要接的详细起始点,实际业务中是已在店
+      dropAddr: '朝阳公园西门',
+      dropContact: '王先生',
+      dropPhone: '136xxxx',
+    },
+    timeline: [
+      { time: '2024-02-04 12:00', content: '下单成功', type: 'info' },
+      { time: '2024-02-04 17:50', content: '履约者出发', type: 'primary' }
+    ]
+  },
+  // 上门喂遛 - 待派单
+  {
+    id: 4,
+    orderNo: 'ORD202402048804',
+    type: 'feeding',
+    petName: '咪咪',
+    petBreed: '布偶猫',
+    petAvatar: '',
+    userName: '李女士',
+    city: '上海市',
+    district: '徐汇区',
+    createTime: '2024-02-04 12:00',
+    merchantName: '爱宠生活馆 (三里屯店)',
+    contactPhone: '13987654321',
+    serviceTime: '2024-02-06 12:00',
+    status: 0,
+    fulfillerName: '',
+    fulfillerFee: 0,
+    detail: {
+      packageName: '春节上门喂猫7天套餐',
+      currentCount: 1,
+      totalCount: 7,
+      area: '客厅、阳台',
+      itemLoc: '厨房柜子里',
+      cleanLoc: '卫生间洗手台',
+      foodAmount: '每次一罐头 + 半碗粮',
+    },
+    timeline: [{ time: '2024-02-04 12:00', content: '订单创建', type: 'info' }]
+  },
+  // 上门喂遛 - 服务中
+  {
+    id: 5,
+    orderNo: 'ORD202402048805',
+    type: 'feeding',
+    petName: '大黄',
+    petBreed: '中华田园犬',
+    petAvatar: '',
+    userName: '张先生',
+    city: '杭州市',
+    district: '西湖区',
+    createTime: '2024-02-04 12:30',
+    merchantName: '爱宠生活馆 (国贸店)',
+    contactPhone: '13555555555',
+    serviceTime: '2024-02-04 14:00',
+    status: 2,
+    fulfillerName: '李建国',
+    fulfillerFee: 50.00,
+    detail: {
+      packageName: '上门遛狗1小时',
+      currentCount: 1,
+      totalCount: 1,
+      area: '小区公园',
+      itemLoc: '门口鞋柜处',
+      cleanLoc: '楼下垃圾桶',
+      foodAmount: '-',
+    },
+    timeline: [
+      { time: '2024-02-04 12:00', content: '张小美 已接单', type: 'primary' },
+      { time: '2024-02-04 13:55', content: '履约者到达并在门口打卡', type: 'success', media: ['https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg'] }
+    ]
+  },
+  // 上门洗护 - 待接单
+  {
+    id: 6,
+    orderNo: 'ORD202402048806',
+    type: 'washing',
+    petName: '豆豆',
+    petBreed: '泰迪',
+    petAvatar: '',
+    userName: '刘小姐',
+    city: '深圳市',
+    district: '南山区',
+    createTime: '2024-02-04 15:00',
+    merchantName: '爱宠生活馆 (三里屯店)',
+    contactPhone: '13666666666',
+    serviceTime: '2024-02-04 16:00',
+    status: 1,
+    fulfillerName: '王大力',
+    fulfillerFee: 80.00,
+    detail: {
+      packageName: '精致洗护套餐',
+      petStatus: '胆小,怕吹风机',
+      area: '浴室',
+      cleanLoc: '浴室淋浴间',
+      toolLoc: '自带工具箱',
+    },
+    timeline: [
+      { time: '2024-02-04 15:00', content: '订单创建', type: 'info' },
+      { time: '2024-02-04 15:05', content: '派单给 王大力', type: 'primary' }
+    ]
+  },
+  // 上门洗护 - 已取消
+  {
+    id: 7,
+    orderNo: 'ORD202402047701',
+    type: 'washing',
+    petName: 'Snow',
+    petBreed: '萨摩耶',
+    petAvatar: '',
+    userName: 'Zhao',
+    city: '北京市',
+    district: '朝阳区',
+    createTime: '2024-02-02 10:00',
+    merchantName: '爱宠生活馆 (国贸店)',
+    contactPhone: '13777777777',
+    serviceTime: '2024-02-03 09:00',
+    status: 5,
+    fulfillerName: '',
+    fulfillerFee: 0,
+    detail: { packageName: '洗澡+美容', petStatus: '温顺' },
+    timeline: [
+      { time: '2024-02-02 10:00', content: '订单创建', type: 'info' },
+      { time: '2024-02-02 12:00', content: '用户取消订单', type: 'warning' }
+    ]
+  },
+  // 宠物接送 - 已完成 (往返)
+  {
+    id: 8,
+    orderNo: 'ORD202402038899',
+    type: 'transport',
+    transportType: 'round',
+    petName: 'Cooper',
+    petBreed: '法斗',
+    petAvatar: '',
+    userName: '孙女士',
+    city: '北京市',
+    district: '通州区',
+    createTime: '2024-02-03 09:00',
+    merchantName: '爱宠生活馆 (三里屯店)',
+    contactPhone: '13333333333',
+    serviceTime: '2024-02-03 10:00',
+    status: 4,
+    fulfillerName: '张小美',
+    fulfillerFee: 65.00,
+    detail: {
+      pickAddr: '朝阳大悦城',
+      dropAddr: '瑞鹏宠物医院',
+    },
+    timeline: [
+      { time: '2024-02-03 09:00', content: '订单创建', type: 'info' },
+      { time: '2024-02-03 11:30', content: '服务完成', type: 'success' }
+    ]
+  },
+  // 待商家确认
+  {
+    id: 99,
+    orderNo: 'ORD202402048999',
+    type: 'feeding',
+    petName: '小黑',
+    petBreed: '拉布拉多',
+    petAvatar: '',
+    userName: '周先生',
+    city: '北京市',
+    district: '海淀区',
+    createTime: '2024-02-04 14:00',
+    merchantName: '爱宠生活馆 (三里屯店)',
+    contactPhone: '13811112222',
+    serviceTime: '2024-02-04 16:00',
+    status: 3,
+    fulfillerName: '赵铁柱',
+    fulfillerFee: 45.00,
+    detail: {
+      packageName: '上门喂遛',
+      area: '小区内部',
+    },
+    timeline: [
+      { time: '2024-02-04 14:00', content: '订单创建', type: 'info' },
+      { time: '2024-02-04 14:05', content: '赵铁柱 已接单', type: 'primary' },
+      { time: '2024-02-04 16:30', content: '履约者完成服务,等待商家确认', type: 'warning' }
+    ]
+  }
+]
+
+const fetchServiceTypes = async () => {
+  try {
+    const res = await listServiceOnOrder()
+    serviceTypeList.value = res.data || []
+  } catch (error) {
+    console.error('获取服务类型失败:', error)
+  }
+}
+
+const fetchAreaStations = async () => {
+  try {
+    const res = await listOnStore()
+    areaStationList.value = res.data || []
+  } catch (error) {
+    console.error('获取区域站点失败:', error)
+  }
+}
+
+onMounted(() => {
+  fetchServiceTypes()
+  fetchAreaStations()
+  handleSearch()
+})
+
+const handleSearch = async () => {
+  loading.value = true
+  try {
+    const params = {
+      content: filters.keyword || undefined,
+      service: filters.orderType || undefined,
+      status: filters.status || undefined,
+      pageNum: pagination.current,
+      pageSize: pagination.size
+    }
+    const res = await listSubOrderOnMerchant(params)
+    if (res) {
+      tableData.value = res.rows || []
+      pagination.total = res.total || 0
+    } else {
+      tableData.value = []
+      pagination.total = 0
+    }
+  } catch (error) {
+    console.error('获取订单列表失败:', error)
+    tableData.value = []
+    pagination.total = 0
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleSizeChange = (val) => {
+  pagination.size = val
+  handleSearch()
+}
+const handleCurrentChange = (val) => {
+  pagination.current = val
+  handleSearch()
+}
+
+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 getServiceName = (serviceId) => {
+  const service = serviceTypeList.value.find(s => s.id === serviceId)
+  return service ? service.name : '未知服务'
+}
+
+const getServiceModeTag = (row) => {
+  if (row.mode === 1 || row.mode === '1') {
+    return '往返'
+  }
+  return null
+}
+
+const getServiceOrderTypeTag = (row) => {
+  const t = row.type
+  if (t === 0 || t === '0' || t === 1 || t === '1') return null
+  if (t === 2 || t === '2') return { label: '接', type: 'primary' }
+  if (t === 3 || t === '3') return { label: '送', type: 'success' }
+  return null
+}
+
+const getCityDistrictText = (row) => {
+  if (!row.site) return { city: '-', district: '-' }
+
+  const findArea = (id) => {
+    return areaStationList.value.find(item =>
+      item.id === id || item.id === String(id) || String(item.id) === String(id)
+    )
+  }
+
+  // site是站点ID,type=2
+  const station = findArea(row.site)
+  if (!station) return { city: '-', district: '-' }
+
+  // 站点的parentId是区域,type=1
+  const district = findArea(station.parentId)
+  if (!district) return { city: station.name || '-', district: '-' }
+
+  // 区域的parentId是城市,type=0
+  const city = findArea(district.parentId)
+
+  return {
+    city: city ? city.name : '-',
+    district: district.name || '-'
+  }
+}
+
+const getStatusName = (status) => {
+  const map = { 0: '待派单', 1: '待接单', 2: '服务中', 3: '待商家确认', 4: '已完成', 5: '已取消' }
+  return map[status] || '未知'
+}
+
+const getStatusTag = (status) => {
+  const map = {
+    0: 'danger',
+    1: 'warning',
+    2: 'primary',
+    3: 'warning',
+    4: 'success',
+    5: 'info'
+  }
+  return map[status] || 'info'
+}
+
+const getStepActive = (status) => {
+  // Kept for table list if needed, but detail now uses computed steps below
+  if (status === 'pending_dispatch') return 1
+  if (status === 'pending_accept') return 2
+  if (status === 'serving') return 3
+  if (status === 'pending_confirm') return 4
+  if (status === 'completed') return 6
+  if (status === 'cancelled') return 0
+  return 0
+}
+
+const getTransportModeName = (type) => {
+  const map = { round: '往返接送', pick: '单程接(到店)', drop: '单程送(回家)' }
+  return map[type] || '接送服务'
+}
+
+// Generate dynamic steps with times for detail view
+const currentOrderSteps = computed(() => {
+  if (!currentOrder.value) return { active: 0, steps: [] }
+
+  // Base timeline nodes
+  const steps = [
+    { title: '商户下单', status: 'created', time: '' },
+    { title: '运营派单', status: 'dispatched', time: '' },
+    { title: '履约接单', status: 'accepted', time: '' },
+    { title: '服务中', status: 'serving', time: '' }, // Includes arrival/start
+    { title: '待商家确认', status: 'confirming', time: '' }, // Finished service
+    { title: '已完成', status: 'completed', time: '' }
+  ]
+
+  const logs = currentOrder.value.orderLogs || []
+  const status = currentOrder.value.status
+
+  let active = 0
+
+  // Map logs to steps time
+  // This is a simple mapper, in real app would match specific log types/codes
+  const findTime = (keyword) => {
+    const log = logs.find(l => l.title.includes(keyword) || l.content.includes(keyword))
+    return log ? log.time : ''
+  }
+
+  // 1. Created
+  steps[0].time = currentOrder.value.createTime || findTime('下单') || findTime('创建')
+  if (steps[0].time) active = 1
+
+  // 2. Dispatched
+  steps[1].time = findTime('派单') || (status !== 0 ? steps[0].time : '') // Mock if passed
+  if ([1, 2, 3, 4].includes(status)) active = 2
+
+  // 3. Accepted
+  steps[2].time = findTime('接单')
+  if (status === 1) {
+    steps[2].title = '待履约者接单'
+  } else if ([2, 3, 4].includes(status)) {
+    steps[2].title = '履约者已接单'
+    active = 3
+  }
+
+  // 4. Serving (Arrival/Start)
+  steps[3].time = findTime('到达') || findTime('出发')
+  if (status === 2) {
+    steps[3].title = '服务进行中'
+  } else if ([3, 4].includes(status)) {
+    steps[3].title = '服务已完成'
+    active = 4
+  }
+
+  // 5. Confirming
+  steps[4].time = findTime('等待商家确认') || findTime('待验收')
+  if (status === 3) {
+    steps[4].title = '待商家确认'
+  } else if (status === 4) {
+    steps[4].title = '商家已确认'
+    active = 5
+  }
+
+  // 6. Completed
+  if (status === 4) {
+    steps[5].time = findTime('完成') // or calculate based on logic
+    active = 6
+  }
+
+  if (status === 5) {
+    // Handle cancelled state simply or insert a cancelled step
+    return {
+      active: 1,
+      steps: [
+        { title: '商户下单', time: steps[0].time },
+        { title: '已取消', time: findTime('取消') || '订单已取消' }
+      ]
+    }
+  }
+
+  return { active, steps }
+})
+
+const activeDetailTab = ref('basic')
+
+const handleDetail = (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' }
+    ]
+  }
+  activeDetailTab.value = 'basic'
+  detailVisible.value = true
+}
+
+
+// --- Dispatch Dialog Logic (Copied from OrderDispatch) ---
+const ridersList = ref([
+  {
+    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.460, lat: 39.920
+  },
+  {
+    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.450, 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.470, lat: 39.930
+  },
+  {
+    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.440, lat: 39.910
+  },
+  {
+    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.480, lat: 39.925
+  }
+])
+
+const serviceProgressSteps = computed(() => {
+  const order = currentOrder.value
+  if (!order) return []
+
+  // 1. Pending / Waiting for Rider: No progress yet
+  // Strict requirement: "待接单时应该为空"
+  if ([0, 1, 5].includes(order.status)) {
+    return []
+  }
+
+  const baseTime = order.serviceTime || '2024-02-10 10:00'
+  const datePart = baseTime.split(' ')[0]
+  const isTransport = order.type === 'transport'
+
+  let steps = []
+
+  // --- Step 1: Accepted (Start for Serving+) ---
+  steps.push({
+    title: '已接单',
+    time: `${datePart} 09:30`,
+    icon: 'Bicycle',
+    color: '#ff9900', // Active color
+    desc: `履约者 ${order.fulfillerName || '当前履约者'} 已确认接单,准备前往服务地点`,
+    media: []
+  })
+
+  // --- Step 2: Arrived (Assume arrived if serving) ---
+  steps.push({
+    title: '到达打卡',
+    time: `${datePart} 09:50`,
+    icon: 'Location',
+    color: '#ff9900',
+    desc: '已到达指定位置,打卡确认',
+    media: [
+      { type: 'image', url: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg' }
+    ]
+  })
+
+  if (isTransport) {
+    // Transport Flow
+    // --- Step 3: Depart (Active in Serving) ---
+    steps.push({
+      title: '确认出发',
+      time: `${datePart} 10:10`,
+      icon: 'Van',
+      color: '#ff9900',
+      desc: '接到宠物,状态良好,开始运输。备注:宠物很乖,已放入航空箱。',
+      media: [
+        { type: 'image', url: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg' },
+        { type: 'image', url: 'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg' }
+      ]
+    })
+
+    // --- Step 4: Deliver (Only if Confirming or Completed) ---
+    if ([3, 4].includes(order.status)) {
+      steps.push({
+        title: '送达打卡',
+        time: `${datePart} 10:50`,
+        icon: 'Place',
+        color: '#ff9900',
+        desc: '宠物已安全送达目的地,等待商家验收',
+        media: [
+          { type: 'image', url: 'https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg' }
+        ]
+      })
+    }
+  } else {
+    // Service Flow
+    // --- Step 3: Start Service (Active in Serving) ---
+    steps.push({
+      title: '开始服务',
+      time: `${datePart} 10:00`,
+      icon: 'VideoPlay',
+      color: '#ff9900',
+      desc: '已确认宠物状态,开始进行服务视频录制',
+      media: [
+        { type: 'image', url: 'https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg' }
+      ]
+    })
+
+    // --- Step 4: End Service (Only if Confirming or Completed) ---
+    if ([3, 4].includes(order.status)) {
+      steps.push({
+        title: '服务结束',
+        time: `${datePart} 10:50`,
+        icon: 'VideoPause',
+        color: '#ff9900',
+        desc: '服务项目已全部完成,清理现场完毕。备注:狗狗今天很配合,完成了梳毛和喂食。',
+        media: [
+          { type: 'image', url: 'https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg' },
+          { type: 'image', url: 'https://fuss10.elemecdn.com/2/11/6535bcfb26e4c79b48ddde44f4b6fjpeg.jpeg' }
+        ]
+      })
+    }
+  }
+
+  // --- Step 5: Wait Confirm (Only if Confirming or Completed) ---
+  if ([3, 4].includes(order.status)) {
+    steps.push({
+      title: '待商家确认',
+      time: `${datePart} 10:55`,
+      icon: 'Clock',
+      color: '#ff9900',
+      desc: '履约者已提交完成信息,等待商家确认订单',
+      media: []
+    })
+  }
+
+  // --- Step 6: Completed (Only if Completed) ---
+  if (order.status === 4) {
+    steps.push({
+      title: '订单完成',
+      time: `${datePart} 11:00`,
+      icon: 'Select',
+      color: '#67C23A',
+      desc: '用户/商家已确认,服务圆满结束',
+      media: []
+    })
+  }
+
+  return steps
+})
+
+const dispatchDialogVisible = ref(false)
+const currentDispatchOrder = ref(null)
+const currentRider = ref(null)
+const dispatchSearchQuery = ref('')
+const selectedRiderId = ref(null)
+const dispatchFee = ref(0) // Default fee
+
+const openDispatchDialog = (row) => {
+  // Map table row to order card structure if needed, or use row directly if fields match
+  // OrderDispatch uses: pickAddr, dropAddr, address, typeCode, daysLater, time
+  // OrderList row has: detail.pickAddr/dropAddr, type, serviceTime (as time), etc.
+  // We need to adapt the row to 'currentDispatchOrder' structure expected by dialog template
+
+  // Construct dispatch order object
+  let orderObj = {
+    id: row.id,
+    typeCode: row.type,
+    transportType: row.splitType || row.transportType, // Add transportType
+    time: row.serviceTime,
+    status: row.status,
+    daysLater: getDaysLater(row.serviceTime), // Need helper
+    address: '', // generic address for feeding/washing
+    pickAddr: '',
+    dropAddr: '',
+    riderId: ([1, 2].includes(row.status)) ? getRiderIdByName(row.fulfillerName) : null // Mock finding rider ID
+  }
+
+  if (row.type === 'transport') {
+    orderObj.pickAddr = row.detail.pickAddr
+    orderObj.dropAddr = row.detail.dropAddr
+  } else {
+    // Feeding/Washing address isn't in main row usually, let's assume city+district or from detail if we had it
+    // row.city + row.district is available.
+    // We will just use row.city + row.district for display in card for now, or add address to mock data details.
+    // Actually, OrderList mock has detailed addresses implicit in description?
+    // Let's use City + District + "详细地址" placeholder or row.detail.address if strictly needed.
+    // For visual, let's just use City+District.
+    orderObj.address = row.city + row.district
+  }
+
+  currentDispatchOrder.value = orderObj
+
+  // Set Current Rider Logic
+  if (orderObj.riderId) {
+    currentRider.value = ridersList.value.find(r => r.id === orderObj.riderId) || null
+  } else {
+    currentRider.value = null
+  }
+
+  dispatchDialogVisible.value = true
+  dispatchSearchQuery.value = ''
+  selectedRiderId.value = null
+  dispatchFee.value = 0
+}
+
+const handleDispatchSubmit = () => {
+  if (!selectedRiderId.value) {
+    ElMessage.warning('请选择履约者')
+    return
+  }
+  if (!dispatchFee.value) {
+    ElMessage.warning('请输入服务费用')
+    return
+  }
+  dispatchDialogVisible.value = false
+  ElMessage.success('派单成功')
+
+  // Update local list
+  if (currentDispatchOrder.value) {
+    const row = tableData.value.find(r => r.id === currentDispatchOrder.value.id)
+    if (row) {
+      row.status = 1
+      const rider = ridersList.value.find(r => r.id === selectedRiderId.value)
+      row.fulfillerName = rider ? rider.name : 'Unknown'
+      row.fulfillerFee = dispatchFee.value
+    }
+  }
+}
+
+const handleExportLogs = () => {
+  const logs = currentOrder.value.orderLogs || []
+  if (logs.length === 0) {
+    ElMessage.warning('暂无日志可导出')
+    return
+  }
+
+  let csvContent = "时间,类型,标题,内容\n"
+  logs.forEach(log => {
+    const time = log.time || ''
+    const type = log.type || ''
+    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_${currentOrder.value.orderNo}.csv`
+  link.click()
+  URL.revokeObjectURL(url)
+
+  ElMessage.success('导出成功')
+}
+
+// Helpers
+const getShortType = (code) => {
+  const map = { 'transport': '接送', 'feeding': '喂遛', 'washing': '洗护' }
+  return map[code] || '订单'
+}
+const getRiderStatusText = (status) => {
+  const map = { 'online': '接单中', 'busy': '接单中', 'offline': '休息中', 'disabled': '禁用' }
+  return map[status]
+}
+const getCategoryClass = (cat) => {
+  const map = { '接送': 'cat-transport', '喂遛': 'cat-feeding', '洗护': 'cat-washing' }
+  return map[cat] || ''
+}
+
+const getServiceTimeRange = (timeStr) => {
+  if (!timeStr) return '--'
+  try {
+    // Assume format YYYY-MM-DD HH:mm
+    if (timeStr.length < 16) return timeStr
+
+    let timePart = timeStr.substring(11, 16)
+    let [hh, mm] = timePart.split(':').map(Number)
+    let endH = hh + 2 // Assume 2 hours duration
+    if (endH >= 24) endH -= 24
+
+    let endHStr = endH.toString().padStart(2, '0')
+    return `${timeStr}-${endHStr}:${mm.toString().padStart(2, '0')}`
+  } catch (e) {
+    return timeStr
+  }
+}
+// Mock helper to get days later text
+const getDaysLater = (dateStr) => {
+  // Simple mock logic
+  if (dateStr.includes('02-07')) return '今天'
+  if (dateStr.includes('02-08')) return '明天'
+  return ''
+}
+// Mock helper to find rider ID by name (since table has name only)
+const getRiderIdByName = (name) => {
+  const rider = ridersList.value.find(r => r.name === name)
+  return rider ? rider.id : null
+}
+const filteredDispatchRiders = computed(() => {
+  let result = ridersList.value.filter(r => r.status === 'online' || r.status === 'busy')
+  if (dispatchSearchQuery.value) {
+    const q = dispatchSearchQuery.value.toLowerCase()
+    result = result.filter(r => r.name.includes(q) || r.phone.includes(q))
+  }
+  result.sort((a, b) => {
+    return a.lastServiceTime.localeCompare(b.lastServiceTime)
+  })
+  return result
+})
+
+
+const handleCancel = (row) => {
+  ElMessageBox.confirm('确认取消该订单吗?', '提示', { type: 'warning' })
+    .then(() => {
+      ElMessage.success('订单已取消')
+      handleSearch()
+    })
+}
+
+const careSummaryVisible = ref(false)
+const careSummaryOrder = ref(null)
+
+const handleCareSummarySuccess = (summaryData) => {
+  if (careSummaryOrder.value) {
+    careSummaryOrder.value.careSummary = summaryData
+  }
+}
+
+const openCareSummary = (row) => {
+  // Inject rich mock data for the detailed profile view
+  careSummaryOrder.value = {
+    ...row,
+    petAge: '3岁',
+    petGender: 'male',
+    petTags: ['易过敏', '胆小'],
+    petWeight: '30 kg',
+    petSize: '大型',
+    petPersonality: '活泼,超级粘人,喜欢玩球',
+    homeTime: '2023-01-01',
+    houseType: '电梯',
+    entryMethod: '密码开门',
+    entryDetail: '密码: 123456 (仅限服务期间使用)',
+    healthStatus: '健康',
+    aggression: '无',
+    vaccineImg: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg',
+    medicalHistory: '无',
+    allergy: '海鲜'
+  }
+  careSummaryVisible.value = true
+}
+
+// Reward / Punish Logic
+const rewardDialogVisible = ref(false)
+const remarkDialogVisible = ref(false)
+const currentOperateRow = ref(null)
+
+const openRewardDialog = (row) => {
+  currentOperateRow.value = row
+  rewardDialogVisible.value = true
+}
+
+const openRemarkDialog = (row) => {
+  currentOperateRow.value = row
+  remarkDialogVisible.value = true
+}
+
+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(() => {
+        row.status = 4
+        ElMessage.success('订单已标记完成')
+      })
+  }
+
+  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;
+}
+
+/* Changed to row */
+.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;
+  margin-left: 4px;
+}
+
+.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.status-0 {
+  background-color: #f56c6c;
+  box-shadow: 0 0 4px rgba(245, 108, 108, 0.4);
+}
+
+.status-dot.status-1 {
+  background-color: #e6a23c;
+}
+
+.status-dot.status-2 {
+  background-color: #409eff;
+}
+
+.status-dot.status-3 {
+  background-color: #bf24e8;
+}
+
+.status-dot.status-4 {
+  background-color: #67c23a;
+}
+
+.status-dot.status-5 {
+  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;
+}
+
+/* Added flex for operation column */
+.el-dropdown-link {
+  cursor: pointer;
+  color: #409eff;
+  font-size: 12px;
+  display: flex;
+  align-items: center;
+  line-height: 1;
+  height: 24px;
+}
+
+/* Ensure alignment */
+
+/* Detail Styles */
+.detail-content {
+  padding: 0 10px;
+}
+
+.order-detail-drawer .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;
+}
+
+.crt-time {
+  font-size: 13px;
+  color: #909399;
+}
+
+.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;
+}
+
+.vip-badge {
+  background: linear-gradient(90deg, #f3d19e, #e6a23c);
+  color: #fff;
+  font-size: 10px;
+  padding: 0 5px;
+  border-radius: 10px;
+  height: 16px;
+  line-height: 16px;
+  font-weight: bold;
+}
+
+.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;
+}
+
+.stat-row {
+  font-size: 12px;
+  color: #909399;
+  display: flex;
+  gap: 10px;
+}
+
+/* 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;
+}
+
+.order-desc :deep(.el-descriptions__label) {
+  width: 90px;
+  font-weight: bold;
+  color: #606266;
+}
+
+/* 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;
+}
+
+.f-row3 {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.score-text {
+  font-weight: bold;
+  color: #ff9900;
+}
+
+.empty-state {
+  padding: 40px 0;
+  text-align: center;
+}
+
+/* Service Block */
+.service-block {
+  margin-bottom: 25px;
+}
+
+.block-title {
+  font-weight: bold;
+  font-size: 15px;
+  margin-bottom: 15px;
+  padding-left: 8px;
+  border-left: 4px solid #409eff;
+}
+
+/* New Dispatch Styles */
+/* Dispatch Dialog Styles from OrderDispatch */
+.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-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;
+}
+
+.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;
+}
+
+
+/* Removed Enhanced Care Summary Styles */
+
+/* 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;
+}
+
+/* Route Graph */
+.route-graph {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+  flex-wrap: wrap;
+  background: #f9f9f9;
+  padding: 20px;
+  border-radius: 6px;
+}
+
+.route-node {
+  display: flex;
+  gap: 10px;
+  align-items: flex-start;
+  background: #fff;
+  padding: 12px;
+  border-radius: 6px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
+  flex: 1;
+  min-width: 200px;
+}
+
+.node-icon {
+  width: 30px;
+  height: 30px;
+  border-radius: 50%;
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 14px;
+  font-weight: bold;
+  flex-shrink: 0;
+}
+
+.node-icon.pick {
+  background: #409eff;
+}
+
+.node-icon.drop {
+  background: #67c23a;
+}
+
+/* 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;
+}
+
+.node-icon.shop {
+  background: #e6a23c;
+  font-size: 16px;
+}
+
+.node-content {
+  display: flex;
+  flex-direction: column;
+  line-height: 1.4;
+  flex: 1;
+}
+
+.addr-t {
+  font-weight: bold;
+  font-size: 14px;
+  color: #303133;
+  margin-bottom: 4px;
+}
+
+.contact-t {
+  font-size: 12px;
+  color: #909399;
+}
+
+.time-t {
+  font-size: 12px;
+  color: #f56c6c;
+  margin-top: 4px;
+  font-weight: 500;
+}
+
+.route-arrow-lg {
+  color: #c0c4cc;
+  font-size: 20px;
+}
+
+/* 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>

+ 170 - 179
src/views/order/management/components/CareSummaryDrawer.vue

@@ -1,186 +1,177 @@
 <template>
-  <el-drawer
-    :model-value="visible"
-    @update:model-value="updateVisible"
-    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>
+    <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">
+                               <el-tag :type="order.petGender==='male'?'':'danger'" effect="light" round>
+                                   <el-icon><component :is="order.petGender==='male'?'Male':'Female'" /></el-icon>
+                                   {{ 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="房屋类型">{{ order.houseType }}</el-descriptions-item>
+                      <el-descriptions-item label="入户方式" :span="2">
+                          <span style="font-weight:bold;">{{ order.entryMethod }}</span> 
+                          <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>
-        <div class="pet-summary-info">
-          <div class="summary-name-row">
-            <span class="name">{{ order.petName }}</span>
-            <div class="tags-group">
-              <el-tag :type="order.petGender==='male'?'':'danger'" effect="light" round>
-                <el-icon><component :is="order.petGender==='male'?'Male':'Female'" /></el-icon>
-                {{ 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="房屋类型">{{ order.houseType }}</el-descriptions-item>
-          <el-descriptions-item label="入户方式" :span="2">
-            <span style="font-weight:bold;">{{ order.entryMethod }}</span>
-            <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="updateVisible(false)">关闭</el-button>
-        </div>
-      </div>
-    </div>
-  </el-drawer>
+    </el-drawer>
 </template>
 
 <script setup>
-import { ref, watch } from 'vue'
-import { ElMessage } from 'element-plus'
+import { ref, computed, watch } from 'vue'
 
 const props = defineProps({
-  visible: Boolean,
-  order: Object
+    visible: Boolean,
+    order: Object
 })
+const emit = defineEmits(['update:visible', 'submit'])
 
-const emit = defineEmits(['update:visible', 'success'])
+const drawerVisible = computed({
+    get: () => props.visible,
+    set: (val) => emit('update:visible', val)
+})
 
-const isEditingSummary = ref(false)
 const careSummaryText = ref('')
+const isEditingSummary = ref(false)
 
 watch(() => props.visible, (val) => {
-  if (val && props.order) {
-    isEditingSummary.value = false
-    if (!props.order.careSummary) {
-      careSummaryText.value = `1. 精神/身体状态:${props.order.petName}精神状态良好,愿意互动。
+    if (val && props.order) {
+        isEditingSummary.value = false
+        if (!props.order.careSummary) {
+            careSummaryText.value = `1. 精神/身体状态:${props.order.petName}精神状态良好,愿意互动。
 2. 进食/饮水:食欲正常,饮水适当,已清洗碗具。
 3. 排泄情况:排便正常,颜色形状正常,已清理。
 4. 卫生情况:猫砂盆/地面已清理干净,无异味。
 5. 互动情况:陪玩了20分钟,${props.order.petName}很开心。
 6. 特殊情况/备注:无特殊异常。`
-    } else {
-      careSummaryText.value = props.order.careSummary
+        } else {
+            careSummaryText.value = props.order.careSummary
+        }
     }
-  }
 })
 
-const updateVisible = (val) => {
-  emit('update:visible', val)
-}
-
 const saveCareSummary = () => {
-  if (props.order) {
-    props.order.careSummary = careSummaryText.value
-    if (!props.order.summaryTime) {
-      props.order.summaryTime = '2024-02-04 17:00'
-    }
-    ElMessage.success('护理小结已保存')
+    emit('submit', careSummaryText.value)
     isEditingSummary.value = false
-    emit('success', careSummaryText.value)
-  }
 }
 </script>
 
@@ -208,15 +199,15 @@ const saveCareSummary = () => {
 
 /* 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 { 
+    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 { 
+    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; }
@@ -228,25 +219,25 @@ const saveCareSummary = () => {
 
 /* 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 { 
+    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;
+    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;
+.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;
+.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; }

+ 0 - 0
template/orderList/components/DispatchDialog.vue → src/views/order/management/components/DispatchDialog.vue


+ 0 - 0
template/orderList/components/OrderDetailDrawer.vue → src/views/order/management/components/OrderDetailDrawer.vue


+ 30 - 35
src/views/order/management/components/RemarkDialog.vue

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

+ 74 - 76
src/views/order/management/components/RewardDialog.vue

@@ -1,101 +1,99 @@
 <template>
-  <el-dialog :model-value="visible" @update:model-value="updateVisible" title="奖惩操作" width="500px">
-    <div v-if="data" 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;">{{ data.fulfillerName || '未指派' }}</span></div>
-        <div style="font-size: 13px; margin-top: 4px;">订单号:{{ data.orderNo }}</div>
-        <div style="font-size: 13px; margin-top: 4px; display:flex; align-items:center; gap:6px;">
-          服务类型:
-          <el-tag :type="getTypeTag(data.type)" size="small">{{ getTypeName(data.type) }}</el-tag>
-          <el-tag v-if="data.type === 'transport' && data.transportType === 'round'" size="small" effect="plain" type="warning">往返</el-tag>
-          <el-tag v-if="data.splitType === 'pick'" size="small" effect="dark" color="#409eff" style="border:none; color:white;">接</el-tag>
-          <el-tag v-if="data.splitType === 'drop'" size="small" effect="dark" color="#67c23a" style="border:none; color:white;">送</el-tag>
-          <el-tag v-if="data.type === 'transport' && data.transportType === 'pick' && !data.splitType" size="small" effect="plain">单程接</el-tag>
-          <el-tag v-if="data.type === 'transport' && data.transportType === 'drop' && !data.splitType" size="small" effect="plain" type="success">单程送</el-tag>
-        </div>
-      </div>
+    <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="amount">金额 (元)</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="updateVisible(false)">取消</el-button>
-      <el-button type="primary" @click="handleSubmit">确认执行</el-button>
-    </template>
-  </el-dialog>
+            <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, watch } from 'vue'
+import { reactive, computed, watch } from 'vue'
 import { ElMessage } from 'element-plus'
 
 const props = defineProps({
-  visible: Boolean,
-  data: Object
+    visible: Boolean,
+    order: Object
 })
+const emit = defineEmits(['update:visible', 'submit'])
 
-const emit = defineEmits(['update:visible', 'success'])
+const dialogVisible = computed({
+    get: () => props.visible,
+    set: (val) => emit('update:visible', val)
+})
 
 const rewardForm = reactive({
-  type: 'reward',
-  item: 'points',
-  value: 10,
-  reason: ''
+    type: 'reward',
+    item: 'points',
+    value: 10,
+    reason: ''
 })
 
 watch(() => props.visible, (val) => {
-  if (val) {
-    rewardForm.type = 'reward'
-    rewardForm.item = 'points'
-    rewardForm.value = 10
-    rewardForm.reason = ''
-  }
+    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 map = { transport: '', feeding: 'warning', washing: 'success' }
+    return map[type]
 }
-
 const getTypeName = (type) => {
-  const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' }
-  return map[type]
-}
-
-const updateVisible = (val) => {
-  emit('update:visible', val)
+    const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' }
+    return map[type]
 }
 
 const handleSubmit = () => {
-  if (!rewardForm.reason) {
-    ElMessage.warning('请输入奖惩原因')
-    return
-  }
-  ElMessage.success(`操作成功:${rewardForm.type === 'reward' ? '奖励' : '惩罚'}已执行`)
-  updateVisible(false)
-  emit('success')
+    if(!rewardForm.reason) {
+        ElMessage.warning('请输入奖惩原因')
+        return
+    }
+    emit('submit', rewardForm)
+    dialogVisible.value = false
 }
 </script>

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


+ 232 - 0
src/views/order/purchase-bak/components/AddPetDialog.vue

@@ -0,0 +1,232 @@
+<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-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-tab-pane>
+      <el-tab-pane label="家庭信息" name="family">
+        <el-form :model="petForm" label-width="120px">
+          <el-form-item label="新来家庭时间">
+            <el-date-picker v-model="petForm.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>
+          </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>
+          </el-form-item>
+          <el-form-item label="密码" v-if="petForm.entryMethod === 'password'">
+            <el-input v-model="petForm.entryPassword" placeholder="请输入门锁密码" />
+          </el-form-item>
+          <el-form-item label="钥匙位置" v-if="petForm.entryMethod === 'key'">
+            <el-input v-model="petForm.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-item label="健康状态">
+            <el-radio-group v-model="petForm.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-form-item>
+          <el-form-item label="疫苗情况">
+            <el-input v-model="petForm.vaccine" type="textarea" placeholder="记录疫苗接种情况" />
+          </el-form-item>
+          <el-form-item label="既往病史">
+            <el-input v-model="petForm.medicalHistory" type="textarea" placeholder="如有病史请记录" />
+          </el-form-item>
+          <el-form-item label="过敏史">
+            <el-input v-model="petForm.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>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { ref, reactive, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+
+const props = defineProps({
+  visible: { type: Boolean, default: false },
+  userId: { type: [String, Number], default: '' },
+  userOptions: { type: Array, default: () => [] }
+})
+
+const emit = defineEmits(['update:visible', 'success'])
+
+const activePetTab = ref('basic')
+
+const petForm = reactive({
+  name: '', breed: '', gender: 'MM', avatar: '',
+  bodyType: 'small', weight: 0, age: 0, keywords: '', desc: '', tags: [],
+
+  // Family
+  arrivalTime: '', houseType: 'stairs', entryMethod: 'key', entryPassword: '', keyLocation: '',
+  // Health
+  healthStatus: '健康', aggression: false, vaccine: '', medicalHistory: '', allergies: ''
+})
+
+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: ''
+    })
+  }
+})
+
+const handleAvatarChange = (uploadFile) => {
+  // Mock upload: create local URL
+  petForm.avatar = URL.createObjectURL(uploadFile.raw)
+}
+
+const submit = () => {
+  if (!petForm.name || !petForm.breed) {
+    ElMessage.warning('请补全宠物必填信息')
+    return
+  }
+  const newPet = {
+    id: Date.now(),
+    name: petForm.name,
+    breed: petForm.breed,
+    avatar: petForm.avatar
+  }
+  emit('success', newPet)
+  emit('update:visible', false)
+}
+</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;
+}
+.avatar { width: 100px; height: 100px; display: block; border-radius: 50%; object-fit: cover; }
+.inner-form { flex: 1; }
+</style>

+ 164 - 0
src/views/order/purchase-bak/components/AddUserDialog.vue

@@ -0,0 +1,164 @@
+<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>
+
+      <div class="form-section-header">基本资料</div>
+      <el-row :gutter="30">
+        <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>
+          </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-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-col>
+        <el-col :span="12">
+          <el-form-item label="电话" required><el-input v-model="userForm.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-form-item>
+        </el-col>
+      </el-row>
+
+      <div class="form-section-header">居住信息</div>
+      <el-row :gutter="30">
+        <el-col :span="24">
+          <el-form-item label="所在地区">
+            <el-cascader v-model="userForm.region" :options="pcaOptions" placeholder="请选择省/市/区" style="width: 100%" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="详细住址"><el-input v-model="userForm.detailAddress" 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>
+          </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>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12" v-if="userForm.entryMethod === 'password'">
+          <el-form-item label="开门密码">
+            <el-input v-model="userForm.entryPassword" placeholder="请输入密码" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12" v-if="userForm.entryMethod === 'key'">
+          <el-form-item label="钥匙位置">
+            <el-input v-model="userForm.keyLocation" placeholder="如:地毯下" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <div class="form-section-header">其他</div>
+      <el-row :gutter="30">
+        <el-col :span="24">
+          <el-form-item label="用户标签">
+            <el-select v-model="userSelectedTagIds" 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-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-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>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { reactive, ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+
+const props = defineProps({
+  visible: { type: Boolean, default: false },
+  pcaOptions: { type: Array, default: () => [] }
+})
+
+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: ''
+})
+
+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: ''
+    })
+  }
+})
+
+const handleUserAvatarChange = (uploadFile) => {
+  userForm.avatar = URL.createObjectURL(uploadFile.raw)
+}
+
+const submit = () => {
+  if (!userForm.name || !userForm.phone) {
+    ElMessage.warning('请补全用户必填信息')
+    return
+  }
+  const newUser = {
+    id: Date.now(),
+    name: userForm.name,
+    phone: userForm.phone
+  }
+  emit('success', newUser)
+  emit('update:visible', false)
+}
+</script>
+
+<style scoped>
+.form-section-header { font-weight: bold; margin-bottom: 20px; font-size: 15px; color: #303133; }
+</style>

+ 86 - 0
src/views/order/purchase-bak/components/FeedingForm.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="business-form">
+    <div style="margin-bottom: 20px;">
+      <div class="section-label">上门服务地址</div>
+      <el-row :gutter="10">
+        <el-col :span="8">
+          <el-cascader v-model="feedingData.region" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+        </el-col>
+        <el-col :span="16">
+          <el-input v-model="feedingData.addressDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
+        </el-col>
+      </el-row>
+    </div>
+
+    <div style="margin-bottom: 20px;">
+      <div class="section-label" style="display:flex; align-items:center; margin-bottom:10px;">
+        预约服务时间
+        <el-tag type="info" size="small" style="margin-left:10px;">共 {{ feedingData.appointments.length }} 次</el-tag>
+      </div>
+      <div v-for="(item, index) in feedingData.appointments" :key="index" style="display:flex; align-items:center; margin-bottom:10px;">
+        <span style="width:30px; color:#999; font-size:12px; font-weight:bold;">{{ index + 1 }}.</span>
+        <el-date-picker
+          v-model="item.startTime"
+          type="datetime"
+          placeholder="开始时间"
+          style="width: 200px; margin-right: 5px;"
+          format="YYYY-MM-DD HH:mm"
+        />
+        <span style="margin:0 5px; color:#999;">~</span>
+        <el-date-picker
+          v-model="item.endTime"
+          type="datetime"
+          placeholder="结束时间 (可选)"
+          style="width: 200px; margin-right: 15px;"
+          format="YYYY-MM-DD HH:mm"
+        />
+
+        <div style="display:flex; gap:8px; margin-left:5px;">
+          <el-button v-if="index === feedingData.appointments.length - 1" type="primary" circle size="small" icon="Plus" @click="addAppointment" />
+          <el-button v-if="feedingData.appointments.length > 1" type="danger" circle size="small" icon="Minus" @click="removeAppointment(index)" plain />
+        </div>
+      </div>
+    </div>
+
+    <div class="remark-section">
+      <div class="section-label">家庭服务及宠物档案备注</div>
+      <el-row :gutter="15">
+        <el-col :span="12"><el-input v-model="feedingData.area" placeholder="宠物活动区域" /></el-col>
+        <el-col :span="12"><el-input v-model="feedingData.itemLoc" placeholder="物品存放位置" /></el-col>
+        <el-col :span="12" style="margin-top:10px"><el-input v-model="feedingData.cleanLoc" placeholder="清洗位置" /></el-col>
+        <el-col :span="12" style="margin-top:10px"><el-input v-model="feedingData.foodAmount" placeholder="喂食量标准" /></el-col>
+        <el-col :span="24" style="margin-top:10px"><el-input v-model="feedingData.other" type="textarea" :rows="2" placeholder="其他注意事项" /></el-col>
+      </el-row>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { defineProps, defineEmits } from 'vue'
+
+const props = defineProps({
+  feedingData: { type: Object, required: true },
+  pcaOptions: { type: Array, default: () => [] }
+})
+
+const emit = defineEmits(['change'])
+
+const addAppointment = () => {
+  props.feedingData.appointments.push({ startTime: '', endTime: '' })
+  props.feedingData.count = props.feedingData.appointments.length
+  emit('change', 'feeding')
+}
+
+const removeAppointment = (index) => {
+  if (props.feedingData.appointments.length <= 1) return
+  props.feedingData.appointments.splice(index, 1)
+  props.feedingData.count = props.feedingData.appointments.length
+  emit('change', 'feeding')
+}
+</script>
+
+<style scoped>
+.business-form { padding-top: 5px; }
+.remark-section { background: #fdfdfd; border: 1px dashed #dcdfe6; padding: 15px; border-radius: 6px; margin-top: 20px; }
+.section-label { font-size: 13px; font-weight: bold; color: #606266; margin-bottom: 12px; }
+</style>

+ 95 - 0
src/views/order/purchase-bak/components/TransportForm.vue

@@ -0,0 +1,95 @@
+<template>
+  <div class="business-form">
+    <el-form-item label="接送模式">
+      <el-radio-group v-model="transportData.subType" size="large" @change="$emit('change', 'transport')">
+        <el-radio-button label="round">往返接送</el-radio-button>
+        <el-radio-button label="pick">单程接 (到店)</el-radio-button>
+        <el-radio-button label="drop">单程送 (回家)</el-radio-button>
+      </el-radio-group>
+    </el-form-item>
+
+    <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">
+            <el-col :span="8">
+              <el-cascader v-model="transportData.pickRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+            </el-col>
+            <el-col :span="16">
+              <el-input v-model="transportData.pickDetail" 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 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">
+            <el-col :span="8">
+              <el-cascader v-model="transportData.dropRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+            </el-col>
+            <el-col :span="16">
+              <el-input v-model="transportData.dropDetail" 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>
+</template>
+
+<script setup>
+import { defineProps, defineEmits } from 'vue'
+
+const props = defineProps({
+  transportData: { type: Object, required: true },
+  pcaOptions: { type: Array, default: () => [] }
+})
+
+const emit = defineEmits(['change'])
+</script>
+
+<style scoped>
+.business-form { padding-top: 5px; }
+.route-box { 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;
+  text-align: center; line-height: 32px; font-weight: bold; flex-shrink: 0;
+}
+.seg-badge.end { background: #67c23a; }
+.seg-content { flex: 1; display: flex; flex-direction: column; gap: 10px; }
+
+.route-connector { display: flex; align-items: center; justify-content: center; margin: 15px 0; gap: 10px; color: #909399; font-size: 12px; }
+.route-connector .line { height: 1px; width: 80px; background: #dcdfe6; }
+.route-connector .store-node { background: white; padding: 4px 12px; border-radius: 20px; border: 1px solid #dcdfe6; display: flex; align-items: center; gap: 5px; }
+</style>

+ 90 - 0
src/views/order/purchase-bak/components/WashingForm.vue

@@ -0,0 +1,90 @@
+<template>
+  <div class="business-form">
+    <div style="margin-bottom: 20px;">
+      <div class="section-label">上门服务地址</div>
+      <el-row :gutter="10">
+        <el-col :span="8">
+          <el-cascader v-model="washingData.region" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
+        </el-col>
+        <el-col :span="16">
+          <el-input v-model="washingData.addressDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
+        </el-col>
+      </el-row>
+    </div>
+
+    <div style="margin-bottom: 20px;">
+      <div class="section-label" style="display:flex; align-items:center; margin-bottom:10px;">
+        预约服务时间
+        <el-tag type="info" size="small" style="margin-left:10px;">共 {{ washingData.appointments.length }} 次</el-tag>
+      </div>
+      <div v-for="(item, index) in washingData.appointments" :key="index" style="display:flex; align-items:center; margin-bottom:10px;">
+        <span style="width:30px; color:#999; font-size:12px; font-weight:bold;">{{ index + 1 }}.</span>
+        <el-date-picker
+          v-model="item.startTime"
+          type="datetime"
+          placeholder="开始时间"
+          style="width: 200px; margin-right: 5px;"
+          format="YYYY-MM-DD HH:mm"
+        />
+        <span style="margin:0 5px; color:#999;">~</span>
+        <el-date-picker
+          v-model="item.endTime"
+          type="datetime"
+          placeholder="结束时间 (可选)"
+          style="width: 200px; margin-right: 15px;"
+          format="YYYY-MM-DD HH:mm"
+        />
+
+        <div style="display:flex; gap:8px; margin-left:5px;">
+          <el-button v-if="index === washingData.appointments.length - 1" type="primary" circle size="small" icon="Plus" @click="addAppointment" />
+          <el-button v-if="washingData.appointments.length > 1" type="danger" circle size="small" icon="Minus" @click="removeAppointment(index)" plain />
+        </div>
+      </div>
+    </div>
+
+    <div class="remark-section">
+      <div class="section-label">服务备注及宠物状态</div>
+      <el-row :gutter="15">
+        <el-col :span="8">
+          <el-select v-model="washingData.petStatus" placeholder="宠物应激状态" style="width:100%">
+            <el-option label="性格温顺" value="calm" />
+            <el-option label="胆小怕人" value="shy" />
+            <el-option label="容易应激" value="stress" />
+            <el-option label="有攻击性" value="aggressive" />
+          </el-select>
+        </el-col>
+        <el-col :span="8"><el-input v-model="washingData.cleanLoc" placeholder="清洗位置" /></el-col>
+        <el-col :span="8"><el-input v-model="washingData.toolLoc" placeholder="工具/水源位置" /></el-col>
+        <el-col :span="24" style="margin-top:10px"><el-input v-model="washingData.other" type="textarea" :rows="2" placeholder="其他注意事项" /></el-col>
+      </el-row>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { defineProps, defineEmits } from 'vue'
+
+const props = defineProps({
+  washingData: { type: Object, required: true },
+  pcaOptions: { type: Array, default: () => [] }
+})
+
+const emit = defineEmits(['change'])
+
+const addAppointment = () => {
+  props.washingData.appointments.push({ startTime: '', endTime: '' })
+  emit('change', 'washing')
+}
+
+const removeAppointment = (index) => {
+  if (props.washingData.appointments.length <= 1) return
+  props.washingData.appointments.splice(index, 1)
+  emit('change', 'washing')
+}
+</script>
+
+<style scoped>
+.business-form { padding-top: 5px; }
+.remark-section { background: #fdfdfd; border: 1px dashed #dcdfe6; padding: 15px; border-radius: 6px; margin-top: 20px; }
+.section-label { font-size: 13px; font-weight: bold; color: #606266; margin-bottom: 12px; }
+</style>

+ 562 - 0
src/views/order/purchase-bak/index.vue

@@ -0,0 +1,562 @@
+<template>
+  <div class="page-container">
+    <div class="create-layout">
+
+      <!-- 左侧:下单填写区 -->
+      <div class="form-container">
+        <!-- 1. 服务类型选择 -->
+        <div class="type-selection">
+          <div
+            v-for="item in serviceList"
+            :key="item.type"
+            class="type-card"
+            :class="[item.type, { active: form.type === item.type }]"
+            @click="handleTypeChange(item.type)"
+          >
+            <div class="icon-box"><el-icon><component :is="item.icon" /></el-icon></div>
+            <div class="text">
+              <div class="type-name">{{ item.name }}</div>
+              <div class="type-desc">{{ item.desc }}</div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 2. 基础信息:门店与宠主 -->
+        <el-card shadow="never" class="section-card">
+          <template #header>
+            <div class="card-title">
+              <span class="step-num">02</span> 基础信息
+            </div>
+          </template>
+          <div class="card-body">
+            <el-form label-position="top" class="base-form">
+              <el-row :gutter="20">
+                <el-col :span="12">
+                  <el-form-item>
+                    <template #label>
+                      <div style="display:flex; align-items:center; height: 24px;">
+                        <span>服务门店 (平台代下单)</span>
+                      </div>
+                    </template>
+                    <el-select v-model="form.merchantId" placeholder="请选择商户门店" size="large" style="width: 100%" filterable>
+                      <el-option v-for="m in merchants" :key="m.id" :label="m.name" :value="m.id" />
+                    </el-select>
+                  </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;">
+                        <span>宠主用户</span>
+                        <el-button type="primary" plain size="small" @click="openAddUser" icon="Plus" style="margin-left: 15px;">添加用户</el-button>
+                      </div>
+                    </template>
+                    <el-select
+                      v-model="form.userId"
+                      placeholder="搜索手机号/姓名"
+                      size="large"
+                      style="width: 100%"
+                      filterable
+                      remote
+                      :remote-method="searchUser"
+                      :loading="userLoading"
+                      @change="handleUserChange"
+                    >
+                      <el-option v-for="u in userOptions" :key="u.id" :label="u.name + ' - ' + u.phone" :value="u.id" />
+                    </el-select>
+                  </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 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>
+
+                  <!-- Add Button Card (Last Item in Grid) -->
+                  <div class="pet-card add-card" @click="openAddPet">
+                    <el-icon :size="24"><Plus /></el-icon>
+                    <span style="font-size: 15px; font-weight: bold;">新增宠物</span>
+                  </div>
+                </div>
+              </el-form-item>
+            </el-form>
+          </div>
+        </el-card>
+
+        <!-- 3. 业务详情表单 -->
+        <el-card shadow="never" class="section-card form-card" v-if="form.type">
+          <template #header>
+            <div class="card-title">
+              <span class="step-num">03</span>
+              {{ getStepTitle(form.type) }}
+            </div>
+          </template>
+
+          <div class="card-body">
+            <!-- 服务套餐信息 -->
+            <el-form-item label="团购套餐">
+              <el-input v-model="form.groupBuyPackage" placeholder="请输入团购套餐名称 (选填)" clearable />
+            </el-form-item>
+
+            <div class="divider"></div>
+
+            <!-- A. 宠物接送表单 -->
+            <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" />
+
+            <!-- C. 上门洗护表单 -->
+            <WashingForm v-show="form.type === 'washing'" :washing-data="form.washing" :pca-options="pcaOptions" @change="calcPrice" />
+
+          </div>
+        </el-card>
+      </div>
+
+      <!-- 右侧:收银台概览 -->
+      <div class="summary-sidebar">
+        <div class="summary-panel">
+          <div class="summary-header">订单概览</div>
+
+          <div class="summary-content">
+            <div class="row" v-if="selectedMerchantName">
+              <span class="label">服务门店</span>
+              <span class="value">{{ selectedMerchantName }}</span>
+            </div>
+            <div class="row" v-if="selectedUserName">
+              <span class="label">客户</span>
+              <span class="value">{{ selectedUserName }}</span>
+            </div>
+            <div class="row" v-if="selectedPetName">
+              <span class="label">服务对象</span>
+              <span class="value action-text">{{ selectedPetName }} ({{ selectedPetBreed }})</span>
+            </div>
+            <div class="divider"></div>
+
+            <div class="service-preview" v-if="form.type">
+              <div class="preview-title">{{ getTypeName(form.type) }}</div>
+
+              <!-- 套餐显示 -->
+              <div class="preview-detail" v-if="selectedPkgName">
+                <div style="font-weight:bold; color:#409eff">{{ selectedPkgName }}</div>
+              </div>
+              <div class="preview-detail" v-else>
+                <div style="color:#e6a23c">非服务套餐 (单次)</div>
+              </div>
+
+              <!-- 接送预览 -->
+              <div v-if="form.type === 'transport'" class="preview-detail">
+                <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>
+            </div>
+
+          </div>
+
+          <div class="summary-footer">
+            <el-button type="primary" size="large" class="submit-btn" :disabled="!canSubmit" @click="handleSubmit">
+              立即下单
+            </el-button>
+          </div>
+        </div>
+      </div>
+
+    </div>
+
+    <!-- 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" />
+
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import TransportForm from './components/TransportForm.vue'
+import FeedingForm from './components/FeedingForm.vue'
+import WashingForm from './components/WashingForm.vue'
+import AddUserDialog from './components/AddUserDialog.vue'
+import AddPetDialog from './components/AddPetDialog.vue'
+
+// --- Mock Data ---
+const merchants = ref([
+  { id: 1, name: '萌它宠物三里屯店' },
+  { id: 2, name: '宠爱国际动物医院' }
+])
+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' }
+  ]
+}
+
+const serviceList = [
+  { type: 'transport', name: '宠物接送', icon: 'Van', desc: '专车接送 · 全程监护', basePrice: 35 },
+  { type: 'feeding', name: '上门喂遛', icon: 'Food', desc: '喂食添水 · 陪玩遛狗', basePrice: 68 },
+  { type: 'washing', name: '上门洗护', icon: 'Soap', desc: '专业设备 · 深度清洁', basePrice: 88 }
+]
+
+const allPackages = [
+  { id: 10, type: 'transport', name: '包月接送套餐', price: 0 },
+  { id: 11, type: 'feeding', name: '基础喂猫套餐', price: 0 },
+  { id: 12, type: 'feeding', name: '深度陪玩套餐', price: 0 },
+  { id: 13, type: 'washing', name: '精致洗护+美容', price: 0 },
+  { id: 14, type: 'washing', name: '除菌药浴套餐', price: 0 },
+]
+
+// --- State ---
+const userLoading = ref(false)
+const currentPets = ref([])
+
+const form = reactive({
+  merchantId: '',
+  userId: '',
+  petId: '',
+  type: 'transport',
+  groupBuyPackage: '',
+
+  // Sub Forms Data
+  transport: {
+    pkgId: '',
+    price: 0,
+    pickPrice: 35,
+    dropPrice: 35,
+    subType: 'round',
+    pickRegion: [], pickDetail: '', pickContact: '', pickPhone: '', pickTime: '',
+    dropRegion: [], dropDetail: '', dropContact: '', dropPhone: '', dropTime: ''
+  },
+  feeding: {
+    pkgId: '', price: 68,
+    appointments: [{ startTime: '', endTime: '' }],
+    region: [], addressDetail: '',
+    count: 1, dates: [], area: '', itemLoc: '', cleanLoc: '', foodAmount: '', other: ''
+  },
+  washing: {
+    pkgId: '', price: 88,
+    appointments: [{ startTime: '', endTime: '' }],
+    region: [], addressDetail: '',
+    time: '', petStatus: '', cleanLoc: '', toolLoc: '', other: ''
+  }
+})
+
+// Address Autofill Watcher
+watch(() => form.petId, (newId) => {
+  if (!newId) return
+  const pet = currentPets.value.find(p => p.id === newId)
+  if (!pet) return
+
+  const user = userOptions.value.find(u => u.id === form.userId)
+
+  // Fill Transport
+  form.transport.pickRegion = pet.region || []
+  form.transport.pickDetail = pet.address || ''
+  form.transport.pickContact = user?.name || ''
+  form.transport.pickPhone = user?.phone || ''
+
+  form.transport.dropRegion = pet.region || []
+  form.transport.dropDetail = pet.address || ''
+  form.transport.dropContact = user?.name || ''
+  form.transport.dropPhone = user?.phone || ''
+
+  // Fill Feeding
+  form.feeding.region = pet.region || []
+  form.feeding.addressDetail = pet.address || ''
+
+  // Fill Washing
+  form.washing.region = pet.region || []
+  form.washing.addressDetail = pet.address || ''
+})
+
+// Current Active Data Helper
+const activeData = computed(() => {
+  return form[form.type]
+})
+
+// --- Logic ---
+
+const handleTypeChange = (type) => {
+  form.type = type
+  calcPrice(type)
+}
+
+const currentPackages = computed(() => {
+  return allPackages.filter(p => p.type === form.type)
+})
+
+const handlePkgSelect = (id) => {
+  activeData.value.pkgId = id
+  // Price calculation should remain same (base price), just payable changes
+  calcPrice(form.type)
+}
+
+const calcPrice = (type) => {
+  const data = form[type]
+  const base = serviceList.find(s => s.type === type)?.basePrice || 0
+
+  // Always use Base Logic for "Order Value", regardless of package
+  if (type === 'transport') {
+    if(data.subType === 'round') {
+      data.pickPrice = base
+      data.dropPrice = base
+    } else if (data.subType === 'pick') {
+      data.pickPrice = base
+      data.dropPrice = 0
+    } else if (data.subType === 'drop') {
+      data.pickPrice = 0
+      data.dropPrice = base
+    }
+  } else if (type === 'feeding') {
+    data.price = base * data.count
+  } else if (type === 'washing') {
+    data.price = base
+  }
+}
+
+// Add User Logic
+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('用户添加成功并已选中')
+}
+
+const pcaOptions = [
+  {
+    value: '北京市', label: '北京市',
+    children: [
+      { value: '市辖区', label: '市辖区', children: [ { value: '朝阳区', label: '朝阳区' }, { value: '海淀区', label: '海淀区' } ] }
+    ]
+  },
+  {
+    value: '上海市', label: '上海市',
+    children: [
+      { value: '市辖区', label: '市辖区', children: [ { value: '浦东新区', label: '浦东新区' }, { value: '徐汇区', label: '徐汇区' } ] }
+    ]
+  }
+]
+
+// Add Pet Logic
+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('宠物添加成功')
+}
+
+
+// --- Computed Helpers ---
+const selectedMerchantName = computed(() => merchants.value.find(m => m.id === form.merchantId)?.name)
+const selectedUserName = computed(() => userOptions.value.find(u => u.id === form.userId)?.name)
+const selectedPet = computed(() => currentPets.value.find(p => p.id === form.petId))
+const selectedPetName = computed(() => selectedPet.value?.name)
+const selectedPetBreed = computed(() => selectedPet.value?.breed)
+
+const selectedPkgName = computed(() => {
+  const pkgId = activeData.value.pkgId
+  return allPackages.find(p => p.id === pkgId)?.name || ''
+})
+
+
+
+const canSubmit = computed(() => {
+  if(!form.merchantId || !form.userId || !form.petId) return false
+  return true
+})
+
+// --- Methods ---
+const searchUser = (query) => { /* Mock */ }
+const handleUserChange = (val) => {
+  currentPets.value = mockPets[val] || []
+  form.petId = ''
+}
+const getStepTitle = (type) => {
+  const map = { transport: '填写接送路线与时间', feeding: '选择套餐与服务的细则', washing: '选择套餐与服务的细则' }
+  return map[type]
+}
+const getTypeName = (type) => {
+  const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' }
+  return map[type]
+}
+const formatTime = (time) => {
+  if(!time) return ''
+  const d = new Date(time)
+  return `${d.getMonth()+1}-${d.getDate()} ${d.getHours()}:${d.getMinutes() < 10 ? '0'+d.getMinutes() : d.getMinutes()}`
+}
+
+const handleSubmit = () => {
+  ElMessage.success('下单成功!订单号:ORD20248888')
+}
+
+// Initialize
+onMounted(() => {
+  calcPrice('transport')
+})
+</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; }
+
+/* Left Content */
+.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; }
+.step-num {
+  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; }
+
+/* Pet Selection */
+/* Pet Selection */
+.pet-select-row {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+  gap: 15px;
+  width: 100%;
+}
+.pet-card {
+  border: 1px solid #8D9095;
+  border-radius: 8px;
+  padding: 12px 15px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  position: relative;
+  transition: all 0.2s ease-in-out;
+  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);
+}
+.pet-card.active {
+  border-color: #409eff;
+  background-color: #fff;
+  box-shadow: 0 0 0 1px #409eff inset;
+}
+.check-mark {
+  position: absolute;
+  right: 0;
+  top: 0;
+  background: #409eff;
+  color: white;
+  width: 28px;
+  height: 18px;
+  border-radius: 0 8px 0 12px;
+  display: flex;
+  align-items: center;
+  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-card.add-card {
+  border: 1px solid #8D9095;
+  justify-content: center;
+  align-items: center;
+  color: #303133;
+  flex-direction: row;
+  gap: 8px;
+  background: #fff;
+  box-shadow: none;
+  height: auto;
+  min-height: 70px;
+}
+.pet-card.add-card:hover {
+  border-color: #303133;
+  color: #303133;
+  background: #f9f9f9;
+  transform: translateY(-2px);
+}
+
+/* Type Selection */
+.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);
+}
+.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;
+}
+.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-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; }
+
+/* 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; }
+.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;
+}
+.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; }
+</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>

+ 1 - 8
src/views/order/purchase/components/FeedingForm.vue

@@ -43,14 +43,7 @@
     </div>
 
     <div class="remark-section">
-      <div class="section-label">家庭服务及宠物档案备注</div>
-      <el-row :gutter="15">
-        <el-col :span="12"><el-input v-model="feedingData.area" placeholder="宠物活动区域" /></el-col>
-        <el-col :span="12"><el-input v-model="feedingData.itemLoc" placeholder="物品存放位置" /></el-col>
-        <el-col :span="12" style="margin-top:10px"><el-input v-model="feedingData.cleanLoc" placeholder="清洗位置" /></el-col>
-        <el-col :span="12" style="margin-top:10px"><el-input v-model="feedingData.foodAmount" placeholder="喂食量标准" /></el-col>
-        <el-col :span="24" style="margin-top:10px"><el-input v-model="feedingData.other" type="textarea" :rows="2" placeholder="其他注意事项" /></el-col>
-      </el-row>
+      <el-input v-model="feedingData.other" type="textarea" :rows="4" placeholder="请添加服务备注和宠物状态等" />
     </div>
   </div>
 </template>

+ 74 - 47
src/views/order/purchase/components/TransportForm.vue

@@ -10,57 +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">
-            <el-col :span="8">
-              <el-cascader v-model="transportData.pickRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
-            </el-col>
-            <el-col :span="16">
-              <el-input v-model="transportData.pickDetail" 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">
-            <el-col :span="8">
-              <el-cascader v-model="transportData.dropRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
-            </el-col>
-            <el-col :span="16">
-              <el-input v-model="transportData.dropDetail" 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>
@@ -80,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;
@@ -92,4 +112,11 @@ const emit = defineEmits(['change'])
 .route-connector { display: flex; align-items: center; justify-content: center; margin: 15px 0; gap: 10px; color: #909399; font-size: 12px; }
 .route-connector .line { height: 1px; width: 80px; background: #dcdfe6; }
 .route-connector .store-node { background: white; padding: 4px 12px; border-radius: 20px; border: 1px solid #dcdfe6; display: flex; align-items: center; gap: 5px; }
+
+.addr-label {
+  text-align: right;
+  color: #606266;
+  font-size: 14px;
+  padding-right: 5px;
+}
 </style>

+ 1 - 14
src/views/order/purchase/components/WashingForm.vue

@@ -43,20 +43,7 @@
     </div>
 
     <div class="remark-section">
-      <div class="section-label">服务备注及宠物状态</div>
-      <el-row :gutter="15">
-        <el-col :span="8">
-          <el-select v-model="washingData.petStatus" placeholder="宠物应激状态" style="width:100%">
-            <el-option label="性格温顺" value="calm" />
-            <el-option label="胆小怕人" value="shy" />
-            <el-option label="容易应激" value="stress" />
-            <el-option label="有攻击性" value="aggressive" />
-          </el-select>
-        </el-col>
-        <el-col :span="8"><el-input v-model="washingData.cleanLoc" placeholder="清洗位置" /></el-col>
-        <el-col :span="8"><el-input v-model="washingData.toolLoc" placeholder="工具/水源位置" /></el-col>
-        <el-col :span="24" style="margin-top:10px"><el-input v-model="washingData.other" type="textarea" :rows="2" placeholder="其他注意事项" /></el-col>
-      </el-row>
+      <el-input v-model="washingData.other" type="textarea" :rows="4" placeholder="请添加服务备注和宠物状态等" />
     </div>
   </div>
 </template>

+ 680 - 190
src/views/order/purchase/index.vue

@@ -4,28 +4,11 @@
 
       <!-- 左侧:下单填写区 -->
       <div class="form-container">
-        <!-- 1. 服务类型选择 -->
-        <div class="type-selection">
-          <div
-            v-for="item in serviceList"
-            :key="item.type"
-            class="type-card"
-            :class="[item.type, { active: form.type === item.type }]"
-            @click="handleTypeChange(item.type)"
-          >
-            <div class="icon-box"><el-icon><component :is="item.icon" /></el-icon></div>
-            <div class="text">
-              <div class="type-name">{{ item.name }}</div>
-              <div class="type-desc">{{ item.desc }}</div>
-            </div>
-          </div>
-        </div>
-
-        <!-- 2. 基础信息:门店与宠主 -->
+        <!-- 1. 基础信息:门店与宠主 -->
         <el-card shadow="never" class="section-card">
           <template #header>
             <div class="card-title">
-              <span class="step-num">02</span> 基础信息
+              <span class="step-num">01</span> 基础信息
             </div>
           </template>
           <div class="card-body">
@@ -38,56 +21,48 @@
                         <span>服务门店 (平台代下单)</span>
                       </div>
                     </template>
-                    <el-select v-model="form.merchantId" placeholder="请选择商户门店" size="large" style="width: 100%" filterable>
-                      <el-option v-for="m in merchants" :key="m.id" :label="m.name" :value="m.id" />
-                    </el-select>
+                    <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>
-                    <el-select
-                      v-model="form.userId"
-                      placeholder="搜索手机号/姓名"
-                      size="large"
-                      style="width: 100%"
-                      filterable
-                      remote
-                      :remote-method="searchUser"
-                      :loading="userLoading"
-                      @change="handleUserChange"
-                    >
-                      <el-option v-for="u in userOptions" :key="u.id" :label="u.name + ' - ' + u.phone" :value="u.id" />
-                    </el-select>
+                    <PageSelect v-model="form.userId" placeholder="搜索姓名/手机号" size="large" style="width: 100%"
+                      :options="userSelectOptions" :total="userTotal" :page-size="5" :filter-method="searchUser"
+                      @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>
@@ -96,12 +71,41 @@
           </div>
         </el-card>
 
+        <!-- 2. 服务类型选择 -->
+        <div class="type-selection" v-if="form.merchantId">
+          <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">
+              <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>
+        <div v-else
+          style="color: #909399; margin: 20px 0; padding: 20px; text-align: center; background: #fff; border-radius: 8px;">
+          请先在上一步中选择服务门店
+        </div>
+
         <!-- 3. 业务详情表单 -->
         <el-card shadow="never" class="section-card form-card" v-if="form.type">
           <template #header>
             <div class="card-title">
-              <span class="step-num">03</span>
-              {{ getStepTitle(form.type) }}
+              <span class="step-num">02</span>
+              {{ getStepTitle(form.mode, form.type) }}
             </div>
           </template>
 
@@ -114,13 +118,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>
@@ -147,7 +154,7 @@
             <div class="divider"></div>
 
             <div class="service-preview" v-if="form.type">
-              <div class="preview-title">{{ getTypeName(form.type) }}</div>
+              <div class="preview-title">{{ selectedServiceName }}</div>
 
               <!-- 套餐显示 -->
               <div class="preview-detail" v-if="selectedPkgName">
@@ -159,9 +166,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>
 
@@ -180,7 +190,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>
@@ -193,25 +204,19 @@ import FeedingForm from './components/FeedingForm.vue'
 import WashingForm from './components/WashingForm.vue'
 import AddUserDialog from './components/AddUserDialog.vue'
 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 merchants = ref([
-  { id: 1, name: '萌它宠物三里屯店' },
-  { id: 2, name: '宠爱国际动物医院' }
-])
-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, name: '' })
+const userLoading = ref(false)
 
 const serviceList = [
   { type: 'transport', name: '宠物接送', icon: 'Van', desc: '专车接送 · 全程监护', basePrice: 35 },
@@ -227,15 +232,20 @@ const allPackages = [
   { id: 14, type: 'washing', name: '除菌药浴套餐', price: 0 },
 ]
 
-// --- State ---
-const userLoading = ref(false)
 const currentPets = ref([])
 
+const stores = ref([])
+const storeTotal = ref(0)
+const allServices = ref([])
+const storeQuery = reactive({ pageNum: 1, pageSize: 5 })
+
 const form = reactive({
   merchantId: '',
   userId: '',
   petId: '',
-  type: 'transport',
+  serviceId: '',
+  type: '',
+  mode: undefined,
   groupBuyPackage: '',
 
   // Sub Forms Data
@@ -245,8 +255,8 @@ const form = reactive({
     pickPrice: 35,
     dropPrice: 35,
     subType: 'round',
-    pickRegion: [], pickDetail: '', pickContact: '', pickPhone: '', pickTime: '',
-    dropRegion: [], dropDetail: '', dropContact: '', dropPhone: '', dropTime: ''
+    pickStartRegion: [], pickStartDetail: '', pickEndRegion: [], pickEndDetail: '', pickContact: '', pickPhone: '', pickTime: '',
+    dropStartRegion: [], dropStartDetail: '', dropEndRegion: [], dropEndDetail: '', dropContact: '', dropPhone: '', dropTime: ''
   },
   feeding: {
     pkgId: '', price: 68,
@@ -263,31 +273,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.pickRegion = pet.region || []
-  form.transport.pickDetail = 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.dropRegion = pet.region || []
-  form.transport.dropDetail = 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
@@ -296,10 +314,54 @@ const activeData = computed(() => {
 })
 
 // --- Logic ---
+const fetchStores = () => {
+  listStoreOnOrder(storeQuery).then(res => {
+    stores.value = res.rows || []
+    storeTotal.value = res.total || 0
+  })
+}
 
-const handleTypeChange = (type) => {
-  form.type = type
-  calcPrice(type)
+const handleStorePageChange = (page) => {
+  storeQuery.pageNum = page
+  fetchStores()
+}
+
+const handleStoreChange = (val) => {
+  const store = stores.value.find(s => s.id === val)
+  if (store && store.services) {
+    if (!store.services.includes(form.serviceId)) {
+      form.serviceId = ''
+      form.type = ''
+      form.mode = undefined
+    }
+  } else {
+    form.serviceId = ''
+    form.type = ''
+    form.mode = undefined
+  }
+}
+
+const getServiceType = (name) => {
+  if (!name) return 'transport'
+  if (name.includes('接送')) return 'transport'
+  if (name.includes('喂') || name.includes('遛')) return 'feeding'
+  if (name.includes('洗护') || name.includes('美容')) return 'washing'
+  return 'transport'
+}
+
+const getServiceIcon = (name) => {
+  const type = getServiceType(name)
+  const map = { transport: 'Van', feeding: 'Food', washing: 'Soap' }
+  return map[type] || 'Menu'
+}
+
+const handleServiceChange = (item) => {
+  form.serviceId = item.id
+  form.mode = item.mode
+  const isRouteMode = item.mode === 1 || item.mode === '1'
+  const sysType = isRouteMode ? 'transport' : getServiceType(item.name)
+  form.type = sysType
+  calcPrice(sysType)
 }
 
 const currentPackages = computed(() => {
@@ -318,7 +380,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') {
@@ -339,102 +401,323 @@ 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('用户添加成功')
+  }
 }
 
-const pcaOptions = [
-  {
-    value: '北京市', label: '北京市',
-    children: [
-      { value: '市辖区', label: '市辖区', children: [ { value: '朝阳区', label: '朝阳区' }, { value: '海淀区', label: '海淀区' } ] }
-    ]
-  },
-  {
-    value: '上海市', label: '上海市',
-    children: [
-      { value: '市辖区', label: '市辖区', children: [ { value: '浦东新区', label: '浦东新区' }, { value: '徐汇区', label: '徐汇区' } ] }
-    ]
-  }
-]
+// Removed mocked pcaOptions since we now use element-china-area-data
 
 // Add Pet Logic
 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('宠物添加成功')
+  }
 }
 
 
 // --- Computed Helpers ---
-const selectedMerchantName = computed(() => merchants.value.find(m => m.id === form.merchantId)?.name)
+const merchantOptions = computed(() => {
+  return stores.value.map(m => ({ label: m.name, value: m.id }))
+})
+const availableServices = computed(() => {
+  const store = stores.value.find(s => s.id === form.merchantId)
+  if (!store || !store.services) return []
+  return allServices.value.filter(srv => store.services.includes(srv.id))
+})
+const userSelectOptions = computed(() => {
+  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)
 const selectedPet = computed(() => currentPets.value.find(p => p.id === form.petId))
 const selectedPetName = computed(() => selectedPet.value?.name)
 const selectedPetBreed = computed(() => selectedPet.value?.breed)
+const selectedServiceName = computed(() => {
+  return availableServices.value.find(s => s.id === form.serviceId)?.name || getTypeName(form.type)
+})
 
 const selectedPkgName = computed(() => {
-  const pkgId = activeData.value.pkgId
+  const pkgId = activeData.value?.pkgId
   return allPackages.find(p => p.id === pkgId)?.name || ''
 })
 
 
 
 const canSubmit = computed(() => {
-  if(!form.merchantId || !form.userId || !form.petId) return false
+  if (!form.merchantId || !form.userId || !form.petId || !form.serviceId) return false
   return true
 })
 
 // --- Methods ---
-const searchUser = (query) => { /* Mock */ }
+const fetchUsers = () => {
+  listCustomerOnOrder(userQuery).then(res => {
+    userOptions.value = res.rows || []
+    userTotal.value = res.total || 0
+  })
+}
+
+const handleUserPageChange = (page) => {
+  userQuery.pageNum = page
+  fetchUsers()
+}
+
+const searchUser = (query) => {
+  userQuery.name = 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 = (type) => {
-  const map = { transport: '填写接送路线与时间', feeding: '选择套餐与服务的细则', washing: '选择套餐与服务的细则' }
-  return map[type]
+const getStepTitle = (mode, type) => {
+  if (mode === 1 || mode === '1') return '填写接送路线与时间'
+  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 handleSubmit = () => {
-  ElMessage.success('下单成功!订单号:ORD20248888')
+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 = 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(() => {
-  calcPrice('transport')
+  fetchStores()
+  fetchUsers()
+  listServiceOnOrder().then(res => {
+    allServices.value = res.data || []
+  })
 })
 </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 */
@@ -444,6 +727,7 @@ onMounted(() => {
   gap: 15px;
   width: 100%;
 }
+
 .pet-card {
   border: 1px solid #8D9095;
   border-radius: 8px;
@@ -457,16 +741,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;
@@ -481,8 +768,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;
@@ -496,6 +794,7 @@ onMounted(() => {
   height: auto;
   min-height: 70px;
 }
+
 .pet-card.add-card:hover {
   border-color: #303133;
   color: #303133;
@@ -504,59 +803,250 @@ 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-card.feeding.active .icon-box {
+  background: #e6a23c;
+}
+
+.type-card.feeding.active {
+  border-color: #e6a23c;
+  background-color: #fdf6ec;
+}
 
-.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.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>

+ 25 - 0
template/api/archieves/changeLog/index.ts

@@ -0,0 +1,25 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { ArcChangeLogVO } from '@/api/archieves/changeLog/types';
+
+/**
+ * 分页查询变更日志
+ */
+export const listChangeLog = (targetId: string | number, targetType: string, query?: PageQuery): AxiosPromise<ArcChangeLogVO[]> => {
+  return request({
+    url: '/archieves/changeLog/list',
+    method: 'get',
+    params: { targetId, targetType, ...query }
+  });
+};
+
+/**
+ * 查询全部变更日志(不分页)
+ */
+export const listAllChangeLog = (targetId: string | number, targetType: string): AxiosPromise<ArcChangeLogVO[]> => {
+  return request({
+    url: '/archieves/changeLog/listAll',
+    method: 'get',
+    params: { targetId, targetType }
+  });
+};

+ 10 - 0
template/api/archieves/changeLog/types.ts

@@ -0,0 +1,10 @@
+export interface ArcChangeLogVO {
+  id: string | number;
+  targetId: number;
+  targetType: string;
+  changeType: string;
+  content: string;
+  operatorId: number;
+  operatorName: string;
+  createTime: string;
+}

+ 100 - 0
template/api/archieves/customer/index.ts

@@ -0,0 +1,100 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { UsrCustomerVO, UsrCustomerForm, UsrCustomerQuery, CustomerOnOrderVO, CustomerOnOrderQuery } from '@/api/archieves/customer/types';
+
+/**
+ * 查询用户列表
+ */
+export const listCustomer = (query?: UsrCustomerQuery): AxiosPromise<UsrCustomerVO[]> => {
+  return request({
+    url: '/archieves/customer/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询全部用户(不分页)
+ */
+export const listAllCustomer = (query?: UsrCustomerQuery): AxiosPromise<UsrCustomerVO[]> => {
+  return request({
+    url: '/archieves/customer/listAll',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询用户详细
+ */
+export const getCustomer = (id: string | number): AxiosPromise<UsrCustomerVO> => {
+  return request({
+    url: '/archieves/customer/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增用户
+ */
+export const addCustomer = (data: UsrCustomerForm) => {
+  return request({
+    url: '/archieves/customer',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 下单页新增用户
+ */
+export const addCustomerOnOrder = (data: UsrCustomerForm) => {
+  return request({
+    url: '/archieves/customer/addOnOrder',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改用户
+ */
+export const updateCustomer = (data: UsrCustomerForm) => {
+  return request({
+    url: '/archieves/customer',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除用户
+ */
+export const delCustomer = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/archieves/customer/' + id,
+    method: 'delete'
+  });
+};
+
+/**
+ * 切换用户状态
+ */
+export const changeCustomerStatus = (id: string | number, status: number) => {
+  return request({
+    url: '/archieves/customer/changeStatus',
+    method: 'put',
+    params: { id, status }
+  });
+};
+
+/**
+ * 下单页宠主列表
+ */
+export const listCustomerOnOrder = (query?: CustomerOnOrderQuery): AxiosPromise<CustomerOnOrderVO[]> => {
+  return request({
+    url: '/archieves/customer/listOnOrder',
+    method: 'get',
+    params: query
+  });
+};

+ 63 - 0
template/api/archieves/customer/types.ts

@@ -0,0 +1,63 @@
+import { SysTagVO } from '@/api/archieves/tag/types';
+
+export interface UsrCustomerVO {
+  id: string | number;
+  name: string;
+  phone: string;
+  avatar: number;
+  avatarUrl: string;
+  gender: number;
+  birthday: string;
+  idCard: string;
+  areaId: number;
+  areaName: string;
+  stationId: number;
+  stationName: string;
+  address: string;
+  emergencyContact: string;
+  emergencyPhone: string;
+  petCount: number;
+  memberLevel: number;
+  status: number;
+  remark: string;
+  createTime: string;
+  tags: SysTagVO[];
+}
+
+export interface UsrCustomerForm extends BaseEntity {
+  id?: string | number;
+  name?: string;
+  phone?: string;
+  avatar?: number;
+  gender?: number;
+  birthday?: string;
+  idCard?: string;
+  areaId?: number;
+  stationId?: number;
+  address?: string;
+  emergencyContact?: string;
+  emergencyPhone?: string;
+  memberLevel?: number;
+  status?: number;
+  remark?: string;
+  tagIds?: (string | number)[];
+}
+
+export interface UsrCustomerQuery extends PageQuery {
+  keyword?: string;
+  areaId?: number;
+  stationId?: number;
+  status?: number;
+}
+
+export interface CustomerOnOrderVO {
+  id: number;
+  name: string;
+  phoneNumber: string;
+  regionCode: string;
+  address: string;
+}
+
+export interface CustomerOnOrderQuery extends PageQuery {
+  content?: string;
+}

+ 77 - 0
template/api/archieves/pet/index.ts

@@ -0,0 +1,77 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { UsrPetVO, UsrPetForm, UsrPetQuery } from '@/api/archieves/pet/types';
+
+/**
+ * 查询宠物列表
+ */
+export const listPet = (query?: UsrPetQuery): AxiosPromise<UsrPetVO[]> => {
+  return request({
+    url: '/archieves/pet/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 按用户查询宠物列表
+ */
+export const listPetByUser = (userId: string | number): AxiosPromise<UsrPetVO[]> => {
+  return request({
+    url: '/archieves/pet/listByUser/' + userId,
+    method: 'get'
+  });
+};
+
+/**
+ * 查询宠物详细
+ */
+export const getPet = (id: string | number): AxiosPromise<UsrPetVO> => {
+  return request({
+    url: '/archieves/pet/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增宠物
+ */
+export const addPet = (data: UsrPetForm) => {
+  return request({
+    url: '/archieves/pet',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 下单页新增宠物
+ */
+export const addPetOnOrder = (data: UsrPetForm) => {
+  return request({
+    url: '/archieves/pet/addOnOrder',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改宠物
+ */
+export const updatePet = (data: UsrPetForm) => {
+  return request({
+    url: '/archieves/pet',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除宠物
+ */
+export const delPet = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/archieves/pet/' + id,
+    method: 'delete'
+  });
+};

+ 71 - 0
template/api/archieves/pet/types.ts

@@ -0,0 +1,71 @@
+import { SysTagVO } from '@/api/archieves/tag/types';
+
+export interface UsrPetVO {
+  id: string | number;
+  userId: number;
+  name: string;
+  avatar: number;
+  avatarUrl: string;
+  type: number;
+  breed: string;
+  gender: number;
+  birthday: string;
+  age: number;
+  weight: number;
+  size: string;
+  isSterilized: number;
+  arrivalTime: string;
+  houseType: string;
+  entryMethod: string;
+  entryPassword: string;
+  keyLocation: string;
+  personality: string;
+  cutePersonality: string;
+  healthStatus: string;
+  aggression: number;
+  vaccineStatus: string;
+  vaccineCert: number;
+  vaccineCertUrl: string;
+  medicalHistory: string;
+  allergies: string;
+  remark: string;
+  createTime: string;
+  ownerName: string;
+  ownerPhone: string;
+  tags: SysTagVO[];
+}
+
+export interface UsrPetForm extends BaseEntity {
+  id?: string | number;
+  userId?: number;
+  name?: string;
+  avatar?: number;
+  type?: number;
+  breed?: string;
+  gender?: number;
+  birthday?: string;
+  age?: number;
+  weight?: number;
+  size?: string;
+  isSterilized?: number;
+  arrivalTime?: string;
+  houseType?: string;
+  entryMethod?: string;
+  entryPassword?: string;
+  keyLocation?: string;
+  personality?: string;
+  cutePersonality?: string;
+  healthStatus?: string;
+  aggression?: number;
+  vaccineStatus?: string;
+  vaccineCert?: number;
+  medicalHistory?: string;
+  allergies?: string;
+  remark?: string;
+  tagIds?: (string | number)[];
+}
+
+export interface UsrPetQuery extends PageQuery {
+  keyword?: string;
+  userId?: number;
+}

+ 67 - 0
template/api/archieves/tag/index.ts

@@ -0,0 +1,67 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { SysTagVO, SysTagForm, SysTagQuery } from '@/api/archieves/tag/types';
+
+/**
+ * 查询标签列表
+ */
+export const listTag = (query?: SysTagQuery): AxiosPromise<SysTagVO[]> => {
+  return request({
+    url: '/archieves/tag/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询全部标签(不分页)
+ */
+export const listAllTag = (query?: SysTagQuery): AxiosPromise<SysTagVO[]> => {
+  return request({
+    url: '/archieves/tag/listAll',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询标签详细
+ */
+export const getTag = (id: string | number): AxiosPromise<SysTagVO> => {
+  return request({
+    url: '/archieves/tag/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增标签
+ */
+export const addTag = (data: SysTagForm) => {
+  return request({
+    url: '/archieves/tag',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改标签
+ */
+export const updateTag = (data: SysTagForm) => {
+  return request({
+    url: '/archieves/tag',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除标签
+ */
+export const delTag = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/archieves/tag/' + id,
+    method: 'delete'
+  });
+};

+ 26 - 0
template/api/archieves/tag/types.ts

@@ -0,0 +1,26 @@
+export interface SysTagVO {
+  id: string | number;
+  name: string;
+  category: string;
+  colorType: string;
+  description: string;
+  type: number;
+  status: number;
+  createTime: string;
+}
+
+export interface SysTagForm extends BaseEntity {
+  id?: string | number;
+  name?: string;
+  category?: string;
+  colorType?: string;
+  description?: string;
+  type?: number;
+  status?: number;
+}
+
+export interface SysTagQuery extends PageQuery {
+  name?: string;
+  category?: string;
+  status?: number;
+}

+ 62 - 0
template/api/demo/demo/index.ts

@@ -0,0 +1,62 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { DemoVO, DemoForm, DemoQuery } from '@/api/demo/demo/types';
+
+/**
+ * 查询测试单列表
+ * @param query
+ * @returns {*}
+ */
+export const listDemo = (query?: DemoQuery): AxiosPromise<DemoVO[]> => {
+  return request({
+    url: '/demo/demo/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询测试单详细
+ * @param id
+ */
+export const getDemo = (id: string | number): AxiosPromise<DemoVO> => {
+  return request({
+    url: '/demo/demo/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增测试单
+ * @param data
+ */
+export const addDemo = (data: DemoForm) => {
+  return request({
+    url: '/demo/demo',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改测试单
+ * @param data
+ */
+export const updateDemo = (data: DemoForm) => {
+  return request({
+    url: '/demo/demo',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除测试单
+ * @param id
+ */
+export const delDemo = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/demo/demo/' + id,
+    method: 'delete'
+  });
+};

+ 90 - 0
template/api/demo/demo/types.ts

@@ -0,0 +1,90 @@
+export interface DemoVO {
+  /**
+   * 主键
+   */
+  id: string | number;
+
+  /**
+   * 部门id
+   */
+  deptId: string | number;
+
+  /**
+   * 用户id
+   */
+  userId: string | number;
+
+  /**
+   * 排序号
+   */
+  orderNum: number;
+
+  /**
+   * key键
+   */
+  testKey: string;
+
+  /**
+   * 值
+   */
+  value: string;
+}
+
+export interface DemoForm extends BaseEntity {
+  /**
+   * 主键
+   */
+  id?: string | number;
+
+  /**
+   * 部门id
+   */
+  deptId?: string | number;
+
+  /**
+   * 用户id
+   */
+  userId?: string | number;
+
+  /**
+   * 排序号
+   */
+  orderNum?: number;
+
+  /**
+   * key键
+   */
+  testKey?: string;
+
+  /**
+   * 值
+   */
+  value?: string;
+}
+
+export interface DemoQuery extends PageQuery {
+  /**
+   * 部门id
+   */
+  deptId?: string | number;
+
+  /**
+   * 用户id
+   */
+  userId?: string | number;
+
+  /**
+   * 排序号
+   */
+  orderNum?: number;
+
+  /**
+   * key键
+   */
+  testKey?: string;
+
+  /**
+   * 值
+   */
+  value?: string;
+}

+ 62 - 0
template/api/demo/tree/index.ts

@@ -0,0 +1,62 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { TreeVO, TreeForm, TreeQuery } from '@/api/demo/tree/types';
+
+/**
+ * 查询测试树列表
+ * @param query
+ * @returns {*}
+ */
+export const listTree = (query?: TreeQuery): AxiosPromise<TreeVO[]> => {
+  return request({
+    url: '/demo/tree/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询测试树详细
+ * @param id
+ */
+export const getTree = (id: string | number): AxiosPromise<TreeVO> => {
+  return request({
+    url: '/demo/tree/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增测试树
+ * @param data
+ */
+export const addTree = (data: TreeForm) => {
+  return request({
+    url: '/demo/tree',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改测试树
+ * @param data
+ */
+export const updateTree = (data: TreeForm) => {
+  return request({
+    url: '/demo/tree',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除测试树
+ * @param id
+ */
+export const delTree = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/demo/tree/' + id,
+    method: 'delete'
+  });
+};

+ 80 - 0
template/api/demo/tree/types.ts

@@ -0,0 +1,80 @@
+export interface TreeVO {
+  /**
+   * 主键
+   */
+  id: string | number;
+
+  /**
+   * 父id
+   */
+  parentId: string | number;
+
+  /**
+   * 部门id
+   */
+  deptId: string | number;
+
+  /**
+   * 用户id
+   */
+  userId: string | number;
+
+  /**
+   * 值
+   */
+  treeName: string;
+
+  /**
+   * 子对象
+   */
+  children: TreeVO[];
+}
+
+export interface TreeForm extends BaseEntity {
+  /**
+   * 主键
+   */
+  id?: string | number;
+
+  /**
+   * 父id
+   */
+  parentId?: string | number;
+
+  /**
+   * 部门id
+   */
+  deptId?: string | number;
+
+  /**
+   * 用户id
+   */
+  userId?: string | number;
+
+  /**
+   * 值
+   */
+  treeName?: string;
+}
+
+export interface TreeQuery {
+  /**
+   * 父id
+   */
+  parentId?: string | number;
+
+  /**
+   * 部门id
+   */
+  deptId?: string | number;
+
+  /**
+   * 用户id
+   */
+  userId?: string | number;
+
+  /**
+   * 值
+   */
+  treeName?: string;
+}

+ 55 - 0
template/api/fulfiller/audit/index.ts

@@ -0,0 +1,55 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { FlfAuditVO, FlfAuditQuery } from './types';
+
+/**
+ * 查询审核记录列表
+ */
+export const listAudit = (query?: FlfAuditQuery): AxiosPromise<FlfAuditVO[]> => {
+  return request({
+    url: '/fulfiller/audit/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询审核记录详细
+ */
+export const getAudit = (id: string | number): AxiosPromise<FlfAuditVO> => {
+  return request({
+    url: '/fulfiller/audit/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 查询待审核数量
+ */
+export const getPendingCount = (): AxiosPromise<number> => {
+  return request({
+    url: '/fulfiller/audit/pendingCount',
+    method: 'get'
+  });
+};
+
+/**
+ * 审核通过
+ */
+export const passAudit = (id: string | number) => {
+  return request({
+    url: '/fulfiller/audit/pass/' + id,
+    method: 'put'
+  });
+};
+
+/**
+ * 审核驳回
+ */
+export const rejectAudit = (id: string | number, rejectReason: string) => {
+  return request({
+    url: '/fulfiller/audit/reject/' + id,
+    method: 'put',
+    params: { rejectReason }
+  });
+};

+ 42 - 0
template/api/fulfiller/audit/types.ts

@@ -0,0 +1,42 @@
+/**
+ * 审核记录 VO
+ */
+export interface FlfAuditVO {
+  id: string | number;
+  fulfillerId: string | number;
+  type: string;
+  name: string;
+  phone: string;
+  gender: string;
+  birthday: string;
+  workType: string;
+  city: string;
+  location: string;
+  realName: string;
+  idCard: string;
+  idValidDate: string;
+  idCardFront: string | number;
+  idCardBack: string | number;
+  idCardFrontUrl: string;
+  idCardBackUrl: string;
+  qualifications: string;
+  qualificationUrls: string[];
+  serviceTypes: string;
+  serviceTypeList: string[];
+  stationId: string | number;
+  stationName: string;
+  status: number;
+  auditBy: string | number;
+  auditTime: string;
+  rejectReason: string;
+  createTime: string;
+}
+
+/**
+ * 审核查询参数
+ */
+export interface FlfAuditQuery extends PageQuery {
+  keyword?: string;
+  type?: string;
+  status?: number;
+}

+ 150 - 0
template/api/fulfiller/pool/index.ts

@@ -0,0 +1,150 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import {
+  FlfFulfillerVO, FlfFulfillerForm, FlfFulfillerQuery,
+  FlfFulfillerOnOrderVO, FlfFulfillerOnOrderQuery,
+  FlfRewardForm, FlfAdjustPointsForm, FlfAdjustBalanceForm,
+  FlfPointsLogVO, FlfBalanceLogVO, FlfRewardLogVO
+} from './types';
+
+/**
+ * 查询履约者列表
+ */
+export const listFulfiller = (query?: FlfFulfillerQuery): AxiosPromise<FlfFulfillerVO[]> => {
+  return request({
+    url: '/fulfiller/fulfiller/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 下单派单时查询履约者(分页)
+ */
+export const pageFulfillerOnOrder = (query?: FlfFulfillerOnOrderQuery): AxiosPromise<{ total: number; rows: FlfFulfillerOnOrderVO[] }> => {
+  return request({
+    url: '/fulfiller/fulfiller/pageOnOrder',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询履约者详细
+ */
+export const getFulfiller = (id: string | number): AxiosPromise<FlfFulfillerVO> => {
+  return request({
+    url: '/fulfiller/fulfiller/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增履约者
+ */
+export const addFulfiller = (data: FlfFulfillerForm) => {
+  return request({
+    url: '/fulfiller/fulfiller',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改履约者
+ */
+export const updateFulfiller = (data: FlfFulfillerForm) => {
+  return request({
+    url: '/fulfiller/fulfiller',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 切换状态
+ */
+export const changeStatus = (id: string | number, status: string) => {
+  return request({
+    url: '/fulfiller/fulfiller/changeStatus',
+    method: 'put',
+    params: { id, status }
+  });
+};
+
+/**
+ * 重置密码
+ */
+export const resetPwd = (id: string | number, password: string) => {
+  return request({
+    url: '/fulfiller/fulfiller/resetPwd',
+    method: 'put',
+    params: { id, password }
+  });
+};
+
+/**
+ * 奖惩操作
+ */
+export const reward = (data: FlfRewardForm) => {
+  return request({
+    url: '/fulfiller/fulfiller/reward',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 调整积分
+ */
+export const adjustPoints = (data: FlfAdjustPointsForm) => {
+  return request({
+    url: '/fulfiller/fulfiller/adjustPoints',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 调整余额
+ */
+export const adjustBalance = (data: FlfAdjustBalanceForm) => {
+  return request({
+    url: '/fulfiller/fulfiller/adjustBalance',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 查询积分日志
+ */
+export const listPointsLog = (fulfillerId: string | number, query?: PageQuery): AxiosPromise<FlfPointsLogVO[]> => {
+  return request({
+    url: '/fulfiller/log/points',
+    method: 'get',
+    params: { fulfillerId, ...query }
+  });
+};
+
+/**
+ * 查询余额日志
+ */
+export const listBalanceLog = (fulfillerId: string | number, query?: PageQuery): AxiosPromise<FlfBalanceLogVO[]> => {
+  return request({
+    url: '/fulfiller/log/balance',
+    method: 'get',
+    params: { fulfillerId, ...query }
+  });
+};
+
+/**
+ * 查询奖惩日志
+ */
+export const listRewardLog = (fulfillerId: string | number, query?: PageQuery): AxiosPromise<FlfRewardLogVO[]> => {
+  return request({
+    url: '/fulfiller/log/reward',
+    method: 'get',
+    params: { fulfillerId, ...query }
+  });
+};

+ 179 - 0
template/api/fulfiller/pool/types.ts

@@ -0,0 +1,179 @@
+/**
+ * 履约者信息 VO
+ */
+export interface FlfFulfillerVO {
+  id: string | number;
+  userId: string | number;
+  name: string;
+  realName: string;
+  phone: string;
+  gender: string;
+  birthday: string;
+  age: number;
+  avatar: string | number;
+  avatarUrl: string;
+  idCard: string;
+  idCardFront: string | number;
+  idCardFrontUrl: string;
+  idCardBack: string | number;
+  idCardBackUrl: string;
+  idCardExpiry: string;
+  serviceTypes: string;
+  cityCode: string;
+  cityName: string;
+  stationId: string | number;
+  stationName: string;
+  workType: string;
+  levelId: string | number;
+  levelName: string;
+  points: number;
+  balance: number;
+  status: string;
+  authId: boolean;
+  authQual: boolean;
+  qualImages: string;
+  qualImageUrls: string[];
+  orderCount: number;
+  rejectCount: number;
+  rating: number;
+  createTime: string;
+  tags: FlfTagVO[];
+}
+
+/**
+ * 履约者标签 VO(简化)
+ */
+export interface FlfTagVO {
+  id: string | number;
+  name: string;
+  colorType: string;
+  description: string;
+  status: number;
+}
+
+/**
+ * 履约者表单
+ */
+export interface FlfFulfillerForm extends BaseEntity {
+  id?: string | number;
+  name?: string;
+  realName?: string;
+  phone?: string;
+  password?: string;
+  gender?: string;
+  birthday?: string;
+  idCard?: string;
+  idCardExpiry?: string;
+  serviceTypes?: string;
+  cityCode?: string;
+  cityName?: string;
+  stationId?: string | number;
+  workType?: string;
+  levelId?: string | number;
+  status?: string;
+  authId?: boolean;
+  authQual?: boolean;
+  tagIds?: (string | number)[];
+}
+
+/**
+ * 履约者查询参数
+ */
+export interface FlfFulfillerQuery extends PageQuery {
+  keyword?: string;
+  status?: string;
+  cityCode?: string;
+  stationId?: string | number;
+  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;
+}
+
+/**
+ * 奖惩操作
+ */
+export interface FlfRewardForm {
+  fulfillerId: string | number;
+  type: string;
+  target: string;
+  amount: number;
+  reason: string;
+}
+
+/**
+ * 积分调整
+ */
+export interface FlfAdjustPointsForm {
+  fulfillerId: string | number;
+  type: string;
+  amount: number;
+  reason: string;
+}
+
+/**
+ * 余额调整
+ */
+export interface FlfAdjustBalanceForm {
+  fulfillerId: string | number;
+  type: string;
+  subType: string;
+  amount: number;
+  reason: string;
+}
+
+/**
+ * 积分日志
+ */
+export interface FlfPointsLogVO {
+  id: string | number;
+  fulfillerId: string | number;
+  type: string;
+  bizType: string;
+  amount: number;
+  pointsAfter: number;
+  reason: string;
+  operatorId: string | number;
+  createTime: string;
+}
+
+/**
+ * 余额日志
+ */
+export interface FlfBalanceLogVO {
+  id: string | number;
+  fulfillerId: string | number;
+  type: string;
+  subType: string;
+  amount: number;
+  balanceAfter: number;
+  reason: string;
+  operatorId: string | number;
+  createTime: string;
+}
+
+/**
+ * 奖惩日志
+ */
+export interface FlfRewardLogVO {
+  id: string | number;
+  fulfillerId: string | number;
+  type: string;
+  target: string;
+  amount: number;
+  reason: string;
+  operatorId: string | number;
+  operatorName: string;
+  createTime: string;
+}

+ 67 - 0
template/api/fulfiller/tag/index.ts

@@ -0,0 +1,67 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { FlfTagVO, FlfTagForm, FlfTagQuery } from './types';
+
+/**
+ * 查询标签列表
+ */
+export const listTag = (query?: FlfTagQuery): AxiosPromise<FlfTagVO[]> => {
+  return request({
+    url: '/fulfiller/tag/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询全部标签(不分页)
+ */
+export const listAllTag = (query?: FlfTagQuery): AxiosPromise<FlfTagVO[]> => {
+  return request({
+    url: '/fulfiller/tag/listAll',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询标签详细
+ */
+export const getTag = (id: string | number): AxiosPromise<FlfTagVO> => {
+  return request({
+    url: '/fulfiller/tag/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增标签
+ */
+export const addTag = (data: FlfTagForm) => {
+  return request({
+    url: '/fulfiller/tag',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改标签
+ */
+export const updateTag = (data: FlfTagForm) => {
+  return request({
+    url: '/fulfiller/tag',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除标签
+ */
+export const delTag = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/fulfiller/tag/' + id,
+    method: 'delete'
+  });
+};

+ 36 - 0
template/api/fulfiller/tag/types.ts

@@ -0,0 +1,36 @@
+/**
+ * 履约者标签 VO
+ */
+export interface FlfTagVO {
+  id: string | number;
+  name: string;
+  colorType: string;
+  category: string;
+  description: string;
+  type: number;
+  status: number;
+  createTime: string;
+}
+
+/**
+ * 履约者标签表单
+ */
+export interface FlfTagForm extends BaseEntity {
+  id?: string | number;
+  name?: string;
+  colorType?: string;
+  category?: string;
+  description?: string;
+  status?: number;
+}
+
+/**
+ * 履约者标签查询参数
+ */
+export interface FlfTagQuery {
+  pageNum?: number;
+  pageSize?: number;
+  name?: string;
+  category?: string;
+  status?: number;
+}

+ 113 - 0
template/api/login.ts

@@ -0,0 +1,113 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { LoginData, LoginResult, VerifyCodeResult, TenantInfo } from './types';
+import { UserInfo } from '@/api/system/user/types';
+
+// pc端固定客户端授权id
+const clientId = import.meta.env.VITE_APP_CLIENT_ID;
+
+/**
+ * @param data {LoginData}
+ * @returns
+ */
+export function login(data: LoginData): AxiosPromise<LoginResult> {
+  const params = {
+    ...data,
+    clientId: data.clientId || clientId,
+    grantType: data.grantType || 'password'
+  };
+  return request({
+    url: '/auth/login',
+    headers: {
+      isToken: false,
+      isEncrypt: true,
+      repeatSubmit: false
+    },
+    method: 'post',
+    data: params
+  });
+}
+
+// 注册方法
+export function register(data: any) {
+  const params = {
+    ...data,
+    clientId: clientId,
+    grantType: 'password'
+  };
+  return request({
+    url: '/auth/register',
+    headers: {
+      isToken: false,
+      isEncrypt: true,
+      repeatSubmit: false
+    },
+    method: 'post',
+    data: params
+  });
+}
+
+/**
+ * 注销
+ */
+export function logout() {
+  if (import.meta.env.VITE_APP_SSE === 'true') {
+    request({
+      url: '/resource/sse/close',
+      method: 'get'
+    });
+  }
+  return request({
+    url: '/auth/logout',
+    method: 'post'
+  });
+}
+
+/**
+ * 获取验证码
+ */
+export function getCodeImg(): AxiosPromise<VerifyCodeResult> {
+  return request({
+    url: '/auth/code',
+    headers: {
+      isToken: false
+    },
+    method: 'get',
+    timeout: 20000
+  });
+}
+
+/**
+ * 第三方登录
+ */
+export function callback(data: LoginData): AxiosPromise<any> {
+  const LoginData = {
+    ...data,
+    clientId: clientId,
+    grantType: 'social'
+  };
+  return request({
+    url: '/auth/social/callback',
+    method: 'post',
+    data: LoginData
+  });
+}
+
+// 获取用户详细信息
+export function getInfo(): AxiosPromise<UserInfo> {
+  return request({
+    url: '/system/user/getInfo',
+    method: 'get'
+  });
+}
+
+// 获取租户列表
+export function getTenantList(isToken: boolean): AxiosPromise<TenantInfo> {
+  return request({
+    url: '/auth/tenant/list',
+    headers: {
+      isToken: isToken
+    },
+    method: 'get'
+  });
+}

+ 11 - 0
template/api/menu.ts

@@ -0,0 +1,11 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { RouteRecordRaw } from 'vue-router';
+
+// 获取路由
+export function getRouters(): AxiosPromise<RouteRecordRaw[]> {
+  return request({
+    url: '/system/menu/getRouters',
+    method: 'get'
+  });
+}

+ 59 - 0
template/api/monitor/cache/index.ts

@@ -0,0 +1,59 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { CacheVO } from './types';
+
+// 查询缓存详细
+export function getCache(): AxiosPromise<CacheVO> {
+  return request({
+    url: '/monitor/cache',
+    method: 'get'
+  });
+}
+
+// 查询缓存名称列表
+export function listCacheName() {
+  return request({
+    url: '/monitor/cache/getNames',
+    method: 'get'
+  });
+}
+
+// 查询缓存键名列表
+export function listCacheKey(cacheName: string) {
+  return request({
+    url: '/monitor/cache/getKeys/' + cacheName,
+    method: 'get'
+  });
+}
+
+// 查询缓存内容
+export function getCacheValue(cacheName: string, cacheKey: string) {
+  return request({
+    url: '/monitor/cache/getValue/' + cacheName + '/' + cacheKey,
+    method: 'get'
+  });
+}
+
+// 清理指定名称缓存
+export function clearCacheName(cacheName: string) {
+  return request({
+    url: '/monitor/cache/clearCacheName/' + cacheName,
+    method: 'delete'
+  });
+}
+
+// 清理指定键名缓存
+export function clearCacheKey(cacheName: string, cacheKey: string) {
+  return request({
+    url: '/monitor/cache/clearCacheKey/' + cacheName + '/' + cacheKey,
+    method: 'delete'
+  });
+}
+
+// 清理全部缓存
+export function clearCacheAll() {
+  return request({
+    url: '/monitor/cache/clearCacheAll',
+    method: 'delete'
+  });
+}

+ 7 - 0
template/api/monitor/cache/types.ts

@@ -0,0 +1,7 @@
+export interface CacheVO {
+  commandStats: Array<{ name: string; value: string }>;
+
+  dbSize: number;
+
+  info: { [key: string]: string };
+}

+ 36 - 0
template/api/monitor/loginInfo/index.ts

@@ -0,0 +1,36 @@
+import request from '@/utils/request';
+import { LoginInfoQuery, LoginInfoVO } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询登录日志列表
+export function list(query: LoginInfoQuery): AxiosPromise<LoginInfoVO[]> {
+  return request({
+    url: '/monitor/logininfor/list',
+    method: 'get',
+    params: query
+  });
+}
+
+// 删除登录日志
+export function delLoginInfo(infoId: string | number | Array<string | number>) {
+  return request({
+    url: '/monitor/logininfor/' + infoId,
+    method: 'delete'
+  });
+}
+
+// 解锁用户登录状态
+export function unlockLoginInfo(userName: string | Array<string>) {
+  return request({
+    url: '/monitor/logininfor/unlock/' + userName,
+    method: 'get'
+  });
+}
+
+// 清空登录日志
+export function cleanLoginInfo() {
+  return request({
+    url: '/monitor/logininfor/clean',
+    method: 'delete'
+  });
+}

+ 20 - 0
template/api/monitor/loginInfo/types.ts

@@ -0,0 +1,20 @@
+export interface LoginInfoVO {
+  infoId: string | number;
+  tenantId: string | number;
+  userName: string;
+  status: string;
+  ipaddr: string;
+  loginLocation: string;
+  browser: string;
+  os: string;
+  msg: string;
+  loginTime: string;
+}
+
+export interface LoginInfoQuery extends PageQuery {
+  ipaddr: string;
+  userName: string;
+  status: string;
+  orderByColumn: string;
+  isAsc: string;
+}

+ 36 - 0
template/api/monitor/online/index.ts

@@ -0,0 +1,36 @@
+import request from '@/utils/request';
+import { OnlineQuery, OnlineVO } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询在线用户列表
+export function list(query: OnlineQuery): AxiosPromise<OnlineVO[]> {
+  return request({
+    url: '/monitor/online/list',
+    method: 'get',
+    params: query
+  });
+}
+
+// 强退用户
+export function forceLogout(tokenId: string) {
+  return request({
+    url: '/monitor/online/' + tokenId,
+    method: 'delete'
+  });
+}
+
+// 获取当前用户登录在线设备
+export function getOnline() {
+  return request({
+    url: '/monitor/online',
+    method: 'get'
+  });
+}
+
+// 删除当前在线设备
+export function delOnline(tokenId: string) {
+  return request({
+    url: '/monitor/online/myself/' + tokenId,
+    method: 'delete'
+  });
+}

+ 15 - 0
template/api/monitor/online/types.ts

@@ -0,0 +1,15 @@
+export interface OnlineQuery extends PageQuery {
+  ipaddr: string;
+  userName: string;
+}
+
+export interface OnlineVO extends BaseEntity {
+  tokenId: string;
+  deptName: string;
+  userName: string;
+  ipaddr: string;
+  loginLocation: string;
+  browser: string;
+  os: string;
+  loginTime: number;
+}

+ 28 - 0
template/api/monitor/operlog/index.ts

@@ -0,0 +1,28 @@
+import request from '@/utils/request';
+import { OperLogQuery, OperLogVO } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询操作日志列表
+export function list(query: OperLogQuery): AxiosPromise<OperLogVO[]> {
+  return request({
+    url: '/monitor/operlog/list',
+    method: 'get',
+    params: query
+  });
+}
+
+// 删除操作日志
+export function delOperlog(operId: string | number | Array<string | number>) {
+  return request({
+    url: '/monitor/operlog/' + operId,
+    method: 'delete'
+  });
+}
+
+// 清空操作日志
+export function cleanOperlog() {
+  return request({
+    url: '/monitor/operlog/clean',
+    method: 'delete'
+  });
+}

+ 53 - 0
template/api/monitor/operlog/types.ts

@@ -0,0 +1,53 @@
+export interface OperLogQuery extends PageQuery {
+  operIp: string;
+  title: string;
+  operName: string;
+  businessType: string;
+  status: string;
+  orderByColumn: string;
+  isAsc: string;
+}
+
+export interface OperLogVO extends BaseEntity {
+  operId: string | number;
+  tenantId: string;
+  title: string;
+  businessType: number;
+  businessTypes: number[] | undefined;
+  method: string;
+  requestMethod: string;
+  operatorType: number;
+  operName: string;
+  deptName: string;
+  operUrl: string;
+  operIp: string;
+  operLocation: string;
+  operParam: string;
+  jsonResult: string;
+  status: number;
+  errorMsg: string;
+  operTime: string;
+  costTime: number;
+}
+
+export interface OperLogForm {
+  operId: number | string | undefined;
+  tenantId: string | number | undefined;
+  title: string;
+  businessType: number;
+  businessTypes: number[] | undefined;
+  method: string;
+  requestMethod: string;
+  operatorType: number;
+  operName: string;
+  deptName: string;
+  operUrl: string;
+  operIp: string;
+  operLocation: string;
+  operParam: string;
+  jsonResult: string;
+  status: number;
+  errorMsg: string;
+  operTime: string;
+  costTime: number;
+}

+ 15 - 0
template/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
template/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[];
+}

+ 40 - 0
template/api/order/subOrder/index.ts

@@ -0,0 +1,40 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { SubOrderVO, SubOrderQuery } from './types';
+
+/**
+ * 查询子订单列表
+ * @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
+    });
+};

+ 36 - 0
template/api/order/subOrder/types.ts

@@ -0,0 +1,36 @@
+export interface SubOrderQuery {
+    status?: number | string;
+    service?: number | string;
+    content?: string;
+    pageNum: number;
+    pageSize: number;
+}
+
+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;
+}

+ 11 - 0
template/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
template/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;
+}

+ 76 - 0
template/api/resource/smsConfig/index.ts

@@ -0,0 +1,76 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { SmsConfigVO, SmsConfigForm, SmsConfigQuery } from '@/api/resource/smsConfig/types';
+
+/**
+ * 查询短信配置列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listSmsConfig = (query?: SmsConfigQuery): AxiosPromise<SmsConfigVO[]> => {
+  return request({
+    url: '/resource/smsConfig/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询短信配置详细
+ * @param id
+ */
+export const getSmsConfig = (id: string | number): AxiosPromise<SmsConfigVO> => {
+  return request({
+    url: '/resource/smsConfig/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增短信配置
+ * @param data
+ */
+export const addSmsConfig = (data: SmsConfigForm) => {
+  return request({
+    url: '/resource/smsConfig',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改短信配置
+ * @param data
+ */
+export const updateSmsConfig = (data: SmsConfigForm) => {
+  return request({
+    url: '/resource/smsConfig',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除短信配置
+ * @param id
+ */
+export const delSmsConfig = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/resource/smsConfig/' + id,
+    method: 'delete'
+  });
+};
+
+/**
+ * 修改短信配置状态
+ * @param id
+ * @param status
+ */
+export const changeSmsConfigStatus = (id: string | number, status: boolean) => {
+  return request({
+    url: '/resource/smsConfig/changeStatus',
+    method: 'put',
+    data: { id, status }
+  });
+};

+ 131 - 0
template/api/resource/smsConfig/types.ts

@@ -0,0 +1,131 @@
+export interface SmsConfigVO {
+  /**
+   * 序号
+   */
+  id: string | number;
+
+  /**
+   * 供应商
+   */
+  supplier: string;
+
+  /**
+   * AccessKey
+   */
+  accessKeyId: string | number;
+
+  /**
+   * AccessKeySecret
+   */
+  accessKeySecret: string;
+
+  /**
+   * 短信签名
+   */
+  signature: string;
+
+  /**
+   * sdk-app-id
+   */
+  sdkAppId: string | number;
+
+  /**
+   * 模板ID
+   */
+  templateId: string | number;
+
+  /**
+   * 状态
+   */
+  status: boolean;
+
+}
+
+export interface SmsConfigForm extends BaseEntity {
+  /**
+   * 序号
+   */
+  id?: string | number;
+
+  /**
+   * 供应商
+   */
+  supplier?: string;
+
+  /**
+   * AccessKey
+   */
+  accessKeyId?: string | number;
+
+  /**
+   * AccessKeySecret
+   */
+  accessKeySecret?: string;
+
+  /**
+   * 短信签名
+   */
+  signature?: string;
+
+  /**
+   * sdk-app-id
+   */
+  sdkAppId?: string | number;
+
+  /**
+   * 模板ID
+   */
+  templateId?: string | number;
+
+  /**
+   * 状态
+   */
+  status?: boolean;
+
+}
+
+export interface SmsConfigQuery extends PageQuery {
+
+  /**
+   * 供应商
+   */
+  supplier?: string;
+
+  /**
+   * AccessKey
+   */
+  accessKeyId?: string | number;
+
+  /**
+   * AccessKeySecret
+   */
+  accessKeySecret?: string;
+
+  /**
+   * 短信签名
+   */
+  signature?: string;
+
+  /**
+   * sdk-app-id
+   */
+  sdkAppId?: string | number;
+
+  /**
+   * 模板ID
+   */
+  templateId?: string | number;
+
+  /**
+   * 状态
+   */
+  status?: boolean;
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+}
+
+
+

+ 85 - 0
template/api/service/list/index.ts

@@ -0,0 +1,85 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { ServiceVO, ServiceForm, ServiceQuery, ServiceOrderVO, ServiceOnStoreVo } from './types';
+
+/**
+ * 查询服务项目列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listService = (query?: ServiceQuery): AxiosPromise<ServiceVO[]> => {
+  return request({
+    url: '/service/list/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询服务项目详细
+ * @param id
+ */
+export const getService = (id: string | number): AxiosPromise<ServiceVO> => {
+  return request({
+    url: '/service/list/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增服务项目
+ * @param data
+ */
+export const addService = (data: ServiceForm) => {
+  return request({
+    url: '/service/list',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改服务项目
+ * @param data
+ */
+export const updateService = (data: ServiceForm) => {
+  return request({
+    url: '/service/list',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除服务项目
+ * @param id
+ */
+export const delService = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/service/list/' + id,
+    method: 'delete'
+  });
+};
+
+/**
+ * 获取服务项目列表
+ * @Author Huanyi
+ */
+export const listOnStore = (): AxiosPromise<ServiceOnStoreVo[]> => {
+  return request({
+    url: '/service/list/listOnStore',
+    method: 'GET'
+  });
+};
+
+/**
+ * 查询下单页的服务列表
+ * @returns {*}
+ */
+export const listServiceOnOrder = (): AxiosPromise<ServiceOrderVO[]> => {
+  return request({
+    url: '/service/list/listOnOrder',
+    method: 'get'
+  });
+};

+ 92 - 0
template/api/service/list/types.ts

@@ -0,0 +1,92 @@
+export interface ServiceVO {
+  /**
+   * 序号
+   */
+  id: string | number;
+
+  /**
+   * 服务名称
+   */
+  name: string;
+
+  /**
+   * 服务图标
+   */
+  icon: number;
+
+  /**
+   * 服务图标Url
+   */
+  iconUrl: string;
+  /**
+   * 服务模式
+   */
+  mode: number;
+
+  /**
+   * 排序权重
+   */
+  sort: number;
+
+  /**
+   * 备注说明
+   */
+  remark: string;
+
+  /**
+   * 创建时间
+   */
+  createTime: string;
+}
+
+export interface ServiceForm extends BaseEntity {
+  /**
+   * 序号
+   */
+  id?: string | number;
+
+  /**
+   * 服务名称
+   */
+  name?: string;
+
+  /**
+   * 服务图标
+   */
+  icon?: number;
+
+  /**
+   * 服务模式
+   */
+  mode?: number;
+
+  /**
+   * 排序权重
+   */
+  sort?: number;
+
+  /**
+   * 备注说明
+   */
+  remark?: string;
+}
+
+export interface ServiceQuery extends PageQuery {
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+}
+
+export interface ServiceOnStoreVo {
+  id?: number | string;
+  name?: string;
+}
+
+export interface ServiceOrderVO {
+  id: number;
+  name: string;
+  remark: string;
+  icon: string;
+  mode: number;
+}

+ 11 - 0
template/api/service/mode/index.ts

@@ -0,0 +1,11 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { SysServiceModeVo } from '@/api/service/mode/types';
+
+export const list = (): AxiosPromise<SysServiceModeVo[]> => {
+  return request({
+    url: '/service/mode/list',
+    method: 'GET'
+  });
+};
+

+ 4 - 0
template/api/service/mode/types.ts

@@ -0,0 +1,4 @@
+export interface SysServiceModeVo {
+  value: number;
+  label: string;
+}

+ 85 - 0
template/api/system/areaStation/index.ts

@@ -0,0 +1,85 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { AreaStationVO, AreaStationForm, AreaStationQuery, SysAreaStationTypeVo, SysAreaStationOnStoreVo } from '@/api/system/areaStation/types';
+
+/**
+ * 查询区域站点列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listAreaStation = (query?: AreaStationQuery): AxiosPromise<AreaStationVO[]> => {
+  return request({
+    url: '/system/areaStation/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询区域站点详细
+ * @param id
+ */
+export const getAreaStation = (id: string | number): AxiosPromise<AreaStationVO> => {
+  return request({
+    url: '/system/areaStation/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增区域站点
+ * @param data
+ */
+export const addAreaStation = (data: AreaStationForm) => {
+  return request({
+    url: '/system/areaStation',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改区域站点
+ * @param data
+ */
+export const updateAreaStation = (data: AreaStationForm) => {
+  return request({
+    url: '/system/areaStation',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除区域站点
+ * @param id
+ */
+export const delAreaStation = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/system/areaStation/' + id,
+    method: 'delete'
+  });
+};
+
+/**
+ * 获取区域类型
+ * @Author Huanyi
+ */
+export const listType = (): AxiosPromise<SysAreaStationTypeVo[]> => {
+  return request({
+    url: '/system/areaStation/listType',
+    method: 'GET'
+  });
+};
+
+/**
+ * 获取区域列表
+ * @Author Huanyi
+ */
+export const listOnStore = (): AxiosPromise<SysAreaStationOnStoreVo[]> => {
+  return request({
+    url: '/system/areaStation/listOnStore',
+    method: 'GET'
+  });
+};

+ 145 - 0
template/api/system/areaStation/types.ts

@@ -0,0 +1,145 @@
+export interface AreaStationVO {
+  /**
+   * 序号
+   */
+  id: string | number;
+
+  /**
+   * 区域名称
+   */
+  name: string;
+
+  /**
+   * 省市编码
+   */
+  code: string;
+
+  /**
+   * 排序权重
+   */
+  sort: number;
+
+  /**
+   * 详细地址
+   */
+  address: string;
+
+  /**
+   * 站长姓名
+   */
+  leaderName: string;
+
+  /**
+   * 联系电话
+   */
+  contactPhone: string;
+
+  /**
+   * 经度
+   */
+  longitude: number;
+
+  /**
+   * 纬度
+   */
+  latitude: number;
+
+  /**
+   * 类型
+   */
+  type: number;
+
+  /**
+   * 状态
+   */
+  status: number;
+
+    /**
+     * 子对象
+     */
+    children: AreaStationVO[];
+}
+
+export interface AreaStationForm extends BaseEntity {
+  /**
+   * 序号
+   */
+  id?: string | number;
+
+  /**
+   * 区域名称
+   */
+  name?: string;
+
+  /**
+   * 父级ID
+   */
+  parentId?: string | number;
+
+  /**
+   * 省市编码
+   */
+  code?: string;
+
+  /**
+   * 排序权重
+   */
+  sort?: number;
+
+  /**
+   * 详细地址
+   */
+  address?: string;
+
+  /**
+   * 站长姓名
+   */
+  leaderName?: string;
+
+  /**
+   * 联系电话
+   */
+  contactPhone?: string;
+
+  /**
+   * 经度
+   */
+  longitude?: number;
+
+  /**
+   * 纬度
+   */
+  latitude?: number;
+
+  /**
+   * 类型
+   */
+  type?: number;
+
+  /**
+   * 状态
+   */
+  status?: number;
+
+}
+
+export interface AreaStationQuery {
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+}
+
+export interface SysAreaStationTypeVo {
+  value?: number;
+  label?: string;
+  style?: string;
+}
+
+export interface SysAreaStationOnStoreVo {
+  id?: number | string;
+  name?: string;
+  type?: number;
+  parentId?: number | string;
+}

+ 80 - 0
template/api/system/client/index.ts

@@ -0,0 +1,80 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { ClientVO, ClientForm, ClientQuery } from '@/api/system/client/types';
+
+/**
+ * 查询客户端管理列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listClient = (query?: ClientQuery): AxiosPromise<ClientVO[]> => {
+  return request({
+    url: '/system/client/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询客户端管理详细
+ * @param id
+ */
+export const getClient = (id: string | number): AxiosPromise<ClientVO> => {
+  return request({
+    url: '/system/client/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增客户端管理
+ * @param data
+ */
+export const addClient = (data: ClientForm) => {
+  return request({
+    url: '/system/client',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改客户端管理
+ * @param data
+ */
+export const updateClient = (data: ClientForm) => {
+  return request({
+    url: '/system/client',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除客户端管理
+ * @param id
+ */
+export const delClient = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/system/client/' + id,
+    method: 'delete'
+  });
+};
+
+/**
+ * 状态修改
+ * @param clientId 客户端id
+ * @param status 状态
+ */
+export function changeStatus(clientId: string, status: string) {
+  const data = {
+    clientId,
+    status
+  };
+  return request({
+    url: '/system/client/changeStatus',
+    method: 'put',
+    data: data
+  });
+}

+ 135 - 0
template/api/system/client/types.ts

@@ -0,0 +1,135 @@
+export interface ClientVO {
+  /**
+   * id
+   */
+  id: string | number;
+
+  /**
+   * 客户端id
+   */
+  clientId: string;
+
+  /**
+   * 客户端key
+   */
+  clientKey: string;
+
+  /**
+   * 客户端秘钥
+   */
+  clientSecret: string;
+
+  /**
+   * 授权类型
+   */
+  grantTypeList: string[];
+
+  /**
+   * 设备类型
+   */
+  deviceType: string;
+
+  /**
+   * token活跃超时时间
+   */
+  activeTimeout: number;
+
+  /**
+   * token固定超时
+   */
+  timeout: number;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status: string;
+}
+
+export interface ClientForm extends BaseEntity {
+  /**
+   * id
+   */
+  id?: string | number;
+
+  /**
+   * 客户端id
+   */
+  clientId?: string | number;
+
+  /**
+   * 客户端key
+   */
+  clientKey?: string;
+
+  /**
+   * 客户端秘钥
+   */
+  clientSecret?: string;
+
+  /**
+   * 授权类型
+   */
+  grantTypeList?: string[];
+
+  /**
+   * 设备类型
+   */
+  deviceType?: string;
+
+  /**
+   * token活跃超时时间
+   */
+  activeTimeout?: number;
+
+  /**
+   * token固定超时
+   */
+  timeout?: number;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status?: string;
+}
+
+export interface ClientQuery extends PageQuery {
+  /**
+   * 客户端id
+   */
+  clientId?: string | number;
+
+  /**
+   * 客户端key
+   */
+  clientKey?: string;
+
+  /**
+   * 客户端秘钥
+   */
+  clientSecret?: string;
+
+  /**
+   * 授权类型
+   */
+  grantType?: string;
+
+  /**
+   * 设备类型
+   */
+  deviceType?: string;
+
+  /**
+   * token活跃超时时间
+   */
+  activeTimeout?: number;
+
+  /**
+   * token固定超时
+   */
+  timeout?: number;
+
+  /**
+   * 状态(0正常 1停用)
+   */
+  status?: string;
+}

+ 74 - 0
template/api/system/config/index.ts

@@ -0,0 +1,74 @@
+import request from '@/utils/request';
+import { ConfigForm, ConfigQuery, ConfigVO } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询参数列表
+export function listConfig(query: ConfigQuery): AxiosPromise<ConfigVO[]> {
+  return request({
+    url: '/system/config/list',
+    method: 'get',
+    params: query
+  });
+}
+
+// 查询参数详细
+export function getConfig(configId: string | number): AxiosPromise<ConfigVO> {
+  return request({
+    url: '/system/config/' + configId,
+    method: 'get'
+  });
+}
+
+// 根据参数键名查询参数值
+export function getConfigKey(configKey: string): AxiosPromise<string> {
+  return request({
+    url: '/system/config/configKey/' + configKey,
+    method: 'get'
+  });
+}
+
+// 新增参数配置
+export function addConfig(data: ConfigForm) {
+  return request({
+    url: '/system/config',
+    method: 'post',
+    data: data
+  });
+}
+
+// 修改参数配置
+export function updateConfig(data: ConfigForm) {
+  return request({
+    url: '/system/config',
+    method: 'put',
+    data: data
+  });
+}
+
+// 修改参数配置
+export function updateConfigByKey(key: string, value: any) {
+  return request({
+    url: '/system/config/updateByKey',
+    method: 'put',
+    data: {
+      configKey: key,
+      configValue: value
+    }
+  });
+}
+
+// 删除参数配置
+export function delConfig(configId: string | number | Array<string | number>) {
+  return request({
+    url: '/system/config/' + configId,
+    method: 'delete'
+  });
+}
+
+// 刷新参数缓存
+export function refreshCache() {
+  return request({
+    url: '/system/config/refreshCache',
+    method: 'delete'
+  });
+}

+ 23 - 0
template/api/system/config/types.ts

@@ -0,0 +1,23 @@
+export interface ConfigVO extends BaseEntity {
+  configId: number | string;
+  configName: string;
+  configKey: string;
+  configValue: string;
+  configType: string;
+  remark: string;
+}
+
+export interface ConfigForm {
+  configId: number | string | undefined;
+  configName: string;
+  configKey: string;
+  configValue: string;
+  configType: string;
+  remark: string;
+}
+
+export interface ConfigQuery extends PageQuery {
+  configName: string;
+  configKey: string;
+  configType: string;
+}

+ 65 - 0
template/api/system/dept/index.ts

@@ -0,0 +1,65 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { DeptForm, DeptQuery, DeptTreeVO, DeptVO } from './types';
+
+// 查询部门列表
+export const listDept = (query?: DeptQuery) => {
+  return request({
+    url: '/system/dept/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 通过deptIds查询部门
+ * @param deptIds
+ */
+export const optionSelect = (deptIds: (number | string)[]): AxiosPromise<DeptVO[]> => {
+  return request({
+    url: '/system/dept/optionselect?deptIds=' + deptIds,
+    method: 'get'
+  });
+};
+
+// 查询部门列表(排除节点)
+export const listDeptExcludeChild = (deptId: string | number): AxiosPromise<DeptVO[]> => {
+  return request({
+    url: '/system/dept/list/exclude/' + deptId,
+    method: 'get'
+  });
+};
+
+// 查询部门详细
+export const getDept = (deptId: string | number): AxiosPromise<DeptVO> => {
+  return request({
+    url: '/system/dept/' + deptId,
+    method: 'get'
+  });
+};
+
+// 新增部门
+export const addDept = (data: DeptForm) => {
+  return request({
+    url: '/system/dept',
+    method: 'post',
+    data: data
+  });
+};
+
+// 修改部门
+export const updateDept = (data: DeptForm) => {
+  return request({
+    url: '/system/dept',
+    method: 'put',
+    data: data
+  });
+};
+
+// 删除部门
+export const delDept = (deptId: number | string) => {
+  return request({
+    url: '/system/dept/' + deptId,
+    method: 'delete'
+  });
+};

+ 60 - 0
template/api/system/dept/types.ts

@@ -0,0 +1,60 @@
+/**
+ * 部门查询参数
+ */
+export interface DeptQuery extends PageQuery {
+  deptName?: string;
+  deptCategory?: string;
+  status?: number;
+}
+
+/**
+ * 部门类型
+ */
+export interface DeptVO extends BaseEntity {
+  id: number | string;
+  parentName: string;
+  parentId: number | string;
+  children: DeptVO[];
+  deptId: number | string;
+  deptName: string;
+  deptCategory: string;
+  orderNum: number;
+  leader: string;
+  phone: string;
+  email: string;
+  status: string;
+  delFlag: string;
+  ancestors: string;
+  menuId: string | number;
+}
+
+/**
+ * 部门类型
+ */
+export interface DeptTreeVO extends BaseEntity {
+  id: number | string;
+  label: string;
+  parentId: number | string;
+  weight: number;
+  children: DeptTreeVO[];
+  disabled: boolean;
+}
+
+/**
+ * 部门表单类型
+ */
+export interface DeptForm {
+  parentName?: string;
+  parentId?: number | string;
+  children?: DeptForm[];
+  deptId?: number | string;
+  deptName?: string;
+  deptCategory?: string;
+  orderNum?: number;
+  leader?: string;
+  phone?: string;
+  email?: string;
+  status?: string;
+  delFlag?: string;
+  ancestors?: string;
+}

+ 53 - 0
template/api/system/dict/data/index.ts

@@ -0,0 +1,53 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { DictDataForm, DictDataQuery, DictDataVO } from './types';
+// 根据字典类型查询字典数据信息
+export function getDicts(dictType: string): AxiosPromise<DictDataVO[]> {
+  return request({
+    url: '/system/dict/data/type/' + dictType,
+    method: 'get'
+  });
+}
+
+// 查询字典数据列表
+export function listData(query: DictDataQuery): AxiosPromise<DictDataVO[]> {
+  return request({
+    url: '/system/dict/data/list',
+    method: 'get',
+    params: query
+  });
+}
+
+// 查询字典数据详细
+export function getData(dictCode: string | number): AxiosPromise<DictDataVO> {
+  return request({
+    url: '/system/dict/data/' + dictCode,
+    method: 'get'
+  });
+}
+
+// 新增字典数据
+export function addData(data: DictDataForm) {
+  return request({
+    url: '/system/dict/data',
+    method: 'post',
+    data: data
+  });
+}
+
+// 修改字典数据
+export function updateData(data: DictDataForm) {
+  return request({
+    url: '/system/dict/data',
+    method: 'put',
+    data: data
+  });
+}
+
+// 删除字典数据
+export function delData(dictCode: string | number | Array<string | number>) {
+  return request({
+    url: '/system/dict/data/' + dictCode,
+    method: 'delete'
+  });
+}

+ 26 - 0
template/api/system/dict/data/types.ts

@@ -0,0 +1,26 @@
+export interface DictDataQuery extends PageQuery {
+  dictName: string;
+  dictType: string;
+  dictLabel: string;
+}
+
+export interface DictDataVO extends BaseEntity {
+  dictCode: string;
+  dictLabel: string;
+  dictValue: string;
+  cssClass: string;
+  listClass: ElTagType;
+  dictSort: number;
+  remark: string;
+}
+
+export interface DictDataForm {
+  dictType?: string;
+  dictCode: string | undefined;
+  dictLabel: string;
+  dictValue: string;
+  cssClass: string;
+  listClass: ElTagType;
+  dictSort: number;
+  remark: string;
+}

+ 62 - 0
template/api/system/dict/type/index.ts

@@ -0,0 +1,62 @@
+import request from '@/utils/request';
+import { DictTypeForm, DictTypeVO, DictTypeQuery } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询字典类型列表
+export function listType(query: DictTypeQuery): AxiosPromise<DictTypeVO[]> {
+  return request({
+    url: '/system/dict/type/list',
+    method: 'get',
+    params: query
+  });
+}
+
+// 查询字典类型详细
+export function getType(dictId: number | string): AxiosPromise<DictTypeVO> {
+  return request({
+    url: '/system/dict/type/' + dictId,
+    method: 'get'
+  });
+}
+
+// 新增字典类型
+export function addType(data: DictTypeForm) {
+  return request({
+    url: '/system/dict/type',
+    method: 'post',
+    data: data
+  });
+}
+
+// 修改字典类型
+export function updateType(data: DictTypeForm) {
+  return request({
+    url: '/system/dict/type',
+    method: 'put',
+    data: data
+  });
+}
+
+// 删除字典类型
+export function delType(dictId: string | number | Array<string | number>) {
+  return request({
+    url: '/system/dict/type/' + dictId,
+    method: 'delete'
+  });
+}
+
+// 刷新字典缓存
+export function refreshCache() {
+  return request({
+    url: '/system/dict/type/refreshCache',
+    method: 'delete'
+  });
+}
+
+// 获取字典选择框列表
+export function optionselect(): AxiosPromise<DictTypeVO[]> {
+  return request({
+    url: '/system/dict/type/optionselect',
+    method: 'get'
+  });
+}

+ 18 - 0
template/api/system/dict/type/types.ts

@@ -0,0 +1,18 @@
+export interface DictTypeVO extends BaseEntity {
+  dictId: number | string;
+  dictName: string;
+  dictType: string;
+  remark: string;
+}
+
+export interface DictTypeForm {
+  dictId: number | string | undefined;
+  dictName: string;
+  dictType: string;
+  remark: string;
+}
+
+export interface DictTypeQuery extends PageQuery {
+  dictName: string;
+  dictType: string;
+}

+ 78 - 0
template/api/system/menu/index.ts

@@ -0,0 +1,78 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { MenuQuery, MenuVO, MenuForm, MenuTreeOption, RoleMenuTree } from './types';
+
+// 查询菜单列表
+export const listMenu = (query?: MenuQuery): AxiosPromise<MenuVO[]> => {
+  return request({
+    url: '/system/menu/list',
+    method: 'get',
+    params: query
+  });
+};
+
+// 查询菜单详细
+export const getMenu = (menuId: string | number): AxiosPromise<MenuVO> => {
+  return request({
+    url: '/system/menu/' + menuId,
+    method: 'get'
+  });
+};
+
+// 查询菜单下拉树结构
+export const treeselect = (): AxiosPromise<MenuTreeOption[]> => {
+  return request({
+    url: '/system/menu/treeselect',
+    method: 'get'
+  });
+};
+
+// 根据角色ID查询菜单下拉树结构
+export const roleMenuTreeselect = (roleId: string | number): AxiosPromise<RoleMenuTree> => {
+  return request({
+    url: '/system/menu/roleMenuTreeselect/' + roleId,
+    method: 'get'
+  });
+};
+
+// 根据角色ID查询菜单下拉树结构
+export const tenantPackageMenuTreeselect = (packageId: string | number): AxiosPromise<RoleMenuTree> => {
+  return request({
+    url: '/system/menu/tenantPackageMenuTreeselect/' + packageId,
+    method: 'get'
+  });
+};
+
+// 新增菜单
+export const addMenu = (data: MenuForm) => {
+  return request({
+    url: '/system/menu',
+    method: 'post',
+    data: data
+  });
+};
+
+// 修改菜单
+export const updateMenu = (data: MenuForm) => {
+  return request({
+    url: '/system/menu',
+    method: 'put',
+    data: data
+  });
+};
+
+// 删除菜单
+export const delMenu = (menuId: string | number) => {
+  return request({
+    url: '/system/menu/' + menuId,
+    method: 'delete'
+  });
+};
+
+// 级联删除菜单
+export const cascadeDelMenu = (menuIds: Array<string | number>) => {
+  return request({
+    url: '/system/menu/cascade/' + menuIds,
+    method: 'delete'
+  });
+};

+ 70 - 0
template/api/system/menu/types.ts

@@ -0,0 +1,70 @@
+import { MenuTypeEnum } from '@/enums/MenuTypeEnum';
+
+/**
+ * 菜单树形结构类型
+ */
+export interface MenuTreeOption {
+  id: string | number;
+  label: string;
+  parentId: string | number;
+  weight: number;
+  children?: MenuTreeOption[];
+}
+
+export interface RoleMenuTree {
+  menus: MenuTreeOption[];
+  checkedKeys: string[];
+}
+
+/**
+ * 菜单查询参数类型
+ */
+export interface MenuQuery {
+  keywords?: string;
+  menuName?: string;
+  status?: string;
+  platformId?: number;
+}
+
+/**
+ * 菜单视图对象类型
+ */
+export interface MenuVO extends BaseEntity {
+  parentName: string;
+  parentId: string | number;
+  children: MenuVO[];
+  menuId: string | number;
+  menuName: string;
+  orderNum: number;
+  path: string;
+  component: string;
+  queryParam: string;
+  isFrame: string;
+  isCache: string;
+  menuType: MenuTypeEnum;
+  visible: string;
+  status: string;
+  icon: string;
+  remark: string;
+}
+
+export interface MenuForm {
+  parentName?: string;
+  parentId?: string | number;
+  children?: MenuForm[];
+  menuId?: string | number;
+  menuName: string;
+  orderNum: number;
+  path: string;
+  component?: string;
+  queryParam?: string;
+  isFrame?: string;
+  isCache?: string;
+  menuType?: MenuTypeEnum;
+  visible?: string;
+  status?: string;
+  icon?: string;
+  remark?: string;
+  query?: string;
+  perms?: string;
+}

+ 45 - 0
template/api/system/notice/index.ts

@@ -0,0 +1,45 @@
+import request from '@/utils/request';
+import { NoticeForm, NoticeQuery, NoticeVO } from './types';
+import { AxiosPromise } from 'axios';
+// 查询公告列表
+export function listNotice(query: NoticeQuery): AxiosPromise<NoticeVO[]> {
+  return request({
+    url: '/system/notice/list',
+    method: 'get',
+    params: query
+  });
+}
+
+// 查询公告详细
+export function getNotice(noticeId: string | number): AxiosPromise<NoticeVO> {
+  return request({
+    url: '/system/notice/' + noticeId,
+    method: 'get'
+  });
+}
+
+// 新增公告
+export function addNotice(data: NoticeForm) {
+  return request({
+    url: '/system/notice',
+    method: 'post',
+    data: data
+  });
+}
+
+// 修改公告
+export function updateNotice(data: NoticeForm) {
+  return request({
+    url: '/system/notice',
+    method: 'put',
+    data: data
+  });
+}
+
+// 删除公告
+export function delNotice(noticeId: string | number | Array<string | number>) {
+  return request({
+    url: '/system/notice/' + noticeId,
+    method: 'delete'
+  });
+}

+ 26 - 0
template/api/system/notice/types.ts

@@ -0,0 +1,26 @@
+export interface NoticeVO extends BaseEntity {
+  noticeId: number;
+  noticeTitle: string;
+  noticeType: string;
+  noticeContent: string;
+  status: string;
+  remark: string;
+  createByName: string;
+}
+
+export interface NoticeQuery extends PageQuery {
+  noticeTitle: string;
+  createByName: string;
+  status: string;
+  noticeType: string;
+}
+
+export interface NoticeForm {
+  noticeId: number | string | undefined;
+  noticeTitle: string;
+  noticeType: string;
+  noticeContent: string;
+  status: string;
+  remark: string;
+  createByName: string;
+}

+ 28 - 0
template/api/system/oss/index.ts

@@ -0,0 +1,28 @@
+import request from '@/utils/request';
+import { OssQuery, OssVO } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询OSS对象存储列表
+export function listOss(query: OssQuery): AxiosPromise<OssVO[]> {
+  return request({
+    url: '/resource/oss/list',
+    method: 'get',
+    params: query
+  });
+}
+
+// 查询OSS对象基于id串
+export function listByIds(ossId: string | number): AxiosPromise<OssVO[]> {
+  return request({
+    url: '/resource/oss/listByIds/' + ossId,
+    method: 'get'
+  });
+}
+
+// 删除OSS对象存储
+export function delOss(ossId: string | number | Array<string | number>) {
+  return request({
+    url: '/resource/oss/' + ossId,
+    method: 'delete'
+  });
+}

+ 22 - 0
template/api/system/oss/types.ts

@@ -0,0 +1,22 @@
+export interface OssVO extends BaseEntity {
+  ossId: string | number;
+  fileName: string;
+  originalName: string;
+  fileSuffix: string;
+  url: string;
+  createByName: string;
+  service: string;
+}
+
+export interface OssQuery extends PageQuery {
+  fileName: string;
+  originalName: string;
+  fileSuffix: string;
+  createTime: string;
+  service: string;
+  orderByColumn: string;
+  isAsc: string;
+}
+export interface OssForm {
+  file: undefined | string;
+}

+ 60 - 0
template/api/system/ossConfig/index.ts

@@ -0,0 +1,60 @@
+import request from '@/utils/request';
+import { OssConfigForm, OssConfigQuery, OssConfigVO } from './types';
+import { AxiosPromise } from 'axios';
+
+// 查询对象存储配置列表
+export function listOssConfig(query: OssConfigQuery): AxiosPromise<OssConfigVO[]> {
+  return request({
+    url: '/resource/oss/config/list',
+    method: 'get',
+    params: query
+  });
+}
+
+// 查询对象存储配置详细
+export function getOssConfig(ossConfigId: string | number): AxiosPromise<OssConfigVO> {
+  return request({
+    url: '/resource/oss/config/' + ossConfigId,
+    method: 'get'
+  });
+}
+
+// 新增对象存储配置
+export function addOssConfig(data: OssConfigForm) {
+  return request({
+    url: '/resource/oss/config',
+    method: 'post',
+    data: data
+  });
+}
+
+// 修改对象存储配置
+export function updateOssConfig(data: OssConfigForm) {
+  return request({
+    url: '/resource/oss/config',
+    method: 'put',
+    data: data
+  });
+}
+
+// 删除对象存储配置
+export function delOssConfig(ossConfigId: string | number | Array<string | number>) {
+  return request({
+    url: '/resource/oss/config/' + ossConfigId,
+    method: 'delete'
+  });
+}
+
+// 对象存储状态修改
+export function changeOssConfigStatus(ossConfigId: string | number, status: string, configKey: string) {
+  const data = {
+    ossConfigId,
+    status,
+    configKey
+  };
+  return request({
+    url: '/resource/oss/config/changeStatus',
+    method: 'put',
+    data: data
+  });
+}

+ 38 - 0
template/api/system/ossConfig/types.ts

@@ -0,0 +1,38 @@
+export interface OssConfigVO extends BaseEntity {
+  ossConfigId: number | string;
+  configKey: string;
+  accessKey: string;
+  secretKey: string;
+  bucketName: string;
+  prefix: string;
+  endpoint: string;
+  domain: string;
+  isHttps: string;
+  region: string;
+  status: string;
+  ext1: string;
+  remark: string;
+  accessPolicy: string;
+}
+
+export interface OssConfigQuery extends PageQuery {
+  configKey: string;
+  bucketName: string;
+  status: string;
+}
+
+export interface OssConfigForm {
+  ossConfigId: string | number | undefined;
+  configKey: string;
+  accessKey: string;
+  secretKey: string;
+  bucketName: string;
+  prefix: string;
+  endpoint: string;
+  domain: string;
+  isHttps: string;
+  accessPolicy: string;
+  region: string;
+  status: string;
+  remark: string;
+}

+ 9 - 0
template/api/system/platform/index.ts

@@ -0,0 +1,9 @@
+import request from '@/utils/request';
+
+// 查询平台列表
+export function listPlatform() {
+  return request({
+    url: '/system/platform/list',
+    method: 'get'
+  });
+}

+ 5 - 0
template/api/system/platform/types.ts

@@ -0,0 +1,5 @@
+// 平台类型
+export interface PlatformVO {
+  id: number;
+  label: string;
+}

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