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 && (