feat: field hover highlight, e-signature upload, surgeon signature linkage
- Add signature?: string to User type and 'signature' to FieldType - Add surgeonSignature field to DEFAULT_FORM_FIELDS (category: 图片) - UserManage: add canvas-based image compression (max 500px) and signature upload UI - TemplateManage: add hover highlight on field buttons via direct DOM style manipulation - TemplateManage: add '图片' category to field library for surgeonSignature insertion - ReportEditor: auto-fill surgeonSignature with currentUser.signature image or placeholder text - index.css & print.ts: add .report-signature-img styling (height 2.4em, vertical-align middle) - Update experience record (#18)
This commit is contained in:
@@ -156,6 +156,13 @@
|
||||
.template-editor-mode .smart-field-wrapper:focus-within .delete-btn {
|
||||
display: block;
|
||||
}
|
||||
.report-signature-img {
|
||||
height: 2.4em;
|
||||
width: auto;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
margin: -0.3em 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
@@ -195,4 +202,11 @@
|
||||
.print-content .smart-field-wrapper .delete-btn {
|
||||
display: none !important;
|
||||
}
|
||||
.report-signature-img {
|
||||
height: 2.4em !important;
|
||||
width: auto !important;
|
||||
vertical-align: middle !important;
|
||||
display: inline-block !important;
|
||||
margin: -0.3em 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -940,6 +940,25 @@ export default function ReportEditor() {
|
||||
const el = node as HTMLElement;
|
||||
const fieldKey = el.getAttribute('data-bind')!;
|
||||
|
||||
if (fieldKey === 'surgeonSignature') {
|
||||
const signatureData = currentUser?.signature;
|
||||
if (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 {
|
||||
if (el.innerText !== '【请上传电子签】') {
|
||||
el.innerText = '【请上传电子签】';
|
||||
el.style.border = '';
|
||||
el.style.backgroundColor = '';
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let newValue = '';
|
||||
if (fieldKey === 'startTime') {
|
||||
newValue = `${reportData.startHour || ''}:${reportData.startMinute || ''}`;
|
||||
|
||||
@@ -254,6 +254,20 @@ export default function TemplateManage() {
|
||||
editorRef.current?.focus();
|
||||
};
|
||||
|
||||
const highlightField = (key: string, active: boolean) => {
|
||||
if (!editorRef.current) return;
|
||||
const el = editorRef.current.querySelector(`[data-bind="${key}"]`) as HTMLElement | null;
|
||||
if (!el) return;
|
||||
if (active) {
|
||||
el.style.transition = 'all 0.2s';
|
||||
el.style.boxShadow = '0 0 0 2px #3b82f6';
|
||||
el.style.backgroundColor = '#e0f2fe';
|
||||
} else {
|
||||
el.style.boxShadow = '';
|
||||
el.style.backgroundColor = '';
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFieldVisible = (key: string) => {
|
||||
const updated = formFields.map(f => f.key === key ? { ...f, visibleInForm: !f.visibleInForm } : f);
|
||||
setFormFields(updated);
|
||||
@@ -601,7 +615,7 @@ export default function TemplateManage() {
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
{fieldLibTab === 'insert' && (
|
||||
<div className="space-y-4">
|
||||
{['填空', '单选', '多选', '时间'].map(cat => {
|
||||
{['填空', '单选', '多选', '时间', '图片'].map(cat => {
|
||||
const catFields = formFields.filter(f => f.category === cat);
|
||||
if (catFields.length === 0) return null;
|
||||
return (
|
||||
@@ -613,6 +627,8 @@ export default function TemplateManage() {
|
||||
key={field.key}
|
||||
type="button"
|
||||
onClick={() => insertSmartField(field)}
|
||||
onMouseEnter={() => highlightField(field.key, true)}
|
||||
onMouseLeave={() => highlightField(field.key, false)}
|
||||
className="px-2 py-1 text-[11px] bg-slate-100 hover:bg-slate-200 text-slate-700 rounded border border-slate-300 transition-colors"
|
||||
title={`插入 ${field.label}`}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Sidebar from '../components/Sidebar';
|
||||
import { UserPlus, Edit, Trash2 } from 'lucide-react';
|
||||
import { UserPlus, Edit, Trash2, Upload, X } from 'lucide-react';
|
||||
import { User, Template } from '../types';
|
||||
import { storage } from '../utils/storage';
|
||||
|
||||
@@ -56,6 +56,50 @@ export default function UserManage() {
|
||||
storage.set('users', updatedUsers);
|
||||
};
|
||||
|
||||
const compressImage = (file: File, maxSize: number = 500): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = (e) => {
|
||||
const img = new Image();
|
||||
img.src = e.target?.result as string;
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
let { width, height } = img;
|
||||
if (width > height && width > maxSize) {
|
||||
height = Math.round((height * maxSize) / width);
|
||||
width = maxSize;
|
||||
} else if (height > maxSize) {
|
||||
width = Math.round((width * maxSize) / height);
|
||||
height = maxSize;
|
||||
}
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
}
|
||||
resolve(canvas.toDataURL('image/jpeg', 0.8));
|
||||
};
|
||||
img.onerror = reject;
|
||||
};
|
||||
reader.onerror = reject;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSignatureUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const compressed = await compressImage(file);
|
||||
setFormData(prev => ({ ...prev, signature: compressed }));
|
||||
} catch {
|
||||
alert('图片压缩失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (username: string) => {
|
||||
if (username === 'admin') {
|
||||
alert('不能删除默认超级管理员');
|
||||
@@ -226,7 +270,7 @@ export default function UserManage() {
|
||||
|
||||
updatedUsers = users.map(u => {
|
||||
if (u.username === formData.username) {
|
||||
return { ...u, role: finalRole, department: finalDepartment, manageableTemplates, visibleTemplates: adminVisible, password: formData.password || u.password } as User;
|
||||
return { ...u, role: finalRole, department: finalDepartment, manageableTemplates, visibleTemplates: adminVisible, password: formData.password || u.password, signature: formData.signature } as User;
|
||||
}
|
||||
if (u.role === 'user' && u.department === (oldUser.department || finalDepartment)) {
|
||||
const currentVisible = Array.isArray(u.visibleTemplates) ? u.visibleTemplates : [];
|
||||
@@ -568,6 +612,43 @@ export default function UserManage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">电子签名</label>
|
||||
{formData.signature ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src={formData.signature}
|
||||
alt="电子签名预览"
|
||||
className="h-16 border border-border rounded bg-white object-contain"
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="px-3 py-1.5 text-xs font-medium rounded-lg bg-slate-100 hover:bg-slate-200 transition-colors cursor-pointer inline-flex items-center gap-1">
|
||||
<Upload size={12} />
|
||||
重新上传
|
||||
<input type="file" accept="image/*" className="hidden" onChange={handleSignatureUpload} />
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormData(prev => ({ ...prev, signature: undefined }))}
|
||||
className="px-3 py-1.5 text-xs font-medium rounded-lg bg-red-50 text-red-600 hover:bg-red-100 transition-colors inline-flex items-center gap-1"
|
||||
>
|
||||
<X size={12} />
|
||||
清除签名
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="px-4 py-2 text-sm font-medium rounded-lg bg-slate-100 hover:bg-slate-200 transition-colors cursor-pointer inline-flex items-center gap-2">
|
||||
<Upload size={14} />
|
||||
上传签名
|
||||
<input type="file" accept="image/*" className="hidden" onChange={handleSignatureUpload} />
|
||||
</label>
|
||||
<span className="text-xs text-text-muted">支持 JPG、PNG,自动压缩至 500px 以内</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showManageableTemplates && (
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface User {
|
||||
createdAt?: string;
|
||||
visibleTemplates?: string[];
|
||||
manageableTemplates?: string[];
|
||||
signature?: string;
|
||||
}
|
||||
|
||||
export interface Report {
|
||||
@@ -102,7 +103,7 @@ export const BINDABLE_FIELDS: BindableField[] = [
|
||||
{ key: 'anesthesiaType', label: '麻醉方式' },
|
||||
];
|
||||
|
||||
export type FieldType = 'text' | 'single_select' | 'multi_select' | 'time' | 'date';
|
||||
export type FieldType = 'text' | 'single_select' | 'multi_select' | 'time' | 'date' | 'signature';
|
||||
|
||||
export interface FormField {
|
||||
key: string;
|
||||
@@ -129,4 +130,5 @@ 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 },
|
||||
];
|
||||
|
||||
@@ -37,6 +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; }
|
||||
@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; }
|
||||
}
|
||||
|
||||
161
工程分析/实现方案-2026-04-17-11-34-24.md
Normal file
161
工程分析/实现方案-2026-04-17-11-34-24.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# 实现方案 — 字段悬浮高亮、电子签上传与手术者签名联动(2026-04-17-11-34-24)
|
||||
|
||||
## 一、修改文件清单
|
||||
|
||||
1. `src/types.ts` — 扩展 `User` / `FieldType` / `DEFAULT_FORM_FIELDS`
|
||||
2. `src/pages/UserManage.tsx` — 电子签上传组件 + 前端压缩逻辑
|
||||
3. `src/pages/TemplateManage.tsx` — 悬浮高亮 + 图片分类 + 手术者签名插入
|
||||
4. `src/pages/ReportEditor.tsx` — `surgeonSignature` 特殊同步逻辑
|
||||
5. `src/index.css` — 签名图片排版样式 + 打印样式
|
||||
6. `src/utils/print.ts` — 打印样式中增加签名图片规则
|
||||
|
||||
## 二、详细改动
|
||||
|
||||
### 2.1 `src/types.ts`
|
||||
|
||||
- `User` 接口追加 `signature?: string`。
|
||||
- `FieldType` 扩展为 `'text' | 'single_select' | 'multi_select' | 'time' | 'date' | 'signature'`。
|
||||
- `DEFAULT_FORM_FIELDS` 末尾追加:
|
||||
```ts
|
||||
{ key: 'surgeonSignature', label: '手术者签名', category: '图片', type: 'signature', visibleInForm: false, isSystemLocked: true }
|
||||
```
|
||||
|
||||
### 2.2 `src/pages/UserManage.tsx`
|
||||
|
||||
#### A. 前端压缩工具函数
|
||||
```ts
|
||||
const compressImage = (file: File, maxSize: number = 500): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = (e) => {
|
||||
const img = new Image();
|
||||
img.src = e.target?.result as string;
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
let { width, height } = img;
|
||||
if (width > height && width > maxSize) {
|
||||
height = Math.round((height * maxSize) / width);
|
||||
width = maxSize;
|
||||
} else if (height > maxSize) {
|
||||
width = Math.round((width * maxSize) / height);
|
||||
height = maxSize;
|
||||
}
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
}
|
||||
resolve(canvas.toDataURL('image/jpeg', 0.8));
|
||||
};
|
||||
img.onerror = reject;
|
||||
};
|
||||
reader.onerror = reject;
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
#### B. 上传组件与保存逻辑
|
||||
- 在模态框表单中("状态"选择器下方或底部按钮上方)增加一个区块:
|
||||
- 标签:"电子签名"
|
||||
- 若 `formData.signature` 有值,显示压缩后的预览图(高度限制 64px)。
|
||||
- "上传签名" 按钮(`type="button"`),触发隐藏的 `<input type="file" accept="image/*">`。
|
||||
- "清除签名" 按钮(有值时显示)。
|
||||
- `handleSubmit` 中保存 `signature` 字段到用户对象。
|
||||
- 编辑当前登录用户时,同步更新 `storage.set('currentUser', currentCached)`,确保 ReportEditor 能立即读取到最新签名。
|
||||
|
||||
### 2.3 `src/pages/TemplateManage.tsx`
|
||||
|
||||
#### A. 悬浮高亮
|
||||
在字段库按钮上增加 `onMouseEnter` 和 `onMouseLeave`:
|
||||
```ts
|
||||
const highlightField = (key: string, active: boolean) => {
|
||||
if (!editorRef.current) return;
|
||||
const el = editorRef.current.querySelector(`[data-bind="${key}"]`) as HTMLElement | null;
|
||||
if (!el) return;
|
||||
if (active) {
|
||||
el.style.transition = 'all 0.2s';
|
||||
el.style.boxShadow = '0 0 0 2px #3b82f6';
|
||||
el.style.backgroundColor = '#e0f2fe';
|
||||
} else {
|
||||
el.style.boxShadow = '';
|
||||
el.style.backgroundColor = '';
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### B. 图片分类与手术者签名
|
||||
- 插入字段分类数组从 `['填空', '单选', '多选', '时间']` 改为 `['填空', '单选', '多选', '时间', '图片']`。
|
||||
- `surgeonSignature` 字段会自动出现在"图片"分类下,按钮点击逻辑复用 `insertSmartField`(已支持唯一性校验)。
|
||||
|
||||
### 2.4 `src/pages/ReportEditor.tsx`
|
||||
|
||||
在"Sync form state -> rich text field values"的 `useEffect` 中,对 `fieldKey === 'surgeonSignature'` 做特殊分支:
|
||||
```ts
|
||||
if (fieldKey === 'surgeonSignature') {
|
||||
const signatureData = currentUser?.signature;
|
||||
if (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 {
|
||||
if (el.innerText !== '【请上传电子签】') {
|
||||
el.innerText = '【请上传电子签】';
|
||||
el.style.border = '';
|
||||
el.style.backgroundColor = '';
|
||||
}
|
||||
}
|
||||
return; // 跳过常规文本同步
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 `src/index.css`
|
||||
|
||||
增加签名图片样式:
|
||||
```css
|
||||
.report-signature-img {
|
||||
height: 2.4em;
|
||||
width: auto;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
margin: -0.3em 0;
|
||||
}
|
||||
```
|
||||
|
||||
在 `@media print` 中同步增加:
|
||||
```css
|
||||
@media print {
|
||||
.report-signature-img {
|
||||
height: 2.4em !important;
|
||||
width: auto !important;
|
||||
vertical-align: middle !important;
|
||||
display: inline-block !important;
|
||||
margin: -0.3em 0 !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.6 `src/utils/print.ts`
|
||||
|
||||
在打印 iframe 的 `<style>` 标签内,`.smart-field-wrapper` 规则之后追加:
|
||||
```css
|
||||
.report-signature-img {
|
||||
height: 2.4em;
|
||||
width: auto;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
margin: -0.3em 0;
|
||||
}
|
||||
```
|
||||
|
||||
## 三、风险与回滚
|
||||
|
||||
- **风险**:`localStorage` 容量有限,压缩后的签名图片通常在 10~50KB,单用户存储安全。
|
||||
- **风险**:旧用户的 `User` 对象没有 `signature` 字段,读取时为 `undefined`,代码中已通过可选链和默认值处理。
|
||||
- **回滚**:如出现问题,可回退 6 个文件的修改。
|
||||
49
工程分析/测试方案-2026-04-17-11-34-24.md
Normal file
49
工程分析/测试方案-2026-04-17-11-34-24.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# 测试方案 — 字段悬浮高亮、电子签上传与手术者签名联动(2026-04-17-11-34-24)
|
||||
|
||||
## 一、编译检查
|
||||
|
||||
- 执行 `npm run lint`(`tsc --noEmit`),确保全量 TypeScript 无编译错误。
|
||||
|
||||
## 二、功能验证步骤
|
||||
|
||||
### 测试 1:TemplateManage 字段悬浮高亮
|
||||
1. 进入【模板管理】,选择默认模板。
|
||||
2. 将鼠标悬浮在右侧字段库中的"姓名"按钮上(不点击)。
|
||||
3. 观察编辑器中"姓名"对应的智能字段方框,确认出现蓝色外发光/背景变浅蓝色高亮。
|
||||
4. 鼠标移开"姓名"按钮,确认高亮效果消失,字段框恢复原样。
|
||||
5. 尝试悬浮其他字段按钮(如"手术名称"、"手术日期"),确认高亮定位准确。
|
||||
|
||||
### 测试 2:UserManage 电子签上传与压缩
|
||||
1. 进入【用户管理】,点击任意医生用户的"编辑"按钮。
|
||||
2. 在编辑弹窗中找到"电子签名"区域,点击"上传签名"。
|
||||
3. 选择一张大于 500×500 像素的本地图片(如 1200×800 的 PNG/JPG)。
|
||||
4. 确认上传后预览图显示正常,且图片已被等比例压缩(宽或高最大不超过 500px)。
|
||||
5. 点击"保存用户",刷新页面后再次编辑该用户,确认签名图片仍然保留。
|
||||
6. 点击"清除签名",确认预览图消失;保存后刷新,确认签名已清除。
|
||||
|
||||
### 测试 3:TemplateManage 新增"手术者签名"字段
|
||||
1. 进入【模板管理】,查看右侧【插入字段】面板。
|
||||
2. 确认分类列表中新增"图片"分类,下方有"手术者签名"按钮。
|
||||
3. 点击"手术者签名"按钮,确认模板中插入一个智能字段方框(`data-bind="surgeonSignature"`)。
|
||||
4. 再次点击"手术者签名",确认弹出"已存在,请勿重复插入"的提示。
|
||||
|
||||
### 测试 4:ReportEditor 签名自动填充
|
||||
1. 确保当前登录用户(如 admin)已通过测试 2 上传了电子签。
|
||||
2. 进入【新建报告】或编辑已有报告,观察模板中的"手术者签名"方框。
|
||||
3. 确认方框中自动显示了当前登录用户的签名图片(高度约 2 行文字)。
|
||||
4. 在 UserManage 中清除当前用户的签名,返回 ReportEditor 刷新页面。
|
||||
5. 确认"手术者签名"方框显示文本"【请上传电子签】"。
|
||||
|
||||
### 测试 5:签名图片排版与打印效果
|
||||
1. 在 ReportEditor 中确认签名图片与周围文字行高协调,没有明显撑大段落间距。
|
||||
2. 点击报告页面的"打印"按钮,在浏览器打印预览中观察签名图片。
|
||||
3. 确认打印输出中签名图片高度仍然保持约 2 行文字,排版正常。
|
||||
|
||||
## 三、预期结果
|
||||
|
||||
- `npm run lint` 0 错误。
|
||||
- TemplateManage 悬浮高亮响应迅速,定位准确。
|
||||
- UserManage 电子签上传、压缩、清除、持久化均正常。
|
||||
- TemplateManage 可插入"手术者签名"字段,且唯一性校验生效。
|
||||
- ReportEditor 能自动根据当前用户签名状态填充图片或提示文本。
|
||||
- 签名图片在编辑态和打印态均保持约 2 行文字高度,排版美观。
|
||||
44
工程分析/经验记录.md
44
工程分析/经验记录.md
@@ -409,6 +409,50 @@ if ((settings.autoInsertDelay || 0) > 0) {
|
||||
|
||||
---
|
||||
|
||||
## 记录 18:字段悬浮高亮、电子签上传与手术者签名联动
|
||||
|
||||
**A. 具体问题**
|
||||
1. `TemplateManage` 中右侧字段库按钮与编辑器中的字段缺乏视觉关联,用户难以快速定位字段位置。
|
||||
2. `UserManage` 缺少电子签名上传功能,无法为医生绑定个人签名图。
|
||||
3. 模板中缺少"手术者签名"字段,报告编辑时无法自动带入医生签名。
|
||||
4. 签名图片若直接放入 `.field-value` 中,容易撑大行高,影响排版和打印效果。
|
||||
|
||||
**B. 产生问题原因**
|
||||
1. 字段库按钮没有任何与编辑器 DOM 联动的交互反馈机制。
|
||||
2. 早期设计未考虑医疗文书中的电子签需求,`User` 模型和 `DEFAULT_FORM_FIELDS` 均缺少签名相关定义。
|
||||
3. 没有针对签名图片设计专门的 CSS 尺寸约束,导致浏览器按原图尺寸渲染,破坏行高。
|
||||
|
||||
**C. 解决问题方案**
|
||||
1. **悬浮高亮**:在 `TemplateManage.tsx` 的字段库按钮上增加 `onMouseEnter` / `onMouseLeave`,直接操作编辑器中对应 `data-bind` 的 `.field-value` 的 `style.boxShadow` 和 `style.backgroundColor`,实现蓝色外发光/背景变浅蓝色的即时高亮反馈。
|
||||
2. **电子签上传与压缩**:
|
||||
- 在 `UserManage.tsx` 中增加 `compressImage(file, maxSize=500)` 工具函数,利用 Canvas 等比例缩放并填充白色背景,输出 JPEG base64(质量 0.8)。
|
||||
- 在用户编辑/新增弹窗中增加"电子签名"区块:预览图、上传按钮、清除按钮。
|
||||
- 编辑当前登录用户时同步更新 `storage.set('currentUser', ...)`,确保 ReportEditor 能读取最新签名。
|
||||
3. **手术者签名字段**:
|
||||
- `types.ts` 中 `User` 增加 `signature?: string`;`FieldType` 增加 `'signature'`;`DEFAULT_FORM_FIELDS` 追加 `surgeonSignature`(分类"图片",系统锁定)。
|
||||
- `TemplateManage` 插入字段分类增加"图片",`surgeonSignature` 自动出现在该分类下。
|
||||
- `ReportEditor` 的"表单 → 编辑器"同步 `useEffect` 中,对 `fieldKey === 'surgeonSignature'` 做特殊分支:有签名则填充 `<img class="report-signature-img" src="..." />`,无签名则填充文本"【请上传电子签】"。
|
||||
4. **签名排版优化**:
|
||||
- 在 `index.css` 和 `print.ts` 中定义 `.report-signature-img`:
|
||||
```css
|
||||
.report-signature-img {
|
||||
height: 2.4em;
|
||||
width: auto;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
margin: -0.3em 0;
|
||||
}
|
||||
```
|
||||
- 打印媒体查询中同步使用 `!important` 确保打印输出也保持同样尺寸。
|
||||
|
||||
**D. 后续如何避免问题**
|
||||
- 当需要在 React 之外直接操作 DOM 样式实现即时反馈时,优先使用原生事件 + inline style(避免触发组件重渲染导致光标丢失)。
|
||||
- 任何新增的持久化字段,应在类型定义(TypeScript interface)、默认值(DEFAULT_xxx)、以及所有相关读写逻辑中同步补齐,防止类型不一致。
|
||||
- 在 `contentEditable` 中插入图片时,务必通过 CSS 对 `height`/`width`/`vertical-align` 做严格约束,避免原图尺寸破坏文本流。
|
||||
- 涉及打印的样式必须在 iframe 打印模板和 `@media print` 中双端同步,防止打印效果与屏幕预览不一致。
|
||||
|
||||
---
|
||||
|
||||
## 记录 14:智能字段插入间距修复与 Backspace 防误删
|
||||
|
||||
**A. 具体问题**
|
||||
|
||||
54
工程分析/需求分析-2026-04-17-11-34-24.md
Normal file
54
工程分析/需求分析-2026-04-17-11-34-24.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# 需求分析 — 字段悬浮高亮、电子签上传与手术者签名联动(2026-04-17-11-34-24)
|
||||
|
||||
## 一、需求来源
|
||||
|
||||
用户提出四个关联需求:优化 TemplateManage 字段定位体验、增加用户电子签上传功能、在模板中新增手术者签名字段、并优化签名图片在编辑器及打印中的排版表现。
|
||||
|
||||
## 二、具体需求拆解
|
||||
|
||||
### 需求 1:TemplateManage 字段悬浮高亮定位
|
||||
|
||||
**期望**:当鼠标悬浮在右侧字段库中的某个字段按钮上时,编辑器中已插入的对应 `data-bind` 字段的 `.field-value` 框会有明显的视觉高亮(如边框发光、背景色变化),帮助用户快速定位该字段在模板中的位置。
|
||||
|
||||
### 需求 2:UserManage 电子签上传与前端压缩
|
||||
|
||||
**期望**:
|
||||
- 在【用户管理】的用户编辑/新增弹窗中增加"电子签名"上传区域。
|
||||
- 支持从本地选择图片文件(PNG/JPG 等)。
|
||||
- 上传后利用 Canvas 在前端自动等比例压缩,使图片的长、宽最大不超过 500 像素。
|
||||
- 压缩后的图片以 Base64(JPEG,质量 0.8,白色背景填充透明 PNG)形式存储在用户对象的 `signature` 字段中,并持久化到 `localStorage`。
|
||||
|
||||
### 需求 3:TemplateManage 新增"手术者签名"字段
|
||||
|
||||
**期望**:
|
||||
- 在 `DEFAULT_FORM_FIELDS` 中新增一个系统锁定字段 `surgeonSignature`,分类为"图片",类型为 `signature`。
|
||||
- `TemplateManage` 的【插入字段】侧边栏中增加"图片"分类,包含"手术者签名"按钮。
|
||||
- `ReportEditor` 中,当渲染到 `data-bind="surgeonSignature"` 的智能字段时,自动从 `currentUser.signature` 读取电子签图片并填充到 `.field-value` 中;若当前用户没有上传签名,则显示提示文本"【请上传电子签】"。
|
||||
|
||||
### 需求 4:签名图片在模板中的排版优化
|
||||
|
||||
**期望**:
|
||||
- 签名图片在 `.field-value` 中显示时,高度应恰好约等于 2 行文字(约 `2.4em`),宽度等比例自适应。
|
||||
- 图片垂直对齐方式需与周围文字协调,避免把行高撑得过大。
|
||||
- 打印输出时(`printDocument` 及 `@media print`),签名图片保持同样的高度约束和排版效果。
|
||||
|
||||
## 三、影响范围分析
|
||||
|
||||
| 文件 | 改动说明 |
|
||||
|------|----------|
|
||||
| `src/types.ts` | `User` 接口增加 `signature?: string`;`FieldType` 增加 `'signature'`;`DEFAULT_FORM_FIELDS` 追加 `surgeonSignature` 字段。 |
|
||||
| `src/pages/UserManage.tsx` | 用户编辑弹窗增加电子签上传组件;增加 `compressImage` 前端压缩工具函数;保存时将 `signature` 写入用户对象。 |
|
||||
| `src/pages/TemplateManage.tsx` | 插入字段分类增加"图片";字段按钮增加 `onMouseEnter` / `onMouseLeave` 事件实现悬浮高亮;`insertSmartField` 增加 `surgeonSignature` 的 HTML 输出(与普通字段一致但 `data-bind="surgeonSignature"`)。 |
|
||||
| `src/pages/ReportEditor.tsx` | 在"表单 → 编辑器"同步的 `useEffect` 中,对 `surgeonSignature` 做特殊处理:填充 `<img>` 或提示文本。 |
|
||||
| `src/index.css` | 增加 `.report-signature-img` 的样式规则(高度 `2.4em`、宽度 `auto`、`vertical-align: middle` 等);增加打印媒体查询中的签名图片样式。 |
|
||||
| `src/utils/print.ts` | 在打印 iframe 的 `<style>` 中增加 `.report-signature-img` 的样式规则。 |
|
||||
|
||||
## 四、验收标准
|
||||
|
||||
- [ ] 鼠标悬浮在 TemplateManage 右侧字段按钮上时,编辑器中对应字段框出现高亮边框/背景变化;移开后恢复。
|
||||
- [ ] UserManage 中可上传电子签图片,上传后预览显示压缩后的图片。
|
||||
- [ ] 压缩后的图片宽/高均不超过 500px,文件体积显著减小。
|
||||
- [ ] TemplateManage 的插入字段列表中出现"图片"分类及"手术者签名"按钮,可正常插入。
|
||||
- [ ] ReportEditor 中,`surgeonSignature` 字段自动显示当前登录用户的电子签图片;无签名时显示"【请上传电子签】"。
|
||||
- [ ] 签名图片在编辑器中高度约 2 行文字,不破坏行高排版;打印输出效果一致。
|
||||
- [ ] `npm run lint` 无编译错误。
|
||||
Reference in New Issue
Block a user