Vue + ElementUI 实现后台管理系统模板 -- 前端篇(九):使用 iframe 标签嵌套页面 及内容顶部tab页签

一、使用 iframe 标签嵌套页面

1、简单了解一下 

(1)什么是 iframe?
  iframe 标签会创建一个行内框架(包含另一个文档的内联框架)。
  简单地理解:页面中嵌套另一个页面。

(2)使用场景?
  有的项目需求,需要在当前页面中显示外部网页,比如访问csdn、查看接口文档等,此时就可以使用 iframe 标签,嵌套一个页面。

 (3)简单使用一下 修改common下的content.vue

<template>
  <el-main class="content">
    <!-- <keep-alive>
      <router-view />
    </keep-alive> -->
    <iframe src="https://passport.csdn.net/newlogin?code=mobile" frameborder="0" width="100%" :height="clientHeight"></iframe>
  </el-main>
</template>

<script>
export default {
  name: 'Content',
  data() {
    return {
      clientHeight: '',
    };
  },
  mounted() {
    setTimeout(() => {
      this.clientHeight = document.querySelector('.content').clientHeight - 40;
    }, 100);
  },
};
</script>

 

2、项目中使用 

1)实现效果
 点击一个菜单项,会跳转到相应的组件,并显示不同的内容。
 若是自身模块,则使用 router-view 显示,若是外部网页,则使用 iframe 显示。 

2)思路:
  由于菜单内容的显示通过路由跳转完成,不同的菜单需要不同的显示效果,所以可以使用 router 的 meta,定义相关路由元信息。
路由元信息:
  iframeUrl : 表示 url,其中 以 http 或者 https 开头的 url 使用 iframe 标签展示。

3)实现
Step1:在路由中添加一个路由元信息,并新增一个路由用于测试 iframe 使用(CSDN)。
其中:
  iframeUrl 用于表示 url,使用 http 或者 https 开头的 url 使用 iframe 标签展示

{
        path: '/demo/iframe',
        name: 'Iframe',
        meta: {
          isRouter: true,
          iframeUrl: 'https://passport.csdn.net/newlogin?code=mobile'
        }
      }

 

 Step2:修改common下的content.vue

 <iframe v-if="$route.meta.iframeUrl != '' && $route.meta.iframeUrl != undefined" :src="$route.meta.iframeUrl" width="100%" :style="{ height: clientHeight + 'px' }" frameborder="0" scrolling="yes">
    </iframe>
    <div v-else class="card" shadow="hover">
      <keep-alive>
        <router-view />
      </keep-alive>
    </div>
<script>
mounted() {
    setTimeout(() => {
      this.clientHeight = document.querySelector('.content').clientHeight - 40;
    }, 100);
  },
</script>

  Step3:菜单 aside.vue增加新菜单

<el-menu-item index="demo-iframe" @click="$router.push({ name: 'Iframe' })">
              <i class="el-icon-document"></i>
              <span slot="title">iframe</span>
            </el-menu-item>

 Step4:测试

二、内容顶部tab页签

1、简单介绍

点击菜单栏时,在内容区显示打开的历史页 方便再次查看

2、项目使用

1)实现效果
 每点击一个菜单项,在内容区会显示一个标签页,
 点击不同的标签页,会跳转到相应的组件或者iframe,显示不同的内容。 

2)思路:
  由于涉及到组件间数据的交互,所以使用 vuex 维护状态。侧边栏(Aside.vue)选中菜单项时,相关数据被修改,而 内容区(Content.vue)根据 相关数据进行展示。
需要维护的数据:
  需要一个数组,用于保存点击的菜单项(标签属性、url、标题等)。
  需要两个字符串,一个用于保存当前菜单选中项,一个保存当前标签选中项。

  由于菜单内容的显示通过路由跳转完成,不同的菜单需要不同的显示效果,所以可以使用 router 的 meta,定义相关路由元信息。
路由元信息:
  isTab: 表示可以显示为标签页。
        title:标签标题

