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

32 KiB
Raw Blame History

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:

  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:
} 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
  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