niuma-ui 1.0.1 → 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 (153) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/package.json +10 -2
  3. package/src/__tests__/RsButton.spec.ts +47 -0
  4. package/src/__tests__/RsCard.spec.ts +160 -1
  5. package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
  6. package/src/__tests__/RsContextMenu.spec.ts +12 -0
  7. package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
  8. package/src/__tests__/RsDescriptions.spec.ts +50 -0
  9. package/src/__tests__/RsDialog.spec.ts +117 -0
  10. package/src/__tests__/RsDrawer.spec.ts +96 -3
  11. package/src/__tests__/RsDynamicTags.spec.ts +52 -0
  12. package/src/__tests__/RsForm.spec.ts +245 -1
  13. package/src/__tests__/RsInput.spec.ts +15 -1
  14. package/src/__tests__/RsMenu.spec.ts +406 -14
  15. package/src/__tests__/RsPagination.spec.ts +67 -2
  16. package/src/__tests__/RsSelect.spec.ts +42 -1
  17. package/src/__tests__/RsSwitch.spec.ts +44 -0
  18. package/src/__tests__/RsTable.a11y.spec.ts +83 -0
  19. package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
  20. package/src/__tests__/RsTable.perf.spec.ts +23 -4
  21. package/src/__tests__/RsTable.spec.ts +63 -8
  22. package/src/__tests__/RsTable.ssr.spec.ts +71 -0
  23. package/src/__tests__/RsTable.summary.spec.ts +45 -0
  24. package/src/__tests__/RsTabs.spec.ts +102 -4
  25. package/src/__tests__/RsTimePicker.spec.ts +11 -0
  26. package/src/__tests__/RsUpload.spec.ts +62 -6
  27. package/src/__tests__/createRsDialog.spec.ts +145 -0
  28. package/src/__tests__/form-rules.spec.ts +76 -0
  29. package/src/__tests__/perf-budgets.ts +32 -0
  30. package/src/__tests__/rs-table-api.spec.ts +206 -0
  31. package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
  32. package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
  33. package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
  34. package/src/__tests__/rs-table-isolation.spec.ts +158 -0
  35. package/src/__tests__/split-pane-utils.spec.ts +21 -0
  36. package/src/__tests__/table-chart-utils.spec.ts +88 -0
  37. package/src/__tests__/table-summary-utils.spec.ts +62 -0
  38. package/src/__tests__/table-utils.spec.ts +124 -0
  39. package/src/__tests__/theme.spec.ts +25 -0
  40. package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
  41. package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
  42. package/src/components/RsAlert.vue +212 -0
  43. package/src/components/RsButton.vue +145 -21
  44. package/src/components/RsCard.vue +200 -16
  45. package/src/components/RsConfirmDialog.vue +215 -28
  46. package/src/components/RsContextMenu.vue +51 -13
  47. package/src/components/RsDatePicker.vue +267 -34
  48. package/src/components/RsDateTimePicker.vue +19 -2
  49. package/src/components/RsDescriptions.vue +186 -0
  50. package/src/components/RsDescriptionsItem.vue +32 -0
  51. package/src/components/RsDialog.vue +363 -51
  52. package/src/components/RsDivider.vue +89 -0
  53. package/src/components/RsDrawer.vue +558 -33
  54. package/src/components/RsDropdown.vue +33 -2
  55. package/src/components/RsDynamicTags.vue +517 -0
  56. package/src/components/RsForm.vue +97 -8
  57. package/src/components/RsInput.vue +80 -82
  58. package/src/components/RsInputNumber.vue +64 -23
  59. package/src/components/RsLabel.vue +20 -5
  60. package/src/components/RsLoadingBar.vue +151 -0
  61. package/src/components/RsMenu.vue +73 -15
  62. package/src/components/RsMenuItems.vue +147 -9
  63. package/src/components/RsPagination.vue +93 -14
  64. package/src/components/RsRadio.vue +81 -0
  65. package/src/components/RsRadioItem.vue +156 -0
  66. package/src/components/RsSelect.vue +67 -58
  67. package/src/components/RsSplitPane.vue +69 -1
  68. package/src/components/RsSwitch.vue +179 -0
  69. package/src/components/RsTable.vue +653 -2555
  70. package/src/components/RsTabs.vue +415 -83
  71. package/src/components/RsTag.vue +191 -0
  72. package/src/components/RsTimePicker.vue +44 -5
  73. package/src/components/RsTimePickerColumns.vue +136 -77
  74. package/src/components/RsToaster.vue +58 -18
  75. package/src/components/RsTree.vue +39 -21
  76. package/src/components/RsUpload.vue +145 -12
  77. package/src/components/RsVNodeHost.vue +20 -0
  78. package/src/components/button-utils.ts +39 -0
  79. package/src/components/context-menu-utils.ts +6 -0
  80. package/src/components/date-picker-utils.ts +84 -4
  81. package/src/components/descriptions-utils.ts +24 -0
  82. package/src/components/dialog-utils.ts +147 -0
  83. package/src/components/dialog-window.ts +74 -9
  84. package/src/components/drawer-utils.ts +73 -0
  85. package/src/components/form-rules.ts +257 -0
  86. package/src/components/form-utils.ts +52 -2
  87. package/src/components/menu-utils.ts +11 -0
  88. package/src/components/overlay-utils.ts +3 -0
  89. package/src/components/radio-utils.ts +12 -0
  90. package/src/components/reka.ts +5 -0
  91. package/src/components/select-utils.ts +21 -2
  92. package/src/components/split-pane-utils.ts +65 -12
  93. package/src/components/table/RsTableBody.vue +217 -0
  94. package/src/components/table/RsTableBodyRow.vue +90 -1
  95. package/src/components/table/RsTableColGroup.vue +41 -0
  96. package/src/components/table/RsTableHeader.vue +158 -0
  97. package/src/components/table/RsTableSummaryRow.vue +52 -0
  98. package/src/components/table/rs-table-api.ts +99 -0
  99. package/src/components/table/rs-table-compat-matrix.ts +71 -0
  100. package/src/components/table/rs-table-echarts-adapter.ts +81 -0
  101. package/src/components/table/rs-table-feature-host.ts +178 -0
  102. package/src/components/table/rs-table-grid-nav.ts +122 -0
  103. package/src/components/table/rs-table-header-types.ts +47 -0
  104. package/src/components/table/rs-table-module-registry.ts +72 -0
  105. package/src/components/table/rs-table-props.ts +274 -0
  106. package/src/components/table/rs-table-view-context.ts +258 -0
  107. package/src/components/table/rs-table.css +84 -6
  108. package/src/components/table/table-chart-utils.ts +165 -0
  109. package/src/components/table/table-features.ts +284 -0
  110. package/src/components/table/table-summary-utils.ts +194 -0
  111. package/src/components/table-utils.ts +452 -2
  112. package/src/components/tabs-utils.ts +142 -2
  113. package/src/components/time-picker-utils.ts +76 -3
  114. package/src/components/upload-utils.ts +122 -0
  115. package/src/components/virtual-list-utils.ts +15 -2
  116. package/src/composables/assembleRsTableApi.ts +79 -0
  117. package/src/composables/bindRsTableViewContext.ts +269 -0
  118. package/src/composables/createRsDialog.ts +357 -0
  119. package/src/composables/createRsTableEditEmitBridge.ts +38 -0
  120. package/src/composables/useRsLoadingBar.ts +36 -0
  121. package/src/composables/useRsTable.ts +134 -0
  122. package/src/composables/useRsTableA11y.ts +52 -0
  123. package/src/composables/useRsTableChartBridge.ts +79 -0
  124. package/src/composables/useRsTableColumnLayout.ts +320 -0
  125. package/src/composables/useRsTableColumnResize.ts +244 -0
  126. package/src/composables/useRsTableColumnVirtual.ts +180 -0
  127. package/src/composables/useRsTableColumns.ts +200 -0
  128. package/src/composables/useRsTableContextMenu.ts +257 -0
  129. package/src/composables/useRsTableCore.ts +256 -0
  130. package/src/composables/useRsTableEditActions.ts +606 -0
  131. package/src/composables/useRsTableEditLayer.ts +251 -0
  132. package/src/composables/useRsTableEngine.ts +357 -0
  133. package/src/composables/useRsTableGridKeyboard.ts +123 -0
  134. package/src/composables/useRsTableHeadless.ts +137 -0
  135. package/src/composables/useRsTableInteraction.ts +433 -0
  136. package/src/composables/useRsTableScrollHost.ts +75 -0
  137. package/src/composables/useRsTableScrollLayout.ts +255 -0
  138. package/src/composables/useRsTableSelectionSource.ts +89 -0
  139. package/src/composables/useRsTableShell.ts +416 -0
  140. package/src/composables/useRsTableShellChrome.ts +170 -0
  141. package/src/composables/useRsTableSummary.ts +113 -0
  142. package/src/composables/useRsTableViewProvide.ts +263 -0
  143. package/src/composables/useRsTableVirtual.ts +154 -0
  144. package/src/composables/useRsTabsNav.ts +4 -0
  145. package/src/composables/useTableEdit.ts +1 -1
  146. package/src/index.ts +309 -9
  147. package/src/locale/messages.ts +30 -0
  148. package/src/styles.css +305 -52
  149. package/src/theme/brand.example.css +21 -2
  150. package/src/theme/presets.ts +4 -0
  151. package/src/theme/types.ts +67 -0
  152. package/src/components/RsTableColgroup.vue +0 -17
  153. package/src/components/RsTableHeaderRow.vue +0 -30
