Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/04_project_structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ Schema 定义 API 的请求参数格式和响应 JSON 格式,由 FastAPI 自

| 文件 | 说明 | 实现状态 |
|------|------|---------|
| `history/HistoryPage.tsx` | 详细执行历史页面。Table 展示 pipeline_history 数据(含跟踪人、失败原因列),支持分页与多维度筛选,Drawer 含基本信息区、失败归因区(仅 failed 时展示)、外部链接区;用例名链至钻取页;工具栏含分析处理、继承、一键分析、**一键通知**(spec/13)、一键生成通报;**分析处理**弹窗在失败类型为 bug 时跟踪人为可编辑输入,默认按模块带出「姓名 工号」(spec/04)。布局:主内容区内占满剩余高度,筛选区与表头、分页固定,**仅表体区域纵向滚动**(`ResizeObserver` + `scroll.y`);样式见 `history-table.css` | ✅ 已实现 |
| `history/HistoryPage.tsx` | 详细执行历史页面。Table 展示 pipeline_history 数据(含跟踪人、失败原因列),支持分页与多维度筛选,Drawer 含基本信息区、失败归因区(仅 failed 时展示)、外部链接区;用例名链至钻取页;工具栏含分析处理、继承、一键分析、**一键通知**(spec/13)、一键生成通报;“已分析”列新增行级「分析」按钮,点击后复用与工具栏「分析处理」相同的弹窗与提交流程;**分析处理**弹窗在失败类型为 bug 时跟踪人为可编辑输入,默认按模块带出「姓名 工号」(spec/04);「详细原因」为多行 `Input.TextArea`,有本地缓存时上方提供可搜索 `Select`(`localStorage`)一键写入历史文案;失败标注 Modal 宽度约 580px、`body` 设 `maxHeight`+纵向滚动,避免内容与页脚重叠。布局:主内容区内占满剩余高度,筛选区与表头、分页固定,**仅表体区域纵向滚动**(`ResizeObserver` + `scroll.y`);样式见 `history-table.css` | ✅ 已实现 |
| `history/CaseExecutionsHistoryPage.tsx` | 用例执行历史钻取壳组件,渲染 `HistoryPage drilldown`(`/history/case-executions`),规约 spec/12 | ✅ 已实现 |
| `dashboard/DashboardPage.tsx` | 首页大盘 | 🔲 占位 |
| `overview/OverviewPage.tsx` | 分组执行历史 | 🔲 占位 |
Expand Down
3 changes: 3 additions & 0 deletions docs/07_task_breakdown_and_plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ oh:
|------|------|
| History 批次筛选 | ✅ 已实现 |
| 失败原因标注(单条/批量) | ✅ 已实现 |
| History 行级「分析」快捷入口 | ✅ 已实现(“已分析”列内按钮,复用分析处理弹窗) |
| 分析处理「详细原因」历史联想 | ✅ 已实现(基于本地缓存的输入联想) |
| 指派逻辑 | ✅ 已实现 |
| analyzed / owner / owner_history | ✅ 已实现 |
| pipeline_failure_reason 关联 | ✅ 已实现 |
Expand Down Expand Up @@ -264,3 +266,4 @@ oh:
| 2026-03-03 | 0.1 | 初稿:需求清单、任务拆解、计划倒排 |
| 2026-03-03 | 0.2 | 按真实使用流程重写:功能优先级、Report MVP、通知最小方案、开发阶段划分 |
| 2026-03-03 | 0.3 | 补充:流转指派通知(预留 API);总结格式固定为 rolling 线看护进展通告(按 platform、跟踪人、模块统计) |
| 2026-04-22 | 0.5 | 同步 History 优化:新增“已分析”列行级分析按钮;分析处理弹窗“详细原因”新增历史缓存与联想输入 |
133 changes: 121 additions & 12 deletions frontend/src/pages/history/HistoryPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState, useCallback, useRef } from "react";
import type { MouseEventHandler } from "react";
import { useSearchParams } from "react-router-dom";
import { Resizable } from "react-resizable";
import "react-resizable/css/styles.css";
Expand Down Expand Up @@ -38,6 +39,8 @@ import {
} from "../../services";

const { Text, Title, Paragraph } = Typography;
const REASON_CACHE_KEY = "history_failure_reason_cache";
const REASON_CACHE_LIMIT = 30;

