Commit e6a0428542af9f5d8f619cfba554d8d993893ca5
1 parent
dfa5aac5
单图情况 宽高70*70
Showing
14 changed files
with
461 additions
and
134 deletions
api/user.js
| ... | ... | @@ -41,3 +41,11 @@ export const moduleList = () => { |
| 41 | 41 | export const getSimpleDictDataList = () => { |
| 42 | 42 | return get('/admin-api/system/dict-data/simple-list') |
| 43 | 43 | } |
| 44 | + | |
| 45 | +/** | |
| 46 | + * 刷新token | |
| 47 | + * @returns {Promise} | |
| 48 | + */ | |
| 49 | +export const refreshToken = (params) => { | |
| 50 | + return post('/admin-api/system/auth/refresh-token?refreshToken='+params) | |
| 51 | +} | ... | ... |
common/config/global.js
| ... | ... | @@ -23,7 +23,8 @@ export default { |
| 23 | 23 | expireTimeKey: 'jcss_token_expire', |
| 24 | 24 | userIdKey:'jcss_user_id', |
| 25 | 25 | moduleListKey:'jcss_module_list', |
| 26 | - dictDataKey:'jcss_dict_data' | |
| 26 | + dictDataKey:'jcss_dict_data', | |
| 27 | + refreshTokenKey: 'jcss_user_refresh_token', // 新增:刷新令牌缓存key | |
| 27 | 28 | }, |
| 28 | 29 | appName: 'JCSS管理系统', |
| 29 | 30 | tokenExpireTime: 7 * 24 * 60 * 60 * 1000 | ... | ... |
common/utils/common.js
| ... | ... | @@ -162,4 +162,23 @@ export const calculateFormatTimeDiff = (startTime, endTime) => { |
| 162 | 162 | |
| 163 | 163 | // 8. 拼接并返回结果 |
| 164 | 164 | return validTimeParts.join(''); |
| 165 | -}; | |
| 166 | 165 | \ No newline at end of file |
| 166 | +}; | |
| 167 | + | |
| 168 | + | |
| 169 | +/** | |
| 170 | + * 将日期数组 [年, 月, 日] 转为标准日期字符串(兼容 iOS/Android) | |
| 171 | + * @param {Array} dateArr 日期数组,如 [2025, 12, 20] | |
| 172 | + * @returns {String} 标准日期字符串,如 '2025-12-20' | |
| 173 | + */ | |
| 174 | +export const convertArrToDateStr= (dateArr)=> { | |
| 175 | + // 边界判断:非数组/长度不足3,返回空 | |
| 176 | + if (!Array.isArray(dateArr) || dateArr.length < 3) { | |
| 177 | + return dateArr; | |
| 178 | + } | |
| 179 | + // 解构年、月、日,补零处理(确保格式统一,如 12月→12,2月→02) | |
| 180 | + const [year, month, day] = dateArr; | |
| 181 | + const formatMonth = month.toString().padStart(2, '0'); | |
| 182 | + const formatDay = day.toString().padStart(2, '0'); | |
| 183 | + // 拼接为标准字符串 | |
| 184 | + return `${year}-${formatMonth}-${formatDay}`; | |
| 185 | +} | |
| 167 | 186 | \ No newline at end of file | ... | ... |
pages-sub/daily/patrol-manage/add-patrol-record.vue
| ... | ... | @@ -6,7 +6,7 @@ |
| 6 | 6 | label-position="left" |
| 7 | 7 | :model="inspectForm" |
| 8 | 8 | ref="inspectFormRef" |
| 9 | - labelWidth="190rpx" | |
| 9 | + labelWidth="200rpx" | |
| 10 | 10 | > |
| 11 | 11 | <!-- 1. 巡查描述 --> |
| 12 | 12 | <up-form-item |
| ... | ... | @@ -110,10 +110,20 @@ |
| 110 | 110 | border="none" |
| 111 | 111 | readonly |
| 112 | 112 | placeholder="点击选择时间" |
| 113 | + > | |
| 114 | + | |
| 113 | 115 | |
| 114 | - ></up-input> | |
| 116 | + </up-input> | |
| 115 | 117 | <template #right> |
| 116 | - <up-icon name="arrow-right" size="16"></up-icon> | |
| 118 | + <!-- 核心修复:用view包裹图标,添加双重@click.stop --> | |
| 119 | + <view v-if="inspectForm.expectedFinishDate" @click.stop> | |
| 120 | + <up-icon | |
| 121 | + name="close" | |
| 122 | + size="16" | |
| 123 | + @click.stop="clearExpectedFinishDate" | |
| 124 | + ></up-icon> | |
| 125 | + </view> | |
| 126 | + <up-icon name="arrow-right" size="16" v-else></up-icon> | |
| 117 | 127 | </template> |
| 118 | 128 | </up-form-item> |
| 119 | 129 | |
| ... | ... | @@ -292,6 +302,12 @@ export default { |
| 292 | 302 | this.show = false |
| 293 | 303 | this.$refs.inspectFormRef.validateField('expectedFinishDate') |
| 294 | 304 | }, |
| 305 | + // 新增:清除希望完成时间的方法 | |
| 306 | + clearExpectedFinishDate() { | |
| 307 | + this.inspectForm.expectedFinishDate = '' | |
| 308 | + // 清除后触发一次字段校验,确保校验状态更新 | |
| 309 | + this.$refs.inspectFormRef.validateField('expectedFinishDate') | |
| 310 | + }, | |
| 295 | 311 | |
| 296 | 312 | deleteImg(event) { |
| 297 | 313 | this.imagesList.splice(event.index, 1) | ... | ... |
pages-sub/daily/patrol-manage/index.vue
| ... | ... | @@ -108,6 +108,7 @@ import { timeFormat } from '@/uni_modules/uview-plus'; |
| 108 | 108 | import { ref } from 'vue'; |
| 109 | 109 | import { onLoad, onShow } from '@dcloudio/uni-app'; |
| 110 | 110 | import { inspectionPlanPage } from "@/api/patrol-manage/patrol-plan"; |
| 111 | +import { convertArrToDateStr } from "@/common/utils/common"; | |
| 111 | 112 | // Tabs 配置 |
| 112 | 113 | const tabList = ref([ |
| 113 | 114 | {name: '待完成', id: '1'}, | ... | ... |
pages-sub/daily/quick-order/order-detail.vue
| ... | ... | @@ -21,7 +21,7 @@ |
| 21 | 21 | <up-cell |
| 22 | 22 | title="工单编号" |
| 23 | 23 | :value="orderDetail.orderNo || '--'" |
| 24 | - class="up-line-1" | |
| 24 | + | |
| 25 | 25 | align="middle" |
| 26 | 26 | ></up-cell> |
| 27 | 27 | |
| ... | ... | @@ -42,7 +42,6 @@ |
| 42 | 42 | <up-cell |
| 43 | 43 | title="工单名称" |
| 44 | 44 | :value="orderDetail.orderName || '--'" |
| 45 | - class="up-line-1" | |
| 46 | 45 | align="middle" |
| 47 | 46 | ></up-cell> |
| 48 | 47 | ... | ... |
pages-sub/problem/work-order-manage/add-maintain-order.vue
| ... | ... | @@ -64,6 +64,7 @@ |
| 64 | 64 | active-color="#1989fa" |
| 65 | 65 | inactive-color="#666666" |
| 66 | 66 | @change = "imgTabChange" |
| 67 | + | |
| 67 | 68 | > |
| 68 | 69 | <!-- 使用content插槽自定义每个tab的内容,添加红色*号 --> |
| 69 | 70 | <template #content="{ item, index }"> |
| ... | ... | @@ -822,7 +823,5 @@ const submitWorkOrder = async () => { |
| 822 | 823 | font-weight: bold; |
| 823 | 824 | } |
| 824 | 825 | |
| 825 | -.tab-text { | |
| 826 | - font-size: 28rpx; | |
| 827 | -} | |
| 826 | + | |
| 828 | 827 | </style> |
| 829 | 828 | \ No newline at end of file | ... | ... |
pages-sub/problem/work-order-manage/add-order.vue
| ... | ... | @@ -7,6 +7,29 @@ |
| 7 | 7 | ref="workOrderFormRef" |
| 8 | 8 | labelWidth="190rpx" |
| 9 | 9 | > |
| 10 | + <!-- 业务线单选框:移入form内,作为第一个表单项(核心调整) --> | |
| 11 | + <up-form-item | |
| 12 | + label="业务线" | |
| 13 | + prop="busiLineCn" | |
| 14 | + border-bottom | |
| 15 | + required | |
| 16 | + > | |
| 17 | + <up-radio-group | |
| 18 | + v-model="workOrderForm.busiLineCn" | |
| 19 | + placement="row" | |
| 20 | + @change="handleBusiLineChange" | |
| 21 | + > | |
| 22 | + <up-radio | |
| 23 | + | |
| 24 | + v-for="item in busiLineOptions" | |
| 25 | + :key="item.name" | |
| 26 | + :label="item.name" | |
| 27 | + :name="item.name" | |
| 28 | + ></up-radio> | |
| 29 | + </up-radio-group> | |
| 30 | + | |
| 31 | + </up-form-item> | |
| 32 | + | |
| 10 | 33 | <!-- 1. 工单位置(地图选择) --> |
| 11 | 34 | <up-form-item |
| 12 | 35 | label="工单位置" |
| ... | ... | @@ -101,7 +124,7 @@ |
| 101 | 124 | </up-form-item> |
| 102 | 125 | |
| 103 | 126 | <!-- 问题照片(核心修复:绑定纯数组) --> |
| 104 | - <up-form-item label="问题照片" prop="problemImgs" required> | |
| 127 | + <up-form-item label="问题照片" prop="problemImgs" border-bottom required> | |
| 105 | 128 | <up-upload |
| 106 | 129 | :file-list="problemImgs.imgList.value||[]" |
| 107 | 130 | @after-read="problemImgs.uploadImgs" |
| ... | ... | @@ -128,7 +151,15 @@ |
| 128 | 151 | placeholder="点击选择时间" |
| 129 | 152 | ></up-input> |
| 130 | 153 | <template #right> |
| 131 | - <up-icon name="arrow-right" size="16"></up-icon> | |
| 154 | + <!-- 核心修复:用view包裹图标,添加双重@click.stop --> | |
| 155 | + <view v-if="workOrderForm.expectedFinishDate" @click.stop> | |
| 156 | + <up-icon | |
| 157 | + name="close" | |
| 158 | + size="16" | |
| 159 | + @click.stop="clearExpectedFinishDate" | |
| 160 | + ></up-icon> | |
| 161 | + </view> | |
| 162 | + <up-icon name="arrow-right" size="16" v-else></up-icon> | |
| 132 | 163 | </template> |
| 133 | 164 | </up-form-item> |
| 134 | 165 | </up-form> |
| ... | ... | @@ -165,13 +196,51 @@ |
| 165 | 196 | </template> |
| 166 | 197 | |
| 167 | 198 | <script setup> |
| 168 | -import { ref, reactive, watch } from 'vue' | |
| 199 | +import { ref, reactive } from 'vue' | |
| 169 | 200 | import { onReady, onShow, onLoad } from '@dcloudio/uni-app'; |
| 170 | -import { useUploadImgs } from '@/common/utils/useUploadImgs' // 引入改造后的上传逻辑 | |
| 201 | +import { useUploadImgs } from '@/common/utils/useUploadImgs' | |
| 171 | 202 | import { getRoadListByLatLng } from '@/api/common' |
| 172 | 203 | import { universalApproval, workorderCreate } from '@/api/work-order-manage/work-order-manage' |
| 173 | 204 | import { timeFormat } from '@/uni_modules/uview-plus' |
| 174 | 205 | import { nextStepMap } from '@/common/utils/common' |
| 206 | +import { useUserStore } from '@/pinia/user'; | |
| 207 | + | |
| 208 | +// ========== 状态管理 ========== | |
| 209 | +const userStore = useUserStore(); | |
| 210 | + | |
| 211 | +// ========== 业务线相关状态(核心改造:已整合到workOrderForm中,删除冗余selectedBusiLine) ========== | |
| 212 | +// 业务线映射表(键:英文标识,值:中文名称;新增反向映射:中文->英文) | |
| 213 | +const busiLineMap = ref({ | |
| 214 | + 'yl': '园林', | |
| 215 | + 'sz': '市政', | |
| 216 | + 'wy': '物业', | |
| 217 | + // 反向映射:用于通过中文名称(workOrderForm.busiLineCn)获取对应的英文标识 | |
| 218 | + '园林': 'yl', | |
| 219 | + '市政': 'sz', | |
| 220 | + '物业': 'wy' | |
| 221 | +}); | |
| 222 | + | |
| 223 | +// 业务线选项列表(仅保留name字段=中文名称,适配单选框配置) | |
| 224 | +const busiLineOptions = ref([]); | |
| 225 | +// 格式化业务线选项(兼容1个/2个/3个的情况,生成中文名称列表) | |
| 226 | +const formatBusiLineOptions = () => { | |
| 227 | + if (!userStore.userInfo?.user?.busiLine) { | |
| 228 | + busiLineOptions.value = []; | |
| 229 | + return; | |
| 230 | + } | |
| 231 | + const rawBusiLines = userStore.userInfo.user.busiLine.split(','); // 拆分字符串为数组 ["yl", "sz", "wy"] | |
| 232 | + console.log(rawBusiLines) | |
| 233 | + // 核心改造:仅生成name字段(值为中文名称),适配单选框:label="item.name" | |
| 234 | + busiLineOptions.value = rawBusiLines.map(item => ({ | |
| 235 | + name: busiLineMap.value[item.trim()] // name=中文名称(园林/市政/物业) | |
| 236 | + })); | |
| 237 | + console.log(busiLineOptions.value) | |
| 238 | +}; | |
| 239 | + | |
| 240 | +// ========== 工具方法:通过中文名称获取对应的英文标识 ========== | |
| 241 | +const getBusiLineEnByCn = (cnName) => { | |
| 242 | + return busiLineMap.value[cnName] || ''; | |
| 243 | +}; | |
| 175 | 244 | |
| 176 | 245 | // ========== 表单Ref ========== |
| 177 | 246 | const workOrderFormRef = ref(null) |
| ... | ... | @@ -190,7 +259,6 @@ if (!Array.isArray(problemImgs.rawImgList.value)) { |
| 190 | 259 | problemImgs.rawImgList.value = []; |
| 191 | 260 | } |
| 192 | 261 | |
| 193 | - | |
| 194 | 262 | // ========== 页面状态 ========== |
| 195 | 263 | // 通用弹窗控制 |
| 196 | 264 | const showActionSheet = ref(false) |
| ... | ... | @@ -213,22 +281,26 @@ const roadNameList = ref([]) |
| 213 | 281 | const orderNameList = ref([]) |
| 214 | 282 | const pressingTypeList = ref([]) |
| 215 | 283 | |
| 216 | -// ========== 工单表单数据 ========== | |
| 284 | +// ========== 工单表单数据(核心调整:新增busiLineCn字段,绑定业务线) ========== | |
| 217 | 285 | const workOrderForm = reactive({ |
| 218 | - roadId: 0, // 道路ID | |
| 219 | - roadName: '', // 道路名称 | |
| 220 | - workLocation: '', // 工单位置 | |
| 221 | - orderName: '', // 工单名称 | |
| 222 | - pressingType: '', // 紧急程度值(提交接口用) | |
| 223 | - pressingTypeName: '', // 紧急程度名称(显示用) | |
| 224 | - problemDesc: '', // 情况描述 | |
| 225 | - lat: 0, // 纬度 | |
| 226 | - lon: 0, // 经度 | |
| 227 | - expectedFinishDate: '', // 希望完成时间 | |
| 286 | + busiLineCn: '', // 新增:业务线中文名称(用于form绑定、校验、回显) | |
| 287 | + roadId: 0, // 原有字段 | |
| 288 | + roadName: '', // 原有字段 | |
| 289 | + workLocation: '', // 原有字段 | |
| 290 | + orderName: '', // 原有字段 | |
| 291 | + pressingType: '', // 原有字段 | |
| 292 | + pressingTypeName: '', // 原有字段 | |
| 293 | + problemDesc: '', // 原有字段 | |
| 294 | + lat: 0, // 原有字段 | |
| 295 | + lon: 0, // 原有字段 | |
| 296 | + expectedFinishDate: '', // 原有字段 | |
| 228 | 297 | }) |
| 229 | 298 | |
| 230 | -// ========== 表单校验规则 ========== | |
| 299 | +// ========== 表单校验规则(核心调整:新增busiLineCn校验规则) ========== | |
| 231 | 300 | const workOrderFormRules = reactive({ |
| 301 | + busiLineCn: [ // 新增:业务线必选校验 | |
| 302 | + { type: 'string', required: true, message: '请选择业务线', trigger: ['change', 'blur'] } | |
| 303 | + ], | |
| 232 | 304 | workLocation: [ |
| 233 | 305 | { type: 'string', required: true, message: '请选择工单位置', trigger: ['change', 'blur'] } |
| 234 | 306 | ], |
| ... | ... | @@ -249,12 +321,19 @@ const workOrderFormRules = reactive({ |
| 249 | 321 | }) |
| 250 | 322 | |
| 251 | 323 | // ========== 生命周期 ========== |
| 252 | -// 页面加载:读取本地存储的工单数据(核心改造) | |
| 324 | +// 页面加载:读取本地存储的工单数据(适配workOrderForm.busiLineCn) | |
| 253 | 325 | onLoad((options) => { |
| 326 | + // 初始化业务线选项 | |
| 327 | + formatBusiLineOptions(); | |
| 328 | + // 默认选中第一个业务线(赋值给workOrderForm.busiLineCn,核心调整) | |
| 329 | + if (busiLineOptions.value.length > 0) { | |
| 330 | + workOrderForm.busiLineCn = busiLineOptions.value[0].name; | |
| 331 | + } | |
| 332 | + | |
| 254 | 333 | // 判断是否为重新提交状态 |
| 255 | 334 | console.log('434') |
| 256 | 335 | console.log(options) |
| 257 | - if (options.isRenew ==1 && options.tempKey) { | |
| 336 | + if (options.isRenew == 1 && options.tempKey) { | |
| 258 | 337 | isRenew.value = true; |
| 259 | 338 | const tempKey = options.tempKey; |
| 260 | 339 | |
| ... | ... | @@ -307,9 +386,14 @@ onShow(() => { |
| 307 | 386 | console.log('紧急程度列表:', pressingTypeList.value) |
| 308 | 387 | }) |
| 309 | 388 | |
| 310 | -// ========== 核心方法:工单数据回显 ========== | |
| 389 | +// ========== 核心方法:工单数据回显(适配workOrderForm.busiLineCn) ========== | |
| 311 | 390 | const echoOrderData = (orderItem) => { |
| 312 | - // 1. 基础表单字段回显(适配工单字段与表单字段映射) | |
| 391 | + // 1. 回显业务线:将工单的英文busiLine转为中文名称,赋值给workOrderForm.busiLineCn(核心调整) | |
| 392 | + if (orderItem.busiLine) { | |
| 393 | + workOrderForm.busiLineCn = busiLineMap.value[orderItem.busiLine]; // 英文->中文(如wy->物业) | |
| 394 | + } | |
| 395 | + | |
| 396 | + // 2. 原有基础表单字段回显 | |
| 313 | 397 | workOrderForm.roadId = orderItem.roadId || 0; |
| 314 | 398 | workOrderForm.roadName = orderItem.roadName || ''; |
| 315 | 399 | workOrderForm.workLocation = orderItem.lonLatAddress || orderItem.roadName || ''; |
| ... | ... | @@ -321,7 +405,7 @@ const echoOrderData = (orderItem) => { |
| 321 | 405 | workOrderForm.lon = orderItem.lon || 0; |
| 322 | 406 | workOrderForm.expectedFinishDate = timeFormat(orderItem.expectedFinishDate, 'yyyy-mm-dd hh:MM:ss') || timeFormat(new Date(), 'yyyy-mm-dd hh:MM:ss'); |
| 323 | 407 | |
| 324 | - // 2. 上传图片回显(兼容useUploadImgs格式) | |
| 408 | + // 3. 上传图片回显(兼容useUploadImgs格式) | |
| 325 | 409 | if (orderItem.problemsImgs && Array.isArray(orderItem.problemsImgs) && orderItem.problemsImgs.length > 0) { |
| 326 | 410 | const imgList = orderItem.problemsImgs.map((imgUrl, index) => ({ |
| 327 | 411 | url: imgUrl, |
| ... | ... | @@ -332,25 +416,70 @@ const echoOrderData = (orderItem) => { |
| 332 | 416 | problemImgs.rawImgList.value = imgList; |
| 333 | 417 | } |
| 334 | 418 | |
| 335 | - // 3. 自动获取道路列表(保证下拉框正常使用) | |
| 419 | + // 4. 自动获取道路列表(修改:传递当前选中的业务线英文标识) | |
| 336 | 420 | if (orderItem.lat && orderItem.lon) { |
| 337 | - getRoadListByLatLng({ | |
| 338 | - companyCode: 'sls', | |
| 339 | - latitude: orderItem.lat, | |
| 340 | - longitude: orderItem.lon | |
| 341 | - }).then((roadRes) => { | |
| 342 | - if (Array.isArray(roadRes)) { | |
| 343 | - roadNameList.value = roadRes.map((item) => ({ | |
| 344 | - name: item.roadName || '', | |
| 345 | - code: item.roadCode || '', | |
| 346 | - id: item.roadId || 0 | |
| 347 | - })); | |
| 348 | - } | |
| 349 | - }).catch(err => console.error('回显道路列表失败:', err)); | |
| 421 | + getRoadListByBusiLine(); // 改用封装后的方法,自动携带选中的业务线 | |
| 350 | 422 | } |
| 351 | 423 | }; |
| 352 | 424 | |
| 353 | -// ========== 方法定义 ========== | |
| 425 | +// ========== 业务线相关方法(适配workOrderForm.busiLineCn) ========== | |
| 426 | +/** | |
| 427 | + * 业务线切换事件:清空道路信息 + 重新请求道路列表 | |
| 428 | + */ | |
| 429 | +const handleBusiLineChange = () => { | |
| 430 | + // 1. 清空已渲染的道路名称和表单字段 | |
| 431 | + workOrderForm.roadName = ''; | |
| 432 | + workOrderForm.roadId = 0; | |
| 433 | + // 2. 清空道路名称列表数据 | |
| 434 | + roadNameList.value = []; | |
| 435 | + // 3. 若已选择工单位置,重新请求对应业务线的道路列表 | |
| 436 | + if (workOrderForm.workLocation) { | |
| 437 | + getRoadListByBusiLine(); | |
| 438 | + } | |
| 439 | +}; | |
| 440 | + | |
| 441 | +/** | |
| 442 | + * 封装:根据当前选中业务线(中文)获取英文标识,再请求道路列表 | |
| 443 | + */ | |
| 444 | +const getRoadListByBusiLine = async () => { | |
| 445 | + if (!workOrderForm.lat || !workOrderForm.lon) { | |
| 446 | + return; | |
| 447 | + } | |
| 448 | + // 核心:从workOrderForm.busiLineCn获取中文名称,转为英文标识(核心调整) | |
| 449 | + const busiLineEn = getBusiLineEnByCn(workOrderForm.busiLineCn); | |
| 450 | + if (!busiLineEn) { | |
| 451 | + uni.showToast({ title: '业务线标识异常', icon: 'none' }); | |
| 452 | + return; | |
| 453 | + } | |
| 454 | + | |
| 455 | + try { | |
| 456 | + uni.showLoading({ title: '获取道路名称中...' }); | |
| 457 | + const roadRes = await getRoadListByLatLng({ | |
| 458 | + busiLine: busiLineEn, // 传递英文标识(yl/sz/wy),接口正常使用 | |
| 459 | + // companyCode: 'sls', // 传递英文标识(yl/sz/wy),接口正常使用 | |
| 460 | + latitude: workOrderForm.lat, | |
| 461 | + longitude: workOrderForm.lon | |
| 462 | + }); | |
| 463 | + uni.hideLoading(); | |
| 464 | + if (Array.isArray(roadRes)) { | |
| 465 | + roadNameList.value = roadRes.map((item) => ({ | |
| 466 | + name: item.roadName || '', | |
| 467 | + code: item.roadCode || '', | |
| 468 | + id: item.roadId || 0 | |
| 469 | + })); | |
| 470 | + } else { | |
| 471 | + roadNameList.value = [{ name: '未查询到道路名称', code: '', id: 0 }]; | |
| 472 | + uni.showToast({ title: '未查询到该位置的道路信息', icon: 'none' }); | |
| 473 | + } | |
| 474 | + } catch (err) { | |
| 475 | + uni.hideLoading(); | |
| 476 | + console.error('获取道路名称失败:', err); | |
| 477 | + uni.showToast({ title: '获取道路名称失败,请重试', icon: 'none' }); | |
| 478 | + roadNameList.value = [{ name: '获取失败,请重新选择位置', code: '', id: 0 }]; | |
| 479 | + } | |
| 480 | +}; | |
| 481 | + | |
| 482 | +// ========== 通用弹窗方法 ========== | |
| 354 | 483 | /** |
| 355 | 484 | * 打开通用下拉弹窗 |
| 356 | 485 | */ |
| ... | ... | @@ -425,7 +554,20 @@ const handleActionSheetSelect = (e) => { |
| 425 | 554 | * 返回上一页 |
| 426 | 555 | */ |
| 427 | 556 | const navigateBack = () => { |
| 428 | - uni.navigateBack() | |
| 557 | + uni.reLaunch({ | |
| 558 | + url: '/pages-sub/problem/work-order-manage/index', // 列表页路径 | |
| 559 | + fail: () => { | |
| 560 | + // 兜底:若 reLaunch 失败,返回2层(跳过提交页) | |
| 561 | + uni.navigateBack({ delta: 2 }); | |
| 562 | + } | |
| 563 | + }); | |
| 564 | +} | |
| 565 | + | |
| 566 | +// 新增:清除希望完成时间的方法 | |
| 567 | +const clearExpectedFinishDate = ()=> { | |
| 568 | + workOrderForm.expectedFinishDate = '' | |
| 569 | + // 清除后触发一次字段校验,确保校验状态更新 | |
| 570 | + // this.$refs.inspectFormRef.validateField('expectedFinishDate') | |
| 429 | 571 | } |
| 430 | 572 | |
| 431 | 573 | /** |
| ... | ... | @@ -445,31 +587,8 @@ const chooseWorkLocation = () => { |
| 445 | 587 | workOrderFormRef.value?.validateField('workLocation') |
| 446 | 588 | workOrderFormRef.value?.validateField('roadName') |
| 447 | 589 | |
| 448 | - try { | |
| 449 | - uni.showLoading({ title: '获取道路名称中...' }) | |
| 450 | - const roadRes = await getRoadListByLatLng({ | |
| 451 | - companyCode: 'sls', | |
| 452 | - latitude: res.latitude, | |
| 453 | - longitude: res.longitude | |
| 454 | - }) | |
| 455 | - uni.hideLoading() | |
| 456 | - | |
| 457 | - if (Array.isArray(roadRes)) { | |
| 458 | - roadNameList.value = roadRes.map((item) => ({ | |
| 459 | - name: item.roadName || '', | |
| 460 | - code: item.roadCode || '', | |
| 461 | - id: item.roadId || 0 | |
| 462 | - })) | |
| 463 | - } else { | |
| 464 | - roadNameList.value = [{ name: '未查询到道路名称', code: '', id: 0 }] | |
| 465 | - uni.showToast({ title: '未查询到该位置的道路信息', icon: 'none' }) | |
| 466 | - } | |
| 467 | - } catch (err) { | |
| 468 | - uni.hideLoading() | |
| 469 | - console.error('获取道路名称失败:', err) | |
| 470 | - uni.showToast({ title: '获取道路名称失败,请重试', icon: 'none' }) | |
| 471 | - roadNameList.value = [{ name: '获取失败,请重新选择位置', code: '', id: 0 }] | |
| 472 | - } | |
| 590 | + // 改用封装后的方法,自动携带选中的业务线英文标识 | |
| 591 | + await getRoadListByBusiLine(); | |
| 473 | 592 | }, |
| 474 | 593 | fail: (err) => { |
| 475 | 594 | console.error('选择位置失败:', err) |
| ... | ... | @@ -495,13 +614,20 @@ const hideKeyboard = () => { |
| 495 | 614 | } |
| 496 | 615 | |
| 497 | 616 | /** |
| 498 | - * 提交工单(区分新增/重新提交,接口隔离) | |
| 617 | + * 提交工单(区分新增/重新提交,接口隔离:从workOrderForm.busiLineCn取值) | |
| 499 | 618 | */ |
| 500 | 619 | const submitWorkOrder = async () => { |
| 501 | 620 | try { |
| 502 | - // 先执行表单校验 | |
| 621 | + // 先执行表单校验(包含业务线校验) | |
| 503 | 622 | await workOrderFormRef.value.validate() |
| 504 | 623 | |
| 624 | + // 核心:从workOrderForm.busiLineCn获取中文名称,转为英文标识(核心调整) | |
| 625 | + const busiLineEn = getBusiLineEnByCn(workOrderForm.busiLineCn); | |
| 626 | + if (!busiLineEn) { | |
| 627 | + uni.showToast({ title: '业务线选择异常,请重新选择', icon: 'none' }); | |
| 628 | + return; | |
| 629 | + } | |
| 630 | + | |
| 505 | 631 | // 构建公共提交数据 |
| 506 | 632 | const commonSubmitData = { |
| 507 | 633 | roadId: workOrderForm.roadId, |
| ... | ... | @@ -514,11 +640,10 @@ const submitWorkOrder = async () => { |
| 514 | 640 | lonLatAddress: workOrderForm.workLocation, |
| 515 | 641 | pressingType: workOrderForm.pressingType, |
| 516 | 642 | orderName: workOrderForm.orderName, |
| 517 | - // expectedFinishDate: workOrderForm.expectedFinishDate, | |
| 518 | 643 | expectedFinishDate: new Date(workOrderForm.expectedFinishDate).getTime(), |
| 519 | 644 | sourceId: 1, |
| 520 | - sourceName: '园林', | |
| 521 | - busiLine: 'yl' | |
| 645 | + sourceName: workOrderForm.busiLineCn, // 从form字段获取业务线中文名称 | |
| 646 | + busiLine: busiLineEn // 传递英文标识(yl/sz/wy),接口正常使用 | |
| 522 | 647 | } |
| 523 | 648 | |
| 524 | 649 | // 显示加载中 |
| ... | ... | @@ -567,7 +692,7 @@ const submitWorkOrder = async () => { |
| 567 | 692 | // 接口调用失败 |
| 568 | 693 | console.error(isRenew.value ? '工单重新提交失败:' : '工单提交失败:', error) |
| 569 | 694 | uni.showToast({ |
| 570 | - title: isRenew.value ? '重新提交失败,请重试' : '提交失败,请重试', | |
| 695 | + title: isRenew.value ? error.msg : error.msg, | |
| 571 | 696 | icon: 'none', |
| 572 | 697 | duration: 2000 |
| 573 | 698 | }) |
| ... | ... | @@ -583,6 +708,7 @@ const submitWorkOrder = async () => { |
| 583 | 708 | padding-bottom: 100rpx; // 给底部按钮留空间 |
| 584 | 709 | } |
| 585 | 710 | |
| 711 | + | |
| 586 | 712 | // 工单表单内容容器 |
| 587 | 713 | .work-order-form-content { |
| 588 | 714 | background: #fff; | ... | ... |
pages-sub/problem/work-order-manage/index.vue
| ... | ... | @@ -51,6 +51,7 @@ |
| 51 | 51 | v-model="orderList" |
| 52 | 52 | @query="queryList" |
| 53 | 53 | :auto-show-system-loading="true" |
| 54 | + | |
| 54 | 55 | > |
| 55 | 56 | <template #empty> |
| 56 | 57 | <empty-view/> |
| ... | ... | @@ -75,7 +76,7 @@ |
| 75 | 76 | </view> |
| 76 | 77 | <view class="u-body-item u-flex"> |
| 77 | 78 | <view class="u-body-item-title">工单位置:</view> |
| 78 | - <view class="u-line-1 u-body-value">{{ item.roadName || '-' }}</view> | |
| 79 | + <view class="u-line-1 u-body-value">{{ item.lonLatAddress || '-' }}</view> | |
| 79 | 80 | </view> |
| 80 | 81 | <view class="u-body-item u-flex"> |
| 81 | 82 | <view class="u-body-item-title">工单名称:</view> |
| ... | ... | @@ -253,6 +254,7 @@ |
| 253 | 254 | :file-list="acceptImgs.imgList" |
| 254 | 255 | @after-read="acceptImgs.uploadImgs" |
| 255 | 256 | @delete="acceptImgs.deleteImg" |
| 257 | + | |
| 256 | 258 | multiple |
| 257 | 259 | width="70" |
| 258 | 260 | height="70" |
| ... | ... | @@ -268,7 +270,7 @@ |
| 268 | 270 | |
| 269 | 271 | <script setup> |
| 270 | 272 | import { ref, computed, watch } from 'vue'; |
| 271 | -import { onShow } from '@dcloudio/uni-app'; | |
| 273 | +import { onShow, onLoad } from '@dcloudio/uni-app'; | |
| 272 | 274 | import { timeFormat } from '@/uni_modules/uview-plus'; |
| 273 | 275 | import { |
| 274 | 276 | myBuzSimplePage, |
| ... | ... | @@ -276,7 +278,7 @@ import { |
| 276 | 278 | doneBuzSimplePage, |
| 277 | 279 | universalApproval |
| 278 | 280 | } from '@/api/work-order-manage/work-order-manage' |
| 279 | -// 假设从用户store获取角色信息 | |
| 281 | +// 从用户store获取角色信息 | |
| 280 | 282 | import { useUserStore } from '@/pinia/user'; |
| 281 | 283 | import { nextStepMap, buzStatusMap } from '@/common/utils/common' |
| 282 | 284 | // 引入图片上传组合式函数(与参考页面一致) |
| ... | ... | @@ -399,7 +401,16 @@ const handleSearch = (val) => { |
| 399 | 401 | const handleDetail = (item) => { |
| 400 | 402 | // 0-待办 1我发起的- 2-已办 |
| 401 | 403 | uni.navigateTo({ |
| 402 | - url: `/pages-sub/problem/work-order-manage/order-detail?taskId=${item.taskId}&activeTab=${activeTab.value}&processInstanceId=${item.processInstanceId}` | |
| 404 | + url: `/pages-sub/problem/work-order-manage/order-detail?taskId=${item.taskId}&activeTab=${activeTab.value}&processInstanceId=${item.processInstanceId}`, | |
| 405 | + events: { | |
| 406 | + // 自定义事件名:needRefresh(与详情页保持一致) | |
| 407 | + needRefresh: () => { | |
| 408 | + console.log('详情页返回,触发工单列表刷新'); | |
| 409 | + if (paging.value) { | |
| 410 | + paging.value.reload(); // 刷新z-paging列表 | |
| 411 | + } | |
| 412 | + } | |
| 413 | + } | |
| 403 | 414 | }); |
| 404 | 415 | }; |
| 405 | 416 | |
| ... | ... | @@ -640,7 +651,7 @@ const handleAcceptModalConfirm = async () => { |
| 640 | 651 | }; |
| 641 | 652 | |
| 642 | 653 | // 页面初始化 |
| 643 | -onShow(() => { | |
| 654 | +onLoad(() => { | |
| 644 | 655 | // 初始化加载列表 |
| 645 | 656 | paging.value?.reload(); |
| 646 | 657 | }); | ... | ... |
pages-sub/problem/work-order-manage/order-detail.vue
| ... | ... | @@ -38,12 +38,12 @@ |
| 38 | 38 | <view style="min-width: 200rpx">工单位置</view> |
| 39 | 39 | </template> |
| 40 | 40 | <template #value> |
| 41 | - <view class="common-text-color up-line-1">{{ orderDetail.roadName || '--' }}</view> | |
| 41 | + <view class="common-text-color up-line-1">{{ orderDetail.lonLatAddress || '--' }}</view> | |
| 42 | 42 | </template> |
| 43 | 43 | </up-cell> |
| 44 | 44 | |
| 45 | 45 | <!-- 工单名称 --> |
| 46 | - <up-cell align="middle"> | |
| 46 | + <up-cell align="middle"> | |
| 47 | 47 | <template #title> |
| 48 | 48 | <view style="min-width: 200rpx">工单名称</view> |
| 49 | 49 | </template> |
| ... | ... | @@ -99,7 +99,9 @@ |
| 99 | 99 | <up-album |
| 100 | 100 | v-if="!!orderDetail.problemsImgs?.length" |
| 101 | 101 | :urls="orderDetail.problemsImgs.slice(0, 3)" |
| 102 | - singleSize="70" | |
| 102 | + :singleSize="70" | |
| 103 | + :multipleSize="70" | |
| 104 | + | |
| 103 | 105 | :preview-full-image="true" |
| 104 | 106 | ></up-album> |
| 105 | 107 | <text v-else class="empty-text">暂无问题照片</text> |
| ... | ... | @@ -134,7 +136,9 @@ |
| 134 | 136 | </template> |
| 135 | 137 | <template #value> |
| 136 | 138 | <view class="common-text-color up-line-1"> |
| 137 | - {{ Array.isArray(orderDetail.coHandlersName) && orderDetail.coHandlersName.length > 0 ? orderDetail.coHandlersName.join(',') : '--' }} | |
| 139 | + {{ | |
| 140 | + Array.isArray(orderDetail.coHandlersName) && orderDetail.coHandlersName.length > 0 ? orderDetail.coHandlersName.join(',') : '--' | |
| 141 | + }} | |
| 138 | 142 | </view> |
| 139 | 143 | </template> |
| 140 | 144 | </up-cell> |
| ... | ... | @@ -154,10 +158,11 @@ |
| 154 | 158 | <up-album |
| 155 | 159 | v-if="currentImgList.length" |
| 156 | 160 | :urls="currentImgList.slice(0, 3)" |
| 157 | - singleSize="80" | |
| 158 | - multipleSize="80" | |
| 161 | + :singleSize="70" | |
| 162 | + :multipleSize="70" | |
| 163 | + | |
| 159 | 164 | :preview-full-image="true" |
| 160 | - class="img-album" | |
| 165 | + class="img-album custom-album" | |
| 161 | 166 | ></up-album> |
| 162 | 167 | <text v-else class="empty-img-text">暂无图片</text> |
| 163 | 168 | </view> |
| ... | ... | @@ -176,19 +181,21 @@ |
| 176 | 181 | inactive-color="#999" |
| 177 | 182 | class="vertical-steps" |
| 178 | 183 | > |
| 179 | - <template > | |
| 184 | + <template> | |
| 180 | 185 | <up-steps-item |
| 181 | 186 | v-for="(item, index) in processData.activityNodes" |
| 182 | 187 | :key="`${item.id}_${index}`" |
| 183 | 188 | > |
| 184 | 189 | <!-- 唯一自定义模板:content,包含标题、描述、相册所有内容 --> |
| 185 | 190 | <template #content> |
| 186 | - <view class="step-content-wrap" > | |
| 191 | + <view class="step-content-wrap"> | |
| 187 | 192 | <!-- 1. 原标题内容:节点名称 + 操作人 --> |
| 188 | 193 | <view class="step-title"> |
| 189 | 194 | {{ item.name }} |
| 190 | 195 | <text class="operator-name"> |
| 191 | - {{ item.tasks && item.tasks[0]?.assigneeUser?.nickname ? '(' + item.tasks[0].assigneeUser.nickname + ')' : '(未知操作人)' }} | |
| 196 | + {{ | |
| 197 | + item.tasks && item.tasks[0]?.assigneeUser?.nickname ? '(' + item.tasks[0].assigneeUser.nickname + ')' : '(未知操作人)' | |
| 198 | + }} | |
| 192 | 199 | </text> |
| 193 | 200 | </view> |
| 194 | 201 | |
| ... | ... | @@ -202,7 +209,7 @@ |
| 202 | 209 | </view> |
| 203 | 210 | |
| 204 | 211 | <view class="reason-line up-line-1" v-if="index!==0&&item.endTime"> |
| 205 | - 总耗时:{{ calculateFormatTimeDiff(item.startTime , item.endTime) }} | |
| 212 | + 总耗时:{{ calculateFormatTimeDiff(item.startTime, item.endTime) }} | |
| 206 | 213 | </view> |
| 207 | 214 | |
| 208 | 215 | <!-- 原因行 --> |
| ... | ... | @@ -216,8 +223,9 @@ |
| 216 | 223 | <up-album |
| 217 | 224 | v-if="item.tasks && item.tasks[0]?.attattmentUrls && item.tasks[0].attattmentUrls.length" |
| 218 | 225 | :urls="item.tasks[0].attattmentUrls.slice(0, 3)" |
| 219 | - singleSize="70" | |
| 220 | - multipleSize="70" | |
| 226 | + :singleSize="70" | |
| 227 | + :multipleSize="70" | |
| 228 | + | |
| 221 | 229 | :preview-full-image="true" |
| 222 | 230 | class="step-album" |
| 223 | 231 | ></up-album> |
| ... | ... | @@ -359,9 +367,9 @@ import { |
| 359 | 367 | getApprovalDetail, |
| 360 | 368 | universalApproval |
| 361 | 369 | } from '@/api/work-order-manage/work-order-manage'; |
| 362 | -import { nextStepMap, buzStatusMap, calculateFormatTimeDiff } from '@/common/utils/common' | |
| 370 | +import {nextStepMap, buzStatusMap, calculateFormatTimeDiff} from '@/common/utils/common' | |
| 363 | 371 | // 引入图片上传组合式函数 |
| 364 | -import { useUploadImgs } from '@/common/utils/useUploadImgs' | |
| 372 | +import {useUploadImgs} from '@/common/utils/useUploadImgs' | |
| 365 | 373 | |
| 366 | 374 | // 状态管理 |
| 367 | 375 | const loading = ref(true); |
| ... | ... | @@ -407,7 +415,7 @@ const orderDetail = ref<any>({ |
| 407 | 415 | curingLevelName: '', |
| 408 | 416 | commitDate: 0, |
| 409 | 417 | finishDate: 0, |
| 410 | - expectedFinishDate:0, | |
| 418 | + expectedFinishDate: 0, | |
| 411 | 419 | pressingType: 0, |
| 412 | 420 | userId: 0, |
| 413 | 421 | userName: '', |
| ... | ... | @@ -450,7 +458,7 @@ const orderDetail = ref<any>({ |
| 450 | 458 | const currentImgList = ref([]) |
| 451 | 459 | |
| 452 | 460 | const tabKeyMap = ['startImgs', 'processingImgs', 'endImgs', 'personImgs', 'materialImgs']; |
| 453 | -const imgTabChange = (({index}: {index: number}) => { | |
| 461 | +const imgTabChange = (({index}: { index: number }) => { | |
| 454 | 462 | console.log(index) |
| 455 | 463 | const currentKey = tabKeyMap[index] |
| 456 | 464 | console.log(currentKey) |
| ... | ... | @@ -476,7 +484,7 @@ const getLimitReason = (reason: string | null | undefined) => { |
| 476 | 484 | * @returns {number} 当前激活的步骤索引(从0开始) |
| 477 | 485 | */ |
| 478 | 486 | const getCurrentStepIndex = () => { |
| 479 | - const { activityNodes } = processData.value; | |
| 487 | + const {activityNodes} = processData.value; | |
| 480 | 488 | if (!activityNodes || !activityNodes.length) return 0; |
| 481 | 489 | |
| 482 | 490 | // 2. 若没有处理中的节点(全部已完成),则激活最后一个节点 |
| ... | ... | @@ -568,7 +576,7 @@ const rejectImgs = useUploadImgs({ |
| 568 | 576 | // 监听上传实例响应式变化,解决u-upload不刷新问题 |
| 569 | 577 | watch(() => rejectImgs.rawImgList.value, (newVal) => { |
| 570 | 578 | rejectImgs.imgList = newVal |
| 571 | -}, { deep: true }) | |
| 579 | +}, {deep: true}) | |
| 572 | 580 | |
| 573 | 581 | // ========== 验收弹窗相关状态(新增图片上传实例) ========== |
| 574 | 582 | const acceptModalShow = ref(false); // 验收弹窗显示开关 |
| ... | ... | @@ -586,7 +594,7 @@ const acceptImgs = useUploadImgs({ |
| 586 | 594 | // 监听验收图片上传实例响应式变化 |
| 587 | 595 | watch(() => acceptImgs.rawImgList.value, (newVal) => { |
| 588 | 596 | acceptImgs.imgList = newVal |
| 589 | -}, { deep: true }) | |
| 597 | +}, {deep: true}) | |
| 590 | 598 | |
| 591 | 599 | // ========== 生成临时key ========== |
| 592 | 600 | const generateTempKey = () => { |
| ... | ... | @@ -742,7 +750,7 @@ const handleProcess = async (item: any) => { |
| 742 | 750 | const requestData = { |
| 743 | 751 | "returnImgs": rejectImgs.getSuccessImgUrls(), |
| 744 | 752 | "workerDataId": item.id, |
| 745 | - "taskKey":'ylInspectorStart', | |
| 753 | + "taskKey": 'ylInspectorStart', | |
| 746 | 754 | "taskId": item.taskId, |
| 747 | 755 | "operateType": 200, |
| 748 | 756 | "agree": 1, |
| ... | ... | @@ -812,13 +820,17 @@ const handleAcceptModalConfirm = async () => { |
| 812 | 820 | } |
| 813 | 821 | const acceptRes = await universalApproval(postData); |
| 814 | 822 | // 4. 操作成功处理 |
| 815 | - uni.showToast({title: '提交成功', icon: 'success', duration: 1000}); | |
| 823 | + | |
| 816 | 824 | handleAcceptModalCancel(); // 清空状态 |
| 817 | 825 | // 重新获取工单详情,刷新页面 |
| 818 | 826 | // await DetailQuery(taskId.value); |
| 819 | - uni.reLaunch({ | |
| 820 | - url: `/pages-sub/problem/work-order-manage/index` | |
| 821 | - }); | |
| 827 | + // uni.reLaunch({ | |
| 828 | + // url: `/pages-sub/problem/work-order-manage/index` | |
| 829 | + // }); | |
| 830 | + eventChannel.emit('needRefresh'); | |
| 831 | + // 4. 返回列表页 | |
| 832 | + uni.navigateBack({delta: 1}); | |
| 833 | + uni.showToast({title: '提交成功', icon: 'success', duration: 1000}); | |
| 822 | 834 | } catch (error) { |
| 823 | 835 | // 5. 操作失败处理 |
| 824 | 836 | console.error('验收失败:', error); |
| ... | ... | @@ -888,13 +900,14 @@ onShow(() => { |
| 888 | 900 | // 图片内容区 |
| 889 | 901 | .img-tab-content { |
| 890 | 902 | padding: 20rpx 15px ; |
| 891 | - min-height: 120rpx; | |
| 892 | - display: flex; | |
| 893 | - align-items: center; | |
| 894 | - justify-content: center; | |
| 895 | - | |
| 903 | + //min-height: 120rpx; | |
| 904 | + //display: flex; | |
| 905 | + //align-items: center; | |
| 906 | + //justify-content: center; | |
| 907 | + // | |
| 896 | 908 | .img-album { |
| 897 | - width: 100%; | |
| 909 | + //width: 100%; | |
| 910 | + | |
| 898 | 911 | } |
| 899 | 912 | |
| 900 | 913 | .empty-img-text { |
| ... | ... | @@ -973,8 +986,7 @@ onShow(() => { |
| 973 | 986 | padding: 10rpx 0; |
| 974 | 987 | |
| 975 | 988 | .step-album { |
| 976 | - width: 100%; | |
| 977 | - max-width: 300rpx; | |
| 989 | + //width: 100%; | |
| 978 | 990 | } |
| 979 | 991 | |
| 980 | 992 | .no-img-tip { |
| ... | ... | @@ -1034,4 +1046,11 @@ onShow(() => { |
| 1034 | 1046 | .mt-30 { |
| 1035 | 1047 | margin-top: 30rpx; |
| 1036 | 1048 | } |
| 1049 | +// 针对 up-album 单图容器的样式(穿透 scoped 限制) | |
| 1050 | +:deep .u-album__row__wrapper image { | |
| 1051 | + width: 70px !important; // 与多图保持一致 | |
| 1052 | + height: 70px !important; | |
| 1053 | +} | |
| 1054 | + | |
| 1055 | + | |
| 1037 | 1056 | </style> |
| 1038 | 1057 | \ No newline at end of file | ... | ... |
pages.json
| 1 | 1 | { |
| 2 | 2 | "pages": [ |
| 3 | 3 | { |
| 4 | - "path": "pages/login/index", | |
| 4 | + "path": "pages/workbench/index", | |
| 5 | 5 | "style": { |
| 6 | - "navigationBarTitleText": "登录" | |
| 6 | + "navigationBarTitleText": "工作台", | |
| 7 | + "navigationStyle": "custom" | |
| 7 | 8 | } |
| 8 | 9 | }, |
| 9 | 10 | { |
| 10 | - "path": "pages/index/index", | |
| 11 | + "path": "pages/login/index", | |
| 11 | 12 | "style": { |
| 12 | - "navigationBarTitleText": "首页" | |
| 13 | + "navigationBarTitleText": "登录" | |
| 13 | 14 | } |
| 14 | 15 | }, |
| 15 | 16 | { |
| 16 | - "path": "pages/workbench/index", | |
| 17 | + "path": "pages/index/index", | |
| 17 | 18 | "style": { |
| 18 | - "navigationBarTitleText": "工作台", | |
| 19 | - "navigationStyle": "custom" | |
| 19 | + "navigationBarTitleText": "首页" | |
| 20 | 20 | } |
| 21 | 21 | }, |
| 22 | + | |
| 22 | 23 | { |
| 23 | 24 | "path": "pages/mine/index", |
| 24 | 25 | "style": { |
| ... | ... | @@ -91,7 +92,7 @@ |
| 91 | 92 | |
| 92 | 93 | { |
| 93 | 94 | "path": "maintain-manage/road-detail-list", |
| 94 | - "style": { "navigationBarTitleText": "道路明细列表" } | |
| 95 | + "style": { "navigationBarTitleText": "计划明细" } | |
| 95 | 96 | }, |
| 96 | 97 | |
| 97 | 98 | { | ... | ... |
pages/login/index.vue
| ... | ... | @@ -107,10 +107,10 @@ const checkLoginStatus = () => { |
| 107 | 107 | // 已登录则直接跳首页 |
| 108 | 108 | if (userStore.isLogin) { |
| 109 | 109 | uni.switchTab({ |
| 110 | - url: globalConfig.router.tabBarList[1].path, | |
| 110 | + url: '/pages/workbench/index', | |
| 111 | 111 | fail: () => { |
| 112 | 112 | // 非tabBar页面用redirectTo |
| 113 | - uni.redirectTo({ url: '/pages/workbench/index' }); | |
| 113 | + uni.reLaunch({ url: '/pages/workbench/index' }); | |
| 114 | 114 | } |
| 115 | 115 | }); |
| 116 | 116 | return; |
| ... | ... | @@ -157,7 +157,7 @@ const handleLogin = async () => { |
| 157 | 157 | password: form.password |
| 158 | 158 | }); |
| 159 | 159 | |
| 160 | - uni.showToast({ title: '登录成功', icon: 'success', duration: 1500 }); | |
| 160 | + uni.showToast({ title: '登录成功', icon: 'success', duration: 1000 }); | |
| 161 | 161 | |
| 162 | 162 | // 登录成功后跳转首页 |
| 163 | 163 | setTimeout(() => { |
| ... | ... | @@ -165,10 +165,10 @@ const handleLogin = async () => { |
| 165 | 165 | url: globalConfig.router.tabBarList[1].path, |
| 166 | 166 | fail: (err) => { |
| 167 | 167 | console.warn('tabBar跳转失败,切换为普通跳转:', err); |
| 168 | - uni.redirectTo({ url: '/pages/workbench/index' }); | |
| 168 | + uni.reLaunch({ url: '/pages/workbench/index' }); | |
| 169 | 169 | } |
| 170 | 170 | }); |
| 171 | - }, 1500); | |
| 171 | + }, 1000); | |
| 172 | 172 | } catch (err) { |
| 173 | 173 | console.error('登录失败详情:', err); |
| 174 | 174 | const errorMsg = | ... | ... |
pages/workbench/index.vue
| ... | ... | @@ -98,6 +98,39 @@ const filteredModuleList = computed(() => { |
| 98 | 98 | onShow(async () => { |
| 99 | 99 | try { |
| 100 | 100 | loading.value = true; |
| 101 | + | |
| 102 | + // ========== 核心新增:登录状态判断 ========== | |
| 103 | + // 1. 定义登录判断逻辑(多维度校验,确保准确性) | |
| 104 | + const isLogin = () => { | |
| 105 | + | |
| 106 | + // 从缓存获取token(核心登录标识) | |
| 107 | + const token = cache.get(globalConfig.cache.tokenKey) || userStore.token; | |
| 108 | + // 从缓存/Pinia获取用户信息 | |
| 109 | + const userInfo = cache.get(globalConfig.cache.userInfoKey) || userStore.userInfo; | |
| 110 | + // 满足任一核心条件即视为已登录 | |
| 111 | + return (!!token && !!userInfo); | |
| 112 | + }; | |
| 113 | + | |
| 114 | + // 2. 未登录处理:跳转登录页,阻止后续逻辑执行 | |
| 115 | + if (!isLogin()) { | |
| 116 | + uni.showToast({ title: '请先登录', icon: 'none', duration: 1500 }); | |
| 117 | + // 延迟跳转,确保提示语正常显示 | |
| 118 | + setTimeout(() => { | |
| 119 | + // 使用reLaunch跳转,清空页面栈,避免返回当前菜单页 | |
| 120 | + uni.reLaunch({ | |
| 121 | + url: '/pages/login/login', // 替换为你的实际登录页路径 | |
| 122 | + fail: (err) => { | |
| 123 | + console.error('跳转登录页失败:', err); | |
| 124 | + uni.showToast({ title: '跳转登录页异常', icon: 'none' }); | |
| 125 | + } | |
| 126 | + }); | |
| 127 | + }, 1500); | |
| 128 | + // 隐藏加载状态 | |
| 129 | + loading.value = false; | |
| 130 | + // 终止后续代码执行 | |
| 131 | + return; | |
| 132 | + } | |
| 133 | + | |
| 101 | 134 | const rawMenuData = userStore.moduleListInfo || cache.get(globalConfig.cache.moduleListKey); |
| 102 | 135 | moduleList.value = rawMenuData || []; |
| 103 | 136 | loading.value = false; | ... | ... |
pinia/user.js
| 1 | 1 | import { defineStore } from 'pinia'; |
| 2 | 2 | import cache from '@/common/utils/cache'; |
| 3 | 3 | import globalConfig from '@/common/config/global'; |
| 4 | -import { login, getUserInfo, logout, moduleList, getSimpleDictDataList } from '@/api/user'; | |
| 4 | +// 新增:导入 refreshToken 接口 | |
| 5 | +import { login, getUserInfo, logout, moduleList, getSimpleDictDataList, refreshToken } from '@/api/user'; | |
| 6 | + | |
| 7 | +// 新增:定义刷新Token的定时器标识(避免重复创建定时器) | |
| 8 | +let refreshTokenTimer = null; | |
| 5 | 9 | |
| 6 | 10 | export const useUserStore = defineStore('user', { |
| 7 | 11 | // 修复1:删除重复的 state 定义,只保留根层级的 state |
| 8 | 12 | state: () => ({ |
| 9 | 13 | token: cache.get(globalConfig.cache.tokenKey) || '', // 初始值从缓存取(兼容持久化) |
| 14 | + refreshToken: cache.get(globalConfig.cache.refreshTokenKey) || '', // 新增:Refresh Token 状态 | |
| 10 | 15 | userInfo: cache.get(globalConfig.cache.userInfoKey) || {}, |
| 11 | 16 | userId: cache.get(globalConfig.cache.userIdKey) || '', |
| 12 | 17 | moduleListInfo: cache.get(globalConfig.cache.moduleListKey) || '', |
| ... | ... | @@ -23,14 +28,22 @@ export const useUserStore = defineStore('user', { |
| 23 | 28 | } |
| 24 | 29 | return true; |
| 25 | 30 | }, |
| 26 | - permissions: (state) => state.userInfo.permissions || [] | |
| 31 | + permissions: (state) => state.userInfo.permissions || [], | |
| 32 | + // 新增:判断是否需要刷新Token(过期前30秒触发,避免已过期才刷新) | |
| 33 | + needRefreshToken: (state) => { | |
| 34 | + if (!state.token || !state.refreshToken) return false; | |
| 35 | + const remainingTime = state.expireTime - Date.now(); | |
| 36 | + // 剩余时间大于0秒 且 小于30秒(即将过期) | |
| 37 | + return remainingTime > 0 && remainingTime < 30 * 1000; | |
| 38 | + } | |
| 27 | 39 | }, |
| 28 | 40 | |
| 29 | 41 | actions: { |
| 30 | 42 | async login(params) { |
| 31 | 43 | try { |
| 32 | 44 | const res = await login(params); |
| 33 | - const { accessToken, expiresTime, userId } = res; | |
| 45 | + // 新增:从登录接口返回值中获取 refreshToken(若接口返回字段名不一致,可调整,如 res.refresh_token) | |
| 46 | + const { accessToken, expiresTime, userId, refreshToken } = res; | |
| 34 | 47 | |
| 35 | 48 | if (!accessToken) { |
| 36 | 49 | throw new Error('登录失败,未获取到令牌'); |
| ... | ... | @@ -38,6 +51,7 @@ export const useUserStore = defineStore('user', { |
| 38 | 51 | |
| 39 | 52 | // 更新 Pinia state |
| 40 | 53 | this.token = accessToken; |
| 54 | + this.refreshToken = refreshToken || ''; // 新增:存储Refresh Token | |
| 41 | 55 | this.expireTime = expiresTime; |
| 42 | 56 | this.userId = userId; |
| 43 | 57 | this.userInfo = {}; |
| ... | ... | @@ -70,6 +84,9 @@ export const useUserStore = defineStore('user', { |
| 70 | 84 | uni.showToast({ title: '获取字典失败,可正常使用', icon: 'none' }); |
| 71 | 85 | } |
| 72 | 86 | |
| 87 | + // 新增:登录成功后,启动 Token 自动刷新定时器 | |
| 88 | + this.startRefreshTokenTimer(); | |
| 89 | + | |
| 73 | 90 | return { ...res, userInfo, moduleListInfo }; |
| 74 | 91 | } catch (err) { |
| 75 | 92 | console.error('登录流程失败:', err); |
| ... | ... | @@ -77,6 +94,73 @@ export const useUserStore = defineStore('user', { |
| 77 | 94 | } |
| 78 | 95 | }, |
| 79 | 96 | |
| 97 | + // 新增:调用刷新Token接口,返回刷新是否成功 | |
| 98 | + async refreshTokenApi() { | |
| 99 | + // 前置校验:无Refresh Token直接返回失败 | |
| 100 | + if (!this.refreshToken) { | |
| 101 | + console.warn('无刷新令牌,无法刷新Token'); | |
| 102 | + this.logout(); // 无刷新令牌,直接退出登录 | |
| 103 | + return false; | |
| 104 | + } | |
| 105 | + | |
| 106 | + try { | |
| 107 | + // 调用修正后的 refreshToken 接口,传入当前的 refreshToken | |
| 108 | + const res = await refreshToken(this.refreshToken); | |
| 109 | + | |
| 110 | + // 校验接口返回结果(根据你的实际接口返回格式调整) | |
| 111 | + if (!res || !res.accessToken) { | |
| 112 | + throw new Error('刷新Token失败,未获取到新令牌'); | |
| 113 | + } | |
| 114 | + | |
| 115 | + // 更新 Token 相关状态 | |
| 116 | + this.token = res.accessToken; // 新的访问令牌 | |
| 117 | + this.refreshToken = res.refreshToken || this.refreshToken; // 若接口返回新的Refresh Token则更新 | |
| 118 | + this.expireTime = res.expiresTime; // 新的过期时间 | |
| 119 | + console.log('Token 刷新成功'); | |
| 120 | + return true; | |
| 121 | + } catch (err) { | |
| 122 | + console.error('刷新Token失败:', err); | |
| 123 | + // 刷新失败,直接退出登录 | |
| 124 | + this.logout(); | |
| 125 | + return false; | |
| 126 | + } | |
| 127 | + }, | |
| 128 | + | |
| 129 | + // 新增:启动 Token 自动刷新定时器 | |
| 130 | + startRefreshTokenTimer() { | |
| 131 | + // 先清除已有定时器,避免重复创建 | |
| 132 | + if (refreshTokenTimer) { | |
| 133 | + clearTimeout(refreshTokenTimer); | |
| 134 | + refreshTokenTimer = null; | |
| 135 | + } | |
| 136 | + | |
| 137 | + // 计算剩余时间(过期前30秒执行刷新) | |
| 138 | + const remainingTime = this.expireTime - Date.now() - 30 * 1000; | |
| 139 | + if (remainingTime <= 0) { | |
| 140 | + // 剩余时间不足,立即执行刷新 | |
| 141 | + this.refreshTokenApi(); | |
| 142 | + return; | |
| 143 | + } | |
| 144 | + | |
| 145 | + // 设置定时器,到期后执行刷新 | |
| 146 | + refreshTokenTimer = setTimeout(async () => { | |
| 147 | + const refreshSuccess = await this.refreshTokenApi(); | |
| 148 | + // 刷新成功后,重新启动定时器(循环自动刷新) | |
| 149 | + if (refreshSuccess) { | |
| 150 | + this.startRefreshTokenTimer(); | |
| 151 | + } | |
| 152 | + }, remainingTime); | |
| 153 | + console.log(`Token 自动刷新定时器已启动,将在 ${Math.ceil(remainingTime / 1000)} 秒后执行刷新`); | |
| 154 | + }, | |
| 155 | + | |
| 156 | + // 新增:停止 Token 自动刷新定时器(避免内存泄漏) | |
| 157 | + stopRefreshTokenTimer() { | |
| 158 | + if (refreshTokenTimer) { | |
| 159 | + clearTimeout(refreshTokenTimer); | |
| 160 | + refreshTokenTimer = null; | |
| 161 | + } | |
| 162 | + }, | |
| 163 | + | |
| 80 | 164 | // 修复3:重构字典获取方法(加 Token 校验 + 强制携带 Token + 宽松错误处理) |
| 81 | 165 | async getAndSaveDictData() { |
| 82 | 166 | // 前置校验:无登录态直接返回,不请求接口 |
| ... | ... | @@ -136,6 +220,9 @@ export const useUserStore = defineStore('user', { |
| 136 | 220 | }, |
| 137 | 221 | |
| 138 | 222 | logout() { |
| 223 | + // 新增:退出登录时,停止刷新定时器 | |
| 224 | + this.stopRefreshTokenTimer(); | |
| 225 | + | |
| 139 | 226 | const pages = getCurrentPages(); |
| 140 | 227 | if (pages.length === 0) return; |
| 141 | 228 | |
| ... | ... | @@ -146,6 +233,7 @@ export const useUserStore = defineStore('user', { |
| 146 | 233 | |
| 147 | 234 | if (currentPageRoute === loginPath) { |
| 148 | 235 | this.token = ''; |
| 236 | + this.refreshToken = ''; // 新增:清空Refresh Token | |
| 149 | 237 | this.userInfo = {}; |
| 150 | 238 | this.userId = ''; |
| 151 | 239 | this.moduleListInfo = ''; |
| ... | ... | @@ -164,6 +252,7 @@ export const useUserStore = defineStore('user', { |
| 164 | 252 | console.error('退出登录接口调用失败:', err); |
| 165 | 253 | } finally { |
| 166 | 254 | this.token = ''; |
| 255 | + this.refreshToken = ''; // 新增:清空Refresh Token | |
| 167 | 256 | this.userInfo = {}; |
| 168 | 257 | this.userId = ''; |
| 169 | 258 | this.moduleListInfo = ''; |
| ... | ... | @@ -197,6 +286,9 @@ export const useUserStore = defineStore('user', { |
| 197 | 286 | } |
| 198 | 287 | return false; |
| 199 | 288 | } |
| 289 | + | |
| 290 | + // 新增:已登录时,启动刷新定时器(防止页面刷新后定时器丢失) | |
| 291 | + this.startRefreshTokenTimer(); | |
| 200 | 292 | return true; |
| 201 | 293 | } |
| 202 | 294 | }, |
| ... | ... | @@ -212,6 +304,7 @@ export const useUserStore = defineStore('user', { |
| 212 | 304 | serializer: { |
| 213 | 305 | serialize: (state) => { |
| 214 | 306 | uni.setStorageSync(globalConfig.cache.tokenKey, state.token); |
| 307 | + uni.setStorageSync(globalConfig.cache.refreshTokenKey, state.refreshToken); // 新增:持久化Refresh Token | |
| 215 | 308 | uni.setStorageSync(globalConfig.cache.userIdKey, state.userId); |
| 216 | 309 | uni.setStorageSync(globalConfig.cache.expireTimeKey, state.expireTime); |
| 217 | 310 | uni.setStorageSync(globalConfig.cache.userInfoKey, state.userInfo); |
| ... | ... | @@ -222,6 +315,7 @@ export const useUserStore = defineStore('user', { |
| 222 | 315 | deserialize: (value) => { |
| 223 | 316 | return { |
| 224 | 317 | token: uni.getStorageSync(globalConfig.cache.tokenKey) || '', |
| 318 | + refreshToken: uni.getStorageSync(globalConfig.cache.refreshTokenKey) || '', // 新增:读取Refresh Token | |
| 225 | 319 | userId: uni.getStorageSync(globalConfig.cache.userIdKey) || '', |
| 226 | 320 | expireTime: uni.getStorageSync(globalConfig.cache.expireTimeKey) || 0, |
| 227 | 321 | userInfo: uni.getStorageSync(globalConfig.cache.userInfoKey) || {}, |
| ... | ... | @@ -231,6 +325,6 @@ export const useUserStore = defineStore('user', { |
| 231 | 325 | }; |
| 232 | 326 | } |
| 233 | 327 | }, |
| 234 | - paths: [] | |
| 328 | + // paths: [] | |
| 235 | 329 | } |
| 236 | 330 | }); |
| 237 | 331 | \ No newline at end of file | ... | ... |