3)实现
Step1:在路由中修改路由元信息 都加上isTab及title属性例如:(注意哦 要tab显示的菜单都要加上哦,其他同理

{
        path: '/demo/ueditor',
        name: 'Ueditor',
        component: () => import('@/views/ueditor/index.vue'),
        meta: {
          title: '编辑',
          isRouter: true,
          isTab: true
        }
      },

Step2:使用 vuex 维护几个必要的状态。
其中:
  menuActiveName 表示侧边栏选中的菜单项的名
  mainTabs 表示标签页数据,数组
  mainTabsActiveName 表示标签页中选中的标签名

       如下,在store文件夹下的modules新建 common.js 并进行相关定义。

export default {
  // 开启命名空间(防止各模块间命名冲突),访问时需要使用 模块名 + 方法名
  namespaced: true,
  // 管理数据(状态)
  state: {
    // 表示侧边栏选中的菜单项的名
    menuActiveName: '',
    // 表示标签页数据,数组
    mainTabs: [],
    // 表示标签页中选中的标签名
    mainTabsActiveName: ''
  },
  // 更改 state(同步)
  mutations: {
    updateMenuActiveName(state, name) {
      state.menuActiveName = name
    },
    updateMainTabs(state, tabs) {
      state.mainTabs = tabs
    },
    updateMainTabsActiveName(state, name) {
      state.mainTabsActiveName = name
    },
    resetState(state) {
      let stateTemp = {
        menuActiveName: '',
        mainTabs: [],
        mainTabsActiveName: ''
      }
      Object.assign(state, stateTemp)
    }
  },
  // 异步触发 mutations
  actions: {
    updateMenuActiveName({
      commit,
      state
    }, name) {
      commit("updateMenuActiveName", name)
    },
    updateMainTabs({
      commit,
      state
    }, tabs) {
      commit("updateMainTabs", tabs)
    },
    updateMainTabsActiveName({
      commit,
      state
    }, name) {
      commit("updateMainTabsActiveName", name)
    },
    resetState({
      commit,
      state
    }) {
      commit("resetState")
    }
  }
}

 Step3:在store下index.js

import Vue from 'vue'
import Vuex from 'vuex'
import common from './modules/common.js'
import user from './modules/user.js'
import getters from './getters'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules: {
    common,
    user
  },
  getters
})

  

 Step4:在侧边栏中(aside.vue),引入 menuActiveName 、 mainTabs、mainTabsActiveName 以及其相关的修改方法。对其进行操作。

import { mapState, mapActions } from 'vuex';

  computed: {
    ...mapState('common', ['menuActiveName', 'mainTabs']),
  },
  methods: {
    ...mapActions('common', ['updateMenuActiveName', 'updateMainTabs', 'updateMainTabsActiveName']),
  },

 Step4:
  在侧边栏中(aside.vue),监视 $route 的变化,路由发生变化后,就会触发。
  每次点击 菜单项,均会触发 路由的跳转,所以监听 $route 的变化,变化时可以进行相关操作。
如下:
  监视路由的变化,路由发生改变后,侧边栏菜单项选中状态需要修改到选中位置。
  根据路由元信息判断,如果可以显示为标签页,则处理标签页相关规则,否则直接跳过。
  标签页规则:
  使用 数组 保存标签页信息,如果当前选中的菜单项 未保存在 数组中,则向数组中添加该标签信息并修改当前选中的标签页名,若已存在,则直接修改当前选中的标签页名。
  标签页信息:
    name 表示路由名
              title 表示标签名
    params、query 表示参数(路由需要的参数)
    type 表示显示类型, iframe 表示使用 iframe 标签显示
    iframeUrl 表示 url,默认为 ‘’