@@ -2,33 +2,77 @@
2
2
  import { computed, useSlots } from 'vue'
3
3
  import type { RsRadius } from '../theme/types'
4
4
  import { Primitive } from './reka'
5
- import { rsRadiusCss, useResolvedRsRadius } from './resolve-radius'
5
+ import { rsRadiusCss } from './resolve-radius'
6
6
 
7
+ /**
8
+ * 卡片表面风格。
9
+ * - grouped / plain / glass:偏 macOS 分层与仪表盘
10
+ * - outlined / filled:偏 Material 3,后台信息面板常用 outlined
11
+ */
7
12
  export type RsCardVariant = 'grouped' | 'plain' | 'outlined' | 'filled' | 'glass'
8
13
 
14
+ /**
15
+ * 卡片密度(疏密),映射 header/body padding 与标题字号。
16
+ * 与控件 `RsComponentSize`(高度)无关,勿混用。
17
+ */
18
+ export type RsCardSize = 'sm' | 'md' | 'lg'
19
+
9
20
  const props = withDefaults(
10
21
  defineProps<{
11
22
  as?: string
12
23
  title?: string
13
24
  description?: string
25
+ /** body 是否使用密度对应的内边距;false 时贴边,适合表格/图表 */
14
26
  padding?: boolean
15
27
  elevated?: boolean
16
- /** grouped=mac 分组面板;plain=mac 小组件;outlined/filled=Material 3;glass=mac 毛玻璃 */
28
+ /**
29
+ * 无外框:去掉边框、阴影与标题底部分隔,背景透明。
30
+ * 适合嵌在页面内容区、业务无需 :deep 清边框的场景。
31
+ */
32
+ borderless?: boolean
33
+ /** grouped=mac 分组;plain=KPI;outlined/filled=MD3;glass=毛玻璃 */
17
34
  variant?: RsCardVariant
18
- /** 圆角档位;默认 lg。直角面板传 `none`。 */
35
+ /**
36
+ * 密度:sm 后台紧凑 / md 默认(后台面板) / lg 前台宽松。
37
+ * @default md
38
+ */
39
+ size?: RsCardSize
40
+ /**
41
+ * 圆角档位。卡片表面独立于 ConfigProvider.controlRadius,
42
+ * 避免表单全局 sm 圆角把面板一并压扁。
43
+ * @default md
44
+ */
19
45
  radius?: RsRadius
46
+ /** hover 时轻微抬升阴影,适合可点击/可选中卡片 */
47
+ hoverable?: boolean
48
+ /**
49
+ * 裁切溢出(封面圆角、内部绝对定位)。
50
+ * 默认 false,避免表格/下拉被 overflow:hidden 裁切。
51
+ */
52
+ clip?: boolean
53
+ /** 占满父级高度,body 纵向伸展;适合后台 Pane 内嵌 */
54
+ fill?: boolean
20
55
  }>(),
21
56
  {
22
57
  as: 'section',
23
58
  padding: true,
24
59
  elevated: false,
60
+ borderless: false,
25
61
  variant: 'grouped',
62
+ size: 'md',
63
+ hoverable: false,
64
+ clip: false,
65
+ fill: false,
26
66
  },
27
67
  )
