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
@@ -119,21 +119,21 @@ const sonnerTheme = computed<'light' | 'dark'>(() => {
119
119
  overflow: hidden;
120
120
  display: grid;
121
121
  grid-template-columns: auto minmax(0, 1fr);
122
- align-items: start;
123
- column-gap: 0.75rem;
124
- row-gap: 0.25rem;
122
+ align-items: center;
123
+ column-gap: 0.625rem;
124
+ row-gap: 0.5rem;
125
125
  width: var(--width, auto);
126
126
  min-width: 19rem;
127
127
  max-width: min(29rem, calc(100vw - 1.5rem));
128
- padding: 0.875rem 0.9375rem 0.9375rem;
128
+ padding: 0.8125rem 2.25rem 0.8125rem 0.875rem;
129
129
  border: 1px solid var(--rs-toast-border);
130
130
  border-radius: var(--rs-toast-radius);
131
131
  background:
132
132
  linear-gradient(
133
133
  180deg,
134
- transparent 0.625rem,
135
- var(--rs-toast-accent) 0.625rem calc(100% - 0.625rem),
136
- transparent calc(100% - 0.625rem)
134
+ transparent 0.5rem,
135
+ var(--rs-toast-accent) 0.5rem calc(100% - 0.5rem),
136
+ transparent calc(100% - 0.5rem)
137
137
  )
138
138
  0.5rem 0 / 2px 100% no-repeat,
139
139
  linear-gradient(180deg, var(--rs-toast-highlight), transparent 38%),
@@ -151,6 +151,13 @@ const sonnerTheme = computed<'light' | 'dark'>(() => {
151
151
  background var(--rs-transition-fast);
152
152
  }
153
153
 
154
+ /* 带描述时顶对齐,图标与标题第一行光学对齐 */
155
+ .rs-toast:has([data-description]) {
156
+ align-items: start;
157
+ padding-top: 0.875rem;
158
+ padding-bottom: 0.875rem;
159
+ }
160
+
154
161
  .rs-toast:hover {
155
162
  box-shadow:
156
163
  0 18px 42px -20px rgb(0 0 0 / 0.34),
@@ -166,32 +173,48 @@ const sonnerTheme = computed<'light' | 'dark'>(() => {
166
173
  .rs-toast [data-icon] {
167
174
  position: relative;
168
175
  z-index: 1;
176
+ grid-column: 1;
177
+ grid-row: 1;
169
178
  display: inline-flex;
170
- grid-row: 1 / span 2;
171
179
  align-items: center;
172
180
  justify-content: center;
173
- width: 1.75rem;
174
- height: 1.75rem;
175
- margin-top: 0.125rem;
181
+ align-self: center;
182
+ width: 1.25rem;
183
+ height: 1.25rem;
184
+ margin: 0;
176
185
  border-radius: 999px;
177
186
  background: transparent;
178
187
  color: var(--rs-info);
179
188
  box-shadow: none;
180
189
  }
181
190
 
191
+ .rs-toast [data-icon] svg {
192
+ width: 1.25rem;
193
+ height: 1.25rem;
194
+ flex-shrink: 0;
195
+ }
196
+
197
+ /* 多行内容时,图标略下移以对齐标题字面中线 */
198
+ .rs-toast:has([data-description]) [data-icon] {
199
+ align-self: start;
200
+ margin-top: 0.125rem;
201
+ }
202
+
182
203
  .rs-toast [data-content] {
183
204
  position: relative;
184
205
  z-index: 1;
206
+ grid-column: 2;
207
+ grid-row: 1;
185
208
  display: grid;
186
209
  gap: 0.1875rem;
187
210
  min-width: 0;
188
- padding-right: 1.5rem;
189
211
  }
190
212
 
191
213
  .rs-toast [data-button] {
192
214
  position: relative;
193
215
  z-index: 1;
194
- margin-top: 0.625rem;
216
+ grid-column: 2;
217
+ margin-top: 0;
195
218
  }
196
219
 
197
220
  .rs-toast__title {
@@ -199,7 +222,7 @@ const sonnerTheme = computed<'light' | 'dark'>(() => {
199
222
  color: var(--rs-text);
200
223
  font-size: 0.875rem;
201
224
  font-weight: 600;
202
- line-height: 1.3;
225
+ line-height: 1.25;
203
226
  letter-spacing: -0.012em;
204
227
  }
205
228
 
@@ -212,7 +235,10 @@ const sonnerTheme = computed<'light' | 'dark'>(() => {
212
235
 
213
236
  .rs-toast [data-close-button].rs-toast__close {
214
237
  position: absolute;
215
- inset: 0.75rem 0.75rem auto auto;
238
+ top: 50%;
239
+ right: 0.625rem;
240
+ left: auto;
241
+ bottom: auto;
216
242
  z-index: 2;
217
243
  display: inline-flex;
218
244
  align-items: center;
@@ -226,7 +252,7 @@ const sonnerTheme = computed<'light' | 'dark'>(() => {
226
252
  color: var(--rs-muted);
227
253
  box-shadow: none;
228
254
  cursor: pointer;
229
- transform: none;
255
+ transform: translateY(-50%);
230
256
  opacity: 0.72;
231
257
  transition:
232
258
  opacity var(--rs-transition-fast),
@@ -236,6 +262,11 @@ const sonnerTheme = computed<'light' | 'dark'>(() => {
236
262
  box-shadow var(--rs-transition-fast);
237
263
  }
238
264
 
265
+ .rs-toast:has([data-description]) [data-close-button].rs-toast__close {
266
+ top: 0.75rem;
267
+ transform: none;
268
+ }
269
+
239
270
  .rs-toast [data-close-button].rs-toast__close svg {
240
271
  width: 0.75rem;
241
272
  height: 0.75rem;
@@ -371,16 +402,25 @@ const sonnerTheme = computed<'light' | 'dark'>(() => {
371
402
  .rs-toast {
372
403
  min-width: min(100%, 18.5rem);
373
404
  max-width: calc(100vw - 1rem);
374
- padding: 0.8125rem 0.875rem 0.875rem;
405
+ padding: 0.75rem 2.125rem 0.75rem 0.8125rem;
375
406
  border-radius: 0.9375rem;
376
407
  }
377
408
 
409
+ .rs-toast:has([data-description]) {
410
+ padding-top: 0.8125rem;
411
+ padding-bottom: 0.8125rem;
412
+ }
413
+
378
414
  .rs-toast [data-close-button].rs-toast__close {
379
- inset: 0.625rem 0.625rem auto auto;
415
+ right: 0.5rem;
380
416
  width: 1.25rem;
381
417
  height: 1.25rem;
382
418
  }
383
419
 
420
+ .rs-toast:has([data-description]) [data-close-button].rs-toast__close {
421
+ top: 0.625rem;
422
+ }
423
+
384
424
  .rs-toast__action {
385
425
  min-height: 1.875rem;
386
426
  padding: 0 0.6875rem;
@@ -39,7 +39,11 @@ import {
39
39
  splitTreeLabelHighlight,
40
40
  toggleTreeCheck,
41
41
  } from './tree-utils'
42
- import { resolveVirtualListHeight } from './virtual-list-utils'
42
+ import {
43
+ isVirtualListFillHeight,
44
+ parseVirtualListHeightPx,
45
+ resolveVirtualListHeight,
46
+ } from './virtual-list-utils'
43
47
 
44
48
  defineOptions({ name: 'RsTree' })
45
49
 
@@ -130,12 +134,19 @@ const _measuredHeight = ref(0)
130
134
  let _heightObs: ResizeObserver | null = null
131
135
 
132
136
  onMounted(() => {
133
- // 必须由消费方显式声明 virtual,才启动自动高度测量
134
- if (!props.virtual || props.height !== undefined || !treeRootRef.value) return
137
+ // 虚拟滚动且高度为填满/百分比/未指定时,用 ResizeObserver 测量真实视口
138
+ if (!props.virtual || !treeRootRef.value) return
139
+ if (
140
+ props.height !== undefined
141
+ && !isVirtualListFillHeight(props.height)
142
+ ) {
143
+ return
144
+ }
135
145
  _heightObs = new ResizeObserver((entries) => {
136
146
  _measuredHeight.value = entries[0]?.contentRect.height ?? 0
137
147
  })
138
148
  _heightObs.observe(treeRootRef.value)
149
+ _measuredHeight.value = treeRootRef.value.clientHeight
139
150
  })
140
151
 
141
152
  onUnmounted(() => {
@@ -243,10 +254,12 @@ if (import.meta.env.DEV) {
243
254
  }
244
255
 
245
256
  const viewportHeightPx = computed(() => {
246
- if (props.height !== undefined) {
247
- return Number.parseInt(resolveVirtualListHeight(props.height, 320) ?? '', 10)
248
- }
249
- return _measuredHeight.value > 0 ? _measuredHeight.value : 320
257
+ const parsed = parseVirtualListHeightPx(props.height)
258
+ if (parsed != null) return parsed
259
+ if (_measuredHeight.value > 0) return _measuredHeight.value
260
+ const el = treeRootRef.value
261
+ if (el && el.clientHeight > 0) return el.clientHeight
262
+ return 320
250
263
  })
251
264
 
252
265
  /** 过滤后滚回顶部;内容缩短时钳位,避免内部 scrollTop 与 DOM 脱节导致虚拟列表空白 */
@@ -288,9 +301,13 @@ const fillCapture = computed(() => props.virtual || props.height !== undefined)
288
301
 
289
302
  const viewportStyle = computed(() => {
290
303
  if (!useVirtualScroll.value && props.height === undefined) return undefined
291
- if (props.height !== undefined) {
304
+ if (props.height !== undefined && !isVirtualListFillHeight(props.height)) {
292
305
  return { maxHeight: resolveVirtualListHeight(props.height, 320) }
293
306
  }
307
+ // 百分比 / 填满:由 CSS height:100% 承接,不再写死 maxHeight px
308
+ if (isVirtualListFillHeight(props.height)) {
309
+ return { height: '100%', maxHeight: '100%' }
310
+ }
294
311
  const h = _measuredHeight.value > 0 ? _measuredHeight.value : 320
295
312
  return { maxHeight: `${h}px` }
296
313
  })
@@ -954,27 +971,27 @@ defineExpose({
954
971
  }
955
972
 
956
973
  .rs-tree__row:hover:not(.rs-tree__row--disabled):not(.rs-tree__row--selected) {
957
- background: var(--rs-item-hover);
974
+ background: var(--rs-tree-row-hover);
958
975
  }
959
976
 
960
977
  .rs-tree__row--selected {
961
- background: color-mix(in srgb, var(--rs-primary) 14%, var(--rs-surface));
978
+ background: var(--rs-tree-selected-bg);
962
979
  }
963
980
 
964
981
  .rs-tree__row--selected .rs-tree__label,
965
982
  .rs-tree__row--selected .rs-tree__label--block {
966
- color: var(--rs-primary);
967
- font-weight: 500;
983
+ color: var(--rs-tree-selected-fg);
984
+ font-weight: inherit;
968
985
  }
969
986
 
970
987
  .rs-tree__row--focused {
971
- background: color-mix(in srgb, var(--rs-primary) 8%, var(--rs-surface));
972
- box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--rs-primary) 55%, transparent);
988
+ background: var(--rs-tree-focused-bg);
989
+ box-shadow: inset 0 0 0 1px var(--rs-tree-focused-ring);
973
990
  }
974
991
 
975
992
  .rs-tree__row--selected.rs-tree__row--focused {
976
- background: color-mix(in srgb, var(--rs-primary) 18%, var(--rs-surface));
977
- box-shadow: inset 0 0 0 1px var(--rs-primary);
993
+ background: var(--rs-tree-selected-focused-bg);
994
+ box-shadow: inset 0 0 0 1px var(--rs-tree-selected-focused-ring);
978
995
  }
979
996
 
980
997
  .rs-tree--block .rs-tree__row {
@@ -993,7 +1010,7 @@ defineExpose({
993
1010
  left: 0;
994
1011
  right: 0;
995
1012
  height: 2px;
996
- background: var(--rs-primary);
1013
+ background: var(--rs-tree-drop-indicator);
997
1014
  pointer-events: none;
998
1015
  }
999
1016
 
@@ -1015,12 +1032,13 @@ defineExpose({
1015
1032
  pointer-events: none;
1016
1033
  }
1017
1034
 
1035
+ /* 导航线:半透明字色 token,避免宿主把 --rs-border-subtle 桥成实色灰后过深 */
1018
1036
  .rs-tree--line .rs-tree__line-vert {
1019
1037
  position: absolute;
1020
1038
  top: 0;
1021
1039
  bottom: 0;
1022
- width: 1px;
1023
- background: var(--rs-border-subtle);
1040
+ width: var(--rs-tree-line-width, 1px);
1041
+ background: var(--rs-tree-line);
1024
1042
  }
1025
1043
 
1026
1044
  .rs-tree--line .rs-tree__line-vert--self.rs-tree__line-vert--end {
@@ -1030,8 +1048,8 @@ defineExpose({
1030
1048
  .rs-tree--line .rs-tree__line-horz {
1031
1049
  position: absolute;
1032
1050
  top: 50%;
1033
- height: 1px;
1034
- background: var(--rs-border-subtle);
1051
+ height: var(--rs-tree-line-width, 1px);
1052
+ background: var(--rs-tree-line);
1035
1053
  }
1036
1054
 
1037
1055
  .rs-tree__drag-handle {
@@ -1,7 +1,14 @@
1
1
  <script setup lang="ts">
2
- import { computed } from 'vue'
2
+ import { computed, ref } from 'vue'
3
3
  import RsIcon from './RsIcon.vue'
4
- import { formatFileSize, mergeUploadFiles, removeUploadFileAt, validateUploadFiles } from './upload-utils'
4
+ import {
5
+ downloadUploadFile,
6
+ formatFileSize,
7
+ mergeUploadFiles,
8
+ removeUploadFileAt,
9
+ resolveUploadFileIcon,
10
+ validateUploadFiles,
11
+ } from './upload-utils'
5
12
  import { useRsI18n } from '../composables/useRsI18n'
6
13
 
7
14
  const model = defineModel<File[]>({ default: () => [] })
@@ -15,25 +22,44 @@ const props = withDefaults(
15
22
  disabled?: boolean
16
23
  label?: string
17
24
  hint?: string
25
+ /** 是否展示已选文件列表 */
26
+ showFileList?: boolean
27
+ /** 文件行是否显示下载按钮 */
28
+ showDownload?: boolean
29
+ /** 达到 maxCount 后隐藏拖拽区(仅保留列表) */
30
+ hideDropzoneWhenFull?: boolean
18
31
  }>(),
19
32
  {
20
33
  multiple: false,
21
34
  disabled: false,
35
+ showFileList: true,
36
+ showDownload: false,
37
+ hideDropzoneWhenFull: false,
22
38
  },
23
39
  )
24
40
 
25
41
  const emit = defineEmits<{
26
42
  reject: [errors: ReturnType<typeof validateUploadFiles>['rejected']]
43
+ download: [file: File, index: number]
44
+ remove: [file: File, index: number]
27
45
  }>()
28
46
 
29
47
  const { t } = useRsI18n()
48
+ const dragging = ref(false)
49
+ const dragDepth = ref(0)
50
+
30
51
  const canAdd = computed(() => !props.maxCount || model.value.length < props.maxCount)
31
52
  const isDropzoneDisabled = computed(() => props.disabled || !canAdd.value)
32
- const resolvedHint = computed(() => props.hint ?? t('upload.browse'))
53
+ const showDropzone = computed(
54
+ () => !(props.hideDropzoneWhenFull && props.maxCount != null && model.value.length >= props.maxCount),
55
+ )
56
+ const resolvedHint = computed(() => {
57
+ if (dragging.value && !isDropzoneDisabled.value) return t('upload.dropActive')
58
+ return props.hint ?? t('upload.browse')
59
+ })
33
60
 
34
- function onFilesChange(event: Event): void {
35
- const input = event.target as HTMLInputElement
36
- const files = Array.from(input.files ?? [])
61
+ function applyFiles(files: File[]): void {
62
+ if (!files.length || isDropzoneDisabled.value) return
37
63
  const result = validateUploadFiles(files, {
38
64
  accept: props.accept,
39
65
  maxSize: props.maxSize,
@@ -41,19 +67,71 @@ function onFilesChange(event: Event): void {
41
67
  })
42
68
  model.value = mergeUploadFiles(model.value, result.accepted, props.maxCount)
43
69
  if (result.rejected.length) emit('reject', result.rejected)
70
+ }
71
+
72
+ function onFilesChange(event: Event): void {
73
+ const input = event.target as HTMLInputElement
74
+ applyFiles(Array.from(input.files ?? []))
44
75
  input.value = ''
45
76
  }
46
77
 
78
+ function onDragEnter(event: DragEvent): void {
79
+ event.preventDefault()
80
+ if (isDropzoneDisabled.value) return
81
+ dragDepth.value += 1
82
+ dragging.value = true
83
+ }
84
+
85
+ function onDragOver(event: DragEvent): void {
86
+ event.preventDefault()
87
+ if (event.dataTransfer) event.dataTransfer.dropEffect = isDropzoneDisabled.value ? 'none' : 'copy'
88
+ }
89
+
90
+ function onDragLeave(event: DragEvent): void {
91
+ event.preventDefault()
92
+ dragDepth.value = Math.max(0, dragDepth.value - 1)
93
+ if (dragDepth.value === 0) dragging.value = false
94
+ }
95
+
96
+ function onDrop(event: DragEvent): void {
97
+ event.preventDefault()
98
+ dragDepth.value = 0
99
+ dragging.value = false
100
+ if (isDropzoneDisabled.value) return
101
+ applyFiles(Array.from(event.dataTransfer?.files ?? []))
102
+ }
103
+
47
104
  function removeFile(index: number): void {
105
+ const file = model.value[index]
106
+ if (!file) return
48
107
  model.value = removeUploadFileAt(model.value, index)
108
+ emit('remove', file, index)
109
+ }
110
+
111
+ function onDownload(file: File, index: number): void {
112
+ emit('download', file, index)
113
+ // 无外部监听时仍提供默认下载;有监听时由业务决定是否自行处理
114
+ downloadUploadFile(file)
115
+ }
116
+
117
+ function fileIcon(file: File): string {
118
+ return resolveUploadFileIcon(file)
49
119
  }
50
120
  </script>
51
121
 
52
122
  <template>
53
123
  <div class="rs-upload">
54
124
  <label
125
+ v-if="showDropzone"
55
126
  class="rs-upload__dropzone"
56
- :class="{ 'rs-upload__dropzone--disabled': isDropzoneDisabled }"
127
+ :class="{
128
+ 'rs-upload__dropzone--disabled': isDropzoneDisabled,
129
+ 'rs-upload__dropzone--dragging': dragging && !isDropzoneDisabled,
130
+ }"
131
+ @dragenter="onDragEnter"
132
+ @dragover="onDragOver"
133
+ @dragleave="onDragLeave"
134
+ @drop="onDrop"
57
135
  >
58
136
  <input
59
137
  class="rs-upload__input"
@@ -71,20 +149,36 @@ function removeFile(index: number): void {
71
149
  <span class="rs-upload__hint">{{ resolvedHint }}</span>
72
150
  </span>
73
151
  </label>
74
- <ul v-if="model.length" class="rs-upload__list">
152
+
153
+ <ul v-if="showFileList && model.length" class="rs-upload__list">
75
154
  <li
76
155
  v-for="(file, index) in model"
77
156
  :key="`${file.name}-${file.size}-${index}`"
78
157
  class="rs-upload__file"
79
158
  >
80
- <span class="rs-upload__file-icon" aria-hidden="true">
81
- <RsIcon name="file" :size="16" />
159
+ <span
160
+ class="rs-upload__file-icon"
161
+ :class="`rs-upload__file-icon--${fileIcon(file)}`"
162
+ aria-hidden="true"
163
+ >
164
+ <RsIcon :name="fileIcon(file)" :size="16" />
82
165
  </span>
83
166
  <span class="rs-upload__file-meta">
84
167
  <span class="rs-upload__file-name">{{ file.name }}</span>
85
168
  <span class="rs-upload__file-size">{{ formatFileSize(file.size) }}</span>
86
169
  </span>
87
170
  <button
171
+ v-if="showDownload"
172
+ type="button"
173
+ class="rs-upload__file-action"
174
+ :aria-label="t('upload.download')"
175
+ :title="t('upload.download')"
176
+ @click.stop="onDownload(file, index)"
177
+ >
178
+ <RsIcon name="download" :size="14" />
179
+ </button>
180
+ <button
181
+ v-if="!disabled"
88
182
  type="button"
89
183
  class="rs-upload__file-remove"
90
184
  :aria-label="t('common.remove')"
@@ -123,7 +217,8 @@ function removeFile(index: number): void {
123
217
  box-shadow var(--rs-transition-fast);
124
218
  }
125
219
 
126
- .rs-upload__dropzone:hover:not(.rs-upload__dropzone--disabled) {
220
+ .rs-upload__dropzone:hover:not(.rs-upload__dropzone--disabled),
221
+ .rs-upload__dropzone--dragging:not(.rs-upload__dropzone--disabled) {
127
222
  border-color: color-mix(in srgb, var(--rs-primary) 55%, var(--rs-border));
128
223
  background: color-mix(in srgb, var(--rs-primary) 6%, var(--rs-surface));
129
224
  }
@@ -163,7 +258,8 @@ function removeFile(index: number): void {
163
258
  box-shadow var(--rs-transition-fast);
164
259
  }
165
260
 
166
- .rs-upload__dropzone:hover:not(.rs-upload__dropzone--disabled) .rs-upload__icon {
261
+ .rs-upload__dropzone:hover:not(.rs-upload__dropzone--disabled) .rs-upload__icon,
262
+ .rs-upload__dropzone--dragging:not(.rs-upload__dropzone--disabled) .rs-upload__icon {
167
263
  transform: translateY(-1px);
168
264
  box-shadow: var(--rs-shadow);
169
265
  }
@@ -226,6 +322,40 @@ function removeFile(index: number): void {
226
322
  color: var(--rs-muted);
227
323
  }
228
324
 
325
+ .rs-upload__file-icon--file-image {
326
+ color: var(--rs-info, #2563eb);
327
+ }
328
+
329
+ .rs-upload__file-icon--file-code,
330
+ .rs-upload__file-icon--file-braces,
331
+ .rs-upload__file-icon--file-terminal {
332
+ color: var(--rs-primary);
333
+ }
334
+
335
+ .rs-upload__file-icon--file-key,
336
+ .rs-upload__file-icon--key-round,
337
+ .rs-upload__file-icon--file-lock {
338
+ color: var(--rs-warning, #d97706);
339
+ }
340
+
341
+ .rs-upload__file-icon--file-archive {
342
+ color: var(--rs-secondary, #7c3aed);
343
+ }
344
+
345
+ .rs-upload__file-icon--file-spreadsheet {
346
+ color: var(--rs-success, #16a34a);
347
+ }
348
+
349
+ .rs-upload__file-icon--file-type,
350
+ .rs-upload__file-icon--file-text {
351
+ color: var(--rs-danger, #dc2626);
352
+ }
353
+
354
+ .rs-upload__file-icon--file-music,
355
+ .rs-upload__file-icon--file-video-camera {
356
+ color: var(--rs-info, #2563eb);
357
+ }
358
+
229
359
  .rs-upload__file-meta {
230
360
  display: grid;
231
361
  gap: 0.125rem;
@@ -249,6 +379,7 @@ function removeFile(index: number): void {
249
379
  line-height: var(--rs-line-height-tight);
250
380
  }
251
381
 
382
+ .rs-upload__file-action,
252
383
  .rs-upload__file-remove {
253
384
  display: inline-flex;
254
385
  flex-shrink: 0;
@@ -267,11 +398,13 @@ function removeFile(index: number): void {
267
398
  color var(--rs-transition-fast);
268
399
  }
269
400
 
401
+ .rs-upload__file-action:hover,
270
402
  .rs-upload__file-remove:hover {
271
403
  background: var(--rs-item-hover);
272
404
  color: var(--rs-text);
273
405
  }
274
406
 
407
+ .rs-upload__file-action:focus-visible,
275
408
  .rs-upload__file-remove:focus-visible {
276
409
  outline: none;
277
410
  box-shadow: 0 0 0 var(--rs-focus-ring-width, 2px) var(--rs-focus-ring);
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ import { defineComponent, type PropType, type VNodeChild } from 'vue'
3
+
4
+ /**
5
+ * 将 VNodeChild(含自定义错误渲染结果)挂到模板中。
6
+ * Form errorRender / 字段自定义渲染共用。
7
+ */
8
+ export default defineComponent({
9
+ name: 'RsVNodeHost',
10
+ props: {
11
+ content: {
12
+ type: null as unknown as PropType<VNodeChild>,
13
+ required: true,
14
+ },
15
+ },
16
+ setup(props) {
17
+ return () => props.content
18
+ },
19
+ })
20
+ </script>
@@ -0,0 +1,39 @@
1
+ /**
2
+ * RsButton 外观变体
3
+ * - primary / default / ghost / danger:常规控件按钮
4
+ * - link:行内链接态(可带下划线)
5
+ * - text:工具栏/表格操作等 quaternary 文字按钮(无底无边,靠 tone 着色)
6
+ */
7
+ export type RsButtonVariant =
8
+ | 'primary'
9
+ | 'default'
10
+ | 'secondary'
11
+ | 'ghost'
12
+ | 'danger'
13
+ | 'link'
14
+ | 'text'
15
+
16
+ /**
17
+ * RsButton 语义色调
18
+ * 主要用于 text / ghost / link:只改文字与图标色,不改变填充形态。
19
+ * 对 primary / default / danger 等填充变体将被忽略。
20
+ */
21
+ export type RsButtonTone =
22
+ | 'neutral'
23
+ | 'primary'
24
+ | 'danger'
25
+ | 'success'
26
+ | 'warning'
27
+ | 'info'
28
+
29
+ /** 填充类变体:已自带语义色,不再叠加 tone */
30
+ export function isRsButtonFilledVariant(variant: RsButtonVariant): boolean {
31
+ const resolved = variant === 'secondary' ? 'default' : variant
32
+ return resolved === 'primary' || resolved === 'default' || resolved === 'danger'
33
+ }
34
+
35
+ /** text / ghost / link 可叠加 tone */
36
+ export function supportsRsButtonTone(variant: RsButtonVariant): boolean {
37
+ const resolved = variant === 'secondary' ? 'default' : variant
38
+ return resolved === 'text' || resolved === 'ghost' || resolved === 'link'
39
+ }
@@ -18,3 +18,9 @@ export interface RsContextMenuItem {
18
18
  /** 键盘快捷键提示(仅展示,不绑定事件),如 "⌘K"、"⌃C" */
19
19
  shortcut?: string
20
20
  }
21
+
22
+ /** 是否存在可操作项(排除纯分隔线),用于避免空菜单气泡 */
23
+ export function hasActionableContextMenuItems(items: RsContextMenuItem[] | undefined): boolean {
24
+ if (!items?.length) return false
25
+ return items.some((item) => !item.separator)
26
+ }