From 96b295f919e41ef26038b9aa1de0b98eb8d90703 Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Sun, 19 Apr 2026 02:04:40 +0800 Subject: [PATCH] =?UTF-8?q?2026-04-19-02-00-33=20=E5=BB=BA=E7=AB=8B?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=BC=96=E7=BA=82=E5=B7=A5=E4=BD=9C=E6=B5=81?= =?UTF-8?q?=EF=BC=9A=E5=B7=A5=E7=A8=8B=E5=88=86=E6=9E=90=E6=A1=86=E6=9E=B6?= =?UTF-8?q?=E3=80=81=E7=BB=8F=E9=AA=8C=E8=AE=B0=E5=BD=95=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E3=80=81=E5=B7=A5=E4=BD=9C=E6=B5=81=E8=A7=84=E8=8C=83=E5=88=B6?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 工程分析/工作流规范.md | 283 +++++++++++++++++++++++++ 工程分析/工程整体分析.md | 174 +++++++++++++++ 工程分析/经验记录.md | 443 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 900 insertions(+) create mode 100644 工程分析/工作流规范.md create mode 100644 工程分析/工程整体分析.md create mode 100644 工程分析/经验记录.md diff --git a/工程分析/工作流规范.md b/工程分析/工作流规范.md new file mode 100644 index 0000000..68e0d23 --- /dev/null +++ b/工程分析/工作流规范.md @@ -0,0 +1,283 @@ +# 代码编纂工作流规范 + +> 版本:V1.0 +> 适用范围:本项目所有代码修改、功能迭代、Bug 修复、重构任务 +> 生效日期:2026-04-19 + +--- + +## 一、工作流总览 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 0. 记录开始时间戳 │ +│ 1. 阅读工程整体分析 + 经验记录 │ +│ 2. 需求分析 → 写入 需求分析-{时间}.md → 【用户审核】 │ +│ 3. 实现方案 → 写入 实现方案-{时间}.md → 【用户审核】 │ +│ 4. 测试方案 → 写入 测试方案-{时间}.md → 【用户审核】 │ +│ 5. 执行前再次阅读 经验记录.md(防止踩坑) │ +│ 执行修改 │ +│ 执行后向 经验记录.md 追加新踩坑记录(四段式) │ +│ 6. Git 提交 → Gitea 推送 → 提醒用户 │ +│ 7. npm 重新构建 + 部署 │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 二、步骤详解 + +### 步骤 0:记录开始时间戳 + +每次接收到修改需求时,首先获取当前时间,格式为: +``` +{Year}-{Mon}-{Day}-{Hour}-{Min}-{Sec} +``` + +**示例**:`2026-04-19-02-00-33` + +该时间戳贯穿整个工作流,用于命名所有相关文档。 + +--- + +### 步骤 1:阅读工程分析文档 + +**必做事项**: +- 阅读 `工程整体分析.md`,确认当前架构、技术栈、高风险区域 +- 阅读 `经验记录.md`,回顾与本次需求相关的历史踩坑记录 +- 若 `工程分析` 文件夹或上述文档不存在,立即创建 + +--- + +### 步骤 2:需求分析 + +**输出文件**:`需求分析-{时间戳}.md` + +**必须包含的内容**: +1. **需求背景**:用户原始需求的转述 +2. **需求拆解**:将大需求拆分为可执行的原子任务 +3. **影响范围**:列出需要修改的文件清单(基于工程整体分析中的高风险区域判断) +4. **优先级排序**:P0(阻塞)/ P1(重要)/ P2(优化) +5. **验收标准**:明确"做到什么程度算完成" + +**流转规则**: +- 文档写完后,**必须停止执行**,等待用户二次人工审核确认 +- 用户确认后,方可进入步骤 3 + +--- + +### 步骤 3:实现方案 + +**输出文件**:`实现方案-{时间戳}.md` + +**必须包含的内容**: +1. **方案概述**:整体技术思路(1-3 句话) +2. **详细步骤**:按文件/模块逐条列出修改点,每条包含: + - 目标文件路径 + - 具体修改内容(新增/删除/修改的代码逻辑) + - 与现有逻辑的兼容策略(如何处理旧数据、默认值、降级) +3. **依赖关系**:哪些修改有先后顺序,哪些可以并行 +4. **风险预案**:如果方案失败,回滚策略是什么 + +**流转规则**: +- 文档写完后,**必须停止执行**,等待用户二次人工审核确认 +- 用户确认后,方可进入步骤 4 + +--- + +### 步骤 4:测试方案 + +**输出文件**:`测试方案-{时间戳}.md` + +**必须包含的内容**: +1. **测试范围**:哪些功能需要验证 +2. **测试步骤**:手动的操作路径(因本项目无自动化测试) +3. **预期结果**:每一步的正确输出是什么 +4. **边界场景**:异常输入、空值、大数据量、快速切换路由等 +5. **回滚检查**:验证失败后如何恢复到修改前状态 + +**流转规则**: +- 文档写完后,**必须停止执行**,等待用户二次人工审核确认 +- 用户确认后,方可进入步骤 5 + +--- + +### 步骤 5:执行修改 + 经验沉淀 + +**执行前**: +- 最后一次阅读 `经验记录.md`,确认本次修改不会触发已知坑点 +- 若发现新的潜在风险,在实现方案文档中补充后再执行 + +**执行中**: +- 严格按照实现方案的步骤执行,不擅自扩大修改范围 +- 若遇到方案外的意外问题,暂停执行,记录问题,与用户沟通后再继续 + +**执行后**: +- 按测试方案逐项验证 +- 向 `经验记录.md` 追加新记录(仅当实际遇到问题时),格式如下: + +```markdown +## 记录 N:{问题标题} + +**A. 具体问题** +{问题描述} + +**B. 产生问题原因** +{根因分析} + +**C. 解决问题方案** +{具体修复步骤} + +**D. 后续如何避免问题** +{给未来自己的警告} +``` + +--- + +### 步骤 6:Git 备份 + +**提交规范**: +```bash +git add -A +git commit -m "{时间戳} {修改简要描述}" +git push origin main +``` + +**必须包含的信息**: +- 时间戳(与需求分析文档一致) +- 本次修改的简要描述(1-2 句话) + +**完成后**: +- 明确提醒用户已完成 Gitea 备份 + +--- + +### 步骤 7:重新部署 + +**标准部署脚本**: +```powershell +# 1. 停止旧服务 +Stop-Process -Id (Get-NetTCPConnection -LocalPort 4173 -ErrorAction SilentlyContinue).OwningProcess -Force -ErrorAction SilentlyContinue + +# 2. 重新构建 +npm run build + +# 3. 以独立进程启动预览服务(避免后台任务超时杀死) +Start-Process -FilePath "powershell.exe" -ArgumentList "-Command","cd '$PWD'; npm run preview -- --host" -WindowStyle Hidden + +# 4. 验证 +Start-Sleep -Seconds 3 +Invoke-WebRequest -Uri http://127.0.0.1:4173/ -UseBasicParsing -TimeoutSec 5 +``` + +**注意**: +- **严禁**使用 `Shell(run_in_background=true)` 运行 `npm run preview`,因为任务超时机制(默认 60s)会强制终止 preview 进程,导致服务中断(参见经验记录-21)。 +- 必须使用 `Start-Process` 创建独立的 Windows 进程。 + +--- + +## 三、禁忌清单(严格执行) + +| 编号 | 禁忌行为 | 后果 | 正确做法 | +|------|----------|------|----------| +| 1 | 跳过需求分析文档直接写代码 | 需求理解偏差,返工 | 必须先写文档,等用户确认 | +| 2 | 跳过实现方案文档直接改代码 | 架构混乱,影响范围失控 | 必须先写文档,等用户确认 | +| 3 | 跳过测试方案文档直接上线 | 遗漏边界场景,线上故障 | 必须先写文档,等用户确认 | +| 4 | 使用 `Shell(run_in_background=true)` 运行 `npm run preview` | 60s 后服务被强制杀死 | 使用 `Start-Process` 启动独立进程 | +| 5 | 修改后不更新 `经验记录.md` | 重复踩坑 | 每次遇到新问题必须追加记录 | +| 6 | `contentEditable` 插入多行缩进 HTML | 浏览器解析出额外文本节点,破坏排版 | 必须压缩为紧凑单行 HTML | +| 7 | 直接 `target.remove()` 删除 DOM 节点 | 撤销栈失效,WebKit 误删父级 `

