Commit 9b30ab8cacdb5d3bcc79ee4b39db40269e8fb6d3

Authored by 刘淇
1 parent 993d98fa

新增快速工单,原版

@@ -61,4 +61,8 @@ page { @@ -61,4 +61,8 @@ page {
61 //padding-bottom: env(safe-area-inset-bottom); 61 //padding-bottom: env(safe-area-inset-bottom);
62 /* #endif */ 62 /* #endif */
63 } 63 }
  64 +.commonPageLRpadding{
  65 + padding-left: 15px;
  66 + padding-right: 15px;
  67 +}
64 </style> 68 </style>
65 \ No newline at end of file 69 \ No newline at end of file
api/common.js
  1 +
  2 +import { post, get } from '@/common/utils/request';
  3 +
  4 +
  5 +/**
  6 + * 根据经纬度获取道路接口
  7 + * @param {Object} params {mobile, password, code}
  8 + * @returns {Promise}
  9 + */
  10 +export const getRoadListByLatLng = (params) => {
  11 + return get('/app-api/bpm/garden/workorder/listRoadInfo', params);
  12 +};
  13 +
  14 +// export const fileUpload = (params) => {
  15 +// return post('/app-api/infra/file/upload', params);
  16 +// };
  17 + export const fileUpload = '/app-api/infra/file/upload'
  18 +
  19 +
  20 +
api/quick-order/quick-order.js 0 → 100644
  1 +import { post, get } from '@/common/utils/request';
  2 +
  3 +// work_name
  4 +/**
  5 + * 快速工单 所有工单列表接口
  6 + * @param {Object} params {mobile, password, code}
  7 + * @returns {Promise}
  8 + */
  9 +export const workorderPage = (params) => {
  10 + return get('/app-api/bpm/garden/workorder/page', params);
  11 +};
  12 +
  13 +
  14 +/**
  15 + * 快速工单 创建
  16 + * @returns {Promise}
  17 + */
  18 +export const createQuick = (data) => {
  19 + return post('/app-api/bpm/garden/workorder/createQuick',data);
  20 +};
  21 +
  22 +/**
  23 + * 快速工单 获取详情
  24 + * @returns {Promise}
  25 + */
  26 +export const inspectionPlanDetail = (params) => {
  27 + return get('/app-api/bpm/garden/workorder/get',params);
  28 +};
api/upload.js
  1 +
  2 +import globalConfig from '@/common/config/global';
  3 +import cache from '@/common/utils/cache';
  4 +
  5 +const fileUpload = '/app-api/infra/file/upload'
  6 +
  7 +export const uploadFilePromise = (url) => {
  8 + return new Promise((resolve, reject) => {
  9 + let a = uni.uploadFile({
  10 + url: `${globalConfig.api.baseUrl}${fileUpload}`,
  11 + filePath: url,
  12 + name: 'file',
  13 + formData: {
  14 + user: 'test',
  15 + },
  16 + success: (res) => {
  17 + setTimeout(() => {
  18 + resolve(res.data.data);
  19 + }, 1000);
  20 + },
  21 + });
  22 + });
  23 +};
0 \ No newline at end of file 24 \ No newline at end of file
api/user.js
@@ -39,5 +39,5 @@ export const moduleList = () =&gt; { @@ -39,5 +39,5 @@ export const moduleList = () =&gt; {
39 * @returns {Promise} 39 * @returns {Promise}
40 */ 40 */
41 export const getSimpleDictDataList = () => { 41 export const getSimpleDictDataList = () => {
42 - return get({ url: '/system/dict-data/simple-list' }) 42 + return get('/admin-api/system/dict-data/simple-list')
43 } 43 }
common/config/global.js
@@ -23,7 +23,7 @@ export default { @@ -23,7 +23,7 @@ export default {
23 expireTimeKey: 'jcss_token_expire', 23 expireTimeKey: 'jcss_token_expire',
24 userIdKey:'jcss_user_id', 24 userIdKey:'jcss_user_id',
25 moduleListKey:'jcss_module_list', 25 moduleListKey:'jcss_module_list',
26 - 26 + dictDataKey:'jcss_dict_data'
27 }, 27 },
28 appName: 'JCSS管理系统', 28 appName: 'JCSS管理系统',
29 tokenExpireTime: 7 * 24 * 60 * 60 * 1000 29 tokenExpireTime: 7 * 24 * 60 * 60 * 1000
common/utils/upload.js
  1 +// @/common/utils/upload.ts
1 import globalConfig from '@/common/config/global'; 2 import globalConfig from '@/common/config/global';
2 import cache from '@/common/utils/cache'; 3 import cache from '@/common/utils/cache';
  4 +import { useUserStore } from '@/pinia/user';
  5 +import { fileUpload } from '@/api/common';
3 6
4 -export const uploadImages = (files, options = {}) => {  
5 - const opts = { count: 9, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], showLoading: true, ...options };  
6 - const token = cache.get(globalConfig.cache.tokenKey); 7 +// 统一的多文件上传方法(单/多图都走这个)
  8 +export const uploadImages = async (options) => {
  9 + const {
  10 + filePaths,
  11 + fileKey = 'file',
  12 + header = {},
  13 + ignoreError = true // 忽略单张失败,继续上传其他
  14 + } = options;
7 15
8 - if (!token) {  
9 - uni.showToast({ title: '请先登录', icon: 'none' });  
10 - return Promise.reject('未登录'); 16 + if (!filePaths || filePaths.length === 0) {
  17 + uni.showToast({title: '请选择要上传的图片', icon: 'none'});
  18 + return [];
11 } 19 }
12 20
13 - if (files.length > opts.count) {  
14 - uni.showToast({ title: `最多上传${opts.count}张`, icon: 'none' });  
15 - return Promise.reject('超出数量');  
16 - }  
17 -  
18 - if (opts.showLoading) uni.showLoading({ title: '上传中...' }); 21 + const userStore = useUserStore()
  22 + const defaultHeader = {
  23 + 'Content-Type': 'multipart/form-data',
  24 + 'Authorization': 'Bearer ' + (userStore.token || cache.get(globalConfig.cache.tokenKey) || '')
  25 + };
  26 + const finalHeader = {...defaultHeader, ...header};
  27 + const resultUrls = []; // 最终返回的URL数组
19 28
20 - const uploadPromises = files.map(filePath => {  
21 - return new Promise((resolve, reject) => {  
22 - uni.uploadFile({  
23 - url: globalConfig.api.uploadUrl,  
24 - filePath,  
25 - name: 'file',  
26 - header: { 'Authorization': `Bearer ${token}` },  
27 - formData: opts.formData || {},  
28 - success: (res) => {  
29 - const data = JSON.parse(res.data);  
30 - if (data.code === 200) resolve(data.data);  
31 - else {  
32 - uni.showToast({ title: data.msg || '上传失败', icon: 'none' });  
33 - reject(data); 29 + // 遍历上传(单/多图统一遍历逻辑)
  30 + for (let i = 0; i < filePaths.length; i++) {
  31 + try {
  32 + const url = await new Promise((resolve, reject) => {
  33 + uni.uploadFile({
  34 + url: `${globalConfig.api.baseUrl}${fileUpload}`,
  35 + filePath: filePaths[i],
  36 + name: fileKey,
  37 + header: finalHeader,
  38 + success: (res) => {
  39 + try {
  40 + const data = JSON.parse(res.data);
  41 + if (data.code === 0) {
  42 + resolve(data.data); // 接口返回的URL在data.data中
  43 + } else {
  44 + reject(new Error(data.msg || '上传失败'));
  45 + }
  46 + } catch (e) {
  47 + reject(new Error('解析返回数据失败'));
  48 + }
  49 + },
  50 + fail: (err) => {
  51 + reject(new Error(err.errMsg || '网络错误'));
34 } 52 }
35 - },  
36 - fail: (err) => {  
37 - uni.showToast({ title: '上传失败', icon: 'none' });  
38 - reject(err);  
39 - } 53 + });
40 }); 54 });
41 - });  
42 - });  
43 -  
44 - return Promise.all(uploadPromises)  
45 - .then(results => {  
46 - if (opts.showLoading) uni.hideLoading();  
47 - return results;  
48 - })  
49 - .catch(err => {  
50 - if (opts.showLoading) uni.hideLoading();  
51 - return Promise.reject(err);  
52 - });  
53 -}; 55 + resultUrls.push(url); // 成功的URL加入数组
  56 + } catch (err) {
  57 + console.error(`第${i+1}张图片上传失败:`, err);
  58 + if (!ignoreError) break; // 不忽略错误则终止上传
  59 + }
  60 + }
54 61
55 -export const chooseAndUploadImages = (options = {}) => {  
56 - const opts = { count: 9, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], ...options };  
57 - return new Promise((resolve, reject) => {  
58 - uni.chooseImage({  
59 - count: opts.count,  
60 - sizeType: opts.sizeType,  
61 - sourceType: opts.sourceType,  
62 - success: (res) => {  
63 - uploadImages(res.tempFilePaths, opts).then(resolve).catch(reject);  
64 - },  
65 - fail: reject  
66 - });  
67 - }); 62 + return resultUrls; // 始终返回数组(单图返回长度1,多图返回对应长度,无成功则返回空数组)
68 }; 63 };
69 \ No newline at end of file 64 \ No newline at end of file
components/upload-image/upload-image.vue
1 <template> 1 <template>
2 - <view class="upload-image">  
3 - <view class="image-list">  
4 - <view class="image-item" v-for="(item, index) in imageList" :key="index">  
5 - <image class="image" :src="item.url || item" mode="aspectFill"></image>  
6 - <view class="delete-btn" @click="deleteImage(index)">  
7 - <u-icon name="close" color="#fff"></u-icon>  
8 - </view>  
9 - </view>  
10 - <view class="upload-btn" v-if="imageList.length < maxCount" @click="chooseImage">  
11 - <u-icon name="plus" size="40" color="#999"></u-icon>  
12 - <view class="upload-text">上传图片</view> 2 + <view class="upload-images-container">
  3 + <!-- 已上传图片预览(uView Album组件) -->
  4 + <up-album
  5 + v-if="imageList.length > 0"
  6 + :list="imageList"
  7 + :max-count="maxCount"
  8 + :show-delete="showDelete"
  9 + @delete="handleDelete"
  10 + @preview="handlePreview"
  11 + :border-radius="8"
  12 + :column="column"
  13 + :gap="15"
  14 + ></up-album>
  15 +
  16 + <!-- 上传按钮 -->
  17 + <up-upload
  18 + class="upload-btn"
  19 + :disabled="imageList.length >= maxCount"
  20 + :multiple="true"
  21 + :max-count="maxCount - imageList.length"
  22 + @after-read="handleAfterRead"
  23 + :previewFullImage="true"
  24 + >
  25 + <view class="upload-btn-inner">
  26 + <up-icon name="plus" color="#999" size="24"></up-icon>
  27 + <text class="upload-tips" v-if="imageList.length < maxCount">上传图片</text>
  28 + <text class="upload-tips" v-else>已达上限</text>
