From 494490f6e637c2a852e311caac70e355dddd5dc1 Mon Sep 17 00:00:00 2001 From: Jayway Liu <14022498+bluejayway@user.noreply.gitee.com> Date: Sun, 11 Oct 2026 09:41:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(display):=20=E8=AF=81=E6=8D=AE=E9=87=91?= =?UTF-8?q?=E9=A2=9D=E5=8E=BB=E5=B0=BE=E9=9B=B6=E4=B8=8E=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 证据接口与 CSV/XLSX 导出去掉 NUMERIC(44,24) 存储尺度尾零(无损精度) - KPI 大金额折行截断:字号收紧 + nowrap + 省略号兜底 - 贡献分解/走势图表轴标签重叠与 Y 轴裁剪:hideOverlap + 万/亿缩写 - 表格隐藏全不可用指标列,空态改为条件渲染(修复空表覆盖层残留) - 数据质量与字段字典/导入确认的中文标签映射(不再暴露内部键名) - 证据弹窗重复点击当前期间停在加载中(F03);证据表格最小宽度 - Playwright 覆盖 F03 重复点击与期间切换场景 --- finance/views.py | 17 +++- frontend/src/App.tsx | 180 ++++++++++++++++++++---------------- frontend/src/dialogs.tsx | 8 +- frontend/src/style.css | 29 ++++-- frontend/src/types.ts | 14 +++ tests/e2e/workspace.spec.ts | 22 ++++- tests/test_finance.py | 33 ++++++- 7 files changed, 206 insertions(+), 97 deletions(-) diff --git a/finance/views.py b/finance/views.py index 8034905..e4c5310 100644 --- a/finance/views.py +++ b/finance/views.py @@ -297,6 +297,14 @@ def query(request, dataset_id): return JsonResponse(analyze(ds, request.member, body(request), pref)) +def amount_text(value): + """金额展示文本:去掉定点数存储尺度产生的多余尾零(如 10.000000000000000000000000 → 10)。""" + if value is None: + return None + text = format(value.normalize(), "f") + return "0" if text == "-0" else text + + @endpoint(("POST",)) def evidence(request, dataset_id): ds = dataset(request, dataset_id, True) @@ -314,6 +322,9 @@ def evidence(request, dataset_id): "source_row", "period", "dimensions", "revenue", "cost", "gross", "raw" ) ) + for row in rows: + for field in ("revenue", "cost", "gross"): + row[field] = amount_text(row[field]) # Raw patient/customer identifiers require an explicit administrator role. if request.member.role != "admin": for row in rows: @@ -412,9 +423,9 @@ def export(request, dataset_id): row.source_row, row.period, *[row.dimensions.get(d["key"], "") for d in dims], - row.revenue, - row.cost, - row.gross, + amount_text(row.revenue), + amount_text(row.cost), + amount_text(row.gross), ] ) if workbook: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d4b2b01..0639361 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -45,6 +45,7 @@ import { import { format, labels, + roleLabels, type Analysis, type Context, type Dataset, @@ -308,6 +309,23 @@ export function App() { setPath([]); setModal(null); } + // 全部分组都为空的指标列(如数据集没有成本/ROI)不展示,避免占位挤压可用列。 + const metricKeys = useMemo(() => { + const candidates = [ + ...new Set([ + "revenue", + "gross", + "cost", + "roi", + "customers", + ctx?.metric || "revenue", + ]), + ]; + const available = candidates.filter( + (m) => m === "revenue" || (result?.rows || []).some((r) => r.values[m]?.current != null), + ); + return available.length ? available : (["revenue"] as Metric[]); + }, [result, ctx]); const columns = useMemo[]>( () => [ { @@ -327,23 +345,14 @@ export function App() { ), }, - ...[ - ...new Set([ - "revenue", - "gross", - "cost", - "roi", - "customers", - ctx?.metric || "revenue", - ]), - ].map((metric) => ({ + ...metricKeys.map((metric) => ({ colId: metric, headerName: labels[metric], minWidth: 130, valueGetter: (p: { data?: Row }) => - p.data?.values[metric].current == null + p.data?.values[metric]?.current == null ? null - : Number(p.data.values[metric].current), + : Number(p.data.values[metric]?.current), valueFormatter: (p: { value: unknown }) => format(p.value as number, metric), cellClass: metric === ctx?.metric ? "metric-column" : "", @@ -396,24 +405,34 @@ export function App() { ), }, ], - [ds, ctx, path], + [ds, ctx, path, metricKeys], ); const rows = (result?.rows || []).filter( (r) => (screen !== "anomalies" || r.anomaly) && r.label.includes(search), ); const metric = ctx?.metric || "revenue"; + const compactAxis = (value: number) => { + const abs = Math.abs(value); + if (abs >= 1e8) return `${value / 1e8}亿`; + if (abs >= 1e4) return `${value / 1e4}万`; + return String(value); + }; const trend = useMemo( () => ({ color: ["#197568", "#5794c9"], tooltip: { trigger: "axis" }, legend: { bottom: 0 }, - grid: { top: 25, left: 65, right: 25, bottom: 65 }, + grid: { top: 25, left: 85, right: 25, bottom: 65 }, xAxis: { type: "category", data: result?.trend.map((r) => r.period), - axisLabel: { color: "#73827b" }, + axisLabel: { color: "#73827b", hideOverlap: true }, + }, + yAxis: { + type: "value", + splitLine: { lineStyle: { color: "#ecf0ee" } }, + axisLabel: { hideOverlap: true }, }, - yAxis: { type: "value", splitLine: { lineStyle: { color: "#ecf0ee" } } }, series: [ { name: "实际收入", @@ -443,7 +462,11 @@ export function App() { () => ({ tooltip: { trigger: "axis" }, grid: { top: 15, left: 95, right: 30, bottom: 30 }, - xAxis: { type: "value", splitLine: { lineStyle: { color: "#ecf0ee" } } }, + xAxis: { + type: "value", + splitLine: { lineStyle: { color: "#ecf0ee" } }, + axisLabel: { hideOverlap: true, formatter: compactAxis }, + }, yAxis: { type: "category", inverse: true, @@ -878,6 +901,8 @@ export function App() { missing_gross: "毛利缺失", missing_customer: "客户标识缺失", blank_rows: "空白行", + g_l_mismatch: "应收金额两列不一致", + i_n_mismatch: "实收与实际收入不一致", } as Record )[k] || k} {v.toLocaleString()} @@ -1030,16 +1055,11 @@ export function App() { )?.name } - {( - [ - "revenue", - "gross", - "cost", - "roi", - ] as Metric[] - ).map((m) => ( - {labels[m]} - ))} + {metricKeys + .filter((m) => m !== "customers") + .map((m) => ( + {labels[m]} + ))} 状态 @@ -1047,15 +1067,10 @@ export function App() { {result.rows.slice(0, 12).map((r) => ( {r.label} - {( - [ - "revenue", - "gross", - "cost", - "roi", - ] as Metric[] - ).map((m) => { - const v = r.values[m].change; + {metricKeys + .filter((m) => m !== "customers") + .map((m) => { + const v = r.values[m]?.change; return (