fix: undo stack, field insertion wrap, backspace/delete precision; feat: signature size limit & isSigned control

- Replace direct DOM remove() with Range+execCommand('delete') in TemplateManage click and keydown handlers to restore undo stack
- Append ​ zero-width space to smart-field-wrapper HTML in insertSmartField and defaultContent.ts to prevent unwanted line breaks
- Refactor ReportEditor surgeonSignature rendering to depend on isSigned field
- Add isSigned to DEFAULT_FORM_FIELDS (single_select: 已签字/未签字)
- Change surgeonSignature to visibleInForm=true, isSystemLocked=false
- Constrain signature image with max-width:120px, max-height:40px, object-fit:contain in CSS and print.ts
- Add weak-blocking signature validation prompts in saveReport('completed')
- Update experience record (#19)
This commit is contained in:
2026-04-17 12:41:07 +08:00
parent 424407a17e
commit f7c7270053
10 changed files with 367 additions and 13 deletions

View File

@@ -157,11 +157,13 @@
display: block;
}
.report-signature-img {
height: 2.4em;
max-width: 120px;
max-height: 40px;
width: auto;
height: auto;
object-fit: contain;
vertical-align: middle;
display: inline-block;
margin: -0.3em 0;
}
}
@@ -203,10 +205,12 @@
display: none !important;
}
.report-signature-img {
height: 2.4em !important;
max-width: 120px !important;
max-height: 40px !important;
width: auto !important;
height: auto !important;
object-fit: contain !important;
vertical-align: middle !important;
display: inline-block !important;
margin: -0.3em 0 !important;
}
}

View File

