初次提交

This commit is contained in:
2025-06-30 10:17:15 +08:00
commit 5446088524
989 changed files with 365987 additions and 0 deletions

View File

@ -0,0 +1,70 @@
<template>
<section id="app-main" class="app-main">
<div class="main-content">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<router-view :key="key" />
</keep-alive>
</transition>
</div>
</section>
</template>
<script>
export default {
name: 'AppMain',
components: {
},
computed: {
cachedViews() {
return this.$store.state.tagsView.cachedViews
},
key() {
return this.$route.path
}
}
}
</script>
<style lang="scss" scoped>
.app-main {
/* 50= navbar 50 */
height: calc(100vh - #{$nav-bar-height});
width: 100%;
min-width: (100% - #{$sideBarWidth});
position: relative;
overflow: hidden;
padding: $content-padding;
.main-content{
// height: calc(100vh - #{$nav-bar-height} - #{$content-padding}*2);
height: 100%;
overflow: hidden;
// background: #091a31;
}
}
.fixed-header+.app-main {
padding-top: 0;
}
.hasTagsView {
.app-main {
/* 84 = navbar + tags-view = 50 + 34 */
min-height: calc(100vh - #{$nav-bar-height});
}
.fixed-header+.app-main {
padding-top: 68px;
}
}
</style>
<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
.fixed-header {
padding-right: 15px;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,133 @@
<template>
<div class="upgrade-dialog">
<el-dialog
:append-to-body="false"
title="温馨提示"
center
:visible.sync="isUpgradeShow"
width="304px"
:close-on-click-modal="false"
destroy-on-close
:show-close="false"
>
<div class="upgrade-content">
为了安全考虑智慧管控平台将清除密码为666666的账户请于2023-09-21 18:00前修改密码如果登录不上请及时联系管理员!</div>
<div slot="footer" class="upgrade-btn">
<el-button class="close" @click="close">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
data() {
return {
isLoading: false,
isUpgradeShow: false,
version: '',
timer: null
}
},
created() {
if (+this.$store.getters.password === +666666) {
this.isUpgradeShow = false
}
// console.log(this.$store.getters.password)
},
methods: {
close() {
this.isUpgradeShow = false
this.timer = null
}
}
}
</script>
<style lang="scss" scoped>
::v-deep .el-dialog{
display: flex;
flex-direction: column;
margin:0 !important;
position:absolute;
top:50%;
left:50%;
transform:translate(-50%,-50%);
max-height:calc(100% - 30px);
max-width:calc(100% - 30px);
}
::v-deep .el-dialog .el-dialog__body{
flex:1;
overflow: auto;
}
::v-deep .el-dialog__header{
height: 30px;
line-height: -20px!important;
padding: 0!important;
border-bottom: 0!important;
.el-dialog__title{
display: inline-block!important;
height:30px;
line-height: -20px!important;
font-size: 14px;
}
}
.upgrade-btn{
width: 100%;
.close {
border: 1px solid #0094FF;
box-shadow: 4px 4px 24px 0px #0094FF80 inset;
background: #11304f;
color: #fff;
border-radius: 6px;
margin-right: 20px;
}
.sure {
border: 1px solid #00FFF6;
box-shadow: 4px 4px 24px 0px #00FFF6 inset;
background: #11304f;
color: #fff;
border-radius: 6px;
margin-left: 20px;
}
}
.upgrade-content{
width: 100%;
height: fit-content;
display: flex;
flex-direction: column;
align-items: center;
letter-spacing:2px;
text-indent:28px!important;
justify-content: center;
color: #fff;
.upgrade-item{
width: 100%;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
}
.version{
color: #0094FF;
font-size: 32px;
}
}
::v-deep .el-dialog{
//margin-top: 20vh!important;
width: 584px;
height: 230px;
border: none;
background: url(../../../assets/images/upgrade-bg.png) no-repeat;
background-size: 100% 100%;
.el-dialog__header{
padding: 6px;
}
}
</style>

View File

@ -0,0 +1,145 @@
<template>
<div class="drawer-container">
<div>
<h3 class="drawer-title">系统布局配置</h3>
<div class="drawer-item">
<span>换肤</span>
<theme-picker style="float: right;height: 26px;margin: -3px 8px 0 0;" @change="themeChange" />
</div>
<div class="drawer-item">
<span>开启 Tags-View</span>
<el-switch v-model="tagsView" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>固定 Header</span>
<el-switch v-model="fixedHeader" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>侧边栏 Logo</span>
<el-switch v-model="sidebarLogo" class="drawer-switch" />
</div>
<a v-if="isShowJob" href="https://panjiachen.github.io/vue-element-admin-site/zh/job/" target="_blank" class="job-link">
<el-alert
title="部门目前非常缺人!有兴趣的可以点击了解详情。坐标: 字节跳动"
type="success"
:closable="false"
/>
</a>
<div v-if="lang === 'zh'" class="drawer-item">
<span>菜单支持拼音搜索</span>
<el-switch v-model="supportPinyinSearch" class="drawer-switch" />
</div>
</div>
</div>
</template>
<script>
import ThemePicker from '@/components/ThemePicker'
export default {
components: { ThemePicker },
data() {
return {}
},
computed: {
isShowJob() {
return this.$store.getters.language === 'zh'
},
fixedHeader: {
get() {
return this.$store.state.settings.fixedHeader
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'fixedHeader',
value: val
})
}
},
tagsView: {
get() {
return this.$store.state.settings.tagsView
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'tagsView',
value: val
})
}
},
sidebarLogo: {
get() {
return this.$store.state.settings.sidebarLogo
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'sidebarLogo',
value: val
})
}
},
supportPinyinSearch: {
get() {
return this.$store.state.settings.supportPinyinSearch
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'supportPinyinSearch',
value: val
})
}
},
lang() {
return this.$store.getters.language
}
},
methods: {
themeChange(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'theme',
value: val
})
}
}
}
</script>
<style lang="scss" scoped>
.drawer-container {
padding: 24px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
.drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, .85);
font-size: 14px;
line-height: 22px;
}
.drawer-item {
color: rgba(0, 0, 0, .65);
font-size: 14px;
padding: 12px 0;
}
.drawer-switch {
float: right
}
.job-link{
display: block;
position: absolute;
width: 100%;
left: 0;
bottom: 0;
}
}
</style>

