feat: 5项UX优化 - 合并占位符prompt、默认200x200尺寸、系统默认设置、签名状态列、素材预加载前置

This commit is contained in:
2026-04-17 19:59:19 +08:00
parent 5fee3352c1
commit ee1ac0d637
5 changed files with 117 additions and 18 deletions

View File

@@ -46,6 +46,21 @@ export default function Login() {
storage.set('formFieldsConfig', DEFAULT_FORM_FIELDS); storage.set('formFieldsConfig', DEFAULT_FORM_FIELDS);
} }
const savedAssets = storage.get<{id: string; name: string; dataUrl: string}[]>('imageAssets', []);
if (savedAssets.length === 0) {
fetch('/logo_square.png')
.then(res => res.blob())
.then(blob => {
const reader = new FileReader();
reader.onloadend = () => {
const dataUrl = reader.result as string;
storage.set('imageAssets', [{ id: 'asset_logo', name: '医院Logo', dataUrl }]);
};
reader.readAsDataURL(blob);
})
.catch(() => {});
}
const settingsRaw = storage.get<SystemSettings>('systemSettings', {} as SystemSettings); const settingsRaw = storage.get<SystemSettings>('systemSettings', {} as SystemSettings);
if (!settingsRaw.frameCount) { if (!settingsRaw.frameCount) {
const round1 = (n: number) => Math.round(n * 10) / 10; const round1 = (n: number) => Math.round(n * 10) / 10;
@@ -59,7 +74,10 @@ export default function Login() {
apiEndpoint: '', apiEndpoint: '',
apiKey: '', apiKey: '',
defaultTemplate: savedTemplates[0]?.id || '', defaultTemplate: savedTemplates[0]?.id || '',
frameMode: 'uniform' frameMode: 'uniform',
autoInsertFrames: true,
autoInsertDelay: 1,
autoInsertFrameIndices: [0, 1, 2, 3, 4, 5]
}; };
storage.set('systemSettings', defaultSettings); storage.set('systemSettings', defaultSettings);
} }

View File

