121 lines
4.7 KiB
TypeScript
121 lines
4.7 KiB
TypeScript
/**
|
||
* 股票列表表格骨架(自选/策略页共享)。
|
||
*
|
||
* 职责:表头渲染(读列配置 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>
|
||
)
|
||
}
|