diff --git a/api/common.js b/api/common.js index 00ad1a9..a919cc8 100644 --- a/api/common.js +++ b/api/common.js @@ -86,6 +86,15 @@ export const workRecordSubmit = (data) => { return post('/app-api/app/garden/work-record/submit', data); }; +/** + * 按来源业务回显已填报的动态字段(工单详情「填报明细」)。 + * @param {Object} params {bizType, bizId} + * @returns {Promise} List:fieldCode/fieldName/fieldType/unit/valueText/text/materialId/materialName/quantity + */ +export const getWorkRecordValues = (params) => { + return get('/app-api/app/garden/work-record/biz-values', params); +}; + diff --git a/api/work-order-manage/work-order-manage.js b/api/work-order-manage/work-order-manage.js index 1a4df88..ac3f18d 100644 --- a/api/work-order-manage/work-order-manage.js +++ b/api/work-order-manage/work-order-manage.js @@ -127,5 +127,7 @@ export const getTaskActions = (taskId) => { * @returns {Promise} */ export const executeTaskAction = (data) => { - return post('/app-api/workorder/task/action', data); + // 或签节点候选人较多(如养护组长 47 人)时,后端完成任务需逐个取消其余实例,耗时可能超过默认 10 秒 + // 这里单独放宽超时,避免请求已在后端生效却被小程序判定为「网络失败」 + return post('/app-api/workorder/task/action', data, { timeout: 120000 }); }; diff --git a/components/work-field-form/work-field-form.vue b/components/work-field-form/work-field-form.vue new file mode 100644 index 0000000..5bc1326 --- /dev/null +++ b/components/work-field-form/work-field-form.vue @@ -0,0 +1,887 @@ + + + + + diff --git a/pages-sub/problem/work-order-manage/add-maintain-order.vue b/pages-sub/problem/work-order-manage/add-maintain-order.vue index 9e20aa8..dc6919e 100644 --- a/pages-sub/problem/work-order-manage/add-maintain-order.vue +++ b/pages-sub/problem/work-order-manage/add-maintain-order.vue @@ -20,7 +20,27 @@ > - + + + + + + + + + + 作业填报 + + + 该作业类型暂无需填报项 + + + + + + @@ -176,6 +221,7 @@ import { ref, reactive, watch } from 'vue' import { onLoad, onReady, onShow } from '@dcloudio/uni-app'; import { useUploadImgs } from '@/common/utils/useUploadImgs' import { getYlWorkersPage, universalApproval } from '@/api/work-order-manage/work-order-manage' +import { workRecordSubmit } from '@/api/common' import { nextStepMap } from '@/common/utils/common' import { useUserStore } from '@/pinia/user'; const userStore = useUserStore(); @@ -183,6 +229,20 @@ const userStore = useUserStore(); // 表单Ref const workOrderFormRef = ref(null) +// 作业填报动态表单Ref(components/work-field-form) +const workFieldFormRef = ref(null) + +// 作业类型选择弹窗 +const showWorkTypeSheet = ref(false) +// 作业类型选项:字典 maintain_type(打开弹窗时取,避免字典异步加载导致首次为空) +const workTypeOptions = ref([]) + +// 当前作业类型下的动态字段数量(0 时给「无需填报」提示) +const dynamicFieldCount = ref(0) + +// 动态字段提交幂等流水号:同一页面生命周期内保持不变,网络重试只落一条记录 +let workRecordClientSeq = '' + // 页面状态 const showActionSheet = ref(false) const currentActionSheetData = reactive({ @@ -207,6 +267,9 @@ const coProcessorList = ref([]) // 工单表单数据(修改:新增coProcessorName数组,coProcessorNameStr用于页面展示) const workOrderForm = reactive({ busiLine:'', + businessType:'', // 业务类型(由 busiLine 首段得出,用于加载动态字段配置) + workTypeId:'', // 作业类型ID(字典 maintain_type,用于加载动态字段配置) + workTypeName:'', // 作业类型名称(页面展示) workerDataId:'', taskId:'', // 任务id orderNo: '', // 工单编号 @@ -288,6 +351,9 @@ const workOrderFormRules = reactive({ reason: [ { type: 'string', required: true, message: '请输入处理情况描述', trigger: ['change', 'blur'] }, { type: 'string', max: 200, message: '处理情况描述最多200字', trigger: ['change', 'blur'] } + ], + workTypeName: [ + { type: 'string', required: true, message: '请选择作业类型', trigger: ['change'] } ] }) @@ -353,6 +419,13 @@ onLoad((options) => { workOrderForm.orderNo = orderItem.orderNo || ''; workOrderForm.reason = orderItem.handleResult || ''; // 处理情况回显 workOrderForm.busiLine = orderItem.busiLine + // 业务线可能是逗号分隔多值(如 "yl,hj"),字段配置按单值定位,取首段 + workOrderForm.businessType = String(orderItem.busiLine || '').split(',')[0].trim() + // 来源数据若已带作业类型则回显(工单主表当前无该字段,通常为空,由用户手选) + if (orderItem.workTypeId) { + workOrderForm.workTypeId = orderItem.workTypeId + workOrderForm.workTypeName = uni.$dict.getDictLabel('maintain_type', orderItem.workTypeId) || '' + } // 共同处理人(如有数据则回显,转换为数组格式) if (orderItem.coHandlers && orderItem.coHandlersName) { // 处理ID:如果是字符串(逗号分隔),转为数组;本身是数组则直接赋值 @@ -620,6 +693,44 @@ const hideKeyboard = () => { uni.hideKeyboard() } +// 打开作业类型选择弹窗 +const openWorkTypeSheet = () => { + // up-action-sheet 用 name 展示,value 作为回传值 + workTypeOptions.value = uni.$dict.getDictSimpleList('maintain_type') + .map(item => ({ name: item.label, value: item.value })) + if (workTypeOptions.value.length === 0) { + uni.showToast({ + title: '暂无作业类型字典数据', + icon: 'none', + duration: 2000 + }) + return + } + showWorkTypeSheet.value = true +} + +// 选择作业类型:驱动动态字段按「业务类型 + 作业类型」重新加载 +const onWorkTypeSelect = (e) => { + showWorkTypeSheet.value = false + if (!e || e.value === undefined || e.value === null || e.value === '') { + return + } + workOrderForm.workTypeId = e.value + workOrderForm.workTypeName = e.name + dynamicFieldCount.value = 0 + if (workOrderFormRef.value) { + workOrderFormRef.value.validateField('workTypeName') + } +} + +// 动态字段加载完成(count=0 时页面提示「暂无需填报项」) +const onDynamicFieldsLoaded = (count) => { + dynamicFieldCount.value = count || 0 +} + +// 动态字段提交幂等流水号(后端按 userId+clientSeq 去重) +const genClientSeq = () => `${Date.now()}-${Math.random().toString(16).slice(2, 10)}` + // 提交工单(修改:将coProcessorName数组一并传入提交参数) const submitWorkOrder = async () => { try { @@ -687,6 +798,23 @@ const submitWorkOrder = async () => { // ========== 原有表单校验 ========== await workOrderFormRef.value.validate() + // ========== 作业类型 + 动态字段校验 ========== + if (!workOrderForm.workTypeId) { + uni.showToast({ + title: '请选择作业类型', + icon: 'none', + duration: 2000 + }); + console.warn('submitWorkOrder校验失败:作业类型为空'); + return; + } + let dynamicValues = [] + if (workFieldFormRef.value) { + // 校验失败时 reject 字段数组(uview-plus 已提示),交给外层 catch 忽略 + await workFieldFormRef.value.validate() + dynamicValues = workFieldFormRef.value.getValues() + } + // ========== 校验核心提交参数 ========== if (!workOrderForm.taskId) { uni.showToast({ @@ -748,6 +876,24 @@ const submitWorkOrder = async () => { } uni.showLoading({ title: '提交中...' }) + + // ========== 先落作业填报(动态字段),再走审批流转 ========== + // 值按「业务类型 + 作业类型」的字段配置落库;同一工单重复实施时后端按 (bizType,bizId) 覆盖原记录并红冲旧物料 + if (dynamicValues.length > 0) { + if (!workRecordClientSeq) { + workRecordClientSeq = genClientSeq() + } + await workRecordSubmit({ + businessType: workOrderForm.businessType, + bizType: 1, + bizId: Number(workOrderForm.workerDataId), + workTypeId: Number(workOrderForm.workTypeId), + clientSeq: workRecordClientSeq, + remark: workOrderForm.reason.trim(), + fields: dynamicValues + }) + } + const res = await universalApproval(submitData) // 校验接口返回结果 @@ -779,6 +925,9 @@ const submitWorkOrder = async () => { } catch (error) { uni.hideLoading() + // 本次提交失败后重置流水号:下次重试允许按最新填报值覆盖原记录(后端按 bizType+bizId 覆盖,不会重复建单) + workRecordClientSeq = '' + // 区分校验失败和接口失败,仅接口失败时提示(校验失败已有内置提示) if (!Array.isArray(error)) { uni.showToast({ @@ -824,5 +973,25 @@ const submitWorkOrder = async () => { font-weight: bold; } +// 作业填报(动态字段)区块 +.dynamic-field-block { + margin-top: 10rpx; + + .dynamic-field-title { + padding: 20rpx 0 10rpx; + font-size: 30rpx; + font-weight: 600; + color: #303133; + border-bottom: 1rpx solid #f0f0f0; + } + + .dynamic-field-empty { + padding: 30rpx 0; + text-align: center; + font-size: 26rpx; + color: #999; + } +} + \ No newline at end of file diff --git a/pages-sub/problem/work-order-manage/order-detail.vue b/pages-sub/problem/work-order-manage/order-detail.vue index 361e2e7..d115782 100644 --- a/pages-sub/problem/work-order-manage/order-detail.vue +++ b/pages-sub/problem/work-order-manage/order-detail.vue @@ -177,6 +177,42 @@ 养护员未上传图片 + + + + 作业填报 + + + + + + @@ -381,6 +417,7 @@ import { universalApproval, getTaskActions } from '@/api/work-order-manage/work-order-manage'; +import {getWorkRecordValues} from '@/api/common' import {nextStepMap, buzStatusMap, calculateFormatTimeDiff, getActionButtonType} from '@/common/utils/common' // 引入图片上传组合式函数 import {useUploadImgs} from '@/common/utils/useUploadImgs' @@ -469,6 +506,9 @@ const orderDetail = ref({ // ========== 新增:声明eventChannel变量 ========== const eventChannel = ref(null); +// 作业填报明细(工单实施时按「业务类型+作业类型」动态字段配置落库的值) +const workFieldValues = ref([]); + /** * 当前激活的图片列表(无需函数调用,直接访问) */ @@ -538,6 +578,8 @@ const DetailQuery = async (taskIdStr: string) => { configuredActions.value = await actionPromise; orderDetail.value = res; currentImgList.value = orderDetail.value.startImgs + // 并行无依赖:填报明细单独拉,失败不影响详情展示 + loadWorkFieldValues(res) } } catch (error) { console.error('获取工单详情失败:', error); @@ -547,6 +589,46 @@ const DetailQuery = async (taskIdStr: string) => { } }; +/** + * 加载作业填报明细(动态字段) + * bizType=1 工单,bizId=workorder_main_info.id + */ +const loadWorkFieldValues = async (detail: any) => { + workFieldValues.value = [] + if (!detail || !detail.id) return + try { + const res = await getWorkRecordValues({bizType: 1, bizId: detail.id}) + workFieldValues.value = Array.isArray(res) ? res : [] + } catch (error) { + console.error('获取填报明细失败:', error) + } +} + +// 7图片/8附件:value_text 存 URL 的 JSON 数组 +const parseFieldUrls = (item: any) => { + try { + const arr = JSON.parse(item.valueText || '[]') + return Array.isArray(arr) ? arr : [] + } catch (e) { + return [] + } +} + +// 附件展示末段文件名 +const fileNameOf = (url: string) => { + if (!url) return '' + const path = String(url).split('?')[0].split('#')[0] + const name = path.substring(path.lastIndexOf('/') + 1) + try { + return decodeURIComponent(name) || url + } catch (e) { + return name || url + } +} + +// 其余类型直接展示后端生成的可读文案(item.text) +const fieldDisplayText = (item: any) => item.text || item.valueText || '--' + // 页面加载参数 const taskId = ref('') const activeTab = ref('') @@ -1241,4 +1323,39 @@ onShow(() => { .main-content.pb-rows-3 { padding-bottom: 390rpx; } + +// 作业填报明细 +.work-field-block { + margin-top: 20rpx; + + .work-field-title { + padding: 16rpx 30rpx; + font-size: 30rpx; + font-weight: 600; + color: #303133; + background: #fff; + } + + .work-field-value { + max-width: 420rpx; + text-align: right; + word-break: break-all; + } + + .work-field-imgs { + width: 100%; + } + + .work-field-files { + display: flex; + flex-direction: column; + align-items: flex-end; + + .work-field-file { + max-width: 420rpx; + font-size: 26rpx; + color: #3c9cff; + } + } +} \ No newline at end of file