<template>
|
<el-container class="layout">
|
<app-header></app-header>
|
<el-container class="layout-body">
|
<el-aside width="228px" class="layout-aside">
|
<div class="aside-caption">业务导航</div>
|
<el-menu ref="sideMenu" :default-active="$route.path" router background-color="#f5f8fb" text-color="#2f4257" active-text-color="#1769aa" unique-opened>
|
<menu-node v-for="item in menus" :key="item.id" :item="item"></menu-node>
|
</el-menu>
|
</el-aside>
|
<el-main class="layout-main">
|
<div class="mdi-tabbar">
|
<div class="mdi-caption"><i class="el-icon-document-copy"></i><span>工作区</span></div>
|
<div ref="tabScroll" class="mdi-tabs-scroll">
|
<div class="mdi-tabs">
|
<div
|
v-for="doc in openDocs"
|
:key="doc.fullPath"
|
class="mdi-tab"
|
:class="{ 'is-active': isActiveDoc(doc) }"
|
:title="doc.title"
|
@click="activateDoc(doc)"
|
>
|
<i :class="doc.icon"></i>
|
<span class="mdi-tab-title">{{ doc.title }}</span>
|
<i v-if="doc.pinned" class="el-icon-lock mdi-tab-pin"></i>
|
<i v-else class="el-icon-close mdi-tab-close" title="关闭" @click.stop="closeDoc(doc)"></i>
|
</div>
|
</div>
|
</div>
|
<div class="mdi-actions">
|
<el-dropdown trigger="click" @command="handleCommand">
|
<button class="mdi-actions-button" type="button" title="文档操作"><i class="el-icon-more"></i></button>
|
<el-dropdown-menu slot="dropdown">
|
<el-dropdown-item command="closeCurrent" :disabled="!activeDoc || activeDoc.pinned">关闭当前</el-dropdown-item>
|
<el-dropdown-item command="closeOthers" :disabled="!canCloseOthers">关闭其他</el-dropdown-item>
|
<el-dropdown-item command="closeAll" :disabled="openDocs.length <= 1">关闭全部</el-dropdown-item>
|
</el-dropdown-menu>
|
</el-dropdown>
|
</div>
|
</div>
|
<div ref="workspace" class="mdi-workspace" @scroll="rememberScroll">
|
<router-view v-slot="{ Component, route }">
|
<keep-alive :include="cachedNames" :max="20">
|
<component :is="Component" :key="viewKey(route)" />
|
</keep-alive>
|
</router-view>
|
</div>
|
</el-main>
|
</el-container>
|
</el-container>
|
</template>
|
|
<script>
|
import api from '@/api'
|
import AppHeader from '@/components/AppHeader.vue'
|
import MenuNode from '@/components/MenuNode.vue'
|
|
const HOME_DOC = {
|
fullPath: '/dashboard',
|
title: '首页门户',
|
icon: 'el-icon-s-home',
|
cacheName: 'Dashboard',
|
pinned: true,
|
version: 1
|
}
|
|
const REPORT_TITLES = {
|
overview: '装备故障总体分析',
|
'top-equipment': '故障高发设备Top',
|
'by-team': '按T统计',
|
contractors: '厂家维修情况',
|
trend: '年度趋势'
|
}
|
|
export default {
|
name: 'AppLayout',
|
components: { AppHeader, MenuNode },
|
data() {
|
return {
|
menus: [],
|
openDocs: [],
|
currentFullPath: '',
|
cacheVersions: {},
|
scrollTops: {}
|
}
|
},
|
computed: {
|
activeDoc() {
|
return this.openDocs.find(doc => doc.fullPath === this.currentFullPath) || null
|
},
|
cachedNames() {
|
return [...new Set(this.openDocs.map(doc => doc.cacheName).filter(Boolean))]
|
},
|
canCloseOthers() {
|
const keepCount = this.activeDoc && this.activeDoc.pinned ? 1 : 2
|
return this.openDocs.length > keepCount
|
}
|
},
|
watch: {
|
'$route': {
|
immediate: true,
|
handler(route) { this.syncRoute(route) }
|
}
|
},
|
created() {
|
this.loadMenus()
|
},
|
methods: {
|
loadMenus() {
|
api.get('/system/menu/nav').then(res => {
|
this.menus = (res.data || []).filter(item => item.visible !== 0)
|
}).catch(() => {})
|
},
|
syncRoute(route) {
|
if (!route || route.path === '/login') return
|
if (this.currentFullPath && this.$refs.workspace) {
|
this.scrollTops[this.currentFullPath] = this.$refs.workspace.scrollTop
|
}
|
this.ensureHomeDoc()
|
const fullPath = route.fullPath
|
let doc = this.openDocs.find(item => item.fullPath === fullPath)
|
if (!doc) {
|
doc = {
|
fullPath,
|
title: this.resolveTitle(route),
|
icon: this.resolveIcon(route.path),
|
cacheName: route.meta.cacheName || route.path,
|
pinned: false,
|
version: (this.cacheVersions[fullPath] || 0) + 1
|
}
|
this.openDocs.push(doc)
|
} else {
|
doc.title = this.resolveTitle(route)
|
doc.icon = this.resolveIcon(route.path)
|
doc.cacheName = route.meta.cacheName || doc.cacheName
|
}
|
this.currentFullPath = fullPath
|
this.$nextTick(() => {
|
this.restoreScroll()
|
this.scrollActiveTab()
|
})
|
},
|
ensureHomeDoc() {
|
if (!this.openDocs.some(doc => doc.pinned)) {
|
this.openDocs.unshift({ ...HOME_DOC })
|
}
|
},
|
resolveTitle(route) {
|
if (route.path.startsWith('/reports/')) {
|
return REPORT_TITLES[route.params.type] || route.meta.title || '统计报表'
|
}
|
return route.meta.title || route.name || route.path
|
},
|
resolveIcon(path) {
|
if (path === '/dashboard') return 'el-icon-s-home'
|
if (path === '/fault/report') return 'el-icon-edit-outline'
|
if (path.includes('/approval/') || path === '/fault/review') return 'el-icon-s-check'
|
if (path === '/fault/dispatch') return 'el-icon-s-promotion'
|
if (path.startsWith('/fault/completion') || path === '/completion/report') return 'el-icon-finished'
|
if (path.startsWith('/acceptance/')) return 'el-icon-circle-check'
|
if (path.startsWith('/reports/')) return 'el-icon-data-analysis'
|
if (path.startsWith('/system/')) return 'el-icon-setting'
|
return 'el-icon-document'
|
},
|
isActiveDoc(doc) { return doc.fullPath === this.currentFullPath },
|
viewKey(route) {
|
const doc = this.openDocs.find(item => item.fullPath === route.fullPath)
|
return route.fullPath + '#' + (doc ? doc.version : 0)
|
},
|
activateDoc(doc) {
|
if (!this.isActiveDoc(doc)) this.$router.push(doc.fullPath).catch(() => {})
|
},
|
closeDoc(doc) {
|
if (!doc || doc.pinned) return
|
const index = this.openDocs.findIndex(item => item.fullPath === doc.fullPath)
|
if (index < 0) return
|
const wasActive = this.isActiveDoc(doc)
|
this.cacheVersions[doc.fullPath] = doc.version + 1
|
this.openDocs.splice(index, 1)
|
if (wasActive) {
|
const target = this.openDocs[index - 1] || this.openDocs[index] || this.openDocs[0]
|
this.$router.push(target.fullPath).catch(() => {})
|
} else {
|
this.$nextTick(this.scrollActiveTab)
|
}
|
},
|
handleCommand(command) {
|
if (command === 'closeCurrent') this.closeDoc(this.activeDoc)
|
if (command === 'closeOthers') this.closeOtherDocs()
|
if (command === 'closeAll') this.closeAllDocs()
|
},
|
closeOtherDocs() {
|
if (!this.activeDoc) return
|
const keepPath = this.activeDoc.fullPath
|
this.openDocs.filter(doc => !doc.pinned && doc.fullPath !== keepPath).forEach(doc => {
|
this.cacheVersions[doc.fullPath] = doc.version + 1
|
})
|
this.openDocs = this.openDocs.filter(doc => doc.pinned || doc.fullPath === keepPath)
|
this.$nextTick(this.scrollActiveTab)
|
},
|
closeAllDocs() {
|
this.openDocs.filter(doc => !doc.pinned).forEach(doc => {
|
this.cacheVersions[doc.fullPath] = doc.version + 1
|
})
|
this.openDocs = this.openDocs.filter(doc => doc.pinned)
|
this.$router.push('/dashboard').catch(() => {})
|
},
|
rememberScroll() {
|
if (this.currentFullPath && this.$refs.workspace) {
|
this.scrollTops[this.currentFullPath] = this.$refs.workspace.scrollTop
|
}
|
},
|
restoreScroll() {
|
if (this.$refs.workspace) this.$refs.workspace.scrollTop = this.scrollTops[this.currentFullPath] || 0
|
},
|
scrollActiveTab() {
|
const active = this.$refs.tabScroll && this.$refs.tabScroll.querySelector('.mdi-tab.is-active')
|
if (active) active.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
}
|
}
|
}
|
</script>
|
|
<style scoped>
|
.layout { display: flex; flex-direction: column; height: 100%; }
|
.layout-body { flex: 1; min-height: 0; }
|
.layout-aside { border-right: 1px solid #d9e2eb; background: #f5f8fb; overflow-x: hidden; overflow-y: auto; }
|
.aside-caption { padding: 14px 18px 8px; color: #8090a2; font-size: 12px; letter-spacing: .08em; }
|
.layout-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; padding: 0; overflow: hidden; background: #eef2f6; }
|
.el-menu { border-right: 0; }
|
.layout-aside ::v-deep .el-menu-item,
|
.layout-aside ::v-deep .el-submenu__title {
|
height: auto;
|
min-height: 50px;
|
padding-top: 8px;
|
padding-bottom: 8px;
|
line-height: 20px;
|
white-space: normal;
|
}
|
.layout-aside ::v-deep .el-menu-item span,
|
.layout-aside ::v-deep .el-submenu__title span {
|
flex: 1;
|
min-width: 0;
|
white-space: normal;
|
overflow-wrap: anywhere;
|
word-break: break-word;
|
}
|
.mdi-tabbar { display: flex; flex: 0 0 40px; align-items: stretch; height: 40px; padding-top: 5px; border-bottom: 1px solid #cfdae5; background: #e3eaf1; }
|
.mdi-caption { display: flex; flex: 0 0 auto; align-items: center; gap: 6px; padding: 0 12px; border-right: 1px solid rgba(125, 147, 169, .22); color: #64788d; font-size: 12px; }
|
.mdi-caption i { color: #2c7fb8; font-size: 15px; }
|
.mdi-tabs-scroll { flex: 1; min-width: 0; overflow-x: auto; overflow-y: hidden; }
|
.mdi-tabs { display: inline-flex; align-items: flex-end; min-width: 100%; height: 100%; padding: 0 4px; }
|
.mdi-tab { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 6px; max-width: 220px; height: 34px; margin-right: 2px; padding: 0 9px; border: 1px solid transparent; border-bottom: 0; border-radius: 4px 4px 0 0; color: #5c6f82; background: rgba(255, 255, 255, .42); cursor: pointer; font-size: 13px; user-select: none; }
|
.mdi-tab:hover { color: #245275; background: rgba(255, 255, 255, .72); }
|
.mdi-tab.is-active { height: 35px; color: #1f3348; background: #eef2f6; border-color: #cfdae5; box-shadow: inset 0 2px 0 #2c7fb8; }
|
.mdi-tab > i { flex: 0 0 auto; }
|
.mdi-tab-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.mdi-tab-close, .mdi-tab-pin { width: 16px; color: #8191a2; text-align: center; }
|
.mdi-tab-close { border-radius: 50%; font-size: 12px; }
|
.mdi-tab-close:hover { color: #fff; background: #d9534f; }
|
.mdi-tab-pin { font-size: 12px; }
|
.mdi-actions { display: flex; flex: 0 0 auto; align-items: center; padding: 0 6px; border-left: 1px solid rgba(125, 147, 169, .22); }
|
.mdi-actions-button { width: 30px; height: 28px; padding: 0; border: 0; border-radius: 3px; color: #587087; background: transparent; cursor: pointer; font-size: 17px; }
|
.mdi-actions-button:hover { color: #1f5f91; background: rgba(255, 255, 255, .68); }
|
.mdi-workspace { position: relative; flex: 1; min-height: 0; overflow: auto; background: #eef2f6; }
|
</style>
|