Vue 电商PC后台管理(ElementUI)

2022-07-28,,,,

Vue 电商PC后台管理(ElementUI)

1.项目概述

1.1电商项目基本业务概述

根据不同的场景,电商系统一般都提供了PC端、移动 APP、移动 Web、微信小程序等多种终端访问方式。

1.2电商后台管理系统的功能

电商后台 管理系统同用于管理用户账号、商品分类、商品信息、订单、数据统计等业务功能。

1.3电商后台管理系统的开发模式(前后端分离)

电商后台管理系统整体采用前后端分离的开发模式,其中前端项目是基于Vue技术栈的SPA项目

1.4电商后台管理系统的技术选型

1.前端项目技术栈

  • Vue
  • Vue-router
  • Element-UI
  • Axios
  • Echarts

2.后端项目技术栈

  • Node.js
  • Express
  • Jwt
  • Mysql
  • Sequelize

2.项目初始化

2.1前端项目初始化步骤

① 安装vue脚手架

② 通过 vue 脚手架创建项目

③ 配置 vue 路由

④ 配置 Element-UI 组件库

⑤ 配置 axios 库

⑥ 初始化 git 远程仓库

⑦ 将本地项目托管到 Github 或码云中

2.2后台项目的环境安装配置

① 安装MySQL数据库

② 安装Node.js 环境

③ 配置项目相关信息

④ 启动项目

⑤ 使用Postman 测试后台项目接口是否正常

3.登录/退出功能

3.1登录概述

1.登录业务流程

① 在登录页面输入用户名和密码

② 调用后台接口进行验证

③ 通过验证之后,根据后台的响应状态跳转到项目主页

2.登录业务的相关技术点

  • http是无状态的
  • 通过cookie在客户端记录状态
  • 通过session在服务器端记录状态
  • 通过token方式维持状态

3.2登录 —— token 原理分析

3.3登录功能实现

1.登录页面的布局

通过Element-UI组件实现布局

  • el-form
  • el-form-item
  • el-input
  • el-button
  • 字体图标

首先打开 vue-shop 项目

  • 查看文件,文件夹在工作区,暂存区的状态
git status
  • 创建并切换 login 分支
git checkout -b login
  • 查看所有分支
git branch

2.Login.vue 文件解读

这是一个 Vue 单文件组件

祖传 Vue 布局如下

<template>
	<div>
        
    </div>
</template>

<script>
	export default {
        
    }
</script>


<style lang="less" scope>

</style>

我们需要在 <template> 标签中进行布局

<script> 标签中编写用户行为和数据定义

<style> 标签中定义样式

我用到了 ElementUI 库,主要是记录一下这个库的用法

先放上源码,我会对下面的代码进行解读

<template>
<div class="login_container">
    <div class="login_box">
        <!-- 头像区域 -->
        <div class="avatar_box">
            <img src="../assets/logo.png" alt="">
    	</div>
            <!-- 登录表单区域 -->
            <el-form :model="loginForm" :rules="loginFormRules" ref="loginFormRef" label-width="0px" class="login_form">
                <!-- 用户名 -->
                <el-form-item prop="username">
                    <el-input v-model="loginForm.username" prefix-icon="iconfont icon-user"></el-input>
                </el-form-item>
                <!-- 密码 -->
                <el-form-item prop="password">
                    <el-input v-model="loginForm.password" type="password" prefix-icon="iconfont icon-password"></el-input>
                </el-form-item>
                <!-- 按钮区域 -->
                <el-form-item class="btns">
                    <el-button type="primary" @click="login">登录</el-button>
                    <el-button type="info" @click="resetLoginForm">重置</el-button>
                </el-form-item>
            </el-form>
    	</div>
    </div>       
</template>

<script>
    export default {
        data() {
            return {
                // 这是登录表单的数据绑定对象
                loginForm: {
                    username: "",
                    password: ""
                },
                // 这是登录表单验证
                loginFormRules: {
                    username: [
                        { required: true, message: '请输入登录名称', trigger: 'blur' },
                        { min: 3, max: 10, message: '长度在 3 到 10 个字符', trigger: 'blur' }
                    ],
                    password: [
                        { required: true, message: '请输入登录密码', trigger: 'blur' },
                        { min: 6, max: 16, message: '长度在 6 到 16 个字符', trigger: 'blur' }
                    ]
                }
            }
        },
        methods: {
            resetLoginForm(){
                // 对整个表单进行重置,将所有字段值重置为初始值并移除校验结果
                this.$refs.loginFormRef.resetFields()
            },
            login(){
                this.$refs.loginFormRef.validate((valid)=>{
                    console.log(valid);
                })
            }
        }
    }
</script>

