Browse Source

下单初步完成

Huanyi 1 tháng trước cách đây
mục cha
commit
18859e3695

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

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

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

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

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

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

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

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

@@ -87,5 +87,6 @@ export interface ServiceOrderVO {
   id: number;
   name: string;
   remark: string;
+  icon: string;
   mode: number;
 }

+ 31 - 0
src/mock/RiderOrdersDialog.json

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

+ 199 - 0
src/mock/dispatch.json

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

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

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

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

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

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

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

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

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

+ 63 - 1169
src/views/order/dispatch/index.vue

@@ -41,338 +41,36 @@
 
       <!-- Right: Dispatch Control Panel -->
       <div class="right-panel">
-        <!-- 1. Order Management Section -->
-        <div class="panel-section order-mgmt">
-          <div class="sec-header">
-            <span class="tit">订单</span>
-            <!-- Right Aligned Tabs -->
-            <div class="header-right-tabs">
-              <span class="h-tab-item" :class="{ active: currentOrderTab === 'PendingDispatch' }" @click="currentOrderTab = 'PendingDispatch'">
-                <span class="txt">待派单</span>
-                <span class="num danger">{{ orderStats.pendingDispatch }}</span>
-              </span>
-              <span class="h-tab-item" :class="{ active: currentOrderTab === 'PendingAccept' }" @click="currentOrderTab = 'PendingAccept'">
-                <span class="txt">待接单</span>
-                <span class="num warning">{{ orderStats.pendingAccept }}</span>
-              </span>
-              <span class="h-tab-item" :class="{ active: currentOrderTab === 'Processing' }" @click="currentOrderTab = 'Processing'">
-                <span class="txt">进行中</span>
-                <span class="num primary">{{ orderStats.processing }}</span>
-              </span>
-            </div>
-          </div>
-
-          <!-- Order List -->
-          <div class="list-wrapper">
-            <el-scrollbar>
-              <div v-if="filteredOrders.length === 0" class="empty-state">暂无数据</div>
-              <div v-else v-for="order in filteredOrders" :key="order.id" class="list-card order-card" @click="focusMapPoint(order.lng, order.lat)">
-                <div class="card-left">
-                  <div class="type-tag" :class="order.typeCode">
-                    {{ getShortType(order.typeCode) }}
-                  </div>
-                </div>
-                <div class="card-main">
-                  <template v-if="order.typeCode === 'transport'">
-                    <div class="row-addr" :title="order.pickAddr"><span class="tag pick">取</span> {{ order.pickAddr }}</div>
-                    <div class="row-addr" :title="order.dropAddr"><span class="tag drop">送</span> {{ order.dropAddr }}</div>
-                    <div class="row-time">
-                      <el-icon><Clock /></el-icon> {{ order.time }}
-                      <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
-                    </div>
-                  </template>
-                  <template v-else>
-                    <div class="row-addr" :title="order.address"><span class="tag home">址</span> {{ order.address }}</div>
-                    <div class="row-time" style="margin-top: 4px">
-                      <el-icon><Clock /></el-icon> {{ order.time }}
-                      <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
-                    </div>
-                  </template>
-                </div>
-
-                <!-- Right: Status & Actions -->
-                <div class="card-right">
-                  <el-tag size="small" :type="getOrderStatusType(order.status)" effect="plain">{{ getOrderStatusText(order.status) }}</el-tag>
-                  <div class="actions">
-                    <el-button v-if="order.status === 'pending_dispatch'" type="primary" size="small" @click.stop="openDispatchDialog(order)"
-                      >派单</el-button
-                    >
-                    <el-button
-                      v-else-if="['pending_accept', 'processing'].includes(order.status)"
-                      type="primary"
-                      size="small"
-                      plain
-                      @click.stop="openDispatchDialog(order)"
-                      >重新派单</el-button
-                    >
-                  </div>
-                </div>
-              </div>
-            </el-scrollbar>
-          </div>
-        </div>
-
-        <!-- 2. Fulfiller Management Section -->
-        <div class="panel-section fulfiller-mgmt">
-          <div class="sec-header no-border">
-            <span class="tit">履约者</span>
-            <!-- Right Aligned Tabs -->
-            <div class="header-right-tabs">
-              <span class="h-tab-item" :class="{ active: currentRiderTab === 'All' }" @click="currentRiderTab = 'All'">
-                <span class="txt">全部</span>
-                <span class="num">12</span>
-              </span>
-              <span class="h-tab-item" :class="{ active: currentRiderTab === 'Working' }" @click="currentRiderTab = 'Working'">
-                <span class="txt">接单中</span>
-                <span class="num success">8</span>
-              </span>
-              <span class="h-tab-item" :class="{ active: currentRiderTab === 'Resting' }" @click="currentRiderTab = 'Resting'">
-                <span class="txt">休息中</span>
-                <span class="num info">3</span>
-              </span>
-              <span class="h-tab-item" :class="{ active: currentRiderTab === 'Disabled' }" @click="currentRiderTab = 'Disabled'">
-                <span class="txt">禁用</span>
-                <span class="num danger">1</span>
-              </span>
-            </div>
-          </div>
-
-          <!-- Rider List -->
-          <div class="list-wrapper">
-            <el-scrollbar>
-              <div v-for="rider in filteredRiders" :key="rider.id" class="list-card rider-card" @click="focusMapPoint(rider.lng, rider.lat)">
-                <div class="card-left relative">
-                  <el-avatar :src="rider.avatar" :size="40" />
-                  <div class="dot" :class="rider.status"></div>
-                </div>
-                <div class="card-main">
-                  <!-- Box 1: Name + Phone + Status (Right) -->
-                  <div class="row-1" style="justify-content: space-between; align-items: flex-start">
-                    <div style="display: flex; align-items: baseline; gap: 8px">
-                      <span class="r-name">{{ rider.name }}</span>
-                      <span class="r-phone">{{ rider.maskPhone }}</span>
-                    </div>
-                    <div class="status-right">
-                      <span class="status-badge" :class="rider.status">{{ getRiderStatusText(rider.status) }}</span>
-                    </div>
-                  </div>
-
-                  <!-- Box 2: Categories -->
-                  <div class="row-2 categories-row" style="margin-top: 6px; display: flex; gap: 4px; flex-wrap: wrap">
-                    <span v-for="cat in rider.categories" :key="cat" class="cat-tag" :class="getCategoryClass(cat)">{{ cat }}</span>
-                  </div>
-
-                  <!-- Box 3: Last Service Time -->
-                  <div class="row-3 time-row" style="margin-top: 4px">
-                    <span class="last-time"
-                      >下一单: {{ rider.status === 'offline' || rider.status === 'disabled' ? '--' : rider.lastServiceTime }}</span
-                    >
-                  </div>
-                </div>
-                <div class="card-right-stats">
-                  <div class="stat-box">
-                    <span class="lbl">待接</span>
-                    <span class="val danger">{{ rider.pendingCount }}</span>
-                  </div>
-                  <div class="stat-box">
-                    <span class="lbl">待服</span>
-                    <span class="val warning">{{ rider.todoCount }}</span>
-                  </div>
-                  <el-button link type="primary" size="small" style="margin-top: 4px; padding: 0" @click.stop="handleViewRiderOrders(rider)"
-                    >查看订单</el-button
-                  >
-                </div>
-              </div>
-            </el-scrollbar>
-          </div>
-        </div>
+        <OrderListPanel
+          v-model="currentOrderTab"
+          :orders="filteredOrders"
+          :stats="orderStats"
+          @focus="focusMapPoint"
+          @dispatch="openDispatchDialog"
+        />
+
+        <RiderListPanel
+          v-model="currentRiderTab"
+          :riders="filteredRiders"
+          @focus="focusMapPoint"
+          @view-orders="handleViewRiderOrders"
+        />
       </div>
     </div>
 
