Initial commit: freedom-web frontend project
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="error-store">
|
||||
<Badge dot :count="countComputed">
|
||||
<Button type="text" @click="openErrorLoggerPage">
|
||||
<Icon :size="20" type="ios-bug"/>
|
||||
</Button>
|
||||
</Badge>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ErrorStore',
|
||||
props: {
|
||||
count: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
hasRead: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
countComputed () {
|
||||
return this.hasRead ? 0 : this.count
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openErrorLoggerPage () {
|
||||
this.$router.push({
|
||||
name: 'error_logger_page'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.error-store{
|
||||
margin-right: 12px;
|
||||
.ivu-badge-dot{
|
||||
top: 20px;
|
||||
}
|
||||
.ivu-btn.ivu-btn-text{
|
||||
padding: 5px 1px 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
import ErrorStore from './error-store.vue'
|
||||
export default ErrorStore
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div v-if="showFullScreenBtn" class="full-screen-btn-con">
|
||||
<Tooltip :content="value ? '退出全屏' : '全屏'" placement="bottom">
|
||||
<Icon @click.native="handleChange" :type="value ? 'md-contract' : 'md-expand'" :size="23"></Icon>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Fullscreen',
|
||||
computed: {
|
||||
showFullScreenBtn () {
|
||||
return window.navigator.userAgent.indexOf('MSIE') < 0
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleFullscreen () {
|
||||
let main = document.body
|
||||
if (this.value) {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen()
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen()
|
||||
} else if (document.webkitCancelFullScreen) {
|
||||
document.webkitCancelFullScreen()
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen()
|
||||
}
|
||||
} else {
|
||||
if (main.requestFullscreen) {
|
||||
main.requestFullscreen()
|
||||
} else if (main.mozRequestFullScreen) {
|
||||
main.mozRequestFullScreen()
|
||||
} else if (main.webkitRequestFullScreen) {
|
||||
main.webkitRequestFullScreen()
|
||||
} else if (main.msRequestFullscreen) {
|
||||
main.msRequestFullscreen()
|
||||
}
|
||||
}
|
||||
},
|
||||
handleChange () {
|
||||
this.handleFullscreen()
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
let isFullscreen = document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen
|
||||
isFullscreen = !!isFullscreen
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
this.$emit('input', !this.value)
|
||||
this.$emit('on-change', !this.value)
|
||||
})
|
||||
document.addEventListener('mozfullscreenchange', () => {
|
||||
this.$emit('input', !this.value)
|
||||
this.$emit('on-change', !this.value)
|
||||
})
|
||||
document.addEventListener('webkitfullscreenchange', () => {
|
||||
this.$emit('input', !this.value)
|
||||
this.$emit('on-change', !this.value)
|
||||
})
|
||||
document.addEventListener('msfullscreenchange', () => {
|
||||
this.$emit('input', !this.value)
|
||||
this.$emit('on-change', !this.value)
|
||||
})
|
||||
this.$emit('input', isFullscreen)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.full-screen-btn-con .ivu-tooltip-rel{
|
||||
height: 64px;
|
||||
line-height: 56px;
|
||||
i{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
import Fullscreen from './fullscreen.vue'
|
||||
export default Fullscreen
|
||||
@@ -0,0 +1,4 @@
|
||||
.custom-bread-crumb{
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="custom-bread-crumb">
|
||||
<Breadcrumb :style="{fontSize: `${fontSize}px`}">
|
||||
<BreadcrumbItem v-for="item in list" :to="item.to" :key="`bread-crumb-${item.name}`">
|
||||
<common-icon style="margin-right: 4px;" :type="item.icon || ''"/>
|
||||
{{ showTitle(item) }}
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { showTitle } from '@/libs/util'
|
||||
import CommonIcon from '_c/common-icon'
|
||||
import './custom-bread-crumb.less'
|
||||
export default {
|
||||
name: 'customBreadCrumb',
|
||||
components: {
|
||||
CommonIcon
|
||||
},
|
||||
props: {
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
fontSize: {
|
||||
type: Number,
|
||||
default: 14
|
||||
},
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showTitle (item) {
|
||||
return showTitle(item, this)
|
||||
},
|
||||
isCustomIcon (iconName) {
|
||||
return iconName.indexOf('_') === 0
|
||||
},
|
||||
getCustomIconName (iconName) {
|
||||
return iconName.slice(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,2 @@
|
||||
import customBreadCrumb from './custom-bread-crumb.vue'
|
||||
export default customBreadCrumb
|
||||
@@ -0,0 +1,14 @@
|
||||
.header-bar{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
.custom-content-con{
|
||||
float: right;
|
||||
height: auto;
|
||||
padding-right: 20px;
|
||||
line-height: 64px;
|
||||
& > *{
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="header-bar">
|
||||
<sider-trigger :collapsed="collapsed" icon="md-menu" @on-change="handleCollpasedChange"></sider-trigger>
|
||||
<custom-bread-crumb show-icon style="margin-left: 30px;" :list="breadCrumbList"></custom-bread-crumb>
|
||||
<div class="custom-content-con">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import siderTrigger from './sider-trigger'
|
||||
import customBreadCrumb from './custom-bread-crumb'
|
||||
import './header-bar.less'
|
||||
export default {
|
||||
name: 'HeaderBar',
|
||||
components: {
|
||||
siderTrigger,
|
||||
customBreadCrumb
|
||||
},
|
||||
props: {
|
||||
collapsed: Boolean
|
||||
},
|
||||
computed: {
|
||||
breadCrumbList () {
|
||||
return this.$store.state.app.breadCrumbList
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleCollpasedChange (state) {
|
||||
this.$emit('on-coll-change', state)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,2 @@
|
||||
import HeaderBar from './header-bar'
|
||||
export default HeaderBar
|
||||
@@ -0,0 +1,2 @@
|
||||
import siderTrigger from './sider-trigger.vue'
|
||||
export default siderTrigger
|
||||
@@ -0,0 +1,21 @@
|
||||
.trans{
|
||||
transition: transform .2s ease;
|
||||
}
|
||||
@size: 40px;
|
||||
.sider-trigger-a{
|
||||
padding: 6px;
|
||||
width: @size;
|
||||
height: @size;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
color: #5c6b77;
|
||||
margin-top: 12px;
|
||||
i{
|
||||
.trans;
|
||||
vertical-align: top;
|
||||
}
|
||||
&.collapsed i{
|
||||
transform: rotateZ(90deg);
|
||||
.trans;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<a @click="handleChange" type="text" :class="['sider-trigger-a', collapsed ? 'collapsed' : '']"><Icon :type="icon" :size="size" /></a>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'siderTrigger',
|
||||
props: {
|
||||
collapsed: Boolean,
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'navicon-round'
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 26
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChange () {
|
||||
this.$emit('on-change', !this.collapsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import './sider-trigger.less';
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
import Language from './language.vue'
|
||||
export default Language
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<Dropdown trigger="click" @on-click="selectLang">
|
||||
<a href="javascript:void(0)">
|
||||
{{ title }}
|
||||
<Icon :size="18" type="md-arrow-dropdown" />
|
||||
</a>
|
||||
<DropdownMenu slot="list">
|
||||
<DropdownItem v-for="(value, key) in localList" :name="key" :key="`lang-${key}`">{{ value }}</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Language',
|
||||
props: {
|
||||
lang: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
langList: {
|
||||
'zh-CN': '语言',
|
||||
'zh-TW': '語言',
|
||||
'en-US': 'Lang'
|
||||
},
|
||||
localList: {
|
||||
'zh-CN': '中文简体',
|
||||
'zh-TW': '中文繁体',
|
||||
'en-US': 'English'
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
lang (lang) {
|
||||
this.$i18n.locale = lang
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title () {
|
||||
return this.langList[this.lang]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectLang (name) {
|
||||
this.$emit('on-lang-change', name)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<Dropdown ref="dropdown" @on-click="handleClick" :class="hideTitle ? '' : 'collased-menu-dropdown'" :transfer="hideTitle" :placement="placement">
|
||||
<a class="drop-menu-a" type="text" @mouseover="handleMousemove($event, children)" :style="{textAlign: !hideTitle ? 'left' : ''}"><common-icon :size="rootIconSize" :color="textColor" :type="parentItem.icon"/><span class="menu-title" v-if="!hideTitle">{{ showTitle(parentItem) }}</span><Icon style="float: right;" v-if="!hideTitle" type="ios-arrow-forward" :size="16"/></a>
|
||||
<DropdownMenu ref="dropdown" slot="list">
|
||||
<template v-for="child in children">
|
||||
<collapsed-menu v-if="showChildren(child)" :icon-size="iconSize" :parent-item="child" :key="`drop-${child.name}`"></collapsed-menu>
|
||||
<DropdownItem v-else :key="`drop-${child.name}`" :name="child.name"><common-icon :size="iconSize" :type="child.icon"/><span class="menu-title">{{ showTitle(child) }}</span></DropdownItem>
|
||||
</template>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</template>
|
||||
<script>
|
||||
import mixin from './mixin'
|
||||
import itemMixin from './item-mixin'
|
||||
import { findNodeUpperByClasses } from '@/libs/util'
|
||||
|
||||
export default {
|
||||
name: 'CollapsedMenu',
|
||||
mixins: [ mixin, itemMixin ],
|
||||
props: {
|
||||
hideTitle: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rootIconSize: {
|
||||
type: Number,
|
||||
default: 16
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
placement: 'right-end'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick (name) {
|
||||
this.$emit('on-click', name)
|
||||
},
|
||||
handleMousemove (event, children) {
|
||||
const { pageY } = event
|
||||
const height = children.length * 38
|
||||
const isOverflow = pageY + height < window.innerHeight
|
||||
this.placement = isOverflow ? 'right-start' : 'right-end'
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
let dropdown = findNodeUpperByClasses(this.$refs.dropdown.$el, ['ivu-select-dropdown', 'ivu-dropdown-transfer'])
|
||||
if (dropdown) dropdown.style.overflow = 'visible'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,2 @@
|
||||
import SideMenu from './side-menu.vue'
|
||||
export default SideMenu
|
||||
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
props: {
|
||||
parentItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
theme: String,
|
||||
iconSize: Number
|
||||
},
|
||||
computed: {
|
||||
parentName () {
|
||||
return this.parentItem.name
|
||||
},
|
||||
children () {
|
||||
return this.parentItem.children
|
||||
},
|
||||
textColor () {
|
||||
return this.theme === 'dark' ? '#fff' : '#495060'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import CommonIcon from '_c/common-icon'
|
||||
import { showTitle } from '@/libs/util'
|
||||
export default {
|
||||
components: {
|
||||
CommonIcon
|
||||
},
|
||||
methods: {
|
||||
showTitle (item) {
|
||||
return showTitle(item, this)
|
||||
},
|
||||
showChildren (item) {
|
||||
return item.children && (item.children.length > 1 || (item.meta && item.meta.showAlways))
|
||||
},
|
||||
getNameOrHref (item, children0) {
|
||||
return item.href ? `isTurnByHref_${item.href}` : (children0 ? item.children[0].name : item.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<Submenu :name="`${parentName}`">
|
||||
<template slot="title">
|
||||
<common-icon :type="parentItem.icon || ''"/>
|
||||
<span>{{ showTitle(parentItem) }}</span>
|
||||
</template>
|
||||
<template v-for="item in children">
|
||||
<template v-if="item.children && item.children.length === 1">
|
||||
<side-menu-item v-if="showChildren(item)" :key="`menu-${item.name}`" :parent-item="item"></side-menu-item>
|
||||
<menu-item v-else :name="getNameOrHref(item, true)" :key="`menu-${item.children[0].name}`"><common-icon :type="item.children[0].icon || ''"/><span>{{ showTitle(item.children[0]) }}</span></menu-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<side-menu-item v-if="showChildren(item)" :key="`menu-${item.name}`" :parent-item="item"></side-menu-item>
|
||||
<menu-item v-else :name="getNameOrHref(item)" :key="`menu-${item.name}`"><common-icon :type="item.icon || ''"/><span>{{ showTitle(item) }}</span></menu-item>
|
||||
</template>
|
||||
</template>
|
||||
</Submenu>
|
||||
</template>
|
||||
<script>
|
||||
import mixin from './mixin'
|
||||
import itemMixin from './item-mixin'
|
||||
export default {
|
||||
name: 'SideMenuItem',
|
||||
mixins: [ mixin, itemMixin ]
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,40 @@
|
||||
.side-menu-wrapper{
|
||||
user-select: none;
|
||||
.menu-collapsed{
|
||||
padding-top: 10px;
|
||||
|
||||
.ivu-dropdown{
|
||||
width: 100%;
|
||||
.ivu-dropdown-rel a{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.ivu-tooltip{
|
||||
width: 100%;
|
||||
.ivu-tooltip-rel{
|
||||
width: 100%;
|
||||
}
|
||||
.ivu-tooltip-popper .ivu-tooltip-content{
|
||||
.ivu-tooltip-arrow{
|
||||
border-right-color: #fff;
|
||||
}
|
||||
.ivu-tooltip-inner{
|
||||
background: #fff;
|
||||
color: #495060;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
a.drop-menu-a{
|
||||
display: inline-block;
|
||||
padding: 6px 15px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #495060;
|
||||
}
|
||||
}
|
||||
.menu-title{
|
||||
padding-left: 6px;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="side-menu-wrapper">
|
||||
<slot></slot>
|
||||
<Menu ref="menu" v-show="!collapsed" :active-name="activeName" :open-names="openedNames" :accordion="accordion" :theme="theme" width="auto" @on-select="handleSelect">
|
||||
<template v-for="item in menuList">
|
||||
<template v-if="item.children && item.children.length === 1">
|
||||
<side-menu-item v-if="showChildren(item)" :key="`menu-${item.name}`" :parent-item="item"></side-menu-item>
|
||||
<menu-item v-else :name="getNameOrHref(item, true)" :key="`menu-${item.children[0].name}`"><common-icon :type="item.children[0].icon || ''"/><span>{{ showTitle(item.children[0]) }}</span></menu-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<side-menu-item v-if="showChildren(item)" :key="`menu-${item.name}`" :parent-item="item"></side-menu-item>
|
||||
<menu-item v-else :name="getNameOrHref(item)" :key="`menu-${item.name}`"><common-icon :type="item.icon || ''"/><span>{{ showTitle(item) }}</span></menu-item>
|
||||
</template>
|
||||
</template>
|
||||
</Menu>
|
||||
<div class="menu-collapsed" v-show="collapsed" :list="menuList">
|
||||
<template v-for="item in menuList">
|
||||
<collapsed-menu v-if="item.children && item.children.length > 1" @on-click="handleSelect" hide-title :root-icon-size="rootIconSize" :icon-size="iconSize" :theme="theme" :parent-item="item" :key="`drop-menu-${item.name}`"></collapsed-menu>
|
||||
<Tooltip transfer v-else :content="(item.meta && item.meta.title) || (item.children && item.children[0] && item.children[0].meta.title)" placement="right" :key="`drop-menu-${item.name}`">
|
||||
<a @click="handleSelect(getNameOrHref(item, true))" class="drop-menu-a" :style="{textAlign: 'center'}"><common-icon :size="rootIconSize" :color="textColor" :type="item.icon || (item.children && item.children[0].icon)"/></a>
|
||||
</Tooltip>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import SideMenuItem from './side-menu-item.vue'
|
||||
import CollapsedMenu from './collapsed-menu.vue'
|
||||
import { getUnion } from '@/libs/tools'
|
||||
import mixin from './mixin'
|
||||
|
||||
export default {
|
||||
name: 'SideMenu',
|
||||
mixins: [ mixin ],
|
||||
components: {
|
||||
SideMenuItem,
|
||||
CollapsedMenu
|
||||
},
|
||||
props: {
|
||||
menuList: {
|
||||
type: Array,
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'dark'
|
||||
},
|
||||
rootIconSize: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
iconSize: {
|
||||
type: Number,
|
||||
default: 16
|
||||
},
|
||||
accordion: Boolean,
|
||||
activeName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
openNames: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
openedNames: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSelect (name) {
|
||||
this.$emit('on-select', name)
|
||||
},
|
||||
getOpenedNamesByActiveName (name) {
|
||||
return this.$route.matched.map(item => item.name).filter(item => item !== name)
|
||||
},
|
||||
updateOpenName (name) {
|
||||
if (name === this.$config.homeName) this.openedNames = []
|
||||
else this.openedNames = this.getOpenedNamesByActiveName(name)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
textColor () {
|
||||
return this.theme === 'dark' ? '#fff' : '#495060'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeName (name) {
|
||||
if (this.accordion) this.openedNames = this.getOpenedNamesByActiveName(name)
|
||||
else this.openedNames = getUnion(this.openedNames, this.getOpenedNamesByActiveName(name))
|
||||
},
|
||||
openNames (newNames) {
|
||||
this.openedNames = newNames
|
||||
},
|
||||
openedNames () {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.menu.updateOpened()
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.openedNames = getUnion(this.openedNames, this.getOpenedNamesByActiveName(name))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import './side-menu.less';
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
import TagsNav from './tags-nav.vue'
|
||||
export default TagsNav
|
||||
@@ -0,0 +1,87 @@
|
||||
.no-select{
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.size{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.tags-nav{
|
||||
position: relative;
|
||||
border-top: 1px solid #F0F0F0;
|
||||
border-bottom: 1px solid #F0F0F0;
|
||||
.no-select;
|
||||
.size;
|
||||
.close-con{
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 32px;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
}
|
||||
.btn-con{
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
padding-top: 3px;
|
||||
z-index: 10;
|
||||
button{
|
||||
padding: 6px 4px;
|
||||
line-height: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
&.left-btn{
|
||||
left: 0px;
|
||||
}
|
||||
&.right-btn{
|
||||
right: 32px;
|
||||
border-right: 1px solid #F0F0F0;
|
||||
}
|
||||
}
|
||||
.scroll-outer{
|
||||
position: absolute;
|
||||
left: 28px;
|
||||
right: 61px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
box-shadow: 0px 0 3px 2px rgba(100,100,100,.1) inset;
|
||||
.scroll-body{
|
||||
height: ~"calc(100% - 1px)";
|
||||
display: inline-block;
|
||||
padding: 1px 4px 0;
|
||||
position: absolute;
|
||||
overflow: visible;
|
||||
white-space: nowrap;
|
||||
transition: left .3s ease;
|
||||
.ivu-tag-dot-inner{
|
||||
transition: background .2s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
.contextmenu {
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
background: #fff;
|
||||
z-index: 100;
|
||||
list-style-type: none;
|
||||
border-radius: 4px;
|
||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 5px 15px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div class="tags-nav">
|
||||
<div class="close-con">
|
||||
<Dropdown transfer @on-click="handleTagsOption" style="margin-top:7px;">
|
||||
<Button size="small" type="text">
|
||||
<Icon :size="18" type="ios-close-circle-outline">选项</Icon>
|
||||
</Button>
|
||||
<DropdownMenu slot="list">
|
||||
<DropdownItem name="close-all">关闭所有</DropdownItem>
|
||||
<DropdownItem name="close-others">关闭其他</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<ul v-show="visible" :style="{left: contextMenuLeft + 'px', top: contextMenuTop + 'px'}" class="contextmenu">
|
||||
<li v-for="(item, key) of menuList" @click="handleTagsOption(key)" :key="key">{{item}}</li>
|
||||
</ul>
|
||||
<div class="btn-con left-btn">
|
||||
<Button type="text" @click="handleScroll(240)">
|
||||
<Icon :size="18" type="ios-arrow-back" />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="btn-con right-btn">
|
||||
<Button type="text" @click="handleScroll(-240)">
|
||||
<Icon :size="18" type="ios-arrow-forward" />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="scroll-outer" ref="scrollOuter" @DOMMouseScroll="handlescroll" @mousewheel="handlescroll">
|
||||
<div ref="scrollBody" class="scroll-body" :style="{left: tagBodyLeft + 'px'}">
|
||||
<transition-group name="taglist-moving-animation">
|
||||
<Tag
|
||||
type="dot"
|
||||
v-for="(item, index) in list"
|
||||
ref="tagsPageOpened"
|
||||
:key="`tag-nav-${index}`"
|
||||
:name="item.name"
|
||||
:data-route-item="item"
|
||||
@on-close="handleClose(item)"
|
||||
@click.native="handleClick(item)"
|
||||
:closable="item.name !== $config.homeName"
|
||||
:color="isCurrentTag(item) ? 'primary' : 'default'"
|
||||
@contextmenu.prevent.native="contextMenu(item, $event)"
|
||||
>{{ showTitleInside(item) }}</Tag>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { showTitle, routeEqual } from '@/libs/util'
|
||||
import beforeClose from '@/router/before-close'
|
||||
export default {
|
||||
name: 'TagsNav',
|
||||
props: {
|
||||
value: Object,
|
||||
list: {
|
||||
type: Array,
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
tagBodyLeft: 0,
|
||||
rightOffset: 40,
|
||||
outerPadding: 4,
|
||||
contextMenuLeft: 0,
|
||||
contextMenuTop: 0,
|
||||
visible: false,
|
||||
menuList: {
|
||||
others: '关闭其他',
|
||||
all: '关闭所有'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentRouteObj () {
|
||||
const { name, params, query } = this.value
|
||||
return { name, params, query }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handlescroll (e) {
|
||||
var type = e.type
|
||||
let delta = 0
|
||||
if (type === 'DOMMouseScroll' || type === 'mousewheel') {
|
||||
delta = (e.wheelDelta) ? e.wheelDelta : -(e.detail || 0) * 40
|
||||
}
|
||||
this.handleScroll(delta)
|
||||
},
|
||||
handleScroll (offset) {
|
||||
const outerWidth = this.$refs.scrollOuter.offsetWidth
|
||||
const bodyWidth = this.$refs.scrollBody.offsetWidth
|
||||
if (offset > 0) {
|
||||
this.tagBodyLeft = Math.min(0, this.tagBodyLeft + offset)
|
||||
} else {
|
||||
if (outerWidth < bodyWidth) {
|
||||
if (this.tagBodyLeft < -(bodyWidth - outerWidth)) {
|
||||
this.tagBodyLeft = this.tagBodyLeft
|
||||
} else {
|
||||
this.tagBodyLeft = Math.max(this.tagBodyLeft + offset, outerWidth - bodyWidth)
|
||||
}
|
||||
} else {
|
||||
this.tagBodyLeft = 0
|
||||
}
|
||||
}
|
||||
},
|
||||
handleTagsOption (type) {
|
||||
if (type.includes('all')) {
|
||||
// 关闭所有,除了home
|
||||
let res = this.list.filter(item => item.name === this.$config.homeName)
|
||||
this.$emit('on-close', res, 'all')
|
||||
} else if (type.includes('others')) {
|
||||
// 关闭除当前页和home页的其他页
|
||||
let res = this.list.filter(item => routeEqual(this.currentRouteObj, item) || item.name === this.$config.homeName)
|
||||
this.$emit('on-close', res, 'others', this.currentRouteObj)
|
||||
setTimeout(() => {
|
||||
this.getTagElementByName(this.currentRouteObj.name)
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
handleClose (current) {
|
||||
if (current.meta && current.meta.beforeCloseName && current.meta.beforeCloseName in beforeClose) {
|
||||
new Promise(beforeClose[current.meta.beforeCloseName]).then(close => {
|
||||
if (close) {
|
||||
this.close(current)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.close(current)
|
||||
}
|
||||
},
|
||||
close (route) {
|
||||
let res = this.list.filter(item => !routeEqual(route, item))
|
||||
this.$emit('on-close', res, undefined, route)
|
||||
},
|
||||
handleClick (item) {
|
||||
this.$emit('input', item)
|
||||
},
|
||||
showTitleInside (item) {
|
||||
return showTitle(item, this)
|
||||
},
|
||||
isCurrentTag (item) {
|
||||
return routeEqual(this.currentRouteObj, item)
|
||||
},
|
||||
moveToView (tag) {
|
||||
const outerWidth = this.$refs.scrollOuter.offsetWidth
|
||||
const bodyWidth = this.$refs.scrollBody.offsetWidth
|
||||
if (bodyWidth < outerWidth) {
|
||||
this.tagBodyLeft = 0
|
||||
} else if (tag.offsetLeft < -this.tagBodyLeft) {
|
||||
// 标签在可视区域左侧
|
||||
this.tagBodyLeft = -tag.offsetLeft + this.outerPadding
|
||||
} else if (tag.offsetLeft > -this.tagBodyLeft && tag.offsetLeft + tag.offsetWidth < -this.tagBodyLeft + outerWidth) {
|
||||
// 标签在可视区域
|
||||
this.tagBodyLeft = Math.min(0, outerWidth - tag.offsetWidth - tag.offsetLeft - this.outerPadding)
|
||||
} else {
|
||||
// 标签在可视区域右侧
|
||||
this.tagBodyLeft = -(tag.offsetLeft - (outerWidth - this.outerPadding - tag.offsetWidth))
|
||||
}
|
||||
},
|
||||
getTagElementByName (route) {
|
||||
this.$nextTick(() => {
|
||||
this.refsTag = this.$refs.tagsPageOpened
|
||||
this.refsTag.forEach((item, index) => {
|
||||
if (routeEqual(route, item.$attrs['data-route-item'])) {
|
||||
let tag = this.refsTag[index].$el
|
||||
this.moveToView(tag)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
contextMenu (item, e) {
|
||||
if (item.name === this.$config.homeName) {
|
||||
return
|
||||
}
|
||||
this.visible = true
|
||||
const offsetLeft = this.$el.getBoundingClientRect().left
|
||||
this.contextMenuLeft = e.clientX - offsetLeft + 10
|
||||
this.contextMenuTop = e.clientY - 64
|
||||
},
|
||||
closeMenu () {
|
||||
this.visible = false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route' (to) {
|
||||
this.getTagElementByName(to)
|
||||
},
|
||||
visible (value) {
|
||||
if (value) {
|
||||
document.body.addEventListener('click', this.closeMenu)
|
||||
} else {
|
||||
document.body.removeEventListener('click', this.closeMenu)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// 默认关闭所有的标签
|
||||
this.handleTagsOption('all')
|
||||
setTimeout(() => {
|
||||
this.getTagElementByName(this.$route)
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import './tags-nav.less';
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
import User from './user.vue'
|
||||
export default User
|
||||
@@ -0,0 +1,9 @@
|
||||
.user{
|
||||
&-avator-dropdown{
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
// height: 64px;
|
||||
vertical-align: middle;
|
||||
// line-height: 64px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="user-avator-dropdown">
|
||||
<Dropdown @on-click="handleClick">
|
||||
<span style="font-size: 15px;font-weight: bold"> {{ nickname }} </span>
|
||||
<Avatar :src="userAvator"/>
|
||||
<Icon :size="18" type="md-arrow-dropdown"></Icon>
|
||||
<DropdownMenu slot="list">
|
||||
<DropdownItem name="personal-center">个人中心</DropdownItem>
|
||||
<DropdownItem name="logout" style="color: #ed4014" divided>退出登录</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import './user.less'
|
||||
import { mapActions } from 'vuex'
|
||||
export default {
|
||||
name: 'User',
|
||||
props: {
|
||||
nickname: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
userAvator: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'handleLogOut'
|
||||
]),
|
||||
handleClick (name) {
|
||||
switch (name) {
|
||||
case 'personal-center':
|
||||
this.$router.push({
|
||||
name: 'personal-center'
|
||||
})
|
||||
break
|
||||
case 'message-center':
|
||||
this.$router.push({
|
||||
name: 'message-center'
|
||||
})
|
||||
break
|
||||
case 'logout':
|
||||
this.handleLogOut().then(() => {
|
||||
this.$router.push({
|
||||
name: 'login'
|
||||
})
|
||||
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,2 @@
|
||||
import Main from './main.vue'
|
||||
export default Main
|
||||
@@ -0,0 +1,77 @@
|
||||
.main{
|
||||
.logo-con{
|
||||
height: 64px;
|
||||
padding: 10px;
|
||||
img{
|
||||
height: 44px;
|
||||
width: auto;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
.header-con{
|
||||
background: #fff;
|
||||
padding: 0 20px;
|
||||
width: 100%;
|
||||
}
|
||||
.main-layout-con{
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.main-content-con{
|
||||
height: ~"calc(100% - 60px)";
|
||||
overflow: hidden;
|
||||
}
|
||||
.tag-nav-wrapper{
|
||||
padding: 0;
|
||||
height:40px;
|
||||
background:#F0F0F0;
|
||||
}
|
||||
.content-wrapper{
|
||||
padding: 18px;
|
||||
height: ~"calc(100% - 80px)";
|
||||
overflow: auto;
|
||||
// 内容填充白色背景
|
||||
background: #fff;
|
||||
}
|
||||
.left-sider{
|
||||
.ivu-layout-sider-children{
|
||||
overflow-y: scroll;
|
||||
margin-right: -18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ivu-menu-item > i{
|
||||
margin-right: 12px !important;
|
||||
}
|
||||
.ivu-menu-submenu > .ivu-menu > .ivu-menu-item > i {
|
||||
margin-right: 8px !important;
|
||||
}
|
||||
.collased-menu-dropdown{
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
line-height: normal;
|
||||
padding: 7px 0 6px 16px;
|
||||
clear: both;
|
||||
font-size: 12px !important;
|
||||
white-space: nowrap;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease-in-out;
|
||||
&:hover{
|
||||
background: rgba(100, 100, 100, 0.1);
|
||||
}
|
||||
& * {
|
||||
color: #515a6e;
|
||||
}
|
||||
.ivu-menu-item > i{
|
||||
margin-right: 12px !important;
|
||||
}
|
||||
.ivu-menu-submenu > .ivu-menu > .ivu-menu-item > i {
|
||||
margin-right: 8px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ivu-select-dropdown.ivu-dropdown-transfer{
|
||||
max-height: 400px;
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<Layout style="height: 100%" class="main">
|
||||
<Sider hide-trigger collapsible :width="256" :collapsed-width="64" v-model="collapsed" class="left-sider" :style="{overflow: 'hidden'}">
|
||||
<side-menu accordion ref="sideMenu" :active-name="$route.name" :collapsed="collapsed" @on-select="turnToPage" :menu-list="menuList">
|
||||
<!-- 需要放在菜单上面的内容,如Logo,写在side-menu标签内部,如下 -->
|
||||
<div class="logo-con" style="text-align: center">
|
||||
<img v-show="!collapsed" :src="maxLogo" key="max-logo" />
|
||||
<img v-show="collapsed" :src="minLogo" key="min-logo" />
|
||||
</div>
|
||||
</side-menu>
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Header class="header-con">
|
||||
<header-bar :collapsed="collapsed" @on-coll-change="handleCollapsedChange">
|
||||
<user :nickname="nickname" :user-avator="userAvator"/>
|
||||
<error-store v-if="$config.plugin['error-store'] && $config.plugin['error-store'].showInHeader" :has-read="hasReadErrorPage" :count="errorCount"></error-store>
|
||||
<fullscreen v-model="isFullscreen" style="margin-right: 10px;"/>
|
||||
</header-bar>
|
||||
</Header>
|
||||
<Content class="main-content-con">
|
||||
<Layout class="main-layout-con">
|
||||
<div class="tag-nav-wrapper">
|
||||
<tags-nav :value="$route" @input="handleClick" :list="tagNavList" @on-close="handleCloseTag"/>
|
||||
</div>
|
||||
<Content class="content-wrapper">
|
||||
<keep-alive :include="cacheList">
|
||||
<router-view/>
|
||||
</keep-alive>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</template>
|
||||
<script>
|
||||
import SideMenu from './components/side-menu'
|
||||
import HeaderBar from './components/header-bar'
|
||||
import TagsNav from './components/tags-nav'
|
||||
import User from './components/user'
|
||||
import Fullscreen from './components/fullscreen'
|
||||
import ErrorStore from './components/error-store'
|
||||
import { mapMutations, mapActions, mapGetters } from 'vuex'
|
||||
import { getNewTagList, getNextRoute, routeEqual } from '@/libs/util'
|
||||
import minLogo from '@/assets/images/logo-min.jpg'
|
||||
import maxLogo from '@/assets/images/logo.jpg'
|
||||
import './main.less'
|
||||
export default {
|
||||
name: 'Main',
|
||||
components: {
|
||||
SideMenu,
|
||||
HeaderBar,
|
||||
TagsNav,
|
||||
Fullscreen,
|
||||
ErrorStore,
|
||||
User
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
collapsed: false,
|
||||
minLogo,
|
||||
maxLogo,
|
||||
isFullscreen: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'errorCount'
|
||||
]),
|
||||
tagNavList () {
|
||||
return this.$store.state.app.tagNavList
|
||||
},
|
||||
tagRouter () {
|
||||
return this.$store.state.app.tagRouter
|
||||
},
|
||||
nickname() {
|
||||
return this.$store.state.user.nickname
|
||||
},
|
||||
userAvator () {
|
||||
return this.$store.state.user.avatorImgPath
|
||||
},
|
||||
cacheList () {
|
||||
return this.tagNavList.length ? this.tagNavList.filter(item => !(item.meta && item.meta.notCache)).map(item => item.name) : []
|
||||
},
|
||||
menuList () {
|
||||
return this.$store.getters.menuList
|
||||
},
|
||||
hasReadErrorPage () {
|
||||
return this.$store.state.app.hasReadErrorPage
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([
|
||||
'setBreadCrumb',
|
||||
'setTagNavList',
|
||||
'addTag',
|
||||
'setLocal'
|
||||
]),
|
||||
...mapActions([
|
||||
'handleLogin'
|
||||
]),
|
||||
turnToPage (route) {
|
||||
let { name, params, query } = {}
|
||||
if (typeof route === 'string') name = route
|
||||
else {
|
||||
name = route.name
|
||||
params = route.params
|
||||
query = route.query
|
||||
}
|
||||
if (name.indexOf('isTurnByHref_') > -1) {
|
||||
window.open(name.split('_')[1])
|
||||
return
|
||||
}
|
||||
this.$router.push({
|
||||
name,
|
||||
params,
|
||||
query
|
||||
})
|
||||
},
|
||||
handleCollapsedChange (state) {
|
||||
this.collapsed = state
|
||||
},
|
||||
handleCloseTag (res, type, route) {
|
||||
if (type === 'all') {
|
||||
this.turnToPage(this.$config.homeName)
|
||||
} else if (routeEqual(this.$route, route)) {
|
||||
if (type !== 'others') {
|
||||
const nextRoute = getNextRoute(this.tagNavList, route)
|
||||
this.$router.push(nextRoute)
|
||||
}
|
||||
}
|
||||
this.setTagNavList(res)
|
||||
},
|
||||
handleClick (item) {
|
||||
this.turnToPage(item)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route' (newRoute) {
|
||||
const { name, query, params, meta } = newRoute
|
||||
this.addTag({
|
||||
route: { name, query, params, meta },
|
||||
type: 'push'
|
||||
})
|
||||
this.setBreadCrumb(newRoute)
|
||||
this.setTagNavList(getNewTagList(this.tagNavList, newRoute))
|
||||
this.$refs.sideMenu.updateOpenName(newRoute.name)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
/**
|
||||
* @description 初始化设置面包屑导航和标签导航
|
||||
*/
|
||||
this.setTagNavList()
|
||||
this.addTag({
|
||||
route: this.$store.state.app.homeRoute
|
||||
})
|
||||
this.setBreadCrumb(this.$route)
|
||||
// 如果当前打开页面不在标签栏中,跳到homeName页
|
||||
if (!this.tagNavList.find(item => item.name === this.$route.name)) {
|
||||
this.$router.push({
|
||||
name: this.$config.homeName
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user