Commit 7da61cdb67f64bdcbe21d4f6b77bedf856c2b6ed

Authored by 刘淇
1 parent 25904bec

养护计划

api/maintain-manage/maintain-manage.js
1 1
2 -import { post, get } from '@/common/utils/request'; 2 +import { post, get, put } from '@/common/utils/request';
3 3
4 /** 4 /**
5 * 道路汇总 5 * 道路汇总
@@ -34,6 +34,15 @@ export const maintainCreate = (data) => { @@ -34,6 +34,15 @@ export const maintainCreate = (data) => {
34 }; 34 };
35 35
36 /** 36 /**
  37 + * 新增养护计划并完成
  38 + * @returns {Promise}
  39 + */
  40 +export const maintainCreateFinish = (data) => {
  41 + return post('/app-api/app/garden/maintain-plan-commit/create-and-finish',data);
  42 +};
  43 +
  44 +
  45 +/**
37 * 未完成养护计划 46 * 未完成养护计划
38 * @returns {Promise} 47 * @returns {Promise}
39 */ 48 */
@@ -51,3 +60,59 @@ export const detailList = (params) => { @@ -51,3 +60,59 @@ export const detailList = (params) => {
51 }; 60 };
52 61
53 62
  63 +/**
  64 + * 查询单条上报记录
  65 + * @returns {Promise}
  66 + */
  67 +export const getDetailList = (params) => {
  68 + return get('/admin-api/garden/maintain-plan-commit/get',params);
  69 +};
  70 +
  71 +
  72 +
  73 +
  74 +/**
  75 + * 养护道路
  76 + * @returns {Promise}
  77 + */
  78 +export const maintainPlan= (params) => {
  79 + return get('/admin-api/garden/maintain-plan/page',params);
  80 +};
  81 +/**
  82 + * 养护类型次数
  83 + * @returns {Promise}
  84 + */
  85 +export const maintainPlanNum= (params) => {
  86 + return get('/admin-api/garden/maintain-plan-detail/page',params);
  87 +};
  88 +
  89 +
  90 +/**
  91 + * 查询上报记录列表
  92 + * @returns {Promise}
  93 + */
  94 +export const maintainPlanList= (params) => {
  95 + return get('/app-api/app/garden/maintain-plan-commit/page',params);
  96 +};
  97 +
  98 +
  99 +/**
  100 + * 直接结束
  101 + * @returns {Promise}
  102 + */
  103 +export const finishPlan= (params) => {
  104 + return post('/app-api/app/garden/maintain-plan-commit/finish',params);
  105 +};
  106 +
  107 +/**
  108 + * 设为失效
  109 + * @returns {Promise}
  110 + */
  111 +export const invalidatePlan= (params) => {
  112 + return put('/admin-api/garden/maintain-plan-detail/invalidate',params);
  113 +};
  114 +
  115 +
  116 +
  117 +
  118 +
pages-sub/daily/maintain-manage/add-record.vue
@@ -4,44 +4,40 @@ @@ -4,44 +4,40 @@
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" class="form-item" @click="openPickerIfNeeded(item)"> 7 + <up-form-item :label="item.fieldName" :prop="item.fieldCode" class="form-item"
  8 + @click="openPickerIfNeeded(item)">
8 9
9 <!-- type 1 数值:u-number-box,最多2位小数,min=0,max取item.length --> 10 <!-- type 1 数值:u-number-box,最多2位小数,min=0,max取item.length -->
10 <view v-if="item.fieldType === 1" class="number-box-wrap"> 11 <view v-if="item.fieldType === 1" class="number-box-wrap">
11 - <u-number-box  
12 - v-model="inspectForm[item.fieldCode]"  
13 - min="1"  
14 - :step="1"  
15 - :max="item.length ? 10 ** Number(item.length) - 1 : null"  
16 - :placeholder="`请输入${item.fieldName}${item.unit} `"  
17 - @change="valChange(item)"  
18 - @blur="valChange(item)"  
19 - class="large-number-box">  
20 -</u-number-box> 12 + <u-number-box v-model="inspectForm[item.fieldCode]" min="1" :step="1"
  13 + :max="item.length ? 10 ** Number(item.length) - 1 : null"
  14 + :placeholder="`请输入${item.fieldName}${item.unit} `" @change="valChange(item)" @blur="valChange(item)"
  15 + class="large-number-box">
  16 + </u-number-box>
21 17
22 <text class="unit-text" v-if="item.unit">{{ item.unit }}</text> 18 <text class="unit-text" v-if="item.unit">{{ item.unit }}</text>
23 </view> 19 </view>
24 <!-- type 2 文本 --> 20 <!-- type 2 文本 -->
25 <up-input v-else-if="item.fieldType === 2" v-model="inspectForm[item.fieldCode]" 21 <up-input v-else-if="item.fieldType === 2" v-model="inspectForm[item.fieldCode]"
26 - :maxlength="item.length || 50" :placeholder="`请输入${item.fieldName}`"></up-input> 22 + :maxlength="item.length || 50" :placeholder="`请输入${item.fieldName}`"></up-input>
27 <!-- 分层选项(3枚举/6多选,且选项带 path):统一只读入口,点开分组弹层选择。 23 <!-- 分层选项(3枚举/6多选,且选项带 path):统一只读入口,点开分组弹层选择。
28 必须放在下面的 3/6 分支之前——v-else-if 短路,放后面就永远进不来 --> 24 必须放在下面的 3/6 分支之前——v-else-if 短路,放后面就永远进不来 -->
29 <up-input v-else-if="isGroupedField(item)" v-model="labelMap[item.fieldCode]" readonly 25 <up-input v-else-if="isGroupedField(item)" v-model="labelMap[item.fieldCode]" readonly
30 - disabled-color="#ffffff" :placeholder="`请选择${item.fieldName}`" border="none"> 26 + disabled-color="#ffffff" :placeholder="`请选择${item.fieldName}`" border="none">
31 <template #right> 27 <template #right>
32 <up-icon name="arrow-right" size="16"></up-icon> 28 <up-icon name="arrow-right" size="16"></up-icon>
33 </template> 29 </template>
34 </up-input> 30 </up-input>
35 <!-- type3 枚举:多选 multiple=1 使用复选框组(name 必须是 value,提交按逗号拼接 value,与值格式契约一致) --> 31 <!-- type3 枚举:多选 multiple=1 使用复选框组(name 必须是 value,提交按逗号拼接 value,与值格式契约一致) -->
36 <u-checkbox-group v-else-if="item.fieldType === 3 && item.multiple === 1" 32 <u-checkbox-group v-else-if="item.fieldType === 3 && item.multiple === 1"
37 - v-model="inspectForm[item.fieldCode]"> 33 + v-model="inspectForm[item.fieldCode]">
38 <u-checkbox v-for="opt in item.selectOptions" :key="opt.value" :label="opt.label" :name="opt.value"> 34 <u-checkbox v-for="opt in item.selectOptions" :key="opt.value" :label="opt.label" :name="opt.value">
39 35
40 </u-checkbox> 36 </u-checkbox>
41 </u-checkbox-group> 37 </u-checkbox-group>
42 <!-- type3 枚举:单选 用up-action-sheet 底部选择 --> 38 <!-- type3 枚举:单选 用up-action-sheet 底部选择 -->
43 <up-input v-else-if="item.fieldType === 3" v-model="labelMap[item.fieldCode]" readonly 39 <up-input v-else-if="item.fieldType === 3" v-model="labelMap[item.fieldCode]" readonly
44 - disabled-color="#ffffff" :placeholder="`请选择${item.fieldName}`" border="none"> 40 + disabled-color="#ffffff" :placeholder="`请选择${item.fieldName}`" border="none">
45 <template #right> 41 <template #right>
46 <up-icon name="arrow-right" size="16"></up-icon> 42 <up-icon name="arrow-right" size="16"></up-icon>
47 </template> 43 </template>
@@ -59,7 +55,7 @@ @@ -59,7 +55,7 @@
59 <view v-else-if="item.fieldType === 6" class="multi-wrap"> 55 <view v-else-if="item.fieldType === 6" class="multi-wrap">
60 <u-checkbox-group v-model="inspectForm[item.fieldCode]" @change="onMultiChange(item)"> 56 <u-checkbox-group v-model="inspectForm[item.fieldCode]" @change="onMultiChange(item)">
61 <u-checkbox v-for="opt in item.selectOptions" :key="opt.value" :label="opt.label" :name="opt.value" 57 <u-checkbox v-for="opt in item.selectOptions" :key="opt.value" :label="opt.label" :name="opt.value"
62 - :disabled="isOptionLocked(item, opt)"> 58 + :disabled="isOptionLocked(item, opt)">
63 </u-checkbox> 59 </u-checkbox>
64 </u-checkbox-group> 60 </u-checkbox-group>
65 <!-- 有限上限时提示已选数。上限靠「禁用未选中项」实现:u-checkbox 的勾选态在组件内部,回写 v-model 撤不掉 --> 61 <!-- 有限上限时提示已选数。上限靠「禁用未选中项」实现:u-checkbox 的勾选态在组件内部,回写 v-model 撤不掉 -->
@@ -73,22 +69,12 @@ @@ -73,22 +69,12 @@
73 </view> 69 </view>
74 </view> 70 </view>
75 <!-- type7 图片:多张上传,提交 JSON 数组(URL) --> 71 <!-- type7 图片:多张上传,提交 JSON 数组(URL) -->
76 - <!-- type7 图片:多张上传,提交 JSON 数组(URL) -->  
77 -<!-- type7 图片:多张上传,提交 JSON 数组(URL)【对齐工单上传组件】 -->  
78 -<up-upload  
79 - v-else-if="item.fieldType === 7"  
80 - :file-list="fileListMap[item.fieldCode] || []"  
81 - @after-read="(event) => uploadFieldFiles(item, event)"  
82 - @delete="(event) => deleteFieldFile(item, event)"  
83 - multiple  
84 - width="70"  
85 - height="70"  
86 - :sizeType="['compressed']"  
87 - :max-count="pickCountOf(item, 5)"  
88 - upload-text="选择图片"  
89 - del-color="#ff4d4f"  
90 - class="upload-wrap"  
91 -></up-upload> 72 + <!-- type7 图片:多张上传,提交 JSON 数组(URL) -->
  73 + <!-- type7 图片:多张上传,提交 JSON 数组(URL)【对齐工单上传组件】 -->
  74 + <up-upload v-else-if="item.fieldType === 7" :file-list="fileListMap[item.fieldCode] || []"
  75 + @after-read="(event) => uploadFieldFiles(item, event)" @delete="(event) => deleteFieldFile(item, event)"
  76 + multiple width="70" height="70" :sizeType="['compressed']" :max-count="pickCountOf(item, 5)"
  77 + upload-text="选择图片" del-color="#ff4d4f" class="upload-wrap"></up-upload>
