Commit 3cbb87547081deaf9f903201077ab1a9e5a32fc4

Authored by 刘淇
1 parent b055b398

动态字典

common/config/env.js
... ... @@ -5,7 +5,7 @@ const env = {
5 5 // baseUrl: 'https://test.jichengshanshui.com.cn:28302',
6 6 // https://test.jichengshanshui.com.cn:28302
7 7  
8   - baseUrl: 'http://192.168.1.111:48081',
  8 + baseUrl: 'https://test.jichengshanshui.com.cn:28302',
9 9  
10 10 uploadUrl: 'https://test.jichengshanshui.com.cn:28302',
11 11 // baseApi:'admin-api',
... ...
common/config/global.js
... ... @@ -25,6 +25,7 @@ export default {
25 25 moduleListKey:'jcss_module_list',
26 26 dictDataKey:'jcss_dict_data',
27 27 refreshTokenKey: 'jcss_user_refresh_token', // 新增:刷新令牌缓存key
  28 + dictListKey: 'jcss_dict_list' // 新增这一行
28 29 },
29 30 appName: 'JCSS管理系统',
30 31 tokenExpireTime: 7 * 24 * 60 * 60 * 1000
... ...
pages-sub/daily/quick-order/add-order.vue
... ... @@ -2,204 +2,88 @@
2 2 <view class="page-container">
3 3 <!-- 核心:将所有 up-form-item 包裹在同一个 up-form 内 -->
4 4 <view class="work-order-form-content commonPageLRpadding">
5   - <up-form
6   - label-position="left"
7   - :model="workOrderForm"
8   - ref="workOrderFormRef"
9   - labelWidth="160rpx"
10   - >
11   -
12   - <up-form-item
13   - label="业务线"
14   - prop="busiLineCn"
15   - border-bottom
16   - required
17   - >
18   - <up-radio-group
19   - v-model="workOrderForm.busiLineCn"
20   - placement="row"
21   - @change="handleBusiLineChange"
22   - >
23   - <up-radio
24   - v-for="item in busiLineOptions"
25   - :key="item.name"
26   - :label="item.name"
27   - :name="item.name"
28   - ></up-radio>
  5 + <up-form label-position="left" :model="workOrderForm" ref="workOrderFormRef" labelWidth="160rpx">
  6 + <up-form-item label="业务线" prop="busiLine" border-bottom required>
  7 + <up-radio-group v-model="busiLineDisplay" placement="row" @change="handleBusiLineChange">
  8 + <up-radio v-for="item in busiLineOptions" :key="item.value" :label="item.label" :name="item.label"
  9 + > </up-radio>
29 10 </up-radio-group>
30   -
31 11 </up-form-item>
32 12 <!-- 1. 工单位置(地图选择) -->
33   - <up-form-item
34   - label="工单位置"
35   - prop="workLocation"
36   - border-bottom
37   - required
38   - @click="chooseWorkLocation(); hideKeyboard()"
39   - >
40   - <up-input
41   - v-model="workOrderForm.workLocation"
42   - border="none"
43   - readonly
44   - suffix-icon="map-fill"
45   - placeholder="点击选择工单位置"
46   - ></up-input>
  13 + <up-form-item label="工单位置" prop="workLocation" border-bottom required
  14 + @click="chooseWorkLocation(); hideKeyboard()">
  15 + <up-input v-model="workOrderForm.workLocation" border="none" readonly suffix-icon="map-fill"
  16 + placeholder="点击选择工单位置"></up-input>
47 17 </up-form-item>
48   -
49 18 <!-- 2. 道路名称(下拉框:位置选择后才可点击) -->
50   - <up-form-item
51   - label="道路名称"
52   - prop="roadName"
53   - border-bottom
54   - required
55   - @click="workOrderForm.workLocation ? (showRoadName = true, hideKeyboard()) : uni.showToast({title: '请先选择工单位置', icon: 'none'})"
56   - >
57   - <up-input
58   - v-model="workOrderForm.roadName"
59   - readonly
60   - disabled-color="#ffffff"
61   - placeholder="请先选择工单位置"
62   - border="none"
63   - ></up-input>
  19 + <up-form-item label="道路名称" prop="roadName" border-bottom required
  20 + @click="workOrderForm.workLocation ? (showRoadName = true, hideKeyboard()) : uni.showToast({ title: '请先选择工单位置', icon: 'none' })">
  21 + <up-input v-model="workOrderForm.roadName" readonly disabled-color="#ffffff" placeholder="请先选择工单位置"
  22 + border="none"></up-input>
64 23 <template #right>
65   - <up-icon name="arrow-right" size="16" ></up-icon>
  24 + <up-icon name="arrow-right" size="16"></up-icon>
66 25 </template>
67 26 </up-form-item>
68   -
69 27 <!-- 3. 问题类型(下拉框) -->
70   - <up-form-item
71   - label="问题类型"
72   - prop="orderName"
73   - border-bottom
74   - required
75   - @click="showOrderName = true; hideKeyboard()"
76   - >
77   - <up-input
78   - v-model="workOrderForm.orderName"
79   - disabled
80   - disabled-color="#ffffff"
81   - placeholder="请选择问题类型"
82   - border="none"
83   - ></up-input>
  28 + <up-form-item label="问题类型" prop="orderName" border-bottom required
  29 + @click="showOrderName = true; hideKeyboard()">
  30 + <up-input v-model="workOrderForm.orderName" disabled disabled-color="#ffffff" placeholder="请选择问题类型"
  31 + border="none"></up-input>
84 32 <template #right>
85 33 <up-icon name="arrow-right" size="16"></up-icon>
86 34 </template>
87 35 </up-form-item>
88   -
89 36 <!-- 4. 情况描述(文本域) -->
90   - <up-form-item
91   - label="情况描述"
92   - prop="problemDesc"
93   - required
94   - >
95   - <up-textarea
96   - placeholder="请输入情况描述(最多200字)"
97   - v-model.trim="workOrderForm.problemDesc"
98   - count
99   - maxlength="200"
100   - rows="4"
101   - @blur="() => $refs.workOrderFormRef.validateField('problemDesc')"
102   - ></up-textarea>
  37 + <up-form-item label="情况描述" prop="problemDesc" required>
  38 + <up-textarea placeholder="请输入情况描述(最多200字)" v-model.trim="workOrderForm.problemDesc" count maxlength="200"
  39 + rows="4" @blur="() => $refs.workOrderFormRef.validateField('problemDesc')"></up-textarea>
103 40 </up-form-item>
104   -
105 41 <!-- 问题照片 -->
106 42 <up-form-item label="问题照片" prop="problemImgs" required>
107   - <up-upload
108   - :file-list="problemImgsList"
109   - @after-read="(event) => uploadImgs(event, 'problemImgsList')"
110   - @delete="(event) => deleteImg(event, 'problemImgsList')"
111   - multiple
112   - width="70"
113   - height="70"
114   - :max-count="3"
115   - upload-text="选择问题照片"
116   - ></up-upload>
  43 + <up-upload :file-list="problemImgsList" @after-read="(event) => uploadImgs(event, 'problemImgsList')"
  44 + @delete="(event) => deleteImg(event, 'problemImgsList')" multiple width="70" height="70" :max-count="3"
  45 + upload-text="选择问题照片"></up-upload>
117 46 </up-form-item>
118   -
119 47 <!-- 完成照片 -->
120 48 <up-form-item label="完成照片" prop="completeImgs" required class="mt-20">
121   - <up-upload
122   - :file-list="completeImgsList"
123   - @after-read="(event) => uploadImgs(event, 'completeImgsList')"
124   - @delete="(event) => deleteImg(event, 'completeImgsList')"
125   - multiple
126   - width="70"
127   - height="70"
128   - :max-count="3"
129   - :sizeType="['compressed']"
130   - upload-text="选择完成照片"
131   - ></up-upload>
  49 + <up-upload :file-list="completeImgsList" @after-read="(event) => uploadImgs(event, 'completeImgsList')"
  50 + @delete="(event) => deleteImg(event, 'completeImgsList')" multiple width="70" height="70" :max-count="3"
  51 + :sizeType="['compressed']" upload-text="选择完成照片"></up-upload>
132 52 </up-form-item>
133   -
134 53 <!-- 处理结果(不必填) -->
135 54 <up-form-item label="处理结果" prop="handleResult" class="mt-20">
136   - <up-textarea
137   - placeholder="请输入处理结果描述(最多200字,选填)"
138   - v-model.trim="workOrderForm.handleResult"
139   - count
140   - maxlength="200"
141   - rows="4"
142   - @blur="() => $refs.workOrderFormRef.validateField('handleResult')"
143   - ></up-textarea>
  55 + <up-textarea placeholder="请输入处理结果描述(最多200字,选填)" v-model.trim="workOrderForm.handleResult" count
  56 + maxlength="200" rows="4" @blur="() => $refs.workOrderFormRef.validateField('handleResult')"></up-textarea>
144 57 </up-form-item>
145   -
146 58 </up-form>
147 59 </view>
148 60 <!-- 底部提交按钮(移除loading和disabled属性) -->
149 61 <view class="fixed-bottom-btn-wrap">
150   - <up-button
151   - type="primary"
152   - size="large"
153   - text="提交工单"
154   - @click="submitWorkOrder"
155   - ></up-button>
  62 + <up-button type="primary" size="large" text="提交工单" @click="submitWorkOrder"></up-button>
156 63 </view>
157   -
158 64 <!-- 道路名称下拉弹窗 -->
159   - <up-action-sheet
160   - :show="showRoadName"
161   - :actions="roadNameList"
162   - title="请选择道路名称"
163   - @close="showRoadName = false"
164   - @select="handleRoadNameSelect"
165   - ></up-action-sheet>
166   -
  65 + <up-action-sheet :show="showRoadName" :actions="roadNameList" title="请选择道路名称" @close="showRoadName = false"
  66 + @select="handleRoadNameSelect"></up-action-sheet>
167 67 <!-- 问题类型下拉弹窗 -->
168   - <up-action-sheet
169   - :show="showOrderName"
170   - :actions="orderNameList"
171   - title="请选择问题类型"
172   - @close="showOrderName = false"
173   - @select="handleOrderNameSelect"
174   - ></up-action-sheet>
  68 + <up-action-sheet :show="showOrderName" :actions="orderNameList" title="请选择问题类型" @close="showOrderName = false"
  69 + @select="handleOrderNameSelect"></up-action-sheet>
175 70 </view>
176 71 </template>
177   -
178 72 <script lang="ts">
179 73 import { getRoadListByLatLng } from '@/api/common'
180 74 import { uploadImages } from '@/common/utils/upload';
181 75 import { createQuick } from '@/api/quick-order/quick-order'
182 76 import { useUserStore } from '@/pinia/user';
183   -
184 77 export default {
  78 + computed: {
  79 + busiLineOptions() {
  80 + const userStore = useUserStore();
  81 + return userStore.dictList || []
  82 + }
  83 + },
185 84 data() {
186 85 return {
187   - // 业务线相关
188   - busiLineMap: {
189   - 'yl': '园林',
190   - 'sz': '市政',
191   - 'wy': '物业',
192   - 'zx': '秩序管理',
193   - 'hj': '环境卫生',
194   - 'yw': '常规运维',
195   - '园林': 'yl',
196   - '市政': 'sz',
197   - '物业': 'wy',
198   - '秩序管理': 'zx',
199   - '环境卫生': 'hj',
200   - '常规运维' : 'yw'
201   - },
202   - busiLineOptions: [], // 业务线选项列表
  86 + busiLineDisplay: '', // 单选框展示用,存中文label
203 87 // 问题照片列表
204 88 problemImgsList: [],
205 89 // 完成照片列表
... ... @@ -212,7 +96,7 @@ export default {
212 96 orderNameList: [],
213 97 // 工单表单数据
214 98 workOrderForm: {
215   - busiLineCn: '', // 业务线中文名称
  99 + busiLine: '', // 业务线value yl / sz / wy
216 100 roadId: 0, // 道路ID
217 101 roadName: '', // 道路名称
218 102 workLocation: '', // 工单位置
... ... @@ -224,7 +108,7 @@ export default {
224 108 },
225 109 // 表单校验规则
226 110 workOrderFormRules: {
227   - busiLineCn: [ // 业务线必选校验
  111 + busiLine: [
228 112 { type: 'string', required: true, message: '请选择业务线', trigger: ['change', 'blur'] }
229 113 ],
230 114 workLocation: [
... ... @@ -266,12 +150,11 @@ export default {
266 150 }
267 151 },
268 152 onLoad() {
269   - // 初始化业务线选项
270   - this.initBusiLineOptions();
271 153 // 默认选中第一个业务线
272 154 if (this.busiLineOptions && this.busiLineOptions.length > 0) {
273   - this.workOrderForm.busiLineCn = this.busiLineOptions[0].name;
274   - console.log('默认选中业务线:', this.workOrderForm.busiLineCn);
  155 + this.workOrderForm.busiLine = this.busiLineOptions[0].value;
  156 + this.busiLineDisplay = this.busiLineOptions[0].label;
  157 + console.log('默认选中业务线value:', this.workOrderForm.busiLine);
275 158 }
276 159 },
277 160 onReady() {
... ... @@ -284,25 +167,13 @@ export default {
284 167 console.log('问题类型列表:', this.orderNameList);
285 168 },
286 169 methods: {
287   - // 初始化业务线选项
288   - initBusiLineOptions() {
289   - // 获取pinia中的用户信息
290   - const userStore = useUserStore();
291   - if (userStore.userInfo?.user?.busiLine) {
292   - const rawBusiLines = userStore.userInfo.user.busiLine.split(',');
293   - // 生成中文名称列表
294   - this.busiLineOptions = rawBusiLines.map(item => ({
295   - name: this.busiLineMap[item.trim()] // name=中文名称(园林/市政/物业)
296   - }));
297   - }
298   - console.log('业务线选项:', this.busiLineOptions);
299   - },
300   - // 工具方法:通过中文名称获取对应的英文标识
301   - getBusiLineEnByCn(cnName) {
302   - return this.busiLineMap[cnName] || '';
303   - },
304 170 // 业务线切换事件
305   - handleBusiLineChange() {
  171 + handleBusiLineChange(selectedLabel) {
  172 + // 根据选中的中文label反向找到value,存入表单
  173 + const selectedItem = this.busiLineOptions.find(item => item.label === selectedLabel)
  174 + if (selectedItem) {
  175 + this.workOrderForm.busiLine = selectedItem.value;
  176 + }
306 177 // 清空道路信息
307 178 this.workOrderForm.roadName = '';
308 179 this.workOrderForm.roadId = 0;
... ... @@ -314,20 +185,13 @@ export default {
314 185 },
315 186 // 根据选中业务线请求道路列表
316 187 async getRoadListByBusiLine() {
317   - if (!this.workOrderForm.lat || !this.workOrderForm.lon) {
318   - return;
319   - }
320   - // 中文转英文标识
321   - const busiLineEn = this.getBusiLineEnByCn(this.workOrderForm.busiLineCn);
322   - if (!busiLineEn) {
323   - uni.showToast({ title: '业务线标识异常', icon: 'none' });
  188 + if (!this.workOrderForm.lat || !this.workOrderForm.lon || !this.workOrderForm.busiLine) {
324 189 return;
325 190 }
326   -
327 191 try {
328 192 uni.showLoading({ title: '获取道路名称中...' });
329 193 const roadRes = await getRoadListByLatLng({
330   - busiLine: busiLineEn, // 传递业务线英文标识
  194 + busiLine: this.workOrderForm.busiLine, // 直接传递value
331 195 latitude: this.workOrderForm.lat,
332 196 longitude: this.workOrderForm.lon
333 197 });
... ... @@ -380,7 +244,6 @@ export default {
380 244 console.log('上传图片事件:', event, '类型:', type);
381 245 const fileList = Array.isArray(event.file) ? event.file : [event.file];
382 246 const targetImgList = type === 'problemImgsList' ? this.problemImgsList : this.completeImgsList;
383   -
384 247 const filePaths = fileList.map(item => item.url);
385 248 const tempItems = fileList.map(item => ({
386 249 ...item,
... ... @@ -389,14 +252,12 @@ export default {
389 252 }));
390 253 const startIndex = targetImgList.length;
391 254 targetImgList.push(...tempItems);
392   -
393 255 try {
394 256 const uploadResultUrls = await uploadImages({
395 257 filePaths: filePaths,
396 258 ignoreError: true
397 259 });
398 260 console.log('上传成功的URL列表:', uploadResultUrls);
399   -
400 261 uploadResultUrls.forEach((url, index) => {
401 262 if (targetImgList[startIndex + index]) {
402 263 targetImgList.splice(startIndex + index, 1, {
... ... @@ -407,7 +268,6 @@ export default {
407 268 });
408 269 }
409 270 });
410   -
411 271 if (uploadResultUrls.length < fileList.length) {
412 272 const failCount = fileList.length - uploadResultUrls.length;
413 273 for (let i = uploadResultUrls.length; i < fileList.length; i++) {
... ... @@ -423,7 +283,6 @@ export default {
423 283 } else {
424 284 uni.showToast({ title: `成功上传${fileList.length}张图片`, icon: 'success' });
425 285 }
426   -
427 286 // 上传完成后重新校验图片字段
428 287 if (type === 'problemImgsList') {
429 288 this.$refs.workOrderFormRef.validateField('problemImgs');
... ... @@ -431,7 +290,6 @@ export default {
431 290 this.$refs.workOrderFormRef.validateField('completeImgs');
432 291 }
433 292 } catch (err) {
434   - console.error('图片上传失败:', err);
435 293 for (let i = 0; i < fileList.length; i++) {
436 294 if (targetImgList[startIndex + i]) {
437 295 targetImgList.splice(startIndex + i, 1, {
... ... @@ -442,7 +300,6 @@ export default {
442 300 }
443 301 }
444 302 uni.showToast({ title: '图片上传失败,请重试', icon: 'none' });
445   -
446 303 // 上传失败后重新校验图片字段
447 304 if (type === 'problemImgsList') {
448 305 this.$refs.workOrderFormRef.validateField('problemImgs');
... ... @@ -461,14 +318,11 @@ export default {
461 318 that.workOrderForm.roadName = '';
462 319 that.workOrderForm.roadId = 0;
463 320 that.roadNameList = [];
464   -
465 321 that.workOrderForm.workLocation = res.name;
466 322 that.workOrderForm.lat = res.latitude;
467 323 that.workOrderForm.lon = res.longitude;
468   -
469 324 that.$refs.workOrderFormRef.validateField('workLocation');
470 325 that.$refs.workOrderFormRef.validateField('roadName');
471   -
472 326 // 调用封装的方法,自动携带业务线
473 327 await that.getRoadListByBusiLine();
474 328 },
... ... @@ -516,14 +370,12 @@ export default {
516 370 try {
517 371 // 先执行表单校验
518 372 await this.$refs.workOrderFormRef.validate();
519   -
520   - // 中文转英文标识
521   - const busiLineEn = this.getBusiLineEnByCn(this.workOrderForm.busiLineCn);
522   - if (!busiLineEn) {
  373 + if (!this.workOrderForm.busiLine) {
523 374 uni.showToast({ title: '业务线选择异常,请重新选择', icon: 'none' });
524 375 return;
525 376 }
526   -
  377 + // 根据value找到中文label
  378 + const sourceName = this.busiLineOptions.find(item => item.value === this.workOrderForm.busiLine)?.label || '';
527 379 const submitData = {
528 380 roadId: this.workOrderForm.roadId,
529 381 roadName: this.workOrderForm.roadName,
... ... @@ -538,24 +390,20 @@ export default {
538 390 pressingType: 2,
539 391 orderName: this.workOrderForm.orderName,
540 392 sourceId: 1,
541   - sourceName: this.workOrderForm.busiLineCn, // 业务线中文名称
  393 + sourceName: sourceName, // 业务线中文名称
542 394 thirdWorkNo: '',
543   - busiLine: busiLineEn // 业务线英文标识(yl/sz/wy)
  395 + busiLine: this.workOrderForm.busiLine // 业务线英文标识(yl/sz/wy)
544 396 };
545   -
546 397 // 显示加载中
547 398 uni.showLoading({ title: '提交中...' });
548   -
549 399 // 调用提交接口
550 400 const res = await createQuick(submitData);
551   -
552 401 uni.hideLoading();
553 402 uni.showToast({
554 403 title: '工单提交成功',
555 404 icon: 'success',
556 405 duration: 1000
557 406 });
558   -
559 407 // 延迟跳转
560 408 setTimeout(() => {
561 409 uni.reLaunch({
... ... @@ -565,7 +413,6 @@ export default {
565 413 } catch (error) {
566 414 // 隐藏加载框
567 415 uni.hideLoading();
568   -
569 416 // 区分是表单校验失败还是接口调用失败
570 417 if (!Array.isArray(error)) {
571 418 // 接口调用失败
... ... @@ -581,16 +428,14 @@ export default {
581 428 }
582 429 }
583 430 </script>
584   -
585 431 <style lang="scss" scoped>
586 432 // 全局页面样式
587 433 .page-container {
588 434 min-height: 100vh;
589 435 }
590   -
591 436 // 工单表单内容容器
592 437 .work-order-form-content {
593 438 background: #fff;
594 439 margin-bottom: 100rpx;
595 440 }
596   -</style>
597 441 \ No newline at end of file
  442 +</style>
... ...
pages-sub/problem/regional-order-manage/add-order.vue
1 1 <template>
2 2 <view class="page-container">
3 3 <view class="work-order-form-content commonPageLRpadding">
4   - <up-form
5   - label-position="left"
6   - :model="baseForm"
7   - ref="workOrderFormRef"
8   - labelWidth="200rpx"
9   - >
  4 + <up-form label-position="left" :model="baseForm" ref="workOrderFormRef" labelWidth="200rpx">
10 5 <!-- 1. 工单位置(地图选择)【公共项】 -->
11   - <up-form-item
12   - label="工单位置"
13   - prop="workLocation"
14   - border-bottom
15   - required
16   - @click="chooseWorkLocation(); hideKeyboard()"
17   - >
18   - <up-input
19   - v-model="baseForm.workLocation"
20   - border="none"
21   - readonly
22   - suffix-icon="map-fill"
23   - placeholder="点击选择工单位置"
24   - ></up-input>
  6 + <up-form-item label="工单位置" prop="workLocation" border-bottom required
  7 + @click="chooseWorkLocation(); hideKeyboard()">
  8 + <up-input v-model="baseForm.workLocation" border="none" readonly suffix-icon="map-fill"
  9 + placeholder="点击选择工单位置"></up-input>
25 10 </up-form-item>
26   -
27 11 <!-- 2. 情况描述【公共项】 -->
28   - <up-form-item
29   - label="情况描述"
30   - prop="problemDesc"
31   - required
32   - border-bottom
33   - >
34   - <up-textarea
35   - placeholder="请输入情况描述(最多200字)"
36   - v-model.trim="baseForm.problemDesc"
37   - count
38   - maxlength="200"
39   - rows="4"
40   - @blur="() => workOrderFormRef.validateField('problemDesc')"
41   - ></up-textarea>
  12 + <up-form-item label="情况描述" prop="problemDesc" required border-bottom>
  13 + <up-textarea placeholder="请输入情况描述(最多200字)" v-model.trim="baseForm.problemDesc" count maxlength="200" rows="4"
  14 + @blur="() => workOrderFormRef.validateField('problemDesc')"></up-textarea>
42 15 </up-form-item>
43   -
44 16 <!-- 3. 问题照片【公共项】 -->
45   - <up-form-item label="问题照片" prop="problemImgs" required >
46   - <up-upload
47   - :file-list="problemImgs.imgList.value || []"
48   - @after-read="problemImgs.uploadImgs"
49   - @delete="problemImgs.deleteImg"
50   - multiple
51   - :width="70"
52   - :height="70"
53   - :max-count="problemImgs.uploadConfig.maxCount"
54   - :upload-text="problemImgs.uploadText"
55   - :size-type="problemImgs.uploadConfig.sizeType"
56   - ></up-upload>
  17 + <up-form-item label="问题照片" prop="problemImgs" required>
  18 + <up-upload :file-list="problemImgs.imgList.value || []" @after-read="problemImgs.uploadImgs"
  19 + @delete="problemImgs.deleteImg" multiple :width="70" :height="70"
  20 + :max-count="problemImgs.uploadConfig.maxCount" :upload-text="problemImgs.uploadText"
  21 + :size-type="problemImgs.uploadConfig.sizeType"></up-upload>
57 22 </up-form-item>
58   -
59 23 <!-- 多组派单区域 -->
60 24 <view class="dispatch-list-wrap">
61 25 <view class="dispatch-title">派单情况</view>
62   -
63 26 <!-- 循环渲染每组派单 -->
64 27 <view class="dispatch-item" v-for="(item, idx) in dispatchList" :key="idx">
65 28 <view class="item-head">
66 29 <text>派单{{ idx + 1 }}</text>
67 30 <!-- 数量大于1才显示红色关闭X -->
68   - <up-icon
69   - v-if="dispatchList.length > 1"
70   - name="close"
71   - color="#f53f3f"
72   - size="26"
73   - class="close-icon"
74   - @click="delDispatchItem(idx)"
75   - ></up-icon>
  31 + <up-icon v-if="dispatchList.length > 1" name="close" color="#f53f3f" size="26" class="close-icon"
  32 + @click="delDispatchItem(idx)"></up-icon>
76 33 </view>
77   -
78 34 <!-- 业务线 -->
79   - <up-form-item
80   - label="业务线"
81   - prop="busiLineCn"
82   - border-bottom
83   - required
84   - @click="handleActionSheetOpen('busiLine', idx); hideKeyboard()"
85   - >
86   - <up-input
87   - v-model="item.busiLineCn"
88   - disabled
89   - disabled-color="#ffffff"
90   - placeholder="请选择业务线"
91   - border="none"
92   - ></up-input>
  35 + <up-form-item label="业务线" prop="busiLineCn" border-bottom required
  36 + @click="handleActionSheetOpen('busiLine', idx); hideKeyboard()">
  37 + <up-input v-model="item.busiLineCn" disabled disabled-color="#ffffff" placeholder="请选择业务线"
  38 + border="none"></up-input>
93 39 <template #right>
94 40 <up-icon name="arrow-right" size="16"></up-icon>
95 41 </template>
96 42 </up-form-item>
97   -
98 43 <!-- 问题类型 -->
99   - <up-form-item
100   - label="问题类型"
101   - prop="orderName"
102   - border-bottom
103   - required
104   - @click="openCascader(idx); hideKeyboard()"
105   - >
106   - <up-input
107   - v-model="item.orderName"
108   - disabled
109   - disabled-color="#ffffff"
110   - placeholder="请选择问题类型"
111   - border="none"
112   - ></up-input>
  44 + <up-form-item label="问题类型" prop="orderName" border-bottom required
  45 + @click="openCascader(idx); hideKeyboard()">
  46 + <up-input v-model="item.orderName" disabled disabled-color="#ffffff" placeholder="请选择问题类型"
  47 + border="none"></up-input>
113 48 <template #right>
114 49 <up-icon name="arrow-right" size="16"></up-icon>
115 50 </template>
116 51 </up-form-item>
117   -
118 52 <!-- 道路名称 -->
119   - <up-form-item
120   - label="道路名称"
121   - prop="roadName"
122   - border-bottom
123   - required
124   - @click="handleActionSheetOpen('roadName', idx); hideKeyboard()"
125   - >
126   - <up-input
127   - v-model="item.roadName"
128   - readonly
129   - disabled-color="#ffffff"
130   - placeholder="请先选择工单位置"
131   - border="none"
132   - ></up-input>
  53 + <up-form-item label="道路名称" prop="roadName" border-bottom required
  54 + @click="handleActionSheetOpen('roadName', idx); hideKeyboard()">
  55 + <up-input v-model="item.roadName" readonly disabled-color="#ffffff" placeholder="请先选择工单位置"
  56 + border="none"></up-input>
133 57 <template #right>
134 58 <up-icon name="arrow-right" size="16"></up-icon>
135 59 </template>
136 60 </up-form-item>
137   -
138 61 <!-- 紧急程度 -->
139   - <up-form-item
140   - label="紧急程度"
141   - prop="pressingTypeName"
142   - border-bottom
143   - required
144   - @click="handleActionSheetOpen('pressingType', idx); hideKeyboard()"
145   - >
146   - <up-input
147   - v-model="item.pressingTypeName"
148   - disabled
149   - disabled-color="#ffffff"
150   - placeholder="请选择紧急程度"
151   - border="none"
152   - ></up-input>
  62 + <up-form-item label="紧急程度" prop="pressingTypeName" border-bottom required
  63 + @click="handleActionSheetOpen('pressingType', idx); hideKeyboard()">
  64 + <up-input v-model="item.pressingTypeName" disabled disabled-color="#ffffff" placeholder="请选择紧急程度"
  65 + border="none"></up-input>
153 66 <template #right>
154 67 <up-icon name="arrow-right" size="16"></up-icon>
155 68 </template>
156 69 </up-form-item>
157   -
158 70 <!-- 每组派单独立:希望完成时间 -->
159   - <up-form-item
160   - label="希望完成时间"
161   -
162   - @click="openItemDatePicker(idx); hideKeyboard()"
163   - >
164   - <up-input
165   - v-model="item.expectedFinishDate"
166   - border="none"
167   - readonly
168   - placeholder="点击选择时间"
169   - ></up-input>
  71 + <up-form-item label="希望完成时间" @click="openItemDatePicker(idx); hideKeyboard()">
  72 + <up-input v-model="item.expectedFinishDate" border="none" readonly placeholder="点击选择时间"></up-input>
170 73 <template #right>
171 74 <view v-if="item.expectedFinishDate" @click.stop>
172 75 <up-icon name="close" size="16" @click.stop="clearItemDate(idx)"></up-icon>
... ... @@ -176,81 +79,49 @@
176 79 </up-form-item>
177 80 </view>
178 81 </view>
179   -
180 82 <!-- 新增派单按钮 -->
181 83 <view class="add-dispatch-btn-wrap" v-if="!isRenew">
182   - <up-button
183   - type="primary"
184   - plain
185   - text="新增派单"
186   - :disabled="dispatchList.length >= 3"
187   - @click="addDispatchItem"
188   - ></up-button>
  84 + <up-button type="primary" plain text="新增派单"
  85 + :disabled="dispatchList.length >= 3 || getAvailableBusiLineList().length === 0"
  86 + @click="addDispatchItem"></up-button>
189 87 </view>
190 88 </up-form>
191 89 </view>
192   -
193 90 <!-- 底部提交按钮 -->
194 91 <view class="fixed-bottom-btn-wrap">
195 92 <up-button type="primary" text="提交工单" @click="submitWorkOrder"></up-button>
196 93 </view>
197   -
198 94 <!-- 通用下拉弹窗:业务线 / 道路 / 紧急程度 共用 -->
199   - <up-action-sheet
200   - :show="showActionSheet"
201   - :actions="currentActionSheetData.list"
202   - :title="currentActionSheetData.title"
203   - @close="handleActionSheetClose"
204   - @select="handleActionSheetSelect"
205   - ></up-action-sheet>
206   -
  95 + <up-action-sheet :show="showActionSheet" :actions="currentActionSheetData.list"
  96 + :title="currentActionSheetData.title" @close="handleActionSheetClose"
  97 + @select="handleActionSheetSelect"></up-action-sheet>
207 98 <!-- 问题类型 级联选择器 -->
208   - <up-cascader
209   - v-model="cascaderValue"
210   - :data="cascaderOptions"
211   - v-model:show="showCascader"
212   - title="问题类型"
213   - :auto-close="false"
214   - @confirm="cascaderConfirm"
215   - ></up-cascader>
216   -
  99 + <up-cascader v-model="cascaderValue" :data="cascaderOptions" v-model:show="showCascader" title="问题类型"
  100 + :auto-close="false" @confirm="cascaderConfirm"></up-cascader>
217 101 <!-- 单独时间选择器(给每组派单用) -->
218   - <up-datetime-picker
219   - :show="showItemDatePicker"
220   - v-model="tempDateValue"
221   - mode="datetime"
222   - :min-date="Date.now()"
223   - @cancel="closeItemDatePicker"
224   - @confirm="itemDateConfirm"
225   - ></up-datetime-picker>
  102 + <up-datetime-picker :show="showItemDatePicker" v-model="tempDateValue" mode="datetime" :min-date="Date.now()"
  103 + @cancel="closeItemDatePicker" @confirm="itemDateConfirm"></up-datetime-picker>
226 104 </view>
227 105 </template>
228   -
229 106 <script setup>
230   -import { ref, reactive, nextTick } from 'vue'
  107 +import { ref, reactive, nextTick, computed } from 'vue'
231 108 import { onReady, onShow, onLoad, onBackPress } from '@dcloudio/uni-app';
232 109 import { useUploadImgs } from '@/common/utils/useUploadImgs'
233 110 import { getRoadListByLatLng, getProblemListByType } from '@/api/common'
234 111 import {
235   - daquUniversalApproval, qyUniversalApproval, qyWorkorderCreate,dycCreateBatchNew,
236   - dcyUniversalApproval, daquWorkorderCreate, dcyWorkorderCreate
  112 +daquUniversalApproval, qyUniversalApproval, qyWorkorderCreate, dycCreateBatchNew,
  113 +dcyUniversalApproval, daquWorkorderCreate, dcyWorkorderCreate
237 114 } from '@/api/regional-order-manage/regional-order-manage'
238 115 import { timeFormat } from '@/uni_modules/uview-plus'
239 116 import { nextStepMap } from '@/common/utils/common'
240 117 import { useUserStore } from '@/pinia/user';
241   -
242 118 // ========== 常量配置区 ==========
243 119 const CONST = {
244   - DICT_PRESSING_TYPE: 'workorder_pressing_type',
245   - UPLOAD_CONFIG: { maxCount: 3, uploadText: '选择问题照片', sizeType: ['compressed'] },
246   - PAGE_ORDER_LIST: '/pages-sub/problem/regional-order-manage/index',
247   - BUSI_LINE_MAP: {
248   - yl: '园林', sz: '市政', wy: '物业', '园林': 'yl', '市政': 'sz', '物业': 'wy',
249   - 'zx': '秩序管理', 'hj': '环境卫生', '秩序管理': 'zx', '环境卫生': 'hj'
250   - },
251   - ROLE: { MANAGER: 'regional_manager', INSPECTOR: 'Inspector_global', PATROL: 'patrol_global' , LEADER: 'Inspector_global_leader'}
  120 +DICT_PRESSING_TYPE: 'workorder_pressing_type',
  121 +UPLOAD_CONFIG: { maxCount: 3, uploadText: '选择问题照片', sizeType: ['compressed'] },
  122 +PAGE_ORDER_LIST: '/pages-sub/problem/regional-order-manage/index',
  123 +ROLE: { MANAGER: 'regional_manager', INSPECTOR: 'Inspector_global', PATROL: 'patrol_global', LEADER: 'Inspector_global_leader' }
252 124 }
253   -
254 125 // ========== 全局实例 & 基础状态 ==========
255 126 const userStore = useUserStore();
256 127 const USER_ROLES = userStore.userInfo?.roles || [];
... ... @@ -258,556 +129,554 @@ const workOrderFormRef = ref(null);
258 129 const showActionSheet = ref(false);
259 130 const isRenew = ref(false);
260 131 const renewOrderData = ref(null);
261   -
262 132 // 当前操作的派单项下标
263 133 const currentDispatchIndex = ref(0);
264   -
265 134 // 三级级联选择器
266 135 const showCascader = ref(false);
267 136 const cascaderValue = ref([]);
268 137 const cascaderOptions = ref([]);
269   -
270 138 // 下拉弹窗数据
271 139 const currentActionSheetData = reactive({ type: '', list: [], title: '', index: 0 });
272 140 const roadNameList = ref([]);
273 141 const pressingTypeList = ref([]);
274 142 const busiLineOptions = ref([]);
275 143 const currentBusiLineCode = ref('');
276   -
277 144 // 每组派单独立时间选择器
278 145 const showItemDatePicker = ref(false);
279 146 const tempDateValue = ref(Date.now());
280 147 const curSelectDateIndex = ref(-1);
281   -
282 148 // 图片上传
283 149 const problemImgs = useUploadImgs({
284   - ...CONST.UPLOAD_CONFIG,
285   - formRef: workOrderFormRef,
286   - fieldName: 'problemImgs'
  150 +...CONST.UPLOAD_CONFIG,
  151 +formRef: workOrderFormRef,
  152 +fieldName: 'problemImgs'
287 153 })
288 154 if (!Array.isArray(problemImgs.rawImgList.value)) problemImgs.rawImgList.value = [];
289 155  
  156 +// ===================== 【动态构建业务线双向映射,从 userStore.dictList】=====================
  157 +let busiLineEnToCnMap = {}; // code -> 中文
  158 +let busiLineCnToEnMap = {}; // 中文 -> code
  159 +/**
  160 + * 从dictList构建业务线双向map
  161 + * @param {Array} dictList userStore.dictList
  162 + */
  163 +const buildBusiLineMap = (dictList) => {
  164 + busiLineEnToCnMap = {};
  165 + busiLineCnToEnMap = {};
  166 + if (!Array.isArray(dictList)) return;
  167 + dictList.forEach(item => {
  168 + const code = item.value?.trim();
  169 + const name = item.label?.trim();
  170 + if (code && name) {
  171 + busiLineEnToCnMap[code] = name;
  172 + busiLineCnToEnMap[name] = code;
  173 + }
  174 + })
  175 +}
  176 +// 工具方法(替换原来硬编码BUSI_LINE_MAP)
  177 +const busiLineCnToEn = (cn) => busiLineCnToEnMap[cn] || '';
  178 +const busiLineEnToCn = (en) => busiLineEnToCnMap[en] || '';
  179 +// ==========================================================================
  180 +
  181 +// ===================== 【新增方法:获取已占用业务线code】=====================
  182 +const getUsedBusiCodeList = () => {
  183 +return dispatchList.value
  184 + .filter(item => !!item.busiLineCode)
  185 + .map(item => item.busiLineCode)
  186 +}
  187 +/**
  188 + * 获取【当前可选业务线】:排除已经被其他派单项占用的业务线
  189 + * @param {number} currentIdx 当前正在操作的派单项下标
  190 + */
  191 +const getAvailableBusiLineList = (currentIdx = -1) => {
  192 +const usedCodes = getUsedBusiCodeList()
  193 +return busiLineOptions.value.filter(opt => {
  194 +// 如果是当前这条自己已经选中的业务线,保留;其他派单项占用的过滤掉
  195 +const isSelfSelected = dispatchList.value[currentIdx]?.busiLineCode === opt.value
  196 +const isUsedByOther = usedCodes.includes(opt.value) && !isSelfSelected
  197 +return !isUsedByOther
  198 + })
  199 +}
  200 +// ==========================================================================
290 201 // ========== 公共表单(所有派单共用) ==========
291 202 const baseForm = reactive({
292   - workLocation: '',
293   - problemDesc: '',
294   - lat: 0,
295   - lon: 0
  203 +workLocation: '',
  204 +problemDesc: '',
  205 +lat: 0,
  206 +lon: 0
296 207 })
297   -
298 208 // ========== 派单列表(多组派单核心数组) ==========
299 209 const createDispatchItem = (defaultBusiLine = null) => {
300   - return {
301   - busiLineCn: defaultBusiLine?.name || '',
302   - busiLineCode: defaultBusiLine?.value || '',
303   - roadId: 0,
304   - roadName: '',
305   - orderName: '',
306   - orderCode: '',
307   - pressingType: '',
308   - pressingTypeName: '',
309   - expectedFinishDate: '' // 每组独立完成时间
  210 +return {
  211 +busiLineCn: defaultBusiLine?.name || '',
  212 +busiLineCode: defaultBusiLine?.value || '',
  213 +roadId: 0,
  214 +roadName: '',
  215 +orderName: '',
  216 +orderCode: '',
  217 +pressingType: '',
  218 +pressingTypeName: '',
  219 +expectedFinishDate: '' // 每组独立完成时间
310 220 }
311 221 }
312 222 // 初始化默认 1 组派单
313 223 const dispatchList = ref([createDispatchItem()])
314   -
315 224 // ========== 表单校验规则 ==========
316 225 const workOrderFormRules = reactive({
317   - workLocation: [{ type: 'string', required: true, message: '请选择工单位置', trigger: ['change', 'blur'] }],
318   - problemDesc: [
  226 +workLocation: [{ type: 'string', required: true, message: '请选择工单位置', trigger: ['change', 'blur'] }],
  227 +problemDesc: [
319 228 { type: 'string', required: true, message: '请输入情况描述', trigger: ['change', 'blur'] },
320 229 { type: 'string', min: 3, max: 200, message: '情况描述需3-200字', trigger: ['change', 'blur'] }
321 230 ],
322   - problemImgs: [problemImgs.imgValidateRule]
  231 +problemImgs: [problemImgs.imgValidateRule]
323 232 })
324   -
325 233 // ========== 工具方法 ==========
326   -const busiLineCnToEn = (cn) => CONST.BUSI_LINE_MAP[cn] || '';
327   -const busiLineEnToCn = (en) => CONST.BUSI_LINE_MAP[en] || '';
328 234 const toOrderListPage = () => uni.reLaunch({ url: CONST.PAGE_ORDER_LIST });
329 235 const formatDate = (dateVal, fmt = 'yyyy-mm-dd hh:MM:ss') => dateVal ? timeFormat(dateVal, fmt) : '';
330 236 const dateToTimestamp = (dateStr) => dateStr ? new Date(dateStr).getTime() : '';
331   -
332 237 /**
333 238 * 加载三级问题类型级联数据
334 239 */
335 240 const loadProblemCascader = async (parentCode) => {
336   - if (!parentCode) {
337   - cascaderOptions.value = [];
338   - return;
  241 +if (!parentCode) {
  242 +cascaderOptions.value = [];
  243 +return;
339 244 }
340   - try {
341   - uni.showLoading({ title: '加载类型中...' });
342   - const res = await getProblemListByType({ parentCode });
343   - cascaderOptions.value = res|| [];
  245 +try {
  246 +uni.showLoading({ title: '加载类型中...' });
  247 +const res = await getProblemListByType({ parentCode });
  248 +cascaderOptions.value = res || [];
344 249 } catch (err) {
345   - console.error('加载问题类型失败', err);
346   - uni.showToast({ title: '问题类型加载失败', icon: 'none' });
347   - cascaderOptions.value = [];
  250 +console.error('加载问题类型失败', err);
  251 +uni.showToast({ title: '问题类型加载失败', icon: 'none' });
  252 +cascaderOptions.value = [];
348 253 } finally {
349   - uni.hideLoading();
  254 +uni.hideLoading();
350 255 }
351 256 };
352   -
353 257 /** 打开级联选择器 */
354 258 const openCascader = (idx) => {
355   - currentDispatchIndex.value = idx;
356   - const item = dispatchList.value[idx];
357   - if (!item.busiLineCode) {
358   - uni.showToast({ title: '请先选择业务线', icon: 'none' });
359   - return;
  259 +currentDispatchIndex.value = idx;
  260 +const item = dispatchList.value[idx];
  261 +if (!item.busiLineCode) {
  262 +uni.showToast({ title: '请先选择业务线', icon: 'none' });
  263 +return;
360 264 }
361   - if (!cascaderOptions.value || cascaderOptions.value.length === 0) {
362   - uni.showToast({ title: '正在加载问题类型,请稍等', icon: 'none' });
363   - loadProblemCascader(item.busiLineCode);
364   - return;
  265 +if (!cascaderOptions.value || cascaderOptions.value.length === 0) {
  266 +uni.showToast({ title: '正在加载问题类型,请稍等', icon: 'none' });
  267 +loadProblemCascader(item.busiLineCode);
  268 +return;
365 269 }
366   - cascaderValue.value = [];
367   - showCascader.value = true;
  270 +cascaderValue.value = [];
  271 +showCascader.value = true;
368 272 };
369   -
370 273 /** 级联选择确认回调 */
371 274 const cascaderConfirm = (selectedArr) => {
372   - const idx = currentDispatchIndex.value;
373   - const item = dispatchList.value[idx];
374   - console.log('最终选中值数组:', selectedArr);
375   -
376   - const getLabelByValueList = (list, valList) => {
377   - const labelArr = [];
378   - let currentList = list;
379   - for (const val of valList) {
380   - const target = currentList.find(item => item.value === val);
381   - if (!target) break;
382   - labelArr.push(target.label);
383   - currentList = target.children || [];
  275 +const idx = currentDispatchIndex.value;
  276 +const item = dispatchList.value[idx];
  277 +console.log('最终选中值数组:', selectedArr);
  278 +const getLabelByValueList = (list, valList) => {
  279 +const labelArr = [];
  280 +let currentList = list;
  281 +for (const val of valList) {
  282 +const target = currentList.find(item => item.value === val);
  283 +if (!target) break;
  284 +labelArr.push(target.label);
  285 +currentList = target.children || [];
384 286 }
385   - return labelArr.join('/');
  287 +return labelArr.join('/');
386 288 };
387   -
388   - const fullLabel = getLabelByValueList(cascaderOptions.value, selectedArr);
389   - item.orderName = fullLabel;
390   - item.orderCode = selectedArr.at(-1);
391   - cascaderValue.value = selectedArr;
392   - workOrderFormRef.value?.validateField('orderName');
  289 +const fullLabel = getLabelByValueList(cascaderOptions.value, selectedArr);
  290 +item.orderName = fullLabel;
  291 +item.orderCode = selectedArr.at(-1);
  292 +cascaderValue.value = selectedArr;
  293 +workOrderFormRef.value?.validateField('orderName');
393 294 };
394   -
395 295 // ========== 每组派单 时间选择相关 ==========
396 296 const openItemDatePicker = (idx) => {
397   - curSelectDateIndex.value = idx;
398   - tempDateValue.value = Date.now();
399   - showItemDatePicker.value = true;
  297 +curSelectDateIndex.value = idx;
  298 +tempDateValue.value = Date.now();
  299 +showItemDatePicker.value = true;
400 300 };
401 301 const closeItemDatePicker = () => {
402   - showItemDatePicker.value = false;
403   - curSelectDateIndex.value = -1;
  302 +showItemDatePicker.value = false;
  303 +curSelectDateIndex.value = -1;
404 304 };
405 305 const itemDateConfirm = (e) => {
406   - const idx = curSelectDateIndex.value;
407   - if (idx > -1 && dispatchList.value[idx]) {
408   - dispatchList.value[idx].expectedFinishDate = formatDate(e.value);
  306 +const idx = curSelectDateIndex.value;
  307 +if (idx > -1 && dispatchList.value[idx]) {
  308 +dispatchList.value[idx].expectedFinishDate = formatDate(e.value);
409 309 }
410   - closeItemDatePicker();
  310 +closeItemDatePicker();
411 311 };
412 312 const clearItemDate = (idx) => {
413   - dispatchList.value[idx].expectedFinishDate = '';
  313 +dispatchList.value[idx].expectedFinishDate = '';
414 314 };
415   -
416 315 // ========== 新增 / 删除 派单项 ==========
417 316 /** 新增派单 */
418 317 const addDispatchItem = () => {
419   - if (dispatchList.value.length >= 3) return;
420   - // 新项:业务线默认第一个,其余置空
421   - const firstBusi = busiLineOptions.value[0] || null;
422   - dispatchList.value.push(createDispatchItem(firstBusi));
423   - if(firstBusi) {
424   - currentBusiLineCode.value = firstBusi.value;
425   - loadProblemCascader(firstBusi.value);
  318 +if (dispatchList.value.length >= 3) return;
  319 +// 【修改】新增取剩下可用业务线第一条
  320 +const availList = getAvailableBusiLineList()
  321 +const firstBusi = availList[0] || null;
  322 +dispatchList.value.push(createDispatchItem(firstBusi));
  323 +if (firstBusi) {
  324 +currentBusiLineCode.value = firstBusi.value;
  325 +loadProblemCascader(firstBusi.value);
426 326 }
427 327 };
428   -
429 328 /** 删除派单项 */
430 329 const delDispatchItem = (idx) => {
431   - if (dispatchList.value.length <= 1) return;
432   - dispatchList.value.splice(idx, 1);
  330 +if (dispatchList.value.length <= 1) return;
  331 +dispatchList.value.splice(idx, 1);
433 332 };
434   -
435   -
436 333 // 生命周期
437 334 onLoad(async (options) => {
438   - initBusiLineOptions();
439   - // 初始化第一组派单默认业务线
440   - if (busiLineOptions.value.length) {
441   - const firstLine = busiLineOptions.value[0];
442   - dispatchList.value[0].busiLineCn = firstLine.name;
443   - dispatchList.value[0].busiLineCode = firstLine.value;
444   - currentBusiLineCode.value = firstLine.value;
445   - // 非重新提交才默认加载级联
446   - if (!(options.isRenew == 1 && options.tempKey)) {
447   - loadProblemCascader(firstLine.value);
  335 +initBusiLineOptions();
  336 +// 初始化第一组派单默认业务线
  337 +if (busiLineOptions.value.length) {
  338 +const availList = getAvailableBusiLineList(0)
  339 +const firstLine = availList[0];
  340 +dispatchList.value[0].busiLineCn = firstLine.name;
  341 +dispatchList.value[0].busiLineCode = firstLine.value;
  342 +currentBusiLineCode.value = firstLine.value;
  343 +// 非重新提交才默认加载级联
  344 +if (!(options.isRenew == 1 && options.tempKey)) {
  345 +loadProblemCascader(firstLine.value);
448 346 }
449 347 }
450   - // 重新提交场景:等待回显完成
451   - if (options.isRenew == 1 && options.tempKey) {
452   - await handleRenewOptions(options.tempKey);
  348 +// 重新提交场景:等待回显完成
  349 +if (options.isRenew == 1 && options.tempKey) {
  350 +await handleRenewOptions(options.tempKey);
453 351 }
454 352 });
455   -
456 353 onReady(() => {
457   - nextTick(() => {
458   - workOrderFormRef.value?.setRules(workOrderFormRules);
  354 +nextTick(() => {
  355 +workOrderFormRef.value?.setRules(workOrderFormRules);
459 356 });
460 357 });
461   -
462 358 onShow(() => {
463   - pressingTypeList.value = uni.$dict.transformLabelValueToNameValue(uni.$dict.getDictSimpleList(CONST.DICT_PRESSING_TYPE));
  359 +pressingTypeList.value = uni.$dict.transformLabelValueToNameValue(uni.$dict.getDictSimpleList(CONST.DICT_PRESSING_TYPE));
464 360 });
465   -
466 361 // 物理返回拦截
467 362 onBackPress(() => {
468   - uni.redirectTo({ url: CONST.PAGE_ORDER_LIST });
469   - return true;
  363 +uni.redirectTo({ url: CONST.PAGE_ORDER_LIST });
  364 +return true;
470 365 });
471   -
472 366 // ========== 业务线相关 ==========
473 367 const initBusiLineOptions = () => {
474   - const rawBusiLines = userStore.userInfo?.user?.busiLine?.split(',') || [];
475   - busiLineOptions.value = rawBusiLines.map(item => ({
476   - name: busiLineEnToCn(item.trim()),
477   - value: item.trim()
  368 +// 从userStore.dictList构建双向映射
  369 +buildBusiLineMap(userStore.dictList);
  370 +// 用户拥有的业务线code数组
  371 +const rawBusiLines = userStore.userInfo?.user?.busiLine?.split(',') || [];
  372 +// 过滤dict,只保留用户有权限的业务线,组装 {name, value}
  373 +busiLineOptions.value = userStore.dictList
  374 + .filter(dictItem => rawBusiLines.includes(dictItem.value))
  375 + .map(dictItem => ({
  376 + name: dictItem.label,
  377 + value: dictItem.value
478 378 }));
479 379 };
480   -
481 380 /** 业务线切换联动清空数据并重新加载级联 */
482 381 const handleBusiLineChange = (code, idx) => {
483   - const item = dispatchList.value[idx];
484   - item.roadName = '';
485   - item.roadId = 0;
486   - item.orderName = '';
487   - item.orderCode = '';
488   - cascaderValue.value = [];
489   - roadNameList.value = [];
490   -
491   - currentBusiLineCode.value = code;
492   - loadProblemCascader(code);
493   -
494   - if(baseForm.workLocation) getRoadListByBusiLine();
  382 +const item = dispatchList.value[idx];
  383 +item.roadName = '';
  384 +item.roadId = 0;
  385 +item.orderName = '';
  386 +item.orderCode = '';
  387 +cascaderValue.value = [];
  388 +roadNameList.value = [];
  389 +currentBusiLineCode.value = code;
  390 +loadProblemCascader(code);
  391 +if (baseForm.workLocation) getRoadListByBusiLine();
495 392 };
496   -
497 393 /** 根据经纬度+业务线获取道路列表 */
498 394 const getRoadListByBusiLine = async () => {
499   - if (!baseForm.lat || !baseForm.lon) return;
500   - const busiLineEn = busiLineCnToEn(dispatchList.value[0].busiLineCn);
501   - if (!busiLineEn) return uni.showToast({ title: '业务线选择异常', icon: 'none' });
502   -
503   - try {
504   - uni.showLoading({ title: '获取道路名称中...' });
505   - const roadRes = await getRoadListByLatLng({
506   - busiLine: busiLineEn,
507   - latitude: baseForm.lat,
508   - longitude: baseForm.lon
  395 +if (!baseForm.lat || !baseForm.lon) return;
  396 +const busiLineEn = busiLineCnToEn(dispatchList.value[0].busiLineCn);
  397 +if (!busiLineEn) return uni.showToast({ title: '业务线选择异常', icon: 'none' });
  398 +try {
  399 +uni.showLoading({ title: '获取道路名称中...' });
  400 +const roadRes = await getRoadListByLatLng({
  401 +busiLine: busiLineEn,
  402 +latitude: baseForm.lat,
  403 +longitude: baseForm.lon
509 404 });
510   - roadNameList.value = Array.isArray(roadRes)
511   - ? roadRes.map(item => ({ name: item.roadName || '', code: item.roadCode || '', id: item.roadId || 0 }))
512   - : [{ name: '未查询到道路名称', code: '', id: 0 }];
513   - !Array.isArray(roadRes) && uni.showToast({ title: '未查询到该位置的道路信息', icon: 'none' });
  405 +roadNameList.value = Array.isArray(roadRes)
  406 +? roadRes.map(item => ({ name: item.roadName || '', code: item.roadCode || '', id: item.roadId || 0 }))
  407 +: [{ name: '未查询到道路名称', code: '', id: 0 }];
  408 +!Array.isArray(roadRes) && uni.showToast({ title: '未查询到该位置的道路信息', icon: 'none' });
514 409 } catch (err) {
515   - console.error('获取道路名称失败:', err);
516   - roadNameList.value = [{ name: '获取失败,请重新选择位置', code: '', id: 0 }];
517   - uni.showToast({ title: '获取道路名称失败,请重试', icon: 'none' });
  410 +console.error('获取道路名称失败:', err);
  411 +roadNameList.value = [{ name: '获取失败,请重新选择位置', code: '', id: 0 }];
  412 +uni.showToast({ title: '获取道路名称失败,请重试', icon: 'none' });
518 413 } finally {
519   - uni.hideLoading();
  414 +uni.hideLoading();
520 415 }
521 416 };
522   -
523 417 // ========== 通用下拉弹窗方法(支持下标) ==========
524 418 const handleActionSheetOpen = (type, idx) => {
525   - currentDispatchIndex.value = idx;
526   - if (type === 'roadName' && !baseForm.workLocation) {
527   - return uni.showToast({ title: '请先选择工单位置', icon: 'none' });
  419 +currentDispatchIndex.value = idx;
  420 +if (type === 'roadName' && !baseForm.workLocation) {
  421 +return uni.showToast({ title: '请先选择工单位置', icon: 'none' });
528 422 }
529   - const configMap = {
530   - busiLine: { title: '请选择业务线', list: busiLineOptions.value },
531   - roadName: { title: '请选择道路名称', list: roadNameList.value },
532   - pressingType: { title: '请选择紧急程度', list: pressingTypeList.value }
  423 +const configMap = {
  424 +// 【重点改动:业务线弹窗,传入当前下标,过滤掉已占用业务线】
  425 +busiLine: { title: '请选择业务线', list: getAvailableBusiLineList(idx) },
  426 +roadName: { title: '请选择道路名称', list: roadNameList.value },
  427 +pressingType: { title: '请选择紧急程度', list: pressingTypeList.value }
533 428 };
534   - Object.assign(currentActionSheetData, configMap[type], { type, index: idx });
535   - showActionSheet.value = true;
  429 +Object.assign(currentActionSheetData, configMap[type], { type, index: idx });
  430 +showActionSheet.value = true;
536 431 };
537   -
538 432 const handleActionSheetClose = () => {
539   - showActionSheet.value = false;
540   - Object.assign(currentActionSheetData, { type: '', list: [], title: '', index: 0 });
  433 +showActionSheet.value = false;
  434 +Object.assign(currentActionSheetData, { type: '', list: [], title: '', index: 0 });
541 435 };
542   -
543 436 const handleActionSheetSelect = (e) => {
544   - const { type, index } = currentActionSheetData;
545   - const item = dispatchList.value[index];
546   - switch (type) {
547   - case 'busiLine':
548   - item.busiLineCn = e.name;
549   - item.busiLineCode = e.value;
550   - handleBusiLineChange(e.value, index);
551   - break;
552   - case 'roadName':
553   - item.roadName = e.name;
554   - item.roadId = e.code;
555   - console.log('选中道路对象e:', e)
556   - console.log('赋值后 roadId:', item.roadName, '类型:', typeof item.roadId)
557   - break;
558   - case 'pressingType':
559   - item.pressingType = e.value;
560   - item.pressingTypeName = e.name;
561   - break;
  437 +const { type, index } = currentActionSheetData;
  438 +const item = dispatchList.value[index];
  439 +switch (type) {
  440 +case 'busiLine':
  441 +item.busiLineCn = e.name;
  442 +item.busiLineCode = e.value;
  443 +handleBusiLineChange(e.value, index);
  444 +break;
  445 +case 'roadName':
  446 +item.roadName = e.name;
  447 +item.roadId = e.code;
  448 +console.log('选中道路对象e:', e)
  449 +console.log('赋值后 roadId:', item.roadName, '类型:', typeof item.roadId)
  450 +break;
  451 +case 'pressingType':
  452 +item.pressingType = e.value;
  453 +item.pressingTypeName = e.name;
  454 +break;
562 455 }
563   - handleActionSheetClose();
  456 +handleActionSheetClose();
564 457 };
565   -
566 458 // ========== 页面交互方法 ==========
567 459 const hideKeyboard = () => uni.hideKeyboard();
568   -
569 460 /** 选择地图位置 */
570 461 const chooseWorkLocation = () => {
571   - uni.chooseLocation({
572   - success: async (res) => {
573   - baseForm.workLocation = res.name;
574   - baseForm.lat = res.latitude;
575   - baseForm.lon = res.longitude;
576   - // 清空所有组道路
577   - dispatchList.value.forEach(item => {
578   - item.roadName = '';
579   - item.roadId = 0;
  462 +uni.chooseLocation({
  463 +success: async (res) => {
  464 +baseForm.workLocation = res.name;
  465 +baseForm.lat = res.latitude;
  466 +baseForm.lon = res.longitude;
  467 +// 清空所有组道路
  468 +dispatchList.value.forEach(item => {
  469 +item.roadName = '';
  470 +item.roadId = 0;
580 471 })
581   - workOrderFormRef.value?.validateField(['workLocation']);
582   - await getRoadListByBusiLine();
  472 +workOrderFormRef.value?.validateField(['workLocation']);
  473 +await getRoadListByBusiLine();
583 474 },
584   - fail: (err) => {
585   - console.error('选择位置失败:', err);
586   - const errMsg = err.errMsg.includes('auth deny') ? '请开启位置权限后重试' : '选择位置失败,请重试';
587   - uni.showToast({ title: errMsg, icon: 'none' });
  475 +fail: (err) => {
  476 +console.error('选择位置失败:', err);
  477 +const errMsg = err.errMsg.includes('auth deny') ? '请开启位置权限后重试' : '选择位置失败,请重试';
  478 +uni.showToast({ title: errMsg, icon: 'none' });
588 479 }
589 480 })
590 481 };
591   -
592 482 /** 重新提交数据回显 */
593 483 const handleRenewOptions = async (tempKey) => {
594   - isRenew.value = true;
595   - try {
596   - const orderData = uni.getStorageSync(tempKey);
597   - if (orderData && typeof orderData === 'object') {
598   - renewOrderData.value = orderData;
599   - await echoOrderData(orderData); // 等待回显执行完毕
  484 +isRenew.value = true;
  485 +try {
  486 +const orderData = uni.getStorageSync(tempKey);
  487 +if (orderData && typeof orderData === 'object') {
  488 +renewOrderData.value = orderData;
  489 +await echoOrderData(orderData); // 等待回显执行完毕
600 490 } else {
601   - uni.showToast({ title: '工单数据不存在,无法重新提交', icon: 'none' });
602   - setTimeout(toOrderListPage, 1000);
  491 +uni.showToast({ title: '工单数据不存在,无法重新提交', icon: 'none' });
  492 +setTimeout(toOrderListPage, 1000);
603 493 }
604 494 } catch (error) {
605   - console.error('读取工单数据失败:', error);
606   - uni.showToast({ title: '数据读取异常,无法重新提交', icon: 'none' });
607   - setTimeout(toOrderListPage, 1000);
  495 +console.error('读取工单数据失败:', error);
  496 +uni.showToast({ title: '数据读取异常,无法重新提交', icon: 'none' });
  497 +setTimeout(toOrderListPage, 1000);
608 498 } finally {
609   - uni.removeStorageSync(tempKey);
  499 +uni.removeStorageSync(tempKey);
610 500 }
611 501 };
612   -
613   -
614 502 /** 工单数据回显 */
615 503 const echoOrderData = async (orderItem) => {
616   - const busiCode = orderItem.busiLine || '';
617   - // 回显公共字段
618   - baseForm.workLocation = orderItem.lonLatAddress || orderItem.roadName || '';
619   - baseForm.problemDesc = orderItem.remark || '';
620   - baseForm.lat = orderItem.lat || 0;
621   - baseForm.lon = orderItem.lon || 0;
622   - console.log(orderItem)
623   - console.log(orderItem.orderCode)
624   -
625   - // 回显第一组派单
626   - if(dispatchList.value[0]){
627   - const firstItem = dispatchList.value[0]
628   - firstItem.busiLineCn = busiLineEnToCn(busiCode);
629   - firstItem.busiLineCode = busiCode;
630   - firstItem.roadId = orderItem.roadId || 0;
631   -
632   - firstItem.roadName = orderItem.roadName || '';
633   - firstItem.orderName = orderItem.orderName || '';
634   - firstItem.orderCode = orderItem.orderCode || ''; // 回显编码
635   - firstItem.pressingType = orderItem.pressingType || '';
636   - firstItem.pressingTypeName = uni.$dict.getDictLabel(CONST.DICT_PRESSING_TYPE, orderItem.pressingType) || '';
637   - firstItem.expectedFinishDate = orderItem.expectedFinishDate ? formatDate(orderItem.expectedFinishDate) : '';
  504 +const busiCode = orderItem.busiLine || '';
  505 +// 回显公共字段
  506 +baseForm.workLocation = orderItem.lonLatAddress || orderItem.roadName || '';
  507 +baseForm.problemDesc = orderItem.remark || '';
  508 +baseForm.lat = orderItem.lat || 0;
  509 +baseForm.lon = orderItem.lon || 0;
  510 +console.log(orderItem)
  511 +console.log(orderItem.orderCode)
  512 +// 回显第一组派单
  513 +if (dispatchList.value[0]) {
  514 +const firstItem = dispatchList.value[0]
  515 +firstItem.busiLineCn = busiLineEnToCn(busiCode);
  516 +firstItem.busiLineCode = busiCode;
  517 +firstItem.roadId = orderItem.roadId || 0;
  518 +firstItem.roadName = orderItem.roadName || '';
  519 +firstItem.orderName = orderItem.orderName || '';
  520 +firstItem.orderCode = orderItem.orderCode || ''; // 回显编码
  521 +firstItem.pressingType = orderItem.pressingType || '';
  522 +firstItem.pressingTypeName = uni.$dict.getDictLabel(CONST.DICT_PRESSING_TYPE, orderItem.pressingType) || '';
  523 +firstItem.expectedFinishDate = orderItem.expectedFinishDate ? formatDate(orderItem.expectedFinishDate) : '';
638 524 }
639   -
640   - // 图片回显
641   - if (Array.isArray(orderItem.problemsImgs) && orderItem.problemsImgs.length) {
642   - const imgList = orderItem.problemsImgs.map((url, idx) => ({ url, name: `renew_img_${idx}`, status: 'success' }));
643   - problemImgs.imgList.value = imgList;
644   - problemImgs.rawImgList.value = imgList;
  525 +// 图片回显
  526 +if (Array.isArray(orderItem.problemsImgs) && orderItem.problemsImgs.length) {
  527 +const imgList = orderItem.problemsImgs.map((url, idx) => ({ url, name: `renew_img_${idx}`, status: 'success' }));
  528 +problemImgs.imgList.value = imgList;
  529 +problemImgs.rawImgList.value = imgList;
645 530 }
646   -
647   - // ========== 关键修复:业务线加载完级联数据后,回填 cascaderValue ==========
648   - if (busiCode) {
649   - // 先加载级联选项
650   - await loadProblemCascader(busiCode);
651   - // 根据回显的 orderCode 反向匹配级联路径,赋值给 cascaderValue
652   - const targetCode = orderItem.orderCode
653   - if (targetCode && cascaderOptions.value.length) {
654   - // 递归查找级联选中路径
655   - const findCascaderPath = (list, targetVal, path = []) => {
656   - for (const node of list) {
657   - const newPath = [...path, node.value]
658   - if (node.value === targetVal) {
659   - return newPath
  531 +// ========== 关键修复:业务线加载完级联数据后,回填 cascaderValue ==========
  532 +if (busiCode) {
  533 +// 先加载级联选项
  534 +await loadProblemCascader(busiCode);
  535 +// 根据回显的 orderCode 反向匹配级联路径,赋值给 cascaderValue
  536 +const targetCode = orderItem.orderCode
  537 +if (targetCode && cascaderOptions.value.length) {
  538 +// 递归查找级联选中路径
  539 +const findCascaderPath = (list, targetVal, path = []) => {
  540 +for (const node of list) {
  541 +const newPath = [...path, node.value]
  542 +if (node.value === targetVal) {
  543 +return newPath
660 544 }
661   - if (node.children && node.children.length) {
662   - const res = findCascaderPath(node.children, targetVal, newPath)
663   - if (res) return res
  545 +if (node.children && node.children.length) {
  546 +const res = findCascaderPath(node.children, targetVal, newPath)
  547 +if (res) return res
664 548 }
665 549 }
666   - return null
  550 +return null
667 551 }
668   - const selectPath = findCascaderPath(cascaderOptions.value, targetCode)
669   - if (selectPath) {
670   - cascaderValue.value = selectPath
  552 +const selectPath = findCascaderPath(cascaderOptions.value, targetCode)
  553 +if (selectPath) {
  554 +cascaderValue.value = selectPath
671 555 }
672 556 }
673 557 }
674   -
675   - if (orderItem.lat && orderItem.lon) getRoadListByBusiLine();
  558 +if (orderItem.lat && orderItem.lon) getRoadListByBusiLine();
676 559 };
677   -
678 560 // ========== 接口分发(按角色) ==========
679 561 const callApiByRole = async (params, isRenewFlag) => {
680   - if (isRenewFlag) {
681   - if (USER_ROLES.includes(CONST.ROLE.MANAGER)) return await daquUniversalApproval(params);
682   - if (USER_ROLES.includes(CONST.ROLE.INSPECTOR)) return await dcyUniversalApproval(params);
683   - if (USER_ROLES.includes(CONST.ROLE.LEADER)) return await dcyUniversalApproval(params);
684   - if (USER_ROLES.includes(CONST.ROLE.PATROL)) return await qyUniversalApproval(params);
  562 +if (isRenewFlag) {
  563 +if (USER_ROLES.includes(CONST.ROLE.MANAGER)) return await daquUniversalApproval(params);
  564 +if (USER_ROLES.includes(CONST.ROLE.INSPECTOR)) return await dcyUniversalApproval(params);
  565 +if (USER_ROLES.includes(CONST.ROLE.LEADER)) return await dcyUniversalApproval(params);
  566 +if (USER_ROLES.includes(CONST.ROLE.PATROL)) return await qyUniversalApproval(params);
685 567 } else {
686   - console.log('dasda')
687   - console.log(CONST.ROLE.INSPECTOR)
688   - if (USER_ROLES.includes(CONST.ROLE.MANAGER)) return await daquWorkorderCreate(params);
689   - // if (USER_ROLES.includes(CONST.ROLE.INSPECTOR)) return await dcyWorkorderCreate(params);
690   - if (USER_ROLES.includes(CONST.ROLE.INSPECTOR)) return await dycCreateBatchNew(params);
691   - if (USER_ROLES.includes(CONST.ROLE.LEADER)) return await dycCreateBatchNew(params);
692   - if (USER_ROLES.includes(CONST.ROLE.PATROL)) return await qyWorkorderCreate(params);
693   -
694   -
  568 +console.log('dasda')
  569 +console.log(CONST.ROLE.INSPECTOR)
  570 +if (USER_ROLES.includes(CONST.ROLE.MANAGER)) return await daquWorkorderCreate(params);
  571 +// if (USER_ROLES.includes(CONST.ROLE.INSPECTOR)) return await dcyWorkorderCreate(params);
  572 +if (USER_ROLES.includes(CONST.ROLE.INSPECTOR)) return await dycCreateBatchNew(params);
  573 +if (USER_ROLES.includes(CONST.ROLE.LEADER)) return await dycCreateBatchNew(params);
  574 +if (USER_ROLES.includes(CONST.ROLE.PATROL)) return await qyWorkorderCreate(params);
695 575 }
696 576 };
697   -
698 577 // ========== 提交工单(纯数组提交,无 dispatchList,校验 roadId 非0) ==========
699 578 const submitWorkOrder = async () => {
700   - try {
701   - // 校验公共表单
702   - await workOrderFormRef.value.validate();
703   -
704   - // 校验:同一业务线 + 同一问题类型 不允许重复
  579 +try {
  580 +// 校验公共表单
  581 +await workOrderFormRef.value.validate();
  582 +// 校验:同一业务线 + 同一问题类型 不允许重复
705 583 const list = dispatchList.value;
706 584 const keySet = new Set();
707 585 let hasRepeat = false;
708   -
709 586 for (const item of list) {
710   - const busi = (item.busiLineCn || '').trim();
711   - const type = (item.orderName || '').trim();
712   - // 拼接唯一标识
713   - const uniqueKey = `${busi}_${type}`;
714   - if (keySet.has(uniqueKey)) {
715   - hasRepeat = true;
716   - break;
717   - }
718   - keySet.add(uniqueKey);
719   -}
720   -
  587 +const busi = (item.busiLineCn || '').trim();
  588 +const type = (item.orderName || '').trim();
  589 +// 拼接唯一标识
  590 +const uniqueKey = `${busi}_${type}`;
  591 +if (keySet.has(uniqueKey)) {
  592 +hasRepeat = true;
  593 +break;
  594 + }
  595 +keySet.add(uniqueKey);
  596 + }
721 597 if (hasRepeat) {
722   - uni.showToast({
723   - title: '存在同业务同问题类型,请修改后再提交',
724   - icon: 'none'
725   - });
726   - return;
727   -}
728   - // ==============================================
729   -
730   - // 组装纯数组,不再包裹 dispatchList
731   - const submitArr = dispatchList.value.map(item => {
732   - return {
733   - roadId: item.roadId,
734   - roadName: item.roadName,
735   - orderName: item.orderName,
736   - orderCode: item.orderCode,
737   - pressingType: item.pressingType,
738   - busiLine: busiLineCnToEn(item.busiLineCn),
739   - busiType: busiLineCnToEn(item.busiLineCn),
740   - expectedFinishDate: dateToTimestamp(item.expectedFinishDate),
741   - problemsImgs: problemImgs.getSuccessImgUrls(),
742   - remark: baseForm.problemDesc.trim(),
743   - latLonType: 2,
744   - lat: baseForm.lat,
745   - lon: baseForm.lon,
746   - lonLatAddress: baseForm.workLocation,
747   - sourceId: 1,
748   - sourceName: item.busiLineCn
  598 +uni.showToast({
  599 +title: '存在同业务同问题类型,请修改后再提交',
  600 +icon: 'none'
  601 + });
  602 +return;
  603 + }
  604 +// ==============================================
  605 +// 组装纯数组,不再包裹 dispatchList
  606 +const submitArr = dispatchList.value.map(item => {
  607 +return {
  608 +roadId: item.roadId,
  609 +roadName: item.roadName,
  610 +orderName: item.orderName,
  611 +orderCode: item.orderCode,
  612 +pressingType: item.pressingType,
  613 +busiLine: busiLineCnToEn(item.busiLineCn),
  614 +busiType: busiLineCnToEn(item.busiLineCn),
  615 +expectedFinishDate: dateToTimestamp(item.expectedFinishDate),
  616 +problemsImgs: problemImgs.getSuccessImgUrls(),
  617 +remark: baseForm.problemDesc.trim(),
  618 +latLonType: 2,
  619 +lat: baseForm.lat,
  620 +lon: baseForm.lon,
  621 +lonLatAddress: baseForm.workLocation,
  622 +sourceId: 1,
  623 +sourceName: item.busiLineCn
749 624 }
750 625 });
751   -
752   - // 逐项校验:保证 roadId 不为 0,以及其他必填项
753   - for (let i = 0; i < submitArr.length; i++) {
754   - const row = submitArr[i];
755   - const num = i + 1;
756   - if (!row.roadId || row.roadId === 0) {
757   - uni.showToast({ title: `第${num}条派单请选择道路名称`, icon: 'none' });
758   - return;
  626 +// 逐项校验:保证 roadId 不为 0,以及其他必填项
  627 +for (let i = 0; i < submitArr.length; i++) {
  628 +const row = submitArr[i];
  629 +const num = i + 1;
  630 +if (!row.roadId || row.roadId === 0) {
  631 +uni.showToast({ title: `第${num}条派单请选择道路名称`, icon: 'none' });
  632 +return;
759 633 }
760   - if (!row.busiLine) {
761   - uni.showToast({ title: `第${num}条派单请选择业务线`, icon: 'none' });
762   - return;
  634 +if (!row.busiLine) {
  635 +uni.showToast({ title: `第${num}条派单请选择业务线`, icon: 'none' });
  636 +return;
763 637 }
764   - if (!row.orderCode) {
765   - uni.showToast({ title: `第${num}条派单请选择问题类型`, icon: 'none' });
766   - return;
  638 +if (!row.orderCode) {
  639 +uni.showToast({ title: `第${num}条派单请选择问题类型`, icon: 'none' });
  640 +return;
767 641 }
768   - if (!row.pressingType) {
769   - uni.showToast({ title: `第${num}条派单请选择紧急程度`, icon: 'none' });
770   - return;
  642 +if (!row.pressingType) {
  643 +uni.showToast({ title: `第${num}条派单请选择紧急程度`, icon: 'none' });
  644 +return;
771 645 }
772 646 }
773   -
774   - uni.showLoading({ title: '提交中...', mask: true });
775   -
776   - let submitParams;
777   - if (isRenew.value) {
778   - // 重新提交:外层对象 + list 承载数组
779   - submitParams = {
780   - workerDataId: renewOrderData.value.id,
781   - taskKey: renewOrderData.value.taskKey,
782   - taskId: renewOrderData.value.taskId,
783   - operateType: nextStepMap[renewOrderData.value.taskKey]?.operateTypeRenew || '',
784   - agree: 0,
785   - reason: baseForm.problemDesc.trim(),
786   - ...submitArr[0]
  647 +uni.showLoading({ title: '提交中...', mask: true });
  648 +let submitParams;
  649 +if (isRenew.value) {
  650 +// 重新提交:外层对象 + list 承载数组
  651 +submitParams = {
  652 +workerDataId: renewOrderData.value.id,
  653 +taskKey: renewOrderData.value.taskKey,
  654 +taskId: renewOrderData.value.taskId,
  655 +operateType: nextStepMap[renewOrderData.value.taskKey]?.operateTypeRenew || '',
  656 +agree: 0,
  657 +reason: baseForm.problemDesc.trim(),
  658 +...submitArr[0]
787 659 };
788 660 } else {
789   - // 新建工单:根节点直接为纯数组
790   - submitParams = submitArr;
  661 +// 新建工单:根节点直接为纯数组
  662 +submitParams = submitArr;
791 663 }
792   -
793   - console.log(submitParams)
794   -
795   - await callApiByRole(submitParams, isRenew.value);
796   -
797   - uni.showToast({
798   - title: isRenew.value ? '重新提交成功' : '工单提交成功',
799   - icon: 'success',
800   - duration: 1000
  664 +console.log(submitParams)
  665 +await callApiByRole(submitParams, isRenew.value);
  666 +uni.showToast({
  667 +title: isRenew.value ? '重新提交成功' : '工单提交成功',
  668 +icon: 'success',
  669 +duration: 1000
801 670 });
802   - setTimeout(toOrderListPage, 1000);
  671 +setTimeout(toOrderListPage, 1000);
803 672 } catch (error) {
804   - uni.hideLoading();
805   - if (!Array.isArray(error)) {
806   - console.error(isRenew.value ? '工单重新提交失败:' : '工单提交失败:', error);
807   - uni.showToast({
808   - title: error.msg || '提交失败,请重试',
809   - icon: 'none',
810   - duration: 2000
  673 +uni.hideLoading();
  674 +if (!Array.isArray(error)) {
  675 +console.error(isRenew.value ? '工单重新提交失败:' : '工单提交失败:', error);
  676 +uni.showToast({
  677 +title: error.msg || '提交失败,请重试',
  678 +icon: 'none',
  679 +duration: 2000
811 680 });
812 681 }
813 682 }
... ... @@ -866,4 +735,4 @@ if (hasRepeat) {
866 735 .add-dispatch-btn-wrap {
867 736 margin: 20rpx 0;
868 737 }
869   -</style>
870 738 \ No newline at end of file
  739 +</style>
... ...
pages-sub/problem/work-order-manage/add-order.vue
... ... @@ -3,20 +3,18 @@
3 3 <view class="work-order-form-content commonPageLRpadding">
4 4 <up-form label-position="left" :model="workOrderForm" ref="workOrderFormRef" labelWidth="200rpx">
5 5 <!-- 业务线单选框:移入form内,作为第一个表单项(核心调整) -->
6   - <up-form-item label="业务线" prop="busiLineCn" border-bottom required>
7   - <up-radio-group v-model="workOrderForm.busiLineCn" placement="row" @change="handleBusiLineChange">
8   - <up-radio v-for="item in busiLineOptions" :key="item.name" :label="item.name" :name="item.name"></up-radio>
  6 + <up-form-item label="业务线" prop="busiLine" border-bottom required>
  7 + <up-radio-group v-model="busiLineDisplay" placement="row" @change="handleBusiLineChange">
  8 + <up-radio v-for="item in busiLineOptions" :key="item.value" :label="item.label"
  9 + :name="item.label"></up-radio>
9 10 </up-radio-group>
10   -
11 11 </up-form-item>
12   -
13 12 <!-- 1. 工单位置(地图选择) -->
14 13 <up-form-item label="工单位置" prop="workLocation" border-bottom required
15 14 @click="chooseWorkLocation(); hideKeyboard()">
16 15 <up-input v-model="workOrderForm.workLocation" border="none" readonly suffix-icon="map-fill"
17 16 placeholder="点击选择工单位置"></up-input>
18 17 </up-form-item>
19   -
20 18 <!-- 2. 道路名称(下拉框) -->
21 19 <up-form-item label="道路名称" prop="roadName" border-bottom required
22 20 @click="handleActionSheetOpen('roadName'); hideKeyboard()">
... ... @@ -26,7 +24,6 @@
26 24 <up-icon name="arrow-right" size="16"></up-icon>
27 25 </template>
28 26 </up-form-item>
29   -
30 27 <!-- 3. 问题类型(下拉框) -->
31 28 <up-form-item label="问题类型" prop="orderName" border-bottom required
32 29 @click="handleActionSheetOpen('orderName'); hideKeyboard()">
... ... @@ -36,7 +33,6 @@
36 33 <up-icon name="arrow-right" size="16"></up-icon>
37 34 </template>
38 35 </up-form-item>
39   -
40 36 <!-- 新增:紧急程度选择 -->
41 37 <up-form-item label="紧急程度" prop="pressingTypeName" border-bottom required
42 38 @click="handleActionSheetOpen('pressingType'); hideKeyboard()">
... ... @@ -46,13 +42,11 @@
46 42 <up-icon name="arrow-right" size="16"></up-icon>
47 43 </template>
48 44 </up-form-item>
49   -
50 45 <!-- 4. 情况描述(文本域) -->
51 46 <up-form-item label="情况描述" prop="problemDesc" required>
52 47 <up-textarea placeholder="请输入情况描述(最多200字)" v-model.trim="workOrderForm.problemDesc" count maxlength="200"
53 48 rows="4" @blur="() => workOrderFormRef.validateField('problemDesc')"></up-textarea>
54 49 </up-form-item>
55   -
56 50 <!-- 问题照片(核心修复:绑定纯数组) -->
57 51 <up-form-item label="问题照片" prop="problemImgs" border-bottom required>
58 52 <up-upload :file-list="problemImgs.imgList.value || []" @after-read="problemImgs.uploadImgs"
... ... @@ -60,7 +54,6 @@
60 54 :max-count="problemImgs.uploadConfig.maxCount" :upload-text="problemImgs.uploadConfig.uploadText"
61 55 :size-type="problemImgs.uploadConfig.sizeType"></up-upload>
62 56 </up-form-item>
63   -
64 57 <!-- 完成时间 -->
65 58 <up-form-item label="希望完成时间" prop="expectedFinishDate" @click="show = true; hideKeyboard()">
66 59 <up-input v-model="workOrderForm.expectedFinishDate" border="none" readonly placeholder="点击选择时间"></up-input>
... ... @@ -74,25 +67,21 @@
74 67 </up-form-item>
75 68 </up-form>
76 69 </view>
77   -
78 70 <!-- 底部提交按钮 -->
79 71 <view class="fixed-bottom-btn-wrap">
80 72 <up-button type="primary" text="提交工单" @click="submitWorkOrder"></up-button>
81 73 </view>
82   -
83 74 <!-- 合并后的通用下拉弹窗 -->
84 75 <up-action-sheet :show="showActionSheet" :actions="currentActionSheetData.list"
85 76 :title="currentActionSheetData.title" @close="handleActionSheetClose"
86 77 @select="handleActionSheetSelect"></up-action-sheet>
87   -
88 78 <!-- 完成时间选择器 -->
89 79 <up-datetime-picker :show="show" v-model="expectedFinishDate" mode="datetime" :min-date="new Date()"
90 80 @cancel="show = false" @confirm="expectedFinishDateConfirm"></up-datetime-picker>
91 81 </view>
92 82 </template>
93   -
94 83 <script setup>
95   -import { ref, reactive } from 'vue'
  84 +import { ref, reactive, computed } from 'vue'
96 85 import { onReady, onShow, onLoad } from '@dcloudio/uni-app';
97 86 import { useUploadImgs } from '@/common/utils/useUploadImgs'
98 87 import { getRoadListByLatLng } from '@/api/common'
... ... @@ -100,52 +89,16 @@ import { universalApproval, workorderCreate } from &#39;@/api/work-order-manage/work
100 89 import { timeFormat } from '@/uni_modules/uview-plus'
101 90 import { nextStepMap } from '@/common/utils/common'
102 91 import { useUserStore } from '@/pinia/user';
103   -
104 92 // ========== 状态管理 ==========
105 93 const userStore = useUserStore();
106   -
107   -// ========== 业务线相关状态(核心改造:已整合到workOrderForm中,删除冗余selectedBusiLine) ==========
108   -// 业务线映射表(键:英文标识,值:中文名称;新增反向映射:中文->英文)
109   -const busiLineMap = ref({
110   - 'yl': '园林',
111   - 'sz': '市政',
112   - 'wy': '物业',
113   - 'zx': '秩序管理',
114   - 'hj': '环境卫生',
115   - 'yw': '常规运维',
116   - '园林': 'yl',
117   - '市政': 'sz',
118   - '物业': 'wy',
119   - '秩序管理': 'zx',
120   - '环境卫生': 'hj',
121   - '常规运维': 'yw'
122   -});
123   -
124   -// 业务线选项列表(仅保留name字段=中文名称,适配单选框配置)
125   -const busiLineOptions = ref([]);
126   -// 格式化业务线选项(兼容1个/2个/3个的情况,生成中文名称列表)
127   -const formatBusiLineOptions = () => {
128   - if (!userStore.userInfo?.user?.busiLine) {
129   - busiLineOptions.value = [];
130   - return;
131   - }
132   - const rawBusiLines = userStore.userInfo.user.busiLine.split(','); // 拆分字符串为数组 ["yl", "sz", "wy"]
133   - console.log(rawBusiLines)
134   - // 核心改造:仅生成name字段(值为中文名称),适配单选框:label="item.name"
135   - busiLineOptions.value = rawBusiLines.map(item => ({
136   - name: busiLineMap.value[item.trim()] // name=中文名称(园林/市政/物业)
137   - }));
138   - console.log(busiLineOptions.value)
139   -};
140   -
141   -// ========== 工具方法:通过中文名称获取对应的英文标识 ==========
142   -const getBusiLineEnByCn = (cnName) => {
143   - return busiLineMap.value[cnName] || '';
144   -};
145   -
  94 +// 业务线选项,直接从pinia dictList拿
  95 +const busiLineOptions = computed(() => {
  96 + return userStore.dictList || []
  97 +})
146 98 // ========== 表单Ref ==========
147 99 const workOrderFormRef = ref(null)
148   -
  100 +// 业务线单选框展示用(存中文label)
  101 +const busiLineDisplay = ref('')
149 102 // ========== 公共上传逻辑复用 ==========
150 103 const problemImgs = useUploadImgs({
151 104 maxCount: 3,
... ... @@ -154,12 +107,10 @@ const problemImgs = useUploadImgs({
154 107 formRef: workOrderFormRef,
155 108 fieldName: 'problemImgs'
156 109 })
157   -
158 110 // 无需再手动初始化普通数组,直接使用 ref 响应式数据
159 111 if (!Array.isArray(problemImgs.rawImgList.value)) {
160 112 problemImgs.rawImgList.value = [];
161 113 }
162   -
163 114 // ========== 页面状态 ==========
164 115 // 通用弹窗控制
165 116 const showActionSheet = ref(false)
... ... @@ -172,19 +123,16 @@ const currentActionSheetData = reactive({
172 123 // 完成时间选择器控制
173 124 const show = ref(false)
174 125 const expectedFinishDate = ref(Date.now())
175   -
176 126 // ========== 重新提交相关状态(核心:本地存储读取) ==========
177 127 const isRenew = ref(false); // 是否为重新提交状态
178 128 const renewOrderData = ref(null); // 重新提交的原有工单数据
179   -
180 129 // ========== 下拉列表数据 ==========
181 130 const roadNameList = ref([])
182 131 const orderNameList = ref([])
183 132 const pressingTypeList = ref([])
184   -
185   -// ========== 工单表单数据(核心调整:新增busiLineCn字段,绑定业务线) ==========
  133 +// ========== 工单表单数据 ==========
186 134 const workOrderForm = reactive({
187   - busiLineCn: '', // 新增:业务线中文名称(用于form绑定、校验、回显)
  135 + busiLine: '', // 业务线value,yl / sz / wy
188 136 roadId: 0, // 原有字段
189 137 roadName: '', // 原有字段
190 138 workLocation: '', // 原有字段
... ... @@ -196,10 +144,9 @@ const workOrderForm = reactive({
196 144 lon: 0, // 原有字段
197 145 expectedFinishDate: '', // 原有字段
198 146 })
199   -
200   -// ========== 表单校验规则(核心调整:新增busiLineCn校验规则) ==========
  147 +// ========== 表单校验规则 ==========
201 148 const workOrderFormRules = reactive({
202   - busiLineCn: [ // 新增:业务线必选校验
  149 + busiLine: [
203 150 { type: 'string', required: true, message: '请选择业务线', trigger: ['change', 'blur'] }
204 151 ],
205 152 workLocation: [
... ... @@ -220,30 +167,25 @@ const workOrderFormRules = reactive({
220 167 ],
221 168 problemImgs: [problemImgs.imgValidateRule] // 复用上传校验规则
222 169 })
223   -
224 170 // ========== 生命周期 ==========
225   -// 页面加载:读取本地存储的工单数据(适配workOrderForm.busiLineCn)
  171 +// 页面加载
226 172 onLoad((options) => {
227   - // 初始化业务线选项
228   - formatBusiLineOptions();
229   - // 默认选中第一个业务线(赋值给workOrderForm.busiLineCn,核心调整)
  173 + // 默认选中第一个业务线
230 174 if (busiLineOptions.value.length > 0) {
231   - workOrderForm.busiLineCn = busiLineOptions.value[0].name;
  175 + // ✅ 业务线:表单存value,单选展示中文label
  176 + workOrderForm.busiLine = busiLineOptions.value[0].value
  177 + busiLineDisplay.value = busiLineOptions.value[0].label
232 178 }
233   -
234 179 // 判断是否为重新提交状态
235 180 console.log(options)
236 181 if (options.isRenew == 1 && options.tempKey) {
237 182 isRenew.value = true;
238 183 const tempKey = options.tempKey;
239   -
240 184 // 1. 从本地同步存储中读取完整工单数据
241 185 try {
242 186 const orderData = uni.getStorageSync(tempKey);
243 187 if (orderData && typeof orderData === 'object') {
244 188 renewOrderData.value = orderData;
245   -
246   - console.log('123213')
247 189 console.log(orderData)
248 190 // 2. 回显工单数据到表单
249 191 echoOrderData(renewOrderData.value);
... ... @@ -264,7 +206,6 @@ onLoad((options) =&gt; {
264 206 }
265 207 }
266 208 });
267   -
268 209 onReady(() => {
269 210 // 设置表单校验规则
270 211 if (workOrderFormRef.value) {
... ... @@ -272,27 +213,26 @@ onReady(() =&gt; {
272 213 }
273 214 console.log('工单表单规则初始化完成')
274 215 })
275   -
276 216 onShow(() => {
277 217 console.log(uni.$dict.getDictLabel('ai_image_status', 20))
278 218 console.log(uni.$dict.getDictSimpleList('work_name'))
279   -
280 219 // 初始化问题类型列表
281 220 orderNameList.value = uni.$dict.transformLabelValueToNameValue(uni.$dict.getDictSimpleList('work_name'))
282 221 console.log('问题类型列表:', orderNameList.value)
283   -
284 222 // 初始化紧急程度列表
285 223 pressingTypeList.value = uni.$dict.transformLabelValueToNameValue(uni.$dict.getDictSimpleList('workorder_pressing_type'))
286 224 console.log('紧急程度列表:', pressingTypeList.value)
287 225 })
288   -
289   -// ========== 核心方法:工单数据回显(适配workOrderForm.busiLineCn) ==========
  226 +// ========== 核心方法:工单数据回显 ==========
290 227 const echoOrderData = (orderItem) => {
291   - // 1. 回显业务线:将工单的英文busiLine转为中文名称,赋值给workOrderForm.busiLineCn(核心调整)
  228 + // 业务线回显,后端给的是value,需要自动找到中文赋值给单选框
292 229 if (orderItem.busiLine) {
293   - workOrderForm.busiLineCn = busiLineMap.value[orderItem.busiLine]; // 英文->中文(如wy->物业)
  230 + workOrderForm.busiLine = orderItem.busiLine;
  231 + const findItem = busiLineOptions.value.find(item => item.value === orderItem.busiLine)
  232 + if (findItem) {
  233 + busiLineDisplay.value = findItem.label
  234 + }
294 235 }
295   -
296 236 // 2. 原有基础表单字段回显
297 237 workOrderForm.roadId = orderItem.roadId || 0;
298 238 workOrderForm.roadName = orderItem.roadName || '';
... ... @@ -304,7 +244,6 @@ const echoOrderData = (orderItem) =&gt; {
304 244 workOrderForm.lat = orderItem.lat || 0;
305 245 workOrderForm.lon = orderItem.lon || 0;
306 246 workOrderForm.expectedFinishDate = orderItem.expectedFinishDate ? timeFormat(orderItem.expectedFinishDate, 'yyyy-mm-dd hh:MM:ss') : '';
307   -
308 247 // 3. 上传图片回显(兼容useUploadImgs格式)
309 248 if (orderItem.problemsImgs && Array.isArray(orderItem.problemsImgs) && orderItem.problemsImgs.length > 0) {
310 249 const imgList = orderItem.problemsImgs.map((imgUrl, index) => ({
... ... @@ -315,18 +254,22 @@ const echoOrderData = (orderItem) =&gt; {
315 254 problemImgs.imgList.value = imgList;
316 255 problemImgs.rawImgList.value = imgList;
317 256 }
318   -
319   - // 4. 自动获取道路列表(修改:传递当前选中的业务线英文标识)
  257 + // 4. 自动获取道路列表
320 258 if (orderItem.lat && orderItem.lon) {
321   - getRoadListByBusiLine(); // 改用封装后的方法,自动携带选中的业务线
  259 + getRoadListByBusiLine();
322 260 }
323 261 };
324   -
325   -// ========== 业务线相关方法(适配workOrderForm.busiLineCn) ==========
  262 +// ========== 业务线相关方法 ==========
326 263 /**
327 264 * 业务线切换事件:清空道路信息 + 重新请求道路列表
  265 + * selectedLabel 是选中的中文label
328 266 */
329   -const handleBusiLineChange = () => {
  267 +const handleBusiLineChange = (selectedLabel) => {
  268 + // 根据中文label反向找到对应的value存入表单
  269 + const selectedItem = busiLineOptions.value.find(item => item.label === selectedLabel)
  270 + if (selectedItem) {
  271 + workOrderForm.busiLine = selectedItem.value
  272 + }
330 273 // 1. 清空已渲染的道路名称和表单字段
331 274 workOrderForm.roadName = '';
332 275 workOrderForm.roadId = 0;
... ... @@ -337,26 +280,17 @@ const handleBusiLineChange = () =&gt; {
337 280 getRoadListByBusiLine();
338 281 }
339 282 };
340   -
341 283 /**
342   - * 封装:根据当前选中业务线(中文)获取英文标识,再请求道路列表
  284 + * 封装:根据当前选中业务线,请求道路列表
343 285 */
344 286 const getRoadListByBusiLine = async () => {
345   - if (!workOrderForm.lat || !workOrderForm.lon) {
346   - return;
347   - }
348   - // 核心:从workOrderForm.busiLineCn获取中文名称,转为英文标识(核心调整)
349   - const busiLineEn = getBusiLineEnByCn(workOrderForm.busiLineCn);
350   - if (!busiLineEn) {
351   - uni.showToast({ title: '业务线标识异常', icon: 'none' });
  287 + if (!workOrderForm.lat || !workOrderForm.lon || !workOrderForm.busiLine) {
352 288 return;
353 289 }
354   -
355 290 try {
356 291 uni.showLoading({ title: '获取道路名称中...' });
357 292 const roadRes = await getRoadListByLatLng({
358   - busiLine: busiLineEn, // 传递英文标识(yl/sz/wy),接口正常使用
359   - // companyCode: 'sls', // 传递英文标识(yl/sz/wy),接口正常使用
  293 + busiLine: workOrderForm.busiLine,
360 294 latitude: workOrderForm.lat,
361 295 longitude: workOrderForm.lon
362 296 });
... ... @@ -378,7 +312,6 @@ const getRoadListByBusiLine = async () =&gt; {
378 312 roadNameList.value = [{ name: '获取失败,请重新选择位置', code: '', id: 0 }];
379 313 }
380 314 };
381   -
382 315 // ========== 通用弹窗方法 ==========
383 316 /**
384 317 * 打开通用下拉弹窗
... ... @@ -389,7 +322,6 @@ const handleActionSheetOpen = (type) =&gt; {
389 322 uni.showToast({ title: '请先选择工单位置', icon: 'none' })
390 323 return
391 324 }
392   -
393 325 // 配置当前弹窗参数
394 326 const configMap = {
395 327 roadName: {
... ... @@ -405,13 +337,11 @@ const handleActionSheetOpen = (type) =&gt; {
405 337 list: pressingTypeList.value
406 338 }
407 339 }
408   -
409 340 currentActionSheetData.type = type
410 341 currentActionSheetData.title = configMap[type].title
411 342 currentActionSheetData.list = configMap[type].list
412 343 showActionSheet.value = true
413 344 }
414   -
415 345 /**
416 346 * 关闭通用下拉弹窗
417 347 */
... ... @@ -422,7 +352,6 @@ const handleActionSheetClose = () =&gt; {
422 352 currentActionSheetData.list = []
423 353 currentActionSheetData.title = ''
424 354 }
425   -
426 355 /**
427 356 * 通用下拉弹窗选择事件
428 357 */
... ... @@ -449,7 +378,6 @@ const handleActionSheetSelect = (e) =&gt; {
449 378 // 关闭弹窗
450 379 showActionSheet.value = false
451 380 }
452   -
453 381 /**
454 382 * 返回上一页
455 383 */
... ... @@ -462,14 +390,12 @@ const navigateBack = () =&gt; {
462 390 }
463 391 });
464 392 }
465   -
466   -// 新增:清除希望完成时间的方法
  393 +/**
  394 + * 清除希望完成时间的方法
  395 + */
467 396 const clearExpectedFinishDate = () => {
468 397 workOrderForm.expectedFinishDate = ''
469   - // 清除后触发一次字段校验,确保校验状态更新
470   - // this.$refs.inspectFormRef.validateField('expectedFinishDate')
471 398 }
472   -
473 399 /**
474 400 * 选择工单位置
475 401 */
... ... @@ -479,15 +405,11 @@ const chooseWorkLocation = () =&gt; {
479 405 workOrderForm.roadName = ''
480 406 workOrderForm.roadId = 0
481 407 roadNameList.value = []
482   -
483 408 workOrderForm.workLocation = res.name
484 409 workOrderForm.lat = res.latitude
485 410 workOrderForm.lon = res.longitude
486   -
487 411 workOrderFormRef.value?.validateField('workLocation')
488 412 workOrderFormRef.value?.validateField('roadName')
489   -
490   - // 改用封装后的方法,自动携带选中的业务线英文标识
491 413 await getRoadListByBusiLine();
492 414 },
493 415 fail: (err) => {
... ... @@ -496,7 +418,6 @@ const chooseWorkLocation = () =&gt; {
496 418 }
497 419 })
498 420 }
499   -
500 421 /**
501 422 * 完成时间确认
502 423 */
... ... @@ -505,25 +426,20 @@ const expectedFinishDateConfirm = (e) =&gt; {
505 426 workOrderForm.expectedFinishDate = timeFormat(e.value, 'yyyy-mm-dd hh:MM:ss')
506 427 show.value = false
507 428 }
508   -
509 429 /**
510 430 * 隐藏键盘
511 431 */
512 432 const hideKeyboard = () => {
513 433 uni.hideKeyboard()
514 434 }
515   -
516 435 /**
517   - * 提交工单(区分新增/重新提交,接口隔离:从workOrderForm.busiLineCn取值)
  436 + * 提交工单
518 437 */
519 438 const submitWorkOrder = async () => {
520 439 try {
521   - // 先执行表单校验(包含业务线校验)
  440 + // 先执行表单校验
522 441 await workOrderFormRef.value.validate()
523   -
524   - // 核心:从workOrderForm.busiLineCn获取中文名称,转为英文标识(核心调整)
525   - const busiLineEn = getBusiLineEnByCn(workOrderForm.busiLineCn);
526   - if (!busiLineEn) {
  442 + if (!workOrderForm.busiLine) {
527 443 uni.showToast({ title: '业务线选择异常,请重新选择', icon: 'none' });
528 444 return;
529 445 }
... ... @@ -532,7 +448,7 @@ const submitWorkOrder = async () =&gt; {
532 448 const commonSubmitData = {
533 449 roadId: workOrderForm.roadId,
534 450 roadName: workOrderForm.roadName,
535   - problemsImgs: problemImgs.getSuccessImgUrls(), // 复用上传逻辑的URL获取方法
  451 + problemsImgs: problemImgs.getSuccessImgUrls(),
536 452 remark: workOrderForm.problemDesc.trim(),
537 453 latLonType: 2,
538 454 lat: workOrderForm.lat,
... ... @@ -542,8 +458,8 @@ const submitWorkOrder = async () =&gt; {
542 458 orderName: workOrderForm.orderName,
543 459 expectedFinishDate: workOrderForm.expectedFinishDate == "" ? "" : new Date(workOrderForm.expectedFinishDate).getTime(),
544 460 sourceId: 1,
545   - sourceName: workOrderForm.busiLineCn, // 从form字段获取业务线中文名称
546   - busiLine: busiLineEn // 传递英文标识(yl/sz/wy),接口正常使用
  461 + sourceName: busiLineOptions.value.find(item => item.value === workOrderForm.busiLine)?.label || '',
  462 + busiLine: workOrderForm.busiLine
547 463 }
548 464 console.log('工单数据')
549 465 console.log('完成时间:' + workOrderForm.expectedFinishDate)
... ... @@ -551,34 +467,28 @@ const submitWorkOrder = async () =&gt; {
551 467 // 显示加载中
552 468 uni.showLoading({ title: '提交中...' })
553 469 let res
554   -
555   - // 核心:根据状态区分接口调用
556 470 if (isRenew.value) {
557 471 // 重新提交:调用 universalApproval 接口
558 472 const renewSubmitData = {
559   - // 原有工单必要参数
560 473 workerDataId: renewOrderData.value.id,
561 474 taskKey: renewOrderData.value.taskKey,
562 475 taskId: renewOrderData.value.taskId,
563 476 operateType: nextStepMap[renewOrderData.value.taskKey]?.operateTypeRenew || '',
564 477 agree: 0,
565 478 reason: workOrderForm.problemDesc.trim(),
566   - // 新编辑的工单数据
567 479 ...commonSubmitData
568 480 }
569 481 res = await universalApproval(renewSubmitData)
570 482 } else {
571   - // 新增工单:调用原有 workorderCreate 接口(不影响原有功能)
  483 + // 新增工单:调用原有 workorderCreate 接口
572 484 res = await workorderCreate(commonSubmitData)
573 485 }
574   -
575 486 uni.hideLoading()
576 487 uni.showToast({
577 488 title: isRenew.value ? '重新提交成功' : '工单提交成功',
578 489 icon: 'success',
579 490 duration: 1000
580 491 })
581   -
582 492 // 延迟跳转回列表页
583 493 setTimeout(() => {
584 494 uni.reLaunch({
... ... @@ -588,13 +498,12 @@ const submitWorkOrder = async () =&gt; {
588 498 } catch (error) {
589 499 // 隐藏加载框
590 500 uni.hideLoading()
591   -
592 501 // 区分是表单校验失败还是接口调用失败
593 502 if (!Array.isArray(error)) {
594 503 // 接口调用失败
595 504 console.error(isRenew.value ? '工单重新提交失败:' : '工单提交失败:', error)
596 505 uni.showToast({
597   - title: isRenew.value ? error.msg : error.msg,
  506 + title: error.msg || '提交失败',
598 507 icon: 'none',
599 508 duration: 2000
600 509 })
... ... @@ -602,7 +511,6 @@ const submitWorkOrder = async () =&gt; {
602 511 }
603 512 }
604 513 </script>
605   -
606 514 <style lang="scss" scoped>
607 515 // 全局页面样式
608 516 .page-container {
... ... @@ -610,9 +518,8 @@ const submitWorkOrder = async () =&gt; {
610 518 padding-bottom: 100rpx; // 给底部按钮留空间
611 519 }
612 520  
613   -
614 521 // 工单表单内容容器
615 522 .work-order-form-content {
616 523 background: #fff;
617 524 }
618   -</style>
619 525 \ No newline at end of file
  526 +</style>
... ...
pinia/user.js
1   -import { defineStore } from 'pinia';
2   -import cache from '@/common/utils/cache';
3   -import globalConfig from '@/common/config/global';
  1 +import { defineStore } from "pinia";
  2 +import cache from "@/common/utils/cache";
  3 +import globalConfig from "@/common/config/global";
4 4 // 新增:导入 refreshToken 接口
5   -import { login,smsLogin, getUserInfo, logout, moduleList, getSimpleDictDataList, refreshToken } from '@/api/user';
6   -
  5 +import {
  6 + login,
  7 + smsLogin,
  8 + getUserInfo,
  9 + logout,
  10 + moduleList,
  11 + getSimpleDictDataList,
  12 + refreshToken,
  13 +} from "@/api/user";
7 14 // 新增:定义刷新Token的定时器标识(避免重复创建定时器)
8 15 let refreshTokenTimer = null;
9   -
10   -export const useUserStore = defineStore('user', {
  16 +export const useUserStore = defineStore("user", {
11 17 // 修复1:删除重复的 state 定义,只保留根层级的 state
12 18 state: () => ({
13   - token: cache.get(globalConfig.cache.tokenKey) || '', // 初始值从缓存取(兼容持久化)
14   - refreshToken: cache.get(globalConfig.cache.refreshTokenKey) || '', // 新增:Refresh Token 状态
  19 + token: cache.get(globalConfig.cache.tokenKey) || "", // 初始值从缓存取(兼容持久化)
  20 + refreshToken: cache.get(globalConfig.cache.refreshTokenKey) || "", // 新增:Refresh Token 状态
15 21 userInfo: cache.get(globalConfig.cache.userInfoKey) || {},
16   - userId: cache.get(globalConfig.cache.userIdKey) || '',
17   - moduleListInfo: cache.get(globalConfig.cache.moduleListKey) || '',
  22 + userId: cache.get(globalConfig.cache.userIdKey) || "",
  23 + moduleListInfo: cache.get(globalConfig.cache.moduleListKey) || "",
18 24 expireTime: cache.get(globalConfig.cache.expireTimeKey) || 0,
19 25 dictData: cache.get(globalConfig.cache.dictDataKey) || {},
20   - logoutLoading: false
  26 + dictList: cache.get(globalConfig.cache.dictListKey) || [], // 用来存放【过滤后的】字典数组
  27 + logoutLoading: false,
21 28 }),
22   -
23 29 getters: {
24 30 isLogin: (state) => {
25 31 if (!state.token) return false;
... ... @@ -35,104 +41,114 @@ export const useUserStore = defineStore(&#39;user&#39;, {
35 41 const remainingTime = state.expireTime - Date.now();
36 42 // 剩余时间大于0秒 且 小于30秒(即将过期)
37 43 return remainingTime > 0 && remainingTime < 30 * 1000;
38   - }
  44 + },
39 45 },
40   -
41 46 actions: {
42 47 async login(params) {
43 48 try {
44 49 // smsLogin
45   - let res
46   - if(params.type=='sms'){
  50 + let res;
  51 + if (params.type == "sms") {
47 52 res = await smsLogin(params);
48   - }else{
  53 + } else {
49 54 res = await login(params);
50 55 }
51   -
52 56 // 新增:从登录接口返回值中获取 refreshToken(若接口返回字段名不一致,可调整,如 res.refresh_token)
53 57 const { accessToken, expiresTime, userId, refreshToken } = res;
54   -
55 58 if (!accessToken) {
56   - throw new Error('登录失败,未获取到令牌');
  59 + throw new Error("登录失败,未获取到令牌");
57 60 }
58   -
59 61 // 更新 Pinia state
60 62 this.token = accessToken;
61   - this.refreshToken = refreshToken || ''; // 新增:存储Refresh Token
  63 + this.refreshToken = refreshToken || ""; // 新增:存储Refresh Token
62 64 this.expireTime = expiresTime;
63 65 this.userId = userId;
64 66 this.userInfo = {};
65   -
66 67 // 等待 Pinia 持久化同步
67   - await new Promise(resolve => setTimeout(resolve, 50));
68   -
  68 + await new Promise((resolve) => setTimeout(resolve, 50));
69 69 // 获取用户信息
70 70 const userInfo = await this.getUserInfo();
71 71 this.userInfo = userInfo;
72   -
73 72 // 获取模块列表
74 73 let moduleListInfo = null;
75 74 try {
76 75 moduleListInfo = await this.getModuleList();
77 76 this.moduleListInfo = moduleListInfo;
78 77 } catch (moduleErr) {
79   - console.warn('获取模块列表失败(不影响登录):', moduleErr);
80   - uni.showToast({ title: '获取模块列表失败,可正常登录', icon: 'none' });
  78 + console.warn("获取模块列表失败(不影响登录):", moduleErr);
  79 + uni.showToast({
  80 + title: "获取模块列表失败,可正常登录",
  81 + icon: "none",
  82 + });
81 83 }
82   -
83 84 // 修复2:调用字典接口时,不直接抛错(避免阻断登录)
84 85 try {
85 86 const dictRes = await this.getAndSaveDictData();
86 87 // 仅接口返回成功时才更新字典
87 88 this.dictData = dictRes || {};
88   - console.log('字典数据获取成功:', this.dictData);
  89 + console.log("字典数据获取成功:", this.dictData);
  90 + // 获取原始完整字典
  91 + const originDictList = uni.$dict.getDictSimpleList("business_line");
  92 + console.log("原始完整字典:", originDictList);
  93 +
  94 + // ========== 核心:根据 userInfo.busiLine 过滤,结果存入 dictList ==========
  95 +
  96 + // this.userInfo?.busiLine
  97 +
  98 + console.log("用户信息", this.userInfo.user);
  99 + if (this.userInfo.user.busiLine && originDictList?.length) {
  100 + const busiValueArr = this.userInfo.user.busiLine
  101 + .split(",")
  102 + .map((item) => item.trim())
  103 + .filter(Boolean);
  104 + this.dictList = originDictList.filter((item) => {
  105 + return busiValueArr.includes(item.value);
  106 + });
  107 + } else {
  108 + this.dictList = [];
  109 + }
  110 + console.log("过滤完成,dictList = ", this.dictList);
89 111 } catch (dictErr) {
90   - console.warn('获取字典失败(不影响登录):', dictErr);
91   - uni.showToast({ title: '获取字典失败,可正常使用', icon: 'none' });
  112 + console.warn("获取字典失败(不影响登录):", dictErr);
  113 + uni.showToast({ title: "获取字典失败,可正常使用", icon: "none" });
  114 + this.dictList = []; // 异常情况置空
92 115 }
93   -
94 116 // 新增:登录成功后,启动 Token 自动刷新定时器
95 117 this.startRefreshTokenTimer();
96   -
97 118 return { ...res, userInfo, moduleListInfo };
98 119 } catch (err) {
99   - console.error('登录流程失败:', err);
  120 + console.error("登录流程失败:", err);
100 121 throw err;
101 122 }
102 123 },
103   -
104 124 // 新增:调用刷新Token接口,返回刷新是否成功
105 125 async refreshTokenApi() {
106 126 // 前置校验:无Refresh Token直接返回失败
107 127 if (!this.refreshToken) {
108   - console.warn('无刷新令牌,无法刷新Token');
  128 + console.warn("无刷新令牌,无法刷新Token");
109 129 this.logout(); // 无刷新令牌,直接退出登录
110 130 return false;
111 131 }
112   -
113 132 try {
114 133 // 调用修正后的 refreshToken 接口,传入当前的 refreshToken
115 134 const res = await refreshToken(this.refreshToken);
116   -
117 135 // 校验接口返回结果(根据你的实际接口返回格式调整)
118 136 if (!res || !res.accessToken) {
119   - throw new Error('刷新Token失败,未获取到新令牌');
  137 + throw new Error("刷新Token失败,未获取到新令牌");
120 138 }
121   -
122 139 // 更新 Token 相关状态
123 140 this.token = res.accessToken; // 新的访问令牌
124 141 this.refreshToken = res.refreshToken || this.refreshToken; // 若接口返回新的Refresh Token则更新
125 142 this.expireTime = res.expiresTime; // 新的过期时间
126   - console.log('Token 刷新成功');
  143 + console.log("Token 刷新成功");
127 144 return true;
128 145 } catch (err) {
129   - console.error('刷新Token失败:', err);
  146 + console.error("刷新Token失败:", err);
130 147 // 刷新失败,直接退出登录
131 148 this.logout();
132 149 return false;
133 150 }
134 151 },
135   -
136 152 // 新增:启动 Token 自动刷新定时器
137 153 startRefreshTokenTimer() {
138 154 // 先清除已有定时器,避免重复创建
... ... @@ -140,7 +156,6 @@ export const useUserStore = defineStore(&#39;user&#39;, {
140 156 clearTimeout(refreshTokenTimer);
141 157 refreshTokenTimer = null;
142 158 }
143   -
144 159 // 计算剩余时间(过期前30秒执行刷新)
145 160 const remainingTime = this.expireTime - Date.now() - 30 * 1000;
146 161 if (remainingTime <= 0) {
... ... @@ -148,7 +163,6 @@ export const useUserStore = defineStore(&#39;user&#39;, {
148 163 this.refreshTokenApi();
149 164 return;
150 165 }
151   -
152 166 // 设置定时器,到期后执行刷新
153 167 refreshTokenTimer = setTimeout(async () => {
154 168 const refreshSuccess = await this.refreshTokenApi();
... ... @@ -157,9 +171,10 @@ export const useUserStore = defineStore(&#39;user&#39;, {
157 171 this.startRefreshTokenTimer();
158 172 }
159 173 }, remainingTime);
160   - console.log(`Token 自动刷新定时器已启动,将在 ${Math.ceil(remainingTime / 1000)} 秒后执行刷新`);
  174 + console.log(
  175 + `Token 自动刷新定时器已启动,将在 ${Math.ceil(remainingTime / 1000)} 秒后执行刷新`,
  176 + );
161 177 },
162   -
163 178 // 新增:停止 Token 自动刷新定时器(避免内存泄漏)
164 179 stopRefreshTokenTimer() {
165 180 if (refreshTokenTimer) {
... ... @@ -167,171 +182,169 @@ export const useUserStore = defineStore(&#39;user&#39;, {
167 182 refreshTokenTimer = null;
168 183 }
169 184 },
170   -
171 185 // 修复3:重构字典获取方法(加 Token 校验 + 强制携带 Token + 宽松错误处理)
172 186 async getAndSaveDictData() {
173 187 // 前置校验:无登录态直接返回,不请求接口
174 188 if (!this.isLogin) {
175   - console.warn('未登录,跳过字典获取');
176   - return { code: -1, msg: '未登录' };
  189 + console.warn("未登录,跳过字典获取");
  190 + return { code: -1, msg: "未登录" };
177 191 }
178   -
179 192 try {
180 193 // 强制携带 Token(和 getModuleList 保持一致,避免拦截器同步延迟)
181 194 const res = await getSimpleDictDataList(
182 195 {}, // 接口入参(按需传,比如 dictType: ['level'])
183   - { header: { 'Authorization': `Bearer ${this.token}` } }
  196 + { header: { Authorization: `Bearer ${this.token}` } },
184 197 );
185   -
186 198 // 校验接口返回码(核心:避免非 0 码数据存入)
187 199 if (res.code !== 0) {
188   - console.warn('字典接口返回失败:', res.msg);
  200 + console.warn("字典接口返回失败:", res.msg);
189 201 return res; // 返回错误信息,但不抛错
190 202 }
191 203 return res;
192 204 } catch (err) {
193 205 // 修复4:宽松错误处理,只打印日志,不抛错(避免阻断登录)
194   - console.error('字典接口请求异常:', err);
195   - return { code: -2, msg: '接口请求异常:' + (err.message || '网络错误') };
  206 + console.error("字典接口请求异常:", err);
  207 + return {
  208 + code: -2,
  209 + msg: "接口请求异常:" + (err.message || "网络错误"),
  210 + };
196 211 }
197 212 },
198   -
199 213 async getModuleList() {
200 214 try {
201 215 if (!this.token) {
202   - throw new Error('未获取到登录令牌,无法获取模块列表');
  216 + throw new Error("未获取到登录令牌,无法获取模块列表");
203 217 }
204   -
205   - const res = await moduleList({}, {
206   - header: { 'Authorization': `Bearer ${this.token}` }
207   - });
  218 + const res = await moduleList(
  219 + {},
  220 + {
  221 + header: { Authorization: `Bearer ${this.token}` },
  222 + },
  223 + );
208 224 return res;
209 225 } catch (err) {
210   - console.error('获取用户菜单失败:', err);
211   - if (err?.data?.code === 401 || err?.message.includes('401')) {
212   - throw new Error('登录态已过期,请重新登录');
  226 + console.error("获取用户菜单失败:", err);
  227 + if (err?.data?.code === 401 || err?.message.includes("401")) {
  228 + throw new Error("登录态已过期,请重新登录");
213 229 } else {
214   - throw new Error('获取用户菜单失败,请重新登录');
  230 + throw new Error("获取用户菜单失败,请重新登录");
215 231 }
216 232 }
217 233 },
218   -
219 234 async getUserInfo() {
220 235 try {
221 236 const res = await getUserInfo();
222 237 return res;
223 238 } catch (err) {
224   - console.error('获取用户信息失败:', err);
225   - throw new Error('获取用户信息失败,请重新登录');
  239 + console.error("获取用户信息失败:", err);
  240 + throw new Error("获取用户信息失败,请重新登录");
226 241 }
227 242 },
228   -
229 243 logout() {
230 244 // 新增:退出登录时,停止刷新定时器
231 245 this.stopRefreshTokenTimer();
232   -
233 246 const pages = getCurrentPages();
234 247 if (pages.length === 0) return;
235   -
236 248 const currentPageRoute = pages[pages.length - 1].route;
237 249 const loginPath = globalConfig.router.loginPath
238   - .replace(/^\//, '')
239   - .split('?')[0];
240   -
  250 + .replace(/^\//, "")
  251 + .split("?")[0];
241 252 if (currentPageRoute === loginPath) {
242   - this.token = '';
243   - this.refreshToken = ''; // 新增:清空Refresh Token
  253 + this.token = "";
  254 + this.refreshToken = ""; // 新增:清空Refresh Token
244 255 this.userInfo = {};
245   - this.userId = '';
246   - this.moduleListInfo = '';
  256 + this.userId = "";
  257 + this.moduleListInfo = "";
247 258 this.expireTime = 0;
248 259 this.dictData = {};
  260 + this.dictList = [];
249 261 return;
250 262 }
251   -
252 263 const logoutWithLock = async () => {
253 264 if (this.logoutLoading) return;
254 265 this.logoutLoading = true;
255   -
256 266 try {
257 267 await logout();
258 268 } catch (err) {
259   - console.error('退出登录接口调用失败:', err);
  269 + console.error("退出登录接口调用失败:", err);
260 270 } finally {
261   - this.token = '';
262   - this.refreshToken = ''; // 新增:清空Refresh Token
  271 + this.token = "";
  272 + this.refreshToken = ""; // 新增:清空Refresh Token
263 273 this.userInfo = {};
264   - this.userId = '';
265   - this.moduleListInfo = '';
  274 + this.userId = "";
  275 + this.moduleListInfo = "";
266 276 this.dictData = {};
  277 + this.dictList = [];
267 278 this.expireTime = 0;
268 279 this.logoutLoading = false;
269   -
270 280 uni.redirectTo({
271   - url: globalConfig.router.loginPath
  281 + url: globalConfig.router.loginPath,
272 282 });
273 283 }
274 284 };
275   -
276 285 logoutWithLock();
277 286 },
278   -
279 287 checkLogin() {
280 288 if (!this.isLogin) {
281 289 const pages = getCurrentPages();
282 290 if (pages.length === 0) return false;
283   -
284 291 const currentPageRoute = pages[pages.length - 1].route;
285 292 const loginPath = globalConfig.router.loginPath
286   - .replace(/^\//, '')
287   - .split('?')[0];
288   -
  293 + .replace(/^\//, "")
  294 + .split("?")[0];
289 295 if (currentPageRoute !== loginPath) {
290 296 uni.redirectTo({
291   - url: globalConfig.router.loginPath
  297 + url: globalConfig.router.loginPath,
292 298 });
293 299 }
294 300 return false;
295 301 }
296   -
297 302 // 新增:已登录时,启动刷新定时器(防止页面刷新后定时器丢失)
298 303 this.startRefreshTokenTimer();
299 304 return true;
300   - }
  305 + },
301 306 },
302   -
303 307 persist: {
304 308 enabled: true,
305   - key: 'user_store',
  309 + key: "user_store",
306 310 storage: {
307 311 getItem: (key) => uni.getStorageSync(key),
308 312 setItem: (key, value) => uni.setStorageSync(key, value),
309   - removeItem: (key) => uni.removeStorageSync(key)
  313 + removeItem: (key) => uni.removeStorageSync(key),
310 314 },
311 315 serializer: {
312 316 serialize: (state) => {
313 317 uni.setStorageSync(globalConfig.cache.tokenKey, state.token);
314   - uni.setStorageSync(globalConfig.cache.refreshTokenKey, state.refreshToken); // 新增:持久化Refresh Token
  318 + uni.setStorageSync(
  319 + globalConfig.cache.refreshTokenKey,
  320 + state.refreshToken,
  321 + );
315 322 uni.setStorageSync(globalConfig.cache.userIdKey, state.userId);
316 323 uni.setStorageSync(globalConfig.cache.expireTimeKey, state.expireTime);
317 324 uni.setStorageSync(globalConfig.cache.userInfoKey, state.userInfo);
318   - uni.setStorageSync(globalConfig.cache.moduleListKey, state.moduleListInfo);
  325 + uni.setStorageSync(
  326 + globalConfig.cache.moduleListKey,
  327 + state.moduleListInfo,
  328 + );
319 329 uni.setStorageSync(globalConfig.cache.dictDataKey, state.dictData);
  330 + uni.setStorageSync(globalConfig.cache.dictListKey, state.dictList);
320 331 return state;
321 332 },
322 333 deserialize: (value) => {
323 334 return {
324   - token: uni.getStorageSync(globalConfig.cache.tokenKey) || '',
325   - refreshToken: uni.getStorageSync(globalConfig.cache.refreshTokenKey) || '', // 新增:读取Refresh Token
326   - userId: uni.getStorageSync(globalConfig.cache.userIdKey) || '',
  335 + token: uni.getStorageSync(globalConfig.cache.tokenKey) || "",
  336 + refreshToken:
  337 + uni.getStorageSync(globalConfig.cache.refreshTokenKey) || "",
  338 + userId: uni.getStorageSync(globalConfig.cache.userIdKey) || "",
327 339 expireTime: uni.getStorageSync(globalConfig.cache.expireTimeKey) || 0,
328 340 userInfo: uni.getStorageSync(globalConfig.cache.userInfoKey) || {},
329   - moduleListInfo: uni.getStorageSync(globalConfig.cache.moduleListKey) || '',
  341 + moduleListInfo:
  342 + uni.getStorageSync(globalConfig.cache.moduleListKey) || "",
330 343 dictData: uni.getStorageSync(globalConfig.cache.dictDataKey) || {},
331   - logoutLoading: false
  344 + dictList: uni.getStorageSync(globalConfig.cache.dictListKey) || [],
  345 + logoutLoading: false,
332 346 };
333   - }
  347 + },
334 348 },
335   - // paths: []
336   - }
337   -});
338 349 \ No newline at end of file
  350 + },
  351 +});
... ...