布局组件封装(具有展开折叠,全屏等效果)

一、设计

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>

效果图: 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值