<template>
|
<div class="app-container dashboard-container">
|
<!-- 顶部统计卡片 -->
|
<el-row :gutter="20" style="margin-bottom: 20px;">
|
<el-col :span="4">
|
<el-card shadow="hover" class="stat-card-wrapper">
|
<div class="stat-card">
|
<div class="stat-icon" style="background: #409EFF;">
|
<i class="el-icon-s-claim" />
|
</div>
|
<div class="stat-content">
|
<div class="stat-title">待领样品</div>
|
<div class="stat-value">{{ overviewData.waitReceive || 0 }}</div>
|
</div>
|
</div>
|
</el-card>
|
</el-col>
|
<el-col :span="4">
|
<el-card shadow="hover" class="stat-card-wrapper">
|
<div class="stat-card">
|
<div class="stat-icon" style="background: #E6A23C;">
|
<i class="el-icon-time" />
|
</div>
|
<div class="stat-content">
|
<div class="stat-title">待检样品</div>
|
<div class="stat-value">{{ overviewData.waitInspection || 0 }}</div>
|
</div>
|
</div>
|
</el-card>
|
</el-col>
|
<el-col :span="4">
|
<el-card shadow="hover" class="stat-card-wrapper">
|
<div class="stat-card">
|
<div class="stat-icon" style="background: #909399;">
|
<i class="el-icon-document-checked" />
|
</div>
|
<div class="stat-content">
|
<div class="stat-title">待审核</div>
|
<div class="stat-value">{{ overviewData.waitAudit || 0 }}</div>
|
</div>
|
</div>
|
</el-card>
|
</el-col>
|
<el-col :span="4">
|
<el-card shadow="hover" class="stat-card-wrapper">
|
<div class="stat-card">
|
<div class="stat-icon" style="background: #F56C6C;">
|
<i class="el-icon-document" />
|
</div>
|
<div class="stat-content">
|
<div class="stat-title">待编制报告</div>
|
<div class="stat-value">{{ overviewData.waitReport || 0 }}</div>
|
</div>
|
</div>
|
</el-card>
|
</el-col>
|
<el-col :span="4">
|
<el-card shadow="hover" class="stat-card-wrapper">
|
<div class="stat-card">
|
<div class="stat-icon" style="background: #67C23A;">
|
<i class="el-icon-circle-check" />
|
</div>
|
<div class="stat-content">
|
<div class="stat-title">今日完成</div>
|
<div class="stat-value">{{ overviewData.todayFinished || 0 }}</div>
|
</div>
|
</div>
|
</el-card>
|
</el-col>
|
<el-col :span="4">
|
<el-card shadow="hover" class="voice-control" @click.native="toggleVoice">
|
<div class="voice-content">
|
<i :class="voiceEnabled ? 'el-icon-microphone' : 'el-icon-turn-off-microphone'" />
|
<span>{{ voiceEnabled ? '语音播报中' : '语音已关闭' }}</span>
|
</div>
|
</el-card>
|
</el-col>
|
</el-row>
|
|
<!-- 图表区域 -->
|
<el-row :gutter="20" style="margin-bottom: 20px;">
|
<!-- 历史15天检测任务 -->
|
<el-col :span="16">
|
<el-card shadow="hover">
|
<div slot="header">
|
<span>近15天检测任务</span>
|
<el-tag type="info" size="mini" style="margin-left: 10px;">实时更新</el-tag>
|
</div>
|
<Echart
|
:xAxis="historyXAxis"
|
:yAxis="historyYAxis"
|
:series="historySeries"
|
:tooltip="{ trigger: 'axis' }"
|
:legend="{ data: ['检测任务', '完成任务'] }"
|
:grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }"
|
:chartStyle="{ height: '280px' }"
|
/>
|
</el-card>
|
</el-col>
|
<!-- 未来15天任务预览 -->
|
<el-col :span="8">
|
<el-card shadow="hover" class="future-task-card">
|
<div slot="header">未来15天任务</div>
|
<div class="future-task-list">
|
<div v-for="(item, index) in futureTasks" :key="index" class="future-task-item">
|
<span class="task-date">{{ item.date }}</span>
|
<el-progress :percentage="item.progress" :stroke-width="10" style="flex: 1; margin: 0 10px;" />
|
<span class="task-count">{{ item.taskCount }}个任务</span>
|
</div>
|
</div>
|
</el-card>
|
</el-col>
|
</el-row>
|
|
<el-row :gutter="20" style="margin-bottom: 20px;">
|
<!-- 提交排行榜 -->
|
<el-col :span="12">
|
<el-card shadow="hover">
|
<div slot="header">
|
<span>近15天提交排行</span>
|
<el-radio-group v-model="rankingType" size="mini" style="float: right;" @change="getRankingData">
|
<el-radio-button label="record">原始记录</el-radio-button>
|
<el-radio-button label="report">报告</el-radio-button>
|
</el-radio-group>
|
</div>
|
<div class="ranking-list">
|
<div v-for="(item, index) in rankingData" :key="index" class="ranking-item">
|
<span class="ranking-index" :class="getRankingClass(index)">{{ index + 1 }}</span>
|
<span class="ranking-name">{{ item.userName }}</span>
|
<el-progress :percentage="item.percentage" :stroke-width="15" :show-text="false" style="flex: 1; margin: 0 15px;" />
|
<span class="ranking-count">{{ item.count }}份</span>
|
</div>
|
</div>
|
</el-card>
|
</el-col>
|
<!-- 检验结果统计 -->
|
<el-col :span="12">
|
<el-card shadow="hover">
|
<div slot="header">近30天检验结果</div>
|
<el-row :gutter="20">
|
<el-col :span="8">
|
<div class="result-card">
|
<div class="result-title">原材料</div>
|
<Echart
|
:series="rawMaterialSeries"
|
:tooltip="{ trigger: 'item', formatter: '{b}: {c} ({d}%)' }"
|
:chartStyle="{ height: '200px' }"
|
/>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="result-card">
|
<div class="result-title">半成品</div>
|
<Echart
|
:series="semiProductSeries"
|
:tooltip="{ trigger: 'item', formatter: '{b}: {c} ({d}%)' }"
|
:chartStyle="{ height: '200px' }"
|
/>
|
</div>
|
</el-col>
|
<el-col :span="8">
|
<div class="result-card">
|
<div class="result-title">成品</div>
|
<Echart
|
:series="finishedProductSeries"
|
:tooltip="{ trigger: 'item', formatter: '{b}: {c} ({d}%)' }"
|
:chartStyle="{ height: '200px' }"
|
/>
|
</div>
|
</el-col>
|
</el-row>
|
</el-card>
|
</el-col>
|
</el-row>
|
|
<!-- 紧急事项播报 -->
|
<el-card shadow="hover">
|
<div slot="header">
|
<span>紧急事项</span>
|
<el-button type="text" style="float: right;" @click="refreshVoiceQueue">
|
<i class="el-icon-refresh" /> 刷新
|
</el-button>
|
</div>
|
<el-table :data="urgentItems" border style="width: 100%" max-height="200">
|
<el-table-column prop="type" label="类型" width="120" />
|
<el-table-column prop="content" label="内容" />
|
<el-table-column prop="time" label="时间" width="180" />
|
<el-table-column prop="level" label="级别" width="100">
|
<template slot-scope="scope">
|
<el-tag :type="getLevelType(scope.row.level)">{{ scope.row.level }}</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="100">
|
<template slot-scope="scope">
|
<el-button type="text" size="mini" @click="speakContent(scope.row.content)">播报</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-card>
|
</div>
|
</template>
|
|
<script>
|
import Echart from '@/components/echarts/echarts.vue'
|
import {
|
getOverview,
|
getHistory15Days,
|
getFuture15Days,
|
getRanking,
|
getInsResult,
|
getVoiceQueue
|
} from '@/api/report/dashboard'
|
|
export default {
|
name: 'TestHall',
|
components: { Echart },
|
data() {
|
return {
|
overviewData: {},
|
futureTasks: [],
|
rankingData: [],
|
rankingType: 'record',
|
urgentItems: [],
|
voiceEnabled: true,
|
refreshTimer: null,
|
// 历史15天图表配置
|
historyXAxis: [{ type: 'category', data: [] }],
|
historyYAxis: [{ type: 'value' }],
|
historySeries: [
|
{ name: '检测任务', type: 'bar', data: [] },
|
{ name: '完成任务', type: 'line', data: [] }
|
],
|
// 检验结果饼图配置
|
rawMaterialSeries: [{
|
type: 'pie',
|
radius: ['50%', '70%'],
|
data: [
|
{ name: '合格', value: 0, itemStyle: { color: '#67C23A' } },
|
{ name: '不合格', value: 0, itemStyle: { color: '#F56C6C' } }
|
]
|
}],
|
semiProductSeries: [{
|
type: 'pie',
|
radius: ['50%', '70%'],
|
data: [
|
{ name: '合格', value: 0, itemStyle: { color: '#67C23A' } },
|
{ name: '不合格', value: 0, itemStyle: { color: '#F56C6C' } }
|
]
|
}],
|
finishedProductSeries: [{
|
type: 'pie',
|
radius: ['50%', '70%'],
|
data: [
|
{ name: '合格', value: 0, itemStyle: { color: '#67C23A' } },
|
{ name: '不合格', value: 0, itemStyle: { color: '#F56C6C' } }
|
]
|
}]
|
}
|
},
|
mounted() {
|
this.initDashboard()
|
this.startAutoRefresh()
|
},
|
beforeDestroy() {
|
this.stopAutoRefresh()
|
},
|
methods: {
|
// 初始化看板数据
|
async initDashboard() {
|
await Promise.all([
|
this.getOverviewData(),
|
this.getHistoryData(),
|
this.getFutureData(),
|
this.getRankingData(),
|
this.getInsResultData(),
|
this.refreshVoiceQueue()
|
])
|
},
|
// 获取概览数据
|
getOverviewData() {
|
return getOverview().then(res => {
|
this.overviewData = res.data || {}
|
})
|
},
|
// 获取历史15天数据
|
getHistoryData() {
|
return getHistory15Days().then(res => {
|
this.historyXAxis[0].data = res.data.dates || []
|
this.historySeries[0].data = res.data.taskCounts || []
|
this.historySeries[1].data = res.data.finishCounts || []
|
})
|
},
|
// 获取未来15天任务
|
getFutureData() {
|
return getFuture15Days().then(res => {
|
this.futureTasks = res.data || []
|
})
|
},
|
// 获取提交排行
|
getRankingData() {
|
return getRanking({ type: this.rankingType }).then(res => {
|
this.rankingData = res.data || []
|
})
|
},
|
// 获取检验结果统计
|
getInsResultData() {
|
return getInsResult().then(res => {
|
if (res.data.rawMaterial) {
|
this.rawMaterialSeries[0].data[0].value = res.data.rawMaterial.passCount || 0
|
this.rawMaterialSeries[0].data[1].value = res.data.rawMaterial.unpassCount || 0
|
}
|
if (res.data.semiProduct) {
|
this.semiProductSeries[0].data[0].value = res.data.semiProduct.passCount || 0
|
this.semiProductSeries[0].data[1].value = res.data.semiProduct.unpassCount || 0
|
}
|
if (res.data.finishedProduct) {
|
this.finishedProductSeries[0].data[0].value = res.data.finishedProduct.passCount || 0
|
this.finishedProductSeries[0].data[1].value = res.data.finishedProduct.unpassCount || 0
|
}
|
})
|
},
|
// 刷新语音播报队列
|
refreshVoiceQueue() {
|
return getVoiceQueue().then(res => {
|
this.urgentItems = res.data || []
|
})
|
},
|
// 切换语音播报状态
|
toggleVoice() {
|
this.voiceEnabled = !this.voiceEnabled
|
if (this.voiceEnabled) {
|
this.$message.success('语音播报已开启')
|
} else {
|
this.$message.info('语音播报已关闭')
|
}
|
},
|
// 语音播报内容
|
speakContent(content) {
|
if ('speechSynthesis' in window) {
|
const utterance = new SpeechSynthesisUtterance(content)
|
utterance.lang = 'zh-CN'
|
speechSynthesis.speak(utterance)
|
} else {
|
this.$message.warning('当前浏览器不支持语音播报')
|
}
|
},
|
// 开启自动刷新
|
startAutoRefresh() {
|
this.refreshTimer = setInterval(() => {
|
this.initDashboard()
|
}, 30000)
|
},
|
// 停止自动刷新
|
stopAutoRefresh() {
|
if (this.refreshTimer) {
|
clearInterval(this.refreshTimer)
|
this.refreshTimer = null
|
}
|
},
|
// 获取排行样式类
|
getRankingClass(index) {
|
if (index === 0) return 'first'
|
if (index === 1) return 'second'
|
if (index === 2) return 'third'
|
return ''
|
},
|
// 获取级别标签类型
|
getLevelType(level) {
|
const map = { '紧急': 'danger', '重要': 'warning', '普通': 'info' }
|
return map[level] || 'info'
|
}
|
}
|
}
|
</script>
|
|
<style scoped>
|
.dashboard-container {
|
background: #f0f2f5;
|
min-height: calc(100vh - 84px);
|
}
|
.stat-card-wrapper {
|
cursor: pointer;
|
}
|
.stat-card {
|
display: flex;
|
align-items: center;
|
padding: 10px;
|
}
|
.stat-icon {
|
width: 50px;
|
height: 50px;
|
border-radius: 8px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
.stat-icon i {
|
font-size: 24px;
|
color: #fff;
|
}
|
.stat-content {
|
margin-left: 10px;
|
}
|
.stat-title {
|
font-size: 12px;
|
color: #909399;
|
}
|
.stat-value {
|
font-size: 22px;
|
font-weight: bold;
|
color: #303133;
|
margin-top: 5px;
|
}
|
.voice-control {
|
cursor: pointer;
|
}
|
.voice-content {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
padding: 15px;
|
color: #409EFF;
|
}
|
.voice-content i {
|
font-size: 32px;
|
}
|
.voice-content span {
|
font-size: 12px;
|
margin-top: 8px;
|
}
|
.future-task-card {
|
height: 380px;
|
overflow: auto;
|
}
|
.future-task-list {
|
padding: 10px;
|
}
|
.future-task-item {
|
display: flex;
|
align-items: center;
|
margin-bottom: 15px;
|
}
|
.task-date {
|
width: 80px;
|
font-size: 12px;
|
color: #606266;
|
}
|
.task-count {
|
width: 60px;
|
text-align: right;
|
font-size: 12px;
|
color: #909399;
|
}
|
.ranking-list {
|
padding: 10px;
|
}
|
.ranking-item {
|
display: flex;
|
align-items: center;
|
margin-bottom: 12px;
|
}
|
.ranking-index {
|
width: 24px;
|
height: 24px;
|
border-radius: 50%;
|
background: #909399;
|
color: #fff;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
font-size: 12px;
|
}
|
.ranking-index.first { background: #FFD700; }
|
.ranking-index.second { background: #C0C0C0; }
|
.ranking-index.third { background: #CD7F32; }
|
.ranking-name {
|
width: 80px;
|
margin-left: 10px;
|
font-size: 14px;
|
}
|
.ranking-count {
|
width: 50px;
|
text-align: right;
|
font-size: 14px;
|
color: #606266;
|
}
|
.result-card {
|
text-align: center;
|
}
|
.result-title {
|
font-size: 14px;
|
color: #606266;
|
margin-bottom: 10px;
|
}
|
</style>
|