vue2server7 7.0.43 → 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.
@@ -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
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue2server7",
3
- "version": "7.0.43",
3
+ "version": "7.0.44",
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