Commit 629df8b3a9663e265004f97b7567cfea234e3058
1 parent
ee792d52
业务线 小程序不用写死,从后端接口获取
Showing
1 changed file
with
696 additions
and
729 deletions
pages-sub/daily/maintain-manage/add-record.vue
| @@ -4,541 +4,233 @@ | @@ -4,541 +4,233 @@ | ||
| 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 | ||
| 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 --> | 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 --> | ||
| 15 | <view v-if="item.fieldType === 1" class="number-box-wrap"> | 10 | <view v-if="item.fieldType === 1" class="number-box-wrap"> |
| 16 | - <u-number-box | ||
| 17 | - v-model="inspectForm[item.fieldCode]" | ||
| 18 | - min="1" | ||
| 19 | - step="1" | ||
| 20 | - :max="item.length ? 10 ** Number(item.length) - 1 : null" | ||
| 21 | - :placeholder="`请输入${item.fieldName}${item.unit ? item.unit : ''}`" | ||
| 22 | - @change="valChange(item)" | ||
| 23 | - class="large-number-box" | ||
| 24 | - ></u-number-box> | 11 | + <u-number-box v-model="inspectForm[item.fieldCode]" min="1" :step="1" |
| 12 | + :max="item.length ? 10 ** Number(item.length) - 1 : null" | ||
| 13 | + :placeholder="`请输入${item.fieldName}${item.unit} `" @change="valChange(item)" class="large-number-box"> | ||
| 14 | + </u-number-box> | ||
| 25 | <text class="unit-text" v-if="item.unit">{{ item.unit }}</text> | 15 | <text class="unit-text" v-if="item.unit">{{ item.unit }}</text> |
| 26 | </view> | 16 | </view> |
| 27 | - | ||
| 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> | ||
| 44 | - </u-checkbox-group> | ||
| 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 | - > | 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"> | ||
| 55 | <template #right> | 24 | <template #right> |
| 56 | <up-icon name="arrow-right" size="16"></up-icon> | 25 | <up-icon name="arrow-right" size="16"></up-icon> |
| 57 | </template> | 26 | </template> |
| 58 | </up-input> | 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"> | ||
| 59 | 32 | ||
| 60 | - <!-- type4 日期时间 --> | ||
| 61 | - <view v-else-if="item.fieldType === 4" @click.stop="openDatePicker(item)" class="date-input-wrap"> | ||
| 62 | - <up-input | ||
| 63 | - v-model="inspectForm[item.fieldCode]" | ||
| 64 | - disabled | ||
| 65 | - placeholder="点击选择时间" | ||
| 66 | - border="none" | ||
| 67 | - ></up-input> | ||
| 68 | - </view> | ||
| 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 | - > | 33 | + </u-checkbox> |
| 34 | + </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"> | ||
| 85 | <template #right> | 38 | <template #right> |
| 86 | <up-icon name="arrow-right" size="16"></up-icon> | 39 | <up-icon name="arrow-right" size="16"></up-icon> |
| 87 | </template> | 40 | </template> |
| 88 | </up-input> | 41 | </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 附件 --> | 42 | + <!-- type4 日期时间 --> |
| 43 | + <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> | ||
| 45 | + </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) --> | ||
| 103 | <view v-else-if="item.fieldType === 8" class="file-wrap"> | 74 | <view v-else-if="item.fieldType === 8" class="file-wrap"> |
| 104 | - <view | ||
| 105 | - v-for="(file, idx) in fileListMap[item.fieldCode] || []" | ||
| 106 | - :key="idx" | ||
| 107 | - class="file-row" | ||
| 108 | - > | 75 | + <view v-for="(file, idx) in fileListMap[item.fieldCode] || []" :key="idx" class="file-row"> |
| 109 | <text class="file-name">{{ file.name || fileNameOf(file.url) }}</text> | 76 | <text class="file-name">{{ file.name || fileNameOf(file.url) }}</text> |
| 110 | - <text class="file-del" @click.stop="deleteFieldFile(item, {index: idx})">删除</text> | 77 | + <text class="file-del" @click.stop="deleteFieldFile(item, { index: idx })">删除</text> |
| 111 | </view> | 78 | </view> |
| 112 | - <up-button | ||
| 113 | - size="small" | ||
| 114 | - type="primary" | ||
| 115 | - plain | ||
| 116 | - :text="fileButtonText(item)" | ||
| 117 | - @click.stop="chooseFieldFiles(item)" | ||
| 118 | - ></up-button> | 79 | + <up-button size="small" type="primary" plain :text="fileButtonText(item)" |
| 80 | + @click.stop="chooseFieldFiles(item)"></up-button> | ||
| 119 | </view> | 81 | </view> |
| 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 | - > | 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"> | ||
| 130 | <template #right> | 85 | <template #right> |
| 131 | <up-icon name="arrow-right" size="16"></up-icon> | 86 | <up-icon name="arrow-right" size="16"></up-icon> |
| 132 | </template> | 87 | </template> |
| 133 | </up-input> | 88 | </up-input> |
| 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 | - > | 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"> | ||
| 151 | <template #right> | 95 | <template #right> |
| 152 | <up-icon name="arrow-right" size="16"></up-icon> | 96 | <up-icon name="arrow-right" size="16"></up-icon> |
| 153 | </template> | 97 | </template> |
| 154 | </up-input> | 98 | </up-input> |
| 155 | - | ||
| 156 | - <!-- 物料附加数量输入框 type3 + optionSource=5 --> | ||
| 157 | - <view v-if="item.fieldType === 3 && item.optionSource === 5" class="qty-row qty-row-material"> | 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"> | ||
| 158 | <text class="qty-label">领用数量</text> | 102 | <text class="qty-label">领用数量</text> |
| 159 | - <u-number-box | ||
| 160 | - v-model="qtyMap[item.fieldCode]" | ||
| 161 | - min="0" | ||
| 162 | - step="1" | ||
| 163 | - ></u-number-box> | 103 | + <u-number-box v-model="qtyMap[item.fieldCode]" :min="0" :step="1"></u-number-box> |
| 164 | </view> | 104 | </view> |
| 165 | </up-form-item> | 105 | </up-form-item> |
| 166 | </template> | 106 | </template> |
| 167 | <!-- ========== 动态表单结束 ========== --> | 107 | <!-- ========== 动态表单结束 ========== --> |
| 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> | 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> | ||
| 177 | </up-form-item> | 111 | </up-form-item> |
| 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> | 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> | ||
| 190 | </up-form-item> | 117 | </up-form-item> |
| 191 | </up-form> | 118 | </up-form> |
| 192 | </view> | 119 | </view> |
| 193 | - | ||
| 194 | <!-- 底部提交按钮 --> | 120 | <!-- 底部提交按钮 --> |
| 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> | 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> | ||
| 202 | </view> | 124 | </view> |
| 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 | - | 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> | ||
| 214 | <!-- ActionSheet 底部选择弹窗 --> | 128 | <!-- ActionSheet 底部选择弹窗 --> |
| 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> | 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> | ||
| 294 | </view> | 137 | </view> |
| 295 | </template> | 138 | </template> |
| 296 | 139 | ||
| 297 | - | ||
| 298 | - | ||
| 299 | - | ||
| 300 | <script lang="ts"> | 140 | <script lang="ts"> |
| 301 | import { uploadImages } from '@/common/utils/upload' | 141 | import { uploadImages } from '@/common/utils/upload' |
| 302 | import { maintainCreate } from "@/api/maintain-manage/maintain-manage" | 142 | import { maintainCreate } from "@/api/maintain-manage/maintain-manage" |
| 303 | import { getByType, getOptionsByCode, getMaterialByCode, workRecordSubmit } from "@/api/common" | 143 | import { getByType, getOptionsByCode, getMaterialByCode, workRecordSubmit } from "@/api/common" |
| 304 | - | ||
| 305 | export default { | 144 | export default { |
| 306 | data() { | 145 | data() { |
| 307 | return { | 146 | return { |
| 308 | - imageList: [], | 147 | + imagesList: [], |
| 309 | initProgress: 0, | 148 | initProgress: 0, |
| 310 | inspectForm: { | 149 | inspectForm: { |
| 311 | remark: '', | 150 | remark: '', |
| 312 | }, | 151 | }, |
| 152 | + // ========== 新增:用来存放展示文本label的映射对象 ========== | ||
| 313 | labelMap: {}, | 153 | labelMap: {}, |
| 154 | + // 类型7图片/8附件的每字段文件列表(fieldCode -> u-upload 兼容数组 {url,status,name}) | ||
| 314 | fileListMap: {}, | 155 | fileListMap: {}, |
| 156 | + // 数量收集:类型6+needQuantity 为 {选项值: 数量};类型3物料为标量(领用数量) | ||
| 315 | qtyMap: {}, | 157 | qtyMap: {}, |
| 316 | - paramsOptions: {}, | 158 | + paramsOptins: {}, |
| 159 | + // 幂等流水号:本次填报页面生命周期内保持不变,重复提交(如网络超时后重试)只会落一条记录 | ||
| 317 | clientSeq: '', | 160 | clientSeq: '', |
| 161 | + // 养护计划本体的 commit.id:取到后缓存,重试时不再重复创建养护记录 | ||
| 318 | commitId: null, | 162 | commitId: null, |
| 319 | inspectFormRules: { | 163 | inspectFormRules: { |
| 320 | images: [ | 164 | images: [ |
| 321 | { | 165 | { |
| 322 | required: true, | 166 | required: true, |
| 323 | - message: '最少上传1张图片', | 167 | + message: '请上传图片', |
| 324 | trigger: 'change', | 168 | trigger: 'change', |
| 325 | validator: (rule, value, callback) => { | 169 | validator: (rule, value, callback) => { |
| 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张图片')) | 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张图片')) | ||
| 332 | } else { | 176 | } else { |
| 333 | callback() | 177 | callback() |
| 334 | } | 178 | } |
| 335 | } | 179 | } |
| 336 | } | 180 | } |
| 337 | - ] | 181 | + ], |
| 338 | }, | 182 | }, |
| 339 | - dynamicFields: [], | ||
| 340 | - | 183 | + // 动态字段列表 |
| 184 | + dynamicFields: [] as any[], | ||
| 185 | + // 日期时间选择器 | ||
| 341 | datePickerShow: false, | 186 | datePickerShow: false, |
| 342 | - datePickerValue: new Date().getTime(), | 187 | + datePickerValue: Number(new Date()), |
| 343 | currentDateFieldCode: '', | 188 | currentDateFieldCode: '', |
| 344 | - | ||
| 345 | - actionSheetShow: false, | ||
| 346 | - actionSheetOptions: [], | ||
| 347 | - actionSheetFieldItem: null, | ||
| 348 | - | ||
| 349 | - // ===== type6弹窗状态 ===== | ||
| 350 | - popupType6: { | 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: { | ||
| 351 | show: false, | 198 | show: false, |
| 352 | fieldCode: '', | 199 | fieldCode: '', |
| 353 | - fieldItem: null, | ||
| 354 | - activeTab: 0, | ||
| 355 | - tabs: [], | ||
| 356 | - activeSubKey: '', | ||
| 357 | - subMenuList: [], | ||
| 358 | - allRawData: [], | ||
| 359 | - rightList: [] | 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: '' | ||
| 360 | } | 209 | } |
| 361 | } | 210 | } |
| 362 | }, | 211 | }, |
| 363 | onLoad(option) { | 212 | onLoad(option) { |
| 364 | console.log('页面参数:', option) | 213 | console.log('页面参数:', option) |
| 365 | - this.paramsOptions = option | 214 | + this.paramsOptins = option |
| 366 | const { businessType, planTypeId } = option | 215 | const { businessType, planTypeId } = option |
| 367 | - const workTypeId = planTypeId | ||
| 368 | - this.loadDynamicFields(businessType, workTypeId) | 216 | + this.loadDynamicFields(businessType, planTypeId) |
| 369 | }, | 217 | }, |
| 370 | onReady() { | 218 | onReady() { |
| 371 | this.$refs.inspectFormRef.setRules(this.inspectFormRules) | 219 | this.$refs.inspectFormRef.setRules(this.inspectFormRules) |
| 372 | - console.log('表单规则加载完成') | 220 | + console.log('巡查表单规则初始化完成') |
| 373 | }, | 221 | }, |
| 374 | methods: { | 222 | methods: { |
| 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 | - | 223 | + // number-box数值变更回调 |
| 534 | valChange(item) { | 224 | valChange(item) { |
| 225 | + // 数值修改后触发表单校验 | ||
| 535 | this.$refs.inspectFormRef.validateField(item.fieldCode) | 226 | this.$refs.inspectFormRef.validateField(item.fieldCode) |
| 536 | }, | 227 | }, |
| 537 | - | 228 | + // ========== 日期时间相关方法 ========== |
| 538 | openDatePicker(field) { | 229 | openDatePicker(field) { |
| 230 | + console.log('打开日期选择器', field) | ||
| 539 | this.currentDateFieldCode = field.fieldCode | 231 | this.currentDateFieldCode = field.fieldCode |
| 540 | const val = this.inspectForm[field.fieldCode] | 232 | const val = this.inspectForm[field.fieldCode] |
| 541 | - this.datePickerValue = val ? new Date(val).getTime() : new Date().getTime() | 233 | + this.datePickerValue = val ? new Date(val).getTime() : Number(new Date()) |
| 542 | this.datePickerShow = true | 234 | this.datePickerShow = true |
| 543 | uni.hideKeyboard() | 235 | uni.hideKeyboard() |
| 544 | }, | 236 | }, |
| @@ -558,71 +250,158 @@ export default { | @@ -558,71 +250,158 @@ export default { | ||
| 558 | onDateClose() { | 250 | onDateClose() { |
| 559 | this.datePickerShow = false | 251 | this.datePickerShow = false |
| 560 | }, | 252 | }, |
| 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 | 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 | ||
| 569 | }, | 304 | }, |
| 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 } | 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 | ||
| 338 | + }, | ||
| 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 } | ||
| 575 | } else { | 348 | } else { |
| 576 | - this.inspectForm[fieldCode] = selected.value | 349 | + this.inspectForm[fieldCode] = selectedItem.value |
| 577 | } | 350 | } |
| 578 | - this.labelMap[fieldCode] = selected.name | ||
| 579 | - this.actionSheetShow = false | 351 | + // 2. labelMap保存label,给input展示 |
| 352 | + this.labelMap[fieldCode] = selectedItem.name | ||
| 353 | + this.actionSheet.show = false | ||
| 354 | + // 选中后触发表单校验 | ||
| 580 | this.$refs.inspectFormRef.validateField(fieldCode) | 355 | this.$refs.inspectFormRef.validateField(fieldCode) |
| 581 | }, | 356 | }, |
| 582 | - | ||
| 583 | - openLocationPicker(item) { | 357 | + /** |
| 358 | + * 类型11 地理位置:微信地图选点(无需地图 AK,manifest.json 已声明 requiredPrivateInfos) | ||
| 359 | + * 提交形态 {lng,lat,address} | ||
| 360 | + */ | ||
| 361 | + openLocation(item: any) { | ||
| 584 | uni.chooseLocation({ | 362 | uni.chooseLocation({ |
| 585 | success: (res) => { | 363 | success: (res) => { |
| 586 | this.inspectForm[item.fieldCode] = { | 364 | this.inspectForm[item.fieldCode] = { |
| 587 | lng: res.longitude, | 365 | lng: res.longitude, |
| 588 | lat: res.latitude, | 366 | lat: res.latitude, |
| 589 | - address: res.address | 367 | + address: res.address || res.name || '' |
| 590 | } | 368 | } |
| 591 | - this.labelMap[item.fieldCode] = res.address || `${res.latitude},${res.longitude}` | 369 | + this.labelMap[item.fieldCode] = res.address || res.name || `${res.latitude}, ${res.longitude}` |
| 592 | this.$refs.inspectFormRef.validateField(item.fieldCode) | 370 | this.$refs.inspectFormRef.validateField(item.fieldCode) |
| 593 | }, | 371 | }, |
| 594 | fail: (err) => { | 372 | fail: (err) => { |
| 595 | - console.error('位置选择失败', err) | ||
| 596 | - if (err.errMsg.indexOf('auth deny') > -1) { | ||
| 597 | - uni.showToast({ title: '请开启位置权限', icon: 'none' }) | 373 | + console.error('地图选择失败', err) |
| 374 | + if (err && err.errMsg && err.errMsg.indexOf('auth deny') > -1) { | ||
| 375 | + uni.showToast({ title: '请授权位置权限', icon: 'none' }) | ||
| 598 | } | 376 | } |
| 599 | } | 377 | } |
| 600 | }) | 378 | }) |
| 601 | }, | 379 | }, |
| 602 | - | ||
| 603 | - // 加载动态字段 & 初始化inspectForm | 380 | + /** |
| 381 | + * 加载动态字段 | ||
| 382 | + */ | ||
| 604 | async loadDynamicFields(businessType: string, workTypeId: string) { | 383 | async loadDynamicFields(businessType: string, workTypeId: string) { |
| 605 | if (!businessType || !workTypeId) { | 384 | if (!businessType || !workTypeId) { |
| 606 | - uni.showToast({ title: '缺少参数', icon: 'none' }) | 385 | + uni.showToast({ title: '缺少业务参数', icon: 'none' }) |
| 607 | return | 386 | return |
| 608 | } | 387 | } |
| 609 | - | ||
| 610 | - const bizType = 3 | 388 | + const bizType = 3 |
| 611 | try { | 389 | try { |
| 612 | - const res = await getByType({ businessType, bizType,workTypeId }) | 390 | + const res = await getByType({ |
| 391 | + businessType, | ||
| 392 | + workTypeId, | ||
| 393 | + bizType | ||
| 394 | + }) | ||
| 613 | let fields = res.fields || [] | 395 | let fields = res.fields || [] |
| 614 | fields.sort((a, b) => a.sort - b.sort) | 396 | fields.sort((a, b) => a.sort - b.sort) |
| 615 | - this.dynamicFields = fields | ||
| 616 | - | 397 | + if (fields.length === 0) { |
| 398 | + uni.showToast({ title: '暂无需填报项', icon: 'none' }) | ||
| 399 | + this.dynamicFields = [] | ||
| 400 | + return | ||
| 401 | + } | ||
| 617 | for (const field of fields) { | 402 | for (const field of fields) { |
| 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) { | 403 | + // 初值:多选为数组;9关联记录/11地理位置 为对象(未选为 null);7图片/8附件为 URL 数组;其余空字符串 |
| 404 | + if (field.fieldType === 6 || (field.fieldType === 3 && field.multiple === 1)) { | ||
| 626 | this.inspectForm[field.fieldCode] = [] | 405 | this.inspectForm[field.fieldCode] = [] |
| 627 | } else if (field.fieldType === 7 || field.fieldType === 8) { | 406 | } else if (field.fieldType === 7 || field.fieldType === 8) { |
| 628 | this.inspectForm[field.fieldCode] = [] | 407 | this.inspectForm[field.fieldCode] = [] |
| @@ -631,161 +410,387 @@ export default { | @@ -631,161 +410,387 @@ export default { | ||
| 631 | } else { | 410 | } else { |
| 632 | this.inspectForm[field.fieldCode] = '' | 411 | this.inspectForm[field.fieldCode] = '' |
| 633 | } | 412 | } |
| 413 | + // 初始化展示label为空 | ||
| 634 | this.labelMap[field.fieldCode] = '' | 414 | this.labelMap[field.fieldCode] = '' |
| 415 | + // 7图片/8附件的文件列表(驱动 up-upload 与自绘文件列表展示) | ||
| 635 | if (field.fieldType === 7 || field.fieldType === 8) { | 416 | if (field.fieldType === 7 || field.fieldType === 8) { |
| 636 | this.fileListMap[field.fieldCode] = [] | 417 | this.fileListMap[field.fieldCode] = [] |
| 637 | } | 418 | } |
| 638 | - if (field.fieldType === 3 || field.fieldType === 6) { | 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) { | ||
| 639 | if (field.optionSource === 1 && Array.isArray(field.options)) { | 422 | if (field.optionSource === 1 && Array.isArray(field.options)) { |
| 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 => ({ | 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 getMaterialByCode({ | ||
| 427 | + businessType, | ||
| 428 | + bizType, | ||
| 429 | + workTypeId, | ||
| 430 | + fieldCode: field.fieldCode | ||
| 431 | + }) | ||
| 432 | + field.selectOptions = (matRes || []).map(mat => ({ | ||
| 647 | label: mat.materialName, | 433 | label: mat.materialName, |
| 648 | - value: String(mat.materialId) | 434 | + value: String(mat.materialId), |
| 435 | + currentNum: mat.currentNum, | ||
| 436 | + unit: mat.unit | ||
| 649 | })) | 437 | })) |
| 650 | } else { | 438 | } else { |
| 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 | - })) | 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 })) | ||
| 656 | } | 447 | } |
| 657 | } | 448 | } |
| 658 | - // 初始化数量映射 | 449 | + // 数量收集初始化(必须在 selectOptions 装载之后): |
| 450 | + // 6多选+needQuantity 预建每个选项的键,避免模板 v-model 写到未定义的键上; | ||
| 451 | + // 3物料为标量(领用数量),默认 0 = 本次不领用、不扣库存 | ||
| 659 | if (field.fieldType === 6 && this.needQty(field)) { | 452 | if (field.fieldType === 6 && this.needQty(field)) { |
| 660 | - const qtyObj = {} | 453 | + const qty = {} |
| 661 | ;(field.selectOptions || []).forEach(opt => { | 454 | ;(field.selectOptions || []).forEach(opt => { |
| 662 | - qtyObj[opt.value] = '' | 455 | + qty[opt.value] = '' |
| 663 | }) | 456 | }) |
| 664 | - this.qtyMap[field.fieldCode] = qtyObj | 457 | + this.qtyMap[field.fieldCode] = qty |
| 665 | } | 458 | } |
| 666 | - if (field.fieldType === 3 && field.optionSource === 5) { | 459 | + if (field.fieldType === 3 && field.optionSource === 3) { |
| 667 | this.qtyMap[field.fieldCode] = 0 | 460 | this.qtyMap[field.fieldCode] = 0 |
| 668 | } | 461 | } |
| 669 | - // 构建表单校验规则 | 462 | + // 必填规则 |
| 670 | if (field.required === 1) { | 463 | if (field.required === 1) { |
| 671 | - this.inspectFormRules[field.fieldCode] = this.buildRequiredRule(field) | 464 | + this.inspectFormRules[field.fieldCode] = [this.buildRequiredRule(field)] |
| 672 | } | 465 | } |
| 673 | } | 466 | } |
| 467 | + this.dynamicFields = fields | ||
| 674 | this.$nextTick(() => { | 468 | this.$nextTick(() => { |
| 675 | this.$refs.inspectFormRef.setRules(this.inspectFormRules) | 469 | this.$refs.inspectFormRef.setRules(this.inspectFormRules) |
| 676 | }) | 470 | }) |
| 677 | } catch (err) { | 471 | } catch (err) { |
| 678 | - console.error('加载动态表单失败', err) | ||
| 679 | - uni.showToast({ title: '加载表单失败', icon: 'none' }) | 472 | + console.error('获取动态表单字段失败:', err) |
| 473 | + uni.showToast({ title: '加载表单配置失败', icon: 'none' }) | ||
| 680 | } | 474 | } |
| 681 | }, | 475 | }, |
| 682 | - | ||
| 683 | - needQty(item) { | 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') | ||
| 528 | + } | ||
| 529 | + }, | ||
| 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) { | ||
| 684 | return !!(item.extConfig && item.extConfig.needQuantity === true) | 542 | return !!(item.extConfig && item.extConfig.needQuantity === true) |
| 685 | }, | 543 | }, |
| 686 | - pickCountOf(item, defaultNum) { | ||
| 687 | - const max = item.extConfig?.maxCount | ||
| 688 | - return max || defaultNum | 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 | ||
| 689 | }, | 568 | }, |
| 690 | - fileButtonText(item) { | ||
| 691 | - const exts = this.extListOf(item) | ||
| 692 | - return exts.length ? `选择文件(${exts.join(',')})` : '选择文件' | 569 | + /** 类型8 允许的扩展名数组(ext_config.fileType,不含点) */ |
| 570 | + extensionsOf(item: any) { | ||
| 571 | + const list = item.extConfig && item.extConfig.fileType | ||
| 572 | + return Array.isArray(list) ? list : [] | ||
| 693 | }, | 573 | }, |
| 694 | - extListOf(item) { | ||
| 695 | - return [] | 574 | + fileButtonText(item: any) { |
| 575 | + const exts = this.extensionsOf(item) | ||
| 576 | + return exts.length ? `选择文件(${exts.join('/')})` : '选择文件' | ||
| 696 | }, | 577 | }, |
| 697 | - fileNameOf(url) { | 578 | + /** 从 URL 取末段当文件名(与 PC 端 FieldValueCell 同口径) */ |
| 579 | + fileNameOf(url: string) { | ||
| 698 | if (!url) return '' | 580 | if (!url) return '' |
| 699 | - const path = String(url).split('?')[0] | 581 | + const path = String(url).split('?')[0].split('#')[0] |
| 700 | const name = path.substring(path.lastIndexOf('/') + 1) | 582 | const name = path.substring(path.lastIndexOf('/') + 1) |
| 701 | try { | 583 | try { |
| 702 | - return decodeURIComponent(name) | ||
| 703 | - } catch { | ||
| 704 | - return name | 584 | + return decodeURIComponent(name) || url |
| 585 | + } catch (err) { | ||
| 586 | + return name || url | ||
| 705 | } | 587 | } |
| 706 | }, | 588 | }, |
| 707 | - | ||
| 708 | - buildRequiredRule(field) { | 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 | ||
| 603 | + } | ||
| 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 | + }, | ||
| 619 | + /** | ||
| 620 | + * 必填规则。 | ||
| 621 | + * 7/8/9/11 的值是数组或对象,async-validator 的 required 按默认 string 类型判定, | ||
| 622 | + * 会放过「空数组」「null」⇒ 必须用 validator 显式判空,否则未选也能提交 | ||
| 623 | + */ | ||
| 624 | + buildRequiredRule(field: any) { | ||
| 709 | const type = field.fieldType | 625 | const type = field.fieldType |
| 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 | 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}`)) | ||
| 728 | } | 633 | } |
| 729 | - pass ? callback() : callback(new Error(`${action}${field.fieldName}`)) | ||
| 730 | } | 634 | } |
| 731 | } | 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}的位置`)) | ||
| 642 | + } | ||
| 643 | + } | ||
| 644 | + } | ||
| 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' } | ||
| 732 | }, | 648 | }, |
| 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') | ||
| 743 | - }, | ||
| 744 | - deleteImage(event) { | ||
| 745 | - const index = event.index | ||
| 746 | - this.imageList.splice(index, 1) | ||
| 747 | - this.$refs.inspectFormRef.validateField('images') | 649 | + /** 幂等流水号(后端按 userId+clientSeq 去重,命中则返回已存在的 recordId) */ |
| 650 | + genClientSeq() { | ||
| 651 | + return `${Date.now()}-${Math.random().toString(16).slice(2, 10)}` | ||
| 748 | }, | 652 | }, |
| 749 | - handleExceed() { | ||
| 750 | - uni.showToast({ title: '最多上传3张图片', icon: 'none' }) | 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 | ||
| 751 | }, | 662 | }, |
| 752 | - | ||
| 753 | - uploadFieldFiles(item, event) { | ||
| 754 | - // 附件上传逻辑 | 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) | ||
| 755 | }, | 667 | }, |
| 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) | 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 | + }))) | ||
| 761 | }, | 679 | }, |
| 762 | - chooseFieldFiles(item) { | 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 | + } | ||
| 763 | uni.chooseMessageFile({ | 696 | uni.chooseMessageFile({ |
| 764 | - count: this.pickCountOf(item,5), | ||
| 765 | - type: 'file', | 697 | + ...options, |
| 766 | success: (res) => { | 698 | success: (res) => { |
| 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) | 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 | + }))) | ||
| 776 | }, | 705 | }, |
| 777 | fail: (err) => { | 706 | fail: (err) => { |
| 778 | - console.error('选择附件失败', err) | 707 | + console.error('选择附件失败:', err) |
| 779 | } | 708 | } |
| 780 | }) | 709 | }) |
| 781 | }, | 710 | }, |
| 782 | - syncFileValue(item) { | 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) { | ||
| 783 | const list = this.fileListMap[item.fieldCode] || [] | 775 | const list = this.fileListMap[item.fieldCode] || [] |
| 784 | - this.inspectForm[item.fieldCode] = list.map(file => file.url) | 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) | ||
| 785 | }, | 783 | }, |
| 786 | - | 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 | + */ | ||
| 787 | buildDynamicFields() { | 792 | buildDynamicFields() { |
| 788 | - const fields = [] | 793 | + const fields: any[] = [] |
| 789 | for (const item of this.dynamicFields) { | 794 | for (const item of this.dynamicFields) { |
| 790 | const code = item.fieldCode | 795 | const code = item.fieldCode |
| 791 | const value = this.inspectForm[code] | 796 | const value = this.inspectForm[code] |
| @@ -793,111 +798,131 @@ export default { | @@ -793,111 +798,131 @@ export default { | ||
| 793 | if (type === 9 || type === 11) { | 798 | if (type === 9 || type === 11) { |
| 794 | if (!value) continue | 799 | if (!value) continue |
| 795 | } else if (type === 6 || type === 7 || type === 8) { | 800 | } else if (type === 6 || type === 7 || type === 8) { |
| 796 | - if ((!value) || (Array.isArray(value) && value.length === 0)) continue | ||
| 797 | - } else if (!value) continue | 801 | + if (!Array.isArray(value) || value.length === 0) continue |
| 802 | + } else if (value === '' || value === null || value === undefined) { | ||
| 803 | + continue | ||
| 804 | + } | ||
| 798 | 805 | ||
| 799 | if (type === 1) { | 806 | if (type === 1) { |
| 800 | fields.push({ fieldCode: code, valueNumber: Number(value) }) | 807 | fields.push({ fieldCode: code, valueNumber: Number(value) }) |
| 801 | - } else if (type === 3 && item.optionSource === 5) { | 808 | + } else if (type === 3 && item.optionSource === 3) { |
| 809 | + // 物料:value_text 存物料ID;数量>0 才落物料三列(0 表示本次不领用) | ||
| 802 | const qty = Number(this.qtyMap[code] || 0) | 810 | const qty = Number(this.qtyMap[code] || 0) |
| 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 | 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 : '' | ||
| 808 | row.quantity = qty | 816 | row.quantity = qty |
| 809 | } | 817 | } |
| 810 | fields.push(row) | 818 | fields.push(row) |
| 811 | } else if (type === 3) { | 819 | } else if (type === 3) { |
| 812 | - if (item.multiple === 1) { | ||
| 813 | - fields.push({ fieldCode: code, valueText: (value || []).join(',') }) | ||
| 814 | - } else { | ||
| 815 | - fields.push({ fieldCode: code, valueText: String(value) }) | ||
| 816 | - } | 820 | + // 3枚举:单选取裸 value;历史「枚举+是否多选」存逗号分隔 |
| 821 | + fields.push({ | ||
| 822 | + fieldCode: code, | ||
| 823 | + valueText: item.multiple === 1 ? (value || []).join(',') : String(value) | ||
| 824 | + }) | ||
| 817 | } else if (type === 6) { | 825 | } else if (type === 6) { |
| 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 | - } | 826 | + const selected = (item.selectOptions || []).filter(opt => (value || []).indexOf(opt.value) > -1) |
| 826 | if (this.needQty(item)) { | 827 | if (this.needQty(item)) { |
| 827 | - const qtyObj = this.qtyMap[code] || {} | 828 | + // 元素 {value,label,quantity};数量留空的不带 quantity 键(不臆测成 0),label 作快照落库 |
| 829 | + const qtyMap = this.qtyMap[code] || {} | ||
| 828 | fields.push({ | 830 | fields.push({ |
| 829 | fieldCode: code, | 831 | fieldCode: code, |
| 830 | valueText: JSON.stringify(selected.map(opt => { | 832 | valueText: JSON.stringify(selected.map(opt => { |
| 831 | - const raw = qtyObj[opt.value] | ||
| 832 | - const rowItem = { value: opt.value, label: opt.label } | 833 | + const raw = qtyMap[opt.value] |
| 834 | + const row: any = { value: opt.value, label: opt.label } | ||
| 833 | if (raw !== '' && raw !== null && raw !== undefined) { | 835 | if (raw !== '' && raw !== null && raw !== undefined) { |
| 834 | - rowItem.quantity = Number(raw) | 836 | + row.quantity = Number(raw) |
| 835 | } | 837 | } |
| 836 | - return rowItem | 838 | + return row |
| 837 | })) | 839 | })) |
| 838 | }) | 840 | }) |
| 839 | } else { | 841 | } else { |
| 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 | - } | 842 | + fields.push({ fieldCode: code, valueText: JSON.stringify(value.map(v => String(v))) }) |
| 845 | } | 843 | } |
| 846 | } else if (type === 7 || type === 8) { | 844 | } else if (type === 7 || type === 8) { |
| 847 | fields.push({ fieldCode: code, valueText: JSON.stringify(value) }) | 845 | fields.push({ fieldCode: code, valueText: JSON.stringify(value) }) |
| 848 | } else if (type === 9) { | 846 | } else if (type === 9) { |
| 849 | fields.push({ fieldCode: code, valueText: JSON.stringify([{ id: value.id, name: value.name }]) }) | 847 | fields.push({ fieldCode: code, valueText: JSON.stringify([{ id: value.id, name: value.name }]) }) |
| 850 | } else if (type === 11) { | 848 | } else if (type === 11) { |
| 851 | - fields.push({ fieldCode: code, valueText: JSON.stringify({ lng: value.lng, lat: value.lat, address: value.address }) }) | 849 | + fields.push({ |
| 850 | + fieldCode: code, | ||
| 851 | + valueText: JSON.stringify({ lng: value.lng, lat: value.lat, address: value.address || '' }) | ||
| 852 | + }) | ||
| 852 | } else { | 853 | } else { |
| 854 | + // 2文本 / 4日期 / 5布尔 / 10链接 均为裸文本 | ||
| 853 | fields.push({ fieldCode: code, valueText: String(value) }) | 855 | fields.push({ fieldCode: code, valueText: String(value) }) |
| 854 | } | 856 | } |
| 855 | } | 857 | } |
| 856 | return fields | 858 | return fields |
| 857 | }, | 859 | }, |
| 858 | - | 860 | + /** |
| 861 | + * 提交养护记录 —— 双写两条链路: | ||
| 862 | + * ① maintain-plan-commit/create:养护计划本身(描述 + 图片 + 人员),返回 commit.id | ||
| 863 | + * ② work-record/submit:动态字段值,bizId 用 ① 的 commit.id(旧接口的 VO 没有动态字段,会被 Jackson 静默丢弃) | ||
| 864 | + * 重试安全:commitId 一旦取到就缓存住,重试只补写 ②(② 自身按 clientSeq 幂等),避免重复创建养护记录 | ||
| 865 | + */ | ||
| 859 | async submitInspect() { | 866 | async submitInspect() { |
| 860 | try { | 867 | try { |
| 861 | await this.$refs.inspectFormRef.validate() | 868 | await this.$refs.inspectFormRef.validate() |
| 862 | - uni.showLoading({ title: '提交中' }) | 869 | + const dynamicValues = this.buildDynamicFields() |
| 863 | if (!this.clientSeq) { | 870 | if (!this.clientSeq) { |
| 864 | - this.clientSeq = Date.now().toString() + '-' + Math.random().toString(16).slice(2,10) | 871 | + this.clientSeq = this.genClientSeq() |
| 865 | } | 872 | } |
| 873 | + uni.showLoading({ title: '提交中...' }) | ||
| 874 | + // ① 养护计划本体:仅首次提交时写入 | ||
| 866 | if (!this.commitId) { | 875 | if (!this.commitId) { |
| 867 | const submitData = { | 876 | const submitData = { |
| 868 | - planNo: this.paramsOptions.planNo, | 877 | + planNo: this.paramsOptins.planNo, |
| 869 | imgHost: "1", | 878 | imgHost: "1", |
| 870 | - beginImg: this.imageList.map(img => img.url), | 879 | + beginImg: this.getImgUrlList(this.imagesList), |
| 871 | commonUserList: [], | 880 | commonUserList: [], |
| 872 | - remark: this.inspectForm.remark.trim() | 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 | + }) | ||
| 873 | } | 901 | } |
| 874 | - const createRes = await maintainCreate(submitData) | ||
| 875 | - this.commitId = createRes.id | ||
| 876 | } | 902 | } |
| 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 | - }) | ||
| 887 | uni.hideLoading() | 903 | uni.hideLoading() |
| 888 | - uni.showToast({ title: '提交成功' }) | ||
| 889 | - setTimeout(()=>{ | ||
| 890 | - uni.reLaunch({url:'/pages/index/index'}) | ||
| 891 | - },1000) | ||
| 892 | - } catch (err) { | 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) { | ||
| 893 | uni.hideLoading() | 909 | uni.hideLoading() |
| 894 | - console.error('提交失败', err) | ||
| 895 | - if (Array.isArray(err)) return | ||
| 896 | - const msg = err?.msg || '提交失败,请重试' | 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 | ||
| 897 | if (this.commitId) { | 917 | if (this.commitId) { |
| 898 | - uni.showToast({ title: `已创建工单,动态字段提交失败:${msg}`, icon: 'none' }) | ||
| 899 | - } else { | ||
| 900 | - uni.showToast({ title: msg, icon: 'none' }) | 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 }) | ||
| 901 | } | 926 | } |
| 902 | } | 927 | } |
| 903 | } | 928 | } |
| @@ -905,13 +930,8 @@ export default { | @@ -905,13 +930,8 @@ export default { | ||
| 905 | } | 930 | } |
| 906 | </script> | 931 | </script> |
| 907 | 932 | ||
| 908 | - | ||
| 909 | - | ||
| 910 | - | ||
| 911 | <style lang="scss" scoped> | 933 | <style lang="scss" scoped> |
| 912 | .page-container { | 934 | .page-container { |
| 913 | - min-height: 100vh; | ||
| 914 | - background-color: #f5f5f5; | ||
| 915 | padding-bottom: 120rpx; | 935 | padding-bottom: 120rpx; |
| 916 | } | 936 | } |
| 917 | .inspect-form-content { | 937 | .inspect-form-content { |
| @@ -922,21 +942,24 @@ export default { | @@ -922,21 +942,24 @@ export default { | ||
| 922 | .date-input-wrap { | 942 | .date-input-wrap { |
| 923 | width: 100%; | 943 | width: 100%; |
| 924 | } | 944 | } |
| 925 | -.fixed-bottom-wrap { | 945 | +.fixed-bottom-btn-wrap { |
| 926 | position: fixed; | 946 | position: fixed; |
| 927 | bottom: 0; | 947 | bottom: 0; |
| 928 | left: 0; | 948 | left: 0; |
| 929 | width: 100%; | 949 | width: 100%; |
| 930 | - background: #fff; | 950 | + padding: 20rpx; |
| 951 | + box-sizing: border-box; | ||
| 952 | + background-color: #fff; | ||
| 931 | } | 953 | } |
| 954 | +// 放大u-number-box输入框 | ||
| 932 | .large-number-box { | 955 | .large-number-box { |
| 933 | :deep(.u-number-box__input) { | 956 | :deep(.u-number-box__input) { |
| 934 | width: 264rpx !important; | 957 | width: 264rpx !important; |
| 935 | height: 72rpx; | 958 | height: 72rpx; |
| 936 | font-size: 32rpx; | 959 | font-size: 32rpx; |
| 937 | } | 960 | } |
| 938 | - :deep(.u-number-box__minus), | ||
| 939 | - :deep(.u-number-box__plus) { | 961 | + :deep(.u-number-box__btn) { |
| 962 | + width: 64rpx; | ||
| 940 | height: 72rpx; | 963 | height: 72rpx; |
| 941 | } | 964 | } |
| 942 | } | 965 | } |
| @@ -944,11 +967,12 @@ export default { | @@ -944,11 +967,12 @@ export default { | ||
| 944 | display: flex; | 967 | display: flex; |
| 945 | align-items: center; | 968 | align-items: center; |
| 946 | gap: 16rpx; | 969 | gap: 16rpx; |
| 970 | + .unit-text { | ||
| 971 | + font-size: 28rpx; | ||
| 972 | + color: #333; | ||
| 973 | + } | ||
| 947 | } | 974 | } |
| 948 | -.unit-text { | ||
| 949 | - font-size: 28rpx; | ||
| 950 | - color: #333; | ||
| 951 | -} | 975 | +// 6多选:复选框组 + needQuantity 时每个已选项一行数量输入 |
| 952 | .multi-wrap { | 976 | .multi-wrap { |
| 953 | width: 100%; | 977 | width: 100%; |
| 954 | .multi-hint { | 978 | .multi-hint { |
| @@ -963,6 +987,7 @@ export default { | @@ -963,6 +987,7 @@ export default { | ||
| 963 | border-left: 4rpx solid #f0f0f0; | 987 | border-left: 4rpx solid #f0f0f0; |
| 964 | } | 988 | } |
| 965 | } | 989 | } |
| 990 | +// 数量行(6多选的每个已选项 / 3物料的领用数量共用) | ||
| 966 | .qty-row { | 991 | .qty-row { |
| 967 | display: flex; | 992 | display: flex; |
| 968 | align-items: center; | 993 | align-items: center; |
| @@ -981,86 +1006,28 @@ export default { | @@ -981,86 +1006,28 @@ export default { | ||
| 981 | padding-top: 12rpx; | 1006 | padding-top: 12rpx; |
| 982 | border-top: 1rpx solid #f0f0f0; | 1007 | border-top: 1rpx solid #f0f0f0; |
| 983 | } | 1008 | } |
| 1009 | +// 8附件:自绘文件列表 + 选择按钮(微信无通用文件选择器,走 chooseMessageFile) | ||
| 984 | .file-wrap { | 1010 | .file-wrap { |
| 985 | width: 100%; | 1011 | width: 100%; |
| 986 | .file-row { | 1012 | .file-row { |
| 987 | display: flex; | 1013 | display: flex; |
| 988 | align-items: center; | 1014 | align-items: center; |
| 989 | justify-content: space-between; | 1015 | justify-content: space-between; |
| 990 | - gap:16rpx; | ||
| 991 | - padding:8rpx 0; | 1016 | + gap: 16rpx; |
| 1017 | + padding: 8rpx 0; | ||
| 992 | .file-name { | 1018 | .file-name { |
| 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; | 1019 | + flex: 1; |
| 1020 | + min-width: 0; | ||
| 1029 | font-size: 28rpx; | 1021 | font-size: 28rpx; |
| 1030 | color: #333; | 1022 | color: #333; |
| 1031 | - &.active { | ||
| 1032 | - background: #fff; | ||
| 1033 | - color: #007aff; | ||
| 1034 | - } | ||
| 1035 | - } | ||
| 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 | - } | 1023 | + overflow: hidden; |
| 1024 | + white-space: nowrap; | ||
| 1025 | + text-overflow: ellipsis; | ||
| 1052 | } | 1026 | } |
| 1053 | - .tip-text { | ||
| 1054 | - margin-top: 20rpx; | 1027 | + .file-del { |
| 1055 | font-size: 26rpx; | 1028 | font-size: 26rpx; |
| 1056 | - color:#999; | 1029 | + color: #ff4d4f; |
| 1057 | } | 1030 | } |
| 1058 | } | 1031 | } |
| 1059 | } | 1032 | } |
| 1060 | -.popup-footer { | ||
| 1061 | - padding:24rpx; | ||
| 1062 | -} | ||
| 1063 | </style> | 1033 | </style> |
| 1064 | - | ||
| 1065 | - | ||
| 1066 | - |