修复实验页右侧内容溢出:宽表固定两端 + 提示;对比表首列固定;月度表收紧内边距
- 实验列表 11 列在 1366px 及更窄窗口放不下,「操作」列被推到卡片外;macOS 覆盖式 滚动条不滚动不显示,看起来就是「右侧内容溢出/被切掉」。现在左右两端固定 (选择列、ID、操作列),列宽按实测单行内容宽度定,只有真溢出时才提示可横向滚动。 - 对比视图(指标对比 / 参数差异)列数随实验个数增长,首列改为固定,同样按需提示。 - 详情图层里的月度收益表 13 列超出 38px:收紧内边距并固定「年份」列。 - 可排序表头命中区 19.2px → 34px(与同排复选框等高),修掉 UI 规范门禁的 7 项失败。 - 顺手把对比区两处会原样显示的字面 ** 改成「」。 门禁:ruff 通过;pytest 524 passed;tsc 0 error;npm run build OK;test:charts 7 passed; verify_ui_alignment.py 160 项通过 / 0 失败。
This commit is contained in:
@@ -160,10 +160,37 @@ export default function ExperimentsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 宽表此刻是否需要横向滚动(按实测 `scrollWidth > clientWidth`,容器尺寸变化时重算)。
|
||||
*
|
||||
* 为什么要提示:卡片内的横向滚动条在 macOS 上是覆盖式的(不滚就不显示),
|
||||
* 用户看到的就是「右侧内容被切掉了」——这正是「experiments 页面右侧内容溢出了」的来源。
|
||||
* 为什么要在运行时判断而不是写死一句提示:列宽固定,窗口够宽时根本不需要滚动,
|
||||
* 那种情况下还挂着提示就是废话(还让人以为页面坏了)。
|
||||
*/
|
||||
function useTableScrollHint(deps: React.DependencyList) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [scrolls, setScrolls] = useState(false);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const check = () => setScrolls(el.scrollWidth > el.clientWidth + 1);
|
||||
check();
|
||||
const ro = new ResizeObserver(check);
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
// deps 由调用方给(行数 / 筛选变化时要重新量)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps);
|
||||
return { ref, scrolls };
|
||||
}
|
||||
|
||||
function ExperimentsInner() {
|
||||
const search = useSearchParams();
|
||||
const [exps, setExps] = useState<ExperimentMeta[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
// 列表容器:窄窗口下宽表要横向滚动,提示只在真需要时出现
|
||||
const { ref: tableWrapRef, scrolls: tableScrolls } = useTableScrollHint([exps.length, loading]);
|
||||
const [detail, setDetail] = useState<ExperimentDetail | null>(null);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [detailError, setDetailError] = useState("");
|
||||
@@ -551,11 +578,21 @@ function ExperimentsInner() {
|
||||
icon="archive"
|
||||
title="实验列表"
|
||||
sub={
|
||||
total !== null
|
||||
? `显示 ${exps.length} 条 / 共 ${total} 条${
|
||||
total > exps.length ? "(后端单页上限内未列全,可用搜索或类型筛选缩小范围)" : ""
|
||||
}`
|
||||
: "第一列勾选可批量删除;「对比」列勾选 2~3 个可叠加曲线对比"
|
||||
<span>
|
||||
{total !== null
|
||||
? `显示 ${exps.length} 条 / 共 ${total} 条${
|
||||
total > exps.length ? "(后端单页上限内未列全,可用搜索或类型筛选缩小范围)" : ""
|
||||
}`
|
||||
: "第一列勾选可批量删除;「对比」列勾选 2~3 个可叠加曲线对比"}
|
||||
{tableScrolls ? (
|
||||
// 只在真的放不下时才说:卡片内的横向滚动条容易看不见(触控板尤其),
|
||||
// 而右端「操作」列已固定,用户不必先滚到底才知道能点哪里。
|
||||
<>
|
||||
{" "}
|
||||
<span className="hint">列较多,可横向滚动;选择列与「操作」列固定不动。</span>
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
}
|
||||
tools={
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
@@ -612,14 +649,13 @@ function ExperimentsInner() {
|
||||
hint="在「选股回测」或「因子研究」中运行一次,即会自动归档于此。"
|
||||
/>
|
||||
) : (
|
||||
<div className="table-wrap">
|
||||
<table className="tbl">
|
||||
<div className="table-wrap table-wrap--wide" ref={tableWrapRef}>
|
||||
<table className="tbl tbl--wide">
|
||||
<thead>
|
||||
<tr>
|
||||
{/* 第一列:批量删除的选择(表头是全选本页)。与「对比」列分开,
|
||||
因为两者用途/上限完全不同(删除可多选且不可逆,对比最多 3 个)。 */}
|
||||
<th
|
||||
style={{ width: 36 }}
|
||||
title={
|
||||
total !== null && total > rows.length
|
||||
? `全选本页(本页 ${rows.length} 个;筛选结果共 ${total} 个,本页之外的未列出,不会被删)`
|
||||
@@ -635,7 +671,7 @@ function ExperimentsInner() {
|
||||
/>
|
||||
</label>
|
||||
</th>
|
||||
<th style={{ width: 36 }}>
|
||||
<th>
|
||||
<span className="hint" title="勾选这一列可挑 2~3 个实验做对比">
|
||||
对比
|
||||
</span>
|
||||
@@ -659,7 +695,9 @@ function ExperimentsInner() {
|
||||
<th>摘要</th>
|
||||
<th>版本(代码 / 数据)</th>
|
||||
<th>体积</th>
|
||||
<th style={{ width: 260, textAlign: "right" }}>操作</th>
|
||||
{/* 列宽由 .tbl--wide 统一给(含固定右端):行内 width 会盖掉 CSS,
|
||||
曾经就是它让「操作」列保持 260px、整表多出 88px 放不下 */}
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -686,9 +724,11 @@ function ExperimentsInner() {
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
<td className="cell-mono cell-strong">{e.id}</td>
|
||||
<td className="cell-mono cell-strong" title={e.id}>
|
||||
{e.id}
|
||||
</td>
|
||||
<td>{kindView(e.kind)}</td>
|
||||
<td className="cell-mono nowrap" title={e.created_at ?? "该归档没有记录发起时间"}>
|
||||
<td className="cell-mono" title={e.created_at ?? "该归档没有记录发起时间"}>
|
||||
{fmtLocalTime(e.created_at)}
|
||||
</td>
|
||||
<td>
|
||||
@@ -700,7 +740,12 @@ function ExperimentsInner() {
|
||||
))}
|
||||
</span>
|
||||
</td>
|
||||
<td className="cell-mono">{e.period ? `${e.period[0]}~${e.period[1]}` : "—"}</td>
|
||||
<td
|
||||
className="cell-mono"
|
||||
title={e.period ? `区间 ${e.period[0]} ~ ${e.period[1]}` : "该归档没有记录区间"}
|
||||
>
|
||||
{e.period ? `${e.period[0]} ~ ${e.period[1]}` : "—"}
|
||||
</td>
|
||||
<td className="hint">{e.summary_text ?? "—"}</td>
|
||||
<td>
|
||||
<span className="row" style={{ gap: 4 }}>
|
||||
@@ -924,6 +969,11 @@ function CompareView({
|
||||
setOnlyDiff: (v: boolean) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
// 指标对比 / 参数差异两张表都是「一行一个指标 + 每个实验一列」:列数随对比个数增长,
|
||||
// 窄窗口必然横向滚动。首列(指标名 / 参数名)必须固定,否则滚到右边不知道这一行是什么。
|
||||
const metricTable = useTableScrollHint([rows.length]);
|
||||
const paramTable = useTableScrollHint([rows.length, onlyDiff]);
|
||||
|
||||
const series = useMemo<LwSeries[]>(
|
||||
() =>
|
||||
rows.map((r, i) => ({
|
||||
@@ -978,12 +1028,12 @@ function CompareView({
|
||||
ariaLabel="实验净值曲线叠加对比"
|
||||
/>
|
||||
<div className="hint" style={{ marginTop: 6 }}>
|
||||
曲线已归一化为**累计收益率 %**(各自以首个净值为 0%),因此初始资金不同也能直接比。
|
||||
曲线已归一化为「累计收益率 %」(各自以首个净值为 0%),因此初始资金不同也能直接比。
|
||||
点击图例可临时隐藏某条曲线(只算收益差的实验建议隐藏基准再比)。
|
||||
</div>
|
||||
|
||||
<div className="table-wrap" style={{ marginTop: 14 }}>
|
||||
<table className="tbl">
|
||||
<div className="table-wrap" style={{ marginTop: 14 }} ref={metricTable.ref}>
|
||||
<table className="tbl tbl--pin-first">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>指标</th>
|
||||
@@ -1028,18 +1078,19 @@ function CompareView({
|
||||
差额列 = 最后一个实验 − 基准({rows[0].id})。✅ 表示该指标改善方向,⚠️ 表示变差
|
||||
(收益/夏普/胜率越高越好,回撤/波动越低越好)。基准区间收益{" "}
|
||||
{base.total_return_pct.toFixed(2)}%。
|
||||
{metricTable.scrolls ? " 列较多,可横向滚动;首列「指标」固定不动。" : ""}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<b style={{ fontSize: 13 }}>参数差异(config_snapshot 逐项对比)</b>
|
||||
{diffRows.length === 0 ? (
|
||||
<div className="hint" style={{ marginTop: 6 }}>
|
||||
这几个实验的参数快照完全一致(说明结果是**同一配置**下的数据变动,或只是复跑)。
|
||||
这几个实验的参数快照完全一致(说明结果是「同一配置」下的数据变动,或只是复跑)。
|
||||
已排除 price_basis.* 运行元数据。
|
||||
</div>
|
||||
) : (
|
||||
<div className="table-wrap" style={{ marginTop: 6 }}>
|
||||
<table className="tbl">
|
||||
<div className="table-wrap" style={{ marginTop: 6 }} ref={paramTable.ref}>
|
||||
<table className="tbl tbl--pin-first">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>参数</th>
|
||||
@@ -1068,9 +1119,10 @@ function CompareView({
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{onlyDiff ? (
|
||||
{onlyDiff || paramTable.scrolls ? (
|
||||
<div className="hint" style={{ marginTop: 6 }}>
|
||||
已隐藏所有实验取值相同的参数(取消上方勾选可看全量)。
|
||||
{onlyDiff ? "已隐藏所有实验取值相同的参数(取消上方勾选可看全量)。" : null}
|
||||
{paramTable.scrolls ? " 参数表列较多,可横向滚动;首列「参数」固定不动。" : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user