Initial commit: freedom-web frontend project

This commit is contained in:
张家训
2026-08-19 17:59:00 +08:00
commit 0131747f90
186 changed files with 18882 additions and 0 deletions
@@ -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>
+2
View File
@@ -0,0 +1,2 @@
import Main from './main.vue'
export default Main
+77
View File
@@ -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;
}
+166
View File
@@ -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>