Vue管理系统前端系列四组件拆分封装


组件封装

在上一篇记录中,首页中有太多的代码,为了避免代码的臃肿,需要对主要的功能模块拆分,来让代码看起来更简洁,且能进行复用。

拆分后还加了些小功能,加入了修改 title 的代码,修改方式参考vue 动态修改 title

还增加了当前请求的页面缓存,使用状态管理器处理。监听路由,保存到 state 中,来处理的。 如何监听可参考vue 计算属性和监听属性

完整效果图如下:

首页布局拆分后结构

拆分后的,布局结构图:

拆分后代码

布局最外层 index 代码,使用头部,侧边栏,主内容栏组成,代码如下:

<!-- 布局的首页 -->
<template>
    <div>
        <l-header></l-header>
        <l-aside></l-aside>
        <l-main></l-main>
    </div>
</template>
<script>
import LHeader from './components/header'
import LAside from './components/aside'
import LMain from './components/main'
export default {
    data() {
        return {}
    },
    //引入组件
    components: {
        LHeader,
        LAside,
        LMain,
    },
}
</script>
<style lang="scss" scoped></style>

头部 index.vue 代码:

<!-- 头部文件 -->
<template>
    <div class="header">
        <!-- logo -->
        <logo></logo>
        <!-- 折叠按钮 -->
        <hamburger></hamburger>
        <!-- 头部导航栏 -->
        <div class="heardNavBar">
            <el-menu default-active="1" class="el-menu-demo" background-color="#4b5f6e" text-color="#fff" active-text-color="#ffd04b" mode="horizontal">
                <el-menu-item index="1" @click="$router.push('/')">首页</el-menu-item>
                <el-menu-item index="2" @click="openUrl('#')">使用文档</el-menu-item>
                <el-menu-item index="3" @click="openUrl('//github.com/levy-w-wang/lion-ui')">GitHub</el-menu-item>
            </el-menu>
        </div>
        <!-- 右侧信息 -->
        <div style="float:right">
            <!-- 全屏 -->
            <div style="float:left;line-height: 60px; padding: 0 10px;">
                <i class="el-icon-full-screen" @click="toggleFull"></i>
            </div>
            <!-- 个人信息 -->
            <div class="userinfo">
                <el-dropdown trigger="hover">
                    <span class="el-dropdown-link userinfo-inner">
                        <img src="@assets/img/user.jpg" />
                        {{ $store.getters.userInfo.username }}<i class="el-icon-caret-bottom"></i>
                    </span>
                    <el-dropdown-menu slot="dropdown">
                        <el-dropdown-item>
                            <router-link to="/"><i class="el-icon-s-home"></i>首页</router-link>
                        </el-dropdown-item>
                        <el-dropdown-item>
                            <router-link to="/"><i class="el-icon-s-custom"></i>我的主页</router-link>
                        </el-dropdown-item>
                        <el-dropdown-item divided>
                            <a @click="loginOut()"><i class="el-icon-switch-button"></i>登出</a>
                        </el-dropdown-item>
                    </el-dropdown-menu>
                </el-dropdown>
            </div>
        </div>
    </div>
</template>

<script>
import screenfull from 'screenfull'
import hamburger from './hamburger'
import logo from './logo'
// import { mapState } from 'vuex'
export default {
    data() {
        return {}
    },
    computed: {
        // ...mapState({
        //     isCollapse: (state) => state.app.isCollapse,
        // }),
    },
    //引入组件
    components: {
        hamburger,
        logo,
    },
    // 方法
    methods: {
        openUrl(url) {
            window.open(url)
        },
        loginOut() {
            this.$confirm('确认退出吗?', '提示', {
                type: 'warning',
            })
                .then(() => {
                    this.$store.commit('logout')
                })
                .catch(() => {})
        },
        toggleFull() {
            if (!screenfull.isEnabled) {
                this.$message({
                    type: 'warning',
                    message: 'you browser can not work',
                })
                return false
            }
            screenfull.toggle()
        },
    },
    //未挂载DOM,不能访问ref为空数组
    //可在这结束loading,还做一些初始化,实现函数自执行,
    //可以对data数据进行操作,可进行一些请求,请求不易过多,避免白屏时间太长。
    created() {},
    //可在这发起后端请求,拿回数据,配合路由钩子做一些事情;可对DOM 进行操作
    mounted() {},
}
</script>