-    <!-- Rider Orders Dialog -->
-    <el-dialog v-model="riderOrdersVisible" title="履约者订单详情" width="1100px" top="6vh" custom-class="rider-orders-dialog">
-      <div class="dialog-content">
-        <!-- 1. Rider Summary Info -->
-        <div class="rider-summary" v-if="currentRiderInfo">
-          <div class="summary-item">
-            <span class="lbl">姓名:</span>
-            <span class="val bold">{{ currentRiderInfo.name }}</span>
-          </div>
-          <div class="summary-item">
-            <span class="lbl">手机号:</span>
-            <span class="val">{{ currentRiderInfo.maskPhone }}</span>
-          </div>
-          <div class="summary-item">
-            <span class="lbl">所属区域:</span>
-            <span class="val">{{ currentRiderInfo.station }}</span>
-          </div>
-          <div class="summary-item">
-            <span class="lbl">工作类型:</span>
-            <span class="val">{{ currentRiderInfo.workType }}</span>
-          </div>
-          <div class="summary-item" style="flex: 1">
-            <span class="lbl">服务类目:</span>
-            <el-tag v-for="tag in currentRiderInfo.categories" :key="tag" size="small" effect="plain" style="margin-right: 4px">{{ tag }}</el-tag>
-          </div>
-        </div>
-
-        <!-- 2. Orders Table -->
-        <div class="rider-orders-list">
-          <el-table :data="currentRiderOrders" stripe style="width: 100%" :header-cell-style="{ background: '#f5f7fa', color: '#606266' }">
-            <el-table-column prop="orderNo" label="订单号" width="150" />
-            <el-table-column prop="serviceName" label="服务项目" width="120" show-overflow-tooltip />
-            <el-table-column prop="petName" label="服务宠物" width="100" />
-            <el-table-column prop="customerName" label="客户" width="100" />
-            <el-table-column prop="address" label="服务详细地址" min-width="200" show-overflow-tooltip />
-            <el-table-column prop="time" label="预约时间" width="160" />
-            <el-table-column label="状态" width="100">
-              <template #default="{ row }">
-                <el-tag v-if="row.status === 'pending_accept'" type="warning" size="small">待接单</el-tag>
-                <el-tag v-else-if="row.status === 'pending_service'" type="primary" size="small">待服务</el-tag>
-                <el-tag v-else type="info" size="small">{{ row.status }}</el-tag>
-              </template>
-            </el-table-column>
-          </el-table>
-        </div>
-      </div>
-    </el-dialog>
-
-    <!-- Dispatch Dialog -->
-    <el-dialog v-model="dispatchDialogVisible" title="派单调度" width="900px" top="5vh" destroy-on-close append-to-body>
-      <div class="dispatch-dialog-content">
-        <!-- Top: Order Info -->
-        <div class="dispatch-order-info box-card" v-if="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" :title="currentDispatchOrder.pickAddr">
-                  <span class="tag pick">取</span> {{ currentDispatchOrder.pickAddr }}
-                </div>
-                <div class="row-addr" :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 (For Re-dispatch) -->
-        <div class="current-rider-section" v-if="currentRider">
-          <div class="select-header" style="margin-bottom: 8px">
-            <span class="tit">当前派单履约者</span>
-          </div>
-          <div class="list-card rider-card" style="margin-bottom: 20px; border: 1px solid #e4e7ed; background: #fafafa; cursor: default">
-            <!-- Reusing Rider Card Layout -->
-            <div class="card-left relative">
-              <el-avatar :src="currentRider.avatar" :size="40" />
-              <div class="dot" :class="currentRider.status"></div>
-            </div>
-            <div class="card-main">
-              <div class="row-1" style="justify-content: space-between; align-items: flex-start">
-                <div style="display: flex; align-items: baseline; gap: 8px">
-                  <span class="r-name">{{ currentRider.name }}</span>
-                  <span class="r-phone">{{ currentRider.maskPhone }}</span>
-                </div>
-                <div class="status-right">
-                  <span class="status-badge" :class="currentRider.status">{{ getRiderStatusText(currentRider.status) }}</span>
-                </div>
-              </div>
-
-              <div class="row-2 categories-row" style="margin-top: 6px; display: flex; gap: 4px; flex-wrap: wrap">
-                <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">
-                      <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>
+    <RiderOrdersDialog
+      v-model="riderOrdersVisible"
+      :riderInfo="currentRiderInfo"
+      :orders="currentRiderOrders"
+    />
+
+    <DispatchDialog
+      v-model="dispatchDialogVisible"
+      :order="currentDispatchOrder"
+      :currentRider="currentRider"
+      :ridersList="ridersList"
+      @submit="handleDispatchSubmit"
+    />
   </div>
 </template>
 
