Files
Mdeical_Sur_Report/工程分析/实现方案-2026-04-18-00-02-08.md

257 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 实现方案 — 2026-04-18-00-02-08
## 根因分析
### 问题1拖拽插入后边框不消失
- `fillPlaceholderSrc`(点击上传路径)设置了 `border='none'``background='transparent'`
- `fillPlaceholder`(拖拽路径)遗漏了这两行样式清除,导致拖拽后虚线框和灰色背景仍然可见。
- 同时 `fillPlaceholder` 中图片 style 缺少 `max-height:100%;object-fit:contain;`,图片可能溢出占位符。
### 问题2prompt 弹窗体验差 + 自动帧插入无区分
- `insertImage` 使用浏览器原生 `prompt` 询问宽高,交互体验不佳。
- 所有 `.image-placeholder` 一视同仁,`autoCaptureFrames` 会自动填入任意空占位符。Logo、签名等位置不应被手术关键帧污染。
- 没有机制区分"接受关键帧"和"不接受关键帧"的占位符。
### 问题3insertTable 使用 prompt
- 与 insertImage 同理,原生 `prompt` 弹窗用户体验差,应替换为与项目风格一致的自定义 Modal。
## 修改文件清单
| 文件 | 修改内容 |
|------|---------|
| `src/pages/ReportEditor.tsx` | ① fillPlaceholder 补齐样式清除和图片约束;② insertImage 改为 placeholderModal③ insertTable 改为 tableModal④ autoCaptureFrames/insertFrameToPlaceholder 选择器增加 `:not([data-mode="manual"])`;⑤ handleDrop 拦截 manual 模式;⑥ JSX 底部新增 2 个 Modal |
| `src/pages/TemplateManage.tsx` | ① insertImage 改为 placeholderModal② insertTable 改为 tableModal③ JSX 底部新增 2 个 Modal |
## 具体代码变更
### 1. ReportEditor.tsx
#### 1.1 fillPlaceholder 修复
```ts
const fillPlaceholder = (placeholder: HTMLElement, frame: CapturedFrame) => {
placeholder.innerHTML = `
<span class="delete-btn" contenteditable="false">×</span>
<img src="${frame.dataUrl}" style="max-width:100%;max-height:100%;object-fit:contain;display:block;margin:0 auto;" draggable="false">
`;
placeholder.classList.add('has-image');
placeholder.style.border = 'none';
placeholder.style.background = 'transparent';
if (editorRef.current) contentRef.current = editorRef.current.innerHTML;
saveDraftToStorage();
};
```
#### 1.2 新增状态
```ts
const [placeholderModal, setPlaceholderModal] = useState({
isOpen: false, width: '200', height: '200', mode: 'frame' as 'frame' | 'manual'
});
const [tableModal, setTableModal] = useState({
isOpen: false, rows: '2', cols: '3'
});
```
#### 1.3 insertImage 改为打开 Modal
```ts
const insertImage = () => {
editorRef.current?.focus();
setPlaceholderModal({ isOpen: true, width: '200', height: '200', mode: 'frame' });
};
```
#### 1.4 insertTable 改为打开 Modal
```ts
const insertTable = () => {
editorRef.current?.focus();
setTableModal({ isOpen: true, rows: '2', cols: '3' });
};
```
#### 1.5 autoCaptureFrames 中选择器修改
`setTimeout` 回调内的:
```ts
const emptyPlaceholder = editorRef.current.querySelector('.image-placeholder:not(.has-image)') as HTMLElement | null;
```
改为:
```ts
const emptyPlaceholder = editorRef.current.querySelector('.image-placeholder:not(.has-image):not([data-mode="manual"])') as HTMLElement | null;
```
#### 1.6 insertFrameToPlaceholder 选择器修改
```ts
const emptyPlaceholder = editorRef.current.querySelector('.image-placeholder:not(.has-image):not([data-mode="manual"])') as HTMLElement | null;
```
#### 1.7 handleDrop 拦截 manual 模式
```ts
const handleDrop = (e: React.DragEvent, placeholder: HTMLElement) => {
e.preventDefault();
if (placeholder.getAttribute('data-mode') === 'manual') {
alert('此处为静态图片占位符仅支持点击插入如Logo/签名),不支持拖入关键帧');
return;
}
const frameId = e.dataTransfer.getData('frameId');
const frame = capturedFrames.find(f => f.id.toString() === frameId);
if (frame) {
fillPlaceholder(placeholder, frame);
}
};
```
#### 1.8 JSX 底部新增 Modal
**Placeholder Insert Modal**(在 `</div>` 关闭之前,与现有 `imagePickerOpen` Modal 并列):
```tsx
{placeholderModal.isOpen && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div className="bg-white rounded-2xl p-6 w-full max-w-[360px] shadow-2xl border border-border">
<h3 className="text-lg font-bold text-text-main mb-4"></h3>
<div className="space-y-3">
<div className="flex gap-2">
<div className="flex-1">
<label className="block text-xs mb-1">(px)</label>
<input type="number" value={placeholderModal.width} onChange={e => setPlaceholderModal({...placeholderModal, width: e.target.value})} className="w-full px-2 py-1 text-xs border border-border rounded" />
</div>
<div className="flex-1">
<label className="block text-xs mb-1">(px)</label>
<input type="number" value={placeholderModal.height} onChange={e => setPlaceholderModal({...placeholderModal, height: e.target.value})} className="w-full px-2 py-1 text-xs border border-border rounded" />
</div>
</div>
<div>
<label className="block text-xs mb-1"></label>
<div className="flex gap-2">
<button onClick={() => setPlaceholderModal({...placeholderModal, mode: 'frame'})} className={`flex-1 py-1.5 text-xs rounded border ${placeholderModal.mode === 'frame' ? 'bg-accent text-white border-accent' : 'bg-white text-slate-600 border-border'}`}><br/><span className="text-[10px] opacity-80">/</span></button>
<button onClick={() => setPlaceholderModal({...placeholderModal, mode: 'manual'})} className={`flex-1 py-1.5 text-xs rounded border ${placeholderModal.mode === 'manual' ? 'bg-accent text-white border-accent' : 'bg-white text-slate-600 border-border'}`}><br/><span className="text-[10px] opacity-80"></span></button>
</div>
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<button onClick={() => setPlaceholderModal({...placeholderModal, isOpen: false})} className="px-4 py-2 bg-slate-100 text-slate-600 rounded text-sm"></button>
<button onClick={() => {
const w = parseInt(placeholderModal.width) || 200;
const h = parseInt(placeholderModal.height) || 200;
const modeAttr = placeholderModal.mode === 'manual' ? ' data-mode="manual"' : '';
const showShortText = w > 0 && w < 80;
const text = showShortText ? '插图' : '插入/点击放置图片';
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;';
styleStr += `width:${w}px;height:${h}px;`;
const id = 'ph_' + Date.now();
const html = `<span id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false"${modeAttr} 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;">${text}</span></span>&#8203;`;
execCmd('insertHTML', html);
setPlaceholderModal({...placeholderModal, isOpen: false});
}} className="px-4 py-2 bg-accent text-white rounded text-sm"></button>
</div>
</div>
</div>
)}
```
**Table Insert Modal**
```tsx
{tableModal.isOpen && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
<div className="bg-white rounded-2xl p-6 w-full max-w-[360px] shadow-2xl border border-border">
<h3 className="text-lg font-bold text-text-main mb-4"></h3>
<div className="space-y-3">
<div className="flex gap-2">
<div className="flex-1">
<label className="block text-xs mb-1"></label>
<input type="number" min="1" value={tableModal.rows} onChange={e => setTableModal({...tableModal, rows: e.target.value})} className="w-full px-2 py-1 text-xs border border-border rounded" />
</div>
<div className="flex-1">
<label className="block text-xs mb-1"></label>
<input type="number" min="1" value={tableModal.cols} onChange={e => setTableModal({...tableModal, cols: e.target.value})} className="w-full px-2 py-1 text-xs border border-border rounded" />
</div>
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<button onClick={() => setTableModal({...tableModal, isOpen: false})} className="px-4 py-2 bg-slate-100 text-slate-600 rounded text-sm"></button>
<button onClick={() => {
const rows = parseInt(tableModal.rows);
const cols = parseInt(tableModal.cols);
if (isNaN(rows) || isNaN(cols) || rows < 1 || cols < 1) {
setTableModal({...tableModal, isOpen: false});
return;
}
let table = '<table style="width: 100%; border-collapse: collapse; margin: 16px 0; table-layout: fixed;">';
for (let i = 0; i < rows; i++) {
table += '<tr>';
for (let j = 0; j < cols; j++) {
table += '<td style="padding: 8px; border: 1px solid #e2e8f0; vertical-align: top;">单元格</td>';
}
table += '</tr>';
}
table += '</table><p></p>';
execCmd('insertHTML', table);
setTableModal({...tableModal, isOpen: false});
}} className="px-4 py-2 bg-accent text-white rounded text-sm"></button>
</div>
</div>
</div>
)}
```
### 2. TemplateManage.tsx
结构与 ReportEditor.tsx 类似,但 `insertImage` 的 Modal 中也需要表格检测逻辑(已在上一轮修改中实现)。
#### 2.1 新增状态
```ts
const [placeholderModal, setPlaceholderModal] = useState({
isOpen: false, width: '200', height: '200', mode: 'frame' as 'frame' | 'manual'
});
const [tableModal, setTableModal] = useState({
isOpen: false, rows: '2', cols: '3'
});
```
#### 2.2 insertImage 改为打开 Modal
```ts
const insertImage = () => {
editorRef.current?.focus();
restoreSelection();
setPlaceholderModal({ isOpen: true, width: '200', height: '200', mode: 'frame' });
};
```
#### 2.3 insertTable 改为打开 Modal
```ts
const insertTable = () => {
editorRef.current?.focus();
restoreSelection();
pushHistory();
setTableModal({ isOpen: true, rows: '2', cols: '3' });
};
```
#### 2.4 JSX 底部新增 Modal
与 ReportEditor.tsx 的 Modal 结构一致。TemplateManage.tsx 的 `insertImage` Modal 中,确认按钮需要执行表格检测(沿用上一轮修改的逻辑),然后调用 `execCmd('insertHTML', html)``pushHistory()`
## 风险点与应对措施
| 风险 | 应对措施 |
|------|---------|
| `data-mode="manual"` 的选择器 `:not([data-mode="manual"])` 可能不兼容旧浏览器 | 项目使用 Chrome/Edge完全支持属性选择器 |
| 新增 Modal 与现有 `imagePickerOpen` Modal 的 z-index 冲突 | 两者都使用 `z-50`,在同一时刻不会同时打开 |
| TemplateManage.tsx 的 insertImage 中 pushHistory() 调用位置 | 确认按钮中在 `execCmd` 之前调用 `pushHistory()` |
| 表格内的 insertImage上一轮修改与本次 Modal 的冲突 | 确认按钮中保留表格检测逻辑,在表格内时不使用 Modal 中的宽高值 |
## 回滚策略
- 删除新增的状态和 Modal JSX恢复 `insertImage``insertTable` 中的 `prompt` 弹窗逻辑。
- 恢复 `fillPlaceholder` 到修改前状态。
- 恢复 `autoCaptureFrames``insertFrameToPlaceholder``handleDrop` 中的选择器和拦截逻辑。