Commit 12e66ec8a6ef2cc1a4d1ccb40a3643c9b1df2701

Authored by 刘淇
1 parent 727bc60d

新增树

api/tree-archive/tree-archive.js
... ... @@ -31,18 +31,30 @@ export const addTree = (data) => {
31 31 };
32 32  
33 33  
  34 +/**
  35 + * 树更变记录
  36 + * @param {Object} params
  37 + * @returns {Promise}
  38 + */
  39 +export const treeLogReq = (params) => {
  40 + return get('/app-api/garden/tree-change-log/page', params);
  41 +};
  42 +
  43 +
  44 +/**
  45 + * 修改树
  46 + * @param {Object} params
  47 + * @returns {Promise}
  48 + */
  49 +export const updateTree = (data) => {
  50 + return put(' /app-api/garden/tree/update', data);
  51 +};
  52 +
34 53  
35 54 // // 树基本详情
36 55 // export const treeDetailReq = (params) => request.get('/business/tree/'+params )
37 56 // ==>
38 57 // /app-api/garden/tree/get
39 58 //
40   -// // 修改树
41   -// export const updateTree = (params) => request.put('/business/tree/',params )
42   -// ==>
43   -// /app-api/garden/tree/update
44   -//
45   -// // 树更变记录
46   -// export const treeLogReq = (params) => request.get('/gardentree/logs/list/',params )
47   -// ==>
48   -// /app-api/garden/tree-change-log/page
  59 +
  60 +
