vue2server7 7.0.42 → 7.0.44
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/frontEnd/src/components/OrgTreeSelect.vue +431 -0
- package/frontEnd/src/components/OrgTreeSelectCascade.vue +482 -0
- package/frontEnd/src/pages/OrgTreeSelectPage.vue +308 -0
- package/frontEnd/src/router/routes.js +10 -0
- package/ms-vscode-remote.remote-ssh-0.119.2025033120.vsix +0 -0
- package/package.json +1 -1
- package/test/13420256837985870.gif +0 -0
- package/test/13420256921603132.gif +0 -0
- package/test/13420256985163546.png +0 -0
- package/ms-vscode-remote.remote-ssh-0.120.0.vsix +0 -0
- package/ms-vscode-remote.remote-ssh-0.122.0.vsix +0 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="org-tree-select">
|
|
3
|
+
<el-input
|
|
4
|
+
v-model="displayValue"
|
|
5
|
+
:placeholder="placeholder"
|
|
6
|
+
:disabled="disabled"
|
|
7
|
+
readonly
|
|
8
|
+
class="org-tree-select__input"
|
|
9
|
+
@click="!disabled && openDialog()"
|
|
10
|
+
>
|
|
11
|
+
<template #suffix>
|
|
12
|
+
<el-icon class="org-tree-select__icon" @click="!disabled && openDialog()">
|
|
13
|
+
<Search />
|
|
14
|
+
</el-icon>
|
|
15
|
+
</template>
|
|
16
|
+
</el-input>
|
|
17
|
+
|
|
18
|
+
<el-dialog
|
|
19
|
+
v-model="dialogVisible"
|
|
20
|
+
:title="dialogTitle"
|
|
21
|
+
width="500px"
|
|
22
|
+
destroy-on-close
|
|
23
|
+
@opened="onDialogOpened"
|
|
24
|
+
>
|
|
25
|
+
<div class="org-tree-select__content">
|
|
26
|
+
<el-input
|
|
27
|
+
v-model="filterText"
|
|
28
|
+
placeholder="请输入关键字搜索"
|
|
29
|
+
clearable
|
|
30
|
+
class="org-tree-select__filter"
|
|
31
|
+
>
|
|
32
|
+
<template #prefix>
|
|
33
|
+
<el-icon><Search /></el-icon>
|
|
34
|
+
</template>
|
|
35
|
+
</el-input>
|
|
36
|
+
|
|
37
|
+
<el-scrollbar class="org-tree-select__tree-wrapper">
|
|
38
|
+
<el-tree
|
|
39
|
+
ref="treeRef"
|
|
40
|
+
:data="treeData"
|
|
41
|
+
:props="treeProps"
|
|
42
|
+
:node-key="nodeKey"
|
|
43
|
+
:highlight-current="true"
|
|
44
|
+
:expand-on-click-node="false"
|
|
45
|
+
:default-expanded-keys="defaultExpandedKeys"
|
|
46
|
+
:filter-node-method="filterNode"
|
|
47
|
+
:check-strictly="checkStrictly"
|
|
48
|
+
:show-checkbox="multiple"
|
|
49
|
+
@node-click="onNodeClick"
|
|
50
|
+
@check-change="onCheckChange"
|
|
51
|
+
>
|
|
52
|
+
<template #default="{ node, data }">
|
|
53
|
+
<span class="org-tree-select__node">
|
|
54
|
+
<el-icon v-if="data.icon" class="org-tree-select__node-icon">
|
|
55
|
+
<component :is="data.icon" />
|
|
56
|
+
</el-icon>
|
|
57
|
+
<span class="org-tree-select__node-label">{{ node.label }}</span>
|
|
58
|
+
</span>
|
|
59
|
+
</template>
|
|
60
|
+
</el-tree>
|
|
61
|
+
</el-scrollbar>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<template #footer>
|
|
65
|
+
<el-button @click="dialogVisible = false">关闭</el-button>
|
|
66
|
+
<el-button type="primary" @click="onConfirm">确认</el-button>
|
|
67
|
+
</template>
|
|
68
|
+
</el-dialog>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
71
|
+
|
|
72
|
+
<script setup lang="ts">
|
|
73
|
+
import { ref, watch, computed, nextTick } from 'vue'
|
|
74
|
+
import { Search } from '@element-plus/icons-vue'
|
|
75
|
+
import type { ElTree, TreeNodeData } from 'element-plus'
|
|
76
|
+
|
|
77
|
+
/** 机构树节点数据类型 */
|
|
78
|
+
export interface OrgTreeNode {
|
|
79
|
+
/** 节点唯一标识 */
|
|
80
|
+
id: string | number
|
|
81
|
+
/** 节点显示名称 */
|
|
82
|
+
name: string
|
|
83
|
+
/** 节点编码(用于显示 code-name 格式) */
|
|
84
|
+
code?: string
|
|
85
|
+
/** 子节点数组 */
|
|
86
|
+
children?: OrgTreeNode[]
|
|
87
|
+
/** 节点图标(Element Plus 图标名称) */
|
|
88
|
+
icon?: string
|
|
89
|
+
/** 是否禁用该节点 */
|
|
90
|
+
disabled?: boolean
|
|
91
|
+
/** 扩展字段 */
|
|
92
|
+
[key: string]: unknown
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 单选时的值类型 */
|
|
96
|
+
export type OrgTreeSingleValue = string | number | null
|
|
97
|
+
|
|
98
|
+
/** 多选时的值类型 */
|
|
99
|
+
export type OrgTreeMultipleValue = (string | number)[]
|
|
100
|
+
|
|
101
|
+
/** 组件绑定的值类型 */
|
|
102
|
+
export type OrgTreeSelectValue = OrgTreeSingleValue | OrgTreeMultipleValue
|
|
103
|
+
|
|
104
|
+
/** 组件 Props 类型定义 */
|
|
105
|
+
interface OrgTreeSelectProps {
|
|
106
|
+
/** 绑定值 - 单选时为 string/number,多选时为数组 */
|
|
107
|
+
modelValue?: OrgTreeSelectValue
|
|
108
|
+
/** 树形数据 */
|
|
109
|
+
data?: OrgTreeNode[]
|
|
110
|
+
/** 输入框占位符 */
|
|
111
|
+
placeholder?: string
|
|
112
|
+
/** 弹窗标题 */
|
|
113
|
+
dialogTitle?: string
|
|
114
|
+
/** 是否禁用 */
|
|
115
|
+
disabled?: boolean
|
|
116
|
+
/** 是否多选 */
|
|
117
|
+
multiple?: boolean
|
|
118
|
+
/** 节点唯一标识字段 */
|
|
119
|
+
nodeKey?: string
|
|
120
|
+
/** 显示字段 */
|
|
121
|
+
labelKey?: string
|
|
122
|
+
/** 子节点字段 */
|
|
123
|
+
childrenKey?: string
|
|
124
|
+
/** 父子节点是否严格独立(多选时有效) */
|
|
125
|
+
checkStrictly?: boolean
|
|
126
|
+
/** 默认展开的节点 */
|
|
127
|
+
defaultExpandedKeys?: (string | number)[]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 组件事件类型定义 */
|
|
131
|
+
interface OrgTreeSelectEmits {
|
|
132
|
+
(e: 'update:modelValue', value: OrgTreeSelectValue): void
|
|
133
|
+
(e: 'change', value: OrgTreeSelectValue, node?: OrgTreeNode | OrgTreeNode[]): void
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 组件暴露的方法类型 */
|
|
137
|
+
interface OrgTreeSelectExpose {
|
|
138
|
+
/** 打开弹窗 */
|
|
139
|
+
openDialog: () => void
|
|
140
|
+
/** 清空选择 */
|
|
141
|
+
clear: () => void
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const props = withDefaults(defineProps<OrgTreeSelectProps>(), {
|
|
145
|
+
modelValue: null,
|
|
146
|
+
data: () => [],
|
|
147
|
+
placeholder: '请选择',
|
|
148
|
+
dialogTitle: '查询机构树',
|
|
149
|
+
disabled: false,
|
|
150
|
+
multiple: false,
|
|
151
|
+
nodeKey: 'id',
|
|
152
|
+
labelKey: 'name',
|
|
153
|
+
childrenKey: 'children',
|
|
154
|
+
checkStrictly: false,
|
|
155
|
+
defaultExpandedKeys: () => []
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const emit = defineEmits<OrgTreeSelectEmits>()
|
|
159
|
+
|
|
160
|
+
/** 树组件引用 */
|
|
161
|
+
const treeRef = ref<InstanceType<typeof ElTree> | null>(null)
|
|
162
|
+
|
|
163
|
+
/** 弹窗显示状态 */
|
|
164
|
+
const dialogVisible = ref<boolean>(false)
|
|
165
|
+
|
|
166
|
+
/** 搜索过滤文本 */
|
|
167
|
+
const filterText = ref<string>('')
|
|
168
|
+
|
|
169
|
+
/** 当前选中的值 */
|
|
170
|
+
const selectedValue = ref<OrgTreeSelectValue>(null)
|
|
171
|
+
|
|
172
|
+
/** 当前选中的节点数据 */
|
|
173
|
+
const selectedNodes = ref<OrgTreeNode[]>([])
|
|
174
|
+
|
|
175
|
+
/** 树形配置 */
|
|
176
|
+
const treeProps = computed(() => ({
|
|
177
|
+
label: props.labelKey,
|
|
178
|
+
children: props.childrenKey,
|
|
179
|
+
disabled: 'disabled'
|
|
180
|
+
}))
|
|
181
|
+
|
|
182
|
+
/** 树形数据(添加 code 前缀) */
|
|
183
|
+
const treeData = computed<OrgTreeNode[]>(() => {
|
|
184
|
+
return formatTreeData(props.data)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
/** 输入框显示值 */
|
|
188
|
+
const displayValue = computed<string>(() => {
|
|
189
|
+
if (!selectedNodes.value.length) return ''
|
|
190
|
+
if (props.multiple) {
|
|
191
|
+
return selectedNodes.value.map(node => node.name).join(', ')
|
|
192
|
+
}
|
|
193
|
+
return selectedNodes.value[0]?.name || ''
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 格式化树数据:将 code 和 name 组合显示
|
|
198
|
+
* @param data - 原始树数据
|
|
199
|
+
* @returns 格式化后的树数据
|
|
200
|
+
*/
|
|
201
|
+
function formatTreeData(data: OrgTreeNode[]): OrgTreeNode[] {
|
|
202
|
+
return data.map((item): OrgTreeNode => {
|
|
203
|
+
const formatted: OrgTreeNode = {
|
|
204
|
+
...item,
|
|
205
|
+
[props.labelKey]: item.code ? `${item.code}-${item.name}` : item.name
|
|
206
|
+
}
|
|
207
|
+
if (item.children && item.children.length > 0) {
|
|
208
|
+
formatted.children = formatTreeData(item.children)
|
|
209
|
+
}
|
|
210
|
+
return formatted
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 查找节点
|
|
216
|
+
* @param data - 树数据
|
|
217
|
+
* @param targetIds - 目标节点 ID 数组
|
|
218
|
+
* @returns 找到的节点数组
|
|
219
|
+
*/
|
|
220
|
+
function findNodes(data: OrgTreeNode[], targetIds: (string | number)[]): OrgTreeNode[] {
|
|
221
|
+
const result: OrgTreeNode[] = []
|
|
222
|
+
for (const item of data) {
|
|
223
|
+
if (targetIds.includes(item.id)) {
|
|
224
|
+
result.push(item)
|
|
225
|
+
}
|
|
226
|
+
if (item.children) {
|
|
227
|
+
result.push(...findNodes(item.children, targetIds))
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return result
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 获取目标节点 ID 数组
|
|
235
|
+
* @returns 节点 ID 数组
|
|
236
|
+
*/
|
|
237
|
+
function getTargetIds(): (string | number)[] {
|
|
238
|
+
if (props.multiple) {
|
|
239
|
+
const value = props.modelValue as OrgTreeMultipleValue | null
|
|
240
|
+
return value ?? []
|
|
241
|
+
}
|
|
242
|
+
const value = props.modelValue as OrgTreeSingleValue
|
|
243
|
+
return value !== null ? [value] : []
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 更新选中的节点
|
|
248
|
+
*/
|
|
249
|
+
function updateSelectedNodes(): void {
|
|
250
|
+
if (!props.modelValue) {
|
|
251
|
+
selectedNodes.value = []
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const targetIds = getTargetIds()
|
|
256
|
+
selectedNodes.value = findNodes(props.data, targetIds)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 打开弹窗
|
|
261
|
+
*/
|
|
262
|
+
function openDialog(): void {
|
|
263
|
+
dialogVisible.value = true
|
|
264
|
+
selectedValue.value = props.modelValue
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 弹窗打开后的回调
|
|
269
|
+
*/
|
|
270
|
+
function onDialogOpened(): void {
|
|
271
|
+
nextTick(() => {
|
|
272
|
+
updateSelectedNodes()
|
|
273
|
+
if (!treeRef.value) return
|
|
274
|
+
|
|
275
|
+
if (props.multiple) {
|
|
276
|
+
const keys = Array.isArray(selectedValue.value) ? selectedValue.value : []
|
|
277
|
+
treeRef.value.setCheckedKeys(keys as (string | number)[])
|
|
278
|
+
} else if (selectedValue.value !== null) {
|
|
279
|
+
treeRef.value.setCurrentKey(selectedValue.value as string | number)
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 节点点击事件(单选模式)
|
|
286
|
+
* @param data - 点击的节点数据
|
|
287
|
+
*/
|
|
288
|
+
function onNodeClick(data: OrgTreeNode): void {
|
|
289
|
+
if (props.multiple) return
|
|
290
|
+
selectedValue.value = data.id
|
|
291
|
+
selectedNodes.value = [data]
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 勾选变化事件(多选模式)
|
|
296
|
+
* @param _data - 变化的节点数据
|
|
297
|
+
* @param _checked - 是否选中
|
|
298
|
+
*/
|
|
299
|
+
function onCheckChange(_data: OrgTreeNode, _checked: boolean): void {
|
|
300
|
+
if (!props.multiple || !treeRef.value) return
|
|
301
|
+
const checkedNodes = treeRef.value.getCheckedNodes(false, false) as OrgTreeNode[]
|
|
302
|
+
selectedNodes.value = checkedNodes
|
|
303
|
+
selectedValue.value = checkedNodes.map(node => node.id)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 确认选择
|
|
308
|
+
*/
|
|
309
|
+
function onConfirm(): void {
|
|
310
|
+
if (props.multiple) {
|
|
311
|
+
const checkedNodes = treeRef.value?.getCheckedNodes(false, false) as OrgTreeNode[] ?? []
|
|
312
|
+
const keys = checkedNodes.map(node => node.id)
|
|
313
|
+
selectedValue.value = keys
|
|
314
|
+
selectedNodes.value = checkedNodes
|
|
315
|
+
emit('update:modelValue', keys)
|
|
316
|
+
emit('change', keys, checkedNodes)
|
|
317
|
+
} else {
|
|
318
|
+
const currentNode = treeRef.value?.getCurrentNode() as OrgTreeNode | null
|
|
319
|
+
if (currentNode) {
|
|
320
|
+
selectedValue.value = currentNode.id
|
|
321
|
+
selectedNodes.value = [currentNode]
|
|
322
|
+
emit('update:modelValue', currentNode.id)
|
|
323
|
+
emit('change', currentNode.id, currentNode)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
dialogVisible.value = false
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 过滤节点
|
|
331
|
+
* @param value - 搜索值
|
|
332
|
+
* @param data - 节点数据
|
|
333
|
+
* @returns 是否匹配
|
|
334
|
+
*/
|
|
335
|
+
function filterNode(value: string, data: TreeNodeData): boolean {
|
|
336
|
+
if (!value) return true
|
|
337
|
+
const label = (data[props.labelKey] as string) ?? ''
|
|
338
|
+
return label.toLowerCase().includes(value.toLowerCase())
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* 清空选择
|
|
343
|
+
*/
|
|
344
|
+
function clear(): void {
|
|
345
|
+
selectedValue.value = null
|
|
346
|
+
selectedNodes.value = []
|
|
347
|
+
emit('update:modelValue', null)
|
|
348
|
+
emit('change', null)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 监听过滤文本变化
|
|
352
|
+
watch(filterText, (val: string) => {
|
|
353
|
+
treeRef.value?.filter(val)
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
// 监听外部值变化
|
|
357
|
+
watch(() => props.modelValue, (val: OrgTreeSelectValue) => {
|
|
358
|
+
selectedValue.value = val
|
|
359
|
+
updateSelectedNodes()
|
|
360
|
+
}, { immediate: true })
|
|
361
|
+
|
|
362
|
+
// 暴露方法
|
|
363
|
+
defineExpose<OrgTreeSelectExpose>({
|
|
364
|
+
openDialog,
|
|
365
|
+
clear
|
|
366
|
+
})
|
|
367
|
+
</script>
|
|
368
|
+
|
|
369
|
+
<style scoped>
|
|
370
|
+
.org-tree-select {
|
|
371
|
+
display: inline-block;
|
|
372
|
+
width: 100%;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.org-tree-select__input {
|
|
376
|
+
cursor: pointer;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.org-tree-select__input :deep(.el-input__wrapper) {
|
|
380
|
+
cursor: pointer;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.org-tree-select__icon {
|
|
384
|
+
cursor: pointer;
|
|
385
|
+
color: var(--el-text-color-placeholder);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.org-tree-select__icon:hover {
|
|
389
|
+
color: var(--el-color-primary);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.org-tree-select__content {
|
|
393
|
+
display: flex;
|
|
394
|
+
flex-direction: column;
|
|
395
|
+
gap: 12px;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.org-tree-select__filter {
|
|
399
|
+
width: 100%;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.org-tree-select__tree-wrapper {
|
|
403
|
+
max-height: 400px;
|
|
404
|
+
border: 1px solid var(--el-border-color-light);
|
|
405
|
+
border-radius: 4px;
|
|
406
|
+
padding: 8px;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.org-tree-select__node {
|
|
410
|
+
display: flex;
|
|
411
|
+
align-items: center;
|
|
412
|
+
gap: 4px;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.org-tree-select__node-icon {
|
|
416
|
+
color: var(--el-color-primary);
|
|
417
|
+
font-size: 16px;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.org-tree-select__node-label {
|
|
421
|
+
font-size: 14px;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
:deep(.el-tree-node__content) {
|
|
425
|
+
height: 32px;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
:deep(.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content) {
|
|
429
|
+
background-color: var(--el-color-primary-light-9);
|
|
430
|
+
}
|
|
431
|
+
</style>
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="org-tree-select">
|
|
3
|
+
<el-input
|
|
4
|
+
v-model="displayValue"
|
|
5
|
+
:placeholder="placeholder"
|
|
6
|
+
:disabled="disabled"
|
|
7
|
+
readonly
|
|
8
|
+
class="org-tree-select__input"
|
|
9
|
+
@click="!disabled && openDialog()"
|
|
10
|
+
>
|
|
11
|
+
<template #suffix>
|
|
12
|
+
<el-icon class="org-tree-select__icon" @click="!disabled && openDialog()">
|
|
13
|
+
<Search />
|
|
14
|
+
</el-icon>
|
|
15
|
+
</template>
|
|
16
|
+
</el-input>
|
|
17
|
+
|
|
18
|
+
<el-dialog
|
|
19
|
+
v-model="dialogVisible"
|
|
20
|
+
:title="dialogTitle"
|
|
21
|
+
width="500px"
|
|
22
|
+
destroy-on-close
|
|
23
|
+
@opened="onDialogOpened"
|
|
24
|
+
>
|
|
25
|
+
<div class="org-tree-select__content">
|
|
26
|
+
<el-input
|
|
27
|
+
v-model="filterText"
|
|
28
|
+
placeholder="请输入关键字搜索"
|
|
29
|
+
clearable
|
|
30
|
+
class="org-tree-select__filter"
|
|
31
|
+
>
|
|
32
|
+
<template #prefix>
|
|
33
|
+
<el-icon><Search /></el-icon>
|
|
34
|
+
</template>
|
|
35
|
+
</el-input>
|
|
36
|
+
|
|
37
|
+
<el-scrollbar class="org-tree-select__tree-wrapper">
|
|
38
|
+
<el-tree
|
|
39
|
+
ref="treeRef"
|
|
40
|
+
:data="treeData"
|
|
41
|
+
:props="treeProps"
|
|
42
|
+
:node-key="nodeKey"
|
|
43
|
+
:highlight-current="true"
|
|
44
|
+
:expand-on-click-node="false"
|
|
45
|
+
:default-expanded-keys="defaultExpandedKeys"
|
|
46
|
+
:filter-node-method="filterNode"
|
|
47
|
+
:check-strictly="true"
|
|
48
|
+
:show-checkbox="multiple"
|
|
49
|
+
@check="onCheck"
|
|
50
|
+
@node-click="onNodeClick"
|
|
51
|
+
>
|
|
52
|
+
<template #default="{ node, data }">
|
|
53
|
+
<span class="org-tree-select__node">
|
|
54
|
+
<el-icon v-if="data.icon" class="org-tree-select__node-icon">
|
|
55
|
+
<component :is="data.icon" />
|
|
56
|
+
</el-icon>
|
|
57
|
+
<span class="org-tree-select__node-label">{{ node.label }}</span>
|
|
58
|
+
</span>
|
|
59
|
+
</template>
|
|
60
|
+
</el-tree>
|
|
61
|
+
</el-scrollbar>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<template #footer>
|
|
65
|
+
<el-button @click="dialogVisible = false">关闭</el-button>
|
|
66
|
+
<el-button type="primary" @click="onConfirm">确认</el-button>
|
|
67
|
+
</template>
|
|
68
|
+
</el-dialog>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
71
|
+
|
|
72
|
+
<script setup lang="ts">
|
|
73
|
+
import { ref, watch, computed, nextTick } from 'vue'
|
|
74
|
+
import { Search } from '@element-plus/icons-vue'
|
|
75
|
+
import type { ElTree, TreeNodeData } from 'element-plus'
|
|
76
|
+
|
|
77
|
+
/** 机构树节点数据类型 */
|
|
78
|
+
export interface OrgTreeNode {
|
|
79
|
+
/** 节点唯一标识 */
|
|
80
|
+
id: string | number
|
|
81
|
+
/** 节点显示名称 */
|
|
82
|
+
name: string
|
|
83
|
+
/** 节点编码(用于显示 code-name 格式) */
|
|
84
|
+
code?: string
|
|
85
|
+
/** 子节点数组 */
|
|
86
|
+
children?: OrgTreeNode[]
|
|
87
|
+
/** 节点图标(Element Plus 图标名称) */
|
|
88
|
+
icon?: string
|
|
89
|
+
/** 是否禁用该节点 */
|
|
90
|
+
disabled?: boolean
|
|
91
|
+
/** 扩展字段 */
|
|
92
|
+
[key: string]: unknown
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 单选时的值类型 */
|
|
96
|
+
export type OrgTreeSingleValue = string | number | null
|
|
97
|
+
|
|
98
|
+
/** 多选时的值类型 */
|
|
99
|
+
export type OrgTreeMultipleValue = (string | number)[]
|
|
100
|
+
|
|
101
|
+
/** 组件绑定的值类型 */
|
|
102
|
+
export type OrgTreeSelectValue = OrgTreeSingleValue | OrgTreeMultipleValue
|
|
103
|
+
|
|
104
|
+
/** 组件 Props 类型定义 */
|
|
105
|
+
interface OrgTreeSelectProps {
|
|
106
|
+
/** 绑定值 - 单选时为 string/number,多选时为数组 */
|
|
107
|
+
modelValue?: OrgTreeSelectValue
|
|
108
|
+
/** 树形数据 */
|
|
109
|
+
data?: OrgTreeNode[]
|
|
110
|
+
/** 输入框占位符 */
|
|
111
|
+
placeholder?: string
|
|
112
|
+
/** 弹窗标题 */
|
|
113
|
+
dialogTitle?: string
|
|
114
|
+
/** 是否禁用 */
|
|
115
|
+
disabled?: boolean
|
|
116
|
+
/** 是否多选 */
|
|
117
|
+
multiple?: boolean
|
|
118
|
+
/** 节点唯一标识字段 */
|
|
119
|
+
nodeKey?: string
|
|
120
|
+
/** 显示字段 */
|
|
121
|
+
labelKey?: string
|
|
122
|
+
/** 子节点字段 */
|
|
123
|
+
childrenKey?: string
|
|
124
|
+
/** 默认展开的节点 */
|
|
125
|
+
defaultExpandedKeys?: (string | number)[]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 组件事件类型定义 */
|
|
129
|
+
interface OrgTreeSelectEmits {
|
|
130
|
+
(e: 'update:modelValue', value: OrgTreeSelectValue): void
|
|
131
|
+
(e: 'change', value: OrgTreeSelectValue, node?: OrgTreeNode | OrgTreeNode[]): void
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 组件暴露的方法类型 */
|
|
135
|
+
interface OrgTreeSelectExpose {
|
|
136
|
+
/** 打开弹窗 */
|
|
137
|
+
openDialog: () => void
|
|
138
|
+
/** 清空选择 */
|
|
139
|
+
clear: () => void
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const props = withDefaults(defineProps<OrgTreeSelectProps>(), {
|
|
143
|
+
modelValue: null,
|
|
144
|
+
data: () => [],
|
|
145
|
+
placeholder: '请选择',
|
|
146
|
+
dialogTitle: '查询机构树',
|
|
147
|
+
disabled: false,
|
|
148
|
+
multiple: false,
|
|
149
|
+
nodeKey: 'id',
|
|
150
|
+
labelKey: 'name',
|
|
151
|
+
childrenKey: 'children',
|
|
152
|
+
defaultExpandedKeys: () => []
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const emit = defineEmits<OrgTreeSelectEmits>()
|
|
156
|
+
|
|
157
|
+
/** 树组件引用 */
|
|
158
|
+
const treeRef = ref<InstanceType<typeof ElTree> | null>(null)
|
|
159
|
+
|
|
160
|
+
/** 弹窗显示状态 */
|
|
161
|
+
const dialogVisible = ref<boolean>(false)
|
|
162
|
+
|
|
163
|
+
/** 搜索过滤文本 */
|
|
164
|
+
const filterText = ref<string>('')
|
|
165
|
+
|
|
166
|
+
/** 当前选中的值 */
|
|
167
|
+
const selectedValue = ref<OrgTreeSelectValue>(null)
|
|
168
|
+
|
|
169
|
+
/** 当前选中的节点数据 */
|
|
170
|
+
const selectedNodes = ref<OrgTreeNode[]>([])
|
|
171
|
+
|
|
172
|
+
/** 树形配置 */
|
|
173
|
+
const treeProps = computed(() => ({
|
|
174
|
+
label: props.labelKey,
|
|
175
|
+
children: props.childrenKey,
|
|
176
|
+
disabled: 'disabled'
|
|
177
|
+
}))
|
|
178
|
+
|
|
179
|
+
/** 树形数据(添加 code 前缀) */
|
|
180
|
+
const treeData = computed<OrgTreeNode[]>(() => {
|
|
181
|
+
return formatTreeData(props.data)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
/** 输入框显示值 */
|
|
185
|
+
const displayValue = computed<string>(() => {
|
|
186
|
+
if (!selectedNodes.value.length) return ''
|
|
187
|
+
if (props.multiple) {
|
|
188
|
+
return selectedNodes.value.map(node => node.name).join(', ')
|
|
189
|
+
}
|
|
190
|
+
return selectedNodes.value[0]?.name || ''
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 格式化树数据:将 code 和 name 组合显示
|
|
195
|
+
* @param data - 原始树数据
|
|
196
|
+
* @returns 格式化后的树数据
|
|
197
|
+
*/
|
|
198
|
+
function formatTreeData(data: OrgTreeNode[]): OrgTreeNode[] {
|
|
199
|
+
return data.map((item): OrgTreeNode => {
|
|
200
|
+
const formatted: OrgTreeNode = {
|
|
201
|
+
...item,
|
|
202
|
+
[props.labelKey]: item.code ? `${item.code}-${item.name}` : item.name
|
|
203
|
+
}
|
|
204
|
+
if (item.children && item.children.length > 0) {
|
|
205
|
+
formatted.children = formatTreeData(item.children)
|
|
206
|
+
}
|
|
207
|
+
return formatted
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 查找节点
|
|
213
|
+
* @param data - 树数据
|
|
214
|
+
* @param targetIds - 目标节点 ID 数组
|
|
215
|
+
* @returns 找到的节点数组
|
|
216
|
+
*/
|
|
217
|
+
function findNodes(data: OrgTreeNode[], targetIds: (string | number)[]): OrgTreeNode[] {
|
|
218
|
+
const result: OrgTreeNode[] = []
|
|
219
|
+
for (const item of data) {
|
|
220
|
+
if (targetIds.includes(item.id)) {
|
|
221
|
+
result.push(item)
|
|
222
|
+
}
|
|
223
|
+
if (item.children) {
|
|
224
|
+
result.push(...findNodes(item.children, targetIds))
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return result
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 获取目标节点 ID 数组
|
|
232
|
+
* @returns 节点 ID 数组
|
|
233
|
+
*/
|
|
234
|
+
function getTargetIds(): (string | number)[] {
|
|
235
|
+
if (props.multiple) {
|
|
236
|
+
const value = props.modelValue as OrgTreeMultipleValue | null
|
|
237
|
+
return value ?? []
|
|
238
|
+
}
|
|
239
|
+
const value = props.modelValue as OrgTreeSingleValue
|
|
240
|
+
return value !== null ? [value] : []
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 更新选中的节点
|
|
245
|
+
*/
|
|
246
|
+
function updateSelectedNodes(): void {
|
|
247
|
+
if (!props.modelValue) {
|
|
248
|
+
selectedNodes.value = []
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const targetIds = getTargetIds()
|
|
253
|
+
selectedNodes.value = findNodes(props.data, targetIds)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 打开弹窗
|
|
258
|
+
*/
|
|
259
|
+
function openDialog(): void {
|
|
260
|
+
dialogVisible.value = true
|
|
261
|
+
selectedValue.value = props.modelValue
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 弹窗打开后的回调
|
|
266
|
+
*/
|
|
267
|
+
function onDialogOpened(): void {
|
|
268
|
+
nextTick(() => {
|
|
269
|
+
updateSelectedNodes()
|
|
270
|
+
if (!treeRef.value) return
|
|
271
|
+
|
|
272
|
+
if (props.multiple) {
|
|
273
|
+
const keys = Array.isArray(selectedValue.value) ? selectedValue.value : []
|
|
274
|
+
treeRef.value.setCheckedKeys(keys as (string | number)[])
|
|
275
|
+
} else if (selectedValue.value !== null) {
|
|
276
|
+
treeRef.value.setCurrentKey(selectedValue.value as string | number)
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 节点点击事件(单选模式)
|
|
283
|
+
* @param data - 点击的节点数据
|
|
284
|
+
*/
|
|
285
|
+
function onNodeClick(data: OrgTreeNode): void {
|
|
286
|
+
if (props.multiple) return
|
|
287
|
+
selectedValue.value = data.id
|
|
288
|
+
selectedNodes.value = [data]
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 获取所有子节点 ID(递归)
|
|
293
|
+
* @param node - 节点数据
|
|
294
|
+
* @returns 子节点 ID 数组
|
|
295
|
+
*/
|
|
296
|
+
function getAllChildIds(node: OrgTreeNode): (string | number)[] {
|
|
297
|
+
const ids: (string | number)[] = []
|
|
298
|
+
if (node.children && node.children.length > 0) {
|
|
299
|
+
for (const child of node.children) {
|
|
300
|
+
ids.push(child.id)
|
|
301
|
+
ids.push(...getAllChildIds(child))
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return ids
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 判断节点是否有子节点
|
|
309
|
+
* @param data - 节点数据
|
|
310
|
+
* @returns 是否有子节点
|
|
311
|
+
*/
|
|
312
|
+
function hasChildren(data: OrgTreeNode): boolean {
|
|
313
|
+
return !!(data.children && data.children.length > 0)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* 勾选事件处理(多选模式)
|
|
318
|
+
* 规则:
|
|
319
|
+
* 1. 勾选父节点时,自动勾选所有子节点
|
|
320
|
+
* 2. 取消勾选父节点时,自动取消所有子节点
|
|
321
|
+
* 3. 勾选子节点时,不自动勾选父节点
|
|
322
|
+
* @param data - 当前勾选/取消勾选的节点数据
|
|
323
|
+
* @param checkedInfo - 勾选状态信息
|
|
324
|
+
*/
|
|
325
|
+
function onCheck(
|
|
326
|
+
data: OrgTreeNode,
|
|
327
|
+
checkedInfo: { checkedNodes: OrgTreeNode[]; checkedKeys: (string | number)[]; halfCheckedNodes: OrgTreeNode[]; halfCheckedKeys: (string | number)[] }
|
|
328
|
+
): void {
|
|
329
|
+
if (!props.multiple || !treeRef.value) return
|
|
330
|
+
|
|
331
|
+
const isChecked = checkedInfo.checkedKeys.includes(data.id)
|
|
332
|
+
|
|
333
|
+
// 如果是有子节点的节点(父节点)
|
|
334
|
+
if (hasChildren(data)) {
|
|
335
|
+
const childIds = getAllChildIds(data)
|
|
336
|
+
|
|
337
|
+
if (isChecked) {
|
|
338
|
+
// 勾选父节点:勾选所有子节点
|
|
339
|
+
const currentCheckedKeys = treeRef.value.getCheckedKeys() as (string | number)[]
|
|
340
|
+
const newCheckedKeys = [...new Set([...currentCheckedKeys, ...childIds])]
|
|
341
|
+
treeRef.value.setCheckedKeys(newCheckedKeys)
|
|
342
|
+
} else {
|
|
343
|
+
// 取消勾选父节点:取消所有子节点
|
|
344
|
+
const currentCheckedKeys = treeRef.value.getCheckedKeys() as (string | number)[]
|
|
345
|
+
const newCheckedKeys = currentCheckedKeys.filter(key => !childIds.includes(key))
|
|
346
|
+
treeRef.value.setCheckedKeys(newCheckedKeys)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// 如果是子节点,不做额外处理(不自动勾选父节点)
|
|
350
|
+
|
|
351
|
+
// 更新选中的节点数据
|
|
352
|
+
const checkedNodes = treeRef.value.getCheckedNodes(false, false) as OrgTreeNode[]
|
|
353
|
+
selectedNodes.value = checkedNodes
|
|
354
|
+
selectedValue.value = checkedNodes.map(node => node.id)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 确认选择
|
|
359
|
+
*/
|
|
360
|
+
function onConfirm(): void {
|
|
361
|
+
if (props.multiple) {
|
|
362
|
+
const checkedNodes = treeRef.value?.getCheckedNodes(false, false) as OrgTreeNode[] ?? []
|
|
363
|
+
const keys = checkedNodes.map(node => node.id)
|
|
364
|
+
selectedValue.value = keys
|
|
365
|
+
selectedNodes.value = checkedNodes
|
|
366
|
+
emit('update:modelValue', keys)
|
|
367
|
+
emit('change', keys, checkedNodes)
|
|
368
|
+
} else {
|
|
369
|
+
const currentNode = treeRef.value?.getCurrentNode() as OrgTreeNode | null
|
|
370
|
+
if (currentNode) {
|
|
371
|
+
selectedValue.value = currentNode.id
|
|
372
|
+
selectedNodes.value = [currentNode]
|
|
373
|
+
emit('update:modelValue', currentNode.id)
|
|
374
|
+
emit('change', currentNode.id, currentNode)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
dialogVisible.value = false
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* 过滤节点
|
|
382
|
+
* @param value - 搜索值
|
|
383
|
+
* @param data - 节点数据
|
|
384
|
+
* @returns 是否匹配
|
|
385
|
+
*/
|
|
386
|
+
function filterNode(value: string, data: TreeNodeData): boolean {
|
|
387
|
+
if (!value) return true
|
|
388
|
+
const label = (data[props.labelKey] as string) ?? ''
|
|
389
|
+
return label.toLowerCase().includes(value.toLowerCase())
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* 清空选择
|
|
394
|
+
*/
|
|
395
|
+
function clear(): void {
|
|
396
|
+
selectedValue.value = null
|
|
397
|
+
selectedNodes.value = []
|
|
398
|
+
emit('update:modelValue', null)
|
|
399
|
+
emit('change', null)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 监听过滤文本变化
|
|
403
|
+
watch(filterText, (val: string) => {
|
|
404
|
+
treeRef.value?.filter(val)
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
// 监听外部值变化
|
|
408
|
+
watch(() => props.modelValue, (val: OrgTreeSelectValue) => {
|
|
409
|
+
selectedValue.value = val
|
|
410
|
+
updateSelectedNodes()
|
|
411
|
+
}, { immediate: true })
|
|
412
|
+
|
|
413
|
+
// 暴露方法
|
|
414
|
+
defineExpose<OrgTreeSelectExpose>({
|
|
415
|
+
openDialog,
|
|
416
|
+
clear
|
|
417
|
+
})
|
|
418
|
+
</script>
|
|
419
|
+
|
|
420
|
+
<style scoped>
|
|
421
|
+
.org-tree-select {
|
|
422
|
+
display: inline-block;
|
|
423
|
+
width: 100%;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.org-tree-select__input {
|
|
427
|
+
cursor: pointer;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.org-tree-select__input :deep(.el-input__wrapper) {
|
|
431
|
+
cursor: pointer;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.org-tree-select__icon {
|
|
435
|
+
cursor: pointer;
|
|
436
|
+
color: var(--el-text-color-placeholder);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.org-tree-select__icon:hover {
|
|
440
|
+
color: var(--el-color-primary);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.org-tree-select__content {
|
|
444
|
+
display: flex;
|
|
445
|
+
flex-direction: column;
|
|
446
|
+
gap: 12px;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.org-tree-select__filter {
|
|
450
|
+
width: 100%;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.org-tree-select__tree-wrapper {
|
|
454
|
+
max-height: 400px;
|
|
455
|
+
border: 1px solid var(--el-border-color-light);
|
|
456
|
+
border-radius: 4px;
|
|
457
|
+
padding: 8px;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.org-tree-select__node {
|
|
461
|
+
display: flex;
|
|
462
|
+
align-items: center;
|
|
463
|
+
gap: 4px;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.org-tree-select__node-icon {
|
|
467
|
+
color: var(--el-color-primary);
|
|
468
|
+
font-size: 16px;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.org-tree-select__node-label {
|
|
472
|
+
font-size: 14px;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
:deep(.el-tree-node__content) {
|
|
476
|
+
height: 32px;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
:deep(.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content) {
|
|
480
|
+
background-color: var(--el-color-primary-light-9);
|
|
481
|
+
}
|
|
482
|
+
</style>
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="page org-tree-select-page">
|
|
3
|
+
<h1 class="title">机构树选择组件</h1>
|
|
4
|
+
<p class="desc">演示 <code>OrgTreeSelect</code> 的单选、多选、搜索等功能。</p>
|
|
5
|
+
|
|
6
|
+
<el-card class="demo-card" shadow="never">
|
|
7
|
+
<template #header>
|
|
8
|
+
<span>单选模式</span>
|
|
9
|
+
</template>
|
|
10
|
+
<OrgTreeSelect
|
|
11
|
+
v-model="singleValue"
|
|
12
|
+
:data="orgData"
|
|
13
|
+
placeholder="请选择机构"
|
|
14
|
+
class="demo-control"
|
|
15
|
+
@change="onSingleChange"
|
|
16
|
+
/>
|
|
17
|
+
<div class="demo-output">
|
|
18
|
+
当前值:<code>{{ singleValue ?? 'null' }}</code>
|
|
19
|
+
</div>
|
|
20
|
+
</el-card>
|
|
21
|
+
|
|
22
|
+
<el-card class="demo-card" shadow="never">
|
|
23
|
+
<template #header>
|
|
24
|
+
<span>多选模式 <code>multiple</code></span>
|
|
25
|
+
</template>
|
|
26
|
+
<OrgTreeSelect
|
|
27
|
+
v-model="multipleValue"
|
|
28
|
+
:data="orgData"
|
|
29
|
+
multiple
|
|
30
|
+
placeholder="请选择机构(可多选)"
|
|
31
|
+
class="demo-control"
|
|
32
|
+
@change="onMultipleChange"
|
|
33
|
+
/>
|
|
34
|
+
<div class="demo-output">
|
|
35
|
+
当前值:<code>{{ JSON.stringify(multipleValue) }}</code>
|
|
36
|
+
</div>
|
|
37
|
+
</el-card>
|
|
38
|
+
|
|
39
|
+
<el-card class="demo-card" shadow="never">
|
|
40
|
+
<template #header>
|
|
41
|
+
<span>禁用状态 <code>disabled</code></span>
|
|
42
|
+
</template>
|
|
43
|
+
<OrgTreeSelect
|
|
44
|
+
v-model="disabledValue"
|
|
45
|
+
:data="orgData"
|
|
46
|
+
disabled
|
|
47
|
+
placeholder="已禁用"
|
|
48
|
+
class="demo-control"
|
|
49
|
+
/>
|
|
50
|
+
</el-card>
|
|
51
|
+
|
|
52
|
+
<el-card class="demo-card" shadow="never">
|
|
53
|
+
<template #header>
|
|
54
|
+
<span>默认展开 <code>default-expanded-keys</code></span>
|
|
55
|
+
</template>
|
|
56
|
+
<OrgTreeSelect
|
|
57
|
+
v-model="expandedValue"
|
|
58
|
+
:data="orgData"
|
|
59
|
+
:default-expanded-keys="['25001', '25111']"
|
|
60
|
+
placeholder="默认展开指定节点"
|
|
61
|
+
class="demo-control"
|
|
62
|
+
/>
|
|
63
|
+
</el-card>
|
|
64
|
+
|
|
65
|
+
<el-card class="demo-card" shadow="never">
|
|
66
|
+
<template #header>
|
|
67
|
+
<span>级联多选模式 <code>OrgTreeSelectCascade</code> - 勾选父节点自动勾选子节点</span>
|
|
68
|
+
</template>
|
|
69
|
+
<OrgTreeSelectCascade
|
|
70
|
+
v-model="cascadeValue"
|
|
71
|
+
:data="orgData"
|
|
72
|
+
multiple
|
|
73
|
+
placeholder="请选择机构(级联多选)"
|
|
74
|
+
class="demo-control"
|
|
75
|
+
@change="onCascadeChange"
|
|
76
|
+
/>
|
|
77
|
+
<div class="demo-output">
|
|
78
|
+
当前值:<code>{{ JSON.stringify(cascadeValue) }}</code>
|
|
79
|
+
</div>
|
|
80
|
+
</el-card>
|
|
81
|
+
</section>
|
|
82
|
+
</template>
|
|
83
|
+
|
|
84
|
+
<script setup lang="ts">
|
|
85
|
+
import { ref } from 'vue'
|
|
86
|
+
import OrgTreeSelect, { type OrgTreeNode } from '../components/OrgTreeSelect.vue'
|
|
87
|
+
import OrgTreeSelectCascade from '../components/OrgTreeSelectCascade.vue'
|
|
88
|
+
|
|
89
|
+
// 机构树数据(模拟)
|
|
90
|
+
const orgData = ref<OrgTreeNode[]>([
|
|
91
|
+
{
|
|
92
|
+
id: '25001',
|
|
93
|
+
code: '25001',
|
|
94
|
+
name: '北京银行总行核算中心',
|
|
95
|
+
icon: 'OfficeBuilding',
|
|
96
|
+
children: [
|
|
97
|
+
{
|
|
98
|
+
id: '00019',
|
|
99
|
+
code: '00019',
|
|
100
|
+
name: '北京分行',
|
|
101
|
+
icon: 'OfficeBuilding',
|
|
102
|
+
children: []
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: '00023',
|
|
106
|
+
code: '00023',
|
|
107
|
+
name: '中关村分行',
|
|
108
|
+
icon: 'OfficeBuilding',
|
|
109
|
+
children: []
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: '00025',
|
|
113
|
+
code: '00025',
|
|
114
|
+
name: '城市副中心分行',
|
|
115
|
+
icon: 'OfficeBuilding',
|
|
116
|
+
children: []
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: '25101',
|
|
120
|
+
code: '25101',
|
|
121
|
+
name: '北京核算中心',
|
|
122
|
+
icon: 'OfficeBuilding',
|
|
123
|
+
children: []
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: '25111',
|
|
129
|
+
code: '25111',
|
|
130
|
+
name: '天津核算中心',
|
|
131
|
+
icon: 'OfficeBuilding',
|
|
132
|
+
children: [
|
|
133
|
+
{
|
|
134
|
+
id: '25311',
|
|
135
|
+
code: '25311',
|
|
136
|
+
name: '天津分行会计部',
|
|
137
|
+
icon: 'Document',
|
|
138
|
+
children: []
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: '30001',
|
|
142
|
+
code: '30001',
|
|
143
|
+
name: '天津营业部',
|
|
144
|
+
icon: 'Document',
|
|
145
|
+
children: []
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: '30011',
|
|
149
|
+
code: '30011',
|
|
150
|
+
name: '天津河西支行营业部',
|
|
151
|
+
icon: 'Document',
|
|
152
|
+
children: []
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: '30021',
|
|
156
|
+
code: '30021',
|
|
157
|
+
name: '天津开发区支行营业部',
|
|
158
|
+
icon: 'Document',
|
|
159
|
+
children: []
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: '30031',
|
|
163
|
+
code: '30031',
|
|
164
|
+
name: '天津南开支行营业部',
|
|
165
|
+
icon: 'Document',
|
|
166
|
+
children: []
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: '30041',
|
|
170
|
+
code: '30041',
|
|
171
|
+
name: '北京银行股份有限公司天津八纬路支行',
|
|
172
|
+
icon: 'Document',
|
|
173
|
+
children: []
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: '30051',
|
|
177
|
+
code: '30051',
|
|
178
|
+
name: '天津梅江支行营业部',
|
|
179
|
+
icon: 'Document',
|
|
180
|
+
children: []
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: '30061',
|
|
184
|
+
code: '30061',
|
|
185
|
+
name: '天津津南支行营业部',
|
|
186
|
+
icon: 'Document',
|
|
187
|
+
children: []
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: '30071',
|
|
191
|
+
code: '30071',
|
|
192
|
+
name: '天津河北支行营业部',
|
|
193
|
+
icon: 'Document',
|
|
194
|
+
children: []
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: '30081',
|
|
198
|
+
code: '30081',
|
|
199
|
+
name: '北京银行股份有限公司天津河东支行',
|
|
200
|
+
icon: 'Document',
|
|
201
|
+
children: []
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: '30091',
|
|
205
|
+
code: '30091',
|
|
206
|
+
name: '北京银行股份有限公司天津和平支行',
|
|
207
|
+
icon: 'Document',
|
|
208
|
+
children: []
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: '30101',
|
|
212
|
+
code: '30101',
|
|
213
|
+
name: '天津滨海支行营业部',
|
|
214
|
+
icon: 'Document',
|
|
215
|
+
children: []
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: '30111',
|
|
219
|
+
code: '30111',
|
|
220
|
+
name: '天津西青支行营业部',
|
|
221
|
+
icon: 'Document',
|
|
222
|
+
children: []
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: '30121',
|
|
226
|
+
code: '30121',
|
|
227
|
+
name: '北京银行股份有限公司天津科创支行',
|
|
228
|
+
icon: 'Document',
|
|
229
|
+
children: []
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: '30131',
|
|
233
|
+
code: '30131',
|
|
234
|
+
name: '北京银行股份有限公司天津北辰支行',
|
|
235
|
+
icon: 'Document',
|
|
236
|
+
children: []
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
id: '30141',
|
|
240
|
+
code: '30141',
|
|
241
|
+
name: '北京银行天津中新生态城绿色支行',
|
|
242
|
+
icon: 'Document',
|
|
243
|
+
children: []
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
])
|
|
248
|
+
|
|
249
|
+
const singleValue = ref<string | null>(null)
|
|
250
|
+
const multipleValue = ref<string[]>([])
|
|
251
|
+
const disabledValue = ref<string>('25001')
|
|
252
|
+
const expandedValue = ref<string | null>(null)
|
|
253
|
+
const cascadeValue = ref<string[]>([])
|
|
254
|
+
|
|
255
|
+
function onSingleChange(value: string | number | (string | number)[] | null, node?: OrgTreeNode | OrgTreeNode[]) {
|
|
256
|
+
console.log('单选变化:', value, node)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function onMultipleChange(value: string | number | (string | number)[] | null, nodes?: OrgTreeNode | OrgTreeNode[]) {
|
|
260
|
+
console.log('多选变化:', value, nodes)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function onCascadeChange(value: string | number | (string | number)[] | null, nodes?: OrgTreeNode | OrgTreeNode[]) {
|
|
264
|
+
console.log('级联多选变化:', value, nodes)
|
|
265
|
+
}
|
|
266
|
+
</script>
|
|
267
|
+
|
|
268
|
+
<style scoped>
|
|
269
|
+
.page.org-tree-select-page {
|
|
270
|
+
padding: 20px 24px;
|
|
271
|
+
max-width: 720px;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.title {
|
|
275
|
+
margin: 0 0 8px;
|
|
276
|
+
font-size: 20px;
|
|
277
|
+
font-weight: 600;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.desc {
|
|
281
|
+
margin: 0 0 20px;
|
|
282
|
+
color: var(--el-text-color-secondary);
|
|
283
|
+
font-size: 14px;
|
|
284
|
+
line-height: 1.5;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.demo-card {
|
|
288
|
+
margin-bottom: 16px;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.demo-card:last-child {
|
|
292
|
+
margin-bottom: 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.demo-control {
|
|
296
|
+
max-width: 400px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.demo-output {
|
|
300
|
+
margin-top: 12px;
|
|
301
|
+
font-size: 13px;
|
|
302
|
+
color: var(--el-text-color-regular);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.demo-output code {
|
|
306
|
+
font-size: 12px;
|
|
307
|
+
}
|
|
308
|
+
</style>
|
|
@@ -4,6 +4,7 @@ import ExportExcelPage from '../pages/ExportExcelPage.vue'
|
|
|
4
4
|
import ImportTablePage from '../pages/ImportTablePage.vue'
|
|
5
5
|
import PositionReportPage from '../pages/PositionReportPage.vue'
|
|
6
6
|
import DateRangePage from '../pages/DateRangePage.vue'
|
|
7
|
+
import OrgTreeSelectPage from '../pages/OrgTreeSelectPage.vue'
|
|
7
8
|
|
|
8
9
|
export const routes = [
|
|
9
10
|
{
|
|
@@ -63,5 +64,14 @@ export const routes = [
|
|
|
63
64
|
title: '日期区间',
|
|
64
65
|
showInMenu: true
|
|
65
66
|
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
path: '/org-tree-select',
|
|
70
|
+
name: 'OrgTreeSelect',
|
|
71
|
+
component: OrgTreeSelectPage,
|
|
72
|
+
meta: {
|
|
73
|
+
title: '机构树选择',
|
|
74
|
+
showInMenu: true
|
|
75
|
+
}
|
|
66
76
|
}
|
|
67
77
|
]
|
|
Binary file
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|