From ac92b3e70be3014ccc3ea415b63130f38a7d33c0 Mon Sep 17 00:00:00 2001 From: yuxuanhui Date: Thu, 10 Sep 2026 13:31:42 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E5=BC=8F=20Grid=20=E5=B1=95=E7=A4=BA=20Alpha=20=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E5=B9=B6=E7=8B=AC=E7=AB=8B=E6=8E=92=E5=88=97=20JSON?= =?UTF-8?q?=20=E5=88=86=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/AlphaDetail.tsx | 88 ++------------- frontend/src/components/DetailFieldGrid.tsx | 82 ++++++++++++++ .../src/components/SelfCorrelationPanel.tsx | 8 +- frontend/src/style.css | 65 +++++++++--- frontend/tests/alpha-management.spec.ts | 100 ++++++++++++++---- 5 files changed, 225 insertions(+), 118 deletions(-) create mode 100644 frontend/src/components/DetailFieldGrid.tsx diff --git a/frontend/src/components/AlphaDetail.tsx b/frontend/src/components/AlphaDetail.tsx index a6dc206..303d2eb 100644 --- a/frontend/src/components/AlphaDetail.tsx +++ b/frontend/src/components/AlphaDetail.tsx @@ -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({ >
- ({ key, - value: ( - - ), + value: detail[key as keyof Detail], }))} />

@@ -266,14 +257,10 @@ export function AlphaDetail({ )}

回测设置