View File

@ -0,0 +1,26 @@
export default {
computed: {
device() {
return this.$store.state.app.device
}
},
mounted() {
// In order to fix the click on menu on the ios device will trigger the mouseleave bug
// https://github.com/PanJiaChen/vue-element-admin/issues/1135
this.fixBugIniOS()
},
methods: {
fixBugIniOS() {
const $subMenu = this.$refs.subMenu
if ($subMenu) {
const handleMouseleave = $subMenu.handleMouseleave
$subMenu.handleMouseleave = (e) => {
if (this.device === 'mobile') {
return
}
handleMouseleave(e)
}
}
}
}
}

View File

@ -0,0 +1,41 @@
<script>
export default {
name: 'MenuItem',
functional: true,
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
render(h, context) {
const { icon, title } = context.props
const vnodes = []
if (icon) {
if (icon.includes('iconfont')) {
vnodes.push(<i class={[icon, 'sub-el-icon']} />)
} else {
vnodes.push(<svg-icon icon-class={icon}/>)
}
}
if (title) {
vnodes.push(<span slot='title' style=' display:inline-block; white-space:nowrap;text-overflow:ellipsis;overflow:hidden;width: 160px;' >{(title)}</span>)
}
return vnodes
}
}
</script>
<style scoped>
.sub-el-icon {
color: currentColor;
width: 1em;
height: 1em;
}
</style>

View File

@ -0,0 +1,77 @@
<template>
<component :is="type" v-bind="linkProps(to)">
<slot />
</component>
</template>
<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import { checkPlatToken, GetApplyLargeScreenToken } from '@/api/user'
import { getToken } from '@/utils/auth'
export default {
props: {
to: {
type: String,
required: true
}
},
computed: {
isExternal() {
return isExternal(this.to)
},
type() {
if (this.isExternal) {
return 'a'
}
return 'router-link'
}
},
methods: {
resolvePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
return path.resolve(this.basePath, routePath)
},
handleMenuClick(path) {
if (this.type === 'div') {
checkPlatToken().then(res => {
if (res.data) {
GetApplyLargeScreenToken().then(response => {
const href = path.href + '?token=' + response.data.accessToken
window.open(href, '_blank')
})
} else {
this.$store.dispatch('user/logout')
this.$router.push(localStorage.getItem('loginPage'))
}
})
}
},
linkProps(to) {
if (this.isExternal) {
const token = getToken(token)
const params = to.split('?')
let path = ''
if (params.length > 1) {
path = to + '&token=' + token
} else {
path = to + '?token=' + token
}
return {
href: path,
target: '_blank',
rel: 'noopener'
}
}
return {
to: to
}
}
}
}
</script>