13 </view> 29 </view>
14 - </view>  
15 - <view class="tips" v-if="tips">{{ tips }}</view> 30 + </up-upload>
16 </view> 31 </view>
17 </template> 32 </template>
18 33
19 -<script setup>  
20 -import { ref, defineProps, defineEmits, watch } from 'vue';  
21 -import { chooseAndUploadImages } from '@/common/utils/upload'; 34 +<script setup lang="ts">
  35 +import { ref, watch, defineProps, defineEmits, withDefaults } from 'vue';
  36 +import { uploadImages } from '@/common/utils/upload.js';
  37 +
  38 +
  39 +// 定义Props
  40 +const props = withDefaults(
  41 + defineProps<{
  42 + // 已上传图片列表(双向绑定)
  43 + value: string[];
  44 + // 最大上传数量
  45 + maxCount?: number;
  46 + // 是否显示删除按钮
  47 + showDelete?: boolean;
  48 + // 相册列数
  49 + column?: number;
  50 + // 上传文件key名
  51 + fileKey?: string;
  52 + // 自定义请求头(覆盖默认)
  53 + header?: Record<string, string>;
  54 + // 上传失败是否继续(多图时)
  55 + ignoreError?: boolean;
  56 + }>(),
  57 + {
  58 + maxCount: 9,
  59 + showDelete: true,
  60 + column: 3,
  61 + fileKey: 'file',
  62 + ignoreError: true
  63 + }
  64 +);
22 65
23 -const props = defineProps({  
24 - maxCount: { type: Number, default: 9 },  
25 - modelValue: { type: Array, default: () => [] },  
26 - tips: { type: String, default: '最多上传9张图片' },  
27 - showTips: { type: Boolean, default: true }  
28 -}); 66 +// 定义Emits
  67 +const emit = defineEmits<{
  68 + // 双向绑定更新图片列表
  69 + (e: 'update:value', val: string[]): void;
  70 + // 单张/多张上传成功
  71 + (e: 'upload-success', urls: string[]): void;
  72 + // 上传失败
  73 + (e: 'upload-error', err: any): void;
  74 + // 删除图片
  75 + (e: 'delete-image', index: number): void;
  76 + // 预览图片
  77 + (e: 'preview-image', index: number): void;
  78 +}>();
29 79
30 -const emit = defineEmits(['update:modelValue', 'change']);  
31 -const imageList = ref([...props.modelValue]); 80 +// 内部维护的图片列表
  81 +const imageList = ref([...props.value]);
