Selaa lähdekoodia

feat(工单管理): 更新一键完成功能和理赔逻辑

- 在回收和销售工单的完成请求中,新增`type`字段以区分工单类型,提升请求的准确性。
- 在编辑对话框中,优化按钮显示逻辑,确保根据工单类型动态渲染反馈和理赔按钮。
- 增强理赔金额输入的验证逻辑,确保用户输入的金额大于0,提升用户体验。
- 更新用户指派功能,支持搜索和选择指派人,优化用户交互体验。
ylong 1 kuukausi sitten
vanhempi
sitoutus
df72e1d691

+ 3 - 2
src/views/workOrder/recycle/index.vue

@@ -264,7 +264,7 @@
             method: 'post',
             url: '/workorder/workorderinfo/finish',
             title: `确认一键完成这 ${selectedRows.length} 个工单吗?`,
-            data: { ids: selectedRows.map((row) => row.id) }
+            data: { ids: selectedRows.map((row) => row.id), type: 2 }
         });
     };
 
@@ -291,7 +291,8 @@
             message: '确认一键完成该工单吗?',
             fetch: () =>
                 proxy.$http.post('/workorder/workorderinfo/finish', {
-                    ids: [row.id]
+                    ids: [row.id],
+                    type: row.type ?? 2
                 })
         });
     };

+ 147 - 61
src/views/workOrder/sell/components/edit-dialog.vue

@@ -34,13 +34,19 @@
         </el-row>
         <template #footer>
             <div class="flex justify-center space-x-4">
-                <template v-if="form.id && !isReadonly">
+                <template v-if="isEditMode && !isReadonly">
                     <el-button type="warning" plain @click="handleFollowUp">待跟进</el-button>
+                    <el-button v-if="isSell" type="success" plain @click="handleFeedback">反馈</el-button>
                     <el-button type="success" plain @click="handleFinish">完成</el-button>
-                    <el-button type="danger" plain @click="openClaimsDialog">一键理赔</el-button>
+                    <el-button v-if="isRecycle" type="danger" plain @click="openClaimsDialog">一键理赔</el-button>
                 </template>
                 <el-button @click="handleClose">取消</el-button>
-                <el-button v-if="!isReadonly" type="primary" @click="handleSubmit" :loading="loading">保存</el-button>
+                <el-button
+                    v-if="!isReadonly && (!isEditMode || isRecycle)"
+                    type="primary"
+                    :loading="loading"
+                    @click="handleSubmit"
+                >保存</el-button>
             </div>
         </template>
     </ele-modal>
@@ -48,7 +54,7 @@
     <ele-modal title="一键理赔" v-model="claimsVisible" width="480px" :close-on-click-modal="false" destroy-on-close>
         <el-form ref="claimsFormRef" :model="claimsForm" :rules="claimsRules" label-width="80px">
             <el-form-item label="理赔金额" prop="money">
-                <el-input-number v-model="claimsForm.money" :min="0" :precision="2" class="w-full" controls-position="right" />
+                <el-input-number v-model="claimsForm.money" :min="0.01" :precision="2" class="w-full" controls-position="right" />
             </el-form-item>
             <el-form-item label="理赔原因" prop="reason">
                 <el-input v-model="claimsForm.reason" type="textarea" :rows="4" placeholder="请输入理赔原因" />
@@ -67,8 +73,11 @@
     import ImageUpload from '@/components/ImageUpload/index.vue';
     import ProForm from '@/components/ProForm/index.vue';
     import TrackingTimeline from './tracking-timeline.vue';
+    import { listAdmins } from '@/api/activation/repository';
     import { pageUsers } from '@/api/system/user';
 
