624 lines
19 KiB
Vue
624 lines
19 KiB
Vue
<template>
|
||
<div class="hfnf-dashboard">
|
||
<!-- 顶部总览横幅 -->
|
||
<div class="summary-banner">
|
||
<div class="banner-left">
|
||
<div class="banner-icon">
|
||
<img src="/media/logo2.png" class="banner-logo" />
|
||
</div>
|
||
<div class="banner-info">
|
||
<h2>水泥生产数据采集总览</h2>
|
||
<p>覆盖 {{ factories.length }} 家工厂 · 实时数据监控</p>
|
||
</div>
|
||
</div>
|
||
<div class="banner-right">
|
||
<div class="total-block">
|
||
<span class="total-label">数据总量</span>
|
||
<span class="total-value">{{ formatNumber(totalCount) }}</span>
|
||
<span class="total-unit">条</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 工厂数据卡片 -->
|
||
<el-row :gutter="16" class="factory-row">
|
||
<el-col :xs="24" :sm="12" :md="6" v-for="(item, index) in factories" :key="item.name">
|
||
<div class="factory-card" :style="{ borderTopColor: item.color }">
|
||
<div class="card-header">
|
||
<div class="card-icon" :style="{ backgroundColor: item.bgColor }">
|
||
<el-icon :size="24" :color="item.color"><OfficeBuilding /></el-icon>
|
||
</div>
|
||
<span class="card-title">{{ item.name }}</span>
|
||
</div>
|
||
<div class="card-body">
|
||
<span class="card-count">{{ formatNumber(item.count) }}</span>
|
||
<span class="card-unit">条</span>
|
||
</div>
|
||
<div class="card-footer">
|
||
<el-progress
|
||
:percentage="item.percentage"
|
||
:color="item.color"
|
||
:stroke-width="6"
|
||
:show-text="false"
|
||
/>
|
||
<div class="card-meta">
|
||
<span class="card-percent">占比 {{ item.percentage.toFixed(1) }}%</span>
|
||
<span class="card-rank">#{{ index + 1 }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- 图表区域 -->
|
||
<el-row :gutter="16" class="chart-row">
|
||
<el-col :xs="24" :md="14">
|
||
<div class="chart-card">
|
||
<div class="chart-card-header">
|
||
<h3>各工厂数据量对比</h3>
|
||
</div>
|
||
<div ref="barChartRef" class="chart-container"></div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :xs="24" :md="10">
|
||
<div class="chart-card">
|
||
<div class="chart-card-header">
|
||
<h3>数据占比分布</h3>
|
||
</div>
|
||
<div ref="pieChartRef" class="chart-container"></div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- 底部信息区域 -->
|
||
<el-row :gutter="16" class="info-row">
|
||
<el-col :xs="24" :md="12">
|
||
<div class="info-card">
|
||
<div class="chart-card-header">
|
||
<h3>工厂详细数据</h3>
|
||
</div>
|
||
<el-table :data="factories" stripe style="width: 100%" size="large">
|
||
<el-table-column type="index" label="#" width="50" />
|
||
<el-table-column prop="name" label="工厂名称" min-width="140">
|
||
<template #default="{ row }">
|
||
<div style="display: flex; align-items: center; gap: 8px;">
|
||
<span class="dot" :style="{ backgroundColor: row.color }"></span>
|
||
{{ row.name }}
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="数据量" min-width="140" align="right">
|
||
<template #default="{ row }">
|
||
<span style="font-weight: 600; font-family: 'DIN Alternate', monospace;">{{ formatNumber(row.count) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="占比" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :color="row.bgColor" :style="{ color: row.color, borderColor: row.color }" size="small">
|
||
{{ row.percentage.toFixed(1) }}%
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</el-col>
|
||
<el-col :xs="24" :md="12">
|
||
<div class="info-card">
|
||
<div class="chart-card-header">
|
||
<h3>系统信息</h3>
|
||
</div>
|
||
<div class="sys-info-list">
|
||
<div class="sys-info-item">
|
||
<div class="sys-info-icon" style="background: #ecf5ff;">
|
||
<el-icon :size="20" color="#409eff"><Monitor /></el-icon>
|
||
</div>
|
||
<div class="sys-info-content">
|
||
<span class="sys-info-label">系统名称</span>
|
||
<span class="sys-info-value">水泥生产数据人工智能采集融合系统</span>
|
||
</div>
|
||
</div>
|
||
<div class="sys-info-item">
|
||
<div class="sys-info-icon" style="background: #f0f9eb;">
|
||
<el-icon :size="20" color="#67c23a"><Connection /></el-icon>
|
||
</div>
|
||
<div class="sys-info-content">
|
||
<span class="sys-info-label">接入工厂</span>
|
||
<span class="sys-info-value">{{ factories.length }} 家</span>
|
||
</div>
|
||
</div>
|
||
<div class="sys-info-item">
|
||
<div class="sys-info-icon" style="background: #fdf6ec;">
|
||
<el-icon :size="20" color="#e6a23c"><Coin /></el-icon>
|
||
</div>
|
||
<div class="sys-info-content">
|
||
<span class="sys-info-label">数据总量</span>
|
||
<span class="sys-info-value">{{ formatNumber(totalCount) }} 条</span>
|
||
</div>
|
||
</div>
|
||
<div class="sys-info-item">
|
||
<div class="sys-info-icon" style="background: #fef0f0;">
|
||
<el-icon :size="20" color="#f56c6c"><DataLine /></el-icon>
|
||
</div>
|
||
<div class="sys-info-content">
|
||
<span class="sys-info-label">平均数据量</span>
|
||
<span class="sys-info-value">{{ formatNumber(Math.round(totalCount / factories.length)) }} 条/厂</span>
|
||
</div>
|
||
</div>
|
||
<div class="sys-info-item">
|
||
<div class="sys-info-icon" style="background: #f4f4f5;">
|
||
<el-icon :size="20" color="#909399"><TrendCharts /></el-icon>
|
||
</div>
|
||
<div class="sys-info-content">
|
||
<span class="sys-info-label">最大采集量</span>
|
||
<span class="sys-info-value">{{ maxFactory.name }}({{ formatNumber(maxFactory.count) }})</span>
|
||
</div>
|
||
</div>
|
||
<div class="sys-info-item">
|
||
<div class="sys-info-icon" style="background: #ecf5ff;">
|
||
<el-icon :size="20" color="#409eff"><View /></el-icon>
|
||
</div>
|
||
<div class="sys-info-content">
|
||
<span class="sys-info-label">查看详情</span>
|
||
<el-button type="primary" link @click="goDetail">进入测点数据明细 →</el-button>
|
||
</div>
|
||
</div>
|
||
<div class="sys-info-item">
|
||
<div class="sys-info-icon" style="background: #fdf6ec;">
|
||
<el-icon :size="20" color="#e6a23c"><Document /></el-icon>
|
||
</div>
|
||
<div class="sys-info-content">
|
||
<span class="sys-info-label">数据标准</span>
|
||
<el-button type="primary" link @click="goStandard">查看数据标准模板 →</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import * as echarts from 'echarts'
|
||
import API from '@/api'
|
||
|
||
const router = useRouter()
|
||
const barChartRef = ref(null)
|
||
const pieChartRef = ref(null)
|
||
let barChart = null
|
||
let pieChart = null
|
||
|
||
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c']
|
||
const bgColors = ['#ecf5ff', '#f0f9eb', '#fdf6ec', '#fef0f0']
|
||
|
||
// 各工厂与数据表的映射关系
|
||
const factoryDefs = [
|
||
{ name: '合肥南方', match: (t) => t === 'mplogx' }, // 合肥南方
|
||
{ name: '中联万吨', match: (t) => t === 'mplogx_xzzl' }, // 中联万吨
|
||
{ name: '铜梁水泥', match: (t) => t === 'mplogx_tlxn' }, // 铜梁水泥
|
||
{ name: '槐坎南方水泥', match: (t) => t.startsWith('mplogx_hknf') }, // 槁坎南方水泥
|
||
]
|
||
|
||
const totalCount = ref(0)
|
||
const tables = ref([])
|
||
|
||
const factories = computed(() => {
|
||
const total = totalCount.value
|
||
return factoryDefs.map((f, i) => {
|
||
// 同一工厂可能对应多张数据表,按表名匹配后累加行数
|
||
const count = tables.value
|
||
.filter((t) => f.match(t.table_name))
|
||
.reduce((sum, t) => sum + (t.approximate_row_count || 0), 0)
|
||
return {
|
||
name: f.name,
|
||
count,
|
||
color: colors[i],
|
||
bgColor: bgColors[i],
|
||
percentage: total > 0 ? (count / total) * 100 : 0,
|
||
}
|
||
})
|
||
})
|
||
|
||
const maxFactory = computed(() => {
|
||
if (factories.value.length === 0) return { name: '-', count: 0 }
|
||
return factories.value.reduce((a, b) => (a.count > b.count ? a : b))
|
||
})
|
||
|
||
const formatNumber = (num) => {
|
||
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||
}
|
||
|
||
const goDetail = () => {
|
||
router.push('/hfnf_mplogx')
|
||
}
|
||
|
||
const goStandard = () => {
|
||
router.push('/cement_data_template')
|
||
}
|
||
|
||
const initBarChart = () => {
|
||
if (!barChartRef.value) return
|
||
barChart = echarts.init(barChartRef.value)
|
||
barChart.setOption({
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
formatter: (params) => {
|
||
const p = params[0]
|
||
return `${p.name}<br/>数据量:<b>${formatNumber(p.value)}</b> 条`
|
||
},
|
||
},
|
||
grid: { left: 20, right: 30, top: 20, bottom: 30, containLabel: true },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: factories.value.map((f) => f.name),
|
||
axisLabel: { color: '#666', fontSize: 13 },
|
||
axisLine: { lineStyle: { color: '#e4e7ed' } },
|
||
axisTick: { show: false },
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLabel: {
|
||
color: '#999',
|
||
formatter: (v) => (v >= 1e8 ? (v / 1e8).toFixed(1) + '亿' : (v / 1e4).toFixed(0) + '万'),
|
||
},
|
||
splitLine: { lineStyle: { color: '#f0f0f0' } },
|
||
},
|
||
series: [
|
||
{
|
||
type: 'bar',
|
||
data: factories.value.map((f) => ({
|
||
value: f.count,
|
||
itemStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: f.color },
|
||
{ offset: 1, color: f.bgColor },
|
||
]),
|
||
borderRadius: [6, 6, 0, 0],
|
||
},
|
||
})),
|
||
barWidth: '45%',
|
||
label: {
|
||
show: true,
|
||
position: 'top',
|
||
formatter: (p) => (p.value / 1e8).toFixed(2) + '亿',
|
||
color: '#333',
|
||
fontSize: 13,
|
||
fontWeight: 600,
|
||
},
|
||
},
|
||
],
|
||
})
|
||
}
|
||
|
||
const initPieChart = () => {
|
||
if (!pieChartRef.value) return
|
||
pieChart = echarts.init(pieChartRef.value)
|
||
pieChart.setOption({
|
||
tooltip: {
|
||
trigger: 'item',
|
||
formatter: (p) => `${p.name}<br/>数据量:<b>${formatNumber(p.value)}</b> 条<br/>占比:<b>${p.percent}%</b>`,
|
||
},
|
||
legend: {
|
||
orient: 'vertical',
|
||
right: 10,
|
||
top: 'center',
|
||
textStyle: { fontSize: 13, color: '#666' },
|
||
},
|
||
series: [
|
||
{
|
||
type: 'pie',
|
||
radius: ['42%', '70%'],
|
||
center: ['40%', '50%'],
|
||
avoidLabelOverlap: true,
|
||
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 3 },
|
||
label: {
|
||
show: true,
|
||
formatter: '{b}\n{d}%',
|
||
fontSize: 12,
|
||
lineHeight: 18,
|
||
},
|
||
labelLine: { length: 15, length2: 10 },
|
||
emphasis: {
|
||
itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.2)' },
|
||
},
|
||
data: factories.value.map((f) => ({
|
||
value: f.count,
|
||
name: f.name,
|
||
itemStyle: { color: f.color },
|
||
})),
|
||
},
|
||
],
|
||
})
|
||
}
|
||
|
||
const handleResize = () => {
|
||
barChart?.resize()
|
||
pieChart?.resize()
|
||
}
|
||
|
||
// 拦截浏览器后退:在大屏页按“返回”时跳转到数据模板页
|
||
const handlePopState = () => {
|
||
router.push('/cement_data_template')
|
||
}
|
||
|
||
onMounted(() => {
|
||
API.hfnf.stats.list.req().then(res => {
|
||
tables.value = res.tables || []
|
||
totalCount.value = res.summary?.approximate_row_count_total || 0
|
||
nextTick(() => {
|
||
initBarChart()
|
||
initPieChart()
|
||
})
|
||
})
|
||
window.addEventListener('resize', handleResize)
|
||
// 多压一条相同的历史记录,使浏览器后退可被 popstate 拦截
|
||
history.pushState(null, '', location.href)
|
||
window.addEventListener('popstate', handlePopState)
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
window.removeEventListener('resize', handleResize)
|
||
window.removeEventListener('popstate', handlePopState)
|
||
barChart?.dispose()
|
||
pieChart?.dispose()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.hfnf-dashboard {
|
||
padding: 16px;
|
||
background: #f5f7fa;
|
||
min-height: calc(100vh - 60px);
|
||
}
|
||
|
||
/* 顶部横幅 */
|
||
.summary-banner {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
background: linear-gradient(135deg, #1b2a4a 0%, #2d4a7a 50%, #3a6fb5 100%);
|
||
border-radius: 12px;
|
||
padding: 24px 32px;
|
||
margin-bottom: 16px;
|
||
color: #fff;
|
||
}
|
||
|
||
.banner-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.banner-icon {
|
||
width: 64px;
|
||
height: 64px;
|
||
border-radius: 16px;
|
||
background: rgba(255, 255, 255, 0.15);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.banner-logo {
|
||
width: 40px;
|
||
height: 40px;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.banner-info h2 {
|
||
margin: 0 0 4px 0;
|
||
font-size: 22px;
|
||
font-weight: 600;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.banner-info p {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.banner-right {
|
||
text-align: right;
|
||
}
|
||
|
||
.total-block {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 8px;
|
||
}
|
||
|
||
.total-label {
|
||
font-size: 14px;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.total-value {
|
||
font-size: 36px;
|
||
font-weight: 700;
|
||
font-family: 'DIN Alternate', 'Helvetica Neue', monospace;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.total-unit {
|
||
font-size: 14px;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
/* 工厂卡片 */
|
||
.factory-row {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.factory-card {
|
||
background: #fff;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
border-top: 3px solid;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.factory-card:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.card-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.card-body {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.card-count {
|
||
font-size: 26px;
|
||
font-weight: 700;
|
||
color: #1a1a1a;
|
||
font-family: 'DIN Alternate', 'Helvetica Neue', monospace;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.card-unit {
|
||
font-size: 13px;
|
||
color: #999;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.card-footer {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.card-meta {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.card-percent {
|
||
font-size: 13px;
|
||
color: #666;
|
||
}
|
||
|
||
.card-rank {
|
||
font-size: 13px;
|
||
color: #bbb;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 图表卡片 */
|
||
.chart-row {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.chart-card {
|
||
background: #fff;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.chart-card-header {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.chart-card-header h3 {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.chart-container {
|
||
height: 320px;
|
||
}
|
||
|
||
/* 底部信息区域 */
|
||
.info-row {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.info-card {
|
||
background: #fff;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
height: 100%;
|
||
}
|
||
|
||
.dot {
|
||
display: inline-block;
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
/* 系统信息列表 */
|
||
.sys-info-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.sys-info-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 10px 0;
|
||
border-bottom: 1px solid #f5f5f5;
|
||
}
|
||
|
||
.sys-info-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.sys-info-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.sys-info-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.sys-info-label {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.sys-info-value {
|
||
font-size: 14px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
</style>
|