Files
Mdeical_Sur_Report/工程分析/实现方案-2026-04-16-22-23-02.md

250 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 实现方案 — 2026-04-16-22-23-02
## 根因分析
当前 `TemplateManage.tsx``ReportEditor.tsx` 均使用原生 `contentEditable` 实现富文本编辑,但模板中的占位符是纯 HTML`姓名:<span style="color: #ff0000;">*姓名*</span>`),存在以下问题:
1. **固定文本无保护**"姓名:" 等标签与普通文本无异,用户可随意删除或篡改。
2. **无双向绑定**:模板中的占位符与右侧表单之间没有数据通道,模板内容不会随表单变化,表单也不会随模板输入自动填充。
3. **打印样式混乱**:现有的红色占位文本在打印报告中显得不专业。
## 修改文件清单
| 文件 | 修改类型 | 说明 |
|------|---------|------|
| `src/pages/TemplateManage.tsx` | 修改 | 新增右侧"字段库"面板,支持点击插入智能占位控件 |
| `src/pages/ReportEditor.tsx` | 修改 | 新增 `data-bind` DOM 的双向监听与同步逻辑 |
| `src/utils/print.ts` | 修改 | 打印样式中增加 `.field-value` 的打印适配 |
| `src/index.css` | 修改 | 新增 `.smart-field-wrapper` 系列样式 |
| `src/types.ts` | 修改(可选) | 定义字段映射常量数组,供两端复用 |
---
## 具体代码变更
### 变更 1`src/types.ts` — 定义字段库常量
**新增内容(在文件末尾追加):**
```typescript
export interface BindableField {
key: string;
label: string;
}
export const BINDABLE_FIELDS: BindableField[] = [
{ key: 'patientName', label: '姓名' },
{ key: 'gender', label: '性别' },
{ key: 'age', label: '年龄' },
{ key: 'hospitalId', label: '住院号' },
{ key: 'bedNumber', label: '床号' },
{ key: 'surgeryDate', label: '手术日期' },
{ key: 'surgeryType', label: '手术类型' },
{ key: 'surgeon', label: '手术者' },
{ key: 'assistant', label: '助手' },
{ key: 'anesthesiaType', label: '麻醉方式' },
{ key: 'preoperativeDiagnosis', label: '术前诊断' },
{ key: 'intraoperativeDiagnosis', label: '术中诊断' },
{ key: 'surgicalProcedure', label: '手术经过' },
];
```
### 变更 2`src/index.css` — 智能占位控件样式
**在 `@layer components` 或文件末尾新增:**
```css
.smart-field-wrapper {
display: inline-flex;
align-items: center;
margin: 0 4px;
vertical-align: middle;
}
.smart-field-wrapper .field-label {
color: #64748b;
user-select: none;
}
.smart-field-wrapper .field-value {
min-width: 60px;
padding: 0 4px;
border: 1px solid #cbd5e1;
border-radius: 4px;
display: inline-block;
background: #fff;
color: #0f172a;
outline: none;
}
.smart-field-wrapper .field-value:empty::before {
content: '\200b'; /* zero-width space to keep min-height */
}
@media print {
.smart-field-wrapper .field-value {
border: none !important;
border-bottom: 1px solid #000 !important;
border-radius: 0 !important;
background: transparent !important;
}
}
```
### 变更 3`src/pages/TemplateManage.tsx` — 字段库面板与插入逻辑
**当前结构:** `TemplateManage.tsx` 右侧通常为操作按钮区(如保存、预览)。
**新增字段库面板(放在保存按钮下方或单独区域):**
```tsx
import { BINDABLE_FIELDS } from '../types';
// 在组件内新增辅助函数
const insertSmartField = (field: typeof BINDABLE_FIELDS[0]) => {
const html = `
<span class="smart-field-wrapper" contenteditable="false">
<span class="field-label">${field.label}</span>
<span class="field-value"
data-bind="${field.key}"
contenteditable="true"
style="min-width: 60px; padding: 0 4px; border: 1px solid #cbd5e1; border-radius: 4px; display: inline-block; background: #fff; color: #0f172a;">
</span>
</span>&nbsp;
`;
document.execCommand('insertHTML', false, html);
};
```
**UI 位置(在保存按钮下方新增卡片):**
```tsx
<div className="card-minimal mt-4">
<h3 className="text-sm font-semibold text-primary mb-2"></h3>
<div className="flex flex-wrap gap-2">
{BINDABLE_FIELDS.map((field) => (
<button
key={field.key}
type="button"
onClick={() => insertSmartField(field)}
className="px-2 py-1 text-xs bg-slate-100 hover:bg-slate-200 text-slate-700 rounded border border-slate-300 transition-colors"
title={`插入 ${field.label}`}
>
{field.label}
</button>
))}
</div>
<p className="text-[10px] text-slate-400 mt-2">Label Value </p>
</div>
```
> 注意:`TemplateManage.tsx` 的具体行号需以实际文件为准,但插入逻辑与 UI 结构如上。
### 变更 4`src/pages/ReportEditor.tsx` — 双向绑定逻辑
#### 4.1 富文本 → 表单(`handleEditorInput` 或 `onInput` 事件)
**当前代码**中 `ReportEditor.tsx` 的编辑器通常已有 `onInput` 处理(保存草稿)。
**在原有 `onInput` 处理器中追加:**
```tsx
const handleEditorInput = (e: React.FormEvent<HTMLDivElement>) => {
// 1. 原有逻辑:同步 contentRef 并保存草稿
if (editorRef.current) {
contentRef.current = editorRef.current.innerHTML;
}
saveDraftToStorage();
// 2. 新增:双向绑定 — 方格内容变更时更新表单 State
const target = e.target as HTMLElement;
if (target && target.hasAttribute('data-bind')) {
const fieldKey = target.getAttribute('data-bind')!;
const newValue = target.innerText;
setReportData((prev) => {
const next = { ...prev, [fieldKey]: newValue };
// 同步 stateRef
stateRef.current.reportData = next;
return next;
});
}
};
```
#### 4.2 表单 → 富文本(`useEffect` 监听 `reportData`
**在 `ReportEditor.tsx` 中新增一个 `useEffect`**
```tsx
useEffect(() => {
if (!editorRef.current) return;
const bindNodes = editorRef.current.querySelectorAll('[data-bind]');
bindNodes.forEach((node) => {
const el = node as HTMLElement;
const fieldKey = el.getAttribute('data-bind')!;
const rawValue = (reportData as any)[fieldKey];
// 处理数组类型(如 surgeon / assistant
let newValue = '';
if (Array.isArray(rawValue)) {
newValue = rawValue.join(', ');
} else if (rawValue !== undefined && rawValue !== null) {
newValue = String(rawValue);
}
// 仅在差异时更新 DOM防止光标跳动
if (el.innerText !== newValue) {
el.innerText = newValue;
}
});
}, [reportData]);
```
#### 4.3 光标/焦点保护(边界处理)
为避免 `useEffect``reportData` 变化时与用户的输入冲突,上述逻辑已通过 `if (el.innerText !== newValue)` 做短路保护。若用户当前正在该方格内输入(此时 `reportData` 已由 `handleEditorInput` 同步更新),`innerText` 通常等于 `newValue`,不会触发 DOM 重写,因此光标不会跳动。
### 变更 5`src/utils/print.ts` — 打印样式适配
**当前 `print.ts` 会将 HTML 内容包裹后打印。**
**在注入的 `<style>` 中追加:**
```css
@media print {
/* 现有打印样式保留 ... */
.smart-field-wrapper .field-value {
border: none !important;
border-bottom: 1px solid #000 !important;
border-radius: 0 !important;
background: transparent !important;
padding: 0 2px !important;
}
}
```
> 若 `print.ts` 本身会读取 `index.css` 中的打印样式,可确认是否重复。保险起见,两边同时维护或仅在 `index.css` 维护即可。此处优先在 `index.css` 中维护,`print.ts` 若已内联样式则同步追加。
---
## 风险点
| 风险 | 级别 | 应对措施 |
|------|------|---------|
| 光标跳动/输入中断 | 中 | `useEffect` 同步时严格判断 `innerText !== newValue`,仅在差异时重写 DOM |
| `contenteditable="false"` 外层导致整个控件无法删除 | 低 | 这是预期行为,用户可通过选中整个控件后按 Delete 删除;若需要允许删除,可在外层增加 `tabindex` 或删除按钮 |
| 数组字段surgeon同步时格式异常 | 低 | 在 `useEffect` 中加入 `Array.isArray(rawValue)` 分支,统一用 `join(', ')` |
| 老模板未自动升级,用户反馈"联动不生效" | 低 | 在 `TemplateManage` 页面增加提示文案:"请重新编辑模板并插入字段库控件以激活联动" |
## 回滚策略
本次改动仅涉及前端 UI 和 DOM 事件处理,不修改数据结构和存储接口。如出现异常,可直接执行以下任一方式:
1. `git revert` 撤销相关提交;
2. 手动注释掉 `ReportEditor.tsx` 中的 `useEffect` 双向绑定逻辑和 `handleEditorInput` 的新增分支,保留原有草稿保存逻辑即可恢复。
---
**⚠️ 请审核以上方案,确认无误后回复「确认」或提出修改意见,我将继续编写测试方案。**