View File

@ -0,0 +1,132 @@
<template>
<div class="sidebar-logo-container" :class="{ collapse: collapse }">
<transition name="sidebarLogoFade">
<div v-if="collapse" class="sidebar-logo-link">
<img v-if="isDefaultLogo && logo" :src="logo" class="sidebar-logo" :style="logoStyle">
<img v-else-if="!isDefaultLogo && logo" :src="logoUrl ? logoUrl : logo" class="sidebar-logo">
</div>
<div v-else class="sidebar-logo-link">
<img v-if="isDefaultLogo && logo" :src="logo" class="sidebar-logo" :style="logoStyle">
<img v-else-if="!isDefaultLogo && logo" :src="logoUrl ? logoUrl : logo" class="sidebar-logo">
<h1 class="sidebar-title" :style="textStyle">{{ title }}</h1>
</div>
</transition>
</div>
</template>
<script>
import logo from '@/assets/images/logo.png'
export default {
name: 'SidebarLogo',
props: {
collapse: {
type: Boolean,
required: true
}
},
data() {
return {
title: this.$t('dashboard.controlPlatform'),
logo: null,
isDefaultLogo: false,
stationId: null,
logoStyle: {},
textStyle: {}
}
},
computed: {
currentStation() {
return this.$store.getters.currentStation || undefined
},
logoUrl() {
return this.$store.getters.logoUrl
},
userId() {
return this.$store.getters.userinfo.userId
},
language() {
return this.$store.getters.language || undefined
}
},
watch: {
currentStation: {
handler(val) {
if (val && val.id) {
this.stationId = val.id
this.logo = logo
this.isDefaultLogo = true
}
},
deep: true,
immediate: true
},
language: {
handler(val) {
this.title = this.$t('dashboard.controlPlatform')
},
deep: true
}
}
}
</script>
<style lang="scss" scoped>
.sidebarLogoFade-enter-active {
transition: opacity 1.5s;
}
.sidebarLogoFade-enter,
.sidebarLogoFade-leave-to {
opacity: 0;
}
.sidebar-logo-container {
// position: relative;
width: 100%;
height: $nav-bar-height;
line-height: $nav-bar-height;
background: linear-gradient(180deg, #024a7b 0%, rgba(0, 148, 255, 0) 94%);
text-align: center;
overflow: hidden;
& .sidebar-logo-link {
height: 100%;
width: 100%;
display: flex;
// flex-direction: row;
align-items: center;
justify-content: center;
padding: 0 2px;
& .sidebar-logo {
width: 32px;
height: 32px;
vertical-align: middle;
}
& .sidebar-title {
text-align: center;
white-space: nowrap;
// display: inline-block;
margin: 0;
color: #fff;
font-weight: 600;
height: 50px;
line-height: 50px;
width: 200px;
font-size: 19px;
font-weight: normal;
letter-spacing: 0.1em;
color: #edf8ff;
opacity: 0.9;
margin-left: 5px;
}
}
&.collapse {
.sidebar-logo {
margin-right: 0px;
}
}
}
</style>

View File

@ -0,0 +1,108 @@
<template>
<div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item.children,item)">
<app-link :to="resolvePath(onlyOneChild.url)">
<el-menu-item :index="resolvePath(onlyOneChild.url)" :class="{'submenu-title-noDropdown':!isNest,'activeCalss':resolvePath(onlyOneChild.path) === activePath , commonClass:resolvePath(onlyOneChild.path) !== activePath}">
<!-- <el-tooltip class="item" effect="dark" :content="generateTitle(onlyOneChild.name)" placement="right-start">
<span> <item :icon="'iconfont ' + item.icon " :title="generateTitle(onlyOneChild.name)" /></span>
</el-tooltip> -->
<item :icon="'iconfont ' + item.icon " :title="generateTitle(onlyOneChild.name)" />
</el-menu-item>
</app-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.url)" popper-:append-to-body="false">
<template slot="title">
<item :icon="'iconfont ' + item.icon " :title="generateTitle(item.name)" />
</template>
<sidebar-item
v-for="child in item.children"
:key="child.url"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.url)"
class="nest-menu"
:active-path="activePath"
/>
</el-submenu>
</div>
</template>
<script>
import path from 'path'
import { generateTitle } from '@/utils/i18n'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'
export default {
name: 'SidebarItem',
components: { Item, AppLink },
mixins: [FixiOSBug],
props: {
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
},
activePath: {
type: String,
default: ''
}
},
data() {
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
// TODO: refactor with render function
this.onlyOneChild = null
return {}
},
methods: {
hasOneShowingChild(children = [], parent) {
const showingChildren = children
// When there is only one child router, the child router is displayed by default
if (showingChildren.length > 0) {
return false
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
return true
}
return false
},
resolvePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
return path.resolve(this.basePath, routePath)
},
generateTitle
}
}
</script>
<style lang="scss" scoped>
.activeCalss{
background-color: $menuActiveBg !important;
color: $subMenuActiveText !important;
}
.commonClass{
background-color: $menuBg !important;
color: $menuText !important;
}
</style>

