jianghu-ui 1.0.1

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.
Files changed (112) hide show
  1. package/README.md +376 -0
  2. package/dist/jianghu-ui.css +2318 -0
  3. package/dist/jianghu-ui.js +2 -0
  4. package/dist/jianghu-ui.js.LICENSE.txt +1 -0
  5. package/package.json +56 -0
  6. package/src/Design.stories.mdx +195 -0
  7. package/src/Introduction.stories.mdx +148 -0
  8. package/src/components/JhAddressSelect/JhAddressSelect.md +250 -0
  9. package/src/components/JhAddressSelect/JhAddressSelect.stories.js +282 -0
  10. package/src/components/JhAddressSelect/JhAddressSelect.vue +261 -0
  11. package/src/components/JhCard/JhCard.md +246 -0
  12. package/src/components/JhCard/JhCard.stories.js +688 -0
  13. package/src/components/JhCard/JhCard.vue +604 -0
  14. package/src/components/JhCheckCard/JhCheckCard.md +245 -0
  15. package/src/components/JhCheckCard/JhCheckCard.stories.js +750 -0
  16. package/src/components/JhCheckCard/JhCheckCard.vue +476 -0
  17. package/src/components/JhConfirmDialog/JhConfirmDialog.md +70 -0
  18. package/src/components/JhConfirmDialog/JhConfirmDialog.stories.js +550 -0
  19. package/src/components/JhConfirmDialog/JhConfirmDialog.vue +181 -0
  20. package/src/components/JhDateRangePicker/JhDateRangePicker.md +56 -0
  21. package/src/components/JhDateRangePicker/JhDateRangePicker.stories.js +320 -0
  22. package/src/components/JhDateRangePicker/JhDateRangePicker.vue +307 -0
  23. package/src/components/JhDescriptions/JhDescriptions.md +724 -0
  24. package/src/components/JhDescriptions/JhDescriptions.stories.js +858 -0
  25. package/src/components/JhDescriptions/JhDescriptions.vue +933 -0
  26. package/src/components/JhDraggable/JhDraggable.md +66 -0
  27. package/src/components/JhDraggable/JhDraggable.stories.js +161 -0
  28. package/src/components/JhDraggable/JhDraggable.vue +254 -0
  29. package/src/components/JhDrawer/JhDrawer.md +68 -0
  30. package/src/components/JhDrawer/JhDrawer.stories.js +478 -0
  31. package/src/components/JhDrawer/JhDrawer.vue +281 -0
  32. package/src/components/JhDrawerForm/JhDrawerForm.md +69 -0
  33. package/src/components/JhDrawerForm/JhDrawerForm.stories.js +492 -0
  34. package/src/components/JhDrawerForm/JhDrawerForm.vue +297 -0
  35. package/src/components/JhEditableTable/JhEditableTable.md +507 -0
  36. package/src/components/JhEditableTable/JhEditableTable.stories.js +615 -0
  37. package/src/components/JhEditableTable/JhEditableTable.vue +685 -0
  38. package/src/components/JhFileInput/JhFileInput.md +56 -0
  39. package/src/components/JhFileInput/JhFileInput.stories.js +103 -0
  40. package/src/components/JhFileInput/JhFileInput.vue +253 -0
  41. package/src/components/JhForm/JhForm.md +676 -0
  42. package/src/components/JhForm/JhForm.stories.js +1375 -0
  43. package/src/components/JhForm/JhForm.vue +657 -0
  44. package/src/components/JhFormField/JhFormField.stories.js +217 -0
  45. package/src/components/JhFormField/JhFormField.vue +439 -0
  46. package/src/components/JhFormFields/JhFormFields.md +647 -0
  47. package/src/components/JhFormFields/JhFormFields.stories.js +922 -0
  48. package/src/components/JhFormFields/JhFormFields.vue +998 -0
  49. package/src/components/JhFormList/JhFormList.md +303 -0
  50. package/src/components/JhFormList/JhFormList.stories.js +661 -0
  51. package/src/components/JhFormList/JhFormList.vue +1127 -0
  52. package/src/components/JhJsonEditor/JhJsonEditor.md +54 -0
  53. package/src/components/JhJsonEditor/JhJsonEditor.stories.js +157 -0
  54. package/src/components/JhJsonEditor/JhJsonEditor.vue +178 -0
  55. package/src/components/JhLayout/JhLayout.md +580 -0
  56. package/src/components/JhLayout/JhLayout.stories.js +414 -0
  57. package/src/components/JhLayout/JhLayout.vue +387 -0
  58. package/src/components/JhList/JhList.md +441 -0
  59. package/src/components/JhList/JhList.stories.js +524 -0
  60. package/src/components/JhList/JhList.vue +571 -0
  61. package/src/components/JhMarkdownEditor/JhMarkdownEditor.md +56 -0
  62. package/src/components/JhMarkdownEditor/JhMarkdownEditor.stories.js +191 -0
  63. package/src/components/JhMarkdownEditor/JhMarkdownEditor.vue +188 -0
  64. package/src/components/JhMask/JhMask.md +62 -0
  65. package/src/components/JhMask/JhMask.stories.js +270 -0
  66. package/src/components/JhMask/JhMask.vue +123 -0
  67. package/src/components/JhMenu/JhMenu.md +85 -0
  68. package/src/components/JhMenu/JhMenu.stories.js +384 -0
  69. package/src/components/JhMenu/JhMenu.vue +545 -0
  70. package/src/components/JhModal/JhModal.md +68 -0
  71. package/src/components/JhModal/JhModal.stories.js +562 -0
  72. package/src/components/JhModal/JhModal.vue +235 -0
  73. package/src/components/JhModalForm/JhModalForm.md +69 -0
  74. package/src/components/JhModalForm/JhModalForm.stories.js +592 -0
  75. package/src/components/JhModalForm/JhModalForm.vue +298 -0
  76. package/src/components/JhPageContainer/JhPageContainer.md +409 -0
  77. package/src/components/JhPageContainer/JhPageContainer.stories.js +209 -0
  78. package/src/components/JhPageContainer/JhPageContainer.vue +72 -0
  79. package/src/components/JhQueryFilter/JhQueryFilter.md +77 -0
  80. package/src/components/JhQueryFilter/JhQueryFilter.stories.js +684 -0
  81. package/src/components/JhQueryFilter/JhQueryFilter.vue +429 -0
  82. package/src/components/JhScene/JhScene.md +64 -0
  83. package/src/components/JhScene/JhScene.stories.js +317 -0
  84. package/src/components/JhScene/JhScene.vue +376 -0
  85. package/src/components/JhStatisticCard/JhStatisticCard.md +363 -0
  86. package/src/components/JhStatisticCard/JhStatisticCard.stories.js +847 -0
  87. package/src/components/JhStatisticCard/JhStatisticCard.vue +459 -0
  88. package/src/components/JhStepsForm/JhStepsForm.md +666 -0
  89. package/src/components/JhStepsForm/JhStepsForm.stories.js +1224 -0
  90. package/src/components/JhStepsForm/JhStepsForm.vue +749 -0
  91. package/src/components/JhTable/JhTable.md +730 -0
  92. package/src/components/JhTable/JhTable.stories.js +1444 -0
  93. package/src/components/JhTable/JhTable.vue +2298 -0
  94. package/src/components/JhTableAttachment/JhTableAttachment.md +70 -0
  95. package/src/components/JhTableAttachment/JhTableAttachment.stories.js +198 -0
  96. package/src/components/JhTableAttachment/JhTableAttachment.vue +264 -0
  97. package/src/components/JhToast/JhToast.md +67 -0
  98. package/src/components/JhToast/JhToast.stories.js +386 -0
  99. package/src/components/JhToast/JhToast.vue +239 -0
  100. package/src/components/JhTreeSelect/JhTreeSelect.md +82 -0
  101. package/src/components/JhTreeSelect/JhTreeSelect.stories.js +391 -0
  102. package/src/components/JhTreeSelect/JhTreeSelect.vue +727 -0
  103. package/src/components/JhWaterMark/JhWaterMark.md +190 -0
  104. package/src/components/JhWaterMark/JhWaterMark.stories.js +675 -0
  105. package/src/components/JhWaterMark/JhWaterMark.vue +351 -0
  106. package/src/components/README.md +52 -0
  107. package/src/index.js +135 -0
  108. package/src/style/globalCSSJHV4.css +348 -0
  109. package/src/style/globalCSSVuetifyV4.css +637 -0
  110. package/src/style/storybook.css +4 -0
  111. package/src/tailwind.css +3 -0
  112. package/src/utils/vuetify.js +31 -0
