离校前端框架,包括数据字典、工作队伍、新闻公告模块
diff --git a/leave-school-vue/src/views/layout/Layout.vue b/leave-school-vue/src/views/layout/Layout.vue
new file mode 100644
index 0000000..df7af87
--- /dev/null
+++ b/leave-school-vue/src/views/layout/Layout.vue
@@ -0,0 +1,74 @@
+<template>
+ <div class="app-wrapper" :class="classObj">
+ <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"></div>
+ <sidebar class="sidebar-container"></sidebar>
+ <div class="main-container">
+ <navbar></navbar>
+ <tags-view v-if='showtags'></tags-view>
+ <app-main></app-main>
+ </div>
+ </div>
+</template>
+
+<script>
+import { Navbar, Sidebar, AppMain, TagsView } from './components'
+import ResizeMixin from './mixin/ResizeHandler'
+
+export default {
+ name: 'layout',
+ components: {
+ Navbar,
+ Sidebar,
+ AppMain,
+ TagsView
+ },
+ mixins: [ResizeMixin],
+ computed: {
+ showtags() {
+ return process.env.SHOW_TAGS
+ },
+ sidebar() {
+ return this.$store.state.app.sidebar
+ },
+ device() {
+ return this.$store.state.app.device
+ },
+ classObj() {
+ return {
+ hideSidebar: !this.sidebar.opened,
+ openSidebar: this.sidebar.opened,
+ withoutAnimation: this.sidebar.withoutAnimation,
+ mobile: this.device === 'mobile'
+ }
+ }
+ },
+ methods: {
+ handleClickOutside() {
+ this.$store.dispatch('CloseSideBar', { withoutAnimation: false })
+ }
+ }
+}
+</script>
+
+<style rel="stylesheet/scss" lang="scss" scoped>
+ @import "src/styles/mixin.scss";
+ .app-wrapper {
+ @include clearfix;
+ position: relative;
+ height: 100%;
+ width: 100%;
+ &.mobile.openSidebar{
+ position: fixed;
+ top: 0;
+ }
+ }
+ .drawer-bg {
+ background: #000;
+ opacity: 0.3;
+ width: 100%;
+ top: 0;
+ height: 100%;
+ position: absolute;
+ z-index: 999;
+ }
+</style>
diff --git a/leave-school-vue/src/views/layout/components/AppMain.vue b/leave-school-vue/src/views/layout/components/AppMain.vue
new file mode 100644
index 0000000..64ecff8
--- /dev/null
+++ b/leave-school-vue/src/views/layout/components/AppMain.vue
@@ -0,0 +1,33 @@
+<template>
+ <section class="app-main">
+ <transition name="fade" mode="out-in">
+ <!-- <router-view :key="key"></router-view> -->
+ <keep-alive :include="cachedViews">
+ <router-view :key="key"></router-view>
+ </keep-alive>
+ </transition>
+ </section>
+</template>
+
+<script>
+export default {
+ name: 'AppMain',
+ computed: {
+ cachedViews() {
+ return this.$store.state.tagsView.cachedViews
+ },
+ key() {
+ return this.$route.fullPath
+ }
+ }
+}
+</script>
+
+<style scoped>
+.app-main {
+ /*50 = navbar */
+ min-height: calc(100vh - 84px);
+ position: relative;
+ overflow: hidden;
+}
+</style>
diff --git a/leave-school-vue/src/views/layout/components/Navbar.vue b/leave-school-vue/src/views/layout/components/Navbar.vue
new file mode 100644
index 0000000..c901e8f
--- /dev/null
+++ b/leave-school-vue/src/views/layout/components/Navbar.vue
@@ -0,0 +1,95 @@
+<template>
+ <el-menu class="navbar" mode="horizontal">
+ <hamburger class="hamburger-container" :toggleClick="toggleSideBar" :isActive="sidebar.opened"></hamburger>
+ <breadcrumb></breadcrumb>
+ <el-dropdown class="avatar-container" trigger="click">
+ <div class="avatar-wrapper">
+ <img class="user-avatar" :src="avatar">
+ <i class="el-icon-caret-bottom"></i>
+ </div>
+ <el-dropdown-menu class="user-dropdown" slot="dropdown">
+ <router-link class="inlineBlock" to="/">
+ <el-dropdown-item>
+ 首页
+ </el-dropdown-item>
+ </router-link>
+ <el-dropdown-item divided>
+ <span @click="logout" style="display:block;">注销</span>
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </el-dropdown>
+ </el-menu>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import Breadcrumb from '@/components/Breadcrumb'
+import Hamburger from '@/components/Hamburger'
+
+export default {
+ components: {
+ Breadcrumb,
+ Hamburger
+ },
+ computed: {
+ ...mapGetters([
+ 'sidebar',
+ 'avatar'
+ ])
+ },
+ methods: {
+ toggleSideBar() {
+ this.$store.dispatch('ToggleSideBar')
+ },
+ logout() {
+ var pro = this.$store.dispatch('LogOut')
+ pro.then(() => {
+ location.reload() // 为了重新实例化vue-router对象 避免bug
+ })
+ }
+ }
+}
+</script>
+
+<style rel="stylesheet/scss" lang="scss" scoped>
+.navbar {
+ height: 50px;
+ line-height: 50px;
+ border-radius: 0px !important;
+ .hamburger-container {
+ line-height: 58px;
+ height: 50px;
+ float: left;
+ padding: 0 10px;
+ }
+ .screenfull {
+ position: absolute;
+ right: 90px;
+ top: 16px;
+ color: red;
+ }
+ .avatar-container {
+ height: 50px;
+ display: inline-block;
+ position: absolute;
+ right: 35px;
+ .avatar-wrapper {
+ cursor: pointer;
+ margin-top: 5px;
+ position: relative;
+ .user-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 10px;
+ }
+ .el-icon-caret-bottom {
+ position: absolute;
+ right: -20px;
+ top: 25px;
+ font-size: 12px;
+ }
+ }
+ }
+}
+</style>
+
diff --git a/leave-school-vue/src/views/layout/components/Sidebar/SidebarItem.vue b/leave-school-vue/src/views/layout/components/Sidebar/SidebarItem.vue
new file mode 100644
index 0000000..78edb91
--- /dev/null
+++ b/leave-school-vue/src/views/layout/components/Sidebar/SidebarItem.vue
@@ -0,0 +1,78 @@
+<template>
+ <div v-if="!item.hidden&&item.children" class="menu-wrapper">
+
+ <router-link v-if="hasOneShowingChild(item.children) && !onlyOneChild.children&&!item.alwaysShow" :to="resolvePath(onlyOneChild.path)">
+ <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
+ <svg-icon v-if="onlyOneChild.meta&&onlyOneChild.meta.icon" :icon-class="onlyOneChild.meta.icon"></svg-icon>
+ <span v-if="onlyOneChild.meta&&onlyOneChild.meta.title" slot="title">{{onlyOneChild.meta.title}}</span>
+ </el-menu-item>
+ </router-link>
+
+ <el-submenu v-else :index="item.name||item.path">
+ <template slot="title">
+ <svg-icon v-if="item.meta&&item.meta.icon" :icon-class="item.meta.icon"></svg-icon>
+ <span v-if="item.meta&&item.meta.title" slot="title">{{item.meta.title}}</span>
+ </template>
+
+ <template v-for="child in item.children" v-if="!child.hidden">
+ <sidebar-item :is-nest="true" class="nest-menu" v-if="child.children&&child.children.length>0" :item="child" :key="child.path" :base-path="resolvePath(child.path)"></sidebar-item>
+
+ <router-link v-else :to="resolvePath(child.path)" :key="child.name">
+ <el-menu-item :index="resolvePath(child.path)">
+ <svg-icon v-if="child.meta&&child.meta.icon" :icon-class="child.meta.icon"></svg-icon>
+ <span v-if="child.meta&&child.meta.title" slot="title">{{child.meta.title}}</span>
+ </el-menu-item>
+ </router-link>
+ </template>
+ </el-submenu>
+
+ </div>
+</template>
+
+<script>
+import path from 'path'
+
+export default {
+ name: 'SidebarItem',
+ props: {
+ // route配置json
+ item: {
+ type: Object,
+ required: true
+ },
+ isNest: {
+ type: Boolean,
+ default: false
+ },
+ basePath: {
+ type: String,
+ default: ''
+ }
+ },
+ data() {
+ return {
+ onlyOneChild: null
+ }
+ },
+ methods: {
+ hasOneShowingChild(children) {
+ 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
+ }
+ })
+ if (showingChildren.length === 1) {
+ return true
+ }
+ return false
+ },
+ resolvePath(...paths) {
+ return path.resolve(this.basePath, ...paths)
+ }
+ }
+}
+</script>
diff --git a/leave-school-vue/src/views/layout/components/Sidebar/index.vue b/leave-school-vue/src/views/layout/components/Sidebar/index.vue
new file mode 100644
index 0000000..a84207e
--- /dev/null
+++ b/leave-school-vue/src/views/layout/components/Sidebar/index.vue
@@ -0,0 +1,35 @@
+<template>
+ <el-scrollbar wrapClass="scrollbar-wrapper">
+ <el-menu
+ mode="vertical"
+ :show-timeout="200"
+ :default-active="$route.path"
+ :collapse="isCollapse"
+ background-color="#304156"
+ text-color="#bfcbd9"
+ active-text-color="#409EFF"
+ >
+ <sidebar-item v-for="route in routes" :key="route.name" :item="route" :base-path="route.path"></sidebar-item>
+ </el-menu>
+ </el-scrollbar>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import SidebarItem from './SidebarItem'
+
+export default {
+ components: { SidebarItem },
+ computed: {
+ ...mapGetters([
+ 'sidebar'
+ ]),
+ routes() {
+ return this.$router.options.routes
+ },
+ isCollapse() {
+ return !this.sidebar.opened
+ }
+ }
+}
+</script>
diff --git a/leave-school-vue/src/views/layout/components/TagsView.vue b/leave-school-vue/src/views/layout/components/TagsView.vue
new file mode 100644
index 0000000..121c2b2
--- /dev/null
+++ b/leave-school-vue/src/views/layout/components/TagsView.vue
@@ -0,0 +1,205 @@
+<template>
+ <div class="tags-view-container">
+ <scroll-pane class='tags-view-wrapper' ref='scrollPane'>
+ <router-link ref='tag' class="tags-view-item" :class="isActive(tag)?'active':''" v-for="tag in Array.from(visitedViews)"
+ :to="tag" :key="tag.path" @contextmenu.prevent.native="openMenu(tag,$event)">
+ {{tag.title}}
+ <span class='el-icon-close' @click.prevent.stop='closeSelectedTag(tag)'></span>
+ </router-link>
+ </scroll-pane>
+ <ul class='contextmenu' v-show="visible" :style="{left:left+'px',top:top+'px'}">
+ <li @click="closeSelectedTag(selectedTag)">关闭当前</li>
+ <li @click="closeOthersTags">关闭其它</li>
+ <li @click="closeAllTags">关闭所有</li>
+ </ul>
+ </div>
+</template>
+
+<script>
+import ScrollPane from '@/components/ScrollPane'
+// import { generateTitle } from '@/utils/i18n'
+
+export default {
+ components: { ScrollPane },
+ data() {
+ return {
+ visible: false,
+ top: 0,
+ left: 0,
+ selectedTag: {}
+ }
+ },
+ computed: {
+ visitedViews() {
+ return this.$store.state.tagsView.visitedViews
+ }
+ },
+ watch: {
+ $route() {
+ this.addViewTags()
+ this.moveToCurrentTag()
+ },
+ visible(value) {
+ if (value) {
+ document.body.addEventListener('click', this.closeMenu)
+ } else {
+ document.body.removeEventListener('click', this.closeMenu)
+ }
+ }
+ },
+ mounted() {
+ this.addViewTags()
+ },
+ methods: {
+ // generateTitle, // generateTitle by vue-i18n
+ generateRoute() {
+ if (this.$route.name) {
+ return this.$route
+ }
+ return false
+ },
+ isActive(route) {
+ return route.path === this.$route.path
+ },
+ addViewTags() {
+ const route = this.generateRoute()
+ if (!route) {
+ return false
+ }
+ this.$store.dispatch('addVisitedViews', route)
+ },
+ 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.$el)
+ break
+ }
+ }
+ })
+ },
+ closeSelectedTag(view) {
+ this.$store.dispatch('delVisitedViews', view).then((views) => {
+ if (this.isActive(view)) {
+ const latestView = views.slice(-1)[0]
+ if (latestView) {
+ this.$router.push(latestView)
+ } else {
+ this.$router.push('/')
+ }
+ }
+ })
+ },
+ closeOthersTags() {
+ this.$router.push(this.selectedTag)
+ this.$store.dispatch('delOthersViews', this.selectedTag).then(() => {
+ this.moveToCurrentTag()
+ })
+ },
+ closeAllTags() {
+ this.$store.dispatch('delAllViews')
+ this.$router.push('/')
+ },
+ openMenu(tag, e) {
+ this.visible = true
+ this.selectedTag = tag
+ const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
+ this.left = e.clientX - offsetLeft + 15 // 15: margin right
+ this.top = e.clientY
+ },
+ closeMenu() {
+ this.visible = false
+ }
+ }
+}
+</script>
+
+<style rel="stylesheet/scss" lang="scss" scoped>
+.tags-view-container {
+ .tags-view-wrapper {
+ background: #fff;
+ height: 34px;
+ border-bottom: 1px solid #d8dce5;
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
+ .tags-view-item {
+ display: inline-block;
+ position: relative;
+ 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;
+ }
+ &.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: 100;
+ 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, .3);
+ li {
+ margin: 0;
+ padding: 7px 16px;
+ cursor: pointer;
+ &:hover {
+ background: #eee;
+ }
+ }
+ }
+}
+</style>
+
+<style rel="stylesheet/scss" 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 .3s cubic-bezier(.645, .045, .355, 1);
+ transform-origin: 100% 50%;
+ &:before {
+ transform: scale(.6);
+ display: inline-block;
+ vertical-align: -3px;
+ }
+ &:hover {
+ background-color: #b4bccc;
+ color: #fff;
+ }
+ }
+ }
+}
+</style>
diff --git a/leave-school-vue/src/views/layout/components/index.js b/leave-school-vue/src/views/layout/components/index.js
new file mode 100644
index 0000000..7cddb7c
--- /dev/null
+++ b/leave-school-vue/src/views/layout/components/index.js
@@ -0,0 +1,4 @@
+export { default as Navbar } from './Navbar'
+export { default as Sidebar } from './Sidebar'
+export { default as TagsView } from './TagsView'
+export { default as AppMain } from './AppMain'
diff --git a/leave-school-vue/src/views/layout/mixin/ResizeHandler.js b/leave-school-vue/src/views/layout/mixin/ResizeHandler.js
new file mode 100644
index 0000000..b22c8bb
--- /dev/null
+++ b/leave-school-vue/src/views/layout/mixin/ResizeHandler.js
@@ -0,0 +1,41 @@
+import store from '@/store'
+
+const { body } = document
+const WIDTH = 1024
+const RATIO = 3
+
+export default {
+ watch: {
+ $route(route) {
+ if (this.device === 'mobile' && this.sidebar.opened) {
+ store.dispatch('CloseSideBar', { withoutAnimation: false })
+ }
+ }
+ },
+ beforeMount() {
+ window.addEventListener('resize', this.resizeHandler)
+ },
+ mounted() {
+ const isMobile = this.isMobile()
+ if (isMobile) {
+ store.dispatch('ToggleDevice', 'mobile')
+ store.dispatch('CloseSideBar', { withoutAnimation: true })
+ }
+ },
+ methods: {
+ isMobile() {
+ const rect = body.getBoundingClientRect()
+ return rect.width - RATIO < WIDTH
+ },
+ resizeHandler() {
+ if (!document.hidden) {
+ const isMobile = this.isMobile()
+ store.dispatch('ToggleDevice', isMobile ? 'mobile' : 'desktop')
+
+ if (isMobile) {
+ store.dispatch('CloseSideBar', { withoutAnimation: true })
+ }
+ }
+ }
+ }
+}