niuma-ui 1.0.0 → 1.0.2
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 +50 -1
- package/NOTICE +2 -2
- package/package.json +13 -5
- package/src/__tests__/RsButton.spec.ts +47 -0
- package/src/__tests__/RsCard.spec.ts +160 -1
- package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
- package/src/__tests__/RsContextMenu.spec.ts +12 -0
- package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
- package/src/__tests__/RsDescriptions.spec.ts +50 -0
- package/src/__tests__/RsDialog.spec.ts +117 -0
- package/src/__tests__/RsDrawer.spec.ts +96 -3
- package/src/__tests__/RsDynamicTags.spec.ts +52 -0
- package/src/__tests__/RsForm.spec.ts +245 -1
- package/src/__tests__/RsInput.spec.ts +15 -1
- package/src/__tests__/RsMenu.spec.ts +406 -14
- package/src/__tests__/RsPagination.spec.ts +67 -2
- package/src/__tests__/RsSelect.spec.ts +42 -1
- package/src/__tests__/RsSwitch.spec.ts +44 -0
- package/src/__tests__/RsTable.a11y.spec.ts +83 -0
- package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
- package/src/__tests__/RsTable.perf.spec.ts +23 -4
- package/src/__tests__/RsTable.spec.ts +63 -8
- package/src/__tests__/RsTable.ssr.spec.ts +71 -0
- package/src/__tests__/RsTable.summary.spec.ts +45 -0
- package/src/__tests__/RsTabs.spec.ts +102 -4
- package/src/__tests__/RsTerminal.spec.ts +1 -1
- package/src/__tests__/RsTimePicker.spec.ts +11 -0
- package/src/__tests__/RsUpload.spec.ts +62 -6
- package/src/__tests__/createRsDialog.spec.ts +145 -0
- package/src/__tests__/form-rules.spec.ts +76 -0
- package/src/__tests__/perf-budgets.ts +32 -0
- package/src/__tests__/rs-table-api.spec.ts +206 -0
- package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
- package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
- package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
- package/src/__tests__/rs-table-isolation.spec.ts +158 -0
- package/src/__tests__/split-pane-utils.spec.ts +21 -0
- package/src/__tests__/table-chart-utils.spec.ts +88 -0
- package/src/__tests__/table-summary-utils.spec.ts +62 -0
- package/src/__tests__/table-utils.spec.ts +124 -0
- package/src/__tests__/theme.spec.ts +25 -0
- package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
- package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
- package/src/components/RsAlert.vue +212 -0
- package/src/components/RsButton.vue +145 -21
- package/src/components/RsCard.vue +200 -16
- package/src/components/RsConfirmDialog.vue +215 -28
- package/src/components/RsContextMenu.vue +51 -13
- package/src/components/RsDatePicker.vue +267 -34
- package/src/components/RsDateTimePicker.vue +19 -2
- package/src/components/RsDescriptions.vue +186 -0
- package/src/components/RsDescriptionsItem.vue +32 -0
- package/src/components/RsDialog.vue +363 -51
- package/src/components/RsDivider.vue +89 -0
- package/src/components/RsDrawer.vue +558 -33
- package/src/components/RsDropdown.vue +33 -2
- package/src/components/RsDynamicTags.vue +517 -0
- package/src/components/RsForm.vue +97 -8
- package/src/components/RsInput.vue +80 -82
- package/src/components/RsInputNumber.vue +64 -23
- package/src/components/RsLabel.vue +20 -5
- package/src/components/RsLoadingBar.vue +151 -0
- package/src/components/RsMenu.vue +73 -15
- package/src/components/RsMenuItems.vue +147 -9
- package/src/components/RsPagination.vue +93 -14
- package/src/components/RsRadio.vue +81 -0
- package/src/components/RsRadioItem.vue +156 -0
- package/src/components/RsSelect.vue +67 -58
- package/src/components/RsSplitPane.vue +69 -1
- package/src/components/RsSwitch.vue +179 -0
- package/src/components/RsTable.vue +653 -2555
- package/src/components/RsTabs.vue +415 -83
- package/src/components/RsTag.vue +191 -0
- package/src/components/RsTerminal.vue +3 -3
- package/src/components/RsTimePicker.vue +44 -5
- package/src/components/RsTimePickerColumns.vue +136 -77
- package/src/components/RsToaster.vue +58 -18
- package/src/components/RsTree.vue +39 -21
- package/src/components/RsUpload.vue +145 -12
- package/src/components/RsVNodeHost.vue +20 -0
- package/src/components/button-utils.ts +39 -0
- package/src/components/context-menu-utils.ts +6 -0
- package/src/components/date-picker-utils.ts +84 -4
- package/src/components/descriptions-utils.ts +24 -0
- package/src/components/dialog-utils.ts +147 -0
- package/src/components/dialog-window.ts +74 -9
- package/src/components/drawer-utils.ts +73 -0
- package/src/components/form-rules.ts +257 -0
- package/src/components/form-utils.ts +52 -2
- package/src/components/menu-utils.ts +11 -0
- package/src/components/overlay-utils.ts +3 -0
- package/src/components/radio-utils.ts +12 -0
- package/src/components/reka.ts +5 -0
- package/src/components/select-utils.ts +21 -2
- package/src/components/split-pane-utils.ts +65 -12
- package/src/components/table/RsTableBody.vue +217 -0
- package/src/components/table/RsTableBodyRow.vue +90 -1
- package/src/components/table/RsTableColGroup.vue +41 -0
- package/src/components/table/RsTableHeader.vue +158 -0
- package/src/components/table/RsTableSummaryRow.vue +52 -0
- package/src/components/table/rs-table-api.ts +99 -0
- package/src/components/table/rs-table-compat-matrix.ts +71 -0
- package/src/components/table/rs-table-echarts-adapter.ts +81 -0
- package/src/components/table/rs-table-feature-host.ts +178 -0
- package/src/components/table/rs-table-grid-nav.ts +122 -0
- package/src/components/table/rs-table-header-types.ts +47 -0
- package/src/components/table/rs-table-module-registry.ts +72 -0
- package/src/components/table/rs-table-props.ts +274 -0
- package/src/components/table/rs-table-view-context.ts +258 -0
- package/src/components/table/rs-table.css +84 -6
- package/src/components/table/table-chart-utils.ts +165 -0
- package/src/components/table/table-features.ts +284 -0
- package/src/components/table/table-summary-utils.ts +194 -0
- package/src/components/table-utils.ts +452 -2
- package/src/components/tabs-utils.ts +142 -2
- package/src/components/terminal-utils.ts +1 -1
- package/src/components/terminal-wheel.ts +1 -1
- package/src/components/time-picker-utils.ts +76 -3
- package/src/components/upload-utils.ts +122 -0
- package/src/components/virtual-list-utils.ts +15 -2
- package/src/composables/assembleRsTableApi.ts +79 -0
- package/src/composables/bindRsTableViewContext.ts +269 -0
- package/src/composables/createRsDialog.ts +357 -0
- package/src/composables/createRsTableEditEmitBridge.ts +38 -0
- package/src/composables/useRsLoadingBar.ts +36 -0
- package/src/composables/useRsTable.ts +134 -0
- package/src/composables/useRsTableA11y.ts +52 -0
- package/src/composables/useRsTableChartBridge.ts +79 -0
- package/src/composables/useRsTableColumnLayout.ts +320 -0
- package/src/composables/useRsTableColumnResize.ts +244 -0
- package/src/composables/useRsTableColumnVirtual.ts +180 -0
- package/src/composables/useRsTableColumns.ts +200 -0
- package/src/composables/useRsTableContextMenu.ts +257 -0
- package/src/composables/useRsTableCore.ts +256 -0
- package/src/composables/useRsTableEditActions.ts +606 -0
- package/src/composables/useRsTableEditLayer.ts +251 -0
- package/src/composables/useRsTableEngine.ts +357 -0
- package/src/composables/useRsTableGridKeyboard.ts +123 -0
- package/src/composables/useRsTableHeadless.ts +137 -0
- package/src/composables/useRsTableInteraction.ts +433 -0
- package/src/composables/useRsTableScrollHost.ts +75 -0
- package/src/composables/useRsTableScrollLayout.ts +255 -0
- package/src/composables/useRsTableSelectionSource.ts +89 -0
- package/src/composables/useRsTableShell.ts +416 -0
- package/src/composables/useRsTableShellChrome.ts +170 -0
- package/src/composables/useRsTableSummary.ts +113 -0
- package/src/composables/useRsTableViewProvide.ts +263 -0
- package/src/composables/useRsTableVirtual.ts +154 -0
- package/src/composables/useRsTabsNav.ts +4 -0
- package/src/composables/useTableEdit.ts +1 -1
- package/src/icons/lucide.ts +2 -2
- package/src/index.ts +309 -9
- package/src/locale/messages.ts +30 -0
- package/src/styles.css +305 -52
- package/src/theme/brand.example.css +21 -2
- package/src/theme/presets.ts +4 -0
- package/src/theme/types.ts +67 -0
- package/src/components/RsTableColgroup.vue +0 -17
- package/src/components/RsTableHeaderRow.vue +0 -30
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import type { RsTranslateFn } from '../composables/useRsI18n'
|
|
2
|
+
import { createTranslator } from '../composables/useRsI18n'
|
|
3
|
+
import { defaultLocale } from '../locale/types'
|
|
4
|
+
import {
|
|
5
|
+
getInputRuleMessage,
|
|
6
|
+
validateInputRule,
|
|
7
|
+
type RsInputRule,
|
|
8
|
+
} from './input-rules'
|
|
9
|
+
|
|
10
|
+
/** 校验触发时机(对齐 Ant Design Form) */
|
|
11
|
+
export type RsFormRuleTrigger = 'blur' | 'change' | 'submit'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 单条表单规则(声明式 + 自定义校验)。
|
|
15
|
+
* 对标 Ant Design / Element Plus Form rules 的常用子集。
|
|
16
|
+
*/
|
|
17
|
+
/** 规则文案:静态字符串,或按当前值动态生成(对齐 Ant Design message 函数能力) */
|
|
18
|
+
export type RsFormRuleMessage =
|
|
19
|
+
| string
|
|
20
|
+
| ((ctx: { value: unknown; rule: RsFormRuleItem }) => string)
|
|
21
|
+
|
|
22
|
+
export interface RsFormRuleItem {
|
|
23
|
+
required?: boolean
|
|
24
|
+
/**
|
|
25
|
+
* 错误文案;未传时使用内置 i18n。
|
|
26
|
+
* 可传函数按当前值动态生成文案。
|
|
27
|
+
*/
|
|
28
|
+
message?: RsFormRuleMessage
|
|
29
|
+
/** 字符串长度下限 / 数值下限 */
|
|
30
|
+
min?: number
|
|
31
|
+
/** 字符串长度上限 / 数值上限 */
|
|
32
|
+
max?: number
|
|
33
|
+
/** 字符串精确长度 */
|
|
34
|
+
len?: number
|
|
35
|
+
pattern?: RegExp
|
|
36
|
+
/** 内置类型:email/phone 等,或 string/number/array */
|
|
37
|
+
type?: RsInputRule | 'string' | 'number' | 'array'
|
|
38
|
+
/**
|
|
39
|
+
* 自定义校验。返回 true 通过;false / 字符串为失败;支持 Promise。
|
|
40
|
+
*/
|
|
41
|
+
validator?: (
|
|
42
|
+
value: unknown,
|
|
43
|
+
) => boolean | string | void | Promise<boolean | string | void>
|
|
44
|
+
trigger?: RsFormRuleTrigger | RsFormRuleTrigger[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 字段名 → 规则(单条或数组) */
|
|
48
|
+
export type RsFormRules = Record<string, RsFormRuleItem | RsFormRuleItem[]>
|
|
49
|
+
|
|
50
|
+
export interface RsFormRuleValidateResult {
|
|
51
|
+
valid: boolean
|
|
52
|
+
message?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function normalizeFormRules(
|
|
56
|
+
rules?: RsFormRuleItem | RsFormRuleItem[] | null,
|
|
57
|
+
): RsFormRuleItem[] {
|
|
58
|
+
if (!rules) return []
|
|
59
|
+
return Array.isArray(rules) ? rules : [rules]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function matchFormRuleTrigger(
|
|
63
|
+
rule: RsFormRuleItem,
|
|
64
|
+
trigger?: RsFormRuleTrigger,
|
|
65
|
+
): boolean {
|
|
66
|
+
if (!trigger || trigger === 'submit') return true
|
|
67
|
+
const ruleTrigger = rule.trigger
|
|
68
|
+
if (!ruleTrigger) return true
|
|
69
|
+
const list = Array.isArray(ruleTrigger) ? ruleTrigger : [ruleTrigger]
|
|
70
|
+
return list.includes(trigger)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isEmptyValue(value: unknown): boolean {
|
|
74
|
+
if (value == null) return true
|
|
75
|
+
if (typeof value === 'string') return value.trim() === ''
|
|
76
|
+
if (Array.isArray(value)) return value.length === 0
|
|
77
|
+
// 日期区间 { start, end }:两端皆空视为空
|
|
78
|
+
if (typeof value === 'object') {
|
|
79
|
+
const record = value as Record<string, unknown>
|
|
80
|
+
if ('start' in record || 'end' in record) {
|
|
81
|
+
return isEmptyValue(record.start) && isEmptyValue(record.end)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function valueLength(value: unknown): number | null {
|
|
88
|
+
if (typeof value === 'string') return value.trim().length
|
|
89
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
90
|
+
if (Array.isArray(value)) return value.length
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function fail(message: string): RsFormRuleValidateResult {
|
|
95
|
+
return { valid: false, message }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 解析规则 message(字符串或函数) */
|
|
99
|
+
export function resolveRuleMessage(
|
|
100
|
+
rule: RsFormRuleItem,
|
|
101
|
+
value: unknown,
|
|
102
|
+
fallback: string,
|
|
103
|
+
): string {
|
|
104
|
+
const message = rule.message
|
|
105
|
+
if (typeof message === 'function') return message({ value, rule })
|
|
106
|
+
if (typeof message === 'string') return message
|
|
107
|
+
return fallback
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function formatInvalid(
|
|
111
|
+
rule: RsFormRuleItem,
|
|
112
|
+
value: unknown,
|
|
113
|
+
tr: RsTranslateFn,
|
|
114
|
+
): string {
|
|
115
|
+
return resolveRuleMessage(rule, value, tr('input.formatInvalid'))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 类型规则:string/number/array 或内置 email/phone 等 */
|
|
119
|
+
function checkTypeRule(
|
|
120
|
+
value: unknown,
|
|
121
|
+
rule: RsFormRuleItem,
|
|
122
|
+
tr: RsTranslateFn,
|
|
123
|
+
): RsFormRuleValidateResult | null {
|
|
124
|
+
const type = rule.type
|
|
125
|
+
if (!type) return null
|
|
126
|
+
|
|
127
|
+
if (type === 'string' && typeof value !== 'string') {
|
|
128
|
+
return fail(formatInvalid(rule, value, tr))
|
|
129
|
+
}
|
|
130
|
+
if (type === 'number' && typeof value !== 'number') {
|
|
131
|
+
return fail(formatInvalid(rule, value, tr))
|
|
132
|
+
}
|
|
133
|
+
if (type === 'array' && !Array.isArray(value)) {
|
|
134
|
+
return fail(formatInvalid(rule, value, tr))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const isBuiltin =
|
|
138
|
+
type !== 'string' && type !== 'number' && type !== 'array'
|
|
139
|
+
if (isBuiltin && typeof value === 'string' && !validateInputRule(value, type)) {
|
|
140
|
+
return fail(
|
|
141
|
+
resolveRuleMessage(rule, value, getInputRuleMessage(type, tr)),
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function checkPatternRule(
|
|
149
|
+
value: unknown,
|
|
150
|
+
rule: RsFormRuleItem,
|
|
151
|
+
tr: RsTranslateFn,
|
|
152
|
+
): RsFormRuleValidateResult | null {
|
|
153
|
+
if (!rule.pattern || typeof value !== 'string') return null
|
|
154
|
+
if (rule.pattern.test(value)) return null
|
|
155
|
+
return fail(formatInvalid(rule, value, tr))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function checkLengthRule(
|
|
159
|
+
value: unknown,
|
|
160
|
+
rule: RsFormRuleItem,
|
|
161
|
+
tr: RsTranslateFn,
|
|
162
|
+
): RsFormRuleValidateResult | null {
|
|
163
|
+
const length = valueLength(value)
|
|
164
|
+
if (length == null) return null
|
|
165
|
+
|
|
166
|
+
if (rule.len != null && length !== rule.len) {
|
|
167
|
+
return fail(formatInvalid(rule, value, tr))
|
|
168
|
+
}
|
|
169
|
+
if (rule.min != null && length < rule.min) {
|
|
170
|
+
return fail(formatInvalid(rule, value, tr))
|
|
171
|
+
}
|
|
172
|
+
if (rule.max != null && length > rule.max) {
|
|
173
|
+
return fail(formatInvalid(rule, value, tr))
|
|
174
|
+
}
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function checkValidatorRule(
|
|
179
|
+
value: unknown,
|
|
180
|
+
rule: RsFormRuleItem,
|
|
181
|
+
tr: RsTranslateFn,
|
|
182
|
+
): Promise<RsFormRuleValidateResult | null> {
|
|
183
|
+
if (!rule.validator) return null
|
|
184
|
+
|
|
185
|
+
const result = await rule.validator(value)
|
|
186
|
+
if (result === true || result === undefined || result === null) return null
|
|
187
|
+
if (result === false) return fail(formatInvalid(rule, value, tr))
|
|
188
|
+
if (typeof result === 'string') return fail(result)
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 执行单条规则;通过返回 null,失败返回结果。
|
|
194
|
+
* 空值且非必填时跳过格式类规则。
|
|
195
|
+
*/
|
|
196
|
+
async function evaluateFormRule(
|
|
197
|
+
value: unknown,
|
|
198
|
+
rule: RsFormRuleItem,
|
|
199
|
+
tr: RsTranslateFn,
|
|
200
|
+
): Promise<RsFormRuleValidateResult | null> {
|
|
201
|
+
if (rule.required && isEmptyValue(value)) {
|
|
202
|
+
return fail(resolveRuleMessage(rule, value, tr('input.required')))
|
|
203
|
+
}
|
|
204
|
+
if (isEmptyValue(value)) return null
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
checkTypeRule(value, rule, tr) ??
|
|
208
|
+
checkPatternRule(value, rule, tr) ??
|
|
209
|
+
checkLengthRule(value, rule, tr) ??
|
|
210
|
+
(await checkValidatorRule(value, rule, tr))
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 按顺序执行字段规则,首条失败即返回。
|
|
216
|
+
*/
|
|
217
|
+
export async function runFormFieldRules(
|
|
218
|
+
value: unknown,
|
|
219
|
+
rules: RsFormRuleItem[],
|
|
220
|
+
options?: {
|
|
221
|
+
trigger?: RsFormRuleTrigger
|
|
222
|
+
t?: RsTranslateFn
|
|
223
|
+
},
|
|
224
|
+
): Promise<RsFormRuleValidateResult> {
|
|
225
|
+
const tr = options?.t ?? createTranslator(defaultLocale)
|
|
226
|
+
const trigger = options?.trigger
|
|
227
|
+
|
|
228
|
+
for (const rule of rules) {
|
|
229
|
+
if (!matchFormRuleTrigger(rule, trigger)) continue
|
|
230
|
+
const failed = await evaluateFormRule(value, rule, tr)
|
|
231
|
+
if (failed) return failed
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { valid: true }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** 将 Input 局部 props 转为规则列表,便于与 Form rules 合并 */
|
|
238
|
+
export function buildLocalInputRules(options: {
|
|
239
|
+
required?: boolean
|
|
240
|
+
rule?: RsInputRule
|
|
241
|
+
validator?: (value: string) => boolean | string
|
|
242
|
+
requiredMessage?: string
|
|
243
|
+
}): RsFormRuleItem[] {
|
|
244
|
+
const list: RsFormRuleItem[] = []
|
|
245
|
+
if (options.required) {
|
|
246
|
+
list.push({ required: true, message: options.requiredMessage })
|
|
247
|
+
}
|
|
248
|
+
if (options.rule) {
|
|
249
|
+
list.push({ type: options.rule })
|
|
250
|
+
}
|
|
251
|
+
if (options.validator) {
|
|
252
|
+
list.push({
|
|
253
|
+
validator: (value) => options.validator!(String(value ?? '')),
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
return list
|
|
257
|
+
}
|
|
@@ -7,7 +7,25 @@ import {
|
|
|
7
7
|
type ComputedRef,
|
|
8
8
|
type InjectionKey,
|
|
9
9
|
type Ref,
|
|
10
|
+
type VNodeChild,
|
|
10
11
|
} from 'vue'
|
|
12
|
+
import type { RsFormRuleItem, RsFormRuleTrigger, RsFormRules } from './form-rules'
|
|
13
|
+
import { normalizeFormRules } from './form-rules'
|
|
14
|
+
|
|
15
|
+
/** 自定义错误渲染上下文(Form / 字段共用) */
|
|
16
|
+
export interface RsFormErrorRenderContext {
|
|
17
|
+
name?: string
|
|
18
|
+
message: string
|
|
19
|
+
value: unknown
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 自定义错误渲染(对齐 Ant Design Form.Item help / Element #error)。
|
|
24
|
+
* 返回 VNode / 字符串;返回 null/undefined 时回退默认文案。
|
|
25
|
+
*/
|
|
26
|
+
export type RsFormErrorRender = (
|
|
27
|
+
ctx: RsFormErrorRenderContext,
|
|
28
|
+
) => VNodeChild | null | undefined
|
|
11
29
|
|
|
12
30
|
export type RsFormLabelPosition = 'top' | 'left'
|
|
13
31
|
export type RsFormLabelAlign = 'start' | 'end'
|
|
@@ -15,15 +33,30 @@ export type RsFormSize = 'ssm' | 'sm' | 'md' | 'lg'
|
|
|
15
33
|
export type RsFormGap = 'sm' | 'md' | 'lg'
|
|
16
34
|
export type RsFormMaxWidth = 'sm' | 'md' | 'lg' | 'full' | 'none'
|
|
17
35
|
|
|
36
|
+
/** 整表校验结果(含按字段名聚合的错误) */
|
|
18
37
|
export interface RsFormValidationResult {
|
|
19
38
|
valid: boolean
|
|
39
|
+
/** 字段名 → 首条错误文案;无 name 的字段不计入 */
|
|
40
|
+
errors: Record<string, string>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RsFormFieldValidationResult {
|
|
44
|
+
valid: boolean
|
|
45
|
+
message?: string
|
|
46
|
+
name?: string
|
|
20
47
|
}
|
|
21
48
|
|
|
22
49
|
export interface RsFormFieldExpose {
|
|
50
|
+
/** 字段名,用于匹配 Form.rules */
|
|
51
|
+
name?: string
|
|
23
52
|
getValue: () => unknown
|
|
24
53
|
setValue: (value: unknown) => void
|
|
25
|
-
validate?: (
|
|
54
|
+
validate?: (
|
|
55
|
+
trigger?: RsFormRuleTrigger,
|
|
56
|
+
) => RsFormFieldValidationResult | Promise<RsFormFieldValidationResult>
|
|
26
57
|
clearValidation?: () => void
|
|
58
|
+
/** 由 Form 或外部写入错误态 */
|
|
59
|
+
setError?: (message: string) => void
|
|
27
60
|
}
|
|
28
61
|
|
|
29
62
|
export interface RsFormContext {
|
|
@@ -32,6 +65,11 @@ export interface RsFormContext {
|
|
|
32
65
|
labelWidth: Readonly<Ref<string>>
|
|
33
66
|
labelAlign: Readonly<Ref<RsFormLabelAlign>>
|
|
34
67
|
size: Readonly<Ref<RsFormSize>>
|
|
68
|
+
/** 当前 Form 上的 rules(响应式读取) */
|
|
69
|
+
rules: Readonly<Ref<RsFormRules | undefined>>
|
|
70
|
+
getFieldRules: (name?: string) => RsFormRuleItem[]
|
|
71
|
+
/** Form 级自定义错误渲染;字段无 #error 时使用 */
|
|
72
|
+
renderError?: RsFormErrorRender
|
|
35
73
|
registerField: (id: symbol, field: RsFormFieldExpose) => void
|
|
36
74
|
unregisterField: (id: symbol) => void
|
|
37
75
|
}
|
|
@@ -72,6 +110,18 @@ export function useRsFormField(getExpose: () => RsFormFieldExpose | undefined):
|
|
|
72
110
|
})
|
|
73
111
|
}
|
|
74
112
|
|
|
75
|
-
export function resolveRsFormSize(
|
|
113
|
+
export function resolveRsFormSize(
|
|
114
|
+
size?: RsFormSize,
|
|
115
|
+
form?: RsFormContext | null,
|
|
116
|
+
): ComputedRef<RsFormSize> {
|
|
76
117
|
return computed(() => size ?? form?.size.value ?? 'md')
|
|
77
118
|
}
|
|
119
|
+
|
|
120
|
+
/** 供 Form 内部解析某字段 rules */
|
|
121
|
+
export function resolveFieldRules(
|
|
122
|
+
rules: RsFormRules | undefined,
|
|
123
|
+
name?: string,
|
|
124
|
+
): RsFormRuleItem[] {
|
|
125
|
+
if (!name || !rules) return []
|
|
126
|
+
return normalizeFormRules(rules[name])
|
|
127
|
+
}
|
|
@@ -46,3 +46,14 @@ export function findMenuParentKeys(items: RsMenuItems, key: string): string[] {
|
|
|
46
46
|
|
|
47
47
|
return []
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
/** 判断菜单项自身或其任意后代是否匹配 key(用于父级高亮) */
|
|
51
|
+
export function menuItemContainsKey(item: RsMenuItem, key: string): boolean {
|
|
52
|
+
if (item.key === key) {
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
if (!item.children?.length) {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
return item.children.some((child) => menuItemContainsKey(child, key))
|
|
59
|
+
}
|
|
@@ -12,6 +12,9 @@ export const RS_Z_PANEL = 'var(--rs-z-panel)'
|
|
|
12
12
|
/** 模态对话框 */
|
|
13
13
|
export const RS_Z_MODAL = 'var(--rs-z-modal)'
|
|
14
14
|
|
|
15
|
+
/** 顶栏加载进度条(低于 toast,避免遮挡通知) */
|
|
16
|
+
export const RS_Z_LOADING_BAR = 'var(--rs-z-loading-bar)'
|
|
17
|
+
|
|
15
18
|
/** 全局 Toast */
|
|
16
19
|
export const RS_Z_TOAST = 'var(--rs-z-toast)'
|
|
17
20
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { InjectionKey, Ref } from 'vue'
|
|
2
|
+
import type { RsComponentSize } from '../theme/types'
|
|
3
|
+
|
|
4
|
+
/** 单选值类型 */
|
|
5
|
+
export type RsRadioValue = string | number | boolean
|
|
6
|
+
|
|
7
|
+
/** RadioGroup 向下注入的上下文 */
|
|
8
|
+
export type RsRadioGroupContext = {
|
|
9
|
+
size: Ref<RsComponentSize>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const RS_RADIO_GROUP_KEY: InjectionKey<RsRadioGroupContext> = Symbol('rs-radio-group')
|
package/src/components/reka.ts
CHANGED
|
@@ -11,6 +11,22 @@ export interface RsSelectOptionGroup {
|
|
|
11
11
|
|
|
12
12
|
export type RsSelectOptions = ReadonlyArray<RsSelectOption | RsSelectOptionGroup>
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Reka ComboboxItem 禁止 value 为空串(空串表示未选中 / placeholder)。
|
|
16
|
+
* 选项若传入 value: '',对内映射为此哨兵,避免崩溃;对外读写仍为 ''。
|
|
17
|
+
*/
|
|
18
|
+
export const RS_SELECT_EMPTY_VALUE = '__rs_select_empty__'
|
|
19
|
+
|
|
20
|
+
/** 业务 value → ComboboxItem value(仅处理空串) */
|
|
21
|
+
export function toComboboxValue(value: string): string {
|
|
22
|
+
return value === '' ? RS_SELECT_EMPTY_VALUE : value
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** ComboboxItem value → 业务 value(仅处理空串哨兵) */
|
|
26
|
+
export function fromComboboxValue(value: string): string {
|
|
27
|
+
return value === RS_SELECT_EMPTY_VALUE ? '' : value
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
export function isSelectOptionGroup(
|
|
15
31
|
item: RsSelectOption | RsSelectOptionGroup,
|
|
16
32
|
): item is RsSelectOptionGroup {
|
|
@@ -33,6 +49,7 @@ export function buildOptionLabelMap(options: RsSelectOptions): Map<string, strin
|
|
|
33
49
|
const map = new Map<string, string>()
|
|
34
50
|
for (const opt of flattenSelectOptions(options)) {
|
|
35
51
|
map.set(opt.value, opt.label)
|
|
52
|
+
if (opt.value === '') map.set(RS_SELECT_EMPTY_VALUE, opt.label)
|
|
36
53
|
}
|
|
37
54
|
return map
|
|
38
55
|
}
|
|
@@ -40,7 +57,9 @@ export function buildOptionLabelMap(options: RsSelectOptions): Map<string, strin
|
|
|
40
57
|
export function buildOptionDisabledMap(options: RsSelectOptions): Map<string, boolean> {
|
|
41
58
|
const map = new Map<string, boolean>()
|
|
42
59
|
for (const opt of flattenSelectOptions(options)) {
|
|
43
|
-
|
|
60
|
+
const disabled = Boolean(opt.disabled)
|
|
61
|
+
map.set(opt.value, disabled)
|
|
62
|
+
if (opt.value === '') map.set(RS_SELECT_EMPTY_VALUE, disabled)
|
|
44
63
|
}
|
|
45
64
|
return map
|
|
46
65
|
}
|
|
@@ -68,5 +87,5 @@ export function filterSelectOptions(
|
|
|
68
87
|
}
|
|
69
88
|
|
|
70
89
|
export function flattenSelectValues(options: RsSelectOptions): string[] {
|
|
71
|
-
return flattenSelectOptions(options).map((opt) => opt.value)
|
|
90
|
+
return flattenSelectOptions(options).map((opt) => toComboboxValue(opt.value))
|
|
72
91
|
}
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
/** 面板分割方向:horizontal 左右排布(拖动横向)· vertical 上下排布(拖动纵向) */
|
|
2
2
|
export type RsSplitOrientation = 'horizontal' | 'vertical'
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/** 面板尺寸:数字为占比百分比;'auto' 按内容自适应(不占百分比配额) */
|
|
5
|
+
export type RsSplitPaneSize = number | 'auto'
|
|
6
|
+
|
|
7
|
+
/** 单个面板定义;百分比尺寸单位为 0~100 */
|
|
5
8
|
export interface RsSplitPaneItem {
|
|
6
9
|
/** 唯一标识,用于插槽名与 v-model:sizes 的顺序映射 */
|
|
7
10
|
key: string
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* 初始尺寸
|
|
13
|
+
* - number:占比百分比
|
|
14
|
+
* - 'auto':按内容自适应(flex-basis: auto)
|
|
15
|
+
* - 缺省:在「非 auto」面板的剩余百分比内均分
|
|
16
|
+
*/
|
|
17
|
+
size?: RsSplitPaneSize
|
|
18
|
+
/** 最小尺寸(百分比),默认 0;auto 面板可作为 min-height/min-width % */
|
|
11
19
|
min?: number
|
|
12
|
-
/** 最大尺寸(百分比),默认 100 */
|
|
20
|
+
/** 最大尺寸(百分比),默认 100;auto 面板可作为 max-height/max-width % */
|
|
13
21
|
max?: number
|
|
14
22
|
/** 是否可折叠:拖动越过阈值时吸附到 collapsedSize */
|
|
15
23
|
collapsible?: boolean
|
|
@@ -36,6 +44,16 @@ export function roundSize(value: number, precision = 4): number {
|
|
|
36
44
|
return Math.round(value * factor) / factor
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
/** 是否为内容自适应尺寸 */
|
|
48
|
+
export function isRsSplitPaneAutoSize(size: RsSplitPaneSize | undefined): size is 'auto' {
|
|
49
|
+
return size === 'auto'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 各面板是否声明为 auto(忽略受控 provided 时的瞬时物化) */
|
|
53
|
+
export function resolveSplitAutoFlags(panes: RsSplitPaneItem[]): boolean[] {
|
|
54
|
+
return panes.map((pane) => isRsSplitPaneAutoSize(pane.size))
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
/** 将面板定义解析为规范化约束,保证 0 ≤ min ≤ max ≤ 100 且 collapsedSize ≤ min */
|
|
40
58
|
export function resolveSplitConstraints(panes: RsSplitPaneItem[]): RsSplitConstraint[] {
|
|
41
59
|
return panes.map((pane) => {
|
|
@@ -65,8 +83,8 @@ function rescaleTo100(sizes: number[]): number[] {
|
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
/**
|
|
68
|
-
*
|
|
69
|
-
* 优先使用 provided(受控值),其次面板自身的 size
|
|
86
|
+
* 计算初始尺寸数组(非 auto 面板总和 100;auto 位为 0 占位,样式层不使用 flex-grow)。
|
|
87
|
+
* 优先使用 provided(受控值),其次面板自身的 number size,未指定的非 auto 项在剩余空间内均分。
|
|
70
88
|
*/
|
|
71
89
|
export function normalizeSplitSizes(
|
|
72
90
|
panes: RsSplitPaneItem[],
|
|
@@ -75,18 +93,53 @@ export function normalizeSplitSizes(
|
|
|
75
93
|
const count = panes.length
|
|
76
94
|
if (count === 0) return []
|
|
77
95
|
|
|
96
|
+
const autoFlags = resolveSplitAutoFlags(panes)
|
|
97
|
+
// 受控 sizes 一旦提供完整数组,视为已全部物化为百分比(含原 auto)
|
|
98
|
+
const fullyControlled =
|
|
99
|
+
Array.isArray(provided) &&
|
|
100
|
+
provided.length === count &&
|
|
101
|
+
provided.every((value) => typeof value === 'number' && Number.isFinite(value))
|
|
102
|
+
|
|
103
|
+
if (fullyControlled) {
|
|
104
|
+
return rescaleTo100(provided!.map((value) => Math.max(0, value))).map((value) =>
|
|
105
|
+
roundSize(value),
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
78
109
|
const seed: (number | null)[] = panes.map((pane, index) => {
|
|
79
|
-
|
|
110
|
+
if (autoFlags[index]) return 0
|
|
111
|
+
const candidate = provided?.[index] ?? (typeof pane.size === 'number' ? pane.size : null)
|
|
80
112
|
return typeof candidate === 'number' && Number.isFinite(candidate)
|
|
81
113
|
? Math.max(0, candidate)
|
|
82
114
|
: null
|
|
83
115
|
})
|
|
84
116
|
|
|
85
|
-
const
|
|
86
|
-
|
|
117
|
+
const flexibleIndices = panes
|
|
118
|
+
.map((_, index) => index)
|
|
119
|
+
.filter((index) => !autoFlags[index])
|
|
120
|
+
|
|
121
|
+
if (flexibleIndices.length === 0) {
|
|
122
|
+
// 全部 auto:占位全 0,由内容撑开
|
|
123
|
+
return panes.map(() => 0)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const missing = flexibleIndices.filter((index) => seed[index] === null).length
|
|
127
|
+
const knownSum = flexibleIndices.reduce<number>((acc, index) => acc + (seed[index] ?? 0), 0)
|
|
87
128
|
const each = missing > 0 ? Math.max(0, 100 - knownSum) / missing : 0
|
|
88
|
-
|
|
89
|
-
|
|
129
|
+
|
|
130
|
+
const filled = seed.map((value, index) => {
|
|
131
|
+
if (autoFlags[index]) return 0
|
|
132
|
+
return value ?? each
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// 只对非 auto 部分缩放到 100
|
|
136
|
+
const flexibleValues = flexibleIndices.map((index) => filled[index] ?? 0)
|
|
137
|
+
const scaledFlexible = rescaleTo100(flexibleValues)
|
|
138
|
+
const next = filled.slice()
|
|
139
|
+
flexibleIndices.forEach((paneIndex, i) => {
|
|
140
|
+
next[paneIndex] = roundSize(scaledFlexible[i] ?? 0)
|
|
141
|
+
})
|
|
142
|
+
return next
|
|
90
143
|
}
|
|
91
144
|
|
|
92
145
|
/** 判断某面板当前是否处于折叠态 */
|