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:
2026-05-05 14:16:30 +08:00
parent dedc3b4183
commit 0721ed099c
13 changed files with 2341 additions and 602 deletions
+125 -57
View File
@@ -16,23 +16,57 @@
</div>
<!-- Navigation -->
<nav class="sidebar-nav">
<router-link
v-for="item in navItems"
:key="item.path"
:to="item.path"
class="nav-item"
:class="{ active: route.path === item.path }"
>
<el-icon :size="20"><component :is="item.icon" /></el-icon>
<transition name="fade">
<span v-if="!isCollapse" class="nav-label">{{ item.label }}</span>
</transition>
<transition name="fade">
<span v-if="!isCollapse && item.badge" class="nav-badge">{{ item.badge }}</span>
</transition>
</router-link>
</nav>
<el-menu
:default-active="route.path"
:default-openeds="filesMenuOpen"
:collapse="isCollapse"
mode="vertical"
background-color="transparent"
text-color="var(--text-sidebar)"
active-text-color="#fafafa"
class="sidebar-nav"
router
>
<el-menu-item index="/">
<el-icon><HomeFilled /></el-icon>
<template #title>处理中心</template>
</el-menu-item>
<el-sub-menu index="/files">
<template #title>
<el-icon><FolderOpened /></el-icon>
<span>文件处理</span>
</template>
<el-menu-item index="/files/orders">采购单</el-menu-item>
<el-menu-item index="/files/tables">表格处理</el-menu-item>
<el-menu-item index="/files/images">图片处理</el-menu-item>
</el-sub-menu>
<el-menu-item index="/tasks">
<el-icon><Timer /></el-icon>
<template #title>任务历史</template>
</el-menu-item>
<el-menu-item index="/logs">
<el-icon><Notebook /></el-icon>
<template #title>日志中心</template>
</el-menu-item>
<el-menu-item index="/memory">
<el-icon><Memo /></el-icon>
<template #title>记忆库</template>
</el-menu-item>
<el-menu-item index="/barcodes">
<el-icon><Connection /></el-icon>
<template #title>条码映射</template>
</el-menu-item>
<el-menu-item index="/config">
<el-icon><Setting /></el-icon>
<template #title>系统配置</template>
</el-menu-item>
<el-menu-item index="/sync">
<el-icon><Cloudy /></el-icon>
<template #title>云端同步</template>
</el-menu-item>
</el-menu>
<!-- Collapse toggle -->
<div class="sidebar-footer">
@@ -105,7 +139,7 @@ import { ref, computed, reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
HomeFilled, Memo, Connection, Setting, Cloudy, Timer, Notebook,
HomeFilled, Memo, Connection, Setting, Cloudy, Timer, Notebook, FolderOpened,
ArrowDown, Lock, SwitchButton, DArrowLeft, DArrowRight
} from '@element-plus/icons-vue'
import { useAuthStore } from '../stores/auth'
@@ -129,9 +163,23 @@ const navItems: { path: string; label: string; icon: any; badge?: string }[] = [
{ path: '/sync', label: '云端同步', icon: Cloudy },
]
const filesMenuOpen = ['/files']
const pageTitles: Record<string, string> = {
'/': '处理中心',
'/files/orders': '采购单',
'/files/tables': '表格处理',
'/files/images': '图片处理',
'/tasks': '任务历史',
'/logs': '日志中心',
'/memory': '记忆库',
'/barcodes': '条码映射',
'/config': '系统配置',
'/sync': '云端同步',
}
const pageTitle = computed(() => {
const item = navItems.find(n => n.path === route.path)
return item?.label || '处理中心'
return pageTitles[route.path] || '处理中心'
})
function handleCommand(cmd: string) {
@@ -164,12 +212,12 @@ async function changePassword() {
/* ── Sidebar ── */
.sidebar {
background: var(--bg-sidebar);
background: #09090b;
display: flex;
flex-direction: column;
transition: width 0.3s var(--ease-out);
overflow: hidden;
border-right: 1px solid rgba(255,255,255,0.06);
border-right: 1px solid #18181b;
}
.sidebar-logo {
@@ -188,9 +236,9 @@ async function changePassword() {
width: 36px;
height: 36px;
border-radius: 10px;
background: linear-gradient(135deg, rgba(255,193,7,0.15), rgba(255,193,7,0.05));
border: 1px solid rgba(255,193,7,0.2);
color: var(--amber-400);
background: #18181b;
border: 1px solid #27272a;
color: #fafafa;
flex-shrink: 0;
}
@@ -202,60 +250,80 @@ async function changePassword() {
letter-spacing: -0.3px;
}
/* ── Nav items ── */
/* ── Nav items (el-menu) ── */
.sidebar-nav {
flex: 1;
padding: 0 12px;
display: flex;
flex-direction: column;
gap: 2px;
border-right: none;
overflow: hidden;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
.sidebar-nav :deep(.el-menu-item),
.sidebar-nav :deep(.el-sub-menu__title) {
height: 42px;
line-height: 42px;
border-radius: 10px;
color: var(--text-sidebar);
text-decoration: none;
margin-bottom: 2px;
font-size: 14px;
font-weight: 500;
transition: all 0.2s var(--ease-out);
}
.sidebar-nav :deep(.el-menu-item:hover),
.sidebar-nav :deep(.el-sub-menu__title:hover) {
background: rgba(255,255,255,0.06) !important;
}
.sidebar-nav :deep(.el-menu-item.is-active) {
background: rgba(255,255,255,0.1) !important;
color: #fafafa !important;
position: relative;
}
.nav-item:hover {
background: rgba(255,255,255,0.06);
color: var(--text-sidebar-active);
}
.nav-item.active {
background: rgba(255,193,7,0.1);
color: var(--amber-400);
}
.nav-item.active::before {
.sidebar-nav :deep(.el-menu-item.is-active)::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 20px;
height: 18px;
border-radius: 0 3px 3px 0;
background: var(--amber-400);
background: #fafafa;
}
.nav-label {
white-space: nowrap;
.sidebar-nav :deep(.el-sub-menu .el-menu-item) {
padding-left: 52px !important;
height: 38px;
line-height: 38px;
font-size: 13px;
}
.sidebar-nav :deep(.el-sub-menu .el-menu) {
background: rgba(255,255,255,0.03) !important;
border-radius: 8px;
margin: 2px 0;
}
.sidebar-nav :deep(.el-sub-menu__icon-arrow) {
color: var(--text-sidebar);
}
.sidebar-nav :deep(.el-menu--collapse .el-sub-menu__title span),
.sidebar-nav :deep(.el-menu--collapse .el-sub-menu__title .el-sub-menu__icon-arrow) {
display: none;
}
.sidebar-nav :deep(.el-menu--collapse .el-menu-item .el-icon),
.sidebar-nav :deep(.el-menu--collapse .el-sub-menu__title .el-icon) {
margin: 0;
}
.nav-badge {
margin-left: auto;
font-size: 11px;
background: var(--amber-400);
color: #000;
background: var(--primary);
color: #fff;
padding: 1px 6px;
border-radius: 8px;
font-weight: 600;
@@ -264,7 +332,7 @@ async function changePassword() {
/* ── Footer ── */
.sidebar-footer {
padding: 12px;
border-top: 1px solid rgba(255,255,255,0.06);
border-top: 1px solid #18181b;
}
.collapse-btn {
@@ -301,8 +369,8 @@ async function changePassword() {
align-items: center;
justify-content: space-between;
padding: 0 28px;
background: #fff;
border-bottom: 1px solid var(--border-light);
background: #ffffff;
border-bottom: 1px solid #e4e4e7;
flex-shrink: 0;
z-index: 10;
}
@@ -332,7 +400,7 @@ async function changePassword() {
width: 32px;
height: 32px;
border-radius: 8px;
background: linear-gradient(135deg, var(--primary), #7c3aed);
background: #18181b;
color: #fff;
display: flex;
align-items: center;