Commit ee792d5205b848320029c7f45274dc39688bf9dd

Authored by 刘淇
1 parent 50ad810a

动态表单

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