watch: {
    // 监视是否折叠侧边栏,折叠则宽度为 64px。
    foldAside(val) {
      this.asideWidth = val ? '200px' : '64px';
      this.iconSize = val;
    },
    // 监视路由的变化,每次点击菜单项时会触发
    $route(route) {
      // 路由变化时,修改当前选中的菜单项
      this.updateMenuActiveName(route.name);
      // 是否显示标签页
      if (route.meta.isTab) {
        // 判断当前标签页数组中是否存在当前选中的标签,根据标签名匹配
        let tab = this.mainTabs.filter((item) => item.name === route.name)[0];
        // 若当前标签页数组不存在该标签,则向数组中添加标签
        console.log('watch route tab', tab, 'route', route);
        if (!tab) {
          // 设置标签页数据
          tab = {
            name: route.name,
            title: route.meta.title,
            params: route.params,
            query: route.query,
            type: isURL(route.meta.iframeUrl) ? 'iframe' : 'module',
            iframeUrl: route.meta.iframeUrl || '',
          };
          // 将数据保存到标签页数组中
          this.updateMainTabs(this.mainTabs.concat(tab));
        }
        // 保存标签页中当前选中的标签名
        this.updateMainTabsActiveName(route.name);
      }
    },
  },

  Step5:上面的 isURL 是封装的一个方法,此处抽取到一个公用 js 中。文件新建在根目录下的utils中名为index.js

/**
 * URL地址
 * @param {*} s
 */
export function isURL(s) {
  return /^http[s]?:\/\/.*/.test(s)
}

Step6:完整aside.vue 修改的地方比较多

<template>
  <div>
    <!-- 系统 Logo -->
    <el-aside class="header-logo" :width="asideWidth">
      <div @click="$router.push({ name: 'Home' })">
        <a v-if="foldAside">CMS管理系统</a>
        <a v-else>CMS</a>
      </div>
    </el-aside>
    <el-aside class="aside" :width="asideWidth" :class="'icon-size-' + iconSize">
      <el-scrollbar style="height: 100%; width: 100%;">
        <!--
            default-active 表示当前选中的菜单,默认为 home。
            collapse 表示是否折叠菜单,仅 mode 为 vertical(默认)可用。
            collapseTransition 表示是否开启折叠动画,默认为 true。
            background-color 表示背景颜色。
            text-color 表示字体颜色。
        -->
        <el-menu :default-active="menuActiveName || 'home'" :collapse="!foldAside" :collapseTransition="false" background-color="#263238" text-color="#8a979e">
          <el-menu-item index="home" @click="$router.push({ name: 'Home' })" style="width:200px">
            <i class="el-icon-s-home"></i>
            <span slot="title">首页</span>
          </el-menu-item>

          <el-submenu index="demo" style="width:200px">
            <template slot="title">
              <i class="el-icon-star-off"></i>
              <span>demo</span>
            </template>
            <el-menu-item index="demo-echarts" @click="$router.push({ name: 'Echarts' })">
              <i class="el-icon-s-data"></i>
              <span slot="title">echarts</span>
            </el-menu-item>
            <el-menu-item index="demo-ueditor" @click="$router.push({ name: 'Ueditor' })">
              <i class="el-icon-document"></i>
              <span slot="title">ueditor</span>
            </el-menu-item>
            <el-menu-item index="demo-iframe" @click="$router.push({ name: 'Iframe' })">
              <i class="el-icon-document"></i>
              <span slot="title">iframe</span>
            </el-menu-item>
          </el-submenu>
        </el-menu>
      </el-scrollbar>
    </el-aside>
  </div>
</template>

