mirror of
https://gitee.com/likeadmin/likeadmin_java.git
synced 2026-06-09 15:52:30 +08:00
新版底层提交
This commit is contained in:
22
admin/src/layout/components/footer.vue
Normal file
22
admin/src/layout/components/footer.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<footer class="layout-footer">
|
||||
<div class="text-center p-2 text-xs text-tx-secondary max-w-[900px] mx-auto">
|
||||
<a
|
||||
class="mx-1 hover:underline"
|
||||
:href="item.value"
|
||||
target="_blank"
|
||||
v-for="item in copyright"
|
||||
:key="item.key"
|
||||
>
|
||||
{{ item.key }}
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const copyright = computed(() => appStore.config.copyright_config || [])
|
||||
</script>
|
||||
@@ -1,85 +0,0 @@
|
||||
<template>
|
||||
<div class="layout-aside">
|
||||
<router-link to="/workbench" class="logo flex col-center">
|
||||
<div v-if="config.webLogo == ''">
|
||||
<img class="logo-img" src="@/assets/images/avatar.png" alt />
|
||||
</div>
|
||||
<div v-else>
|
||||
<img class="logo-img" :src="config.webLogo" alt />
|
||||
</div>
|
||||
<div class="line-1">{{ config.webName }}</div>
|
||||
</router-link>
|
||||
<div class="scrollbar-wrap">
|
||||
<el-scrollbar style="height: 100%" class="ls-scrollbar">
|
||||
<el-menu
|
||||
active-text-color="#fff"
|
||||
background-color="#2a2c41"
|
||||
:default-active="currentPath"
|
||||
text-color="#E5E5E5"
|
||||
>
|
||||
<template v-for="(item, index) in sidebar" :key="index">
|
||||
<sub-menu :route="item" :path="item.path" />
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import { useAdmin } from '@/core/hooks/app'
|
||||
import SubMenu from './sub-menu.vue'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
SubMenu
|
||||
},
|
||||
setup() {
|
||||
const { store, route } = useAdmin()
|
||||
const sidebar = computed(() => store.state.permission.sidebar)
|
||||
const currentPath = computed(() => route.meta?.activeMenu ?? route.path)
|
||||
const config = computed(() => store.getters.config)
|
||||
return {
|
||||
config,
|
||||
sidebar,
|
||||
currentPath
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-aside {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #2a2c41;
|
||||
.logo {
|
||||
height: $layout-header-height;
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
padding: 0 20px;
|
||||
.logo-img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.scrollbar-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
.el-menu {
|
||||
box-sizing: border-box;
|
||||
padding: 10px 0 20px;
|
||||
:deep(.el-menu-item) {
|
||||
&.is-active {
|
||||
background-color: $color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,84 +0,0 @@
|
||||
<template>
|
||||
<div v-if="!route.meta.hidden">
|
||||
<el-sub-menu v-if="hasChildren" :index="path">
|
||||
<template #title>
|
||||
<i class="iconfont m-r-10 icon-szie" :class="route.meta.icon"></i>
|
||||
<span>{{ route.meta.title }}</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<sub-menu
|
||||
v-for="(item, index) in route.children"
|
||||
:key="index"
|
||||
:route="item"
|
||||
:path="resolvePath(item.path)"
|
||||
/>
|
||||
</template>
|
||||
</el-sub-menu>
|
||||
<router-link
|
||||
v-else
|
||||
:to="{
|
||||
path: path,
|
||||
query: resolveQuery,
|
||||
}"
|
||||
>
|
||||
<el-menu-item :index="path">
|
||||
<i class="iconfont m-r-10" :class="route.meta.icon"></i>
|
||||
<span>{{ route.meta.title }}</span>
|
||||
</el-menu-item>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { queryToObject } from '@/utils/util'
|
||||
import { isQuery } from '@/utils/validate'
|
||||
import { computed, defineComponent, toRefs } from 'vue'
|
||||
import { RouteRecordRaw } from 'vue-router'
|
||||
export default defineComponent({
|
||||
components: {},
|
||||
props: {
|
||||
route: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { path, route } = toRefs(props)
|
||||
// 是否有子路由
|
||||
const hasChildren = computed(() => {
|
||||
const children: RouteRecordRaw[] = route.value.children ?? []
|
||||
return !!children.filter((item) => !item.meta?.hidden).length
|
||||
})
|
||||
// 解析路径,后台上传的并非完整路径
|
||||
const resolvePath = computed(() => (p?: string) => {
|
||||
return p !== undefined ? `${path.value}/${p}` : path.value
|
||||
})
|
||||
// 解析参数'{id:1}'|| id=1 => {id: 1}
|
||||
const resolveQuery = computed(() => {
|
||||
const query = route.value.query
|
||||
try {
|
||||
if (isQuery(query)) {
|
||||
return queryToObject(query)
|
||||
} else {
|
||||
return JSON.parse(query)
|
||||
}
|
||||
} catch (error) {}
|
||||
})
|
||||
|
||||
return {
|
||||
hasChildren,
|
||||
resolvePath,
|
||||
resolveQuery,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.iconfont {
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<div class="layout-header">
|
||||
<!-- <input class="search-input" placeholder="请输入搜索内容…" type="text" /> -->
|
||||
<div class="admin-info flex flex-center m-l-40">
|
||||
<div v-if="userInfo.avatar == ''">
|
||||
<img class="default-avatar" src="@/assets/images/avatar.png" alt />
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-avatar :size="40" :src="userInfo.avatar"></el-avatar>
|
||||
</div>
|
||||
|
||||
<div class="m-l-10">
|
||||
<el-dropdown trigger="hover" @command="handleCommand">
|
||||
<div class="flex flex-center">
|
||||
{{ userInfo.username }}
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<router-link to="/setting/personal_data">
|
||||
<el-dropdown-item>个人设置</el-dropdown-item>
|
||||
</router-link>
|
||||
</el-dropdown-menu>
|
||||
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import { useAdmin } from '@/core/hooks/app'
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { store, router } = useAdmin()
|
||||
const userInfo = computed(() => store.getters.userInfo)
|
||||
console.log('____userInfo____', userInfo)
|
||||
|
||||
const handleCommand = (command: string) => {
|
||||
switch (command) {
|
||||
case 'logout':
|
||||
store.dispatch('user/logout').then(() => {
|
||||
router.push('/login')
|
||||
store.commit('permission/setPermission', {
|
||||
auth: null,
|
||||
root: 0
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
userInfo,
|
||||
handleCommand
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex: none;
|
||||
height: $layout-header-height;
|
||||
background: #fff;
|
||||
padding: 0 24px;
|
||||
.search-input {
|
||||
width: 460px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
background: #f6f6f6;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.default-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,33 +0,0 @@
|
||||
<template>
|
||||
<div class="layout-main">
|
||||
<el-scrollbar>
|
||||
<div class="p-15">
|
||||
<router-view />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useAdmin } from '@/core/hooks/app'
|
||||
import { computed, defineComponent } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'layout-main',
|
||||
setup() {
|
||||
const { route } = useAdmin()
|
||||
const keepAlive = computed(() => {
|
||||
return route.meta.keepAlive
|
||||
})
|
||||
return {
|
||||
keepAlive
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-main {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<!-- <div v-if="permission" class="perm"> -->
|
||||
<div class="perm">
|
||||
<!-- <template v-if="hasPermission"> -->
|
||||
|
||||
<router-view></router-view>
|
||||
<!-- <template v-else>
|
||||
<div class="no-perm flex flex-col flex-center">
|
||||
<img src="@/assets/images/no_perm.png" />
|
||||
<div class="muted">暂无查看权限</div>
|
||||
</div>
|
||||
</template> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import { useAdmin } from '@/core/hooks/app'
|
||||
export default defineComponent({
|
||||
components: {},
|
||||
props: {},
|
||||
setup(props) {
|
||||
const { store, route } = useAdmin()
|
||||
const permission = computed(() => store.getters.permission)
|
||||
const isAdmin = computed(() => store.getters.isAdmin)
|
||||
const hasPermission = computed(() => {
|
||||
const { path, meta } = route
|
||||
if (isAdmin.value) {
|
||||
return true
|
||||
}
|
||||
const actions = permission.value[path]
|
||||
console.log(permission.value, path)
|
||||
if (!actions || !meta?.permission) {
|
||||
return true
|
||||
}
|
||||
return actions.some((item: string) => {
|
||||
return (meta?.permission as string[]).includes(item)
|
||||
})
|
||||
})
|
||||
return {
|
||||
permission,
|
||||
hasPermission
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.perm {
|
||||
.no-perm {
|
||||
height: calc(100vh - #{$layout-header-height} - 32px);
|
||||
img {
|
||||
width: 152px;
|
||||
height: 152px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
20
admin/src/layout/default/components/header/breadcrumb.vue
Normal file
20
admin/src/layout/default/components/header/breadcrumb.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<el-breadcrumb class="app-breadcrumb">
|
||||
<el-breadcrumb-item v-for="item in breadcrumbs" :key="item.path">
|
||||
{{ item.meta.title }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useWatchRoute } from '@/hooks/useWatchRoute'
|
||||
import type { RouteLocationMatched, RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
const breadcrumbs = ref<RouteLocationMatched[]>([])
|
||||
const getBreadcrumb = (route: RouteLocationNormalizedLoaded) => {
|
||||
const matched = route.matched.filter((item) => item.meta && item.meta.title)
|
||||
breadcrumbs.value = matched
|
||||
}
|
||||
|
||||
useWatchRoute((route) => {
|
||||
getBreadcrumb(route)
|
||||
})
|
||||
</script>
|
||||
15
admin/src/layout/default/components/header/fold.vue
Normal file
15
admin/src/layout/default/components/header/fold.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="fold h-full cursor-pointer flex items-center px-2" @click="toggleCollapsed">
|
||||
<icon :name="`local-icon-${isCollapsed ? 'close' : 'open'}`" :size="20" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
const appStore = useAppStore()
|
||||
const isCollapsed = computed(() => appStore.isCollapsed)
|
||||
// 折叠展开菜单
|
||||
const toggleCollapsed = () => {
|
||||
appStore.toggleCollapsed()
|
||||
}
|
||||
</script>
|
||||
10
admin/src/layout/default/components/header/full-screen.vue
Normal file
10
admin/src/layout/default/components/header/full-screen.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="full-screen h-full cursor-pointer flex items-center px-2" @click="toggleFullscreen">
|
||||
<icon :size="16" :name="`local-icon-${isFullscreen ? 'fullscreen-exit' : 'fullscreen'}`" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
const { toggle: toggleFullscreen, isFullscreen } = useFullscreen()
|
||||
</script>
|
||||
53
admin/src/layout/default/components/header/index.vue
Normal file
53
admin/src/layout/default/components/header/index.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<header class="header">
|
||||
<div class="navbar">
|
||||
<div class="flex-1 flex">
|
||||
<div class="navbar-item">
|
||||
<fold />
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
<refresh />
|
||||
</div>
|
||||
<div class="flex items-center px-2" v-if="!isMobile">
|
||||
<breadcrumb />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="navbar-item" v-if="!isMobile">
|
||||
<full-screen />
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
<user-drop-down />
|
||||
</div>
|
||||
<div class="navbar-item">
|
||||
<setting />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<multiple-tabs />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import Fold from './fold.vue'
|
||||
import Refresh from './refresh.vue'
|
||||
import Breadcrumb from './breadcrumb.vue'
|
||||
import FullScreen from './full-screen.vue'
|
||||
import UserDropDown from './user-drop-down.vue'
|
||||
import Setting from '../setting/index.vue'
|
||||
import MultipleTabs from './multiple-tabs.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const isMobile = computed(() => appStore.isMobile)
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.navbar {
|
||||
height: var(--navbar-height);
|
||||
@apply flex px-2 bg-body;
|
||||
.navbar-item {
|
||||
@apply h-full flex justify-center items-center hover:bg-page;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
131
admin/src/layout/default/components/header/multiple-tabs.vue
Normal file
131
admin/src/layout/default/components/header/multiple-tabs.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="app-tabs pl-4 flex bg-body">
|
||||
<div class="flex-1 min-w-0">
|
||||
<el-tabs
|
||||
:model-value="currentTab"
|
||||
:closable="tabsState.length > 1"
|
||||
@tab-change="handleChange"
|
||||
@tab-remove="handleRemove"
|
||||
>
|
||||
<template v-for="item in tabsState" :key="item.path">
|
||||
<el-tab-pane :label="item.title" :name="item.path"></el-tab-pane>
|
||||
</template>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<el-dropdown @command="handleCommand">
|
||||
<span class="flex items-center px-3">
|
||||
<icon :size="16" name="el-icon-arrow-down" />
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="closeCurrent"> 关闭当前 </el-dropdown-item>
|
||||
<el-dropdown-item command="closeOther"> 关闭其他 </el-dropdown-item>
|
||||
<el-dropdown-item command="closeAll"> 关闭全部 </el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWatchRoute } from '@/hooks/useWatchRoute'
|
||||
import useTabsStore, { getRouteParams } from '@/stores/modules/multipleTabs'
|
||||
const router = useRouter()
|
||||
const tabsStore = useTabsStore()
|
||||
const { route } = useWatchRoute((route) => {
|
||||
tabsStore.addTab(route)
|
||||
})
|
||||
|
||||
const currentTab = computed(() => {
|
||||
return route.path
|
||||
})
|
||||
|
||||
const tabsState = computed(() => {
|
||||
return tabsStore.getTabList
|
||||
})
|
||||
|
||||
const handleChange = (path: any) => {
|
||||
const tabItem = tabsStore.tasMap[path]
|
||||
router.push(getRouteParams(tabItem))
|
||||
}
|
||||
|
||||
const handleRemove = (path: any) => {
|
||||
tabsStore.removeTab(path, router)
|
||||
}
|
||||
const handleCommand = (command: any) => {
|
||||
switch (command) {
|
||||
case 'closeCurrent':
|
||||
handleRemove(route.path)
|
||||
break
|
||||
case 'closeOther':
|
||||
tabsStore.removeOtherTab(route.path)
|
||||
break
|
||||
case 'closeAll':
|
||||
tabsStore.removeAllTab(router)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.app-tabs {
|
||||
@apply border-t border-br;
|
||||
:deep(.el-tabs) {
|
||||
height: 40px;
|
||||
.el-tabs {
|
||||
&__header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&__content {
|
||||
display: none;
|
||||
}
|
||||
&__nav-next,
|
||||
&__nav-prev {
|
||||
@apply text-xl;
|
||||
}
|
||||
|
||||
&__nav-wrap::after {
|
||||
height: 0;
|
||||
}
|
||||
&__item {
|
||||
font-weight: normal;
|
||||
padding: 0 15px !important;
|
||||
box-sizing: border-box;
|
||||
&.is-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: var(--el-color-primary);
|
||||
margin-right: 6px;
|
||||
border-radius: 50%;
|
||||
vertical-align: 2px;
|
||||
}
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
display: block;
|
||||
top: 0;
|
||||
height: 2px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.is-icon-close {
|
||||
color: var(--el-text-color-regular);
|
||||
vertical-align: -2px;
|
||||
&:hover {
|
||||
color: var(--color-white);
|
||||
background-color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
&__active-bar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
14
admin/src/layout/default/components/header/refresh.vue
Normal file
14
admin/src/layout/default/components/header/refresh.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div class="refresh cursor-pointer h-full flex items-center px-2" @click="refreshView">
|
||||
<icon name="el-icon-RefreshRight" :size="18" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
const appStore = useAppStore()
|
||||
// 刷新页面
|
||||
const refreshView = () => {
|
||||
appStore.refreshView()
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<el-dropdown class="px-2" @command="handleCommand">
|
||||
<div class="flex items-center">
|
||||
<el-avatar :size="34" :src="userInfo.avatar" />
|
||||
<div class="ml-3 mr-1">{{ userInfo.nickname }}</div>
|
||||
<icon name="el-icon-ArrowDown" />
|
||||
</div>
|
||||
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<router-link to="/user/setting">
|
||||
<el-dropdown-item>个人设置</el-dropdown-item>
|
||||
</router-link>
|
||||
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import feedback from '@/utils/feedback'
|
||||
const userStore = useUserStore()
|
||||
|
||||
const userInfo = computed(() => userStore.userInfo)
|
||||
|
||||
const handleCommand = async (command: string) => {
|
||||
switch (command) {
|
||||
case 'logout':
|
||||
await feedback.confirm('确定退出登录吗?')
|
||||
userStore.logout()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
19
admin/src/layout/default/components/main.vue
Normal file
19
admin/src/layout/default/components/main.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<main class="main-wrap h-full bg-page">
|
||||
<el-scrollbar>
|
||||
<div class="p-4">
|
||||
<router-view v-if="isRouteShow" v-slot="{ Component, route }">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
const appStore = useAppStore()
|
||||
const isRouteShow = computed(() => appStore.isRouteShow)
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
123
admin/src/layout/default/components/setting/drawer.vue
Normal file
123
admin/src/layout/default/components/setting/drawer.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div class="setting-drawer">
|
||||
<el-drawer
|
||||
v-model="showSetting"
|
||||
append-to-body
|
||||
direction="rtl"
|
||||
size="250px"
|
||||
title="主题设置"
|
||||
>
|
||||
<div class="setting-item mb-5">
|
||||
<span class="text-tx-secondary">风格设置</span>
|
||||
<div class="flex mt-4 cursor-pointer">
|
||||
<div
|
||||
class="mr-4 flex relative text-primary"
|
||||
v-for="item in sideThemeList"
|
||||
:key="item.type"
|
||||
@click="sideTheme = item.type"
|
||||
>
|
||||
<img :src="item.image" width="52" height="36" />
|
||||
<icon
|
||||
v-if="sideTheme == item.type"
|
||||
class="icon-select"
|
||||
name="el-icon-Select"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item mb-5 flex justify-between items-center">
|
||||
<span class="text-tx-secondary">主题颜色</span>
|
||||
<div>
|
||||
<el-color-picker v-model="theme" :predefine="predefineColors" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item mb-5 flex justify-between items-center">
|
||||
<span class="text-tx-secondary">开启黑暗模式</span>
|
||||
<div>
|
||||
<el-switch :model-value="isDark" @change="toggleDark" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item mb-5 flex justify-between items-center">
|
||||
<el-button @click="resetTheme">重置主题</el-button>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useSettingStore from '@/stores/modules/setting'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import theme_light from '@/assets/images/theme_white.png'
|
||||
import theme_dark from '@/assets/images/theme_black.png'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const predefineColors = ref(['#409EFF', '#28C76F', '#EA5455', '#FF9F43', '#01CFE8', '#4A5DFF'])
|
||||
const sideThemeList = [
|
||||
{
|
||||
type: 'dark',
|
||||
image: theme_dark
|
||||
},
|
||||
{
|
||||
type: 'light',
|
||||
image: theme_light
|
||||
}
|
||||
]
|
||||
|
||||
const sideTheme = computed({
|
||||
get() {
|
||||
return settingStore.sideTheme
|
||||
},
|
||||
set(value) {
|
||||
settingStore.setSetting({
|
||||
key: 'sideTheme',
|
||||
value
|
||||
})
|
||||
}
|
||||
})
|
||||
const showSetting = computed({
|
||||
get() {
|
||||
return settingStore.showDrawer
|
||||
},
|
||||
set(value) {
|
||||
settingStore.setSetting({
|
||||
key: 'showDrawer',
|
||||
value
|
||||
})
|
||||
}
|
||||
})
|
||||
const theme = computed({
|
||||
get() {
|
||||
return settingStore.theme
|
||||
},
|
||||
set(value) {
|
||||
settingStore.setSetting({
|
||||
key: 'theme',
|
||||
value
|
||||
})
|
||||
themeChange()
|
||||
}
|
||||
})
|
||||
|
||||
const isDark = useDark()
|
||||
const themeChange = () => {
|
||||
settingStore.setTheme(isDark.value)
|
||||
}
|
||||
|
||||
const toggleDark = () => {
|
||||
useToggle(isDark)()
|
||||
themeChange()
|
||||
}
|
||||
const resetTheme = () => {
|
||||
isDark.value = false
|
||||
settingStore.resetTheme()
|
||||
themeChange()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icon-select {
|
||||
@apply absolute left-1/2 top-1/2;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
19
admin/src/layout/default/components/setting/index.vue
Normal file
19
admin/src/layout/default/components/setting/index.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="setting flex cursor-pointer h-full items-center px-2" @click="openSetting">
|
||||
<icon :size="16" name="el-icon-Setting" />
|
||||
<layout-setting />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useSettingStore from '@/stores/modules/setting'
|
||||
import LayoutSetting from './drawer.vue'
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const openSetting = () => {
|
||||
settingStore.setSetting({
|
||||
key: 'showDrawer',
|
||||
value: true
|
||||
})
|
||||
}
|
||||
</script>
|
||||
44
admin/src/layout/default/components/sidebar/index.vue
Normal file
44
admin/src/layout/default/components/sidebar/index.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<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 Side from './side.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useSettingStore from '@/stores/modules/setting'
|
||||
|
||||
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>
|
||||
55
admin/src/layout/default/components/sidebar/logo.vue
Normal file
55
admin/src/layout/default/components/sidebar/logo.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="logo">
|
||||
<ImageContain :width="szie" :height="szie" :src="config.webLogo" />
|
||||
<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` }"
|
||||
>
|
||||
{{ title || config.webName }}
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import { ThemeEnum } from '@/enums/appEnums'
|
||||
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)
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.logo {
|
||||
height: var(--navbar-height);
|
||||
overflow: hidden;
|
||||
@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>
|
||||
87
admin/src/layout/default/components/sidebar/menu-item.vue
Normal file
87
admin/src/layout/default/components/sidebar/menu-item.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<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 { getNormalPath, objectToQuery } from '@/utils/util'
|
||||
import { isExternal } from '@/utils/validate'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
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>
|
||||
88
admin/src/layout/default/components/sidebar/menu.vue
Normal file
88
admin/src/layout/default/components/sidebar/menu.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="menu flex-1 min-h-0" :class="themeClass">
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
v-bind="config"
|
||||
:default-active="activeMenu"
|
||||
:collapse="isCollapsed"
|
||||
mode="vertical"
|
||||
@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 MenuItem from './menu-item.vue'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
routes: {
|
||||
type: Object as PropType<RouteRecordRaw[]>
|
||||
},
|
||||
config: {
|
||||
type: Object
|
||||
},
|
||||
isCollapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
theme: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['select'])
|
||||
|
||||
const route = useRoute()
|
||||
const activeMenu = computed<string>(() => (route.meta?.activeMenu as string) ?? 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>
|
||||
60
admin/src/layout/default/components/sidebar/side.vue
Normal file
60
admin/src/layout/default/components/sidebar/side.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="side" :style="sideStyle">
|
||||
<side-logo :show-title="!isCollapsed" :theme="sideTheme" />
|
||||
<side-menu
|
||||
:routes="routes"
|
||||
:isCollapsed="isCollapsed"
|
||||
: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>
|
||||
22
admin/src/layout/default/index.vue
Normal file
22
admin/src/layout/default/index.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="layout-default flex h-screen w-full">
|
||||
<div class="app-aside">
|
||||
<layout-sidebar />
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<div class="app-header">
|
||||
<layout-header />
|
||||
</div>
|
||||
<div class="app-main flex-1 min-h-0">
|
||||
<layout-main />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LayoutMain from './components/main.vue'
|
||||
import LayoutSidebar from './components/sidebar/index.vue'
|
||||
import LayoutHeader from './components/header/index.vue'
|
||||
</script>
|
||||
@@ -1,44 +0,0 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="aside">
|
||||
<layout-aside />
|
||||
</div>
|
||||
<div class="main">
|
||||
<layout-header />
|
||||
<layout-main />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import LayoutAside from './components/layout-aside/index.vue'
|
||||
import LayoutMain from './components/layout-main.vue'
|
||||
import LayoutHeader from './components/layout-header.vue'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
LayoutAside,
|
||||
LayoutMain,
|
||||
LayoutHeader
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
min-width: $layout-min-width;
|
||||
.aside {
|
||||
flex: none;
|
||||
width: $layout-aside-width;
|
||||
}
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user