<style lang="less" scope>
    .login_container {
        background: #2b4b6b;
        height: 100%;
        position: relative;
    }
    .login_box {
        width: 450px;
        height: 300px;
        background: #ffffff;
        border-radius: 3px;
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
    }
    .avatar_box {
        width: 130px;
        height: 130px;
        border: solid 1px #eee;
        border-radius: 50%;
        background: #ffffff;
        padding: 10px;
        box-shadow: 0 0 10px #ddd;
        position: absolute;
        left: 50%;
        transform: translate(-50%, -50%);
        img {
            width: 100%;
            height: 100%;
            border-radius: 50%;
            background: #eee;
        }
    }

    .login_form {
        position: absolute;
        bottom: 0;
        width: 100%;
        padding: 0 10px;
        box-sizing: border-box;
    }
    .btns {
        display: flex;
        justify-content: flex-end;
    }
</style>

<el-form> 标签中添加 :model 属性,这是登录表单的数据绑定对象,Ta需要和 <el-input> 标签中的 v-module 配合使用

:rules 是登录表单验证,Ta需要在 JS 区域的 data 中返回一个登录表单验证对象,其中的属性为需要验证的内容,如何使用?在 el-form-item 标签中新增 prop 属性,使它的值为刚刚在 data 中定义的属性值

<el-input> 标签中的 prefix-icon 属性是用来设置前置字体图标的,我引用的阿里矢量图库,也可以通过 suffix-icon 属性在 input 组件的尾部增加显示图标。

<el-form> 表单中的 ref 属性是为当前表单注册一个实例对象,Ta的属性名就是这个实例对象的名称,比如说我自定义的名称为 loginFormRef ,那我就可以通过 this.$refs.loginFormRef 来访问到这个实例对象。

resetFields 可以通过 this.$refs.loginFormRef.resetFields() 调用此方法,此方法的作用是:对整个表单进行重置,将所有字段值重置为初始值并移除校验结果。

validate 可以通过 this.$refs.loginFormRef.validate() 调用此方法,此方法的作用是:对整个表单进行校验的方法,参数为一个回调函数。该回调函数会在校验结束后被调用,并传入两个参数:是否校验成功和未通过校验的字段。若不传入回调函数,则会返回一个 promise。Function(callback: Function(boolean, object))

3.路由导航守卫控制访问权限

如果用户没有登录,但是直接通过URL访问特定页面,需要重新导航到登录页面

// 为路由对象,添加 beforeEach 导航守卫
router.beforeEach((to, from, next) => {
    // 如果用户访问的登录页,直接放行
    if (to.path === "/login") return next()
    // 从 sessionStorage 中获取到保存的 token 值
    const tokenStr = window.sessionStorage.getItem("token")
    // 没有 token ,强制跳转到登录页
    if (!tokenStr) return next("/login")
    next()
})

3.4退出

退出功能实现原理

基于token 的方式实现退出比较简单,只需要销毁本地的token即可。这样,后续的请求就不会携带token,必须重新登录生成一个新的token之后才可以访问页面。

// 清空token
window.sessionStorage.clear()
// 跳转到登录页
this.$router.push('/login')

解决 eslintrc 格式化报错问题

在项目中新建 .prettierrc 文件,文件内容如下

{
	"semi": false,
    "singleQuote": true
}

