2026-05-27 16:38:40 +08:00
|
|
|
<template>
|
|
|
|
|
<div class="layout">
|
|
|
|
|
|
|
|
|
|
<!-- ─── TOP BAR ─── -->
|
|
|
|
|
<div class="top-bar">
|
2026-05-27 16:52:53 +08:00
|
|
|
<div class="logo">
|
|
|
|
|
<span class="logo-mark">S</span>
|
|
|
|
|
<span class="logo-text">SIMATIC L2</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="sys-title">推拉酸洗线 · L2 过程控制系统</div>
|
2026-05-27 16:38:40 +08:00
|
|
|
<div class="spacer"></div>
|
|
|
|
|
<div class="status-pills">
|
2026-05-27 16:52:53 +08:00
|
|
|
<span class="pill run"><i class="dot"></i>机组运行</span>
|
|
|
|
|
<span class="pill run"><i class="dot"></i>L2 在线</span>
|
|
|
|
|
<span :class="['pill', l3Status]"><i class="dot"></i>{{ l3StatusText }}</span>
|
2026-05-27 16:38:40 +08:00
|
|
|
</div>
|
|
|
|
|
<div class="top-user">
|
|
|
|
|
<span class="username">{{ user && (user.full_name || user.username) }}</span>
|
|
|
|
|
<span class="divider">|</span>
|
|
|
|
|
<span class="logout" @click="logout">退出</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="clock">{{ clock }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-05-27 17:04:48 +08:00
|
|
|
<!-- ─── NAV BAR ─── -->
|
|
|
|
|
<div class="nav-bar">
|
|
|
|
|
<div
|
|
|
|
|
v-for="item in menuItems"
|
|
|
|
|
:key="item.path"
|
|
|
|
|
:class="['nav-item', { active: $route.path === item.path }]"
|
|
|
|
|
@click="$router.push(item.path)"
|
|
|
|
|
>
|
|
|
|
|
<span class="nav-icon" v-html="item.icon"></span>
|
|
|
|
|
<span class="nav-label">{{ item.title }}</span>
|
2026-05-27 16:38:40 +08:00
|
|
|
</div>
|
2026-05-27 17:04:48 +08:00
|
|
|
</div>
|
2026-05-27 16:38:40 +08:00
|
|
|
|
2026-05-27 17:04:48 +08:00
|
|
|
<!-- ─── MAIN ─── -->
|
|
|
|
|
<div class="main-area">
|
|
|
|
|
<router-view />
|
2026-05-27 16:38:40 +08:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- ─── FOOTER ─── -->
|
|
|
|
|
<div class="footer">
|
2026-05-27 16:52:53 +08:00
|
|
|
<div class="fp"><i class="dot g"></i>数据库</div>
|
|
|
|
|
<div class="fp"><i class="dot g"></i>UDP :9000</div>
|
|
|
|
|
<div class="fp"><i :class="['dot', l3Status === 'run' ? 'g' : 'y']"></i>L3 {{ l3StatusText }}</div>
|
|
|
|
|
<div style="margin-left:auto;">推拉酸洗线 L2 MES · v1.0.0</div>
|
2026-05-27 16:38:40 +08:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
import { mapGetters } from 'vuex'
|
|
|
|
|
|
2026-05-27 16:52:53 +08:00
|
|
|
const IC = {
|
2026-05-27 17:04:48 +08:00
|
|
|
dashboard: `<svg viewBox="0 0 14 14" fill="currentColor"><rect x="1" y="1" width="5" height="5"/><rect x="8" y="1" width="5" height="5"/><rect x="1" y="8" width="5" height="5"/><rect x="8" y="8" width="5" height="5"/></svg>`,
|
|
|
|
|
material: `<svg viewBox="0 0 14 14" fill="currentColor"><path d="M7 1L1 4v6l6 3 6-3V4L7 1zm0 1.8L11.5 5v4.5L7 11.8 2.5 9.5V5L7 2.8z"/></svg>`,
|
|
|
|
|
production: `<svg viewBox="0 0 14 14" fill="currentColor"><rect x="1" y="8" width="3" height="5"/><rect x="5.5" y="5" width="3" height="8"/><rect x="10" y="2" width="3" height="11"/></svg>`,
|
|
|
|
|
plan: `<svg viewBox="0 0 14 14" fill="currentColor"><rect x="2" y="2.5" width="10" height="2"/><rect x="2" y="6" width="10" height="2"/><rect x="2" y="9.5" width="6" height="2"/></svg>`,
|
|
|
|
|
downtime: `<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="7" cy="7" r="5.5"/><rect x="5" y="4.5" width="1.8" height="5" fill="currentColor" stroke="none"/><rect x="7.2" y="4.5" width="1.8" height="5" fill="currentColor" stroke="none"/></svg>`,
|
|
|
|
|
equipment: `<svg viewBox="0 0 14 14" fill="currentColor"><path d="M7 4.5a2.5 2.5 0 100 5 2.5 2.5 0 000-5zm0 1.4a1.1 1.1 0 110 2.2A1.1 1.1 0 017 5.9zM5.9 1l-.4 1.6A4.6 4.6 0 003.9 3.8L2.3 3.2 1.2 5.3l1.3 1A4.4 4.4 0 002.2 7c0 .25.02.5.06.75L1.2 8.7l1.1 2.1 1.6-.6c.44.37.9.68 1.6.93L5.9 13h2.2l.4-1.6c.6-.25 1.1-.56 1.6-.93l1.6.6 1.1-2.1-1.3-1c.04-.25.06-.5.06-.75 0-.25-.02-.5-.06-.75l1.3-1-1.1-2.1-1.6.6A4.6 4.6 0 008.1 2.6L7.7 1H5.9z"/></svg>`,
|
|
|
|
|
inspection: `<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="5.5" cy="5.5" r="4"/><line x1="8.5" y1="8.5" x2="13" y2="13"/><circle cx="5.5" cy="5.5" r="1.2" fill="currentColor" stroke="none"/></svg>`,
|
|
|
|
|
message: `<svg viewBox="0 0 14 14" fill="currentColor"><path d="M1 1.5h12v9H8.5L7 12.5 5.5 10.5H1V1.5zm1.3 1.3v6.4H6l1 1.6 1-1.6h3.7V2.8H2.3z"/></svg>`,
|
|
|
|
|
process: `<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"><path d="M1 7 Q2.5 3 4 7 Q5.5 11 7 7 Q8.5 3 10 7 Q11 9 13 7"/></svg>`,
|
|
|
|
|
tension: `<svg viewBox="0 0 14 14" fill="currentColor"><path d="M7 1a6 6 0 100 12A6 6 0 007 1zm0 1.3a4.7 4.7 0 110 9.4A4.7 4.7 0 017 2.3z"/><line x1="7" y1="7" x2="10" y2="4.5" stroke="currentColor" stroke-width="1.3"/><circle cx="7" cy="7" r="1.1"/></svg>`,
|
|
|
|
|
quality: `<svg viewBox="0 0 14 14" fill="currentColor"><path d="M7 1l1.6 3.7 4 .5-3 2.7.9 4L7 9.8l-3.5 1.9.9-4-3-2.7 4-.5z"/></svg>`,
|
|
|
|
|
capacity: `<svg viewBox="0 0 14 14" fill="currentColor"><rect x="1" y="9" width="2.5" height="4"/><rect x="5" y="6.5" width="2.5" height="6.5"/><rect x="9" y="3.5" width="2.5" height="9.5"/></svg>`,
|
2026-05-27 16:52:53 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-27 16:38:40 +08:00
|
|
|
const MENU = [
|
2026-05-27 16:52:53 +08:00
|
|
|
{ path: '/dashboard', title: '生产看板', icon: IC.dashboard },
|
|
|
|
|
{ path: '/material', title: '物料跟踪', icon: IC.material },
|
|
|
|
|
{ path: '/production', title: '实绩管理', icon: IC.production },
|
|
|
|
|
{ path: '/plan', title: '计划管理', icon: IC.plan },
|
|
|
|
|
{ path: '/downtime', title: '停机管理', icon: IC.downtime },
|
|
|
|
|
{ path: '/equipment', title: '设备管理', icon: IC.equipment },
|
|
|
|
|
{ path: '/inspection', title: '设备巡检', icon: IC.inspection },
|
|
|
|
|
{ path: '/message', title: '报文监控', icon: IC.message },
|
|
|
|
|
{ path: '/process-model', title: '工艺段模型', icon: IC.process },
|
|
|
|
|
{ path: '/tension-model', title: '张力设定', icon: IC.tension },
|
|
|
|
|
{ path: '/quality', title: '质量管理', icon: IC.quality },
|
|
|
|
|
{ path: '/capacity', title: '产能分析', icon: IC.capacity },
|
2026-05-27 16:38:40 +08:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
name: 'Layout',
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
clock: '--:--:--',
|
|
|
|
|
l3Status: 'warn',
|
2026-05-27 16:52:53 +08:00
|
|
|
l3StatusText: 'L3 待机',
|
2026-05-27 16:38:40 +08:00
|
|
|
menuItems: MENU,
|
|
|
|
|
_timer: null,
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
...mapGetters({ user: 'auth/user' }),
|
|
|
|
|
},
|
|
|
|
|
mounted() {
|
|
|
|
|
this._timer = setInterval(() => {
|
2026-05-27 16:52:53 +08:00
|
|
|
this.clock = new Date().toTimeString().slice(0, 8)
|
2026-05-27 16:38:40 +08:00
|
|
|
}, 1000)
|
|
|
|
|
this.clock = new Date().toTimeString().slice(0, 8)
|
|
|
|
|
},
|
|
|
|
|
beforeDestroy() {
|
|
|
|
|
clearInterval(this._timer)
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
logout() {
|
|
|
|
|
this.$confirm('确认退出登录?', '提示', { type: 'warning' }).then(() => {
|
|
|
|
|
this.$store.dispatch('auth/logout')
|
|
|
|
|
this.$router.push('/login')
|
|
|
|
|
}).catch(() => {})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
@import '@/assets/styles/variables';
|
|
|
|
|
|
|
|
|
|
.layout {
|
|
|
|
|
height: 100vh;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
2026-05-27 16:52:53 +08:00
|
|
|
background: $bg-base;
|
2026-05-27 16:38:40 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── TOP BAR ───
|
|
|
|
|
.top-bar {
|
2026-05-27 17:04:48 +08:00
|
|
|
height: 46px;
|
2026-05-27 16:52:53 +08:00
|
|
|
background: $bg-secondary;
|
2026-05-27 16:38:40 +08:00
|
|
|
border-bottom: 1px solid $border;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2026-05-27 16:52:53 +08:00
|
|
|
padding: 0 14px;
|
|
|
|
|
gap: 14px;
|
2026-05-27 16:38:40 +08:00
|
|
|
flex-shrink: 0;
|
2026-05-27 16:52:53 +08:00
|
|
|
}
|
2026-05-27 16:38:40 +08:00
|
|
|
|
2026-05-27 16:52:53 +08:00
|
|
|
.logo {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2026-05-27 17:04:48 +08:00
|
|
|
gap: 8px;
|
2026-05-27 16:52:53 +08:00
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
2026-05-27 16:38:40 +08:00
|
|
|
|
2026-05-27 16:52:53 +08:00
|
|
|
.logo-mark {
|
2026-05-27 17:04:48 +08:00
|
|
|
width: 26px;
|
|
|
|
|
height: 26px;
|
2026-05-27 16:52:53 +08:00
|
|
|
background: $sms-teal;
|
|
|
|
|
color: $bg-base;
|
2026-05-27 17:04:48 +08:00
|
|
|
font-size: 14px;
|
2026-05-27 16:52:53 +08:00
|
|
|
font-weight: 900;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
2026-05-27 17:04:48 +08:00
|
|
|
border-radius: 2px;
|
2026-05-27 16:52:53 +08:00
|
|
|
}
|
2026-05-27 16:38:40 +08:00
|
|
|
|
2026-05-27 16:52:53 +08:00
|
|
|
.logo-text {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: $text-primary;
|
2026-05-27 17:04:48 +08:00
|
|
|
letter-spacing: 1.8px;
|
2026-05-27 16:52:53 +08:00
|
|
|
text-transform: uppercase;
|
|
|
|
|
}
|
2026-05-27 16:38:40 +08:00
|
|
|
|
2026-05-27 16:52:53 +08:00
|
|
|
.sys-title {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: $text-secondary;
|
|
|
|
|
border-left: 1px solid $border;
|
|
|
|
|
padding-left: 14px;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
2026-05-27 16:38:40 +08:00
|
|
|
|
2026-05-27 16:52:53 +08:00
|
|
|
.spacer { flex: 1; }
|
2026-05-27 16:38:40 +08:00
|
|
|
|
2026-05-27 16:52:53 +08:00
|
|
|
.status-pills {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
align-items: center;
|
2026-05-27 16:38:40 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.pill {
|
2026-05-27 16:52:53 +08:00
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 5px;
|
2026-05-27 17:04:48 +08:00
|
|
|
padding: 2px 8px;
|
|
|
|
|
border-radius: 2px;
|
2026-05-27 16:52:53 +08:00
|
|
|
font-size: 10px;
|
|
|
|
|
font-weight: 700;
|
2026-05-27 16:38:40 +08:00
|
|
|
letter-spacing: .5px;
|
2026-05-27 16:52:53 +08:00
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
|
|
|
|
.dot {
|
|
|
|
|
width: 5px;
|
|
|
|
|
height: 5px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
}
|
2026-05-27 16:38:40 +08:00
|
|
|
|
2026-05-27 17:04:48 +08:00
|
|
|
&.run { background: rgba($status-run,.12); color: $status-run; border: 1px solid rgba($status-run,.35); .dot { background: $status-run; } }
|
|
|
|
|
&.warn { background: rgba($status-warn,.12); color: $status-warn; border: 1px solid rgba($status-warn,.35); .dot { background: $status-warn; } }
|
|
|
|
|
&.stop { background: rgba($text-muted,.08); color: $text-muted; border: 1px solid $border; .dot { background: $text-muted; } }
|
2026-05-27 16:38:40 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-27 16:52:53 +08:00
|
|
|
.top-user {
|
2026-05-27 16:38:40 +08:00
|
|
|
display: flex;
|
2026-05-27 16:52:53 +08:00
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: $text-secondary;
|
|
|
|
|
|
2026-05-27 17:04:48 +08:00
|
|
|
.divider { color: $border; }
|
|
|
|
|
.logout { cursor: pointer; color: $text-muted; transition: color .12s; &:hover { color: $sms-teal; } }
|
2026-05-27 16:52:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.clock {
|
|
|
|
|
font-family: $font-mono;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: $sms-teal;
|
2026-05-27 17:04:48 +08:00
|
|
|
min-width: 68px;
|
2026-05-27 16:52:53 +08:00
|
|
|
text-align: right;
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 17:04:48 +08:00
|
|
|
// ─── NAV BAR ───
|
|
|
|
|
.nav-bar {
|
|
|
|
|
height: 36px;
|
|
|
|
|
background: $bg-primary;
|
|
|
|
|
border-bottom: 1px solid $border;
|
2026-05-27 16:52:53 +08:00
|
|
|
display: flex;
|
2026-05-27 17:04:48 +08:00
|
|
|
align-items: stretch;
|
|
|
|
|
overflow-x: auto;
|
2026-05-27 16:38:40 +08:00
|
|
|
flex-shrink: 0;
|
2026-05-27 16:52:53 +08:00
|
|
|
scrollbar-width: none;
|
|
|
|
|
&::-webkit-scrollbar { display: none; }
|
2026-05-27 16:38:40 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.nav-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2026-05-27 17:04:48 +08:00
|
|
|
gap: 6px;
|
|
|
|
|
padding: 0 14px;
|
2026-05-27 16:52:53 +08:00
|
|
|
font-size: 11px;
|
2026-05-27 17:04:48 +08:00
|
|
|
font-weight: 500;
|
2026-05-27 16:38:40 +08:00
|
|
|
color: $text-secondary;
|
|
|
|
|
cursor: pointer;
|
2026-05-27 17:04:48 +08:00
|
|
|
white-space: nowrap;
|
|
|
|
|
border-bottom: 2px solid transparent;
|
2026-05-27 16:52:53 +08:00
|
|
|
transition: all .12s;
|
2026-05-27 17:04:48 +08:00
|
|
|
user-select: none;
|
2026-05-27 16:52:53 +08:00
|
|
|
|
2026-05-27 17:04:48 +08:00
|
|
|
&:hover { color: $text-primary; background: rgba($sms-teal, .05); }
|
|
|
|
|
&.active { color: $sms-teal; border-bottom-color: $sms-teal; background: rgba($sms-teal, .07); }
|
2026-05-27 16:52:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.nav-icon {
|
2026-05-27 17:04:48 +08:00
|
|
|
width: 13px;
|
|
|
|
|
height: 13px;
|
2026-05-27 16:52:53 +08:00
|
|
|
flex-shrink: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
2026-05-27 17:04:48 +08:00
|
|
|
opacity: .9;
|
2026-05-27 16:38:40 +08:00
|
|
|
|
2026-05-27 17:04:48 +08:00
|
|
|
::v-deep svg { width: 13px; height: 13px; }
|
2026-05-27 16:38:40 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── MAIN AREA ───
|
|
|
|
|
.main-area {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
2026-05-27 16:52:53 +08:00
|
|
|
padding: 12px;
|
2026-05-27 16:38:40 +08:00
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2026-05-27 16:52:53 +08:00
|
|
|
gap: 12px;
|
|
|
|
|
background: $bg-primary;
|
2026-05-27 16:38:40 +08:00
|
|
|
scrollbar-width: thin;
|
|
|
|
|
scrollbar-color: $border transparent;
|
2026-05-27 16:52:53 +08:00
|
|
|
&::-webkit-scrollbar { width: 5px; }
|
2026-05-27 17:04:48 +08:00
|
|
|
&::-webkit-scrollbar-thumb { background: $border; border-radius: 1px; }
|
2026-05-27 16:38:40 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── FOOTER ───
|
|
|
|
|
.footer {
|
2026-05-27 17:04:48 +08:00
|
|
|
height: 22px;
|
2026-05-27 16:38:40 +08:00
|
|
|
background: $bg-secondary;
|
|
|
|
|
border-top: 1px solid $border;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 0 14px;
|
2026-05-27 16:52:53 +08:00
|
|
|
gap: 18px;
|
|
|
|
|
font-size: 10px;
|
2026-05-27 16:38:40 +08:00
|
|
|
color: $text-muted;
|
|
|
|
|
flex-shrink: 0;
|
2026-05-27 16:52:53 +08:00
|
|
|
letter-spacing: .3px;
|
2026-05-27 16:38:40 +08:00
|
|
|
|
|
|
|
|
.fp { display: flex; align-items: center; gap: 5px; }
|
2026-05-27 16:52:53 +08:00
|
|
|
.dot {
|
|
|
|
|
width: 5px; height: 5px; border-radius: 50%; display: inline-block;
|
|
|
|
|
&.g { background: $status-run; }
|
|
|
|
|
&.y { background: $status-warn; }
|
|
|
|
|
&.r { background: $status-fault; }
|
|
|
|
|
}
|
2026-05-27 16:38:40 +08:00
|
|
|
}
|
|
|
|
|
</style>
|