持久化与工作簿:F05/F06/F07 定义库并发、说明页字典、对账容差 #3

Open
jianwel wants to merge 12 commits from fix/remaining-issues-2026-09-26 into fix/dialogs-f03-f04
2 changed files with 122 additions and 28 deletions
Showing only changes of commit 6cf236a811 - Show all commits
+28 -5
View File
@@ -1,5 +1,12 @@
import { useEffect, useRef, useState } from "react";
import { ChevronLeft, ChevronRight, Download, Save, Trash2, X } from "lucide-react";
import {
ChevronLeft,
ChevronRight,
Download,
Save,
Trash2,
X,
} from "lucide-react";
import { api } from "./api";
import {
labels,
@@ -349,7 +356,8 @@ export function EvidenceDialog({
}>(),
[side, setSide] = useState<"current" | "baseline">("current"),
[page, setPage] = useState(0),
[error, setError] = useState("");
[error, setError] = useState(""),
[reloadKey, setReloadKey] = useState(0);
useEffect(() => {
let active = true;
setResult(undefined);
@@ -364,8 +372,14 @@ export function EvidenceDialog({
return () => {
active = false;
};
}, [ds.id, ctx, side, page]);
}, [ds.id, ctx, side, page, reloadKey]);
function selectSide(value: "current" | "baseline") {
if (value === side) {
// 重复点击当前期间:仅在不在第一页时回到第一页触发刷新,
// 同期间同页直接返回,避免清空列表后停在“加载中”。
if (page !== 0) setPage(0);
return;
}
setResult(undefined);
setPage(0);
setSide(value);
@@ -401,7 +415,8 @@ export function EvidenceDialog({
{user.role !== "admin" && <p>客户原始标识不包含在明细中。</p>}
{error && (
<p role="alert" className="error">
{error}
{error}{" "}
<button onClick={() => setReloadKey((n) => n + 1)}>重试</button>
</p>
)}
<div className="table-scroll">
@@ -435,6 +450,11 @@ export function EvidenceDialog({
<td>{String(r.gross ?? "")}</td>
</tr>
))}
{result && result.rows.length === 0 && (
<tr>
<td colSpan={3 + ds.dimensions.length}>本期范围内没有数据</td>
</tr>
)}
</tbody>
</table>
</div>
@@ -776,7 +796,10 @@ export function FormulaExportDialog({
);
setDefinitions((items) =>
[...items.filter((item) => item.id !== saved.id), saved].sort((a, b) =>
String(a.definition.name).localeCompare(String(b.definition.name), "zh-CN"),
String(a.definition.name).localeCompare(
String(b.definition.name),
"zh-CN",
),
),
);
setSelectedDefinition(saved.id);
+94 -23
View File
@@ -30,7 +30,9 @@ test("桌面:真实导入、异常、下钻、证据、设置、Copilot、导
});
await uploaded;
const dialog = page.getByRole("dialog");
await expect(dialog.getByRole("heading", { name: "确认导入字段" })).toBeVisible();
await expect(
dialog.getByRole("heading", { name: "确认导入字段" }),
).toBeVisible();
await dialog.getByRole("button", { name: "确认并导入" }).click();
await expect(
page.getByRole("heading", { name: "集团经营总览", exact: true }),
@@ -85,14 +87,27 @@ test("桌面:真实导入、异常、下钻、证据、设置、Copilot、导
await expect(evidenceDialog).toContainText("1 条");
await expect(evidenceDialog).toContainText("2026-08");
await expect(evidenceDialog).toContainText("CSV");
await evidenceDialog.getByRole("button", { name: "基期", exact: true }).click();
await evidenceDialog
.getByRole("button", { name: "基期", exact: true })
.click();
await expect(evidenceDialog).toContainText("2025-08");
await evidenceDialog.getByLabel("关闭", { exact: true }).click();
const evidenceOffsets: number[] = [];
let evidenceFailOnce = false;
let evidenceEmpty = false;
await page.route("**/api/datasets/*/evidence", async (route) => {
const request = route.request().postDataJSON() as { offset?: number };
const offset = request.offset || 0;
if (evidenceFailOnce) {
evidenceFailOnce = false;
await route.fulfill({ status: 500, json: { error: "服务暂不可用" } });
return;
}
evidenceOffsets.push(offset);
if (evidenceEmpty) {
await route.fulfill({ json: { total: 0, sheet: "CSV", rows: [] } });
return;
}
const count = offset === 0 ? 50 : 1;
await route.fulfill({
json: {
@@ -111,10 +126,51 @@ test("桌面:真实导入、异常、下钻、证据、设置、Copilot、导
});
await page.getByLabel("查看医生A证据", { exact: true }).click();
await expect(evidenceDialog).toContainText("第 1-50 条,共 51 条");
await evidenceDialog.getByRole("button", { name: "下一页", exact: true }).click();
await evidenceDialog
.getByRole("button", { name: "下一页", exact: true })
.click();
await expect(evidenceDialog).toContainText("第 51-51 条,共 51 条");
await expect(evidenceDialog).toContainText("2026-09");
expect(evidenceOffsets).toEqual([0, 50]);
// F03 回归:第二页时重复点击当前期间应回到第一页并重新加载
await evidenceDialog
.getByRole("button", { name: "本期", exact: true })
.click();
await expect(evidenceDialog).toContainText("第 1-50 条,共 51 条");
expect(evidenceOffsets).toEqual([0, 50, 0]);
// F03 回归:第一页重复点击当前期间不清空列表、不重新请求、不停在“加载中”
await evidenceDialog
.getByRole("button", { name: "本期", exact: true })
.click();
await expect(evidenceDialog).toContainText("第 1-50 条,共 51 条");
await expect(evidenceDialog).not.toContainText("加载中");
expect(evidenceOffsets).toEqual([0, 50, 0]);
// F03 回归:快速本期/基期切换最终停留在当前期间的数据
await evidenceDialog
.getByRole("button", { name: "基期", exact: true })
.click();
await evidenceDialog
.getByRole("button", { name: "本期", exact: true })
.click();
await expect(evidenceDialog).toContainText("第 1-50 条,共 51 条");
await expect(evidenceDialog).not.toContainText("加载中");
// F03 回归:请求失败显示错误并可重试恢复
evidenceFailOnce = true;
await evidenceDialog
.getByRole("button", { name: "下一页", exact: true })
.click();
await expect(evidenceDialog).toContainText("服务暂不可用");
await expect(evidenceDialog).toContainText("加载失败");
await evidenceDialog.getByRole("button", { name: "重试" }).click();
await expect(evidenceDialog).toContainText("第 51-51 条,共 51 条");
// F03 回归:空结果显示空态而不是过期数据或加载中
evidenceEmpty = true;
await evidenceDialog
.getByRole("button", { name: "上一页", exact: true })
.click();
await expect(evidenceDialog).toContainText("本期范围内没有数据");
await expect(evidenceDialog).toContainText("共 0 条");
await expect(evidenceDialog).not.toContainText("加载中");
await evidenceDialog.getByLabel("关闭", { exact: true }).click();
await page
.getByRole("navigation", { name: "分析路径" })
@@ -164,38 +220,53 @@ test("桌面:真实导入、异常、下钻、证据、设置、Copilot、导
await page.route("**/formula-design", async (route) => {
designCalls++;
await route.fulfill({
json: designCalls === 1
? { complete: false, questions: ["缺失值和零分母如何处理?"], column: null }
: {
complete: true,
questions: [],
column: {
name: "收入成本差",
meaning: "实际收入减成本",
scope: "所选分组与期间",
missing: "结果不可用(#N/A);零分母不可用",
expression: {
operator: "subtract",
left: { metric: "revenue" },
right: { metric: "cost" },
json:
designCalls === 1
? {
complete: false,
questions: ["缺失值和零分母如何处理?"],
column: null,
}
: {
complete: true,
questions: [],
column: {
name: "收入成本差",
meaning: "实际收入减成本",
scope: "所选分组与期间",
missing: "结果不可用(#N/A);零分母不可用",
expression: {
operator: "subtract",
left: { metric: "revenue" },
right: { metric: "cost" },
},
},
},
},
});
});
await page.getByRole("button", { name: "生成公式工作簿" }).click();
const formulaDialog = page.getByRole("dialog", { name: "生成公式工作簿" });
await formulaDialog.getByRole("checkbox").check();
await formulaDialog.getByLabel("列计算描述").fill("新增收入成本差,缺失或零分母显示不可用");
await formulaDialog
.getByLabel("列计算描述")
.fill("新增收入成本差,缺失或零分母显示不可用");
await formulaDialog.getByRole("button", { name: "发送描述" }).click();
await expect(formulaDialog).toContainText("缺失值和零分母如何处理?");
await formulaDialog.getByLabel("列计算描述").fill("缺失项和零分母都按不可用处理");
await formulaDialog
.getByLabel("列计算描述")
.fill("缺失项和零分母都按不可用处理");
await formulaDialog.getByRole("button", { name: "发送描述" }).click();
await formulaDialog.getByRole("button", { name: "确认并生成公式工作簿" }).click();
await expect(formulaDialog.getByRole("status")).toContainText("生成完成", { timeout: 30000 });
await formulaDialog
.getByRole("button", { name: "确认并生成公式工作簿" })
.click();
await expect(formulaDialog.getByRole("status")).toContainText("生成完成", {
timeout: 30000,
});
const formulaDownload = page.waitForEvent("download");
await formulaDialog.getByRole("button", { name: "下载公式工作簿" }).click();
expect((await formulaDownload).suggestedFilename()).toBe("traceiq-formula-analysis.xlsx");
expect((await formulaDownload).suggestedFilename()).toBe(
"traceiq-formula-analysis.xlsx",
);
await formulaDialog.getByLabel("关闭").click();
});
test("手机:登录、总览、设置、Copilot 布局无横向溢出", async ({ page }) => {