ant-design-vue3.x首页多页签支持

经过上一篇文档ant-design-vue3.x应用框架页面开发的学习,我们准备了一个基本的首页框架页面,只是这个页面还缺少多页签的支持,本编文档将继续完善框架,添加多页签支持。
要支持多页签,需要解决这个问题:ant-design-vue的Tabs组件的内容部分仅支持输出文本内容(如果理解有误,请批评指正),不支持嵌套组件,而且vue也不建议采用iframe嵌入其他页面。因此我们采用只使用页签头而隐藏页签体,用<router-view>替换页签体,在页签切换时配合使用router编程式路由,以达到一个页签对应一个路由的目的。同时为了缓存组件,使用<keep-alive>标签缓存组件的输出,配合使用include属性,在页签关闭后删除缓存。
这就是加入多页签支持后要解决的核心问题,其他细节都在代码中进行了说明。

1、创建Tabs.vue

文件内容如下:

<script setup>
import { ref, defineProps } from 'vue';
import { useRouter } from 'vue-router';

const emit = defineEmits(["removeCachePageId", "addCachePageId"]);//定义可调用的父组件方法名
const activedKeyIdArray = [];//存放曾经激活tab页的key,用于删除时计算应该激活的tab页
const panes = ref([{
  key: '0',
  title: '首页',
  closable: false,
  path: '/'
}]); //页签数组
const activeKey = ref(panes.value[0].key);//当前激活的tab页的key
activedKeyIdArray.push(panes.value[0].key);
emit('addCachePageId', "Welcome"); //缓存首页组件名称
const $router = useRouter(); //全局路由对象
const add = (paneObj) => { //添加tab页签,选中侧边栏菜单项时调用
  $router.push(paneObj.path);
  let oneTab = getTabByKey(paneObj.key);
  if (oneTab == null) { //页签不存在,创建页签
    emit('addCachePageId', paneObj.path.substring(1));//调用父组件函数,加入组件缓存列表
    setActiveedKeyId(paneObj.key); 
    panes.value.push(paneObj);
  } else {
    setActiveedKeyId(oneTab.key);
  }
}

const remove = targetKey => { //关闭页签函数
  /*
  let lastIndex = 0;
  panes.value.forEach((pane, i) => {
    if (pane.key === targetKey) {
        lastIndex = i - 1;
    }
  });*/
  emit('removeCachePageId', getTabByKey(targetKey).path.substring(1));//删除组件列表,要求组件名称和和路由path一致(例如path=/page1,组件名称由应该时page1。也可以使用路由定义中的name,只要这个值和组件的<script setup name="xxx">标签中name的值保持一致即可)
  panes.value = panes.value.filter(pane => pane.key !== targetKey);

  if (activeKey.value === targetKey) {//关闭的是当前激活的页页签,找到最近一次激活的页签并激活
    let loopFlag = true;
    while(loopFlag) {
      let keyId = activedKeyIdArray.pop();
      if (keyId != targetKey) {
        let oneTab = getTabByKey(keyId)
        if (oneTab != null) {
          setActiveedKeyId(oneTab.key);
          $router.push(oneTab.path);
          break;
        }
      }
    }
  }

  /*
  if (panes.value.length && activeKey.value === targetKey) {
    if (lastIndex >= 0) {
        activeKey.value = panes.value[lastIndex].key;
        activedKeyIdArray.push(panes.value[lastIndex].key);
        $router.push(panes.value[lastIndex].path);
    } else {
        activeKey.value = panes.value[0].key;
        activedKeyIdArray.push(panes.value[0].key);
        $router.push(panes.value[0].path);
    }
  }*/
};

const onEdit = (targetKey, action) => {
  if (action === 'add') {
    add();
  } else {
    remove(targetKey);
  }
};

const selectedTab = (activeKeyId) => { //选择某页签时激活该页签
  let oneTab = getTabByKey(activeKeyId);
  if (oneTab != null) {
    setActiveedKeyId(oneTab.key);
    $router.push(oneTab.path);
  }
};

const getTabByKey = (keyId) => { //根据key获得页签对象
  let rtnValue = null;
  for (let index = 0;index < panes.value.length;index++) {
    if (panes.value[index].key == keyId) {
      rtnValue = panes.value[index];
      break;
    }
  }
  return rtnValue;
};

const setActiveedKeyId = (keyId) => {
  activeKey.value = keyId;
  activedKeyIdArray.push(keyId);
};

defineExpose({ add });//定义可被父组件调用的组件
</script>
<template>
  <a-tabs v-model:activeKey="activeKey" type="editable-card" @edit="onEdit" :hideAdd="true" @change="selectedTab">
    <a-tab-pane v-for="pane in panes" :key="pane.key" :tab="pane.title" :closable="pane.closable">
    </a-tab-pane>
  </a-tabs>
</template>
<style scoped>
:deep(div.ant-tabs-nav-list div.ant-tabs-tab) {
}
:deep(div.ant-tabs-content-holder) {
  display: none;/*隐藏标签页内容,标签页内容由<router-view>内容替代*/
}
:deep(div.ant-tabs-nav) {
  padding: 2px 2px 0 2px;
  margin: 0;
}
/*
:deep(div.ant-tabs-tab) {
    background-color: #ededed;
}*/
</style>

