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
@@ -189,15 +189,87 @@ export interface RsTableColumn<T extends RsTableRowData = Record<string, unknown
189
189
  * 纯文本格式化请优先用 `formatter`。
190
190
  */
191
191
  render?: (row: T, index: number) => RsTableCellRenderResult
192
+ /**
193
+ * 列汇总(footer / 分组小计)。配合表格 `summaryMode`:
194
+ * - client:对当前 viewRows 做本地聚合
195
+ * - server:展示 `summaryData[column.key]`
196
+ */
197
+ summary?: import('./table/table-summary-utils').RsTableColumnSummary<T>
192
198
  }
193
199
 
194
200
  export type RsTableSelectAllState = 'checked' | 'indeterminate' | 'unchecked'
195
201
 
196
202
  export type RsTableRowEntry<T extends RsTableRowData = Record<string, unknown>> =
197
- | { type: 'row'; row: T; rowIndex: number }
203
+ | {
204
+ type: 'row'
205
+ row: T
206
+ rowIndex: number
207
+ /** 树表深度(根为 0);非树表为 undefined */
208
+ depth?: number
209
+ /** 树表:是否有可展开子节点 */
210
+ hasChildren?: boolean
211
+ /** 树表:父行 key;根为 null */
212
+ parentKey?: string | null
213
+ /** 树表:展平时确定的稳定行 key(优先于重新 resolve) */
214
+ treeKey?: string
215
+ }
198
216
  | { type: 'group'; key: string; label: string }
199
217
  | { type: 'expand'; row: T; rowIndex: number; rowKey: string }
200
218
 
219
+ /**
220
+ * 树形表格配置。
221
+ * 与 detail 展开(`expandable`)互斥:树表在数据列内缩进展开,不插入 `#expand` 明细行。
222
+ */
223
+ export interface RsTableTreeConfig<T extends RsTableRowData = RsTableRowData> {
224
+ /** 子节点字段名,默认 `children` */
225
+ childrenField?: string
226
+ /** 叶子标记字段名,默认 `isLeaf`;为 true 时不可展开 */
227
+ isLeafField?: string
228
+ /**
229
+ * 展示缩进与展开按钮的列 key;
230
+ * 缺省为第一列(`columns[0].key`)。
231
+ */
232
+ expandColumnKey?: string
233
+ /**
234
+ * 是否自动将展开列设为 `fixed: 'left'`(避免横向滚动 / 列虚拟时丢失展开钮)。
235
+ * @default true
236
+ */
237
+ fixExpandColumn?: boolean
238
+ /** 每一层缩进像素;缺省按表格 size(sm/md/lg → 16/20/24) */
239
+ indent?: number
240
+ /**
241
+ * 仅在首次有数据时展开全部可展开节点(非受控)。
242
+ * 若同时提供非空 `defaultExpandedRowKeys`,则以后者为准且不再全开。
243
+ */
244
+ defaultExpandAll?: boolean
245
+ /**
246
+ * 勾选是否父子独立。
247
+ * - `true`(默认):与平面表一致,只切换当前行
248
+ * - `false`:父子联动,父级可呈半选
249
+ */
250
+ checkStrictly?: boolean
251
+ /**
252
+ * 懒加载:无子节点且非叶子时仍显示展开按钮;
253
+ * 首次展开时调用 `loadData`。
254
+ */
255
+ lazy?: boolean
256
+ /**
257
+ * 懒加载回调。可直接改 `row[childrenField]`,或返回子节点数组由表格写入。
258
+ * 使用 bivarianceHack:具体行类型的 treeConfig 可赋给更宽泛的 treeConfig prop(如 RsGrid)。
259
+ */
260
+ loadData?: {
261
+ bivarianceHack(row: T, key: string): void | T[] | Promise<void | T[]>
262
+ }['bivarianceHack']
263
+ }
264
+
265
+ /** 树表节点索引(供级联勾选 / 半选) */
266
+ export interface RsTableTreeNodeIndex {
267
+ parentKey: string | null
268
+ childrenKeys: string[]
269
+ }
270
+
271
+ export type RsTableTreeCheckState = 'checked' | 'indeterminate' | 'unchecked'
272
+
201
273
  export const TABLE_ROW_HEIGHT = {
202
274
  sm: 33,
203
275
  md: 41,
@@ -523,6 +595,383 @@ export function toggleExpandedRowKeys(keys: readonly string[], key: string): str
523
595
  return [...next]
524
596
  }