“semi”: false 表示移除分号(😉

“singleQuote”: true 表示用单引号(’’)来表示字符串

最后在每个需要修改的文件中使用快捷键 Ctrl+Alt+\ 进行格式化文档

.eslintrc.js --> rules 下新建一条命令

"space-before-function-paren": 0

3.5将本地代码提交到码云上

  • 查看文件,文件夹在工作区,暂存区的状态
git status

此时提示你的是红色信息

它会提示你 Changes not staged for commit: 尚未提交

  • 把所有的文件都添加到暂存区
git add .

把所有的文件都添加到暂存区之后继续使用 git status ,提示信息就变成绿色了

  • 把暂存区的所有代码提交到本地仓库中
git commit -m "完成了登录功能"
  • 查看当前所在分支
git branch

当前分支是 login ,我们需要把 login 中的所有代码合并到 master 主分支中

注意:你需要先切换到 master 分支之后再合并 login

  • 切换到 master 分支
git checkout master

出现以下提示说明成功

Switched to branch ‘master’
Your branch is up to date with ‘origin/master’.

  • 如果没有成功删除 .gitignore 暂存文件,重新从第一步开始

再使用 git branch 可以看到已经切换到了 master 主分支

我的 .gitignore 文件内容如下:

node_modules
  • 基于 master 合并 login 中的所有文件
git merge login
  • 把本地仓库推送到远程仓库
git push

但是我们发现码云中只要 master 一个分支,并没有 login 分支

所有我们需要将 login 也推送到码云中

  • 首先切换到 login 分支
git checkout login
  • 把 login 分支推送到码云
git push -u origin login

-u 是指定推送 login 到码云

4.主页布局

4.1整体布局

先上下划分,再左右划分

<el-container class="home-container">
    <!-- 头部区域 -->
    <el-header>
        <div>
            <img src="https://cdn.jsdelivr.net/gh/extheor/images/%E7%A0%81%E5%B0%8F%E4%BD%99.jpg" alt="">
            <span>码小余后台管理</span>
        </div>
        <el-button type="info" @click="goBack">退出</el-button>
    </el-header>
    <!-- 页面主题区域 -->
    <el-container>
        <!-- 侧边栏 -->
        <el-aside width="200px">Aside</el-aside>
        <!-- 右侧内容主体 -->
        <el-main>Main</el-main>
    </el-container>
</el-container>

4.2左侧菜单布局

<el-menu background-color="#333744" text-color="#fff" active-text-color="#ffd04b">
    <!-- 一级菜单 -->
    <el-submenu index="1">
        <!-- 一级菜单的模板区域 -->
        <template slot="title">
<!-- 图标 -->
<i class="el-icon-location"></i>
<!-- 文本 -->
<span>导航一</span>
        </template>
        <!-- 二级菜单 -->
        <el-menu-item index="1-4-1">
            <!-- 图标 -->
            <i class="el-icon-location"></i>
            <!-- 文本 -->
            <span>导航一</span>
        </el-menu-item>
    </el-submenu>  
</el-menu>

4.3通过接口获取菜单数据

通过 axios 请求拦截器添加 token,保证拥有获取数据的权限。

// axios请求拦截
axios.interceptors.request.use((config) => {
  // console.log(config);
  config.headers.Authorization = window.sessionStorage.getItem("token");
  return config;
});

Home.vue 文件解读

<template>
    <el-container class="home-container">
        <!-- 头部区域 -->
        <el-header>
            <div>
                <img src="https://cdn.jsdelivr.net/gh/extheor/images/%E7%A0%81%E5%B0%8F%E4%BD%99.jpg" alt="">
                <span>码小余后台管理</span>
        </div>
            <el-button type="info" @click="goBack">退出</el-button>
        </el-header>
        <!-- 页面主体区域 -->
        <el-container>
            <!-- 侧边栏 -->
            <el-aside :width="isCollapse ? '64px' : '200px'">
                <div class="toggle-button" @click="toggleCollapse"><span>|||</span></div>
                <!-- 
                    :unique-opened - 表示是否只显示一个一级菜单
                    :collapse - 表示是否折叠左侧菜单
                    :collapse-transition - 是否开启默认折叠动画效果
                    :router - 是否开启前端路由
                    :default-active - 对当前点击的菜单高亮显示
                -->
                <el-menu background-color="#333744" text-color="#fff" active-text-color="#409EFF" :unique-opened="true" :collapse="isCollapse" :collapse-transition="false" :router="true" :default-active="activePath">
                    <!-- 一级菜单 -->
                    <el-submenu :index="item.id + ''" v-for="item in menuList" :key="item.id">
                        <!-- 一级菜单的模板区域 -->
                        <template slot="title">
                            <!-- 图标 -->
                            <i :class="iconObj[item.id]"></i>
                            <!-- 文本 -->
                            <span>{{ item.authName }}</span>
                        </template>
                        <!-- 二级菜单 -->
                        <el-menu-item :index="subItem.path" v-for="subItem in item.children" :key="subItem.id" @click="saveNavState(subItem.path)">
                            <!-- 图标 -->
                            <i class="el-icon-menu"></i>
                            <!-- 文本 -->
                            <span>{{ subItem.authName }}</span>
                        </el-menu-item>
                    </el-submenu>  
                </el-menu>
            </el-aside>
            <!-- 右侧内容主体 -->
            <el-main>
                <!-- 路由占位符 -->
                <router-view></router-view>
            </el-main>
        </el-container>
    </el-container>
</template>

<script>
    export default {
        data(){
            return {
                menuList: [],
                iconObj: {
                    "125": "iconfont icon-yonghuguanli",
                    "103": "iconfont icon-quanxianguanli",
                    "101": "iconfont icon-shangpinguanli",
                    "102": "iconfont icon-dingdanguanli",
                    "145": "iconfont icon-shujutongji"
                },
                // 是否折叠
                isCollapse: false,
                // 点击二级菜单高亮
                activePath: ""
            }
        },
        // 页面刷新的操作
        created(){
            this.getMenuList()
            this.activePath = window.sessionStorage.getItem("activePath")
        },
        methods: {
            goBack(){
                window.sessionStorage.clear()
                this.$router.push("/login")
            },
            // 获取所有的菜单
            async getMenuList(){
                const { data: res } = await this.$http.get("menus")
                if(res.meta.status !== 200) return this.$message.error(res.meta.msg)
                this.menuList = res.data
                console.log(this.menuList);
            },
            toggleCollapse(){
                this.isCollapse = !this.isCollapse
            },
            saveNavState(activePath){
                window.sessionStorage.setItem("activePath", activePath)
                this.activePath = activePath
            }
        }
    }
</script>

<style lang="less" scope>
    .home-container {
        height: 100%;
    }
    .el-header {
        background-color: #373d41;
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 0 !important;

        > div {
            height: 100%;
            color: #fff;
            font-size: 20px;
            display: flex;
            align-items: center;
            // padding: 0;

            img {
                height: 100%;
                border-radius: 50%;
                margin-right: 10px;
            }
        }
    }
    .el-aside {
        background-color: #333744;
        .el-menu {
            border: 0;
        }
        .toggle-button {
            color: #ffffff;
            height: 50px;
            > span {
                display: block;
                position: absolute;
                transform: rotate(-90deg) translateY(9px);
                transform-origin: 150%;
                cursor: pointer;
            }

            &:hover {
                background-color: #292C36;
            }
        }
    }
    .el-main {
        background-color: #eaedf1;
    }

    .iconfont {
        font-weight: 800;
        margin-right: 10px;
    }
</style>

User.vue 文件解读

<template>
    <div>
        <!-- 面包屑导航 -->
        <el-breadcrumb separator-class="el-icon-arrow-right">
            <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
            <el-breadcrumb-item>用户管理</el-breadcrumb-item>
            <el-breadcrumb-item>用户列表</el-breadcrumb-item>
        </el-breadcrumb>
        <!-- Card卡片视图区域 -->
        <el-card class="box-card">
            <!-- 搜索与添加区域 -->
            <el-row :gutter="20">
                <el-col :span="8">
                    <!-- 
                        :clearable - 是否可清空
                        @clear - 在点击由 clearable 属性生成的清空按钮时触发
                     -->
                    <el-input placeholder="请输入内容" v-model="queryInfo.query" :clearable="true" @clear="getUserList">
                        <el-button slot="append" icon="el-icon-search" @click="getUserList"></el-button>
                    </el-input>
                </el-col>
                <el-col :span="4">
                    <el-button type="primary" @click="addDialogVisible = true">添加用户</el-button>
                </el-col>
            </el-row>

            <!-- 
                用户列表区域
                border - 表格样式
                stripe - 表格条纹
             -->
            <el-table :data="userList" border stripe>
                <el-table-column type="index"></el-table-column>
                <el-table-column prop="username" label="姓名"></el-table-column>
                <el-table-column prop="email" label="邮箱"></el-table-column>
                <el-table-column prop="mobile" label="电话"></el-table-column>
                <el-table-column prop="role_name" label="角色"></el-table-column>
                <el-table-column label="状态">
                    <template v-slot="scope">
                        <!-- {{ scope.row }} -->
                        <el-switch v-model="scope.row.mg_state" @change="userStateChanged(scope.row)"></el-switch>
                    </template>
                </el-table-column>
                <el-table-column label="操作" width="180px">
                    <template>
                        <!-- 修改按钮 -->
                        <el-tooltip class="item" effect="dark" content="修改角色" placement="top" :enterable="false">
                            <el-button type="primary" icon="el-icon-edit" size="mini"></el-button>
                        </el-tooltip>
                        <!-- 删除按钮 -->
                        <el-tooltip class="item" effect="dark" content="删除角色" placement="top" :enterable="false">
                            <el-button type="danger" icon="el-icon-delete" size="mini"></el-button>
                        </el-tooltip>
                        <!-- 分配角色按钮 -->
                        <el-tooltip class="item" effect="dark" content="分配角色" placement="top" :enterable="false">
                            <el-button type="warning" icon="el-icon-setting" size="mini"></el-button>
                        </el-tooltip>  
                    </template>
                </el-table-column>
            </el-table>

            <!-- 
                分页区域
                @size-change - 选择页数的列表发生切换,返回最新的一页显示多少条
                @current-change - 页码值发生切换,返回最新的页数
                :current-page - 当前显示的是第几页的数据
                :page-sizes - 选择页数的下拉列表
                :page-size - 每页显示多少条数据
                layout - 需要展示的功能组件
                :total - 总条数(目前有 bug ,只能写成固定的数字才能正常显示)
             -->
            <el-pagination
                @size-change="handleSizeChange"
                @current-change="handleCurrentChange"
                :current-page="queryInfo.pagenum"
                :page-sizes="[1, 2, 5, 10]"
                :page-size="queryInfo.pagesize"
                layout="total, sizes, prev, pager, next, jumper"
                :total="4">
            </el-pagination>
        </el-card>

        <!-- 
            添加用户Dialog对话框
            :visible.sync - 控制添加对话框的显示与隐藏
            -->
        <el-dialog
            title="提示"
            :visible.sync="addDialogVisible"
            width="50%"
            @close="addDialogClosed"
            >
            <!-- 内容主体区域 -->
            <el-form ref="addFormRef" :model="addForm" :rules="addFormRules" label-width="70px">
                <el-form-item label="用户名" prop="username">
                    <el-input v-model="addForm.username"></el-input>
                </el-form-item>
                <el-form-item label="密码" prop="password">
                    <el-input type="password" v-model="addForm.password"></el-input>
                </el-form-item>
                <el-form-item label="邮箱" prop="email">
                    <el-input type="email" v-model="addForm.email"></el-input>
                </el-form-item>
                <el-form-item label="手机" prop="mobile">
                    <el-input v-model="addForm.mobile"></el-input>
                </el-form-item>
            </el-form>
            <!-- 底部区域 -->
            <span slot="footer" class="dialog-footer">
                <el-button @click="addDialogVisible = false">取 消</el-button>
                <el-button type="primary" @click="addDialogVisible = false">确 定</el-button>
            </span>
        </el-dialog>
    </div>
</template>

<script>
export default {
    data(){
        // 验证邮箱的规则
        var checkEmail = (rule, value, cb) => {
            // 验证邮箱的正则表达式
            const regEmail = /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+/

            if(regEmail.test(value)){
                // 合法的邮箱
                return cb()
            }
            cb(new Error("请输入合法的邮箱"))
        }

        // 验证手机号的规则
        var checkMobile = (rule, value, cb) => {
            // 验证手机号的正则表达式
            const regMobile = /^(0|86|17951)?(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$/

            if(regMobile.test(value)){
                // 合法的邮箱
                return cb()
            }
            cb(new Error("请输入合法的手机号"))
        }

        return {
            // 获取用户列表的参数对象
            queryInfo: {
                query: "",
                // 当前的页数
                pagenum: 1,
                // 当前每页显示多少条数据
                pagesize: 2
            },
            userList: [],
            total: 0,
            // 控制添加用户对话框的显示与隐藏
            addDialogVisible: false,
            // 添加用户的表单数据
            addForm: {
                username: "",
                password: "",
                email: "",
                mobile: ""
            },
            // 添加表单的验证规则对象
            addFormRules: {
                username: [
                    { required: true, message: '请输入用户名', trigger: 'blur' },
                    { min: 3, max: 10, message: '长度在 3 到 10 个字符', trigger: 'blur' }
                ],
                password: [
                    { required: true, message: '请输入密码', trigger: 'blur' },
                    { min: 3, max: 15, message: '长度在 3 到 15 个字符', trigger: 'blur' }
                ],
                email: [
                    { required: true, message: '请输入邮箱', trigger: 'blur' },
                    { validator: checkEmail, trigger: 'blur' }
                ],
                mobile: [
                    { required: true, message: '请输入手机号', trigger: 'blur' },
                    { validator: checkMobile, trigger: 'blur' }
                ]
            }
        }
    },
    created(){
        this.getUserList()
    },
    methods: {
        async getUserList(){
            const {data: res} = await this.$http.get("users", {params: this.queryInfo})
            // console.log(res);
            if(res.meta.status !== 200) return this.$message.error(res.meta.msg)
            this.userList = res.data.users
            this.total = res.total
        },
        // 监听 pagesize 改变的事件
        handleSizeChange(newSize){
            console.log(newSize);
            this.queryInfo.pagesize = newSize
            // pagesize 发生变化需要重新发起 axios 请求来展示数据
            this.getUserList()
        },
        // 监听 页码值 改变的事件
        handleCurrentChange(newPage){
            console.log(newPage);
            this.queryInfo.pagenum = newPage
            this.getUserList()
        },
        async userStateChanged(userinfo){
            // console.log("userinfo:",userinfo);
            const {data: res} = await this.$http.put(`users/${userinfo.id}/state/${userinfo.mg_state}`)
            console.log(res);
            if(res.meta.status !== 200){
                userinfo.mg_state = !userinfo.mg_state
                return this.$message.error("更新用户状态失败!")
            }
            this.$message.success("更新用户状态成功!")
        },
        addDialogClosed(){
            this.$refs.addFormRef.resetFields()
        }

    }
}
</script>

<style lang="less" scope>
    
</style>

权限管理

权限管理业务分析

通过权限管理模块控制不同的用户可以进行哪些操作,具体可以通过角色的方式进行控制,即每个用户分配一个特定的角色,角色包括不同的功能权限。

比如说:王者荣耀,你充钱的玩家有皮肤,不充钱的玩家没有皮肤…

项目要点

如何实现一个好看的3级 UI 结构

<!-- 角色列表区域 -->
<el-table
   :data="rolesList"
   width="100%"
   border
   stripe
   >
   <!-- 
       展开列
       type="expand" - 开启展开列功能
   -->
   <el-table-column type="expand">
       <template v-slot="scope">
           <el-row v-for="(item1, i1) in scope.row.children" :key="item1.id" :class="['bdbottom', i1 === 0 ? 'bdtop' : 'bdbottom', 'vcenter']">
               <!-- 渲染一级权限 -->
               <el-col :span="5">
                   <el-tag>{{ item1.authName }}</el-tag>
                   <i class="el-icon-caret-right"></i>
               </el-col>
               <!-- 渲染二级权限 -->
               <el-col :span="19">
                   <el-row v-for="(item2, i2) in item1.children" :key="item2.id" :class="[i2 === 0 ? '' : 'bdtop', 'vcenter']">
                       <el-col :span="6">
                           <el-tag type="success">{{ item2.authName }}</el-tag>
                           <i class="el-icon-caret-right"></i>
                       </el-col>
                       <!-- 渲染三级权限 -->
                       <el-col :span="18">
                           <el-tag type="warning" v-for="(item3, i3) in item2.children" :key="item3.id">{{ item3.authName }}</el-tag>
                       </el-col>
                    </el-row>
               </el-col>
           </el-row>
       </template>
   </el-table-column>
</el-table>      

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-njOQiVro-1605152538773)(…/upload/image-20201107144025650.png)]