28
68
 
29
69
  const slots = useSlots()
30
- const hasHeader = computed(() => Boolean(props.title || props.description || slots.header))
31
- const resolvedRadius = useResolvedRsRadius(() => props.radius, 'lg')
70
+ const hasHeader = computed(() =>
71
+ Boolean(props.title || props.description || slots.header || slots.actions),
72
+ )
73
+ const hasFooter = computed(() => Boolean(slots.footer))
74
+ /** 卡片圆角只认 props,不跟 controlRadius 走 */
75
+ const resolvedRadius = computed(() => props.radius ?? 'md')
32
76
  const rootStyle = computed(() => ({
33
77
  '--rs-card-radius': rsRadiusCss(resolvedRadius.value),
34
78
  }))
@@ -40,10 +84,20 @@ const rootStyle = computed(() => ({
40
84
  class="rs-card"
41
85
  :class="[
42
86
  `rs-card--${variant}`,
43
- { 'rs-card--elevated': elevated },
87
+ `rs-card--${size}`,
88
+ {
89
+ 'rs-card--elevated': elevated,
90
+ 'rs-card--borderless': borderless,
91
+ 'rs-card--hoverable': hoverable,
92
+ 'rs-card--clip': clip,
93
+ 'rs-card--fill': fill,
94
+ },
44
95
  ]"
45
96
  :style="rootStyle"
46
97
  >
98
+ <div v-if="$slots.cover" class="rs-card__cover">
99
+ <slot name="cover" />
100
+ </div>
47
101
  <header v-if="hasHeader" class="rs-card__header">
48
102
  <div class="rs-card__heading">
49
103
  <slot name="header">
@@ -58,21 +112,91 @@ const rootStyle = computed(() => ({
58
112
  <div class="rs-card__body" :class="{ 'rs-card__body--padded': padding }">
59
113
  <slot />
60
114
  </div>
115
+ <footer v-if="hasFooter" class="rs-card__footer">
116
+ <slot name="footer" />
117
+ </footer>
61
118
  </Primitive>
62
119
  </template>
63
120
 
64
121
  <style>
65
122
  .rs-card {
123
+ --rs-card-header-padding-y: 0.75rem;
124
+ --rs-card-header-padding-x: 1rem;
125
+ --rs-card-body-padding: 1rem;
126
+ --rs-card-footer-padding-y: 0.625rem;
127
+ --rs-card-footer-padding-x: 1rem;
128
+ --rs-card-title-size: var(--rs-font-size-sm);
129
+ --rs-card-desc-size: var(--rs-font-size-xs);
130
+
66
131
  position: relative;
67
- overflow: hidden;
68
- border-radius: var(--rs-card-radius, var(--rs-radius-lg));
132
+ overflow: visible;
133
+ border-radius: var(--rs-card-radius, var(--rs-radius));
69
134
  border: 1px solid var(--rs-card-border);
70
135
  background: var(--rs-card-body-bg);
71
136
  box-shadow: var(--rs-card-shadow);
72
137
  isolation: isolate;
73
138
  }
74
139
 
140
+ .rs-card--sm {
141
+ --rs-card-header-padding-y: 0.5rem;
142
+ --rs-card-header-padding-x: 0.75rem;
143
+ --rs-card-body-padding: 0.75rem;
144
+ --rs-card-footer-padding-y: 0.5rem;
145
+ --rs-card-footer-padding-x: 0.75rem;
146
+ --rs-card-title-size: var(--rs-font-size-xs);
147
+ }
148
+
149
+ .rs-card--md {
150
+ --rs-card-header-padding-y: 0.75rem;
151
+ --rs-card-header-padding-x: 1rem;
152
+ --rs-card-body-padding: 1rem;
153
+ --rs-card-footer-padding-y: 0.625rem;
154
+ --rs-card-footer-padding-x: 1rem;
155
+ --rs-card-title-size: var(--rs-font-size-sm);
156
+ }
157
+
158
+ .rs-card--lg {
159
+ --rs-card-header-padding-y: 1rem;
160
+ --rs-card-header-padding-x: 1.25rem;
161
+ --rs-card-body-padding: 1.25rem;
162
+ --rs-card-footer-padding-y: 0.75rem;
163
+ --rs-card-footer-padding-x: 1.25rem;
164
+ --rs-card-title-size: var(--rs-font-size-base);
165
+ }
166
+
167
+ .rs-card--clip {
168
+ overflow: hidden;
169
+ }
170
+
171
+ .rs-card--fill {
172
+ display: flex;
173
+ flex-direction: column;
174
+ height: 100%;
175
+ min-height: 0;
176
+ }
177
+
178
+ .rs-card--fill .rs-card__body {
179
+ flex: 1 1 auto;
180
+ min-height: 0;
181
+ }
182
+
183
+ .rs-card--hoverable {
184
+ transition:
185
+ box-shadow 0.2s ease,
186
+ border-color 0.2s ease,
187
+ transform 0.2s ease;
188
+ cursor: default;
189
+ }
190
+
191
+ .rs-card--hoverable:hover {
192
+ box-shadow: var(--rs-card-shadow-elevated);
193
+ }
194
+
75
195
  /* ── grouped:macOS 系统设置分组面板(header 微分层 + inset 高光)── */
196
+ /* overflow:hidden 必须:header 实色与 backdrop-filter 才能贴合圆角,否则弧边溢出成直角 */
197
+ .rs-card--grouped {
198
+ overflow: hidden;
199
+ }
76
200
  [data-rs-theme='light'] .rs-card--grouped {
77
201
  background: color-mix(in srgb, var(--rs-card-body-bg) 94%, transparent);
78
202
  backdrop-filter: blur(20px) saturate(150%);
@@ -102,7 +226,6 @@ const rootStyle = computed(() => ({
102
226
  box-shadow: none;
103
227
  }
104
228
  .rs-card--plain .rs-card__header {
105
- padding-block: 0.625rem;
106
229
  background: transparent;
107
230
  border-bottom: 1px solid var(--rs-card-separator);
108
231
  box-shadow: none;
@@ -127,13 +250,11 @@ const rootStyle = computed(() => ({
127
250
  background: var(--rs-card-body-bg);
128
251
  }
129
252
  .rs-card--outlined .rs-card__header {
130
- padding-block: 1rem;
131
253
  background: transparent;
132
254
  border-bottom: 1px solid var(--rs-card-separator);
133
255
  box-shadow: none;
134
256
  }
135
257
  .rs-card--outlined .rs-card__title {
136
- font-size: var(--rs-font-size-base);
137
258
  font-weight: 500;
138
259
  letter-spacing: 0;
139
260
  }
@@ -156,7 +277,6 @@ const rootStyle = computed(() => ({
156
277
  box-shadow: none;
157
278
  }
158
279
  .rs-card--filled .rs-card__title {
159
- font-size: var(--rs-font-size-base);
160
280
  font-weight: 500;
161
281
  letter-spacing: 0;
162
282
  }
@@ -168,7 +288,9 @@ const rootStyle = computed(() => ({
168
288
  }
169
289
 
170
290
  /* ── glass:macOS Vibrancy 毛玻璃(强模糊 + 半透明)── */
291
+ /* 同 grouped:毛玻璃与 header 分层依赖 overflow 裁切圆角 */
171
292
  .rs-card--glass {
293
+ overflow: hidden;
172
294
  border-color: var(--rs-card-glass-border);
173
295
  background: var(--rs-card-glass-bg);
174
296
  backdrop-filter: blur(28px) saturate(180%);
@@ -200,38 +322,100 @@ const rootStyle = computed(() => ({
200
322
  0 12px 32px rgb(0 0 0 / 0.18);
201
323
  }
202
324
 
325
+ /* ── borderless:无外框(对齐 RsTabs borderless,业务无需 :deep)── */
326
+ .rs-card--borderless {
327
+ border: none;
328
+ box-shadow: none;
329
+ background: transparent;
330
+ backdrop-filter: none;
331
+ -webkit-backdrop-filter: none;
332
+ }
333
+ .rs-card--borderless .rs-card__header {
334
+ background: transparent;
335
+ border-bottom: none;
336
+ box-shadow: none;
337
+ }
338
+ .rs-card--borderless .rs-card__body {
339
+ background: transparent;
340
+ }
341
+ .rs-card--borderless .rs-card__footer {
342
+ border-top: none;
343
+ }
344
+ .rs-card--borderless.rs-card--elevated {
345
+ box-shadow: var(--rs-card-shadow-elevated);
346
+ }
347
+
203
348
  /* ── 公共结构 ── */
349
+ /* 首尾区块跟卡片圆角对齐,避免 overflow:visible 时直角底色顶出弧边 */
350
+ .rs-card__cover:first-child,
351
+ .rs-card__header:first-child {
352
+ border-top-left-radius: var(--rs-card-radius, var(--rs-radius));
353
+ border-top-right-radius: var(--rs-card-radius, var(--rs-radius));
354
+ }
355
+
356
+ .rs-card__footer:last-child,
357
+ .rs-card__body:last-child {
358
+ border-bottom-left-radius: var(--rs-card-radius, var(--rs-radius));
359
+ border-bottom-right-radius: var(--rs-card-radius, var(--rs-radius));
360
+ }
361
+
362
+ .rs-card__cover {
363
+ flex-shrink: 0;
364
+ line-height: 0;
365
+ }
366
+
367
+ .rs-card__cover > * {
368
+ display: block;
369
+ width: 100%;
370
+ }
371
+
204
372
  .rs-card__header {
205
373
  display: flex;
206
374
  align-items: flex-start;
207
375
  justify-content: space-between;
208
376
  gap: var(--rs-space-md);
209
377
  flex-shrink: 0;
210
- padding: 0.75rem 1rem 0.75rem 1.25rem;
378
+ padding: var(--rs-card-header-padding-y) var(--rs-card-header-padding-x);
211
379
  }
380
+
212
381
  .rs-card__heading {
213
382
  min-width: 0;
214
383
  }
384
+
215
385
  .rs-card__title {
216
386
  margin: 0;
217
- font-size: var(--rs-font-size-sm);
387
+ font-size: var(--rs-card-title-size);
218
388
  font-weight: 600;
219
389
  letter-spacing: -0.01em;
220
390
  color: var(--rs-card-title-fg);
221
391
  }
392
+
222
393
  .rs-card__description {
223
394
  margin: 0.2rem 0 0;
224
- font-size: var(--rs-font-size-xs);
395
+ font-size: var(--rs-card-desc-size);
225
396
  color: var(--rs-card-description-fg);
226
397
  line-height: var(--rs-line-height-normal);
227
398
  }
399
+
228
400
  .rs-card__actions {
229
401
  display: flex;
230
402
  align-items: center;
231
403
  gap: var(--rs-space-xs);
232
404
  flex-shrink: 0;
233
405
  }
406
+
234
407
  .rs-card__body--padded {
235
- padding: 1.25rem;
408
+ padding: var(--rs-card-body-padding);
409
+ }
410
+
411
+ .rs-card__footer {
412
+ display: flex;
413
+ align-items: center;
414
+ justify-content: flex-end;
415
+ flex-wrap: wrap;
416
+ gap: var(--rs-space-sm);
417
+ flex-shrink: 0;
418
+ padding: var(--rs-card-footer-padding-y) var(--rs-card-footer-padding-x);
419
+ border-top: 1px solid var(--rs-card-separator);
236
420
  }
237
421
  </style>
@@ -1,7 +1,6 @@
1
1
  <script setup lang="ts">
2
+ import { computed, type Component } from 'vue'
2
3
  import {
3
- AlertDialogAction,
4
- AlertDialogCancel,
5
4
  AlertDialogContent,
6
5
  AlertDialogDescription,
7
6
  AlertDialogOverlay,
@@ -11,25 +10,58 @@ import {
11
10
  } from './reka'
12
11
  import RsButton from './RsButton.vue'
13
12
  import { useRsI18n } from '../composables/useRsI18n'
13
+ import {
14
+ isRsDialogWidthPreset,
15
+ resolveDialogOverlayStyle,
16
+ resolveRsDialogCssWidth,
17
+ runRsConfirmBeforeClose,
18
+ type RsConfirmBeforeClose,
19
+ type RsConfirmCloseReason,
20
+ type RsDialogWidth,
21
+ } from './dialog-utils'
14
22
  import type { RsFeedbackTone } from './overlay-utils'
15
23
 
16
24
  const open = defineModel<boolean>('open', { default: false })
17
25
 
18
- withDefaults(
26
+ const props = withDefaults(
19
27
  defineProps<{
20
28
  title?: string
29
+ /** 次要说明,显示在标题与正文之间 */
30
+ subtitle?: string
21
31
  description?: string
22
32
  tone?: RsFeedbackTone
33
+ /** 自定义图标组件;不传则使用默认 "!" */
34
+ icon?: Component
35
+ /** 宽度:预设或自定义;不传保持历史默认 max-width: 28rem */
36
+ width?: RsDialogWidth
23
37
  confirmText?: string
24
38
  cancelText?: string
25
39
  confirmVariant?: 'primary' | 'danger'
40
+ /** 是否显示取消按钮;false 时为单按钮提示 */
41
+ showCancel?: boolean
42
+ /** 确认中:禁用按钮并阻止 Esc 关闭 */
43
+ confirmLoading?: boolean
44
+ /**
45
+ * 点击确认后是否自动关闭。默认 true。
46
+ * 命令式异步 onConfirm 场景可设为 false,由调用方在结束后关闭。
47
+ */
48
+ autoCloseOnConfirm?: boolean
49
+ /** 关闭前钩子;返回 false 可阻止关闭(支持 async) */
50
+ beforeClose?: RsConfirmBeforeClose
26
51
  showOverlay?: boolean
27
- /** AlertDialogPortal 挂载目标(id 选择器或 Element) */
28
- teleportTo?: string | HTMLElement
52
+ /** 遮罩不透明度 0–1;覆盖主题默认(最浅) */
53
+ overlayOpacity?: number
54
+ /** 遮罩模糊;number 为 px。默认主题为 0 */
55
+ overlayBlur?: number | string
56
+ /** AlertDialogPortal 挂载目标;false 禁用 Teleport */
57
+ teleportTo?: string | HTMLElement | false
29
58
  }>(),
30
59
  {
31
60
  tone: 'danger',
32
61
  confirmVariant: 'danger',
62
+ showCancel: true,
63
+ confirmLoading: false,
64
+ autoCloseOnConfirm: true,
33
65
  showOverlay: false,
34
66
  },
35
67
  )
@@ -41,45 +73,170 @@ const emit = defineEmits<{
41
73
 
42
74
  const { t } = useRsI18n()
43
75
 
44
- function onConfirm(): void {
76
+ const contentClass = computed(() => {
77
+ if (props.width && isRsDialogWidthPreset(props.width)) {
78
+ return `rs-confirm-dialog__content--${props.width}`
79
+ }
80
+ return undefined
81
+ })
82
+
83
+ const overlayStyle = computed(() =>
84
+ resolveDialogOverlayStyle({
85
+ overlayOpacity: props.overlayOpacity,
86
+ overlayBlur: props.overlayBlur,
87
+ }),
88
+ )
89
+
90
+ const contentStyle = computed(() => {
91
+ if (props.width == null || isRsDialogWidthPreset(props.width)) return undefined
92
+ const css = resolveRsDialogCssWidth(props.width)
93
+ if (!css) return undefined
94
+ return { maxWidth: css }
95
+ })
96
+
97
+ const descriptionText = computed(() => {
98
+ if (props.description != null && props.description !== '') return props.description
99
+ return undefined
100
+ })
101
+
102
+ const showDefaultDescription = computed(
103
+ () => descriptionText.value == null && !props.subtitle,
104
+ )
105
+
106
+ let closing = false
107
+ let pendingCloseReason: RsConfirmCloseReason = 'programmatic'
108
+
109
+ async function requestClose(reason: RsConfirmCloseReason): Promise<boolean> {
110
+ if (!open.value || closing) return false
111
+ closing = true
112
+ try {
113
+ const allowed = await runRsConfirmBeforeClose(props.beforeClose, reason)
114
+ if (!allowed) return false
115
+ open.value = false
116
+ return true
117
+ } finally {
118
+ closing = false
119
+ }
120
+ }
121
+
122
+ async function onConfirmClick(): Promise<void> {
45
123
  emit('confirm')
46
- open.value = false
124
+ if (props.autoCloseOnConfirm !== false) {
125
+ await requestClose('confirm')
126
+ }
127
+ }
128
+
129
+ async function onCancelClick(): Promise<void> {
130
+ const closed = await requestClose('cancel')
131
+ if (closed) emit('cancel')
132
+ }
133
+
134
+ function onEscapeKeyDown(event: Event): void {
135
+ if (props.confirmLoading) {
136
+ event.preventDefault()
137
+ return
138
+ }
139
+ pendingCloseReason = 'escape'
47
140
  }
48
141
 
49
- function onCancel(): void {
50
- emit('cancel')
142
+ async function onUpdateOpen(next: boolean): Promise<void> {
143
+ if (next) {
144
+ open.value = true
145
+ return
146
+ }
147
+ // requestClose 已处理 beforeClose;此处承接 Esc 等 Root 发起的关闭
148
+ if (closing) {
149
+ open.value = false
150
+ return
151
+ }
152
+ const reason = pendingCloseReason
153
+ pendingCloseReason = 'programmatic'
154
+ const allowed = await runRsConfirmBeforeClose(props.beforeClose, reason)
155
+ if (!allowed) {
156
+ open.value = true
157
+ return
158
+ }
159
+ open.value = false
160
+ if (reason === 'escape' || reason === 'programmatic') {
161
+ emit('cancel')
162
+ }
51
163
  }
164
+
165
+ defineExpose({
166
+ /** 请求关闭(走 beforeClose) */
167
+ close: (reason: RsConfirmCloseReason = 'programmatic') => requestClose(reason),
168
+ })
52
169
  </script>
53
170
 
54
171
  <template>
55
- <AlertDialogRoot v-model:open="open">
56
- <AlertDialogPortal :to="teleportTo">
57
- <AlertDialogOverlay v-if="showOverlay" class="rs-confirm-dialog__overlay" />
172
+ <AlertDialogRoot :open="open" @update:open="onUpdateOpen">
173
+ <AlertDialogPortal
174
+ :disabled="teleportTo === false"
175
+ :to="teleportTo === false ? undefined : teleportTo"
176
+ >
177
+ <AlertDialogOverlay v-if="showOverlay" class="rs-confirm-dialog__overlay" :style="overlayStyle" />
58
178
  <AlertDialogContent
59
179
  class="rs-confirm-dialog__content"
180
+ :class="contentClass"
181
+ :style="contentStyle"
60
182
  :disable-outside-pointer-events="true"
183
+ @escape-key-down="onEscapeKeyDown"
61
184
  >
62
185
  <div class="rs-confirm-dialog__icon" :class="`rs-confirm-dialog__icon--${tone}`">
63
- !
186
+ <slot name="icon">
187
+ <component :is="icon" v-if="icon" class="rs-confirm-dialog__icon-glyph" />
188
+ <template v-else>!</template>
189
+ </slot>
64
190
  </div>
65
191
  <div class="rs-confirm-dialog__main">
66
192
  <AlertDialogTitle class="rs-confirm-dialog__title">
67
193
  {{ title ?? t('confirm.title') }}
68
194
  </AlertDialogTitle>
69
- <AlertDialogDescription class="rs-confirm-dialog__description">
70
- {{ description ?? t('confirm.description') }}
195
+ <p
196
+ v-if="subtitle && descriptionText != null"
197
+ class="rs-confirm-dialog__subtitle"
198
+ >
199
+ {{ subtitle }}
200
+ </p>
201
+ <AlertDialogDescription
202
+ v-if="descriptionText != null"
203
+ class="rs-confirm-dialog__description"
204
+ >
205
+ {{ descriptionText }}
206
+ </AlertDialogDescription>
207
+ <AlertDialogDescription
208
+ v-else-if="subtitle"
209
+ class="rs-confirm-dialog__subtitle"
210
+ >
211
+ {{ subtitle }}
212
+ </AlertDialogDescription>
213
+ <AlertDialogDescription
214
+ v-else-if="showDefaultDescription"
215
+ class="rs-confirm-dialog__description"
216
+ >
217
+ {{ t('confirm.description') }}
71
218
  </AlertDialogDescription>
219
+ <div v-if="$slots.extra" class="rs-confirm-dialog__extra">
220
+ <slot name="extra" />
221
+ </div>
72
222
  <footer class="rs-confirm-dialog__footer">
73
- <AlertDialogCancel as-child>
74
- <RsButton variant="default" @click="onCancel">
75
- {{ cancelText ?? t('common.cancel') }}
76
- </RsButton>
77
- </AlertDialogCancel>
78
- <AlertDialogAction as-child>
79
- <RsButton :variant="confirmVariant" @click="onConfirm">
80
- {{ confirmText ?? t('common.confirm') }}
81
- </RsButton>
82
- </AlertDialogAction>
223
+ <!-- 不用 AlertDialogCancel/Action:关闭统一走 beforeClose / autoCloseOnConfirm -->
224
+ <RsButton
225
+ v-if="showCancel"
226
+ variant="default"
227
+ :disabled="confirmLoading"
228
+ @click="onCancelClick"
229
+ >
230
+ {{ cancelText ?? t('common.cancel') }}
231
+ </RsButton>
232
+ <RsButton
233
+ :variant="confirmVariant"
234
+ :loading="confirmLoading"
235
+ :disabled="confirmLoading"
236
+ @click="onConfirmClick"
237
+ >
238
+ {{ confirmText ?? t('common.confirm') }}
239
+ </RsButton>
83
240
  </footer>
84
241
  </div>
85
242
  </AlertDialogContent>
@@ -114,10 +271,19 @@ function onCancel(): void {
114
271
  border: 1px solid var(--rs-dialog-border);
115
272
  border-radius: var(--rs-radius-lg);
116
273
  background: var(--rs-dialog-body-bg);
117
- color: var(--rs-text);
274
+ color: var(--rs-dialog-title-fg);
118
275
  box-shadow: var(--rs-dialog-shadow);
119
276
  outline: none;
120
277
  }
278
+ .rs-confirm-dialog__content--sm {
279
+ max-width: 24rem;
280
+ }
281
+ .rs-confirm-dialog__content--md {
282
+ max-width: 32rem;
283
+ }
284
+ .rs-confirm-dialog__content--lg {
285
+ max-width: 42rem;
286
+ }
121
287
  [data-rs-theme='light'] .rs-confirm-dialog__content {
122
288
  background: color-mix(in srgb, var(--rs-dialog-bg) 94%, transparent);
123
289
  backdrop-filter: blur(24px) saturate(180%);
@@ -133,6 +299,11 @@ function onCancel(): void {
133
299
  border-radius: var(--rs-radius);
134
300
  font-weight: 700;
135
301
  }
302
+ .rs-confirm-dialog__icon-glyph {
303
+ width: 1.25rem;
304
+ height: 1.25rem;
305
+ display: block;
306
+ }
136
307
  .rs-confirm-dialog__icon--danger {
137
308
  background: var(--rs-danger-container);
138
309
  color: var(--rs-on-danger-container);
@@ -161,18 +332,34 @@ function onCancel(): void {
161
332
  letter-spacing: -0.01em;
162
333
  color: var(--rs-dialog-title-fg);
163
334
  }
335
+ .rs-confirm-dialog__subtitle {
336
+ margin: var(--rs-space-xs) 0 0;
337
+ color: var(--rs-dialog-description-fg);
338
+ font-size: var(--rs-font-size-sm);
339
+ line-height: var(--rs-line-height-normal);
340
+ opacity: 0.85;
341
+ }
164
342
  .rs-confirm-dialog__description {
165
343
  margin: var(--rs-space-sm) 0 0;
166
344
  color: var(--rs-dialog-description-fg);
167
345
  font-size: var(--rs-font-size-sm);
168
346
  line-height: var(--rs-line-height-normal);
347
+ white-space: pre-wrap;
348
+ word-break: break-word;
349
+ }
350
+ .rs-confirm-dialog__extra {
351
+ margin-top: var(--rs-space-md);
352
+ min-width: 0;
169
353
  }
170
354
  .rs-confirm-dialog__footer {
171
355
  display: flex;
172
356
  justify-content: flex-end;
357
+ flex-wrap: wrap;
173
358
  gap: var(--rs-space-sm);
359
+ box-sizing: border-box;
360
+ min-height: var(--rs-dialog-footer-min-height);
174
361
  margin-top: var(--rs-space-xl);
175
- padding-top: var(--rs-space-lg);
176
- border-top: 1px solid var(--rs-dialog-separator);
362
+ padding-top: var(--rs-dialog-footer-padding-y);
363
+ border-top: 1px solid color-mix(in srgb, var(--rs-dialog-separator) 72%, transparent);
177
364
  }
178
365
  </style>