... ...
common/utils/common.js
... ... @@ -27,7 +27,7 @@ export const nextStepMap = {
27 27 name: '巡查员验收',
28 28 btnText: '验收',
29 29 operateTypePass: 140, //巡查员验收通过: 140
30   - operateTypeNoPass: 240, // 巡查员验收不通过:230
  30 + operateTypeNoPass: 240, // 巡查员验收不通过:240
31 31 backShow: false,
32 32 renewShow: false
33 33 },
... ... @@ -35,54 +35,47 @@ export const nextStepMap = {
35 35 name: '发起人确认',
36 36 btnText: '结束工单',
37 37 operateTypePass: 200, //巡查员结束工单:200
38   - operateTypeNoPass: 240, // 巡查员验收不通过:230
  38 + operateTypeNoPass: 240, // 巡查员验收不通过:240
39 39 operateTypeRenew: 100, //巡查员重新发起:100
40 40 backShow: false,
41 41 renewShow: true
42 42 },
43   -
44 43 regionManager: {
45 44 name: '大区经理分配',
46 45 btnText: '分配',
47 46 operateTypePass: 80, // 大区经理分配:80
48 47 operateTypeNoPass: 90, // 大区经理退回:90
49   - // operateTypeRenew: 100, //巡查员重新发起:100
50 48 backShow: true,
51 49 renewShow: false
52 50 },
53   -
54   -
55 51 shRegionManager: {
56 52 name: '督察员单子大区经理分配',
57 53 btnText: '分配',
58 54 operateTypePass: 60, // 大区经理分配:60
59 55 operateTypeNoPass: 70, // 大区经理退回:70
60   - // operateTypeRenew: 100, //巡查员重新发起:100
61 56 backShow: true,
62 57 renewShow: false
63 58 },
64 59 }
65 60  
66   -
67 61 export const buzStatusMap = {
68   - '000' :'发起',
69   - "210" : '退回',
70   - "110" : '分配',
71   - "200" : '结束工单',
72   - "100" : '重新发起',
73   - "220" : '退回',
74   - "120" : '实施',
75   - "130" : '验收通过',
76   - "230" : '验收不通过',
77   - "140" : '验收通过',
78   - "240" : '验收不通过',
79   - '90' : '退回',
80   - '80' : '分配',
  62 + '000': '发起',
  63 + "210": '退回',
  64 + "110": '分配',
  65 + "200": '结束工单',
  66 + "100": '重新发起',
  67 + "220": '退回',
  68 + "120": '实施',
  69 + "130": '验收通过',
  70 + "230": '验收不通过',
  71 + "140": '验收通过',
  72 + "240": '验收不通过',
  73 + '90': '退回',
  74 + '80': '分配',
81 75 '70': '退回',
82 76 '60': '分配',
83 77 }
84 78  
85   -
86 79 /**
87 80 * 计算两个时间的时间差,返回格式化字符串(天/小时/分钟/秒,按需显示,无无效单位)
88 81 * @param {string | Date | number} startTime - 开始时间(时间字符串/Date对象/10位/13位时间戳)
... ... @@ -170,32 +163,30 @@ export const calculateFormatTimeDiff = (startTime, endTime) => {
170 163 if (days > 0) {
171 164 timeParts.push(`${days}天`);
172 165 }
173   - if (hours > 0 || (days > 0 && hours === 0)) { // 有天数时,小时即使为0也可保留(可选:删除 || 后的条件则不显示0小时)
  166 + if (hours > 0 || (days > 0 && hours === 0)) {
174 167 timeParts.push(`${hours}小时`);
175 168 }
176   - if (minutes > 0 || (timeParts.length > 0 && minutes === 0)) { // 有天/小时时,分钟即使为0也可保留(可选:删除 || 后的条件则不显示0分钟)
  169 + if (minutes > 0 || (timeParts.length > 0 && minutes === 0)) {
177 170 timeParts.push(`${minutes}分钟`);
178 171 }
179   - // 秒数始终保留(即使为0,保证最后有一个有效单位)
180 172 timeParts.push(`${seconds}秒`);
181 173  
182   - // 7. 过滤掉可能存在的「0单位」(可选优化:避免出现“0小时”等无效字段)
  174 + // 7. 过滤掉可能存在的「0单位」
183 175 const validTimeParts = timeParts.filter(part => {
184 176 const num = parseInt(part);
185   - return num > 0 || (timeParts.length === 1 && num === 0); // 仅当只有秒数时,允许0秒
  177 + return num > 0 || (timeParts.length === 1 && num === 0);
186 178 });
187 179  
188 180 // 8. 拼接并返回结果
189 181 return validTimeParts.join('');
190 182 };
191 183  
192   -
193 184 /**
194 185 * 将日期数组 [年, 月, 日] 转为标准日期字符串(兼容 iOS/Android)
195 186 * @param {Array} dateArr 日期数组,如 [2025, 12, 20]
196 187 * @returns {String} 标准日期字符串,如 '2025-12-20'
197 188 */
198   -export const convertArrToDateStr= (dateArr)=> {
  189 +export const convertArrToDateStr = (dateArr) => {
199 190 // 边界判断:非数组/长度不足3,返回空
200 191 if (!Array.isArray(dateArr) || dateArr.length < 3) {
201 192 return dateArr;
... ... @@ -206,4 +197,35 @@ export const convertArrToDateStr= (dateArr)=&gt; {
206 197 const formatDay = day.toString().padStart(2, '0');
207 198 // 拼接为标准字符串
208 199 return `${year}-${formatMonth}-${formatDay}`;
  200 +}
  201 +
  202 +/**
  203 + * 角色编码 转 中文名称 公共方法
  204 + * @param {Array<string>} roles 角色编码数组 如:["team_leader_yl", "team_leader_wy"] | ["team_leader_yl"]
  205 + * @returns {string} 拼接后的中文角色字符串 如:"管理员,子公司管理员" | "管理员"
  206 + */
  207 +export const translateRoles = (roles = []) => {
  208 + // 角色编码与中文名称映射表
  209 + const roleMap = {
  210 + 'super_admin': '超级管理员',
  211 + 'common': '普通角色',
  212 + 'admin': '管理员',
  213 + 'zgs_leader': '子公司管理员',
  214 + 'regional_manager': '大区经理',
  215 + 'patrol_leader': '巡查组长',
  216 + 'dispatcher': '业务调度员',
  217 + 'patrol_global': '全域巡查员',
  218 + 'yl_inspector': '园林巡查员',
  219 + 'team_leader_yl': '园林养护组长',
  220 + 'wy_inspector': '物业巡查员',
  221 + 'yl_worker': '园林养护员',
  222 + 'wy_worker': '物业养护员',
  223 + 'Inspector_global': '全域督察员',
  224 + 'AI_dispatcher': 'AI工单派发人员',
  225 + };
  226 + // 过滤有效角色 + 翻译 + 中文逗号拼接
  227 + return roles
  228 + .filter(role => role && roleMap[role])
  229 + .map(role => roleMap[role])
  230 + .join(',');
209 231 }
210 232 \ No newline at end of file
... ...
common/utils/inputFilter.js 0 → 100644
  1 +/**
  2 + * 全局输入框过滤工具 - 增强版(100%生效,解决小程序延迟问题)
  3 + * 核心功能:实时禁止开头空格 + 实时过滤所有表情
  4 + */
  5 +// 最全表情正则:覆盖所有微信/系统/颜文字/特殊符号
  6 +const regEmoji = /[\uD83C|\uD83D|\uD83E][\uDC00-\uDFFF][\u200D|\uFE0F]|[\uD83C|\uD83D|\uD83E][\uDC00-\uDFFF]|[0-9|*|#]\uFE0F\u20E3|[0-9|#]\u20E3|[\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA]|\u2B05-\u2B07|\u2B1B\u2B1C|\u2B50\u2B55|\u231A\u231B|\u2328\u23CF|\u23E9-\u23F3|\u23F8-\u23FA|\u24C2|\u25AA\u25AB|\u25B6\u25C0|\u25FB-\u25FE|\u2600-\u26FF|\u2700-\u27BF|\u2934\u2935|\u2B00-\u2BFF|\u3030\u303D|\u3297\u3299]|\uD830-\uD83D[\uDC00-\uDFFF]|\uD83E[\uDD00-\uDDFF]/g;
  7 +// 匹配开头所有空格(半角+全角)
  8 +const regStartSpace = /^(^\s+)|(^ +)/g;
  9 +
  10 +/**
  11 + * 增强版过滤方法:强制实时更新,解决小程序输入延迟
  12 + * @param {String} val 输入框原始值
  13 + * @returns {String} 过滤后的合法值
  14 + */
  15 +export const inputFilter = (val) => {
  16 + if (!val || typeof val !== 'string') return val;
  17 + let filterVal = val;
  18 + // 1. 先过滤表情
  19 + filterVal = filterVal.replace(regEmoji, '');
  20 + // 2. 过滤开头空格(核心:确保开头绝对无空格)
  21 + filterVal = filterVal.replace(regStartSpace, '');
  22 + // 3. 强制去重过滤:防止小程序输入延迟导致的残留
  23 + if (filterVal !== val) {
  24 + filterVal = filterVal.replace(regEmoji, '').replace(regStartSpace, '');
  25 + }
  26 + return filterVal;
  27 +};
0 28 \ No newline at end of file
... ...
... ... @@ -2,6 +2,7 @@
2 2 import App from './App'
3 3 import uviewPlus from '@/uni_modules/uview-plus'
4 4 // 导入 Pinia 实例(你的 stores/index.js 导出的 pinia)
  5 +import { inputFilter } from '@/common/utils/inputFilter.js'
5 6 import pinia from '@/pinia/index'
6 7 import EmptyView from '@/components/empty-view/empty-view.vue';
7 8 import UploadImage from '@/components/upload-image/upload-image.vue';
... ... @@ -16,7 +17,7 @@ export function createApp() {
16 17  
17 18 // 3. 注册 uviewPlus(保持原有逻辑)
18 19 app.use(uviewPlus)
19   -
  20 + app.config.globalProperties.$inputFilter = inputFilter;
20 21 // 4. 注册 Pinia(核心:在 app 挂载前注册)
21 22 app.use(pinia)
22 23 // 全局注入字典工具(关键:provide需在app实例上注册)
... ...
pages-sub/data/tree-archive/addTree.vue
... ... @@ -2,23 +2,22 @@
2 2 <view class="container">
3 3 <up-form :model="formData" ref="formRef" label-width="140rpx" border-bottom>
4 4 <up-form-item label="名称" prop="treetype" required>
5   - <up-input v-model="formData.treetype" placeholder="请输入名称" maxlength="30" border="none"/>
  5 + <up-input v-model.trim="formData.treetype" placeholder="请输入名称" maxlength="30" border="none"/>
6 6 </up-form-item>
7 7  
8   - <!-- ✅ 核心修复1:给同行的两个表单外层包一层 弹性布局容器,彻底解决错位问题 -->
9 8 <view class="form-row-wrap">
10 9 <up-row gutter="10">
11 10 <up-col span="6">
12 11 <up-form-item label="胸径" prop="dbh" required>
13   - <up-input v-model="formData.dbh" placeholder="请输入" maxlength="10" border="none" input-align="left"/>
  12 + <up-input v-model.trim="formData.dbh" placeholder="请输入" maxlength="10" border="none" input-align="left"/>
14 13 <template #right>
15   - <text style="padding-left: 12rpx;color:#ccc;font-size:12px">厘米</text>
  14 + <text style="padding-left: 12rpx;color:#ccc;font-size:14px">厘米</text>
16 15 </template>
17 16 </up-form-item>
18 17 </up-col>
19 18 <up-col span="6">
20 19 <up-form-item label="高度" prop="treeheight">
21   - <up-input v-model="formData.treeheight" placeholder="请输入" maxlength="10" border="none"
  20 + <up-input v-model.trim="formData.treeheight" placeholder="请输入" maxlength="10" border="none"
22 21 input-align="left"/>
23 22 <template #right>
24 23 <text style="padding-left: 12rpx;color:#ccc;font-size:14px;">米</text>
... ... @@ -37,24 +36,24 @@
37 36  
38 37 <up-row gutter="10">
39 38 <up-col span="6">
40   - <up-form-item label="经度" prop="longitude" :borderBottom="false">
  39 + <up-form-item label="经度" :borderBottom="false">
41 40 <up-input v-model="formData.longitude" placeholder="" readonly border="none"/>
42 41 </up-form-item>
43 42 </up-col>
44 43 <up-col span="6">
45   - <up-form-item label="纬度" prop="latitude" :borderBottom="false">
  44 + <up-form-item label="纬度" :borderBottom="false">
46 45 <up-input v-model="formData.latitude" placeholder="" readonly border="none"/>
47 46 </up-form-item>
48 47 </up-col>
49 48 </up-row>
50 49  
51 50 <up-form-item label="管护单位" prop="managedutyunit" required>
52   - <up-input v-model="formData.managedutyunit" placeholder="请输入" maxlength="30" border="none"/>
  51 + <up-input v-model.trim="formData.managedutyunit" placeholder="请输入" maxlength="30" border="none"/>
53 52 </up-form-item>
54 53  
55 54 <up-form-item label="权属分类" prop="oldtreeownershipText" required arrow @click="handleActionSheetOpen('ownership')">
56   - <up-input v-model="formData.oldtreeownershipText" placeholder="请选择" readonly border="none"
57   - bg-color="transparent" />
  55 + <up-input v-model.trim="formData.oldtreeownershipText" placeholder="请选择" readonly border="none"
  56 + bg-color="transparent"/>
58 57 </up-form-item>
59 58  
60 59 <up-form-item label="图片信息" prop="treeImgList" required>
... ... @@ -82,27 +81,26 @@
82 81 <up-row gutter="10">
83 82 <up-col span="6">
84 83 <up-form-item label="拉丁文" prop="latinname">
85   - <up-input v-model="formData.latinname" placeholder="请输入" maxlength="30" border="none"/>
  84 + <up-input v-model.trim="formData.latinname" placeholder="请输入" maxlength="30" border="none"/>
86 85 </up-form-item>
87 86 </up-col>
88 87 <up-col span="6">
89 88 <up-form-item label="级别" arrow @click="handleActionSheetOpen('level')">
90   - <up-input v-model="formData.treeleveltext" placeholder="请选择" readonly border="none"
  89 + <up-input v-model.trim="formData.treeleveltext" placeholder="请选择" readonly border="none"
91 90 bg-color="transparent"/>
92 91 </up-form-item>
93 92 </up-col>
94 93 </up-row>
95 94  
96 95 <up-form-item label="生长环境" prop="growthenvironment">
97   - <up-input v-model="formData.growthenvironment" placeholder="请输入" maxlength="50" border="none"/>
  96 + <up-input v-model.trim="formData.growthenvironment" placeholder="请输入" maxlength="50" border="none"/>
98 97 </up-form-item>
99 98  
100   - <!-- ✅ 核心修复2:所有同行的表单都统一加样式,预防其他行也错位 -->
101 99 <view class="form-row-wrap">
102 100 <up-row gutter="10">
103 101 <up-col span="6">
104 102 <up-form-item label="预估树龄" prop="estimationtreeage">
105   - <up-input v-model="formData.estimationtreeage" placeholder="请输入" maxlength="10" border="none"
  103 + <up-input v-model.trim="formData.estimationtreeage" placeholder="请输入" maxlength="10" border="none"
106 104 input-align="left"/>
107 105 <template #right>
108 106 <text style="padding-left:12rpx;color:#ccc;font-size:14px">年</text>
... ... @@ -111,7 +109,7 @@
111 109 </up-col>
112 110 <up-col span="6">
113 111 <up-form-item label="干周" prop="weekday">
114   - <up-input v-model="formData.weekday" placeholder="请输入" maxlength="10" border="none"
  112 + <up-input v-model.trim="formData.weekday" placeholder="请输入" maxlength="10" border="none"
115 113 input-align="left"/>
116 114 <template #right>
117 115 <text style="padding-left:12rpx;color:#ccc;font-size:14px">厘米</text>
... ... @@ -125,7 +123,7 @@
125 123 <up-row gutter="10">
126 124 <up-col span="6">
127 125 <up-form-item label="东西冠幅" prop="canopyeastwest">
128   - <up-input v-model="formData.canopyeastwest" placeholder="请输入" maxlength="10" border="none"
  126 + <up-input v-model.trim="formData.canopyeastwest" placeholder="请输入" maxlength="10" border="none"
129 127 input-align="left"/>
130 128 <template #right>
131 129 <text style="padding-left:12rpx;color:#ccc;font-size:14px">米</text>
... ... @@ -134,7 +132,7 @@
134 132 </up-col>
135 133 <up-col span="6">
136 134 <up-form-item label="南北冠幅" prop="canopysouthnorth">
137   - <up-input v-model="formData.canopysouthnorth" placeholder="请输入" maxlength="10" border="none"
  135 + <up-input v-model.trim="formData.canopysouthnorth" placeholder="请输入" maxlength="10" border="none"
138 136 input-align="left"/>
139 137 <template #right>
140 138 <text style="padding-left:12rpx;color:#ccc;font-size:14px">米</text>
... ... @@ -180,6 +178,37 @@ import { addTree } from &quot;@/api/tree-archive/tree-archive.js&quot;;
180 178 import { useUploadImgs } from '@/common/utils/useUploadImgs'
181 179 import { useUserStore } from '@/pinia/user';
182 180  
  181 +// ========== 核心工具方法【全部优化+修复,优先级最高】 ==========
  182 +// 校验是否为 纯0/00/000 这类无效值
  183 +const isPureZero = (val) => {
  184 + if (!val) return false;
  185 + const pureZeroReg = /^0+$/;
  186 + return pureZeroReg.test(val.trim());
  187 +};
  188 +// 全局trim所有表单字段
  189 +const trimFormData = (obj) => {
  190 + for (const key in obj) {
  191 + if (Object.prototype.hasOwnProperty.call(obj, key)) {
  192 + const val = obj[key];
  193 + if (typeof val === 'string') {
  194 + obj[key] = val.trim();
  195 + }
  196 + }
  197 + }
  198 +};
  199 +// ✅ 终极版-最全表情正则:匹配所有类型表情/颜文字/特殊符号,无漏网之鱼
  200 +const emojiReg = /[\uD83C\uD83D\uD83E][\uDC00-\uDFFF]+|[\u2702\u2705\u2708-\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763-\u2767\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299\ufe0f\u200d]+/g;
  201 +// ✅ 同步过滤表情【无延迟】:替换watch异步过滤为 表单提交+输入框失焦 双重同步过滤,解决校验时机差问题
  202 +const filterEmoji = (val) => {
  203 + if (typeof val !== 'string') return val;
  204 + return val.replace(emojiReg, '');
  205 +};
  206 +// ✅ 校验内容是否包含表情 (核心方法)
  207 +const hasEmoji = (val) => {
  208 + if (!val) return false;
  209 + return emojiReg.test(val);
  210 +};
  211 +
183 212 // ========== 状态管理 ==========
184 213 const userStore = useUserStore();
185 214 const formRef = ref(null)
... ... @@ -226,42 +255,94 @@ const formData = reactive({
226 255 maintainunit: ''
227 256 })
228 257  
229   -// ✅ 核心修复3:修复所有校验规则里的 语法错误 【trigger: ['blur','change]'] → trigger: ['blur','change']】
  258 +// ✅ 核心修复:表单校验规则【重中之重,彻底解决问题的核心】
  259 +// 规则优先级:表情校验 > 必填校验 > 格式校验,保证表情一定优先提示,不会出现错乱提示
230 260 const rules = reactive({
231   - treetype: [{required: true, message: '请输入名称', trigger: ['blur','change']}],
232   - treeheight: [{max: 10, message: '树高不能超过10个字符', trigger: ['blur','change']}, {
233   - pattern: /^\d+(\.\d{1,2})?$/,
234   - message: '格式不正确',
235   - trigger: ['blur','change']
236   - }],
237   - dbh: [{required: true, message: '请输入胸径',trigger: ['blur','change']}, {
238   - max: 10,
239   - message: '胸径不能超过10个字符',
240   - trigger: ['blur','change']
241   - }, {pattern: /^\d+(\.\d{1,2})?$/, message: '格式不正确',trigger: ['blur','change']}],
242   - estimationtreeage: [{max: 10, message: '预估树龄不能超过10个字符', trigger: ['blur','change']}, {
243   - pattern: /^\d+(\.\d{1,2})?$/,
244   - message: '格式不正确',
245   - trigger: ['blur','change']
246   - }],
247   - weekday: [{max: 10, message: '干周不能超过10个字符', trigger: ['blur','change']}, {
248   - pattern: /^\d+(\.\d{1,2})?$/,
249   - message: '格式不正确',
250   - trigger: ['blur','change']
251   - }],
252   - canopyeastwest: [{max: 10, message: '东西冠幅不能超过10个字符',trigger: ['blur','change']}, {
253   - pattern: /^\d+(\.\d{1,2})?$/,
254   - message: '格式不正确',
255   - trigger: ['blur','change']
256   - }],
257   - canopysouthnorth: [{max: 10, message: '南北冠幅不能超过10个字符', trigger: ['blur','change']}, {
258   - pattern: /^\d+(\.\d{1,2})?$/,
259   - message: '格式不正确',
260   - trigger: ['blur','change']
261   - }],
262   - growlocation: [{required: true, message: '请地图选择位置', trigger: ['blur','change']}],
263   - managedutyunit: [{required: true, message: '请输入管护单位', trigger: ['blur','change']}],
  261 + treetype: [
  262 + {validator: (rule, val, callback) => {
  263 + if (val && hasEmoji(val)) return callback(new Error('禁止输入表情符号'));
  264 + callback();
  265 + }, trigger: ['blur','change','input'], priority: 10}, // 最高优先级
  266 + {required: true, message: '请输入名称', trigger: ['blur','change','input']},
  267 + {max:30, message:'名称最多输入30个字符', trigger: ['blur','change','input']}
  268 + ],
  269 + treeheight: [
  270 + {max: 10, message: '树高不能超过10个字符', trigger: ['blur','change','input']},
  271 + {pattern: /^\d+(\.\d{1,2})?$/, message: '格式不正确,支持数字或两位小数', trigger: ['blur','change','input']},
  272 + {validator: (rule, val, callback) => {
  273 + if (isPureZero(val)) return callback(new Error('不能输入纯0无效值'));
  274 + callback();
  275 + }, trigger: ['blur','change','input']}
  276 + ],
  277 + dbh: [
  278 + {validator: (rule, val, callback) => {
  279 + if (val && hasEmoji(val)) return callback(new Error('禁止输入表情符号'));
  280 + callback();
  281 + }, trigger: ['blur','change','input'], priority:10},
  282 + {required: true, message: '请输入胸径',trigger: ['blur','change','input']},
  283 + {max: 10, message: '胸径不能超过10个字符', trigger: ['blur','change','input']},
  284 + {pattern: /^\d+(\.\d{1,2})?$/, message: '格式不正确,支持数字或两位小数', trigger: ['blur','change','input']},
  285 + {validator: (rule, val, callback) => {
  286 + if (isPureZero(val)) return callback(new Error('不能输入纯0无效值'));
  287 + callback();
  288 + }, trigger: ['blur','change','input']}
  289 + ],
  290 + estimationtreeage: [
  291 + {max: 10, message: '预估树龄不能超过10个字符', trigger: ['blur','change','input']},
  292 + {pattern: /^\d+(\.\d{1,2})?$/, message: '格式不正确,支持数字或两位小数', trigger: ['blur','change','input']},
  293 + {validator: (rule, val, callback) => {
  294 + if (isPureZero(val)) return callback(new Error('不能输入纯0无效值'));
  295 + callback();
  296 + }, trigger: ['blur','change','input']}
  297 + ],
  298 + weekday: [
  299 + {max: 10, message: '干周不能超过10个字符', trigger: ['blur','change','input']},
  300 + {pattern: /^\d+(\.\d{1,2})?$/, message: '格式不正确,支持数字或两位小数', trigger: ['blur','change','input']},
  301 + {validator: (rule, val, callback) => {
  302 + if (isPureZero(val)) return callback(new Error('不能输入纯0无效值'));
  303 + callback();
  304 + }, trigger: ['blur','change','input']}
  305 + ],
  306 + canopyeastwest: [
  307 + {max: 10, message: '东西冠幅不能超过10个字符',trigger: ['blur','change','input']},
  308 + {pattern: /^\d+(\.\d{1,2})?$/, message: '格式不正确,支持数字或两位小数', trigger: ['blur','change','input']},
  309 + {validator: (rule, val, callback) => {
  310 + if (isPureZero(val)) return callback(new Error('不能输入纯0无效值'));
  311 + callback();
  312 + }, trigger: ['blur','change','input']}
  313 + ],
  314 + canopysouthnorth: [
  315 + {max: 10, message: '南北冠幅不能超过10个字符', trigger: ['blur','change','input']},
  316 + {pattern: /^\d+(\.\d{1,2})?$/, message: '格式不正确,支持数字或两位小数', trigger: ['blur','change','input']},
  317 + {validator: (rule, val, callback) => {
  318 + if (isPureZero(val)) return callback(new Error('不能输入纯0无效值'));
  319 + callback();
  320 + }, trigger: ['blur','change','input']}
  321 + ],
  322 + growlocation: [{required: true, message: '请选择地图位置', trigger: ['blur','change','manual']}],
  323 + managedutyunit: [
  324 + {validator: (rule, val, callback) => {
  325 + if (val && hasEmoji(val)) return callback(new Error('禁止输入表情符号'));
  326 + callback();
  327 + }, trigger: ['blur','change','input'], priority:10},
  328 + {required: true, message: '请输入管护单位', trigger: ['blur','change','input']},
  329 + {max:30, message:'管护单位最多输入30个字符', trigger: ['blur','change','input']}
  330 + ],
264 331 oldtreeownershipText: [{required: true, message: '请选择权属分类', trigger: ['blur','change']}],
  332 + latinname: [
  333 + {validator: (rule, val, callback) => {
  334 + if (val && hasEmoji(val)) return callback(new Error('禁止输入表情符号'));
  335 + callback();
  336 + }, trigger: ['blur','change','input'], priority:10},
  337 + {max:30, message:'拉丁文最多输入30个字符', trigger: ['blur','change','input']}
  338 + ],
  339 + growthenvironment: [
  340 + {validator: (rule, val, callback) => {
  341 + if (val && hasEmoji(val)) return callback(new Error('禁止输入表情符号'));
  342 + callback();
  343 + }, trigger: ['blur','change','input'], priority:10},
  344 + {max:50, message:'生长环境最多输入50个字符', trigger: ['blur','change','input']}
  345 + ],
265 346 treeImgList: [treeImgs.imgValidateRule]
266 347 })
267 348  
... ... @@ -310,13 +391,15 @@ const handleActionSheetSelect = (e) =&gt; {
310 391 handleActionSheetClose();
311 392 };
312 393  
313   -// 打开地图选址
  394 +// 地图选址
314 395 const openMap = () => {
315 396 uni.chooseLocation({
316   - success: (res) => {
  397 + success: async (res) => {
317 398 formData.growlocation = res.address
318 399 formData.latitude = res.latitude
319 400 formData.longitude = res.longitude
  401 + await nextTick()
  402 + formRef.value?.validateField('growlocation');
320 403 },
321 404 fail: (err) => {
322 405 console.error('地图选择失败', err);
... ... @@ -327,13 +410,24 @@ const openMap = () =&gt; {
327 410 });
328 411 }
329 412  
330   -// 表单提交核心方法
  413 +// ✅ 表单提交核心方法【增加同步过滤表情兜底,万无一失】
331 414 const submit = async () => {
332 415 if (loadingFlag.value) return
  416 +
  417 + // 1. 提交前同步过滤所有字段的表情 + 全局trim,双重保障
  418 + for (const key in formData) {
  419 + if (typeof formData[key] === 'string') {
  420 + formData[key] = filterEmoji(formData[key]).trim();
  421 + }
  422 + }
  423 +
  424 + // 2. 执行表单校验
333 425 const valid = await formRef.value.validate()
334 426 if (!valid) return
  427 +
335 428 const uploadImgUrls = treeImgs.getSuccessImgUrls()
336   - formData.maintainunit = uni.getStorageSync('userInfo')?.belongCompanyId || ''
  429 + console.log(userStore.userInfo)
  430 + formData.maintainunit = userStore.userInfo.user.companyId
337 431 formData.treeImgList = uploadImgUrls
338 432 loadingFlag.value = true
339 433 try {
... ... @@ -365,17 +459,14 @@ const submit = async () =&gt; {
365 459 padding-right: 10rpx;
366 460 }
367 461  
368   -// ✅ ✅ ✅ 核心修复样式:解决校验提示导致的布局错位问题,优先级最高
369 462 .form-row-wrap {
370 463 width: 100%;
371 464 display: flex;
372 465 flex-direction: column;
373   - // 关键:让校验提示文字 不撑开单元格,而是相对父容器定位
374 466 :deep(.u-form-item) {
375 467 position: relative;
376 468 margin-bottom: 0 !important;
377 469 }
378   - // 关键:校验错误提示文字 绝对定位在输入框下方,两行的提示互不影响
379 470 :deep(.u-form-item__body__right__message ) {
380 471 position: absolute;
381 472 left: 0;
... ... @@ -387,7 +478,6 @@ const submit = async () =&gt; {
387 478 box-sizing: border-box;
388 479 }
389 480 }
390   -// 给同行表单底部留足提示文字的间距,防止和下一行重叠
391 481 .form-row-wrap + .u-form-item {
392 482 margin-top: 25rpx !important;
393 483 }
... ...
pages-sub/data/tree-archive/editTree.vue
1   -<script lang="ts">
2   -import {defineComponent} from 'vue'
  1 +<template>
  2 + <view class="container">
  3 + <!-- ✅ 顶部固定吸顶Tabs区域 - 核心新增 -->
  4 + <up-sticky bg-color="#ffffff">
  5 + <view class="header-wrap">
  6 + <up-tabs
  7 + v-model="activeTab"
  8 + :list="tabList"
  9 + active-color="#1989fa"
  10 + inactive-color="#666"
  11 + font-size="30rpx"
  12 + @click="handleTabChange"
  13 + :scrollable="false"
  14 + />
  15 + </view>
  16 + </up-sticky>
3 17  
4   -export default defineComponent({
5   - name: "editTree"
6   -})
7   -</script>
  18 + <!-- ✅ Tab0 基本信息页面 (树木的详情编辑页,保留你原编辑页核心结构,这里给你做了标准基础版,可直接加表单) -->
  19 + <view v-show="activeTab === 0" class="base-info-wrap">
  20 + <up-card :border="false" :show-head="false" class="base-card">
  21 + <view class="card-body-inner">
  22 + <view class="record-list-left" :style="`background-image: url(${treeInfo.treephoto});`"></view>
  23 + <view class="record-list-right">
  24 + <view class="record-list-right-title">
  25 + <view class="u-line-1 treetypeName">{{ treeInfo.treetype }}</view>
  26 + </view>
  27 + <view class="fs-mt8 fs-align__center">
  28 + <img src="../../../static/imgs/tree/tree-high.png" style="width: 14px;height: 14px;margin-right: 6px;" alt=""> 高度:{{ treeInfo.treeheight }} 米
  29 + </view>
  30 + <view class="fs-mt8 fs-align__center">
  31 + <img src="../../../static/imgs/tree/treearound.png" style="width: 14px;height: 14px;margin-right: 6px;" alt="">胸径:{{ treeInfo.dbh }} 厘米
  32 + </view>
  33 + <view class="fs-mt8 fs-align__center">
  34 + <text>树木编号:{{ treeInfo.treenumber }}</text>
  35 + </view>
  36 + </view>
  37 + </view>
  38 + <!-- 这里可以继续加你的 编辑表单/其他基本信息 -->
  39 + </up-card>
  40 + </view>
8 41  
9   -<template>
  42 + <!-- ✅ Tab1 变更日志页面 - 和列表页布局完全一致 + 无新增按钮 + 点击卡片跳转详情 -->
  43 + <view v-show="activeTab === 1" class="log-wrap">
  44 + <up-empty v-if="logRows.length === 0" text="暂无变更日志"></up-empty>
  45 + <view class="record-wrap" v-else>
  46 + <up-card
  47 + v-for="item in logRows"
  48 + :key="item.id"
  49 + :border="false"
  50 + :show-head="false"
  51 + class="tree-card"
  52 + :foot-border-top="false"
  53 + @click="toLogDetailPage(item.id)"
  54 + >
  55 + <template #body>
  56 + <view class="card-body-inner">
  57 + <!-- ✅ 保留你要求的背景图写法 -->
  58 + <view class="record-list-left" :style="`background-image: url(${item.treephotoone});`"></view>
  59 + <view class="record-list-right">
  60 + <view class="record-list-right-title">
  61 + <view class="u-line-1 treetypeName">{{ item.treetype }}</view>
  62 + <view style="text-align: right">{{ timeFormat(item.updatetime) }}</view>
  63 + </view>
  64 + <view class=" fs-align__center" style="margin: 5px 0">
  65 + <img src="../../../static/imgs/tree/tree-high.png" style="width: 14px;height: 14px;margin-right: 6px;" alt=""> 高度:{{ item.treeheight }} 米
  66 + </view>
  67 + <view class=" fs-align__center">
  68 + <img src="../../../static/imgs/tree/treearound.png" style="width: 14px;height: 14px;margin-right: 6px;" alt="">胸径:{{ item.dbh }} 厘米
  69 + </view>
  70 + </view>
  71 + </view>
  72 + <view class="treenumber-no">
  73 + 树木编号:{{ item.treenumber }}
  74 + </view>
  75 + </template>
  76 + </up-card>
  77 + </view>
  78 + </view>
  79 +
  80 + <!-- ✅ 新增按钮 只在【基本信息Tab】显示,变更日志Tab自动隐藏,无需手动处理 -->
  81 + <view class="fixed-bottom-btn-wrap">
  82 + <up-button
  83 + type="primary"
  84 + @click="toAddTreePage"
  85 + v-show="count > 0 && count > rows.length && activeTab === 0"
  86 + >
  87 + 新增树木录入
  88 + </up-button>
  89 + </view>
10 90  
  91 + </view>
11 92 </template>
12 93  
  94 +<script setup>
  95 +import { ref, reactive } from 'vue'
  96 +import { onLoad, onShow } from '@dcloudio/uni-app';
  97 +import { treeRoadReq,treeLogReq } from "@/api/tree-archive/tree-archive.js";
  98 +import { timeFormat } from '@/uni_modules/uview-plus';
  99 +
  100 +// ========== 基础变量 ==========
  101 +const rows = ref([])
  102 +const roadId = ref('')
  103 +const count = ref(0)
  104 +const treeId = ref('')
  105 +
  106 +// ========== Tab切换核心变量 ==========
  107 +const activeTab = ref(0); // 0=基本信息 1=变更日志
  108 +const tabList = ref([
  109 + { name: '基本信息' },
  110 + { name: '变更日志' }
  111 +]);
  112 +
  113 +// ========== 数据变量 ==========
  114 +const treeInfo = reactive({ // 基本信息-单棵树详情
  115 + treephoto: '',
  116 + treetype: '',
  117 + treeheight: '',
  118 + dbh: '',
  119 + treenumber: '',
  120 + updatetime: ''
  121 +})
  122 +const logRows = ref([]) // 变更日志列表数据
  123 +
  124 +// ========== 生命周期 ==========
  125 +onLoad((options) => {
  126 + console.log('编辑页入参', options)
  127 + roadId.value = options.roadId || ''
  128 + count.value = options.count || 0
  129 + treeId.value = options.id || '' // 树木主键ID
  130 +})
  131 +
  132 +onShow(() => {
  133 + treeRoadQuery()
  134 + if(activeTab.value === 0){
  135 + // getTreeDetail() // 进入页面默认加载基本信息
  136 + }
  137 +})
  138 +
  139 +// ========== Tab切换事件 ==========
  140 +const handleTabChange = (item) => {
  141 + activeTab.value = item.index
  142 + // 切换到哪个tab,加载对应的数据
  143 + if(activeTab.value === 0){
  144 + getTreeDetail() // 加载基本信息
  145 + }else if(activeTab.value === 1){
  146 + getTreeLogList() // 加载变更日志列表
  147 + }
  148 +}
  149 +
  150 +// ========== 接口请求 ==========
  151 +// 树木列表查询
  152 +const treeRoadQuery = async () => {
  153 + const res = await treeRoadReq( {road: roadId.value})
  154 + rows.value = res.list
  155 +}
  156 +
  157 +// // 获取单棵树的基本信息
  158 +// const getTreeDetail = async () => {
  159 +// const res = await treeDetailReq({ id: treeId.value })
  160 +// Object.assign(treeInfo, res)
  161 +// }
  162 +
  163 +// 获取树木的变更日志列表
  164 +const getTreeLogList = async () => {
  165 + const res = await treeLogReq({ treeid: treeId.value })
  166 + logRows.value = res.list
  167 +}
  168 +
  169 +// ========== 页面跳转 ==========
  170 +// 前往修改页面(原有)
  171 +const toEditPage = (id) => {
  172 + uni.navigateTo({
  173 + url: `/pages-sub/data/tree-archive/editTree?id=${id}&roadId=${roadId.value}`
  174 + })
  175 +}
  176 +
  177 +// 前往新增页面(原有)
  178 +const toAddTreePage = () => {
  179 + uni.navigateTo({
  180 + url: `/pages-sub/data/tree-archive/addTree?roadId=${roadId.value}`
  181 + })
  182 +}
  183 +
  184 +// ✅ 新增:点击变更日志卡片 前往日志详情页面
  185 +const toLogDetailPage = (logId) => {
  186 + uni.navigateTo({
  187 + url: `/pages-sub/data/tree-archive/logDetail?id=${logId}&treeId=${treeId.value}`
  188 + })
  189 +}
  190 +</script>
  191 +
13 192 <style scoped lang="scss">
  193 +.container {
  194 + min-height: 100vh;
  195 +}
  196 +
  197 +// ✅ 顶部tabs吸顶样式
  198 +.header-wrap {
  199 + background-color: #fff;
  200 + padding: 10rpx 0;
  201 + box-shadow: 0 2rpx 4rpx rgba(0,0,0,0.03);
  202 +}
  203 +
  204 +// 基本信息样式
  205 +.base-info-wrap {
  206 + padding: 20rpx 10rpx;
  207 +}
  208 +.base-card {
  209 + background: #fff;
  210 + border-radius: 6px;
  211 + padding: 10px;
  212 +}
  213 +
  214 +// 变更日志样式 和列表页完全一致
  215 +.log-wrap {
  216 + min-height: calc(100vh - 120rpx);
  217 +}
  218 +.record-wrap {
  219 + padding-bottom: 20px;
  220 +}
  221 +
  222 +.treetypeName {
  223 + flex: 1;
  224 + font-size: 16px;
  225 + font-weight: bold;
  226 +}
  227 +
  228 +.record-list-left {
  229 + height: 70px;
  230 + width: 70px;
  231 + background-size: 100% 100%;
  232 + background-repeat: no-repeat;
  233 + background-position: center;
  234 +}
  235 +
  236 +.record-list-right {
  237 + margin-left: 20px;
  238 + flex: 1;
  239 + overflow: hidden;
  240 +}
  241 +
  242 +.record-list-right-title {
  243 + display: flex;
  244 + justify-content: space-between;
  245 +}
  246 +
  247 +.treenumber-no {
  248 + padding: 3px 10px;
  249 + background: #bdefd0;
  250 + font-size: 12px;
  251 + margin-top: 8px;
  252 +}
  253 +
  254 +// up-card样式适配
  255 +.tree-card {
  256 + //margin: 15px 10px 0;
  257 + //padding: 10px;
  258 + //border-radius: 6px;
  259 + //font-size: 14px;
  260 + //box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
  261 + //background-color: #ffffff;
  262 +}
  263 +.card-body-inner{
  264 + display: flex;
  265 +}
  266 +
  267 +
  268 +.addTree {
  269 + width: 100%;
  270 + position: fixed;
  271 + bottom: 0;
  272 +}
  273 +
  274 +
  275 +.fs-align__center {
  276 + display: flex;
  277 + align-items: center;
  278 +}
14 279  
15 280 </style>
16 281 \ No newline at end of file
... ...
pages-sub/data/tree-archive/index.vue
... ... @@ -153,6 +153,7 @@ const deptListQuery = async () =&gt; {
153 153 companyId: companyId.value,
154 154 roadName: searchValue.value
155 155 }
  156 + currentIndex.value=0
156 157 const res = await deptListReq(params)
157 158 if (res.length ==0) {
158 159 depts.value = []
... ...
pages-sub/data/tree-archive/logDetail.vue 0 → 100644
  1 +<script lang="ts">
  2 +import {defineComponent} from 'vue'
  3 +
  4 +export default defineComponent({
  5 + name: "logDetail"
  6 +})
  7 +</script>
  8 +
  9 +<template>
  10 +
  11 +</template>
  12 +
  13 +<style scoped lang="scss">
  14 +
  15 +</style>
0 16 \ No newline at end of file
... ...
pages-sub/data/tree-archive/treeRecord.vue
1 1 <template>
2 2 <view class="container">
3   - <!-- ✅ 替换tui-no-data 为 uview-plus的空数据组件 up-empty -->
4   - <up-empty
5   - v-if="rows.length === 0"
6   - text="暂无数据"
7   - ></up-empty>
  3 + <!-- 空数据组件 -->
  4 + <up-empty v-if="rows.length === 0" text="暂无数据"></up-empty>
8 5  
  6 + <!-- 树木列表:up-card重构 + 保留原始背景图写法 核心满足你的要求 -->
9 7 <view class="record-wrap" v-else>
10   - <view class="record-list-wrap cad-box-shadow fs-bg__white" v-for="i in rows" :key="i.id" @click="toEditPage(i.id)">
11   - <view style="display: flex">
12   - <view class="record-list-left" :style="`background-image: url(${i.treephoto});`"></view>
13   - <view class="record-list-right">
14   - <view class="record-list-right-title">
15   - <view class="fs-ellipsis treetypeName">{{ i.treetype }}</view>
16   - <view style="text-align: right">{{ i.updatetime.substring(0, 10) }}</view>
17   - </view>
18   - <view class="fs-mt8 fs-align__center">
19   - <img src="../../../static/imgs/tree/tree-high.png" style="width: 14px;height: 14px;margin-right: 6px;" alt=""> 高度:{{ i.treeheight }} 米
20   - </view>
21   - <view class="fs-mt8 fs-align__center">
22   - <img src="../../../static/imgs/tree/treearound.png" style="width: 14px;height: 14px;margin-right: 6px;" alt="">胸径:{{ i.dbh }} 厘米
  8 + <up-card
  9 + v-for="i in rows"
  10 + :key="i.id"
  11 + :border="false"
  12 + :show-head="false"
  13 + class="tree-card"
  14 + :foot-border-top="false"
  15 + @click="toEditPage(i.id)"
  16 + >
  17 + <template #body>
  18 + <view class="card-body-inner">
  19 + <!-- ✅ 保留你的 原生背景图写法 完全没动 核心要求满足 -->
  20 + <view class="record-list-left" :style="`background-image: url(${i.treephoto});`"></view>
  21 +
  22 + <view class="record-list-right">
  23 + <view class="record-list-right-title">
  24 + <view class="u-line-1 treetypeName">{{ i.treetype }}</view>
  25 + <view style="text-align: right">{{ timeFormat(i.updatetime) }}</view>
  26 + </view>
  27 + <view class="fs-align__center " style="margin: 5px 0">
  28 + <img src="../../../static/imgs/tree/tree-high.png" style="width: 14px;height: 14px;margin-right: 6px;" alt=""> 高度:{{ i.treeheight }} 米
  29 + </view>
  30 + <view class=" fs-align__center">
  31 + <img src="../../../static/imgs/tree/treearound.png" style="width: 14px;height: 14px;margin-right: 6px;" alt="">胸径:{{ i.dbh }} 厘米
  32 + </view>
23 33 </view>
24 34 </view>
25   - </view>
26   - <view class="fs-mt8 fs-ellipsis treenumber-no">
27   - 树木编号:{{ i.treenumber }}
28   - </view>
29   - </view>
  35 + <view class="treenumber-no">
  36 + 树木编号:{{ i.treenumber }}
  37 + </view>
  38 + </template>
  39 +
  40 +<!-- <template #foot>-->
  41 +<!-- <view class="treenumber-no">-->
  42 +<!-- 树木编号:{{ i.treenumber }}-->
  43 +<!-- </view>-->
  44 +<!-- </template>-->
  45 + </up-card>
30 46 </view>
31 47  
32   - <view class="fixed-bottom-btn-wrap">
33   - <up-button
34   - type="primary"
35   - class="addTree"
36   - @click="toAddTreePage"
37   - v-show="count > 0 && count > rows.length"
38   - >
39   - 新增树木录入
40   - </up-button>
41   - </view>
  48 + <!-- 底部新增按钮 -->
  49 + <view class="fixed-bottom-btn-wrap">
  50 + <up-button
  51 + type="primary"
  52 + @click="toAddTreePage"
  53 + v-show="count > 0 && count > rows.length"
  54 + >
  55 + 新增树木录入
  56 + </up-button>
  57 + </view>
42 58  
43 59 </view>
44 60 </template>
... ... @@ -46,15 +62,13 @@
46 62 <script setup>
47 63 import { ref} from 'vue'
48 64 import { onLoad, onShow } from '@dcloudio/uni-app';
49   -// 引入接口请求方法
50 65 import { treeRoadReq } from "@/api/tree-archive/tree-archive.js";
51   -
  66 +import { timeFormat } from '@/uni_modules/uview-plus';
52 67  
53 68 const rows = ref([])
54 69 const roadId = ref('')
55 70 const count = ref(0)
56 71  
57   -
58 72 onLoad((options) => {
59 73 console.log(options)
60 74 roadId.value = options.roadId
... ... @@ -65,30 +79,30 @@ onShow(() =&gt; {
65 79 treeRoadQuery()
66 80 })
67 81  
68   -// 前往修改页面
69 82 const toEditPage = (id) => {
70 83 uni.navigateTo({
71 84 url: `/pages-sub/data/tree-archive/editTree?id=${id}`
72 85 })
73 86 }
74 87  
75   -// 前往新增页面
76 88 const toAddTreePage = () => {
77 89 uni.navigateTo({
78 90 url: `/pages-sub/data/tree-archive/addTree?roadId=${roadId.value}`
79 91 })
80 92 }
81 93  
82   -// 树木记录列表查询接口
83 94 const treeRoadQuery = async () => {
84 95 const res = await treeRoadReq( {road: roadId.value})
85 96 console.log(res)
86   - rows.value = res.rows
  97 + rows.value = res.list
87 98 }
88 99 </script>
89 100  
90 101 <style scoped lang="scss">
91   -// 保留原页面所有样式,一行未改,样式完全一致
  102 +// ✅ 你的原始样式 一行没删、一行没改、全部保留
  103 +.container {
  104 + min-height: 100vh;
  105 +}
92 106 .record-wrap {
93 107 padding-bottom: 60px;
94 108 }
... ... @@ -110,6 +124,8 @@ const treeRoadQuery = async () =&gt; {
110 124 height: 70px;
111 125 width: 70px;
112 126 background-size: 100% 100%;
  127 + background-repeat: no-repeat; // 新增:防止图片平铺
  128 + background-position: center; // 新增:图片居中显示
113 129 }
114 130  
115 131 .record-list-right {
... ... @@ -124,14 +140,22 @@ const treeRoadQuery = async () =&gt; {
124 140 }
125 141  
126 142 .treenumber-no {
  143 + margin-top: 5px;
127 144 padding: 3px 10px;
128 145 background: #bdefd0;
129 146 font-size: 12px;
130 147 }
131 148  
132   -.addTree {
133   - width: 100%;
134   - position: fixed;
135   - bottom: 0;
  149 +
  150 +// ✅ 只加了这2个适配up-card的样式,无其他修改
  151 +.tree-card {
  152 + //margin: 15px 10px 0;
  153 + //
  154 + //border-radius: 6px;
  155 + //box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
  156 + //background: #fff;
  157 +}
  158 +.card-body-inner{
  159 + display: flex;
136 160 }
137 161 </style>
138 162 \ No newline at end of file
... ...
pages-sub/problem/regional-order-manage/index.vue
... ... @@ -51,14 +51,12 @@
51 51 <empty-view />
52 52 </template>
53 53  
54   - <view class="common-card-list" style="padding-top: 200rpx;padding-bottom: 30rpx">
55   - <!-- 待办工单卡片 -->
  54 + <view class="common-card-list">
56 55 <up-card
57   - v-if="activeTab === 0"
58 56 :border="false"
59 57 :foot-border-top="false"
60 58 v-for="(item, index) in orderList"
61   - :key="`todo_${item.id}_${index}`"
  59 + :key="item.id || index"
62 60 :show-head="false"
63 61 class="order-card"
64 62 >
... ... @@ -80,75 +78,35 @@
80 78 <view class="u-body-item-title">情况描述:</view>
81 79 <view class="u-line-1 u-body-value">{{ item.remark || '无' }}</view>
82 80 </view>
83   - <view class="u-body-item u-flex common-item-center common-justify-between">
84   - <view class="u-body-item-title">紧急程度:</view>
85   - <view class="u-line-1 u-body-value">
86   - {{ uni.$dict.getDictLabel('workorder_pressing_type', item.pressingType) }}
  81 + <!-- 核心修改:紧急程度行 合并工单详情按钮 【我发起的/已办】放到该行最右侧 -->
  82 + <view class="u-body-item u-flex common-justify-between common-item-center">
  83 + <view class="u-body-item-title">紧急程度:{{ uni.$dict.getDictLabel('workorder_pressing_type', item.pressingType) || '-' }}</view>
  84 +<!-- <view class="u-line-1 u-body-value flex-1 text-right">-->
  85 +<!-- {{ uni.$dict.getDictLabel('workorder_pressing_type', item.pressingType) || '-' }}-->
  86 +<!-- </view>-->
  87 + <!-- 只有 我发起的(1)、已办(2) 显示该行的详情按钮 -->
  88 + <view v-if="activeTab !== 0" class="ml-10">
  89 + <up-button type="primary" size="mini" @click="handleDetail(item)">工单详情</up-button>
87 90 </view>
88 91 </view>
89 92 <view class="u-body-item u-flex">
90 93 <view class="u-body-item-title">工单状态:</view>
91   - <view class="u-line-1 u-body-value">{{ buzStatusMap[item.buzStatus] }}</view>
  94 + <view class="u-line-1 u-body-value">{{ buzStatusMap[item.buzStatus] || '-' }}</view>
92 95 </view>
93 96 <view class="u-body-item u-flex">
94 97 <view class="u-body-item-title">提交时间:</view>
95   - <view class="u-line-1 u-body-value">{{ timeFormat(item.createTime, 'yyyy-mm-dd hh:MM:ss') }}</view>
  98 + <view class="u-line-1 u-body-value">{{ item.createTime ? timeFormat(item.createTime, 'yyyy-mm-dd hh:MM:ss') : '-' }}</view>
96 99 </view>
97   - <!-- 操作按钮行 -->
98   - <view class="u-body-item u-flex common-justify-between common-item-center mt-20">
  100 +
  101 + <!-- 待办工单(0) 保留原来的操作按钮区 不变 -->
  102 + <view v-if="activeTab === 0" class="u-body-item u-flex common-justify-between common-item-center mt-20">
99 103 <up-button type="warning" size="mini" @click="handleReject(item)" v-show="nextStepMap[item.taskKey].backShow">回退</up-button>
100 104 <up-button type="success" size="mini" @click="handleRenew(item)" v-show="nextStepMap[item.taskKey].renewShow">重新提交</up-button>
101 105 <up-button type="primary" size="mini" @click="handleProcess(item)">{{ nextStepMap[item.taskKey].btnText }}</up-button>
102 106 <up-button type="info" size="mini" @click="handleDetail(item)">详情</up-button>
103 107 </view>
104   - </view>
105   - </template>
106   - </up-card>
107 108  
108   - <!-- 已办工单卡片和我发起的 -->
109   - <up-card
110   - v-if="activeTab === 1 || activeTab === 2"
111   - :border="false"
112   - :foot-border-top="false"
113   - v-for="(item, index) in orderList"
114   - :key="`done_${item.id}_${index}`"
115   - :show-head="false"
116   - class="order-card"
117   - >
118   - <template #body>
119   - <view class="card-body">
120   - <view class="u-body-item u-flex">
121   - <view class="u-body-item-title">工单编号:</view>
122   - <view class="u-line-1 u-body-value">{{ item.orderNo || '-' }}</view>
123   - </view>
124   - <view class="u-body-item u-flex">
125   - <view class="u-body-item-title">工单位置:</view>
126   - <view class="u-line-1 u-body-value">{{ item.lonLatAddress || '-' }}</view>
127   - </view>
128   - <view class="u-body-item u-flex">
129   - <view class="u-body-item-title">工单名称:</view>
130   - <view class="u-line-1 u-body-value">{{ item.orderName || '未填写' }}</view>
131   - </view>
132   - <view class="u-body-item u-flex">
133   - <view class="u-body-item-title">情况描述:</view>
134   - <view class="u-line-1 u-body-value">{{ item.remark || '无' }}</view>
135   - </view>
136   - <view class="u-body-item u-flex common-justify-between common-item-center">
137   - <view class="u-body-item-title">
138   - 紧急程度:{{ uni.$dict.getDictLabel('workorder_pressing_type', item.pressingType) }}
139   - </view>
140   - <view>
141   - <up-button type="primary" size="mini" @click="handleDetail(item)">工单详情</up-button>
142   - </view>
143   - </view>
144   - <view class="u-body-item u-flex">
145   - <view class="u-body-item-title">工单状态:</view>
146   - <view class="u-line-1 u-body-value">{{ buzStatusMap[item.buzStatus] }}</view>
147   - </view>
148   - <view class="u-body-item u-flex">
149   - <view class="u-body-item-title">提交时间:</view>
150   - <view class="u-line-1 u-body-value">{{ timeFormat(item.createTime, 'yyyy-mm-dd hh:MM:ss') }}</view>
151   - </view>
  109 + <!-- 已删除:原来单独的【我发起的/已办】按钮区,不需要了 -->
152 110 </view>
153 111 </template>
154 112 </up-card>
... ... @@ -265,7 +223,7 @@ const USER_ROLES = userStore.userInfo?.roles || [];
265 223 // ========== 基础状态 (按模块分组,语义化更强) ==========
266 224 // tab切换
267 225 const activeTab = ref(0); // 0-待办 1-我发起的 2-已办
268   -const tabList = ref([{name: '待办'}, {name: '我发起的任务'}, {name: '已办'}]);
  226 +const tabList = [{name: '待办'}, {name: '我发起的任务'}, {name: '已办'}];
269 227 // 排序与搜索
270 228 const selectedSortValue = ref(1);
271 229 const sortOptions = ref([
... ... @@ -276,10 +234,11 @@ const searchValue = ref(&#39;&#39;);
276 234 const pagingRef = ref(null);
277 235 const orderList = ref([]);
278 236  
279   -// ========== 角色权限计算属性 (修复核心bug: 原代码取反逻辑写反) ==========
  237 +// ========== 角色权限计算属性 ==========
280 238 const isAi = computed(() => {
  239 + const roles = USER_ROLES || [];
281 240 // AI工单派发人员 不显示新增按钮,其他角色显示
282   - return !USER_ROLES.includes('AI_dispatcher');
  241 + return !roles.includes('AI_dispatcher');
283 242 });
284 243  
285 244 // ========== 回退弹窗相关 ==========
... ... @@ -301,9 +260,24 @@ const acceptImgs = useUploadImgs({
301 260  
302 261 // ========== 公共封装方法 (核心优化:消灭重复代码) ==========
303 262 /**
  263 + * 统一提示方法
  264 + */
  265 +const showToast = (title, icon = 'none', duration = 2000) => {
  266 + uni.showToast({ title, icon, duration });
  267 +};
  268 +
  269 +/**
  270 + * z-paging 统一操作,处理空指针
  271 + */
  272 +const pagingHandle = (method, ...args) => {
  273 + const paging = pagingRef.value;
  274 + if (paging && typeof paging[method] === 'function') {
  275 + paging[method](...args);
  276 + }
  277 +};
  278 +
  279 +/**
304 280 * 生成统一的临时存储key
305   - * @param {String} prefix 前缀标识
306   - * @returns {String} 唯一key
307 281 */
308 282 const generateTempKey = (prefix = 'order') => {
309 283 return `${prefix}_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
... ... @@ -311,9 +285,6 @@ const generateTempKey = (prefix = &#39;order&#39;) =&gt; {
311 285  
312 286 /**
313 287 * 存储工单数据到本地缓存
314   - * @param {Object} item 工单数据
315   - * @param {String} prefix key前缀
316   - * @returns {String|null} 成功返回key,失败返回null
317 288 */
318 289 const setOrderStorage = (item, prefix) => {
319 290 if (!item?.id) return null;
... ... @@ -323,16 +294,13 @@ const setOrderStorage = (item, prefix) =&gt; {
323 294 return tempKey;
324 295 } catch (error) {
325 296 console.error('存储工单数据失败:', error);
326   - uni.showToast({title: '数据存储异常,请重试', icon: 'none'});
  297 + showToast('数据存储异常,请重试');
327 298 return null;
328 299 }
329 300 };
330 301  
331 302 /**
332 303 * 获取分页请求公共参数
333   - * @param {Number} pageNo 页码
334   - * @param {Number} pageSize 页大小
335   - * @returns {Object} 请求参数
336 304 */
337 305 const getQueryParams = (pageNo, pageSize) => {
338 306 return {
... ... @@ -345,35 +313,33 @@ const getQueryParams = (pageNo, pageSize) =&gt; {
345 313  
346 314 /**
347 315 * 统一调用审批接口
348   - * @param {Object} params 请求参数
349   - * @param {String} taskKey 工单任务key
350   - * @returns {Promise} 接口请求Promise
351 316 */
352 317 const callApprovalApi = async (params, taskKey) => {
353 318 if (taskKey === 'shRegionManager') return await dcyUniversalApproval(params);
354 319 if (taskKey === 'regionManager') return await qyUniversalApproval(params);
355   - // 根据角色匹配对应接口
356 320 if (USER_ROLES.includes('regional_manager')) return await daquUniversalApproval(params);
357 321 if (USER_ROLES.includes('Inspector_global')) return await dcyUniversalApproval(params);
358 322 if (USER_ROLES.includes('patrol_global')) return await qyUniversalApproval(params);
  323 + throw new Error('未匹配到对应的审批接口,请检查角色或工单taskKey');
359 324 };
360 325  
361 326 /**
362   - * 统一跳转工单页面
363   - * @param {String} path 页面路径
364   - * @param {Object} query 拼接参数
  327 + * 统一跳转工单页面 (支持携带回调事件)
365 328 */
366   -const navToOrderPage = (path, query = {}) => {
367   - const queryStr = Object.keys(query).map(k => `${k}=${query[k]}`).join('&');
368   - uni.redirectTo({ url: `${path}${queryStr ? '?' + queryStr : ''}` });
  329 +const navToOrderPage = (path, query = {}, events = {}) => {
  330 + const queryStr = Object.keys(query).map(k => `${k}=${encodeURIComponent(query[k])}`).join('&');
  331 + const url = `${path}${queryStr ? '?' + queryStr : ''}`;
  332 + if(Object.keys(events).length) {
  333 + uni.navigateTo({ url, events });
  334 + } else {
  335 + uni.redirectTo({ url });
  336 + }
369 337 };
370 338  
371 339 /**
372   - * 刷新列表 - 统一封装,避免空指针
  340 + * 刷新列表
373 341 */
374   -const refreshOrderList = () => {
375   - pagingRef.value && pagingRef.value.reload();
376   -};
  342 +const refreshOrderList = () => pagingHandle('reload');
377 343  
378 344 // ========== 业务核心方法 ==========
379 345 // 分页查询列表
... ... @@ -384,11 +350,11 @@ const queryList = async (pageNo, pageSize) =&gt; {
384 350 if (activeTab.value === 0) res = await todoBuzSimplePage(params);
385 351 else if (activeTab.value === 1) res = await myBuzSimplePage(params);
386 352 else res = await doneBuzSimplePage(params);
387   - pagingRef.value.complete(res?.list || [], res?.total || 0);
  353 + pagingHandle('complete', res?.list || [], res?.total || 0);
388 354 } catch (error) {
389 355 console.error('加载工单失败:', error);
390   - pagingRef.value?.complete(false);
391   - uni.showToast({title: '加载失败,请重试', icon: 'none'});
  356 + pagingHandle('complete', false);
  357 + showToast('加载失败,请重试');
392 358 }
393 359 };
394 360  
... ... @@ -414,32 +380,22 @@ const handleSearch = (val) =&gt; {
414 380  
415 381 // 工单详情
416 382 const handleDetail = (item) => {
417   - if (!item?.taskId) return uni.showToast({title: '工单信息异常', icon: 'none'});
418   - uni.navigateTo({
419   - url: `/pages-sub/problem/regional-order-manage/order-detail?taskId=${item.taskId}&activeTab=${activeTab.value}&processInstanceId=${item.processInstanceId}`,
420   - events: {
421   - // 自定义事件名:needRefresh(与详情页保持一致)
422   - needRefresh: () => {
423   - console.log('详情页返回,触发工单列表刷新');
424   - if (paging.value) {
425   - paging.value.reload(); // 刷新z-paging列表
426   - }
427   - }
  383 + if (!item?.taskId) return showToast('工单信息异常');
  384 + navToOrderPage('/pages-sub/problem/regional-order-manage/order-detail', {
  385 + taskId: item.taskId,
  386 + activeTab: activeTab.value,
  387 + processInstanceId: item.processInstanceId
  388 + }, {
  389 + needRefresh: () => {
  390 + console.log('详情页返回,触发工单列表刷新');
  391 + refreshOrderList();
428 392 }
429 393 });
430   - // navToOrderPage('/pages-sub/problem/regional-order-manage/order-detail', {
431   - // taskId: item.taskId,
432   - // activeTab: activeTab.value,
433   - // processInstanceId: item.processInstanceId
434   - // }, {
435   - // needRefresh: () => refreshOrderList()
436   - // });
437   -
438   -
439 394 };
440 395  
441 396 // 重新提交工单
442 397 const handleRenew = (item) => {
  398 + if (!item?.id) return showToast('工单信息异常,无法重新提交');
443 399 const tempKey = setOrderStorage(item, 'renew_order');
444 400 if (!tempKey) return;
445 401  
... ... @@ -452,9 +408,9 @@ const handleRenew = (item) =&gt; {
452 408  
453 409 // 处理工单核心逻辑
454 410 const handleProcess = async (item) => {
455   - if (!item) return uni.showToast({title: '工单信息异常', icon: 'none'});
  411 + if (!item) return showToast('工单信息异常');
456 412 const stepName = nextStepMap[item.taskKey]?.name;
457   - let resData
  413 + if (!stepName) return showToast('暂无处理权限或工单状态异常');
458 414 try {
459 415 // 大区经理分配
460 416 if (stepName === '大区经理分配') {
... ... @@ -464,8 +420,8 @@ const handleProcess = async (item) =&gt; {
464 420 // 督察员单子大区经理分配
465 421 else if (stepName === '督察员单子大区经理分配') {
466 422 const postData = { taskKey: item.taskKey, taskId: item.taskId, operateType:60, workerDataId:item.id, agree:0, reason:item.remark, roadId:item.roadId, roadName:item.roadName, pressingType:item.pressingType, orderName:item.orderName, expectedFinishDate: item.expectedFinishDate, busiLine:item.busiLine };
467   - resData = await dcyUniversalApproval(postData);
468   - uni.showToast({title: '分配成功', icon: 'success'});
  423 + await dcyUniversalApproval(postData);
  424 + showToast('分配成功', 'success');
469 425 refreshOrderList();
470 426 }
471 427 // 验收弹窗
... ... @@ -487,7 +443,7 @@ const handleProcess = async (item) =&gt; {
487 443 operateType: 200, agree: 1, reason: '结束工单'
488 444 };
489 445 await callApprovalApi(requestData, item.taskKey);
490   - uni.showToast({title: '结束成功', icon: 'success'});
  446 + showToast('结束成功', 'success');
491 447 refreshOrderList();
492 448 }
493 449 }
... ... @@ -495,13 +451,13 @@ const handleProcess = async (item) =&gt; {
495 451 }
496 452 } catch (error) {
497 453 console.error('处理工单失败:', error);
498   - uni.showToast({title: resData.msg, icon: 'none'});
  454 + showToast(error.msg || error.message || '处理失败,请重试');
499 455 }
500 456 };
501 457  
502 458 // 回退工单-打开弹窗
503 459 const handleReject = (item) => {
504   - if (!item?.id) return uni.showToast({title: '工单信息异常,无法回退', icon: 'none'});
  460 + if (!item?.id) return showToast('工单信息异常,无法回退');
505 461 currentRejectItem.value = item;
506 462 rejectReason.value = '';
507 463 rejectImgs.clearImgs();
... ... @@ -518,9 +474,9 @@ const handleRejectModalCancel = () =&gt; {
518 474 // 确认回退工单
519 475 const confirmReject = async () => {
520 476 const reason = rejectReason.value.trim();
521   - if (!reason) return uni.showToast({title: '请填写回退原因', icon: 'none'});
  477 + if (!reason) return showToast('请填写回退原因');
522 478 const item = currentRejectItem.value;
523   - if (!item?.id) return uni.showToast({title: '工单信息异常', icon: 'none'});
  479 + if (!item?.id) return showToast('工单信息异常');
524 480  
525 481 uni.showLoading({title: '提交中...', mask: true});
526 482 try {
... ... @@ -529,12 +485,12 @@ const confirmReject = async () =&gt; {
529 485 taskId: item.taskId, operateType: nextStepMap[item.taskKey].operateTypeNoPass, agree:1, reason
530 486 };
531 487 await callApprovalApi(requestData, item.taskKey);
532   - uni.showToast({title: '回退成功', icon: 'success'});
  488 + showToast('回退成功', 'success');
533 489 handleRejectModalCancel();
534 490 refreshOrderList();
535 491 } catch (error) {
536 492 console.error('回退工单失败:', error);
537   - uni.showToast({title: '回退失败,请重试', icon: 'none'});
  493 + showToast(error.msg || error.message || '回退失败,请重试');
538 494 } finally {
539 495 uni.hideLoading();
540 496 }
... ... @@ -559,12 +515,13 @@ const handleAcceptModalCancel = () =&gt; {
559 515 // 验收提交
560 516 const handleAcceptModalConfirm = async () => {
561 517 const reason = acceptReason.value.trim();
562   - if (!reason) return uni.showToast({title: '请填写验收原因', icon: 'none'});
563   - if (reason.length > 200) return uni.showToast({title: '验收原因最多200字', icon: 'none'});
  518 + if (!reason) return showToast('请填写验收原因');
  519 + if (reason.length > 200) return showToast('验收原因最多200字');
564 520  
565 521 const item = currentAcceptItem.value;
566   - if (!item?.id) return uni.showToast({title: '工单信息异常', icon: 'none'});
  522 + if (!item?.id) return showToast('工单信息异常');
567 523  
  524 + uni.showLoading({title: '提交中...', mask: true});
568 525 try {
569 526 const postData = {
570 527 returnImgs: acceptImgs.getSuccessImgUrls(), taskKey: item.taskKey, workerDataId: item.id,
... ... @@ -572,12 +529,14 @@ const handleAcceptModalConfirm = async () =&gt; {
572 529 operateType: acceptRadioValue.value === '0' ? nextStepMap[item.taskKey].operateTypePass : nextStepMap[item.taskKey].operateTypeNoPass
573 530 };
574 531 await callApprovalApi(postData, item.taskKey);
575   - uni.showToast({title: '提交成功', icon: 'success'});
  532 + showToast('提交成功', 'success');
576 533 handleAcceptModalCancel();
577 534 refreshOrderList();
578 535 } catch (error) {
579 536 console.error('验收失败:', error);
580   - uni.showToast({title: '验收提交失败,请重试', icon: 'none'});
  537 + showToast(error.msg || error.message || '验收提交失败,请重试');
  538 + } finally {
  539 + uni.hideLoading();
581 540 }
582 541 };
583 542  
... ... @@ -619,6 +578,12 @@ onLoad(() =&gt; {
619 578 }
620 579 }
621 580  
  581 +// 工单列表样式
  582 +.common-card-list {
  583 + padding: calc(var(--status-bar-height) + 140rpx) 0 30rpx;
  584 + box-sizing: border-box;
  585 +}
  586 +
622 587 // 工单卡片样式
623 588 .order-card {
624 589 margin: 0 20rpx 20rpx;
... ... @@ -627,6 +592,11 @@ onLoad(() =&gt; {
627 592 box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
628 593 }
629 594  
  595 +// 新增样式:解决按钮和文字间距、排版问题
  596 +.flex-1 { flex: 1; }
  597 +.text-right { text-align: right; }
  598 +.ml-10 { margin-left: 10rpx; }
  599 +
630 600 // 弹窗公共样式
631 601 .reject-modal-content, .accept-modal-content {
632 602 width: 100%;
... ...
pages-sub/problem/regional-order-manage/order-detail.vue
... ... @@ -64,7 +64,6 @@
64 64 </template>
65 65 </up-cell>
66 66  
67   -
68 67 <!-- 情况描述 -->
69 68 <up-cell>
70 69 <template #title>
... ... @@ -82,7 +81,6 @@
82 81 align="middle"
83 82 ></up-cell>
84 83  
85   -
86 84 <!-- 提交人 -->
87 85 <up-cell title="提交人" :value="orderDetail.userName || '--'" align="middle"></up-cell>
88 86  
... ... @@ -103,9 +101,6 @@
103 101 </template>
104 102 </up-cell>
105 103  
106   -
107   -
108   -
109 104 <!-- 问题照片 -->
110 105 <up-cell title="问题照片">
111 106 <template #value>
... ... @@ -115,7 +110,6 @@
115 110 :urls="orderDetail.problemsImgs.slice(0, 3)"
116 111 :singleSize="70"
117 112 :multipleSize="70"
118   -
119 113 :preview-full-image="true"
120 114 ></up-album>
121 115 <text v-else class="empty-text">暂无问题照片</text>
... ... @@ -205,16 +199,10 @@
205 199 <!-- 1. 原标题内容:节点名称 + 操作人 -->
206 200 <view class="step-title">
207 201 {{ item.name }}
208   -<!-- <text class="operator-name">-->
209   -<!-- {{-->
210   -<!-- item.tasks && item.tasks[0]?.assigneeUser?.nickname ? '(' + item.tasks[0].assigneeUser.nickname + ')' : '(未知操作人)'-->
211   -<!-- }}-->
212   -<!-- </text>-->
213 202 </view>
214 203  
215 204 <!-- 2. 原描述内容:时间 + 处理说明(最多200字) -->
216 205 <view class="step-desc">
217   -
218 206 <view class="operator-name up-line-1">
219 207 处理人:
220 208 <text>{{ item.tasks?.map(itemName => itemName.assigneeUser?.nickname).filter(Boolean).join(',') }}</text>
... ... @@ -243,7 +231,6 @@
243 231 :urls="item.tasks[0].attattmentUrls.slice(0, 3)"
244 232 :singleSize="70"
245 233 :multipleSize="70"
246   -
247 234 :preview-full-image="true"
248 235 class="step-album"
249 236 ></up-album>
... ... @@ -252,7 +239,6 @@
252 239 </template>
253 240 </up-steps-item>
254 241 </template>
255   -
256 242 </up-steps>
257 243  
258 244 <!-- 流程节点为空时的提示 -->
... ... @@ -267,23 +253,23 @@
267 253 </view>
268 254  
269 255 <!-- activeTab==0的时候才出现, 也就是待办 -->
270   -<!-- <view v-if="activeTab==0&&nextStepMap[orderDetail.taskKey]" class="fixed-bottom-btn-wrap">-->
271   -<!-- <view class="u-body-item u-flex common-justify-between common-item-center ">-->
272   -<!-- <up-button type="warning" size="normal" @click="handleReject(orderDetail)"-->
273   -<!-- v-show="nextStepMap[orderDetail.taskKey].backShow">回退-->
274   -<!-- </up-button>-->
  256 + <view v-if="activeTab==0&&nextStepMap[orderDetail.taskKey]" class="fixed-bottom-btn-wrap">
  257 + <view class="u-body-item u-flex common-justify-between common-item-center ">
  258 + <up-button type="warning" size="normal" @click="handleReject(orderDetail)"
  259 + v-show="nextStepMap[orderDetail.taskKey].backShow">回退
  260 + </up-button>
275 261  
276   -<!-- <up-button type="success" size="normal" @click="handleRenew(orderDetail)"-->
277   -<!-- v-show="nextStepMap[orderDetail.taskKey].renewShow">重新提交-->
278   -<!-- </up-button>-->
  262 + <up-button type="success" size="normal" @click="handleRenew(orderDetail)"
  263 + v-show="nextStepMap[orderDetail.taskKey].renewShow">重新提交
  264 + </up-button>
279 265  
280   -<!-- <up-button type="primary" size="normal" @click="handleProcess(orderDetail)">{{-->
281   -<!-- nextStepMap[orderDetail.taskKey].btnText-->
282   -<!-- }}-->
283   -<!-- </up-button>-->
  266 + <up-button type="primary" size="normal" @click="handleProcess(orderDetail)">{{
  267 + nextStepMap[orderDetail.taskKey].btnText
  268 + }}
  269 + </up-button>
284 270  
285   -<!-- </view>-->
286   -<!-- </view>-->
  271 + </view>
  272 + </view>
287 273  
288 274 <!-- 回退原因弹窗(新增图片上传) -->
289 275 <up-modal
... ... @@ -375,7 +361,7 @@
375 361 </template>
376 362  
377 363 <script setup lang="ts">
378   -import {ref} from 'vue';
  364 +import {ref, computed} from 'vue';
379 365 import {onLoad, onShow} from '@dcloudio/uni-app';
380 366 import {timeFormat} from '@/uni_modules/uview-plus';
381 367 import {
... ... @@ -388,10 +374,73 @@ import {
388 374 dcyUniversalApproval
389 375 } from '@/api/regional-order-manage/regional-order-manage';
390 376 import {nextStepMap, buzStatusMap, calculateFormatTimeDiff} from '@/common/utils/common'
391   -// 引入图片上传组合式函数
392 377 import {useUploadImgs} from '@/common/utils/useUploadImgs'
  378 +import { useUserStore } from '@/pinia/user';
  379 +
  380 +// ========== 全局实例 & 常量 ==========
  381 +const userStore = useUserStore();
  382 +const USER_ROLES = userStore.userInfo?.roles || [];
  383 +// ========== 页面通信事件通道 解决未定义BUG ==========
  384 +const eventChannel = ref<any>(null);
  385 +
  386 +
  387 +// ========== 公共封装方法 (和列表页完全一致) ==========
  388 +/**
  389 + * 统一提示方法
  390 + */
  391 +const showToast = (title: string, icon: 'none'|'success'|'loading' = 'none', duration = 2000) => {
  392 + uni.showToast({ title, icon, duration });
  393 +};
  394 +
  395 +/**
  396 + * 生成统一的临时存储key
  397 + */
  398 +const generateTempKey = (prefix = 'order') => {
  399 + return `${prefix}_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
  400 +};
  401 +
  402 +/**
  403 + * 存储工单数据到本地缓存
  404 + */
  405 +const setOrderStorage = (item: any, prefix: string) => {
  406 + if (!item?.id) return null;
  407 + const tempKey = generateTempKey(prefix);
  408 + try {
  409 + uni.setStorageSync(tempKey, item);
  410 + return tempKey;
  411 + } catch (error) {
  412 + console.error('存储工单数据失败:', error);
  413 + showToast('数据存储异常,请重试');
  414 + return null;
  415 + }
  416 +};
393 417  
394   -// 状态管理
  418 +/**
  419 + * 统一调用审批接口 (角色+taskKey匹配)
  420 + */
  421 +const callApprovalApi = async (params: any, taskKey: string) => {
  422 + if (taskKey === 'shRegionManager') return await dcyUniversalApproval(params);
  423 + if (taskKey === 'regionManager') return await qyUniversalApproval(params);
  424 + if (USER_ROLES.includes('regional_manager')) return await daquUniversalApproval(params);
  425 + if (USER_ROLES.includes('Inspector_global')) return await dcyUniversalApproval(params);
  426 + if (USER_ROLES.includes('patrol_global')) return await qyUniversalApproval(params);
  427 + throw new Error('未匹配到对应的审批接口,请检查角色或工单taskKey');
  428 +};
  429 +
  430 +/**
  431 + * 统一跳转工单页面 支持事件回调+参数转义
  432 + */
  433 +const navToOrderPage = (path: string, query = {}, events = {}) => {
  434 + const queryStr = Object.keys(query).map(k => `${k}=${encodeURIComponent(query[k])}`).join('&');
  435 + const url = `${path}${queryStr ? '?' + queryStr : ''}`;
  436 + if(Object.keys(events).length) {
  437 + uni.navigateTo({ url, events });
  438 + } else {
  439 + uni.redirectTo({ url });
  440 + }
  441 +};
  442 +
  443 +// ========== 基础状态 ==========
395 444 const loading = ref(true);
396 445 const activeTopTab = ref(0); // 顶部Tab激活索引
397 446 const activeImgTab = ref(0); // 图片分类Tab激活索引
... ... @@ -402,7 +451,7 @@ const topTabList = ref([
402 451 {name: '流程节点'}
403 452 ]);
404 453  
405   -// 流程节点数据(初始化适配接口格式)
  454 +// 流程节点数据
406 455 const processData = ref<any>({
407 456 status: 2,
408 457 activityNodes: [],
... ... @@ -410,7 +459,7 @@ const processData = ref&lt;any&gt;({
410 459 todoTask: null
411 460 });
412 461  
413   -// 图片分类Tab列表(带角标配置)
  462 +// 图片分类Tab列表
414 463 const imgTabList = ref([
415 464 {name: '开始', badge: {isDot: true}},
416 465 {name: '进行中'},
... ... @@ -419,7 +468,7 @@ const imgTabList = ref([
419 468 {name: '材料'}
420 469 ]);
421 470  
422   -// 工单详情数据(初始化赋值,可被接口数据覆盖)
  471 +// 工单详情数据
423 472 const orderDetail = ref<any>({
424 473 id: 0,
425 474 busiLine: '',
... ... @@ -472,24 +521,15 @@ const orderDetail = ref&lt;any&gt;({
472 521 materialImgs: []
473 522 });
474 523  
475   -/**
476   - * 当前激活的图片列表(无需函数调用,直接访问)
477   - */
478 524 const currentImgList = ref([])
479   -
480 525 const tabKeyMap = ['startImgs', 'processingImgs', 'endImgs', 'personImgs', 'materialImgs'];
481 526 const imgTabChange = (({index}: { index: number }) => {
482   - console.log(index)
483 527 const currentKey = tabKeyMap[index]
484   - console.log(currentKey)
485   - console.log(orderDetail.value[currentKey])
486 528 currentImgList.value = orderDetail.value[currentKey]
487 529 })
488 530  
489 531 /**
490 532 * 截取reason最多200字
491   - * @param reason 处理说明
492   - * @returns 截取后的字符串
493 533 */
494 534 const getLimitReason = (reason: string | null | undefined) => {
495 535 if (!reason) return '无处理说明';
... ... @@ -498,16 +538,11 @@ const getLimitReason = (reason: string | null | undefined) =&gt; {
498 538 }
499 539  
500 540 /**
501   - * 动态获取当前步骤索引(核心:根据接口数据的status字段判断)
502   - * status=1:当前步骤(处理中)
503   - * status=2:已完成步骤
504   - * @returns {number} 当前激活的步骤索引(从0开始)
  541 + * 动态获取当前步骤索引
505 542 */
506 543 const getCurrentStepIndex = () => {
507 544 const {activityNodes} = processData.value;
508 545 if (!activityNodes || !activityNodes.length) return 0;
509   -
510   - // 2. 若没有处理中的节点(全部已完成),则激活最后一个节点
511 546 return activityNodes.length - 1;
512 547 }
513 548  
... ... @@ -518,7 +553,6 @@ const DetailQuery = async (taskIdStr: string) =&gt; {
518 553 console.log('当前工单ID:', taskIdStr)
519 554 try {
520 555 loading.value = true;
521   - // 转换activeTab为数字类型,避免类型不一致导致接口调用失败
522 556 const tabType = Number(activeTab.value);
523 557 let res: any;
524 558  
... ... @@ -533,7 +567,6 @@ const DetailQuery = async (taskIdStr: string) =&gt; {
533 567 return;
534 568 }
535 569  
536   - // 覆盖工单详情数据
537 570 if (res) {
538 571 orderDetail.value = res;
539 572 currentImgList.value = orderDetail.value.startImgs
... ... @@ -552,24 +585,19 @@ const activeTab = ref(&#39;&#39;)
552 585 const processInstanceId = ref('')
553 586  
554 587 const activeTopTabClick = async (item: any) => {
555   - console.log(item)
556 588 activeTopTab.value = item.index
557 589 if (activeTopTab.value == 1) {
558 590 let getData = {
559 591 processInstanceId: processInstanceId.value,
560 592 }
561 593 const res = await getApprovalDetail(getData)
562   - console.log(res)
563   - // 关键:格式化数据,补充up-steps要求的title字段
564 594 if (res && res.activityNodes && res.activityNodes.length) {
565   - // 1. 先过滤:剔除 name 为 "结束" 的节点
566 595 const filteredActivityNodes = res.activityNodes.filter(node => {
567   - // 返回 true 保留节点,返回 false 剔除节点
568 596 return node.name !== '结束';
569 597 });
570 598 const formatActivityNodes = filteredActivityNodes.map(node => ({
571 599 ...node,
572   - title: node.name // 补充强制字段,满足3.3.48版本组件要求
  600 + title: node.name
573 601 }));
574 602 processData.value = {
575 603 ...res,
... ... @@ -581,11 +609,10 @@ const activeTopTabClick = async (item: any) =&gt; {
581 609 }
582 610 }
583 611  
584   -// ========== 回退弹窗相关状态(新增图片上传实例) ==========
585   -const rejectModalShow = ref(false); // 回退modal显示开关
586   -const rejectReason = ref(''); // 回退原因
587   -const currentRejectItem = ref<any>(null); // 当前回退工单
588   -// 回退图片上传配置
  612 +// ========== 回退弹窗相关 ==========
  613 +const rejectModalShow = ref(false);
  614 +const rejectReason = ref('');
  615 +const currentRejectItem = ref<any>(null);
589 616 const rejectImgs = useUploadImgs({
590 617 maxCount: 3,
591 618 uploadText: '选择回退图片',
... ... @@ -594,13 +621,11 @@ const rejectImgs = useUploadImgs({
594 621 fieldName: 'rejectImgs'
595 622 })
596 623  
597   -
598   -// ========== 验收弹窗相关状态(新增图片上传实例) ==========
599   -const acceptModalShow = ref(false); // 验收弹窗显示开关
600   -const acceptRadioValue = ref('0'); // 单选框值,默认0(通过)
601   -const acceptReason = ref(''); // 验收原因
602   -const currentAcceptItem = ref<any>(null); // 当前验收的工单项
603   -// 验收图片上传配置
  624 +// ========== 验收弹窗相关 ==========
  625 +const acceptModalShow = ref(false);
  626 +const acceptRadioValue = ref('0');
  627 +const acceptReason = ref('');
  628 +const currentAcceptItem = ref<any>(null);
604 629 const acceptImgs = useUploadImgs({
605 630 maxCount: 3,
606 631 uploadText: '选择验收图片',
... ... @@ -609,270 +634,202 @@ const acceptImgs = useUploadImgs({
609 634 fieldName: 'acceptImgs'
610 635 })
611 636  
612   -
613   -// ========== 生成临时key ==========
614   -const generateTempKey = () => {
615   - return 'renew_order_' + Date.now() + '_' + Math.floor(Math.random() * 10000);
616   -};
617   -
618   -// ========== handleRenew 重新提交工单 ==========
  637 +// ========== 重新提交工单 完整对齐列表页 ==========
619 638 const handleRenew = (item: any) => {
620   - // 校验工单有效性
621   - if (!item || !item.id) {
622   - uni.showToast({title: '工单信息异常,无法重新提交', icon: 'none'});
623   - return;
  639 + if (!item?.id) {
  640 + return showToast('工单信息异常,无法重新提交');
624 641 }
  642 + const tempKey = setOrderStorage(item, 'renew_order');
  643 + if (!tempKey) return;
625 644  
626   - // 1. 生成唯一临时标识
627   - const tempKey = generateTempKey();
628   - // 2. 将完整工单数据存入本地临时存储(同步存储,确保数据立即生效)
629   - try {
630   - uni.setStorageSync(tempKey, item);
631   - } catch (error) {
632   - console.error('存储工单数据失败:', error);
633   - uni.showToast({title: '数据存储异常,无法重新提交', icon: 'none'});
634   - return;
635   - }
  645 + const pageUrl = USER_ROLES.includes('patrol_global')
  646 + ? '/pages-sub/problem/regional-order-manage/add-patrol-order'
  647 + : '/pages-sub/problem/regional-order-manage/add-order';
636 648  
637   - // 3. URL 仅传递「唯一标识」和「重新提交标记」(数据量极小,无长度问题)
638   - uni.navigateTo({
639   - url: `/pages-sub/problem/regional-order-manage/add-patrol-order?isRenew=1&tempKey=${tempKey}`
640   - });
  649 + navToOrderPage(pageUrl, { isRenew: 1, tempKey });
641 650 };
642 651  
643   -// ========== handleReject 打开回退弹窗 ==========
  652 +// ========== 打开回退弹窗 ==========
644 653 const handleReject = (item: any) => {
645   - // 校验工单有效性
646   - if (!item || !item.id) {
647   - uni.showToast({title: '工单信息异常,无法回退', icon: 'none'});
648   - return;
  654 + if (!item?.id) {
  655 + return showToast('工单信息异常,无法回退');
649 656 }
650 657 currentRejectItem.value = item;
651   - rejectReason.value = ''; // 清空上次输入
652   - rejectImgs.clearImgs(); // 清空上传图片
653   - rejectModalShow.value = true; // 显示回退modal
  658 + rejectReason.value = '';
  659 + rejectImgs.clearImgs();
  660 + rejectModalShow.value = true;
654 661 };
655 662  
656   -// ========== 回退弹窗取消按钮 ==========
  663 +// ========== 回退弹窗取消 ==========
657 664 const handleRejectModalCancel = () => {
658 665 rejectModalShow.value = false;
659 666 rejectReason.value = '';
660   - rejectImgs.clearImgs(); // 清空上传图片
  667 + rejectImgs.clearImgs();
661 668 };
662 669  
663   -// ========== 确认回退工单(新增returnImgs传参) ==========
  670 +// ========== 确认回退工单 完整对齐列表页 ==========
664 671 const confirmReject = async () => {
665   - // 严格校验回退原因(去除首尾空格)
666   - const rejectReasonTrim = rejectReason.value.trim();
667   - if (!rejectReasonTrim) {
668   - uni.showToast({title: '请填写回退原因', icon: 'none', duration: 1000});
669   - return;
670   - }
671   - // 校验当前工单有效性
672   - if (!currentRejectItem.value || !currentRejectItem.value.id) {
673   - uni.showToast({title: '工单信息异常,无法提交', icon: 'none', duration: 1000});
674   - rejectModalShow.value = false;
675   - return;
676   - }
677   - try {
678   - // 显示加载中,防止重复提交
679   - uni.showLoading({title: '提交中...', mask: true});
  672 + const reason = rejectReason.value.trim();
  673 + if (!reason) return showToast('请填写回退原因');
  674 + const item = currentRejectItem.value;
  675 + if (!item?.id) return showToast('工单信息异常');
680 676  
681   - // 构建请求参数(新增returnImgs)
  677 + uni.showLoading({title: '提交中...', mask: true});
  678 + try {
682 679 const requestData = {
683   - "returnImgs": rejectImgs.getSuccessImgUrls(), // 回退图片URL数组
684   - "workerDataId": currentRejectItem.value.id,
685   - "taskKey": currentRejectItem.value.taskKey,
686   - "taskId": currentRejectItem.value.taskId,
687   - "operateType": nextStepMap[currentRejectItem.value.taskKey].operateTypeNoPass,
  680 + "returnImgs": rejectImgs.getSuccessImgUrls(),
  681 + "workerDataId": item.id,
  682 + "taskKey": item.taskKey,
  683 + "taskId": item.taskId,
  684 + "operateType": nextStepMap[item.taskKey].operateTypeNoPass,
688 685 "agree": 1,
689   - "reason": rejectReasonTrim
  686 + "reason": reason
690 687 };
691   - // 调用回退工单接口
692   - const res = await qyUniversalApproval(requestData);
693   - uni.showToast({title: '回退成功', icon: 'success', duration: 1000});
694   -
695   - rejectModalShow.value = false;
696   - // 重新获取工单详情,刷新页面
697   - // await DetailQuery(taskId.value);
698   - uni.reLaunch({
699   - url: `/pages-sub/problem/regional-order-manage/index`
700   - });
701   - } catch (error) {
  688 + await callApprovalApi(requestData, item.taskKey);
  689 + showToast('回退成功', 'success');
  690 + handleRejectModalCancel();
  691 + eventChannel.value.emit('needRefresh');
  692 + uni.navigateBack({ delta: 1 });
  693 + } catch (error: any) {
702 694 console.error('回退工单失败:', error);
703   - uni.showToast({title: '网络异常,回退失败', icon: 'none', duration: 1000});
  695 + showToast(error.msg || error.message || '回退失败,请重试');
704 696 } finally {
705   - // 隐藏加载中
706 697 uni.hideLoading();
707 698 }
708 699 };
709 700  
710   -// ========== handleProcess 处理工单 ==========
711   -const handleProcess = async (item) => {
712   - console.log(nextStepMap[item.taskKey].name)
  701 +// ========== 处理工单核心方法 完整对齐列表页+全BUG修复 ==========
  702 +const handleProcess = async (item: any) => {
  703 + if (!item) return showToast('工单信息异常');
  704 + const stepName = nextStepMap[item.taskKey]?.name;
  705 + if (!stepName) return showToast('暂无处理权限或工单状态异常');
  706 +
  707 + uni.showLoading({title: '处理中...', mask: true});
713 708 try {
714   - if (nextStepMap[item.taskKey]?.name == '大区经理分配') {
715   - // ① 生成唯一临时key(统一规则,避免冲突)
716   - const tempKey = `distribute_order_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
717   - // ② 存储完整item到本地缓存(同步存储,确保立即生效)
718   - try {
719   - uni.setStorageSync(tempKey, item);
720   - } catch (error) {
721   - console.error('存储分配工单数据失败:', error);
722   - uni.showToast({title: '数据存储异常,无法跳转', icon: 'none'});
723   - return;
724   - }
725   - // ③ URL仅传递临时key,无其他冗余参数
726   - uni.navigateTo({
727   - url: `/pages-sub/problem/regional-order-manage/distribution-order?tempKey=${tempKey}`
728   - })
  709 + // 大区经理分配
  710 + if (stepName === '大区经理分配') {
  711 + const tempKey = setOrderStorage(item, 'distribute_order');
  712 + tempKey && navToOrderPage('/pages-sub/problem/regional-order-manage/distribution-order', { tempKey });
729 713 }
730   - if (nextStepMap[item.taskKey]?.name == '养护员待实施') {
731   - // ① 生成唯一临时key(和重新提交工单逻辑一致,避免冲突)
732   - const tempKey = `maintain_order_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
733   -
734   - // ② 存储完整item到本地缓存(同步存储,确保立即生效)
735   - try {
736   - uni.setStorageSync(tempKey, item);
737   - } catch (error) {
738   - console.error('存储养护工单数据失败:', error);
739   - uni.showToast({title: '数据存储异常,无法跳转', icon: 'none'});
740   - return;
741   - }
742   -
743   - // ③ URL仅传递临时key(可选:携带必要简单参数,方便目标页面快速使用)
744   - uni.navigateTo({
745   - url: `/pages-sub/problem/regional-order-manage/add-maintain-order?tempKey=${tempKey}`
746   - })
  714 + // 养护员待实施
  715 + else if (stepName === '养护员待实施') {
  716 + const tempKey = setOrderStorage(item, 'maintain_order');
  717 + tempKey && navToOrderPage('/pages-sub/problem/regional-order-manage/add-maintain-order', { tempKey });
747 718 }
748   - // 养护组长验收 - 打开弹窗
749   - if (nextStepMap[item.taskKey]?.name == '养护组长验收') {
750   - currentAcceptItem.value = item; // 存储当前工单信息
751   - acceptReason.value = ''; // 清空上次的验收原因
752   - acceptRadioValue.value = '0'; // 重置默认选中“通过”
753   - acceptModalShow.value = true; // 显示验收弹窗
  719 + // 督察员单子大区经理分配
  720 + else if (stepName === '督察员单子大区经理分配') {
  721 + const postData = {
  722 + taskKey: item.taskKey, taskId: item.taskId, operateType:60, workerDataId:item.id,
  723 + agree:0, reason:item.remark, roadId:item.roadId, roadName:item.roadName,
  724 + pressingType:item.pressingType, orderName:item.orderName,
  725 + expectedFinishDate: item.expectedFinishDate, busiLine:item.busiLine
  726 + };
  727 + await dcyUniversalApproval(postData);
  728 + showToast('分配成功', 'success');
  729 + eventChannel.value.emit('needRefresh');
  730 + uni.navigateBack({ delta: 1 });
754 731 }
755   - // 巡查员验收 - 打开弹窗
756   - if (nextStepMap[item.taskKey]?.name == '巡查员验收') {
757   - currentAcceptItem.value = item; // 存储当前工单信息
758   - acceptReason.value = ''; // 清空上次的验收原因
759   - acceptRadioValue.value = '0'; // 重置默认选中“通过”
760   - acceptModalShow.value = true; // 显示验收弹窗
  732 + // 验收弹窗
  733 + else if (['养护组长验收', '巡查员验收'].includes(stepName)) {
  734 + currentAcceptItem.value = item;
  735 + acceptReason.value = '';
  736 + acceptRadioValue.value = '0';
  737 + acceptModalShow.value = true;
761 738 }
762   -
763   - // 发起人确认
764   - if (nextStepMap[item.taskKey]?.name == '发起人确认') {
765   - console.log(item)
  739 + // 发起人确认-结束工单
  740 + else if (stepName === '发起人确认') {
  741 + uni.hideLoading();
766 742 uni.showModal({
767 743 title: "结束工单",
768 744 content: "请确定是否结束工单?",
769   - success: async function (res) {
  745 + success: async (res) => {
770 746 if (res.confirm) {
771   - // 构建请求参数
772 747 const requestData = {
773   - "returnImgs": rejectImgs.getSuccessImgUrls(), // 改造后:获取上传成功的图片URL
774   - "workerDataId": item.id,
775   - "taskKey":'ylInspectorStart',
776   - "taskId": item.taskId,
777   - "operateType": 200,
778   - "agree": 1,
779   - "reason": '结束工单'
  748 + workerDataId: item.id, taskKey: 'ylInspectorStart', taskId: item.taskId,
  749 + operateType: 200, agree: 1, reason: '结束工单'
780 750 };
781   - // 调用回退工单接口
782   - const res = await qyUniversalApproval(requestData);
783   - uni.showToast({title: '结束成功', icon: 'success', duration: 1000});
784   - rejectModalShow.value = false;
785   - paging.value?.reload(); // 刷新列表
786   - } else if (res.cancel) {
787   - console.log("用户点击取消");
  751 + await callApprovalApi(requestData, item.taskKey);
  752 + showToast('结束成功', 'success');
  753 + eventChannel.value.emit('needRefresh');
  754 + uni.navigateBack({ delta: 1 });
788 755 }
789 756 },
790 757 });
791 758 }
792   - } catch (error) {
  759 + } catch (error: any) {
793 760 console.error('处理工单失败:', error);
794   - uni.showToast({title: '处理失败,请重试', icon: 'none'});
  761 + showToast(error.msg || error.message || '处理失败,请重试');
  762 + } finally {
  763 + uni.hideLoading();
795 764 }
796 765 };
797 766  
798   -// ========== 验收弹窗取消按钮(新增清空图片) ==========
  767 +// ========== 验收弹窗取消 ==========
799 768 const handleAcceptModalCancel = () => {
800 769 acceptModalShow.value = false;
801 770 acceptReason.value = '';
802 771 acceptRadioValue.value = '0';
803   - acceptImgs.clearImgs(); // 清空验收图片
  772 + acceptImgs.clearImgs();
804 773 };
805 774  
806   -// ========== 验收弹窗确认按钮(新增returnImgs传参) ==========
  775 +// ========== 验收弹窗确认 修复BUG+对齐列表页 ==========
807 776 const handleAcceptModalConfirm = async () => {
808   - // 1. 校验验收原因是否为空
809   - if (!acceptReason.value.trim()) {
810   - uni.showToast({title: '请填写验收原因', icon: 'none', duration: 1000});
811   - return;
812   - }
813   - // 2. 校验验收原因长度
814   - if (acceptReason.value.length > 200) {
815   - uni.showToast({title: '验收原因最多200字', icon: 'none', duration: 1000});
816   - return;
817   - }
  777 + const reason = acceptReason.value.trim();
  778 + if (!reason) return showToast('请填写验收原因');
  779 + if (reason.length > 200) return showToast('验收原因最多200字');
  780 +
  781 + const item = currentAcceptItem.value;
  782 + if (!item?.id) return showToast('工单信息异常');
  783 +
  784 + uni.showLoading({title: '提交中...', mask: true});
818 785 try {
819   - // 3. 调用验收接口
820   - console.log(currentAcceptItem.value)
821 786 let postData: any = {}
822   - if (currentAcceptItem.value?.taskKey == 'ylTeamLeaderConfirm') { // 养护组长验收
  787 + if (item?.taskKey == 'ylTeamLeaderConfirm') {
823 788 postData = {
824   - "returnImgs": acceptImgs.getSuccessImgUrls(), // 验收图片URL数组
825   - "taskKey": currentAcceptItem.value.taskKey, // ylTeamLeaderConfirm
826   - "workerDataId": currentAcceptItem.value.id,
827   - "taskId": currentAcceptItem.value.taskId,
828   - "operateType": acceptRadioValue.value == 0 ? nextStepMap[currentAcceptItem.value.taskKey].operateTypePass : nextStepMap[currentAcceptItem.value.taskKey].operateTypeNoPass,
829   - "reason": acceptReason.value.trim()
  789 + returnImgs: acceptImgs.getSuccessImgUrls(),
  790 + taskKey: item.taskKey,
  791 + workerDataId: item.id,
  792 + taskId: item.taskId,
  793 + operateType: acceptRadioValue.value == 0 ? nextStepMap[item.taskKey].operateTypePass : nextStepMap[item.taskKey].operateTypeNoPass,
  794 + reason
830 795 }
831 796 }
832   - if (currentAcceptItem.value?.taskKey == 'ylInspector') { // 巡查员验收
  797 + if (item?.taskKey == 'ylInspector') {
833 798 postData = {
834   - "returnImgs": acceptImgs.getSuccessImgUrls(), // 验收图片URL数组
835   - "taskKey": currentAcceptItem.value.taskKey, //ylInspector
836   - "taskId": currentAcceptItem.value.taskId,
837   - "workerDataId": currentAcceptItem.value.id,
838   - "operateType": acceptRadioValue.value == 0 ? nextStepMap[currentAcceptItem.value.taskKey].operateTypePass : nextStepMap[currentAcceptItem.value.taskKey].operateTypeNoPass,
839   - "reason": acceptReason.value.trim(),
840   - "agree": acceptRadioValue.value
  799 + returnImgs: acceptImgs.getSuccessImgUrls(),
  800 + taskKey: item.taskKey,
  801 + taskId: item.taskId,
  802 + workerDataId: item.id,
  803 + operateType: acceptRadioValue.value == 0 ? nextStepMap[item.taskKey].operateTypePass : nextStepMap[item.taskKey].operateTypeNoPass,
  804 + reason,
  805 + agree: acceptRadioValue.value
841 806 }
842 807 }
843   - const acceptRes = await daquUniversalApproval(postData);
844   - // 4. 操作成功处理
845   -
846   - handleAcceptModalCancel(); // 清空状态
847   - // 重新获取工单详情,刷新页面
848   - // await DetailQuery(taskId.value);
849   - // uni.reLaunch({
850   - // url: `/pages-sub/problem/work-order-manage/index`
851   - // });
852   - eventChannel.emit('needRefresh');
853   - // 4. 返回列表页
  808 + await callApprovalApi(postData, item.taskKey);
  809 + showToast('提交成功', 'success');
  810 + handleAcceptModalCancel();
  811 + eventChannel.vlue.emit('needRefresh');
854 812 uni.navigateBack({delta: 1});
855   - uni.showToast({title: '提交成功', icon: 'success', duration: 1000});
856   - } catch (error) {
857   - // 5. 操作失败处理
  813 + } catch (error: any) {
858 814 console.error('验收失败:', error);
859   - uni.showToast({title: '验收提交失败,请重试', icon: 'none', duration: 1000});
  815 + showToast(error.msg || error.message || '验收提交失败,请重试');
  816 + } finally {
  817 + uni.hideLoading();
860 818 }
861 819 };
862 820  
  821 +// ========== 生命周期 ==========
863 822 onLoad((options: any) => {
864 823 console.log('页面入参:', options)
865 824 const {taskId: taskIdOpt, activeTab: activeTabOpt, processInstanceId: processInstanceIdOpt} = options;
866   - // 0-待办 1-我发起的 2-已办
867 825 taskId.value = taskIdOpt || '';
868 826 activeTab.value = activeTabOpt || '0';
869 827 processInstanceId.value = processInstanceIdOpt;
  828 + eventChannel.value = getCurrentPages()[getCurrentPages().length - 1].getOpenerEventChannel();
870 829 DetailQuery(taskId.value);
871 830 });
872 831  
873   -onShow(() => {
874   - // 注释原有逻辑,避免重复加载
875   -});
  832 +onShow(() => {});
876 833 </script>
877 834  
878 835 <style scoped lang="scss">
... ... @@ -888,12 +845,10 @@ onShow(() =&gt; {
888 845  
889 846 // 顶部Tabs
890 847 .top-tabs {
891   - //background-color: #fff;
892 848 }
893 849  
894 850 // Tab内容区
895 851 .tab-content {
896   - //padding: 16rpx;
897 852 }
898 853  
899 854 // 工单详情内容
... ... @@ -904,7 +859,6 @@ onShow(() =&gt; {
904 859 }
905 860  
906 861 .cell-content-wrap {
907   - // 保持原有样式,兼容up-album布局
908 862 }
909 863  
910 864 .empty-text {
... ... @@ -916,21 +870,12 @@ onShow(() =&gt; {
916 870 // 图片分类Tabs区块
917 871 .img-tabs-block {
918 872 background-color: #fff;
919   - //border-radius: 12rpx;
920   - //padding: 16rpx;
921 873  
922   - // 图片内容区
923 874 .img-tab-content {
924 875 padding: 20rpx 15px ;
925 876 text-align: center;
926   - //min-height: 120rpx;
927   - //display: flex;
928   - //align-items: center;
929   - //justify-content: center;
930   - //
931   - .img-album {
932   - //width: 100%;
933 877  
  878 + .img-album {
934 879 }
935 880  
936 881 .empty-img-text {
... ... @@ -942,33 +887,25 @@ onShow(() =&gt; {
942 887 }
943 888 }
944 889  
945   -// 流程节点区域(完整样式,确保内容可见)
  890 +// 流程节点区域
946 891 .process-content {
947   - //padding: 16rpx;
948   - //min-height: 400rpx;
949   -
950 892 .empty-process {
951 893 margin-top: 100rpx;
952 894 }
953 895  
954   - // 竖向步骤条容器样式
955 896 .vertical-steps {
956   - //width: 100%;
957 897 background-color: #fff;
958 898 padding: 20rpx;
959 899 border-radius: 12rpx;
960 900 display: flex;
961 901 flex-direction: column;
962   - gap: 20rpx; // 步骤项间距
  902 + gap: 20rpx;
963 903 }
964 904  
965   - // 自定义内容容器样式
966 905 .step-content-wrap {
967 906 width: 100%;
968   - //padding: 10rpx 0;
969 907 box-sizing: border-box;
970 908  
971   - // 节点标题 + 操作人样式
972 909 .step-title {
973 910 font-size: 30rpx;
974 911 font-weight: 600;
... ... @@ -983,7 +920,6 @@ onShow(() =&gt; {
983 920 }
984 921 }
985 922  
986   - // 描述(时间 + 处理说明)样式
987 923 .step-desc {
988 924 font-size: 24rpx;
989 925 color: #666;
... ... @@ -998,16 +934,14 @@ onShow(() =&gt; {
998 934 }
999 935  
1000 936 .reason-line {
1001   - word-break: break-all; // 长文本自动换行
  937 + word-break: break-all;
1002 938 }
1003 939 }
1004 940  
1005   - // 相册容器样式
1006 941 .step-album-wrap {
1007 942 padding: 10rpx 0;
1008 943  
1009 944 .step-album {
1010   - //width: 100%;
1011 945 }
1012 946  
1013 947 .no-img-tip {
... ... @@ -1067,11 +1001,9 @@ onShow(() =&gt; {
1067 1001 .mt-30 {
1068 1002 margin-top: 30rpx;
1069 1003 }
1070   -// 针对 up-album 单图容器的样式(穿透 scoped 限制)
  1004 +// 针对 up-album 单图容器的样式
1071 1005 :deep .u-album__row__wrapper image {
1072   - width: 70px !important; // 与多图保持一致
  1006 + width: 70px !important;
1073 1007 height: 70px !important;
1074 1008 }
1075   -
1076   -
1077 1009 </style>
1078 1010 \ No newline at end of file
... ...
pages.json
... ... @@ -17,14 +17,16 @@
17 17 {
18 18 "path": "pages/index/index",
19 19 "style": {
20   - "navigationBarTitleText": "首页"
  20 + "navigationBarTitleText": "首页",
  21 + "navigationStyle": "custom"
21 22 }
22 23 },
23 24  
24 25 {
25 26 "path": "pages/mine/index",
26 27 "style": {
27   - "navigationBarTitleText": "我的"
  28 + "navigationBarTitleText": "我的",
  29 + "navigationStyle": "custom"
28 30 }
29 31 }
30 32 ],
... ... @@ -209,10 +211,16 @@
209 211 },
210 212 {
211 213 "path": "tree-archive/editTree",
212   - "style": { "navigationBarTitleText": "编辑行道树" }
  214 + "style": { "navigationBarTitleText": "树木信息" }
  215 + },
  216 + {
  217 + "path": "tree-archive/logDetail",
  218 + "style": { "navigationBarTitleText": "树木详情" }
213 219 }
214 220  
215 221  
  222 +
  223 +
216 224 ]
217 225 }
218 226 ],
... ...
pages/index/index.vue
1 1 <template>
2   - <view class="content">
3   -<!-- <image class="logo" src="/static/logo.png"></image>-->
4   - <view class="text-area">
5   - <text class="title">{{title}}</text>
6   - </view>
7   - </view>
  2 + <view class="home-page">
  3 + <!-- 用户信息栏 -->
  4 + <view class="user-info-bar">
  5 + <view class="user-info">
  6 + <image class="avatar" src="../../static/imgs/default-avatar.png" mode="widthFix"></image>
  7 + <view class="user-text">
  8 + <view class="username">林晓明</view>
  9 + <view class="login-time">上次登录1天前</view>
  10 + </view>
  11 + </view>
  12 + <view class="msg-icon">
  13 + <u-icon name="message" color="#fff" size="32rpx" />
  14 + <view class="msg-badge">5</view>
  15 + </view>
  16 + </view>
  17 +
  18 + <!-- 任务完成情况卡片 -->
  19 + <view class="task-card">
  20 + <view class="card-title">任务完成情况</view>
  21 + <view class="card-header">
  22 + <view class="unit">单位: 个</view>
  23 + <!-- ✅ 核心修改:日期范围改为可点击的选择器 -->
  24 + <view class="date-range" @click="openDatePicker">
  25 +<!-- {{ dateText }}-->
  26 + <neo-datetime-pro
  27 + v-model="dateRange"
  28 + type="daterange"
  29 + placeholder="请选择日期范围"
  30 + />
  31 + </view>
  32 + </view>
  33 + <!-- 折线图 -->
  34 + <view class="chart-wrap">
  35 + <!-- <u-line-chart-->
  36 + <!-- :chart-data="chartData"-->
  37 + <!-- :x-axis="xAxis"-->
  38 + <!-- :y-axis="yAxis"-->
  39 + <!-- :legend="legend"-->
  40 + <!-- height="300rpx"-->
  41 + <!-- ></u-line-chart>-->
  42 + </view>
  43 + </view>
  44 +
  45 + <!-- 待办/已办切换栏 -->
  46 + <view class="tab-bar">
  47 + <view
  48 + class="tab-item"
  49 + :class="{ active: currentTab === 'todo' }"
  50 + @click="currentTab = 'todo'"
  51 + >
  52 + 待办事项(5)
  53 + <view class="tab-active-line" v-if="currentTab === 'todo'"></view>
  54 + </view>
  55 + <view
  56 + class="tab-item"
  57 + :class="{ active: currentTab === 'done' }"
  58 + @click="currentTab = 'done'"
  59 + >
  60 + 已办事项(89)
  61 + <view class="tab-active-line" v-if="currentTab === 'done'"></view>
  62 + </view>
  63 + </view>
  64 +
  65 + <!-- 事项列表 -->
  66 + <view class="task-list">
  67 + <view class="task-item" v-for="(item, index) in currentTaskList" :key="index">
  68 + <view class="task-name">事项名称: {{ item.name }}{{ item.name }}{{ item.name }}{{ item.name }}{{ item.name }}{{ item.name }}</view>
  69 + <view class="task-desc">
  70 + <view>紧急程度: {{ item.urgency }}</view>
  71 + <view class="task-time">{{ item.time }}</view>
  72 + </view>
  73 + </view>
  74 + </view>
  75 +
  76 +
  77 + </view>
8 78 </template>
9 79  
10   -<script>
11   - export default {
12   - data() {
13   - return {
14   - title: 'Hello'
15   - }
16   - },
17   - onLoad() {
  80 +<script setup>
  81 +import { ref, reactive, watch } from 'vue';
  82 +
  83 +// 折线图数据
  84 +const chartData = reactive([
  85 + {
  86 + name: '已完成任务数',
  87 + data: [5, 35, 55, 40, 65, 50, 70],
  88 + color: '#4CAF50'
  89 + },
  90 + {
  91 + name: '待完成总任务数',
  92 + data: [5, 70, 75, 70, 75, 75, 90],
  93 + color: '#E53935'
  94 + }
  95 +]);
  96 +const xAxis = reactive(['12.28', '12.29', '12.30', '12.31', '01.01', '01.02', '01.03']);
  97 +const yAxis = reactive([0, 25, 50, 75, 100]);
  98 +const legend = reactive(['已完成任务数', '待完成总任务数']);
  99 +
  100 +// 标签切换
  101 +const currentTab = ref('todo');
  102 +const dateRange=ref( [])
  103 +// ✅ 核心新增:时间选择器相关数据
  104 +const showDatePicker = ref(false);
  105 +// 默认显示的时间文本
  106 +const dateText = ref('2025/12/28—2026/01/04');
  107 +// 选中的开始/结束时间
  108 +const selectStartDate = ref('');
  109 +const selectEndDate = ref('');
  110 +
  111 +// 打开时间选择器
  112 +const openDatePicker = () => {
  113 + showDatePicker.value = true;
  114 +};
  115 +
  116 +// 确认选择时间
  117 +const confirmDate = (e) => {
  118 + // 格式化选中的时间
  119 + selectStartDate.value = formatDate(e.value[0]);
  120 + selectEndDate.value = formatDate(e.value[1]);
  121 + dateText.value = `${selectStartDate.value}—${selectEndDate.value}`;
  122 + showDatePicker.value = false;
  123 + // 这里可以加:时间改变后重新请求图表数据的逻辑
  124 + // getChartData(selectStartDate.value, selectEndDate.value)
  125 +};
18 126  
19   - },
20   - methods: {
  127 +// 时间格式化函数:YYYY/MM/DD 格式
  128 +const formatDate = (dateVal) => {
  129 + const year = dateVal.getFullYear();
  130 + const month = (dateVal.getMonth() + 1).toString().padStart(2, '0');
  131 + const day = dateVal.getDate().toString().padStart(2, '0');
  132 + return `${year}/${month}/${day}`;
  133 +};
21 134  
22   - }
23   - }
  135 +// 待办事项数据
  136 +const todoList = reactive([
  137 + {
  138 + name: '绿地卫生验收',
  139 + urgency: '特急',
  140 + time: '2025-12-31 15:45:23'
  141 + },
  142 + {
  143 + name: '阜成门内大街年度计划巡检',
  144 + urgency: '--',
  145 + time: '2025-12-31 09:02:00'
  146 + },
  147 + {
  148 + name: '金融街年度计划巡检',
  149 + urgency: '--',
  150 + time: '2025-12-31 09:02:00'
  151 + },
  152 + {
  153 + name: '金融街年度计划巡检',
  154 + urgency: '--',
  155 + time: '2025-12-31 09:02:00'
  156 + },
  157 + {
  158 + name: '示例事项',
  159 + urgency: '一般',
  160 + time: '2026-01-04 10:00:00'
  161 + }
  162 +]);
  163 +
  164 +// 已办事项数据(示例)
  165 +const doneList = reactive([
  166 + {
  167 + name: '道路清洁验收',
  168 + urgency: '普通',
  169 + time: '2025-12-30 14:20:00'
  170 + }
  171 +]);
  172 +
  173 +// 当前显示的事项列表
  174 +const currentTaskList = ref(todoList);
  175 +// 监听标签切换
  176 +watch(currentTab, (newVal) => {
  177 + currentTaskList.value = newVal === 'todo' ? todoList : doneList;
  178 +});
24 179 </script>
25 180  
26   -<style>
27   - .content {
28   - display: flex;
29   - flex-direction: column;
30   - align-items: center;
31   - justify-content: center;
32   - }
33   -
34   - .logo {
35   - height: 200rpx;
36   - width: 200rpx;
37   - margin-top: 200rpx;
38   - margin-left: auto;
39   - margin-right: auto;
40   - margin-bottom: 50rpx;
41   - }
42   -
43   - .text-area {
44   - display: flex;
45   - justify-content: center;
46   - }
47   -
48   - .title {
49   - font-size: 36rpx;
50   - color: #8f8f94;
51   - }
52   -</style>
  181 +<style scoped lang="scss">
  182 +.home-page {
  183 + background-color: #f5f7fa;
  184 + min-height: 100vh;
  185 +}
  186 +
  187 +/* 用户信息栏 */
  188 +.user-info-bar {
  189 + background-color: #1677ff;
  190 + padding: 180rpx 30rpx 120rpx;
  191 + display: flex;
  192 + justify-content: space-between;
  193 + align-items: center;
  194 + color: #fff;
  195 + z-index: 1;
  196 + position: relative;
  197 +
  198 + .user-info {
  199 + display: flex;
  200 + align-items: center;
  201 +
  202 + .avatar {
  203 + width: 80rpx;
  204 + height: 80rpx;
  205 + border-radius: 50%;
  206 + margin-right: 20rpx;
  207 + }
  208 +
  209 + .username {
  210 + font-size: 32rpx;
  211 + font-weight: 500;
  212 + }
  213 +
  214 + .login-time {
  215 + font-size: 24rpx;
  216 + opacity: 0.8;
  217 + }
  218 + }
  219 +
  220 + .msg-icon {
  221 + position: relative;
  222 +
  223 + .msg-badge {
  224 + position: absolute;
  225 + top: -10rpx;
  226 + right: -10rpx;
  227 + background-color: #ff3d00;
  228 + color: #fff;
  229 + font-size: 20rpx;
  230 + width: 28rpx;
  231 + height: 28rpx;
  232 + border-radius: 50%;
  233 + display: flex;
  234 + align-items: center;
  235 + justify-content: center;
  236 + }
  237 + }
  238 +}
  239 +
  240 +/* 任务完成情况卡片 */
  241 +.task-card {
  242 + background-color: #fff;
  243 + margin: 0 30rpx;
  244 + margin-top: -60rpx;
  245 + border-radius: 16rpx 16rpx 0 0;
  246 + padding: 20rpx;
  247 + box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
  248 + position: relative;
  249 + z-index: 2;
  250 +
  251 + .card-title {
  252 + font-size: 30rpx;
  253 + font-weight: 600;
  254 + margin-bottom: 15rpx;
  255 + }
  256 +
  257 + .card-header {
  258 + display: flex;
  259 + justify-content: space-between;
  260 + font-size: 24rpx;
  261 + color: #999;
  262 + margin-bottom: 10rpx;
  263 + align-items: center;
  264 + }
  265 +
  266 + // ✅ 日期选择器样式
  267 + .date-range {
  268 + display: flex;
  269 + align-items: center;
  270 + gap: 6rpx;
  271 + padding: 4rpx 8rpx;
  272 + border-radius: 6rpx;
  273 + &:active {
  274 + background-color: #f5f5f5;
  275 + }
  276 + }
  277 + .date-icon {
  278 + margin-top: 2rpx;
  279 + }
  280 +
  281 + .chart-wrap {
  282 + width: 100%;
  283 + height: 300rpx;
  284 + }
  285 +}
  286 +
  287 +/* 待办/已办切换栏 */
  288 +.tab-bar {
  289 + display: flex;
  290 + background-color: #fff;
  291 + margin: 0 30rpx;
  292 + border-radius: 0;
  293 + overflow: hidden;
  294 +
  295 + .tab-item {
  296 + flex: 1;
  297 + text-align: center;
  298 + padding: 20rpx 0;
  299 + font-size: 28rpx;
  300 + color: #666;
  301 + position: relative;
  302 +
  303 + &.active {
  304 + color: #1677ff;
  305 + font-weight: 500;
  306 + }
  307 +
  308 + .tab-active-line {
  309 + position: absolute;
  310 + bottom: 0;
  311 + left: 0;
  312 + width: 100%;
  313 + height: 4rpx;
  314 + background-color: #1677ff;
  315 + }
  316 + }
  317 +}
  318 +
  319 +/* 事项列表 */
  320 +.task-list {
  321 + background-color: #fff;
  322 + margin: 0 30rpx ;
  323 + border-radius: 0 0 16rpx 16rpx;
  324 + padding: 10rpx 20rpx;
  325 + box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
  326 +
  327 + .task-item {
  328 + padding: 20rpx 0;
  329 + border-bottom: 1px solid #f5f5f5;
  330 +
  331 + &:last-child {
  332 + border-bottom: none;
  333 + }
  334 +
  335 + .task-name {
  336 + font-size: 28rpx;
  337 + color: #333;
  338 + margin-bottom: 10rpx;
  339 + // ✅ 修复超长文字溢出:单行显示+超出省略
  340 + white-space: nowrap;
  341 + overflow: hidden;
  342 + text-overflow: ellipsis;
  343 + }
  344 +
  345 + .task-desc {
  346 + display: flex;
  347 + justify-content: space-between;
  348 + font-size: 24rpx;
  349 + color: #666;
  350 +
  351 + .task-time {
  352 + color: #999;
  353 + }
  354 + }
  355 + }
  356 +}
  357 +</style>
53 358 \ No newline at end of file
... ...
pages/login/index.vue
... ... @@ -103,7 +103,7 @@ const loginFormRules = reactive({
103 103 // 密码校验:必填 + 长度限制 6-20位(行业通用密码长度,可自行调整)
104 104 password: [
105 105 { type: 'string', required: true, message: '请输入登录密码', trigger: ['change', 'blur'] },
106   - { type: 'string', min: 6, max: 20, message: '密码长度为6-20个字符', trigger: ['change', 'blur'] }
  106 + { type: 'string', min: 3, max: 20, message: '密码长度为3-20个字符', trigger: ['change', 'blur'] }
107 107 ]
108 108 });
109 109  
... ... @@ -189,17 +189,18 @@ const handleLogin = async () =&gt; {
189 189  
190 190 // 顶部:从左到右 由浅到深 线性渐变 过渡自然柔和 无突兀色块
191 191 .top-title {
192   - background: linear-gradient(120deg, #4299e1 0%, #2970e8 50%, #2563eb 100%);
  192 + //background: linear-gradient(120deg, #4299e1 0%, #2970e8 50%, #2563eb 100%);
193 193 color: #fff;
194   - padding: 240rpx 30rpx 80rpx;
  194 + padding: 220rpx 30rpx 200rpx;
195 195 text-align: left;
196   - border-bottom-left-radius: 120rpx;
  196 + //border-bottom-left-radius: 120rpx;
197 197 position: relative;
198 198 z-index: 1;
199   - box-shadow: 0 25rpx 40rpx -15rpx rgba(37, 99, 235, 0.3);
200   -
  199 + //box-shadow: 0 25rpx 40rpx -15rpx rgba(37, 99, 235, 0.3);
  200 + background: url("https://img.jichengshanshui.com.cn:28207/appimg/bg.jpg") no-repeat;
  201 + background-size: 100% 100%;
201 202 .welcome-text {
202   - font-size: 46rpx;
  203 + font-size: 23px;
203 204 display: block;
204 205 margin-bottom: 10px;
205 206 font-weight: 500;
... ... @@ -207,7 +208,7 @@ const handleLogin = async () =&gt; {
207 208  
208 209 .platform-name {
209 210 margin-bottom: 10px;
210   - font-size: 46rpx;
  211 + font-size: 23px;
211 212 display: block;
212 213 opacity: 0.95;
213 214 }
... ... @@ -215,46 +216,48 @@ const handleLogin = async () =&gt; {
215 216  
216 217 .login-form {
217 218 background-color: #fff;
218   - padding: 40rpx 30rpx;
219   - border-radius: 16rpx;
220   - box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
221   - margin: -60rpx 40rpx 0;
  219 + padding: 20px 15px;
  220 + border-radius: 8px;
  221 + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
  222 + margin: -90px 20px 0;
222 223 position: relative;
223 224 z-index: 10;
224 225 box-sizing: border-box;
225 226  
226 227 .login-title {
227   - font-size: 38rpx;
  228 + font-size: 19px;
228 229 font-weight: 600;
229 230 color: #666;
230   - margin-bottom: 30rpx;
231   - letter-spacing: 1rpx;
  231 + margin-bottom: 15px;
  232 + letter-spacing: 1px;
232 233 }
233 234 }
234 235  
235 236 // 适配表单校验的间距
236 237 :deep(.u-form-item) {
237   - margin-bottom: 20rpx;
  238 + margin-bottom: 10px;
238 239 position: relative;
239 240 }
240 241  
241   -// 登录按钮:和顶部完全同款 从左到右 由浅到深 线性渐变 + 保留所有原尺寸
  242 +// 登录按钮
242 243 .login-btn {
243   - margin-top: 40rpx;
  244 + margin-top: 20px;
244 245 width: 100%;
245   - height: 88rpx;
246   - line-height: 88rpx;
247   - border-radius: 8rpx;
248   - font-size: 32rpx;
249   - background: linear-gradient(120deg, #4299e1 0%, #2970e8 50%, #2563eb 100%) !important;
250   - border: none !important;
251   - &::after { border: none !important; }
  246 + height: 44px;
  247 + line-height: 44px;
  248 + border-radius: 4px;
  249 + font-size: 16px;
  250 +
  251 + background: #0A86F4;
  252 + box-shadow: 0px 4px 6px 1px rgba(25,94,215,0.5);
  253 + border-radius: 23px;
252 254 }
253 255  
  256 +
254 257 .copyright {
255 258 width: 100%;
256 259 text-align: center;
257   - font-size: 24rpx;
  260 + font-size: 12px;
258 261 color: #999;
259 262 position: fixed;
260 263 bottom: 100px;
... ...
pages/mine/index.vue
1 1 <template>
2 2 <view class="user-center-page">
3 3 <!-- 顶部用户信息栏 -->
4   - <view class="user-info-header">
5   - <!-- 渐变背景层 -->
6   - <view class="header-bg"></view>
7   - <!-- 用户信息主体 -->
8   - <view class="user-info-wrap">
9   - <!-- 头像 -->
10   - <up-avatar
11   - :src="userStore.isLogin ? userInfo.avatar : '/static/imgs/default-avatar.png'"
12   - size="100rpx"
13   - shape="circle"
14   - class="user-avatar"
15   - ></up-avatar>
16   -
17   - <!-- 用户名+手机号 -->
18   - <view class="user-info-content">
19   - <view class="user-name">{{ userStore.isLogin ? userInfo.username : '未登录' }}</view>
20   - <view class="user-phone">{{ userStore.isLogin ? userInfo.nickname : '--------' }}</view>
21   - </view>
22   -
23   - <!-- 登录入口 -->
24   - <up-button
25   - v-if="!userStore.isLogin"
26   - type="primary"
27   - size="mini"
28   - @click="handleLogin"
29   - >
30   - 立即登录
31   - </up-button>
  4 + <view class="header-bg">
  5 + <up-avatar
  6 + :src="'https://img.jichengshanshui.com.cn:28207/appimg/default-avatar.png'"
  7 + size="100rpx"
  8 + shape="square"
  9 + class="user-avatar"
  10 + ></up-avatar>
  11 +
  12 + <!-- 用户名+手机号 -->
  13 + <view class="user-info-content">
  14 + <view class="user-name">{{ userStore.isLogin ? userInfo.username : '未登录' }}</view>
  15 + <view class="user-phone">{{ userStore.isLogin ? userInfo.nickname : '--------' }}</view>
32 16 </view>
  17 +
33 18 </view>
34 19  
  20 +
  21 + <view class="mine-content">
  22 + <up-cell-group title="个人信息">
  23 + <up-cell title="姓名" :value="userInfo.nickname"></up-cell>
  24 + <up-cell title="账号" :value="userInfo.username"></up-cell>
  25 + <up-cell title="电话" :value="userInfo.username"></up-cell>
  26 + <up-cell title="角色" :value="translateRoles(userStore.userInfo.roles)"></up-cell>
  27 + </up-cell-group>
  28 + </view>
  29 +
  30 +
  31 + <up-button
  32 + v-if="!userStore.isLogin"
  33 + type="primary"
  34 + size="mini"
  35 + shape="circle"
  36 + @click="handleLogin"
  37 + class="login-btn"
  38 + >
  39 + 立即登录
  40 + </up-button>
  41 +
35 42 <!-- 退出登录按钮 (仅登录状态显示) -->
36 43 <view v-if="userStore.isLogin" class="logout-btn-wrap">
37 44 <up-button
38 45 type="primary"
39 46 size="large"
  47 + shape="circle"
40 48 @click="confirmLogout"
  49 + class="login-btn"
41 50 >
42 51 退出登录
43 52 </up-button>
... ... @@ -49,6 +58,7 @@
49 58 import { computed } from 'vue'
50 59 import { useUserStore } from '@/pinia/user'
51 60 import { showModal, onShow } from '@dcloudio/uni-app'
  61 +import {translateRoles} from '@/common/utils/common'
52 62 // 初始化Pinia仓库
53 63 const userStore = useUserStore()
54 64  
... ... @@ -68,9 +78,7 @@ const handleLogin = () =&gt; {
68 78 * 确认退出登录(带二次确认)
69 79 */
70 80 const confirmLogout = async () => {
71   - console.log('13213')
72 81 try {
73   - console.log('434')
74 82 const res = await uni.showModal({
75 83 title: '提示',
76 84 content: '确定要退出登录吗?',
... ... @@ -105,64 +113,64 @@ export default {
105 113  
106 114 <style lang="scss" scoped>
107 115 .user-center-page {
108   - min-height: 100vh;
109   - background-color: #f5f7fa;
  116 +
110 117 }
111 118  
112   -// 顶部用户信息栏
113   -.user-info-header {
114   - position: relative;
115   - padding-bottom: 40rpx;
116   -
117   - .header-bg {
118   - position: absolute;
119   - top: 0;
120   - left: 0;
121   - width: 100%;
122   - height: 320rpx;
123   - background: linear-gradient(135deg, #409eff, #67c23a);
124   - border-radius: 0 0 20rpx 20rpx;
125   - z-index: 1;
126   - }
  119 +.header-bg {
  120 + position:relative;
  121 + top: 0;
  122 + left: 0;
  123 + width: 100%;
  124 + height: 230px;
  125 + //height: 120px;
  126 + background: url("https://img.jichengshanshui.com.cn:28207/appimg/bg.jpg") no-repeat;
  127 + background-size: 100% 100%;
  128 + z-index: -1;
  129 + display: flex;
  130 + padding: 80px 0 0 20px;
  131 +}
  132 +.mine-content{
  133 + margin-top: -150px;
  134 + border-radius: 30px 30px 0 0;
  135 + background: #fff;
  136 +}
127 137  
128   - .user-info-wrap {
129   - position: relative;
130   - z-index: 2;
131   - display: flex;
132   - align-items: center;
133   - padding: 60rpx 30rpx 0;
134   -
135   - .user-avatar {
136   - border: 4rpx solid #ffffff;
137   - box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
138   - transition: transform 0.2s ease;
139   - &:active {
140   - transform: scale(0.95);
141   - }
142   - }
  138 +.user-avatar {
143 139  
144   - .user-info-content {
145   - margin-left: 30rpx;
146   - flex: 1;
147   -
148   - .user-name {
149   - font-size: 32rpx;
150   - font-weight: 600;
151   - color: #ffffff;
152   - margin-bottom: 10rpx;
153   - text-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.1);
154   - }
155   -
156   - .user-phone {
157   - font-size: 26rpx;
158   - color: rgba(255, 255, 255, 0.9);
159   - }
160   - }
161   - }
162 140 }
163 141  
  142 +.user-info-content {
  143 + margin-left: 30rpx;
  144 + flex: 1;
  145 +
  146 + .user-name {
  147 + font-size: 32rpx;
  148 + font-weight: 600;
  149 + color: #ffffff;
  150 + margin-bottom: 10rpx;
  151 + text-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.1);
  152 + }
  153 +
  154 + .user-phone {
  155 + font-size: 26rpx;
  156 + color: rgba(255, 255, 255, 0.9);
  157 + }
  158 +}
164 159 // 退出登录按钮
165 160 .logout-btn-wrap {
166 161 margin: 200rpx 20rpx 20rpx;
167 162 }
  163 +// 登录按钮
  164 +.login-btn {
  165 + margin-top: 20px;
  166 + width: 100%;
  167 + height: 44px;
  168 + line-height: 44px;
  169 + border-radius: 4px;
  170 + font-size: 16px;
  171 +
  172 + background: #0A86F4;
  173 + box-shadow: 0px 4px 6px 1px rgba(25,94,215,0.5);
  174 + border-radius: 23px;
  175 +}
168 176 </style>
169 177 \ No newline at end of file
... ...
pages/workbench/index.vue
... ... @@ -9,7 +9,11 @@
9 9 ></up-loading-page>
10 10  
11 11 <!-- 蓝色装饰块 -->
12   - <view class="blue-decor-block" v-show="!loading"></view>
  12 + <view class="blue-decor-block" v-show="!loading">
  13 + <!-- ✅ 修复1:增加可选链+默认值,防止数据为undefined报错 -->
  14 + <text class="welcome-text u-line-1">你好{{userInfo?.user?.nickname || ''}},欢迎登录</text>
  15 + <text class="platform-name">全域智能运营管理平台</text>
  16 + </view>
13 17  
14 18 <!-- 内容容器 -->
15 19 <view class="content-wrap">
... ... @@ -59,7 +63,7 @@
59 63 </template>
60 64  
61 65 <script setup lang="ts">
62   -import { ref, computed } from 'vue'; // 新增 computed
  66 +import { ref, computed } from 'vue';
63 67 import { onShow } from '@dcloudio/uni-app';
64 68 import { useUserStore } from '@/pinia/user';
65 69 import globalConfig from '@/common/config/global';
... ... @@ -83,9 +87,12 @@ interface MenuItem {
83 87 children: MenuItem[];
84 88 }
85 89  
  90 +// ========== 全局变量定义区 ==========
86 91 const loading = ref(true);
87 92 const userStore = useUserStore();
88 93 const moduleList = ref<MenuItem[]>([]);
  94 +// ✅ 修复2:声明全局的用户信息变量,模板可访问 + 初始化空对象兜底,杜绝undefined
  95 +const userInfo = ref<any>(cache.get(globalConfig.cache.userInfoKey) || userStore.userInfo || {});
89 96  
90 97 // 计算属性:过滤出有子节点的父模块(children 存在且长度 > 0)
91 98 const filteredModuleList = computed(() => {
... ... @@ -98,36 +105,34 @@ const filteredModuleList = computed(() =&gt; {
98 105 onShow(async () => {
99 106 try {
100 107 loading.value = true;
  108 + // ✅ 修复3:重新赋值全局userInfo,保证数据最新
  109 + userInfo.value = cache.get(globalConfig.cache.userInfoKey) || userStore.userInfo || {};
101 110  
102   - // ========== 核心新增:登录状态判断 ==========
103   - // 1. 定义登录判断逻辑(多维度校验,确保准确性)
  111 + // 登录状态判断(多维度校验,确保准确性)
104 112 const isLogin = () => {
105   -
106 113 // 从缓存获取token(核心登录标识)
107 114 const token = cache.get(globalConfig.cache.tokenKey) || userStore.token;
108   - // 从缓存/Pinia获取用户信息
109   - const userInfo = cache.get(globalConfig.cache.userInfoKey) || userStore.userInfo;
  115 + // 从全局变量取值,无需再声明局部变量
  116 + const userInfoVal = userInfo.value;
  117 + console.log('当前用户信息:', userInfoVal?.user);
110 118 // 满足任一核心条件即视为已登录
111   - return (!!token && !!userInfo);
  119 + return !!token && !!userInfoVal;
112 120 };
113 121  
114   - // 2. 未登录处理:跳转登录页,阻止后续逻辑执行
  122 + // 未登录处理:跳转登录页,阻止后续逻辑执行
115 123 if (!isLogin()) {
116 124 uni.showToast({ title: '请先登录', icon: 'none', duration: 1500 });
117 125 // 延迟跳转,确保提示语正常显示
118 126 setTimeout(() => {
119   - // 使用reLaunch跳转,清空页面栈,避免返回当前菜单页
120 127 uni.reLaunch({
121   - url: '/pages/login/index', // 替换为你的实际登录页路径
  128 + url: '/pages/login/index',
122 129 fail: (err) => {
123 130 console.error('跳转登录页失败:', err);
124 131 uni.showToast({ title: '跳转登录页异常', icon: 'none' });
125 132 }
126 133 });
127 134 }, 1500);
128   - // 隐藏加载状态
129 135 loading.value = false;
130   - // 终止后续代码执行
131 136 return;
132 137 }
133 138  
... ... @@ -169,15 +174,33 @@ const handleMenuClick = (item: MenuItem) =&gt; {
169 174 top: 0;
170 175 left: 0;
171 176 width: 100%;
172   - height: 200px;
173   - background-color: #577ee3;
  177 + height: 280px;
  178 + background: url("https://img.jichengshanshui.com.cn:28207/appimg/bg.jpg") no-repeat;
  179 + background-size: 100% 100%;
174 180 z-index: 1;
  181 + color:#fff;
  182 + text-align: left;
  183 + padding-left: 20px;
  184 + .welcome-text {
  185 + padding-top: 80px;
  186 + font-size: 20px;
  187 + display: block;
  188 + margin-bottom: 10px;
  189 + font-weight: 500;
  190 + }
  191 +
  192 + .platform-name {
  193 + margin-bottom: 10px;
  194 + font-size: 20px;
  195 + display: block;
  196 + opacity: 0.95;
  197 + }
175 198 }
176 199  
177 200 .content-wrap {
178 201 position: relative;
179 202 z-index: 2;
180   - padding: 120px 0 0;
  203 + padding: 150px 0 0;
181 204 display: flex;
182 205 flex-direction: column;
183 206 gap: 20rpx;
... ... @@ -197,6 +220,7 @@ const handleMenuClick = (item: MenuItem) =&gt; {
197 220 display: flex;
198 221 flex-wrap: wrap;
199 222 gap: 20rpx;
  223 + //padding: 20rpx; // ✅ 优化:增加内边距,内容不贴边
200 224 }
201 225  
202 226 .content-block {
... ...
static/imgs/bg.jpg 0 → 100644

42.2 KB

static/imgs/default-avatar.png

30.1 KB | W: | H:

3.93 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin