From 424407a17e96a3b693425a8c3e91dd05436a3ff8 Mon Sep 17 00:00:00 2001
From: admin <572701190@qq.com>
Date: Fri, 17 Apr 2026 12:04:23 +0800
Subject: [PATCH] feat: field hover highlight, e-signature upload, surgeon
signature linkage
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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)
---
src/index.css | 14 ++
src/pages/ReportEditor.tsx | 19 +++
src/pages/TemplateManage.tsx | 18 ++-
src/pages/UserManage.tsx | 85 +++++++++++-
src/types.ts | 4 +-
src/utils/print.ts | 1 +
工程分析/实现方案-2026-04-17-11-34-24.md | 161 +++++++++++++++++++++++
工程分析/测试方案-2026-04-17-11-34-24.md | 49 +++++++
工程分析/经验记录.md | 44 +++++++
工程分析/需求分析-2026-04-17-11-34-24.md | 54 ++++++++
10 files changed, 445 insertions(+), 4 deletions(-)
create mode 100644 工程分析/实现方案-2026-04-17-11-34-24.md
create mode 100644 工程分析/测试方案-2026-04-17-11-34-24.md
create mode 100644 工程分析/需求分析-2026-04-17-11-34-24.md
diff --git a/src/index.css b/src/index.css
index ea0e089..3e87168 100644
--- a/src/index.css
+++ b/src/index.css
@@ -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;
+ }
}
diff --git a/src/pages/ReportEditor.tsx b/src/pages/ReportEditor.tsx
index 5db9d6c..e988411 100644
--- a/src/pages/ReportEditor.tsx
+++ b/src/pages/ReportEditor.tsx
@@ -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 = `
`;
+ 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 || ''}`;
diff --git a/src/pages/TemplateManage.tsx b/src/pages/TemplateManage.tsx
index 29eee79..9a3277e 100644
--- a/src/pages/TemplateManage.tsx
+++ b/src/pages/TemplateManage.tsx
@@ -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() {
{fieldLibTab === 'insert' && (
- {['填空', '单选', '多选', '时间'].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}`}
>
diff --git a/src/pages/UserManage.tsx b/src/pages/UserManage.tsx
index 09af005..6ef68ff 100644
--- a/src/pages/UserManage.tsx
+++ b/src/pages/UserManage.tsx
@@ -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 => {
+ 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) => {
+ 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() {
)}
+
+
+ {formData.signature ? (
+
+

+
+
+
+
+
+ ) : (
+
+
+ 支持 JPG、PNG,自动压缩至 500px 以内
+
+ )}
+
+
{showManageableTemplates && (