ソースを参照

整改基本完成

Huanyi 1 ヶ月 前
コミット
f50b7e10c7

+ 219 - 0
src/views/order/dispatch/components/CustomerDetailDrawer.vue

@@ -0,0 +1,219 @@
+<template>
+  <el-drawer v-model="drawerVisible" title="用户档案详情" size="60%" destroy-on-close>
+    <div class="profile-header">
+      <el-avatar :size="80" :src="currentUser.avatarUrl" />
+      <div class="profile-basic">
+        <div class="name-row">
+          <span class="name">{{ currentUser.name }}</span>
+          <dict-tag :options="sys_user_sex" :value="currentUser.gender" />
+          <span class="phone">{{ currentUser.phone }}</span>
+        </div>
+        <div class="tags-row" style="margin-top: 8px">
+          <el-tag v-for="tag in currentUser.tags" :key="tag.id" :type="tag.colorType || 'info'" effect="light" size="small" style="margin-right: 5px">
+            {{ tag.name }}
+          </el-tag>
+        </div>
+      </div>
+    </div>
+
+    <el-tabs v-model="detailActiveTab" class="profile-tabs">
+      <el-tab-pane label="档案信息" name="info">
+        <div class="section-title">基本信息</div>
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="姓名">{{ currentUser.name }}</el-descriptions-item>
+          <el-descriptions-item label="电话">{{ currentUser.phone }}</el-descriptions-item>
+          <el-descriptions-item label="所属区域">{{ currentUser.areaName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="所属站点">{{ currentUser.stationName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="录入来源">{{ currentUser.source || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="录入时间">{{ currentUser.createTime || '-' }}</el-descriptions-item>
+        </el-descriptions>
+
+        <div class="section-title" style="margin-top: 20px">居住信息</div>
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="详细住址" :span="2">{{ currentUser.address }}</el-descriptions-item>
+          <el-descriptions-item label="房屋类型">
+            <dict-tag :options="sys_house_type" :value="currentUser.houseType" />
+          </el-descriptions-item>
+          <el-descriptions-item label="入门方式">
+            <dict-tag :options="sys_entry_method" :value="currentUser.entryMethod" />
+          </el-descriptions-item>
+          <el-descriptions-item label="开门详情" :span="2">
+            {{ currentUser.entryMethod === 'password' ? currentUser.entryPassword : currentUser.keyLocation }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-tab-pane>
+
+      <el-tab-pane label="宠物列表" name="pets">
+        <el-table :data="currentPets" border style="width: 100%">
+          <el-table-column label="宠物信息" width="200">
+            <template #default="scope">
+              <div style="display: flex; align-items: center;">
+                <el-avatar :size="30" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" style="margin-right: 8px;" />
+                {{ scope.row.name }}
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column prop="breed" label="品种" />
+          <el-table-column prop="gender" label="性别" width="60" />
+          <el-table-column prop="age" label="年龄" width="60" />
+          <el-table-column prop="status" label="健康状态">
+            <template #default="scope">
+              <el-tag :type="scope.row.status === '健康' ? 'success' : 'warning'" size="small">{{ scope.row.status }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="疫苗接种" width="120" align="center">
+            <template #default="scope">
+              {{ scope.row.vaccine || '-' }}
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-tab-pane>
+
+      <el-tab-pane label="历史订单" name="orders">
+        <el-table :data="mockOrders" border style="width: 100%">
+          <el-table-column prop="orderNo" label="订单编号" width="180" />
+          <el-table-column prop="service" label="服务项目" />
+          <el-table-column prop="pets" label="服务宠物" />
+          <el-table-column prop="time" label="服务时间" width="180" />
+          <el-table-column prop="status" label="状态" width="100">
+            <template #default="scope">
+              <el-tag type="success" size="small">完成</el-tag>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-tab-pane>
+
+      <el-tab-pane label="档案日志" name="logs">
+        <el-timeline style="margin-top: 10px; padding-left: 5px;">
+          <el-timeline-item
+            v-for="(log, index) in changeLogs"
+            :key="index"
+            :timestamp="log.createTime"
+            type="primary"
+          >
+            [{{ log.logType }}] {{ log.content }}
+            <div style="font-size: 12px; color: #999; margin-top: 4px">操作人: {{ log.operatorName }}</div>
+          </el-timeline-item>
+        </el-timeline>
+      </el-tab-pane>
+    </el-tabs>
+  </el-drawer>
+</template>
+
+<script setup>
+import { ref, computed, watch, getCurrentInstance, toRefs } from 'vue'
+import { getCustomer } from '@/api/archieves/customer'
+import { listPetByUser } from '@/api/archieves/pet'
+import { listAllChangeLog } from '@/api/archieves/changeLog'
+import { listOnStore } from '@/api/system/areaStation'
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  },
+  customerId: {
+    type: [String, Number],
+    default: null
+  }
+})
+
+const emit = defineEmits(['update:visible'])
+
+const drawerVisible = computed({
+  get: () => props.visible,
+  set: (val) => emit('update:visible', val)
+})
+
+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 currentUser = ref({})
+const currentPets = ref([])
+const changeLogs = ref([])
+const detailActiveTab = ref('info')
+const allNodes = ref([])
+
+const mockOrders = ref([
+  { orderNo: 'DD20231001001', service: '上门喂养 (标准版)', pets: '旺财', time: '2023-10-01 10:00', amount: '88.00', status: 'completed' },
+  { orderNo: 'DD20230915002', service: '深度洗护套餐', pets: '旺财, 咪咪', time: '2023-09-15 14:00', amount: '158.00', status: 'completed' }
+])
+
+const loadAreaStation = async () => {
+  if(allNodes.value.length === 0) {
+    const res = await listOnStore()
+    allNodes.value = res.data || []
+  }
+}
+
+watch(() => props.visible, async (val) => {
+  if (val && props.customerId) {
+    detailActiveTab.value = 'info'
+    currentUser.value = {}
+    currentPets.value = []
+    changeLogs.value = []
+    
+    await loadAreaStation()
+    getCustomer(props.customerId).then((res) => {
+      const data = res.data
+      if (data.areaId) {
+        const area = allNodes.value.find(n => n.id === data.areaId)
+        data.areaName = area ? area.name : '-'
+      } else {
+        data.areaName = '-'
+      }
+      if (data.stationId) {
+        const station = allNodes.value.find(n => n.id === data.stationId)
+        data.stationName = station ? station.name : '-'
+      } else {
+        data.stationName = '-'
+      }
+      currentUser.value = data
+    })
+    
+    listPetByUser(props.customerId).then((res) => {
+      currentPets.value = res.data || []
+    })
+    
+    listAllChangeLog(props.customerId, 'customer').then((res) => {
+      changeLogs.value = res.data || []
+    })
+  }
+})
+</script>
+
+<style scoped>
+.profile-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  padding-bottom: 20px;
+  border-bottom: 1px solid #f0f0f0;
+}
+.profile-basic {
+  margin-left: 20px;
+}
+.name-row {
+  display: flex;
+  align-items: center;
+}
+.name {
+  font-size: 20px;
+  font-weight: bold;
+  color: #303133;
+}
+.phone {
+  margin-left: 10px;
+  color: #666;
+}
+.section-title {
+  font-size: 16px;
+  font-weight: bold;
+  margin-bottom: 15px;
+  border-left: 4px solid #409eff;
+  padding-left: 10px;
+  line-height: 1.2;
+}
+</style>

+ 557 - 476
src/views/order/dispatch/components/DispatchDialog.vue

@@ -1,605 +1,686 @@
 <template>
-  <el-dialog v-model="visible" title="派单调度" width="900px" top="5vh" destroy-on-close append-to-body>
-    <div class="dispatch-dialog-content">
-      <!-- Top: Order Info -->
-      <div class="dispatch-order-info box-card" v-if="order">
-        <div class="list-card order-card" style="margin: 0; box-shadow: none; cursor: default; border: none">
-          <div class="card-left">
-            <div class="type-tag" :class="order.typeCode">
-              {{ getShortType(order.typeCode) }}
-            </div>
-          </div>
-          <div class="card-main">
-            <template v-if="order.typeCode === 'transport'">
-              <div class="row-addr" :title="order.fromAddress">
-                <span class="tag pick">取</span> {{ order.fromAddress }}
-              </div>
-              <div class="row-addr" :title="order.toAddress">
-                <span class="tag drop">送</span> {{ order.toAddress }}
-              </div>
-            </template>
-            <template v-else>
-              <div class="row-addr" :title="order.toAddress"><span class="tag home">址</span> {{ order.toAddress }}</div>
-            </template>
-            <div class="row-time" style="margin-top: 4px">
-              <el-icon>
-                <Clock />
-              </el-icon> {{ order.serviceTime }}
-              <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- Current Rider Info (For Re-dispatch) -->
-      <div class="current-rider-section" v-if="currentRider">
-        <div class="select-header" style="margin-bottom: 8px">
-          <span class="tit">当前派单履约者</span>
-        </div>
-        <div class="list-card rider-card"
-          style="margin-bottom: 20px; border: 1px solid #e4e7ed; background: #fafafa; cursor: default">
-          <div class="card-left relative">
-            <el-avatar :src="currentRider.avatar" :size="40" />
-            <div class="dot" :class="currentRider.status"></div>
-          </div>
-          <div class="card-main">
-            <div class="row-1" style="justify-content: space-between; align-items: flex-start">
-              <div style="display: flex; align-items: baseline; gap: 8px">
-                <span class="r-name">{{ currentRider.name }}</span>
-                <dict-tag :options="sys_user_sex" :value="currentRider.gender ?? currentRider.sex" />
-                <span class="r-phone">{{ currentRider.maskPhone }}</span>
-              </div>
-              <div class="status-right">
-                <span class="status-badge" :class="currentRider.status">{{ getRiderStatusText(currentRider.status)
-                  }}</span>
-              </div>
-            </div>
-
-            <div class="row-2 categories-row" style="margin-top: 6px; display: flex; gap: 4px; flex-wrap: wrap">
-              <el-tag v-for="cat in (currentRider.tags || [])" :key="cat" size="small" :type="getTagType(cat)" effect="plain">{{
-                getTagText(cat)
-                }}</el-tag>
-            </div>
-
-            <div class="row-3 time-row" style="margin-top: 4px">
-              <span class="last-time">下一单: {{ currentRider.nextOrderTime || '14:30' }}</span>
+    <el-dialog v-model="dialogVisible" title="派单调度" width="900px" top="5vh" destroy-on-close append-to-body>
+        <div class="dispatch-dialog-content">
+            <!-- Top: Order Info (OrderDispatch Style) -->
+            <div class="dispatch-order-info" v-if="order">
+                <div class="list-card order-card" style="margin:0; box-shadow:none; cursor:default; border:none;">
+                    <div class="card-left">
+                        <div class="type-tag" :class="order.typeCode">
+                            {{ getShortType(order.typeCode) }}
+                        </div>
+                    </div>
+                    <div class="card-main">
+                        <template v-if="order.typeCode === 'transport'">
+                            <div class="row-addr" :title="order.pickAddr">
+                                <span class="tag pick">取</span> {{ order.pickAddr }}
+                            </div>
+                            <div class="row-addr" :title="order.dropAddr">
+                                <span class="tag drop">送</span> {{ order.dropAddr }}
+                            </div>
+                        </template>
+                        <template v-else>
+                            <div class="row-addr" :title="order.address">
+                                <span class="tag home">址</span> {{ order.address }}
+                            </div>
+                        </template>
+                        <div class="row-time" style="margin-top: 4px;">
+                            <el-icon>
+                                <Clock />
+                            </el-icon> {{ order.time }}
+                            <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
+                        </div>
+                    </div>
+                    <!-- 新增右侧按钮组 -->
+                    <div class="card-right" style="display: flex; align-items: center; gap: 10px; padding-left: 20px;">
+                        <el-button type="primary" size="small" plain round icon="User" @click="openCustomerDetail" :loading="orderInfoLoading">用户档案</el-button>
+                        <el-button type="success" size="small" plain round @click="openPetDetail" :loading="orderInfoLoading" style="margin-left: 0;">宠物档案</el-button>
+                    </div>
+                </div>
             </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>
+            <!-- Current Rider Info (If Exists) -->
+            <div class="current-rider-section" v-if="currentRider">
+                <div class="select-header" style="margin-bottom:8px;">
+                    <span class="tit">当前派单履约者</span>
                 </div>
-                <div class="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>
-                      <dict-tag :options="sys_user_sex" :value="rider.gender ?? rider.sex" />
-                      <span class="r-phone">{{ rider.maskPhone }}</span>
+                <div class="list-card rider-card"
+                    style="margin-bottom: 20px; border: 1px solid #e4e7ed; background:#fafafa; cursor:default;">
+                    <div class="card-left relative">
+                        <el-avatar :src="currentRider.avatar" :size="40" />
                     </div>
-                    <div class="status-right">
-                      <span class="status-badge" :class="rider.status">{{ getRiderStatusText(rider.status) }}</span>
+                    <div class="card-main">
+                        <div class="row-1"
+                            style="justify-content: space-between; align-items: flex-start; display: flex;">
+                            <div style="display:flex; align-items:baseline; gap:8px;">
+                                <span class="r-name">{{ currentRider.name || '--' }}</span>
+                                <span class="r-phone">{{ currentRider.phone || '--' }}</span>
+                                <dict-tag :options="sys_user_sex" :value="currentRider.gender" />
+                                <el-tag v-if="currentRider.status" size="small" :type="getStatusType(currentRider.status)" effect="plain">
+                                    {{ getStatusText(currentRider.status) }}
+                                </el-tag>
+                            </div>
+                        </div>
+
+                        <div class="row-2 categories-row"
+                            style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
+                            <el-tag v-for="typeId in (currentRider.serviceTypes ? String(currentRider.serviceTypes).split(',') : [])" :key="typeId" size="small"
+                                type="primary" effect="plain">{{ getServiceTypeText(typeId) }}</el-tag>
+                        </div>
+                        <div class="row-3 time-row" style="margin-top: 4px;">
+                            <span class="last-time">下一单: {{ currentRider.nextOrderTime || '-' }}</span>
+                        </div>
                     </div>
-                  </div>
+                </div>
+            </div>
 
-                  <div class="row-2 categories-row" style="margin-top: 6px; display: flex; gap: 4px; flex-wrap: wrap">
-                    <el-tag v-for="cat in (rider.tags || [])" :key="cat" size="small" :type="getTagType(cat)" effect="plain">{{
-                      getTagText(cat)
-                      }}</el-tag>
-                  </div>
+            <!-- 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="row-3 time-row" style="margin-top: 4px">
-                    <span class="last-time">下一单: {{ rider.nextOrderTime || '14:30' }}</span>
-                  </div>
+                <div class="rider-grid-wrapper">
+                    <el-scrollbar class="rider-scroll">
+                        <div class="rider-grid">
+                            <div v-for="rider in filteredDispatchRiders" :key="rider.id"
+                                class="list-card rider-card select-card"
+                                :class="{ active: selectedRiderId === rider.id }" @click="selectedRiderId = rider.id">
+                                <!-- Reusing Rider Card Layout -->
+                                <div class="card-left relative">
+                                    <el-avatar :src="rider.avatar" :size="40" />
+                                </div>
+                                <div class="card-main">
+                                    <div class="row-1" style="justify-content: space-between; align-items: flex-start; display: flex;">
+                                        <div style="display:flex; align-items:baseline; gap:8px;">
+                                            <span class="r-name">{{ rider.name || '--' }}</span>
+                                            <span class="r-phone">{{ rider.phone || '--' }}</span>
+                                            <dict-tag :options="sys_user_sex" :value="rider.gender" />
+                                        </div>
+                                        <el-tag v-if="rider.status" size="small" :type="getStatusType(rider.status)" effect="plain">
+                                            {{ getStatusText(rider.status) }}
+                                        </el-tag>
+                                    </div>
+
+                                    <div class="row-2 categories-row"
+                                        style="margin-top: 6px; display:flex; gap:4px; flex-wrap:wrap;">
+                                        <el-tag v-for="typeId in (rider.serviceTypes ? String(rider.serviceTypes).split(',') : [])" :key="typeId" size="small"
+                                            type="primary" effect="plain">{{ getServiceTypeText(typeId) }}</el-tag>
+                                    </div>
+                                    <div class="row-3 time-row" style="margin-top: 4px">
+                                        <span class="last-time">下一单: {{ rider.nextOrderTime || '-' }}</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>
 
-                <!-- Selected Check -->
-                <div class="selected-mark" v-if="selectedId === rider.id">
-                  <el-icon>
-                    <Check />
-                  </el-icon>
+                <div class="rider-pagination" style="margin-top: 20px;">
+                    <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize"
+                        :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" :total="total"
+                        @current-change="loadRiders" @size-change="handlePageSizeChange" />
                 </div>
-              </div>
+            </div>
 
-              <div v-if="filteredRiders.length === 0" class="empty-text">暂无符合条件的履约者</div>
+            <!-- Bottom: Fee & Submit -->
+            <div class="dispatch-footer">
+                <div class="fee-input">
+                    <span class="label">服务费用:</span>
+                    <el-input-number v-model="dispatchFee" :min="0" :precision="2" :step="10" placeholder="请输入"
+                        style="width: 140px;" />
+                    <span class="unit">元</span>
+                </div>
+                <div class="btns">
+                    <el-button @click="dialogVisible = false">取消</el-button>
+                    <el-button type="primary" :disabled="!canSubmit" @click="handleDispatchSubmit">确认派单</el-button>
+                </div>
             </div>
-          </el-scrollbar>
         </div>
+    </el-dialog>
 
-        <div class="rider-pagination" style="margin-top: 20px;">
-          <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize"
-            :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" :total="total"
-            @current-change="loadRiders" @size-change="handlePageSizeChange" />
-        </div>
-      </div>
-
-      <!-- Bottom: Fee & Submit -->
-      <div class="dispatch-footer">
-        <div class="fee-input">
-          <span class="label">服务费用:</span>
-          <el-input-number v-model="fee" :min="0" :precision="2" :step="10" placeholder="请输入" style="width: 140px" />
-          <span class="unit">元</span>
-        </div>
-        <div class="btns">
-          <el-button @click="visible = false">取消</el-button>
-          <el-button type="primary" @click="handleSubmit">确认派单</el-button>
-        </div>
-      </div>
-    </div>
-  </el-dialog>
+    <CustomerDetailDrawer v-model:visible="customerDialogVisible" :customer-id="customerId" />
+    <PetDetailDrawer v-model:visible="petDialogVisible" :pet-id="petId" />
 </template>
 
 <script setup>
-import { ref, computed, watch } from 'vue';
-import { ElMessage } from 'element-plus';
-import { Check, Clock, Search } from '@element-plus/icons-vue';
-import { useDict } from '@/utils/dict';
-import DictTag from '@/components/DictTag/index.vue';
-
-const { sys_user_sex } = useDict('sys_user_sex');
-import { pageFulfillerOnOrder } from '@/api/fulfiller/pool';
-import { listAllTag } from '@/api/fulfiller/tag';
+import { ref, computed, watch, getCurrentInstance, toRefs } from 'vue'
+import { ElMessage } from 'element-plus'
+import { pageFulfillerOnOrder } from '@/api/fulfiller/pool'
+import { listAllTag } from '@/api/fulfiller/tag'
+import { getSubOrderInfo } from '@/api/order/subOrder/index'
+import { listServiceOnOrder } from '@/api/service/list/index'
+import CustomerDetailDrawer from './CustomerDetailDrawer.vue'
+import PetDetailDrawer from './PetDetailDrawer.vue'
 
 const props = defineProps({
-  modelValue: { type: Boolean, default: false },
-  order: { type: Object, default: () => null },
-  currentRider: { type: Object, default: () => null }
-});
+    visible: Boolean,
+    order: Object
+})
+const emit = defineEmits(['update:visible', 'submit'])
 
-const emit = defineEmits(['update:modelValue', 'submit']);
+const { proxy } = getCurrentInstance();
+const { sys_user_sex } = toRefs(proxy.useDict('sys_user_sex'));
 
-const visible = computed({
-  get: () => props.modelValue,
-  set: (val) => emit('update:modelValue', val)
-});
+const dialogVisible = computed({
+    get: () => props.visible,
+    set: (val) => emit('update:visible', val)
+})
 
-const searchQuery = ref('');
-const selectedId = ref(null);
-const fee = ref(0);
+const ridersList = ref([])
+const total = ref(0)
+const pageNum = ref(1)
+const pageSize = ref(10)
 
-const ridersList = ref([]);
-const total = ref(0);
-const pageNum = ref(1);
-const pageSize = ref(10);
-
-const allTags = ref([]);
+const allTags = ref([])
 const tagMap = computed(() => {
-  const map = {};
-  for (const t of (allTags.value || [])) {
-    if (t && t.id !== undefined && t.id !== null) map[t.id] = t;
-  }
-  return map;
-});
+    const map = {}
+    for (const t of (allTags.value || [])) {
+        if (t && t.id !== undefined && t.id !== null) map[t.id] = t
+    }
+    return map
+})
+
+const currentRider = ref(null)
+const dispatchSearchQuery = ref('')
+const selectedRiderId = ref(null)
+const dispatchFee = ref(0)
+
+const customerDialogVisible = ref(false)
+const petDialogVisible = ref(false)
+const customerId = ref(null)
+const petId = ref(null)
+const orderInfoLoading = ref(false)
 
 const loadAllTags = async () => {
-  if (allTags.value && allTags.value.length > 0) return;
-  try {
-    const res = await listAllTag({ category: 'fulfiller' });
-    allTags.value = res?.data || [];
-  } catch {
-    allTags.value = [];
-  }
-};
+    if (allTags.value && allTags.value.length > 0) return
+    try {
+        const res = await listAllTag({ category: 'fulfiller' })
+        allTags.value = res?.data || []
+    } catch {
+        allTags.value = []
+    }
+}
+
+const serviceOptions = ref([])
+const loadServiceOptions = async () => {
+    if (serviceOptions.value.length > 0) return
+    try {
+        const res = await listServiceOnOrder()
+        serviceOptions.value = res?.data || []
+    } catch { /* ignore */ }
+}
+
+const getServiceTypeText = (id) => {
+    const s = serviceOptions.value.find(item => String(item.id) === String(id))
+    return s ? s.name : String(id)
+}
 
 const loadRiders = async () => {
-  try {
-    const res = await pageFulfillerOnOrder({
-      content: searchQuery.value || undefined,
-      pageNum: pageNum.value,
-      pageSize: pageSize.value,
-      service: props.order?.service
-    });
-    const list = res?.rows || [];
-    ridersList.value = list.map(r => ({
-      ...r,
-      nextOrderTime: r.nextOrderTime || '14:30' // 使用假数据代替下一单时间
-    }));
-    total.value = res?.total || 0;
-  } catch {
-    ridersList.value = [];
-    total.value = 0;
-  }
-};
+    try {
+        const res = await pageFulfillerOnOrder({
+            content: dispatchSearchQuery.value || undefined,
+            pageNum: pageNum.value,
+            pageSize: pageSize.value,
+            service: props.order?.service
+        })
+        const list = res?.rows || []
+        ridersList.value = list.map(r => ({
+            ...r,
+            nextOrderTime: r.nextOrderTime || '-',
+            gender: r.gender ?? r.sex
+        }))
+        total.value = res?.total || 0
+
+        if (props.order?.riderId) {
+            currentRider.value = ridersList.value.find(r => r.id === props.order.riderId) || null
+        }
+    } catch {
+        ridersList.value = []
+        total.value = 0
+    }
+}
 
 const handlePageSizeChange = (size) => {
-  pageSize.value = size;
-  pageNum.value = 1;
-  loadRiders();
-};
-
-watch(searchQuery, () => {
-  pageNum.value = 1;
-  loadRiders();
-});
-
-// Reset form when dialog opens
-watch(visible, (val) => {
-  if (val) {
-    searchQuery.value = '';
-    selectedId.value = null;
-    fee.value = 0;
-    pageNum.value = 1;
-    loadAllTags();
-    loadRiders();
-  }
-});
-
-const filteredRiders = computed(() => {
-  return ridersList.value || [];
-});
-
-const handleSubmit = () => {
-  if (!selectedId.value) {
-    ElMessage.warning('请选择履约者');
-    return;
-  }
-  if (!fee.value) {
-    ElMessage.warning('请输入服务费用');
-    return;
-  }
-  emit('submit', { order: props.order, riderId: selectedId.value, fee: fee.value });
-};
-
-// Helpers
+    pageSize.value = size
+    pageNum.value = 1
+    loadRiders()
+}
+
+watch(() => props.visible, (val) => {
+    if (val && props.order) {
+        currentRider.value = null
+        dispatchSearchQuery.value = ''
+        selectedRiderId.value = null
+        // price 单位为分,转成元显示
+        dispatchFee.value = props.order?.price ? Number((props.order.price / 100).toFixed(2)) : 0
+        if (props.order?.riderId) {
+            currentRider.value = {
+                id: props.order.riderId,
+                gender: props.order.riderGender ?? props.order.riderSex
+            }
+        }
+        pageNum.value = 1
+        loadAllTags()
+        loadServiceOptions()
+        loadRiders()
+
+        // 获取订单详细信息
+        customerId.value = null
+        petId.value = null
+        orderInfoLoading.value = true
+        getSubOrderInfo(props.order.id).then((res) => {
+            if(res.data) {
+                // 如果 usrCustomer / usrPet 是对象则取其 id,如果是 ID 直接取
+                customerId.value = res.data.usrCustomer?.id || res.data.usrCustomer
+                petId.value = res.data.usrPet?.id || res.data.usrPet
+                
+                // 接到详情后,把真实的金额放进去(后端金额单位为分)
+                if (res.data.price !== undefined && res.data.price !== null) {
+                    dispatchFee.value = Number((res.data.price / 100).toFixed(2))
+                }
+
+                // 如果已经有履约者且不是在列表中找到的,从详情中补全性别
+                if (props.order?.riderId && !currentRider.value) {
+                    currentRider.value = {
+                        id: props.order.riderId,
+                        name: res.data.fulfillerName,
+                        gender: res.data.fulfillerGender ?? res.data.fulfillerSex,
+                        status: res.data.fulfillerStatus
+                    }
+                } else if (currentRider.value && (res.data.fulfillerGender || res.data.fulfillerSex)) {
+                    currentRider.value.gender = res.data.fulfillerGender ?? res.data.fulfillerSex
+                }
+            }
+        }).catch((e) => {
+            console.error('获取订单详细信息失败', e)
+        }).finally(() => {
+            orderInfoLoading.value = false
+        })
+    }
+})
+
+const openCustomerDetail = () => {
+    if (!customerId.value) {
+        ElMessage.warning('未能获取到用户信息')
+        return
+    }
+    customerDialogVisible.value = true
+}
+
+const openPetDetail = () => {
+    if (!petId.value) {
+        ElMessage.warning('未能获取到宠物信息')
+        return
+    }
+    petDialogVisible.value = true
+}
+
 const getTagText = (tagId) => {
-  const t = tagMap.value?.[tagId];
-  return t?.name || String(tagId);
-};
+    const t = tagMap.value?.[tagId]
+    return t?.name || String(tagId)
+}
+
 const getTagType = (tagId) => {
-  const t = tagMap.value?.[tagId];
-  const type = t?.colorType;
-  if (type === 'success' || type === 'warning' || type === 'danger' || type === 'info') return type;
-  return '';
-};
-const getShortType = (code) => {
-  const map = { 'transport': '接送', 'feeding': '喂遛', 'washing': '洗护' };
-  return map[code] || '订单';
-};
-const getRiderStatusText = (status) => {
-  const statusMap = {
-    resting: '休息',
-    busy: '接单中',
-    disabled: '禁用'
-  };
-  return statusMap[status] || status;
-};
+    const t = tagMap.value?.[tagId]
+    const type = t?.colorType
+    if (type === 'success' || type === 'warning' || type === 'danger' || type === 'info') return type
+    return ''
+}
 
