一、设计
1、布局组件包含3部分,layout、content、menu
2、使用tsx、vue-property-decorator、antdesign封装
二、XLayout
(1)作为顶级父元素(此处不支持多层layout嵌套)
(2)可以通过layout获取menu和content(XLayout.Menu和XLayout.Content)
(3)使用默认插槽
(4)内部存储子元素列表以及子元素折叠map
(5)使用provide将自己传给子组件
(5)提供方法初始化和改变子元素折叠的map
import { Component, Provide } from 'vue-property-decorator';
import { Component as TsxComponent } from 'vue-tsx-support';
import { LayoutContext } from './type';
import XLayoutMenu from './menu';
import XLayoutContent from './content';
interface XLayoutProps {}
interface XLayoutEvents {}
@Component
export default class XLayout extends TsxComponent<XLayoutProps, XLayoutEvents> {
//menu和content
static Menu = XLayoutMenu;
static Content = XLayoutContent;
//将自己传给子元素
@Provide(LayoutContext.Symbol)
get layout() {
return this;
}
// 子元素列表
childrenList: Vue[] = [];
//子元素折叠map
collapseMap: Record<number, boolean> = {};
//改变子元素折叠
handleCollapseChange(index: number) {
this.collapseMap[index] = !this.collapseMap[index];
}
//初始化子元素折叠map
updateSlot() {
this.collapseMap = this.childrenList.reduce(
(map, slot, index) => {
// map[index] = map[index] || false;
map[index] = false;
return map;
},
{ ...this.collapseMap },
);
}
//使用默认插槽渲染
render() {
return <div class="x-layout">{this.$slots.default}</div>;
}
}
三、XContent
(1)在created的时候将自己添加到layout的childrenList里并更新和初始化layout里子元素折叠的map(当有多层其他组件嵌套的时候仍然没问题)
(2)在beforeDestroy的时候将自己从layout的childrenList中移除(保证layout的childrenList中元素列表是正确的,因为可能存在页面变化内部子元素会切换,旧的出现未清除的情况)
(3)collapse:是否折叠以及折叠箭头方向(true:右,false:左)
(4)contentCollapse:内容是否折叠(由后一个子元素决定,即后一个子元素的collapse为true)
(5)通过inject获取layout
(4)内部控制是否全屏
(6)提供头部左中右和内容插槽用于自定义
import { Component, Inject, Prop } from 'vue-property-decorator';
import { Component as TsxComponent, modifiers } from 'vue-tsx-support';
import { LayoutContext } from './type';
import Layout from './layout';
import { ATooltip } from '../../ant-design-vue';
import { Icon as XIcon } from '@triascloud/x-components';
interface XContentProps {
fullScreen?: boolean;
collapseArrow?: boolean;
}
interface XContentEvents {}
@Component
export default class XContent extends TsxComponent<
XContentProps,
XContentEvents
> {
@Inject(LayoutContext.Symbol) layout!: Layout;
@Prop({ default: true, type: Boolean }) fullScreen?: boolean;
@Prop({ default: true, type: Boolean }) collapseArrow?: boolean;
contentFullScreen = false;
//在layout中的位置
get index() {
return this.layout.childrenList.findIndex(slot => slot === this);
}
//是否折叠
get collapse() {
return this.layout.collapseMap[this.index];
}
//内容是否折叠
get contentCollapse() {
return this.layout.collapseMap[this.index + 1];
}
locale(key: string, data?: Record<string, string>) {
return data
? (this.$t(key, data, 'package') as string)
: (this.$t(key, 'package') as string);
}
//更新折叠
handleCollapseChange() {
this.layout.handleCollapseChange(this.index);
}
//全屏切换
handleFullScreen() {
this.contentFullScreen = !this.contentFullScreen;
}
//在layout中childrenList清空自己
beforeDestroy() {
this.layout.childrenList.splice(this.index, 1);
}
//在layout中childrenList添加自己并初始化折叠元素的map
created() {
this.layout.childrenList.push(this);
this.layout.updateSlot();
}
render() {
return (
<div
class={[
'x-layout-content',
{
'x-layout-content--collapse': this.contentCollapse,
'x-layout-content-fullScreen': this.contentFullScreen,
},
]}
>
{!this.contentFullScreen ? (
<div class="x-layout-content-header">
<div class="x-layout-content-header-left">
{this.collapseArrow ? (
<XIcon
type={this.collapse ? 'right' : 'left'}
onClick={modifiers.stop(this.handleCollapseChange)}
class="x-layout-content-header-arrow"
/>
) : null}
{this.$slots['header-left']}
</div>
{this.$slots['header-center'] && (
<div class="x-layout-content-header-center">
{this.$slots['header-center']}
</div>
)}
{this.$slots['header-right'] && (
<div class="x-layout-content-header-right">
{this.$slots['header-right']}
</div>
)}
</div>
) : null}
{this.fullScreen ? (
<ATooltip placement="bottomRight" onClick={this.handleFullScreen}>
<span slot="title">
<span>
{this.contentFullScreen ? this.locale('common.dropOut') : ''}
{this.locale('common.fullscreen')}
</span>
</span>
<XIcon
class={[
'x-layout-content-fullScreen-icon',
{
'x-layout-content-fullScreen-icon-overturn': this
.contentFullScreen,
},
]}
type="tc-icon-full-screen"
/>
</ATooltip>
) : null}
<div class="x-layout-content-main">{this.$slots.content}</div>
</div>
);
}
}
四、XMenu
(1)折叠原理和content一致
(2)使用antdesign的menu封装
import { Component, Inject, Prop, Emit } from 'vue-property-decorator';
import { Component as TsxComponent } from 'vue-tsx-support';
import { LayoutContext, MenuData, MenuItem } from './type';
import Layout from './layout';
import { AMenu, ASubMenu, AMenuItem } from '../../ant-design-vue';
import RouterLinkFallback from '../router-link-fallback';
interface XLayoutMenuProps {
menus: MenuData[];
defaultOpenKeys: Array<string>;
defaultSelectedKey: string;
value: string;
}
interface XLayoutMenuEvents {
menuClick: { key: string };
}
interface XLayoutMenuScopeSlots {
menu: MenuData;
}
@Component
export default class XLayoutMenu extends TsxComponent<
XLayoutMenuProps,
XLayoutMenuEvents,
XLayoutMenuScopeSlots
> {
@Inject(LayoutContext.Symbol) layout!: Layout;
@Prop({ default: () => [] }) menus!: MenuData[];
@Prop({ default: () => [] }) defaultOpenKeys!: string[];
@Prop({ default: '' }) defaultSelectedKey!: string;
@Prop({ default: '' }) value!: string;
get index() {
return this.layout.childrenList.findIndex(slot => slot === this);
}
get contentCollapse() {
return this.layout.collapseMap[this.index + 1];
}
get computedDefaultOpenKeys() {
const result = this.menus.find(
item =>
item.children &&
item.children.some(
children =>
children.path === this.defaultSelectedKey ||
children.path === this.value,
),
);
return result
? [...this.defaultOpenKeys, result.path]
: this.defaultOpenKeys;
}
get computedSelectedKey() {
return this.value ? [this.value] : [];
}
created() {
this.layout.childrenList.push(this);
this.layout.updateSlot();
}
beforeDestroy() {
this.layout.childrenList.splice(this.index, 1);
}
@Emit('menuClick')
handleMenuClick(data: MenuItem) {
this.$emit('input', data.key);
return data;
}
renderMenuItem(menuItem: MenuData) {
if (menuItem.children) {
return (
<ASubMenu key={menuItem.path} title={menuItem.title}>
{menuItem.children.map(item => this.renderMenuItem(item))}
</ASubMenu>
);
}
return (
<AMenuItem key={menuItem.path}>
<RouterLinkFallback to={menuItem.path} target={menuItem.target}>
{this.$scopedSlots.menu
? this.$scopedSlots.menu(menuItem)
: menuItem.title}
</RouterLinkFallback>
</AMenuItem>
);
}
render() {
return (
<div
class={[
'x-layout-menu',
{ 'x-layout-menu--collapse': this.contentCollapse },
]}
>
<div class="x-layout-menu-header">
<div class="x-layout-menu-header-left">
{this.$slots['header-left']}
</div>
<div class="x-layout-menu-header-right">
{this.$slots['header-right']}
</div>
</div>
<div class="x-layout-menu-main">
<AMenu
mode="inline"
onClick={this.handleMenuClick}
default-selected-keys={[this.defaultSelectedKey]}
default-open-keys={this.computedDefaultOpenKeys}
selectedKeys={this.computedSelectedKey}
>
{this.menus.map(item => this.renderMenuItem(item))}
</AMenu>
</div>
</div>
);
}
}
五、使用
<x-layout class="x-layout">
<x-layout-menu
class="x-layout-menu"
:menus="menus"
:value="$route.path"
@menuClick="handleMenuClick"
>
<template v-slot:header-left>
<span>我的待办</span>
</template>
<template v-slot:header-right>
<a-icon type="file-search" />
</template>
</x-layout-menu>
<x-layout-content class="x-layout-content">
<template v-slot:header-left>
<span>我的待办</span>
</template>
<template v-slot:header-right>
<a-icon type="file-search" />
</template>
<template v-slot:content>
<span>content</span>
</template>
</x-layout-content>
<x-layout-content>
<template v-slot:header-left>
<span>表单标题</span>
</template>
<template v-slot:header-right>
<a-icon type="file-search" />
</template>
<template v-slot:content>
<span>content</span>
</template>
</x-layout-content>
</x-layout>
效果图: