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
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, nextTick, ref } from 'vue'
2
+ import { computed, nextTick, ref, useSlots } from 'vue'
3
3
  import {
4
4
  DropdownMenuContent,
5
5
  DropdownMenuItem,
@@ -13,12 +13,20 @@ import {
13
13
  } from './reka'
14
14
  import { useRsI18n } from '../composables/useRsI18n'
15
15
  import { useRsTabsNav } from '../composables/useRsTabsNav'
16
+ import RsContextMenu from './RsContextMenu.vue'
16
17
  import RsIcon from './RsIcon.vue'
17
18
  import {
19
+ buildTabContextMenuItems,
20
+ getNextTabAfterBatchClose,
18
21
  getNextTabAfterClose,
19
22
  isTabClosable,
23
+ isTabFixed,
20
24
  isTabRenamable,
25
+ resolveTabsToClose,
21
26
  type RsTabItem,
27
+ type RsTabsCloseAction,
28
+ type RsTabsContentGap,
29
+ type RsTabsJustify,
22
30
  type RsTabsOverflow,
23
31
  type RsTabsSize,
24
32
  type RsTabsVariant,
@@ -31,39 +39,77 @@ const props = withDefaults(
31
39
  items: RsTabItem[]
32
40
  size?: RsTabsSize
33
41
  variant?: RsTabsVariant
42
+ /**
43
+ * 仅渲染导航栏,不渲染内容面板;内容由业务自行根据 v-model 切换。
44
+ * 需要内容插槽且无外框时,用 borderless 替代(保留具名内容插槽)。
45
+ */
34
46
  panelless?: boolean
47
+ /**
48
+ * 无外框:去掉 body/nav 边框与底色,内容区默认无内边距。
49
+ * 适合嵌在 Card 内的登录 Tab 等,业务无需 :deep 清边框。
50
+ */
51
+ borderless?: boolean
52
+ /**
53
+ * 标题栏与内容区间距(borderless 下控制 panel 上边距)。
54
+ * 也可用样式覆盖 --rs-tabs-content-gap。
55
+ */
56
+ contentGap?: RsTabsContentGap
57
+ /**
58
+ * 标签栏对齐方式;stretch/evenly 用于等分铺满,避免业务 :deep 改 list。
59
+ */
60
+ justify?: RsTabsJustify
35
61
  closable?: boolean
36
62
  addable?: boolean
37
63
  maxCount?: number
38
64
  /** 双击标签重命名 */
39
65
  renamable?: boolean
40
- /** 拖拽调整标签顺序 */
66
+ /** 按住标签拖动排序(整项可拖,不展示手柄) */
41
67
  draggable?: boolean
42
68
  /** 标签过多:scroll 横向滚动 · dropdown 折叠到「更多」 */
43
69
  overflow?: RsTabsOverflow | false
70
+ /**
71
+ * 顶栏导航右键菜单:关闭 / 关闭其他 / 关闭左侧 / 关闭右侧 / 关闭全部
72
+ *(对齐 GTabs / Chrome / VS Code 多页签)
73
+ */
74
+ contextMenu?: boolean
75
+ /**
76
+ * 切换前校验(对齐 Ant Design Tabs beforeLeave)。
77
+ * 返回 false 时阻止切换;支持异步。
78
+ */
79
+ beforeLeave?: (to: string, from: string) => boolean | void | Promise<boolean | void>
44
80
  }>(),
45
81
  {
46
82
  size: 'md',
47
83
  variant: 'line',
48
84
  panelless: false,
85
+ borderless: false,
86
+ contentGap: 'none',
87
+ justify: 'start',
49
88
  closable: false,
50
89
  addable: false,
51
90
  renamable: false,
52
91
  draggable: false,
53
92
  overflow: false,
93
+ contextMenu: false,
54
94
  },
55
95
  )
56
96
 
57
97
  const emit = defineEmits<{
58
98
  close: [value: string]
99
+ /** 批量关闭(右键菜单 / 中键以外的批量动作),values 为待移除的标签 value */
100
+ closeBatch: [values: string[], action: RsTabsCloseAction, anchor?: string]
59
101
  add: []
60
102
  rename: [value: string, label: string]
61
103
  reorder: [dragValue: string, dropValue: string]
104
+ /** 右键菜单选中动作 */
105
+ contextMenu: [action: RsTabsCloseAction, value: string]
62
106
  }>()
63
107
 
108
+ const slots = useSlots()
64
109
  const { t } = useRsI18n()
65
110
  const itemsRef = computed(() => props.items)
66
111
  const overflowMode = computed(() => props.overflow)
112
+ const hasExtra = computed(() => Boolean(slots.extra))
67
113
  const canAdd = computed(() => {
68
114
  if (!props.addable) return false
69
115
  if (props.maxCount == null) return true
@@ -73,6 +119,7 @@ const canAdd = computed(() => {
73
119
  const navRef = ref<HTMLElement | null>(null)
74
120
  const measureRef = ref<HTMLElement | null>(null)
75
121
  const overflowWrapRef = ref<HTMLElement | null>(null)
122
+ const extraRef = ref<HTMLElement | null>(null)
76
123
 
77
124
  const {
78
125
  navViewportRef,
@@ -96,6 +143,7 @@ const {
96
143
  navRef,
97
144
  measureRef,
98
145
  overflowRef: overflowWrapRef,
146
+ extraRef,
99
147
  addButtonWidth: 36,
100
148
  })
101
149
 
@@ -104,6 +152,7 @@ const renameDraft = ref('')
104
152
  const dragValue = ref<string | null>(null)
105
153
  const dragOverValue = ref<string | null>(null)
106
154
  let renameInputEl: HTMLInputElement | null = null
155
+ let leaveLock = false
107
156
 
108
157
  function setRenameInputRef(el: unknown, item: RsTabItem) {
109
158
  if (!(el instanceof HTMLInputElement) || renamingValue.value !== item.value) return
@@ -127,17 +176,78 @@ const moreLabel = computed(() => {
127
176
  return t('tabs.more')
128
177
  })
129
178
 
179
+ const contextMenuLabels = computed(() => ({
180
+ close: t('tabs.closeTab'),
181
+ closeOthers: t('tabs.closeOthers'),
182
+ closeLeft: t('tabs.closeLeft'),
183
+ closeRight: t('tabs.closeRight'),
184
+ closeAll: t('tabs.closeAll'),
185
+ }))
186
+
130
187
  function closeAriaLabel(item: RsTabItem): string {
131
188
  return t('tabs.close', 'Close {label}').replace('{label}', item.label)
132
189
  }
133
190
 
191
+ function contextItemsFor(item: RsTabItem) {
192
+ return buildTabContextMenuItems(
193
+ props.items,
194
+ item,
195
+ props.closable,
196
+ contextMenuLabels.value,
197
+ )
198
+ }
199
+
200
+ function labelTitle(item: RsTabItem): string | undefined {
201
+ return item.label.length > 12 ? item.label : undefined
202
+ }
203
+
204
+ async function onSelectTab(next: string | number): Promise<void> {
205
+ const value = String(next)
206
+ if (value === model.value || leaveLock) return
207
+ if (props.beforeLeave) {
208
+ leaveLock = true
209
+ try {
210
+ const ok = await props.beforeLeave(value, model.value)
211
+ if (ok === false) return
212
+ } finally {
213
+ leaveLock = false
214
+ }
215
+ }
216
+ model.value = value
217
+ }
218
+
219
+ function applyCloseValues(
220
+ values: string[],
221
+ action: RsTabsCloseAction,
222
+ anchor?: string,
223
+ ): void {
224
+ if (!values.length) return
225
+ if (action === 'close' && values.length === 1) {
226
+ const closed = values[0]!
227
+ const next = getNextTabAfterClose(props.items, closed, model.value)
228
+ if (next && model.value === closed) model.value = next
229
+ emit('close', closed)
230
+ return
231
+ }
232
+
233
+ const remaining = props.items.filter((item) => !values.includes(item.value))
234
+ const next = getNextTabAfterBatchClose(remaining, model.value, anchor)
235
+ if (next !== undefined && next !== model.value) model.value = next
236
+ else if (next === undefined) model.value = ''
237
+ emit('closeBatch', values, action, anchor)
238
+ }
239
+
134
240
  function onCloseTab(item: RsTabItem): void {
135
241
  if (item.disabled || !isTabClosable(item, props.closable)) return
136
- const next = getNextTabAfterClose(props.items, item.value, model.value)
137
- if (next && model.value === item.value) {
138
- model.value = next
139
- }
140
- emit('close', item.value)
242
+ applyCloseValues([item.value], 'close', item.value)
243
+ }
244
+
245
+ function onContextSelect(actionKey: string, item: RsTabItem): void {
246
+ const action = actionKey as RsTabsCloseAction
247
+ if (!['close', 'others', 'left', 'right', 'all'].includes(action)) return
248
+ const values = resolveTabsToClose(props.items, action, item.value, props.closable)
249
+ emit('contextMenu', action, item.value)
250
+ applyCloseValues(values, action, item.value)
141
251
  }
142
252
 
143
253
  function onAddTab(): void {
@@ -146,7 +256,26 @@ function onAddTab(): void {
146
256
  }
147
257
 
148
258
  function onOverflowSelect(value: string): void {
149
- model.value = value
259
+ void onSelectTab(value)
260
+ }
261
+
262
+ /** 中键关闭(对齐浏览器 / VS Code 页签) */
263
+ function onTabAuxClick(item: RsTabItem, event: MouseEvent): void {
264
+ if (event.button !== 1) return
265
+ event.preventDefault()
266
+ onCloseTab(item)
267
+ }
268
+
269
+ /** 纵向滚轮转为横向滚动(scroll 溢出模式) */
270
+ function onNavWheel(event: WheelEvent): void {
271
+ if (!useScrollOverflow.value) return
272
+ const viewport = navViewportRef.value
273
+ if (!viewport) return
274
+ if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return
275
+ if (viewport.scrollWidth <= viewport.clientWidth) return
276
+ event.preventDefault()
277
+ viewport.scrollLeft += event.deltaY
278
+ onNavScroll()
150
279
  }
151
280
 
152
281
  async function startRename(item: RsTabItem): Promise<void> {
@@ -170,8 +299,24 @@ function commitRename(item: RsTabItem): void {
170
299
  cancelRename()
171
300
  }
172
301
 
302
+ function canDragItem(item: RsTabItem): boolean {
303
+ return props.draggable && !item.disabled && !isTabFixed(item)
304
+ }
305
+
173
306
  function onDragStart(item: RsTabItem, event: DragEvent): void {
174
- if (!props.draggable || item.disabled) return
307
+ if (!canDragItem(item)) {
308
+ event.preventDefault()
309
+ return
310
+ }
311
+ // 关闭 / 重命名控件上不启动拖拽,避免误触
312
+ const target = event.target
313
+ if (
314
+ target instanceof Element &&
315
+ target.closest('.rs-tabs__close, .rs-tabs__rename-input')
316
+ ) {
317
+ event.preventDefault()
318
+ return
319
+ }
175
320
  dragValue.value = item.value
176
321
  event.dataTransfer?.setData('text/plain', item.value)
177
322
  if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'
@@ -179,6 +324,7 @@ function onDragStart(item: RsTabItem, event: DragEvent): void {
179
324
 
180
325
  function onDragOver(item: RsTabItem, event: DragEvent): void {
181
326
  if (!props.draggable || !dragValue.value || dragValue.value === item.value) return
327
+ if (isTabFixed(item)) return
182
328
  event.preventDefault()
183
329
  dragOverValue.value = item.value
184
330
  }
@@ -190,7 +336,7 @@ function onDragLeave(item: RsTabItem): void {
190
336
  function onDrop(item: RsTabItem, event: DragEvent): void {
191
337
  if (!props.draggable || !dragValue.value) return
192
338
  event.preventDefault()
193
- if (dragValue.value !== item.value) {
339
+ if (dragValue.value !== item.value && !isTabFixed(item)) {
194
340
  emit('reorder', dragValue.value, item.value)
195
341
  }
196
342
  dragValue.value = null
@@ -206,18 +352,24 @@ function onDragEnd(): void {
206
352
 
207
353
  <template>
208
354
  <TabsRoot
209
- v-model="model"
355
+ :model-value="model"
210
356
  class="rs-tabs"
211
357
  :class="[
212
358
  `rs-tabs--${variant}`,
213
359
  `rs-tabs--${size}`,
360
+ `rs-tabs--justify-${justify}`,
361
+ `rs-tabs--content-gap-${contentGap}`,
214
362
  {
215
363
  'rs-tabs--panelless': panelless,
364
+ 'rs-tabs--borderless': borderless,
216
365
  'rs-tabs--scrollable': useScrollOverflow,
217
366
  'rs-tabs--dropdown-overflow': useDropdownOverflow,
367
+ 'rs-tabs--has-extra': hasExtra,
368
+ 'rs-tabs--draggable': draggable,
218
369
  },
219
370
  ]"
220
371
  :aria-label="t('tabs.label')"
372
+ @update:model-value="onSelectTab"
221
373
  >
222
374
  <div :class="panelless ? 'rs-tabs__shell' : 'rs-tabs__body'">
223
375
  <div
@@ -258,72 +410,86 @@ function onDragEnd(): void {
258
410
  ref="navViewportRef"
259
411
  class="rs-tabs__nav-viewport"
260
412
  @scroll="onNavScroll"
413
+ @wheel="onNavWheel"
261
414
  >
262
415
  <TabsList class="rs-tabs__list">
263
- <TabsTrigger
416
+ <RsContextMenu
264
417
  v-for="item in tabBarItems"
265
418
  :key="item.value"
266
- :value="item.value"
267
- :disabled="item.disabled"
268
- class="rs-tabs__trigger"
269
- :class="{
270
- 'rs-tabs__trigger--dragging': dragValue === item.value,
271
- 'rs-tabs__trigger--drag-over': dragOverValue === item.value,
272
- }"
273
- :data-tab-value="item.value"
274
- @dragover="onDragOver(item, $event)"
275
- @dragleave="onDragLeave(item)"
276
- @drop="onDrop(item, $event)"
419
+ :items="contextItemsFor(item)"
420
+ :disabled="!contextMenu"
421
+ @select="(key) => onContextSelect(key, item)"
277
422
  >
278
- <span
279
- v-if="draggable"
280
- class="rs-tabs__drag-handle"
281
- :aria-label="t('tabs.dragHandle')"
282
- draggable="true"
423
+ <TabsTrigger
424
+ :value="item.value"
425
+ :disabled="item.disabled"
426
+ class="rs-tabs__trigger"
427
+ :class="{
428
+ 'rs-tabs__trigger--dragging': dragValue === item.value,
429
+ 'rs-tabs__trigger--drag-over': dragOverValue === item.value,
430
+ 'rs-tabs__trigger--fixed': isTabFixed(item),
431
+ 'rs-tabs__trigger--movable': canDragItem(item),
432
+ }"
433
+ :data-tab-value="item.value"
434
+ :data-fixed="isTabFixed(item) ? 'true' : undefined"
435
+ :draggable="canDragItem(item) || undefined"
283
436
  @dragstart="onDragStart(item, $event)"
437
+ @dragover="onDragOver(item, $event)"
438
+ @dragleave="onDragLeave(item)"
439
+ @drop="onDrop(item, $event)"
284
440
  @dragend="onDragEnd"
285
- @mousedown.stop
286
- @click.stop
287
- >
288
- <RsIcon name="grip-vertical" :size="12" />
289
- </span>
290
- <RsIcon v-if="item.icon" :name="item.icon" :size="14" class="rs-tabs__icon" />
291
- <input
292
- v-if="renamingValue === item.value"
293
- :ref="(el) => setRenameInputRef(el, item)"
294
- v-model="renameDraft"
295
- class="rs-tabs__rename-input"
296
- :aria-label="t('tabs.rename')"
297
- :placeholder="t('tabs.renamePlaceholder')"
298
- @keydown.enter.prevent="commitRename(item)"
299
- @keydown.escape.prevent="cancelRename"
300
- @blur="commitRename(item)"
301
- @mousedown.stop
302
- @click.stop
303
- @dblclick.stop
304
- />
305
- <span
306
- v-else
307
- class="rs-tabs__label"
308
- @dblclick.stop="startRename(item)"
309
- >
310
- {{ item.label }}
311
- </span>
312
- <span v-if="item.badge != null && item.badge !== ''" class="rs-tabs__badge">
313
- {{ item.badge }}
314
- </span>
315
- <button
316
- v-if="isTabClosable(item, closable)"
317
- type="button"
318
- class="rs-tabs__close"
319
- :aria-label="closeAriaLabel(item)"
320
- :disabled="item.disabled"
321
- @mousedown.stop.prevent
322
- @click.stop="onCloseTab(item)"
441
+ @auxclick="onTabAuxClick(item, $event)"
323
442
  >
324
- <RsIcon name="x" :size="12" />
325
- </button>
326
- </TabsTrigger>
443
+ <RsIcon
444
+ v-if="isTabFixed(item)"
445
+ name="pin"
446
+ :size="12"
447
+ class="rs-tabs__pin"
448
+ />
449
+ <RsIcon v-if="item.icon" :name="item.icon" :size="14" class="rs-tabs__icon" />
450
+ <input
451
+ v-if="renamingValue === item.value"
452
+ :ref="(el) => setRenameInputRef(el, item)"
453
+ v-model="renameDraft"
454
+ class="rs-tabs__rename-input"
455
+ :aria-label="t('tabs.rename')"
456
+ :placeholder="t('tabs.renamePlaceholder')"
457
+ draggable="false"
458
+ @keydown.enter.prevent="commitRename(item)"
459
+ @keydown.escape.prevent="cancelRename"
460
+ @blur="commitRename(item)"
461
+ @mousedown.stop
462
+ @click.stop
463
+ @dblclick.stop
464
+ @dragstart.stop.prevent
465
+ />
466
+ <span
467
+ v-else
468
+ class="rs-tabs__label"
469
+ :title="labelTitle(item)"
470
+ @dblclick.stop="startRename(item)"
471
+ >
472
+ {{ item.label }}
473
+ </span>
474
+ <span v-if="item.badge != null && item.badge !== ''" class="rs-tabs__badge">
475
+ {{ item.badge }}
476
+ </span>
477
+ <button
478
+ v-if="isTabClosable(item, closable)"
479
+ type="button"
480
+ class="rs-tabs__close"
481
+ :aria-label="closeAriaLabel(item)"
482
+ :disabled="item.disabled"
483
+ draggable="false"
484
+ @mousedown.stop.prevent
485
+ @click.stop="onCloseTab(item)"
486
+ @auxclick.stop.prevent
487
+ @dragstart.stop.prevent
488
+ >
489
+ <RsIcon name="x" :size="12" />
490
+ </button>
491
+ </TabsTrigger>
492
+ </RsContextMenu>
327
493
  </TabsList>
328
494
  </div>
329
495
 
@@ -379,6 +545,10 @@ function onDragEnd(): void {
379
545
  >
380
546
  <RsIcon name="chevron-right" :size="14" />
381
547
  </button>
548
+
549
+ <div v-if="hasExtra" ref="extraRef" class="rs-tabs__extra">
550
+ <slot name="extra" />
551
+ </div>
382
552
  </div>
383
553
 
384
554
  <template v-if="!panelless">
@@ -402,6 +572,14 @@ function onDragEnd(): void {
402
572
  display: flex;
403
573
  flex-direction: column;
404
574
  width: 100%;
575
+ /* 标题与内容间距;可由 contentGap 或样式覆盖 */
576
+ --rs-tabs-content-gap: 0px;
577
+ }
578
+
579
+ /* 非激活面板必须彻底隐藏,避免多页内容叠层 */
580
+ .rs-tabs__panel[data-state='inactive'],
581
+ .rs-tabs__panel[hidden] {
582
+ display: none !important;
405
583
  }
406
584
 
407
585
  .rs-tabs__shell,
@@ -416,6 +594,7 @@ function onDragEnd(): void {
416
594
  display: flex;
417
595
  align-items: stretch;
418
596
  min-width: 0;
597
+ width: 100%;
419
598
  }
420
599
 
421
600
  .rs-tabs__nav-viewport {
@@ -499,17 +678,25 @@ function onDragEnd(): void {
499
678
  box-shadow: inset 0 -2px 0 0 var(--rs-primary);
500
679
  }
501
680
 
502
- .rs-tabs__drag-handle {
503
- display: inline-flex;
504
- align-items: center;
505
- color: var(--rs-placeholder);
681
+ .rs-tabs__trigger--fixed {
682
+ font-weight: 500;
683
+ }
684
+
685
+ .rs-tabs--draggable .rs-tabs__trigger--movable {
506
686
  cursor: grab;
507
687
  }
508
688
 
509
- .rs-tabs__drag-handle:active {
689
+ .rs-tabs--draggable .rs-tabs__trigger--movable:active,
690
+ .rs-tabs--draggable .rs-tabs__trigger--dragging {
510
691
  cursor: grabbing;
511
692
  }
512
693
 
694
+ .rs-tabs__pin {
695
+ flex: 0 0 auto;
696
+ color: var(--rs-placeholder);
697
+ opacity: 0.85;
698
+ }
699
+
513
700
  .rs-tabs__icon {
514
701
  flex: 0 0 auto;
515
702
  color: currentColor;
@@ -517,6 +704,9 @@ function onDragEnd(): void {
517
704
 
518
705
  .rs-tabs__label {
519
706
  min-width: 0;
707
+ max-width: 12rem;
708
+ overflow: hidden;
709
+ text-overflow: ellipsis;
520
710
  }
521
711
 
522
712
  .rs-tabs__rename-input {
@@ -567,7 +757,9 @@ function onDragEnd(): void {
567
757
  background: transparent;
568
758
  color: var(--rs-muted);
569
759
  cursor: pointer;
570
- opacity: 0.72;
760
+ /* 非激活:默认隐藏,悬停标签时再显示;占位保留避免宽度跳动 */
761
+ opacity: 0;
762
+ pointer-events: none;
571
763
  transition:
572
764
  opacity var(--rs-transition-fast),
573
765
  background var(--rs-transition-fast),
@@ -575,8 +767,10 @@ function onDragEnd(): void {
575
767
  }
576
768
 
577
769
  .rs-tabs__trigger:hover .rs-tabs__close,
770
+ .rs-tabs__trigger:focus-within .rs-tabs__close,
578
771
  .rs-tabs__trigger[data-state='active'] .rs-tabs__close {
579
772
  opacity: 1;
773
+ pointer-events: auto;
580
774
  }
581
775
 
582
776
  .rs-tabs__close:hover:not(:disabled) {
@@ -586,14 +780,40 @@ function onDragEnd(): void {
586
780
 
587
781
  .rs-tabs__close:focus-visible {
588
782
  outline: none;
783
+ opacity: 1;
784
+ pointer-events: auto;
589
785
  box-shadow: 0 0 0 var(--rs-focus-ring-width, 2px) var(--rs-focus-ring);
590
786
  }
591
787
 
592
788
  .rs-tabs__close:disabled {
593
- opacity: 0.38;
594
789
  cursor: not-allowed;
595
790
  }
596
791
 
792
+ .rs-tabs__trigger:hover .rs-tabs__close:disabled,
793
+ .rs-tabs__trigger:focus-within .rs-tabs__close:disabled,
794
+ .rs-tabs__trigger[data-state='active'] .rs-tabs__close:disabled {
795
+ opacity: 0.38;
796
+ pointer-events: none;
797
+ }
798
+
799
+ /* 触摸设备无悬停:可关闭标签始终显示关闭按钮 */
800
+ @media (hover: none) {
801
+ .rs-tabs__close {
802
+ opacity: 0.85;
803
+ pointer-events: auto;
804
+ }
805
+
806
+ .rs-tabs__trigger[data-state='active'] .rs-tabs__close {
807
+ opacity: 1;
808
+ }
809
+
810
+ .rs-tabs__close:disabled,
811
+ .rs-tabs__trigger[data-state='active'] .rs-tabs__close:disabled {
812
+ opacity: 0.38;
813
+ pointer-events: none;
814
+ }
815
+ }
816
+
597
817
  .rs-tabs__add,
598
818
  .rs-tabs__scroll-btn {
599
819
  display: inline-flex;
@@ -624,6 +844,17 @@ function onDragEnd(): void {
624
844
  background: var(--rs-surface-hover);
625
845
  }
626
846
 
847
+ .rs-tabs__extra {
848
+ display: inline-flex;
849
+ flex: 0 0 auto;
850
+ align-items: center;
851
+ align-self: stretch;
852
+ gap: var(--rs-space-xs);
853
+ margin-inline-start: auto;
854
+ padding-inline: var(--rs-space-sm);
855
+ border-inline-start: 1px solid var(--rs-border-subtle);
856
+ }
857
+
627
858
  .rs-tabs__overflow-wrap {
628
859
  display: flex;
629
860
  flex: 0 0 auto;
@@ -734,6 +965,7 @@ function onDragEnd(): void {
734
965
  .rs-tabs--dropdown-overflow.rs-tabs--line .rs-tabs__nav {
735
966
  align-items: stretch;
736
967
  padding-inline: 0;
968
+ border-bottom: 1px solid var(--rs-border-subtle);
737
969
  }
738
970
 
739
971
  .rs-tabs--dropdown-overflow.rs-tabs--line .rs-tabs__list {
@@ -761,10 +993,6 @@ function onDragEnd(): void {
761
993
  background: var(--rs-primary);
762
994
  }
763
995
 
764
- .rs-tabs--dropdown-overflow.rs-tabs--line .rs-tabs__nav {
765
- border-bottom: 1px solid var(--rs-border-subtle);
766
- }
767
-
768
996
  .rs-tabs--line.rs-tabs--sm .rs-tabs__more {
769
997
  min-height: var(--rs-control-height-sm);
770
998
  padding: 0 var(--rs-space-sm);
@@ -789,12 +1017,14 @@ function onDragEnd(): void {
789
1017
  }
790
1018
 
791
1019
  .rs-tabs--line .rs-tabs__add,
792
- .rs-tabs--line .rs-tabs__scroll-btn {
1020
+ .rs-tabs--line .rs-tabs__scroll-btn,
1021
+ .rs-tabs--line .rs-tabs__extra {
793
1022
  min-height: var(--rs-control-height-md);
794
1023
  }
795
1024
 
796
1025
  .rs-tabs--line.rs-tabs--sm .rs-tabs__add,
797
- .rs-tabs--line.rs-tabs--sm .rs-tabs__scroll-btn {
1026
+ .rs-tabs--line.rs-tabs--sm .rs-tabs__scroll-btn,
1027
+ .rs-tabs--line.rs-tabs--sm .rs-tabs__extra {
798
1028
  min-height: var(--rs-control-height-sm);
799
1029
  }
800
1030
 
@@ -829,8 +1059,17 @@ function onDragEnd(): void {
829
1059
  font-size: var(--rs-font-size-xs);
830
1060
  }
831
1061
 
832
- .rs-tabs--line.rs-tabs--panelless .rs-tabs__list {
1062
+ .rs-tabs--line.rs-tabs--panelless .rs-tabs__nav {
833
1063
  border-bottom: 1px solid var(--rs-border);
1064
+ background: var(--rs-surface);
1065
+ }
1066
+
1067
+ .rs-tabs--line.rs-tabs--panelless .rs-tabs__list {
1068
+ border-bottom: none;
1069
+ }
1070
+
1071
+ .rs-tabs--line.rs-tabs--panelless.rs-tabs--has-extra .rs-tabs__extra {
1072
+ background: transparent;
834
1073
  }
835
1074
 
836
1075
  /* —— segmented —— */
@@ -948,4 +1187,97 @@ function onDragEnd(): void {
948
1187
  .rs-tabs--card.rs-tabs--panelless .rs-tabs__list {
949
1188
  padding-bottom: var(--rs-space-sm);
950
1189
  }
1190
+
1191
+ .rs-tabs--card .rs-tabs__extra {
1192
+ align-self: center;
1193
+ margin-bottom: 0;
1194
+ border-inline-start: none;
1195
+ }
1196
+
1197
+ /* —— contentGap:标题与内容间距(borderless 默认 0,业务可设 sm/md/lg/xl) —— */
1198
+ .rs-tabs--content-gap-sm {
1199
+ --rs-tabs-content-gap: var(--rs-space-sm);
1200
+ }
1201
+
1202
+ .rs-tabs--content-gap-md {
1203
+ --rs-tabs-content-gap: var(--rs-space-md);
1204
+ }
1205
+
1206
+ .rs-tabs--content-gap-lg {
1207
+ --rs-tabs-content-gap: var(--rs-space-lg);
1208
+ }
1209
+
1210
+ .rs-tabs--content-gap-xl {
1211
+ --rs-tabs-content-gap: var(--rs-space-xl);
1212
+ }
1213
+
1214
+ /* —— borderless:无外框 / 无导航底色(业务用具名插槽,无需 :deep) —— */
1215
+ .rs-tabs--borderless.rs-tabs--line .rs-tabs__body,
1216
+ .rs-tabs--borderless.rs-tabs--segmented .rs-tabs__body,
1217
+ .rs-tabs--borderless.rs-tabs--card .rs-tabs__body {
1218
+ border: none;
1219
+ border-radius: 0;
1220
+ background: transparent;
1221
+ box-shadow: none;
1222
+ overflow: visible;
1223
+ }
1224
+
1225
+ .rs-tabs--borderless .rs-tabs__shell,
1226
+ .rs-tabs--borderless .rs-tabs__nav,
1227
+ .rs-tabs--borderless .rs-tabs__list,
1228
+ .rs-tabs--borderless .rs-tabs__nav-viewport {
1229
+ border: none;
1230
+ border-bottom: none;
1231
+ background: transparent;
1232
+ box-shadow: none;
1233
+ }
1234
+
1235
+ .rs-tabs--borderless.rs-tabs--line .rs-tabs__list,
1236
+ .rs-tabs--borderless.rs-tabs--line.rs-tabs--panelless .rs-tabs__nav,
1237
+ .rs-tabs--borderless.rs-tabs--dropdown-overflow.rs-tabs--line .rs-tabs__nav {
1238
+ border-bottom: none;
1239
+ background: transparent;
1240
+ padding-inline: 0;
1241
+ }
1242
+
1243
+ .rs-tabs--borderless .rs-tabs__panel,
1244
+ .rs-tabs--borderless .rs-tabs__panel-inner {
1245
+ padding: 0;
1246
+ border: none;
1247
+ background: transparent;
1248
+ box-shadow: none;
1249
+ }
1250
+
1251
+ .rs-tabs--borderless .rs-tabs__panel-inner {
1252
+ padding-top: var(--rs-tabs-content-gap);
1253
+ }
1254
+
1255
+ .rs-tabs--borderless.rs-tabs--card .rs-tabs__panel-inner {
1256
+ border-top: none;
1257
+ }
1258
+
1259
+ /* —— justify:标签栏对齐(stretch/evenly 铺满,免业务 :deep) —— */
1260
+ .rs-tabs--justify-center .rs-tabs__list {
1261
+ justify-content: center;
1262
+ }
1263
+
1264
+ .rs-tabs--justify-evenly .rs-tabs__list {
1265
+ width: 100%;
1266
+ justify-content: space-evenly;
1267
+ }
1268
+
1269
+ .rs-tabs--justify-stretch .rs-tabs__list {
1270
+ display: flex;
1271
+ width: 100%;
1272
+ justify-content: stretch;
1273
+ }
1274
+
1275
+ .rs-tabs--justify-stretch .rs-tabs__trigger {
1276
+ flex: 1 1 0;
1277
+ justify-content: center;
1278
+ }
1279
+
1280
+ .rs-tabs--justify-stretch.rs-tabs--line .rs-tabs__trigger[data-state='active']::after {
1281
+ inset-inline: 18%;
1282
+ }
951
1283
  </style>