项目仓库:https://github.com/changeclass/vue-shop

主页布局

主页布局使用ElementUI提供的布局。

<el-container class="home-container">
    <!-- 头部区域 -->
    <el-header>
        <el-button type="info" @click="logout">退出</el-button>
    </el-header>
    <!-- 页面主体 -->
    <el-container>
        <!-- 侧边栏 -->
        <el-aside width="200px">Aside</el-aside>
        <!-- 页面右侧 -->
        <el-main>Main</el-main>
    </el-container>
</el-container>

ElementUI提供的组件,其组件名就是他的class名,因此样式可以写成如下

.home-container{
    height: 100%;
}
.el-header {
    background-color: #373d41;
}
.el-aside {
    background-color: #333744;
}
.el-main {
    background-color: #eaedf1;
}

header布局

header布局使用flex布局很容易实现。

  1. 对于HTML结构改造成如下

    <el-header>
        <div>
            <img src="../assets/heima.png" alt="">
            <span>电商后台管理系统</span>
        </div>
        <el-button type="info" @click="logout">退出</el-button>
    </el-header>
  2. css样式

    .el-header {
        background-color: #373d41;
        display: flex;
        justify-content: space-between;
        padding-left: 0;
        align-items: center;
        color: #fff;
        font-size: 20px;
        >div{
            display: flex;
            align-items: center;
            span{
                margin-left: 15px;
            }
        }
    }

侧边栏布局

<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">
            <template slot="title">
                <!-- 图标 -->
                <i class="el-icon-location"></i>
                <!-- 文本 -->
                <span>导航一</span>
            </template>
        </el-menu-item>
    </el-submenu>
</el-menu>

但由于是按需导入的ElementUI组件,因此还需要在注册相关的组件。

import {
  Menu,
  MenuItem,
  Submenu,
  MenuItemGroup
} from 'element-ui'
Vue.use(Menu)
Vue.use(MenuItem)
Vue.use(Submenu)
Vue.use(MenuItemGroup)

配置请求拦截器

配置请求拦截器的目的主要是因为请求数据时需要进行头部的TOKEN验证,因此在请求拦截器中设置TOKEN的无疑是最好的方式。在入口函数中对axios进行设置

// 设置拦截器
axios.interceptors.request.use(config => {
    config.headers.Authorization = window.sessionStorage.getItem('token')
    return config
})

发起请求获取左侧菜单数据

组件一渲染就应该发起请求去获取数据,根据数据将其赋值给自己的属性。根据属性进行UI渲染。

image-20201105092652272

export default {
    created () {
        this.getMenuList()
    },
    data: function () {
        return {
            menulist: []
        }
    },
    methods: {
        async getMenuList () {
            const { data: res } = await this.$http.get('menus')
            if (res.meta.status !== 200) return this.$message.error(res.meta.message)
            this.menulist = res.data
        }
    }
}

左侧菜单UI绘制

根据接口返回的数据格式,可以使用两个for循环渲染菜单。

<el-menu
         background-color="#333744"
         text-color="#fff"
         active-text-color="#ffd04b"
         >
    <!-- 一级菜单 -->
    <el-submenu
                :index="item.id + ''"
                v-for="item in menulist"
                :key="item.id"
                >
        <!-- 一级菜单模板区域 -->
        <template slot="title">
            <!-- 图标 -->
            <i class="el-icon-location"></i>
            <!-- 文本 -->
            <span>{{ item.authName }}</span>
        </template>
        <!-- 二级菜单 -->
        <el-menu-item
                      :index="subItem.id + ''"
                      v-for="subItem in item.children"
                      :key="subItem.id"
                      >
            <template slot="title">
                <!-- 图标 -->
                <i class="el-icon-location"></i>
                <!-- 文本 -->
                <span>{{ subItem.authName }}</span>
            </template>
        </el-menu-item>
    </el-submenu>
</el-menu>

图标

对于二级菜单图标使用的是相同的图标,即写死即可。但一级菜单会根据不同而变化。因此可以定义一个属性用于记录每个分类应展示的图标。

