将项目文件整理到 refer 目录

This commit is contained in:
2026-07-03 22:26:00 +08:00
parent 49880e785f
commit 9f57af4d37
1223 changed files with 98 additions and 0 deletions
@@ -0,0 +1,120 @@
/**
* 股票列表表格骨架(自选/策略页共享)。
*
* 职责:表头渲染(读列配置 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>
)
}