feat: 重构仪表盘布局并优化交互

This commit is contained in:
2026-05-05 16:05:59 +08:00
parent 0721ed099c
commit c18039f790
8 changed files with 119 additions and 97 deletions
+13
View File
@@ -163,8 +163,21 @@ body {
background: transparent !important;
}
.el-button--primary.is-link {
color: #18181b !important;
}
.el-button--primary.is-link:hover {
background: transparent !important;
color: #27272a !important;
}
.el-button--danger.is-link {
color: #ef4444 !important;
}
.el-button--danger.is-link:hover {
color: #dc2626 !important;
}
/* Small buttons */
+100 -91
View File
@@ -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);
+1 -1
View File
@@ -159,7 +159,7 @@ onMounted(() => {
<style scoped>
.logs-page {
max-width: 1400px;
width: 100%;
}
.stats-row {
+1 -1
View File
@@ -237,7 +237,7 @@ onMounted(loadData)
<style scoped>
.memory-page {
max-width: 1400px;
width: 100%;
}
/* ── Stats row ── */
+1 -1
View File
@@ -215,7 +215,7 @@ onMounted(() => {
<style scoped>
.tasks-page {
max-width: 1400px;
width: 100%;
}
.stats-row {
+1 -1
View File
@@ -211,7 +211,7 @@ onMounted(loadData)
<style scoped>
.file-page {
max-width: 1400px;
width: 100%;
}
.page-header {
display: flex;
+1 -1
View File
@@ -174,7 +174,7 @@ onMounted(loadData)
<style scoped>
.file-page {
max-width: 1400px;
width: 100%;
}
.page-header {
display: flex;
+1 -1
View File
@@ -178,7 +178,7 @@ onMounted(loadData)
<style scoped>
.file-page {
max-width: 1400px;
width: 100%;
}
.page-header {
display: flex;