Commit ee792d5205b848320029c7f45274dc39688bf9dd

Authored by 刘淇
1 parent 50ad810a

动态表单

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