diff --git a/frontend/src/dialogs.tsx b/frontend/src/dialogs.tsx
index 0e52997..24828be 100644
--- a/frontend/src/dialogs.tsx
+++ b/frontend/src/dialogs.tsx
@@ -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" &&
客户原始标识不包含在明细中。
}
{error && (
- {error}
+ {error}{" "}
+
)}
@@ -435,6 +450,11 @@ export function EvidenceDialog({
{String(r.gross ?? "")} |
))}
+ {result && result.rows.length === 0 && (
+
+ | 本期范围内没有数据 |
+
+ )}
@@ -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);
diff --git a/tests/e2e/workspace.spec.ts b/tests/e2e/workspace.spec.ts
index 20f26af..ddef609 100644
--- a/tests/e2e/workspace.spec.ts
+++ b/tests/e2e/workspace.spec.ts
@@ -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 }) => {