Parcourir la source

订单列表到派单功能基本没问题

Huanyi il y a 1 mois
Parent
commit
1379a516f2

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

@@ -24,3 +24,15 @@ export function addAnamaly(data: AnamalyForm) {
         data: data
     });
 }
+
+/**
+ * 审核异常上报
+ * @param data 审核数据
+ */
+export function auditAnamaly(data: { id: number, result: number, remark?: string }) {
+    return request({
+        url: '/fulfiller/anamaly/audit',
+        method: 'put',
+        data: data
+    });
+}

+ 3 - 1
src/api/fulfiller/anamaly/types.ts

@@ -17,11 +17,13 @@ export interface AnamalyVO {
     status: number;
     createTime: string;
     // 以下为前端展示可能需要的扩充字段,因为在 Swagger 示例中可能没有体现
-    orderNo?: string;
+    orderCode?: string;
     fulfillerName?: string;
     fulfillerPhone?: string;
     auditRemark?: string;
     auditTime?: string;
+    auditor?: number | string;
+    auditorName?: string;
 }
 
 export interface AnamalyForm {

+ 4 - 4
src/views/archieves/tag/index.vue

@@ -8,7 +8,7 @@
       </template>
 
       <el-tabs v-model="activeTab" class="demo-tabs" @tab-change="handleTabChange">
-        <el-tab-pane label="用户标签" name="user">
+        <el-tab-pane label="用户标签" name="customer">
           <div class="operation-bar">
             <el-button type="primary" icon="Plus" @click="handleAdd('user')" v-hasPermi="['archieves:tag:add']">新增标签</el-button>
           </div>
@@ -134,7 +134,7 @@ const colorOptions = [
   { label: '危险', value: 'danger', color: '#F56C6C' }
 ];
 
-const activeTab = ref('user');
+const activeTab = ref('customer');
 const dialogVisible = ref(false);
 const isEdit = ref(false);
 const loading = ref(false);
@@ -145,13 +145,13 @@ const total = ref(0);
 const queryParams = reactive({
   pageNum: 1,
   pageSize: 10,
-  category: 'user'
+  category: 'customer'
 });
 
 const form = reactive({
   id: undefined,
   name: '',
-  category: 'user',
+  category: 'customer',
   colorType: 'info',
   description: '',
   type: 2,

+ 24 - 12
src/views/fulfiller/anamaly/index.vue

@@ -24,9 +24,9 @@
       </template>
 
       <el-table :data="tableData" style="width: 100%" v-loading="loading">
-        <el-table-column prop="orderId" label="关联订单号" width="180">
+        <el-table-column prop="orderCode" label="关联订单号" width="180">
           <template #default="scope">
-            <el-link type="primary" :underline="false">{{ scope.row.orderNo || scope.row.orderId }}</el-link>
+            <el-link type="primary" :underline="false">{{ scope.row.orderCode }}</el-link>
           </template>
         </el-table-column>
         <el-table-column prop="type" label="异常类型" width="120">
@@ -103,7 +103,7 @@
       <div class="drawer-content">
         <!-- 1. Basic Info -->
         <el-descriptions title="基础信息" :column="2" border>
-          <el-descriptions-item label="订单号">{{ currentItem.orderNo || currentItem.orderId }}</el-descriptions-item>
+          <el-descriptions-item label="订单号">{{ currentItem.orderCode }}</el-descriptions-item>
           <el-descriptions-item label="提交时间">{{ currentItem.createTime }}</el-descriptions-item>
           <el-descriptions-item label="履约者">{{ currentItem.fulfillerName || currentItem.fulfiller }}</el-descriptions-item>
           <el-descriptions-item label="联系电话">{{ currentItem.fulfillerPhone || '-' }}</el-descriptions-item>
@@ -161,7 +161,8 @@
             <el-timeline-item :timestamp="currentItem.createTime" placement="top" type="primary">
               <el-card shadow="never" class="log-card">
                 <h4>提交上报</h4>
-                <p>履约者 {{ currentItem.fulfillerName || currentItem.fulfiller }} 提交了异常上报</p>
+                <p>操作人:{{ currentItem.fulfillerName || '未知' }}</p>
+                <p>内容:提交了异常上报</p>
               </el-card>
             </el-timeline-item>
             <el-timeline-item
@@ -172,7 +173,7 @@
             >
               <el-card shadow="never" class="log-card">
                 <h4>{{ currentItem.status === 1 ? '审核通过' : '审核驳回' }}</h4>
-                <p>操作人:系统管理员</p>
+                <p>操作人:{{ currentItem.auditorName || '未知' }}</p>
                 <p>备注:{{ currentItem.auditRemark || '无' }}</p>
               </el-card>
             </el-timeline-item>
@@ -185,7 +186,7 @@
     <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑异常' : '新增异常'" width="600px">
       <el-form :model="form" label-width="100px">
         <el-form-item label="关联订单" required>
-          <el-input v-model="form.orderCode" placeholder="请输入订单号" />
+          <el-input v-model="form.orderCode" placeholder="请输入订单号" :disabled="isEdit" />
         </el-form-item>
         <el-form-item label="履约者" required>
           <PageSelect
@@ -198,6 +199,7 @@
             @page-change="handleFulfillerPageChange"
             @change="handleFulfillerSelect"
             style="width: 100%"
+            :disabled="isEdit"
           />
         </el-form-item>
         <el-form-item label="异常类型" required>
@@ -225,7 +227,7 @@
 <script setup lang="ts">
 import { ref, reactive, onMounted, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
-import { getAnamalyList, addAnamaly } from '@/api/fulfiller/anamaly';
+import { getAnamalyList, addAnamaly, auditAnamaly } from '@/api/fulfiller/anamaly';
 import type { AnamalyQuery, AnamalyVO, AnamalyForm } from '@/api/fulfiller/anamaly/types';
 import { listByNameAndPhoneNumber } from '@/api/fulfiller/fulfiller';
 import type { FulfillerSearchQuery } from '@/api/fulfiller/fulfiller/types';
@@ -344,7 +346,7 @@ const handleEdit = (row: AnamalyVO) => {
   isEdit.value = true;
   Object.assign(form, {
     id: row.id,
-    orderCode: row.orderNo || String(row.orderId),
+    orderCode: row.orderCode,
     fulfiller: row.fulfiller,
     type: row.type,
     content: row.content,
@@ -375,10 +377,20 @@ const handleOpenDrawer = (row: AnamalyVO, mode: string) => {
   drawerVisible.value = true;
 };
 
-const submitAudit = () => {
-  // 假设未来对接审核接口: await auditAnamaly(currentItem.value.id, auditForm.result, auditForm.remark);
-  ElMessage.success('请完善审核接口');
-  drawerVisible.value = false;
+const submitAudit = async () => {
+  if (!currentItem.value.id) return;
+  try {
+    await auditAnamaly({
+      id: currentItem.value.id,
+      result: auditForm.result,
+      remark: auditForm.remark
+    });
+    ElMessage.success('审核成功');
+    drawerVisible.value = false;
+    getList();
+  } catch (error) {
+    console.error(error);
+  }
 };
 
 const saveData = async () => {

+ 219 - 0
src/views/order/orderList/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, 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>

+ 79 - 8
src/views/order/orderList/components/DispatchDialog.vue

@@ -30,6 +30,11 @@
                             <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>
 
@@ -57,11 +62,11 @@
 
                         <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>
+                            <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 || '14:30' }}</span>
+                            <span class="last-time">下一单: {{ currentRider.nextOrderTime || '-' }}</span>
                         </div>
                     </div>
                 </div>
@@ -98,11 +103,11 @@
 
                                     <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>
+                                        <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 || '14:30' }}</span>
+                                        <span class="last-time">下一单: {{ rider.nextOrderTime || '-' }}</span>
                                     </div>
                                 </div>
 
@@ -141,6 +146,9 @@
             </div>
         </div>
     </el-dialog>
+
+    <CustomerDetailDrawer v-model:visible="customerDialogVisible" :customer-id="customerId" />
+    <PetDetailDrawer v-model:visible="petDialogVisible" :pet-id="petId" />
 </template>
 
 <script setup>
@@ -148,6 +156,10 @@ import { ref, computed, watch } 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({
     visible: Boolean,
@@ -179,6 +191,12 @@ 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 {
@@ -189,6 +207,20 @@ const loadAllTags = async () => {
     }
 }
 
+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({
@@ -200,7 +232,7 @@ const loadRiders = async () => {
         const list = res?.rows || []
         ridersList.value = list.map(r => ({
             ...r,
-            nextOrderTime: r.nextOrderTime || '14:30'
+            nextOrderTime: r.nextOrderTime || '-'
         }))
         total.value = res?.total || 0
 
@@ -224,13 +256,52 @@ watch(() => props.visible, (val) => {
         currentRider.value = null
         dispatchSearchQuery.value = ''
         selectedRiderId.value = null
-        dispatchFee.value = 0
+        // price 单位为分,转成元显示
+        dispatchFee.value = props.order?.price ? Number((props.order.price / 100).toFixed(2)) : 0
         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))
+                }
+            }
+        }).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)

+ 179 - 0
src/views/order/orderList/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>

+ 21 - 11
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>
@@ -73,7 +73,7 @@
         </el-table-column>
         <el-table-column label="门店地址" align="center" width="300">
           <template #default="scope">
-            <div>{{ scope.row.detailAddress }}</div>
+            <div>{{ getFullAddress(scope.row) }}</div>
           </template>
         </el-table-column>
         <el-table-column label="联系方式" align="left" width="180">
@@ -241,7 +241,7 @@
             <el-descriptions-item label="联系人">{{ detailData.contact }}</el-descriptions-item>
             <el-descriptions-item label="联系电话">{{ detailData.contactNumber }}</el-descriptions-item>
             <el-descriptions-item label="所在区域">{{ detailData.regionName || '北京市朝阳区' }}</el-descriptions-item>
-            <el-descriptions-item label="详细地址">{{ detailData.detailAddress }}</el-descriptions-item>
+            <el-descriptions-item label="详细地址">{{ getFullAddress(detailData) }}</el-descriptions-item>
             <el-descriptions-item label="营业执照">
               <image-preview v-if="detailData.businessLicenseUrl" :src="detailData.businessLicenseUrl" :width="80" :height="60" />
               <span v-else>-</span>
@@ -509,7 +509,7 @@ const handleUpdate = async (row?: StoreVO) => {
   const _id = row?.id || ids.value[0]
   const res = await getStore(_id);
   Object.assign(form.value, res.data);
-  
+
   if (res.data.areaCode) {
     if (Array.isArray(res.data.areaCode)) {
       addressCascaderValue.value = res.data.areaCode;
@@ -523,7 +523,7 @@ const handleUpdate = async (row?: StoreVO) => {
     if (siteData) {
       const regionId = siteData.parentId;
       form.value.regionId = regionId;
-      
+
       const path: any[] = [];
       let currentId = regionId;
       while (currentId && String(currentId) !== '0') {
@@ -536,7 +536,7 @@ const handleUpdate = async (row?: StoreVO) => {
         }
       }
       regionValue.value = path;
-      
+
       siteOptions.value = areaStationList.value
         .filter((item: any) => item.type === 2 && String(item.parentId) === String(regionId))
         .map((item: any) => ({
@@ -545,7 +545,7 @@ const handleUpdate = async (row?: StoreVO) => {
         }));
     }
   }
-  
+
   dialog.visible = true;
   dialog.title = "修改门店管理";
 }
@@ -800,6 +800,16 @@ const formatTime = (time: string | number): string => {
   return `${hours}:${minutes}`;
 };
 
+/** 获取完整门店地址:{省}{市}{区} {详细地址} */
+const getFullAddress = (row: any): string => {
+  let areaText = '';
+  if (row.areaCode) {
+    const codes = typeof row.areaCode === 'string' ? row.areaCode.split(',') : row.areaCode;
+    areaText = codes.map((code: string) => codeToText[code] || '').join('');
+  }
+  return areaText ? `${areaText} ${row.detailAddress || ''}` : (row.detailAddress || '');
+};
+
 onMounted(() => {
   getList();
   getBrandList();

+ 1 - 1
vite.config.ts

@@ -24,7 +24,7 @@ export default defineConfig(({ mode, command }) => {
       // open: true,
       proxy: {
         [env.VITE_APP_BASE_API]: {
-          target: 'http://192.168.1.118:8080',
+          target: 'http://127.0.0.1:8080',
           changeOrigin: true,
           ws: true,
           rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')