View File

@ -0,0 +1,282 @@
<template>
<div :class="{ 'has-logo': showLogo }">
<logo :collapse="isCollapse" />
<div v-show="!isCollapse" style="padding: 10px">
<el-cascader
:key="showKey"
v-model="selectData"
:options="stations"
filterable
:props="{ label: 'name', children: 'list', value: 'id', emitPath: false, }"
:show-all-levels="false"
@change="handleToSelect"
/>
</div>
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item
v-for="route in menuList"
:key="route.id"
:item="route"
:base-path="route.url"
:active-path="activePath"
/>
</el-menu>
</el-scrollbar>
<div class="open-btn" @click="handleClickOutside">
<i class="iconfont icon-celanyincang" />
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { windowResize } from '@/utils/index'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
// import { GetExclusiveMenu } from '@/api/user'
import {
getProvinceStation,
GetLogo
} from '@/api/station/maintain'
import { findItemsWithSameId } from '@/utils/index'
export default {
components: { SidebarItem, Logo },
data() {
return {
stationId: undefined,
index: '',
stations: [],
showKey: 0,
selectData: [],
taitanStationids: [579, 576, 578, 577, 384, 481, 512],
rundaStationIds: [591, 721, 422],
qingtaoStationIds: [996, 997, 999, 1000, 1001]
}
},
computed: {
...mapGetters(['sidebar']),
username() {
return this.$store.getters.userinfo.username
},
menuList() {
return this.$store.getters.menuList
},
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
activePath() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
},
activeMenuName() {
return this.$store.getters.activeMenuName
},
isScreenEnter() {
return this.$store.state.user.isScreenEnter
},
language() {
return this.$store.getters.language || undefined
}
},
watch: {
language: {
async handler() {
if (this.stationId) {
this.$store.dispatch('user/getNewMenu', {
id: this.stationId,
path: this.$route.path
})
await this.$store.dispatch('user/getInfo')
this.$store.dispatch('user/getAllDict')
this.getProvinceStation()
this.$store.dispatch('user/getStationByUser')
}
},
deep: true
},
activeMenu: {
handler(val) {
if (val) {
this.changeIco()
if (val === '/task/all-task-detail') {
localStorage.setItem(this.activeMenuName, '/task/all-task')
} else {
localStorage.setItem(this.activeMenuName, val)
}
}
},
deep: true
},
$route: {
handler: function(route) {
setTimeout(() => {
windowResize()
}, 300)
},
immediate: true
},
isScreenEnter: {
handler(val) {
if (val) {
// this.selectData = ['浙江', 349]
}
},
deep: true,
immediate: true
}
},
created() {
this.getProvinceStation()
},
methods: {
async getProvinceStation() {
const res = await getProvinceStation()
this.stations = res.data.list
this.stations.map((i) => {
i.id = i.name
return i
})
if (localStorage.getItem('currentStationId')) {
this.selectData = +localStorage.getItem('currentStationId')
this.handleToSelect(+localStorage.getItem('currentStationId'))
} else {
this.selectData = this.stations[0].list[0].list[0].id
this.handleToSelect(this.selectData)
}
},
handleClickOutside() {
this.$store.dispatch('app/toggleSideBar')
},
// 修改Favicon的方法
changeFavicon(link) {
let $favicon = document.querySelector('link[rel="icon"]')
if ($favicon !== null) {
$favicon.href = link
} else {
$favicon = document.createElement('link')
$favicon.rel = 'icon'
$favicon.href = link
document.head.appendChild($favicon)
}
},
// 根据传递的参数修改Favicon
changeIco(id) {
},
async getLogo() {
const res = await GetLogo(this.stationId)
if (res.data) {
this.$store.commit('user/SET_LOGOURL', res.data.url)
} else {
this.$store.commit('user/SET_LOGOURL', null)
}
},
async handleToSelect(val) {
const item = findItemsWithSameId(this.stations, val)
this.stationId = val
localStorage.setItem('stationAddress', item[0].address)
this.changeIco(val)
this.$store.commit(
'user/SET_SELECT_ID',
val
)
this.$store.commit(
'user/SET_CURRENT_STATION',
item[0]
)
this.$store.dispatch('user/getNewMenu', {
id: val,
path: this.$route.path
})
this.getLogo()
this.showKey++
}
}
}
</script>
<style lang="scss" scoped>
.company-wrapper {
width: $sideBarWidth;
height: $nav-bar-height;
display: flex;
align-items: center;
flex-direction: row;
justify-content: center;
img {
width: 26px;
height: 26px;
}
.name {
font-size: 22px;
font-weight: normal;
letter-spacing: 0.1em;
color: #edf8ff;
opacity: 0.9;
margin-left: 5px;
}
.mobileName {
font-size: 20px !important;
color: #fff;
}
.mobileLogo {
width: 50px;
height: 50px;
}
.enName {
font-size: 24px;
max-width: 400px;
letter-spacing: 0;
}
}
/deep/.el-select-dropdown__item {
width: 50px !important;
}
::v-deep .el-input--small .el-input__inner {
width: calc(#{$sideBarWidth} - 20px) !important;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
<slot />
</el-scrollbar>
</template>
<script>
const tagAndTagSpacing = 4 // tagAndTagSpacing
export default {
name: 'ScrollPane',
data() {
return {
left: 0
}
},
computed: {
scrollWrapper() {
return this.$refs.scrollContainer.$refs.wrap
}
},
mounted() {
this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
},
beforeDestroy() {
this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
},
methods: {
handleScroll(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40
const $scrollWrapper = this.scrollWrapper
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
},
emitScroll() {
this.$emit('scroll')
},
moveToTarget(currentTag) {
const $container = this.$refs.scrollContainer.$el
const $containerWidth = $container.offsetWidth
const $scrollWrapper = this.scrollWrapper
const tagList = this.$parent.$refs.tag
let firstTag = null
let lastTag = null
// find first tag and last tag
if (tagList.length > 0) {
firstTag = tagList[0]
lastTag = tagList[tagList.length - 1]
}
if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = 0
} else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
} else {
// find preTag and nextTag
const currentIndex = tagList.findIndex(item => item === currentTag)
const prevTag = tagList[currentIndex - 1]
const nextTag = tagList[currentIndex + 1]
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
}
}
}
}
}
</script>
<style lang="scss" scoped>
.scroll-container {
white-space: nowrap;
position: relative;
overflow: hidden;
width: 100%;
::v-deep {
.el-scrollbar__bar {
bottom: 0px;
}
.el-scrollbar__wrap {
height: $nav-bar-height;
}
}
}
</style>

