Commit ee792d5205b848320029c7f45274dc39688bf9dd
1 parent
50ad810a
动态表单
Showing
6 changed files
with
861 additions
and
711 deletions
api/common.js
| @@ -71,7 +71,7 @@ export const getOptionsByCode = (params) => { | @@ -71,7 +71,7 @@ export const getOptionsByCode = (params) => { | ||
| 71 | * @param {Object} params {} | 71 | * @param {Object} params {} |
| 72 | * @returns {Promise} | 72 | * @returns {Promise} |
| 73 | */ | 73 | */ |
| 74 | -export const getMaterialsByCode = (params) => { | 74 | +export const getMaterialByCode = (params) => { |
| 75 | return get('/app-api/app/garden/work-field-config/materials', params); | 75 | return get('/app-api/app/garden/work-field-config/materials', params); |
| 76 | }; | 76 | }; |
| 77 | 77 | ||
| @@ -93,3 +93,6 @@ export const workRecordSubmit = (data) => { | @@ -93,3 +93,6 @@ export const workRecordSubmit = (data) => { | ||
| 93 | 93 | ||
| 94 | 94 | ||
| 95 | 95 | ||
| 96 | + | ||
| 97 | + | ||
| 98 | + |
common/utils/request.js
| @@ -7,7 +7,10 @@ const request = (options) => { | @@ -7,7 +7,10 @@ const request = (options) => { | ||
| 7 | url: '', | 7 | url: '', |
| 8 | method: 'GET', | 8 | method: 'GET', |
| 9 | data: {}, | 9 | data: {}, |
| 10 | - header: { 'Content-Type': 'application/json' }, | 10 | + header: { |
| 11 | + 'Content-Type': 'application/json', | ||
| 12 | + 'tenant-id':'1' | ||
| 13 | + }, | ||
| 11 | timeout: globalConfig.api.timeout | 14 | timeout: globalConfig.api.timeout |
| 12 | }; | 15 | }; |
| 13 | 16 |
pages-sub/daily/maintain-manage/add-record.vue
| @@ -4,233 +4,541 @@ | @@ -4,233 +4,541 @@ | ||
| 4 | <up-form label-position="left" :model="inspectForm" ref="inspectFormRef" label-width="140rpx"> | 4 | <up-form label-position="left" :model="inspectForm" ref="inspectFormRef" label-width="140rpx"> |
| 5 | <!-- ========== 动态表单区域 ========== --> | 5 | <!-- ========== 动态表单区域 ========== --> |
| 6 | <template v-for="item in dynamicFields" :key="item.fieldCode"> | 6 | <template v-for="item in dynamicFields" :key="item.fieldCode"> |
| 7 | - <up-form-item :label="item.fieldName" :prop="item.fieldCode" :required="item.required === 1" class="form-item" | ||
| 8 | - @click="openPickerIfNeeded(item)"> | ||
| 9 | - <!-- type 1 数值:u-number-box,最多2位小数,min=0,max取item.length --> | 7 | + <up-form-item |
| 8 | + :label="item.fieldName" | ||
| 9 | + :prop="item.fieldCode" | ||
| 10 | + :required="item.required === 1" | ||
| 11 | + class="form-item" | ||
| 12 | + @click="openPickerIfNeeded(item)" | ||
| 13 | + > | ||
| 14 | + <!-- type1 数值:u-number-box --> | ||
| 10 | <view v-if="item.fieldType === 1" class="number-box-wrap"> | 15 | <view v-if="item.fieldType === 1" class="number-box-wrap"> |
| 11 | - <u-number-box v-model="inspectForm[item.fieldCode]" min="1" :step="1" | 16 | + <u-number-box |
| 17 | + v-model="inspectForm[item.fieldCode]" | ||
| 18 | + min="1" | ||
| 19 | + step="1" | ||
| 12 | :max="item.length ? 10 ** Number(item.length) - 1 : null" | 20 | :max="item.length ? 10 ** Number(item.length) - 1 : null" |
| 13 | - :placeholder="`请输入${item.fieldName}${item.unit} `" @change="valChange(item)" class="large-number-box"> | ||
| 14 | - </u-number-box> | 21 | + :placeholder="`请输入${item.fieldName}${item.unit ? item.unit : ''}`" |
| 22 | + @change="valChange(item)" | ||
| 23 | + class="large-number-box" | ||
| 24 | + ></u-number-box> | ||
| 15 | <text class="unit-text" v-if="item.unit">{{ item.unit }}</text> | 25 | <text class="unit-text" v-if="item.unit">{{ item.unit }}</text> |
| 16 | </view> | 26 | </view> |
| 17 | - <!-- type 2 文本 --> | ||
| 18 | - <up-input v-else-if="item.fieldType === 2" v-model="inspectForm[item.fieldCode]" | ||
| 19 | - :maxlength="item.length || 50" :placeholder="`请输入${item.fieldName}`"></up-input> | ||
| 20 | - <!-- 分层选项(3枚举/6多选,且选项带 path):统一只读入口,点开分组弹层选择。 | ||
| 21 | - 必须放在下面的 3/6 分支之前——v-else-if 短路,放后面就永远进不来 --> | ||
| 22 | - <up-input v-else-if="isGroupedField(item)" v-model="labelMap[item.fieldCode]" readonly | ||
| 23 | - disabled-color="#ffffff" :placeholder="`请选择${item.fieldName}`" border="none"> | ||
| 24 | - <template #right> | ||
| 25 | - <up-icon name="arrow-right" size="16"></up-icon> | ||
| 26 | - </template> | ||
| 27 | - </up-input> | ||
| 28 | - <!-- type3 枚举:多选 multiple=1 使用复选框组(name 必须是 value,提交按逗号拼接 value,与值格式契约一致) --> | ||
| 29 | - <u-checkbox-group v-else-if="item.fieldType === 3 && item.multiple === 1" | ||
| 30 | - v-model="inspectForm[item.fieldCode]"> | ||
| 31 | - <u-checkbox v-for="opt in item.selectOptions" :key="opt.value" :label="opt.label" :name="opt.value"> | ||
| 32 | 27 | ||
| 33 | - </u-checkbox> | 28 | + <!-- type2 文本 --> |
| 29 | + <up-input | ||
| 30 | + v-else-if="item.fieldType === 2" | ||
| 31 | + v-model="inspectForm[item.fieldCode]" | ||
| 32 | + :maxlength="item.length || 50" | ||
| 33 | + :placeholder="`请输入${item.fieldName}`" | ||
| 34 | + ></up-input> | ||
| 35 | + | ||
| 36 | + <!-- type3 枚举多选 multiple=1 --> | ||
| 37 | + <u-checkbox-group v-else-if="item.fieldType === 3 && item.multiple === 1" v-model="inspectForm[item.fieldCode]"> | ||
| 38 | + <u-checkbox | ||
| 39 | + v-for="opt in item.selectOptions" | ||
| 40 | + :key="opt.value" | ||
| 41 | + :label="opt.label" | ||
| 42 | + :name="opt.value" | ||
| 43 | + ></u-checkbox> | ||
| 34 | </u-checkbox-group> | 44 | </u-checkbox-group> |
| 35 | - <!-- type3 枚举:单选 用up-action-sheet 底部选择 --> | ||
| 36 | - <up-input v-else-if="item.fieldType === 3" v-model="labelMap[item.fieldCode]" readonly | ||
| 37 | - disabled-color="#ffffff" :placeholder="`请选择${item.fieldName}`" border="none"> | 45 | + |
| 46 | + <!-- type3 枚举单选 multiple=0 --> | ||
| 47 | + <up-input | ||
| 48 | + v-else-if="item.fieldType === 3" | ||
| 49 | + v-model="labelMap[item.fieldCode]" | ||
| 50 | + readonly | ||
| 51 | + disabled-color="#ffffff" | ||
| 52 | + :placeholder="`请选择${item.fieldName}`" | ||
| 53 | + border="none" | ||
| 54 | + > | ||
| 38 | <template #right> | 55 | <template #right> |
| 39 | <up-icon name="arrow-right" size="16"></up-icon> | 56 | <up-icon name="arrow-right" size="16"></up-icon> |
| 40 | </template> | 57 | </template> |
| 41 | </up-input> | 58 | </up-input> |
| 59 | + | ||
| 42 | <!-- type4 日期时间 --> | 60 | <!-- type4 日期时间 --> |
| 43 | <view v-else-if="item.fieldType === 4" @click.stop="openDatePicker(item)" class="date-input-wrap"> | 61 | <view v-else-if="item.fieldType === 4" @click.stop="openDatePicker(item)" class="date-input-wrap"> |
| 44 | - <up-input v-model="inspectForm[item.fieldCode]" disabled placeholder="点击选择时间"></up-input> | 62 | + <up-input |
| 63 | + v-model="inspectForm[item.fieldCode]" | ||
| 64 | + disabled | ||
| 65 | + placeholder="点击选择时间" | ||
| 66 | + border="none" | ||
| 67 | + ></up-input> | ||
| 45 | </view> | 68 | </view> |
| 46 | - <!-- type5 布尔单选 是/否 【重点修复】增加空label=""占位项 --> | ||
| 47 | - <up-radio-group v-else-if="item.fieldType === 5" v-model="inspectForm[item.fieldCode]"> | ||
| 48 | - <up-radio label="是" name="是"></up-radio> | ||
| 49 | - <up-radio label="否" name="否"></up-radio> | ||
| 50 | - </up-radio-group> | ||
| 51 | - <!-- type6 多选:复选框组;选项来源可为静态/字典/物料/数据表/SQL/接口(物料不再占用 6 号) --> | ||
| 52 | - <view v-else-if="item.fieldType === 6" class="multi-wrap"> | ||
| 53 | - <u-checkbox-group v-model="inspectForm[item.fieldCode]" @change="onMultiChange(item)"> | ||
| 54 | - <u-checkbox v-for="opt in item.selectOptions" :key="opt.value" :label="opt.label" :name="opt.value" | ||
| 55 | - :disabled="isOptionLocked(item, opt)"> | ||
| 56 | - </u-checkbox> | ||
| 57 | - </u-checkbox-group> | ||
| 58 | - <!-- 有限上限时提示已选数。上限靠「禁用未选中项」实现:u-checkbox 的勾选态在组件内部,回写 v-model 撤不掉 --> | ||
| 59 | - <text v-if="multiHint(item)" class="multi-hint">{{ multiHint(item) }}</text> | ||
| 60 | - <!-- needQuantity:已选项各配一个数量输入,值按 {value,label,quantity} 提交。此形态仅记录用量、不扣库存 --> | ||
| 61 | - <view v-if="needQty(item)" class="qty-list"> | ||
| 62 | - <view v-for="opt in checkedOptions(item)" :key="opt.value" class="qty-row"> | ||
| 63 | - <text class="qty-label">{{ opt.label }}</text> | ||
| 64 | - <u-number-box v-model="qtyMap[item.fieldCode][opt.value]" :min="0" :step="1"></u-number-box> | ||
| 65 | - </view> | ||
| 66 | - </view> | ||
| 67 | - </view> | ||
| 68 | - <!-- type7 图片:多张上传,提交 JSON 数组(URL) --> | ||
| 69 | - <up-upload v-else-if="item.fieldType === 7" :file-list="fileListMap[item.fieldCode] || []" | ||
| 70 | - @after-read="(event) => uploadFieldFiles(item, event)" @delete="(event) => deleteFieldFile(item, event)" | ||
| 71 | - multiple :max-count="pickCountOf(item, 5)" upload-text="选择图片" del-color="#ff4d4f" | ||
| 72 | - class="upload-wrap"></up-upload> | ||
| 73 | - <!-- type8 附件:微信只能从聊天记录选文件(无通用文件选择器),提交 JSON 数组(URL) --> | 69 | + |
| 70 | + <!-- type5 布尔单选 是/否 --> | ||
| 71 | + <u-radio-group v-else-if="item.fieldType === 5" v-model="inspectForm[item.fieldCode]"> | ||
| 72 | + <u-radio label="是" name="是"></u-radio> | ||
| 73 | + <u-radio label="否" name="否"></u-radio> | ||
| 74 | + </u-radio-group> | ||
| 75 | + | ||
| 76 | + <!-- ===================== type6 弹窗选择 ===================== --> | ||
| 77 | + <up-input | ||
| 78 | + v-else-if="item.fieldType === 6" | ||
| 79 | + :model-value="getType6ShowLabel(item)" | ||
| 80 | + readonly | ||
| 81 | + disabled-color="#ffffff" | ||
| 82 | + :placeholder="`请选择${item.fieldName}`" | ||
| 83 | + border="none" | ||
| 84 | + > | ||
| 85 | + <template #right> | ||
| 86 | + <up-icon name="arrow-right" size="16"></up-icon> | ||
| 87 | + </template> | ||
| 88 | + </up-input> | ||
| 89 | + | ||
| 90 | + <!-- type7 图片上传 --> | ||
| 91 | + <up-upload | ||
| 92 | + v-else-if="item.fieldType === 7" | ||
| 93 | + :file-list="fileListMap[item.fieldCode] || []" | ||
| 94 | + @after-read="(event) => uploadFieldFiles(item, event)" | ||
| 95 | + @delete="(event) => deleteFieldFile(item, event)" | ||
| 96 | + multiple | ||
| 97 | + :max-count="pickCountOf(item, 5)" | ||
| 98 | + upload-text="上传图片" | ||
| 99 | + class="upload-wrap" | ||
| 100 | + ></up-upload> | ||
| 101 | + | ||
| 102 | + <!-- type8 附件 --> | ||
| 74 | <view v-else-if="item.fieldType === 8" class="file-wrap"> | 103 | <view v-else-if="item.fieldType === 8" class="file-wrap"> |
| 75 | - <view v-for="(file, idx) in fileListMap[item.fieldCode] || []" :key="idx" class="file-row"> | 104 | + <view |
| 105 | + v-for="(file, idx) in fileListMap[item.fieldCode] || []" | ||
| 106 | + :key="idx" | ||
| 107 | + class="file-row" | ||
| 108 | + > | ||
| 76 | <text class="file-name">{{ file.name || fileNameOf(file.url) }}</text> | 109 | <text class="file-name">{{ file.name || fileNameOf(file.url) }}</text> |
| 77 | - <text class="file-del" @click.stop="deleteFieldFile(item, { index: idx })">删除</text> | 110 | + <text class="file-del" @click.stop="deleteFieldFile(item, {index: idx})">删除</text> |
| 78 | </view> | 111 | </view> |
| 79 | - <up-button size="small" type="primary" plain :text="fileButtonText(item)" | ||
| 80 | - @click.stop="chooseFieldFiles(item)"></up-button> | 112 | + <up-button |
| 113 | + size="small" | ||
| 114 | + type="primary" | ||
| 115 | + plain | ||
| 116 | + :text="fileButtonText(item)" | ||
| 117 | + @click.stop="chooseFieldFiles(item)" | ||
| 118 | + ></up-button> | ||
| 81 | </view> | 119 | </view> |
| 82 | - <!-- type9 关联记录:底部选择,提交 JSON 数组 [{id,name}] --> | ||
| 83 | - <up-input v-else-if="item.fieldType === 9" v-model="labelMap[item.fieldCode]" readonly | ||
| 84 | - disabled-color="#ffffff" :placeholder="`请选择${item.fieldName}`" border="none"> | 120 | + |
| 121 | + <!-- type9 关联记录 单选 --> | ||
| 122 | + <up-input | ||
| 123 | + v-else-if="item.fieldType === 9" | ||
| 124 | + v-model="labelMap[item.fieldCode]" | ||
| 125 | + readonly | ||
| 126 | + disabled-color="#ffffff" | ||
| 127 | + :placeholder="`请选择${item.fieldName}`" | ||
| 128 | + border="none" | ||
| 129 | + > | ||
| 85 | <template #right> | 130 | <template #right> |
| 86 | <up-icon name="arrow-right" size="16"></up-icon> | 131 | <up-icon name="arrow-right" size="16"></up-icon> |
| 87 | </template> | 132 | </template> |
| 88 | </up-input> | 133 | </up-input> |
| 89 | - <!-- type10 链接:URL 输入,提交裸 URL --> | ||
| 90 | - <up-input v-else-if="item.fieldType === 10" v-model.trim="inspectForm[item.fieldCode]" | ||
| 91 | - :placeholder="`请输入${item.fieldName}(http/https 开头)`"></up-input> | ||
| 92 | - <!-- type11 地理位置:uni.chooseLocation 选点,提交 JSON 对象 {lng,lat,address} --> | ||
| 93 | - <up-input v-else-if="item.fieldType === 11" v-model="labelMap[item.fieldCode]" readonly | ||
| 94 | - disabled-color="#ffffff" :placeholder="`点击选择${item.fieldName}`" border="none"> | 134 | + |
| 135 | + <!-- type10 URL链接 --> | ||
| 136 | + <up-input | ||
| 137 | + v-else-if="item.fieldType === 10" | ||
| 138 | + v-model.trim="inspectForm[item.fieldCode]" | ||
| 139 | + placeholder="请输入链接(http/https开头)" | ||
| 140 | + ></up-input> | ||
| 141 | + | ||
| 142 | + <!-- type11 地理位置 --> | ||
| 143 | + <up-input | ||
| 144 | + v-else-if="item.fieldType === 11" | ||
| 145 | + v-model="labelMap[item.fieldCode]" | ||
| 146 | + readonly | ||
| 147 | + disabled-color="#ffffff" | ||
| 148 | + placeholder="点击选择位置" | ||
| 149 | + border="none" | ||
| 150 | + > | ||
| 95 | <template #right> | 151 | <template #right> |
| 96 | <up-icon name="arrow-right" size="16"></up-icon> | 152 | <up-icon name="arrow-right" size="16"></up-icon> |
| 97 | </template> | 153 | </template> |
| 98 | </up-input> | 154 | </up-input> |
| 99 | - <!-- 物料(fieldType=3 + optionSource=3):额外收领用数量;数量>0 才提交 materialId/materialName/quantity 触发扣库存。 | ||
| 100 | - 必须放在 v-if/v-else-if 链末尾——插在中间会打断链导致编译报错 --> | ||
| 101 | - <view v-if="item.fieldType === 3 && item.optionSource === 3" class="qty-row qty-row-material"> | 155 | + |
| 156 | + <!-- 物料附加数量输入框 type3 + optionSource=5 --> | ||
| 157 | + <view v-if="item.fieldType === 3 && item.optionSource === 5" class="qty-row qty-row-material"> | ||
| 102 | <text class="qty-label">领用数量</text> | 158 | <text class="qty-label">领用数量</text> |
| 103 | - <u-number-box v-model="qtyMap[item.fieldCode]" :min="0" :step="1"></u-number-box> | 159 | + <u-number-box |
| 160 | + v-model="qtyMap[item.fieldCode]" | ||
| 161 | + min="0" | ||
| 162 | + step="1" | ||
| 163 | + ></u-number-box> | ||
| 104 | </view> | 164 | </view> |
| 105 | </up-form-item> | 165 | </up-form-item> |
| 106 | </template> | 166 | </template> |
| 107 | <!-- ========== 动态表单结束 ========== --> | 167 | <!-- ========== 动态表单结束 ========== --> |
| 108 | - <!-- 1. 养护描述(文本域) --> | ||
| 109 | - <up-form-item prop="remark" class="form-item"> | ||
| 110 | - <up-textarea v-model.trim="inspectForm.remark" placeholder="请输入养护描述" maxlength="200" count></up-textarea> | 168 | + |
| 169 | + <!-- 养护描述 --> | ||
| 170 | + <up-form-item prop="remark" label="养护描述"> | ||
| 171 | + <up-textarea | ||
| 172 | + v-model.trim="inspectForm.remark" | ||
| 173 | + placeholder="请输入养护描述" | ||
| 174 | + maxlength="200" | ||
| 175 | + count | ||
| 176 | + ></up-textarea> | ||
| 111 | </up-form-item> | 177 | </up-form-item> |
| 112 | - <!-- 2. 上传图片 --> | ||
| 113 | - <up-form-item label="上传图片" prop="images" required class="form-item"> | ||
| 114 | - <up-upload :file-list="imagesList" @after-read="(event) => uploadImgs(event)" | ||
| 115 | - @delete="(event) => deleteImg(event)" @on-exceed="handleExceed" multiple :max-count="3" upload-text="选择图片" | ||
| 116 | - del-color="#ff4d4f" class="upload-wrap"></up-upload> | 178 | + |
| 179 | + <!-- 图片上传 --> | ||
| 180 | + <up-form-item label="上传图片" prop="images" required> | ||
| 181 | + <up-upload | ||
| 182 | + :file-list="imageList" | ||
| 183 | + @after-read="uploadImages" | ||
| 184 | + @delete="deleteImage" | ||
| 185 | + @on-exceed="handleExceed" | ||
| 186 | + multiple | ||
| 187 | + :max-count="3" | ||
| 188 | + upload-text="上传图片" | ||
| 189 | + ></up-upload> | ||
| 117 | </up-form-item> | 190 | </up-form-item> |
| 118 | </up-form> | 191 | </up-form> |
| 119 | </view> | 192 | </view> |
| 193 | + | ||
| 120 | <!-- 底部提交按钮 --> | 194 | <!-- 底部提交按钮 --> |
| 121 | - <view class="fixed-bottom-btn-wrap"> | ||
| 122 | - <up-button type="primary" text="提交" @click="submitInspect" | ||
| 123 | - style="width: 100%; height: 88rpx; font-size:32rpx; border-radius: 0;"></up-button> | 195 | + <view class="fixed-bottom-wrap"> |
| 196 | + <up-button | ||
| 197 | + type="primary" | ||
| 198 | + text="提交" | ||
| 199 | + @click="submitInspect" | ||
| 200 | + style="width:100%;height:88rpx;font-size:32rpx;border-radius:0" | ||
| 201 | + ></up-button> | ||
| 124 | </view> | 202 | </view> |
| 125 | - <!-- uview-plus 日期时间选择器(全局复用) --> | ||
| 126 | - <up-datetime-picker :show="datePickerShow" v-model="datePickerValue" mode="datetime" close-on-click-overlay | ||
| 127 | - @confirm="onDateConfirm" @cancel="onDateClose" @close="onDateClose"></up-datetime-picker> | 203 | + |
| 204 | + <!-- 日期选择弹窗 --> | ||
| 205 | + <up-datetime-picker | ||
| 206 | + :show="datePickerShow" | ||
| 207 | + v-model="datePickerValue" | ||
| 208 | + mode="datetime" | ||
| 209 | + close-on-click-overlay | ||
| 210 | + @confirm="onDateConfirm" | ||
| 211 | + @cancel="onDateClose" | ||
| 212 | + ></up-datetime-picker> | ||
| 213 | + | ||
| 128 | <!-- ActionSheet 底部选择弹窗 --> | 214 | <!-- ActionSheet 底部选择弹窗 --> |
| 129 | - <up-action-sheet :show="actionSheet.show" :actions="actionSheet.options" @select="onActionSheetSelect" | ||
| 130 | - @close="actionSheet.show = false" cancel-text="取消"></up-action-sheet> | ||
| 131 | - <!-- 分层选项选择器(选项带 path 的动态字段) --> | ||
| 132 | - <grouped-picker :show="groupPicker.show" :title="groupPicker.title" :options="groupPicker.options" | ||
| 133 | - :multiple="groupPicker.multiple" :max-count="groupPicker.maxCount" | ||
| 134 | - :need-quantity="groupPicker.needQuantity" :value="groupPicker.value" | ||
| 135 | - :quantities="groupPicker.quantities" :unit="groupPicker.unit" @update:show="groupPicker.show = $event" | ||
| 136 | - @confirm="onGroupedConfirm"></grouped-picker> | 215 | + <up-action-sheet |
| 216 | + :show="actionSheetShow" | ||
| 217 | + :actions="actionSheetOptions" | ||
| 218 | + @select="onActionSheetSelect" | ||
| 219 | + @close="actionSheetShow = false" | ||
| 220 | + cancel-text="取消" | ||
| 221 | + ></up-action-sheet> | ||
| 222 | + | ||
| 223 | + <!-- ===================== type6 全屏弹窗 ===================== --> | ||
| 224 | + <up-popup | ||
| 225 | + :show="popupType6.show" | ||
| 226 | + round="0" | ||
| 227 | + close-on-click-overlay="false" | ||
| 228 | + @close="closeType6Popup" | ||
| 229 | + style="height:100%;width:100%" | ||
| 230 | + > | ||
| 231 | + <view class="popup-header"> | ||
| 232 | + <text class="popup-title">{{ popupType6.fieldItem?.fieldName || '选择项目' }}</text> | ||
| 233 | + <up-icon name="close" size="32rpx" @click="closeType6Popup"></up-icon> | ||
| 234 | + </view> | ||
| 235 | + <!-- tabs --> | ||
| 236 | + <up-tabs | ||
| 237 | + v-model="popupType6.activeTab" | ||
| 238 | + :list="popupType6.tabs" | ||
| 239 | + @change="onTabChange" | ||
| 240 | + swipeable="false" | ||
| 241 | + ></up-tabs> | ||
| 242 | + <view class="popup-body"> | ||
| 243 | + <view class="left-sidebar"> | ||
| 244 | + <view | ||
| 245 | + v-for="subItem in popupType6.subMenuList" | ||
| 246 | + :key="subItem.key" | ||
| 247 | + class="sidebar-item" | ||
| 248 | + :class="{active: popupType6.activeSubKey === subItem.key}" | ||
| 249 | + @click="popupType6.activeSubKey = subItem.key; filterRightList()" | ||
| 250 | + > | ||
| 251 | + {{ subItem.label }} | ||
| 252 | + </view> | ||
| 253 | + </view> | ||
| 254 | + <view class="right-content"> | ||
| 255 | + <view class="list-item" v-for="row in popupType6.rightList" :key="row.value"> | ||
| 256 | + <!-- multiple:0 = 多选checkbox; multiple:1 = 单选radio --> | ||
| 257 | + <view v-if="popupType6.multiple === 0"> | ||
| 258 | + <u-checkbox | ||
| 259 | + v-model="inspectForm[popupType6.fieldCode]" | ||
| 260 | + :name="row.value" | ||
| 261 | + :disabled="isType6OptionLocked(row)" | ||
| 262 | + ></u-checkbox> | ||
| 263 | + </view> | ||
| 264 | + <u-radio | ||
| 265 | + v-else | ||
| 266 | + v-model="inspectForm[popupType6.fieldCode]" | ||
| 267 | + :label="row.value" | ||
| 268 | + ></u-radio> | ||
| 269 | + | ||
| 270 | + <text class="row-label">{{ row.label }}</text> | ||
| 271 | + | ||
| 272 | + <!-- needQuantity 数量输入框 --> | ||
| 273 | + <u-number-box | ||
| 274 | + v-if="popupType6.extConfig?.needQuantity === true" | ||
| 275 | + v-model="qtyMap[popupType6.fieldCode][row.value]" | ||
| 276 | + min="0" | ||
| 277 | + step="1" | ||
| 278 | + size="small" | ||
| 279 | + ></u-number-box> | ||
| 280 | + </view> | ||
| 281 | + <!-- 数量提示,仅多选 multiple:0 才显示 --> | ||
| 282 | + <view | ||
| 283 | + class="tip-text" | ||
| 284 | + v-if="popupType6.multiple === 0 && popupType6.extConfig?.maxCount" | ||
| 285 | + > | ||
| 286 | + 已选:{{ (inspectForm[popupType6.fieldCode] || []).length }} / {{ popupType6.extConfig.maxCount }} | ||
| 287 | + </view> | ||
| 288 | + </view> | ||
| 289 | + </view> | ||
| 290 | + <view class="popup-footer"> | ||
| 291 | + <up-button type="primary" text="确定" @click="confirmType6Select"></up-button> | ||
| 292 | + </view> | ||
| 293 | + </up-popup> | ||
| 137 | </view> | 294 | </view> |
| 138 | </template> | 295 | </template> |
| 139 | 296 | ||
| 297 | + | ||
| 298 | + | ||
| 299 | + | ||
| 140 | <script lang="ts"> | 300 | <script lang="ts"> |
| 141 | import { uploadImages } from '@/common/utils/upload' | 301 | import { uploadImages } from '@/common/utils/upload' |
| 142 | import { maintainCreate } from "@/api/maintain-manage/maintain-manage" | 302 | import { maintainCreate } from "@/api/maintain-manage/maintain-manage" |
| 143 | -import { getByType, getOptionsByCode, getMaterialsByCode, workRecordSubmit } from "@/api/common" | 303 | +import { getByType, getOptionsByCode, getMaterialByCode, workRecordSubmit } from "@/api/common" |
| 304 | + | ||
| 144 | export default { | 305 | export default { |
| 145 | data() { | 306 | data() { |
| 146 | return { | 307 | return { |
| 147 | - imagesList: [], | 308 | + imageList: [], |
| 148 | initProgress: 0, | 309 | initProgress: 0, |
| 149 | inspectForm: { | 310 | inspectForm: { |
| 150 | remark: '', | 311 | remark: '', |
| 151 | }, | 312 | }, |
| 152 | - // ========== 新增:用来存放展示文本label的映射对象 ========== | ||
| 153 | labelMap: {}, | 313 | labelMap: {}, |
| 154 | - // 类型7图片/8附件的每字段文件列表(fieldCode -> u-upload 兼容数组 {url,status,name}) | ||
| 155 | fileListMap: {}, | 314 | fileListMap: {}, |
| 156 | - // 数量收集:类型6+needQuantity 为 {选项值: 数量};类型3物料为标量(领用数量) | ||
| 157 | qtyMap: {}, | 315 | qtyMap: {}, |
| 158 | - paramsOptins: {}, | ||
| 159 | - // 幂等流水号:本次填报页面生命周期内保持不变,重复提交(如网络超时后重试)只会落一条记录 | 316 | + paramsOptions: {}, |
| 160 | clientSeq: '', | 317 | clientSeq: '', |
| 161 | - // 养护计划本体的 commit.id:取到后缓存,重试时不再重复创建养护记录 | ||
| 162 | commitId: null, | 318 | commitId: null, |
| 163 | inspectFormRules: { | 319 | inspectFormRules: { |
| 164 | images: [ | 320 | images: [ |
| 165 | { | 321 | { |
| 166 | required: true, | 322 | required: true, |
| 167 | - message: '请上传图片', | 323 | + message: '最少上传1张图片', |
| 168 | trigger: 'change', | 324 | trigger: 'change', |
| 169 | validator: (rule, value, callback) => { | 325 | validator: (rule, value, callback) => { |
| 170 | - const hasSuccessImg = this.imagesList.some(item => item.status === 'success') | ||
| 171 | - const imgCount = this.imagesList.filter(item => item.status === 'success').length | ||
| 172 | - if (!hasSuccessImg || imgCount < 1) { | ||
| 173 | - callback(new Error('最少需要上传1张图片')) | ||
| 174 | - } else if (imgCount > 3) { | ||
| 175 | - callback(new Error('最多只能上传3张图片')) | 326 | + const hasSuccess = this.imageList.some(item => item.status === 'success') |
| 327 | + const count = this.imageList.filter(item => item.status === 'success').length | ||
| 328 | + if (!hasSuccess || count < 1) { | ||
| 329 | + callback(new Error('最少上传1张图片')) | ||
| 330 | + } else if (count > 3) { | ||
| 331 | + callback(new Error('最多上传3张图片')) | ||
| 176 | } else { | 332 | } else { |
| 177 | callback() | 333 | callback() |
| 178 | } | 334 | } |
| 179 | } | 335 | } |
| 180 | } | 336 | } |
| 181 | - ], | 337 | + ] |
| 182 | }, | 338 | }, |
| 183 | - // 动态字段列表 | ||
| 184 | - dynamicFields: [] as any[], | ||
| 185 | - // 日期时间选择器 | 339 | + dynamicFields: [], |
| 340 | + | ||
| 186 | datePickerShow: false, | 341 | datePickerShow: false, |
| 187 | - datePickerValue: Number(new Date()), | 342 | + datePickerValue: new Date().getTime(), |
| 188 | currentDateFieldCode: '', | 343 | currentDateFieldCode: '', |
| 189 | - // actionSheet配置 | ||
| 190 | - actionSheet: { | ||
| 191 | - show: false, | ||
| 192 | - fieldCode: '', // 当前操作的字段 | ||
| 193 | - options: [] as any[], | ||
| 194 | - fieldItem: null // 保存当前点击的field对象,用于后面查找label | ||
| 195 | - }, | ||
| 196 | - // 分层选项选择器配置(选项带 path 的 3/6 字段用,其余字段仍走上面的 actionSheet / 内联复选框) | ||
| 197 | - groupPicker: { | 344 | + |
| 345 | + actionSheetShow: false, | ||
| 346 | + actionSheetOptions: [], | ||
| 347 | + actionSheetFieldItem: null, | ||
| 348 | + | ||
| 349 | + // ===== type6弹窗状态 ===== | ||
| 350 | + popupType6: { | ||
| 198 | show: false, | 351 | show: false, |
| 199 | fieldCode: '', | 352 | fieldCode: '', |
| 200 | - fieldItem: null as any, | ||
| 201 | - title: '', | ||
| 202 | - options: [] as any[], | ||
| 203 | - multiple: false, | ||
| 204 | - maxCount: Infinity, | ||
| 205 | - needQuantity: false, | ||
| 206 | - value: '' as any, | ||
| 207 | - quantities: {} as any, | ||
| 208 | - unit: '' | 353 | + fieldItem: null, |
| 354 | + activeTab: 0, | ||
| 355 | + tabs: [], | ||
| 356 | + activeSubKey: '', | ||
| 357 | + subMenuList: [], | ||
| 358 | + allRawData: [], | ||
| 359 | + rightList: [] | ||
| 209 | } | 360 | } |
| 210 | } | 361 | } |
| 211 | }, | 362 | }, |
| 212 | onLoad(option) { | 363 | onLoad(option) { |
| 213 | console.log('页面参数:', option) | 364 | console.log('页面参数:', option) |
| 214 | - this.paramsOptins = option | 365 | + this.paramsOptions = option |
| 215 | const { businessType, planTypeId } = option | 366 | const { businessType, planTypeId } = option |
| 216 | - this.loadDynamicFields(businessType, planTypeId) | 367 | + const workTypeId = planTypeId |
| 368 | + this.loadDynamicFields(businessType, workTypeId) | ||
| 217 | }, | 369 | }, |
| 218 | onReady() { | 370 | onReady() { |
| 219 | this.$refs.inspectFormRef.setRules(this.inspectFormRules) | 371 | this.$refs.inspectFormRef.setRules(this.inspectFormRules) |
| 220 | - console.log('巡查表单规则初始化完成') | 372 | + console.log('表单规则加载完成') |
| 221 | }, | 373 | }, |
| 222 | methods: { | 374 | methods: { |
| 223 | - // number-box数值变更回调 | 375 | + // 获取type6展示文本 |
| 376 | + getType6ShowLabel(item) { | ||
| 377 | + const val = this.inspectForm[item.fieldCode] | ||
| 378 | + const optList = item.selectOptions || [] | ||
| 379 | + if (item.multiple === 0) { | ||
| 380 | + // multiple:0 多选,数组 | ||
| 381 | + if (!val || val.length === 0) return '' | ||
| 382 | + const nameArr = optList.filter(o => val.includes(o.value)).map(o => o.label) | ||
| 383 | + return nameArr.join('、') | ||
| 384 | + } else { | ||
| 385 | + // multiple:1 单选,单个值 | ||
| 386 | + if (!val) return '' | ||
| 387 | + const findOpt = optList.find(o => o.value === val) | ||
| 388 | + return findOpt ? findOpt.label : '' | ||
| 389 | + } | ||
| 390 | + }, | ||
| 391 | + | ||
| 392 | + // 打开type6弹窗 | ||
| 393 | + openType6Popup(item) { | ||
| 394 | + this.popupType6.fieldItem = item | ||
| 395 | + this.popupType6.fieldCode = item.fieldCode | ||
| 396 | + this.popupType6.allRawData = this.mockLargeTreeData(item.selectOptions || []) | ||
| 397 | + this.popupType6.tabs = [ | ||
| 398 | + { label: '机电设备', value: 0 }, | ||
| 399 | + { label: '消防器材', value: 1 }, | ||
| 400 | + { label: '给排水', value: 2 }, | ||
| 401 | + { label: '照明系统', value: 3 }, | ||
| 402 | + { label: '安防监控', value: 4 } | ||
| 403 | + ] | ||
| 404 | + this.popupType6.activeTab = 0 | ||
| 405 | + this.onTabChange(0) | ||
| 406 | + this.popupType6.show = true | ||
| 407 | + }, | ||
| 408 | + | ||
| 409 | + mockLargeTreeData(baseOpts) { | ||
| 410 | + const tree = [ | ||
| 411 | + { | ||
| 412 | + tabIndex: 0, | ||
| 413 | + subMenu: [ | ||
| 414 | + { key: 'sub01', label: '水泵类' }, | ||
| 415 | + { key: 'sub02', label: '风机类' }, | ||
| 416 | + { key: 'sub03', label: '控制柜' }, | ||
| 417 | + { key: 'sub04', label: '换热器' }, | ||
| 418 | + ], | ||
| 419 | + items: [ | ||
| 420 | + { subKey: 'sub01', value: 'p001', label: '立式离心泵A' }, | ||
| 421 | + { subKey: 'sub01', value: 'p002', label: '卧式增压泵B' }, | ||
| 422 | + { subKey: 'sub01', value: 'p003', label: '潜污泵C' }, | ||
| 423 | + { subKey: 'sub02', value: 'f001', label: '排烟风机1号' }, | ||
| 424 | + { subKey: 'sub02', value: 'f002', label: '正压送风机' }, | ||
| 425 | + { subKey: 'sub03', value: 'c001', label: '动力控制柜#1' }, | ||
| 426 | + { subKey: 'sub03', value: 'c002', label: 'PLC控制柜#2' }, | ||
| 427 | + { subKey: 'sub04', value: 'h001', label: '板式换热器' }, | ||
| 428 | + ] | ||
| 429 | + }, | ||
| 430 | + { | ||
| 431 | + tabIndex: 1, | ||
| 432 | + subMenu: [ | ||
| 433 | + { key: 'sub11', label: '灭火器' }, | ||
| 434 | + { key: 'sub12', label: '消火栓' }, | ||
| 435 | + { key: 'sub13', label: '烟感温感' }, | ||
| 436 | + ], | ||
| 437 | + items: [ | ||
| 438 | + { subKey: 'sub11', value: 'xf001', label: '4kg干粉灭火器' }, | ||
| 439 | + { subKey: 'sub11', value: 'xf002', label: '二氧化碳灭火器' }, | ||
| 440 | + { subKey: 'sub12', value: 'xf010', label: '室内消火栓箱' }, | ||
| 441 | + { subKey: 'sub13', value: 'xf020', label: '光电烟感探测器' }, | ||
| 442 | + { subKey: 'sub13', value: 'xf021', label: '温度探测器' }, | ||
| 443 | + ] | ||
| 444 | + }, | ||
| 445 | + { | ||
| 446 | + tabIndex: 2, | ||
| 447 | + subMenu: [ | ||
| 448 | + { key: 'sub21', label: '阀门' }, | ||
| 449 | + { key: 'sub22', label: '管道配件' }, | ||
| 450 | + ], | ||
| 451 | + items: [ | ||
| 452 | + { subKey: 'sub21', value: 'w001', label: 'DN100闸阀' }, | ||
| 453 | + { subKey: 'sub21', value: 'w002', label: '止回阀' }, | ||
| 454 | + { subKey: 'sub22', value: 'w010', label: '柔性接头' }, | ||
| 455 | + ] | ||
| 456 | + }, | ||
| 457 | + { | ||
| 458 | + tabIndex: 3, | ||
| 459 | + subMenu: [ | ||
| 460 | + { key: 'sub31', label: 'LED灯具' }, | ||
| 461 | + { key: 'sub32', label: '应急照明' }, | ||
| 462 | + ], | ||
| 463 | + items: [ | ||
| 464 | + { subKey: 'sub31', value: 'l001', label: 'LED平板灯 300*600' }, | ||
| 465 | + { subKey: 'sub32', value: 'l002', label: '消防应急灯' }, | ||
| 466 | + ] | ||
| 467 | + }, | ||
| 468 | + { | ||
| 469 | + tabIndex: 4, | ||
| 470 | + subMenu: [ | ||
| 471 | + { key: 'sub41', label: '摄像头' }, | ||
| 472 | + { key: 'sub42', label: '门禁' }, | ||
| 473 | + ], | ||
| 474 | + items: [ | ||
| 475 | + { subKey: 'sub41', value: 'a001', label: '枪机摄像头' }, | ||
| 476 | + { subKey: 'sub41', value: 'a002', label: '半球摄像机' }, | ||
| 477 | + { subKey: 'sub42', value: 'a010', label: '刷卡门禁主机' }, | ||
| 478 | + ] | ||
| 479 | + } | ||
| 480 | + ] | ||
| 481 | + return tree | ||
| 482 | + }, | ||
| 483 | + | ||
| 484 | + onTabChange(tabVal) { | ||
| 485 | + const tabData = this.popupType6.allRawData.find(item => item.tabIndex === tabVal) | ||
| 486 | + if (!tabData) return | ||
| 487 | + this.popupType6.subMenuList = tabData.subMenu | ||
| 488 | + this.popupType6.activeSubKey = tabData.subMenu[0].key | ||
| 489 | + this.filterRightList() | ||
| 490 | + }, | ||
| 491 | + | ||
| 492 | + filterRightList() { | ||
| 493 | + const tabData = this.popupType6.allRawData.find(item => item.tabIndex === this.popupType6.activeTab) | ||
| 494 | + if (!tabData) return | ||
| 495 | + this.popupType6.rightList = tabData.items.filter(it => it.subKey === this.popupType6.activeSubKey) | ||
| 496 | + }, | ||
| 497 | + | ||
| 498 | + // 多选maxCount禁用判断,仅 multiple:0(多选)启用 | ||
| 499 | + isType6OptionLocked(opt) { | ||
| 500 | + const item = this.popupType6.fieldItem | ||
| 501 | + if (item.multiple !== 0) return false | ||
| 502 | + const max = item.extConfig?.maxCount | ||
| 503 | + if (!max) return false | ||
| 504 | + const selectedArr = this.inspectForm[item.fieldCode] || [] | ||
| 505 | + if (selectedArr.length >= max && !selectedArr.includes(opt.value)) { | ||
| 506 | + return true | ||
| 507 | + } | ||
| 508 | + return false | ||
| 509 | + }, | ||
| 510 | + | ||
| 511 | + confirmType6Select() { | ||
| 512 | + this.$refs.inspectFormRef.validateField(this.popupType6.fieldCode) | ||
| 513 | + this.popupType6.show = false | ||
| 514 | + }, | ||
| 515 | + closeType6Popup() { | ||
| 516 | + this.popupType6.show = false | ||
| 517 | + }, | ||
| 518 | + | ||
| 519 | + openPickerIfNeeded(item) { | ||
| 520 | + if (item.fieldType === 6) { | ||
| 521 | + return this.openType6Popup(item) | ||
| 522 | + } | ||
| 523 | + if (item.fieldType === 3 && item.multiple !== 1) { | ||
| 524 | + return this.openActionSheet(item) | ||
| 525 | + } | ||
| 526 | + if (item.fieldType === 9) { | ||
| 527 | + return this.openActionSheet(item) | ||
| 528 | + } | ||
| 529 | + if (item.fieldType === 11) { | ||
| 530 | + return this.openLocationPicker(item) | ||
| 531 | + } | ||
| 532 | + }, | ||
| 533 | + | ||
| 224 | valChange(item) { | 534 | valChange(item) { |
| 225 | - // 数值修改后触发表单校验 | ||
| 226 | this.$refs.inspectFormRef.validateField(item.fieldCode) | 535 | this.$refs.inspectFormRef.validateField(item.fieldCode) |
| 227 | }, | 536 | }, |
| 228 | - // ========== 日期时间相关方法 ========== | 537 | + |
| 229 | openDatePicker(field) { | 538 | openDatePicker(field) { |
| 230 | - console.log('打开日期选择器', field) | ||
| 231 | this.currentDateFieldCode = field.fieldCode | 539 | this.currentDateFieldCode = field.fieldCode |
| 232 | const val = this.inspectForm[field.fieldCode] | 540 | const val = this.inspectForm[field.fieldCode] |
| 233 | - this.datePickerValue = val ? new Date(val).getTime() : Number(new Date()) | 541 | + this.datePickerValue = val ? new Date(val).getTime() : new Date().getTime() |
| 234 | this.datePickerShow = true | 542 | this.datePickerShow = true |
| 235 | uni.hideKeyboard() | 543 | uni.hideKeyboard() |
| 236 | }, | 544 | }, |
| @@ -250,158 +558,71 @@ export default { | @@ -250,158 +558,71 @@ export default { | ||
| 250 | onDateClose() { | 558 | onDateClose() { |
| 251 | this.datePickerShow = false | 559 | this.datePickerShow = false |
| 252 | }, | 560 | }, |
| 253 | - /** | ||
| 254 | - * 点击表单项时按类型决定弹出哪个选择器: | ||
| 255 | - * 分层选项(带 path 的 3/6) → 分组弹层;3 枚举(单选) / 9 关联记录 → 底部 actionSheet;11 地理位置 → 微信地图选点 | ||
| 256 | - * 其余类型(多选、图片、附件、可输入的链接)不在这里处理,由控件自身响应 | ||
| 257 | - */ | ||
| 258 | - openPickerIfNeeded(item: any) { | ||
| 259 | - // 分层选项优先:它的只读入口是靠本方法打开弹层的(模板里 3/6 的原有分支已被 isGroupedField 挡在前面) | ||
| 260 | - if (this.isGroupedField(item)) { | ||
| 261 | - return this.openGroupedPicker(item) | ||
| 262 | - } | ||
| 263 | - // 条件与模板互补:模板用 multiple === 1 渲染复选框组,这里就必须用 !== 1 才打开弹窗, | ||
| 264 | - // 否则 multiple 为 NULL/0 的存量单选枚举会既不渲染复选框、又点不开弹窗 | ||
| 265 | - if (item.fieldType === 3 && item.multiple !== 1) { | ||
| 266 | - return this.openActionSheet(item) | ||
| 267 | - } | ||
| 268 | - if (item.fieldType === 9) { | ||
| 269 | - return this.openActionSheet(item) | ||
| 270 | - } | ||
| 271 | - if (item.fieldType === 11) { | ||
| 272 | - return this.openLocation(item) | ||
| 273 | - } | ||
| 274 | - }, | ||
| 275 | - /** | ||
| 276 | - * 是否为「分层选项」字段:3枚举/6多选,且选项带了后端下发的 path。 | ||
| 277 | - * path 是纯增量字段,老字段(静态/字典/物料/数据表/接口/无 gN_ 别名的 SQL)没有它 → 走原路径,行为不变 | ||
| 278 | - */ | ||
| 279 | - isGroupedField(item: any) { | ||
| 280 | - if (!item || (item.fieldType !== 3 && item.fieldType !== 6)) { | ||
| 281 | - return false | ||
| 282 | - } | ||
| 283 | - return (item.selectOptions || []).some((opt: any) => Array.isArray(opt.path) && opt.path.length > 0) | ||
| 284 | - }, | ||
| 285 | - /** | ||
| 286 | - * 打开分层选项弹层。单选/多选判定与模板同一口径: | ||
| 287 | - * 6 → 多选;3 且 multiple===1 → 多选(提交按逗号拼接);3 其余 → 单选 | ||
| 288 | - */ | ||
| 289 | - openGroupedPicker(item: any) { | ||
| 290 | - const code = item.fieldCode | ||
| 291 | - const multiple = item.fieldType === 6 || item.multiple === 1 | ||
| 292 | - this.groupPicker.fieldCode = code | ||
| 293 | - this.groupPicker.fieldItem = item | ||
| 294 | - this.groupPicker.title = item.fieldName | ||
| 295 | - this.groupPicker.options = item.selectOptions || [] | ||
| 296 | - this.groupPicker.multiple = multiple | ||
| 297 | - this.groupPicker.maxCount = this.maxCountOf(item, Infinity) | ||
| 298 | - // 只有 6 多选需要每项数量;3 枚举没有这层语义 | ||
| 299 | - this.groupPicker.needQuantity = item.fieldType === 6 && this.needQty(item) | ||
| 300 | - this.groupPicker.value = multiple ? (this.inspectForm[code] || []).slice() : (this.inspectForm[code] || '') | ||
| 301 | - this.groupPicker.quantities = Object.assign({}, this.qtyMap[code] || {}) | ||
| 302 | - this.groupPicker.unit = item.unit || '' | ||
| 303 | - this.groupPicker.show = true | ||
| 304 | - }, | ||
| 305 | - /** | ||
| 306 | - * 分层弹层确定回调。写入位置与原有两条路径完全一致(inspectForm/labelMap/qtyMap), | ||
| 307 | - * 因此 buildDynamicFields() 一行都不用改 | ||
| 308 | - */ | ||
| 309 | - onGroupedConfirm(payload: any) { | ||
| 310 | - const code = this.groupPicker.fieldCode | ||
| 311 | - this.inspectForm[code] = payload.values | ||
| 312 | - this.labelMap[code] = payload.labelText | ||
| 313 | - if (this.groupPicker.needQuantity) { | ||
| 314 | - // 合并而不是替换:保留 loadDynamicFields 预建的选项键,避免非分层路径读到 undefined | ||
| 315 | - this.qtyMap[code] = Object.assign({}, this.qtyMap[code] || {}, payload.quantities || {}) | ||
| 316 | - } | ||
| 317 | - this.groupPicker.show = false | ||
| 318 | - this.$refs.inspectFormRef.validateField(code) | ||
| 319 | - }, | ||
| 320 | - /** | ||
| 321 | - * 打开底部选择弹窗 actionSheet | ||
| 322 | - */ | ||
| 323 | - openActionSheet(item: any) { | ||
| 324 | - this.actionSheet.fieldCode = item.fieldCode | ||
| 325 | - this.actionSheet.fieldItem = item | ||
| 326 | - // 选项统一挂在 selectOptions(静态来源直接用 field.options,物料来源调 materials 接口拿,其余调 options 接口) | ||
| 327 | - // 物料带 currentNum 时在展示名后附库存余量(只影响展示名,提交的 materialName 仍取干净的 opt.label) | ||
| 328 | - this.actionSheet.options = (item.selectOptions || []).map(opt => { | ||
| 329 | - const stock = opt.currentNum === null || opt.currentNum === undefined || opt.currentNum === '' | ||
| 330 | - ? NaN | ||
| 331 | - : Number(opt.currentNum) | ||
| 332 | - return { | ||
| 333 | - name: isNaN(stock) ? opt.label : `${opt.label}(库存${stock}${opt.unit || ''})`, | ||
| 334 | - value: opt.value | ||
| 335 | - } | ||
| 336 | - }) | ||
| 337 | - this.actionSheet.show = true | 561 | + |
| 562 | + openActionSheet(item) { | ||
| 563 | + this.actionSheetFieldItem = item | ||
| 564 | + this.actionSheetOptions = (item.selectOptions || []).map(opt => ({ | ||
| 565 | + name: opt.label, | ||
| 566 | + value: opt.value | ||
| 567 | + })) | ||
| 568 | + this.actionSheetShow = true | ||
| 338 | }, | 569 | }, |
| 339 | - /** | ||
| 340 | - * actionSheet选中回调 | ||
| 341 | - */ | ||
| 342 | - onActionSheetSelect(selectedItem: any) { | ||
| 343 | - const fieldItem = this.actionSheet.fieldItem | ||
| 344 | - const fieldCode = this.actionSheet.fieldCode | ||
| 345 | - // 1. 表单保存提交值:9 关联记录存 {id,name}(提交时转成 [{id,name}]),其余存裸 value | ||
| 346 | - if (fieldItem && fieldItem.fieldType === 9) { | ||
| 347 | - this.inspectForm[fieldCode] = { id: selectedItem.value, name: selectedItem.name } | 570 | + onActionSheetSelect(selected) { |
| 571 | + const fieldItem = this.actionSheetFieldItem | ||
| 572 | + const fieldCode = fieldItem.fieldCode | ||
| 573 | + if (fieldItem.fieldType === 9) { | ||
| 574 | + this.inspectForm[fieldCode] = { id: selected.value, name: selected.name } | ||
| 348 | } else { | 575 | } else { |
| 349 | - this.inspectForm[fieldCode] = selectedItem.value | 576 | + this.inspectForm[fieldCode] = selected.value |
| 350 | } | 577 | } |
| 351 | - // 2. labelMap保存label,给input展示 | ||
| 352 | - this.labelMap[fieldCode] = selectedItem.name | ||
| 353 | - this.actionSheet.show = false | ||
| 354 | - // 选中后触发表单校验 | 578 | + this.labelMap[fieldCode] = selected.name |
| 579 | + this.actionSheetShow = false | ||
| 355 | this.$refs.inspectFormRef.validateField(fieldCode) | 580 | this.$refs.inspectFormRef.validateField(fieldCode) |
| 356 | }, | 581 | }, |
| 357 | - /** | ||
| 358 | - * 类型11 地理位置:微信地图选点(无需地图 AK,manifest.json 已声明 requiredPrivateInfos) | ||
| 359 | - * 提交形态 {lng,lat,address} | ||
| 360 | - */ | ||
| 361 | - openLocation(item: any) { | 582 | + |
| 583 | + openLocationPicker(item) { | ||
| 362 | uni.chooseLocation({ | 584 | uni.chooseLocation({ |
| 363 | success: (res) => { | 585 | success: (res) => { |
| 364 | this.inspectForm[item.fieldCode] = { | 586 | this.inspectForm[item.fieldCode] = { |
| 365 | lng: res.longitude, | 587 | lng: res.longitude, |
| 366 | lat: res.latitude, | 588 | lat: res.latitude, |
| 367 | - address: res.address || res.name || '' | 589 | + address: res.address |
| 368 | } | 590 | } |
| 369 | - this.labelMap[item.fieldCode] = res.address || res.name || `${res.latitude}, ${res.longitude}` | 591 | + this.labelMap[item.fieldCode] = res.address || `${res.latitude},${res.longitude}` |
| 370 | this.$refs.inspectFormRef.validateField(item.fieldCode) | 592 | this.$refs.inspectFormRef.validateField(item.fieldCode) |
| 371 | }, | 593 | }, |
| 372 | fail: (err) => { | 594 | fail: (err) => { |
| 373 | - console.error('地图选择失败', err) | ||
| 374 | - if (err && err.errMsg && err.errMsg.indexOf('auth deny') > -1) { | ||
| 375 | - uni.showToast({ title: '请授权位置权限', icon: 'none' }) | 595 | + console.error('位置选择失败', err) |
| 596 | + if (err.errMsg.indexOf('auth deny') > -1) { | ||
| 597 | + uni.showToast({ title: '请开启位置权限', icon: 'none' }) | ||
| 376 | } | 598 | } |
| 377 | } | 599 | } |
| 378 | }) | 600 | }) |
| 379 | }, | 601 | }, |
| 380 | - /** | ||
| 381 | - * 加载动态字段 | ||
| 382 | - */ | 602 | + |
| 603 | + // 加载动态字段 & 初始化inspectForm | ||
| 383 | async loadDynamicFields(businessType: string, workTypeId: string) { | 604 | async loadDynamicFields(businessType: string, workTypeId: string) { |
| 384 | if (!businessType || !workTypeId) { | 605 | if (!businessType || !workTypeId) { |
| 385 | - uni.showToast({ title: '缺少业务参数', icon: 'none' }) | 606 | + uni.showToast({ title: '缺少参数', icon: 'none' }) |
| 386 | return | 607 | return |
| 387 | } | 608 | } |
| 388 | - const bizType = 3 | 609 | + |
| 610 | + const bizType = 3 | ||
| 389 | try { | 611 | try { |
| 390 | - const res = await getByType({ | ||
| 391 | - businessType, | ||
| 392 | - workTypeId, | ||
| 393 | - bizType | ||
| 394 | - }) | 612 | + const res = await getByType({ businessType, bizType,workTypeId }) |
| 395 | let fields = res.fields || [] | 613 | let fields = res.fields || [] |
| 396 | fields.sort((a, b) => a.sort - b.sort) | 614 | fields.sort((a, b) => a.sort - b.sort) |
| 397 | - if (fields.length === 0) { | ||
| 398 | - uni.showToast({ title: '暂无需填报项', icon: 'none' }) | ||
| 399 | - this.dynamicFields = [] | ||
| 400 | - return | ||
| 401 | - } | 615 | + this.dynamicFields = fields |
| 616 | + | ||
| 402 | for (const field of fields) { | 617 | for (const field of fields) { |
| 403 | - // 初值:多选为数组;9关联记录/11地理位置 为对象(未选为 null);7图片/8附件为 URL 数组;其余空字符串 | ||
| 404 | - if (field.fieldType === 6 || (field.fieldType === 3 && field.multiple === 1)) { | 618 | + if (field.fieldType === 6) { |
| 619 | + // multiple:0=多选(数组), multiple:1=单选(单个值null) | ||
| 620 | + if (field.multiple === 0) { | ||
| 621 | + this.inspectForm[field.fieldCode] = [] | ||
| 622 | + } else { | ||
| 623 | + this.inspectForm[field.fieldCode] = null | ||
| 624 | + } | ||
| 625 | + } else if (field.fieldType === 3 && field.multiple === 1) { | ||
| 405 | this.inspectForm[field.fieldCode] = [] | 626 | this.inspectForm[field.fieldCode] = [] |
| 406 | } else if (field.fieldType === 7 || field.fieldType === 8) { | 627 | } else if (field.fieldType === 7 || field.fieldType === 8) { |
| 407 | this.inspectForm[field.fieldCode] = [] | 628 | this.inspectForm[field.fieldCode] = [] |
| @@ -410,387 +631,161 @@ export default { | @@ -410,387 +631,161 @@ export default { | ||
| 410 | } else { | 631 | } else { |
| 411 | this.inspectForm[field.fieldCode] = '' | 632 | this.inspectForm[field.fieldCode] = '' |
| 412 | } | 633 | } |
| 413 | - // 初始化展示label为空 | ||
| 414 | this.labelMap[field.fieldCode] = '' | 634 | this.labelMap[field.fieldCode] = '' |
| 415 | - // 7图片/8附件的文件列表(驱动 up-upload 与自绘文件列表展示) | ||
| 416 | if (field.fieldType === 7 || field.fieldType === 8) { | 635 | if (field.fieldType === 7 || field.fieldType === 8) { |
| 417 | this.fileListMap[field.fieldCode] = [] | 636 | this.fileListMap[field.fieldCode] = [] |
| 418 | } | 637 | } |
| 419 | - // 选项装载:静态来源(1)直接用下发的 field.options,其余来源(2字典/3物料/4数据表/5SQL/6接口)调 options 接口, | ||
| 420 | - // 由后端按 optionSource 分发。类型 3/6/9 需要选项,其余类型不需要 | ||
| 421 | - if (field.fieldType === 3 || field.fieldType === 6 || field.fieldType === 9) { | 638 | + if (field.fieldType === 3 || field.fieldType === 6) { |
| 422 | if (field.optionSource === 1 && Array.isArray(field.options)) { | 639 | if (field.optionSource === 1 && Array.isArray(field.options)) { |
| 423 | - field.selectOptions = field.options.map(opt => ({ label: opt.label, value: String(opt.value), path: opt.path })) | ||
| 424 | - } else if (field.optionSource === 3) { | ||
| 425 | - // 物料来源:调 materials(比 options 多返回 currentNum 库存余量/unit),供选择时显示库存 | ||
| 426 | - const matRes = await getMaterialsByCode({ | ||
| 427 | - businessType, | ||
| 428 | - bizType, | ||
| 429 | - workTypeId, | ||
| 430 | - fieldCode: field.fieldCode | ||
| 431 | - }) | ||
| 432 | - field.selectOptions = (matRes || []).map(mat => ({ | 640 | + field.selectOptions = field.options.map(opt => ({ |
| 641 | + label: opt.label, | ||
| 642 | + value: String(opt.value) | ||
| 643 | + })) | ||
| 644 | + } else if (field.optionSource === 5) { | ||
| 645 | + const matRes = await getMaterialByCode({ businessType, bizType, fieldCode: field.fieldCode,workTypeId }) | ||
| 646 | + field.selectOptions = (matRes.list || []).map(mat => ({ | ||
| 433 | label: mat.materialName, | 647 | label: mat.materialName, |
| 434 | - value: String(mat.materialId), | ||
| 435 | - currentNum: mat.currentNum, | ||
| 436 | - unit: mat.unit | 648 | + value: String(mat.materialId) |
| 437 | })) | 649 | })) |
| 438 | } else { | 650 | } else { |
| 439 | - const optRes = await getOptionsByCode({ | ||
| 440 | - businessType, | ||
| 441 | - bizType, | ||
| 442 | - workTypeId, | ||
| 443 | - fieldCode: field.fieldCode | ||
| 444 | - }) | ||
| 445 | - // path 后端仅在 SQL 来源按 gN_key/gN_label 别名产出;透传后由 isGroupedField 判定是否走分层弹层 | ||
| 446 | - field.selectOptions = (optRes || []).map(opt => ({ label: opt.label, value: String(opt.value), path: opt.path })) | 651 | + const optRes = await getOptionsByCode({ businessType, bizType, fieldCode: field.fieldCode,workTypeId }) |
| 652 | + field.selectOptions = (optRes.list || []).map(opt => ({ | ||
| 653 | + label: opt.label, | ||
| 654 | + value: String(opt.value) | ||
| 655 | + })) | ||
| 447 | } | 656 | } |
| 448 | } | 657 | } |
| 449 | - // 数量收集初始化(必须在 selectOptions 装载之后): | ||
| 450 | - // 6多选+needQuantity 预建每个选项的键,避免模板 v-model 写到未定义的键上; | ||
| 451 | - // 3物料为标量(领用数量),默认 0 = 本次不领用、不扣库存 | 658 | + // 初始化数量映射 |
| 452 | if (field.fieldType === 6 && this.needQty(field)) { | 659 | if (field.fieldType === 6 && this.needQty(field)) { |
| 453 | - const qty = {} | 660 | + const qtyObj = {} |
| 454 | ;(field.selectOptions || []).forEach(opt => { | 661 | ;(field.selectOptions || []).forEach(opt => { |
| 455 | - qty[opt.value] = '' | 662 | + qtyObj[opt.value] = '' |
| 456 | }) | 663 | }) |
| 457 | - this.qtyMap[field.fieldCode] = qty | 664 | + this.qtyMap[field.fieldCode] = qtyObj |
| 458 | } | 665 | } |
| 459 | - if (field.fieldType === 3 && field.optionSource === 3) { | 666 | + if (field.fieldType === 3 && field.optionSource === 5) { |
| 460 | this.qtyMap[field.fieldCode] = 0 | 667 | this.qtyMap[field.fieldCode] = 0 |
| 461 | } | 668 | } |
| 462 | - // 必填规则 | 669 | + // 构建表单校验规则 |
| 463 | if (field.required === 1) { | 670 | if (field.required === 1) { |
| 464 | - this.inspectFormRules[field.fieldCode] = [this.buildRequiredRule(field)] | 671 | + this.inspectFormRules[field.fieldCode] = this.buildRequiredRule(field) |
| 465 | } | 672 | } |
| 466 | } | 673 | } |
| 467 | - this.dynamicFields = fields | ||
| 468 | this.$nextTick(() => { | 674 | this.$nextTick(() => { |
| 469 | this.$refs.inspectFormRef.setRules(this.inspectFormRules) | 675 | this.$refs.inspectFormRef.setRules(this.inspectFormRules) |
| 470 | }) | 676 | }) |
| 471 | } catch (err) { | 677 | } catch (err) { |
| 472 | - console.error('获取动态表单字段失败:', err) | ||
| 473 | - uni.showToast({ title: '加载表单配置失败', icon: 'none' }) | ||
| 474 | - } | ||
| 475 | - }, | ||
| 476 | - /** | ||
| 477 | - * 删除图片 | ||
| 478 | - */ | ||
| 479 | - deleteImg(event) { | ||
| 480 | - this.imagesList.splice(event.index, 1) | ||
| 481 | - this.$refs.inspectFormRef.validateField('images') | ||
| 482 | - uni.showToast({ title: '图片删除成功', icon: 'success' }) | ||
| 483 | - }, | ||
| 484 | - /** | ||
| 485 | - * 上传图片 | ||
| 486 | - */ | ||
| 487 | - async uploadImgs(event) { | ||
| 488 | - const fileList = Array.isArray(event.file) ? event.file : [event.file] | ||
| 489 | - const targetImgList = this.imagesList | ||
| 490 | - const filePaths = fileList.map(item => item.url) | ||
| 491 | - const tempItems = fileList.map(item => ({ ...item, status: 'uploading', message: '上传中' })) | ||
| 492 | - const startIndex = targetImgList.length | ||
| 493 | - targetImgList.push(...tempItems) | ||
| 494 | - try { | ||
| 495 | - const uploadResultUrls = await uploadImages({ filePaths, ignoreError: true }) | ||
| 496 | - uploadResultUrls.forEach((url, index) => { | ||
| 497 | - if (targetImgList[startIndex + index]) { | ||
| 498 | - targetImgList.splice(startIndex + index, 1, { | ||
| 499 | - ...fileList[index], status: 'success', message: '', url | ||
| 500 | - }) | ||
| 501 | - } | ||
| 502 | - }) | ||
| 503 | - if (uploadResultUrls.length < fileList.length) { | ||
| 504 | - const failCount = fileList.length - uploadResultUrls.length | ||
| 505 | - for (let i = uploadResultUrls.length; i < fileList.length; i++) { | ||
| 506 | - if (targetImgList[startIndex + i]) { | ||
| 507 | - targetImgList.splice(startIndex + i, 1, { | ||
| 508 | - ...fileList[i], status: 'failed', message: '上传失败' | ||
| 509 | - }) | ||
| 510 | - } | ||
| 511 | - } | ||
| 512 | - uni.showToast({ title: `成功上传${uploadResultUrls.length}张,失败${failCount}张`, icon: 'none' }) | ||
| 513 | - } else { | ||
| 514 | - uni.showToast({ title: `成功上传${fileList.length}张图片`, icon: 'success' }) | ||
| 515 | - } | ||
| 516 | - this.$refs.inspectFormRef.validateField('images') | ||
| 517 | - } catch (err) { | ||
| 518 | - console.error('图片上传失败:', err) | ||
| 519 | - for (let i = 0; i < fileList.length; i++) { | ||
| 520 | - if (targetImgList[startIndex + i]) { | ||
| 521 | - targetImgList.splice(startIndex + i, 1, { | ||
| 522 | - ...fileList[i], status: 'failed', message: '上传失败' | ||
| 523 | - }) | ||
| 524 | - } | ||
| 525 | - } | ||
| 526 | - uni.showToast({ title: '图片上传失败,请重试', icon: 'none' }) | ||
| 527 | - this.$refs.inspectFormRef.validateField('images') | 678 | + console.error('加载动态表单失败', err) |
| 679 | + uni.showToast({ title: '加载表单失败', icon: 'none' }) | ||
| 528 | } | 680 | } |
| 529 | }, | 681 | }, |
| 530 | - handleExceed() { | ||
| 531 | - uni.showToast({ title: '最多只能上传3张图片', icon: 'none' }) | ||
| 532 | - }, | ||
| 533 | - getImgUrlList(imgList) { | ||
| 534 | - return imgList.filter(item => item.status === 'success').map(item => item.url) | ||
| 535 | - }, | ||
| 536 | - // ========== 动态字段:渲染与取值辅助 ========== | ||
| 537 | - /** | ||
| 538 | - * 类型6 多选是否要求每项填「使用数量」(ext_config.needQuantity) | ||
| 539 | - * 仅记录用量,不写物料三列 ⇒ 结构性不触发扣库存(扣减一律按值行 materialId 非空判定) | ||
| 540 | - */ | ||
| 541 | - needQty(item: any) { | 682 | + |
| 683 | + needQty(item) { | ||
| 542 | return !!(item.extConfig && item.extConfig.needQuantity === true) | 684 | return !!(item.extConfig && item.extConfig.needQuantity === true) |
| 543 | }, | 685 | }, |
| 544 | - /** | ||
| 545 | - * 有效上限:ext_config.maxCount 的「填 0 表示不限」(PC 端提示语)在提交侧照此生效 | ||
| 546 | - * 未配置 → 类型默认值;配 0 → Infinity(不限);配正数 → 该数 | ||
| 547 | - */ | ||
| 548 | - maxCountOf(item: any, defaultValue: number) { | ||
| 549 | - const raw = item.extConfig ? item.extConfig.maxCount : undefined | ||
| 550 | - if (raw === undefined || raw === null || raw === '') { | ||
| 551 | - return defaultValue | ||
| 552 | - } | ||
| 553 | - const num = Number(raw) | ||
| 554 | - if (isNaN(num)) { | ||
| 555 | - return defaultValue | ||
| 556 | - } | ||
| 557 | - return num > 0 ? num : Infinity | ||
| 558 | - }, | ||
| 559 | - /** 传给选择器的单次可选数量:上限为「不限」时用组件默认值 52(平台要求 count 是有限数) */ | ||
| 560 | - pickCountOf(item: any, defaultValue: number) { | ||
| 561 | - const max = this.maxCountOf(item, defaultValue) | ||
| 562 | - return max === Infinity ? 52 : max | ||
| 563 | - }, | ||
| 564 | - /** 单文件大小上限(MB):ext_config.maxSizeMb,未配置时用默认值 */ | ||
| 565 | - maxSizeMbOf(item: any, defaultValue: number) { | ||
| 566 | - const num = item.extConfig ? Number(item.extConfig.maxSizeMb) : NaN | ||
| 567 | - return num > 0 ? num : defaultValue | 686 | + pickCountOf(item, defaultNum) { |
| 687 | + const max = item.extConfig?.maxCount | ||
| 688 | + return max || defaultNum | ||
| 568 | }, | 689 | }, |
| 569 | - /** 类型8 允许的扩展名数组(ext_config.fileType,不含点) */ | ||
| 570 | - extensionsOf(item: any) { | ||
| 571 | - const list = item.extConfig && item.extConfig.fileType | ||
| 572 | - return Array.isArray(list) ? list : [] | 690 | + fileButtonText(item) { |
| 691 | + const exts = this.extListOf(item) | ||
| 692 | + return exts.length ? `选择文件(${exts.join(',')})` : '选择文件' | ||
| 573 | }, | 693 | }, |
| 574 | - fileButtonText(item: any) { | ||
| 575 | - const exts = this.extensionsOf(item) | ||
| 576 | - return exts.length ? `选择文件(${exts.join('/')})` : '选择文件' | 694 | + extListOf(item) { |
| 695 | + return [] | ||
| 577 | }, | 696 | }, |
| 578 | - /** 从 URL 取末段当文件名(与 PC 端 FieldValueCell 同口径) */ | ||
| 579 | - fileNameOf(url: string) { | 697 | + fileNameOf(url) { |
| 580 | if (!url) return '' | 698 | if (!url) return '' |
| 581 | - const path = String(url).split('?')[0].split('#')[0] | 699 | + const path = String(url).split('?')[0] |
| 582 | const name = path.substring(path.lastIndexOf('/') + 1) | 700 | const name = path.substring(path.lastIndexOf('/') + 1) |
| 583 | try { | 701 | try { |
| 584 | - return decodeURIComponent(name) || url | ||
| 585 | - } catch (err) { | ||
| 586 | - return name || url | ||
| 587 | - } | ||
| 588 | - }, | ||
| 589 | - /** 类型6 已勾选的选项(用于渲染每个已选项的数量输入行) */ | ||
| 590 | - checkedOptions(item: any) { | ||
| 591 | - const values = this.inspectForm[item.fieldCode] || [] | ||
| 592 | - return (item.selectOptions || []).filter(opt => values.indexOf(opt.value) > -1) | ||
| 593 | - }, | ||
| 594 | - /** | ||
| 595 | - * 多选是否达到上限(用于禁用未选中项)。 | ||
| 596 | - * 不用「超限后回写 v-model 截断」:u-checkbox 的勾选态是组件内部状态(只受 checked prop 影响), | ||
| 597 | - * 回写父级 v-model 撤不掉已经勾上的项,下一次点击还会按内部状态重新算一遍,等于没限住 | ||
| 598 | - */ | ||
| 599 | - isOptionLocked(item: any, opt: any) { | ||
| 600 | - const max = this.maxCountOf(item, Infinity) | ||
| 601 | - if (max === Infinity) { | ||
| 602 | - return false | 702 | + return decodeURIComponent(name) |
| 703 | + } catch { | ||
| 704 | + return name | ||
| 603 | } | 705 | } |
| 604 | - const values = this.inspectForm[item.fieldCode] || [] | ||
| 605 | - return values.length >= max && values.indexOf(opt.value) === -1 | ||
| 606 | - }, | ||
| 607 | - /** 多选的「已选 x/上限」提示,不限时返回空串 */ | ||
| 608 | - multiHint(item: any) { | ||
| 609 | - const max = this.maxCountOf(item, Infinity) | ||
| 610 | - if (max === Infinity) { | ||
| 611 | - return '' | ||
| 612 | - } | ||
| 613 | - return `已选 ${(this.inspectForm[item.fieldCode] || []).length}/${max}` | ||
| 614 | - }, | ||
| 615 | - onMultiChange(item: any) { | ||
| 616 | - // 上限由 isOptionLocked 禁用未选项保证,这里只负责触发必填校验 | ||
| 617 | - this.$refs.inspectFormRef.validateField(item.fieldCode) | ||
| 618 | }, | 706 | }, |
| 619 | - /** | ||
| 620 | - * 必填规则。 | ||
| 621 | - * 7/8/9/11 的值是数组或对象,async-validator 的 required 按默认 string 类型判定, | ||
| 622 | - * 会放过「空数组」「null」⇒ 必须用 validator 显式判空,否则未选也能提交 | ||
| 623 | - */ | ||
| 624 | - buildRequiredRule(field: any) { | 707 | + |
| 708 | + buildRequiredRule(field) { | ||
| 625 | const type = field.fieldType | 709 | const type = field.fieldType |
| 626 | - if (type === 6 || type === 7 || type === 8) { | ||
| 627 | - const action = type === 6 ? '请选择' : '请上传' | ||
| 628 | - return { | ||
| 629 | - required: true, | ||
| 630 | - trigger: 'change', | ||
| 631 | - validator: (rule, value, callback) => { | ||
| 632 | - Array.isArray(value) && value.length > 0 ? callback() : callback(new Error(`${action}${field.fieldName}`)) | ||
| 633 | - } | ||
| 634 | - } | ||
| 635 | - } | ||
| 636 | - if (type === 9 || type === 11) { | ||
| 637 | - return { | ||
| 638 | - required: true, | ||
| 639 | - trigger: 'change', | ||
| 640 | - validator: (rule, value, callback) => { | ||
| 641 | - value ? callback() : callback(new Error(type === 9 ? `请选择${field.fieldName}` : `请选择${field.fieldName}的位置`)) | 710 | + const action = type === 6 ? '请选择' : type === 7 || type === 8 ? '请上传' : '请填写' |
| 711 | + return { | ||
| 712 | + required: true, | ||
| 713 | + trigger: 'change', | ||
| 714 | + validator: (rule, value, callback) => { | ||
| 715 | + let pass = false | ||
| 716 | + if (type === 6) { | ||
| 717 | + if (field.multiple === 0) { | ||
| 718 | + // 多选:数组非空 | ||
| 719 | + pass = Array.isArray(value) && value.length > 0 | ||
| 720 | + } else { | ||
| 721 | + // 单选:value不为空 | ||
| 722 | + pass = !!value | ||
| 723 | + } | ||
| 724 | + } else if (type === 7 || type === 8) { | ||
| 725 | + pass = Array.isArray(value) && value.length > 0 | ||
| 726 | + } else { | ||
| 727 | + pass = !!value | ||
| 642 | } | 728 | } |
| 729 | + pass ? callback() : callback(new Error(`${action}${field.fieldName}`)) | ||
| 643 | } | 730 | } |
| 644 | } | 731 | } |
| 645 | - // 1数值/2文本/4日期/5布尔/10链接 为字符串;3枚举/6单选为选择项 | ||
| 646 | - const action = type === 3 || type === 5 ? '请选择' : '请填写' | ||
| 647 | - return { required: true, message: `${action}${field.fieldName}`, trigger: 'change' } | ||
| 648 | }, | 732 | }, |
| 649 | - /** 幂等流水号(后端按 userId+clientSeq 去重,命中则返回已存在的 recordId) */ | ||
| 650 | - genClientSeq() { | ||
| 651 | - return `${Date.now()}-${Math.random().toString(16).slice(2, 10)}` | 733 | + |
| 734 | + // 图片上传 | ||
| 735 | + async uploadImages(event) { | ||
| 736 | + const file = event.file | ||
| 737 | + const res = await uploadImages(file) | ||
| 738 | + this.imageList.push({ | ||
| 739 | + url: res.url, | ||
| 740 | + status: 'success' | ||
| 741 | + }) | ||
| 742 | + this.$refs.inspectFormRef.validateField('images') | ||
| 652 | }, | 743 | }, |
| 653 | - // ========== 动态字段:7图片/8附件 ========== | ||
| 654 | - /** 单文件是否超过 ext_config.maxSizeMb(超限即 toast 并返回 true 以便过滤掉) */ | ||
| 655 | - isOversize(file: any, maxSizeMb: number) { | ||
| 656 | - if (!maxSizeMb || !file || !file.size) return false | ||
| 657 | - const over = file.size / 1024 / 1024 > maxSizeMb | ||
| 658 | - if (over) { | ||
| 659 | - uni.showToast({ title: `单个文件不能超过${maxSizeMb}MB`, icon: 'none' }) | ||
| 660 | - } | ||
| 661 | - return over | 744 | + deleteImage(event) { |
| 745 | + const index = event.index | ||
| 746 | + this.imageList.splice(index, 1) | ||
| 747 | + this.$refs.inspectFormRef.validateField('images') | ||
| 662 | }, | 748 | }, |
| 663 | - /** 把已上传成功的 URL 同步进表单值(提交与校验都读 inspectForm) */ | ||
| 664 | - syncFileValue(item: any) { | ||
| 665 | - const list = this.fileListMap[item.fieldCode] || [] | ||
| 666 | - this.inspectForm[item.fieldCode] = list.filter(file => file.status === 'success').map(file => file.url) | 749 | + handleExceed() { |
| 750 | + uni.showToast({ title: '最多上传3张图片', icon: 'none' }) | ||
| 667 | }, | 751 | }, |
| 668 | - /** | ||
| 669 | - * 7图片 上传(u-upload 的 after-read 回调) | ||
| 670 | - */ | ||
| 671 | - uploadFieldFiles(item: any, event: any) { | ||
| 672 | - const rawFile = event && event.file | ||
| 673 | - const fileList = Array.isArray(rawFile) ? rawFile : [rawFile] | ||
| 674 | - return this.uploadToField(item, fileList.filter(file => file && file.url).map(file => ({ | ||
| 675 | - url: file.url, | ||
| 676 | - name: file.name || '', | ||
| 677 | - size: file.size | ||
| 678 | - }))) | 752 | + |
| 753 | + uploadFieldFiles(item, event) { | ||
| 754 | + // 附件上传逻辑 | ||
| 679 | }, | 755 | }, |
| 680 | - /** | ||
| 681 | - * 8附件 选择并上传。 | ||
| 682 | - * 微信小程序没有通用文件选择器,只能从聊天记录里选(uni.chooseMessageFile) | ||
| 683 | - */ | ||
| 684 | - chooseFieldFiles(item: any) { | ||
| 685 | - const maxCount = this.maxCountOf(item, 5) | ||
| 686 | - const remain = maxCount - (this.fileListMap[item.fieldCode] || []).length | ||
| 687 | - if (remain <= 0) { | ||
| 688 | - uni.showToast({ title: `最多只能上传${maxCount}个`, icon: 'none' }) | ||
| 689 | - return | ||
| 690 | - } | ||
| 691 | - const exts = this.extensionsOf(item) | ||
| 692 | - const options: any = { count: Math.min(remain, this.pickCountOf(item, 5)), type: 'file' } | ||
| 693 | - if (exts.length) { | ||
| 694 | - options.extension = exts | ||
| 695 | - } | 756 | + deleteFieldFile(item, event) { |
| 757 | + const idx = event.index | ||
| 758 | + this.fileListMap[item.fieldCode].splice(idx, 1) | ||
| 759 | + this.syncFileValue(item) | ||
| 760 | + this.$refs.inspectFormRef.validateField(item.fieldCode) | ||
| 761 | + }, | ||
| 762 | + chooseFieldFiles(item) { | ||
| 696 | uni.chooseMessageFile({ | 763 | uni.chooseMessageFile({ |
| 697 | - ...options, | 764 | + count: this.pickCountOf(item,5), |
| 765 | + type: 'file', | ||
| 698 | success: (res) => { | 766 | success: (res) => { |
| 699 | - const tempFiles = res.tempFiles || [] | ||
| 700 | - this.uploadToField(item, tempFiles.map(file => ({ | ||
| 701 | - url: file.path, | ||
| 702 | - name: file.name || '', | ||
| 703 | - size: file.size | ||
| 704 | - }))) | 767 | + const files = res.tempFiles |
| 768 | + files.forEach(tempFile => { | ||
| 769 | + this.fileListMap[item.fieldCode].push({ | ||
| 770 | + name: tempFile.name, | ||
| 771 | + url: tempFile.path | ||
| 772 | + }) | ||
| 773 | + }) | ||
| 774 | + this.syncFileValue(item) | ||
| 775 | + this.$refs.inspectFormRef.validateField(item.fieldCode) | ||
| 705 | }, | 776 | }, |
| 706 | fail: (err) => { | 777 | fail: (err) => { |
| 707 | - console.error('选择附件失败:', err) | 778 | + console.error('选择附件失败', err) |
| 708 | } | 779 | } |
| 709 | }) | 780 | }) |
| 710 | }, | 781 | }, |
| 711 | - /** | ||
| 712 | - * 把选中的本地文件逐个上传并把 http 地址回填到 fileListMap / inspectForm。 | ||
| 713 | - * 7图片与8附件共用(uploadImages 是通用 uni.uploadFile,不限图片类型) | ||
| 714 | - */ | ||
| 715 | - async uploadToField(item: any, files: any[]) { | ||
| 716 | - const code = item.fieldCode | ||
| 717 | - const maxCount = this.maxCountOf(item, 5) | ||
| 718 | - const maxSizeMb = this.maxSizeMbOf(item, 5) | ||
| 719 | - const validFiles = (files || []).filter(file => file && file.url && !this.isOversize(file, maxSizeMb)) | ||
| 720 | - if (!validFiles.length) { | ||
| 721 | - return | ||
| 722 | - } | ||
| 723 | - const targetList = this.fileListMap[code] || (this.fileListMap[code] = []) | ||
| 724 | - if (targetList.length + validFiles.length > maxCount) { | ||
| 725 | - uni.showToast({ title: `最多只能上传${maxCount}个`, icon: 'none' }) | ||
| 726 | - return | ||
| 727 | - } | ||
| 728 | - const startIndex = targetList.length | ||
| 729 | - targetList.push(...validFiles.map(file => ({ | ||
| 730 | - url: file.url, status: 'uploading', message: '上传中', name: file.name || '' | ||
| 731 | - }))) | ||
| 732 | - try { | ||
| 733 | - const uploadResultUrls = await uploadImages({ | ||
| 734 | - filePaths: validFiles.map(file => file.url), | ||
| 735 | - ignoreError: true | ||
| 736 | - }) | ||
| 737 | - uploadResultUrls.forEach((url, index) => { | ||
| 738 | - if (targetList[startIndex + index]) { | ||
| 739 | - targetList.splice(startIndex + index, 1, { | ||
| 740 | - url, status: 'success', message: '', name: validFiles[index].name || '' | ||
| 741 | - }) | ||
| 742 | - } | ||
| 743 | - }) | ||
| 744 | - for (let i = uploadResultUrls.length; i < validFiles.length; i++) { | ||
| 745 | - if (targetList[startIndex + i]) { | ||
| 746 | - targetList.splice(startIndex + i, 1, { | ||
| 747 | - url: validFiles[i].url, status: 'failed', message: '上传失败', name: validFiles[i].name || '' | ||
| 748 | - }) | ||
| 749 | - } | ||
| 750 | - } | ||
| 751 | - if (uploadResultUrls.length < validFiles.length) { | ||
| 752 | - uni.showToast({ | ||
| 753 | - title: `成功${uploadResultUrls.length}个,失败${validFiles.length - uploadResultUrls.length}个`, | ||
| 754 | - icon: 'none' | ||
| 755 | - }) | ||
| 756 | - } | ||
| 757 | - } catch (err) { | ||
| 758 | - console.error(`【${code}】文件上传失败:`, err) | ||
| 759 | - for (let i = 0; i < validFiles.length; i++) { | ||
| 760 | - if (targetList[startIndex + i]) { | ||
| 761 | - targetList.splice(startIndex + i, 1, { | ||
| 762 | - url: validFiles[i].url, status: 'failed', message: '上传失败', name: validFiles[i].name || '' | ||
| 763 | - }) | ||
| 764 | - } | ||
| 765 | - } | ||
| 766 | - uni.showToast({ title: '上传失败,请重试', icon: 'none' }) | ||
| 767 | - } | ||
| 768 | - this.syncFileValue(item) | ||
| 769 | - this.$refs.inspectFormRef.validateField(code) | ||
| 770 | - }, | ||
| 771 | - /** | ||
| 772 | - * 删除 7图片/8附件 的某一个已选文件 | ||
| 773 | - */ | ||
| 774 | - deleteFieldFile(item: any, event: any) { | 782 | + syncFileValue(item) { |
| 775 | const list = this.fileListMap[item.fieldCode] || [] | 783 | const list = this.fileListMap[item.fieldCode] || [] |
| 776 | - const index = Number(event && event.index) | ||
| 777 | - if (isNaN(index) || index < 0 || index >= list.length) { | ||
| 778 | - return | ||
| 779 | - } | ||
| 780 | - list.splice(index, 1) | ||
| 781 | - this.syncFileValue(item) | ||
| 782 | - this.$refs.inspectFormRef.validateField(item.fieldCode) | 784 | + this.inspectForm[item.fieldCode] = list.map(file => file.url) |
| 783 | }, | 785 | }, |
| 784 | - // ========== 动态字段:组装提交体 ========== | ||
| 785 | - /** | ||
| 786 | - * 按值格式契约组装 fields[](见 docs/design/app-work-field-api.md §四)。 | ||
| 787 | - * 两条硬规则: | ||
| 788 | - * ① 空值一律不出现在 fields[] 里(不提交 ''/'[]'/'null') | ||
| 789 | - * ② 物料的数量>0 才写 materialId/materialName/quantity —— 非空 materialId 即触发扣库存, | ||
| 790 | - * 因此 6多选+needQuantity(只记录用量)绝不能写这三列,只把 quantity 放进 valueText 的 JSON 里 | ||
| 791 | - */ | 786 | + |
| 792 | buildDynamicFields() { | 787 | buildDynamicFields() { |
| 793 | - const fields: any[] = [] | 788 | + const fields = [] |
| 794 | for (const item of this.dynamicFields) { | 789 | for (const item of this.dynamicFields) { |
| 795 | const code = item.fieldCode | 790 | const code = item.fieldCode |
| 796 | const value = this.inspectForm[code] | 791 | const value = this.inspectForm[code] |
| @@ -798,131 +793,111 @@ export default { | @@ -798,131 +793,111 @@ export default { | ||
| 798 | if (type === 9 || type === 11) { | 793 | if (type === 9 || type === 11) { |
| 799 | if (!value) continue | 794 | if (!value) continue |
| 800 | } else if (type === 6 || type === 7 || type === 8) { | 795 | } else if (type === 6 || type === 7 || type === 8) { |
| 801 | - if (!Array.isArray(value) || value.length === 0) continue | ||
| 802 | - } else if (value === '' || value === null || value === undefined) { | ||
| 803 | - continue | ||
| 804 | - } | 796 | + if ((!value) || (Array.isArray(value) && value.length === 0)) continue |
| 797 | + } else if (!value) continue | ||
| 805 | 798 | ||
| 806 | if (type === 1) { | 799 | if (type === 1) { |
| 807 | fields.push({ fieldCode: code, valueNumber: Number(value) }) | 800 | fields.push({ fieldCode: code, valueNumber: Number(value) }) |
| 808 | - } else if (type === 3 && item.optionSource === 3) { | ||
| 809 | - // 物料:value_text 存物料ID;数量>0 才落物料三列(0 表示本次不领用) | 801 | + } else if (type === 3 && item.optionSource === 5) { |
| 810 | const qty = Number(this.qtyMap[code] || 0) | 802 | const qty = Number(this.qtyMap[code] || 0) |
| 811 | - const matched = (item.selectOptions || []).filter(opt => opt.value === String(value))[0] | ||
| 812 | - const row: any = { fieldCode: code, valueText: String(value) } | ||
| 813 | - if (qty > 0) { | ||
| 814 | - row.materialId = Number(value) | ||
| 815 | - row.materialName = matched ? matched.label : '' | 803 | + const matched = (item.selectOptions || []).find(opt => opt.value === String(value)) |
| 804 | + const row = { fieldCode: code, valueText: String(value) } | ||
| 805 | + if (qty > 0 && matched) { | ||
| 806 | + row.materialId = matched.value | ||
| 807 | + row.materialName = matched.label | ||
| 816 | row.quantity = qty | 808 | row.quantity = qty |
| 817 | } | 809 | } |
| 818 | fields.push(row) | 810 | fields.push(row) |
| 819 | } else if (type === 3) { | 811 | } else if (type === 3) { |
| 820 | - // 3枚举:单选取裸 value;历史「枚举+是否多选」存逗号分隔 | ||
| 821 | - fields.push({ | ||
| 822 | - fieldCode: code, | ||
| 823 | - valueText: item.multiple === 1 ? (value || []).join(',') : String(value) | ||
| 824 | - }) | 812 | + if (item.multiple === 1) { |
| 813 | + fields.push({ fieldCode: code, valueText: (value || []).join(',') }) | ||
| 814 | + } else { | ||
| 815 | + fields.push({ fieldCode: code, valueText: String(value) }) | ||
| 816 | + } | ||
| 825 | } else if (type === 6) { | 817 | } else if (type === 6) { |
| 826 | - const selected = (item.selectOptions || []).filter(opt => (value || []).indexOf(opt.value) > -1) | 818 | + let selected = [] |
| 819 | + if (item.multiple === 0) { | ||
| 820 | + // multiple=0 多选,数组 | ||
| 821 | + selected = (item.selectOptions || []).filter(opt => (value || []).includes(opt.value)) | ||
| 822 | + } else { | ||
| 823 | + // multiple=1 单选,单个值 | ||
| 824 | + selected = (item.selectOptions || []).filter(opt => opt.value === value) | ||
| 825 | + } | ||
| 827 | if (this.needQty(item)) { | 826 | if (this.needQty(item)) { |
| 828 | - // 元素 {value,label,quantity};数量留空的不带 quantity 键(不臆测成 0),label 作快照落库 | ||
| 829 | - const qtyMap = this.qtyMap[code] || {} | 827 | + const qtyObj = this.qtyMap[code] || {} |
| 830 | fields.push({ | 828 | fields.push({ |
| 831 | fieldCode: code, | 829 | fieldCode: code, |
| 832 | valueText: JSON.stringify(selected.map(opt => { | 830 | valueText: JSON.stringify(selected.map(opt => { |
| 833 | - const raw = qtyMap[opt.value] | ||
| 834 | - const row: any = { value: opt.value, label: opt.label } | 831 | + const raw = qtyObj[opt.value] |
| 832 | + const rowItem = { value: opt.value, label: opt.label } | ||
| 835 | if (raw !== '' && raw !== null && raw !== undefined) { | 833 | if (raw !== '' && raw !== null && raw !== undefined) { |
| 836 | - row.quantity = Number(raw) | 834 | + rowItem.quantity = Number(raw) |
| 837 | } | 835 | } |
| 838 | - return row | 836 | + return rowItem |
| 839 | })) | 837 | })) |
| 840 | }) | 838 | }) |
| 841 | } else { | 839 | } else { |
| 842 | - fields.push({ fieldCode: code, valueText: JSON.stringify(value.map(v => String(v))) }) | 840 | + if (item.multiple === 0) { |
| 841 | + fields.push({ fieldCode: code, valueText: JSON.stringify((value || []).map(v => String(v))) }) | ||
| 842 | + } else { | ||
| 843 | + fields.push({ fieldCode: code, valueText: JSON.stringify(String(value)) }) | ||
| 844 | + } | ||
| 843 | } | 845 | } |
| 844 | } else if (type === 7 || type === 8) { | 846 | } else if (type === 7 || type === 8) { |
| 845 | fields.push({ fieldCode: code, valueText: JSON.stringify(value) }) | 847 | fields.push({ fieldCode: code, valueText: JSON.stringify(value) }) |
| 846 | } else if (type === 9) { | 848 | } else if (type === 9) { |
| 847 | fields.push({ fieldCode: code, valueText: JSON.stringify([{ id: value.id, name: value.name }]) }) | 849 | fields.push({ fieldCode: code, valueText: JSON.stringify([{ id: value.id, name: value.name }]) }) |
| 848 | } else if (type === 11) { | 850 | } else if (type === 11) { |
| 849 | - fields.push({ | ||
| 850 | - fieldCode: code, | ||
| 851 | - valueText: JSON.stringify({ lng: value.lng, lat: value.lat, address: value.address || '' }) | ||
| 852 | - }) | 851 | + fields.push({ fieldCode: code, valueText: JSON.stringify({ lng: value.lng, lat: value.lat, address: value.address }) }) |
| 853 | } else { | 852 | } else { |
| 854 | - // 2文本 / 4日期 / 5布尔 / 10链接 均为裸文本 | ||
| 855 | fields.push({ fieldCode: code, valueText: String(value) }) | 853 | fields.push({ fieldCode: code, valueText: String(value) }) |
| 856 | } | 854 | } |
| 857 | } | 855 | } |
| 858 | return fields | 856 | return fields |
| 859 | }, | 857 | }, |
| 860 | - /** | ||
| 861 | - * 提交养护记录 —— 双写两条链路: | ||
| 862 | - * ① maintain-plan-commit/create:养护计划本身(描述 + 图片 + 人员),返回 commit.id | ||
| 863 | - * ② work-record/submit:动态字段值,bizId 用 ① 的 commit.id(旧接口的 VO 没有动态字段,会被 Jackson 静默丢弃) | ||
| 864 | - * 重试安全:commitId 一旦取到就缓存住,重试只补写 ②(② 自身按 clientSeq 幂等),避免重复创建养护记录 | ||
| 865 | - */ | 858 | + |
| 866 | async submitInspect() { | 859 | async submitInspect() { |
| 867 | try { | 860 | try { |
| 868 | await this.$refs.inspectFormRef.validate() | 861 | await this.$refs.inspectFormRef.validate() |
| 869 | - const dynamicValues = this.buildDynamicFields() | 862 | + uni.showLoading({ title: '提交中' }) |
| 870 | if (!this.clientSeq) { | 863 | if (!this.clientSeq) { |
| 871 | - this.clientSeq = this.genClientSeq() | 864 | + this.clientSeq = Date.now().toString() + '-' + Math.random().toString(16).slice(2,10) |
| 872 | } | 865 | } |
| 873 | - uni.showLoading({ title: '提交中...' }) | ||
| 874 | - // ① 养护计划本体:仅首次提交时写入 | ||
| 875 | if (!this.commitId) { | 866 | if (!this.commitId) { |
| 876 | const submitData = { | 867 | const submitData = { |
| 877 | - planNo: this.paramsOptins.planNo, | 868 | + planNo: this.paramsOptions.planNo, |
| 878 | imgHost: "1", | 869 | imgHost: "1", |
| 879 | - beginImg: this.getImgUrlList(this.imagesList), | 870 | + beginImg: this.imageList.map(img => img.url), |
| 880 | commonUserList: [], | 871 | commonUserList: [], |
| 881 | - remark: this.inspectForm.remark.trim(), | ||
| 882 | - } | ||
| 883 | - const commitRes: any = await maintainCreate(submitData) | ||
| 884 | - // 兼容后端返回裸 id 或 {id:...} 两种形态 | ||
| 885 | - this.commitId = commitRes && typeof commitRes === 'object' ? commitRes.id : commitRes | ||
| 886 | - } | ||
| 887 | - // ② 动态字段值:无动态字段时不产生空的字段值记录 | ||
| 888 | - if (dynamicValues.length > 0) { | ||
| 889 | - if (!this.commitId) { | ||
| 890 | - console.warn('未取到养护计划的 commit.id,跳过动态字段提交') | ||
| 891 | - } else { | ||
| 892 | - await workRecordSubmit({ | ||
| 893 | - businessType: this.paramsOptins.businessType, | ||
| 894 | - bizType: 3, | ||
| 895 | - bizId: this.commitId, | ||
| 896 | - workTypeId: Number(this.paramsOptins.planTypeId), | ||
| 897 | - clientSeq: this.clientSeq, | ||
| 898 | - remark: this.inspectForm.remark.trim(), | ||
| 899 | - fields: dynamicValues | ||
| 900 | - }) | 872 | + remark: this.inspectForm.remark.trim() |
| 901 | } | 873 | } |
| 874 | + const createRes = await maintainCreate(submitData) | ||
| 875 | + this.commitId = createRes.id | ||
| 902 | } | 876 | } |
| 877 | + const dynamicValues = this.buildDynamicFields() | ||
| 878 | + await workRecordSubmit({ | ||
| 879 | + businessType: this.paramsOptions.businessType, | ||
| 880 | + planTypeId: this.paramsOptions.planTypeId, | ||
| 881 | + bizType:3, | ||
| 882 | + recordId: this.commitId, | ||
| 883 | + clientSeq: this.clientSeq, | ||
| 884 | + remark: this.inspectForm.remark.trim(), | ||
| 885 | + fields: dynamicValues | ||
| 886 | + }) | ||
| 903 | uni.hideLoading() | 887 | uni.hideLoading() |
| 904 | - uni.showToast({ title: '提交成功', icon: 'success', duration: 1000 }) | ||
| 905 | - setTimeout(() => { | ||
| 906 | - uni.reLaunch({ url: '/pages-sub/daily/maintain-manage/index' }) | ||
| 907 | - }, 1000) | ||
| 908 | - } catch (error) { | 888 | + uni.showToast({ title: '提交成功' }) |
| 889 | + setTimeout(()=>{ | ||
| 890 | + uni.reLaunch({url:'/pages/index/index'}) | ||
| 891 | + },1000) | ||
| 892 | + } catch (err) { | ||
| 909 | uni.hideLoading() | 893 | uni.hideLoading() |
| 910 | - // 表单校验失败时 error 是字段数组(uview-plus 已提示),不重复弹 | ||
| 911 | - if (Array.isArray(error)) { | ||
| 912 | - return | ||
| 913 | - } | ||
| 914 | - console.error('养护记录提交失败:', error) | ||
| 915 | - // request.js 已把后端 msg 弹过一次(如「库存不足」),此处不要用通用文案盖掉真实原因 | ||
| 916 | - const backendMsg = error && error.msg | 894 | + console.error('提交失败', err) |
| 895 | + if (Array.isArray(err)) return | ||
| 896 | + const msg = err?.msg || '提交失败,请重试' | ||
| 917 | if (this.commitId) { | 897 | if (this.commitId) { |
| 918 | - // 养护计划已落库,失败只发生在耗材字段。必须说清「已提交」,否则用户会以为整单没提交而重复填 | ||
| 919 | - uni.showToast({ | ||
| 920 | - title: backendMsg ? `养护记录已提交:${backendMsg}` : '养护记录已提交,耗材数据保存失败,请重试', | ||
| 921 | - icon: 'none', | ||
| 922 | - duration: 2500 | ||
| 923 | - }) | ||
| 924 | - } else if (!backendMsg) { | ||
| 925 | - uni.showToast({ title: '提交失败,请重试', icon: 'none', duration: 2000 }) | 898 | + uni.showToast({ title: `已创建工单,动态字段提交失败:${msg}`, icon: 'none' }) |
| 899 | + } else { | ||
| 900 | + uni.showToast({ title: msg, icon: 'none' }) | ||
| 926 | } | 901 | } |
| 927 | } | 902 | } |
| 928 | } | 903 | } |
| @@ -930,8 +905,13 @@ export default { | @@ -930,8 +905,13 @@ export default { | ||
| 930 | } | 905 | } |
| 931 | </script> | 906 | </script> |
| 932 | 907 | ||
| 908 | + | ||
| 909 | + | ||
| 910 | + | ||
| 933 | <style lang="scss" scoped> | 911 | <style lang="scss" scoped> |
| 934 | .page-container { | 912 | .page-container { |
| 913 | + min-height: 100vh; | ||
| 914 | + background-color: #f5f5f5; | ||
| 935 | padding-bottom: 120rpx; | 915 | padding-bottom: 120rpx; |
| 936 | } | 916 | } |
| 937 | .inspect-form-content { | 917 | .inspect-form-content { |
| @@ -942,24 +922,21 @@ export default { | @@ -942,24 +922,21 @@ export default { | ||
| 942 | .date-input-wrap { | 922 | .date-input-wrap { |
| 943 | width: 100%; | 923 | width: 100%; |
| 944 | } | 924 | } |
| 945 | -.fixed-bottom-btn-wrap { | 925 | +.fixed-bottom-wrap { |
| 946 | position: fixed; | 926 | position: fixed; |
| 947 | bottom: 0; | 927 | bottom: 0; |
| 948 | left: 0; | 928 | left: 0; |
| 949 | width: 100%; | 929 | width: 100%; |
| 950 | - padding: 20rpx; | ||
| 951 | - box-sizing: border-box; | ||
| 952 | - background-color: #fff; | 930 | + background: #fff; |
| 953 | } | 931 | } |
| 954 | -// 放大u-number-box输入框 | ||
| 955 | .large-number-box { | 932 | .large-number-box { |
| 956 | :deep(.u-number-box__input) { | 933 | :deep(.u-number-box__input) { |
| 957 | width: 264rpx !important; | 934 | width: 264rpx !important; |
| 958 | height: 72rpx; | 935 | height: 72rpx; |
| 959 | font-size: 32rpx; | 936 | font-size: 32rpx; |
| 960 | } | 937 | } |
| 961 | - :deep(.u-number-box__btn) { | ||
| 962 | - width: 64rpx; | 938 | + :deep(.u-number-box__minus), |
| 939 | + :deep(.u-number-box__plus) { | ||
| 963 | height: 72rpx; | 940 | height: 72rpx; |
| 964 | } | 941 | } |
| 965 | } | 942 | } |
| @@ -967,12 +944,11 @@ export default { | @@ -967,12 +944,11 @@ export default { | ||
| 967 | display: flex; | 944 | display: flex; |
| 968 | align-items: center; | 945 | align-items: center; |
| 969 | gap: 16rpx; | 946 | gap: 16rpx; |
| 970 | - .unit-text { | ||
| 971 | - font-size: 28rpx; | ||
| 972 | - color: #333; | ||
| 973 | - } | ||
| 974 | } | 947 | } |
| 975 | -// 6多选:复选框组 + needQuantity 时每个已选项一行数量输入 | 948 | +.unit-text { |
| 949 | + font-size: 28rpx; | ||
| 950 | + color: #333; | ||
| 951 | +} | ||
| 976 | .multi-wrap { | 952 | .multi-wrap { |
| 977 | width: 100%; | 953 | width: 100%; |
| 978 | .multi-hint { | 954 | .multi-hint { |
| @@ -987,7 +963,6 @@ export default { | @@ -987,7 +963,6 @@ export default { | ||
| 987 | border-left: 4rpx solid #f0f0f0; | 963 | border-left: 4rpx solid #f0f0f0; |
| 988 | } | 964 | } |
| 989 | } | 965 | } |
| 990 | -// 数量行(6多选的每个已选项 / 3物料的领用数量共用) | ||
| 991 | .qty-row { | 966 | .qty-row { |
| 992 | display: flex; | 967 | display: flex; |
| 993 | align-items: center; | 968 | align-items: center; |
| @@ -1006,28 +981,86 @@ export default { | @@ -1006,28 +981,86 @@ export default { | ||
| 1006 | padding-top: 12rpx; | 981 | padding-top: 12rpx; |
| 1007 | border-top: 1rpx solid #f0f0f0; | 982 | border-top: 1rpx solid #f0f0f0; |
| 1008 | } | 983 | } |
| 1009 | -// 8附件:自绘文件列表 + 选择按钮(微信无通用文件选择器,走 chooseMessageFile) | ||
| 1010 | .file-wrap { | 984 | .file-wrap { |
| 1011 | width: 100%; | 985 | width: 100%; |
| 1012 | .file-row { | 986 | .file-row { |
| 1013 | display: flex; | 987 | display: flex; |
| 1014 | align-items: center; | 988 | align-items: center; |
| 1015 | justify-content: space-between; | 989 | justify-content: space-between; |
| 1016 | - gap: 16rpx; | ||
| 1017 | - padding: 8rpx 0; | 990 | + gap:16rpx; |
| 991 | + padding:8rpx 0; | ||
| 1018 | .file-name { | 992 | .file-name { |
| 1019 | - flex: 1; | ||
| 1020 | - min-width: 0; | 993 | + flex:1; |
| 994 | + min-width:0; | ||
| 995 | + font-size:28rpx; | ||
| 996 | + color:#333; | ||
| 997 | + overflow:hidden; | ||
| 998 | + white-space:nowrap; | ||
| 999 | + text-overflow:ellipsis; | ||
| 1000 | + } | ||
| 1001 | + .file-del { | ||
| 1002 | + font-size:26rpx; | ||
| 1003 | + color:#f56c6c; | ||
| 1004 | + } | ||
| 1005 | + } | ||
| 1006 | +} | ||
| 1007 | + | ||
| 1008 | +// type6弹窗样式 | ||
| 1009 | +.popup-header { | ||
| 1010 | + display: flex; | ||
| 1011 | + justify-content: space-between; | ||
| 1012 | + align-items: center; | ||
| 1013 | + padding: 30rpx; | ||
| 1014 | + .popup-title { | ||
| 1015 | + font-size: 34rpx; | ||
| 1016 | + font-weight: bold; | ||
| 1017 | + } | ||
| 1018 | +} | ||
| 1019 | +.popup-body { | ||
| 1020 | + display: flex; | ||
| 1021 | + flex: 1; | ||
| 1022 | + height: calc(100vh - 220rpx); | ||
| 1023 | + .left-sidebar { | ||
| 1024 | + width: 220rpx; | ||
| 1025 | + background: #f6f7f8; | ||
| 1026 | + overflow-y: auto; | ||
| 1027 | + .sidebar-item { | ||
| 1028 | + padding: 24rpx 16rpx; | ||
| 1021 | font-size: 28rpx; | 1029 | font-size: 28rpx; |
| 1022 | color: #333; | 1030 | color: #333; |
| 1023 | - overflow: hidden; | ||
| 1024 | - white-space: nowrap; | ||
| 1025 | - text-overflow: ellipsis; | 1031 | + &.active { |
| 1032 | + background: #fff; | ||
| 1033 | + color: #007aff; | ||
| 1034 | + } | ||
| 1026 | } | 1035 | } |
| 1027 | - .file-del { | 1036 | + } |
| 1037 | + .right-content { | ||
| 1038 | + flex: 1; | ||
| 1039 | + background: #fff; | ||
| 1040 | + padding: 20rpx; | ||
| 1041 | + overflow-y: auto; | ||
| 1042 | + .list-item { | ||
| 1043 | + display: flex; | ||
| 1044 | + align-items: center; | ||
| 1045 | + gap:20rpx; | ||
| 1046 | + padding:20rpx 0; | ||
| 1047 | + border-bottom:1rpx solid #eee; | ||
| 1048 | + .row-label { | ||
| 1049 | + flex:1; | ||
| 1050 | + font-size:28rpx; | ||
| 1051 | + } | ||
| 1052 | + } | ||
| 1053 | + .tip-text { | ||
| 1054 | + margin-top: 20rpx; | ||
| 1028 | font-size: 26rpx; | 1055 | font-size: 26rpx; |
| 1029 | - color: #ff4d4f; | 1056 | + color:#999; |
| 1030 | } | 1057 | } |
| 1031 | } | 1058 | } |
| 1032 | } | 1059 | } |
| 1060 | +.popup-footer { | ||
| 1061 | + padding:24rpx; | ||
| 1062 | +} | ||
| 1033 | </style> | 1063 | </style> |
| 1064 | + | ||
| 1065 | + | ||
| 1066 | + |
pages/index/index.vue
| @@ -24,20 +24,51 @@ | @@ -24,20 +24,51 @@ | ||
| 24 | 24 | ||
| 25 | <!-- 任务完成情况卡片 --> | 25 | <!-- 任务完成情况卡片 --> |
| 26 | <view class="task-chart-card"> | 26 | <view class="task-chart-card"> |
| 27 | - <view class="card-header"> | ||
| 28 | - <view class="unit-tip">单位: 个</view> | ||
| 29 | - <view class="date-picker-wrap"> | ||
| 30 | - <neo-datetime-pro v-model="dateRange" type="daterange" :clearable="false" placeholder="请选择日期范围" | ||
| 31 | - @confirm="handleDateConfirm" :max-date="maxDate" @popup-show="handlePopupShow" | ||
| 32 | - @popup-hide="handlePopupHide" /> | ||
| 33 | - </view> | ||
| 34 | - </view> | 27 | + <view class="card-header"> |
| 28 | + <view class="unit-tip">数字指标</view> | ||
| 29 | + <!-- ActionSheet 点击唤起 --> | ||
| 30 | + <view class="sort-trigger" @click="openActionSheet"> | ||
| 31 | + {{ sortLabel }} | ||
| 32 | + <up-icon name="arrow-down" size="14" style="margin-left:4rpx;"></up-icon> | ||
| 33 | + </view> | ||
| 34 | + <!-- ActionSheet组件 --> | ||
| 35 | + <up-action-sheet | ||
| 36 | + :show="showActionSheet" | ||
| 37 | + :actions="actionSheetOptions" | ||
| 38 | + @select="handleSheetSelect" | ||
| 39 | + @close="showActionSheet = false" | ||
| 40 | + /> | ||
| 41 | + <view class="date-picker-wrap"> | ||
| 42 | + <neo-datetime-pro v-model="dateRange" type="daterange" :clearable="false" placeholder="请选择日期范围" | ||
| 43 | + @confirm="handleDateConfirm" :max-date="maxDate" @popup-show="handlePopupShow" | ||
| 44 | + @popup-hide="handlePopupHide" /> | ||
| 45 | + </view> | ||
| 46 | +</view> | ||
| 47 | + | ||
| 35 | 48 | ||
| 36 | <!-- 双折线K线图 --> | 49 | <!-- 双折线K线图 --> |
| 37 | - <view class="chart-container"> | 50 | + <!-- <view class="chart-container"> |
| 38 | <uni-charts v-if="!isDatePickerOpen" type="line" :data="klineChartData" :categories="klineCategories" | 51 | <uni-charts v-if="!isDatePickerOpen" type="line" :data="klineChartData" :categories="klineCategories" |
| 39 | :option="klineOption" :width="chartWidth" :height="chartHeight" /> | 52 | :option="klineOption" :width="chartWidth" :height="chartHeight" /> |
| 40 | - </view> | 53 | + </view> --> |
| 54 | + | ||
| 55 | + <up-grid | ||
| 56 | + :border="false" | ||
| 57 | + col="4" | ||
| 58 | + > | ||
| 59 | + <up-grid-item | ||
| 60 | + v-for="(baseListItem,baseListIndex) in baseList" | ||
| 61 | + :key="baseListIndex" | ||
| 62 | + > | ||
| 63 | + <up-icon | ||
| 64 | + :customStyle="{paddingTop:20+'rpx'}" | ||
| 65 | + :name="baseListItem.name" | ||
| 66 | + :size="22" | ||
| 67 | + ></up-icon> | ||
| 68 | + <text class="grid-text">{{baseListItem.num}}</text> | ||
| 69 | + <text class="grid-text">{{baseListItem.title}}</text> | ||
| 70 | + </up-grid-item> | ||
| 71 | + </up-grid> | ||
| 41 | </view> | 72 | </view> |
| 42 | </view> | 73 | </view> |
| 43 | 74 | ||
| @@ -131,6 +162,46 @@ type GroupItem = { | @@ -131,6 +162,46 @@ type GroupItem = { | ||
| 131 | showTipHeader: boolean; // 是否展示顶部进度条 | 162 | showTipHeader: boolean; // 是否展示顶部进度条 |
| 132 | useWalkText: boolean; // 是否展示步行距离文案 | 163 | useWalkText: boolean; // 是否展示步行距离文案 |
| 133 | }; | 164 | }; |
| 165 | + | ||
| 166 | +// 创建响应式数据 | ||
| 167 | +const baseList = ref([ | ||
| 168 | + { | ||
| 169 | + num:'123', | ||
| 170 | + name: 'photo', | ||
| 171 | + title: '应办任务数' | ||
| 172 | + }, | ||
| 173 | + { | ||
| 174 | + num:'32', | ||
| 175 | + name: 'photo', | ||
| 176 | + title: '已办任务数' | ||
| 177 | + }, | ||
| 178 | + { | ||
| 179 | + num:'456', | ||
| 180 | + name: 'lock', | ||
| 181 | + title: '待办任务数' | ||
| 182 | + }, | ||
| 183 | + { | ||
| 184 | + num:'0', | ||
| 185 | + name: 'star', | ||
| 186 | + title: '超时未完成' | ||
| 187 | + }, | ||
| 188 | +]); | ||
| 189 | + | ||
| 190 | +const options1 = ref([ | ||
| 191 | + { | ||
| 192 | + label: '默认排序', | ||
| 193 | + value: 1, | ||
| 194 | + }, | ||
| 195 | + { | ||
| 196 | + label: '距离优先', | ||
| 197 | + value: 2, | ||
| 198 | + }, | ||
| 199 | + { | ||
| 200 | + label: '价格优先', | ||
| 201 | + value: 3, | ||
| 202 | + } | ||
| 203 | +]); | ||
| 204 | + | ||
| 134 | const groupConfig: GroupItem[] = [ | 205 | const groupConfig: GroupItem[] = [ |
| 135 | // 组2 A1B2 纯效率·有提醒 | 206 | // 组2 A1B2 纯效率·有提醒 |
| 136 | { | 207 | { |
| @@ -263,6 +334,35 @@ const klineOption = ref({ | @@ -263,6 +334,35 @@ const klineOption = ref({ | ||
| 263 | } | 334 | } |
| 264 | }); | 335 | }); |
| 265 | 336 | ||
| 337 | +const value1 = ref(1) | ||
| 338 | +const showActionSheet = ref(false) | ||
| 339 | + | ||
| 340 | +// actionSheet选项映射 | ||
| 341 | +const actionSheetOptions = computed(()=>{ | ||
| 342 | + return options1.value.map(opt => ({ | ||
| 343 | + name: opt.label, | ||
| 344 | + value: opt.value | ||
| 345 | + })) | ||
| 346 | +}) | ||
| 347 | +// 显示当前选中的文字 | ||
| 348 | +const sortLabel = computed(()=>{ | ||
| 349 | + const cur = options1.value.find(i=>i.value === value1.value) | ||
| 350 | + return cur?.label ?? "默认排序" | ||
| 351 | +}) | ||
| 352 | + | ||
| 353 | +// 打开菜单 | ||
| 354 | +const openActionSheet = ()=>{ | ||
| 355 | + showActionSheet.value = true | ||
| 356 | +} | ||
| 357 | +// 选中回调 | ||
| 358 | +const handleSheetSelect = (item: {name:string, value:number})=>{ | ||
| 359 | + value1.value = item.value | ||
| 360 | + showActionSheet.value = false | ||
| 361 | + // ========== 在这里添加排序业务逻辑 ========== | ||
| 362 | + console.log('选中排序类型:', item.value) | ||
| 363 | +} | ||
| 364 | + | ||
| 365 | + | ||
| 266 | const userName = computed(() => userStore.userInfo?.user?.nickname || '用户'); | 366 | const userName = computed(() => userStore.userInfo?.user?.nickname || '用户'); |
| 267 | 367 | ||
| 268 | const rpx2px = (rpx: number) => { | 368 | const rpx2px = (rpx: number) => { |
| @@ -621,7 +721,6 @@ $border-color: #e5e5e5; | @@ -621,7 +721,6 @@ $border-color: #e5e5e5; | ||
| 621 | color: $text-color-placeholder; | 721 | color: $text-color-placeholder; |
| 622 | margin-bottom: $spacing-md; | 722 | margin-bottom: $spacing-md; |
| 623 | .date-picker-wrap { | 723 | .date-picker-wrap { |
| 624 | - padding: 2px 4px; | ||
| 625 | border-radius: 3px; | 724 | border-radius: 3px; |
| 626 | transition: background-color 0.2s; | 725 | transition: background-color 0.2s; |
| 627 | position: relative; | 726 | position: relative; |
| @@ -708,6 +807,13 @@ $border-color: #e5e5e5; | @@ -708,6 +807,13 @@ $border-color: #e5e5e5; | ||
| 708 | .empty-box { | 807 | .empty-box { |
| 709 | padding: 60rpx 0; | 808 | padding: 60rpx 0; |
| 710 | } | 809 | } |
| 810 | +.sort-trigger { | ||
| 811 | + display: flex; | ||
| 812 | + align-items: center; | ||
| 813 | + font-size: 12px; | ||
| 814 | + color: $text-color-placeholder; | ||
| 815 | +} | ||
| 816 | + | ||
| 711 | 817 | ||
| 712 | .task-item { | 818 | .task-item { |
| 713 | padding: $spacing-md; | 819 | padding: $spacing-md; |
pages/login/index.vue
| @@ -406,10 +406,15 @@ const handleLogin = async () => { | @@ -406,10 +406,15 @@ const handleLogin = async () => { | ||
| 406 | loginParams.mobile = form.mobile; | 406 | loginParams.mobile = form.mobile; |
| 407 | loginParams.code = form.code; | 407 | loginParams.code = form.code; |
| 408 | loginParams.type = 'sms'; | 408 | loginParams.type = 'sms'; |
| 409 | + loginParams.tenantName='蓟城山水集团' | ||
| 410 | + | ||
| 411 | + loginParams.rememberMe= true | ||
| 409 | } else { | 412 | } else { |
| 410 | loginParams.username = form.account; | 413 | loginParams.username = form.account; |
| 411 | loginParams.password = form.password; | 414 | loginParams.password = form.password; |
| 412 | loginParams.type = 'password'; | 415 | loginParams.type = 'password'; |
| 416 | + loginParams.tenantName='蓟城山水集团' | ||
| 417 | + loginParams.rememberMe= true | ||
| 413 | } | 418 | } |
| 414 | 419 | ||
| 415 | // 执行登录 | 420 | // 执行登录 |
uni_modules/neo-datetime-pro/components/neo-datetime-pro/neo-datetime-pro.vue
| @@ -867,7 +867,7 @@ export default { | @@ -867,7 +867,7 @@ export default { | ||
| 867 | display: flex; | 867 | display: flex; |
| 868 | align-items: center; | 868 | align-items: center; |
| 869 | justify-content: space-between; | 869 | justify-content: space-between; |
| 870 | - padding: 4rpx 32rpx; | 870 | + padding: 4rpx 12rpx; |
| 871 | background: var(--neo-color-bg-elevated, #fff); | 871 | background: var(--neo-color-bg-elevated, #fff); |
| 872 | border: 2rpx solid var(--neo-color-border, rgba(0, 0, 0, 0.08)); | 872 | border: 2rpx solid var(--neo-color-border, rgba(0, 0, 0, 0.08)); |
| 873 | border-radius: 12rpx; | 873 | border-radius: 12rpx; |
| @@ -894,7 +894,7 @@ export default { | @@ -894,7 +894,7 @@ export default { | ||
| 894 | } | 894 | } |
| 895 | 895 | ||
| 896 | .trigger-value { | 896 | .trigger-value { |
| 897 | - font-size: 28rpx; | 897 | + font-size: 24rpx; |
| 898 | 898 | ||
| 899 | } | 899 | } |
| 900 | 900 |