From 7a48af3e9e209f69c82d28aa0d19764bea4cea49 Mon Sep 17 00:00:00 2001 From: simonkoson <28867558@qq.com> Date: Mon, 6 Jul 2026 20:38:33 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20AI=E8=AF=8A=E6=96=AD=E6=91=98=E8=A6=81?= =?UTF-8?q?=E6=AD=A3=E5=88=99=E6=8F=90=E5=8F=96=20+=20=E4=BB=AA=E8=A1=A8?= =?UTF-8?q?=E7=9B=98=E6=8E=92=E5=BA=8F/=E5=AE=BD=E5=BA=A6=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DiagnosisSummary extractSection 改为正则模糊匹配,兼容 DeepSeek 不同标题格式 - 仪表盘近12期柱状图改为从左到右按播出时间升序 - 全局内容区宽度参数定位:.app-content max-width 1190px + width 100%(修复 Ant Design flex 下 margin auto shrink-to-fit) - Dashboard.css 去除多余 max-width 限制 Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 7 +++- .../components/Analytics/DiagnosisSummary.jsx | 39 ++++++++++++------- frontend/src/components/Layout/AppLayout.css | 6 ++- frontend/src/pages/Dashboard/Dashboard.css | 4 +- frontend/src/pages/Dashboard/Dashboard.jsx | 1 + 5 files changed, 38 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 21e1dea..c510c40 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,8 +13,8 @@ ## 🔖 状态栏(每次结束 session 前必须更新这三行) - - **最后更新**:Claude(顾问+动手)| 2026-07-03 - - **当前状态一句话**:L4 AI 诊断报告已实现(DeepSeek V4 Pro 生成、摘要块+详情页、三档自适应、22期内容摘要卡已入库)。收视分析看板 L1-L4 全部完成。 + - **最后更新**:Claude Opus(顾问+动手)| 2026-07-06 + - **当前状态一句话**:Bug 修复轮完成——AI 诊断摘要块正则提取、仪表盘柱状图排序、全局页面宽度参数定位并调优(1190px)。收视分析看板 L1-L4 + 仪表盘均已就绪。 - **下一个动手的人从这里开始**:见下方「⏩ 交接备注」 --- @@ -84,6 +84,7 @@ ## 4. 已完成(只追加,最新在上) + - [2026-07-06] **三处 Bug 修复 + 全局宽度调优**:① AI 诊断摘要块 `extractSection` 从硬编码 `indexOf` 改为正则模糊匹配,解决 DeepSeek 返回标题格式不一致导致干条不显示的问题;② 仪表盘近 12 期柱状图加 `.reverse()` 改为从左到右按播出时间升序;③ 全局内容区宽度:定位到 `.app-content`(`AppLayout.css`)的 `max-width` 是唯一有效参数(必须搭配 `width: 100%`,否则 Ant Design flex 布局下 `margin: 0 auto` 会触发 shrink-to-fit),最终定为 **1190px**。 - [2026-07-03] **L4 AI 诊断报告**:① Prompt 4(内容摘要卡,MiMo 生成)+ Prompt 5(诊断报告,DeepSeek V4 Pro)撰写完成;② 22 期文稿摘要卡批量生成并导入 episodes.content_digest(004 迁移);③ 后端 POST /api/analytics/diagnosis-report 端点(组装数据+调 DeepSeek+内存缓存);④ 前端摘要块 DiagnosisSummary(左右两块、三档自适应色调、可滚动、粗体渲染)+ 详情页 DiagnosisReport(react-markdown 渲染+重新生成+免责声明);⑤ 话题性(社交货币)三维框架设计(大众认知度/降维切口/惊奇密度)。模型选型:DeepSeek V4 Pro 胜出(vs Qwen-Max 对比测试)。 - [2026-07-03] **收视分析看板 React 前端实现(L1–L3)**:① 指标卡 5 列(均值/基础达标率/摸高完成率含四档动画/期次数/年度目标);② 走势折线图(dataZoom 滑块+确认按钮范围过滤,下游模块联动);③ 双列对比区(左列季度+编导、右列题材饼图+条形图);④ 双引擎象限图(题材热度×叙事结构散点,气泡编码份额/三色/钩子强度)。附带:25 期真实收视+AI 标签数据导入、侧边栏 fixed 定位、滑块滚轮冲突修复。后端 analytics API 增返 4 个 AI 标签字段。 - [2026-07-03] 看板升级子项目原型阶段收工:L2 打标流水线就绪(GT v0.6.0 / 25 期 / 三个 Prompt 可用)+ L3 HTML 原型验证通过(双引擎象限图+5 模块+视觉定稿)+ 003 迁移已执行(episodes +7 AI 标签列)。交付清单见寄存条。 @@ -131,6 +132,7 @@ - **L4 摘要块视觉**:左块固定粉红色系(`#c0584f`),右块跟 tier 三档变色(绿/蓝/红)。两块均可滚动查看完整内容。 - **L4 话题性框架**:社交货币理论三维(大众认知度/降维切口/惊奇密度),写入摘要卡不作为独立 AI 标签字段。 - **L4 prompt5 约束**:① 提及期次必须用「第X期《节目名》」格式;② 话题性必须独立分析段;③ 军事装备分类须准确(枪械≠冷兵器)。 + - **全局页面内容区宽度**:唯一有效参数是 `frontend/src/components/Layout/AppLayout.css` 里 `.app-content` 的 `max-width`(当前 **1190px**)。**必须保留 `width: 100%` + `box-sizing: border-box`**,不能单独用 `margin: 0 auto`(Ant Design flex 布局下会触发 shrink-to-fit,宽度锁死无法调节)。制片人直接说"调这个参数"即可。 --- @@ -149,6 +151,7 @@ 3. **已验证好用的布局别为对齐整体重构**(flex 改 Grid 连坏两轮);样式微调优先。 4. Cline 报告自相矛盾时,**把源码发给 Opus 一眼定位**胜过让 Cline 反复试错。 5. 像素级打磨边际收益递减,**见好就收**,记 backlog 改天连视觉规范统一弄。 + 6. **页面宽度问题**:Ant Design `Layout > Content` 是 flex 子元素,`margin: 0 auto` 不搭配显式 `width` 会导致元素缩到内容宽度(shrink-to-fit),改 `max-width` 数值完全无效。必须同时写 `width: 100%`。 --- diff --git a/frontend/src/components/Analytics/DiagnosisSummary.jsx b/frontend/src/components/Analytics/DiagnosisSummary.jsx index 268b0f7..0737711 100644 --- a/frontend/src/components/Analytics/DiagnosisSummary.jsx +++ b/frontend/src/components/Analytics/DiagnosisSummary.jsx @@ -10,18 +10,29 @@ import { generateDiagnosisReport } from '../../services/analyticsService' * - 从 markdown 提取核心发现和行动建议 */ -/** 从 markdown 中提取指定章节内容 */ -function extractSection(markdown, startMarker, endMarkers) { +/** 从 markdown 中提取指定章节内容(正则模糊匹配) */ +function extractSection(markdown, keyword, endKeywords) { if (!markdown) return '' - const startIdx = markdown.indexOf(startMarker) - if (startIdx === -1) return '' - const afterStart = startIdx + startMarker.length - // 找最近的结束标记 + // 用正则匹配:任意数量的 # 开头,后面包含关键词的行 + const startRegex = new RegExp(`^#{1,6}\\s*.*${keyword}.*$`, 'm') + const match = markdown.match(startRegex) + if (!match) return '' + const afterStart = match.index + match[0].length + // 找最近的结束标记(下一个同级或更高级标题) let endIdx = markdown.length - for (const marker of endMarkers) { - const idx = markdown.indexOf(marker, afterStart) - if (idx !== -1 && idx < endIdx) { - endIdx = idx + if (endKeywords && endKeywords.length > 0) { + for (const kw of endKeywords) { + const endRegex = new RegExp(`^#{1,6}\\s*.*${kw}.*$`, 'm') + const endMatch = markdown.substring(afterStart).match(endRegex) + if (endMatch && (afterStart + endMatch.index) < endIdx) { + endIdx = afterStart + endMatch.index + } + } + } else { + // 没有 endKeywords 时,找下一个同级标题作为结束 + const nextHeading = markdown.substring(afterStart).match(/^#{1,6}\s+/m) + if (nextHeading) { + endIdx = afterStart + nextHeading.index } } return markdown.substring(afterStart, endIdx).trim() @@ -156,10 +167,10 @@ function DiagnosisSummary({ episodes, yearlyTarget, selectedYear }) { // 提取核心发现和行动建议 const markdown = report?.report_markdown || '' - // 核心发现:从 "#### 二、核心发现" 到 "#### 三、" - let coreFindings = extractSection(markdown, '#### 二、核心发现', ['#### 三、']) - // 行动建议:从 "#### 四、行动建议" 到末尾 - let actions = extractSection(markdown, '#### 四、行动建议', []) + // 核心发现:关键词匹配"核心发现",到"深度分析"或"三、" + let coreFindings = extractSection(markdown, '核心发现', ['深度分析', '三、']) + // 行动建议:关键词匹配"行动建议",到末尾 + let actions = extractSection(markdown, '行动建议', []) const coreLines = parseMarkdownLines(coreFindings) const actionLines = parseMarkdownLines(actions) diff --git a/frontend/src/components/Layout/AppLayout.css b/frontend/src/components/Layout/AppLayout.css index 562b505..bb65e76 100644 --- a/frontend/src/components/Layout/AppLayout.css +++ b/frontend/src/components/Layout/AppLayout.css @@ -18,6 +18,8 @@ .app-main { min-height: 100vh; margin-left: 220px; + width: calc(100vw - 220px); + overflow-x: hidden; } .app-header { @@ -35,6 +37,8 @@ padding: 24px; background: var(--color-bg-cream); min-height: calc(100vh - 64px); - max-width: 1400px; + width: 100%; + max-width: 1190px; margin: 0 auto; + box-sizing: border-box; } diff --git a/frontend/src/pages/Dashboard/Dashboard.css b/frontend/src/pages/Dashboard/Dashboard.css index 1a20717..3f71e0d 100644 --- a/frontend/src/pages/Dashboard/Dashboard.css +++ b/frontend/src/pages/Dashboard/Dashboard.css @@ -1,6 +1,6 @@ .dashboard { - max-width: 1200px; - padding: 12px; + padding: 12px 0; + box-sizing: border-box; } /* ===== Banner ===== */ diff --git a/frontend/src/pages/Dashboard/Dashboard.jsx b/frontend/src/pages/Dashboard/Dashboard.jsx index 843cdd7..6d06a8d 100644 --- a/frontend/src/pages/Dashboard/Dashboard.jsx +++ b/frontend/src/pages/Dashboard/Dashboard.jsx @@ -86,6 +86,7 @@ function Dashboard() { const displayEpisodes = episodes .filter(e => e.audience_share != null) .slice(0, 12) + .reverse() const bestEpisode = [...displayEpisodes].sort((a, b) => Number(b.audience_share) - Number(a.audience_share))[0]