Files
orc-order-v2/docs/superpowers/plans/2026-05-12-frontend-bugfix-quality.md
T
houhuan beaf7c6203 docs: add frontend bug fix and code quality implementation plan
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>
2026-05-12 21:14:29 +08:00

1109 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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