niuma-ui 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/NOTICE +2 -2
  3. package/package.json +13 -5
  4. package/src/__tests__/RsButton.spec.ts +47 -0
  5. package/src/__tests__/RsCard.spec.ts +160 -1
  6. package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
  7. package/src/__tests__/RsContextMenu.spec.ts +12 -0
  8. package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
  9. package/src/__tests__/RsDescriptions.spec.ts +50 -0
  10. package/src/__tests__/RsDialog.spec.ts +117 -0
  11. package/src/__tests__/RsDrawer.spec.ts +96 -3
  12. package/src/__tests__/RsDynamicTags.spec.ts +52 -0
  13. package/src/__tests__/RsForm.spec.ts +245 -1
  14. package/src/__tests__/RsInput.spec.ts +15 -1
  15. package/src/__tests__/RsMenu.spec.ts +406 -14
  16. package/src/__tests__/RsPagination.spec.ts +67 -2
  17. package/src/__tests__/RsSelect.spec.ts +42 -1
  18. package/src/__tests__/RsSwitch.spec.ts +44 -0
  19. package/src/__tests__/RsTable.a11y.spec.ts +83 -0
  20. package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
  21. package/src/__tests__/RsTable.perf.spec.ts +23 -4
  22. package/src/__tests__/RsTable.spec.ts +63 -8
  23. package/src/__tests__/RsTable.ssr.spec.ts +71 -0
  24. package/src/__tests__/RsTable.summary.spec.ts +45 -0
  25. package/src/__tests__/RsTabs.spec.ts +102 -4
  26. package/src/__tests__/RsTerminal.spec.ts +1 -1
  27. package/src/__tests__/RsTimePicker.spec.ts +11 -0
  28. package/src/__tests__/RsUpload.spec.ts +62 -6
  29. package/src/__tests__/createRsDialog.spec.ts +145 -0
  30. package/src/__tests__/form-rules.spec.ts +76 -0
  31. package/src/__tests__/perf-budgets.ts +32 -0
  32. package/src/__tests__/rs-table-api.spec.ts +206 -0
  33. package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
  34. package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
  35. package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
  36. package/src/__tests__/rs-table-isolation.spec.ts +158 -0
  37. package/src/__tests__/split-pane-utils.spec.ts +21 -0
  38. package/src/__tests__/table-chart-utils.spec.ts +88 -0
  39. package/src/__tests__/table-summary-utils.spec.ts +62 -0
  40. package/src/__tests__/table-utils.spec.ts +124 -0
  41. package/src/__tests__/theme.spec.ts +25 -0
  42. package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
  43. package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
  44. package/src/components/RsAlert.vue +212 -0
  45. package/src/components/RsButton.vue +145 -21
  46. package/src/components/RsCard.vue +200 -16
  47. package/src/components/RsConfirmDialog.vue +215 -28
  48. package/src/components/RsContextMenu.vue +51 -13
  49. package/src/components/RsDatePicker.vue +267 -34
  50. package/src/components/RsDateTimePicker.vue +19 -2
  51. package/src/components/RsDescriptions.vue +186 -0
  52. package/src/components/RsDescriptionsItem.vue +32 -0
  53. package/src/components/RsDialog.vue +363 -51
  54. package/src/components/RsDivider.vue +89 -0
  55. package/src/components/RsDrawer.vue +558 -33
  56. package/src/components/RsDropdown.vue +33 -2
  57. package/src/components/RsDynamicTags.vue +517 -0
  58. package/src/components/RsForm.vue +97 -8
  59. package/src/components/RsInput.vue +80 -82
  60. package/src/components/RsInputNumber.vue +64 -23
  61. package/src/components/RsLabel.vue +20 -5
  62. package/src/components/RsLoadingBar.vue +151 -0
  63. package/src/components/RsMenu.vue +73 -15
  64. package/src/components/RsMenuItems.vue +147 -9
  65. package/src/components/RsPagination.vue +93 -14
  66. package/src/components/RsRadio.vue +81 -0
  67. package/src/components/RsRadioItem.vue +156 -0
  68. package/src/components/RsSelect.vue +67 -58
  69. package/src/components/RsSplitPane.vue +69 -1
  70. package/src/components/RsSwitch.vue +179 -0
  71. package/src/components/RsTable.vue +653 -2555
  72. package/src/components/RsTabs.vue +415 -83
  73. package/src/components/RsTag.vue +191 -0
  74. package/src/components/RsTerminal.vue +3 -3
  75. package/src/components/RsTimePicker.vue +44 -5
  76. package/src/components/RsTimePickerColumns.vue +136 -77
  77. package/src/components/RsToaster.vue +58 -18
  78. package/src/components/RsTree.vue +39 -21
  79. package/src/components/RsUpload.vue +145 -12
  80. package/src/components/RsVNodeHost.vue +20 -0
  81. package/src/components/button-utils.ts +39 -0
  82. package/src/components/context-menu-utils.ts +6 -0
  83. package/src/components/date-picker-utils.ts +84 -4
  84. package/src/components/descriptions-utils.ts +24 -0
  85. package/src/components/dialog-utils.ts +147 -0
  86. package/src/components/dialog-window.ts +74 -9
  87. package/src/components/drawer-utils.ts +73 -0
  88. package/src/components/form-rules.ts +257 -0
  89. package/src/components/form-utils.ts +52 -2
  90. package/src/components/menu-utils.ts +11 -0
  91. package/src/components/overlay-utils.ts +3 -0
  92. package/src/components/radio-utils.ts +12 -0
  93. package/src/components/reka.ts +5 -0
  94. package/src/components/select-utils.ts +21 -2
  95. package/src/components/split-pane-utils.ts +65 -12
  96. package/src/components/table/RsTableBody.vue +217 -0
  97. package/src/components/table/RsTableBodyRow.vue +90 -1
  98. package/src/components/table/RsTableColGroup.vue +41 -0
  99. package/src/components/table/RsTableHeader.vue +158 -0
  100. package/src/components/table/RsTableSummaryRow.vue +52 -0
  101. package/src/components/table/rs-table-api.ts +99 -0
  102. package/src/components/table/rs-table-compat-matrix.ts +71 -0
  103. package/src/components/table/rs-table-echarts-adapter.ts +81 -0
  104. package/src/components/table/rs-table-feature-host.ts +178 -0
  105. package/src/components/table/rs-table-grid-nav.ts +122 -0
  106. package/src/components/table/rs-table-header-types.ts +47 -0
  107. package/src/components/table/rs-table-module-registry.ts +72 -0
  108. package/src/components/table/rs-table-props.ts +274 -0
  109. package/src/components/table/rs-table-view-context.ts +258 -0
  110. package/src/components/table/rs-table.css +84 -6
  111. package/src/components/table/table-chart-utils.ts +165 -0
  112. package/src/components/table/table-features.ts +284 -0
  113. package/src/components/table/table-summary-utils.ts +194 -0
  114. package/src/components/table-utils.ts +452 -2
  115. package/src/components/tabs-utils.ts +142 -2
  116. package/src/components/terminal-utils.ts +1 -1
  117. package/src/components/terminal-wheel.ts +1 -1
  118. package/src/components/time-picker-utils.ts +76 -3
  119. package/src/components/upload-utils.ts +122 -0
  120. package/src/components/virtual-list-utils.ts +15 -2
  121. package/src/composables/assembleRsTableApi.ts +79 -0
  122. package/src/composables/bindRsTableViewContext.ts +269 -0
  123. package/src/composables/createRsDialog.ts +357 -0
  124. package/src/composables/createRsTableEditEmitBridge.ts +38 -0
  125. package/src/composables/useRsLoadingBar.ts +36 -0
  126. package/src/composables/useRsTable.ts +134 -0
  127. package/src/composables/useRsTableA11y.ts +52 -0
  128. package/src/composables/useRsTableChartBridge.ts +79 -0
  129. package/src/composables/useRsTableColumnLayout.ts +320 -0
  130. package/src/composables/useRsTableColumnResize.ts +244 -0
  131. package/src/composables/useRsTableColumnVirtual.ts +180 -0
  132. package/src/composables/useRsTableColumns.ts +200 -0
  133. package/src/composables/useRsTableContextMenu.ts +257 -0
  134. package/src/composables/useRsTableCore.ts +256 -0
  135. package/src/composables/useRsTableEditActions.ts +606 -0
  136. package/src/composables/useRsTableEditLayer.ts +251 -0
  137. package/src/composables/useRsTableEngine.ts +357 -0
  138. package/src/composables/useRsTableGridKeyboard.ts +123 -0
  139. package/src/composables/useRsTableHeadless.ts +137 -0
  140. package/src/composables/useRsTableInteraction.ts +433 -0
  141. package/src/composables/useRsTableScrollHost.ts +75 -0
  142. package/src/composables/useRsTableScrollLayout.ts +255 -0
  143. package/src/composables/useRsTableSelectionSource.ts +89 -0
  144. package/src/composables/useRsTableShell.ts +416 -0
  145. package/src/composables/useRsTableShellChrome.ts +170 -0
  146. package/src/composables/useRsTableSummary.ts +113 -0
  147. package/src/composables/useRsTableViewProvide.ts +263 -0
  148. package/src/composables/useRsTableVirtual.ts +154 -0
  149. package/src/composables/useRsTabsNav.ts +4 -0
  150. package/src/composables/useTableEdit.ts +1 -1
  151. package/src/icons/lucide.ts +2 -2
  152. package/src/index.ts +309 -9
  153. package/src/locale/messages.ts +30 -0
  154. package/src/styles.css +305 -52
  155. package/src/theme/brand.example.css +21 -2
  156. package/src/theme/presets.ts +4 -0
  157. package/src/theme/types.ts +67 -0
  158. package/src/components/RsTableColgroup.vue +0 -17
  159. package/src/components/RsTableHeaderRow.vue +0 -30
@@ -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 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)
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
- export type RsDialogWidth = 'sm' | 'md' | 'lg'
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<RsDialogWidth, number> = {
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<RsDialogWidth>
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: Math.min(maxW, Math.max(minWidth, Math.round(maxW * defaultWidthRatio))),
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 stopInteractions()
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 width = widthMap[options.widthPreset.value]
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
- bounds.value = clampBounds({ ...dragState.origin, x: dragState.origin.x + dx, y: dragState.origin.y + dy })
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
- bounds.value = clampBounds(next)
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
- startInteraction('grabbing')
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
+ }