Commit 629df8b3a9663e265004f97b7567cfea234e3058

Authored by 王彪总
1 parent ee792d52

业务线 小程序不用写死,从后端接口获取

pages-sub/daily/maintain-manage/add-record.vue
... ... @@ -4,541 +4,233 @@
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
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 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 15 <text class="unit-text" v-if="item.unit">{{ item.unit }}</text>
26 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 24 <template #right>
56 25 <up-icon name="arrow-right" size="16"></up-icon>
57 26 </template>
58 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 38 <template #right>
86 39 <up-icon name="arrow-right" size="16"></up-icon>
87 40 </template>
88 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 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 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 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 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 85 <template #right>
131 86 <up-icon name="arrow-right" size="16"></up-icon>
132 87 </template>
133 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 95 <template #right>
152 96 <up-icon name="arrow-right" size="16"></up-icon>
153 97 </template>
154 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 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 104 </view>
165 105 </up-form-item>
166 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 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 117 </up-form-item>
191 118 </up-form>
192 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 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 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 137 </view>
295 138 </template>
296 139  
297   -
298   -
299   -
300 140 <script lang="ts">
301 141 import { uploadImages } from '@/common/utils/upload'
302 142 import { maintainCreate } from "@/api/maintain-manage/maintain-manage"
303 143 import { getByType, getOptionsByCode, getMaterialByCode, workRecordSubmit } from "@/api/common"
304   -
305 144 export default {
306 145 data() {
307 146 return {
308   - imageList: [],
  147 + imagesList: [],
309 148 initProgress: 0,
310 149 inspectForm: {
311 150 remark: '',
312 151 },
  152 + // ========== 新增:用来存放展示文本label的映射对象 ==========
313 153 labelMap: {},
  154 + // 类型7图片/8附件的每字段文件列表(fieldCode -> u-upload 兼容数组 {url,status,name})
314 155 fileListMap: {},
  156 + // 数量收集:类型6+needQuantity 为 {选项值: 数量};类型3物料为标量(领用数量)
315 157 qtyMap: {},
316   - paramsOptions: {},
  158 + paramsOptins: {},
  159 + // 幂等流水号:本次填报页面生命周期内保持不变,重复提交(如网络超时后重试)只会落一条记录
317 160 clientSeq: '',
  161 + // 养护计划本体的 commit.id:取到后缓存,重试时不再重复创建养护记录
318 162 commitId: null,
319 163 inspectFormRules: {
320 164 images: [
321 165 {
322 166 required: true,
323   - message: '最少上传1张图片',
  167 + message: '请上传图片',
324 168 trigger: 'change',
325 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 176 } else {
333 177 callback()
334 178 }
335 179 }
336 180 }
337   - ]
  181 + ],
338 182 },
339   - dynamicFields: [],
340   -
  183 + // 动态字段列表
  184 + dynamicFields: [] as any[],
  185 + // 日期时间选择器
341 186 datePickerShow: false,
342   - datePickerValue: new Date().getTime(),
  187 + datePickerValue: Number(new Date()),
343 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 198 show: false,
352 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 212 onLoad(option) {
364 213 console.log('页面参数:', option)
365   - this.paramsOptions = option
  214 + this.paramsOptins = option
366 215 const { businessType, planTypeId } = option
367   - const workTypeId = planTypeId
368   - this.loadDynamicFields(businessType, workTypeId)
  216 + this.loadDynamicFields(businessType, planTypeId)
369 217 },
370 218 onReady() {
371 219 this.$refs.inspectFormRef.setRules(this.inspectFormRules)
372   - console.log('表单规则加载完成')
  220 + console.log('巡查表单规则初始化完成')
373 221 },
374 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 224 valChange(item) {
  225 + // 数值修改后触发表单校验
535 226 this.$refs.inspectFormRef.validateField(item.fieldCode)
536 227 },
537   -
  228 + // ========== 日期时间相关方法 ==========
