niuma-ui 1.0.0

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 (220) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +25 -0
  4. package/README.en.md +99 -0
  5. package/README.md +161 -0
  6. package/package.json +106 -0
  7. package/src/__tests__/RsBadge.spec.ts +40 -0
  8. package/src/__tests__/RsBreadcrumb.spec.ts +172 -0
  9. package/src/__tests__/RsButton.spec.ts +108 -0
  10. package/src/__tests__/RsCalendarGrid.spec.ts +82 -0
  11. package/src/__tests__/RsCard.spec.ts +97 -0
  12. package/src/__tests__/RsCheckbox.spec.ts +36 -0
  13. package/src/__tests__/RsCodeEditor.spec.ts +111 -0
  14. package/src/__tests__/RsConfirmDialog.spec.ts +158 -0
  15. package/src/__tests__/RsContainer.spec.ts +89 -0
  16. package/src/__tests__/RsContextMenu.spec.ts +118 -0
  17. package/src/__tests__/RsDatePicker.spec.ts +88 -0
  18. package/src/__tests__/RsDateTimePicker.spec.ts +69 -0
  19. package/src/__tests__/RsDialog.spec.ts +404 -0
  20. package/src/__tests__/RsDrawer.spec.ts +145 -0
  21. package/src/__tests__/RsDropdown.spec.ts +135 -0
  22. package/src/__tests__/RsEmpty.spec.ts +99 -0
  23. package/src/__tests__/RsForm.spec.ts +173 -0
  24. package/src/__tests__/RsIcon.spec.ts +244 -0
  25. package/src/__tests__/RsInput.spec.ts +269 -0
  26. package/src/__tests__/RsInputNumber.spec.ts +118 -0
  27. package/src/__tests__/RsLabel.spec.ts +106 -0
  28. package/src/__tests__/RsLink.spec.ts +49 -0
  29. package/src/__tests__/RsLoading.spec.ts +84 -0
  30. package/src/__tests__/RsMarkdown.spec.ts +143 -0
  31. package/src/__tests__/RsMenu.spec.ts +119 -0
  32. package/src/__tests__/RsNewComponents.spec.ts +17 -0
  33. package/src/__tests__/RsPagination.spec.ts +158 -0
  34. package/src/__tests__/RsPopover.spec.ts +126 -0
  35. package/src/__tests__/RsScrollbar.spec.ts +53 -0
  36. package/src/__tests__/RsSelect.spec.ts +291 -0
  37. package/src/__tests__/RsSidebar.spec.ts +119 -0
  38. package/src/__tests__/RsSplitPane.spec.ts +177 -0
  39. package/src/__tests__/RsStatCard.spec.ts +79 -0
  40. package/src/__tests__/RsSteps.spec.ts +125 -0
  41. package/src/__tests__/RsTable.perf.spec.ts +103 -0
  42. package/src/__tests__/RsTable.spec.ts +1135 -0
  43. package/src/__tests__/RsTabs.spec.ts +328 -0
  44. package/src/__tests__/RsTerminal.spec.ts +218 -0
  45. package/src/__tests__/RsTimePicker.spec.ts +77 -0
  46. package/src/__tests__/RsToaster.spec.ts +95 -0
  47. package/src/__tests__/RsToolbar.spec.ts +75 -0
  48. package/src/__tests__/RsTooltip.spec.ts +111 -0
  49. package/src/__tests__/RsTree.spec.ts +572 -0
  50. package/src/__tests__/RsUpload.spec.ts +160 -0
  51. package/src/__tests__/RsVirtualList.spec.ts +77 -0
  52. package/src/__tests__/date-picker-utils.spec.ts +70 -0
  53. package/src/__tests__/dialog-viewport.spec.ts +38 -0
  54. package/src/__tests__/iso-local-datetime.spec.ts +58 -0
  55. package/src/__tests__/rs-clipboard.spec.ts +142 -0
  56. package/src/__tests__/setup.ts +12 -0
  57. package/src/__tests__/split-pane-utils.spec.ts +136 -0
  58. package/src/__tests__/table-drag.spec.ts +35 -0
  59. package/src/__tests__/table-edit-utils.spec.ts +194 -0
  60. package/src/__tests__/table-utils.spec.ts +286 -0
  61. package/src/__tests__/theme.spec.ts +22 -0
  62. package/src/__tests__/tree-utils.spec.ts +234 -0
  63. package/src/__tests__/useTableCellTooltip.spec.ts +101 -0
  64. package/src/components/RsAvatar.vue +115 -0
  65. package/src/components/RsBadge.vue +59 -0
  66. package/src/components/RsBreadcrumb.vue +65 -0
  67. package/src/components/RsBreadcrumbItems.vue +36 -0
  68. package/src/components/RsButton.vue +393 -0
  69. package/src/components/RsCalendarGrid.vue +284 -0
  70. package/src/components/RsCard.vue +237 -0
  71. package/src/components/RsCheckbox.vue +189 -0
  72. package/src/components/RsCodeBlock.vue +278 -0
  73. package/src/components/RsCodeEditor.vue +597 -0
  74. package/src/components/RsConfigProvider.vue +92 -0
  75. package/src/components/RsConfirmDialog.vue +178 -0
  76. package/src/components/RsContainer.vue +235 -0
  77. package/src/components/RsContextMenu.vue +238 -0
  78. package/src/components/RsContextMenuItems.vue +77 -0
  79. package/src/components/RsDatePicker.vue +756 -0
  80. package/src/components/RsDateTimePicker.vue +42 -0
  81. package/src/components/RsDialog.vue +486 -0
  82. package/src/components/RsDrawer.vue +175 -0
  83. package/src/components/RsDropdown.vue +252 -0
  84. package/src/components/RsDropdownItems.vue +90 -0
  85. package/src/components/RsEmpty.vue +107 -0
  86. package/src/components/RsForm.vue +131 -0
  87. package/src/components/RsIcon.vue +104 -0
  88. package/src/components/RsInput.vue +1101 -0
  89. package/src/components/RsInputNumber.vue +532 -0
  90. package/src/components/RsLabel.vue +54 -0
  91. package/src/components/RsLink.vue +106 -0
  92. package/src/components/RsLoading.vue +171 -0
  93. package/src/components/RsMarkdown.vue +329 -0
  94. package/src/components/RsMenu.vue +256 -0
  95. package/src/components/RsMenuItems.vue +125 -0
  96. package/src/components/RsMonacoEditor.vue +541 -0
  97. package/src/components/RsPagination.vue +184 -0
  98. package/src/components/RsPopover.vue +77 -0
  99. package/src/components/RsProseEditor.vue +160 -0
  100. package/src/components/RsScrollbar.vue +171 -0
  101. package/src/components/RsSelect.vue +1045 -0
  102. package/src/components/RsSidebar.vue +121 -0
  103. package/src/components/RsSidebarGroup.vue +32 -0
  104. package/src/components/RsSidebarItem.vue +72 -0
  105. package/src/components/RsSplitPane.vue +619 -0
  106. package/src/components/RsStatCard.vue +85 -0
  107. package/src/components/RsSteps.vue +138 -0
  108. package/src/components/RsTable.vue +2664 -0
  109. package/src/components/RsTableColgroup.vue +17 -0
  110. package/src/components/RsTableHeaderRow.vue +30 -0
  111. package/src/components/RsTabs.vue +951 -0
  112. package/src/components/RsTerminal.vue +697 -0
  113. package/src/components/RsTimePicker.vue +554 -0
  114. package/src/components/RsTimePickerColumns.vue +226 -0
  115. package/src/components/RsToaster.vue +396 -0
  116. package/src/components/RsToolbar.vue +135 -0
  117. package/src/components/RsTooltip.vue +142 -0
  118. package/src/components/RsTooltipProvider.vue +20 -0
  119. package/src/components/RsTree.vue +1248 -0
  120. package/src/components/RsUpload.vue +279 -0
  121. package/src/components/RsVirtualList.vue +217 -0
  122. package/src/components/avatar-utils.ts +19 -0
  123. package/src/components/breadcrumb-utils.ts +19 -0
  124. package/src/components/class-utils.ts +29 -0
  125. package/src/components/code-editor-utils.ts +106 -0
  126. package/src/components/code-mirror-completion.ts +40 -0
  127. package/src/components/code-mirror-diagnostics.ts +52 -0
  128. package/src/components/code-mirror-ghost.ts +158 -0
  129. package/src/components/code-mirror-inline-edit.ts +55 -0
  130. package/src/components/code-mirror-intel.ts +81 -0
  131. package/src/components/code-mirror-lang.ts +128 -0
  132. package/src/components/context-menu-utils.ts +20 -0
  133. package/src/components/date-picker-utils.ts +284 -0
  134. package/src/components/date-range-validation.ts +29 -0
  135. package/src/components/date-validation.ts +38 -0
  136. package/src/components/dialog-viewport.ts +48 -0
  137. package/src/components/dialog-window.ts +253 -0
  138. package/src/components/dropdown-utils.ts +31 -0
  139. package/src/components/form-utils.ts +77 -0
  140. package/src/components/input-number-utils.ts +128 -0
  141. package/src/components/input-rules.ts +84 -0
  142. package/src/components/loading-utils.ts +8 -0
  143. package/src/components/markdown-utils.ts +126 -0
  144. package/src/components/menu-utils.ts +48 -0
  145. package/src/components/overlay-utils.ts +47 -0
  146. package/src/components/pagination-utils.ts +38 -0
  147. package/src/components/popover-utils.ts +8 -0
  148. package/src/components/reka.ts +106 -0
  149. package/src/components/resolve-radius.ts +22 -0
  150. package/src/components/resolve-size.ts +29 -0
  151. package/src/components/scrollbar-utils.ts +10 -0
  152. package/src/components/select-utils.ts +72 -0
  153. package/src/components/split-pane-utils.ts +187 -0
  154. package/src/components/steps-utils.ts +22 -0
  155. package/src/components/table/RsTableBodyRow.vue +287 -0
  156. package/src/components/table/RsTableCell.vue +239 -0
  157. package/src/components/table/RsTableCellContent.ts +18 -0
  158. package/src/components/table/RsTableCellEditor.vue +351 -0
  159. package/src/components/table/RsTableEditGutter.vue +54 -0
  160. package/src/components/table/RsTableHeaderFilter.vue +107 -0
  161. package/src/components/table/RsTableRowStatus.vue +69 -0
  162. package/src/components/table/rs-table.css +1141 -0
  163. package/src/components/table/table-context-menu.ts +53 -0
  164. package/src/components/table/table-edit-utils.ts +496 -0
  165. package/src/components/table/table-keys.ts +12 -0
  166. package/src/components/table-drag.ts +215 -0
  167. package/src/components/table-utils.ts +950 -0
  168. package/src/components/tabs-utils.ts +107 -0
  169. package/src/components/terminal-utils.ts +301 -0
  170. package/src/components/terminal-wheel.ts +49 -0
  171. package/src/components/time-picker-utils.ts +167 -0
  172. package/src/components/time-range-validation.ts +23 -0
  173. package/src/components/time-validation.ts +20 -0
  174. package/src/components/tree-utils.ts +518 -0
  175. package/src/components/upload-utils.ts +55 -0
  176. package/src/components/virtual-list-utils.ts +17 -0
  177. package/src/composables/useClickOutside.ts +22 -0
  178. package/src/composables/useRsConfig.ts +91 -0
  179. package/src/composables/useRsI18n.ts +22 -0
  180. package/src/composables/useRsTabsNav.ts +187 -0
  181. package/src/composables/useRsToast.ts +48 -0
  182. package/src/composables/useTableCellTooltip.ts +239 -0
  183. package/src/composables/useTableEdit.ts +271 -0
  184. package/src/composables/useTableRowHighlight.ts +50 -0
  185. package/src/dev/vite-codemirror-deps.ts +20 -0
  186. package/src/icons/custom/api.ts +54 -0
  187. package/src/icons/custom/clickhouse.ts +52 -0
  188. package/src/icons/custom/dameng.ts +83 -0
  189. package/src/icons/custom/ftp.ts +61 -0
  190. package/src/icons/custom/kingbase.ts +52 -0
  191. package/src/icons/custom/mongodb.ts +50 -0
  192. package/src/icons/custom/mysql.ts +51 -0
  193. package/src/icons/custom/oracle.ts +52 -0
  194. package/src/icons/custom/redis.ts +51 -0
  195. package/src/icons/custom/sqlite.ts +50 -0
  196. package/src/icons/custom/sqlserver.ts +52 -0
  197. package/src/icons/custom/vastbase.ts +57 -0
  198. package/src/icons/lucide.ts +78 -0
  199. package/src/icons/registry.ts +35 -0
  200. package/src/index.ts +425 -0
  201. package/src/lib/iso-local-datetime.ts +152 -0
  202. package/src/lib/rs-dayjs.ts +56 -0
  203. package/src/locale/messages.ts +362 -0
  204. package/src/locale/types.ts +5 -0
  205. package/src/monaco/debug-decorations.css +20 -0
  206. package/src/monaco/debug-decorations.ts +70 -0
  207. package/src/monaco/index.ts +22 -0
  208. package/src/monaco/languages/index.ts +21 -0
  209. package/src/monaco/languages/mongodb-shell.ts +125 -0
  210. package/src/monaco/languages/types.ts +55 -0
  211. package/src/monaco/setup.ts +101 -0
  212. package/src/styles.css +1040 -0
  213. package/src/theme/apply.ts +12 -0
  214. package/src/theme/brand.example.css +37 -0
  215. package/src/theme/presets.ts +69 -0
  216. package/src/theme/types.ts +81 -0
  217. package/src/utils/rs-clipboard.ts +272 -0
  218. package/src/vite-env.d.ts +7 -0
  219. package/vite-plugins/monaco-zh-nls.ts +101 -0
  220. package/vite-plugins/silence-antlr-parse-console.ts +29 -0