View File

@ -0,0 +1,303 @@
<template>
<div id="tags-view-container" class="tags-view-container">
<scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
<img :src="tagview_colspan" alt="" class="tagview_colspan" @click.prevent.stop="toggleSideBar">
<router-link
v-for="tag in visitedViews"
ref="tag"
:key="tag.path"
:class="isActive(tag)?'active':''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span"
class="tags-view-item"
@click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
@contextmenu.prevent.native="openMenu(tag,$event)"
>
<img v-if="!isActive(tag)" :src="tagview" class="tags-view-icon-close">
<img v-if="isActive(tag)" :src="tagview_active" class="tags-view-icon-close">
<span class="route-name">{{ generateTitle(tag.title) }}</span>
<img v-if="!isAffix(tag) && !isActive(tag)" :src="tagview_close" class="tags-view-icon-close" @click.prevent.stop="closeSelectedTag(tag)">
<img v-if="!isAffix(tag) && isActive(tag)" :src="tagview_close_active" class="tags-view-icon-close" @click.prevent.stop="closeSelectedTag(tag)">
</router-link>
</scroll-pane>
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)">刷新</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li>
<li @click="closeOthersTags">关闭其它</li>
<li @click="closeAllTags(selectedTag)">关闭所有</li>
</ul>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import ScrollPane from './ScrollPane'
import { generateTitle } from '@/utils/i18n'
import tagview_close from '@/assets/icon/tagview_close.png'
import tagview_close_active from '@/assets/icon/tagview_close_active.png'
import tagview_colspan from '@/assets/icon/tagview_colspan.png'
import tagview from '@/assets/icon/tagview.png'
import tagview_active from '@/assets/icon/tagview_active.png'
import path from 'path'
export default {
components: { ScrollPane },
data() {
return {
tagview_close,
tagview_close_active,
tagview_colspan,
tagview,
tagview_active,
visible: false,
top: 0,
left: 0,
selectedTag: {},
affixTags: []
}
},
computed: {
...mapGetters(['sidebar', 'device']),
visitedViews() {
return this.$store.state.tagsView.visitedViews
},
routes() {
return [{
fullPath: '/dashboard',
path: 'dashboard',
name: 'Dashboard',
meta: { title: 'dashboard', icon: 'dashboard', affix: true }
}]
}
},
watch: {
$route() {
this.addTags()
this.moveToCurrentTag()
},
visible(value) {
if (value) {
document.body.addEventListener('click', this.closeMenu)
} else {
document.body.removeEventListener('click', this.closeMenu)
}
}
},
mounted() {
this.initTags()
this.addTags()
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
generateTitle, // generateTitle by vue-i18n
isActive(route) {
return route.path === this.$route.path
},
isAffix(tag) {
return tag.meta && tag.meta.affix
},
filterAffixTags(routes, basePath = '/') {
let tags = []
routes.forEach(route => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path)
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta }
})
}
if (route.children) {
const tempTags = this.filterAffixTags(route.children, route.path)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags]
}
}
})
return tags
},
initTags() {
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
for (const tag of affixTags) {
// Must have tag name
if (tag.name) {
this.$store.dispatch('tagsView/addVisitedView', tag)
}
}
},
addTags() {
const { name } = this.$route
if (name) {
this.$store.dispatch('tagsView/addView', this.$route)
}
return false
},
moveToCurrentTag() {
const tags = this.$refs.tag
this.$nextTick(() => {
for (const tag of tags) {
if (tag.to.path === this.$route.path) {
this.$refs.scrollPane.moveToTarget(tag)
// when query is different then update
if (tag.to.fullPath !== this.$route.fullPath) {
this.$store.dispatch('tagsView/updateVisitedView', this.$route)
}
break
}
}
})
},
refreshSelectedTag(view) {
this.$store.dispatch('tagsView/delCachedView', view).then(() => {
const { fullPath } = view
this.$nextTick(() => {
this.$router.replace({
path: '/redirect' + fullPath
})
})
})
},
closeSelectedTag(view) {
this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
if (this.isActive(view)) {
this.toLastView(visitedViews, view)
}
})
},
closeOthersTags() {
this.$router.push(this.selectedTag)
this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
this.moveToCurrentTag()
})
},
closeAllTags(view) {
this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
if (this.affixTags.some(tag => tag.path === view.path)) {
return
}
this.toLastView(visitedViews, view)
})
},
toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
this.$router.push(latestView.fullPath)
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view.name === 'Dashboard') {
// to reload home page
this.$router.replace({ path: '/redirect' + view.fullPath })
} else {
this.$router.push(localStorage.getItem('loginPage'))
}
}
},
openMenu(tag, e) {
const menuMinWidth = 105
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
const offsetWidth = this.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const left = e.clientX - offsetLeft + 15 // 15: margin right
if (left > maxLeft) {
this.left = maxLeft
} else {
this.left = left
}
this.top = e.clientY - 20
this.visible = true
this.selectedTag = tag
},
closeMenu() {
this.visible = false
},
handleScroll() {
this.closeMenu()
}
}
}
</script>
<style lang="scss" scoped>
.tags-view-container {
height: $tag-view-height;
width: 100%;
background: $tagview-background;
border-bottom: $parting-line;
position: relative;
.tags-view-wrapper {
position: absolute;
top: 2px;
left :0;
overflow: hidden;
.tagview_colspan{
width: 20px;
height: 20px;
cursor: pointer;
margin: 0 10px;
}
.tags-view-item {
display: inline-block;
position: relative;
cursor: pointer;
height: $tag-view-height;
line-height: $tag-view-height;
color: $text-color;
padding: 0 10px;
font-size: 14px;
margin-left: 5px;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
background-color: $content-background;
color: $menuActiveText;
border-left: $parting-line;
border-right: $parting-line;
border-bottom: none;
}
.tags-view-icon-close{
width: 15px;
height: 15px;
position: relative;
top: 2px;
right: 0;
}
.route-name{
display: inline-block;
padding-right: 6px;
}
}
}
.contextmenu {
margin: 0;
background: $content-background;
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #fff;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: $parting-line-color;
}
}
}
}
</style>

