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
|
@@ -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:
|
|
123
|
-
column-gap: 0.
|
|
124
|
-
row-gap: 0.
|
|
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.
|
|
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.
|
|
135
|
-
var(--rs-toast-accent) 0.
|
|
136
|
-
transparent calc(100% - 0.
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
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
|
-
//
|
|
134
|
-
if (!props.virtual ||
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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-
|
|
974
|
+
background: var(--rs-tree-row-hover);
|
|
958
975
|
}
|
|
959
976
|
|
|
960
977
|
.rs-tree__row--selected {
|
|
961
|
-
background:
|
|
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-
|
|
967
|
-
font-weight:
|
|
983
|
+
color: var(--rs-tree-selected-fg);
|
|
984
|
+
font-weight: inherit;
|
|
968
985
|
}
|
|
969
986
|
|
|
970
987
|
.rs-tree__row--focused {
|
|
971
|
-
background:
|
|
972
|
-
box-shadow: inset 0 0 0 1px
|
|
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:
|
|
977
|
-
box-shadow: inset 0 0 0 1px var(--rs-
|
|
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-
|
|
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-
|
|
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-
|
|
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 {
|
|
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
|
|
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
|
|
35
|
-
|
|
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="{
|
|
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
|
-
|
|
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
|
|
81
|
-
|
|
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
|
+
}
|