Browse Source

commit code

master
刘力 3 years ago
parent
commit
c785d2aefb
  1. 160
      src/components/ThemePicker/index.vue
  2. 60
      src/layout/components/AppMain.vue
  3. 265
      src/layout/components/Navbar.vue
  4. 31
      src/layout/components/Sidebar/FixiOSBug.js
  5. 36
      src/layout/components/Sidebar/Link.vue
  6. 90
      src/layout/components/Sidebar/SidebarItem.vue
  7. 44
      src/layout/components/Sidebar/TopMenu.vue
  8. 45
      src/layout/components/Sidebar/TopMenus.vue
  9. 81
      src/layout/components/TagsView/ScrollPane.vue
  10. 275
      src/layout/components/TagsView/index.vue
  11. 5
      src/layout/components/index.js
  12. 72
      src/layout/index.vue
  13. 6
      src/main.js
  14. 11
      src/router/routers.js
  15. 166
      src/utils/validate.js
  16. 4
      vue.config.js

160
src/components/ThemePicker/index.vue

@ -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>

60
src/layout/components/AppMain.vue

@ -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>

265
src/layout/components/Navbar.vue

@ -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>

31
src/layout/components/Sidebar/FixiOSBug.js

@ -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)
}
}
}
}
}

36
src/layout/components/Sidebar/Link.vue

@ -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>

90
src/layout/components/Sidebar/SidebarItem.vue

@ -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>

44
src/layout/components/Sidebar/TopMenu.vue

@ -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>

45
src/layout/components/Sidebar/TopMenus.vue

@ -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>

81
src/layout/components/TagsView/ScrollPane.vue

@ -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>

275
src/layout/components/TagsView/index.vue

@ -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>

5
src/layout/components/index.js

@ -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'

72
src/layout/index.vue

@ -1,71 +1,71 @@
<template>
<div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<sidebar class="sidebar-container" />
<div :class="{hasTagsView:needTagsView}" class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<div>
<div class="drawer-bg" @click="handleClickOutside" />
<!-- <sidebar class="sidebar-container" /> -->
<!-- <div :class="{hasTagsView:needTagsView}" class="main-container">
<div :class="{'fixed-header':fixedHeader}"> -->
<navbar />
<tags-view v-if="needTagsView" />
<!-- <tags-view v-if="needTagsView" />
</div>
<app-main />
<right-panel v-if="showSettings">
<settings />
</right-panel>
</div>
</right-panel> -->
<!-- </div> -->
<!-- 防止刷新后主题丢失 -->
<Theme v-show="false" ref="theme" />
<!-- <Theme v-show="false" ref="theme" /> -->
</div>
</template>
<script>
import RightPanel from '@/components/RightPanel'
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import ResizeMixin from './mixin/ResizeHandler'
//import RightPanel from '@/components/RightPanel'
import { AppMain,Navbar} from './components'
//import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'
import Theme from '@/components/ThemePicker'
//import Theme from '@/components/ThemePicker'
import Cookies from 'js-cookie'
export default {
name: 'Layout',
components: {
AppMain,
Navbar,
RightPanel,
Settings,
Sidebar,
TagsView,
Theme
//RightPanel,
//Settings,
//Sidebar,
//TagsView,
//Theme
},
mixins: [ResizeMixin],
//mixins: [ResizeMixin],
computed: {
...mapState({
sidebar: state => state.app.sidebar,
device: state => state.app.device,
showSettings: state => state.settings.showSettings,
needTagsView: state => state.settings.tagsView,
fixedHeader: state => state.settings.fixedHeader
//sidebar: state => state.app.sidebar,
//device: state => state.app.device,
//showSettings: state => state.settings.showSettings,
//needTagsView: state => state.settings.tagsView,
//fixedHeader: state => state.settings.fixedHeader
}),
classObj () {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile'
//hideSidebar: !this.sidebar.opened,
// openSidebar: this.sidebar.opened,
// withoutAnimation: this.sidebar.withoutAnimation,
// mobile: this.device === 'mobile'
}
}
},
mounted () {
if (Cookies.get('theme')) {
this.$refs.theme.theme = Cookies.get('theme')
this.$store.dispatch('settings/changeSetting', {
key: 'theme',
value: Cookies.get('theme')
})
}
// if (Cookies.get('theme')) {
// //this.$refs.theme.theme = Cookies.get('theme')
// this.$store.dispatch('settings/changeSetting', {
// key: 'theme',
// value: Cookies.get('theme')
// })
// }
},
methods: {
handleClickOutside () {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
//this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
}

6
src/main.js

@ -13,10 +13,10 @@ import 'element-ui/lib/theme-chalk/index.css'
import VueHighlightJS from 'vue-highlightjs'
import 'highlight.js/styles/atom-one-dark.css'
// 加载权限控制
import './router/index'
//import './router/index'
//加载权限指令
import checkPer from '@/utils/permission'
//import checkPer from '@/utils/permission'
//import permission from './components/Permission'
// 加载代码高亮
@ -27,7 +27,7 @@ Vue.use(ElementUI, {
size: Cookies.get('size') || 'small'
})
//加载权限指令
Vue.use(checkPer)
//Vue.use(checkPer)
//Vue.use(permission)
Vue.config.productionTip = false

11
src/router/routers.js

@ -27,10 +27,11 @@ export const constantRouterMap = [
component: (resolve) => require(['@/views/features/401'], resolve),
hidden: true
},
// {
// path: '/redirect',
// component: Layout,
// hidden: true,
{
path: '/redirect',
component: Layout,
hidden: true,
// children: [
// {
// path: '/redirect/:path*',
@ -39,7 +40,7 @@ export const constantRouterMap = [
// meta: { title: '首页', icon: 'index', affix: true, noCache: true }
// }
// ]
// },
},
// {
// path: '/user',
// component: Layout,

166
src/utils/validate.js

@ -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()
}
}
}

4
vue.config.js

@ -61,6 +61,10 @@ module.exports = {
}
}
},
//调试VUE
configureWebpack: {
devtool: 'source-map'
}
// configureWebpack: {
// // 覆盖webpack默认配置的都在这里
// name: name,

Loading…
Cancel
Save