break: 去掉switch配置,改为使用navigation

This commit is contained in:
wanchun 2022-06-17 12:44:21 +08:00
parent 4d25799d70
commit 1503d8b8af
9 changed files with 128 additions and 154 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -3,14 +3,14 @@
## 介绍 ## 介绍
为了进一步降低研发成本,我们将布局利用 `fes.js` 插件的方式内置,只需通过简单的配置即可拥有布局,包括导航以及侧边栏。从而做到用户无需关心布局。 为了进一步降低研发成本,我们将布局利用 `fes.js` 插件的方式内置,只需通过简单的配置即可拥有布局,包括导航以及侧边栏。从而做到用户无需关心布局。
- 侧边栏菜单数据根据路由中的配置自动生成。 - 侧边栏菜单数据根据路由中的配置自动生成。
- 布局,提供 `side``top``mixin`种布局。 - 布局,提供 `side``top``mixin``left-right`种布局。
- 主题,提供 `light``dark` 两种主题。 - 主题,提供 `light``dark` 两种主题。
- 默认实现对路由的 404、403 处理。 - 默认实现对路由的 404、403 处理。
- 搭配 [@fesjs/plugin-access](./access.html) 插件使用,可以完成对路由的权限控制。 - 搭配 [@fesjs/plugin-access](./access.html) 插件使用,可以完成对路由的权限控制。
- 搭配 [@fesjs/plugin-locale](./locale.html) 插件使用,提供切换语言的能力。 - 搭配 [@fesjs/plugin-locale](./locale.html) 插件使用,提供切换语言的能力。
- 支持自定义头部区域。 - 支持自定义头部或者侧边栏区域。
- 菜单支持配置icon - 菜单支持配置icon
- 菜单标题支持国际化 - 菜单标题支持国际化
- 可配置页面是否需要 layout。 - 可配置页面是否需要 layout。
@ -26,7 +26,7 @@
``` ```
## 布局类型 ## 布局类型
配置参数是 `navigation`, 布局有三种类型 `side``mixin` `top` 默认是 `side` 配置参数是 `navigation`, 布局有三种类型 `side``mixin` `top``left-right` 默认是 `side`
### side ### side
<!-- ![side](/side.png) --> <!-- ![side](/side.png) -->
@ -40,54 +40,23 @@
<!-- ![mixin](/mixin.png) --> <!-- ![mixin](/mixin.png) -->
<img :src="$withBase('mixin.png')" alt="mixin"> <img :src="$withBase('mixin.png')" alt="mixin">
### 布局开关 ### left-right
<!-- ![mixin](/mixin.png) -->
<img :src="$withBase('left-right.png')" alt="left-right">
布局默认开启,可以通过一些方式更改。 ### 页面个性化
开关的可选配置有: 可以为页面单独设置布局类型:
- **sidebar** 左侧区域
- **header** 头部区域
- **logo**logo和标题区域。
#### 全局定义
全局定义可以通过配置`switch`实现,在 `app.js` 中配置:
```js
import UserCenter from '@/components/UserCenter';
export const layout = {
switch: {
header: false
}
};
```
#### 页面定义
可以通过[定义路由元信息](../../../guide/route.html#扩展路由元信息)配置页面的布局开关,添加如下配置:
```vue
<config lang="json">
{
"layout": false
}
</config>
```
如果只是不想展示`sidebar`,则:
``` ```
<config lang="json"> <config lang="json">
{ {
"layout": { "layout": {
"sidebar": false "navigation": 'top'
} }
} }
</config> </config>
``` ```
当设置为 `null` 时,页面不使用布局。
#### 地址参数定义
通过路由的`query`参数定义,比如当访问`http://localhost:8080/#/?layout={%22sidebar%22:%20false,%20%22header%22:%20false}` 时,页面隐藏 `sidebar``header`区域。此种方式优先级最高!
## 页面缓存 ## 页面缓存
@ -282,19 +251,6 @@ export const layout = (layoutConfig, { initialState }) => ({
- **accordion**:是否只保持一个子菜单的展开。 - **accordion**:是否只保持一个子菜单的展开。
### switch
- **类型**`Object`
- **默认值**`{ logo: true, sidebar: true, header: true }`
- **详情**:布局的开关:
- **logo**是否展示logo区域。
- **sidebar**:配置默认展开的菜单,需要传子项是菜单路径的数组。
- **header**:是否只保持一个子菜单的展开。
### sideWidth ### sideWidth
- **类型**`Number` - **类型**`Number`
@ -303,12 +259,12 @@ export const layout = (layoutConfig, { initialState }) => ({
- **详情**sidebar的宽度 - **详情**sidebar的宽度
### renderHeader ### renderCustom
- **类型** `()=> VNodes` - **类型** `()=> VNodes`
- **默认值**`null` - **默认值**`null`
- **详情** 自定义`top`区域部分位置,仅运行时。 - **详情** 自定义区域内容,仅运行时。
### unAccessHandler ### unAccessHandler

View File

@ -1,8 +1,7 @@
<template> <template>
<f-layout v-if="routeLayout" class="main-layout"> <f-layout class="main-layout">
<template v-if="navigation === 'side'"> <template v-if="currentNavigation === 'side'">
<f-aside <f-aside
v-if="routeLayout.sidebar"
v-model:collapsed="collapsedRef" v-model:collapsed="collapsedRef"
:fixed="isSidebarFixed" :fixed="isSidebarFixed"
:width="`${sideWidth}px`" :width="`${sideWidth}px`"
@ -10,7 +9,7 @@
collapsible collapsible
:inverted="theme === 'dark'" :inverted="theme === 'dark'"
> >
<div v-if="routeLayout.logo" class="layout-logo"> <div class="layout-logo">
<img :src="logo" class="logo-img" /> <img :src="logo" class="logo-img" />
<div class="logo-name">{{ title }}</div> <div class="logo-name">{{ title }}</div>
</div> </div>
@ -26,9 +25,9 @@
/> />
</f-aside> </f-aside>
<f-layout :fixed="isSidebarFixed" :style="sideStyleRef"> <f-layout :fixed="isSidebarFixed" :style="sideStyleRef">
<f-header v-if="routeLayout.header" ref="headerRef" class="layout-header" :fixed="currentFixedHeaderRef"> <f-header ref="headerRef" class="layout-header" :fixed="currentFixedHeaderRef">
<div class="layout-header-custom"> <div class="layout-header-custom">
<slot name="customHeader"></slot> <slot name="renderCustom"></slot>
</div> </div>
<template v-if="locale"> <template v-if="locale">
<slot name="locale"></slot> <slot name="locale"></slot>
@ -44,9 +43,56 @@
</f-layout> </f-layout>
</f-layout> </f-layout>
</template> </template>
<template v-if="navigation === 'top'"> <template v-if="currentNavigation === 'left-right'">
<f-header v-if="routeLayout.header" ref="headerRef" class="layout-header" :inverted="theme === 'dark'" :fixed="currentFixedHeaderRef"> <f-aside
<div v-if="routeLayout.logo" class="layout-logo"> v-model:collapsed="collapsedRef"
:fixed="isSidebarFixed"
:width="`${sideWidth}px`"
class="layout-aside"
collapsible
:inverted="theme === 'dark'"
>
<div class="flex-between">
<div>
<div class="layout-logo">
<img :src="logo" class="logo-img" />
<div class="logo-name">{{ title }}</div>
</div>
<Menu
class="layout-menu"
:menus="menus"
:collapsed="collapsedRef"
mode="vertical"
:inverted="theme === 'dark'"
:expandedKeys="menuProps?.expandedKeys"
:defaultExpandAll="menuProps?.defaultExpandAll"
:accordion="menuProps?.accordion"
/>
</div>
<div>
<div class="layout-aside-custom">
<slot name="renderCustom"></slot>
</div>
<div v-if="locale" class="layout-aside-locale">
<slot name="locale"></slot>
</div>
</div>
</div>
</f-aside>
<f-layout :fixed="isSidebarFixed" :style="sideStyleRef">
<f-layout :embedded="!multiTabs">
<f-main class="layout-main">
<MultiTabProvider :multiTabs="multiTabs" />
</f-main>
<f-footer v-if="footer" class="layout-footer">
{{ footer }}
</f-footer>
</f-layout>
</f-layout>
</template>
<template v-else-if="currentNavigation === 'top'">
<f-header ref="headerRef" class="layout-header" :inverted="theme === 'dark'" :fixed="currentFixedHeaderRef">
<div class="layout-logo">
<img :src="logo" class="logo-img" /> <img :src="logo" class="logo-img" />
<div class="logo-name">{{ title }}</div> <div class="logo-name">{{ title }}</div>
</div> </div>
@ -60,7 +106,7 @@
:accordion="menuProps?.accordion" :accordion="menuProps?.accordion"
/> />
<div class="layout-header-custom"> <div class="layout-header-custom">
<slot name="customHeader"></slot> <slot name="renderCustom"></slot>
</div> </div>
<template v-if="locale"> <template v-if="locale">
<slot name="locale"></slot> <slot name="locale"></slot>
@ -75,28 +121,21 @@
</f-footer> </f-footer>
</f-layout> </f-layout>
</template> </template>
<template v-if="navigation === 'mixin'"> <template v-else-if="currentNavigation === 'mixin'">
<f-header v-if="routeLayout.header" ref="headerRef" class="layout-header" :fixed="currentFixedHeaderRef" :inverted="theme === 'dark'"> <f-header ref="headerRef" class="layout-header" :fixed="currentFixedHeaderRef" :inverted="theme === 'dark'">
<div v-if="routeLayout.logo" class="layout-logo"> <div class="layout-logo">
<img :src="logo" class="logo-img" /> <img :src="logo" class="logo-img" />
<div class="logo-name">{{ title }}</div> <div class="logo-name">{{ title }}</div>
</div> </div>
<div class="layout-header-custom"> <div class="layout-header-custom">
<slot name="customHeader"></slot> <slot name="renderCustom"></slot>
</div> </div>
<template v-if="locale"> <template v-if="locale">
<slot name="locale"></slot> <slot name="locale"></slot>
</template> </template>
</f-header> </f-header>
<f-layout :fixed="currentFixedHeaderRef" :style="headerStyleRef"> <f-layout :fixed="currentFixedHeaderRef" :style="headerStyleRef">
<f-aside <f-aside v-model:collapsed="collapsedRef" :fixed="isSidebarFixed" :width="`${sideWidth}px`" collapsible class="layout-aside">
v-if="routeLayout.sidebar"
v-model:collapsed="collapsedRef"
:fixed="isSidebarFixed"
:width="`${sideWidth}px`"
collapsible
class="layout-aside"
>
<Menu <Menu
class="layout-menu" class="layout-menu"
:menus="menus" :menus="menus"
@ -117,8 +156,12 @@
</f-layout> </f-layout>
</f-layout> </f-layout>
</template> </template>
<template v-else>
<f-main class="layout-main">
<router-view></router-view>
</f-main>
</template>
</f-layout> </f-layout>
<router-view v-else></router-view>
</template> </template>
<script> <script>
@ -186,72 +229,41 @@ export default {
menuProps: { menuProps: {
type: Object, type: Object,
}, },
switch: {
type: Object,
default() {
return {
logo: true,
sidebar: true,
header: true,
};
},
},
}, },
setup(props) { setup(props) {
const headerRef = ref(); const headerRef = ref();
const headerHeightRef = ref(0); const headerHeightRef = ref(0);
const collapsedRef = ref(false);
const route = useRoute();
const currentNavigation = computed(() => {
if (route.meta.layout && route.meta.layout.navigation !== undefined) {
return route.meta.layout.navigation;
}
return props.navigation;
});
const currentFixedHeaderRef = computed(() => props.isHeaderFixed || props.navigation === 'mixin');
const headerStyleRef = computed(() => (currentFixedHeaderRef.value ? { top: `${headerHeightRef.value}px` } : null));
const sideStyleRef = computed(() => {
const left = collapsedRef.value ? '48px' : `${props.sideWidth}px`;
return props.isSidebarFixed ? { left } : null;
});
onMounted(() => { onMounted(() => {
if (headerRef.value) { if (headerRef.value) {
headerHeightRef.value = headerRef.value.$el.offsetHeight; headerHeightRef.value = headerRef.value.$el.offsetHeight;
} }
}); });
const collapsedRef = ref(false);
const route = useRoute();
const routeLayout = computed(() => {
let config;
// meta layout true
const metaLayoutConfig = route.meta.layout === undefined ? true : route.meta.layout;
if (typeof metaLayoutConfig === 'boolean') {
config = metaLayoutConfig ? props.switch : false;
} else if (typeof metaLayoutConfig === 'object') {
config = { ...props.switch, ...metaLayoutConfig };
} else {
console.error('[plugin-layout]: meta layout must be object or boolean');
}
// query layout false
const routeQueryLayoutConfig = route.query.layout && JSON.parse(route.query.layout);
if (typeof routeQueryLayoutConfig === 'boolean') {
config = routeQueryLayoutConfig ? props.switch : false;
} else if (typeof routeQueryLayoutConfig === 'object') {
config = { ...config, ...routeQueryLayoutConfig };
} else if (routeQueryLayoutConfig !== undefined) {
console.error('[plugin-layout]: query layout must be object or boolean');
}
return config;
});
const currentFixedHeaderRef = computed(() => props.isHeaderFixed || props.navigation === 'mixin');
const headerStyleRef = computed(() => {
if (!routeLayout.value) return;
if (!routeLayout.value.header) return;
return currentFixedHeaderRef.value ? { top: `${headerHeightRef.value}px` } : null;
});
const sideStyleRef = computed(() => {
if (!routeLayout.value) return;
if (!routeLayout.value.sidebar) return;
const left = collapsedRef.value ? '48px' : `${props.sideWidth}px`;
return props.isSidebarFixed ? { left } : null;
});
return { return {
headerRef, headerRef,
headerHeightRef, headerHeightRef,
route, route,
routeLayout,
collapsedRef, collapsedRef,
currentFixedHeaderRef, currentFixedHeaderRef,
headerStyleRef, headerStyleRef,
sideStyleRef, sideStyleRef,
currentNavigation,
}; };
}, },
}; };
@ -262,6 +274,13 @@ export default {
.layout-main { .layout-main {
z-index: 0; z-index: 0;
} }
.flex-between {
display: flex;
flex-flow: column;
align-items: stretch;
justify-content: space-between;
min-height: 100%;
}
.layout-header { .layout-header {
display: flex; display: flex;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
@ -320,6 +339,15 @@ export default {
.layout-menu { .layout-menu {
margin-top: 24px; margin-top: 24px;
} }
.layout-aside-custom {
padding: 8px 16px;
}
.layout-aside-locale {
padding: 8px 16px;
}
&.is-collapsed { &.is-collapsed {
.layout-logo { .layout-logo {
justify-content: center; justify-content: center;

View File

@ -23,12 +23,7 @@ const Layout = defineComponent({
const localeShared = plugin.getShared('locale'); const localeShared = plugin.getShared('locale');
return () => { return () => {
const slots = { const slots = {
customHeader: () => { renderCustom: config.renderCustom,
if (config.renderHeader) {
return config.renderHeader();
}
return null;
},
locale: () => { locale: () => {
if (localeShared) { if (localeShared) {
return <localeShared.SelectLang></localeShared.SelectLang>; return <localeShared.SelectLang></localeShared.SelectLang>;

View File

@ -5,17 +5,9 @@
</div> </div>
<template #content> <template #content>
<FScrollbar height="274" class="lang-container"> <FScrollbar height="274" class="lang-container">
<div <div v-for="item in configs" :key="item.lang" :class="['lang-option', item.lang === locale && 'is-selected']" @click="handleSelect(item)">
v-for="item in configs" <span>{{ item.icon }}</span>
:key="item.lang" <span>{{ item.label }}</span>
:class="[
'lang-option',
item.lang === locale && 'is-selected'
]"
@click="handleSelect(item)"
>
<span>{{item.icon}}</span>
<span>{{item.label}}</span>
</div> </div>
</FScrollbar> </FScrollbar>
</template> </template>
@ -28,13 +20,14 @@ import { LanguageOutlined } from '@fesjs/fes-design/icon';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import langUConfigMap from '../langUConfigMap'; import langUConfigMap from '../langUConfigMap';
// eslint-disable-next-line import/extensions
import { locale as _locale } from '../core'; import { locale as _locale } from '../core';
export default { export default {
components: { components: {
FTooltip, FTooltip,
FScrollbar, FScrollbar,
LanguageOutlined LanguageOutlined,
}, },
setup() { setup() {
const { messages, locale } = useI18n(); const { messages, locale } = useI18n();
@ -57,9 +50,9 @@ export default {
handleSelect, handleSelect,
locale, locale,
configs, configs,
isOpened isOpened,
}; };
} },
}; };
</script> </script>
<style> <style>
@ -68,10 +61,10 @@ export default {
} }
</style> </style>
<style lang="less" scoped> <style lang="less" scoped>
.lang-icon { .lang-icon {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
margin: 0 8px; margin: 0 8px;
padding: 0 4px; padding: 0 4px;
cursor: pointer; cursor: pointer;

View File

@ -23,5 +23,5 @@ export const beforeRender = {
}; };
export const layout = { export const layout = {
renderHeader: () => <UserCenter />, renderCustom: () => <UserCenter />,
}; };

View File

@ -26,7 +26,7 @@ export const beforeRender = {
export const layout = (layoutConfig, { initialState }) => ({ export const layout = (layoutConfig, { initialState }) => ({
...layoutConfig, ...layoutConfig,
renderHeader: () => <UserCenter />, renderCustom: () => <UserCenter />,
menus: () => { menus: () => {
const menusRef = ref(layoutConfig.menus); const menusRef = ref(layoutConfig.menus);
watch( watch(

View File

@ -15,6 +15,6 @@ export default {
</script> </script>
<style lang="less"> <style lang="less">
.user-center { .user-center {
text-align: right; text-align: center;
} }
</style> </style>

View File

@ -6,7 +6,9 @@
{ {
"name": "pinia", "name": "pinia",
"title": "pinia", "title": "pinia",
"layout": false "layout": {
"navigation": null
}
} }
</config> </config>
<script> <script>