247 lines
6.5 KiB
Vue
247 lines
6.5 KiB
Vue
|
|
<template>
|
||
|
|
<div class="layout">
|
||
|
|
|
||
|
|
<!-- ─── TOP BAR ─── -->
|
||
|
|
<div class="top-bar">
|
||
|
|
<div class="logo">SMS <span>L2</span></div>
|
||
|
|
<div class="sys-title">推拉酸洗线 L2 过程控制系统 | PUSH-PULL PICKLING LINE</div>
|
||
|
|
<div class="spacer"></div>
|
||
|
|
<div class="status-pills">
|
||
|
|
<span class="pill run">● 机组运行</span>
|
||
|
|
<span class="pill run">● L2在线</span>
|
||
|
|
<span :class="['pill', l3Status]">{{ l3StatusText }}</span>
|
||
|
|
</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>
|
||
|
|
|
||
|
|
<!-- ─── 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">{{ item.icon }}</span>
|
||
|
|
{{ item.title }}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ─── MAIN ─── -->
|
||
|
|
<div class="main-area">
|
||
|
|
<router-view />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ─── FOOTER ─── -->
|
||
|
|
<div class="footer">
|
||
|
|
<div class="fp"><span class="dot g"></span>数据库 正常</div>
|
||
|
|
<div class="fp"><span class="dot g"></span>UDP 监听 :9000</div>
|
||
|
|
<div class="fp"><span :class="['dot', l3Status === 'run' ? 'g' : 'y']"></span>L3 {{ l3StatusText }}</div>
|
||
|
|
<div style="margin-left:auto;color:var(--text-muted);">推拉酸洗线 L2 MES v1.0.0</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
import { mapGetters } from 'vuex'
|
||
|
|
|
||
|
|
const MENU = [
|
||
|
|
{ path: '/dashboard', title: '生产看板', icon: 'DB' },
|
||
|
|
{ path: '/material', title: '物料跟踪', icon: 'MT' },
|
||
|
|
{ path: '/production', title: '实绩管理', icon: 'PR' },
|
||
|
|
{ path: '/plan', title: '计划管理', icon: 'PL' },
|
||
|
|
{ path: '/downtime', title: '停机管理', icon: 'DT' },
|
||
|
|
{ path: '/equipment', title: '设备管理', icon: 'EQ' },
|
||
|
|
{ path: '/inspection', title: '设备巡检', icon: 'INS' },
|
||
|
|
{ path: '/message', title: '报文监控', icon: 'MSG' },
|
||
|
|
{ path: '/process-model', title: '工艺段模型', icon: 'PM' },
|
||
|
|
{ path: '/tension-model', title: '张力设定', icon: 'TM' },
|
||
|
|
{ path: '/quality', title: '质量管理', icon: 'QC' },
|
||
|
|
{ path: '/capacity', title: '产能分析', icon: 'CAP' },
|
||
|
|
]
|
||
|
|
|
||
|
|
export default {
|
||
|
|
name: 'Layout',
|
||
|
|
data() {
|
||
|
|
return {
|
||
|
|
clock: '--:--:--',
|
||
|
|
l3Status: 'warn',
|
||
|
|
l3StatusText: 'L3待机',
|
||
|
|
menuItems: MENU,
|
||
|
|
_timer: null,
|
||
|
|
}
|
||
|
|
},
|
||
|
|
computed: {
|
||
|
|
...mapGetters({ user: 'auth/user' }),
|
||
|
|
},
|
||
|
|
mounted() {
|
||
|
|
this._timer = setInterval(() => {
|
||
|
|
const now = new Date()
|
||
|
|
this.clock = now.toTimeString().slice(0, 8)
|
||
|
|
}, 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;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── TOP BAR ───
|
||
|
|
.top-bar {
|
||
|
|
height: 48px;
|
||
|
|
background: linear-gradient(90deg, #0d1b2e 0%, #0a1628 100%);
|
||
|
|
border-bottom: 1px solid $border;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
padding: 0 16px;
|
||
|
|
gap: 16px;
|
||
|
|
flex-shrink: 0;
|
||
|
|
|
||
|
|
.logo {
|
||
|
|
font-size: 15px;
|
||
|
|
font-weight: 700;
|
||
|
|
color: $sms-highlight;
|
||
|
|
letter-spacing: 1px;
|
||
|
|
white-space: nowrap;
|
||
|
|
span { color: $accent-yellow; }
|
||
|
|
}
|
||
|
|
|
||
|
|
.sys-title {
|
||
|
|
font-size: 12px;
|
||
|
|
color: $text-secondary;
|
||
|
|
border-left: 1px solid $border;
|
||
|
|
padding-left: 16px;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.spacer { flex: 1; }
|
||
|
|
|
||
|
|
.status-pills {
|
||
|
|
display: flex;
|
||
|
|
gap: 10px;
|
||
|
|
align-items: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.top-user {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
font-size: 12px;
|
||
|
|
color: $text-secondary;
|
||
|
|
.divider { color: $border; }
|
||
|
|
.logout { cursor: pointer; color: $text-muted; &:hover { color: $sms-highlight; } }
|
||
|
|
}
|
||
|
|
|
||
|
|
.clock {
|
||
|
|
font-family: $font-mono;
|
||
|
|
font-size: 13px;
|
||
|
|
color: $sms-highlight;
|
||
|
|
min-width: 72px;
|
||
|
|
text-align: right;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.pill {
|
||
|
|
padding: 2px 10px;
|
||
|
|
border-radius: 10px;
|
||
|
|
font-size: 11px;
|
||
|
|
font-weight: 600;
|
||
|
|
letter-spacing: .5px;
|
||
|
|
|
||
|
|
&.run { background: #1a3a1f; color: $status-run; border: 1px solid $status-run; }
|
||
|
|
&.warn { background: #3a2a00; color: $status-warn; border: 1px solid $status-warn; }
|
||
|
|
&.stop { background: #222; color: $text-muted; border: 1px solid $border; }
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── NAV BAR ───
|
||
|
|
.nav-bar {
|
||
|
|
height: 38px;
|
||
|
|
background: $bg-secondary;
|
||
|
|
border-bottom: 1px solid $border;
|
||
|
|
display: flex;
|
||
|
|
align-items: stretch;
|
||
|
|
overflow-x: auto;
|
||
|
|
flex-shrink: 0;
|
||
|
|
scrollbar-width: thin;
|
||
|
|
scrollbar-color: $border transparent;
|
||
|
|
&::-webkit-scrollbar { height: 3px; }
|
||
|
|
&::-webkit-scrollbar-thumb { background: $border; }
|
||
|
|
}
|
||
|
|
|
||
|
|
.nav-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 5px;
|
||
|
|
padding: 0 16px;
|
||
|
|
font-size: 12px;
|
||
|
|
font-weight: 500;
|
||
|
|
color: $text-secondary;
|
||
|
|
cursor: pointer;
|
||
|
|
white-space: nowrap;
|
||
|
|
border-bottom: 2px solid transparent;
|
||
|
|
transition: all .15s;
|
||
|
|
user-select: none;
|
||
|
|
|
||
|
|
&:hover { color: $text-primary; background: rgba(255,255,255,.03); }
|
||
|
|
&.active { color: $sms-highlight; border-bottom-color: $sms-highlight; background: rgba(0,200,255,.05); }
|
||
|
|
|
||
|
|
.nav-icon { font-size: 10px; font-family: $font-mono; color: $text-muted; letter-spacing: .5px; }
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── MAIN AREA ───
|
||
|
|
.main-area {
|
||
|
|
flex: 1;
|
||
|
|
overflow-y: auto;
|
||
|
|
padding: 14px;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 14px;
|
||
|
|
scrollbar-width: thin;
|
||
|
|
scrollbar-color: $border transparent;
|
||
|
|
&::-webkit-scrollbar { width: 6px; }
|
||
|
|
&::-webkit-scrollbar-thumb { background: $border; border-radius: 3px; }
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── FOOTER ───
|
||
|
|
.footer {
|
||
|
|
height: 26px;
|
||
|
|
background: $bg-secondary;
|
||
|
|
border-top: 1px solid $border;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
padding: 0 14px;
|
||
|
|
gap: 16px;
|
||
|
|
font-size: 11px;
|
||
|
|
color: $text-muted;
|
||
|
|
flex-shrink: 0;
|
||
|
|
|
||
|
|
.fp { display: flex; align-items: center; gap: 5px; }
|
||
|
|
.dot { width: 6px; height: 6px; border-radius: 50%; &.g { background: $accent-green; } &.y { background: $accent-yellow; } &.r { background: $accent-red; } }
|
||
|
|
}
|
||
|
|
</style>
|