- ({ key, - value: , + value, }))} />
@@ -494,15 +481,8 @@ function MetricTable({

{title}

{entries.length ? ( - ({ - key, - value: , - }))} + ({ key, value }))} /> ) : (

未提供

@@ -510,57 +490,3 @@ function MetricTable({
); } - -/** 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 ? ( - ({ - key, - value: , - }))} - /> - ) : ( - {Array.isArray(value) ? "[]" : "{}"} - ); - } - // 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 ( - - {typeof value === "boolean" - ? value - ? "是" - : "否" - : displayValue(value)} - - ); - } - return {displayValue(value)}; -} diff --git a/frontend/src/components/DetailFieldGrid.tsx b/frontend/src/components/DetailFieldGrid.tsx new file mode 100644 index 0000000..21154ac --- /dev/null +++ b/frontend/src/components/DetailFieldGrid.tsx @@ -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 ( +
+
+ {data.map(({ key, value }) => { + const structured = value !== null && typeof value === "object"; + return ( +
+
{key}
+
+ {structured ? ( + Object.keys(value).length ? ( + ({ + key: Array.isArray(value) ? `[${name}]` : name, + value: child, + }))} + /> + ) : ( + + {Array.isArray(value) ? "[]" : "{}"} + + ) + ) : ( + + )} +
+
+ ); + })} +
+
+ ); +} + +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 ( + + {typeof value === "boolean" + ? value + ? "是" + : "否" + : displayValue(value)} + + ); + } + return {displayValue(value)}; +} diff --git a/frontend/src/components/SelfCorrelationPanel.tsx b/frontend/src/components/SelfCorrelationPanel.tsx index f104dd2..a70334f 100644 --- a/frontend/src/components/SelfCorrelationPanel.tsx +++ b/frontend/src/components/SelfCorrelationPanel.tsx @@ -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)} - 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); + } } diff --git a/frontend/tests/alpha-management.spec.ts b/frontend/tests/alpha-management.spec.ts index 5101948..ef0d6fa 100644 --- a/frontend/tests/alpha-management.spec.ts +++ b/frontend/tests/alpha-management.spec.ts @@ -95,11 +95,31 @@ test("submission tabs, day selection, local correlation and reload", async ({ await page.route("**/api/v1/alphas/TEST0004", async (route) => { const response = await route.fetch(); const detail = await response.json(); - detail.is_metrics.nested = { + detail.is_metrics.glbAmer = { yearly: { sharpe: 1.25, status: "PASS" }, samples: 120, enabled: false, }; + detail.is_metrics.glbApac = { + pnl: 439696, + bookSize: 20000000, + longCount: 215, + turnover: 0.0242, + sharpe: 0.85, + }; + detail.is_metrics.glbEmea = { pnl: 571980, fitness: 0.19, sharpe: 0.72 }; + detail.is_metrics.investabilityConstrained = { + pnl: 3696382, + longCount: 890, + turnover: 0.0919, + }; + detail.is_metrics.history = [ + { status: "PASS", sharpe: 1.1 }, + { status: "FAIL", sharpe: 0.2 }, + ]; + detail.is_metrics.emptyObject = {}; + detail.is_metrics.emptyArray = []; + detail.is_metrics.unavailable = null; await route.fulfill({ response, json: detail }); }); await page.locator(".alpha-link").click(); @@ -123,31 +143,73 @@ test("submission tabs, day selection, local correlation and reload", async ({ const metrics = sheet.locator(".detail-section").filter({ has: page.getByRole("heading", { name: "IS 指标", exact: true }), }); - await expect(metrics.locator(".nested-descriptions").first()).toContainText( - "1.25", - ); await expect( metrics - .locator(".semi-descriptions-horizontal > table > tbody > tr") - .first() - .locator(":scope > th"), - ).toHaveCount(3); + .locator(".detail-field-group") + .filter({ has: page.locator("dt", { hasText: "yearly" }) }) + .first(), + ).toContainText("1.25"); await expect( - metrics.locator(".semi-tag").filter({ hasText: "PASS" }), + metrics.locator(".semi-tag").filter({ hasText: "PASS" }).first(), ).toBeVisible(); await expect( metrics.locator(".semi-tag").filter({ hasText: "否" }), ).toBeVisible(); - expect( - await metrics - .locator("th, td") - .evaluateAll((cells) => - cells.every((cell) => getComputedStyle(cell).textAlign === "left"), - ), - ).toBe(true); - await page.screenshot({ - path: "../output/playwright/alpha-detail-metrics.png", - }); + await expect(metrics).toContainText("[]"); + await expect(metrics).toContainText("{}"); + await expect(metrics).toContainText("未提供"); + for (const [viewportWidth, columns] of [ + [1440, 3], + [900, 2], + [600, 1], + [390, 1], + ]) { + await page.setViewportSize({ width: viewportWidth, height: 1000 }); + const grid = metrics.locator( + ":scope > .detail-field-container > .detail-field-grid", + ); + await expect + .poll(() => + grid.evaluate( + (element) => + getComputedStyle(element).gridTemplateColumns.split(" ").length, + ), + ) + .toBe(columns); + const layout = await metrics.evaluate((element) => { + const groups = Array.from( + element.querySelectorAll(".detail-field-group"), + ); + return { + fullRows: groups.every((group) => { + const box = group.getBoundingClientRect(); + const parent = group.parentElement!.getBoundingClientRect(); + return ( + Math.abs(box.x - parent.x) < 1 && + Math.abs(box.width - parent.width) < 1 + ); + }), + leftAligned: Array.from(element.querySelectorAll("dt, dd")).every( + (cell) => getComputedStyle(cell).textAlign === "left", + ), + noOverflow: Array.from( + element.querySelectorAll( + ".detail-field-container, dt, dd", + ), + ).every((cell) => cell.scrollWidth <= cell.clientWidth + 1), + }; + }); + expect(layout).toEqual({ + fullRows: true, + leftAligned: true, + noOverflow: true, + }); + await page.screenshot({ + path: `../output/playwright/alpha-grid-${viewportWidth}.png`, + animations: "disabled", + }); + } + await page.setViewportSize({ width: 1440, height: 1000 }); await sheet.getByRole("tab", { name: "变体关系", exact: true }).click(); await expect( sheet.getByRole("button", { name: "研究此 Alpha 的变体" }),