niuma-ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +25 -0
  4. package/README.en.md +99 -0
  5. package/README.md +161 -0
  6. package/package.json +106 -0
  7. package/src/__tests__/RsBadge.spec.ts +40 -0
  8. package/src/__tests__/RsBreadcrumb.spec.ts +172 -0
  9. package/src/__tests__/RsButton.spec.ts +108 -0
  10. package/src/__tests__/RsCalendarGrid.spec.ts +82 -0
  11. package/src/__tests__/RsCard.spec.ts +97 -0
  12. package/src/__tests__/RsCheckbox.spec.ts +36 -0
  13. package/src/__tests__/RsCodeEditor.spec.ts +111 -0
  14. package/src/__tests__/RsConfirmDialog.spec.ts +158 -0
  15. package/src/__tests__/RsContainer.spec.ts +89 -0
  16. package/src/__tests__/RsContextMenu.spec.ts +118 -0
  17. package/src/__tests__/RsDatePicker.spec.ts +88 -0
  18. package/src/__tests__/RsDateTimePicker.spec.ts +69 -0
  19. package/src/__tests__/RsDialog.spec.ts +404 -0
  20. package/src/__tests__/RsDrawer.spec.ts +145 -0
  21. package/src/__tests__/RsDropdown.spec.ts +135 -0
  22. package/src/__tests__/RsEmpty.spec.ts +99 -0
  23. package/src/__tests__/RsForm.spec.ts +173 -0
  24. package/src/__tests__/RsIcon.spec.ts +244 -0
  25. package/src/__tests__/RsInput.spec.ts +269 -0
  26. package/src/__tests__/RsInputNumber.spec.ts +118 -0
  27. package/src/__tests__/RsLabel.spec.ts +106 -0
  28. package/src/__tests__/RsLink.spec.ts +49 -0
  29. package/src/__tests__/RsLoading.spec.ts +84 -0
  30. package/src/__tests__/RsMarkdown.spec.ts +143 -0
  31. package/src/__tests__/RsMenu.spec.ts +119 -0
  32. package/src/__tests__/RsNewComponents.spec.ts +17 -0
  33. package/src/__tests__/RsPagination.spec.ts +158 -0
  34. package/src/__tests__/RsPopover.spec.ts +126 -0
  35. package/src/__tests__/RsScrollbar.spec.ts +53 -0
  36. package/src/__tests__/RsSelect.spec.ts +291 -0
  37. package/src/__tests__/RsSidebar.spec.ts +119 -0
  38. package/src/__tests__/RsSplitPane.spec.ts +177 -0
  39. package/src/__tests__/RsStatCard.spec.ts +79 -0
  40. package/src/__tests__/RsSteps.spec.ts +125 -0
  41. package/src/__tests__/RsTable.perf.spec.ts +103 -0
  42. package/src/__tests__/RsTable.spec.ts +1135 -0
  43. package/src/__tests__/RsTabs.spec.ts +328 -0
  44. package/src/__tests__/RsTerminal.spec.ts +218 -0
  45. package/src/__tests__/RsTimePicker.spec.ts +77 -0
  46. package/src/__tests__/RsToaster.spec.ts +95 -0
  47. package/src/__tests__/RsToolbar.spec.ts +75 -0
  48. package/src/__tests__/RsTooltip.spec.ts +111 -0
  49. package/src/__tests__/RsTree.spec.ts +572 -0
  50. package/src/__tests__/RsUpload.spec.ts +160 -0
  51. package/src/__tests__/RsVirtualList.spec.ts +77 -0
  52. package/src/__tests__/date-picker-utils.spec.ts +70 -0
  53. package/src/__tests__/dialog-viewport.spec.ts +38 -0
  54. package/src/__tests__/iso-local-datetime.spec.ts +58 -0
  55. package/src/__tests__/rs-clipboard.spec.ts +142 -0
  56. package/src/__tests__/setup.ts +12 -0
  57. package/src/__tests__/split-pane-utils.spec.ts +136 -0
  58. package/src/__tests__/table-drag.spec.ts +35 -0
  59. package/src/__tests__/table-edit-utils.spec.ts +194 -0
  60. package/src/__tests__/table-utils.spec.ts +286 -0
  61. package/src/__tests__/theme.spec.ts +22 -0
  62. package/src/__tests__/tree-utils.spec.ts +234 -0
  63. package/src/__tests__/useTableCellTooltip.spec.ts +101 -0
  64. package/src/components/RsAvatar.vue +115 -0
  65. package/src/components/RsBadge.vue +59 -0
  66. package/src/components/RsBreadcrumb.vue +65 -0
  67. package/src/components/RsBreadcrumbItems.vue +36 -0
  68. package/src/components/RsButton.vue +393 -0
  69. package/src/components/RsCalendarGrid.vue +284 -0
  70. package/src/components/RsCard.vue +237 -0
  71. package/src/components/RsCheckbox.vue +189 -0
  72. package/src/components/RsCodeBlock.vue +278 -0
  73. package/src/components/RsCodeEditor.vue +597 -0
  74. package/src/components/RsConfigProvider.vue +92 -0
  75. package/src/components/RsConfirmDialog.vue +178 -0
  76. package/src/components/RsContainer.vue +235 -0
  77. package/src/components/RsContextMenu.vue +238 -0
  78. package/src/components/RsContextMenuItems.vue +77 -0
  79. package/src/components/RsDatePicker.vue +756 -0
  80. package/src/components/RsDateTimePicker.vue +42 -0
  81. package/src/components/RsDialog.vue +486 -0
  82. package/src/components/RsDrawer.vue +175 -0
  83. package/src/components/RsDropdown.vue +252 -0
  84. package/src/components/RsDropdownItems.vue +90 -0
  85. package/src/components/RsEmpty.vue +107 -0
  86. package/src/components/RsForm.vue +131 -0
  87. package/src/components/RsIcon.vue +104 -0
  88. package/src/components/RsInput.vue +1101 -0
  89. package/src/components/RsInputNumber.vue +532 -0
  90. package/src/components/RsLabel.vue +54 -0
  91. package/src/components/RsLink.vue +106 -0
  92. package/src/components/RsLoading.vue +171 -0
  93. package/src/components/RsMarkdown.vue +329 -0
  94. package/src/components/RsMenu.vue +256 -0
  95. package/src/components/RsMenuItems.vue +125 -0
  96. package/src/components/RsMonacoEditor.vue +541 -0
  97. package/src/components/RsPagination.vue +184 -0
  98. package/src/components/RsPopover.vue +77 -0
  99. package/src/components/RsProseEditor.vue +160 -0
  100. package/src/components/RsScrollbar.vue +171 -0
  101. package/src/components/RsSelect.vue +1045 -0
  102. package/src/components/RsSidebar.vue +121 -0
  103. package/src/components/RsSidebarGroup.vue +32 -0
  104. package/src/components/RsSidebarItem.vue +72 -0
  105. package/src/components/RsSplitPane.vue +619 -0
  106. package/src/components/RsStatCard.vue +85 -0
  107. package/src/components/RsSteps.vue +138 -0
  108. package/src/components/RsTable.vue +2664 -0
  109. package/src/components/RsTableColgroup.vue +17 -0
  110. package/src/components/RsTableHeaderRow.vue +30 -0
  111. package/src/components/RsTabs.vue +951 -0
  112. package/src/components/RsTerminal.vue +697 -0
  113. package/src/components/RsTimePicker.vue +554 -0
  114. package/src/components/RsTimePickerColumns.vue +226 -0
  115. package/src/components/RsToaster.vue +396 -0
  116. package/src/components/RsToolbar.vue +135 -0
  117. package/src/components/RsTooltip.vue +142 -0
  118. package/src/components/RsTooltipProvider.vue +20 -0
  119. package/src/components/RsTree.vue +1248 -0
  120. package/src/components/RsUpload.vue +279 -0
  121. package/src/components/RsVirtualList.vue +217 -0
  122. package/src/components/avatar-utils.ts +19 -0
  123. package/src/components/breadcrumb-utils.ts +19 -0
  124. package/src/components/class-utils.ts +29 -0
  125. package/src/components/code-editor-utils.ts +106 -0
  126. package/src/components/code-mirror-completion.ts +40 -0
  127. package/src/components/code-mirror-diagnostics.ts +52 -0
  128. package/src/components/code-mirror-ghost.ts +158 -0
  129. package/src/components/code-mirror-inline-edit.ts +55 -0
  130. package/src/components/code-mirror-intel.ts +81 -0
  131. package/src/components/code-mirror-lang.ts +128 -0
  132. package/src/components/context-menu-utils.ts +20 -0
  133. package/src/components/date-picker-utils.ts +284 -0
  134. package/src/components/date-range-validation.ts +29 -0
  135. package/src/components/date-validation.ts +38 -0
  136. package/src/components/dialog-viewport.ts +48 -0
  137. package/src/components/dialog-window.ts +253 -0
  138. package/src/components/dropdown-utils.ts +31 -0
  139. package/src/components/form-utils.ts +77 -0
  140. package/src/components/input-number-utils.ts +128 -0
  141. package/src/components/input-rules.ts +84 -0
  142. package/src/components/loading-utils.ts +8 -0
  143. package/src/components/markdown-utils.ts +126 -0
  144. package/src/components/menu-utils.ts +48 -0
  145. package/src/components/overlay-utils.ts +47 -0
  146. package/src/components/pagination-utils.ts +38 -0
  147. package/src/components/popover-utils.ts +8 -0
  148. package/src/components/reka.ts +106 -0
  149. package/src/components/resolve-radius.ts +22 -0
  150. package/src/components/resolve-size.ts +29 -0
  151. package/src/components/scrollbar-utils.ts +10 -0
  152. package/src/components/select-utils.ts +72 -0
  153. package/src/components/split-pane-utils.ts +187 -0
  154. package/src/components/steps-utils.ts +22 -0
  155. package/src/components/table/RsTableBodyRow.vue +287 -0
  156. package/src/components/table/RsTableCell.vue +239 -0
  157. package/src/components/table/RsTableCellContent.ts +18 -0
  158. package/src/components/table/RsTableCellEditor.vue +351 -0
  159. package/src/components/table/RsTableEditGutter.vue +54 -0
  160. package/src/components/table/RsTableHeaderFilter.vue +107 -0
  161. package/src/components/table/RsTableRowStatus.vue +69 -0
  162. package/src/components/table/rs-table.css +1141 -0
  163. package/src/components/table/table-context-menu.ts +53 -0
  164. package/src/components/table/table-edit-utils.ts +496 -0
  165. package/src/components/table/table-keys.ts +12 -0
  166. package/src/components/table-drag.ts +215 -0
  167. package/src/components/table-utils.ts +950 -0
  168. package/src/components/tabs-utils.ts +107 -0
  169. package/src/components/terminal-utils.ts +301 -0
  170. package/src/components/terminal-wheel.ts +49 -0
  171. package/src/components/time-picker-utils.ts +167 -0
  172. package/src/components/time-range-validation.ts +23 -0
  173. package/src/components/time-validation.ts +20 -0
  174. package/src/components/tree-utils.ts +518 -0
  175. package/src/components/upload-utils.ts +55 -0
  176. package/src/components/virtual-list-utils.ts +17 -0
  177. package/src/composables/useClickOutside.ts +22 -0
  178. package/src/composables/useRsConfig.ts +91 -0
  179. package/src/composables/useRsI18n.ts +22 -0
  180. package/src/composables/useRsTabsNav.ts +187 -0
  181. package/src/composables/useRsToast.ts +48 -0
  182. package/src/composables/useTableCellTooltip.ts +239 -0
  183. package/src/composables/useTableEdit.ts +271 -0
  184. package/src/composables/useTableRowHighlight.ts +50 -0
  185. package/src/dev/vite-codemirror-deps.ts +20 -0
  186. package/src/icons/custom/api.ts +54 -0
  187. package/src/icons/custom/clickhouse.ts +52 -0
  188. package/src/icons/custom/dameng.ts +83 -0
  189. package/src/icons/custom/ftp.ts +61 -0
  190. package/src/icons/custom/kingbase.ts +52 -0
  191. package/src/icons/custom/mongodb.ts +50 -0
  192. package/src/icons/custom/mysql.ts +51 -0
  193. package/src/icons/custom/oracle.ts +52 -0
  194. package/src/icons/custom/redis.ts +51 -0
  195. package/src/icons/custom/sqlite.ts +50 -0
  196. package/src/icons/custom/sqlserver.ts +52 -0
  197. package/src/icons/custom/vastbase.ts +57 -0
  198. package/src/icons/lucide.ts +78 -0
  199. package/src/icons/registry.ts +35 -0
  200. package/src/index.ts +425 -0
  201. package/src/lib/iso-local-datetime.ts +152 -0
  202. package/src/lib/rs-dayjs.ts +56 -0
  203. package/src/locale/messages.ts +362 -0
  204. package/src/locale/types.ts +5 -0
  205. package/src/monaco/debug-decorations.css +20 -0
  206. package/src/monaco/debug-decorations.ts +70 -0
  207. package/src/monaco/index.ts +22 -0
  208. package/src/monaco/languages/index.ts +21 -0
  209. package/src/monaco/languages/mongodb-shell.ts +125 -0
  210. package/src/monaco/languages/types.ts +55 -0
  211. package/src/monaco/setup.ts +101 -0
  212. package/src/styles.css +1040 -0
  213. package/src/theme/apply.ts +12 -0
  214. package/src/theme/brand.example.css +37 -0
  215. package/src/theme/presets.ts +69 -0
  216. package/src/theme/types.ts +81 -0
  217. package/src/utils/rs-clipboard.ts +272 -0
  218. package/src/vite-env.d.ts +7 -0
  219. package/vite-plugins/monaco-zh-nls.ts +101 -0
  220. package/vite-plugins/silence-antlr-parse-console.ts +29 -0
