|
|
|
@@ -2,7 +2,13 @@
|
|
|
|
|
<div class="dashboard">
|
|
|
|
|
<!-- Top stats row -->
|
|
|
|
|
<div class="stats-row animate-in">
|
|
|
|
|
<div class="stat-card" v-for="stat in stats" :key="stat.label">
|
|
|
|
|
<div
|
|
|
|
|
class="stat-card"
|
|
|
|
|
v-for="stat in stats"
|
|
|
|
|
:key="stat.label"
|
|
|
|
|
:class="{ clickable: stat.route }"
|
|
|
|
|
@click="stat.route && $router.push(stat.route)"
|
|
|
|
|
>
|
|
|
|
|
<div class="stat-icon" :style="{ background: stat.bg }">
|
|
|
|
|
<span class="stat-emoji">{{ stat.emoji }}</span>
|
|
|
|
|
</div>
|
|
|
|
@@ -14,8 +20,65 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="main-grid">
|
|
|
|
|
<!-- Left column -->
|
|
|
|
|
<!-- Left column: Progress + Logs -->
|
|
|
|
|
<div class="col-left">
|
|
|
|
|
<!-- Progress -->
|
|
|
|
|
<div class="card progress-card animate-in animate-in-delay-1">
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
<h3>处理进度</h3>
|
|
|
|
|
<el-tag v-if="currentTask" :type="statusType" size="small" effect="dark">
|
|
|
|
|
{{ statusText }}
|
|
|
|
|
</el-tag>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="currentTask" class="progress-area">
|
|
|
|
|
<div class="progress-bar-wrapper">
|
|
|
|
|
<div class="progress-bar-track">
|
|
|
|
|
<div
|
|
|
|
|
class="progress-bar-fill"
|
|
|
|
|
:style="{ width: currentTask.progress + '%', background: statusColor }"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="progress-meta">
|
|
|
|
|
<span class="progress-pct" :style="{ color: statusColor }">{{ currentTask.progress }}%</span>
|
|
|
|
|
<span class="progress-msg">{{ currentTask.message }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else class="empty-state">
|
|
|
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#d1d5db" stroke-width="1">
|
|
|
|
|
<circle cx="12" cy="12" r="10"/>
|
|
|
|
|
<polyline points="12,6 12,12 16,14"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<p>等待任务启动</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Logs -->
|
|
|
|
|
<div class="card log-card animate-in animate-in-delay-2">
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
<h3>处理日志</h3>
|
|
|
|
|
<el-button size="small" link @click="clearLogs">清空</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<div ref="logBox" class="log-box">
|
|
|
|
|
<div v-if="logs.length === 0" class="empty-state small">
|
|
|
|
|
<p>暂无日志</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
v-for="(line, i) in logs"
|
|
|
|
|
:key="i"
|
|
|
|
|
class="log-line"
|
|
|
|
|
:class="logCls(line)"
|
|
|
|
|
>
|
|
|
|
|
<span class="log-time">{{ fmtTime(i) }}</span>
|
|
|
|
|
{{ line }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Right column: Upload + Actions -->
|
|
|
|
|
<div class="col-right">
|
|
|
|
|
<!-- Upload zone -->
|
|
|
|
|
<div class="card animate-in animate-in-delay-1">
|
|
|
|
|
<div class="card-head">
|
|
|
|
@@ -68,9 +131,9 @@
|
|
|
|
|
<h3>快捷操作</h3>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="action-grid">
|
|
|
|
|
<button class="action-btn primary" @click="runPipeline" :disabled="processing">
|
|
|
|
|
<div class="action-icon">
|
|
|
|
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
|
|
|
<button class="action-btn" @click="runPipeline" :disabled="processing">
|
|
|
|
|
<div class="action-icon secondary">
|
|
|
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
|
|
|
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
@@ -100,63 +163,6 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Right column: Progress + Logs -->
|
|
|
|
|
<div class="col-right">
|
|
|
|
|
<!-- Progress -->
|
|
|
|
|
<div class="card progress-card animate-in animate-in-delay-2">
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
<h3>处理进度</h3>
|
|
|
|
|
<el-tag v-if="currentTask" :type="statusType" size="small" effect="dark">
|
|
|
|
|
{{ statusText }}
|
|
|
|
|
</el-tag>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="currentTask" class="progress-area">
|
|
|
|
|
<div class="progress-bar-wrapper">
|
|
|
|
|
<div class="progress-bar-track">
|
|
|
|
|
<div
|
|
|
|
|
class="progress-bar-fill"
|
|
|
|
|
:style="{ width: currentTask.progress + '%', background: statusColor }"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="progress-meta">
|
|
|
|
|
<span class="progress-pct" :style="{ color: statusColor }">{{ currentTask.progress }}%</span>
|
|
|
|
|
<span class="progress-msg">{{ currentTask.message }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else class="empty-state">
|
|
|
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#d1d5db" stroke-width="1">
|
|
|
|
|
<circle cx="12" cy="12" r="10"/>
|
|
|
|
|
<polyline points="12,6 12,12 16,14"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<p>等待任务启动</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Logs -->
|
|
|
|
|
<div class="card log-card animate-in animate-in-delay-3">
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
<h3>处理日志</h3>
|
|
|
|
|
<el-button size="small" link @click="clearLogs">清空</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<div ref="logBox" class="log-box">
|
|
|
|
|
<div v-if="logs.length === 0" class="empty-state small">
|
|
|
|
|
<p>暂无日志</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
v-for="(line, i) in logs"
|
|
|
|
|
:key="i"
|
|
|
|
|
class="log-line"
|
|
|
|
|
:class="logCls(line)"
|
|
|
|
|
>
|
|
|
|
|
<span class="log-time">{{ fmtTime(i) }}</span>
|
|
|
|
|
{{ line }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
@@ -226,24 +232,28 @@ const stats = computed(() => [
|
|
|
|
|
value: detailedStats.value.unprocessed_images,
|
|
|
|
|
emoji: '🖼️',
|
|
|
|
|
bg: '#f4f4f5',
|
|
|
|
|
route: '/files/images',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: '待处理Excel',
|
|
|
|
|
value: detailedStats.value.unprocessed_excel,
|
|
|
|
|
emoji: '📊',
|
|
|
|
|
bg: '#f4f4f5',
|
|
|
|
|
route: '/files/tables',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: '已完成采购单',
|
|
|
|
|
value: detailedStats.value.completed_results,
|
|
|
|
|
emoji: '✅',
|
|
|
|
|
bg: '#f0fdf4',
|
|
|
|
|
route: '/files/orders',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: '已处理总数',
|
|
|
|
|
value: detailedStats.value.total_processed,
|
|
|
|
|
emoji: '📦',
|
|
|
|
|
bg: '#f4f4f5',
|
|
|
|
|
route: null,
|
|
|
|
|
},
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
@@ -314,6 +324,7 @@ function getFileTypeLabel(fileName: string): string {
|
|
|
|
|
async function upload(files: File[]): Promise<void> {
|
|
|
|
|
uploading.value = true
|
|
|
|
|
uploadPct.value = 0
|
|
|
|
|
const uploadedFiles: { name: string; type: string }[] = []
|
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
|
|
|
const file = files[i]
|
|
|
|
|
uploadingName.value = file.name
|
|
|
|
@@ -329,6 +340,7 @@ async function upload(files: File[]): Promise<void> {
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
const typeLabel = getFileTypeLabel(file.name)
|
|
|
|
|
uploadedFiles.push({ name: file.name, type: typeLabel })
|
|
|
|
|
ElMessage.success(`${file.name} → ${typeLabel === 'OCR' ? 'OCR识别队列' : 'Excel处理队列'}`)
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
ElMessage.error(`上传失败: ${file.name}`)
|
|
|
|
@@ -338,6 +350,19 @@ async function upload(files: File[]): Promise<void> {
|
|
|
|
|
uploadingName.value = ''
|
|
|
|
|
uploadPct.value = 0
|
|
|
|
|
refreshStats()
|
|
|
|
|
|
|
|
|
|
// Auto-process: run pipeline for images, excel for Excel files
|
|
|
|
|
if (uploadedFiles.length > 0) {
|
|
|
|
|
const hasImages = uploadedFiles.some(f => f.type === 'OCR')
|
|
|
|
|
const hasExcel = uploadedFiles.some(f => f.type === 'Excel')
|
|
|
|
|
if (hasImages) {
|
|
|
|
|
ElMessage.info('自动启动OCR识别...')
|
|
|
|
|
await doAction('/processing/ocr-batch')
|
|
|
|
|
} else if (hasExcel) {
|
|
|
|
|
ElMessage.info('自动启动Excel处理...')
|
|
|
|
|
await doAction('/processing/excel')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function doAction(endpoint: string): Promise<void> {
|
|
|
|
@@ -384,7 +409,7 @@ onMounted(() => {
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.dashboard {
|
|
|
|
|
max-width: 1400px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Stats row ── */
|
|
|
|
@@ -410,6 +435,15 @@ onMounted(() => {
|
|
|
|
|
border-color: #d4d4d8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-card.clickable {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-card.clickable:hover {
|
|
|
|
|
border-color: var(--primary);
|
|
|
|
|
box-shadow: 0 0 0 1px var(--primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-icon {
|
|
|
|
|
width: 44px;
|
|
|
|
|
height: 44px;
|
|
|
|
@@ -572,7 +606,7 @@ onMounted(() => {
|
|
|
|
|
/* ── Action buttons ── */
|
|
|
|
|
.action-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
grid-template-columns: repeat(1, 1fr);
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -603,31 +637,6 @@ onMounted(() => {
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn.primary {
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
background: #18181b;
|
|
|
|
|
border-color: #18181b;
|
|
|
|
|
color: #fff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn.primary:hover:not(:disabled) {
|
|
|
|
|
background: #27272a;
|
|
|
|
|
border-color: #27272a;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn.primary .action-icon {
|
|
|
|
|
background: rgba(255, 255, 255, 0.15);
|
|
|
|
|
color: #fff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn.primary .action-name {
|
|
|
|
|
color: #fff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-btn.primary .action-desc {
|
|
|
|
|
color: rgba(255, 255, 255, 0.6);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-icon {
|
|
|
|
|
width: 36px;
|
|
|
|
|
height: 36px;
|
|
|
|
@@ -739,8 +748,8 @@ onMounted(() => {
|
|
|
|
|
|
|
|
|
|
.log-box {
|
|
|
|
|
flex: 1;
|
|
|
|
|
max-height: 480px;
|
|
|
|
|
min-height: 200px;
|
|
|
|
|
max-height: 600px;
|
|
|
|
|
min-height: 400px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
background: #09090b;
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|