<style lang="scss" scoped>
.header {
    padding-left: 0px !important;
    height: 60px;
    line-height: 60px;
    width: 100%;
    background: #4b5f6e;
    color: #fff;

    .heardNavBar {
        float: left;
        background: #4b5f6e;
        padding: 0px 0px;
        height: 60px;
        line-height: 60px;
        font-size: 28px;
        cursor: pointer;
    }

    .userinfo {
        text-align: right;
        padding-right: 24px;
        float: right;
        padding: 0 10px;
        .userinfo-inner {
            font-size: 20px;
            cursor: pointer;
            color: #fff;
            img {
                width: 40px;
                height: 40px;
                border-radius: 10px;
                margin: 10px 0px 10px 10px;
                float: right;
            }
        }
    }
}
</style>

头部中引用的相关组件代码如下

折叠导航栏 hamburger 下的 index.vue 代码:

<template>
    <div @click="toggleCollapse">
        <svg :class="{ 'is-active': !isCollapse }" class="hamburger" viewBox="0 0 1024 1024" xmlns="//www.w3.org/2000/svg" width="64" height="64">
            <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
        </svg>
    </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
    name: 'Hamburger',
    computed: {
        ...mapState({
            isCollapse: (state) => state.app.isCollapse,
        }),
    },
    methods: {
        //折叠导航栏
        toggleCollapse: function () {
            this.$store.commit('toggleCollapse')
        },
    },
}
</script>

<style scoped>
.hamburger {
    padding-left: 13px;
    padding-right: 13px;
    text-align: center;
    width: 34px;
    height: 60px;
    line-height: 60px;
    float: left;
    cursor: pointer;
}

.is-active {
    transform: rotate(180deg);
}
</style>

折叠导航栏 logo 下的 index.vue 代码:

<!--  -->
<template>
    <div class="logo" :class="isCollapse ? 'logo-collapse-width' : 'logo-width'">
        <img v-if="isCollapse" src="@assets/logo6065.png" @click="$router.push('/')" />
        <img v-else src="@assets/logo.png" @click="$router.push('/')" />
    </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
    data() {
        return {}
    },
    computed: {
        ...mapState({
            isCollapse: (state) => state.app.isCollapse,
        }),
    },
}
</script>
<style lang="scss" scoped>
.logo {
    float: left;
    height: 60px;
    padding: 0;
    margin: 0;
}
.logo-width {
    width: 230px;
}
.logo-collapse-width {
    width: 65px;
}
</style>

侧边栏下的 index.vue代码:

<!-- aside -->
<template>
    <div class="aside-container" :class="isCollapse ? 'aside-collapse-width' : 'aside-width'">
        <!--导航菜单  default-active="1-1"-->
        <el-menu class="el-menu-vertical-demo" :class="isCollapse ? 'aside-collapse-width' : 'aside-width'" :collapse-transition="false" :unique-opened="true" :collapse="isCollapse">
            <el-submenu index="1">
                <template slot="title">
                    <i class="el-icon-setting"></i>
                    <span slot="title">系统管理</span>
                </template>
                <el-menu-item index="1-1" @click="$router.push('usermanage')">用户管理</el-menu-item>
                <el-menu-item index="1-2" @click="$router.push('menumanage')">菜单管理</el-menu-item>
            </el-submenu>
            <el-menu-item index="2" disabled>
                <i class="el-icon-magic-stick"></i>
                <span slot="title">导航一</span>
            </el-menu-item>
            <el-menu-item index="3" disabled>
                <i class="el-icon-reading"></i>
                <span slot="title">导航二</span>
            </el-menu-item>
        </el-menu>
    </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
    data() {
        return {}
    },
    //$store.getters.isCollapse
    computed: {
        ...mapState({
            isCollapse: (state) => state.app.isCollapse,
        }),
        mainTabs: {
            get() {
                return this.$store.state.app.mainTabs
            },
            set(val) {
                this.$store.commit('updateMainTabs', val)
            },
        },
        mainTabsActiveName: {
            get() {
                return this.$store.state.app.mainTabsActiveName
            },
            set(val) {
                this.$store.commit('updateMainTabsActiveName', val)
            },
        },
    },
    watch: {
        $route: 'handleRoute',
    },
    created() {
        console.log(this.$route)
        this.handleRoute(this.$route)
    },
    methods: {
        // 路由操作处理
        handleRoute(route) {
            // tab标签页选中, 如果不存在则先添加
            var tab = this.mainTabs.filter((item) => item.name === route.name)[0]
            if (!tab) {
                tab = {
                    name: route.name,
                    title: route.meta.title,
                    icon: route.meta.icon,
                }
                this.mainTabs = this.mainTabs.concat(tab)
            }
            this.mainTabsActiveName = tab.name
        },
    },
}
</script>
<style lang="scss" scoped>
.aside-container {
    position: fixed;
    top: 0px;
    left: 0;
    bottom: 0;
    z-index: 1020;
    .el-menu {
        position: absolute;
        top: 60px;
        bottom: 0px;
        text-align: left;
    }
}
.aside-width {
    width: 230px;
}
.aside-collapse-width {
    width: 65px;
}
</style>

内容模块下的 index.vue代码:

<!--  -->
<template>
    <div class="main-container clear" :class="isCollapse ? 'position-collapse-left' : 'position-left'">
        <!-- 标签页 -->
        <el-tabs class="tabs" :class="isCollapse ? 'position-collapse-left' : 'position-left'" v-model="mainTabsActiveName" :closable="true" type="card" @tab-click="selectedTabHandle" @tab-remove="removeTabHandle">
            <el-dropdown class="tabs-tools" :show-timeout="0" trigger="hover">
                <div style="font-size:20px;width:50px;">
                    <i class="el-icon-arrow-down"></i>
                </div>
                <el-dropdown-menu slot="dropdown">
                    <el-dropdown-item @click.native="tabsCloseCurrentHandle">关闭当前标签</el-dropdown-item>
                    <el-dropdown-item @click.native="tabsCloseOtherHandle">关闭其它标签</el-dropdown-item>
                    <el-dropdown-item @click.native="tabsCloseAllHandle">关闭全部标签</el-dropdown-item>
                    <el-dropdown-item @click.native="tabsRefreshCurrentHandle">刷新当前标签</el-dropdown-item>
                </el-dropdown-menu>
            </el-dropdown>
            <el-tab-pane v-for="item in mainTabs" :key="item.name" :label="item.title" :name="item.name">
                <span slot="label"> <i :class="item.icon"></i> {{ item.title }} </span>
            </el-tab-pane>
        </el-tabs>

        <!-- 主内容区域 -->
        <div class="main-content">
            <keep-alive>
                <transition name="fade" mode="out-in">
                    <router-view></router-view>
                </transition>
            </keep-alive>
        </div>
    </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
    data() {
        return {}
    },
    computed: {
        ...mapState({
            isCollapse: (state) => state.app.isCollapse,
        }),
        mainTabs: {
            get() {
                return this.$store.state.app.mainTabs
            },
            set(val) {
                this.$store.commit('updateMainTabs', val)
            },
        },
        mainTabsActiveName: {
            get() {
                return this.$store.state.app.mainTabsActiveName
            },
            set(val) {
                this.$store.commit('updateMainTabsActiveName', val)
            },
        },
    },
    methods: {
        // tabs, 选中tab
        selectedTabHandle(tab) {
            tab = this.mainTabs.filter((item) => item.name === tab.name)
            if (tab.length >= 1) {
                this.$router.push({ name: tab[0].name })
            }
        },
        // tabs, 删除tab
        removeTabHandle(tabName) {
            // 当只有首页时,不允许关掉。 若是其它页面可关掉后,push 首页进去
            if (this.mainTabs.length == 1 && this.mainTabs[0].name == 'index') {
                return
            }
            this.mainTabs = this.mainTabs.filter((item) => item.name !== tabName)
            if (this.mainTabs.length >= 1) {
                // 当前选中tab被删除
                if (tabName === this.mainTabsActiveName) {
                    this.$router.push({ name: this.mainTabs[this.mainTabs.length - 1].name }, () => {
                        this.mainTabsActiveName = this.$route.name
                    })
                }
            } else {
                this.$router.push('/')
            }
        },
        // tabs, 关闭当前
        tabsCloseCurrentHandle() {
            this.removeTabHandle(this.mainTabsActiveName)
        },
        // tabs, 关闭其它
        tabsCloseOtherHandle() {
            this.mainTabs = this.mainTabs.filter((item) => item.name === this.mainTabsActiveName)
        },
        // tabs, 关闭全部
        tabsCloseAllHandle() {
            this.mainTabs = []
            this.$router.push('/')
        },
        // tabs, 刷新当前
        tabsRefreshCurrentHandle() {
            var tempTabName = this.mainTabsActiveName
            this.removeTabHandle(tempTabName)
            this.$nextTick(() => {
                this.$router.push({ name: tempTabName })
            })
        },
    },
}
</script>
<style lang="scss" scoped>
.main-container {
    padding: 0 5px 5px;
    position: absolute;
    top: 60px;
    left: 1px;
    right: 1px;
    bottom: 0px;
    .tabs {
        position: fixed;
        top: 60px;
        right: 50px;
        padding-left: 0px;
        padding-right: 2px;
        z-index: 1020;
        height: 40px;
        line-height: 40px;
        font-size: 14px;
        background: rgb(255, 253, 255);
        border-color: rgba(200, 206, 206, 0.5);
        // border-left-width: 1px;
        // border-left-style: solid;
        border-bottom-width: 1px;
        border-bottom-style: solid;
    }
    .tabs-tools {
        position: fixed;
        top: 60px;
        right: 0;
        z-index: 1020;
        height: 40px;
        // padding: 0 10px;
        font-size: 14px;
        line-height: 40px;
        cursor: pointer;
        border-color: rgba(200, 206, 206, 0.5);
        border-left-width: 1px;
        border-left-style: solid;
        border-bottom-width: 1px;
        border-bottom-style: solid;
        background: rgba(255, 255, 255, 1);
    }
    .tabs-tools:hover {
        background: rgba(200, 206, 206, 1);
    }
    .main-content {
        position: absolute;
        top: 45px;
        left: 5px;
        right: 5px;
        bottom: 5px;
        padding: 5px;
        // background: rgba(209, 212, 212, 0.5);
    }
}
.position-left {
    left: 230px;
}
.position-collapse-left {
    left: 65px;
}
</style>

状态管理中添加 app 模块

代码如下:

export default {
    state: {
        // 是否折叠导航栏
        isCollapse: false,
        // 访问页集合
        mainTabs: [],
        // 当前访问页名
        mainTabsActiveName: '',
    },
    getters: {
        isCollapse: (state) => {
            return state.isCollapse
        },
    },
    mutations: {
        toggleCollapse(state) {
            state.isCollapse = !state.isCollapse
        },
        updateMainTabs(state, tabs) {
            state.mainTabs = tabs
        },
        updateMainTabsActiveName(state, name) {
            state.mainTabsActiveName = name
        },
    },
    actions: {},
}

当然还有一些小的调整点,可参考 git 上的提交版本 首页组件拆分

Tags: