fix: 使用响应式 Grid 展示 Alpha 详情并独立排列 JSON 分组
Deploy production / deploy (push) Successful in 35s

This commit is contained in:
yuxuanhui
2026-09-10 13:31:42 +08:00
parent 1ba8d3ff3d
commit ac92b3e70b
5 changed files with 225 additions and 118 deletions
+7 -81
View File
@@ -3,7 +3,6 @@ import {
Banner,
Button,
Checkbox,
Descriptions,
Empty,
Divider,
Input,
@@ -31,6 +30,7 @@ import type {
Research,
ResearchState,
} from "../types";
import { DetailFieldGrid } from "./DetailFieldGrid";
import { PnlChart } from "./PnlChart";
import { SelfCorrelationPanel } from "./SelfCorrelationPanel";
import { SubmissionPanel } from "./SubmissionPanel";
@@ -231,19 +231,10 @@ export function AlphaDetail({
>
<TabPane tab="概览与表达式" itemKey="overview">
<div className="detail-section">
<Descriptions
size="small"
align="left"
layout="horizontal"
column={3}
<DetailFieldGrid
data={["region", "universe", "stage"].map((key) => ({
key,
value: (
<DescriptionValue
field={key}
value={detail[key as keyof Detail]}
/>
),
value: detail[key as keyof Detail],
}))}
/>
<p className="muted">
@@ -266,14 +257,10 @@ export function AlphaDetail({
)}
<Divider />
<h3>回测设置</h3>
<Descriptions
size="small"
align="left"
layout="horizontal"
column={3}
<DetailFieldGrid
data={Object.entries(detail.settings).map(([key, value]) => ({
key,
value: <DescriptionValue field={key} value={value} />,
value,
}))}
/>
</div>
@@ -494,15 +481,8 @@ function MetricTable({
<div className="detail-section">
<h3>{title}</h3>
{entries.length ? (
<Descriptions
size="small"
align="left"
layout="horizontal"
column={3}
data={entries.map(([key, value]) => ({
key,
value: <DescriptionValue field={key} value={value} />,
}))}
<DetailFieldGrid
data={entries.map(([key, value]) => ({ key, value }))}
/>
) : (
<p className="muted">未提供</p>
@@ -510,57 +490,3 @@ function MetricTable({
</div>
);
}
/** Render structured metric values without flattening their field hierarchy. */
function DescriptionValue({ field, value }: { field: string; value: unknown }) {
if (value !== null && typeof value === "object") {
const entries = Object.entries(value);
return entries.length ? (
<Descriptions
className="nested-descriptions"
size="small"
align="left"
data={entries.map(([key, child]) => ({
key,
value: <DescriptionValue field={key} value={child} />,
}))}
/>
) : (
<span>{Array.isArray(value) ? "[]" : "{}"}</span>
);
}
// Use field semantics for states; numbers and ordinary text remain plain values.
const stateField = /(?:^|_)(status|state|stage|result|verdict)$/.test(
field.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase(),
);
const state = typeof value === "string" ? value.toUpperCase() : "";
if (
value !== null &&
value !== undefined &&
value !== "" &&
(typeof value === "boolean" ||
stateField ||
state === "ON" ||
state === "OFF")
) {
const color =
value === true ||
["PASS", "PASSED", "SUCCESS", "COMPLETED", "ON"].includes(state)
? "green"
: ["FAIL", "FAILED", "ERROR", "BLOCK"].includes(state)
? "red"
: ["WARNING", "PENDING", "PARTIAL", "REVIEW"].includes(state)
? "orange"
: "grey";
return (
<Tag size="small" color={color}>
{typeof value === "boolean"
? value
? "是"
: "否"
: displayValue(value)}
</Tag>
);
}
return <span>{displayValue(value)}</span>;
}
@@ -0,0 +1,82 @@
import { Tag } from "@douyinfe/semi-ui-19";
import { displayValue } from "../api";
/** Keep structured values on their own row at every nesting level. */
export function DetailFieldGrid({
data,
}: {
data: { key: string; value: unknown }[];
}) {
return (
<div className="detail-field-container">
<dl className="detail-field-grid">
{data.map(({ key, value }) => {
const structured = value !== null && typeof value === "object";
return (
<div
className={structured ? "detail-field-group" : "detail-field"}
key={key}
>
<dt>{key}</dt>
<dd>
{structured ? (
Object.keys(value).length ? (
<DetailFieldGrid
data={Object.entries(value).map(([name, child]) => ({
key: Array.isArray(value) ? `[${name}]` : name,
value: child,
}))}
/>
) : (
<span className="muted">
{Array.isArray(value) ? "[]" : "{}"}
</span>
)
) : (
<FieldValue field={key} value={value} />
)}
</dd>
</div>
);
})}
</dl>
</div>
);
}
function FieldValue({ field, value }: { field: string; value: unknown }) {
// Use field semantics for states; numbers and ordinary text remain plain values.
const stateField = /(?:^|_)(status|state|stage|result|verdict)$/.test(
field.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase(),
);
const state = typeof value === "string" ? value.toUpperCase() : "";
if (
value !== null &&
value !== undefined &&
value !== "" &&
(typeof value === "boolean" ||
stateField ||
state === "ON" ||
state === "OFF")
) {
const color =
value === true ||
["PASS", "PASSED", "SUCCESS", "COMPLETED", "ON"].includes(state)
? "green"
: ["FAIL", "FAILED", "ERROR", "BLOCK"].includes(state)
? "red"
: ["WARNING", "PENDING", "PARTIAL", "REVIEW"].includes(state)
? "orange"
: "grey";
return (
<Tag size="small" color={color}>
{typeof value === "boolean"
? value
? "是"
: "否"
: displayValue(value)}
</Tag>
);
}
return <span>{displayValue(value)}</span>;
}
@@ -2,7 +2,6 @@ import { useEffect, useState } from "react";
import {
Banner,
Button,
Descriptions,
Empty,
Spin,
Table,
@@ -10,6 +9,7 @@ import {
Toast,
} from "@douyinfe/semi-ui-19";
import { api, correlationLabels, formatNumber, formatTime, post } from "../api";
import { DetailFieldGrid } from "./DetailFieldGrid";
import type { CorrelationResult } from "../types";
export function SelfCorrelationPanel({
@@ -108,11 +108,7 @@ export function SelfCorrelationPanel({
计算于 {formatTime(result.calculated_at, timezone)}
</span>
</div>
<Descriptions
size="small"
align="left"
layout="horizontal"
column={3}
<DetailFieldGrid
data={[
{
key: "最大相关系数",
+53 -12
View File
@@ -919,20 +919,61 @@ label:not(.semi-checkbox):not(.semi-radio) {
.alpha-detail .semi-divider-horizontal {
margin: 16px 0;
}
.alpha-detail .semi-descriptions table {
table-layout: fixed;
width: 100%;
/* Container queries follow the available drawer width, including chat offsets. */
.detail-field-container {
container-type: inline-size;
min-width: 0;
}
.alpha-detail .semi-descriptions th,
.alpha-detail .semi-descriptions td {
padding-bottom: 6px;
vertical-align: top;
overflow-wrap: anywhere;
.detail-field-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px 24px;
margin: 0;
padding: 0;
text-align: left;
}
.alpha-detail .nested-descriptions {
margin-bottom: 0;
.detail-field {
display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
align-items: start;
gap: 12px;
min-width: 0;
}
.alpha-detail .nested-descriptions th {
width: 45%;
.detail-field-grid dt,
.detail-field-grid dd {
min-width: 0;
margin: 0;
overflow-wrap: anywhere;
font-size: 13px;
line-height: 20px;
text-align: left;
}
.detail-field > dt {
color: var(--muted);
}
.detail-field-group {
grid-column: 1 / -1;
min-width: 0;
border-top: 1px solid var(--line);
padding-top: 12px;
margin-top: 4px;
}
.detail-field-group > dt {
margin-bottom: 8px;
font-weight: 500;
color: var(--ink);
}
.detail-field-group .detail-field-group {
border-left: 2px solid var(--line);
padding-left: 12px;
}
@container (max-width: 760px) {
.detail-field-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@container (max-width: 480px) {
.detail-field-grid {
grid-template-columns: minmax(0, 1fr);
}
}