@@ -380,53 +78,16 @@
 import { ref, computed, reactive, onMounted, watch } from 'vue';
 import { ElMessage } from 'element-plus';
 
-// --- Data & State ---
-const riderOrdersVisible = ref(false);
-const currentRiderOrders = ref([]);
-const currentRiderInfo = ref(null);
+import OrderListPanel from './components/OrderListPanel.vue';
+import RiderListPanel from './components/RiderListPanel.vue';
+import RiderOrdersDialog from './components/RiderOrdersDialog.vue';
+import DispatchDialog from './components/DispatchDialog.vue';
 
-const handleViewRiderOrders = (rider) => {
-  // Mock Rider Info
-  currentRiderInfo.value = {
-    name: rider.name,
-    maskPhone: rider.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'),
-    station: rider.station + ' - 东城片区',
-    workType: '全职专送',
-    categories: ['宠物接送', '上门喂遛', '洗护套餐']
-  };
+// Mock Data
+import dispatchMockData from '@/mock/dispatch.json';
+import riderOrdersMockData from '@/mock/RiderOrdersDialog.json';
 
-  // Mock Data for specific rider
-  currentRiderOrders.value = [
-    {
-      orderNo: 'ORD20240205001',
-      serviceName: '上门喂遛',
-      petName: '金毛(大黄)',
-      customerName: '张先生',
-      address: '北京市朝阳区三里屯SOHO A座 1202',
-      time: '2024-02-05 14:00',
-      status: 'pending_accept'
-    },
-    {
-      orderNo: 'ORD20240205002',
-      serviceName: '宠物接送',
-      petName: '布偶(咪咪)',
-      customerName: '李女士',
-      address: '北京市朝阳区国贸三期 35层',
-      time: '2024-02-05 16:30',
-      status: 'pending_service'
-    },
-    {
-      orderNo: 'ORD20240205003',
-      serviceName: '洗护套餐',
-      petName: '泰迪(可乐)',
-      customerName: '王小姐',
-      address: '北京市海淀区中关村软件园 5号楼',
-      time: '2024-02-06 10:00',
-      status: 'pending_service'
-    }
-  ];
-  riderOrdersVisible.value = true;
-};
+// --- Data & State ---
 const filters = reactive({
   orderType: 'all',
   city: 'beijing',
@@ -437,180 +98,29 @@ const currentOrderTab = ref('PendingDispatch');
 const currentRiderTab = ref('All');
 const activeMapFilter = ref('all');
 
-// Mock Stats
-const orderStats = reactive({
-  pendingDispatch: 3,
-  pendingAccept: 4,
-  processing: 8,
-  delivering: 2,
-  merchants: 16
-});
-
-// Real Params (Beijing Chaoyang Area)
-// Center: 116.4551, 39.9255
+const ordersList = ref(dispatchMockData.ordersList);
+const ridersList = ref(dispatchMockData.ridersList);
+const merchantList = ref(dispatchMockData.merchantList);
+const orderStats = reactive(dispatchMockData.stats);
 
-// Mock Orders Data (With Real Coords)
-const ordersList = ref([
-  {
-    id: 1,
-    type: '宠物接送',
-    typeCode: 'transport',
-    status: 'pending_dispatch',
-    pickAddr: '北京市朝阳区三里屯SOHO A座',
-    dropAddr: '北京市朝阳区朝阳大悦城',
-    time: '2024-02-07 14:30',
-    daysLater: '今天',
-    lng: 116.4552,
-    lat: 39.9338
-  },
-  {
-    id: 2,
-    type: '上门喂遛',
-    typeCode: 'feeding',
-    status: 'pending_accept',
-    address: '北京市海淀区中关村软件园',
-    riderId: 101,
-    time: '2024-02-08 15:00',
-    daysLater: '明天',
-    lng: 116.298,
-    lat: 40.044
-  },
-  {
-    id: 3,
-    type: '上门洗护',
-    typeCode: 'washing',
-    status: 'processing',
-    address: '北京市朝阳区望京SOHO T3',
-    riderId: 102,
-    time: '2024-02-07 10:00',
-    daysLater: '进行中',
-    lng: 116.486,
-    lat: 39.998
-  },
-  {
-    id: 4,
-    type: '宠物接送',
-    typeCode: 'transport',
-    status: 'completed',
-    pickAddr: '北京市通州区万达广场',
-    dropAddr: '北京市通州区北苑',
-    time: '2024-02-06 09:00',
-    daysLater: '',
-    lng: 116.643,
-    lat: 39.905
-  },
-  {
-    id: 5,
-    type: '上门喂遛',
-    typeCode: 'feeding',
-    status: 'pending_dispatch',
-    address: '北京市朝阳区朝阳公园南门',
-    time: '2024-02-09 16:00',
-    daysLater: '2天后',
-    lng: 116.488,
-    lat: 39.932
-  },
-  {
-    id: 6,
-    type: '宠物接送',
-    typeCode: 'transport',
-    status: 'pending_dispatch',
-    pickAddr: '北京市朝阳区国贸三期',
-    dropAddr: '北京市朝阳区双井富力城',
-    time: '2024-02-07 16:30',
-    daysLater: '今天',
-    lng: 116.463,
-    lat: 39.907
-  }
-]);
-
-// Mock Riders Data (With Real Coords)
-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.46,
-    lat: 39.92
-  },
-  {
-    id: 102,
-    name: '李小龙',
-    station: '海淀站',
-    phone: '13912345678',
-    maskPhone: '139****5678',
-    status: 'online',
-    categories: ['接送', '洗护'],
-    lastServiceTime: '2024-02-07 10:30',
-    avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
-    pendingCount: 1,
-    todoCount: 3,
-    lng: 116.45,
-    lat: 39.915
-  },
-  {
-    id: 103,
-    name: '张小美',
-    station: '望京站',
-    phone: '13666666666',
-    maskPhone: '136****6666',
-    status: 'online',
-    categories: ['喂遛'],
-    lastServiceTime: '2024-02-07 10:45',
-    avatar: '',
-    pendingCount: 0,
-    todoCount: 0,
-    lng: 116.47,
-    lat: 39.93
-  },
-  {
-    id: 104,
-    name: '赵铁柱',
-    station: '通州站',
-    phone: '13555555555',
-    maskPhone: '135****5555',
-    status: 'offline',
-    categories: ['接送'],
-    lastServiceTime: '2024-02-06 18:00',
-    avatar: '',
-    pendingCount: 0,
-    todoCount: 0,
-    lng: 116.44,
-    lat: 39.91
-  },
-  {
-    id: 105,
-    name: '孙悟空',
-    station: '花果山',
-    phone: '13888888888',
-    maskPhone: '138****8888',
-    status: 'online',
-    categories: ['接送', '喂遛', '洗护'],
-    lastServiceTime: '2024-02-07 09:30',
-    avatar: '',
-    pendingCount: 0,
-    todoCount: 1,
-    lng: 116.48,
-    lat: 39.925
-  }
-]);
+// Rider Orders State
+const riderOrdersVisible = ref(false);
+const currentRiderInfo = ref(null);
+const currentRiderOrders = ref([]);
 
