864 lines
33 KiB
Vue
864 lines
33 KiB
Vue
<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>
|