cloud-web-corejs 1.0.54-dev.745 → 1.0.54-dev.746

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/package.json +192 -192
  2. package/src/App.vue +73 -71
  3. package/src/components/baseArea/index.vue +1628 -1628
  4. package/src/components/fileLibrary/categoryMoveDialog.vue +109 -109
  5. package/src/components/fileLibrary/fileObjAuthDialog.vue +297 -297
  6. package/src/components/fileLibrary/index.vue +1109 -1109
  7. package/src/components/fileLibrary/propertiesDialog.vue +190 -190
  8. package/src/components/fileLibrary/recycleBinDialog.vue +237 -237
  9. package/src/components/wf/mixins/setCandidateDialog.js +217 -217
  10. package/src/components/wf/mixins/setCandidateDialog2.js +252 -252
  11. package/src/components/xform/docs//345/257/271/346/257/224/345/212/237/350/203/275/350/220/275/345/234/260/346/226/271/346/241/210.md +986 -986
  12. package/src/components/xform/form-designer/form-widget/field-widget/area-select-widget.vue +272 -272
  13. package/src/components/xform/form-designer/form-widget/field-widget/baseAttachment-widget.vue +216 -216
  14. package/src/components/xform/form-designer/form-widget/field-widget/form-item-wrapper.vue +928 -928
  15. package/src/components/xform/form-designer/index.vue +195 -195
  16. package/src/components/xform/form-designer/indexMixin.js +848 -848
  17. package/src/components/xform/form-designer/setting-panel/index.vue +313 -313
  18. package/src/components/xform/form-designer/setting-panel/property-editor/container-data-table/columnRenderDialog.vue +124 -124
  19. package/src/components/xform/form-designer/setting-panel/propertyRegister.js +369 -369
  20. package/src/components/xform/form-designer/widget-panel/index.vue +389 -389
  21. package/src/components/xform/form-designer/widget-panel/indexMixin.js +343 -343
  22. package/src/components/xform/form-designer/widget-panel/widgetsConfig.js +4531 -4531
  23. package/src/components/xform/form-render/container-item/data-table-mixin.js +4748 -4576
  24. package/src/components/xform/form-render/container-item/tab-item.vue +125 -125
  25. package/src/components/xform/form-render/indexMixin.js +5061 -5078
  26. package/src/components/xform/lang/en-US.js +457 -457
  27. package/src/components/xform/lang/zh-CN.js +628 -628
  28. package/src/components/xform/utils/util.js +1585 -1554
  29. package/src/index.js +344 -230
  30. package/src/layout/components/AppMain.vue +122 -125
  31. package/src/layout/components/extractedCode/viewDialog.vue +207 -207
  32. package/src/layout/components/langTool.vue +128 -123
  33. package/src/layout/defaultLayout.vue +164 -158
  34. package/src/permission.js +185 -135
  35. package/src/store/config/index.js +667 -669
  36. package/src/store/modules/micro.js +46 -0
  37. package/src/store/modules/permission.js +461 -373
  38. package/src/store/modules/user.js +419 -415
  39. package/src/utils/auth.js +27 -1
  40. package/src/utils/index.js +579 -6
  41. package/src/utils/keepAlive.js +4 -0
  42. package/src/utils/menuAdapter.js +183 -0
  43. package/src/utils/request.js +417 -28
  44. package/src/views/bd/setting/formVersion/ftHistoryDialog.vue +483 -483
  45. package/src/views/bd/setting/form_script/form_list.vue +174 -174
  46. package/src/views/bd/setting/form_script/mixins/form_list.js +330 -330
  47. package/src/views/bd/setting/form_template/formDesignerDialog.vue +171 -171
  48. package/src/views/bd/setting/form_template/wfObjConfigDialog.vue +253 -253
  49. package/src/views/user/menuFallback/index.vue +123 -0
  50. package/src/views/user/wf/wfReport/index.vue +625 -625
