322 lines
6.9 KiB
Markdown
322 lines
6.9 KiB
Markdown
# 处方组件重复请求修复
|
||
|
||
## 问题描述
|
||
|
||
在预约列表页面点击"开处方"或"查看病历"按钮时,会出现重复请求 `/tcm.prescription/detail` 接口的问题,导致错误。
|
||
|
||
## 问题原因
|
||
|
||
### 1. 快速连续点击
|
||
|
||
用户可能快速连续点击按钮,导致 `open()` 或 `openById()` 方法被多次调用。
|
||
|
||
### 2. 异步操作未加锁
|
||
|
||
`open()` 和 `openById()` 方法是异步的,但没有加锁机制,导致:
|
||
- 第一次点击开始执行
|
||
- 第二次点击也开始执行
|
||
- 两次请求同时发送
|
||
|
||
### 3. 代码流程
|
||
|
||
```javascript
|
||
const open = async (data: any) => {
|
||
// 没有防重复检查
|
||
await loadDictOptions()
|
||
|
||
if (appointmentId) {
|
||
const existing = await prescriptionGetByAppointment(...) // 请求1
|
||
if (existing?.id) {
|
||
const detail = await prescriptionDetail(...) // 请求2
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 解决方案
|
||
|
||
### 添加防重复打开标志
|
||
|
||
使用 `isOpening` 标志来防止重复调用:
|
||
|
||
```typescript
|
||
// 防止重复打开
|
||
const isOpening = ref(false)
|
||
|
||
const open = async (data: any, options?: { templateId?: number; stationName?: string }) => {
|
||
// 防止重复打开
|
||
if (isOpening.value) {
|
||
console.warn('处方正在打开中,请勿重复点击')
|
||
return
|
||
}
|
||
|
||
isOpening.value = true
|
||
|
||
try {
|
||
// 原有逻辑...
|
||
} finally {
|
||
// 延迟重置,避免快速连续点击
|
||
setTimeout(() => {
|
||
isOpening.value = false
|
||
}, 500)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 关键点
|
||
|
||
1. **检查标志**:方法开始时检查 `isOpening.value`,如果为 true 则直接返回
|
||
2. **设置标志**:开始执行时设置 `isOpening.value = true`
|
||
3. **finally 重置**:无论成功还是失败,都在 finally 中重置标志
|
||
4. **延迟重置**:使用 `setTimeout` 延迟 500ms 重置,防止快速连续点击
|
||
|
||
## 修改的文件
|
||
|
||
**文件**:`admin/src/components/tcm-prescription/index.vue`
|
||
|
||
### 1. 添加防重复标志
|
||
|
||
```typescript
|
||
// 防止重复打开
|
||
const isOpening = ref(false)
|
||
```
|
||
|
||
### 2. 修改 open 方法
|
||
|
||
```typescript
|
||
const open = async (data: any, options?: { templateId?: number; stationName?: string }) => {
|
||
// 防止重复打开
|
||
if (isOpening.value) {
|
||
console.warn('处方正在打开中,请勿重复点击')
|
||
return
|
||
}
|
||
|
||
isOpening.value = true
|
||
|
||
try {
|
||
// 原有逻辑...
|
||
} finally {
|
||
setTimeout(() => {
|
||
isOpening.value = false
|
||
}, 500)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. 修改 openById 方法
|
||
|
||
```typescript
|
||
const openById = async (prescriptionId: number) => {
|
||
// 防止重复打开
|
||
if (isOpening.value) {
|
||
console.warn('处方正在打开中,请勿重复点击')
|
||
return
|
||
}
|
||
|
||
isOpening.value = true
|
||
|
||
try {
|
||
const res = await prescriptionDetail({ id: prescriptionId })
|
||
savedPrescription.value = res
|
||
if (res?.case_record && Object.keys(res.case_record).length > 0) {
|
||
await loadDictOptions()
|
||
}
|
||
visible.value = true
|
||
} catch (e) {
|
||
feedback.msgError('加载处方失败')
|
||
} finally {
|
||
setTimeout(() => {
|
||
isOpening.value = false
|
||
}, 500)
|
||
}
|
||
}
|
||
```
|
||
|
||
## 工作流程
|
||
|
||
### 修复前
|
||
|
||
```
|
||
用户点击"开处方"按钮
|
||
↓
|
||
调用 open() 方法
|
||
↓
|
||
用户再次点击(快速连续点击)
|
||
↓
|
||
再次调用 open() 方法
|
||
↓
|
||
两次请求同时发送
|
||
↓
|
||
❌ 重复请求错误
|
||
```
|
||
|
||
### 修复后
|
||
|
||
```
|
||
用户点击"开处方"按钮
|
||
↓
|
||
调用 open() 方法
|
||
↓
|
||
设置 isOpening = true
|
||
↓
|
||
用户再次点击(快速连续点击)
|
||
↓
|
||
检查 isOpening = true
|
||
↓
|
||
✓ 直接返回,不执行
|
||
↓
|
||
第一次请求完成
|
||
↓
|
||
500ms 后重置 isOpening = false
|
||
```
|
||
|
||
## 测试场景
|
||
|
||
### 场景1:快速连续点击
|
||
|
||
**操作**:快速连续点击"开处方"按钮 2-3 次
|
||
|
||
**预期结果**:
|
||
- 只发送一次请求
|
||
- 控制台显示警告:"处方正在打开中,请勿重复点击"
|
||
- 处方正常打开
|
||
|
||
---
|
||
|
||
### 场景2:正常点击
|
||
|
||
**操作**:正常点击"开处方"按钮
|
||
|
||
**预期结果**:
|
||
- 发送请求
|
||
- 处方正常打开
|
||
- 无警告信息
|
||
|
||
---
|
||
|
||
### 场景3:间隔点击
|
||
|
||
**操作**:点击"开处方",等待 1 秒后再次点击
|
||
|
||
**预期结果**:
|
||
- 第一次点击正常打开
|
||
- 第二次点击也正常打开(因为已过 500ms)
|
||
|
||
---
|
||
|
||
### 场景4:查看已有处方
|
||
|
||
**操作**:点击"查看病历"按钮
|
||
|
||
**预期结果**:
|
||
- 调用 `prescriptionGetByAppointment` 查询
|
||
- 如果有处方,调用 `prescriptionDetail` 获取详情
|
||
- 只发送一次 detail 请求
|
||
|
||
---
|
||
|
||
### 场景5:请求失败
|
||
|
||
**操作**:点击"开处方",但请求失败
|
||
|
||
**预期结果**:
|
||
- 显示错误提示
|
||
- 500ms 后 `isOpening` 重置
|
||
- 可以再次点击
|
||
|
||
## 其他优化建议
|
||
|
||
### 1. 按钮禁用状态
|
||
|
||
可以在按钮上添加 loading 状态:
|
||
|
||
```vue
|
||
<el-button
|
||
:loading="isOpening"
|
||
@click="handlePrescription(row)"
|
||
>
|
||
{{ prescriptionActionLabel(row) }}
|
||
</el-button>
|
||
```
|
||
|
||
但这需要将 `isOpening` 暴露给父组件,可能不太合适。
|
||
|
||
### 2. 防抖处理
|
||
|
||
可以使用 lodash 的 debounce 函数:
|
||
|
||
```typescript
|
||
import { debounce } from 'lodash-es'
|
||
|
||
const debouncedOpen = debounce(open, 500, { leading: true, trailing: false })
|
||
```
|
||
|
||
但当前的解决方案已经足够简单有效。
|
||
|
||
### 3. 请求取消
|
||
|
||
可以使用 AbortController 取消重复请求:
|
||
|
||
```typescript
|
||
let abortController: AbortController | null = null
|
||
|
||
const open = async (data: any) => {
|
||
if (abortController) {
|
||
abortController.abort()
|
||
}
|
||
abortController = new AbortController()
|
||
|
||
// 在请求中使用 signal
|
||
await fetch(url, { signal: abortController.signal })
|
||
}
|
||
```
|
||
|
||
但这需要修改 API 调用层,改动较大。
|
||
|
||
## 注意事项
|
||
|
||
### 1. 延迟时间
|
||
|
||
当前设置为 500ms,可以根据实际情况调整:
|
||
- 太短(< 300ms):可能无法有效防止快速连续点击
|
||
- 太长(> 1000ms):用户体验不好,需要等待较长时间
|
||
|
||
### 2. 控制台警告
|
||
|
||
`console.warn` 只在开发环境有用,生产环境可以考虑:
|
||
- 移除警告
|
||
- 或者改为用户提示:`feedback.msgWarning('请勿重复点击')`
|
||
|
||
### 3. 多个入口
|
||
|
||
如果有多个地方调用 `open()` 方法,都会受到这个锁的保护。
|
||
|
||
## 相关文件
|
||
|
||
### 修改的文件
|
||
- `admin/src/components/tcm-prescription/index.vue` - 处方组件
|
||
|
||
### 调用的文件
|
||
- `admin/src/views/tcm/appointment/list.vue` - 预约列表
|
||
- 其他可能调用处方组件的页面
|
||
|
||
## 总结
|
||
|
||
✅ 添加 `isOpening` 标志防止重复打开
|
||
✅ 使用 try-finally 确保标志正确重置
|
||
✅ 延迟 500ms 重置,防止快速连续点击
|
||
✅ 同时修复 `open()` 和 `openById()` 方法
|
||
✅ 添加控制台警告提示
|
||
|
||
**效果**:
|
||
- 防止重复请求
|
||
- 避免接口错误
|
||
- 提升用户体验
|
||
- 代码简单易维护
|
||
|
||
**下一步**:
|
||
1. 测试快速连续点击
|
||
2. 测试正常点击流程
|
||
3. 测试请求失败情况
|
||
4. 考虑是否需要用户提示
|