# 实现方案 — 2026-04-18-00-02-08 ## 根因分析 ### 问题1:拖拽插入后边框不消失 - `fillPlaceholderSrc`(点击上传路径)设置了 `border='none'` 和 `background='transparent'`。 - `fillPlaceholder`(拖拽路径)遗漏了这两行样式清除,导致拖拽后虚线框和灰色背景仍然可见。 - 同时 `fillPlaceholder` 中图片 style 缺少 `max-height:100%;object-fit:contain;`,图片可能溢出占位符。 ### 问题2:prompt 弹窗体验差 + 自动帧插入无区分 - `insertImage` 使用浏览器原生 `prompt` 询问宽高,交互体验不佳。 - 所有 `.image-placeholder` 一视同仁,`autoCaptureFrames` 会自动填入任意空占位符。Logo、签名等位置不应被手术关键帧污染。 - 没有机制区分"接受关键帧"和"不接受关键帧"的占位符。 ### 问题3:insertTable 使用 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 = ` × `; 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**(在 `` 关闭之前,与现有 `imagePickerOpen` Modal 并列): ```tsx {placeholderModal.isOpen && (

插入图片占位符

setPlaceholderModal({...placeholderModal, width: e.target.value})} className="w-full px-2 py-1 text-xs border border-border rounded" />
setPlaceholderModal({...placeholderModal, height: e.target.value})} className="w-full px-2 py-1 text-xs border border-border rounded" />
)} ``` **Table Insert Modal**: ```tsx {tableModal.isOpen && (

插入表格

setTableModal({...tableModal, rows: e.target.value})} className="w-full px-2 py-1 text-xs border border-border rounded" />
setTableModal({...tableModal, cols: e.target.value})} className="w-full px-2 py-1 text-xs border border-border rounded" />
)} ``` ### 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` 中的选择器和拦截逻辑。