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,619 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue'
3
+ import { useRsI18n } from '../composables/useRsI18n'
4
+ import RsIcon from './RsIcon.vue'
5
+ import {
6
+ applySplitResize,
7
+ collapseSplitPane,
8
+ expandSplitPane,
9
+ isSplitPaneCollapsed,
10
+ normalizeSplitSizes,
11
+ resolveSplitConstraints,
12
+ roundSize,
13
+ splitSizesEqual,
14
+ type RsSplitOrientation,
15
+ type RsSplitPaneItem,
16
+ } from './split-pane-utils'
17
+
18
+ const props = withDefaults(
19
+ defineProps<{
20
+ /** 面板定义列表(顺序即渲染顺序) */
21
+ panes: RsSplitPaneItem[]
22
+ /** horizontal 左右 · vertical 上下 */
23
+ orientation?: RsSplitOrientation
24
+ /** 禁用拖拽 / 键盘调整 */
25
+ disabled?: boolean
26
+ /** 键盘方向键单步调整的百分比 */
27
+ keyboardStep?: number
28
+ /** 在分隔条中央展示抓手(grip)指示 */
29
+ withHandle?: boolean
30
+ }>(),
31
+ {
32
+ orientation: 'horizontal',
33
+ disabled: false,
34
+ keyboardStep: 4,
35
+ withHandle: false,
36
+ },
37
+ )
38
+
39
+ /** 各面板尺寸(百分比,总和 100) */
40
+ const model = defineModel<number[]>('sizes', { default: () => [] })
41
+
42
+ const emit = defineEmits<{
43
+ /** 拖拽 / 键盘调整过程中持续触发 */
44
+ resize: [sizes: number[]]
45
+ /** 一次调整结束时触发(指针抬起、键盘按键或程序化操作) */
46
+ 'resize-end': [sizes: number[]]
47
+ collapse: [key: string]
48
+ expand: [key: string]
49
+ }>()
50
+
51
+ const { t } = useRsI18n()
52
+
53
+ const rootRef = ref<HTMLElement | null>(null)
54
+ const sizes = ref<number[]>([])
55
+
56
+ const constraints = computed(() => resolveSplitConstraints(props.panes))
57
+ const resizerCount = computed(() => Math.max(0, props.panes.length - 1))
58
+
59
+ /** 归一化初始值,作为双击复位的基准 */
60
+ let initialSizes: number[] = normalizeSplitSizes(props.panes, modelSeed())
61
+ sizes.value = initialSizes.slice()
62
+
63
+ function modelSeed(): number[] | undefined {
64
+ return model.value && model.value.length === props.panes.length ? model.value : undefined
65
+ }
66
+
67
+ function paneKeySignature(): string {
68
+ return props.panes.map((pane) => pane.key).join('|')
69
+ }
70
+
71
+ watch(
72
+ () => paneKeySignature(),
73
+ () => {
74
+ const provided = sizes.value.length === props.panes.length ? sizes.value : undefined
75
+ initialSizes = normalizeSplitSizes(props.panes, provided)
76
+ sizes.value = initialSizes.slice()
77
+ syncModel(sizes.value)
78
+ },
79
+ )
80
+
81
+ watch(model, (value) => {
82
+ if (!value || value.length !== props.panes.length) return
83
+ if (splitSizesEqual(value, sizes.value)) return
84
+ sizes.value = normalizeSplitSizes(props.panes, value)
85
+ })
86
+
87
+ function syncModel(next: number[]): void {
88
+ if (!splitSizesEqual(model.value ?? [], next)) {
89
+ model.value = next.slice()
90
+ }
91
+ }
92
+
93
+ function commit(next: number[]): void {
94
+ sizes.value = next
95
+ syncModel(next)
96
+ emit('resize', next.slice())
97
+ }
98
+
99
+ /** pane key → 折叠前尺寸;expand 未显式传 toSize 时还原 */
100
+ const sizeBeforeCollapse = new Map<string, number>()
101
+
102
+ function emitBoundaryTransitions(before: number[], after: number[]): void {
103
+ props.panes.forEach((pane, index) => {
104
+ const constraint = constraints.value[index]
105
+ const wasCollapsed = isSplitPaneCollapsed(before[index] ?? 0, constraint)
106
+ const nowCollapsed = isSplitPaneCollapsed(after[index] ?? 0, constraint)
107
+ if (!wasCollapsed && nowCollapsed) {
108
+ const prev = before[index]
109
+ if (prev !== undefined) sizeBeforeCollapse.set(pane.key, prev)
110
+ emit('collapse', pane.key)
111
+ } else if (wasCollapsed && !nowCollapsed) {
112
+ emit('expand', pane.key)
113
+ }
114
+ })
115
+ }
116
+
117
+ /** 展开目标:显式 toSize > 折叠前记忆 > 初始 size */
118
+ function resolveExpandSize(key: string, index: number, toSize?: number): number | undefined {
119
+ if (toSize !== undefined) return toSize
120
+ return sizeBeforeCollapse.get(key) ?? initialSizes[index]
121
+ }
122
+
123
+ // —— 指针拖拽 ——
124
+ let dragIndex: number | null = null
125
+ let dragOrigin = 0
126
+ let dragStartSizes: number[] = []
127
+ let flexPx = 0
128
+ let pendingDragSizes: number[] | null = null
129
+ let dragFrame = 0
130
+
131
+ function directPaneElements(root: HTMLElement): HTMLElement[] {
132
+ return [...root.children].filter(
133
+ (el): el is HTMLElement => el instanceof HTMLElement && el.classList.contains('rs-split__pane'),
134
+ )
135
+ }
136
+
137
+ function paneElements(): HTMLElement[] {
138
+ const root = rootRef.value
139
+ if (!root) return []
140
+ return directPaneElements(root)
141
+ }
142
+
143
+ /** 拖拽过程中直接写 flex-grow,避免每帧触发 Vue 重渲染 */
144
+ function applyDragToDom(next: number[]): void {
145
+ const panes = paneElements()
146
+ next.forEach((size, i) => {
147
+ panes[i]?.style.setProperty('flex-grow', String(size))
148
+ })
149
+ }
150
+
151
+ function setDraggingUi(active: boolean, index: number): void {
152
+ const root = rootRef.value
153
+ if (!root) {
154
+ return
155
+ }
156
+ root.classList.toggle('rs-split--dragging', active)
157
+ const host = root.querySelector<HTMLElement>(`[data-resizer-index="${index}"]`)
158
+ if (!host) {
159
+ return
160
+ }
161
+ if (active) {
162
+ host.setAttribute('data-active', '')
163
+ } else {
164
+ host.removeAttribute('data-active')
165
+ }
166
+ }
167
+
168
+ function scheduleDragApply(next: number[]): void {
169
+ pendingDragSizes = next
170
+ if (dragFrame) {
171
+ return
172
+ }
173
+ dragFrame = requestAnimationFrame(() => {
174
+ dragFrame = 0
175
+ if (pendingDragSizes) {
176
+ applyDragToDom(pendingDragSizes)
177
+ }
178
+ })
179
+ }
180
+
181
+ function finishDrag(index: number): void {
182
+ if (dragIndex !== index) {
183
+ return
184
+ }
185
+ dragIndex = null
186
+ if (dragFrame) {
187
+ cancelAnimationFrame(dragFrame)
188
+ dragFrame = 0
189
+ }
190
+ setDraggingUi(false, index)
191
+ const final = pendingDragSizes ?? sizes.value
192
+ pendingDragSizes = null
193
+ commit(final)
194
+ emitBoundaryTransitions(dragStartSizes, final)
195
+ emit('resize-end', final.slice())
196
+ }
197
+
198
+ function axisSize(el: HTMLElement): number {
199
+ return props.orientation === 'horizontal' ? el.offsetWidth : el.offsetHeight
200
+ }
201
+
202
+ /** 可伸缩空间的像素总量(不含分隔条),用于像素→百分比换算 */
203
+ function measureFlexPx(): number {
204
+ const root = rootRef.value
205
+ if (!root) return 0
206
+ let sum = 0
207
+ directPaneElements(root).forEach((el) => {
208
+ sum += axisSize(el)
209
+ })
210
+ return sum
211
+ }
212
+
213
+ function onResizerPointerDown(index: number, event: PointerEvent): void {
214
+ if (props.disabled) return
215
+ if (event.pointerType === 'mouse' && event.button !== 0) return
216
+ const target = event.currentTarget as HTMLElement
217
+ target.setPointerCapture?.(event.pointerId)
218
+ dragIndex = index
219
+ dragOrigin = props.orientation === 'horizontal' ? event.clientX : event.clientY
220
+ dragStartSizes = sizes.value.slice()
221
+ pendingDragSizes = dragStartSizes
222
+ flexPx = measureFlexPx()
223
+ setDraggingUi(true, index)
224
+ event.preventDefault()
225
+ }
226
+
227
+ function onResizerPointerMove(index: number, event: PointerEvent): void {
228
+ if (dragIndex !== index || flexPx <= 0) return
229
+ const position = props.orientation === 'horizontal' ? event.clientX : event.clientY
230
+ const deltaPercent = ((position - dragOrigin) / flexPx) * 100
231
+ scheduleDragApply(applySplitResize(dragStartSizes, constraints.value, index, deltaPercent))
232
+ }
233
+
234
+ function endDrag(index: number, event: PointerEvent): void {
235
+ if (dragIndex !== index) return
236
+ const target = event.currentTarget as HTMLElement
237
+ target.releasePointerCapture?.(event.pointerId)
238
+ finishDrag(index)
239
+ }
240
+
241
+ // —— 键盘无障碍 ——
242
+ function onResizerKeydown(index: number, event: KeyboardEvent): void {
243
+ if (props.disabled) return
244
+ const horizontal = props.orientation === 'horizontal'
245
+ const step = props.keyboardStep
246
+ let delta = 0
247
+ switch (event.key) {
248
+ case 'ArrowLeft':
249
+ if (horizontal) delta = -step
250
+ break
251
+ case 'ArrowRight':
252
+ if (horizontal) delta = step
253
+ break
254
+ case 'ArrowUp':
255
+ if (!horizontal) delta = -step
256
+ break
257
+ case 'ArrowDown':
258
+ if (!horizontal) delta = step
259
+ break
260
+ case 'Home':
261
+ delta = -100
262
+ break
263
+ case 'End':
264
+ delta = 100
265
+ break
266
+ case 'Enter':
267
+ case ' ':
268
+ event.preventDefault()
269
+ toggleCollapseAt(index)
270
+ return
271
+ default:
272
+ return
273
+ }
274
+ if (delta === 0) return
275
+ event.preventDefault()
276
+ const before = sizes.value.slice()
277
+ commit(applySplitResize(sizes.value, constraints.value, index, delta))
278
+ emitBoundaryTransitions(before, sizes.value)
279
+ emit('resize-end', sizes.value.slice())
280
+ }
281
+
282
+ function onResizerDblClick(index: number): void {
283
+ if (props.disabled) return
284
+ const a = sizes.value[index]
285
+ const b = sizes.value[index + 1]
286
+ const initA = initialSizes[index]
287
+ const initB = initialSizes[index + 1]
288
+ if (a === undefined || b === undefined || initA === undefined || initB === undefined) return
289
+ const total = a + b
290
+ const initTotal = initA + initB
291
+ const targetA = initTotal > 0 ? (initA / initTotal) * total : total / 2
292
+ const next = sizes.value.slice()
293
+ next[index] = roundSize(targetA)
294
+ next[index + 1] = roundSize(total - targetA)
295
+ const before = sizes.value.slice()
296
+ commit(next)
297
+ emitBoundaryTransitions(before, sizes.value)
298
+ emit('resize-end', sizes.value.slice())
299
+ }
300
+
301
+ /** 分隔条 index 两侧择一可折叠面板进行折叠/展开切换 */
302
+ function collapsibleCandidate(index: number): number {
303
+ if (constraints.value[index]?.collapsible) return index
304
+ if (constraints.value[index + 1]?.collapsible) return index + 1
305
+ return -1
306
+ }
307
+
308
+ function toggleCollapseAt(index: number): void {
309
+ const candidate = collapsibleCandidate(index)
310
+ if (candidate < 0) return
311
+ const before = sizes.value.slice()
312
+ const collapsed = isSplitPaneCollapsed(sizes.value[candidate] ?? 0, constraints.value[candidate])
313
+ const key = props.panes[candidate]?.key
314
+ const next = collapsed
315
+ ? expandSplitPane(
316
+ sizes.value,
317
+ constraints.value,
318
+ candidate,
319
+ key ? resolveExpandSize(key, candidate) : undefined,
320
+ )
321
+ : collapseSplitPane(sizes.value, constraints.value, candidate)
322
+ commit(next)
323
+ emitBoundaryTransitions(before, sizes.value)
324
+ emit('resize-end', sizes.value.slice())
325
+ }
326
+
327
+ function paneStyle(index: number): Record<string, string> {
328
+ const size = sizes.value[index] ?? 0
329
+ return { flexGrow: String(size), flexShrink: '1', flexBasis: '0%' }
330
+ }
331
+
332
+ function paneCollapsed(index: number): boolean {
333
+ return isSplitPaneCollapsed(sizes.value[index] ?? 0, constraints.value[index])
334
+ }
335
+
336
+ /** 分隔条 index(位于 panes[index] 与 panes[index+1] 之间)是否显示抓手 */
337
+ function resizerShowsHandle(index: number): boolean {
338
+ const pane = props.panes[index]
339
+ if (pane?.resizerHandle !== undefined) {
340
+ return pane.resizerHandle
341
+ }
342
+ return props.withHandle
343
+ }
344
+
345
+ // —— 程序化 API ——
346
+ function indexOfKey(key: string): number {
347
+ return props.panes.findIndex((pane) => pane.key === key)
348
+ }
349
+
350
+ function collapse(key: string): void {
351
+ const index = indexOfKey(key)
352
+ if (index < 0) return
353
+ const before = sizes.value.slice()
354
+ commit(collapseSplitPane(sizes.value, constraints.value, index))
355
+ emitBoundaryTransitions(before, sizes.value)
356
+ emit('resize-end', sizes.value.slice())
357
+ }
358
+
359
+ function expand(key: string, toSize?: number): void {
360
+ const index = indexOfKey(key)
361
+ if (index < 0) return
362
+ const before = sizes.value.slice()
363
+ commit(
364
+ expandSplitPane(sizes.value, constraints.value, index, resolveExpandSize(key, index, toSize)),
365
+ )
366
+ emitBoundaryTransitions(before, sizes.value)
367
+ emit('resize-end', sizes.value.slice())
368
+ }
369
+
370
+ function reset(): void {
371
+ initialSizes = normalizeSplitSizes(props.panes)
372
+ commit(initialSizes.slice())
373
+ emit('resize-end', sizes.value.slice())
374
+ }
375
+
376
+ defineExpose({ collapse, expand, reset, getSizes: () => sizes.value.slice() })
377
+ </script>
378
+
379
+ <template>
380
+ <div
381
+ ref="rootRef"
382
+ class="rs-split"
383
+ :class="[
384
+ `rs-split--${orientation}`,
385
+ { 'rs-split--disabled': disabled },
386
+ ]"
387
+ >
388
+ <template v-for="(pane, index) in panes" :key="pane.key">
389
+ <div
390
+ class="rs-split__pane"
391
+ :class="{ 'rs-split__pane--collapsed': paneCollapsed(index) }"
392
+ :style="paneStyle(index)"
393
+ :data-pane-key="pane.key"
394
+ >
395
+ <slot :name="pane.key" :size="sizes[index]" :collapsed="paneCollapsed(index)" />
396
+ </div>
397
+
398
+ <div
399
+ v-if="index < resizerCount"
400
+ class="rs-split__resizer-host"
401
+ :class="{ 'rs-split__resizer-host--handle': resizerShowsHandle(index) }"
402
+ :data-resizer-index="index"
403
+ @pointerdown="onResizerPointerDown(index, $event)"
404
+ @pointermove="onResizerPointerMove(index, $event)"
405
+ @pointerup="endDrag(index, $event)"
406
+ @pointercancel="endDrag(index, $event)"
407
+ >
408
+ <hr
409
+ class="rs-split__resizer"
410
+ :class="{ 'rs-split__resizer--handle': resizerShowsHandle(index) }"
411
+ :tabindex="disabled ? -1 : 0"
412
+ :aria-orientation="orientation === 'horizontal' ? 'vertical' : 'horizontal'"
413
+ :aria-valuenow="Math.round(sizes[index] ?? 0)"
414
+ :aria-valuemin="Math.round(constraints[index]?.min ?? 0)"
415
+ :aria-valuemax="Math.round(constraints[index]?.max ?? 100)"
416
+ :aria-label="t('split.resize')"
417
+ :aria-disabled="disabled || undefined"
418
+ @keydown="onResizerKeydown(index, $event)"
419
+ @dblclick="onResizerDblClick(index)"
420
+ />
421
+ <span v-if="resizerShowsHandle(index)" class="rs-split__grip" aria-hidden="true">
422
+ <RsIcon
423
+ :name="orientation === 'horizontal' ? 'grip-vertical' : 'grip-horizontal'"
424
+ :size="12"
425
+ />
426
+ </span>
427
+ </div>
428
+ </template>
429
+ </div>
430
+ </template>
431
+
432
+ <style>
433
+ .rs-split {
434
+ display: flex;
435
+ width: 100%;
436
+ height: 100%;
437
+ min-width: 0;
438
+ min-height: 0;
439
+ isolation: isolate;
440
+ /* macOS 风格 resize 光标:两侧纤细箭头 + 中央分隔竖条,白芯 + 半透明描边,热点居中 */
441
+ --rs-split-cursor-h: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cg fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7,8.5L3,12L7,15.5M3,12H8.5M17,8.5L21,12L17,15.5M21,12H15.5' stroke='%23000' stroke-opacity='.38' stroke-width='2.6'/%3E%3Cpath d='M12,6V18' stroke='%23000' stroke-opacity='.3' stroke-width='2.6'/%3E%3Cpath d='M7,8.5L3,12L7,15.5M3,12H8.5M17,8.5L21,12L17,15.5M21,12H15.5' stroke='%23fff' stroke-width='1.1'/%3E%3Cpath d='M12,6V18' stroke='%23fff' stroke-width='1.1'/%3E%3C/g%3E%3C/svg%3E") 12 12, ew-resize;
442
+ --rs-split-cursor-v: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cg fill='none' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8.5,7L12,3L15.5,7M12,3V8.5M8.5,17L12,21L15.5,17M12,21V15.5' stroke='%23000' stroke-opacity='.38' stroke-width='2.6'/%3E%3Cpath d='M6,12H18' stroke='%23000' stroke-opacity='.3' stroke-width='2.6'/%3E%3Cpath d='M8.5,7L12,3L15.5,7M12,3V8.5M8.5,17L12,21L15.5,17M12,21V15.5' stroke='%23fff' stroke-width='1.1'/%3E%3Cpath d='M6,12H18' stroke='%23fff' stroke-width='1.1'/%3E%3C/g%3E%3C/svg%3E") 12 12, ns-resize;
443
+ }
444
+
445
+ .rs-split--horizontal {
446
+ flex-direction: row;
447
+ }
448
+
449
+ .rs-split--vertical {
450
+ flex-direction: column;
451
+ }
452
+
453
+ .rs-split__pane {
454
+ position: relative;
455
+ min-width: 0;
456
+ min-height: 0;
457
+ overflow: hidden;
458
+ }
459
+
460
+ .rs-split__pane--collapsed {
461
+ overflow: hidden;
462
+ }
463
+
464
+ .rs-split__resizer-host {
465
+ position: relative;
466
+ z-index: 10;
467
+ flex: 0 0 1px;
468
+ align-self: stretch;
469
+ min-width: 0;
470
+ min-height: 0;
471
+ touch-action: none;
472
+ user-select: none;
473
+ }
474
+
475
+ .rs-split__resizer {
476
+ display: block;
477
+ width: 100%;
478
+ height: 100%;
479
+ border: none;
480
+ margin: 0;
481
+ padding: 0;
482
+ background: var(--rs-border-subtle);
483
+ opacity: 0.5;
484
+ transition:
485
+ background var(--rs-transition-normal),
486
+ opacity var(--rs-transition-normal);
487
+ }
488
+
489
+ .rs-split__resizer-host:hover .rs-split__resizer,
490
+ .rs-split__resizer-host[data-active] .rs-split__resizer {
491
+ opacity: 1;
492
+ }
493
+
494
+ .rs-split--dragging .rs-split__resizer {
495
+ transition: none;
496
+ }
497
+
498
+ .rs-split--dragging .rs-split__grip {
499
+ transition: none;
500
+ }
501
+
502
+ /* 隐形命中区:向两侧扩展,不占布局尺寸 */
503
+ .rs-split__resizer-host::before {
504
+ content: '';
505
+ position: absolute;
506
+ z-index: 1;
507
+ }
508
+
509
+ .rs-split--horizontal > .rs-split__resizer-host {
510
+ cursor: var(--rs-split-cursor-h);
511
+ }
512
+
513
+ .rs-split--horizontal > .rs-split__resizer-host::before {
514
+ inset: 0 -4px;
515
+ }
516
+
517
+ .rs-split--vertical > .rs-split__resizer-host {
518
+ cursor: var(--rs-split-cursor-v);
519
+ }
520
+
521
+ .rs-split--vertical > .rs-split__resizer-host::before {
522
+ inset: -4px 0;
523
+ }
524
+
525
+ /* 拖拽中:整个容器沿用同一光标,指针移出细缝也不闪回默认 */
526
+ .rs-split--horizontal.rs-split--dragging {
527
+ cursor: var(--rs-split-cursor-h);
528
+ }
529
+
530
+ .rs-split--vertical.rs-split--dragging {
531
+ cursor: var(--rs-split-cursor-v);
532
+ }
533
+
534
+ .rs-split__resizer-host:hover .rs-split__resizer,
535
+ .rs-split__resizer:hover,
536
+ .rs-split__resizer-host[data-active] .rs-split__resizer {
537
+ background: var(--rs-border);
538
+ opacity: 1;
539
+ }
540
+
541
+ .rs-split__resizer:focus-visible {
542
+ outline: none;
543
+ background: var(--rs-primary);
544
+ opacity: 1;
545
+ box-shadow: 0 0 0 var(--rs-focus-ring-width, 2px) var(--rs-focus-ring);
546
+ }
547
+
548
+ .rs-split--disabled > .rs-split__resizer-host {
549
+ cursor: default;
550
+ }
551
+
552
+ .rs-split--disabled > .rs-split__resizer-host:hover .rs-split__resizer {
553
+ background: var(--rs-border);
554
+ }
555
+
556
+ /* 抓手指示(仅 withHandle 时常驻显示,悬浮不额外浮现胶囊) */
557
+ .rs-split__resizer--handle {
558
+ background: var(--rs-border-subtle);
559
+ }
560
+
561
+ .rs-split__grip {
562
+ position: absolute;
563
+ top: 50%;
564
+ left: 50%;
565
+ z-index: 2;
566
+ display: inline-flex;
567
+ align-items: center;
568
+ justify-content: center;
569
+ transform: translate(-50%, -50%);
570
+ border-radius: var(--rs-radius-full);
571
+ border: 1px solid var(--rs-border);
572
+ background: var(--rs-surface-elevated);
573
+ color: var(--rs-placeholder);
574
+ box-shadow: var(--rs-shadow-sm);
575
+ pointer-events: none;
576
+ opacity: 0;
577
+ transition:
578
+ opacity var(--rs-transition-fast),
579
+ color var(--rs-transition-fast),
580
+ background var(--rs-transition-fast),
581
+ border-color var(--rs-transition-fast),
582
+ box-shadow var(--rs-transition-fast);
583
+ }
584
+
585
+ .rs-split--horizontal > .rs-split__resizer-host--handle .rs-split__grip {
586
+ width: 0.875rem;
587
+ height: 1.5rem;
588
+ }
589
+
590
+ .rs-split--vertical > .rs-split__resizer-host--handle .rs-split__grip {
591
+ width: 1.5rem;
592
+ height: 0.875rem;
593
+ }
594
+
595
+ /* 悬浮 / 聚焦 / 拖拽:常驻抓手高亮为主色 */
596
+ .rs-split:not(.rs-split--disabled) > .rs-split__resizer-host--handle:hover .rs-split__grip,
597
+ .rs-split:not(.rs-split--disabled)
598
+ > .rs-split__resizer-host--handle:has(.rs-split__resizer:focus-visible)
599
+ .rs-split__grip,
600
+ .rs-split:not(.rs-split--disabled) > .rs-split__resizer-host--handle[data-active] .rs-split__grip {
601
+ opacity: 1;
602
+ color: var(--rs-primary-foreground);
603
+ border-color: var(--rs-primary);
604
+ background: var(--rs-primary);
605
+ box-shadow: var(--rs-shadow);
606
+ }
607
+
608
+ /* 禁用态:抓手淡化 */
609
+ .rs-split--disabled .rs-split__grip {
610
+ opacity: 0.5;
611
+ box-shadow: none;
612
+ }
613
+
614
+ /* 拖拽中:屏蔽子内容命中,避免选中/iframe 抢占指针 */
615
+ .rs-split--dragging .rs-split__pane {
616
+ pointer-events: none;
617
+ user-select: none;
618
+ }
619
+ </style>
@@ -0,0 +1,85 @@
1
+ <script setup lang="ts">
2
+ export type RsStatAccent = 'primary' | 'success' | 'warning' | 'danger' | 'info'
3
+
4
+ withDefaults(
5
+ defineProps<{
6
+ label: string
7
+ value?: string | number
8
+ description?: string
9
+ accent?: RsStatAccent
10
+ loading?: boolean
11
+ }>(),
12
+ {
13
+ accent: 'primary',
14
+ loading: false,
15
+ },
16
+ )
17
+ </script>
18
+
19
+ <template>
20
+ <section class="rs-stat-card" :class="`rs-stat-card--${accent}`">
21
+ <div class="rs-stat-card__accent" />
22
+ <div class="rs-stat-card__body">
23
+ <div class="rs-stat-card__label">{{ label }}</div>
24
+ <div v-if="loading" class="rs-stat-card__skeleton" />
25
+ <div v-else class="rs-stat-card__value">
26
+ <slot name="value">{{ value }}</slot>
27
+ </div>
28
+ <p v-if="description" class="rs-stat-card__description">{{ description }}</p>
29
+ <slot />
30
+ </div>
31
+ </section>
32
+ </template>
33
+
34
+ <style scoped>
35
+ .rs-stat-card {
36
+ position: relative;
37
+ overflow: hidden;
38
+ border: 1px solid var(--rs-border);
39
+ border-radius: var(--rs-radius);
40
+ background: var(--rs-surface);
41
+ box-shadow: var(--rs-shadow-sm);
42
+ }
43
+ .rs-stat-card__accent {
44
+ height: 3px;
45
+ background: var(--rs-primary);
46
+ }
47
+ .rs-stat-card--success .rs-stat-card__accent {
48
+ background: var(--rs-success);
49
+ }
50
+ .rs-stat-card--warning .rs-stat-card__accent {
51
+ background: var(--rs-warning);
52
+ }
53
+ .rs-stat-card--danger .rs-stat-card__accent {
54
+ background: var(--rs-danger);
55
+ }
56
+ .rs-stat-card--info .rs-stat-card__accent {
57
+ background: var(--rs-info);
58
+ }
59
+ .rs-stat-card__body {
60
+ padding: var(--rs-space-lg);
61
+ }
62
+ .rs-stat-card__label {
63
+ color: var(--rs-muted);
64
+ font-size: var(--rs-font-size-sm);
65
+ }
66
+ .rs-stat-card__value {
67
+ margin-top: var(--rs-space-sm);
68
+ color: var(--rs-text);
69
+ font-size: 1.75rem;
70
+ font-weight: 700;
71
+ line-height: 1.1;
72
+ }
73
+ .rs-stat-card__description {
74
+ margin: var(--rs-space-sm) 0 0;
75
+ color: var(--rs-muted);
76
+ font-size: var(--rs-font-size-sm);
77
+ }
78
+ .rs-stat-card__skeleton {
79
+ width: 8rem;
80
+ height: 2rem;
81
+ margin-top: var(--rs-space-sm);
82
+ border-radius: var(--rs-radius-sm);
83
+ background: var(--rs-surface-hover);
84
+ }
85
+ </style>