@truenewx/tnxvue3 3.0.4 → 3.0.6

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.4",
3
+ "version": "3.0.6",
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"
@@ -1,11 +1,11 @@
1
1
  <template>
2
- <BButton class="tnxbsv-button" :loading="loading" :disabled="loading">
2
+ <BButton class="tnxbsv-button" :variant="type || 'outline-secondary'" :loading="loading" :disabled="loading">
3
3
  <i class="me-1" :class="icon" v-if="icon"></i>
4
4
  <div>
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,12 +15,13 @@
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
+ type: String,
24
25
  icon: String,
25
26
  loading: Boolean,
26
27
  },
@@ -0,0 +1,451 @@
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: [],
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
+ options: {
144
+ deep: true,
145
+ handler(newOptions) {
146
+ this.optionsArray = [newOptions];
147
+ // 清空已选项,避免选项更新后出现无效选择
148
+ this.selections = [];
149
+ this.inputValue = '';
150
+ this.$emit('update:modelValue', '');
151
+ },
152
+ },
153
+ optionVisibleSize: {
154
+ immediate: true,
155
+ handler(value) {
156
+ const fullHeight = value * 2.5 + 1.25;
157
+ document.documentElement.style.setProperty('--tnxbsv-cascader-menu-max-height', `${fullHeight}rem`);
158
+ }
159
+ }
160
+ },
161
+ mounted() {
162
+ document.addEventListener('click', this.handleClickOutside);
163
+ },
164
+ beforeUnmount() {
165
+ document.removeEventListener('click', this.handleClickOutside);
166
+ },
167
+ methods: {
168
+ clearValue(e) {
169
+ e.stopPropagation();
170
+ this.selections = [];
171
+ this.optionsArray = [this.options];
172
+ this.$emit('update:modelValue', '');
173
+ this.inputValue = '';
174
+ },
175
+ handleFilter(value) {
176
+ if (!this.filterable) {
177
+ return;
178
+ }
179
+ this.filtering = !!value;
180
+ if (!value) {
181
+ this.filterResults = [];
182
+ this.inputValue = this.displayValue;
183
+ return;
184
+ }
185
+ this.filterResults = this.searchOptions(this.options, value);
186
+ },
187
+ handleFocus() {
188
+ if (this.filterable && !this.filtering) {
189
+ this.inputValue = '';
190
+ }
191
+ },
192
+ searchOptions(options, keyword, path = []) {
193
+ let results = [];
194
+ options.forEach(option => {
195
+ if (this.filterMethod(option, keyword)) {
196
+ results.push({
197
+ ...option,
198
+ path: [...path, option],
199
+ });
200
+ }
201
+ const children = option[this.props.children];
202
+ if (children) {
203
+ results = results.concat(
204
+ this.searchOptions(children, keyword, [...path, option])
205
+ );
206
+ }
207
+ });
208
+ return results;
209
+ },
210
+ getOptionPath(option) {
211
+ return option.path.map(item => item[this.props.label]).join(this.separator);
212
+ },
213
+ handleFilterOptionClick(option) {
214
+ this.selections = option.path;
215
+ this.visible = false;
216
+ this.filtering = false;
217
+ this.inputValue = this.displayValue;
218
+ this.$emit('update:modelValue', option[this.props.value]);
219
+ },
220
+ toggleDropdown() {
221
+ if (!this.disabled) {
222
+ this.visible = !this.visible;
223
+ if (this.visible) {
224
+ this.$nextTick(() => {
225
+ const dropdown = this.$el.querySelector('.tnxbsv-cascader-dropdown');
226
+ const input = this.$el.querySelector('.input-group');
227
+ if (dropdown && input) {
228
+ const rect = input.getBoundingClientRect();
229
+ dropdown.style.top = `${rect.bottom}px`;
230
+ dropdown.style.left = `${rect.left}px`;
231
+ // 移除固定宽度设置,让下拉菜单自动扩展
232
+ dropdown.style.zIndex = window.tnx.util.dom.minTopZIndex();
233
+ }
234
+ });
235
+ }
236
+ }
237
+ },
238
+ isOptionSelected(level, option) {
239
+ return this.selections[level] &&
240
+ this.selections[level][this.props.value] === option[this.props.value];
241
+ },
242
+ handleOptionClick(level, option) {
243
+ if (option[this.props.disabled]) {
244
+ return;
245
+ }
246
+
247
+ this.selections.splice(level);
248
+ this.optionsArray.splice(level + 1);
249
+ this.selections[level] = option;
250
+
251
+ const children = option[this.props.children];
252
+ if (children && children.length) {
253
+ this.optionsArray.push(children);
254
+ } else {
255
+ this.visible = false;
256
+ this.$emit('update:modelValue', option[this.props.value]);
257
+ }
258
+ },
259
+ handleClickOutside(e) {
260
+ if (!this.$el.contains(e.target)) {
261
+ this.visible = false;
262
+ if (this.selections.length > 0) {
263
+ const lastSelection = this.selections[this.selections.length - 1];
264
+ const hasChildren = lastSelection[this.props.children]?.length > 0;
265
+
266
+ if (!hasChildren || this.parentSelectable) {
267
+ // 如果是叶子节点,或者允许父节点选择,则发送最后选择的值
268
+ this.$emit('update:modelValue', lastSelection[this.props.value]);
269
+ }
270
+ // 不做任何清空操作,保持当前选择状态
271
+ }
272
+ }
273
+ }
274
+ },
275
+ };
276
+ </script>
277
+
278
+ <style>
279
+ .tnxbsv-cascader {
280
+ position: relative;
281
+ display: inline-block;
282
+ width: 100%;
283
+ }
284
+
285
+ .tnxbsv-cascader .input-group.active .display,
286
+ .tnxbsv-cascader .input-group.active .btn-append {
287
+ border-color: var(--bs-primary);
288
+ }
289
+
290
+ .tnxbsv-cascader .display {
291
+ border-right: none;
292
+ box-shadow: none;
293
+ text-overflow: ellipsis;
294
+ color: var(--bs-secondary-color);
295
+ padding-right: 0.5rem;
296
+ }
297
+
298
+ .tnxbsv-cascader .display:focus {
299
+ box-shadow: none;
300
+ }
301
+
302
+ .tnxbsv-cascader .btn-append,
303
+ .tnxbsv-cascader .btn-append:hover,
304
+ .tnxbsv-cascader .btn-append:active {
305
+ padding-left: 0.5rem;
306
+ border-color: var(--bs-border-color);
307
+ border-left: none;
308
+ background-color: transparent;
309
+ color: var(--bs-tertiary-color);
310
+ }
311
+
312
+ .tnxbsv-cascader .btn-append .bi-chevron-down {
313
+ display: inline-block;
314
+ transition: transform 0.3s ease;
315
+ }
316
+
317
+ .tnxbsv-cascader .btn-append .rotate {
318
+ transform: rotate(180deg);
319
+ }
320
+
321
+ .tnxbsv-cascader-dropdown {
322
+ position: fixed;
323
+ margin-top: 0.25rem;
324
+ background: var(--bs-body-bg);
325
+ border: var(--bs-border-width) solid var(--bs-border-color);
326
+ border-radius: var(--bs-border-radius);
327
+ box-shadow: var(--bs-dropdown-box-shadow);
328
+ }
329
+
330
+ .tnxbsv-cascader-menus {
331
+ display: flex;
332
+ min-width: 11.25rem;
333
+ overflow-x: auto;
334
+ -webkit-overflow-scrolling: touch;
335
+ scrollbar-width: none;
336
+ -ms-overflow-style: none;
337
+ }
338
+
339
+ .tnxbsv-cascader-menus::-webkit-scrollbar {
340
+ display: none;
341
+ }
342
+
343
+ .tnxbsv-cascader-menu {
344
+ min-width: 11.25rem;
345
+ border-right: var(--bs-border-width) solid var(--bs-border-color);
346
+ flex-shrink: 0;
347
+ border-radius: 0;
348
+ max-height: var(--tnxbsv-cascader-menu-max-height);
349
+ overflow-y: auto;
350
+ }
351
+
352
+ .tnxbsv-cascader-menu:first-child {
353
+ border-top-left-radius: var(--bs-border-radius);
354
+ border-bottom-left-radius: var(--bs-border-radius);
355
+ }
356
+
357
+ .tnxbsv-cascader-menu:last-child {
358
+ border-right: none;
359
+ }
360
+
361
+ .tnxbsv-cascader-menu::-webkit-scrollbar {
362
+ width: 6px;
363
+ }
364
+
365
+ .tnxbsv-cascader-menu::-webkit-scrollbar-track {
366
+ background: transparent;
367
+ border: none;
368
+ }
369
+
370
+ .tnxbsv-cascader-menu::-webkit-scrollbar-thumb {
371
+ background-color: rgba(var(--bs-secondary-rgb), 0.3);
372
+ border-radius: 3px;
373
+ border: 1px solid transparent;
374
+ background-clip: padding-box;
375
+ }
376
+
377
+ .tnxbsv-cascader-menu::-webkit-scrollbar-thumb:hover {
378
+ background-color: rgba(var(--bs-secondary-rgb), 0.5);
379
+ }
380
+
381
+ .tnxbsv-cascader-search-list {
382
+ min-width: 11.25rem;
383
+ max-height: var(--tnxbsv-cascader-menu-max-height);
384
+ overflow-y: auto;
385
+ border-radius: var(--bs-border-radius);
386
+ }
387
+
388
+ .tnxbsv-cascader-search-list .tnxbsv-cascader-node {
389
+ white-space: nowrap;
390
+ overflow: hidden;
391
+ text-overflow: ellipsis;
392
+ }
393
+
394
+ .tnxbsv-cascader-search-list::-webkit-scrollbar {
395
+ width: 6px;
396
+ }
397
+
398
+ .tnxbsv-cascader-search-list::-webkit-scrollbar-track {
399
+ background: transparent;
400
+ border: none;
401
+ }
402
+
403
+ .tnxbsv-cascader-search-list::-webkit-scrollbar-thumb {
404
+ background-color: rgba(var(--bs-secondary-rgb), 0.3);
405
+ border-radius: 3px;
406
+ border: 1px solid transparent;
407
+ background-clip: padding-box;
408
+ }
409
+
410
+ .tnxbsv-cascader-search-list::-webkit-scrollbar-thumb:hover {
411
+ background-color: rgba(var(--bs-secondary-rgb), 0.5);
412
+ }
413
+
414
+ .tnxbsv-cascader-node {
415
+ display: flex;
416
+ align-items: center;
417
+ justify-content: space-between;
418
+ border: none;
419
+ border-radius: 0;
420
+ }
421
+
422
+ .tnxbsv-cascader-node:hover {
423
+ background-color: var(--bs-tertiary-bg);
424
+ }
425
+
426
+ .tnxbsv-cascader-node.active {
427
+ color: var(--bs-primary);
428
+ background-color: var(--bs-tertiary-bg);
429
+ }
430
+
431
+ .tnxbsv-cascader-node.is-disabled {
432
+ color: var(--bs-secondary-color);
433
+ cursor: not-allowed;
434
+ }
435
+
436
+ /* 使用固定的 Bootstrap 断点值 */
437
+ @media (max-width: 576px) {
438
+ .tnxbsv-cascader-menu {
439
+ min-width: 8.75rem;
440
+ max-height: var(--tnxbsv-cascader-menu-max-height);
441
+ }
442
+
443
+ .tnxbsv-cascader-node {
444
+ padding: var(--bs-dropdown-item-padding-y) 0.5rem;
445
+ }
446
+
447
+ .tnxbsv-cascader-search-list {
448
+ min-width: 8.75rem;
449
+ }
450
+ }
451
+ </style>