diff --git a/src/index.css b/src/index.css
index 3e87168..38ad15a 100644
--- a/src/index.css
+++ b/src/index.css
@@ -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;
}
}
diff --git a/src/pages/ReportEditor.tsx b/src/pages/ReportEditor.tsx
index e988411..572a895 100644
--- a/src/pages/ReportEditor.tsx
+++ b/src/pages/ReportEditor.tsx
@@ -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 = `
`;
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 = '';
}
diff --git a/src/pages/TemplateManage.tsx b/src/pages/TemplateManage.tsx
index 9a3277e..29abc35 100644
--- a/src/pages/TemplateManage.tsx
+++ b/src/pages/TemplateManage.tsx
@@ -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 = ` ×`;
+ const html = ` ×`;
document.execCommand('insertHTML', false, html);
editorRef.current?.focus();
};
diff --git a/src/types.ts b/src/types.ts
index 37759c1..c2531c3 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -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 },
];
diff --git a/src/utils/defaultContent.ts b/src/utils/defaultContent.ts
index fa55202..0bc9c18 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/src/utils/print.ts b/src/utils/print.ts
index f4e8e3c..cae0db5 100644
--- a/src/utils/print.ts
+++ b/src/utils/print.ts
@@ -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; }
}
diff --git a/工程分析/实现方案-2026-04-17-12-34-56.md b/工程分析/实现方案-2026-04-17-12-34-56.md
new file mode 100644
index 0000000..485ec68
--- /dev/null
+++ b/工程分析/实现方案-2026-04-17-12-34-56.md
@@ -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 字符串末尾增加 ``(零宽空格),作为行内锚点,防止浏览器将字段挤到新行:
+```html
+...
+```
+
+### 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>({
+ // ... 其他字段
+ isSigned: '未签字',
+ // ...
+});
+```
+
+#### B. 签名同步逻辑重构
+将 `surgeonSignature` 的特殊处理从 `useEffect` 移到更前面的位置,逻辑改为:
+```ts
+if (fieldKey === 'surgeonSignature') {
+ const isSigned = (reportData as any).isSigned === '已签字';
+ const signatureData = currentUser?.signature;
+ if (isSigned && signatureData) {
+ const imgHtml = `
`;
+ 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 的 `