- 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)
139 lines
5.4 KiB
Markdown
139 lines
5.4 KiB
Markdown
# 实现方案 — 模板字段唯一性、删除交互与报告批量导出(2026-04-17-10-21-18)
|
||
|
||
## 一、修改文件清单
|
||
|
||
1. `src/pages/TemplateManage.tsx` — 字段唯一性校验 + 删除按钮 + 键盘删除增强
|
||
2. `src/utils/defaultContent.ts` — `smartField()` 增加删除按钮 HTML
|
||
3. `src/index.css` — 智能字段删除按钮样式
|
||
4. `src/pages/ReportManage.tsx` — 复选框、批量操作栏、导出功能
|
||
|
||
## 二、详细改动
|
||
|
||
### 2.1 `src/pages/TemplateManage.tsx`
|
||
|
||
#### A. `insertSmartField` 增加唯一性校验
|
||
```ts
|
||
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 改为:
|
||
```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` 内的判断:
|
||
```ts
|
||
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` 的样式:
|
||
```css
|
||
.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. 状态扩展
|
||
```ts
|
||
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>` 最左侧增加 Checkbox,checked 状态为 `selectedIds.includes(report.id)`,onChange 时切换选中状态。
|
||
- 当 `selectedIds.length > 0` 时,在搜索栏下方显示批量操作栏(flex row),包含:
|
||
- `已选择 N 项` 文本
|
||
- **批量删除** 按钮(红色)
|
||
- **批量导出 PDF** 按钮
|
||
- **批量导出 JSON** 按钮
|
||
- **取消选择** 按钮
|
||
|
||
#### C. 单报告导出
|
||
- **PDF**:调用 `printDocument(report.content)`。
|
||
- **JSON**:构建对象:
|
||
```ts
|
||
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. 批量导出
|
||
- **批量 PDF**:`const mergedHTML = selectedReports.map(r => r.content).join('<div style="page-break-after: always;"></div>'); printDocument(mergedHTML);`
|
||
- **批量 JSON**:`const exportData = selectedReports.map(r => ({ meta: ..., fields: ... }));` 同样通过 Blob 下载为 `reports_export_时间戳.json`。
|
||
|
||
#### E. 批量删除
|
||
```ts
|
||
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.tsx`、`defaultContent.ts`、`index.css`、`ReportManage.tsx` 的修改。
|