Commit 71518877d10407cdd3007c93bf5498aedd39c0eb
1 parent
2c6690f5
业务线 小程序不用写死,从后端接口获取
Showing
7 changed files
with
707 additions
and
130 deletions
api/common.js
| ... | ... | @@ -57,7 +57,7 @@ export const getByType = (params) => { |
| 57 | 57 | }; |
| 58 | 58 | |
| 59 | 59 | /** |
| 60 | - * 拉取枚举选项 用于 `fieldType=3`(枚举)且 `options` 为空(即非静态来源)时获取可选项。 | |
| 60 | + * 拉取选项 用于 fieldType 3(枚举)/6(多选)/9(关联记录) 且 field.options 为空(即非静态来源)时获取可选项。 | |
| 61 | 61 | * @param {Object} params {} |
| 62 | 62 | * @returns {Promise} |
| 63 | 63 | */ |
| ... | ... | @@ -66,7 +66,8 @@ export const getOptionsByCode = (params) => { |
| 66 | 66 | }; |
| 67 | 67 | |
| 68 | 68 | /** |
| 69 | - * 拉取拉取物料列表 用于`fieldType=6`(物料)字段,列出可领物料及库存。 | |
| 69 | + * 拉取物料列表 用于 optionSource=3 的字段(物料不是字段类型,是「fieldType=3 + optionSource=3」的配置组合), | |
| 70 | + * 比 options 多返回单位与库存余量。 | |
| 70 | 71 | * @param {Object} params {} |
| 71 | 72 | * @returns {Promise} |
| 72 | 73 | */ |
| ... | ... | @@ -74,6 +75,17 @@ export const getMaterialsByCode = (params) => { |
| 74 | 75 | return get('/app-api/app/garden/work-field-config/materials', params); |
| 75 | 76 | }; |
| 76 | 77 | |
| 78 | +/** | |
| 79 | + * 提交耗材作业记录(动态字段落库)。 | |
| 80 | + * 注意与 maintain-plan-commit/create 是两条链路:后者提交养护计划本身(图片/描述), | |
| 81 | + * 本接口提交动态字段值,bizId 用后者返回的 commit.id。 | |
| 82 | + * @param {Object} params {businessType, bizType, bizId, workTypeId, clientSeq, remark, fields} | |
| 83 | + * @returns {Promise} recordId | |
| 84 | + */ | |
| 85 | +export const workRecordSubmit = (data) => { | |
| 86 | + return post('/app-api/app/garden/work-record/submit', data); | |
| 87 | +}; | |
| 88 | + | |
| 77 | 89 | |
| 78 | 90 | |
| 79 | 91 | ... | ... |
pages-sub/daily/maintain-manage/add-record.vue
| ... | ... | @@ -5,7 +5,7 @@ |
| 5 | 5 | <!-- ========== 动态表单区域 ========== --> |
| 6 | 6 | <template v-for="item in dynamicFields" :key="item.fieldCode"> |
| 7 | 7 | <up-form-item :label="item.fieldName" :prop="item.fieldCode" :required="item.required === 1" class="form-item" |
| 8 | - @click="(item.fieldType === 3 || item.fieldType === 6) && item.multiple === 0 && openActionSheet(item)"> | |
| 8 | + @click="openPickerIfNeeded(item)"> | |
| 9 | 9 | <!-- type 1 数值:u-number-box,最多2位小数,min=0,max取item.length --> |
| 10 | 10 | <view v-if="item.fieldType === 1" class="number-box-wrap"> |
| 11 | 11 | <u-number-box v-model="inspectForm[item.fieldCode]" min="1" :step="1" |
| ... | ... | @@ -17,11 +17,11 @@ |
| 17 | 17 | <!-- type 2 文本 --> |
| 18 | 18 | <up-input v-else-if="item.fieldType === 2" v-model="inspectForm[item.fieldCode]" |
| 19 | 19 | :maxlength="item.length || 50" :placeholder="`请输入${item.fieldName}`"></up-input> |
| 20 | - <!-- type3 枚举:多选 multiple=1 使用复选框组 --> | |
| 20 | + <!-- type3 枚举:多选 multiple=1 使用复选框组(name 必须是 value,提交按逗号拼接 value,与值格式契约一致) --> | |
| 21 | 21 | <u-checkbox-group v-else-if="item.fieldType === 3 && item.multiple === 1" |
| 22 | 22 | v-model="inspectForm[item.fieldCode]"> |
| 23 | - <u-checkbox v-for="opt in item.selectOptions" :key="opt.value" :label="opt.label" :name="opt.label"> | |
| 24 | - | |
| 23 | + <u-checkbox v-for="opt in item.selectOptions" :key="opt.value" :label="opt.label" :name="opt.value"> | |
| 24 | + | |
| 25 | 25 | </u-checkbox> |
| 26 | 26 | </u-checkbox-group> |
| 27 | 27 | <!-- type3 枚举:单选 用up-action-sheet 底部选择 --> |
| ... | ... | @@ -40,13 +40,60 @@ |
| 40 | 40 | <up-radio label="是" name="是"></up-radio> |
| 41 | 41 | <up-radio label="否" name="否"></up-radio> |
| 42 | 42 | </up-radio-group> |
| 43 | - <!-- type6 物料选择:up-action-sheet 底部选择 --> | |
| 44 | - <up-input v-else-if="item.fieldType === 6" v-model="labelMap[item.fieldCode]" readonly | |
| 43 | + <!-- type6 多选:复选框组;选项来源可为静态/字典/物料/数据表/SQL/接口(物料不再占用 6 号) --> | |
| 44 | + <view v-else-if="item.fieldType === 6" class="multi-wrap"> | |
| 45 | + <u-checkbox-group v-model="inspectForm[item.fieldCode]" @change="onMultiChange(item)"> | |
| 46 | + <u-checkbox v-for="opt in item.selectOptions" :key="opt.value" :label="opt.label" :name="opt.value" | |
| 47 | + :disabled="isOptionLocked(item, opt)"> | |
| 48 | + </u-checkbox> | |
| 49 | + </u-checkbox-group> | |
| 50 | + <!-- 有限上限时提示已选数。上限靠「禁用未选中项」实现:u-checkbox 的勾选态在组件内部,回写 v-model 撤不掉 --> | |
| 51 | + <text v-if="multiHint(item)" class="multi-hint">{{ multiHint(item) }}</text> | |
| 52 | + <!-- needQuantity:已选项各配一个数量输入,值按 {value,label,quantity} 提交。此形态仅记录用量、不扣库存 --> | |
| 53 | + <view v-if="needQty(item)" class="qty-list"> | |
| 54 | + <view v-for="opt in checkedOptions(item)" :key="opt.value" class="qty-row"> | |
| 55 | + <text class="qty-label">{{ opt.label }}</text> | |
| 56 | + <u-number-box v-model="qtyMap[item.fieldCode][opt.value]" :min="0" :step="1"></u-number-box> | |
| 57 | + </view> | |
| 58 | + </view> | |
| 59 | + </view> | |
| 60 | + <!-- type7 图片:多张上传,提交 JSON 数组(URL) --> | |
| 61 | + <up-upload v-else-if="item.fieldType === 7" :file-list="fileListMap[item.fieldCode] || []" | |
| 62 | + @after-read="(event) => uploadFieldFiles(item, event)" @delete="(event) => deleteFieldFile(item, event)" | |
| 63 | + multiple :max-count="pickCountOf(item, 5)" upload-text="选择图片" del-color="#ff4d4f" | |
| 64 | + class="upload-wrap"></up-upload> | |
| 65 | + <!-- type8 附件:微信只能从聊天记录选文件(无通用文件选择器),提交 JSON 数组(URL) --> | |
| 66 | + <view v-else-if="item.fieldType === 8" class="file-wrap"> | |
| 67 | + <view v-for="(file, idx) in fileListMap[item.fieldCode] || []" :key="idx" class="file-row"> | |
| 68 | + <text class="file-name">{{ file.name || fileNameOf(file.url) }}</text> | |
| 69 | + <text class="file-del" @click.stop="deleteFieldFile(item, { index: idx })">删除</text> | |
| 70 | + </view> | |
| 71 | + <up-button size="small" type="primary" plain :text="fileButtonText(item)" | |
| 72 | + @click.stop="chooseFieldFiles(item)"></up-button> | |
| 73 | + </view> | |
| 74 | + <!-- type9 关联记录:底部选择,提交 JSON 数组 [{id,name}] --> | |
| 75 | + <up-input v-else-if="item.fieldType === 9" v-model="labelMap[item.fieldCode]" readonly | |
| 45 | 76 | disabled-color="#ffffff" :placeholder="`请选择${item.fieldName}`" border="none"> |
| 46 | 77 | <template #right> |
| 47 | 78 | <up-icon name="arrow-right" size="16"></up-icon> |
| 48 | 79 | </template> |
| 49 | 80 | </up-input> |
| 81 | + <!-- type10 链接:URL 输入,提交裸 URL --> | |
| 82 | + <up-input v-else-if="item.fieldType === 10" v-model.trim="inspectForm[item.fieldCode]" | |
| 83 | + :placeholder="`请输入${item.fieldName}(http/https 开头)`"></up-input> | |
| 84 | + <!-- type11 地理位置:uni.chooseLocation 选点,提交 JSON 对象 {lng,lat,address} --> | |
| 85 | + <up-input v-else-if="item.fieldType === 11" 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 | + <!-- 物料(fieldType=3 + optionSource=3):额外收领用数量;数量>0 才提交 materialId/materialName/quantity 触发扣库存。 | |
| 92 | + 必须放在 v-if/v-else-if 链末尾——插在中间会打断链导致编译报错 --> | |
| 93 | + <view v-if="item.fieldType === 3 && item.optionSource === 3" class="qty-row qty-row-material"> | |
| 94 | + <text class="qty-label">领用数量</text> | |
| 95 | + <u-number-box v-model="qtyMap[item.fieldCode]" :min="0" :step="1"></u-number-box> | |
| 96 | + </view> | |
| 50 | 97 | </up-form-item> |
| 51 | 98 | </template> |
| 52 | 99 | <!-- ========== 动态表单结束 ========== --> |
| ... | ... | @@ -79,7 +126,7 @@ |
| 79 | 126 | <script lang="ts"> |
| 80 | 127 | import { uploadImages } from '@/common/utils/upload' |
| 81 | 128 | import { maintainCreate } from "@/api/maintain-manage/maintain-manage" |
| 82 | -import { getByType, getOptionsByCode, getMaterialsByCode } from "@/api/common" | |
| 129 | +import { getByType, getOptionsByCode, getMaterialsByCode, workRecordSubmit } from "@/api/common" | |
| 83 | 130 | export default { |
| 84 | 131 | data() { |
| 85 | 132 | return { |
| ... | ... | @@ -90,7 +137,15 @@ export default { |
| 90 | 137 | }, |
| 91 | 138 | // ========== 新增:用来存放展示文本label的映射对象 ========== |
| 92 | 139 | labelMap: {}, |
| 140 | + // 类型7图片/8附件的每字段文件列表(fieldCode -> u-upload 兼容数组 {url,status,name}) | |
| 141 | + fileListMap: {}, | |
| 142 | + // 数量收集:类型6+needQuantity 为 {选项值: 数量};类型3物料为标量(领用数量) | |
| 143 | + qtyMap: {}, | |
| 93 | 144 | paramsOptins: {}, |
| 145 | + // 幂等流水号:本次填报页面生命周期内保持不变,重复提交(如网络超时后重试)只会落一条记录 | |
| 146 | + clientSeq: '', | |
| 147 | + // 养护计划本体的 commit.id:取到后缓存,重试时不再重复创建养护记录 | |
| 148 | + commitId: null, | |
| 94 | 149 | inspectFormRules: { |
| 95 | 150 | images: [ |
| 96 | 151 | { |
| ... | ... | @@ -168,17 +223,37 @@ export default { |
| 168 | 223 | this.datePickerShow = false |
| 169 | 224 | }, |
| 170 | 225 | /** |
| 226 | + * 点击表单项时按类型决定弹出哪个选择器: | |
| 227 | + * 3 枚举(单选) / 9 关联记录 → 底部 actionSheet;11 地理位置 → 微信地图选点 | |
| 228 | + * 其余类型(多选、图片、附件、可输入的链接)不在这里处理,由控件自身响应 | |
| 229 | + */ | |
| 230 | + openPickerIfNeeded(item: any) { | |
| 231 | + // 条件与模板互补:模板用 multiple === 1 渲染复选框组,这里就必须用 !== 1 才打开弹窗, | |
| 232 | + // 否则 multiple 为 NULL/0 的存量单选枚举会既不渲染复选框、又点不开弹窗 | |
| 233 | + if (item.fieldType === 3 && item.multiple !== 1) { | |
| 234 | + return this.openActionSheet(item) | |
| 235 | + } | |
| 236 | + if (item.fieldType === 9) { | |
| 237 | + return this.openActionSheet(item) | |
| 238 | + } | |
| 239 | + if (item.fieldType === 11) { | |
| 240 | + return this.openLocation(item) | |
| 241 | + } | |
| 242 | + }, | |
| 243 | + /** | |
| 171 | 244 | * 打开底部选择弹窗 actionSheet |
| 172 | 245 | */ |
| 173 | 246 | openActionSheet(item: any) { |
| 174 | - console.log('12312'); | |
| 175 | 247 | this.actionSheet.fieldCode = item.fieldCode |
| 176 | 248 | this.actionSheet.fieldItem = item |
| 177 | - // 把selectOptions / materialOptions 转为 actionSheet 需要格式 {name, value} | |
| 178 | - let sourceOptions = item.fieldType === 3 ? item.selectOptions : item.materialOptions | |
| 179 | - this.actionSheet.options = (sourceOptions || []).map(opt => { | |
| 249 | + // 选项统一挂在 selectOptions(静态来源直接用 field.options,物料来源调 materials 接口拿,其余调 options 接口) | |
| 250 | + // 物料带 currentNum 时在展示名后附库存余量(只影响展示名,提交的 materialName 仍取干净的 opt.label) | |
| 251 | + this.actionSheet.options = (item.selectOptions || []).map(opt => { | |
| 252 | + const stock = opt.currentNum === null || opt.currentNum === undefined || opt.currentNum === '' | |
| 253 | + ? NaN | |
| 254 | + : Number(opt.currentNum) | |
| 180 | 255 | return { |
| 181 | - name: opt.label, | |
| 256 | + name: isNaN(stock) ? opt.label : `${opt.label}(库存${stock}${opt.unit || ''})`, | |
| 182 | 257 | value: opt.value |
| 183 | 258 | } |
| 184 | 259 | }) |
| ... | ... | @@ -190,8 +265,12 @@ export default { |
| 190 | 265 | onActionSheetSelect(selectedItem: any) { |
| 191 | 266 | const fieldItem = this.actionSheet.fieldItem |
| 192 | 267 | const fieldCode = this.actionSheet.fieldCode |
| 193 | - // 1. 表单保存value,提交后端 | |
| 194 | - this.inspectForm[fieldCode] = selectedItem.value | |
| 268 | + // 1. 表单保存提交值:9 关联记录存 {id,name}(提交时转成 [{id,name}]),其余存裸 value | |
| 269 | + if (fieldItem && fieldItem.fieldType === 9) { | |
| 270 | + this.inspectForm[fieldCode] = { id: selectedItem.value, name: selectedItem.name } | |
| 271 | + } else { | |
| 272 | + this.inspectForm[fieldCode] = selectedItem.value | |
| 273 | + } | |
| 195 | 274 | // 2. labelMap保存label,给input展示 |
| 196 | 275 | this.labelMap[fieldCode] = selectedItem.name |
| 197 | 276 | this.actionSheet.show = false |
| ... | ... | @@ -199,6 +278,29 @@ export default { |
| 199 | 278 | this.$refs.inspectFormRef.validateField(fieldCode) |
| 200 | 279 | }, |
| 201 | 280 | /** |
| 281 | + * 类型11 地理位置:微信地图选点(无需地图 AK,manifest.json 已声明 requiredPrivateInfos) | |
| 282 | + * 提交形态 {lng,lat,address} | |
| 283 | + */ | |
| 284 | + openLocation(item: any) { | |
| 285 | + uni.chooseLocation({ | |
| 286 | + success: (res) => { | |
| 287 | + this.inspectForm[item.fieldCode] = { | |
| 288 | + lng: res.longitude, | |
| 289 | + lat: res.latitude, | |
| 290 | + address: res.address || res.name || '' | |
| 291 | + } | |
| 292 | + this.labelMap[item.fieldCode] = res.address || res.name || `${res.latitude}, ${res.longitude}` | |
| 293 | + this.$refs.inspectFormRef.validateField(item.fieldCode) | |
| 294 | + }, | |
| 295 | + fail: (err) => { | |
| 296 | + console.error('地图选择失败', err) | |
| 297 | + if (err && err.errMsg && err.errMsg.indexOf('auth deny') > -1) { | |
| 298 | + uni.showToast({ title: '请授权位置权限', icon: 'none' }) | |
| 299 | + } | |
| 300 | + } | |
| 301 | + }) | |
| 302 | + }, | |
| 303 | + /** | |
| 202 | 304 | * 加载动态字段 |
| 203 | 305 | */ |
| 204 | 306 | async loadDynamicFields(businessType: string, workTypeId: string) { |
| ... | ... | @@ -221,45 +323,67 @@ export default { |
| 221 | 323 | return |
| 222 | 324 | } |
| 223 | 325 | for (const field of fields) { |
| 224 | - // type3多选checkbox,初始值为空数组,其余字段空字符串 | |
| 225 | - if (field.fieldType === 3 && field.multiple === 1) { | |
| 226 | - this.$set(this.inspectForm, field.fieldCode, []) | |
| 326 | + // 初值:多选为数组;9关联记录/11地理位置 为对象(未选为 null);7图片/8附件为 URL 数组;其余空字符串 | |
| 327 | + if (field.fieldType === 6 || (field.fieldType === 3 && field.multiple === 1)) { | |
| 328 | + this.inspectForm[field.fieldCode] = [] | |
| 329 | + } else if (field.fieldType === 7 || field.fieldType === 8) { | |
| 330 | + this.inspectForm[field.fieldCode] = [] | |
| 331 | + } else if (field.fieldType === 9 || field.fieldType === 11) { | |
| 332 | + this.inspectForm[field.fieldCode] = null | |
| 227 | 333 | } else { |
| 228 | - this.$set(this.inspectForm, field.fieldCode, '') | |
| 334 | + this.inspectForm[field.fieldCode] = '' | |
| 229 | 335 | } |
| 230 | 336 | // 初始化展示label为空 |
| 231 | - this.$set(this.labelMap, field.fieldCode, '') | |
| 232 | - | |
| 233 | - if (field.required === 1) { | |
| 234 | - const msg = field.fieldType === 5 ? `请选择${field.fieldName}` : `请填写${field.fieldName}` | |
| 235 | - this.inspectFormRules[field.fieldCode] = [ | |
| 236 | - { required: true, message: msg, trigger: 'change' } | |
| 237 | - ] | |
| 337 | + this.labelMap[field.fieldCode] = '' | |
| 338 | + // 7图片/8附件的文件列表(驱动 up-upload 与自绘文件列表展示) | |
| 339 | + if (field.fieldType === 7 || field.fieldType === 8) { | |
| 340 | + this.fileListMap[field.fieldCode] = [] | |
| 238 | 341 | } |
| 239 | - // type3 枚举:优先使用field.options,有则不请求getOptionsByCode | |
| 240 | - if (field.fieldType === 3) { | |
| 241 | - if (field.options && Array.isArray(field.options) && field.options.length > 0) { | |
| 242 | - field.selectOptions = field.options | |
| 342 | + // 选项装载:静态来源(1)直接用下发的 field.options,其余来源(2字典/3物料/4数据表/5SQL/6接口)调 options 接口, | |
| 343 | + // 由后端按 optionSource 分发。类型 3/6/9 需要选项,其余类型不需要 | |
| 344 | + if (field.fieldType === 3 || field.fieldType === 6 || field.fieldType === 9) { | |
| 345 | + if (field.optionSource === 1 && Array.isArray(field.options)) { | |
| 346 | + field.selectOptions = field.options.map(opt => ({ label: opt.label, value: String(opt.value) })) | |
| 347 | + } else if (field.optionSource === 3) { | |
| 348 | + // 物料来源:调 materials(比 options 多返回 currentNum 库存余量/unit),供选择时显示库存 | |
| 349 | + const matRes = await getMaterialsByCode({ | |
| 350 | + businessType, | |
| 351 | + bizType, | |
| 352 | + workTypeId, | |
| 353 | + fieldCode: field.fieldCode | |
| 354 | + }) | |
| 355 | + field.selectOptions = (matRes || []).map(mat => ({ | |
| 356 | + label: mat.materialName, | |
| 357 | + value: String(mat.materialId), | |
| 358 | + currentNum: mat.currentNum, | |
| 359 | + unit: mat.unit | |
| 360 | + })) | |
| 243 | 361 | } else { |
| 244 | - console.log('12312') | |
| 245 | - const dictRes = await getOptionsByCode({ | |
| 362 | + const optRes = await getOptionsByCode({ | |
| 246 | 363 | businessType, |
| 247 | 364 | bizType, |
| 248 | 365 | workTypeId, |
| 249 | 366 | fieldCode: field.fieldCode |
| 250 | 367 | }) |
| 251 | - field.selectOptions = dictRes.map(item => ({ label: item.label, value: item.value })) | |
| 368 | + field.selectOptions = (optRes || []).map(opt => ({ label: opt.label, value: String(opt.value) })) | |
| 252 | 369 | } |
| 253 | 370 | } |
| 254 | - // type6 物料选项 | |
| 255 | - if (field.fieldType === 6) { | |
| 256 | - const matRes = await getMaterialsByCode({ | |
| 257 | - businessType, | |
| 258 | - bizType, | |
| 259 | - workTypeId, | |
| 260 | - fieldCode: field.fieldCode | |
| 371 | + // 数量收集初始化(必须在 selectOptions 装载之后): | |
| 372 | + // 6多选+needQuantity 预建每个选项的键,避免模板 v-model 写到未定义的键上; | |
| 373 | + // 3物料为标量(领用数量),默认 0 = 本次不领用、不扣库存 | |
| 374 | + if (field.fieldType === 6 && this.needQty(field)) { | |
| 375 | + const qty = {} | |
| 376 | + ;(field.selectOptions || []).forEach(opt => { | |
| 377 | + qty[opt.value] = '' | |
| 261 | 378 | }) |
| 262 | - field.materialOptions = matRes.map(item => ({ label: item.materialName, value: item.materialId })) | |
| 379 | + this.qtyMap[field.fieldCode] = qty | |
| 380 | + } | |
| 381 | + if (field.fieldType === 3 && field.optionSource === 3) { | |
| 382 | + this.qtyMap[field.fieldCode] = 0 | |
| 383 | + } | |
| 384 | + // 必填规则 | |
| 385 | + if (field.required === 1) { | |
| 386 | + this.inspectFormRules[field.fieldCode] = [this.buildRequiredRule(field)] | |
| 263 | 387 | } |
| 264 | 388 | } |
| 265 | 389 | this.dynamicFields = fields |
| ... | ... | @@ -331,22 +455,373 @@ export default { |
| 331 | 455 | getImgUrlList(imgList) { |
| 332 | 456 | return imgList.filter(item => item.status === 'success').map(item => item.url) |
| 333 | 457 | }, |
| 458 | + // ========== 动态字段:渲染与取值辅助 ========== | |
| 459 | + /** | |
| 460 | + * 类型6 多选是否要求每项填「使用数量」(ext_config.needQuantity) | |
| 461 | + * 仅记录用量,不写物料三列 ⇒ 结构性不触发扣库存(扣减一律按值行 materialId 非空判定) | |
| 462 | + */ | |
| 463 | + needQty(item: any) { | |
| 464 | + return !!(item.extConfig && item.extConfig.needQuantity === true) | |
| 465 | + }, | |
| 466 | + /** | |
| 467 | + * 有效上限:ext_config.maxCount 的「填 0 表示不限」(PC 端提示语)在提交侧照此生效 | |
| 468 | + * 未配置 → 类型默认值;配 0 → Infinity(不限);配正数 → 该数 | |
| 469 | + */ | |
| 470 | + maxCountOf(item: any, defaultValue: number) { | |
| 471 | + const raw = item.extConfig ? item.extConfig.maxCount : undefined | |
| 472 | + if (raw === undefined || raw === null || raw === '') { | |
| 473 | + return defaultValue | |
| 474 | + } | |
| 475 | + const num = Number(raw) | |
| 476 | + if (isNaN(num)) { | |
| 477 | + return defaultValue | |
| 478 | + } | |
| 479 | + return num > 0 ? num : Infinity | |
| 480 | + }, | |
| 481 | + /** 传给选择器的单次可选数量:上限为「不限」时用组件默认值 52(平台要求 count 是有限数) */ | |
| 482 | + pickCountOf(item: any, defaultValue: number) { | |
| 483 | + const max = this.maxCountOf(item, defaultValue) | |
| 484 | + return max === Infinity ? 52 : max | |
| 485 | + }, | |
| 486 | + /** 单文件大小上限(MB):ext_config.maxSizeMb,未配置时用默认值 */ | |
| 487 | + maxSizeMbOf(item: any, defaultValue: number) { | |
| 488 | + const num = item.extConfig ? Number(item.extConfig.maxSizeMb) : NaN | |
| 489 | + return num > 0 ? num : defaultValue | |
| 490 | + }, | |
| 491 | + /** 类型8 允许的扩展名数组(ext_config.fileType,不含点) */ | |
| 492 | + extensionsOf(item: any) { | |
| 493 | + const list = item.extConfig && item.extConfig.fileType | |
| 494 | + return Array.isArray(list) ? list : [] | |
| 495 | + }, | |
| 496 | + fileButtonText(item: any) { | |
| 497 | + const exts = this.extensionsOf(item) | |
| 498 | + return exts.length ? `选择文件(${exts.join('/')})` : '选择文件' | |
| 499 | + }, | |
| 500 | + /** 从 URL 取末段当文件名(与 PC 端 FieldValueCell 同口径) */ | |
| 501 | + fileNameOf(url: string) { | |
| 502 | + if (!url) return '' | |
| 503 | + const path = String(url).split('?')[0].split('#')[0] | |
| 504 | + const name = path.substring(path.lastIndexOf('/') + 1) | |
| 505 | + try { | |
| 506 | + return decodeURIComponent(name) || url | |
| 507 | + } catch (err) { | |
| 508 | + return name || url | |
| 509 | + } | |
| 510 | + }, | |
| 511 | + /** 类型6 已勾选的选项(用于渲染每个已选项的数量输入行) */ | |
| 512 | + checkedOptions(item: any) { | |
| 513 | + const values = this.inspectForm[item.fieldCode] || [] | |
| 514 | + return (item.selectOptions || []).filter(opt => values.indexOf(opt.value) > -1) | |
| 515 | + }, | |
| 516 | + /** | |
| 517 | + * 多选是否达到上限(用于禁用未选中项)。 | |
| 518 | + * 不用「超限后回写 v-model 截断」:u-checkbox 的勾选态是组件内部状态(只受 checked prop 影响), | |
| 519 | + * 回写父级 v-model 撤不掉已经勾上的项,下一次点击还会按内部状态重新算一遍,等于没限住 | |
| 520 | + */ | |
| 521 | + isOptionLocked(item: any, opt: any) { | |
| 522 | + const max = this.maxCountOf(item, Infinity) | |
| 523 | + if (max === Infinity) { | |
| 524 | + return false | |
| 525 | + } | |
| 526 | + const values = this.inspectForm[item.fieldCode] || [] | |
| 527 | + return values.length >= max && values.indexOf(opt.value) === -1 | |
| 528 | + }, | |
| 529 | + /** 多选的「已选 x/上限」提示,不限时返回空串 */ | |
| 530 | + multiHint(item: any) { | |
| 531 | + const max = this.maxCountOf(item, Infinity) | |
| 532 | + if (max === Infinity) { | |
| 533 | + return '' | |
| 534 | + } | |
| 535 | + return `已选 ${(this.inspectForm[item.fieldCode] || []).length}/${max}` | |
| 536 | + }, | |
| 537 | + onMultiChange(item: any) { | |
| 538 | + // 上限由 isOptionLocked 禁用未选项保证,这里只负责触发必填校验 | |
| 539 | + this.$refs.inspectFormRef.validateField(item.fieldCode) | |
| 540 | + }, | |
| 541 | + /** | |
| 542 | + * 必填规则。 | |
| 543 | + * 7/8/9/11 的值是数组或对象,async-validator 的 required 按默认 string 类型判定, | |
| 544 | + * 会放过「空数组」「null」⇒ 必须用 validator 显式判空,否则未选也能提交 | |
| 545 | + */ | |
| 546 | + buildRequiredRule(field: any) { | |
| 547 | + const type = field.fieldType | |
| 548 | + if (type === 6 || type === 7 || type === 8) { | |
| 549 | + const action = type === 6 ? '请选择' : '请上传' | |
| 550 | + return { | |
| 551 | + required: true, | |
| 552 | + trigger: 'change', | |
| 553 | + validator: (rule, value, callback) => { | |
| 554 | + Array.isArray(value) && value.length > 0 ? callback() : callback(new Error(`${action}${field.fieldName}`)) | |
| 555 | + } | |
| 556 | + } | |
| 557 | + } | |
| 558 | + if (type === 9 || type === 11) { | |
| 559 | + return { | |
| 560 | + required: true, | |
| 561 | + trigger: 'change', | |
| 562 | + validator: (rule, value, callback) => { | |
| 563 | + value ? callback() : callback(new Error(type === 9 ? `请选择${field.fieldName}` : `请选择${field.fieldName}的位置`)) | |
| 564 | + } | |
| 565 | + } | |
| 566 | + } | |
| 567 | + // 1数值/2文本/4日期/5布尔/10链接 为字符串;3枚举/6单选为选择项 | |
| 568 | + const action = type === 3 || type === 5 ? '请选择' : '请填写' | |
| 569 | + return { required: true, message: `${action}${field.fieldName}`, trigger: 'change' } | |
| 570 | + }, | |
| 571 | + /** 幂等流水号(后端按 userId+clientSeq 去重,命中则返回已存在的 recordId) */ | |
| 572 | + genClientSeq() { | |
| 573 | + return `${Date.now()}-${Math.random().toString(16).slice(2, 10)}` | |
| 574 | + }, | |
| 575 | + // ========== 动态字段:7图片/8附件 ========== | |
| 576 | + /** 单文件是否超过 ext_config.maxSizeMb(超限即 toast 并返回 true 以便过滤掉) */ | |
| 577 | + isOversize(file: any, maxSizeMb: number) { | |
| 578 | + if (!maxSizeMb || !file || !file.size) return false | |
| 579 | + const over = file.size / 1024 / 1024 > maxSizeMb | |
| 580 | + if (over) { | |
| 581 | + uni.showToast({ title: `单个文件不能超过${maxSizeMb}MB`, icon: 'none' }) | |
| 582 | + } | |
| 583 | + return over | |
| 584 | + }, | |
| 585 | + /** 把已上传成功的 URL 同步进表单值(提交与校验都读 inspectForm) */ | |
| 586 | + syncFileValue(item: any) { | |
| 587 | + const list = this.fileListMap[item.fieldCode] || [] | |
| 588 | + this.inspectForm[item.fieldCode] = list.filter(file => file.status === 'success').map(file => file.url) | |
| 589 | + }, | |
| 590 | + /** | |
| 591 | + * 7图片 上传(u-upload 的 after-read 回调) | |
| 592 | + */ | |
| 593 | + uploadFieldFiles(item: any, event: any) { | |
| 594 | + const rawFile = event && event.file | |
| 595 | + const fileList = Array.isArray(rawFile) ? rawFile : [rawFile] | |
| 596 | + return this.uploadToField(item, fileList.filter(file => file && file.url).map(file => ({ | |
| 597 | + url: file.url, | |
| 598 | + name: file.name || '', | |
| 599 | + size: file.size | |
| 600 | + }))) | |
| 601 | + }, | |
| 602 | + /** | |
| 603 | + * 8附件 选择并上传。 | |
| 604 | + * 微信小程序没有通用文件选择器,只能从聊天记录里选(uni.chooseMessageFile) | |
| 605 | + */ | |
| 606 | + chooseFieldFiles(item: any) { | |
| 607 | + const maxCount = this.maxCountOf(item, 5) | |
| 608 | + const remain = maxCount - (this.fileListMap[item.fieldCode] || []).length | |
| 609 | + if (remain <= 0) { | |
| 610 | + uni.showToast({ title: `最多只能上传${maxCount}个`, icon: 'none' }) | |
| 611 | + return | |
| 612 | + } | |
| 613 | + const exts = this.extensionsOf(item) | |
| 614 | + const options: any = { count: Math.min(remain, this.pickCountOf(item, 5)), type: 'file' } | |
| 615 | + if (exts.length) { | |
| 616 | + options.extension = exts | |
| 617 | + } | |
| 618 | + uni.chooseMessageFile({ | |
| 619 | + ...options, | |
| 620 | + success: (res) => { | |
| 621 | + const tempFiles = res.tempFiles || [] | |
| 622 | + this.uploadToField(item, tempFiles.map(file => ({ | |
| 623 | + url: file.path, | |
| 624 | + name: file.name || '', | |
| 625 | + size: file.size | |
| 626 | + }))) | |
| 627 | + }, | |
| 628 | + fail: (err) => { | |
| 629 | + console.error('选择附件失败:', err) | |
| 630 | + } | |
| 631 | + }) | |
| 632 | + }, | |
| 633 | + /** | |
| 634 | + * 把选中的本地文件逐个上传并把 http 地址回填到 fileListMap / inspectForm。 | |
| 635 | + * 7图片与8附件共用(uploadImages 是通用 uni.uploadFile,不限图片类型) | |
| 636 | + */ | |
| 637 | + async uploadToField(item: any, files: any[]) { | |
| 638 | + const code = item.fieldCode | |
| 639 | + const maxCount = this.maxCountOf(item, 5) | |
| 640 | + const maxSizeMb = this.maxSizeMbOf(item, 5) | |
| 641 | + const validFiles = (files || []).filter(file => file && file.url && !this.isOversize(file, maxSizeMb)) | |
| 642 | + if (!validFiles.length) { | |
| 643 | + return | |
| 644 | + } | |
| 645 | + const targetList = this.fileListMap[code] || (this.fileListMap[code] = []) | |
| 646 | + if (targetList.length + validFiles.length > maxCount) { | |
| 647 | + uni.showToast({ title: `最多只能上传${maxCount}个`, icon: 'none' }) | |
| 648 | + return | |
| 649 | + } | |
| 650 | + const startIndex = targetList.length | |
| 651 | + targetList.push(...validFiles.map(file => ({ | |
| 652 | + url: file.url, status: 'uploading', message: '上传中', name: file.name || '' | |
| 653 | + }))) | |
| 654 | + try { | |
| 655 | + const uploadResultUrls = await uploadImages({ | |
| 656 | + filePaths: validFiles.map(file => file.url), | |
| 657 | + ignoreError: true | |
| 658 | + }) | |
| 659 | + uploadResultUrls.forEach((url, index) => { | |
| 660 | + if (targetList[startIndex + index]) { | |
| 661 | + targetList.splice(startIndex + index, 1, { | |
| 662 | + url, status: 'success', message: '', name: validFiles[index].name || '' | |
| 663 | + }) | |
| 664 | + } | |
| 665 | + }) | |
| 666 | + for (let i = uploadResultUrls.length; i < validFiles.length; i++) { | |
| 667 | + if (targetList[startIndex + i]) { | |
| 668 | + targetList.splice(startIndex + i, 1, { | |
| 669 | + url: validFiles[i].url, status: 'failed', message: '上传失败', name: validFiles[i].name || '' | |
| 670 | + }) | |
| 671 | + } | |
| 672 | + } | |
| 673 | + if (uploadResultUrls.length < validFiles.length) { | |
| 674 | + uni.showToast({ | |
| 675 | + title: `成功${uploadResultUrls.length}个,失败${validFiles.length - uploadResultUrls.length}个`, | |
| 676 | + icon: 'none' | |
| 677 | + }) | |
| 678 | + } | |
| 679 | + } catch (err) { | |
| 680 | + console.error(`【${code}】文件上传失败:`, err) | |
| 681 | + for (let i = 0; i < validFiles.length; i++) { | |
| 682 | + if (targetList[startIndex + i]) { | |
| 683 | + targetList.splice(startIndex + i, 1, { | |
| 684 | + url: validFiles[i].url, status: 'failed', message: '上传失败', name: validFiles[i].name || '' | |
| 685 | + }) | |
| 686 | + } | |
| 687 | + } | |
| 688 | + uni.showToast({ title: '上传失败,请重试', icon: 'none' }) | |
| 689 | + } | |
| 690 | + this.syncFileValue(item) | |
| 691 | + this.$refs.inspectFormRef.validateField(code) | |
| 692 | + }, | |
| 693 | + /** | |
| 694 | + * 删除 7图片/8附件 的某一个已选文件 | |
| 695 | + */ | |
| 696 | + deleteFieldFile(item: any, event: any) { | |
| 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.validateField(item.fieldCode) | |
| 705 | + }, | |
| 706 | + // ========== 动态字段:组装提交体 ========== | |
| 334 | 707 | /** |
| 335 | - * 提交巡查表单 | |
| 708 | + * 按值格式契约组装 fields[](见 docs/design/app-work-field-api.md §四)。 | |
| 709 | + * 两条硬规则: | |
| 710 | + * ① 空值一律不出现在 fields[] 里(不提交 ''/'[]'/'null') | |
| 711 | + * ② 物料的数量>0 才写 materialId/materialName/quantity —— 非空 materialId 即触发扣库存, | |
| 712 | + * 因此 6多选+needQuantity(只记录用量)绝不能写这三列,只把 quantity 放进 valueText 的 JSON 里 | |
| 713 | + */ | |
| 714 | + buildDynamicFields() { | |
| 715 | + const fields: any[] = [] | |
| 716 | + for (const item of this.dynamicFields) { | |
| 717 | + const code = item.fieldCode | |
| 718 | + const value = this.inspectForm[code] | |
| 719 | + const type = item.fieldType | |
| 720 | + if (type === 9 || type === 11) { | |
| 721 | + if (!value) continue | |
| 722 | + } else if (type === 6 || type === 7 || type === 8) { | |
| 723 | + if (!Array.isArray(value) || value.length === 0) continue | |
| 724 | + } else if (value === '' || value === null || value === undefined) { | |
| 725 | + continue | |
| 726 | + } | |
| 727 | + | |
| 728 | + if (type === 1) { | |
| 729 | + fields.push({ fieldCode: code, valueNumber: Number(value) }) | |
| 730 | + } else if (type === 3 && item.optionSource === 3) { | |
| 731 | + // 物料:value_text 存物料ID;数量>0 才落物料三列(0 表示本次不领用) | |
| 732 | + const qty = Number(this.qtyMap[code] || 0) | |
| 733 | + const matched = (item.selectOptions || []).filter(opt => opt.value === String(value))[0] | |
| 734 | + const row: any = { fieldCode: code, valueText: String(value) } | |
| 735 | + if (qty > 0) { | |
| 736 | + row.materialId = Number(value) | |
| 737 | + row.materialName = matched ? matched.label : '' | |
| 738 | + row.quantity = qty | |
| 739 | + } | |
| 740 | + fields.push(row) | |
| 741 | + } else if (type === 3) { | |
| 742 | + // 3枚举:单选取裸 value;历史「枚举+是否多选」存逗号分隔 | |
| 743 | + fields.push({ | |
| 744 | + fieldCode: code, | |
| 745 | + valueText: item.multiple === 1 ? (value || []).join(',') : 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 键(不臆测成 0),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: any = { 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 | + * ① maintain-plan-commit/create:养护计划本身(描述 + 图片 + 人员),返回 commit.id | |
| 785 | + * ② work-record/submit:动态字段值,bizId 用 ① 的 commit.id(旧接口的 VO 没有动态字段,会被 Jackson 静默丢弃) | |
| 786 | + * 重试安全:commitId 一旦取到就缓存住,重试只补写 ②(② 自身按 clientSeq 幂等),避免重复创建养护记录 | |
| 336 | 787 | */ |
| 337 | 788 | async submitInspect() { |
| 338 | 789 | try { |
| 339 | 790 | await this.$refs.inspectFormRef.validate() |
| 340 | - const submitData = { | |
| 341 | - planNo: this.paramsOptins.planNo, | |
| 342 | - imgHost: "1", | |
| 343 | - beginImg: this.getImgUrlList(this.imagesList), | |
| 344 | - commonUserList: [], | |
| 345 | - remark: this.inspectForm.remark.trim(), | |
| 346 | - ...this.inspectForm | |
| 791 | + const dynamicValues = this.buildDynamicFields() | |
| 792 | + if (!this.clientSeq) { | |
| 793 | + this.clientSeq = this.genClientSeq() | |
| 347 | 794 | } |
| 348 | 795 | uni.showLoading({ title: '提交中...' }) |
| 349 | - await maintainCreate(submitData) | |
| 796 | + // ① 养护计划本体:仅首次提交时写入 | |
| 797 | + if (!this.commitId) { | |
| 798 | + const submitData = { | |
| 799 | + planNo: this.paramsOptins.planNo, | |
| 800 | + imgHost: "1", | |
| 801 | + beginImg: this.getImgUrlList(this.imagesList), | |
| 802 | + commonUserList: [], | |
| 803 | + remark: this.inspectForm.remark.trim(), | |
| 804 | + } | |
| 805 | + const commitRes: any = await maintainCreate(submitData) | |
| 806 | + // 兼容后端返回裸 id 或 {id:...} 两种形态 | |
| 807 | + this.commitId = commitRes && typeof commitRes === 'object' ? commitRes.id : commitRes | |
| 808 | + } | |
| 809 | + // ② 动态字段值:无动态字段时不产生空的字段值记录 | |
| 810 | + if (dynamicValues.length > 0) { | |
| 811 | + if (!this.commitId) { | |
| 812 | + console.warn('未取到养护计划的 commit.id,跳过动态字段提交') | |
| 813 | + } else { | |
| 814 | + await workRecordSubmit({ | |
| 815 | + businessType: this.paramsOptins.businessType, | |
| 816 | + bizType: 3, | |
| 817 | + bizId: this.commitId, | |
| 818 | + workTypeId: Number(this.paramsOptins.planTypeId), | |
| 819 | + clientSeq: this.clientSeq, | |
| 820 | + remark: this.inspectForm.remark.trim(), | |
| 821 | + fields: dynamicValues | |
| 822 | + }) | |
| 823 | + } | |
| 824 | + } | |
| 350 | 825 | uni.hideLoading() |
| 351 | 826 | uni.showToast({ title: '提交成功', icon: 'success', duration: 1000 }) |
| 352 | 827 | setTimeout(() => { |
| ... | ... | @@ -354,8 +829,21 @@ export default { |
| 354 | 829 | }, 1000) |
| 355 | 830 | } catch (error) { |
| 356 | 831 | uni.hideLoading() |
| 357 | - if (!Array.isArray(error)) { | |
| 358 | - console.error('巡查表单提交失败:', error) | |
| 832 | + // 表单校验失败时 error 是字段数组(uview-plus 已提示),不重复弹 | |
| 833 | + if (Array.isArray(error)) { | |
| 834 | + return | |
| 835 | + } | |
| 836 | + console.error('养护记录提交失败:', error) | |
| 837 | + // request.js 已把后端 msg 弹过一次(如「库存不足」),此处不要用通用文案盖掉真实原因 | |
| 838 | + const backendMsg = error && error.msg | |
| 839 | + if (this.commitId) { | |
| 840 | + // 养护计划已落库,失败只发生在耗材字段。必须说清「已提交」,否则用户会以为整单没提交而重复填 | |
| 841 | + uni.showToast({ | |
| 842 | + title: backendMsg ? `养护记录已提交:${backendMsg}` : '养护记录已提交,耗材数据保存失败,请重试', | |
| 843 | + icon: 'none', | |
| 844 | + duration: 2500 | |
| 845 | + }) | |
| 846 | + } else if (!backendMsg) { | |
| 359 | 847 | uni.showToast({ title: '提交失败,请重试', icon: 'none', duration: 2000 }) |
| 360 | 848 | } |
| 361 | 849 | } |
| ... | ... | @@ -406,4 +894,62 @@ export default { |
| 406 | 894 | color: #333; |
| 407 | 895 | } |
| 408 | 896 | } |
| 897 | +// 6多选:复选框组 + needQuantity 时每个已选项一行数量输入 | |
| 898 | +.multi-wrap { | |
| 899 | + width: 100%; | |
| 900 | + .multi-hint { | |
| 901 | + display: block; | |
| 902 | + margin-top: 8rpx; | |
| 903 | + font-size: 24rpx; | |
| 904 | + color: #909399; | |
| 905 | + } | |
| 906 | + .qty-list { | |
| 907 | + margin-top: 16rpx; | |
| 908 | + padding-left: 20rpx; | |
| 909 | + border-left: 4rpx solid #f0f0f0; | |
| 910 | + } | |
| 911 | +} | |
| 912 | +// 数量行(6多选的每个已选项 / 3物料的领用数量共用) | |
| 913 | +.qty-row { | |
| 914 | + display: flex; | |
| 915 | + align-items: center; | |
| 916 | + justify-content: space-between; | |
| 917 | + gap: 16rpx; | |
| 918 | + padding: 8rpx 0; | |
| 919 | + .qty-label { | |
| 920 | + flex: 1; | |
| 921 | + min-width: 0; | |
| 922 | + font-size: 28rpx; | |
| 923 | + color: #333; | |
| 924 | + } | |
| 925 | +} | |
| 926 | +.qty-row-material { | |
| 927 | + margin-top: 12rpx; | |
| 928 | + padding-top: 12rpx; | |
| 929 | + border-top: 1rpx solid #f0f0f0; | |
| 930 | +} | |
| 931 | +// 8附件:自绘文件列表 + 选择按钮(微信无通用文件选择器,走 chooseMessageFile) | |
| 932 | +.file-wrap { | |
| 933 | + width: 100%; | |
| 934 | + .file-row { | |
| 935 | + display: flex; | |
| 936 | + align-items: center; | |
| 937 | + justify-content: space-between; | |
| 938 | + gap: 16rpx; | |
| 939 | + padding: 8rpx 0; | |
| 940 | + .file-name { | |
| 941 | + flex: 1; | |
| 942 | + min-width: 0; | |
| 943 | + font-size: 28rpx; | |
| 944 | + color: #333; | |
| 945 | + overflow: hidden; | |
| 946 | + white-space: nowrap; | |
| 947 | + text-overflow: ellipsis; | |
| 948 | + } | |
| 949 | + .file-del { | |
| 950 | + font-size: 26rpx; | |
| 951 | + color: #ff4d4f; | |
| 952 | + } | |
| 953 | + } | |
| 954 | +} | |
| 409 | 955 | </style> | ... | ... |
pages-sub/daily/quick-order/add-order.vue
| ... | ... | @@ -180,25 +180,16 @@ import { getRoadListByLatLng } from '@/api/common' |
| 180 | 180 | import { uploadImages } from '@/common/utils/upload'; |
| 181 | 181 | import { createQuick } from '@/api/quick-order/quick-order' |
| 182 | 182 | import { useUserStore } from '@/pinia/user'; |
| 183 | +import { getDictLabel, getDictValue } from '@/common/utils/dict'; | |
| 184 | + | |
| 185 | +// 业务线中文名/编码统一取自数据字典 business_line(系统管理 → 字典管理),不再硬编码 | |
| 186 | +const BUSI_LINE_DICT_TYPE = 'business_line'; | |
| 187 | +// 中文判定:用于区分「传进来的是中文名」还是「传进来的是编码」 | |
| 188 | +const CJK_RE = /[一-龥]/; | |
| 183 | 189 | |
| 184 | 190 | export default { |
| 185 | 191 | data() { |
| 186 | 192 | return { |
| 187 | - // 业务线相关 | |
| 188 | - busiLineMap: { | |
| 189 | - 'yl': '园林', | |
| 190 | - 'sz': '市政', | |
| 191 | - 'wy': '物业', | |
| 192 | - 'zx': '秩序管理', | |
| 193 | - 'hj': '环境卫生', | |
| 194 | - 'yw': '常规运维', | |
| 195 | - '园林': 'yl', | |
| 196 | - '市政': 'sz', | |
| 197 | - '物业': 'wy', | |
| 198 | - '秩序管理': 'zx', | |
| 199 | - '环境卫生': 'hj', | |
| 200 | - '常规运维' : 'yw' | |
| 201 | - }, | |
| 202 | 193 | busiLineOptions: [], // 业务线选项列表 |
| 203 | 194 | // 问题照片列表 |
| 204 | 195 | problemImgsList: [], |
| ... | ... | @@ -292,14 +283,25 @@ export default { |
| 292 | 283 | const rawBusiLines = userStore.userInfo.user.busiLine.split(','); |
| 293 | 284 | // 生成中文名称列表 |
| 294 | 285 | this.busiLineOptions = rawBusiLines.map(item => ({ |
| 295 | - name: this.busiLineMap[item.trim()] // name=中文名称(园林/市政/物业) | |
| 286 | + name: this.getBusiLineCnByEn(item.trim()) // name=中文名称(园林/市政/物业) | |
| 296 | 287 | })); |
| 297 | 288 | } |
| 298 | 289 | console.log('业务线选项:', this.busiLineOptions); |
| 299 | 290 | }, |
| 291 | + // 工具方法:编码 → 中文名称 | |
| 292 | + // 字典未拉到时(登录时字典请求是容错的)兜底显示原始编码,保证选项不为空、页面仍可用 | |
| 293 | + getBusiLineCnByEn(busiLineEn: string) { | |
| 294 | + if (!busiLineEn) return ''; | |
| 295 | + return getDictLabel(BUSI_LINE_DICT_TYPE, busiLineEn) || busiLineEn; | |
| 296 | + }, | |
| 300 | 297 | // 工具方法:通过中文名称获取对应的英文标识 |
| 301 | - getBusiLineEnByCn(cnName) { | |
| 302 | - return this.busiLineMap[cnName] || ''; | |
| 298 | + // 注:字典未拉到时选项里放的就是原始编码,故对「非中文」入参原样返回; | |
| 299 | + // 中文名解析不到仍返回空串,沿用调用方原有的异常提示 | |
| 300 | + getBusiLineEnByCn(cnName: string) { | |
| 301 | + if (!cnName) return ''; | |
| 302 | + const code = getDictValue(BUSI_LINE_DICT_TYPE, cnName); | |
| 303 | + if (code) return code; | |
| 304 | + return CJK_RE.test(cnName) ? '' : cnName; | |
| 303 | 305 | }, |
| 304 | 306 | // 业务线切换事件 |
| 305 | 307 | handleBusiLineChange() { | ... | ... |
pages-sub/problem/regional-order-manage/add-order.vue
| ... | ... | @@ -238,16 +238,15 @@ import { |
| 238 | 238 | import { timeFormat } from '@/uni_modules/uview-plus' |
| 239 | 239 | import { nextStepMap } from '@/common/utils/common' |
| 240 | 240 | import { useUserStore } from '@/pinia/user'; |
| 241 | +import { getDictLabel, getDictValue } from '@/common/utils/dict'; | |
| 241 | 242 | |
| 242 | 243 | // ========== 常量配置区 ========== |
| 243 | 244 | const CONST = { |
| 244 | 245 | DICT_PRESSING_TYPE: 'workorder_pressing_type', |
| 246 | + // 业务线的中文名/编码统一取自数据字典(系统管理 → 字典管理),不再硬编码 | |
| 247 | + DICT_BUSI_LINE: 'business_line', | |
| 245 | 248 | UPLOAD_CONFIG: { maxCount: 3, uploadText: '选择问题照片', sizeType: ['compressed'] }, |
| 246 | 249 | PAGE_ORDER_LIST: '/pages-sub/problem/regional-order-manage/index', |
| 247 | - BUSI_LINE_MAP: { | |
| 248 | - yl: '园林', sz: '市政', wy: '物业', '园林': 'yl', '市政': 'sz', '物业': 'wy', | |
| 249 | - 'zx': '秩序管理', 'hj': '环境卫生', '秩序管理': 'zx', '环境卫生': 'hj' | |
| 250 | - }, | |
| 251 | 250 | ROLE: { MANAGER: 'regional_manager', INSPECTOR: 'Inspector_global', PATROL: 'patrol_global' , LEADER: 'Inspector_global_leader'} |
| 252 | 251 | } |
| 253 | 252 | |
| ... | ... | @@ -323,8 +322,19 @@ const workOrderFormRules = reactive({ |
| 323 | 322 | }) |
| 324 | 323 | |
| 325 | 324 | // ========== 工具方法 ========== |
| 326 | -const busiLineCnToEn = (cn) => CONST.BUSI_LINE_MAP[cn] || ''; | |
| 327 | -const busiLineEnToCn = (en) => CONST.BUSI_LINE_MAP[en] || ''; | |
| 325 | +// 中文名 → 编码。注:字典未拉到时(登录时字典请求是容错的)选项里放的就是原始编码, | |
| 326 | +// 故对「非中文」入参原样返回;中文名解析不到仍返回空串,沿用调用方原有的异常提示 | |
| 327 | +const busiLineCnToEn = (cn) => { | |
| 328 | + if (!cn) return ''; | |
| 329 | + const code = getDictValue(CONST.DICT_BUSI_LINE, cn); | |
| 330 | + if (code) return code; | |
| 331 | + return /[一-龥]/.test(cn) ? '' : cn; | |
| 332 | +}; | |
| 333 | +// 编码 → 中文名。字典未拉到时兜底显示原始编码,保证选项不为空、页面仍可用 | |
| 334 | +const busiLineEnToCn = (en) => { | |
| 335 | + if (!en) return ''; | |
| 336 | + return getDictLabel(CONST.DICT_BUSI_LINE, en) || en; | |
| 337 | +}; | |
| 328 | 338 | const toOrderListPage = () => uni.reLaunch({ url: CONST.PAGE_ORDER_LIST }); |
| 329 | 339 | const formatDate = (dateVal, fmt = 'yyyy-mm-dd hh:MM:ss') => dateVal ? timeFormat(dateVal, fmt) : ''; |
| 330 | 340 | const dateToTimestamp = (dateStr) => dateStr ? new Date(dateStr).getTime() : ''; | ... | ... |
pages-sub/problem/regional-order-manage/add-patrol-order.vue
| ... | ... | @@ -57,26 +57,16 @@ import { qyUniversalApproval, qyWorkorderCreate } from '@/api/regional-order-man |
| 57 | 57 | import { timeFormat } from '@/uni_modules/uview-plus' |
| 58 | 58 | import { nextStepMap } from '@/common/utils/common' |
| 59 | 59 | import { useUserStore } from '@/pinia/user'; |
| 60 | +import { getDictLabel, getDictValue } from '@/common/utils/dict'; | |
| 60 | 61 | |
| 61 | 62 | // ========== 状态管理 ========== |
| 62 | 63 | const userStore = useUserStore(); |
| 63 | 64 | |
| 64 | 65 | // ========== 业务线相关状态 ========== |
| 65 | -// 业务线映射表 | |
| 66 | -const busiLineMap = ref({ | |
| 67 | - 'yl': '园林', | |
| 68 | - 'sz': '市政', | |
| 69 | - 'wy': '物业', | |
| 70 | - 'zx': '秩序管理', | |
| 71 | - 'hj': '环境卫生', | |
| 72 | - 'yw': '常规运维', | |
| 73 | - '园林': 'yl', | |
| 74 | - '市政': 'sz', | |
| 75 | - '物业': 'wy', | |
| 76 | - '秩序管理': 'zx', | |
| 77 | - '环境卫生': 'hj', | |
| 78 | - '常规运维': 'yw' | |
| 79 | -}); | |
| 66 | +// 业务线中文名/编码统一取自数据字典 business_line(系统管理 → 字典管理),不再硬编码 | |
| 67 | +const BUSI_LINE_DICT_TYPE = 'business_line'; | |
| 68 | +// 中文判定:用于区分「传进来的是中文名」还是「传进来的是编码」 | |
| 69 | +const CJK_RE = /[一-龥]/; | |
| 80 | 70 | |
| 81 | 71 | // 业务线选项列表 |
| 82 | 72 | const busiLineOptions = ref([]); |
| ... | ... | @@ -87,13 +77,25 @@ const formatBusiLineOptions = () => { |
| 87 | 77 | } |
| 88 | 78 | const rawBusiLines = userStore.userInfo.user.busiLine.split(','); |
| 89 | 79 | busiLineOptions.value = rawBusiLines.map(item => ({ |
| 90 | - name: busiLineMap.value[item.trim()] | |
| 80 | + name: getBusiLineCnByEn(item.trim()) | |
| 91 | 81 | })); |
| 92 | 82 | }; |
| 93 | 83 | |
| 84 | +// 工具方法:编码 → 中文名称 | |
| 85 | +// 字典未拉到时(登录时字典请求是容错的)兜底显示原始编码,保证选项不为空、页面仍可用 | |
| 86 | +const getBusiLineCnByEn = (busiLineEn) => { | |
| 87 | + if (!busiLineEn) return ''; | |
| 88 | + return getDictLabel(BUSI_LINE_DICT_TYPE, busiLineEn) || busiLineEn; | |
| 89 | +}; | |
| 90 | + | |
| 94 | 91 | // 工具方法:通过中文名称获取对应的英文标识 |
| 92 | +// 注:字典未拉到时选项里放的就是原始编码,故对「非中文」入参原样返回; | |
| 93 | +// 中文名解析不到仍返回空串,沿用调用方原有的「业务线选择异常」校验 | |
| 95 | 94 | const getBusiLineEnByCn = (cnName) => { |
| 96 | - return busiLineMap.value[cnName] || ''; | |
| 95 | + if (!cnName) return ''; | |
| 96 | + const code = getDictValue(BUSI_LINE_DICT_TYPE, cnName); | |
| 97 | + if (code) return code; | |
| 98 | + return CJK_RE.test(cnName) ? '' : cnName; | |
| 97 | 99 | }; |
| 98 | 100 | |
| 99 | 101 | // ========== 表单Ref ========== | ... | ... |
pages-sub/problem/regional-order-manage/distribution-order.vue
| ... | ... | @@ -96,26 +96,16 @@ import { daquUniversalApproval, daquWorkorderCreate, qyUniversalApproval } from |
| 96 | 96 | import { timeFormat } from '@/uni_modules/uview-plus' |
| 97 | 97 | import { nextStepMap } from '@/common/utils/common' |
| 98 | 98 | import { useUserStore } from '@/pinia/user'; |
| 99 | +import { getDictLabel, getDictValue } from '@/common/utils/dict'; | |
| 99 | 100 | |
| 100 | 101 | // ========== 状态管理 ========== |
| 101 | 102 | const userStore = useUserStore(); |
| 102 | 103 | |
| 103 | 104 | // ========== 业务线相关状态 ========== |
| 104 | -// 业务线映射表 | |
| 105 | -const busiLineMap = ref({ | |
| 106 | - 'yl': '园林', | |
| 107 | - 'sz': '市政', | |
| 108 | - 'wy': '物业', | |
| 109 | - 'zx': '秩序管理', | |
| 110 | - 'hj': '环境卫生', | |
| 111 | - 'yw': '常规运维', | |
| 112 | - '园林': 'yl', | |
| 113 | - '市政': 'sz', | |
| 114 | - '物业': 'wy', | |
| 115 | - '秩序管理': 'zx', | |
| 116 | - '环境卫生': 'hj', | |
| 117 | - '常规运维': 'yw' | |
| 118 | -}); | |
| 105 | +// 业务线中文名/编码统一取自数据字典 business_line(系统管理 → 字典管理),不再硬编码 | |
| 106 | +const BUSI_LINE_DICT_TYPE = 'business_line'; | |
| 107 | +// 中文判定:用于区分「传进来的是中文名」还是「传进来的是编码」 | |
| 108 | +const CJK_RE = /[一-龥]/; | |
| 119 | 109 | |
| 120 | 110 | // 业务线选项列表 |
| 121 | 111 | const busiLineOptions = ref([]); |
| ... | ... | @@ -126,13 +116,25 @@ const formatBusiLineOptions = () => { |
| 126 | 116 | } |
| 127 | 117 | const rawBusiLines = userStore.userInfo.user.busiLine.split(','); |
| 128 | 118 | busiLineOptions.value = rawBusiLines.map(item => ({ |
| 129 | - name: busiLineMap.value[item.trim()] | |
| 119 | + name: getBusiLineCnByEn(item.trim()) | |
| 130 | 120 | })); |
| 131 | 121 | }; |
| 132 | 122 | |
| 123 | +// 工具方法:编码 → 中文名称 | |
| 124 | +// 字典未拉到时(登录时字典请求是容错的)兜底显示原始编码,保证选项不为空、页面仍可用 | |
| 125 | +const getBusiLineCnByEn = (busiLineEn) => { | |
| 126 | + if (!busiLineEn) return ''; | |
| 127 | + return getDictLabel(BUSI_LINE_DICT_TYPE, busiLineEn) || busiLineEn; | |
| 128 | +}; | |
| 129 | + | |
| 133 | 130 | // 工具方法:通过中文名称获取对应的英文标识 |
| 131 | +// 注:字典未拉到时选项里放的就是原始编码,故对「非中文」入参原样返回; | |
| 132 | +// 中文名解析不到仍返回空串,沿用调用方原有的「业务线选择异常」校验 | |
| 134 | 133 | const getBusiLineEnByCn = (cnName) => { |
| 135 | - return busiLineMap.value[cnName] || ''; | |
| 134 | + if (!cnName) return ''; | |
| 135 | + const code = getDictValue(BUSI_LINE_DICT_TYPE, cnName); | |
| 136 | + if (code) return code; | |
| 137 | + return CJK_RE.test(cnName) ? '' : cnName; | |
| 136 | 138 | }; |
| 137 | 139 | |
| 138 | 140 | // ========== 表单Ref ========== |
| ... | ... | @@ -270,7 +272,7 @@ onBackPress(() => { |
| 270 | 272 | const echoOrderData = (orderItem) => { |
| 271 | 273 | // 回显业务线 |
| 272 | 274 | if (orderItem.busiLine) { |
| 273 | - workOrderForm.busiLineCn = busiLineMap.value[orderItem.busiLine]; | |
| 275 | + workOrderForm.busiLineCn = getBusiLineCnByEn(orderItem.busiLine); | |
| 274 | 276 | } |
| 275 | 277 | |
| 276 | 278 | // 回显基础字段【核心:工单位置/问题类型/情况描述 赋值】 | ... | ... |
pages-sub/problem/work-order-manage/add-order.vue
| ... | ... | @@ -100,26 +100,16 @@ import { universalApproval, workorderCreate } from '@/api/work-order-manage/work |
| 100 | 100 | import { timeFormat } from '@/uni_modules/uview-plus' |
| 101 | 101 | import { nextStepMap } from '@/common/utils/common' |
| 102 | 102 | import { useUserStore } from '@/pinia/user'; |
| 103 | +import { getDictLabel, getDictValue } from '@/common/utils/dict'; | |
| 103 | 104 | |
| 104 | 105 | // ========== 状态管理 ========== |
| 105 | 106 | const userStore = useUserStore(); |
| 106 | 107 | |
| 107 | 108 | // ========== 业务线相关状态(核心改造:已整合到workOrderForm中,删除冗余selectedBusiLine) ========== |
| 108 | -// 业务线映射表(键:英文标识,值:中文名称;新增反向映射:中文->英文) | |
| 109 | -const busiLineMap = ref({ | |
| 110 | - 'yl': '园林', | |
| 111 | - 'sz': '市政', | |
| 112 | - 'wy': '物业', | |
| 113 | - 'zx': '秩序管理', | |
| 114 | - 'hj': '环境卫生', | |
| 115 | - 'yw': '常规运维', | |
| 116 | - '园林': 'yl', | |
| 117 | - '市政': 'sz', | |
| 118 | - '物业': 'wy', | |
| 119 | - '秩序管理': 'zx', | |
| 120 | - '环境卫生': 'hj', | |
| 121 | - '常规运维': 'yw' | |
| 122 | -}); | |
| 109 | +// 业务线中文名/编码统一取自数据字典 business_line(系统管理 → 字典管理),不再硬编码 | |
| 110 | +const BUSI_LINE_DICT_TYPE = 'business_line'; | |
| 111 | +// 中文判定:用于区分「传进来的是中文名」还是「传进来的是编码」 | |
| 112 | +const CJK_RE = /[一-龥]/; | |
| 123 | 113 | |
| 124 | 114 | // 业务线选项列表(仅保留name字段=中文名称,适配单选框配置) |
| 125 | 115 | const busiLineOptions = ref([]); |
| ... | ... | @@ -133,14 +123,27 @@ const formatBusiLineOptions = () => { |
| 133 | 123 | console.log(rawBusiLines) |
| 134 | 124 | // 核心改造:仅生成name字段(值为中文名称),适配单选框:label="item.name" |
| 135 | 125 | busiLineOptions.value = rawBusiLines.map(item => ({ |
| 136 | - name: busiLineMap.value[item.trim()] // name=中文名称(园林/市政/物业) | |
| 126 | + name: getBusiLineCnByEn(item.trim()) // name=中文名称(园林/市政/物业) | |
| 137 | 127 | })); |
| 138 | 128 | console.log(busiLineOptions.value) |
| 139 | 129 | }; |
| 140 | 130 | |
| 131 | +// ========== 工具方法 ========== | |
| 132 | +// 编码 → 中文名称。字典未拉到时(登录时字典请求是容错的)兜底显示原始编码, | |
| 133 | +// 保证选项不为空、页面仍可用 | |
| 134 | +const getBusiLineCnByEn = (busiLineEn) => { | |
| 135 | + if (!busiLineEn) return ''; | |
| 136 | + return getDictLabel(BUSI_LINE_DICT_TYPE, busiLineEn) || busiLineEn; | |
| 137 | +}; | |
| 138 | + | |
| 141 | 139 | // ========== 工具方法:通过中文名称获取对应的英文标识 ========== |
| 140 | +// 注:字典未拉到时选项里放的就是原始编码,故对「非中文」入参原样返回; | |
| 141 | +// 中文名解析不到仍返回空串,沿用调用方原有的异常提示 | |
| 142 | 142 | const getBusiLineEnByCn = (cnName) => { |
| 143 | - return busiLineMap.value[cnName] || ''; | |
| 143 | + if (!cnName) return ''; | |
| 144 | + const code = getDictValue(BUSI_LINE_DICT_TYPE, cnName); | |
| 145 | + if (code) return code; | |
| 146 | + return CJK_RE.test(cnName) ? '' : cnName; | |
| 144 | 147 | }; |
| 145 | 148 | |
| 146 | 149 | // ========== 表单Ref ========== |
| ... | ... | @@ -290,7 +293,7 @@ onShow(() => { |
| 290 | 293 | const echoOrderData = (orderItem) => { |
| 291 | 294 | // 1. 回显业务线:将工单的英文busiLine转为中文名称,赋值给workOrderForm.busiLineCn(核心调整) |
| 292 | 295 | if (orderItem.busiLine) { |
| 293 | - workOrderForm.busiLineCn = busiLineMap.value[orderItem.busiLine]; // 英文->中文(如wy->物业) | |
| 296 | + workOrderForm.busiLineCn = getBusiLineCnByEn(orderItem.busiLine); // 英文->中文(如wy->物业) | |
| 294 | 297 | } |
| 295 | 298 | |
| 296 | 299 | // 2. 原有基础表单字段回显 | ... | ... |