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
@@ -0,0 +1,165 @@
1
+ /**
2
+ * 表格 → 图表系列纯函数(库无关)。
3
+ *
4
+ * 产出结构可直接喂给 ECharts / Chart.js 等:
5
+ * - pie / 普通 bar:points[{ name, value }]
6
+ * - 分类轴 bar:categories + values
7
+ *
8
+ * 不引入任何图表依赖,避免拖重内核。
9
+ */
10
+
11
+ import type { RsTableRowData } from '../table-utils'
12
+
13
+ /** 图表意图类型(渲染层自行映射到具体库) */
14
+ export type RsTableChartKind = 'bar' | 'pie' | 'line'
15
+
16
+ /** 单点:饼图扇区 / 柱条 */
17
+ export interface RsTableChartPoint {
18
+ /** 分类名(展示) */
19
+ name: string
20
+ /** 聚合值 */
21
+ value: number
22
+ /** 原始分类 key(与 name 通常相同,供下钻) */
23
+ key: string
24
+ /** 该分类下的行数(便于 tooltip) */
25
+ count: number
26
+ }
27
+
28
+ /** 一组图表系列 */
29
+ export interface RsTableChartSeries {
30
+ id: string
31
+ kind: RsTableChartKind
32
+ points: RsTableChartPoint[]
33
+ /** 分类轴(与 points 顺序一致) */
34
+ categories: string[]
35
+ /** 数值轴(与 points 顺序一致) */
36
+ values: number[]
37
+ }
38
+
39
+ export type RsTableChartValueAgg = 'count' | 'sum' | 'avg' | 'min' | 'max'
40
+
41
+ /**
42
+ * 图表系列定义。
43
+ * - categoryField:分组字段
44
+ * - valueField + valueAgg:度量;仅 count 时可省略 valueField
45
+ */
46
+ export interface RsTableChartSeriesDef {
47
+ id: string
48
+ kind: RsTableChartKind
49
+ categoryField: string
50
+ valueField?: string
51
+ valueAgg?: RsTableChartValueAgg
52
+ /** 最多保留前 N 类,其余合并为 otherLabel;0/不传表示不截断 */
53
+ topN?: number
54
+ otherLabel?: string
55
+ /** 空分类展示名 */
56
+ emptyCategoryLabel?: string
57
+ }
58
+
59
+ function readField(row: RsTableRowData, field: string): unknown {
60
+ return (row as Record<string, unknown>)[field]
61
+ }
62
+
63
+ function toNumber(value: unknown): number | null {
64
+ if (typeof value === 'number' && Number.isFinite(value)) return value
65
+ if (typeof value === 'string' && value.trim() !== '') {
66
+ const n = Number(value)
67
+ return Number.isFinite(n) ? n : null
68
+ }
69
+ return null
70
+ }
71
+
72
+ function categoryKey(raw: unknown, emptyLabel: string): string {
73
+ if (raw == null || raw === '') return emptyLabel
74
+ return String(raw)
75
+ }
76
+
77
+ interface Bucket {
78
+ key: string
79
+ name: string
80
+ count: number
81
+ nums: number[]
82
+ }
83
+
84
+ function aggregateBucket(bucket: Bucket, agg: RsTableChartValueAgg): number {
85
+ if (agg === 'count') return bucket.count
86
+ if (!bucket.nums.length) return 0
87
+ if (agg === 'sum') return bucket.nums.reduce((a, b) => a + b, 0)
88
+ if (agg === 'avg') return bucket.nums.reduce((a, b) => a + b, 0) / bucket.nums.length
89
+ if (agg === 'min') return Math.min(...bucket.nums)
90
+ if (agg === 'max') return Math.max(...bucket.nums)
91
+ return 0
92
+ }
93
+
94
+ /**
95
+ * 按分类字段对行做聚合,生成库无关图表系列。
96
+ *
97
+ * @param rows 通常为 SelectionSource.sourceRows(选中优先)
98
+ * @param def 系列定义
99
+ */
100
+ export function buildTableChartSeries<T extends RsTableRowData>(
101
+ rows: readonly T[],
102
+ def: RsTableChartSeriesDef,
103
+ ): RsTableChartSeries {
104
+ const emptyLabel = def.emptyCategoryLabel ?? '(空)'
105
+ const agg = def.valueAgg ?? (def.valueField ? 'sum' : 'count')
106
+ const map = new Map<string, Bucket>()
107
+
108
+ for (let i = 0; i < rows.length; i += 1) {
109
+ const row = rows[i] as T
110
+ const key = categoryKey(readField(row, def.categoryField), emptyLabel)
111
+ let bucket = map.get(key)
112
+ if (!bucket) {
113
+ bucket = { key, name: key, count: 0, nums: [] }
114
+ map.set(key, bucket)
115
+ }
116
+ bucket.count += 1
117
+ if (def.valueField && agg !== 'count') {
118
+ const n = toNumber(readField(row, def.valueField))
119
+ if (n != null) bucket.nums.push(n)
120
+ }
121
+ }
122
+
123
+ let points: RsTableChartPoint[] = [...map.values()].map((bucket) => ({
124
+ key: bucket.key,
125
+ name: bucket.name,
126
+ value: aggregateBucket(bucket, agg),
127
+ count: bucket.count,
128
+ }))
129
+
130
+ // 按 value 降序,便于 topN / 饼图可读性
131
+ points.sort((a, b) => b.value - a.value || a.name.localeCompare(b.name))
132
+
133
+ const topN = def.topN ?? 0
134
+ if (topN > 0 && points.length > topN) {
135
+ const head = points.slice(0, topN)
136
+ const rest = points.slice(topN)
137
+ const otherValue = rest.reduce((s, p) => s + p.value, 0)
138
+ const otherCount = rest.reduce((s, p) => s + p.count, 0)
139
+ head.push({
140
+ key: '__other__',
141
+ name: def.otherLabel ?? '其他',
142
+ value: otherValue,
143
+ count: otherCount,
144
+ })
145
+ points = head
146
+ }
147
+
148
+ return {
149
+ id: def.id,
150
+ kind: def.kind,
151
+ points,
152
+ categories: points.map((p) => p.name),
153
+ values: points.map((p) => p.value),
154
+ }
155
+ }
156
+
157
+ /**
158
+ * 批量构建多组系列(同一 sourceRows,多套分组/度量)。
159
+ */
160
+ export function buildTableChartSeriesList<T extends RsTableRowData>(
161
+ rows: readonly T[],
162
+ defs: readonly RsTableChartSeriesDef[],
163
+ ): RsTableChartSeries[] {
164
+ return defs.map((def) => buildTableChartSeries(rows, def))
165
+ }
@@ -0,0 +1,284 @@
1
+ /**
2
+ * RsTable 特性插件协议。
3
+ *
4
+ * 目标:新能力以 feature 注册,避免继续向 RsTable.vue 堆分支。
5
+ *
6
+ * 已迁出 composable 的内置能力(行为真源):
7
+ * - virtual → useRsTableVirtual + useRsTableColumnVirtual + useRsTableScrollLayout
8
+ * - contextMenu → useRsTableContextMenu
9
+ * - summary → useRsTableSummary
10
+ * - sort/selection/filter/tree → useRsTableEngine / useRsTableColumns / useRsTableInteraction
11
+ * - edit → useTableEdit + useRsTableEditActions
12
+ * - analytics / chart → useRsTableSelectionSource + useRsTableChartBridge(表外)
13
+ *
14
+ * Feature.setup 用于生命周期登记与扩展点;重逻辑不在此重复执行。
15
+ *
16
+ * 视图分层(世界级演进):
17
+ * - Core / Shell / EditLayer = 行为真源
18
+ * - RsTableColGroup / Header / Body / BodyRow = 纯视图,不持业务状态
19
+ */
20
+
21
+ import type { RsContextMenuItem } from '../context-menu-utils'
22
+ import type { RsTableRowData } from '../table-utils'
23
+ import type { RsTableAnalyticsSnapshot } from '../../composables/useRsTableSelectionSource'
24
+ import type {
25
+ RsTableOverlayContribution,
26
+ RsTableToolbarItem,
27
+ } from './rs-table-feature-host'
28
+ import {
29
+ buildTableChartSeriesList,
30
+ type RsTableChartSeries,
31
+ type RsTableChartSeriesDef,
32
+ } from './table-chart-utils'
33
+
34
+ /**
35
+ * 表格特性插件上下文(只读查询 + 订阅 + 有限贡献点)。
36
+ * Feature 不得直接操作表格 DOM;图表/指标通过 snapshot / subscribe 接入。
37
+ */
38
+ export interface RsTableFeatureContext<T extends RsTableRowData = RsTableRowData> {
39
+ /** 当前筛选/排序后的业务行(Engine.viewRows) */
40
+ getViewRows: () => T[]
41
+ /** 当前选中行 */
42
+ getSelectedRows: () => T[]
43
+ /**
44
+ * 分析快照:含 sourceRows(选中优先)与 sourceMode。
45
+ * 柱状图/饼图等应消费 sourceRows,而不是直接绑原始 data。
46
+ */
47
+ getAnalyticsSnapshot: () => RsTableAnalyticsSnapshot<T>
48
+ /**
49
+ * 订阅分析快照变化(浅层:view/selected 引用或 sourceMode)。
50
+ * @returns teardown
51
+ */
52
+ subscribeAnalytics: (listener: (snap: RsTableAnalyticsSnapshot<T>) => void) => () => void
53
+ /**
54
+ * 向右键菜单贡献项(setup 时调用)。
55
+ * - 传入数组:静态追加
56
+ * - 传入函数:每次打开菜单时动态贡献(可 push 到 items)
57
+ */
58
+ contributeContextMenuItems?: (
59
+ contributor:
60
+ | RsContextMenuItem[]
61
+ | ((
62
+ items: RsContextMenuItem[],
63
+ menuCtx: { row: T | null; selectedRows: T[] },
64
+ ) => void),
65
+ ) => void
66
+ /**
67
+ * 向工具条贡献项(实例级;多表互不共享贡献袋)。
68
+ */
69
+ contributeToolbarItems?: (
70
+ contributor: RsTableToolbarItem[] | ((items: RsTableToolbarItem[]) => void),
71
+ ) => void
72
+ /**
73
+ * 向 overlay 旁路贡献挂载描述(图表壳等,不直接改表格 DOM)。
74
+ */
75
+ contributeOverlay?: (
76
+ contributor:
77
+ | RsTableOverlayContribution[]
78
+ | ((items: RsTableOverlayContribution[]) => void),
79
+ ) => void
80
+ }
81
+
82
+ /**
83
+ * 企业级表格特性插件接口。
84
+ *
85
+ * @property id 稳定标识,与 props 开关 / 遥测对齐
86
+ * @property setup 挂载时调用;可返回 teardown
87
+ */
88
+ export interface RsTableFeature<T extends RsTableRowData = RsTableRowData> {
89
+ id: string
90
+ setup(ctx: RsTableFeatureContext<T>): void | (() => void)
91
+ }
92
+
93
+ /**
94
+ * 内置特性 id(与 RsTable props 开关映射)。
95
+ * analytics 不进内置 props 开关:由业务通过 ChartBridge / Feature 外挂。
96
+ */
97
+ export type RsTableBuiltinFeatureId =
98
+ | 'selection'
99
+ | 'sort'
100
+ | 'filter'
101
+ | 'virtual'
102
+ | 'edit'
103
+ | 'tree'
104
+ | 'contextMenu'
105
+ | 'summary'
106
+
107
+ /** 已有独立 composable 承载的内置 id(feature 仅作登记) */
108
+ export const RS_TABLE_FEATURE_COMPOSABLE_MAP: Record<
109
+ RsTableBuiltinFeatureId,
110
+ string | null
111
+ > = {
112
+ selection: 'useRsTableEngine',
113
+ sort: 'useRsTableEngine',
114
+ filter: 'useRsTableEngine',
115
+ virtual: 'useRsTableVirtual+useRsTableColumnVirtual+useRsTableScrollLayout',
116
+ edit: 'useTableEdit+useRsTableEditActions',
117
+ tree: 'useRsTableEngine+useRsTableInteraction',
118
+ contextMenu: 'useRsTableContextMenu',
119
+ summary: 'useRsTableSummary',
120
+ }
121
+
122
+ /** 列布局/拖拽非 builtin feature id,但属内核横切能力 */
123
+ export const RS_TABLE_COLUMN_SHELL = {
124
+ layout: 'useRsTableColumnLayout',
125
+ resize: 'useRsTableColumnResize',
126
+ } as const
127
+
128
+ /** 表外分析/图表扩展入口(不进 RsTable 内核渲染) */
129
+ export const RS_TABLE_ANALYTICS_SHELL = {
130
+ selectionSource: 'useRsTableSelectionSource',
131
+ chartBridge: 'useRsTableChartBridge',
132
+ chartUtils: 'table-chart-utils',
133
+ } as const
134
+
135
+ /**
136
+ * 按 props 开关解析应启用的内置特性 id 列表。
137
+ */
138
+ export function resolveBuiltinTableFeatures(flags: {
139
+ selectable?: boolean
140
+ sortableColumns?: boolean
141
+ filterable?: boolean
142
+ virtual?: boolean | 'auto'
143
+ editable?: boolean
144
+ tree?: boolean
145
+ contextMenu?: boolean
146
+ summary?: boolean
147
+ }): RsTableBuiltinFeatureId[] {
148
+ const ids: RsTableBuiltinFeatureId[] = []
149
+ if (flags.selectable) ids.push('selection')
150
+ if (flags.sortableColumns !== false) ids.push('sort')
151
+ if (flags.filterable) ids.push('filter')
152
+ if (flags.virtual !== false) ids.push('virtual')
153
+ if (flags.editable) ids.push('edit')
154
+ if (flags.tree) ids.push('tree')
155
+ if (flags.contextMenu !== false) ids.push('contextMenu')
156
+ if (flags.summary) ids.push('summary')
157
+ return ids
158
+ }
159
+
160
+ /**
161
+ * 依次 setup 特性列表,并返回统一 teardown(逆序 dispose)。
162
+ */
163
+ export function setupTableFeatures<T extends RsTableRowData>(
164
+ features: RsTableFeature<T>[],
165
+ ctx: RsTableFeatureContext<T>,
166
+ ): () => void {
167
+ const teardowns: Array<() => void> = []
168
+ for (const feature of features) {
169
+ const dispose = feature.setup(ctx)
170
+ if (typeof dispose === 'function') teardowns.push(dispose)
171
+ }
172
+ return () => {
173
+ for (const dispose of teardowns.reverse()) dispose()
174
+ }
175
+ }
176
+
177
+ /**
178
+ * 创建已绑定 composable 真源的内置 feature。
179
+ */
180
+ export function createBuiltinTableFeatures<T extends RsTableRowData>(
181
+ ids: RsTableBuiltinFeatureId[],
182
+ ): RsTableFeature<T>[] {
183
+ return ids.map((id) => ({
184
+ id,
185
+ setup(ctx) {
186
+ if (import.meta.env.DEV && id === 'summary') {
187
+ void ctx.getAnalyticsSnapshot()
188
+ }
189
+ },
190
+ }))
191
+ }
192
+
193
+ /**
194
+ * 创建分析/图表旁路 feature:setup 时订阅 snapshot,交由业务 listener。
195
+ * 用于把「选中 → 图表」挂到统一生命周期,而不改 RsTable 模板。
196
+ */
197
+ export function createAnalyticsTableFeature<T extends RsTableRowData>(options: {
198
+ id?: string
199
+ onSnapshot: (snap: RsTableAnalyticsSnapshot<T>) => void
200
+ }): RsTableFeature<T> {
201
+ return {
202
+ id: options.id ?? 'analytics',
203
+ setup(ctx) {
204
+ return ctx.subscribeAnalytics(options.onSnapshot)
205
+ },
206
+ }
207
+ }
208
+
209
+ /**
210
+ * 创建右键菜单贡献 feature(静态项或动态函数)。
211
+ */
212
+ export function createContextMenuTableFeature<T extends RsTableRowData>(options: {
213
+ id?: string
214
+ items:
215
+ | RsContextMenuItem[]
216
+ | ((
217
+ items: RsContextMenuItem[],
218
+ menuCtx: { row: T | null; selectedRows: T[] },
219
+ ) => void)
220
+ }): RsTableFeature<T> {
221
+ return {
222
+ id: options.id ?? 'contextMenuExtra',
223
+ setup(ctx) {
224
+ ctx.contributeContextMenuItems?.(options.items)
225
+ },
226
+ }
227
+ }
228
+
229
+ /** 创建工具条贡献 feature */
230
+ export function createToolbarTableFeature(options: {
231
+ id?: string
232
+ items: RsTableToolbarItem[] | ((items: RsTableToolbarItem[]) => void)
233
+ }): RsTableFeature {
234
+ return {
235
+ id: options.id ?? 'toolbarExtra',
236
+ setup(ctx) {
237
+ ctx.contributeToolbarItems?.(options.items)
238
+ },
239
+ }
240
+ }
241
+
242
+ /** 创建 overlay 贡献 feature(图表旁路挂载点等) */
243
+ export function createOverlayTableFeature(options: {
244
+ id?: string
245
+ items:
246
+ | RsTableOverlayContribution[]
247
+ | ((items: RsTableOverlayContribution[]) => void)
248
+ }): RsTableFeature {
249
+ return {
250
+ id: options.id ?? 'overlayExtra',
251
+ setup(ctx) {
252
+ ctx.contributeOverlay?.(options.items)
253
+ },
254
+ }
255
+ }
256
+
257
+ /**
258
+ * 官方图表 Feature:订阅 analytics → 库无关 series,并登记 overlay 挂载点。
259
+ * 不捆绑 ECharts/Chart.js;业务用 onSeries 映射 option。
260
+ */
261
+ export function createChartSeriesTableFeature<T extends RsTableRowData>(options: {
262
+ id?: string
263
+ seriesDefs: RsTableChartSeriesDef[] | (() => RsTableChartSeriesDef[])
264
+ onSeries: (
265
+ series: RsTableChartSeries[],
266
+ snap: RsTableAnalyticsSnapshot<T>,
267
+ ) => void
268
+ /** overlay 登记;默认 key=chart / slot=chart */
269
+ overlay?: RsTableOverlayContribution
270
+ }): RsTableFeature<T> {
271
+ return {
272
+ id: options.id ?? 'chartSeries',
273
+ setup(ctx) {
274
+ ctx.contributeOverlay?.([
275
+ options.overlay ?? { key: 'chart', slot: 'chart', meta: { kind: 'series' } },
276
+ ])
277
+ return ctx.subscribeAnalytics((snap) => {
278
+ const defs =
279
+ typeof options.seriesDefs === 'function' ? options.seriesDefs() : options.seriesDefs
280
+ options.onSeries(buildTableChartSeriesList(snap.sourceRows, defs), snap)
281
+ })
282
+ },
283
+ }
284
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * RsTable 列汇总纯函数:聚合算法、footer 单元格构建、兼容性警告。
3
+ *
4
+ * 不依赖 Vue;由 RsTable 在 computed 中调用。
5
+ * 聚合范围约定为「当前 viewRows」(筛选/排序后的业务行),不含 group/expand 伪行。
6
+ */
7
+
8
+ import type { RsTableColumn, RsTableRowData } from '../table-utils'
9
+ import { resolveColumnRawValue } from './table-edit-utils'
10
+
11
+ /** 列汇总类型:内置数值聚合 + custom reduce */
12
+ export type RsTableSummaryType = 'sum' | 'avg' | 'count' | 'min' | 'max' | 'custom'
13
+
14
+ /**
15
+ * 列级汇总配置(挂在 `RsTableColumn.summary`)。
16
+ *
17
+ * - sum/avg/min/max:从行字段取可解析数字;无可解析值时单元格为空
18
+ * - count:行数(不读字段)
19
+ * - custom:必须提供 `reduce`
20
+ */
21
+ export interface RsTableColumnSummary<T extends RsTableRowData = RsTableRowData> {
22
+ type: RsTableSummaryType
23
+ /** 展示格式化;未传时用默认 String(value) */
24
+ formatter?: (value: number | string, rows: T[]) => string
25
+ /** type=custom 时必填;返回值再经 formatter(若有) */
26
+ reduce?: (rows: T[]) => string | number
27
+ }
28
+
29
+ /**
30
+ * 汇总数据来源:
31
+ * - client:本地对 viewRows 聚合
32
+ * - server:直接展示业务传入的 summaryData,不做本地 reduce
33
+ */
34
+ export type RsTableSummaryMode = 'client' | 'server'
35
+
36
+ /** 服务端汇总:列 key → 展示值(null/undefined 显示为空) */
37
+ export type RsTableSummaryData = Record<string, string | number | null | undefined>
38
+
39
+ /** 单列汇总单元格结果(供 footer 行渲染) */
40
+ export interface RsTableSummaryCell {
41
+ key: string
42
+ /** 原始聚合值;无数据时为 null */
43
+ value: string | number | null
44
+ /** 已格式化的展示文案 */
45
+ text: string
46
+ }
47
+
48
+ /** 将未知值解析为有限数字;失败返回 null(跳过该行) */
49
+ function toNumber(value: unknown): number | null {
50
+ if (typeof value === 'number' && Number.isFinite(value)) return value
51
+ if (typeof value === 'string' && value.trim() !== '') {
52
+ const n = Number(value)
53
+ return Number.isFinite(n) ? n : null
54
+ }
55
+ return null
56
+ }
57
+
58
+ /**
59
+ * 收集列上可解析的数值序列。
60
+ * 取值路径与编辑/复制一致:resolveColumnRawValue(dataIndex / key)。
61
+ */
62
+ function collectNumericValues<T extends RsTableRowData>(
63
+ rows: readonly T[],
64
+ column: RsTableColumn<T>,
65
+ ): number[] {
66
+ const nums: number[] = []
67
+ for (let i = 0; i < rows.length; i += 1) {
68
+ const raw = resolveColumnRawValue(rows[i] as T, column, i)
69
+ const n = toNumber(raw)
70
+ if (n != null) nums.push(n)
71
+ }
72
+ return nums
73
+ }
74
+
75
+ /**
76
+ * 对单列做汇总。列未配置 `summary` 时返回 null(由调用方填空单元格)。
77
+ *
78
+ * @param column 含可选 summary 的列定义
79
+ * @param rows 参与聚合的业务行(通常为 Engine.viewRows)
80
+ */
81
+ export function aggregateColumnSummary<T extends RsTableRowData>(
82
+ column: RsTableColumn<T> & { summary?: RsTableColumnSummary<T> },
83
+ rows: readonly T[],
84
+ ): RsTableSummaryCell | null {
85
+ const config = column.summary
86
+ if (!config) return null
87
+
88
+ let value: string | number | null = null
89
+
90
+ if (config.type === 'custom') {
91
+ value = config.reduce ? config.reduce(rows as T[]) : null
92
+ } else if (config.type === 'count') {
93
+ value = rows.length
94
+ } else {
95
+ const nums = collectNumericValues(rows, column)
96
+ if (nums.length === 0) {
97
+ value = null
98
+ } else if (config.type === 'sum') {
99
+ value = nums.reduce((a, b) => a + b, 0)
100
+ } else if (config.type === 'avg') {
101
+ value = nums.reduce((a, b) => a + b, 0) / nums.length
102
+ } else if (config.type === 'min') {
103
+ value = Math.min(...nums)
104
+ } else if (config.type === 'max') {
105
+ value = Math.max(...nums)
106
+ }
107
+ }
108
+
109
+ const text =
110
+ value == null
111
+ ? ''
112
+ : config.formatter
113
+ ? config.formatter(value, rows as T[])
114
+ : String(value)
115
+
116
+ return { key: column.key, value, text }
117
+ }
118
+
119
+ /**
120
+ * 计算整行 footer 汇总单元格(与 displayColumns 一一对应)。
121
+ *
122
+ * - client:按 columns.summary 对 rows 聚合;无 summary 的列 text 为空
123
+ * - server:优先读 summaryData[key];仍可用列上 formatter;忽略本地 sum/avg 等
124
+ *
125
+ * @returns 与 columns 等长的单元格数组
126
+ */
127
+ export function buildTableSummaryCells<T extends RsTableRowData>(options: {
128
+ columns: Array<RsTableColumn<T> & { summary?: RsTableColumnSummary<T> }>
129
+ rows: readonly T[]
130
+ mode: RsTableSummaryMode
131
+ summaryData?: RsTableSummaryData | null
132
+ }): RsTableSummaryCell[] {
133
+ const { columns, rows, mode, summaryData } = options
134
+ if (mode === 'server') {
135
+ return columns.map((column) => {
136
+ if (!column.summary && summaryData?.[column.key] === undefined) {
137
+ return { key: column.key, value: null, text: '' }
138
+ }
139
+ const raw = summaryData?.[column.key]
140
+ const value = raw == null ? null : raw
141
+ const text =
142
+ value == null
143
+ ? ''
144
+ : column.summary?.formatter
145
+ ? column.summary.formatter(value as string | number, rows as T[])
146
+ : String(value)
147
+ return { key: column.key, value, text }
148
+ })
149
+ }
150
+
151
+ return columns.map((column) => {
152
+ const cell = aggregateColumnSummary(column, rows)
153
+ return cell ?? { key: column.key, value: null, text: '' }
154
+ })
155
+ }
156
+
157
+ /**
158
+ * 是否存在任何可展示的汇总配置。
159
+ *
160
+ * - client:任一列带 summary
161
+ * - server:summaryData 非空对象
162
+ *
163
+ * 用于决定是否启用 summary feature / 渲染内置汇总行。
164
+ */
165
+ export function hasTableSummaryConfig<T extends RsTableRowData>(options: {
166
+ columns: Array<RsTableColumn<T> & { summary?: RsTableColumnSummary<T> }>
167
+ mode: RsTableSummaryMode
168
+ summaryData?: RsTableSummaryData | null
169
+ }): boolean {
170
+ if (options.mode === 'server') {
171
+ return Boolean(options.summaryData && Object.keys(options.summaryData).length > 0)
172
+ }
173
+ return options.columns.some((column) => Boolean(column.summary))
174
+ }
175
+
176
+ /**
177
+ * summary 与互斥/易误解能力的兼容矩阵检查(仅 DEV 警告)。
178
+ *
179
+ * - tree:合计基于已展开的 viewRows,不含折叠子节点
180
+ * - detail expandable:合计不含 expand 明细槽内容
181
+ */
182
+ export function warnSummaryCompatibility(flags: {
183
+ summaryEnabled: boolean
184
+ tree: boolean
185
+ detailExpandable: boolean
186
+ }): void {
187
+ if (!flags.summaryEnabled) return
188
+ if (flags.tree) {
189
+ console.warn('[RsTable] summary 与 treeConfig 同开时,合计基于展平行 viewRows,不含折叠子节点')
190
+ }
191
+ if (flags.detailExpandable) {
192
+ console.warn('[RsTable] summary 与 detail expandable 同开时,合计不含 expand 明细槽内容')
193
+ }
194
+ }