data: function () {
  return {
    iconObj: {
      125: 'iconfont icon-user',
      103: 'iconfont icon-tijikongjian',
      101: 'iconfont icon-shangpin',
      102: 'iconfont icon-danju',
      145: 'iconfont icon-baobiao'
    }
  }
},

渲染时只需要取出对应的键的值即可。

<i :class="iconObj[item.id]"></i>

只能展开一个一级菜单

对menu添加属性unique-openedtrue即可

<el-menu :unique-opened="true"></el-menu>

折叠与展开

5cda81b6-4207-4346-81b1-72ec53794d42

组件提供了一个属性collapse用于控制是否折叠展开,但展开与折叠时宽度也要随之变化,因此通过此值也需要变化宽度

<el-aside :width="isCollpase ? '64px' : '200px'">
    <div class="toggle-button" @click="toggleCollapse">|||</div>
    <el-menu
             background-color="#333744"
             text-color="#fff"
             active-text-color="#409bff"
             :unique-opened="true"
             :collapse="isCollpase"
             :collapse-transition="false"
             ></el-menu>
</el-aside>

在data中定义一个属性用于控制当前是否展开

data: function () {
  return {
    isCollpase: false
  }
},

点击按钮后保持激活状态

ElementUI组件对menu提供一个属性default-active,只需要将其设置为需要高刘高亮的index即可。为了记录当前点击的状态,也将其记录在sessionstore中。并且在每次点击按钮时将当前的路径存储带sessionstore中,组件创建时自动取出其值。

<el-menu :default-active="activePath">
export default {
  created () {
    this.getMenuList()
    this.activePath = window.sessionStorage.getItem('activePath')
  },
  data: function () {
    return {
      activePath: ''
    }
  },
  methods: {
    saveNavState (activePath) {
      window.sessionStorage.setItem('activePath', activePath)
      this.activePath = activePath
    }
  }
}

首页重定向

定义一个新的Welcome组件,并对home设置子路由

import Welcome from '../components/Welcome.vue'
const routes = [
  // home页面
  {
    path: '/home',
    component: Home,
    redirect: '/welcome',
    children: [{ path: '/welcome', component: Welcome }]
  }
]

为每一个子菜单设置路由,ElementUI提供了路由功能,只需要在menu组件中添加属性router即可。点击子菜单会自动转到当前item的id。因此还需要将子菜单的id改为数据返回的path属性(加/

首页主要区域

使用的组件同样需要按需导入。

面包屑导航

<el-breadcrumb separator="/">
    <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
    <el-breadcrumb-item>用户管理</el-breadcrumb-item>
    <el-breadcrumb-item>用户列表</el-breadcrumb-item>
</el-breadcrumb>

在全局样式中覆盖此组件的样式

.el-breadcrumb {
  margin-bottom: 15px;
  font-size: 12px;
}

卡片视图

<!-- 卡片视图 -->
<el-card>
    <!-- 搜搜与添加 -->
    <el-row :gutter="20">
        <el-col :span="7">
            <el-input placeholder="请输入内容">
                <el-button slot="append" icon="el-icon-search"></el-button>
            </el-input>
        </el-col>
        <el-col :span="4">
            <el-button type="primary">添加用户</el-button>
        </el-col>
    </el-row>
</el-card>

同样的为了美观也在全局样式中覆盖一下样式

.el-card {
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15) !important;
}

获取数据

export default {
    data () {
        return {
            queryInfo: {
                query: '',
                pagenum: 1,
                pagesize: 2
            },
            userlist: [],
            total: 0
        }
    },
    created () {
        this.getUserList()
    },
    methods: {
        async getUserList () {
            const { data: res } = await this.$http.get('users', { params: this.queryInfo })
            if (res.meta.status !== 200) return this.$message.error('获取用户信息失败')
            this.userlist = res.data.users
            this.total = res.data.total
        }
    }
}

渲染用户数据

渲染用户数据使用table元素,

<el-table :data="userlist" border stripe>
    <!-- 索引列只需要用type属性即可 -->
    <el-table-column type="index" label="#"></el-table-column>
    <el-table-column label="姓名" prop="username"></el-table-column>
    <el-table-column label="邮箱" prop="email"></el-table-column>
    <el-table-column label="电话" prop="mobile"></el-table-column>
    <el-table-column label="角色" prop="role_name"></el-table-column>
    <el-table-column label="状态" prop="mg_state"></el-table-column>
    <el-table-column label="操作"></el-table-column>
