Files
stock/serve/frontend/src/components/stock-table/StockDataTable.tsx
T
2026-07-04 16:59:25 +08:00

121 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 股票列表表格骨架(自选/策略页共享)。
*
* 职责:表头渲染(读列配置 label/align + 可排序三态指示器)、表体遍历、sticky 表头。
* 不内置任何业务逻辑:单元格内容(含 symbol 列交互、操作列、ext 列)由调用方通过
* renderCell / renderExtraCol 注入。这样两个页面的特有交互得以保留,同时表头能力一致。
*/
import type { ReactNode } from 'react'
import type { ColumnConfig } from '@/lib/list-columns'
import { UNSORTABLE_KEYS } from '@/lib/stock-table'
import type { SortState } from './useTableSort'
export type { SortState }
export interface StockDataTableProps {
columns: ColumnConfig[]
rows: any[]
/** 单元格渲染回调。返回 null 时回退到内置纯数据列渲染。 */
renderCell: (r: any, col: ColumnConfig) => ReactNode
/** 行 key(默认取 r.symbol */
rowKey?: (r: any) => string | number
/** 行 className(默认含 hover */
rowClassName?: (r: any) => string
/** 表头是否 sticky(自选页需要,策略页不需要) */
headerSticky?: boolean
/** 最小表格宽度,默认按列数计算 */
minWidth?: number
/** 排序:外部受控时传入(含当前 sort 与 toggle);不传则表头不可排序 */
sort?: SortState | null
onSortToggle?: (colId: string) => void
/** 追加在每行末尾的额外单元格(如自选页的操作列) */
renderExtraCol?: (r: any) => ReactNode
/** 追加的表头单元格(对应 renderExtraCol */
extraHeader?: ReactNode
/** 自定义表头单元格内容覆盖(如日k眼睛按钮)。返回 undefined 则用 col.label */
renderHeaderContent?: (col: ColumnConfig) => ReactNode | undefined
/** 外层容器 className */
className?: string
}
function alignThClass(align: ColumnConfig['align']): string {
if (align === 'right') return 'px-3 py-2.5 font-medium text-right'
if (align === 'center') return 'px-3 py-2.5 font-medium text-center'
return 'px-3 py-2.5 font-medium'
}
export function StockDataTable({
columns, rows, renderCell,
rowKey = (r: any) => r.symbol,
rowClassName = () => 'border-t border-border hover:bg-elevated/50',
headerSticky = false,
minWidth,
sort,
onSortToggle,
renderExtraCol,
extraHeader,
renderHeaderContent,
className = 'rounded-card border border-border overflow-x-auto',
}: StockDataTableProps) {
const visibleColumns = columns.filter(c => c.visible)
const computedMinWidth = minWidth ?? Math.max(900, visibleColumns.length * 110)
const isColSortable = (col: ColumnConfig): boolean => {
// 排序能力由调用方是否提供 onSortToggle 决定;sort 是否为 null 只影响当前指示器
if (!onSortToggle) return false
if (col.source.type === 'builtin' && UNSORTABLE_KEYS.has(col.source.key)) return false
return true
}
const theadClass = headerSticky
? 'sticky top-0 z-10 bg-surface after:absolute after:inset-x-0 after:bottom-0 after:h-px after:bg-border'
: 'bg-elevated'
return (
<div className={className}>
<table className="w-full text-sm" style={{ minWidth: computedMinWidth }}>
<thead className={theadClass}>
<tr className="text-left text-secondary">
{visibleColumns.map(col => {
const sortable = isColSortable(col)
const isSorted = sort?.key === col.id
const dir = isSorted ? sort!.dir : null
const contentOverride = renderHeaderContent?.(col)
return (
<th
key={col.id}
className={`${alignThClass(col.align)} ${sortable ? 'cursor-pointer select-none group' : ''}`}
onClick={sortable ? () => onSortToggle!(col.id) : undefined}
>
{contentOverride !== undefined ? contentOverride : col.label}
{sortable && (
<span className="inline-block ml-1 text-[10px] opacity-30 group-hover:opacity-60 transition-opacity">
{isSorted ? (dir === 'asc' ? '↑' : '↓') : '↕'}
</span>
)}
</th>
)
})}
{extraHeader && (
<th className="px-3 py-2.5 font-medium text-right">{extraHeader}</th>
)}
</tr>
</thead>
<tbody>
{rows.map((r: any) => {
return (
<tr
key={rowKey(r)}
className={`transition-colors duration-150 ease-smooth group ${rowClassName(r)}`}
>
{visibleColumns.map(col => renderCell(r, col))}
{renderExtraCol && renderExtraCol(r)}
</tr>
)
})}
</tbody>
</table>
</div>
)
}