niuma-ui 1.0.2 → 1.1.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 +28 -1
- package/package.json +1 -1
- package/src/__tests__/RsDatePicker.spec.ts +21 -0
- package/src/__tests__/RsDialog.spec.ts +61 -0
- package/src/__tests__/RsForm.spec.ts +5 -5
- package/src/__tests__/RsFormItem.spec.ts +478 -0
- package/src/__tests__/RsFormList.spec.ts +109 -0
- package/src/__tests__/RsInput.spec.ts +93 -0
- package/src/__tests__/RsInputNumber.spec.ts +33 -0
- package/src/__tests__/RsSelect.spec.ts +423 -1
- package/src/__tests__/RsSwitch.spec.ts +30 -0
- package/src/__tests__/RsTimePicker.spec.ts +9 -0
- package/src/__tests__/RsTooltip.spec.ts +43 -0
- package/src/__tests__/css-token.spec.ts +51 -0
- package/src/__tests__/date-picker-utils.spec.ts +49 -0
- package/src/__tests__/dialog-utils.spec.ts +43 -0
- package/src/__tests__/form-path.spec.ts +46 -0
- package/src/__tests__/form-rules.spec.ts +63 -0
- package/src/__tests__/theme.spec.ts +2 -0
- package/src/components/RsAlert.vue +1 -1
- package/src/components/RsAvatar.vue +1 -1
- package/src/components/RsBadge.vue +1 -1
- package/src/components/RsBreadcrumb.vue +1 -1
- package/src/components/RsButton.vue +4 -4
- package/src/components/RsCalendarGrid.vue +3 -3
- package/src/components/RsCard.vue +4 -4
- package/src/components/RsCodeBlock.vue +4 -4
- package/src/components/RsCodeEditor.vue +8 -8
- package/src/components/RsConfirmDialog.vue +2 -2
- package/src/components/RsContextMenu.vue +6 -4
- package/src/components/RsDatePicker.vue +75 -31
- package/src/components/RsDateTimePicker.vue +9 -1
- package/src/components/RsDescriptions.vue +2 -2
- package/src/components/RsDialog.vue +26 -16
- package/src/components/RsDrawer.vue +1 -1
- package/src/components/RsDropdown.vue +1 -1
- package/src/components/RsDynamicTags.vue +18 -4
- package/src/components/RsEmpty.vue +1 -1
- package/src/components/RsForm.vue +68 -1
- package/src/components/RsFormItem.vue +423 -0
- package/src/components/RsFormItemControl.vue +21 -0
- package/src/components/RsFormList.vue +117 -0
- package/src/components/RsInput.vue +411 -18
- package/src/components/RsInputNumber.vue +13 -4
- package/src/components/RsLink.vue +1 -1
- package/src/components/RsMarkdown.vue +5 -5
- package/src/components/RsMenu.vue +2 -2
- package/src/components/RsMonacoEditor.vue +9 -3
- package/src/components/RsPagination.vue +1 -1
- package/src/components/RsProseEditor.vue +3 -3
- package/src/components/RsSelect.vue +332 -611
- package/src/components/RsSidebarGroup.vue +1 -1
- package/src/components/RsSplitPane.vue +22 -1
- package/src/components/RsStatCard.vue +2 -2
- package/src/components/RsSteps.vue +2 -2
- package/src/components/RsSwitch.vue +20 -7
- package/src/components/RsTabs.vue +9 -9
- package/src/components/RsTag.vue +1 -1
- package/src/components/RsTerminal.vue +58 -19
- package/src/components/RsTimePicker.vue +28 -4
- package/src/components/RsTimePickerColumns.vue +4 -4
- package/src/components/RsToaster.vue +7 -7
- package/src/components/RsTooltip.vue +10 -1
- package/src/components/RsTooltipProvider.vue +11 -1
- package/src/components/RsTree.vue +6 -6
- package/src/components/RsUpload.vue +2 -2
- package/src/components/date-picker-utils.ts +113 -2
- package/src/components/dialog-utils.ts +37 -2
- package/src/components/form-item-control.ts +55 -0
- package/src/components/form-path.ts +92 -0
- package/src/components/form-rules.ts +64 -10
- package/src/components/form-utils.ts +94 -2
- package/src/components/select-utils.ts +272 -12
- package/src/components/table/RsTableHeaderFilter.vue +1 -1
- package/src/components/table/rs-table.css +1 -1
- package/src/components/use-rs-select.ts +436 -0
- package/src/composables/useRsConfig.ts +10 -3
- package/src/composables/useRsI18n.ts +16 -5
- package/src/index.ts +69 -1
- package/src/locale/interpolate.ts +32 -0
- package/src/locale/messages.ts +10 -0
- package/src/styles.css +68 -12
- package/src/theme/brand.example.css +4 -1
- package/src/theme/css-token.ts +107 -0
- package/src/theme/types.ts +1 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { resolveRsDialogCssWidth, resolveRsDialogWidthPx } from '../components/dialog-utils'
|
|
3
|
+
|
|
4
|
+
describe('resolveRsDialogWidthPx', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
document.documentElement.style.removeProperty('font-size')
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('returns undefined for presets', () => {
|
|
10
|
+
expect(resolveRsDialogWidthPx('sm')).toBeUndefined()
|
|
11
|
+
expect(resolveRsDialogWidthPx('md')).toBeUndefined()
|
|
12
|
+
expect(resolveRsDialogWidthPx('lg')).toBeUndefined()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('keeps numeric and px widths', () => {
|
|
16
|
+
expect(resolveRsDialogWidthPx(1200)).toBe(1200)
|
|
17
|
+
expect(resolveRsDialogWidthPx('960px')).toBe(960)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('converts rem using root font size', () => {
|
|
21
|
+
document.documentElement.style.fontSize = '16px'
|
|
22
|
+
expect(resolveRsDialogWidthPx('40rem')).toBe(640)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('converts percent against the given viewport width', () => {
|
|
26
|
+
expect(resolveRsDialogWidthPx('90%', 1000)).toBe(900)
|
|
27
|
+
expect(resolveRsDialogWidthPx('80%', 800)).toBe(640)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('returns undefined for empty or unknown units', () => {
|
|
31
|
+
expect(resolveRsDialogWidthPx('')).toBeUndefined()
|
|
32
|
+
expect(resolveRsDialogWidthPx('min(90vw, 1200px)')).toBeUndefined()
|
|
33
|
+
expect(resolveRsDialogWidthPx(0)).toBeUndefined()
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('resolveRsDialogCssWidth', () => {
|
|
38
|
+
it('maps number to px and keeps custom CSS strings', () => {
|
|
39
|
+
expect(resolveRsDialogCssWidth('md')).toBeUndefined()
|
|
40
|
+
expect(resolveRsDialogCssWidth(520)).toBe('520px')
|
|
41
|
+
expect(resolveRsDialogCssWidth('90%')).toBe('90%')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
concatNamePath,
|
|
4
|
+
getByNamePath,
|
|
5
|
+
hasByNamePath,
|
|
6
|
+
namePathKey,
|
|
7
|
+
normalizeNamePath,
|
|
8
|
+
setByNamePath,
|
|
9
|
+
} from '../components/form-path'
|
|
10
|
+
|
|
11
|
+
describe('form-path', () => {
|
|
12
|
+
it('normalizes string and array name paths', () => {
|
|
13
|
+
expect(normalizeNamePath('user.email')).toEqual(['user', 'email'])
|
|
14
|
+
expect(normalizeNamePath(['user', 'email'])).toEqual(['user', 'email'])
|
|
15
|
+
expect(namePathKey(['a', 0, 'b'])).toBe('a.0.b')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('gets and sets nested values', () => {
|
|
19
|
+
const model: Record<string, unknown> = {}
|
|
20
|
+
setByNamePath(model, 'user.email', 'a@b.com')
|
|
21
|
+
expect(model).toEqual({ user: { email: 'a@b.com' } })
|
|
22
|
+
expect(getByNamePath(model, ['user', 'email'])).toBe('a@b.com')
|
|
23
|
+
expect(hasByNamePath(model, 'user.email')).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('walks multi-level JSON paths', () => {
|
|
27
|
+
const model: Record<string, unknown> = {}
|
|
28
|
+
setByNamePath(model, 'config.headerConfig.isRequestHeader', true)
|
|
29
|
+
expect(model).toEqual({
|
|
30
|
+
config: { headerConfig: { isRequestHeader: true } },
|
|
31
|
+
})
|
|
32
|
+
expect(getByNamePath(model, 'config.headerConfig.isRequestHeader')).toBe(true)
|
|
33
|
+
expect(hasByNamePath(model, 'config.headerConfig.headerName')).toBe(false)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('preserves arrays when setting nested list fields', () => {
|
|
37
|
+
const model: Record<string, unknown> = { users: [{ name: 'A' }] }
|
|
38
|
+
setByNamePath(model, ['users', 0, 'name'], 'B')
|
|
39
|
+
expect(model.users).toEqual([{ name: 'B' }])
|
|
40
|
+
expect(Array.isArray(model.users)).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('concatenates list prefix and item name', () => {
|
|
44
|
+
expect(concatNamePath('users', [0, 'name'])).toEqual(['users', 0, 'name'])
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -63,6 +63,11 @@ describe('form-rules', () => {
|
|
|
63
63
|
expect(rules[1]?.type).toBe('email')
|
|
64
64
|
})
|
|
65
65
|
|
|
66
|
+
it('interpolates required message with label', async () => {
|
|
67
|
+
const labeled = await runFormFieldRules('', [{ required: true }], { label: '邮箱' })
|
|
68
|
+
expect(labeled.message).toBe('请填写邮箱')
|
|
69
|
+
})
|
|
70
|
+
|
|
66
71
|
it('supports message as function', async () => {
|
|
67
72
|
const result = await runFormFieldRules('x', [
|
|
68
73
|
{
|
|
@@ -73,4 +78,62 @@ describe('form-rules', () => {
|
|
|
73
78
|
expect(result.valid).toBe(false)
|
|
74
79
|
expect(result.message).toBe('当前「x」太短')
|
|
75
80
|
})
|
|
81
|
+
|
|
82
|
+
it('validator can read other fields via context', async () => {
|
|
83
|
+
const result = await runFormFieldRules('two', [
|
|
84
|
+
{
|
|
85
|
+
validator(value, ctx) {
|
|
86
|
+
if (String(value) !== String(ctx.getFieldValue('a'))) return '不一致'
|
|
87
|
+
return true
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
], {
|
|
91
|
+
getFieldValue: (name) => (String(name) === 'a' ? 'one' : undefined),
|
|
92
|
+
})
|
|
93
|
+
expect(result).toEqual({ valid: false, message: '不一致' })
|
|
94
|
+
|
|
95
|
+
const ok = await runFormFieldRules('one', [
|
|
96
|
+
{
|
|
97
|
+
validator(value, ctx) {
|
|
98
|
+
if (String(value) !== String(ctx.getFieldValue('a'))) return '不一致'
|
|
99
|
+
return true
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
], {
|
|
103
|
+
getFieldValue: (name) => (String(name) === 'a' ? 'one' : undefined),
|
|
104
|
+
})
|
|
105
|
+
expect(ok.valid).toBe(true)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('runs custom validator on empty value for cross-field required', async () => {
|
|
109
|
+
const result = await runFormFieldRules(
|
|
110
|
+
'',
|
|
111
|
+
[
|
|
112
|
+
{
|
|
113
|
+
validator(_value, ctx) {
|
|
114
|
+
if (ctx.getFieldValue('kind') === 'email') return '邮箱必填'
|
|
115
|
+
return true
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
{ getFieldValue: (name) => (String(name) === 'kind' ? 'email' : undefined) },
|
|
120
|
+
)
|
|
121
|
+
expect(result).toEqual({ valid: false, message: '邮箱必填' })
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('treats Error return and thrown errors as failure', async () => {
|
|
125
|
+
const returned = await runFormFieldRules('x', [
|
|
126
|
+
{ validator: () => new Error('返回错误') as unknown as string },
|
|
127
|
+
])
|
|
128
|
+
expect(returned).toEqual({ valid: false, message: '返回错误' })
|
|
129
|
+
|
|
130
|
+
const thrown = await runFormFieldRules('x', [
|
|
131
|
+
{
|
|
132
|
+
validator() {
|
|
133
|
+
throw new Error('抛出错误')
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
])
|
|
137
|
+
expect(thrown).toEqual({ valid: false, message: '抛出错误' })
|
|
138
|
+
})
|
|
76
139
|
})
|
|
@@ -17,6 +17,8 @@ describe('useRsConfig / theme', () => {
|
|
|
17
17
|
expect(en.t('select.placeholder')).toBe('Select')
|
|
18
18
|
expect(zh.t('input.required')).toBe('此项为必填')
|
|
19
19
|
expect(en.t('input.required')).toBe('This field is required')
|
|
20
|
+
expect(zh.t('form.validate.required', { label: '邮箱' })).toBe('请填写邮箱')
|
|
21
|
+
expect(en.t('form.validate.required', { label: 'Email' })).toBe('Please enter Email')
|
|
20
22
|
expect(en.t('breadcrumb.label')).toBe('Breadcrumb')
|
|
21
23
|
})
|
|
22
24
|
|
|
@@ -108,7 +108,7 @@ function onClose(event: MouseEvent): void {
|
|
|
108
108
|
|
|
109
109
|
.rs-alert__title {
|
|
110
110
|
font-size: var(--rs-font-size-sm);
|
|
111
|
-
font-weight:
|
|
111
|
+
font-weight: var(--rs-font-weight-semibold);
|
|
112
112
|
line-height: var(--rs-line-height-tight);
|
|
113
113
|
color: inherit;
|
|
114
114
|
}
|
|
@@ -90,7 +90,7 @@ const ariaLabel = computed(() => props.label ?? props.alt ?? props.name ?? undef
|
|
|
90
90
|
width: 100%;
|
|
91
91
|
height: 100%;
|
|
92
92
|
font-size: var(--rs-font-size-xs);
|
|
93
|
-
font-weight:
|
|
93
|
+
font-weight: var(--rs-font-weight-semibold);
|
|
94
94
|
}
|
|
95
95
|
.rs-avatar__fallback--default {
|
|
96
96
|
background: var(--rs-surface-hover);
|
|
@@ -22,7 +22,7 @@ withDefaults(
|
|
|
22
22
|
padding: 0.125rem 0.625rem;
|
|
23
23
|
border-radius: var(--rs-radius-full);
|
|
24
24
|
font-size: var(--rs-font-size-xs);
|
|
25
|
-
font-weight:
|
|
25
|
+
font-weight: var(--rs-font-weight-medium);
|
|
26
26
|
line-height: var(--rs-line-height-tight);
|
|
27
27
|
letter-spacing: 0.02em;
|
|
28
28
|
}
|
|
@@ -56,7 +56,7 @@ const renderItems = computed(() => buildBreadcrumbRenderItems(props.items))
|
|
|
56
56
|
}
|
|
57
57
|
.rs-breadcrumb__link--current {
|
|
58
58
|
color: var(--rs-text);
|
|
59
|
-
font-weight:
|
|
59
|
+
font-weight: var(--rs-font-weight-medium);
|
|
60
60
|
pointer-events: none;
|
|
61
61
|
}
|
|
62
62
|
.rs-breadcrumb__sep {
|
|
@@ -196,7 +196,7 @@ watch(
|
|
|
196
196
|
border-radius: var(--rs-btn-radius, var(--rs-radius-full));
|
|
197
197
|
border: 1px solid transparent;
|
|
198
198
|
font-size: var(--rs-font-size-sm);
|
|
199
|
-
font-weight:
|
|
199
|
+
font-weight: var(--rs-font-weight-medium);
|
|
200
200
|
letter-spacing: 0.01em;
|
|
201
201
|
line-height: 1;
|
|
202
202
|
white-space: nowrap;
|
|
@@ -329,7 +329,7 @@ watch(
|
|
|
329
329
|
background: transparent;
|
|
330
330
|
color: var(--rs-btn-tone, var(--rs-primary));
|
|
331
331
|
box-shadow: none;
|
|
332
|
-
font-weight:
|
|
332
|
+
font-weight: var(--rs-font-weight-medium);
|
|
333
333
|
}
|
|
334
334
|
.rs-btn--link:hover:not(:disabled) {
|
|
335
335
|
color: var(--rs-btn-tone-hover, var(--rs-primary-hover));
|
|
@@ -346,7 +346,7 @@ watch(
|
|
|
346
346
|
border-color: transparent;
|
|
347
347
|
background: transparent;
|
|
348
348
|
box-shadow: none;
|
|
349
|
-
font-weight:
|
|
349
|
+
font-weight: var(--rs-font-weight-regular);
|
|
350
350
|
color: var(--rs-btn-tone, var(--rs-text));
|
|
351
351
|
}
|
|
352
352
|
.rs-btn--text.rs-btn--ssm {
|
|
@@ -474,7 +474,7 @@ watch(
|
|
|
474
474
|
background: var(--rs-surface-elevated);
|
|
475
475
|
color: var(--rs-text);
|
|
476
476
|
font-size: var(--rs-font-size-xs);
|
|
477
|
-
font-weight:
|
|
477
|
+
font-weight: var(--rs-font-weight-medium);
|
|
478
478
|
line-height: 1.25rem;
|
|
479
479
|
white-space: nowrap;
|
|
480
480
|
box-shadow: var(--rs-shadow-sm);
|
|
@@ -204,7 +204,7 @@ function selectDate(cell: RsCalendarCell): void {
|
|
|
204
204
|
flex: 1;
|
|
205
205
|
text-align: center;
|
|
206
206
|
font-size: var(--rs-font-size-sm);
|
|
207
|
-
font-weight:
|
|
207
|
+
font-weight: var(--rs-font-weight-semibold);
|
|
208
208
|
color: var(--rs-text);
|
|
209
209
|
}
|
|
210
210
|
.rs-calendar-grid__nav {
|
|
@@ -233,8 +233,8 @@ function selectDate(cell: RsCalendarCell): void {
|
|
|
233
233
|
.rs-calendar-grid__weekday {
|
|
234
234
|
padding: 0.125rem 0;
|
|
235
235
|
text-align: center;
|
|
236
|
-
font-size:
|
|
237
|
-
font-weight:
|
|
236
|
+
font-size: var(--rs-font-size-xs);
|
|
237
|
+
font-weight: var(--rs-font-weight-medium);
|
|
238
238
|
color: var(--rs-muted);
|
|
239
239
|
}
|
|
240
240
|
.rs-calendar-grid__day {
|
|
@@ -232,7 +232,7 @@ const rootStyle = computed(() => ({
|
|
|
232
232
|
}
|
|
233
233
|
.rs-card--plain .rs-card__title {
|
|
234
234
|
font-size: var(--rs-font-size-xs);
|
|
235
|
-
font-weight:
|
|
235
|
+
font-weight: var(--rs-font-weight-medium);
|
|
236
236
|
color: var(--rs-card-description-fg);
|
|
237
237
|
letter-spacing: 0;
|
|
238
238
|
}
|
|
@@ -255,7 +255,7 @@ const rootStyle = computed(() => ({
|
|
|
255
255
|
box-shadow: none;
|
|
256
256
|
}
|
|
257
257
|
.rs-card--outlined .rs-card__title {
|
|
258
|
-
font-weight:
|
|
258
|
+
font-weight: var(--rs-font-weight-medium);
|
|
259
259
|
letter-spacing: 0;
|
|
260
260
|
}
|
|
261
261
|
.rs-card--outlined .rs-card__body {
|
|
@@ -277,7 +277,7 @@ const rootStyle = computed(() => ({
|
|
|
277
277
|
box-shadow: none;
|
|
278
278
|
}
|
|
279
279
|
.rs-card--filled .rs-card__title {
|
|
280
|
-
font-weight:
|
|
280
|
+
font-weight: var(--rs-font-weight-medium);
|
|
281
281
|
letter-spacing: 0;
|
|
282
282
|
}
|
|
283
283
|
.rs-card--filled .rs-card__body {
|
|
@@ -385,7 +385,7 @@ const rootStyle = computed(() => ({
|
|
|
385
385
|
.rs-card__title {
|
|
386
386
|
margin: 0;
|
|
387
387
|
font-size: var(--rs-card-title-size);
|
|
388
|
-
font-weight:
|
|
388
|
+
font-weight: var(--rs-font-weight-semibold);
|
|
389
389
|
letter-spacing: -0.01em;
|
|
390
390
|
color: var(--rs-card-title-fg);
|
|
391
391
|
}
|
|
@@ -192,7 +192,7 @@ function download() {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
.rs-code-block__lang {
|
|
195
|
-
font-family:
|
|
195
|
+
font-family: var(--rs-code-font-family, var(--rs-font-mono));
|
|
196
196
|
font-size: var(--rs-font-size-xs);
|
|
197
197
|
color: var(--rs-muted);
|
|
198
198
|
text-transform: lowercase;
|
|
@@ -251,9 +251,9 @@ function download() {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
.rs-code-block__editor :deep(.cm-scroller) {
|
|
254
|
-
font-family:
|
|
255
|
-
font-size: var(--rs-font-size-sm);
|
|
256
|
-
line-height:
|
|
254
|
+
font-family: var(--rs-code-font-family, var(--rs-font-mono));
|
|
255
|
+
font-size: var(--rs-code-font-size, var(--rs-font-size-sm));
|
|
256
|
+
line-height: var(--rs-code-line-height, var(--rs-line-height-relaxed));
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
/* 只读时隐藏光标,保持纯展示外观 */
|
|
@@ -445,14 +445,14 @@ defineExpose({
|
|
|
445
445
|
justify-content: space-between;
|
|
446
446
|
margin-bottom: 0.5rem;
|
|
447
447
|
font-size: var(--rs-font-size-xs);
|
|
448
|
-
font-weight:
|
|
448
|
+
font-weight: var(--rs-font-weight-semibold);
|
|
449
449
|
color: var(--rs-foreground);
|
|
450
450
|
}
|
|
451
451
|
.rs-code-editor__inline-edit-head kbd {
|
|
452
452
|
padding: 0.1rem 0.35rem;
|
|
453
453
|
border-radius: 4px;
|
|
454
454
|
border: 1px solid var(--rs-border-subtle);
|
|
455
|
-
font-size:
|
|
455
|
+
font-size: var(--rs-font-size-xs);
|
|
456
456
|
color: var(--rs-muted);
|
|
457
457
|
}
|
|
458
458
|
.rs-code-editor__inline-preview {
|
|
@@ -461,8 +461,8 @@ defineExpose({
|
|
|
461
461
|
border-radius: 4px;
|
|
462
462
|
background: color-mix(in srgb, var(--rs-muted) 8%, transparent);
|
|
463
463
|
color: var(--rs-muted);
|
|
464
|
-
font-family:
|
|
465
|
-
font-size:
|
|
464
|
+
font-family: var(--rs-code-font-family, var(--rs-font-mono));
|
|
465
|
+
font-size: var(--rs-font-size-xs);
|
|
466
466
|
line-height: 1.4;
|
|
467
467
|
white-space: pre-wrap;
|
|
468
468
|
max-height: 4.5rem;
|
|
@@ -520,9 +520,9 @@ defineExpose({
|
|
|
520
520
|
background: var(--rs-surface-elevated) !important;
|
|
521
521
|
}
|
|
522
522
|
.rs-code-editor__surface :deep(.cm-scroller) {
|
|
523
|
-
font-family:
|
|
524
|
-
font-size: var(--rs-font-size-sm);
|
|
525
|
-
line-height:
|
|
523
|
+
font-family: var(--rs-code-font-family, var(--rs-font-mono));
|
|
524
|
+
font-size: var(--rs-code-font-size, var(--rs-font-size-sm));
|
|
525
|
+
line-height: var(--rs-code-line-height, var(--rs-line-height-relaxed));
|
|
526
526
|
}
|
|
527
527
|
.rs-code-editor__surface :deep(.cm-gutters) {
|
|
528
528
|
background: color-mix(in srgb, var(--rs-surface-elevated) 85%, var(--rs-border) 15%) !important;
|
|
@@ -571,7 +571,7 @@ defineExpose({
|
|
|
571
571
|
border: 1px solid var(--rs-border);
|
|
572
572
|
background: var(--rs-surface-elevated);
|
|
573
573
|
color: var(--rs-foreground);
|
|
574
|
-
font-family:
|
|
574
|
+
font-family: var(--rs-code-font-family, var(--rs-font-mono));
|
|
575
575
|
font-size: var(--rs-font-size-xs);
|
|
576
576
|
line-height: 1.45;
|
|
577
577
|
white-space: pre-wrap;
|
|
@@ -297,7 +297,7 @@ defineExpose({
|
|
|
297
297
|
height: 2.5rem;
|
|
298
298
|
flex: 0 0 auto;
|
|
299
299
|
border-radius: var(--rs-radius);
|
|
300
|
-
font-weight:
|
|
300
|
+
font-weight: var(--rs-font-weight-bold);
|
|
301
301
|
}
|
|
302
302
|
.rs-confirm-dialog__icon-glyph {
|
|
303
303
|
width: 1.25rem;
|
|
@@ -328,7 +328,7 @@ defineExpose({
|
|
|
328
328
|
.rs-confirm-dialog__title {
|
|
329
329
|
margin: 0;
|
|
330
330
|
font-size: var(--rs-font-size-base);
|
|
331
|
-
font-weight:
|
|
331
|
+
font-weight: var(--rs-font-weight-semibold);
|
|
332
332
|
letter-spacing: -0.01em;
|
|
333
333
|
color: var(--rs-dialog-title-fg);
|
|
334
334
|
}
|
|
@@ -152,9 +152,10 @@ function onSelect(item: RsContextMenuItem) {
|
|
|
152
152
|
align-items: center;
|
|
153
153
|
gap: 0;
|
|
154
154
|
padding: 0 8px 0 0;
|
|
155
|
-
height:
|
|
155
|
+
height: 24px;
|
|
156
156
|
border-radius: 6px;
|
|
157
|
-
font-size:
|
|
157
|
+
font-size: var(--rs-font-size-xs);
|
|
158
|
+
font-weight: var(--rs-font-weight-regular);
|
|
158
159
|
line-height: 1;
|
|
159
160
|
color: var(--rs-text);
|
|
160
161
|
cursor: default;
|
|
@@ -185,10 +186,11 @@ function onSelect(item: RsContextMenuItem) {
|
|
|
185
186
|
.rs-context-menu__shortcut {
|
|
186
187
|
flex-shrink: 0;
|
|
187
188
|
margin-left: 16px;
|
|
188
|
-
font-size:
|
|
189
|
+
font-size: var(--rs-font-size-xs);
|
|
190
|
+
font-weight: var(--rs-font-weight-regular);
|
|
189
191
|
color: var(--rs-ctx-shortcut);
|
|
190
192
|
letter-spacing: 0.02em;
|
|
191
|
-
font-family: -
|
|
193
|
+
font-family: var(--rs-font-sans);
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
/* 子菜单展开箭头 */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, ref, useId, watch } from 'vue'
|
|
2
|
+
import { computed, ref, useAttrs, useId, watch } from 'vue'
|
|
3
3
|
import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from './reka'
|
|
4
4
|
import RsCalendarGrid from './RsCalendarGrid.vue'
|
|
5
5
|
import RsIcon from './RsIcon.vue'
|
|
@@ -7,7 +7,12 @@ import RsTimePicker from './RsTimePicker.vue'
|
|
|
7
7
|
import { useRsI18n } from '../composables/useRsI18n'
|
|
8
8
|
import { RS_COMPONENT_SIZE_ICON_PX, type RsComponentSize } from '../theme/types'
|
|
9
9
|
import { useResolvedRsComponentSize } from './resolve-size'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
isRsFormItemBoundControl,
|
|
12
|
+
useRsFormContext,
|
|
13
|
+
useRsFormField,
|
|
14
|
+
useRsFormItemContext,
|
|
15
|
+
} from './form-utils'
|
|
11
16
|
import {
|
|
12
17
|
buildLocalInputRules,
|
|
13
18
|
runFormFieldRules,
|
|
@@ -26,6 +31,7 @@ import {
|
|
|
26
31
|
getNextMonth,
|
|
27
32
|
getNowDateTime,
|
|
28
33
|
formatTimestampValue,
|
|
34
|
+
fromInternalPickerValue,
|
|
29
35
|
getTodayDate,
|
|
30
36
|
isDateRangeEmpty,
|
|
31
37
|
isDateRangeOrdered,
|
|
@@ -35,6 +41,8 @@ import {
|
|
|
35
41
|
parseDateTimeValue,
|
|
36
42
|
parseDateValue,
|
|
37
43
|
parseTimestampValue,
|
|
44
|
+
toInternalPickerValue,
|
|
45
|
+
toRangeEndpointString,
|
|
38
46
|
type RsDatePickerModelValue,
|
|
39
47
|
type RsDatePickerShortcut,
|
|
40
48
|
type RsDatePickerValueFormat,
|
|
@@ -55,6 +63,8 @@ function isRangeModel(value: RsDatePickerModelValue): value is RsDateRangeValue
|
|
|
55
63
|
return typeof value === 'object' && value !== null && !Array.isArray(value) && ('start' in value || 'end' in value)
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
defineOptions({ inheritAttrs: false })
|
|
67
|
+
|
|
58
68
|
const model = defineModel<RsDatePickerModelValue>({ default: '' })
|
|
59
69
|
const open = defineModel<boolean>('open', { default: false })
|
|
60
70
|
|
|
@@ -75,12 +85,19 @@ const props = withDefaults(
|
|
|
75
85
|
* 是否选择秒。未传时:withTime 为 true 则默认带秒,纯日期为 false。
|
|
76
86
|
*/
|
|
77
87
|
withSeconds?: boolean
|
|
78
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* 绑定值格式(展示仍为墙钟)。
|
|
90
|
+
* string:YYYY-MM-DD[/ HH:mm:ss];timestamp:毫秒;iso:本地偏移 RFC3339;
|
|
91
|
+
* 亦可传入 dayjs 模板(对齐 Element Plus value-format)。
|
|
92
|
+
*/
|
|
79
93
|
valueFormat?: RsDatePickerValueFormat
|
|
80
94
|
labelPosition?: RsDatePickerLabelPosition
|
|
81
95
|
/** 底部快捷选项(有值时替代「今天/现在」链接) */
|
|
82
96
|
shortcuts?: RsDatePickerShortcut[]
|
|
83
97
|
size?: RsComponentSize
|
|
98
|
+
id?: string
|
|
99
|
+
invalid?: boolean
|
|
100
|
+
showValidateMessage?: boolean
|
|
84
101
|
}>(),
|
|
85
102
|
{
|
|
86
103
|
disabled: false,
|
|
@@ -89,10 +106,12 @@ const props = withDefaults(
|
|
|
89
106
|
withTime: false,
|
|
90
107
|
valueFormat: 'string',
|
|
91
108
|
labelPosition: 'top',
|
|
109
|
+
showValidateMessage: true,
|
|
92
110
|
},
|
|
93
111
|
)
|
|
94
112
|
|
|
95
113
|
const fieldId = useId()
|
|
114
|
+
const attrs = useAttrs()
|
|
96
115
|
const { t } = useRsI18n()
|
|
97
116
|
const resolvedSize = useResolvedRsComponentSize(() => props.size)
|
|
98
117
|
const triggerIconSize = computed(() => RS_COMPONENT_SIZE_ICON_PX[resolvedSize.value])
|
|
@@ -101,7 +120,22 @@ const resolvedWithSeconds = computed(() =>
|
|
|
101
120
|
props.withSeconds !== undefined ? props.withSeconds : props.withTime,
|
|
102
121
|
)
|
|
103
122
|
const formContext = useRsFormContext()
|
|
123
|
+
const formItem = useRsFormItemContext()
|
|
124
|
+
const boundToItem = computed(() =>
|
|
125
|
+
isRsFormItemBoundControl(formItem, { id: props.id, name: props.name }),
|
|
126
|
+
)
|
|
104
127
|
const autoMessage = ref('')
|
|
128
|
+
const isInvalid = computed(() =>
|
|
129
|
+
Boolean(
|
|
130
|
+
props.invalid ||
|
|
131
|
+
(boundToItem.value && formItem?.invalid.value) ||
|
|
132
|
+
(!boundToItem.value && autoMessage.value),
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
const triggerId = computed(() => props.id || fieldId)
|
|
136
|
+
const visibleMessage = computed(() =>
|
|
137
|
+
boundToItem.value || props.showValidateMessage === false ? '' : autoMessage.value,
|
|
138
|
+
)
|
|
105
139
|
|
|
106
140
|
const viewYear = ref(getTodayDate().year)
|
|
107
141
|
const viewMonth = ref(getTodayDate().month)
|
|
@@ -117,47 +151,52 @@ const draftTime = ref('')
|
|
|
117
151
|
const draftStartTime = ref('')
|
|
118
152
|
const draftEndTime = ref('')
|
|
119
153
|
|
|
154
|
+
const convertOptions = computed(() => ({
|
|
155
|
+
valueFormat: props.valueFormat ?? 'string',
|
|
156
|
+
withTime: props.withTime,
|
|
157
|
+
}))
|
|
158
|
+
|
|
120
159
|
const useTimestamp = computed(() => props.valueFormat === 'timestamp')
|
|
121
160
|
|
|
122
161
|
const rangeModel = computed<RsDateRangeValue>({
|
|
123
162
|
get() {
|
|
124
163
|
if (!props.range) return { ...EMPTY_DATE_RANGE }
|
|
125
164
|
const value = model.value
|
|
126
|
-
|
|
127
|
-
|
|
165
|
+
const opts = convertOptions.value
|
|
166
|
+
if (isTimestampRange(value)) {
|
|
128
167
|
return {
|
|
129
|
-
start: formatTimestampValue(value[0],
|
|
130
|
-
end: formatTimestampValue(value[1],
|
|
168
|
+
start: formatTimestampValue(value[0], opts.withTime),
|
|
169
|
+
end: formatTimestampValue(value[1], opts.withTime),
|
|
131
170
|
}
|
|
132
171
|
}
|
|
133
|
-
|
|
172
|
+
if (!isRangeModel(value)) return { ...EMPTY_DATE_RANGE }
|
|
173
|
+
return {
|
|
174
|
+
start: toInternalPickerValue(value.start, opts) || undefined,
|
|
175
|
+
end: toInternalPickerValue(value.end, opts) || undefined,
|
|
176
|
+
}
|
|
134
177
|
},
|
|
135
178
|
set(value) {
|
|
179
|
+
const opts = convertOptions.value
|
|
136
180
|
if (useTimestamp.value) {
|
|
137
181
|
const start = parseTimestampValue(value.start)
|
|
138
182
|
const end = parseTimestampValue(value.end)
|
|
139
183
|
model.value = start != null && end != null ? [start, end] : null
|
|
140
184
|
return
|
|
141
185
|
}
|
|
142
|
-
model.value =
|
|
186
|
+
model.value = {
|
|
187
|
+
start: toRangeEndpointString(fromInternalPickerValue(value.start || '', opts)),
|
|
188
|
+
end: toRangeEndpointString(fromInternalPickerValue(value.end || '', opts)),
|
|
189
|
+
}
|
|
143
190
|
},
|
|
144
191
|
})
|
|
145
192
|
|
|
146
193
|
const singleModel = computed({
|
|
147
194
|
get() {
|
|
148
195
|
if (props.range) return ''
|
|
149
|
-
|
|
150
|
-
if (useTimestamp.value) {
|
|
151
|
-
return typeof value === 'number' ? formatTimestampValue(value, props.withTime) : ''
|
|
152
|
-
}
|
|
153
|
-
return typeof value === 'string' ? value : ''
|
|
196
|
+
return toInternalPickerValue(model.value, convertOptions.value)
|
|
154
197
|
},
|
|
155
198
|
set(value: string) {
|
|
156
|
-
|
|
157
|
-
model.value = value ? parseTimestampValue(value) : null
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
model.value = value
|
|
199
|
+
model.value = fromInternalPickerValue(value, convertOptions.value)
|
|
161
200
|
},
|
|
162
201
|
})
|
|
163
202
|
|
|
@@ -422,11 +461,7 @@ function clearSelection(): void {
|
|
|
422
461
|
draftEnd.value = null
|
|
423
462
|
if (props.withTime) resetRangeTimeDrafts()
|
|
424
463
|
} else {
|
|
425
|
-
|
|
426
|
-
model.value = null
|
|
427
|
-
} else {
|
|
428
|
-
singleModel.value = ''
|
|
429
|
-
}
|
|
464
|
+
singleModel.value = ''
|
|
430
465
|
draftDate.value = null
|
|
431
466
|
if (props.withTime) draftTime.value = currentTimeValue()
|
|
432
467
|
}
|
|
@@ -577,15 +612,16 @@ watch(open, (isOpen) => {
|
|
|
577
612
|
<div class="rs-date-picker" :class="`rs-date-picker--${resolvedSize}`">
|
|
578
613
|
<PopoverRoot v-model:open="open">
|
|
579
614
|
<PopoverTrigger
|
|
580
|
-
|
|
615
|
+
v-bind="attrs"
|
|
616
|
+
:id="triggerId"
|
|
581
617
|
type="button"
|
|
582
618
|
class="rs-date-picker__trigger"
|
|
583
619
|
:class="{
|
|
584
620
|
'rs-date-picker__trigger--placeholder': isEmpty,
|
|
585
|
-
'rs-date-picker__trigger--invalid':
|
|
621
|
+
'rs-date-picker__trigger--invalid': isInvalid,
|
|
586
622
|
}"
|
|
587
623
|
:disabled="disabled"
|
|
588
|
-
:aria-invalid="
|
|
624
|
+
:aria-invalid="isInvalid || undefined"
|
|
589
625
|
>
|
|
590
626
|
<span class="rs-date-picker__leading">
|
|
591
627
|
<RsIcon :name="triggerIcon" :size="triggerIconSize" class="rs-date-picker__icon" />
|
|
@@ -728,8 +764,8 @@ watch(open, (isOpen) => {
|
|
|
728
764
|
</PopoverRoot>
|
|
729
765
|
</div>
|
|
730
766
|
|
|
731
|
-
<p v-if="
|
|
732
|
-
{{
|
|
767
|
+
<p v-if="visibleMessage" class="rs-date-picker-field__error" role="alert">
|
|
768
|
+
{{ visibleMessage }}
|
|
733
769
|
</p>
|
|
734
770
|
<span v-if="hint" class="rs-field__hint">{{ hint }}</span>
|
|
735
771
|
</div>
|
|
@@ -758,6 +794,7 @@ watch(open, (isOpen) => {
|
|
|
758
794
|
line-height: var(--rs-line-height-tight);
|
|
759
795
|
text-align: left;
|
|
760
796
|
cursor: pointer;
|
|
797
|
+
outline: none;
|
|
761
798
|
box-shadow: var(--rs-input-shadow, none);
|
|
762
799
|
transition:
|
|
763
800
|
border-color var(--rs-transition-fast),
|
|
@@ -803,6 +840,13 @@ watch(open, (isOpen) => {
|
|
|
803
840
|
.rs-date-picker__trigger--invalid {
|
|
804
841
|
border-color: var(--rs-danger, var(--rs-color-danger, #dc2626));
|
|
805
842
|
}
|
|
843
|
+
.rs-date-picker__trigger--invalid:focus-visible {
|
|
844
|
+
border-color: var(--rs-danger, var(--rs-color-danger, #dc2626));
|
|
845
|
+
box-shadow:
|
|
846
|
+
var(--rs-input-shadow, none),
|
|
847
|
+
0 0 0 var(--rs-focus-ring-width, 2px)
|
|
848
|
+
color-mix(in srgb, var(--rs-danger, #dc2626) 14%, transparent);
|
|
849
|
+
}
|
|
806
850
|
.rs-date-picker-field__error {
|
|
807
851
|
margin: var(--rs-space-xs) 0 0;
|
|
808
852
|
color: var(--rs-danger, var(--rs-color-danger, #dc2626));
|
|
@@ -874,7 +918,7 @@ watch(open, (isOpen) => {
|
|
|
874
918
|
}
|
|
875
919
|
.rs-date-picker__pane-title {
|
|
876
920
|
font-size: var(--rs-font-size-xs);
|
|
877
|
-
font-weight:
|
|
921
|
+
font-weight: var(--rs-font-weight-semibold);
|
|
878
922
|
color: var(--rs-muted);
|
|
879
923
|
}
|
|
880
924
|
.rs-date-picker__footer {
|
|
@@ -979,7 +1023,7 @@ watch(open, (isOpen) => {
|
|
|
979
1023
|
.rs-date-picker__time-label {
|
|
980
1024
|
flex-shrink: 0;
|
|
981
1025
|
font-size: var(--rs-font-size-xs);
|
|
982
|
-
font-weight:
|
|
1026
|
+
font-weight: var(--rs-font-weight-medium);
|
|
983
1027
|
color: var(--rs-muted);
|
|
984
1028
|
}
|
|
985
1029
|
.rs-date-picker__time-picker {
|