-// Mock Merchants (Static Coords)
-const merchantList = ref([
-  { id: 201, name: '萌它宠物', orders: 5, lng: 116.453, lat: 39.923, icon: '' },
-  { id: 202, name: '宠爱国际', orders: 0, lng: 116.465, lat: 39.928, icon: '' },
-  { id: 203, name: '顽皮狗', orders: 8, lng: 116.448, lat: 39.918, icon: '' }
-]);
+const handleViewRiderOrders = (rider) => {
+  currentRiderInfo.value = {
+    name: rider.name,
+    maskPhone: rider.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'),
+    station: rider.station + ' - 东城片区',
+    workType: '全职专送',
+    categories: ['宠物接送', '上门喂遛', '洗护套餐']
+  };
+  currentRiderOrders.value = riderOrdersMockData.orders;
+  riderOrdersVisible.value = true;
+};
 
-// --- Map Logic ---
+// Map Logic
 let map = null;
 const ak = 'E4805d16520de693a3fe707cdc962045'; // Public Key
 
@@ -658,12 +168,12 @@ const refreshMarkers = () => {
                 <div style="background:#fff; border-radius:8px; padding: 12px; display: flex; align-items: center; gap: 10px; position: relative;">
                      <!-- Close Icon -->
                     <div style="position: absolute; top: 4px; right: 8px; color: #999; font-size: 16px; cursor: pointer;">×</div>
-
+                    
                     <!-- Icon Ring -->
                     <div style="width: 48px; height: 48px; border-radius: 50%; border: 4px solid #F56C6C; padding: 2px; flex-shrink: 0; box-sizing: border-box; display: flex; align-items: center; justify-content: center;">
                          <img src="${iconImg}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;">
                     </div>
-
+                    
                     <!-- Info -->
                     <div style="flex: 1; overflow: hidden;">
                         <div style="font-weight: bold; font-size: 14px; color: #333; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${m.name}</div>
@@ -695,12 +205,12 @@ const refreshMarkers = () => {
                 <div style="background:#fff; border-radius:8px; padding: 10px; display: flex; align-items: center; gap: 10px; position: relative;">
                     <!-- Close Icon -->
                     <div style="position: absolute; top: 2px; right: 6px; color: #999; font-size: 14px; cursor: pointer;">×</div>
-
+                    
                     <!-- Avatar Ring -->
                     <div style="width: 44px; height: 44px; border-radius: 50%; border: 3px solid ${borderColor}; padding: 1px; flex-shrink: 0; box-sizing: border-box;">
                         <img src="${avatar}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;">
                     </div>
-
+                    
                     <!-- Info -->
                     <div style="flex: 1; overflow: hidden;">
                         <div style="font-weight: bold; font-size: 13px; color: #333; margin-bottom: 2px;">[履约者]${r.name}</div>
@@ -749,7 +259,6 @@ watch(activeMapFilter, () => {
   refreshMarkers();
 });
 
-// Also refresh when tab changes if logic depends on it (e.g. only showing active orders)
 watch([currentOrderTab, currentRiderTab], () => {
   refreshMarkers();
 });
@@ -765,68 +274,33 @@ onMounted(() => {
     });
 });
 
+// Dispatch Dialog State
 const dispatchDialogVisible = ref(false);
 const currentDispatchOrder = ref(null);
 const currentRider = ref(null);
-const dispatchSearchQuery = ref('');
-const selectedRiderId = ref(null);
-const dispatchFee = ref(0);
 
 const openDispatchDialog = (order) => {
   currentDispatchOrder.value = order;
-  dispatchDialogVisible.value = true;
-  dispatchSearchQuery.value = '';
-  selectedRiderId.value = null;
-  dispatchFee.value = 0;
-
-  // Find current rider for re-dispatch
   if (order.riderId) {
     currentRider.value = ridersList.value.find((r) => r.id === order.riderId) || null;
   } else {
     currentRider.value = null;
   }
+  dispatchDialogVisible.value = true;
 };
 
