factory_web/src/views/fac_cal/hfnf_index.vue

624 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>