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.
- package/CHANGELOG.md +29 -0
- package/LICENSE +202 -0
- package/NOTICE +25 -0
- package/README.en.md +99 -0
- package/README.md +161 -0
- package/package.json +106 -0
- package/src/__tests__/RsBadge.spec.ts +40 -0
- package/src/__tests__/RsBreadcrumb.spec.ts +172 -0
- package/src/__tests__/RsButton.spec.ts +108 -0
- package/src/__tests__/RsCalendarGrid.spec.ts +82 -0
- package/src/__tests__/RsCard.spec.ts +97 -0
- package/src/__tests__/RsCheckbox.spec.ts +36 -0
- package/src/__tests__/RsCodeEditor.spec.ts +111 -0
- package/src/__tests__/RsConfirmDialog.spec.ts +158 -0
- package/src/__tests__/RsContainer.spec.ts +89 -0
- package/src/__tests__/RsContextMenu.spec.ts +118 -0
- package/src/__tests__/RsDatePicker.spec.ts +88 -0
- package/src/__tests__/RsDateTimePicker.spec.ts +69 -0
- package/src/__tests__/RsDialog.spec.ts +404 -0
- package/src/__tests__/RsDrawer.spec.ts +145 -0
- package/src/__tests__/RsDropdown.spec.ts +135 -0
- package/src/__tests__/RsEmpty.spec.ts +99 -0
- package/src/__tests__/RsForm.spec.ts +173 -0
- package/src/__tests__/RsIcon.spec.ts +244 -0
- package/src/__tests__/RsInput.spec.ts +269 -0
- package/src/__tests__/RsInputNumber.spec.ts +118 -0
- package/src/__tests__/RsLabel.spec.ts +106 -0
- package/src/__tests__/RsLink.spec.ts +49 -0
- package/src/__tests__/RsLoading.spec.ts +84 -0
- package/src/__tests__/RsMarkdown.spec.ts +143 -0
- package/src/__tests__/RsMenu.spec.ts +119 -0
- package/src/__tests__/RsNewComponents.spec.ts +17 -0
- package/src/__tests__/RsPagination.spec.ts +158 -0
- package/src/__tests__/RsPopover.spec.ts +126 -0
- package/src/__tests__/RsScrollbar.spec.ts +53 -0
- package/src/__tests__/RsSelect.spec.ts +291 -0
- package/src/__tests__/RsSidebar.spec.ts +119 -0
- package/src/__tests__/RsSplitPane.spec.ts +177 -0
- package/src/__tests__/RsStatCard.spec.ts +79 -0
- package/src/__tests__/RsSteps.spec.ts +125 -0
- package/src/__tests__/RsTable.perf.spec.ts +103 -0
- package/src/__tests__/RsTable.spec.ts +1135 -0
- package/src/__tests__/RsTabs.spec.ts +328 -0
- package/src/__tests__/RsTerminal.spec.ts +218 -0
- package/src/__tests__/RsTimePicker.spec.ts +77 -0
- package/src/__tests__/RsToaster.spec.ts +95 -0
- package/src/__tests__/RsToolbar.spec.ts +75 -0
- package/src/__tests__/RsTooltip.spec.ts +111 -0
- package/src/__tests__/RsTree.spec.ts +572 -0
- package/src/__tests__/RsUpload.spec.ts +160 -0
- package/src/__tests__/RsVirtualList.spec.ts +77 -0
- package/src/__tests__/date-picker-utils.spec.ts +70 -0
- package/src/__tests__/dialog-viewport.spec.ts +38 -0
- package/src/__tests__/iso-local-datetime.spec.ts +58 -0
- package/src/__tests__/rs-clipboard.spec.ts +142 -0
- package/src/__tests__/setup.ts +12 -0
- package/src/__tests__/split-pane-utils.spec.ts +136 -0
- package/src/__tests__/table-drag.spec.ts +35 -0
- package/src/__tests__/table-edit-utils.spec.ts +194 -0
- package/src/__tests__/table-utils.spec.ts +286 -0
- package/src/__tests__/theme.spec.ts +22 -0
- package/src/__tests__/tree-utils.spec.ts +234 -0
- package/src/__tests__/useTableCellTooltip.spec.ts +101 -0
- package/src/components/RsAvatar.vue +115 -0
- package/src/components/RsBadge.vue +59 -0
- package/src/components/RsBreadcrumb.vue +65 -0
- package/src/components/RsBreadcrumbItems.vue +36 -0
- package/src/components/RsButton.vue +393 -0
- package/src/components/RsCalendarGrid.vue +284 -0
- package/src/components/RsCard.vue +237 -0
- package/src/components/RsCheckbox.vue +189 -0
- package/src/components/RsCodeBlock.vue +278 -0
- package/src/components/RsCodeEditor.vue +597 -0
- package/src/components/RsConfigProvider.vue +92 -0
- package/src/components/RsConfirmDialog.vue +178 -0
- package/src/components/RsContainer.vue +235 -0
- package/src/components/RsContextMenu.vue +238 -0
- package/src/components/RsContextMenuItems.vue +77 -0
- package/src/components/RsDatePicker.vue +756 -0
- package/src/components/RsDateTimePicker.vue +42 -0
- package/src/components/RsDialog.vue +486 -0
- package/src/components/RsDrawer.vue +175 -0
- package/src/components/RsDropdown.vue +252 -0
- package/src/components/RsDropdownItems.vue +90 -0
- package/src/components/RsEmpty.vue +107 -0
- package/src/components/RsForm.vue +131 -0
- package/src/components/RsIcon.vue +104 -0
- package/src/components/RsInput.vue +1101 -0
- package/src/components/RsInputNumber.vue +532 -0
- package/src/components/RsLabel.vue +54 -0
- package/src/components/RsLink.vue +106 -0
- package/src/components/RsLoading.vue +171 -0
- package/src/components/RsMarkdown.vue +329 -0
- package/src/components/RsMenu.vue +256 -0
- package/src/components/RsMenuItems.vue +125 -0
- package/src/components/RsMonacoEditor.vue +541 -0
- package/src/components/RsPagination.vue +184 -0
- package/src/components/RsPopover.vue +77 -0
- package/src/components/RsProseEditor.vue +160 -0
- package/src/components/RsScrollbar.vue +171 -0
- package/src/components/RsSelect.vue +1045 -0
- package/src/components/RsSidebar.vue +121 -0
- package/src/components/RsSidebarGroup.vue +32 -0
- package/src/components/RsSidebarItem.vue +72 -0
- package/src/components/RsSplitPane.vue +619 -0
- package/src/components/RsStatCard.vue +85 -0
- package/src/components/RsSteps.vue +138 -0
- package/src/components/RsTable.vue +2664 -0
- package/src/components/RsTableColgroup.vue +17 -0
- package/src/components/RsTableHeaderRow.vue +30 -0
- package/src/components/RsTabs.vue +951 -0
- package/src/components/RsTerminal.vue +697 -0
- package/src/components/RsTimePicker.vue +554 -0
- package/src/components/RsTimePickerColumns.vue +226 -0
- package/src/components/RsToaster.vue +396 -0
- package/src/components/RsToolbar.vue +135 -0
- package/src/components/RsTooltip.vue +142 -0
- package/src/components/RsTooltipProvider.vue +20 -0
- package/src/components/RsTree.vue +1248 -0
- package/src/components/RsUpload.vue +279 -0
- package/src/components/RsVirtualList.vue +217 -0
- package/src/components/avatar-utils.ts +19 -0
- package/src/components/breadcrumb-utils.ts +19 -0
- package/src/components/class-utils.ts +29 -0
- package/src/components/code-editor-utils.ts +106 -0
- package/src/components/code-mirror-completion.ts +40 -0
- package/src/components/code-mirror-diagnostics.ts +52 -0
- package/src/components/code-mirror-ghost.ts +158 -0
- package/src/components/code-mirror-inline-edit.ts +55 -0
- package/src/components/code-mirror-intel.ts +81 -0
- package/src/components/code-mirror-lang.ts +128 -0
- package/src/components/context-menu-utils.ts +20 -0
- package/src/components/date-picker-utils.ts +284 -0
- package/src/components/date-range-validation.ts +29 -0
- package/src/components/date-validation.ts +38 -0
- package/src/components/dialog-viewport.ts +48 -0
- package/src/components/dialog-window.ts +253 -0
- package/src/components/dropdown-utils.ts +31 -0
- package/src/components/form-utils.ts +77 -0
- package/src/components/input-number-utils.ts +128 -0
- package/src/components/input-rules.ts +84 -0
- package/src/components/loading-utils.ts +8 -0
- package/src/components/markdown-utils.ts +126 -0
- package/src/components/menu-utils.ts +48 -0
- package/src/components/overlay-utils.ts +47 -0
- package/src/components/pagination-utils.ts +38 -0
- package/src/components/popover-utils.ts +8 -0
- package/src/components/reka.ts +106 -0
- package/src/components/resolve-radius.ts +22 -0
- package/src/components/resolve-size.ts +29 -0
- package/src/components/scrollbar-utils.ts +10 -0
- package/src/components/select-utils.ts +72 -0
- package/src/components/split-pane-utils.ts +187 -0
- package/src/components/steps-utils.ts +22 -0
- package/src/components/table/RsTableBodyRow.vue +287 -0
- package/src/components/table/RsTableCell.vue +239 -0
- package/src/components/table/RsTableCellContent.ts +18 -0
- package/src/components/table/RsTableCellEditor.vue +351 -0
- package/src/components/table/RsTableEditGutter.vue +54 -0
- package/src/components/table/RsTableHeaderFilter.vue +107 -0
- package/src/components/table/RsTableRowStatus.vue +69 -0
- package/src/components/table/rs-table.css +1141 -0
- package/src/components/table/table-context-menu.ts +53 -0
- package/src/components/table/table-edit-utils.ts +496 -0
- package/src/components/table/table-keys.ts +12 -0
- package/src/components/table-drag.ts +215 -0
- package/src/components/table-utils.ts +950 -0
- package/src/components/tabs-utils.ts +107 -0
- package/src/components/terminal-utils.ts +301 -0
- package/src/components/terminal-wheel.ts +49 -0
- package/src/components/time-picker-utils.ts +167 -0
- package/src/components/time-range-validation.ts +23 -0
- package/src/components/time-validation.ts +20 -0
- package/src/components/tree-utils.ts +518 -0
- package/src/components/upload-utils.ts +55 -0
- package/src/components/virtual-list-utils.ts +17 -0
- package/src/composables/useClickOutside.ts +22 -0
- package/src/composables/useRsConfig.ts +91 -0
- package/src/composables/useRsI18n.ts +22 -0
- package/src/composables/useRsTabsNav.ts +187 -0
- package/src/composables/useRsToast.ts +48 -0
- package/src/composables/useTableCellTooltip.ts +239 -0
- package/src/composables/useTableEdit.ts +271 -0
- package/src/composables/useTableRowHighlight.ts +50 -0
- package/src/dev/vite-codemirror-deps.ts +20 -0
- package/src/icons/custom/api.ts +54 -0
- package/src/icons/custom/clickhouse.ts +52 -0
- package/src/icons/custom/dameng.ts +83 -0
- package/src/icons/custom/ftp.ts +61 -0
- package/src/icons/custom/kingbase.ts +52 -0
- package/src/icons/custom/mongodb.ts +50 -0
- package/src/icons/custom/mysql.ts +51 -0
- package/src/icons/custom/oracle.ts +52 -0
- package/src/icons/custom/redis.ts +51 -0
- package/src/icons/custom/sqlite.ts +50 -0
- package/src/icons/custom/sqlserver.ts +52 -0
- package/src/icons/custom/vastbase.ts +57 -0
- package/src/icons/lucide.ts +78 -0
- package/src/icons/registry.ts +35 -0
- package/src/index.ts +425 -0
- package/src/lib/iso-local-datetime.ts +152 -0
- package/src/lib/rs-dayjs.ts +56 -0
- package/src/locale/messages.ts +362 -0
- package/src/locale/types.ts +5 -0
- package/src/monaco/debug-decorations.css +20 -0
- package/src/monaco/debug-decorations.ts +70 -0
- package/src/monaco/index.ts +22 -0
- package/src/monaco/languages/index.ts +21 -0
- package/src/monaco/languages/mongodb-shell.ts +125 -0
- package/src/monaco/languages/types.ts +55 -0
- package/src/monaco/setup.ts +101 -0
- package/src/styles.css +1040 -0
- package/src/theme/apply.ts +12 -0
- package/src/theme/brand.example.css +37 -0
- package/src/theme/presets.ts +69 -0
- package/src/theme/types.ts +81 -0
- package/src/utils/rs-clipboard.ts +272 -0
- package/src/vite-env.d.ts +7 -0
- package/vite-plugins/monaco-zh-nls.ts +101 -0
- package/vite-plugins/silence-antlr-parse-console.ts +29 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import type { RsComponentSize } from '../theme/types'
|
|
2
|
+
|
|
3
|
+
export interface RsTreeNode {
|
|
4
|
+
key?: string
|
|
5
|
+
id?: string
|
|
6
|
+
label?: string
|
|
7
|
+
title?: string
|
|
8
|
+
children?: RsTreeNode[]
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
isLeaf?: boolean
|
|
11
|
+
icon?: string
|
|
12
|
+
disableCheckbox?: boolean
|
|
13
|
+
loading?: boolean
|
|
14
|
+
[key: string]: unknown
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RsTreeFieldNames {
|
|
18
|
+
key?: string
|
|
19
|
+
label?: string
|
|
20
|
+
children?: string
|
|
21
|
+
disabled?: string
|
|
22
|
+
isLeaf?: string
|
|
23
|
+
icon?: string
|
|
24
|
+
disableCheckbox?: string
|
|
25
|
+
loading?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RsTreeFlatNode {
|
|
29
|
+
key: string
|
|
30
|
+
node: RsTreeNode
|
|
31
|
+
depth: number
|
|
32
|
+
hasChildren: boolean
|
|
33
|
+
isLast: boolean
|
|
34
|
+
parentKey: string | null
|
|
35
|
+
/**
|
|
36
|
+
* showLine 用:下标对应祖先深度 `0..depth-1`。
|
|
37
|
+
* `true` 表示该祖先不是同级最后一项,竖线需贯穿当前行;`false` 则留空缺口。
|
|
38
|
+
*/
|
|
39
|
+
levelLines: boolean[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RsTreeNodeIndex {
|
|
43
|
+
node: RsTreeNode
|
|
44
|
+
parentKey: string | null
|
|
45
|
+
childrenKeys: string[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type RsTreeCheckState = 'checked' | 'indeterminate' | 'unchecked'
|
|
49
|
+
export type RsTreeFocusMove = 'next' | 'prev' | 'parent' | 'first' | 'last'
|
|
50
|
+
export type RsTreeDropPosition = 'before' | 'inside' | 'after'
|
|
51
|
+
/** handle=显示拖拽手柄;row=整行拖拽且不显示手柄 */
|
|
52
|
+
export type RsTreeDragTrigger = 'handle' | 'row'
|
|
53
|
+
export type RsTreeSize = RsComponentSize
|
|
54
|
+
|
|
55
|
+
export const DEFAULT_TREE_FIELD_NAMES: Required<RsTreeFieldNames> = {
|
|
56
|
+
key: 'key',
|
|
57
|
+
label: 'label',
|
|
58
|
+
children: 'children',
|
|
59
|
+
disabled: 'disabled',
|
|
60
|
+
isLeaf: 'isLeaf',
|
|
61
|
+
icon: 'icon',
|
|
62
|
+
disableCheckbox: 'disableCheckbox',
|
|
63
|
+
loading: 'loading',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 各尺寸档行高 / 缩进。
|
|
68
|
+
* 显式标注 Record<RsTreeSize, number>:尺寸类型再加档位时这里会直接编译报错,
|
|
69
|
+
* 而不是在运行时取到 undefined —— 行高 undefined 会让虚拟滚动的 padding 全变 NaN。
|
|
70
|
+
*/
|
|
71
|
+
export const TREE_ROW_HEIGHT: Record<RsTreeSize, number> = { ssm: 24, sm: 28, md: 32, lg: 40 }
|
|
72
|
+
export const TREE_INDENT_BY_SIZE: Record<RsTreeSize, number> = { ssm: 12, sm: 16, md: 20, lg: 24 }
|
|
73
|
+
|
|
74
|
+
export function resolveTreeFieldNames(fieldNames?: RsTreeFieldNames): Required<RsTreeFieldNames> {
|
|
75
|
+
return { ...DEFAULT_TREE_FIELD_NAMES, ...fieldNames }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function resolveTreeRowHeight(size: RsTreeSize = 'md', custom?: number): number {
|
|
79
|
+
if (custom !== undefined) return custom
|
|
80
|
+
return TREE_ROW_HEIGHT[size]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resolveTreeIndent(size: RsTreeSize = 'md'): number {
|
|
84
|
+
return TREE_INDENT_BY_SIZE[size]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readNodeField(node: RsTreeNode, field: string): unknown {
|
|
88
|
+
return node[field]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getTreeKey(node: RsTreeNode, fields = DEFAULT_TREE_FIELD_NAMES): string {
|
|
92
|
+
const mapped = readNodeField(node, fields.key)
|
|
93
|
+
if (mapped !== undefined && mapped !== null && mapped !== '') return String(mapped)
|
|
94
|
+
if (node.key !== undefined && node.key !== '') return String(node.key)
|
|
95
|
+
if (node.id !== undefined && node.id !== '') return String(node.id)
|
|
96
|
+
const label = readNodeField(node, fields.label) ?? node.label ?? node.title
|
|
97
|
+
return String(label ?? '')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getTreeLabel(node: RsTreeNode, fields = DEFAULT_TREE_FIELD_NAMES): string {
|
|
101
|
+
const mapped = readNodeField(node, fields.label)
|
|
102
|
+
if (mapped !== undefined && mapped !== null && mapped !== '') return String(mapped)
|
|
103
|
+
if (node.label !== undefined && node.label !== '') return String(node.label)
|
|
104
|
+
if (node.title !== undefined && node.title !== '') return String(node.title)
|
|
105
|
+
return getTreeKey(node, fields)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getTreeChildren(node: RsTreeNode, fields = DEFAULT_TREE_FIELD_NAMES): RsTreeNode[] {
|
|
109
|
+
const mapped = readNodeField(node, fields.children)
|
|
110
|
+
if (Array.isArray(mapped)) return mapped as RsTreeNode[]
|
|
111
|
+
return node.children ?? []
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getTreeIsLeaf(node: RsTreeNode, fields = DEFAULT_TREE_FIELD_NAMES): boolean {
|
|
115
|
+
return Boolean(readNodeField(node, fields.isLeaf) ?? node.isLeaf)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function isTreeNodeDisabled(node: RsTreeNode, fields = DEFAULT_TREE_FIELD_NAMES): boolean {
|
|
119
|
+
return Boolean(readNodeField(node, fields.disabled) ?? node.disabled)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function isTreeCheckboxDisabled(
|
|
123
|
+
node: RsTreeNode,
|
|
124
|
+
fields = DEFAULT_TREE_FIELD_NAMES,
|
|
125
|
+
options: { onlyCheckLeaf?: boolean; lazy?: boolean } = {},
|
|
126
|
+
): boolean {
|
|
127
|
+
if (isTreeNodeDisabled(node, fields)) return true
|
|
128
|
+
if (options.onlyCheckLeaf && hasTreeChildren(node, fields, options.lazy ?? false)) return true
|
|
129
|
+
return Boolean(readNodeField(node, fields.disableCheckbox) ?? node.disableCheckbox)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function shouldShowTreeCheckbox(
|
|
133
|
+
node: RsTreeNode,
|
|
134
|
+
fields = DEFAULT_TREE_FIELD_NAMES,
|
|
135
|
+
options: { onlyCheckLeaf?: boolean; lazy?: boolean } = {},
|
|
136
|
+
): boolean {
|
|
137
|
+
if (!options.onlyCheckLeaf) return true
|
|
138
|
+
return !hasTreeChildren(node, fields, options.lazy ?? false)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function isTreeNodeLoading(
|
|
142
|
+
node: RsTreeNode,
|
|
143
|
+
key: string,
|
|
144
|
+
loadingKeys: ReadonlySet<string>,
|
|
145
|
+
fields = DEFAULT_TREE_FIELD_NAMES,
|
|
146
|
+
): boolean {
|
|
147
|
+
return loadingKeys.has(key) || Boolean(readNodeField(node, fields.loading) ?? node.loading)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function hasTreeChildren(
|
|
151
|
+
node: RsTreeNode,
|
|
152
|
+
fields = DEFAULT_TREE_FIELD_NAMES,
|
|
153
|
+
lazy = false,
|
|
154
|
+
): boolean {
|
|
155
|
+
if (getTreeIsLeaf(node, fields)) return false
|
|
156
|
+
if (getTreeChildren(node, fields).length > 0) return true
|
|
157
|
+
return lazy
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function flattenTreeNodeIds(
|
|
161
|
+
nodes: readonly RsTreeNode[],
|
|
162
|
+
fields = DEFAULT_TREE_FIELD_NAMES,
|
|
163
|
+
): string[] {
|
|
164
|
+
return nodes.flatMap((node) => [
|
|
165
|
+
getTreeKey(node, fields),
|
|
166
|
+
...flattenTreeNodeIds(getTreeChildren(node, fields), fields),
|
|
167
|
+
])
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function collectExpandableKeys(
|
|
171
|
+
nodes: readonly RsTreeNode[],
|
|
172
|
+
fields = DEFAULT_TREE_FIELD_NAMES,
|
|
173
|
+
lazy = false,
|
|
174
|
+
): string[] {
|
|
175
|
+
const keys: string[] = []
|
|
176
|
+
function walk(items: readonly RsTreeNode[]): void {
|
|
177
|
+
for (const node of items) {
|
|
178
|
+
const key = getTreeKey(node, fields)
|
|
179
|
+
if (hasTreeChildren(node, fields, lazy)) {
|
|
180
|
+
keys.push(key)
|
|
181
|
+
walk(getTreeChildren(node, fields))
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
walk(nodes)
|
|
186
|
+
return keys
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function buildTreeNodeIndex(
|
|
190
|
+
nodes: readonly RsTreeNode[],
|
|
191
|
+
fields = DEFAULT_TREE_FIELD_NAMES,
|
|
192
|
+
lazy = false,
|
|
193
|
+
): Map<string, RsTreeNodeIndex> {
|
|
194
|
+
const map = new Map<string, RsTreeNodeIndex>()
|
|
195
|
+
function walk(items: readonly RsTreeNode[], parentKey: string | null): void {
|
|
196
|
+
for (const node of items) {
|
|
197
|
+
const key = getTreeKey(node, fields)
|
|
198
|
+
const childrenKeys = hasTreeChildren(node, fields, lazy)
|
|
199
|
+
? getTreeChildren(node, fields).map((child) => getTreeKey(child, fields))
|
|
200
|
+
: []
|
|
201
|
+
map.set(key, { node, parentKey, childrenKeys })
|
|
202
|
+
if (getTreeChildren(node, fields).length > 0) walk(getTreeChildren(node, fields), key)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
walk(nodes, null)
|
|
206
|
+
return map
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function collectDescendantKeys(
|
|
210
|
+
key: string,
|
|
211
|
+
index: ReadonlyMap<string, RsTreeNodeIndex>,
|
|
212
|
+
): string[] {
|
|
213
|
+
const entry = index.get(key)
|
|
214
|
+
if (!entry) return []
|
|
215
|
+
const result: string[] = []
|
|
216
|
+
for (const childKey of entry.childrenKeys) {
|
|
217
|
+
result.push(childKey, ...collectDescendantKeys(childKey, index))
|
|
218
|
+
}
|
|
219
|
+
return result
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* `ancestorKey` 是否为 `descendantKey` 的祖先。
|
|
224
|
+
* 沿 parentKey 上溯 O(depth),比收集整棵子树再 includes 便宜得多(拖拽 dragover 每帧都要判一次)。
|
|
225
|
+
*/
|
|
226
|
+
export function isTreeAncestorKey(
|
|
227
|
+
ancestorKey: string,
|
|
228
|
+
descendantKey: string,
|
|
229
|
+
index: ReadonlyMap<string, RsTreeNodeIndex>,
|
|
230
|
+
): boolean {
|
|
231
|
+
let parentKey = index.get(descendantKey)?.parentKey ?? null
|
|
232
|
+
while (parentKey !== null) {
|
|
233
|
+
if (parentKey === ancestorKey) return true
|
|
234
|
+
parentKey = index.get(parentKey)?.parentKey ?? null
|
|
235
|
+
}
|
|
236
|
+
return false
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function collectHalfCheckedKeys(
|
|
240
|
+
index: ReadonlyMap<string, RsTreeNodeIndex>,
|
|
241
|
+
checkedKeys: ReadonlySet<string>,
|
|
242
|
+
checkStrictly: boolean,
|
|
243
|
+
onlyCheckLeaf = false,
|
|
244
|
+
): string[] {
|
|
245
|
+
if (checkStrictly && !onlyCheckLeaf) return []
|
|
246
|
+
// 无勾选项 → 不可能有半选,省去整棵树的状态遍历
|
|
247
|
+
if (checkedKeys.size === 0) return []
|
|
248
|
+
const half: string[] = []
|
|
249
|
+
for (const key of index.keys()) {
|
|
250
|
+
if (resolveTreeCheckState(key, checkedKeys, index, checkStrictly, onlyCheckLeaf) === 'indeterminate') {
|
|
251
|
+
half.push(key)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return half
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** 根层节点无祖先竖线;共享同一个空数组,省去每个节点一次数组分配 */
|
|
258
|
+
const NO_LEVEL_LINES: readonly boolean[] = Object.freeze([])
|
|
259
|
+
|
|
260
|
+
export function flattenVisibleTreeNodes(
|
|
261
|
+
nodes: readonly RsTreeNode[],
|
|
262
|
+
expandedKeys: ReadonlySet<string>,
|
|
263
|
+
fields = DEFAULT_TREE_FIELD_NAMES,
|
|
264
|
+
lazy = false,
|
|
265
|
+
depth = 0,
|
|
266
|
+
parentKey: string | null = null,
|
|
267
|
+
/** 路径上各祖先是否为同级最后一项(长度 = depth) */
|
|
268
|
+
ancestorIsLast: readonly boolean[] = [],
|
|
269
|
+
): RsTreeFlatNode[] {
|
|
270
|
+
const result: RsTreeFlatNode[] = []
|
|
271
|
+
|
|
272
|
+
// 递归写入同一个 result,而非 push(...子结果):后者在单层可见节点达数万时会因实参过多抛 RangeError
|
|
273
|
+
function walk(
|
|
274
|
+
items: readonly RsTreeNode[],
|
|
275
|
+
currentDepth: number,
|
|
276
|
+
currentParentKey: string | null,
|
|
277
|
+
ancestors: readonly boolean[],
|
|
278
|
+
): void {
|
|
279
|
+
const lastIndex = items.length - 1
|
|
280
|
+
// 祖先非末项 → 画贯穿竖线;末项 → 该列断开,避免「幽灵」延伸。同级共用一份,行内只读
|
|
281
|
+
const levelLines = ancestors.length === 0 ? NO_LEVEL_LINES : ancestors.map((last) => !last)
|
|
282
|
+
for (let index = 0; index <= lastIndex; index += 1) {
|
|
283
|
+
const node = items[index] as RsTreeNode
|
|
284
|
+
const key = getTreeKey(node, fields)
|
|
285
|
+
const hasChildren = hasTreeChildren(node, fields, lazy)
|
|
286
|
+
const isLast = index === lastIndex
|
|
287
|
+
result.push({
|
|
288
|
+
key,
|
|
289
|
+
node,
|
|
290
|
+
depth: currentDepth,
|
|
291
|
+
hasChildren,
|
|
292
|
+
isLast,
|
|
293
|
+
parentKey: currentParentKey,
|
|
294
|
+
levelLines: levelLines as boolean[],
|
|
295
|
+
})
|
|
296
|
+
if (hasChildren && expandedKeys.has(key)) {
|
|
297
|
+
walk(getTreeChildren(node, fields), currentDepth + 1, key, [...ancestors, isLast])
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
walk(nodes, depth, parentKey, ancestorIsLast)
|
|
303
|
+
return result
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function defaultTreeFilterNode(
|
|
307
|
+
node: RsTreeNode,
|
|
308
|
+
keyword: string,
|
|
309
|
+
fields = DEFAULT_TREE_FIELD_NAMES,
|
|
310
|
+
): boolean {
|
|
311
|
+
return getTreeLabel(node, fields).toLowerCase().includes(keyword.trim().toLowerCase())
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function filterTreeNodes(
|
|
315
|
+
nodes: readonly RsTreeNode[],
|
|
316
|
+
keyword: string,
|
|
317
|
+
options: {
|
|
318
|
+
fieldNames?: RsTreeFieldNames
|
|
319
|
+
filterNode?: (node: RsTreeNode, keyword: string) => boolean
|
|
320
|
+
} = {},
|
|
321
|
+
): RsTreeNode[] {
|
|
322
|
+
const fields = resolveTreeFieldNames(options.fieldNames)
|
|
323
|
+
const query = keyword.trim()
|
|
324
|
+
if (!query) return [...nodes]
|
|
325
|
+
|
|
326
|
+
const match = options.filterNode ?? ((node, value) => defaultTreeFilterNode(node, value, fields))
|
|
327
|
+
|
|
328
|
+
function filterNode(node: RsTreeNode): RsTreeNode | null {
|
|
329
|
+
const children = getTreeChildren(node, fields)
|
|
330
|
+
const filteredChildren = children
|
|
331
|
+
.map(filterNode)
|
|
332
|
+
.filter((item): item is RsTreeNode => item !== null)
|
|
333
|
+
|
|
334
|
+
if (match(node, query)) return { ...node, children }
|
|
335
|
+
if (filteredChildren.length > 0) return { ...node, children: filteredChildren }
|
|
336
|
+
return null
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return nodes
|
|
340
|
+
.map(filterNode)
|
|
341
|
+
.filter((item): item is RsTreeNode => item !== null)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function splitTreeLabelHighlight(
|
|
345
|
+
label: string,
|
|
346
|
+
keyword: string,
|
|
347
|
+
): Array<{ text: string; highlight: boolean }> {
|
|
348
|
+
const query = keyword.trim()
|
|
349
|
+
if (!query) return [{ text: label, highlight: false }]
|
|
350
|
+
|
|
351
|
+
const lowerLabel = label.toLowerCase()
|
|
352
|
+
const lowerQuery = query.toLowerCase()
|
|
353
|
+
const index = lowerLabel.indexOf(lowerQuery)
|
|
354
|
+
if (index < 0) return [{ text: label, highlight: false }]
|
|
355
|
+
|
|
356
|
+
const parts: Array<{ text: string; highlight: boolean }> = []
|
|
357
|
+
if (index > 0) parts.push({ text: label.slice(0, index), highlight: false })
|
|
358
|
+
parts.push({ text: label.slice(index, index + query.length), highlight: true })
|
|
359
|
+
if (index + query.length < label.length) {
|
|
360
|
+
parts.push({ text: label.slice(index + query.length), highlight: false })
|
|
361
|
+
}
|
|
362
|
+
return parts
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function resolveTreeCheckState(
|
|
366
|
+
key: string,
|
|
367
|
+
checkedKeys: ReadonlySet<string>,
|
|
368
|
+
index: ReadonlyMap<string, RsTreeNodeIndex>,
|
|
369
|
+
checkStrictly: boolean,
|
|
370
|
+
onlyCheckLeaf = false,
|
|
371
|
+
): RsTreeCheckState {
|
|
372
|
+
if (checkStrictly && !onlyCheckLeaf) return checkedKeys.has(key) ? 'checked' : 'unchecked'
|
|
373
|
+
// 无勾选项时任何子树都是 unchecked,提前返回避免递归整棵子树
|
|
374
|
+
if (checkedKeys.size === 0) return 'unchecked'
|
|
375
|
+
|
|
376
|
+
const entry = index.get(key)
|
|
377
|
+
if (!entry || entry.childrenKeys.length === 0) {
|
|
378
|
+
return checkedKeys.has(key) ? 'checked' : 'unchecked'
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const childStates = entry.childrenKeys.map((childKey) =>
|
|
382
|
+
resolveTreeCheckState(childKey, checkedKeys, index, checkStrictly, onlyCheckLeaf),
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
if (childStates.every((state) => state === 'checked')) return 'checked'
|
|
386
|
+
if (childStates.some((state) => state === 'checked' || state === 'indeterminate')) return 'indeterminate'
|
|
387
|
+
return 'unchecked'
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function toggleTreeCheck(
|
|
391
|
+
key: string,
|
|
392
|
+
checkedKeys: ReadonlySet<string>,
|
|
393
|
+
index: ReadonlyMap<string, RsTreeNodeIndex>,
|
|
394
|
+
checkStrictly: boolean,
|
|
395
|
+
onlyCheckLeaf = false,
|
|
396
|
+
): string[] {
|
|
397
|
+
const next = new Set(checkedKeys)
|
|
398
|
+
const current = resolveTreeCheckState(key, next, index, checkStrictly, onlyCheckLeaf)
|
|
399
|
+
const shouldCheck = current !== 'checked'
|
|
400
|
+
|
|
401
|
+
if (checkStrictly && !onlyCheckLeaf) {
|
|
402
|
+
if (shouldCheck) next.add(key)
|
|
403
|
+
else next.delete(key)
|
|
404
|
+
return [...next]
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (onlyCheckLeaf) {
|
|
408
|
+
const entry = index.get(key)
|
|
409
|
+
if (!entry || entry.childrenKeys.length > 0) return [...checkedKeys]
|
|
410
|
+
if (shouldCheck) next.add(key)
|
|
411
|
+
else next.delete(key)
|
|
412
|
+
return [...next]
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
for (const id of [key, ...collectDescendantKeys(key, index)]) {
|
|
416
|
+
if (shouldCheck) next.add(id)
|
|
417
|
+
else next.delete(id)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let parentKey = index.get(key)?.parentKey ?? null
|
|
421
|
+
while (parentKey) {
|
|
422
|
+
const parent = index.get(parentKey)
|
|
423
|
+
if (!parent) break
|
|
424
|
+
const allChecked = parent.childrenKeys.every(
|
|
425
|
+
(childKey) => resolveTreeCheckState(childKey, next, index, false, onlyCheckLeaf) === 'checked',
|
|
426
|
+
)
|
|
427
|
+
if (allChecked) next.add(parentKey)
|
|
428
|
+
else next.delete(parentKey)
|
|
429
|
+
parentKey = parent.parentKey
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return [...next]
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function resolveAccordionExpandedKeys(
|
|
436
|
+
key: string,
|
|
437
|
+
expandedKeys: readonly string[],
|
|
438
|
+
index: ReadonlyMap<string, RsTreeNodeIndex>,
|
|
439
|
+
rootKeys: readonly string[],
|
|
440
|
+
): string[] {
|
|
441
|
+
const parentKey = index.get(key)?.parentKey ?? null
|
|
442
|
+
const siblings = parentKey ? (index.get(parentKey)?.childrenKeys ?? []) : [...rootKeys]
|
|
443
|
+
const next = new Set(expandedKeys)
|
|
444
|
+
for (const sibling of siblings) {
|
|
445
|
+
if (sibling !== key) next.delete(sibling)
|
|
446
|
+
}
|
|
447
|
+
next.add(key)
|
|
448
|
+
return [...next]
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function resolveTreeFocusKey(
|
|
452
|
+
flatNodes: readonly RsTreeFlatNode[],
|
|
453
|
+
currentKey: string | null,
|
|
454
|
+
move: RsTreeFocusMove,
|
|
455
|
+
index: ReadonlyMap<string, RsTreeNodeIndex>,
|
|
456
|
+
): string | null {
|
|
457
|
+
const keys = flatNodes.map((item) => item.key)
|
|
458
|
+
if (keys.length === 0) return null
|
|
459
|
+
if (!currentKey) return keys[0] ?? null
|
|
460
|
+
|
|
461
|
+
const currentIndex = keys.indexOf(currentKey)
|
|
462
|
+
if (currentIndex < 0) return keys[0] ?? null
|
|
463
|
+
|
|
464
|
+
switch (move) {
|
|
465
|
+
case 'next':
|
|
466
|
+
return keys[Math.min(currentIndex + 1, keys.length - 1)] ?? null
|
|
467
|
+
case 'prev':
|
|
468
|
+
return keys[Math.max(currentIndex - 1, 0)] ?? null
|
|
469
|
+
case 'first':
|
|
470
|
+
return keys[0] ?? null
|
|
471
|
+
case 'last':
|
|
472
|
+
return keys[keys.length - 1] ?? null
|
|
473
|
+
case 'parent':
|
|
474
|
+
return index.get(currentKey)?.parentKey ?? currentKey
|
|
475
|
+
default:
|
|
476
|
+
return currentKey
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function resolveTreeVirtualEnabled(options: {
|
|
481
|
+
virtual?: boolean
|
|
482
|
+
flatCount: number
|
|
483
|
+
virtualThreshold?: number
|
|
484
|
+
}): boolean {
|
|
485
|
+
if (options.virtual) return true
|
|
486
|
+
return options.flatCount > (options.virtualThreshold ?? 100)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function sliceVirtualTreeNodes(
|
|
490
|
+
nodes: readonly RsTreeFlatNode[],
|
|
491
|
+
scrollTop: number,
|
|
492
|
+
viewportHeight: number,
|
|
493
|
+
itemHeight: number,
|
|
494
|
+
overscan = 4,
|
|
495
|
+
): {
|
|
496
|
+
nodes: RsTreeFlatNode[]
|
|
497
|
+
/** 切片首项在完整扁平列表中的下标,供 aria-posinset 等按 O(1) 定位 */
|
|
498
|
+
startIndex: number
|
|
499
|
+
paddingTop: number
|
|
500
|
+
paddingBottom: number
|
|
501
|
+
} {
|
|
502
|
+
if (nodes.length === 0) return { nodes: [], startIndex: 0, paddingTop: 0, paddingBottom: 0 }
|
|
503
|
+
|
|
504
|
+
const totalHeight = nodes.length * itemHeight
|
|
505
|
+
// 过滤/折叠后内容变短时,浏览器不一定派发 scroll;钳位避免 start 越界导致空白切片
|
|
506
|
+
const maxScrollTop = Math.max(0, totalHeight - Math.max(0, viewportHeight))
|
|
507
|
+
const clampedScrollTop = Math.min(Math.max(0, scrollTop), maxScrollTop)
|
|
508
|
+
const start = Math.max(0, Math.floor(clampedScrollTop / itemHeight) - overscan)
|
|
509
|
+
const visibleCount = Math.ceil(Math.max(0, viewportHeight) / itemHeight) + overscan * 2
|
|
510
|
+
const end = Math.min(nodes.length, start + visibleCount)
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
nodes: nodes.slice(start, end),
|
|
514
|
+
startIndex: start,
|
|
515
|
+
paddingTop: start * itemHeight,
|
|
516
|
+
paddingBottom: Math.max(0, totalHeight - end * itemHeight),
|
|
517
|
+
}
|
|
518
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface RsUploadValidationRules {
|
|
2
|
+
accept?: string
|
|
3
|
+
maxSize?: number
|
|
4
|
+
maxCount?: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RsUploadValidationError {
|
|
8
|
+
file: File
|
|
9
|
+
reason: 'accept' | 'maxSize' | 'maxCount'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatFileSize(size: number): string {
|
|
13
|
+
if (size < 1024) return `${size} B`
|
|
14
|
+
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
|
15
|
+
return `${(size / 1024 / 1024).toFixed(1)} MB`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isFileAccepted(file: File, accept?: string): boolean {
|
|
19
|
+
if (!accept) return true
|
|
20
|
+
const rules = accept.split(',').map((item) => item.trim()).filter(Boolean)
|
|
21
|
+
return rules.some((rule) => {
|
|
22
|
+
if (rule.startsWith('.')) return file.name.toLowerCase().endsWith(rule.toLowerCase())
|
|
23
|
+
if (rule.endsWith('/*')) return file.type.startsWith(rule.slice(0, -1))
|
|
24
|
+
return file.type === rule
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function validateUploadFiles(
|
|
29
|
+
files: readonly File[],
|
|
30
|
+
rules: RsUploadValidationRules = {},
|
|
31
|
+
): { accepted: File[]; rejected: RsUploadValidationError[] } {
|
|
32
|
+
const accepted: File[] = []
|
|
33
|
+
const rejected: RsUploadValidationError[] = []
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
if (rules.maxCount && accepted.length >= rules.maxCount) {
|
|
36
|
+
rejected.push({ file, reason: 'maxCount' })
|
|
37
|
+
} else if (!isFileAccepted(file, rules.accept)) {
|
|
38
|
+
rejected.push({ file, reason: 'accept' })
|
|
39
|
+
} else if (rules.maxSize && file.size > rules.maxSize) {
|
|
40
|
+
rejected.push({ file, reason: 'maxSize' })
|
|
41
|
+
} else {
|
|
42
|
+
accepted.push(file)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { accepted, rejected }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function mergeUploadFiles(current: readonly File[], next: readonly File[], maxCount?: number): File[] {
|
|
49
|
+
const merged = [...current, ...next]
|
|
50
|
+
return maxCount ? merged.slice(0, maxCount) : merged
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function removeUploadFileAt(files: readonly File[], index: number): File[] {
|
|
54
|
+
return files.filter((_, currentIndex) => currentIndex !== index)
|
|
55
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type RsVirtualListItemSize = number | ((index: number) => number)
|
|
2
|
+
|
|
3
|
+
export function resolveItemSize(itemSize: RsVirtualListItemSize, index = 0): number {
|
|
4
|
+
const size = typeof itemSize === 'function' ? itemSize(index) : itemSize
|
|
5
|
+
return Number.isFinite(size) && size > 0 ? size : 32
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function resolveVirtualListHeight(height?: number | string, fallback = 240): string | undefined {
|
|
9
|
+
if (height === 0) return undefined
|
|
10
|
+
if (height === undefined) return `${fallback}px`
|
|
11
|
+
return typeof height === 'number' ? `${height}px` : height
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** height 为 0 时由父级 flex 撑满,组件内用 ResizeObserver 测量可视高度 */
|
|
15
|
+
export function isVirtualListFillHeight(height?: number | string): boolean {
|
|
16
|
+
return height === 0
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { onBeforeUnmount, onMounted, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export function useClickOutside(
|
|
4
|
+
target: Ref<HTMLElement | null>,
|
|
5
|
+
handler: () => void,
|
|
6
|
+
) {
|
|
7
|
+
function onPointerDown(event: MouseEvent) {
|
|
8
|
+
const el = target.value
|
|
9
|
+
if (!el || el.contains(event.target as Node)) {
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
handler()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
onMounted(() => {
|
|
16
|
+
document.addEventListener('mousedown', onPointerDown)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
onBeforeUnmount(() => {
|
|
20
|
+
document.removeEventListener('mousedown', onPointerDown)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { inject, provide, ref, type InjectionKey, type Ref } from 'vue'
|
|
2
|
+
import { localeMap } from '../locale/messages'
|
|
3
|
+
import { defaultLocale, type RsLocale } from '../locale/types'
|
|
4
|
+
import type { RsComponentSize, RsRadius, RsThemeMode } from '../theme/types'
|
|
5
|
+
|
|
6
|
+
export interface RsConfigContext {
|
|
7
|
+
theme: Ref<RsThemeMode>
|
|
8
|
+
locale: Ref<RsLocale>
|
|
9
|
+
/** 全局默认控件尺寸(ssm / sm / md / lg)。 */
|
|
10
|
+
controlSize: Ref<RsComponentSize>
|
|
11
|
+
/**
|
|
12
|
+
* 全局默认圆角。未设置时各组件保留自身默认档(如按钮 full、输入 sm)。
|
|
13
|
+
* 设为 `none` 可整站直角风格。
|
|
14
|
+
*/
|
|
15
|
+
controlRadius: Ref<RsRadius | undefined>
|
|
16
|
+
setTheme: (mode: RsThemeMode) => void
|
|
17
|
+
setLocale: (locale: RsLocale) => void
|
|
18
|
+
setControlSize: (size: RsComponentSize) => void
|
|
19
|
+
setControlRadius: (radius: RsRadius | undefined) => void
|
|
20
|
+
t: (key: string, fallback?: string) => string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const rsConfigKey: InjectionKey<RsConfigContext> = Symbol('rs-config')
|
|
24
|
+
|
|
25
|
+
export function useRsConfig(): RsConfigContext {
|
|
26
|
+
const ctx = inject(rsConfigKey)
|
|
27
|
+
if (!ctx) {
|
|
28
|
+
throw new Error('useRsConfig() must be used within RsConfigProvider')
|
|
29
|
+
}
|
|
30
|
+
return ctx
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 可选读取;组件在 Provider 外也可独立使用。 */
|
|
34
|
+
export function useRsConfigOptional(): RsConfigContext | null {
|
|
35
|
+
return inject(rsConfigKey, null)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createRsConfigState(
|
|
39
|
+
initialTheme: RsThemeMode = 'light',
|
|
40
|
+
initialLocale: RsLocale = defaultLocale,
|
|
41
|
+
initialControlSize: RsComponentSize = 'md',
|
|
42
|
+
initialControlRadius?: RsRadius,
|
|
43
|
+
): RsConfigContext {
|
|
44
|
+
const theme = ref<RsThemeMode>(initialTheme)
|
|
45
|
+
const locale = ref<RsLocale>(initialLocale)
|
|
46
|
+
const controlSize = ref<RsComponentSize>(initialControlSize)
|
|
47
|
+
const controlRadius = ref<RsRadius | undefined>(initialControlRadius)
|
|
48
|
+
|
|
49
|
+
function setTheme(mode: RsThemeMode) {
|
|
50
|
+
theme.value = mode
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function setLocale(next: RsLocale) {
|
|
54
|
+
locale.value = next
|
|
55
|
+
if (typeof document !== 'undefined') {
|
|
56
|
+
document.documentElement.setAttribute('data-rs-locale', next)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setControlSize(size: RsComponentSize) {
|
|
61
|
+
controlSize.value = size
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function setControlRadius(radius: RsRadius | undefined) {
|
|
65
|
+
controlRadius.value = radius
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function t(key: string, fallback?: string) {
|
|
69
|
+
return localeMap[locale.value][key] ?? fallback ?? key
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (typeof document !== 'undefined') {
|
|
73
|
+
document.documentElement.setAttribute('data-rs-locale', locale.value)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
theme,
|
|
78
|
+
locale,
|
|
79
|
+
controlSize,
|
|
80
|
+
controlRadius,
|
|
81
|
+
setTheme,
|
|
82
|
+
setLocale,
|
|
83
|
+
setControlSize,
|
|
84
|
+
setControlRadius,
|
|
85
|
+
t,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function provideRsConfig(ctx: RsConfigContext) {
|
|
90
|
+
provide(rsConfigKey, ctx)
|
|
91
|
+
}
|