# 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
