Parcourir la source

订单模块基本完成

Huanyi il y a 1 mois
Parent
commit
eacdfc2f88

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

@@ -93,6 +93,7 @@ export interface FlfFulfillerOnOrderVO {
   avatar?: string;
   phone?: string;
   tags?: Array<string | number>;
+  gender?: string;
 }
 
 export interface FlfFulfillerOnOrderQuery {
@@ -188,4 +189,5 @@ export interface FlfFulfillerOnDispatchVO {
   tags: (string | number)[];
   serviceTypes: string;
   nextOrderTime?: string;
+  gender?: string;
 }

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

@@ -30,6 +30,7 @@ export interface SubOrderVO {
     status: number;
     fulfiller: number;
     fulfillerName: string;
+    fulfillerGender?: string;
     fulfillerStatus?: 'resting' | 'busy' | 'disabled';
     price: number;
     // 以下为可能需要用到的扩充字段以兼顾页面展现

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

@@ -689,7 +689,7 @@ const handleSearch = () => {
 }
 
 const loadTags = () => {
-  listAllTag({ category: 'user', status: 0 }).then((res) => {
+  listAllTag({ category: 'customer', status: 0 }).then((res) => {
     allUserTags.value = res.data || []
   }).catch((err) => {
     console.error('加载用户标签失败', err)
@@ -801,11 +801,11 @@ const handleRemark = (row) => {
 
 const saveRemark = () => {
   if (!remarkForm.content) return ElMessage.warning('请输入内容')
-  const data = { 
-    id: currentUser.value.id, 
+  const data = {
+    id: currentUser.value.id,
     name: currentUser.value.name,
     phone: currentUser.value.phone,
-    remark: remarkForm.content 
+    remark: remarkForm.content
   }
   updateCustomer(data).then(() => {
     ElMessage.success('备注添加成功')

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

@@ -46,6 +46,7 @@
             <div class="row-1" style="justify-content: space-between; align-items: flex-start">
               <div style="display: flex; align-items: baseline; gap: 8px">
                 <span class="r-name">{{ currentRider.name }}</span>
+                <dict-tag :options="sys_user_sex" :value="currentRider.gender ?? currentRider.sex" />
                 <span class="r-phone">{{ currentRider.maskPhone }}</span>
               </div>
               <div class="status-right">
@@ -88,6 +89,7 @@
                   <div class="row-1" style="justify-content: space-between; align-items: flex-start">
                     <div style="display: flex; align-items: baseline; gap: 8px">
                       <span class="r-name">{{ rider.name }}</span>
+                      <dict-tag :options="sys_user_sex" :value="rider.gender ?? rider.sex" />
                       <span class="r-phone">{{ rider.maskPhone }}</span>
                     </div>
                     <div class="status-right">
@@ -146,6 +148,10 @@
 import { ref, computed, watch } from 'vue';
 import { ElMessage } from 'element-plus';
 import { Check, Clock, Search } from '@element-plus/icons-vue';
+import { useDict } from '@/utils/dict';
+import DictTag from '@/components/DictTag/index.vue';
+
+const { sys_user_sex } = useDict('sys_user_sex');
 import { pageFulfillerOnOrder } from '@/api/fulfiller/pool';
 import { listAllTag } from '@/api/fulfiller/tag';
 

+ 6 - 1
src/views/order/dispatch/components/RiderListPanel.vue

@@ -36,6 +36,7 @@
             <div class="row-1" style="justify-content: space-between; align-items: flex-start">
               <div style="display: flex; align-items: baseline; gap: 8px">
                 <span class="r-name">{{ rider.name }}</span>
+                <dict-tag :options="sys_user_sex" :value="rider.gender ?? rider.sex" />
                 <span class="r-phone">{{ rider.maskPhone }}</span>
               </div>
               <div class="status-right">
@@ -73,7 +74,11 @@
 </template>
 
 <script setup>
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
+import { useDict } from '@/utils/dict';
+import DictTag from '@/components/DictTag/index.vue';
+
+const { sys_user_sex } = useDict('sys_user_sex');
 
 const props = defineProps({
   modelValue: { type: String, default: 'All' },

+ 26 - 2
src/views/order/orderList/components/DispatchDialog.vue

@@ -54,6 +54,7 @@
                             <div style="display:flex; align-items:baseline; gap:8px;">
                                 <span class="r-name">{{ currentRider.name || '--' }}</span>
                                 <span class="r-phone">{{ currentRider.phone || '--' }}</span>
+                                <dict-tag :options="sys_user_sex" :value="currentRider.gender" />
                                 <el-tag v-if="currentRider.status" size="small" :type="getStatusType(currentRider.status)" effect="plain">
                                     {{ getStatusText(currentRider.status) }}
                                 </el-tag>
@@ -95,6 +96,7 @@
                                         <div style="display:flex; align-items:baseline; gap:8px;">
                                             <span class="r-name">{{ rider.name || '--' }}</span>
                                             <span class="r-phone">{{ rider.phone || '--' }}</span>
+                                            <dict-tag :options="sys_user_sex" :value="rider.gender" />
                                         </div>
                                         <el-tag v-if="rider.status" size="small" :type="getStatusType(rider.status)" effect="plain">
                                             {{ getStatusText(rider.status) }}
@@ -152,7 +154,7 @@
 </template>
 
 <script setup>
-import { ref, computed, watch } from 'vue'
+import { ref, computed, watch, getCurrentInstance, toRefs } from 'vue'
 import { ElMessage } from 'element-plus'
 import { pageFulfillerOnOrder } from '@/api/fulfiller/pool'
 import { listAllTag } from '@/api/fulfiller/tag'
@@ -167,6 +169,9 @@ const props = defineProps({
 })
 const emit = defineEmits(['update:visible', 'submit'])
 
+const { proxy } = getCurrentInstance();
+const { sys_user_sex } = toRefs(proxy.useDict('sys_user_sex'));
+
 const dialogVisible = computed({
     get: () => props.visible,
     set: (val) => emit('update:visible', val)
@@ -232,7 +237,8 @@ const loadRiders = async () => {
         const list = res?.rows || []
         ridersList.value = list.map(r => ({
             ...r,
-            nextOrderTime: r.nextOrderTime || '-'
+            nextOrderTime: r.nextOrderTime || '-',
+            gender: r.gender ?? r.sex
         }))
         total.value = res?.total || 0
 
@@ -258,6 +264,12 @@ watch(() => props.visible, (val) => {
         selectedRiderId.value = null
         // price 单位为分,转成元显示
         dispatchFee.value = props.order?.price ? Number((props.order.price / 100).toFixed(2)) : 0
+        if (props.order?.riderId) {
+            currentRider.value = {
+                id: props.order.riderId,
+                gender: props.order.riderGender ?? props.order.riderSex
+            }
+        }
         pageNum.value = 1
         loadAllTags()
         loadServiceOptions()
@@ -277,6 +289,18 @@ watch(() => props.visible, (val) => {
                 if (res.data.price !== undefined && res.data.price !== null) {
                     dispatchFee.value = Number((res.data.price / 100).toFixed(2))
                 }
+
+                // 如果已经有履约者且不是在列表中找到的,从详情中补全性别
+                if (props.order?.riderId && !currentRider.value) {
+                    currentRider.value = {
+                        id: props.order.riderId,
+                        name: res.data.fulfillerName,
+                        gender: res.data.fulfillerGender ?? res.data.fulfillerSex,
+                        status: res.data.fulfillerStatus
+                    }
+                } else if (currentRider.value && (res.data.fulfillerGender || res.data.fulfillerSex)) {
+                    currentRider.value.gender = res.data.fulfillerGender ?? res.data.fulfillerSex
+                }
             }
         }).catch((e) => {
             console.error('获取订单详细信息失败', e)

+ 80 - 11
src/views/order/orderList/components/OrderDetailDrawer.vue

@@ -31,7 +31,7 @@
                                 @click="emit('command', 'complete', order)">确认完成</el-button>
                         </template>
 
-                        <template v-if="[3, 4].includes(order.status)">
+                        <template v-if="[3, 4].includes(order.status) && order.mode == 0">
                             <el-button icon="Notebook" @click="emit('care-summary', order)">护理小结</el-button>
                         </template>
 
@@ -241,9 +241,18 @@
                                         <p class="p-desc">{{ step.desc }}</p>
                                         <div class="p-media" v-if="step.media && step.media.length">
                                             <div v-for="(item, i) in step.media" :key="i" class="media-item">
+                                                <!-- 图片类型 -->
                                                 <el-image v-if="item.type === 'image'" :src="item.url"
-                                                    :preview-src-list="step.media.map(m => m.url)" fit="cover"
+                                                    :preview-src-list="step.media.filter(m => m.type === 'image').map(m => m.url)" fit="cover"
                                                     class="p-img" :preview-teleported="true" />
+                                                
+                                                <!-- 视频类型:由于后端没给第一帧,这里采用 video 标签 preload 方式尝试展示 -->
+                                                <div v-else-if="item.type === 'video'" class="p-video-box" @click="openVideoPreview(item.url)">
+                                                    <video :src="item.url" preload="metadata" class="p-img p-video"></video>
+                                                    <div class="play-icon-overlay">
+                                                        <el-icon><VideoPlay /></el-icon>
+                                                    </div>
+                                                </div>
                                             </div>
                                         </div>
                                     </div>
@@ -274,15 +283,40 @@
             </div>
         </div>
     </el-drawer>
+
+    <!-- 视频播放弹窗 -->
+    <el-dialog v-model="videoPreview.visible" title="视频播放" width="800px" append-to-body @closed="videoPreview.url = ''">
+        <div style="width: 100%; display: flex; justify-content: center; background: #000; border-radius: 4px; overflow: hidden;">
+            <video v-if="videoPreview.url" :src="videoPreview.url" controls autoplay style="max-width: 100%; max-height: 70vh;"></video>
+        </div>
+    </el-dialog>
 </template>
 
 <script setup>
-import { ref, computed, watch } from 'vue'
+import { ref, reactive, computed, watch } from 'vue'
 import { ElMessage } from 'element-plus'
 import { getPet } from '@/api/archieves/pet'
 import { getCustomer } from '@/api/archieves/customer'
 import { listSubOrderLog } from '@/api/order/subOrderLog'
 
+// 视频判定辅助函数
+const isVideo = (url) => {
+    if (!url) return false;
+    const videoExts = ['.mp4', '.mov', '.avi', '.wmv', '.webm', '.ogg'];
+    return videoExts.some(ext => String(url).toLowerCase().endsWith(ext));
+}
+
+// 视频预览状态
+const videoPreview = reactive({
+    visible: false,
+    url: ''
+})
+
+const openVideoPreview = (url) => {
+    videoPreview.url = url;
+    videoPreview.visible = true;
+}
+
 const props = defineProps({
     visible: Boolean,
     order: Object
@@ -481,18 +515,19 @@ const currentOrderSteps = computed(() => {
 const serviceProgressSteps = computed(() => {
     const list = fulfillerLogs.value || []
     return list.map((i) => {
-        const photos = (i?.photos || '')
-            .split(',')
-            .map(s => s.trim())
-            .filter(Boolean)
-            .map(url => ({ type: 'image', url }))
+        // 使用 photoUrls 展示,而非 photos
+        const media = (i?.photoUrls || []).map(url => {
+            const type = isVideo(url) ? 'video' : 'image';
+            return { type, url }
+        });
+        
         return {
             title: i?.title || '--',
             time: i?.createTime || i?.time || '',
-            icon: undefined,
-            color: '#ff9900',
+            icon: i.step === 4 || i.step === 99 ? 'CircleCheck' : undefined,
+            color: i.step === 4 || i.step === 99 ? '#67c23a' : '#ff9900',
             desc: i?.content || '',
-            media: photos
+            media: media
         }
     })
 })
@@ -797,6 +832,40 @@ const handleExportLogs = () => {
     border-radius: 4px;
     border: 1px solid #e4e7ed;
     cursor: pointer;
+    background: #f5f7fa;
+}
+
+.p-video-box {
+    position: relative;
+    width: 80px;
+    height: 80px;
+    cursor: pointer;
+}
+
+.p-video {
+    object-fit: cover;
+}
+
+.play-icon-overlay {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    background: rgba(0,0,0,0.4);
+    color: #fff;
+    width: 32px;
+    height: 32px;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 18px;
+    transition: all 0.2s;
+}
+
+.p-video-box:hover .play-icon-overlay {
+    background: rgba(0,0,0,0.6);
+    transform: translate(-50%, -50%) scale(1.1);
 }
 
 /* New Transport Split Styles */

+ 10 - 4
src/views/order/orderList/index.vue

@@ -119,7 +119,7 @@
               <el-button link type="primary" size="small" @click="handleDetail(row)">详情</el-button>
               <el-button v-if="row.status === 0" link type="success" size="small"
                 @click="openDispatchDialog(row)">派单</el-button>
-              <el-button v-if="[1, 2, 3].includes(row.status)" link type="warning" size="small"
+              <el-button v-if="![0, 4].includes(row.status)" link type="warning" size="small"
                 @click="openDispatchDialog(row)">重新派单</el-button>
               <el-button v-if="[0, 1].includes(row.status)" link type="danger" size="small"
                 @click="handleCancel(row)">取消</el-button>
@@ -134,7 +134,7 @@
                 <template #dropdown>
                   <el-dropdown-menu>
                     <el-dropdown-item v-if="row.status === 3" command="complete">确认完成</el-dropdown-item>
-                    <el-dropdown-item v-if="row.status === 4" command="care_summary">护理小结</el-dropdown-item>
+                    <el-dropdown-item v-if="row.status === 4 && getServiceMode(row.service) == 0" command="care_summary">护理小结</el-dropdown-item>
                     <el-dropdown-item command="reward">奖惩</el-dropdown-item>
                     <el-dropdown-item command="remark">备注</el-dropdown-item>
                   </el-dropdown-menu>
@@ -316,6 +316,11 @@ const getServiceName = (serviceId) => {
   return item ? item.name : '未知服务';
 };
 
+const getServiceMode = (serviceId) => {
+  const item = serviceOptions.value.find((i) => i.id === serviceId);
+  return item ? item.mode : null;
+};
+
 const getServiceModeTag = (row) => {
   const t = row?.type;
   if (t === 0 || t === '0' || t === 1 || t === '1') return '往返';
@@ -430,7 +435,7 @@ const handleDetail = async (row) => {
         code: info.code,
         subOrderType: info.type,
         status: info.status ?? currentOrder.value?.status,
-        mode: info.mode ?? currentOrder.value?.mode,
+        mode: info.mode ?? getServiceMode(info.service) ?? currentOrder.value?.mode,
         type: info.mode === 1 || info.mode === '1' ? 'transport' : currentOrder.value?.type,
         transportType: info.mode === 1 || info.mode === '1' ? 'round' : currentOrder.value?.transportType,
         serviceTime: info.serviceTime || currentOrder.value?.serviceTime,
@@ -513,7 +518,8 @@ const openDispatchDialog = (row) => {
     pickAddr,
     dropAddr,
     service: row.service,
-    riderId: row.riderId || row.fulfiller || null
+    riderId: row.riderId || row.fulfiller || null,
+    riderGender: row.fulfillerGender
   };
   currentDispatchOrder.value = orderObj;
   dispatchDialogVisible.value = true;

+ 13 - 6
src/views/order/purchase/index.vue

@@ -343,11 +343,11 @@ const handleStoreChange = (val) => {
 }
 
 const getServiceType = (name) => {
-  if (!name) return 'transport'
-  if (name.includes('接送')) return 'transport'
+  if (!name) return 'feeding'
   if (name.includes('喂') || name.includes('遛')) return 'feeding'
   if (name.includes('洗护') || name.includes('美容')) return 'washing'
-  return 'transport'
+  if (name.includes('接送')) return 'transport'
+  return 'feeding'
 }
 
 const getServiceIcon = (name) => {
@@ -360,9 +360,16 @@ const handleServiceChange = (item) => {
   form.serviceId = item.id
   form.mode = item.mode
   const isRouteMode = item.mode === 1 || item.mode === '1'
-  const sysType = isRouteMode ? 'transport' : getServiceType(item.name)
-  form.type = sysType
-  calcPrice(sysType)
+  
+  if (isRouteMode) {
+    form.type = 'transport'
+  } else {
+    const t = getServiceType(item.name)
+    // 如果 mode=0 但识别为 transport,强制改为 feeding 作为兜底服务单
+    form.type = t === 'transport' ? 'feeding' : t
+  }
+  
+  calcPrice(form.type)
 }
 
 const currentPackages = computed(() => {