feat: 重构仪表盘布局并优化交互
This commit is contained in:
@@ -163,8 +163,21 @@ body {
|
|||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-button--primary.is-link {
|
||||||
|
color: #18181b !important;
|
||||||
|
}
|
||||||
|
|
||||||
.el-button--primary.is-link:hover {
|
.el-button--primary.is-link:hover {
|
||||||
background: transparent !important;
|
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 */
|
/* Small buttons */
|
||||||
|
|||||||
@@ -2,7 +2,13 @@
|
|||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
<!-- Top stats row -->
|
<!-- Top stats row -->
|
||||||
<div class="stats-row animate-in">
|
<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 }">
|
<div class="stat-icon" :style="{ background: stat.bg }">
|
||||||
<span class="stat-emoji">{{ stat.emoji }}</span>
|
<span class="stat-emoji">{{ stat.emoji }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,8 +20,65 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-grid">
|
<div class="main-grid">
|
||||||
<!-- Left column -->
|
<!-- Left column: Progress + Logs -->
|
||||||
<div class="col-left">
|
<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 -->
|
<!-- Upload zone -->
|
||||||
<div class="card animate-in animate-in-delay-1">
|
<div class="card animate-in animate-in-delay-1">
|
||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
@@ -68,9 +131,9 @@
|
|||||||
<h3>快捷操作</h3>
|
<h3>快捷操作</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-grid">
|
<div class="action-grid">
|
||||||
<button class="action-btn primary" @click="runPipeline" :disabled="processing">
|
<button class="action-btn" @click="runPipeline" :disabled="processing">
|
||||||
<div class="action-icon">
|
<div class="action-icon secondary">
|
||||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
<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"/>
|
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,63 +163,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -226,24 +232,28 @@ const stats = computed(() => [
|
|||||||
value: detailedStats.value.unprocessed_images,
|
value: detailedStats.value.unprocessed_images,
|
||||||
emoji: '🖼️',
|
emoji: '🖼️',
|
||||||
bg: '#f4f4f5',
|
bg: '#f4f4f5',
|
||||||
|
route: '/files/images',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '待处理Excel',
|
label: '待处理Excel',
|
||||||
value: detailedStats.value.unprocessed_excel,
|
value: detailedStats.value.unprocessed_excel,
|
||||||
emoji: '📊',
|
emoji: '📊',
|
||||||
bg: '#f4f4f5',
|
bg: '#f4f4f5',
|
||||||
|
route: '/files/tables',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '已完成采购单',
|
label: '已完成采购单',
|
||||||
value: detailedStats.value.completed_results,
|
value: detailedStats.value.completed_results,
|
||||||
emoji: '✅',
|
emoji: '✅',
|
||||||
bg: '#f0fdf4',
|
bg: '#f0fdf4',
|
||||||
|
route: '/files/orders',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '已处理总数',
|
label: '已处理总数',
|
||||||
value: detailedStats.value.total_processed,
|
value: detailedStats.value.total_processed,
|
||||||
emoji: '📦',
|
emoji: '📦',
|
||||||
bg: '#f4f4f5',
|
bg: '#f4f4f5',
|
||||||
|
route: null,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -314,6 +324,7 @@ function getFileTypeLabel(fileName: string): string {
|
|||||||
async function upload(files: File[]): Promise<void> {
|
async function upload(files: File[]): Promise<void> {
|
||||||
uploading.value = true
|
uploading.value = true
|
||||||
uploadPct.value = 0
|
uploadPct.value = 0
|
||||||
|
const uploadedFiles: { name: string; type: string }[] = []
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const file = files[i]
|
const file = files[i]
|
||||||
uploadingName.value = file.name
|
uploadingName.value = file.name
|
||||||
@@ -329,6 +340,7 @@ async function upload(files: File[]): Promise<void> {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
const typeLabel = getFileTypeLabel(file.name)
|
const typeLabel = getFileTypeLabel(file.name)
|
||||||
|
uploadedFiles.push({ name: file.name, type: typeLabel })
|
||||||
ElMessage.success(`${file.name} → ${typeLabel === 'OCR' ? 'OCR识别队列' : 'Excel处理队列'}`)
|
ElMessage.success(`${file.name} → ${typeLabel === 'OCR' ? 'OCR识别队列' : 'Excel处理队列'}`)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ElMessage.error(`上传失败: ${file.name}`)
|
ElMessage.error(`上传失败: ${file.name}`)
|
||||||
@@ -338,6 +350,19 @@ async function upload(files: File[]): Promise<void> {
|
|||||||
uploadingName.value = ''
|
uploadingName.value = ''
|
||||||
uploadPct.value = 0
|
uploadPct.value = 0
|
||||||
refreshStats()
|
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> {
|
async function doAction(endpoint: string): Promise<void> {
|
||||||
@@ -384,7 +409,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.dashboard {
|
.dashboard {
|
||||||
max-width: 1400px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Stats row ── */
|
/* ── Stats row ── */
|
||||||
@@ -410,6 +435,15 @@ onMounted(() => {
|
|||||||
border-color: #d4d4d8;
|
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 {
|
.stat-icon {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
@@ -572,7 +606,7 @@ onMounted(() => {
|
|||||||
/* ── Action buttons ── */
|
/* ── Action buttons ── */
|
||||||
.action-grid {
|
.action-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: repeat(1, 1fr);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,31 +637,6 @@ onMounted(() => {
|
|||||||
cursor: not-allowed;
|
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 {
|
.action-icon {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
@@ -739,8 +748,8 @@ onMounted(() => {
|
|||||||
|
|
||||||
.log-box {
|
.log-box {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-height: 480px;
|
max-height: 600px;
|
||||||
min-height: 200px;
|
min-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: #09090b;
|
background: #09090b;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.logs-page {
|
.logs-page {
|
||||||
max-width: 1400px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-row {
|
.stats-row {
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ onMounted(loadData)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.memory-page {
|
.memory-page {
|
||||||
max-width: 1400px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Stats row ── */
|
/* ── Stats row ── */
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.tasks-page {
|
.tasks-page {
|
||||||
max-width: 1400px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-row {
|
.stats-row {
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ onMounted(loadData)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.file-page {
|
.file-page {
|
||||||
max-width: 1400px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.page-header {
|
.page-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ onMounted(loadData)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.file-page {
|
.file-page {
|
||||||
max-width: 1400px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.page-header {
|
.page-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ onMounted(loadData)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.file-page {
|
.file-page {
|
||||||
max-width: 1400px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.page-header {
|
.page-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user