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>
32 KiB
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
// 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>:
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:
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:
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:
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
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
// 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
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
// 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
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:
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
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:
onMounted(async () => {
window.addEventListener('online', updateOnlineStatus)
window.addEventListener('offline', updateOnlineStatus)
await authStore.fetchUser()
})
- Step 2: Fix redundant navigator.onLine
Change line 160 from:
const isOnline = ref(navigator.onLine !== false)
to:
const isOnline = ref(navigator.onLine)
Change line 164 from:
isOnline.value = navigator.onLine !== false
to:
isOnline.value = navigator.onLine
- Step 3: Remove dead navItems code
Delete lines 175-183 (the unused navItems array):
// 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:
<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:
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):
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:
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:
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
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:
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:
const { showPreview, previewType, previewSrc, previewRows, openPreview, cleanupPreview } = useFilePreview()
Remove the local preview refs (lines 138-141):
// 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:
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:
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:
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:
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
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:
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:
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:
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:
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:
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:
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
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:
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:
const { showPreview, previewType, previewSrc, previewRows, openPreview, cleanupPreview } = useFilePreview()
Remove the local preview refs (lines 137-141):
// 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:
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:
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:
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:
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
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:
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:
} catch {
ElMessage.error('加载记忆库失败')
}
- Step 3: Fix deleteItem error handling
Find the deleteItem function's catch block. If it's empty, change it to:
} 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
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:
- Remove the unused
Plusimport from@element-plus/icons-vue(line 186 area — check the actual import line). - Fix the
deleteItemcatch block — if empty, change to:
} 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:
} catch {
ElMessage.error('加载统计数据失败')
}
- Step 3: Fix Logs.vue
In Logs.vue, fix the loadStats catch block (around line 164). If empty, change to:
} catch {
ElMessage.error('加载统计数据失败')
}
- Step 4: Fix Sync.vue
In Sync.vue, fix the checkStatus catch block (around line 127). If empty, change to:
} catch {
ElMessage.error('检查同步状态失败')
}
- Step 5: Verify build
Run: cd web/frontend && npm run build
Expected: Build succeeds.
- Step 6: Commit
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:
// 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:
return { currentTask, tasks, logs, taskSource, connectWebSocket, disconnectWebSocket, startTask, pollTaskStatus }
to:
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:
// 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
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:
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:
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:
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:
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
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
- Start the backend:
cd web && python -m uvicorn backend.main:app --reload - Start the frontend dev server:
cd web/frontend && npm run dev - Open
http://localhost:5173 - Login with admin/admin123
- Verify avatar shows "A" (not "U") after page refresh
- Navigate to each page and verify no console errors
- Test password change dialog — verify validation works
- Upload a test file, verify error toasts appear on failure