新增
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<aside class="sidebar h-full">
|
||||
<el-drawer
|
||||
v-model="showMenuDrawer"
|
||||
direction="ltr"
|
||||
:size="drawderSize"
|
||||
title="主题设置"
|
||||
:with-header="false"
|
||||
>
|
||||
<side />
|
||||
</el-drawer>
|
||||
<side v-show="!isMobile" />
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useSettingStore from '@/stores/modules/setting'
|
||||
|
||||
import Side from './side.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const settingStore = useSettingStore()
|
||||
const isMobile = computed(() => appStore.isMobile)
|
||||
const showMenuDrawer = computed({
|
||||
get() {
|
||||
return !appStore.isCollapsed && isMobile.value
|
||||
},
|
||||
set(value) {
|
||||
appStore.toggleCollapsed(!value)
|
||||
}
|
||||
})
|
||||
|
||||
const drawderSize = computed(() => {
|
||||
return `${settingStore.sideWidth + 1}px`
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar {
|
||||
:deep(.el-drawer__body) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="logo" @click="handleClick">
|
||||
<image-contain :width="szie" :height="szie" :src="config.web_logo" />
|
||||
<transition name="title-width">
|
||||
<div
|
||||
v-show="showTitle"
|
||||
class="logo-title overflow-hidden whitespace-nowrap"
|
||||
:class="{ 'text-white': theme == ThemeEnum.DARK }"
|
||||
:style="{ left: `${szie + 16}px` }"
|
||||
>
|
||||
<overflow-tooltip
|
||||
:content="title || config.web_name"
|
||||
:teleported="true"
|
||||
placement="bottom"
|
||||
overflo-type="unset"
|
||||
>
|
||||
</overflow-tooltip>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ThemeEnum } from '@/enums/appEnums'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
|
||||
defineProps({
|
||||
szie: { type: Number, default: 34 },
|
||||
title: { type: String },
|
||||
theme: { type: String },
|
||||
showTitle: { type: Boolean, default: true }
|
||||
})
|
||||
const appStore = useAppStore()
|
||||
const config = computed(() => appStore.config)
|
||||
const router = useRouter()
|
||||
|
||||
const handleClick = () => {
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.logo {
|
||||
height: var(--navbar-height);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
@apply flex items-center p-2 relative;
|
||||
.logo-title {
|
||||
width: 70%;
|
||||
position: absolute;
|
||||
@apply text-xl;
|
||||
}
|
||||
|
||||
.title-width-enter-active {
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.title-width-leave-active {
|
||||
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
|
||||
}
|
||||
|
||||
.title-width-enter-from,
|
||||
.title-width-leave-to {
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<template v-if="!route.meta?.hidden">
|
||||
<app-link v-if="!hasShowChild" :to="`${routePath}?${queryStr}`">
|
||||
<el-menu-item :index="routePath">
|
||||
<icon
|
||||
class="menu-item-icon"
|
||||
:size="16"
|
||||
v-if="routeMeta?.icon"
|
||||
:name="routeMeta?.icon"
|
||||
/>
|
||||
<template #title>
|
||||
<span>{{ routeMeta?.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</app-link>
|
||||
<el-sub-menu v-else :index="routePath" :popper-class="popperClass">
|
||||
<template #title>
|
||||
<icon
|
||||
class="menu-item-icon"
|
||||
:size="16"
|
||||
v-if="routeMeta?.icon"
|
||||
:name="routeMeta?.icon"
|
||||
/>
|
||||
<span>{{ routeMeta?.title }}</span>
|
||||
</template>
|
||||
<menu-item
|
||||
v-for="item in route?.children"
|
||||
:key="resolvePath(item.path)"
|
||||
:route="item"
|
||||
:route-path="resolvePath(item.path)"
|
||||
:popper-class="popperClass"
|
||||
/>
|
||||
</el-sub-menu>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
import { getNormalPath, objectToQuery } from '@/utils/util'
|
||||
import { isExternal } from '@/utils/validate'
|
||||
|
||||
interface Props {
|
||||
route: RouteRecordRaw
|
||||
routePath: string
|
||||
popperClass: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const hasShowChild = computed(() => {
|
||||
const children: RouteRecordRaw[] = props.route.children ?? []
|
||||
return !!children.filter((item) => !item.meta?.hidden).length
|
||||
})
|
||||
|
||||
const routeMeta = computed(() => {
|
||||
return props.route.meta
|
||||
})
|
||||
|
||||
const resolvePath = (path: string) => {
|
||||
if (isExternal(path)) {
|
||||
return path
|
||||
}
|
||||
const newPath = getNormalPath(`${props.routePath}/${path}`)
|
||||
return newPath
|
||||
}
|
||||
const queryStr = computed<string>(() => {
|
||||
const query = props.route.meta?.query as string
|
||||
try {
|
||||
const queryObj = JSON.parse(query)
|
||||
return objectToQuery(queryObj)
|
||||
} catch (error) {
|
||||
// console.log(error)
|
||||
|
||||
return query
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
.menu-item-icon {
|
||||
margin-right: 8px;
|
||||
width: var(--el-menu-icon-width);
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div
|
||||
class="menu flex-1 min-h-0"
|
||||
:class="themeClass"
|
||||
:style="isCollapsed ? '' : `--aside-width: ${width}px`"
|
||||
>
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
v-bind="config"
|
||||
:default-active="activeMenu"
|
||||
:collapse="isCollapsed"
|
||||
mode="vertical"
|
||||
:unique-opened="uniqueOpened"
|
||||
@select="$emit('select')"
|
||||
>
|
||||
<menu-item
|
||||
v-for="route in routes"
|
||||
:key="route.path"
|
||||
:route="route"
|
||||
:route-path="route.path"
|
||||
:popper-class="themeClass"
|
||||
/>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
import MenuItem from './menu-item.vue'
|
||||
|
||||
const props = defineProps({
|
||||
routes: {
|
||||
type: Object as PropType<RouteRecordRaw[]>
|
||||
},
|
||||
config: {
|
||||
type: Object
|
||||
},
|
||||
uniqueOpened: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isCollapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
theme: {
|
||||
type: String
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 200
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['select'])
|
||||
|
||||
const route = useRoute()
|
||||
const activeMenu = computed<string>(() => route.meta?.activeMenu || route.path)
|
||||
const themeClass = computed(() => `theme-${props.theme}`)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu {
|
||||
&.theme-dark {
|
||||
.el-menu {
|
||||
:deep(.el-menu-item) {
|
||||
&.is-active {
|
||||
@apply bg-primary border-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.el-menu--collapse) {
|
||||
.el-sub-menu.is-active .el-sub-menu__title {
|
||||
@apply bg-primary #{!important};
|
||||
}
|
||||
}
|
||||
}
|
||||
&.theme-light {
|
||||
:deep(.el-menu) {
|
||||
.el-menu-item {
|
||||
border-color: transparent;
|
||||
&.is-active {
|
||||
@apply bg-primary-light-9 border-r-2 border-primary;
|
||||
}
|
||||
}
|
||||
.el-menu-item:hover,
|
||||
.el-sub-menu__title:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
&:not(.el-menu--collapse) {
|
||||
width: var(--aside-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="side" :style="sideStyle">
|
||||
<side-logo v-if="settingStore.showLogo" :show-title="!isCollapsed" :theme="sideTheme" />
|
||||
<side-menu
|
||||
:routes="routes"
|
||||
:is-collapsed="isCollapsed"
|
||||
:width="settingStore.sideWidth"
|
||||
:unique-opened="settingStore.isUniqueOpened"
|
||||
:config="menuProp"
|
||||
:theme="sideTheme"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useSettingStore from '@/stores/modules/setting'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
|
||||
import SideLogo from './logo.vue'
|
||||
import SideMenu from './menu.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const isCollapsed = computed(() => {
|
||||
if (appStore.isMobile) {
|
||||
return false
|
||||
} else {
|
||||
return appStore.isCollapsed
|
||||
}
|
||||
})
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const sideTheme = computed(() => settingStore.sideTheme)
|
||||
const userStore = useUserStore()
|
||||
|
||||
const routes = computed(() => userStore.routes)
|
||||
|
||||
const sideStyle = computed(() => {
|
||||
return sideTheme.value == 'dark'
|
||||
? {
|
||||
'--side-dark-color': settingStore.sideDarkColor
|
||||
}
|
||||
: ''
|
||||
})
|
||||
const menuProp = computed(() => {
|
||||
return {
|
||||
backgroundColor: sideTheme.value == 'dark' ? settingStore.sideDarkColor : '',
|
||||
textColor: sideTheme.value == 'dark' ? 'var(--el-color-white)' : '',
|
||||
activeTextColor: sideTheme.value == 'dark' ? 'var(--el-color-white)' : ''
|
||||
}
|
||||
})
|
||||
const handleSelect = () => {
|
||||
if (appStore.isMobile) {
|
||||
appStore.toggleCollapsed(true)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.side {
|
||||
position: relative;
|
||||
z-index: 999;
|
||||
@apply border-r border-br-light h-full flex flex-col;
|
||||
background-color: var(--side-dark-color, var(--el-bg-color));
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user