cloud-web-corejs 1.0.54-dev.744 → 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 (53) 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 -298
  6. package/src/components/fileLibrary/index.vue +1109 -1110
  7. package/src/components/fileLibrary/propertiesDialog.vue +190 -190
  8. package/src/components/fileLibrary/recycleBinDialog.vue +237 -236
  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/dialog/baseFormulaDialog copy.vue +971 -0
  13. package/src/components/xform/form-designer/form-widget/dialog/vabSearchDialog.vue +408 -0
  14. package/src/components/xform/form-designer/form-widget/field-widget/area-select-widget.vue +272 -272
  15. package/src/components/xform/form-designer/form-widget/field-widget/baseAttachment-widget.vue +216 -216
  16. package/src/components/xform/form-designer/form-widget/field-widget/form-item-wrapper.vue +928 -928
  17. package/src/components/xform/form-designer/index.vue +195 -195
  18. package/src/components/xform/form-designer/indexMixin.js +848 -848
  19. package/src/components/xform/form-designer/setting-panel/index.vue +313 -314
  20. package/src/components/xform/form-designer/setting-panel/property-editor/container-data-table/columnRenderDialog.vue +124 -124
  21. package/src/components/xform/form-designer/setting-panel/propertyRegister.js +369 -369
  22. package/src/components/xform/form-designer/widget-panel/index.vue +389 -389
  23. package/src/components/xform/form-designer/widget-panel/indexMixin.js +343 -343
  24. package/src/components/xform/form-designer/widget-panel/widgetsConfig.js +4531 -4531
  25. package/src/components/xform/form-render/container-item/data-table-mixin.js +4748 -4576
  26. package/src/components/xform/form-render/container-item/tab-item.vue +125 -125
  27. package/src/components/xform/form-render/indexMixin.js +5061 -5078
  28. package/src/components/xform/lang/en-US.js +457 -457
  29. package/src/components/xform/lang/zh-CN.js +628 -628
  30. package/src/components/xform/utils/formHttp.js +162 -0
  31. package/src/components/xform/utils/util.js +1585 -1554
  32. package/src/index.js +344 -230
  33. package/src/layout/components/AppMain.vue +122 -122
  34. package/src/layout/components/extractedCode/viewDialog.vue +207 -207
  35. package/src/layout/components/langTool.vue +128 -123
  36. package/src/layout/defaultLayout.vue +164 -158
  37. package/src/permission.js +185 -135
  38. package/src/store/config/index.js +667 -669
  39. package/src/store/modules/micro.js +46 -0
  40. package/src/store/modules/permission.js +461 -373
  41. package/src/store/modules/user.js +419 -415
  42. package/src/utils/auth.js +27 -1
  43. package/src/utils/index.js +579 -6
  44. package/src/utils/keepAlive.js +4 -0
  45. package/src/utils/menuAdapter.js +183 -0
  46. package/src/utils/request.js +417 -28
  47. package/src/views/bd/setting/formVersion/ftHistoryDialog.vue +483 -491
  48. package/src/views/bd/setting/form_script/form_list.vue +174 -174
  49. package/src/views/bd/setting/form_script/mixins/form_list.js +330 -330
  50. package/src/views/bd/setting/form_template/formDesignerDialog.vue +171 -171
  51. package/src/views/bd/setting/form_template/wfObjConfigDialog.vue +253 -254
  52. package/src/views/user/menuFallback/index.vue +123 -0
  53. package/src/views/user/wf/wfReport/index.vue +625 -619
@@ -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>