欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Vue 电商PC后台管理(ElementUI)

程序员文章站 2022-03-10 18:27:26
Vue 电商PC后台管理(ElementUI)1.项目概述1.1电商项目基本业务概述根据不同的场景,电商系统一般都提供了PC端、移动 APP、移动 Web、微信小程序等多种终端访问方式。1.2电商后台管理系统的功能电商后台 管理系统同用于管理用户账号、商品分类、商品信息、订单、数据统计等业务功能。1.3电商后台管理系统的开发模式(前后端分离)电商后台管理系统整体采用前后端分离的开发模式,其中前端项目是基于Vue技术栈的SPA项目。1.4电商后台管理系统的技术选型1.前端项目技术栈...

Vue 电商PC后台管理(ElementUI)

1.项目概述

1.1电商项目基本业务概述

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

Vue 电商PC后台管理(ElementUI)

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

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

Vue 电商PC后台管理(ElementUI)

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

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

Vue 电商PC后台管理(ElementUI)

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 原理分析

Vue 电商PC后台管理(ElementUI)

3.3登录功能实现

1.登录页面的布局

通过Element-UI组件实现布局

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

Vue 电商PC后台管理(ElementUI)

首先打开 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>

Vue 电商PC后台管理(ElementUI)

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>

权限管理

权限管理业务分析

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

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

Vue 电商PC后台管理(ElementUI)

项目要点

如何实现一个好看的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 实现一个动态级联选择器

Vue 电商PC后台管理(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"
            },
        }
    }
}

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

Vue 电商PC后台管理(ElementUI)

<!-- 
    步骤条区域
    :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) 把其中的每一项都变成数字。

项目优化 - 添加进度条

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

Vue 电商PC后台管理(ElementUI)

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