+watch(dispatchSearchQuery, () => {
+    pageNum.value = 1
+    loadRiders()
+})
+
+const getShortType = (code) => {
+    const map = { 'transport': '接送', 'feeding': '喂遛', 'washing': '洗护' }
+    return map[code] || '订单'
+}
+
+const getStatusText = (status) => {
+    const statusMap = {
+        resting: '休息',
+        busy: '接单中',
+        disabled: '禁用'
+    }
+    return statusMap[status] || status
+}
+
+const getStatusType = (status) => {
+    const typeMap = {
+        resting: 'info',
+        busy: 'success',
+        disabled: 'danger'
+    }
+    return typeMap[status] || 'info'
+}
+
+const filteredDispatchRiders = computed(() => {
+    return ridersList.value || []
+})
+
+const canSubmit = computed(() => {
+    return !!selectedRiderId.value && !!dispatchFee.value
+})
+
+const handleDispatchSubmit = () => {
+    if (!selectedRiderId.value) {
+        ElMessage.warning('请选择履约者')
+        return
+    }
+    if (!dispatchFee.value) {
+        ElMessage.warning('请输入服务费用')
+        return
+    }
+    const rider = ridersList.value.find(r => r.id === selectedRiderId.value)
+    emit('submit', {
+        riderId: rider.id,
+        riderName: rider.name,
+        fee: dispatchFee.value
+    })
+    dialogVisible.value = false
+}
 </script>
 
 <style scoped>
