@beppla/tapas-ui 1.2.36 → 1.4.0

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.
@@ -1,7 +1,8 @@
1
- import React, { useMemo, useCallback, useState, useRef } from 'react';
2
- import { View, ScrollView, StyleSheet, Platform, TouchableOpacity, NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
3
- import Text from '../Text/Text';
1
+ import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react';
2
+ import { View, ScrollView, StyleSheet, Platform, TouchableOpacity, NativeScrollEvent, NativeSyntheticEvent, Text, Animated } from 'react-native';
3
+ import { useTheme } from '@rneui/themed';
4
4
  import Pagination from '../Pagination/Pagination';
5
+ import Hoverable from '../Hoverable/Hoverable';
5
6
 
6
7
  export type CellAlignment = 'left' | 'center' | 'right';
7
8
  export type CellDataType = 'text' | 'number' | 'currency' | 'custom';
@@ -9,12 +10,12 @@ export type CellDataType = 'text' | 'number' | 'currency' | 'custom';
9
10
  export interface StatisticsTableColumn {
10
11
  key: string;
11
12
  title: string;
12
- width?: number; // 动态列宽
13
- minWidth?: number; // 最小列宽
14
- maxWidth?: number; // 最大列宽
13
+ width?: number;
14
+ minWidth?: number;
15
+ maxWidth?: number;
15
16
  align?: CellAlignment;
16
17
  dataType?: CellDataType;
17
- isAction?: boolean; // 是否是 action 列
18
+ isAction?: boolean;
18
19
  render?: (cell: StatisticsTableCell, rowData: any) => React.ReactNode;
19
20
  }
20
21
 
@@ -23,7 +24,7 @@ export interface StatisticsTableCell {
23
24
  columnKey: string;
24
25
  quantity: number;
25
26
  amount: number;
26
- data?: any; // 额外数据,用于 render 函数
27
+ data?: any;
27
28
  }
28
29
 
29
30
  export interface StatisticsTableAction {
@@ -32,18 +33,20 @@ export interface StatisticsTableAction {
32
33
  icon?: string;
33
34
  }
34
35
 
36
+ // Tooltip 内容类型
37
+ export interface TooltipContent {
38
+ title?: string;
39
+ subtitle?: string;
40
+ items?: Array<{ label: string; value: string }>;
41
+ customContent?: React.ReactNode;
42
+ }
43
+
35
44
  export interface StatisticsTableProps {
36
- // 行标题(如日期)
37
45
  rows: Array<{ key: string; label: string; data?: any; height?: number }>;
38
- // 列标题(如门店)
39
46
  columns: StatisticsTableColumn[];
40
- // 单元格数据
41
47
  cells: StatisticsTableCell[];
42
- // 是否显示行统计(Sum/Mean 列)
43
48
  showRowStats?: boolean;
44
- // 是否显示列统计(Sum/Mean 行)
45
49
  showColumnStats?: boolean;
46
- // 分页配置
47
50
  pagination?: {
48
51
  current: number;
49
52
  pageSize: number;
@@ -55,27 +58,125 @@ export interface StatisticsTableProps {
55
58
  maxHeight?: number;
56
59
  rowLabelWidth?: number;
57
60
  style?: any;
58
- // 格式化函数
59
61
  formatQuantity?: (quantity: number) => string;
60
62
  formatAmount?: (amount: number) => string;
61
- // 虚拟滚动(性能优化)
62
63
  enableVirtualization?: boolean;
63
64
  virtualRowHeight?: number;
64
- // 行点击事件
65
65
  onRowPress?: (rowKey: string, data?: any) => void;
66
- // Action 列配置
67
66
  rowActions?: StatisticsTableAction[];
68
- // 动态行高
69
- rowHeight?: number; // 默认行高
70
- getRowHeight?: (rowKey: string, rowData?: any) => number; // 自定义每行高度
71
- // 动态列宽
72
- getColumnWidth?: (columnKey: string, column: StatisticsTableColumn) => number; // 自定义列宽
73
- // 滚动阴影效果
74
- enableScrollShadow?: boolean; // 启用滚动阴影
75
- scrollShadowColor?: string; // 阴影颜色
76
- scrollShadowSize?: number; // 阴影大小
67
+ rowHeight?: number;
68
+ getRowHeight?: (rowKey: string, rowData?: any) => number;
69
+ getColumnWidth?: (columnKey: string, column: StatisticsTableColumn) => number;
70
+ enableScrollShadow?: boolean;
71
+ showScrollIndicator?: boolean;
72
+ statsColumnWidth?: number;
73
+ onCellHover?: (rowKey: string, columnKey: string, isHovered: boolean) => void;
74
+ renderCellTooltip?: (rowKey: string, columnKey: string, cell: StatisticsTableCell, column: StatisticsTableColumn) => TooltipContent | null;
75
+ renderRowStatsTooltip?: (rowKey: string, stats: { sumQuantity: number; sumAmount: number; meanQuantity: number; meanAmount: number }, storeCount: number) => TooltipContent | null;
76
+ renderColumnStatsTooltip?: (columnKey: string, column: StatisticsTableColumn, stats: { sumQuantity: number; sumAmount: number; meanQuantity: number; meanAmount: number }) => TooltipContent | null;
77
+ // 统计列/行表头配置
78
+ rowStatsHeaders?: {
79
+ sum: string;
80
+ mean: string;
81
+ };
82
+ columnStatsLabels?: {
83
+ sum: string;
84
+ mean: string;
85
+ };
86
+ // 表头样式定制
87
+ headerStyle?: {
88
+ backgroundColor?: string;
89
+ textColor?: string;
90
+ fontSize?: number;
91
+ fontWeight?: 'normal' | 'bold' | '500' | '600' | '700';
92
+ };
93
+ // Row Stats 表头样式定制
94
+ rowStatsHeaderStyle?: {
95
+ backgroundColor?: string;
96
+ textColor?: string;
97
+ fontSize?: number;
98
+ fontWeight?: 'normal' | 'bold' | '500' | '600' | '700';
99
+ };
100
+ // Column Stats 标签样式定制
101
+ columnStatsLabelStyle?: {
102
+ textColor?: string;
103
+ fontSize?: number;
104
+ fontWeight?: 'normal' | 'bold' | '500' | '600' | '700';
105
+ };
106
+ // 动画配置
107
+ enableStatsAnimation?: boolean;
108
+ statsAnimationDuration?: number; // 动画持续时间(毫秒)
77
109
  }
78
110
 
111
+ // Tooltip 组件
112
+ const CellTooltip = ({ visible, content, style }: {
113
+ visible: boolean;
114
+ content: TooltipContent | null;
115
+ style?: any;
116
+ }) => {
117
+ if (!visible || !content) return null;
118
+
119
+ return (
120
+ <View style={[tooltipStyles.container, style]}>
121
+ {content.customContent ? (
122
+ content.customContent
123
+ ) : (
124
+ <>
125
+ {content.title && <Text style={tooltipStyles.title}>{content.title}</Text>}
126
+ {content.subtitle && <Text style={tooltipStyles.subtitle}>{content.subtitle}</Text>}
127
+ {content.items?.map((item, index) => (
128
+ <View key={index} style={tooltipStyles.row}>
129
+ <Text style={tooltipStyles.label}>{item.label}</Text>
130
+ <Text style={tooltipStyles.value}>{item.value}</Text>
131
+ </View>
132
+ ))}
133
+ </>
134
+ )}
135
+ </View>
136
+ );
137
+ };
138
+
139
+ const tooltipStyles = StyleSheet.create({
140
+ container: {
141
+ position: 'absolute',
142
+ backgroundColor: '#FFF',
143
+ borderRadius: 8,
144
+ padding: 12,
145
+ shadowColor: '#000',
146
+ shadowOffset: { width: 0, height: 2 },
147
+ shadowOpacity: 0.15,
148
+ shadowRadius: 8,
149
+ elevation: 5,
150
+ zIndex: 100,
151
+ minWidth: 150,
152
+ },
153
+ title: {
154
+ fontSize: 14,
155
+ fontWeight: '600',
156
+ color: 'rgba(0, 0, 0, 0.87)',
157
+ marginBottom: 4,
158
+ },
159
+ subtitle: {
160
+ fontSize: 12,
161
+ color: 'rgba(0, 0, 0, 0.54)',
162
+ marginBottom: 8,
163
+ },
164
+ row: {
165
+ flexDirection: 'row',
166
+ justifyContent: 'space-between',
167
+ marginTop: 4,
168
+ },
169
+ label: {
170
+ fontSize: 12,
171
+ color: 'rgba(0, 0, 0, 0.54)',
172
+ },
173
+ value: {
174
+ fontSize: 12,
175
+ fontWeight: '600',
176
+ color: 'rgba(0, 0, 0, 0.87)',
177
+ },
178
+ });
179
+
79
180
  export function StatisticsTable({
80
181
  rows,
81
182
  columns,
@@ -98,22 +199,156 @@ export function StatisticsTable({
98
199
  getRowHeight,
99
200
  getColumnWidth,
100
201
  enableScrollShadow = true,
101
- scrollShadowColor = 'rgba(0, 0, 0, 0.1)',
102
- scrollShadowSize = 8,
202
+ showScrollIndicator = false,
203
+ statsColumnWidth = 100,
204
+ onCellHover,
205
+ renderCellTooltip,
206
+ renderRowStatsTooltip,
207
+ renderColumnStatsTooltip,
208
+ rowStatsHeaders = { sum: 'Sum (€)', mean: 'Mean (€)' },
209
+ columnStatsLabels = { sum: 'Sum', mean: 'Mean' },
210
+ headerStyle,
211
+ rowStatsHeaderStyle,
212
+ columnStatsLabelStyle,
213
+ enableStatsAnimation = true,
214
+ statsAnimationDuration = 300,
103
215
  }: StatisticsTableProps) {
216
+ const { theme } = useTheme();
217
+ const colors = theme.colors as any;
218
+
219
+ // 动画值
220
+ const rowStatsAnimation = useRef(new Animated.Value(0)).current;
221
+ const columnStatsAnimation = useRef(new Animated.Value(0)).current;
104
222
 
105
223
  // 滚动状态
106
- const [horizontalScrollState, setHorizontalScrollState] = useState({
107
- isAtStart: true,
108
- isAtEnd: false,
109
- });
110
- const [verticalScrollState, setVerticalScrollState] = useState({
224
+ const [scrollState, setScrollState] = useState({
225
+ left: 0,
226
+ top: 0,
111
227
  isAtStart: true,
112
228
  isAtEnd: false,
229
+ isAtTop: true,
230
+ isAtBottom: false,
113
231
  });
114
232
 
115
- const horizontalScrollRef = useRef<ScrollView>(null);
116
- const verticalScrollRef = useRef<ScrollView>(null);
233
+ // Hover 状态
234
+ const [hoveredCell, setHoveredCell] = useState<{
235
+ rowKey: string;
236
+ columnKey: string;
237
+ } | null>(null);
238
+
239
+ const scrollViewRef = useRef<ScrollView>(null);
240
+ const bodyScrollViewRef = useRef<ScrollView>(null);
241
+ const rowStatsScrollRef = useRef<ScrollView>(null);
242
+
243
+ // Column stats 和 Row stats 是互斥的,columnStats 优先
244
+ const actualShowColumnStats = showColumnStats;
245
+ const actualShowRowStats = showRowStats && !showColumnStats;
246
+
247
+ // Row Stats 动画效果
248
+ useEffect(() => {
249
+ if (enableStatsAnimation && actualShowRowStats) {
250
+ Animated.timing(rowStatsAnimation, {
251
+ toValue: 1,
252
+ duration: statsAnimationDuration,
253
+ useNativeDriver: true,
254
+ }).start();
255
+ } else {
256
+ rowStatsAnimation.setValue(actualShowRowStats ? 1 : 0);
257
+ }
258
+ }, [actualShowRowStats, enableStatsAnimation, statsAnimationDuration, rowStatsAnimation]);
259
+
260
+ // Column Stats 动画效果
261
+ useEffect(() => {
262
+ if (enableStatsAnimation && actualShowColumnStats) {
263
+ Animated.timing(columnStatsAnimation, {
264
+ toValue: 1,
265
+ duration: statsAnimationDuration,
266
+ useNativeDriver: true,
267
+ }).start();
268
+ } else {
269
+ columnStatsAnimation.setValue(actualShowColumnStats ? 1 : 0);
270
+ }
271
+ }, [actualShowColumnStats, enableStatsAnimation, statsAnimationDuration, columnStatsAnimation]);
272
+
273
+ // 动态样式 - 根据设计图
274
+ const themedStyles = useMemo(() => ({
275
+ container: {
276
+ backgroundColor: colors.colorSurface, // 米色背景
277
+ },
278
+ header: {
279
+ backgroundColor: headerStyle?.backgroundColor || colors.colorSurface, // 米色
280
+ borderBottomColor: colors.colorTableBorder,
281
+ },
282
+ headerCell: {
283
+ borderRightColor: colors.colorTableBorder,
284
+ },
285
+ headerText: {
286
+ color: headerStyle?.textColor || colors.colorTextPrimary,
287
+ fontSize: headerStyle?.fontSize || 14,
288
+ fontWeight: headerStyle?.fontWeight || '500',
289
+ },
290
+ // Row Stats 表头样式
291
+ statsHeaderCell: {
292
+ backgroundColor: rowStatsHeaderStyle?.backgroundColor || 'rgba(0, 0, 0, 0.08)',
293
+ borderRightColor: colors.colorTableBorder,
294
+ borderBottomColor: colors.colorTableBorder,
295
+ },
296
+ statsHeaderText: {
297
+ color: rowStatsHeaderStyle?.textColor || colors.colorTextPrimary,
298
+ fontSize: rowStatsHeaderStyle?.fontSize || 14,
299
+ fontWeight: rowStatsHeaderStyle?.fontWeight || '500',
300
+ },
301
+ // 数据行
302
+ row: {
303
+ borderBottomColor: colors.colorTableBorder,
304
+ backgroundColor: colors.colorSurface, // 米色
305
+ },
306
+ // Hover 行
307
+ rowHovered: {
308
+ backgroundColor: colors.colorSurface1, // 白色
309
+ },
310
+ // Hover 单元格 - rgba(0, 0, 0, 0.08)
311
+ cellHovered: {
312
+ backgroundColor: 'rgba(0, 0, 0, 0.08)',
313
+ },
314
+ // Column Stats 行(Sum/Mean)- 纯白色背景
315
+ columnStatsRow: {
316
+ backgroundColor: '#FFFFFF', // 纯白色
317
+ borderBottomColor: colors.colorTableBorder,
318
+ },
319
+ // Row Stats 列 - 白色背景
320
+ rowStatsCell: {
321
+ backgroundColor: colors.colorSurface1, // 白色
322
+ borderBottomColor: colors.colorTableBorder,
323
+ borderRightColor: colors.colorTableBorder,
324
+ },
325
+ cell: {
326
+ borderRightColor: colors.colorTableBorder,
327
+ },
328
+ rowLabel: {
329
+ color: colors.colorTextPrimary,
330
+ },
331
+ cellQuantity: {
332
+ color: colors.colorTextPrimary,
333
+ },
334
+ cellAmount: {
335
+ color: colors.colorTextSecondary,
336
+ },
337
+ statsLabel: {
338
+ color: columnStatsLabelStyle?.textColor || colors.colorTextBrand, // 棕色
339
+ fontSize: columnStatsLabelStyle?.fontSize || 14,
340
+ fontWeight: columnStatsLabelStyle?.fontWeight || '600',
341
+ },
342
+ paginationContainer: {
343
+ borderTopColor: colors.colorTableBorder,
344
+ backgroundColor: colors.colorSurface,
345
+ },
346
+ // Column Stats 分隔线
347
+ columnStatsDivider: {
348
+ borderTopWidth: 2,
349
+ borderTopColor: colors.colorTableBorder,
350
+ },
351
+ }), [colors, headerStyle, rowStatsHeaderStyle, columnStatsLabelStyle]);
117
352
 
118
353
  // 构建矩阵数据
119
354
  const matrix = useMemo(() => {
@@ -130,126 +365,158 @@ export function StatisticsTable({
130
365
  return data;
131
366
  }, [rows, columns, cells]);
132
367
 
133
- // 计算行统计(每行的总和和平均值)
368
+ // 计算行统计
134
369
  const rowStats = useMemo(() => {
135
370
  if (!showRowStats) return null;
136
371
  return rows.map(row => {
137
372
  const rowCells = columns
138
- .filter(col => !col.isAction) // 排除 action 列
373
+ .filter(col => !col.isAction)
139
374
  .map(col => matrix[row.key]?.[col.key] || { quantity: 0, amount: 0 });
140
375
  const sumQuantity = rowCells.reduce((sum, cell) => sum + cell.quantity, 0);
141
376
  const sumAmount = rowCells.reduce((sum, cell) => sum + cell.amount, 0);
142
377
  const count = rowCells.length || 1;
143
- const meanQuantity = sumQuantity / count;
144
- const meanAmount = sumAmount / count;
145
- return { sumQuantity, sumAmount, meanQuantity, meanAmount };
378
+ return { sumQuantity, sumAmount, meanQuantity: sumQuantity / count, meanAmount: sumAmount / count };
146
379
  });
147
380
  }, [rows, columns, matrix, showRowStats]);
148
381
 
149
- // 计算列统计(每列的总和和平均值)
382
+ // 计算列统计
150
383
  const columnStats = useMemo(() => {
151
384
  if (!showColumnStats) return null;
152
385
  return columns
153
- .filter(col => !col.isAction) // 排除 action 列
386
+ .filter(col => !col.isAction)
154
387
  .map(col => {
155
388
  const colCells = rows.map(row => matrix[row.key]?.[col.key] || { quantity: 0, amount: 0 });
156
389
  const sumQuantity = colCells.reduce((sum, cell) => sum + cell.quantity, 0);
157
390
  const sumAmount = colCells.reduce((sum, cell) => sum + cell.amount, 0);
158
391
  const count = colCells.length || 1;
159
- const meanQuantity = sumQuantity / count;
160
- const meanAmount = sumAmount / count;
161
- return { sumQuantity, sumAmount, meanQuantity, meanAmount };
392
+ return { sumQuantity, sumAmount, meanQuantity: sumQuantity / count, meanAmount: sumAmount / count };
162
393
  });
163
394
  }, [rows, columns, matrix, showColumnStats]);
164
395
 
165
- // 获取列宽
166
396
  const getColWidth = useCallback((column: StatisticsTableColumn) => {
167
- if (getColumnWidth) {
168
- return getColumnWidth(column.key, column);
169
- }
397
+ if (getColumnWidth) return getColumnWidth(column.key, column);
170
398
  return column.width || 150;
171
399
  }, [getColumnWidth]);
172
400
 
173
- // 获取行高
174
401
  const getRowHeightValue = useCallback((row: { key: string; label: string; data?: any; height?: number }) => {
175
402
  if (row.height) return row.height;
176
- if (getRowHeight) {
177
- return getRowHeight(row.key, row.data);
178
- }
403
+ if (getRowHeight) return getRowHeight(row.key, row.data);
179
404
  return rowHeight;
180
405
  }, [rowHeight, getRowHeight]);
181
406
 
182
- // 处理水平滚动
183
- const handleHorizontalScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
407
+ // 处理滚动
408
+ const handleScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
184
409
  const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
185
- const isAtStart = contentOffset.x <= 0;
186
- const isAtEnd = contentOffset.x + layoutMeasurement.width >= contentSize.width - 1;
187
-
188
- setHorizontalScrollState({ isAtStart, isAtEnd });
410
+ setScrollState({
411
+ left: contentOffset.x,
412
+ top: contentOffset.y,
413
+ isAtStart: contentOffset.x <= 0,
414
+ isAtEnd: contentOffset.x + layoutMeasurement.width >= contentSize.width - 1,
415
+ isAtTop: contentOffset.y <= 0,
416
+ isAtBottom: contentOffset.y + layoutMeasurement.height >= contentSize.height - 1,
417
+ });
189
418
  }, []);
190
419
 
191
- // 处理垂直滚动
192
- const handleVerticalScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
193
- const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
194
- const isAtStart = contentOffset.y <= 0;
195
- const isAtEnd = contentOffset.y + layoutMeasurement.height >= contentSize.height - 1;
196
-
197
- setVerticalScrollState({ isAtStart, isAtEnd });
198
- }, []);
420
+ // 同步 Row Stats 滚动
421
+ const handleBodyScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
422
+ const { contentOffset } = event.nativeEvent;
423
+ rowStatsScrollRef.current?.scrollTo({ y: contentOffset.y, animated: false });
424
+ handleScroll(event);
425
+ }, [handleScroll]);
199
426
 
200
- // 获取单元格对齐方式
201
- const getCellAlignment = useCallback((column: StatisticsTableColumn, isHeader: boolean = false) => {
202
- if (column.align) return column.align;
203
- if (isHeader) return 'center';
204
- // 默认:数字右对齐,文本左对齐
205
- if (column.dataType === 'number' || column.dataType === 'currency') {
206
- return 'right';
427
+ // 处理单元格 hover
428
+ const handleCellHover = useCallback((rowKey: string, columnKey: string, isHovered: boolean) => {
429
+ if (isHovered) {
430
+ setHoveredCell({ rowKey, columnKey });
431
+ } else {
432
+ setHoveredCell(null);
207
433
  }
208
- return 'left';
209
- }, []);
434
+ onCellHover?.(rowKey, columnKey, isHovered);
435
+ }, [onCellHover]);
436
+
437
+ // 获取单元格 tooltip 内容
438
+ const getCellTooltipContent = useCallback((rowKey: string, columnKey: string, cell: StatisticsTableCell, column: StatisticsTableColumn): TooltipContent | null => {
439
+ if (renderCellTooltip) {
440
+ return renderCellTooltip(rowKey, columnKey, cell, column);
441
+ }
442
+ return {
443
+ title: column.title,
444
+ items: [
445
+ { label: 'Quantity', value: formatQuantity(cell.quantity) },
446
+ { label: 'Amount', value: formatAmount(cell.amount) },
447
+ ],
448
+ };
449
+ }, [renderCellTooltip, formatQuantity, formatAmount]);
450
+
451
+ // 获取行统计 tooltip
452
+ const getRowStatsTooltipContent = useCallback((rowKey: string, stats: { sumQuantity: number; sumAmount: number; meanQuantity: number; meanAmount: number }, storeCount: number): TooltipContent | null => {
453
+ if (renderRowStatsTooltip) {
454
+ return renderRowStatsTooltip(rowKey, stats, storeCount);
455
+ }
456
+ const row = rows.find(r => r.key === rowKey);
457
+ return {
458
+ title: row?.label || rowKey,
459
+ subtitle: `${storeCount} stores`,
460
+ items: [
461
+ { label: 'Sum', value: formatAmount(stats.sumAmount) },
462
+ { label: 'Mean', value: formatAmount(stats.meanAmount) },
463
+ ],
464
+ };
465
+ }, [renderRowStatsTooltip, formatAmount, rows]);
466
+
467
+ // 获取列统计 tooltip
468
+ const getColumnStatsTooltipContent = useCallback((columnKey: string, column: StatisticsTableColumn, stats: { sumQuantity: number; sumAmount: number; meanQuantity: number; meanAmount: number }): TooltipContent | null => {
469
+ if (renderColumnStatsTooltip) {
470
+ return renderColumnStatsTooltip(columnKey, column, stats);
471
+ }
472
+ return {
473
+ title: column.title,
474
+ items: [
475
+ { label: 'Sum', value: formatAmount(stats.sumAmount) },
476
+ { label: 'Mean', value: formatAmount(stats.meanAmount) },
477
+ ],
478
+ };
479
+ }, [renderColumnStatsTooltip, formatAmount]);
210
480
 
211
481
  // 渲染单元格内容
212
482
  const renderCellContent = useCallback((column: StatisticsTableColumn, cell: any, rowData?: any) => {
213
- // 自定义渲染
214
- if (column.render) {
215
- return column.render(cell, rowData);
216
- }
483
+ if (column.render) return column.render(cell, rowData);
217
484
 
218
- // Action 列
219
485
  if (column.isAction && rowActions) {
220
486
  return (
221
487
  <View style={styles.actionContainer}>
222
488
  {rowActions.map((action, index) => (
223
489
  <TouchableOpacity
224
490
  key={index}
225
- style={styles.actionButton}
491
+ style={[styles.actionButton, { backgroundColor: colors.colorTextAccent }]}
226
492
  onPress={() => action.onPress(cell.rowKey, rowData)}
227
493
  activeOpacity={0.7}
228
494
  >
229
- <Text style={styles.actionButtonText}>{action.label}</Text>
495
+ <Text style={[styles.actionButtonText, { color: colors.colorTextInvert }]}>{action.label}</Text>
230
496
  </TouchableOpacity>
231
497
  ))}
232
498
  </View>
233
499
  );
234
500
  }
235
501
 
236
- // 默认渲染
237
- const align = getCellAlignment(column);
238
502
  return (
239
- <View style={[styles.cellContentContainer, { alignItems: align === 'left' ? 'flex-start' : align === 'right' ? 'flex-end' : 'center' }]}>
240
- <Text style={[styles.cellQuantity, align === 'right' && styles.cellQuantityRight]}>
503
+ <View style={styles.cellContentContainer}>
504
+ <Text style={[styles.cellQuantity, themedStyles.cellQuantity]}>
241
505
  {formatQuantity(cell.quantity)}
242
506
  </Text>
243
- <Text style={[styles.cellAmount, align === 'right' && styles.cellAmountRight]}>
507
+ <Text style={[styles.cellAmount, themedStyles.cellAmount]}>
244
508
  {formatAmount(cell.amount)}
245
509
  </Text>
246
510
  </View>
247
511
  );
248
- }, [rowActions, getCellAlignment, formatQuantity, formatAmount]);
512
+ }, [rowActions, formatQuantity, formatAmount, colors, themedStyles]);
513
+
514
+ // 计算统计列宽度
515
+ const statsColumnsWidth = actualShowRowStats ? statsColumnWidth * 2 : 0;
249
516
 
250
517
  if (loading) {
251
518
  return (
252
- <View style={[styles.container, style]}>
519
+ <View style={[styles.container, themedStyles.container, style]}>
253
520
  <View style={styles.loadingRow}>
254
521
  <Text>Loading...</Text>
255
522
  </View>
@@ -259,298 +526,368 @@ export function StatisticsTable({
259
526
 
260
527
  if (rows.length === 0 || columns.length === 0) {
261
528
  return (
262
- <View style={[styles.container, style]}>
529
+ <View style={[styles.container, themedStyles.container, style]}>
263
530
  <View style={styles.emptyRow}>
264
- <Text style={styles.emptyText}>{emptyText}</Text>
531
+ <Text style={[styles.emptyText, { color: colors.colorTextPlaceholder }]}>{emptyText}</Text>
265
532
  </View>
266
533
  </View>
267
534
  );
268
535
  }
269
536
 
270
- // 虚拟滚动实现(简化版)
271
537
  const visibleRows = enableVirtualization
272
538
  ? rows.slice(0, Math.ceil(maxHeight / virtualRowHeight))
273
539
  : rows;
274
540
 
541
+ // Web 阴影样式
542
+ const webShadowStyle = Platform.OS === 'web' ? {
543
+ leftShadow: { boxShadow: '2px 0 6px 4px rgba(0, 0, 0, 0.08)' } as any,
544
+ rightShadow: { boxShadow: '-2px 0 6px 4px rgba(0, 0, 0, 0.08)' } as any,
545
+ topShadow: { boxShadow: '0 2px 6px 4px rgba(0, 0, 0, 0.08)' } as any,
546
+ bottomShadow: { boxShadow: '0 -2px 6px 4px rgba(0, 0, 0, 0.08)' } as any,
547
+ } : {};
548
+
549
+ // 隐藏滚动条的样式
550
+ const hideScrollbarStyle = Platform.OS === 'web' ? {
551
+ scrollbarWidth: 'none',
552
+ msOverflowStyle: 'none',
553
+ } as any : {};
554
+
555
+ const columnStatsHeight = actualShowColumnStats ? 120 : 0;
556
+ const storeCount = columns.filter(c => !c.isAction).length;
557
+
275
558
  return (
276
- <View style={[styles.container, style]}>
559
+ <View style={[styles.container, themedStyles.container, style]}>
277
560
  {/* 左边阴影 */}
278
- {enableScrollShadow && !horizontalScrollState.isAtStart && (
561
+ {enableScrollShadow && !scrollState.isAtStart && (
279
562
  <View style={[
563
+ styles.shadowBar,
280
564
  styles.shadowLeft,
281
- {
282
- width: scrollShadowSize,
283
- backgroundColor: 'transparent',
284
- shadowColor: scrollShadowColor,
285
- }
565
+ Platform.OS === 'web' && webShadowStyle.leftShadow,
286
566
  ]} />
287
567
  )}
288
568
 
289
569
  {/* 右边阴影 */}
290
- {enableScrollShadow && !horizontalScrollState.isAtEnd && (
570
+ {enableScrollShadow && !scrollState.isAtEnd && (
291
571
  <View style={[
572
+ styles.shadowBar,
292
573
  styles.shadowRight,
293
- {
294
- width: scrollShadowSize,
295
- backgroundColor: 'transparent',
296
- shadowColor: scrollShadowColor,
297
- }
574
+ { right: actualShowRowStats ? statsColumnsWidth : 0 },
575
+ Platform.OS === 'web' && webShadowStyle.rightShadow,
298
576
  ]} />
299
577
  )}
300
578
 
301
- <ScrollView
302
- ref={horizontalScrollRef}
303
- horizontal
304
- showsHorizontalScrollIndicator={Platform.OS === 'web'}
305
- onScroll={handleHorizontalScroll}
306
- scrollEventThrottle={16}
307
- >
308
- <View>
309
- {/* 顶部阴影 */}
310
- {enableScrollShadow && !verticalScrollState.isAtStart && (
311
- <View style={[
312
- styles.shadowTop,
313
- {
314
- height: scrollShadowSize,
315
- backgroundColor: 'transparent',
316
- shadowColor: scrollShadowColor,
317
- }
318
- ]} />
319
- )}
320
-
321
- {/* Table Header */}
322
- <View style={styles.header}>
323
- <View style={[styles.headerCell, { width: rowLabelWidth }]}>
324
- <Text style={styles.headerText}></Text>
325
- </View>
326
- {columns.map((column) => {
327
- const align = getCellAlignment(column, true);
328
- const colWidth = getColWidth(column);
329
- return (
330
- <View
331
- key={column.key}
332
- style={[
333
- styles.headerCell,
334
- {
335
- width: colWidth,
336
- minWidth: column.minWidth,
337
- maxWidth: column.maxWidth,
338
- },
339
- align === 'right' && styles.headerCellRight,
340
- align === 'left' && styles.headerCellLeft,
341
- ]}
342
- >
343
- <Text style={[
344
- styles.headerText,
345
- align === 'right' && styles.headerTextRight,
346
- align === 'left' && styles.headerTextLeft,
347
- ]}>
348
- {column.title}
349
- </Text>
350
- </View>
351
- );
352
- })}
353
- {showRowStats && (
354
- <>
355
- <View style={[styles.headerCell, { width: 120 }]}>
356
- <Text style={styles.headerText}>Sum</Text>
357
- </View>
358
- <View style={[styles.headerCell, { width: 120 }]}>
359
- <Text style={styles.headerText}>Mean</Text>
360
- </View>
361
- </>
362
- )}
363
- </View>
579
+ {/* 顶部阴影 */}
580
+ {enableScrollShadow && !scrollState.isAtTop && (
581
+ <View style={[
582
+ styles.shadowBarHorizontal,
583
+ styles.shadowTop,
584
+ Platform.OS === 'web' && webShadowStyle.topShadow,
585
+ ]} />
586
+ )}
587
+
588
+ {/* 底部阴影 */}
589
+ {enableScrollShadow && !scrollState.isAtBottom && (
590
+ <View style={[
591
+ styles.shadowBarHorizontal,
592
+ styles.shadowBottom,
593
+ { bottom: actualShowColumnStats ? columnStatsHeight : 0 },
594
+ Platform.OS === 'web' && webShadowStyle.bottomShadow,
595
+ ]} />
596
+ )}
364
597
 
365
- {/* Table Body */}
598
+ <View style={styles.tableContainer}>
599
+ {/* 主表格区域 */}
600
+ <View style={{ flex: 1 }}>
366
601
  <ScrollView
367
- ref={verticalScrollRef}
368
- style={{ maxHeight }}
369
- showsVerticalScrollIndicator={Platform.OS === 'web'}
370
- removeClippedSubviews={enableVirtualization}
371
- onScroll={handleVerticalScroll}
602
+ ref={scrollViewRef}
603
+ horizontal
604
+ showsHorizontalScrollIndicator={showScrollIndicator}
605
+ onScroll={handleScroll}
372
606
  scrollEventThrottle={16}
607
+ style={hideScrollbarStyle}
373
608
  >
374
- {visibleRows.map((row, rowIndex) => {
375
- const rowStat = rowStats?.[rowIndex];
376
- const isClickable = !!onRowPress;
377
- const currentRowHeight = getRowHeightValue(row);
378
-
379
- return (
380
- <TouchableOpacity
381
- key={row.key}
382
- style={[styles.row, { minHeight: currentRowHeight }]}
383
- onPress={isClickable ? () => onRowPress(row.key, row.data) : undefined}
384
- activeOpacity={isClickable ? 0.7 : 1}
385
- disabled={!isClickable}
386
- >
387
- <View style={[styles.cell, { width: rowLabelWidth, minHeight: currentRowHeight }]}>
388
- <Text style={styles.rowLabel}>{row.label}</Text>
389
- </View>
390
- {columns.map((column) => {
391
- const cell = matrix[row.key]?.[column.key] || { quantity: 0, amount: 0, rowKey: row.key, columnKey: column.key };
392
- const align = getCellAlignment(column);
393
- const colWidth = getColWidth(column);
394
-
395
- return (
396
- <View
397
- key={column.key}
398
- style={[
399
- styles.cell,
400
- {
401
- width: colWidth,
402
- minWidth: column.minWidth,
403
- maxWidth: column.maxWidth,
404
- minHeight: currentRowHeight,
405
- },
406
- align === 'right' && styles.cellRight,
407
- align === 'left' && styles.cellLeft,
408
- ]}
409
- >
410
- {renderCellContent(column, cell, row.data)}
411
- </View>
412
- );
413
- })}
414
- {showRowStats && rowStat && (
415
- <>
416
- <View style={[styles.cell, styles.cellRight, { width: 120 }]}>
417
- <View style={styles.cellContentContainer}>
418
- <Text style={[styles.cellQuantity, styles.cellQuantityRight]}>
419
- {formatQuantity(rowStat.sumQuantity)}
420
- </Text>
421
- <Text style={[styles.cellAmount, styles.cellAmountRight]}>
422
- {formatAmount(rowStat.sumAmount)}
423
- </Text>
424
- </View>
425
- </View>
426
- <View style={[styles.cell, styles.cellRight, { width: 120 }]}>
427
- <View style={styles.cellContentContainer}>
428
- <Text style={[styles.cellQuantity, styles.cellQuantityRight]}>
429
- {formatQuantity(Math.round(rowStat.meanQuantity * 10) / 10)}
430
- </Text>
431
- <Text style={[styles.cellAmount, styles.cellAmountRight]}>
432
- {formatAmount(rowStat.meanAmount)}
433
- </Text>
434
- </View>
609
+ <View>
610
+ {/* 表头 */}
611
+ <View style={[styles.header, themedStyles.header]}>
612
+ <View style={[styles.headerCell, themedStyles.headerCell, { width: rowLabelWidth }]}>
613
+ <Text style={[styles.headerText, themedStyles.headerText]}></Text>
614
+ </View>
615
+ {columns.map((column) => {
616
+ const colWidth = getColWidth(column);
617
+ return (
618
+ <View
619
+ key={column.key}
620
+ style={[styles.headerCell, themedStyles.headerCell, { width: colWidth }]}
621
+ >
622
+ <Text style={[styles.headerText, themedStyles.headerText]}>
623
+ {column.title}
624
+ </Text>
625
+ </View>
626
+ );
627
+ })}
628
+ </View>
629
+
630
+ {/* 表体 */}
631
+ <ScrollView
632
+ ref={bodyScrollViewRef}
633
+ style={{ maxHeight: maxHeight - columnStatsHeight }}
634
+ showsVerticalScrollIndicator={showScrollIndicator}
635
+ onScroll={handleBodyScroll}
636
+ scrollEventThrottle={16}
637
+ >
638
+ {visibleRows.map((row) => {
639
+ const currentRowHeight = getRowHeightValue(row);
640
+ const isClickable = !!onRowPress;
641
+ const isRowHovered = hoveredCell?.rowKey === row.key;
642
+
643
+ return (
644
+ <TouchableOpacity
645
+ key={row.key}
646
+ style={[
647
+ styles.row,
648
+ themedStyles.row,
649
+ { minHeight: currentRowHeight },
650
+ isRowHovered && themedStyles.rowHovered,
651
+ ]}
652
+ onPress={isClickable ? () => onRowPress(row.key, row.data) : undefined}
653
+ activeOpacity={isClickable ? 0.7 : 1}
654
+ disabled={!isClickable}
655
+ >
656
+ <View style={[styles.cell, themedStyles.cell, { width: rowLabelWidth }]}>
657
+ <Text style={[styles.rowLabel, themedStyles.rowLabel]}>{row.label}</Text>
435
658
  </View>
436
- </>
437
- )}
438
- </TouchableOpacity>
439
- );
440
- })}
441
-
442
- {/* Column Statistics (Sum/Mean rows) */}
443
- {showColumnStats && columnStats && (
444
- <>
445
- <View style={[styles.row, styles.statsRow]}>
446
- <View style={[styles.cell, { width: rowLabelWidth }]}>
447
- <Text style={styles.statsLabel}>Sum</Text>
659
+ {columns.map((column) => {
660
+ const cell = matrix[row.key]?.[column.key] || { quantity: 0, amount: 0, rowKey: row.key, columnKey: column.key };
661
+ const colWidth = getColWidth(column);
662
+ const isHovered = hoveredCell?.rowKey === row.key && hoveredCell?.columnKey === column.key;
663
+ const tooltipContent = isHovered ? getCellTooltipContent(row.key, column.key, cell as StatisticsTableCell, column) : null;
664
+
665
+ return (
666
+ <Hoverable
667
+ key={column.key}
668
+ onHoverIn={() => handleCellHover(row.key, column.key, true)}
669
+ onHoverOut={() => handleCellHover(row.key, column.key, false)}
670
+ >
671
+ <View style={[
672
+ styles.cell,
673
+ themedStyles.cell,
674
+ { width: colWidth },
675
+ isHovered && themedStyles.cellHovered,
676
+ ]}>
677
+ {renderCellContent(column, cell, row.data)}
678
+ <CellTooltip
679
+ visible={isHovered}
680
+ content={tooltipContent}
681
+ style={{ bottom: '100%', left: 0, marginBottom: 8 }}
682
+ />
683
+ </View>
684
+ </Hoverable>
685
+ );
686
+ })}
687
+ </TouchableOpacity>
688
+ );
689
+ })}
690
+ </ScrollView>
691
+
692
+ {/* Column Stats - 底部统计行 */}
693
+ {actualShowColumnStats && columnStats && (
694
+ <Animated.View style={[
695
+ styles.columnStatsContainer,
696
+ themedStyles.columnStatsDivider,
697
+ enableStatsAnimation && {
698
+ opacity: columnStatsAnimation,
699
+ transform: [{
700
+ translateY: columnStatsAnimation.interpolate({
701
+ inputRange: [0, 1],
702
+ outputRange: [60, 0],
703
+ }),
704
+ }],
705
+ },
706
+ ]}>
707
+ {/* Sum 行 */}
708
+ <View style={[styles.row, themedStyles.columnStatsRow]}>
709
+ <View style={[styles.cell, themedStyles.cell, { width: rowLabelWidth }]}>
710
+ <Text style={[styles.statsLabel, themedStyles.statsLabel]}>{columnStatsLabels.sum}</Text>
711
+ </View>
712
+ {columns.map((column) => {
713
+ const colWidth = getColWidth(column);
714
+ if (column.isAction) {
715
+ return <View key={column.key} style={[styles.cell, { width: colWidth }]} />;
716
+ }
717
+ const stat = columnStats[columns.filter(c => !c.isAction).indexOf(column)];
718
+ const isHovered = hoveredCell?.rowKey === '__column_sum__' && hoveredCell?.columnKey === column.key;
719
+ const tooltipContent = isHovered && stat ? getColumnStatsTooltipContent(column.key, column, stat) : null;
720
+
721
+ return (
722
+ <Hoverable
723
+ key={column.key}
724
+ onHoverIn={() => handleCellHover('__column_sum__', column.key, true)}
725
+ onHoverOut={() => handleCellHover('__column_sum__', column.key, false)}
726
+ >
727
+ <View style={[
728
+ styles.cell,
729
+ themedStyles.cell,
730
+ { width: colWidth },
731
+ isHovered && themedStyles.cellHovered,
732
+ ]}>
733
+ <View style={styles.cellContentContainer}>
734
+ <Text style={[styles.cellQuantity, themedStyles.cellQuantity]}>
735
+ {formatQuantity(stat?.sumQuantity || 0)}
736
+ </Text>
737
+ <Text style={[styles.cellAmount, themedStyles.cellAmount]}>
738
+ {formatAmount(stat?.sumAmount || 0)}
739
+ </Text>
740
+ </View>
741
+ <CellTooltip
742
+ visible={isHovered}
743
+ content={tooltipContent}
744
+ style={{ bottom: '100%', left: 0, marginBottom: 8 }}
745
+ />
746
+ </View>
747
+ </Hoverable>
748
+ );
749
+ })}
448
750
  </View>
449
- {columns.map((column) => {
450
- const colWidth = getColWidth(column);
451
- if (column.isAction) {
452
- return <View key={column.key} style={[styles.cell, { width: colWidth, minWidth: column.minWidth, maxWidth: column.maxWidth }]} />;
453
- }
454
- const stat = columnStats[columns.filter(c => !c.isAction).indexOf(column)];
455
- const align = getCellAlignment(column);
456
-
457
- return (
458
- <View
459
- key={column.key}
460
- style={[
461
- styles.cell,
462
- {
463
- width: colWidth,
464
- minWidth: column.minWidth,
465
- maxWidth: column.maxWidth,
466
- },
467
- align === 'right' && styles.cellRight,
468
- align === 'left' && styles.cellLeft,
469
- ]}
470
- >
471
- <View style={styles.cellContentContainer}>
472
- <Text style={[styles.cellQuantity, align === 'right' && styles.cellQuantityRight]}>
473
- {formatQuantity(stat?.sumQuantity || 0)}
474
- </Text>
475
- <Text style={[styles.cellAmount, align === 'right' && styles.cellAmountRight]}>
476
- {formatAmount(stat?.sumAmount || 0)}
477
- </Text>
478
- </View>
479
- </View>
480
- );
481
- })}
482
- {showRowStats && (
483
- <>
484
- <View style={[styles.cell, { width: 120 }]} />
485
- <View style={[styles.cell, { width: 120 }]} />
486
- </>
487
- )}
488
- </View>
489
- <View style={[styles.row, styles.statsRow]}>
490
- <View style={[styles.cell, { width: rowLabelWidth }]}>
491
- <Text style={styles.statsLabel}>Mean</Text>
751
+ {/* Mean 行 */}
752
+ <View style={[styles.row, themedStyles.columnStatsRow]}>
753
+ <View style={[styles.cell, themedStyles.cell, { width: rowLabelWidth }]}>
754
+ <Text style={[styles.statsLabel, themedStyles.statsLabel]}>{columnStatsLabels.mean}</Text>
755
+ </View>
756
+ {columns.map((column) => {
757
+ const colWidth = getColWidth(column);
758
+ if (column.isAction) {
759
+ return <View key={column.key} style={[styles.cell, { width: colWidth }]} />;
760
+ }
761
+ const stat = columnStats[columns.filter(c => !c.isAction).indexOf(column)];
762
+ const isHovered = hoveredCell?.rowKey === '__column_mean__' && hoveredCell?.columnKey === column.key;
763
+ const tooltipContent = isHovered && stat ? getColumnStatsTooltipContent(column.key, column, stat) : null;
764
+
765
+ return (
766
+ <Hoverable
767
+ key={column.key}
768
+ onHoverIn={() => handleCellHover('__column_mean__', column.key, true)}
769
+ onHoverOut={() => handleCellHover('__column_mean__', column.key, false)}
770
+ >
771
+ <View style={[
772
+ styles.cell,
773
+ themedStyles.cell,
774
+ { width: colWidth },
775
+ isHovered && themedStyles.cellHovered,
776
+ ]}>
777
+ <View style={styles.cellContentContainer}>
778
+ <Text style={[styles.cellQuantity, themedStyles.cellQuantity]}>
779
+ {formatQuantity(Math.round((stat?.meanQuantity || 0) * 10) / 10)}
780
+ </Text>
781
+ <Text style={[styles.cellAmount, themedStyles.cellAmount]}>
782
+ {formatAmount(stat?.meanAmount || 0)}
783
+ </Text>
784
+ </View>
785
+ <CellTooltip
786
+ visible={isHovered}
787
+ content={tooltipContent}
788
+ style={{ bottom: '100%', left: 0, marginBottom: 8 }}
789
+ />
790
+ </View>
791
+ </Hoverable>
792
+ );
793
+ })}
492
794
  </View>
493
- {columns.map((column) => {
494
- const colWidth = getColWidth(column);
495
- if (column.isAction) {
496
- return <View key={column.key} style={[styles.cell, { width: colWidth, minWidth: column.minWidth, maxWidth: column.maxWidth }]} />;
497
- }
498
- const stat = columnStats[columns.filter(c => !c.isAction).indexOf(column)];
499
- const align = getCellAlignment(column);
500
-
501
- return (
502
- <View
503
- key={column.key}
504
- style={[
505
- styles.cell,
506
- {
507
- width: colWidth,
508
- minWidth: column.minWidth,
509
- maxWidth: column.maxWidth,
510
- },
511
- align === 'right' && styles.cellRight,
512
- align === 'left' && styles.cellLeft,
513
- ]}
514
- >
795
+ </Animated.View>
796
+ )}
797
+ </View>
798
+ </ScrollView>
799
+ </View>
800
+
801
+ {/* Row Stats - 右侧统计列 */}
802
+ {actualShowRowStats && (
803
+ <Animated.View style={[
804
+ styles.rowStatsContainer,
805
+ { width: statsColumnsWidth },
806
+ Platform.OS === 'web' && { boxShadow: '-2px 0 6px rgba(0, 0, 0, 0.08)' } as any,
807
+ enableStatsAnimation && {
808
+ opacity: rowStatsAnimation,
809
+ transform: [{
810
+ translateX: rowStatsAnimation.interpolate({
811
+ inputRange: [0, 1],
812
+ outputRange: [statsColumnsWidth, 0],
813
+ }),
814
+ }],
815
+ },
816
+ ]}>
817
+ {/* 统计列表头 */}
818
+ <View style={[styles.header, themedStyles.header]}>
819
+ <View style={[styles.headerCell, themedStyles.statsHeaderCell, { width: statsColumnWidth }]}>
820
+ <Text style={[styles.headerText, themedStyles.statsHeaderText]}>{rowStatsHeaders.sum}</Text>
821
+ </View>
822
+ <View style={[styles.headerCell, themedStyles.statsHeaderCell, { width: statsColumnWidth }]}>
823
+ <Text style={[styles.headerText, themedStyles.statsHeaderText]}>{rowStatsHeaders.mean}</Text>
824
+ </View>
825
+ </View>
826
+
827
+ {/* 统计列数据 */}
828
+ <ScrollView
829
+ ref={rowStatsScrollRef}
830
+ style={{ maxHeight: maxHeight - columnStatsHeight }}
831
+ showsVerticalScrollIndicator={false}
832
+ scrollEnabled={false}
833
+ >
834
+ {visibleRows.map((row, rowIndex) => {
835
+ const rowStat = rowStats?.[rowIndex];
836
+ const currentRowHeight = getRowHeightValue(row);
837
+ const isRowHovered = hoveredCell?.rowKey === row.key;
838
+ const isSumHovered = hoveredCell?.rowKey === row.key && hoveredCell?.columnKey === '__row_sum__';
839
+ const sumTooltipContent = isSumHovered && rowStat ? getRowStatsTooltipContent(row.key, rowStat, storeCount) : null;
840
+
841
+ return (
842
+ <View key={row.key} style={[
843
+ styles.row,
844
+ themedStyles.rowStatsCell,
845
+ { minHeight: currentRowHeight },
846
+ isRowHovered && themedStyles.rowHovered,
847
+ ]}>
848
+ <Hoverable
849
+ onHoverIn={() => handleCellHover(row.key, '__row_sum__', true)}
850
+ onHoverOut={() => handleCellHover(row.key, '__row_sum__', false)}
851
+ >
852
+ <View style={[styles.cell, themedStyles.cell, { width: statsColumnWidth }]}>
515
853
  <View style={styles.cellContentContainer}>
516
- <Text style={[styles.cellQuantity, align === 'right' && styles.cellQuantityRight]}>
517
- {formatQuantity(Math.round((stat?.meanQuantity || 0) * 10) / 10)}
854
+ <Text style={[styles.cellQuantity, themedStyles.cellQuantity]}>
855
+ {formatQuantity(rowStat?.sumQuantity || 0)}
518
856
  </Text>
519
- <Text style={[styles.cellAmount, align === 'right' && styles.cellAmountRight]}>
520
- {formatAmount(stat?.meanAmount || 0)}
857
+ <Text style={[styles.cellAmount, themedStyles.cellAmount]}>
858
+ {formatAmount(rowStat?.sumAmount || 0)}
521
859
  </Text>
522
860
  </View>
861
+ <CellTooltip
862
+ visible={isSumHovered}
863
+ content={sumTooltipContent}
864
+ style={{ bottom: '100%', right: 0, marginBottom: 8 }}
865
+ />
523
866
  </View>
524
- );
525
- })}
526
- {showRowStats && (
527
- <>
528
- <View style={[styles.cell, { width: 120 }]} />
529
- <View style={[styles.cell, { width: 120 }]} />
530
- </>
531
- )}
532
- </View>
533
- </>
534
- )}
535
- </ScrollView>
867
+ </Hoverable>
868
+ <View style={[styles.cell, themedStyles.cell, { width: statsColumnWidth }]}>
869
+ <View style={styles.cellContentContainer}>
870
+ <Text style={[styles.cellQuantity, themedStyles.cellQuantity]}>
871
+ {formatQuantity(Math.round((rowStat?.meanQuantity || 0) * 10) / 10)}
872
+ </Text>
873
+ <Text style={[styles.cellAmount, themedStyles.cellAmount]}>
874
+ {formatAmount(rowStat?.meanAmount || 0)}
875
+ </Text>
876
+ </View>
877
+ </View>
878
+ </View>
879
+ );
880
+ })}
881
+ </ScrollView>
536
882
 
537
- {/* 底部阴影 */}
538
- {enableScrollShadow && !verticalScrollState.isAtEnd && (
539
- <View style={[
540
- styles.shadowBottom,
541
- {
542
- height: scrollShadowSize,
543
- backgroundColor: 'transparent',
544
- shadowColor: scrollShadowColor,
545
- }
546
- ]} />
547
- )}
548
- </View>
549
- </ScrollView>
883
+ {/* 由于 column stats 和 row stats 互斥,这里不需要底部占位 */}
884
+ </Animated.View>
885
+ )}
886
+ </View>
550
887
 
551
888
  {/* Pagination */}
552
889
  {pagination !== false && pagination && (
553
- <View style={styles.paginationContainer}>
890
+ <View style={[styles.paginationContainer, themedStyles.paginationContainer]}>
554
891
  <Pagination
555
892
  totalCount={`${pagination.total} items`}
556
893
  pageParams={{
@@ -571,93 +908,58 @@ export function StatisticsTable({
571
908
 
572
909
  const styles = StyleSheet.create({
573
910
  container: {
574
- backgroundColor: '#FFFFFF',
575
- borderRadius: 8,
911
+ borderRadius: 12,
576
912
  overflow: 'hidden',
577
913
  },
914
+ tableContainer: {
915
+ flexDirection: 'row',
916
+ },
578
917
  header: {
579
918
  flexDirection: 'row',
580
- backgroundColor: '#F5F5F5',
581
- borderBottomWidth: 2,
582
- borderBottomColor: '#E0E0E0',
919
+ borderBottomWidth: 1,
583
920
  },
584
921
  headerCell: {
585
- padding: 12,
586
- borderRightWidth: 1,
587
- borderRightColor: '#E0E0E0',
922
+ paddingVertical: 16,
923
+ paddingHorizontal: 12,
588
924
  justifyContent: 'center',
589
925
  alignItems: 'center',
590
926
  },
591
- headerCellLeft: {
592
- alignItems: 'flex-start',
593
- },
594
- headerCellRight: {
595
- alignItems: 'flex-end',
596
- },
597
927
  headerText: {
598
928
  fontSize: 14,
599
- fontWeight: '600',
600
- color: '#2C3E50',
929
+ fontWeight: '500',
601
930
  textAlign: 'center',
602
931
  },
603
- headerTextLeft: {
604
- textAlign: 'left',
605
- },
606
- headerTextRight: {
607
- textAlign: 'right',
608
- },
609
932
  row: {
610
933
  flexDirection: 'row',
611
934
  borderBottomWidth: 1,
612
- borderBottomColor: '#F0F0F0',
613
- backgroundColor: '#FFFFFF',
614
- },
615
- statsRow: {
616
- backgroundColor: '#F9F9F9',
617
935
  },
618
936
  cell: {
619
- padding: 12,
937
+ paddingVertical: 16,
938
+ paddingHorizontal: 12,
620
939
  justifyContent: 'center',
621
940
  alignItems: 'center',
622
- borderRightWidth: 1,
623
- borderRightColor: '#F0F0F0',
624
- },
625
- cellLeft: {
626
- alignItems: 'flex-start',
627
- },
628
- cellRight: {
629
- alignItems: 'flex-end',
630
941
  },
631
942
  cellContentContainer: {
632
943
  width: '100%',
944
+ alignItems: 'center',
633
945
  },
634
946
  rowLabel: {
635
947
  fontSize: 14,
636
948
  fontWeight: '500',
637
- color: '#2C3E50',
638
949
  },
639
950
  cellQuantity: {
640
- fontSize: 14,
641
- fontWeight: '500',
642
- color: '#2C3E50',
643
- marginBottom: 4,
644
- textAlign: 'left',
645
- },
646
- cellQuantityRight: {
647
- textAlign: 'right',
951
+ fontSize: 16,
952
+ fontWeight: '600',
953
+ marginBottom: 2,
954
+ textAlign: 'center',
648
955
  },
649
956
  cellAmount: {
650
957
  fontSize: 12,
651
- color: '#7F8C8D',
652
- textAlign: 'left',
653
- },
654
- cellAmountRight: {
655
- textAlign: 'right',
958
+ textAlign: 'center',
656
959
  },
657
960
  statsLabel: {
658
961
  fontSize: 14,
659
962
  fontWeight: '600',
660
- color: '#2C3E50',
661
963
  },
662
964
  actionContainer: {
663
965
  flexDirection: 'row',
@@ -667,13 +969,11 @@ const styles = StyleSheet.create({
667
969
  actionButton: {
668
970
  paddingHorizontal: 12,
669
971
  paddingVertical: 6,
670
- backgroundColor: '#FF6B00',
671
972
  borderRadius: 4,
672
973
  },
673
974
  actionButtonText: {
674
975
  fontSize: 12,
675
976
  fontWeight: '600',
676
- color: '#FFFFFF',
677
977
  },
678
978
  loadingRow: {
679
979
  padding: 40,
@@ -687,57 +987,48 @@ const styles = StyleSheet.create({
687
987
  },
688
988
  emptyText: {
689
989
  fontSize: 14,
690
- color: '#999',
691
990
  },
692
991
  paginationContainer: {
693
992
  padding: 16,
694
993
  borderTopWidth: 1,
695
- borderTopColor: '#E0E0E0',
696
- backgroundColor: '#FAFAFA',
697
994
  },
698
- // 滚动阴影
699
- shadowLeft: {
995
+ // Row Stats 容器
996
+ rowStatsContainer: {
997
+ position: 'relative',
998
+ },
999
+ // Column Stats 容器
1000
+ columnStatsContainer: {
1001
+ // 顶部边框由 themedStyles.columnStatsDivider 提供
1002
+ },
1003
+ // 垂直阴影条
1004
+ shadowBar: {
700
1005
  position: 'absolute',
1006
+ zIndex: 10,
1007
+ width: 1,
1008
+ backgroundColor: 'transparent',
1009
+ },
1010
+ shadowLeft: {
701
1011
  left: 0,
702
1012
  top: 0,
703
1013
  bottom: 0,
704
- zIndex: 10,
705
- shadowOffset: { width: 2, height: 0 },
706
- shadowOpacity: 0.15,
707
- shadowRadius: 4,
708
- elevation: 4,
709
1014
  },
710
1015
  shadowRight: {
711
- position: 'absolute',
712
- right: 0,
713
1016
  top: 0,
714
1017
  bottom: 0,
715
- zIndex: 10,
716
- shadowOffset: { width: -2, height: 0 },
717
- shadowOpacity: 0.15,
718
- shadowRadius: 4,
719
- elevation: 4,
720
1018
  },
721
- shadowTop: {
1019
+ // 水平阴影条
1020
+ shadowBarHorizontal: {
722
1021
  position: 'absolute',
1022
+ zIndex: 10,
1023
+ height: 1,
723
1024
  left: 0,
724
1025
  right: 0,
725
- top: 0,
726
- zIndex: 10,
727
- shadowOffset: { width: 0, height: 2 },
728
- shadowOpacity: 0.15,
729
- shadowRadius: 4,
730
- elevation: 4,
1026
+ backgroundColor: 'transparent',
1027
+ },
1028
+ shadowTop: {
1029
+ top: 56, // header height
731
1030
  },
732
1031
  shadowBottom: {
733
- position: 'absolute',
734
- left: 0,
735
- right: 0,
736
- bottom: 0,
737
- zIndex: 10,
738
- shadowOffset: { width: 0, height: -2 },
739
- shadowOpacity: 0.15,
740
- shadowRadius: 4,
741
- elevation: 4,
1032
+ // bottom 由动态样式设置
742
1033
  },
743
1034
  });