` | 使用 `Range.selectNode + execCommand('delete')` | +| 8 | 将 `useRef` 作为自动保存唯一数据源 | StrictMode 下首次卸载覆盖有效 draft | 自动保存函数从最新 React state 读取 | +| 9 | 异步循环中用 `await setTimeout` 阻塞 | 抽帧/UI 更新被串行延迟 | 使用裸 `setTimeout` 推入事件队列 | +| 10 | 用 `body { padding }` 控制打印边距 | 第二页及后续页边距失效 | 使用 `@page { margin }` | + +--- + +## 四、文档模板 + +### 需求分析模板 + +```markdown +# 需求分析 — {时间戳} + +## 1. 需求背景 +{用户原始需求} + +## 2. 需求拆解 +- [ ] 任务 1:{描述} +- [ ] 任务 2:{描述} + +## 3. 影响范围 +| 文件 | 修改类型 | 风险等级 | +|------|----------|----------| +| {路径} | 新增/修改/删除 | 高/中/低 | + +## 4. 优先级 +- P0:{阻塞项} +- P1:{重要项} +- P2:{优化项} + +## 5. 验收标准 +- {标准 1} +- {标准 2} +``` + +### 实现方案模板 + +```markdown +# 实现方案 — {时间戳} + +## 1. 方案概述 +{整体思路} + +## 2. 详细步骤 + +### 步骤 1:{模块/文件} +**目标文件**:`{路径}` +**修改内容**:{详细描述} +**兼容策略**:{如何处理旧数据/默认值} + +### 步骤 2:{模块/文件} +... + +## 3. 依赖关系 +- {步骤 A} 必须在 {步骤 B} 之前执行 +- {步骤 C} 和 {步骤 D} 可并行 + +## 4. 风险预案 +- 若 {某步骤} 失败,回滚方式为 {...} +``` + +### 测试方案模板 + +```markdown +# 测试方案 — {时间戳} + +## 1. 测试范围 +{需要验证的功能点} + +## 2. 测试步骤与预期结果 + +### 场景 1:{正常流程} +1. 操作:{步骤} + 预期:{结果} +2. 操作:{步骤} + 预期:{结果} + +### 场景 2:{边界/异常} +... + +## 3. 回滚检查 +- 若测试失败,执行 `{命令}` 恢复到修改前状态 +``` + +--- + +## 五、附录:项目快速参考 + +- **开发**:`npm run dev`(端口 3000) +- **构建**:`npm run build` +- **预览**:`npm run preview -- --host`(端口 4173) +- **类型检查**:`npm run lint` +- **Gitea 仓库**:`http://192.168.31.5:5002/admin/Mdeical_Sur_Report.git` +- **当前分支**:`main` diff --git a/工程分析/工程整体分析.md b/工程分析/工程整体分析.md new file mode 100644 index 0000000..9256d35 --- /dev/null +++ b/工程分析/工程整体分析.md @@ -0,0 +1,174 @@ +# 手术图文病历报告系统 — 工程整体分析 + +> 版本:V1.3 +> 最后更新:2026-04-19 +> 分析维度:架构、数据流、核心模块、风险点、扩展方向 + +--- + +## 一、项目定位 + +**手术图文病历报告系统** 是一款面向医院手术室场景的纯前端单页应用(SPA),核心能力包括: + +- 富文本编辑器撰写结构化手术图文报告 +- 手术视频上传 + 自动/手动关键帧抽取,拖拽插入报告 +- 报告模板管理、用户权限(RBAC)、系统设置 +- 导出 PDF / JSON 格式报告与模板 + +**关键约束**:无后端服务器,所有数据持久化在浏览器 `localStorage`(约 5MB 容量上限)。 + +--- + +## 二、技术栈 + +| 层级 | 技术 | +|------|------| +| 框架 | React 19 + TypeScript 5.8 | +| 构建 | Vite 6 | +| 样式 | Tailwind CSS v4(`@theme` / `@import "tailwindcss"` 新语法) | +| 路由 | React Router DOM v7 | +| 图标 | lucide-react | +| 动画 | motion | +| AI SDK | `@google/genai`(已安装,业务代码中**未实际调用**) | + +**无 ESLint、无 Prettier、无单元测试框架**,唯一类型检查为 `tsc --noEmit`。 + +--- + +## 三、项目结构 + +``` +src/ +├── components/ +│ └── Sidebar.tsx # 左侧导航(角色过滤、自动折叠) +├── pages/ +│ ├── Login.tsx # 登录页 + 全局 initData(默认用户/模板/字段/素材) +│ ├── Dashboard.tsx # 工作台(统计卡片、SVG 趋势图) +│ ├── ReportEditor.tsx # 核心:报告编辑器(2,200+ 行,最大文件) +│ ├── ReportManage.tsx # 报告列表(搜索、筛选、批量操作、历史回溯) +│ ├── ReportView.tsx # 报告只读查看 + 打印 +│ ├── TemplateManage.tsx # 模板编辑器(1,600+ 行,自定义 Undo/Redo) +│ ├── UserManage.tsx # 用户管理(RBAC、签名上传、模板权限) +│ └── SystemSettings.tsx # 系统设置(抽帧配置、AI API、默认模板) +├── utils/ +│ ├── storage.ts # localStorage / sessionStorage 封装 +│ ├── print.ts # iframe 打印工具(A4 样式、@page 边距) +│ └── defaultContent.ts # 默认模板 HTML(腹腔镜胆囊切除术报告) +├── App.tsx # BrowserRouter + 路由表 +├── main.tsx # React 根挂载(StrictMode) +├── types.ts # 核心 TypeScript 类型 +└── index.css # Tailwind 入口 + @theme 变量 + 打印媒体查询 +``` + +--- + +## 四、数据持久化架构 + +**无全局状态库**(无 Redux/Zustand/Context)。每个页面独立通过 `useState` + `useEffect` 管理状态,`localStorage` 即数据库。 + +| localStorage Key | 说明 | +|------------------|------| +| `users` | 用户列表 | +| `currentUser` | 当前登录用户 | +| `reports` | 报告列表 | +| `templates` | 模板列表 | +| `systemSettings` | 系统设置 | +| `formFieldsConfig` | 动态字段配置 | +| `imageAssets` | 系统素材库(Base64 图片) | +| `reportEditorDraft_${username}` | 每用户报告草稿 | +| `customTimeFormats` | 用户自定义时间格式缓存 | + +**容量风险**:关键帧采用 Canvas 压缩(最大宽度 800px、JPEG 质量 0.6)以控制体积。`storage.ts` 异常已改为 `console.error` 输出,不再静默吞掉。 + +--- + +## 五、核心模块深度分析 + +### 5.1 富文本编辑器(ReportEditor / TemplateManage) + +- **底层**:原生 `contentEditable` + `document.execCommand` +- **智能字段(Smart Field)三层嵌套**: + ```html + + 标签: + + ​ + ``` + - 外层 `contenteditable="false"` 保护标签不被逐字删除 + - 输入层 `data-bind` 实现与右侧表单双向绑定 + - 末尾追加 `​`(零宽空格)防止排版换行异常 +- **图片占位符**:``,支持 `data-mode="frame|manual"` 分类隔离 +- **自定义 Undo/Redo**(TemplateManage):基于 HTML 字符串快照的 `undoStack`/`redoStack`,完全接管撤销逻辑 + +### 5.2 视频分析(ReportEditor) + +- 上传本地视频 → 生成 object URL +- 自动抽帧:按 `systemSettings.framePositions` 百分比位置逐帧截图 +- 手动截图:点击按钮从当前播放时间捕获 +- 图片压缩:Canvas 等比缩放至最大 800px 宽,JPEG 质量 0.6 +- 非阻塞 `setTimeout` 队列式自动帧插入,避免阻塞抽帧循环 + +### 5.3 打印系统(utils/print.ts) + +- 创建隐藏 iframe,写入带 A4 打印样式的 HTML +- `@page { margin: 15mm 10mm; }` 为**每一页**纸张独立分配边距 +- `body { padding: 0 }` — 不可用 body padding 代替 @page margin +- 打印前临时设置 `document.title` 并注入 iframe ``,确保 PDF 默认文件名正确 + +### 5.4 角色权限(RBAC) + +| 角色 | 权限 | +|------|------| +| `super` | 全部页面、全部数据 | +| `admin` | 仅管理本科室用户;可管理模板;不能看系统设置中的 AI 配置 | +| `user` | 仅创建/查看/编辑自己的报告;可见被分配模板 | + +--- + +## 六、高风险修改区域 + +以下模块**牵一发而动全身**,修改时必须同步检索所有相关文件: + +1. **智能字段结构** → `types.ts`、`defaultContent.ts`、`ReportEditor.tsx`、`TemplateManage.tsx`、`index.css`、`print.ts` +2. **图片占位符(创建/填充/删除恢复)** → `defaultContent.ts`、`ReportEditor.tsx`、`TemplateManage.tsx` +3. **打印样式** → `print.ts`、`index.css`(`@media print`) +4. **时间/日期格式** → `types.ts`、`ReportEditor.tsx`、`TemplateManage.tsx` +5. **数据初始化/默认值** → `Login.tsx`、`SystemSettings.tsx` +6. **自动保存/草稿** → `ReportEditor.tsx` 中的 `saveDraftToStorage`、`stateRef`、`contentRef` + +--- + +## 七、构建与部署 + +```bash +# 开发 +npm run dev # vite --port=3000 --host=0.0.0.0 + +# 生产构建 +npm run build # vite build → dist/ +npm run preview # vite preview(默认端口 4173) + +# 类型检查 +npm run lint # tsc --noEmit +``` + +**当前部署状态**:通过 `npm run build && npm run preview -- --host` 在本机运行。 + +--- + +## 八、安全与限制 + +1. **密码明文存储**:`localStorage` 中 `users` 数组明文保存密码,纯前端架构固有限制 +2. **XSS 风险**:报告/模板内容直接以 HTML 字符串存储并在 `innerHTML` 中渲染 +3. **Gemini API Key**:通过 Vite `define` 注入客户端,构建后 key 暴露在静态 JS 中(当前源码未实际调用) +4. **无 HTTPS 强制**:Docker 部署默认 HTTP 80 端口 + +--- + +## 九、默认账号 + +| 账号 | 密码 | 角色 | +|------|------|------| +| admin | 123456 | super | +| manager | 123456 | admin | +| doctor / 0001 | 123456 | user | diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md new file mode 100644 index 0000000..74ea8f7 --- /dev/null +++ b/工程分析/经验记录.md @@ -0,0 +1,443 @@ +# 经验记录 + +> 本文档为项目统一知识库,记录开发过程中遇到的关键问题及解决方案。每次执行修改前必须阅读,防止重复踩坑。 +> 记录格式:A. 具体问题 → B. 产生问题原因 → C. 解决问题方案 → D. 后续如何避免问题 + +--- + +## 记录 1:report-editor 新建报告时显示空白模板 + +**A. 具体问题** +超级管理员进入 `/report-editor`(新建报告)时,编辑区域为纯白色空白,顶部模板选择器显示"无",但 system-settings 中已配置了默认模板。 + +**B. 产生问题原因** +1. `ReportEditor.tsx` 在组件卸载时会自动将当前编辑器内容保存为草稿。即使用户未输入任何内容,保存的 `content` 也是空字符串 `""`。 +2. 初始化 effect 中判断草稿是否有效的条件仅使用了 `typeof draft.content === 'string'`,空字符串满足该条件,导致编辑器被填充为空白 HTML,并将 `contentLoadedRef.current` 设为 `true`。 +3. 由于 `contentLoadedRef.current` 已被置为 `true`,后续加载 `settings.defaultTemplate` 的默认模板分支被完全跳过。 + +**C. 解决问题方案** +1. 在 `saveDraftToStorage` 中将当前 `loadedTemplateId` 一并存入 draft。 +2. 将四处草稿恢复的判断条件从 `typeof draft.content === 'string'` 收紧为 `typeof draft.content === 'string' && draft.content.trim().length > 0`。 +3. 恢复草稿时同步执行 `setLoadedTemplateId(draft.loadedTemplateId || '')`。 + +**D. 后续如何避免问题** +- 在前端使用 contentEditable 的自动保存机制时,保存和恢复草稿都应增加对空/仅空白内容的过滤。 +- 若草稿与某个业务状态(如当前模板 ID)强关联,应确保两者一并持久化和恢复,避免状态不一致。 + +--- + +## 记录 2:关键帧一键插入占位符功能实现 + +**A. 具体问题** +用户希望视频分析面板中的关键帧截图除了拖拽插入外,还能通过点击 "插入" 按钮一键自动填充到编辑器中第一个空置的 `image-placeholder`。 + +**B. 产生问题原因** +原先仅支持拖拽方式将关键帧放入占位符。当关键帧数量多或占位符位置较远时,操作不便。且 `handleDrop` 中的填充逻辑未抽离,无法被其他交互方式复用。 + +**C. 解决问题方案** +1. 将 `handleDrop` 中的 HTML 填充逻辑抽离为 `fillPlaceholder(placeholder, frame)` 公共函数。 +2. 新增 `insertFrameToPlaceholder(frame)` 函数:通过 `editorRef.current.querySelector('.image-placeholder:not(.has-image)')` 查找第一个空置占位符。 +3. 在关键帧卡片底部新增 "插入" 按钮,使用 `opacity-0 group-hover:opacity-100 transition-opacity`,并通过 `e.stopPropagation()` 避免触发卡片的视频跳转 `onClick`。 + +**D. 后续如何避免问题** +- 当同一交互效果需要支持多种触发方式时,应将核心逻辑抽离为独立函数,避免重复代码。 +- 在可点击子元素上务必注意事件冒泡控制,防止触发父级不必要的副作用。 + +--- + +## 记录 3:路由切换后视频分析图片丢失 + +**A. 具体问题** +在 `/report-editor` 中上传视频、自动摘取关键帧后,切换到 `/report-manage` 再返回 `/report-editor`,右侧「视频分析」面板中的所有截图和关键帧全部消失。 + +**B. 产生问题原因** +1. `ReportEditor.tsx` 在组件卸载时通过 `stateRef.current` 保存草稿到 `localStorage`。 +2. 初始化 `useEffect` 从 draft 恢复数据时,仅通过 `setState` 更新了 React state,但 **没有同步更新 `stateRef.current`**。 +3. 离开页面时,`stateRef.current` 仍保存着初始值(空数组),导致 `saveDraftToStorage()` 用空数组覆盖了 localStorage 中的 draft。 + +**C. 解决问题方案** +在 `ReportEditor.tsx` 的所有数据恢复入口中,恢复 `reportData`、`videos`、`capturedFrames` 后立即同步赋值给 `stateRef.current`。 + +**D. 后续如何避免问题** +- 当使用 `useRef` 作为「自动保存」的数据快照时,**任何从持久化存储恢复数据到 React state 的操作,必须同步更新对应的 ref**。 +- 在涉及草稿/自动保存的功能中,应定期审查所有数据恢复路径,确保 ref 与 state 的一致性。 + +--- + +## 记录 4:路由切换后报告内容、基本信息、视频分析全部丢失 + 自动帧插入 UI 延迟刷新 + +**A. 具体问题** +1. 在 `/report-editor` 中编辑报告后,切换到 `/report-manage` 再返回,**报告内容变空、基本信息清空、视频分析数据全部丢失**。 +2. 开启「自动帧插入」后,自动关键帧摘取过程中右侧关键帧列表和 placeholder 中的图片**不会逐张实时更新**。 + +**B. 产生问题原因** +1. **数据丢失**:在初始化 `useEffect` 中,将 `stateRef.current` 的同步赋值放在了 `if (editorRef.current && draft.content.trim().length > 0)` 条件块内部。当 `editorRef` 尚未挂载或 `draft.content` 为空时,`stateRef.current` 就得不到同步。 +2. **UI 延迟**:`autoCaptureFrames` 是 async 函数,内部循环中连续调用 `setCapturedFrames`。React 18 的自动批处理机制在异步函数中会合并状态更新,DOM 重渲染被推迟到整个循环结束后。 + +**C. 解决问题方案** +1. 将 `stateRef.current` 的同步赋值**移到 `editorRef.current/content` 判断条件的外部**。 +2. 在 `autoCaptureFrames` 的 for 循环中,将 `setCapturedFrames` 包裹在 `flushSync(() => { ... })` 中,强制每一帧被摘取后立即触发 DOM 更新。 + +**D. 后续如何避免问题** +- ref 的同步赋值绝对不能依赖于任何与 UI 渲染相关的条件判断。 +- 在异步函数中需要让用户看到实时状态更新时,应使用 `flushSync` 强制同步渲染。 + +--- + +## 记录 5:路由切换后所有内容仍然丢失——彻底重构自动保存机制 + +**A. 具体问题** +在 `/report-editor` 中编辑报告后,切换到 `/report-manage` 再返回,报告编辑器内容、基本信息、视频列表、关键帧截图**全部丢失**。 + +**B. 产生问题原因** +1. 自动保存机制过度依赖 `stateRef` 和 `contentRef` 作为"数据快照"。 +2. **React 18 `StrictMode`** 在开发/预览环境下会执行"挂载 → 立即卸载 → 重新挂载"。在首次模拟卸载时,`stateRef.current` 仍然是组件创建时的初始空值。 +3. 组件卸载(cleanup)时调用保存,用这个空值**覆盖了 localStorage 中已有的正确 draft**。 + +**C. 解决问题方案** +1. **彻底重构 `saveDraftToStorage`**:不再读取 `contentRef.current` 和 `stateRef.current`,而是直接从最新的 React state 和 `editorRef.current?.innerHTML` 获取数据。`useCallback` 的 dependency 数组包含所有相关 state,确保闭包永远绑定当前渲染周期的最新 state。 +2. 将 `beforeunload` 和 `visibilitychange` 事件处理器直接绑定到 `saveDraftToStorage`,effect 的 dependency 改为 `[saveDraftToStorage]`。 + +**D. 后续如何避免问题** +- **永远不要将 `useRef` 作为自动保存的唯一数据源**。ref 在 React 18 `StrictMode` 的模拟卸载阶段仍然保持初始值,会导致用空数据覆盖有效持久化数据。 +- 自动保存函数应直接从最新的 React state 和 DOM 读取数据,通过 `useCallback` + 完整的 dependency 数组保证闭包始终新鲜。 + +--- + +## 记录 6:编辑器内容和关键帧在路由切换后仍然丢失——从 Ref 读取避免闭包陷阱和 DOM 失效 + +**A. 具体问题** +在 `/report-editor` 中编辑报告后,切换到 `/report-manage` 再返回:报告内容全部丢失;视频分析面板中的自动关键帧和手动截图全部丢失。 + +**B. 产生问题原因** +1. **闭包陷阱**:`saveDraftToStorage` 直接从 React state 读取,但代码中存在 `setCapturedFrames(nextFrames); saveDraftToStorage();` 的写法。由于 `setState` 是异步的,`saveDraftToStorage` 闭包中读到的 `capturedFrames` 仍然是旧值。 +2. **卸载时 DOM 失效**:组件卸载时 React 开始销毁 DOM 树,`editorRef.current` 可能已经变为 `null`,`content: editorRef.current?.innerHTML || ''` 会把空字符串保存到 draft 中。 +3. **`contentRef` 更新遗漏**:在 `handleEditorClick` 中删除 placeholder 后,直接调用了 `saveDraftToStorage()`,但没有先更新 `contentRef.current`。 + +**C. 解决问题方案** +1. **重构 `saveDraftToStorage` 从 Ref 读取**:`content` 优先读取 `contentRef.current`(内存引用,卸载时仍稳定存在);`reportData`、`videos`、`capturedFrames` 全部从 `stateRef.current` 读取。 +2. **补齐 `contentRef` 遗漏**:在 `handleEditorClick` 的 `document.execCommand('delete')` 分支后,增加 `if (editorRef.current) contentRef.current = editorRef.current.innerHTML;`。 + +**D. 后续如何避免问题** +- 对于需要在异步操作或组件卸载时读取的"最新状态",**应优先使用 `useRef` 作为稳定的数据快照**,而不是依赖 React state 的闭包。 +- 任何直接操作 DOM 修改编辑器内容的代码,都必须**紧跟一行 `contentRef.current = editorRef.current.innerHTML`**。 + +--- + +## 记录 7:自动帧插入阻塞关键帧摘取——改为 setTimeout 非阻塞异步插入 + +**A. 具体问题** +开启「自动帧插入」后,点击「自动关键帧摘取」时,系统不是快速完成所有关键帧的摘取,而是每摘取一张就停下来等待插入延迟,整体过程非常缓慢。 + +**B. 产生问题原因** +`autoCaptureFrames` 的 `for` 循环内部,自动插入逻辑使用了 `await new Promise<void>(r => setTimeout(...))`,`await` 会暂停整个 `for` 循环的执行。 + +**C. 解决问题方案** +1. 将 `await new Promise(...)` 替换为 `setTimeout(...)`,把插入操作推入事件队列异步执行。 +2. 实现延迟叠加(顺序插入):通过 `settings.autoInsertFrameIndices.indexOf(i)` 计算当前帧是第几个需要插入的,延迟时间为 `baseDelay * (insertOrderIndex + 1)`。 +3. `setTimeout` 回调中实时查询 `.image-placeholder:not(.has-image)`,找到则插入,并同步更新 `contentRef.current` 和调用 `saveDraftToStorage()`。 + +**D. 后续如何避免问题** +- 在异步循环中,如果某个操作不需要依赖前一步的完成结果,**绝对不要使用 `await` 阻塞主循环**,应改用 `setTimeout` 或 `Promise.all` 实现并行/异步解耦。 +- 在 `setTimeout` 等异步回调中操作 DOM 时,应在回调触发时"实时查询"目标元素,而不是在循环中提前捕获元素引用。 + +--- + +## 记录 8:关键帧在路由切换后丢失——压缩 Canvas 分辨率并增加存储错误日志 + +**A. 具体问题** +报告编辑器内容和视频列表在路由切换后能正常保留,但视频分析面板中的自动摘取关键帧和手动截图全部丢失。 + +**B. 产生问题原因** +1. **LocalStorage 5MB 容量限制**:当前抽帧逻辑使用视频原始分辨率 + JPEG 质量 0.9,对于 1080p/4K 视频,单张 Base64 图片可达 300KB~1MB,十几张关键帧即可超过 5MB。 +2. **静默失败**:`storage.ts` 中的 `set` 方法捕获了 `QuotaExceededError` 但没有任何日志,导致用户和开发者都感知不到错误。 + +**C. 解决问题方案** +1. **压缩关键帧分辨率与质量**:Canvas 等比缩放至最大 800px 宽,JPEG 导出质量从 `0.9` 降到 `0.6`。单张图片体积可从 500KB 降至 30KB~80KB。 +2. **增加存储错误可见性**:将静默 `catch` 改为输出 `console.error`。 + +**D. 后续如何避免问题** +- 任何将 Base64 图片持久化到 `localStorage` 的场景,都必须**预估数据体积**并对图片进行适当的分辨率/质量压缩。 +- 存储层的异常捕获**绝不应静默吞掉**,至少要输出日志,必要时还应弹出用户提示。 + +--- + +## 记录 9:contentEditable 中实现标签锁定与输入方格的双向绑定 + +**A. 具体问题** +需要在富文本编辑器中插入"标签锁定、内容可调"的智能占位控件,使"姓名:"等固定文本不会被用户误删,同时方格内的输入能与右侧表单双向联动。 + +**B. 产生问题原因** +原生 `contentEditable` 区域内所有文本节点对用户都是可编辑的,无法直接保护某一段固定标签不被单独删除或篡改。 + +**C. 解决问题方案** +采用三层嵌套 HTML 结构: +1. **外层** `<span class="smart-field-wrapper" contenteditable="false">` +2. **标签层** `<span class="field-label">` +3. **输入层** `<span class="field-value" contenteditable="true" data-bind="patientName">` + +双向绑定逻辑:富文本 → 表单通过 `handleEditorInput` 中 `e.target.hasAttribute('data-bind')` 判断;表单 → 富文本通过 `useEffect` 监听 `reportData` 变化,仅当 `el.innerText !== newValue` 时才重写 DOM。 + +**D. 后续如何避免问题** +- 对于需要在富文本中保护的固定文本,优先采用 `contenteditable="false"` 的包装器。 +- 在 `State -> DOM` 的同步中务必加入差异判断,避免不必要的 DOM 重写导致输入焦点异常。 + +--- + +## 记录 10:智能字段插入间距修复与 Backspace 防误删 + +**A. 具体问题** +1. 插入智能字段后,字段后方会出现一个可见的空格(由 ` ` 和多行模板字符串中的换行/缩进空白引起)。 +2. 光标位于 `<p>` 行首且后紧跟 `.smart-field-wrapper` 时按 Backspace,WebKit 内核会直接删除整段 `<p>` 而不是仅删除字段节点。 + +**B. 产生问题原因** +1. `insertSmartField` 的 HTML 字符串使用反引号多行模板,缩进和换行被浏览器解析为额外的文本节点;末尾显式拼接了 ` `。 +2. `contenteditable="false"` 的 inline 元素处于行边界时,WebKit 的默认编辑行为会将整个包含该元素的块级父节点一并删除。 + +**C. 解决问题方案** +1. **压缩 HTML 字符串**:将 `insertSmartField` 和 `defaultContent.ts` 的 `smartField` 输出改为单行 HTML,移除所有无意义的换行和缩进,并去掉尾部的 ` `。 +2. **防止内部折行**:给 `.smart-field-wrapper` 增加 `white-space: nowrap;`。 +3. **拦截 Backspace/Delete**:在编辑器上增加 `keydown` 事件监听(capture 阶段)。当光标位于文本节点起始位置且前一个兄弟节点是 `.smart-field-wrapper` 时按 Backspace,主动 `preventDefault()` 并手动移除该字段节点。 + +**D. 后续如何避免问题** +- 在 `contentEditable` 中使用 `document.execCommand('insertHTML', ...)` 插入 HTML 时,**传入的字符串必须是无多余空白的紧凑单行**。 +- 对于 `contenteditable="false"` 的内联控件,若放置在块级边界,务必增加键盘事件拦截。 + +--- + +## 记录 11:撤销栈修复、字段删除交互优化与签名字段闭环 + +**A. 具体问题** +1. 删除智能字段后,浏览器撤销栈(Undo)失效,点击"撤销"按钮无法恢复。 +2. 插入字段后,字段框有时会跳到下一行。 +3. Backspace 键无法删除字段;Delete 键会误删字段前面的大段文本。 + +**B. 产生问题原因** +1. 删除字段时使用了 `target.remove()` 直接操作 DOM,绕过了浏览器的原生撤销栈。 +2. 插入的 `smart-field-wrapper` 是 `inline-block` 元素,但其后缺少行内锚点文本节点,浏览器容易将其挤到新行。 +3. `keydown` 拦截逻辑中 `target.remove()` 同样会误删父级块节点。 + +**C. 解决问题方案** +1. **撤销栈修复**:将点击红 × 删除和键盘 Backspace/Delete 删除全部改为 `Range.selectNode(target)` + `document.execCommand('delete')`。 +2. **防换行**:在 `insertSmartField` 和 `defaultContent.ts` 的 `smartField()` 生成的 HTML 末尾增加 `​`(零宽空格),作为稳定的行内锚点。 +3. **精准键盘删除**:配合 `Range.selectNode` + `execCommand('delete')`,不再直接 `remove()` DOM 节点。 + +**D. 后续如何避免问题** +- 在 `contentEditable` 中删除元素时,**优先使用 `Range.selectNode` + `execCommand('delete')`** 而非直接 `remove()`,以确保撤销/重做等原生编辑行为正常工作。 +- 插入 `inline-block` 或 `inline-flex` 控件时,可在其后追加 `​` 零宽空格,为浏览器提供稳定的行内文本锚点。 + +--- + +## 记录 12:TemplateManage 自定义 Undo/Redo 与插入字段光标定位修复 + +**A. 具体问题** +1. 删除智能字段后,点击工具栏的"撤销"按钮无法恢复字段,"重做"也失效。 +2. 点击右侧字段库按钮插入字段时,字段经常跳到下一行或文档末尾。 + +**B. 产生问题原因** +1. 即使将删除逻辑改为 `execCommand('delete')`,浏览器原生的 undo stack 在 `contentEditable` 中结合 React 状态更新时仍然非常脆弱,容易被清空。 +2. 点击侧边栏按钮会导致编辑器 `blur`,浏览器内部的光标位置(Selection/Range)丢失;再次 `focus()` 后光标被重置,导致 `insertHTML` 插入位置错误。 + +**C. 解决问题方案** +1. **自定义 Undo/Redo 栈**:引入 `undoStack` 和 `redoStack` 两个 `useRef<string[]>([])`。实现 `pushHistory()`,在执行任何结构性变更前将当前 `editorRef.current.innerHTML` 推入 undo 栈。 +2. **阻止焦点流失**:在所有工具栏按钮和字段库插入按钮上增加 `onMouseDown={(e) => e.preventDefault()}`,阻止 mousedown 默认行为导致编辑器失去焦点。 +3. **光标位置记忆与恢复**:利用 `savedRangeRef`,实现 `saveSelection()` 和 `restoreSelection()`。在编辑器 `<div>` 上绑定 `onBlur={saveSelection}`、`onMouseUp={saveSelection}`、`onKeyUp={saveSelection}`。 + +**D. 后续如何避免问题** +- 对于 `contentEditable` 编辑器中的结构性变更,如果原生 undo 不可靠,应尽早实现自定义历史栈(基于 HTML 字符串快照),完全接管撤销/重做逻辑。 +- 侧边栏/工具栏按钮与编辑器共存时,**必须**通过 `onMouseDown={e => e.preventDefault()}` 阻止焦点流失。 + +--- + +## 记录 13:时间/日期字段格式配置与撰写时间动态字段 + +**A. 具体问题** +1. 时间/日期字段缺少配置:date 可选显示格式;time 可选 24h / 12h 显示格式;两者均可选「当前时间」或「固定时间」作为默认值策略。 +2. 默认模板底部写死的「年 月 日」改为动态「撰写时间」智能字段,自动取当前日期。 + +**B. 产生问题原因** +1. `FormField` 数据结构缺少格式和默认值配置字段。 +2. `ReportEditor` 中 time 字段的表单渲染仅支持 `startTime/endTime` 且固定为 24 小时制;smart field 同步时直接显示原始值,不做任何格式转换。 + +**C. 解决问题方案** +1. **扩展数据结构**:`FormField` 增加 `timeFormat?: string` 和 `timeDefault?: 'current' | 'specific'`。 +2. **ReportEditor 表单渲染重构**:`startTime/endTime` 根据 `timeFormat` 选择 hour select 的选项范围;12h 时额外增加 AM/PM select。 +3. **smart field 同步格式化**:同步 useEffect 中,根据字段定义调用 `formatDateDisplay`/`formatTimeDisplay`。 +4. **编辑器反向编辑解析**:`handleEditorInput` 中,通过正则解析格式化文本,转回原始值后存入 `reportData`。 + +**D. 后续如何避免问题** +- 显示格式与存储格式分离时,**必须同时实现「正向格式化」(存储→显示)和「反向解析」(显示→存储)**,否则用户在编辑器中直接编辑格式化后的值会导致数据格式混乱。 +- 自动填充当前时间必须增加「仅当值为空时触发」的保护,防止编辑已有报告时覆盖用户数据。 + +--- + +## 记录 14:时间字段联动修复——默认格式、固定时间自动填充、12/24h 动态切换 + +**A. 具体问题** +1. 新建日期字段时默认格式为 `YYYY-MM-DD`,缺少中文格式;新建时间字段时默认格式为不可解析的 `'24h'`。 +2. 时间字段设为「固定时间」后,进入报告编辑器新建报告时,该固定值未自动填充到表单中。 +3. `startTime` 格式改为 `hh:mm A`(12小时制),报告编辑器中的表单仍显示为 24 小时制下拉框。 + +**B. 产生问题原因** +1. **默认格式错误**:`TemplateManage.tsx` 中 `newFieldForm.type` 的 `onChange` 将时间字段默认值硬编码为 `'24h'`,而实际通用格式化函数使用的是 `HH`、`hh`、`mm`、`A` 等 token。 +2. **固定时间未注入**:`ReportEditor.tsx` 初始 `reportData` 中 `surgeryDate` 被强制赋值为 `new Date().toISOString().split('T')[0]`,导致后续「仅当值为空时才填充固定时间」的判断被跳过。 +3. **12h 判断写死**:`const is12h = field.timeFormat === '12h';` 仅匹配精确的 `'12h'` 字符串。 + +**C. 解决问题方案** +1. 默认格式改为:`t === 'date' ? 'YYYY年MM月DD日' : 'HH:mm'`。 +2. `surgeryDate` 初始值从 `new Date()` 改为空字符串 `''`;切换模板时显式遍历 `formFields` 注入固定值/当前值。 +3. 12h 判断改为包含性判断:`field.timeFormat.includes('hh') || field.timeFormat.includes('A')`。 + +**D. 后续如何避免问题** +- 时间/日期格式的默认值必须与通用格式化函数的 token 体系保持一致,不能使用简写别名(如 `'24h'`、`'12h'`)作为存储值。 +- 当字段配置了「固定默认值」或「自动填充当前值」时,必须在所有「创建新数据」的入口中显式遍历字段配置并注入。 +- 对于「格式→UI 形态」的联动判断,应使用**包含性判断**(`includes`)而非**精确匹配**。 + +--- + +## 记录 15:打印分页边距失效 + +**A. 具体问题** +`report-editor` / `report-view` 打印多页报告时,第二页及后续页面的上下边距几乎为 0,内容紧贴纸张边缘。 + +**B. 产生问题原因** +`@page { margin: 0 }` 将物理纸张边距设为 0,`body { padding: 10mm }` 只在整个 HTML 文档的顶部和底部各生效一次。当内容跨页时,浏览器在分页切断处不会保留 `body` 的 padding。 + +**C. 解决问题方案** +`print.ts` 中: +- `@page { margin: 15mm 10mm; }` 让打印引擎为每一页物理纸张独立分配边距 +- `body { padding: 0; }` 清除 body padding +- `.content { width: 100%; }` 让内容自然撑满可用区域 + +**D. 后续如何避免问题** +- 打印样式的边距控制**必须使用 `@page { margin: ... }` 而非 `body { padding: ... }`**,前者会让打印引擎为每一页物理纸张独立分配边距,后者只在文档首尾生效一次。 + +--- + +## 记录 16:表格内 execCommand 插入破坏结构 + +**A. 具体问题** +在 `template-manage` 编辑器表格中点击"插入图片占位符"后,HTML 结构被破坏——外层 `<span class="image-placeholder">` 丢失,仅剩内部子元素散落为 `<td>` 的直接子元素。 + +**B. 产生问题原因** +`document.execCommand('insertHTML')` 在 `<td>` 内处理复杂的 `inline-flex` 嵌套 `<span>` 时,WebKit/Blink 会将其自动"拍平"或重新排列。外层 `contenteditable="false"` 的 inline 容器被浏览器移除。 + +**C. 解决问题方案** +在 `insertImage` 中通过 `window.getSelection().anchorNode` 向上遍历检测是否在 `<td>` / `<th>` 内: +- 若在表格内:不弹出 prompt,使用 `<div>` 块级容器 + `width:100%;height:100%;` +- 若不在表格内:保持现有 `<span>` 行内容器 + +**D. 后续如何避免问题** +- `document.execCommand('insertHTML')` 对块级元素边界(尤其是 `<td>` 内)的自动修正行为不可控;在表格等复杂容器内插入 HTML 时,应优先使用块级标签(如 `<div>`)作为外层容器。 + +--- + +## 记录 17:图片占位符体系重构与双端统一 + +**A. 具体问题** +1. `template-manage` 的"插入字段"中仍存在"图片"分类(手术者签名、医院Logo)。 +2. 插入图片占位符时无法自定义默认宽高,且使用 `<div>` 导致强制换行。 +3. 占位符框太小时"插入/点击放置图片"文字显示不全。 + +**B. 产生问题原因** +1. `DEFAULT_FORM_FIELDS` 仍包含 `surgeonSignature` 和 `hospitalLogo`。 +2. 两端编辑器的 `insertImage()` 使用块级 `<div>` 插入,未提供尺寸 prompt。 +3. 占位符提示文本固定为长文本,未根据容器宽度做缩写适配。 + +**C. 解决问题方案** +1. 从 `DEFAULT_FORM_FIELDS` 和 `types.ts` 中移除 `surgeonSignature` 和 `hospitalLogo`;在 `TemplateManage.tsx` 中彻底移除"图片"分类。 +2. 改造 `insertImage()`:插入前通过 `prompt` 获取最大宽度/高度(px),生成带 `max-width/max-height` 的 `<span>` 行内占位符。 +3. 根据 prompt 输入的宽度决定提示文字:宽度 < 80px 时显示"插入图片",否则显示"插入/点击放置图片"。 + +**D. 后续如何避免问题** +- 当从字段体系中彻底移除某一分类时,需要同时清理:`DEFAULT_FORM_FIELDS`、UI 渲染数组、新增表单 options、以及可能残留的分类判断逻辑。 +- 在 `contentEditable` 中实现"同行插入"必须使用行内元素(`<span>`)并显式设置 `display:inline-flex` + `vertical-align:middle`。 + +--- + +## 记录 18:默认模板中 image-placeholder 缺少 data-mode 导致来源隔离失效 + +**A. 具体问题** +默认模板 `defaultContent.ts` 中的 8 个 `.image-placeholder` 使用的是旧版 HTML 结构,缺少 `data-mode="frame|manual"` 属性。新建报告加载默认模板后,签名和 Logo 区域可被关键帧拖拽误填充。 + +**B. 产生问题原因** +此前对「插入图片占位符」进行弹窗改造时,仅在运行时插入逻辑中新增了 `data-mode` 属性,但未同步回刷默认模板 `defaultContent.ts`。 + +**C. 解决问题方案** +在 `defaultContent.ts` 中对 8 个占位符做最小化修补: +1. 医院 Logo(65×65)和手术者签名(200×40)添加 `data-mode="manual"`。 +2. 表格内 6 个术中影像占位符(100%×150)添加 `data-mode="frame"`。 +3. 所有占位符的 `width/height/margin/display` 等布局属性绝对保持不变。 + +**D. 后续如何避免问题** +- 当为 `image-placeholder` 引入新的核心属性(如 `data-mode`、`data-allow-source`)时,**必须同步检索 `defaultContent.ts` 和任何预置模板文件**,确保静态模板中的占位符结构与运行时插入逻辑保持一致。 + +--- + +## 记录 19:5 项交互修复(虚线框恢复、prompt 文案、删除按钮、多选输入、label 提示) + +**A. 具体问题** +1. 删除 `image-placeholder` 中的图片后,虚线框消失。 +2. `ReportEditor.tsx` 的 `insertImage` prompt 文案仍显示旧版 "用英文逗号分隔",未同步修改。 +3. 新生成的 `image-placeholder` 右上角红色 `×` 显示不完全——`overflow:hidden` 未移除。 +4. 多选框无法输入 `,`、`;`、`,`、`、` 等分隔符——`onChange` 实时调用 `parseMultiInput` + `filter(Boolean)`,末尾的分隔符被瞬间吃掉。 +5. 多选框 label 缺少 "(可多选)" 提示。 + +**B. 产生问题原因** +1. `fillPlaceholderSrc` 设置了 `border='none'`,但删除图片的代码没有恢复。 +2. `overflow:hidden` 仅在新版 `TemplateManage.tsx` 中被移除,`ReportEditor.tsx` 中仍保留。 +3. 多选框使用受控 `value={displayText}` + `onChange={handleMultiChange}`,每次输入都会触发 `split(/[,,;;、]/)` 和 `filter(Boolean)`。当用户输入一个逗号时,split 产生空字符串,filter 将其过滤,输入框值立即回退。 + +**C. 解决问题方案** +1. 在删除图片分支中增加:`placeholder.style.border = '1px dashed #cbd5e1'; placeholder.style.background = '#f8fafc';` +2. 将 `ReportEditor.tsx` 的 `insertImage` 重写为与 `TemplateManage.tsx` 一致的新版逻辑(`*` 分隔 + while 循环校验)。 +3. 从 `ReportEditor.tsx` 的 `styleStr` 中删除 `overflow:hidden;`。 +4. **多选输入解耦**:引入本地状态 `multiInputText: Record<string, string>`,`onChange` 仅更新 `multiInputText`,不触发拆分;`onBlur` 和 `Enter` 时才调用 `handleMultiCommit` 执行拆分。 +5. label 追加 `(可多选)` 提示。 + +**D. 后续如何避免问题** +- 同类型函数在多个文件中存在时,务必逐个文件 grep 确认修改结果,不能假设一次替换就能覆盖所有实例。 +- 任何 "实时解析输入" 的逻辑都必须警惕 `filter(Boolean)` 对空字符串的过滤效应——如果允许用户输入分隔符,应使用独立状态缓存原始输入,仅在确认时(blur/enter)执行解析。 + +--- + +## 记录 20:图片占位符填充后高度自适应 + +**A. 具体问题** +图片占位符填充后仍保留固定高度(如 200px),导致图片下方出现大片空白。 + +**B. 产生问题原因** +此前仅将 `height` 改为 `auto`,未同步处理 `width`,也未利用 `max-width`/`max-height` 作为硬限制来实现等比例缩放。 + +**C. 解决问题方案** +1. **插入时**:为 inline-block 占位符追加 `max-width:${w}px;max-height:${h}px;`。 +2. **填充时**:统一执行以下步骤: + - 读取 `placeholder.style.maxWidth || placeholder.style.width` 和 `placeholder.style.maxHeight || placeholder.style.height` 作为硬限制值 `mw` / `mh` + - 将 `<img>` 的 style 设为 `max-width:${mw};max-height:${mh};display:block;object-fit:contain;object-position:left top;` + - 将占位符外壳设为 `width:auto;height:auto;line-height:normal;max-width:${mw};max-height:${mh};` + +**D. 后续如何避免问题** +- `image-placeholder` 的尺寸逻辑涉及「创建时预设」和「填充后自适应」两个阶段,修改时必须同时考虑:创建时是否写入了 `max-width`/`max-height`;填充时是否同步清除了固定宽高并保留了硬限制。 + +--- + +## 记录 21:重新部署应用(Vite 生产构建 + Vite Preview) + +**A. 具体问题** +重新部署到生产环境时,当前运行环境中未安装 Docker,无法使用项目自带的 `docker-compose.yaml` 进行容器化部署。 + +**B. 产生问题原因** +1. 当前 Windows 环境缺少 Docker 和 docker-compose CLI。 +2. 项目本身是基于 Vite 的前端应用,可通过 `npm run build` 生成静态文件后,使用 `vite preview` 或任意静态文件服务器进行部署。 +3. 系统中已存在旧版本的 `vite preview` 进程在运行,需要先停止旧服务再启动新服务。 + +**C. 解决问题方案** +1. 使用 PowerShell 查询并强制停止所有属于当前项目目录的旧 `vite preview` 进程。 +2. 执行 `npm run build` 重新构建生产包。 +3. 使用 `Start-Process` 以独立 Windows 进程启动 `npm run preview -- --host`(避免后台任务超时杀死服务)。 +4. 通过 `Invoke-WebRequest` 访问 `http://localhost:4173/` 验证服务返回 HTTP 200。 + +**D. 后续如何避免问题** +- 在无法使用 Docker 的环境中,可将 `npm run build && npm run preview -- --host` 作为标准部署脚本。 +- 重新部署前务必先清理旧的同类型进程,避免端口冲突或多版本服务同时运行导致访问混乱。 +- **切勿使用 Shell 后台任务(`run_in_background=true`)长时间运行 `npm run preview`**,因为任务超时机制(默认 60s)会强制终止 preview 进程,导致服务中断。