对于源码中部分跨组件共享的数据,比如页签定义数组panes、保存页签激活顺序的栈activedKeyIdArray等,可以采用vuex保存,这样就可以避免跨组件调用函数或共享数据。关于vuex的使用是后话。

2、修改Layout.vue

文件内容如下:

<script setup>
import { ref } from 'vue';
import sysmenu from './SysMenu.vue';
import tabs from './Tabs.vue';
const cacheNamesList = ref([]); //需要缓存的组件的name集合,与组件的<script set name="xxx">中的name对应
const isCollapsed = ref(false);
const switchCollapsed = () => {
  isCollapsed.value = !isCollapsed.value;
};
const addTab = ref();
function addTabByMenu(tabInfo) {
    addTab.value.add(tabInfo);
}
function removeCachePageId(removedId) { //页签关闭前调用,删除需要缓存的组件名称
  cacheNamesList.value = cacheNamesList.value.filter(pageId => pageId !== removedId);
}
function addCachePageId(addedId) { //添加页签前时调用,缓存对应组件名称
  cacheNamesList.value.push(addedId);
}
</script>
<template>
  <a-layout>
    <a-layout-header><span style="color: white;">Header</span></a-layout-header>
    <a-layout>
      <a-layout-sider v-model:collapsed="isCollapsed">
        <sysmenu @addTabByMenu="addTabByMenu"></sysmenu>
      </a-layout-sider>
      <a-layout-content>
        <tabs ref="addTab" @removeCachePageId="removeCachePageId" @addCachePageId="addCachePageId"></tabs><!--自定义触发时间,以备子组件调用-->
        <div class="mainDiv">
          <router-view v-slot="{ Component }">
            <keep-alive :include="cacheNamesList"><!--缓存组件的代码,注意与VUE2.x的区别-->
              <component :is="Component"/>
            </keep-alive>
          </router-view>
        </div>
      </a-layout-content>
    </a-layout>
  </a-layout>
</template>

<style scoped>
section.ant-layout {
    height: 100%;
    background-color: white;
}
.mainDiv {
  height: calc(100% - 42px);/*计算值,42px为顶部header的高度*/
  overflow: auto;
  padding: 0 2px;
}
</style>

3、修改SysMenu.vue

文件内容如下:


<script setup>
import { ref } from 'vue';
import { PieChartOutlined, MailOutlined } from '@ant-design/icons-vue';

const SubMenu = {
  name: 'SubMenu',
  props: {
    menuInfo: {
      type: Object,
      default: () => ({}),
    },
  },
  template: `
    <a-sub-menu :key="menuInfo.key">
      <template #icon><MailOutlined /></template>
      <template #title>{{ menuInfo.title }}</template>
      <template v-for="item in menuInfo.children" :key="item.key">
        <template v-if="!item.children">
          <a-menu-item :key="item.key">
            <template #icon>
              <PieChartOutlined />
            </template>
            {{ item.title }}
          </a-menu-item>
        </template>
        <template v-else>
          <sub-menu :menu-info="item" :key="item.key" />
        </template>
      </template>
    </a-sub-menu>
  `,
  components: {
    PieChartOutlined,
    MailOutlined,
  },
};
const list = [{
  key: '0',
  title: '首页',
  path: '/'
}, {
  key: '1',
  title: '页面1',
  path: '/page1'
}, {
  key: '2',
  title: '页面2',
  path: '/page2'
}, {
  key: '3',
  title: '页面3',
  path: '/page3'
}, {
  key: '4',
  title: '页面4',
  path: '/page4'
}, {
  key: '5',
  title: '页面5',
  path: '/page5'
}, {
  key: '6',
  title: '页面6',
  path: '/page6'
}, {
  key: '7',
  title: '页面7',
  path: '/page7'
}, {
  key: '8',
  title: '页面8',
  path: '/page8'
}]; //菜单项对应路由及相关vue文件请自行增加
const selectedKeys = ref([]);//处于选中状态的节点key列表
const openKeys = ref([]);//处于展开状态的节点key列表
const emit = defineEmits(["addTabByMenu"]);
function selectedMenuNode(nodeInfo) {
  for (let index = 0;index < list.length;index++) {
    if (list[index].key == nodeInfo.key) {
      emit('addTabByMenu', list[index]); //调用父组件,添加页签
      break;
    }
  }
}
</script>
<template>
    <a-menu v-model:openKeys="openKeys" v-model:selectedKeys="selectedKeys" mode="inline" theme="dark" @select="selectedMenuNode">
      <template v-for="item in list" :key="item.key">
        <template v-if="!item.children">
          <a-menu-item :key="item.key">
            <template #icon>
              <PieChartOutlined />
            </template>
            {{ item.title }}
          </a-menu-item>
        </template>
        <template v-else>
          <sub-menu :key="item.key" :menu-info="item" />
        </template>
      </template>
    </a-menu>
</template>
<style scoped>

</style>

至此,多页签支持完成。这里需要注意,在<router-view>中嵌入的路由对一个的页面,建议页尽量在<template>标签后页用一个根标签包裹所有元素,否则可能在页签切换过程中,导致部分组件缓存错乱,造成组件缓存错乱。

上一篇:ant-design-vue3.x应用框架页面开发

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容