@@ -422,15 +422,26 @@ export default function ReportEditor() {
const insertImage = () => { const insertImage = () => {
editorRef.current?.focus(); editorRef.current?.focus();
const widthStr = prompt('请输入占位符最大宽度 (px)留空无限制:\n(提示正文一行文字高度约为 20 像素左右)', ''); const input = prompt('请输入占位符最大宽度和高度(px)用英文逗号分隔(如: 100,50。留空则默认宽高为 200*200。(提示: 正文一行文字高度约为 20 像素左右)', '');
const heightStr = prompt('请输入占位符最大高度 (px),留空无限制:', ''); if (input === null) return;
const width = parseInt(widthStr || '0'); const parts = input.split(',').map(s => s.trim());
const height = parseInt(heightStr || '0'); const widthStr = parts[0] || '';
const heightStr = parts[1] || '';
let styleStr = 'display:inline-flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;'; let width = parseInt(widthStr) || 0;
if (width > 0) styleStr += ` max-width:${width}px;`; let height = parseInt(heightStr) || 0;
if (height > 0) styleStr += ` max-height:${height}px;`; if (!widthStr && !heightStr) {
if (!width && !height) styleStr += ' padding:8px 16px;'; width = 200;
height = 200;
} else if (widthStr && !heightStr) {
height = 200;
} else if (!widthStr && heightStr) {
width = 200;
}
let styleStr = 'display:inline-flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;overflow:hidden;';
if (width > 0) styleStr += `width:${width}px;`;
if (height > 0) styleStr += `height:${height}px;`;
const showShortText = width > 0 && width < 80; const showShortText = width > 0 && width < 80;
const hintText = showShortText ? '插入图片' : '插入/点击放置图片'; const hintText = showShortText ? '插入图片' : '插入/点击放置图片';

View File

@@ -486,18 +486,29 @@ export default function TemplateManage() {
const insertImage = () => { const insertImage = () => {
editorRef.current?.focus(); editorRef.current?.focus();
restoreSelection(); restoreSelection();
const widthStr = prompt('请输入占位符最大宽度 (px)留空无限制:\n(提示正文一行文字高度约为 20 像素左右)', ''); const input = prompt('请输入占位符最大宽度和高度(px)用英文逗号分隔(如: 100,50。留空则默认宽高为 200*200。(提示: 正文一行文字高度约为 20 像素左右)', '');
const heightStr = prompt('请输入占位符最大高度 (px),留空无限制:', ''); if (input === null) return;
const width = parseInt(widthStr || '0'); const parts = input.split(',').map(s => s.trim());
const height = parseInt(heightStr || '0'); const widthStr = parts[0] || '';
const heightStr = parts[1] || '';
let styleStr = 'display:inline-flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;'; let width = parseInt(widthStr) || 0;
if (width > 0) styleStr += ` max-width:${width}px;`; let height = parseInt(heightStr) || 0;
if (height > 0) styleStr += ` max-height:${height}px;`; if (!widthStr && !heightStr) {
if (!width && !height) styleStr += ' padding:8px 16px;'; width = 200;
height = 200;
} else if (widthStr && !heightStr) {
height = 200;
} else if (!widthStr && heightStr) {
width = 200;
}
let styleStr = 'display:inline-flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;overflow:hidden;';
if (width > 0) styleStr += `width:${width}px;`;
if (height > 0) styleStr += `height:${height}px;`;
const showShortText = width > 0 && width < 80; const showShortText = width > 0 && width < 80;
const hintText = showShortText ? '插入图片' : '插入/点击放置图片'; const hintText = showShortText ? '插' : '插入/点击放置图片';
const id = 'ph_' + Date.now(); const id = 'ph_' + Date.now();
const html = `<span id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false" style="${styleStr}"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">${hintText}</span></span>&#8203;`; const html = `<span id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false" style="${styleStr}"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">${hintText}</span></span>&#8203;`;

View File

@@ -416,6 +416,7 @@ export default function UserManage() {
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th> <th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th>
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th> <th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th>
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th> <th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th>
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th>
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th> <th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th>
<th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th> <th className="px-6 py-4 text-left text-[11px] font-bold text-text-muted uppercase tracking-wider border-b border-border"></th>
</tr> </tr>
@@ -437,6 +438,13 @@ export default function UserManage() {
</span> </span>
</td> </td>
<td className="px-6 py-4 text-sm text-text-main">{user.department || '-'}</td> <td className="px-6 py-4 text-sm text-text-main">{user.department || '-'}</td>
<td className="px-6 py-4">
<span className={`inline-block px-2.5 py-1 rounded-full text-[11px] font-bold ${
user.signature ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-500'
}`}>
{user.signature ? '已上传' : '未上传'}
</span>
</td>
<td className="px-6 py-4"> <td className="px-6 py-4">
<span className={`inline-block px-2.5 py-1 rounded-full text-[11px] font-bold ${ <span className={`inline-block px-2.5 py-1 rounded-full text-[11px] font-bold ${
user.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700' user.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'

View File

@@ -236,6 +236,57 @@ if ((settings.autoInsertDelay || 0) > 0) {
-`setTimeout` 等异步回调中操作 DOM 时,应在回调触发时"实时查询"目标元素,而不是在循环中提前捕获元素引用,以防 DOM 在延迟期间已被用户修改。 -`setTimeout` 等异步回调中操作 DOM 时,应在回调触发时"实时查询"目标元素,而不是在循环中提前捕获元素引用,以防 DOM 在延迟期间已被用户修改。
---
## 记录 125 项交互与默认值优化(占位符尺寸、签名状态、素材预加载)
**A. 具体问题**
用户提出 5 个 UI/UX 改进需求:
1. 插入图片占位符时两次 `prompt` 弹窗合并为一次,用英文逗号分隔宽高;
2. 占位符未指定尺寸时默认显示为 `200×200px`,且样式直接使用 `width`/`height` 而非 `max-width`/`max-height`
3. 系统重置后的默认设置中增加 `autoInsertFrames: true``autoInsertDelay: 1``autoInsertFrameIndices: [0,1,2,3,4,5]`
4. 用户管理表格在「部门」与「状态」之间新增「签名状态」列,根据 `user.signature` 显示「已上传」/「未上传」;
5. 修复系统重置后 `ReportEditor` 的素材库为空的问题,将 logo 预加载逻辑从 `TemplateManage.tsx` 前置到 `Login.tsx``initData()` 中。
**B. 产生问题原因**
1. `insertImage()` 在两个编辑器(`TemplateManage``ReportEditor`)中均使用两次独立的 `prompt()`,操作冗余且中断感强。
2. 旧占位符样式使用 `max-width`/`max-height`,当内容区域大于占位符时,边框和背景不会收缩到指定尺寸,视觉尺寸不可控;且未指定时的 `padding:8px 16px` 导致占位符尺寸随文字变化,不统一。
3. `Login.tsx` 初始化 `systemSettings` 时遗漏了自动帧插入相关的 3 个字段,导致新系统首次进入 `/system-settings` 时相关开关为空。
4. `UserManage.tsx` 表格缺少签名可视化列,管理员无法一眼辨别哪些医生已上传电子签名。
5. `imageAssets` 的预加载仅在 `TemplateManage.tsx``useEffect` 中执行。若用户首次登录后直接进入 `ReportEditor`,素材库为空,图片选择器无法使用系统默认 logo。
**C. 解决问题方案**
1. **合并 prompt**
```ts
const input = prompt('请输入占位符的最大宽度和高度(px),用英文逗号分隔(如: 100,50。留空则默认宽高为 200*200。(提示: 正文一行文字高度约为 20 像素左右)', '');
const parts = input.split(',').map(s => s.trim());
```
按逗号分割,第一部分为宽度,第二部分为高度。留空或单侧留空时,另一侧自动回退到 `200`。
2. **固定尺寸样式**
- 移除 `max-width`/`max-height`,改用 `width:${width}px;` / `height:${height}px;`。
- 默认值逻辑:`!widthStr && !heightStr` → `200×200``widthStr && !heightStr` → 宽自定义、高 `200``!widthStr && heightStr` → 宽 `200`、高自定义。
3. **默认设置补全**:在 `Login.tsx` 的 `defaultSettings` 中显式加入:
```ts
autoInsertFrames: true,
autoInsertDelay: 1,
autoInsertFrameIndices: [0, 1, 2, 3, 4, 5]
```
4. **签名状态列**:在 `UserManage.tsx` 表格的 `<th>` 和 `<td>` 中,于「部门」之后、「状态」之前插入:
```tsx
<span className={`inline-block px-2.5 py-1 rounded-full text-[11px] font-bold ${
user.signature ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-500'
}`}>
{user.signature ? '已上传' : '未上传'}
</span>
```
5. **素材预加载前置**:将 `fetch('/logo_square.png') → FileReader → storage.set('imageAssets', [...])` 的逻辑从 `TemplateManage.tsx` 迁移到 `Login.tsx` 的 `initData()` 中,并增加 `savedAssets.length === 0` 的判空保护,避免覆盖用户后续上传的素材。
**D. 后续如何避免问题**
- 对于成对的数值输入(如宽高、行列),优先考虑单输入框 + 分隔符,减少弹窗次数;同时做好格式解析和容错(空值、单侧空值、非数字)。
- 使用 `width`/`height` 代替 `max-width`/`max-height` 能确保占位符尺寸严格可控,避免 `inline-flex` 内容撑大容器。
- 任何需要在多个页面共享的初始化数据(如素材库、默认配置),应放在全局初始化入口(如登录页的 `initData`),而不是分散在各个页面的 `useEffect` 中。
- 表格字段变更时,注意保持 `<thead>` 与 `<tbody>` 的列顺序严格一致,避免列错位。
--- ---
## 记录 11关键帧在路由切换后丢失——压缩 Canvas 分辨率并增加存储错误日志 ## 记录 11关键帧在路由切换后丢失——压缩 Canvas 分辨率并增加存储错误日志