@@ -0,0 +1,727 @@
1
+ <!--
2
+ JhTreeSelect - 树形选择组件
3
+
4
+ Props:
5
+ - value: v-model 绑定的已选节点列表 Array
6
+ - visible: 是否显示对话框 Boolean
7
+ - mode: 'multiple' | 'single' - 多选或单选模式
8
+ - title: 对话框标题 String
9
+ - placeholder: 搜索框占位符 String
10
+ - maxWidth: 对话框最大宽度 String
11
+ - data: 树形数据 Array<TreeNode>
12
+ - nodeKey: 节点唯一标识字段名 String (默认: 'id')
13
+ - nodeLabel: 节点显示文本字段名 String (默认: 'label')
14
+ - nodeChildren: 子节点字段名 String (默认: 'children')
15
+ - allowSelectNode: 是否允许选择节点本身 Boolean (默认: false)
16
+ - showSearch: 是否显示搜索框 Boolean (默认: true)
17
+ - showSelectAll: 是否显示全选按钮 Boolean (默认: true, 仅多选模式)
18
+
19
+ Events:
20
+ - input: v-model 更新事件
21
+ - update:visible: 显示状态更新事件
22
+ - confirm: 确认选择事件
23
+ - cancel: 取消选择事件
24
+
25
+ TreeNode 数据结构:
26
+ {
27
+ id: String, // 节点唯一标识
28
+ label: String, // 节点显示文本
29
+ children: Array, // 子节点数组
30
+ [其他自定义字段]
31
+ }
32
+
33
+ Usage:
34
+ <jh-tree-select
35
+ v-model="selectedNodes"
36
+ :visible.sync="isDialogShown"
37
+ :data="treeData"
38
+ mode="multiple"
39
+ title="选择节点"
40
+ @confirm="handleConfirm"
41
+ ></jh-tree-select>
42
+ -->
43
+
44
+ <template>
45
+ <v-dialog
46
+ :value="visible"
47
+ @input="handleDialogInput"
48
+ :max-width="maxWidth"
49
+ persistent
50
+ :fullscreen="isMobile"
51
+ >
52
+ <v-card>
53
+ <!-- 标题栏 -->
54
+ <v-card-title class="d-flex align-center pa-4">
55
+ <span>{{ title }}</span>
56
+ <v-spacer></v-spacer>
57
+ <v-icon @click="handleClose">mdi-close</v-icon>
58
+ </v-card-title>
59
+
60
+ <v-card-text class="pa-0">
61
+ <v-row no-gutters :style="contentStyle">
62
+ <!-- 左侧:选择区 -->
63
+ <v-col :cols="isMobile ? 12 : 7" class="border-right">
64
+ <!-- 搜索和工具栏 -->
65
+ <div class="px-3 pt-3" v-if="showSearch || (showSelectAll && mode === 'multiple')">
66
+ <v-text-field
67
+ v-if="showSearch"
68
+ v-model="searchKeyword"
69
+ :placeholder="placeholder"
70
+ prepend-inner-icon="mdi-magnify"
71
+ outlined
72
+ dense
73
+ hide-details
74
+ clearable
75
+ class="mb-3"
76
+ ></v-text-field>
77
+
78
+ <!-- 工具按钮 -->
79
+ <div class="mb-3 d-flex flex-wrap" v-if="showSelectAll && mode === 'multiple'">
80
+ <v-btn
81
+ small
82
+ outlined
83
+ :disabled="filteredAllNodes.length === 0"
84
+ @click="handleBatchSelect"
85
+ class="mr-2 mb-2"
86
+ >
87
+ <v-icon small left>mdi-checkbox-multiple-marked</v-icon>
88
+ {{ isAllSelected ? '取消全选' : '全选当前' }}
89
+ </v-btn>
90
+ </div>
91
+ </div>
92
+
93
+ <v-divider v-if="showSearch || (showSelectAll && mode === 'multiple')"></v-divider>
94
+
95
+ <!-- 内容区域 -->
96
+ <div :style="listStyle">
97
+ <div class="px-3 py-2" style="height: 100%; overflow-y: auto;">
98
+ <div v-if="loading" class="text-center py-8">
99
+ <v-progress-circular indeterminate color="primary"></v-progress-circular>
100
+ </div>
101
+
102
+ <div v-else-if="filteredTreeData.length === 0" class="text-center grey--text py-8">
103
+ <v-icon size="48" color="grey lighten-1">mdi-file-tree</v-icon>
104
+ <div class="mt-2">未找到匹配的节点</div>
105
+ </div>
106
+
107
+ <div v-else>
108
+ <!-- 树形结构 - 递归渲染 -->
109
+ <tree-node
110
+ v-for="node in filteredTreeData"
111
+ :key="getNodeKey(node)"
112
+ :node="node"
113
+ :level="0"
114
+ :mode="mode"
115
+ :allow-select-node="allowSelectNode"
116
+ :expandedNodes="expandedNodes"
117
+ :tempSelectedNodes="tempSelectedNodes"
118
+ :nodeKey="nodeKey"
119
+ :nodeLabel="nodeLabel"
120
+ :nodeChildren="nodeChildren"
121
+ :searchKeyword="searchKeyword"
122
+ :isNodeSelected="isNodeSelected"
123
+ :isNodeChildrenSelected="isNodeChildrenSelected"
124
+ @toggle-expand="toggleNodeExpand"
125
+ @toggle-node="toggleNode"
126
+ @select-single-node="selectSingleNode"
127
+ @node-click="handleNodeClick"
128
+ ></tree-node>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </v-col>
133
+
134
+ <!-- 右侧:已选区 -->
135
+ <v-col :cols="isMobile ? 12 : 5" v-if="mode === 'multiple' && !isMobile">
136
+ <div class="pa-3" style="height: 100%; display: flex; flex-direction: column;">
137
+ <div class="d-flex align-center mb-3">
138
+ <span class="text-subtitle-2">
139
+ 已选: {{ tempSelectedNodes.length }}
140
+ </span>
141
+ <v-spacer></v-spacer>
142
+ <v-btn text small color="primary" @click="clearSelection" :disabled="tempSelectedNodes.length === 0">
143
+ 清除
144
+ </v-btn>
145
+ </div>
146
+
147
+ <v-divider class="mb-3"></v-divider>
148
+
149
+ <div class="flex-grow-1" style="overflow-y: auto;">
150
+ <div v-if="tempSelectedNodes.length === 0" class="text-center py-8">
151
+ <v-icon size="64" color="grey lighten-2">mdi-checkbox-multiple-blank-outline</v-icon>
152
+ <div class="grey--text mt-2">暂无选中内容</div>
153
+ </div>
154
+
155
+ <div v-else>
156
+ <!-- 显示选中的节点 -->
157
+ <v-chip
158
+ v-for="node in tempSelectedNodes"
159
+ :key="getNodeKey(node)"
160
+ class="ma-1"
161
+ close
162
+ small
163
+ @click:close="removeNodeFromTemp(node)"
164
+ >
165
+ <v-icon left small>mdi-file-tree</v-icon>
166
+ {{ getNodeLabel(node) }}
167
+ </v-chip>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </v-col>
172
+ </v-row>
173
+ </v-card-text>
174
+
175
+ <v-divider></v-divider>
176
+
177
+ <!-- 底部操作栏 -->
178
+ <v-card-actions class="pa-4">
179
+ <div v-if="mode === 'multiple' && isMobile" class="text-caption">
180
+ 已选 {{ tempSelectedNodes.length }}
181
+ </div>
182
+ <v-spacer></v-spacer>
183
+ <v-btn text @click="handleCancel">取消</v-btn>
184
+ <v-btn
185
+ color="success"
186
+ @click="handleConfirm"
187
+ :disabled="mode === 'single' && tempSelectedNodes.length === 0"
188
+ >
189
+ 确定
190
+ </v-btn>
191
+ </v-card-actions>
192
+ </v-card>
193
+ </v-dialog>
194
+ </template>
195
+
196
+ <script>
197
+ // 递归树节点组件
198
+ const TreeNode = {
199
+ name: 'TreeNode',
200
+ template: `
201
+ <div class="mb-1">
202
+ <!-- 节点 -->
203
+ <div class="d-flex align-center py-2 tree-item" :style="{ paddingLeft: (level * 16) + 'px' }">
204
+ <v-checkbox
205
+ v-if="mode === 'multiple' && !allowSelectNode"
206
+ :input-value="isNodeChildrenSelected(node)"
207
+ @change="$emit('toggle-node', node)"
208
+ @click.stop
209
+ hide-details
210
+ dense
211
+ class="mr-2 mt-0"
212
+ ></v-checkbox>
213
+ <v-icon
214
+ class="mr-2"
215
+ small
216
+ @click="$emit('toggle-expand', getNodeKey(node))"
217
+ style="cursor: pointer;"
218
+ v-if="hasChildren(node)"
219
+ >
220
+ {{ expandedNodes.includes(getNodeKey(node)) ? 'mdi-chevron-down' : 'mdi-chevron-right' }}
221
+ </v-icon>
222
+ <span v-else class="mr-2" style="width: 20px;"></span>
223
+ <v-icon small class="mr-2" color="primary">mdi-file-tree</v-icon>
224
+ <div class="flex-grow-1" @click="$emit('toggle-expand', getNodeKey(node))" style="cursor: pointer;">
225
+ <div class="font-weight-medium">{{ getNodeLabel(node) }}</div>
226
+ <div class="text-caption grey--text" v-if="hasChildren(node)">
227
+ {{ getChildrenCount(node) }}个子节点
228
+ </div>
229
+ </div>
230
+ <!-- 选择节点按钮 -->
231
+ <v-btn
232
+ v-if="allowSelectNode && mode === 'multiple'"
233
+ x-small
234
+ outlined
235
+ color="primary"
236
+ @click.stop="$emit('toggle-node', node)"
237
+ class="ml-2"
238
+ >
239
+ <v-icon x-small left>mdi-check</v-icon>
240
+ {{ isNodeSelected(node) ? '已选' : '选择' }}
241
+ </v-btn>
242
+ <v-radio
243
+ v-else-if="allowSelectNode && mode === 'single'"
244
+ :value="getNodeKey(node)"
245
+ :input-value="selectedNodeKey"
246
+ @click.stop="handleRadioClick(node)"
247
+ hide-details
248
+ dense
249
+ class="ml-2 mt-0"
250
+ ></v-radio>
251
+ </div>
252
+
253
+ <!-- 展开内容:子节点 -->
254
+ <v-expand-transition>
255
+ <div v-show="expandedNodes.includes(getNodeKey(node))">
256
+ <!-- 子节点(递归) -->
257
+ <tree-node
258
+ v-for="child in getChildren(node)"
259
+ :key="getNodeKey(child)"
260
+ :node="child"
261
+ :level="level + 1"
262
+ :mode="mode"
263
+ :allow-select-node="allowSelectNode"
264
+ :expandedNodes="expandedNodes"
265
+ :tempSelectedNodes="tempSelectedNodes"
266
+ :nodeKey="nodeKey"
267
+ :nodeLabel="nodeLabel"
268
+ :nodeChildren="nodeChildren"
269
+ :searchKeyword="searchKeyword"
270
+ :isNodeSelected="isNodeSelected"
271
+ :isNodeChildrenSelected="isNodeChildrenSelected"
272
+ @toggle-expand="$emit('toggle-expand', $event)"
273
+ @toggle-node="$emit('toggle-node', $event)"
274
+ @select-single-node="$emit('select-single-node', $event)"
275
+ @node-click="$emit('node-click', $event)"
276
+ ></tree-node>
277
+ </div>
278
+ </v-expand-transition>
279
+ </div>
280
+ `,
281
+ props: {
282
+ // 当前节点对象
283
+ node: {
284
+ type: Object,
285
+ required: true
286
+ },
287
+ // 当前节点所属层级
288
+ level: {
289
+ type: Number,
290
+ default: 0
291
+ },
292
+ // 选择模式(single/multiple)
293
+ mode: String,
294
+ // 是否允许点击节点本身选择
295
+ allowSelectNode: Boolean,
296
+ // 展开的节点 key 列表
297
+ expandedNodes: Array,
298
+ // 临时选中节点集合
299
+ tempSelectedNodes: Array,
300
+ // 节点唯一字段名
301
+ nodeKey: String,
302
+ // 节点显示字段名
303
+ nodeLabel: String,
304
+ // 子节点字段名
305
+ nodeChildren: String,
306
+ // 当前搜索关键字
307
+ searchKeyword: String,
308
+ // 判断节点是否选中的函数
309
+ isNodeSelected: Function,
310
+ // 判断子节点是否全选的函数
311
+ isNodeChildrenSelected: Function
312
+ },
313
+ computed: {
314
+ selectedNodeKey() {
315
+ return this.tempSelectedNodes.length > 0 ? this.getNodeKey(this.tempSelectedNodes[0]) : null;
316
+ }
317
+ },
318
+ methods: {
319
+ getNodeKey(node) {
320
+ return node[this.nodeKey];
321
+ },
322
+ getNodeLabel(node) {
323
+ return node[this.nodeLabel] || '';
324
+ },
325
+ getChildren(node) {
326
+ const children = node[this.nodeChildren] || [];
327
+ if (!this.searchKeyword) {
328
+ return children;
329
+ }
330
+ const keyword = this.searchKeyword.toLowerCase();
331
+ return children.filter(child =>
332
+ this.getNodeLabel(child).toLowerCase().includes(keyword) ||
333
+ this.hasMatchingChildren(child, keyword)
334
+ );
335
+ },
336
+ hasMatchingChildren(node, keyword) {
337
+ const children = node[this.nodeChildren] || [];
338
+ return children.some(child =>
339
+ this.getNodeLabel(child).toLowerCase().includes(keyword) ||
340
+ this.hasMatchingChildren(child, keyword)
341
+ );
342
+ },
343
+ hasChildren(node) {
344
+ const children = node[this.nodeChildren] || [];
345
+ return children && children.length > 0;
346
+ },
347
+ getChildrenCount(node) {
348
+ const children = node[this.nodeChildren] || [];
349
+ return children ? children.length : 0;
350
+ },
351
+ handleRadioClick(node) {
352
+ this.$emit('select-single-node', node);
353
+ }
354
+ }
355
+ };
356
+
357
+ export default {
358
+ name: 'JhTreeSelect',
359
+ components: {
360
+ TreeNode
361
+ },
362
+ props: {
363
+ // 双向绑定的已选节点
364
+ value: {
365
+ type: Array,
366
+ default: () => []
367
+ },
368
+ // 弹窗显示状态
369
+ visible: {
370
+ type: Boolean,
371
+ default: false
372
+ },
373
+ // 选择模式(multiple | single)
374
+ mode: {
375
+ type: String,
376
+ default: 'multiple', // 'multiple' | 'single'
377
+ validator: value => ['multiple', 'single'].includes(value)
378
+ },
379
+ // 弹窗标题
380
+ title: {
381
+ type: String,
382
+ default: '选择节点'
383
+ },
384
+ // 搜索输入占位
385
+ placeholder: {
386
+ type: String,
387
+ default: '搜索节点'
388
+ },
389
+ // 弹窗最大宽度
390
+ maxWidth: {
391
+ type: String,
392
+ default: '1000px'
393
+ },
394
+ // 树型数据源
395
+ data: {
396
+ type: Array,
397
+ default: () => []
398
+ },
399
+ // 节点 key 字段名
400
+ nodeKey: {
401
+ type: String,
402
+ default: 'id'
403
+ },
404
+ // 节点 label 字段名
405
+ nodeLabel: {
406
+ type: String,
407
+ default: 'label'
408
+ },
409
+ // 子节点字段名
410
+ nodeChildren: {
411
+ type: String,
412
+ default: 'children'
413
+ },
414
+ // 是否允许点击节点选择
415
+ allowSelectNode: {
416
+ type: Boolean,
417
+ default: false
418
+ },
419
+ // 是否展示搜索框
420
+ showSearch: {
421
+ type: Boolean,
422
+ default: true
423
+ },
424
+ // 是否展示全选按钮
425
+ showSelectAll: {
426
+ type: Boolean,
427
+ default: true
428
+ },
429
+ // 数据加载状态
430
+ loading: {
431
+ type: Boolean,
432
+ default: false
433
+ }
434
+ },
435
+ data() {
436
+ return {
437
+ isMobile: typeof window !== 'undefined' && window.innerWidth < 600,
438
+ currentTab: 0,
439
+ searchKeyword: '',
440
+ tempSelectedNodes: [],
441
+ expandedNodes: []
442
+ };
443
+ },
444
+ computed: {
445
+ contentStyle() {
446
+ return this.isMobile ? 'height: calc(100vh - 120px);' : 'height: 600px;';
447
+ },
448
+ listStyle() {
449
+ const baseHeight = this.showSearch || (this.showSelectAll && this.mode === 'multiple')
450
+ ? 'calc(100% - 140px)'
451
+ : 'calc(100% - 60px)';
452
+ return `height: ${baseHeight};`;
453
+ },
454
+ filteredTreeData() {
455
+ if (!this.searchKeyword) {
456
+ return this.data;
457
+ }
458
+ const keyword = this.searchKeyword.toLowerCase();
459
+ return this.filterTreeRecursive(this.data, keyword);
460
+ },
461
+ filteredAllNodes() {
462
+ // 扁平化所有节点(包括子节点)
463
+ const flatten = (nodes) => {
464
+ let result = [];
465
+ nodes.forEach(node => {
466
+ result.push(node);
467
+ const children = node[this.nodeChildren] || [];
468
+ if (children.length > 0) {
469
+ result = result.concat(flatten(children));
470
+ }
471
+ });
472
+ return result;
473
+ };
474
+ const allNodes = flatten(this.data);
475
+ if (!this.searchKeyword) {
476
+ return allNodes;
477
+ }
478
+ const keyword = this.searchKeyword.toLowerCase();
479
+ return allNodes.filter(node =>
480
+ this.getNodeLabel(node).toLowerCase().includes(keyword)
481
+ );
482
+ },
483
+ isAllSelected() {
484
+ if (this.filteredAllNodes.length === 0) return false;
485
+ return this.filteredAllNodes.every(node => this.isNodeSelected(node));
486
+ }
487
+ },
488
+ watch: {
489
+ visible: {
490
+ immediate: true,
491
+ handler(val) {
492
+ if (val) {
493
+ this.initDialog();
494
+ }
495
+ }
496
+ }
497
+ },
498
+ mounted() {
499
+ if (typeof window !== 'undefined') {
500
+ window.addEventListener('resize', this.handleResize);
501
+ }
502
+ },
503
+ beforeDestroy() {
504
+ if (typeof window !== 'undefined') {
505
+ window.removeEventListener('resize', this.handleResize);
506
+ }
507
+ },
508
+ methods: {
509
+ handleResize() {
510
+ this.isMobile = window.innerWidth < 600;
511
+ },
512
+
513
+ initDialog() {
514
+ // 初始化临时选择列表
515
+ this.tempSelectedNodes = [...(this.value || [])];
516
+ this.searchKeyword = '';
517
+ this.expandedNodes = [];
518
+
519
+ // 自动展开有选中节点的父级
520
+ this.autoExpandSelectedNodes();
521
+
522
+ // 默认展开第一层节点
523
+ if (this.data.length > 0) {
524
+ this.expandedNodes = this.data.slice(0, 3).map(node => this.getNodeKey(node));
525
+ }
526
+ },
527
+
528
+ autoExpandSelectedNodes() {
529
+ // 自动展开有选中节点的路径
530
+ const selectedKeys = new Set(this.tempSelectedNodes.map(node => this.getNodeKey(node)));
531
+
532
+ const findPath = (nodes, targetKey, path = []) => {
533
+ for (const node of nodes) {
534
+ const key = this.getNodeKey(node);
535
+ const currentPath = [...path, key];
536
+
537
+ if (key === targetKey) {
538
+ return currentPath;
539
+ }
540
+
541
+ const children = node[this.nodeChildren] || [];
542
+ if (children.length > 0) {
543
+ const found = findPath(children, targetKey, currentPath);
544
+ if (found) return found;
545
+ }
546
+ }
547
+ return null;
548
+ };
549
+
550
+ selectedKeys.forEach(key => {
551
+ const path = findPath(this.data, key);
552
+ if (path) {
553
+ path.forEach(p => this.expandedNodes.push(p));
554
+ }
555
+ });
556
+
557
+ // 去重
558
+ this.expandedNodes = [...new Set(this.expandedNodes)];
559
+ },
560
+
561
+ /**
562
+ * 递归过滤树形结构
563
+ */
564
+ filterTreeRecursive(tree, keyword) {
565
+ const result = [];
566
+ tree.forEach(node => {
567
+ const matches = this.getNodeLabel(node).toLowerCase().includes(keyword);
568
+ const children = node[this.nodeChildren] || [];
569
+ const filteredChildren = children.length > 0 ? this.filterTreeRecursive(children, keyword) : [];
570
+ const hasMatchingChildren = filteredChildren.length > 0;
571
+
572
+ if (matches || hasMatchingChildren) {
573
+ result.push({
574
+ ...node,
575
+ [this.nodeChildren]: filteredChildren
576
+ });
577
+ }
578
+ });
579
+ return result;
580
+ },
581
+
582
+ getNodeKey(node) {
583
+ return node[this.nodeKey];
584
+ },
585
+
586
+ getNodeLabel(node) {
587
+ return node[this.nodeLabel] || '';
588
+ },
589
+
590
+ isNodeSelected(node) {
591
+ const key = this.getNodeKey(node);
592
+ return this.tempSelectedNodes.some(n => this.getNodeKey(n) === key);
593
+ },
594
+
595
+ isNodeChildrenSelected(node) {
596
+ const children = node[this.nodeChildren] || [];
597
+ if (children.length === 0) return false;
598
+ return children.every(child => this.isNodeSelected(child));
599
+ },
600
+
601
+ toggleNodeExpand(nodeKey) {
602
+ const index = this.expandedNodes.indexOf(nodeKey);
603
+ if (index > -1) {
604
+ this.expandedNodes.splice(index, 1);
605
+ } else {
606
+ this.expandedNodes.push(nodeKey);
607
+ }
608
+ },
609
+
610
+ toggleNode(node) {
611
+ if (this.mode === 'single') {
612
+ this.selectSingleNode(node);
613
+ return;
614
+ }
615
+
616
+ const key = this.getNodeKey(node);
617
+ const index = this.tempSelectedNodes.findIndex(n => this.getNodeKey(n) === key);
618
+
619
+ if (index > -1) {
620
+ this.tempSelectedNodes.splice(index, 1);
621
+ } else {
622
+ this.tempSelectedNodes.push(node);
623
+ }
624
+ },
625
+
626
+ selectSingleNode(node) {
627
+ this.tempSelectedNodes = [node];
628
+ },
629
+
630
+ handleNodeClick(node) {
631
+ if (this.allowSelectNode) {
632
+ this.toggleNode(node);
633
+ } else {
634
+ // 如果不允许选择节点本身,点击时展开/折叠
635
+ this.toggleNodeExpand(this.getNodeKey(node));
636
+ }
637
+ },
638
+
639
+ handleBatchSelect() {
640
+ if (this.isAllSelected) {
641
+ // 取消全选
642
+ this.filteredAllNodes.forEach(node => {
643
+ const key = this.getNodeKey(node);
644
+ const index = this.tempSelectedNodes.findIndex(n => this.getNodeKey(n) === key);
645
+ if (index > -1) {
646
+ this.tempSelectedNodes.splice(index, 1);
647
+ }
648
+ });
649
+ } else {
650
+ // 全选
651
+ this.filteredAllNodes.forEach(node => {
652
+ if (!this.isNodeSelected(node)) {
653
+ this.tempSelectedNodes.push(node);
654
+ }
655
+ });
656
+ }
657
+ },
658
+
659
+ removeNodeFromTemp(node) {
660
+ const key = this.getNodeKey(node);
661
+ const index = this.tempSelectedNodes.findIndex(n => this.getNodeKey(n) === key);
662
+ if (index > -1) {
663
+ this.tempSelectedNodes.splice(index, 1);
664
+ }
665
+ },
666
+
667
+ clearSelection() {
668
+ this.tempSelectedNodes = [];
669
+ },
670
+
671
+ handleDialogInput(value) {
672
+ this.$emit('update:visible', value);
673
+ },
674
+
675
+ handleClose() {
676
+ this.handleCancel();
677
+ },
678
+
679
+ handleCancel() {
680
+ this.$emit('update:visible', false);
681
+ this.$emit('cancel');
682
+ },
683
+
684
+ handleConfirm() {
685
+ // 触发事件
686
+ this.$emit('input', this.tempSelectedNodes);
687
+ this.$emit('update:visible', false);
688
+ this.$emit('confirm', this.tempSelectedNodes);
689
+ }
690
+ }
691
+ };
692
+ </script>
693
+
694
+ <style scoped>
695
+ .border-right {
696
+ border-right: 1px solid #e0e0e0;
697
+ }
698
+
699
+ .tree-item {
700
+ cursor: pointer;
701
+ border-radius: 4px;
702
+ transition: background-color 0.2s;
703
+ }
704
+
705
+ .tree-item:hover {
706
+ background-color: #f5f5f5;
707
+ }
708
+
709
+ /* 滚动条样式 */
710
+ ::-webkit-scrollbar {
711
+ width: 6px;
712
+ height: 6px;
713
+ }
714
+
715
+ ::-webkit-scrollbar-track {
716
+ background: #f1f1f1;
717
+ }
718
+
719
+ ::-webkit-scrollbar-thumb {
720
+ background: #c1c1c1;
721
+ border-radius: 3px;
722
+ }
723
+
724
+ ::-webkit-scrollbar-thumb:hover {
725
+ background: #a8a8a8;
726
+ }
727
+ </style>