feat: focus highlight, delete-btn visibility isolation, multi_select crash fix

- Move delete-btn to top-right of smart-field-wrapper via absolute positioning
- Add template-editor-mode class to TemplateManage editor for CSS isolation
- Show delete-btn only on hover/focus-within inside template-editor-mode
- Add .field-value:focus highlight with background darken and blue glow
- Sync defaultContent.ts smartField() HTML structure
- Fix ReportEditor multi_select .map crash with Array.isArray guard
This commit is contained in:
2026-04-17 11:21:32 +08:00
parent db5df13a05
commit 0ff1cbe5f0
8 changed files with 273 additions and 11 deletions

View File

@@ -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 {

View File

@@ -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 (
<div key={field.key} className="space-y-1 select-dropdown-root relative">
<label className="block text-xs font-bold text-text-main">{field.label}</label>
@@ -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) => (
<span key={tag} className="px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-700 flex items-center gap-1">
{tag}
<span className="cursor-pointer hover:text-amber-900" onClick={(e) => { e.stopPropagation(); removeTag(field.key, tag); }}>×</span>

View File

@@ -249,7 +249,7 @@ export default function TemplateManage() {
alert(`字段 "${field.label}" 已存在,请勿重复插入。`);
return;
}
const html = `<span class="smart-field-wrapper" contenteditable="false" style="white-space:nowrap;"><span class="delete-btn" contenteditable="false">×</span><span class="field-value" data-bind="${field.key}" contenteditable="true" style="min-width:32px;padding:0 4px;margin:0 2px;border:1px solid #cbd5e1;border-radius:2px;display:inline-block;background:#f8fafc;color:#0f172a;line-height:1.2;font-size:inherit;vertical-align:text-bottom;box-sizing:border-box;min-height:1.2em;"> </span></span>`;
const html = `<span class="smart-field-wrapper" contenteditable="false" style="white-space:nowrap;position:relative;"><span class="field-value" data-bind="${field.key}" contenteditable="true" style="min-width:32px;padding:0 4px;margin:0 2px;border:1px solid #cbd5e1;border-radius:2px;display:inline-block;background:#f8fafc;color:#0f172a;line-height:1.2;font-size:inherit;vertical-align:text-bottom;box-sizing:border-box;min-height:1.2em;outline:none;"> </span><span class="delete-btn" contenteditable="false">×</span></span>`;
document.execCommand('insertHTML', false, html);
editorRef.current?.focus();
};
@@ -577,7 +577,7 @@ export default function TemplateManage() {
<div
ref={editorRef}
contentEditable
className="editor-content print-content"
className="editor-content print-content template-editor-mode"
>
</div>
</div>

View File

@@ -1,4 +1,4 @@
const smartField = (key: string) => `<span class="smart-field-wrapper" contenteditable="false" style="white-space:nowrap;"><span class="delete-btn" contenteditable="false">×</span><span class="field-value" data-bind="${key}" contenteditable="true" style="min-width:32px;padding:0 4px;margin:0 2px;border:1px solid #cbd5e1;border-radius:2px;display:inline-block;background:#f8fafc;color:#0f172a;line-height:1.2;font-size:inherit;vertical-align:text-bottom;box-sizing:border-box;min-height:1.2em;"> </span></span>`;
const smartField = (key: string) => `<span class="smart-field-wrapper" contenteditable="false" style="white-space:nowrap;position:relative;"><span class="field-value" data-bind="${key}" contenteditable="true" style="min-width:32px;padding:0 4px;margin:0 2px;border:1px solid #cbd5e1;border-radius:2px;display:inline-block;background:#f8fafc;color:#0f172a;line-height:1.2;font-size:inherit;vertical-align:text-bottom;box-sizing:border-box;min-height:1.2em;outline:none;"> </span><span class="delete-btn" contenteditable="false">×</span></span>`;
export const defaultReportContent = `
<!-- 医院Logo -->

View File

@@ -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
<span class="smart-field-wrapper" contenteditable="false" style="white-space:nowrap;position:relative;">
<span class="field-value" data-bind="..." contenteditable="true" style="..."> </span>
<span class="delete-btn" contenteditable="false">×</span>
</span>
```
#### B. 编辑器容器增加专属 class
在渲染编辑器的 `<div ref={editorRef} ...>` 上增加 `template-editor-mode`
```tsx
<div
ref={editorRef}
contentEditable
className="editor-content print-content template-editor-mode"
>
</div>
```
### 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) => (
<span key={tag} ...>
...
</span>
))}
...
);
}
```
## 三、风险与回滚
- **风险**:修改 `smartField` HTML 结构和 CSS 后,旧报告中已存在的智能字段没有删除按钮。这是预期行为(旧数据不回溯)。
- **风险**`delete-btn``display: none` 默认隐藏 + `.template-editor-mode` 控制显示ReportEditor 中由于容器没有 `template-editor-mode` class删除按钮不会显示。
- **回滚**:如出现问题,可回退上述 4 个文件的修改。

View File

@@ -0,0 +1,56 @@
# 测试方案 — 字段聚焦高亮、删除按钮显隐控制与 .map Bug 修复2026-04-17-11-14-28
## 一、编译检查
- 执行 `npm run lint``tsc --noEmit`),确保全量 TypeScript 无编译错误。
## 二、功能验证步骤
### 测试 1TemplateManage 字段聚焦高亮
1. 进入【模板管理】,选择默认模板。
2. 点击模板中的任意智能字段方框(如"姓名"),观察背景色是否明显变深(如 `#e2e8f0`),边框是否变深,是否有蓝色外发光。
3. 点击另一个字段方框,确认高亮状态随焦点切换。
### 测试 2TemplateManage 删除按钮位置与显隐
1. 鼠标悬浮在智能字段方框上(不点击),确认字段**右上角**出现红色圆形 ×。
2. 鼠标移开字段区域,确认红色 × 消失。
3. 点击字段方框使其获得焦点,确认红色 × 保持显示。
4. 点击红色 ×,确认该字段被删除,模板自动保存。
### 测试 3ReportEditor 中不显示删除按钮
1. 进入【新建报告】或编辑已有报告。
2. 观察编辑器中的所有智能字段方框,确认**没有任何红色 × 显示**(无论悬浮还是聚焦)。
3. 在 ReportEditor 的 field-value 中输入文字,确认编辑器正常工作。
### 测试 4ReportEditor 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: '<p>测试内容</p>',
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 脏数据,不崩溃。

View File

@@ -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. 具体问题**

View File

@@ -0,0 +1,50 @@
# 需求分析 — 字段聚焦高亮、删除按钮显隐控制与 .map Bug 修复2026-04-17-11-14-28
## 一、需求来源
用户反馈 TemplateManage 中字段删除按钮位置和显示时机需要优化,同时要求字段获得焦点时有视觉高亮反馈,并修复 ReportEditor 中多选字段渲染崩溃的 Bug。
## 二、具体需求拆解
### 需求 1field-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` 无编译错误。