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.
Files changed (159) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/NOTICE +2 -2
  3. package/package.json +13 -5
  4. package/src/__tests__/RsButton.spec.ts +47 -0
  5. package/src/__tests__/RsCard.spec.ts +160 -1
  6. package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
  7. package/src/__tests__/RsContextMenu.spec.ts +12 -0
  8. package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
  9. package/src/__tests__/RsDescriptions.spec.ts +50 -0
  10. package/src/__tests__/RsDialog.spec.ts +117 -0
  11. package/src/__tests__/RsDrawer.spec.ts +96 -3
  12. package/src/__tests__/RsDynamicTags.spec.ts +52 -0
  13. package/src/__tests__/RsForm.spec.ts +245 -1
  14. package/src/__tests__/RsInput.spec.ts +15 -1
  15. package/src/__tests__/RsMenu.spec.ts +406 -14
  16. package/src/__tests__/RsPagination.spec.ts +67 -2
  17. package/src/__tests__/RsSelect.spec.ts +42 -1
  18. package/src/__tests__/RsSwitch.spec.ts +44 -0
  19. package/src/__tests__/RsTable.a11y.spec.ts +83 -0
  20. package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
  21. package/src/__tests__/RsTable.perf.spec.ts +23 -4
  22. package/src/__tests__/RsTable.spec.ts +63 -8
  23. package/src/__tests__/RsTable.ssr.spec.ts +71 -0
  24. package/src/__tests__/RsTable.summary.spec.ts +45 -0
  25. package/src/__tests__/RsTabs.spec.ts +102 -4
  26. package/src/__tests__/RsTerminal.spec.ts +1 -1
  27. package/src/__tests__/RsTimePicker.spec.ts +11 -0
  28. package/src/__tests__/RsUpload.spec.ts +62 -6
  29. package/src/__tests__/createRsDialog.spec.ts +145 -0
  30. package/src/__tests__/form-rules.spec.ts +76 -0
  31. package/src/__tests__/perf-budgets.ts +32 -0
  32. package/src/__tests__/rs-table-api.spec.ts +206 -0
  33. package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
  34. package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
  35. package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
  36. package/src/__tests__/rs-table-isolation.spec.ts +158 -0
  37. package/src/__tests__/split-pane-utils.spec.ts +21 -0
  38. package/src/__tests__/table-chart-utils.spec.ts +88 -0
  39. package/src/__tests__/table-summary-utils.spec.ts +62 -0
  40. package/src/__tests__/table-utils.spec.ts +124 -0
  41. package/src/__tests__/theme.spec.ts +25 -0
  42. package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
  43. package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
  44. package/src/components/RsAlert.vue +212 -0
  45. package/src/components/RsButton.vue +145 -21
  46. package/src/components/RsCard.vue +200 -16
  47. package/src/components/RsConfirmDialog.vue +215 -28
  48. package/src/components/RsContextMenu.vue +51 -13
  49. package/src/components/RsDatePicker.vue +267 -34
  50. package/src/components/RsDateTimePicker.vue +19 -2
  51. package/src/components/RsDescriptions.vue +186 -0
  52. package/src/components/RsDescriptionsItem.vue +32 -0
  53. package/src/components/RsDialog.vue +363 -51
  54. package/src/components/RsDivider.vue +89 -0
  55. package/src/components/RsDrawer.vue +558 -33
  56. package/src/components/RsDropdown.vue +33 -2
  57. package/src/components/RsDynamicTags.vue +517 -0
  58. package/src/components/RsForm.vue +97 -8
  59. package/src/components/RsInput.vue +80 -82
  60. package/src/components/RsInputNumber.vue +64 -23
  61. package/src/components/RsLabel.vue +20 -5
  62. package/src/components/RsLoadingBar.vue +151 -0
  63. package/src/components/RsMenu.vue +73 -15
  64. package/src/components/RsMenuItems.vue +147 -9
  65. package/src/components/RsPagination.vue +93 -14
  66. package/src/components/RsRadio.vue +81 -0
  67. package/src/components/RsRadioItem.vue +156 -0
  68. package/src/components/RsSelect.vue +67 -58
  69. package/src/components/RsSplitPane.vue +69 -1
  70. package/src/components/RsSwitch.vue +179 -0
  71. package/src/components/RsTable.vue +653 -2555
  72. package/src/components/RsTabs.vue +415 -83
  73. package/src/components/RsTag.vue +191 -0
  74. package/src/components/RsTerminal.vue +3 -3
  75. package/src/components/RsTimePicker.vue +44 -5
  76. package/src/components/RsTimePickerColumns.vue +136 -77
  77. package/src/components/RsToaster.vue +58 -18
  78. package/src/components/RsTree.vue +39 -21
  79. package/src/components/RsUpload.vue +145 -12
  80. package/src/components/RsVNodeHost.vue +20 -0
  81. package/src/components/button-utils.ts +39 -0
  82. package/src/components/context-menu-utils.ts +6 -0
  83. package/src/components/date-picker-utils.ts +84 -4
  84. package/src/components/descriptions-utils.ts +24 -0
  85. package/src/components/dialog-utils.ts +147 -0
  86. package/src/components/dialog-window.ts +74 -9
  87. package/src/components/drawer-utils.ts +73 -0
  88. package/src/components/form-rules.ts +257 -0
  89. package/src/components/form-utils.ts +52 -2
  90. package/src/components/menu-utils.ts +11 -0
  91. package/src/components/overlay-utils.ts +3 -0
  92. package/src/components/radio-utils.ts +12 -0
  93. package/src/components/reka.ts +5 -0
  94. package/src/components/select-utils.ts +21 -2
  95. package/src/components/split-pane-utils.ts +65 -12
  96. package/src/components/table/RsTableBody.vue +217 -0
  97. package/src/components/table/RsTableBodyRow.vue +90 -1
  98. package/src/components/table/RsTableColGroup.vue +41 -0
  99. package/src/components/table/RsTableHeader.vue +158 -0
  100. package/src/components/table/RsTableSummaryRow.vue +52 -0
  101. package/src/components/table/rs-table-api.ts +99 -0
  102. package/src/components/table/rs-table-compat-matrix.ts +71 -0
  103. package/src/components/table/rs-table-echarts-adapter.ts +81 -0
  104. package/src/components/table/rs-table-feature-host.ts +178 -0
  105. package/src/components/table/rs-table-grid-nav.ts +122 -0
  106. package/src/components/table/rs-table-header-types.ts +47 -0
  107. package/src/components/table/rs-table-module-registry.ts +72 -0
  108. package/src/components/table/rs-table-props.ts +274 -0
  109. package/src/components/table/rs-table-view-context.ts +258 -0
  110. package/src/components/table/rs-table.css +84 -6
  111. package/src/components/table/table-chart-utils.ts +165 -0
  112. package/src/components/table/table-features.ts +284 -0
  113. package/src/components/table/table-summary-utils.ts +194 -0
  114. package/src/components/table-utils.ts +452 -2
  115. package/src/components/tabs-utils.ts +142 -2
  116. package/src/components/terminal-utils.ts +1 -1
  117. package/src/components/terminal-wheel.ts +1 -1
  118. package/src/components/time-picker-utils.ts +76 -3
  119. package/src/components/upload-utils.ts +122 -0
  120. package/src/components/virtual-list-utils.ts +15 -2
  121. package/src/composables/assembleRsTableApi.ts +79 -0
  122. package/src/composables/bindRsTableViewContext.ts +269 -0
  123. package/src/composables/createRsDialog.ts +357 -0
  124. package/src/composables/createRsTableEditEmitBridge.ts +38 -0
  125. package/src/composables/useRsLoadingBar.ts +36 -0
  126. package/src/composables/useRsTable.ts +134 -0
  127. package/src/composables/useRsTableA11y.ts +52 -0
  128. package/src/composables/useRsTableChartBridge.ts +79 -0
  129. package/src/composables/useRsTableColumnLayout.ts +320 -0
  130. package/src/composables/useRsTableColumnResize.ts +244 -0
  131. package/src/composables/useRsTableColumnVirtual.ts +180 -0
  132. package/src/composables/useRsTableColumns.ts +200 -0
  133. package/src/composables/useRsTableContextMenu.ts +257 -0
  134. package/src/composables/useRsTableCore.ts +256 -0
  135. package/src/composables/useRsTableEditActions.ts +606 -0
  136. package/src/composables/useRsTableEditLayer.ts +251 -0
  137. package/src/composables/useRsTableEngine.ts +357 -0
  138. package/src/composables/useRsTableGridKeyboard.ts +123 -0
  139. package/src/composables/useRsTableHeadless.ts +137 -0
  140. package/src/composables/useRsTableInteraction.ts +433 -0
  141. package/src/composables/useRsTableScrollHost.ts +75 -0
  142. package/src/composables/useRsTableScrollLayout.ts +255 -0
  143. package/src/composables/useRsTableSelectionSource.ts +89 -0
  144. package/src/composables/useRsTableShell.ts +416 -0
  145. package/src/composables/useRsTableShellChrome.ts +170 -0
  146. package/src/composables/useRsTableSummary.ts +113 -0
  147. package/src/composables/useRsTableViewProvide.ts +263 -0
  148. package/src/composables/useRsTableVirtual.ts +154 -0
  149. package/src/composables/useRsTabsNav.ts +4 -0
  150. package/src/composables/useTableEdit.ts +1 -1
  151. package/src/icons/lucide.ts +2 -2
  152. package/src/index.ts +309 -9
  153. package/src/locale/messages.ts +30 -0
  154. package/src/styles.css +305 -52
  155. package/src/theme/brand.example.css +21 -2
  156. package/src/theme/presets.ts +4 -0
  157. package/src/theme/types.ts +67 -0
  158. package/src/components/RsTableColgroup.vue +0 -17
  159. package/src/components/RsTableHeaderRow.vue +0 -30