/** 轮次群通告正文(与产品约定模板一致) */
function buildRollingReportMarkdown(data: BatchReportResponse): string {
Expand Down Expand Up @@ -245,6 +248,8 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); // 勾选的行 id
const [processModalVisible, setProcessModalVisible] = useState(false);
const [failureProcessOptions, setFailureProcessOptions] = useState<FailureProcessOptions | null>(null);
const [processTargetIds, setProcessTargetIds] = useState<number[] | null>(null);
const [reasonCache, setReasonCache] = useState<string[]>([]);
const [processSubmitLoading, setProcessSubmitLoading] = useState(false);
const processFailedType = Form.useWatch("failed_type", processForm); // 监听失败类型,控制模块字段显隐
const [inheritForm] = Form.useForm();
Expand Down Expand Up @@ -379,6 +384,23 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {
fetchOptions();
}, []);

useEffect(() => {
try {
const raw = localStorage.getItem(REASON_CACHE_KEY);
if (!raw) return;
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return;
const next = parsed
.filter((item): item is string => typeof item === "string")
.map((item) => item.trim())
.filter((item) => item.length > 0)
.slice(0, REASON_CACHE_LIMIT);
setReasonCache(next);
} catch {
setReasonCache([]);
}
}, []);

useEffect(() => {
if (!drilldown) {
drilldownInvalidWarnedRef.current = false;
Expand Down Expand Up @@ -514,6 +536,13 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {
};

const selectedRows = data.filter((r) => selectedRowKeys.includes(r.id));
/** 与 `handleProcessModalOk` / `openProcessModal` 一致:行级「分析」仅写 processTargetIds 时,不以勾选行为准 */
const processModalContextRows =
processTargetIds && processTargetIds.length > 0
? processTargetIds
.map((id) => data.find((r) => r.id === id))
.filter((r): r is HistoryItem => !!r)
: selectedRows;
const hasSelectedFailedOrError = selectedRows.some(
(r) => r.case_result === "failed" || r.case_result === "error"
);
Expand All @@ -533,13 +562,23 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {
const notifyBtnEnabled = processBtnEnabled && currentBatch != null;
const showBatchDimension = currentBatch != null;

const openProcessModal = async () => {
if (!processBtnEnabled) return;
const openProcessModal = async (targetRows?: HistoryItem[]) => {
const effectiveRows = targetRows && targetRows.length > 0 ? targetRows : selectedRows;
if (!effectiveRows.length) return;
const hasFailedOrError = effectiveRows.some(
(r) => r.case_result === "failed" || r.case_result === "error"
);
if (!hasFailedOrError) return;
setProcessTargetIds(
effectiveRows
.filter((r) => r.case_result === "failed" || r.case_result === "error")
.map((r) => r.id)
);
setProcessModalVisible(true);
try {
const opts = await historyApi.failureProcessOptions();
setFailureProcessOptions(opts);
const firstFailed = selectedRows.find(
const firstFailed = effectiveRows.find(
(r) => r.case_result === "failed" || r.case_result === "error"
);
const rawModule = firstFailed?.main_module ?? undefined;
Expand All @@ -557,12 +596,39 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {
}
};

/** 工具栏「分析处理」:显式事件类型,避免将 `openProcessModal` 直接作 onClick 时与 MouseEvent 参数冲突(TS2322) */
const handleToolbarOpenProcessModal: MouseEventHandler<HTMLElement> = () => {
void openProcessModal();
};

const openProcessModalByRow = async (record: HistoryItem) => {
if (record.case_result !== "failed" && record.case_result !== "error") return;
setDrawerVisible(false);
await openProcessModal([record]);
};

const cacheReasonIfNeeded = (reason: string | undefined) => {
const value = reason?.trim();
if (!value) return;
const next = [value, ...reasonCache.filter((item) => item !== value)].slice(0, REASON_CACHE_LIMIT);
setReasonCache(next);
try {
localStorage.setItem(REASON_CACHE_KEY, JSON.stringify(next));
} catch {
// 忽略本地存储异常,避免影响标注主流程
}
};

const handleProcessModalOk = async () => {
try {
const values = await processForm.validateFields();
const failedOnlyIds = selectedRows
const selectedFailedOnlyIds = selectedRows
.filter((r) => r.case_result === "failed" || r.case_result === "error")
.map((r) => r.id);
const failedOnlyIds =
processTargetIds && processTargetIds.length > 0
? processTargetIds
: selectedFailedOnlyIds;
if (!failedOnlyIds.length) {
message.warning("请至少勾选一条失败或异常记录");
return;
Expand All @@ -580,8 +646,10 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {
}
setProcessSubmitLoading(true);
await historyApi.failureProcess(payload);
cacheReasonIfNeeded(payload.reason);
message.success("标注成功");
setProcessModalVisible(false);
setProcessTargetIds(null);
processForm.resetFields();
setSelectedRowKeys([]);
const params = paramsFromUrl();
Expand All @@ -608,6 +676,7 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {

const handleProcessModalCancel = () => {
setProcessModalVisible(false);
setProcessTargetIds(null);
processForm.resetFields();
};

Expand Down Expand Up @@ -1118,12 +1187,27 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {
sorter: true,
sortOrder: sortOrderFor("analyzed"),
ellipsis: { showTitle: false },
render: (val: number | null) => {
render: (val: number | null, record: HistoryItem) => {
const text = val === 1 ? "已分析" : "未分析";
const canAnalyze = record.case_result === "failed" || record.case_result === "error";
return (
<EllipsisTooltip title={text} placement="topLeft">
<Tag color={val === 1 ? "blue" : "default"}>{text}</Tag>
</EllipsisTooltip>
<div style={{ display: "flex", alignItems: "center", gap: 8, justifyContent: "center" }}>
<EllipsisTooltip title={text} placement="topLeft">
<Tag color={val === 1 ? "blue" : "default"}>{text}</Tag>
</EllipsisTooltip>
<Button
size="small"
type="link"
disabled={!canAnalyze}
onClick={(e) => {
e.stopPropagation();
openProcessModalByRow(record);
}}
style={{ padding: 0, height: "auto" }}
>
分析
</Button>
</div>
);
},
onHeaderCell: (col) => ({
Expand Down Expand Up @@ -1452,7 +1536,7 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {
<Col>
{/* 至少勾选一条失败记录时可用 */}
<Button
onClick={openProcessModal}
onClick={handleToolbarOpenProcessModal}
disabled={!processBtnEnabled || loading}
>
分析处理
Expand Down Expand Up @@ -1531,14 +1615,17 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {

<Modal
title="失败记录标注"
width={500}
width={580}
open={processModalVisible}
onOk={handleProcessModalOk}
onCancel={handleProcessModalCancel}
confirmLoading={processSubmitLoading}
okText="确定"
cancelText="取消"
destroyOnClose
styles={{
body: { maxHeight: "min(70vh, 520px)", overflowY: "auto" },
}}
>
{/* 字段顺序:失败类型 → 模块(仅 bug 时显示)→ 跟踪人 → 详细原因 */}
<Form
Expand All @@ -1556,7 +1643,7 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {
);
processForm.setFieldValue("owner", cft?.owner ?? undefined);
} else {
const firstFailed = selectedRows.find(
const firstFailed = processModalContextRows.find(
(r) => r.case_result === "failed" || r.case_result === "error"
);
const rawModule = firstFailed?.main_module ?? undefined;
Expand Down Expand Up @@ -1647,6 +1734,28 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {
/>
)}
</Form.Item>
{reasonCache.length > 0 && (
<Form.Item label="从历史记录填入(可选)">
<Select
allowClear
showSearch
placeholder="输入关键字筛选,选择后写入下方详细原因"
style={{ width: "100%" }}
options={reasonCache.map((text) => ({
label: text.length > 100 ? `${text.slice(0, 100)}…` : text,
value: text,
}))}
filterOption={(input, option) =>
String(option?.value ?? "")
.toLowerCase()
.includes(input.trim().toLowerCase())
}
onChange={(value) => {
if (value) processForm.setFieldValue("reason", value);
}}
/>
</Form.Item>
)}
<Form.Item
name="reason"
label="详细原因"
Expand All @@ -1656,7 +1765,7 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) {
{ max: 2000, message: "最多 2000 字符" },
]}
>
<Input.TextArea rows={4} placeholder="请输入详细原因" maxLength={2000} showCount />
<Input.TextArea rows={4} maxLength={2000} showCount placeholder="请输入详细原因" />
</Form.Item>
</Form>
</Modal>
Expand Down
Loading