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,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
inject,
|
|
4
|
+
onMounted,
|
|
5
|
+
onUnmounted,
|
|
6
|
+
provide,
|
|
7
|
+
type ComputedRef,
|
|
8
|
+
type InjectionKey,
|
|
9
|
+
type Ref,
|
|
10
|
+
} from 'vue'
|
|
11
|
+
|
|
12
|
+
export type RsFormLabelPosition = 'top' | 'left'
|
|
13
|
+
export type RsFormLabelAlign = 'start' | 'end'
|
|
14
|
+
export type RsFormSize = 'ssm' | 'sm' | 'md' | 'lg'
|
|
15
|
+
export type RsFormGap = 'sm' | 'md' | 'lg'
|
|
16
|
+
export type RsFormMaxWidth = 'sm' | 'md' | 'lg' | 'full' | 'none'
|
|
17
|
+
|
|
18
|
+
export interface RsFormValidationResult {
|
|
19
|
+
valid: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RsFormFieldExpose {
|
|
23
|
+
getValue: () => unknown
|
|
24
|
+
setValue: (value: unknown) => void
|
|
25
|
+
validate?: () => RsFormValidationResult | Promise<RsFormValidationResult>
|
|
26
|
+
clearValidation?: () => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RsFormContext {
|
|
30
|
+
disabled: Readonly<Ref<boolean>>
|
|
31
|
+
labelPosition: ComputedRef<RsFormLabelPosition>
|
|
32
|
+
labelWidth: Readonly<Ref<string>>
|
|
33
|
+
labelAlign: Readonly<Ref<RsFormLabelAlign>>
|
|
34
|
+
size: Readonly<Ref<RsFormSize>>
|
|
35
|
+
registerField: (id: symbol, field: RsFormFieldExpose) => void
|
|
36
|
+
unregisterField: (id: symbol) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const RS_FORM_INJECTION_KEY: InjectionKey<RsFormContext> = Symbol('RsForm')
|
|
40
|
+
|
|
41
|
+
export function cloneFormFieldValue(value: unknown): unknown {
|
|
42
|
+
if (value === undefined || value === null) return value
|
|
43
|
+
if (typeof value !== 'object') return value
|
|
44
|
+
try {
|
|
45
|
+
return structuredClone(value)
|
|
46
|
+
} catch {
|
|
47
|
+
return value
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function provideRsFormContext(context: RsFormContext): void {
|
|
52
|
+
provide(RS_FORM_INJECTION_KEY, context)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useRsFormContext(): RsFormContext | null {
|
|
56
|
+
return inject(RS_FORM_INJECTION_KEY, null)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function useRsFormField(getExpose: () => RsFormFieldExpose | undefined): void {
|
|
60
|
+
const form = useRsFormContext()
|
|
61
|
+
if (!form) return
|
|
62
|
+
|
|
63
|
+
const fieldId = Symbol('rs-form-field')
|
|
64
|
+
|
|
65
|
+
onMounted(() => {
|
|
66
|
+
const expose = getExpose()
|
|
67
|
+
if (expose) form.registerField(fieldId, expose)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
onUnmounted(() => {
|
|
71
|
+
form.unregisterField(fieldId)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function resolveRsFormSize(size?: RsFormSize, form?: RsFormContext | null): ComputedRef<RsFormSize> {
|
|
76
|
+
return computed(() => size ?? form?.size.value ?? 'md')
|
|
77
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RsInputNumber 纯函数工具(对齐 Ant InputNumber 核心语义)。
|
|
3
|
+
* 展示/编辑过程用文本草稿;提交边界再解析为 number / string。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type RsInputNumberValue = number | string | null
|
|
7
|
+
|
|
8
|
+
export function countDecimalPlaces(value: number | string): number {
|
|
9
|
+
const s = String(value)
|
|
10
|
+
const exp = s.toLowerCase().indexOf('e')
|
|
11
|
+
if (exp >= 0) {
|
|
12
|
+
const base = s.slice(0, exp)
|
|
13
|
+
const power = Number(s.slice(exp + 1))
|
|
14
|
+
const baseDecimals = Math.max(0, (base.split('.')[1] ?? '').length)
|
|
15
|
+
return Math.max(0, baseDecimals - power)
|
|
16
|
+
}
|
|
17
|
+
const dot = s.indexOf('.')
|
|
18
|
+
return dot >= 0 ? s.length - dot - 1 : 0
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveNumberPrecision(step: number | string, precision?: number): number {
|
|
22
|
+
if (precision != null && Number.isFinite(precision) && precision >= 0) {
|
|
23
|
+
return Math.floor(precision)
|
|
24
|
+
}
|
|
25
|
+
return countDecimalPlaces(step)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function roundToPrecision(value: number, precision: number): number {
|
|
29
|
+
if (!Number.isFinite(value)) return value
|
|
30
|
+
if (precision <= 0) return Math.round(value)
|
|
31
|
+
const factor = 10 ** precision
|
|
32
|
+
return Math.round(value * factor) / factor
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 是否为尚可继续输入的中间态(如 `-`、`1.`、`1e`)。 */
|
|
36
|
+
export function isNumberInputInterim(text: string): boolean {
|
|
37
|
+
const t = text.trim()
|
|
38
|
+
if (!t) return true
|
|
39
|
+
// 末尾符号:继续输入小数/指数
|
|
40
|
+
if (/[+-]$/.test(t) || /\.$/.test(t) || /[eE][+-]?$/.test(t)) return true
|
|
41
|
+
if (!/^[+-]?(\d+\.?\d*|\.\d*)([eE][+-]?\d+)?$/.test(t)) return false
|
|
42
|
+
return Number.isNaN(Number(t))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function parseNumberInput(text: string): number | null {
|
|
46
|
+
const t = text.trim()
|
|
47
|
+
if (!t || t === '+' || t === '-' || t === '.' || t === '+.' || t === '-.') return null
|
|
48
|
+
const n = Number(t)
|
|
49
|
+
return Number.isFinite(n) ? n : null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function clampNumber(value: number, min?: number, max?: number): number {
|
|
53
|
+
let next = value
|
|
54
|
+
if (min != null && Number.isFinite(min) && next < min) next = min
|
|
55
|
+
if (max != null && Number.isFinite(max) && next > max) next = max
|
|
56
|
+
return next
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatNumberValue(
|
|
60
|
+
value: number | null,
|
|
61
|
+
opts?: {
|
|
62
|
+
precision?: number
|
|
63
|
+
formatter?: (value: number, info: { userTyping: boolean; input: string }) => string
|
|
64
|
+
userTyping?: boolean
|
|
65
|
+
input?: string
|
|
66
|
+
},
|
|
67
|
+
): string {
|
|
68
|
+
if (value == null || !Number.isFinite(value)) return ''
|
|
69
|
+
if (opts?.formatter) {
|
|
70
|
+
return opts.formatter(value, {
|
|
71
|
+
userTyping: Boolean(opts.userTyping),
|
|
72
|
+
input: opts.input ?? String(value),
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
if (opts?.precision != null && opts.precision >= 0) {
|
|
76
|
+
return value.toFixed(opts.precision)
|
|
77
|
+
}
|
|
78
|
+
return String(value)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function stepNumberValue(
|
|
82
|
+
current: number | null,
|
|
83
|
+
direction: 1 | -1,
|
|
84
|
+
opts: {
|
|
85
|
+
step?: number | string
|
|
86
|
+
min?: number
|
|
87
|
+
max?: number
|
|
88
|
+
precision?: number
|
|
89
|
+
} = {},
|
|
90
|
+
): number {
|
|
91
|
+
const step = Number(opts.step ?? 1)
|
|
92
|
+
const safeStep = Number.isFinite(step) && step !== 0 ? Math.abs(step) : 1
|
|
93
|
+
const precision = resolveNumberPrecision(safeStep, opts.precision)
|
|
94
|
+
const base = current == null || !Number.isFinite(current) ? 0 : current
|
|
95
|
+
const next = roundToPrecision(base + direction * safeStep, precision)
|
|
96
|
+
return clampNumber(next, opts.min, opts.max)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function normalizeCommittedNumber(
|
|
100
|
+
raw: string,
|
|
101
|
+
opts: {
|
|
102
|
+
min?: number
|
|
103
|
+
max?: number
|
|
104
|
+
precision?: number
|
|
105
|
+
step?: number | string
|
|
106
|
+
parser?: (display: string) => string
|
|
107
|
+
} = {},
|
|
108
|
+
): number | null {
|
|
109
|
+
const text = opts.parser ? opts.parser(raw) : raw
|
|
110
|
+
const parsed = parseNumberInput(text)
|
|
111
|
+
if (parsed == null) return null
|
|
112
|
+
const precision = resolveNumberPrecision(opts.step ?? 1, opts.precision)
|
|
113
|
+
return clampNumber(roundToPrecision(parsed, precision), opts.min, opts.max)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function toModelValue(
|
|
117
|
+
value: number | null,
|
|
118
|
+
stringMode: boolean,
|
|
119
|
+
): RsInputNumberValue {
|
|
120
|
+
if (value == null) return null
|
|
121
|
+
return stringMode ? String(value) : value
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function fromModelValue(value: RsInputNumberValue): number | null {
|
|
125
|
+
if (value == null || value === '') return null
|
|
126
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : null
|
|
127
|
+
return parseNumberInput(String(value))
|
|
128
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { RsTranslateFn } from '../composables/useRsI18n'
|
|
2
|
+
import { createTranslator } from '../composables/useRsI18n'
|
|
3
|
+
import { defaultLocale } from '../locale/types'
|
|
4
|
+
|
|
5
|
+
export type RsInputRule = 'email' | 'number' | 'integer' | 'phone' | 'url' | 'minComplexity'
|
|
6
|
+
|
|
7
|
+
export type RsInputValidateTrigger = 'blur' | 'input' | 'both'
|
|
8
|
+
|
|
9
|
+
export const inputRuleMessageKeys: Record<RsInputRule, string> = {
|
|
10
|
+
email: 'input.rule.email',
|
|
11
|
+
number: 'input.rule.number',
|
|
12
|
+
integer: 'input.rule.integer',
|
|
13
|
+
phone: 'input.rule.phone',
|
|
14
|
+
url: 'input.rule.url',
|
|
15
|
+
minComplexity: 'input.rule.minComplexity',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
19
|
+
const numberPattern = /^-?\d+(\.\d+)?$/
|
|
20
|
+
const integerPattern = /^-?\d+$/
|
|
21
|
+
const phonePattern = /^1[3-9]\d{9}$/
|
|
22
|
+
const urlPattern = /^https?:\/\/.+/i
|
|
23
|
+
const minComplexityPattern = /^(?=.*[A-Za-z])(?=.*\d).{8,}$/
|
|
24
|
+
|
|
25
|
+
export function validateInputRule(value: string, rule: RsInputRule): boolean {
|
|
26
|
+
if (value === '') return true
|
|
27
|
+
|
|
28
|
+
switch (rule) {
|
|
29
|
+
case 'email':
|
|
30
|
+
return emailPattern.test(value)
|
|
31
|
+
case 'number':
|
|
32
|
+
return numberPattern.test(value)
|
|
33
|
+
case 'integer':
|
|
34
|
+
return integerPattern.test(value)
|
|
35
|
+
case 'phone':
|
|
36
|
+
return phonePattern.test(value)
|
|
37
|
+
case 'url':
|
|
38
|
+
return urlPattern.test(value)
|
|
39
|
+
case 'minComplexity':
|
|
40
|
+
return minComplexityPattern.test(value)
|
|
41
|
+
default:
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getInputRuleMessage(rule: RsInputRule, t?: RsTranslateFn): string {
|
|
47
|
+
const key = inputRuleMessageKeys[rule]
|
|
48
|
+
const tr = t ?? createTranslator(defaultLocale)
|
|
49
|
+
return tr(key)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RsInputValidateResult {
|
|
53
|
+
valid: boolean
|
|
54
|
+
message?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function runInputValidation(
|
|
58
|
+
value: string,
|
|
59
|
+
options: {
|
|
60
|
+
rule?: RsInputRule
|
|
61
|
+
required?: boolean
|
|
62
|
+
validator?: (value: string) => boolean | string
|
|
63
|
+
},
|
|
64
|
+
t?: RsTranslateFn,
|
|
65
|
+
): RsInputValidateResult {
|
|
66
|
+
const tr = t ?? createTranslator(defaultLocale)
|
|
67
|
+
|
|
68
|
+
if (options.required && value.trim() === '') {
|
|
69
|
+
return { valid: false, message: tr('input.required') }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (options.rule && !validateInputRule(value, options.rule)) {
|
|
73
|
+
return { valid: false, message: getInputRuleMessage(options.rule, tr) }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (options.validator) {
|
|
77
|
+
const result = options.validator(value)
|
|
78
|
+
if (result === true) return { valid: true }
|
|
79
|
+
if (result === false) return { valid: false, message: tr('input.formatInvalid') }
|
|
80
|
+
if (typeof result === 'string') return { valid: false, message: result }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { valid: true }
|
|
84
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown 渲染工具:GFM + 安全链接/图片 + DOMPurify 消毒。
|
|
3
|
+
* 面向表单/说明类场景,不含 AI 流式、公式、图表等业务能力。
|
|
4
|
+
*/
|
|
5
|
+
import { Marked } from 'marked'
|
|
6
|
+
import type { Tokens } from 'marked'
|
|
7
|
+
import DOMPurify from 'dompurify'
|
|
8
|
+
|
|
9
|
+
export type RsMarkdownMode = 'edit' | 'preview' | 'split'
|
|
10
|
+
|
|
11
|
+
export type RsMarkdownRenderOptions = {
|
|
12
|
+
/** GFM 换行(单个换行转 <br>),默认 true */
|
|
13
|
+
breaks?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function escapeHtml(text: string): string {
|
|
17
|
+
return text
|
|
18
|
+
.replaceAll('&', '&')
|
|
19
|
+
.replaceAll('<', '<')
|
|
20
|
+
.replaceAll('>', '>')
|
|
21
|
+
.replaceAll('"', '"')
|
|
22
|
+
.replaceAll("'", ''')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isSafeHref(href: string): boolean {
|
|
26
|
+
const value = href.trim()
|
|
27
|
+
if (!value) return false
|
|
28
|
+
if (value.startsWith('#') || value.startsWith('/')) return true
|
|
29
|
+
return /^(https?:|mailto:)/i.test(value)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isSafeImageSrc(src: string): boolean {
|
|
33
|
+
const value = src.trim()
|
|
34
|
+
if (!value) return false
|
|
35
|
+
if (value.startsWith('data:image/')) return true
|
|
36
|
+
return /^(https?:)/i.test(value)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sanitizeLang(lang: string | undefined): string {
|
|
40
|
+
const raw = (lang ?? 'text').trim().split(/\s+/)[0] ?? 'text'
|
|
41
|
+
return raw.replace(/[^\w.+#-]/g, '') || 'text'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderCodeBlock(text: string, lang: string | undefined): string {
|
|
45
|
+
const language = sanitizeLang(lang)
|
|
46
|
+
const code = text.replace(/\n$/, '')
|
|
47
|
+
return [
|
|
48
|
+
`<div class="rs-markdown__codeblock" data-rs-md-lang="${escapeHtml(language)}">`,
|
|
49
|
+
`<div class="rs-markdown__code-head">`,
|
|
50
|
+
`<span class="rs-markdown__code-lang">${escapeHtml(language)}</span>`,
|
|
51
|
+
`</div>`,
|
|
52
|
+
`<pre class="rs-markdown__pre"><code class="language-${escapeHtml(language)}">${escapeHtml(code)}</code></pre>`,
|
|
53
|
+
`</div>`,
|
|
54
|
+
].join('')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function wrapTables(html: string): string {
|
|
58
|
+
return html.replace(/<table\b[\s\S]*?<\/table>/gi, (table) => {
|
|
59
|
+
return `<div class="rs-markdown__table-wrap">${table}</div>`
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createMarked(breaks: boolean): Marked {
|
|
64
|
+
return new Marked({
|
|
65
|
+
gfm: true,
|
|
66
|
+
breaks,
|
|
67
|
+
renderer: {
|
|
68
|
+
code({ text, lang }) {
|
|
69
|
+
return renderCodeBlock(text, lang)
|
|
70
|
+
},
|
|
71
|
+
link(
|
|
72
|
+
this: { parser: { parseInline: (tokens: Tokens.Link['tokens']) => string } },
|
|
73
|
+
token: Tokens.Link,
|
|
74
|
+
) {
|
|
75
|
+
const text = this.parser.parseInline(token.tokens)
|
|
76
|
+
const href = token.href?.trim() ?? ''
|
|
77
|
+
if (!isSafeHref(href)) return text
|
|
78
|
+
const title = token.title ? ` title="${escapeHtml(token.title)}"` : ''
|
|
79
|
+
return `<a href="${escapeHtml(href)}"${title} target="_blank" rel="noopener noreferrer">${text}</a>`
|
|
80
|
+
},
|
|
81
|
+
image({ href, title, text }) {
|
|
82
|
+
const src = href?.trim() ?? ''
|
|
83
|
+
if (!isSafeImageSrc(src)) return escapeHtml(text || '')
|
|
84
|
+
const alt = escapeHtml(text || '')
|
|
85
|
+
const titleAttr = title ? ` title="${escapeHtml(title)}"` : ''
|
|
86
|
+
return `<img class="rs-markdown__img" src="${escapeHtml(src)}" alt="${alt}"${titleAttr} loading="lazy" decoding="async" />`
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const markedCache = new Map<boolean, Marked>()
|
|
93
|
+
|
|
94
|
+
function getMarked(breaks: boolean): Marked {
|
|
95
|
+
const cached = markedCache.get(breaks)
|
|
96
|
+
if (cached) return cached
|
|
97
|
+
const instance = createMarked(breaks)
|
|
98
|
+
markedCache.set(breaks, instance)
|
|
99
|
+
return instance
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 将 Markdown 转为可安全注入的 HTML(必须配合 v-html 使用消毒结果)。
|
|
104
|
+
*/
|
|
105
|
+
export function renderMarkdown(source = '', options?: RsMarkdownRenderOptions): string {
|
|
106
|
+
if (!source.trim()) return ''
|
|
107
|
+
const breaks = options?.breaks !== false
|
|
108
|
+
const dirty = wrapTables(getMarked(breaks).parse(source, { async: false }) as string)
|
|
109
|
+
return DOMPurify.sanitize(dirty, {
|
|
110
|
+
USE_PROFILES: { html: true },
|
|
111
|
+
ADD_ATTR: ['class', 'target', 'rel', 'loading', 'decoding', 'alt', 'src', 'data-rs-md-lang'],
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function resolveMarkdownHeight(height?: number | string): string {
|
|
116
|
+
if (height === undefined) return '16rem'
|
|
117
|
+
return typeof height === 'number' ? `${height}px` : height
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function resolveMarkdownMode(
|
|
121
|
+
mode: RsMarkdownMode | undefined,
|
|
122
|
+
readonly: boolean,
|
|
123
|
+
): RsMarkdownMode {
|
|
124
|
+
if (readonly) return 'preview'
|
|
125
|
+
return mode ?? 'edit'
|
|
126
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface RsMenuItem {
|
|
2
|
+
key: string
|
|
3
|
+
label: string
|
|
4
|
+
icon?: string
|
|
5
|
+
disabled?: boolean
|
|
6
|
+
children?: RsMenuItem[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface RsMenuItemGroup {
|
|
10
|
+
label: string
|
|
11
|
+
children: RsMenuItem[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type RsMenuItems = RsMenuItem[] | RsMenuItemGroup[]
|
|
15
|
+
|
|
16
|
+
export function isMenuItemGroup(
|
|
17
|
+
item: RsMenuItem | RsMenuItemGroup,
|
|
18
|
+
): item is RsMenuItemGroup {
|
|
19
|
+
return !('key' in item) && 'children' in item
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function findMenuParentKeys(items: RsMenuItems, key: string): string[] {
|
|
23
|
+
function walk(list: RsMenuItem[], ancestors: string[]): string[] | null {
|
|
24
|
+
for (const item of list) {
|
|
25
|
+
if (item.key === key) {
|
|
26
|
+
return ancestors
|
|
27
|
+
}
|
|
28
|
+
if (item.children?.length) {
|
|
29
|
+
const found = walk(item.children, [...ancestors, item.key])
|
|
30
|
+
if (found) {
|
|
31
|
+
return found
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const entry of items) {
|
|
39
|
+
const found = isMenuItemGroup(entry)
|
|
40
|
+
? walk(entry.children, [])
|
|
41
|
+
: walk([entry], [])
|
|
42
|
+
if (found) {
|
|
43
|
+
return found
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return []
|
|
48
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浮层 / 模态 z-index 常量,与 `styles.css` 中 `--rs-z-*` token 对齐。
|
|
3
|
+
* 组件样式应使用 `z-index: var(--rs-z-*)`,禁止硬编码层级。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** 下拉 / Popover / Tooltip 浮层 */
|
|
7
|
+
export const RS_Z_DROPDOWN = 'var(--rs-z-dropdown)'
|
|
8
|
+
|
|
9
|
+
/** Shell 常驻面板(底部 Dock 等),须低于 modal */
|
|
10
|
+
export const RS_Z_PANEL = 'var(--rs-z-panel)'
|
|
11
|
+
|
|
12
|
+
/** 模态对话框 */
|
|
13
|
+
export const RS_Z_MODAL = 'var(--rs-z-modal)'
|
|
14
|
+
|
|
15
|
+
/** 全局 Toast */
|
|
16
|
+
export const RS_Z_TOAST = 'var(--rs-z-toast)'
|
|
17
|
+
|
|
18
|
+
/** 反馈语义色调(Dialog 图标等) */
|
|
19
|
+
export type RsFeedbackTone = 'default' | 'info' | 'success' | 'warning' | 'danger'
|
|
20
|
+
|
|
21
|
+
export type RsToastType = 'success' | 'error' | 'info' | 'warning'
|
|
22
|
+
|
|
23
|
+
export type RsToastPosition =
|
|
24
|
+
| 'top-left'
|
|
25
|
+
| 'top-right'
|
|
26
|
+
| 'bottom-left'
|
|
27
|
+
| 'bottom-right'
|
|
28
|
+
| 'top-center'
|
|
29
|
+
| 'bottom-center'
|
|
30
|
+
|
|
31
|
+
export const RS_TOAST_DEFAULT_POSITION: RsToastPosition = 'top-center'
|
|
32
|
+
|
|
33
|
+
/** 多条 Toast 之间的垂直间距(px),传给 vue-sonner `gap` */
|
|
34
|
+
export const RS_TOAST_DEFAULT_GAP = 4
|
|
35
|
+
|
|
36
|
+
export const rsToastPositions = [
|
|
37
|
+
'top-center',
|
|
38
|
+
'top-left',
|
|
39
|
+
'top-right',
|
|
40
|
+
'bottom-center',
|
|
41
|
+
'bottom-left',
|
|
42
|
+
'bottom-right',
|
|
43
|
+
] as const satisfies readonly RsToastPosition[]
|
|
44
|
+
|
|
45
|
+
export function rsFeedbackIconClass(tone: RsFeedbackTone): string {
|
|
46
|
+
return `rs-feedback-icon rs-feedback-icon--${tone}`
|
|
47
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const DEFAULT_PAGE_SIZE_OPTIONS = [10, 20, 50, 100] as const
|
|
2
|
+
|
|
3
|
+
export function getPageCount(total: number, pageSize: number): number {
|
|
4
|
+
if (pageSize <= 0) return 1
|
|
5
|
+
return Math.max(1, Math.ceil(Math.max(0, total) / pageSize))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function clampPage(page: number, pageCount: number): number {
|
|
9
|
+
return Math.min(Math.max(1, page), Math.max(1, pageCount))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getPaginationRange(page: number, pageCount: number, siblingCount = 1): Array<number | 'ellipsis'> {
|
|
13
|
+
const totalNumbers = siblingCount * 2 + 5
|
|
14
|
+
if (pageCount <= totalNumbers) return Array.from({ length: pageCount }, (_, index) => index + 1)
|
|
15
|
+
|
|
16
|
+
const leftSibling = Math.max(page - siblingCount, 1)
|
|
17
|
+
const rightSibling = Math.min(page + siblingCount, pageCount)
|
|
18
|
+
const showLeftEllipsis = leftSibling > 2
|
|
19
|
+
const showRightEllipsis = rightSibling < pageCount - 1
|
|
20
|
+
|
|
21
|
+
if (!showLeftEllipsis && showRightEllipsis) {
|
|
22
|
+
return [...Array.from({ length: 3 + siblingCount * 2 }, (_, index) => index + 1), 'ellipsis', pageCount]
|
|
23
|
+
}
|
|
24
|
+
if (showLeftEllipsis && !showRightEllipsis) {
|
|
25
|
+
const start = pageCount - (2 + siblingCount * 2)
|
|
26
|
+
return [1, 'ellipsis', ...Array.from({ length: 3 + siblingCount * 2 }, (_, index) => start + index)]
|
|
27
|
+
}
|
|
28
|
+
return [1, 'ellipsis', ...Array.from({ length: rightSibling - leftSibling + 1 }, (_, index) => leftSibling + index), 'ellipsis', pageCount]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function slicePageData<T>(data: readonly T[], page: number, pageSize: number): T[] {
|
|
32
|
+
const start = (clampPage(page, getPageCount(data.length, pageSize)) - 1) * pageSize
|
|
33
|
+
return data.slice(start, start + pageSize)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createPageSizeSelectOptions(options: readonly number[] = DEFAULT_PAGE_SIZE_OPTIONS) {
|
|
37
|
+
return options.map((value) => ({ label: String(value), value: String(value) }))
|
|
38
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export {
|
|
2
|
+
AlertDialogAction,
|
|
3
|
+
AlertDialogCancel,
|
|
4
|
+
AlertDialogContent,
|
|
5
|
+
AlertDialogDescription,
|
|
6
|
+
AlertDialogOverlay,
|
|
7
|
+
AlertDialogPortal,
|
|
8
|
+
AlertDialogRoot,
|
|
9
|
+
AlertDialogTitle,
|
|
10
|
+
AlertDialogTrigger,
|
|
11
|
+
Primitive,
|
|
12
|
+
AvatarFallback,
|
|
13
|
+
AvatarImage,
|
|
14
|
+
AvatarRoot,
|
|
15
|
+
CollapsibleContent,
|
|
16
|
+
CollapsibleRoot,
|
|
17
|
+
CollapsibleTrigger,
|
|
18
|
+
ComboboxAnchor,
|
|
19
|
+
ComboboxContent,
|
|
20
|
+
ComboboxEmpty,
|
|
21
|
+
ComboboxGroup,
|
|
22
|
+
ComboboxInput,
|
|
23
|
+
ComboboxItem,
|
|
24
|
+
ComboboxItemIndicator,
|
|
25
|
+
ComboboxLabel,
|
|
26
|
+
ComboboxPortal,
|
|
27
|
+
ComboboxRoot,
|
|
28
|
+
ComboboxTrigger,
|
|
29
|
+
ComboboxViewport,
|
|
30
|
+
ComboboxVirtualizer,
|
|
31
|
+
ContextMenuContent,
|
|
32
|
+
ContextMenuItem,
|
|
33
|
+
ContextMenuPortal,
|
|
34
|
+
ContextMenuRoot,
|
|
35
|
+
ContextMenuSeparator,
|
|
36
|
+
ContextMenuSub,
|
|
37
|
+
ContextMenuSubContent,
|
|
38
|
+
ContextMenuSubTrigger,
|
|
39
|
+
ContextMenuTrigger,
|
|
40
|
+
DialogClose,
|
|
41
|
+
DialogContent,
|
|
42
|
+
DialogDescription,
|
|
43
|
+
DialogOverlay,
|
|
44
|
+
DialogPortal,
|
|
45
|
+
DialogRoot,
|
|
46
|
+
DialogTitle,
|
|
47
|
+
DialogTrigger,
|
|
48
|
+
DropdownMenuContent,
|
|
49
|
+
DropdownMenuGroup,
|
|
50
|
+
DropdownMenuItem,
|
|
51
|
+
DropdownMenuLabel,
|
|
52
|
+
DropdownMenuPortal,
|
|
53
|
+
DropdownMenuRadioGroup,
|
|
54
|
+
DropdownMenuRadioItem,
|
|
55
|
+
DropdownMenuRoot,
|
|
56
|
+
DropdownMenuTrigger,
|
|
57
|
+
PopoverContent,
|
|
58
|
+
PopoverPortal,
|
|
59
|
+
PopoverRoot,
|
|
60
|
+
PopoverTrigger,
|
|
61
|
+
PaginationEllipsis,
|
|
62
|
+
PaginationFirst,
|
|
63
|
+
PaginationLast,
|
|
64
|
+
PaginationList,
|
|
65
|
+
PaginationListItem,
|
|
66
|
+
PaginationNext,
|
|
67
|
+
PaginationPrev,
|
|
68
|
+
PaginationRoot,
|
|
69
|
+
ScrollAreaCorner,
|
|
70
|
+
ScrollAreaRoot,
|
|
71
|
+
ScrollAreaScrollbar,
|
|
72
|
+
ScrollAreaThumb,
|
|
73
|
+
ScrollAreaViewport,
|
|
74
|
+
SelectContent,
|
|
75
|
+
SelectItem,
|
|
76
|
+
SelectItemIndicator,
|
|
77
|
+
SelectItemText,
|
|
78
|
+
SelectPortal,
|
|
79
|
+
SelectRoot,
|
|
80
|
+
SelectTrigger,
|
|
81
|
+
SelectValue,
|
|
82
|
+
SelectViewport,
|
|
83
|
+
StepperDescription,
|
|
84
|
+
StepperIndicator,
|
|
85
|
+
StepperItem,
|
|
86
|
+
StepperRoot,
|
|
87
|
+
StepperSeparator,
|
|
88
|
+
StepperTitle,
|
|
89
|
+
StepperTrigger,
|
|
90
|
+
TabsContent,
|
|
91
|
+
TabsList,
|
|
92
|
+
TabsRoot,
|
|
93
|
+
TabsTrigger,
|
|
94
|
+
TooltipContent,
|
|
95
|
+
TooltipPortal,
|
|
96
|
+
TooltipProvider,
|
|
97
|
+
TooltipRoot,
|
|
98
|
+
TooltipTrigger,
|
|
99
|
+
TreeItem,
|
|
100
|
+
TreeRoot,
|
|
101
|
+
TreeVirtualizer,
|
|
102
|
+
useForwardExpose,
|
|
103
|
+
useFilter,
|
|
104
|
+
} from 'reka-ui'
|
|
105
|
+
|
|
106
|
+
export type { AcceptableValue, PrimitiveProps, TooltipRootEmits, TooltipRootProps } from 'reka-ui'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { computed, type ComputedRef } from 'vue'
|
|
2
|
+
import { RS_RADIUS_CSS, type RsRadius } from '../theme/types'
|
|
3
|
+
import { useRsConfigOptional } from '../composables/useRsConfig'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 解析圆角:props → ConfigProvider.controlRadius → 组件默认档。
|
|
7
|
+
* 业务应传 `radius` / `iconRadius` 或依赖全局配置,避免 :deep 改 border-radius。
|
|
8
|
+
*/
|
|
9
|
+
export function useResolvedRsRadius(
|
|
10
|
+
radius: () => RsRadius | undefined,
|
|
11
|
+
fallback: RsRadius,
|
|
12
|
+
): ComputedRef<RsRadius> {
|
|
13
|
+
const config = useRsConfigOptional()
|
|
14
|
+
return computed(
|
|
15
|
+
() => radius() ?? config?.controlRadius.value ?? fallback,
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 将档位转为可写入 style / CSS 变量的值。 */
|
|
20
|
+
export function rsRadiusCss(radius: RsRadius): string {
|
|
21
|
+
return RS_RADIUS_CSS[radius]
|
|
22
|
+
}
|