diff --git a/src/index.css b/src/index.css index bee5f3f..ea0e089 100644 --- a/src/index.css +++ b/src/index.css @@ -127,24 +127,35 @@ .smart-field-wrapper .field-value:empty::before { content: '\200b'; } + .smart-field-wrapper .field-value:focus { + background-color: #e2e8f0; + border-color: #94a3b8; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25); + } .smart-field-wrapper .delete-btn { - display: inline-flex; - align-items: center; - justify-content: center; - width: 14px; - height: 14px; - margin-right: 2px; + position: absolute; + top: -8px; + right: -8px; + width: 16px; + height: 16px; background: #ef4444; color: white; border-radius: 50%; font-size: 10px; - line-height: 1; + line-height: 16px; + text-align: center; cursor: pointer; user-select: none; + display: none; + z-index: 10; } .smart-field-wrapper .delete-btn:hover { background: #dc2626; } + .template-editor-mode .smart-field-wrapper:hover .delete-btn, + .template-editor-mode .smart-field-wrapper:focus-within .delete-btn { + display: block; + } } @media print { diff --git a/src/pages/ReportEditor.tsx b/src/pages/ReportEditor.tsx index e3b7c1d..5db9d6c 100644 --- a/src/pages/ReportEditor.tsx +++ b/src/pages/ReportEditor.tsx @@ -1224,6 +1224,8 @@ export default function ReportEditor() { if (field.type === 'multi_select') { const isOpen = openDropdown === field.key; const opts = field.options || multiSelectOptions[field.key] || []; + const rawValue = (reportData as any)[field.key]; + const tags = Array.isArray(rawValue) ? rawValue : (rawValue ? [String(rawValue)] : []); return (
@@ -1231,7 +1233,7 @@ export default function ReportEditor() { className="w-full px-3 py-2 border border-border rounded-lg bg-white flex flex-wrap gap-1 items-center min-h-[42px] cursor-text" onClick={() => setOpenDropdown(field.key)} > - {((reportData as any)[field.key] || []).map((tag: string) => ( + {tags.map((tag: string) => ( {tag} { e.stopPropagation(); removeTag(field.key, tag); }}>× diff --git a/src/pages/TemplateManage.tsx b/src/pages/TemplateManage.tsx index e616bbc..29eee79 100644 --- a/src/pages/TemplateManage.tsx +++ b/src/pages/TemplateManage.tsx @@ -249,7 +249,7 @@ export default function TemplateManage() { alert(`字段 "${field.label}" 已存在,请勿重复插入。`); return; } - const html = `× `; + const html = ` ×`; document.execCommand('insertHTML', false, html); editorRef.current?.focus(); }; @@ -577,7 +577,7 @@ export default function TemplateManage() {
diff --git a/src/utils/defaultContent.ts b/src/utils/defaultContent.ts index ca740f0..fa55202 100644 --- a/src/utils/defaultContent.ts +++ b/src/utils/defaultContent.ts @@ -1,4 +1,4 @@ -const smartField = (key: string) => `× `; +const smartField = (key: string) => ` ×`; export const defaultReportContent = ` diff --git a/工程分析/实现方案-2026-04-17-11-14-28.md b/工程分析/实现方案-2026-04-17-11-14-28.md new file mode 100644 index 0000000..2295199 --- /dev/null +++ b/工程分析/实现方案-2026-04-17-11-14-28.md @@ -0,0 +1,111 @@ +# 实现方案 — 字段聚焦高亮、删除按钮显隐控制与 .map Bug 修复(2026-04-17-11-14-28) + +## 一、修改文件清单 + +1. `src/pages/TemplateManage.tsx` — 调整 `insertSmartField` HTML 结构;给编辑器增加 `template-editor-mode` class +2. `src/utils/defaultContent.ts` — 同步调整 `smartField()` HTML 结构 +3. `src/index.css` — 聚焦高亮样式 + 删除按钮绝对定位 + 显隐控制 +4. `src/pages/ReportEditor.tsx` — 修复 `multi_select` 的 `.map` 类型安全 + +## 二、详细改动 + +### 2.1 `src/pages/TemplateManage.tsx` + +#### A. `insertSmartField` HTML 结构调整 +将 `delete-btn` 放到 `.field-value` **之后**,并给 `.smart-field-wrapper` 增加 `position:relative`: +```html + + + × + +``` + +#### B. 编辑器容器增加专属 class +在渲染编辑器的 `
` 上增加 `template-editor-mode`: +```tsx +
+
+``` + +### 2.2 `src/utils/defaultContent.ts` + +同步修改 `smartField(key)` 的 HTML 结构,与 `insertSmartField` 完全一致。 + +### 2.3 `src/index.css` + +#### A. field-value 聚焦高亮 +```css +.smart-field-wrapper .field-value:focus { + background-color: #e2e8f0; + border-color: #94a3b8; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25); + transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; +} +``` + +#### B. 删除按钮样式(默认隐藏) +```css +.smart-field-wrapper .delete-btn { + position: absolute; + top: -8px; + right: -8px; + width: 16px; + height: 16px; + background: #ef4444; + color: white; + border-radius: 50%; + font-size: 10px; + line-height: 16px; + text-align: center; + cursor: pointer; + user-select: none; + display: none; + z-index: 10; +} +.smart-field-wrapper .delete-btn:hover { + background: #dc2626; +} +``` + +#### C. 显隐控制(仅在 TemplateManage 显示) +```css +.template-editor-mode .smart-field-wrapper:hover .delete-btn, +.template-editor-mode .smart-field-wrapper:focus-within .delete-btn { + display: block; +} +``` + +#### D. 打印隐藏 +保留已有的 `.print-content .smart-field-wrapper .delete-btn { display: none !important; }`。 + +### 2.4 `src/pages/ReportEditor.tsx` + +修复 `multi_select` 渲染处的 `.map` 调用: + +```tsx +if (field.type === 'multi_select') { + const isOpen = openDropdown === field.key; + const opts = field.options || multiSelectOptions[field.key] || []; + const rawValue = (reportData as any)[field.key]; + const tags = Array.isArray(rawValue) ? rawValue : (rawValue ? [String(rawValue)] : []); + return ( + ... + {tags.map((tag: string) => ( + + ... + + ))} + ... + ); +} +``` + +## 三、风险与回滚 + +- **风险**:修改 `smartField` HTML 结构和 CSS 后,旧报告中已存在的智能字段没有删除按钮。这是预期行为(旧数据不回溯)。 +- **风险**:`delete-btn` 的 `display: none` 默认隐藏 + `.template-editor-mode` 控制显示,ReportEditor 中由于容器没有 `template-editor-mode` class,删除按钮不会显示。 +- **回滚**:如出现问题,可回退上述 4 个文件的修改。 diff --git a/工程分析/测试方案-2026-04-17-11-14-28.md b/工程分析/测试方案-2026-04-17-11-14-28.md new file mode 100644 index 0000000..24c546b --- /dev/null +++ b/工程分析/测试方案-2026-04-17-11-14-28.md @@ -0,0 +1,56 @@ +# 测试方案 — 字段聚焦高亮、删除按钮显隐控制与 .map Bug 修复(2026-04-17-11-14-28) + +## 一、编译检查 + +- 执行 `npm run lint`(`tsc --noEmit`),确保全量 TypeScript 无编译错误。 + +## 二、功能验证步骤 + +### 测试 1:TemplateManage 字段聚焦高亮 +1. 进入【模板管理】,选择默认模板。 +2. 点击模板中的任意智能字段方框(如"姓名"),观察背景色是否明显变深(如 `#e2e8f0`),边框是否变深,是否有蓝色外发光。 +3. 点击另一个字段方框,确认高亮状态随焦点切换。 + +### 测试 2:TemplateManage 删除按钮位置与显隐 +1. 鼠标悬浮在智能字段方框上(不点击),确认字段**右上角**出现红色圆形 ×。 +2. 鼠标移开字段区域,确认红色 × 消失。 +3. 点击字段方框使其获得焦点,确认红色 × 保持显示。 +4. 点击红色 ×,确认该字段被删除,模板自动保存。 + +### 测试 3:ReportEditor 中不显示删除按钮 +1. 进入【新建报告】或编辑已有报告。 +2. 观察编辑器中的所有智能字段方框,确认**没有任何红色 × 显示**(无论悬浮还是聚焦)。 +3. 在 ReportEditor 的 field-value 中输入文字,确认编辑器正常工作。 + +### 测试 4:ReportEditor multi_select 脏数据兼容性 +1. DevTools Console 执行以下代码,手动构造一份脏数据报告(`surgeon` 为字符串而非数组): + ```js + const dirtyReport = { + id: 'test_dirty_001', + title: '测试脏数据报告', + patientName: '张三', + hospitalId: 'H0001', + author: 'admin', + authorName: '管理员', + createdAt: new Date().toISOString(), + status: 'draft', + content: '

测试内容

', + surgeon: '张医生', // 脏数据:应该是数组 + assistant: [], + anesthesiologist: ['周医生'] + }; + const reports = JSON.parse(localStorage.getItem('reports') || '[]'); + reports.push(dirtyReport); + localStorage.setItem('reports', JSON.stringify(reports)); + ``` +2. 刷新页面,进入【报告管理】,点击编辑该测试报告。 +3. 确认页面**没有白屏崩溃**,右侧【基本信息】中的"手术者"字段正确显示为单标签 `张医生`,且可以正常添加/删除标签。 +4. 删除该测试报告,避免污染数据。 + +## 三、预期结果 + +- `npm run lint` 0 错误。 +- TemplateManage 中字段聚焦时有明显高亮效果。 +- TemplateManage 中删除按钮仅悬浮/聚焦时显示在右上角,点击可删除字段。 +- ReportEditor 中完全看不到删除按钮。 +- ReportEditor 能兼容非数组类型的 multi_select 脏数据,不崩溃。 diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md index 253fa31..3e34f3f 100644 --- a/工程分析/经验记录.md +++ b/工程分析/经验记录.md @@ -377,6 +377,38 @@ if ((settings.autoInsertDelay || 0) > 0) { --- +## 记录 17:字段聚焦高亮、删除按钮显隐隔离与 multi_select 脏数据崩溃修复 + +**A. 具体问题** +1. `TemplateManage` 中编辑智能字段时缺少视觉焦点反馈,用户体验不够直观。 +2. 红色 × 删除按钮始终显示在字段内部左侧,且在任何包含 `smart-field-wrapper` 的页面(包括 `ReportEditor`)都会显示。 +3. `ReportEditor` 加载某些历史报告时崩溃,报错 `(y[x.key] || []).map is not a function`。 + +**B. 产生问题原因** +1. 之前没有为 `.field-value` 定义 `:focus` 状态的 CSS 样式。 +2. `delete-btn` 使用 `display: inline-flex` 默认常驻显示,且没有针对页面做显隐隔离。 +3. `multi_select` 字段(如 `surgeon`、`assistant`)的渲染直接对值调用 `.map()`,但旧数据或异常存储可能将其保存为字符串(如 `"张医生"` 而非 `["张医生"]`),导致 `.map` 在字符串上调用时抛出 `TypeError`。 + +**C. 解决问题方案** +1. **聚焦高亮**:在 `index.css` 中为 `.smart-field-wrapper .field-value:focus` 增加背景色加深(`#e2e8f0`)、边框变深(`#94a3b8`)和蓝色外发光(`box-shadow: 0 0 0 2px rgba(59,130,246,0.25)`)的样式,配合 `transition` 实现平滑反馈。 +2. **删除按钮定位与显隐隔离**: + - 将 `delete-btn` 从字段内部移到 `.field-value` 之后,并给 `.smart-field-wrapper` 增加 `position:relative`,使 `delete-btn` 可绝对定位到右上角(`top: -8px; right: -8px`)。 + - 默认 `display: none`;在 `TemplateManage` 的编辑器容器上增加 `template-editor-mode` class,通过 `.template-editor-mode .smart-field-wrapper:hover .delete-btn` 和 `:focus-within .delete-btn` 控制仅在 TemplateManage 中悬浮/聚焦时显示。 + - `ReportEditor` 的编辑器容器没有 `template-editor-mode`,因此删除按钮不会显示。 +3. **类型安全修复**:在 `ReportEditor.tsx` 的 `multi_select` 渲染分支中,增加 `Array.isArray` 检查: + ```ts + const rawValue = (reportData as any)[field.key]; + const tags = Array.isArray(rawValue) ? rawValue : (rawValue ? [String(rawValue)] : []); + ``` + 确保无论旧数据是数组、字符串还是空值,都能安全渲染为标签列表。 + +**D. 后续如何避免问题** +- 任何需要在不同页面显隐不同的 UI 元素,应通过容器级 class 做样式隔离,而不是依赖全局显示/隐藏。 +- `contentEditable` 控件的焦点状态必须有明确的视觉反馈(背景/边框/阴影变化),否则用户难以感知当前编辑位置。 +- 对从持久化存储读取的数组类型数据,在 React 渲染前务必做 `Array.isArray` 校验,防止历史脏数据导致整页崩溃。 + +--- + ## 记录 14:智能字段插入间距修复与 Backspace 防误删 **A. 具体问题** diff --git a/工程分析/需求分析-2026-04-17-11-14-28.md b/工程分析/需求分析-2026-04-17-11-14-28.md new file mode 100644 index 0000000..a9576e7 --- /dev/null +++ b/工程分析/需求分析-2026-04-17-11-14-28.md @@ -0,0 +1,50 @@ +# 需求分析 — 字段聚焦高亮、删除按钮显隐控制与 .map Bug 修复(2026-04-17-11-14-28) + +## 一、需求来源 + +用户反馈 TemplateManage 中字段删除按钮位置和显示时机需要优化,同时要求字段获得焦点时有视觉高亮反馈,并修复 ReportEditor 中多选字段渲染崩溃的 Bug。 + +## 二、具体需求拆解 + +### 需求 1:field-value 聚焦高亮 + +**期望**:当光标落入 `.field-value`(`contenteditable="true"` 的输入框)时,背景色加深、边框色变化,并带一个短暂的过渡动画,让用户明确感知当前正在编辑哪个字段。 + +### 需求 2:删除按钮定位与显隐控制 + +**期望**: +- 红色 × 删除按钮从字段内部左侧移到**右上角**(绝对定位)。 +- 仅在鼠标**悬浮在 `.field-value` 上**或 `.field-value` 获得焦点时显示删除按钮。 +- **ReportEditor 中完全不显示**该删除按钮(编辑器中不应有删除字段的入口)。 + +### 需求 3:修复 ReportEditor `.map is not a function` 崩溃 + +**问题**: +``` +Uncaught TypeError: (y[x.key] || []).map is not a function +``` +发生在 `ReportEditor.tsx` 渲染 `multi_select` 类型字段时: +```tsx +{((reportData as any)[field.key] || []).map((tag: string) => ... +``` + +**原因**:历史脏数据或异常情况下,`surgeon`/`assistant`/`anesthesiologist` 等字段的值不是数组,而是字符串或其他类型,导致 `.map()` 调用崩溃。 + +**期望**:在渲染前对值做类型安全检查,非数组时安全转换为空数组或包裹为单元素数组,避免页面白屏。 + +## 三、影响范围分析 + +| 文件 | 改动说明 | +|------|----------| +| `src/pages/TemplateManage.tsx` | `insertSmartField` 的 HTML 结构调整(wrapper 增加 `position:relative`,delete-btn 改为绝对定位在右上角)。编辑器 div 增加专属 class `template-editor-mode` 用于样式隔离。 | +| `src/utils/defaultContent.ts` | `smartField()` 辅助函数同步调整 HTML 结构,与 `insertSmartField` 保持一致。 | +| `src/index.css` | 增加 `.field-value:focus` 高亮样式;重写 `.delete-btn` 样式为绝对定位;增加 `.template-editor-mode .smart-field-wrapper:hover .delete-btn` / `:focus-within .delete-btn` 显隐控制;确保 `.print-content .delete-btn` 打印隐藏。 | +| `src/pages/ReportEditor.tsx` | 修复 `multi_select` 渲染处的 `.map` 调用,增加 `Array.isArray` 安全转换。 | + +## 四、验收标准 + +- [ ] TemplateManage 中点击 field-value,背景色明显加深并有过渡动画。 +- [ ] TemplateManage 中鼠标悬浮/聚焦 field-value 时,右上角出现红色 ×;移开后隐藏。 +- [ ] ReportEditor 中所有 smart-field-wrapper 均不显示红色 ×。 +- [ ] ReportEditor 加载存在脏数据(非数组类型)的报告时,multi_select 字段不再崩溃,页面正常渲染。 +- [ ] `npm run lint` 无编译错误。