factory_web/src/views/fac_cal/hfnf_index.vue

686 lines
21 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" :class="{ 'is-fullscreen': isFullscreen }" v-loading="loading" element-loading-text="数据加载中..." element-loading-background="rgba(245, 247, 250, 0.85)">
<!-- 顶部总览横幅 -->
<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>
<el-button type="primary" plain class="back-btn" @click="toggleFullscreen">
<el-icon><FullScreen /></el-icon>
{{ isFullscreen ? '退出全屏' : '进入全屏' }}
</el-button>
</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 }" @click="goDetail(item)">
<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', '#9b59b6']
const bgColors = ['#ecf5ff', '#f0f9eb', '#fdf6ec', '#fef0f0', '#f5eef8']
// 各工厂与数据表的映射关系
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') }, // 槁坎南方水泥
{ name: '瓮福烧结矿', match: (t) => t === 'mplogx_wengfu' }, // 瓮福烧结矿
]
const totalCount = ref(0)
const tables = ref([])
const loading = ref(true)
const isFullscreen = ref(false)
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 || 0).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
const toggleFullscreen = () => {
// 浏览器沉浸式全屏;菜单的显隐由 isFullscreen 驱动的 CSS class 控制(见样式)
if (document.fullscreenElement) {
document.exitFullscreen()
} else {
document.documentElement.requestFullscreen()
}
}
const handleFullscreenChange = () => {
isFullscreen.value = !!document.fullscreenElement
// 切换全屏后图表需要重绘以适配新尺寸
nextTick(() => handleResize())
}
const goDetail = (item) => {
// 瓮福烧结矿 跳转独立明细页,其余跳默认明细页
if (item && item.name === '瓮福烧结矿') {
router.push('/wengfu_mplogx')
} else {
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()
})
}).finally(() => {
loading.value = false
})
window.addEventListener('resize', handleResize)
document.addEventListener('fullscreenchange', handleFullscreenChange)
// 多压一条相同的历史记录,使浏览器后退可被 popstate 拦截
history.pushState(null, '', location.href)
window.addEventListener('popstate', handlePopState)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
document.removeEventListener('fullscreenchange', handleFullscreenChange)
window.removeEventListener('popstate', handlePopState)
barChart?.dispose()
pieChart?.dispose()
})
</script>
<style scoped>
.hfnf-dashboard {
padding: 16px;
background: #f5f7fa;
min-height: calc(100vh - 60px);
}
/* 全屏态dashboard 固定铺满屏幕、遮住左侧菜单与顶栏;退出移除 class 即恢复 */
.hfnf-dashboard.is-fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
min-height: 100%;
z-index: 9999;
overflow-y: auto;
background: #f5f7fa;
}
/* 顶部横幅 */
.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;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 12px;
}
.back-btn {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.4);
color: #fff;
}
.back-btn:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.6);
color: #fff;
}
.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>