@
feat: shadcn主题 + 文件关系追踪 + 处理流程修复 前端: - 全站应用 shadcn/ui 主题 (zinc灰调, Inter字体, 1px细边框, 无硬阴影) - 重写 global.css / Dashboard.vue / Login.vue / Layout.vue 样式 - 新增文件处理子页面: 采购单(Orders), 表格处理(Tables), 图片处理(Images) - 侧边栏使用 el-sub-menu 组织文件处理导航 后端: - 新增 file_relations 表追踪 input→output→result 链路 - 新增 /files/relations, /files/stats/detailed 等关系查询API - 新增 ocr-single, excel-single, pipeline-single, merge-batch 端点 - 处理流程增加跳过逻辑 (已处理文件自动跳过) - 全流程不再自动合并, 合并仅在采购单页面手动触发 Bug修复: - TaskManager: asyncio.create_task 在线程池中无事件循环 → 改用 _schedule() 调度 - PurchaseOrderMerger 缺少 config 参数 → 传入 ConfigManager() - FastAPI regex= 弃用 → 改为 pattern= - merger.process() 接收 Path 对象 → 转为字符串 @
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<div class="file-page animate-in">
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<h3>采购单管理</h3>
|
||||
<el-tag type="info" size="small">共 {{ total }} 个</el-tag>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" :disabled="!selected.length" @click="batchMerge">
|
||||
合并选中 ({{ selected.length }})
|
||||
</el-button>
|
||||
<el-button :disabled="!selected.length" @click="batchDownload">
|
||||
批量下载
|
||||
</el-button>
|
||||
<el-button type="danger" :disabled="!selected.length" @click="batchDelete">
|
||||
批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="items"
|
||||
v-loading="loading"
|
||||
@selection-change="onSelect"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column type="selection" width="45" />
|
||||
<el-table-column label="采购单文件名" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span class="file-name primary">{{ row.result_purchase || '--' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="" width="40" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-icon :color="row.output_exists ? '#52C41A' : '#d1d5db'" :size="16">
|
||||
<Right />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Excel处理文件" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span class="file-name secondary">{{ row.output_excel || '--' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="" width="40" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-icon :color="row.input_exists ? '#52C41A' : '#d1d5db'" :size="16">
|
||||
<Right />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Input图片" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span class="file-name secondary">{{ row.input_image || '--' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="downloadFile(row)">
|
||||
下载
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="deleteFile(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-wrap">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="loadData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Right } from '@element-plus/icons-vue'
|
||||
import api from '../../api'
|
||||
|
||||
const items = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 50
|
||||
const loading = ref(false)
|
||||
const selected = ref<any[]>([])
|
||||
|
||||
function statusType(s: string) {
|
||||
const m: Record<string, string> = { done: 'success', merged: 'success', excel_done: 'warning', ocr_done: 'info', pending: 'info' }
|
||||
return m[s] || 'info'
|
||||
}
|
||||
function statusText(s: string) {
|
||||
const m: Record<string, string> = { done: '已完成', merged: '已合并', excel_done: '已处理', ocr_done: '已OCR', pending: '待处理' }
|
||||
return m[s] || s
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.get('/files/relations', { params: { view: 'orders', page: page.value, page_size: pageSize } })
|
||||
items.value = res.data.items
|
||||
total.value = res.data.total
|
||||
} catch {}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function onSelect(rows: any[]) { selected.value = rows }
|
||||
|
||||
async function downloadFile(row: any) {
|
||||
const token = localStorage.getItem('token')
|
||||
window.open(`/api/files/download/result/${encodeURIComponent(row.result_purchase)}?token=${token}`, '_blank')
|
||||
}
|
||||
|
||||
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 {}
|
||||
}
|
||||
|
||||
async function batchMerge() {
|
||||
if (!selected.value.length) return
|
||||
try {
|
||||
const filenames = selected.value.map(r => r.result_purchase).filter(Boolean)
|
||||
const res = await api.post('/processing/merge-batch', { filenames })
|
||||
ElMessage.success(`合并任务已创建: ${res.data.task_id}`)
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.response?.data?.detail || '合并失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function batchDownload() {
|
||||
const token = localStorage.getItem('token')
|
||||
for (const row of selected.value) {
|
||||
if (row.result_purchase) {
|
||||
window.open(`/api/files/download/result/${encodeURIComponent(row.result_purchase)}?token=${token}`, '_blank')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {}
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-page {
|
||||
max-width: 1400px;
|
||||
}
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.header-left h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.file-name.primary {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.file-name.secondary {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user