mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2025-11-09 06:57:14 +08:00
feat: 增强布局组件(同步GI-DEMO),支持更多布局选项并优化布局切换功能
Co-authored-by: kiki1373639299<zkai0106@163.com> # message auto-generated for no-merge-commit merge: !11 merge dev into dev feat: 增强布局组件(同步GI-DEMO),支持更多布局选项并优化布局切换功能 Created-by: kiki1373639299 Commit-by: kiki1373639299 Merged-by: Charles_7c Description: <!-- 非常感谢您的 PR!在提交之前,请务必确保您 PR 的代码经过了完整测试,并且通过了代码规范检查。 --> <!-- 在 [] 中输入 x 来勾选) --> ## PR 类型 <!-- 您的 PR 引入了哪种类型的变更? --> <!-- 只支持选择一种类型,如果有多种类型,可以在更新日志中增加 “类型” 列。 --> - [X] 新 feature - [ ] Bug 修复 - [ ] 功能增强 - [ ] 文档变更 - [ ] 代码样式变更 - [ ] 重构 - [ ] 性能改进 - [ ] 单元测试 - [ ] CI/CD - [ ] 其他 ## PR 目的 同步GI-DEMO系统布局,并自定义样式增强 <!-- 描述一下您的 PR 解决了什么问题。如果可以,请链接到相关 issues。 --> ## 解决方案 <!-- 详细描述您是如何解决的问题 --> ## PR 测试 <!-- 如果可以,请为您的 PR 添加或更新单元测试。 --> <!-- 请描述一下您是如何测试 PR 的。例如:创建/更新单元测试或添加相关的截图。 --> ## Changelog | 模块 | Changelog | Related issues | |-----|-----------| -------------- | | | | | <!-- 如果有多种类型的变更,可以在变更日志表中增加 “类型” 列,该列的值与上方 “PR 类型” 相同。 --> <!-- Related issues 格式为 Closes #<issue号>,或者 Fixes #<issue号>,或者 Resolves #<issue号>。 --> ## 其他信息 <!-- 请描述一下还有哪些注意事项。例如:如果引入了一个不向下兼容的变更,请描述其影响。 --> ## 提交前确认 - [X] PR 代码经过了完整测试,并且通过了代码规范检查 - [ ] 已经完整填写 Changelog,并链接到了相关 issues - [X] PR 代码将要提交到 dev 分支 See merge request: continew/continew-admin-ui!11
This commit is contained in:
208
src/layout/LayoutColumns.vue
Normal file
208
src/layout/LayoutColumns.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div class="layout-columns">
|
||||
<div v-show="isDesktop" class="layout-columns__left">
|
||||
<div class="layout-columns__menu-wrapper">
|
||||
<!-- 左侧一级菜单区域 -->
|
||||
<OneLevelMenu :menus="oneLevelMenus" @menu-click="handleMenuClick"></OneLevelMenu>
|
||||
<div class="layout-columns__right-menu" :class="{ collapsed: appStore.menuCollapse }">
|
||||
<!-- 系统标题 -->
|
||||
<div class="layout-columns__title" @click="toHome">
|
||||
<span v-show="!appStore.menuCollapse" class="system-name gi_line_1">{{ title }}</span>
|
||||
</div>
|
||||
<!-- 左侧二级菜单区域 -->
|
||||
<Menu
|
||||
v-if="twoLevelMenus.length > 1" class="layout-columns__menu" :menus="twoLevelMenus"
|
||||
:menu-style="menuStyle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容区域 -->
|
||||
<section class="layout-columns__content">
|
||||
<Header />
|
||||
<Tabs v-if="appStore.tab" />
|
||||
<Main />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { findTree, mapTree } from 'xe-utils'
|
||||
import Header from './components/Header/index.vue'
|
||||
import Main from './components/Main.vue'
|
||||
import Menu from './components/Menu/index.vue'
|
||||
import OneLevelMenu from './components/OneLevelMenu/index.vue'
|
||||
import Tabs from './components/Tabs/index.vue'
|
||||
import { filterTree } from '@/utils'
|
||||
import { useAppStore, useRouteStore } from '@/stores'
|
||||
import { useDevice } from '@/hooks'
|
||||
|
||||
defineOptions({ name: 'LayoutColumns' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const routeStore = useRouteStore()
|
||||
const { isDesktop } = useDevice()
|
||||
|
||||
// 系统标题和Logo
|
||||
const title = computed(() => appStore.getTitle())
|
||||
|
||||
// 菜单样式 - 根据折叠状态动态调整宽度
|
||||
const menuStyle = computed(() => {
|
||||
return {
|
||||
width: appStore.menuCollapse ? '48px' : '200px',
|
||||
}
|
||||
})
|
||||
|
||||
// 跳转首页
|
||||
const toHome = () => {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 处理菜单路由数据
|
||||
const cloneRoutes = JSON.parse(JSON.stringify(routeStore.routes)) as RouteRecordRaw[]
|
||||
const showMenuList = filterTree(cloneRoutes, (i) => i.meta?.hidden === false) as RouteRecordRaw[]
|
||||
|
||||
// 一级菜单
|
||||
const oneLevelMenus = ref<RouteRecordRaw[]>([])
|
||||
function getOneLevelMenus() {
|
||||
const cloneList = JSON.parse(JSON.stringify(routeStore.routes)) as RouteRecordRaw[]
|
||||
const formatList = mapTree(cloneList, (i) => {
|
||||
if (i?.children?.length === 1 && i?.meta?.alwaysShow !== true) {
|
||||
return i.children?.[0]
|
||||
}
|
||||
return i
|
||||
})
|
||||
const arr = formatList.filter((i) => i.meta?.hidden === false)
|
||||
return arr
|
||||
}
|
||||
oneLevelMenus.value = getOneLevelMenus()
|
||||
|
||||
// 二级菜单
|
||||
const twoLevelMenus = computed(() => {
|
||||
const obj = findTree(showMenuList, (i) => i.path === route.path)
|
||||
return showMenuList?.[Number(obj.path[0])]?.children || []
|
||||
})
|
||||
|
||||
function handleMenuClick(item: RouteRecordRaw) {
|
||||
if (item.redirect === 'noRedirect') {
|
||||
router.replace({ path: item.children?.[0]?.path })
|
||||
return
|
||||
}
|
||||
router.replace({ path: (item.redirect as string) || item.path })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-columns {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
&__left {
|
||||
height: 100%;
|
||||
background-color: var(--color-bg-1);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__menu-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__right-menu {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--color-border-2);
|
||||
transition: width 0.2s;
|
||||
width: 200px;
|
||||
|
||||
&.collapsed {
|
||||
width: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
height: 56px;
|
||||
padding: 0 12px;
|
||||
color: var(--color-text-1);
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: padding 0.2s;
|
||||
|
||||
.layout-columns__right-menu.collapsed & {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.system-name {
|
||||
padding-left: 6px;
|
||||
white-space: nowrap;
|
||||
transition: color 0.3s;
|
||||
line-height: 1.5;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
color: $color-theme !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__menu {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 折叠状态下的菜单样式
|
||||
:deep(.arco-menu.arco-menu-vertical.arco-menu-collapsed) {
|
||||
.arco-menu-icon {
|
||||
margin-right: 0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.arco-menu-has-icon {
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.arco-menu-title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
51
src/layout/LayoutTop.vue
Normal file
51
src/layout/LayoutTop.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="layout-top">
|
||||
<a-row align="center" class="layout-top__header">
|
||||
<Logo></Logo>
|
||||
<Menu class="layout-top__menu"></Menu>
|
||||
<HeaderRightBar></HeaderRightBar>
|
||||
</a-row>
|
||||
<Tabs v-if="appStore.tab"></Tabs>
|
||||
<Main></Main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import HeaderRightBar from './components/HeaderRightBar/index.vue'
|
||||
import Logo from './components/Logo.vue'
|
||||
import Main from './components/Main.vue'
|
||||
import Menu from './components/Menu/index.vue'
|
||||
import Tabs from './components/Tabs/index.vue'
|
||||
import { useAppStore } from '@/stores'
|
||||
|
||||
defineOptions({ name: 'LayoutTop' })
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-top {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
&__header {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background-color: var(--color-bg-1);
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
&__menu {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.arco-menu-inner) {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,29 +5,10 @@
|
||||
「复制配置」按钮,并将配置粘贴到 src/config/settings.ts 文件中。
|
||||
</a-alert>
|
||||
<a-divider v-if="settingOpen" orientation="center">系统布局</a-divider>
|
||||
<a-row v-if="settingOpen" justify="center">
|
||||
<a-space>
|
||||
<a-badge>
|
||||
<template #content>
|
||||
<icon-check-circle-fill
|
||||
v-if="appStore.layout === 'left'" style="color: rgb(var(--success-6))"
|
||||
:size="16"
|
||||
></icon-check-circle-fill>
|
||||
</template>
|
||||
<LayoutItem mode="left" @click="appStore.layout = 'left'"></LayoutItem>
|
||||
<p class="layout-text">默认布局</p>
|
||||
</a-badge>
|
||||
<a-badge>
|
||||
<template #content>
|
||||
<icon-check-circle-fill
|
||||
v-if="appStore.layout === 'mix'" :size="16"
|
||||
style="color: rgb(var(--success-6))"
|
||||
></icon-check-circle-fill>
|
||||
</template>
|
||||
<LayoutItem mode="mix" @click="appStore.layout = 'mix'"></LayoutItem>
|
||||
<p class="layout-text">混合布局</p>
|
||||
</a-badge>
|
||||
</a-space>
|
||||
<a-row v-if="settingOpen" :gutter="[8, 8]">
|
||||
<a-col v-for="item in LAYOUT_OPTIONS" :key="item.value" :span="8">
|
||||
<LayoutItem :mode="item.value" :name="item.label" @click="toggleLayout(item.value)" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-divider orientation="center">系统主题</a-divider>
|
||||
@@ -111,6 +92,15 @@ defineOptions({ name: 'SettingDrawer' })
|
||||
const appStore = useAppStore()
|
||||
const visible = ref(false)
|
||||
const settingOpen = JSON.parse(import.meta.env.VITE_APP_SETTING)
|
||||
interface LayoutItemProps { label: string, value: App.AppSettings['layout'] }
|
||||
|
||||
/** 布局选项 */
|
||||
const LAYOUT_OPTIONS: LayoutItemProps[] = [
|
||||
{ label: '默认布局', value: 'left' },
|
||||
{ label: '混合布局', value: 'mix' },
|
||||
{ label: '顶部布局', value: 'top' },
|
||||
{ label: '双列布局', value: 'columns' },
|
||||
]
|
||||
const tabModeList: App.TabItem[] = [
|
||||
{ label: '卡片', value: 'card' },
|
||||
{ label: '间隔卡片', value: 'card-gutter' },
|
||||
@@ -190,6 +180,10 @@ const copySettings = () => {
|
||||
Message.error({ content: '请检查浏览器权限是否开启' })
|
||||
}
|
||||
}
|
||||
/** 切换布局 */
|
||||
const toggleLayout = (layout: App.AppSettings['layout']) => {
|
||||
appStore.layout = layout
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
@@ -1,62 +1,191 @@
|
||||
<!--
|
||||
@file LayoutItem 组件
|
||||
@description 布局切换选项组件,支持左侧布局、顶部布局和混合布局三种模式
|
||||
-->
|
||||
<template>
|
||||
<div class="layout-item" :class="`layout-item-${mode}`" @click="emit('click')"></div>
|
||||
<a-badge class="w-full">
|
||||
<!-- 选中状态标记 -->
|
||||
<template #content>
|
||||
<icon-check-circle-fill
|
||||
v-if="appStore.layout === props.mode" style="color: rgb(var(--success-6))"
|
||||
:size="16"
|
||||
></icon-check-circle-fill>
|
||||
</template>
|
||||
|
||||
<!-- 布局预览 -->
|
||||
<div class="layout-mode-item" :class="`layout-mode-item__${props.mode}`" @click="emit('click')">
|
||||
<!-- 左侧布局 -->
|
||||
<template v-if="props.mode === 'left'">
|
||||
<div class="block-left"></div>
|
||||
<div class="block-right"></div>
|
||||
</template>
|
||||
|
||||
<!-- 顶部布局 -->
|
||||
<template v-if="props.mode === 'top'">
|
||||
<div class="block-top"></div>
|
||||
<div class="block-bottom"></div>
|
||||
</template>
|
||||
|
||||
<!-- 混合布局 -->
|
||||
<template v-if="props.mode === 'mix'">
|
||||
<div class="block-top"></div>
|
||||
<div class="block-main">
|
||||
<div class="block-left"></div>
|
||||
<div class="block-right"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 双列布局 -->
|
||||
<template v-if="props.mode === 'columns'">
|
||||
<div class="block-left block-column"></div>
|
||||
<div class="block-left"></div>
|
||||
<div class="block-right"></div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 布局名称 -->
|
||||
<p class="layout-mode-item__text">{{ props.name }}</p>
|
||||
</a-badge>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAppStore } from '@/stores'
|
||||
|
||||
defineOptions({
|
||||
name: 'LayoutModeItem',
|
||||
})
|
||||
|
||||
/** Props 默认值 */
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
mode: 'left',
|
||||
})
|
||||
|
||||
/** Emits 定义 */
|
||||
const emit = defineEmits<{
|
||||
(e: 'click'): void
|
||||
}>()
|
||||
|
||||
/** 布局模式类型 */
|
||||
type LayoutMode = 'left' | 'top' | 'mix' | 'columns'
|
||||
|
||||
/** Props 类型定义 */
|
||||
interface Props {
|
||||
mode: 'left' | 'top' | 'mix'
|
||||
/** 布局模式 */
|
||||
mode?: LayoutMode
|
||||
/** 布局名称 */
|
||||
name: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {})
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
/** 应用状态 */
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-item {
|
||||
width: 60px;
|
||||
<style lang="scss" scoped>
|
||||
// 布局项基础样式
|
||||
.layout-mode-item {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background-color: var(--color-fill-3);
|
||||
border-radius: 3px;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
&::before {
|
||||
content: '';
|
||||
width: 12px;
|
||||
height: 100%;
|
||||
background-color: rgb(var(--gray-9));
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
border-radius: 2px;
|
||||
background-color: var(--color-bg-5);
|
||||
border: 1px solid var(--color-border-2);
|
||||
box-shadow: 0 1px 2px -2px rgba(0, 0, 0, .08),
|
||||
0 3px 6px 0 rgba(0, 0, 0, .06),
|
||||
0 5px 12px 4px rgba(0, 0, 0, .04);
|
||||
|
||||
&__text {
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background-color: rgb(var(--gray-9));
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.block-left,
|
||||
.block-right,
|
||||
.block-top,
|
||||
.block-bottom {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
// 左侧布局样式
|
||||
.layout-mode-item__left {
|
||||
.block-left {
|
||||
width: 10px;
|
||||
background-color: $color-theme;
|
||||
}
|
||||
&-left {
|
||||
&::before {
|
||||
display: block;
|
||||
|
||||
.block-right {
|
||||
flex: 1;
|
||||
margin-left: 4px;
|
||||
background-color: var(--color-fill-3);
|
||||
}
|
||||
}
|
||||
|
||||
// 顶部布局样式
|
||||
.layout-mode-item__top {
|
||||
flex-direction: column;
|
||||
|
||||
.block-top {
|
||||
height: 8px;
|
||||
background-color: $color-theme;
|
||||
}
|
||||
|
||||
.block-bottom {
|
||||
flex: 1;
|
||||
margin-top: 4px;
|
||||
background-color: var(--color-fill-3);
|
||||
}
|
||||
}
|
||||
|
||||
// 混合布局样式
|
||||
.layout-mode-item__mix {
|
||||
flex-direction: column;
|
||||
|
||||
.block-top {
|
||||
height: 8px;
|
||||
margin-bottom: 3px;
|
||||
background-color: $color-theme;
|
||||
}
|
||||
|
||||
.block-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
.block-left {
|
||||
width: 10px;
|
||||
background-color: $color-theme;
|
||||
}
|
||||
}
|
||||
&-top {
|
||||
&::after {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&-mix {
|
||||
&::before,
|
||||
&::after {
|
||||
display: block;
|
||||
|
||||
.block-right {
|
||||
flex: 1;
|
||||
margin-left: 3px;
|
||||
background-color: var(--color-fill-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 双列布局样式
|
||||
.layout-mode-item__columns {
|
||||
|
||||
.block-left {
|
||||
width: 10px;
|
||||
background-color: $color-theme;
|
||||
}
|
||||
|
||||
.block-right {
|
||||
flex: 1;
|
||||
margin-left: 4px;
|
||||
background-color: var(--color-fill-3);
|
||||
}
|
||||
|
||||
.block-column {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<section class="system-logo" :class="{ collapsed: props.collapsed }" @click="toHome">
|
||||
<img v-if="logo" class="logo" :src="logo" alt="logo" />
|
||||
<img v-else class="logo" src="/logo.svg" alt="logo" />
|
||||
<span class="system-name gi_line_1">{{ title }}</span>
|
||||
<span v-if="!props.hideName" class="system-name gi_line_1">{{ title }}</span>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -11,13 +11,19 @@ import { useAppStore } from '@/stores'
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
collapsed: false,
|
||||
hideName: false,
|
||||
|
||||
})
|
||||
const appStore = useAppStore()
|
||||
const title = computed(() => appStore.getTitle())
|
||||
const logo = computed(() => appStore.getLogo())
|
||||
|
||||
/** Props 类型定义 */
|
||||
interface Props {
|
||||
/** 是否折叠状态 */
|
||||
collapsed?: boolean
|
||||
/** 是否隐藏名称 */
|
||||
hideName?: boolean
|
||||
}
|
||||
const router = useRouter()
|
||||
// 跳转首页
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:accordion="appStore.menuAccordion"
|
||||
:breakpoint="appStore.layout === 'mix' ? 'xl' : undefined"
|
||||
:trigger-props="{ animationName: 'slide-dynamic-origin' }"
|
||||
:collapsed="!isDesktop ? false : appStore.menuCollapse"
|
||||
:collapsed="mode === 'vertical' && isDesktop ? appStore.menuCollapse : false"
|
||||
:style="menuStyle"
|
||||
@menu-item-click="onMenuItemClick"
|
||||
@collapse="onCollapse"
|
||||
@@ -44,11 +44,7 @@ const sidebarRoutes = computed(() => (props.menus ? props.menus : routeStore.rou
|
||||
|
||||
// 菜单垂直模式/水平模式
|
||||
const mode = computed(() => {
|
||||
if (!['left', 'mix'].includes(appStore.layout)) {
|
||||
return 'horizontal'
|
||||
} else {
|
||||
return 'vertical'
|
||||
}
|
||||
return ['left', 'mix', 'columns'].includes(appStore.layout) ? 'vertical' : 'horizontal'
|
||||
})
|
||||
|
||||
// 是否默认展开选中的菜单
|
||||
|
||||
15
src/layout/components/OneLevelMenu/MenuIcon.vue
Normal file
15
src/layout/components/OneLevelMenu/MenuIcon.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<GiSvgIcon v-if="props.svgIcon" :name="props.svgIcon" :size="24"></GiSvgIcon>
|
||||
<component :is="props.icon" v-else-if="props.icon"></component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
svgIcon?: string
|
||||
icon?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
104
src/layout/components/OneLevelMenu/index.vue
Normal file
104
src/layout/components/OneLevelMenu/index.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="one-level-menu">
|
||||
<Logo class="one-level-menu__logo" hide-name></Logo>
|
||||
<div class="one-level-menu__wrap">
|
||||
<a-scrollbar style="height:100%;overflow: auto;" :outer-style="{ width: '100%' }">
|
||||
<ul class="one-level-menu__list">
|
||||
<li
|
||||
v-for="(item) in props.menus" :key="item.path" class="one-level-menu__item"
|
||||
:class="{ 'one-level-menu__item--active': calcIsActive(item) }" @click="emits('menu-click', item)"
|
||||
>
|
||||
<GiSvgIcon :name="item?.meta?.icon || ''"></GiSvgIcon>
|
||||
<p class="one-level-menu__item__title gi_line_1" :title="item?.meta?.title">{{ item?.meta?.title }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</a-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import Logo from '../Logo.vue'
|
||||
import MenuIcon from './MenuIcon.vue'
|
||||
|
||||
interface Props {
|
||||
menus?: RouteRecordRaw[]
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
menus: () => [],
|
||||
})
|
||||
const emits = defineEmits<{
|
||||
(e: 'menu-click', item: RouteRecordRaw): void
|
||||
}>()
|
||||
const route = useRoute()
|
||||
const calcIsActive = (item: RouteRecordRaw) => {
|
||||
return route.path.startsWith(item.path) && item.path !== '/'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.arco-scrollbar-track) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.one-level-menu {
|
||||
width: 68px;
|
||||
height: 100%;
|
||||
background-color: var(--color-bg-1);
|
||||
border-right: 1px solid var(--color-border-2);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__logo {
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid var(--color-border-2);
|
||||
|
||||
:deep(.logo) {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
&__wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__list {
|
||||
padding: 4px 4px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 0;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&--active {
|
||||
background-color: var(--color-primary-light-2);
|
||||
color: rgb(var(--primary-6))
|
||||
}
|
||||
|
||||
&:not(.one-level-menu__item--active):hover {
|
||||
background-color: var(--color-fill-2);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
line-height: 1;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +1,36 @@
|
||||
<!--
|
||||
@file Layout 组件
|
||||
@description 布局根组件,支持默认布局、混合布局和顶部布局三种模式
|
||||
-->
|
||||
<template>
|
||||
<LayoutMix v-if="appStore.layout === 'mix'"></LayoutMix>
|
||||
<LayoutDefault v-else></LayoutDefault>
|
||||
<component :is="currentLayout" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LayoutDefault from './LayoutDefault.vue'
|
||||
import LayoutMix from './LayoutMix.vue'
|
||||
import { useAppStore } from '@/stores'
|
||||
|
||||
/** 组件名称 */
|
||||
defineOptions({ name: 'Layout' })
|
||||
const LayoutDefault = defineAsyncComponent(() => import('./LayoutDefault.vue'))
|
||||
const LayoutColumns = defineAsyncComponent(() => import('./LayoutColumns.vue'))
|
||||
const LayoutMix = defineAsyncComponent(() => import('./LayoutMix.vue'))
|
||||
const LayoutTop = defineAsyncComponent(() => import('./LayoutTop.vue'))
|
||||
|
||||
/** 状态管理 */
|
||||
const appStore = useAppStore()
|
||||
|
||||
/** 布局组件映射 */
|
||||
const layoutMap = {
|
||||
mix: LayoutMix,
|
||||
top: LayoutTop,
|
||||
default: LayoutDefault,
|
||||
columns: LayoutColumns,
|
||||
} as const
|
||||
|
||||
/** 当前布局组件 */
|
||||
const currentLayout = computed(() =>
|
||||
layoutMap[appStore.layout as keyof typeof layoutMap] || layoutMap.default,
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
2
src/types/app.d.ts
vendored
2
src/types/app.d.ts
vendored
@@ -10,7 +10,7 @@ declare namespace App {
|
||||
menuAccordion: boolean
|
||||
menuDark: boolean
|
||||
copyrightDisplay: boolean
|
||||
layout: 'left' | 'mix'
|
||||
layout: 'left' | 'mix' | 'columns' | 'top'
|
||||
isOpenWatermark?: boolean
|
||||
watermark?: string
|
||||
enableColorWeaknessMode?: boolean
|
||||
|
||||
Reference in New Issue
Block a user