xt-element-ui 2.1.3 → 2.1.5

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.
@@ -0,0 +1,494 @@
1
+ <template>
2
+ <div class="xt-transfer-tree">
3
+ <div class="xt-transfer-tree__panel xt-transfer-tree__source">
4
+ <div class="xt-transfer-tree__header">
5
+ <span class="xt-transfer-tree__title">{{ leftTitle }}</span>
6
+ <span class="xt-transfer-tree__count">{{ leftCheckedCount }}/{{ leftTotalCount }}</span>
7
+ </div>
8
+ <div class="xt-transfer-tree__search" v-if="filterable">
9
+ <el-input
10
+ v-model="leftFilterText"
11
+ placeholder="搜索"
12
+ prefix-icon="el-icon-search"
13
+ size="small"
14
+ />
15
+ </div>
16
+ <div class="xt-transfer-tree__body">
17
+ <el-tree
18
+ ref="leftTree"
19
+ :data="sourceData"
20
+ :props="treeProps"
21
+ :default-expand-all="defaultExpandAll"
22
+ :filter-node-method="filterLeftNode"
23
+ :show-checkbox="showCheckbox"
24
+ :check-strictly="!cascade"
25
+ :default-checked-keys="leftCheckedKeys"
26
+ @check-change="handleLeftCheckChange"
27
+ @node-click="handleLeftNodeClick"
28
+ >
29
+ <span class="xt-transfer-tree__node" slot-scope="{ node, data }">
30
+ <span>{{ node.label }}</span>
31
+ <span v-if="data.children && data.children.length" class="xt-transfer-tree__node-count">
32
+ ({{ data.children.length }})
33
+ </span>
34
+ </span>
35
+ </el-tree>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="xt-transfer-tree__buttons">
40
+ <el-button
41
+ type="primary"
42
+ :disabled="!leftCheckedCount"
43
+ @click="transferToRight"
44
+ :size="buttonSize"
45
+ >
46
+ <i class="el-icon-arrow-right"></i>
47
+ </el-button>
48
+ <el-button
49
+ type="primary"
50
+ :disabled="!rightCheckedCount"
51
+ @click="transferToLeft"
52
+ :size="buttonSize"
53
+ >
54
+ <i class="el-icon-arrow-left"></i>
55
+ </el-button>
56
+ </div>
57
+
58
+ <div class="xt-transfer-tree__panel xt-transfer-tree__target">
59
+ <div class="xt-transfer-tree__header">
60
+ <span class="xt-transfer-tree__title">{{ rightTitle }}</span>
61
+ <span class="xt-transfer-tree__count">{{ rightCheckedCount }}/{{ rightTotalCount }}</span>
62
+ </div>
63
+ <div class="xt-transfer-tree__search" v-if="filterable">
64
+ <el-input
65
+ v-model="rightFilterText"
66
+ placeholder="搜索"
67
+ prefix-icon="el-icon-search"
68
+ size="small"
69
+ />
70
+ </div>
71
+ <div class="xt-transfer-tree__body">
72
+ <el-tree
73
+ ref="rightTree"
74
+ :data="targetData"
75
+ :props="treeProps"
76
+ :default-expand-all="defaultExpandAll"
77
+ :filter-node-method="filterRightNode"
78
+ :show-checkbox="showCheckbox"
79
+ :check-strictly="!cascade"
80
+ :default-checked-keys="rightCheckedKeys"
81
+ @check-change="handleRightCheckChange"
82
+ @node-click="handleRightNodeClick"
83
+ >
84
+ <span class="xt-transfer-tree__node" slot-scope="{ node, data }">
85
+ <span>{{ node.label }}</span>
86
+ <span v-if="data.children && data.children.length" class="xt-transfer-tree__node-count">
87
+ ({{ data.children.length }})
88
+ </span>
89
+ </span>
90
+ </el-tree>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </template>
95
+
96
+ <script>
97
+ export default {
98
+ name: 'XtTransferTree',
99
+
100
+ props: {
101
+ data: {
102
+ type: Array,
103
+ default: () => []
104
+ },
105
+ value: {
106
+ type: Array,
107
+ default: () => []
108
+ },
109
+ leftTitle: {
110
+ type: String,
111
+ default: '待选择'
112
+ },
113
+ rightTitle: {
114
+ type: String,
115
+ default: '已选择'
116
+ },
117
+ treeProps: {
118
+ type: Object,
119
+ default: () => ({
120
+ label: 'label',
121
+ children: 'children',
122
+ value: 'id'
123
+ })
124
+ },
125
+ defaultExpandAll: {
126
+ type: Boolean,
127
+ default: true
128
+ },
129
+ filterable: {
130
+ type: Boolean,
131
+ default: false
132
+ },
133
+ showCheckbox: {
134
+ type: Boolean,
135
+ default: true
136
+ },
137
+ cascade: {
138
+ type: Boolean,
139
+ default: true
140
+ },
141
+ transferMode: {
142
+ type: String,
143
+ default: 'single',
144
+ validator: (val) => ['single', 'multiple', 'parent-child'].includes(val)
145
+ },
146
+ buttonSize: {
147
+ type: String,
148
+ default: 'small'
149
+ }
150
+ },
151
+
152
+ data() {
153
+ return {
154
+ leftFilterText: '',
155
+ rightFilterText: '',
156
+ leftCheckedKeys: [],
157
+ rightCheckedKeys: []
158
+ }
159
+ },
160
+
161
+ computed: {
162
+ sourceData() {
163
+ const valueSet = new Set(this.value)
164
+ return this.filterTreeData(this.data, valueSet, true)
165
+ },
166
+
167
+ targetData() {
168
+ const valueSet = new Set(this.value)
169
+ return this.filterTreeData(this.data, valueSet, false)
170
+ },
171
+
172
+ leftTotalCount() {
173
+ return this.countTreeNodes(this.sourceData)
174
+ },
175
+
176
+ rightTotalCount() {
177
+ return this.countTreeNodes(this.targetData)
178
+ },
179
+
180
+ leftCheckedCount() {
181
+ return this.leftCheckedKeys.length
182
+ },
183
+
184
+ rightCheckedCount() {
185
+ return this.rightCheckedKeys.length
186
+ }
187
+ },
188
+
189
+ watch: {
190
+ leftFilterText(val) {
191
+ this.$refs.leftTree && this.$refs.leftTree.filter(val)
192
+ },
193
+ rightFilterText(val) {
194
+ this.$refs.rightTree && this.$refs.rightTree.filter(val)
195
+ }
196
+ },
197
+
198
+ methods: {
199
+ filterTreeData(data, valueSet, exclude) {
200
+ return data
201
+ .map(node => {
202
+ const isTarget = valueSet.has(node[this.treeProps.value])
203
+ if (exclude && isTarget) return null
204
+ if (!exclude && !isTarget) return null
205
+
206
+ const result = { ...node }
207
+ if (node.children && node.children.length) {
208
+ const filteredChildren = this.filterTreeData(node.children, valueSet, exclude)
209
+ if (filteredChildren.length) {
210
+ result.children = filteredChildren
211
+ } else {
212
+ delete result.children
213
+ }
214
+ }
215
+ return result
216
+ })
217
+ .filter(Boolean)
218
+ },
219
+
220
+ countTreeNodes(data) {
221
+ let count = 0
222
+ data.forEach(node => {
223
+ count++
224
+ if (node.children) {
225
+ count += this.countTreeNodes(node.children)
226
+ }
227
+ })
228
+ return count
229
+ },
230
+
231
+ getAllKeys(node, keys = []) {
232
+ keys.push(node[this.treeProps.value])
233
+ if (node.children) {
234
+ node.children.forEach(child => this.getAllKeys(child, keys))
235
+ }
236
+ return keys
237
+ },
238
+
239
+ getParentKeys(data, key, parentKeys = []) {
240
+ for (const node of data) {
241
+ if (node[this.treeProps.value] === key) {
242
+ return parentKeys
243
+ }
244
+ if (node.children) {
245
+ const result = this.getParentKeys(node.children, key, [...parentKeys, node[this.treeProps.value]])
246
+ if (result.length > 0) return result
247
+ }
248
+ }
249
+ return []
250
+ },
251
+
252
+ filterLeftNode(value, data) {
253
+ if (!value) return true
254
+ return String(data[this.treeProps.label]).toLowerCase().indexOf(value.toLowerCase()) !== -1
255
+ },
256
+
257
+ filterRightNode(value, data) {
258
+ if (!value) return true
259
+ return String(data[this.treeProps.label]).toLowerCase().indexOf(value.toLowerCase()) !== -1
260
+ },
261
+
262
+ handleLeftCheckChange(data, checked) {
263
+ const key = data[this.treeProps.value]
264
+ if (checked) {
265
+ if (!this.leftCheckedKeys.includes(key)) {
266
+ this.leftCheckedKeys.push(key)
267
+ }
268
+ if (this.cascade && data.children) {
269
+ data.children.forEach(child => {
270
+ const childKey = child[this.treeProps.value]
271
+ if (!this.leftCheckedKeys.includes(childKey)) {
272
+ this.leftCheckedKeys.push(childKey)
273
+ }
274
+ })
275
+ }
276
+ } else {
277
+ this.leftCheckedKeys = this.leftCheckedKeys.filter(k => k !== key)
278
+ if (this.cascade && data.children) {
279
+ data.children.forEach(child => {
280
+ this.leftCheckedKeys = this.leftCheckedKeys.filter(k => k !== child[this.treeProps.value])
281
+ })
282
+ }
283
+ }
284
+ },
285
+
286
+ handleRightCheckChange(data, checked) {
287
+ const key = data[this.treeProps.value]
288
+ if (checked) {
289
+ if (!this.rightCheckedKeys.includes(key)) {
290
+ this.rightCheckedKeys.push(key)
291
+ }
292
+ if (this.cascade && data.children) {
293
+ data.children.forEach(child => {
294
+ const childKey = child[this.treeProps.value]
295
+ if (!this.rightCheckedKeys.includes(childKey)) {
296
+ this.rightCheckedKeys.push(childKey)
297
+ }
298
+ })
299
+ }
300
+ } else {
301
+ this.rightCheckedKeys = this.rightCheckedKeys.filter(k => k !== key)
302
+ if (this.cascade && data.children) {
303
+ data.children.forEach(child => {
304
+ this.rightCheckedKeys = this.rightCheckedKeys.filter(k => k !== child[this.treeProps.value])
305
+ })
306
+ }
307
+ }
308
+ },
309
+
310
+ handleLeftNodeClick(data) {
311
+ if (!this.showCheckbox) {
312
+ const key = data[this.treeProps.value]
313
+ const keysToTransfer = this.getTransferKeys(data)
314
+ this.transferKeys(keysToTransfer, true)
315
+ }
316
+ },
317
+
318
+ handleRightNodeClick(data) {
319
+ if (!this.showCheckbox) {
320
+ const key = data[this.treeProps.value]
321
+ const keysToTransfer = this.getTransferKeys(data)
322
+ this.transferKeys(keysToTransfer, false)
323
+ }
324
+ },
325
+
326
+ getTransferKeys(data) {
327
+ const key = data[this.treeProps.value]
328
+ switch (this.transferMode) {
329
+ case 'parent-child':
330
+ return this.getAllKeys(data)
331
+ case 'multiple':
332
+ return [key]
333
+ default:
334
+ return [key]
335
+ }
336
+ },
337
+
338
+ transferToRight() {
339
+ const keys = [...this.leftCheckedKeys]
340
+ this.transferKeys(keys, true)
341
+ this.leftCheckedKeys = []
342
+ },
343
+
344
+ transferToLeft() {
345
+ const keys = [...this.rightCheckedKeys]
346
+ this.transferKeys(keys, false)
347
+ this.rightCheckedKeys = []
348
+ },
349
+
350
+ transferKeys(keys, toRight) {
351
+ const currentValue = [...this.value]
352
+ const newKeys = new Set(currentValue)
353
+
354
+ if (toRight) {
355
+ keys.forEach(k => newKeys.add(k))
356
+ if (this.cascade) {
357
+ keys.forEach(k => {
358
+ const parentKeys = this.getParentKeys(this.data, k)
359
+ parentKeys.forEach(pk => newKeys.add(pk))
360
+ })
361
+ }
362
+ } else {
363
+ keys.forEach(k => newKeys.delete(k))
364
+ if (this.cascade) {
365
+ keys.forEach(k => {
366
+ const allChildKeys = []
367
+ this.findAllChildKeys(this.data, k, allChildKeys)
368
+ allChildKeys.forEach(ck => newKeys.delete(ck))
369
+ })
370
+ }
371
+ }
372
+
373
+ const newValue = Array.from(newKeys)
374
+ this.$emit('update:value', newValue)
375
+ this.$emit('change', {
376
+ value: newValue,
377
+ addedKeys: toRight ? keys : [],
378
+ removedKeys: toRight ? [] : keys
379
+ })
380
+ },
381
+
382
+ findAllChildKeys(data, parentKey, childKeys) {
383
+ for (const node of data) {
384
+ if (node[this.treeProps.value] === parentKey && node.children) {
385
+ node.children.forEach(child => {
386
+ childKeys.push(child[this.treeProps.value])
387
+ if (child.children) {
388
+ this.findAllChildKeys([child], child[this.treeProps.value], childKeys)
389
+ }
390
+ })
391
+ }
392
+ if (node.children) {
393
+ this.findAllChildKeys(node.children, parentKey, childKeys)
394
+ }
395
+ }
396
+ },
397
+
398
+ clearSelection() {
399
+ this.leftCheckedKeys = []
400
+ this.rightCheckedKeys = []
401
+ }
402
+ }
403
+ }
404
+ </script>
405
+
406
+ <style lang="scss" scoped>
407
+ .xt-transfer-tree {
408
+ display: flex;
409
+ align-items: stretch;
410
+ width: 100%;
411
+ height: 100%;
412
+ }
413
+
414
+ .xt-transfer-tree__panel {
415
+ flex: 1;
416
+ display: flex;
417
+ flex-direction: column;
418
+ border: 1px solid #ebeef5;
419
+ border-radius: 4px;
420
+ overflow: hidden;
421
+ }
422
+
423
+ .xt-transfer-tree__source {
424
+ margin-right: 8px;
425
+ }
426
+
427
+ .xt-transfer-tree__target {
428
+ margin-left: 8px;
429
+ }
430
+
431
+ .xt-transfer-tree__header {
432
+ display: flex;
433
+ align-items: center;
434
+ justify-content: space-between;
435
+ padding: 12px;
436
+ background: #f5f7fa;
437
+ border-bottom: 1px solid #ebeef5;
438
+ }
439
+
440
+ .xt-transfer-tree__title {
441
+ font-size: 14px;
442
+ font-weight: 600;
443
+ color: #303133;
444
+ }
445
+
446
+ .xt-transfer-tree__count {
447
+ font-size: 12px;
448
+ color: #909399;
449
+ }
450
+
451
+ .xt-transfer-tree__search {
452
+ padding: 8px 12px;
453
+ border-bottom: 1px solid #ebeef5;
454
+ }
455
+
456
+ .xt-transfer-tree__body {
457
+ flex: 1;
458
+ overflow-y: auto;
459
+ padding: 8px;
460
+ }
461
+
462
+ .xt-transfer-tree__buttons {
463
+ display: flex;
464
+ flex-direction: column;
465
+ justify-content: center;
466
+ align-items: center;
467
+ padding: 8px;
468
+ gap: 8px;
469
+ }
470
+
471
+ .xt-transfer-tree__node {
472
+ display: flex;
473
+ align-items: center;
474
+ gap: 4px;
475
+ }
476
+
477
+ .xt-transfer-tree__node-count {
478
+ font-size: 12px;
479
+ color: #909399;
480
+ }
481
+
482
+ .xt-transfer-tree__body::-webkit-scrollbar {
483
+ width: 6px;
484
+ }
485
+
486
+ .xt-transfer-tree__body::-webkit-scrollbar-thumb {
487
+ background: #c1c1c1;
488
+ border-radius: 3px;
489
+ }
490
+
491
+ .xt-transfer-tree__body::-webkit-scrollbar-track {
492
+ background: transparent;
493
+ }
494
+ </style>
package/src/index.js CHANGED
@@ -46,6 +46,7 @@ import XtMulti from './components/xt-chart/XtMulti.vue'
46
46
  import XtPage from './components/xt-page'