-const filteredDispatchRiders = computed(() => {
-  // User said "Select Accepting Riders". Usually means "Online/Busy".
-  // But the main list logic for "Working" includes online and busy.
-  // Let's filter for riders who are NOT disabled or offline?
-  // Or just show all eligible. Let's show 'online' and 'busy'.
-  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));
-  }
-  // Sort by Next Order Time
-  // Mock sort: just string compare for now since format is YYYY-MM-DD HH:mm
-  result.sort((a, b) => {
-    return a.lastServiceTime.localeCompare(b.lastServiceTime);
-  });
-  return result;
-});
-
-const handleDispatchSubmit = () => {
-  if (!selectedRiderId.value) {
-    ElMessage.warning('请选择履约者');
-    return;
-  }
-  if (!dispatchFee.value) {
-    ElMessage.warning('请输入服务费用');
-    return;
-  }
+const handleDispatchSubmit = (data) => {
   dispatchDialogVisible.value = false;
   ElMessage.success('派单成功');
 
-  // update status locally
   if (currentDispatchOrder.value && currentDispatchOrder.value.status === 'pending_dispatch') {
     const idx = ordersList.value.findIndex((o) => o.id === currentDispatchOrder.value.id);
     if (idx !== -1) ordersList.value[idx].status = 'pending_accept';
   }
 };
+
 const filteredOrders = computed(() => {
   let result = ordersList.value;
-  // Tab Filter
   const statusMap = {
     'PendingDispatch': 'pending_dispatch',
     'PendingAccept': 'pending_accept',
@@ -836,7 +310,6 @@ const filteredOrders = computed(() => {
   if (targetStatus) {
     result = result.filter((o) => o.status === targetStatus);
   }
-  // Type Filter
   if (filters.orderType !== 'all') {
     result = result.filter((o) => o.typeCode === filters.orderType);
   }
@@ -853,29 +326,6 @@ const filteredRiders = computed(() => {
   const allowed = map[currentRiderTab.value] || [];
   return ridersList.value.filter((r) => allowed.includes(r.status));
 });
-
-// --- Helpers ---
-const getShortType = (code) => {
-  const map = { 'transport': '接送', 'feeding': '喂遛', 'washing': '洗护' };
-  return map[code] || '订单';
-};
-const getOrderTabLabel = (key) => map[key]; // Simplified in template
-const getOrderStatusText = (status) => {
-  const map = { 'pending_dispatch': '待派单', 'pending_accept': '待接单', 'processing': '进行中', 'completed': '已完成' };
-  return map[status];
-};
-const getOrderStatusType = (status) => {
-  const map = { 'pending_dispatch': 'danger', 'pending_accept': 'warning', 'processing': 'primary', 'completed': 'success' };
-  return map[status];
-};
-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] || '';
-};
 </script>
 
 <style scoped>
@@ -984,560 +434,4 @@ const getCategoryClass = (cat) => {
   box-shadow: -4px 0 16px rgba(0, 0, 0, 0.05);
   z-index: 30;
 }
-.panel-section {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  overflow: hidden;
-}
-.order-mgmt {
-  border-bottom: 8px solid #f5f7fa;
-  flex: default;
-  height: 50%;
-}
-.fulfiller-mgmt {
-  flex: default;
-  height: 50%;
-}
-
-.sec-header {
-  height: 48px;
-  padding: 0 16px;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  border-bottom: 1px solid #f0f0f0;
-}
-.sec-header.no-border {
-  border-bottom: none;
-  height: 40px;
-}
-.sec-header .tit {
-  font-weight: bold;
-  font-size: 15px;
-  color: #1f2f3d;
-}
-
-/* New Header Tabs Style */
-.header-tabs-wrapper {
-  display: flex;
-  padding: 0 16px 8px;
-  gap: 12px;
-  border-bottom: 1px solid #f5f7fa;
-}
-.header-tab {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  cursor: pointer;
-  padding: 4px 8px;
-  border-radius: 6px;
-  transition: all 0.2s;
-  min-width: 60px;
-}
-.header-tab:hover {
-  background: #f5f7fa;
-}
-.header-tab.active {
-  background: #ecf5ff;
-}
-.header-tab.active .lbl {
-  color: #409eff;
-  font-weight: bold;
-}
-
-.header-tab .num {
-  font-size: 16px;
-  font-weight: bold;
-  line-height: 1.2;
-}
-.header-tab .lbl {
-  font-size: 12px;
-  color: #909399;
-  margin-top: 2px;
-}
-
-.header-tab .num.danger {
-  color: #f56c6c;
-}
-.header-tab .num.warning {
-  color: #e6a23c;
-}
-.header-tab .num.primary {
-  color: #409eff;
-}
-.header-tab .num.success {
-  color: #67c23a;
-}
-
-.fulfiller-tabs .header-tab {
-  flex-direction: row;
-  gap: 4px;
-  min-width: auto;
-}
-.fulfiller-tabs .header-tab .num {
-  font-size: 14px;
-}
-.fulfiller-tabs .header-tab .lbl {
-  margin-top: 0;
-}
-
-/* Header Right Tabs (Orders) */
-.header-right-tabs {
-  display: flex;
-  gap: 4px;
-}
-.h-tab-item {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: center;
-  cursor: pointer;
-  position: relative;
-  padding: 6px 12px;
-  gap: 6px;
-  border-radius: 4px;
-  transition: all 0.2s;
-  color: #606266;
-}
-.h-tab-item:hover {
-  background: #f5f7fa;
-}
-.h-tab-item.active {
-  background: #ecf5ff;
-}
-.h-tab-item.active .txt {
-  color: #409eff;
-  font-weight: bold;
-}
-.h-tab-item.active::after {
-  display: none;
-}
-
-.h-tab-item .txt {
-  font-size: 14px;
-}
-.h-tab-item .num {
-  font-size: 14px;
-  font-weight: bold;
-  margin-bottom: 0;
-}
-.h-tab-item .num.danger {
-  color: #f56c6c;
-}
-.h-tab-item .num.warning {
-  color: #e6a23c;
-}
-.h-tab-item .num.primary {
-  color: #409eff;
-}
-
-/* List */
-.list-wrapper {
-  flex: 1;
-  overflow: hidden;
-  padding: 8px 12px;
-}
-.empty-state {
-  text-align: center;
-  color: #909399;
-  padding: 20px;
-  font-size: 13px;
-}
-
-.list-card {
-  background: #fff;
-  border: 1px solid #ebeef5;
-  border-radius: 8px;
-  padding: 12px;
-  margin-bottom: 10px;
-  display: flex;
-  align-items: stretch;
-  gap: 12px;
-  transition: all 0.2s;
-  cursor: pointer;
-}
-.list-card:hover {
-  border-color: #c6e2ff;
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
-}
-
-.card-left {
-  flex-shrink: 0;
-  display: flex;
-  align-items: center;
-}
-.order-card .type-tag {
-  width: 40px;
-  height: 40px;
-  border-radius: 8px;
-  color: #fff;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 12px;
-  font-weight: bold;
-}
-.type-tag.transport {
-  background: #e6a23c;
-}
-.type-tag.feeding {
-  background: #67c23a;
-}
-.type-tag.washing {
-  background: #409eff;
-}
-
-.card-main {
-  flex: 1;
-  overflow: hidden;
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  gap: 4px;
-}
-.row-1 {
-  display: flex;
-  align-items: center;
-}
-.o-type {
-  font-weight: bold;
-  font-size: 14px;
-  color: #303133;
-}
-.row-2 {
-  font-size: 12px;
-  color: #606266;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-.row-3 {
-  font-size: 12px;
-  color: #909399;
-  display: flex;
-  align-items: center;
-  gap: 4px;
-}
-
-.card-right {
-  display: flex;
-  flex-direction: column;
-  align-items: flex-end;
-  justify-content: center;
-  gap: 8px;
-  margin-left: 8px;
-  flex-shrink: 0;
-}
-.card-right .actions {
-  display: flex;
-  align-items: center;
-  gap: 4px;
-}
-
-.rider-card .card-left .dot {
-  position: absolute;
-  bottom: 0;
-  right: 0;
-  width: 10px;
-  height: 10px;
-  border-radius: 50%;
-  border: 2px solid #fff;
-}
-.dot.online {
-  background: #67c23a;
-}
-.dot.busy {
-  background: #409eff;
-}
-.dot.offline {
-  background: #909399;
-}
-
-.r-name {
-  font-weight: bold;
-  font-size: 14px;
-  color: #303133;
-  margin-right: 8px;
-}
-.r-station {
-  font-size: 12px;
-  color: #909399;
-  background: #f4f4f5;
-  padding: 1px 4px;
-  border-radius: 2px;
-}
-.status-desc.online {
-  color: #67c23a;
-}
-.status-desc.busy {
-  color: #409eff;
-}
-.status-desc.offline {
-  color: #909399;
-}
-
-.card-right-stats {
-  display: flex;
-  flex-direction: column;
-  align-items: flex-end;
-  justify-content: center;
-  gap: 6px;
-}
-.stat-box {
-  font-size: 11px;
-  color: #909399;
-  display: flex;
-  align-items: center;
-  gap: 4px;
-}
-.stat-box .val {
-  font-weight: bold;
-  font-size: 13px;
-}
-.stat-box .val.danger {
-  color: #f56c6c;
-}
-.stat-box .val.warning {
-  color: #e6a23c;
-}
-
-.rider-summary {
-  display: flex;
-  align-items: center;
-  gap: 24px;
-  background: #fdfdfd;
-  border: 1px solid #ebeef5;
-  border-radius: 8px;
-  padding: 20px;
-  margin-bottom: 24px;
-  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
-  flex-wrap: nowrap;
-  overflow-x: auto;
-}
-.summary-item {
-  display: flex;
-  align-items: center;
-  font-size: 14px;
-  color: #606266;
-  white-space: nowrap;
-  flex-shrink: 0;
-}
-.summary-item .lbl {
-  color: #909399;
-  margin-right: 6px;
-}
-.summary-item .val {
-  color: #303133;
-  font-weight: 500;
-}
-.summary-item .val.bold {
-  font-weight: bold;
-  font-size: 16px;
-  color: #303133;
-}
-
-.rider-orders-list {
-  border: 1px solid #ebeef5;
-  border-radius: 8px;
-  overflow: hidden; /* Hide potential scrollbars from container */
-}
-
-/* Order Card New Layout */
-.row-addr {
-  font-size: 13px;
-  color: #303133;
-  display: flex;
-  align-items: center;
-  gap: 4px;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  line-height: 1.5;
-}
-.row-addr .tag {
-  font-size: 11px;
-  color: #fff;
-  padding: 1px 4px;
-  border-radius: 4px;
-  flex-shrink: 0;
-  transform: scale(0.9);
-}
-.tag.pick {
-  background: #409eff;
-}
-.tag.drop {
-  background: #e6a23c;
-}
-.tag.home {
-  background: #67c23a;
-}
-
-.row-time {
-  font-size: 12px;
-  color: #909399;
-  display: flex;
-  align-items: center;
-  gap: 4px;
-}
-.days-tag {
-  color: #f56c6c;
-  background: #fef0f0;
-  padding: 0 4px;
-  border-radius: 4px;
-  font-size: 11px;
-  border: 1px solid #fde2e2;
-  transform: scale(0.95);
-}
-
-/* Updated Rider Card Layout */
-.r-name {
-  font-size: 14px;
-  font-weight: bold;
-  color: #303133;
-}
-.r-phone {
-  font-size: 12px;
-  color: #909399;
-}
-.categories-right {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 4px;
-  justify-content: flex-end;
-  max-width: 100px;
-}
-
-.cat-tag {
-  background: #f4f4f5;
-  color: #909399;
-  font-size: 10px;
-  padding: 1px 4px;
-  border-radius: 2px;
-}
-.cat-tag.cat-transport {
-  background: #e6f7ff;
-  color: #1890ff;
-  border: 1px solid #91d5ff;
-}
-.cat-tag.cat-feeding {
-  background: #f6ffed;
-  color: #52c41a;
-  border: 1px solid #b7eb8f;
-}
-.cat-tag.cat-washing {
-  background: #fff0f6;
-  color: #eb2f96;
-  border: 1px solid #ffadd2;
-}
-
-.status-badge {
-  font-size: 11px;
-  padding: 2px 6px;
-  border-radius: 4px;
-  display: inline-block;
-  font-weight: bold;
-}
-.status-badge.online {
-  background: #f0f9eb;
-  color: #67c23a;
-}
-.status-badge.busy {
-  background: #ecf5ff;
-  color: #409eff;
-}
-.status-badge.offline {
-  background: #f4f4f5;
-  color: #909399;
-}
-.status-badge.disabled {
-  background: #fef0f0;
-  color: #f56c6c;
-}
-
-.last-time {
-  font-size: 11px;
-  color: #999;
-}
-
-/* Dispatch Dialog */
-.dispatch-order-info {
-  background: #f5f7fa;
-  padding: 10px;
-  border-radius: 4px;
-  margin-bottom: 20px;
-  border: 1px solid #e4e7ed;
-}
-.dispatch-rider-select .select-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 10px;
-}
-.dispatch-rider-select .tit {
-  font-weight: bold;
-  font-size: 14px;
-}
-
-.rider-grid {
-  display: grid;
-  grid-template-columns: repeat(2, 1fr);
-  gap: 12px;
-  padding-right: 10px;
-}
-.rider-card.select-card {
-  cursor: pointer;
-  border: 1px solid #dcdfe6;
-  position: relative;
-  transition: all 0.2s;
-  margin-bottom: 0; /* Override default list card margin */
-}
-.rider-card.select-card:hover {
-  border-color: #409eff;
-}
-.rider-card.select-card.active {
-  border-color: #409eff;
-  background-color: #ecf5ff;
-}
-.selected-mark {
-  position: absolute;
-  top: 0;
-  right: 0;
-  background: #409eff;
-  color: #fff;
-  border-bottom-left-radius: 6px;
-  width: 20px;
-  height: 20px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 12px;
-}
-
-.dispatch-footer {
-  margin-top: 20px;
-  padding-top: 20px;
-  border-top: 1px solid #ebeef5;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-.dispatch-footer .fee-input {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  font-size: 14px;
-}
-.empty-text {
-  text-align: center;
-  color: #909399;
-  padding: 20px;
-  width: 100%;
-  grid-column: span 2;
-}
-
-.card-main {
-  justify-content: flex-start;
-} /* Override default center */
 </style>

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

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

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

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

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

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

+ 209 - 51
src/views/order/purchase/index.vue

@@ -48,7 +48,10 @@
                       size="large"
                       style="width: 100%"
                       :options="userSelectOptions"
-                      :total="userSelectOptions.length"
+                      :total="userTotal"
+                      :page-size="5"
+                      :filter-method="searchUser"
+                      @page-change="handleUserPageChange"
                       @update:modelValue="handleUserChange"
                     />
                   </el-form-item>
@@ -92,7 +95,11 @@
             :class="[getServiceType(item.name), { active: form.serviceId === item.id }]"
             @click="handleServiceChange(item)"
           >
-            <div class="icon-box"><el-icon><component :is="getServiceIcon(item.name)" /></el-icon></div>
+            <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>
@@ -204,22 +211,16 @@ import AddPetDialog from './components/AddPetDialog.vue'
 import PageSelect from '@/components/PageSelect/index.vue'
 import { listStoreOnOrder } from '@/api/system/store'
 import { listServiceOnOrder } from '@/api/service/list'
+import { listCustomerOnOrder } from '@/api/archieves/customer'
+import { listPetByUser } from '@/api/archieves/pet'
 import { regionData as pcaOptions } from 'element-china-area-data'
+import { createOrder } from '@/api/order/order'
 
-// --- Mock Data ---
-const userOptions = ref([
-  { id: 101, name: '张三', phone: '13812345678' },
-  { id: 102, name: '李四', phone: '13987654321' }
-])
-const mockPets = {
-  101: [
-    { id: 1, name: '旺财', breed: '金毛', avatar: '', region: ['北京市', '市辖区', '朝阳区'], address: '三里屯SOHO A座 1001' },
-    { id: 2, name: '咪咪', breed: '布偶', avatar: '', region: ['北京市', '市辖区', '朝阳区'], address: '三里屯SOHO A座 1001' }
-  ],
-  102: [
-    { id: 3, name: '奥利奥', breed: '边牧', avatar: '', region: ['上海市', '市辖区', '浦东新区'], address: '陆家嘴一号院 5-502' }
-  ]
-}
+// --- State ---
+const userOptions = ref([])
+const userTotal = ref(0)
+const userQuery = reactive({ pageNum: 1, pageSize: 5, name: '' })
+const userLoading = ref(false)
 
 const serviceList = [
   { type: 'transport', name: '宠物接送', icon: 'Van', desc: '专车接送 · 全程监护', basePrice: 35 },
@@ -235,8 +236,6 @@ const allPackages = [
   { id: 14, type: 'washing', name: '除菌药浴套餐', price: 0 },
 ]
 
-// --- State ---
-const userLoading = ref(false)
 const currentPets = ref([])
 
 const stores = ref([])
@@ -278,31 +277,39 @@ const form = reactive({
 })
 
 // Address Autofill Watcher
-watch(() => form.petId, (newId) => {
-  if (!newId) return
-  const pet = currentPets.value.find(p => p.id === newId)
-  if (!pet) return
-
+watch([() => form.merchantId, () => form.userId, () => form.petId], () => {
+  const store = stores.value.find(s => s.id === form.merchantId)
   const user = userOptions.value.find(u => u.id === form.userId)
 
-  // Fill Transport
-  form.transport.pickStartRegion = pet.region || []
-  form.transport.pickStartDetail = pet.address || ''
+  const storeRegion = store?.areaCode ? store.areaCode.split(',') : []
+  const storeAddr = store?.address || ''
+
+  const userRegion = user?.regionCode ? user.regionCode.split('/') : []
+  const userAddr = user?.address || ''
+
+  // Fill Transport Pick
+  form.transport.pickStartRegion = userRegion
+  form.transport.pickStartDetail = userAddr
+  form.transport.pickEndRegion = storeRegion
+  form.transport.pickEndDetail = storeAddr
   form.transport.pickContact = user?.name || ''
-  form.transport.pickPhone = user?.phone || ''
+  form.transport.pickPhone = user?.phoneNumber || user?.phone || ''
 
-  form.transport.dropEndRegion = pet.region || []
-  form.transport.dropEndDetail = pet.address || ''
+  // Fill Transport Drop
+  form.transport.dropStartRegion = storeRegion
+  form.transport.dropStartDetail = storeAddr
+  form.transport.dropEndRegion = userRegion
+  form.transport.dropEndDetail = userAddr
   form.transport.dropContact = user?.name || ''
-  form.transport.dropPhone = user?.phone || ''
+  form.transport.dropPhone = user?.phoneNumber || user?.phone || ''
 
-  // Fill Feeding
-  form.feeding.region = pet.region || []
-  form.feeding.addressDetail = pet.address || ''
+  // Fill Feeding (上门服务)
+  form.feeding.region = userRegion
+  form.feeding.addressDetail = userAddr
 
-  // Fill Washing
-  form.washing.region = pet.region || []
-  form.washing.addressDetail = pet.address || ''
+  // Fill Washing (上门服务)
+  form.washing.region = userRegion
+  form.washing.addressDetail = userAddr
 })
 
 // Current Active Data Helper
@@ -397,11 +404,27 @@ const calcPrice = (type) => {
 const userDialogVisible = ref(false)
 const openAddUser = () => { userDialogVisible.value = true }
 const handleUserSuccess = (newUser) => {
-  userOptions.value.push(newUser)
-  form.userId = newUser.id
-  currentPets.value = []
-  form.petId = ''
-  ElMessage.success('用户添加成功并已选中')
+  // 重新获取列表
+  userQuery.pageNum = 1
+  fetchUsers()
+  
+  if (newUser && newUser.id) {
+    // 后端如果直接返回了用户信息或主键,尝试将其加入列表中并选中
+    const exists = userOptions.value.find(u => u.id === newUser.id)
+    if (!exists) {
+      userOptions.value.unshift(newUser)
+    }
+    form.userId = newUser.id
+    currentPets.value = []
+    form.petId = ''
+    ElMessage.success('用户添加成功并已自动选中')
+  } else {
+    // 未返回具体信息情况,清空当前选中项让用户重选
+    form.userId = ''
+    currentPets.value = []
+    form.petId = ''
+    ElMessage.success('用户添加成功')
+  }
 }
 
 // Removed mocked pcaOptions since we now use element-china-area-data
@@ -410,10 +433,20 @@ const handleUserSuccess = (newUser) => {
 const petDialogVisible = ref(false)
 const openAddPet = () => { petDialogVisible.value = true }
 const handlePetSuccess = (newPet) => {
-  if(!currentPets.value) currentPets.value = []
-  currentPets.value.push(newPet)
-  form.petId = newPet.id
-  ElMessage.success('宠物添加成功')
+  if (form.userId) {
+    listPetByUser(form.userId).then(res => {
+      currentPets.value = res.data || res.rows || []
+      if (newPet && newPet.id) {
+        form.petId = newPet.id
+        ElMessage.success('宠物添加成功并已自动选中')
+      } else {
+        form.petId = ''
+        ElMessage.success('宠物添加成功')
+      }
+    })
+  } else {
+    ElMessage.success('宠物添加成功')
+  }
 }
 
 
@@ -427,7 +460,7 @@ const availableServices = computed(() => {
   return allServices.value.filter(srv => store.services.includes(srv.id))
 })
 const userSelectOptions = computed(() => {
-  return userOptions.value.map(u => ({ label: `${u.name} - ${u.phone}`, value: u.id }))
+  return userOptions.value.map(u => ({ label: `${u.name} - ${u.phoneNumber || u.phone}`, value: u.id }))
 })
 const selectedMerchantName = computed(() => stores.value.find(m => m.id === form.merchantId)?.name)
 const selectedUserName = computed(() => userOptions.value.find(u => u.id === form.userId)?.name)
@@ -451,10 +484,31 @@ const canSubmit = computed(() => {
 })
 
 // --- 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 = (mode, type) => {
   if (mode === 1 || mode === '1') return '填写接送路线与时间'
@@ -473,13 +527,109 @@ const formatTime = (time) => {
   return `${d.getMonth()+1}-${d.getDate()} ${d.getHours()}:${d.getMinutes() < 10 ? '0'+d.getMinutes() : d.getMinutes()}`
 }
 
-const handleSubmit = () => {
-  ElMessage.success('下单成功!订单号:ORD20248888')
+const handleSubmit = async () => {
+  try {
+    const storeObj = stores.value.find(s => s.id === form.merchantId)
+    if (!storeObj) {
+      ElMessage.warning('请选择门店')
+      return
+    }
+
+    let subOrders = []
+    const baseMode = form.mode || 0
+    
+    // 获取默认客户联系方式
+    const userObj = userOptions.value.find(u => u.id === form.userId)
+    const defaultContact = userObj?.name || ''
+    const defaultPhone = userObj?.phoneNumber || userObj?.phone || ''
+
+    if (form.type === 'transport') {
+      const td = form.transport
+      const createTransportSubOrder = (time, startRegion, startDetail, endRegion, endDetail, contact, phone) => {
+        return {
+          mode: baseMode,
+          type: 0,
+          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.pickTime, td.pickStartRegion, td.pickStartDetail, td.pickEndRegion, td.pickEndDetail, td.pickContact, td.pickPhone
+        ))
+      }
+      
+      if (td.subType === 'round' || td.subType === 'drop') {
+        subOrders.push(createTransportSubOrder(
+          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,
+          type: 0,
+          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: form.merchantId, // 若后端有区分storeSite可以稍作调整,此处暂时一样
+      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('下单成功!订单号:' + (res.data || res.msg || 'ORD' + new Date().getTime()))
+    } else {
+      // 如果没有抛异常,走这里
+      ElMessage.success('下单成功!订单号:' + (res?.data || res?.msg || 'ORD' + new Date().getTime()))
+    }
+  } catch (error) {
+    console.error('Create order error: ', error)
+  }
 }
 
 // Initialize
 onMounted(() => {
   fetchStores()
+  fetchUsers()
   listServiceOnOrder().then(res => {
     allServices.value = res.data || []
   })
@@ -591,9 +741,17 @@ onMounted(() => {
 .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-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; }