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

8.4 KiB
Raw Blame History

实现方案 — 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 marginbody padding 调整,.content width 改为 100%

具体代码变更

1. TemplateManage.tsx

1.1 新增状态(组件顶部)

const [formatDropdownOpen, setFormatDropdownOpen] = useState(false);
const [newFormatDropdownOpen, setNewFormatDropdownOpen] = useState(false);

1.2 编辑字段格式输入替换为自定义下拉

将原 input[list] + datalist 替换为:

<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 增加表格检测

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

/* 修改前 */
@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 的修改:恢复原始的 @pagebodycontent 样式。