</el-table>
  • label

    表格标题

  • prop

    表格列的数据源

  • :data

    表格数据绑定的数据

  • border

    加入边框

  • stripe

    隔行变色

改造状态列显示效果

在单元格中添加一个作用域插槽,其属性slot-cope表示接收的属性

<el-table-column label="状态" prop="mg_state">
    <template v-slot="scope">
        <el-switch v-model="scope.row.mg_state"></el-switch>
    </template>
</el-table-column>

此时scope相当于当前项。

操作列的改造

基本UI效果以及el-tooltip提示。

<el-table-column label="操作" width="180px">
    <template>
        <!-- 修改按钮 -->
        <el-button
                   type="primary"
                   icon="el-icon-edit"
                   size="mini"
                   ></el-button>

        <!-- 删除按钮 -->
        <el-button
                   type="danger"
                   icon="el-icon-delete"
                   size="mini"
                   ></el-button>

        <!-- 分配角色按钮 -->
        <el-tooltip
                    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>

分页

https://element.eleme.cn/#/zh-CN/component/pagination

<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>
methods: {
    // 改变 pagesize 事件
    handleSizeChange (newSize) {
        this.queryInfo.pagesize = newSize
        this.getUserList()
    },
        // 监听 页码值 改变的事件
        handleCurrentChange (newPage) {
            this.queryInfo.pagenum = newPage
            this.getUserList()
        }
}

用户状态修改

当switch开关被点击后会触发change事件,因此只需要在事件中请求修改状态的API即可。

<el-switch
           @change="userStateChanged(scope.row)"
           v-model="scope.row.mg_state"
           ></el-switch>
methods: {
    async userStateChanged (userinfo) {
        console.log(userinfo)
        const { data: res } = await this.$http.put(`users/${userinfo.id}/state/${userinfo.mg_state}`)
        if (res.meta.status !== 200) {
            userinfo.mg_state = !userinfo.mg_state
            console.log(res)
            return this.$message.error('更新用户状态失败')
        }
        this.$message.success('更新用户状态成功')
    }
}

搜索用户

搜索用户只需要对输入框进行双向数据绑定并提交事件即可。

输入框提供了一个clearable属性,即可以清空输入框,同时触发一个clear事件。

<el-input
          placeholder="请输入内容"
          v-model="queryInfo.query"
          clearable
          @clear="getUserList"
          >
    <el-button
               slot="append"
               icon="el-icon-search"
               @click="getUserList"
               ></el-button>
</el-input>

添加用户

对话框

<el-dialog title="提示" :visible.sync="addDialogVisible" width="50%">
    <!-- 内容主体 -->
    <span>这是一段信息</span>
    <!-- 底部区域 -->
    <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>

通过属性addDialogVisible控制对话框的显示与隐藏。

<el-button type="primary" @click="addDialogVisible = true" >添加用户</el-button>

绘制提交表单

<el-form
         :model="addForm"
         :rules="addFormRules"
         ref="ruleFormRef"
         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 v-model="addForm.password"></el-input>
    </el-form-item>
    <el-form-item label="邮箱" prop="email">
        <el-input 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>
data () {
    return {
        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: 6, max: 15, message: '请输入6到15位', trigger: 'blur' }],
            email: [
                { required: true, message: '请输入邮箱', trigger: 'blur' }],
            mobile: [
                { required: true, message: '请输入手机号', trigger: 'blur' }]
        }

    }
},

自定义验证规则

在data函数中定义函数用于校验规则,然后在验证规则中使用validator关键字调用即可。

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 {
        // 添加用户表单验证规则
        addFormRules: {
            username: [
                { required: true, message: '请输入用户名', trigger: 'blur' },
                { min: 3, max: 10, message: '请输入3到10位', trigger: 'blur' }],
            password: [
                { required: true, message: '请输入密码', trigger: 'blur' },
                { min: 6, max: 15, message: '请输入6到15位', trigger: 'blur' }],
            email: [
                { required: true, message: '请输入邮箱', trigger: 'blur' },
                { validator: checkEmail, trigger: 'blur' }],
            mobile: [
                { required: true, message: '请输入手机号', trigger: 'blur' },
                { validator: checkMobile, trigger: 'blur' }]
        }

    }
},