@@ -0,0 +1,1248 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
3
+ import { useRsI18n } from '../composables/useRsI18n'
4
+ import RsEmpty from './RsEmpty.vue'
5
+ import RsIcon from './RsIcon.vue'
6
+ import type {
7
+ RsTreeCheckState,
8
+ RsTreeDragTrigger,
9
+ RsTreeDropPosition,
10
+ RsTreeFlatNode,
11
+ RsTreeFocusMove,
12
+ RsTreeNode,
13
+ RsTreeFieldNames,
14
+ RsTreeSize,
15
+ } from './tree-utils'
16
+ import {
17
+ buildTreeNodeIndex,
18
+ collectExpandableKeys,
19
+ collectHalfCheckedKeys,
20
+ filterTreeNodes,
21
+ flattenVisibleTreeNodes,
22
+ getTreeChildren,
23
+ getTreeKey,
24
+ getTreeLabel,
25
+ hasTreeChildren,
26
+ isTreeAncestorKey,
27
+ isTreeCheckboxDisabled,
28
+ isTreeNodeDisabled,
29
+ isTreeNodeLoading,
30
+ resolveAccordionExpandedKeys,
31
+ resolveTreeCheckState,
32
+ resolveTreeFieldNames,
33
+ resolveTreeFocusKey,
34
+ resolveTreeIndent,
35
+ resolveTreeRowHeight,
36
+ resolveTreeVirtualEnabled,
37
+ sliceVirtualTreeNodes,
38
+ shouldShowTreeCheckbox,
39
+ splitTreeLabelHighlight,
40
+ toggleTreeCheck,
41
+ } from './tree-utils'
42
+ import { resolveVirtualListHeight } from './virtual-list-utils'
43
+
44
+ defineOptions({ name: 'RsTree' })
45
+
46
+ const model = defineModel<string | string[]>({ default: '' })
47
+ const expandedKeysModel = defineModel<string[]>('expandedKeys')
48
+ const checkedKeysModel = defineModel<string[]>('checkedKeys')
49
+ const halfCheckedKeysModel = defineModel<string[]>('halfCheckedKeys')
50
+
51
+ const props = withDefaults(
52
+ defineProps<{
53
+ nodes: RsTreeNode[]
54
+ fieldNames?: RsTreeFieldNames
55
+ size?: RsTreeSize
56
+ multiple?: boolean
57
+ selectable?: boolean
58
+ checkable?: boolean
59
+ checkStrictly?: boolean
60
+ onlyCheckLeaf?: boolean
61
+ checkOnClickNode?: boolean
62
+ defaultExpandAll?: boolean
63
+ defaultExpandedKeys?: string[]
64
+ defaultCheckedKeys?: string[]
65
+ expandOnClickNode?: boolean
66
+ blockNode?: boolean
67
+ showLine?: boolean
68
+ accordion?: boolean
69
+ filter?: string
70
+ filterNode?: (node: RsTreeNode, keyword: string) => boolean
71
+ highlight?: boolean
72
+ autoExpandParent?: boolean
73
+ lazy?: boolean
74
+ loadData?: (node: RsTreeNode, key: string) => void | Promise<void>
75
+ draggable?: boolean
76
+ /** handle=显示拖拽手柄;row=整行拖拽且不显示手柄 */
77
+ dragTrigger?: RsTreeDragTrigger
78
+ allowDrop?: (dragKey: string, dropKey: string, position: RsTreeDropPosition) => boolean
79
+ virtual?: boolean
80
+ virtualThreshold?: number
81
+ height?: number | string
82
+ itemHeight?: number
83
+ overscan?: number
84
+ }>(),
85
+ {
86
+ size: 'md',
87
+ multiple: false,
88
+ selectable: true,
89
+ checkable: false,
90
+ checkStrictly: false,
91
+ onlyCheckLeaf: false,
92
+ checkOnClickNode: false,
93
+ defaultExpandAll: false,
94
+ defaultExpandedKeys: () => [],
95
+ defaultCheckedKeys: () => [],
96
+ expandOnClickNode: false,
97
+ blockNode: false,
98
+ showLine: false,
99
+ accordion: false,
100
+ filter: '',
101
+ highlight: true,
102
+ autoExpandParent: true,
103
+ lazy: false,
104
+ draggable: false,
105
+ dragTrigger: 'handle',
106
+ virtual: false,
107
+ virtualThreshold: 100,
108
+ overscan: 4,
109
+ },
110
+ )
111
+
112
+ const emit = defineEmits<{
113
+ 'node-click': [node: RsTreeNode, key: string]
114
+ 'node-dblclick': [node: RsTreeNode, key: string]
115
+ expand: [key: string, expanded: boolean]
116
+ check: [keys: string[], halfCheckedKeys: string[], node: RsTreeNode, key: string]
117
+ 'node-drop': [dragKey: string, dropKey: string, position: RsTreeDropPosition]
118
+ }>()
119
+
120
+ const { t } = useRsI18n()
121
+
122
+ const fields = computed(() => resolveTreeFieldNames(props.fieldNames))
123
+ const rowHeight = computed(() => resolveTreeRowHeight(props.size, props.itemHeight))
124
+ const indentPx = computed(() => resolveTreeIndent(props.size))
125
+ const rootKeys = computed(() => props.nodes.map((node) => getTreeKey(node, fields.value)))
126
+
127
+ /* ── 自动高度(仅当业务组件显式传 virtual 时启动,由消费方决定是否开启) ── */
128
+ const treeRootRef = ref<HTMLElement | null>(null)
129
+ const _measuredHeight = ref(0)
130
+ let _heightObs: ResizeObserver | null = null
131
+
132
+ onMounted(() => {
133
+ // 必须由消费方显式声明 virtual,才启动自动高度测量
134
+ if (!props.virtual || props.height !== undefined || !treeRootRef.value) return
135
+ _heightObs = new ResizeObserver((entries) => {
136
+ _measuredHeight.value = entries[0]?.contentRect.height ?? 0
137
+ })
138
+ _heightObs.observe(treeRootRef.value)
139
+ })
140
+
141
+ onUnmounted(() => {
142
+ _heightObs?.disconnect()
143
+ _heightObs = null
144
+ })
145
+
146
+ const expandedKeysInternal = ref<string[]>(
147
+ props.defaultExpandAll
148
+ ? collectExpandableKeys(props.nodes, fields.value, props.lazy)
149
+ : [...props.defaultExpandedKeys],
150
+ )
151
+ /** 进入过滤前的展开快照;清空过滤时还原,避免无匹配时被清空后无法恢复 */
152
+ const expandedKeysBeforeFilter = ref<string[] | null>(null)
153
+ const checkedKeysInternal = ref<string[]>([...props.defaultCheckedKeys])
154
+ const scrollTop = ref(0)
155
+ const viewportRef = ref<HTMLElement | null>(null)
156
+ const focusedKey = ref<string | null>(null)
157
+ const loadingKeys = ref<Set<string>>(new Set())
158
+ const dragKey = ref<string | null>(null)
159
+ const dropTargetKey = ref<string | null>(null)
160
+ const dropPosition = ref<RsTreeDropPosition>('inside')
161
+ const rowRefs = ref<Map<string, HTMLElement>>(new Map())
162
+
163
+ const expandedKeys = computed({
164
+ get: () => expandedKeysModel.value ?? expandedKeysInternal.value,
165
+ set: (value: string[]) => {
166
+ expandedKeysInternal.value = value
167
+ expandedKeysModel.value = value
168
+ },
169
+ })
170
+
171
+ const checkedKeys = computed({
172
+ get: () => checkedKeysModel.value ?? checkedKeysInternal.value,
173
+ set: (value: string[]) => {
174
+ checkedKeysInternal.value = value
175
+ checkedKeysModel.value = value
176
+ },
177
+ })
178
+
179
+ const expandedSet = computed(() => new Set(expandedKeys.value))
180
+ const checkedSet = computed(() => new Set(checkedKeys.value))
181
+
182
+ const displayNodes = computed(() => {
183
+ const keyword = props.filter.trim()
184
+ if (!keyword) return props.nodes
185
+ return filterTreeNodes(props.nodes, keyword, {
186
+ fieldNames: fields.value,
187
+ filterNode: props.filterNode,
188
+ })
189
+ })
190
+
191
+ const nodeIndex = computed(() => buildTreeNodeIndex(props.nodes, fields.value, props.lazy))
192
+ const flatNodes = computed(() =>
193
+ flattenVisibleTreeNodes(displayNodes.value, expandedSet.value, fields.value, props.lazy),
194
+ )
195
+
196
+ /**
197
+ * 无勾选项时不存在半选态,直接短路。
198
+ * 关键在于「不读取 nodeIndex」——否则这个 immediate watch 会把全树索引拽成 eager,
199
+ * 使不带 checkbox 的树也在每次数据变更时重建 O(N) 索引并做一次全量勾选态遍历。
200
+ */
201
+ const halfCheckedKeys = computed(() => {
202
+ if (checkedSet.value.size === 0) return []
203
+ return collectHalfCheckedKeys(nodeIndex.value, checkedSet.value, props.checkStrictly, props.onlyCheckLeaf)
204
+ })
205
+
206
+ function checkboxOptions() {
207
+ return { onlyCheckLeaf: props.onlyCheckLeaf, lazy: props.lazy }
208
+ }
209
+
210
+ watch(
211
+ halfCheckedKeys,
212
+ (value) => {
213
+ halfCheckedKeysModel.value = value
214
+ },
215
+ { immediate: true },
216
+ )
217
+
218
+ const useVirtualScroll = computed(() =>
219
+ resolveTreeVirtualEnabled({
220
+ virtual: props.virtual,
221
+ flatCount: flatNodes.value.length,
222
+ virtualThreshold: props.virtualThreshold,
223
+ }),
224
+ )
225
+
226
+ if (import.meta.env.DEV) {
227
+ watch(
228
+ [useVirtualScroll, _measuredHeight],
229
+ ([virtual, h]) => {
230
+ const status = virtual ? '✅ 虚拟滚动已开启' : '⬜ 虚拟滚动未开启'
231
+ let heightInfo: string
232
+ if (h > 0) {
233
+ heightInfo = `${h}px (ResizeObserver)`
234
+ } else if (props.height === undefined) {
235
+ heightInfo = '320px (fallback)'
236
+ } else {
237
+ heightInfo = `${props.height} (prop)`
238
+ }
239
+ console.debug('[RsTree]', status, '| 节点数:', flatNodes.value.length, '| 视口高度:', heightInfo)
240
+ },
241
+ { immediate: true },
242
+ )
243
+ }
244
+
245
+ const viewportHeightPx = computed(() => {
246
+ if (props.height !== undefined) {
247
+ return Number.parseInt(resolveVirtualListHeight(props.height, 320) ?? '', 10)
248
+ }
249
+ return _measuredHeight.value > 0 ? _measuredHeight.value : 320
250
+ })
251
+
252
+ /** 过滤后滚回顶部;内容缩短时钳位,避免内部 scrollTop 与 DOM 脱节导致虚拟列表空白 */
253
+ function resetViewportScroll(): void {
254
+ scrollTop.value = 0
255
+ const el = viewportRef.value
256
+ if (el && el.scrollTop !== 0) el.scrollTop = 0
257
+ }
258
+
259
+ function clampViewportScroll(): void {
260
+ if (!useVirtualScroll.value) return
261
+ const totalHeight = flatNodes.value.length * rowHeight.value
262
+ const maxScroll = Math.max(0, totalHeight - viewportHeightPx.value)
263
+ if (scrollTop.value <= maxScroll) return
264
+ scrollTop.value = maxScroll
265
+ const el = viewportRef.value
266
+ if (el && el.scrollTop !== maxScroll) el.scrollTop = maxScroll
267
+ }
268
+
269
+ const virtualSlice = computed(() => {
270
+ if (!useVirtualScroll.value) {
271
+ return { nodes: flatNodes.value, startIndex: 0, paddingTop: 0, paddingBottom: 0 }
272
+ }
273
+ return sliceVirtualTreeNodes(
274
+ flatNodes.value,
275
+ scrollTop.value,
276
+ viewportHeightPx.value,
277
+ rowHeight.value,
278
+ props.overscan,
279
+ )
280
+ })
281
+
282
+ const visibleFlatNodes = computed(() => virtualSlice.value.nodes)
283
+ const isEmpty = computed(() => displayNodes.value.length === 0)
284
+ const showDragHandle = computed(() => props.draggable && props.dragTrigger === 'handle')
285
+ const dragWholeRow = computed(() => props.draggable && props.dragTrigger === 'row')
286
+ /** 限定高度或虚拟滚动时铺满父级,并用 ::before 捕获空白区右键(对齐 RsTable shell) */
287
+ const fillCapture = computed(() => props.virtual || props.height !== undefined)
288
+
289
+ const viewportStyle = computed(() => {
290
+ if (!useVirtualScroll.value && props.height === undefined) return undefined
291
+ if (props.height !== undefined) {
292
+ return { maxHeight: resolveVirtualListHeight(props.height, 320) }
293
+ }
294
+ const h = _measuredHeight.value > 0 ? _measuredHeight.value : 320
295
+ return { maxHeight: `${h}px` }
296
+ })
297
+
298
+ const selectedSet = computed(() => {
299
+ if (props.multiple) return new Set(Array.isArray(model.value) ? model.value.map(String) : [])
300
+ return new Set(typeof model.value === 'string' && model.value ? [model.value] : [])
301
+ })
302
+
303
+ watch(
304
+ () => props.filter,
305
+ (keyword, previousKeyword) => {
306
+ if (props.autoExpandParent) {
307
+ const query = keyword.trim()
308
+ if (!query) {
309
+ if (expandedKeysBeforeFilter.value !== null) {
310
+ expandedKeys.value = expandedKeysBeforeFilter.value
311
+ expandedKeysBeforeFilter.value = null
312
+ }
313
+ } else {
314
+ if (expandedKeysBeforeFilter.value === null) {
315
+ expandedKeysBeforeFilter.value = [...expandedKeys.value]
316
+ }
317
+ // 复用 displayNodes 的过滤结果,避免每次输入对整棵树重复跑一遍 filterTreeNodes
318
+ const filtered = displayNodes.value
319
+ // 无匹配时不覆盖展开键,否则清空搜索后原展开状态会丢失
320
+ if (filtered.length > 0) {
321
+ expandedKeys.value = collectExpandableKeys(filtered, fields.value, props.lazy)
322
+ }
323
+ }
324
+ }
325
+ // 挂载时的首次运行(oldValue 为 undefined)只处理展开键,不该动滚动位置
326
+ if (previousKeyword === undefined) return
327
+ // 列表高度骤变时浏览器不一定派发 scroll,主动归零避免虚拟切片空白
328
+ void nextTick(() => {
329
+ resetViewportScroll()
330
+ })
331
+ },
332
+ { immediate: true },
333
+ )
334
+
335
+ watch(
336
+ flatNodes,
337
+ (nodes) => {
338
+ if (nodes.length === 0) {
339
+ focusedKey.value = null
340
+ void nextTick(() => {
341
+ clampViewportScroll()
342
+ })
343
+ return
344
+ }
345
+ if (!focusedKey.value || !nodes.some((item) => item.key === focusedKey.value)) {
346
+ focusedKey.value = nodes[0]?.key ?? null
347
+ }
348
+ void nextTick(() => {
349
+ clampViewportScroll()
350
+ })
351
+ },
352
+ { immediate: true },
353
+ )
354
+
355
+ watch(
356
+ () => props.nodes,
357
+ () => {
358
+ dragKey.value = null
359
+ dropTargetKey.value = null
360
+ },
361
+ { deep: true },
362
+ )
363
+
364
+ function setRowRef(key: string, element: Element | null): void {
365
+ if (element instanceof HTMLElement) rowRefs.value.set(key, element)
366
+ else rowRefs.value.delete(key)
367
+ }
368
+
369
+ async function scrollFocusedIntoView(key: string): Promise<void> {
370
+ await nextTick()
371
+ rowRefs.value.get(key)?.scrollIntoView?.({ block: 'nearest' })
372
+ }
373
+
374
+ function setExpanded(key: string, expanded: boolean): void {
375
+ let next = [...expandedKeys.value]
376
+ if (expanded) {
377
+ next = props.accordion ? resolveAccordionExpandedKeys(key, next, nodeIndex.value, rootKeys.value) : [...new Set([...next, key])]
378
+ } else {
379
+ const set = new Set(next)
380
+ set.delete(key)
381
+ next = [...set]
382
+ }
383
+ expandedKeys.value = next
384
+ emit('expand', key, expanded)
385
+ }
386
+
387
+ async function toggleExpanded(node: RsTreeNode, key: string): Promise<void> {
388
+ const expanded = expandedSet.value.has(key)
389
+ if (expanded) {
390
+ setExpanded(key, false)
391
+ return
392
+ }
393
+
394
+ if (
395
+ props.lazy
396
+ && props.loadData
397
+ && hasTreeChildren(node, fields.value, true)
398
+ && getTreeChildren(node, fields.value).length === 0
399
+ ) {
400
+ const nextLoading = new Set(loadingKeys.value)
401
+ nextLoading.add(key)
402
+ loadingKeys.value = nextLoading
403
+ try {
404
+ await props.loadData(node, key)
405
+ } finally {
406
+ const doneLoading = new Set(loadingKeys.value)
407
+ doneLoading.delete(key)
408
+ loadingKeys.value = doneLoading
409
+ }
410
+ }
411
+
412
+ setExpanded(key, true)
413
+ }
414
+
415
+ function selectNode(node: RsTreeNode, key: string): void {
416
+ if (!props.selectable || isTreeNodeDisabled(node, fields.value)) return
417
+
418
+ if (props.multiple) {
419
+ const next = new Set(selectedSet.value)
420
+ if (next.has(key)) next.delete(key)
421
+ else next.add(key)
422
+ model.value = [...next]
423
+ return
424
+ }
425
+ model.value = key
426
+ }
427
+
428
+ function toggleCheck(node: RsTreeNode, key: string): void {
429
+ if (isTreeCheckboxDisabled(node, fields.value, checkboxOptions())) return
430
+ const next = toggleTreeCheck(
431
+ key,
432
+ checkedSet.value,
433
+ nodeIndex.value,
434
+ props.checkStrictly,
435
+ props.onlyCheckLeaf,
436
+ )
437
+ checkedKeys.value = next
438
+ emit('check', next, halfCheckedKeys.value, node, key)
439
+ }
440
+
441
+ function handleNodeAction(node: RsTreeNode, key: string): void {
442
+ emit('node-click', node, key)
443
+ focusedKey.value = key
444
+
445
+ if (props.checkable && props.checkOnClickNode) {
446
+ toggleCheck(node, key)
447
+ if (!props.selectable && !props.expandOnClickNode) return
448
+ }
449
+
450
+ if (props.expandOnClickNode && hasTreeChildren(node, fields.value, props.lazy)) {
451
+ void toggleExpanded(node, key)
452
+ return
453
+ }
454
+
455
+ if (props.selectable) selectNode(node, key)
456
+ }
457
+
458
+ function handleRowClick(node: RsTreeNode, key: string, event: MouseEvent): void {
459
+ const target = event.target as HTMLElement
460
+ if (target.closest('.rs-tree__toggle, .rs-tree__checkbox, .rs-tree__drag-handle')) return
461
+ handleNodeAction(node, key)
462
+ }
463
+
464
+ function handleRowDblclick(node: RsTreeNode, key: string, event: MouseEvent): void {
465
+ const target = event.target as HTMLElement
466
+ if (target.closest('.rs-tree__toggle, .rs-tree__checkbox, .rs-tree__drag-handle')) return
467
+ emit('node-dblclick', node, key)
468
+ }
469
+
470
+ function handleLabelClick(node: RsTreeNode, key: string, event: MouseEvent): void {
471
+ event.stopPropagation()
472
+ handleNodeAction(node, key)
473
+ }
474
+
475
+ /** 逐行渲染都会调用(含 title 插槽的 check-state);无勾选项时全部为 unchecked,不必建索引 */
476
+ function checkStateFor(key: string): RsTreeCheckState {
477
+ if (checkedSet.value.size === 0) return 'unchecked'
478
+ return resolveTreeCheckState(
479
+ key,
480
+ checkedSet.value,
481
+ nodeIndex.value,
482
+ props.checkStrictly,
483
+ props.onlyCheckLeaf,
484
+ )
485
+ }
486
+
487
+ function isSelected(key: string): boolean {
488
+ return selectedSet.value.has(key)
489
+ }
490
+
491
+ function isExpanded(key: string): boolean {
492
+ return expandedSet.value.has(key)
493
+ }
494
+
495
+ function isFocused(key: string): boolean {
496
+ return focusedKey.value === key
497
+ }
498
+
499
+ function labelParts(node: RsTreeNode): Array<{ text: string; highlight: boolean }> {
500
+ const label = getTreeLabel(node, fields.value)
501
+ if (!props.highlight || !props.filter.trim()) return [{ text: label, highlight: false }]
502
+ return splitTreeLabelHighlight(label, props.filter)
503
+ }
504
+
505
+ function rowIndentStyle(depth: number): Record<string, string> {
506
+ return { paddingInlineStart: `${depth * indentPx.value}px` }
507
+ }
508
+
509
+ function expandAll(): void {
510
+ expandedKeys.value = collectExpandableKeys(displayNodes.value, fields.value, props.lazy)
511
+ }
512
+
513
+ function collapseAll(): void {
514
+ expandedKeys.value = []
515
+ }
516
+
517
+ function expandNode(key: string): void {
518
+ setExpanded(key, true)
519
+ }
520
+
521
+ function collapseNode(key: string): void {
522
+ setExpanded(key, false)
523
+ }
524
+
525
+ function focusNode(key: string): void {
526
+ focusedKey.value = key
527
+ void scrollFocusedIntoView(key)
528
+ }
529
+
530
+ function moveFocus(move: RsTreeFocusMove): void {
531
+ const nextKey = resolveTreeFocusKey(flatNodes.value, focusedKey.value, move, nodeIndex.value)
532
+ if (!nextKey) return
533
+ focusedKey.value = nextKey
534
+ void scrollFocusedIntoView(nextKey)
535
+ }
536
+
537
+ const TREE_NAVIGATION_KEYS: Record<string, RsTreeFocusMove> = {
538
+ ArrowDown: 'next',
539
+ ArrowUp: 'prev',
540
+ Home: 'first',
541
+ End: 'last',
542
+ }
543
+
544
+ function handleTreeNavigationKey(event: KeyboardEvent): boolean {
545
+ const move = TREE_NAVIGATION_KEYS[event.key]
546
+ if (!move) return false
547
+ event.preventDefault()
548
+ moveFocus(move)
549
+ return true
550
+ }
551
+
552
+ function handleTreeArrowRight(event: KeyboardEvent, entry: RsTreeFlatNode, current: string): void {
553
+ event.preventDefault()
554
+ if (entry.hasChildren && !isExpanded(current)) void toggleExpanded(entry.node, current)
555
+ else moveFocus('next')
556
+ }
557
+
558
+ function handleTreeArrowLeft(event: KeyboardEvent, entry: RsTreeFlatNode, current: string): void {
559
+ event.preventDefault()
560
+ if (entry.hasChildren && isExpanded(current)) setExpanded(current, false)
561
+ else moveFocus('parent')
562
+ }
563
+
564
+ function handleTreeEntryKeydown(event: KeyboardEvent, entry: RsTreeFlatNode, current: string): void {
565
+ const key = event.key
566
+ if (key === 'ArrowRight') {
567
+ handleTreeArrowRight(event, entry, current)
568
+ return
569
+ }
570
+ if (key === 'ArrowLeft') {
571
+ handleTreeArrowLeft(event, entry, current)
572
+ return
573
+ }
574
+ if (key === 'Enter' || key === ' ') {
575
+ event.preventDefault()
576
+ handleNodeAction(entry.node, current)
577
+ }
578
+ }
579
+
580
+ function handleKeydown(event: KeyboardEvent): void {
581
+ if (handleTreeNavigationKey(event)) return
582
+
583
+ const current = focusedKey.value
584
+ if (!current) return
585
+ const entry = flatNodes.value.find((item) => item.key === current)
586
+ if (!entry) return
587
+
588
+ handleTreeEntryKeydown(event, entry, current)
589
+ }
590
+
591
+ function canDrop(drag: string, drop: string, position: RsTreeDropPosition): boolean {
592
+ if (drag === drop) return false
593
+ // 只禁止拖进自己的子孙(会成环)。放到自己的祖先上是合法操作——例如把节点拖出所在文件夹,
594
+ // 落在该文件夹的前/后;是否真的允许交由 allowDrop 决定,组件内不越权否决。
595
+ if (isTreeAncestorKey(drag, drop, nodeIndex.value)) return false
596
+ if (props.allowDrop) return props.allowDrop(drag, drop, position)
597
+ return true
598
+ }
599
+
600
+ function resolveDropPosition(event: MouseEvent, element: HTMLElement): RsTreeDropPosition {
601
+ const rect = element.getBoundingClientRect()
602
+ const offset = event.clientY - rect.top
603
+ if (offset < rect.height * 0.25) return 'before'
604
+ if (offset > rect.height * 0.75) return 'after'
605
+ return 'inside'
606
+ }
607
+
608
+ function onDragStart(key: string, event: DragEvent): void {
609
+ if (!props.draggable) return
610
+ dragKey.value = key
611
+ if (!event.dataTransfer) return
612
+ event.dataTransfer.setData('text/plain', key)
613
+ event.dataTransfer.effectAllowed = 'move'
614
+ }
615
+
616
+ function onDragOver(key: string, event: DragEvent): void {
617
+ if (!props.draggable || !dragKey.value) return
618
+ const row = event.currentTarget as HTMLElement
619
+ const position = resolveDropPosition(event, row)
620
+ if (!canDrop(dragKey.value, key, position)) return
621
+ event.preventDefault()
622
+ dropTargetKey.value = key
623
+ dropPosition.value = position
624
+ }
625
+
626
+ function onDragLeave(): void {
627
+ dropTargetKey.value = null
628
+ }
629
+
630
+ function onDrop(key: string, event: DragEvent): void {
631
+ if (!props.draggable || !dragKey.value) return
632
+ const row = event.currentTarget as HTMLElement
633
+ const position = resolveDropPosition(event, row)
634
+ event.preventDefault()
635
+ if (!canDrop(dragKey.value, key, position)) return
636
+ emit('node-drop', dragKey.value, key, position)
637
+ dragKey.value = null
638
+ dropTargetKey.value = null
639
+ }
640
+
641
+ function onDragEnd(): void {
642
+ dragKey.value = null
643
+ dropTargetKey.value = null
644
+ }
645
+
646
+ function onViewportScroll(event: Event): void {
647
+ scrollTop.value = (event.target as HTMLElement).scrollTop
648
+ }
649
+
650
+ defineExpose({
651
+ expandAll,
652
+ collapseAll,
653
+ expandNode,
654
+ collapseNode,
655
+ focusNode,
656
+ })
657
+ </script>
658
+
659
+ <template>
660
+ <div
661
+ ref="treeRootRef"
662
+ class="rs-tree"
663
+ :class="[
664
+ `rs-tree--${size}`,
665
+ {
666
+ 'rs-tree--virtual': useVirtualScroll,
667
+ 'rs-tree--fill-capture': fillCapture,
668
+ 'rs-tree--block': blockNode,
669
+ 'rs-tree--line': showLine,
670
+ 'rs-tree--draggable': draggable,
671
+ 'rs-tree--drag-row': dragWholeRow,
672
+ },
673
+ ]"
674
+ role="tree"
675
+ tabindex="0"
676
+ :aria-label="t('tree.label')"
677
+ :aria-multiselectable="multiple || checkable || undefined"
678
+ @keydown="handleKeydown"
679
+ >
680
+ <RsEmpty
681
+ v-if="isEmpty"
682
+ fill
683
+ class="rs-tree__empty"
684
+ :description="t('tree.empty')"
685
+ />
686
+
687
+ <div
688
+ v-else
689
+ ref="viewportRef"
690
+ class="rs-tree__viewport"
691
+ :class="{ 'rs-tree__viewport--scroll': useVirtualScroll || height !== undefined }"
692
+ :style="viewportStyle"
693
+ @scroll="onViewportScroll"
694
+ >
695
+ <div
696
+ v-if="useVirtualScroll && virtualSlice.paddingTop > 0"
697
+ class="rs-tree__pad"
698
+ :style="{ height: `${virtualSlice.paddingTop}px` }"
699
+ aria-hidden="true"
700
+ />
701
+
702
+ <ul class="rs-tree__list">
703
+ <li
704
+ v-for="(entry, entryIndex) in visibleFlatNodes"
705
+ :key="`${entry.parentKey ?? 'root'}:${entry.key}`"
706
+ class="rs-tree__item"
707
+ >
708
+ <div
709
+ :ref="(el) => setRowRef(entry.key, el as Element | null)"
710
+ class="rs-tree__row"
711
+ role="treeitem"
712
+ :tabindex="isFocused(entry.key) ? 0 : -1"
713
+ :aria-expanded="entry.hasChildren ? isExpanded(entry.key) : undefined"
714
+ :aria-selected="selectable ? isSelected(entry.key) : undefined"
715
+ :aria-level="entry.depth + 1"
716
+ :aria-setsize="flatNodes.length"
717
+ :aria-posinset="virtualSlice.startIndex + entryIndex + 1"
718
+ :draggable="draggable && !isTreeNodeDisabled(entry.node, fields)"
719
+ :class="{
720
+ 'rs-tree__row--selected': selectable && isSelected(entry.key),
721
+ 'rs-tree__row--focused': isFocused(entry.key),
722
+ 'rs-tree__row--disabled': isTreeNodeDisabled(entry.node, fields),
723
+ 'rs-tree__row--drop-before': dropTargetKey === entry.key && dropPosition === 'before',
724
+ 'rs-tree__row--drop-inside': dropTargetKey === entry.key && dropPosition === 'inside',
725
+ 'rs-tree__row--drop-after': dropTargetKey === entry.key && dropPosition === 'after',
726
+ 'rs-tree__row--last': showLine && entry.isLast,
727
+ }"
728
+ :style="[rowIndentStyle(entry.depth), { minHeight: `${rowHeight}px` }]"
729
+ @keydown="handleKeydown"
730
+ @dragstart="onDragStart(entry.key, $event)"
731
+ @dragover="onDragOver(entry.key, $event)"
732
+ @dragleave="onDragLeave"
733
+ @drop="onDrop(entry.key, $event)"
734
+ @dragend="onDragEnd"
735
+ @click="blockNode ? handleRowClick(entry.node, entry.key, $event) : undefined"
736
+ @dblclick="handleRowDblclick(entry.node, entry.key, $event)"
737
+ >
738
+ <span v-if="showLine" class="rs-tree__lines" aria-hidden="true">
739
+ <span
740
+ v-for="(draw, level) in entry.levelLines"
741
+ v-show="draw"
742
+ :key="`v-${level}`"
743
+ class="rs-tree__line-vert"
744
+ :style="{ insetInlineStart: `${level * indentPx + indentPx / 2}px` }"
745
+ />
746
+ <span
747
+ class="rs-tree__line-vert rs-tree__line-vert--self"
748
+ :class="{ 'rs-tree__line-vert--end': entry.isLast }"
749
+ :style="{ insetInlineStart: `${entry.depth * indentPx + indentPx / 2}px` }"
750
+ />
751
+ <span
752
+ class="rs-tree__line-horz"
753
+ :style="{
754
+ insetInlineStart: `${entry.depth * indentPx + indentPx / 2}px`,
755
+ width: `${indentPx / 2}px`,
756
+ }"
757
+ />
758
+ </span>
759
+
760
+ <span
761
+ v-if="showDragHandle"
762
+ class="rs-tree__drag-handle"
763
+ :aria-label="t('tree.drag')"
764
+ aria-hidden="true"
765
+ >
766
+ <RsIcon name="grip-vertical" size="sm" />
767
+ </span>
768
+
769
+ <button
770
+ v-if="entry.hasChildren"
771
+ type="button"
772
+ class="rs-tree__toggle"
773
+ :class="{ 'rs-tree__toggle--expanded': isExpanded(entry.key) }"
774
+ :aria-label="isExpanded(entry.key) ? t('tree.collapse') : t('tree.expand')"
775
+ :aria-expanded="isExpanded(entry.key)"
776
+ :disabled="isTreeNodeLoading(entry.node, entry.key, loadingKeys, fields)"
777
+ @click.stop="toggleExpanded(entry.node, entry.key)"
778
+ >
779
+ <RsIcon
780
+ v-if="isTreeNodeLoading(entry.node, entry.key, loadingKeys, fields)"
781
+ name="loader"
782
+ size="sm"
783
+ class="rs-tree__loading-icon"
784
+ spin
785
+ />
786
+ <span v-else class="rs-tree__toggle-caret" aria-hidden="true">›</span>
787
+ </button>
788
+ <span v-else class="rs-tree__spacer" aria-hidden="true" />
789
+
790
+ <label
791
+ v-if="checkable && shouldShowTreeCheckbox(entry.node, fields, checkboxOptions())"
792
+ class="rs-tree__checkbox"
793
+ :class="{
794
+ 'rs-tree__checkbox--checked': checkStateFor(entry.key) === 'checked',
795
+ 'rs-tree__checkbox--indeterminate': checkStateFor(entry.key) === 'indeterminate',
796
+ }"
797
+ >
798
+ <input
799
+ type="checkbox"
800
+ class="rs-tree__checkbox-input"
801
+ :checked="checkStateFor(entry.key) === 'checked'"
802
+ :disabled="isTreeCheckboxDisabled(entry.node, fields, checkboxOptions())"
803
+ :aria-label="t('tree.check')"
804
+ @click.stop
805
+ @change="toggleCheck(entry.node, entry.key)"
806
+ />
807
+ <span class="rs-tree__checkbox-box" aria-hidden="true" />
808
+ </label>
809
+ <span
810
+ v-else-if="checkable && onlyCheckLeaf && hasTreeChildren(entry.node, fields, lazy)"
811
+ class="rs-tree__checkbox-spacer"
812
+ aria-hidden="true"
813
+ />
814
+
815
+ <RsIcon
816
+ v-if="entry.node[fields.icon] || entry.node.icon"
817
+ :name="String(entry.node[fields.icon] ?? entry.node.icon)"
818
+ size="sm"
819
+ class="rs-tree__node-icon"
820
+ />
821
+
822
+ <button
823
+ v-if="!blockNode"
824
+ type="button"
825
+ class="rs-tree__label"
826
+ :disabled="isTreeNodeDisabled(entry.node, fields)"
827
+ @click="handleLabelClick(entry.node, entry.key, $event)"
828
+ >
829
+ <slot
830
+ name="title"
831
+ :node="entry.node"
832
+ :key="entry.key"
833
+ :label="getTreeLabel(entry.node, fields)"
834
+ :expanded="isExpanded(entry.key)"
835
+ :selected="isSelected(entry.key)"
836
+ :check-state="checkStateFor(entry.key)"
837
+ :depth="entry.depth"
838
+ :focused="isFocused(entry.key)"
839
+ >
840
+ <span class="rs-tree__label-text">
841
+ <template v-for="(part, index) in labelParts(entry.node)" :key="index">
842
+ <mark v-if="part.highlight" class="rs-tree__highlight">{{ part.text }}</mark>
843
+ <template v-else>{{ part.text }}</template>
844
+ </template>
845
+ </span>
846
+ </slot>
847
+ </button>
848
+ <span
849
+ v-else
850
+ class="rs-tree__label rs-tree__label--block"
851
+ :class="{ 'rs-tree__label--disabled': isTreeNodeDisabled(entry.node, fields) }"
852
+ >
853
+ <slot
854
+ name="title"
855
+ :node="entry.node"
856
+ :key="entry.key"
857
+ :label="getTreeLabel(entry.node, fields)"
858
+ :expanded="isExpanded(entry.key)"
859
+ :selected="isSelected(entry.key)"
860
+ :check-state="checkStateFor(entry.key)"
861
+ :depth="entry.depth"
862
+ :focused="isFocused(entry.key)"
863
+ >
864
+ <span class="rs-tree__label-text">
865
+ <template v-for="(part, index) in labelParts(entry.node)" :key="index">
866
+ <mark v-if="part.highlight" class="rs-tree__highlight">{{ part.text }}</mark>
867
+ <template v-else>{{ part.text }}</template>
868
+ </template>
869
+ </span>
870
+ </slot>
871
+ </span>
872
+ </div>
873
+ </li>
874
+ </ul>
875
+
876
+ <div
877
+ v-if="useVirtualScroll && virtualSlice.paddingBottom > 0"
878
+ class="rs-tree__pad"
879
+ :style="{ height: `${virtualSlice.paddingBottom}px` }"
880
+ aria-hidden="true"
881
+ />
882
+ </div>
883
+ </div>
884
+ </template>
885
+
886
+ <style scoped>
887
+ .rs-tree {
888
+ outline: none;
889
+ }
890
+
891
+ /* 限定高度 / 虚拟滚动:铺满父级,::before 捕获节点列表下方空白区右键 */
892
+ .rs-tree--fill-capture {
893
+ position: relative;
894
+ height: 100%;
895
+ min-height: 0;
896
+ }
897
+
898
+ .rs-tree--fill-capture::before {
899
+ content: '';
900
+ position: absolute;
901
+ inset: 0;
902
+ z-index: 0;
903
+ }
904
+
905
+ .rs-tree--fill-capture > .rs-tree__empty,
906
+ .rs-tree--fill-capture > .rs-tree__viewport {
907
+ position: relative;
908
+ z-index: 1;
909
+ height: 100%;
910
+ min-height: 0;
911
+ box-sizing: border-box;
912
+ }
913
+
914
+ .rs-tree:focus-visible {
915
+ box-shadow: 0 0 0 var(--rs-focus-ring-width, 2px) var(--rs-focus-ring);
916
+ border-radius: var(--rs-radius-sm);
917
+ }
918
+
919
+ .rs-tree__viewport--scroll {
920
+ overflow: auto;
921
+ }
922
+
923
+ /* 虚拟模式下组件自动撑满父容器,使内置 ResizeObserver 能测到真实可用高度 */
924
+ .rs-tree--virtual {
925
+ height: 100%;
926
+ }
927
+
928
+ .rs-tree--virtual .rs-tree__viewport--scroll,
929
+ .rs-tree__viewport--scroll {
930
+ border-radius: var(--rs-radius-sm);
931
+ }
932
+
933
+ .rs-tree__empty {
934
+ padding: var(--rs-space-xl) var(--rs-space-md);
935
+ }
936
+
937
+ .rs-tree__list,
938
+ .rs-tree__item {
939
+ margin: 0;
940
+ padding: 0;
941
+ list-style: none;
942
+ }
943
+
944
+ .rs-tree__row {
945
+ position: relative;
946
+ display: flex;
947
+ align-items: center;
948
+ gap: var(--rs-space-xs);
949
+ margin-inline: var(--rs-space-xs);
950
+ padding-inline: var(--rs-space-xs);
951
+ border-radius: var(--rs-radius-sm);
952
+ box-sizing: border-box;
953
+ transition: background 0.15s ease, box-shadow 0.15s ease, color 0.15s ease;
954
+ }
955
+
956
+ .rs-tree__row:hover:not(.rs-tree__row--disabled):not(.rs-tree__row--selected) {
957
+ background: var(--rs-item-hover);
958
+ }
959
+
960
+ .rs-tree__row--selected {
961
+ background: color-mix(in srgb, var(--rs-primary) 14%, var(--rs-surface));
962
+ }
963
+
964
+ .rs-tree__row--selected .rs-tree__label,
965
+ .rs-tree__row--selected .rs-tree__label--block {
966
+ color: var(--rs-primary);
967
+ font-weight: 500;
968
+ }
969
+
970
+ .rs-tree__row--focused {
971
+ background: color-mix(in srgb, var(--rs-primary) 8%, var(--rs-surface));
972
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--rs-primary) 55%, transparent);
973
+ }
974
+
975
+ .rs-tree__row--selected.rs-tree__row--focused {
976
+ background: color-mix(in srgb, var(--rs-primary) 18%, var(--rs-surface));
977
+ box-shadow: inset 0 0 0 1px var(--rs-primary);
978
+ }
979
+
980
+ .rs-tree--block .rs-tree__row {
981
+ cursor: pointer;
982
+ }
983
+
984
+ .rs-tree__row--disabled {
985
+ opacity: 0.38;
986
+ cursor: not-allowed;
987
+ }
988
+
989
+ .rs-tree__row--drop-before::before,
990
+ .rs-tree__row--drop-after::after {
991
+ content: '';
992
+ position: absolute;
993
+ left: 0;
994
+ right: 0;
995
+ height: 2px;
996
+ background: var(--rs-primary);
997
+ pointer-events: none;
998
+ }
999
+
1000
+ .rs-tree__row--drop-before::before {
1001
+ top: 0;
1002
+ }
1003
+
1004
+ .rs-tree__row--drop-after::after {
1005
+ bottom: 0;
1006
+ }
1007
+
1008
+ .rs-tree__row--drop-inside {
1009
+ background: color-mix(in srgb, var(--rs-primary) 8%, transparent);
1010
+ }
1011
+
1012
+ .rs-tree--line .rs-tree__lines {
1013
+ position: absolute;
1014
+ inset: 0;
1015
+ pointer-events: none;
1016
+ }
1017
+
1018
+ .rs-tree--line .rs-tree__line-vert {
1019
+ position: absolute;
1020
+ top: 0;
1021
+ bottom: 0;
1022
+ width: 1px;
1023
+ background: var(--rs-border-subtle);
1024
+ }
1025
+
1026
+ .rs-tree--line .rs-tree__line-vert--self.rs-tree__line-vert--end {
1027
+ bottom: 50%;
1028
+ }
1029
+
1030
+ .rs-tree--line .rs-tree__line-horz {
1031
+ position: absolute;
1032
+ top: 50%;
1033
+ height: 1px;
1034
+ background: var(--rs-border-subtle);
1035
+ }
1036
+
1037
+ .rs-tree__drag-handle {
1038
+ display: inline-flex;
1039
+ align-items: center;
1040
+ color: var(--rs-muted);
1041
+ cursor: move;
1042
+ flex: 0 0 auto;
1043
+ }
1044
+
1045
+
1046
+ .rs-tree__toggle,
1047
+ .rs-tree__spacer {
1048
+ display: inline-flex;
1049
+ align-items: center;
1050
+ justify-content: center;
1051
+ width: 1.25rem;
1052
+ height: 1.25rem;
1053
+ flex: 0 0 auto;
1054
+ }
1055
+
1056
+ .rs-tree__toggle {
1057
+ border: 0;
1058
+ padding: 0;
1059
+ border-radius: var(--rs-radius-xs);
1060
+ background: transparent;
1061
+ color: var(--rs-muted);
1062
+ cursor: pointer;
1063
+ outline: none;
1064
+ transition: color var(--rs-transition-fast);
1065
+ }
1066
+
1067
+ .rs-tree__toggle:hover:not(:disabled) {
1068
+ color: var(--rs-text);
1069
+ }
1070
+
1071
+ .rs-tree__toggle:focus-visible {
1072
+ box-shadow: 0 0 0 var(--rs-focus-ring-width, 2px) var(--rs-focus-ring);
1073
+ }
1074
+
1075
+ .rs-tree__toggle:disabled {
1076
+ cursor: default;
1077
+ }
1078
+
1079
+ .rs-tree__toggle-caret {
1080
+ display: block;
1081
+ font-size: 1.125rem;
1082
+ font-weight: 500;
1083
+ line-height: 1;
1084
+ transform: translateY(-0.5px);
1085
+ transition: transform var(--rs-transition-fast);
1086
+ }
1087
+
1088
+ .rs-tree__toggle--expanded .rs-tree__toggle-caret {
1089
+ transform: translateY(-0.5px) rotate(90deg);
1090
+ }
1091
+
1092
+ .rs-tree__loading-icon {
1093
+ color: var(--rs-primary);
1094
+ }
1095
+
1096
+ .rs-tree__checkbox {
1097
+ position: relative;
1098
+ display: inline-flex;
1099
+ align-items: center;
1100
+ flex: 0 0 auto;
1101
+ cursor: pointer;
1102
+ }
1103
+
1104
+ .rs-tree__checkbox-spacer {
1105
+ display: inline-block;
1106
+ width: 1rem;
1107
+ height: 1rem;
1108
+ flex: 0 0 auto;
1109
+ }
1110
+
1111
+ .rs-tree__checkbox-input {
1112
+ position: absolute;
1113
+ width: 1px;
1114
+ height: 1px;
1115
+ margin: -1px;
1116
+ padding: 0;
1117
+ overflow: hidden;
1118
+ clip: rect(0, 0, 0, 0);
1119
+ white-space: nowrap;
1120
+ border: 0;
1121
+ }
1122
+
1123
+ .rs-tree__checkbox-box {
1124
+ display: inline-flex;
1125
+ align-items: center;
1126
+ justify-content: center;
1127
+ width: 1rem;
1128
+ height: 1rem;
1129
+ border: 1px solid var(--rs-border);
1130
+ border-radius: var(--rs-radius-xs, 4px);
1131
+ background: var(--rs-surface);
1132
+ transition: background 0.15s ease, border-color 0.15s ease;
1133
+ }
1134
+
1135
+ .rs-tree__checkbox--checked .rs-tree__checkbox-box,
1136
+ .rs-tree__checkbox--indeterminate .rs-tree__checkbox-box {
1137
+ border-color: var(--rs-primary);
1138
+ background: var(--rs-primary);
1139
+ }
1140
+
1141
+ .rs-tree__checkbox--checked .rs-tree__checkbox-box::after {
1142
+ content: '';
1143
+ width: 0.3rem;
1144
+ height: 0.55rem;
1145
+ border: solid var(--rs-on-primary, #fff);
1146
+ border-width: 0 2px 2px 0;
1147
+ transform: rotate(45deg) translate(-1px, -1px);
1148
+ }
1149
+
1150
+ .rs-tree__checkbox--indeterminate .rs-tree__checkbox-box::after {
1151
+ content: '';
1152
+ width: 0.5rem;
1153
+ height: 2px;
1154
+ background: var(--rs-on-primary, #fff);
1155
+ border-radius: 1px;
1156
+ }
1157
+
1158
+ .rs-tree__node-icon {
1159
+ flex: 0 0 auto;
1160
+ color: var(--rs-muted);
1161
+ }
1162
+
1163
+ .rs-tree__label {
1164
+ flex: 1 1 auto;
1165
+ min-width: 0;
1166
+ border: 0;
1167
+ padding: 0;
1168
+ background: transparent;
1169
+ color: var(--rs-text);
1170
+ font: inherit;
1171
+ text-align: start;
1172
+ cursor: pointer;
1173
+ }
1174
+
1175
+ .rs-tree__label-text {
1176
+ display: block;
1177
+ overflow: hidden;
1178
+ text-overflow: ellipsis;
1179
+ white-space: nowrap;
1180
+ }
1181
+
1182
+ .rs-tree__label--block {
1183
+ cursor: inherit;
1184
+ }
1185
+
1186
+ .rs-tree__label:disabled,
1187
+ .rs-tree__label--disabled {
1188
+ cursor: not-allowed;
1189
+ }
1190
+
1191
+ .rs-tree__highlight {
1192
+ padding: 0 0.125rem;
1193
+ border: 0;
1194
+ border-radius: 2px;
1195
+ background: color-mix(in srgb, var(--rs-warning) 45%, transparent);
1196
+ color: inherit;
1197
+ font-weight: 600;
1198
+ }
1199
+
1200
+ .rs-tree__pad {
1201
+ flex: 0 0 auto;
1202
+ pointer-events: none;
1203
+ }
1204
+
1205
+ .rs-tree--ssm .rs-tree__label,
1206
+ .rs-tree--ssm .rs-tree__toggle {
1207
+ font-size: var(--rs-font-size-xs);
1208
+ }
1209
+
1210
+ .rs-tree--ssm .rs-tree__toggle,
1211
+ .rs-tree--ssm .rs-tree__spacer {
1212
+ width: 0.875rem;
1213
+ height: 0.875rem;
1214
+ }
1215
+
1216
+ .rs-tree--ssm .rs-tree__toggle-caret {
1217
+ font-size: 0.875rem;
1218
+ }
1219
+
1220
+ .rs-tree--sm .rs-tree__label,
1221
+ .rs-tree--sm .rs-tree__toggle {
1222
+ font-size: var(--rs-font-size-xs);
1223
+ }
1224
+
1225
+ .rs-tree--sm .rs-tree__toggle,
1226
+ .rs-tree--sm .rs-tree__spacer {
1227
+ width: 1rem;
1228
+ height: 1rem;
1229
+ }
1230
+
1231
+ .rs-tree--sm .rs-tree__toggle-caret {
1232
+ font-size: 1rem;
1233
+ }
1234
+
1235
+ .rs-tree--lg .rs-tree__label {
1236
+ font-size: var(--rs-font-size-sm);
1237
+ }
1238
+
1239
+ .rs-tree--lg .rs-tree__toggle,
1240
+ .rs-tree--lg .rs-tree__spacer {
1241
+ width: 1.5rem;
1242
+ height: 1.5rem;
1243
+ }
1244
+
1245
+ .rs-tree--lg .rs-tree__toggle-caret {
1246
+ font-size: 1.25rem;
1247
+ }
1248
+ </style>