47
47
  import XtSelectTree from './components/xt-select-tree'
48
48
  import XtUpload from './components/xt-upload'
49
+ import XtTransferTree from './components/xt-transfer-tree' // XtTransferTree 组件(树形穿梭框)
49
50
 
50
51
 
51
52
  const components = [
@@ -78,7 +79,8 @@ const components = [
78
79
  XtMulti,
79
80
  XtPage,
80
81
  XtSelectTree,
81
- XtUpload
82
+ XtUpload,
83
+ XtTransferTree
82
84
  ]
83
85
 
84
86
  const install = function (Vue, options = {}) {
@@ -156,7 +158,8 @@ export default {
156
158
  XtMulti,
157
159
  XtPage,
158
160
  XtSelectTree,
159
- XtUpload
161
+ XtUpload,
162
+ XtTransferTree
160
163
  }
161
164
 
162
165
  // XtChart 组件按需导出(使用时需自行安装 echarts 依赖)
@@ -11,76 +11,4 @@
11
11
  xtColorDanger: #{$xt-color-danger}; // 危险色 - Element UI 标准红色
12
12
  xtColorInfo: #{$xt-color-info}; // 信息色 - Element UI 标准灰色
13
13
 
14
- // 主色调浅色系列
15
- xtColorPrimaryLight3: #{$xt-color-primary-light-3}; // 主色浅色 30%
16
- xtColorPrimaryLight5: #{$xt-color-primary-light-5}; // 主色浅色 50%
17
- xtColorPrimaryLight7: #{$xt-color-primary-light-7}; // 主色浅色 70%
18
- xtColorPrimaryLight8: #{$xt-color-primary-light-8}; // 主色浅色 80%
19
- xtColorPrimaryLight9: #{$xt-color-primary-light-9}; // 主色浅色 90%
20
-
21
- // ===========================
22
- // 文字颜色
23
- // ===========================
24
- xtColorTextPrimary: #{$xt-color-text-primary}; // 主要文字颜色 - 最深
25
- xtColorTextRegular: #{$xt-color-text-regular}; // 常规文字颜色 - 中等
26
- xtColorTextSecondary: #{$xt-color-text-secondary}; // 次要文字颜色 - 较浅
27
- xtColorTextPlaceholder: #{$xt-color-text-placeholder}; // 占位符文字颜色
28
- xtColorTextDisabled: #{$xt-color-text-disabled}; // 禁用状态文字颜色
29
-
30
- // ===========================
31
- // 背景颜色
32
- // ===========================
33
- xtColorBgPrimary: #{$xt-color-bg-primary}; // 主背景色 - 白色
34
- xtColorBgSecondary: #{$xt-color-bg-secondary}; // 次要背景色 - 浅灰
35
- xtColorBgHover: #{$xt-color-bg-hover}; // 悬停背景色
36
- xtColorBgContainer: #{$xt-color-bg-container}; // 容器背景色
37
- xtColorBgOverlay: #{$xt-color-bg-overlay}; // 浮层背景色
38
-
39
- // ===========================
40
- // 边框颜色
41
- // ===========================
42
- xtColorBorder: #{$xt-color-border}; // 边框颜色 - 标准
43
- xtColorBorderLight: #{$xt-color-border-light}; // 边框浅色
44
- xtColorBorderLighter: #{$xt-color-border-lighter}; // 边框更浅色
45
- xtColorBorderExtraLight: #{$xt-color-border-extra-light}; // 边框极浅色
46
-
47
- // ===========================
48
- // 字体大小
49
- // ===========================
50
- xtFontSizeExtraLarge: #{$xt-font-size-extra-large}; // 特大字体
51
- xtFontSizeLarge: #{$xt-font-size-large}; // 大字体
52
- xtFontSizeMedium: #{$xt-font-size-medium}; // 中等字体
53
- xtFontSizeBase: #{$xt-font-size-small}; // 基准字体
54
- xtFontSizeSmall: #{$xt-font-size-mini}; // 小字体
55
- xtFontSizeExtraSmall: #{$xt-font-size-extra-small}; // 特小字体
56
-
57
- // ===========================
58
- // 间距
59
- // ===========================
60
- xtSpacingXs: #{$xt-spacing-xs}; // 最小间距
61
- xtSpacingSm: #{$xt-spacing-sm}; // 小间距
62
- xtSpacingMd: #{$xt-spacing-md}; // 中间距
63
- xtSpacingLg: #{$xt-spacing-lg}; // 大间距
64
- xtSpacingXl: #{$xt-spacing-xl}; // 特大间距
65
-
66
- // ===========================
67
- // 圆角
68
- // ===========================
69
- xtBorderRadiusBase: #{$xt-border-radius-base}; // 基准圆角
70
- xtBorderRadiusSmall: #{$xt-border-radius-small}; // 小圆角
71
- xtBorderRadiusRound: #{$xt-border-radius-round}; // 圆角(胶囊状)
72
- xtBorderRadiusCircle: #{$xt-border-radius-circle}; // 圆形
73
-
74
- // ===========================
75
- // 阴影
76
- // ===========================
77
- xtShadowSm: #{$xt-shadow-sm}; // 小阴影
78
- xtShadowMd: #{$xt-shadow-md}; // 中等阴影
79
- xtShadowLg: #{$xt-shadow-lg}; // 大阴影
80
-
81
- // ===========================
82
- // 过渡动画
83
- // ===========================
84
- xtTransitionDuration: #{$xt-transition-duration}; // 默认过渡时长
85
- xtTransitionDurationFast: #{$xt-transition-duration-fast}; // 快速过渡时长
86
14
  }