Files
Mdeical_Sur_Report/工程分析/实现方案-2026-04-17-23-38-34.md

197 lines
8.4 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-17-23-38-34
## 根因分析
### 问题1原生 datalist 交互体验差
- 原生 `<input list>` + `<datalist>` 在不同浏览器中表现不一致,部分浏览器不会自动展开全部选项,且不支持样式自定义。
- 用户已习惯 `ReportEditor.tsx` 中单选下拉框的交互模式,期望统一体验。
### 问题2execCommand('insertHTML') 在表格中破坏结构
-`insertImage``<td>` 内执行 `execCommand('insertHTML', ...)`WebKit/Blink 会将复杂的 `inline-flex` 嵌套 `<span>` 结构自动"拍平"或重新排列。
- 外层 `<span class="image-placeholder">` 被浏览器移除,仅剩内部的 `.delete-btn``.placeholder-text` 散落为 `<td>` 的直接子元素。
- 表格单元格本身就是块级上下文,使用块级 `<div>` 作为占位符容器更符合浏览器预期,不会被强制修正。
### 问题3@page margin 与 body padding 的分页失效
- `@page { margin: 0 }` 将物理纸张边距设为 0。
- `body { padding: 10mm }` 只在整个 HTML 文档的顶部和底部各生效一次。
- 当内容跨页时,浏览器在分页切断处不会保留 `body` 的 padding导致第二页顶部和底部紧贴纸张边缘。
- 正确做法是将边距交给 `@page` 规则,让打印引擎为每一张物理纸张独立留出边距。
## 修改文件清单
| 文件 | 修改内容 |
|------|---------|
| `src/pages/TemplateManage.tsx` | ① 引入 `formatDropdownOpen` / `newFormatDropdownOpen` 状态;② 将编辑/新增字段的格式 `input[list]+datalist` 替换为自定义下拉组件;③ `insertImage` 增加表格检测,表格内使用 `<div>` 块级容器+自适应尺寸 |
| `src/pages/ReportEditor.tsx` | `insertImage` 增加表格检测,表格内使用 `<div>` 块级容器+自适应尺寸 |
| `src/utils/print.ts` | `@page margin``body padding` 调整,`.content` width 改为 `100%` |
## 具体代码变更
### 1. TemplateManage.tsx
#### 1.1 新增状态(组件顶部)
```tsx
const [formatDropdownOpen, setFormatDropdownOpen] = useState(false);
const [newFormatDropdownOpen, setNewFormatDropdownOpen] = useState(false);
```
#### 1.2 编辑字段格式输入替换为自定义下拉
将原 `input[list]` + `datalist` 替换为:
```tsx
<div className="relative">
<input
type="text"
value={editFieldTimeFormat}
onChange={(e) => setEditFieldTimeFormat(e.target.value)}
onFocus={() => setFormatDropdownOpen(true)}
onBlur={() => {
setTimeout(() => setFormatDropdownOpen(false), 200);
const val = editFieldTimeFormat.trim();
if (val && !customTimeFormats.includes(val)) {
const next = [...customTimeFormats, val];
setCustomTimeFormats(next);
storage.set('customTimeFormats', next);
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
const val = editFieldTimeFormat.trim();
if (val && !customTimeFormats.includes(val)) {
const next = [...customTimeFormats, val];
setCustomTimeFormats(next);
storage.set('customTimeFormats', next);
}
setFormatDropdownOpen(false);
}
}}
className="w-full px-1.5 py-1 text-xs border border-border rounded"
placeholder="输入格式或下拉选择"
/>
{formatDropdownOpen && (
<div className="absolute z-10 left-0 right-0 top-full mt-1 bg-white border border-border rounded shadow-lg max-h-32 overflow-y-auto">
{customTimeFormats
.filter(fmt => {
const isDateFormat = /YYYY|MM|DD/.test(fmt);
const isTimeFormat = /HH|hh|mm|A/.test(fmt);
if (field.type === 'date') return isDateFormat;
if (field.type === 'time') return isTimeFormat;
return true;
})
.map(fmt => (
<div
key={fmt}
className="px-2 py-1 text-xs hover:bg-slate-100 cursor-pointer"
onMouseDown={(e) => {
e.preventDefault();
setEditFieldTimeFormat(fmt);
setFormatDropdownOpen(false);
}}
>
{fmt}
</div>
))}
</div>
)}
</div>
```
#### 1.3 新增字段格式输入同理替换
使用 `newFormatDropdownOpen` 状态,结构同上,过滤条件改为 `newFieldForm.type`
#### 1.4 insertImage 增加表格检测
```tsx
const insertImage = () => {
editorRef.current?.focus();
restoreSelection();
// 检测是否在表格单元格内
const sel = window.getSelection();
let node: Node | null = sel?.anchorNode ?? null;
let inTable = false;
while (node) {
if ((node as Element).nodeName === 'TD' || (node as Element).nodeName === 'TH') {
inTable = true;
break;
}
node = node.parentNode;
}
let width = 200;
let height = 200;
if (!inTable) {
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');
}
}
const id = 'ph_' + Date.now();
const hintText = '插入/点击放置图片';
let html: string;
if (inTable) {
// 表格内使用 div 块级容器,自适应单元格
const styleStr = 'display:flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;cursor:pointer;width:100%;height:100%;max-width:200px;max-height:200px;min-height:60px;margin:0 auto;';
html = `<div 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;">${hintText}</span></div>`;
} else {
// 普通文本中保持行内 span
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;`;
const showShortText = width > 0 && width < 80;
const text = showShortText ? '插图' : hintText;
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;">${text}</span></span>&#8203;`;
}
pushHistory();
execCmd('insertHTML', html);
};
```
### 2. ReportEditor.tsx
`insertImage` 同理增加表格检测分支,与 TemplateManage 保持一致(去除 `restoreSelection()``pushHistory()` 调用)。
### 3. print.ts
```css
/* 修改前 */
@page { size: A4; margin: 0; }
body { margin: 0; padding: 10mm; ... }
.content { width: 190mm; min-height: 277mm; margin: 0 auto; }
/* 修改后 */
@page { size: A4; margin: 15mm 10mm; }
body { margin: 0; padding: 0; ... }
.content { width: 100%; min-height: 277mm; margin: 0 auto; }
```
## 风险点与应对措施
| 风险 | 应对措施 |
|------|---------|
| 自定义下拉组件在滚动容器内可能被裁切 | 父容器设置 `relative`,下拉层设置 `absolute z-10`,并确保外层有适当的 overflow-visible 或足够空间 |
| 表格检测 `while (node)` 循环在编辑器外部可能遍历到 body/html | 以 `node.nodeName === 'TD' \|\| node.nodeName === 'TH'` 为终止条件,安全 |
| 表格内使用 div 后fillPlaceholderSrc 需要兼容 | fillPlaceholderSrc 通过 `querySelector('.image-placeholder')` 匹配 class不受标签名影响已验证兼容 |
| @page margin 增加后 .content width 190mm 会溢出 | 改为 `width: 100%`,让内容自然撑满可用区域 |
## 回滚策略
- TemplateManage.tsx 的修改:删除新增状态和替换的 JSX 条件块,恢复原有的 `input[list]` + `datalist`
- ReportEditor.tsx 的修改:删除 insertImage 中的表格检测分支。
- print.ts 的修改:恢复原始的 `@page``body``content` 样式。