+    const EXPRESS_CLAIM_TASK_TYPE = 4;
+
     const props = defineProps({
         orderType: {
             type: String,
@@ -91,7 +100,19 @@
     const claimsFormRef = ref(null);
     const claimsForm = reactive({ money: undefined, reason: '' });
     const claimsRules = {
-        money: [{ required: true, message: '请输入理赔金额', trigger: 'blur' }],
+        money: [
+            { required: true, message: '请输入理赔金额', trigger: 'blur' },
+            {
+                validator: (_rule, value, callback) => {
+                    if (value === undefined || value === null || Number(value) <= 0) {
+                        callback(new Error('理赔金额必须大于 0'));
+                    } else {
+                        callback();
+                    }
+                },
+                trigger: 'blur'
+            }
+        ],
         reason: [{ required: true, message: '请输入理赔原因', trigger: 'blur' }]
     };
 
@@ -112,9 +133,12 @@
     });
 
     const logs = ref([]);
+    const allUsers = ref([]);
     const userList = ref([]);
     const userLoading = ref(false);
     const isEditMode = computed(() => !!form.id);
+    const isSell = computed(() => Number(form.type) === 1);
+    const isRecycle = computed(() => Number(form.type) === 2);
     const fieldDisabled = computed(() => isEditMode.value || isReadonly.value);
 
     const rules = computed(() => {
@@ -132,6 +156,12 @@
         return base;
     });
 
+    const toNumber = (value) => {
+        if (value === undefined || value === null || value === '') return undefined;
+        const num = Number(value);
+        return Number.isFinite(num) ? num : undefined;
+    };
+
     const parseImgInfoToFileList = (imgInfo) => {
         if (!imgInfo) return '';
         if (imgInfo.imgUrlList && Array.isArray(imgInfo.imgUrlList)) {
@@ -163,13 +193,11 @@
             label: '任务类型', prop: 'taskType', type: 'dictSelect',
             props: { class: 'w-full', placeholder: '请选择任务类型', code: form.type === 1 ? 'sell_task_type' : 'task_type', disabled: fieldDisabled.value }
         });
-        if (!isEditMode.value) {
-            items.push({
-                label: '指派给', prop: 'handleUserId', type: 'select',
-                options: userList.value.map((user) => ({ label: user.nickName || user.userName, value: user.userId })),
-                props: { class: 'w-full', placeholder: '请选择', filterable: true, remote: true, multiple: true, remoteMethod: searchUsers, loading: userLoading.value, disabled: isReadonly.value }
-            });
-        }
+        items.push({
+            label: '指派给', prop: 'handleUserId', type: 'select',
+            options: userList.value.map((user) => ({ label: user.nickName || user.userName || String(user.userId), value: user.userId })),
+            props: { class: 'w-full', placeholder: '请选择或搜索指派人', filterable: true, remote: true, reserveKeyword: true, multiple: true, remoteMethod: filterUsers, loading: userLoading.value, disabled: isReadonly.value }
+        });
         items.push({ label: '任务详情', prop: 'detail', type: 'textarea', props: { rows: 4, placeholder: '请输入', disabled: fieldDisabled.value } });
         if (isEditMode.value) {
             items.push({ label: '备注', prop: 'remark', type: 'textarea', props: { rows: 3, placeholder: '请输入备注', disabled: isReadonly.value } });
@@ -185,12 +213,37 @@
         return items;
     });
 
