16 changed files with 1265 additions and 104 deletions
-
160src/components/ThemePicker/index.vue
-
60src/layout/components/AppMain.vue
-
265src/layout/components/Navbar.vue
-
31src/layout/components/Sidebar/FixiOSBug.js
-
36src/layout/components/Sidebar/Link.vue
-
90src/layout/components/Sidebar/SidebarItem.vue
-
44src/layout/components/Sidebar/TopMenu.vue
-
45src/layout/components/Sidebar/TopMenus.vue
-
81src/layout/components/TagsView/ScrollPane.vue
-
275src/layout/components/TagsView/index.vue
-
5src/layout/components/index.js
-
72src/layout/index.vue
-
6src/main.js
-
11src/router/routers.js
-
166src/utils/validate.js
-
4vue.config.js
@ -0,0 +1,160 @@ |
|||
<template> |
|||
<el-color-picker v-model="theme" :predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d']" class="theme-picker" popper-class="theme-picker-dropdown" /> |
|||
</template> |
|||
|
|||
<script> |
|||
const version = require('element-ui/package.json').version // element-ui version from node_modules |
|||
const ORIGINAL_THEME = '#409EFF' // default color |
|||
import Cookies from 'js-cookie' |
|||
export default { |
|||
data() { |
|||
return { |
|||
chalk: '', // content of theme-chalk css |
|||
theme: '' |
|||
} |
|||
}, |
|||
computed: { |
|||
defaultTheme() { |
|||
return this.$store.state.settings.theme |
|||
} |
|||
}, |
|||
watch: { |
|||
defaultTheme: { |
|||
handler: function(val, oldVal) { |
|||
this.theme = val |
|||
}, |
|||
immediate: true |
|||
}, |
|||
async theme(val) { |
|||
Cookies.set('theme', val, { expires: 365 }) |
|||
const oldVal = this.chalk ? this.theme : Cookies.get('theme') ? Cookies.get('theme') : ORIGINAL_THEME |
|||
if (typeof val !== 'string') return |
|||
const themeCluster = this.getThemeCluster(val.replace('#', '')) |
|||
const originalCluster = this.getThemeCluster(oldVal.replace('#', '')) |
|||
|
|||
const getHandler = (variable, id) => { |
|||
return () => { |
|||
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', '')) |
|||
const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster) |
|||
|
|||
let styleTag = document.getElementById(id) |
|||
if (!styleTag) { |
|||
styleTag = document.createElement('style') |
|||
styleTag.setAttribute('id', id) |
|||
document.head.appendChild(styleTag) |
|||
} |
|||
styleTag.innerText = newStyle |
|||
} |
|||
} |
|||
|
|||
if (!this.chalk) { |
|||
const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css` |
|||
await this.getCSSString(url, 'chalk') |
|||
} |
|||
|
|||
const chalkHandler = getHandler('chalk', 'chalk-style') |
|||
|
|||
chalkHandler() |
|||
|
|||
const styles = [].slice.call(document.querySelectorAll('style')) |
|||
.filter(style => { |
|||
const text = style.innerText |
|||
return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text) |
|||
}) |
|||
styles.forEach(style => { |
|||
const { innerText } = style |
|||
if (typeof innerText !== 'string') return |
|||
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster) |
|||
}) |
|||
|
|||
this.$emit('change', val) |
|||
} |
|||
}, |
|||
|
|||
methods: { |
|||
updateStyle(style, oldCluster, newCluster) { |
|||
let newStyle = style |
|||
oldCluster.forEach((color, index) => { |
|||
newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index]) |
|||
}) |
|||
return newStyle |
|||
}, |
|||
|
|||
getCSSString(url, variable) { |
|||
return new Promise(resolve => { |
|||
const xhr = new XMLHttpRequest() |
|||
xhr.onreadystatechange = () => { |
|||
if (xhr.readyState === 4 && xhr.status === 200) { |
|||
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '') |
|||
resolve() |
|||
} |
|||
} |
|||
xhr.open('GET', url) |
|||
xhr.send() |
|||
}) |
|||
}, |
|||
|
|||
getThemeCluster(theme) { |
|||
const tintColor = (color, tint) => { |
|||
let red = parseInt(color.slice(0, 2), 16) |
|||
let green = parseInt(color.slice(2, 4), 16) |
|||
let blue = parseInt(color.slice(4, 6), 16) |
|||
|
|||
if (tint === 0) { // when primary color is in its rgb space |
|||
return [red, green, blue].join(',') |
|||
} else { |
|||
red += Math.round(tint * (255 - red)) |
|||
green += Math.round(tint * (255 - green)) |
|||
blue += Math.round(tint * (255 - blue)) |
|||
|
|||
red = red.toString(16) |
|||
green = green.toString(16) |
|||
blue = blue.toString(16) |
|||
|
|||
return `#${red}${green}${blue}` |
|||
} |
|||
} |
|||
|
|||
const shadeColor = (color, shade) => { |
|||
let red = parseInt(color.slice(0, 2), 16) |
|||
let green = parseInt(color.slice(2, 4), 16) |
|||
let blue = parseInt(color.slice(4, 6), 16) |
|||
|
|||
red = Math.round((1 - shade) * red) |
|||
green = Math.round((1 - shade) * green) |
|||
blue = Math.round((1 - shade) * blue) |
|||
|
|||
red = red.toString(16) |
|||
green = green.toString(16) |
|||
blue = blue.toString(16) |
|||
|
|||
return `#${red}${green}${blue}` |
|||
} |
|||
|
|||
const clusters = [theme] |
|||
for (let i = 0; i <= 9; i++) { |
|||
clusters.push(tintColor(theme, Number((i / 10).toFixed(2)))) |
|||
} |
|||
clusters.push(shadeColor(theme, 0.1)) |
|||
return clusters |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
.theme-message, |
|||
.theme-picker-dropdown { |
|||
z-index: 99999 !important; |
|||
} |
|||
|
|||
.theme-picker .el-color-picker__trigger { |
|||
height: 26px !important; |
|||
width: 26px !important; |
|||
padding: 2px; |
|||
} |
|||
|
|||
.theme-picker-dropdown .el-color-dropdown__link-btn { |
|||
display: none; |
|||
} |
|||
</style> |
@ -0,0 +1,60 @@ |
|||
<template> |
|||
<section class="app-main"> |
|||
<transition name="fade-transform" mode="out-in"> |
|||
<keep-alive :include="cachedViews"> |
|||
<router-view :key="key" /> |
|||
</keep-alive> |
|||
</transition> |
|||
<div v-if="$store.state.settings.showFooter" id="el-main-footer"> |
|||
<span v-html="$store.state.settings.footerTxt" /> |
|||
<span> ⋅ </span> |
|||
<a href="https://beian.miit.gov.cn/#/Integrated/index" target="_blank">{{ $store.state.settings.caseNumber }}</a> |
|||
</div> |
|||
</section> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'AppMain', |
|||
computed: { |
|||
cachedViews() { |
|||
return this.$store.state.tagsView.cachedViews |
|||
}, |
|||
key() { |
|||
return this.$route.path |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.app-main { |
|||
/* 50= navbar 50 */ |
|||
min-height: calc(100vh - 50px); |
|||
width: 100%; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.fixed-header + .app-main { |
|||
padding-top: 50px; |
|||
} |
|||
|
|||
.hasTagsView { |
|||
.app-main { |
|||
min-height: calc(100vh - 84px); |
|||
} |
|||
|
|||
.fixed-header + .app-main { |
|||
padding-top: 84px; |
|||
} |
|||
} |
|||
</style> |
|||
|
|||
<style lang="scss"> |
|||
.el-popup-parent--hidden { |
|||
.fixed-header { |
|||
padding-right: 15px; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,265 @@ |
|||
<template> |
|||
<div class="navbar"> |
|||
|
|||
<hamburger id="hamburger-container" class="hamburger-container" @toggleClick="toggleSideBar" /> |
|||
<!-- 移除面包屑效果 --> |
|||
<!-- <breadcrumb id="breadcrumb-container" class="breadcrumb-container" /> --> |
|||
|
|||
<!-- 顶部菜单 start--> |
|||
<div class="navmenu"> |
|||
<template> |
|||
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal" @select="handleSelect"> |
|||
<el-menu-item :index="resolvePath(onlyOneChild.path)"> |
|||
</el-menu-item> |
|||
</el-menu> |
|||
</template> |
|||
</div> |
|||
<!-- 顶部菜单 end--> |
|||
<div class="right-menu"> |
|||
<template> |
|||
<search id="header-search" class="right-menu-item" /> |
|||
<el-tooltip content="项目文档" effect="dark" placement="bottom"> |
|||
<Doc class="right-menu-item hover-effect" /> |
|||
</el-tooltip> |
|||
<el-tooltip content="全屏缩放" effect="dark" placement="bottom"> |
|||
<screenfull id="screenfull" class="right-menu-item hover-effect" /> |
|||
</el-tooltip> |
|||
<el-tooltip content="布局设置" effect="dark" placement="bottom"> |
|||
<size-select id="size-select" class="right-menu-item hover-effect" /> |
|||
</el-tooltip> |
|||
</template> |
|||
|
|||
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click"> |
|||
<div class="avatar-wrapper"> |
|||
<img class="user-avatar"> |
|||
<i class="el-icon-caret-bottom" /> |
|||
</div> |
|||
<el-dropdown-menu slot="dropdown"> |
|||
<span style="display:block;" @click="show = true"> |
|||
<el-dropdown-item> |
|||
布局设置 |
|||
</el-dropdown-item> |
|||
</span> |
|||
<router-link to="/user/center"> |
|||
<el-dropdown-item> |
|||
个人中心 |
|||
</el-dropdown-item> |
|||
</router-link> |
|||
<span style="display:block;" @click="open"> |
|||
<el-dropdown-item divided> |
|||
退出登录 |
|||
</el-dropdown-item> |
|||
</span> |
|||
</el-dropdown-menu> |
|||
</el-dropdown> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { mapGetters } from "vuex"; |
|||
//import Breadcrumb from "@/components/Breadcrumb"; |
|||
//import Hamburger from "@/components/Hamburger"; |
|||
//import Doc from "@/components/Doc"; |
|||
//import Screenfull from "@/components/Screenfull"; |
|||
//import SizeSelect from "@/components/SizeSelect"; |
|||
//import Search from "@/components/HeaderSearch"; |
|||
import Avatar from "@/assets/images/avatar.png"; |
|||
import path from "path"; |
|||
import { isExternal } from "@/utils/validate"; |
|||
//import Item from './Sidebar/Item' |
|||
import AppLink from './Sidebar/Link' |
|||
|
|||
export default { |
|||
components: { |
|||
//Breadcrumb, 移除面包屑 |
|||
// Hamburger, |
|||
//Screenfull, |
|||
// SizeSelect, |
|||
//Search, |
|||
//Doc, |
|||
// Item, |
|||
AppLink |
|||
}, |
|||
props: { |
|||
// item: { |
|||
// type: Object, |
|||
// required: true |
|||
// }, |
|||
isNest: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
basePath: { |
|||
type: String, |
|||
default: '' |
|||
} |
|||
}, |
|||
data() { |
|||
this.onlyOneChild=null |
|||
return { |
|||
Avatar: Avatar, |
|||
dialogVisible: false, |
|||
activeIndex: "1", |
|||
activeIndex2: "1", |
|||
}; |
|||
}, |
|||
computed: { |
|||
//...mapGetters(["sidebar", "device", "user", "baseApi"]), |
|||
show: { |
|||
get() { |
|||
return this.$store.state.settings.showSettings; |
|||
}, |
|||
set(val) { |
|||
this.$store.dispatch("settings/changeSetting", { |
|||
key: "showSettings", |
|||
value: val, |
|||
}); |
|||
}, |
|||
}, |
|||
}, |
|||
methods: { |
|||
toggleSideBar() { |
|||
this.$store.dispatch("app/toggleSideBar"); |
|||
}, |
|||
open() { |
|||
this.$confirm("确定注销并退出系统吗?", "提示", { |
|||
confirmButtonText: "确定", |
|||
cancelButtonText: "取消", |
|||
type: "warning", |
|||
}).then(() => { |
|||
this.logout(); |
|||
}); |
|||
}, |
|||
handleSelect(key, keyPath) { |
|||
console.log(key, keyPath); |
|||
}, |
|||
hasOneShowingChild(children = [], parent) { |
|||
// const showingChildren = children.filter((item) => { |
|||
// if (item.hidden) { |
|||
// return false; |
|||
// } else { |
|||
// // Temp set(will be used if only has one showing child) |
|||
// this.onlyOneChild = item; |
|||
// return true; |
|||
// } |
|||
// }); |
|||
|
|||
// When there is only one child router, the child router is displayed by default |
|||
// if (showingChildren.length === 1) { |
|||
// return true; |
|||
// } |
|||
|
|||
// // 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); |
|||
}, |
|||
logout() { |
|||
this.$store.dispatch("LogOut").then(() => { |
|||
location.reload(); |
|||
}); |
|||
}, |
|||
}, |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.navbar { |
|||
height: 60px; |
|||
overflow: hidden; |
|||
position: relative; |
|||
background: #fff; |
|||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); |
|||
|
|||
.navmenu { |
|||
float: left; |
|||
} |
|||
|
|||
.hamburger-container { |
|||
line-height: 46px; |
|||
height: 100%; |
|||
float: left; |
|||
cursor: pointer; |
|||
transition: background 0.3s; |
|||
-webkit-tap-highlight-color: transparent; |
|||
|
|||
&:hover { |
|||
background: rgba(0, 0, 0, 0.025); |
|||
} |
|||
} |
|||
|
|||
.breadcrumb-container { |
|||
float: left; |
|||
} |
|||
|
|||
.errLog-container { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
} |
|||
|
|||
.right-menu { |
|||
float: right; |
|||
height: 100%; |
|||
line-height: 50px; |
|||
|
|||
&:focus { |
|||
outline: none; |
|||
} |
|||
|
|||
.right-menu-item { |
|||
display: inline-block; |
|||
padding: 0 8px; |
|||
height: 100%; |
|||
font-size: 18px; |
|||
color: #5a5e66; |
|||
vertical-align: text-bottom; |
|||
|
|||
&.hover-effect { |
|||
cursor: pointer; |
|||
transition: background 0.3s; |
|||
|
|||
&:hover { |
|||
background: rgba(0, 0, 0, 0.025); |
|||
} |
|||
} |
|||
} |
|||
|
|||
.avatar-container { |
|||
margin-right: 30px; |
|||
|
|||
.avatar-wrapper { |
|||
margin-top: 5px; |
|||
position: relative; |
|||
|
|||
.user-avatar { |
|||
cursor: pointer; |
|||
width: 40px; |
|||
height: 40px; |
|||
border-radius: 10px; |
|||
} |
|||
|
|||
.el-icon-caret-bottom { |
|||
cursor: pointer; |
|||
position: absolute; |
|||
right: -20px; |
|||
top: 25px; |
|||
font-size: 12px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -1,7 +1,24 @@ |
|||
// export default{
|
|||
// computed:{
|
|||
// device(){
|
|||
// return this.$store.state.app
|
|||
// }
|
|||
// }
|
|||
// }
|
|||
export default { |
|||
computed: { |
|||
device() { |
|||
return this.$store.state.app.device |
|||
} |
|||
}, |
|||
mounted() { |
|||
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) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,36 @@ |
|||
|
|||
<template> |
|||
|
|||
<component v-bind:is="linkProps(to)"> |
|||
<slot /> |
|||
</component> |
|||
</template> |
|||
|
|||
<script> |
|||
import { isExternal } from '@/utils/validate' |
|||
|
|||
export default { |
|||
props: { |
|||
to: { |
|||
type: String, |
|||
required: true |
|||
} |
|||
}, |
|||
methods: { |
|||
linkProps(url) { |
|||
if (isExternal(url)) { |
|||
return { |
|||
is: 'a', |
|||
href: url, |
|||
target: '_blank', |
|||
rel: 'noopener' |
|||
} |
|||
} |
|||
return { |
|||
is: 'router-link', |
|||
to: url |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
@ -0,0 +1,90 @@ |
|||
<template> |
|||
<div v-if="!item.hidden" class="menu-wrapper"> |
|||
<template v-if=" |
|||
hasOneShowingChild(item.children, item) && |
|||
(!onlyOneChild.children || onlyOneChild.noShowingChildren) && |
|||
!item.alwaysShow |
|||
"> |
|||
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)"> |
|||
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }"> |
|||
<item :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" :title="onlyOneChild.meta.title" /> |
|||
</el-menu-item> |
|||
</app-link> |
|||
</template> |
|||
|
|||
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body> |
|||
<template slot="title"> |
|||
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" /> |
|||
</template> |
|||
<sidebar-item v-for="child in item.children" :key="child.path" :is-nest="true" :item="child" :base-path="resolvePath(child.path)" class="nest-menu" /> |
|||
</el-submenu> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import path from 'path' |
|||
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: '' |
|||
} |
|||
}, |
|||
data() { |
|||
this.onlyOneChild = null |
|||
return {} |
|||
}, |
|||
methods: { |
|||
hasOneShowingChild(children = [], parent) { |
|||
const showingChildren = children.filter(item => { |
|||
if (item.hidden) { |
|||
return false |
|||
} else { |
|||
// Temp set(will be used if only has one showing child) |
|||
this.onlyOneChild = item |
|||
return true |
|||
} |
|||
}) |
|||
|
|||
// When there is only one child router, the child router is displayed by default |
|||
if (showingChildren.length === 1) { |
|||
return true |
|||
} |
|||
|
|||
// 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) |
|||
} |
|||
} |
|||
} |
|||
</script> |
@ -1,44 +0,0 @@ |
|||
<!-- |
|||
* @Author: Liu_li |
|||
* @Descripttion: |
|||
* @Date: 2021-09-26 11:03:50 |
|||
--> |
|||
<template> |
|||
<div> |
|||
<template> |
|||
<app-link> |
|||
<el-menu-item> |
|||
<item :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" :title="onlyOneChild.meta.title" /> |
|||
</el-menu-item> |
|||
</app-link> |
|||
</template> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import path from 'path' |
|||
import {isExternal} from '@/utils/validate' |
|||
|
|||
export default { |
|||
name:'SidebarItem', |
|||
props:{ |
|||
item:{ |
|||
type:Object, |
|||
required:true |
|||
}, |
|||
isNest:{ |
|||
type:Boolean, |
|||
default:false |
|||
}, |
|||
basePath:{ |
|||
type:String, |
|||
default:'' |
|||
} |
|||
}, |
|||
data() { |
|||
this.onlyOneChild=null |
|||
return { |
|||
} |
|||
}, |
|||
} |
|||
</script> |
@ -0,0 +1,45 @@ |
|||
<!-- |
|||
* @Author: Liu_li |
|||
* @Descripttion: 顶部菜单 |
|||
* @Date: 2021-09-26 11:03:50 |
|||
--> |
|||
<template> |
|||
<div> |
|||
<el-menu :default-active="activeMenu" mode="horizontal" @select="handleSelect"> |
|||
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)"> |
|||
</app-link> |
|||
</el-menu> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import path from 'path' |
|||
import { isExternal } from '@/utils/validate' |
|||
import Item from './Item' |
|||
|
|||
export default { |
|||
name: 'TopMenus', |
|||
comments: { Item }, |
|||
props: { |
|||
item: { |
|||
type: Object, |
|||
required: true |
|||
} |
|||
}, |
|||
data() { |
|||
this.onlyOneChild = null |
|||
return { |
|||
activeIndex: '1', |
|||
activeIndex2: '1' |
|||
} |
|||
}, |
|||
methods: { |
|||
hasOneShowingChild(children = [], parent) { |
|||
const showingChildren = children.filter(item => { |
|||
this.onlyOneChild = item |
|||
return true |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
@ -0,0 +1,81 @@ |
|||
<template> |
|||
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll"> |
|||
<slot /> |
|||
</el-scrollbar> |
|||
</template> |
|||
|
|||
<script> |
|||
const tagAndTagSpacing = 4 |
|||
|
|||
export default { |
|||
name: 'ScrollPane', |
|||
data() { |
|||
return { |
|||
left: 0 |
|||
} |
|||
}, |
|||
computed: { |
|||
scrollWrapper() { |
|||
return this.$refs.scrollContainer.$refs.wrap |
|||
} |
|||
}, |
|||
methods: { |
|||
handleScroll(e) { |
|||
const eventDelta = e.wheelDelta || -e.deltaY * 40 |
|||
const $scrollWrapper = this.scrollWrapper |
|||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4 |
|||
}, |
|||
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 |
|||
|
|||
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 { |
|||
const currentIndex = tagList.findIndex(item => item === currentTag) |
|||
const prevTag = tagList[currentIndex - 1] |
|||
const nextTag = tagList[currentIndex + 1] |
|||
|
|||
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing |
|||
|
|||
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: 49px; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,275 @@ |
|||
<template> |
|||
<div id="tags-view-container" class="tags-view-container"> |
|||
<scroll-pane ref="scrollPane" class="tags-view-wrapper"> |
|||
<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="closeSelectedTag(tag)" @contextmenu.prevent.native="openMenu(tag,$event)"> |
|||
{{ tag.title }} |
|||
<span v-if="!tag.meta.affix" class="el-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="!(selectedTag.meta&&selectedTag.meta.affix)" @click="closeSelectedTag(selectedTag)">关闭</li> |
|||
<li @click="closeOthersTags">关闭其他</li> |
|||
<li @click="closeAllTags(selectedTag)">关闭全部</li> |
|||
</ul> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import ScrollPane from './ScrollPane' |
|||
import path from 'path' |
|||
|
|||
export default { |
|||
components: { ScrollPane }, |
|||
data() { |
|||
return { |
|||
visible: false, |
|||
top: 0, |
|||
left: 0, |
|||
selectedTag: {}, |
|||
affixTags: [] |
|||
} |
|||
}, |
|||
computed: { |
|||
visitedViews() { |
|||
return this.$store.state.tagsView.visitedViews |
|||
}, |
|||
routes() { |
|||
return this.$store.state.permission.routers |
|||
} |
|||
}, |
|||
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: { |
|||
isActive(route) { |
|||
return route.path === this.$route.path |
|||
}, |
|||
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) |
|||
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) |
|||
} 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('/') |
|||
} |
|||
} |
|||
}, |
|||
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 |
|||
this.visible = true |
|||
this.selectedTag = tag |
|||
}, |
|||
closeMenu() { |
|||
this.visible = false |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.tags-view-container { |
|||
height: 34px; |
|||
width: 100%; |
|||
background: #fff; |
|||
border-bottom: 1px solid #d8dce5; |
|||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04); |
|||
.tags-view-wrapper { |
|||
.tags-view-item { |
|||
display: inline-block; |
|||
position: relative; |
|||
cursor: pointer; |
|||
height: 26px; |
|||
line-height: 26px; |
|||
border: 1px solid #d8dce5; |
|||
color: #495060; |
|||
background: #fff; |
|||
padding: 0 8px; |
|||
font-size: 12px; |
|||
margin-left: 5px; |
|||
margin-top: 4px; |
|||
&:first-of-type { |
|||
margin-left: 15px; |
|||
} |
|||
&:last-of-type { |
|||
margin-right: 15px; |
|||
} |
|||
&.active { |
|||
background-color: #42b983; |
|||
color: #fff; |
|||
border-color: #42b983; |
|||
&::before { |
|||
content: ''; |
|||
background: #fff; |
|||
display: inline-block; |
|||
width: 8px; |
|||
height: 8px; |
|||
border-radius: 50%; |
|||
position: relative; |
|||
margin-right: 2px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
.contextmenu { |
|||
margin: 0; |
|||
background: #fff; |
|||
z-index: 3000; |
|||
position: absolute; |
|||
list-style-type: none; |
|||
padding: 5px 0; |
|||
border-radius: 4px; |
|||
font-size: 12px; |
|||
font-weight: 400; |
|||
color: #333; |
|||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3); |
|||
li { |
|||
margin: 0; |
|||
padding: 7px 16px; |
|||
cursor: pointer; |
|||
&:hover { |
|||
background: #eee; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
|
|||
<style lang="scss"> |
|||
//reset element css of el-icon-close |
|||
.tags-view-wrapper { |
|||
.tags-view-item { |
|||
.el-icon-close { |
|||
width: 16px; |
|||
height: 16px; |
|||
vertical-align: 2px; |
|||
border-radius: 50%; |
|||
text-align: center; |
|||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); |
|||
transform-origin: 100% 50%; |
|||
&:before { |
|||
transform: scale(0.6); |
|||
display: inline-block; |
|||
vertical-align: -3px; |
|||
} |
|||
&:hover { |
|||
background-color: #b4bccc; |
|||
color: #fff; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,5 @@ |
|||
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' |
@ -0,0 +1,166 @@ |
|||
|
|||
/** |
|||
* 是否合法URL |
|||
* @param {string} path |
|||
* @returns {Boolean} |
|||
*/ |
|||
export function isExternal(path) { |
|||
return /^(https?:|mailto:|tel:)/.test(path) |
|||
} |
|||
|
|||
/** |
|||
* @param {string} str |
|||
* @returns {Boolean} |
|||
*/ |
|||
export function validUsername(str) { |
|||
const valid_map = ['admin', 'editor'] |
|||
return valid_map.indexOf(str.trim()) >= 0 |
|||
} |
|||
|
|||
/** |
|||
* @param {string} url |
|||
* @returns {Boolean} |
|||
*/ |
|||
export function validURL(url) { |
|||
const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ |
|||
return reg.test(url) |
|||
} |
|||
|
|||
/** |
|||
* @param {string} str |
|||
* @returns {Boolean} |
|||
*/ |
|||
export function validLowerCase(str) { |
|||
const reg = /^[a-z]+$/ |
|||
return reg.test(str) |
|||
} |
|||
|
|||
/** |
|||
* @param {string} str |
|||
* @returns {Boolean} |
|||
*/ |
|||
export function validUpperCase(str) { |
|||
const reg = /^[A-Z]+$/ |
|||
return reg.test(str) |
|||
} |
|||
|
|||
/** |
|||
* @param {string} str |
|||
* @returns {Boolean} |
|||
*/ |
|||
export function validAlphabets(str) { |
|||
const reg = /^[A-Za-z]+$/ |
|||
return reg.test(str) |
|||
} |
|||
|
|||
/** |
|||
* @param {string} email |
|||
* @returns {Boolean} |
|||
*/ |
|||
export function validEmail(email) { |
|||
const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ |
|||
return reg.test(email) |
|||
} |
|||
|
|||
export function isvalidPhone(phone) { |
|||
const reg = /^1[3|4|5|7|8][0-9]\d{8}$/ |
|||
return reg.test(phone) |
|||
} |
|||
|
|||
/** |
|||
* @param {string} str |
|||
* @returns {Boolean} |
|||
*/ |
|||
export function isString(str) { |
|||
if (typeof str === 'string' || str instanceof String) { |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
|
|||
/** |
|||
* @param {Array} arg |
|||
* @returns {Boolean} |
|||
*/ |
|||
export function isArray(arg) { |
|||
if (typeof Array.isArray === 'undefined') { |
|||
return Object.prototype.toString.call(arg) === '[object Array]' |
|||
} |
|||
return Array.isArray(arg) |
|||
} |
|||
|
|||
/** |
|||
* 是否合法IP地址 |
|||
* @param rule |
|||
* @param value |
|||
* @param callback |
|||
*/ |
|||
export function validateIP(rule, value, callback) { |
|||
if (value === '' || value === undefined || value == null) { |
|||
callback() |
|||
} else { |
|||
const reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/ |
|||
if ((!reg.test(value)) && value !== '') { |
|||
callback(new Error('请输入正确的IP地址')) |
|||
} else { |
|||
callback() |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 是否手机号码或者固话*/ |
|||
export function validatePhoneTwo(rule, value, callback) { |
|||
const reg = /^((0\d{2,3}-\d{7,8})|(1[34578]\d{9}))$/ |
|||
if (value === '' || value === undefined || value == null) { |
|||
callback() |
|||
} else { |
|||
if ((!reg.test(value)) && value !== '') { |
|||
callback(new Error('请输入正确的电话号码或者固话号码')) |
|||
} else { |
|||
callback() |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 是否固话*/ |
|||
export function validateTelephone(rule, value, callback) { |
|||
const reg = /0\d{2}-\d{7,8}/ |
|||
if (value === '' || value === undefined || value == null) { |
|||
callback() |
|||
} else { |
|||
if ((!reg.test(value)) && value !== '') { |
|||
callback(new Error('请输入正确的固话(格式:区号+号码,如010-1234567)')) |
|||
} else { |
|||
callback() |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 是否手机号码*/ |
|||
export function validatePhone(rule, value, callback) { |
|||
const reg = /^[1][3,4,5,7,8][0-9]{9}$/ |
|||
if (value === '' || value === undefined || value == null) { |
|||
callback() |
|||
} else { |
|||
if ((!reg.test(value)) && value !== '') { |
|||
callback(new Error('请输入正确的电话号码')) |
|||
} else { |
|||
callback() |
|||
} |
|||
} |
|||
} |
|||
|
|||
/* 是否身份证号码*/ |
|||
export function validateIdNo(rule, value, callback) { |
|||
const reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/ |
|||
if (value === '' || value === undefined || value == null) { |
|||
callback() |
|||
} else { |
|||
if ((!reg.test(value)) && value !== '') { |
|||
callback(new Error('请输入正确的身份证号码')) |
|||
} else { |
|||
callback() |
|||
} |
|||
} |
|||
} |
|||
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue