factory_web/src/views/bigScreen/bxerp/jiejingdept.vue

864 lines
33 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="page">
<div class="grid-bg"></div>
<!-- HEADER -->
<header>
<div class="header-left">
<div>
<div class="datetime">{{ currentDay }} {{ currentTime }}</div>
<div style="display:flex;align-items:center;gap:6px;margin-top:4px">
<div class="status-dot"></div>
<span class="status-label">系统运行中</span>
</div>
</div>
</div>
<div class="header-title"> </div>
<div class="header-right">
<!-- <span class="shift-badge">白班</span> -->
<div class="user-badge">
<div class="user-avatar"></div>
<span style="font-size:13px">张三</span>
</div>
</div>
</header>
<!-- KPI ROW -->
<div class="kpi-row">
<div class="kpi-card cyan">
<div class="corner-deco corner-tl"></div><div class="corner-deco corner-br"></div>
<div class="kpi-icon">📦</div>
<div class="kpi-label">今日工序报工量</div>
<div class="kpi-value">3470</div>
<div class="kpi-trend up"> 较昨日 +12.4%</div>
<div class="kpi-progress"><div class="kpi-progress-fill" style="width:86%"></div></div>
</div>
<div class="kpi-card teal">
<div class="corner-deco corner-tl"></div><div class="corner-deco corner-br"></div>
<div class="kpi-icon"></div>
<div class="kpi-label">产品合格率</div>
<div class="kpi-value">98.6<span style="font-size:0.55em">%</span></div>
<div class="kpi-trend up"> 较上周 +0.3%</div>
<div class="kpi-progress"><div class="kpi-progress-fill" style="width:98.6%"></div></div>
</div>
<div class="kpi-card amber">
<div class="corner-deco corner-tl"></div><div class="corner-deco corner-br"></div>
<div class="kpi-icon">🎯</div>
<div class="kpi-label">今日目标完成率</div>
<div class="kpi-value">86<span style="font-size:0.55em">%</span></div>
<div class="kpi-trend neutral">目标 4000</div>
<div class="kpi-progress"><div class="kpi-progress-fill" style="width:86%"></div></div>
</div>
<div class="kpi-card blue">
<div class="corner-deco corner-tl"></div><div class="corner-deco corner-br"></div>
<div class="kpi-icon"></div>
<div class="kpi-label">设备综合利用率</div>
<div class="kpi-value">91<span style="font-size:0.55em">%</span></div>
<div class="kpi-trend up"> 较昨日 +2.1%</div>
<div class="kpi-progress"><div class="kpi-progress-fill" style="width:91%"></div></div>
</div>
<div class="kpi-card coral">
<div class="corner-deco corner-tl"></div><div class="corner-deco corner-br"></div>
<div class="kpi-icon"></div>
<div class="kpi-label">今日故障次数</div>
<div class="kpi-value">2</div>
<div class="kpi-trend down"> 较昨日 +1</div>
<div class="kpi-progress"><div class="kpi-progress-fill" style="width:20%"></div></div>
</div>
</div>
<!-- MAIN GRID -->
<div class="main-grid">
<!-- LEFT: Donut + Target -->
<div class="panel">
<div class="scan-line"></div>
<div class="corner-deco corner-tl"></div><div class="corner-deco corner-br"></div>
<div class="panel-title">产品种类占比</div>
<div class="donut-wrap">
<div id="donutChart" style="width:100%;height:100%"></div>
<div class="donut-center">
<div class="donut-center-num">3470</div>
<div class="donut-center-label">总产量</div>
</div>
</div>
<div style="margin-top:4px">
<div class="legend-row" v-for="item in donutLegend" :key="item.name">
<div class="legend-dot" :style="{background: item.color}"></div>
<span class="legend-name">{{ item.name }}</span>
<span class="legend-pct">{{ item.pct }}</span>
<span class="legend-val">{{ item.val }}</span>
</div>
</div>
<div class="target-section">
<div class="panel-title" style="margin-top:1vh;margin-bottom:0.8vh">今日生产进度</div>
<div class="target-header"><span>已完成 3470件</span><span>目标 4000件</span></div>
<div class="target-bar"><div class="target-fill" style="width:86.75%"></div></div>
<div class="target-header"><span>班次进度</span><span>当前班 74%</span></div>
<div class="target-bar"><div class="target-fill" style="width:74%;background:linear-gradient(90deg,#ffc107,#ff9800)"></div></div>
<div class="target-header"><span>周计划</span><span>本周 62%</span></div>
<div class="target-bar"><div class="target-fill" style="width:62%;background:linear-gradient(90deg,#bb86fc,#4fc3f7)"></div></div>
</div>
</div>
<!-- CENTER: Realtime Chart -->
<div class="panel panel-flex">
<div class="corner-deco corner-tl"></div><div class="corner-deco corner-tr"></div>
<div class="corner-deco corner-bl"></div><div class="corner-deco corner-br"></div>
<div class="panel-title">
设备实时检测曲线
<span style="color:#00e5ff;font-size:11px;margin-left:4px">{{ activeWorkstation }}</span>
<div class="panel-badge">每5s采集 · 实时更新</div>
</div>
<div class="chart-toolbar">
<div
v-for="ws in workstations"
:key="ws"
class="device-tab"
:class="{ active: activeWorkstation === ws }"
@click="switchWorkstation(ws)"
>{{ ws }}</div>
<span class="chart-info">实时刷新 ● LIVE</span>
</div>
<div class="realtime-wrap">
<div id="realtimeChart" style="width:100%;height:100%"></div>
</div>
<div class="chart-stats">
<div class="chart-stat">
<div class="chart-stat-label">当前值</div>
<div class="chart-stat-val" style="color:#00e5ff">{{ rtStats.current }}</div>
</div>
<div class="chart-stat">
<div class="chart-stat-label">均值</div>
<div class="chart-stat-val" style="color:#1de9b6">{{ rtStats.avg }}</div>
</div>
<div class="chart-stat">
<div class="chart-stat-label">峰值</div>
<div class="chart-stat-val" style="color:#ffc107">{{ rtStats.max }}</div>
</div>
<div class="chart-stat">
<div class="chart-stat-label">谷值</div>
<div class="chart-stat-val" style="color:#5a8a9f">{{ rtStats.min }}</div>
</div>
</div>
</div>
<!-- RIGHT: Personnel -->
<div class="panel">
<div class="corner-deco corner-tl"></div><div class="corner-deco corner-br"></div>
<div class="panel-title">
人员周产量
<div class="panel-badge" style="margin-left:auto">本周排行</div>
</div>
<div class="person-row" v-for="(p, i) in persons" :key="p.name">
<div class="person-meta">
<span class="person-name" style="display:flex;align-items:center;gap:6px">
<span :style="{fontFamily:'monospace',fontSize:'10px',color:i<3?p.color:'#3a6070',width:'16px',display:'inline-block'}">{{ String(i+1).padStart(2,'0') }}</span>
{{ p.name }}
</span>
<span class="person-val" :style="{color: p.color}">{{ p.val }}</span>
</div>
<div class="person-bar">
<div class="person-fill" :style="{width:(p.val/maxPersonVal*100)+'%',background:p.color,opacity:i===0?1:0.7}"></div>
</div>
</div>
</div>
</div>
<!-- ─── BOTTOM ROW ─── -->
<div class="bottom-grid">
<!-- Equipment Status -->
<div class="panel">
<div class="corner-deco corner-tl"></div><div class="corner-deco corner-br"></div>
<div class="panel-title">设备运行状态</div>
<table class="equip-table">
<thead>
<tr>
<th>设备名称</th>
<th>状态</th>
<th>利用率</th>
<th>今日产量</th>
<th>运行时长</th>
</tr>
</thead>
<tbody>
<tr v-for="eq in equipments" :key="eq.name">
<td style="color:#00e5ff">{{ eq.name }}</td>
<td>
<span class="status-tag" :class="eq.statusClass">
<span class="status-dot-sm" :class="eq.dotClass"></span>{{ eq.statusText }}
</span>
</td>
<td>
<div style="display:inline-flex;align-items:center;gap:4px">
<div class="util-bar"><div class="util-fill" :style="{width:eq.util+'%',background:eq.utilColor}"></div></div>
{{ eq.util }}%
</div>
</td>
<td :style="{color: eq.statusClass==='status-err'?'#ff6b6b':'#cce8f4'}">{{ eq.output }}</td>
<td style="color:#5a8a9f">{{ eq.duration }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Hourly Output Chart -->
<div class="panel panel-flex">
<div class="corner-deco corner-tl"></div><div class="corner-deco corner-br"></div>
<div class="panel-title">今日每小时产量</div>
<div class="hourly-wrap">
<div id="hourlyChart" style="width:100%;height:100%"></div>
</div>
</div>
<!-- Stock List -->
<div class="panel">
<div class="corner-deco corner-tl"></div><div class="corner-deco corner-br"></div>
<div class="panel-title">
库存列表
<div class="panel-badge" style="margin-left:auto">实时库存</div>
</div>
<table class="equip-table">
<thead>
<tr>
<th>物料名称</th>
<th>工段</th>
<th>库存量</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr v-for="s in stockList" :key="s.name">
<td style="color:#cce8f4">{{ s.name }}</td>
<td style="color:#5a8a9f">{{ s.dept }}</td>
<td style="font-family:'Orbitron',monospace" :style="{color: s.statusColor}">{{ s.count }}</td>
<td>
<span class="status-tag" :class="s.statusClass">
<span class="status-dot-sm" :class="s.dotClass"></span>{{ s.statusText }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'JiejingDept',
data() {
return {
currentTime: '',
currentDay: '',
dayInterval: null,
rtInterval: null,
rtChart: null,
charts: [],
rtLabels: [],
rtValues: [],
rtStats: { current: '24.3', avg: '22.1', max: '29.7', min: '16.2' },
activeWorkstation: '拉单丝',
workstations: ['拉单丝', '一次复丝', '二次复丝', '洗棒'],
donutLegend: [
{ name: '倒像器', color: '#4fc3f7', pct: '41.0%', val: 1423 },
{ name: '直板', color: '#69f0ae', pct: '27.7%', val: 960 },
{ name: '光锥', color: '#ffc107', pct: '20.6%', val: 714 },
{ name: '其他', color: '#ff6b6b', pct: '9.8%', val: 340 },
{ name: '光棒', color: '#bb86fc', pct: '0.9%', val: 33 },
],
persons: [
{ name: '张三', val: 920, color: '#4fc3f7' },
{ name: '李四', val: 796, color: '#ff6b6b' },
{ name: '王五', val: 734, color: '#00e5ff' },
{ name: '赵六', val: 661, color: '#ffc107' },
{ name: '钱七', val: 594, color: '#bb86fc' },
{ name: '孙八', val: 508, color: '#4fc3f7' },
{ name: '周九', val: 438, color: '#ff6b6b' },
{ name: '吴十', val: 360, color: '#69f0ae' },
],
equipments: [
{ name: '拉单丝-01', statusClass: 'status-run', statusText: '运行中', dotClass: 'run-dot', util: 94, utilColor: '#1de9b6', output: 924, duration: '7h12m' },
{ name: '拉单丝-02', statusClass: 'status-run', statusText: '运行中', dotClass: 'run-dot', util: 88, utilColor: '#1de9b6', output: 876, duration: '7h08m' },
{ name: '一次复丝-01', statusClass: 'status-idle', statusText: '待机', dotClass: 'idle-dot', util: 71, utilColor: '#ffc107', output: 712, duration: '5h45m' },
{ name: '二次复丝-01', statusClass: 'status-run', statusText: '运行中', dotClass: 'run-dot', util: 95, utilColor: '#1de9b6', output: 958, duration: '7h15m' },
{ name: '洗棒-01', statusClass: 'status-err', statusText: '故障', dotClass: 'err-dot', util: 32, utilColor: '#ff6b6b', output: 324, duration: '2h41m' },
],
stockList: [
{ name: '倒像器毛坯', dept: '拉单丝', count: 1423, statusClass: 'status-run', statusText: '充足', dotClass: 'run-dot', statusColor: '#1de9b6' },
{ name: '直板半成品', dept: '一次复丝', count: 960, statusClass: 'status-run', statusText: '充足', dotClass: 'run-dot', statusColor: '#1de9b6' },
{ name: '光锥坯料', dept: '二次复丝', count: 714, statusClass: 'status-run', statusText: '正常', dotClass: 'run-dot', statusColor: '#4fc3f7' },
{ name: '光棒原材料', dept: '洗棒', count: 128, statusClass: 'status-idle', statusText: '偏低', dotClass: 'idle-dot', statusColor: '#ffc107' },
{ name: '倒像器成品', dept: '拉单丝', count: 836, statusClass: 'status-run', statusText: '充足', dotClass: 'run-dot', statusColor: '#1de9b6' },
{ name: '直板成品', dept: '一次复丝', count: 54, statusClass: 'status-err', statusText: '告急', dotClass: 'err-dot', statusColor: '#ff6b6b' },
{ name: '光锥成品', dept: '二次复丝', count: 392, statusClass: 'status-run', statusText: '正常', dotClass: 'run-dot', statusColor: '#4fc3f7' },
],
};
},
computed: {
maxPersonVal() {
return Math.max(...this.persons.map(p => p.val));
},
},
mounted() {
this.showTime();
this.dayInterval = setInterval(this.showTime, 1000);
this.$nextTick(() => {
this.initDonut();
this.initRealtime();
this.initHourly();
});
window.addEventListener('resize', this.resizeCharts);
},
beforeDestroy() {
clearInterval(this.dayInterval);
clearInterval(this.rtInterval);
window.removeEventListener('resize', this.resizeCharts);
this.charts.forEach(c => c && c.dispose());
},
methods: {
showTime() {
const now = new Date();
const pad = n => String(n).padStart(2, '0');
this.currentTime = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
this.currentDay = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
},
switchWorkstation(ws) {
this.activeWorkstation = ws;
},
resizeCharts() {
this.charts.forEach(c => c && c.resize());
},
initDonut() {
const el = document.getElementById('donutChart');
if (!el) return;
const chart = echarts.init(el);
this.charts.push(chart);
chart.setOption({
backgroundColor: 'transparent',
tooltip: {
backgroundColor: '#071e32',
borderColor: '#00e5ff',
borderWidth: 1,
textStyle: { color: '#cce8f4' },
formatter: params => `${params.name}: ${params.value}件 (${params.percent.toFixed(1)}%)`,
},
series: [{
type: 'pie',
radius: ['68%', '85%'],
data: this.donutLegend.map(d => ({
name: d.name, value: d.val,
itemStyle: { color: d.color },
})),
label: { show: false },
itemStyle: { borderWidth: 2, borderColor: '#041525' },
}],
});
},
initRealtime() {
const el = document.getElementById('realtimeChart');
if (!el) return;
const pad = n => String(n).padStart(2, '0');
const base = new Date();
base.setHours(0, 12, 0, 0);
for (let i = 0; i < 80; i++) {
const t = new Date(base.getTime() + i * 18000);
this.rtLabels.push(`${pad(t.getHours())}:${pad(t.getMinutes())}:${pad(t.getSeconds())}`);
const noise = Math.sin(i * 0.4) * 3 + Math.sin(i * 0.7) * 2 + Math.sin(i * 0.13) * 4 + (Math.random() - 0.5) * 2;
this.rtValues.push(parseFloat((22 + noise).toFixed(1)));
}
this.calcRtStats();
this.rtChart = echarts.init(el);
this.charts.push(this.rtChart);
this.rtChart.setOption({
backgroundColor: 'transparent',
animation: false,
grid: { left: '3%', right: '3%', top: '8%', bottom: '15%', containLabel: true },
tooltip: {
trigger: 'axis',
backgroundColor: '#071e32',
borderColor: '#00e5ff',
borderWidth: 1,
textStyle: { color: '#00e5ff' },
axisPointer: { lineStyle: { color: 'rgba(0,229,255,0.3)' } },
},
xAxis: {
type: 'category',
data: this.rtLabels,
boundaryGap: false,
axisLabel: { color: '#3a6070', fontSize: 9, interval: 9 },
axisLine: { lineStyle: { color: 'rgba(0,229,255,0.1)' } },
splitLine: { lineStyle: { color: 'rgba(0,229,255,0.05)' } },
},
yAxis: {
type: 'value',
min: 5, max: 35,
axisLabel: { color: '#3a6070', fontSize: 9 },
axisLine: { lineStyle: { color: 'rgba(0,229,255,0.1)' } },
splitLine: { lineStyle: { color: 'rgba(0,229,255,0.06)' } },
},
series: [{
type: 'line',
data: this.rtValues,
smooth: 0.4,
symbol: 'none',
lineStyle: { color: '#00e5ff', width: 1.5 },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(0,229,255,0.12)' },
{ offset: 1, color: 'rgba(0,229,255,0.01)' },
]),
},
}],
});
this.rtInterval = setInterval(() => {
const newVal = parseFloat((22 + Math.sin(Date.now() * 0.001) * 3 + (Math.random() - 0.5) * 3).toFixed(1));
this.rtValues.push(newVal);
this.rtValues.shift();
const now = new Date();
this.rtLabels.push(`${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`);
this.rtLabels.shift();
this.calcRtStats();
this.rtChart.setOption({
xAxis: { data: this.rtLabels },
series: [{ data: this.rtValues }],
});
}, 1000);
},
calcRtStats() {
const vals = this.rtValues;
if (!vals.length) return;
const current = vals[vals.length - 1];
const avg = (vals.reduce((a, b) => a + b, 0) / vals.length).toFixed(1);
const max = Math.max(...vals).toFixed(1);
const min = Math.min(...vals).toFixed(1);
this.rtStats = {
current: typeof current === 'number' ? current.toFixed(1) : current,
avg, max, min,
};
},
initHourly() {
const el = document.getElementById('hourlyChart');
if (!el) return;
const hours = ['8时', '9时', '10时', '11时', '12时', '13时', '14时', '15时', '16时'];
const vals = [312, 428, 445, 410, 180, 430, 452, 441, 372];
const chart = echarts.init(el);
this.charts.push(chart);
chart.setOption({
backgroundColor: 'transparent',
animation: false,
grid: { left: '3%', right: '3%', top: '8%', bottom: '10%', containLabel: true },
tooltip: {
trigger: 'axis',
backgroundColor: '#071e32',
borderColor: '#00e5ff',
borderWidth: 1,
textStyle: { color: '#cce8f4' },
formatter: params => `${params[0].axisValue}: ${params[0].value}`,
},
xAxis: {
type: 'category',
data: hours,
axisLabel: { color: '#3a6070', fontSize: 10 },
axisLine: { lineStyle: { color: 'rgba(0,229,255,0.1)' } },
splitLine: { show: false },
},
yAxis: {
type: 'value',
axisLabel: { color: '#3a6070', fontSize: 9 },
axisLine: { lineStyle: { color: 'rgba(0,229,255,0.1)' } },
splitLine: { lineStyle: { color: 'rgba(0,229,255,0.06)' } },
},
series: [{
type: 'bar',
data: vals.map((v, i) => ({
value: v,
itemStyle: {
color: i === vals.length - 1 ? 'rgba(0,229,255,0.7)' : 'rgba(0,229,255,0.25)',
borderColor: i === vals.length - 1 ? '#00e5ff' : 'rgba(0,229,255,0.4)',
borderWidth: 1,
borderRadius: [3, 3, 0, 0],
},
})),
}],
});
},
},
};
</script>
<style scoped>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
/* ─── PAGE ROOT: flex column, strict 100vh ─── */
.page {
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
padding: 0 1vw 0.5vh;
font-family: 'Noto Sans SC', 'Microsoft Yahei', sans-serif;
background: #020d1a;
color: #cce8f4;
background-image:
radial-gradient(ellipse 80% 40% at 50% 0%, rgba(0,80,120,0.3) 0%, transparent 70%),
linear-gradient(180deg, #020d1a 0%, #031422 100%);
}
.grid-bg {
position: fixed; inset: 0;
background-image:
linear-gradient(rgba(0,229,255,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,229,255,0.03) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
z-index: 0;
}
/* ─── HEADER ─── */
header {
flex-shrink: 0;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: 0.8vh 0.5vw 0.6vh;
border-bottom: 1px solid rgba(0,229,255,0.07);
margin-bottom: 0.8vh;
}
.header-left { display: flex; align-items: center; }
.datetime { font-family: 'Orbitron', monospace; font-size: 1.1vh; color: #5a8a9f; letter-spacing: 1px; }
.status-dot { width: 0.8vh; height: 0.8vh; border-radius: 50%; background: #1de9b6; box-shadow: 0 0 8px #1de9b6; animation: pulse 2s infinite; }
.status-label { font-size: 1vh; color: #1de9b6; letter-spacing: 1px; }
.header-title {
text-align: center;
font-family: 'Orbitron', monospace;
font-size: 2.2vw;
font-weight: 900;
letter-spacing: 0.5vw;
color: #00e5ff;
text-shadow: 0 0 30px rgba(0,229,255,0.5), 0 0 60px rgba(0,229,255,0.2);
position: relative;
}
.header-title::before, .header-title::after {
content: '';
position: absolute; top: 50%;
width: 2vw; height: 1px;
background: linear-gradient(90deg, transparent, #00e5ff);
}
.header-title::before { right: calc(100% + 0.8vw); }
.header-title::after { left: calc(100% + 0.8vw); background: linear-gradient(90deg, #00e5ff, transparent); }
.header-right { display: flex; align-items: center; justify-content: flex-end; gap: 1vw; }
.user-badge {
display: flex; align-items: center; gap: 0.5vw;
background: #071e32; border: 1px solid rgba(0,229,255,0.18);
border-radius: 2vh; padding: 0.3vh 1vw 0.3vh 0.5vw;
font-size: 1.2vh;
}
.user-avatar {
width: 2.4vh; height: 2.4vh; border-radius: 50%;
background: linear-gradient(135deg, #00e5ff, #1de9b6);
display: flex; align-items: center; justify-content: center;
font-size: 1vh; font-weight: 700; color: #020d1a;
}
.shift-badge {
font-size: 1vh; color: #ffc107; border: 1px solid rgba(255,193,7,0.3);
border-radius: 4px; padding: 0.2vh 0.6vw; letter-spacing: 1px;
}
/* ─── KPI ROW ─── */
.kpi-row {
flex-shrink: 0;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0.7vw;
margin-bottom: 0.8vh;
}
.kpi-card {
background: #071e32;
border: 1px solid rgba(0,229,255,0.18);
border-radius: 8px;
padding: 1.2vh 1.2vw;
position: relative;
overflow: hidden;
transition: border-color 0.3s;
}
.kpi-card::before {
content: '';
position: absolute; top: 0; left: 0; right: 0; height: 2px;
}
.kpi-card.cyan::before { background: linear-gradient(90deg, transparent, #00e5ff, transparent); }
.kpi-card.teal::before { background: linear-gradient(90deg, transparent, #1de9b6, transparent); }
.kpi-card.amber::before { background: linear-gradient(90deg, transparent, #ffc107, transparent); }
.kpi-card.coral::before { background: linear-gradient(90deg, transparent, #ff6b6b, transparent); }
.kpi-card.blue::before { background: linear-gradient(90deg, transparent, #4fc3f7, transparent); }
.kpi-card:hover { border-color: rgba(0,229,255,0.4); }
.kpi-icon {
width: 3.2vh; height: 3.2vh; border-radius: 6px;
display: flex; align-items: center; justify-content: center;
font-size: 1.6vh; margin-bottom: 0.7vh;
}
.kpi-card.cyan .kpi-icon { background: rgba(0,229,255,0.1); }
.kpi-card.teal .kpi-icon { background: rgba(29,233,182,0.1); }
.kpi-card.amber .kpi-icon { background: rgba(255,193,7,0.1); }
.kpi-card.coral .kpi-icon { background: rgba(255,107,107,0.1); }
.kpi-card.blue .kpi-icon { background: rgba(79,195,247,0.1); }
.kpi-label { font-size: 1vh; color: #5a8a9f; letter-spacing: 1px; margin-bottom: 0.4vh; }
.kpi-value {
font-family: 'Orbitron', monospace;
font-size: 2.6vh; font-weight: 700;
line-height: 1; margin-bottom: 0.5vh;
}
.kpi-card.cyan .kpi-value { color: #00e5ff; }
.kpi-card.teal .kpi-value { color: #1de9b6; }
.kpi-card.amber .kpi-value { color: #ffc107; }
.kpi-card.coral .kpi-value { color: #ff6b6b; }
.kpi-card.blue .kpi-value { color: #4fc3f7; }
.kpi-trend { font-size: 1vh; display: flex; align-items: center; gap: 4px; }
.kpi-trend.up { color: #1de9b6; }
.kpi-trend.down { color: #ff6b6b; }
.kpi-trend.neutral { color: #5a8a9f; }
.kpi-progress {
position: absolute; bottom: 0; left: 0; right: 0;
height: 3px; background: rgba(255,255,255,0.05);
border-radius: 0 0 8px 8px;
}
.kpi-progress-fill { height: 100%; border-radius: 0 0 8px 8px; }
.cyan .kpi-progress-fill { background: #00e5ff; }
.teal .kpi-progress-fill { background: #1de9b6; }
.amber .kpi-progress-fill { background: #ffc107; }
.coral .kpi-progress-fill { background: #ff6b6b; }
.blue .kpi-progress-fill { background: #4fc3f7; }
/* ─── MAIN GRID: fills remaining height ─── */
.main-grid {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 22vw 1fr 23vw;
gap: 0.7vw;
margin-bottom: 0.8vh;
}
/* ─── PANELS ─── */
.panel {
background: #071e32;
border: 1px solid rgba(0,229,255,0.18);
border-radius: 8px;
padding: 1.2vh 1.2vw;
position: relative;
overflow: hidden;
box-sizing: border-box;
height: 100%;
}
/* Center & hourly chart panels use flex column so charts can flex-grow */
.panel-flex {
display: flex;
flex-direction: column;
}
.panel-title {
flex-shrink: 0;
font-size: 1.1vh;
color: #00e5ff;
letter-spacing: 2px;
margin-bottom: 1vh;
display: flex;
align-items: center;
gap: 0.5vw;
}
.panel-title::before {
content: '';
width: 3px; height: 1.4vh;
background: #00e5ff;
border-radius: 2px;
box-shadow: 0 0 8px #00e5ff;
flex-shrink: 0;
}
.panel-badge {
margin-left: auto;
font-size: 0.9vh; color: #5a8a9f;
background: #0a2540;
border: 1px solid rgba(0,229,255,0.07);
border-radius: 4px; padding: 0.2vh 0.5vw;
white-space: nowrap;
}
/* ─── DONUT ─── */
.donut-wrap { position: relative; width: 16vh; height: 16vh; margin: 0 auto 1vh; flex-shrink: 0; }
.donut-center {
position: absolute; inset: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
pointer-events: none;
}
.donut-center-num { font-family: 'Orbitron', monospace; font-size: 1.8vh; color: #00e5ff; }
.donut-center-label { font-size: 0.9vh; color: #5a8a9f; margin-top: 2px; }
.legend-row {
display: flex; align-items: center; gap: 0.4vw;
padding: 0.45vh 0;
border-bottom: 1px solid rgba(0,229,255,0.07);
font-size: 1.1vh;
}
.legend-row:last-child { border-bottom: none; }
.legend-dot { width: 0.7vh; height: 0.7vh; border-radius: 2px; flex-shrink: 0; }
.legend-name { flex: 1; color: #5a8a9f; }
.legend-pct { color: #cce8f4; font-weight: 500; }
.legend-val { color: #5a8a9f; font-size: 1vh; width: 3vw; text-align: right; }
/* ─── TARGET PROGRESS ─── */
.target-section { margin-top: 0.8vh; }
.target-header { display: flex; justify-content: space-between; margin-bottom: 0.4vh; font-size: 1vh; color: #5a8a9f; }
.target-bar { height: 0.6vh; background: rgba(255,255,255,0.05); border-radius: 3px; overflow: hidden; margin-bottom: 0.8vh; }
.target-fill {
height: 100%; border-radius: 3px;
background: linear-gradient(90deg, #00e5ff, #1de9b6);
position: relative; overflow: hidden;
transition: width 1.5s ease;
}
.target-fill::after {
content: '';
position: absolute; top: 0; left: -100%; width: 60%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
animation: shimmer 2s infinite;
}
/* ─── REALTIME CHART ─── */
.chart-toolbar {
flex-shrink: 0;
display: flex; align-items: center; gap: 0.5vw;
margin-bottom: 0.8vh;
}
.device-tab {
padding: 0.3vh 0.7vw; border-radius: 4px; cursor: pointer;
border: 1px solid rgba(0,229,255,0.07);
color: #5a8a9f; font-size: 1vh; letter-spacing: 1px;
transition: all 0.2s; white-space: nowrap;
}
.device-tab.active {
background: rgba(0,229,255,0.15); border-color: #00e5ff; color: #00e5ff;
}
.chart-info { margin-left: auto; color: #3a6070; font-size: 0.9vh; letter-spacing: 1px; }
/* realtime wrap grows to fill center panel */
.realtime-wrap { flex: 1; min-height: 0; position: relative; }
.chart-stats {
flex-shrink: 0;
display: grid; grid-template-columns: repeat(4, 1fr);
gap: 0.5vw; margin-top: 0.8vh;
}
.chart-stat {
background: #0a2540; border-radius: 6px;
padding: 0.6vh 0.5vw; text-align: center;
}
.chart-stat-label { font-size: 0.9vh; color: #5a8a9f; margin-bottom: 0.3vh; }
.chart-stat-val { font-family: 'Orbitron', monospace; font-size: 1.3vh; }
/* ─── PERSONNEL ─── */
.person-row { margin-bottom: 0.9vh; }
.person-meta { display: flex; justify-content: space-between; margin-bottom: 0.3vh; font-size: 1.1vh; }
.person-name { color: #cce8f4; }
.person-val { font-family: 'Orbitron', monospace; font-size: 1.1vh; }
.person-bar { height: 0.7vh; background: rgba(255,255,255,0.06); border-radius: 4px; overflow: hidden; }
.person-fill { height: 100%; border-radius: 4px; }
/* ─── BOTTOM GRID: fixed height ─── */
.bottom-grid {
flex-shrink: 0;
height: 26vh;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 0.7vw;
}
/* ─── EQUIPMENT TABLE ─── */
.equip-table { width: 100%; border-collapse: collapse; font-size: 1.1vh; }
.equip-table th {
color: #5a8a9f; font-weight: 400; letter-spacing: 1px;
padding: 0.5vh 0.5vw; border-bottom: 1px solid rgba(0,229,255,0.07);
text-align: left; font-size: 0.9vh;
}
.equip-table td { padding: 0.6vh 0.5vw; border-bottom: 1px solid rgba(0,229,255,0.07); }
.equip-table tr:last-child td { border-bottom: none; }
.equip-table tr:hover td { background: rgba(0,229,255,0.07); }
.status-tag {
display: inline-flex; align-items: center; gap: 4px;
padding: 0.2vh 0.5vw; border-radius: 4px; font-size: 0.9vh;
}
.status-run { background: rgba(29,233,182,0.12); color: #1de9b6; border: 1px solid rgba(29,233,182,0.3); }
.status-idle { background: rgba(255,193,7,0.12); color: #ffc107; border: 1px solid rgba(255,193,7,0.3); }
.status-err { background: rgba(255,107,107,0.12); color: #ff6b6b; border: 1px solid rgba(255,107,107,0.3); }
.status-dot-sm { width: 5px; height: 5px; border-radius: 50%; display: inline-block; }
.run-dot { background: #1de9b6; box-shadow: 0 0 4px #1de9b6; animation: pulse 2s infinite; }
.idle-dot { background: #ffc107; }
.err-dot { background: #ff6b6b; box-shadow: 0 0 4px #ff6b6b; animation: pulse 1s infinite; }
.util-bar { height: 4px; background: rgba(255,255,255,0.06); border-radius: 2px; width: 4vw; display: inline-block; overflow: hidden; vertical-align: middle; }
.util-fill { height: 100%; border-radius: 2px; }
/* ─── HOURLY CHART: grows in panel-flex ─── */
.hourly-wrap { flex: 1; min-height: 0; position: relative; }
/* ─── CORNER DECORATIONS ─── */
.corner-deco {
position: absolute; width: 12px; height: 12px;
border-color: #00e5ff; opacity: 0.5;
}
.corner-tl { top: 0; left: 0; border-top: 1px solid; border-left: 1px solid; }
.corner-tr { top: 0; right: 0; border-top: 1px solid; border-right: 1px solid; }
.corner-bl { bottom: 0; left: 0; border-bottom: 1px solid; border-left: 1px solid; }
.corner-br { bottom: 0; right: 0; border-bottom: 1px solid; border-right: 1px solid; }
/* ─── SCAN LINE ─── */
.scan-line {
position: absolute; left: 0; right: 0; height: 1px;
background: linear-gradient(90deg, transparent, rgba(0,229,255,0.2), transparent);
animation: scan 4s linear infinite; pointer-events: none;
}
@keyframes scan { from { top: 0; } to { top: 100%; } }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
@keyframes shimmer { to { left: 200%; } }
</style>