first commit
This commit is contained in:
9
src/layout/components/AppMain.vue
Normal file
9
src/layout/components/AppMain.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<router-view>
|
||||
<template #default="{ Component, route }">
|
||||
<transition name="fade-slide" mode="out-in" appear>
|
||||
<component :is="Component" :key="route.fullPath"></component>
|
||||
</transition>
|
||||
</template>
|
||||
</router-view>
|
||||
</template>
|
19
src/layout/components/header/BreadCrumb.vue
Normal file
19
src/layout/components/header/BreadCrumb.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
import { NBreadcrumb, NBreadcrumbItem } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
const { currentRoute } = router
|
||||
|
||||
function handleBreadClick(path) {
|
||||
if (path === currentRoute.value.path) return
|
||||
router.push(path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-breadcrumb>
|
||||
<n-breadcrumb-item v-for="item in currentRoute.matched" :key="item.path" @click="handleBreadClick(item.path)">
|
||||
{{ item.meta.title }}
|
||||
</n-breadcrumb-item>
|
||||
</n-breadcrumb>
|
||||
</template>
|
47
src/layout/components/header/HeaderAction.vue
Normal file
47
src/layout/components/header/HeaderAction.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup>
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NDropdown } from 'naive-ui'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '退出登录',
|
||||
key: 'logout',
|
||||
},
|
||||
]
|
||||
|
||||
function handleSelect(key) {
|
||||
if (key === 'logout') {
|
||||
userStore.logout()
|
||||
$message.success('已退出登录')
|
||||
router.push({ path: '/login' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-dropdown :options="options" @select="handleSelect">
|
||||
<div class="avatar">
|
||||
<img :src="userStore.avatar" />
|
||||
<span>{{ userStore.name }}</span>
|
||||
</div>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
img {
|
||||
width: 100%;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
21
src/layout/components/header/index.vue
Normal file
21
src/layout/components/header/index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import BreadCrumb from './BreadCrumb.vue'
|
||||
import HeaderAction from './HeaderAction.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="header">
|
||||
<bread-crumb />
|
||||
<header-action />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
padding: 0 35px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
32
src/layout/components/sidebar/SideLogo.vue
Normal file
32
src/layout/components/sidebar/SideLogo.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup>
|
||||
import { NGradientText, NIcon } from 'naive-ui'
|
||||
import { LastfmSquare } from '@vicons/fa'
|
||||
const title = import.meta.env.VITE_APP_TITLE
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="logo">
|
||||
<n-icon size="36" color="#316c72">
|
||||
<lastfm-square />
|
||||
</n-icon>
|
||||
<router-link to="/">
|
||||
<n-gradient-text type="primary">{{ title }}</n-gradient-text>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.logo {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
a {
|
||||
margin-left: 5px;
|
||||
.n-gradient-text {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
160
src/layout/components/sidebar/SideMenu.vue
Normal file
160
src/layout/components/sidebar/SideMenu.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<script setup>
|
||||
import { NMenu } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
import { usePermissionStore } from '@/store/modules/permission'
|
||||
|
||||
const router = useRouter()
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const { currentRoute } = router
|
||||
const routes = permissionStore.routes
|
||||
|
||||
const menuOptions = computed(() => {
|
||||
return generateOptions(routes, '')
|
||||
})
|
||||
|
||||
function resolvePath(...pathes) {
|
||||
return (
|
||||
'/' +
|
||||
pathes
|
||||
.filter((path) => !!path && path !== '/')
|
||||
.map((path) => path.replace(/(^\/)|(\/$)/g, ''))
|
||||
.join('/')
|
||||
)
|
||||
}
|
||||
|
||||
function generateOptions(routes, basePath) {
|
||||
let options = []
|
||||
routes.forEach((route) => {
|
||||
if (route.name && !route.isHidden) {
|
||||
let curOption = {
|
||||
label: (route.meta && route.meta.title) || route.name,
|
||||
key: route.name,
|
||||
path: resolvePath(basePath, route.path),
|
||||
}
|
||||
if (route.children && route.children.length) {
|
||||
curOption.children = generateOptions(route.children, resolvePath(basePath, route.path))
|
||||
}
|
||||
if (curOption.children && curOption.children.length <= 1) {
|
||||
if (curOption.children.length === 1) {
|
||||
curOption = { ...curOption.children[0] }
|
||||
}
|
||||
delete curOption.children
|
||||
}
|
||||
options.push(curOption)
|
||||
}
|
||||
})
|
||||
return options
|
||||
}
|
||||
|
||||
function handleMenuSelect(key, item) {
|
||||
router.push(item.path)
|
||||
|
||||
// 通过path重定向
|
||||
// router.push({
|
||||
// path: '/redirect',
|
||||
// query: { redirect: item.path },
|
||||
// })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-menu
|
||||
class="side-menu"
|
||||
:root-indent="20"
|
||||
:options="menuOptions"
|
||||
:value="(currentRoute.meta && currentRoute.meta.activeMenu) || currentRoute.name"
|
||||
@update:value="handleMenuSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.n-menu {
|
||||
margin-top: 10px;
|
||||
padding-left: 10px;
|
||||
.n-menu-item {
|
||||
margin-top: 0;
|
||||
position: relative;
|
||||
&::before {
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 0;
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.n-menu-item--selected {
|
||||
border-radius: 0 !important;
|
||||
|
||||
&::before {
|
||||
border-right: 3px solid $primaryColor;
|
||||
background-color: #16243a;
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba($primaryColor, 0.3) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.n-menu-item-content-header {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.n-submenu-children {
|
||||
.n-menu-item-content-header {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
position: relative;
|
||||
overflow: visible !important;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -15px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// .side-menu {
|
||||
// // padding-left: 15px;
|
||||
// .n-menu-item-content-header {
|
||||
// color: #fff !important;
|
||||
// font-weight: bold;
|
||||
// font-size: 14px;
|
||||
// }
|
||||
|
||||
// .n-submenu {
|
||||
// .n-menu-item-content-header {
|
||||
// color: #fff !important;
|
||||
// font-weight: bold;
|
||||
// font-size: 14px;
|
||||
// }
|
||||
// }
|
||||
// .n-submenu-children {
|
||||
// .n-menu-item-content-header {
|
||||
// color: #fff !important;
|
||||
// font-weight: normal;
|
||||
// font-size: 12px;
|
||||
// }
|
||||
// }
|
||||
// .n-menu-item {
|
||||
// border-top-left-radius: 5px;
|
||||
// border-bottom-left-radius: 5px;
|
||||
// &:hover,
|
||||
// &.n-menu-item--selected::before {
|
||||
// background-color: #16243a;
|
||||
// right: 0;
|
||||
// left: 0;
|
||||
// border-right: 3px solid $primaryColor;
|
||||
// background-color: unset !important;
|
||||
// background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba($primaryColor, 0.3) 100%);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
</style>
|
9
src/layout/components/sidebar/index.vue
Normal file
9
src/layout/components/sidebar/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
import SideLogo from './SideLogo.vue'
|
||||
import SideMenu from './SideMenu.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<side-logo />
|
||||
<side-menu />
|
||||
</template>
|
30
src/layout/index.vue
Normal file
30
src/layout/index.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
import { NLayout, NLayoutHeader, NLayoutSider } from 'naive-ui'
|
||||
|
||||
import AppHeader from './components/header/index.vue'
|
||||
import SideMenu from './components/sidebar/index.vue'
|
||||
import AppMain from './components/AppMain.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<n-layout has-sider position="absolute">
|
||||
<n-layout-sider :width="200" :collapsed-width="0" :native-scrollbar="false">
|
||||
<side-menu />
|
||||
</n-layout-sider>
|
||||
<n-layout>
|
||||
<n-layout-header style="height: 100px; background-color: #f5f6fb">
|
||||
<app-header />
|
||||
</n-layout-header>
|
||||
<n-layout
|
||||
position="absolute"
|
||||
content-style="padding: 0 35px 35px"
|
||||
style="top: 100px; background-color: #f5f6fb"
|
||||
:native-scrollbar="false"
|
||||
>
|
||||
<app-main />
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</div>
|
||||
</template>
|
Reference in New Issue
Block a user