+/* Dispatch Dialog Styles */
 .list-card {
-  background: #fff;
-  border: 1px solid #ebeef5;
-  border-radius: 8px;
-  padding: 12px;
-  margin-bottom: 10px;
-  display: flex;
-  align-items: stretch;
-  gap: 12px;
-  transition: all 0.2s;
+    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;
+    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;
+    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;
+    background: #e6a23c;
 }
 
 .type-tag.feeding {
-  background: #67c23a;
+    background: #67c23a;
 }
 
 .type-tag.washing {
-  background: #409eff;
+    background: #409eff;
 }
 
 .card-main {
-  flex: 1;
-  overflow: hidden;
-  display: flex;
-  flex-direction: column;
-  justify-content: flex-start;
-  gap: 4px;
+    flex: 1;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    gap: 4px;
 }
 
-.row-1 {
-  display: flex;
-  align-items: center;
+.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;
 }
 
-.o-type {
-  font-weight: bold;
-  font-size: 14px;
-  color: #303133;
+.row-addr .tag {
+    font-size: 11px;
+    color: #fff;
+    padding: 1px 4px;
+    border-radius: 4px;
+    flex-shrink: 0;
+    transform: scale(0.9);
 }
 
-.row-2 {
-  font-size: 12px;
-  color: #606266;
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
+.tag.pick {
+    background: #409eff;
 }
 
-.row-3 {
-  font-size: 12px;
-  color: #909399;
-  display: flex;
-  align-items: center;
-  gap: 4px;
+.tag.drop {
+    background: #e6a23c;
 }
 
-.card-right {
-  display: flex;
-  flex-direction: column;
-  align-items: flex-end;
-  justify-content: center;
-  gap: 8px;
-  margin-left: 8px;
-  flex-shrink: 0;
+.tag.home {
+    background: #67c23a;
 }
 
-.card-right .actions {
-  display: flex;
-  align-items: center;
-  gap: 4px;
+.row-time {
+    font-size: 12px;
+    color: #909399;
+    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;
+.days-tag {
+    color: #f56c6c;
+    background: #fef0f0;
+    padding: 0 4px;
+    border-radius: 4px;
+    font-size: 11px;
+    border: 1px solid #fde2e2;
+    transform: scale(0.95);
 }
 
-.dot.online {
-  background: #67c23a;
+.dispatch-order-info {
+    background: #f5f7fa;
+    padding: 10px;
+    border-radius: 4px;
+    margin-bottom: 20px;
+    border: 1px solid #e4e7ed;
+    display: block;
 }
 
-.dot.busy {
-  background: #409eff;
+.dispatch-rider-select .select-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 10px;
 }
 
-.dot.offline {
-  background: #909399;
+.dispatch-rider-select .tit {
+    font-weight: bold;
+    font-size: 14px;
 }
 
-.r-name {
-  font-size: 14px;
-  font-weight: bold;
-  color: #303133;
+.rider-scroll {
+    max-height: 45vh;
 }
 
-.r-phone {
-  font-size: 12px;
-  color: #909399;
+.rider-grid {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    gap: 12px;
+    padding-right: 10px;
 }
 
-.cat-tag {
-  background: #f4f4f5;
-  color: #909399;
-  font-size: 10px;
-  padding: 1px 4px;
-  border-radius: 2px;
+.rider-pagination {
+    margin-top: 10px;
 }
 
-.cat-tag.cat-transport {
-  background: #e6f7ff;
-  color: #1890ff;
-  border: 1px solid #91d5ff;
+.rider-card.select-card {
+    cursor: pointer;
+    border: 1px solid #dcdfe6;
+    position: relative;
+    transition: all 0.2s;
+    margin-bottom: 0;
 }
 
-.cat-tag.cat-feeding {
-  background: #f6ffed;
-  color: #52c41a;
-  border: 1px solid #b7eb8f;
+.rider-card.select-card:hover {
+    border-color: #409eff;
 }
 
-.cat-tag.cat-washing {
-  background: #fff0f6;
-  color: #eb2f96;
-  border: 1px solid #ffadd2;
+.rider-card.select-card.active {
+    border-color: #409eff;
+    background-color: #ecf5ff;
 }
 
-.status-badge {
-  font-size: 11px;
-  padding: 2px 6px;
-  border-radius: 4px;
-  display: inline-block;
-  font-weight: bold;
+.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;
 }
 
-.status-badge.online {
-  background: #f0f9eb;
-  color: #67c23a;
+.rider-card .card-left .dot {
+    position: absolute;
+    bottom: 0;
+    right: 0;
+    width: 10px;
+    height: 10px;
+    border-radius: 50%;
+    border: 2px solid #fff;
 }
 
-.status-badge.busy {
-  background: #ecf5ff;
-  color: #409eff;
+.dot.online {
+    background: #67c23a;
 }
 
-.status-badge.offline {
-  background: #f4f4f5;
-  color: #909399;
+.dot.busy {
+    background: #409eff;
 }
 
-.status-badge.disabled {
-  background: #fef0f0;
-  color: #f56c6c;
+.dot.offline {
+    background: #909399;
 }
 
-.last-time {
-  font-size: 11px;
-  color: #999;
+.r-name {
+    font-weight: bold;
+    font-size: 14px;
+    color: #303133;
+    margin-right: 8px;
 }
 
-.dispatch-order-info {
-  background: #f5f7fa;
-  padding: 10px;
-  border-radius: 4px;
-  margin-bottom: 20px;
-  border: 1px solid #e4e7ed;
+.r-phone {
+    font-size: 12px;
+    color: #909399;
 }
 
-.dispatch-rider-select .select-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 10px;
+.status-badge {
+    font-size: 11px;
+    padding: 2px 6px;
+    border-radius: 4px;
+    display: inline-block;
+    font-weight: bold;
 }
 
-.dispatch-rider-select .tit {
-  font-weight: bold;
-  font-size: 14px;
+.status-badge.online {
+    background: #f0f9eb;
+    color: #67c23a;
 }
 
-.rider-grid {
-  display: grid;
-  grid-template-columns: repeat(2, 1fr);
-  gap: 12px;
-  padding-right: 10px;
+.status-badge.busy {
+    background: #ecf5ff;
+    color: #409eff;
 }
 
-.rider-card.select-card {
-  cursor: pointer;
-  border: 1px solid #dcdfe6;
-  position: relative;
-  transition: all 0.2s;
-  margin-bottom: 0;
+.status-badge.offline {
+    background: #f4f4f5;
+    color: #909399;
 }
 
-.rider-card.select-card:hover {
-  border-color: #409eff;
+.cat-tag {
+    background: #f4f4f5;
+    color: #909399;
+    font-size: 10px;
+    padding: 1px 4px;
+    border-radius: 2px;
+    margin-right: 4px;
 }
 
-.rider-card.select-card.active {
-  border-color: #409eff;
-  background-color: #ecf5ff;
+.cat-tag.cat-transport {
+    background: #e6f7ff;
+    color: #1890ff;
+    border: 1px solid #91d5ff;
 }
 
-.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;
+.cat-tag.cat-feeding {
+    background: #f6ffed;
+    color: #52c41a;
+    border: 1px solid #b7eb8f;
 }
 
-.dispatch-footer {
-  margin-top: 20px;
-  padding-top: 20px;
-  border-top: 1px solid #ebeef5;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
+.cat-tag.cat-washing {
+    background: #fff0f6;
+    color: #eb2f96;
+    border: 1px solid #ffadd2;
 }
 
-.dispatch-footer .fee-input {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  font-size: 14px;
+.last-time {
+    font-size: 11px;
+    color: #999;
 }
 
 .empty-text {
-  text-align: center;
-  color: #909399;
-  padding: 20px;
-  width: 100%;
-  grid-column: span 2;
+    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;
+.dispatch-footer {
+    margin-top: 20px;
+    padding-top: 20px;
+    border-top: 1px solid #ebeef5;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
 }
 
-.days-tag {
-  color: #f56c6c;
-  background: #fef0f0;
-  padding: 0 4px;
-  border-radius: 4px;
-  font-size: 11px;
-  border: 1px solid #fde2e2;
-  transform: scale(0.95);
+.dispatch-footer .fee-input {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    font-size: 14px;
 }
 </style>

+ 179 - 0
src/views/order/dispatch/components/PetDetailDrawer.vue

@@ -0,0 +1,179 @@
+<template>
+  <el-drawer v-model="drawerVisible" title="宠物档案详情" size="60%" destroy-on-close>
+    <div class="profile-header">
+      <el-avatar :size="80" :src="currentPet.avatarUrl" />
+      <div class="profile-basic">
+        <div class="name-row">
+          <span class="name">{{ currentPet.name }}</span>
+          <dict-tag :options="sys_pet_gender" :value="currentPet.gender" />
+          <el-tag size="small" effect="plain" type="info" style="margin-left: 5px">{{ currentPet.age }}岁</el-tag>
+        </div>
+        <div class="tags-row" style="margin-top: 8px">
+          <el-tag v-for="tag in currentPet.tags" :key="tag.id" :type="tag.colorType || 'info'" effect="light" size="small" style="margin-right: 5px">
+            {{ tag.name }}
+          </el-tag>
+        </div>
+      </div>
+    </div>
+
+    <el-tabs v-model="detailActiveTab" class="profile-tabs">
+      <el-tab-pane label="档案信息" name="info">
+        <div class="section-title">基本信息</div>
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="品种">{{ currentPet.breed }}</el-descriptions-item>
+          <el-descriptions-item label="体型">
+            <dict-tag :options="sys_pet_size" :value="currentPet.size" />
+          </el-descriptions-item>
+          <el-descriptions-item label="体重">{{ currentPet.weight }} kg</el-descriptions-item>
+          <el-descriptions-item label="所属主人">{{ currentPet.ownerName }} ({{ currentPet.ownerPhone }})</el-descriptions-item>
+          <el-descriptions-item label="性格关键词">{{ currentPet.personality || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="萌宠性格" :span="2">{{ currentPet.cutePersonality || '-' }}</el-descriptions-item>
+        </el-descriptions>
+
+        <div class="section-title" style="margin-top: 20px">家庭信息</div>
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="到家时间">{{ currentPet.arrivalTime || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="房屋类型">
+            <dict-tag :options="sys_house_type" :value="currentPet.houseType" />
+          </el-descriptions-item>
+          <el-descriptions-item label="入门方式">
+            <dict-tag :options="sys_entry_method" :value="currentPet.entryMethod" />
+          </el-descriptions-item>
+          <el-descriptions-item label="开门详情">
+            {{ currentPet.entryMethod === 'password' ? currentPet.entryPassword : currentPet.keyLocation }}
+          </el-descriptions-item>
+        </el-descriptions>
+
+        <div class="section-title" style="margin-top: 20px">健康状况</div>
+        <el-descriptions :column="2" border>
+          <el-descriptions-item label="健康状态">
+            <el-tag :type="currentPet.healthStatus === '健康' ? 'success' : 'warning'" size="small">{{ currentPet.healthStatus }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="攻击倾向">
+            <el-tag :type="currentPet.aggression ? 'danger' : 'success'" size="small">{{ currentPet.aggression ? '有' : '无' }}</el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="疫苗情况" :span="2">
+            <div>{{ currentPet.vaccineStatus || '-' }}</div>
+            <div v-if="currentPet.vaccineCertUrl" style="margin-top: 10px">
+              <el-image
+                style="width: 100px; height: 100px; border-radius: 4px"
+                :src="currentPet.vaccineCertUrl"
+                :preview-src-list="[currentPet.vaccineCertUrl]"
+                fit="cover"
+              />
+            </div>
+          </el-descriptions-item>
+          <el-descriptions-item label="既往病史" :span="2">{{ currentPet.medicalHistory || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="过敏史" :span="2">{{ currentPet.allergies || '-' }}</el-descriptions-item>
+        </el-descriptions>
+      </el-tab-pane>
+
+      <el-tab-pane label="历史订单" name="orders">
+        <el-table :data="mockOrders" border style="width: 100%">
+          <el-table-column prop="orderNo" label="订单编号" width="180" />
+          <el-table-column prop="service" label="服务项目" />
+          <el-table-column prop="time" label="服务时间" width="180" />
+          <el-table-column prop="amount" label="金额" width="100" />
+          <el-table-column prop="status" label="状态" width="100">
+            <template #default="scope">
+              <el-tag type="success" size="small">完成</el-tag>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-tab-pane>
+
+      <el-tab-pane label="备注日志" name="logs">
+        <el-timeline style="margin-top: 10px; padding-left: 5px">
+          <el-timeline-item v-for="(log, index) in changeLogs" :key="index" :timestamp="log.createTime" type="primary">
+            [{{ log.logType }}] {{ log.content }}
+            <div style="font-size: 12px; color: #999; margin-top: 4px">操作人: {{ log.operatorName }}</div>
+          </el-timeline-item>
+        </el-timeline>
+      </el-tab-pane>
+    </el-tabs>
+  </el-drawer>
+</template>
+
+<script setup>
+import { ref, computed, watch, getCurrentInstance, toRefs } from 'vue'
+import { getPet } from '@/api/archieves/pet'
+import { listAllChangeLog } from '@/api/archieves/changeLog'
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  },
+  petId: {
+    type: [String, Number],
+    default: null
+  }
+})
+
+const emit = defineEmits(['update:visible'])
+
+const drawerVisible = computed({
+  get: () => props.visible,
+  set: (val) => emit('update:visible', val)
+})
+
+const { proxy } = getCurrentInstance()
+const { sys_pet_gender, sys_pet_size, sys_house_type, sys_entry_method } = toRefs(
+  proxy?.useDict('sys_pet_gender', 'sys_pet_size', 'sys_house_type', 'sys_entry_method')
+)
+
+const currentPet = ref({})
+const changeLogs = ref([])
+const detailActiveTab = ref('info')
+
+const mockOrders = ref([
+  { orderNo: 'DD20231001001', service: '上门喂养 (标准版)', time: '2023-10-01 10:00', amount: '88.00', status: 'completed' },
+  { orderNo: 'DD20230915002', service: '深度洗护套餐', time: '2023-09-15 14:00', amount: '158.00', status: 'completed' }
+])
+
+watch(() => props.visible, (val) => {
+  if (val && props.petId) {
+    detailActiveTab.value = 'info'
+    currentPet.value = {}
+    changeLogs.value = []
+
+    getPet(props.petId).then((res) => {
+      currentPet.value = res.data || {}
+    })
+    
+    listAllChangeLog(props.petId, 'pet').then((res) => {
+      changeLogs.value = res.data || []
+    })
+  }
+})
+</script>
+
+<style scoped>
+.profile-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  padding-bottom: 20px;
+  border-bottom: 1px solid #f0f0f0;
+}
+.profile-basic {
+  margin-left: 20px;
+}
+.name-row {
+  display: flex;
+  align-items: center;
+}
+.name {
+  font-size: 20px;
+  font-weight: bold;
+  color: #303133;
+}
+.section-title {
+  font-size: 16px;
+  font-weight: bold;
+  margin-bottom: 15px;
+  border-left: 4px solid #409eff;
+  padding-left: 10px;
+  line-height: 1.2;
+}
+</style>

+ 2 - 2
src/views/order/dispatch/components/RiderListPanel.vue

@@ -74,11 +74,11 @@
 </template>
 
 <script setup>
-import { computed, ref } from 'vue';
+import { computed, ref, toRefs } from 'vue';
 import { useDict } from '@/utils/dict';
 import DictTag from '@/components/DictTag/index.vue';
 
-const { sys_user_sex } = useDict('sys_user_sex');
+const { sys_user_sex } = toRefs(useDict('sys_user_sex'));
 
 const props = defineProps({
   modelValue: { type: String, default: 'All' },

+ 48 - 12
src/views/order/dispatch/index.vue

@@ -50,7 +50,7 @@
 
     <RiderOrdersDialog v-model="riderOrdersVisible" :riderInfo="currentRiderInfo" :orders="currentRiderOrders" />
 
-    <DispatchDialog v-model="dispatchDialogVisible" :order="currentDispatchOrder" :currentRider="currentRider"
+    <DispatchDialog v-model:visible="dispatchDialogVisible" :order="currentDispatchOrder"
       @submit="handleDispatchSubmit" />
   </div>
 </template>
@@ -110,6 +110,7 @@ const getServiceList = () => {
 }
 
 const getOrdersList = () => {
+  if (!filters.station) return;
   listSubOrderOnDispatch({
     service: filters.orderType !== 'all' ? filters.orderType : undefined,
     site: filters.station
@@ -139,8 +140,10 @@ const getServiceName = (serviceId) => {
 };
 
 const getRidersList = () => {
+  if (!filters.station) return;
   listFulfillerOnDispatch({
-    service: filters.orderType !== 'all' ? filters.orderType : undefined
+    service: filters.orderType !== 'all' ? filters.orderType : undefined,
+    site: filters.station
   }).then(res => {
     ridersList.value = (res.data || []).map(r => ({
       ...r,
@@ -186,8 +189,15 @@ const getMerchantList = () => {
 }
 
 watch([() => filters.orderType, () => filters.station], () => {
+  if (!filters.station) {
+    ordersList.value = [];
+    ridersList.value = [];
+    merchantList.value = [];
+    refreshMarkers();
+    return;
+  }
   getOrdersList();
-  if (filters.orderType) getRidersList();
+  getRidersList();
   getMerchantList();
 });
 
@@ -418,8 +428,6 @@ watch([currentOrderTab, currentRiderTab], () => {
 onMounted(async () => {
   getServiceList()
   await getAreaStationList()
-  getRidersList()
-  getMerchantList()
   loadBMapScript()
     .then(() => {
       initMap();
@@ -436,20 +444,48 @@ const currentDispatchOrder = ref(null);
 const currentRider = ref(null);
 
 const openDispatchDialog = (order) => {
-  currentDispatchOrder.value = order;
-  if (order.fulfiller) {
-    currentRider.value = ridersList.value.find((r) => r.id === order.fulfiller) || null;
-  } else {
-    currentRider.value = null;
-  }
+  const typeName = getServiceName(order?.service);
+  const isTransport = order?.mode === 1 || order?.mode === '1';
+  const typeCode = isTransport ? 'transport' : typeName?.includes('喂') || typeName?.includes('遛') ? 'feeding' : 'washing';
+
+  const t = order?.subOrderType ?? order?.type;
+  const transportType =
+    t === 0 || t === '0' || t === 1 || t === '1'
+      ? 'round'
+      : t === 2 || t === '2'
+        ? 'pick'
+        : t === 3 || t === '3'
+          ? 'drop'
+          : order?.splitType || order?.transportType;
+
+  const toAddress = order?.toAddress || '';
+  const pickAddr = isTransport ? toAddress : '';
+  const dropAddr = isTransport ? toAddress : '';
+  const address = isTransport ? '' : toAddress;
+
+  const orderObj = {
+    id: order.id,
+    typeCode,
+    transportType,
+    time: order.serviceTime || order.appointTime || order.createTime,
+    status: order.status,
+    address,
+    pickAddr,
+    dropAddr,
+    service: order.service,
+    riderId: order.riderId || order.fulfiller || null,
+    riderGender: order.fulfillerGender
+  };
+  currentDispatchOrder.value = orderObj;
   dispatchDialogVisible.value = true;
 };
 
 const handleDispatchSubmit = async (data) => {
+  if (!currentDispatchOrder.value) return;
   try {
     const priceFen = Math.round(Number(data.fee || 0) * 100);
     await dispatchSubOrder({
-      orderId: data.order.id,
+      orderId: currentDispatchOrder.value.id,
       fulfiller: data.riderId,
       price: priceFen
     });

+ 26 - 5
src/views/system/store/index.vue

@@ -61,11 +61,11 @@
             <div>{{ scope.row.siteName }}</div>
           </template>
         </el-table-column>
-<!--        <el-table-column label="服务单" align="center" width="150">-->
-<!--          <template #default="scope">-->
-<!--            <div>{{ scope.row.serviceOrder }}</div>-->
-<!--          </template>-->
-<!--        </el-table-column>-->
+        <el-table-column label="服务单" align="center" width="150">
+          <template #default="scope">
+            <div>{{ scope.row.serviceOrder }}</div>
+          </template>
+        </el-table-column>
         <el-table-column label="营业时间" align="center" width="150">
           <template #default="scope">
             <div>{{ formatTime(scope.row.startBusinessTime) }}-{{ formatTime(scope.row.endBusinessTime) }}</div>
@@ -469,6 +469,10 @@ const reset = () => {
   city.value = '';
   district.value = '';
   addressCascaderValue.value = [];
+  tenantCategoriesList.value = [];
+  tenantCategoriesTotal.value = 0;
+  brandList.value = [];
+  brandTotal.value = 0;
   storeFormRef.value?.resetFields();
 }
 
@@ -510,6 +514,23 @@ const handleUpdate = async (row?: StoreVO) => {
   const res = await getStore(_id);
   Object.assign(form.value, res.data);
 
+  // 预填商户分类名,防止PageSelect组件在点击前显示为空或ID
+  if (res.data.tenantCatergories && (res.data as any).tenantCatergoriesName) {
+    tenantCategoriesList.value = [{
+      id: res.data.tenantCatergories,
+      name: (res.data as any).tenantCatergoriesName
+    }];
+    tenantCategoriesTotal.value = 1;
+  }
+  // 预填品牌名
+  if (res.data.tenantId && (res.data as any).tenantName) {
+    brandList.value = [{
+      tenantId: res.data.tenantId,
+      name: (res.data as any).tenantName
+    }];
+    brandTotal.value = 1;
+  }
+
   if (res.data.areaCode) {
     if (Array.isArray(res.data.areaCode)) {
       addressCascaderValue.value = res.data.areaCode;