<script>
import { mapActions, mapState } from 'vuex';
import { isURL } from '@/utils/index.js';
export default {
  name: 'Aside',
  data() {
    return {
      foldAside: true,
      // 保存当前选中的菜单
      // menuActiveName: 'home',
      // 保存当前侧边栏的宽度
      asideWidth: '200px',
      // 用于拼接当前图标的 class 样式
      iconSize: 'true',
    };
  },
  computed: {
    ...mapState('common', ['menuActiveName', 'mainTabs']),
  },
  watch: {
    // 监视是否折叠侧边栏,折叠则宽度为 64px。
    foldAside(val) {
      this.asideWidth = val ? '200px' : '64px';
      this.iconSize = val;
    },
    // 监视路由的变化,每次点击菜单项时会触发
    $route(route) {
      // 路由变化时,修改当前选中的菜单项
      this.updateMenuActiveName(route.name);
      // 是否显示标签页
      if (route.meta.isTabs) {
        // 判断当前标签页数组中是否存在当前选中的标签,根据标签名匹配
        let tab = this.mainTabs.filter((item) => item.name === route.name)[0];
        // 若当前标签页数组不存在该标签,则向数组中添加标签
        console.log('watch route tab', tab, 'route', route);
        if (!tab) {
          // 设置标签页数据
          tab = {
            name: route.name,
            title: route.meta.title,
            params: route.params,
            query: route.query,
            type: isURL(route.meta.iframeUrl) ? 'iframe' : 'module',
            iframeUrl: route.meta.iframeUrl || '',
          };
          // 将数据保存到标签页数组中
          this.updateMainTabs(this.mainTabs.concat(tab));
        }
        // 保存标签页中当前选中的标签名
        this.updateMainTabsActiveName(route.name);
      }
    },
  },
  methods: {
    ...mapActions('common', ['updateMenuActiveName', 'updateMainTabs', 'updateMainTabsActiveName']),
  },
};
</script>

<style>
.aside {
  margin-bottom: 0;
  height: 100%;
  max-height: calc(100% - 50px);
  width: 100%;
  max-width: 200px;
  background-color: #263238;
  text-align: left;
  right: 0;
}

.header-logo {
  background-color: #17b3a3;
  text-align: center;
  height: 50px;
  line-height: 50px;
  width: 200px;
  font-size: 24px;
  color: #fff;
  font-weight: bold;
  margin-bottom: 0;
  cursor: pointer;
}
.el-submenu .el-menu-item {
  max-width: 200px !important;
}
.el-scrollbar__wrap {
  overflow-x: hidden !important;
}
.icon-size-false i {
  font-size: 30px !important;
}
.icon-size-true i {
  font-size: 18px !important;
}
</style>

Step7:新建contentTabs.vue。

<template>
  <!--
        el-tabs 用于显示标签页,
        其中:
            v-model 绑定当前选中的 标签
            :closable = true 表示当前标签可以关闭
            @tab-click 绑定标签选中事件
            @tab-remove 绑定标签删除事件
    -->
  <div class="content_tabs">
    <!-- mainTabs{{ mainTabs }} -->
    <el-tabs v-model="mainTabsActiveName" class="tab" :closable="true" @tab-click="selectedTabHandle" @tab-remove="removeTabHandle">
      <!-- 循环遍历标签数组,用于生成标签列表 -->
      <el-tab-pane v-for="item in mainTabs" :key="item.name" :label="item.name" :name="item.name">
        <div class="content_wrapper" :style="{ height: clientHeight + 'px' }" shadow="hover">
          <div class="con">
            <!-- 以 http 或者 https 开头的地址,均使用 iframe 进行展示 -->
            <iframe
              v-if="$route.meta.iframeUrl != '' && $route.meta.iframeUrl != undefined"
              :src="item.iframeUrl"
              width="100%"
              :style="{ height: clientHeight + 'px' }"
              frameborder="0"
              scrolling="yes"
            >
            </iframe>
            <!-- 自身组件模块路由跳转,使用 router-view 表示 -->
            <keep-alive v-else>
              <div>ddodo</div>
              <router-view />
            </keep-alive>
          </div>
        </div>
      </el-tab-pane>
      <!-- 定义下拉框,用于操作标签列表 -->
      <el-dropdown class="dropdown-tool" :show-timeout="0">
        <i class="el-icon-arrow-down"></i>
        <el-dropdown-menu slot="dropdown">
          <el-dropdown-item @click.native="closeCurrentTabsHandle">关闭当前标签页</el-dropdown-item>
          <el-dropdown-item @click.native="closeOtherTabsHandle">关闭其它标签页</el-dropdown-item>
          <el-dropdown-item @click.native="closeAllTabsHandle">关闭全部标签页</el-dropdown-item>
          <el-dropdown-item @click.native="refreshCurrentTabs">刷新当前标签页</el-dropdown-item>
        </el-dropdown-menu>
      </el-dropdown>
    </el-tabs>
  </div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
