Commit dd60ac144bffb7fd015b2fcf24b385100c2e85b0
1 parent
6853427f
业务线 小程序不用写死,从后端接口获取
Showing
5 changed files
with
1186 additions
and
2 deletions
api/common.js
| @@ -86,6 +86,15 @@ export const workRecordSubmit = (data) => { | @@ -86,6 +86,15 @@ export const workRecordSubmit = (data) => { | ||
| 86 | return post('/app-api/app/garden/work-record/submit', data); | 86 | return post('/app-api/app/garden/work-record/submit', data); |
| 87 | }; | 87 | }; |
| 88 | 88 | ||
| 89 | +/** | ||
| 90 | + * 按来源业务回显已填报的动态字段(工单详情「填报明细」)。 | ||
| 91 | + * @param {Object} params {bizType, bizId} | ||
| 92 | + * @returns {Promise} List<ValueItem>:fieldCode/fieldName/fieldType/unit/valueText/text/materialId/materialName/quantity | ||
| 93 | + */ | ||
| 94 | +export const getWorkRecordValues = (params) => { | ||
| 95 | + return get('/app-api/app/garden/work-record/biz-values', params); | ||
| 96 | +}; | ||
| 97 | + | ||
| 89 | 98 | ||
| 90 | 99 | ||
| 91 | 100 |
api/work-order-manage/work-order-manage.js
| @@ -127,5 +127,7 @@ export const getTaskActions = (taskId) => { | @@ -127,5 +127,7 @@ export const getTaskActions = (taskId) => { | ||
| 127 | * @returns {Promise} | 127 | * @returns {Promise} |
| 128 | */ | 128 | */ |
| 129 | export const executeTaskAction = (data) => { | 129 | export const executeTaskAction = (data) => { |
| 130 | - return post('/app-api/workorder/task/action', data); | 130 | + // 或签节点候选人较多(如养护组长 47 人)时,后端完成任务需逐个取消其余实例,耗时可能超过默认 10 秒 |
| 131 | + // 这里单独放宽超时,避免请求已在后端生效却被小程序判定为「网络失败」 | ||
| 132 | + return post('/app-api/workorder/task/action', data, { timeout: 120000 }); | ||
| 131 | }; | 133 | }; |
components/work-field-form/work-field-form.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="work-field-form"> | ||
| 3 | + <up-form label-position="left" :model="inspectForm" ref="inspectFormRef" :label-width="labelWidth"> | ||
| 4 | + <!-- ========== 动态表单区域 ========== --> | ||
| 5 | + <template v-for="item in dynamicFields" :key="item.fieldCode"> | ||
| 6 | + <up-form-item :label="item.fieldName" :prop="item.fieldCode" class="form-item" | ||
| 7 | + @click="openPickerIfNeeded(item)"> | ||
| 8 | + | ||
| 9 | + <!-- type 1 数值:u-number-box,最多2位小数,min=0,max取item.length --> | ||
| 10 | + <view v-if="item.fieldType === 1" class="number-box-wrap"> | ||
| 11 | + <u-number-box v-model="inspectForm[item.fieldCode]" min="1" :step="1" | ||
| 12 | + :max="item.length ? 10 ** Number(item.length) - 1 : null" | ||
| 13 | + :placeholder="`请输入${item.fieldName}${item.unit} `" @change="valChange(item)" @blur="valChange(item)" | ||
| 14 | + class="large-number-box"> | ||
| 15 | + </u-number-box> | ||
| 16 | + | ||
| 17 | + <text class="unit-text" v-if="item.unit">{{ item.unit }}</text> | ||
| 18 | + </view> | ||
| 19 | + <!-- type 2 文本 --> | ||
| 20 | + <up-input v-else-if="item.fieldType === 2" v-model="inspectForm[item.fieldCode]" | ||
| 21 | + :maxlength="item.length || 50" :placeholder="`请输入${item.fieldName}`"></up-input> | ||
| 22 | + <!-- 分层选项(3枚举/6多选,且选项带 path):统一只读入口,点开分组弹层选择。 | ||
| 23 | + 必须放在下面的 3/6 分支之前——v-else-if 短路,放后面就永远进不来 --> | ||
| 24 | + <up-input v-else-if="isGroupedField(item)" v-model="labelMap[item.fieldCode]" readonly | ||
| 25 | + disabled-color="#ffffff" :placeholder="`请选择${item.fieldName}`" border="none"> | ||
| 26 | + <template #right> | ||
| 27 | + <up-icon name="arrow-right" size="16"></up-icon> | ||
| 28 | + </template> | ||
| 29 | + </up-input> | ||
| 30 | + <!-- type3 枚举:多选 multiple=1 使用复选框组(name 必须是 value,提交按逗号拼接 value,与值格式契约一致) --> | ||
| 31 | + <u-checkbox-group v-else-if="item.fieldType === 3 && item.multiple === 1" | ||
| 32 | + v-model="inspectForm[item.fieldCode]"> | ||
| 33 | + <u-checkbox v-for="opt in item.selectOptions" :key="opt.value" :label="opt.label" :name="opt.value"> | ||
| 34 | + </u-checkbox> | ||
| 35 | + </u-checkbox-group> | ||
| 36 | + <!-- type3 枚举:单选 用up-action-sheet 底部选择 --> | ||
| 37 | + <up-input v-else-if="item.fieldType === 3" v-model="labelMap[item.fieldCode]" readonly | ||
| 38 | + disabled-color="#ffffff" :placeholder="`请选择${item.fieldName}`" border="none"> | ||
| 39 | + <template #right> | ||
| 40 | + <up-icon name="arrow-right" size="16"></up-icon> | ||
| 41 | + </template> | ||
| 42 | + </up-input> | ||
| 43 | + <!-- type4 日期时间 --> | ||
| 44 | + <view v-else-if="item.fieldType === 4" @click.stop="openDatePicker(item)" class="date-input-wrap"> | ||
| 45 | + <up-input v-model="inspectForm[item.fieldCode]" disabled placeholder="点击选择时间"></up-input> | ||
| 46 | + </view> | ||
| 47 | + <!-- type5 布尔单选 是/否 --> | ||
| 48 | + <up-radio-group v-else-if="item.fieldType === 5" v-model="inspectForm[item.fieldCode]"> | ||
| 49 | + <up-radio label="是" name="是"></up-radio> | ||
| 50 | + <up-radio label="否" name="否"></up-radio> | ||
| 51 | + </up-radio-group> | ||
| 52 | + <!-- type6 多选:复选框组;选项来源可为静态/字典/物料/数据表/SQL/接口 --> | ||
| 53 | + <view v-else-if="item.fieldType === 6" class="multi-wrap"> | ||
| 54 | + <u-checkbox-group v-model="inspectForm[item.fieldCode]" @change="onMultiChange(item)"> | ||
| 55 | + <u-checkbox v-for="opt in item.selectOptions" :key="opt.value" :label="opt.label" :name="opt.value" | ||
| 56 | + :disabled="isOptionLocked(item, opt)"> | ||
| 57 | + </u-checkbox> | ||
| 58 | + </u-checkbox-group> | ||
| 59 | + <!-- 有限上限时提示已选数。上限靠「禁用未选中项」实现 --> | ||
| 60 | + <text v-if="multiHint(item)" class="multi-hint">{{ multiHint(item) }}</text> | ||
| 61 | + <!-- needQuantity:已选项各配一个数量输入,值按 {value,label,quantity} 提交。此形态仅记录用量、不扣库存 --> | ||
| 62 | + <view v-if="needQty(item)" class="qty-list"> | ||
| 63 | + <view v-for="opt in checkedOptions(item)" :key="opt.value" class="qty-row"> | ||
| 64 | + <text class="qty-label">{{ opt.label }}</text> | ||
| 65 | + <u-number-box v-model="qtyMap[item.fieldCode][opt.value]" :min="0" :step="1"></u-number-box> | ||
| 66 | + </view> | ||
| 67 | + </view> | ||
| 68 | + </view> | ||
| 69 | + <!-- type7 图片:多张上传,提交 JSON 数组(URL) --> | ||
| 70 | + <up-upload v-else-if="item.fieldType === 7" :file-list="fileListMap[item.fieldCode] || []" | ||
| 71 | + @after-read="(event) => uploadFieldFiles(item, event)" @delete="(event) => deleteFieldFile(item, event)" | ||
| 72 | + multiple width="70" height="70" :sizeType="['compressed']" :max-count="pickCountOf(item, 5)" | ||
| 73 | + upload-text="选择图片" del-color="#ff4d4f" class="upload-wrap"></up-upload> | ||
| 74 | + | ||
| 75 | + <!-- type8 附件:微信只能从聊天记录选文件(无通用文件选择器),提交 JSON 数组(URL) --> | ||
| 76 | + <view v-else-if="item.fieldType === 8" class="file-wrap"> | ||
| 77 | + <view v-for="(file, idx) in fileListMap[item.fieldCode] || []" :key="idx" class="file-row"> | ||
| 78 | + <text class="file-name">{{ file.name || fileNameOf(file.url) }}</text> | ||
| 79 | + <text class="file-del" @click.stop="deleteFieldFile(item, { index: idx })">删除</text> | ||
| 80 | + </view> | ||
| 81 | + <up-button size="small" type="primary" plain :text="fileButtonText(item)" | ||
| 82 | + @click.stop="chooseFieldFiles(item)"></up-button> | ||
| 83 | + </view> | ||
| 84 | + <!-- type9 关联记录:底部选择,提交 JSON 数组 [{id,name}] --> | ||
| 85 | + <up-input v-else-if="item.fieldType === 9" v-model="labelMap[item.fieldCode]" readonly | ||
| 86 | + disabled-color="#ffffff" :placeholder="`请选择${item.fieldName}`" border="none"> | ||
| 87 | + <template #right> | ||
| 88 | + <up-icon name="arrow-right" size="16"></up-icon> | ||
| 89 | + </template> | ||
| 90 | + </up-input> | ||
| 91 | + <!-- type10 链接:URL 输入,提交裸 URL --> | ||
| 92 | + <up-input v-else-if="item.fieldType === 10" v-model.trim="inspectForm[item.fieldCode]" | ||
| 93 | + :placeholder="`请输入${item.fieldName}(http/https 开头)`"></up-input> | ||
| 94 | + <!-- type11 地理位置:uni.chooseLocation 选点,提交 JSON 对象 {lng,lat,address} --> | ||
| 95 | + <up-input v-else-if="item.fieldType === 11" v-model="labelMap[item.fieldCode]" readonly | ||
| 96 | + disabled-color="#ffffff" :placeholder="`点击选择${item.fieldName}`" border="none"> | ||
| 97 | + <template #right> | ||
| 98 | + <up-icon name="arrow-right" size="16"></up-icon> | ||
| 99 | + </template> | ||
| 100 | + </up-input> | ||
| 101 | + <!-- 物料(fieldType=3 + optionSource=3):额外收领用数量;数量>0 才提交 materialId/materialName/quantity 触发扣库存。 | ||
| 102 | + 必须放在 v-if/v-else-if 链末尾——插在中间会打断链导致编译报错 --> | ||
| 103 | + <view v-if="item.fieldType === 3 && item.optionSource === 3" class="qty-row qty-row-material"> | ||
| 104 | + <text class="qty-label">领用数量</text> | ||
| 105 | + <u-number-box v-model="qtyMap[item.fieldCode]" :min="0" :step="1"></u-number-box> | ||
| 106 | + </view> | ||
| 107 | + </up-form-item> | ||
| 108 | + </template> | ||
| 109 | + <!-- ========== 动态表单结束 ========== --> | ||
| 110 | + </up-form> | ||
| 111 | + | ||
| 112 | + <!-- uview-plus 日期时间选择器(全局复用) --> | ||
| 113 | + <up-datetime-picker :show="datePickerShow" v-model="datePickerValue" mode="datetime" close-on-click-overlay | ||
| 114 | + @confirm="onDateConfirm" @cancel="onDateClose" @close="onDateClose"></up-datetime-picker> | ||
| 115 | + <!-- ActionSheet 底部选择弹窗 --> | ||
| 116 | + <up-action-sheet :show="actionSheet.show" :actions="actionSheet.options" @select="onActionSheetSelect" | ||
| 117 | + @close="actionSheet.show = false" cancel-text="取消"></up-action-sheet> | ||
| 118 | + <!-- 分层选项选择器(选项带 path 的动态字段) --> | ||
| 119 | + <grouped-picker :show="groupPicker.show" :title="groupPicker.title" :options="groupPicker.options" | ||
| 120 | + :multiple="groupPicker.multiple" :max-count="groupPicker.maxCount" :need-quantity="groupPicker.needQuantity" | ||
| 121 | + :value="groupPicker.value" :container-width="groupPicker.containerWidth" :quantities="groupPicker.quantities" | ||
| 122 | + @update:show="groupPicker.show = $event" @confirm="onGroupedConfirm"></grouped-picker> | ||
| 123 | + </view> | ||
| 124 | +</template> | ||
| 125 | + | ||
| 126 | +<script> | ||
| 127 | +import { uploadImages } from '@/common/utils/upload' | ||
| 128 | +import { getByType, getOptionsByCode, getMaterialByCode } from '@/api/common' | ||
| 129 | + | ||
| 130 | +/** | ||
| 131 | + * 动态字段表单(业务类型字段配置驱动) | ||
| 132 | + * | ||
| 133 | + * 数据来源:数据中心 / 业务类型字段配置(按 businessType + bizType + workTypeId 定位分类与版本)。 | ||
| 134 | + * 使用方式: | ||
| 135 | + * <work-field-form ref="workFieldFormRef" :business-type="bizLine" :biz-type="1" :work-type-id="workTypeId" | ||
| 136 | + * @loaded="onFieldsLoaded" /> | ||
| 137 | + * 父页面通过 ref 调用 validate() / getValues() 完成校验与提交体组装。 | ||
| 138 | + * | ||
| 139 | + * 说明:businessType / bizType / workTypeId 任一变化都会重新加载;bizType 1=工单 2=道路 3=养护。 | ||
| 140 | + */ | ||
| 141 | +export default { | ||
| 142 | + name: 'WorkFieldForm', | ||
| 143 | + props: { | ||
| 144 | + // 业务类型(园林养护数据口径):yl园林 / hj环卫 / sz市政 / zx / wy | ||
| 145 | + businessType: { type: String, default: '' }, | ||
| 146 | + // 来源:1工单 / 2道路 / 3养护 | ||
| 147 | + bizType: { type: Number, default: 1 }, | ||
| 148 | + // 作业类型(dict: maintain_type) | ||
| 149 | + workTypeId: { type: [String, Number], default: '' }, | ||
| 150 | + // 与父表单对齐的标签宽度 | ||
| 151 | + labelWidth: { type: String, default: '170rpx' } | ||
| 152 | + }, | ||
| 153 | + data() { | ||
| 154 | + return { | ||
| 155 | + inspectForm: {}, | ||
| 156 | + // 用来存放展示文本label的映射对象 | ||
| 157 | + labelMap: {}, | ||
| 158 | + // 类型7图片/8附件的每字段文件列表(fieldCode -> u-upload 兼容数组 {url,status,name}) | ||
| 159 | + fileListMap: {}, | ||
| 160 | + // 数量收集:类型6+needQuantity 为 {选项值: 数量};类型3物料为标量(领用数量) | ||
| 161 | + qtyMap: {}, | ||
| 162 | + // 动态字段列表 | ||
| 163 | + dynamicFields: [], | ||
| 164 | + // 必填规则(随字段配置动态生成) | ||
| 165 | + inspectFormRules: {}, | ||
| 166 | + // 日期时间选择器 | ||
| 167 | + datePickerShow: false, | ||
| 168 | + datePickerValue: Number(new Date()), | ||
| 169 | + currentDateFieldCode: '', | ||
| 170 | + // actionSheet配置 | ||
| 171 | + actionSheet: { | ||
| 172 | + show: false, | ||
| 173 | + fieldCode: '', | ||
| 174 | + options: [], | ||
| 175 | + fieldItem: null | ||
| 176 | + }, | ||
| 177 | + // 分层选项选择器配置(选项带 path 的 3/6 字段用,其余字段仍走上面的 actionSheet / 内联复选框) | ||
| 178 | + groupPicker: { | ||
| 179 | + show: false, | ||
| 180 | + fieldCode: '', | ||
| 181 | + fieldItem: null, | ||
| 182 | + title: '', | ||
| 183 | + options: [], | ||
| 184 | + multiple: false, | ||
| 185 | + maxCount: Infinity, | ||
| 186 | + needQuantity: false, | ||
| 187 | + value: '', | ||
| 188 | + quantities: {}, | ||
| 189 | + containerWidth: 0 | ||
| 190 | + } | ||
| 191 | + } | ||
| 192 | + }, | ||
| 193 | + watch: { | ||
| 194 | + businessType() { | ||
| 195 | + this.reload() | ||
| 196 | + }, | ||
| 197 | + workTypeId() { | ||
| 198 | + this.reload() | ||
| 199 | + } | ||
| 200 | + }, | ||
| 201 | + mounted() { | ||
| 202 | + this.reload() | ||
| 203 | + }, | ||
| 204 | + methods: { | ||
| 205 | + /** | ||
| 206 | + * 重新加载字段配置(作业类型切换时复用同一实例) | ||
| 207 | + */ | ||
| 208 | + reload() { | ||
| 209 | + if (!this.businessType || !this.workTypeId) { | ||
| 210 | + this.dynamicFields = [] | ||
| 211 | + this.$emit('loaded', 0) | ||
| 212 | + return | ||
| 213 | + } | ||
| 214 | + this.resetForm() | ||
| 215 | + this.loadDynamicFields(this.businessType, this.workTypeId) | ||
| 216 | + }, | ||
| 217 | + resetForm() { | ||
| 218 | + this.inspectForm = {} | ||
| 219 | + this.labelMap = {} | ||
| 220 | + this.fileListMap = {} | ||
| 221 | + this.qtyMap = {} | ||
| 222 | + this.dynamicFields = [] | ||
| 223 | + this.inspectFormRules = {} | ||
| 224 | + }, | ||
| 225 | + // ========== 对外方法 ========== | ||
| 226 | + /** 表单校验,失败时 reject 字段数组(与 uview-plus 一致) */ | ||
| 227 | + validate() { | ||
| 228 | + const formRef = this.$refs.inspectFormRef | ||
| 229 | + if (!formRef) { | ||
| 230 | + return Promise.resolve() | ||
| 231 | + } | ||
| 232 | + return formRef.validate() | ||
| 233 | + }, | ||
| 234 | + /** 组装 fields[] 提交体(空值不出现;物料数量>0 才带物料三列) */ | ||
| 235 | + getValues() { | ||
| 236 | + return this.buildDynamicFields() | ||
| 237 | + }, | ||
| 238 | + /** 当前是否有可填字段 */ | ||
| 239 | + hasFields() { | ||
| 240 | + return this.dynamicFields.length > 0 | ||
| 241 | + }, | ||
| 242 | + // number-box数值变更回调 | ||
| 243 | + valChange(item) { | ||
| 244 | + this.$refs.inspectFormRef && this.$refs.inspectFormRef.validateField(item.fieldCode) | ||
| 245 | + }, | ||
| 246 | + // ========== 日期时间相关方法 ========== | ||
| 247 | + openDatePicker(field) { | ||
| 248 | + this.currentDateFieldCode = field.fieldCode | ||
| 249 | + const val = this.inspectForm[field.fieldCode] | ||
| 250 | + this.datePickerValue = val ? new Date(val).getTime() : Number(new Date()) | ||
| 251 | + this.datePickerShow = true | ||
| 252 | + uni.hideKeyboard() | ||
| 253 | + }, | ||
| 254 | + onDateConfirm(e) { | ||
| 255 | + const ts = e.value | ||
| 256 | + const d = new Date(ts) | ||
| 257 | + const y = d.getFullYear() | ||
| 258 | + const m = String(d.getMonth() + 1).padStart(2, '0') | ||
| 259 | + const day = String(d.getDate()).padStart(2, '0') | ||
| 260 | + const h = String(d.getHours()).padStart(2, '0') | ||
| 261 | + const mi = String(d.getMinutes()).padStart(2, '0') | ||
| 262 | + const s = String(d.getSeconds()).padStart(2, '0') | ||
| 263 | + this.inspectForm[this.currentDateFieldCode] = `${y}-${m}-${day} ${h}:${mi}:${s}` | ||
| 264 | + this.datePickerShow = false | ||
| 265 | + this.$refs.inspectFormRef && this.$refs.inspectFormRef.validateField(this.currentDateFieldCode) | ||
| 266 | + }, | ||
| 267 | + onDateClose() { | ||
| 268 | + this.datePickerShow = false | ||
| 269 | + }, | ||
| 270 | + /** | ||
| 271 | + * 点击表单项时按类型决定弹出哪个选择器 | ||
| 272 | + */ | ||
| 273 | + openPickerIfNeeded(item) { | ||
| 274 | + if (this.isGroupedField(item)) { | ||
| 275 | + return this.openGroupedPicker(item) | ||
| 276 | + } | ||
| 277 | + // 模板用 multiple === 1 渲染复选框组,这里就必须用 !== 1 才打开弹窗 | ||
| 278 | + if (item.fieldType === 3 && item.multiple !== 1) { | ||
| 279 | + return this.openActionSheet(item) | ||
| 280 | + } | ||
| 281 | + if (item.fieldType === 9) { | ||
| 282 | + return this.openActionSheet(item) | ||
| 283 | + } | ||
| 284 | + if (item.fieldType === 11) { | ||
| 285 | + return this.openLocation(item) | ||
| 286 | + } | ||
| 287 | + }, | ||
| 288 | + /** 是否为「分层选项」字段:3枚举/6多选,且选项带了后端下发的 path */ | ||
| 289 | + isGroupedField(item) { | ||
| 290 | + if (!item || (item.fieldType !== 3 && item.fieldType !== 6)) { | ||
| 291 | + return false | ||
| 292 | + } | ||
| 293 | + return (item.selectOptions || []).some((opt) => Array.isArray(opt.path) && opt.path.length > 0) | ||
| 294 | + }, | ||
| 295 | + /** 打开分层选项弹层 */ | ||
| 296 | + openGroupedPicker(item) { | ||
| 297 | + const code = item.fieldCode | ||
| 298 | + const multiple = item.fieldType === 6 || item.multiple === 1 | ||
| 299 | + this.groupPicker.fieldCode = code | ||
| 300 | + this.groupPicker.fieldItem = item | ||
| 301 | + this.groupPicker.title = item.fieldName | ||
| 302 | + this.groupPicker.options = item.selectOptions || [] | ||
| 303 | + this.groupPicker.multiple = multiple | ||
| 304 | + this.groupPicker.maxCount = this.maxCountOf(item, Infinity) | ||
| 305 | + // 只有 6 多选需要每项数量;3 枚举没有这层语义 | ||
| 306 | + this.groupPicker.needQuantity = item.fieldType === 6 && this.needQty(item) | ||
| 307 | + this.groupPicker.value = multiple ? (this.inspectForm[code] || []).slice() : (this.inspectForm[code] || '') | ||
| 308 | + this.groupPicker.quantities = Object.assign({}, this.qtyMap[code] || {}) | ||
| 309 | + // 打开弹窗瞬间获取父容器宽度 | ||
| 310 | + const query = uni.createSelectorQuery().in(this) | ||
| 311 | + query.select('.work-field-form').boundingClientRect((rect) => { | ||
| 312 | + if (rect && rect.width) { | ||
| 313 | + this.groupPicker.containerWidth = rect.width | ||
| 314 | + } | ||
| 315 | + this.groupPicker.show = true | ||
| 316 | + }).exec() | ||
| 317 | + }, | ||
| 318 | + /** 分层弹层确定回调 */ | ||
| 319 | + onGroupedConfirm(payload) { | ||
| 320 | + const code = this.groupPicker.fieldCode | ||
| 321 | + this.inspectForm[code] = payload.values | ||
| 322 | + this.labelMap[code] = payload.labelText | ||
| 323 | + if (this.groupPicker.needQuantity) { | ||
| 324 | + // 合并而不是替换:保留 loadDynamicFields 预建的选项键 | ||
| 325 | + this.qtyMap[code] = Object.assign({}, this.qtyMap[code] || {}, payload.quantities || {}) | ||
| 326 | + } | ||
| 327 | + this.groupPicker.show = false | ||
| 328 | + this.$refs.inspectFormRef && this.$refs.inspectFormRef.validateField(code) | ||
| 329 | + }, | ||
| 330 | + /** 打开底部选择弹窗 actionSheet */ | ||
| 331 | + openActionSheet(item) { | ||
| 332 | + this.actionSheet.fieldCode = item.fieldCode | ||
| 333 | + this.actionSheet.fieldItem = item | ||
| 334 | + // 物料带 currentNum 时在展示名后附库存余量(只影响展示名,提交的 materialName 仍取干净的 opt.label) | ||
| 335 | + this.actionSheet.options = (item.selectOptions || []).map((opt) => { | ||
| 336 | + const stock = opt.currentNum === null || opt.currentNum === undefined || opt.currentNum === '' | ||
| 337 | + ? NaN | ||
| 338 | + : Number(opt.currentNum) | ||
| 339 | + return { | ||
| 340 | + name: isNaN(stock) ? opt.label : `${opt.label}(库存${stock}${opt.unit || ''})`, | ||
| 341 | + value: opt.value | ||
| 342 | + } | ||
| 343 | + }) | ||
| 344 | + this.actionSheet.show = true | ||
| 345 | + }, | ||
| 346 | + /** actionSheet选中回调 */ | ||
| 347 | + onActionSheetSelect(selectedItem) { | ||
| 348 | + const fieldItem = this.actionSheet.fieldItem | ||
| 349 | + const fieldCode = this.actionSheet.fieldCode | ||
| 350 | + // 9 关联记录存 {id,name}(提交时转成 [{id,name}]),其余存裸 value | ||
| 351 | + if (fieldItem && fieldItem.fieldType === 9) { | ||
| 352 | + this.inspectForm[fieldCode] = { id: selectedItem.value, name: selectedItem.name } | ||
| 353 | + } else { | ||
| 354 | + this.inspectForm[fieldCode] = selectedItem.value | ||
| 355 | + } | ||
| 356 | + this.labelMap[fieldCode] = selectedItem.name | ||
| 357 | + this.actionSheet.show = false | ||
| 358 | + this.$refs.inspectFormRef && this.$refs.inspectFormRef.validateField(fieldCode) | ||
| 359 | + }, | ||
| 360 | + /** 类型11 地理位置:微信地图选点 */ | ||
| 361 | + openLocation(item) { | ||
| 362 | + uni.chooseLocation({ | ||
| 363 | + success: (res) => { | ||
| 364 | + this.inspectForm[item.fieldCode] = { | ||
| 365 | + lng: res.longitude, | ||
| 366 | + lat: res.latitude, | ||
| 367 | + address: res.address || res.name || '' | ||
| 368 | + } | ||
| 369 | + this.labelMap[item.fieldCode] = res.address || res.name || `${res.latitude}, ${res.longitude}` | ||
| 370 | + this.$refs.inspectFormRef && this.$refs.inspectFormRef.validateField(item.fieldCode) | ||
| 371 | + }, | ||
| 372 | + fail: (err) => { | ||
| 373 | + console.error('地图选择失败', err) | ||
| 374 | + if (err && err.errMsg && err.errMsg.indexOf('auth deny') > -1) { | ||
| 375 | + uni.showToast({ title: '请授权位置权限', icon: 'none' }) | ||
| 376 | + } | ||
| 377 | + } | ||
| 378 | + }) | ||
| 379 | + }, | ||
| 380 | + /** | ||
| 381 | + * 加载动态字段 | ||
| 382 | + */ | ||
| 383 | + async loadDynamicFields(businessType, workTypeId) { | ||
| 384 | + try { | ||
| 385 | + const res = await getByType({ | ||
| 386 | + businessType, | ||
| 387 | + workTypeId, | ||
| 388 | + bizType: this.bizType | ||
| 389 | + }) | ||
| 390 | + let fields = res.fields || [] | ||
| 391 | + fields.sort((a, b) => a.sort - b.sort) | ||
| 392 | + if (fields.length === 0) { | ||
| 393 | + this.dynamicFields = [] | ||
| 394 | + this.$emit('loaded', 0) | ||
| 395 | + return | ||
| 396 | + } | ||
| 397 | + for (const field of fields) { | ||
| 398 | + // 初值:多选为数组;9关联记录/11地理位置 为对象(未选为 null);7图片/8附件为 URL 数组;其余空字符串 | ||
| 399 | + if (field.fieldType === 6 || (field.fieldType === 3 && field.multiple === 1)) { | ||
| 400 | + this.inspectForm[field.fieldCode] = [] | ||
| 401 | + } else if (field.fieldType === 7 || field.fieldType === 8) { | ||
| 402 | + this.inspectForm[field.fieldCode] = [] | ||
| 403 | + } else if (field.fieldType === 9 || field.fieldType === 11) { | ||
| 404 | + this.inspectForm[field.fieldCode] = null | ||
| 405 | + } else { | ||
| 406 | + this.inspectForm[field.fieldCode] = '' | ||
| 407 | + } | ||
| 408 | + this.labelMap[field.fieldCode] = '' | ||
| 409 | + if (field.fieldType === 7 || field.fieldType === 8) { | ||
| 410 | + this.fileListMap[field.fieldCode] = [] | ||
| 411 | + } | ||
| 412 | + // 选项装载:静态来源(1)直接用下发的 field.options,其余来源(2字典/3物料/4数据表/5SQL/6接口)调 options 接口 | ||
| 413 | + if (field.fieldType === 3 || field.fieldType === 6 || field.fieldType === 9) { | ||
| 414 | + if (field.optionSource === 1 && Array.isArray(field.options)) { | ||
| 415 | + field.selectOptions = field.options.map((opt) => ({ label: opt.label, value: String(opt.value), path: opt.path })) | ||
| 416 | + } else if (field.optionSource === 3) { | ||
| 417 | + const matRes = await getMaterialByCode({ | ||
| 418 | + businessType, | ||
| 419 | + bizType: this.bizType, | ||
| 420 | + workTypeId, | ||
| 421 | + fieldCode: field.fieldCode | ||
| 422 | + }) | ||
| 423 | + field.selectOptions = (matRes || []).map((mat) => ({ | ||
| 424 | + label: mat.materialName, | ||
| 425 | + value: String(mat.materialId), | ||
| 426 | + currentNum: mat.currentNum, | ||
| 427 | + unit: mat.unit | ||
| 428 | + })) | ||
| 429 | + } else { | ||
| 430 | + const optRes = await getOptionsByCode({ | ||
| 431 | + businessType, | ||
| 432 | + bizType: this.bizType, | ||
| 433 | + workTypeId, | ||
| 434 | + fieldCode: field.fieldCode | ||
| 435 | + }) | ||
| 436 | + field.selectOptions = (optRes || []).map((opt) => ({ label: opt.label, value: String(opt.value), path: opt.path })) | ||
| 437 | + } | ||
| 438 | + } | ||
| 439 | + // 数量收集初始化(必须在 selectOptions 装载之后) | ||
| 440 | + if (field.fieldType === 6 && this.needQty(field)) { | ||
| 441 | + const qty = {} | ||
| 442 | + ;(field.selectOptions || []).forEach((opt) => { | ||
| 443 | + qty[opt.value] = '' | ||
| 444 | + }) | ||
| 445 | + this.qtyMap[field.fieldCode] = qty | ||
| 446 | + } | ||
| 447 | + if (field.fieldType === 3 && field.optionSource === 3) { | ||
| 448 | + this.qtyMap[field.fieldCode] = 0 | ||
| 449 | + } | ||
| 450 | + // 必填规则 | ||
| 451 | + if (field.required === 1) { | ||
| 452 | + this.inspectFormRules[field.fieldCode] = [this.buildRequiredRule(field)] | ||
| 453 | + } | ||
| 454 | + } | ||
| 455 | + this.dynamicFields = fields | ||
| 456 | + this.$nextTick(() => { | ||
| 457 | + this.$refs.inspectFormRef && this.$refs.inspectFormRef.setRules(this.inspectFormRules) | ||
| 458 | + }) | ||
| 459 | + this.$emit('loaded', fields.length) | ||
| 460 | + } catch (err) { | ||
| 461 | + console.error('获取动态表单字段失败:', err) | ||
| 462 | + uni.showToast({ title: '加载表单配置失败', icon: 'none' }) | ||
| 463 | + this.$emit('loaded', 0) | ||
| 464 | + } | ||
| 465 | + }, | ||
| 466 | + // ========== 动态字段:渲染与取值辅助 ========== | ||
| 467 | + /** 类型6 多选是否要求每项填「使用数量」(ext_config.needQuantity) */ | ||
| 468 | + needQty(item) { | ||
| 469 | + return !!(item.extConfig && item.extConfig.needQuantity === true) | ||
| 470 | + }, | ||
| 471 | + /** 有效上限:ext_config.maxCount(0 表示不限) */ | ||
| 472 | + maxCountOf(item, defaultValue) { | ||
| 473 | + const raw = item.extConfig ? item.extConfig.maxCount : undefined | ||
| 474 | + if (raw === undefined || raw === null || raw === '') { | ||
| 475 | + return defaultValue | ||
| 476 | + } | ||
| 477 | + const num = Number(raw) | ||
| 478 | + if (isNaN(num)) { | ||
| 479 | + return defaultValue | ||
| 480 | + } | ||
| 481 | + return num > 0 ? num : Infinity | ||
| 482 | + }, | ||
| 483 | + /** 传给选择器的单次可选数量:上限为「不限」时用组件默认值 52 */ | ||
| 484 | + pickCountOf(item, defaultValue) { | ||
| 485 | + const max = this.maxCountOf(item, defaultValue) | ||
| 486 | + return max === Infinity ? 52 : max | ||
| 487 | + }, | ||
| 488 | + /** 单文件大小上限(MB):ext_config.maxSizeMb */ | ||
| 489 | + maxSizeMbOf(item, defaultValue) { | ||
| 490 | + const num = item.extConfig ? Number(item.extConfig.maxSizeMb) : NaN | ||
| 491 | + return num > 0 ? num : defaultValue | ||
| 492 | + }, | ||
| 493 | + /** 类型8 允许的扩展名数组(ext_config.fileType,不含点) */ | ||
| 494 | + extensionsOf(item) { | ||
| 495 | + const list = item.extConfig && item.extConfig.fileType | ||
| 496 | + return Array.isArray(list) ? list : [] | ||
| 497 | + }, | ||
| 498 | + fileButtonText(item) { | ||
| 499 | + const exts = this.extensionsOf(item) | ||
| 500 | + return exts.length ? `选择文件(${exts.join('/')})` : '选择文件' | ||
| 501 | + }, | ||
| 502 | + /** 从 URL 取末段当文件名 */ | ||
| 503 | + fileNameOf(url) { | ||
| 504 | + if (!url) return '' | ||
| 505 | + const path = String(url).split('?')[0].split('#')[0] | ||
| 506 | + const name = path.substring(path.lastIndexOf('/') + 1) | ||
| 507 | + try { | ||
| 508 | + return decodeURIComponent(name) || url | ||
| 509 | + } catch (err) { | ||
| 510 | + return name || url | ||
| 511 | + } | ||
| 512 | + }, | ||
| 513 | + /** 类型6 已勾选的选项 */ | ||
| 514 | + checkedOptions(item) { | ||
| 515 | + const values = this.inspectForm[item.fieldCode] || [] | ||
| 516 | + return (item.selectOptions || []).filter((opt) => values.indexOf(opt.value) > -1) | ||
| 517 | + }, | ||
| 518 | + /** 多选是否达到上限(用于禁用未选中项) */ | ||
| 519 | + isOptionLocked(item, opt) { | ||
| 520 | + const max = this.maxCountOf(item, Infinity) | ||
| 521 | + if (max === Infinity) { | ||
| 522 | + return false | ||
| 523 | + } | ||
| 524 | + const values = this.inspectForm[item.fieldCode] || [] | ||
| 525 | + return values.length >= max && values.indexOf(opt.value) === -1 | ||
| 526 | + }, | ||
| 527 | + /** 多选的「已选 x/上限」提示,不限时返回空串 */ | ||
| 528 | + multiHint(item) { | ||
| 529 | + const max = this.maxCountOf(item, Infinity) | ||
| 530 | + if (max === Infinity) { | ||
| 531 | + return '' | ||
| 532 | + } | ||
| 533 | + return `已选 ${(this.inspectForm[item.fieldCode] || []).length}/${max}` | ||
| 534 | + }, | ||
| 535 | + onMultiChange(item) { | ||
| 536 | + this.$refs.inspectFormRef && this.$refs.inspectFormRef.validateField(item.fieldCode) | ||
| 537 | + }, | ||
| 538 | + /** 必填规则:7/8/9/11 的值是数组或对象,必须用 validator 显式判空 */ | ||
| 539 | + buildRequiredRule(field) { | ||
| 540 | + const type = field.fieldType | ||
| 541 | + if (type === 6 || type === 7 || type === 8) { | ||
| 542 | + const action = type === 6 ? '请选择' : '请上传' | ||
| 543 | + return { | ||
| 544 | + required: true, | ||
| 545 | + trigger: ['change', 'blur'], | ||
| 546 | + validator: (rule, value, callback) => { | ||
| 547 | + Array.isArray(value) && value.length > 0 ? callback() : callback(new Error(`${action}${field.fieldName}`)) | ||
| 548 | + } | ||
| 549 | + } | ||
| 550 | + } | ||
| 551 | + if (type === 9 || type === 11) { | ||
| 552 | + return { | ||
| 553 | + required: true, | ||
| 554 | + trigger: ['change', 'blur'], | ||
| 555 | + validator: (rule, value, callback) => { | ||
| 556 | + value ? callback() : callback(new Error(type === 9 ? `请选择${field.fieldName}` : `请选择${field.fieldName}的位置`)) | ||
| 557 | + } | ||
| 558 | + } | ||
| 559 | + } | ||
| 560 | + if (type === 1) { | ||
| 561 | + return { | ||
| 562 | + required: true, | ||
| 563 | + trigger: ['change', 'blur'], | ||
| 564 | + validator: (rule, value, callback) => { | ||
| 565 | + // 空字符串、null、undefined 才是空;数字 0 合法 | ||
| 566 | + if (value === '' || value === null || value === undefined) { | ||
| 567 | + callback(new Error(`请填写${field.fieldName}`)) | ||
| 568 | + } else { | ||
| 569 | + callback() | ||
| 570 | + } | ||
| 571 | + } | ||
| 572 | + } | ||
| 573 | + } | ||
| 574 | + // 2文本 /4日期 /5布尔 /10链接 | ||
| 575 | + const action = type === 3 || type === 5 ? '请选择' : '请填写' | ||
| 576 | + return { | ||
| 577 | + required: true, | ||
| 578 | + message: `${action}${field.fieldName}`, | ||
| 579 | + trigger: ['change', 'blur'] | ||
| 580 | + } | ||
| 581 | + }, | ||
| 582 | + // ========== 动态字段:7图片/8附件 ========== | ||
| 583 | + /** 单文件是否超过 ext_config.maxSizeMb */ | ||
| 584 | + isOversize(file, maxSizeMb) { | ||
| 585 | + if (!maxSizeMb || !file || !file.size) return false | ||
| 586 | + const over = file.size / 1024 / 1024 > maxSizeMb | ||
| 587 | + if (over) { | ||
| 588 | + uni.showToast({ title: `单个文件不能超过${maxSizeMb}MB`, icon: 'none' }) | ||
| 589 | + } | ||
| 590 | + return over | ||
| 591 | + }, | ||
| 592 | + /** 把已上传成功的 URL 同步进表单值(提交与校验都读 inspectForm) */ | ||
| 593 | + syncFileValue(item) { | ||
| 594 | + const list = this.fileListMap[item.fieldCode] || [] | ||
| 595 | + this.inspectForm[item.fieldCode] = list.filter((file) => file.status === 'success').map((file) => file.url) | ||
| 596 | + }, | ||
| 597 | + /** 7图片 上传(u-upload 的 after-read 回调) */ | ||
| 598 | + uploadFieldFiles(item, event) { | ||
| 599 | + const rawFile = event && event.file | ||
| 600 | + const fileList = Array.isArray(rawFile) ? rawFile : [rawFile] | ||
| 601 | + this.uploadToField(item, fileList.filter((file) => file && file.url).map((file) => ({ | ||
| 602 | + url: file.url, | ||
| 603 | + name: file.name || '', | ||
| 604 | + size: file.size | ||
| 605 | + }))) | ||
| 606 | + }, | ||
| 607 | + /** | ||
| 608 | + * 8附件 选择并上传。 | ||
| 609 | + * 微信小程序没有通用文件选择器,只能从聊天记录里选(uni.chooseMessageFile) | ||
| 610 | + */ | ||
| 611 | + chooseFieldFiles(item) { | ||
| 612 | + const maxCount = this.maxCountOf(item, 5) | ||
| 613 | + const remain = maxCount - (this.fileListMap[item.fieldCode] || []).length | ||
| 614 | + if (remain <= 0) { | ||
| 615 | + uni.showToast({ title: `最多只能上传${maxCount}个`, icon: 'none' }) | ||
| 616 | + return | ||
| 617 | + } | ||
| 618 | + const exts = this.extensionsOf(item) | ||
| 619 | + const options = { count: Math.min(remain, this.pickCountOf(item, 5)), type: 'file' } | ||
| 620 | + if (exts.length) { | ||
| 621 | + options.extension = exts | ||
| 622 | + } | ||
| 623 | + uni.chooseMessageFile({ | ||
| 624 | + ...options, | ||
| 625 | + success: (res) => { | ||
| 626 | + const tempFiles = res.tempFiles || [] | ||
| 627 | + this.uploadToField(item, tempFiles.map((file) => ({ | ||
| 628 | + url: file.path, | ||
| 629 | + name: file.name || '', | ||
| 630 | + size: file.size | ||
| 631 | + }))) | ||
| 632 | + }, | ||
| 633 | + fail: (err) => { | ||
| 634 | + console.error('选择附件失败:', err) | ||
| 635 | + } | ||
| 636 | + }) | ||
| 637 | + }, | ||
| 638 | + /** 把选中的本地文件逐个上传并把 http 地址回填到 fileListMap / inspectForm */ | ||
| 639 | + async uploadToField(item, files) { | ||
| 640 | + const code = item.fieldCode | ||
| 641 | + const maxCount = this.maxCountOf(item, 5) | ||
| 642 | + const maxSizeMb = this.maxSizeMbOf(item, 5) | ||
| 643 | + const validFiles = (files || []).filter((file) => file && file.url && !this.isOversize(file, maxSizeMb)) | ||
| 644 | + if (!validFiles.length) { | ||
| 645 | + return | ||
| 646 | + } | ||
| 647 | + const targetList = this.fileListMap[code] || (this.fileListMap[code] = []) | ||
| 648 | + if (targetList.length + validFiles.length > maxCount) { | ||
| 649 | + uni.showToast({ title: `最多只能上传${maxCount}个`, icon: 'none' }) | ||
| 650 | + return | ||
| 651 | + } | ||
| 652 | + const startIndex = targetList.length | ||
| 653 | + targetList.push(...validFiles.map((file) => ({ | ||
| 654 | + url: file.url, status: 'uploading', message: '上传中', name: file.name || '' | ||
| 655 | + }))) | ||
| 656 | + try { | ||
| 657 | + const uploadResultUrls = await uploadImages({ | ||
| 658 | + filePaths: validFiles.map((file) => file.url), | ||
| 659 | + ignoreError: true | ||
| 660 | + }) | ||
| 661 | + uploadResultUrls.forEach((url, index) => { | ||
| 662 | + if (targetList[startIndex + index]) { | ||
| 663 | + targetList.splice(startIndex + index, 1, { | ||
| 664 | + url, status: 'success', message: '', name: validFiles[index].name || '' | ||
| 665 | + }) | ||
| 666 | + } | ||
| 667 | + }) | ||
| 668 | + for (let i = uploadResultUrls.length; i < validFiles.length; i++) { | ||
| 669 | + if (targetList[startIndex + i]) { | ||
| 670 | + targetList.splice(startIndex + i, 1, { | ||
| 671 | + url: validFiles[i].url, status: 'failed', message: '上传失败', name: validFiles[i].name || '' | ||
| 672 | + }) | ||
| 673 | + } | ||
| 674 | + } | ||
| 675 | + if (uploadResultUrls.length < validFiles.length) { | ||
| 676 | + uni.showToast({ | ||
| 677 | + title: `成功${uploadResultUrls.length}个,失败${validFiles.length - uploadResultUrls.length}个`, | ||
| 678 | + icon: 'none' | ||
| 679 | + }) | ||
| 680 | + } | ||
| 681 | + } catch (err) { | ||
| 682 | + console.error(`【${code}】文件上传失败:`, err) | ||
| 683 | + for (let i = 0; i < validFiles.length; i++) { | ||
| 684 | + if (targetList[startIndex + i]) { | ||
| 685 | + targetList.splice(startIndex + i, 1, { | ||
| 686 | + url: validFiles[i].url, status: 'failed', message: '上传失败', name: validFiles[i].name || '' | ||
| 687 | + }) | ||
| 688 | + } | ||
| 689 | + } | ||
| 690 | + uni.showToast({ title: '上传失败,请重试', icon: 'none' }) | ||
| 691 | + } | ||
| 692 | + this.syncFileValue(item) | ||
| 693 | + this.$refs.inspectFormRef && this.$refs.inspectFormRef.validateField(code) | ||
| 694 | + }, | ||
| 695 | + /** 删除 7图片/8附件 的某一个已选文件 */ | ||
| 696 | + deleteFieldFile(item, event) { | ||
| 697 | + const list = this.fileListMap[item.fieldCode] || [] | ||
| 698 | + const index = Number(event && event.index) | ||
| 699 | + if (isNaN(index) || index < 0 || index >= list.length) { | ||
| 700 | + return | ||
| 701 | + } | ||
| 702 | + list.splice(index, 1) | ||
| 703 | + this.syncFileValue(item) | ||
| 704 | + this.$refs.inspectFormRef && this.$refs.inspectFormRef.validateField(item.fieldCode) | ||
| 705 | + }, | ||
| 706 | + // ========== 动态字段:组装提交体 ========== | ||
| 707 | + /** | ||
| 708 | + * 按值格式契约组装 fields[]。 | ||
| 709 | + * ① 空值一律不出现在 fields[] 里(不提交 ''/'[]'/'null') | ||
| 710 | + * ② 物料的数量>0 才写 materialId/materialName/quantity —— 非空 materialId 即触发扣库存 | ||
| 711 | + */ | ||
| 712 | + buildDynamicFields() { | ||
| 713 | + const fields = [] | ||
| 714 | + for (const item of this.dynamicFields) { | ||
| 715 | + const code = item.fieldCode | ||
| 716 | + const value = this.inspectForm[code] | ||
| 717 | + const type = item.fieldType | ||
| 718 | + if (type === 9 || type === 11) { | ||
| 719 | + if (!value) continue | ||
| 720 | + } else if (type === 6 || type === 7 || type === 8) { | ||
| 721 | + if (!Array.isArray(value) || value.length === 0) continue | ||
| 722 | + } else if (value === '' || value === null || value === undefined) { | ||
| 723 | + continue | ||
| 724 | + } | ||
| 725 | + | ||
| 726 | + if (type === 1) { | ||
| 727 | + fields.push({ fieldCode: code, valueNumber: Number(value) }) | ||
| 728 | + } else if (type === 3 && item.optionSource === 3) { | ||
| 729 | + // 物料:value_text 存物料ID;数量>0 才落物料三列(0 表示本次不领用) | ||
| 730 | + const qty = Number(this.qtyMap[code] || 0) | ||
| 731 | + const matched = (item.selectOptions || []).filter((opt) => opt.value === String(value))[0] | ||
| 732 | + const row = { fieldCode: code, valueText: String(value) } | ||
| 733 | + if (qty > 0) { | ||
| 734 | + row.materialId = Number(value) | ||
| 735 | + row.materialName = matched ? matched.label : '' | ||
| 736 | + row.quantity = qty | ||
| 737 | + } | ||
| 738 | + fields.push(row) | ||
| 739 | + } else if (type === 3) { | ||
| 740 | + // 3枚举:单选取裸 value;历史「枚举+是否多选」存逗号分隔 | ||
| 741 | + fields.push({ | ||
| 742 | + fieldCode: code, | ||
| 743 | + valueText: item.multiple === 1 | ||
| 744 | + ? (Array.isArray(value) ? value.join(',') : String(value || '')) | ||
| 745 | + : String(value) | ||
| 746 | + }) | ||
| 747 | + } else if (type === 6) { | ||
| 748 | + const selected = (item.selectOptions || []).filter((opt) => (value || []).indexOf(opt.value) > -1) | ||
| 749 | + if (this.needQty(item)) { | ||
| 750 | + // 元素 {value,label,quantity};数量留空的不带 quantity 键,label 作快照落库 | ||
| 751 | + const qtyMap = this.qtyMap[code] || {} | ||
| 752 | + fields.push({ | ||
| 753 | + fieldCode: code, | ||
| 754 | + valueText: JSON.stringify(selected.map((opt) => { | ||
| 755 | + const raw = qtyMap[opt.value] | ||
| 756 | + const row = { value: opt.value, label: opt.label } | ||
| 757 | + if (raw !== '' && raw !== null && raw !== undefined) { | ||
| 758 | + row.quantity = Number(raw) | ||
| 759 | + } | ||
| 760 | + return row | ||
| 761 | + })) | ||
| 762 | + }) | ||
| 763 | + } else { | ||
| 764 | + fields.push({ fieldCode: code, valueText: JSON.stringify(value.map((v) => String(v))) }) | ||
| 765 | + } | ||
| 766 | + } else if (type === 7 || type === 8) { | ||
| 767 | + fields.push({ fieldCode: code, valueText: JSON.stringify(value) }) | ||
| 768 | + } else if (type === 9) { | ||
| 769 | + fields.push({ fieldCode: code, valueText: JSON.stringify([{ id: value.id, name: value.name }]) }) | ||
| 770 | + } else if (type === 11) { | ||
| 771 | + fields.push({ | ||
| 772 | + fieldCode: code, | ||
| 773 | + valueText: JSON.stringify({ lng: value.lng, lat: value.lat, address: value.address || '' }) | ||
| 774 | + }) | ||
| 775 | + } else { | ||
| 776 | + // 2文本 / 4日期 / 5布尔 / 10链接 均为裸文本 | ||
| 777 | + fields.push({ fieldCode: code, valueText: String(value) }) | ||
| 778 | + } | ||
| 779 | + } | ||
| 780 | + return fields | ||
| 781 | + } | ||
| 782 | + } | ||
| 783 | +} | ||
| 784 | +</script> | ||
| 785 | + | ||
| 786 | +<style lang="scss" scoped> | ||
| 787 | +.work-field-form { | ||
| 788 | + width: 100%; | ||
| 789 | +} | ||
| 790 | + | ||
| 791 | +.date-input-wrap { | ||
| 792 | + width: 100%; | ||
| 793 | +} | ||
| 794 | + | ||
| 795 | +// 放大u-number-box输入框 | ||
| 796 | +.large-number-box { | ||
| 797 | + :deep(.u-number-box__input) { | ||
| 798 | + width: 264rpx !important; | ||
| 799 | + height: 72rpx; | ||
| 800 | + font-size: 32rpx; | ||
| 801 | + } | ||
| 802 | + | ||
| 803 | + :deep(.u-number-box__btn) { | ||
| 804 | + width: 64rpx; | ||
| 805 | + height: 72rpx; | ||
| 806 | + } | ||
| 807 | +} | ||
| 808 | + | ||
| 809 | +.number-box-wrap { | ||
| 810 | + display: flex; | ||
| 811 | + align-items: center; | ||
| 812 | + gap: 16rpx; | ||
| 813 | + | ||
| 814 | + .unit-text { | ||
| 815 | + font-size: 28rpx; | ||
| 816 | + color: #333; | ||
| 817 | + } | ||
| 818 | +} | ||
| 819 | + | ||
| 820 | +// 6多选:复选框组 + needQuantity 时每个已选项一行数量输入 | ||
| 821 | +.multi-wrap { | ||
| 822 | + width: 100%; | ||
| 823 | + | ||
| 824 | + .multi-hint { | ||
| 825 | + display: block; | ||
| 826 | + margin-top: 8rpx; | ||
| 827 | + font-size: 24rpx; | ||
| 828 | + color: #909399; | ||
| 829 | + } | ||
| 830 | + | ||
| 831 | + .qty-list { | ||
| 832 | + margin-top: 16rpx; | ||
| 833 | + padding-left: 20rpx; | ||
| 834 | + border-left: 4rpx solid #f0f0f0; | ||
| 835 | + } | ||
| 836 | +} | ||
| 837 | + | ||
| 838 | +// 数量行(6多选的每个已选项 / 3物料的领用数量共用) | ||
| 839 | +.qty-row { | ||
| 840 | + display: flex; | ||
| 841 | + align-items: center; | ||
| 842 | + justify-content: space-between; | ||
| 843 | + gap: 16rpx; | ||
| 844 | + padding: 8rpx 0; | ||
| 845 | + | ||
| 846 | + .qty-label { | ||
| 847 | + flex: 1; | ||
| 848 | + min-width: 0; | ||
| 849 | + font-size: 28rpx; | ||
| 850 | + color: #333; | ||
| 851 | + } | ||
| 852 | +} | ||
| 853 | + | ||
| 854 | +.qty-row-material { | ||
| 855 | + margin-top: 12rpx; | ||
| 856 | + padding-top: 12rpx; | ||
| 857 | + border-top: 1rpx solid #f0f0f0; | ||
| 858 | +} | ||
| 859 | + | ||
| 860 | +// 8附件:自绘文件列表 + 选择按钮 | ||
| 861 | +.file-wrap { | ||
| 862 | + width: 100%; | ||
| 863 | + | ||
| 864 | + .file-row { | ||
| 865 | + display: flex; | ||
| 866 | + align-items: center; | ||
| 867 | + justify-content: space-between; | ||
| 868 | + gap: 16rpx; | ||
| 869 | + padding: 8rpx 0; | ||
| 870 | + | ||
| 871 | + .file-name { | ||
| 872 | + flex: 1; | ||
| 873 | + min-width: 0; | ||
| 874 | + font-size: 28rpx; | ||
| 875 | + color: #333; | ||
| 876 | + overflow: hidden; | ||
| 877 | + white-space: nowrap; | ||
| 878 | + text-overflow: ellipsis; | ||
| 879 | + } | ||
| 880 | + | ||
| 881 | + .file-del { | ||
| 882 | + font-size: 26rpx; | ||
| 883 | + color: #ff4d4f; | ||
| 884 | + } | ||
| 885 | + } | ||
| 886 | +} | ||
| 887 | +</style> |
pages-sub/problem/work-order-manage/add-maintain-order.vue
| @@ -20,7 +20,27 @@ | @@ -20,7 +20,27 @@ | ||
| 20 | ></up-input> | 20 | ></up-input> |
| 21 | </up-form-item> | 21 | </up-form-item> |
| 22 | 22 | ||
| 23 | - <!-- 2. 共同处理人(支持多选) --> | 23 | + <!-- 2. 作业类型(决定加载哪套动态字段配置:数据中心/业务类型字段配置 配置归属=工单) --> |
| 24 | + <up-form-item | ||
| 25 | + label="作业类型" | ||
| 26 | + prop="workTypeName" | ||
| 27 | + required | ||
| 28 | + border-bottom | ||
| 29 | + @click="openWorkTypeSheet(); hideKeyboard()" | ||
| 30 | + > | ||
| 31 | + <up-input | ||
| 32 | + v-model="workOrderForm.workTypeName" | ||
| 33 | + disabled | ||
| 34 | + disabled-color="#ffffff" | ||
| 35 | + placeholder="请选择作业类型" | ||
| 36 | + border="none" | ||
| 37 | + ></up-input> | ||
| 38 | + <template #right> | ||
| 39 | + <up-icon name="arrow-right" size="16"></up-icon> | ||
| 40 | + </template> | ||
| 41 | + </up-form-item> | ||
| 42 | + | ||
| 43 | + <!-- 3. 共同处理人(支持多选) --> | ||
| 24 | <up-form-item | 44 | <up-form-item |
| 25 | label="共同处理人" | 45 | label="共同处理人" |
| 26 | prop="coProcessorName" | 46 | prop="coProcessorName" |
| @@ -55,6 +75,21 @@ | @@ -55,6 +75,21 @@ | ||
| 55 | ></up-textarea> | 75 | ></up-textarea> |
| 56 | </up-form-item> | 76 | </up-form-item> |
| 57 | 77 | ||
| 78 | + <!-- 4. 动态字段:按「业务类型(busiLine) + 作业类型」加载数据中心配置的工单填报项 --> | ||
| 79 | + <view class="dynamic-field-block" v-if="workOrderForm.workTypeId"> | ||
| 80 | + <view class="dynamic-field-title">作业填报</view> | ||
| 81 | + <work-field-form | ||
| 82 | + ref="workFieldFormRef" | ||
| 83 | + :business-type="workOrderForm.businessType" | ||
| 84 | + :biz-type="1" | ||
| 85 | + :work-type-id="workOrderForm.workTypeId" | ||
| 86 | + @loaded="onDynamicFieldsLoaded" | ||
| 87 | + ></work-field-form> | ||
| 88 | + <view class="dynamic-field-empty" v-if="dynamicFieldCount === 0"> | ||
| 89 | + <text>该作业类型暂无需填报项</text> | ||
| 90 | + </view> | ||
| 91 | + </view> | ||
| 92 | + | ||
| 58 | <!-- Tabs标签栏(图片切换:开始、进行中、结束、人员、材料)- 改造后 --> | 93 | <!-- Tabs标签栏(图片切换:开始、进行中、结束、人员、材料)- 改造后 --> |
| 59 | <view class="tabs-wrap " style="margin-top: 30rpx;"> | 94 | <view class="tabs-wrap " style="margin-top: 30rpx;"> |
| 60 | <up-tabs | 95 | <up-tabs |
| @@ -168,6 +203,16 @@ | @@ -168,6 +203,16 @@ | ||
| 168 | @close="handleActionSheetClose" | 203 | @close="handleActionSheetClose" |
| 169 | @select="handleActionSheetSelect" | 204 | @select="handleActionSheetSelect" |
| 170 | ></up-action-sheet> | 205 | ></up-action-sheet> |
| 206 | + | ||
| 207 | + <!-- 作业类型选择弹窗(字典 maintain_type) --> | ||
| 208 | + <up-action-sheet | ||
| 209 | + :show="showWorkTypeSheet" | ||
| 210 | + :actions="workTypeOptions" | ||
| 211 | + title="请选择作业类型" | ||
| 212 | + cancel-text="取消" | ||
| 213 | + @close="showWorkTypeSheet = false" | ||
| 214 | + @select="onWorkTypeSelect" | ||
| 215 | + ></up-action-sheet> | ||
| 171 | </view> | 216 | </view> |
| 172 | </template> | 217 | </template> |
| 173 | 218 | ||
| @@ -176,6 +221,7 @@ import { ref, reactive, watch } from 'vue' | @@ -176,6 +221,7 @@ import { ref, reactive, watch } from 'vue' | ||
| 176 | import { onLoad, onReady, onShow } from '@dcloudio/uni-app'; | 221 | import { onLoad, onReady, onShow } from '@dcloudio/uni-app'; |
| 177 | import { useUploadImgs } from '@/common/utils/useUploadImgs' | 222 | import { useUploadImgs } from '@/common/utils/useUploadImgs' |
| 178 | import { getYlWorkersPage, universalApproval } from '@/api/work-order-manage/work-order-manage' | 223 | import { getYlWorkersPage, universalApproval } from '@/api/work-order-manage/work-order-manage' |
| 224 | +import { workRecordSubmit } from '@/api/common' | ||
| 179 | import { nextStepMap } from '@/common/utils/common' | 225 | import { nextStepMap } from '@/common/utils/common' |
| 180 | import { useUserStore } from '@/pinia/user'; | 226 | import { useUserStore } from '@/pinia/user'; |
| 181 | const userStore = useUserStore(); | 227 | const userStore = useUserStore(); |
| @@ -183,6 +229,20 @@ const userStore = useUserStore(); | @@ -183,6 +229,20 @@ const userStore = useUserStore(); | ||
| 183 | // 表单Ref | 229 | // 表单Ref |
| 184 | const workOrderFormRef = ref(null) | 230 | const workOrderFormRef = ref(null) |
| 185 | 231 | ||
| 232 | +// 作业填报动态表单Ref(components/work-field-form) | ||
| 233 | +const workFieldFormRef = ref(null) | ||
| 234 | + | ||
| 235 | +// 作业类型选择弹窗 | ||
| 236 | +const showWorkTypeSheet = ref(false) | ||
| 237 | +// 作业类型选项:字典 maintain_type(打开弹窗时取,避免字典异步加载导致首次为空) | ||
| 238 | +const workTypeOptions = ref([]) | ||
| 239 | + | ||
| 240 | +// 当前作业类型下的动态字段数量(0 时给「无需填报」提示) | ||
| 241 | +const dynamicFieldCount = ref(0) | ||
| 242 | + | ||
| 243 | +// 动态字段提交幂等流水号:同一页面生命周期内保持不变,网络重试只落一条记录 | ||
| 244 | +let workRecordClientSeq = '' | ||
| 245 | + | ||
| 186 | // 页面状态 | 246 | // 页面状态 |
| 187 | const showActionSheet = ref(false) | 247 | const showActionSheet = ref(false) |
| 188 | const currentActionSheetData = reactive({ | 248 | const currentActionSheetData = reactive({ |
| @@ -207,6 +267,9 @@ const coProcessorList = ref([]) | @@ -207,6 +267,9 @@ const coProcessorList = ref([]) | ||
| 207 | // 工单表单数据(修改:新增coProcessorName数组,coProcessorNameStr用于页面展示) | 267 | // 工单表单数据(修改:新增coProcessorName数组,coProcessorNameStr用于页面展示) |
| 208 | const workOrderForm = reactive({ | 268 | const workOrderForm = reactive({ |
| 209 | busiLine:'', | 269 | busiLine:'', |
| 270 | + businessType:'', // 业务类型(由 busiLine 首段得出,用于加载动态字段配置) | ||
| 271 | + workTypeId:'', // 作业类型ID(字典 maintain_type,用于加载动态字段配置) | ||
| 272 | + workTypeName:'', // 作业类型名称(页面展示) | ||
| 210 | workerDataId:'', | 273 | workerDataId:'', |
| 211 | taskId:'', // 任务id | 274 | taskId:'', // 任务id |
| 212 | orderNo: '', // 工单编号 | 275 | orderNo: '', // 工单编号 |
| @@ -288,6 +351,9 @@ const workOrderFormRules = reactive({ | @@ -288,6 +351,9 @@ const workOrderFormRules = reactive({ | ||
| 288 | reason: [ | 351 | reason: [ |
| 289 | { type: 'string', required: true, message: '请输入处理情况描述', trigger: ['change', 'blur'] }, | 352 | { type: 'string', required: true, message: '请输入处理情况描述', trigger: ['change', 'blur'] }, |
| 290 | { type: 'string', max: 200, message: '处理情况描述最多200字', trigger: ['change', 'blur'] } | 353 | { type: 'string', max: 200, message: '处理情况描述最多200字', trigger: ['change', 'blur'] } |
| 354 | + ], | ||
| 355 | + workTypeName: [ | ||
| 356 | + { type: 'string', required: true, message: '请选择作业类型', trigger: ['change'] } | ||
| 291 | ] | 357 | ] |
| 292 | }) | 358 | }) |
| 293 | 359 | ||
| @@ -353,6 +419,13 @@ onLoad((options) => { | @@ -353,6 +419,13 @@ onLoad((options) => { | ||
| 353 | workOrderForm.orderNo = orderItem.orderNo || ''; | 419 | workOrderForm.orderNo = orderItem.orderNo || ''; |
| 354 | workOrderForm.reason = orderItem.handleResult || ''; // 处理情况回显 | 420 | workOrderForm.reason = orderItem.handleResult || ''; // 处理情况回显 |
| 355 | workOrderForm.busiLine = orderItem.busiLine | 421 | workOrderForm.busiLine = orderItem.busiLine |
| 422 | + // 业务线可能是逗号分隔多值(如 "yl,hj"),字段配置按单值定位,取首段 | ||
| 423 | + workOrderForm.businessType = String(orderItem.busiLine || '').split(',')[0].trim() | ||
| 424 | + // 来源数据若已带作业类型则回显(工单主表当前无该字段,通常为空,由用户手选) | ||
| 425 | + if (orderItem.workTypeId) { | ||
| 426 | + workOrderForm.workTypeId = orderItem.workTypeId | ||
| 427 | + workOrderForm.workTypeName = uni.$dict.getDictLabel('maintain_type', orderItem.workTypeId) || '' | ||
| 428 | + } | ||
| 356 | // 共同处理人(如有数据则回显,转换为数组格式) | 429 | // 共同处理人(如有数据则回显,转换为数组格式) |
| 357 | if (orderItem.coHandlers && orderItem.coHandlersName) { | 430 | if (orderItem.coHandlers && orderItem.coHandlersName) { |
| 358 | // 处理ID:如果是字符串(逗号分隔),转为数组;本身是数组则直接赋值 | 431 | // 处理ID:如果是字符串(逗号分隔),转为数组;本身是数组则直接赋值 |
| @@ -620,6 +693,44 @@ const hideKeyboard = () => { | @@ -620,6 +693,44 @@ const hideKeyboard = () => { | ||
| 620 | uni.hideKeyboard() | 693 | uni.hideKeyboard() |
| 621 | } | 694 | } |
| 622 | 695 | ||
| 696 | +// 打开作业类型选择弹窗 | ||
| 697 | +const openWorkTypeSheet = () => { | ||
| 698 | + // up-action-sheet 用 name 展示,value 作为回传值 | ||
| 699 | + workTypeOptions.value = uni.$dict.getDictSimpleList('maintain_type') | ||
| 700 | + .map(item => ({ name: item.label, value: item.value })) | ||
| 701 | + if (workTypeOptions.value.length === 0) { | ||
| 702 | + uni.showToast({ | ||
| 703 | + title: '暂无作业类型字典数据', | ||
| 704 | + icon: 'none', | ||
| 705 | + duration: 2000 | ||
| 706 | + }) | ||
| 707 | + return | ||
| 708 | + } | ||
| 709 | + showWorkTypeSheet.value = true | ||
| 710 | +} | ||
| 711 | + | ||
| 712 | +// 选择作业类型:驱动动态字段按「业务类型 + 作业类型」重新加载 | ||
| 713 | +const onWorkTypeSelect = (e) => { | ||
| 714 | + showWorkTypeSheet.value = false | ||
| 715 | + if (!e || e.value === undefined || e.value === null || e.value === '') { | ||
| 716 | + return | ||
| 717 | + } | ||
| 718 | + workOrderForm.workTypeId = e.value | ||
| 719 | + workOrderForm.workTypeName = e.name | ||
| 720 | + dynamicFieldCount.value = 0 | ||
| 721 | + if (workOrderFormRef.value) { | ||
| 722 | + workOrderFormRef.value.validateField('workTypeName') | ||
| 723 | + } | ||
| 724 | +} | ||
| 725 | + | ||
| 726 | +// 动态字段加载完成(count=0 时页面提示「暂无需填报项」) | ||
| 727 | +const onDynamicFieldsLoaded = (count) => { | ||
| 728 | + dynamicFieldCount.value = count || 0 | ||
| 729 | +} | ||
| 730 | + | ||
| 731 | +// 动态字段提交幂等流水号(后端按 userId+clientSeq 去重) | ||
| 732 | +const genClientSeq = () => `${Date.now()}-${Math.random().toString(16).slice(2, 10)}` | ||
| 733 | + | ||
| 623 | // 提交工单(修改:将coProcessorName数组一并传入提交参数) | 734 | // 提交工单(修改:将coProcessorName数组一并传入提交参数) |
| 624 | const submitWorkOrder = async () => { | 735 | const submitWorkOrder = async () => { |
| 625 | try { | 736 | try { |
| @@ -687,6 +798,23 @@ const submitWorkOrder = async () => { | @@ -687,6 +798,23 @@ const submitWorkOrder = async () => { | ||
| 687 | // ========== 原有表单校验 ========== | 798 | // ========== 原有表单校验 ========== |
| 688 | await workOrderFormRef.value.validate() | 799 | await workOrderFormRef.value.validate() |
| 689 | 800 | ||
| 801 | + // ========== 作业类型 + 动态字段校验 ========== | ||
| 802 | + if (!workOrderForm.workTypeId) { | ||
| 803 | + uni.showToast({ | ||
| 804 | + title: '请选择作业类型', | ||
| 805 | + icon: 'none', | ||
| 806 | + duration: 2000 | ||
| 807 | + }); | ||
| 808 | + console.warn('submitWorkOrder校验失败:作业类型为空'); | ||
| 809 | + return; | ||
| 810 | + } | ||
| 811 | + let dynamicValues = [] | ||
| 812 | + if (workFieldFormRef.value) { | ||
| 813 | + // 校验失败时 reject 字段数组(uview-plus 已提示),交给外层 catch 忽略 | ||
| 814 | + await workFieldFormRef.value.validate() | ||
| 815 | + dynamicValues = workFieldFormRef.value.getValues() | ||
| 816 | + } | ||
| 817 | + | ||
| 690 | // ========== 校验核心提交参数 ========== | 818 | // ========== 校验核心提交参数 ========== |
| 691 | if (!workOrderForm.taskId) { | 819 | if (!workOrderForm.taskId) { |
| 692 | uni.showToast({ | 820 | uni.showToast({ |
| @@ -748,6 +876,24 @@ const submitWorkOrder = async () => { | @@ -748,6 +876,24 @@ const submitWorkOrder = async () => { | ||
| 748 | } | 876 | } |
| 749 | 877 | ||
| 750 | uni.showLoading({ title: '提交中...' }) | 878 | uni.showLoading({ title: '提交中...' }) |
| 879 | + | ||
| 880 | + // ========== 先落作业填报(动态字段),再走审批流转 ========== | ||
| 881 | + // 值按「业务类型 + 作业类型」的字段配置落库;同一工单重复实施时后端按 (bizType,bizId) 覆盖原记录并红冲旧物料 | ||
| 882 | + if (dynamicValues.length > 0) { | ||
| 883 | + if (!workRecordClientSeq) { | ||
| 884 | + workRecordClientSeq = genClientSeq() | ||
| 885 | + } | ||
| 886 | + await workRecordSubmit({ | ||
| 887 | + businessType: workOrderForm.businessType, | ||
| 888 | + bizType: 1, | ||
| 889 | + bizId: Number(workOrderForm.workerDataId), | ||
| 890 | + workTypeId: Number(workOrderForm.workTypeId), | ||
| 891 | + clientSeq: workRecordClientSeq, | ||
| 892 | + remark: workOrderForm.reason.trim(), | ||
| 893 | + fields: dynamicValues | ||
| 894 | + }) | ||
| 895 | + } | ||
| 896 | + | ||
| 751 | const res = await universalApproval(submitData) | 897 | const res = await universalApproval(submitData) |
| 752 | 898 | ||
| 753 | // 校验接口返回结果 | 899 | // 校验接口返回结果 |
| @@ -779,6 +925,9 @@ const submitWorkOrder = async () => { | @@ -779,6 +925,9 @@ const submitWorkOrder = async () => { | ||
| 779 | } catch (error) { | 925 | } catch (error) { |
| 780 | uni.hideLoading() | 926 | uni.hideLoading() |
| 781 | 927 | ||
| 928 | + // 本次提交失败后重置流水号:下次重试允许按最新填报值覆盖原记录(后端按 bizType+bizId 覆盖,不会重复建单) | ||
| 929 | + workRecordClientSeq = '' | ||
| 930 | + | ||
| 782 | // 区分校验失败和接口失败,仅接口失败时提示(校验失败已有内置提示) | 931 | // 区分校验失败和接口失败,仅接口失败时提示(校验失败已有内置提示) |
| 783 | if (!Array.isArray(error)) { | 932 | if (!Array.isArray(error)) { |
| 784 | uni.showToast({ | 933 | uni.showToast({ |
| @@ -824,5 +973,25 @@ const submitWorkOrder = async () => { | @@ -824,5 +973,25 @@ const submitWorkOrder = async () => { | ||
| 824 | font-weight: bold; | 973 | font-weight: bold; |
| 825 | } | 974 | } |
| 826 | 975 | ||
| 976 | +// 作业填报(动态字段)区块 | ||
| 977 | +.dynamic-field-block { | ||
| 978 | + margin-top: 10rpx; | ||
| 979 | + | ||
| 980 | + .dynamic-field-title { | ||
| 981 | + padding: 20rpx 0 10rpx; | ||
| 982 | + font-size: 30rpx; | ||
| 983 | + font-weight: 600; | ||
| 984 | + color: #303133; | ||
| 985 | + border-bottom: 1rpx solid #f0f0f0; | ||
| 986 | + } | ||
| 987 | + | ||
| 988 | + .dynamic-field-empty { | ||
| 989 | + padding: 30rpx 0; | ||
| 990 | + text-align: center; | ||
| 991 | + font-size: 26rpx; | ||
| 992 | + color: #999; | ||
| 993 | + } | ||
| 994 | +} | ||
| 995 | + | ||
| 827 | 996 | ||
| 828 | </style> | 997 | </style> |
| 829 | \ No newline at end of file | 998 | \ No newline at end of file |
pages-sub/problem/work-order-manage/order-detail.vue
| @@ -177,6 +177,42 @@ | @@ -177,6 +177,42 @@ | ||
| 177 | <text v-else class="empty-img-text">养护员未上传图片</text> | 177 | <text v-else class="empty-img-text">养护员未上传图片</text> |
| 178 | </view> | 178 | </view> |
| 179 | </view> | 179 | </view> |
| 180 | + | ||
| 181 | + <!-- 作业填报明细(工单实施时按「业务类型+作业类型」动态字段配置填报) --> | ||
| 182 | + <view class="work-field-block" v-if="workFieldValues.length > 0"> | ||
| 183 | + <view class="work-field-title">作业填报</view> | ||
| 184 | + <up-cell-group :border="false"> | ||
| 185 | + <up-cell | ||
| 186 | + v-for="item in workFieldValues" | ||
| 187 | + :key="item.fieldCode" | ||
| 188 | + :title="item.fieldName" | ||
| 189 | + align="middle" | ||
| 190 | + :border="false" | ||
| 191 | + > | ||
| 192 | + <template #value> | ||
| 193 | + <!-- 7图片:value_text 为 URL JSON 数组 --> | ||
| 194 | + <view v-if="item.fieldType === 7 && parseFieldUrls(item).length > 0" class="work-field-imgs"> | ||
| 195 | + <up-album | ||
| 196 | + :urls="parseFieldUrls(item)" | ||
| 197 | + :singleSize="60" | ||
| 198 | + :multipleSize="60" | ||
| 199 | + :preview-full-image="true" | ||
| 200 | + ></up-album> | ||
| 201 | + </view> | ||
| 202 | + <!-- 8附件:展示文件名 --> | ||
| 203 | + <view v-else-if="item.fieldType === 8 && parseFieldUrls(item).length > 0" class="work-field-files"> | ||
| 204 | + <text | ||
| 205 | + v-for="(url, idx) in parseFieldUrls(item)" | ||
| 206 | + :key="idx" | ||
| 207 | + class="work-field-file up-line-1" | ||
| 208 | + >{{ fileNameOf(url) }}</text> | ||
| 209 | + </view> | ||
| 210 | + <!-- 其余类型:后端已按配置把编码翻成可读文案 --> | ||
| 211 | + <view v-else class="common-text-color work-field-value">{{ fieldDisplayText(item) }}</view> | ||
| 212 | + </template> | ||
| 213 | + </up-cell> | ||
| 214 | + </up-cell-group> | ||
| 215 | + </view> | ||
| 180 | </view> | 216 | </view> |
| 181 | 217 | ||
| 182 | <!-- 2. 流程节点Tab --> | 218 | <!-- 2. 流程节点Tab --> |
| @@ -381,6 +417,7 @@ import { | @@ -381,6 +417,7 @@ import { | ||
| 381 | universalApproval, | 417 | universalApproval, |
| 382 | getTaskActions | 418 | getTaskActions |
| 383 | } from '@/api/work-order-manage/work-order-manage'; | 419 | } from '@/api/work-order-manage/work-order-manage'; |
| 420 | +import {getWorkRecordValues} from '@/api/common' | ||
| 384 | import {nextStepMap, buzStatusMap, calculateFormatTimeDiff, getActionButtonType} from '@/common/utils/common' | 421 | import {nextStepMap, buzStatusMap, calculateFormatTimeDiff, getActionButtonType} from '@/common/utils/common' |
| 385 | // 引入图片上传组合式函数 | 422 | // 引入图片上传组合式函数 |
| 386 | import {useUploadImgs} from '@/common/utils/useUploadImgs' | 423 | import {useUploadImgs} from '@/common/utils/useUploadImgs' |
| @@ -469,6 +506,9 @@ const orderDetail = ref<any>({ | @@ -469,6 +506,9 @@ const orderDetail = ref<any>({ | ||
| 469 | // ========== 新增:声明eventChannel变量 ========== | 506 | // ========== 新增:声明eventChannel变量 ========== |
| 470 | const eventChannel = ref(null); | 507 | const eventChannel = ref(null); |
| 471 | 508 | ||
| 509 | +// 作业填报明细(工单实施时按「业务类型+作业类型」动态字段配置落库的值) | ||
| 510 | +const workFieldValues = ref<any[]>([]); | ||
| 511 | + | ||
| 472 | /** | 512 | /** |
| 473 | * 当前激活的图片列表(无需函数调用,直接访问) | 513 | * 当前激活的图片列表(无需函数调用,直接访问) |
| 474 | */ | 514 | */ |
| @@ -538,6 +578,8 @@ const DetailQuery = async (taskIdStr: string) => { | @@ -538,6 +578,8 @@ const DetailQuery = async (taskIdStr: string) => { | ||
| 538 | configuredActions.value = await actionPromise; | 578 | configuredActions.value = await actionPromise; |
| 539 | orderDetail.value = res; | 579 | orderDetail.value = res; |
| 540 | currentImgList.value = orderDetail.value.startImgs | 580 | currentImgList.value = orderDetail.value.startImgs |
| 581 | + // 并行无依赖:填报明细单独拉,失败不影响详情展示 | ||
| 582 | + loadWorkFieldValues(res) | ||
| 541 | } | 583 | } |
| 542 | } catch (error) { | 584 | } catch (error) { |
| 543 | console.error('获取工单详情失败:', error); | 585 | console.error('获取工单详情失败:', error); |
| @@ -547,6 +589,46 @@ const DetailQuery = async (taskIdStr: string) => { | @@ -547,6 +589,46 @@ const DetailQuery = async (taskIdStr: string) => { | ||
| 547 | } | 589 | } |
| 548 | }; | 590 | }; |
| 549 | 591 | ||
| 592 | +/** | ||
| 593 | + * 加载作业填报明细(动态字段) | ||
| 594 | + * bizType=1 工单,bizId=workorder_main_info.id | ||
| 595 | + */ | ||
| 596 | +const loadWorkFieldValues = async (detail: any) => { | ||
| 597 | + workFieldValues.value = [] | ||
| 598 | + if (!detail || !detail.id) return | ||
| 599 | + try { | ||
| 600 | + const res = await getWorkRecordValues({bizType: 1, bizId: detail.id}) | ||
| 601 | + workFieldValues.value = Array.isArray(res) ? res : [] | ||
| 602 | + } catch (error) { | ||
| 603 | + console.error('获取填报明细失败:', error) | ||
| 604 | + } | ||
| 605 | +} | ||
| 606 | + | ||
| 607 | +// 7图片/8附件:value_text 存 URL 的 JSON 数组 | ||
| 608 | +const parseFieldUrls = (item: any) => { | ||
| 609 | + try { | ||
| 610 | + const arr = JSON.parse(item.valueText || '[]') | ||
| 611 | + return Array.isArray(arr) ? arr : [] | ||
| 612 | + } catch (e) { | ||
| 613 | + return [] | ||
| 614 | + } | ||
| 615 | +} | ||
| 616 | + | ||
| 617 | +// 附件展示末段文件名 | ||
| 618 | +const fileNameOf = (url: string) => { | ||
| 619 | + if (!url) return '' | ||
| 620 | + const path = String(url).split('?')[0].split('#')[0] | ||
| 621 | + const name = path.substring(path.lastIndexOf('/') + 1) | ||
| 622 | + try { | ||
| 623 | + return decodeURIComponent(name) || url | ||
| 624 | + } catch (e) { | ||
| 625 | + return name || url | ||
| 626 | + } | ||
| 627 | +} | ||
| 628 | + | ||
| 629 | +// 其余类型直接展示后端生成的可读文案(item.text) | ||
| 630 | +const fieldDisplayText = (item: any) => item.text || item.valueText || '--' | ||
| 631 | + | ||
| 550 | // 页面加载参数 | 632 | // 页面加载参数 |
| 551 | const taskId = ref('') | 633 | const taskId = ref('') |
| 552 | const activeTab = ref('') | 634 | const activeTab = ref('') |
| @@ -1241,4 +1323,39 @@ onShow(() => { | @@ -1241,4 +1323,39 @@ onShow(() => { | ||
| 1241 | .main-content.pb-rows-3 { | 1323 | .main-content.pb-rows-3 { |
| 1242 | padding-bottom: 390rpx; | 1324 | padding-bottom: 390rpx; |
| 1243 | } | 1325 | } |
| 1326 | + | ||
| 1327 | +// 作业填报明细 | ||
| 1328 | +.work-field-block { | ||
| 1329 | + margin-top: 20rpx; | ||
| 1330 | + | ||
| 1331 | + .work-field-title { | ||
| 1332 | + padding: 16rpx 30rpx; | ||
| 1333 | + font-size: 30rpx; | ||
| 1334 | + font-weight: 600; | ||
| 1335 | + color: #303133; | ||
| 1336 | + background: #fff; | ||
| 1337 | + } | ||
| 1338 | + | ||
| 1339 | + .work-field-value { | ||
| 1340 | + max-width: 420rpx; | ||
| 1341 | + text-align: right; | ||
| 1342 | + word-break: break-all; | ||
| 1343 | + } | ||
| 1344 | + | ||
| 1345 | + .work-field-imgs { | ||
| 1346 | + width: 100%; | ||
| 1347 | + } | ||
| 1348 | + | ||
| 1349 | + .work-field-files { | ||
| 1350 | + display: flex; | ||
| 1351 | + flex-direction: column; | ||
| 1352 | + align-items: flex-end; | ||
| 1353 | + | ||
| 1354 | + .work-field-file { | ||
| 1355 | + max-width: 420rpx; | ||
| 1356 | + font-size: 26rpx; | ||
| 1357 | + color: #3c9cff; | ||
| 1358 | + } | ||
| 1359 | + } | ||
| 1360 | +} | ||
| 1244 | </style> | 1361 | </style> |
| 1245 | \ No newline at end of file | 1362 | \ No newline at end of file |