beaf7c6203
12 tasks covering: useDebounce/useFileUtils/useFilePreview composables, global error handler, fetchUser fix, file view refactoring, error handling across 9 views, dead code cleanup, password validation, batch-delete API. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1109 lines
32 KiB
Markdown
1109 lines
32 KiB
Markdown
# Frontend Bug Fix + Code Quality Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Fix critical frontend bugs and eliminate code duplication across the Vue 3 web application.
|
||
|
||
**Architecture:** Create shared composables (`useDebounce`, `useFileUtils`, `useFilePreview`) to eliminate duplication across 7+ view files. Fix silent error swallowing, loading state management, and add global error handling. Clean up dead code.
|
||
|
||
**Tech Stack:** Vue 3.4+, TypeScript, Element Plus, Pinia, Vite
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
### New files
|
||
| File | Responsibility |
|
||
|------|---------------|
|
||
| `web/frontend/src/composables/useDebounce.ts` | Debounce utility (replaces 4 duplicate copies) |
|
||
| `web/frontend/src/composables/useFileUtils.ts` | `statusType`, `statusText`, `fmtTime` (replaces 3 duplicate copies) |
|
||
| `web/frontend/src/composables/useFilePreview.ts` | Preview dialog state + logic (replaces 3 duplicate copies) |
|
||
|
||
### Modified files
|
||
| File | Changes |
|
||
|------|---------|
|
||
| `web/frontend/src/main.ts` | Add global error handler |
|
||
| `web/frontend/src/views/Layout.vue` | Fix fetchUser, dead code, password validation, redundant code |
|
||
| `web/frontend/src/views/files/Images.vue` | Use composables, fix error handling + loading |
|
||
| `web/frontend/src/views/files/Tables.vue` | Use composables, fix error handling + loading |
|
||
| `web/frontend/src/views/files/Orders.vue` | Use composables, fix error handling + loading |
|
||
| `web/frontend/src/views/Memory.vue` | Fix error handling, use useDebounce, fix stats fallback |
|
||
| `web/frontend/src/views/Barcodes.vue` | Use useDebounce, remove dead import |
|
||
| `web/frontend/src/views/Tasks.vue` | Fix error handling, use useDebounce |
|
||
| `web/frontend/src/views/Logs.vue` | Fix error handling, use useDebounce |
|
||
| `web/frontend/src/views/Sync.vue` | Fix error handling |
|
||
| `web/frontend/src/stores/processing.ts` | Remove dead code |
|
||
| `web/frontend/src/router/index.ts` | Remove dead code |
|
||
| `web/backend/routers/files.py` | Add batch-delete endpoint |
|
||
|
||
---
|
||
|
||
## Task 1: Create useDebounce composable
|
||
|
||
**Files:**
|
||
- Create: `web/frontend/src/composables/useDebounce.ts`
|
||
|
||
- [ ] **Step 1: Create composables directory and useDebounce**
|
||
|
||
```typescript
|
||
// web/frontend/src/composables/useDebounce.ts
|
||
import { ref } from 'vue'
|
||
|
||
export function useDebounce<T extends (...args: any[]) => any>(fn: T, delay = 300) {
|
||
const timer = ref<ReturnType<typeof setTimeout> | null>(null)
|
||
return (...args: Parameters<T>) => {
|
||
if (timer.value) clearTimeout(timer.value)
|
||
timer.value = setTimeout(() => fn(...args), delay)
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Update Memory.vue — replace inline useDebounce with import**
|
||
|
||
In `web/frontend/src/views/Memory.vue`, remove lines 166-174 (the inline `useDebounce` function) and add import at the top of `<script setup>`:
|
||
|
||
```typescript
|
||
import { useDebounce } from '../composables/useDebounce'
|
||
```
|
||
|
||
The usage `const debouncedSearch = useDebounce(() => loadData())` remains unchanged.
|
||
|
||
- [ ] **Step 3: Update Barcodes.vue — replace inline useDebounce with import**
|
||
|
||
In `web/frontend/src/views/Barcodes.vue`, remove lines 190-198 (the inline `useDebounce` function) and add import:
|
||
|
||
```typescript
|
||
import { useDebounce } from '../composables/useDebounce'
|
||
```
|
||
|
||
- [ ] **Step 4: Update Tasks.vue — replace inline useDebounce with import**
|
||
|
||
In `web/frontend/src/views/Tasks.vue`, remove lines 144-152 (the inline `useDebounce` function) and add import:
|
||
|
||
```typescript
|
||
import { useDebounce } from '../composables/useDebounce'
|
||
```
|
||
|
||
- [ ] **Step 5: Update Logs.vue — replace inline useDebounce with import**
|
||
|
||
In `web/frontend/src/views/Logs.vue`, remove lines 107-115 (the inline `useDebounce` function) and add import:
|
||
|
||
```typescript
|
||
import { useDebounce } from '../composables/useDebounce'
|
||
```
|
||
|
||
- [ ] **Step 6: Verify build**
|
||
|
||
Run: `cd web/frontend && npm run build`
|
||
Expected: Build succeeds with no errors.
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
git add web/frontend/src/composables/useDebounce.ts web/frontend/src/views/Memory.vue web/frontend/src/views/Barcodes.vue web/frontend/src/views/Tasks.vue web/frontend/src/views/Logs.vue
|
||
git commit -m "refactor: extract useDebounce composable from 4 duplicate copies"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: Create useFileUtils composable
|
||
|
||
**Files:**
|
||
- Create: `web/frontend/src/composables/useFileUtils.ts`
|
||
|
||
- [ ] **Step 1: Create useFileUtils.ts**
|
||
|
||
```typescript
|
||
// web/frontend/src/composables/useFileUtils.ts
|
||
|
||
export function statusType(status: string): string {
|
||
const map: Record<string, string> = {
|
||
done: 'success', merged: 'success', excel_done: 'warning',
|
||
ocr_done: 'info', pending: 'info'
|
||
}
|
||
return map[status] || 'info'
|
||
}
|
||
|
||
export function statusText(status: string): string {
|
||
const map: Record<string, string> = {
|
||
done: '已完成', merged: '已合并', excel_done: '已处理',
|
||
ocr_done: '已OCR', pending: '待处理'
|
||
}
|
||
return map[status] || status
|
||
}
|
||
|
||
export function fmtTime(t: string): string {
|
||
if (!t) return '--'
|
||
return t.replace('T', ' ').slice(0, 19)
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify build**
|
||
|
||
Run: `cd web/frontend && npm run build`
|
||
Expected: Build succeeds.
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add web/frontend/src/composables/useFileUtils.ts
|
||
git commit -m "refactor: add useFileUtils composable for shared file helpers"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: Create useFilePreview composable
|
||
|
||
**Files:**
|
||
- Create: `web/frontend/src/composables/useFilePreview.ts`
|
||
|
||
- [ ] **Step 1: Create useFilePreview.ts**
|
||
|
||
```typescript
|
||
// web/frontend/src/composables/useFilePreview.ts
|
||
import { ref } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
|
||
export function useFilePreview() {
|
||
const showPreview = ref(false)
|
||
const previewType = ref<'image' | 'excel' | ''>('')
|
||
const previewSrc = ref('')
|
||
const previewRows = ref<string[][]>([])
|
||
|
||
async function openPreview(dir: string, fname: string) {
|
||
const token = localStorage.getItem('token')
|
||
try {
|
||
const resp = await fetch(`/api/files/preview/${dir}/${encodeURIComponent(fname)}`, {
|
||
headers: { Authorization: `Bearer ${token}` }
|
||
})
|
||
const ct = resp.headers.get('content-type') || ''
|
||
if (ct.includes('image')) {
|
||
previewType.value = 'image'
|
||
const blob = await resp.blob()
|
||
previewSrc.value = URL.createObjectURL(blob)
|
||
} else {
|
||
const data = await resp.json()
|
||
if (data.type === 'excel') {
|
||
previewType.value = 'excel'
|
||
previewRows.value = data.rows
|
||
}
|
||
}
|
||
showPreview.value = true
|
||
} catch {
|
||
ElMessage.error('预览失败')
|
||
}
|
||
}
|
||
|
||
function cleanupPreview() {
|
||
if (previewSrc.value && previewSrc.value.startsWith('blob:')) {
|
||
URL.revokeObjectURL(previewSrc.value)
|
||
}
|
||
previewSrc.value = ''
|
||
previewType.value = ''
|
||
previewRows.value = []
|
||
}
|
||
|
||
return {
|
||
showPreview, previewType, previewSrc, previewRows,
|
||
openPreview, cleanupPreview
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify build**
|
||
|
||
Run: `cd web/frontend && npm run build`
|
||
Expected: Build succeeds.
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add web/frontend/src/composables/useFilePreview.ts
|
||
git commit -m "refactor: add useFilePreview composable for shared preview logic"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: Add global error handler to main.ts
|
||
|
||
**Files:**
|
||
- Modify: `web/frontend/src/main.ts`
|
||
|
||
- [ ] **Step 1: Add ElMessage import and error handler**
|
||
|
||
Replace the contents of `web/frontend/src/main.ts` with:
|
||
|
||
```typescript
|
||
import { createApp } from 'vue'
|
||
import { createPinia } from 'pinia'
|
||
import ElementPlus, { ElMessage } from 'element-plus'
|
||
import 'element-plus/dist/index.css'
|
||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||
import './styles/global.css'
|
||
|
||
import App from './App.vue'
|
||
import router from './router'
|
||
|
||
const app = createApp(App)
|
||
|
||
app.config.errorHandler = (err, _instance, info) => {
|
||
console.error('Vue error:', err, info)
|
||
ElMessage.error('操作失败,请稍后重试')
|
||
}
|
||
|
||
app.use(createPinia())
|
||
app.use(router)
|
||
app.use(ElementPlus, { locale: zhCn })
|
||
|
||
app.mount('#app')
|
||
```
|
||
|
||
- [ ] **Step 2: Verify build**
|
||
|
||
Run: `cd web/frontend && npm run build`
|
||
Expected: Build succeeds.
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add web/frontend/src/main.ts
|
||
git commit -m "fix: add global Vue error handler with user-facing toast"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: Fix Layout.vue — fetchUser, dead code, password validation, redundant code
|
||
|
||
**Files:**
|
||
- Modify: `web/frontend/src/views/Layout.vue`
|
||
|
||
- [ ] **Step 1: Fix fetchUser — add onMounted call**
|
||
|
||
In `web/frontend/src/views/Layout.vue`, the `onMounted` block at line 166 currently only registers event listeners. Change it to also call `fetchUser`:
|
||
|
||
```typescript
|
||
onMounted(async () => {
|
||
window.addEventListener('online', updateOnlineStatus)
|
||
window.addEventListener('offline', updateOnlineStatus)
|
||
await authStore.fetchUser()
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 2: Fix redundant navigator.onLine**
|
||
|
||
Change line 160 from:
|
||
```typescript
|
||
const isOnline = ref(navigator.onLine !== false)
|
||
```
|
||
to:
|
||
```typescript
|
||
const isOnline = ref(navigator.onLine)
|
||
```
|
||
|
||
Change line 164 from:
|
||
```typescript
|
||
isOnline.value = navigator.onLine !== false
|
||
```
|
||
to:
|
||
```typescript
|
||
isOnline.value = navigator.onLine
|
||
```
|
||
|
||
- [ ] **Step 3: Remove dead navItems code**
|
||
|
||
Delete lines 175-183 (the unused `navItems` array):
|
||
|
||
```typescript
|
||
// DELETE THIS BLOCK:
|
||
const navItems: { path: string; label: string; icon: any; badge?: string }[] = [
|
||
{ path: '/', label: '处理中心', icon: HomeFilled },
|
||
{ path: '/tasks', label: '任务历史', icon: Timer },
|
||
{ path: '/logs', label: '日志中心', icon: Notebook },
|
||
{ path: '/memory', label: '记忆库', icon: Memo },
|
||
{ path: '/barcodes', label: '条码映射', icon: Connection },
|
||
{ path: '/config', label: '系统配置', icon: Setting },
|
||
{ path: '/sync', label: '云端同步', icon: Cloudy },
|
||
]
|
||
```
|
||
|
||
- [ ] **Step 4: Add password form validation and confirmation field**
|
||
|
||
Replace the password dialog template (lines 126-139) with:
|
||
|
||
```html
|
||
<el-dialog v-model="showPwd" title="修改密码" width="420px" :close-on-click-modal="false">
|
||
<el-form ref="pwdFormRef" :model="pwdForm" :rules="pwdRules" label-width="70px">
|
||
<el-form-item label="旧密码" prop="old_password">
|
||
<el-input v-model="pwdForm.old_password" type="password" show-password />
|
||
</el-form-item>
|
||
<el-form-item label="新密码" prop="new_password">
|
||
<el-input v-model="pwdForm.new_password" type="password" show-password />
|
||
</el-form-item>
|
||
<el-form-item label="确认密码" prop="confirm_password">
|
||
<el-input v-model="pwdForm.confirm_password" type="password" show-password />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="showPwd = false">取消</el-button>
|
||
<el-button type="primary" @click="changePassword">确认修改</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
```
|
||
|
||
Update the script section — add `confirm_password` to `pwdForm` and add validation rules:
|
||
|
||
```typescript
|
||
import { ref, computed, reactive, onMounted, onUnmounted } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||
```
|
||
|
||
Update `pwdForm` (line 159):
|
||
```typescript
|
||
const pwdForm = reactive({ old_password: '', new_password: '', confirm_password: '' })
|
||
const pwdFormRef = ref<FormInstance>()
|
||
const pwdRules: FormRules = {
|
||
old_password: [{ required: true, message: '请输入旧密码', trigger: 'blur' }],
|
||
new_password: [
|
||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||
{ min: 6, message: '密码至少6位', trigger: 'blur' }
|
||
],
|
||
confirm_password: [
|
||
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
||
{
|
||
validator: (_rule: any, value: string, callback: any) => {
|
||
if (value !== pwdForm.new_password) {
|
||
callback(new Error('两次输入的密码不一致'))
|
||
} else {
|
||
callback()
|
||
}
|
||
},
|
||
trigger: 'blur'
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Update `handleCommand` to reset `confirm_password`:
|
||
```typescript
|
||
function handleCommand(cmd: string) {
|
||
if (cmd === 'logout') {
|
||
authStore.logout()
|
||
router.push('/login')
|
||
} else if (cmd === 'password') {
|
||
pwdForm.old_password = ''
|
||
pwdForm.new_password = ''
|
||
pwdForm.confirm_password = ''
|
||
showPwd.value = true
|
||
}
|
||
}
|
||
```
|
||
|
||
Update `changePassword` to validate before submit:
|
||
```typescript
|
||
async function changePassword() {
|
||
if (!pwdFormRef.value) return
|
||
await pwdFormRef.value.validate(async (valid) => {
|
||
if (!valid) return
|
||
try {
|
||
await api.post('/auth/change-password', {
|
||
old_password: pwdForm.old_password,
|
||
new_password: pwdForm.new_password
|
||
})
|
||
ElMessage.success('密码修改成功')
|
||
showPwd.value = false
|
||
} catch (err: any) {
|
||
ElMessage.error(err.response?.data?.detail || '修改失败')
|
||
}
|
||
})
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: Verify build**
|
||
|
||
Run: `cd web/frontend && npm run build`
|
||
Expected: Build succeeds.
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add web/frontend/src/views/Layout.vue
|
||
git commit -m "fix: fetchUser on mount, password validation, remove dead code"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: Refactor Images.vue — use composables + fix error handling
|
||
|
||
**Files:**
|
||
- Modify: `web/frontend/src/views/files/Images.vue`
|
||
|
||
- [ ] **Step 1: Update imports**
|
||
|
||
Replace the imports section (lines 119-124) with:
|
||
|
||
```typescript
|
||
import { ref, onMounted } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Right } from '@element-plus/icons-vue'
|
||
import { useProcessingStore } from '../../stores/processing'
|
||
import { statusType, statusText, fmtTime } from '../../composables/useFileUtils'
|
||
import { useFilePreview } from '../../composables/useFilePreview'
|
||
import api from '../../api'
|
||
```
|
||
|
||
- [ ] **Step 2: Use useFilePreview and remove duplicate preview code**
|
||
|
||
After `const processingStore = useProcessingStore()`, add:
|
||
|
||
```typescript
|
||
const { showPreview, previewType, previewSrc, previewRows, openPreview, cleanupPreview } = useFilePreview()
|
||
```
|
||
|
||
Remove the local preview refs (lines 138-141):
|
||
```typescript
|
||
// DELETE: const showPreview = ref(false)
|
||
// DELETE: const previewType = ref('')
|
||
// DELETE: const previewSrc = ref('')
|
||
// DELETE: const previewRows = ref<string[][]>([])
|
||
```
|
||
|
||
Remove the local `statusType`, `statusText`, `fmtTime` functions (lines 145-157).
|
||
|
||
Replace `previewFile` function (lines 171-188) with:
|
||
|
||
```typescript
|
||
async function previewFile(row: any) {
|
||
const fname = row.input_image || row.output_excel || row.result_purchase
|
||
const dir = row.input_image ? 'input' : row.output_excel ? 'output' : 'result'
|
||
await openPreview(dir, fname)
|
||
}
|
||
```
|
||
|
||
Remove the local `cleanupPreview` function (lines 190-197).
|
||
|
||
- [ ] **Step 3: Fix loadData error handling and loading state**
|
||
|
||
Replace `loadData` (lines 159-167) with:
|
||
|
||
```typescript
|
||
async function loadData() {
|
||
loading.value = true
|
||
try {
|
||
const res = await api.get('/files/relations', { params: { view: 'images', page: page.value, page_size: pageSize, sort_by: sortBy.value, sort_order: sortOrder.value } })
|
||
items.value = res.data.items
|
||
total.value = res.data.total
|
||
} catch {
|
||
ElMessage.error('加载文件列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Fix deleteFile error handling**
|
||
|
||
Replace `deleteFile` (lines 265-273) with:
|
||
|
||
```typescript
|
||
async function deleteFile(row: any) {
|
||
try {
|
||
await ElMessageBox.confirm(`确定删除 ${row.input_image}?`, '确认')
|
||
await api.delete(`/files/input/${encodeURIComponent(row.input_image)}`)
|
||
if (row.id) await api.delete('/files/relations', { data: { ids: [row.id] } })
|
||
ElMessage.success('已删除')
|
||
loadData()
|
||
} catch (err: any) {
|
||
if (err !== 'cancel') ElMessage.error('删除失败')
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: Fix batchDelete error handling**
|
||
|
||
Replace `batchDelete` (lines 304-318) with:
|
||
|
||
```typescript
|
||
async function batchDelete() {
|
||
try {
|
||
await ElMessageBox.confirm(`确定删除选中的 ${selected.value.length} 个文件?`, '确认')
|
||
for (const row of selected.value) {
|
||
if (row.input_image) {
|
||
await api.delete(`/files/input/${encodeURIComponent(row.input_image)}`)
|
||
}
|
||
if (row.id) {
|
||
await api.delete('/files/relations', { data: { ids: [row.id] } })
|
||
}
|
||
}
|
||
ElMessage.success('批量删除完成')
|
||
loadData()
|
||
} catch (err: any) {
|
||
if (err !== 'cancel') ElMessage.error('批量删除失败')
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 6: Verify build**
|
||
|
||
Run: `cd web/frontend && npm run build`
|
||
Expected: Build succeeds.
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
git add web/frontend/src/views/files/Images.vue
|
||
git commit -m "refactor: Images.vue uses shared composables, fix error handling"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: Refactor Tables.vue — use composables + fix error handling
|
||
|
||
**Files:**
|
||
- Modify: `web/frontend/src/views/files/Tables.vue`
|
||
|
||
- [ ] **Step 1: Update imports**
|
||
|
||
Replace the imports section (lines 115-120) with:
|
||
|
||
```typescript
|
||
import { ref, onMounted } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Right } from '@element-plus/icons-vue'
|
||
import { useProcessingStore } from '../../stores/processing'
|
||
import { statusType, statusText, fmtTime } from '../../composables/useFileUtils'
|
||
import { useFilePreview } from '../../composables/useFilePreview'
|
||
import api from '../../api'
|
||
```
|
||
|
||
- [ ] **Step 2: Use useFilePreview and remove duplicate code**
|
||
|
||
After `const processingStore = useProcessingStore()`, add:
|
||
|
||
```typescript
|
||
const { showPreview, previewType, previewSrc, previewRows, openPreview, cleanupPreview } = useFilePreview()
|
||
```
|
||
|
||
Remove the local preview refs (lines 134-137).
|
||
|
||
Remove the local `statusType`, `statusText`, `fmtTime` functions (lines 141-153).
|
||
|
||
Replace `previewFile` (lines 167-184) with:
|
||
|
||
```typescript
|
||
async function previewFile(row: any) {
|
||
const fname = row.output_excel || row.result_purchase || row.input_image
|
||
const dir = row.output_excel ? 'output' : row.result_purchase ? 'result' : 'input'
|
||
await openPreview(dir, fname)
|
||
}
|
||
```
|
||
|
||
Remove the local `cleanupPreview` function (lines 186-193).
|
||
|
||
- [ ] **Step 3: Fix loadData error handling and loading state**
|
||
|
||
Replace `loadData` (lines 155-163) with:
|
||
|
||
```typescript
|
||
async function loadData() {
|
||
loading.value = true
|
||
try {
|
||
const res = await api.get('/files/relations', { params: { view: 'tables', page: page.value, page_size: pageSize, sort_by: sortBy.value, sort_order: sortOrder.value } })
|
||
items.value = res.data.items
|
||
total.value = res.data.total
|
||
} catch {
|
||
ElMessage.error('加载文件列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Fix deleteFile error handling**
|
||
|
||
Replace `deleteFile` (lines 243-251) with:
|
||
|
||
```typescript
|
||
async function deleteFile(row: any) {
|
||
try {
|
||
await ElMessageBox.confirm(`确定删除 ${row.output_excel}?`, '确认')
|
||
await api.delete(`/files/output/${encodeURIComponent(row.output_excel)}`)
|
||
if (row.id) await api.delete('/files/relations', { data: { ids: [row.id] } })
|
||
ElMessage.success('已删除')
|
||
loadData()
|
||
} catch (err: any) {
|
||
if (err !== 'cancel') ElMessage.error('删除失败')
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: Fix batchDelete error handling**
|
||
|
||
Replace `batchDelete` (lines 263-277) with:
|
||
|
||
```typescript
|
||
async function batchDelete() {
|
||
try {
|
||
await ElMessageBox.confirm(`确定删除选中的 ${selected.value.length} 个文件?`, '确认')
|
||
for (const row of selected.value) {
|
||
if (row.output_excel) {
|
||
await api.delete(`/files/output/${encodeURIComponent(row.output_excel)}`)
|
||
}
|
||
if (row.id) {
|
||
await api.delete('/files/relations', { data: { ids: [row.id] } })
|
||
}
|
||
}
|
||
ElMessage.success('批量删除完成')
|
||
loadData()
|
||
} catch (err: any) {
|
||
if (err !== 'cancel') ElMessage.error('批量删除失败')
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 6: Verify build**
|
||
|
||
Run: `cd web/frontend && npm run build`
|
||
Expected: Build succeeds.
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
git add web/frontend/src/views/files/Tables.vue
|
||
git commit -m "refactor: Tables.vue uses shared composables, fix error handling"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: Refactor Orders.vue — use composables + fix error handling
|
||
|
||
**Files:**
|
||
- Modify: `web/frontend/src/views/files/Orders.vue`
|
||
|
||
- [ ] **Step 1: Update imports**
|
||
|
||
Replace the imports section (lines 119-124) with:
|
||
|
||
```typescript
|
||
import { ref, onMounted } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Right } from '@element-plus/icons-vue'
|
||
import { useProcessingStore } from '../../stores/processing'
|
||
import { statusType, statusText, fmtTime } from '../../composables/useFileUtils'
|
||
import { useFilePreview } from '../../composables/useFilePreview'
|
||
import api from '../../api'
|
||
```
|
||
|
||
- [ ] **Step 2: Use useFilePreview and remove duplicate code**
|
||
|
||
After `const processingStore = useProcessingStore()`, add:
|
||
|
||
```typescript
|
||
const { showPreview, previewType, previewSrc, previewRows, openPreview, cleanupPreview } = useFilePreview()
|
||
```
|
||
|
||
Remove the local preview refs (lines 137-141):
|
||
```typescript
|
||
// DELETE: const showPreview = ref(false)
|
||
// DELETE: const previewType = ref('')
|
||
// DELETE: const previewSrc = ref('')
|
||
// DELETE: const previewRows = ref<string[][]>([])
|
||
```
|
||
|
||
Remove the local `statusType`, `statusText`, `fmtTime` functions (lines 147-159).
|
||
|
||
Replace `previewFile` (lines 173-198) with:
|
||
|
||
```typescript
|
||
async function previewFile(row: any) {
|
||
const fname = row.result_purchase || row.output_excel || row.input_image
|
||
const dir = row.result_purchase ? 'result' : row.output_excel ? 'output' : 'input'
|
||
await openPreview(dir, fname)
|
||
}
|
||
```
|
||
|
||
Remove the local `cleanupPreview` function (lines 200-207).
|
||
|
||
- [ ] **Step 3: Fix loadData error handling and loading state**
|
||
|
||
Replace `loadData` (lines 161-169) with:
|
||
|
||
```typescript
|
||
async function loadData() {
|
||
loading.value = true
|
||
try {
|
||
const res = await api.get('/files/relations', { params: { view: 'orders', page: page.value, page_size: pageSize, sort_by: sortBy.value, sort_order: sortOrder.value } })
|
||
items.value = res.data.items
|
||
total.value = res.data.total
|
||
} catch {
|
||
ElMessage.error('加载文件列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Fix deleteFile error handling**
|
||
|
||
Replace `deleteFile` (lines 235-243) with:
|
||
|
||
```typescript
|
||
async function deleteFile(row: any) {
|
||
try {
|
||
await ElMessageBox.confirm(`确定删除 ${row.result_purchase}?`, '确认')
|
||
await api.delete(`/files/result/${encodeURIComponent(row.result_purchase)}`)
|
||
if (row.id) await api.delete('/files/relations', { data: { ids: [row.id] } })
|
||
ElMessage.success('已删除')
|
||
loadData()
|
||
} catch (err: any) {
|
||
if (err !== 'cancel') ElMessage.error('删除失败')
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: Fix batchDelete error handling**
|
||
|
||
Replace `batchDelete` (lines 265-279) with:
|
||
|
||
```typescript
|
||
async function batchDelete() {
|
||
try {
|
||
await ElMessageBox.confirm(`确定删除选中的 ${selected.value.length} 个文件?`, '确认')
|
||
for (const row of selected.value) {
|
||
if (row.result_purchase) {
|
||
await api.delete(`/files/result/${encodeURIComponent(row.result_purchase)}`)
|
||
}
|
||
if (row.id) {
|
||
await api.delete('/files/relations', { data: { ids: [row.id] } })
|
||
}
|
||
}
|
||
ElMessage.success('批量删除完成')
|
||
loadData()
|
||
} catch (err: any) {
|
||
if (err !== 'cancel') ElMessage.error('批量删除失败')
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 6: Verify build**
|
||
|
||
Run: `cd web/frontend && npm run build`
|
||
Expected: Build succeeds.
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
git add web/frontend/src/views/files/Orders.vue
|
||
git commit -m "refactor: Orders.vue uses shared composables, fix error handling"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 9: Fix Memory.vue — error handling + stats fallback
|
||
|
||
**Files:**
|
||
- Modify: `web/frontend/src/views/Memory.vue`
|
||
|
||
- [ ] **Step 1: Fix stats fallback logic**
|
||
|
||
Find the stats computation section (around lines 221-225) where `highConfidence`, `mediumConfidence`, `lowConfidence` fall back to computing from `items.value`. Change the fallback to show 0 instead of page-scoped data:
|
||
|
||
```typescript
|
||
const highConfidence = computed(() => stats.value?.high ?? 0)
|
||
const mediumConfidence = computed(() => stats.value?.medium ?? 0)
|
||
const lowConfidence = computed(() => stats.value?.low ?? 0)
|
||
```
|
||
|
||
- [ ] **Step 2: Fix loadData error handling**
|
||
|
||
Find the `loadData` function and add error handling to its catch block. It currently has an empty catch or minimal handling. Ensure it shows:
|
||
|
||
```typescript
|
||
} catch {
|
||
ElMessage.error('加载记忆库失败')
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Fix deleteItem error handling**
|
||
|
||
Find the `deleteItem` function's catch block. If it's empty, change it to:
|
||
|
||
```typescript
|
||
} catch (err: any) {
|
||
if (err !== 'cancel') ElMessage.error('删除失败')
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Verify build**
|
||
|
||
Run: `cd web/frontend && npm run build`
|
||
Expected: Build succeeds.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add web/frontend/src/views/Memory.vue
|
||
git commit -m "fix: Memory.vue stats fallback and error handling"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 10: Fix remaining views — error handling
|
||
|
||
**Files:**
|
||
- Modify: `web/frontend/src/views/Barcodes.vue`
|
||
- Modify: `web/frontend/src/views/Tasks.vue`
|
||
- Modify: `web/frontend/src/views/Logs.vue`
|
||
- Modify: `web/frontend/src/views/Sync.vue`
|
||
|
||
- [ ] **Step 1: Fix Barcodes.vue**
|
||
|
||
In `Barcodes.vue`:
|
||
1. Remove the unused `Plus` import from `@element-plus/icons-vue` (line 186 area — check the actual import line).
|
||
2. Fix the `deleteItem` catch block — if empty, change to:
|
||
```typescript
|
||
} catch (err: any) {
|
||
if (err !== 'cancel') ElMessage.error('删除失败')
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Fix Tasks.vue**
|
||
|
||
In `Tasks.vue`, fix the `loadStats` catch block (around line 207). If empty, change to:
|
||
```typescript
|
||
} catch {
|
||
ElMessage.error('加载统计数据失败')
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Fix Logs.vue**
|
||
|
||
In `Logs.vue`, fix the `loadStats` catch block (around line 164). If empty, change to:
|
||
```typescript
|
||
} catch {
|
||
ElMessage.error('加载统计数据失败')
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Fix Sync.vue**
|
||
|
||
In `Sync.vue`, fix the `checkStatus` catch block (around line 127). If empty, change to:
|
||
```typescript
|
||
} catch {
|
||
ElMessage.error('检查同步状态失败')
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: Verify build**
|
||
|
||
Run: `cd web/frontend && npm run build`
|
||
Expected: Build succeeds.
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add web/frontend/src/views/Barcodes.vue web/frontend/src/views/Tasks.vue web/frontend/src/views/Logs.vue web/frontend/src/views/Sync.vue
|
||
git commit -m "fix: add error handling to Barcodes, Tasks, Logs, Sync views"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 11: Clean up dead code in stores and router
|
||
|
||
**Files:**
|
||
- Modify: `web/frontend/src/stores/processing.ts`
|
||
- Modify: `web/frontend/src/router/index.ts`
|
||
|
||
- [ ] **Step 1: Remove pollTaskStatus from processing.ts**
|
||
|
||
Delete lines 124-127 from `web/frontend/src/stores/processing.ts`:
|
||
```typescript
|
||
// DELETE:
|
||
async function pollTaskStatus(taskId: string) {
|
||
const res = await api.get(`/processing/status/${taskId}`)
|
||
return res.data
|
||
}
|
||
```
|
||
|
||
Also remove `pollTaskStatus` from the return statement (line 129). Change:
|
||
```typescript
|
||
return { currentTask, tasks, logs, taskSource, connectWebSocket, disconnectWebSocket, startTask, pollTaskStatus }
|
||
```
|
||
to:
|
||
```typescript
|
||
return { currentTask, tasks, logs, taskSource, connectWebSocket, disconnectWebSocket, startTask }
|
||
```
|
||
|
||
- [ ] **Step 2: Remove routeLoadingTimer from router/index.ts**
|
||
|
||
Delete lines 86-94 from `web/frontend/src/router/index.ts`:
|
||
```typescript
|
||
// DELETE:
|
||
// Track route loading state for page transitions
|
||
let routeLoadingTimer: ReturnType<typeof setTimeout> | null = null
|
||
|
||
router.afterEach(() => {
|
||
if (routeLoadingTimer) clearTimeout(routeLoadingTimer)
|
||
routeLoadingTimer = setTimeout(() => {
|
||
routeLoadingTimer = null
|
||
}, 300)
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 3: Verify build**
|
||
|
||
Run: `cd web/frontend && npm run build`
|
||
Expected: Build succeeds.
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add web/frontend/src/stores/processing.ts web/frontend/src/router/index.ts
|
||
git commit -m "refactor: remove dead code (pollTaskStatus, routeLoadingTimer)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 12: Backend batch-delete endpoint
|
||
|
||
**Files:**
|
||
- Modify: `web/backend/routers/files.py`
|
||
|
||
- [ ] **Step 1: Add batch-delete endpoint**
|
||
|
||
Add the following endpoint to `web/backend/routers/files.py`, after the existing single-file delete endpoint:
|
||
|
||
```python
|
||
from pydantic import BaseModel
|
||
|
||
class BatchDeleteRequest(BaseModel):
|
||
files: list[dict] # Each dict: {"directory": "input"|"output"|"result", "filename": "..."}
|
||
|
||
@router.post("/batch-delete")
|
||
async def batch_delete_files(req: BatchDeleteRequest, user=Depends(get_current_user)):
|
||
dir_map = {
|
||
'input': Path('data/input'),
|
||
'output': Path('data/output'),
|
||
'result': Path('data/result'),
|
||
}
|
||
deleted = 0
|
||
errors = []
|
||
for item in req.files:
|
||
d = item.get('directory', '')
|
||
fname = item.get('filename', '')
|
||
if d not in dir_map or not fname:
|
||
errors.append(f"无效参数: {d}/{fname}")
|
||
continue
|
||
file_path = dir_map[d] / fname
|
||
try:
|
||
if file_path.exists():
|
||
file_path.unlink()
|
||
deleted += 1
|
||
# Clean up relation
|
||
_cleanup_relation_for_deleted_file(d, fname)
|
||
except Exception as e:
|
||
errors.append(f"{fname}: {str(e)}")
|
||
return {"deleted": deleted, "errors": errors}
|
||
```
|
||
|
||
Note: The `_cleanup_relation_for_deleted_file` helper already exists in the file. The `BatchDeleteRequest` model should be placed near the top of the file with other models. Adjust the import of `Depends` and `Path` if not already imported.
|
||
|
||
- [ ] **Step 2: Update frontend batchDelete to use new endpoint**
|
||
|
||
In all three file views (`Images.vue`, `Tables.vue`, `Orders.vue`), replace the sequential batch delete loop with a single API call.
|
||
|
||
**Images.vue** — replace `batchDelete`:
|
||
```typescript
|
||
async function batchDelete() {
|
||
try {
|
||
await ElMessageBox.confirm(`确定删除选中的 ${selected.value.length} 个文件?`, '确认')
|
||
const files = selected.value
|
||
.filter(r => r.input_image)
|
||
.map(r => ({ directory: 'input', filename: r.input_image }))
|
||
const res = await api.post('/files/batch-delete', { files })
|
||
if (res.data.errors?.length) {
|
||
ElMessage.warning(`删除完成,${res.data.errors.length} 个文件失败`)
|
||
} else {
|
||
ElMessage.success('批量删除完成')
|
||
}
|
||
loadData()
|
||
} catch (err: any) {
|
||
if (err !== 'cancel') ElMessage.error('批量删除失败')
|
||
}
|
||
}
|
||
```
|
||
|
||
**Tables.vue** — replace `batchDelete`:
|
||
```typescript
|
||
async function batchDelete() {
|
||
try {
|
||
await ElMessageBox.confirm(`确定删除选中的 ${selected.value.length} 个文件?`, '确认')
|
||
const files = selected.value
|
||
.filter(r => r.output_excel)
|
||
.map(r => ({ directory: 'output', filename: r.output_excel }))
|
||
const res = await api.post('/files/batch-delete', { files })
|
||
if (res.data.errors?.length) {
|
||
ElMessage.warning(`删除完成,${res.data.errors.length} 个文件失败`)
|
||
} else {
|
||
ElMessage.success('批量删除完成')
|
||
}
|
||
loadData()
|
||
} catch (err: any) {
|
||
if (err !== 'cancel') ElMessage.error('批量删除失败')
|
||
}
|
||
}
|
||
```
|
||
|
||
**Orders.vue** — replace `batchDelete`:
|
||
```typescript
|
||
async function batchDelete() {
|
||
try {
|
||
await ElMessageBox.confirm(`确定删除选中的 ${selected.value.length} 个文件?`, '确认')
|
||
const files = selected.value
|
||
.filter(r => r.result_purchase)
|
||
.map(r => ({ directory: 'result', filename: r.result_purchase }))
|
||
const res = await api.post('/files/batch-delete', { files })
|
||
if (res.data.errors?.length) {
|
||
ElMessage.warning(`删除完成,${res.data.errors.length} 个文件失败`)
|
||
} else {
|
||
ElMessage.success('批量删除完成')
|
||
}
|
||
loadData()
|
||
} catch (err: any) {
|
||
if (err !== 'cancel') ElMessage.error('批量删除失败')
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Verify build**
|
||
|
||
Run: `cd web/frontend && npm run build`
|
||
Expected: Build succeeds.
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add web/backend/routers/files.py web/frontend/src/views/files/Images.vue web/frontend/src/views/files/Tables.vue web/frontend/src/views/files/Orders.vue
|
||
git commit -m "feat: add batch-delete API endpoint, replace N+1 frontend calls"
|
||
```
|
||
|
||
---
|
||
|
||
## Final Verification
|
||
|
||
- [ ] **Step 1: Full build verification**
|
||
|
||
Run: `cd web/frontend && npm run build`
|
||
Expected: Build succeeds with no errors or warnings.
|
||
|
||
- [ ] **Step 2: Manual smoke test**
|
||
|
||
1. Start the backend: `cd web && python -m uvicorn backend.main:app --reload`
|
||
2. Start the frontend dev server: `cd web/frontend && npm run dev`
|
||
3. Open `http://localhost:5173`
|
||
4. Login with admin/admin123
|
||
5. Verify avatar shows "A" (not "U") after page refresh
|
||
6. Navigate to each page and verify no console errors
|
||
7. Test password change dialog — verify validation works
|
||
8. Upload a test file, verify error toasts appear on failure
|