From 2581eeff59abca916d412f0ee36b9152c87f0fe7 Mon Sep 17 00:00:00 2001 From: liangyihua Date: Wed, 22 Apr 2026 15:57:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(history):=20=E5=8E=86=E5=8F=B2=E9=A1=B5?= =?UTF-8?q?=E5=88=86=E6=9E=90=E4=B8=8E=E6=A0=87=E6=B3=A8=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E4=B8=80=E4=BD=93=E5=8C=96=E6=94=B9=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 已分析列行级「分析」,与工具栏分析处理共用弹窗;processTargetIds 与 processModalContextRows 保证行级入口与 bug 默认模块/跟踪人一致 - 详细原因:localStorage 历史 + 可搜索 Select 填入 + TextArea;失败标注 Modal 加宽与 body 滚动,避免页脚与字数统计错位 - 分析处理按钮 onClick 使用显式 MouseEventHandler,满足 tsc 与 Ant Design 类型 - 同步 docs/04_project_structure.md、docs/07_task_breakdown_and_plan.md Made-with: Cursor --- docs/04_project_structure.md | 2 +- docs/07_task_breakdown_and_plan.md | 3 + frontend/src/pages/history/HistoryPage.tsx | 133 +++++++++++++++++++-- 3 files changed, 125 insertions(+), 13 deletions(-) diff --git a/docs/04_project_structure.md b/docs/04_project_structure.md index 40f1a39..92f8075 100644 --- a/docs/04_project_structure.md +++ b/docs/04_project_structure.md @@ -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` | 分组执行历史 | 🔲 占位 | diff --git a/docs/07_task_breakdown_and_plan.md b/docs/07_task_breakdown_and_plan.md index 88620d7..4088212 100644 --- a/docs/07_task_breakdown_and_plan.md +++ b/docs/07_task_breakdown_and_plan.md @@ -234,6 +234,8 @@ oh: |------|------| | History 批次筛选 | ✅ 已实现 | | 失败原因标注(单条/批量) | ✅ 已实现 | +| History 行级「分析」快捷入口 | ✅ 已实现(“已分析”列内按钮,复用分析处理弹窗) | +| 分析处理「详细原因」历史联想 | ✅ 已实现(基于本地缓存的输入联想) | | 指派逻辑 | ✅ 已实现 | | analyzed / owner / owner_history | ✅ 已实现 | | pipeline_failure_reason 关联 | ✅ 已实现 | @@ -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 优化:新增“已分析”列行级分析按钮;分析处理弹窗“详细原因”新增历史缓存与联想输入 | diff --git a/frontend/src/pages/history/HistoryPage.tsx b/frontend/src/pages/history/HistoryPage.tsx index 909d7d2..aee8a5a 100644 --- a/frontend/src/pages/history/HistoryPage.tsx +++ b/frontend/src/pages/history/HistoryPage.tsx @@ -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"; @@ -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 { @@ -245,6 +248,8 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) { const [selectedRowKeys, setSelectedRowKeys] = useState([]); // 勾选的行 id const [processModalVisible, setProcessModalVisible] = useState(false); const [failureProcessOptions, setFailureProcessOptions] = useState(null); + const [processTargetIds, setProcessTargetIds] = useState(null); + const [reasonCache, setReasonCache] = useState([]); const [processSubmitLoading, setProcessSubmitLoading] = useState(false); const processFailedType = Form.useWatch("failed_type", processForm); // 监听失败类型,控制模块字段显隐 const [inheritForm] = Form.useForm(); @@ -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; @@ -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" ); @@ -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; @@ -557,12 +596,39 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) { } }; + /** 工具栏「分析处理」:显式事件类型,避免将 `openProcessModal` 直接作 onClick 时与 MouseEvent 参数冲突(TS2322) */ + const handleToolbarOpenProcessModal: MouseEventHandler = () => { + 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; @@ -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(); @@ -608,6 +676,7 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) { const handleProcessModalCancel = () => { setProcessModalVisible(false); + setProcessTargetIds(null); processForm.resetFields(); }; @@ -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 ( - - {text} - +
+ + {text} + + +
); }, onHeaderCell: (col) => ({ @@ -1452,7 +1536,7 @@ export default function HistoryPage({ drilldown = false }: HistoryPageProps) { {/* 至少勾选一条失败记录时可用 */}