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 | 71 | * @param {Object} params {} |
| 72 | 72 | * @returns {Promise} |
| 73 | 73 | */ |
| 74 | -export const getMaterialsByCode = (params) => { | |
| 74 | +export const getMaterialByCode = (params) => { | |
| 75 | 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 | 93 | |
| 94 | 94 | |
| 95 | 95 | |
| 96 | + | |
| 97 | + | |
| 98 | + | ... | ... |
common/utils/request.js
| ... | ... | @@ -7,7 +7,10 @@ const request = (options) => { |
| 7 | 7 | url: '', |
| 8 | 8 | method: 'GET', |
| 9 | 9 | data: {}, |
| 10 | - header: { 'Content-Type': 'application/json' }, | |
| 10 | + header: { | |
| 11 | + 'Content-Type': 'application/json', | |
| 12 | + 'tenant-id':'1' | |
| 13 | + }, | |
| 11 | 14 | timeout: globalConfig.api.timeout |
| 12 | 15 | }; |
| 13 | 16 | ... | ... |
pages-sub/daily/maintain-manage/add-record.vue
| ... | ... | @@ -4,233 +4,541 @@ |
| 4 | 4 | <up-form label-position="left" :model="inspectForm" ref="inspectFormRef" label-width="140rpx"> |
| 5 | 5 | <!-- ========== 动态表单区域 ========== --> |
| 6 | 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 | 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 | 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 | 25 | <text class="unit-text" v-if="item.unit">{{ item.unit }}</text> |
| 16 | 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 | 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 | 55 | <template #right> |
| 39 | 56 | <up-icon name="arrow-right" size="16"></up-icon> |
| 40 | 57 | </template> |
| 41 | 58 | </up-input> |
| 59 | + | |
| 42 | 60 | <!-- type4 日期时间 --> |
| 43 | 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 | 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 | 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 | 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 | 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 | 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 | 130 | <template #right> |
| 86 | 131 | <up-icon name="arrow-right" size="16"></up-icon> |
| 87 | 132 | </template> |
| 88 | 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 | 151 | <template #right> |
| 96 | 152 | <up-icon name="arrow-right" size="16"></up-icon> |
| 97 | 153 | </template> |
| 98 | 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 | 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 | 164 | </view> |
| 105 | 165 | </up-form-item> |
| 106 | 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 | 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 | 190 | </up-form-item> |
| 118 | 191 | </up-form> |
| 119 | 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 | 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 | 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 | 294 | </view> |
| 138 | 295 | </template> |
| 139 | 296 | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 140 | 300 | <script lang="ts"> |
| 141 | 301 | import { uploadImages } from '@/common/utils/upload' |
| 142 | 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 | 305 | export default { |
| 145 | 306 | data() { |
| 146 | 307 | return { |
| 147 | - imagesList: [], | |
| 308 | + imageList: [], | |
| 148 | 309 | initProgress: 0, |
| 149 | 310 | inspectForm: { |
| 150 | 311 | remark: '', |
| 151 | 312 | }, |
| 152 | - // ========== 新增:用来存放展示文本label的映射对象 ========== | |
| 153 | 313 | labelMap: {}, |
| 154 | - // 类型7图片/8附件的每字段文件列表(fieldCode -> u-upload 兼容数组 {url,status,name}) | |
| 155 | 314 | fileListMap: {}, |
| 156 | - // 数量收集:类型6+needQuantity 为 {选项值: 数量};类型3物料为标量(领用数量) | |
| 157 | 315 | qtyMap: {}, |
| 158 | - paramsOptins: {}, | |
| 159 | - // 幂等流水号:本次填报页面生命周期内保持不变,重复提交(如网络超时后重试)只会落一条记录 | |
| 316 | + paramsOptions: {}, | |
| 160 | 317 | clientSeq: '', |
| 161 | - // 养护计划本体的 commit.id:取到后缓存,重试时不再重复创建养护记录 | |
| 162 | 318 | commitId: null, |
| 163 | 319 | inspectFormRules: { |
| 164 | 320 | images: [ |
| 165 | 321 | { |
| 166 | 322 | required: true, |
| 167 | - message: '请上传图片', | |
| 323 | + message: '最少上传1张图片', | |
| 168 | 324 | trigger: 'change', |
| 169 | 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 | 332 | } else { |
| 177 | 333 | callback() |
| 178 | 334 | } |
| 179 | 335 | } |
| 180 | 336 | } |
| 181 | - ], | |
| 337 | + ] | |
| 182 | 338 | }, |
| 183 | - // 动态字段列表 | |
| 184 | - dynamicFields: [] as any[], | |
| 185 | - // 日期时间选择器 | |
| 339 | + dynamicFields: [], | |
| 340 | + | |
| 186 | 341 | datePickerShow: false, |
| 187 | - datePickerValue: Number(new Date()), | |
| 342 | + datePickerValue: new Date().getTime(), | |
| 188 | 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 | 351 | show: false, |
| 199 | 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 | 363 | onLoad(option) { |
| 213 | 364 | console.log('页面参数:', option) |
| 214 | - this.paramsOptins = option | |
| 365 | + this.paramsOptions = option | |
| 215 | 366 | const { businessType, planTypeId } = option |
| 216 | - this.loadDynamicFields(businessType, planTypeId) | |
| 367 | + const workTypeId = planTypeId | |
| 368 | + this.loadDynamicFields(businessType, workTypeId) | |
| 217 | 369 | }, |
| 218 | 370 | onReady() { |
| 219 | 371 | this.$refs.inspectFormRef.setRules(this.inspectFormRules) |
| 220 | - console.log('巡查表单规则初始化完成') | |
| 372 | + console.log('表单规则加载完成') | |
| 221 | 373 | }, |
| 222 | 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 | 534 | valChange(item) { |
| 225 | - // 数值修改后触发表单校验 | |
| 226 | 535 | this.$refs.inspectFormRef.validateField(item.fieldCode) |
| 227 | 536 | }, |
| 228 | - // ========== 日期时间相关方法 ========== | |
| 537 | + | |
| 229 | 538 | openDatePicker(field) { |
| 230 | - console.log('打开日期选择器', field) | |
| 231 | 539 | this.currentDateFieldCode = field.fieldCode |
| 232 | 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 | 542 | this.datePickerShow = true |
| 235 | 543 | uni.hideKeyboard() |
| 236 | 544 | }, |
| ... | ... | @@ -250,158 +558,71 @@ export default { |
| 250 | 558 | onDateClose() { |
| 251 | 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 | 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 | 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 | 584 | uni.chooseLocation({ |
| 363 | 585 | success: (res) => { |
| 364 | 586 | this.inspectForm[item.fieldCode] = { |
| 365 | 587 | lng: res.longitude, |
| 366 | 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 | 592 | this.$refs.inspectFormRef.validateField(item.fieldCode) |
| 371 | 593 | }, |
| 372 | 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 | 604 | async loadDynamicFields(businessType: string, workTypeId: string) { |
| 384 | 605 | if (!businessType || !workTypeId) { |
| 385 | - uni.showToast({ title: '缺少业务参数', icon: 'none' }) | |
| 606 | + uni.showToast({ title: '缺少参数', icon: 'none' }) | |
| 386 | 607 | return |
| 387 | 608 | } |
| 388 | - const bizType = 3 | |
| 609 | + | |
| 610 | + const bizType = 3 | |
| 389 | 611 | try { |
| 390 | - const res = await getByType({ | |
| 391 | - businessType, | |
| 392 | - workTypeId, | |
| 393 | - bizType | |
| 394 | - }) | |
| 612 | + const res = await getByType({ businessType, bizType,workTypeId }) | |
| 395 | 613 | let fields = res.fields || [] |
| 396 | 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 | 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 | 626 | this.inspectForm[field.fieldCode] = [] |
| 406 | 627 | } else if (field.fieldType === 7 || field.fieldType === 8) { |
| 407 | 628 | this.inspectForm[field.fieldCode] = [] |
| ... | ... | @@ -410,387 +631,161 @@ export default { |
| 410 | 631 | } else { |
| 411 | 632 | this.inspectForm[field.fieldCode] = '' |
| 412 | 633 | } |
| 413 | - // 初始化展示label为空 | |
| 414 | 634 | this.labelMap[field.fieldCode] = '' |
| 415 | - // 7图片/8附件的文件列表(驱动 up-upload 与自绘文件列表展示) | |
| 416 | 635 | if (field.fieldType === 7 || field.fieldType === 8) { |
| 417 | 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 | 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 | 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 | 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 | 659 | if (field.fieldType === 6 && this.needQty(field)) { |
| 453 | - const qty = {} | |
| 660 | + const qtyObj = {} | |
| 454 | 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 | 667 | this.qtyMap[field.fieldCode] = 0 |
| 461 | 668 | } |
| 462 | - // 必填规则 | |
| 669 | + // 构建表单校验规则 | |
| 463 | 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 | 674 | this.$nextTick(() => { |
| 469 | 675 | this.$refs.inspectFormRef.setRules(this.inspectFormRules) |
| 470 | 676 | }) |
| 471 | 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 | 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 | 698 | if (!url) return '' |
| 581 | - const path = String(url).split('?')[0].split('#')[0] | |
| 699 | + const path = String(url).split('?')[0] | |
| 582 | 700 | const name = path.substring(path.lastIndexOf('/') + 1) |
| 583 | 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 | 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 | 763 | uni.chooseMessageFile({ |
| 697 | - ...options, | |
| 764 | + count: this.pickCountOf(item,5), | |
| 765 | + type: 'file', | |
| 698 | 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 | 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 | 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 | 787 | buildDynamicFields() { |
| 793 | - const fields: any[] = [] | |
| 788 | + const fields = [] | |
| 794 | 789 | for (const item of this.dynamicFields) { |
| 795 | 790 | const code = item.fieldCode |
| 796 | 791 | const value = this.inspectForm[code] |
| ... | ... | @@ -798,131 +793,111 @@ export default { |
| 798 | 793 | if (type === 9 || type === 11) { |
| 799 | 794 | if (!value) continue |
| 800 | 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 | 799 | if (type === 1) { |
| 807 | 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 | 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 | 808 | row.quantity = qty |
| 817 | 809 | } |
| 818 | 810 | fields.push(row) |
| 819 | 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 | 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 | 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 | 828 | fields.push({ |
| 831 | 829 | fieldCode: code, |
| 832 | 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 | 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 | 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 | 846 | } else if (type === 7 || type === 8) { |
| 845 | 847 | fields.push({ fieldCode: code, valueText: JSON.stringify(value) }) |
| 846 | 848 | } else if (type === 9) { |
| 847 | 849 | fields.push({ fieldCode: code, valueText: JSON.stringify([{ id: value.id, name: value.name }]) }) |
| 848 | 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 | 852 | } else { |
| 854 | - // 2文本 / 4日期 / 5布尔 / 10链接 均为裸文本 | |
| 855 | 853 | fields.push({ fieldCode: code, valueText: String(value) }) |
| 856 | 854 | } |
| 857 | 855 | } |
| 858 | 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 | 859 | async submitInspect() { |
| 867 | 860 | try { |
| 868 | 861 | await this.$refs.inspectFormRef.validate() |
| 869 | - const dynamicValues = this.buildDynamicFields() | |
| 862 | + uni.showLoading({ title: '提交中' }) | |
| 870 | 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 | 866 | if (!this.commitId) { |
| 876 | 867 | const submitData = { |
| 877 | - planNo: this.paramsOptins.planNo, | |
| 868 | + planNo: this.paramsOptions.planNo, | |
| 878 | 869 | imgHost: "1", |
| 879 | - beginImg: this.getImgUrlList(this.imagesList), | |
| 870 | + beginImg: this.imageList.map(img => img.url), | |
| 880 | 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 | 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 | 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 | 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 | 905 | } |
| 931 | 906 | </script> |
| 932 | 907 | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 933 | 911 | <style lang="scss" scoped> |
| 934 | 912 | .page-container { |
| 913 | + min-height: 100vh; | |
| 914 | + background-color: #f5f5f5; | |
| 935 | 915 | padding-bottom: 120rpx; |
| 936 | 916 | } |
| 937 | 917 | .inspect-form-content { |
| ... | ... | @@ -942,24 +922,21 @@ export default { |
| 942 | 922 | .date-input-wrap { |
| 943 | 923 | width: 100%; |
| 944 | 924 | } |
| 945 | -.fixed-bottom-btn-wrap { | |
| 925 | +.fixed-bottom-wrap { | |
| 946 | 926 | position: fixed; |
| 947 | 927 | bottom: 0; |
| 948 | 928 | left: 0; |
| 949 | 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 | 932 | .large-number-box { |
| 956 | 933 | :deep(.u-number-box__input) { |
| 957 | 934 | width: 264rpx !important; |
| 958 | 935 | height: 72rpx; |
| 959 | 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 | 940 | height: 72rpx; |
| 964 | 941 | } |
| 965 | 942 | } |
| ... | ... | @@ -967,12 +944,11 @@ export default { |
| 967 | 944 | display: flex; |
| 968 | 945 | align-items: center; |
| 969 | 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 | 952 | .multi-wrap { |
| 977 | 953 | width: 100%; |
| 978 | 954 | .multi-hint { |
| ... | ... | @@ -987,7 +963,6 @@ export default { |
| 987 | 963 | border-left: 4rpx solid #f0f0f0; |
| 988 | 964 | } |
| 989 | 965 | } |
| 990 | -// 数量行(6多选的每个已选项 / 3物料的领用数量共用) | |
| 991 | 966 | .qty-row { |
| 992 | 967 | display: flex; |
| 993 | 968 | align-items: center; |
| ... | ... | @@ -1006,28 +981,86 @@ export default { |
| 1006 | 981 | padding-top: 12rpx; |
| 1007 | 982 | border-top: 1rpx solid #f0f0f0; |
| 1008 | 983 | } |
| 1009 | -// 8附件:自绘文件列表 + 选择按钮(微信无通用文件选择器,走 chooseMessageFile) | |
| 1010 | 984 | .file-wrap { |
| 1011 | 985 | width: 100%; |
| 1012 | 986 | .file-row { |
| 1013 | 987 | display: flex; |
| 1014 | 988 | align-items: center; |
| 1015 | 989 | justify-content: space-between; |
| 1016 | - gap: 16rpx; | |
| 1017 | - padding: 8rpx 0; | |
| 990 | + gap:16rpx; | |
| 991 | + padding:8rpx 0; | |
| 1018 | 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 | 1029 | font-size: 28rpx; |
| 1022 | 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 | 1055 | font-size: 26rpx; |
| 1029 | - color: #ff4d4f; | |
| 1056 | + color:#999; | |
| 1030 | 1057 | } |
| 1031 | 1058 | } |
| 1032 | 1059 | } |
| 1060 | +.popup-footer { | |
| 1061 | + padding:24rpx; | |
| 1062 | +} | |
| 1033 | 1063 | </style> |
| 1064 | + | |
| 1065 | + | |
| 1066 | + | ... | ... |
pages/index/index.vue
| ... | ... | @@ -24,20 +24,51 @@ |
| 24 | 24 | |
| 25 | 25 | <!-- 任务完成情况卡片 --> |
| 26 | 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 | 49 | <!-- 双折线K线图 --> |
| 37 | - <view class="chart-container"> | |
| 50 | + <!-- <view class="chart-container"> | |
| 38 | 51 | <uni-charts v-if="!isDatePickerOpen" type="line" :data="klineChartData" :categories="klineCategories" |
| 39 | 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 | 72 | </view> |
| 42 | 73 | </view> |
| 43 | 74 | |
| ... | ... | @@ -131,6 +162,46 @@ type GroupItem = { |
| 131 | 162 | showTipHeader: boolean; // 是否展示顶部进度条 |
| 132 | 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 | 205 | const groupConfig: GroupItem[] = [ |
| 135 | 206 | // 组2 A1B2 纯效率·有提醒 |
| 136 | 207 | { |
| ... | ... | @@ -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 | 366 | const userName = computed(() => userStore.userInfo?.user?.nickname || '用户'); |
| 267 | 367 | |
| 268 | 368 | const rpx2px = (rpx: number) => { |
| ... | ... | @@ -621,7 +721,6 @@ $border-color: #e5e5e5; |
| 621 | 721 | color: $text-color-placeholder; |
| 622 | 722 | margin-bottom: $spacing-md; |
| 623 | 723 | .date-picker-wrap { |
| 624 | - padding: 2px 4px; | |
| 625 | 724 | border-radius: 3px; |
| 626 | 725 | transition: background-color 0.2s; |
| 627 | 726 | position: relative; |
| ... | ... | @@ -708,6 +807,13 @@ $border-color: #e5e5e5; |
| 708 | 807 | .empty-box { |
| 709 | 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 | 818 | .task-item { |
| 713 | 819 | padding: $spacing-md; | ... | ... |
pages/login/index.vue
| ... | ... | @@ -406,10 +406,15 @@ const handleLogin = async () => { |
| 406 | 406 | loginParams.mobile = form.mobile; |
| 407 | 407 | loginParams.code = form.code; |
| 408 | 408 | loginParams.type = 'sms'; |
| 409 | + loginParams.tenantName='蓟城山水集团' | |
| 410 | + | |
| 411 | + loginParams.rememberMe= true | |
| 409 | 412 | } else { |
| 410 | 413 | loginParams.username = form.account; |
| 411 | 414 | loginParams.password = form.password; |
| 412 | 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 | 867 | display: flex; |
| 868 | 868 | align-items: center; |
| 869 | 869 | justify-content: space-between; |
| 870 | - padding: 4rpx 32rpx; | |
| 870 | + padding: 4rpx 12rpx; | |
| 871 | 871 | background: var(--neo-color-bg-elevated, #fff); |
| 872 | 872 | border: 2rpx solid var(--neo-color-border, rgba(0, 0, 0, 0.08)); |
| 873 | 873 | border-radius: 12rpx; |
| ... | ... | @@ -894,7 +894,7 @@ export default { |
| 894 | 894 | } |
| 895 | 895 | |
| 896 | 896 | .trigger-value { |
| 897 | - font-size: 28rpx; | |
| 897 | + font-size: 24rpx; | |
| 898 | 898 | |
| 899 | 899 | } |
| 900 | 900 | ... | ... |