如何优雅的写一个树形表格

<!-- 
    表格
    (参考 https://github.com/MisterTaki/vue-table-with-tree-grid)
    ref - 为当前组件注册一个引用
    :data - 表格各行的数据
    :columns - 表格各列的配置,这是一个列表,其中包含对象,对象的属性
    * label - 列标题名称
    * prop - 对应列内容的属性名
    :show-index - 是否显示数据索引
    :index-text - 数据索引名称
    :stripe - 是否显示间隔斑马纹
    :border - 是否显示纵向边框
    :show-row-hover - 鼠标悬停时,是否高亮当前行
    :tree-type - 是否为树形表格
    :selection-type - 是否为多选类型表格
    :expand-type - 是否为展开行类型表格
-->
<tree-table
    class="treetable"
    ref="treeTableRef"
    :data="cateList"
    :columns="columns"
    :show-index="true"
    index-text=""
    stripe
    border
    :show-row-hover="false"
    :tree-type="true"
    :expand-type="false"
    :selection-type="false" >
    <!-- 是否有效 -->
    <template slot="isok" slot-scope="scope">
<i class="el-icon-success" v-if="scope.row.cat_deleted === false" style="color: lightgreen"></i>
<i class="el-icon-error" v-else style="color: lightgreen"></i>
    </template>
    <!-- 排序 -->
    <template slot="order" slot-scope="scope">
<el-tag size="mini" v-if="scope.row.cat_level === 0">一级</el-tag>
<el-tag type="success" size="mini" v-else-if="scope.row.cat_level === 1">二级</el-tag>
<el-tag type="warning" size="mini" v-else>三级</el-tag>
    </template>
    <!-- 编辑 -->
    <template slot="opt" slot-scope="scope">
<el-button icon="el-icon-edit" type="primary" size="mini">编辑</el-button>
<el-button icon="el-icon-delete" type="danger" size="mini">删除</el-button>
    </template>
</tree-table>

如何优雅的写一个分页器

<!-- 
    分页区域
    @size-change - 选择页数的列表发生切换,返回最新的一页显示多少条
    @current-change - 页码值发生切换,返回最新的页数
    :current-page - 当前显示的是第几页的数据
    :page-sizes - 选择页数的下拉列表
    :page-size - 每页显示多少条数据
    layout - 需要展示的功能组件
    :total - 总条数(目前有 bug ,只能写成固定的数字才能正常显示)
-->
<el-pagination
    @size-change="handleSizeChange"
    @current-change="handleCurrentChange"
    :current-page="queryInfo.pagenum"
    :page-sizes="[1, 2, 5, 10]"
    :page-size="queryInfo.pagesize"
    layout="total, sizes, prev, pager, next, jumper"
    :total="total">
</el-pagination>

如何用 ElementUI 实现一个动态级联选择器

没错,就是它

首先,它需要一个数组,数组中有很多对象,并且这个数组数据是从 后台接口 动态获取的,所以说它返回来的数据也是固定的,但是 这个Cascader 级联选择器,它里面需要接收必须有 value 和 label 的对象(value为分类id, label为分类名称),那么我应该考虑如何把后台返回来的列表变成符合要求的列表,我的解决方案是 把后台数据列表先转换成字符串,再使用替换方法,再转成列表返回,这样问题就解决了,但是它还是有bug,它的选择器的长度,超乎我的想象,于是我用F12找到对应的元素,进行修改,到此,完美结束

第一种方法:

<!-- 
    :options - 用来指定数据源
    :props - 用来指定配置对象
	change-on-select - 是否允许选择任意一级的选项
    clearable - 是否支持清空选项
-->
<el-cascader
    v-model="selectedKeys"
    :options="options"
    :props="{ expandTrigger: 'hover' }"
    @change="parentCateChanged"
    change-on-select
    clearable >
</el-cascader>
export default {
    data(){
        return {
            // 选择的分类id列表
            selectedKeys: []
            // 父级分类列表
            parentCateList: [],

            // options 列表
            options: [],
        }
    }
}

// 更改 parentCateList 列表中属性名重新赋值给 options 列表,来供el-cascader组件中的 :options 使用
getOptionsList(){
    this.options = JSON.parse(JSON.stringify(this.parentCateList).replace(/cat_id/g, "value").replace(/cat_name/g, "label").replace(/cat_deleted/g, "del").replace(/cat_level/g, "del").replace(/cat_pid/g, "del"))
    this.options.forEach((item1)=>{
        delete item1.del
        item1.children.forEach((item2)=>{
            delete item2.del
        })
    })
},
// 修改 cscader 默认的超长选择框的高度
.el-cascader-panel {
    height: 300px;
}

第二种方法,老师的方法:

<el-cascader
    expand-trigger="hover"
    v-model="selectedKeys"
    :options="parentCateList"
    :props="cascaderProps"
    @change="parentCateChanged"
    change-on-select
    clearable >
</el-cascader>
export default {
    data(){
        return {
            // 选择的分类id列表
            selectedKeys: []
            // 父级分类列表
            parentCateList: [],
			// 指定级联选择器的配置对象
            cascaderProps: {
                value: "cat_id",
                label: "cat_name",
                children: "children"
            },
        }
    }
}

如何实现步骤条和标签栏的同步

<!-- 
    步骤条区域
    :space - 步骤条的线长
    :active - 开始的索引值
    align-center - 居中
-->
<el-steps :space="200" :active="activeIndex - 0" finish-status="success" align-center>
    <el-step title="基本信息"></el-step>
    <el-step title="商品参数"></el-step>
    <el-step title="商品属性"></el-step>
    <el-step title="商品图片"></el-step>
    <el-step title="商品内容"></el-step>
    <el-step title="完成"></el-step>
</el-steps>

<!-- tab栏区域 -->
<el-tabs v-model="activeIndex" :tab-position="'left'" style="height: 200px;">
    <el-tab-pane label="基本信息" name="0">基本信息</el-tab-pane>
    <el-tab-pane label="商品参数" name="1">商品参数</el-tab-pane>
    <el-tab-pane label="商品属性" name="2">商品属性</el-tab-pane>
    <el-tab-pane label="商品图片" name="3">商品图片</el-tab-pane>
    <el-tab-pane label="商品内容" name="4">商品内容</el-tab-pane>
</el-tabs>
export default {
    data(){
        return {
            activeIndex: '0'
        }
    }
}

首先和主要的就是 data 中的 activeIndex 的定义,首先,你应该知道它是双向绑定在 <el-steps> 标签和 <el-tabs> 标签中的,<el-steps> 标签是用 :active 绑定的,它的定义是:索引值(数字类型)为几,那么它就跳到第几步, <el-tabs> 标签是用 v-model 绑定的,它的定义是:你点的是哪一栏,v-model 的值(字符串)就是什么,关键是这两个的类型不同,所以我定义了 activeIndex 的值默认是字符串的 ‘0’,需要数字的地方直接减 0 就行了

如何实现商品列表中编辑商品的功能

首先,你应该能够获取到这一行的数据,无论是小康的(在路径后面传入该商品对应的id,然后通过id查询商品列表),还是我的(把该行的数据存入sessonStorage 中,然后在需要的地方直接拿出来),这都能获取到该行的数据,关键是你怎么把数据重新渲染到编辑页面,我是直接给 editForm ,需要提交的表单进行重新赋值,让该商品的数据重新赋值给 editForm 中对应的属性上,但是,需要主要的是,商品分类是该商品没有给到的数据,那你就应该通过商品 id 来查询该商品,你在它的返回结果就可以找到 goods_cat(商品分类)这个数据,但是,它返回的这个 goods_cat 是一个字符串(这是因为我在Add.vue 中提交表单时作了处理,因为它最终只能提交字符串格式的数据),所以我需要把它先转换为一个数组,然后再重新赋值给 editForm,最后只需要改一下编辑的接口即可成功修改该商品。

List.vue

// 通过编程式导航跳转到编辑商品页面
goEditpage(row){
    this.$router.push(`/goods/edit`)
    const rowObj = JSON.stringify(row)
    console.log(rowObj);
    window.sessionStorage.setItem("edit", [rowObj])
}

Edit.vue

async created(){
    this.getCateList()
    const rowObj = JSON.parse(window.sessionStorage.getItem("edit"))
    console.log(rowObj);

    // 根据 ID 查询商品
    const {data: res} = await this.$http.get("goods/" + rowObj.goods_id)
    console.log(res.data);

    this.goodsId = rowObj.goods_id
    this.editForm.goods_cat = res.data.goods_cat.split(",").map(Number)
    this.editForm.goods_name = rowObj.goods_name
    this.editForm.goods_number = rowObj.goods_number
    this.editForm.goods_price = rowObj.goods_price
    this.editForm.goods_weight = rowObj.goods_weight
    // console.log(this.editForm); 
},

需要注意的是,把数字字符串转成数组之后它的每一项还是字符串,所以需要用 map(Number) 把其中的每一项都变成数字。

项目优化 - 添加进度条

就是顶部一闪而过的进度条

npm install --save nprogress
// 导入 NProgress 包对应的JS和CSS
import NProgress from "nprogress";
import "nprogress/nprogress.css";
// 在 request 拦截器中,展示进度条 NProgress.start()
axios.interceptors.request.use((config) => {
    // console.log(config);
    NProgress.start();
    config.headers.Authorization = window.sessionStorage.getItem("token");
    return config;
});
// 在 resopnse 拦截器中,隐藏进度条 NProgress.done()
axios.interceptors.response.use((response) => {
    NProgress.done();
    return response;
});

如何在上线时移除项目中所有的console

使用 babel-plugin-transform-remove-console 插件移除

在开发依赖中安装

npm install babel-plugin-transform-remove-console --save-dev

按需使用 这个插件,在开发时不移除 console,在发布时移除 console

babel.config.js

// 这是项目发布阶段需要用到的 babel 插件
const prodPlugins = []
if(process.env.NODE_ENV === "production"){
    proPlugins.push("transform-remove-console")
}

module.exports = {
    // 发布产品时候的插件数组
    ...prodPlugins
}

项目优化 - 通过 externals 加载外部CDN资源

具体配置如下:

config.set ('externals', {
    vue: 'vue',
	'vue-router': 'vueRouter',axios: 'axios',
	loaash: '_',
	echarts: 'echarts', nprogress: 'NProgress',
	'vue-quill-editor': 'vueQuillEditor'})

项目优化 - 路由懒加载

当打包构建项目时,JavaScript包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
具体需要3步:
① 安装 @babel/plugin-syntax-dynamic-import 包。

② 在babel.config.js 配置文件中声明该插件。

③ 将路由改为按需加载的形式,示例代码如下;

const Foo = () => import(/* webpackChunkName: "group-foo" */ "./Foo.vue")
const Bar = () => import(/* webpackChunkName: "group-foo" */ "./Bar.vue")
const Baz = () => import(/* webpackChunkName: "group-boo" */ "./Baz.vue")

项目优化 - 开启 gzip 压缩配置

使用 gzip 可以减小文件体积,使传输速度更快。
② 可以通过服务器端使用Express做gzip 压缩。其配置如下:

//安装相应包
npm install compression -D
//导入包
const compression = require('compression');
//启用中间件
app.use(compression());

注意:先注册 gizp 压缩,再托管静态资源

使用pm2管理应用

pm2 可以使你的终端窗口被关闭后也可以正常的处于开启服务的状态

① 在服务器中安装pm2: npm i pm2 -g

② 启动项目: pm2 start脚本--name自定义名称

③ 查看运行项目: pm2 ls

④ 重启项目: pm2 restart 自定义名称

⑤ 停止项目: pm2 stop 自定义名称

== “production”){
proPlugins.push(“transform-remove-console”)
}

