3d6305376b
1. PostgREST 使用情况分析文档 - PostgREST使用情况-后端API替代建议.md: 完整的迁移建议和优先级分析 - PostgREST实际使用清单.md: 当前使用的 PostgREST 接口清单 - PostgREST未使用函数清单.md: 已封装但未使用的函数列表 - PostgREST请求模块清单.md: 所有请求模块的使用情况 2. 删除操作延迟确认功能实施文档 - 功能设计和实现细节 - 使用示例和最佳实践 - 技术实现说明 这些文档用于: - 追踪 PostgREST 到 FastAPI 的迁移进度 - 指导后续的接口迁移工作 - 记录 UI 改进的实施细节 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
558 lines
13 KiB
Markdown
558 lines
13 KiB
Markdown
# 删除操作延迟确认功能实施文档
|
|
|
|
> **实施时间**: 2025-11-25
|
|
> **功能描述**: 为所有删除操作添加4秒延迟确认功能,防止误删除操作
|
|
|
|
---
|
|
|
|
## 📋 目录
|
|
|
|
- [功能概述](#功能概述)
|
|
- [技术实现](#技术实现)
|
|
- [已更新的文件](#已更新的文件)
|
|
- [使用示例](#使用示例)
|
|
- [测试验证](#测试验证)
|
|
|
|
---
|
|
|
|
## 功能概述
|
|
|
|
### 需求背景
|
|
|
|
为了防止用户误操作导致数据被删除,所有删除操作都需要:
|
|
1. 显示确认弹窗提示
|
|
2. 确认按钮在4秒倒计时结束后才能点击
|
|
3. 倒计时期间按钮显示剩余秒数
|
|
|
|
### 核心功能
|
|
|
|
- **延迟确认**: 确认按钮在4秒倒计时后才可点击
|
|
- **倒计时显示**: 按钮文本显示 "删除 (4s)" → "删除 (3s)" → ... → "删除"
|
|
- **视觉反馈**: 倒计时期间按钮呈半透明状态且鼠标样式为禁用
|
|
- **统一体验**: 所有删除操作使用相同的确认流程
|
|
|
|
---
|
|
|
|
## 技术实现
|
|
|
|
### 1. MessageModal 组件增强
|
|
|
|
**文件**: `app/components/ui/MessageModal.tsx`
|
|
|
|
#### 新增 Props
|
|
|
|
```typescript
|
|
interface MessageModalProps {
|
|
// ... 现有属性
|
|
// 确认按钮延迟时间(秒)- 用于危险操作(如删除)
|
|
confirmDelay?: number;
|
|
}
|
|
```
|
|
|
|
#### 状态管理
|
|
|
|
```typescript
|
|
const [remainingSeconds, setRemainingSeconds] = useState(confirmDelay);
|
|
```
|
|
|
|
#### 倒计时逻辑
|
|
|
|
```typescript
|
|
useEffect(() => {
|
|
if (isOpen && confirmDelay > 0) {
|
|
setRemainingSeconds(confirmDelay);
|
|
const timer = setInterval(() => {
|
|
setRemainingSeconds((prev) => {
|
|
if (prev <= 1) {
|
|
clearInterval(timer);
|
|
return 0;
|
|
}
|
|
return prev - 1;
|
|
});
|
|
}, 1000);
|
|
return () => clearInterval(timer);
|
|
}
|
|
}, [isOpen, confirmDelay]);
|
|
```
|
|
|
|
#### 按钮渲染
|
|
|
|
```typescript
|
|
<button
|
|
className="message-modal-button primary"
|
|
onClick={handleConfirm}
|
|
disabled={remainingSeconds > 0}
|
|
style={{
|
|
opacity: remainingSeconds > 0 ? 0.5 : 1,
|
|
cursor: remainingSeconds > 0 ? 'not-allowed' : 'pointer'
|
|
}}
|
|
>
|
|
{remainingSeconds > 0 ? `${confirmText} (${remainingSeconds}s)` : confirmText}
|
|
</button>
|
|
```
|
|
|
|
---
|
|
|
|
## 已更新的文件
|
|
|
|
### 1. 文档管理模块 (documents.list.tsx)
|
|
|
|
**文件路径**: `app/routes/documents.list.tsx`
|
|
|
|
#### 更新位置 1: 单个文档删除 (Line 580-596)
|
|
|
|
```typescript
|
|
messageService.show({
|
|
title: "确认删除",
|
|
message: `确定要删除文档"${name}"吗?`,
|
|
type: "warning",
|
|
confirmText: "删除",
|
|
cancelText: "取消",
|
|
confirmDelay: 4, // ✅ 新增
|
|
onConfirm: () => {
|
|
// 删除逻辑
|
|
}
|
|
});
|
|
```
|
|
|
|
#### 更新位置 2: 批量删除文档 (Line 617-642)
|
|
|
|
```typescript
|
|
messageService.show({
|
|
title: "确认批量删除",
|
|
message: `确认删除选中的 ${selectedRowKeys.length} 个文档?`,
|
|
type: "warning",
|
|
confirmText: "删除",
|
|
cancelText: "取消",
|
|
confirmDelay: 4, // ✅ 新增
|
|
onConfirm: () => {
|
|
// 批量删除逻辑
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### 2. 评查点管理模块 (rules.list.tsx)
|
|
|
|
**文件路径**: `app/routes/rules.list.tsx`
|
|
|
|
#### 更新位置 1: 单个评查点删除 (Line 526-542)
|
|
|
|
```typescript
|
|
messageService.show({
|
|
title: "确认删除",
|
|
message: `确认删除评查点【${rule.name}】吗?`,
|
|
type: "warning",
|
|
confirmText: "删除",
|
|
cancelText: "取消",
|
|
confirmDelay: 4, // ✅ 新增
|
|
onConfirm: () => {
|
|
// 删除逻辑
|
|
}
|
|
});
|
|
```
|
|
|
|
#### 更新位置 2: 批量删除评查点 (Line 603-634)
|
|
|
|
```typescript
|
|
messageService.show({
|
|
title: "确认批量删除",
|
|
message: `确定要删除选中的 ${selectedIds.length} 个评查点吗?此操作不可恢复。`,
|
|
type: "warning",
|
|
confirmText: "删除",
|
|
cancelText: "取消",
|
|
confirmDelay: 4, // ✅ 新增
|
|
onConfirm: async () => {
|
|
// 批量删除逻辑
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### 3. 评查点分组管理模块 (rule-groups._index.tsx)
|
|
|
|
**文件路径**: `app/routes/rule-groups._index.tsx`
|
|
|
|
#### 导入更新
|
|
|
|
```typescript
|
|
// 原代码
|
|
import { toastService } from "~/components/ui";
|
|
|
|
// 新代码
|
|
import { toastService, messageService } from "~/components/ui";
|
|
```
|
|
|
|
#### 更新位置 1: 单个分组删除 (Line 233-275)
|
|
|
|
**原代码** (使用 `confirm()`):
|
|
```typescript
|
|
if (confirm("确定要删除该分组吗?此操作将同时删除该分组下的所有评查点,且不可恢复。")) {
|
|
// 删除逻辑
|
|
}
|
|
```
|
|
|
|
**新代码** (使用 `messageService`):
|
|
```typescript
|
|
messageService.show({
|
|
title: "确认删除",
|
|
message: "确定要删除该分组吗?此操作将同时删除该分组下的所有评查点,且不可恢复。",
|
|
type: "warning",
|
|
confirmText: "删除",
|
|
cancelText: "取消",
|
|
confirmDelay: 4, // ✅ 新增
|
|
onConfirm: async () => {
|
|
// 删除逻辑
|
|
}
|
|
});
|
|
```
|
|
|
|
#### 更新位置 2: 批量删除分组 (Line 307-328)
|
|
|
|
**原代码** (使用 `confirm()`):
|
|
```typescript
|
|
if (!confirm(`确定要删除选中的 ${selectedIds.length} 个分组吗?此操作不可恢复。`)) {
|
|
return;
|
|
}
|
|
```
|
|
|
|
**新代码** (使用 `messageService`):
|
|
```typescript
|
|
messageService.show({
|
|
title: "确认批量删除",
|
|
message: `确定要删除选中的 ${selectedIds.length} 个分组吗?此操作不可恢复。`,
|
|
type: "warning",
|
|
confirmText: "删除",
|
|
cancelText: "取消",
|
|
confirmDelay: 4, // ✅ 新增
|
|
onConfirm: async () => {
|
|
// 删除逻辑
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### 4. 提示词模板管理模块 (prompts._index.tsx)
|
|
|
|
**文件路径**: `app/routes/prompts._index.tsx`
|
|
|
|
#### 导入更新
|
|
|
|
```typescript
|
|
// 原代码
|
|
import { toastService } from "~/components/ui";
|
|
|
|
// 新代码
|
|
import { toastService, messageService } from "~/components/ui";
|
|
```
|
|
|
|
#### 更新位置: 删除模板 (Line 220-234)
|
|
|
|
**原代码** (使用 `confirm()`):
|
|
```typescript
|
|
if (confirm('确定要删除该模板吗?删除后无法恢复。')) {
|
|
const formData = new FormData();
|
|
formData.append('id', id);
|
|
formData.append('intent', 'delete');
|
|
fetcher.submit(formData, { method: 'post' });
|
|
}
|
|
```
|
|
|
|
**新代码** (使用 `messageService`):
|
|
```typescript
|
|
messageService.show({
|
|
title: "确认删除",
|
|
message: "确定要删除该模板吗?删除后无法恢复。",
|
|
type: "warning",
|
|
confirmText: "删除",
|
|
cancelText: "取消",
|
|
confirmDelay: 4, // ✅ 新增
|
|
onConfirm: () => {
|
|
const formData = new FormData();
|
|
formData.append('id', id);
|
|
formData.append('intent', 'delete');
|
|
fetcher.submit(formData, { method: 'post' });
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### 5. 入口模块管理模块 (entry-modules._index.tsx)
|
|
|
|
**文件路径**: `app/routes/entry-modules._index.tsx`
|
|
|
|
#### 导入更新
|
|
|
|
```typescript
|
|
// 原代码
|
|
import { toastService } from "~/components/ui/Toast";
|
|
|
|
// 新代码
|
|
import { toastService } from "~/components/ui/Toast";
|
|
import { messageService } from "~/components/ui/MessageModal";
|
|
```
|
|
|
|
#### 更新位置: 删除入口模块 (Line 215-250)
|
|
|
|
**原代码** (使用 `confirm()`):
|
|
```typescript
|
|
if (confirm('确定要删除该入口模块吗?此操作不可撤销。')) {
|
|
setIsDeleting(true);
|
|
// 删除逻辑
|
|
}
|
|
```
|
|
|
|
**新代码** (使用 `messageService`):
|
|
```typescript
|
|
messageService.show({
|
|
title: "确认删除",
|
|
message: "确定要删除该入口模块吗?此操作不可撤销。",
|
|
type: "warning",
|
|
confirmText: "删除",
|
|
cancelText: "取消",
|
|
confirmDelay: 4, // ✅ 新增
|
|
onConfirm: async () => {
|
|
setIsDeleting(true);
|
|
// 删除逻辑
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
### 6. 文档类型管理模块 (document-types._index.tsx)
|
|
|
|
**文件路径**: `app/routes/document-types._index.tsx`
|
|
|
|
#### 导入更新
|
|
|
|
```typescript
|
|
// 原代码
|
|
import { toastService } from "~/components/ui/Toast";
|
|
|
|
// 新代码
|
|
import { toastService } from "~/components/ui/Toast";
|
|
import { messageService } from "~/components/ui/MessageModal";
|
|
```
|
|
|
|
#### 更新位置: 删除文档类型 (Line 204-239)
|
|
|
|
**原代码** (使用 `confirm()` 和 `alert()`):
|
|
```typescript
|
|
if (confirm('确定要删除该文档类型吗?此操作不会影响关联的评查点分组,但可能会影响使用该类型的文档评查。')) {
|
|
setIsDeleting(true);
|
|
// 删除逻辑
|
|
if (result.success) {
|
|
alert('删除成功!');
|
|
} else {
|
|
alert(`删除失败: ${result.error || '未知错误'}`);
|
|
}
|
|
}
|
|
```
|
|
|
|
**新代码** (使用 `messageService` 和 `toastService`):
|
|
```typescript
|
|
messageService.show({
|
|
title: "确认删除",
|
|
message: "确定要删除该文档类型吗?此操作不会影响关联的评查点分组,但可能会影响使用该类型的文档评查。",
|
|
type: "warning",
|
|
confirmText: "删除",
|
|
cancelText: "取消",
|
|
confirmDelay: 4, // ✅ 新增
|
|
onConfirm: async () => {
|
|
setIsDeleting(true);
|
|
// 删除逻辑
|
|
if (result.success) {
|
|
toastService.success('删除成功!');
|
|
} else {
|
|
toastService.error(`删除失败: ${result.error || '未知错误'}`);
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 使用示例
|
|
|
|
### 基本用法
|
|
|
|
```typescript
|
|
import { messageService } from "~/components/ui/MessageModal";
|
|
|
|
const handleDelete = (id: string, name: string) => {
|
|
messageService.show({
|
|
title: "确认删除",
|
|
message: `确定要删除"${name}"吗?`,
|
|
type: "warning",
|
|
confirmText: "删除",
|
|
cancelText: "取消",
|
|
confirmDelay: 4, // 4秒延迟
|
|
onConfirm: () => {
|
|
// 执行删除操作
|
|
deleteItem(id);
|
|
}
|
|
});
|
|
};
|
|
```
|
|
|
|
### 批量删除示例
|
|
|
|
```typescript
|
|
const handleBatchDelete = () => {
|
|
if (selectedIds.length === 0) {
|
|
toastService.error('请至少选择一项');
|
|
return;
|
|
}
|
|
|
|
messageService.show({
|
|
title: "确认批量删除",
|
|
message: `确定要删除选中的 ${selectedIds.length} 项吗?此操作不可恢复。`,
|
|
type: "warning",
|
|
confirmText: "删除",
|
|
cancelText: "取消",
|
|
confirmDelay: 4,
|
|
onConfirm: async () => {
|
|
// 执行批量删除
|
|
await batchDeleteItems(selectedIds);
|
|
}
|
|
});
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 测试验证
|
|
|
|
### 测试清单
|
|
|
|
#### 1. 单个删除操作测试
|
|
|
|
- [ ] 文档删除 (`/documents`)
|
|
- [ ] 评查点删除 (`/rules`)
|
|
- [ ] 评查点分组删除 (`/rule-groups`)
|
|
- [ ] 提示词模板删除 (`/prompts`)
|
|
- [ ] 入口模块删除 (`/entry-modules`)
|
|
- [ ] 文档类型删除 (`/document-types`)
|
|
|
|
#### 2. 批量删除操作测试
|
|
|
|
- [ ] 批量删除文档
|
|
- [ ] 批量删除评查点
|
|
- [ ] 批量删除评查点分组
|
|
|
|
### 测试要点
|
|
|
|
1. **倒计时功能**:
|
|
- ✅ 弹窗打开后,确认按钮显示 "删除 (4s)"
|
|
- ✅ 每秒递减: "删除 (3s)" → "删除 (2s)" → "删除 (1s)" → "删除"
|
|
- ✅ 倒计时期间按钮不可点击
|
|
|
|
2. **视觉反馈**:
|
|
- ✅ 倒计时期间按钮半透明 (opacity: 0.5)
|
|
- ✅ 鼠标悬停显示禁用光标 (cursor: not-allowed)
|
|
- ✅ 倒计时结束后按钮恢复正常样式
|
|
|
|
3. **交互行为**:
|
|
- ✅ 点击取消按钮可以立即关闭弹窗
|
|
- ✅ 点击遮罩层可以立即关闭弹窗
|
|
- ✅ 按 ESC 键可以立即关闭弹窗
|
|
- ✅ 倒计时结束后点击确认执行删除操作
|
|
|
|
4. **边界情况**:
|
|
- ✅ 快速打开/关闭弹窗不会导致计时器泄漏
|
|
- ✅ 多次打开弹窗,倒计时每次都重新开始
|
|
|
|
### 预期行为
|
|
|
|
**正常流程**:
|
|
```
|
|
用户点击删除 → 弹窗显示 → 确认按钮显示 "删除 (4s)"
|
|
→ 倒计时 3秒 → 倒计时 2秒 → 倒计时 1秒 → "删除" 可点击
|
|
→ 用户点击确认 → 执行删除 → 显示成功提示
|
|
```
|
|
|
|
**取消流程**:
|
|
```
|
|
用户点击删除 → 弹窗显示 → 倒计时进行中
|
|
→ 用户点击取消/遮罩层/ESC键 → 弹窗关闭 → 不执行删除
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 统计总结
|
|
|
|
### 更新统计
|
|
|
|
| 模块 | 文件名 | 删除操作数 | 导入变更 | 替换 confirm() |
|
|
|------|--------|------------|----------|----------------|
|
|
| 文档管理 | documents.list.tsx | 2 | - | - |
|
|
| 评查点管理 | rules.list.tsx | 2 | - | - |
|
|
| 评查点分组 | rule-groups._index.tsx | 2 | ✅ | ✅ |
|
|
| 提示词模板 | prompts._index.tsx | 1 | ✅ | ✅ |
|
|
| 入口模块 | entry-modules._index.tsx | 1 | ✅ | ✅ |
|
|
| 文档类型 | document-types._index.tsx | 1 | ✅ | ✅ |
|
|
| **总计** | **6 个文件** | **9 个操作** | **4 个文件** | **4 个文件** |
|
|
|
|
### 代码改动
|
|
|
|
- **新增代码**: MessageModal 组件增强 (~50 行)
|
|
- **修改代码**: 9 个删除操作函数 (~200 行)
|
|
- **导入变更**: 4 个文件添加 messageService 导入
|
|
- **替换操作**: 4 个文件从 `confirm()` 迁移到 `messageService.show()`
|
|
|
|
---
|
|
|
|
## 🎯 后续维护
|
|
|
|
### 新增删除操作开发规范
|
|
|
|
当开发人员需要添加新的删除功能时,请遵循以下规范:
|
|
|
|
```typescript
|
|
// ✅ 正确做法
|
|
import { messageService } from "~/components/ui/MessageModal";
|
|
|
|
const handleDelete = (id: string) => {
|
|
messageService.show({
|
|
title: "确认删除",
|
|
message: "确定要删除该项吗?",
|
|
type: "warning",
|
|
confirmText: "删除",
|
|
cancelText: "取消",
|
|
confirmDelay: 4, // 必须添加
|
|
onConfirm: () => {
|
|
// 删除逻辑
|
|
}
|
|
});
|
|
};
|
|
|
|
// ❌ 错误做法
|
|
const handleDelete = (id: string) => {
|
|
if (confirm("确定要删除吗?")) { // 不要使用原生 confirm
|
|
// 删除逻辑
|
|
}
|
|
};
|
|
```
|
|
|
|
### Code Review 检查点
|
|
|
|
在代码审查时,请确认:
|
|
1. ✅ 所有删除操作都使用 `messageService.show()`
|
|
2. ✅ 所有删除确认都包含 `confirmDelay: 4`
|
|
3. ✅ 没有使用原生 `confirm()` 或 `alert()`
|
|
4. ✅ 导入了正确的 `messageService`
|
|
|
|
---
|
|
|
|
## 📞 联系支持
|
|
|
|
如遇到问题,请联系开发团队。
|
|
|
|
**文档维护人**: Claude Code
|
|
**最后更新**: 2025-11-25
|