32 82
33 -watch(() => props.modelValue, (newVal) => imageList.value = [...newVal], { deep: true }); 83 +// 监听父组件传入的value变化
  84 +watch(
  85 + () => props.value,
  86 + (newVal) => {
  87 + imageList.value = [...newVal];
  88 + },
  89 + { immediate: true }
  90 +);
  91 +
  92 +// 监听内部列表变化,同步给父组件
  93 +watch(
  94 + () => imageList.value,
  95 + (newVal) => {
  96 + emit('update:value', [...newVal]);
  97 + },
  98 + { deep: true }
  99 +);
  100 +
  101 +/**
  102 + * 选择图片后触发(上传核心逻辑)
  103 + */
  104 +const handleAfterRead = async (event: any) => {
  105 + // 获取选择的图片临时路径
  106 + const filePaths = event.file.map((item: any) => item.url);
34 107
35 -const chooseImage = async () => {  
36 try { 108 try {
37 - const res = await chooseAndUploadImages({ count: props.maxCount - imageList.value.length });  
38 - imageList.value = [...imageList.value, ...res];  
39 - emit('update:modelValue', imageList.value);  
40 - emit('change', imageList.value); 109 + uni.showLoading({ title: '上传中...' });
  110 + // 调用公共上传方法
  111 + const uploadUrls = await uploadImages({
  112 + filePaths,
  113 + fileKey: props.fileKey,
  114 + header: props.header,
  115 + ignoreError: props.ignoreError
  116 + });
  117 +
  118 + if (uploadUrls.length > 0) {
  119 + // 合并到已上传列表
  120 + imageList.value = [...imageList.value, ...uploadUrls];
  121 + // 通知父组件上传成功
  122 + emit('upload-success', uploadUrls);
  123 + uni.showToast({ title: `成功上传${uploadUrls.length}张图片`, icon: 'success' });
  124 + }
41 } catch (err) { 125 } catch (err) {
42 - console.error('上传失败:', err); 126 + emit('upload-error', err);
  127 + console.error('图片上传失败:', err);
  128 + } finally {
  129 + uni.hideLoading();
43 } 130 }
44 }; 131 };
45 132
46 -const deleteImage = (index) => { 133 +/**
  134 + * 删除图片
  135 + */
  136 +const handleDelete = (index: number) => {
47 imageList.value.splice(index, 1); 137 imageList.value.splice(index, 1);
48 - emit('update:modelValue', imageList.value);  
49 - emit('change', imageList.value); 138 + emit('delete-image', index);
  139 + uni.showToast({ title: '删除成功', icon: 'success' });
  140 +};
  141 +
  142 +/**
  143 + * 预览图片
  144 + */
  145 +const handlePreview = (index: number) => {
  146 + emit('preview-image', index);
  147 + // 原生预览(可选)
  148 + uni.previewImage({
  149 + urls: imageList.value,
  150 + current: imageList.value[index]
  151 + });
50 }; 152 };
51 </script> 153 </script>
52 154
53 -<style scoped>  
54 -.upload-image { width: 100%; }  
55 -.image-list { display: flex; flex-wrap: wrap; gap: 10rpx; }  
56 -.image-item { position: relative; width: 200rpx; height: 200rpx; border-radius: 8rpx; overflow: hidden; }  
57 -.image { width: 100%; height: 100%; }  
58 -.delete-btn { position: absolute; top: 0; right: 0; width: 40rpx; height: 40rpx; background: rgba(0,0,0,0.5); border-radius: 50%; display: flex; align-items: center; justify-content: center; }  
59 -.upload-btn { width: 200rpx; height: 200rpx; border: 1rpx dashed #ddd; border-radius: 8rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #f9f9f9; }  
60 -.upload-text { font-size: 24rpx; color: #999; margin-top: 10rpx; }  
61 -.tips { font-size: 24rpx; color: #999; margin-top: 10rpx; } 155 +<style lang="scss" scoped>
  156 +.upload-images-container {
  157 + padding: 10rpx 0;
  158 +}
  159 +
  160 +// 上传按钮样式
  161 +.upload-btn {
  162 + margin-top: 15rpx;
  163 + width: 200rpx;
  164 + height: 200rpx;
  165 + border: 1px dashed #e5e5e5;
  166 + border-radius: 8rpx;
  167 + display: flex;
  168 + align-items: center;
  169 + justify-content: center;
  170 + background-color: #f9f9f9;
  171 +
  172 + .upload-btn-inner {
  173 + display: flex;
  174 + flex-direction: column;
  175 + align-items: center;
  176 + justify-content: center;
  177 + }
  178 +
  179 + .upload-tips {
  180 + font-size: 24rpx;
  181 + color: #999;
  182 + margin-top: 10rpx;
  183 + }
  184 +}
  185 +
  186 +// 适配Album组件样式
  187 +:deep(.u-album__item) {
  188 + border-radius: 8rpx !important;
  189 + overflow: hidden;
  190 +}
  191 +
  192 +:deep(.u-album__delete) {
  193 + background-color: rgba(0, 0, 0, 0.5) !important;
  194 + border-radius: 50% !important;
  195 + width: 40rpx !important;
  196 + height: 40rpx !important;
  197 + top: 0 !important;
  198 + right: 0 !important;
  199 +}
62 </style> 200 </style>
63 \ No newline at end of file 201 \ No newline at end of file
@@ -4,6 +4,8 @@ import uviewPlus from &#39;@/uni_modules/uview-plus&#39; @@ -4,6 +4,8 @@ import uviewPlus from &#39;@/uni_modules/uview-plus&#39;
4 // 导入 Pinia 实例(你的 stores/index.js 导出的 pinia) 4 // 导入 Pinia 实例(你的 stores/index.js 导出的 pinia)
5 import pinia from '@/pinia/index' 5 import pinia from '@/pinia/index'
6 import EmptyView from '@/components/empty-view/empty-view.vue'; 6 import EmptyView from '@/components/empty-view/empty-view.vue';
  7 +import UploadImage from '@/components/upload-image/upload-image.vue';
  8 +
7 // #ifdef VUE3 9 // #ifdef VUE3
8 import { createSSRApp } from 'vue' 10 import { createSSRApp } from 'vue'
9 11
@@ -17,6 +19,7 @@ export function createApp() { @@ -17,6 +19,7 @@ export function createApp() {
17 // 4. 注册 Pinia(核心:在 app 挂载前注册) 19 // 4. 注册 Pinia(核心:在 app 挂载前注册)
18 app.use(pinia) 20 app.use(pinia)
19 app.component('EmptyView', EmptyView) 21 app.component('EmptyView', EmptyView)
  22 + app.component('UploadImage', UploadImage)
20 // 5. 返回 app + pinia(可选,便于调试) 23 // 5. 返回 app + pinia(可选,便于调试)
21 return { 24 return {
22 app, 25 app,
manifest.json
@@ -55,6 +55,17 @@ @@ -55,6 +55,17 @@
55 "urlCheck" : false 55 "urlCheck" : false
56 }, 56 },
57 "usingComponents" : true, 57 "usingComponents" : true,
  58 + "permission" : {
  59 + "scope.userLocation" : {
  60 + "desc" : "定位"
  61 + }
  62 + },
  63 + "requiredPrivateInfos" : [
  64 + "getLocation",
  65 + "chooseLocation",
  66 + "startLocationUpdate",
  67 + "onLocationChange"
  68 + ],
58 "mergeVirtualHostAttributes" : true 69 "mergeVirtualHostAttributes" : true
59 }, 70 },
60 "mp-alipay" : { 71 "mp-alipay" : {
pages-sub/daily/patrol-manage/patrol-plan/index.vue
@@ -32,7 +32,7 @@ @@ -32,7 +32,7 @@
32 ></up-search> 32 ></up-search>
33 </view> 33 </view>
34 34
35 - <!-- 修复点1:绑定total + 修正ref名称 + 移除auto=false(改用默认auto=true更易控制) --> 35 +
36 <z-paging 36 <z-paging
37 ref="paging" 37 ref="paging"
38 v-model="planList" 38 v-model="planList"
@@ -98,9 +98,9 @@ const searchValue = ref(&#39;&#39;); @@ -98,9 +98,9 @@ const searchValue = ref(&#39;&#39;);
98 // 分页相关 98 // 分页相关
99 const pageNo = ref(1); 99 const pageNo = ref(1);
100 const pageSize = ref(10); 100 const pageSize = ref(10);
101 -const total = ref(0); 101 +
102 const planList = ref([]); 102 const planList = ref([]);
103 -const paging = ref(null); // 修复点2:ref名称与模板中一致 103 +const paging = ref(null);
104 // 养护级别/计划类型映射 104 // 养护级别/计划类型映射
105 const levelMap = { 105 const levelMap = {
106 11: '一级养护', 106 11: '一级养护',
@@ -119,8 +119,6 @@ const handleTabChange = (val) =&gt; { @@ -119,8 +119,6 @@ const handleTabChange = (val) =&gt; {
119 console.log(activeTab.value) 119 console.log(activeTab.value)
120 activeTab.value = val.id 120 activeTab.value = val.id
121 searchValue.value = '' 121 searchValue.value = ''
122 - // paging.value.reload()  
123 - // queryList(pageNo, pageSize)  
124 paging.value.reload() 122 paging.value.reload()
125 }; 123 };
126 // 搜索/清空搜索 124 // 搜索/清空搜索
@@ -133,7 +131,7 @@ const handleSearchClear = () =&gt; { @@ -133,7 +131,7 @@ const handleSearchClear = () =&gt; {
133 // searchValue.value = '' 131 // searchValue.value = ''
134 paging.value.reload() 132 paging.value.reload()
135 }; 133 };
136 -// 加载数据(核心修复) 134 +// 加载数据
137 const queryList = async (pageNo, pageSize) => { 135 const queryList = async (pageNo, pageSize) => {
138 try { 136 try {
139 const params = { 137 const params = {
pages-sub/daily/quick-order/add-order.vue 0 → 100644
  1 +<template>
  2 + <view class="u-page">
  3 + <!-- 工单表单容器 -->
  4 + <view class="work-order-form-content commonPageLRpadding">
  5 + <!-- uview-plus表单(Vue3 适配) -->
  6 + <up-form
  7 + label-position="left"
  8 + :model="workOrderForm"
  9 + ref="workOrderFormRef"
  10 + labelWidth="220rpx"
  11 + :rules="workOrderFormRules"
  12 + >
  13 + <!-- 1. 工单位置(地图选择) -->
  14 + <up-form-item
  15 + label="工单位置"
  16 + prop="workLocation"
  17 + border-bottom
  18 + required
  19 + @click="chooseWorkLocation(); hideKeyboard()"
  20 + >
  21 + <up-input
  22 + v-model="workOrderForm.workLocation"
  23 + border="none"
  24 + readonly
  25 + suffix-icon="map-fill"
  26 + placeholder="点击选择工单位置"
  27 + ></up-input>
  28 + </up-form-item>
  29 +
  30 + <!-- 2. 道路名称(下拉框:位置选择后才可点击) -->
  31 + <up-form-item
  32 + label="道路名称"
  33 + prop="roadName"
  34 + border-bottom
  35 + required
  36 + @click="workOrderForm.workLocation ? (showRoadName = true, hideKeyboard()) : uni.showToast({title: '请先选择工单位置', icon: 'none'})"
  37 + >
  38 + <up-input
  39 + v-model="workOrderForm.roadName"
  40 + disabled
  41 + disabled-color="#ffffff"
  42 + placeholder="请先选择工单位置"
  43 + border="none"
  44 + :placeholder-style="workOrderForm.workLocation ? '' : 'color:#999;'"
  45 + ></up-input>
  46 + <template #right>
  47 + <up-icon name="arrow-right" size="16" :color="workOrderForm.workLocation ? '#333' : '#999'"></up-icon>
  48 + </template>
  49 + </up-form-item>
  50 +
  51 + <!-- 3. 工单名称(下拉框) -->
  52 + <up-form-item
  53 + label="工单名称"
  54 + prop="orderName"
  55 + border-bottom
  56 + required
  57 + @click="showOrderName = true; hideKeyboard()"
  58 + >
  59 + <up-input
  60 + v-model="workOrderForm.orderName"
  61 + disabled
  62 + disabled-color="#ffffff"
  63 + placeholder="请选择工单名称"
  64 + border="none"
  65 + ></up-input>
  66 + <template #right>
  67 + <up-icon name="arrow-right" size="16"></up-icon>
  68 + </template>
  69 + </up-form-item>
  70 +
  71 + <!-- 4. 情况描述(文本域) -->
  72 + <up-form-item
  73 + label="情况描述"
  74 + prop="problemDesc"
  75 + required
  76 + >
  77 + <up-textarea
  78 + placeholder="请输入情况描述(最多150字)"
  79 + v-model="workOrderForm.problemDesc"
  80 + count
  81 + maxlength="150"
  82 + rows="4"
  83 + ></up-textarea>
  84 + </up-form-item>
  85 + </up-form>
  86 +
  87 + <!-- 道路名称下拉弹窗 -->
  88 + <up-action-sheet
  89 + :show="showRoadName"
  90 + :actions="roadNameList"
  91 + title="请选择道路名称"
  92 + @close="showRoadName = false"
  93 + @select="handleRoadNameSelect"
  94 + ></up-action-sheet>
  95 +
  96 + <!-- 工单名称下拉弹窗 -->
  97 + <up-action-sheet
  98 + :show="showOrderName"
  99 + :actions="orderNameList"
  100 + title="请选择工单名称"
  101 + @close="showOrderName = false"
  102 + @select="handleOrderNameSelect"
  103 + ></up-action-sheet>
  104 + </view>
  105 +
  106 + <!-- 图片分类标签 -->
  107 + <up-tabs :list="imgTabList" @click="switchImgTab"></up-tabs>
  108 +
  109 + <!-- 图片上传组件(根据选中标签显示对应列表) -->
  110 + <view class="img-upload-wrap" v-if="activeImgTab === '1'">
  111 + <up-form-item label="问题照片" prop="problemImgs" required>
  112 + <up-upload
  113 + :file-list="problemImgsList"
  114 + @after-read="(event) => uploadImgs(event, 'problemImgsList')"
  115 + @delete="(event) => deleteImg(event, 'problemImgsList')"
  116 + multiple
  117 + :max-count="3"
  118 + upload-text="选择问题照片"
  119 + ></up-upload>
  120 + </up-form-item>
  121 + </view>
  122 + <view class="img-upload-wrap" v-else>
  123 + <up-form-item label="完成照片" prop="completeImgs" required>
  124 + <up-upload
  125 + :file-list="completeImgsList"
  126 + @after-read="(event) => uploadImgs(event, 'completeImgsList')"
  127 + @delete="(event) => deleteImg(event, 'completeImgsList')"
  128 + multiple
  129 + :max-count="3"
  130 + :sizeType="['compressed']"
  131 + upload-text="选择完成照片"
  132 + ></up-upload>
  133 + </up-form-item>
  134 + </view>
  135 +
  136 + <!-- 处理结果描述(独立显示) -->
  137 + <view class="handle-result-wrap">
  138 + <up-form-item
  139 + label="处理结果"
  140 + prop="handleResultDesc"
  141 + required
  142 + >
  143 + <up-textarea
  144 + placeholder="请输入处理结果描述(最多200字)"
  145 + v-model="workOrderForm.handleResultDesc"
  146 + count
  147 + maxlength="200"
  148 + rows="4"
  149 + ></up-textarea>
  150 + </up-form-item>
  151 + </view>
  152 +
  153 + <!-- 底部提交按钮 -->
  154 + <view class="fixed-bottom-btn-wrap">
  155 + <up-button
  156 + type="primary"
  157 + text="提交工单"
  158 + @click="submitWorkOrder"
  159 + :loading="isSubmitting"
  160 + :disabled="isSubmitting"
  161 + ></up-button>
  162 + </view>
  163 + </view>
  164 +</template>
  165 +
  166 +<script setup lang="ts">
  167 +import { ref, reactive, onMounted } from 'vue'
  168 +import type { UniFormRef, UniActionSheetSelectEvent, UploadFile, UploadDeleteEvent } from '@/uni_modules/uview-plus/types'
  169 +// 导入接口
  170 +import { getRoadListByLatLng } from '@/api/common'
  171 +// 导入统一的多文件上传方法
  172 +import { uploadImages } from '@/common/utils/upload';
  173 +import { createQuick } from '@/api/quick-order/quick-order'
  174 +
  175 +// ========== 基础变量定义(语义化命名) ==========
  176 +// 图片分类标签列表
  177 +const imgTabList = ref([
  178 + { name: '问题照片', id: '1' },
  179 + { name: '完成照片', id: '2' }
  180 +])
  181 +// 激活的图片标签(默认选中问题照片)
  182 +const activeImgTab = ref('1')
  183 +// 问题照片列表
  184 +const problemImgsList = ref<UploadFile[]>([])
  185 +// 完成照片列表
  186 +const completeImgsList = ref<UploadFile[]>([])
  187 +// 表单Ref(语义化:工单表单)
  188 +const workOrderFormRef = ref<UniFormRef>(null)
  189 +// 提交加载状态
  190 +const isSubmitting = ref(false)
  191 +// 弹窗显隐控制
  192 +const showRoadName = ref(false)
  193 +const showOrderName = ref(false)
  194 +// 下拉列表数据
  195 +const roadNameList = ref<any[]>([])
  196 +const orderNameList = ref([
  197 + { name: '绿地卫生', code: 'ORDER001' },
  198 + { name: '设施维修', code: 'ORDER002' },
  199 + { name: '垃圾清理', code: 'ORDER003' }
  200 +])
  201 +
  202 +// ========== 工单表单数据(语义化字段名) ==========
  203 +const workOrderForm = reactive({
  204 + roadId: 0, // 道路ID
  205 + roadName: '', // 道路名称
  206 + workLocation: '', // 工单位置
  207 + orderName: '', // 工单名称
  208 + problemDesc: '', // 情况描述
  209 + handleResultDesc: '', // 处理结果描述
  210 + lat: 0, // 纬度
  211 + lon: 0 // 经度
  212 +})
  213 +
  214 +// ========== 表单校验规则(修复trigger + 语义化) ==========
  215 +const workOrderFormRules = reactive({
  216 + workLocation: [
  217 + { type: 'string', required: true, message: '请选择工单位置', trigger: 'change' }
  218 + ],
  219 + roadName: [
  220 + { type: 'string', required: true, message: '请选择道路名称', trigger: 'change' }
  221 + ],
  222 + orderName: [
  223 + { type: 'string', required: true, message: '请选择工单名称', trigger: 'change' }
  224 + ],
  225 + problemDesc: [
  226 + { type: 'string', required: true, message: '请输入情况描述', trigger: 'change' },
  227 + { type: 'string', min: 3, max: 150, message: '情况描述需3-150字', trigger: 'change' }
  228 + ],
  229 + problemImgs: [
  230 + { required: true, message: '请上传问题照片', trigger: 'change' }
  231 + ],
  232 + completeImgs: [
  233 + { required: true, message: '请上传完成照片', trigger: 'change' }
  234 + ],
  235 + handleResultDesc: [
  236 + { type: 'string', required: true, message: '请输入处理结果描述', trigger: 'change' },
  237 + { type: 'string', min: 3, max: 200, message: '处理结果需3-200字', trigger: 'change' }
  238 + ]
  239 +})
  240 +
  241 +// ========== 方法定义(语义化命名 + 修复逻辑) ==========
  242 +/**
  243 + * 删除图片
  244 + * @param event 删除事件
  245 + * @param type 图片类型(problemImgsList/completeImgsList)
  246 + */
  247 +const deleteImg = (event: UploadDeleteEvent, type: string) => {
  248 + console.log('删除图片事件:', event, '类型:', type)
  249 + if (type === 'problemImgsList') {
  250 + problemImgsList.value.splice(event.index, 1)
  251 + } else if (type === 'completeImgsList') {
  252 + completeImgsList.value.splice(event.index, 1)
  253 + }
  254 + uni.showToast({ title: '图片删除成功', icon: 'success' })
  255 +}
  256 +
  257 +/**
  258 + * 上传图片(单/多图统一逻辑)
  259 + * @param event 上传事件
  260 + * @param type 图片类型(problemImgsList/completeImgsList)
  261 + */
  262 +const uploadImgs = async (event: { file: UploadFile | UploadFile[] }, type: string) => {
  263 + console.log('上传图片事件:', event, '类型:', type)
  264 + // 统一处理为数组格式
  265 + const fileList = Array.isArray(event.file) ? event.file : [event.file]
  266 + const targetImgList = type === 'problemImgsList' ? problemImgsList : completeImgsList
  267 +
  268 + // 1. 提取文件路径
  269 + const filePaths = fileList.map(item => item.url)
  270 + // 2. 添加上传中临时项
  271 + const tempItems: UploadFile[] = fileList.map(item => ({
  272 + ...item,
  273 + status: 'uploading' as const,
  274 + message: '上传中'
  275 + }))
  276 + const startIndex = targetImgList.value.length
  277 + targetImgList.value.push(...tempItems)
  278 +
  279 + try {
  280 + // 3. 调用统一上传方法
  281 + const uploadResultUrls = await uploadImages({
  282 + filePaths: filePaths,
  283 + ignoreError: true
  284 + })
  285 + console.log('上传成功的URL列表:', uploadResultUrls)
  286 +
  287 + // 4. 更新成功状态
  288 + uploadResultUrls.forEach((url, index) => {
  289 + if (targetImgList.value[startIndex + index]) {
  290 + targetImgList.value.splice(startIndex + index, 1, {
  291 + ...fileList[index],
  292 + status: 'success' as const,
  293 + message: '',
  294 + url: url
  295 + })
  296 + }
  297 + })
  298 +
  299 + // 5. 处理失败项
  300 + if (uploadResultUrls.length < fileList.length) {
  301 + const failCount = fileList.length - uploadResultUrls.length
  302 + for (let i = uploadResultUrls.length; i < fileList.length; i++) {
  303 + if (targetImgList.value[startIndex + i]) {
  304 + targetImgList.value.splice(startIndex + i, 1, {
  305 + ...fileList[i],
  306 + status: 'failed' as const,
  307 + message: '上传失败'
  308 + })
  309 + }
  310 + }
  311 + uni.showToast({ title: `成功上传${uploadResultUrls.length}张,失败${failCount}张`, icon: 'none' })
  312 + } else {
  313 + uni.showToast({ title: `成功上传${fileList.length}张图片`, icon: 'success' })
  314 + }
  315 + } catch (err) {
  316 + console.error('图片上传失败:', err)
  317 + // 标记所有为失败
  318 + for (let i = 0; i < fileList.length; i++) {
  319 + if (targetImgList.value[startIndex + i]) {
  320 + targetImgList.value.splice(startIndex + i, 1, {
  321 + ...fileList[i],
  322 + status: 'failed' as const,
  323 + message: '上传失败'
  324 + })
  325 + }
  326 + }
  327 + uni.showToast({ title: '图片上传失败,请重试', icon: 'none' })
  328 + }
  329 +}
  330 +
  331 +/**
  332 + * 切换图片标签
  333 + * @param item 标签项
  334 + */
  335 +const switchImgTab = (item: { name: string; id: string }) => {
  336 + console.log('切换图片标签:', item)
  337 + activeImgTab.value = item.id
  338 +}
  339 +
  340 +/**
  341 + * 选择工单位置(地图)- 核心修改:清空道路名称
  342 + */
  343 +const chooseWorkLocation = () => {
  344 + if (isSubmitting.value) return
  345 +
  346 + uni.authorize({
  347 + scope: 'scope.userLocation',
  348 + success: () => {
  349 + uni.chooseLocation({
  350 + success: async (res) => {
  351 + // ========== 核心新增:清空道路名称相关数据 ==========
  352 + workOrderForm.roadName = '' // 清空道路名称
  353 + workOrderForm.roadId = 0 // 清空道路ID
  354 + roadNameList.value = [] // 清空道路名称列表
  355 +
  356 + // 更新工单位置
  357 + workOrderForm.workLocation = res.name
  358 + workOrderForm.lat = res.latitude
  359 + workOrderForm.lon = res.longitude
  360 +
  361 + // 触发工单位置和道路名称的表单校验更新
  362 + workOrderFormRef.value?.validateField('workLocation')
  363 + workOrderFormRef.value?.validateField('roadName')
  364 +
  365 + try {
  366 + uni.showLoading({ title: '获取道路名称中...' })
  367 + const roadRes = await getRoadListByLatLng({
  368 + companyCode: 'sls',
  369 + latitude: res.latitude,
  370 + longitude: res.longitude
  371 + })
  372 + uni.hideLoading()
  373 +
  374 + if (Array.isArray(roadRes)) {
  375 + roadNameList.value = roadRes.map((item: any) => ({
  376 + name: item.roadName || '',
  377 + code: item.roadCode || '',
  378 + id: item.roadId || 0
  379 + }))
  380 + } else {
  381 + roadNameList.value = [{ name: '未查询到道路名称', code: '', id: 0 }]
  382 + uni.showToast({ title: '未查询到该位置的道路信息', icon: 'none' })
  383 + }
  384 + } catch (err) {
  385 + uni.hideLoading()
  386 + console.error('获取道路名称失败:', err)
  387 + uni.showToast({ title: '获取道路名称失败,请重试', icon: 'none' })
  388 + roadNameList.value = [{ name: '获取失败,请重新选择位置', code: '', id: 0 }]
  389 + }
  390 + },
  391 + fail: (err) => {
  392 + console.error('选择位置失败:', err)
  393 + uni.showToast({ title: '选择位置失败:' + err.errMsg, icon: 'none' })
  394 + }
  395 + })
  396 + },
  397 + fail: (err) => {
  398 + console.error('位置授权失败:', err)
  399 + uni.showModal({
  400 + title: '权限提示',
  401 + content: '需要获取您的位置权限才能选择工单位置,请前往设置开启',
  402 + confirmText: '去设置',
  403 + cancelText: '取消',
  404 + success: (res) => {
  405 + if (res.confirm) {
  406 + uni.openSetting({
  407 + success: (settingRes) => {
  408 + if (settingRes.authSetting['scope.userLocation']) {
  409 + uni.showToast({ title: '权限已开启,请重新选择位置', icon: 'none' })
  410 + }
  411 + }
  412 + })
  413 + }
  414 + }
  415 + })
  416 + }
  417 + })
  418 +}
  419 +
  420 +/**
  421 + * 选择道路名称
  422 + * @param e 选择事件
  423 + */
  424 +const handleRoadNameSelect = (e: UniActionSheetSelectEvent) => {
  425 + console.log('选择道路名称:', e)
  426 + workOrderForm.roadName = e.name
  427 + workOrderForm.roadId = e.code
  428 + showRoadName.value = false
  429 + workOrderFormRef.value?.validateField('roadName')
  430 +}
  431 +
  432 +/**
  433 + * 选择工单名称
  434 + * @param e 选择事件
  435 + */
  436 +const handleOrderNameSelect = (e: UniActionSheetSelectEvent) => {
  437 + workOrderForm.orderName = e.name
  438 + showOrderName.value = false
  439 + workOrderFormRef.value?.validateField('orderName')
  440 +}
  441 +
  442 +/**
  443 + * 隐藏键盘
  444 + */
  445 +const hideKeyboard = () => {
  446 + uni.hideKeyboard()
  447 +}
  448 +
  449 +/**
  450 + * 提取图片URL数组
  451 + * @param imgList 图片列表
  452 + * @returns URL数组
  453 + */
  454 +const getImgUrlList = (imgList: UploadFile[]) => {
  455 + console.log('提取图片URL:', imgList)
  456 + return imgList.filter(item => item.status === 'success').map(item => item.url)
  457 +}
  458 +
  459 +/**
  460 + * 校验上传图片(兜底校验)
  461 + * @returns 是否校验通过
  462 + */
  463 +const validateUploadImgs = () => {
  464 + // 修复:补充返回值,避免函数无返回
  465 + if (activeImgTab.value === '1') {
  466 + const hasValidImgs = problemImgsList.value.some(item => item.status === 'success')
  467 + if (!hasValidImgs) {
  468 + uni.showToast({ title: '请上传至少1张问题照片', icon: 'none' })
  469 + return false
  470 + }
  471 + } else {
  472 + const hasValidcompleteImgs = completeImgsList.value.some(item => item.status === 'success')
  473 + if (!hasValidcompleteImgs) {
  474 + uni.showToast({ title: '请上传至少1张完成照片', icon: 'none' })
  475 + return false
  476 + }
  477 + }
  478 + return true
  479 +}
  480 +
  481 +/**
  482 + * 提交工单(修复核心:await位置 + 表单Ref名称错误)
  483 + */
  484 +const submitWorkOrder = async () => {
  485 + if (isSubmitting.value) {
  486 + uni.showToast({ title: '提交中,请稍候', icon: 'none' })
  487 + return
  488 + }
  489 +
  490 + try {
  491 + isSubmitting.value = true
  492 +
  493 + // 修复1:兜底校验图片
  494 + if (!validateUploadImgs()) {
  495 + isSubmitting.value = false
  496 + return
  497 + }
  498 +
  499 + // 修复2:使用正确的表单Ref名称(workOrderFormRef 而非 uFormRef)
  500 + // 修复3:将validate转为Promise形式,配合async/await
  501 + const valid = await new Promise((resolve) => {
  502 + workOrderFormRef.value?.validate((isValid) => {
  503 + resolve(isValid)
  504 + })
  505 + })
  506 +
  507 + if (!valid) {
  508 + uni.showToast({ title: '表单校验失败,请检查必填项', icon: 'none' })
  509 + isSubmitting.value = false
  510 + return
  511 + }
  512 +
  513 + // 构造提交数据
  514 + const submitData = {
  515 + roadId: workOrderForm.roadId,
  516 + roadName: workOrderForm.roadName,
  517 + imgs: getImgUrlList(problemImgsList.value), // 问题照片URL数组
  518 + longRangeImgList: getImgUrlList(completeImgsList.value), // 完成照片URL数组
  519 + remark: activeImgTab.value === '2' ? workOrderForm.handleResultDesc : workOrderForm.problemDesc,
  520 + latLonType: 2,
  521 + lat: workOrderForm.lat,
  522 + lon: workOrderForm.lon,
  523 + lonLatAddress: workOrderForm.workLocation,
  524 + pressingType: 2,
  525 + orderName: workOrderForm.orderName,
  526 + sourceId: 1,
  527 + sourceName: '园林',
  528 + thirdWorkNo: ''
  529 + }
  530 +
  531 + console.log('提交工单数据:', submitData)
  532 + // 修复4:await 现在在async函数中,可正常使用
  533 + await createQuick(submitData)
  534 + uni.showToast({ title: '工单提交成功', icon: 'success' })
  535 +
  536 + // 可选:提交成功后重置表单
  537 + // problemImgsList.value = []
  538 + // completeImgsList.value = []
  539 + // Object.assign(workOrderForm, {
  540 + // roadId: 0, roadName: '', workLocation: '', orderName: '',
  541 + // problemDesc: '', handleResultDesc: '', lat: 0, lon: 0
  542 + // })
  543 + // workOrderFormRef.value?.clearValidate()
  544 +
  545 + } catch (error) {
  546 + console.error('提交工单失败:', error)
  547 + uni.showToast({ title: '提交失败,请重试', icon: 'none' })
  548 + } finally {
  549 + isSubmitting.value = false
  550 + }
  551 +}
  552 +
  553 +// ========== 页面挂载初始化 ==========
  554 +onMounted(() => {
  555 + setTimeout(() => {
  556 + if (workOrderFormRef.value && !workOrderFormRef.value.rules) {
  557 + workOrderFormRef.value.setRules(workOrderFormRules)
  558 + console.log('工单表单规则初始化完成')
  559 + }
  560 + }, 200)
  561 +})
  562 +</script>
  563 +
  564 +<style lang="scss" scoped>
  565 +// 全局页面样式
  566 +.u-page {
  567 + background-color: #f5f5f5;
  568 + min-height: 100vh;
  569 + padding-bottom: 100rpx; // 给底部按钮留空间
  570 +}
  571 +
  572 +// 工单表单容器
  573 +.work-order-form-content {
  574 + background: #fff;
  575 + padding: 20rpx;
  576 + box-sizing: border-box;
  577 + margin-bottom: 20rpx;
  578 +}
  579 +
  580 +// 图片上传区域
  581 +.img-upload-wrap {
  582 + background: #fff;
  583 + padding: 20rpx;
  584 + margin-bottom: 20rpx;
  585 +
  586 + :deep(.u-upload) {
  587 + --u-upload-item-width: 200rpx;
  588 + --u-upload-item-height: 200rpx;
  589 + }
  590 +
  591 + :deep(.u-form-item) {
  592 + margin-bottom: 0;
  593 + }
  594 +}
  595 +
  596 +// 处理结果描述区域
  597 +.handle-result-wrap {
  598 + background: #fff;
  599 + padding: 0 20rpx 20rpx;
  600 + margin-bottom: 20rpx;
  601 + border-top: 1px solid #f0f0f0;
  602 +
  603 + :deep(.u-form-item) {
  604 + --u-form-item-label-font-size: 28rpx;
  605 + --u-form-item-content-font-size: 28rpx;
  606 + }
  607 +
  608 + :deep(.u-textarea) {
  609 + --u-textarea-font-size: 28rpx;
  610 + padding: 10rpx 0;
  611 + }
  612 +}
  613 +
  614 +// 提交按钮区域
  615 +.fixed-bottom-btn-wrap { // 修复:原样式类名是submit-btn-wrap,与模板不一致
  616 + position: fixed;
  617 + bottom: 0;
  618 + left: 0;
  619 + right: 0;
  620 + padding: 20rpx;
  621 + background: #fff;
  622 + box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
  623 +
  624 + :deep(.u-button) {
  625 + width: 100%;
  626 + height: 80rpx;
  627 + line-height: 80rpx;
  628 + font-size: 32rpx;
  629 + border-radius: 8rpx;
  630 + }
  631 +}
  632 +
  633 +// ========== 修复Label折行样式 ==========
  634 +:deep(.u-form-item__label) {
  635 + white-space: nowrap; // 强制不换行
  636 + text-overflow: ellipsis; // 超出省略
  637 + overflow: hidden; // 隐藏超出部分
  638 + line-height: 1.4; // 行高适配
  639 +}
  640 +
  641 +// 表单基础样式适配
  642 +:deep(.u-form-item) {
  643 + --u-form-item-label-font-size: 28rpx;
  644 + --u-form-item-content-font-size: 28rpx;
  645 + margin-bottom: 10rpx;
  646 +}
  647 +
  648 +:deep(.u-input) {
  649 + --u-input-font-size: 28rpx;
  650 +}
  651 +
  652 +:deep(.u-textarea) {
  653 + --u-textarea-font-size: 28rpx;
  654 + padding: 10rpx 0;
  655 +}
  656 +
  657 +:deep(.u-action-sheet) {
  658 + z-index: 9999 !important;
  659 +}
  660 +
  661 +:deep(.u-tabs) {
  662 + background: #fff;
  663 +}
  664 +</style>
0 \ No newline at end of file 665 \ No newline at end of file
pages-sub/daily/quick-order/index.vue
1 -<script setup lang="ts"> 1 +<template>
  2 + <view class="work-order-page">
  3 + <!-- 顶部固定搜索栏 -->
  4 + <up-sticky>
  5 + <view class="search-header">
  6 + <!-- 左侧下拉框 -->
  7 + <view class="dropdown-wrap">
  8 + <up-dropdown ref="uDropdownRef" @open="open" @close="close">
  9 + <up-dropdown-item
  10 + v-model="selectedSortValue"
  11 + title="距离"
  12 + :options="sortOptions"
  13 + @change="handleSortChange"
  14 + />
  15 + </up-dropdown>
  16 + </view>
2 17
3 -</script> 18 + <!-- 右侧搜索框 -->
  19 + <view class="search-input-wrap">
  20 + <up-search
  21 + v-model="searchValue"
  22 + placeholder="请输入关键字"
  23 + @search="handleSearch"
  24 + bg-color="#f5f5f5"
  25 + :clearabled="false"
  26 + :show-action="true"
  27 + actionText="搜索"
  28 + :animation="true"
  29 + @custom="handleSearch"
  30 + />
  31 + </view>
  32 + </view>
  33 + </up-sticky>
4 34
5 -<template> 35 + <!-- 列表容器 -->
  36 + <z-paging
  37 + ref="paging"
  38 + v-model="orderList"
  39 + @query="queryList"
  40 + :top="100"
  41 + :bottom="120"
  42 + >
  43 + <template #empty>
  44 + <view class="empty-tip">暂无工单数据</view>
  45 + </template>
  46 +
  47 + <!-- 修复:新增列表容器,配置顶部内边距 -->
  48 + <view class="order-list">
  49 + <view class="order-card" v-for="item in orderList" :key="item.orderNo">
  50 + <view class="order-item up-line-1">工单编号:{{ item.orderNo }}</view>
  51 + <view class="order-item up-line-1">工单位置:{{ item.roadName || '未填写' }}</view>
  52 + <view class="order-item up-line-1">工单名称:{{ item.orderName || '未填写' }}</view>
  53 + <view class="order-item up-line-1">情况描述:{{ item.remark || '无' }}</view>
  54 + <view class="order-footer">
  55 + <view class="submit-time up-line-1">提交时间:{{ timeFormat(item.createTime,'yyyy-mm-dd') }}</view>
  56 + <up-button
  57 + type="primary"
  58 + size="mini"
  59 + @click="handleDetail(item)"
  60 + :style="{ width: '80px', height: '28px', fontSize: '14px', borderRadius: 4 }"
  61 + >
  62 + 工单详情
  63 + </up-button>
  64 + </view>
  65 + </view>
  66 + </view>
  67 + </z-paging>
6 68
  69 + <!-- 修复:补充底部按钮样式层级 -->
  70 + <view class="fixed-bottom-btn-wrap">
  71 + <up-button type="primary" size="large" @click="handleAddOrder">
  72 + 新增工单
  73 + </up-button>
  74 + </view>
  75 + </view>
7 </template> 76 </template>
8 77
9 -<style scoped> 78 +<script setup>
  79 +import { ref } from 'vue';
  80 +import { workorderPage } from "@/api/quick-order/quick-order";
  81 +import { timeFormat } from '@/uni_modules/uview-plus';
  82 +// ========== 修复1:声明所有核心响应式变量 ==========
  83 +// 顶部/底部高度(rpx)
  84 +const headerHeight = 100;
  85 +const bottomBtnHeight = 120;
  86 +// 排序相关
  87 +const selectedSortValue = ref(1);
  88 +const sortOptions = ref([
  89 + { label: '默认排序', value: 1 },
  90 + { label: '距离优先', value: 2 }
  91 +]);
  92 +// 分页相关(核心:声明orderList)
  93 +const pageNo = ref(1);
  94 +const pageSize = ref(10);
  95 +const paging = ref(null);
  96 +const orderList = ref([]); // 修复:新增列表变量
  97 +// 搜索相关
  98 +const searchValue = ref('');
  99 +
  100 +
  101 +
  102 +// ========== 修复3:适配z-paging回调参数 ==========
  103 +const queryList = async (pageNo,pageSize) => {
  104 + try {
  105 + // 修复:z-paging的query回调参数是对象 {pageNo, pageSize}
  106 + const apiParams = {
  107 + searchContent: searchValue.value.trim() || '',
  108 + pageNo: pageNo,
  109 + pageSize: pageSize,
  110 + type:1 // 1 位置 2 工单名称 3 情况描述 4 工单编号
  111 + };
  112 + console.log('请求参数:', apiParams);
  113 + const res = await workorderPage(apiParams);
  114 + console.log('接口返回:', res);
  115 +
  116 +
  117 + paging.value.complete(res.list);
  118 + } catch (error) {
  119 + console.error('加载失败:', error);
  120 + paging.value?.complete(false);
  121 + uni.showToast({ title: '加载失败,请重试', icon: 'none' });
  122 + }
  123 +};
  124 +
  125 +// ========== 其他方法补充 ==========
  126 +const handleSortChange = (val) => {
  127 + console.log('排序变更:', val);
  128 + paging.value?.reload(); // 排序后刷新列表
  129 +};
  130 +
  131 +const handleSearch = (val) => {
  132 + console.log('搜索内容:', val);
  133 + searchValue.value = val;
  134 + paging.value?.reload(); // 搜索后刷新列表
  135 +};
  136 +
  137 +const handleDetail = (item) => {
  138 + console.log('工单详情:', item);
  139 + uni.navigateTo({
  140 + url: `/pages-sub/daily/quick-order/order-detail?id=${item.id}`
  141 + });
  142 +};
  143 +
  144 +const handleAddOrder = () => {
  145 + uni.navigateTo({
  146 + url: '/pages-sub/daily/quick-order/add-order'
  147 + });
  148 +};
  149 +
  150 +// 空方法(避免下拉框报错)
  151 +const open = () => {};
  152 +const close = () => {};
  153 +</script>
  154 +
  155 +<style scoped lang="scss">
  156 +// 修复:页面基础样式
  157 +.work-order-page {
  158 + min-height: 100vh;
  159 + background-color: #f5f5f5;
  160 + padding-bottom: env(safe-area-inset-bottom);
  161 + box-sizing: border-box;
  162 +}
  163 +
  164 +// 顶部搜索栏
  165 +.search-header {
  166 + display: flex;
  167 + align-items: center;
  168 + padding: 20rpx;
  169 + background-color: #fff;
  170 + border-bottom: 1px solid #f0f0f0;
  171 + height: v-bind(headerHeight + 'rpx');
  172 + box-sizing: border-box;
  173 +
  174 + .dropdown-wrap {
  175 + flex: 1;
  176 + margin-right: 20rpx;
  177 + }
  178 +
  179 + .search-input-wrap {
  180 + flex: 2;
  181 + }
  182 +}
  183 +
  184 +// 修复:列表容器样式(核心)
  185 +.order-list {
  186 + padding: 120rpx 20rpx 20rpx; // 顶部内边距避开搜索栏
  187 + box-sizing: border-box;
  188 +}
  189 +
  190 +// 空状态样式
  191 +.empty-tip {
  192 + text-align: center;
  193 + padding: 100rpx 0;
  194 + color: #999;
  195 + font-size: 28rpx;
  196 +}
  197 +
  198 +// 工单卡片样式
  199 +.order-card {
  200 + background-color: #fff;
  201 + border-radius: 12rpx;
  202 + padding: 24rpx;
  203 + margin-bottom: 20rpx;
  204 + box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  205 +
  206 + .order-item {
  207 + font-size: 28rpx;
  208 + color: #333;
  209 + line-height: 48rpx;
  210 + margin-bottom: 8rpx;
  211 + }
  212 +
  213 + .order-footer {
  214 + display: flex;
  215 + justify-content: space-between;
  216 + align-items: center;
  217 + margin-top: 12rpx;
  218 +
  219 + .submit-time {
  220 + font-size: 26rpx;
  221 + color: #666;
  222 + line-height: 40rpx;
  223 + }
  224 + }
  225 +}
10 226
11 </style> 227 </style>
12 \ No newline at end of file 228 \ No newline at end of file
pages-sub/daily/quick-order/order-detail.vue 0 → 100644
  1 +<template>
  2 + <view class="u-page">
  3 +
  4 +
  5 + <!-- 页面级加载组件(up-loading-page,uview-plus标准组件) -->
  6 + <up-loading-page
  7 + v-if="loading"
  8 + :loading="true"
  9 + title="加载中..."
  10 + color="#3c9cff"
  11 + ></up-loading-page>
  12 +
  13 + <!-- 内容容器(仅在非加载状态显示) -->
  14 + <view v-else class="content-wrap">
  15 + <!-- 单元格分组(核心uview组件) -->
  16 + <up-cell-group :border="false" inset style="margin: 20rpx;" >
  17 + <!-- 1. 工单编号 -->
  18 + <up-cell
  19 + title="工单编号"
  20 + :value="orderDetail.orderNo || '--'"
  21 + class="up-line-1"
  22 + align="middle"
  23 + ></up-cell>
  24 +
  25 + <!-- 2. 工单位置 -->
  26 + <up-cell
  27 + title="工单位置"
  28 + :value="orderDetail.roadName || '--'"
  29 + class="up-line-1"
  30 + align="middle"
  31 + ></up-cell>
  32 +
  33 + <!-- 3. 工单名称 -->
  34 + <up-cell
  35 + title="工单名称"
  36 + :value="orderDetail.orderName || '--'"
  37 + class="up-line-1"
  38 + align="middle"
  39 + ></up-cell>
  40 +
  41 + <!-- 4. 情况描述 -->
  42 + <up-cell
  43 + title="情况描述"
  44 + :value="orderDetail.remark || '--'"
  45 + class="up-line-1"
  46 + align="middle"
  47 + ></up-cell>
  48 +
  49 + <!-- 5. 问题照片 -->
  50 + <up-cell title="问题照片" align="top">
  51 + <view class="cell-content-wrap">
  52 + <up-album
  53 + v-if="orderDetail.imgs && orderDetail.imgs.length"
  54 + :list="formatAlbumList(orderDetail.imgs)"
  55 + :column="3"
  56 + :max-count="9"
  57 + :preview-full-image="true"
  58 + disabled
  59 + style="width: 100%;"
  60 + ></up-album>
  61 + <text v-else class="empty-text">暂无问题照片</text>
  62 + </view>
  63 + </up-cell>
  64 +
  65 + <!-- 6. 完成照片 -->
  66 + <up-cell title="完成照片" align="top">
  67 + <view class="cell-content-wrap">
  68 + <up-album
  69 + v-if="orderDetail.longRangeImgList && orderDetail.longRangeImgList.length"
  70 + :list="formatAlbumList(orderDetail.longRangeImgList)"
  71 + :column="3"
  72 + :max-count="9"
  73 + :preview-full-image="true"
  74 + disabled
  75 + style="width: 100%;"
  76 + ></up-album>
  77 + <text v-else class="empty-text">暂无完成照片</text>
  78 + </view>
  79 + </up-cell>
  80 +
  81 + <!-- 7. 处理结果 -->
  82 + <up-cell
  83 + title="处理结果"
  84 + :value="orderDetail.handleResultDesc || '--'"
  85 + class="up-line-1"
  86 + align="middle"
  87 + :border="false"
  88 + ></up-cell>
  89 + </up-cell-group>
  90 +
  91 + <!-- 空状态(原生样式,替代up-empty) -->
  92 + <view v-if="!Object.keys(orderDetail).length" class="empty-wrap">
  93 + <text class="empty-icon">📄</text>
  94 + <text class="empty-text">暂无工单详情</text>
  95 + <text class="empty-subtext">请检查工单ID是否正确</text>
  96 + </view>
  97 + </view>
  98 + </view>
  99 +</template>
  100 +
  101 +<script setup lang="ts">
  102 +import { ref, reactive } from 'vue';
  103 +import { inspectionPlanDetail } from "@/api/quick-order/quick-order";
  104 +
  105 +import { onLoad, onShow } from '@dcloudio/uni-app';
  106 +// 状态管理
  107 +const loading = ref(true);
  108 +const orderDetail = ref({});
  109 +
  110 +/**
  111 + * 格式化相册列表(适配up-album)
  112 + */
  113 +const formatAlbumList = (imgUrls: string[]) => {
  114 + return imgUrls.map(url => ({
  115 + url,
  116 + thumb: url,
  117 + name: `图片-${Date.now()}`
  118 + }));
  119 +};
  120 +
  121 +/**
  122 + * 获取工单详情
  123 + */
  124 +const getOrderDetail = async (id: string) => {
  125 + try {
  126 + loading.value = true;
  127 + const res = await inspectionPlanDetail({ id });
  128 + console.log('接口返回:', res)
  129 + orderDetail.value = res
  130 +
  131 + } catch (error) {
  132 + console.error('获取工单详情失败:', error);
  133 + uni.showToast({ title: '加载失败,请重试', icon: 'none' });
  134 + } finally {
  135 + loading.value = false;
  136 + }
  137 +};
  138 +
  139 +// 页面加载
  140 +onLoad((options) => {
  141 + const { id } = options;
  142 + if (id) {
  143 + getOrderDetail(id);
  144 + } else {
  145 + loading.value = false;
  146 + uni.showToast({ title: '缺少工单ID', icon: 'none' });
  147 + }
  148 +});
  149 +</script>
  150 +
  151 +<style scoped lang="scss">
  152 +
  153 +
  154 +// 内容容器(原生滚动)
  155 +.content-wrap {
  156 + background: #fff;
  157 + width: 100%;
  158 + box-sizing: border-box;
  159 + overflow-y: auto;
  160 +}
  161 +
  162 +// 单元格内容包裹层
  163 +.cell-content-wrap {
  164 + width: 100%;
  165 + padding: 10rpx 0;
  166 + box-sizing: border-box;
  167 +}
  168 +
  169 +// 空文本样式
  170 +.empty-text {
  171 + color: #999;
  172 + font-size: 28rpx;
  173 + line-height: 80rpx;
  174 + display: block;
  175 + text-align: left;
  176 +}
  177 +
  178 +// 优化uview组件样式
  179 +:deep(.up-cell-group) {
  180 + --u-cell-group-background-color: #fff;
  181 + --u-cell-group-border-radius: 12rpx;
  182 + --u-cell-group-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  183 +}
  184 +:deep(.up-cell) {
  185 + --u-cell-title-font-size: 28rpx;
  186 + --u-cell-value-font-size: 28rpx;
  187 + --u-cell-title-color: #666;
  188 + --u-cell-value-color: #333;
  189 + --u-cell-padding: 20rpx 15rpx;
  190 + --u-cell-border-color: #f5f5f5;
  191 +}
  192 +:deep(.up-album__item) {
  193 + margin: 10rpx 5rpx;
  194 + border-radius: 8rpx;
  195 +}
  196 +
  197 +// 原生空状态样式
  198 +.empty-wrap {
  199 + margin-top: 200rpx;
  200 + text-align: center;
  201 +}
  202 +.empty-icon {
  203 + font-size: 80rpx;
  204 + display: block;
  205 + margin-bottom: 20rpx;
  206 + color: #ddd;
  207 +}
  208 +.empty-text {
  209 + font-size: 28rpx;
  210 + color: #999;
  211 + display: block;
  212 +}
  213 +.empty-subtext {
  214 + font-size: 24rpx;
  215 + color: #ccc;
  216 + margin-top: 10rpx;
  217 + display: block;
  218 +}
  219 +
  220 +// 适配up-loading-page样式
  221 +:deep(.up-loading-page) {
  222 + margin-top: 100rpx;
  223 +}
  224 +</style>
0 \ No newline at end of file 225 \ No newline at end of file
pages-sub/daily/quick-order/order-list.vue 0 → 100644
  1 +<script setup lang="ts">
  2 +
  3 +</script>
  4 +
  5 +<template>
  6 +
  7 +</template>
  8 +
  9 +<style scoped lang="scss">
  10 +
  11 +</style>
0 \ No newline at end of file 12 \ No newline at end of file
pages.json
@@ -70,6 +70,16 @@ @@ -70,6 +70,16 @@
70 "style": { "navigationBarTitleText": "快速工单" } 70 "style": { "navigationBarTitleText": "快速工单" }
71 }, 71 },
72 { 72 {
  73 + "path": "quick-order/add-order",
  74 + "style": { "navigationBarTitleText": "新增快速工单" }
  75 + },
  76 + {
  77 + "path": "quick-order/order-detail",
  78 + "style": { "navigationBarTitleText": "快速工单详情" }
  79 + },
  80 +
  81 +
  82 + {
73 "path": "12345-order/index", 83 "path": "12345-order/index",
74 "style": { "navigationBarTitleText": "12345工单" } 84 "style": { "navigationBarTitleText": "12345工单" }
75 }, 85 },
pages/workbench/index.vue
1 <template> 1 <template>
2 <!-- 外层容器:包含蓝色块 + 原始内容 --> 2 <!-- 外层容器:包含蓝色块 + 原始内容 -->
3 <view class="workbench-container"> 3 <view class="workbench-container">
  4 + <!-- 加载页:覆盖整个容器,渲染完成后隐藏 -->
  5 + <up-loading-page
  6 + v-if="loading"
  7 + loading-text="加载中..."
  8 + color="#577ee3"
  9 + style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 9999;"
  10 + ></up-loading-page>
  11 +
4 <!-- 蓝色装饰块:#577ee3 --> 12 <!-- 蓝色装饰块:#577ee3 -->
5 <view class="blue-decor-block"></view> 13 <view class="blue-decor-block"></view>
6 14
@@ -8,7 +16,7 @@ @@ -8,7 +16,7 @@
8 <view class="content-wrap"> 16 <view class="content-wrap">
9 <!-- uview-plus空状态组件 --> 17 <!-- uview-plus空状态组件 -->
10 <u-empty 18 <u-empty
11 - v-if="!moduleList.length" 19 + v-if="!moduleList.length && !loading"
12 mode="list" 20 mode="list"
13 text="暂无菜单数据" 21 text="暂无菜单数据"
14 color="#999" 22 color="#999"
@@ -16,15 +24,13 @@ @@ -16,15 +24,13 @@
16 ></u-empty> 24 ></u-empty>
17 25
18 <!-- 菜单卡片列表(修复header插槽语法,恢复标题显示) --> 26 <!-- 菜单卡片列表(修复header插槽语法,恢复标题显示) -->
19 - <view v-else class="menu-card-wrap">  
20 - 27 + <view v-else-if="!loading" class="menu-card-wrap">
21 <up-card 28 <up-card
22 :title-size="18" 29 :title-size="18"
23 v-for="(parentModule, index) in moduleList" 30 v-for="(parentModule, index) in moduleList"
24 :key="parentModule.id" 31 :key="parentModule.id"
25 :title="parentModule.name" 32 :title="parentModule.name"
26 > 33 >
27 -  
28 <template #body> 34 <template #body>
29 <view> 35 <view>
30 <up-grid 36 <up-grid
@@ -57,7 +63,7 @@ @@ -57,7 +63,7 @@
57 </template> 63 </template>
58 64
59 <script setup lang="ts"> 65 <script setup lang="ts">
60 -// 原始代码完全保留 66 +// 原始代码完全保留,新增loading状态
61 import {ref, nextTick} from 'vue'; 67 import {ref, nextTick} from 'vue';
62 import { onShow } from '@dcloudio/uni-app'; 68 import { onShow } from '@dcloudio/uni-app';
63 import { useUserStore } from '@/pinia/user'; 69 import { useUserStore } from '@/pinia/user';
@@ -82,6 +88,8 @@ interface MenuItem { @@ -82,6 +88,8 @@ interface MenuItem {
82 children: MenuItem[]; 88 children: MenuItem[];
83 } 89 }
84 90
  91 +// 新增:加载状态(默认开启)
  92 +const loading = ref(true);
85 const userStore = useUserStore(); 93 const userStore = useUserStore();
86 const moduleList = ref<MenuItem[]>([]); 94 const moduleList = ref<MenuItem[]>([]);
87 95
@@ -96,15 +104,27 @@ const headClick = () =&gt; { @@ -96,15 +104,27 @@ const headClick = () =&gt; {
96 104
97 onShow(async () => { 105 onShow(async () => {
98 try { 106 try {
  107 + // 重置加载状态
  108 + loading.value = true;
  109 +
99 const rawMenuData = userStore.moduleListInfo || cache.get(globalConfig.cache.moduleListKey); 110 const rawMenuData = userStore.moduleListInfo || cache.get(globalConfig.cache.moduleListKey);
100 - // const rawMenuData = userStore.moduleListInfo as MenuItem[];  
101 const menuData = rawMenuData || []; 111 const menuData = rawMenuData || [];
102 moduleList.value = menuData; 112 moduleList.value = menuData;
  113 +
  114 + // 关键:等待DOM完全渲染后再隐藏加载页
103 await nextTick(); 115 await nextTick();
  116 + // 额外延迟(可选,确保视觉更流畅)
  117 + setTimeout(() => {
  118 + loading.value = false;
  119 + }, 300);
  120 +
104 console.log('菜单数据:', moduleList.value); 121 console.log('菜单数据:', moduleList.value);
105 } catch (error) { 122 } catch (error) {
106 console.error('获取菜单数据失败:', error); 123 console.error('获取菜单数据失败:', error);
107 moduleList.value = []; 124 moduleList.value = [];
  125 + // 出错时也隐藏加载页
  126 + await nextTick();
  127 + loading.value = false;
108 } 128 }
109 }); 129 });
110 130
@@ -179,4 +199,9 @@ const handleMenuClick = (item: MenuItem) =&gt; { @@ -179,4 +199,9 @@ const handleMenuClick = (item: MenuItem) =&gt; {
179 text-align: center; 199 text-align: center;
180 margin-top: 10rpx; 200 margin-top: 10rpx;
181 } 201 }
  202 +
  203 +/* 加载页样式优化(可选) */
  204 +:deep(.up-loading-page) {
  205 + background-color: rgba(255, 255, 255, 0.9);
  206 +}
182 </style> 207 </style>
183 \ No newline at end of file 208 \ No newline at end of file
pinia/user.js
1 import { defineStore } from 'pinia'; 1 import { defineStore } from 'pinia';
2 import cache from '@/common/utils/cache'; 2 import cache from '@/common/utils/cache';
3 import globalConfig from '@/common/config/global'; 3 import globalConfig from '@/common/config/global';
4 -import { login, getUserInfo, logout, moduleList } from '@/api/user'; 4 +import { login, getUserInfo, logout, moduleList, getSimpleDictDataList } from '@/api/user';
5 5
6 export const useUserStore = defineStore('user', { 6 export const useUserStore = defineStore('user', {
7 - // 1. 状态定义(修正 userId 类型 + 字段名) 7 + // 修复1:删除重复的 state 定义,只保留根层级的 state
8 state: () => ({ 8 state: () => ({
9 - token: '',  
10 - userInfo: {},  
11 - userId: '',  
12 - moduleListInfo: '',  
13 - expireTime: 0,  
14 - logoutLoading: false // 防重锁移到这里 9 + token: cache.get(globalConfig.cache.tokenKey) || '', // 初始值从缓存取(兼容持久化)
  10 + userInfo: cache.get(globalConfig.cache.userInfoKey) || {},
  11 + userId: cache.get(globalConfig.cache.userIdKey) || '',
  12 + moduleListInfo: cache.get(globalConfig.cache.moduleListKey) || '',
  13 + expireTime: cache.get(globalConfig.cache.expireTimeKey) || 0,
  14 + dictData: cache.get(globalConfig.cache.dictDataKey) || {},
  15 + logoutLoading: false
15 }), 16 }),
16 17
17 - // 2. 计算属性(保持不变)  
18 getters: { 18 getters: {
19 isLogin: (state) => { 19 isLogin: (state) => {
20 if (!state.token) return false; 20 if (!state.token) return false;
@@ -26,7 +26,6 @@ export const useUserStore = defineStore(&#39;user&#39;, { @@ -26,7 +26,6 @@ export const useUserStore = defineStore(&#39;user&#39;, {
26 permissions: (state) => state.userInfo.permissions || [] 26 permissions: (state) => state.userInfo.permissions || []
27 }, 27 },
28 28
29 - // 3. 核心动作(修正语法错误 + 登录后自动调用户信息接口)  
30 actions: { 29 actions: {
31 async login(params) { 30 async login(params) {
32 try { 31 try {
@@ -37,36 +36,40 @@ export const useUserStore = defineStore(&#39;user&#39;, { @@ -37,36 +36,40 @@ export const useUserStore = defineStore(&#39;user&#39;, {
37 throw new Error('登录失败,未获取到令牌'); 36 throw new Error('登录失败,未获取到令牌');
38 } 37 }
39 38
40 - // 仅更新 Pinia state(持久化插件会自动同步到 storage) 39 + // 更新 Pinia state
41 this.token = accessToken; 40 this.token = accessToken;
42 this.expireTime = expiresTime; 41 this.expireTime = expiresTime;
43 this.userId = userId; 42 this.userId = userId;
44 this.userInfo = {}; 43 this.userInfo = {};
45 44
46 - // 移除手动 cache.set() 代码  
47 - // cache.set(globalConfig.cache.tokenKey, accessToken);  
48 - // cache.set(globalConfig.cache.expireTimeKey, expiresTime);  
49 - // cache.set(globalConfig.cache.userIdKey, userId);  
50 -  
51 - // 等待 Pinia 持久化同步(可选,50ms 足够) 45 + // 等待 Pinia 持久化同步
52 await new Promise(resolve => setTimeout(resolve, 50)); 46 await new Promise(resolve => setTimeout(resolve, 50));
53 47
54 // 获取用户信息 48 // 获取用户信息
55 const userInfo = await this.getUserInfo(); 49 const userInfo = await this.getUserInfo();
56 this.userInfo = userInfo; 50 this.userInfo = userInfo;
57 - // 移除 cache.set(globalConfig.cache.userInfoKey, userInfo);  
58 51
59 // 获取模块列表 52 // 获取模块列表
60 let moduleListInfo = null; 53 let moduleListInfo = null;
61 try { 54 try {
62 moduleListInfo = await this.getModuleList(); 55 moduleListInfo = await this.getModuleList();
63 this.moduleListInfo = moduleListInfo; 56 this.moduleListInfo = moduleListInfo;
64 - // 移除 cache.set(globalConfig.cache.moduleListKey, moduleListInfo);  
65 } catch (moduleErr) { 57 } catch (moduleErr) {
66 console.warn('获取模块列表失败(不影响登录):', moduleErr); 58 console.warn('获取模块列表失败(不影响登录):', moduleErr);
67 uni.showToast({ title: '获取模块列表失败,可正常登录', icon: 'none' }); 59 uni.showToast({ title: '获取模块列表失败,可正常登录', icon: 'none' });
68 } 60 }
69 61
  62 + // 修复2:调用字典接口时,不直接抛错(避免阻断登录)
  63 + try {
  64 + const dictRes = await this.getAndSaveDictData();
  65 + // 仅接口返回成功时才更新字典
  66 + this.dictData = dictRes || {};
  67 + console.log('字典数据获取成功:', this.dictData);
  68 + } catch (dictErr) {
  69 + console.warn('获取字典失败(不影响登录):', dictErr);
  70 + uni.showToast({ title: '获取字典失败,可正常使用', icon: 'none' });
  71 + }
  72 +
70 return { ...res, userInfo, moduleListInfo }; 73 return { ...res, userInfo, moduleListInfo };
71 } catch (err) { 74 } catch (err) {
72 console.error('登录流程失败:', err); 75 console.error('登录流程失败:', err);
@@ -74,23 +77,46 @@ export const useUserStore = defineStore(&#39;user&#39;, { @@ -74,23 +77,46 @@ export const useUserStore = defineStore(&#39;user&#39;, {
74 } 77 }
75 }, 78 },
76 79
  80 + // 修复3:重构字典获取方法(加 Token 校验 + 强制携带 Token + 宽松错误处理)
  81 + async getAndSaveDictData() {
  82 + // 前置校验:无登录态直接返回,不请求接口
  83 + if (!this.isLogin) {
  84 + console.warn('未登录,跳过字典获取');
  85 + return { code: -1, msg: '未登录' };
  86 + }
  87 +
  88 + try {
  89 + // 强制携带 Token(和 getModuleList 保持一致,避免拦截器同步延迟)
  90 + const res = await getSimpleDictDataList(
  91 + {}, // 接口入参(按需传,比如 dictType: ['level'])
  92 + { header: { 'Authorization': `Bearer ${this.token}` } }
  93 + );
  94 +
  95 + // 校验接口返回码(核心:避免非 0 码数据存入)
  96 + if (res.code !== 0) {
  97 + console.warn('字典接口返回失败:', res.msg);
  98 + return res; // 返回错误信息,但不抛错
  99 + }
  100 + return res;
  101 + } catch (err) {
  102 + // 修复4:宽松错误处理,只打印日志,不抛错(避免阻断登录)
  103 + console.error('字典接口请求异常:', err);
  104 + return { code: -2, msg: '接口请求异常:' + (err.message || '网络错误') };
  105 + }
  106 + },
  107 +
77 async getModuleList() { 108 async getModuleList() {
78 try { 109 try {
79 - // 前置校验:无 Token 直接抛错,避免无效请求  
80 if (!this.token) { 110 if (!this.token) {
81 throw new Error('未获取到登录令牌,无法获取模块列表'); 111 throw new Error('未获取到登录令牌,无法获取模块列表');
82 } 112 }
83 113
84 - // 强制携带 Token(覆盖请求工具的自动携带,避免缓存同步延迟)  
85 const res = await moduleList({}, { 114 const res = await moduleList({}, {
86 - header: {  
87 - 'Authorization': `Bearer ${this.token}`  
88 - } 115 + header: { 'Authorization': `Bearer ${this.token}` }
89 }); 116 });
90 return res; 117 return res;
91 } catch (err) { 118 } catch (err) {
92 console.error('获取用户菜单失败:', err); 119 console.error('获取用户菜单失败:', err);
93 - // 区分 401 错误:仅登录态失效时抛错,避免触发 logout 循环  
94 if (err?.data?.code === 401 || err?.message.includes('401')) { 120 if (err?.data?.code === 401 || err?.message.includes('401')) {
95 throw new Error('登录态已过期,请重新登录'); 121 throw new Error('登录态已过期,请重新登录');
96 } else { 122 } else {
@@ -101,7 +127,6 @@ export const useUserStore = defineStore(&#39;user&#39;, { @@ -101,7 +127,6 @@ export const useUserStore = defineStore(&#39;user&#39;, {
101 127
102 async getUserInfo() { 128 async getUserInfo() {
103 try { 129 try {
104 - // 调用用户信息接口(此时 token 已存入缓存,请求工具会自动携带)  
105 const res = await getUserInfo(); 130 const res = await getUserInfo();
106 return res; 131 return res;
107 } catch (err) { 132 } catch (err) {
@@ -109,6 +134,7 @@ export const useUserStore = defineStore(&#39;user&#39;, { @@ -109,6 +134,7 @@ export const useUserStore = defineStore(&#39;user&#39;, {
109 throw new Error('获取用户信息失败,请重新登录'); 134 throw new Error('获取用户信息失败,请重新登录');
110 } 135 }
111 }, 136 },
  137 +
112 logout() { 138 logout() {
113 const pages = getCurrentPages(); 139 const pages = getCurrentPages();
114 if (pages.length === 0) return; 140 if (pages.length === 0) return;
@@ -119,16 +145,12 @@ export const useUserStore = defineStore(&#39;user&#39;, { @@ -119,16 +145,12 @@ export const useUserStore = defineStore(&#39;user&#39;, {
119 .split('?')[0]; 145 .split('?')[0];
120 146
121 if (currentPageRoute === loginPath) { 147 if (currentPageRoute === loginPath) {
122 - // 仅清空 Pinia state(持久化插件自动同步到 storage)  
123 this.token = ''; 148 this.token = '';
124 this.userInfo = {}; 149 this.userInfo = {};
125 this.userId = ''; 150 this.userId = '';
126 this.moduleListInfo = ''; 151 this.moduleListInfo = '';
127 this.expireTime = 0; 152 this.expireTime = 0;
128 -  
129 - // 移除手动 cache.remove() 代码  
130 - // cache.remove(globalConfig.cache.tokenKey);  
131 - // ... 其他 cache.remove 都删除 153 + this.dictData = {};
132 return; 154 return;
133 } 155 }
134 156
@@ -141,16 +163,14 @@ export const useUserStore = defineStore(&#39;user&#39;, { @@ -141,16 +163,14 @@ export const useUserStore = defineStore(&#39;user&#39;, {
141 } catch (err) { 163 } catch (err) {
142 console.error('退出登录接口调用失败:', err); 164 console.error('退出登录接口调用失败:', err);
143 } finally { 165 } finally {
144 - // 清空 state  
145 this.token = ''; 166 this.token = '';
146 this.userInfo = {}; 167 this.userInfo = {};
147 this.userId = ''; 168 this.userId = '';
148 this.moduleListInfo = ''; 169 this.moduleListInfo = '';
  170 + this.dictData = {};
149 this.expireTime = 0; 171 this.expireTime = 0;
150 this.logoutLoading = false; 172 this.logoutLoading = false;
151 173
152 - // 移除手动 cache.remove() 代码  
153 -  
154 uni.redirectTo({ 174 uni.redirectTo({
155 url: globalConfig.router.loginPath 175 url: globalConfig.router.loginPath
156 }); 176 });
@@ -160,19 +180,8 @@ export const useUserStore = defineStore(&#39;user&#39;, { @@ -160,19 +180,8 @@ export const useUserStore = defineStore(&#39;user&#39;, {
160 logoutWithLock(); 180 logoutWithLock();
161 }, 181 },
162 182
163 -// 新增:状态中添加 logoutLoading 防重锁  
164 - state: () => ({  
165 - token: cache.get(globalConfig.cache.tokenKey) || '',  
166 - userInfo: cache.get(globalConfig.cache.userInfoKey) || {},  
167 - userId: cache.get(globalConfig.cache.userIdKey) || '',  
168 - moduleListInfo: cache.get(globalConfig.cache.moduleListKey) || '',  
169 - expireTime: cache.get(globalConfig.cache.expireTimeKey) || 0,  
170 - logoutLoading: false // 新增:退出登录防重锁  
171 - }),  
172 -  
173 checkLogin() { 183 checkLogin() {
174 if (!this.isLogin) { 184 if (!this.isLogin) {
175 - // 先判断是否已在登录页,避免重复跳转  
176 const pages = getCurrentPages(); 185 const pages = getCurrentPages();
177 if (pages.length === 0) return false; 186 if (pages.length === 0) return false;
178 187
@@ -192,38 +201,36 @@ export const useUserStore = defineStore(&#39;user&#39;, { @@ -192,38 +201,36 @@ export const useUserStore = defineStore(&#39;user&#39;, {
192 } 201 }
193 }, 202 },
194 203
195 - // 4. 持久化配置(修正:persist 应放在 store 根层级,非 actions 内)  
196 persist: { 204 persist: {
197 enabled: true, 205 enabled: true,
198 - key: 'user_store', // 自定义存储键名(默认是 store 名 'user') 206 + key: 'user_store',
199 storage: { 207 storage: {
200 getItem: (key) => uni.getStorageSync(key), 208 getItem: (key) => uni.getStorageSync(key),
201 setItem: (key, value) => uni.setStorageSync(key, value), 209 setItem: (key, value) => uni.setStorageSync(key, value),
202 removeItem: (key) => uni.removeStorageSync(key) 210 removeItem: (key) => uni.removeStorageSync(key)
203 }, 211 },
204 - // 自定义序列化:将 state 拆分为原有的 jcss_xxx 键(可选)  
205 serializer: { 212 serializer: {
206 serialize: (state) => { 213 serialize: (state) => {
207 - // 拆分为多个独立键(和原 cache 格式对齐)  
208 uni.setStorageSync(globalConfig.cache.tokenKey, state.token); 214 uni.setStorageSync(globalConfig.cache.tokenKey, state.token);
209 uni.setStorageSync(globalConfig.cache.userIdKey, state.userId); 215 uni.setStorageSync(globalConfig.cache.userIdKey, state.userId);
210 uni.setStorageSync(globalConfig.cache.expireTimeKey, state.expireTime); 216 uni.setStorageSync(globalConfig.cache.expireTimeKey, state.expireTime);
211 uni.setStorageSync(globalConfig.cache.userInfoKey, state.userInfo); 217 uni.setStorageSync(globalConfig.cache.userInfoKey, state.userInfo);
212 uni.setStorageSync(globalConfig.cache.moduleListKey, state.moduleListInfo); 218 uni.setStorageSync(globalConfig.cache.moduleListKey, state.moduleListInfo);
213 - return state; // 返回完整 state(兼容 Pinia 默认逻辑) 219 + uni.setStorageSync(globalConfig.cache.dictDataKey, state.dictData);
  220 + return state;
214 }, 221 },
215 deserialize: (value) => { 222 deserialize: (value) => {
216 - // 从多个独立键恢复 state  
217 return { 223 return {
218 token: uni.getStorageSync(globalConfig.cache.tokenKey) || '', 224 token: uni.getStorageSync(globalConfig.cache.tokenKey) || '',
219 userId: uni.getStorageSync(globalConfig.cache.userIdKey) || '', 225 userId: uni.getStorageSync(globalConfig.cache.userIdKey) || '',
220 expireTime: uni.getStorageSync(globalConfig.cache.expireTimeKey) || 0, 226 expireTime: uni.getStorageSync(globalConfig.cache.expireTimeKey) || 0,
221 userInfo: uni.getStorageSync(globalConfig.cache.userInfoKey) || {}, 227 userInfo: uni.getStorageSync(globalConfig.cache.userInfoKey) || {},
222 moduleListInfo: uni.getStorageSync(globalConfig.cache.moduleListKey) || '', 228 moduleListInfo: uni.getStorageSync(globalConfig.cache.moduleListKey) || '',
  229 + dictData: uni.getStorageSync(globalConfig.cache.dictDataKey) || {},
223 logoutLoading: false 230 logoutLoading: false
224 }; 231 };
225 } 232 }
226 }, 233 },
227 - paths: [] // 序列化自定义后,paths 可留空 234 + paths: []
228 } 235 }
229 }); 236 });
230 \ No newline at end of file 237 \ No newline at end of file