niuma-ui 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -1
- package/NOTICE +2 -2
- package/package.json +13 -5
- package/src/__tests__/RsButton.spec.ts +47 -0
- package/src/__tests__/RsCard.spec.ts +160 -1
- package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
- package/src/__tests__/RsContextMenu.spec.ts +12 -0
- package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
- package/src/__tests__/RsDescriptions.spec.ts +50 -0
- package/src/__tests__/RsDialog.spec.ts +117 -0
- package/src/__tests__/RsDrawer.spec.ts +96 -3
- package/src/__tests__/RsDynamicTags.spec.ts +52 -0
- package/src/__tests__/RsForm.spec.ts +245 -1
- package/src/__tests__/RsInput.spec.ts +15 -1
- package/src/__tests__/RsMenu.spec.ts +406 -14
- package/src/__tests__/RsPagination.spec.ts +67 -2
- package/src/__tests__/RsSelect.spec.ts +42 -1
- package/src/__tests__/RsSwitch.spec.ts +44 -0
- package/src/__tests__/RsTable.a11y.spec.ts +83 -0
- package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
- package/src/__tests__/RsTable.perf.spec.ts +23 -4
- package/src/__tests__/RsTable.spec.ts +63 -8
- package/src/__tests__/RsTable.ssr.spec.ts +71 -0
- package/src/__tests__/RsTable.summary.spec.ts +45 -0
- package/src/__tests__/RsTabs.spec.ts +102 -4
- package/src/__tests__/RsTerminal.spec.ts +1 -1
- package/src/__tests__/RsTimePicker.spec.ts +11 -0
- package/src/__tests__/RsUpload.spec.ts +62 -6
- package/src/__tests__/createRsDialog.spec.ts +145 -0
- package/src/__tests__/form-rules.spec.ts +76 -0
- package/src/__tests__/perf-budgets.ts +32 -0
- package/src/__tests__/rs-table-api.spec.ts +206 -0
- package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
- package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
- package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
- package/src/__tests__/rs-table-isolation.spec.ts +158 -0
- package/src/__tests__/split-pane-utils.spec.ts +21 -0
- package/src/__tests__/table-chart-utils.spec.ts +88 -0
- package/src/__tests__/table-summary-utils.spec.ts +62 -0
- package/src/__tests__/table-utils.spec.ts +124 -0
- package/src/__tests__/theme.spec.ts +25 -0
- package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
- package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
- package/src/components/RsAlert.vue +212 -0
- package/src/components/RsButton.vue +145 -21
- package/src/components/RsCard.vue +200 -16
- package/src/components/RsConfirmDialog.vue +215 -28
- package/src/components/RsContextMenu.vue +51 -13
- package/src/components/RsDatePicker.vue +267 -34
- package/src/components/RsDateTimePicker.vue +19 -2
- package/src/components/RsDescriptions.vue +186 -0
- package/src/components/RsDescriptionsItem.vue +32 -0
- package/src/components/RsDialog.vue +363 -51
- package/src/components/RsDivider.vue +89 -0
- package/src/components/RsDrawer.vue +558 -33
- package/src/components/RsDropdown.vue +33 -2
- package/src/components/RsDynamicTags.vue +517 -0
- package/src/components/RsForm.vue +97 -8
- package/src/components/RsInput.vue +80 -82
- package/src/components/RsInputNumber.vue +64 -23
- package/src/components/RsLabel.vue +20 -5
- package/src/components/RsLoadingBar.vue +151 -0
- package/src/components/RsMenu.vue +73 -15
- package/src/components/RsMenuItems.vue +147 -9
- package/src/components/RsPagination.vue +93 -14
- package/src/components/RsRadio.vue +81 -0
- package/src/components/RsRadioItem.vue +156 -0
- package/src/components/RsSelect.vue +67 -58
- package/src/components/RsSplitPane.vue +69 -1
- package/src/components/RsSwitch.vue +179 -0
- package/src/components/RsTable.vue +653 -2555
- package/src/components/RsTabs.vue +415 -83
- package/src/components/RsTag.vue +191 -0
- package/src/components/RsTerminal.vue +3 -3
- package/src/components/RsTimePicker.vue +44 -5
- package/src/components/RsTimePickerColumns.vue +136 -77
- package/src/components/RsToaster.vue +58 -18
- package/src/components/RsTree.vue +39 -21
- package/src/components/RsUpload.vue +145 -12
- package/src/components/RsVNodeHost.vue +20 -0
- package/src/components/button-utils.ts +39 -0
- package/src/components/context-menu-utils.ts +6 -0
- package/src/components/date-picker-utils.ts +84 -4
- package/src/components/descriptions-utils.ts +24 -0
- package/src/components/dialog-utils.ts +147 -0
- package/src/components/dialog-window.ts +74 -9
- package/src/components/drawer-utils.ts +73 -0
- package/src/components/form-rules.ts +257 -0
- package/src/components/form-utils.ts +52 -2
- package/src/components/menu-utils.ts +11 -0
- package/src/components/overlay-utils.ts +3 -0
- package/src/components/radio-utils.ts +12 -0
- package/src/components/reka.ts +5 -0
- package/src/components/select-utils.ts +21 -2
- package/src/components/split-pane-utils.ts +65 -12
- package/src/components/table/RsTableBody.vue +217 -0
- package/src/components/table/RsTableBodyRow.vue +90 -1
- package/src/components/table/RsTableColGroup.vue +41 -0
- package/src/components/table/RsTableHeader.vue +158 -0
- package/src/components/table/RsTableSummaryRow.vue +52 -0
- package/src/components/table/rs-table-api.ts +99 -0
- package/src/components/table/rs-table-compat-matrix.ts +71 -0
- package/src/components/table/rs-table-echarts-adapter.ts +81 -0
- package/src/components/table/rs-table-feature-host.ts +178 -0
- package/src/components/table/rs-table-grid-nav.ts +122 -0
- package/src/components/table/rs-table-header-types.ts +47 -0
- package/src/components/table/rs-table-module-registry.ts +72 -0
- package/src/components/table/rs-table-props.ts +274 -0
- package/src/components/table/rs-table-view-context.ts +258 -0
- package/src/components/table/rs-table.css +84 -6
- package/src/components/table/table-chart-utils.ts +165 -0
- package/src/components/table/table-features.ts +284 -0
- package/src/components/table/table-summary-utils.ts +194 -0
- package/src/components/table-utils.ts +452 -2
- package/src/components/tabs-utils.ts +142 -2
- package/src/components/terminal-utils.ts +1 -1
- package/src/components/terminal-wheel.ts +1 -1
- package/src/components/time-picker-utils.ts +76 -3
- package/src/components/upload-utils.ts +122 -0
- package/src/components/virtual-list-utils.ts +15 -2
- package/src/composables/assembleRsTableApi.ts +79 -0
- package/src/composables/bindRsTableViewContext.ts +269 -0
- package/src/composables/createRsDialog.ts +357 -0
- package/src/composables/createRsTableEditEmitBridge.ts +38 -0
- package/src/composables/useRsLoadingBar.ts +36 -0
- package/src/composables/useRsTable.ts +134 -0
- package/src/composables/useRsTableA11y.ts +52 -0
- package/src/composables/useRsTableChartBridge.ts +79 -0
- package/src/composables/useRsTableColumnLayout.ts +320 -0
- package/src/composables/useRsTableColumnResize.ts +244 -0
- package/src/composables/useRsTableColumnVirtual.ts +180 -0
- package/src/composables/useRsTableColumns.ts +200 -0
- package/src/composables/useRsTableContextMenu.ts +257 -0
- package/src/composables/useRsTableCore.ts +256 -0
- package/src/composables/useRsTableEditActions.ts +606 -0
- package/src/composables/useRsTableEditLayer.ts +251 -0
- package/src/composables/useRsTableEngine.ts +357 -0
- package/src/composables/useRsTableGridKeyboard.ts +123 -0
- package/src/composables/useRsTableHeadless.ts +137 -0
- package/src/composables/useRsTableInteraction.ts +433 -0
- package/src/composables/useRsTableScrollHost.ts +75 -0
- package/src/composables/useRsTableScrollLayout.ts +255 -0
- package/src/composables/useRsTableSelectionSource.ts +89 -0
- package/src/composables/useRsTableShell.ts +416 -0
- package/src/composables/useRsTableShellChrome.ts +170 -0
- package/src/composables/useRsTableSummary.ts +113 -0
- package/src/composables/useRsTableViewProvide.ts +263 -0
- package/src/composables/useRsTableVirtual.ts +154 -0
- package/src/composables/useRsTabsNav.ts +4 -0
- package/src/composables/useTableEdit.ts +1 -1
- package/src/icons/lucide.ts +2 -2
- package/src/index.ts +309 -9
- package/src/locale/messages.ts +30 -0
- package/src/styles.css +305 -52
- package/src/theme/brand.example.css +21 -2
- package/src/theme/presets.ts +4 -0
- package/src/theme/types.ts +67 -0
- package/src/components/RsTableColgroup.vue +0 -17
- package/src/components/RsTableHeaderRow.vue +0 -30
|
@@ -25,6 +25,80 @@ export interface RsDateRangeValue {
|
|
|
25
25
|
end?: string
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** v-model 取值格式:string 为默认字符串;timestamp 为毫秒时间戳 */
|
|
29
|
+
export type RsDatePickerValueFormat = 'string' | 'timestamp'
|
|
30
|
+
|
|
31
|
+
/** 时间戳范围(毫秒),用于 valueFormat=timestamp 的 range 模式 */
|
|
32
|
+
export type RsDatePickerTimestampRange = [number, number]
|
|
33
|
+
|
|
34
|
+
/** 组件对外 v-model 联合类型 */
|
|
35
|
+
export type RsDatePickerModelValue =
|
|
36
|
+
| string
|
|
37
|
+
| number
|
|
38
|
+
| null
|
|
39
|
+
| RsDateRangeValue
|
|
40
|
+
| RsDatePickerTimestampRange
|
|
41
|
+
|
|
42
|
+
/** 快捷项可返回字符串、时间戳或范围 */
|
|
43
|
+
export type RsDatePickerShortcutValue =
|
|
44
|
+
| string
|
|
45
|
+
| number
|
|
46
|
+
| RsDateRangeValue
|
|
47
|
+
| RsDatePickerTimestampRange
|
|
48
|
+
|
|
49
|
+
/** 日期/时间范围面板快捷项 */
|
|
50
|
+
export interface RsDatePickerShortcut {
|
|
51
|
+
label: string
|
|
52
|
+
value: () => RsDatePickerShortcutValue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 将毫秒时间戳格式化为组件内部日期时间字符串 */
|
|
56
|
+
export function formatTimestampValue(ms: number, withTime = true): string {
|
|
57
|
+
const parsed = dayjs(ms)
|
|
58
|
+
return withTime ? parsed.format(RS_DATETIME_FORMAT) : parsed.format(RS_DATE_FORMAT)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 将组件内部日期/日期时间字符串解析为毫秒时间戳 */
|
|
62
|
+
export function parseTimestampValue(value?: string): number | null {
|
|
63
|
+
if (!value) return null
|
|
64
|
+
const parsed = parseRsDateTimeDayjs(value) ?? parseRsDayjs(value)
|
|
65
|
+
return parsed ? parsed.valueOf() : null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 判断是否为时间戳范围元组 */
|
|
69
|
+
export function isTimestampRange(value: unknown): value is RsDatePickerTimestampRange {
|
|
70
|
+
return (
|
|
71
|
+
Array.isArray(value)
|
|
72
|
+
&& value.length === 2
|
|
73
|
+
&& typeof value[0] === 'number'
|
|
74
|
+
&& typeof value[1] === 'number'
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 将快捷项返回值规范为内部 string / RsDateRangeValue */
|
|
79
|
+
export function normalizeShortcutValue(
|
|
80
|
+
next: RsDatePickerShortcutValue,
|
|
81
|
+
options?: { withTime?: boolean },
|
|
82
|
+
): string | RsDateRangeValue | null {
|
|
83
|
+
const withTime = options?.withTime ?? true
|
|
84
|
+
if (typeof next === 'number') {
|
|
85
|
+
return formatTimestampValue(next, withTime)
|
|
86
|
+
}
|
|
87
|
+
if (isTimestampRange(next)) {
|
|
88
|
+
return {
|
|
89
|
+
start: formatTimestampValue(next[0], withTime),
|
|
90
|
+
end: formatTimestampValue(next[1], withTime),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (typeof next === 'string') {
|
|
94
|
+
return next || null
|
|
95
|
+
}
|
|
96
|
+
if (next && typeof next === 'object') {
|
|
97
|
+
return { ...next }
|
|
98
|
+
}
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
28
102
|
export interface RsCalendarCell extends RsParsedDate {
|
|
29
103
|
inCurrentMonth: boolean
|
|
30
104
|
}
|
|
@@ -78,10 +152,16 @@ export function formatDateTimeParts(
|
|
|
78
152
|
withSeconds = true,
|
|
79
153
|
): string | null {
|
|
80
154
|
const dateText = formatDateParts(date)
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
155
|
+
const formats = withSeconds
|
|
156
|
+
? [`${RS_DATE_FORMAT} ${RS_TIME_SECONDS_FORMAT}`, `${RS_DATE_FORMAT} ${RS_TIME_MINUTE_FORMAT}`]
|
|
157
|
+
: [`${RS_DATE_FORMAT} ${RS_TIME_MINUTE_FORMAT}`]
|
|
158
|
+
for (const format of formats) {
|
|
159
|
+
const parsed = dayjs(`${dateText} ${time}`, format, true)
|
|
160
|
+
if (parsed.isValid()) {
|
|
161
|
+
return parsed.format(withSeconds ? RS_DATETIME_FORMAT : `${RS_DATE_FORMAT} ${RS_TIME_MINUTE_FORMAT}`)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null
|
|
85
165
|
}
|
|
86
166
|
|
|
87
167
|
export function extractTimeFromDateTime(value?: string, withSeconds = true): string {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { InjectionKey } from 'vue'
|
|
2
|
+
import type { RsComponentSize } from '../theme/types'
|
|
3
|
+
|
|
4
|
+
/** Descriptions 尺寸与全局控件尺寸对齐:ssm / sm / md / lg */
|
|
5
|
+
export type RsDescriptionsSize = RsComponentSize
|
|
6
|
+
|
|
7
|
+
export type RsDescriptionsLabelPlacement = 'left' | 'top'
|
|
8
|
+
|
|
9
|
+
export type RsDescriptionsItem = {
|
|
10
|
+
label: string
|
|
11
|
+
value?: string | number | null
|
|
12
|
+
span?: number
|
|
13
|
+
key?: string | number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type RsDescriptionsContext = {
|
|
17
|
+
labelPlacement: RsDescriptionsLabelPlacement
|
|
18
|
+
bordered: boolean
|
|
19
|
+
size: RsDescriptionsSize
|
|
20
|
+
columns: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const RS_DESCRIPTIONS_KEY: InjectionKey<RsDescriptionsContext> =
|
|
24
|
+
Symbol('rs-descriptions')
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { Component } from 'vue'
|
|
2
|
+
import type { RsLocale } from '../locale/types'
|
|
3
|
+
import type { RsThemeMode } from '../theme/types'
|
|
4
|
+
import type { RsFeedbackTone } from './overlay-utils'
|
|
5
|
+
|
|
6
|
+
/** 宽度预设(与历史 sm/md/lg 一致) */
|
|
7
|
+
export type RsDialogWidthPreset = 'sm' | 'md' | 'lg'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 对话框宽度:预设或自定义(number 按 px;string 原样写入 CSS,如 `40rem` / `60%`)。
|
|
11
|
+
* 仅传入预设时行为与旧版完全一致。
|
|
12
|
+
*/
|
|
13
|
+
export type RsDialogWidth = RsDialogWidthPreset | number | string
|
|
14
|
+
|
|
15
|
+
export type RsDialogLayout = 'window' | 'confirm'
|
|
16
|
+
|
|
17
|
+
/** 关闭来源,供 beforeClose / afterClose 区分 */
|
|
18
|
+
export type RsDialogCloseReason =
|
|
19
|
+
| 'close'
|
|
20
|
+
| 'overlay'
|
|
21
|
+
| 'escape'
|
|
22
|
+
| 'cancel'
|
|
23
|
+
| 'confirm'
|
|
24
|
+
| 'programmatic'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 关闭前钩子:返回 false 阻止关闭;支持异步。
|
|
28
|
+
* 未提供或返回 void/true 时允许关闭。
|
|
29
|
+
*/
|
|
30
|
+
export type RsDialogBeforeClose = (
|
|
31
|
+
reason: RsDialogCloseReason,
|
|
32
|
+
) => boolean | void | Promise<boolean | void>
|
|
33
|
+
|
|
34
|
+
export function isRsDialogWidthPreset(width: RsDialogWidth): width is RsDialogWidthPreset {
|
|
35
|
+
return width === 'sm' || width === 'md' || width === 'lg'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 将自定义宽度规范为 CSS 长度 */
|
|
39
|
+
export function resolveRsDialogCssWidth(width: RsDialogWidth): string | undefined {
|
|
40
|
+
if (isRsDialogWidthPreset(width)) return undefined
|
|
41
|
+
if (typeof width === 'number') {
|
|
42
|
+
if (!Number.isFinite(width) || width <= 0) return undefined
|
|
43
|
+
return `${width}px`
|
|
44
|
+
}
|
|
45
|
+
const trimmed = String(width).trim()
|
|
46
|
+
return trimmed || undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function runRsDialogBeforeClose(
|
|
50
|
+
beforeClose: RsDialogBeforeClose | undefined,
|
|
51
|
+
reason: RsDialogCloseReason,
|
|
52
|
+
): Promise<boolean> {
|
|
53
|
+
if (!beforeClose) return true
|
|
54
|
+
const result = await beforeClose(reason)
|
|
55
|
+
return result !== false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 确认框关闭来源,供 beforeClose 区分 */
|
|
59
|
+
export type RsConfirmCloseReason = 'confirm' | 'cancel' | 'escape' | 'programmatic'
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 确认框关闭前钩子:返回 false 阻止关闭;支持异步。
|
|
63
|
+
* 未提供或返回 void/true 时允许关闭。
|
|
64
|
+
*/
|
|
65
|
+
export type RsConfirmBeforeClose = (
|
|
66
|
+
reason: RsConfirmCloseReason,
|
|
67
|
+
) => boolean | void | Promise<boolean | void>
|
|
68
|
+
|
|
69
|
+
export interface RsConfirmOptions {
|
|
70
|
+
title?: string
|
|
71
|
+
/** 次要说明(标题与正文之间,对应业务侧常见 subtitle) */
|
|
72
|
+
subtitle?: string
|
|
73
|
+
description?: string
|
|
74
|
+
tone?: RsFeedbackTone
|
|
75
|
+
/** 自定义图标组件;不传则使用默认 "!" */
|
|
76
|
+
icon?: Component
|
|
77
|
+
/** 宽度:预设 sm/md/lg,或 number(px) / CSS 长度;默认保持 28rem */
|
|
78
|
+
width?: RsDialogWidth
|
|
79
|
+
confirmText?: string
|
|
80
|
+
cancelText?: string
|
|
81
|
+
confirmVariant?: 'primary' | 'danger'
|
|
82
|
+
/** 是否显示取消按钮;false 时为单按钮提示(info/success/error) */
|
|
83
|
+
showCancel?: boolean
|
|
84
|
+
/** 受控加载态;若同时提供 onConfirm,命令式 API 会在执行期间自动置 true */
|
|
85
|
+
confirmLoading?: boolean
|
|
86
|
+
/**
|
|
87
|
+
* 确认回调。返回 Promise 时对话框保持打开并进入 loading,
|
|
88
|
+
* resolve 后关闭并 resolve(true);reject 则取消 loading 并保持打开。
|
|
89
|
+
*/
|
|
90
|
+
onConfirm?: () => void | Promise<void>
|
|
91
|
+
/** 取消/关闭时回调(不区分来源) */
|
|
92
|
+
onCancel?: () => void
|
|
93
|
+
/** 关闭前钩子;返回 false 可阻止关闭 */
|
|
94
|
+
beforeClose?: RsConfirmBeforeClose
|
|
95
|
+
/**
|
|
96
|
+
* 正文与按钮之间的额外内容(渲染函数)。
|
|
97
|
+
* 声明式请用 RsConfirmDialog 的 #extra 插槽。
|
|
98
|
+
*/
|
|
99
|
+
extra?: () => unknown
|
|
100
|
+
showOverlay?: boolean
|
|
101
|
+
/**
|
|
102
|
+
* 遮罩不透明度 0–1;覆盖 CSS 变量 --rs-dialog-overlay-bg。
|
|
103
|
+
* 未传时使用主题默认(最浅)。
|
|
104
|
+
*/
|
|
105
|
+
overlayOpacity?: number
|
|
106
|
+
/**
|
|
107
|
+
* 遮罩模糊半径;number 按 px。覆盖 --rs-dialog-overlay-blur。
|
|
108
|
+
* 未传时使用主题默认(0)。
|
|
109
|
+
*/
|
|
110
|
+
overlayBlur?: number | string
|
|
111
|
+
/** false 表示禁用 Teleport,就地渲染 */
|
|
112
|
+
teleportTo?: string | HTMLElement | false
|
|
113
|
+
/** 命令式挂载时覆盖主题;默认读 document data-rs-theme */
|
|
114
|
+
theme?: RsThemeMode
|
|
115
|
+
/** 命令式挂载时覆盖语言;默认读 document data-rs-locale */
|
|
116
|
+
locale?: RsLocale
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 运行确认框 beforeClose;未提供或非 false 视为允许关闭 */
|
|
120
|
+
export async function runRsConfirmBeforeClose(
|
|
121
|
+
beforeClose: RsConfirmBeforeClose | undefined,
|
|
122
|
+
reason: RsConfirmCloseReason,
|
|
123
|
+
): Promise<boolean> {
|
|
124
|
+
if (!beforeClose) return true
|
|
125
|
+
const result = await beforeClose(reason)
|
|
126
|
+
return result !== false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 将 overlayOpacity / overlayBlur 转为遮罩 CSS 变量(可挂到 overlay 节点 style)。
|
|
131
|
+
*/
|
|
132
|
+
export function resolveDialogOverlayStyle(options?: {
|
|
133
|
+
overlayOpacity?: number
|
|
134
|
+
overlayBlur?: number | string
|
|
135
|
+
}): Record<string, string> | undefined {
|
|
136
|
+
if (!options) return undefined
|
|
137
|
+
const style: Record<string, string> = {}
|
|
138
|
+
if (options.overlayOpacity != null && Number.isFinite(options.overlayOpacity)) {
|
|
139
|
+
const opacity = Math.min(1, Math.max(0, options.overlayOpacity))
|
|
140
|
+
style['--rs-dialog-overlay-bg'] = `rgb(0 0 0 / ${opacity})`
|
|
141
|
+
}
|
|
142
|
+
if (options.overlayBlur != null && options.overlayBlur !== '') {
|
|
143
|
+
style['--rs-dialog-overlay-blur'] =
|
|
144
|
+
typeof options.overlayBlur === 'number' ? `${options.overlayBlur}px` : String(options.overlayBlur)
|
|
145
|
+
}
|
|
146
|
+
return Object.keys(style).length ? style : undefined
|
|
147
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { computed, onBeforeUnmount, ref, watch, type Ref } from 'vue'
|
|
2
|
+
import type { RsDialogWidthPreset } from './dialog-utils'
|
|
2
3
|
import { dialogViewportSize, readDialogViewportInsets } from './dialog-viewport'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
/** @deprecated 使用 RsDialogWidthPreset;保留别名避免外部类型断裂 */
|
|
6
|
+
export type RsDialogWidth = RsDialogWidthPreset
|
|
5
7
|
export type RsDialogResizeHandle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'
|
|
6
8
|
|
|
7
9
|
export interface RsDialogBounds {
|
|
@@ -11,7 +13,7 @@ export interface RsDialogBounds {
|
|
|
11
13
|
height: number
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
const widthMap: Record<
|
|
16
|
+
const widthMap: Record<RsDialogWidthPreset, number> = {
|
|
15
17
|
sm: 384,
|
|
16
18
|
md: 448,
|
|
17
19
|
lg: 512,
|
|
@@ -24,7 +26,9 @@ const minHeight = 240
|
|
|
24
26
|
|
|
25
27
|
export function useRsDialogWindow(options: {
|
|
26
28
|
open: Ref<boolean>
|
|
27
|
-
widthPreset: Ref<
|
|
29
|
+
widthPreset: Ref<RsDialogWidthPreset>
|
|
30
|
+
/** 自定义初始宽度(px);有值时优先于预设/比例 */
|
|
31
|
+
initialWidth?: Ref<number | undefined>
|
|
28
32
|
draggable: Ref<boolean>
|
|
29
33
|
resizable: Ref<boolean>
|
|
30
34
|
compact?: Ref<boolean>
|
|
@@ -39,6 +43,9 @@ export function useRsDialogWindow(options: {
|
|
|
39
43
|
|
|
40
44
|
let boundsTransitionTimer: ReturnType<typeof setTimeout> | null = null
|
|
41
45
|
let cachedInsets = readDialogViewportInsets()
|
|
46
|
+
/** 交互中的目标面板;有值时 pointermove 只改 DOM,pointerup 再写回 bounds */
|
|
47
|
+
let panelEl: HTMLElement | null = null
|
|
48
|
+
let liveBounds: RsDialogBounds | null = null
|
|
42
49
|
|
|
43
50
|
let dragState: { startX: number; startY: number; origin: RsDialogBounds } | null = null
|
|
44
51
|
let resizeState: {
|
|
@@ -48,10 +55,41 @@ export function useRsDialogWindow(options: {
|
|
|
48
55
|
origin: RsDialogBounds
|
|
49
56
|
} | null = null
|
|
50
57
|
|
|
58
|
+
function writePanelBox(next: RsDialogBounds): void {
|
|
59
|
+
if (!panelEl) return
|
|
60
|
+
panelEl.style.left = `${next.x}px`
|
|
61
|
+
panelEl.style.top = `${next.y}px`
|
|
62
|
+
panelEl.style.width = `${next.width}px`
|
|
63
|
+
panelEl.style.height = `${next.height}px`
|
|
64
|
+
panelEl.style.transform = 'none'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 绑定可定位的对话框根节点(DialogContent 的 DOM) */
|
|
68
|
+
function setPanelEl(el: HTMLElement | null): void {
|
|
69
|
+
panelEl = el
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 优先用已绑定节点;否则从事件目标向上找面板(Reka DialogContent ref 不一定有 $el) */
|
|
73
|
+
function ensurePanelEl(from?: EventTarget | null): HTMLElement | null {
|
|
74
|
+
if (panelEl?.isConnected) return panelEl
|
|
75
|
+
const node = from instanceof Element ? from : null
|
|
76
|
+
const found = node?.closest('.rs-dialog__content')
|
|
77
|
+
if (found instanceof HTMLElement) {
|
|
78
|
+
panelEl = found
|
|
79
|
+
return found
|
|
80
|
+
}
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
51
84
|
function defaultWindowSize(): Pick<RsDialogBounds, 'width' | 'height'> {
|
|
52
85
|
const { width: maxW, height: maxH } = dialogViewportSize()
|
|
86
|
+
const customW = options.initialWidth?.value
|
|
87
|
+
const width =
|
|
88
|
+
customW != null && Number.isFinite(customW) && customW > 0
|
|
89
|
+
? Math.min(maxW, Math.max(minWidth, Math.round(customW)))
|
|
90
|
+
: Math.min(maxW, Math.max(minWidth, Math.round(maxW * defaultWidthRatio)))
|
|
53
91
|
return {
|
|
54
|
-
width
|
|
92
|
+
width,
|
|
55
93
|
height: Math.min(maxH, Math.max(minHeight, Math.round(maxH * defaultHeightRatio))),
|
|
56
94
|
}
|
|
57
95
|
}
|
|
@@ -97,7 +135,12 @@ export function useRsDialogWindow(options: {
|
|
|
97
135
|
options.open,
|
|
98
136
|
(value) => {
|
|
99
137
|
if (value) resetOnOpen()
|
|
100
|
-
else
|
|
138
|
+
else {
|
|
139
|
+
stopInteractions()
|
|
140
|
+
// 关闭时清全屏态,避免 KeepAlive 缓存残留 --fullscreen 类导致 Presence 补丁异常
|
|
141
|
+
isFullscreen.value = false
|
|
142
|
+
restoreBounds.value = null
|
|
143
|
+
}
|
|
101
144
|
},
|
|
102
145
|
// 弹出等场景:组件挂载时 open 已为 true,需立即居中,不能只等 false→true
|
|
103
146
|
{ immediate: true },
|
|
@@ -115,7 +158,11 @@ export function useRsDialogWindow(options: {
|
|
|
115
158
|
}
|
|
116
159
|
}
|
|
117
160
|
if (options.compact?.value) {
|
|
118
|
-
const
|
|
161
|
+
const customW = options.initialWidth?.value
|
|
162
|
+
const width =
|
|
163
|
+
customW != null && Number.isFinite(customW) && customW > 0
|
|
164
|
+
? customW
|
|
165
|
+
: widthMap[options.widthPreset.value]
|
|
119
166
|
return {
|
|
120
167
|
left: '50%',
|
|
121
168
|
top: `calc(${insets.top}px + (100vh - ${insets.top + insets.bottom}px) / 2)`,
|
|
@@ -169,7 +216,14 @@ export function useRsDialogWindow(options: {
|
|
|
169
216
|
if (dragState) {
|
|
170
217
|
const dx = event.clientX - dragState.startX
|
|
171
218
|
const dy = event.clientY - dragState.startY
|
|
172
|
-
|
|
219
|
+
const next = clampBounds({
|
|
220
|
+
...dragState.origin,
|
|
221
|
+
x: dragState.origin.x + dx,
|
|
222
|
+
y: dragState.origin.y + dy,
|
|
223
|
+
})
|
|
224
|
+
// 拖拽中只改 DOM,避免触发 Vue 对对话框子树(如虚拟滚动树)的高频 patch
|
|
225
|
+
liveBounds = next
|
|
226
|
+
writePanelBox(next)
|
|
173
227
|
return
|
|
174
228
|
}
|
|
175
229
|
|
|
@@ -187,10 +241,16 @@ export function useRsDialogWindow(options: {
|
|
|
187
241
|
next.height = resizeState.origin.height - dy
|
|
188
242
|
next.y = resizeState.origin.y + dy
|
|
189
243
|
}
|
|
190
|
-
|
|
244
|
+
const clamped = clampBounds(next)
|
|
245
|
+
liveBounds = clamped
|
|
246
|
+
writePanelBox(clamped)
|
|
191
247
|
}
|
|
192
248
|
|
|
193
249
|
function stopInteractions(): void {
|
|
250
|
+
if (liveBounds) {
|
|
251
|
+
bounds.value = liveBounds
|
|
252
|
+
liveBounds = null
|
|
253
|
+
}
|
|
194
254
|
dragState = null
|
|
195
255
|
resizeState = null
|
|
196
256
|
window.removeEventListener('pointermove', onPointerMove)
|
|
@@ -203,6 +263,7 @@ export function useRsDialogWindow(options: {
|
|
|
203
263
|
boundsTransitionEnabled.value = false
|
|
204
264
|
clearBoundsTransitionTimer()
|
|
205
265
|
refreshCachedInsets()
|
|
266
|
+
liveBounds = { ...bounds.value }
|
|
206
267
|
window.addEventListener('pointermove', onPointerMove)
|
|
207
268
|
window.addEventListener('pointerup', stopInteractions)
|
|
208
269
|
document.body.style.userSelect = 'none'
|
|
@@ -214,8 +275,10 @@ export function useRsDialogWindow(options: {
|
|
|
214
275
|
const target = event.target as HTMLElement
|
|
215
276
|
if (target.closest('button,a,input,textarea,select')) return
|
|
216
277
|
event.preventDefault()
|
|
278
|
+
ensurePanelEl(event.target)
|
|
217
279
|
dragState = { startX: event.clientX, startY: event.clientY, origin: { ...bounds.value } }
|
|
218
|
-
|
|
280
|
+
// move:四向十字箭头,表示可自由拖移窗口
|
|
281
|
+
startInteraction('move')
|
|
219
282
|
}
|
|
220
283
|
|
|
221
284
|
const resizeCursor: Record<RsDialogResizeHandle, string> = {
|
|
@@ -232,6 +295,7 @@ export function useRsDialogWindow(options: {
|
|
|
232
295
|
function onResizePointerDown(handle: RsDialogResizeHandle, event: PointerEvent): void {
|
|
233
296
|
if (!options.resizable.value || isFullscreen.value) return
|
|
234
297
|
event.preventDefault()
|
|
298
|
+
ensurePanelEl(event.target)
|
|
235
299
|
resizeState = { handle, startX: event.clientX, startY: event.clientY, origin: { ...bounds.value } }
|
|
236
300
|
startInteraction(resizeCursor[handle])
|
|
237
301
|
}
|
|
@@ -246,6 +310,7 @@ export function useRsDialogWindow(options: {
|
|
|
246
310
|
boundsTransitionEnabled,
|
|
247
311
|
dialogStyle,
|
|
248
312
|
resizeHandles,
|
|
313
|
+
setPanelEl,
|
|
249
314
|
toggleFullscreen,
|
|
250
315
|
onHeaderPointerDown,
|
|
251
316
|
onResizePointerDown,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveDialogOverlayStyle,
|
|
3
|
+
runRsDialogBeforeClose,
|
|
4
|
+
type RsDialogBeforeClose,
|
|
5
|
+
type RsDialogCloseReason,
|
|
6
|
+
} from './dialog-utils'
|
|
7
|
+
|
|
8
|
+
/** 抽屉滑出方向 */
|
|
9
|
+
export type RsDrawerSide = 'left' | 'right' | 'top' | 'bottom'
|
|
10
|
+
|
|
11
|
+
/** 抽屉尺寸预设(左右为宽度,上下为高度) */
|
|
12
|
+
export type RsDrawerSize = 'sm' | 'md' | 'lg' | 'full'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 自定义面板尺寸:number 按 px;string 原样写入 CSS(如 `24rem` / `40%`)。
|
|
16
|
+
* 与 size 同时传入时,自定义尺寸优先。
|
|
17
|
+
*/
|
|
18
|
+
export type RsDrawerDimension = number | string
|
|
19
|
+
|
|
20
|
+
/** 关闭来源,与 Dialog 共用语义,便于业务统一 beforeClose */
|
|
21
|
+
export type RsDrawerCloseReason = RsDialogCloseReason
|
|
22
|
+
|
|
23
|
+
/** 关闭前钩子:返回 false 阻止关闭;支持异步 */
|
|
24
|
+
export type RsDrawerBeforeClose = RsDialogBeforeClose
|
|
25
|
+
|
|
26
|
+
const SIZE_INLINE: Record<Exclude<RsDrawerSize, 'full'>, string> = {
|
|
27
|
+
sm: '20rem',
|
|
28
|
+
md: '28rem',
|
|
29
|
+
lg: '36rem',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 将 size 预设解析为 CSS 长度(full 由样式类处理) */
|
|
33
|
+
export function resolveRsDrawerSizeCss(size: RsDrawerSize): string | undefined {
|
|
34
|
+
if (size === 'full') return undefined
|
|
35
|
+
return SIZE_INLINE[size]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 将自定义 width/height 规范为 CSS 长度 */
|
|
39
|
+
export function resolveRsDrawerDimensionCss(value: RsDrawerDimension | undefined): string | undefined {
|
|
40
|
+
if (value == null) return undefined
|
|
41
|
+
if (typeof value === 'number') {
|
|
42
|
+
if (!Number.isFinite(value) || value <= 0) return undefined
|
|
43
|
+
return `${value}px`
|
|
44
|
+
}
|
|
45
|
+
const trimmed = String(value).trim()
|
|
46
|
+
return trimmed || undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function runRsDrawerBeforeClose(
|
|
50
|
+
beforeClose: RsDrawerBeforeClose | undefined,
|
|
51
|
+
reason: RsDrawerCloseReason,
|
|
52
|
+
): Promise<boolean> {
|
|
53
|
+
return runRsDialogBeforeClose(beforeClose, reason)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 遮罩样式:复用 Dialog 的 opacity/blur 解析,写入抽屉专用 CSS 变量。
|
|
58
|
+
*/
|
|
59
|
+
export function resolveDrawerOverlayStyle(options?: {
|
|
60
|
+
overlayOpacity?: number
|
|
61
|
+
overlayBlur?: number | string
|
|
62
|
+
}): Record<string, string> | undefined {
|
|
63
|
+
const dialogStyle = resolveDialogOverlayStyle(options)
|
|
64
|
+
if (!dialogStyle) return undefined
|
|
65
|
+
const style: Record<string, string> = {}
|
|
66
|
+
if (dialogStyle['--rs-dialog-overlay-bg']) {
|
|
67
|
+
style['--rs-drawer-overlay-bg'] = dialogStyle['--rs-dialog-overlay-bg']
|
|
68
|
+
}
|
|
69
|
+
if (dialogStyle['--rs-dialog-overlay-blur']) {
|
|
70
|
+
style['--rs-drawer-overlay-blur'] = dialogStyle['--rs-dialog-overlay-blur']
|
|
71
|
+
}
|
|
72
|
+
return Object.keys(style).length ? style : undefined
|
|
73
|
+
}
|