@truenewx/tnxvue3 3.0.5 → 3.0.7

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truenewx/tnxvue3",
3
- "version": "3.0.5",
3
+ "version": "3.0.7",
4
4
  "description": "互联网技术解决方案:Vue3扩展支持",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -25,7 +25,7 @@
25
25
  "vue-router": "~4.4.0"
26
26
  },
27
27
  "dependencies": {
28
- "@truenewx/tnxcore": "3.0.3",
28
+ "@truenewx/tnxcore": "3.0.5",
29
29
  "@element-plus/icons-vue": "2.3.1",
30
30
  "async-validator": "4.2.5",
31
31
  "mitt": "3.0.1"
@@ -5,7 +5,7 @@
5
5
  <slot></slot>
6
6
  </div>
7
7
  <template #loading>
8
- <Loading class="me-1" theme="inherit"/>
8
+ <LoadingIcon class="me-1" theme="inherit"/>
9
9
  <div>
10
10
  <slot></slot>
11
11
  </div>
@@ -15,11 +15,11 @@
15
15
 
16
16
  <script>
17
17
  import {BButton} from 'bootstrap-vue-next';
18
- import Loading from '../loading/Loading.vue';
18
+ import LoadingIcon from '../loading-icon/LoadingIcon.vue';
19
19
 
20
20
  export default {
21
21
  name: 'TnxbsvButton',
22
- components: {BButton, Loading},
22
+ components: {BButton, LoadingIcon},
23
23
  props: {
24
24
  type: String,
25
25
  icon: String,
@@ -0,0 +1,491 @@
1
+ <template>
2
+ <div class="tnxbsv-cascader" :title="displayValue">
3
+ <BInputGroup @click="toggleDropdown" :class="{'active': visible}">
4
+ <BFormInput class="display"
5
+ v-model="inputValue"
6
+ :readonly="!filterable"
7
+ :placeholder="placeholder"
8
+ :disabled="disabled"
9
+ @input="handleFilter"
10
+ @focus="handleFocus"
11
+ />
12
+ <template #append>
13
+ <BButton variant="outline-secondary" class="btn-append">
14
+ <i v-if="clearable && !disabled && modelValue"
15
+ class="bi bi-x-circle me-1"
16
+ @click.stop="clearValue">
17
+ </i>
18
+ <i class="bi bi-chevron-down" :class="{'rotate': visible}"></i>
19
+ </BButton>
20
+ </template>
21
+ </BInputGroup>
22
+
23
+ <div v-show="visible" class="tnxbsv-cascader-dropdown">
24
+ <div v-if="filtering" class="tnxbsv-cascader-search-list">
25
+ <div v-for="(option, index) of filterResults"
26
+ :key="index"
27
+ class="tnxbsv-cascader-node"
28
+ @click="handleFilterOptionClick(option)">
29
+ <span>{{ getOptionPath(option) }}</span>
30
+ </div>
31
+ </div>
32
+ <div v-else class="tnxbsv-cascader-menus">
33
+ <BListGroup v-for="(options, level) of optionsArray"
34
+ :key="level"
35
+ class="tnxbsv-cascader-menu">
36
+ <BListGroupItem v-for="option of options"
37
+ :key="option[props.value]"
38
+ button
39
+ :disabled="option[props.disabled]"
40
+ :active="isOptionSelected(level, option)"
41
+ class="tnxbsv-cascader-node"
42
+ @click="handleOptionClick(level, option)">
43
+ <span>{{ option[props.label] }}</span>
44
+ <i v-if="option[props.children]" class="bi bi-chevron-right"></i>
45
+ </BListGroupItem>
46
+ </BListGroup>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ </template>
51
+
52
+ <script>
53
+ import {BInputGroup, BFormInput, BButton, BListGroup, BListGroupItem,} from 'bootstrap-vue-next';
54
+
55
+ export default {
56
+ name: 'TnxbsvCascader',
57
+ components: {BInputGroup, BFormInput, BButton, BListGroup, BListGroupItem,},
58
+ props: {
59
+ modelValue: {
60
+ type: String,
61
+ default: '',
62
+ },
63
+ options: {
64
+ type: Array,
65
+ default: () => [],
66
+ },
67
+ placeholder: {
68
+ type: String,
69
+ default: '请选择',
70
+ },
71
+ disabled: {
72
+ type: Boolean,
73
+ default: false,
74
+ },
75
+ parentSelectable: {
76
+ type: Boolean,
77
+ default: false,
78
+ },
79
+ separator: {
80
+ type: String,
81
+ default: ' / ',
82
+ },
83
+ clearable: {
84
+ type: Boolean,
85
+ default: false,
86
+ },
87
+ showAllLevels: {
88
+ type: Boolean,
89
+ default: true,
90
+ },
91
+ filterable: {
92
+ type: Boolean,
93
+ default: false,
94
+ },
95
+ filterMethod: {
96
+ type: Function,
97
+ default: (node, keyword) => node.label.toLowerCase().includes(keyword.toLowerCase()),
98
+ },
99
+ props: {
100
+ type: Object,
101
+ default: () => ({
102
+ label: 'label',
103
+ value: 'value',
104
+ children: 'children',
105
+ disabled: 'disabled'
106
+ })
107
+ },
108
+ optionVisibleSize: {
109
+ type: Number,
110
+ default: 5,
111
+ },
112
+ },
113
+ data() {
114
+ return {
115
+ visible: false,
116
+ selections: this.initSelections(),
117
+ optionsArray: [this.options],
118
+ inputValue: '',
119
+ filtering: false,
120
+ filterResults: [],
121
+ };
122
+ },
123
+ computed: {
124
+ displayValue() {
125
+ if (!this.selections.length) {
126
+ return '';
127
+ }
128
+ // 如果不允许父节点选择,且最后一个选择是非叶子节点,则不显示任何值
129
+ const lastSelection = this.selections[this.selections.length - 1];
130
+ if (!this.parentSelectable && lastSelection[this.props.children]?.length > 0) {
131
+ return this.inputValue;
132
+ }
133
+ const labels = this.selections.filter(item => item).map(item => item[this.props.label]);
134
+ return this.showAllLevels ? labels.join(this.separator) : labels[labels.length - 1];
135
+ },
136
+ },
137
+ watch: {
138
+ displayValue() {
139
+ if (!this.filtering) {
140
+ this.inputValue = this.displayValue;
141
+ }
142
+ },
143
+ modelValue(newValue) {
144
+ const currentValue = this.selections.length > 0 ? this.selections[this.selections.length - 1][this.props.value] : '';
145
+ if (newValue !== currentValue) {
146
+ this.selections = this.initSelections();
147
+ }
148
+ },
149
+ options: {
150
+ deep: true,
151
+ handler(newOptions) {
152
+ this.optionsArray = [newOptions];
153
+ // 清空已选项,避免选项更新后出现无效选择
154
+ this.selections = [];
155
+ this.inputValue = '';
156
+ this.$emit('update:modelValue', '');
157
+ },
158
+ },
159
+ optionVisibleSize: {
160
+ immediate: true,
161
+ handler(value) {
162
+ const fullHeight = value * 2.5 + 1.25;
163
+ document.documentElement.style.setProperty('--tnxbsv-cascader-menu-max-height', `${fullHeight}rem`);
164
+ }
165
+ }
166
+ },
167
+ mounted() {
168
+ document.addEventListener('click', this.handleClickOutside);
169
+ },
170
+ beforeUnmount() {
171
+ document.removeEventListener('click', this.handleClickOutside);
172
+ },
173
+ methods: {
174
+ initSelections() {
175
+ if (!this.modelValue || !this.options) {
176
+ return [];
177
+ }
178
+ const path = this.findPathByValue(this.options, this.modelValue);
179
+ if (path) {
180
+ // 初始化 optionsArray
181
+ path.forEach((option, index) => {
182
+ if (index < path.length - 1) {
183
+ const children = option[this.props.children];
184
+ if (children) {
185
+ this.optionsArray[index + 1] = children;
186
+ }
187
+ }
188
+ });
189
+ return path;
190
+ }
191
+ return [];
192
+ },
193
+ findPathByValue(options, value, path = []) {
194
+ for (const option of options) {
195
+ const currentPath = [...path, option];
196
+ if (option[this.props.value] === value) {
197
+ return currentPath;
198
+ }
199
+ if (option[this.props.children]) {
200
+ const foundPath = this.findPathByValue(option[this.props.children], value, currentPath);
201
+ if (foundPath) {
202
+ return foundPath;
203
+ }
204
+ }
205
+ }
206
+ return null;
207
+ },
208
+ clearValue(e) {
209
+ e.stopPropagation();
210
+ this.selections = [];
211
+ this.optionsArray = [this.options];
212
+ this.$emit('update:modelValue', '');
213
+ this.inputValue = '';
214
+ },
215
+ handleFilter(value) {
216
+ if (!this.filterable) {
217
+ return;
218
+ }
219
+ this.filtering = !!value;
220
+ if (!value) {
221
+ this.filterResults = [];
222
+ this.inputValue = this.displayValue;
223
+ return;
224
+ }
225
+ this.filterResults = this.searchOptions(this.options, value);
226
+ },
227
+ handleFocus() {
228
+ if (this.filterable && !this.filtering) {
229
+ this.inputValue = '';
230
+ }
231
+ },
232
+ searchOptions(options, keyword, path = []) {
233
+ let results = [];
234
+ options.forEach(option => {
235
+ if (this.filterMethod(option, keyword)) {
236
+ results.push({
237
+ ...option,
238
+ path: [...path, option],
239
+ });
240
+ }
241
+ const children = option[this.props.children];
242
+ if (children) {
243
+ results = results.concat(
244
+ this.searchOptions(children, keyword, [...path, option])
245
+ );
246
+ }
247
+ });
248
+ return results;
249
+ },
250
+ getOptionPath(option) {
251
+ return option.path.map(item => item[this.props.label]).join(this.separator);
252
+ },
253
+ handleFilterOptionClick(option) {
254
+ this.selections = option.path;
255
+ this.visible = false;
256
+ this.filtering = false;
257
+ this.inputValue = this.displayValue;
258
+ this.$emit('update:modelValue', option[this.props.value]);
259
+ },
260
+ toggleDropdown() {
261
+ if (!this.disabled) {
262
+ this.visible = !this.visible;
263
+ if (this.visible) {
264
+ this.$nextTick(() => {
265
+ const dropdown = this.$el.querySelector('.tnxbsv-cascader-dropdown');
266
+ const input = this.$el.querySelector('.input-group');
267
+ if (dropdown && input) {
268
+ const rect = input.getBoundingClientRect();
269
+ dropdown.style.top = `${rect.bottom}px`;
270
+ dropdown.style.left = `${rect.left}px`;
271
+ // 移除固定宽度设置,让下拉菜单自动扩展
272
+ dropdown.style.zIndex = window.tnx.util.dom.minTopZIndex();
273
+ }
274
+ });
275
+ }
276
+ }
277
+ },
278
+ isOptionSelected(level, option) {
279
+ return this.selections[level] &&
280
+ this.selections[level][this.props.value] === option[this.props.value];
281
+ },
282
+ handleOptionClick(level, option) {
283
+ if (option[this.props.disabled]) {
284
+ return;
285
+ }
286
+
287
+ this.selections.splice(level);
288
+ this.optionsArray.splice(level + 1);
289
+ this.selections[level] = option;
290
+
291
+ const children = option[this.props.children];
292
+ if (children && children.length) {
293
+ this.optionsArray.push(children);
294
+ } else {
295
+ this.visible = false;
296
+ this.$emit('update:modelValue', option[this.props.value]);
297
+ }
298
+ },
299
+ handleClickOutside(e) {
300
+ if (!this.$el.contains(e.target)) {
301
+ this.visible = false;
302
+ if (this.selections.length > 0) {
303
+ const lastSelection = this.selections[this.selections.length - 1];
304
+ const hasChildren = lastSelection[this.props.children]?.length > 0;
305
+
306
+ if (!hasChildren || this.parentSelectable) {
307
+ // 如果是叶子节点,或者允许父节点选择,则发送最后选择的值
308
+ this.$emit('update:modelValue', lastSelection[this.props.value]);
309
+ }
310
+ // 不做任何清空操作,保持当前选择状态
311
+ }
312
+ }
313
+ }
314
+ },
315
+ };
316
+ </script>
317
+
318
+ <style>
319
+ .tnxbsv-cascader {
320
+ position: relative;
321
+ display: inline-block;
322
+ width: 100%;
323
+ }
324
+
325
+ .tnxbsv-cascader .input-group.active .display,
326
+ .tnxbsv-cascader .input-group.active .btn-append {
327
+ border-color: var(--bs-primary);
328
+ }
329
+
330
+ .tnxbsv-cascader .display {
331
+ border-right: none;
332
+ box-shadow: none;
333
+ text-overflow: ellipsis;
334
+ color: var(--bs-secondary-color);
335
+ padding-right: 0.5rem;
336
+ }
337
+
338
+ .tnxbsv-cascader .display:focus {
339
+ box-shadow: none;
340
+ }
341
+
342
+ .tnxbsv-cascader .btn-append,
343
+ .tnxbsv-cascader .btn-append:hover,
344
+ .tnxbsv-cascader .btn-append:active {
345
+ padding-left: 0.5rem;
346
+ border-color: var(--bs-border-color);
347
+ border-left: none;
348
+ background-color: transparent;
349
+ color: var(--bs-tertiary-color);
350
+ }
351
+
352
+ .tnxbsv-cascader .btn-append .bi-chevron-down {
353
+ display: inline-block;
354
+ transition: transform 0.3s ease;
355
+ }
356
+
357
+ .tnxbsv-cascader .btn-append .rotate {
358
+ transform: rotate(180deg);
359
+ }
360
+
361
+ .tnxbsv-cascader-dropdown {
362
+ position: fixed;
363
+ margin-top: 0.25rem;
364
+ background: var(--bs-body-bg);
365
+ border: var(--bs-border-width) solid var(--bs-border-color);
366
+ border-radius: var(--bs-border-radius);
367
+ box-shadow: var(--bs-dropdown-box-shadow);
368
+ }
369
+
370
+ .tnxbsv-cascader-menus {
371
+ display: flex;
372
+ min-width: 11.25rem;
373
+ overflow-x: auto;
374
+ -webkit-overflow-scrolling: touch;
375
+ scrollbar-width: none;
376
+ -ms-overflow-style: none;
377
+ }
378
+
379
+ .tnxbsv-cascader-menus::-webkit-scrollbar {
380
+ display: none;
381
+ }
382
+
383
+ .tnxbsv-cascader-menu {
384
+ min-width: 11.25rem;
385
+ border-right: var(--bs-border-width) solid var(--bs-border-color);
386
+ flex-shrink: 0;
387
+ border-radius: 0;
388
+ max-height: var(--tnxbsv-cascader-menu-max-height);
389
+ overflow-y: auto;
390
+ }
391
+
392
+ .tnxbsv-cascader-menu:first-child {
393
+ border-top-left-radius: var(--bs-border-radius);
394
+ border-bottom-left-radius: var(--bs-border-radius);
395
+ }
396
+
397
+ .tnxbsv-cascader-menu:last-child {
398
+ border-right: none;
399
+ }
400
+
401
+ .tnxbsv-cascader-menu::-webkit-scrollbar {
402
+ width: 6px;
403
+ }
404
+
405
+ .tnxbsv-cascader-menu::-webkit-scrollbar-track {
406
+ background: transparent;
407
+ border: none;
408
+ }
409
+
410
+ .tnxbsv-cascader-menu::-webkit-scrollbar-thumb {
411
+ background-color: rgba(var(--bs-secondary-rgb), 0.3);
412
+ border-radius: 3px;
413
+ border: 1px solid transparent;
414
+ background-clip: padding-box;
415
+ }
416
+
417
+ .tnxbsv-cascader-menu::-webkit-scrollbar-thumb:hover {
418
+ background-color: rgba(var(--bs-secondary-rgb), 0.5);
419
+ }
420
+
421
+ .tnxbsv-cascader-search-list {
422
+ min-width: 11.25rem;
423
+ max-height: var(--tnxbsv-cascader-menu-max-height);
424
+ overflow-y: auto;
425
+ border-radius: var(--bs-border-radius);
426
+ }
427
+
428
+ .tnxbsv-cascader-search-list .tnxbsv-cascader-node {
429
+ white-space: nowrap;
430
+ overflow: hidden;
431
+ text-overflow: ellipsis;
432
+ }
433
+
434
+ .tnxbsv-cascader-search-list::-webkit-scrollbar {
435
+ width: 6px;
436
+ }
437
+
438
+ .tnxbsv-cascader-search-list::-webkit-scrollbar-track {
439
+ background: transparent;
440
+ border: none;
441
+ }
442
+
443
+ .tnxbsv-cascader-search-list::-webkit-scrollbar-thumb {
444
+ background-color: rgba(var(--bs-secondary-rgb), 0.3);
445
+ border-radius: 3px;
446
+ border: 1px solid transparent;
447
+ background-clip: padding-box;
448
+ }
449
+
450
+ .tnxbsv-cascader-search-list::-webkit-scrollbar-thumb:hover {
451
+ background-color: rgba(var(--bs-secondary-rgb), 0.5);
452
+ }
453
+
454
+ .tnxbsv-cascader-node {
455
+ display: flex;
456
+ align-items: center;
457
+ justify-content: space-between;
458
+ border: none;
459
+ border-radius: 0;
460
+ }
461
+
462
+ .tnxbsv-cascader-node:hover {
463
+ background-color: var(--bs-tertiary-bg);
464
+ }
465
+
466
+ .tnxbsv-cascader-node.active {
467
+ color: var(--bs-primary);
468
+ background-color: var(--bs-tertiary-bg);
469
+ }
470
+
471
+ .tnxbsv-cascader-node.is-disabled {
472
+ color: var(--bs-secondary-color);
473
+ cursor: not-allowed;
474
+ }
475
+
476
+ /* 使用固定的 Bootstrap 断点值 */
477
+ @media (max-width: 576px) {
478
+ .tnxbsv-cascader-menu {
479
+ min-width: 8.75rem;
480
+ max-height: var(--tnxbsv-cascader-menu-max-height);
481
+ }
482
+
483
+ .tnxbsv-cascader-node {
484
+ padding: var(--bs-dropdown-item-padding-y) 0.5rem;
485
+ }
486
+
487
+ .tnxbsv-cascader-search-list {
488
+ min-width: 8.75rem;
489
+ }
490
+ }
491
+ </style>