.
This commit is contained in:
@@ -33,7 +33,7 @@ npm run dev
|
|||||||
npm install vue-router@next
|
npm install vue-router@next
|
||||||
```
|
```
|
||||||
|
|
||||||
src 下创建 router/index.ts
|
src 下创建 router/interface.ts
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import {createRouter, createWebHashHistory, RouteRecordRaw} from 'vue-router'
|
import {createRouter, createWebHashHistory, RouteRecordRaw} from 'vue-router'
|
||||||
@@ -71,7 +71,7 @@ export default router
|
|||||||
npm install vuex@next
|
npm install vuex@next
|
||||||
```
|
```
|
||||||
|
|
||||||
src 下创建 store/index.ts
|
src 下创建 store/interface.ts
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import {InjectionKey} from 'vue'
|
import {InjectionKey} from 'vue'
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"element-plus": "^1.2.0-beta.3",
|
"element-plus": "^1.2.0-beta.3",
|
||||||
|
"path-to-regexp": "^6.2.0",
|
||||||
"vue": "^3.2.16",
|
"vue": "^3.2.16",
|
||||||
"vue-router": "^4.0.12",
|
"vue-router": "^4.0.12",
|
||||||
"vuex": "^4.0.2"
|
"vuex": "^4.0.2"
|
||||||
|
|||||||
103
src/components/Breadcrumb/index.vue
Normal file
103
src/components/Breadcrumb/index.vue
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<el-breadcrumb class="app-breadcrumb" separator="/">
|
||||||
|
<transition-group name="breadcrumb">
|
||||||
|
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
|
||||||
|
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1"
|
||||||
|
class="no-redirect">
|
||||||
|
{{ item.meta.title }}
|
||||||
|
</span>
|
||||||
|
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
|
||||||
|
</el-breadcrumb-item>
|
||||||
|
</transition-group>
|
||||||
|
</el-breadcrumb>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import {defineComponent, onBeforeMount, reactive, toRefs} from "vue";
|
||||||
|
import {compile} from 'path-to-regexp'
|
||||||
|
import {useRoute, RouteLocationMatched} from "vue-router";
|
||||||
|
import router from "@router";
|
||||||
|
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const currentRoute = useRoute();
|
||||||
|
const pathCompile = (path: string) => {
|
||||||
|
const {params} = currentRoute
|
||||||
|
const toPath = compile(path)
|
||||||
|
return toPath(params)
|
||||||
|
}
|
||||||
|
const state = reactive({
|
||||||
|
breadcrumbs: [] as Array<RouteLocationMatched>,
|
||||||
|
getBreadcrumb:()=>{
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
levelList: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route() {
|
||||||
|
this.getBreadcrumb()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getBreadcrumb()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getBreadcrumb() {
|
||||||
|
// only show routes with meta.title
|
||||||
|
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
|
||||||
|
const first = matched[0]
|
||||||
|
|
||||||
|
if (!this.isDashboard(first)) {
|
||||||
|
matched = [{path: '/dashboard', meta: {title: 'Dashboard'}}].concat(matched)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
|
||||||
|
},
|
||||||
|
isDashboard(route) {
|
||||||
|
const name = route && route.name
|
||||||
|
if (!name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
|
||||||
|
},
|
||||||
|
pathCompile(path) {
|
||||||
|
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
|
||||||
|
const {params} = this.$route
|
||||||
|
var toPath = pathToRegexp.compile(path)
|
||||||
|
return toPath(params)
|
||||||
|
},
|
||||||
|
handleLink(item) {
|
||||||
|
const {redirect, path} = item
|
||||||
|
if (redirect) {
|
||||||
|
this.$router.push(redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$router.push(this.pathCompile(path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.app-breadcrumb.el-breadcrumb {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 50px;
|
||||||
|
margin-left: 8px;
|
||||||
|
|
||||||
|
.no-redirect {
|
||||||
|
color: #97a8be;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
44
src/components/Hamburger/index.vue
Normal file
44
src/components/Hamburger/index.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<div style="padding: 0 15px;" @click="toggleClick">
|
||||||
|
<svg
|
||||||
|
:class="{'is-active':isActive}"
|
||||||
|
class="hamburger"
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
>
|
||||||
|
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Hamburger',
|
||||||
|
props: {
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleClick() {
|
||||||
|
this.$emit('toggleClick')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hamburger {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger.is-active {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-input v-model="num"/>
|
|
||||||
<el-button @click="handleClick">点击+1</el-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import {defineComponent, computed} from 'vue'
|
|
||||||
import {userStore} from '@/store';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
setup() {
|
|
||||||
const store = userStore()
|
|
||||||
const num = computed(()=>{
|
|
||||||
return store.state.count
|
|
||||||
})
|
|
||||||
const handleClick = () => {
|
|
||||||
store.commit('increment')
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
num,
|
|
||||||
handleClick
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
62
src/components/SvgIcon/index.vue
Normal file
62
src/components/SvgIcon/index.vue
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
|
||||||
|
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
|
||||||
|
<use :xlink:href="iconName" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
|
||||||
|
import { isExternal } from '@/utils/validate'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SvgIcon',
|
||||||
|
props: {
|
||||||
|
iconClass: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isExternal() {
|
||||||
|
return isExternal(this.iconClass)
|
||||||
|
},
|
||||||
|
iconName() {
|
||||||
|
return `#icon-${this.iconClass}`
|
||||||
|
},
|
||||||
|
svgClass() {
|
||||||
|
if (this.className) {
|
||||||
|
return 'svg-icon ' + this.className
|
||||||
|
} else {
|
||||||
|
return 'svg-icon'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
styleExternalIcon() {
|
||||||
|
return {
|
||||||
|
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
|
||||||
|
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.svg-icon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
vertical-align: -0.15em;
|
||||||
|
fill: currentColor;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg-external-icon {
|
||||||
|
background-color: currentColor;
|
||||||
|
mask-size: cover!important;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
40
src/layout/components/AppMain.vue
Normal file
40
src/layout/components/AppMain.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<section class="app-main">
|
||||||
|
<transition name="fade-transform" mode="out-in">
|
||||||
|
<router-view :key="key" />
|
||||||
|
</transition>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'AppMain',
|
||||||
|
computed: {
|
||||||
|
key() {
|
||||||
|
return this.$route.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-main {
|
||||||
|
/*50 = navbar */
|
||||||
|
min-height: calc(100vh - 50px);
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.fixed-header+.app-main {
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
// fix css style bug in open el-dialog
|
||||||
|
.el-popup-parent--hidden {
|
||||||
|
.fixed-header {
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
139
src/layout/components/Navbar.vue
Normal file
139
src/layout/components/Navbar.vue
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<template>
|
||||||
|
<div class="navbar">
|
||||||
|
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
|
||||||
|
|
||||||
|
<breadcrumb class="breadcrumb-container" />
|
||||||
|
|
||||||
|
<div class="right-menu">
|
||||||
|
<el-dropdown class="avatar-container" trigger="click">
|
||||||
|
<div class="avatar-wrapper">
|
||||||
|
<img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar">
|
||||||
|
<i class="el-icon-caret-bottom" />
|
||||||
|
</div>
|
||||||
|
<el-dropdown-menu slot="dropdown" class="user-dropdown">
|
||||||
|
<router-link to="/">
|
||||||
|
<el-dropdown-item>
|
||||||
|
Home
|
||||||
|
</el-dropdown-item>
|
||||||
|
</router-link>
|
||||||
|
<a target="_blank" href="https://github.com/PanJiaChen/vue-admin-template/">
|
||||||
|
<el-dropdown-item>Github</el-dropdown-item>
|
||||||
|
</a>
|
||||||
|
<a target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/#/">
|
||||||
|
<el-dropdown-item>Docs</el-dropdown-item>
|
||||||
|
</a>
|
||||||
|
<el-dropdown-item divided @click.native="logout">
|
||||||
|
<span style="display:block;">Log Out</span>
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import Breadcrumb from '@/components/Breadcrumb/index.vue'
|
||||||
|
import Hamburger from '@/components/Hamburger/index.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Breadcrumb,
|
||||||
|
Hamburger
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters([
|
||||||
|
'sidebar',
|
||||||
|
'avatar'
|
||||||
|
])
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleSideBar() {
|
||||||
|
this.$store.dispatch('app/toggleSideBar')
|
||||||
|
},
|
||||||
|
async logout() {
|
||||||
|
await this.$store.dispatch('user/logout')
|
||||||
|
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.navbar {
|
||||||
|
height: 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,21,41,.08);
|
||||||
|
|
||||||
|
.hamburger-container {
|
||||||
|
line-height: 46px;
|
||||||
|
height: 100%;
|
||||||
|
float: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background .3s;
|
||||||
|
-webkit-tap-highlight-color:transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, .025)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-container {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 .3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(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>
|
||||||
43
src/layout/components/Sidebar/Link.vue
Normal file
43
src/layout/components/Sidebar/Link.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<component :is="type" v-bind="linkProps(to)">
|
||||||
|
<slot />
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { isExternal } from '@/utils/validate'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
to: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isExternal() {
|
||||||
|
return isExternal(this.to)
|
||||||
|
},
|
||||||
|
type() {
|
||||||
|
if (this.isExternal) {
|
||||||
|
return 'a'
|
||||||
|
}
|
||||||
|
return 'router-link'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
linkProps(to) {
|
||||||
|
if (this.isExternal) {
|
||||||
|
return {
|
||||||
|
href: to,
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noopener'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
to: to
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
82
src/layout/components/Sidebar/Logo.vue
Normal file
82
src/layout/components/Sidebar/Logo.vue
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
|
||||||
|
<transition name="sidebarLogoFade">
|
||||||
|
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
|
||||||
|
<img v-if="logo" :src="logo" class="sidebar-logo">
|
||||||
|
<h1 v-else class="sidebar-title">{{ title }} </h1>
|
||||||
|
</router-link>
|
||||||
|
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
||||||
|
<img v-if="logo" :src="logo" class="sidebar-logo">
|
||||||
|
<h1 class="sidebar-title">{{ title }} </h1>
|
||||||
|
</router-link>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'SidebarLogo',
|
||||||
|
props: {
|
||||||
|
collapse: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
title: 'Vue Admin Template',
|
||||||
|
logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sidebarLogoFade-enter-active {
|
||||||
|
transition: opacity 1.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarLogoFade-enter,
|
||||||
|
.sidebarLogoFade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-logo-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
background: #2b2f3a;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
& .sidebar-logo-link {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
& .sidebar-logo {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .sidebar-title {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 50px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.collapse {
|
||||||
|
.sidebar-logo {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
103
src/layout/components/Sidebar/SidebarItem.vue
Normal file
103
src/layout/components/Sidebar/SidebarItem.vue
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="!item.hidden">
|
||||||
|
<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 #title>
|
||||||
|
<svg
|
||||||
|
v-if="item.meta && item.meta.icon"
|
||||||
|
class="icon"
|
||||||
|
aria-hidden="true"
|
||||||
|
font-size="16px"
|
||||||
|
>
|
||||||
|
<use :xlink:href="item.meta.icon" />
|
||||||
|
</svg>
|
||||||
|
<span v-if="item.meta && item.meta.title">{{
|
||||||
|
t("route." + item.meta.title)
|
||||||
|
}}</span>
|
||||||
|
</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 AppLink from './Link.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SidebarItem',
|
||||||
|
components: { AppLink },
|
||||||
|
props: {
|
||||||
|
// route object
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isNest: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
basePath: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
|
||||||
|
// TODO: refactor with render function
|
||||||
|
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>
|
||||||
56
src/layout/components/Sidebar/index.vue
Normal file
56
src/layout/components/Sidebar/index.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="{'has-logo':showLogo}">
|
||||||
|
<logo v-if="showLogo" :collapse="isCollapse" />
|
||||||
|
<el-scrollbar wrap-class="scrollbar-wrapper">
|
||||||
|
<el-menu
|
||||||
|
:default-active="activeMenu"
|
||||||
|
:collapse="isCollapse"
|
||||||
|
:background-color="variables.menuBg"
|
||||||
|
:text-color="variables.menuText"
|
||||||
|
:unique-opened="false"
|
||||||
|
:active-text-color="variables.menuActiveText"
|
||||||
|
:collapse-transition="false"
|
||||||
|
mode="vertical"
|
||||||
|
>
|
||||||
|
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
|
||||||
|
</el-menu>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import Logo from './Logo.vue'
|
||||||
|
import SidebarItem from './SidebarItem.vue'
|
||||||
|
import variables from '@styles/variables.scss'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { SidebarItem, Logo },
|
||||||
|
computed: {
|
||||||
|
...mapGetters([
|
||||||
|
'sidebar'
|
||||||
|
]),
|
||||||
|
routes() {
|
||||||
|
return this.$router.options.routes
|
||||||
|
},
|
||||||
|
activeMenu() {
|
||||||
|
const route = this.$route
|
||||||
|
const { meta, path } = route
|
||||||
|
// if set path, the sidebar will highlight the path you set
|
||||||
|
if (meta.activeMenu) {
|
||||||
|
return meta.activeMenu
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
},
|
||||||
|
showLogo() {
|
||||||
|
return this.$store.state.settings.sidebarLogo
|
||||||
|
},
|
||||||
|
variables() {
|
||||||
|
return variables
|
||||||
|
},
|
||||||
|
isCollapse() {
|
||||||
|
return !this.sidebar.opened
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
3
src/layout/components/index.ts
Normal file
3
src/layout/components/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as Navbar } from './Navbar.vue'
|
||||||
|
export { default as Sidebar } from './Sidebar/index.vue'
|
||||||
|
export { default as AppMain } from './AppMain.vue'
|
||||||
@@ -1,57 +1,93 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container>
|
<div :class="classObj" class="app-wrapper">
|
||||||
<el-header>Header</el-header>
|
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||||
<el-container>
|
<sidebar class="sidebar-container" />
|
||||||
<el-aside width="200px">Aside</el-aside>
|
<div class="main-container">
|
||||||
<el-container>
|
<div :class="{'fixed-header':fixedHeader}">
|
||||||
<el-main>
|
<navbar />
|
||||||
<router-view/>
|
</div>
|
||||||
</el-main>
|
<app-main />
|
||||||
<el-footer>Footer</el-footer>
|
</div>
|
||||||
</el-container>
|
</div>
|
||||||
</el-container>
|
|
||||||
</el-container>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { Navbar, Sidebar, AppMain } from './components'
|
||||||
|
import ResizeMixin from './mixin/ResizeHandler'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "index"
|
name: 'Layout',
|
||||||
|
components: {
|
||||||
|
Navbar,
|
||||||
|
Sidebar,
|
||||||
|
AppMain
|
||||||
|
},
|
||||||
|
mixins: [ResizeMixin],
|
||||||
|
computed: {
|
||||||
|
sidebar() {
|
||||||
|
return this.$store.state.app.sidebar
|
||||||
|
},
|
||||||
|
device() {
|
||||||
|
return this.$store.state.app.device
|
||||||
|
},
|
||||||
|
fixedHeader() {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
classObj() {
|
||||||
|
return {
|
||||||
|
hideSidebar: !this.sidebar.opened,
|
||||||
|
openSidebar: this.sidebar.opened,
|
||||||
|
withoutAnimation: this.sidebar.withoutAnimation,
|
||||||
|
mobile: this.device === 'mobile'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClickOutside() {
|
||||||
|
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss" scoped>
|
||||||
.el-header,
|
@import "@styles/mixin.scss";
|
||||||
.el-footer {
|
@import "@styles/variables.scss";
|
||||||
background-color: #b3c0d1;
|
|
||||||
color: var(--el-text-color-primary);
|
.app-wrapper {
|
||||||
text-align: center;
|
@include clearfix;
|
||||||
line-height: 60px;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-aside {
|
.fixed-header {
|
||||||
background-color: #d3dce6;
|
position: fixed;
|
||||||
color: var(--el-text-color-primary);
|
top: 0;
|
||||||
text-align: center;
|
right: 0;
|
||||||
line-height: 200px;
|
z-index: 9;
|
||||||
|
width: calc(100% - #{$sideBarWidth});
|
||||||
|
transition: width 0.28s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-main {
|
.hideSidebar .fixed-header {
|
||||||
background-color: #e9eef3;
|
width: calc(100% - 54px)
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
text-align: center;
|
|
||||||
line-height: 160px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body > .el-container {
|
.mobile .fixed-header {
|
||||||
margin-bottom: 40px;
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
.el-container:nth-child(5) .el-aside,
|
|
||||||
.el-container:nth-child(6) .el-aside {
|
|
||||||
line-height: 260px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-container:nth-child(7) .el-aside {
|
|
||||||
line-height: 320px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
63
src/layout/mixin/ResizeHandler.ts
Normal file
63
src/layout/mixin/ResizeHandler.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import {userStore} from '@/store'
|
||||||
|
import {computed, watch} from "vue";
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
const store = userStore()
|
||||||
|
|
||||||
|
const {body} = document
|
||||||
|
const WIDTH = 992 // refer to Bootstrap's responsive design
|
||||||
|
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const device = computed(() => {
|
||||||
|
return store.state.app.device
|
||||||
|
})
|
||||||
|
|
||||||
|
const sidebar = computed(() => {
|
||||||
|
return store.state.app.sidebar
|
||||||
|
})
|
||||||
|
|
||||||
|
const isMobile = () => {
|
||||||
|
const rect = body.getBoundingClientRect()
|
||||||
|
return rect.width - 1 < WIDTH
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeHandler = () => {
|
||||||
|
if (!document.hidden) {
|
||||||
|
store.dispatch('app/toggleDevice', isMobile() ? 'mobile' : 'desktop')
|
||||||
|
if (isMobile()) {
|
||||||
|
store.dispatch('app/closeSideBar', {withoutAnimation: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeMounted = () => {
|
||||||
|
if(isMobile()){
|
||||||
|
store.dispatch('app/toggleDevice', 'mobile')
|
||||||
|
store.dispatch('app/closeSideBar', {withoutAnimation: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const addEventListenerOnResize = () => {
|
||||||
|
window.addEventListener('resize', resizeHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeEventListenerResize = () => {
|
||||||
|
window.removeEventListener('resize', resizeHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const currentRoute = useRoute()
|
||||||
|
const watchRouter = watch(() => currentRoute.name, () => {
|
||||||
|
if (store.state.app.device === 'mobile' && store.state.app.sidebar.opened) {
|
||||||
|
store.dispatch('app/closeSideBar', false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
device,
|
||||||
|
sidebar,
|
||||||
|
resizeMounted,
|
||||||
|
addEventListenerOnResize,
|
||||||
|
removeEventListenerResize,
|
||||||
|
watchRouter
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,25 @@
|
|||||||
import {InjectionKey} from 'vue'
|
import {InjectionKey} from 'vue'
|
||||||
import {createStore,useStore as baseUseStore ,Store} from 'vuex'
|
import {createStore,useStore as baseUseStore ,Store} from 'vuex'
|
||||||
|
import {RootStateTypes} from "@store/interface";
|
||||||
|
|
||||||
export interface State {
|
// Vite 使用特殊的 import.meta.glob 函数从文件系统导入多个模块
|
||||||
count: number
|
// see https://cn.vitejs.dev/guide/features.html#glob-import
|
||||||
|
const moduleFiles = import.meta.globEager('./modules/*.ts')
|
||||||
|
const paths:string[]=[]
|
||||||
|
|
||||||
|
for (const path in moduleFiles) {
|
||||||
|
paths.push(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const key: InjectionKey<Store<State>> = Symbol()
|
const modules = paths.reduce((modules: { [x: string]: any }, modulePath: string) => {
|
||||||
|
const moduleKey = modulePath.replace(/^\.\/modules\/(.*)\.\w+$/, '$1');
|
||||||
|
modules[moduleKey] = moduleFiles[modulePath].default;
|
||||||
|
return modules;
|
||||||
|
}, {});
|
||||||
|
|
||||||
export const store = createStore<State>({
|
export const key: InjectionKey<Store<RootStateTypes>> = Symbol()
|
||||||
state() {
|
|
||||||
return {
|
export const store = createStore<RootStateTypes>({modules})
|
||||||
count: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
increment(state: { count: number }) {
|
|
||||||
state.count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export function userStore(){
|
export function userStore(){
|
||||||
return baseUseStore(key)
|
return baseUseStore(key)
|
||||||
|
|||||||
@@ -8,7 +8,16 @@ export interface UserState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface AppState {
|
||||||
|
device: string,
|
||||||
|
sidebar: {
|
||||||
|
opened: boolean,
|
||||||
|
withoutAnimation: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 顶级类型声明
|
// 顶级类型声明
|
||||||
export interface RootStateTypes {
|
export interface RootStateTypes {
|
||||||
user: UserState
|
user: UserState,
|
||||||
|
app:AppState
|
||||||
}
|
}
|
||||||
47
src/store/modules/app.ts
Normal file
47
src/store/modules/app.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import {Module} from "vuex";
|
||||||
|
import {RootStateTypes, AppState} from "@store/interface";
|
||||||
|
import {Local} from "@utils/storage";
|
||||||
|
|
||||||
|
const appModule: Module<AppState, RootStateTypes> = {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
device: 'desktop',
|
||||||
|
sidebar: {
|
||||||
|
opened: Local.get('sidebarStatus') ? !!+Local.get('sidebarStatus') : true,
|
||||||
|
withoutAnimation: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
TOGGLE_SIDEBAR: state => {
|
||||||
|
state.sidebar.opened = !state.sidebar.opened
|
||||||
|
state.sidebar.withoutAnimation = false
|
||||||
|
if (state.sidebar.opened) {
|
||||||
|
Local.set('sidebarStatus', 1)
|
||||||
|
} else {
|
||||||
|
Local.set('sidebarStatus', 0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CLOSE_SIDEBAR: (state, withoutAnimation) => {
|
||||||
|
Local.set('sidebarStatus', 0)
|
||||||
|
state.sidebar.opened = false
|
||||||
|
state.sidebar.withoutAnimation = withoutAnimation
|
||||||
|
},
|
||||||
|
TOGGLE_DEVICE: (state, device) => {
|
||||||
|
state.device = device
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
toggleSideBar({commit}) {
|
||||||
|
commit('TOGGLE_SIDEBAR')
|
||||||
|
},
|
||||||
|
closeSideBar({commit}, {withoutAnimation}) {
|
||||||
|
commit('CLOSE_SIDEBAR', withoutAnimation)
|
||||||
|
},
|
||||||
|
toggleDevice({commit}, device) {
|
||||||
|
commit('TOGGLE_DEVICE', device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default appModule;
|
||||||
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import {Module} from "vuex";
|
import {Module} from "vuex";
|
||||||
import {UserState, RootStateTypes} from "@store/interface";
|
import {UserState, RootStateTypes} from "@store/interface";
|
||||||
import {Local} from "@utils/storage";
|
import {Local} from "@utils/storage";
|
||||||
import {getUserInfo, login,logout} from "@api/login"
|
import {getUserInfo, login, logout} from "@api/login"
|
||||||
|
|
||||||
|
|
||||||
const getDefaultState = () => {
|
const getDefaultState = () => {
|
||||||
return {
|
return {
|
||||||
@@ -14,7 +13,6 @@ const getDefaultState = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const userModule: Module<UserState, RootStateTypes> = {
|
const userModule: Module<UserState, RootStateTypes> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
@@ -104,12 +102,12 @@ const userModule: Module<UserState, RootStateTypes> = {
|
|||||||
/**
|
/**
|
||||||
* 注销
|
* 注销
|
||||||
*/
|
*/
|
||||||
logout({commit,state}){
|
logout({commit, state}) {
|
||||||
return new Promise(((resolve, reject) => {
|
return new Promise(((resolve, reject) => {
|
||||||
logout().then(()=>{
|
logout().then(() => {
|
||||||
Local.remove('token')
|
Local.remove('token')
|
||||||
commit('RESET_STATE')
|
commit('RESET_STATE')
|
||||||
}).catch(error=>{
|
}).catch(error => {
|
||||||
reject(error)
|
reject(error)
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|||||||
12
src/utils/validate.ts
Normal file
12
src/utils/validate.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Created by PanJiaChen on 16/11/18.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
export function isExternal(path : string) {
|
||||||
|
return /^(https?:|mailto:|tel:)/.test(path)
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user