View File

@ -0,0 +1,153 @@
<template>
<div class="upgrade-dialog">
<el-dialog
:append-to-body="false"
title="检测到版本更新"
center
:visible.sync="isUpgradeShow"
width="584px"
:close-on-click-modal="false"
destroy-on-close
:show-close="false"
>
<div class="upgrade-content">
<div class="upgrade-item version">V {{ version }}</div>
<div class="upgrade-item desc">
<div class="circle" />
<div class="name">新的模块</div>
</div>
<div class="upgrade-item desc">
<div class="circle" />
<div class="name">安全保障</div>
</div>
<div class="upgrade-item desc">
<div class="circle" />
<div class="name">更多服务</div>
</div>
</div>
<div slot="footer" class="upgrade-btn">
<el-button class="close" @click="close">暂不更新</el-button>
<el-button type="primary" class="sure" @click="sure">马上更新</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import config from '../../../../package.json'
export default {
data() {
return {
isLoading: false,
isUpgradeShow: false,
version: '',
timer: null
}
},
created() {
if (!localStorage.getItem('hoenergy_control_version')) {
localStorage.setItem('hoenergy_control_version', process.env.__NEXT_VERSION__)
}
this.checkedVersion()
self.timer = setInterval(() => {
this.checkedVersion()
}, 1000 * 60 * 30)
},
methods: {
checkedVersion() {
const self = this
if (config.version !== localStorage.getItem('hoenergy_control_version')) {
self.isUpgradeShow = true
this.version = config.version
} else {
self.isUpgradeShow = false
}
},
sure() {
this.isUpgradeShow = false
clearInterval(this.timer)
this.timer = null
localStorage.setItem('hoenergy_control_version', config.version)
window.location.reload()
},
close() {
this.isUpgradeShow = false
clearInterval(this.timer)
this.timer = null
}
}
}
</script>
<style lang="scss" scoped>
::v-deep .el-dialog__header{
border-bottom: 0!important;
}
.upgrade-btn{
width: 100%;
.close {
border: 1px solid #0094FF;
box-shadow: 4px 4px 24px 0px #0094FF80 inset;
background: #11304f;
color: #fff;
border-radius: 6px;
margin-right: 20px;
}
.sure {
border: 1px solid #00FFF6;
box-shadow: 4px 4px 24px 0px #00FFF6 inset;
background: #11304f;
color: #fff;
border-radius: 6px;
margin-left: 20px;
}
}
.upgrade-content{
width: 100%;
height: fit-content;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.upgrade-item{
width: 100%;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
}
.version{
color: #0094FF;
font-size: 32px;
}
.desc{
.circle{
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #fff;
}
.name{
color: #eee;
font-size: 20px;
padding-left: 10px;
}
}
}
::v-deep .el-dialog{
//margin-top: 20vh!important;
width: 584px;
height: 347px;
border: none;
background: url(../../../assets/images/upgrade-bg.png) no-repeat;
background-size: 100% 100%;
.el-dialog__header{
padding: 6px;
}
}
</style>

