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 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 47 * @returns {Promise}
39 48 */
... ... @@ -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 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" 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 10 <!-- type 1 数值:u-number-box,最多2位小数,min=0,max取item.length -->
10 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 18 <text class="unit-text" v-if="item.unit">{{ item.unit }}</text>
23 19 </view>
24 20 <!-- type 2 文本 -->
25 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 23 <!-- 分层选项(3枚举/6多选,且选项带 path):统一只读入口,点开分组弹层选择。
28 24 必须放在下面的 3/6 分支之前——v-else-if 短路,放后面就永远进不来 -->
29 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 27 <template #right>
32 28 <up-icon name="arrow-right" size="16"></up-icon>
33 29 </template>
34 30 </up-input>
35 31 <!-- type3 枚举:多选 multiple=1 使用复选框组(name 必须是 value,提交按逗号拼接 value,与值格式契约一致) -->
36 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 34 <u-checkbox v-for="opt in item.selectOptions" :key="opt.value" :label="opt.label" :name="opt.value">
39 35  
40 36 </u-checkbox>
41 37 </u-checkbox-group>
42 38 <!-- type3 枚举:单选 用up-action-sheet 底部选择 -->
43 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 41 <template #right>
46 42 <up-icon name="arrow-right" size="16"></up-icon>
47 43 </template>
... ... @@ -59,7 +55,7 @@
59 55 <view v-else-if="item.fieldType === 6" class="multi-wrap">
60 56 <u-checkbox-group v-model="inspectForm[item.fieldCode]" @change="onMultiChange(item)">
61 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 59 </u-checkbox>
64 60 </u-checkbox-group>
65 61 <!-- 有限上限时提示已选数。上限靠「禁用未选中项」实现:u-checkbox 的勾选态在组件内部,回写 v-model 撤不掉 -->
... ... @@ -73,22 +69,12 @@
73 69 </view>
74 70 </view>
75 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 80 <!-- type8 附件:微信只能从聊天记录选文件(无通用文件选择器),提交 JSON 数组(URL) -->
... ... @@ -98,21 +84,21 @@
98 84 <text class="file-del" @click.stop="deleteFieldFile(item, { index: idx })">删除</text>
99 85 </view>
100 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 88 </view>
103 89 <!-- type9 关联记录:底部选择,提交 JSON 数组 [{id,name}] -->
104 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 92 <template #right>
107 93 <up-icon name="arrow-right" size="16"></up-icon>
108 94 </template>
109 95 </up-input>
110 96 <!-- type10 链接:URL 输入,提交裸 URL -->
111 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 99 <!-- type11 地理位置:uni.chooseLocation 选点,提交 JSON 对象 {lng,lat,address} -->
114 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 102 <template #right>
117 103 <up-icon name="arrow-right" size="16"></up-icon>
118 104 </template>
... ... @@ -138,38 +124,40 @@
138 124 </up-form-item> -->
139 125 </up-form>
140 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 139 <!-- uview-plus 日期时间选择器(全局复用) -->
151 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 142 <!-- ActionSheet 底部选择弹窗 -->
154 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 145 <!-- 分层选项选择器(选项带 path 的动态字段) -->
157 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 150 </view>
164 151 </template>
165 152  
166 153 <script lang="ts">
167 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 156 import { getByType, getOptionsByCode, getMaterialByCode, workRecordSubmit } from "@/api/common"
170 157 export default {
171 158 data() {
172 159 return {
  160 + submitting: false,
173 161 imagesList: [],
174 162 initProgress: 0,
175 163 inspectForm: {
... ... @@ -234,7 +222,7 @@ export default {
234 222 // 数量单位不再传进弹层(原型列表行右侧只有步进器,没有单位),页面行内仍显示 item.unit
235 223 unit: '',
236 224 // 新增:由父页面点击时赋值
237   - containerWidth: 0
  225 + containerWidth: 0
238 226 }
239 227 }
240 228 },
... ... @@ -328,15 +316,15 @@ export default {
328 316 this.groupPicker.needQuantity = item.fieldType === 6 && this.needQty(item)
329 317 this.groupPicker.value = multiple ? (this.inspectForm[code] || []).slice() : (this.inspectForm[code] || '')
330 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 330 * 分层弹层确定回调。写入位置与原有两条路径完全一致(inspectForm/labelMap/qtyMap),
... ... @@ -363,8 +351,8 @@ export default {
363 351 // 物料带 currentNum 时在展示名后附库存余量(只影响展示名,提交的 materialName 仍取干净的 opt.label)
364 352 this.actionSheet.options = (item.selectOptions || []).map(opt => {
365 353 const stock = opt.currentNum === null || opt.currentNum === undefined || opt.currentNum === ''
366   - ? NaN
367   - : Number(opt.currentNum)
  354 + ? NaN
  355 + : Number(opt.currentNum)
368 356 return {
369 357 name: isNaN(stock) ? opt.label : `${opt.label}(库存${stock}${opt.unit || ''})`,
370 358 value: opt.value
... ... @@ -487,9 +475,9 @@ export default {
487 475 // 3物料为标量(领用数量),默认 0 = 本次不领用、不扣库存
488 476 if (field.fieldType === 6 && this.needQty(field)) {
489 477 const qty = {}
490   - ;(field.selectOptions || []).forEach(opt => {
491   - qty[opt.value] = ''
492   - })
  478 + ; (field.selectOptions || []).forEach(opt => {
  479 + qty[opt.value] = ''
  480 + })
493 481 this.qtyMap[field.fieldCode] = qty
494 482 }
495 483 if (field.fieldType === 3 && field.optionSource === 3) {
... ... @@ -657,50 +645,50 @@ export default {
657 645 * 7/8/9/11 的值是数组或对象,async-validator 的 required 按默认 string 类型判定,
658 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 693 /** 幂等流水号(后端按 userId+clientSeq 去重,命中则返回已存在的 recordId) */
706 694 genClientSeq() {
... ... @@ -727,26 +715,26 @@ buildRequiredRule(field: any) {
727 715 /**
728 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 740 * 8附件 选择并上传。
... ... @@ -783,65 +771,65 @@ uploadFieldFiles(item: any, event: any) {
783 771 * 把选中的本地文件逐个上传并把 http 地址回填到 fileListMap / inspectForm。
784 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 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 835 * 删除 7图片/8附件 的某一个已选文件
... ... @@ -893,12 +881,12 @@ this.$refs.inspectFormRef.validateField(code)
893 881 fields.push(row)
894 882 } else if (type === 3) {
895 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 890 } else if (type === 6) {
903 891 const selected = (item.selectOptions || []).filter(opt => (value || []).indexOf(opt.value) > -1)
904 892 if (this.needQty(item)) {
... ... @@ -934,6 +922,70 @@ this.$refs.inspectFormRef.validateField(code)
934 922 }
935 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 991 * ① maintain-plan-commit/create:养护计划本身(描述 + 图片 + 人员),返回 commit.id
... ... @@ -942,11 +994,12 @@ this.$refs.inspectFormRef.validateField(code)
942 994 */
943 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 1003 try {
951 1004 await this.$refs.inspectFormRef.validate()
952 1005 const dynamicValues = this.buildDynamicFields()
... ... @@ -954,6 +1007,7 @@ this.$refs.inspectFormRef.validateField(code)
954 1007 this.clientSeq = this.genClientSeq()
955 1008 }
956 1009 uni.showLoading({ title: '提交中...' })
  1010 + this.submitting = true
957 1011 // ① 养护计划本体:仅首次提交时写入
958 1012 if (!this.commitId) {
959 1013 const submitData = {
... ... @@ -1017,11 +1071,13 @@ this.$refs.inspectFormRef.validateField(code)
1017 1071 .page-container {
1018 1072 padding-bottom: 120rpx;
1019 1073 }
  1074 +
1020 1075 .inspect-form-content {
1021 1076 background: #fff;
1022 1077 padding: 20rpx;
1023 1078 margin-bottom: 20rpx;
1024 1079 }
  1080 +
1025 1081 .date-input-wrap {
1026 1082 width: 100%;
1027 1083 }
... ... @@ -1033,35 +1089,42 @@ this.$refs.inspectFormRef.validateField(code)
1033 1089 height: 72rpx;
1034 1090 font-size: 32rpx;
1035 1091 }
  1092 +
1036 1093 :deep(.u-number-box__btn) {
1037 1094 width: 64rpx;
1038 1095 height: 72rpx;
1039 1096 }
1040 1097 }
  1098 +
1041 1099 .number-box-wrap {
1042 1100 display: flex;
1043 1101 align-items: center;
1044 1102 gap: 16rpx;
  1103 +
1045 1104 .unit-text {
1046 1105 font-size: 28rpx;
1047 1106 color: #333;
1048 1107 }
1049 1108 }
  1109 +
1050 1110 // 6多选:复选框组 + needQuantity 时每个已选项一行数量输入
1051 1111 .multi-wrap {
1052 1112 width: 100%;
  1113 +
1053 1114 .multi-hint {
1054 1115 display: block;
1055 1116 margin-top: 8rpx;
1056 1117 font-size: 24rpx;
1057 1118 color: #909399;
1058 1119 }
  1120 +
1059 1121 .qty-list {
1060 1122 margin-top: 16rpx;
1061 1123 padding-left: 20rpx;
1062 1124 border-left: 4rpx solid #f0f0f0;
1063 1125 }
1064 1126 }
  1127 +
1065 1128 // 数量行(6多选的每个已选项 / 3物料的领用数量共用)
1066 1129 .qty-row {
1067 1130 display: flex;
... ... @@ -1069,6 +1132,7 @@ this.$refs.inspectFormRef.validateField(code)
1069 1132 justify-content: space-between;
1070 1133 gap: 16rpx;
1071 1134 padding: 8rpx 0;
  1135 +
1072 1136 .qty-label {
1073 1137 flex: 1;
1074 1138 min-width: 0;
... ... @@ -1076,20 +1140,24 @@ this.$refs.inspectFormRef.validateField(code)
1076 1140 color: #333;
1077 1141 }
1078 1142 }
  1143 +
1079 1144 .qty-row-material {
1080 1145 margin-top: 12rpx;
1081 1146 padding-top: 12rpx;
1082 1147 border-top: 1rpx solid #f0f0f0;
1083 1148 }
  1149 +
1084 1150 // 8附件:自绘文件列表 + 选择按钮(微信无通用文件选择器,走 chooseMessageFile)
1085 1151 .file-wrap {
1086 1152 width: 100%;
  1153 +
1087 1154 .file-row {
1088 1155 display: flex;
1089 1156 align-items: center;
1090 1157 justify-content: space-between;
1091 1158 gap: 16rpx;
1092 1159 padding: 8rpx 0;
  1160 +
1093 1161 .file-name {
1094 1162 flex: 1;
1095 1163 min-width: 0;
... ... @@ -1099,6 +1167,7 @@ this.$refs.inspectFormRef.validateField(code)
1099 1167 white-space: nowrap;
1100 1168 text-overflow: ellipsis;
1101 1169 }
  1170 +
1102 1171 .file-del {
1103 1172 font-size: 26rpx;
1104 1173 color: #ff4d4f;
... ... @@ -1113,5 +1182,38 @@ this.$refs.inspectFormRef.validateField(code)
1113 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 1219 </style>
... ...
pages-sub/daily/maintain-manage/finish-plan-detail.vue
... ... @@ -154,7 +154,7 @@
154 154  
155 155 <script setup lang="ts">
156 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 158 import {onLoad} from '@dcloudio/uni-app';
159 159 import {timeFormat} from '@/uni_modules/uview-plus';
160 160  
... ... @@ -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 203 try {
204 204 loading.value = true;
205 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 211 console.log('接口返回:', res);
212 212  
213 213 // 修复2:正确解析接口数据(res.data.list 而非 res.list)
... ... @@ -527,9 +527,9 @@ const openGeo = (geo) =&gt; {
527 527  
528 528 // 页面加载
529 529 onLoad((options) => {
530   - const {planNo} = options;
531   - if (planNo) {
532   - getOrderDetail(planNo);
  530 + const {id} = options;
  531 + if (id) {
  532 + getOrderDetail(id);
533 533 } else {
534 534 loading.value = false;
535 535 uni.showToast({title: '缺少工单ID', icon: 'none'});
... ...
pages-sub/daily/maintain-manage/index.vue
... ... @@ -185,7 +185,7 @@
185 185 <script setup>
186 186 import { ref, computed } from 'vue'
187 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 189 import { timeFormat } from '@/uni_modules/uview-plus';
190 190  
191 191 // 初始化响应式数据
... ... @@ -274,26 +274,14 @@ const handleSearch = () =&gt; {
274 274 const fetchData = async (pageNo, pageSize) => {
275 275 try {
276 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 280 roadName: searchValue.value.trim(),
281 281 pageNo,
282 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 286 // 告诉z-paging数据加载完成
299 287 // res.list为当前页数据,res.total为总条数(如果接口返回)
... ... @@ -313,8 +301,9 @@ const goToDetail = (item) =&gt; {
313 301 // })
314 302  
315 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 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 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 25 </view>
29   - </view>
30 26 </template>
31   -
32 27 <script setup lang="ts">
33 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 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 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 86 </script>
73   -
74 87 <style scoped lang="scss">
75 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 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 103 .list-wrap {
90   - padding: 0;
  104 + padding: 0;
91 105 }
  106 +
92 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 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 149 </style>
... ...
pages-sub/daily/maintain-manage/type-list-page.vue
1 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 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 58 </template>
93   -
94 59 <script setup lang="ts">
95 60 import { ref, computed } from 'vue'
96   -import { onShow } from '@dcloudio/uni-app'
  61 +import { onShow, onUnload,onLoad } from '@dcloudio/uni-app'
97 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 65 import { useUploadImgs } from '@/common/utils/useUploadImgs'
100   -
101 66 const userStore = useUserStore();
102 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 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 81 const activeStatus = ref('1')
111 82 const pagingRef = ref(null)
112 83 const currentList = ref([])
113   -
  84 +// 保存上一页传过来的原始数组
  85 +let originRawList: any[] = []
  86 +// 合并之后全部接口返回的总数据
  87 +const allTotalData = ref<any[]>([])
114 88 // ===================== 失效弹窗状态 =====================
115 89 const invalidModalShow = ref(false)
116 90 const invalidReason = ref('')
117 91 const currentInvalidItem = ref(null)
118 92 // 图片上传实例,和回退保持一致,最多3张
119 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 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 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 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 191 return
174 192 }
175 193 const item = currentInvalidItem.value
176   - if (!item) return
  194 + if (!item || !item.id) return
177 195  
178 196 uni.showLoading({ title: '提交中...', mask: true })
179 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 211 uni.showToast({ title: '操作成功', icon: 'success' })
192 212 handleInvalidModalCancel()
  213 + // 重新加载数据刷新列表
  214 + await loadData()
193 215 pagingRef.value?.reload()
194 216 } catch (err) {
195 217 console.error(err)
... ... @@ -200,84 +222,117 @@ const confirmSetInvalid = async () =&gt; {
200 222 }
201 223  
202 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 255 </script>
212   -
213 256 <style scoped lang="scss">
214 257 .page-container {
215   - height: 100vh;
216   - background-color: #f5f5f5;
  258 + height: 100vh;
  259 + background-color: #f5f5f5;
217 260 }
  261 +
218 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 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 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 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 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 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 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 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 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 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 338 </style>
... ...
pages-sub/daily/maintain-manage/type-list.vue
1 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 17 </view>
12   - </view>
13 18 </template>
14   -
15 19 <script setup lang="ts">
16 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 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 153 </script>
36   -
37 154 <style scoped lang="scss">
38 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 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 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 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 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 200 .tag-icon {
83   - flex-shrink: 0;
84   - margin-left: 12rpx;
  201 + flex-shrink: 0;
  202 + margin-left: 12rpx;
85 203 }
86 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 +})
... ...