-    const searchUsers = async (query = '') => {
+    const mergeUsers = (users = []) => {
+        users.forEach((user) => {
+            if (!user || user.userId == null) return;
+            if (!allUsers.value.find((item) => String(item.userId) === String(user.userId))) {
+                allUsers.value.push({ userId: user.userId, nickName: user.nickName || user.userName, userName: user.userName });
+            }
+        });
+        userList.value = allUsers.value;
+    };
+    const filterUsers = (query = '') => {
+        const keyword = String(query || '').trim().toLowerCase();
+        if (!keyword) { userList.value = allUsers.value; return; }
+        userList.value = allUsers.value.filter((user) => {
+            const nickName = String(user.nickName || '').toLowerCase();
+            const userName = String(user.userName || '').toLowerCase();
+            return nickName.includes(keyword) || userName.includes(keyword);
+        });
+    };
+    const loadUsers = async () => {
         userLoading.value = true;
         try {
-            const res = await pageUsers({ pageNum: 1, pageSize: 50, nickName: query });
-            if (res.code === 200) userList.value = res.rows || res.data;
-        } catch (e) { console.error(e); } finally { userLoading.value = false; }
+            const list = await listAdmins();
+            allUsers.value = Array.isArray(list) ? list : [];
+            userList.value = allUsers.value;
+        } catch (e) {
+            try {
+                const res = await pageUsers({ pageNum: 1, pageSize: 200, status: '0' });
+                allUsers.value = res.rows || [];
+                userList.value = allUsers.value;
+            } catch (err) { console.error(err); }
+        } finally { userLoading.value = false; }
     };
 
     const resetForm = (type) => {
@@ -212,13 +265,8 @@
         form.createTime = detail.createTime || '';
         form.fileList = parseImgInfoToFileList(detail.imgInfo);
         if (detail.handleUsers && detail.handleUsers.length > 0) {
-            form.handleUserId = detail.handleUsers.map((user) => {
-                if (typeof user === 'object' && user.userId) {
-                    if (!userList.value.find((u) => u.userId === user.userId)) userList.value.push(user);
-                    return user.userId;
-                }
-                return user;
-            });
+            mergeUsers(detail.handleUsers);
+            form.handleUserId = detail.handleUsers.map((user) => (typeof user === 'object' && user.userId != null ? user.userId : user));
         } else { form.handleUserId = []; }
         if (detail.changeRecords) {
             logs.value = detail.changeRecords.map((item) => ({ createTime: item.createTime, createName: item.createUserName, oldContent: item.oldContent, newContent: item.newContent, remark: item.remark }));
@@ -230,7 +278,7 @@
         visible.value = true;
         const orderTypeValue = data.type !== undefined && data.type !== null ? Number(data.type) : defaultOrderType.value;
         title.value = isReadonly.value ? '查看工单' : data.id ? '编辑工单' : '新增工单';
-        if (userList.value.length === 0) searchUsers();
+        if (allUsers.value.length === 0) await loadUsers();
         resetForm(orderTypeValue);
         if (!data.id) return;
         loading.value = true;
@@ -243,56 +291,83 @@
     };
 
     const handleClose = () => { visible.value = false; claimsVisible.value = false; proFormRef.value?.resetFields(); };
-    const buildHandleUsers = () => Array.isArray(form.handleUserId) ? form.handleUserId : form.handleUserId ? [form.handleUserId] : [];
-    const parseFileList = () => { if (!form.fileList) return []; try { return JSON.parse(form.fileList); } catch (e) { return []; } };
+    const buildHandleUsers = () => {
+        const ids = Array.isArray(form.handleUserId) ? form.handleUserId : form.handleUserId ? [form.handleUserId] : [];
+        return ids.map((id) => toNumber(id)).filter((id) => id !== undefined);
+    };
+    const parseFileList = () => {
+        if (!form.fileList) return [];
+        if (Array.isArray(form.fileList)) return form.fileList.filter((item) => typeof item === 'string');
+        try {
+            const parsed = JSON.parse(form.fileList);
+            return Array.isArray(parsed) ? parsed.filter((item) => typeof item === 'string') : [];
+        } catch (e) { return []; }
+    };
+    const showRequestError = (res, fallback = '操作失败') => { ElMessage.error(res?.data?.msg || res?.msg || fallback); };
+    const postWorkOrder = (url, payload) => {
+        return proxy.$http.post(url, payload).then((res) => {
+            if (res.data?.code === 200) return res;
+            showRequestError(res);
+            return Promise.reject(new Error(res.data?.msg || '操作失败'));
+        });
+    };
+    const buildSavePayload = () => ({
+        id: form.id,
+        taskType: toNumber(form.taskType),
+        inspectionStatus: toNumber(form.inspectionStatus),
+        waybillCode: form.waybillCode,
+        deatil: form.detail,
+        orderId: form.orderNo,
+        expressType: toNumber(form.carrierId),
+        type: toNumber(form.type),
+        handleUsers: buildHandleUsers(),
+        imgInfo: parseFileList()
+    });
 
     const handleSubmit = () => {
         proFormRef.value?.validate((valid) => {
             if (!valid) return;
             loading.value = true;
-            const inspectionStatus =
-                form.inspectionStatus === undefined || form.inspectionStatus === null || form.inspectionStatus === ''
-                    ? undefined
-                    : Number(form.inspectionStatus);
-            const submitData = {
-                id: form.id,
-                taskType: form.taskType,
-                inspectionStatus,
-                waybillCode: form.waybillCode,
-                deatil: isEditMode.value ? form.remark || form.detail : form.detail,
-                remark: form.remark,
-                orderId: form.orderNo,
-                expressType: form.carrierId,
-                type: form.type,
-                handleUsers: buildHandleUsers(),
-                imgInfo: parseFileList()
-            };
-            // 新增时确保带上验货状态入参
-            if (!form.id && inspectionStatus !== undefined) {
-                submitData.inspectionStatus = inspectionStatus;
-            }
             const url = form.id ? '/workorder/workorderinfo/update' : '/workorder/workorderinfo/add';
-            proxy.$http.post(url, submitData).then((res) => {
-                if (res.data.code === 200) { ElMessage.success(form.id ? '修改成功' : '新增成功'); emit('success'); handleClose(); }
-                else ElMessage.error(res.data.msg || '操作失败');
-            }).catch((e) => console.error(e)).finally(() => { loading.value = false; });
+            postWorkOrder(url, buildSavePayload())
+                .then(() => { ElMessage.success(form.id ? '修改成功' : '新增成功'); emit('success'); handleClose(); })
+                .catch((e) => console.error(e))
+                .finally(() => { loading.value = false; });
         });
     };
 
+    const ensureRemark = () => {
+        if (!String(form.remark || '').trim()) {
+            ElMessage.warning('请先填写备注');
+            return false;
+        }
+        return true;
+    };
     const handleFollowUp = () => {
-        if (!form.id) return;
+        if (!form.id || !ensureRemark()) return;
+        loading.value = true;
+        postWorkOrder('/workorder/workorderinfo/update', { id: toNumber(form.id), updateType: 1, deatil: form.remark, imgInfo: parseFileList() })
+            .then(() => { ElMessage.success('已设置为待跟进'); emit('success'); handleClose(); })
+            .catch((e) => console.error(e))
+            .finally(() => { loading.value = false; });
+    };
+    const handleFeedback = () => {
+        if (!form.id || !ensureRemark()) return;
+        if (!buildHandleUsers().length) { ElMessage.warning('请选择指派人'); return; }
         loading.value = true;
-        proxy.$http.post('/workorder/workorderinfo/update', { id: form.id, updateType: 1, deatil: form.remark || form.detail || '', remark: form.remark, handleUsers: buildHandleUsers(), imgInfo: parseFileList() })
-            .then((res) => { if (res.data.code === 200) { ElMessage.success('已设置为待跟进'); emit('success'); handleClose(); } else ElMessage.error(res.data.msg || '操作失败'); })
-            .catch((e) => { console.error(e); ElMessage.error('操作失败'); }).finally(() => { loading.value = false; });
+        postWorkOrder('/workorder/workorderinfo/update', { id: toNumber(form.id), updateType: 2, deatil: form.remark, handleUsers: buildHandleUsers(), imgInfo: parseFileList() })
+            .then(() => { ElMessage.success('已提交反馈'); emit('success'); handleClose(); })
+            .catch((e) => console.error(e))
+            .finally(() => { loading.value = false; });
     };
 
     const handleFinish = () => {
         if (!form.id) return;
         loading.value = true;
-        proxy.$http.post('/workorder/workorderinfo/finish', { ids: [form.id] })
-            .then((res) => { if (res.data.code === 200) { ElMessage.success('工单已完成'); emit('success'); handleClose(); } else ElMessage.error(res.data.msg || '操作失败'); })
-            .catch((e) => { console.error(e); ElMessage.error('操作失败'); }).finally(() => { loading.value = false; });
+        postWorkOrder('/workorder/workorderinfo/finish', { ids: [toNumber(form.id)], type: toNumber(form.type) })
+            .then(() => { ElMessage.success('工单已完成'); emit('success'); handleClose(); })
+            .catch((e) => console.error(e))
+            .finally(() => { loading.value = false; });
     };
 
     const openClaimsDialog = () => { claimsForm.money = undefined; claimsForm.reason = ''; claimsVisible.value = true; };
@@ -300,9 +375,20 @@
         claimsFormRef.value?.validate((valid) => {
             if (!valid || !form.id) return;
             claimsLoading.value = true;
-            proxy.$http.post('/workorder/workorderinfo/claims', { id: form.id, money: claimsForm.money, reason: claimsForm.reason })
-                .then((res) => { if (res.data.code === 200) { ElMessage.success('理赔提交成功'); claimsVisible.value = false; emit('success'); handleClose(); } else ElMessage.error(res.data.msg || '操作失败'); })
-                .catch((e) => { console.error(e); ElMessage.error('操作失败'); }).finally(() => { claimsLoading.value = false; });
+            const workOrderId = toNumber(form.id);
+            postWorkOrder('/workorder/workorderinfo/claims', { id: workOrderId, money: claimsForm.money, reason: claimsForm.reason })
+                .then(() => postWorkOrder('/workorder/workorderinfo/update', { id: workOrderId, taskType: EXPRESS_CLAIM_TASK_TYPE }).catch((e) => {
+                    console.error(e);
+                    ElMessage.warning('理赔已提交,但任务类型未自动改为快递理赔');
+                }))
+                .then(() => {
+                    ElMessage.success('理赔提交成功,任务类型已改为快递理赔');
+                    claimsVisible.value = false;
+                    emit('success');
+                    handleClose();
+                })
+                .catch((e) => console.error(e))
+                .finally(() => { claimsLoading.value = false; });
         });
     };
 

