feat: update the layout mode

This commit is contained in:
chansee97 2025-08-01 10:52:29 +08:00
parent fcd28fc9d5
commit 4e08b796c0
16 changed files with 232 additions and 400 deletions

View File

@ -31,7 +31,7 @@
- Secondary encapsulation of commonly used components to meet basic work requirements.
- Dark theme adaptation, maintaining the Naive style for interface aesthetics.
- Only performs eslint validation during submission without excessive restrictions for simpler development.
- Flexible and configurable interface style layout.
- Flexible and configurable interface layout based on [pro-naive-ui](https://github.com/Zheng-Changfu/pro-naive-ui)
- Multilanguage (i18n) support.
## Project preview

View File

@ -31,7 +31,7 @@
- 对日常使用频率较高的组件二次封装,满足基础工作需求
- 黑暗主题适配, 界面样式保持Naive风格
- 仅在提交时进行eslint校验没有过多限制开发更简便
- 界面样式布局灵活可配置
- 基于[pro-naive-ui](https://github.com/Zheng-Changfu/pro-naive-ui)的界面布局灵活可配置
- 多语言i18n支持
## 项目预览

View File

@ -59,9 +59,12 @@
"backHome": "Back to the homepage",
"getRouteError": "Failed to obtain route, please try again later.",
"layoutSetting": "Layout settings",
"leftMenu": "Left menu",
"topMenu": "Top menu",
"mixMenu": "Mix menu"
"verticalLayout": "Vertical layout",
"horizontalLayout": "Horizontal layout",
"twoColumnLayout": "Two column layout",
"mixedTwoColumnLayout": "Mixed two column layout",
"sidebarLayout": "Sidebar layout",
"mixedSidebarLayout": "Mixed sidebar layout"
},
"login": {
"signInTitle": "Login",

View File

@ -59,9 +59,12 @@
"backHome": "回到首页",
"getRouteError": "获取路由失败,请稍后再试",
"layoutSetting": "布局设置",
"leftMenu": "左侧菜单",
"topMenu": "顶部菜单",
"mixMenu": "混合菜单"
"verticalLayout": "竖向布局",
"horizontalLayout": "横向布局",
"twoColumnLayout": "双栏布局",
"mixedTwoColumnLayout": "混合双栏布局",
"sidebarLayout": "侧边栏布局",
"mixedSidebarLayout": "双栏布局"
},
"http": {
"400": "请求出现语法错误",

View File

@ -57,6 +57,7 @@
"md-editor-v3": "^5.6.1",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.3.0",
"pro-naive-ui": "^2.4.3",
"quill": "^2.0.3",
"radash": "^12.1.0",
"vue": "^3.5.16",

22
src/layouts/Content.vue Normal file
View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import { useAppStore, useRouteStore } from '@/store'
const appStore = useAppStore()
const routeStore = useRouteStore()
</script>
<template>
<n-el>
<router-view
v-slot="{ Component, route }"
class="flex-1 p-16px"
style="background-color: var(--action-color);"
>
<transition :name="appStore.transitionAnimation" mode="out-in">
<keep-alive :include="routeStore.cacheRoutes">
<component :is="Component" v-if="appStore.loadFlag" :key="route.fullPath" />
</keep-alive>
</transition>
</router-view>
</n-el>
</template>

View File

@ -1,63 +1,120 @@
<script setup lang="ts">
import type { LayoutMode } from '@/store/app'
import type { ProLayoutMode } from 'pro-naive-ui'
const value = defineModel<LayoutMode>('value', { required: true })
const value = defineModel<ProLayoutMode>('value', { required: true })
</script>
<template>
<div class="flex-center gap-4">
<n-tooltip placement="bottom" trigger="hover">
<div class="selector-wapper gap-4">
<n-tooltip placement="top" trigger="hover">
<template #trigger>
<n-el
:class="{
'outline outline-2': value === 'leftMenu',
'outline outline-2': value === 'vertical',
}"
class="grid grid-cols-[20%_1fr] outline-[var(--primary-color)] hover:(outline outline-2) cursor-pointer"
@click="value = 'leftMenu'"
@click="value = 'vertical'"
>
<div class="bg-[var(--primary-color)]" />
<div class="bg-[var(--divider-color)]" />
</n-el>
</template>
<span> {{ $t('app.leftMenu') }} </span>
<span> {{ $t('app.verticalLayout') }} </span>
</n-tooltip>
<n-tooltip placement="bottom" trigger="hover">
<n-tooltip placement="top" trigger="hover">
<template #trigger>
<n-el
:class="{
'outline outline-2': value === 'topMenu',
'outline outline-2': value === 'horizontal',
}"
class="grid grid-rows-[30%_1fr] outline-[var(--primary-color)] hover:(outline outline-2) cursor-pointer"
@click="value = 'topMenu'"
@click="value = 'horizontal'"
>
<div class="bg-[var(--primary-color)]" />
<div class="bg-[var(--divider-color)]" />
</n-el>
</template>
<span> {{ $t('app.topMenu') }} </span>
<span> {{ $t('app.horizontalLayout') }} </span>
</n-tooltip>
<n-tooltip placement="top" trigger="hover">
<template #trigger>
<n-el
:class="{
'outline outline-2': value === 'two-column',
}"
class="grid grid-cols-[10%_15%_1fr] outline-[var(--primary-color)] hover:(outline outline-2) cursor-pointer"
@click="value = 'two-column'"
>
<div class="bg-[var(--primary-color)]" />
<div class="bg-[var(--primary-color-suppl)]" />
<div class="bg-[var(--divider-color)]" />
</n-el>
</template>
<span> {{ $t('app.twoColumnLayout') }} </span>
</n-tooltip>
<n-tooltip placement="bottom" trigger="hover">
<template #trigger>
<n-el
:class="{
'outline outline-2': value === 'mixMenu',
'outline outline-2': value === 'mixed-two-column',
}"
class="grid grid-cols-[20%_1fr] grid-rows-[20%_1fr] outline-[var(--primary-color)] hover:(outline outline-2) cursor-pointer"
@click="value = 'mixMenu'"
class="grid grid-cols-[10%_15%_1fr] grid-rows-[20%_1fr] outline-[var(--primary-color)] hover:(outline outline-2) cursor-pointer"
@click="value = 'mixed-two-column'"
>
<div class="bg-[var(--primary-color)] row-span-2" />
<div class="bg-[var(--primary-color-suppl)] row-span-2" />
<div class="bg-[var(--primary-color-suppl)] row-span-2" />
<div class="bg-[var(--primary-color)]" />
<div class="bg-[var(--divider-color)]" />
</n-el>
</template>
<span> {{ $t('app.mixMenu') }} </span>
<span> {{ $t('app.mixedTwoColumnLayout') }} </span>
</n-tooltip>
<n-tooltip placement="bottom" trigger="hover">
<template #trigger>
<n-el
:class="{
'outline outline-2': value === 'sidebar',
}"
class="grid grid-cols-[20%_1fr] grid-rows-[20%_1fr] outline-[var(--primary-color)] hover:(outline outline-2) cursor-pointer"
@click="value = 'sidebar'"
>
<div class="bg-[var(--divider-color)] col-span-2" />
<div class="bg-[var(--primary-color)]" />
<div class="bg-[var(--divider-color)]" />
</n-el>
</template>
<span> {{ $t('app.sidebarLayout') }} </span>
</n-tooltip>
<n-tooltip placement="bottom" trigger="hover">
<template #trigger>
<n-el
:class="{
'outline outline-2': value === 'mixed-sidebar',
}"
class="grid grid-cols-[20%_1fr] grid-rows-[20%_1fr] outline-[var(--primary-color)] hover:(outline outline-2) cursor-pointer"
@click="value = 'mixed-sidebar'"
>
<div class="bg-[var(--primary-color)] col-span-2" />
<div class="bg-[var(--primary-color-suppl)]" />
<div class="bg-[var(--divider-color)]" />
</n-el>
</template>
<span> {{ $t('app.mixedSidebarLayout') }} </span>
</n-tooltip>
</div>
</template>
<style lang="scss" scoped>
.selector-wapper{
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.grid{
height: 60px;
width: 86px;

View File

@ -5,6 +5,16 @@ const router = useRouter()
const appStore = useAppStore()
const name = import.meta.env.VITE_APP_NAME
const hidenLogoText = computed(() => {
if (['sidebar', 'mixed-sidebar', 'horizontal'].includes(appStore.layoutMode)) {
return false
}
if (['two-column', 'mixed-two-column'].includes(appStore.layoutMode)) {
return true
}
return appStore.collapsed
})
</script>
<template>
@ -14,7 +24,7 @@ const name = import.meta.env.VITE_APP_NAME
>
<svg-icons-logo class="text-1.5em" />
<span
v-show="!appStore.collapsed"
v-show="!hidenLogoText"
class="text-ellipsis overflow-hidden whitespace-nowrap"
>{{ name }}</span>
</div>

View File

@ -1,6 +1,7 @@
import BackTop from './common/BackTop.vue'
import Setting from './common/Setting.vue'
import SettingDrawer from './common/SettingDrawer.vue'
import Logo from './common/Logo.vue'
import Breadcrumb from './header/Breadcrumb.vue'
import CollapaseButton from './header/CollapaseButton.vue'
@ -9,9 +10,6 @@ import Notices from './header/Notices.vue'
import Search from './header/Search.vue'
import UserCenter from './header/UserCenter.vue'
import Logo from './sider/Logo.vue'
import Menu from './sider/Menu.vue'
import TabBar from './tab/TabBar.vue'
export {
@ -20,7 +18,6 @@ export {
CollapaseButton,
FullScreen,
Logo,
Menu,
Notices,
Search,
Setting,

View File

@ -1,28 +0,0 @@
<script setup lang="ts">
import type { MenuInst } from 'naive-ui'
import { useAppStore, useRouteStore } from '@/store'
const route = useRoute()
const appStore = useAppStore()
const routeStore = useRouteStore()
const menuInstRef = ref<MenuInst | null>(null)
watch(
() => route.path,
() => {
menuInstRef.value?.showOption(routeStore.activeMenu as string)
},
{ immediate: true },
)
</script>
<template>
<n-menu
ref="menuInstRef"
:collapsed="appStore.collapsed"
:indent="20"
:collapsed-width="64"
:options="routeStore.menus"
:value="routeStore.activeMenu"
/>
</template>

View File

@ -114,7 +114,7 @@ useDraggable(el, tabs, {
</script>
<template>
<n-scrollbar ref="scrollbar" class="relative flex h-full tab-bar-scroller-wrapper" content-class="pr-34 tab-bar-scroller-content" :x-scrollable="true" @wheel="onWheel">
<n-scrollbar ref="scrollbar" class="relative flex h-full tab-bar-scroller-wrapper" content-class="h-full pr-34 tab-bar-scroller-content" :x-scrollable="true" @wheel="onWheel">
<div class="p-l-2 flex wh-full relative">
<div class="flex items-end">
<TabBarItem

View File

@ -1,19 +1,115 @@
<script setup lang="ts">
import { useAppStore } from '@/store/app'
import { SettingDrawer } from './components'
import leftMenu from './leftMenu.layout.vue'
import mixMenu from './mixMenu.layout.vue'
import topMenu from './topMenu.layout.vue'
import { useAppStore, useRouteStore } from '@/store'
import {
BackTop,
Breadcrumb,
CollapaseButton,
FullScreen,
Logo,
Notices,
Search,
Setting,
SettingDrawer,
TabBar,
UserCenter,
} from './components'
import Content from './Content.vue'
import { ProLayout, useLayoutMenu } from 'pro-naive-ui'
const route = useRoute()
const appStore = useAppStore()
const layoutMap = {
leftMenu,
topMenu,
mixMenu,
}
const routeStore = useRouteStore()
const { layoutMode } = storeToRefs(useAppStore())
const {
layout,
activeKey,
} = useLayoutMenu({
mode: layoutMode,
accordion: true,
menus: routeStore.menus,
})
watch(() => route.path, (value) => {
activeKey.value = value
}, { immediate: true })
const sidebarWidth = ref(240)
const sidebarCollapsedWidth = ref(64)
const hasHorizontalMenu = computed(() => ['horizontal', 'mixed-two-column', 'mixed-sidebar'].includes(layoutMode.value))
const hidenCollapaseButton = computed(() => ['horizontal'].includes(layoutMode.value))
</script>
<template>
<SettingDrawer />
<component :is="layoutMap[appStore.layoutMode]" />
<ProLayout
v-model:collapsed="appStore.collapsed"
:mode="layoutMode"
:show-logo="appStore.showLogo"
:show-footer="appStore.showFooter"
:show-tabbar="appStore.showTabs"
nav-fixed
show-nav
show-sidebar
:nav-height="60"
:tabbar-height="45"
:footer-height="40"
:sidebar-width="sidebarWidth"
:sidebar-collapsed-width="sidebarCollapsedWidth"
>
<template #logo>
<Logo />
</template>
<template #nav-left>
<div v-if="!hasHorizontalMenu || !hidenCollapaseButton" class="h-full flex-y-center gap-1 p-x-sm">
<CollapaseButton v-if="!hidenCollapaseButton" />
<Breadcrumb v-if="!hasHorizontalMenu" />
</div>
</template>
<template #nav-center>
<div class="h-full flex-y-center gap-1">
<n-menu v-if="hasHorizontalMenu" v-bind="layout.horizontalMenuProps" />
</div>
</template>
<template #nav-right>
<div class="h-full flex-y-center gap-1 p-x-xl">
<Search />
<Notices />
<FullScreen />
<DarkModeSwitch />
<LangsSwitch />
<Setting />
<UserCenter />
</div>
</template>
<template #sidebar>
<n-menu v-bind="layout.verticalMenuProps" :collapsed-width="sidebarCollapsedWidth" />
</template>
<template #sidebar-extra>
<n-scrollbar class="flex-[1_0_0]">
<n-menu v-bind="layout.verticalExtraMenuProps" :collapsed-width="sidebarCollapsedWidth" />
</n-scrollbar>
</template>
<template #tabbar>
<TabBar />
</template>
<template #footer>
<div class="flex-center h-full">
{{ appStore.footerText }}
</div>
</template>
<Content />
<BackTop />
<SettingDrawer />
</ProLayout>
</template>

View File

@ -1,102 +0,0 @@
<script lang="ts" setup>
import { useAppStore, useRouteStore } from '@/store'
import {
BackTop,
Breadcrumb,
CollapaseButton,
FullScreen,
Logo,
Menu,
Notices,
Search,
Setting,
TabBar,
UserCenter,
} from './components'
const routeStore = useRouteStore()
const appStore = useAppStore()
</script>
<template>
<n-layout
has-sider
class="wh-full"
embedded
>
<n-layout-sider
v-if="!appStore.contentFullScreen"
bordered
:collapsed="appStore.collapsed"
collapse-mode="width"
:collapsed-width="64"
:width="240"
content-style="display: flex;flex-direction: column;min-height:100%;"
>
<Logo v-if="appStore.showLogo" />
<n-scrollbar class="flex-1">
<Menu />
</n-scrollbar>
</n-layout-sider>
<n-layout
class="h-full flex flex-col"
content-style="display: flex;flex-direction: column;min-height:100%;"
embedded
:native-scrollbar="false"
>
<n-layout-header bordered position="absolute" class="z-999">
<div v-if="!appStore.contentFullScreen" class="h-60px flex-y-center justify-between">
<div class="flex-y-center h-full">
<CollapaseButton />
<Breadcrumb />
</div>
<div class="flex-y-center gap-1 h-full p-x-xl">
<Search />
<Notices />
<FullScreen />
<DarkModeSwitch />
<LangsSwitch />
<Setting />
<UserCenter />
</div>
</div>
<TabBar v-if="appStore.showTabs" class="h-45px" />
</n-layout-header>
<!-- 121 = 16 + 45 + 60 45是面包屑高度 60是标签栏高度 -->
<!-- 56 = 16 + 40 40是页脚高度 -->
<div
class="flex-1 p-16px flex flex-col"
:class="{
'p-t-121px': appStore.showTabs,
'p-b-56px': appStore.showFooter && !appStore.contentFullScreen,
'p-t-76px': !appStore.showTabs,
'p-t-61px': appStore.contentFullScreen,
}"
>
<router-view v-slot="{ Component, route }" class="flex-1">
<transition
:name="appStore.transitionAnimation"
mode="out-in"
>
<keep-alive :include="routeStore.cacheRoutes">
<component
:is="Component"
v-if="appStore.loadFlag"
:key="route.fullPath"
/>
</keep-alive>
</transition>
</router-view>
</div>
<n-layout-footer
v-if="appStore.showFooter && !appStore.contentFullScreen"
bordered
position="absolute"
class="h-40px flex-center"
>
{{ appStore.footerText }}
</n-layout-footer>
<BackTop />
</n-layout>
</n-layout>
</template>

View File

@ -1,160 +0,0 @@
<script lang="ts" setup>
import type { MenuInst, MenuOption } from 'naive-ui'
import { useAppStore, useRouteStore } from '@/store'
import {
BackTop,
CollapaseButton,
FullScreen,
Logo,
Notices,
Search,
Setting,
TabBar,
UserCenter,
} from './components'
const routeStore = useRouteStore()
const appStore = useAppStore()
const pageRoute = useRoute()
const router = useRouter()
const menuInstRef = ref<MenuInst | null>(null)
watch(
() => pageRoute.path,
() => {
menuInstRef.value?.showOption(routeStore.activeMenu as string)
},
{ immediate: true },
)
const topMenu = ref<MenuOption[]>([])
const activeTopMenu = ref<string>('')
function handleTopMenu(rowMenu: MenuOption[]) {
topMenu.value = rowMenu.map((i) => {
const { icon, label, key } = i
return {
icon,
label,
key,
}
})
}
onMounted(() => {
handleTopMenu(routeStore.menus)
//
const currentMenuKey = pageRoute.matched[1].path
handleSideMenu(currentMenuKey)
activeTopMenu.value = currentMenuKey
})
const sideMenu = ref<MenuOption[]>([])
function handleSideMenu(key: string) {
const routeMenu = routeStore.menus as MenuOption[]
const targetMenu = routeMenu.find(i => i.key === key)
if (targetMenu) {
sideMenu.value = targetMenu.children ? targetMenu.children : [targetMenu]
}
}
function updateTopMenu(key: string) {
handleSideMenu(key)
activeTopMenu.value = key
router.push(key)
}
</script>
<template>
<n-layout
has-sider
class="wh-full"
embedded
>
<n-layout-sider
v-if="!appStore.contentFullScreen"
bordered
:collapsed="appStore.collapsed"
collapse-mode="width"
:collapsed-width="64"
:width="240"
content-style="display: flex;flex-direction: column;min-height:100%;"
>
<Logo v-if="appStore.showLogo" />
<n-scrollbar class="flex-1">
<n-menu
ref="menuInstRef"
:collapsed="appStore.collapsed"
:indent="20"
:collapsed-width="64"
:options="sideMenu"
:value="routeStore.activeMenu"
/>
</n-scrollbar>
</n-layout-sider>
<n-layout
class="h-full flex flex-col"
content-style="display: flex;flex-direction: column;min-height:100%;"
embedded
:native-scrollbar="false"
>
<n-layout-header bordered position="absolute" class="z-999">
<div v-if="!appStore.contentFullScreen" class="h-60px flex-y-center justify-between">
<CollapaseButton />
<n-menu
ref="menuInstRef"
mode="horizontal"
responsive
:options="topMenu"
:value="activeTopMenu"
@update:value="updateTopMenu"
/>
<div class="flex-y-center gap-1 h-full p-x-xl">
<Search />
<Notices />
<FullScreen />
<DarkModeSwitch />
<LangsSwitch />
<Setting />
<UserCenter />
</div>
</div>
<TabBar v-if="appStore.showTabs" class="h-45px" />
</n-layout-header>
<div
class="flex-1 p-16px flex flex-col"
:class="{
'p-t-121px': appStore.showTabs,
'p-b-56px': appStore.showFooter && !appStore.contentFullScreen,
'p-t-76px': !appStore.showTabs,
'p-t-61px': appStore.contentFullScreen,
}"
>
<router-view v-slot="{ Component, route }" class="flex-1">
<transition
:name="appStore.transitionAnimation"
mode="out-in"
>
<keep-alive :include="routeStore.cacheRoutes">
<component
:is="Component"
v-if="appStore.loadFlag"
:key="route.fullPath"
/>
</keep-alive>
</transition>
</router-view>
</div>
<n-layout-footer
v-if="appStore.showFooter && !appStore.contentFullScreen"
bordered
position="absolute"
class="h-40px flex-center"
>
{{ appStore.footerText }}
</n-layout-footer>
<BackTop />
</n-layout>
</n-layout>
</template>

View File

@ -1,67 +0,0 @@
<script lang="ts" setup>
import { useAppStore, useRouteStore } from '@/store'
import {
BackTop,
FullScreen,
Logo,
Menu,
Notices,
Search,
Setting,
TabBar,
UserCenter,
} from './components'
const routeStore = useRouteStore()
const appStore = useAppStore()
</script>
<template>
<n-layout class="wh-full" embedded>
<n-layout
class="h-full flex flex-col" content-style="display: flex;flex-direction: column;min-height:100%;"
embedded :native-scrollbar="false"
>
<n-layout-header bordered position="absolute" class="z-999">
<div v-if="!appStore.contentFullScreen" class="h-60px flex-y-center justify-between shrink-0">
<Logo v-if="appStore.showLogo" />
<Menu mode="horizontal" responsive />
<div class="flex-y-center gap-1 h-full p-x-xl">
<Search />
<Notices />
<FullScreen />
<DarkModeSwitch />
<LangsSwitch />
<Setting />
<UserCenter />
</div>
</div>
<TabBar v-if="appStore.showTabs" class="h-45px" />
</n-layout-header>
<div
class="flex-1 p-16px flex flex-col"
:class="{
'p-t-121px': appStore.showTabs,
'p-b-56px': appStore.showFooter && !appStore.contentFullScreen,
'p-t-76px': !appStore.showTabs,
'p-t-61px': appStore.contentFullScreen,
}"
>
<router-view v-slot="{ Component, route }" class="flex-1">
<transition :name="appStore.transitionAnimation" mode="out-in">
<keep-alive :include="routeStore.cacheRoutes">
<component :is="Component" v-if="appStore.loadFlag" :key="route.fullPath" />
</keep-alive>
</transition>
</router-view>
</div>
<n-layout-footer
v-if="appStore.showFooter && !appStore.contentFullScreen"
bordered position="absolute" class="h-40px flex-center"
>
{{ appStore.footerText }}
</n-layout-footer>
<BackTop />
</n-layout>
</n-layout>
</template>

View File

@ -3,9 +3,9 @@ import { local, setLocale } from '@/utils'
import { colord } from 'colord'
import { set } from 'radash'
import themeConfig from './theme.json'
import type { ProLayoutMode } from 'pro-naive-ui'
export type TransitionAnimation = '' | 'fade-slide' | 'fade-bottom' | 'fade-scale' | 'zoom-fade' | 'zoom-out'
export type LayoutMode = 'leftMenu' | 'topMenu' | 'mixMenu'
const { VITE_DEFAULT_LANG, VITE_COPYRIGHT_INFO } = import.meta.env
@ -37,7 +37,7 @@ export const useAppStore = defineStore('app-store', {
showWatermark: false,
showSetting: false,
transitionAnimation: 'fade-slide' as TransitionAnimation,
layoutMode: 'leftMenu' as LayoutMode,
layoutMode: 'vertical' as ProLayoutMode,
contentFullScreen: false,
}
},