niuma-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/LICENSE +202 -0
- package/NOTICE +25 -0
- package/README.en.md +99 -0
- package/README.md +161 -0
- package/package.json +106 -0
- package/src/__tests__/RsBadge.spec.ts +40 -0
- package/src/__tests__/RsBreadcrumb.spec.ts +172 -0
- package/src/__tests__/RsButton.spec.ts +108 -0
- package/src/__tests__/RsCalendarGrid.spec.ts +82 -0
- package/src/__tests__/RsCard.spec.ts +97 -0
- package/src/__tests__/RsCheckbox.spec.ts +36 -0
- package/src/__tests__/RsCodeEditor.spec.ts +111 -0
- package/src/__tests__/RsConfirmDialog.spec.ts +158 -0
- package/src/__tests__/RsContainer.spec.ts +89 -0
- package/src/__tests__/RsContextMenu.spec.ts +118 -0
- package/src/__tests__/RsDatePicker.spec.ts +88 -0
- package/src/__tests__/RsDateTimePicker.spec.ts +69 -0
- package/src/__tests__/RsDialog.spec.ts +404 -0
- package/src/__tests__/RsDrawer.spec.ts +145 -0
- package/src/__tests__/RsDropdown.spec.ts +135 -0
- package/src/__tests__/RsEmpty.spec.ts +99 -0
- package/src/__tests__/RsForm.spec.ts +173 -0
- package/src/__tests__/RsIcon.spec.ts +244 -0
- package/src/__tests__/RsInput.spec.ts +269 -0
- package/src/__tests__/RsInputNumber.spec.ts +118 -0
- package/src/__tests__/RsLabel.spec.ts +106 -0
- package/src/__tests__/RsLink.spec.ts +49 -0
- package/src/__tests__/RsLoading.spec.ts +84 -0
- package/src/__tests__/RsMarkdown.spec.ts +143 -0
- package/src/__tests__/RsMenu.spec.ts +119 -0
- package/src/__tests__/RsNewComponents.spec.ts +17 -0
- package/src/__tests__/RsPagination.spec.ts +158 -0
- package/src/__tests__/RsPopover.spec.ts +126 -0
- package/src/__tests__/RsScrollbar.spec.ts +53 -0
- package/src/__tests__/RsSelect.spec.ts +291 -0
- package/src/__tests__/RsSidebar.spec.ts +119 -0
- package/src/__tests__/RsSplitPane.spec.ts +177 -0
- package/src/__tests__/RsStatCard.spec.ts +79 -0
- package/src/__tests__/RsSteps.spec.ts +125 -0
- package/src/__tests__/RsTable.perf.spec.ts +103 -0
- package/src/__tests__/RsTable.spec.ts +1135 -0
- package/src/__tests__/RsTabs.spec.ts +328 -0
- package/src/__tests__/RsTerminal.spec.ts +218 -0
- package/src/__tests__/RsTimePicker.spec.ts +77 -0
- package/src/__tests__/RsToaster.spec.ts +95 -0
- package/src/__tests__/RsToolbar.spec.ts +75 -0
- package/src/__tests__/RsTooltip.spec.ts +111 -0
- package/src/__tests__/RsTree.spec.ts +572 -0
- package/src/__tests__/RsUpload.spec.ts +160 -0
- package/src/__tests__/RsVirtualList.spec.ts +77 -0
- package/src/__tests__/date-picker-utils.spec.ts +70 -0
- package/src/__tests__/dialog-viewport.spec.ts +38 -0
- package/src/__tests__/iso-local-datetime.spec.ts +58 -0
- package/src/__tests__/rs-clipboard.spec.ts +142 -0
- package/src/__tests__/setup.ts +12 -0
- package/src/__tests__/split-pane-utils.spec.ts +136 -0
- package/src/__tests__/table-drag.spec.ts +35 -0
- package/src/__tests__/table-edit-utils.spec.ts +194 -0
- package/src/__tests__/table-utils.spec.ts +286 -0
- package/src/__tests__/theme.spec.ts +22 -0
- package/src/__tests__/tree-utils.spec.ts +234 -0
- package/src/__tests__/useTableCellTooltip.spec.ts +101 -0
- package/src/components/RsAvatar.vue +115 -0
- package/src/components/RsBadge.vue +59 -0
- package/src/components/RsBreadcrumb.vue +65 -0
- package/src/components/RsBreadcrumbItems.vue +36 -0
- package/src/components/RsButton.vue +393 -0
- package/src/components/RsCalendarGrid.vue +284 -0
- package/src/components/RsCard.vue +237 -0
- package/src/components/RsCheckbox.vue +189 -0
- package/src/components/RsCodeBlock.vue +278 -0
- package/src/components/RsCodeEditor.vue +597 -0
- package/src/components/RsConfigProvider.vue +92 -0
- package/src/components/RsConfirmDialog.vue +178 -0
- package/src/components/RsContainer.vue +235 -0
- package/src/components/RsContextMenu.vue +238 -0
- package/src/components/RsContextMenuItems.vue +77 -0
- package/src/components/RsDatePicker.vue +756 -0
- package/src/components/RsDateTimePicker.vue +42 -0
- package/src/components/RsDialog.vue +486 -0
- package/src/components/RsDrawer.vue +175 -0
- package/src/components/RsDropdown.vue +252 -0
- package/src/components/RsDropdownItems.vue +90 -0
- package/src/components/RsEmpty.vue +107 -0
- package/src/components/RsForm.vue +131 -0
- package/src/components/RsIcon.vue +104 -0
- package/src/components/RsInput.vue +1101 -0
- package/src/components/RsInputNumber.vue +532 -0
- package/src/components/RsLabel.vue +54 -0
- package/src/components/RsLink.vue +106 -0
- package/src/components/RsLoading.vue +171 -0
- package/src/components/RsMarkdown.vue +329 -0
- package/src/components/RsMenu.vue +256 -0
- package/src/components/RsMenuItems.vue +125 -0
- package/src/components/RsMonacoEditor.vue +541 -0
- package/src/components/RsPagination.vue +184 -0
- package/src/components/RsPopover.vue +77 -0
- package/src/components/RsProseEditor.vue +160 -0
- package/src/components/RsScrollbar.vue +171 -0
- package/src/components/RsSelect.vue +1045 -0
- package/src/components/RsSidebar.vue +121 -0
- package/src/components/RsSidebarGroup.vue +32 -0
- package/src/components/RsSidebarItem.vue +72 -0
- package/src/components/RsSplitPane.vue +619 -0
- package/src/components/RsStatCard.vue +85 -0
- package/src/components/RsSteps.vue +138 -0
- package/src/components/RsTable.vue +2664 -0
- package/src/components/RsTableColgroup.vue +17 -0
- package/src/components/RsTableHeaderRow.vue +30 -0
- package/src/components/RsTabs.vue +951 -0
- package/src/components/RsTerminal.vue +697 -0
- package/src/components/RsTimePicker.vue +554 -0
- package/src/components/RsTimePickerColumns.vue +226 -0
- package/src/components/RsToaster.vue +396 -0
- package/src/components/RsToolbar.vue +135 -0
- package/src/components/RsTooltip.vue +142 -0
- package/src/components/RsTooltipProvider.vue +20 -0
- package/src/components/RsTree.vue +1248 -0
- package/src/components/RsUpload.vue +279 -0
- package/src/components/RsVirtualList.vue +217 -0
- package/src/components/avatar-utils.ts +19 -0
- package/src/components/breadcrumb-utils.ts +19 -0
- package/src/components/class-utils.ts +29 -0
- package/src/components/code-editor-utils.ts +106 -0
- package/src/components/code-mirror-completion.ts +40 -0
- package/src/components/code-mirror-diagnostics.ts +52 -0
- package/src/components/code-mirror-ghost.ts +158 -0
- package/src/components/code-mirror-inline-edit.ts +55 -0
- package/src/components/code-mirror-intel.ts +81 -0
- package/src/components/code-mirror-lang.ts +128 -0
- package/src/components/context-menu-utils.ts +20 -0
- package/src/components/date-picker-utils.ts +284 -0
- package/src/components/date-range-validation.ts +29 -0
- package/src/components/date-validation.ts +38 -0
- package/src/components/dialog-viewport.ts +48 -0
- package/src/components/dialog-window.ts +253 -0
- package/src/components/dropdown-utils.ts +31 -0
- package/src/components/form-utils.ts +77 -0
- package/src/components/input-number-utils.ts +128 -0
- package/src/components/input-rules.ts +84 -0
- package/src/components/loading-utils.ts +8 -0
- package/src/components/markdown-utils.ts +126 -0
- package/src/components/menu-utils.ts +48 -0
- package/src/components/overlay-utils.ts +47 -0
- package/src/components/pagination-utils.ts +38 -0
- package/src/components/popover-utils.ts +8 -0
- package/src/components/reka.ts +106 -0
- package/src/components/resolve-radius.ts +22 -0
- package/src/components/resolve-size.ts +29 -0
- package/src/components/scrollbar-utils.ts +10 -0
- package/src/components/select-utils.ts +72 -0
- package/src/components/split-pane-utils.ts +187 -0
- package/src/components/steps-utils.ts +22 -0
- package/src/components/table/RsTableBodyRow.vue +287 -0
- package/src/components/table/RsTableCell.vue +239 -0
- package/src/components/table/RsTableCellContent.ts +18 -0
- package/src/components/table/RsTableCellEditor.vue +351 -0
- package/src/components/table/RsTableEditGutter.vue +54 -0
- package/src/components/table/RsTableHeaderFilter.vue +107 -0
- package/src/components/table/RsTableRowStatus.vue +69 -0
- package/src/components/table/rs-table.css +1141 -0
- package/src/components/table/table-context-menu.ts +53 -0
- package/src/components/table/table-edit-utils.ts +496 -0
- package/src/components/table/table-keys.ts +12 -0
- package/src/components/table-drag.ts +215 -0
- package/src/components/table-utils.ts +950 -0
- package/src/components/tabs-utils.ts +107 -0
- package/src/components/terminal-utils.ts +301 -0
- package/src/components/terminal-wheel.ts +49 -0
- package/src/components/time-picker-utils.ts +167 -0
- package/src/components/time-range-validation.ts +23 -0
- package/src/components/time-validation.ts +20 -0
- package/src/components/tree-utils.ts +518 -0
- package/src/components/upload-utils.ts +55 -0
- package/src/components/virtual-list-utils.ts +17 -0
- package/src/composables/useClickOutside.ts +22 -0
- package/src/composables/useRsConfig.ts +91 -0
- package/src/composables/useRsI18n.ts +22 -0
- package/src/composables/useRsTabsNav.ts +187 -0
- package/src/composables/useRsToast.ts +48 -0
- package/src/composables/useTableCellTooltip.ts +239 -0
- package/src/composables/useTableEdit.ts +271 -0
- package/src/composables/useTableRowHighlight.ts +50 -0
- package/src/dev/vite-codemirror-deps.ts +20 -0
- package/src/icons/custom/api.ts +54 -0
- package/src/icons/custom/clickhouse.ts +52 -0
- package/src/icons/custom/dameng.ts +83 -0
- package/src/icons/custom/ftp.ts +61 -0
- package/src/icons/custom/kingbase.ts +52 -0
- package/src/icons/custom/mongodb.ts +50 -0
- package/src/icons/custom/mysql.ts +51 -0
- package/src/icons/custom/oracle.ts +52 -0
- package/src/icons/custom/redis.ts +51 -0
- package/src/icons/custom/sqlite.ts +50 -0
- package/src/icons/custom/sqlserver.ts +52 -0
- package/src/icons/custom/vastbase.ts +57 -0
- package/src/icons/lucide.ts +78 -0
- package/src/icons/registry.ts +35 -0
- package/src/index.ts +425 -0
- package/src/lib/iso-local-datetime.ts +152 -0
- package/src/lib/rs-dayjs.ts +56 -0
- package/src/locale/messages.ts +362 -0
- package/src/locale/types.ts +5 -0
- package/src/monaco/debug-decorations.css +20 -0
- package/src/monaco/debug-decorations.ts +70 -0
- package/src/monaco/index.ts +22 -0
- package/src/monaco/languages/index.ts +21 -0
- package/src/monaco/languages/mongodb-shell.ts +125 -0
- package/src/monaco/languages/types.ts +55 -0
- package/src/monaco/setup.ts +101 -0
- package/src/styles.css +1040 -0
- package/src/theme/apply.ts +12 -0
- package/src/theme/brand.example.css +37 -0
- package/src/theme/presets.ts +69 -0
- package/src/theme/types.ts +81 -0
- package/src/utils/rs-clipboard.ts +272 -0
- package/src/vite-env.d.ts +7 -0
- package/vite-plugins/monaco-zh-nls.ts +101 -0
- package/vite-plugins/silence-antlr-parse-console.ts +29 -0
|
@@ -0,0 +1,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
|
+
}
|