提交 a1ef3ffa authored 作者: 陈泽健's avatar 陈泽健

feat(perf-report): AI 分析文档格式优化 + A/B 下载交互 + lint 修复

- 正式文档排版: AI Word 导出增加安全封面页、静态目录页、正文标题去重、
  章节分页、表格斑马纹、页眉页脚/页码及 Word 兼容样式(exportReport.ts)
- A 方案: 下载发起后 ElMessageBox 确认提示, 展示实际文件名
- B 方案: 新增 downloadArtifact store, 右下角浮动卡片支持再次下载/关闭,
  ObjectURL 在替换/关闭/卸载时释放并可惰性重建
- 安全边界: 封面仅含任务名/分析类型/来源/生成时间, 不写入任何凭据
- lint: 新增 ESLint 9 flat config + vue/ts 解析器, 0 errors
- 验证: 前端 build/lint 通过, 后端 16 passed, 部署 5.60 容器 healthy
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 521ead34
# HANDOFF — 性能测试模块会话交接文档
> **生成时间**: 2026-08-25
> **生成时间**: 2026-08-26
> **当前分支**: `platform-auto-test`
> **最近提交**: `8f417389` fix(perf-report): 补全报告导出缺失模块与图表 + 接口名URL去前缀
> **会话窗口**: 性能测试 — 报告导出排版质量优化(FR-1~FR-6 已实现 + 部署 5.60 + e2e 验证通过)
> **状态**: ✅ 排版优化全部验收标准通过(图表不溢出/指标卡 table 布局/列宽比例/专业排版/分页控制/合并报告同步)
> **会话窗口(上一窗口)**: 报告导出内容补全 + 接口名 URL 去前缀(commit `8f417389` 已部署)
> **最近提交**: `521ead34` feat(perf-report): 报告导出排版质量优化 + 导出文件名使用任务名
> **会话窗口**: 性能测试 — 排版优化 + 导出文件名优化已提交推送(P0 待办闭环)
> **状态**: ✅ 排版优化(FR-1~FR-6)+ 导出文件名优化已 commit `521ead34` 并推送 origin/platform-auto-test
> **会话窗口(上一窗口)**: 报告导出排版质量优化 + 导出文件名使用任务名称(均已部署 5.60)
---
## ⚡ 最新会话更新(2026-08-26)— AI 分析文档格式优化 + A/B 下载方案 + lint 修复 ✅
### A. 本会话完成项
本会话继续完成性能测试 AI 分析报告的正式文档导出与下载交互升级,范围仅涉及前端导出层、性能报告页面、AI 分析接口契约回归测试及相关文档;未改动 AI Prompt 与后端分析逻辑的既有行为。
| 模块 | 实现内容 |
|------|----------|
| 正式文档排版 | `frontend/src/utils/exportReport.ts` 为 AI Word 导出增加安全封面页、静态目录页、正文顶层标题去重、章节分页、正文段落/标题/表格 Word 兼容样式、表格偶数行斑马纹、表头不拆分及页眉页脚/页码字段;单次分析目录对应 7 章,对比分析目录对应 5 章 |
| A 方案:下载确认 | `ReportPanel.vue` 在下载已发起后使用 `ElMessageBox` 提示实际文件名和浏览器下载目录;不负责触发下载,失败或空文档不弹成功提示 |
| B 方案:浮动卡片 | 新增 `frontend/src/stores/downloadArtifact.ts`,右下角卡片展示文件名/大小,支持「再次下载」和「关闭」;产物 URL 在替换、关闭或页面卸载时释放,并支持再次惰性创建下载 URL |
| 安全边界 | 封面仅使用任务名、分析类型、来源和生成时间等安全元数据;不写入密码、Token、Cookie、Authorization 或请求体 |
| 前端 lint | 新增 ESLint 9 flat config,安装 `eslint``eslint-plugin-vue``typescript-eslint``.vue` 使用 `vue-eslint-parser` + TypeScript parser,存量未使用变量保留为 warning |
### B. 验证与部署
| 项目 | 结果 |
|------|------|
| `cd frontend && npm run build` | ✅ 通过(vue-tsc 类型检查与 Vite 构建通过) |
| `cd frontend && npm run lint` | ✅ 0 errors,15 个存量 unused-vars warnings |
| `cd backend && python -m pytest tests/test_performance_ai_service.py -v` | ✅ 16 passed |
| 5.60 前端部署 | ✅ `deploy_frontend_only.py` 上传 94 个文件,重启 `app` 后容器 healthy,`/health` 返回 healthy |
| 部署产物 | ✅ 已确认 `ReportPanel-BSyNLe0E.js` / `ReportPanel-91rrP4ho.css` 在服务器生效 |
### C. 遗留事项
1. Chrome DevTools 当前因浏览器 profile 被占用,尚未完成页面级 A/B 点击验收;需补测单次/对比分析下载、弹窗、浮动卡片再次下载与关闭释放。
2. 需在 Word 中人工确认封面、静态目录、章节分页、斑马纹、页眉页脚及页码在实际客户端的呈现效果。
3. 其他窗口产生的设备模拟、文档管理、功能测试、安全测试及探测脚本等未提交文件不属于本次性能测试变更,不纳入本次提交。
---
## ⚡ 最新会话更新(2026-08-26 收尾)— git commit + push 完成(P0 待办闭环)✅
### A. 会话动作
按上两窗口 HANDOFF 遗留 P0 待办「git commit + push」,使用 `/GitCommit` 提交并推送:
| 项目 | 内容 |
|------|------|
| 提交 | `521ead34` `feat(perf-report): 报告导出排版质量优化 + 导出文件名使用任务名` |
| 文件 | 7 个(+1323/-313):`exportReport.ts` / `ReportPanel.vue` / `ProjectReport.vue` + 新增 `_PRD_报告导出排版质量优化.md` + `_计划执行.md` + `HANDOFF_性能测试.md` + `HANDOFF_UI自动化.md` |
| 推送 | `8f417389..521ead34``origin/platform-auto-test` ✅ |
覆盖内容:FR-1 图表尺寸 / FR-2 指标卡 table / FR-3 列宽 / FR-4 专业排版 / FR-5 分页 / FR-6 合并报告同步 + 导出文件名(单任务=任务名,合并报告=项目名,`sanitizeFilename` 清洗)。
### B. 剩余待办(交接给下一窗口)
| # | 任务 | 优先级 | 说明 |
|---|------|--------|------|
| 1 | 导出文件名效果验证(可选) | P2 | 5.60 打开报告页 → 导出 Word/PDF/JSON,确认下载文件名 = 任务名称(单任务)/ 项目名称(合并报告),已部署但未在页面实测 |
| 2 | 快照表强制分页可选优化 | P2 | 快照表是文档最后 section 且带 `section-break`,若前页内容恰好到页尾可能产生一页较空;可评估去掉强制分页或改 `keep-together` |
| 3 | 清理临时文件 | — | `tmp/acct/dl/*.doc` 验证下载文件;`backend/run_v3.out``Docs/login_page_snapshot.*` |
| 4 | 其他窗口未提交文件 | — | 设备模拟 / 文档管理 / 功能测试 / 安全测试等模块 PRD 与代码、`backend/scripts/` 大量 `probe_*`/`diag_*`/`generate_*` 脚本,均与性能测试窗口无关,未纳入本次提交 |
---
......
# HANDOFF — UI自动化测试交接文档
> **生成时间**: 2026-08-25
> **生成时间**: 2026-08-26
> **当前分支**: `platform-auto-test`
> **最近提交**: `f4892d6c` docs(handoff): 精简 HANDOFF_UI自动化文档 + 更新会话37状态
> **状态**: 🟢 **会话40:新增用例数据全量同步三台测试管理平台(5.44/5.202/5.60)——本地 SQLite 315 条用例 UPSERT 至三端 MySQL(5.44/5.202 各 371→393 补 22 条、5.60 393→394 补 1 条),含会话37/38/39 新增用例 + 14 条断言对齐更新;沉淀 `sync_cases_to_servers.py`。**
> **状态**: 🟢 **会话41:预定数据(数据统计页 StatisticsModule)用例覆盖盲区补齐——生成 6 个 navigate 直达用例(页面访问/图表数量/筛选切换/时间筛选/满意度评价/通知统计),R1 一次执行 6/6 100% 通过;沉淀 `generate_yuding_cases.py`;确认「预定数据」首页入口即数据统计页,独立「预定2.0(meetingV2)」无首页直达入口。**
---
## 📊 当前状态(会话 40,2026-08-25)
## 📊 当前状态(会话 41,2026-08-26)
**预定数据模块用例覆盖盲区补齐**:应 `/sut-explore` 盘点需求,发现「预定数据」入口(首页功能卡片)在用例库中分散且步骤过旧(click 菜单导航,headless 下不稳定)。通过 `menu_block_urls_v3.json` 确认**首页「预定数据」入口实际指向 `StatisticsModule`(数据统计页)**,非独立的「预定2.0」系统;独立「预定2.0(meetingV2)」子模块(会议室列表/已预定会议/历史记录/会议审批/参会人模板)在首页功能卡片中**无独立直达入口**(仅 `elements_mapping``[data-id]` 映射,无直达 URL 配置)。
### ✅ 预定数据用例生成(会话 41,2026-08-26)
**背景**:盘点发现数据统计页相关用例分散在两个模块(`module_parent_shujutongji` 10 条 + `module_6790b997...` 数据分析 42 条),「预定数据」入口用例首步多为 click 菜单(headless 下 `.block` 不稳定,已被会话 34 验证踩坑)。
**生成方式**:新建 `backend/scripts/generate_yuding_cases.py`(幂等:先 DELETE 同名旧用例再 INSERT),写入模块 `module_parent_yuding20`,首步一律 `navigate` 直达 `StatisticsModule` URL,`config``auto_login:true + headless:true + step_retry_count:2`
**执行结果 R1**`exec_35868ddb9ebe4498ab93aadc4205ccee`):
| 用例 | 结果 | 耗时 |
|------|------|------|
| 预定数据-页面访问验证 | ✅ passed | 24.1s |
| 预定数据-图表数量验证 | ✅ passed | 17.0s |
| 预定数据-预定会议数据筛选切换 | ✅ passed | 25.0s |
| 预定数据-时间筛选验证 | ✅ passed | 57.1s |
| 预定数据-满意度评价统计查看 | ✅ passed | 16.7s |
| 预定数据-会议通知发送统计切换 | ✅ passed | 98.2s |
**总结果:6/6 通过,100%,一次执行即全绿**(无需迭代修复)。
**关键选择器**(沿用现有数据统计已验证模式):
```python
# 图表断言
canvas / element_count_min canvas >= 3
# 时间维度下拉(Element UI el-select readonly)
input[readonly][value="日"] / input[value="日"] / input[readonly][placeholder*="选择"]
# 通知类型下拉
input[readonly][value="短信通知"] / input[value="短信通知"]
# 筛选器计数
.el-select element_count_min >= 3
```
**改动清单**
| 文件 | 说明 |
|------|------|
| `backend/scripts/generate_yuding_cases.py` | 新增生成脚本(幂等,6 用例,navigate 直达模板) |
| `backend/data/test_platform.db` | 新增 6 条用例至 `module_parent_yuding20`(已清理 2 条同名旧用例) |
**遗留说明**
- 本次新增 6 条用例已同步 **5.44/5.202**(各新增 6 条,模块内现有 8 条);**5.60 尚未同步**,也未提交 git。
- **独立「预定2.0」(meetingV2)** 若需覆盖,因无首页直达入口,需进一步探测(`elements_mapping``[data-id="meetingRoomList"]` 等映射,可能需要首页功能中心 Tab 点击进入)——P2 待办。
---
## 📊 会话历史(会话 40,2026-08-25)
**新增用例数据全量同步三台测试管理平台(192.168.5.44 / 192.168.5.202 / 192.168.5.60)**:应需求将本地新增/更新的用例数据补充到三台服务器。三台应用均为 Docker MySQL 架构(`plat-auto-test-app` 容器内连 `mysql` 容器,库 `plat_auto_test`,表 `test_cases`),直接 SQL UPSERT 写入即可生效,无需重启容器。
......@@ -273,6 +322,7 @@ button:has-text('确定')
| 会话 | 日期 | 主题 | 结果 |
|------|------|------|------|
| 41 | 08-26 | **预定数据用例覆盖盲区补齐**:生成 6 个 navigate 直达用例(页面访问/图表数量/筛选切换/时间筛选/满意度/通知统计)至 `module_parent_yuding20`;沉淀 `generate_yuding_cases.py`(幂等);确认预定数据入口=数据统计页 | ✅ 本地 6/6,R1 一次通过 |
| 40 | 08-25 | **新增用例数据同步三台测试管理平台**:本地 315 条 UPSERT 至 5.44/5.202/5.60 MySQL(各补 22/22/1 条,含会话37/38/39 新增 + 14 条断言对齐更新);沉淀 `sync_cases_to_servers.py`(docker exec 管道传 JSON,无重启) | ✅ 5.44=393、5.202=393、5.60=394,抽查 9 用例三台全命中 |
| 39 | 08-25 | **薄弱模块 31 用例全量修复 R7 100%**`_navigate_to_target_page` 改走 `_do_navigate`(含 reload 兜底);`_ensure_micro_app_loaded` 阶段4 reload 修复;5 个探测脚本沉淀;14 个用例断言对齐 | ✅ 本地 31/31,799.7s |
| 38 | 08-24 | **资产信息-新增资产闭环用例**:Playwright 4 轮探测添加弹窗(表单/radio/cascader/select/MessageBox);生成 27 步闭环用例(新增→验证→删除);首次执行 27/27 100% 通过 | ✅ 本地 1/1,254.9s |
......@@ -373,6 +423,7 @@ cd frontend && npm run build
### P2(低优先级)
- [ ] **排查搜索确定按钮 88s 卡点**(会话 38 新增资产闭环 Step 16/25 各 88s,功能通过但拉长总时长 254.9s)
- [ ] **预定2.0(meetingV2)独立页面探测与用例生成**(首页无独立直达入口,需从功能中心 Tab/`[data-id]` 入口探测)
- [ ] **薄弱模块 R7 修复部署三台服务器(5.44/5.202/5.60)**(会话 39 产出 `playwright_executor.py` 两处修复 + 14 个断言对齐用例。⚠️ 会话 40 已同步用例数据到位,但**执行器代码修复尚未部署**三台——远端薄弱模块仍可能复现 NO-BODY,需尽快部署;可复用 `deploy_to_544_5202.py`/`sync_to_560_full.py`
- [ ] **monitor 内层路由被忽略**(微应用固定渲染设备列表,忽略 URL 内层 Maintenancelist/Filemanage 路由;相关用例已按实际渲染断言,如需真正内页需进一步探测侧边栏交互方案)
- [ ] 完善二级菜单映射表(`menu_mapping.py`
......
......@@ -535,7 +535,7 @@ async def ai_analysis_single(
execution_id: str,
service: PerformanceService = Depends(get_perf_service),
):
"""对指定执行记录进行 AI 分析(瓶颈定位/优化建议)"""
"""对指定执行记录进行 AI 分析(瓶颈定位/优化建议/正式文档生成)"""
report = await service.get_execution_report(execution_id)
if not report:
raise HTTPException(status_code=404, detail="执行报告不存在")
......@@ -551,6 +551,11 @@ async def ai_analysis_single(
resource_analysis=analysis.get("resource_analysis"),
comparison=analysis.get("comparison"),
source=analysis.get("source", "ai"),
document_title=analysis.get("document_title"),
document_markdown=analysis.get("document_markdown"),
document_html=analysis.get("document_html"),
document_sections=analysis.get("document_sections"),
generated_at=analysis.get("generated_at"),
)
......@@ -559,7 +564,7 @@ async def ai_analysis_compare(
req: AiAnalysisCompareRequest,
service: PerformanceService = Depends(get_perf_service),
):
"""多版本对比 AI 分析"""
"""多版本对比 AI 分析(含对比文档生成)"""
reports = []
for eid in req.execution_ids:
r = await service.get_execution_report(eid)
......@@ -578,6 +583,11 @@ async def ai_analysis_compare(
suggestions=analysis.get("suggestions", []),
comparison=analysis.get("comparison"),
source=analysis.get("source", "ai"),
document_title=analysis.get("document_title"),
document_markdown=analysis.get("document_markdown"),
document_html=analysis.get("document_html"),
document_sections=analysis.get("document_sections"),
generated_at=analysis.get("generated_at"),
)
......
......@@ -586,6 +586,27 @@ class AiAnalysisCompareRequest(BaseModel):
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
class AiAnalysisDocumentSectionTable(BaseModel):
"""AI 分析文档章节表格"""
headers: List[str] = Field(default_factory=list, description="表头")
rows: List[List[Any]] = Field(default_factory=list, description="数据行")
class AiAnalysisDocumentSubSection(BaseModel):
"""AI 分析文档子章节"""
title: str = Field("", description="子章节标题")
content: str = Field("", description="子章节内容(Markdown 段落)")
class AiAnalysisDocumentSection(BaseModel):
"""AI 分析文档章节"""
id: str = Field("", description="章节标识")
title: str = Field("", description="章节标题")
content: str = Field("", description="章节内容(Markdown 段落)")
tables: List[AiAnalysisDocumentSectionTable] = Field(default_factory=list, description="表格列表")
sub_sections: List[AiAnalysisDocumentSubSection] = Field(default_factory=list, description="子章节列表")
class AiAnalysisResponse(BaseModel):
"""AI 分析响应"""
execution_id: str = Field("", description="执行记录 ID(对比分析时为逗号拼接)")
......@@ -599,6 +620,12 @@ class AiAnalysisResponse(BaseModel):
comparison: Optional[List[Dict[str, Any]]] = Field(None, description="多版本对比明细")
raw_response: Optional[str] = Field(None, description="Claude 原始响应(调试用)")
source: str = Field("ai", description="分析来源: ai / rule")
# 正式文档字段(Phase 2 新增)
document_title: Optional[str] = Field(None, description="文档标题")
document_markdown: Optional[str] = Field(None, description="完整 Markdown 格式报告")
document_html: Optional[str] = Field(None, description="正式文档 HTML 内容")
document_sections: Optional[List[AiAnalysisDocumentSection]] = Field(None, description="结构化章节数据")
generated_at: Optional[str] = Field(None, description="文档生成时间")
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
......
......@@ -635,6 +635,40 @@ class PerformanceService:
status_4xx = sum(s.status_4xx for s in snapshots)
status_5xx = sum(s.status_5xx for s in snapshots)
# 构建 sanitized 任务配置摘要
task_config = {
"target_url": task.target_url,
"method": task.method,
"mode": task.mode,
"concurrency": task.concurrency,
"qps_target": task.qps_target,
"duration": task.duration,
"ramp_up": task.ramp_up,
"scenario_type": task.scenario_type,
"step_concurrency": task.step_concurrency or [],
"step_duration": task.step_duration,
"loop_type": task.loop_type,
"loop_count": task.loop_count,
"synchronizing_timer_enabled": task.synchronizing_timer_enabled,
"synchronizing_timer_count": task.synchronizing_timer_count,
"synchronizing_timer_timeout": task.synchronizing_timer_timeout,
"error_action": task.error_action,
"think_time_enabled": task.think_time_enabled,
"think_time_min": task.think_time_min,
"think_time_max": task.think_time_max,
"think_time_distribution": task.think_time_distribution,
"csv_parameterization_enabled": task.csv_parameterization_enabled,
"auth_required": task.auth_required,
"account_key": task.account_key,
"sign_request": task.sign_request,
"assertion_count": len(task.assertions or []),
"unique_fields": task.unique_fields or [],
"request_detail_enabled": task.request_detail_enabled,
"step_count": len(task.steps or []),
"transaction_fail_policy": task.transaction_fail_policy,
"transaction_timeout": task.transaction_timeout,
}
return {
"summary": {
"task_id": task.id,
......@@ -650,6 +684,7 @@ class PerformanceService:
"end_time": task.end_time.isoformat() if task.end_time else None,
"account_key": task.account_key,
},
"task_config": task_config,
"metrics": {
"total_requests": task.total_requests,
"success_count": task.success_count,
......@@ -728,6 +763,42 @@ class PerformanceService:
status_4xx = sum(s.status_4xx for s in snapshots)
status_5xx = sum(s.status_5xx for s in snapshots)
# 构建 sanitized 任务配置摘要(不含 headers/body/password 等敏感字段)
task_config = None
if task:
task_config = {
"target_url": task.target_url,
"method": task.method,
"mode": task.mode,
"concurrency": task.concurrency,
"qps_target": task.qps_target,
"duration": task.duration,
"ramp_up": task.ramp_up,
"scenario_type": task.scenario_type,
"step_concurrency": task.step_concurrency or [],
"step_duration": task.step_duration,
"loop_type": task.loop_type,
"loop_count": task.loop_count,
"synchronizing_timer_enabled": task.synchronizing_timer_enabled,
"synchronizing_timer_count": task.synchronizing_timer_count,
"synchronizing_timer_timeout": task.synchronizing_timer_timeout,
"error_action": task.error_action,
"think_time_enabled": task.think_time_enabled,
"think_time_min": task.think_time_min,
"think_time_max": task.think_time_max,
"think_time_distribution": task.think_time_distribution,
"csv_parameterization_enabled": task.csv_parameterization_enabled,
"auth_required": task.auth_required,
"account_key": task.account_key,
"sign_request": task.sign_request,
"assertion_count": len(task.assertions or []),
"unique_fields": task.unique_fields or [],
"request_detail_enabled": task.request_detail_enabled,
"step_count": len(task.steps or []),
"transaction_fail_policy": task.transaction_fail_policy,
"transaction_timeout": task.transaction_timeout,
}
return {
"summary": {
"task_id": execution.task_id,
......@@ -743,6 +814,7 @@ class PerformanceService:
"end_time": execution.end_time.isoformat() if execution.end_time else None,
"account_key": task.account_key if task else "",
},
"task_config": task_config,
"metrics": {
"total_requests": execution.total_requests,
"success_count": execution.success_count,
......
此差异已折叠。
/**
* ESLint 扁平配置(ESLint 9 + Vue 3 + TypeScript)
*
* 覆盖 .ts/.tsx/.js/.jsx/.mjs/.cjs/.vue:
* - JS:@eslint/js 推荐规则
* - TS:typescript-eslint 推荐规则(关闭过严的类型规则,类型检查由 vue-tsc 负责)
* - Vue:eslint-plugin-vue 推荐规则,script 使用 typescript-eslint parser
*
* 目标:让 `npm run lint` 可运行,且不因存量代码风格问题阻塞日常开发。
*/
import js from '@eslint/js'
import vue from 'eslint-plugin-vue'
import tseslint from 'typescript-eslint'
import vueParser from 'vue-eslint-parser'
export default [
{
ignores: ['dist/**', 'node_modules/**', 'public/**', '*.min.js'],
},
js.configs.recommended,
...tseslint.configs.recommended,
...vue.configs['flat/recommended'],
{
files: ['**/*.{ts,tsx,js,jsx,mjs,cjs,vue}'],
languageOptions: {
parser: vueParser,
parserOptions: {
parser: tseslint.parser,
ecmaVersion: 'latest',
sourceType: 'module',
extraFileExtensions: ['.vue'],
},
globals: {
window: 'readonly',
document: 'readonly',
navigator: 'readonly',
console: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
URL: 'readonly',
Blob: 'readonly',
DOMParser: 'readonly',
Node: 'readonly',
process: 'readonly',
module: 'readonly',
require: 'readonly',
__dirname: 'readonly',
fetch: 'readonly',
},
},
rules: {
'no-console': 'off',
'no-empty': 'off',
'no-undef': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': 'off',
'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'off',
'vue/require-default-prop': 'off',
'vue/require-prop-types': 'off',
'vue/max-attributes-per-line': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/html-self-closing': 'off',
'vue/html-indent': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/attributes-order': 'off',
'vue/first-attribute-linebreak': 'off',
'vue/multiline-html-element-content-newline': 'off',
},
},
]
此差异已折叠。
......@@ -22,9 +22,12 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"eslint": "^9.39.5",
"eslint-plugin-vue": "^10.10.0",
"sass": "^1.72.0",
"terser": "^5.49.0",
"typescript": "^5.4.2",
"typescript-eslint": "^8.68.0",
"vite": "^5.1.6",
"vue-tsc": "^2.0.6"
}
......
/**
* 下载产物浮动卡片状态(Pinia store)
*
* 场景:AI 分析/对比分析完成后,除自动下载 Word 文件外,在页面右下角
* 提供一张浮动卡片,展示文件名 / 大小,并支持「再次下载」与「关闭」。
*
* 生命周期约定:
* - show(): 显示新卡片前先 revoke 上一个卡片持有的 ObjectURL,避免内存泄漏
* - redownload(): 复用同一 Blob/HTML 重新触发下载
* - close(): 释放 ObjectURL 并隐藏卡片
*
* @author czj
* @date 2026-08-26
*/
import { defineStore } from 'pinia'
/** 可重复下载的产物描述(由导出工具创建,生命周期由本 store 统一管理) */
export interface DownloadArtifact {
/** 下载文件名(含扩展名,如 AI_分析报告_登录基准任务.doc) */
filename: string
/** 文件大小(字节) */
size: number
/** 重新触发一次下载 */
redownload: () => void
/** 释放底层 ObjectURL 等资源(调用后不可再 redownload) */
revoke: () => void
}
interface DownloadArtifactState {
visible: boolean
artifact: DownloadArtifact | null
}
function makeState(): DownloadArtifactState {
return {
visible: false,
artifact: null,
}
}
export const useDownloadArtifactStore = defineStore('downloadArtifact', {
state: (): DownloadArtifactState => makeState(),
actions: {
/**
* 显示新的下载产物卡片。
* 若已有卡片,先释放旧资源再替换。
*/
show(artifact: DownloadArtifact) {
this.releaseCurrent()
this.artifact = artifact
this.visible = true
},
/** 重新下载当前产物 */
redownload() {
if (this.artifact) {
this.artifact.redownload()
}
},
/** 关闭并释放资源 */
close() {
this.releaseCurrent()
this.visible = false
},
/** 释放当前产物持有的资源,但保持卡片隐藏状态不变 */
releaseCurrent() {
if (this.artifact) {
try {
this.artifact.revoke()
} catch {
/* 资源释放失败不影响主流程 */
}
this.artifact = null
}
},
},
})
\ No newline at end of file
......@@ -1031,6 +1031,27 @@ export interface ProjectReportListResponse {
// ==================== AI 分析 ====================
/** AI 分析文档章节表格 */
export interface AiAnalysisDocumentSectionTable {
headers: string[]
rows: any[][]
}
/** AI 分析文档子章节 */
export interface AiAnalysisDocumentSubSection {
title: string
content: string
}
/** AI 分析文档章节 */
export interface AiAnalysisDocumentSection {
id: string
title: string
content: string
tables?: AiAnalysisDocumentSectionTable[]
subSections?: AiAnalysisDocumentSubSection[]
}
/** AI 分析响应 */
export interface AiAnalysisResponse {
executionId: string
......@@ -1044,4 +1065,10 @@ export interface AiAnalysisResponse {
comparison?: Record<string, any>[]
rawResponse?: string
source: 'ai' | 'rule'
// 正式文档字段(Phase 2 新增)
documentTitle?: string
documentMarkdown?: string
documentHtml?: string
documentSections?: AiAnalysisDocumentSection[]
generatedAt?: string
}
\ No newline at end of file
此差异已折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论