92 78
93 79
94 <!-- type8 附件:微信只能从聊天记录选文件(无通用文件选择器),提交 JSON 数组(URL) --> 80 <!-- type8 附件:微信只能从聊天记录选文件(无通用文件选择器),提交 JSON 数组(URL) -->
@@ -98,21 +84,21 @@ @@ -98,21 +84,21 @@
98 <text class="file-del" @click.stop="deleteFieldFile(item, { index: idx })">删除</text> 84 <text class="file-del" @click.stop="deleteFieldFile(item, { index: idx })">删除</text>
99 </view> 85 </view>
100 <up-button size="small" type="primary" plain :text="fileButtonText(item)" 86 <up-button size="small" type="primary" plain :text="fileButtonText(item)"
101 - @click.stop="chooseFieldFiles(item)"></up-button> 87 + @click.stop="chooseFieldFiles(item)"></up-button>
102 </view> 88 </view>
103 <!-- type9 关联记录:底部选择,提交 JSON 数组 [{id,name}] --> 89 <!-- type9 关联记录:底部选择,提交 JSON 数组 [{id,name}] -->
104 <up-input v-else-if="item.fieldType === 9" v-model="labelMap[item.fieldCode]" readonly 90 <up-input v-else-if="item.fieldType === 9" v-model="labelMap[item.fieldCode]" readonly
105 - disabled-color="#ffffff" :placeholder="`请选择${item.fieldName}`" border="none"> 91 + disabled-color="#ffffff" :placeholder="`请选择${item.fieldName}`" border="none">
106 <template #right> 92 <template #right>
107 <up-icon name="arrow-right" size="16"></up-icon> 93 <up-icon name="arrow-right" size="16"></up-icon>
108 </template> 94 </template>
109 </up-input> 95 </up-input>
110 <!-- type10 链接:URL 输入,提交裸 URL --> 96 <!-- type10 链接:URL 输入,提交裸 URL -->
111 <up-input v-else-if="item.fieldType === 10" v-model.trim="inspectForm[item.fieldCode]" 97 <up-input v-else-if="item.fieldType === 10" v-model.trim="inspectForm[item.fieldCode]"
112 - :placeholder="`请输入${item.fieldName}(http/https 开头)`"></up-input> 98 + :placeholder="`请输入${item.fieldName}(http/https 开头)`"></up-input>
113 <!-- type11 地理位置:uni.chooseLocation 选点,提交 JSON 对象 {lng,lat,address} --> 99 <!-- type11 地理位置:uni.chooseLocation 选点,提交 JSON 对象 {lng,lat,address} -->
114 <up-input v-else-if="item.fieldType === 11" v-model="labelMap[item.fieldCode]" readonly 100 <up-input v-else-if="item.fieldType === 11" v-model="labelMap[item.fieldCode]" readonly
115 - disabled-color="#ffffff" :placeholder="`点击选择${item.fieldName}`" border="none"> 101 + disabled-color="#ffffff" :placeholder="`点击选择${item.fieldName}`" border="none">
116 <template #right> 102 <template #right>
117 <up-icon name="arrow-right" size="16"></up-icon> 103 <up-icon name="arrow-right" size="16"></up-icon>
118 </template> 104 </template>
@@ -138,38 +124,40 @@ @@ -138,38 +124,40 @@
138 </up-form-item> --> 124 </up-form-item> -->
139 </up-form> 125 </up-form>
140 </view> 126 </view>
141 - <!-- 底部提交按钮 -->  
142 - <view v-if="dynamicFields.length === 0" class="empty-field-tip">  
143 - <text>请去平台配置业务类型字段</text>  
144 -</view>  
145 -<!-- ========== 底部提交按钮:只有存在动态字段才显示 ========== -->  
146 -<view class="fixed-bottom-btn-wrap" v-if="dynamicFields.length > 0">  
147 -<up-button type="primary" text="提交" @click="submitInspect"  
148 -style="width: 100%; height: 88rpx; font-size:32rpx; border-radius: 0;"></up-button>  
149 -</view> 127 + <!-- 底部双按钮 -->
  128 + <view v-if="dynamicFields.length === 0" class="empty-field-tip">
  129 + <text>请去平台配置业务类型字段</text>
  130 + </view>
  131 + <view class="fixed-bottom-btn-wrap" v-if="dynamicFields.length > 0">
  132 + <view class="btn-item btn-left" @click="submitInspect">
  133 + <text>提交</text>
  134 + </view>
  135 + <view class="btn-item btn-right" @click="submitAndFinish">
  136 + <text>提交并结束本次任务</text>
  137 + </view>
  138 + </view>
150 <!-- uview-plus 日期时间选择器(全局复用) --> 139 <!-- uview-plus 日期时间选择器(全局复用) -->
151 <up-datetime-picker :show="datePickerShow" v-model="datePickerValue" mode="datetime" close-on-click-overlay 140 <up-datetime-picker :show="datePickerShow" v-model="datePickerValue" mode="datetime" close-on-click-overlay
152 - @confirm="onDateConfirm" @cancel="onDateClose" @close="onDateClose"></up-datetime-picker> 141 + @confirm="onDateConfirm" @cancel="onDateClose" @close="onDateClose"></up-datetime-picker>
153 <!-- ActionSheet 底部选择弹窗 --> 142 <!-- ActionSheet 底部选择弹窗 -->
154 <up-action-sheet :show="actionSheet.show" :actions="actionSheet.options" @select="onActionSheetSelect" 143 <up-action-sheet :show="actionSheet.show" :actions="actionSheet.options" @select="onActionSheetSelect"
155 - @close="actionSheet.show = false" cancel-text="取消"></up-action-sheet> 144 + @close="actionSheet.show = false" cancel-text="取消"></up-action-sheet>
156 <!-- 分层选项选择器(选项带 path 的动态字段) --> 145 <!-- 分层选项选择器(选项带 path 的动态字段) -->
157 <grouped-picker :show="groupPicker.show" :title="groupPicker.title" :options="groupPicker.options" 146 <grouped-picker :show="groupPicker.show" :title="groupPicker.title" :options="groupPicker.options"
158 - :multiple="groupPicker.multiple" :max-count="groupPicker.maxCount"  
159 - :need-quantity="groupPicker.needQuantity" :value="groupPicker.value"  
160 - :container-width="groupPicker.containerWidth"  
161 - :quantities="groupPicker.quantities" @update:show="groupPicker.show = $event"  
162 - @confirm="onGroupedConfirm"></grouped-picker> 147 + :multiple="groupPicker.multiple" :max-count="groupPicker.maxCount" :need-quantity="groupPicker.needQuantity"
  148 + :value="groupPicker.value" :container-width="groupPicker.containerWidth" :quantities="groupPicker.quantities"
  149 + @update:show="groupPicker.show = $event" @confirm="onGroupedConfirm"></grouped-picker>