export default {
  name: 'contentTabs',
  data() {
    return {
      clientHeight: '',
    };
  },
  computed: {
    ...mapState('common', ['mainTabs']),
    mainTabsActiveName: {
      get() {
        return this.$store.state.common.mainTabsActiveName;
      },
      set(val) {
        this.updateMainTabsActiveName(val);
      },
    },
  },
  mounted() {
    setTimeout(() => {
      this.clientHeight = document.querySelector('.tab').clientHeight - 100;
    }, 100);
  },
  methods: {
    ...mapActions('common', ['updateMainTabs', 'updateMainTabsActiveName']),
    //     【selectedTabHandle:】
    // 选中事件处理很简单,首先找到选中的标签页,然后路由跳转即可,
    // 由于 Aside.vue 中,已经监听了 $route,所以路由一变化,就会进行相关处理(修改 vuex 的三个值)。
    // 注:
    //     选中已选中的标签时,由于是同一个路由,路由($route)不变化,
    //     若想实现变化,可以见后面的 refreshCurrentTabs 方法处理。

    // 处理标签选中事件
    selectedTabHandle(tab) {
      // 选择某个标签,标签存在于标签数组时,则跳转到相应的路由(根据名字跳转)
      tab = this.mainTabs.filter((item) => item.name === tab.name)[0];
      if (tab) {
        // 已经在 Aside.vue 中使用 watch 监视了 $route,所以一旦路由变化,其就可以感知到,从而维护 vuex 状态。
        this.$router.push({
          name: tab.name,
          query: tab.query,
          params: tab.params,
          title: tab.title,
        });
      }
      console.log(tab);
    },

    // 【removeTabHandle】
    // 移除事件,只要从 标签列表 中找到选中的标签移除即可。
    // 若标签列表没有数据,则跳转到首页。
    // 若移除的标签是当前选中的标签,则移除后跳转到最后一个标签页。

    // 处理标签删除事件
    removeTabHandle(tabName) {
      // 从 mainTabs 中删除标签即可
      this.updateMainTabs(this.mainTabs.filter((item) => item.name !== tabName));
      // 如果当前 mainTabs 中仍有值,则进行当前选中标签逻辑处理
      if (this.mainTabs.length > 0) {
        // 如果删除的是当前选中的标签,则默认选择最后一个标签
        let tab = this.mainTabs[this.mainTabs.length - 1];
        if (tabName === this.mainTabsActiveName) {
          this.$router.push({
            name: tab.name,
            query: tab.query,
            params: tab.params,
          });
        }
      } else {
        // 如果当前 mainTabs 中没有值,则跳转到 HomePage 主页面
        this.updateMainTabsActiveName('');
        this.$router.push({ name: 'HomePage' });
      }
    },

    // 【closeCurrentTabsHandle、closeOtherTabsHandle、closeAllTabsHandle】
    // 直接操作 标签列表 mainTabs 即可。
    // 关闭所有列表后,需要跳转到首页。

    // 关闭当前标签
    closeCurrentTabsHandle() {
      this.removeTabHandle(this.mainTabsActiveName);
    },
    // 关闭其他标签
    closeOtherTabsHandle() {
      this.updateMainTabs(this.mainTabs.filter((item) => item.name === this.mainTabsActiveName));
    },
    // 关闭所有标签
    closeAllTabsHandle() {
      // 清空 mainTabs 数组,并跳转到 主页面
      this.updateMainTabs([]);
      // 如果当前 mainTabs 中没有值,则跳转到 HomePage 主页面
      this.updateMainTabsActiveName('');
      this.$router.push({ name: 'HomePage' });
    },

    // 【refreshCurrentTabs:】
    // 由于同一个路由跳转时, $route 不会变化,即 watch 失效。
    // 想要实现刷新效果,可以先移除标签,再添加标签,并重新跳转。

    // 刷新当前选中的标签
    refreshCurrentTabs() {
      // 用于保存当前标签数组
      let tabs = [];
      Object.assign(tabs, this.mainTabs);
      // 保存当前选中的标签
      let tab = this.mainTabs.filter((item) => item.name === this.mainTabsActiveName)[0];
      // 先移除标签
      this.removeTabHandle(tab.name);
      this.$nextTick(() => {
        // 移除渲染后,再重新添加标签数组,并跳转路由
        this.updateMainTabs(tabs);
        this.$router.push({
          name: tab.name,
          query: tab.query,
          params: tab.params,
        });
      });
    },
  },
};
</script>
<style scoped>
.content_tabs {
  padding: -15px -20px 10px -20px;
  box-sizing: border-box;
  width: 100%;
}
.tab /deep/ .content_wrapper {
  overflow-y: hidden;
}
.tab /deep/ .el-tabs__header {
  margin: 0 !important;
  padding-left: 10px;
}
.content_wrapper {
  overflow-y: scroll;
}
.con {
  padding: 15px;
  box-sizing: border-box;
}
.dropdown-tool {
  float: left;
  position: fixed !important;
  right: 10px;
  width: 30px;
  height: 30px;
  line-height: 30px;
  top: 70px;
  background-color: #f1f4f5;
}
</style>

