fix: 5项交互修复 - 删除图片恢复虚线框、prompt文案统一、移除overflow、多选输入解耦、label加可多选

This commit is contained in:
2026-04-17 21:19:20 +08:00
parent 28b913692c
commit 8f746c25f3
3 changed files with 88 additions and 26 deletions

View File

@@ -60,6 +60,7 @@ export default function ReportEditor() {
});
const [anesthesiaOptions, setAnesthesiaOptions] = useState<string[]>(['全麻', '局麻', '腰麻', '硬膜外麻醉', '静脉麻醉', '吸入麻醉']);
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
const [multiInputText, setMultiInputText] = useState<Record<string, string>>({});
const [touched, setTouched] = useState<Record<string, boolean>>({});
const [formFields, setFormFields] = useState<FormField[]>([]);
const [imagePickerOpen, setImagePickerOpen] = useState(false);
@@ -307,6 +308,8 @@ export default function ReportEditor() {
<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;">插入/点击放置图片</span>
`;
placeholder.style.border = '1px dashed #cbd5e1';
placeholder.style.background = '#f8fafc';
if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
saveDraftToStorage();
} else {
@@ -423,24 +426,23 @@ export default function ReportEditor() {
const insertImage = () => {
editorRef.current?.focus();
const input = prompt('请输入占位符的最大宽度和高度(px),用英文逗号分隔(如: 100,50。留空则默认宽高为 200*200。(提示: 正文一行文字高度约为 20 像素左右)', '');
if (input === null) return;
const parts = input.split(',').map(s => s.trim());
const widthStr = parts[0] || '';
const heightStr = parts[1] || '';
let width = parseInt(widthStr) || 0;
let height = parseInt(heightStr) || 0;
if (!widthStr && !heightStr) {
width = 200;
height = 200;
} else if (widthStr && !heightStr) {
height = 200;
} else if (!widthStr && heightStr) {
width = 200;
let width = 200;
let height = 200;
while (true) {
const input = prompt('请输入占位符的最大宽度和高度(px),用*号分隔(如: 100*50。留空则默认宽高为 200*200。(提示: 正文一行文字高度约为 20 像素左右)', '');
if (input === null) return;
const trimmed = input.trim();
if (trimmed === '') break;
const parts = trimmed.split('*').map(s => s.trim());
if (parts.length === 2 && /^\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) {
width = parseInt(parts[0]) || 0;
height = parseInt(parts[1]) || 0;
break;
}
alert('格式错误,请确保使用 * 分隔两个数字,例如 100*50');
}
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;';
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;';
if (width > 0) styleStr += `width:${width}px;`;
if (height > 0) styleStr += `height:${height}px;`;
@@ -1301,16 +1303,13 @@ export default function ReportEditor() {
return Array.from(new Set(text.split(/[,;;、]/).map(s => s.trim()).filter(Boolean)));
};
const handleMultiChange = (text: string) => {
const handleMultiCommit = (text: string) => {
const values = parseMultiInput(text);
const next = { ...reportData, [field.key]: values };
setReportData(next);
stateRef.current = { ...stateRef.current, reportData: next };
saveDraftToStorage();
};
const handleMultiCommit = (text: string) => {
const values = parseMultiInput(text);
const currentOpts = field.options || multiSelectOptions[field.key] || [];
const newOpts = values.filter(v => !currentOpts.includes(v));
if (newOpts.length > 0) {
@@ -1327,9 +1326,11 @@ export default function ReportEditor() {
}
};
const currentInputText = multiInputText[field.key] !== undefined ? multiInputText[field.key] : displayText;
return (
<div key={field.key} className="space-y-1 select-dropdown-root relative">
<label className="block text-xs font-bold text-text-main">{field.label}</label>
<label className="block text-xs font-bold text-text-main">{field.label}</label>
<div
className="w-full px-3 py-2 border border-border rounded-lg bg-white flex flex-wrap gap-1 items-center min-h-[42px] cursor-text"
onClick={() => setOpenDropdown(field.key)}
@@ -1338,14 +1339,28 @@ export default function ReportEditor() {
type="text"
className="outline-none text-sm w-full bg-transparent"
placeholder="输入或选择,多个用逗号分隔"
value={displayText}
onChange={(e) => handleMultiChange(e.target.value)}
value={currentInputText}
onChange={(e) => {
setMultiInputText(prev => ({ ...prev, [field.key]: e.target.value }));
}}
onFocus={() => setOpenDropdown(field.key)}
onBlur={(e) => handleMultiCommit(e.target.value)}
onBlur={(e) => {
handleMultiCommit(e.target.value);
setMultiInputText(prev => {
const next = { ...prev };
delete next[field.key];
return next;
});
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleMultiCommit((e.target as HTMLInputElement).value);
setMultiInputText(prev => {
const next = { ...prev };
delete next[field.key];
return next;
});
}
}}
/>
@@ -1357,9 +1372,15 @@ export default function ReportEditor() {
key={opt}
className="px-3 py-2 text-sm hover:bg-slate-50 cursor-pointer flex justify-between items-center"
onClick={() => {
const newText = currentValues.length > 0 ? `${displayText}, ${opt}` : opt;
handleMultiChange(newText);
const base = multiInputText[field.key] !== undefined ? multiInputText[field.key] : displayText;
const newText = base.length > 0 ? `${base}, ${opt}` : opt;
setMultiInputText(prev => ({ ...prev, [field.key]: newText }));
handleMultiCommit(newText);
setMultiInputText(prev => {
const next = { ...prev };
delete next[field.key];
return next;
});
}}
>
<span>{opt}</span>

View File

@@ -183,6 +183,8 @@ export default function TemplateManage() {
<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;">插入/点击放置图片</span>
`;
placeholder.style.border = '1px dashed #cbd5e1';
placeholder.style.background = '#f8fafc';
} else {
const range = document.createRange();
range.selectNode(placeholder);

View File

@@ -398,3 +398,42 @@ if ((settings.autoInsertDelay || 0) > 0) {
- 存储层的异常捕获**绝不应静默吞掉**,至少要输出日志,必要时还应弹出用户提示。
- 对于需要存储大量图片的医疗/图文报告系统,应将 `localStorage` 逐步迁移到 `IndexedDB`,从根本上解除 5MB 容量瓶颈。
- 在开发测试阶段,应使用高分辨率视频和大批量关键帧进行压力测试,提前暴露存储容量问题。
---
## 记录 145 项交互修复虚线框恢复、prompt 文案、删除按钮、多选输入、label 提示)
**A. 具体问题**
用户提出 5 个修复需求:
1. 删除 `image-placeholder` 中的图片后,虚线框消失——`fillPlaceholderSrc` 中设置了 `border='none'`,但删除图片的代码没有恢复;
2. `ReportEditor.tsx` 的 `insertImage` prompt 文案仍显示旧版 "用英文逗号分隔",未同步修改;
3. 新生成的 `image-placeholder` 右上角红色 `×` 显示不完全——`ReportEditor.tsx` 的 `insertImage` 中 `overflow:hidden` 未移除;
4. 多选框无法输入 `,`、`;`、``、`` 等分隔符——`onChange` 实时调用 `parseMultiInput` + `filter(Boolean)`,末尾的分隔符被瞬间吃掉;
5. 多选框 label 缺少 "(可多选)" 提示。
**B. 产生问题原因**
1. 上批次修改时,`ReportEditor.tsx` 的 `insertImage` 替换未成功匹配(旧字符串与文件实际内容有微小差异),导致该函数保留了旧代码。删除图片逻辑同样缺少 border/background 恢复。
2. `overflow:hidden` 仅在新版 `TemplateManage.tsx` 中被移除,`ReportEditor.tsx` 中仍保留。
3. 多选框使用受控 `value={displayText}` + `onChange={handleMultiChange}`,每次输入都会触发 `split(/[,;;、]/)` 和 `filter(Boolean)`。当用户输入一个逗号时split 产生空字符串filter 将其过滤,输入框值立即回退到无逗号状态。
4. label 渲染时直接使用 `{field.label}`,未追加多选提示。
**C. 解决问题方案**
1. **恢复虚线框**:在 `ReportEditor.tsx` 和 `TemplateManage.tsx` 的删除图片分支中,增加:
```ts
placeholder.style.border = '1px dashed #cbd5e1';
placeholder.style.background = '#f8fafc';
```
2. **修正 prompt 文案**:将 `ReportEditor.tsx` 的 `insertImage` 重写为与 `TemplateManage.tsx` 一致的新版逻辑(`*` 分隔 + while 循环校验)。
3. **移除 overflow:hidden**:从 `ReportEditor.tsx` 的 `styleStr` 中删除 `overflow:hidden;`。
4. **多选输入解耦**:引入本地状态 `multiInputText: Record<string, string>`
- `onChange` 仅更新 `multiInputText`,不触发拆分;
- `onBlur` 和 `Enter` 时才调用 `handleMultiCommit` 执行 `split` + `filter(Boolean)` + 保存新选项;
- 输入框 `value` 优先读取 `multiInputText[field.key]`,无本地缓存时回退到 `displayText`。
5. **label 追加提示**`{field.label}(可多选)`。
**D. 后续如何避免问题**
- 同类型函数在多个文件中存在时,务必逐个文件 grep 确认修改结果,不能假设一次替换就能覆盖所有实例。
- 任何 "实时解析输入" 的逻辑都必须警惕 `filter(Boolean)` 对空字符串的过滤效应——如果允许用户输入分隔符应使用独立状态缓存原始输入仅在确认时blur/enter执行解析。
- `StrReplaceFile` 的批量替换若返回 "Applied N edit(s) with M total replacement(s)" 且 M < N应立即检查未匹配的文件避免遗漏。