525
597
 
598
+ /** 读取树表子节点数组(非数组视为无子节点) */
599
+ export function getTableTreeChildren<T extends RsTableRowData>(
600
+ row: T,
601
+ childrenField = 'children',
602
+ ): T[] {
603
+ const value = (row as Record<string, unknown>)[childrenField]
604
+ return Array.isArray(value) ? (value as T[]) : []
605
+ }
606
+
607
+ /** 是否为叶子(显式 isLeaf 优先) */
608
+ export function getTableTreeIsLeaf<T extends RsTableRowData>(
609
+ row: T,
610
+ isLeafField = 'isLeaf',
611
+ ): boolean {
612
+ return Boolean((row as Record<string, unknown>)[isLeafField])
613
+ }
614
+
615
+ /** 是否可展开:非叶子且(有子节点,或懒加载) */
616
+ export function hasTableTreeChildren<T extends RsTableRowData>(
617
+ row: T,
618
+ options: { childrenField?: string; isLeafField?: string; lazy?: boolean } = {},
619
+ ): boolean {
620
+ const childrenField = options.childrenField ?? 'children'
621
+ const isLeafField = options.isLeafField ?? 'isLeaf'
622
+ if (getTableTreeIsLeaf(row, isLeafField)) return false
623
+ if (getTableTreeChildren(row, childrenField).length > 0) return true
624
+ return Boolean(options.lazy)
625
+ }
626
+
627
+ /**
628
+ * 解析树表行 key。
629
+ * 有 `rowKey` 时走 `resolveRowKey`;否则用路径回退(避免兄弟下标跨分支碰撞)。
630
+ * 树表强烈建议显式传 `rowKey`。
631
+ */
632
+ export function resolveTableTreeRowKey<T extends RsTableRowData>(
633
+ row: T,
634
+ index: number,
635
+ parentKey: string | null,
636
+ rowKey?: RsTableRowKey<T>,
637
+ ): string {
638
+ if (rowKey !== undefined && rowKey !== null && rowKey !== '') {
639
+ return resolveRowKey(row, index, rowKey)
640
+ }
641
+ return parentKey == null ? `tree:${index}` : `${parentKey}/${index}`
642
+ }
643
+
644
+ /** 是否配置了可用的稳定 rowKey(字段名或函数) */
645
+ export function hasStableTableTreeRowKey<T extends RsTableRowData>(
646
+ rowKey?: RsTableRowKey<T>,
647
+ ): boolean {
648
+ return rowKey !== undefined && rowKey !== null && rowKey !== ''
649
+ }
650
+
651
+ /** 收集所有可展开节点 key(用于 defaultExpandAll) */
652
+ export function collectTableTreeExpandableKeys<T extends RsTableRowData>(
653
+ rows: readonly T[],
654
+ options: {
655
+ childrenField?: string
656
+ isLeafField?: string
657
+ lazy?: boolean
658
+ rowKey?: RsTableRowKey<T>
659
+ } = {},
660
+ ): string[] {
661
+ const childrenField = options.childrenField ?? 'children'
662
+ const keys: string[] = []
663
+ const walk = (items: readonly T[], parentKey: string | null) => {
664
+ items.forEach((row, index) => {
665
+ const key = resolveTableTreeRowKey(row, index, parentKey, options.rowKey)
666
+ if (hasTableTreeChildren(row, options)) {
667
+ keys.push(key)
668
+ walk(getTableTreeChildren(row, childrenField), key)
669
+ }
670
+ })
671
+ }
672
+ walk(rows, null)
673
+ return keys
674
+ }
675
+
676
+ /** 构建整棵树的 parent/children key 索引(级联勾选) */
677
+ export function buildTableTreeNodeIndex<T extends RsTableRowData>(
678
+ rows: readonly T[],
679
+ options: {
680
+ childrenField?: string
681
+ isLeafField?: string
682
+ lazy?: boolean
683
+ rowKey?: RsTableRowKey<T>
684
+ } = {},
685
+ ): Map<string, RsTableTreeNodeIndex> {
686
+ const childrenField = options.childrenField ?? 'children'
687
+ const map = new Map<string, RsTableTreeNodeIndex>()
688
+ const walk = (items: readonly T[], parentKey: string | null) => {
689
+ for (let i = 0; i < items.length; i += 1) {
690
+ const row = items[i] as T
691
+ const key = resolveTableTreeRowKey(row, i, parentKey, options.rowKey)
692
+ const children = getTableTreeChildren(row, childrenField)
693
+ const childrenKeys = hasTableTreeChildren(row, options)
694
+ ? children.map((child, childIndex) =>
695
+ resolveTableTreeRowKey(child, childIndex, key, options.rowKey),
696
+ )
697
+ : []
698
+ map.set(key, { parentKey, childrenKeys })
699
+ if (children.length > 0) walk(children, key)
700
+ }
701
+ }
702
+ walk(rows, null)
703
+ return map
704
+ }
705
+
706
+ export function collectTableTreeDescendantKeys(
707
+ key: string,
708
+ index: ReadonlyMap<string, RsTableTreeNodeIndex>,
709
+ ): string[] {
710
+ const entry = index.get(key)
711
+ if (!entry) return []
712
+ const result: string[] = []
713
+ for (const childKey of entry.childrenKeys) {
714
+ result.push(childKey, ...collectTableTreeDescendantKeys(childKey, index))
715
+ }
716
+ return result
717
+ }
718
+
719
+ export function resolveTableTreeCheckState(
720
+ key: string,
721
+ checkedKeys: ReadonlySet<string>,
722
+ index: ReadonlyMap<string, RsTableTreeNodeIndex>,
723
+ checkStrictly: boolean,
724
+ ): RsTableTreeCheckState {
725
+ if (checkStrictly) return checkedKeys.has(key) ? 'checked' : 'unchecked'
726
+ if (checkedKeys.size === 0) return 'unchecked'
727
+
728
+ const entry = index.get(key)
729
+ if (!entry || entry.childrenKeys.length === 0) {
730
+ return checkedKeys.has(key) ? 'checked' : 'unchecked'
731
+ }
732
+
733
+ const childStates = entry.childrenKeys.map((childKey) =>
734
+ resolveTableTreeCheckState(childKey, checkedKeys, index, checkStrictly),
735
+ )
736
+
737
+ if (childStates.every((state) => state === 'checked')) return 'checked'
738
+ if (childStates.some((state) => state === 'checked' || state === 'indeterminate')) {
739
+ return 'indeterminate'
740
+ }
741
+ return 'unchecked'
742
+ }
743
+
744
+ export function collectTableTreeHalfCheckedKeys(
745
+ index: ReadonlyMap<string, RsTableTreeNodeIndex>,
746
+ checkedKeys: ReadonlySet<string>,
747
+ checkStrictly: boolean,
748
+ ): string[] {
749
+ if (checkStrictly || checkedKeys.size === 0) return []
750
+ const half: string[] = []
751
+ for (const key of index.keys()) {
752
+ if (resolveTableTreeCheckState(key, checkedKeys, index, checkStrictly) === 'indeterminate') {
753
+ half.push(key)
754
+ }
755
+ }
756
+ return half
757
+ }
758
+
759
+ /** 切换树表勾选;`checkStrictly=false` 时父子联动 */
760
+ export function toggleTableTreeCheck(
761
+ key: string,
762
+ checkedKeys: ReadonlySet<string>,
763
+ index: ReadonlyMap<string, RsTableTreeNodeIndex>,
764
+ checkStrictly: boolean,
765
+ ): string[] {
766
+ const next = new Set(checkedKeys)
767
+ const current = resolveTableTreeCheckState(key, next, index, checkStrictly)
768
+ const shouldCheck = current !== 'checked'
769
+
770
+ if (checkStrictly) {
771
+ if (shouldCheck) next.add(key)
772
+ else next.delete(key)
773
+ return [...next]
774
+ }
775
+
776
+ for (const id of [key, ...collectTableTreeDescendantKeys(key, index)]) {
777
+ if (shouldCheck) next.add(id)
778
+ else next.delete(id)
779
+ }
780
+
781
+ let parentKey = index.get(key)?.parentKey ?? null
782
+ while (parentKey) {
783
+ const parent = index.get(parentKey)
784
+ if (!parent) break
785
+ const allChecked = parent.childrenKeys.every(
786
+ (childKey) => resolveTableTreeCheckState(childKey, next, index, false) === 'checked',
787
+ )
788
+ if (allChecked) next.add(parentKey)
789
+ else next.delete(parentKey)
790
+ parentKey = parent.parentKey
791
+ }
792
+
793
+ return [...next]
794
+ }
795
+
796
+ /**
797
+ * 树过滤:匹配节点或其后代匹配时保留该节点,并裁剪子树。
798
+ * 与 RsTree `filterTreeNodes` 语义一致(保留祖先路径)。
799
+ */
800
+ export function filterTableTreeRows<T extends RsTableRowData>(
801
+ rows: readonly T[],
802
+ query: string,
803
+ columns: readonly RsTableColumn<T>[],
804
+ options: {
805
+ childrenField?: string
806
+ filterKeys?: string[]
807
+ columnFilters?: Record<string, string>
808
+ } = {},
809
+ ): T[] {
810
+ const childrenField = options.childrenField ?? 'children'
811
+ const trimmed = query.trim()
812
+ const columnFilters = options.columnFilters ?? {}
813
+ const hasColumnFilters = Object.values(columnFilters).some((q) => q.trim())
814
+ if (!trimmed && !hasColumnFilters) return rows as T[]
815
+
816
+ const matchesSelf = (row: T): boolean => {
817
+ let ok = true
818
+ if (trimmed) {
819
+ const filtered = filterTableRows([row], trimmed, columns, options.filterKeys)
820
+ ok = filtered.length > 0
821
+ }
822
+ if (ok && hasColumnFilters) {
823
+ const filtered = filterTableRowsByColumnFilters([row], columns, columnFilters)
824
+ ok = filtered.length > 0
825
+ }
826
+ return ok
827
+ }
828
+
829
+ const walk = (items: readonly T[]): T[] => {
830
+ const result: T[] = []
831
+ for (const row of items) {
832
+ const children = getTableTreeChildren(row, childrenField)
833
+ const nextChildren = children.length ? walk(children) : []
834
+ if (matchesSelf(row) || nextChildren.length > 0) {
835
+ result.push(
836
+ nextChildren.length || children.length
837
+ ? ({ ...row, [childrenField]: nextChildren } as T)
838
+ : row,
839
+ )
840
+ }
841
+ }
842
+ return result
843
+ }
844
+ return walk(rows)
845
+ }
846
+
847
+ /** 树排序:仅对同级节点排序,递归处理子树 */
848
+ export function sortTableTreeRows<T extends RsTableRowData>(
849
+ rows: readonly T[],
850
+ columns: readonly RsTableColumn<T>[],
851
+ options: {
852
+ childrenField?: string
853
+ sort?: RsTableSortState | null
854
+ sorts?: readonly RsTableSortState[]
855
+ multiSort?: boolean
856
+ },
857
+ ): T[] {
858
+ const childrenField = options.childrenField ?? 'children'
859
+ let sorted: T[]
860
+ if (options.multiSort && options.sorts?.length) {
861
+ sorted = sortTableRowsMulti(rows, columns, options.sorts)
862
+ } else if (options.sort) {
863
+ sorted = sortTableRows(rows, columns, options.sort)
864
+ } else {
865
+ sorted = rows as T[]
866
+ }
867
+ return sorted.map((row) => {
868
+ const children = getTableTreeChildren(row, childrenField)
869
+ if (!children.length) return row
870
+ return {
871
+ ...row,
872
+ [childrenField]: sortTableTreeRows(children, columns, options),
873
+ } as T
874
+ })
875
+ }
876
+
877
+ /**
878
+ * 按展开 key 展平可见树行为表格 row entries(不含 detail expand 行)。
879
+ */
880
+ export function flattenVisibleTableTreeEntries<T extends RsTableRowData>(
881
+ rows: readonly T[],
882
+ expandedKeys: ReadonlySet<string>,
883
+ options: {
884
+ childrenField?: string
885
+ isLeafField?: string
886
+ lazy?: boolean
887
+ rowKey?: RsTableRowKey<T>
888
+ } = {},
889
+ ): Extract<RsTableRowEntry<T>, { type: 'row' }>[] {
890
+ const childrenField = options.childrenField ?? 'children'
891
+ const result: Extract<RsTableRowEntry<T>, { type: 'row' }>[] = []
892
+
893
+ const walk = (items: readonly T[], depth: number, parentKey: string | null) => {
894
+ items.forEach((row, index) => {
895
+ const key = resolveTableTreeRowKey(row, index, parentKey, options.rowKey)
896
+ const hasChildren = hasTableTreeChildren(row, options)
897
+ const rowIndex = result.length
898
+ result.push({
899
+ type: 'row',
900
+ row,
901
+ rowIndex,
902
+ depth,
903
+ hasChildren,
904
+ parentKey,
905
+ treeKey: key,
906
+ })
907
+ if (hasChildren && expandedKeys.has(key)) {
908
+ walk(getTableTreeChildren(row, childrenField), depth + 1, key)
909
+ }
910
+ })
911
+ }
912
+
913
+ walk(rows, 0, null)
914
+ return result
915
+ }
916
+
917
+ /**
918
+ * 树表入口构建:过滤 → 同级排序 → 按展开状态展平。
919
+ * 不支持与 `groupBy` / detail `expandable` 同时使用。
920
+ */
921
+ export function buildTableTreeEntries<T extends RsTableRowData>(
922
+ rows: readonly T[],
923
+ columns: readonly RsTableColumn<T>[],
924
+ expandedKeys: ReadonlySet<string>,
925
+ options: {
926
+ tree: RsTableTreeConfig<T>
927
+ rowKey?: RsTableRowKey<T>
928
+ sort?: RsTableSortState | null
929
+ sorts?: readonly RsTableSortState[]
930
+ multiSort?: boolean
931
+ filterText?: string
932
+ filterKeys?: string[]
933
+ columnFilters?: Record<string, string>
934
+ remoteSort?: boolean
935
+ },
936
+ ): RsTableRowEntry<T>[] {
937
+ const childrenField = options.tree.childrenField ?? 'children'
938
+ let processed: readonly T[] = rows
939
+
940
+ const filterText = options.filterText ?? ''
941
+ const columnFilters = options.columnFilters ?? {}
942
+ if (filterText.trim() || Object.values(columnFilters).some((q) => q.trim())) {
943
+ processed = filterTableTreeRows(processed, filterText, columns, {
944
+ childrenField,
945
+ filterKeys: options.filterKeys,
946
+ columnFilters,
947
+ })
948
+ }
949
+
950
+ if (!options.remoteSort && (options.sort || (options.multiSort && options.sorts?.length))) {
951
+ processed = sortTableTreeRows(processed, columns, {
952
+ childrenField,
953
+ sort: options.sort,
954
+ sorts: options.sorts,
955
+ multiSort: options.multiSort,
956
+ })
957
+ }
958
+
959
+ return flattenVisibleTableTreeEntries(processed, expandedKeys, {
960
+ childrenField,
961
+ isLeafField: options.tree.isLeafField,
962
+ lazy: options.tree.lazy,
963
+ rowKey: options.rowKey,
964
+ })
965
+ }
966
+
967
+ /** 按表格 size 解析树缩进默认值 */
968
+ export function resolveTableTreeIndent(size: RsTableSize = 'md', custom?: number): number {
969
+ if (custom !== undefined) return custom
970
+ if (size === 'sm') return 16
971
+ if (size === 'lg') return 24
972
+ return 20
973
+ }
974
+
526
975
  export function resolveTableSize(compact: boolean, size: RsTableSize = 'md'): RsTableSize {
527
976
  if (compact) return 'sm'
528
977
  return size
@@ -583,7 +1032,7 @@ export function resolveLeadingColumnWidth(options: {
583
1032
  if (options.expandable) width += 40
584
1033
  if (options.selectable) width += 40
585
1034
  if (options.showEditGutter) width += options.gutterWidth ?? 32
586
- else if (options.showIndex) width += options.indexWidth ?? 56
1035
+ else if (options.showIndex) width += options.indexWidth ?? 48
587
1036
  return width
588
1037
  }
589
1038
 
@@ -843,6 +1292,7 @@ export function resolveEntryKey<T extends RsTableRowData>(
843
1292
  ): string {
844
1293
  if (entry.type === 'group') return `group:${entry.key}`
845
1294
  if (entry.type === 'expand') return `expand:${entry.rowKey}`
1295
+ if (entry.treeKey) return entry.treeKey
846
1296
  return resolveRowKey(entry.row, entry.rowIndex, rowKey)
847
1297
  }
848
1298
 
@@ -1,3 +1,5 @@
1
+ import type { RsContextMenuItem } from './context-menu-utils'
2
+
1
3
  /** 选项卡项定义 */
2
4
  export interface RsTabItem {
3
5
  value: string
@@ -5,8 +7,10 @@ export interface RsTabItem {
5
7
  icon?: string
6
8
  disabled?: boolean
7
9
  badge?: string | number
8
- /** 是否可关闭;未设置时继承 RsTabs 的 closable */
10
+ /** 是否可关闭;未设置时继承 RsTabs 的 closable;fixed 为 true 时强制不可关 */
9
11
  closable?: boolean
12
+ /** 固定/常驻标签(对齐 GTabs fixed / 浏览器固定标签),不可关闭、不可被批量关闭 */
13
+ fixed?: boolean
10
14
  /** 是否可重命名;未设置时继承 RsTabs 的 renamable */
11
15
  renamable?: boolean
12
16
  }
@@ -17,11 +21,32 @@ export type RsTabsSize = 'sm' | 'md'
17
21
  /** line 下划线 · segmented 胶囊 · card 卡片(Ant editable-card) */
18
22
  export type RsTabsVariant = 'line' | 'segmented' | 'card'
19
23
 
24
+ /**
25
+ * 标签栏对齐:start 左齐 · center 居中 · evenly 均分空隙 · stretch 项等宽铺满。
26
+ * 用于登录等场景,避免业务侧 :deep 改 list/trigger。
27
+ */
28
+ export type RsTabsJustify = 'start' | 'center' | 'evenly' | 'stretch'
29
+
30
+ /**
31
+ * 标题栏与内容区间距(主要用于 borderless)。
32
+ * none=0 · sm=8px · md=12px · lg=16px · xl=24px;也可用 CSS 变量 --rs-tabs-content-gap 覆盖。
33
+ */
34
+ export type RsTabsContentGap = 'none' | 'sm' | 'md' | 'lg' | 'xl'
35
+
20
36
  /** 标签过多时的处理方式 */
21
37
  export type RsTabsOverflow = 'scroll' | 'dropdown'
22
38
 
39
+ /** 批量关闭动作(顶栏导航右键菜单) */
40
+ export type RsTabsCloseAction = 'close' | 'others' | 'left' | 'right' | 'all'
41
+
42
+ /** 判断是否为固定标签 */
43
+ export function isTabFixed(item: RsTabItem): boolean {
44
+ return item.fixed === true
45
+ }
46
+
23
47
  /** 判断单项是否展示关闭按钮 */
24
48
  export function isTabClosable(item: RsTabItem, globalClosable = false): boolean {
49
+ if (isTabFixed(item)) return false
25
50
  if (item.closable !== undefined) return item.closable
26
51
  return globalClosable
27
52
  }
@@ -46,7 +71,22 @@ export function getNextTabAfterClose(
46
71
  return remaining[index]?.value ?? remaining[index - 1]?.value ?? remaining[0]?.value
47
72
  }
48
73
 
49
- /** 将 dragValue 对应的项移动到 dropValue 之前 */
74
+ /**
75
+ * 批量关闭后解析应保留的激活项。
76
+ * 若当前激活项仍在剩余列表中则保持,否则取锚点(若仍在)或首项。
77
+ */
78
+ export function getNextTabAfterBatchClose(
79
+ remaining: RsTabItem[],
80
+ activeValue: string,
81
+ anchorValue?: string,
82
+ ): string | undefined {
83
+ if (!remaining.length) return undefined
84
+ if (remaining.some((item) => item.value === activeValue)) return activeValue
85
+ if (anchorValue && remaining.some((item) => item.value === anchorValue)) return anchorValue
86
+ return remaining[0]?.value
87
+ }
88
+
89
+ /** 将 dragValue 对应的项移动到 dropValue 之前(fixed 项不可作为拖拽源或落点) */
50
90
  export function reorderTabItems(
51
91
  items: RsTabItem[],
52
92
  dragValue: string,
@@ -55,6 +95,9 @@ export function reorderTabItems(
55
95
  const from = items.findIndex((item) => item.value === dragValue)
56
96
  const to = items.findIndex((item) => item.value === dropValue)
57
97
  if (from < 0 || to < 0 || from === to) return items
98
+ const dragItem = items[from]
99
+ const dropItem = items[to]
100
+ if (!dragItem || !dropItem || isTabFixed(dragItem) || isTabFixed(dropItem)) return items
58
101
  const next = [...items]
59
102
  const [moved] = next.splice(from, 1)
60
103
  if (!moved) return items
@@ -62,6 +105,103 @@ export function reorderTabItems(
62
105
  return next
63
106
  }
64
107
 
108
+ /**
109
+ * 按动作解析待关闭的 value 列表(跳过 fixed / 不可关闭项)。
110
+ */
111
+ export function resolveTabsToClose(
112
+ items: RsTabItem[],
113
+ action: RsTabsCloseAction,
114
+ anchorValue: string,
115
+ globalClosable = false,
116
+ ): string[] {
117
+ const index = items.findIndex((item) => item.value === anchorValue)
118
+ if (index < 0 && action !== 'all') return []
119
+
120
+ const canClose = (item: RsTabItem) => isTabClosable(item, globalClosable)
121
+
122
+ switch (action) {
123
+ case 'close': {
124
+ const item = items[index]
125
+ return item && canClose(item) ? [item.value] : []
126
+ }
127
+ case 'others':
128
+ return items
129
+ .filter((item) => item.value !== anchorValue && canClose(item))
130
+ .map((item) => item.value)
131
+ case 'left':
132
+ return items.filter((item, i) => i < index && canClose(item)).map((item) => item.value)
133
+ case 'right':
134
+ return items.filter((item, i) => i > index && canClose(item)).map((item) => item.value)
135
+ case 'all':
136
+ return items.filter((item) => canClose(item)).map((item) => item.value)
137
+ default:
138
+ return []
139
+ }
140
+ }
141
+
142
+ export interface BuildTabContextMenuLabels {
143
+ close: string
144
+ closeOthers: string
145
+ closeLeft: string
146
+ closeRight: string
147
+ closeAll: string
148
+ }
149
+
150
+ /**
151
+ * 构建顶栏导航右键菜单项(关闭 / 关闭其他 / 左 / 右 / 全部)。
152
+ */
153
+ export function buildTabContextMenuItems(
154
+ items: RsTabItem[],
155
+ anchor: RsTabItem,
156
+ globalClosable: boolean,
157
+ labels: BuildTabContextMenuLabels,
158
+ ): RsContextMenuItem[] {
159
+ const index = items.findIndex((item) => item.value === anchor.value)
160
+ if (index < 0) return []
161
+
162
+ const canCloseAnchor = isTabClosable(anchor, globalClosable)
163
+ const others = resolveTabsToClose(items, 'others', anchor.value, globalClosable)
164
+ const left = resolveTabsToClose(items, 'left', anchor.value, globalClosable)
165
+ const right = resolveTabsToClose(items, 'right', anchor.value, globalClosable)
166
+ const all = resolveTabsToClose(items, 'all', anchor.value, globalClosable)
167
+
168
+ return [
169
+ {
170
+ key: 'close',
171
+ label: labels.close,
172
+ icon: 'x',
173
+ disabled: !canCloseAnchor,
174
+ },
175
+ { key: 'sep-1', label: '', separator: true },
176
+ {
177
+ key: 'others',
178
+ label: labels.closeOthers,
179
+ icon: 'copy-x',
180
+ disabled: others.length === 0,
181
+ },
182
+ {
183
+ key: 'left',
184
+ label: labels.closeLeft,
185
+ icon: 'arrow-left-to-line',
186
+ disabled: left.length === 0,
187
+ },
188
+ {
189
+ key: 'right',
190
+ label: labels.closeRight,
191
+ icon: 'arrow-right-to-line',
192
+ disabled: right.length === 0,
193
+ },
194
+ { key: 'sep-2', label: '', separator: true },
195
+ {
196
+ key: 'all',
197
+ label: labels.closeAll,
198
+ icon: 'trash-2',
199
+ danger: true,
200
+ disabled: all.length === 0,
201
+ },
202
+ ]
203
+ }
204
+
65
205
  export interface ResolveVisibleTabOptions {
66
206
  /** dropdown 模式下为 false:激活项仅在「更多」展示,不占标签栏 */
67
207
  keepActiveVisible?: boolean
@@ -1,4 +1,4 @@
1
- import type { ITheme, Terminal } from 'xterm'
1
+ import type { ITheme, Terminal } from '@xterm/xterm'
2
2
  import { readDocumentTheme } from './code-editor-utils'
3
3
 
4
4
  export type RsTerminalThemeMode = 'auto' | 'light' | 'dark'
@@ -1,4 +1,4 @@
1
- import type { Terminal } from 'xterm'
1
+ import type { Terminal } from '@xterm/xterm'
2
2
 
3
3
  export type RsTerminalWheelScrollModifier = 'none' | 'shift'
4
4