Step8:修改content.vue根据路由元信息中istab字段判断是否显示tabs。引入contentTabs.vue

<template>
  <el-main class="content">
    <ContentTabs v-if="$route.meta.isTabs"></ContentTabs>
    <div v-else class="card" shadow="hover">
      <keep-alive>
        <router-view />
      </keep-alive>
    </div>
  </el-main>
</template>

<script>
import ContentTabs from './myContentTabs';
export default {
  name: 'Content',
  components: {
    ContentTabs,
  },
  data() {
    return {};
  },
  mounted() {},
};
</script>

<style>
.el-main {
  padding: 15px !important;
}
.content {
  background-color: #fff;
}
</style>

Step9:测试

页面刷新时保存 state 数据

由于 使用 vuex 维护了数据,页面一刷新,state 数据会变化,就会出现很诡异的效果。
如下图:
选中了标签后,但是一刷新页面,数据相关效果就会变得很奇怪。

解决方法:在页面刷新之前,将 state 信息保存,页面刷新后,再将该值赋给 state。

在 Home.vue 中添加如下代码:
  使用 localStorage 保存 state 信息(也可以使用 sessionStorage)。

created() {
    //在页面加载时读取localStorage里的状态信息
    if (localStorage.getItem("store") ) {
        this.$store.replaceState(Object.assign({}, this.$store.state,JSON.parse(localStorage.getItem("store"))))
    }

    //在页面刷新时将vuex里的信息保存到localStorage里
    window.addEventListener("beforeunload",()=>{
        localStorage.setItem("store",JSON.stringify(this.$store.state))
    })
}

  当然,为了防止登录时获取到上一个用户保存的 state 值,需要在 登录时将其移除。
如下:
  在 vuex 中定义一个重置数据的方法,并在登录页面创建时调用。

// 更改 state(同步)
mutations: {
    resetState(state) {
        let stateTemp = {
            language: 'zh',
            menuActiveName: '',
            mainTabs: [],
            mainTabsActiveName: ''
        }
        Object.assign(state, stateTemp)
    }
},
// 异步触发 mutations
actions: {
    resetState({commit, state}) {
        commit("resetState")
    }
}

在 登录页面 引入并调用。

 

...mapActions('common', {resetState: "resetState"})

created() {
    // 进入画面前,移除主页面保存的 state 信息
    localStorage.removeItem("store")
    this.resetState()
}

 

  • 3
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hongc93

感谢鼓励 继续航行

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值