Jelajahi Sumber

bug均已修复

Huanyi 1 Minggu lalu
induk
melakukan
bd97a8b2c6

+ 1 - 1
.env.production

@@ -39,4 +39,4 @@ VITE_APP_PLATFORM_CODE = '4pwuAzDBzUd6hekvGHHKedT4VX5WHERAXHpeztPFAzRaUsBUrD'
 VITE_APP_WEBSOCKET = false
 
 # sse 开关
-VITE_APP_SSE = false
+VITE_APP_SSE = true

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

@@ -24,6 +24,7 @@ export interface AnamalyVO {
     auditTime?: string;
     auditor?: number | string;
     auditorName?: string;
+    storeName?: string;
 }
 
 export interface AnamalyForm {

+ 11 - 0
src/api/fulfiller/fulfiller/index.ts

@@ -24,3 +24,14 @@ export function getFulfillerGps(id: number | string): AxiosPromise<FulfillerGpsV
         method: 'get'
     });
 }
+
+/**
+ * 获取详细履约者信息
+ * @param id 履约者 ID
+ */
+export function getFulfiller(id: number | string) {
+    return request({
+        url: `/fulfiller/fulfiller/${id}`,
+        method: 'get'
+    });
+}

+ 1 - 1
src/api/order/subOrder/index.ts

@@ -42,7 +42,7 @@ export const getSubOrderInfo = (id: string | number): AxiosPromise<SubOrderVO> =
     });
 };
 