@@ -1,21 +1,25 @@
1
1
  <script setup lang="ts">
2
+ import { computed, watch } from 'vue'
2
3
  import {
3
4
  ContextMenuContent,
4
5
  ContextMenuPortal,
5
6
  ContextMenuRoot,
6
7
  ContextMenuTrigger,
7
8
  } from './reka'
8
- import type { RsContextMenuItem } from './context-menu-utils'
9
+ import {
10
+ hasActionableContextMenuItems,
11
+ type RsContextMenuItem,
12
+ } from './context-menu-utils'
9
13
  import RsContextMenuItems from './RsContextMenuItems.vue'
10
14
 
11
- defineProps<{
15
+ const props = defineProps<{
12
16
  items: RsContextMenuItem[]
13
17
  /** 禁用时不拦截右键事件,直接透传 slot,浏览器原生菜单正常显示 */
14
18
  disabled?: boolean
15
19
  }>()
16
20
 
17
21
  /**
18
- * 展开状态。不绑定时组件自行维护(与原行为一致);
22
+ * 展开状态。不绑定时组件自行维护(与原行为一致)。
19
23
  * 绑定后可由外部主动关闭——例如菜单所依赖的数据已失效时。
20
24
  */
21
25
  const open = defineModel<boolean>('open', { default: false })
@@ -24,14 +28,49 @@ const emit = defineEmits<{
24
28
  select: [key: string]
25
29
  }>()
26
30
 
31
+ /** 无有效菜单项时不打开,避免空表/空白区右键出现空气泡 */
32
+ const hasActionableItems = computed(() => hasActionableContextMenuItems(props.items))
33
+
34
+ watch(hasActionableItems, (ok) => {
35
+ if (!ok && open.value) open.value = false
36
+ })
37
+
38
+ function onUpdateOpen(next: boolean): void {
39
+ if (next && !hasActionableItems.value) {
40
+ open.value = false
41
+ return
42
+ }
43
+ open.value = next
44
+ }
45
+
46
+ function onTriggerContextMenu(event: MouseEvent): void {
47
+ if (!hasActionableItems.value) {
48
+ event.preventDefault()
49
+ open.value = false
50
+ }
51
+ }
52
+
27
53
  function onSelect(item: RsContextMenuItem) {
28
54
  emit('select', item.key)
29
55
  }
30
56
  </script>
31
57
 
32
58
  <template>
33
- <ContextMenuRoot v-if="!disabled" v-model:open="open">
34
- <ContextMenuTrigger class="rs-ctx-trigger" @contextmenu.stop>
59
+ <!--
60
+ as-child:把触发器 props 合并到 slot 唯一根节点,避免 span + display:contents
61
+ 在 Teleport/Presence 开关时把子 DOM 移出 Vue 追踪(__vnode null)。
62
+ Content 不用 v-if:由 Root open + Presence 管理显隐,避免与 items 更新竞态。
63
+ -->
64
+ <ContextMenuRoot v-if="!disabled" :open="open" @update:open="onUpdateOpen">
65
+ <!--
66
+ 不因 items 为空禁用 Trigger:表格等场景会在同一次右键里先同步填充 items,
67
+ 若 Trigger 已 disabled,Reka 不会打开菜单(与子节点填充产生竞态)。
68
+ 空菜单仍由 onUpdateOpen / onTriggerContextMenu 拦截。
69
+ -->
70
+ <ContextMenuTrigger
71
+ as-child
72
+ @contextmenu.stop="onTriggerContextMenu"
73
+ >
35
74
  <slot />
36
75
  </ContextMenuTrigger>
37
76
  <ContextMenuPortal>
@@ -40,7 +79,11 @@ function onSelect(item: RsContextMenuItem) {
40
79
  :side-offset="4"
41
80
  :collision-padding="8"
42
81
  >
43
- <RsContextMenuItems :items="items" @select="onSelect" />
82
+ <RsContextMenuItems
83
+ v-if="hasActionableItems"
84
+ :items="items"
85
+ @select="onSelect"
86
+ />
44
87
  </ContextMenuContent>
45
88
  </ContextMenuPortal>
46
89
  </ContextMenuRoot>
@@ -48,16 +91,11 @@ function onSelect(item: RsContextMenuItem) {
48
91
  </template>
49
92
 
50
93
  <style>
51
- /* 触发器包装:display:contents 令 span 不产生盒模型,不影响父级 flex/grid 布局。
52
- ContextMenuTrigger 默认渲染为 span,加此 class 使其透明于布局。 */
53
- .rs-ctx-trigger {
54
- display: contents;
55
- }
56
-
57
94
  /* ── 菜单容器:macOS 毛玻璃浮层 ── */
58
95
  .rs-context-menu__content,
59
96
  .rs-context-menu__sub-content {
60
- z-index: var(--rs-z-dropdown);
97
+ /* 与 Select/DatePicker 一致:Portal 挂到 body 时须高于 modal,否则 Dialog 内右键会被挡住 */
98
+ z-index: calc(var(--rs-z-modal) + 2);
61
99
  box-sizing: border-box;
62
100
  min-width: 11rem;
63
101
  max-width: min(22rem, calc(100vw - 1rem));
@@ -5,6 +5,14 @@ import RsCalendarGrid from './RsCalendarGrid.vue'
5
5
  import RsIcon from './RsIcon.vue'
6
6
  import RsTimePicker from './RsTimePicker.vue'
7
7
  import { useRsI18n } from '../composables/useRsI18n'
8
+ import { RS_COMPONENT_SIZE_ICON_PX, type RsComponentSize } from '../theme/types'
9
+ import { useResolvedRsComponentSize } from './resolve-size'
10
+ import { useRsFormContext, useRsFormField } from './form-utils'
11
+ import {
12
+ buildLocalInputRules,
13
+ runFormFieldRules,
14
+ type RsFormRuleTrigger,
15
+ } from './form-rules'
8
16
  import {
9
17
  compareDates,
10
18
  EMPTY_DATE_RANGE,
@@ -17,29 +25,43 @@ import {
17
25
  formatDateTimeRangeLabel,
18
26
  getNextMonth,
19
27
  getNowDateTime,
28
+ formatTimestampValue,
20
29
  getTodayDate,
21
30
  isDateRangeEmpty,
22
31
  isDateRangeOrdered,
23
32
  isDateWithinBounds,
33
+ isTimestampRange,
34
+ normalizeShortcutValue,
24
35
  parseDateTimeValue,
25
36
  parseDateValue,
37
+ parseTimestampValue,
38
+ type RsDatePickerModelValue,
39
+ type RsDatePickerShortcut,
40
+ type RsDatePickerValueFormat,
26
41
  type RsDateRangeValue,
27
42
  type RsParsedDate,
28
43
  } from './date-picker-utils'
29
44
  import { formatTimeParts } from './time-picker-utils'
30
45
 
31
- export type { RsDateRangeValue } from './date-picker-utils'
46
+ export type {
47
+ RsDatePickerModelValue,
48
+ RsDatePickerShortcut,
49
+ RsDatePickerValueFormat,
50
+ RsDateRangeValue,
51
+ } from './date-picker-utils'
32
52
  export type RsDatePickerLabelPosition = 'top' | 'left'
33
53
 
34
- function isRangeModel(value: string | RsDateRangeValue): value is RsDateRangeValue {
35
- return typeof value === 'object' && value !== null && ('start' in value || 'end' in value)
54
+ function isRangeModel(value: RsDatePickerModelValue): value is RsDateRangeValue {
55
+ return typeof value === 'object' && value !== null && !Array.isArray(value) && ('start' in value || 'end' in value)
36
56
  }
37
57
 
38
- const model = defineModel<string | RsDateRangeValue>({ default: '' })
58
+ const model = defineModel<RsDatePickerModelValue>({ default: '' })
39
59
  const open = defineModel<boolean>('open', { default: false })
40
60
 
41
61
  const props = withDefaults(
42
62
  defineProps<{
63
+ /** 表单字段名,用于 RsForm rules 匹配与校验注册 */
64
+ name?: string
43
65
  label?: string
44
66
  hint?: string
45
67
  placeholder?: string
@@ -49,21 +71,37 @@ const props = withDefaults(
49
71
  minDate?: string
50
72
  maxDate?: string
51
73
  withTime?: boolean
74
+ /**
75
+ * 是否选择秒。未传时:withTime 为 true 则默认带秒,纯日期为 false。
76
+ */
52
77
  withSeconds?: boolean
78
+ /** string:YYYY-MM-DD[/HH:mm:ss];timestamp:毫秒时间戳 / [start, end] */
79
+ valueFormat?: RsDatePickerValueFormat
53
80
  labelPosition?: RsDatePickerLabelPosition
81
+ /** 底部快捷选项(有值时替代「今天/现在」链接) */
82
+ shortcuts?: RsDatePickerShortcut[]
83
+ size?: RsComponentSize
54
84
  }>(),
55
85
  {
56
86
  disabled: false,
57
87
  required: false,
58
88
  range: false,
59
89
  withTime: false,
60
- withSeconds: false,
90
+ valueFormat: 'string',
61
91
  labelPosition: 'top',
62
92
  },
63
93
  )
64
94
 
65
95
  const fieldId = useId()
66
96
  const { t } = useRsI18n()
97
+ const resolvedSize = useResolvedRsComponentSize(() => props.size)
98
+ const triggerIconSize = computed(() => RS_COMPONENT_SIZE_ICON_PX[resolvedSize.value])
99
+ /** withTime 默认带秒;显式 withSeconds 优先(勿与 prop 同名,避免模板取到未传 prop) */
100
+ const resolvedWithSeconds = computed(() =>
101
+ props.withSeconds !== undefined ? props.withSeconds : props.withTime,
102
+ )
103
+ const formContext = useRsFormContext()
104
+ const autoMessage = ref('')
67
105
 
68
106
  const viewYear = ref(getTodayDate().year)
69
107
  const viewMonth = ref(getTodayDate().month)
@@ -79,12 +117,28 @@ const draftTime = ref('')
79
117
  const draftStartTime = ref('')
80
118
  const draftEndTime = ref('')
81
119
 
120
+ const useTimestamp = computed(() => props.valueFormat === 'timestamp')
121
+
82
122
  const rangeModel = computed<RsDateRangeValue>({
83
123
  get() {
84
124
  if (!props.range) return { ...EMPTY_DATE_RANGE }
85
- return isRangeModel(model.value) ? model.value : { ...EMPTY_DATE_RANGE }
125
+ const value = model.value
126
+ if (useTimestamp.value) {
127
+ if (!isTimestampRange(value)) return { ...EMPTY_DATE_RANGE }
128
+ return {
129
+ start: formatTimestampValue(value[0], props.withTime),
130
+ end: formatTimestampValue(value[1], props.withTime),
131
+ }
132
+ }
133
+ return isRangeModel(value) ? value : { ...EMPTY_DATE_RANGE }
86
134
  },
87
135
  set(value) {
136
+ if (useTimestamp.value) {
137
+ const start = parseTimestampValue(value.start)
138
+ const end = parseTimestampValue(value.end)
139
+ model.value = start != null && end != null ? [start, end] : null
140
+ return
141
+ }
88
142
  model.value = value
89
143
  },
90
144
  })
@@ -92,9 +146,17 @@ const rangeModel = computed<RsDateRangeValue>({
92
146
  const singleModel = computed({
93
147
  get() {
94
148
  if (props.range) return ''
95
- return typeof model.value === 'string' ? model.value : ''
149
+ const value = model.value
150
+ if (useTimestamp.value) {
151
+ return typeof value === 'number' ? formatTimestampValue(value, props.withTime) : ''
152
+ }
153
+ return typeof value === 'string' ? value : ''
96
154
  },
97
155
  set(value: string) {
156
+ if (useTimestamp.value) {
157
+ model.value = value ? parseTimestampValue(value) : null
158
+ return
159
+ }
98
160
  model.value = value
99
161
  },
100
162
  })
@@ -139,7 +201,7 @@ const triggerIcon = computed(() => {
139
201
 
140
202
  function currentTimeValue(): string {
141
203
  const now = getNowDateTime()
142
- return formatTimeParts(now.hour, now.minute, props.withSeconds ? now.second : undefined)
204
+ return formatTimeParts(now.hour, now.minute, resolvedWithSeconds.value ? now.second : undefined)
143
205
  }
144
206
 
145
207
  function resetRangeTimeDrafts(): void {
@@ -149,7 +211,7 @@ function resetRangeTimeDrafts(): void {
149
211
  }
150
212
 
151
213
  function combineDraftDateTime(date: RsParsedDate, time: string): string | null {
152
- return formatDateTimeParts(date, time, props.withSeconds)
214
+ return formatDateTimeParts(date, time, resolvedWithSeconds.value)
153
215
  }
154
216
 
155
217
  function setEndViewFromStart(start: RsParsedDate): void {
@@ -163,7 +225,7 @@ function syncSingleDraft(): void {
163
225
  const parsed = parseDateTimeValue(singleModel.value)
164
226
  if (parsed) {
165
227
  draftDate.value = { year: parsed.year, month: parsed.month, day: parsed.day }
166
- draftTime.value = extractTimeFromDateTime(singleModel.value, props.withSeconds)
228
+ draftTime.value = extractTimeFromDateTime(singleModel.value, resolvedWithSeconds.value)
167
229
  viewYear.value = parsed.year
168
230
  viewMonth.value = parsed.month
169
231
  return
@@ -205,7 +267,7 @@ function syncRangeDraft(): void {
205
267
  draftEnd.value = end ? { year: end.year, month: end.month, day: end.day } : null
206
268
 
207
269
  if (start) {
208
- draftStartTime.value = extractTimeFromDateTime(value.start, props.withSeconds)
270
+ draftStartTime.value = extractTimeFromDateTime(value.start, resolvedWithSeconds.value)
209
271
  startViewYear.value = start.year
210
272
  startViewMonth.value = start.month
211
273
  } else {
@@ -215,7 +277,7 @@ function syncRangeDraft(): void {
215
277
  }
216
278
 
217
279
  if (end) {
218
- draftEndTime.value = extractTimeFromDateTime(value.end, props.withSeconds)
280
+ draftEndTime.value = extractTimeFromDateTime(value.end, resolvedWithSeconds.value)
219
281
  endViewYear.value = end.year
220
282
  endViewMonth.value = end.month
221
283
  } else if (start) {
@@ -329,14 +391,42 @@ function confirmSelection(): void {
329
391
  confirmSingleSelection()
330
392
  }
331
393
 
394
+ function applyShortcut(shortcut: RsDatePickerShortcut): void {
395
+ if (props.disabled) return
396
+ const normalized = normalizeShortcutValue(shortcut.value(), { withTime: props.withTime })
397
+ if (!normalized) return
398
+
399
+ if (props.range) {
400
+ const rangeValue: RsDateRangeValue =
401
+ typeof normalized === 'string' ? { start: normalized, end: normalized } : normalized
402
+ if (!rangeValue.start || !rangeValue.end) return
403
+ if (!isDateRangeOrdered(rangeValue)) return
404
+ rangeModel.value = rangeValue
405
+ open.value = false
406
+ return
407
+ }
408
+
409
+ if (typeof normalized !== 'string') return
410
+ singleModel.value = normalized
411
+ open.value = false
412
+ }
413
+
332
414
  function clearSelection(): void {
333
415
  if (props.range) {
334
- rangeModel.value = { ...EMPTY_DATE_RANGE }
416
+ if (useTimestamp.value) {
417
+ model.value = null
418
+ } else {
419
+ rangeModel.value = { ...EMPTY_DATE_RANGE }
420
+ }
335
421
  draftStart.value = null
336
422
  draftEnd.value = null
337
423
  if (props.withTime) resetRangeTimeDrafts()
338
424
  } else {
339
- singleModel.value = ''
425
+ if (useTimestamp.value) {
426
+ model.value = null
427
+ } else {
428
+ singleModel.value = ''
429
+ }
340
430
  draftDate.value = null
341
431
  if (props.withTime) draftTime.value = currentTimeValue()
342
432
  }
@@ -355,7 +445,7 @@ function selectToday(): void {
355
445
  endViewYear.value = today.year
356
446
  endViewMonth.value = today.month
357
447
  if (props.withTime) {
358
- const time = formatTimeParts(now.hour, now.minute, props.withSeconds ? now.second : undefined)
448
+ const time = formatTimeParts(now.hour, now.minute, resolvedWithSeconds.value ? now.second : undefined)
359
449
  draftStartTime.value = time
360
450
  draftEndTime.value = time
361
451
  }
@@ -367,7 +457,7 @@ function selectToday(): void {
367
457
  viewYear.value = today.year
368
458
  viewMonth.value = today.month
369
459
  if (props.withTime) {
370
- draftTime.value = formatTimeParts(now.hour, now.minute, props.withSeconds ? now.second : undefined)
460
+ draftTime.value = formatTimeParts(now.hour, now.minute, resolvedWithSeconds.value ? now.second : undefined)
371
461
  }
372
462
  confirmSingleSelection()
373
463
  }
@@ -417,8 +507,62 @@ function handleEndSelect(date: RsParsedDate): void {
417
507
  endViewMonth.value = date.month
418
508
  }
419
509
 
510
+ function clearValidation(): void {
511
+ autoMessage.value = ''
512
+ }
513
+
514
+ function setFieldValue(value: unknown): void {
515
+ if (props.range) {
516
+ if (isRangeModel(value as RsDatePickerModelValue)) {
517
+ model.value = value as RsDateRangeValue
518
+ return
519
+ }
520
+ if (Array.isArray(value) && value.length >= 2) {
521
+ model.value = { start: String(value[0] ?? ''), end: String(value[1] ?? '') }
522
+ return
523
+ }
524
+ model.value = { ...EMPTY_DATE_RANGE }
525
+ return
526
+ }
527
+ model.value = value == null ? '' : (value as RsDatePickerModelValue)
528
+ }
529
+
530
+ async function runValidate(trigger: RsFormRuleTrigger = 'submit') {
531
+ const formRules = formContext?.getFieldRules(props.name) ?? []
532
+ const localRules = buildLocalInputRules({ required: props.required })
533
+ const rules = [...formRules, ...localRules]
534
+ if (!rules.length) {
535
+ autoMessage.value = ''
536
+ return { valid: true as const, name: props.name }
537
+ }
538
+ const result = await runFormFieldRules(model.value, rules, { trigger })
539
+ autoMessage.value = result.message ?? ''
540
+ return { valid: result.valid, message: result.message, name: props.name }
541
+ }
542
+
543
+ useRsFormField(() => ({
544
+ get name() {
545
+ return props.name
546
+ },
547
+ getValue: () => model.value,
548
+ setValue: setFieldValue,
549
+ validate: (trigger) => runValidate(trigger ?? 'submit'),
550
+ clearValidation,
551
+ setError: (message: string) => {
552
+ autoMessage.value = message
553
+ },
554
+ }))
555
+
556
+ defineExpose({
557
+ setValue: setFieldValue,
558
+ clearValidation,
559
+ validate: runValidate,
560
+ })
561
+
420
562
  watch(open, (isOpen) => {
421
563
  if (isOpen) syncDraftFromModel()
564
+ // 关闭面板后按 change 触发校验(对齐选择类控件)
565
+ if (!isOpen && props.name) void runValidate('change')
422
566
  })
423
567
  </script>
424
568
 
@@ -430,20 +574,24 @@ watch(open, (isOpen) => {
430
574
  <span v-if="required" class="rs-field__required" aria-hidden="true">*</span>
431
575
  </span>
432
576
 
433
- <div class="rs-date-picker">
577
+ <div class="rs-date-picker" :class="`rs-date-picker--${resolvedSize}`">
434
578
  <PopoverRoot v-model:open="open">
435
579
  <PopoverTrigger
436
580
  :id="fieldId"
437
581
  type="button"
438
582
  class="rs-date-picker__trigger"
439
- :class="{ 'rs-date-picker__trigger--placeholder': isEmpty }"
583
+ :class="{
584
+ 'rs-date-picker__trigger--placeholder': isEmpty,
585
+ 'rs-date-picker__trigger--invalid': !!autoMessage,
586
+ }"
440
587
  :disabled="disabled"
588
+ :aria-invalid="autoMessage ? true : undefined"
441
589
  >
442
590
  <span class="rs-date-picker__leading">
443
- <RsIcon :name="triggerIcon" :size="16" class="rs-date-picker__icon" />
591
+ <RsIcon :name="triggerIcon" :size="triggerIconSize" class="rs-date-picker__icon" />
444
592
  <span class="rs-date-picker__value">{{ displayValue }}</span>
445
593
  </span>
446
- <RsIcon name="chevron-down" :size="16" class="rs-date-picker__chevron" />
594
+ <RsIcon name="chevron-down" :size="triggerIconSize" class="rs-date-picker__chevron" />
447
595
  </PopoverTrigger>
448
596
 
449
597
  <PopoverPortal>
@@ -476,7 +624,8 @@ watch(open, (isOpen) => {
476
624
  v-model="draftStartTime"
477
625
  class="rs-date-picker__time-picker"
478
626
  embedded
479
- :with-seconds="withSeconds"
627
+ :size="resolvedSize"
628
+ :with-seconds="resolvedWithSeconds"
480
629
  :disabled="!draftStart || disabled"
481
630
  />
482
631
  </div>
@@ -502,7 +651,8 @@ watch(open, (isOpen) => {
502
651
  v-model="draftEndTime"
503
652
  class="rs-date-picker__time-picker"
504
653
  embedded
505
- :with-seconds="withSeconds"
654
+ :size="resolvedSize"
655
+ :with-seconds="resolvedWithSeconds"
506
656
  :disabled="!draftEnd || disabled"
507
657
  />
508
658
  </div>
@@ -525,24 +675,39 @@ watch(open, (isOpen) => {
525
675
  v-model="draftTime"
526
676
  class="rs-date-picker__time-picker"
527
677
  embedded
528
- :with-seconds="withSeconds"
678
+ :size="resolvedSize"
679
+ :with-seconds="resolvedWithSeconds"
529
680
  :disabled="!draftDate || disabled"
530
681
  />
531
682
  </div>
532
683
  </template>
533
684
 
534
685
  <footer class="rs-date-picker__footer">
535
- <button type="button" class="rs-date-picker__link" @click="selectToday">
536
- {{
537
- range
538
- ? withTime
539
- ? t('dateTimePicker.now')
540
- : t('datePicker.rangeToday')
541
- : withTime
542
- ? t('dateTimePicker.now')
543
- : t('datePicker.today')
544
- }}
545
- </button>
686
+ <div class="rs-date-picker__footer-start">
687
+ <div v-if="shortcuts?.length" class="rs-date-picker__shortcuts">
688
+ <button
689
+ v-for="item in shortcuts"
690
+ :key="item.label"
691
+ type="button"
692
+ class="rs-date-picker__shortcut"
693
+ :disabled="disabled"
694
+ @click="applyShortcut(item)"
695
+ >
696
+ {{ item.label }}
697
+ </button>
698
+ </div>
699
+ <button v-else type="button" class="rs-date-picker__link" @click="selectToday">
700
+ {{
701
+ range
702
+ ? withTime
703
+ ? t('dateTimePicker.now')
704
+ : t('datePicker.rangeToday')
705
+ : withTime
706
+ ? t('dateTimePicker.now')
707
+ : t('datePicker.today')
708
+ }}
709
+ </button>
710
+ </div>
546
711
  <div class="rs-date-picker__actions">
547
712
  <button type="button" class="rs-date-picker__ghost" @click="clearSelection">
548
713
  {{ withTime ? t('dateTimePicker.clear') : t('datePicker.clear') }}
@@ -563,6 +728,9 @@ watch(open, (isOpen) => {
563
728
  </PopoverRoot>
564
729
  </div>
565
730
 
731
+ <p v-if="autoMessage" class="rs-date-picker-field__error" role="alert">
732
+ {{ autoMessage }}
733
+ </p>
566
734
  <span v-if="hint" class="rs-field__hint">{{ hint }}</span>
567
735
  </div>
568
736
  </template>
@@ -576,7 +744,9 @@ watch(open, (isOpen) => {
576
744
  align-items: center;
577
745
  justify-content: space-between;
578
746
  gap: var(--rs-space-sm);
747
+ box-sizing: border-box;
579
748
  width: 100%;
749
+ height: var(--rs-control-height-md);
580
750
  min-height: var(--rs-control-height-md);
581
751
  padding: 0 var(--rs-space-md);
582
752
  border: 1px solid var(--rs-input-border, var(--rs-border));
@@ -585,6 +755,7 @@ watch(open, (isOpen) => {
585
755
  color: var(--rs-text);
586
756
  font: inherit;
587
757
  font-size: var(--rs-font-size-sm);
758
+ line-height: var(--rs-line-height-tight);
588
759
  text-align: left;
589
760
  cursor: pointer;
590
761
  box-shadow: var(--rs-input-shadow, none);
@@ -593,6 +764,24 @@ watch(open, (isOpen) => {
593
764
  box-shadow var(--rs-transition-fast),
594
765
  background var(--rs-transition-fast);
595
766
  }
767
+ .rs-date-picker--ssm .rs-date-picker__trigger {
768
+ height: var(--rs-control-height-ssm);
769
+ min-height: var(--rs-control-height-ssm);
770
+ padding: 0 var(--rs-space-xs);
771
+ font-size: var(--rs-font-size-xs);
772
+ }
773
+ .rs-date-picker--sm .rs-date-picker__trigger {
774
+ height: var(--rs-control-height-sm);
775
+ min-height: var(--rs-control-height-sm);
776
+ padding: 0 var(--rs-space-sm);
777
+ font-size: var(--rs-font-size-xs);
778
+ }
779
+ .rs-date-picker--lg .rs-date-picker__trigger {
780
+ height: var(--rs-control-height-lg);
781
+ min-height: var(--rs-control-height-lg);
782
+ padding: 0 var(--rs-space-lg);
783
+ font-size: var(--rs-font-size-base);
784
+ }
596
785
  .rs-date-picker__trigger:hover:not(:disabled) {
597
786
  border-color: var(--rs-input-border-hover, var(--rs-border));
598
787
  }
@@ -611,6 +800,15 @@ watch(open, (isOpen) => {
611
800
  .rs-date-picker__trigger--placeholder .rs-date-picker__value {
612
801
  color: var(--rs-placeholder);
613
802
  }
803
+ .rs-date-picker__trigger--invalid {
804
+ border-color: var(--rs-danger, var(--rs-color-danger, #dc2626));
805
+ }
806
+ .rs-date-picker-field__error {
807
+ margin: var(--rs-space-xs) 0 0;
808
+ color: var(--rs-danger, var(--rs-color-danger, #dc2626));
809
+ font-size: var(--rs-font-size-xs);
810
+ line-height: var(--rs-line-height-tight);
811
+ }
614
812
  .rs-date-picker__leading {
615
813
  display: inline-flex;
616
814
  min-width: 0;
@@ -691,6 +889,41 @@ watch(open, (isOpen) => {
691
889
  border-top: 1px solid var(--rs-border-subtle);
692
890
  background: var(--rs-surface-elevated);
693
891
  }
892
+ .rs-date-picker__footer-start {
893
+ display: flex;
894
+ flex: 1;
895
+ align-items: center;
896
+ min-width: 0;
897
+ }
898
+ .rs-date-picker__shortcuts {
899
+ display: flex;
900
+ flex-wrap: wrap;
901
+ gap: 0.375rem;
902
+ align-items: center;
903
+ }
904
+ .rs-date-picker__shortcut {
905
+ padding: 0.125rem 0.5rem;
906
+ border: 1px solid var(--rs-border);
907
+ border-radius: var(--rs-radius-xs);
908
+ background: transparent;
909
+ color: var(--rs-text);
910
+ font-size: var(--rs-font-size-xs);
911
+ line-height: 1.5;
912
+ cursor: pointer;
913
+ transition:
914
+ border-color var(--rs-transition-fast),
915
+ color var(--rs-transition-fast),
916
+ background var(--rs-transition-fast);
917
+ }
918
+ .rs-date-picker__shortcut:hover:not(:disabled) {
919
+ border-color: var(--rs-primary);
920
+ color: var(--rs-primary);
921
+ background: color-mix(in srgb, var(--rs-primary) 8%, transparent);
922
+ }
923
+ .rs-date-picker__shortcut:disabled {
924
+ opacity: 0.38;
925
+ cursor: not-allowed;
926
+ }
694
927
  .rs-date-picker__link {
695
928
  border: 0;
696
929
  background: transparent;
@@ -1,11 +1,22 @@
1
1
  <script setup lang="ts">
2
2
  import RsDatePicker from './RsDatePicker.vue'
3
- import type { RsDateRangeValue } from './date-picker-utils'
3
+ import type { RsComponentSize } from '../theme/types'
4
+ import type {
5
+ RsDatePickerModelValue,
6
+ RsDatePickerShortcut,
7
+ RsDatePickerValueFormat,
8
+ RsDateRangeValue,
9
+ } from './date-picker-utils'
4
10
 
5
11
  export type RsDateTimeRangeValue = RsDateRangeValue
6
12
  export type RsDateTimePickerLabelPosition = 'top' | 'left'
13
+ export type {
14
+ RsDatePickerModelValue,
15
+ RsDatePickerShortcut,
16
+ RsDatePickerValueFormat,
17
+ }
7
18
 
8
- const model = defineModel<string | RsDateTimeRangeValue>({ default: '' })
19
+ const model = defineModel<RsDatePickerModelValue>({ default: '' })
9
20
  const open = defineModel<boolean>('open', { default: false })
10
21
 
11
22
  withDefaults(
@@ -19,13 +30,18 @@ withDefaults(
19
30
  withSeconds?: boolean
20
31
  minDate?: string
21
32
  maxDate?: string
33
+ /** string:YYYY-MM-DD HH:mm:ss;timestamp:毫秒时间戳 / [start, end] */
34
+ valueFormat?: RsDatePickerValueFormat
22
35
  labelPosition?: RsDateTimePickerLabelPosition
36
+ shortcuts?: RsDatePickerShortcut[]
37
+ size?: RsComponentSize
23
38
  }>(),
24
39
  {
25
40
  disabled: false,
26
41
  required: false,
27
42
  range: false,
28
43
  withSeconds: false,
44
+ valueFormat: 'string',
29
45
  labelPosition: 'top',
30
46
  },
31
47
  )
@@ -38,5 +54,6 @@ withDefaults(
38
54
  v-model:open="open"
39
55
  with-time
40
56
  :with-seconds="withSeconds"
57
+ :value-format="valueFormat"
41
58
  />
42
59
  </template>