+ 18 - 5
src/views/workOrder/sell/components/tracking-timeline.vue

@@ -19,16 +19,16 @@
                         </div>
                         <div
                             v-if="log.oldContent || log.newContent"
-                            class="text-gray-600"
+                            class="text-gray-600 break-all whitespace-pre-wrap"
                         >
-                            <span v-if="log.oldContent">{{ log.oldContent }}</span>
+                            <span v-if="log.oldContent">{{ formatLogText(log.oldContent) }}</span>
                             <span v-if="log.oldContent && log.newContent">
                             </span>
-                            <span v-if="log.newContent">{{ log.newContent }}</span>
+                            <span v-if="log.newContent">{{ formatLogText(log.newContent) }}</span>
                         </div>
-                        <div v-if="log.remark" class="text-gray-500 mt-1">
-                            {{ log.remark }}
+                        <div v-if="log.remark" class="text-gray-500 mt-1 break-all whitespace-pre-wrap">
+                            {{ formatLogText(log.remark) }}
                         </div>
                         <div
                             v-if="!log.oldContent && !log.newContent && !log.remark && log.content"
@@ -51,4 +51,17 @@
             default: () => []
         }
     });
+
+    const formatLogText = (text) => {
+        if (text == null || text === '') return '';
+        const raw = String(text);
+        try {
+            const parsed = JSON.parse(raw);
+            if (Array.isArray(parsed) && parsed.every((item) => typeof item === 'string')) {
+                const images = parsed.filter((item) => /^https?:\/\//.test(item));
+                if (images.length) return '上传图片 ' + images.length + ' 张';
+            }
+        } catch (e) {}
+        return raw;
+    };
 </script>

+ 3 - 2
src/views/workOrder/sell/index.vue

@@ -247,7 +247,7 @@
             method: 'post',
             url: '/workorder/workorderinfo/finish',
             title: `确认一键完成这 ${selectedRows.length} 个工单吗?`,
-            data: { ids: selectedRows.map((row) => row.id) }
+            data: { ids: selectedRows.map((row) => row.id), type: 1 }
         });
     };
 
@@ -274,7 +274,8 @@
             message: '确认一键完成该工单吗?',
             fetch: () =>
                 proxy.$http.post('/workorder/workorderinfo/finish', {
-                    ids: [row.id]
+                    ids: [row.id],
+                    type: row.type ?? 1
                 })
         });
     };