-export const cancelSubOrder = (data: { orderId: string | number; }) => {
+export const cancelSubOrder = (data: { orderId: string | number; reason?: string; }) => {
     return request({
         url: '/order/subOrder/cancel',
         method: 'put',

+ 55 - 38
src/views/archieves/customer/index.vue

@@ -141,7 +141,7 @@
 
           <el-col :span="24"><div class="form-section-header">基本资料</div></el-col>
           <el-col :span="12">
-            <el-form-item label="所属品牌">
+            <el-form-item label="所属品牌" required>
               <PageSelect v-model="form.tenantId"
                 :options="brandList.map(item => ({ value: item.tenantId, label: item.name }))"
                 :total="brandTotal" :pageSize="10" placeholder="请选择所属品牌"
@@ -163,7 +163,7 @@
             </el-form-item>
           </el-col>
           <el-col :span="24">
-            <el-form-item label="所属站点">
+            <el-form-item label="所属站点" required>
               <el-cascader v-model="formAreaValue" :options="areaTreeOptions" :props="{ value: 'id', label: 'name' }" placeholder="请选择站点"
                 style="width: 100%" clearable @change="handleFormAreaChange" />
             </el-form-item>
@@ -182,7 +182,7 @@
             </el-form-item>
           </el-col>
           <el-col :span="24">
-            <el-form-item label="详细住址"><el-input v-model="form.address" placeholder="请输入街道/门牌号" /></el-form-item>
+            <el-form-item label="详细住址" required><el-input v-model="form.address" placeholder="请输入街道/门牌号" /></el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="房屋类型">
@@ -192,19 +192,19 @@
             </el-form-item>
           </el-col>
           <el-col :span="12">
-            <el-form-item label="入门方式">
+            <el-form-item label="入门方式" required>
               <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="form.entryMethod === 'password'">
-            <el-form-item label="开门密码">
+            <el-form-item label="开门密码" required>
               <el-input v-model="form.entryPassword" placeholder="请输入密码" />
             </el-form-item>
           </el-col>
           <el-col :span="12" v-if="form.entryMethod === 'key'">
-            <el-form-item label="钥匙位置">
+            <el-form-item label="钥匙位置" required>
               <el-input v-model="form.keyLocation" placeholder="如:地毯下" />
             </el-form-item>
           </el-col>
@@ -281,26 +281,24 @@
                 </el-form-item>
               </el-col>
               <el-col :span="12">
-                <el-form-item label="品种">
+                <el-form-item label="品种" required>
                   <el-select v-model="petForm.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-form-item label="体型" required>
                   <el-select v-model="petForm.size" style="width: 100%">
-                    <el-option label="小型" value="small" />
-                    <el-option label="中型" value="medium" />
-                    <el-option label="大型" value="large" />
+                    <el-option v-for="dict in sys_pet_size" :key="dict.value" :label="dict.label" :value="dict.value" />
                   </el-select>
                 </el-form-item>
               </el-col>
               <el-col :span="12">
-                <el-form-item label="体重(kg)"><el-input-number v-model="petForm.weight" :min="0" :precision="1" style="width: 100%" /></el-form-item>
+                <el-form-item label="体重(kg)" required><el-input-number v-model="petForm.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="petForm.age" :min="0" style="width: 100%" /></el-form-item>
+                <el-form-item label="年龄(岁)" required><el-input-number v-model="petForm.age" :min="0" style="width: 100%" /></el-form-item>
               </el-col>
               <el-col :span="24">
                 <el-form-item label="性格关键词"><el-input v-model="petForm.personality" placeholder="如:活泼、粘人" /></el-form-item>
@@ -325,40 +323,38 @@
             <el-form-item label="新来家庭时间">
               <el-date-picker v-model="petForm.arrivalTime" type="date" placeholder="选择日期" style="width: 100%" />
             </el-form-item>
-            <el-form-item label="家庭房屋类型">
+            <el-form-item label="家庭房屋类型" required>
               <el-radio-group v-model="petForm.houseType">
-                <el-radio label="stairs">楼梯</el-radio>
-                <el-radio label="elevator">电梯</el-radio>
+                <el-radio v-for="dict in sys_house_type" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
               </el-radio-group>
             </el-form-item>
-            <el-form-item label="入门方式">
+            <el-form-item label="入门方式" required>
               <el-radio-group v-model="petForm.entryMethod">
-                <el-radio label="password">密码开门</el-radio>
-                <el-radio label="key">钥匙开门</el-radio>
+                <el-radio v-for="dict in sys_entry_method" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
               </el-radio-group>
             </el-form-item>
-            <el-form-item label="密码" v-if="petForm.entryMethod === 'password'">
+            <el-form-item label="密码" v-if="petForm.entryMethod === 'password'" required>
               <el-input v-model="petForm.entryPassword" placeholder="请输入门锁密码" />
             </el-form-item>
-            <el-form-item label="钥匙位置" v-if="petForm.entryMethod === 'key'">
+            <el-form-item label="钥匙位置" v-if="petForm.entryMethod === 'key'" required>
               <el-input v-model="petForm.keyLocation" placeholder="请输入钥匙存放位置" />
             </el-form-item>
           </el-form>
         </el-tab-pane>
         <el-tab-pane label="健康状况" name="health">
           <el-form :model="petForm" label-width="120px">
-            <el-form-item label="健康状态">
+            <el-form-item label="健康状态" required>
               <el-radio-group v-model="petForm.healthStatus">
                 <el-radio label="健康">健康</el-radio>
                 <el-radio label="亚健康">亚健康</el-radio>
                 <el-radio label="疾病">疾病</el-radio>
               </el-radio-group>
             </el-form-item>
-            <el-form-item label="是否有攻击倾向">
+            <el-form-item label="是否有攻击倾向" required>
               <el-switch v-model="petForm.aggression" active-text="是" inactive-text="否" :active-value="1" :inactive-value="0" />
             </el-form-item>
-            <el-form-item label="疫苗情况">
-              <el-radio-group v-model="petForm.vaccine">
+            <el-form-item label="疫苗情况" required>
+              <el-radio-group v-model="petForm.vaccineStatus">
                 <el-radio label="无">无</el-radio>
                 <el-radio label="已打1次">已打1次</el-radio>
                 <el-radio label="已打2次">已打2次</el-radio>
@@ -371,10 +367,10 @@
                 <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-form-item label="既往病史" required>
               <el-input v-model="petForm.medicalHistory" type="textarea" placeholder="如有病史请记录" />
             </el-form-item>
-            <el-form-item label="过敏史">
+            <el-form-item label="过敏史" required>
               <el-input v-model="petForm.allergies" type="textarea" placeholder="如有过敏源请记录" />
             </el-form-item>
           </el-form>
@@ -764,8 +760,14 @@ const savePetRemark = () => {
 }
 
 const saveUser = () => {
+  if (!form.tenantId) return ElMessage.warning('请选择所属品牌')
   if (!form.name) return ElMessage.warning('请输入姓名')
   if (!form.phone) return ElMessage.warning('请输入电话')
+  if (!form.stationId) return ElMessage.warning('请选择所属站点')
+  if (!form.address) return ElMessage.warning('请输入详细住址')
+  if (!form.entryMethod) return ElMessage.warning('请选择入门方式')
+  if (form.entryMethod === 'password' && !form.entryPassword) return ElMessage.warning('请输入开门密码')
+  if (form.entryMethod === 'key' && !form.keyLocation) return ElMessage.warning('请输入钥匙存放位置')
   submitLoading.value = true
   form.tagIds = selectedTagIds.value
   if (regionCascaderValue.value && regionCascaderValue.value.length > 0) {
@@ -943,19 +945,34 @@ const handlePetDelete = (row) => {
 }
 
 const savePet = () => {
-  if (!petForm.name) return ElMessage.warning('请输入宠物昵称')
-  submitLoading.value = true
-  const data = { ...petForm, aggression: Number(petForm.aggression) || 0 }
-  const api = data.id ? updatePet(data) : addPet(data)
+  if (!petForm.name) return ElMessage.warning('请输入宠物姓名');
+  if (!petForm.userId) return ElMessage.warning('请选择所属主人');
+  if (!petForm.breed) return ElMessage.warning('请选择品种');
+  if (!petForm.size) return ElMessage.warning('请选择体型');
+  if (petForm.weight === undefined || petForm.weight === null) return ElMessage.warning('请输入体重(kg)');
+  if (petForm.age === undefined || petForm.age === null) return ElMessage.warning('请输入年龄(岁)');
+  if (!petForm.houseType) return ElMessage.warning('请选择家庭房屋类型');
+  if (!petForm.entryMethod) return ElMessage.warning('请选择入门方式');
+  if (petForm.entryMethod === 'password' && !petForm.entryPassword) return ElMessage.warning('请输入门锁密码');
+  if (petForm.entryMethod === 'key' && !petForm.keyLocation) return ElMessage.warning('请输入钥匙存放位置');
+  if (!petForm.healthStatus) return ElMessage.warning('请选择健康状态');
+  if (petForm.aggression === undefined || petForm.aggression === null) return ElMessage.warning('请选择是否有攻击倾向');
+  if (!petForm.vaccineStatus) return ElMessage.warning('请选择疫苗情况');
+  if (!petForm.medicalHistory) return ElMessage.warning('请输入既往病史');
+  if (!petForm.allergies) return ElMessage.warning('请输入过敏史');
+
+  submitLoading.value = true;
+  const data = { ...petForm, aggression: Number(petForm.aggression) || 0 };
+  const api = data.id ? updatePet(data) : addPet(data);
   api.then(() => {
-    ElMessage.success('宠物档案保存成功')
-    petDialogVisible.value = false
-    customerDetailRef.value?.refresh()
-    getList()
+    ElMessage.success('宠物档案保存成功');
+    petDialogVisible.value = false;
+    customerDetailRef.value?.refresh();
+    getList();
   }).finally(() => {
-    submitLoading.value = false
-  })
-}
+    submitLoading.value = false;
+  });
+};
 
 onMounted(() => {
   getList()

+ 27 - 14
src/views/archieves/pet/index.vue

@@ -102,24 +102,24 @@
                 </el-form-item>
               </el-col>
               <el-col :span="12">
-                <el-form-item label="品种">
+                <el-form-item label="品种" required>
                   <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-form-item label="体型" required>
                   <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-form-item label="体重(kg)" required><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-form-item label="年龄(岁)" required><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>
@@ -144,37 +144,37 @@
             <el-form-item label="新来家庭时间">
               <el-date-picker v-model="form.arrivalTime" type="date" placeholder="选择日期" style="width: 100%" />
             </el-form-item>
-            <el-form-item label="家庭房屋类型">
+            <el-form-item label="家庭房屋类型" required>
               <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-form-item label="入门方式" required>
               <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="form.entryMethod === 'password'">
+            <el-form-item label="密码" v-if="form.entryMethod === 'password'" required>
               <el-input v-model="form.entryPassword" placeholder="请输入门锁密码" />
             </el-form-item>
-            <el-form-item label="钥匙位置" v-if="form.entryMethod === 'key'">
+            <el-form-item label="钥匙位置" v-if="form.entryMethod === 'key'" required>
               <el-input v-model="form.keyLocation" placeholder="请输入钥匙存放位置" />
             </el-form-item>
           </el-form>
         </el-tab-pane>
         <el-tab-pane label="健康状况" name="health">
           <el-form :model="form" label-width="120px">
-            <el-form-item label="健康状态">
+            <el-form-item label="健康状态" required>
               <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="form.aggression" active-text="是" inactive-text="否" />
+            <el-form-item label="是否有攻击倾向" required>
+              <el-switch v-model="form.aggression" active-text="是" inactive-text="否" :active-value="1" :inactive-value="0" />
             </el-form-item>
-            <el-form-item label="疫苗情况">
+            <el-form-item label="疫苗情况" required>
               <el-radio-group v-model="form.vaccineStatus">
                 <el-radio label="无">无</el-radio>
                 <el-radio label="已打1次">已打1次</el-radio>
@@ -188,10 +188,10 @@
                 <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-form-item label="既往病史" required>
               <el-input v-model="form.medicalHistory" type="textarea" placeholder="如有病史请记录" />
             </el-form-item>
-            <el-form-item label="过敏史">
+            <el-form-item label="过敏史" required>
               <el-input v-model="form.allergies" type="textarea" placeholder="如有过敏源请记录" />
             </el-form-item>
           </el-form>
@@ -441,6 +441,19 @@ const handleUploadVaccineCert = async (file) => {
 const saveData = () => {
   if (!form.name) return ElMessage.warning('请输入宠物姓名');
   if (!form.userId) return ElMessage.warning('请选择所属主人');
+  if (!form.breed) return ElMessage.warning('请选择品种');
+  if (!form.size) return ElMessage.warning('请选择体型');
+  if (form.weight === undefined || form.weight === null) return ElMessage.warning('请输入体重(kg)');
+  if (form.age === undefined || form.age === null) return ElMessage.warning('请输入年龄(岁)');
+  if (!form.houseType) return ElMessage.warning('请选择家庭房屋类型');
+  if (!form.entryMethod) return ElMessage.warning('请选择入门方式');
+  if (form.entryMethod === 'password' && !form.entryPassword) return ElMessage.warning('请输入门锁密码');
+  if (form.entryMethod === 'key' && !form.keyLocation) return ElMessage.warning('请输入钥匙存放位置');
+  if (!form.healthStatus) return ElMessage.warning('请选择健康状态');
+  if (form.aggression === undefined || form.aggression === null) return ElMessage.warning('请选择是否有攻击倾向');
+  if (!form.vaccineStatus) return ElMessage.warning('请选择疫苗情况');
+  if (!form.medicalHistory) return ElMessage.warning('请输入既往病史');
+  if (!form.allergies) return ElMessage.warning('请输入过敏史');
   submitLoading.value = true;
   const api = isEdit.value ? updatePet(form) : addPet(form);
   api.then(() => {

+ 6 - 0
src/views/fulfiller/anamaly/index.vue

@@ -29,6 +29,11 @@
             <el-link type="primary" :underline="false">{{ scope.row.orderCode }}</el-link>
           </template>
         </el-table-column>
+        <el-table-column prop="storeName" label="所属门店" width="150" show-overflow-tooltip>
+          <template #default="scope">
+            <span>{{ scope.row.storeName || '-' }}</span>
+          </template>
+        </el-table-column>
         <el-table-column prop="type" label="异常类型" width="120">
           <template #default="scope">
             <dict-tag :options="flf_anamaly_type" :value="scope.row.type" />
@@ -104,6 +109,7 @@
         <!-- 1. Basic Info -->
         <el-descriptions title="基础信息" :column="2" border>
           <el-descriptions-item label="订单号">{{ currentItem.orderCode }}</el-descriptions-item>
+          <el-descriptions-item label="所属门店">{{ currentItem.storeName || '-' }}</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>

+ 21 - 4
src/views/fulfiller/pool/index.vue

@@ -691,7 +691,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue'
+import { ref, reactive, computed, onMounted, onUnmounted, getCurrentInstance } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import {
   listFulfiller, getFulfiller, addFulfiller, updateFulfiller,
@@ -782,8 +782,11 @@ const getStationPathText = (stationId: number | string | undefined) => {
 }
 
 /** 查询列表 */
-const getList = async () => {
-  loading.value = true
+const getList = async (isPolling: any = false) => {
+  const isAutoPoll = isPolling === true;
+  if (!isAutoPoll) {
+    loading.value = true
+  }
   try {
     const params: FlfFulfillerQuery = {
       pageNum: queryParams.pageNum,
@@ -796,7 +799,9 @@ const getList = async () => {
     tableData.value = res.rows
     total.value = res.total
   } finally {
-    loading.value = false
+    if (!isAutoPoll) {
+      loading.value = false
+    }
   }
 }
 
@@ -1284,11 +1289,23 @@ const handleCreateCascaderChange = (val: any[]) => {
   }
 }
 
+let timer: any = null;
+
 onMounted(() => {
   getList()
   loadAllTags()
   loadAreaStations()
   loadServiceOptions()
+  timer = setInterval(() => {
+    getList(true);
+  }, 5000);
+})
+
+onUnmounted(() => {
+  if (timer) {
+    clearInterval(timer);
+    timer = null;
+  }
 })
 </script>
 

+ 18 - 10
src/views/order/dispatch/components/DispatchDialog.vue

@@ -29,11 +29,19 @@
                             </el-icon> {{ order.time }}
                             <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
                         </div>
+                        <div class="row-remark" style="margin-top: 4px; font-size: 13px; color: #909399;">
+                            备注: {{ orderDetail?.remark || '-' }}
+                        </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 class="card-right" style="display: flex; flex-direction: column; gap: 8px; justify-content: center; padding-left: 20px;">
+                        <div style="display: flex; gap: 10px;">
+                            <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 style="font-size: 12px; color: #ff9900; background: #fff8e6; padding: 2px 6px; border-radius: 4px; border: 1px solid #ffd591; text-align: center;">
+                            团购套餐: {{ orderDetail?.groupPurchasePackageName || '-' }}
+                        </div>
                     </div>
                 </div>
             </div>
@@ -143,7 +151,7 @@
                 </div>
                 <div class="btns">
                     <el-button @click="dialogVisible = false">取消</el-button>
-                    <el-button type="primary" :disabled="!canSubmit" @click="handleDispatchSubmit">确认派单</el-button>
+                    <el-button type="primary" @click="handleDispatchSubmit">确认派单</el-button>
                 </div>
             </div>
         </div>
@@ -206,6 +214,7 @@ const petDialogVisible = ref(false)
 const customerId = ref(null)
 const petId = ref(null)
 const orderInfoLoading = ref(false)
+const orderDetail = ref(null)
 
 const loadAllTags = async () => {
     if (allTags.value && allTags.value.length > 0) return
@@ -240,7 +249,8 @@ const loadRiders = async () => {
             content: dispatchSearchQuery.value || undefined,
             pageNum: pageNum.value,
             pageSize: pageSize.value,
-            service: props.order?.service
+            service: props.order?.service,
+            orderId: props.order?.id
         })
         const list = res?.rows || []
         ridersList.value = list.map(r => ({
@@ -270,6 +280,7 @@ watch(() => props.visible, (val) => {
         currentRider.value = null
         dispatchSearchQuery.value = ''
         selectedRiderId.value = null
+        orderDetail.value = null
         // price 单位为分,转成元显示
         dispatchFee.value = props.order?.price ? Number((props.order.price / 100).toFixed(2)) : 0
         if (props.order?.riderId) {
@@ -289,6 +300,7 @@ watch(() => props.visible, (val) => {
         orderInfoLoading.value = true
         getSubOrderInfo(props.order.id).then((res) => {
             if(res.data) {
+                orderDetail.value = res.data;
                 // 如果 usrCustomer / usrPet 是对象则取其 id,如果是 ID 直接取
                 customerId.value = res.data.usrCustomer?.id || res.data.usrCustomer
                 petId.value = res.data.usrPet?.id || res.data.usrPet
@@ -378,10 +390,6 @@ const filteredDispatchRiders = computed(() => {
     return ridersList.value || []
 })
 
-const canSubmit = computed(() => {
-    return !!selectedRiderId.value && !!dispatchFee.value
-})
-
 const handleDispatchSubmit = () => {
     if (!selectedRiderId.value) {
         ElMessage.warning('请选择履约者')
@@ -533,7 +541,7 @@ const handleDispatchSubmit = () => {
 }
 
 .rider-scroll {
-    max-height: 45vh;
+    height: 320px;
 }
 
 .rider-grid {

+ 19 - 10
src/views/order/orderList/components/DispatchDialog.vue

@@ -29,11 +29,19 @@
                             </el-icon> {{ order.time }}
                             <span class="days-tag" v-if="order.daysLater">{{ order.daysLater }}</span>
                         </div>
+                        <div class="row-remark" style="margin-top: 4px; font-size: 13px; color: #909399;">
+                            备注: {{ orderDetail?.remark || '-' }}
+                        </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 class="card-right" style="display: flex; flex-direction: column; gap: 8px; justify-content: center; padding-left: 20px;">
+                        <div style="display: flex; gap: 10px;">
+                            <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 style="font-size: 12px; color: #ff9900; background: #fff8e6; padding: 2px 6px; border-radius: 4px; border: 1px solid #ffd591; text-align: center;">
+                            团购套餐: {{ orderDetail?.groupPurchasePackageName || '-' }}
+                        </div>
                     </div>
                 </div>
             </div>
@@ -143,7 +151,7 @@
                 </div>
                 <div class="btns">
                     <el-button @click="dialogVisible = false">取消</el-button>
-                    <el-button type="primary" :disabled="!canSubmit" @click="handleDispatchSubmit">确认派单</el-button>
+                    <el-button type="primary" @click="handleDispatchSubmit">确认派单</el-button>
                 </div>
             </div>
         </div>
@@ -202,6 +210,7 @@ const petDialogVisible = ref(false)
 const customerId = ref(null)
 const petId = ref(null)
 const orderInfoLoading = ref(false)
+const orderDetail = ref(null)
 
 const loadAllTags = async () => {
     if (allTags.value && allTags.value.length > 0) return
@@ -242,7 +251,8 @@ const loadRiders = async () => {
             content: dispatchSearchQuery.value || undefined,
             pageNum: pageNum.value,
             pageSize: pageSize.value,
-            service: props.order?.service
+            service: props.order?.service,
+            orderId: props.order?.id
         })
         const list = res?.rows || []
         ridersList.value = list.map(r => ({
@@ -272,6 +282,7 @@ watch(() => props.visible, (val) => {
         currentRider.value = null
         dispatchSearchQuery.value = ''
         selectedRiderId.value = null
+        orderDetail.value = null
         // price 单位为分,转成元显示
         dispatchFee.value = props.order?.price ? Number((props.order.price / 100).toFixed(2)) : 0
         if (props.order?.riderId) {
@@ -292,6 +303,7 @@ watch(() => props.visible, (val) => {
         orderInfoLoading.value = true
         getSubOrderInfo(props.order.id).then((res) => {
             if(res.data) {
+                orderDetail.value = res.data;
                 // 如果 usrCustomer / usrPet 是对象则取其 id,如果是 ID 直接取
                 customerId.value = res.data.usrCustomer?.id || res.data.usrCustomer
                 petId.value = res.data.usrPet?.id || res.data.usrPet
@@ -381,10 +393,6 @@ const filteredDispatchRiders = computed(() => {
     return ridersList.value || []
 })
 
-const canSubmit = computed(() => {
-    return !!selectedRiderId.value && !!dispatchFee.value
-})
-
 const handleDispatchSubmit = () => {
     if (!selectedRiderId.value) {
         ElMessage.warning('请选择履约者')
@@ -398,6 +406,7 @@ const handleDispatchSubmit = () => {
     emit('submit', {
         riderId: rider.id,
         riderName: rider.name,
+        riderPhone: rider.phone,
         fee: dispatchFee.value
     })
     dialogVisible.value = false
@@ -536,7 +545,7 @@ const handleDispatchSubmit = () => {
 }
 
 .rider-scroll {
-    max-height: 45vh;
+    height: 320px;
 }
 
 .rider-grid {

+ 56 - 19
src/views/order/orderList/components/OrderDetailDrawer.vue

@@ -82,18 +82,18 @@
                                     </el-icon>
                                 </div>
                                 <div class="b-tags">
-                                    <el-tag size="small" type="info">{{ order.petAge || '未知年龄' }}</el-tag>
-                                    <el-tag size="small" type="info">{{ order.petWeight || '未知体重' }}</el-tag>
+                                    <el-tag size="small" type="info">{{ order.petAge || '-' }}</el-tag>
+                                    <el-tag size="small" type="info">{{ order.petWeight || '-' }}</el-tag>
                                 </div>
                             </div>
                         </div>
                         <el-descriptions :column="2" size="small" class="pet-desc" border>
-                            <el-descriptions-item label="宠物品种">{{ order.petBreed || '未知'
+                            <el-descriptions-item label="宠物品种">{{ order.petBreed || '-'
                             }}</el-descriptions-item>
-                            <el-descriptions-item label="疫苗状态"><span style="color:#67c23a">{{ order.petVaccine || '未知'
+                            <el-descriptions-item label="疫苗状态"><span style="color:#67c23a">{{ order.petVaccine || '-'
                             }}</span></el-descriptions-item>
-                            <el-descriptions-item label="性格特点">{{ order.petCharacter || '温顺' }}</el-descriptions-item>
-                            <el-descriptions-item label="健康状况">{{ order.petHealth || '健康' }}</el-descriptions-item>
+                            <el-descriptions-item label="性格特点">{{ order.petCharacter || '-' }}</el-descriptions-item>
+                            <el-descriptions-item label="健康状况">{{ order.petHealth || '-' }}</el-descriptions-item>
                         </el-descriptions>
                     </div>
 
@@ -145,12 +145,12 @@
 
                                     <el-descriptions-item label="预约时间">{{ getServiceTimeRange(order.serviceTime)
                                     }}</el-descriptions-item>
-                                    <el-descriptions-item label="团购套餐">{{ order.groupBuyPackage || '未使用团购套餐'
+                                    <el-descriptions-item label="团购套餐">{{ order.groupBuyPackage || '-'
                                     }}</el-descriptions-item>
                                     <el-descriptions-item label="创建时间">{{ order.createTime }}</el-descriptions-item>
 
                                     <el-descriptions-item label="订单备注" :span="3">
-                                        {{ order.remark || '暂无备注' }}
+                                        {{ order.remark || '-' }}
                                     </el-descriptions-item>
                                 </el-descriptions>
                             </div>
@@ -165,19 +165,19 @@
                                     </div>
                                     <div class="t-row">
                                         <span class="t-k">起点</span>
-                                        <span class="t-v">{{ order.detail?.fromAddress || order.detail?.pickAddr || '--'
+                                        <span class="t-v">{{ order.detail?.fromAddress || order.detail?.pickAddr || '-'
                                             }}</span>
                                     </div>
                                     <div class="t-row">
                                         <span class="t-k">终点</span>
                                         <span class="t-v">{{ order.detail?.toAddress || order.detail?.dropAddr ||
                                             order.toAddress ||
-                                            '--' }}</span>
+                                            '-' }}</span>
                                     </div>
                                     <div class="t-row sub">
-                                        <span class="t-v">{{ order.contact || order.userName || '--' }} {{
+                                        <span class="t-v">{{ order.contact || order.userName || '-' }} {{
                                             order.contactPhoneNumber
-                                            || order.contactPhone || '--' }}</span>
+                                            || order.contactPhone || '-' }}</span>
                                     </div>
                                 </div>
                             </div>
@@ -203,12 +203,12 @@
                                 <div class="f-right">
                                     <div class="f-row1">
                                         <span class="f-name">{{ order.fulfillerName }}</span>
-                                        <el-tag size="small" type="primary" effect="plain" round>Lv1 普通</el-tag>
+                                        <el-tag size="small" type="primary" effect="plain" round>{{ order.fulfillerLevelName || '普通履约者' }}</el-tag>
                                     </div>
                                     <div class="f-row2">
-                                        <span>联系电话:{{ order.fulfillerPhone || '138****0000' }}</span>
+                                        <span>联系电话:{{ order.fulfillerPhone || '-' }}</span>
                                         <span class="sep">|</span>
-                                        <span>归属区域:{{ order.fulfillerStation || '朝阳一站' }}</span>
+                                        <span>归属区域:{{ order.fulfillerStation || '-' }}</span>
                                     </div>
                                     <div class="f-row3"
                                         style="margin-top: 8px; font-size: 13px; color: #606266; background: #f9fafe; padding: 8px; border-radius: 4px; display: flex; gap: 20px;">
@@ -323,6 +323,8 @@ import { ref, reactive, computed, watch, getCurrentInstance } from 'vue'
 import { ElMessage } from 'element-plus'
 import { getPet } from '@/api/archieves/pet'
 import { getCustomer } from '@/api/archieves/customer'
+import { getSubOrderInfo } from '@/api/order/subOrder/index'
+import { getFulfiller } from '@/api/fulfiller/fulfiller/index'
 import { listSubOrderLog, exportSubOrderLogUrl } from '@/api/order/subOrderLog/index'
 import { listComplaintByOrder } from '@/api/fulfiller/complaint'
 
@@ -398,7 +400,29 @@ const loadPetAndCustomer = async (order) => {
     const seq = ++loadSeq.value
     const next = { ...(order || {}) }
 
-    const petId = next?.pet || next?.petId
+    if (next.id) {
+        try {
+            const orderRes = await getSubOrderInfo(next.id)
+            if (orderRes.data) {
+                if (orderRes.data.type == null) {
+                    delete orderRes.data.type;
+                } else {
+                    const typeMap = { 0: 'transport', 1: 'feeding', 2: 'washing' };
+                    if (typeMap[orderRes.data.type] !== undefined) {
+                        orderRes.data.type = typeMap[orderRes.data.type];
+                    }
+                }
+                Object.assign(next, orderRes.data)
+                if (orderRes.data.price != null) {
+                    next.fulfillerFee = (orderRes.data.price / 100).toFixed(2)
+                }
+            }
+        } catch (e) {
+            console.error('获取订单详细信息失败', e)
+        }
+    }
+
+    const petId = next?.usrPet || next?.pet || next?.petId
     if (petId) {
         try {
             const res = await getPet(petId)
@@ -421,7 +445,7 @@ const loadPetAndCustomer = async (order) => {
         }
     }
 
-    const customerId = next?.customer || next?.customerId
+    const customerId = next?.usrCustomer || next?.customer || next?.customerId
     if (customerId) {
         try {
             const res = await getCustomer(customerId)
@@ -437,6 +461,19 @@ const loadPetAndCustomer = async (order) => {
         }
     }
 
+    const fulfillerId = next?.fulfiller
+    if (fulfillerId) {
+        try {
+            const fRes = await getFulfiller(fulfillerId)
+            const fulfiller = fRes?.data
+            if (fulfiller) {
+                next.fulfillerStation = fulfiller.stationName ?? next.fulfillerStation
+                next.fulfillerLevelName = fulfiller.levelName ?? next.fulfillerLevelName
+            }
+        } catch {
+        }
+    }
+
     if (seq !== loadSeq.value) return
     orderDetail.value = next
 }
@@ -464,7 +501,7 @@ const getStatusTag = (status) => {
 }
 const getTypeName = (type) => {
     const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' }
-    return map[type]
+    return map[type] || '-'
 }
 const getTransportModeName = (type) => {
     const map = { round: '往返接送', pick: '单程接(到店)', drop: '单程送(回家)' }
@@ -479,7 +516,7 @@ const getTransportLabel = (t) => {
     return '接送'
 }
 const getServiceTimeRange = (timeStr) => {
-    if (!timeStr) return '--'
+    if (!timeStr) return '-'
     try {
         if (timeStr.length < 16) return timeStr
         let timePart = timeStr.substring(11, 16)

+ 61 - 30
src/views/order/orderList/index.vue

@@ -105,17 +105,33 @@
           </template>
         </el-table-column>
 
-        <el-table-column label="履约信息" width="140">
+        <el-table-column label="履约者" width="120">
           <template #default="{ row }">
-            <div v-if="row.fulfillerName" class="fulfiller-info">
-              <span class="fulfiller-name">{{ row.fulfillerName }}</span>
-              <span class="fulfiller-fee" v-if="row.price !== null && row.price !== undefined">¥{{ row.price / 100.0
-              }}</span>
-            </div>
+            <span v-if="row.fulfillerName" style="font-weight: 500; color: #333;">{{ row.fulfillerName }}</span>
             <span v-else class="text-gray">暂未指派</span>
           </template>
         </el-table-column>
 
+        <el-table-column label="履约电话" width="130">
+          <template #default="{ row }">
+            <div v-if="row.fulfillerPhone" style="display: flex; align-items: center; gap: 4px;">
+              <el-icon><Phone /></el-icon>
+              <span>{{ row.fulfillerPhone }}</span>
+            </div>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+
+        <el-table-column label="履约佣金" width="100">
+          <template #default="{ row }">
+            <span v-if="row.price !== null && row.price !== undefined"
+              style="color: #f56c6c; font-size: 14px; font-weight: bold;">
+              ¥{{ (row.price / 100).toFixed(2) }}
+            </span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+
         <el-table-column label="操作" width="200" fixed="right">
           <template #default="{ row }">
             <div class="op-cell">
@@ -186,7 +202,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted, nextTick, getCurrentInstance } from 'vue';
+import { ref, reactive, onMounted, onUnmounted, nextTick, getCurrentInstance } from 'vue';
 import { useRouter } from 'vue-router';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import fulfillerEnums from '@/json/fulfiller.json';
@@ -234,10 +250,22 @@ const areaStationList = ref([]);
 const areaStationMap = ref({});
 const storeMap = ref({});
 
+let timer: any = null;
+
 onMounted(() => {
   getServiceList();
   getAreaStationList();
   handleSearch();
+  timer = setInterval(() => {
+    handleSearch(true);
+  }, 5000);
+});
+
+onUnmounted(() => {
+  if (timer) {
+    clearInterval(timer);
+    timer = null;
+  }
 });
 
 const getServiceList = () => {
@@ -289,8 +317,11 @@ const handleStatusTabChange = async () => {
   handleSearch();
 };
 
-const handleSearch = () => {
-  loading.value = true;
+const handleSearch = (isPolling: any = false) => {
+  const isAutoPoll = isPolling === true;
+  if (!isAutoPoll) {
+    loading.value = true;
+  }
   listSubOrder({
     pageNum: pagination.current,
     pageSize: pagination.size,
@@ -302,10 +333,10 @@ const handleSearch = () => {
       tableData.value = res.rows || [];
       pagination.total = res.total || 0;
       loadStoresForRows(tableData.value);
-      loading.value = false;
+      if (!isAutoPoll) loading.value = false;
     })
     .catch(() => {
-      loading.value = false;
+      if (!isAutoPoll) loading.value = false;
     });
 };
 
@@ -506,12 +537,25 @@ const handleDetail = async (row) => {
 };
 
 // 取消订单
-const handleCancel = (row) => {
-  ElMessageBox.confirm('确认取消该订单吗?', '提示', { type: 'warning' }).then(() => {
-    cancelSubOrder({ orderId: row?.id }).then(() => {
-      ElMessage.success('订单已取消');
+const handleCancel = (row: any) => {
+  ElMessageBox.prompt('请输入订单取消原因', '订单确认取消', {
+    confirmButtonText: '确认取消',
+    cancelButtonText: '暂不取消',
+    inputPlaceholder: '请输入取消原因(必填)',
+    inputValidator: (value) => {
+      if (!value || !value.trim()) {
+        return '取消原因不能为空';
+      }
+      return true;
+    },
+    type: 'warning'
+  }).then(({ value }) => {
+    cancelSubOrder({ orderId: row?.id, reason: value }).then(() => {
+      ElMessage.success('订单已成功取消');
       handleSearch();
     });
+  }).catch(() => {
+    // 用户取消输入
   });
 };
 
@@ -566,7 +610,8 @@ const handleDispatchSubmit = (payload) => {
     if (row) {
       row.status = 1;
       row.fulfillerName = payload.riderName || 'Unknown';
-      row.price = payload.fee;
+      row.fulfillerPhone = payload.riderPhone;
+      row.price = Math.round(Number(payload.fee || 0) * 100);
     }
     handleSearch();
   });
@@ -932,20 +977,6 @@ const handleExport = () => {
   background-color: #909399;
 }
 
-.fulfiller-info {
-  display: flex;
-  flex-direction: column;
-}
-
-.fulfiller-name {
-  font-weight: 500;
-  color: #333;
-}
-
-.fulfiller-fee {
-  font-size: 12px;
-  color: #e6a23c;
-}
 
 .op-cell {
   display: flex;

+ 26 - 13
src/views/order/purchase/components/AddPetDialog.vue

@@ -28,24 +28,24 @@
               </el-form-item>
             </el-col>
             <el-col :span="12">
-              <el-form-item label="品种">
+              <el-form-item label="品种" required>
                 <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-form-item label="体型" required>
                 <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-form-item label="体重(kg)" required><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-form-item label="年龄(岁)" required><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>
@@ -70,37 +70,37 @@
           <el-form-item label="新来家庭时间">
             <el-date-picker v-model="form.arrivalTime" type="date" placeholder="选择日期" style="width: 100%" />
           </el-form-item>
-          <el-form-item label="家庭房屋类型">
+          <el-form-item label="家庭房屋类型" required>
             <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-form-item label="入门方式" required>
             <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="form.entryMethod === 'password'">
+          <el-form-item label="密码" v-if="form.entryMethod === 'password'" required>
             <el-input v-model="form.entryPassword" placeholder="请输入门锁密码" />
           </el-form-item>
-          <el-form-item label="钥匙位置" v-if="form.entryMethod === 'key'">
+          <el-form-item label="钥匙位置" v-if="form.entryMethod === 'key'" required>
             <el-input v-model="form.keyLocation" placeholder="请输入钥匙存放位置" />
           </el-form-item>
         </el-form>
       </el-tab-pane>
       <el-tab-pane label="健康状况" name="health">
         <el-form :model="form" label-width="120px">
-          <el-form-item label="健康状态">
+          <el-form-item label="健康状态" required>
             <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-form-item label="是否有攻击倾向" required>
             <el-switch v-model="form.aggression" active-text="是" inactive-text="否" :active-value="1" :inactive-value="0" />
           </el-form-item>
-          <el-form-item label="疫苗情况">
+          <el-form-item label="疫苗情况" required>
             <el-radio-group v-model="form.vaccineStatus">
               <el-radio label="无">无</el-radio>
               <el-radio label="已打1次">已打1次</el-radio>
@@ -114,10 +114,10 @@
               <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-form-item label="既往病史" required>
             <el-input v-model="form.medicalHistory" type="textarea" placeholder="如有病史请记录" />
           </el-form-item>
-          <el-form-item label="过敏史">
+          <el-form-item label="过敏史" required>
             <el-input v-model="form.allergies" type="textarea" placeholder="如有过敏源请记录" />
           </el-form-item>
         </el-form>
@@ -265,6 +265,19 @@ const handleUploadVaccineCert = async (file) => {
 const saveData = () => {
   if (!form.name) return ElMessage.warning('请输入宠物姓名')
   if (!form.userId) return ElMessage.warning('请先选择或新增所属主人')
+  if (!form.breed) return ElMessage.warning('请选择品种');
+  if (!form.size) return ElMessage.warning('请选择体型');
+  if (form.weight === undefined || form.weight === null) return ElMessage.warning('请输入体重(kg)');
+  if (form.age === undefined || form.age === null) return ElMessage.warning('请输入年龄(岁)');
+  if (!form.houseType) return ElMessage.warning('请选择家庭房屋类型');
+  if (!form.entryMethod) return ElMessage.warning('请选择入门方式');
+  if (form.entryMethod === 'password' && !form.entryPassword) return ElMessage.warning('请输入门锁密码');
+  if (form.entryMethod === 'key' && !form.keyLocation) return ElMessage.warning('请输入钥匙存放位置');
+  if (!form.healthStatus) return ElMessage.warning('请选择健康状态');
+  if (form.aggression === undefined || form.aggression === null) return ElMessage.warning('请选择是否有攻击倾向');
+  if (!form.vaccineStatus) return ElMessage.warning('请选择疫苗情况');
+  if (!form.medicalHistory) return ElMessage.warning('请输入既往病史');
+  if (!form.allergies) return ElMessage.warning('请输入过敏史');
 
   submitLoading.value = true
   addPetOnOrder(form).then(res => {

+ 10 - 5
src/views/order/purchase/components/AddUserDialog.vue

@@ -24,7 +24,7 @@
           </el-form-item>
         </el-col>
         <el-col :span="24">
-          <el-form-item label="所属站点">
+          <el-form-item label="所属站点" required>
             <el-cascader v-model="formAreaValue" :options="areaTreeOptions" :props="{ value: 'id', label: 'name' }" placeholder="请选择站点"
               style="width: 100%" clearable @change="handleFormAreaChange" />
           </el-form-item>
@@ -43,7 +43,7 @@
           </el-form-item>
         </el-col>
         <el-col :span="24">
-          <el-form-item label="详细住址"><el-input v-model="form.address" placeholder="请输入街道/门牌号" /></el-form-item>
+          <el-form-item label="详细住址" required><el-input v-model="form.address" placeholder="请输入街道/门牌号" /></el-form-item>
         </el-col>
         <el-col :span="12">
           <el-form-item label="房屋类型">
@@ -53,19 +53,19 @@
           </el-form-item>
         </el-col>
         <el-col :span="12">
-          <el-form-item label="入门方式">
+          <el-form-item label="入门方式" required>
             <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="form.entryMethod === 'password'">
-          <el-form-item label="开门密码">
+          <el-form-item label="开门密码" required>
             <el-input v-model="form.entryPassword" placeholder="请输入密码" />
           </el-form-item>
         </el-col>
         <el-col :span="12" v-if="form.entryMethod === 'key'">
-          <el-form-item label="钥匙位置">
+          <el-form-item label="钥匙位置" required>
             <el-input v-model="form.keyLocation" placeholder="如:地毯下" />
           </el-form-item>
         </el-col>
@@ -253,6 +253,11 @@ const handleUserUploadFile = async (file) => {
 const saveUser = () => {
   if (!form.name) return ElMessage.warning('请输入姓名')
   if (!form.phone) return ElMessage.warning('请输入电话')
+  if (!form.stationId) return ElMessage.warning('请选择所属站点')
+  if (!form.address) return ElMessage.warning('请输入详细住址')
+  if (!form.entryMethod) return ElMessage.warning('请选择入门方式')
+  if (form.entryMethod === 'password' && !form.entryPassword) return ElMessage.warning('请输入开门密码')
+  if (form.entryMethod === 'key' && !form.keyLocation) return ElMessage.warning('请输入钥匙存放位置')
 
   submitLoading.value = true
   form.tagIds = selectedTagIds.value

+ 7 - 4
src/views/order/purchase/index.vue

@@ -727,7 +727,7 @@ const handleSubmit = async () => {
       pet: form.petId,
       groupPurchasePackageName: form.groupBuyPackage || '',
       service: form.serviceId,
-      remark: "",
+      remark: form[form.type] && form[form.type].other ? form[form.type].other : "",
       tenantId: storeObj.tenantId || "",
       subOrders: subOrders
     }
@@ -1059,9 +1059,12 @@ onMounted(() => {
 .summary-panel {
   background: white;
   border-radius: 8px;
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
-  position: sticky;
-  top: 20px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  position: fixed;
+  top: 150px;
+  right: 80px;
+  width: 320px;
+  z-index: 2000;
 }
 
 .summary-header {

+ 14 - 2
src/views/service/list/index.vue

@@ -90,10 +90,10 @@
                   <span>步骤 {{ index + 1 }}</span>
                 </div>
               </template>
-              <el-form-item label="步骤标题" label-width="80px" class="inner-form-item">
+              <el-form-item label="步骤标题" label-width="80px" class="inner-form-item" required>
                 <el-input v-model="item.title" placeholder="请输入步骤标题" />
               </el-form-item>
-              <el-form-item label="打卡备注" label-width="80px" class="inner-form-item last">
+              <el-form-item label="打卡备注" label-width="80px" class="inner-form-item last" required>
                 <el-input v-model="item.remark" type="textarea" placeholder="请输入当前步骤打卡备注" />
               </el-form-item>
             </el-card>
@@ -250,6 +250,18 @@ const handleUpdate = async (row: ServiceVO) => {
 const submitForm = () => {
   serviceFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
+      for (let i = 0; i < clockInRemarkList.value.length; i++) {
+        const item = clockInRemarkList.value[i];
+        if (!item.title) {
+          proxy?.$modal.msgError(`请填写步骤 ${i + 1} 的标题`);
+          return;
+        }
+        if (!item.remark) {
+          proxy?.$modal.msgError(`请填写步骤 ${i + 1} 的备注`);
+          return;
+        }
+      }
+
       form.value.clockInRemark = clockInRemarkList.value.length > 0 ? JSON.stringify(clockInRemarkList.value) : undefined;
       buttonLoading.value = true;
       if (form.value.id) {

+ 105 - 94
src/views/system/store/index.vue

@@ -7,25 +7,13 @@
             <span class="title">门店管理</span>
           </div>
           <div class="header-right">
-            <el-input
-              v-model="queryParams.storeOrContact"
-              placeholder="搜索门店名称/联系人"
-              class="search-input"
-              prefix-icon="Search"
-              clearable
-              @keyup.enter="handleQuery"
-            />
-            <el-cascader
-              v-model="searchRegionValue"
-              :options="areaOptions"
-              :props="{ value: 'id', label: 'name' }"
-              placeholder="所属站点"
-              class="station-select"
-              style="width: 240px"
-              clearable
-              @change="handleSearchAreaChange"
-            />
-            <el-select v-model="queryParams.status" placeholder="状态" class="status-select" clearable @change="handleQuery">
+            <el-input v-model="queryParams.storeOrContact" placeholder="搜索门店名称/联系人" class="search-input"
+              prefix-icon="Search" clearable @keyup.enter="handleQuery" />
+            <el-cascader v-model="searchRegionValue" :options="areaOptions" :props="{ value: 'id', label: 'name' }"
+              placeholder="所属站点" class="station-select" style="width: 240px" clearable
+              @change="handleSearchAreaChange" />
+            <el-select v-model="queryParams.status" placeholder="状态" class="status-select" clearable
+              @change="handleQuery">
               <el-option v-for="item in statusList" :key="item.value" :label="item.label" :value="item.value" />
             </el-select>
             <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['system:store:add']">新增门店</el-button>
@@ -33,7 +21,8 @@
         </div>
       </template>
 
-      <el-table v-loading="loading" :data="storeList" style="width: 100%" :header-cell-style="{ background: '#f8f9fb', color: '#606266' }">
+      <el-table v-loading="loading" :data="storeList" style="width: 100%"
+        :header-cell-style="{ background: '#f8f9fb', color: '#606266' }">
         <el-table-column label="门店信息" min-width="240">
           <template #default="scope">
             <div class="store-info-box">
@@ -41,8 +30,10 @@
               <div class="store-desc">
                 <div class="name">{{ scope.row.name }}</div>
                 <div class="tags">
-                  <el-tag size="small" type="warning" effect="plain" v-if="scope.row.tenantName">{{ scope.row.tenantName }}</el-tag>
-                  <el-tag size="small" type="success" effect="plain" v-if="scope.row.tenantCatergoriesName">{{ scope.row.tenantCatergoriesName }}</el-tag>
+                  <el-tag size="small" type="warning" effect="plain" v-if="scope.row.tenantName">{{ scope.row.tenantName
+                    }}</el-tag>
+                  <el-tag size="small" type="success" effect="plain" v-if="scope.row.tenantCatergoriesName">{{
+                    scope.row.tenantCatergoriesName }}</el-tag>
                 </div>
               </div>
             </div>
@@ -52,7 +43,8 @@
         <el-table-column label="服务项目" min-width="180">
           <template #default="scope">
             <div class="service-tags">
-              <el-tag v-for="service in scope.row.services" :key="service" size="small" effect="light" class="service-tag">
+              <el-tag v-for="service in scope.row.services" :key="service" size="small" effect="light"
+                class="service-tag">
                 {{ getServiceName(service) }}
               </el-tag>
             </div>
@@ -61,8 +53,7 @@
 
         <el-table-column label="资质认证" align="center" width="100">
           <template #default="scope">
-            <image-preview v-if="scope.row.businessLicenseUrl" :src="scope.row.businessLicenseUrl" :width="40" :height="40" />
-            <span v-else>-</span>
+            <image-preview :src="scope.row.businessLicenseUrl" :width="40" :height="40" />
           </template>
         </el-table-column>
 
@@ -71,7 +62,9 @@
             <div class="region-info">
               <div class="region-name">{{ getRegionNameBySite(scope.row.site) }}</div>
               <div class="site-name">
-                <el-icon><Location /></el-icon>
+                <el-icon>
+                  <Location />
+                </el-icon>
                 <span>{{ scope.row.siteName }}</span>
               </div>
             </div>
@@ -82,7 +75,8 @@
 
         <el-table-column label="营业时间" align="center" width="140">
           <template #default="scope">
-            <span class="time-text">{{ formatTime(scope.row.startBusinessTime) }},{{ formatTime(scope.row.endBusinessTime) }}</span>
+            <span class="time-text">{{ formatTime(scope.row.startBusinessTime) }},{{
+              formatTime(scope.row.endBusinessTime) }}</span>
           </template>
         </el-table-column>
 
@@ -90,10 +84,14 @@
           <template #default="scope">
             <div class="contact-info">
               <div class="contact-item">
-                <el-icon><User /></el-icon><span>{{ scope.row.contact }}</span>
+                <el-icon>
+                  <User />
+                </el-icon><span>{{ scope.row.contact }}</span>
               </div>
               <div class="contact-item phone">
-                <el-icon><Phone /></el-icon><span>{{ scope.row.contactNumber }}</span>
+                <el-icon>
+                  <Phone />
+                </el-icon><span>{{ scope.row.contactNumber }}</span>
               </div>
             </div>
           </template>
@@ -118,8 +116,10 @@
         <el-table-column label="操作" align="right" width="180" fixed="right">
           <template #default="scope">
             <div class="op-btns">
-              <el-button link type="primary" @click="handleDetail(scope.row)" v-hasPermi="['system:store:query']">详情</el-button>
-              <el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['system:store:edit']">编辑</el-button>
+              <el-button link type="primary" @click="handleDetail(scope.row)"
+                v-hasPermi="['system:store:query']">详情</el-button>
+              <el-button link type="primary" @click="handleUpdate(scope.row)"
+                v-hasPermi="['system:store:edit']">编辑</el-button>
               <el-dropdown trigger="click" @command="(cmd) => handleCommand(cmd, scope.row)">
                 <el-button link type="primary" class="more-btn">
                   更多<el-icon class="el-icon--right"><arrow-down /></el-icon>
@@ -127,8 +127,10 @@
                 <template #dropdown>
                   <el-dropdown-menu>
                     <el-dropdown-item command="handleRenew" v-hasPermi="['system:store:renew']">续期</el-dropdown-item>
-                    <el-dropdown-item v-if="scope.row.status === 1 && checkPermi(['system:store:disable'])" command="handleBan" class="delete-item">禁用</el-dropdown-item>
-                    <el-dropdown-item v-if="scope.row.status === 3 && checkPermi(['system:store:enable'])" command="handleEnable">启用</el-dropdown-item>
+                    <el-dropdown-item v-if="scope.row.status === 1 && checkPermi(['system:store:disable'])"
+                      command="handleBan" class="delete-item">禁用</el-dropdown-item>
+                    <el-dropdown-item v-if="scope.row.status === 3 && checkPermi(['system:store:enable'])"
+                      command="handleEnable">启用</el-dropdown-item>
                   </el-dropdown-menu>
                 </template>
               </el-dropdown>
@@ -138,13 +140,8 @@
       </el-table>
 
       <div class="pagination-container">
-        <pagination
-          v-show="total > 0"
-          v-model:total="total"
-          v-model:page="queryParams.pageNum"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
+        <pagination v-show="total > 0" v-model:total="total" v-model:page="queryParams.pageNum"
+          v-model:limit="queryParams.pageSize" @pagination="getList" />
       </div>
     </el-card>
 
@@ -168,35 +165,27 @@
           </el-checkbox-group>
         </el-form-item>
         <el-form-item label="商户分类" prop="tenantCatergories">
-          <PageSelect
-            v-model="form.tenantCatergories"
+          <PageSelect v-model="form.tenantCatergories"
             :options="tenantCategoriesList.map(item => ({ value: item.id, label: item.name }))"
-            :total="tenantCategoriesTotal"
-            :pageSize="10"
-            placeholder="请选择商户分类"
-            @page-change="handleTenantCategoriesPageChange"
-            @visible-change="handleTenantCategoriesVisibleChange"
-          />
+            :total="tenantCategoriesTotal" :pageSize="10" placeholder="请选择商户分类"
+            @page-change="handleTenantCategoriesPageChange" @visible-change="handleTenantCategoriesVisibleChange" />
         </el-form-item>
         <el-form-item label="所属品牌" prop="tenantId">
-          <PageSelect
-            v-model="form.tenantId"
-            :options="brandList.map(item => ({ value: item.tenantId, label: item.name }))"
-            :total="brandTotal"
-            :pageSize="10"
-            placeholder="请选择所属品牌"
-            @page-change="handleBrandPageChange"
-            @visible-change="handleBrandSelectVisibleChange"
-          />
+          <PageSelect v-model="form.tenantId"
+            :options="brandList.map(item => ({ value: item.tenantId, label: item.name }))" :total="brandTotal"
+            :pageSize="10" placeholder="请选择所属品牌" @page-change="handleBrandPageChange"
+            @visible-change="handleBrandSelectVisibleChange" />
         </el-form-item>
         <el-form-item label="营业时间" prop="startBusinessTime">
           <el-row :gutter="10">
             <el-col :span="10">
-              <el-time-picker clearable v-model="form.startBusinessTime" format="HH:mm" value-format="HH:mm" placeholder="开始时间" style="width: 100%" />
+              <el-time-picker clearable v-model="form.startBusinessTime" format="HH:mm" value-format="HH:mm"
+                placeholder="开始时间" style="width: 100%" />
             </el-col>
             <el-col :span="4" style="text-align: center; line-height: 32px">至</el-col>
             <el-col :span="10">
-              <el-time-picker clearable v-model="form.endBusinessTime" format="HH:mm" value-format="HH:mm" placeholder="结束时间" style="width: 100%" />
+              <el-time-picker clearable v-model="form.endBusinessTime" format="HH:mm" value-format="HH:mm"
+                placeholder="结束时间" style="width: 100%" />
             </el-col>
           </el-row>
         </el-form-item>
@@ -206,16 +195,19 @@
         <el-form-item label="联系电话" prop="contactNumber">
           <el-input v-model="form.contactNumber" placeholder="请输入联系电话" />
         </el-form-item>
-        <el-form-item label="有效期至" prop="validity">
-          <el-date-picker :disabled="!!form.id" clearable v-model="form.validity" type="date" value-format="YYYY-MM-DD" placeholder="请选择有效期至" style="width: 100%" />
+        <el-form-item label="合作有效期" prop="validity">
+          <el-date-picker :disabled="!!form.id" clearable v-model="form.validity" type="date" value-format="YYYY-MM-DD"
+            placeholder="请选择合作有效期" style="width: 100%" />
         </el-form-item>
         <el-form-item label="所属站点" prop="site">
-          <el-cascader v-model="regionValue" :options="areaOptions" :props="{ value: 'id', label: 'name' }" placeholder="选择站点" style="width: 100%" @change="handleAreaChange" />
+          <el-cascader v-model="regionValue" :options="areaOptions" :props="{ value: 'id', label: 'name' }"
+            placeholder="选择站点" style="width: 100%" @change="handleAreaChange" />
         </el-form-item>
-        <el-form-item label="详细地址">
+        <el-form-item label="详细地址" prop="detailAddress">
           <el-row :gutter="10" style="margin-bottom: 10px">
             <el-col :span="24">
-              <el-cascader v-model="addressCascaderValue" :options="regionData" placeholder="选择省市区" style="width: 100%" />
+              <el-cascader v-model="addressCascaderValue" :options="regionData" placeholder="选择省市区"
+                style="width: 100%" />
             </el-col>
           </el-row>
           <el-input v-model="form.detailAddress" type="textarea" placeholder="输入详细地址" rows="3" style="width: 100%" />
@@ -253,14 +245,17 @@
             <el-descriptions-item label="门店名称">{{ detailData.name }}</el-descriptions-item>
             <el-descriptions-item label="商户分类">{{ detailData.tenantCatergoriesName || '-' }}</el-descriptions-item>
             <el-descriptions-item label="所属品牌">{{ detailData.tenantName || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="营业时间">{{ formatTime(detailData.startBusinessTime) }} - {{ formatTime(detailData.endBusinessTime) }}</el-descriptions-item>
-            <el-descriptions-item label="有效期至">{{ parseTime(detailData.validity, '{y}-{m}-{d}') }}</el-descriptions-item>
+            <el-descriptions-item label="营业时间">{{ formatTime(detailData.startBusinessTime) }} - {{
+              formatTime(detailData.endBusinessTime) }}</el-descriptions-item>
+            <el-descriptions-item label="合作有效期">{{ parseTime(detailData.validity, '{y}-{m}-{d}')
+              }}</el-descriptions-item>
             <el-descriptions-item label="联系人">{{ detailData.contact }}</el-descriptions-item>
             <el-descriptions-item label="联系电话">{{ detailData.contactNumber }}</el-descriptions-item>
             <el-descriptions-item label="归属站点">{{ detailData.siteName }}</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" />
+              <image-preview v-if="detailData.businessLicenseUrl" :src="detailData.businessLicenseUrl" :width="80"
+                :height="60" />
               <span v-else>-</span>
             </el-descriptions-item>
           </el-descriptions>
@@ -282,7 +277,8 @@
             <el-table-column label="下单时间" prop="createTime" min-width="160" />
             <el-table-column label="状态" align="center" width="100">
               <template #default="scope">
-                <el-tag :type="getOrderStatusType(scope.row.status)" effect="plain" size="small">{{ getOrderStatusName(scope.row.status) }}</el-tag>
+                <el-tag :type="getOrderStatusType(scope.row.status)" effect="plain" size="small">{{
+                  getOrderStatusName(scope.row.status) }}</el-tag>
               </template>
             </el-table-column>
           </el-table>
@@ -300,14 +296,9 @@
     <!-- 门店续期对话框 -->
     <el-dialog title="门店续期" v-model="renewDialog.visible" width="400px" append-to-body>
       <el-form :model="renewForm" label-width="80px">
-        <el-form-item label="有效期至">
-          <el-date-picker
-            v-model="renewForm.to"
-            type="datetime"
-            value-format="YYYY-MM-DD HH:mm:ss"
-            placeholder="选择日期时间"
-            style="width: 100%"
-          />
+        <el-form-item label="合作有效期">
+          <el-date-picker v-model="renewForm.to" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="选择日期时间"
+            style="width: 100%" />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -533,14 +524,14 @@ const data = reactive<PageData<StoreForm, SysStorePageBo>>({
     params: {}
   },
   rules: {
-    businessLicense: [{ required: true, message: "营业执照不能为空", trigger: "blur" }],
     name: [{ required: true, message: "门店名称不能为空", trigger: "blur" }],
+    detailAddress: [{ required: true, message: "详细地址不能为空", trigger: "blur" }],
     tenantCatergories: [{ required: true, message: "商户分类不能为空", trigger: "change" }],
     startBusinessTime: [{ required: true, message: "开始营业时间不能为空", trigger: "blur" }],
     endBusinessTime: [{ required: true, message: "结束营业时间不能为空", trigger: "blur" }],
     contact: [{ required: true, message: "联系人不能为空", trigger: "blur" }],
     contactNumber: [{ required: true, message: "联系电话不能为空", trigger: "blur" }],
-    validity: [{ required: true, message: "有效期不能为空", trigger: "blur" }],
+    validity: [{ required: true, message: "合作有效期不能为空", trigger: "blur" }],
     tenantId: [{ required: true, message: "所属品牌不能为空", trigger: "change" }],
     regionId: [{ required: true, message: "所在区域不能为空", trigger: "change" }],
     site: [{ required: true, message: "归属站点不能为空", trigger: "change" }],
@@ -664,6 +655,10 @@ const geoErrorMsg = ref('');
 const submitForm = () => {
   storeFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
+      if (!form.value.areaCode) {
+        proxy?.$modal.msgError("请选择省市区");
+        return;
+      }
       buttonLoading.value = true;
       try {
         if (form.value.id) {
@@ -740,7 +735,7 @@ const getGeolocation = async () => {
   try {
     // 确保高德地图脚本已加载
     await loadAMapScript();
-    
+
     const AMap = (window as any).AMap;
     if (!AMap) {
       throw new Error('AMap is not defined');
@@ -967,7 +962,7 @@ onMounted(() => {
 .table-card {
   border: none;
   border-radius: 8px;
-  
+
   :deep(.el-card__header) {
     padding: 20px 24px;
     border-bottom: 1px solid #f0f0f0;
@@ -991,17 +986,27 @@ onMounted(() => {
 .header-right {
   display: flex;
   gap: 12px;
-  
-  .search-input { width: 200px; }
-  .station-select { width: 420px; flex-shrink: 0; }
-  .status-select { width: 140px; }
-  
+
+  .search-input {
+    width: 200px;
+  }
+
+  .station-select {
+    width: 420px;
+    flex-shrink: 0;
+  }
+
+  .status-select {
+    width: 140px;
+  }
+
   :deep(.el-input__wrapper) {
     background-color: #f4f5f7;
     box-shadow: none;
     border: 1px solid transparent;
-    
-    &:hover, &.is-focus {
+
+    &:hover,
+    &.is-focus {
       border-color: #409eff;
       background-color: #fff;
     }
@@ -1012,13 +1017,14 @@ onMounted(() => {
   display: flex;
   align-items: center;
   gap: 12px;
-  
+
   .store-desc {
     .name {
       font-weight: 600;
       color: #333;
       margin-bottom: 4px;
     }
+
     .tags {
       display: flex;
       gap: 4px;
@@ -1037,6 +1043,7 @@ onMounted(() => {
     font-weight: 500;
     color: #333;
   }
+
   .site-name {
     font-size: 12px;
     color: #909399;
@@ -1054,17 +1061,20 @@ onMounted(() => {
     gap: 4px;
     font-size: 13px;
     color: #606266;
-    
+
     &.phone {
       color: #409eff;
       margin-top: 2px;
     }
-    
-    .el-icon { font-size: 14px; }
+
+    .el-icon {
+      font-size: 14px;
+    }
   }
 }
 
-.time-text, .count-text {
+.time-text,
+.count-text {
   font-weight: 500;
   color: #606266;
 }
@@ -1090,6 +1100,7 @@ onMounted(() => {
 
 .delete-item {
   color: #f56c6c !important;
+
   &:hover {
     color: #f56c6c !important;
     background-color: #fef0f0 !important;
@@ -1104,11 +1115,11 @@ onMounted(() => {
 
 :deep(.el-table) {
   --el-table-border-color: #f0f0f0;
-  
+
   th.el-table__cell {
     font-weight: 600;
   }
-  
+
   td.el-table__cell {
     padding: 12px 0;
   }

+ 2 - 2
vite.config.ts

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