@
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:
@@ -50,6 +50,25 @@ const router = createRouter({
|
||||
name: 'Logs',
|
||||
component: () => import('../views/Logs.vue'),
|
||||
},
|
||||
{
|
||||
path: 'files',
|
||||
redirect: '/files/orders',
|
||||
},
|
||||
{
|
||||
path: 'files/orders',
|
||||
name: 'FilesOrders',
|
||||
component: () => import('../views/files/Orders.vue'),
|
||||
},
|
||||
{
|
||||
path: 'files/tables',
|
||||
name: 'FilesTables',
|
||||
component: () => import('../views/files/Tables.vue'),
|
||||
},
|
||||
{
|
||||
path: 'files/images',
|
||||
name: 'FilesImages',
|
||||
component: () => import('../views/files/Images.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,53 +1,59 @@
|
||||
/* ═══════════════════════════════════════════
|
||||
益选 OCR — Industrial Command Center Theme
|
||||
益选 OCR — shadcn/ui Theme
|
||||
Clean · Minimal · Zinc palette
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,500;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
:root {
|
||||
/* Core palette */
|
||||
--bg-dark: #0f1117;
|
||||
--bg-sidebar: #161922;
|
||||
/* ── Backgrounds ── */
|
||||
--bg-page: #fafafa;
|
||||
--bg-card: #ffffff;
|
||||
--bg-page: #f0f2f5;
|
||||
--bg-hover: #f7f8fa;
|
||||
--bg-sidebar: #09090b;
|
||||
--bg-hover: #f4f4f5;
|
||||
--bg-dark: #09090b;
|
||||
|
||||
/* Amber accent system */
|
||||
--amber-50: #fff8e1;
|
||||
--amber-100: #ffecb3;
|
||||
--amber-400: #ffca28;
|
||||
--amber-500: #ffc107;
|
||||
--amber-600: #ffb300;
|
||||
|
||||
/* Semantic */
|
||||
--primary: #2563eb;
|
||||
--primary-hover: #1d4ed8;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
/* ── Semantic colors (shadcn zinc) ── */
|
||||
--primary: #18181b;
|
||||
--primary-hover: #27272a;
|
||||
--primary-active: #09090b;
|
||||
--primary-light: #f5f5f5;
|
||||
--success: #22c55e;
|
||||
--success-light: #f0fdf4;
|
||||
--warning: #f97316;
|
||||
--warning-light: #fff7ed;
|
||||
--danger: #ef4444;
|
||||
--info: #6366f1;
|
||||
--danger-light: #fef2f2;
|
||||
--info: #18181b;
|
||||
|
||||
/* Text */
|
||||
--text-primary: #111827;
|
||||
--text-secondary: #6b7280;
|
||||
--text-muted: #9ca3af;
|
||||
--text-inverse: #e5e7eb;
|
||||
--text-sidebar: #94a3b8;
|
||||
--text-sidebar-active: #ffffff;
|
||||
/* ── Text ── */
|
||||
--text-primary: #18181b;
|
||||
--text-secondary: #525252;
|
||||
--text-muted: #a1a1aa;
|
||||
--text-inverse: #ffffff;
|
||||
--text-sidebar: #a1a1aa;
|
||||
--text-sidebar-active: #fafafa;
|
||||
|
||||
/* Borders & shadows */
|
||||
--border-light: #e5e7eb;
|
||||
--border-subtle: #f3f4f6;
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
|
||||
--shadow-md: 0 4px 12px rgba(0,0,0,0.06);
|
||||
--shadow-lg: 0 8px 24px rgba(0,0,0,0.08);
|
||||
--shadow-glow: 0 0 20px rgba(255,193,7,0.15);
|
||||
/* ── Borders ── */
|
||||
--border-light: #e4e4e7;
|
||||
--border-subtle: #f4f4f5;
|
||||
--border-focus: #18181b;
|
||||
|
||||
/* Typography */
|
||||
--font-sans: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
/* ── Shadows ── */
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.05);
|
||||
--shadow-md: 0 1px 3px 0 rgba(0,0,0,0.1), 0 1px 2px -1px rgba(0,0,0,0.1);
|
||||
--shadow-lg: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
|
||||
|
||||
/* ── Radius ── */
|
||||
--radius: 10px;
|
||||
--radius-lg: 14px;
|
||||
--radius-sm: 6px;
|
||||
|
||||
/* ── Typography ── */
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
|
||||
/* Transitions */
|
||||
/* ── Transitions ── */
|
||||
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
@@ -66,62 +72,268 @@ body {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Element Plus overrides */
|
||||
/* ═══════════════════════════════════════════
|
||||
Element Plus — shadcn Overrides
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
/* ── Card ── */
|
||||
.el-card {
|
||||
border: 1px solid var(--border-light) !important;
|
||||
border-radius: 12px !important;
|
||||
border-radius: var(--radius) !important;
|
||||
box-shadow: var(--shadow-sm) !important;
|
||||
transition: box-shadow 0.2s var(--ease-out) !important;
|
||||
background: var(--bg-card) !important;
|
||||
transition: box-shadow 0.2s, border-color 0.2s !important;
|
||||
}
|
||||
|
||||
.el-card:hover {
|
||||
box-shadow: var(--shadow-md) !important;
|
||||
border-color: #d4d4d8 !important;
|
||||
}
|
||||
|
||||
/* ── Buttons ── */
|
||||
.el-button {
|
||||
border-radius: var(--radius-sm) !important;
|
||||
font-weight: 500 !important;
|
||||
letter-spacing: 0;
|
||||
transition: all 0.15s ease !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.el-button--default {
|
||||
border: 1px solid #e4e4e7 !important;
|
||||
box-shadow: none !important;
|
||||
background: #ffffff !important;
|
||||
color: #18181b !important;
|
||||
}
|
||||
|
||||
.el-button--default:hover {
|
||||
background: #f4f4f5 !important;
|
||||
border-color: #d4d4d8 !important;
|
||||
color: #18181b !important;
|
||||
}
|
||||
|
||||
.el-button--default:active {
|
||||
background: #e4e4e7 !important;
|
||||
}
|
||||
|
||||
.el-button--primary {
|
||||
background: var(--primary) !important;
|
||||
border-color: var(--primary) !important;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s var(--ease-out);
|
||||
background: #18181b !important;
|
||||
border: 1px solid #18181b !important;
|
||||
box-shadow: none !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.el-button--primary:hover {
|
||||
background: var(--primary-hover) !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(37,99,235,0.3);
|
||||
background: #27272a !important;
|
||||
border-color: #27272a !important;
|
||||
}
|
||||
|
||||
.el-button--primary:active {
|
||||
background: #09090b !important;
|
||||
}
|
||||
|
||||
.el-button--danger {
|
||||
background: #ef4444 !important;
|
||||
border: 1px solid #ef4444 !important;
|
||||
box-shadow: none !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.el-button--danger:hover {
|
||||
background: #dc2626 !important;
|
||||
border-color: #dc2626 !important;
|
||||
}
|
||||
|
||||
.el-button--warning {
|
||||
border: 1px solid #f97316 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.el-button--success {
|
||||
border: 1px solid #22c55e !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Link buttons — no border/shadow */
|
||||
.el-button--primary.is-link,
|
||||
.el-button--danger.is-link,
|
||||
.el-button--default.is-link {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.el-button--primary.is-link:hover {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Small buttons */
|
||||
.el-button--small {
|
||||
border-radius: var(--radius-sm) !important;
|
||||
}
|
||||
|
||||
/* ── Input / Select ── */
|
||||
.el-input__wrapper,
|
||||
.el-select .el-input__wrapper {
|
||||
border: 1px solid #e4e4e7 !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
box-shadow: none !important;
|
||||
background: #ffffff !important;
|
||||
transition: border-color 0.15s ease !important;
|
||||
}
|
||||
|
||||
.el-input__wrapper:hover,
|
||||
.el-select .el-input__wrapper:hover {
|
||||
border-color: #a1a1aa !important;
|
||||
}
|
||||
|
||||
.el-input__wrapper.is-focus,
|
||||
.el-select .el-input__wrapper.is-focus {
|
||||
border-color: #18181b !important;
|
||||
box-shadow: 0 0 0 1px #18181b !important;
|
||||
}
|
||||
|
||||
/* ── Table ── */
|
||||
.el-table {
|
||||
--el-table-border-color: var(--border-light);
|
||||
--el-table-header-bg-color: #fafbfc;
|
||||
border-radius: 8px;
|
||||
--el-table-border-color: #e4e4e7;
|
||||
--el-table-header-bg-color: #fafafa;
|
||||
border: 1px solid #e4e4e7 !important;
|
||||
border-radius: var(--radius) !important;
|
||||
overflow: hidden;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.el-table th.el-table__cell {
|
||||
font-weight: 600;
|
||||
font-weight: 600 !important;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--text-secondary);
|
||||
color: #525252 !important;
|
||||
background: #fafafa !important;
|
||||
}
|
||||
|
||||
.el-table td.el-table__cell {
|
||||
border-bottom: 1px solid #f4f4f5 !important;
|
||||
}
|
||||
|
||||
.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell {
|
||||
background: #fafafa !important;
|
||||
}
|
||||
|
||||
/* ── Dialog ── */
|
||||
.el-dialog {
|
||||
border: 1px solid #e4e4e7 !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
box-shadow: var(--shadow-lg) !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
border-bottom: 1px solid #e4e4e7;
|
||||
padding: 16px 20px !important;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
border-top: 1px solid #e4e4e7;
|
||||
padding: 12px 20px !important;
|
||||
}
|
||||
|
||||
/* ── Tag ── */
|
||||
.el-tag {
|
||||
border: 1px solid transparent !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
font-weight: 500 !important;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.el-tag--success {
|
||||
background: #f0fdf4 !important;
|
||||
color: #16a34a !important;
|
||||
border-color: #bbf7d0 !important;
|
||||
}
|
||||
|
||||
.el-tag--warning {
|
||||
background: #fff7ed !important;
|
||||
color: #ea580c !important;
|
||||
border-color: #fed7aa !important;
|
||||
}
|
||||
|
||||
.el-tag--danger {
|
||||
background: #fef2f2 !important;
|
||||
color: #dc2626 !important;
|
||||
border-color: #fecaca !important;
|
||||
}
|
||||
|
||||
.el-tag--info {
|
||||
background: #f5f5f5 !important;
|
||||
color: #525252 !important;
|
||||
border-color: #e4e4e7 !important;
|
||||
}
|
||||
|
||||
/* ── Progress ── */
|
||||
.el-progress-bar__outer {
|
||||
border-radius: 6px;
|
||||
border: none !important;
|
||||
border-radius: 999px !important;
|
||||
box-shadow: none !important;
|
||||
background: #f4f4f5 !important;
|
||||
height: 8px !important;
|
||||
}
|
||||
|
||||
.el-progress-bar__inner {
|
||||
border-radius: 6px;
|
||||
transition: width 0.4s var(--ease-out);
|
||||
border-radius: 999px !important;
|
||||
transition: width 0.4s ease;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.el-tag {
|
||||
border-radius: 6px;
|
||||
/* ── Dropdown / Popover ── */
|
||||
.el-dropdown-menu,
|
||||
.el-popover.el-popper {
|
||||
border: 1px solid #e4e4e7 !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
box-shadow: var(--shadow-lg) !important;
|
||||
}
|
||||
|
||||
/* ── Menu (sidebar sub-menu) ── */
|
||||
.el-menu {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
.el-sub-menu .el-menu {
|
||||
background: rgba(255,255,255,0.03) !important;
|
||||
}
|
||||
|
||||
.el-sub-menu .el-menu-item {
|
||||
padding-left: 52px !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
/* ── Breadcrumb ── */
|
||||
.el-breadcrumb__inner {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
/* ── Message / Notification ── */
|
||||
.el-message {
|
||||
border: 1px solid #e4e4e7 !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
box-shadow: var(--shadow-lg) !important;
|
||||
}
|
||||
|
||||
/* ── Tooltip ── */
|
||||
.el-tooltip__popper.is-dark {
|
||||
border: none !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Scrollbar
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
@@ -132,19 +344,22 @@ body {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
background: #d4d4d8;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
background: #a1a1aa;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
/* ═══════════════════════════════════════════
|
||||
Animations
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
@@ -157,13 +372,8 @@ body {
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(255,193,7,0); }
|
||||
50% { box-shadow: 0 0 16px 4px rgba(255,193,7,0.15); }
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation: fadeInUp 0.4s var(--ease-out) both;
|
||||
animation: fadeInUp 0.3s var(--ease-out) both;
|
||||
}
|
||||
|
||||
.animate-in-delay-1 { animation-delay: 0.05s; }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -106,70 +106,47 @@ async function handleLogin() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-dark);
|
||||
background: #fafafa;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bg-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
}
|
||||
|
||||
.bg-glow {
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
left: 50%;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
transform: translate(-50%, -50%);
|
||||
background: radial-gradient(circle, rgba(255,193,7,0.08) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
padding: 48px 40px;
|
||||
background: rgba(22, 25, 34, 0.8);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 24px 48px rgba(0,0,0,0.4);
|
||||
width: 380px;
|
||||
padding: 40px 36px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e4e4e7;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0,0,0,0.1), 0 1px 2px -1px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.brand {
|
||||
text-align: center;
|
||||
margin-bottom: 36px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
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);
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 14px;
|
||||
background: #f4f4f5;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
font-size: 24px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.brand p {
|
||||
font-size: 14px;
|
||||
color: var(--text-sidebar);
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@@ -178,48 +155,57 @@ async function handleLogin() {
|
||||
}
|
||||
|
||||
.login-form :deep(.el-input__wrapper) {
|
||||
background: rgba(255,255,255,0.06);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 10px;
|
||||
box-shadow: none;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid #e4e4e7 !important;
|
||||
border-radius: 6px !important;
|
||||
box-shadow: none !important;
|
||||
background: #ffffff !important;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.login-form :deep(.el-input__wrapper:hover) {
|
||||
border-color: #a1a1aa !important;
|
||||
}
|
||||
|
||||
.login-form :deep(.el-input__wrapper:hover),
|
||||
.login-form :deep(.el-input__wrapper.is-focus) {
|
||||
border-color: var(--amber-400);
|
||||
background: rgba(255,255,255,0.08);
|
||||
box-shadow: 0 0 0 3px rgba(255,193,7,0.1);
|
||||
border-color: #18181b !important;
|
||||
box-shadow: 0 0 0 1px #18181b !important;
|
||||
}
|
||||
|
||||
.login-form :deep(.el-input__inner) {
|
||||
color: #ffffff;
|
||||
font-size: 15px;
|
||||
color: #18181b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-form :deep(.el-input__inner::placeholder) {
|
||||
color: rgba(255,255,255,0.35);
|
||||
color: #a1a1aa;
|
||||
}
|
||||
|
||||
.login-form :deep(.el-input__prefix .el-icon) {
|
||||
color: rgba(255,255,255,0.4);
|
||||
color: #a1a1aa;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
height: 44px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-radius: 10px !important;
|
||||
background: linear-gradient(135deg, var(--amber-500), var(--amber-600)) !important;
|
||||
border: none !important;
|
||||
color: #000 !important;
|
||||
transition: all 0.25s var(--ease-out);
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
border-radius: 6px !important;
|
||||
background: #18181b !important;
|
||||
border: 1px solid #18181b !important;
|
||||
color: #fff !important;
|
||||
box-shadow: none !important;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(255,193,7,0.3) !important;
|
||||
background: #27272a !important;
|
||||
border-color: #27272a !important;
|
||||
}
|
||||
|
||||
.login-btn:active {
|
||||
background: #09090b !important;
|
||||
}
|
||||
|
||||
.hint {
|
||||
@@ -228,14 +214,13 @@ async function handleLogin() {
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: rgba(255,255,255,0.3);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.hint-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--amber-400);
|
||||
opacity: 0.6;
|
||||
background: #d4d4d8;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
<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="batchPipeline">
|
||||
批量生成采购单 ({{ selected.length }})
|
||||
</el-button>
|
||||
<el-button :disabled="!selected.length" @click="batchOcr">
|
||||
批量OCR
|
||||
</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.input_image || '--' }}</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.result_exists ? '#52C41A' : '#d1d5db'" :size="16">
|
||||
<Right />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="采购单" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span class="file-name secondary">{{ row.result_purchase || '--' }}</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="200" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="pipelineFile(row)">
|
||||
生成采购单
|
||||
</el-button>
|
||||
<el-button link type="primary" size="small" @click="ocrFile(row)">
|
||||
仅OCR
|
||||
</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: 'images', 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 pipelineFile(row: any) {
|
||||
try {
|
||||
const res = await api.post('/processing/pipeline-single', { filename: row.input_image })
|
||||
ElMessage.success(`处理任务已创建: ${res.data.task_id}`)
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.response?.data?.detail || '处理失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function ocrFile(row: any) {
|
||||
try {
|
||||
const res = await api.post('/processing/ocr-single', { filename: row.input_image })
|
||||
ElMessage.success(`OCR任务已创建: ${res.data.task_id}`)
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.response?.data?.detail || 'OCR失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFile(row: any) {
|
||||
const token = localStorage.getItem('token')
|
||||
if (row.result_purchase) {
|
||||
window.open(`/api/files/download/result/${encodeURIComponent(row.result_purchase)}?token=${token}`, '_blank')
|
||||
} else if (row.output_excel) {
|
||||
window.open(`/api/files/download/output/${encodeURIComponent(row.output_excel)}?token=${token}`, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
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 {}
|
||||
}
|
||||
|
||||
async function batchPipeline() {
|
||||
try {
|
||||
const filenames = selected.value.map(r => r.input_image).filter(Boolean)
|
||||
const res = await api.post('/processing/pipeline', { files: filenames })
|
||||
ElMessage.success(`批量处理任务已创建: ${res.data.task_id}`)
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.response?.data?.detail || '处理失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function batchOcr() {
|
||||
try {
|
||||
const filenames = selected.value.map(r => r.input_image).filter(Boolean)
|
||||
const res = await api.post('/processing/ocr-batch', { files: filenames })
|
||||
ElMessage.success(`批量OCR任务已创建: ${res.data.task_id}`)
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.response?.data?.detail || 'OCR失败')
|
||||
}
|
||||
}
|
||||
|
||||
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.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 {}
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,216 @@
|
||||
<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="batchProcess">
|
||||
批量处理 ({{ selected.length }})
|
||||
</el-button>
|
||||
<el-button :disabled="!selected.length" @click="batchDelete">
|
||||
批量删除
|
||||
</el-button>
|
||||
<el-button type="danger" @click="clearAll">
|
||||
删除全部
|
||||
</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="Excel处理文件" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<span class="file-name primary">{{ row.output_excel || '--' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="" width="40" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-icon :color="row.result_exists ? '#52C41A' : '#d1d5db'" :size="16">
|
||||
<Right />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="采购单文件" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<span class="file-name secondary">{{ row.result_purchase || '--' }}</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="processFile(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: 'tables', 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 processFile(row: any) {
|
||||
try {
|
||||
const res = await api.post('/processing/excel-single', { filename: row.output_excel })
|
||||
ElMessage.success(`处理任务已创建: ${res.data.task_id}`)
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.response?.data?.detail || '处理失败')
|
||||
}
|
||||
}
|
||||
|
||||
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 {}
|
||||
}
|
||||
|
||||
async function batchProcess() {
|
||||
try {
|
||||
const filenames = selected.value.map(r => r.output_excel).filter(Boolean)
|
||||
const res = await api.post('/processing/excel', { files: filenames })
|
||||
ElMessage.success(`批量处理任务已创建: ${res.data.task_id}`)
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.response?.data?.detail || '处理失败')
|
||||
}
|
||||
}
|
||||
|
||||
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 {}
|
||||
}
|
||||
|
||||
async function clearAll() {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定清空所有 Excel 处理文件?此操作不可恢复。', '确认')
|
||||
await api.post('/files/clear/output')
|
||||
await api.post('/files/relations/sync')
|
||||
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