163 </view> 150 </view>
164 </template> 151 </template>
165 152
166 <script lang="ts"> 153 <script lang="ts">
167 import { uploadImages } from '@/common/utils/upload' 154 import { uploadImages } from '@/common/utils/upload'
168 -import { maintainCreate } from "@/api/maintain-manage/maintain-manage" 155 +import { maintainCreate, maintainCreateFinish } from "@/api/maintain-manage/maintain-manage"
169 import { getByType, getOptionsByCode, getMaterialByCode, workRecordSubmit } from "@/api/common" 156 import { getByType, getOptionsByCode, getMaterialByCode, workRecordSubmit } from "@/api/common"
170 export default { 157 export default {
171 data() { 158 data() {
172 return { 159 return {
  160 + submitting: false,
173 imagesList: [], 161 imagesList: [],
174 initProgress: 0, 162 initProgress: 0,
175 inspectForm: { 163 inspectForm: {
@@ -234,7 +222,7 @@ export default { @@ -234,7 +222,7 @@ export default {
234 // 数量单位不再传进弹层(原型列表行右侧只有步进器,没有单位),页面行内仍显示 item.unit 222 // 数量单位不再传进弹层(原型列表行右侧只有步进器,没有单位),页面行内仍显示 item.unit
235 unit: '', 223 unit: '',
236 // 新增:由父页面点击时赋值 224 // 新增:由父页面点击时赋值
237 - containerWidth: 0 225 + containerWidth: 0
238 } 226 }
239 } 227 }
240 }, 228 },
@@ -328,15 +316,15 @@ export default { @@ -328,15 +316,15 @@ export default {
328 this.groupPicker.needQuantity = item.fieldType === 6 && this.needQty(item) 316 this.groupPicker.needQuantity = item.fieldType === 6 && this.needQty(item)
329 this.groupPicker.value = multiple ? (this.inspectForm[code] || []).slice() : (this.inspectForm[code] || '') 317 this.groupPicker.value = multiple ? (this.inspectForm[code] || []).slice() : (this.inspectForm[code] || '')
330 this.groupPicker.quantities = Object.assign({}, this.qtyMap[code] || {}) 318 this.groupPicker.quantities = Object.assign({}, this.qtyMap[code] || {})
331 - // ========== 新增:点击打开弹窗瞬间,获取父表单容器宽度 ==========  
332 - const query = uni.createSelectorQuery().in(this)  
333 - query.select('.inspect-form-content').boundingClientRect((rect) => {  
334 - if (rect && rect.width) {  
335 - // rect.width 单位 px,直接传给子组件  
336 - this.groupPicker.containerWidth = rect.width  
337 - }  
338 - this.groupPicker.show = true  
339 - }).exec() 319 + // ========== 新增:点击打开弹窗瞬间,获取父表单容器宽度 ==========
  320 + const query = uni.createSelectorQuery().in(this)
  321 + query.select('.inspect-form-content').boundingClientRect((rect) => {
  322 + if (rect && rect.width) {
  323 + // rect.width 单位 px,直接传给子组件
  324 + this.groupPicker.containerWidth = rect.width
  325 + }
  326 + this.groupPicker.show = true
  327 + }).exec()
340 }, 328 },
341 /** 329 /**
342 * 分层弹层确定回调。写入位置与原有两条路径完全一致(inspectForm/labelMap/qtyMap), 330 * 分层弹层确定回调。写入位置与原有两条路径完全一致(inspectForm/labelMap/qtyMap),
@@ -363,8 +351,8 @@ export default { @@ -363,8 +351,8 @@ export default {
363 // 物料带 currentNum 时在展示名后附库存余量(只影响展示名,提交的 materialName 仍取干净的 opt.label) 351 // 物料带 currentNum 时在展示名后附库存余量(只影响展示名,提交的 materialName 仍取干净的 opt.label)
364 this.actionSheet.options = (item.selectOptions || []).map(opt => { 352 this.actionSheet.options = (item.selectOptions || []).map(opt => {
365 const stock = opt.currentNum === null || opt.currentNum === undefined || opt.currentNum === '' 353 const stock = opt.currentNum === null || opt.currentNum === undefined || opt.currentNum === ''
366 - ? NaN  
367 - : Number(opt.currentNum) 354 + ? NaN
  355 + : Number(opt.currentNum)
368 return { 356 return {
369 name: isNaN(stock) ? opt.label : `${opt.label}(库存${stock}${opt.unit || ''})`, 357 name: isNaN(stock) ? opt.label : `${opt.label}(库存${stock}${opt.unit || ''})`,
370 value: opt.value 358 value: opt.value
@@ -487,9 +475,9 @@ export default { @@ -487,9 +475,9 @@ export default {
487 // 3物料为标量(领用数量),默认 0 = 本次不领用、不扣库存 475 // 3物料为标量(领用数量),默认 0 = 本次不领用、不扣库存
488 if (field.fieldType === 6 && this.needQty(field)) { 476 if (field.fieldType === 6 && this.needQty(field)) {
489 const qty = {} 477 const qty = {}
490 - ;(field.selectOptions || []).forEach(opt => {  
491 - qty[opt.value] = ''  
492 - }) 478 + ; (field.selectOptions || []).forEach(opt => {
  479 + qty[opt.value] = ''
  480 + })
493 this.qtyMap[field.fieldCode] = qty 481 this.qtyMap[field.fieldCode] = qty
494 } 482 }
495 if (field.fieldType === 3 && field.optionSource === 3) { 483 if (field.fieldType === 3 && field.optionSource === 3) {
@@ -657,50 +645,50 @@ export default { @@ -657,50 +645,50 @@ export default {
657 * 7/8/9/11 的值是数组或对象,async-validator 的 required 按默认 string 类型判定, 645 * 7/8/9/11 的值是数组或对象,async-validator 的 required 按默认 string 类型判定,
658 * 会放过「空数组」「null」⇒ 必须用 validator 显式判空,否则未选也能提交 646 * 会放过「空数组」「null」⇒ 必须用 validator 显式判空,否则未选也能提交
659 */ 647 */
660 -buildRequiredRule(field: any) {  
661 - const type = field.fieldType  
662 - if (type === 6 || type === 7 || type === 8) {  
663 - const action = type === 6 ? '请选择' : '请上传'  
664 - return {  
665 - required: true,  
666 - trigger: ['change','blur'],  
667 - validator: (rule, value, callback) => {  
668 - Array.isArray(value) && value.length > 0 ? callback() : callback(new Error(`${action}${field.fieldName}`)) 648 + buildRequiredRule(field: any) {
  649 + const type = field.fieldType
  650 + if (type === 6 || type === 7 || type === 8) {
  651 + const action = type === 6 ? '请选择' : '请上传'
  652 + return {
  653 + required: true,
  654 + trigger: ['change', 'blur'],
  655 + validator: (rule, value, callback) => {
  656 + Array.isArray(value) && value.length > 0 ? callback() : callback(new Error(`${action}${field.fieldName}`))
  657 + }
  658 + }
669 } 659 }
670 - }  
671 - }  
672 - if (type === 9 || type === 11) {  
673 - return {  
674 - required: true,  
675 - trigger: ['change','blur'],  
676 - validator: (rule, value, callback) => {  
677 - value ? callback() : callback(new Error(type === 9 ? `请选择${field.fieldName}` : `请选择${field.fieldName}的位置`)) 660 + if (type === 9 || type === 11) {
  661 + return {
  662 + required: true,
  663 + trigger: ['change', 'blur'],
  664 + validator: (rule, value, callback) => {
  665 + value ? callback() : callback(new Error(type === 9 ? `请选择${field.fieldName}` : `请选择${field.fieldName}的位置`))
  666 + }
  667 + }
678 } 668 }
679 - }  
680 - }  
681 - // ========= 新增:数值类型单独校验 =========  
682 - if (type === 1) {  
683 - return {  
684 - required: true,  
685 - trigger: ['change','blur'],  
686 - validator: (rule, value, callback) => {  
687 - // 空字符串、null、undefined 才是空;数字 1,7,100 全部合法  
688 - if (value === '' || value === null || value === undefined) {  
689 - callback(new Error(`请填写${field.fieldName}`))  
690 - } else {  
691 - callback() 669 + // ========= 新增:数值类型单独校验 =========
  670 + if (type === 1) {
  671 + return {
  672 + required: true,
  673 + trigger: ['change', 'blur'],
  674 + validator: (rule, value, callback) => {
  675 + // 空字符串、null、undefined 才是空;数字 1,7,100 全部合法
  676 + if (value === '' || value === null || value === undefined) {
  677 + callback(new Error(`请填写${field.fieldName}`))
  678 + } else {
  679 + callback()
  680 + }
  681 + }
692 } 682 }
693 } 683 }
694 - }  
695 - }  
696 - // 2文本 /4日期 /5布尔 /10链接  
697 - const action = type === 3 || type === 5 ? '请选择' : '请填写'  
698 - return {  
699 - required: true,  
700 - message: `${action}${field.fieldName}`,  
701 - trigger: ['change','blur']  
702 - }  
703 -}, 684 + // 2文本 /4日期 /5布尔 /10链接
  685 + const action = type === 3 || type === 5 ? '请选择' : '请填写'
  686 + return {
  687 + required: true,
  688 + message: `${action}${field.fieldName}`,
  689 + trigger: ['change', 'blur']
  690 + }
  691 + },
704 692
705 /** 幂等流水号(后端按 userId+clientSeq 去重,命中则返回已存在的 recordId) */ 693 /** 幂等流水号(后端按 userId+clientSeq 去重,命中则返回已存在的 recordId) */
706 genClientSeq() { 694 genClientSeq() {
@@ -727,26 +715,26 @@ buildRequiredRule(field: any) { @@ -727,26 +715,26 @@ buildRequiredRule(field: any) {
727 /** 715 /**
728 * 7图片 上传(u-upload 的 after-read 回调)【对齐工单上传逻辑】 716 * 7图片 上传(u-upload 的 after-read 回调)【对齐工单上传逻辑】
729 */ 717 */
730 -uploadFieldFiles(item: any, event: any) {  
731 - console.log('动态图片上传', item.fieldName, event)  
732 - const rawFile = event && event.file  
733 - const fileList = Array.isArray(rawFile) ? rawFile : [rawFile]  
734 - const targetImgList = this.fileListMap[item.fieldCode] || (this.fileListMap[item.fieldCode] = [])  
735 - const filePaths = fileList.map(file => file.url)  
736 - // const tempItems = fileList.map(item => ({  
737 - // ...item,  
738 - // status: 'uploading',  
739 - // message: '上传中'  
740 - // }))  
741 - // const startIndex = targetImgList.length  
742 - // targetImgList.push(...tempItems) 718 + uploadFieldFiles(item: any, event: any) {
  719 + console.log('动态图片上传', item.fieldName, event)
  720 + const rawFile = event && event.file
  721 + const fileList = Array.isArray(rawFile) ? rawFile : [rawFile]
  722 + const targetImgList = this.fileListMap[item.fieldCode] || (this.fileListMap[item.fieldCode] = [])
  723 + const filePaths = fileList.map(file => file.url)
  724 + // const tempItems = fileList.map(item => ({
  725 + // ...item,
  726 + // status: 'uploading',
  727 + // message: '上传中'
  728 + // }))
  729 + // const startIndex = targetImgList.length
  730 + // targetImgList.push(...tempItems)
743 731
744 - this.uploadToField(item, fileList.filter(file => file && file.url).map(file => ({  
745 - url: file.url,  
746 - name: file.name || '',  
747 - size: file.size  
748 - })))  
749 -}, 732 + this.uploadToField(item, fileList.filter(file => file && file.url).map(file => ({
  733 + url: file.url,
  734 + name: file.name || '',
  735 + size: file.size
  736 + })))
  737 + },
750 738
751 /** 739 /**
752 * 8附件 选择并上传。 740 * 8附件 选择并上传。
@@ -783,65 +771,65 @@ uploadFieldFiles(item: any, event: any) { @@ -783,65 +771,65 @@ uploadFieldFiles(item: any, event: any) {
783 * 把选中的本地文件逐个上传并把 http 地址回填到 fileListMap / inspectForm。 771 * 把选中的本地文件逐个上传并把 http 地址回填到 fileListMap / inspectForm。
784 * 7图片与8附件共用(uploadImages 是通用 uni.uploadFile,不限图片类型) 772 * 7图片与8附件共用(uploadImages 是通用 uni.uploadFile,不限图片类型)
785 */ 773 */
786 - async uploadToField(item: any, files: any[]) {  
787 -const code = item.fieldCode  
788 -const maxCount = this.maxCountOf(item, 5)  
789 -const maxSizeMb = this.maxSizeMbOf(item, 5)  
790 -const validFiles = (files || []).filter(file => file && file.url && !this.isOversize(file, maxSizeMb))  
791 -if (!validFiles.length) {  
792 -return  
793 -}  
794 -const targetList = this.fileListMap[code] || (this.fileListMap[code] = [])  
795 -if (targetList.length + validFiles.length > maxCount) {  
796 -uni.showToast({ title: `最多只能上传${maxCount}个`, icon: 'none' })  
797 -return  
798 -}  
799 -// 保存起始下标  
800 -const startIndex = targetList.length  
801 -// 只在这里push一次  
802 -targetList.push(...validFiles.map(file => ({  
803 -url: file.url, status: 'uploading', message: '上传中', name: file.name || ''  
804 -})))  
805 -try {  
806 -const uploadResultUrls = await uploadImages({  
807 -filePaths: validFiles.map(file => file.url),  
808 -ignoreError: true 774 + async uploadToField(item: any, files: any[]) {
  775 + const code = item.fieldCode
  776 + const maxCount = this.maxCountOf(item, 5)
  777 + const maxSizeMb = this.maxSizeMbOf(item, 5)
  778 + const validFiles = (files || []).filter(file => file && file.url && !this.isOversize(file, maxSizeMb))
  779 + if (!validFiles.length) {
  780 + return
  781 + }
  782 + const targetList = this.fileListMap[code] || (this.fileListMap[code] = [])
  783 + if (targetList.length + validFiles.length > maxCount) {
  784 + uni.showToast({ title: `最多只能上传${maxCount}个`, icon: 'none' })
  785 + return
  786 + }
  787 + // 保存起始下标
  788 + const startIndex = targetList.length
  789 + // 只在这里push一次
  790 + targetList.push(...validFiles.map(file => ({
  791 + url: file.url, status: 'uploading', message: '上传中', name: file.name || ''
  792 + })))
  793 + try {
  794 + const uploadResultUrls = await uploadImages({
  795 + filePaths: validFiles.map(file => file.url),
  796 + ignoreError: true
809 }) 797 })
810 -uploadResultUrls.forEach((url, index) => {  
811 -// 用startIndex + index 替换对应条目  
812 -if (targetList[startIndex + index]) {  
813 -targetList.splice(startIndex + index, 1, {  
814 -url, status: 'success', message: '', name: validFiles[index].name || '' 798 + uploadResultUrls.forEach((url, index) => {
  799 + // 用startIndex + index 替换对应条目
  800 + if (targetList[startIndex + index]) {
  801 + targetList.splice(startIndex + index, 1, {
  802 + url, status: 'success', message: '', name: validFiles[index].name || ''
815 }) 803 })
816 } 804 }
817 }) 805 })
818 -for (let i = uploadResultUrls.length; i < validFiles.length; i++) {  
819 -if (targetList[startIndex + i]) {  
820 -targetList.splice(startIndex + i, 1, {  
821 -url: validFiles[i].url, status: 'failed', message: '上传失败', name: validFiles[i].name || '' 806 + for (let i = uploadResultUrls.length; i < validFiles.length; i++) {
  807 + if (targetList[startIndex + i]) {
  808 + targetList.splice(startIndex + i, 1, {
  809 + url: validFiles[i].url, status: 'failed', message: '上传失败', name: validFiles[i].name || ''
822 }) 810 })
823 } 811 }
824 } 812 }
825 -if (uploadResultUrls.length < validFiles.length) {  
826 -uni.showToast({  
827 -title: `成功${uploadResultUrls.length}个,失败${validFiles.length - uploadResultUrls.length}个`,  
828 -icon: 'none' 813 + if (uploadResultUrls.length < validFiles.length) {
  814 + uni.showToast({
  815 + title: `成功${uploadResultUrls.length}个,失败${validFiles.length - uploadResultUrls.length}个`,
  816 + icon: 'none'
829 }) 817 })
830 } 818 }
831 } catch (err) { 819 } catch (err) {
832 -console.error(`【${code}】文件上传失败:`, err)  
833 -for (let i = 0; i < validFiles.length; i++) {  
834 -if (targetList[startIndex + i]) {  
835 -targetList.splice(startIndex + i, 1, {  
836 -url: validFiles[i].url, status: 'failed', message: '上传失败', name: validFiles[i].name || '' 820 + console.error(`【${code}】文件上传失败:`, err)
  821 + for (let i = 0; i < validFiles.length; i++) {
  822 + if (targetList[startIndex + i]) {
  823 + targetList.splice(startIndex + i, 1, {
  824 + url: validFiles[i].url, status: 'failed', message: '上传失败', name: validFiles[i].name || ''
837 }) 825 })
838 } 826 }
839 } 827 }
840 -uni.showToast({ title: '上传失败,请重试', icon: 'none' }) 828 + uni.showToast({ title: '上传失败,请重试', icon: 'none' })
841 } 829 }
842 -this.syncFileValue(item)  
843 -this.$refs.inspectFormRef.validateField(code)  
844 -}, 830 + this.syncFileValue(item)
  831 + this.$refs.inspectFormRef.validateField(code)
  832 + },
845 833
846 /** 834 /**
847 * 删除 7图片/8附件 的某一个已选文件 835 * 删除 7图片/8附件 的某一个已选文件
@@ -893,12 +881,12 @@ this.$refs.inspectFormRef.validateField(code) @@ -893,12 +881,12 @@ this.$refs.inspectFormRef.validateField(code)
893 fields.push(row) 881 fields.push(row)
894 } else if (type === 3) { 882 } else if (type === 3) {
895 // 3枚举:单选取裸 value;历史「枚举+是否多选」存逗号分隔 883 // 3枚举:单选取裸 value;历史「枚举+是否多选」存逗号分隔
896 - fields.push({  
897 - fieldCode: code,  
898 - valueText: item.multiple === 1  
899 - ? (Array.isArray(value) ? value.join(',') : String(value || ''))  
900 - : String(value)  
901 -}) 884 + fields.push({
  885 + fieldCode: code,
  886 + valueText: item.multiple === 1
  887 + ? (Array.isArray(value) ? value.join(',') : String(value || ''))
  888 + : String(value)
  889 + })
902 } else if (type === 6) { 890 } else if (type === 6) {
903 const selected = (item.selectOptions || []).filter(opt => (value || []).indexOf(opt.value) > -1) 891 const selected = (item.selectOptions || []).filter(opt => (value || []).indexOf(opt.value) > -1)
904 if (this.needQty(item)) { 892 if (this.needQty(item)) {
@@ -934,6 +922,70 @@ this.$refs.inspectFormRef.validateField(code) @@ -934,6 +922,70 @@ this.$refs.inspectFormRef.validateField(code)
934 } 922 }
935 return fields 923 return fields
936 }, 924 },
  925 +
  926 + async submitAndFinish() {
  927 +
  928 + try {
  929 + await this.$refs.inspectFormRef.validate()
  930 + const dynamicValues = this.buildDynamicFields()
  931 + if (!this.clientSeq) {
  932 + this.clientSeq = this.genClientSeq()
  933 + }
  934 + uni.showLoading({ title: '提交中...' })
  935 + console.log('dsahdjashj')
  936 + // this.submitting = true
  937 +
  938 + // if (!this.commitId) {
  939 + // const submitData = {
  940 + // planNo: this.paramsOptins.planNo,
  941 + // imgHost: this.getImgUrlList(this.imagesList),
  942 + // commonUserList: [],
  943 + // remark: this.inspectForm.remark.trim(),
  944 + // }
  945 + // const commitRes: any = await maintainCreate(submitData)
  946 + // this.commitId = commitRes && typeof commitRes === 'object' ? commitRes.id : commitRes
  947 + // }
  948 +
  949 + // if (dynamicValues.length > 0 && this.commitId) {
  950 + // await workRecordSubmit({
  951 + // businessType: this.paramsOptins.businessType,
  952 + // bizType: 3,
  953 + // bizId: this.commitId,
  954 + // workTypeId: Number(this.paramsOptins.planTypeId),
  955 + // clientSeq: this.clientSeq,
  956 + // remark: this.inspectForm.remark.trim(),
  957 + // fields: dynamicValues
  958 + // })
  959 + // }
  960 +
  961 + // 新增:调用【提交并结束】接口
  962 + await maintainCreateFinish({
  963 + submitData
  964 + })
  965 +
  966 + uni.hideLoading()
  967 + uni.showToast({ title: '提交并结束任务成功', icon: 'success', duration: 1000 })
  968 + setTimeout(() => {
  969 + uni.reLaunch({ url: '/pages-sub/daily/maintain-manage/index' })
  970 + }, 1000)
  971 + } catch (error) {
  972 + uni.hideLoading()
  973 + // if (Array.isArray(error)) return
  974 + // console.error('提交并结束失败', error)
  975 + // const backendMsg = error && error.msg
  976 + // if (this.commitId) {
  977 + // uni.showToast({
  978 + // title: backendMsg ? `养护记录已提交:${backendMsg}` : '养护记录已提交,结束任务接口失败',
  979 + // icon: 'none', duration: 2500
  980 + // })
  981 + // } else if (!backendMsg) {
  982 + // uni.showToast({ title: '提交失败,请重试', icon: 'none', duration: 2000 })
  983 + // }
  984 + } finally {
  985 + // this.submitting = false
  986 + }
  987 + },
  988 +
937 /** 989 /**
938 * 提交养护记录 —— 双写两条链路: 990 * 提交养护记录 —— 双写两条链路:
939 * ① maintain-plan-commit/create:养护计划本身(描述 + 图片 + 人员),返回 commit.id 991 * ① maintain-plan-commit/create:养护计划本身(描述 + 图片 + 人员),返回 commit.id
@@ -942,11 +994,12 @@ this.$refs.inspectFormRef.validateField(code) @@ -942,11 +994,12 @@ this.$refs.inspectFormRef.validateField(code)
942 */ 994 */
943 async submitInspect() { 995 async submitInspect() {
944 // 调试打印,打开小程序控制台查看 996 // 调试打印,打开小程序控制台查看
945 - console.log('=====调试=====')  
946 - console.log('dynamicFields', this.dynamicFields)  
947 - this.dynamicFields.forEach(item=>{  
948 - console.log(item.fieldCode, this.inspectForm[item.fieldCode], typeof this.inspectForm[item.fieldCode])  
949 - }) 997 + console.log('=====调试=====')
  998 + console.log('dynamicFields', this.dynamicFields)
  999 + if (this.submitting) return
  1000 + this.dynamicFields.forEach(item => {
  1001 + console.log(item.fieldCode, this.inspectForm[item.fieldCode], typeof this.inspectForm[item.fieldCode])
  1002 + })
950 try { 1003 try {
951 await this.$refs.inspectFormRef.validate() 1004 await this.$refs.inspectFormRef.validate()
952 const dynamicValues = this.buildDynamicFields() 1005 const dynamicValues = this.buildDynamicFields()
@@ -954,6 +1007,7 @@ this.$refs.inspectFormRef.validateField(code) @@ -954,6 +1007,7 @@ this.$refs.inspectFormRef.validateField(code)
954 this.clientSeq = this.genClientSeq() 1007 this.clientSeq = this.genClientSeq()
955 } 1008 }
956 uni.showLoading({ title: '提交中...' }) 1009 uni.showLoading({ title: '提交中...' })
  1010 + this.submitting = true
957 // ① 养护计划本体:仅首次提交时写入 1011 // ① 养护计划本体:仅首次提交时写入
958 if (!this.commitId) { 1012 if (!this.commitId) {
959 const submitData = { 1013 const submitData = {
@@ -1017,11 +1071,13 @@ this.$refs.inspectFormRef.validateField(code) @@ -1017,11 +1071,13 @@ this.$refs.inspectFormRef.validateField(code)
1017 .page-container { 1071 .page-container {
1018 padding-bottom: 120rpx; 1072 padding-bottom: 120rpx;
1019 } 1073 }
  1074 +
1020 .inspect-form-content { 1075 .inspect-form-content {
1021 background: #fff; 1076 background: #fff;
1022 padding: 20rpx; 1077 padding: 20rpx;
1023 margin-bottom: 20rpx; 1078 margin-bottom: 20rpx;
1024 } 1079 }
  1080 +
1025 .date-input-wrap { 1081 .date-input-wrap {
1026 width: 100%; 1082 width: 100%;
1027 } 1083 }
@@ -1033,35 +1089,42 @@ this.$refs.inspectFormRef.validateField(code) @@ -1033,35 +1089,42 @@ this.$refs.inspectFormRef.validateField(code)
1033 height: 72rpx; 1089 height: 72rpx;
1034 font-size: 32rpx; 1090 font-size: 32rpx;
1035 } 1091 }
  1092 +
1036 :deep(.u-number-box__btn) { 1093 :deep(.u-number-box__btn) {
1037 width: 64rpx; 1094 width: 64rpx;
1038 height: 72rpx; 1095 height: 72rpx;
1039 } 1096 }
1040 } 1097 }
  1098 +
1041 .number-box-wrap { 1099 .number-box-wrap {
1042 display: flex; 1100 display: flex;
1043 align-items: center; 1101 align-items: center;
1044 gap: 16rpx; 1102 gap: 16rpx;
  1103 +
1045 .unit-text { 1104 .unit-text {
1046 font-size: 28rpx; 1105 font-size: 28rpx;
1047 color: #333; 1106 color: #333;
1048 } 1107 }
1049 } 1108 }
  1109 +
1050 // 6多选:复选框组 + needQuantity 时每个已选项一行数量输入 1110 // 6多选:复选框组 + needQuantity 时每个已选项一行数量输入
1051 .multi-wrap { 1111 .multi-wrap {
1052 width: 100%; 1112 width: 100%;
  1113 +
1053 .multi-hint { 1114 .multi-hint {
1054 display: block; 1115 display: block;
1055 margin-top: 8rpx; 1116 margin-top: 8rpx;
1056 font-size: 24rpx; 1117 font-size: 24rpx;
1057 color: #909399; 1118 color: #909399;
1058 } 1119 }
  1120 +
1059 .qty-list { 1121 .qty-list {
1060 margin-top: 16rpx; 1122 margin-top: 16rpx;
1061 padding-left: 20rpx; 1123 padding-left: 20rpx;
1062 border-left: 4rpx solid #f0f0f0; 1124 border-left: 4rpx solid #f0f0f0;
1063 } 1125 }
1064 } 1126 }
  1127 +
1065 // 数量行(6多选的每个已选项 / 3物料的领用数量共用) 1128 // 数量行(6多选的每个已选项 / 3物料的领用数量共用)
1066 .qty-row { 1129 .qty-row {
1067 display: flex; 1130 display: flex;
@@ -1069,6 +1132,7 @@ this.$refs.inspectFormRef.validateField(code) @@ -1069,6 +1132,7 @@ this.$refs.inspectFormRef.validateField(code)
1069 justify-content: space-between; 1132 justify-content: space-between;
1070 gap: 16rpx; 1133 gap: 16rpx;
1071 padding: 8rpx 0; 1134 padding: 8rpx 0;
  1135 +
1072 .qty-label { 1136 .qty-label {
1073 flex: 1; 1137 flex: 1;
1074 min-width: 0; 1138 min-width: 0;
@@ -1076,20 +1140,24 @@ this.$refs.inspectFormRef.validateField(code) @@ -1076,20 +1140,24 @@ this.$refs.inspectFormRef.validateField(code)
1076 color: #333; 1140 color: #333;
1077 } 1141 }
1078 } 1142 }
  1143 +
1079 .qty-row-material { 1144 .qty-row-material {
1080 margin-top: 12rpx; 1145 margin-top: 12rpx;
1081 padding-top: 12rpx; 1146 padding-top: 12rpx;
1082 border-top: 1rpx solid #f0f0f0; 1147 border-top: 1rpx solid #f0f0f0;
1083 } 1148 }
  1149 +
1084 // 8附件:自绘文件列表 + 选择按钮(微信无通用文件选择器,走 chooseMessageFile) 1150 // 8附件:自绘文件列表 + 选择按钮(微信无通用文件选择器,走 chooseMessageFile)
1085 .file-wrap { 1151 .file-wrap {
1086 width: 100%; 1152 width: 100%;
  1153 +
1087 .file-row { 1154 .file-row {
1088 display: flex; 1155 display: flex;
1089 align-items: center; 1156 align-items: center;
1090 justify-content: space-between; 1157 justify-content: space-between;
1091 gap: 16rpx; 1158 gap: 16rpx;
1092 padding: 8rpx 0; 1159 padding: 8rpx 0;
  1160 +
1093 .file-name { 1161 .file-name {
1094 flex: 1; 1162 flex: 1;
1095 min-width: 0; 1163 min-width: 0;
@@ -1099,6 +1167,7 @@ this.$refs.inspectFormRef.validateField(code) @@ -1099,6 +1167,7 @@ this.$refs.inspectFormRef.validateField(code)
1099 white-space: nowrap; 1167 white-space: nowrap;
1100 text-overflow: ellipsis; 1168 text-overflow: ellipsis;
1101 } 1169 }
  1170 +
1102 .file-del { 1171 .file-del {
1103 font-size: 26rpx; 1172 font-size: 26rpx;
1104 color: #ff4d4f; 1173 color: #ff4d4f;
@@ -1113,5 +1182,38 @@ this.$refs.inspectFormRef.validateField(code) @@ -1113,5 +1182,38 @@ this.$refs.inspectFormRef.validateField(code)
1113 color: #999; 1182 color: #999;
1114 } 1183 }
1115 1184
  1185 +.fixed-bottom-btn-wrap {
  1186 + background-color: #ffffff;
  1187 + display: flex;
  1188 + .btn-item {
  1189 + flex: 1;
  1190 + height: 88rpx;
  1191 + display: flex;
  1192 + align-items: center;
  1193 + justify-content: center;
  1194 + font-size: 32rpx;
  1195 + color: #fff;
  1196 + }
  1197 +
  1198 + .btn-left {
  1199 + background: #1989fa;
  1200 + }
  1201 +
  1202 + .btn-right {
  1203 + background: #8b572a;
  1204 + /* 褐色 */
  1205 + }
  1206 +}
  1207 +
  1208 +// 页面底部预留空白,防止表单内容被固定按钮遮挡
  1209 +.page-container {
  1210 + padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
  1211 +}
1116 1212
  1213 +.empty-field-tip {
  1214 + text-align: center;
  1215 + padding: 80rpx 20rpx;
  1216 + font-size: 30rpx;
  1217 + color: #999;
  1218 +}
1117 </style> 1219 </style>
pages-sub/daily/maintain-manage/finish-plan-detail.vue
@@ -154,7 +154,7 @@ @@ -154,7 +154,7 @@
154 154
155 <script setup lang="ts"> 155 <script setup lang="ts">
156 import {ref} from 'vue'; 156 import {ref} from 'vue';
157 -import {detailList} from "@/api/maintain-manage/maintain-manage"; 157 +import {detailList,getDetailList} from "@/api/maintain-manage/maintain-manage";
158 import {onLoad} from '@dcloudio/uni-app'; 158 import {onLoad} from '@dcloudio/uni-app';
159 import {timeFormat} from '@/uni_modules/uview-plus'; 159 import {timeFormat} from '@/uni_modules/uview-plus';
160 160
@@ -199,15 +199,15 @@ const previewImage = (currentUrl, allUrls) =&gt; { @@ -199,15 +199,15 @@ const previewImage = (currentUrl, allUrls) =&gt; {
199 /** 199 /**
200 * 获取工单详情 200 * 获取工单详情
201 */ 201 */
202 -const getOrderDetail = async (planNo: string) => { 202 +const getOrderDetail = async (id: string) => {
203 try { 203 try {
204 loading.value = true; 204 loading.value = true;
205 let queryData = { 205 let queryData = {
206 - planNo: planNo,  
207 - pageSize: 100,  
208 - pageNo: 1, 206 + id: id,
  207 + // pageSize: 100,
  208 + // pageNo: 1,
209 }; 209 };
210 - const res = await detailList(queryData); 210 + const res = await getDetailList(queryData);
211 console.log('接口返回:', res); 211 console.log('接口返回:', res);
212 212
213 // 修复2:正确解析接口数据(res.data.list 而非 res.list) 213 // 修复2:正确解析接口数据(res.data.list 而非 res.list)
@@ -527,9 +527,9 @@ const openGeo = (geo) =&gt; { @@ -527,9 +527,9 @@ const openGeo = (geo) =&gt; {
527 527
528 // 页面加载 528 // 页面加载
529 onLoad((options) => { 529 onLoad((options) => {
530 - const {planNo} = options;  
531 - if (planNo) {  
532 - getOrderDetail(planNo); 530 + const {id} = options;
  531 + if (id) {
  532 + getOrderDetail(id);
533 } else { 533 } else {
534 loading.value = false; 534 loading.value = false;
535 uni.showToast({title: '缺少工单ID', icon: 'none'}); 535 uni.showToast({title: '缺少工单ID', icon: 'none'});
pages-sub/daily/maintain-manage/index.vue
@@ -185,7 +185,7 @@ @@ -185,7 +185,7 @@
185 <script setup> 185 <script setup>
186 import { ref, computed } from 'vue' 186 import { ref, computed } from 'vue'
187 import { onLoad, onShow } from '@dcloudio/uni-app' 187 import { onLoad, onShow } from '@dcloudio/uni-app'
188 -import { roadPage, planTypePage } from "@/api/maintain-manage/maintain-manage"; 188 +import { maintainPlan } from "@/api/maintain-manage/maintain-manage";
189 import { timeFormat } from '@/uni_modules/uview-plus'; 189 import { timeFormat } from '@/uni_modules/uview-plus';
190 190
191 // 初始化响应式数据 191 // 初始化响应式数据
@@ -274,26 +274,14 @@ const handleSearch = () =&gt; { @@ -274,26 +274,14 @@ const handleSearch = () =&gt; {
274 const fetchData = async (pageNo, pageSize) => { 274 const fetchData = async (pageNo, pageSize) => {
275 try { 275 try {
276 let res = null 276 let res = null
277 - // 道路类型请求  
278 - if (activeType.value === '道路') {  
279 - res = await roadPage({ 277 + const currentType = typeTabs.value.find(item => item.name === activeType.value)
  278 + res = await maintainPlan({
  279 + // planTypeId: currentType?.planTypeId || '',
280 roadName: searchValue.value.trim(), 280 roadName: searchValue.value.trim(),
281 pageNo, 281 pageNo,
282 pageSize, 282 pageSize,
283 - finishState: activeStatus.value 283 + // finishState: activeStatus.value
284 }) 284 })
285 - } else {  
286 - // 其他养护类型请求  
287 - // 找到当前类型对应的planTypeId  
288 - const currentType = typeTabs.value.find(item => item.name === activeType.value)  
289 - res = await planTypePage({  
290 - planTypeId: currentType?.planTypeId || '',  
291 - planName: searchValue.value.trim(),  
292 - pageNo,  
293 - pageSize,  
294 - finishState: activeStatus.value  
295 - })  
296 - }  
297 285
298 // 告诉z-paging数据加载完成 286 // 告诉z-paging数据加载完成
299 // res.list为当前页数据,res.total为总条数(如果接口返回) 287 // res.list为当前页数据,res.total为总条数(如果接口返回)
@@ -313,8 +301,9 @@ const goToDetail = (item) =&gt; { @@ -313,8 +301,9 @@ const goToDetail = (item) =&gt; {
313 // }) 301 // })
314 302
315 uni.navigateTo({ 303 uni.navigateTo({
316 - url: `/pages-sub/daily/maintain-manage/type-list?roadId=${item.roadId}&finish_state=${item.finishState || ''} &businessType=${item.busiLine}`  
317 - }) 304 + url: `/pages-sub/daily/maintain-manage/type-list?roadName=${encodeURIComponent(item.roadName)}&levelId=${item.levelId}`
  305 +})
  306 +
318 } 307 }
319 308
320 // 提交记录 309 // 提交记录
pages-sub/daily/maintain-manage/now-list-page.vue
1 <template> 1 <template>
2 - <view class="page-wrap">  
3 - <!-- 顶部提示栏 -->  
4 - <view class="tip-bar">  
5 - 一个任务可提交多次上报记录,每一次作业需如实上报。  
6 - </view>  
7 -  
8 - <!-- 上报记录列表 -->  
9 - <view class="list-wrap" v-if="reportList.length > 0">  
10 - <view class="report-item" v-for="(item, index) in reportList" :key="index">  
11 - <view class="item-title">【第{{ item.no }}次上报】{{ item.taskName }}</view>  
12 -  
13 - <view class="item-row item-row-bottom">  
14 - <view class="item-time">提交时间:{{ item.submitTime }}</view>  
15 - <view class="look-btn" @click="handleView(item)">查看</view> 2 + <view class="page-wrap">
  3 + <!-- 顶部提示栏 -->
  4 + <view class="tip-bar">
  5 + 一个任务可提交多次上报记录,每一次作业需如实上报。
  6 + </view>
  7 + <!-- 上报记录列表 -->
  8 + <view class="list-wrap" v-if="reportList.length > 0">
  9 + <view class="report-item" v-for="(item, index) in reportList" :key="index">
  10 + <view class="item-title">【第{{ index+1 }}次上报】{{ item.planName }}{{ item.planName }}</view>
  11 + <view class="item-row item-row-bottom">
  12 + <view class="item-time">提交时间:{{ item.submitTime }}</view>
  13 + <view class="look-btn" @click="handleView(item)">查看</view>
  14 + </view>
  15 + </view>
  16 + </view>
  17 + <!-- 暂无数据 -->
  18 + <view class="empty-wrap" v-else>
  19 + <empty-view text="暂无数据"></empty-view>
  20 + </view>
  21 + <!-- 底部固定按钮 -->
  22 + <view class="fixed-bottom-btn-wrap">
  23 + <up-button type="primary" size="large" block @click="handleAddReport">上报本次记录</up-button>
16 </view> 24 </view>
17 - </view>  
18 - </view>  
19 -  
20 - <!-- 暂无数据 -->  
21 - <view class="empty-wrap" v-else>  
22 - <empty-view text="暂无数据"></empty-view>  
23 - </view>  
24 -  
25 - <!-- 底部固定按钮 -->  
26 - <view class="fixed-bottom-btn-wrap">  
27 - <up-button type="primary" size="large" block @click="handleAddReport">上报本次记录</up-button>  
28 </view> 25 </view>
29 - </view>  
30 </template> 26 </template>
31 -  
32 <script setup lang="ts"> 27 <script setup lang="ts">
33 import { ref } from 'vue' 28 import { ref } from 'vue'
  29 +import { onShow, onLoad } from '@dcloudio/uni-app'
  30 +import { maintainPlanList } from "@/api/maintain-manage/maintain-manage";
34 31
35 // 上报记录数据 32 // 上报记录数据
36 -const reportList = ref([  
37 - {  
38 - no:3,  
39 - taskName:'德胜门东大街月季补植',  
40 - varityName:'果汁阳台月季',  
41 - submitTime:'2026-06-22 14:34:33'  
42 - },  
43 - {  
44 - no:2,  
45 - taskName:'德胜门东大街月季补植',  
46 - varityName:'蓝色风暴月季',  
47 - submitTime:'2026-06-21 10:34:33'  
48 - },  
49 - {  
50 - no:1,  
51 - taskName:'德胜门东大街月季补植',  
52 - varityName:'金丝雀月季',  
53 - submitTime:'2026-06-20 10:34:33'  
54 - }  
55 -]) 33 +const reportList = ref<any[]>([])
  34 +let planNo = ref('')
  35 +let planTypeId = ref('')
  36 +let businessType = ref('')
  37 +// onLoad接收url传过来的planNo
  38 +onLoad((options) => {
  39 + if (options.planNo) {
  40 + planNo.value = options.planNo
  41 + planTypeId.value = options.planTypeId
  42 + businessType.value = options.businessType
  43 + // planTypeId=${item.planTypeId}&businessType=${item.busiLine}
  44 + getReportList()
  45 + } else {
  46 + uni.showToast({ title: 'planNo参数缺失', icon: 'none' })
  47 + }
  48 +})
  49 +
  50 +// 请求上报记录接口
  51 +const getReportList = async () => {
  52 + console.log('3123')
  53 + uni.showLoading({ title: '加载中...', mask: true })
  54 + try {
  55 + // 传入planNo调取接口
  56 + let data = {
  57 + planNo: planNo.value,
  58 + pageNo: 1,
  59 + pageSize: 10
  60 + }
  61 + const res = await maintainPlanList(data)
  62 + // 接口返回list,无数据则reportList为空,页面自动显示empty
  63 + reportList.value = res?.list || []
  64 + } catch (err) {
  65 + console.error('获取上报记录失败', err)
  66 + uni.showToast({ title: '加载失败', icon: 'none' })
  67 + } finally {
  68 + uni.hideLoading()
  69 + }
  70 +}
56 71
57 // 查看上报详情 72 // 查看上报详情
58 const handleView = (item) => { 73 const handleView = (item) => {
59 - console.log('查看详情', item)  
60 - uni.navigateTo({  
61 - url: `/pages-sub/daily/maintain-manage/finish-plan-detail`  
62 - }) 74 + console.log('查看详情', item)
  75 + uni.navigateTo({
  76 + url: `/pages-sub/daily/maintain-manage/finish-plan-detail?id=${item.commitId}`
  77 + })
63 } 78 }
64 -  
65 -// 新增上报记录 79 +// 新增上报记录,把planNo带到新增页面
66 const handleAddReport = () => { 80 const handleAddReport = () => {
67 - console.log('上报本次记录')  
68 - uni.navigateTo({  
69 - url: `/pages-sub/daily/maintain-manage/add-record`  
70 - }) 81 + console.log('上报本次记录')
  82 + uni.navigateTo({
  83 + url: `/pages-sub/daily/maintain-manage/add-record?planNo=${planNo.value}&planTypeId=${planTypeId.value}&businessType=${businessType.value}`
  84 + })
71 } 85 }
72 </script> 86 </script>
73 -  
74 <style scoped lang="scss"> 87 <style scoped lang="scss">
75 .page-wrap { 88 .page-wrap {
76 - min-height: 100vh;  
77 - padding-bottom: 120rpx; // 给底部按钮预留空间  
78 - background: #ffffff; 89 + min-height: 100vh;
  90 + padding-bottom: 120rpx; // 给底部按钮预留空间
  91 + background: #ffffff;
79 } 92 }
  93 +
80 // 顶部黄色提示条 94 // 顶部黄色提示条
81 .tip-bar { 95 .tip-bar {
82 - background-color: #f9e0a2;  
83 - color: #6b4b00;  
84 - font-size: 28rpx;  
85 - padding:24rpx 20rpx;  
86 - line-height:1.5; 96 + background-color: #f9e0a2;
  97 + color: #6b4b00;
  98 + font-size: 28rpx;
  99 + padding: 24rpx 20rpx;
  100 + line-height: 1.5;
87 } 101 }
88 102
89 .list-wrap { 103 .list-wrap {
90 - padding: 0; 104 + padding: 0;
91 } 105 }
  106 +
92 .report-item { 107 .report-item {
93 - padding:30rpx 24rpx;  
94 - border-bottom:1rpx solid #eee;  
95 - .item-title {  
96 - font-size:32rpx;  
97 - color:#222;  
98 - font-weight:500;  
99 - margin-bottom:20rpx;  
100 - }  
101 - .item-row {  
102 - font-size:28rpx;  
103 - color:#555;  
104 - margin-bottom:12rpx;  
105 - }  
106 - .item-row-bottom {  
107 - display:flex;  
108 - justify-content:space-between;  
109 - align-items:center;  
110 - margin-bottom:0;  
111 - }  
112 - .look-btn {  
113 - font-size:28rpx;  
114 - color:#1989fa;  
115 - } 108 + padding: 30rpx 24rpx;
  109 + border-bottom: 1rpx solid #eee;
  110 +
  111 + .item-title {
  112 + font-size: 32rpx;
  113 + color: #222;
  114 + font-weight: 500;
  115 + margin-bottom: 20rpx;
  116 + }
  117 +
  118 + .item-row {
  119 + font-size: 28rpx;
  120 + color: #555;
  121 + margin-bottom: 12rpx;
  122 + }
  123 +
  124 + .item-row-bottom {
  125 + display: flex;
  126 + justify-content: space-between;
  127 + align-items: center;
  128 + margin-bottom: 0;
  129 + }
  130 +
  131 + .look-btn {
  132 + font-size: 28rpx;
  133 + color: #1989fa;
  134 + }
116 } 135 }
  136 +
117 .empty-wrap { 137 .empty-wrap {
118 - padding-top: 120rpx; 138 + padding-top: 120rpx;
119 } 139 }
120 140
  141 +.fixed-bottom-btn-wrap {
  142 + // position: fixed;
  143 + // bottom: 0;
  144 + // left:0;
  145 + // width:100%;
  146 + // padding:20rpx;
  147 + // background:#fff;
  148 +}
121 </style> 149 </style>
pages-sub/daily/maintain-manage/type-list-page.vue
1 <template> 1 <template>
2 - <view class="page-container">  
3 - <up-sticky>  
4 - <view class="sticky-wrap">  
5 - <up-tabs  
6 - v-model="activeStatus"  
7 - :list="statusTabs"  
8 - line-width="40rpx"  
9 - active-color="#1989fa"  
10 - inactive-color="#666"  
11 - :scrollable="false"  
12 - @change="handleStatusChange"  
13 - ></up-tabs>  
14 - </view>  
15 - </up-sticky>  
16 - <z-paging  
17 - ref="pagingRef"  
18 - v-model="currentList"  
19 - @query="fetchData"  
20 - :auto-show-system-loading="true"  
21 - >  
22 - <template #empty>  
23 - <empty-view/>  
24 - </template>  
25 - <view class="card-list">  
26 - <view  
27 - class="task-card"  
28 - v-for="(item,index) in currentList"  
29 - :key="`${item.planNo}_${index}`"  
30 - >  
31 - <!-- 第一行:任务名称 + 置为失效 -->  
32 - <view class="row row-1">  
33 - <view class="plan-name u-line-1">{{ item.planName || '无计划名称' }}</view>  
34 - <view  
35 - v-if="isMaintainLeader"  
36 - class="btn-set-invalid"  
37 - @click.stop="handleSetInvalid(item)"  
38 - >  
39 - 置为失效 2 + <view class="page-container">
  3 + <up-sticky>
  4 + <view class="sticky-wrap">
  5 + <up-tabs v-model="activeStatus" :list="statusTabs" line-width="40rpx" active-color="#1989fa"
  6 + inactive-color="#666" :scrollable="false" @change="handleStatusChange"></up-tabs>
40 </view> 7 </view>
41 - </view>  
42 - <!-- 第二行:任务次数 + 去作业按钮 -->  
43 - <view class="row row-2">  
44 - <view class="info-text">任务次数:第{{ item.taskTimes || 1 }}次</view>  
45 - <view class="mini-btn" @click.stop="handleGoWork(item)">去作业</view>  
46 - </view>  
47 - <!-- 第三行:有效期 + 结束任务按钮 -->  
48 - <view class="row row-3">  
49 - <view class="info-text u-line-1">有效期:{{ item.beginTime || '-' }}—{{ item.endTime || '-' }}</view>  
50 - <view class="mini-btn" @click.stop="handleEndTask(item)">结束任务</view>  
51 - </view>  
52 - </view>  
53 - </view>  
54 - </z-paging>  
55 -  
56 - <!-- ========== 置为失效弹窗(参考回退弹窗结构) ========== -->  
57 - <up-modal  
58 - :show="invalidModalShow"  
59 - title="失效原因"  
60 - :closeOnClickOverlay="false"  
61 - :showConfirmButton="true"  
62 - :showCancelButton="true"  
63 - @cancel="handleInvalidModalCancel"  
64 - @confirm="confirmSetInvalid"  
65 - >  
66 - <view class="invalid-modal-content">  
67 - <up-textarea  
68 - v-model.trim="invalidReason"  
69 - placeholder="请输入失效原因(必填)"  
70 - rows="6"  
71 - :count="200"  
72 - maxlength="200"  
73 - class="invalid-textarea"  
74 - />  
75 - <view class="upload-wrap mt-20">  
76 - <view class="upload-title">上传图片(选填)</view>  
77 - <up-upload  
78 - :file-list="invalidImgs.rawImgList.value || []"  
79 - @after-read="invalidImgs.uploadImgs"  
80 - @delete="invalidImgs.deleteImg"  
81 - multiple  
82 - width="70"  
83 - height="70"  
84 - :max-count="invalidImgs.uploadConfig.maxCount"  
85 - :upload-text="invalidImgs.uploadConfig.uploadText"  
86 - :size-type="invalidImgs.uploadConfig.sizeType"  
87 - />  
88 - </view>  
89 - </view>  
90 - </up-modal>  
91 - </view> 8 + </up-sticky>
  9 + <z-paging ref="pagingRef" v-model="currentList" @query="fetchData" :auto-show-system-loading="true">
  10 + <template #empty>
  11 + <empty-view />
  12 + </template>
  13 + <view class="card-list">
  14 + <view class="task-card" v-for="(item, index) in currentList" :key="`${item.planNo}_${index}`">
  15 + <!-- 第一行:任务名称 + 置为失效 -->
  16 + <view class="row row-1">
  17 + <view class="plan-name u-line-1">{{ item.planName || '无计划名称' }}</view>
  18 + <!-- 权限:包含team_leader_yl / team_leader_wy / team_leader_hjws 并且当前是待完成tab,才显示置为失效 -->
  19 + <view v-if="hasInvalidPerm && activeStatus === '1'" class="btn-set-invalid"
  20 + @click.stop="handleSetInvalid(item)">
  21 + 置为失效
  22 + </view>
  23 + </view>
  24 + <!-- 第二行:任务次数 + 去作业按钮 -->
  25 + <view class="row row-2">
  26 + <view class="info-text">任务次数:第{{ item.plannum || 1 }}次</view>
  27 + <!-- 仅待完成才显示去作业 -->
  28 + <view v-if="activeStatus === '1'" class="mini-btn" @click.stop="handleGoWork(item)">去作业</view>
  29 + </view>
  30 + <!-- 第三行:有效期 + 结束任务按钮 -->
  31 + <view class="row row-3">
  32 + <!-- timeFormat(start, 'yyyy-mm-dd'), -->
  33 + <view class="info-text u-line-1">有效期:{{ timeFormat(item.beginTime, 'yyyy-mm-dd') }}—{{ timeFormat(item.endTime, 'yyyy-mm-dd') }}
  34 + </view>
  35 + <!-- 仅待完成才显示结束任务 -->
  36 + <view v-if="activeStatus === '1'" class="mini-btn" @click.stop="handleEndTask(item)">结束任务</view>
  37 + </view>
  38 + </view>
  39 + </view>
  40 + </z-paging>
  41 + <!-- ========== 置为失效弹窗(参考回退弹窗结构) ========== -->
  42 + <up-modal :show="invalidModalShow" title="失效原因" :closeOnClickOverlay="false" :showConfirmButton="true"
  43 + :showCancelButton="true" @cancel="handleInvalidModalCancel" @confirm="confirmSetInvalid">
  44 + <view class="invalid-modal-content">
  45 + <up-textarea v-model.trim="invalidReason" placeholder="请输入失效原因(必填)" rows="6" :count="200"
  46 + maxlength="200" class="invalid-textarea" />
  47 + <view class="upload-wrap mt-20">
  48 + <view class="upload-title">上传图片(选填)</view>
  49 + <up-upload :file-list="invalidImgs.rawImgList.value || []" @after-read="invalidImgs.uploadImgs"
  50 + @delete="invalidImgs.deleteImg" multiple width="70" height="70"
  51 + :max-count="invalidImgs.uploadConfig.maxCount"
  52 + :upload-text="invalidImgs.uploadConfig.uploadText"
  53 + :size-type="invalidImgs.uploadConfig.sizeType" />
  54 + </view>
  55 + </view>
  56 + </up-modal>
  57 + </view>
92 </template> 58 </template>
93 -  
94 <script setup lang="ts"> 59 <script setup lang="ts">
95 import { ref, computed } from 'vue' 60 import { ref, computed } from 'vue'
96 -import { onShow } from '@dcloudio/uni-app' 61 +import { onShow, onUnload,onLoad } from '@dcloudio/uni-app'
97 import { useUserStore } from '@/pinia/user'; 62 import { useUserStore } from '@/pinia/user';
98 -// 引入图片上传hook(和你回退弹窗同一个工具) 63 +import { maintainPlanNum, finishPlan, invalidatePlan } from "@/api/maintain-manage/maintain-manage";
  64 +import { timeFormat } from '@/uni_modules/uview-plus';
99 import { useUploadImgs } from '@/common/utils/useUploadImgs' 65 import { useUploadImgs } from '@/common/utils/useUploadImgs'
100 -  
101 const userStore = useUserStore(); 66 const userStore = useUserStore();
102 const userRoles = computed<string[]>(() => userStore.userInfo?.roles || []); 67 const userRoles = computed<string[]>(() => userStore.userInfo?.roles || []);
103 -const isMaintainLeader = computed(() => userRoles.value.includes('yl_worker')) 68 +
  69 +// 判断是否拥有置为失效权限:任意一个组长角色就可以
  70 +const hasInvalidPerm = computed(() => {
  71 + const roles = userRoles.value
  72 + const leaderRoles = ['team_leader_yl', 'team_leader_wy', 'team_leader_hjws']
  73 + return leaderRoles.some(key => roles.includes(key))
  74 +})
104 75
105 const statusTabs = ref([ 76 const statusTabs = ref([
106 - { name: '待完成', id: '1' },  
107 - { name: '已失效', id: '3' },  
108 - { name: '已完成', id: '2' } 77 + { name: '待完成', id: '1' },
  78 + { name: '已失效', id: '3' },
  79 + { name: '已完成', id: '2' }
109 ]) 80 ])
110 const activeStatus = ref('1') 81 const activeStatus = ref('1')
111 const pagingRef = ref(null) 82 const pagingRef = ref(null)
112 const currentList = ref([]) 83 const currentList = ref([])
113 - 84 +// 保存上一页传过来的原始数组
  85 +let originRawList: any[] = []
  86 +// 合并之后全部接口返回的总数据
  87 +const allTotalData = ref<any[]>([])
114 // ===================== 失效弹窗状态 ===================== 88 // ===================== 失效弹窗状态 =====================
115 const invalidModalShow = ref(false) 89 const invalidModalShow = ref(false)
116 const invalidReason = ref('') 90 const invalidReason = ref('')
117 const currentInvalidItem = ref(null) 91 const currentInvalidItem = ref(null)
118 // 图片上传实例,和回退保持一致,最多3张 92 // 图片上传实例,和回退保持一致,最多3张
119 const invalidImgs = useUploadImgs({ 93 const invalidImgs = useUploadImgs({
120 - maxCount: 3,  
121 - uploadText: '选择失效图片',  
122 - sizeType: ['compressed'],  
123 - formRef: null,  
124 - fieldName: 'invalidImgs' 94 + maxCount: 3,
  95 + uploadText: '选择失效图片',
  96 + sizeType: ['compressed'],
  97 + formRef: null,
  98 + fieldName: 'invalidImgs'
125 }) 99 })
126 -  
127 -const mockAllData = [  
128 - { planNo: 'MH20260616001', planName: '德胜门东大街大街月季补植', taskTimes: 1, beginTime: '2026/06/16', endTime: '2026/12/31', finishState: '1' },  
129 - { planNo: 'MH20260616002', planName: '西长安街行道树修剪养护作业任务', taskTimes: 2, beginTime: '2026/05/01', endTime: '2026/11/30', finishState: '1' },  
130 - { planNo: 'MH20260616003', planName: '北三环绿地浇水、防寒养护专项任务超长名称测试文字溢出', taskTimes: 1, beginTime: '2026/04/10', endTime: '2026/12/20', finishState: '1' },  
131 - { planNo: 'MH20260616004', planName: '奥森园区绿地保洁日常养护', taskTimes: 3, beginTime: '2026/03/01', endTime: '2026/10/15', finishState: '3' },  
132 - { planNo: 'MH20260616005', planName: '护城河沿岸病虫害防治专项作业', taskTimes: 2, beginTime: '2026/02/15', endTime: '2026/09/30', finishState: '2' },  
133 - { planNo: 'MH20260616006', planName: '朝阳公园施肥、除草春季养护任务', taskTimes: 1, beginTime: '2026/04/05', endTime: '2026/11/10', finishState: '1' },  
134 - { planNo: 'MH20260616007', planName: '中关村大街灌木修剪与枯枝清理', taskTimes: 4, beginTime: '2026/01/20', endTime: '2026/12/25', finishState: '1' },  
135 - { planNo: 'MH20260616008', planName: '永定河滨河绿地冬季防寒浇水作业', taskTimes: 1, beginTime: '2026/07/01', endTime: '2026/12/31', finishState: '3' },  
136 - { planNo: 'MH20260616009', planName: '颐和园外围绿带病虫害防治', taskTimes: 2, beginTime: '2026/05/20', endTime: '2026/10/20', finishState: '2' },  
137 - { planNo: 'MH20260616010', planName: '上地街道行道树补植更换任务', taskTimes: 1, beginTime: '2026/06/01', endTime: '2026/12/20', finishState: '1' }  
138 -]  
139 - 100 +// 加载数据,根据当前activeStatus动态调用接口
  101 +const loadData = async () => {
  102 + // originRawList = uni.getStorageSync('temp_maintain_rawList')
  103 + console.log('123123')
  104 + if (!originRawList || originRawList.length === 0) {
  105 + return
  106 + }
  107 + uni.showLoading({ title: '加载数据...', mask: true })
  108 + try {
  109 + const tempResult: any[] = []
  110 + // 获取需要请求的finishState数组
  111 + let stateArr: string[] = []
  112 + if (activeStatus.value === '1') {
  113 + // 待完成,只查1
  114 + stateArr = ['1']
  115 + } else if (activeStatus.value === '3') {
  116 + // 已失效,同时查3、4
  117 + stateArr = ['3', '4']
  118 + } else if (activeStatus.value === '2') {
  119 + // 已完成,只查2
  120 + stateArr = ['2']
  121 + }
  122 + // 遍历state数组,每个状态,循环rawList里面所有batchNo请求
  123 + for (const finishState of stateArr) {
  124 + for (const item of originRawList) {
  125 + const res = await maintainPlanNum({
  126 + pageNo: 1,
  127 + pageSize: 10,
  128 + planNo: item.planNo || '',
  129 + planAttr: '',
  130 + finishState: finishState,
  131 + batchNo: item.batchNo
  132 + })
  133 + if (Array.isArray(res.list)) {
  134 + tempResult.push(...res.list)
  135 + }
  136 + }
  137 + }
  138 + allTotalData.value = tempResult
  139 + } catch (err) {
  140 + console.error('批量请求接口异常', err)
  141 + uni.showToast({ title: '数据加载失败', icon: 'none' })
  142 + } finally {
  143 + uni.hideLoading()
  144 + pagingRef.value?.reload()
  145 + }
  146 +}
  147 +const planTypeId = ref('')
  148 +onLoad((options) => {
  149 + planTypeId.value = options.planTypeId
  150 +
  151 +});
  152 +// onShow 只保留一处,读取storage,只第一次读取storage
  153 +onShow(async () => {
  154 + originRawList = uni.getStorageSync('temp_maintain_rawList') || []
  155 + // ✅ 读取完立刻删除,防止脏数据
  156 + // uni.removeStorageSync('temp_maintain_rawList')
  157 + await loadData()
  158 +})
  159 +// z-paging分页查询,直接使用allTotalData
140 const fetchData = async (pageNo, pageSize) => { 160 const fetchData = async (pageNo, pageSize) => {
141 - await new Promise(resolve => setTimeout(resolve, 400))  
142 - const filterList = mockAllData.filter(item => item.finishState === activeStatus.value)  
143 - const start = (pageNo - 1) * pageSize  
144 - const pageData = filterList.slice(start, start + pageSize)  
145 - pagingRef.value?.complete(pageData, filterList.length) 161 + await new Promise(resolve => setTimeout(resolve, 300))
  162 + const filterList = allTotalData.value
  163 + const start = (pageNo - 1) * pageSize
  164 + const pageData = filterList.slice(start, start + pageSize)
  165 + pagingRef.value?.complete(pageData, filterList.length)
146 } 166 }
147 -  
148 -const handleStatusChange = (item) => {  
149 - activeStatus.value = item.id  
150 - pagingRef.value?.reload() 167 +const handleStatusChange = async (tabItem) => {
  168 + activeStatus.value = tabItem.id
  169 + // 切换tab,重新拉接口
  170 + await loadData()
151 } 171 }
152 -  
153 // 打开失效弹窗 172 // 打开失效弹窗
154 const handleSetInvalid = (item) => { 173 const handleSetInvalid = (item) => {
155 - currentInvalidItem.value = item  
156 - invalidReason.value = ''  
157 - invalidImgs.clearImgs()  
158 - invalidModalShow.value = true 174 + currentInvalidItem.value = item
  175 + invalidReason.value = ''
  176 + invalidImgs.clearImgs()
  177 + invalidModalShow.value = true
159 } 178 }
160 -  
161 // 关闭弹窗,重置数据 179 // 关闭弹窗,重置数据
162 const handleInvalidModalCancel = () => { 180 const handleInvalidModalCancel = () => {
163 - invalidModalShow.value = false  
164 - invalidReason.value = ''  
165 - invalidImgs.clearImgs() 181 + invalidModalShow.value = false
  182 + invalidReason.value = ''
  183 + invalidImgs.clearImgs()
166 } 184 }
167 185
168 // 确认提交置为失效 186 // 确认提交置为失效
@@ -173,23 +191,27 @@ const confirmSetInvalid = async () =&gt; { @@ -173,23 +191,27 @@ const confirmSetInvalid = async () =&gt; {
173 return 191 return
174 } 192 }
175 const item = currentInvalidItem.value 193 const item = currentInvalidItem.value
176 - if (!item) return 194 + if (!item || !item.id) return
177 195
178 uni.showLoading({ title: '提交中...', mask: true }) 196 uni.showLoading({ title: '提交中...', mask: true })
179 try { 197 try {
180 - // 这里后续替换真实接口,获取上传图片url: invalidImgs.getSuccessImgUrls()  
181 - const imgList = invalidImgs.getSuccessImgUrls()  
182 - console.log('置为失效提交参数:', {  
183 - planNo: item.planNo,  
184 - reason,  
185 - imgList  
186 - }) 198 + // 获取上传成功图片url数组,拼接逗号
  199 + const imgUrlArr = invalidImgs.getSuccessImgUrls()
  200 + const invalidFiles = imgUrlArr.join(',')
187 201
188 - // ============ 此处替换后端接口调用 ============  
189 - // await api.setTaskInvalid({ ... }) 202 + const params = {
  203 + id: item.id,
  204 + invalidReason: reason,
  205 + invalidFiles: invalidFiles
  206 + }
  207 + console.log('置为失效提交参数:', params)
  208 + // 调用接口 invalidatePlan
  209 + await invalidatePlan(params)
190 210
191 uni.showToast({ title: '操作成功', icon: 'success' }) 211 uni.showToast({ title: '操作成功', icon: 'success' })
192 handleInvalidModalCancel() 212 handleInvalidModalCancel()
  213 + // 重新加载数据刷新列表
  214 + await loadData()
193 pagingRef.value?.reload() 215 pagingRef.value?.reload()
194 } catch (err) { 216 } catch (err) {
195 console.error(err) 217 console.error(err)
@@ -200,84 +222,117 @@ const confirmSetInvalid = async () =&gt; { @@ -200,84 +222,117 @@ const confirmSetInvalid = async () =&gt; {
200 } 222 }
201 223
202 const handleGoWork = (item) => { 224 const handleGoWork = (item) => {
203 -  
204 - uni.navigateTo({  
205 - url: `/pages-sub/daily/maintain-manage/now-list-page?roadId=${item.roadId}&finish_state=${item.finishState || ''} &businessType=${item.busiLine}`  
206 - }) 225 + uni.navigateTo({
  226 + // 重点:把planNo传过去
  227 + url: `/pages-sub/daily/maintain-manage/now-list-page?planNo=${item.planNo}&planTypeId=${planTypeId.value}&businessType=${item.busiLine}`
  228 + })
  229 +}
  230 +const handleEndTask = async (item) => {
  231 + console.log(item)
  232 + uni.showLoading({ title: '提交中...', mask: true })
  233 + try {
  234 + // 接口 finishPlan,传 planNo
  235 + await finishPlan({
  236 + planNo: item.planNo
  237 + })
  238 + uni.showToast({ title: '结束任务成功', icon: 'success' })
  239 + // 操作成功,重新加载数据,刷新列表
  240 + await loadData()
  241 + pagingRef.value?.reload()
  242 + } catch (err) {
  243 + console.error('结束任务失败', err)
  244 + uni.showToast({ title: '结束任务失败,请重试', icon: 'none' })
  245 + } finally {
  246 + uni.hideLoading()
  247 + }
207 } 248 }
208 -const handleEndTask = (item) => console.log('结束任务', item)  
209 249
210 -onShow(() => pagingRef.value?.reload()) 250 +// 页面卸载清空,防止内存残留
  251 +onUnload(() => {
  252 + allTotalData.value = []
  253 + originRawList = []
  254 +})
211 </script> 255 </script>
212 -  
213 <style scoped lang="scss"> 256 <style scoped lang="scss">
214 .page-container { 257 .page-container {
215 - height: 100vh;  
216 - background-color: #f5f5f5; 258 + height: 100vh;
  259 + background-color: #f5f5f5;
217 } 260 }
  261 +
218 .sticky-wrap { 262 .sticky-wrap {
219 - background: #fff;  
220 - border-bottom: 1rpx solid #eee; 263 + background: #fff;
  264 + border-bottom: 1rpx solid #eee;
221 } 265 }
  266 +
222 .card-list { 267 .card-list {
223 - padding:100rpx 20rpx 20rpx;  
224 - display: flex;  
225 - flex-direction: column;  
226 - gap: 20rpx; 268 + padding: 100rpx 20rpx 20rpx;
  269 + display: flex;
  270 + flex-direction: column;
  271 + gap: 20rpx;
227 } 272 }
  273 +
228 .task-card { 274 .task-card {
229 - background: #fff;  
230 - border-radius: 12rpx;  
231 - padding: 24rpx;  
232 - display: flex;  
233 - flex-direction: column;  
234 - gap: 20rpx; 275 + background: #fff;
  276 + border-radius: 12rpx;
  277 + padding: 24rpx;
  278 + display: flex;
  279 + flex-direction: column;
  280 + gap: 20rpx;
235 } 281 }
  282 +
236 .row { 283 .row {
237 - display: flex;  
238 - align-items: center;  
239 - justify-content: space-between;  
240 - gap: 16rpx; 284 + display: flex;
  285 + align-items: center;
  286 + justify-content: space-between;
  287 + gap: 16rpx;
241 } 288 }
  289 +
242 .plan-name { 290 .plan-name {
243 - font-size: 30rpx;  
244 - color: #333;  
245 - font-weight: 500;  
246 - flex: 1; 291 + font-size: 30rpx;
  292 + color: #333;
  293 + font-weight: 500;
  294 + flex: 1;
247 } 295 }
  296 +
248 .btn-set-invalid { 297 .btn-set-invalid {
249 - font-size: 26rpx;  
250 - color: #1677ff;  
251 - flex-shrink: 0;  
252 - padding-left: 16rpx; 298 + font-size: 26rpx;
  299 + color: #1677ff;
  300 + flex-shrink: 0;
  301 + padding-left: 16rpx;
253 } 302 }
  303 +
254 .info-text { 304 .info-text {
255 - font-size: 26rpx;  
256 - color: #666;  
257 - flex: 1; 305 + font-size: 26rpx;
  306 + color: #666;
  307 + flex: 1;
258 } 308 }
  309 +
259 .mini-btn { 310 .mini-btn {
260 - font-size: 24rpx;  
261 - color: #333;  
262 - background: #f5f5f5;  
263 - border: 1rpx solid #ddd;  
264 - border-radius: 6rpx;  
265 - padding: 8rpx 20rpx;  
266 - flex-shrink: 0; 311 + font-size: 24rpx;
  312 + color: #333;
  313 + background: #f5f5f5;
  314 + border: 1rpx solid #ddd;
  315 + border-radius: 6rpx;
  316 + padding: 8rpx 20rpx;
  317 + flex-shrink: 0;
267 } 318 }
268 319
269 // 弹窗样式,和回退弹窗保持一致 320 // 弹窗样式,和回退弹窗保持一致
270 .invalid-modal-content { 321 .invalid-modal-content {
271 - width: 100%;  
272 - box-sizing: border-box;  
273 - padding: 10rpx 0; 322 + width: 100%;
  323 + box-sizing: border-box;
  324 + padding: 10rpx 0;
274 } 325 }
  326 +
275 .upload-wrap { 327 .upload-wrap {
276 - .upload-title {  
277 - font-size: 28rpx;  
278 - color: #333;  
279 - margin-bottom: 10rpx;  
280 - } 328 + .upload-title {
  329 + font-size: 28rpx;
  330 + color: #333;
  331 + margin-bottom: 10rpx;
  332 + }
  333 +}
  334 +
  335 +.mt-20 {
  336 + margin-top: 20rpx;
281 } 337 }
282 -.mt-20 { margin-top: 20rpx; }  
283 </style> 338 </style>
pages-sub/daily/maintain-manage/type-list.vue
1 <template> 1 <template>
2 - <view class="tag-wrap">  
3 - <!-- 动态渲染标签列表 -->  
4 - <view class="tag-item" v-for="(item, idx) in tagList" :key="idx" hover-class="tag-item--hover" @click="toNextPage">  
5 - <view class="tag-text">  
6 - <!-- 文字区域:超长省略号,数字括号单独放后面,永远可见 -->  
7 - <text class="tag-name">{{ item.label }}</text>  
8 - <text class="tag-count">({{ item.count }})</text>  
9 - </view>  
10 - 2 + <view class="tag-wrap">
  3 + <!-- 动态渲染标签列表 -->
  4 + <template v-if="tagList.length > 0">
  5 + <view class="tag-item" v-for="(item, idx) in tagList" :key="idx" hover-class="tag-item--hover"
  6 + @click="toNextPage(item)">
  7 + <view class="tag-text">
  8 + <!-- 文字区域:超长省略号,数字括号单独放后面,永远可见 -->
  9 + <text class="tag-name">{{ item.label }}</text>
  10 + <text class="tag-count">({{ item.count }})</text>
  11 + </view>
  12 + </view>
  13 + </template>
  14 + <template v-else>
  15 + <view style="padding-top: 30px;text-align: center;width: 100%;">暂无养护类型,请去平台配置</view>
  16 + </template>
11 </view> 17 </view>
12 - </view>  
13 </template> 18 </template>
14 -  
15 <script setup lang="ts"> 19 <script setup lang="ts">
16 import { ref } from 'vue' 20 import { ref } from 'vue'
17 -  
18 -// 数据源,完全动态,你可以接口返回覆盖  
19 -const tagList = ref([  
20 - { label: '补植', count: 5 },  
21 - { label: '修剪', count: 2 },  
22 - { label: '浇水、防寒', count: 3 },  
23 - { label: '绿地保洁', count: 3 },  
24 - { label: '病虫害防治', count: 3 },  
25 - { label: '施肥、除草', count: 4 },  
26 - // 测试超长文本  
27 - { label: '非常长的养护任务名称测试文字溢出效果', count: 9 },  
28 -])  
29 -  
30 -const toNextPage = (item)=> { 21 +import { maintainPlan, maintainPlanNum } from "@/api/maintain-manage/maintain-manage";
  22 +import { getDictLabel } from '@/common/utils/dict';
  23 +import { useUserStore } from '@/pinia/user';
  24 +// 新增维护仓库
  25 +// import { useMaintainStore } from '@/pinia/maintain';
  26 +import { onLoad, onShow } from '@dcloudio/uni-app';
  27 +const userStore = useUserStore();
  28 +// const maintainStore = useMaintainStore();
  29 +// 标签列表,合并之后的数据结构
  30 +// rawList:保存该分类下全部原始数据,用于点击跳转
  31 +const tagList = ref<Array<{
  32 + label: string,
  33 + count: number,
  34 + rawList: any[],
  35 + planTypeId: string | number
  36 +}>>([])
  37 +const roadName = ref<string | null>(null)
  38 +const levelId = ref<string | null>(null)
  39 +onLoad((options) => {
  40 + roadName.value = decodeURIComponent(options.roadName)
  41 + levelId.value = decodeURIComponent(options.levelId);
  42 +});
  43 +onShow(() => {
  44 + if (!roadName.value) {
  45 + uni.showToast({
  46 + title: '道路不存在',
  47 + icon: 'none'
  48 + });
  49 + return;
  50 + }
  51 + fetchData();
  52 +});
  53 +// 第一步:获取plan列表
  54 +const fetchData = async () => {
  55 + try {
  56 + const res = await maintainPlan({
  57 + roadName: roadName.value,
  58 + levelId: levelId.value
  59 + })
  60 + console.log('maintainPlan返回', res)
  61 + if (!Array.isArray(res.list) || res.list.length === 0) {
  62 + tagList.value = []
  63 + uni.showToast({
  64 + title: '暂无养护类型',
  65 + icon: 'none'
  66 + });
  67 + return
  68 + }
  69 + await requestBatchList(res.list)
  70 + } catch (error) {
  71 + console.error('请求失败:', error)
  72 + uni.showToast({ title: '网络异常,请重试', icon: 'none' })
  73 + }
  74 +}
  75 +// 串行遍历所有batchNo,逐个调用第二个接口
  76 +const requestBatchList = async (list: any[]) => {
  77 + const resultTags: any[] = []
  78 + for (const item of list) {
  79 + try {
  80 + const numRes = await maintainPlanNum({
  81 + pageNo: 1,
  82 + pageSize: 10,
  83 + planNo: '',
  84 + planAttr: item.planAttr ?? '',
  85 + finishState: 1,
  86 + batchNo: item.batchNo
  87 + })
  88 + const total = numRes?.total || 0
  89 + // 字典转换,把planTypeId转为养护类型名称
  90 + const typeLabel = getDictLabel('maintain_type', item.planTypeId)
  91 + resultTags.push({
  92 + label: typeLabel,
  93 + planTypeId: item.planTypeId,
  94 + count: total,
  95 + // 保留原始整条数据
  96 + originItem: item
  97 + })
  98 + } catch (err) {
  99 + console.error('batch请求异常', item.batchNo, err)
  100 + const typeLabel = getDictLabel('maintain_type', item.planTypeId)
  101 + resultTags.push({
  102 + label: typeLabel,
  103 + planTypeId: item.planTypeId,
  104 + count: 0,
  105 + originItem: item
  106 + })
  107 + }
  108 + }
  109 + console.log(resultTags)
  110 + // ========== 分组合并相同养护类型,count累加,保存原始数组 ==========
  111 + const groupMap = new Map<string, {
  112 + count: number,
  113 + rawList: any[],
  114 + planTypeId: string | number
  115 + }>()
  116 + for (const item of resultTags) {
  117 + console.log(item);
  118 + const name = item.label
  119 + if (!name) continue // 过滤字典为空的脏数据
  120 + if (groupMap.has(name)) {
  121 + const groupData = groupMap.get(name)!
  122 + groupData.count += item.count
  123 + groupData.rawList.push(item.originItem)
  124 + } else {
  125 + groupMap.set(name, {
  126 + count: item.count,
  127 + rawList: [item.originItem],
  128 + planTypeId: item.planTypeId
  129 + })
  130 + }
  131 + }
  132 + // map转为数组给页面渲染
  133 + const groupList: any[] = []
  134 + groupMap.forEach((group, label) => {
  135 + groupList.push({
  136 + label,
  137 + count: group.count,
  138 + rawList: group.rawList,
  139 + planTypeId: group.planTypeId
  140 + })
  141 + })
  142 + tagList.value = groupList
  143 + console.log(tagList.value)
  144 +}
  145 +// 标签点击跳转,rawList存入storage,并且把planTypeId拼到url传给下一页
  146 +const toNextPage = (item: any) => {
  147 + // maintainStore.setRawList(item.rawList)
  148 + uni.setStorageSync('temp_maintain_rawList', item.rawList)
31 uni.navigateTo({ 149 uni.navigateTo({
32 - url: `/pages-sub/daily/maintain-manage/type-list-page?roadId=${item.roadId}&finish_state=${item.finishState || ''} &businessType=${item.busiLine}`  
33 - }) 150 + url: `/pages-sub/daily/maintain-manage/type-list-page?planTypeId=${item.planTypeId}`
  151 + })
34 } 152 }
35 </script> 153 </script>
36 -  
37 <style scoped lang="scss"> 154 <style scoped lang="scss">
38 .tag-wrap { 155 .tag-wrap {
39 - display: flex;  
40 - flex-wrap: wrap;  
41 - gap: 24rpx;  
42 - padding: 30rpx; 156 + display: flex;
  157 + flex-wrap: wrap;
  158 + gap: 24rpx;
  159 + padding: 30rpx;
43 } 160 }
44 161
45 .tag-item { 162 .tag-item {
46 - display: inline-flex;  
47 - align-items: center;  
48 - background: #e8f3ff;  
49 - border: 1rpx solid #94c5ff;  
50 - border-radius: 8rpx;  
51 - padding: 16rpx 24rpx;  
52 - max-width: 320rpx; // 限制标签最大宽度,超出自动省略  
53 - transition: all 0.2s;  
54 - &--hover {  
55 - background: #d8eaff;  
56 - } 163 + display: inline-flex;
  164 + align-items: center;
  165 + background: #e8f3ff;
  166 + border: 1rpx solid #94c5ff;
  167 + border-radius: 8rpx;
  168 + padding: 16rpx 24rpx;
  169 + max-width: 320rpx; // 限制标签最大宽度,超出自动省略
  170 + transition: all 0.2s;
  171 +
  172 + &--hover {
  173 + background: #d8eaff;
  174 + }
57 } 175 }
58 176
59 .tag-text { 177 .tag-text {
60 - display: flex;  
61 - align-items: center;  
62 - flex: 1;  
63 - overflow: hidden; 178 + display: flex;
  179 + align-items: center;
  180 + flex: 1;
  181 + overflow: hidden;
64 } 182 }
65 183
66 .tag-name { 184 .tag-name {
67 - // 核心:文字超长省略,只对名称生效  
68 - white-space: nowrap;  
69 - overflow: hidden;  
70 - text-overflow: ellipsis;  
71 - color: #1677ff;  
72 - font-size: 28rpx; 185 + // 核心:文字超长省略,只对名称生效
  186 + white-space: nowrap;
  187 + overflow: hidden;
  188 + text-overflow: ellipsis;
  189 + color: #1677ff;
  190 + font-size: 28rpx;
73 } 191 }
74 192
75 .tag-count { 193 .tag-count {
76 - color: #1677ff;  
77 - font-size: 28rpx;  
78 - flex-shrink: 0; // 重点!禁止压缩,保证(数字)永远显示  
79 - margin-left: 4rpx; 194 + color: #1677ff;
  195 + font-size: 28rpx;
  196 + flex-shrink: 0; // 重点!禁止压缩,保证(数字)永远显示
  197 + margin-left: 4rpx;
80 } 198 }
81 199
82 .tag-icon { 200 .tag-icon {
83 - flex-shrink: 0;  
84 - margin-left: 12rpx; 201 + flex-shrink: 0;
  202 + margin-left: 12rpx;
85 } 203 }
86 </style> 204 </style>
87 -  
pinia/maintain.js 0 → 100644
  1 +import { defineStore } from 'pinia'
  2 +export const useMaintainStore = defineStore('maintain', {
  3 + state() {
  4 + return {
  5 + planRawList: []
  6 + }
  7 + },
  8 + actions: {
  9 + setRawList(list) {
  10 + this.planRawList = list
  11 + },
  12 + clearRawList() {
  13 + this.planRawList = []
  14 + }
  15 + }
  16 +})