module.exports = {
// 发布产品时候的插件数组
…prodPlugins
}




### 项目优化 - 通过 externals 加载外部CDN资源

> 具体配置如下:

```javascript
config.set ('externals', {
    vue: 'vue',
	'vue-router': 'vueRouter',axios: 'axios',
	loaash: '_',
	echarts: 'echarts', nprogress: 'NProgress',
	'vue-quill-editor': 'vueQuillEditor'})

项目优化 - 路由懒加载

当打包构建项目时,JavaScript包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
具体需要3步:
① 安装 @babel/plugin-syntax-dynamic-import 包。

② 在babel.config.js 配置文件中声明该插件。

③ 将路由改为按需加载的形式,示例代码如下;

const Foo = () => import(/* webpackChunkName: "group-foo" */ "./Foo.vue")
const Bar = () => import(/* webpackChunkName: "group-foo" */ "./Bar.vue")
const Baz = () => import(/* webpackChunkName: "group-boo" */ "./Baz.vue")

项目优化 - 开启 gzip 压缩配置

使用 gzip 可以减小文件体积,使传输速度更快。
② 可以通过服务器端使用Express做gzip 压缩。其配置如下:

//安装相应包
npm install compression -D
//导入包
const compression = require('compression');
//启用中间件
app.use(compression());

注意:先注册 gizp 压缩,再托管静态资源

使用pm2管理应用

pm2 可以使你的终端窗口被关闭后也可以正常的处于开启服务的状态

① 在服务器中安装pm2: npm i pm2 -g

② 启动项目: pm2 start脚本--name自定义名称

③ 查看运行项目: pm2 ls

④ 重启项目: pm2 restart 自定义名称

⑤ 停止项目: pm2 stop 自定义名称

⑥ 删除项目: pm2 delete 自定义名称

本文地址:https://blog.csdn.net/Cool_breeze_/article/details/109640761

《Vue 电商PC后台管理(ElementUI).doc》

下载本文的Word格式文档,以方便收藏与打印。