@@ -0,0 +1,284 @@
1
+ import {
2
+ dayjs,
3
+ parseRsDateTimeDayjs,
4
+ parseRsDayjs,
5
+ RS_DATE_FORMAT,
6
+ RS_DATETIME_FORMAT,
7
+ RS_TIME_MINUTE_FORMAT,
8
+ RS_TIME_SECONDS_FORMAT,
9
+ } from '../lib/rs-dayjs'
10
+
11
+ export interface RsParsedDate {
12
+ year: number
13
+ month: number
14
+ day: number
15
+ }
16
+
17
+ export interface RsParsedDateTime extends RsParsedDate {
18
+ hour: number
19
+ minute: number
20
+ second: number
21
+ }
22
+
23
+ export interface RsDateRangeValue {
24
+ start?: string
25
+ end?: string
26
+ }
27
+
28
+ export interface RsCalendarCell extends RsParsedDate {
29
+ inCurrentMonth: boolean
30
+ }
31
+
32
+ export const EMPTY_DATE_RANGE: RsDateRangeValue = {}
33
+
34
+ export function parseDateValue(value?: string): RsParsedDate | null {
35
+ const parsed = parseRsDayjs(value)
36
+ if (!parsed) return null
37
+ return {
38
+ year: parsed.year(),
39
+ month: parsed.month() + 1,
40
+ day: parsed.date(),
41
+ }
42
+ }
43
+
44
+ export function parseDateTimeValue(value?: string): RsParsedDateTime | null {
45
+ const parsed = parseRsDateTimeDayjs(value)
46
+ if (!parsed) return null
47
+ return {
48
+ year: parsed.year(),
49
+ month: parsed.month() + 1,
50
+ day: parsed.date(),
51
+ hour: parsed.hour(),
52
+ minute: parsed.minute(),
53
+ second: parsed.second(),
54
+ }
55
+ }
56
+
57
+ export function formatDateValue(value?: string): string {
58
+ const parsed = parseRsDayjs(value)
59
+ return parsed ? parsed.format(RS_DATE_FORMAT) : (value ?? '')
60
+ }
61
+
62
+ export function formatDateParts(date: RsParsedDate): string {
63
+ return dayjs()
64
+ .year(date.year)
65
+ .month(date.month - 1)
66
+ .date(date.day)
67
+ .format(RS_DATE_FORMAT)
68
+ }
69
+
70
+ export function formatDateTimeValue(value?: string): string {
71
+ const parsed = parseRsDateTimeDayjs(value)
72
+ return parsed ? parsed.format(RS_DATETIME_FORMAT) : (value ?? '')
73
+ }
74
+
75
+ export function formatDateTimeParts(
76
+ date: RsParsedDate,
77
+ time: string,
78
+ withSeconds = true,
79
+ ): string | null {
80
+ const dateText = formatDateParts(date)
81
+ const timeFormat = withSeconds ? RS_TIME_SECONDS_FORMAT : RS_TIME_MINUTE_FORMAT
82
+ const parsed = dayjs(`${dateText} ${time}`, `${RS_DATE_FORMAT} ${timeFormat}`, true)
83
+ if (!parsed.isValid()) return null
84
+ return parsed.format(RS_DATETIME_FORMAT)
85
+ }
86
+
87
+ export function extractTimeFromDateTime(value?: string, withSeconds = true): string {
88
+ const parsed = parseRsDateTimeDayjs(value)
89
+ if (!parsed) return withSeconds ? '00:00:00' : '00:00'
90
+ return parsed.format(withSeconds ? RS_TIME_SECONDS_FORMAT : RS_TIME_MINUTE_FORMAT)
91
+ }
92
+
93
+ export function isDateRangeOrdered(range: RsDateRangeValue): boolean {
94
+ if (!range.start || !range.end) return true
95
+ const start = parseRsDateTimeDayjs(range.start) ?? parseRsDayjs(range.start)
96
+ const end = parseRsDateTimeDayjs(range.end) ?? parseRsDayjs(range.end)
97
+ if (!start || !end) return range.start <= range.end
98
+ return start.isSame(end) || start.isBefore(end)
99
+ }
100
+
101
+ export const isDateTimeRangeOrdered = isDateRangeOrdered
102
+
103
+ export function formatDateRangeDisplay(
104
+ range: RsDateRangeValue,
105
+ options?: { separator?: string; datetime?: boolean },
106
+ ): string {
107
+ const separator = options?.separator ?? ' ~ '
108
+ const format = options?.datetime ? formatDateTimeDisplay : formatDateDisplay
109
+ const start = range.start ? format(range.start) : ''
110
+ const end = range.end ? format(range.end) : ''
111
+ if (start && end) return `${start}${separator}${end}`
112
+ return start || end
113
+ }
114
+
115
+ export function formatLocalDate(date: Date): string {
116
+ return dayjs(date).format(RS_DATE_FORMAT)
117
+ }
118
+
119
+ export function formatDateDisplay(value?: string): string {
120
+ return formatDateValue(value)
121
+ }
122
+
123
+ export function formatDateTimeDisplay(value?: string): string {
124
+ return formatDateTimeValue(value)
125
+ }
126
+
127
+ export function formatDateRangeLabel(range: RsDateRangeValue, separator = ' ~ '): string {
128
+ return formatDateRangeDisplay(range, { separator })
129
+ }
130
+
131
+ export function formatDateTimeRangeLabel(range: RsDateRangeValue, separator = ' ~ '): string {
132
+ return formatDateRangeDisplay(range, { separator, datetime: true })
133
+ }
134
+
135
+ export function isDateBetween(value: string, start?: string, end?: string): boolean {
136
+ if (!start || !end) return false
137
+ const current = parseRsDayjs(value)
138
+ const from = parseRsDayjs(start)
139
+ const to = parseRsDayjs(end)
140
+ if (!current || !from || !to) return false
141
+ const min = from.isBefore(to) ? from : to
142
+ const max = from.isBefore(to) ? to : from
143
+ return (current.isSame(min) || current.isAfter(min)) && (current.isSame(max) || current.isBefore(max))
144
+ }
145
+
146
+ export function getTodayDate(): RsParsedDate {
147
+ const today = dayjs()
148
+ return {
149
+ year: today.year(),
150
+ month: today.month() + 1,
151
+ day: today.date(),
152
+ }
153
+ }
154
+
155
+ export function formatTimeFromDateTimeParts(
156
+ value: RsParsedDateTime,
157
+ withSeconds = true,
158
+ ): string {
159
+ const hour = String(value.hour).padStart(2, '0')
160
+ const minute = String(value.minute).padStart(2, '0')
161
+ if (!withSeconds) return `${hour}:${minute}`
162
+ const second = String(value.second).padStart(2, '0')
163
+ return `${hour}:${minute}:${second}`
164
+ }
165
+
166
+ export function getNowDateTime(): RsParsedDateTime {
167
+ const now = dayjs()
168
+ return {
169
+ year: now.year(),
170
+ month: now.month() + 1,
171
+ day: now.date(),
172
+ hour: now.hour(),
173
+ minute: now.minute(),
174
+ second: now.second(),
175
+ }
176
+ }
177
+
178
+ export function getNextMonth(year: number, month: number): Pick<RsParsedDate, 'year' | 'month'> {
179
+ const next = dayjs().year(year).month(month - 1).add(1, 'month')
180
+ return { year: next.year(), month: next.month() + 1 }
181
+ }
182
+
183
+ export function getDaysInMonth(year: number, month: number): number {
184
+ return dayjs().year(year).month(month - 1).daysInMonth()
185
+ }
186
+
187
+ export function dateToNumber(year: number, month: number, day: number): number {
188
+ return year * 10000 + month * 100 + day
189
+ }
190
+
191
+ export function parseDateToNumber(value: string): number | null {
192
+ const parsed = parseDateValue(value)
193
+ if (!parsed) return null
194
+ return dateToNumber(parsed.year, parsed.month, parsed.day)
195
+ }
196
+
197
+ export function compareDates(a: RsParsedDate, b: RsParsedDate): number {
198
+ return dateToNumber(a.year, a.month, a.day) - dateToNumber(b.year, b.month, b.day)
199
+ }
200
+
201
+ export function isSameDate(a: RsParsedDate, b: RsParsedDate): boolean {
202
+ return a.year === b.year && a.month === b.month && a.day === b.day
203
+ }
204
+
205
+ export function isDateBetweenParsed(
206
+ cell: RsParsedDate,
207
+ start: RsParsedDate,
208
+ end: RsParsedDate,
209
+ ): boolean {
210
+ const value = dateToNumber(cell.year, cell.month, cell.day)
211
+ const from = dateToNumber(start.year, start.month, start.day)
212
+ const to = dateToNumber(end.year, end.month, end.day)
213
+ return value >= Math.min(from, to) && value <= Math.max(from, to)
214
+ }
215
+
216
+ export function isDateWithinBounds(
217
+ year: number,
218
+ month: number,
219
+ day: number,
220
+ options: { minDate?: string; maxDate?: string } = {},
221
+ ): boolean {
222
+ const candidate = dateToNumber(year, month, day)
223
+
224
+ if (options.minDate) {
225
+ const min = parseDateToNumber(options.minDate)
226
+ if (min !== null && candidate < min) return false
227
+ }
228
+
229
+ if (options.maxDate) {
230
+ const max = parseDateToNumber(options.maxDate)
231
+ if (max !== null && candidate > max) return false
232
+ }
233
+
234
+ return true
235
+ }
236
+
237
+ export function isDateRangeEmpty(value: RsDateRangeValue): boolean {
238
+ return !value.start && !value.end
239
+ }
240
+
241
+ export function buildCalendarGrid(
242
+ year: number,
243
+ month: number,
244
+ weekStartsOn = 1,
245
+ ): RsCalendarCell[] {
246
+ const daysInMonth = getDaysInMonth(year, month)
247
+ const firstWeekday = dayjs().year(year).month(month - 1).date(1).day()
248
+ const leading = (firstWeekday - weekStartsOn + 7) % 7
249
+
250
+ const prevMonth = month === 1 ? 12 : month - 1
251
+ const prevYear = month === 1 ? year - 1 : year
252
+ const daysInPrevMonth = getDaysInMonth(prevYear, prevMonth)
253
+
254
+ const cells: RsCalendarCell[] = []
255
+
256
+ for (let index = leading; index > 0; index -= 1) {
257
+ cells.push({
258
+ year: prevYear,
259
+ month: prevMonth,
260
+ day: daysInPrevMonth - index + 1,
261
+ inCurrentMonth: false,
262
+ })
263
+ }
264
+
265
+ for (let day = 1; day <= daysInMonth; day += 1) {
266
+ cells.push({ year, month, day, inCurrentMonth: true })
267
+ }
268
+
269
+ const nextMonth = month === 12 ? 1 : month + 1
270
+ const nextYear = month === 12 ? year + 1 : year
271
+ let nextDay = 1
272
+
273
+ while (cells.length < 42) {
274
+ cells.push({
275
+ year: nextYear,
276
+ month: nextMonth,
277
+ day: nextDay,
278
+ inCurrentMonth: false,
279
+ })
280
+ nextDay += 1
281
+ }
282
+
283
+ return cells
284
+ }
@@ -0,0 +1,29 @@
1
+ import type { RsDateRangeValue } from './date-picker-utils'
2
+ import { isDateRangeOrdered } from './date-picker-utils'
3
+
4
+ export interface RsDateRangeValidationRules {
5
+ required?: boolean
6
+ ordered?: boolean
7
+ }
8
+
9
+ export type RsDateRangeValidationError = 'required' | 'order'
10
+
11
+ export interface RsDateRangeValidationResult {
12
+ valid: boolean
13
+ error?: RsDateRangeValidationError
14
+ }
15
+
16
+ export type RsDateTimeRangeValidationRules = RsDateRangeValidationRules
17
+ export type RsDateTimeRangeValidationError = RsDateRangeValidationError
18
+ export type RsDateTimeRangeValidationResult = RsDateRangeValidationResult
19
+
20
+ export function validateDateRangeValue(
21
+ value: RsDateRangeValue,
22
+ rules: RsDateRangeValidationRules = {},
23
+ ): RsDateRangeValidationResult {
24
+ if (rules.required && (!value.start || !value.end)) return { valid: false, error: 'required' }
25
+ if (rules.ordered !== false && !isDateRangeOrdered(value)) return { valid: false, error: 'order' }
26
+ return { valid: true }
27
+ }
28
+
29
+ export const validateDateTimeRangeValue = validateDateRangeValue
@@ -0,0 +1,38 @@
1
+ import { parseRsDateTimeDayjs, parseRsDayjs } from '../lib/rs-dayjs'
2
+
3
+ export interface RsDateValidationRules {
4
+ required?: boolean
5
+ min?: string
6
+ max?: string
7
+ }
8
+
9
+ export type RsDateValidationError = 'required' | 'min' | 'max' | 'format'
10
+
11
+ export interface RsDateValidationResult {
12
+ valid: boolean
13
+ error?: RsDateValidationError
14
+ }
15
+
16
+ export type RsDateTimeValidationRules = RsDateValidationRules
17
+ export type RsDateTimeValidationError = RsDateValidationError
18
+ export type RsDateTimeValidationResult = RsDateValidationResult
19
+
20
+ export function validateDateValue(value: string, rules: RsDateValidationRules = {}): RsDateValidationResult {
21
+ if (!value) return rules.required ? { valid: false, error: 'required' } : { valid: true }
22
+ const parsed = parseRsDayjs(value)
23
+ if (!parsed) return { valid: false, error: 'format' }
24
+ const canonical = parsed.format('YYYY-MM-DD')
25
+ if (rules.min && canonical < rules.min) return { valid: false, error: 'min' }
26
+ if (rules.max && canonical > rules.max) return { valid: false, error: 'max' }
27
+ return { valid: true }
28
+ }
29
+
30
+ export function validateDateTimeValue(value: string, rules: RsDateTimeValidationRules = {}): RsDateTimeValidationResult {
31
+ if (!value) return rules.required ? { valid: false, error: 'required' } : { valid: true }
32
+ const parsed = parseRsDateTimeDayjs(value)
33
+ if (!parsed) return { valid: false, error: 'format' }
34
+ const canonical = parsed.format('YYYY-MM-DD HH:mm:ss')
35
+ if (rules.min && canonical < rules.min) return { valid: false, error: 'min' }
36
+ if (rules.max && canonical > rules.max) return { valid: false, error: 'max' }
37
+ return { valid: true }
38
+ }
@@ -0,0 +1,48 @@
1
+ const viewportGap = 16
2
+
3
+ export interface RsDialogViewportInsets {
4
+ top: number
5
+ right: number
6
+ bottom: number
7
+ left: number
8
+ }
9
+
10
+ function parseCssPx(value: string, fallback: number): number {
11
+ const trimmed = value.trim()
12
+ if (!trimmed) return fallback
13
+ const num = Number.parseFloat(trimmed)
14
+ if (Number.isNaN(num)) return fallback
15
+ if (trimmed.endsWith('rem')) {
16
+ const rootPx = Number.parseFloat(getComputedStyle(document.documentElement).fontSize) || 16
17
+ return num * rootPx
18
+ }
19
+ return num
20
+ }
21
+
22
+ /** 读取对话框可用视口 inset(px),未设置时回退 viewportGap。 */
23
+ export function readDialogViewportInsets(): RsDialogViewportInsets {
24
+ if (typeof document === 'undefined') {
25
+ return { top: viewportGap, right: viewportGap, bottom: viewportGap, left: viewportGap }
26
+ }
27
+ const root = getComputedStyle(document.documentElement)
28
+ const x = parseCssPx(root.getPropertyValue('--rs-dialog-inset-x'), viewportGap)
29
+ return {
30
+ top: parseCssPx(root.getPropertyValue('--rs-dialog-inset-top'), viewportGap),
31
+ right: x,
32
+ bottom: parseCssPx(root.getPropertyValue('--rs-dialog-inset-bottom'), viewportGap),
33
+ left: x,
34
+ }
35
+ }
36
+
37
+ export function dialogViewportSize(): { width: number; height: number } {
38
+ if (typeof window === 'undefined') {
39
+ return { width: 800, height: 600 }
40
+ }
41
+ const insets = readDialogViewportInsets()
42
+ return {
43
+ width: window.innerWidth - insets.left - insets.right,
44
+ height: window.innerHeight - insets.top - insets.bottom,
45
+ }
46
+ }
47
+
48
+ export { viewportGap }
@@ -0,0 +1,253 @@
1
+ import { computed, onBeforeUnmount, ref, watch, type Ref } from 'vue'
2
+ import { dialogViewportSize, readDialogViewportInsets } from './dialog-viewport'
3
+
4
+ export type RsDialogWidth = 'sm' | 'md' | 'lg'
5
+ export type RsDialogResizeHandle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'
6
+
7
+ export interface RsDialogBounds {
8
+ x: number
9
+ y: number
10
+ width: number
11
+ height: number
12
+ }
13
+
14
+ const widthMap: Record<RsDialogWidth, number> = {
15
+ sm: 384,
16
+ md: 448,
17
+ lg: 512,
18
+ }
19
+
20
+ const defaultWidthRatio = 0.8
21
+ const defaultHeightRatio = 0.75
22
+ const minWidth = 320
23
+ const minHeight = 240
24
+
25
+ export function useRsDialogWindow(options: {
26
+ open: Ref<boolean>
27
+ widthPreset: Ref<RsDialogWidth>
28
+ draggable: Ref<boolean>
29
+ resizable: Ref<boolean>
30
+ compact?: Ref<boolean>
31
+ /** 全屏/还原时是否播放 left/top/width/height 过渡(含编辑器时建议 false) */
32
+ boundsTransition?: Ref<boolean>
33
+ }) {
34
+ const isFullscreen = ref(false)
35
+ const boundsTransitionEnabled = ref(false)
36
+ const bounds = ref<RsDialogBounds>({ x: 0, y: 0, width: widthMap.md, height: minHeight })
37
+ const restoreBounds = ref<RsDialogBounds | null>(null)
38
+ const resizeHandles: RsDialogResizeHandle[] = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw']
39
+
40
+ let boundsTransitionTimer: ReturnType<typeof setTimeout> | null = null
41
+ let cachedInsets = readDialogViewportInsets()
42
+
43
+ let dragState: { startX: number; startY: number; origin: RsDialogBounds } | null = null
44
+ let resizeState: {
45
+ handle: RsDialogResizeHandle
46
+ startX: number
47
+ startY: number
48
+ origin: RsDialogBounds
49
+ } | null = null
50
+
51
+ function defaultWindowSize(): Pick<RsDialogBounds, 'width' | 'height'> {
52
+ const { width: maxW, height: maxH } = dialogViewportSize()
53
+ return {
54
+ width: Math.min(maxW, Math.max(minWidth, Math.round(maxW * defaultWidthRatio))),
55
+ height: Math.min(maxH, Math.max(minHeight, Math.round(maxH * defaultHeightRatio))),
56
+ }
57
+ }
58
+
59
+ function refreshCachedInsets(): void {
60
+ cachedInsets = readDialogViewportInsets()
61
+ }
62
+
63
+ function clampBounds(next: RsDialogBounds): RsDialogBounds {
64
+ const insets = cachedInsets
65
+ const maxW = window.innerWidth - insets.left - insets.right
66
+ const maxH = window.innerHeight - insets.top - insets.bottom
67
+ const width = Math.min(maxW, Math.max(minWidth, next.width))
68
+ const height = Math.min(maxH, Math.max(minHeight, next.height))
69
+ return {
70
+ x: Math.min(window.innerWidth - width - insets.right, Math.max(insets.left, next.x)),
71
+ y: Math.min(window.innerHeight - height - insets.bottom, Math.max(insets.top, next.y)),
72
+ width,
73
+ height,
74
+ }
75
+ }
76
+
77
+ function resetOnOpen(): void {
78
+ isFullscreen.value = false
79
+ restoreBounds.value = null
80
+ if (options.compact?.value) {
81
+ bounds.value = { x: bounds.value.x, y: bounds.value.y, width: widthMap[options.widthPreset.value], height: 0 }
82
+ return
83
+ }
84
+ refreshCachedInsets()
85
+ const insets = cachedInsets
86
+ const { width, height } = defaultWindowSize()
87
+ const availableH = window.innerHeight - insets.top - insets.bottom
88
+ bounds.value = clampBounds({
89
+ x: insets.left + (window.innerWidth - insets.left - insets.right - width) / 2,
90
+ y: insets.top + (availableH - height) / 2,
91
+ width,
92
+ height,
93
+ })
94
+ }
95
+
96
+ watch(
97
+ options.open,
98
+ (value) => {
99
+ if (value) resetOnOpen()
100
+ else stopInteractions()
101
+ },
102
+ // 弹出等场景:组件挂载时 open 已为 true,需立即居中,不能只等 false→true
103
+ { immediate: true },
104
+ )
105
+
106
+ const dialogStyle = computed(() => {
107
+ const insets = cachedInsets
108
+ if (isFullscreen.value) {
109
+ return {
110
+ left: `${insets.left}px`,
111
+ top: `${insets.top}px`,
112
+ width: `calc(100vw - ${insets.left + insets.right}px)`,
113
+ height: `calc(100vh - ${insets.top + insets.bottom}px)`,
114
+ transform: 'none',
115
+ }
116
+ }
117
+ if (options.compact?.value) {
118
+ const width = widthMap[options.widthPreset.value]
119
+ return {
120
+ left: '50%',
121
+ top: `calc(${insets.top}px + (100vh - ${insets.top + insets.bottom}px) / 2)`,
122
+ width: `${width}px`,
123
+ maxWidth: `calc(100vw - ${insets.left + insets.right}px)`,
124
+ height: 'auto',
125
+ transform: 'translate(-50%, -50%)',
126
+ }
127
+ }
128
+ const b = bounds.value
129
+ return {
130
+ left: `${b.x}px`,
131
+ top: `${b.y}px`,
132
+ width: `${b.width}px`,
133
+ height: `${b.height}px`,
134
+ transform: 'none',
135
+ }
136
+ })
137
+
138
+ function clearBoundsTransitionTimer(): void {
139
+ if (!boundsTransitionTimer) return
140
+ clearTimeout(boundsTransitionTimer)
141
+ boundsTransitionTimer = null
142
+ }
143
+
144
+ function enableBoundsTransition(): void {
145
+ boundsTransitionEnabled.value = true
146
+ clearBoundsTransitionTimer()
147
+ boundsTransitionTimer = setTimeout(() => {
148
+ boundsTransitionEnabled.value = false
149
+ boundsTransitionTimer = null
150
+ }, 280)
151
+ }
152
+
153
+ function toggleFullscreen(): void {
154
+ refreshCachedInsets()
155
+ if (options.boundsTransition?.value) {
156
+ enableBoundsTransition()
157
+ }
158
+ if (isFullscreen.value) {
159
+ isFullscreen.value = false
160
+ bounds.value = restoreBounds.value ? { ...restoreBounds.value } : bounds.value
161
+ restoreBounds.value = null
162
+ return
163
+ }
164
+ restoreBounds.value = { ...bounds.value }
165
+ isFullscreen.value = true
166
+ }
167
+
168
+ function onPointerMove(event: PointerEvent): void {
169
+ if (dragState) {
170
+ const dx = event.clientX - dragState.startX
171
+ const dy = event.clientY - dragState.startY
172
+ bounds.value = clampBounds({ ...dragState.origin, x: dragState.origin.x + dx, y: dragState.origin.y + dy })
173
+ return
174
+ }
175
+
176
+ if (!resizeState) return
177
+ const dx = event.clientX - resizeState.startX
178
+ const dy = event.clientY - resizeState.startY
179
+ const next = { ...resizeState.origin }
180
+ if (resizeState.handle.includes('e')) next.width = resizeState.origin.width + dx
181
+ if (resizeState.handle.includes('w')) {
182
+ next.width = resizeState.origin.width - dx
183
+ next.x = resizeState.origin.x + dx
184
+ }
185
+ if (resizeState.handle.includes('s')) next.height = resizeState.origin.height + dy
186
+ if (resizeState.handle.includes('n')) {
187
+ next.height = resizeState.origin.height - dy
188
+ next.y = resizeState.origin.y + dy
189
+ }
190
+ bounds.value = clampBounds(next)
191
+ }
192
+
193
+ function stopInteractions(): void {
194
+ dragState = null
195
+ resizeState = null
196
+ window.removeEventListener('pointermove', onPointerMove)
197
+ window.removeEventListener('pointerup', stopInteractions)
198
+ document.body.style.userSelect = ''
199
+ document.body.style.cursor = ''
200
+ }
201
+
202
+ function startInteraction(cursor: string): void {
203
+ boundsTransitionEnabled.value = false
204
+ clearBoundsTransitionTimer()
205
+ refreshCachedInsets()
206
+ window.addEventListener('pointermove', onPointerMove)
207
+ window.addEventListener('pointerup', stopInteractions)
208
+ document.body.style.userSelect = 'none'
209
+ document.body.style.cursor = cursor
210
+ }
211
+
212
+ function onHeaderPointerDown(event: PointerEvent): void {
213
+ if (!options.draggable.value || isFullscreen.value) return
214
+ const target = event.target as HTMLElement
215
+ if (target.closest('button,a,input,textarea,select')) return
216
+ event.preventDefault()
217
+ dragState = { startX: event.clientX, startY: event.clientY, origin: { ...bounds.value } }
218
+ startInteraction('grabbing')
219
+ }
220
+
221
+ const resizeCursor: Record<RsDialogResizeHandle, string> = {
222
+ n: 'ns-resize',
223
+ s: 'ns-resize',
224
+ e: 'ew-resize',
225
+ w: 'ew-resize',
226
+ ne: 'nesw-resize',
227
+ sw: 'nesw-resize',
228
+ nw: 'nwse-resize',
229
+ se: 'nwse-resize',
230
+ }
231
+
232
+ function onResizePointerDown(handle: RsDialogResizeHandle, event: PointerEvent): void {
233
+ if (!options.resizable.value || isFullscreen.value) return
234
+ event.preventDefault()
235
+ resizeState = { handle, startX: event.clientX, startY: event.clientY, origin: { ...bounds.value } }
236
+ startInteraction(resizeCursor[handle])
237
+ }
238
+
239
+ onBeforeUnmount(() => {
240
+ stopInteractions()
241
+ clearBoundsTransitionTimer()
242
+ })
243
+
244
+ return {
245
+ isFullscreen,
246
+ boundsTransitionEnabled,
247
+ dialogStyle,
248
+ resizeHandles,
249
+ toggleFullscreen,
250
+ onHeaderPointerDown,
251
+ onResizePointerDown,
252
+ }
253
+ }
@@ -0,0 +1,31 @@
1
+ export interface RsDropdownItem {
2
+ label: string
3
+ value: string
4
+ icon?: string
5
+ disabled?: boolean
6
+ }
7
+
8
+ export interface RsDropdownItemGroup {
9
+ label: string
10
+ options: RsDropdownItem[]
11
+ }
12
+
13
+ export type RsDropdownItems = RsDropdownItem[] | RsDropdownItemGroup[]
14
+
15
+ export function isDropdownItemGroup(
16
+ item: RsDropdownItem | RsDropdownItemGroup,
17
+ ): item is RsDropdownItemGroup {
18
+ return 'options' in item && Array.isArray(item.options)
19
+ }
20
+
21
+ export function flattenDropdownItems(items: RsDropdownItems): RsDropdownItem[] {
22
+ const result: RsDropdownItem[] = []
23
+ for (const item of items) {
24
+ if (isDropdownItemGroup(item)) {
25
+ result.push(...item.options)
26
+ } else {
27
+ result.push(item)
28
+ }
29
+ }
30
+ return result
31
+ }