@@ -36,6 +36,7 @@ export default function ReportEditor() {
assistant: [],
anesthesiologist: [],
anesthesiaType: '',
isSigned: '未签字',
reportNote: '',
status: 'draft'
});
@@ -855,6 +856,21 @@ export default function ReportEditor() {
return;
}
if (status === 'completed') {
const hasSignatureField = editorRef.current?.querySelector('[data-bind="surgeonSignature"]');
if (hasSignatureField) {
const isSigned = (reportData as any).isSigned === '已签字';
const hasSignatureImage = !!currentUser?.signature;
if (!isSigned) {
const proceed = window.confirm('提示:模板中包含【手术者签名】字段,但您在基本信息中未选择"已签字"。是否继续完成报告?');
if (!proceed) return;
} else if (!hasSignatureImage) {
const proceed = window.confirm('提示:您选择了"已签字",但您的账号尚未上传电子签名图片。报告中将不显示签名图片,是否继续完成?');
if (!proceed) return;
}
}
}
const content = editorRef.current?.innerHTML || '';
const now = new Date().toISOString();
const finalReport: Report = {
@@ -941,8 +957,9 @@ export default function ReportEditor() {
const fieldKey = el.getAttribute('data-bind')!;
if (fieldKey === 'surgeonSignature') {
const isSigned = (reportData as any).isSigned === '已签字';
const signatureData = currentUser?.signature;
if (signatureData) {
if (isSigned && signatureData) {
const imgHtml = `<img src="${signatureData}" class="report-signature-img" alt="签名" draggable="false" />`;
if (el.innerHTML !== imgHtml) {
el.innerHTML = imgHtml;
@@ -950,8 +967,9 @@ export default function ReportEditor() {
el.style.backgroundColor = 'transparent';
}
} else {
if (el.innerText !== '【请上传电子签】') {
el.innerText = '【请上传电子签】';
const placeholder = isSigned ? '【请上传电子签】' : '【未签字】';
if (el.innerText !== placeholder) {
el.innerText = placeholder;
el.style.border = '';
el.style.backgroundColor = '';
}

View File

@@ -125,7 +125,12 @@ export default function TemplateManage() {
if (smartField && targetEl.closest('.delete-btn')) {
e.stopPropagation();
e.preventDefault();
smartField.remove();
const sel = window.getSelection();
const range = document.createRange();
range.selectNode(smartField);
sel?.removeAllRanges();
sel?.addRange(range);
document.execCommand('delete');
saveTemplateContent();
return;
}
@@ -216,7 +221,12 @@ export default function TemplateManage() {
if (target) {
e.preventDefault();
target.remove();
const sel = window.getSelection();
const range = document.createRange();
range.selectNode(target);
sel?.removeAllRanges();
sel?.addRange(range);
document.execCommand('delete');
saveTemplateContent();
}
};
@@ -249,7 +259,7 @@ export default function TemplateManage() {
alert(`字段 "${field.label}" 已存在,请勿重复插入。`);
return;
}
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>`;
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>&#8203;`;
document.execCommand('insertHTML', false, html);
editorRef.current?.focus();
};

View File

@@ -130,5 +130,6 @@ export const DEFAULT_FORM_FIELDS: FormField[] = [
{ key: 'assistant', label: '助手', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: false, options: ['赵医生', '钱医生', '孙医生'] },
{ key: 'anesthesiologist', label: '麻醉师', category: '多选', type: 'multi_select', visibleInForm: true, isSystemLocked: false, options: ['周医生', '吴医生', '郑医生'] },
{ key: 'anesthesiaType', label: '麻醉方式', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: false, options: ['全麻', '局麻', '腰麻', '硬膜外麻醉', '静脉麻醉', '吸入麻醉'] },
{ key: 'surgeonSignature', label: '手术者签名', category: '图片', type: 'signature', visibleInForm: false, isSystemLocked: true },
{ key: 'isSigned', label: '手术者签名确认', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: false, options: ['已签字', '未签字'] },
{ key: 'surgeonSignature', label: '手术者签名', category: '图片', type: 'signature', visibleInForm: true, isSystemLocked: false },
];

View File

@@ -1,4 +1,4 @@
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>`;
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>&#8203;`;
export const defaultReportContent = `
<!-- 医院Logo -->

View File

@@ -37,7 +37,7 @@ export const printDocument = (htmlContent: string) => {
.smart-field-wrapper { display: inline-flex; align-items: center; margin: 0 2px; vertical-align: text-bottom; }
.smart-field-wrapper .field-label { color: #64748b; user-select: none; }
.smart-field-wrapper .field-value { 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; }
.report-signature-img { height: 2.4em; width: auto; vertical-align: middle; display: inline-block; margin: -0.3em 0; }
.report-signature-img { max-width: 120px; max-height: 40px; width: auto; height: auto; object-fit: contain; vertical-align: middle; display: inline-block; }
@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; }
}

View File

@@ -0,0 +1,157 @@
# 实现方案 — 撤销栈修复、字段删除交互优化与签名字段闭环2026-04-17-12-34-56
## 一、修改文件清单
1. `src/pages/TemplateManage.tsx` — 删除逻辑改用 `execCommand('delete')`;插入 HTML 增加零宽空格防换行
2. `src/types.ts` — 修改 `surgeonSignature` 显隐属性;新增 `isSigned` 字段
3. `src/pages/ReportEditor.tsx` — 初始值增加 `isSigned`;签名同步逻辑重构;完成报告签名校验
4. `src/index.css` — 签名图片尺寸约束
5. `src/utils/print.ts` — 打印样式同步签名尺寸约束
## 二、详细改动
### 2.1 `src/pages/TemplateManage.tsx`
#### A. 点击红 X 删除改用 `execCommand('delete')`
```ts
if (smartField && targetEl.closest('.delete-btn')) {
e.stopPropagation();
e.preventDefault();
const sel = window.getSelection();
const range = document.createRange();
range.selectNode(smartField);
sel?.removeAllRanges();
sel?.addRange(range);
document.execCommand('delete');
saveTemplateContent();
return;
}
```
#### B. 键盘 Backspace/Delete 改用 `execCommand('delete')`
`handleKeyDown` 中,当定位到 `smart-field-wrapper` 目标后:
```ts
if (target) {
e.preventDefault();
const sel = window.getSelection();
const range = document.createRange();
range.selectNode(target);
sel?.removeAllRanges();
sel?.addRange(range);
document.execCommand('delete');
saveTemplateContent();
}
```
#### C. 插入 HTML 防换行
`insertSmartField` 的 HTML 字符串末尾增加 `&#8203;`(零宽空格),作为行内锚点,防止浏览器将字段挤到新行:
```html
<span class="smart-field-wrapper" ...>...</span>&#8203;
```
### 2.2 `src/types.ts`
-`surgeonSignature` 改为:
```ts
{ key: 'surgeonSignature', label: '手术者签名', category: '图片', type: 'signature', visibleInForm: true, isSystemLocked: false }
```
- 在 `DEFAULT_FORM_FIELDS` 末尾追加(放在 `surgeonSignature` 之前或之后均可):
```ts
{ key: 'isSigned', label: '手术者签名确认', category: '单选', type: 'single_select', visibleInForm: true, isSystemLocked: false, options: ['已签字', '未签字'] },
```
### 2.3 `src/pages/ReportEditor.tsx`
#### A. 初始 `reportData` 增加 `isSigned`
```ts
const [reportData, setReportData] = useState<Partial<Report>>({
// ... 其他字段
isSigned: '未签字',
// ...
});
```
#### B. 签名同步逻辑重构
将 `surgeonSignature` 的特殊处理从 `useEffect` 移到更前面的位置,逻辑改为:
```ts
if (fieldKey === 'surgeonSignature') {
const isSigned = (reportData as any).isSigned === '已签字';
const signatureData = currentUser?.signature;
if (isSigned && signatureData) {
const imgHtml = `<img src="${signatureData}" class="report-signature-img" alt="签名" draggable="false" />`;
if (el.innerHTML !== imgHtml) {
el.innerHTML = imgHtml;
el.style.border = 'none';
el.style.backgroundColor = 'transparent';
}
} else {
const placeholder = isSigned ? '【请上传电子签】' : '【未签字】';
if (el.innerText !== placeholder) {
el.innerText = placeholder;
el.style.border = '';
el.style.backgroundColor = '';
}
}
return;
}
```
#### C. 完成报告签名校验
在 `saveReport` 的 `status === 'completed'` 分支中,在现有患者信息校验之后追加:
```ts
const hasSignatureField = editorRef.current?.querySelector('[data-bind="surgeonSignature"]');
if (hasSignatureField) {
const isSigned = reportData.isSigned === '已签字';
const hasSignatureImage = !!currentUser?.signature;
if (!isSigned) {
const proceed = window.confirm('提示:模板中包含【手术者签名】字段,但您在基本信息中未选择"已签字"。是否继续完成报告?');
if (!proceed) return;
} else if (!hasSignatureImage) {
const proceed = window.confirm('提示:您选择了"已签字",但您的账号尚未上传电子签名图片。报告中将不显示签名图片,是否继续完成?');
if (!proceed) return;
}
}
```
### 2.4 `src/index.css`
修改 `.report-signature-img`
```css
.report-signature-img {
max-width: 120px;
max-height: 40px;
width: auto;
height: auto;
object-fit: contain;
vertical-align: middle;
display: inline-block;
}
```
在 `@media print` 中同步:
```css
@media print {
.report-signature-img {
max-width: 120px !important;
max-height: 40px !important;
width: auto !important;
height: auto !important;
object-fit: contain !important;
vertical-align: middle !important;
display: inline-block !important;
}
}
```
### 2.5 `src/utils/print.ts`
在 iframe 的 `<style>` 中,`.smart-field-wrapper` 规则之后追加:
```css
.report-signature-img { max-width: 120px; max-height: 40px; width: auto; height: auto; object-fit: contain; vertical-align: middle; display: inline-block; }
```
## 三、风险与回滚
- **风险**:改用 `execCommand('delete')` 后,少数旧版浏览器可能行为不一致,但现代 Chromium/Edge 支持良好。
- **风险**`&#8203;` 零宽空格在极少数场景下可能导致光标异常,但其为无形字符,影响极小。
- **回滚**:如出现问题,可回退上述 5 个文件的修改。

View File

@@ -0,0 +1,56 @@
# 测试方案 — 撤销栈修复、字段删除交互优化与签名字段闭环2026-04-17-12-34-56
## 一、编译检查
- 执行 `npm run lint``tsc --noEmit`),确保全量 TypeScript 无编译错误。
## 二、功能验证步骤
### 测试 1TemplateManage 撤销功能恢复
1. 进入【模板管理】,选择默认模板。
2. 点击某智能字段(如"手术日期")右上角的红色 × 删除该字段。
3. 点击编辑器工具栏的"撤销"按钮(↶)。
4. 确认被删除的字段重新出现,撤销功能正常。
### 测试 2TemplateManage 插入字段不强制换行
1. 在模板编辑器中,将光标定位到一行文字中间(如"手术名称:"后面)。
2. 点击右侧字段库插入"手术日期"。
3. 确认"手术日期"字段框紧跟在光标位置,没有跳到下一行。
4. 再次插入"手术者签名",确认同样保持在当前行。
### 测试 3TemplateManage Backspace/Delete 精准删除
1. 将光标定位在某智能字段框**正后方**,按 Backspace。
2. 确认仅该字段被删除,前面和后面的文本不受影响。
3. 将光标定位在某智能字段框**正前方**,按 Delete。
4. 确认仅该字段被删除,其他文本完好无损。
5. 删除后点击"撤销",确认字段恢复。
### 测试 4签名图片尺寸约束
1. 进入【用户管理】,给当前登录用户上传一张较大的电子签图片。
2. 进入【新建报告】,在右侧【基本信息】中将"手术者签名确认"选择为"已签字"。
3. 观察模板中的签名图片,确认其宽度不超过 120px高度不超过 40px且等比例缩放未变形。
### 测试 5签名字段显隐与表单联动
1. 进入【模板管理】,查看右侧【字段管理】,确认"手术者签名"和"手术者签名确认"字段存在且可切换"显示/隐藏"。
2. 进入【新建报告】,确认右侧【基本信息】表单中出现了"手术者签名确认"下拉框(默认"未签字")。
3. 选择"未签字",确认模板中的签名方框显示"【未签字】"。
4. 选择"已签字",确认模板中的签名方框显示签名图片。
5. 在 UserManage 中清除当前用户签名,返回 ReportEditor 将"手术者签名确认"选为"已签字"。
6. 确认签名方框显示"【请上传电子签】"。
### 测试 6完成报告签名校验提示
1. 确保模板中存在"手术者签名"字段,且 ReportEditor 中"手术者签名确认"为"未签字"。
2. 点击"完成报告",确认弹出提示:"模板中包含【手术者签名】字段,但您在基本信息中未选择'已签字'。是否继续完成报告?"
3. 点击"取消",确认报告未被保存,停留在编辑页。
4. 将"手术者签名确认"改为"已签字",清除当前用户的电子签图片。
5. 再次点击"完成报告",确认弹出提示:"您选择了'已签字',但您的账号尚未上传电子签名图片。报告中将不显示签名图片,是否继续完成?"
6. 点击"确定",确认报告正常保存并跳转至报告管理页。
## 三、预期结果
- `npm run lint` 0 错误。
- TemplateManage 中删除字段后撤销正常。
- 插入字段不强制换行Backspace/Delete 精准删除单个字段。
- 签名图片受 120×40px 约束,等比例缩放。
- 表单联动正常:已签字→显示图片,未签字→显示"【未签字】",无签名图→显示"【请上传电子签】"。
- 完成报告时签名异常给出弱阻断提示,用户可取消或继续。

View File

@@ -453,6 +453,47 @@ if ((settings.autoInsertDelay || 0) > 0) {
---
## 记录 19撤销栈修复、字段删除交互优化与签名字段闭环
**A. 具体问题**
1. `TemplateManage` 中通过红色 × 或键盘删除智能字段后浏览器撤销栈Undo失效点击"撤销"按钮无法恢复。
2. 插入"手术日期"、"手术者签名"等字段后,字段框有时会跳到下一行。
3. Backspace 键无法删除字段Delete 键会误删字段前面的大段文本(如"手术步骤、术中出现的情况及处理:")。
4. 签名图片没有最大尺寸限制;"手术者签名"字段不在 ReportEditor 表单中显示,无法受控管理签字状态。
5. 点击"完成报告"时缺少对签名状态的确认提示。
**B. 产生问题原因**
1. 删除字段时使用了 `target.remove()` 直接操作 DOM绕过了浏览器的原生撤销栈`undo stack`)。
2. 插入的 `smart-field-wrapper` 是 `inline-block` 元素,但其后缺少行内锚点文本节点,浏览器在特定光标位置插入时容易将其挤到新行。
3. `keydown` 拦截逻辑中 `target.remove()` 同样会误删父级块节点WebKit 在边界处对 `contenteditable="false"` inline 元素的处理缺陷)。
4. `surgeonSignature` 字段原先 `visibleInForm: false`,且签名图片样式仅用 `height: 2.4em` 约束,没有 `max-width/max-height` 的硬限制。
5. 完成报告逻辑中缺少针对签名字段的业务校验。
**C. 解决问题方案**
1. **撤销栈修复**:将点击红 × 删除和键盘 Backspace/Delete 删除全部改为 `Range.selectNode(target)` + `document.execCommand('delete')`。这样浏览器会将删除操作记录到撤销栈中,`execCommand('undo')` 可以正确恢复。
2. **防换行**:在 `insertSmartField` 和 `defaultContent.ts` 的 `smartField()` 生成的 HTML 末尾增加 `&#8203;`(零宽空格),作为稳定的行内锚点,防止字段被浏览器排到新行。
3. **精准键盘删除**:配合 `Range.selectNode` + `execCommand('delete')`,不再直接 `remove()` DOM 节点,彻底避免误删父级 `<p>` 的问题。
4. **签名尺寸与字段管理**
- `types.ts` 中将 `surgeonSignature` 改为 `visibleInForm: true, isSystemLocked: false`,使其出现在字段管理和右侧表单中。
- 新增 `isSigned` 字段(单选:已签字 / 未签字,默认"未签字")。
- 签名图片样式改为 `max-width: 120px; max-height: 40px; object-fit: contain;`,并在打印样式和 `print.ts` 中同步。
5. **签名同步逻辑重构**`ReportEditor` 中 `surgeonSignature` 的渲染由 `isSigned` 控制:
- `已签字` 且 `currentUser.signature` 存在 → 显示签名图片。
- `已签字` 但无签名图 → 显示 "【请上传电子签】"。
- `未签字` → 显示 "【未签字】"。
6. **完成报告签名校验**`saveReport('completed')` 中,若模板包含 `surgeonSignature`
- 未选择"已签字" → `confirm` 弱阻断提示。
- 已选择"已签字"但无签名图 → `confirm` 弱阻断提示。
- 用户点击"取消"则中断保存,点击"确定"仍可继续保存。
**D. 后续如何避免问题**
- 在 `contentEditable` 中删除元素时,**优先使用 `Range.selectNode` + `execCommand('delete')`** 而非直接 `remove()`,以确保撤销/重做等原生编辑行为正常工作。
- 插入 `inline-block` 或 `inline-flex` 控件时,可在其后追加 `&#8203;` 零宽空格,为浏览器提供稳定的行内文本锚点,减少排版异常。
- 任何需要从"不可见"改为"可见/可配置"的字段,应在 `DEFAULT_FORM_FIELDS`、`Report 类型`、`reportData 初始值` 三处同步更新,防止表单渲染遗漏。
- 对于图片类嵌入内容,应使用 `max-width`/`max-height` + `object-fit: contain` 做硬约束,避免不同来源图片破坏页面布局。
---
## 记录 14智能字段插入间距修复与 Backspace 防误删
**A. 具体问题**

View File

@@ -0,0 +1,67 @@
# 需求分析 — 撤销栈修复、字段删除交互优化与签名字段闭环2026-04-17-12-34-56
## 一、需求来源
用户反馈 TemplateManage 中删除字段后撤销失效、插入字段导致非预期换行、Backspace/Delete 误删父级内容;同时希望优化手术者签名的业务逻辑闭环,包括尺寸约束、表单显隐控制及完成报告时的弱阻断提示。
## 二、具体需求拆解
### 需求 1修复撤销Undo失效
**问题**:在 TemplateManage 中点击红色 × 或通过键盘删除智能字段后,点击工具栏的"撤销"按钮无法恢复被删除的字段。
**原因**:当前代码使用 `smartField.remove()``target.remove()` 直接操作 DOM绕过了浏览器的原生撤销栈undo stack
**期望**:删除字段的操作能被浏览器的 `execCommand('undo')` 正确撤销。
### 需求 2修复插入字段换行与 Backspace/Delete 误删
**问题**
- 在 TemplateManage 中插入"手术日期"或"手术者签名"后,字段框有时会直接跳到下一行。
- Backspace 键无法删除字段(无反应)。
- Delete 键会误删字段前面的大段文本(如"手术步骤、术中出现的情况及处理:")。
**期望**
- 插入的字段保持在当前行内,不强制换行。
- Backspace/Delete 键能精准删除光标相邻的单个字段节点,不影响周围文本。
### 需求 3签名图片尺寸约束与字段管理
**问题**
- 当前签名图片没有最大尺寸限制,可能过大。
- "手术者签名"字段不在 ReportEditor 的右侧基本信息表单中显示,无法控制是否签字。
**期望**
- 签名图片设置最大宽度 `120px`、最大高度 `40px`,等比例缩放(`object-fit: contain`)。
-`surgeonSignature` 字段改为 `visibleInForm: true``isSystemLocked: false`,使其可在字段管理中显隐控制。
- 在 ReportEditor 右侧表单中增加"手术者签名确认"`isSigned`)单选下拉框,选项为"已签字"、"未签字",默认"未签字"。
- 模板中的 `surgeonSignature` 字段根据 `isSigned` 的值渲染:
- `isSigned === '已签字'` 且用户有签名图 → 显示等比例缩放后的签名图片。
- 其他情况 → 显示"【未签字】"。
### 需求 4完成报告时的签名校验提示
**期望**:点击"完成报告"时:
- 若模板中存在 `data-bind="surgeonSignature"` 的字段,但 `isSigned !== '已签字'`,弹出 `confirm` 提示"模板中包含【手术者签名】字段,但您未选择'已签字'。是否继续完成报告?",用户选择"取消"则中断保存,选择"确定"则继续保存。
-`isSigned === '已签字'` 但当前用户未上传电子签名图片,弹出 `confirm` 提示"您选择了'已签字',但账号尚未上传电子签名图片。报告中将不显示签名图片,是否继续完成?",同样弱阻断(可继续保存)。
## 三、影响范围分析
| 文件 | 改动说明 |
|------|----------|
| `src/pages/TemplateManage.tsx` | 点击删除和键盘删除逻辑全部改用 `Selection + Range + execCommand('delete')``insertSmartField` 的 HTML 末尾增加 `&#8203;`(零宽空格)防止强制换行。 |
| `src/types.ts` | `surgeonSignature` 改为 `visibleInForm: true, isSystemLocked: false`;新增 `isSigned` 字段到 `DEFAULT_FORM_FIELDS`。 |
| `src/pages/ReportEditor.tsx` | `reportData` 初始值增加 `isSigned: '未签字'`;签名同步逻辑改为基于 `isSigned` 判断;`saveReport('completed')` 增加签名校验提示。 |
| `src/index.css` | 调整 `.report-signature-img``max-width: 120px; max-height: 40px; object-fit: contain;`。 |
| `src/utils/print.ts` | 打印样式中同步签名图片尺寸约束。 |
## 四、验收标准
- [ ] TemplateManage 中删除字段后,点击"撤销"按钮能正确恢复字段。
- [ ] TemplateManage 中插入字段后保持在当前行,不跳到下一行。
- [ ] Backspace/Delete 键能精准删除单个字段节点,不误删周围文本。
- [ ] ReportEditor 中签名图片最大 120×40px等比例缩放。
- [ ] ReportEditor 右侧表单出现"手术者签名确认"下拉框(已签字/未签字)。
- [ ] 选择"已签字"且有签名图时,模板中显示签名图片;选择"未签字"或无签名图时显示"【未签字】"。
- [ ] 点击"完成报告"时,签名状态异常会弹出弱阻断提示,用户可取消或继续保存。
- [ ] `npm run lint` 无编译错误。