mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2025-11-09 16:57:13 +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 文件中。
|
「复制配置」按钮,并将配置粘贴到 src/config/settings.ts 文件中。
|
||||||
</a-alert>
|
</a-alert>
|
||||||
<a-divider v-if="settingOpen" orientation="center">系统布局</a-divider>
|
<a-divider v-if="settingOpen" orientation="center">系统布局</a-divider>
|
||||||
<a-row v-if="settingOpen" justify="center">
|
<a-row v-if="settingOpen" :gutter="[8, 8]">
|
||||||
<a-space>
|
<a-col v-for="item in LAYOUT_OPTIONS" :key="item.value" :span="8">
|
||||||
<a-badge>
|
<LayoutItem :mode="item.value" :name="item.label" @click="toggleLayout(item.value)" />
|
||||||
<template #content>
|
</a-col>
|
||||||
<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>
|
</a-row>
|
||||||
|
|
||||||
<a-divider orientation="center">系统主题</a-divider>
|
<a-divider orientation="center">系统主题</a-divider>
|
||||||
@@ -111,6 +92,15 @@ defineOptions({ name: 'SettingDrawer' })
|
|||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
const settingOpen = JSON.parse(import.meta.env.VITE_APP_SETTING)
|
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[] = [
|
const tabModeList: App.TabItem[] = [
|
||||||
{ label: '卡片', value: 'card' },
|
{ label: '卡片', value: 'card' },
|
||||||
{ label: '间隔卡片', value: 'card-gutter' },
|
{ label: '间隔卡片', value: 'card-gutter' },
|
||||||
@@ -190,6 +180,10 @@ const copySettings = () => {
|
|||||||
Message.error({ content: '请检查浏览器权限是否开启' })
|
Message.error({ content: '请检查浏览器权限是否开启' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/** 切换布局 */
|
||||||
|
const toggleLayout = (layout: App.AppSettings['layout']) => {
|
||||||
|
appStore.layout = layout
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({ open })
|
defineExpose({ open })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,62 +1,191 @@
|
|||||||
|
<!--
|
||||||
|
@file LayoutItem 组件
|
||||||
|
@description 布局切换选项组件,支持左侧布局、顶部布局和混合布局三种模式
|
||||||
|
-->
|
||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 {
|
interface Props {
|
||||||
mode: 'left' | 'top' | 'mix'
|
/** 布局模式 */
|
||||||
|
mode?: LayoutMode
|
||||||
|
/** 布局名称 */
|
||||||
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {})
|
/** 应用状态 */
|
||||||
|
const appStore = useAppStore()
|
||||||
const emit = defineEmits(['click'])
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style lang="scss" scoped>
|
||||||
.layout-item {
|
// 布局项基础样式
|
||||||
width: 60px;
|
.layout-mode-item {
|
||||||
|
width: 100%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
background-color: var(--color-fill-3);
|
padding: 4px;
|
||||||
border-radius: 3px;
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: relative;
|
border-radius: 2px;
|
||||||
&::before {
|
background-color: var(--color-bg-5);
|
||||||
content: '';
|
border: 1px solid var(--color-border-2);
|
||||||
width: 12px;
|
box-shadow: 0 1px 2px -2px rgba(0, 0, 0, .08),
|
||||||
height: 100%;
|
0 3px 6px 0 rgba(0, 0, 0, .06),
|
||||||
background-color: rgb(var(--gray-9));
|
0 5px 12px 4px rgba(0, 0, 0, .04);
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
&__text {
|
||||||
left: 0;
|
font-size: 12px;
|
||||||
display: none;
|
margin-top: 5px;
|
||||||
}
|
text-align: center;
|
||||||
&::after {
|
color: var(--color-text-2);
|
||||||
content: '';
|
|
||||||
width: 100%;
|
|
||||||
height: 10px;
|
|
||||||
background-color: rgb(var(--gray-9));
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
&-left {
|
|
||||||
&::before {
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&-top {
|
|
||||||
&::after {
|
.block-left,
|
||||||
display: block;
|
.block-right,
|
||||||
|
.block-top,
|
||||||
|
.block-bottom {
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 左侧布局样式
|
||||||
|
.layout-mode-item__left {
|
||||||
|
.block-left {
|
||||||
|
width: 10px;
|
||||||
|
background-color: $color-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-right {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 4px;
|
||||||
|
background-color: var(--color-fill-3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&-mix {
|
|
||||||
&::before,
|
// 顶部布局样式
|
||||||
&::after {
|
.layout-mode-item__top {
|
||||||
display: block;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<section class="system-logo" :class="{ collapsed: props.collapsed }" @click="toHome">
|
<section class="system-logo" :class="{ collapsed: props.collapsed }" @click="toHome">
|
||||||
<img v-if="logo" class="logo" :src="logo" alt="logo" />
|
<img v-if="logo" class="logo" :src="logo" alt="logo" />
|
||||||
<img v-else class="logo" src="/logo.svg" 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>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -11,13 +11,19 @@ import { useAppStore } from '@/stores'
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
|
hideName: false,
|
||||||
|
|
||||||
})
|
})
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const title = computed(() => appStore.getTitle())
|
const title = computed(() => appStore.getTitle())
|
||||||
const logo = computed(() => appStore.getLogo())
|
const logo = computed(() => appStore.getLogo())
|
||||||
|
|
||||||
|
/** Props 类型定义 */
|
||||||
interface Props {
|
interface Props {
|
||||||
|
/** 是否折叠状态 */
|
||||||
collapsed?: boolean
|
collapsed?: boolean
|
||||||
|
/** 是否隐藏名称 */
|
||||||
|
hideName?: boolean
|
||||||
}
|
}
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
// 跳转首页
|
// 跳转首页
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
:accordion="appStore.menuAccordion"
|
:accordion="appStore.menuAccordion"
|
||||||
:breakpoint="appStore.layout === 'mix' ? 'xl' : undefined"
|
:breakpoint="appStore.layout === 'mix' ? 'xl' : undefined"
|
||||||
:trigger-props="{ animationName: 'slide-dynamic-origin' }"
|
:trigger-props="{ animationName: 'slide-dynamic-origin' }"
|
||||||
:collapsed="!isDesktop ? false : appStore.menuCollapse"
|
:collapsed="mode === 'vertical' && isDesktop ? appStore.menuCollapse : false"
|
||||||
:style="menuStyle"
|
:style="menuStyle"
|
||||||
@menu-item-click="onMenuItemClick"
|
@menu-item-click="onMenuItemClick"
|
||||||
@collapse="onCollapse"
|
@collapse="onCollapse"
|
||||||
@@ -44,11 +44,7 @@ const sidebarRoutes = computed(() => (props.menus ? props.menus : routeStore.rou
|
|||||||
|
|
||||||
// 菜单垂直模式/水平模式
|
// 菜单垂直模式/水平模式
|
||||||
const mode = computed(() => {
|
const mode = computed(() => {
|
||||||
if (!['left', 'mix'].includes(appStore.layout)) {
|
return ['left', 'mix', 'columns'].includes(appStore.layout) ? 'vertical' : 'horizontal'
|
||||||
return 'horizontal'
|
|
||||||
} else {
|
|
||||||
return 'vertical'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 是否默认展开选中的菜单
|
// 是否默认展开选中的菜单
|
||||||
|
|||||||
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>
|
<template>
|
||||||
<LayoutMix v-if="appStore.layout === 'mix'"></LayoutMix>
|
<component :is="currentLayout" />
|
||||||
<LayoutDefault v-else></LayoutDefault>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LayoutDefault from './LayoutDefault.vue'
|
|
||||||
import LayoutMix from './LayoutMix.vue'
|
|
||||||
import { useAppStore } from '@/stores'
|
import { useAppStore } from '@/stores'
|
||||||
|
|
||||||
|
/** 组件名称 */
|
||||||
defineOptions({ name: 'Layout' })
|
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 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>
|
</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
|
menuAccordion: boolean
|
||||||
menuDark: boolean
|
menuDark: boolean
|
||||||
copyrightDisplay: boolean
|
copyrightDisplay: boolean
|
||||||
layout: 'left' | 'mix'
|
layout: 'left' | 'mix' | 'columns' | 'top'
|
||||||
isOpenWatermark?: boolean
|
isOpenWatermark?: boolean
|
||||||
watermark?: string
|
watermark?: string
|
||||||
enableColorWeaknessMode?: boolean
|
enableColorWeaknessMode?: boolean
|
||||||
|
|||||||
Reference in New Issue
Block a user