Files
Mdeical_Sur_Report/工程分析/实现方案-2026-04-17-10-21-18.md
admin db5df13a05 feat: smart field uniqueness, delete button, bulk export in report manage
- TemplateManage: add uniqueness check for smart fields to prevent duplicate inserts
- Add red circular delete button to smart-field-wrapper (visible on hover via CSS)
- Enhance keydown handler to delete smart fields at block-level boundaries
- Update defaultContent.ts smartField() to include delete-btn
- ReportManage: add per-row checkboxes, select-all, bulk delete
- Add single-report export modal (PDF via printDocument, JSON via Blob)
- Add bulk export actions for PDF and JSON
- Update experience record (#16)
2026-04-17 10:32:07 +08:00

5.4 KiB
Raw Blame History

实现方案 — 模板字段唯一性、删除交互与报告批量导出2026-04-17-10-21-18

一、修改文件清单

  1. src/pages/TemplateManage.tsx — 字段唯一性校验 + 删除按钮 + 键盘删除增强
  2. src/utils/defaultContent.tssmartField() 增加删除按钮 HTML
  3. src/index.css — 智能字段删除按钮样式
  4. src/pages/ReportManage.tsx — 复选框、批量操作栏、导出功能

二、详细改动

2.1 src/pages/TemplateManage.tsx

A. insertSmartField 增加唯一性校验

const insertSmartField = (field: FormField) => {
  editorRef.current?.focus();
  if (editorRef.current?.querySelector(`[data-bind="${field.key}"]`)) {
    alert(`字段 "${field.label}" 已存在,请勿重复插入。`);
    return;
  }
  // ... 原有 insertHTML 逻辑,同时给 wrapper 增加 delete-btn
};

B. 给 smart-field-wrapper 增加删除按钮

插入的 HTML 改为:

<span class="smart-field-wrapper" contenteditable="false" style="white-space:nowrap;">
  <span class="delete-btn" contenteditable="false">×</span>
  <span class="field-value" data-bind="..." ...> </span>
</span>

C. 点击删除按钮事件(复用或扩展已有的 handleEditorClick capture 事件)

在已有的 handleEditorClick 中,除了处理 .image-placeholder,再增加对 .delete-btn.smart-field-wrapper 内的判断:

const wrapper = targetEl.closest('.smart-field-wrapper') as HTMLElement | null;
if (targetEl.closest('.delete-btn') && wrapper) {
  e.preventDefault();
  e.stopPropagation();
  wrapper.remove();
  // 同步保存模板内容到 localStorage
  return;
}

D. 增强 keydown 删除逻辑

当前逻辑只处理 "光标在文本节点内且 offset 为 0/末尾" 的情况。需要额外处理:

  • 光标直接在字段后面(range.startContainer<p>range.startOffset 指向字段节点位置)时按 Backspace。
  • 光标直接在字段前面时按 Delete。
  • 选区 collapsed 且紧邻字段的各种边界情况。

实现策略:在 keydown 中统一写一个 findAdjacentSmartField(range, direction) 辅助函数,先尝试从文本节点 sibling 找,若找不到则从父级块节点的 childNodes 中按 offset 找。

2.2 src/utils/defaultContent.ts

修改 smartField(key) 辅助函数,使其输出包含 <span class="delete-btn" contenteditable="false">×</span> 的单行 HTML。

2.3 src/index.css

新增 .smart-field-wrapper .delete-btn 的样式:

.smart-field-wrapper .delete-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  margin-right: 2px;
  background: #ef4444;
  color: white;
  border-radius: 50%;
  font-size: 10px;
  line-height: 1;
  cursor: pointer;
  user-select: none;
}
.smart-field-wrapper .delete-btn:hover {
  background: #dc2626;
}
@media print {
  .smart-field-wrapper .delete-btn {
    display: none !important;
  }
}

2.4 src/pages/ReportManage.tsx

A. 状态扩展

const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [exportModalOpen, setExportModalOpen] = useState(false);
const [exportTarget, setExportTarget] = useState<Report | null>(null);

B. 复选框与全选逻辑

  • 表头 <th> 增加 Checkbox状态为 selectedIds.length === filteredReports.length && filteredReports.length > 0
  • 每行 <td> 最左侧增加 Checkboxchecked 状态为 selectedIds.includes(report.id)onChange 时切换选中状态。
  • selectedIds.length > 0在搜索栏下方显示批量操作栏flex row包含
    • 已选择 N 项 文本
    • 批量删除 按钮(红色)
    • 批量导出 PDF 按钮
    • 批量导出 JSON 按钮
    • 取消选择 按钮

C. 单报告导出

  • PDF:调用 printDocument(report.content)
  • JSON:构建对象:
    const exportData = {
      meta: { id: report.id, title: report.title, createdAt: report.createdAt, updatedAt: report.updatedAt, author: report.author, authorName: report.authorName, status: report.status },
      fields: { /* 所有 DEFAULT_FORM_FIELDS 的 key 对应的值 */ }
    };
    
    通过 Blob + URL.createObjectURL + <a download> 触发下载。

D. 批量导出

  • 批量 PDFconst mergedHTML = selectedReports.map(r => r.content).join('<div style="page-break-after: always;"></div>'); printDocument(mergedHTML);
  • 批量 JSONconst exportData = selectedReports.map(r => ({ meta: ..., fields: ... })); 同样通过 Blob 下载为 reports_export_时间戳.json

E. 批量删除

const handleBulkDelete = () => {
  if (!window.confirm(`确定要删除选中的 ${selectedIds.length} 份报告吗?`)) return;
  const updated = reports.filter(r => !selectedIds.includes(r.id));
  setReports(updated);
  storage.set('reports', updated);
  setSelectedIds([]);
};

三、风险与回滚

  • 风险:修改 defaultContent.ts 中的 smartField HTML 结构后,旧模板中已存在的 smart-field-wrapper 没有删除按钮。这属于正常行为(旧数据不 retroactive新建报告时的新模板会带删除按钮。
  • 风险keydown 删除逻辑改动较大,可能在不同浏览器下对边界 selection 的解析有差异,需要手工测试。
  • 回滚:如出现问题,可回退 TemplateManage.tsxdefaultContent.tsindex.cssReportManage.tsx 的修改。