重置表单

当dialog关闭时会触发close事件,因此为其触发函数重置表单即可。

addDialogClosed () {
    this.$refs.ruleFormRef.resetFields()
}
<el-dialog @close="addDialogClosed"> </el-dialog>

提交验证

点击确定会触发提交事件,提交前应该预验证是否符合格式要求。

addUser () {
    this.$refs.ruleFormRef.validate(async valid => {
        if (!valid) return
        console.log(valid)
        // 添加网路请求
        const { data: res } = await this.$http.post('users', this.addForm)
        if (res.meta.status !== 201) {
            this.$message.error('添加用户失败')
        }
        this.$message.success('添加用户成功')
        // 因此对话框
        this.addDialogVisible = false
        // 重新获取列表
        this.getUserList()
    })
}
<el-button type="primary" @click="addUser">确 定</el-button>

修改用户

修改用户逻辑与添加用户逻辑大致相同,点击编辑按钮打开对话框,此时应根据ID查询相应的数据并填充到表单中。

// 修改用户并提交
editUserInfo () {
    this.$refs.editFormRef.validate(async valid => {
        if (!valid) return
        // 添加网路请求
        const { data: res } = await this.$http.put(`users/${this.editForm.id}`, {
            email: this.editForm.email,
            mobile: this.editForm.mobile
        })
        console.log(res)
        if (res.meta.status !== 200) {
            this.$message.error('修改用户失败')
        }
        this.$message.success('修改用户成功')
        // 隐藏对话框
        this.editDialogVisible = false
        // 重新获取列表
        this.getUserList()
    })
},
    // 展示编辑用户的对话框
    async showEditDialog (val) {
        const { data: res } = await this.$http.get(`users/${val.id}`)
        if (res.meta.status !== 200) return this.$message.error('查询用户信息失败!')
        this.editForm = res.data
        this.editDialogVisible = true
        console.log(this.editForm)
    }
<el-dialog
           title="编辑用户"
           :visible.sync="editDialogVisible"
           width="50%"
           @close="editDialogClosed"
           >
    <el-form
             :model="editForm"
             :rules="editFormRules"
             ref="editFormRef"
             label-width="70px"
             >
        <el-form-item label="用户名">
            <el-input v-model="editForm.username" disabled></el-input>
        </el-form-item>
        <el-form-item label="邮箱" prop="email">
            <el-input v-model="editForm.email"></el-input>
        </el-form-item>
        <el-form-item label="电话" prop="mobile">
            <el-input v-model="editForm.mobile"></el-input>
        </el-form-item>
    </el-form>
    <span slot="footer" class="dialog-footer">
        <el-button @click="editDialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="editUserInfo">确 定</el-button>
    </span>
</el-dialog>

删除用户

删除用户应该弹出提示框提示用户是否删除,从而避免误删行为。使用提示框需要全局挂载

image-20201105161456244

import {
  MessageBox
} from 'element-ui'
Vue.prototype.$confirm = MessageBox.confirm

未删除按钮绑定事件,并将待删除的用户ID传入

<el-button
           type="danger"
           icon="el-icon-delete"
           size="mini"
           @click="removeUserById(scopeEdit.row.id)"
           ></el-button>

定义事件

// 通过ID删除用户
async removeUserById (id) {
    // 弹窗询问是否删除
    const confirmResult = await this.$confirm('此操作将永久删除该用户,是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'waring'
    }).catch(err => err)
    // 如果用户确认则返回字符串 - confirm
    // 如果用户确认则返回字符串 - cancel
    console.log(confirmResult)
    if (confirmResult !== 'confirm') {
        return this.$message.info('已经取消了删除')
    } else {
        return this.$message.success('删除了该用户')
    }
}

点击取消后会抛出错误,因此我们需要用catch捕获错误并抛出解决报错问题。