538 229 openDatePicker(field) {
  230 + console.log('打开日期选择器', field)
539 231 this.currentDateFieldCode = field.fieldCode
540 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 234 this.datePickerShow = true
543 235 uni.hideKeyboard()
544 236 },
... ... @@ -558,71 +250,158 @@ export default {
558 250 onDateClose() {
559 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 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 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 362 uni.chooseLocation({
585 363 success: (res) => {
586 364 this.inspectForm[item.fieldCode] = {
587 365 lng: res.longitude,
588 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 370 this.$refs.inspectFormRef.validateField(item.fieldCode)
593 371 },
594 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 383 async loadDynamicFields(businessType: string, workTypeId: string) {
605 384 if (!businessType || !workTypeId) {
606   - uni.showToast({ title: '缺少参数', icon: 'none' })
  385 + uni.showToast({ title: '缺少业务参数', icon: 'none' })
607 386 return
608 387 }
609   -
610   - const bizType = 3
  388 + const bizType = 3
611 389 try {
612   - const res = await getByType({ businessType, bizType,workTypeId })
  390 + const res = await getByType({
  391 + businessType,
  392 + workTypeId,
  393 + bizType
  394 + })
613 395 let fields = res.fields || []
614 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 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 405 this.inspectForm[field.fieldCode] = []
627 406 } else if (field.fieldType === 7 || field.fieldType === 8) {
628 407 this.inspectForm[field.fieldCode] = []
... ... @@ -631,161 +410,387 @@ export default {
631 410 } else {
632 411 this.inspectForm[field.fieldCode] = ''
633 412 }
  413 + // 初始化展示label为空
634 414 this.labelMap[field.fieldCode] = ''
  415 + // 7图片/8附件的文件列表(驱动 up-upload 与自绘文件列表展示)
635 416 if (field.fieldType === 7 || field.fieldType === 8) {
636 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 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 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 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 452 if (field.fieldType === 6 && this.needQty(field)) {
660   - const qtyObj = {}
  453 + const qty = {}
661 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 460 this.qtyMap[field.fieldCode] = 0
668 461 }
669   - // 构建表单校验规则
  462 + // 必填规则
670 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 468 this.$nextTick(() => {
675 469 this.$refs.inspectFormRef.setRules(this.inspectFormRules)
676 470 })
677 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 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 580 if (!url) return ''
699   - const path = String(url).split('?')[0]
  581 + const path = String(url).split('?')[0].split('#')[0]
700 582 const name = path.substring(path.lastIndexOf('/') + 1)
701 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 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 696 uni.chooseMessageFile({
764   - count: this.pickCountOf(item,5),
765   - type: 'file',
  697 + ...options,
766 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 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 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 792 buildDynamicFields() {
788   - const fields = []
  793 + const fields: any[] = []
789 794 for (const item of this.dynamicFields) {
790 795 const code = item.fieldCode
791 796 const value = this.inspectForm[code]
... ... @@ -793,111 +798,131 @@ export default {
793 798 if (type === 9 || type === 11) {
794 799 if (!value) continue
795 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 806 if (type === 1) {
800 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 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 816 row.quantity = qty
809 817 }
810 818 fields.push(row)
811 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 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 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 830 fields.push({
829 831 fieldCode: code,
830 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 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 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 844 } else if (type === 7 || type === 8) {
847 845 fields.push({ fieldCode: code, valueText: JSON.stringify(value) })
848 846 } else if (type === 9) {
849 847 fields.push({ fieldCode: code, valueText: JSON.stringify([{ id: value.id, name: value.name }]) })
850 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 853 } else {
  854 + // 2文本 / 4日期 / 5布尔 / 10链接 均为裸文本
853 855 fields.push({ fieldCode: code, valueText: String(value) })
854 856 }
855 857 }
856 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 866 async submitInspect() {
860 867 try {
861 868 await this.$refs.inspectFormRef.validate()
862   - uni.showLoading({ title: '提交中' })
  869 + const dynamicValues = this.buildDynamicFields()
863 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 875 if (!this.commitId) {
867 876 const submitData = {
868   - planNo: this.paramsOptions.planNo,
  877 + planNo: this.paramsOptins.planNo,
869 878 imgHost: "1",
870   - beginImg: this.imageList.map(img => img.url),
  879 + beginImg: this.getImgUrlList(this.imagesList),
871 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 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 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 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 930 }
906 931 </script>
907 932  
908   -
909   -
910   -
911 933 <style lang="scss" scoped>
912 934 .page-container {
913   - min-height: 100vh;
914   - background-color: #f5f5f5;
915 935 padding-bottom: 120rpx;
916 936 }
917 937 .inspect-form-content {
... ... @@ -922,21 +942,24 @@ export default {
922 942 .date-input-wrap {
923 943 width: 100%;
924 944 }
925   -.fixed-bottom-wrap {
  945 +.fixed-bottom-btn-wrap {
926 946 position: fixed;
927 947 bottom: 0;
928 948 left: 0;
929 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 955 .large-number-box {
933 956 :deep(.u-number-box__input) {
934 957 width: 264rpx !important;
935 958 height: 72rpx;
936 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 963 height: 72rpx;
941 964 }
942 965 }
... ... @@ -944,11 +967,12 @@ export default {
944 967 display: flex;
945 968 align-items: center;
946 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 976 .multi-wrap {
953 977 width: 100%;
954 978 .multi-hint {
... ... @@ -963,6 +987,7 @@ export default {
963 987 border-left: 4rpx solid #f0f0f0;
964 988 }
965 989 }
  990 +// 数量行(6多选的每个已选项 / 3物料的领用数量共用)
966 991 .qty-row {
967 992 display: flex;
968 993 align-items: center;
... ... @@ -981,86 +1006,28 @@ export default {
981 1006 padding-top: 12rpx;
982 1007 border-top: 1rpx solid #f0f0f0;
983 1008 }
  1009 +// 8附件:自绘文件列表 + 选择按钮(微信无通用文件选择器,走 chooseMessageFile)
984 1010 .file-wrap {
985 1011 width: 100%;
986 1012 .file-row {
987 1013 display: flex;
988 1014 align-items: center;
989 1015 justify-content: space-between;
990   - gap:16rpx;
991   - padding:8rpx 0;
  1016 + gap: 16rpx;
  1017 + padding: 8rpx 0;
992 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 1021 font-size: 28rpx;
1030 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 1028 font-size: 26rpx;
1056   - color:#999;
  1029 + color: #ff4d4f;
1057 1030 }
1058 1031 }
1059 1032 }
1060   -.popup-footer {
1061   - padding:24rpx;
1062   -}
1063 1033 </style>
1064   -
1065   -
1066   -
... ...