vue2server7 7.0.43 → 7.0.45

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