View File

@ -0,0 +1,7 @@
export { default as AppMain } from './AppMain'
export { default as Navbar } from './Navbar'
export { default as Settings } from './Settings'
export { default as Sidebar } from './Sidebar/index.vue'
export { default as TagsView } from './TagsView/index.vue'
export { default as Upgrade } from './Upgrade/index.vue'
export { default as PasswordMessage } from './PasswordMessage/index.vue'

139
src/layout/index.vue Normal file
View File

@ -0,0 +1,139 @@
<template>
<div :class="classObj" class="app-wrapper">
<!-- <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" /> -->
<!-- <div v-if="device==='mobile'&&sidebar.opened" @click="handleClickOutside" /> -->
<navbar />
<sidebar class="sidebar-container" />
<div :class="{ hasTagsView: needTagsView }" class="main-container">
<div :class="{ 'fixed-header': fixedHeader }">
<tags-view v-if="needTagsView" />
</div>
<app-main />
<right-panel v-if="showSettings">
<settings />
</right-panel>
</div>
<!-- <Upgrade /> -->
<PasswordMessage />
</div>
</template>
<script>
import RightPanel from '@/components/RightPanel'
import { AppMain, Navbar, Settings, Sidebar, TagsView, PasswordMessage } from './components'
import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'
export default {
name: 'Layout',
components: {
AppMain,
Navbar,
RightPanel,
Settings,
Sidebar,
TagsView,
PasswordMessage
},
mixins: [ResizeMixin],
data() {
return {
theme: {
// 'url(../assets/images/dashboardBg.png)',
'--main-bg-img': 'url(' + require('../assets/images/dashboardBg.png') + ')',
'--table-bg': 'rgba(4, 48, 80, 0.6)',
'--topNav-bg': 'linear-gradient(91deg,rgba(2, 74, 123, 0.6) 0%,rgba(2, 61, 100, 0) 94%)',
'--item-header-bg': 'url(' + require('../assets/images/home-item-header.png') + ')',
'--font-family': 'PangMenZhengDao'
}
}
},
computed: {
...mapState({
sidebar: state => state.app.sidebar,
device: state => state.app.device,
showSettings: state => state.settings.showSettings,
needTagsView: state => state.settings.tagsView,
fixedHeader: state => state.settings.fixedHeader
}),
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation
// mobile: this.device === 'mobile'
}
}
},
created() {
if (this.$store.getters.config?.title === 'th') {
const result = JSON.parse(this.$store.getters.config.theme)
for (const [key, value] of Object.entries(result)) {
document.getElementsByTagName('body')[0].style.setProperty(key, value)
}
document.getElementsByTagName('body')[0].style.setProperty('--item-header-bg', 'url(' + require('../assets/images/home-item-header-dark.png') + ')')
document.getElementsByTagName('body')[0].style.setProperty('--font-family', 'HYQY')
} else {
for (const [key, value] of Object.entries(this.theme)) {
document.getElementsByTagName('body')[0].style.setProperty(key, value)
}
}
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/styles/mixin.scss";
@import "~@/styles/variables.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
background: var(--main-bg-img);
overflow: hidden;
background-size: 100% 100%;
background-attachment: fixed;
// &.mobile.openSidebar {
// position: fixed;
// top: 0;
// }
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed;
top: $nav-bar-height;
right: 0;
z-index: 9;
width: calc(100% - #{$sideBarWidth});
transition: width 0.28s;
}
// .hideSidebar .fixed-header {
// width: calc(100% - #{$nav-bar-height})
// }
.mobile .fixed-header {
width: 100%;
}
</style>

View File

@ -0,0 +1,42 @@
const { body } = document
const WIDTH = 1024 // refer to Bootstrap's responsive design
export default {
watch: {
$route(route) {
if (this.device === 'mobile' && this.sidebar.opened) {
// store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
},
beforeMount() {
window.addEventListener('resize', this.$_resizeHandler)
},
beforeDestroy() {
window.removeEventListener('resize', this.$_resizeHandler)
},
mounted() {
const isMobile = this.$_isMobile()
if (isMobile || this.$store.getters.menuList.length === 0) {
// store.dispatch('app/toggleDevice', 'mobile')
// store.dispatch('app/closeSideBar', { withoutAnimation: true })
}
},
methods: {
// use $_ for mixins properties
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
$_isMobile() {
const rect = body.getBoundingClientRect()
return rect.width - 1 < WIDTH
},
$_resizeHandler() {
if (!document.hidden) {
const isMobile = this.$_isMobile() || this.$store.getters.menuList.length === 0
// store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'mobile')
if (isMobile) {
// store.dispatch('app/closeSideBar', { withoutAnimation: true })
}
}
}
}
}