@@ -1,1628 +1,1628 @@
1
- <template>
2
- <div class="base-area">
3
- <el-popover
4
- v-model="popoverVisible"
5
- placement="bottom-start"
6
- trigger="click"
7
- popper-class="base-area-popover"
8
- :width="popoverWidth"
9
- :disabled="disabled || readonly"
10
- @show="handlePopoverShow"
11
- @hide="handlePopoverHide"
12
- >
13
- <div class="base-area-panel">
14
- <div class="base-area-search">
15
- <el-input
16
- ref="searchInputRef"
17
- v-model="searchKeyword"
18
- class="base-area-search-input"
19
- size="small"
20
- clearable
21
- :placeholder="searchPlaceholder"
22
- @input="handleSearchInput"
23
- @clear="handleSearchClear"
24
- @focus="searchDropdownVisible = true"
25
- />
26
- <transition name="base-area-fade">
27
- <div
28
- v-show="showSearchDropdown"
29
- class="base-area-search-dropdown"
30
- v-loading="searchLoading"
31
- >
32
- <div
33
- v-for="item in searchList"
34
- :key="'search-' + item.id"
35
- class="base-area-result-item"
36
- :class="{ on: isSearchItemActive(item) }"
37
- @mousedown.prevent
38
- @click="selectSearchItem(item)"
39
- >
40
- {{ item.fullName || item.name }}
41
- </div>
42
- <div
43
- v-if="!searchLoading && !searchList.length"
44
- class="base-area-dropdown-empty"
45
- >
46
- {{ emptyText }}
47
- </div>
48
- </div>
49
- </transition>
50
- </div>
51
- <div class="base-area-path">
52
- <div class="base-area-tabs">
53
- <template v-for="(tab, index) in tabs">
54
- <span
55
- v-if="index > 0"
56
- :key="'sep-' + index"
57
- class="base-area-path-sep"
58
- >/</span
59
- >
60
- <span
61
- :key="'tab-' + index"
62
- class="base-area-tab"
63
- :class="{
64
- active: activeLevel === index + 1,
65
- selected: !!tab.selected,
66
- disabled: !isTabEnabled(index + 1),
67
- }"
68
- @click="switchLevel(index + 1)"
69
- >{{ getTabLabel(tab) }}</span
70
- >
71
- </template>
72
- </div>
73
- </div>
74
- <div class="base-area-grid-wrap">
75
- <div class="base-area-grid" v-loading="browseLoading">
76
- <div
77
- v-for="item in levelItems"
78
- :key="item.id"
79
- class="base-area-item"
80
- :class="{ on: isSelected(item) }"
81
- @click="selectItem(item)"
82
- >
83
- <span>{{ item.name }}</span>
84
- </div>
85
- <div
86
- v-if="!browseLoading && !levelItems.length"
87
- class="base-area-empty"
88
- >
89
- {{ emptyText }}
90
- </div>
91
- </div>
92
- </div>
93
- <div class="base-area-level-nav">
94
- <el-button
95
- size="mini"
96
- class="base-area-nav-btn"
97
- :disabled="!canPrevLevel"
98
- @mousedown.prevent
99
- @click="prevLevel"
100
- >
101
- <i class="el-icon-arrow-left"></i>
102
- </el-button>
103
- <span class="base-area-level-label">{{ currentLevelLabel }}</span>
104
- <el-button
105
- size="mini"
106
- class="base-area-nav-btn"
107
- :disabled="!canNextLevel"
108
- @mousedown.prevent
109
- @click="nextLevel"
110
- >
111
- <i class="el-icon-arrow-right"></i>
112
- </el-button>
113
- </div>
114
- <div class="base-area-footer">
115
- <el-button
116
- type="primary"
117
- plain
118
- class="button-sty"
119
- @mousedown.prevent
120
- @click="cancelSelection"
121
- >
122
- <i class="el-icon-close el-icon"></i>
123
- {{ cancelText }}
124
- </el-button>
125
- <el-button
126
- type="primary"
127
- class="button-sty"
128
- :disabled="!canConfirm"
129
- @mousedown.prevent
130
- @click="confirmSelection"
131
- >
132
- <i class="el-icon-check el-icon"></i>
133
- {{ confirmText }}
134
- </el-button>
135
- </div>
136
- </div>
137
- <div slot="reference" class="base-area-trigger">
138
- <el-input
139
- ref="inputRef"
140
- class="base-area-input"
141
- :class="{ 'is-focus': referenceFocused }"
142
- :value="displayText"
143
- :size="size"
144
- :disabled="disabled"
145
- :placeholder="placeholder"
146
- readonly
147
- @focus="referenceFocused = true"
148
- @blur="referenceFocused = false"
149
- >
150
- <i
151
- v-if="showClearIcon"
152
- slot="suffix"
153
- class="el-input__icon el-icon-circle-close base-area-clear"
154
- @mousedown.prevent
155
- @click.stop="handleClear"
156
- />
157
- </el-input>
158
- </div>
159
- </el-popover>
160
- </div>
161
- </template>
162
-
163
- <script>
164
- const LEVEL_LABELS = ["省份", "城市", "区县", "乡镇"];
165
- const AREA_LEVEL_KEYS = ["province", "city", "district", "town"];
166
- const MAX_AREA_DEPTH = 4;
167
- const SEARCH_DEBOUNCE = 300;
168
-
169
- export default {
170
- name: "baseArea",
171
- props: {
172
- value: {
173
- type: [String, Number],
174
- default: null,
175
- },
176
- labelValue: {
177
- type: String,
178
- default: null,
179
- },
180
- maxLevel: {
181
- type: Number,
182
- default: 4,
183
- },
184
- /** 至少选到该层级才提交值(1-4);分支无下级数据时视为末级 */
185
- minLevel: {
186
- type: Number,
187
- default: 1,
188
- },
189
- locale: {
190
- type: String,
191
- default: null,
192
- },
193
- disabled: {
194
- type: Boolean,
195
- default: false,
196
- },
197
- readonly: {
198
- type: Boolean,
199
- default: false,
200
- },
201
- clearable: {
202
- type: Boolean,
203
- default: true,
204
- },
205
- placeholder: {
206
- type: String,
207
- default: "请选择地区",
208
- },
209
- searchPlaceholder: {
210
- type: String,
211
- default: "请输入地区名称",
212
- },
213
- emptyText: {
214
- type: String,
215
- default: "暂无匹配地区",
216
- },
217
- confirmText: {
218
- type: String,
219
- default: "确 定",
220
- },
221
- cancelText: {
222
- type: String,
223
- default: "取 消",
224
- },
225
- size: {
226
- type: String,
227
- default: "small",
228
- },
229
- popoverWidth: {
230
- type: Number,
231
- default: 630,
232
- },
233
- /** leaf: 绑定末级 id;path: 兼容旧版逗号分隔路径 */
234
- valueFormat: {
235
- type: String,
236
- default: "leaf",
237
- },
238
- },
239
- data() {
240
- return {
241
- popoverVisible: false,
242
- searchKeyword: "",
243
- searchList: [],
244
- selectedPath: [],
245
- activeLevel: 1,
246
- levelItems: [],
247
- browseLoading: false,
248
- searchLoading: false,
249
- searchDropdownVisible: false,
250
- referenceFocused: false,
251
- searchTimer: null,
252
- syncToken: 0,
253
- skipValueSync: false,
254
- committedBrowseSnapshot: null,
255
- committedBoundValue: null,
256
- lastSelectionPayload: null,
257
- pendingLeaf: false,
258
- };
259
- },
260
- computed: {
261
- hasSearchKeyword() {
262
- return !!(this.searchKeyword || "").trim();
263
- },
264
- showSearchDropdown() {
265
- return this.searchDropdownVisible && this.hasSearchKeyword;
266
- },
267
- displayText() {
268
- if (this.labelValue) {
269
- return this.labelValue;
270
- }
271
- const pathLabel = this.selectedPath.length
272
- ? this.buildPathLabel(this.selectedPath)
273
- : "";
274
- if (this.popoverVisible && pathLabel) {
275
- return pathLabel;
276
- }
277
- return pathLabel || "";
278
- },
279
- tabs() {
280
- const tabs = [];
281
- for (let i = 0; i < this.browseLevelCount; i++) {
282
- tabs.push({
283
- level: i + 1,
284
- label: LEVEL_LABELS[i] || `第${i + 1}级`,
285
- selected: this.selectedPath[i] || null,
286
- });
287
- }
288
- return tabs;
289
- },
290
- showClearIcon() {
291
- return (
292
- this.clearable && !this.readonly && !this.disabled && !!this.displayText
293
- );
294
- },
295
- canPrevLevel() {
296
- return this.activeLevel > 1;
297
- },
298
- canNextLevel() {
299
- return (
300
- this.activeLevel < this.browseLevelCount &&
301
- this.isTabEnabled(this.activeLevel + 1)
302
- );
303
- },
304
- browseLevelCount() {
305
- return Math.max(this.maxLevel, this.selectedPath.length, 1);
306
- },
307
- effectiveMinLevel() {
308
- const min = this.minLevel == null ? 1 : this.minLevel;
309
- return Math.max(1, Math.min(min, this.maxLevel));
310
- },
311
- canConfirm() {
312
- if (!this.selectedPath.length) {
313
- return false;
314
- }
315
- /* 达到最小选择层级,或该地区无下级数据(视为末级) */
316
- return (
317
- this.selectedPath.length >= this.effectiveMinLevel || this.pendingLeaf
318
- );
319
- },
320
- currentLevelLabel() {
321
- return LEVEL_LABELS[this.activeLevel - 1] || `第${this.activeLevel}级`;
322
- },
323
- },
324
- watch: {
325
- value: {
326
- immediate: true,
327
- handler() {
328
- this.syncFromValue();
329
- },
330
- },
331
- labelValue(val) {
332
- if (!val && !this.value) {
333
- this.resetBrowseState(false);
334
- }
335
- },
336
- },
337
- beforeDestroy() {
338
- clearTimeout(this.searchTimer);
339
- },
340
- methods: {
341
- getResolveDepthLimit(item) {
342
- if (item && item.treePath) {
343
- const ids = this.parseTreePathIds(item.treePath);
344
- return Math.min(Math.max(ids.length, this.maxLevel, 1), MAX_AREA_DEPTH);
345
- }
346
- return MAX_AREA_DEPTH;
347
- },
348
- isPathResolvedForItem(path, item) {
349
- if (!path || !path.length || !item || item.id == null) {
350
- return false;
351
- }
352
- return this.getAreaId(path[path.length - 1]) == item.id;
353
- },
354
- isSearchItemActive(item) {
355
- if (!item || this.value == null || this.value === "") {
356
- return false;
357
- }
358
- const leafId = this.parseValueId(this.value);
359
- if (leafId == null) {
360
- return false;
361
- }
362
- return leafId == this.getAreaId(item);
363
- },
364
- parseValueId(val) {
365
- if (val == null || val === "") {
366
- return null;
367
- }
368
- const str = String(val).trim();
369
- if (this.isLegacyPathValue(str)) {
370
- const parts = str
371
- .split(",")
372
- .map((part) => part.trim())
373
- .filter(Boolean);
374
- return parts.length ? parts[parts.length - 1] : null;
375
- }
376
- return str;
377
- },
378
- parseLegacyPathIds(val) {
379
- if (val == null || val === "") {
380
- return [];
381
- }
382
- return String(val)
383
- .split(",")
384
- .map((part) => part.trim())
385
- .filter((part) => part !== "");
386
- },
387
- isLegacyPathValue(val) {
388
- return (
389
- this.valueFormat === "path" &&
390
- val != null &&
391
- String(val).includes(",") &&
392
- !String(val).startsWith(",")
393
- );
394
- },
395
- parseTreePathIds(treePath) {
396
- if (treePath == null || treePath === "") {
397
- return [];
398
- }
399
- return String(treePath)
400
- .split(",")
401
- .map((part) => part.trim())
402
- .filter((part) => part !== "");
403
- },
404
- formatBoundValue(path, item) {
405
- const last = item || (path && path.length ? path[path.length - 1] : null);
406
- const leafId = this.getAreaId(last);
407
- if (leafId == null) {
408
- return null;
409
- }
410
- if (this.valueFormat === "path") {
411
- const ids = (path || [])
412
- .map((node) => this.getAreaId(node))
413
- .filter((id) => id != null && id !== "");
414
- return ids.length ? ids.join(",") : String(leafId);
415
- }
416
- return leafId;
417
- },
418
- getAreaId(item) {
419
- if (!item) {
420
- return null;
421
- }
422
- if (item.id != null && item.id !== "") {
423
- return item.id;
424
- }
425
- if (item.value != null && item.value !== "") {
426
- return item.value;
427
- }
428
- if (item.areaId != null && item.areaId !== "") {
429
- return item.areaId;
430
- }
431
- return null;
432
- },
433
- isValueSynced(leafId) {
434
- if (leafId == null || leafId === "" || !this.selectedPath.length) {
435
- return false;
436
- }
437
- const currentLeaf = this.getAreaId(
438
- this.selectedPath[this.selectedPath.length - 1]
439
- );
440
- return currentLeaf == leafId;
441
- },
442
- buildPathLabel(path) {
443
- if (!path || !path.length) {
444
- return "";
445
- }
446
- const last = path[path.length - 1];
447
- if (last && last.fullName) {
448
- return last.fullName;
449
- }
450
- return path
451
- .map((item) => item.name || "")
452
- .filter((name) => !!name)
453
- .join("");
454
- },
455
- createEmptyAreaPathResult(value) {
456
- return {
457
- value: value != null && value !== "" ? value : null,
458
- label: null,
459
- path: [],
460
- province: null,
461
- city: null,
462
- district: null,
463
- town: null,
464
- };
465
- },
466
- formatAreaPathResult(value, path, label) {
467
- const result = this.createEmptyAreaPathResult(value);
468
- result.label =
469
- label != null && label !== ""
470
- ? label
471
- : this.buildPathLabel(path) || this.labelValue || null;
472
- result.path = path || [];
473
- (path || []).forEach((node, index) => {
474
- const key = AREA_LEVEL_KEYS[index];
475
- if (key && Object.prototype.hasOwnProperty.call(result, key)) {
476
- result[key] = node;
477
- }
478
- });
479
- return result;
480
- },
481
- async ensurePathDetail(path) {
482
- const result = [];
483
- for (let i = 0; i < path.length; i++) {
484
- const node = path[i];
485
- const id = this.getAreaId(node);
486
- if (id != null) {
487
- const detail = await this.fetchAreaById(id);
488
- result.push(detail || node);
489
- } else {
490
- result.push(node);
491
- }
492
- }
493
- return result;
494
- },
495
- async getAreaPath(value) {
496
- const val = value !== undefined ? value : this.value;
497
- if (val == null || val === "") {
498
- return this.createEmptyAreaPathResult(null);
499
- }
500
- const leafId = this.getLeafIdFromValue(val);
501
- let path = null;
502
- if (
503
- leafId != null &&
504
- this.isValueSynced(leafId) &&
505
- !this.shouldResyncBrowsePath() &&
506
- this.selectedPath.length
507
- ) {
508
- path = this.selectedPath.map((item) => ({ ...item }));
509
- }
510
- if (!path || !path.length) {
511
- if (this.isLegacyPathValue(val)) {
512
- path = await this.resolvePathByIds(this.parseLegacyPathIds(val));
513
- } else if (leafId != null) {
514
- const area = await this.fetchAreaById(leafId);
515
- if (area) {
516
- path = await this.resolveAreaPath(area);
517
- } else {
518
- path = [{ id: leafId, fullName: this.labelValue || undefined }];
519
- }
520
- }
521
- }
522
- if (!path || !path.length) {
523
- return this.createEmptyAreaPathResult(val);
524
- }
525
- path = await this.ensurePathDetail(path);
526
- return this.formatAreaPathResult(val, path);
527
- },
528
- hasDisplayLabel() {
529
- return !!(this.labelValue && String(this.labelValue).trim());
530
- },
531
- getLeafIdFromValue(val) {
532
- if (val == null || val === "") {
533
- return null;
534
- }
535
- if (this.isLegacyPathValue(val)) {
536
- const ids = this.parseLegacyPathIds(val);
537
- return ids.length ? ids[ids.length - 1] : null;
538
- }
539
- return this.parseValueId(val);
540
- },
541
- emitResolvedLabelIfNeeded() {
542
- if (this.popoverVisible || this.labelValue) {
543
- return;
544
- }
545
- const label = this.buildPathLabel(this.selectedPath);
546
- if (label) {
547
- this.$emit("update:labelValue", label);
548
- }
549
- },
550
- cancelPendingSync() {
551
- this.syncToken += 1;
552
- },
553
- getTabLabel(tab) {
554
- if (tab.selected) {
555
- return tab.selected.name || tab.label;
556
- }
557
- return tab.label;
558
- },
559
- isSelected(item) {
560
- const current = this.selectedPath[this.activeLevel - 1];
561
- return current && this.getAreaId(current) === this.getAreaId(item);
562
- },
563
- isTabEnabled(level) {
564
- return level === 1 || this.selectedPath.length >= level - 1;
565
- },
566
- handlePopoverShow() {
567
- this.pendingLeaf = false;
568
- this.saveCommittedBrowseSnapshot();
569
- this.$emit("popover-open");
570
- this.$nextTick(() => {
571
- this.$refs.searchInputRef && this.$refs.searchInputRef.focus();
572
- });
573
- this.initBrowseStateFromValue();
574
- },
575
- saveCommittedBrowseSnapshot() {
576
- this.committedBoundValue = this.value;
577
- this.committedBrowseSnapshot = {
578
- boundValue: this.value,
579
- leafId: this.getLeafIdFromValue(this.value),
580
- selectedPath: this.selectedPath.map((item) => ({ ...item })),
581
- activeLevel: this.activeLevel,
582
- };
583
- },
584
- isSameBoundValue(a, b) {
585
- const normalize = (val) => {
586
- if (val == null || val === "") {
587
- return null;
588
- }
589
- return String(val);
590
- };
591
- return normalize(a) === normalize(b);
592
- },
593
- emitChangeIfCommitted() {
594
- const snapshot = this.committedBrowseSnapshot;
595
- if (!snapshot) {
596
- return;
597
- }
598
- const oldVal = snapshot.boundValue;
599
- const newVal = this.value;
600
- if (!this.isSameBoundValue(oldVal, newVal)) {
601
- const payload = this.lastSelectionPayload || {
602
- value: newVal,
603
- row: null,
604
- label: this.labelValue,
605
- };
606
- this.$emit("change", {
607
- value: payload.value != null ? payload.value : newVal,
608
- row: payload.row || null,
609
- label: payload.label != null ? payload.label : this.labelValue,
610
- path: payload.path || [],
611
- });
612
- }
613
- this.committedBoundValue = null;
614
- this.lastSelectionPayload = null;
615
- },
616
- restoreBrowseStateIfUncommitted() {
617
- const snapshot = this.committedBrowseSnapshot;
618
- if (!snapshot) {
619
- return;
620
- }
621
- if (!this.isSameBoundValue(this.value, snapshot.boundValue)) {
622
- this.levelItems = [];
623
- this.committedBrowseSnapshot = null;
624
- return;
625
- }
626
- const committedLeafId = snapshot.leafId;
627
- if (this.isValueSynced(committedLeafId)) {
628
- this.levelItems = [];
629
- this.committedBrowseSnapshot = null;
630
- return;
631
- }
632
- if (snapshot.selectedPath && snapshot.selectedPath.length) {
633
- this.selectedPath = snapshot.selectedPath.map((item) => ({ ...item }));
634
- this.activeLevel = snapshot.activeLevel;
635
- } else if (committedLeafId) {
636
- this.selectedPath = [
637
- {
638
- id: committedLeafId,
639
- fullName: this.labelValue || undefined,
640
- name: this.labelValue || undefined,
641
- },
642
- ];
643
- this.activeLevel = 1;
644
- } else {
645
- this.selectedPath = [];
646
- this.activeLevel = 1;
647
- }
648
- this.levelItems = [];
649
- this.committedBrowseSnapshot = null;
650
- },
651
- syncFromValue() {
652
- if (this.skipValueSync) {
653
- return;
654
- }
655
- const val = this.value;
656
- if (!val) {
657
- this.resetBrowseState(false);
658
- return;
659
- }
660
- if (!this.hasDisplayLabel()) {
661
- this.resolveLabelFromValue();
662
- }
663
- },
664
- async resolveLabelFromValue() {
665
- const val = this.value;
666
- if (!val || this.hasDisplayLabel()) {
667
- return;
668
- }
669
- const leafId = this.getLeafIdFromValue(val);
670
- if (leafId == null) {
671
- return;
672
- }
673
- const token = ++this.syncToken;
674
- const area = await this.fetchAreaById(leafId);
675
- if (token !== this.syncToken || this.hasDisplayLabel()) {
676
- return;
677
- }
678
- const label = (area && area.fullName) || (area && area.name) || null;
679
- if (label) {
680
- this.$emit("update:labelValue", label);
681
- }
682
- },
683
- shouldResyncBrowsePath() {
684
- if (!this.labelValue || !this.selectedPath.length) {
685
- return false;
686
- }
687
- const pathLabel = this.buildPathLabel(this.selectedPath);
688
- return pathLabel !== String(this.labelValue).trim();
689
- },
690
- syncBrowseStateForPopover() {
691
- const val = this.value;
692
- if (!val) {
693
- if (!this.levelItems.length) {
694
- this.loadLevelItems(this.getParentId());
695
- }
696
- return;
697
- }
698
- const leafId = this.getLeafIdFromValue(val);
699
- if (this.isValueSynced(leafId) && !this.shouldResyncBrowsePath()) {
700
- const expectedActive = this.selectedPath.length || 1;
701
- if (this.activeLevel !== expectedActive) {
702
- this.activeLevel = expectedActive;
703
- this.loadLevelItems(this.getParentId());
704
- return;
705
- }
706
- if (!this.levelItems.length) {
707
- this.loadLevelItems(this.getParentId());
708
- }
709
- return;
710
- }
711
- if (this.isLegacyPathValue(val)) {
712
- this.syncBrowseStateFromPathIds(this.parseLegacyPathIds(val));
713
- return;
714
- }
715
- this.syncBrowseStateFromLeafId(leafId);
716
- },
717
- initBrowseStateFromValue() {
718
- this.syncBrowseStateForPopover();
719
- },
720
- getAreaRequestData(extra = {}) {
721
- return {
722
- enabled: true,
723
- locale: this.locale || null,
724
- ...extra,
725
- };
726
- },
727
- normalizeAreaRows(res) {
728
- const objx = res && res.objx;
729
- if (Array.isArray(objx)) {
730
- return objx;
731
- }
732
- if (objx && Array.isArray(objx.records)) {
733
- return objx.records;
734
- }
735
- return [];
736
- },
737
- requestAreaList(extra = {}) {
738
- return new Promise((resolve) => {
739
- this.$http({
740
- url: USER_PREFIX + "/area/listPage",
741
- method: "post",
742
- data: this.getAreaRequestData(extra),
743
- success: (res) => resolve(this.normalizeAreaRows(res)),
744
- error: () => resolve([]),
745
- });
746
- });
747
- },
748
- fetchChildren(parentId) {
749
- return new Promise((resolve) => {
750
- this.$http({
751
- url: USER_PREFIX + "/area/getChildren",
752
- method: "post",
753
- data: this.getAreaRequestData({ id: parentId }),
754
- success: (res) => resolve(this.normalizeAreaRows(res)),
755
- error: () => resolve([]),
756
- });
757
- });
758
- },
759
- getParentIdFromRow(row) {
760
- if (!row) return null;
761
- if (row.parentId != null && row.parentId !== "") {
762
- return row.parentId;
763
- }
764
- const parent = row.parent;
765
- if (parent == null || parent === undefined || parent === "") {
766
- return null;
767
- }
768
- if (typeof parent === "object") {
769
- return parent.id != null ? parent.id : null;
770
- }
771
- return parent;
772
- },
773
- fetchAreaById(id) {
774
- if (id == null || id === "") {
775
- return Promise.resolve(null);
776
- }
777
- return new Promise((resolve) => {
778
- this.$http({
779
- url: USER_PREFIX + "/area/get",
780
- method: "post",
781
- data: this.getAreaRequestData({ id }),
782
- success: (res) => {
783
- const objx = res && res.objx;
784
- if (objx && typeof objx === "object" && !Array.isArray(objx)) {
785
- resolve(objx);
786
- return;
787
- }
788
- resolve(null);
789
- },
790
- error: () => resolve(null),
791
- });
792
- });
793
- },
794
- isAreaObject(node) {
795
- return node && typeof node === "object" && node.id != null;
796
- },
797
- buildAreaPath(item, depthLimit) {
798
- const limit = depthLimit == null ? this.maxLevel : depthLimit;
799
- if (
800
- item.pathList &&
801
- Array.isArray(item.pathList) &&
802
- item.pathList.length > 1
803
- ) {
804
- if (this.isAreaObject(item.pathList[0])) {
805
- return item.pathList.slice(0, limit);
806
- }
807
- }
808
- if (
809
- item.areaPath &&
810
- Array.isArray(item.areaPath) &&
811
- item.areaPath.length > 1
812
- ) {
813
- if (this.isAreaObject(item.areaPath[0])) {
814
- return item.areaPath.slice(0, limit);
815
- }
816
- }
817
- return null;
818
- },
819
- matchAreaByPrefix(fullName, prefix, children) {
820
- const sorted = [...children].sort(
821
- (a, b) => (b.name || "").length - (a.name || "").length
822
- );
823
- for (let i = 0; i < sorted.length; i++) {
824
- const child = sorted[i];
825
- const name = child.name || "";
826
- if (!name) {
827
- continue;
828
- }
829
- if (fullName.startsWith(prefix + name)) {
830
- return child;
831
- }
832
- }
833
- return null;
834
- },
835
- async resolvePathByFullName(item, depthLimit) {
836
- const fullName = (item.fullName || "").trim();
837
- if (!fullName) {
838
- return null;
839
- }
840
- const path = [];
841
- let parentId = 0;
842
- let prefix = "";
843
- for (let level = 0; level < MAX_AREA_DEPTH; level++) {
844
- const children = await this.fetchChildren(parentId);
845
- if (!children.length) {
846
- break;
847
- }
848
- const matched = this.matchAreaByPrefix(fullName, prefix, children);
849
- if (!matched) {
850
- break;
851
- }
852
- path.push(matched);
853
- prefix += matched.name || "";
854
- if (matched.id == item.id) {
855
- return path;
856
- }
857
- parentId = matched.id;
858
- }
859
- if (path.length && path[path.length - 1].id != item.id) {
860
- const siblings = await this.fetchChildren(path[path.length - 1].id);
861
- const leaf = siblings.find((row) => row.id == item.id);
862
- if (leaf) {
863
- path.push(leaf);
864
- } else if (item.name && fullName.endsWith(item.name)) {
865
- path.push(item);
866
- }
867
- }
868
- return path.length ? path : null;
869
- },
870
- async resolvePathByParentChain(item, depthLimit) {
871
- const chain = [];
872
- let current = item;
873
- let safety = 0;
874
- while (current && safety < MAX_AREA_DEPTH + 2) {
875
- safety += 1;
876
- chain.unshift(current);
877
- const parentId = this.getParentIdFromRow(current);
878
- if (!parentId) {
879
- break;
880
- }
881
- const parent = await this.fetchAreaById(parentId);
882
- if (!parent || parent.id != parentId) {
883
- break;
884
- }
885
- current = parent;
886
- }
887
- return chain.length ? chain : null;
888
- },
889
- async resolvePathFromTreePath(area, depthLimit) {
890
- const ids = this.parseTreePathIds(area && area.treePath);
891
- if (!ids.length) {
892
- return area && area.id != null ? [area] : null;
893
- }
894
- const path = [];
895
- for (let i = 0; i < ids.length; i++) {
896
- const id = ids[i];
897
- let node = null;
898
- if (area && this.getAreaId(area) == id) {
899
- node = area;
900
- } else {
901
- node = await this.fetchAreaById(id);
902
- }
903
- path.push(node || { id });
904
- }
905
- if (area && area.id != null) {
906
- const exists = path.some((node) => this.getAreaId(node) == area.id);
907
- if (!exists) {
908
- path.push(area);
909
- }
910
- }
911
- return path.length ? path : null;
912
- },
913
- async resolveAreaPath(item, options = {}) {
914
- if (!item || item.id == null) {
915
- return item ? [item] : [];
916
- }
917
- const parentPath = await this.resolvePathByParentChain(item);
918
- if (this.isPathResolvedForItem(parentPath, item)) {
919
- return parentPath;
920
- }
921
- const depthLimit =
922
- options.depthLimit != null
923
- ? options.depthLimit
924
- : this.getResolveDepthLimit(item);
925
- if (item.treePath) {
926
- const treePath = await this.resolvePathFromTreePath(item, depthLimit);
927
- if (this.isPathResolvedForItem(treePath, item)) {
928
- return treePath;
929
- }
930
- }
931
- const presetPath = this.buildAreaPath(item, depthLimit);
932
- if (this.isPathResolvedForItem(presetPath, item)) {
933
- return presetPath;
934
- }
935
- const fullNamePath = await this.resolvePathByFullName(item, depthLimit);
936
- if (this.isPathResolvedForItem(fullNamePath, item)) {
937
- return fullNamePath;
938
- }
939
- if (parentPath && parentPath.length) {
940
- return parentPath;
941
- }
942
- if (item.treePath) {
943
- const treePath = await this.resolvePathFromTreePath(item, depthLimit);
944
- if (treePath && treePath.length) {
945
- return treePath;
946
- }
947
- }
948
- if (presetPath && presetPath.length) {
949
- return presetPath;
950
- }
951
- if (fullNamePath && fullNamePath.length) {
952
- return fullNamePath;
953
- }
954
- return [item];
955
- },
956
- async syncBrowseStateFromLeafId(id) {
957
- if (id == null || id === "") {
958
- return;
959
- }
960
- const token = ++this.syncToken;
961
- const area = await this.fetchAreaById(id);
962
- if (token !== this.syncToken) {
963
- return;
964
- }
965
- if (area) {
966
- await this.syncBrowseStateFromArea(area);
967
- return;
968
- }
969
- await this.syncBrowseStateFromArea({
970
- id,
971
- fullName: this.labelValue || undefined,
972
- });
973
- },
974
- async syncBrowseStateFromArea(item) {
975
- const token = ++this.syncToken;
976
- let seed = item || {};
977
- if (
978
- seed.id != null &&
979
- !seed.fullName &&
980
- !seed.name &&
981
- !this.getParentIdFromRow(seed)
982
- ) {
983
- const detail = await this.fetchAreaById(seed.id);
984
- if (detail) {
985
- seed = detail;
986
- }
987
- } else if (seed.id != null && !seed.fullName && this.labelValue) {
988
- seed = { ...seed, fullName: this.labelValue };
989
- }
990
- let path = await this.resolveAreaPath(seed);
991
- if (token !== this.syncToken) {
992
- return;
993
- }
994
- if (!path.length) {
995
- path = [seed];
996
- }
997
- const last = path[path.length - 1];
998
- if (last && last.id != seed.id && seed.id != null) {
999
- const siblings = await this.fetchChildren(
1000
- path.length > 1 ? path[path.length - 2].id : 0
1001
- );
1002
- const exact = siblings.find((row) => row.id == seed.id);
1003
- if (exact) {
1004
- path = path.slice(0, -1).concat(exact);
1005
- } else {
1006
- path = path.concat(seed);
1007
- }
1008
- }
1009
- if (token !== this.syncToken) {
1010
- return;
1011
- }
1012
- this.selectedPath = path;
1013
- this.activeLevel = path.length || 1;
1014
- this.loadLevelItems(this.getParentId());
1015
- this.emitResolvedLabelIfNeeded();
1016
- },
1017
- async resolvePathByIds(ids, depthLimit) {
1018
- const limit = depthLimit == null ? ids.length : depthLimit;
1019
- const path = [];
1020
- let parentId = 0;
1021
- for (let i = 0; i < ids.length && i < limit; i++) {
1022
- const targetId = ids[i];
1023
- const children = await this.fetchChildren(parentId);
1024
- let matched = children.find((row) => this.getAreaId(row) == targetId);
1025
- if (matched) {
1026
- path.push(matched);
1027
- parentId = matched.id;
1028
- continue;
1029
- }
1030
- const fetched = await this.fetchAreaById(targetId);
1031
- if (fetched) {
1032
- path.push(fetched);
1033
- parentId = fetched.id;
1034
- } else {
1035
- path.push({ id: targetId });
1036
- break;
1037
- }
1038
- }
1039
- return path;
1040
- },
1041
- async syncBrowseStateFromPathIds(ids) {
1042
- const token = ++this.syncToken;
1043
- const depthLimit = Math.min(
1044
- Math.max(ids.length, this.maxLevel, 1),
1045
- MAX_AREA_DEPTH
1046
- );
1047
- const path = await this.resolvePathByIds(ids, depthLimit);
1048
- if (token !== this.syncToken) {
1049
- return;
1050
- }
1051
- if (!path.length) {
1052
- if (!this.levelItems.length) {
1053
- this.loadLevelItems(0);
1054
- }
1055
- return;
1056
- }
1057
- this.selectedPath = path;
1058
- this.activeLevel = path.length;
1059
- this.loadLevelItems(this.getParentId());
1060
- this.emitResolvedLabelIfNeeded();
1061
- },
1062
- handlePopoverHide() {
1063
- this.searchKeyword = "";
1064
- this.searchList = [];
1065
- this.searchDropdownVisible = false;
1066
- this.pendingLeaf = false;
1067
- this.cancelPendingSync();
1068
- this.emitChangeIfCommitted();
1069
- this.restoreBrowseStateIfUncommitted();
1070
- },
1071
- handleSearchInput() {
1072
- const keyword = (this.searchKeyword || "").trim();
1073
- clearTimeout(this.searchTimer);
1074
- this.searchDropdownVisible = !!keyword;
1075
- if (!keyword) {
1076
- this.handleSearchClear();
1077
- return;
1078
- }
1079
- this.searchTimer = setTimeout(() => {
1080
- this.doSearch(keyword);
1081
- }, SEARCH_DEBOUNCE);
1082
- },
1083
- handleSearchClear() {
1084
- this.searchList = [];
1085
- this.searchDropdownVisible = false;
1086
- },
1087
- doSearch(keyword) {
1088
- this.searchLoading = true;
1089
- this.requestAreaList({ fullName: keyword, size: 20 }).then((rows) => {
1090
- this.searchList = rows;
1091
- this.searchLoading = false;
1092
- });
1093
- },
1094
- getParentId() {
1095
- if (this.activeLevel <= 1) {
1096
- return 0;
1097
- }
1098
- const parent = this.selectedPath[this.activeLevel - 2];
1099
- return parent ? this.getAreaId(parent) : 0;
1100
- },
1101
- loadLevelItems(parentId) {
1102
- this.browseLoading = true;
1103
- this.fetchChildren(parentId).then((rows) => {
1104
- this.levelItems = rows;
1105
- this.browseLoading = false;
1106
- });
1107
- },
1108
- switchLevel(level) {
1109
- if (level > this.browseLevelCount || !this.isTabEnabled(level)) {
1110
- return;
1111
- }
1112
- this.cancelPendingSync();
1113
- this.activeLevel = level;
1114
- this.loadLevelItems(this.getParentId());
1115
- },
1116
- prevLevel() {
1117
- if (!this.canPrevLevel) {
1118
- return;
1119
- }
1120
- this.switchLevel(this.activeLevel - 1);
1121
- },
1122
- confirmSelection() {
1123
- if (!this.canConfirm) {
1124
- return;
1125
- }
1126
- const last = this.selectedPath[this.selectedPath.length - 1];
1127
- this.applySelection(last).then(() => {
1128
- this.popoverVisible = false;
1129
- });
1130
- },
1131
- cancelSelection() {
1132
- /* 关闭弹层,未确认的浏览状态由 hide 回调恢复 */
1133
- this.popoverVisible = false;
1134
- },
1135
- nextLevel() {
1136
- if (!this.canNextLevel) {
1137
- return;
1138
- }
1139
- this.switchLevel(this.activeLevel + 1);
1140
- },
1141
- selectSearchItem(item) {
1142
- this.cancelPendingSync();
1143
- this.syncBrowseStateFromArea(item).then(() => {
1144
- this.searchKeyword = "";
1145
- this.searchList = [];
1146
- this.searchDropdownVisible = false;
1147
- this.pendingLeaf = false;
1148
- if (this.selectedPath.length >= this.effectiveMinLevel) {
1149
- /* 已达到最小选择层级:等待确认 */
1150
- return;
1151
- }
1152
- /* 搜索结果未达到最小选择层级:有下级只定位下钻,无下级视为末级 */
1153
- const last = this.selectedPath[this.selectedPath.length - 1] || item;
1154
- this.fetchChildren(this.getAreaId(last)).then((children) => {
1155
- if (!children.length) {
1156
- this.pendingLeaf = true;
1157
- return;
1158
- }
1159
- this.activeLevel = this.selectedPath.length + 1;
1160
- this.levelItems = children;
1161
- });
1162
- });
1163
- },
1164
- selectItem(item) {
1165
- if (!this.isTabEnabled(this.activeLevel)) {
1166
- return;
1167
- }
1168
- this.cancelPendingSync();
1169
- const level = this.activeLevel;
1170
- this.selectedPath = this.selectedPath.slice(0, level - 1);
1171
- this.selectedPath.push(item);
1172
- this.pendingLeaf = false;
1173
- if (level >= this.maxLevel) {
1174
- /* 已到最大层级:等待确认 */
1175
- return;
1176
- }
1177
- this.fetchChildren(this.getAreaId(item)).then((children) => {
1178
- if (!children.length) {
1179
- /* 无下级数据:视为末级,等待确认 */
1180
- this.pendingLeaf = true;
1181
- return;
1182
- }
1183
- this.activeLevel = level + 1;
1184
- this.levelItems = children;
1185
- });
1186
- },
1187
- async clampPathToMaxLevel(path) {
1188
- if (!path || !path.length || path.length <= this.maxLevel) {
1189
- return path || [];
1190
- }
1191
- const trimmed = path.slice(0, this.maxLevel);
1192
- const last = trimmed[trimmed.length - 1];
1193
- const id = this.getAreaId(last);
1194
- if (id != null && (!last.name || !last.fullName)) {
1195
- const detail = await this.fetchAreaById(id);
1196
- if (detail) {
1197
- trimmed[trimmed.length - 1] = detail;
1198
- }
1199
- }
1200
- return trimmed;
1201
- },
1202
- async applySelection(item) {
1203
- let path = this.selectedPath.length
1204
- ? this.selectedPath.map((node) => ({ ...node }))
1205
- : [item];
1206
- path = await this.clampPathToMaxLevel(path);
1207
- this.selectedPath = path.map((node) => ({ ...node }));
1208
- this.activeLevel = path.length || 1;
1209
- let last = path[path.length - 1] || item;
1210
- const leafId = this.getAreaId(last);
1211
- if (leafId != null && !last.fullName) {
1212
- const detail = await this.fetchAreaById(leafId);
1213
- if (detail) {
1214
- last = detail;
1215
- if (path.length) {
1216
- path = path.slice();
1217
- path[path.length - 1] = detail;
1218
- this.$set(this.selectedPath, this.selectedPath.length - 1, detail);
1219
- }
1220
- }
1221
- }
1222
- const label = last.fullName || this.buildPathLabel(path);
1223
- const formattedValue = this.formatBoundValue(path, last);
1224
- this.lastSelectionPayload = {
1225
- value: formattedValue,
1226
- row: last,
1227
- label,
1228
- path: path.map((node) => ({ ...node })),
1229
- };
1230
- this.skipValueSync = true;
1231
- this.$emit("input", formattedValue);
1232
- this.$emit("update:labelValue", label);
1233
- this.$nextTick(() => {
1234
- this.skipValueSync = false;
1235
- });
1236
- },
1237
- handleClear() {
1238
- const oldVal = this.value;
1239
- const wasOpen = this.popoverVisible;
1240
- this.lastSelectionPayload = { value: null, row: null, label: null };
1241
- this.skipValueSync = true;
1242
- this.$emit("input", null);
1243
- this.$emit("update:labelValue", null);
1244
- this.resetBrowseState(false);
1245
- this.popoverVisible = false;
1246
- this.$nextTick(() => {
1247
- this.skipValueSync = false;
1248
- if (!wasOpen && !this.isSameBoundValue(oldVal, null)) {
1249
- this.$emit("change", this.lastSelectionPayload);
1250
- this.committedBoundValue = null;
1251
- this.lastSelectionPayload = null;
1252
- }
1253
- });
1254
- },
1255
- resetBrowseState(emitEvent) {
1256
- this.searchKeyword = "";
1257
- this.searchList = [];
1258
- this.selectedPath = [];
1259
- this.activeLevel = 1;
1260
- this.levelItems = [];
1261
- this.committedBrowseSnapshot = null;
1262
- if (emitEvent) {
1263
- this.$emit("input", null);
1264
- this.$emit("update:labelValue", null);
1265
- }
1266
- },
1267
- },
1268
- };
1269
- </script>
1270
-
1271
- <style lang="scss" scoped>
1272
- @import "~@/styles/variables.scss";
1273
-
1274
- .base-area {
1275
- width: 100%;
1276
- display: block;
1277
- box-sizing: border-box;
1278
- }
1279
-
1280
- .base-area-trigger {
1281
- display: block;
1282
- width: 100%;
1283
- box-sizing: border-box;
1284
- }
1285
-
1286
- .base-area-input {
1287
- width: 100%;
1288
- cursor: pointer;
1289
-
1290
- .base-area-clear {
1291
- cursor: pointer;
1292
- display: none;
1293
- }
1294
-
1295
- &:hover .base-area-clear,
1296
- &.is-focus .base-area-clear {
1297
- display: inline-block;
1298
- }
1299
- }
1300
-
1301
- .base-area-panel {
1302
- padding: 4px 0 0;
1303
- overflow: visible;
1304
- }
1305
-
1306
- .base-area-search {
1307
- position: relative;
1308
- margin-bottom: 8px;
1309
- z-index: 2;
1310
- }
1311
-
1312
- .base-area-search-dropdown {
1313
- position: absolute;
1314
- top: calc(100% + 4px);
1315
- left: 0;
1316
- right: 0;
1317
- max-height: 200px;
1318
- overflow-y: auto;
1319
- background: #fff;
1320
- border: 1px solid #e4e7ed;
1321
- border-radius: 4px;
1322
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
1323
- z-index: 20;
1324
- }
1325
-
1326
- .base-area-dropdown-empty {
1327
- padding: 12px 10px;
1328
- text-align: center;
1329
- color: #909399;
1330
- font-size: 12px;
1331
- }
1332
-
1333
- .base-area-fade-enter-active,
1334
- .base-area-fade-leave-active {
1335
- transition: opacity 0.15s ease;
1336
- }
1337
-
1338
- .base-area-fade-enter,
1339
- .base-area-fade-leave-to {
1340
- opacity: 0;
1341
- }
1342
-
1343
- .base-area-result-item {
1344
- padding: 8px 10px;
1345
- font-size: 12px;
1346
- line-height: 1.4;
1347
- color: #606266;
1348
- cursor: pointer;
1349
- border-radius: 4px;
1350
-
1351
- &:hover {
1352
- background: rgba($baseColor, 0.08);
1353
- color: $baseColor;
1354
- }
1355
-
1356
- &.on {
1357
- background: rgba($baseColor, 0.12);
1358
- color: $baseColor;
1359
- }
1360
- }
1361
-
1362
- .base-area-path {
1363
- padding: 6px 10px;
1364
- margin-bottom: 10px;
1365
- background: #f5f7fa;
1366
- border: 1px solid #ebeef5;
1367
- border-radius: 4px;
1368
- }
1369
-
1370
- .base-area-tabs {
1371
- display: flex;
1372
- flex-wrap: wrap;
1373
- align-items: center;
1374
- width: 100%;
1375
- gap: 0;
1376
- }
1377
-
1378
- .base-area-path-sep {
1379
- flex-shrink: 0;
1380
- padding: 0 2px;
1381
- font-size: 12px;
1382
- color: #c0c4cc;
1383
- line-height: 24px;
1384
- user-select: none;
1385
- }
1386
-
1387
- .base-area-tab {
1388
- flex-shrink: 1;
1389
- min-width: 0;
1390
- max-width: 100%;
1391
- padding: 0 4px;
1392
- height: 24px;
1393
- line-height: 24px;
1394
- border: none;
1395
- border-bottom: 2px solid transparent;
1396
- border-radius: 0;
1397
- background: transparent;
1398
- font-size: 12px;
1399
- color: #909399;
1400
- cursor: pointer;
1401
- overflow: hidden;
1402
- text-overflow: ellipsis;
1403
- white-space: nowrap;
1404
- transition: color 0.15s, border-color 0.15s;
1405
-
1406
- &.active {
1407
- color: $baseColor;
1408
- border-bottom-color: $baseColor;
1409
- font-weight: 500;
1410
- background: transparent;
1411
- }
1412
-
1413
- &.selected:not(.active) {
1414
- color: #303133;
1415
- }
1416
-
1417
- &.disabled {
1418
- color: #c0c4cc;
1419
- cursor: not-allowed;
1420
- background: transparent;
1421
- pointer-events: none;
1422
- }
1423
-
1424
- &:hover:not(.disabled):not(.active) {
1425
- color: $baseColor;
1426
- }
1427
- }
1428
-
1429
- .base-area-grid-wrap {
1430
- padding: 8px;
1431
- background: #fff;
1432
- border: 1px solid #eee;
1433
- border-radius: 8px;
1434
- }
1435
-
1436
- .base-area-grid {
1437
- display: flex;
1438
- flex-wrap: wrap;
1439
- min-height: 120px;
1440
- max-height: 294px;
1441
- overflow-y: auto;
1442
- margin: 0 -4px;
1443
- }
1444
-
1445
- .base-area-item {
1446
- width: calc(25% - 8px);
1447
- margin: 0 4px 8px;
1448
- padding: 0 10px;
1449
- height: 34px;
1450
- line-height: 34px;
1451
- border: 1px solid #eee;
1452
- border-radius: 8px;
1453
- font-size: 12px;
1454
- cursor: pointer;
1455
- overflow: hidden;
1456
- text-overflow: ellipsis;
1457
- white-space: nowrap;
1458
-
1459
- &:hover {
1460
- color: $baseColor;
1461
- border-color: $baseColor;
1462
- }
1463
-
1464
- &.on {
1465
- background: $baseColor;
1466
- border-color: $baseColor;
1467
- color: #fff;
1468
- }
1469
- }
1470
-
1471
- .base-area-empty {
1472
- width: 100%;
1473
- padding: 24px 0;
1474
- text-align: center;
1475
- color: #909399;
1476
- font-size: 12px;
1477
- }
1478
-
1479
- .base-area-level-nav {
1480
- display: flex;
1481
- align-items: center;
1482
- justify-content: space-between;
1483
- margin-top: 8px;
1484
-
1485
- .base-area-nav-btn {
1486
- min-width: 32px;
1487
- padding: 7px 10px;
1488
- border-radius: 8px;
1489
- color: #fff;
1490
- border-color: $baseColor;
1491
- background-color: $baseColor;
1492
-
1493
- i {
1494
- font-weight: bold;
1495
- }
1496
-
1497
- &.el-button:focus,
1498
- &.el-button:active {
1499
- color: #fff;
1500
- border-color: $baseColor;
1501
- background-color: $baseColor;
1502
- }
1503
-
1504
- &.el-button:hover:not(.is-disabled) {
1505
- color: #fff;
1506
- border-color: $baseColor;
1507
- background-color: $baseColor;
1508
- opacity: 0.92;
1509
- }
1510
-
1511
- &.is-disabled,
1512
- &.el-button.is-disabled {
1513
- color: #c0c4cc;
1514
- border-color: #eee;
1515
- background-color: #f5f7fa;
1516
- opacity: 1;
1517
- }
1518
- }
1519
-
1520
- .base-area-level-label {
1521
- flex: 1;
1522
- text-align: center;
1523
- font-size: 12px;
1524
- color: #606266;
1525
- line-height: 28px;
1526
- user-select: none;
1527
- }
1528
- }
1529
-
1530
- .base-area-footer {
1531
- display: flex;
1532
- align-items: center;
1533
- justify-content: flex-end;
1534
- margin-top: 8px;
1535
- padding-top: 8px;
1536
- border-top: 1px solid #ebeef5;
1537
- }
1538
- </style>
1539
-
1540
- <style lang="scss">
1541
- @import "~@/styles/variables.scss";
1542
-
1543
- /* el-popover 根节点为 inline span,需强制块级铺满 */
1544
- .base-area {
1545
- width: 100% !important;
1546
- display: block !important;
1547
- box-sizing: border-box;
1548
-
1549
- > span {
1550
- display: block !important;
1551
- width: 100% !important;
1552
- box-sizing: border-box;
1553
- }
1554
-
1555
- .el-popover__reference-wrapper,
1556
- .el-popover__reference,
1557
- .base-area-trigger {
1558
- display: block !important;
1559
- width: 100% !important;
1560
- box-sizing: border-box;
1561
- }
1562
-
1563
- .base-area-input.el-input {
1564
- width: 100% !important;
1565
- display: block !important;
1566
- }
1567
-
1568
- .base-area-input .base-area-clear {
1569
- cursor: pointer;
1570
- display: none;
1571
- }
1572
-
1573
- .base-area-input:hover .base-area-clear,
1574
- .base-area-input.is-focus .base-area-clear {
1575
- display: inline-block;
1576
- }
1577
- }
1578
-
1579
- .base-area-popover {
1580
- padding: 10px 12px;
1581
- overflow: visible;
1582
- box-sizing: border-box;
1583
-
1584
- .base-area-panel {
1585
- width: 100%;
1586
- box-sizing: border-box;
1587
- }
1588
-
1589
- .base-area-search {
1590
- width: 100%;
1591
- box-sizing: border-box;
1592
- }
1593
-
1594
- .base-area-search-input,
1595
- .base-area-search .el-input {
1596
- width: 100% !important;
1597
- display: block;
1598
- box-sizing: border-box;
1599
- }
1600
-
1601
- .base-area-nav-btn.el-button {
1602
- border-radius: 8px;
1603
- color: #fff;
1604
- border-color: $baseColor;
1605
- background-color: $baseColor;
1606
- }
1607
-
1608
- .base-area-nav-btn.el-button:focus,
1609
- .base-area-nav-btn.el-button:active {
1610
- color: #fff;
1611
- border-color: $baseColor;
1612
- background-color: $baseColor;
1613
- }
1614
-
1615
- .base-area-nav-btn.el-button.is-disabled {
1616
- color: #c0c4cc;
1617
- border-color: #eee;
1618
- background-color: #f5f7fa;
1619
- }
1620
-
1621
- .base-area-tab.disabled {
1622
- color: #c0c4cc;
1623
- cursor: not-allowed;
1624
- background: transparent;
1625
- pointer-events: none;
1626
- }
1627
- }
1628
- </style>
1
+ <template>
2
+ <div class="base-area">
3
+ <el-popover
4
+ v-model="popoverVisible"
5
+ placement="bottom-start"
6
+ trigger="click"
7
+ popper-class="base-area-popover"
8
+ :width="popoverWidth"
9
+ :disabled="disabled || readonly"
10
+ @show="handlePopoverShow"
11
+ @hide="handlePopoverHide"
12
+ >
13
+ <div class="base-area-panel">
14
+ <div class="base-area-search">
15
+ <el-input
16
+ ref="searchInputRef"
17
+ v-model="searchKeyword"
18
+ class="base-area-search-input"
19
+ size="small"
20
+ clearable
21
+ :placeholder="searchPlaceholder"
22
+ @input="handleSearchInput"
23
+ @clear="handleSearchClear"
24
+ @focus="searchDropdownVisible = true"
25
+ />
26
+ <transition name="base-area-fade">
27
+ <div
28
+ v-show="showSearchDropdown"
29
+ class="base-area-search-dropdown"
30
+ v-loading="searchLoading"
31
+ >
32
+ <div
33
+ v-for="item in searchList"
34
+ :key="'search-' + item.id"
35
+ class="base-area-result-item"
36
+ :class="{ on: isSearchItemActive(item) }"
37
+ @mousedown.prevent
38
+ @click="selectSearchItem(item)"
39
+ >
40
+ {{ item.fullName || item.name }}
41
+ </div>
42
+ <div
43
+ v-if="!searchLoading && !searchList.length"
44
+ class="base-area-dropdown-empty"
45
+ >
46
+ {{ emptyText }}
47
+ </div>
48
+ </div>
49
+ </transition>
50
+ </div>
51
+ <div class="base-area-path">
52
+ <div class="base-area-tabs">
53
+ <template v-for="(tab, index) in tabs">
54
+ <span
55
+ v-if="index > 0"
56
+ :key="'sep-' + index"
57
+ class="base-area-path-sep"
58
+ >/</span
59
+ >
60
+ <span
61
+ :key="'tab-' + index"
62
+ class="base-area-tab"
63
+ :class="{
64
+ active: activeLevel === index + 1,
65
+ selected: !!tab.selected,
66
+ disabled: !isTabEnabled(index + 1),
67
+ }"
68
+ @click="switchLevel(index + 1)"
69
+ >{{ getTabLabel(tab) }}</span
70
+ >
71
+ </template>
72
+ </div>
73
+ </div>
74
+ <div class="base-area-grid-wrap">
75
+ <div class="base-area-grid" v-loading="browseLoading">
76
+ <div
77
+ v-for="item in levelItems"
78
+ :key="item.id"
79
+ class="base-area-item"
80
+ :class="{ on: isSelected(item) }"
81
+ @click="selectItem(item)"
82
+ >
83
+ <span>{{ item.name }}</span>
84
+ </div>
85
+ <div
86
+ v-if="!browseLoading && !levelItems.length"
87
+ class="base-area-empty"
88
+ >
89
+ {{ emptyText }}
90
+ </div>
91
+ </div>
92
+ </div>
93
+ <div class="base-area-level-nav">
94
+ <el-button
95
+ size="mini"
96
+ class="base-area-nav-btn"
97
+ :disabled="!canPrevLevel"
98
+ @mousedown.prevent
99
+ @click="prevLevel"
100
+ >
101
+ <i class="el-icon-arrow-left"></i>
102
+ </el-button>
103
+ <span class="base-area-level-label">{{ currentLevelLabel }}</span>
104
+ <el-button
105
+ size="mini"
106
+ class="base-area-nav-btn"
107
+ :disabled="!canNextLevel"
108
+ @mousedown.prevent
109
+ @click="nextLevel"
110
+ >
111
+ <i class="el-icon-arrow-right"></i>
112
+ </el-button>
113
+ </div>
114
+ <div class="base-area-footer">
115
+ <el-button
116
+ type="primary"
117
+ plain
118
+ class="button-sty"
119
+ @mousedown.prevent
120
+ @click="cancelSelection"
121
+ >
122
+ <i class="el-icon-close el-icon"></i>
123
+ {{ cancelText }}
124
+ </el-button>
125
+ <el-button
126
+ type="primary"
127
+ class="button-sty"
128
+ :disabled="!canConfirm"
129
+ @mousedown.prevent
130
+ @click="confirmSelection"
131
+ >
132
+ <i class="el-icon-check el-icon"></i>
133
+ {{ confirmText }}
134
+ </el-button>
135
+ </div>
136
+ </div>
137
+ <div slot="reference" class="base-area-trigger">
138
+ <el-input
139
+ ref="inputRef"
140
+ class="base-area-input"
141
+ :class="{ 'is-focus': referenceFocused }"
142
+ :value="displayText"
143
+ :size="size"
144
+ :disabled="disabled"
145
+ :placeholder="placeholder"
146
+ readonly
147
+ @focus="referenceFocused = true"
148
+ @blur="referenceFocused = false"
149
+ >
150
+ <i
151
+ v-if="showClearIcon"
152
+ slot="suffix"
153
+ class="el-input__icon el-icon-circle-close base-area-clear"
154
+ @mousedown.prevent
155
+ @click.stop="handleClear"
156
+ />
157
+ </el-input>
158
+ </div>
159
+ </el-popover>
160
+ </div>
161
+ </template>
162
+
163
+ <script>
164
+ const LEVEL_LABELS = ["省份", "城市", "区县", "乡镇"];
165
+ const AREA_LEVEL_KEYS = ["province", "city", "district", "town"];
166
+ const MAX_AREA_DEPTH = 4;
167
+ const SEARCH_DEBOUNCE = 300;
168
+
169
+ export default {
170
+ name: "baseArea",
171
+ props: {
172
+ value: {
173
+ type: [String, Number],
174
+ default: null,
175
+ },
176
+ labelValue: {
177
+ type: String,
178
+ default: null,
179
+ },
180
+ maxLevel: {
181
+ type: Number,
182
+ default: 4,
183
+ },
184
+ /** 至少选到该层级才提交值(1-4);分支无下级数据时视为末级 */
185
+ minLevel: {
186
+ type: Number,
187
+ default: 1,
188
+ },
189
+ locale: {
190
+ type: String,
191
+ default: null,
192
+ },
193
+ disabled: {
194
+ type: Boolean,
195
+ default: false,
196
+ },
197
+ readonly: {
198
+ type: Boolean,
199
+ default: false,
200
+ },
201
+ clearable: {
202
+ type: Boolean,
203
+ default: true,
204
+ },
205
+ placeholder: {
206
+ type: String,
207
+ default: "请选择地区",
208
+ },
209
+ searchPlaceholder: {
210
+ type: String,
211
+ default: "请输入地区名称",
212
+ },
213
+ emptyText: {
214
+ type: String,
215
+ default: "暂无匹配地区",
216
+ },
217
+ confirmText: {
218
+ type: String,
219
+ default: "确 定",
220
+ },
221
+ cancelText: {
222
+ type: String,
223
+ default: "取 消",
224
+ },
225
+ size: {
226
+ type: String,
227
+ default: "small",
228
+ },
229
+ popoverWidth: {
230
+ type: Number,
231
+ default: 630,
232
+ },
233
+ /** leaf: 绑定末级 id;path: 兼容旧版逗号分隔路径 */
234
+ valueFormat: {
235
+ type: String,
236
+ default: "leaf",
237
+ },
238
+ },
239
+ data() {
240
+ return {
241
+ popoverVisible: false,
242
+ searchKeyword: "",
243
+ searchList: [],
244
+ selectedPath: [],
245
+ activeLevel: 1,
246
+ levelItems: [],
247
+ browseLoading: false,
248
+ searchLoading: false,
249
+ searchDropdownVisible: false,
250
+ referenceFocused: false,
251
+ searchTimer: null,
252
+ syncToken: 0,
253
+ skipValueSync: false,
254
+ committedBrowseSnapshot: null,
255
+ committedBoundValue: null,
256
+ lastSelectionPayload: null,
257
+ pendingLeaf: false,
258
+ };
259
+ },
260
+ computed: {
261
+ hasSearchKeyword() {
262
+ return !!(this.searchKeyword || "").trim();
263
+ },
264
+ showSearchDropdown() {
265
+ return this.searchDropdownVisible && this.hasSearchKeyword;
266
+ },
267
+ displayText() {
268
+ if (this.labelValue) {
269
+ return this.labelValue;
270
+ }
271
+ const pathLabel = this.selectedPath.length
272
+ ? this.buildPathLabel(this.selectedPath)
273
+ : "";
274
+ if (this.popoverVisible && pathLabel) {
275
+ return pathLabel;
276
+ }
277
+ return pathLabel || "";
278
+ },
279
+ tabs() {
280
+ const tabs = [];
281
+ for (let i = 0; i < this.browseLevelCount; i++) {
282
+ tabs.push({
283
+ level: i + 1,
284
+ label: LEVEL_LABELS[i] || `第${i + 1}级`,
285
+ selected: this.selectedPath[i] || null,
286
+ });
287
+ }
288
+ return tabs;
289
+ },
290
+ showClearIcon() {
291
+ return (
292
+ this.clearable && !this.readonly && !this.disabled && !!this.displayText
293
+ );
294
+ },
295
+ canPrevLevel() {
296
+ return this.activeLevel > 1;
297
+ },
298
+ canNextLevel() {
299
+ return (
300
+ this.activeLevel < this.browseLevelCount &&
301
+ this.isTabEnabled(this.activeLevel + 1)
302
+ );
303
+ },
304
+ browseLevelCount() {
305
+ return Math.max(this.maxLevel, this.selectedPath.length, 1);
306
+ },
307
+ effectiveMinLevel() {
308
+ const min = this.minLevel == null ? 1 : this.minLevel;
309
+ return Math.max(1, Math.min(min, this.maxLevel));
310
+ },
311
+ canConfirm() {
312
+ if (!this.selectedPath.length) {
313
+ return false;
314
+ }
315
+ /* 达到最小选择层级,或该地区无下级数据(视为末级) */
316
+ return (
317
+ this.selectedPath.length >= this.effectiveMinLevel || this.pendingLeaf
318
+ );
319
+ },
320
+ currentLevelLabel() {
321
+ return LEVEL_LABELS[this.activeLevel - 1] || `第${this.activeLevel}级`;
322
+ },
323
+ },
324
+ watch: {
325
+ value: {
326
+ immediate: true,
327
+ handler() {
328
+ this.syncFromValue();
329
+ },
330
+ },
331
+ labelValue(val) {
332
+ if (!val && !this.value) {
333
+ this.resetBrowseState(false);
334
+ }
335
+ },
336
+ },
337
+ beforeDestroy() {
338
+ clearTimeout(this.searchTimer);
339
+ },
340
+ methods: {
341
+ getResolveDepthLimit(item) {
342
+ if (item && item.treePath) {
343
+ const ids = this.parseTreePathIds(item.treePath);
344
+ return Math.min(Math.max(ids.length, this.maxLevel, 1), MAX_AREA_DEPTH);
345
+ }
346
+ return MAX_AREA_DEPTH;
347
+ },
348
+ isPathResolvedForItem(path, item) {
349
+ if (!path || !path.length || !item || item.id == null) {
350
+ return false;
351
+ }
352
+ return this.getAreaId(path[path.length - 1]) == item.id;
353
+ },
354
+ isSearchItemActive(item) {
355
+ if (!item || this.value == null || this.value === "") {
356
+ return false;
357
+ }
358
+ const leafId = this.parseValueId(this.value);
359
+ if (leafId == null) {
360
+ return false;
361
+ }
362
+ return leafId == this.getAreaId(item);
363
+ },
364
+ parseValueId(val) {
365
+ if (val == null || val === "") {
366
+ return null;
367
+ }
368
+ const str = String(val).trim();
369
+ if (this.isLegacyPathValue(str)) {
370
+ const parts = str
371
+ .split(",")
372
+ .map((part) => part.trim())
373
+ .filter(Boolean);
374
+ return parts.length ? parts[parts.length - 1] : null;
375
+ }
376
+ return str;
377
+ },
378
+ parseLegacyPathIds(val) {
379
+ if (val == null || val === "") {
380
+ return [];
381
+ }
382
+ return String(val)
383
+ .split(",")
384
+ .map((part) => part.trim())
385
+ .filter((part) => part !== "");
386
+ },
387
+ isLegacyPathValue(val) {
388
+ return (
389
+ this.valueFormat === "path" &&
390
+ val != null &&
391
+ String(val).includes(",") &&
392
+ !String(val).startsWith(",")
393
+ );
394
+ },
395
+ parseTreePathIds(treePath) {
396
+ if (treePath == null || treePath === "") {
397
+ return [];
398
+ }
399
+ return String(treePath)
400
+ .split(",")
401
+ .map((part) => part.trim())
402
+ .filter((part) => part !== "");
403
+ },
404
+ formatBoundValue(path, item) {
405
+ const last = item || (path && path.length ? path[path.length - 1] : null);
406
+ const leafId = this.getAreaId(last);
407
+ if (leafId == null) {
408
+ return null;
409
+ }
410
+ if (this.valueFormat === "path") {
411
+ const ids = (path || [])
412
+ .map((node) => this.getAreaId(node))
413
+ .filter((id) => id != null && id !== "");
414
+ return ids.length ? ids.join(",") : String(leafId);
415
+ }
416
+ return leafId;
417
+ },
418
+ getAreaId(item) {
419
+ if (!item) {
420
+ return null;
421
+ }
422
+ if (item.id != null && item.id !== "") {
423
+ return item.id;
424
+ }
425
+ if (item.value != null && item.value !== "") {
426
+ return item.value;
427
+ }
428
+ if (item.areaId != null && item.areaId !== "") {
429
+ return item.areaId;
430
+ }
431
+ return null;
432
+ },
433
+ isValueSynced(leafId) {
434
+ if (leafId == null || leafId === "" || !this.selectedPath.length) {
435
+ return false;
436
+ }
437
+ const currentLeaf = this.getAreaId(
438
+ this.selectedPath[this.selectedPath.length - 1]
439
+ );
440
+ return currentLeaf == leafId;
441
+ },
442
+ buildPathLabel(path) {
443
+ if (!path || !path.length) {
444
+ return "";
445
+ }
446
+ const last = path[path.length - 1];
447
+ if (last && last.fullName) {
448
+ return last.fullName;
449
+ }
450
+ return path
451
+ .map((item) => item.name || "")
452
+ .filter((name) => !!name)
453
+ .join("");
454
+ },
455
+ createEmptyAreaPathResult(value) {
456
+ return {
457
+ value: value != null && value !== "" ? value : null,
458
+ label: null,
459
+ path: [],
460
+ province: null,
461
+ city: null,
462
+ district: null,
463
+ town: null,
464
+ };
465
+ },
466
+ formatAreaPathResult(value, path, label) {
467
+ const result = this.createEmptyAreaPathResult(value);
468
+ result.label =
469
+ label != null && label !== ""
470
+ ? label
471
+ : this.buildPathLabel(path) || this.labelValue || null;
472
+ result.path = path || [];
473
+ (path || []).forEach((node, index) => {
474
+ const key = AREA_LEVEL_KEYS[index];
475
+ if (key && Object.prototype.hasOwnProperty.call(result, key)) {
476
+ result[key] = node;
477
+ }
478
+ });
479
+ return result;
480
+ },
481
+ async ensurePathDetail(path) {
482
+ const result = [];
483
+ for (let i = 0; i < path.length; i++) {
484
+ const node = path[i];
485
+ const id = this.getAreaId(node);
486
+ if (id != null) {
487
+ const detail = await this.fetchAreaById(id);
488
+ result.push(detail || node);
489
+ } else {
490
+ result.push(node);
491
+ }
492
+ }
493
+ return result;
494
+ },
495
+ async getAreaPath(value) {
496
+ const val = value !== undefined ? value : this.value;
497
+ if (val == null || val === "") {
498
+ return this.createEmptyAreaPathResult(null);
499
+ }
500
+ const leafId = this.getLeafIdFromValue(val);
501
+ let path = null;
502
+ if (
503
+ leafId != null &&
504
+ this.isValueSynced(leafId) &&
505
+ !this.shouldResyncBrowsePath() &&
506
+ this.selectedPath.length
507
+ ) {
508
+ path = this.selectedPath.map((item) => ({ ...item }));
509
+ }
510
+ if (!path || !path.length) {
511
+ if (this.isLegacyPathValue(val)) {
512
+ path = await this.resolvePathByIds(this.parseLegacyPathIds(val));
513
+ } else if (leafId != null) {
514
+ const area = await this.fetchAreaById(leafId);
515
+ if (area) {
516
+ path = await this.resolveAreaPath(area);
517
+ } else {
518
+ path = [{ id: leafId, fullName: this.labelValue || undefined }];
519
+ }
520
+ }
521
+ }
522
+ if (!path || !path.length) {
523
+ return this.createEmptyAreaPathResult(val);
524
+ }
525
+ path = await this.ensurePathDetail(path);
526
+ return this.formatAreaPathResult(val, path);
527
+ },
528
+ hasDisplayLabel() {
529
+ return !!(this.labelValue && String(this.labelValue).trim());
530
+ },
531
+ getLeafIdFromValue(val) {
532
+ if (val == null || val === "") {
533
+ return null;
534
+ }
535
+ if (this.isLegacyPathValue(val)) {
536
+ const ids = this.parseLegacyPathIds(val);
537
+ return ids.length ? ids[ids.length - 1] : null;
538
+ }
539
+ return this.parseValueId(val);
540
+ },
541
+ emitResolvedLabelIfNeeded() {
542
+ if (this.popoverVisible || this.labelValue) {
543
+ return;
544
+ }
545
+ const label = this.buildPathLabel(this.selectedPath);
546
+ if (label) {
547
+ this.$emit("update:labelValue", label);
548
+ }
549
+ },
550
+ cancelPendingSync() {
551
+ this.syncToken += 1;
552
+ },
553
+ getTabLabel(tab) {
554
+ if (tab.selected) {
555
+ return tab.selected.name || tab.label;
556
+ }
557
+ return tab.label;
558
+ },
559
+ isSelected(item) {
560
+ const current = this.selectedPath[this.activeLevel - 1];
561
+ return current && this.getAreaId(current) === this.getAreaId(item);
562
+ },
563
+ isTabEnabled(level) {
564
+ return level === 1 || this.selectedPath.length >= level - 1;
565
+ },
566
+ handlePopoverShow() {
567
+ this.pendingLeaf = false;
568
+ this.saveCommittedBrowseSnapshot();
569
+ this.$emit("popover-open");
570
+ this.$nextTick(() => {
571
+ this.$refs.searchInputRef && this.$refs.searchInputRef.focus();
572
+ });
573
+ this.initBrowseStateFromValue();
574
+ },
575
+ saveCommittedBrowseSnapshot() {
576
+ this.committedBoundValue = this.value;
577
+ this.committedBrowseSnapshot = {
578
+ boundValue: this.value,
579
+ leafId: this.getLeafIdFromValue(this.value),
580
+ selectedPath: this.selectedPath.map((item) => ({ ...item })),
581
+ activeLevel: this.activeLevel,
582
+ };
583
+ },
584
+ isSameBoundValue(a, b) {
585
+ const normalize = (val) => {
586
+ if (val == null || val === "") {
587
+ return null;
588
+ }
589
+ return String(val);
590
+ };
591
+ return normalize(a) === normalize(b);
592
+ },
593
+ emitChangeIfCommitted() {
594
+ const snapshot = this.committedBrowseSnapshot;
595
+ if (!snapshot) {
596
+ return;
597
+ }
598
+ const oldVal = snapshot.boundValue;
599
+ const newVal = this.value;
600
+ if (!this.isSameBoundValue(oldVal, newVal)) {
601
+ const payload = this.lastSelectionPayload || {
602
+ value: newVal,
603
+ row: null,
604
+ label: this.labelValue,
605
+ };
606
+ this.$emit("change", {
607
+ value: payload.value != null ? payload.value : newVal,
608
+ row: payload.row || null,
609
+ label: payload.label != null ? payload.label : this.labelValue,
610
+ path: payload.path || [],
611
+ });
612
+ }
613
+ this.committedBoundValue = null;
614
+ this.lastSelectionPayload = null;
615
+ },
616
+ restoreBrowseStateIfUncommitted() {
617
+ const snapshot = this.committedBrowseSnapshot;
618
+ if (!snapshot) {
619
+ return;
620
+ }
621
+ if (!this.isSameBoundValue(this.value, snapshot.boundValue)) {
622
+ this.levelItems = [];
623
+ this.committedBrowseSnapshot = null;
624
+ return;
625
+ }
626
+ const committedLeafId = snapshot.leafId;
627
+ if (this.isValueSynced(committedLeafId)) {
628
+ this.levelItems = [];
629
+ this.committedBrowseSnapshot = null;
630
+ return;
631
+ }
632
+ if (snapshot.selectedPath && snapshot.selectedPath.length) {
633
+ this.selectedPath = snapshot.selectedPath.map((item) => ({ ...item }));
634
+ this.activeLevel = snapshot.activeLevel;
635
+ } else if (committedLeafId) {
636
+ this.selectedPath = [
637
+ {
638
+ id: committedLeafId,
639
+ fullName: this.labelValue || undefined,
640
+ name: this.labelValue || undefined,
641
+ },
642
+ ];
643
+ this.activeLevel = 1;
644
+ } else {
645
+ this.selectedPath = [];
646
+ this.activeLevel = 1;
647
+ }
648
+ this.levelItems = [];
649
+ this.committedBrowseSnapshot = null;
650
+ },
651
+ syncFromValue() {
652
+ if (this.skipValueSync) {
653
+ return;
654
+ }
655
+ const val = this.value;
656
+ if (!val) {
657
+ this.resetBrowseState(false);
658
+ return;
659
+ }
660
+ if (!this.hasDisplayLabel()) {
661
+ this.resolveLabelFromValue();
662
+ }
663
+ },
664
+ async resolveLabelFromValue() {
665
+ const val = this.value;
666
+ if (!val || this.hasDisplayLabel()) {
667
+ return;
668
+ }
669
+ const leafId = this.getLeafIdFromValue(val);
670
+ if (leafId == null) {
671
+ return;
672
+ }
673
+ const token = ++this.syncToken;
674
+ const area = await this.fetchAreaById(leafId);
675
+ if (token !== this.syncToken || this.hasDisplayLabel()) {
676
+ return;
677
+ }
678
+ const label = (area && area.fullName) || (area && area.name) || null;
679
+ if (label) {
680
+ this.$emit("update:labelValue", label);
681
+ }
682
+ },
683
+ shouldResyncBrowsePath() {
684
+ if (!this.labelValue || !this.selectedPath.length) {
685
+ return false;
686
+ }
687
+ const pathLabel = this.buildPathLabel(this.selectedPath);
688
+ return pathLabel !== String(this.labelValue).trim();
689
+ },
690
+ syncBrowseStateForPopover() {
691
+ const val = this.value;
692
+ if (!val) {
693
+ if (!this.levelItems.length) {
694
+ this.loadLevelItems(this.getParentId());
695
+ }
696
+ return;
697
+ }
698
+ const leafId = this.getLeafIdFromValue(val);
699
+ if (this.isValueSynced(leafId) && !this.shouldResyncBrowsePath()) {
700
+ const expectedActive = this.selectedPath.length || 1;
701
+ if (this.activeLevel !== expectedActive) {
702
+ this.activeLevel = expectedActive;
703
+ this.loadLevelItems(this.getParentId());
704
+ return;
705
+ }
706
+ if (!this.levelItems.length) {
707
+ this.loadLevelItems(this.getParentId());
708
+ }
709
+ return;
710
+ }
711
+ if (this.isLegacyPathValue(val)) {
712
+ this.syncBrowseStateFromPathIds(this.parseLegacyPathIds(val));
713
+ return;
714
+ }
715
+ this.syncBrowseStateFromLeafId(leafId);
716
+ },
717
+ initBrowseStateFromValue() {
718
+ this.syncBrowseStateForPopover();
719
+ },
720
+ getAreaRequestData(extra = {}) {
721
+ return {
722
+ enabled: true,
723
+ locale: this.locale || null,
724
+ ...extra,
725
+ };
726
+ },
727
+ normalizeAreaRows(res) {
728
+ const objx = res && res.objx;
729
+ if (Array.isArray(objx)) {
730
+ return objx;
731
+ }
732
+ if (objx && Array.isArray(objx.records)) {
733
+ return objx.records;
734
+ }
735
+ return [];
736
+ },
737
+ requestAreaList(extra = {}) {
738
+ return new Promise((resolve) => {
739
+ this.$http({
740
+ url: USER_PREFIX + "/area/listPage",
741
+ method: "post",
742
+ data: this.getAreaRequestData(extra),
743
+ success: (res) => resolve(this.normalizeAreaRows(res)),
744
+ error: () => resolve([]),
745
+ });
746
+ });
747
+ },
748
+ fetchChildren(parentId) {
749
+ return new Promise((resolve) => {
750
+ this.$http({
751
+ url: USER_PREFIX + "/area/getChildren",
752
+ method: "post",
753
+ data: this.getAreaRequestData({ id: parentId }),
754
+ success: (res) => resolve(this.normalizeAreaRows(res)),
755
+ error: () => resolve([]),
756
+ });
757
+ });
758
+ },
759
+ getParentIdFromRow(row) {
760
+ if (!row) return null;
761
+ if (row.parentId != null && row.parentId !== "") {
762
+ return row.parentId;
763
+ }
764
+ const parent = row.parent;
765
+ if (parent == null || parent === undefined || parent === "") {
766
+ return null;
767
+ }
768
+ if (typeof parent === "object") {
769
+ return parent.id != null ? parent.id : null;
770
+ }
771
+ return parent;
772
+ },
773
+ fetchAreaById(id) {
774
+ if (id == null || id === "") {
775
+ return Promise.resolve(null);
776
+ }
777
+ return new Promise((resolve) => {
778
+ this.$http({
779
+ url: USER_PREFIX + "/area/get",
780
+ method: "post",
781
+ data: this.getAreaRequestData({ id }),
782
+ success: (res) => {
783
+ const objx = res && res.objx;
784
+ if (objx && typeof objx === "object" && !Array.isArray(objx)) {
785
+ resolve(objx);
786
+ return;
787
+ }
788
+ resolve(null);
789
+ },
790
+ error: () => resolve(null),
791
+ });
792
+ });
793
+ },
794
+ isAreaObject(node) {
795
+ return node && typeof node === "object" && node.id != null;
796
+ },
797
+ buildAreaPath(item, depthLimit) {
798
+ const limit = depthLimit == null ? this.maxLevel : depthLimit;
799
+ if (
800
+ item.pathList &&
801
+ Array.isArray(item.pathList) &&
802
+ item.pathList.length > 1
803
+ ) {
804
+ if (this.isAreaObject(item.pathList[0])) {
805
+ return item.pathList.slice(0, limit);
806
+ }
807
+ }
808
+ if (
809
+ item.areaPath &&
810
+ Array.isArray(item.areaPath) &&
811
+ item.areaPath.length > 1
812
+ ) {
813
+ if (this.isAreaObject(item.areaPath[0])) {
814
+ return item.areaPath.slice(0, limit);
815
+ }
816
+ }
817
+ return null;
818
+ },
819
+ matchAreaByPrefix(fullName, prefix, children) {
820
+ const sorted = [...children].sort(
821
+ (a, b) => (b.name || "").length - (a.name || "").length
822
+ );
823
+ for (let i = 0; i < sorted.length; i++) {
824
+ const child = sorted[i];
825
+ const name = child.name || "";
826
+ if (!name) {
827
+ continue;
828
+ }
829
+ if (fullName.startsWith(prefix + name)) {
830
+ return child;
831
+ }
832
+ }
833
+ return null;
834
+ },
835
+ async resolvePathByFullName(item, depthLimit) {
836
+ const fullName = (item.fullName || "").trim();
837
+ if (!fullName) {
838
+ return null;
839
+ }
840
+ const path = [];
841
+ let parentId = 0;
842
+ let prefix = "";
843
+ for (let level = 0; level < MAX_AREA_DEPTH; level++) {
844
+ const children = await this.fetchChildren(parentId);
845
+ if (!children.length) {
846
+ break;
847
+ }
848
+ const matched = this.matchAreaByPrefix(fullName, prefix, children);
849
+ if (!matched) {
850
+ break;
851
+ }
852
+ path.push(matched);
853
+ prefix += matched.name || "";
854
+ if (matched.id == item.id) {
855
+ return path;
856
+ }
857
+ parentId = matched.id;
858
+ }
859
+ if (path.length && path[path.length - 1].id != item.id) {
860
+ const siblings = await this.fetchChildren(path[path.length - 1].id);
861
+ const leaf = siblings.find((row) => row.id == item.id);
862
+ if (leaf) {
863
+ path.push(leaf);
864
+ } else if (item.name && fullName.endsWith(item.name)) {
865
+ path.push(item);
866
+ }
867
+ }
868
+ return path.length ? path : null;
869
+ },
870
+ async resolvePathByParentChain(item, depthLimit) {
871
+ const chain = [];
872
+ let current = item;
873
+ let safety = 0;
874
+ while (current && safety < MAX_AREA_DEPTH + 2) {
875
+ safety += 1;
876
+ chain.unshift(current);
877
+ const parentId = this.getParentIdFromRow(current);
878
+ if (!parentId) {
879
+ break;
880
+ }
881
+ const parent = await this.fetchAreaById(parentId);
882
+ if (!parent || parent.id != parentId) {
883
+ break;
884
+ }
885
+ current = parent;
886
+ }
887
+ return chain.length ? chain : null;
888
+ },
889
+ async resolvePathFromTreePath(area, depthLimit) {
890
+ const ids = this.parseTreePathIds(area && area.treePath);
891
+ if (!ids.length) {
892
+ return area && area.id != null ? [area] : null;
893
+ }
894
+ const path = [];
895
+ for (let i = 0; i < ids.length; i++) {
896
+ const id = ids[i];
897
+ let node = null;
898
+ if (area && this.getAreaId(area) == id) {
899
+ node = area;
900
+ } else {
901
+ node = await this.fetchAreaById(id);
902
+ }
903
+ path.push(node || { id });
904
+ }
905
+ if (area && area.id != null) {
906
+ const exists = path.some((node) => this.getAreaId(node) == area.id);
907
+ if (!exists) {
908
+ path.push(area);
909
+ }
910
+ }
911
+ return path.length ? path : null;
912
+ },
913
+ async resolveAreaPath(item, options = {}) {
914
+ if (!item || item.id == null) {
915
+ return item ? [item] : [];
916
+ }
917
+ const parentPath = await this.resolvePathByParentChain(item);
918
+ if (this.isPathResolvedForItem(parentPath, item)) {
919
+ return parentPath;
920
+ }
921
+ const depthLimit =
922
+ options.depthLimit != null
923
+ ? options.depthLimit
924
+ : this.getResolveDepthLimit(item);
925
+ if (item.treePath) {
926
+ const treePath = await this.resolvePathFromTreePath(item, depthLimit);
927
+ if (this.isPathResolvedForItem(treePath, item)) {
928
+ return treePath;
929
+ }
930
+ }
931
+ const presetPath = this.buildAreaPath(item, depthLimit);
932
+ if (this.isPathResolvedForItem(presetPath, item)) {
933
+ return presetPath;
934
+ }
935
+ const fullNamePath = await this.resolvePathByFullName(item, depthLimit);
936
+ if (this.isPathResolvedForItem(fullNamePath, item)) {
937
+ return fullNamePath;
938
+ }
939
+ if (parentPath && parentPath.length) {
940
+ return parentPath;
941
+ }
942
+ if (item.treePath) {
943
+ const treePath = await this.resolvePathFromTreePath(item, depthLimit);
944
+ if (treePath && treePath.length) {
945
+ return treePath;
946
+ }
947
+ }
948
+ if (presetPath && presetPath.length) {
949
+ return presetPath;
950
+ }
951
+ if (fullNamePath && fullNamePath.length) {
952
+ return fullNamePath;
953
+ }
954
+ return [item];
955
+ },
956
+ async syncBrowseStateFromLeafId(id) {
957
+ if (id == null || id === "") {
958
+ return;
959
+ }
960
+ const token = ++this.syncToken;
961
+ const area = await this.fetchAreaById(id);
962
+ if (token !== this.syncToken) {
963
+ return;
964
+ }
965
+ if (area) {
966
+ await this.syncBrowseStateFromArea(area);
967
+ return;
968
+ }
969
+ await this.syncBrowseStateFromArea({
970
+ id,
971
+ fullName: this.labelValue || undefined,
972
+ });
973
+ },
974
+ async syncBrowseStateFromArea(item) {
975
+ const token = ++this.syncToken;
976
+ let seed = item || {};
977
+ if (
978
+ seed.id != null &&
979
+ !seed.fullName &&
980
+ !seed.name &&
981
+ !this.getParentIdFromRow(seed)
982
+ ) {
983
+ const detail = await this.fetchAreaById(seed.id);
984
+ if (detail) {
985
+ seed = detail;
986
+ }
987
+ } else if (seed.id != null && !seed.fullName && this.labelValue) {
988
+ seed = { ...seed, fullName: this.labelValue };
989
+ }
990
+ let path = await this.resolveAreaPath(seed);
991
+ if (token !== this.syncToken) {
992
+ return;
993
+ }
994
+ if (!path.length) {
995
+ path = [seed];
996
+ }
997
+ const last = path[path.length - 1];
998
+ if (last && last.id != seed.id && seed.id != null) {
999
+ const siblings = await this.fetchChildren(
1000
+ path.length > 1 ? path[path.length - 2].id : 0
1001
+ );
1002
+ const exact = siblings.find((row) => row.id == seed.id);
1003
+ if (exact) {
1004
+ path = path.slice(0, -1).concat(exact);
1005
+ } else {
1006
+ path = path.concat(seed);
1007
+ }
1008
+ }
1009
+ if (token !== this.syncToken) {
1010
+ return;
1011
+ }
1012
+ this.selectedPath = path;
1013
+ this.activeLevel = path.length || 1;
1014
+ this.loadLevelItems(this.getParentId());
1015
+ this.emitResolvedLabelIfNeeded();
1016
+ },
1017
+ async resolvePathByIds(ids, depthLimit) {
1018
+ const limit = depthLimit == null ? ids.length : depthLimit;
1019
+ const path = [];
1020
+ let parentId = 0;
1021
+ for (let i = 0; i < ids.length && i < limit; i++) {
1022
+ const targetId = ids[i];
1023
+ const children = await this.fetchChildren(parentId);
1024
+ let matched = children.find((row) => this.getAreaId(row) == targetId);
1025
+ if (matched) {
1026
+ path.push(matched);
1027
+ parentId = matched.id;
1028
+ continue;
1029
+ }
1030
+ const fetched = await this.fetchAreaById(targetId);
1031
+ if (fetched) {
1032
+ path.push(fetched);
1033
+ parentId = fetched.id;
1034
+ } else {
1035
+ path.push({ id: targetId });
1036
+ break;
1037
+ }
1038
+ }
1039
+ return path;
1040
+ },
1041
+ async syncBrowseStateFromPathIds(ids) {
1042
+ const token = ++this.syncToken;
1043
+ const depthLimit = Math.min(
1044
+ Math.max(ids.length, this.maxLevel, 1),
1045
+ MAX_AREA_DEPTH
1046
+ );
1047
+ const path = await this.resolvePathByIds(ids, depthLimit);
1048
+ if (token !== this.syncToken) {
1049
+ return;
1050
+ }
1051
+ if (!path.length) {
1052
+ if (!this.levelItems.length) {
1053
+ this.loadLevelItems(0);
1054
+ }
1055
+ return;
1056
+ }
1057
+ this.selectedPath = path;
1058
+ this.activeLevel = path.length;
1059
+ this.loadLevelItems(this.getParentId());
1060
+ this.emitResolvedLabelIfNeeded();
1061
+ },
1062
+ handlePopoverHide() {
1063
+ this.searchKeyword = "";
1064
+ this.searchList = [];
1065
+ this.searchDropdownVisible = false;
1066
+ this.pendingLeaf = false;
1067
+ this.cancelPendingSync();
1068
+ this.emitChangeIfCommitted();
1069
+ this.restoreBrowseStateIfUncommitted();
1070
+ },
1071
+ handleSearchInput() {
1072
+ const keyword = (this.searchKeyword || "").trim();
1073
+ clearTimeout(this.searchTimer);
1074
+ this.searchDropdownVisible = !!keyword;
1075
+ if (!keyword) {
1076
+ this.handleSearchClear();
1077
+ return;
1078
+ }
1079
+ this.searchTimer = setTimeout(() => {
1080
+ this.doSearch(keyword);
1081
+ }, SEARCH_DEBOUNCE);
1082
+ },
1083
+ handleSearchClear() {
1084
+ this.searchList = [];
1085
+ this.searchDropdownVisible = false;
1086
+ },
1087
+ doSearch(keyword) {
1088
+ this.searchLoading = true;
1089
+ this.requestAreaList({ fullName: keyword, size: 20 }).then((rows) => {
1090
+ this.searchList = rows;
1091
+ this.searchLoading = false;
1092
+ });
1093
+ },
1094
+ getParentId() {
1095
+ if (this.activeLevel <= 1) {
1096
+ return 0;
1097
+ }
1098
+ const parent = this.selectedPath[this.activeLevel - 2];
1099
+ return parent ? this.getAreaId(parent) : 0;
1100
+ },
1101
+ loadLevelItems(parentId) {
1102
+ this.browseLoading = true;
1103
+ this.fetchChildren(parentId).then((rows) => {
1104
+ this.levelItems = rows;
1105
+ this.browseLoading = false;
1106
+ });
1107
+ },
1108
+ switchLevel(level) {
1109
+ if (level > this.browseLevelCount || !this.isTabEnabled(level)) {
1110
+ return;
1111
+ }
1112
+ this.cancelPendingSync();
1113
+ this.activeLevel = level;
1114
+ this.loadLevelItems(this.getParentId());
1115
+ },
1116
+ prevLevel() {
1117
+ if (!this.canPrevLevel) {
1118
+ return;
1119
+ }
1120
+ this.switchLevel(this.activeLevel - 1);
1121
+ },
1122
+ confirmSelection() {
1123
+ if (!this.canConfirm) {
1124
+ return;
1125
+ }
1126
+ const last = this.selectedPath[this.selectedPath.length - 1];
1127
+ this.applySelection(last).then(() => {
1128
+ this.popoverVisible = false;
1129
+ });
1130
+ },
1131
+ cancelSelection() {
1132
+ /* 关闭弹层,未确认的浏览状态由 hide 回调恢复 */
1133
+ this.popoverVisible = false;
1134
+ },
1135
+ nextLevel() {
1136
+ if (!this.canNextLevel) {
1137
+ return;
1138
+ }
1139
+ this.switchLevel(this.activeLevel + 1);
1140
+ },
1141
+ selectSearchItem(item) {
1142
+ this.cancelPendingSync();
1143
+ this.syncBrowseStateFromArea(item).then(() => {
1144
+ this.searchKeyword = "";
1145
+ this.searchList = [];
1146
+ this.searchDropdownVisible = false;
1147
+ this.pendingLeaf = false;
1148
+ if (this.selectedPath.length >= this.effectiveMinLevel) {
1149
+ /* 已达到最小选择层级:等待确认 */
1150
+ return;
1151
+ }
1152
+ /* 搜索结果未达到最小选择层级:有下级只定位下钻,无下级视为末级 */
1153
+ const last = this.selectedPath[this.selectedPath.length - 1] || item;
1154
+ this.fetchChildren(this.getAreaId(last)).then((children) => {
1155
+ if (!children.length) {
1156
+ this.pendingLeaf = true;
1157
+ return;
1158
+ }
1159
+ this.activeLevel = this.selectedPath.length + 1;
1160
+ this.levelItems = children;
1161
+ });
1162
+ });
1163
+ },
1164
+ selectItem(item) {
1165
+ if (!this.isTabEnabled(this.activeLevel)) {
1166
+ return;
1167
+ }
1168
+ this.cancelPendingSync();
1169
+ const level = this.activeLevel;
1170
+ this.selectedPath = this.selectedPath.slice(0, level - 1);
1171
+ this.selectedPath.push(item);
1172
+ this.pendingLeaf = false;
1173
+ if (level >= this.maxLevel) {
1174
+ /* 已到最大层级:等待确认 */
1175
+ return;
1176
+ }
1177
+ this.fetchChildren(this.getAreaId(item)).then((children) => {
1178
+ if (!children.length) {
1179
+ /* 无下级数据:视为末级,等待确认 */
1180
+ this.pendingLeaf = true;
1181
+ return;
1182
+ }
1183
+ this.activeLevel = level + 1;
1184
+ this.levelItems = children;
1185
+ });
1186
+ },
1187
+ async clampPathToMaxLevel(path) {
1188
+ if (!path || !path.length || path.length <= this.maxLevel) {
1189
+ return path || [];
1190
+ }
1191
+ const trimmed = path.slice(0, this.maxLevel);
1192
+ const last = trimmed[trimmed.length - 1];
1193
+ const id = this.getAreaId(last);
1194
+ if (id != null && (!last.name || !last.fullName)) {
1195
+ const detail = await this.fetchAreaById(id);
1196
+ if (detail) {
1197
+ trimmed[trimmed.length - 1] = detail;
1198
+ }
1199
+ }
1200
+ return trimmed;
1201
+ },
1202
+ async applySelection(item) {
1203
+ let path = this.selectedPath.length
1204
+ ? this.selectedPath.map((node) => ({ ...node }))
1205
+ : [item];
1206
+ path = await this.clampPathToMaxLevel(path);
1207
+ this.selectedPath = path.map((node) => ({ ...node }));
1208
+ this.activeLevel = path.length || 1;
1209
+ let last = path[path.length - 1] || item;
1210
+ const leafId = this.getAreaId(last);
1211
+ if (leafId != null && !last.fullName) {
1212
+ const detail = await this.fetchAreaById(leafId);
1213
+ if (detail) {
1214
+ last = detail;
1215
+ if (path.length) {
1216
+ path = path.slice();
1217
+ path[path.length - 1] = detail;
1218
+ this.$set(this.selectedPath, this.selectedPath.length - 1, detail);
1219
+ }
1220
+ }
1221
+ }
1222
+ const label = last.fullName || this.buildPathLabel(path);
1223
+ const formattedValue = this.formatBoundValue(path, last);
1224
+ this.lastSelectionPayload = {
1225
+ value: formattedValue,
1226
+ row: last,
1227
+ label,
1228
+ path: path.map((node) => ({ ...node })),
1229
+ };
1230
+ this.skipValueSync = true;
1231
+ this.$emit("input", formattedValue);
1232
+ this.$emit("update:labelValue", label);
1233
+ this.$nextTick(() => {
1234
+ this.skipValueSync = false;
1235
+ });
1236
+ },
1237
+ handleClear() {
1238
+ const oldVal = this.value;
1239
+ const wasOpen = this.popoverVisible;
1240
+ this.lastSelectionPayload = { value: null, row: null, label: null };
1241
+ this.skipValueSync = true;
1242
+ this.$emit("input", null);
1243
+ this.$emit("update:labelValue", null);
1244
+ this.resetBrowseState(false);
1245
+ this.popoverVisible = false;
1246
+ this.$nextTick(() => {
1247
+ this.skipValueSync = false;
1248
+ if (!wasOpen && !this.isSameBoundValue(oldVal, null)) {
1249
+ this.$emit("change", this.lastSelectionPayload);
1250
+ this.committedBoundValue = null;
1251
+ this.lastSelectionPayload = null;
1252
+ }
1253
+ });
1254
+ },
1255
+ resetBrowseState(emitEvent) {
1256
+ this.searchKeyword = "";
1257
+ this.searchList = [];
1258
+ this.selectedPath = [];
1259
+ this.activeLevel = 1;
1260
+ this.levelItems = [];
1261
+ this.committedBrowseSnapshot = null;
1262
+ if (emitEvent) {
1263
+ this.$emit("input", null);
1264
+ this.$emit("update:labelValue", null);
1265
+ }
1266
+ },
1267
+ },
1268
+ };
1269
+ </script>
1270
+
1271
+ <style lang="scss" scoped>
1272
+ @import "~@/styles/variables.scss";
1273
+
1274
+ .base-area {
1275
+ width: 100%;
1276
+ display: block;
1277
+ box-sizing: border-box;
1278
+ }
1279
+
1280
+ .base-area-trigger {
1281
+ display: block;
1282
+ width: 100%;
1283
+ box-sizing: border-box;
1284
+ }
1285
+
1286
+ .base-area-input {
1287
+ width: 100%;
1288
+ cursor: pointer;
1289
+
1290
+ .base-area-clear {
1291
+ cursor: pointer;
1292
+ display: none;
1293
+ }
1294
+
1295
+ &:hover .base-area-clear,
1296
+ &.is-focus .base-area-clear {
1297
+ display: inline-block;
1298
+ }
1299
+ }
1300
+
1301
+ .base-area-panel {
1302
+ padding: 4px 0 0;
1303
+ overflow: visible;
1304
+ }
1305
+
1306
+ .base-area-search {
1307
+ position: relative;
1308
+ margin-bottom: 8px;
1309
+ z-index: 2;
1310
+ }
1311
+
1312
+ .base-area-search-dropdown {
1313
+ position: absolute;
1314
+ top: calc(100% + 4px);
1315
+ left: 0;
1316
+ right: 0;
1317
+ max-height: 200px;
1318
+ overflow-y: auto;
1319
+ background: #fff;
1320
+ border: 1px solid #e4e7ed;
1321
+ border-radius: 4px;
1322
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
1323
+ z-index: 20;
1324
+ }
1325
+
1326
+ .base-area-dropdown-empty {
1327
+ padding: 12px 10px;
1328
+ text-align: center;
1329
+ color: #909399;
1330
+ font-size: 12px;
1331
+ }
1332
+
1333
+ .base-area-fade-enter-active,
1334
+ .base-area-fade-leave-active {
1335
+ transition: opacity 0.15s ease;
1336
+ }
1337
+
1338
+ .base-area-fade-enter,
1339
+ .base-area-fade-leave-to {
1340
+ opacity: 0;
1341
+ }
1342
+
1343
+ .base-area-result-item {
1344
+ padding: 8px 10px;
1345
+ font-size: 12px;
1346
+ line-height: 1.4;
1347
+ color: #606266;
1348
+ cursor: pointer;
1349
+ border-radius: 4px;
1350
+
1351
+ &:hover {
1352
+ background: rgba($baseColor, 0.08);
1353
+ color: $baseColor;
1354
+ }
1355
+
1356
+ &.on {
1357
+ background: rgba($baseColor, 0.12);
1358
+ color: $baseColor;
1359
+ }
1360
+ }
1361
+
1362
+ .base-area-path {
1363
+ padding: 6px 10px;
1364
+ margin-bottom: 10px;
1365
+ background: #f5f7fa;
1366
+ border: 1px solid #ebeef5;
1367
+ border-radius: 4px;
1368
+ }
1369
+
1370
+ .base-area-tabs {
1371
+ display: flex;
1372
+ flex-wrap: wrap;
1373
+ align-items: center;
1374
+ width: 100%;
1375
+ gap: 0;
1376
+ }
1377
+
1378
+ .base-area-path-sep {
1379
+ flex-shrink: 0;
1380
+ padding: 0 2px;
1381
+ font-size: 12px;
1382
+ color: #c0c4cc;
1383
+ line-height: 24px;
1384
+ user-select: none;
1385
+ }
1386
+
1387
+ .base-area-tab {
1388
+ flex-shrink: 1;
1389
+ min-width: 0;
1390
+ max-width: 100%;
1391
+ padding: 0 4px;
1392
+ height: 24px;
1393
+ line-height: 24px;
1394
+ border: none;
1395
+ border-bottom: 2px solid transparent;
1396
+ border-radius: 0;
1397
+ background: transparent;
1398
+ font-size: 12px;
1399
+ color: #909399;
1400
+ cursor: pointer;
1401
+ overflow: hidden;
1402
+ text-overflow: ellipsis;
1403
+ white-space: nowrap;
1404
+ transition: color 0.15s, border-color 0.15s;
1405
+
1406
+ &.active {
1407
+ color: $baseColor;
1408
+ border-bottom-color: $baseColor;
1409
+ font-weight: 500;
1410
+ background: transparent;
1411
+ }
1412
+
1413
+ &.selected:not(.active) {
1414
+ color: #303133;
1415
+ }
1416
+
1417
+ &.disabled {
1418
+ color: #c0c4cc;
1419
+ cursor: not-allowed;
1420
+ background: transparent;
1421
+ pointer-events: none;
1422
+ }
1423
+
1424
+ &:hover:not(.disabled):not(.active) {
1425
+ color: $baseColor;
1426
+ }
1427
+ }
1428
+
1429
+ .base-area-grid-wrap {
1430
+ padding: 8px;
1431
+ background: #fff;
1432
+ border: 1px solid #eee;
1433
+ border-radius: 8px;
1434
+ }
1435
+
1436
+ .base-area-grid {
1437
+ display: flex;
1438
+ flex-wrap: wrap;
1439
+ min-height: 120px;
1440
+ max-height: 294px;
1441
+ overflow-y: auto;
1442
+ margin: 0 -4px;
1443
+ }
1444
+
1445
+ .base-area-item {
1446
+ width: calc(25% - 8px);
1447
+ margin: 0 4px 8px;
1448
+ padding: 0 10px;
1449
+ height: 34px;
1450
+ line-height: 34px;
1451
+ border: 1px solid #eee;
1452
+ border-radius: 8px;
1453
+ font-size: 12px;
1454
+ cursor: pointer;
1455
+ overflow: hidden;
1456
+ text-overflow: ellipsis;
1457
+ white-space: nowrap;
1458
+
1459
+ &:hover {
1460
+ color: $baseColor;
1461
+ border-color: $baseColor;
1462
+ }
1463
+
1464
+ &.on {
1465
+ background: $baseColor;
1466
+ border-color: $baseColor;
1467
+ color: #fff;
1468
+ }
1469
+ }
1470
+
1471
+ .base-area-empty {
1472
+ width: 100%;
1473
+ padding: 24px 0;
1474
+ text-align: center;
1475
+ color: #909399;
1476
+ font-size: 12px;
1477
+ }
1478
+
1479
+ .base-area-level-nav {
1480
+ display: flex;
1481
+ align-items: center;
1482
+ justify-content: space-between;
1483
+ margin-top: 8px;
1484
+
1485
+ .base-area-nav-btn {
1486
+ min-width: 32px;
1487
+ padding: 7px 10px;
1488
+ border-radius: 8px;
1489
+ color: #fff;
1490
+ border-color: $baseColor;
1491
+ background-color: $baseColor;
1492
+
1493
+ i {
1494
+ font-weight: bold;
1495
+ }
1496
+
1497
+ &.el-button:focus,
1498
+ &.el-button:active {
1499
+ color: #fff;
1500
+ border-color: $baseColor;
1501
+ background-color: $baseColor;
1502
+ }
1503
+
1504
+ &.el-button:hover:not(.is-disabled) {
1505
+ color: #fff;
1506
+ border-color: $baseColor;
1507
+ background-color: $baseColor;
1508
+ opacity: 0.92;
1509
+ }
1510
+
1511
+ &.is-disabled,
1512
+ &.el-button.is-disabled {
1513
+ color: #c0c4cc;
1514
+ border-color: #eee;
1515
+ background-color: #f5f7fa;
1516
+ opacity: 1;
1517
+ }
1518
+ }
1519
+
1520
+ .base-area-level-label {
1521
+ flex: 1;
1522
+ text-align: center;
1523
+ font-size: 12px;
1524
+ color: #606266;
1525
+ line-height: 28px;
1526
+ user-select: none;
1527
+ }
1528
+ }
1529
+
1530
+ .base-area-footer {
1531
+ display: flex;
1532
+ align-items: center;
1533
+ justify-content: flex-end;
1534
+ margin-top: 8px;
1535
+ padding-top: 8px;
1536
+ border-top: 1px solid #ebeef5;
1537
+ }
1538
+ </style>
1539
+
1540
+ <style lang="scss">
1541
+ @import "~@/styles/variables.scss";
1542
+
1543
+ /* el-popover 根节点为 inline span,需强制块级铺满 */
1544
+ .base-area {
1545
+ width: 100% !important;
1546
+ display: block !important;
1547
+ box-sizing: border-box;
1548
+
1549
+ > span {
1550
+ display: block !important;
1551
+ width: 100% !important;
1552
+ box-sizing: border-box;
1553
+ }
1554
+
1555
+ .el-popover__reference-wrapper,
1556
+ .el-popover__reference,
1557
+ .base-area-trigger {
1558
+ display: block !important;
1559
+ width: 100% !important;
1560
+ box-sizing: border-box;
1561
+ }
1562
+
1563
+ .base-area-input.el-input {
1564
+ width: 100% !important;
1565
+ display: block !important;
1566
+ }
1567
+
1568
+ .base-area-input .base-area-clear {
1569
+ cursor: pointer;
1570
+ display: none;
1571
+ }
1572
+
1573
+ .base-area-input:hover .base-area-clear,
1574
+ .base-area-input.is-focus .base-area-clear {
1575
+ display: inline-block;
1576
+ }
1577
+ }
1578
+
1579
+ .base-area-popover {
1580
+ padding: 10px 12px;
1581
+ overflow: visible;
1582
+ box-sizing: border-box;
1583
+
1584
+ .base-area-panel {
1585
+ width: 100%;
1586
+ box-sizing: border-box;
1587
+ }
1588
+
1589
+ .base-area-search {
1590
+ width: 100%;
1591
+ box-sizing: border-box;
1592
+ }
1593
+
1594
+ .base-area-search-input,
1595
+ .base-area-search .el-input {
1596
+ width: 100% !important;
1597
+ display: block;
1598
+ box-sizing: border-box;
1599
+ }
1600
+
1601
+ .base-area-nav-btn.el-button {
1602
+ border-radius: 8px;
1603
+ color: #fff;
1604
+ border-color: $baseColor;
1605
+ background-color: $baseColor;
1606
+ }
1607
+
1608
+ .base-area-nav-btn.el-button:focus,
1609
+ .base-area-nav-btn.el-button:active {
1610
+ color: #fff;
1611
+ border-color: $baseColor;
1612
+ background-color: $baseColor;
1613
+ }
1614
+
1615
+ .base-area-nav-btn.el-button.is-disabled {
1616
+ color: #c0c4cc;
1617
+ border-color: #eee;
1618
+ background-color: #f5f7fa;
1619
+ }
1620
+
1621
+ .base-area-tab.disabled {
1622
+ color: #c0c4cc;
1623
+ cursor: not-allowed;
1624
+ background: transparent;
1625
+ pointer-events: none;
1626
+ }
1627
+ }
1628
+ </style>