cy-element-ui 1.1.26 → 1.1.28

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.
@@ -1,290 +1,388 @@
1
- <template>
2
- <div class="cy-tree-select">
3
- <el-popover v-model="popoverOpen" placement="bottom-start" width="300" trigger="click" :popper-class="popperClass" @show="popoverOpen = true" @hide="popoverOpen = false">
4
- <div class="cy-tree-select-select-con">
5
- <div v-if="filterable" class="cy-tree-select-select-con-s">
6
- <el-input
7
- v-model="filterText"
8
- placeholder="搜索"
9
- prefix-icon="el-icon-search"
10
- clearable
11
- size="small"
12
- ></el-input>
13
- </div>
14
- <div v-if="showCheckStrictly" class="cy-tree-select-select-con-t">
15
- <el-checkbox v-model="isCheckStrictly">父子节点联动</el-checkbox>
16
- </div>
17
-
18
- <div class="cy-tree-select-select-con-c">
19
- <el-tree
20
- ref="tree"
21
- :data="treeOptions"
22
- :show-checkbox="multiple"
23
- :node-key="defaultOptions.id"
24
- :check-strictly="!isCheckStrictly"
25
- :props="defaultOptions"
26
- :expand-on-click-node="false"
27
- :default-expand-all="defaultExpandAll"
28
- :filter-node-method="filterNode"
29
- @check="treeCheck"
30
- @node-click="treeClick"
31
- ></el-tree>
32
- </div>
33
- </div>
34
-
35
- <div slot="reference" class="cy-tree-select-select" :class="size" :style="vStyle">
36
- <div class="cy-tree-select-select-v">
37
- <span v-if="checkedArr.length < 1" class="cy-tree-select-select-v-placeholder">{{ placeholder }}</span>
38
- <span v-else>{{ checkedArr.map(item => item[defaultOptions.label]).join() }}</span>
39
- </div>
40
-
41
- <div class="cy-tree-select-select-v-icon" @mouseenter="isClearIcon = true" @mouseleave="isClearIcon = false">
42
- <i v-if="isClearIcon && clearable" class="el-icon-circle-close" @click.stop="handleClear"></i>
43
- <i v-else class="el-icon-arrow-down cy-tree-select-select-v-arrow" :class="{ 'cy-tree-select-select-v-arrow-show': popoverOpen }"></i>
44
- </div>
45
- </div>
46
- </el-popover>
47
- </div>
48
- </template>
49
-
50
- <script>
51
- import ElCheckbox from '../../../checkbox';
52
- import ElPopover from '../../../popover';
53
- import ElInput from '../../../input';
54
- import ElTree from '../../../tree';
55
-
56
- export default {
57
- name: 'CyTreeSelect',
58
- components: {
59
- ElCheckbox,
60
- ElPopover,
61
- ElInput,
62
- ElTree
63
- },
64
- model: {
65
- prop: 'value',
66
- event: 'change'
67
- },
68
- props: {
69
- value: {
70
- type: [String, Number, Array],
71
- default: ''
72
- },
73
-
74
- options: {
75
- type: Array,
76
- default: []
77
- },
78
-
79
- placeholder: {
80
- type: String,
81
- default: ''
82
- },
83
-
84
- size: {
85
- type: String
86
- },
87
-
88
- filterable: {
89
- type: Boolean,
90
- default: false
91
- },
92
-
93
- showCheckStrictly: {
94
- type: Boolean,
95
- default: true
96
- },
97
-
98
- checkStrictly: {
99
- type: Boolean,
100
- default: true
101
- },
102
-
103
- clearable: {
104
- type: Boolean,
105
- default: false
106
- },
107
-
108
- multiple: {
109
- type: Boolean,
110
- default: false
111
- },
112
-
113
- defaultOptions: {
114
- type: Object,
115
- default: () => {
116
- return {
117
- children: 'children',
118
- label: 'label',
119
- id: 'id'
120
- };
121
- }
122
- },
123
-
124
- vStyle: {
125
- type: [Array, String, Object],
126
- default: ''
127
- },
128
-
129
- popperClass: {
130
- type: String,
131
- default: ''
132
- },
133
-
134
- defaultExpandAll: {
135
- type: Boolean,
136
- default: false
137
- }
138
- },
139
- data() {
140
- return {
141
- isCheckStrictly: true,
142
- popoverOpen: false,
143
- isClearIcon: false,
144
-
145
- treeOptions: [],
146
- treeArr: [],
147
- filterText: '',
148
- matchedNodeIds: []
149
- };
150
- },
151
- computed: {
152
- checkedArr() {
153
- if (this.multiple) {
154
- let arr = [];
155
- this.value && this.value.forEach(id => {
156
- let cItem = this.treeArr.filter(tItem => tItem[this.defaultOptions.id] === id)[0];
157
- if (cItem) arr.push(cItem);
158
- });
159
- return arr;
160
- } else {
161
- let cItem = this.treeArr.filter(tItem => tItem[this.defaultOptions.id] === Number(this.value))[0];
162
- return cItem ? [cItem] : [];
163
- }
164
- }
165
- },
166
- watch: {
167
- checkStrictly: {
168
- immediate: true,
169
- handler() {
170
- this.isCheckStrictly = this.checkStrictly;
171
- }
172
- },
173
-
174
- value: {
175
- immediate: true,
176
- deep: true,
177
- handler() {
178
- this.$nextTick(() => {
179
- this.init();
180
- });
181
- }
182
- },
183
-
184
- options: {
185
- deep: true,
186
- immediate: true,
187
- handler() {
188
- this.treeOptions = this.options;
189
- this.treeArr = this.turnFormatTreeData([], this.treeOptions, 0);
190
- }
191
- },
192
-
193
- filterText(val) {
194
- if (val) {
195
- this.matchedNodeIds = this.findMatchedNodes(val, this.treeOptions);
196
- this.$refs.tree.filter(val);
197
- this.$nextTick(() => {
198
- this.expandMatchedNodes();
199
- });
200
- } else {
201
- this.matchedNodeIds = [];
202
- this.$refs.tree.filter(val);
203
- }
204
- }
205
- },
206
- created() {
207
-
208
- },
209
- methods: {
210
- filterNode(value, data) {
211
- if (!value) return true;
212
- return this.matchedNodeIds.includes(data[this.defaultOptions.id]);
213
- },
214
-
215
- findMatchedNodes(value, nodes, parentMatched = false) {
216
- let matchedIds = [];
217
-
218
- nodes.forEach(node => {
219
- const currentNodeMatched = node[this.defaultOptions.label].indexOf(value) !== -1;
220
- const shouldShow = currentNodeMatched || parentMatched;
221
-
222
- if (shouldShow) {
223
- matchedIds.push(node[this.defaultOptions.id]);
224
- }
225
-
226
- if (node[this.defaultOptions.children] && node[this.defaultOptions.children].length > 0) {
227
- const childIds = this.findMatchedNodes(value, node[this.defaultOptions.children], shouldShow);
228
- matchedIds = matchedIds.concat(childIds);
229
- }
230
- });
231
-
232
- return matchedIds;
233
- },
234
-
235
- expandMatchedNodes() {
236
- this.matchedNodeIds.forEach(nodeId => {
237
- const treeNode = this.$refs.tree.store.nodesMap[nodeId];
238
- if (treeNode) {
239
- treeNode.expand();
240
- }
241
- });
242
- },
243
-
244
- init() {
245
- let arr = [];
246
- if (this.multiple) {
247
- arr = this.value ? this.value : [];
248
- } else {
249
- arr = this.value ? [this.value] : [];
250
- }
251
- this.$nextTick(() => {
252
- this.$refs.tree.setCheckedKeys(arr);
253
- });
254
- },
255
-
256
- treeCheck(value, { checkedKeys, checkedNodes }) {
257
- let arr = [];
258
- arr = checkedNodes;
259
- let idArr = arr.map(item => item[this.defaultOptions.id]);
260
- this.$emit('change', this.multiple ? idArr : idArr[0], this.multiple ? arr : arr[0]);
261
- },
262
-
263
- treeClick(value, node, event) {
264
- if (this.multiple) return;
265
-
266
- this.$emit('change', value.id, value);
267
- this.popoverOpen = false;
268
- },
269
-
270
- handleClear() {
271
- this.$emit('change', this.multiple ? [] : '', this.multiple ? [] : null);
272
- this.$refs.tree.setCheckedKeys([]);
273
- },
274
-
275
- turnFormatTreeData(arr, data, pId) {
276
- data.forEach(item => {
277
- let cItem = JSON.parse(JSON.stringify(item));
278
- delete cItem[this.defaultOptions.children];
279
- cItem.pId = pId;
280
- arr.push(cItem);
281
-
282
- if (item[this.defaultOptions.children] && item[this.defaultOptions.children].length > 0) {
283
- this.turnFormatTreeData(arr, item[this.defaultOptions.children], item[this.defaultOptions.id]);
284
- }
285
- });
286
- return arr;
287
- }
288
- }
289
- };
290
- </script>
1
+ <template>
2
+ <!-- 组件根容器 -->
3
+ <div class="cy-tree-select">
4
+ <!-- 弹出层:点击下方 reference 区域后展示树形选择面板 -->
5
+ <el-popover v-model="popoverOpen" placement="bottom-start" width="300" trigger="click" :popper-class="popperClass" @show="popoverOpen = true" @hide="popoverOpen = false">
6
+ <!-- 弹出层内容容器 -->
7
+ <div class="cy-tree-select-select-con">
8
+ <!-- 搜索过滤输入框(filterable 为 true 时显示) -->
9
+ <div v-if="filterable" class="cy-tree-select-select-con-s">
10
+ <el-input
11
+ v-model="filterText"
12
+ placeholder="搜索"
13
+ prefix-icon="el-icon-search"
14
+ clearable
15
+ size="small"
16
+ ></el-input>
17
+ </div>
18
+ <!-- 父子节点联动开关(多选且 showCheckStrictly 为 true 时显示) -->
19
+ <div v-if="showCheckStrictly && multiple" class="cy-tree-select-select-con-t">
20
+ <el-checkbox v-model="isCheckStrictly">父子节点联动</el-checkbox>
21
+ </div>
22
+
23
+ <!-- 树形选择器主体 -->
24
+ <div class="cy-tree-select-select-con-c">
25
+ <el-tree
26
+ ref="tree"
27
+ :data="treeOptions"
28
+ :show-checkbox="multiple"
29
+ :node-key="treeFieldMap.id"
30
+ :check-strictly="!isCheckStrictly"
31
+ :props="treeFieldMap"
32
+ :expand-on-click-node="false"
33
+ :default-expand-all="defaultExpandAll"
34
+ :filter-node-method="filterNode"
35
+ @check="treeCheck"
36
+ @node-click="treeClick"
37
+ ></el-tree>
38
+ </div>
39
+ </div>
40
+
41
+ <!-- 触发区域:页面上显示已选内容,点击后展开弹出层 -->
42
+ <div slot="reference" class="cy-tree-select-select" :class="size" :style="vStyle">
43
+ <div class="cy-tree-select-select-v">
44
+ <!-- 未选中时显示占位提示,选中后显示已选节点 label 拼接文本 -->
45
+ <span v-if="checkedArr.length < 1" class="cy-tree-select-select-v-placeholder">{{ placeholder }}</span>
46
+ <span v-else>{{ checkedArr.map(item => item[treeFieldMap.label]).join() }}</span>
47
+ </div>
48
+
49
+ <!-- 右侧图标区:hover 显示清空按钮,否则显示下拉箭头 -->
50
+ <div class="cy-tree-select-select-v-icon" @mouseenter="isClearIcon = true" @mouseleave="isClearIcon = false">
51
+ <i v-if="isClearIcon && clearable" class="el-icon-circle-close" @click.stop="handleClear"></i>
52
+ <i v-else class="el-icon-arrow-down cy-tree-select-select-v-arrow" :class="{ 'cy-tree-select-select-v-arrow-show': popoverOpen }"></i>
53
+ </div>
54
+ </div>
55
+ </el-popover>
56
+ </div>
57
+ </template>
58
+
59
+ <script>
60
+ // 引入 Element UI 底层组件,避免走全量注册
61
+ import ElCheckbox from '../../../checkbox';
62
+ import ElPopover from '../../../popover';
63
+ import ElInput from '../../../input';
64
+ import ElTree from '../../../tree';
65
+
66
+ export default {
67
+ name: 'CyTreeSelect',
68
+ components: {
69
+ ElCheckbox,
70
+ ElPopover,
71
+ ElInput,
72
+ ElTree
73
+ },
74
+ // 自定义 v-model:使用 value 作为 prop,change 作为回调事件
75
+ model: {
76
+ prop: 'value',
77
+ event: 'change'
78
+ },
79
+ props: {
80
+ // 绑定值:单选时为节点 id(String/Number),多选时为 id 数组
81
+ value: {
82
+ type: [String, Number, Array],
83
+ default: ''
84
+ },
85
+
86
+ // 树形数据源(children 嵌套结构)
87
+ options: {
88
+ type: Array,
89
+ default: []
90
+ },
91
+
92
+ // 未选中时的占位提示文本
93
+ placeholder: {
94
+ type: String,
95
+ default: ''
96
+ },
97
+
98
+ // 组件尺寸(medium / small / mini)
99
+ size: {
100
+ type: String
101
+ },
102
+
103
+ // 是否显示搜索过滤输入框
104
+ filterable: {
105
+ type: Boolean,
106
+ default: false
107
+ },
108
+
109
+ // 是否显示「父子节点联动」开关
110
+ showCheckStrictly: {
111
+ type: Boolean,
112
+ default: true
113
+ },
114
+
115
+ // 父子节点是否互相关联(默认联动)
116
+ checkStrictly: {
117
+ type: Boolean,
118
+ default: true
119
+ },
120
+
121
+ // 是否显示清空按钮
122
+ clearable: {
123
+ type: Boolean,
124
+ default: false
125
+ },
126
+
127
+ // 是否多选模式(多选时树节点带 checkbox)
128
+ multiple: {
129
+ type: Boolean,
130
+ default: false
131
+ },
132
+
133
+ // 树节点字段映射配置:指定 id / label / children 对应的字段名
134
+ defaultOptions: {
135
+ type: Object,
136
+ default: () => {
137
+ return {
138
+ children: 'children',
139
+ label: 'label',
140
+ id: 'id'
141
+ };
142
+ }
143
+ },
144
+
145
+ // 触发区域(reference)的自定义样式
146
+ vStyle: {
147
+ type: [Array, String, Object],
148
+ default: ''
149
+ },
150
+
151
+ // 弹出层自定义类名
152
+ popperClass: {
153
+ type: String,
154
+ default: ''
155
+ },
156
+
157
+ // 是否默认展开所有节点
158
+ defaultExpandAll: {
159
+ type: Boolean,
160
+ default: false
161
+ }
162
+ },
163
+ data() {
164
+ return {
165
+ // 当前是否父子联动(受 checkStrictly 控制)
166
+ isCheckStrictly: true,
167
+ // 弹出层是否展开
168
+ popoverOpen: false,
169
+ // 鼠标是否悬浮在图标区(控制清空/箭头图标切换)
170
+ isClearIcon: false,
171
+
172
+ // 原始树数据(来自 options)
173
+ treeOptions: [],
174
+ // 扁平化后的树数据(便于按 id 反查节点)
175
+ treeArr: [],
176
+ // 搜索关键字
177
+ filterText: '',
178
+ // 搜索命中的节点 id 集合
179
+ matchedNodeIds: []
180
+ };
181
+ },
182
+ computed: {
183
+ /**
184
+ * 字段映射配置:将用户传入的 defaultOptions 与默认值合并,
185
+ * 保证 children / label / id 三个 key 始终存在,
186
+ * 避免只传部分字段时取到的 key 为 undefined 而导致层级/选中/搜索失效
187
+ * @returns {Object} 合并后的字段映射
188
+ */
189
+ treeFieldMap() {
190
+ return Object.assign({ children: 'children', label: 'label', id: 'id' }, this.defaultOptions);
191
+ },
192
+
193
+ /**
194
+ * 根据 value 反查当前选中节点对象数组,用于回显文本
195
+ * @returns {Array} 选中节点对象数组
196
+ */
197
+ checkedArr() {
198
+ if (this.multiple) {
199
+ let arr = [];
200
+ this.value && this.value.forEach(id => {
201
+ let cItem = this.treeArr.filter(tItem => tItem[this.treeFieldMap.id] === id)[0];
202
+ if (cItem) arr.push(cItem);
203
+ });
204
+ return arr;
205
+ } else {
206
+ let cItem = this.treeArr.filter(tItem => tItem[this.treeFieldMap.id] === Number(this.value))[0];
207
+ return cItem ? [cItem] : [];
208
+ }
209
+ }
210
+ },
211
+ watch: {
212
+ // 监听 checkStrictly:同步到内部状态 isCheckStrictly
213
+ checkStrictly: {
214
+ immediate: true,
215
+ handler() {
216
+ this.isCheckStrictly = this.checkStrictly;
217
+ }
218
+ },
219
+
220
+ // 监听 value:变化时重新设置树节点的勾选状态
221
+ value: {
222
+ immediate: true,
223
+ deep: true,
224
+ handler() {
225
+ this.$nextTick(() => {
226
+ this.init();
227
+ });
228
+ }
229
+ },
230
+
231
+ // 监听 options:数据变化时重新扁平化
232
+ options: {
233
+ deep: true,
234
+ immediate: true,
235
+ handler() {
236
+ this.treeOptions = this.options;
237
+ this.treeArr = this.turnFormatTreeData([], this.treeOptions, 0);
238
+ }
239
+ },
240
+
241
+ // 监听搜索关键字:过滤树节点并自动展开命中节点
242
+ filterText(val) {
243
+ if (val) {
244
+ this.matchedNodeIds = this.findMatchedNodes(val, this.treeOptions);
245
+ this.$refs.tree.filter(val);
246
+ this.$nextTick(() => {
247
+ this.expandMatchedNodes();
248
+ });
249
+ } else {
250
+ this.matchedNodeIds = [];
251
+ this.$refs.tree.filter(val);
252
+ }
253
+ }
254
+ },
255
+ created() {
256
+
257
+ },
258
+ methods: {
259
+ /**
260
+ * 树节点过滤方法:根据命中 id 集合决定节点是否显示
261
+ * @param {*} value - 搜索关键字
262
+ * @param {Object} data - 当前树节点数据
263
+ * @returns {boolean}
264
+ */
265
+ filterNode(value, data) {
266
+ if (!value) return true;
267
+ return this.matchedNodeIds.includes(data[this.treeFieldMap.id]);
268
+ },
269
+
270
+ /**
271
+ * 递归查找名称包含关键字的节点 id(父节点命中则连带子节点)
272
+ * @param {*} value - 搜索关键字
273
+ * @param {Array} nodes - 待查找的树节点数组
274
+ * @param {boolean} [parentMatched=false] - 父节点是否已命中
275
+ * @returns {Array<number>} 命中节点 id 集合
276
+ */
277
+ findMatchedNodes(value, nodes, parentMatched = false) {
278
+ let matchedIds = [];
279
+
280
+ nodes.forEach(node => {
281
+ const currentNodeMatched = node[this.treeFieldMap.label].indexOf(value) !== -1;
282
+ const shouldShow = currentNodeMatched || parentMatched;
283
+
284
+ if (shouldShow) {
285
+ matchedIds.push(node[this.treeFieldMap.id]);
286
+ }
287
+
288
+ if (node[this.treeFieldMap.children] && node[this.treeFieldMap.children].length > 0) {
289
+ const childIds = this.findMatchedNodes(value, node[this.treeFieldMap.children], shouldShow);
290
+ matchedIds = matchedIds.concat(childIds);
291
+ }
292
+ });
293
+
294
+ return matchedIds;
295
+ },
296
+
297
+ /**
298
+ * 将搜索命中的节点自动展开
299
+ * @returns {void}
300
+ */
301
+ expandMatchedNodes() {
302
+ this.matchedNodeIds.forEach(nodeId => {
303
+ const treeNode = this.$refs.tree.store.nodesMap[nodeId];
304
+ if (treeNode) {
305
+ treeNode.expand();
306
+ }
307
+ });
308
+ },
309
+
310
+ /**
311
+ * 根据 value 初始化树节点勾选状态(单选/多选分别处理)
312
+ * @returns {void}
313
+ */
314
+ init() {
315
+ let arr = [];
316
+ if (this.multiple) {
317
+ arr = this.value ? this.value : [];
318
+ } else {
319
+ arr = this.value ? [this.value] : [];
320
+ }
321
+ this.$nextTick(() => {
322
+ this.$refs.tree.setCheckedKeys(arr);
323
+ });
324
+ },
325
+
326
+ /**
327
+ * 多选时勾选变化回调:向外 emit 选中 id 数组及节点数组
328
+ * @param {*} value - el-tree 回调的勾选节点数据
329
+ * @param checkedKeys
330
+ * @param {Object} checkedNodes - 选中节点对象数组
331
+ * @fires {Event} change
332
+ * @returns {void}
333
+ */
334
+ treeCheck(value, { checkedKeys, checkedNodes }) {
335
+ let arr = [];
336
+ arr = checkedNodes;
337
+ let idArr = arr.map(item => item[this.treeFieldMap.id]);
338
+ this.$emit('change', this.multiple ? idArr : idArr[0], this.multiple ? arr : arr[0]);
339
+ },
340
+
341
+ /**
342
+ * 单选时点击节点回调:直接选中并关闭弹出层
343
+ * @param {Object} value - 当前点击的树节点对象
344
+ * @param {Object} node - 节点对象
345
+ * @param {Object} event - 点击事件对象
346
+ * @fires {Event} change
347
+ * @returns {void}
348
+ */
349
+ treeClick(value, node, event) {
350
+ if (this.multiple) return;
351
+
352
+ this.$emit('change', value[this.treeFieldMap.id], value);
353
+ this.popoverOpen = false;
354
+ },
355
+
356
+ /**
357
+ * 清空选择:emit 空值并清空树勾选
358
+ * @fires {Event} change
359
+ * @returns {void}
360
+ */
361
+ handleClear() {
362
+ this.$emit('change', this.multiple ? [] : '', this.multiple ? [] : null);
363
+ this.$refs.tree.setCheckedKeys([]);
364
+ },
365
+
366
+ /**
367
+ * 将嵌套树数据拍平为一维数组,并补充父节点 id(pId)便于反查
368
+ * @param {Array} arr - 输出的一维数组(原地收集)
369
+ * @param {Array} data - 当前层级的树节点数组
370
+ * @param {number} pId - 父节点 id
371
+ * @returns {Array} 拍平后的一维节点数组
372
+ */
373
+ turnFormatTreeData(arr, data, pId) {
374
+ data.forEach(item => {
375
+ let cItem = JSON.parse(JSON.stringify(item));
376
+ delete cItem[this.treeFieldMap.children];
377
+ cItem.pId = pId;
378
+ arr.push(cItem);
379
+
380
+ if (item[this.treeFieldMap.children] && item[this.treeFieldMap.children].length > 0) {
381
+ this.turnFormatTreeData(arr, item[this.treeFieldMap.children], item[this.treeFieldMap.id]);
382
+ }
383
+ });
384
+ return arr;
385
+ }
386
+ }
387
+ };
388
+ </script>