结合 服务器+后端+前端,完成 vue项目 后台管理系统

2023-10-26

目录

 以上是项目的服务器php、后端、前端、已经可以正常运行

 一 登录:

登录页进度条:戳这里Vue项目电商后台管理系统 nprogress--进度条_活在风浪里的博客-CSDN博客

 二 侧导航

三 列表页源码:

 四 角色分配

五 、权限页面开发

权限列表:

 角色列表:

1、面包屑 

2、卡片区-添加按钮 

 3、获取表格数据:

 4、渲染列表

​ 5、分配权限

​ 5.2、分配一级 二级权限

 5.3、渲染一级权限

 5.4 、二级权限渲染

5.5、渲染三级权限

​ 6、优化bug

 7、实现删除tag标签

8、分配权限

递归 分配权限取出三级id

 线性转化树形

树形转为线性 :

分配权限确定按钮

9、添加用户

10、编辑用户 

11、删除用户 

哈哈! 权限列表完!​  

 六、商品管理

1、商品分类

2、分页: 

3、编辑: 

4、删除: 

 5、添加分类:

6、商品分类源码: 

七、分类参数

 1、渲染级联选择器和面包屑和el-alert

 2、只允许获取三级分类

3、实现没有选中三级分类添加按钮就不显示

 4、参数获取

 5、渲染 动态参数表格、静态属性表格:

 6、添加参数:

  7、修改参数:

   8、删除参数:

    9、表格展开操作:

10 、优化分类参数bug,选中非三级分类,清空表格

八、商品列表

商品表格

 添加商品

​1、面包屑和步骤条:

2、渲染页面 基本信息form

 3、请求级联选择器的数据

  4、实现不选中分类不允许进行下一步

  5、商品参数  (动态参数)

   6、商品属性 (静态属性)

    7、商品图片 (重要!)

8、富文本 添加按钮操作

8.1、处理goods_cat 

 8.2、处理attr参数

九、商品订单-列表

 十、数据统计-ECharts

 十一、结束 上线部署服务器


十万字 从零到一 完成电商后台管理项目 看完如果不会你揍我watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_6,color_FFFFFF,t_70,g_se,x_16

  • 前言:3步
  • 1 .安装搭建服务器,
  • 2 .运行后端代码(node app.js),
  • 3 .运行前端代码(npm run serve)

 

  1. 本次使用自己的搭建的后端服务器,因为如果将线上接口发至互联网,大量的人修改会导致接口紊乱,所以在自己本地开发练习一下,如果计算机安装不了php,私信我,发送给你线上接口,
  2. 亲测可用 gitee 网址 包括php包,后端node.js代码,前端源码 以及文档接口说明https://gitee.com/zhang-kun8888/background-anagement-project-zk.githttps://gitee.com/zhang-kun8888/background-anagement-project-zk.git

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

效果示例图:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

篇幅有限就不一一展示功能了

Gitee clone下载文件说明

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

打开步骤

1 开启php

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 2 点击MySOL管理器导入后端代码

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 

 3 如何查看是否导入成功?

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 4 运行 后端代码,下载依赖 cnpm i  启动命令 node app.js  、运行前端代码,下载依赖 cnpm i 启动命令npm run serve

当然了这是在本地开启了后端服务器,前后端自己干,才这么麻烦,如果是线上接口,修改一下公共网址,

可直接运行前端的代码

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

5 Gitee截图事例说明

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 以上是项目的服务器php、后端、前端、已经可以正常运行

实在安装不上找我要线上接口,只需要修改前端代码的公共网址,可直接运行,不用自己服务器、后端。。。配置了

接下来说前端代码的细节

 

 一 登录

  • 登录没什么困难,在点击登录按钮时,调用接口,传递后端要的必填参数,存储token,进行全局前置守卫判断,后置路由钩子获取dom
  • 退出登录,则删除token,
<template>
  <div id="login">
    <div class="formBox">
      <div class="imgBox">
        <img src="../assets/logo.png" alt="" />
      </div>
      <!-- main -->
      <div class="main">
        <el-form
          :model="ruleForm"
          :rules="rules"
          status-icon
          ref="ruleForm"
          label-width="100px"
          class="demo-ruleForm"
        >
          <el-form-item label="用户名" prop="username">
            <el-input
              v-model="ruleForm.username"
              prefix-icon="el-icon-user"
            ></el-input>
          </el-form-item>
          <el-form-item label="用户密码" prop="password">
            <el-input
              prefix-icon="el-icon-lock"
              placeholder="请输入密码"
              show-password
              v-model="ruleForm.password"
            ></el-input>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="submitForm">提交</el-button>
            <el-button @click="resetForm('ruleForm')">重置</el-button>
          </el-form-item>
        </el-form>
      </div>
    </div>
  </div>
</template>

<script>
import { login } from "./api";
export default {
  data() {
    return {
      ruleForm: {
        password: "123456",
        username: "admin",
      },
      rules: {
        username: [
          { required: true, message: "请输入用户名", trigger: "blur" },
        ],
        password: [{ required: true, message: "请输入密码", trigger: "blur" }],
      },
    };
  },
  methods: {
    async submitForm() {
      await login(this.ruleForm).then((res) => {
        // console.log(res);
        localStorage.setItem("token", JSON.stringify(res.data.token));
        this.$router.push("/main");
      });
    },
    resetForm(formName) {
      this.$refs[formName].resetFields();
    },
  },

  computed: {},
  components: {},
  created() {},
};
</script>

<style lang="scss" scoped>
#login {
  width: 100%;
  height: 100%;
  background-color: #3f4b6b;
  position: relative;
  .formBox {
    border-radius: 5px;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background-color: #fff;
    width: 500px;
    height: 350px;
    .imgBox {
      position: absolute;
      top: -70px;
      left: 50%;
      transform: translateX(-50%);
      background-color: #fff;
      border-radius: 80px;
      width: 120px;
      height: 120px;
      box-sizing: border-box;
      padding: 15px;
      overflow: hidden;
      box-shadow: 1px 1px 15px #ddd;
      img {
        width: 100%;
        height: 100%;
      }
    }
    ::v-deep.demo-ruleForm {
      margin-top: 100px;
      margin-right: 50px;
    }
  }
}
</style>

图例

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

router/index 页面

import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)

const routes = [{
        path: '/',
        redirect: 'login'
    }, {
        path: '/login',
        name: 'Login',
        meta: {
            title: '后台管理登录',
            requiredPath: false,

        },
        component: () =>
            import ('../views/Login.vue')
    },
    {
        path: '/main',
        name: '',
        component: () =>
            import ('../views/main/main.vue'),

        children: [{
                path: '/',
                name: '',
                meta: {
                    title: '欢迎来到后台管理主页',
                    requiredPath: true,

                },
                component: () =>
                    import ('../views/main/index.vue')
            },
            {
                path: '/users',
                name: '',
                meta: {
                    title: "后台管理列表页",
                    requiredPath: true
                },
                component: () =>
                    import ('../views/main/users.vue')
            }
        ]
    }

]

const router = new VueRouter({
    mode: 'history',
    base: process.env.BASE_URL,
    routes
})

// 在路由元信息配置守卫 requiredPath为true, 适合守卫多个页面 vue3next() 变成return true
router.beforeEach((to, from, next) => {
        if (localStorage.getItem('token')) {
            next()
        } else {
            if (to.meta.requiredPath) { //没有token requiredPath为true 守卫不让进,跳入login
                next('/Login')
            } else {
                next()
            }
        }

    })
    // 导航后置守卫(当你真正进入到某个页面之后才执行)
router.afterEach((to, from) => {
    // 设置路由的标题 (可自定义)
    document.title = to.meta.title || '常驻标题'
        // 将所有的页面切换之后滚动到最顶部
    window.scrollTo(0, 0)
})
export default router

登录页进度条:戳这里Vue项目电商后台管理系统 nprogress--进度条_活在风浪里的博客-CSDN博客

 二 侧导航

进入后 书写侧边栏导航(这个也没什么难度,细心噢,别把ele的标签复制多 或 少le)

<template>
  <div id="about">
    <!-- 高100% -->
    <el-container class="box">
      <!-- el-header高60PX -->
      <el-header class="header">
        <span @click="$router.push('/main')">电商管理系统</span>
        <el-button type="primary" @click="outLogin">退出</el-button>
      </el-header>
      <!-- 高100% - 60PX (el-header高60PX) -->
      <el-container class="header_main">
        <!-- aside -->
        <el-aside :width="asideWidth" class="aside">
          <div
            data-v-bcdba65e
            class="top-bar"
            @click="isCollapse = !isCollapse"
          >
            |||
          </div>
          <!-- 
            1 unique-opened 保持一个子导航开启,因为循环的是所有,打开一个就会打开全部,
              所以开启 unique-opened,它是el-menu的属性,开启router属性值目的是否使用 vue-router 的模式,
              启用该模式会在激活导航时以 index 作为 path 进行路由跳转,
              在el-menu-item标签里设置跳转属性index='xx'

            2 还要开启el-submenu的属性唯一标识index它的值是字符串或空
            它是可选值,如果你是循环必须要开始唯一标识,因为循环的是所有,打开一个就会打开全部,
            所以要一个字符串唯一标识来区分
            如果是一个个写的导航不用也可以

            default-active="/users" 默认当前激活菜单的 index,它是el-menu的属性
            在el-menu标签写default-active="/users"
                       -->
          <el-menu
            class="el-menu-vertical-demo"
            background-color="#333744"
            text-color="#fff"
            active-text-color="#409fff"
            unique-opened
            :collapse="isCollapse"
            :collapse-transition="false"
            router
          >
            <!-- 动画不能写死,是变动的 -->
            <!-- 2 循环还要开启el-submenu的属性唯一标识index它的值是字符串或空 -->
            <el-submenu
              :index="value.id + ''"
              v-for="value in menuList"
              :key="value.id"
            >
              <template slot="title">
                <i :class="iconObj[value.id]"></i>
                <span>{{ value.authName }}</span>
              </template>
              <!-- 和文件夹没关系,要进入的是users  现在是请求的数据动态的路径-->
              <el-menu-item
                class="el-menu"
                :index="'/' + item.path"
                v-for="item in value.children"
                :key="item.id"
              >
                <i :class="iconObj[item.id]"></i>

                <span>{{ item.authName }}</span>
              </el-menu-item>
            </el-submenu>
          </el-menu>
        </el-aside>
        <!-- main里面有子路由就得给main配坑 -->
        <el-main> <router-view></router-view> </el-main>
      </el-container>
    </el-container>
  </div>
</template>

<script>
import { menus } from "../api";
export default {
  data() {
    return {
      // 默认的是否折合aside侧边栏
      isCollapse: false,
      // 左侧菜单数据
      menuList: null,
      // 解决循环字体图标,请求的数据默认有id,自定义一个数据键是id对应的数,值是icon
      iconObj: {
        125: "iconfont icon-test1",
        103: "iconfont ego-box",
        101: "iconfont shangpin-xianxing",
        102: "iconfont dingdan",
        145: "iconfont shujutongjixuanzhong",
        110: "el-icon-user-solid",
        111: "el-icon-tickets",
        112: "el-icon-film",
        104: "el-icon-s-goods",
        115: "el-icon-star-off",
        121: "el-icon-delete-solid",
        107: "el-icon-s-promotion",
        146: "el-icon-s-flag",
      },
    };
  },
  methods: {
    outLogin() {
      localStorage.removeItem("token");
      this.$router.replace("/login");
    },
    async getMenus() {
      await menus()
        .then((result) => {
          // console.log(result);
          this.menuList = result.data;
          console.log(this.menuList);
        })
        .catch((err) => {
          throw new Error(err);
        });
    },
  },

  computed: {
    // 对象写法直接return
    asideWidth() {
      return this.isCollapse == true ? "64px" : "200px";
    },
  },
  components: {},
  created() {
    this.getMenus();
  },
};
</script>

<style lang="scss" scoped>
* {
  box-sizing: border-box;
}
i {
  margin-right: 15px;
}
#about,
.box {
  width: 100%;
  height: 100%;
  background-color: #eee;
}
.header {
  background-color: #373d41;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 15px;
  span {
    color: #fff;
    font-size: 25px;
    cursor: pointer;
  }
}
//如果导航太多可能会溢出
.header_main {
  height: calc(100% - 60px);
  .aside {
    background-color: #343744;
    height: 100%;
    overflow: auto;
  }
}

.top-bar {
  background-color: #8f97b3;
  text-align: center;
  color: #fff;
  // 鼠标放上去变成小手
  cursor: pointer;
}
.el-menu-vertical-demo:not(.el-menu--collapse) {
  width: 200px;
  min-height: 400px;
}
::v-deep.el-menu {
  border: none;
}
</style>

图例:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 解释下侧导航

  1. 用到了ele组件库的经典布局 `el-container` ,  用到 `el-menu`, 请求测导航接口,循环,循环后打开一个测导航,全部都打开,需要配置unique-opened 保持一个子导航开启,不同的字体图标,需要用以id做key,icon做值,具体看代码实现
  2.   unique-opened 保持一个子导航开启,因为循环的是所有,打开一个就会打开全部,

                  所以开启 unique-opened,它是el-menu的属性,开启router属性值目的是否使用 vue-router 的模式,
                  启用该模式会在激活导航时以 index 作为 path 进行路由跳转,
                  在el-menu-item标签里设置跳转属性index='xx'

                2 还要开启el-submenu的属性唯一标识index它的值是字符串或空null
                它是可选值,如果你是循环必须要开启唯一标识,因为循环的是所有,打开一个就会打开全部,
                所以要一个字符串唯一标识来区分
                如果是一个个写的导航不用也可以,就是不循环一个个复制导航手写导航名字

                default-active="/users" 默认当前激活菜单的 index,它是el-menu的属性
                在el-menu标签写default-active="/users"

  3. 要多看组件库说明

  4.  

    watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

     

    watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

三 列表页源码:

列表页还是比较细碎的,注意细节

<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>
    <!-- 卡片视图 -->
    <el-card>
      <!-- 搜索 添加 -->
      <el-row :gutter="20">
        <el-col :span="6">
          <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-col>
        <el-col :span="4">
          <el-button type="primary" @click="addDialogVisible = true">添加用户</el-button>
        </el-col>
      </el-row>
      <!-- 用户列表区域 -->
      <el-table :data="userlist" border stripe>
        <!-- stripe: 斑马条纹
        border:边框-->
        <el-table-column type="index" label="#"></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 slot-scope="scope">
            <el-switch v-model="scope.row.mg_state" @change="userStateChanged(scope.row)"></el-switch>
          </template>
        </el-table-column>
        <el-table-column label="操作">
          <template slot-scope="scope">
            <el-button type="primary" icon="el-icon-edit" size="mini" circle @click="showEditDialog(scope.row.id)"></el-button>
            <el-button type="danger" icon="el-icon-delete" size="mini" circle @click="removeUserById(scope.row.id)"></el-button>
            <el-tooltip class="item" effect="dark" content="角色分配" :enterable="false" placement="top">
              <el-button type="warning" icon="el-icon-setting" size="mini" circle @click="showSetRole(scope.row)"></el-button>
            </el-tooltip>
          </template>
        </el-table-column>
      </el-table>
      <!-- 分页区域 -->
      <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="queryInfo.pagenum" :page-sizes="[5, 15, 20, 35]" :page-size="queryInfo.pagesize" layout="sizes, prev, pager, next, jumper,total" :total="total"></el-pagination>
    </el-card>
    <!-- 添加用户的对话框 -->
    <el-dialog title="添加用户" center :visible.sync="addDialogVisible" width="50%" @close="addDialogClosed">
      <!-- 内容主体 -->
      <el-form :model="addUserForm" ref="addUserFormRef" :rules="addUserFormRules" label-width="100px">
        <el-form-item label="用户名" prop="username">
          <el-input v-model="addUserForm.username"></el-input>
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input v-model="addUserForm.password"></el-input>
        </el-form-item>
        <el-form-item label="邮箱" prop="email">
          <el-input v-model="addUserForm.email"></el-input>
        </el-form-item>
        <el-form-item label="手机" prop="mobile">
          <el-input v-model="addUserForm.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="addUser">确 定</el-button>
      </span>
    </el-dialog>

    <!-- 修改用户的对话框 -->
    <el-dialog title="修改用户信息" :visible.sync="editDialogVisible" width="50%" @close="editDialogClosed">
      <!-- 内容主体 -->
      <el-form :model="editUserForm" ref="editUserFormRef" :rules="editUserFormRules" label-width="70px">
        <el-form-item label="用户名">
          <el-input v-model="editUserForm.username" disabled></el-input>
        </el-form-item>
        <el-form-item label="邮箱" prop="email">
          <el-input v-model="editUserForm.email"></el-input>
        </el-form-item>
        <el-form-item label="手机" prop="mobile">
          <el-input v-model="editUserForm.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="editUser">确 定</el-button>
      </span>
    </el-dialog>

    <!-- 分配角色对话框 -->
    <el-dialog title="分配角色" :visible.sync="setRoleDialogVisible" width="50%" @close="setRoleDialogClosed">
      <div>
        <p>当前用户:{{userInfo.username}}</p>
        <p>当前角色:{{userInfo.role_name}}</p>
        <p>
          分配角色:
          <el-select v-model="selectRoleId" filterable allow-create default-first-option placeholder="请选择文章标签">
            <el-option v-for="item in rolesLsit" :key="item.id" :label="item.roleName" :value="item.id"></el-option>
          </el-select>
        </p>
      </div>
      <span slot="footer" class="dialog-footer">
        <el-button @click="setRoleDialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="saveRoleInfo">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
export default {
  data() {
    // 自定义邮箱规则
    var checkEmail = (rule, value, callback) => {
      const regEmail = /^\w+@\w+(\.\w+)+$/;
      if (regEmail.test(value)) {
        // 合法邮箱
        return callback();
      }
      callback(new Error("请输入合法邮箱"));
    };
    // 自定义手机号规则
    var checkMobile = (rule, value, callback) => {
      const regMobile = /^1[34578]\d{9}$/;
      if (regMobile.test(value)) {
        return callback();
      }
      // 返回一个错误提示
      callback(new Error("请输入合法的手机号码"));
    };
    return {
      // 获取用户列表查询参数对象
      queryInfo: {
        query: "",
        // 当前页数
        pagenum: 1,
        // 每页显示多少数据
        pagesize: 5,
      },
      userlist: [],
      total: 0,
      // 添加用户对话框
      addDialogVisible: false,
      // 用户添加
      addUserForm: {
        username: "",
        password: "",
        email: "",
        mobile: "",
      },
      // 用户添加表单验证规则
      addUserFormRules: {
        username: [
          { required: true, message: "请输入用户名", trigger: "blur" },
          {
            min: 2,
            max: 10,
            message: "用户名的长度在2~10个字",
            trigger: "blur",
          },
        ],
        password: [
          { required: true, message: "请输入用户密码", trigger: "blur" },
          {
            min: 6,
            max: 18,
            message: "用户密码的长度在6~18个字",
            trigger: "blur",
          },
        ],
        email: [
          { required: true, message: "请输入邮箱", trigger: "blur" },
          { validator: checkEmail, trigger: "blur" },
        ],
        mobile: [
          { required: true, message: "请输入手机号码", trigger: "blur" },
          { validator: checkMobile, trigger: "blur" },
        ],
      },
      // 修改用户
      editDialogVisible: false,
      editUserForm: {},
      // 编辑用户表单验证
      editUserFormRules: {
        email: [
          { required: true, message: "请输入邮箱", trigger: "blur" },
          { validator: checkEmail, trigger: "blur" },
        ],
        mobile: [
          { required: true, message: "请输入手机号码", trigger: "blur" },
          { validator: checkMobile, trigger: "blur" },
        ],
      },
      // 分配角色对话框
      setRoleDialogVisible: false,
      // 当前需要被分配角色的用户
      userInfo: {},
      // 所有角色数据列表
      rolesLsit: [],
      // 已选中的角色Id值
      selectRoleId: "",
    };
  },
  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("获取用户列表失败!");
      }
      console.log(res.data);
      this.userlist = res.data.users;
      this.total = res.data.total;
    },
    // 监听 pagesize改变的事件
    handleSizeChange(newSize) {
      // console.log(newSize)
      this.queryInfo.pagesize = newSize;
      this.getUserList();
    },
    // 监听 页码值 改变事件
    handleCurrentChange(newSize) {
      // console.log(newSize)
      this.queryInfo.pagenum = newSize;
      this.getUserList();
    },
    // 监听 switch开关 状态改变
    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;
        return this.$message.error("更新用户状态失败");
      }
      this.$message({
        type: "success",
        message: '<i class="el-icon-s-promotion"></i>操作成功!',
        showClose: true,
        center: true,
        dangerouslyUseHTMLString: true,
      });
    },
    // 监听 添加用户对话框的关闭事件
    addDialogClosed() {
      this.$refs.addUserFormRef.resetFields();
    },
    // 添加用户
    addUser() {
      // 提交请求前,表单预验证
      this.$refs.addUserFormRef.validate(async (valid) => {
        // console.log(valid)
        // 表单预校验失败
        if (!valid) return;
        const { data: res } = await this.$http.post("users", this.addUserForm);
        if (res.meta.status !== 201) {
          this.$message.error("添加用户失败!");
        }
        this.$message.success("添加用户成功!");
        // 隐藏添加用户对话框
        this.addDialogVisible = false;
        this.getUserList();
      });
    },
    // 编辑用户信息
    async showEditDialog(id) {
      const { data: res } = await this.$http.get("users/" + id);
      if (res.meta.status !== 200) {
        return this.$message.error("查询用户信息失败!");
      }
      console.log(res);
      this.editUserForm = res.data;
      this.editDialogVisible = true;
    },
    // 监听修改用户对话框的关闭事件
    editDialogClosed() {
      this.$refs.editUserFormRef.resetFields();
    },
    // 修改用户信息
    editUser() {
      // 提交请求前,表单预验证
      this.$refs.editUserFormRef.validate(async (valid) => {
        // console.log(valid)
        // 表单预校验失败
        if (!valid) return;
        const { data: res } = await this.$http.put(
          "users/" + this.editUserForm.id,
          {
            email: this.editUserForm.email,
            mobile: this.editUserForm.mobile,
          }
        );
        if (res.meta.status !== 200) {
          this.$message.error("更新用户信息失败!");
        }
        // 隐藏添加用户对话框
        this.editDialogVisible = false;
        this.$message.success("更新用户信息成功!");
        this.getUserList();
      });
    },
    // 删除用户
    async removeUserById(id) {
      const confirmResult = await this.$confirm(
        "此操作将永久删除该用户, 是否继续?",
        "提示",
        {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        }
      ).catch((err) => err);
      // 点击确定 返回值为:confirm
      // 点击取消 返回值为: cancel
      if (confirmResult !== "confirm") {
        return this.$message.info("已取消删除");
      }
      const { data: res } = await this.$http.delete("users/" + id);
      if (res.meta.status !== 200) return this.$message.error("删除用户失败!");
      this.$message.success("删除用户成功!");
      this.getUserList();
    },
    // 展示分配角色的对话框
    async showSetRole(role) {
      this.userInfo = role;
      // 展示对话框之前,获取所有角色列表
      const { data: res } = await this.$http.get("roles");
      if (res.meta.status !== 200) {
        return this.$message.error("获取角色列表失败!");
      }
      this.rolesLsit = res.data;
      this.setRoleDialogVisible = true;
    },
    // 分配角色
    async saveRoleInfo() {
      if (!this.selectRoleId) {
        return this.$message.error("请选择要分配的角色");
      }
      const { data: res } = await this.$http.put(
        `users/${this.userInfo.id}/role`,
        { rid: this.selectRoleId }
      );
      if (res.meta.status !== 200) {
        return this.$message.error("更新用户角色失败!");
      }
      this.$message.success("更新角色成功!");
      this.getUserList();
      this.setRoleDialogVisible = false;
    },
    // 分配角色对话框关闭事件
    setRoleDialogClosed() {
      this.selectRoleId = "";
      this.userInfo = {};
    },
  },
};
</script>

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

列表 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 

 修改

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

    删除

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 文字提示 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 分页

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

文档 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

Element组件 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16  

分页详解:

  •   改变 layout="total, sizes, prev, pager, next, jumper" 顺序可以修改 分页布局
  •  @size-change="handleSizeChangeEvent"当前页事件 赋值后端规定参数 第一页 pagenum 
  •  @current-change="handleCurrentChangeEvent"页数改变事件 赋值后端规定参数 页容量 pagesize
/*
  templeate
*/
 <div class="block">
      <el-pagination 
         :page-sizes="[3, 5, 10]" 
         layout="total, sizes, prev, pager, next, jumper" 
         :total="total" 
         :page-size="3" 
         @size-change="handleSizeChangeEvent" 
         @current-change="handleCurrentChangeEvent">
       </el-pagination>
  </div>
/*
  data
*/
 // 后端必填参数
      obj: {
        // 第一页
        pagenum: 1,
        // 页容量
        pagesize: 10,
      },
/*

methods

*/
  
//这个是请求后端数据,我是经过封装api的,使用的时候可以直接用axios去请求数据,
     list() {
      getUserList(this.obj).then((res) => {
        console.log(res);
        // 将请求的数据赋值给变量
        this.tableData = res.users;
        // 总条数
        this.total = res.total;
      });
    },

    // 当前页事件 有默认参数newVal 赋值给默认定义的数据当前页(后端参数pagenum ),这样点击当前页,就把前端的第几页传给后端了,页数就会跟着变
    handleCurrentChangeEvent(newVal) {
      // console.log(newVal);
      // 将newVal赋值当前页,页数改变重新请求
      this.obj.pagenum = newVal;
      this.list();
    },

    // 页数改变事件 有默认参数newVal, 赋值给后端规定的 pagesize页容量,这样点击分页的页容量切换,
    //后端的参数pagesize也会变,页面改变了重新渲染
    handleSizeChangeEvent(newVal) {
      // console.log(nrwVal);
      this.obj.pagesize = newVal;
      this.list();
    },

 四 角色分配

  • dialog对话框

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

文档  

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

分配角色详解: 

  • 上面文档说了,需要用户id(拼接url),角色id(body参数体),看一下Select 选择器Element官网怎么定义属性、事件

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

  •  可以看出  v-model的值为当前被选中的el-option的value属性值, v-model等于:value等于角色id,得到角色id了,还需要后端用户id,这个可以根据点击分配权限按钮获取点击那一行,scope.row,
/*
  分配角色按钮
*/
<el-button type="warning" icon="el-icon-s-tools" circle @click="showSetRole(scope.row)"> </el-button>
  •  现在后端必填参数全部得到,可以定义Select 选择器标签了
  /*
     template
  */
      
      <el-dialog title="分配角色" :visible.sync="setRoleDialogVisible" width="50%" @close="setRoleDialogClosed">
        <div>
          <p>当前用户:{{userInfo.username}}</p>
          <br />
          <p>当前角色:{{userInfo.role_name}}</p>
          <br />
          <p>
            分配角色:
            <!-- v-model的值为当前被选中的el-option的value属性值, v-model等于:value等于角色id -->
            <el-select v-model="selectRoleId" filterable allow-create default-first-option placeholder="请选择文章标签">
              <!--el-select的:value是不同id 选项1 选项2  :label是不同值-->
              <el-option v-for="item in rolesList" :key="item.id" :label="item.roleName" :value="item.id"></el-option>
            </el-select>
          </p>
        </div>
        <span slot="footer" class="dialog-footer">
          <el-button @click="setRoleDialogVisible = false">取 消</el-button>
          <el-button type="primary" @click="saveRoleInfo">确 定</el-button>
        </span>
      </el-dialog>
 /*
    data数据 
*/   
// 分配角色对话框
    setRoleDialogVisible: false,
// 当前需要被分配角色的用户
   userInfo: {},
// 所有角色数据列表
   rolesList: [],
// 已选中的角色Id值 :value
  selectRoleId: "",

/*
  methods
*/

 // 展示分配角色的对话框
    async showSetRole(role) {
      //将点击的那一行数据赋值定义的userInfo,因为上面用户角色、用户姓名需要用到
      this.userInfo = role;
      // 展示对话框之前,获取所有角色列表,没角色怎么渲染,
     //rolesAjax()是我封装好的接口,根据自己写的来定义
      await rolesAjax().then((res) => {
        // console.log(res);
        if (res.meta.status !== 200) {
          return this.$message.error("获取角色列表失败!");
        }
        // 将获取的数据赋值自定义全部角色数据,用于渲染,如 主管、text、等
        this.rolesList = res.data;
        this.setRoleDialogVisible = true;
      });
    },
    // 分配角色 确定按钮
    async saveRoleInfo() {
      // selectRoleId就是下拉选择的v-model的默认值,v-model的默认值是:角色id
      if (!this.selectRoleId) {
        return this.$message.error("请选择要分配的角色");
      }
      // 用户id 角色id后端必传参数
      rolesOkBtnAjax(this.userInfo.id, {
        rid: this.selectRoleId,
      }).then((res) => {
        if (res.meta.status !== 200) {
          return this.$message.error("更新用户角色失败!");
        }
      });

      this.$message.success("更新角色成功!");
      location.reload();
      this.setRoleDialogVisible = false;
    },
    // 分配角色对话框关闭事件
    setRoleDialogClosed() {
      this.selectRoleId = "";
      this.userInfo = {};
    },

 

列表页完 。

五 、权限页面开发

权限列表:

权限列表图例:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

源码: 

<template>
    <div id="rights">
        <!-- 权限列表 -->
        <!-- 面包屑导航区 -->
        <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>
        <!-- 卡片视图 -->
        <el-card>
            <el-table :data="rightsList" border stripe>
                <el-table-column type="index" label="#"></el-table-column>
                <el-table-column label="权限名称" prop="authName"></el-table-column>
                <el-table-column label="路径">
                    <!-- 给路径加上 / -->
                    <template slot-scope="scope">
                        {{'/'+ scope.row.path}}
                    </template>
                </el-table-column>
                <el-table-column label="权限等级" prop="level">
                    <!-- 用el-tag判断 -->
                    <template slot-scope="scope">
                        <el-tag v-if="scope.row.level === '0'">一级</el-tag>
                        <el-tag type="success" v-else-if="scope.row.level === '1'">二级</el-tag>
                        <el-tag type="danger" v-else>三级</el-tag>
                    </template>
                </el-table-column>
            </el-table>
        </el-card>
    </div>
</template>

<script>
import { rightsAjax } from "../api";
export default {
  name: "Rights", //对应路由的name,params传参用路由的name 没有 /
  data() {
    return {
      // 定义权限列表数据
      rightsList: [],
    };
  },
  mounted() {
    this.getList();
  },
  methods: {
    async getList() {
      //同步方式执行异步请求,等待then的结果
      await rightsAjax().then((res) => {
        console.log(res);
        if (res.meta.status != 200) {
          this.$message({
            type: "error", // success error warning
            message: res.meta.msg,
            duration: 2000,
          });
          return;
        } else {
          //状态码等于200
          this.rightsList = res.data;
          this.$message({
            type: "success", // success error warning
            message: res.meta.msg,
            duration: 2000,
          });
        }
      });
    },
  },
};
</script>


<style lang="scss" scoped >
</style>
  •  这个没有难点,主要是el表格的熟练度掌握,用到了el-tag
  • 自定义列插槽 复杂点的在下面这个吧
       <el-table-column label="路径">
          <!-- 给路径加上 / -->
          <template slot-scope="scope">
            {{'/'+ scope.row.path}}
          </template>
        </el-table-column>
        <el-table-column label="权限等级" prop="level">
          <!-- 用el-tag判断 level是请求的数据,代表权限等级-->
          <template slot-scope="scope">
            <el-tag v-if="scope.row.level === '0'">一级</el-tag>
            <el-tag type="success" v-else-if="scope.row.level === '1'">二级</el-tag>
            <el-tag type="danger" v-else>三级</el-tag>
          </template>
        </el-table-column>

 角色列表:

  • 先看ajax文档事例:
  • 请求路径:roles

  • 请求方法:get

  • 响应数据说明

    • 第一层为角色信息

    • 第二层开始为权限说明,权限一共有 3 层权限

    • 最后一层权限,不包含 children 属性

  • 响应数据:

  • {
        "data": [
            {
                "id": 30,
                "roleName": "主管",
                "roleDesc": "技术负责人",
                "children": [
                    {
                        "id": 101,
                        "authName": "商品管理",
                        "path": null,
                        "children": [
                            {
                                "id": 104,
                                "authName": "商品列表",
                                "path": null,
                                "children": [
                                    {
                                        "id": 105,
                                        "authName": "添加商品",
                                        "path": null
                                    }
                                ]
                            }
                        ]
                    }
                ]
            }
        ],
        "meta": {
            "msg": "获取成功",
            "status": 200
        }
    }

源码:

<template>
  <!-- 角色列表页 -->
  <div id="roles">
    <!-- 面包屑导航区 -->
    <el-breadcrumb separator-class="el-icon-arrow-right">
      <el-breadcrumb-item :to="{ path: '/main' }">首页</el-breadcrumb-item>
      <el-breadcrumb-item>权限管理</el-breadcrumb-item>
      <el-breadcrumb-item>角色列表</el-breadcrumb-item>
    </el-breadcrumb>
    <!-- 卡片 -->
    <el-card>
      <!-- 添加角色按钮 -->
      <el-row>
        <el-col>
          <el-button type="primary" @click="AddRoleDialogVisible=true">添加角色</el-button>
        </el-col>
      </el-row>
      <!-- 表格 -->
      <!-- 
        type	对应列的类型。如果设置了 selection 则显示多选框;
        如果设置了 index 则显示该行的索引(从 1 开始计算);
        如果设置了 expand 则显示为一个可展开的按钮
       -->
      <!-- 展开行 -->
      <el-table :data="rolesList" border stripe>
        <el-table-column type="expand" width="50">
          <template slot-scope="scope">
            <!-- 给行加边框 加在el-row上-->
            <el-row v-for="(item1,index1) in scope.row.children" :key="item1.id" :class="['bd-bottom','display-center',index1==0?'bt-top':'']">
              <!-- 渲染一级权限 -->
              <el-col :span="5">
                <el-tag closable @close="removeId(scope.row,item1.id)">{{item1.authName}}</el-tag>
                <i class="el-icon-caret-right"></i>
              </el-col>
              <!-- 渲染二级、三级权限,二级权限的行中又分二级、三级  -->
              <el-col :span="19">
                <!-- 二级权限占6分,三级权限占13分,撑满二级权限el-col 19分 -->
                <el-row v-for="(item2,index2) in item1.children" :key="item2.id" :class="['display-center',index2==0?'':'bt-top']">
                  <!-- 注意:边框是加给行el-row,如果加给列就不会显示一行都有边框 -->
                  <el-col :span="6">
                    <el-tag type="success" closable @close="removeId(scope.row,item2.id)">{{item2.authName}}</el-tag>
                    <i class="el-icon-caret-right"></i>
                  </el-col>
                  <!-- 三级 -->
                  <el-col :span="13">
                    <el-tag type="warning" closable @close="removeId(scope.row,item3.id)" v-for="item3 in item2.children" :key="item3.id">
                      {{item3.authName}}
                    </el-tag>
                  </el-col>
                </el-row>
              </el-col>
            </el-row>
            <!-- v-pre vue中跳过标签,不会编译
                 html原样输出 pre标签
               -->
            <!-- <pre>
              {{scope.row}}
            </pre> -->
          </template>
        </el-table-column>
        <el-table-column label="序号" type="index" width="60">
        </el-table-column>
        <el-table-column label="角色名称" prop="roleName">
        </el-table-column>
        <el-table-column label="角色描述" prop="roleDesc">
        </el-table-column>
        <el-table-column label="操作" width="300">
          <!-- 
            size	尺寸	string	medium / small / mini	—
            type	类型	string	primary / success / warning / danger / info / text
            常用图标
            搜索:search 编辑:edit  对号:check
            邮件:message 收藏(星):star-off
            删除:delete
           -->
          <template slot-scope="scope">
            <el-button type="primary" size="mini" icon="el-icon-edit" @click="showEditDialog(scope.row.id)">编辑</el-button>
            <el-button type="danger" size="mini" icon="el-icon-delete" @click="removeRoleById(scope.row.id)">删除</el-button>
            <el-button type="warning" size="mini" icon="el-icon-setting" @click="showDialogVisible(scope.row)">分配权限</el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- 分配权限对话框 -->
      <el-dialog title="分配权限" :visible.sync="dialogVisible" width="50%" @close="resetDefaultKeys">
        <!-- 树形控件 :data展示的数据  :props="defaultProps"定义展示的内容, -->
        <!-- node-key="id" 指定只要选中了节点就是选中了我的id值 -->
        <!-- :default-checked-keys='defaultKeys' 默认选中的数组 就是让所有三级被勾选-->
        <!-- ref="treeRef" 获取当前dom -->
        <!-- getCheckedKeys 返回目前被选中的节点的 key 所组成的数组  -->
        <!-- getHalfCheckedKeys  返回目前半选中的节点的 key 所组成的数组-->
        <el-tree :data="treeList" :props="defaultProps" ref="treeRef" :default-checked-keys='defaultKeys' default-expand-all show-checkbox node-key="id"></el-tree>
        <!-- 树形控件结束 -->
        <span slot="footer" class="dialog-footer">
          <el-button @click="dialogVisible = false">取 消</el-button>
          <el-button type="primary" @click="onClick">确 定</el-button>
        </span>
      </el-dialog>
      <!-- 添加角色对话框 -->
      <el-dialog title="添加角色" :visible.sync="AddRoleDialogVisible" width="40%" @close="addRoleDialogClosed">
        <el-form :model="addRoleForm" ref="addRoleFormRef" :rules="addRoleFormRules" label-width="100px">
          <el-form-item label="角色名称" prop="roleName">
            <el-input v-model="addRoleForm.roleName"></el-input>
          </el-form-item>
          <el-form-item label="角色描述" prop="roleDesc">
            <el-input v-model="addRoleForm.roleDesc"></el-input>
          </el-form-item>
        </el-form>
        <span slot="footer" class="dialog-footer">
          <el-button @click="AddRoleDialogVisible = false">取 消</el-button>
          <el-button type="primary" @click="addRoles">确 定</el-button>
        </span>
      </el-dialog>
      <!-- 编辑角色对话框 -->
      <el-dialog title="编辑角色" :visible.sync="editRoleDialogVisible" width="40%">
        <el-form :model="editRoleForm" ref="editRoleFormRef" :rules="editRoleFormRules" label-width="100px">
          <el-form-item label="角色名称" prop="roleName">
            <el-input v-model="editRoleForm.roleName"></el-input>
          </el-form-item>
          <el-form-item label="角色描述" prop="roleDesc">
            <el-input v-model="editRoleForm.roleDesc"></el-input>
          </el-form-item>
        </el-form>
        <span slot="footer" class="dialog-footer">
          <el-button @click="editRoleDialogVisible = false">取 消</el-button>
          <el-button type="primary" @click="editRolesBtn">确 定</el-button>
        </span>
      </el-dialog>
    </el-card>
  </div>
</template>

<script>
// users用户列表页(展示分配角色的对话框)也用到这个接口所以直接调用
import {
  rolesAjax,
  deleteAjax,
  rightsTreeAjax,
  roleAuthorizationAjax,
  addRolesAjax,
  ediRolesAjax,
  ediRoles1Ajax,
  deleteRolesAjax,
} from "../api";
export default {
  name: "Roles",
  data() {
    return {
      // 定义所有的角色数据变量
      rolesList: [],
      // 分配权限的默认值
      dialogVisible: false,
      // 权限数据
      treeList: [],
      // 定义树形控件展示的内容
      defaultProps: {
        //  父子节点通过那个属性嵌套
        children: "children",
        // 看到的是那个值,树形控件显示的节点
        label: "authName",
      },
      // 默认展开被选中的数组,将三级id递归进来,
      //注意:点击x关闭对话框需要将数组清空,因为点击不一样,不能让之前的id还存在在数组中
      defaultKeys: [],
      //当前即将分配权限的Id,需要拼接url
      roleId: 0,
      //   添加用户对话框
      AddRoleDialogVisible: false,
      // 添加角色表单
      addRoleForm: {},
      //   添加角色表单验证
      addRoleFormRules: {
        roleName: [
          { required: true, message: "请输入角色名称", trigger: "blur" },
        ],
        roleDesc: [
          { required: true, message: "请输入角色描述", trigger: "blur" },
        ],
      },
      //   编辑用户对话框
      editRoleDialogVisible: false,
      //   编辑角色信息
      editRoleForm: {},
      //   编辑角色表单验证
      editRoleFormRules: {
        roleName: [
          { required: true, message: "请输入角色名称", trigger: "blur" },
        ],
        roleDesc: [
          { required: true, message: "请输入角色描述", trigger: "blur" },
        ],
      },
    };
  },
  methods: {
    // 请求角色数据
    async getList() {
      //同步方式执行异步请求,等待then的结果
      await rolesAjax().then((res) => {
        // console.log(res);
        if (res.meta.status != 200) {
          // 如果状态码不是200就return 错误消息
          return this.$message({
            type: "error", // success error warning
            message: res.meta.msg,
            duration: 2000,
          });
        } else {
          //状态码等于200
          this.rolesList = res.data;
          this.$message({
            type: "success", // success error warning
            message: res.meta.msg,
            duration: 2000,
          });
        }
      });
    },
    // 删除角色tag标签
    removeId(row, rightId) {
      this.$confirm("此操作将永久删除该文件, 是否继续?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          deleteAjax(row.id, rightId).then((res) => {
            // console.log(res);
            if (res.meta.status == 200) {
              //如果重新调用list会刷新页面,接口返回的是最新的数据,可以将返回的数据赋值children
              // this.getList();
              row.children = res.data;
              this.$message({
                type: "success",
                message: "删除成功!",
              });
            }
          });
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消删除",
          });
        });
    },
    // 显示分配权限弹框事件
    async showDialogVisible(row) {
      this.roleId = row.id;
      // 获取分配权限数据
      await rightsTreeAjax().then((res) => {
        // console.log(res);
        // 如果状态码不是200就会return这一句
        if (res.meta.status !== 200)
          return this.$message({
            type: "error", // success error warning
            message: res.meta.msg,
            duration: 2000,
          });
        // 等于200就将数据赋值树形权限数据渲染
        this.treeList = res.data;
        // 在对话框开启之前需要获取所有三级权限的id 注意:必须写到ajax中不能写出去否则切换角色无法找到三级id
        // 当前角色 就是父级了,是ajax请求的数据,将他传入递归函数,他里面有children,要在ajax中调用递归
        // console.log(row);
        this.getKeys(row, this.defaultKeys);
        this.dialogVisible = true;
        // 我将上面两行写在ajax外,导致无法找到三级id,因为递归的数据是在ajax中得到的,所以写在ajax中
        // 正确思路应该是 点击权限按钮-请求数据-赋值tree树形数据-调用递归-显示弹框
        // 用思维解bug:点击无法在弹框前调用递归,肯定是调用递归时问题
      });
    },
    // 递归函数 需求:将三级的id添加到arr数组中
    getKeys(node, defaultKeys) {
      //没有children追加到defaultKeys,因为一二级有children所以执行下一步递归
      if (!node.children) return defaultKeys.push(node.id);
      // 这个地方很精妙!
      // 如果有children就将循环的每一项,再次调用一下本函数
      // 递归:在运行中自己调用自己
      // 这个函数就会一次次遍历,最终执行到三级权限,没有children就会执行上一句代码
      node.children.forEach((item) => this.getKeys(item, defaultKeys));
    },
    //注意:点击x关闭对话框需要将三级id数组清空,因为点击不一样,不能让之前的id还存在在数组中
    resetDefaultKeys() {
      this.defaultKeys = [];
    },
    // 点击按钮关闭分配权限弹框
    async onClick() {
      //element 定义用ref获取keys
      // console.log(this.$refs.treeRef.getCheckedNodes());
      // 定义一个数组用于合并全选、半选的权限
      const mergeKeys = [
        ...this.$refs.treeRef.getHalfCheckedKeys(),
        ...this.$refs.treeRef.getCheckedKeys(),
      ];
      // console.log(mergeKeys); // [101,104,2033,555,556]
      const idStr = mergeKeys.join(",");
      // console.log(idStr);//  '101,104,2033,555,556'
      // 发起请求
      await roleAuthorizationAjax(this.roleId, { rids: idStr }).then((res) => {
        if (res.meta.status !== 200) {
          return this.$message.error("分配权限失败!");
        }
        this.$message.success("分配权限成功!");
        this.getList();
        this.dialogVisible = false;
      });
    },
    // 添加角色
    addRoles() {
      this.$refs.addRoleFormRef.validate(async (valid) => {
        if (!valid) return;
        addRolesAjax(this.addRoleForm).then((res) => {
          console.log(res);
          if (res.meta.status !== 201) {
            this.$message.error("添加角色失败!");
          }
          this.$message.success("添加角色成功!");
          this.AddRoleDialogVisible = false;
          this.getList();
        });
      });
    },
    // 添加角色对话框的关闭
    addRoleDialogClosed() {
      this.$refs.addRoleFormRef.resetFields();
    },
    // 获取编辑角色
    async showEditDialog(id) {
      await ediRolesAjax(id).then((res) => {
        // console.log(res);
        if (res.meta.status !== 200)
          return this.$message.error("查询角色信息失败!");
        this.editRoleForm = res.data;
        this.editRoleDialogVisible = true;
      });
    },

    // 编辑确定按钮
    editRolesBtn() {
      this.$refs.editRoleFormRef.validate(async (valid) => {
        // console.log(valid); // 空就是false 有值就是true
        // 表单非空校验失败,是false就return
        if (!valid) return;
        await ediRoles1Ajax(this.editRoleForm.roleId, {
          roleName: this.editRoleForm.roleName,
          roleDesc: this.editRoleForm.roleDesc,
        }).then((res) => {
          if (res.meta.status !== 200) {
            this.$message.error("更新角色信息失败!");
          }
          // 隐藏编辑角色对话框
          this.editRoleDialogVisible = false;
          this.$message.success("更新角色信息成功!");
          this.getList();
        });
      });
    },
    // 删除用户
    removeRoleById(id) {
      this.$confirm("此操作将永久删除该文件, 是否继续?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          deleteRolesAjax(id).then((res) => {
            // console.log(res);
            if (res.meta.status == 200) {
              //如果重新调用list会刷新页面,接口返回的是最新的数据,可以将返回的数据赋值children
              this.getList();
              this.$message({
                type: "success",
                message: "删除成功!",
              });
            }
          });
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消删除",
          });
        });
    },
  },
  created() {
    this.getList();
  },
};
</script>


<style lang="scss" scoped >
.el-tag {
  margin: 7px;
}
// 加上底边框
.bd-bottom {
  border-bottom: 1px solid #eee;
  // 只让一级的的el-row有margin值,因为bd-bottom在el-row上,
  // 所以给bd-bottom加就相当于给一级权限加,二级权限的el-row
  // 是不需要margin值,要撑满二级权限的19份
  margin: 0 55px;
}
// 加上,上边框(只加第一个,判断第0个索引)
.bt-top {
  border-top: 1px solid #eee;
}
// 让tag居中
.display-center {
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>

一步步分析,一点点写 :

1、面包屑 

    <!-- 面包屑导航区 -->
    <el-breadcrumb separator-class="el-icon-arrow-right">
      <el-breadcrumb-item :to="{ path: '/main' }">首页</el-breadcrumb-item>
      <el-breadcrumb-item>权限管理</el-breadcrumb-item>
      <el-breadcrumb-item>角色列表</el-breadcrumb-item>
    </el-breadcrumb>

2、卡片区-添加按钮 

 <!-- 卡片 -->
    <el-card>
      <!-- 添加角色按钮 -->
      <el-row >
        <el-col >
          <el-button type="primary" >添加角色</el-button>
        </el-col>
      </el-row>
      <!-- 表格 -->
    </el-card>

图例:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 3、获取表格数据:

<script>
// users用户列表页(展示分配角色的对话框)也用到这个接口所以直接调用
import { rolesAjax } from "../api";
export default {
  name: "Roles",
  data() {
    return {
      // 定义所有的角色数据变量
      rolesList: [],
    };
  },
  created() {
    this.getList();
  },
  methods: {
    async getList() {
      //同步方式执行异步请求,等待then的结果
      await rolesAjax().then((res) => {
        // console.log(res);
        if (res.meta.status != 200) {
          // 如果状态码不是200就return 错误消息
          return this.$message({
            type: "error", // success error warning
            message: res.meta.msg,
            duration: 2000,
          });
        } else {
          //状态码等于200
          this.rolesList = res.data;
          this.$message({
            type: "success", // success error warning
            message: res.meta.msg,
            duration: 2000,
          });
        }
      });
    },
  },
  
};
</script>

 4、渲染列表

 <!-- 表格 -->
      <!-- 
        type:对应列的类型。如果设置了 selection 则显示多选框;
        如果设置了 index 则显示该行的索引(从 1 开始计算);
        如果设置了 expand 则显示为一个可展开的按钮
       -->
      <el-table :data="rolesList" border stripe>
        <el-table-column label="展开权限" type="expand" width="120">
        </el-table-column>
        <el-table-column label="序号" type="index" width="60">
        </el-table-column>
        <el-table-column label="角色名称" prop="roleName">
        </el-table-column>
        <el-table-column label="角色描述" prop="roleDesc">
        </el-table-column>
        <el-table-column label="操作" width="200">
          <!-- 
            size	尺寸	string	medium / small / mini	—
            type	类型	string	primary / success / warning / danger / info / text
            常用图标:
            搜索:search 编辑:edit  对号:check
            邮件:message 收藏(星):star-off
            删除:delete
           -->
          <template slot-scope="scope">
            <el-button type="primary" size="mini" icon="el-icon-edit"></el-button>
            <el-button type="danger" size="mini" icon="el-icon-delete"></el-button>
            <el-button type="warning" size="mini" icon="el-icon-setting"></el-button>
          </template>
        </el-table-column>
      </el-table>
  • 分配权限等下做,先做简单的,以上展开是空的

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16 5、分配权限

  • 5.1、 先打印出来数据看一下,代码被编译输出了

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

  • 5.1、 使用html原样输出 pre标签, 代码就不是一大坨了

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16 5.2、分配一级 二级权限

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

代码使用el-row

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16 效果:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 5.3、渲染一级权限

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

为一级权限el-tag加边框

//展开行的表格html
 <el-table-column  type="expand" width="50">
          <template slot-scope="scope">
            <!-- 给行加边框 加在el-row上-->
            <el-row v-for="(item1,index1) in scope.row.children" :key="item1.id"
                   :class="['bd-bottom',index1==0?'bt-top':'']">
              <!-- 渲染一级权限 -->
              <el-col :span="5">
                <el-tag >{{item1.authName}}</el-tag>
              </el-col>
              <!-- 渲染二级、三级权限 -->
              <el-col :span="19">2</el-col>
            </el-row>
            <!-- v-pre vue中跳过标签,不会编译
                 html原样输出 pre标签
               -->
            <!-- <pre>
              {{scope.row}}
            </pre> -->
          </template>
        </el-table-column>
//对应的css

<style lang="scss" scoped >
.el-tag {
  margin: 7px;
}
// 加上底边框
.bd-bottom {
  border-bottom: 1px solid #eee;
  // 只让一级的的el-row有margin值,因为bd-bottom在el-row上,
  // 所以给bd-bottom加就相当于给一级权限加,二级权限的el-row
  // 是不需要margin值,要撑满二级权限的19份
  margin: 0 55px;
}
// 加上,上边框(只加第一个,判断第0个索引)
.bt-top {
  border-top: 1px solid #eee;
}
</style>

 图例:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

加箭头图标

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 5.4 、二级权限渲染

  • 在二级权限中 又写了一个el-row,要占满二级权限的19份,分别占6份、13份

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

图例:

  •  二级权限是绿色的,修改el-tag的type值,顺便在加上右箭头:

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

  •  给二级权限加上边框,第一个索引对应的会多出一个上边框,所以将第一个索引的上边框取消掉

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

5.5、渲染三级权限

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16 6、优化bug

  • 防止屏幕太小,导致布局乱套 
//位置:src/assets 公共样式 在main引入
html,
body,
#app {
    width: 100%;
    height: 100%;
    overflow-x: hidden;
    background-color: #f6f6f6;
    /* 防止屏幕过小导致页面布局乱套,所以加上最小宽度 */
    min-width: 1000px;
}
  • 让el-tag居中 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 7、实现删除tag标签

  • 文档 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

<el-tag type="warning" closable @close="removeId(scope.row,item3.id)" 
       v-for="item3 in item2.children" :key="item3.id">
                      {{item3.authName}}
                    </el-tag>

当然这里同时为一级、二级也添加@close事件 ,就不一一写了,

  // 删除角色tag标签
    removeId(row, rightId) {
         this.$confirm("此操作将永久删除该文件, 是否继续?", "提示", {
           confirmButtonText: "确定",
           cancelButtonText: "取消",
           type: "warning",
         })
           .then(() => {
             deleteAjax(row.id, rightId).then((res) => {
               // console.log(res);
               if (res.meta.status == 200) {
                 //如果重新调用list会刷新页面,接口返回的是最新的数据,可以将返回的数据赋值children
                 // this.getList();
                 row.children = res.data;
                 this.$message({
                   type: "success",
                   message: "删除成功!",
                 });
               }
             });
           })
           .catch(() => {
             this.$message({
               type: "info",
               message: "已取消删除",
             });
           });
       },

8、分配权限

  • 首先给按钮绑定一个事件弹出一个对话框dialog 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 // 默认展开被选中的数组,其实就是将三级id递归进来,
      //注意:点击x关闭对话框需要将数组清空,因为点击不一样,不能让之前的id还存在在数组中
      defaultKeys: [],

 

 // 显示分配权限弹框
    async showDialogVisible(row) {
//点击确定按钮接口要用角色id,先存起来
      this.roleId = row.id;
      // 获取分配权限数据
      await rightsTreeAjax().then((res) => {
        // console.log(res);
        // 如果状态码不是200就会return这一句
        if (res.meta.status !== 200)
          return this.$message({
            type: "error", // success error warning
            message: res.meta.msg,
            duration: 2000,
          });
        // 等于200就将数据赋值树形权限数据渲染
        this.treeList = res.data;
        // 在对话框开启之前需要获取所有三级权限的id 注意:必须写到ajax中不能写出去否则切换角色无法找到三级id
        // 当前角色 就是父级了,是ajax请求的数据,将他传入递归函数,他里面有children,要在ajax中调用递归
        // console.log(row);
//!!!调用递归事件,获取三级id
        this.getKeys(row, this.defaultKeys);
        this.dialogVisible = true;
        // 我将上面两行写在ajax外,导致无法找到三级id,因为递归的数据是在ajax中得到的,所以写在ajax中
        // 正确思路应该是 点击权限按钮-请求数据-赋值tree树形数据-调用递归-显示弹框
        // 用思维解bug:点击无法在弹框前调用递归,肯定是调用递归时问题
      });
    },
  • 递归函数获取三级id
 // 递归函数 需求:将三级的id添加到arr数组中
    getKeys(node, defaultKeys) {
      //没有children追加到defaultKeys,因为一二级有children所以执行下一步递归
      if (!node.children) return defaultKeys.push(node.id);
      // 这个地方很精妙!
      // 如果有children就将循环的每一项,再次调用一下本函数
      // 递归:在运行中自己调用自己
      // 这个函数就会一次次遍历,最终执行到三级权限,没有children就会执行上一句代码
      node.children.forEach((item) => this.getKeys(item, defaultKeys));
    },

----------------------------- 

递归 分配权限取出三级id

写一个递归取出这个树形结构的数组 的三级id 一个简单例子

----------------------------- 

​
 var list = {
        "id": 30,
        "roleName": "主管",
        "roleDesc": "技术负责人",
        // 一级有children
        "children": [{
            "id": 101,
            "authName": "商品管理",
            "path": null,
            // 二级有children
            "children": [{
                "id": 104,
                "authName": "商品列表",
                "path": null,
                // 三级 没有children
                "children": [{
                    "id": 105,
                    "authName": "添加商品",
                    "path": null
                }, {
                    "id": 1098,
                    "authName": "商品1",
                    "path": null
                }, {
                    "id": 1055,
                    "authName": "添加8465",
                    "path": null
                }, ]
            }]
        }]
    };

​

递归 遍历

 var arr = [];
    // 递归函数 需求:将三级的id添加到arr数组中
    function getKeys(node, arr) {
        //没有children追加到arr,因为一二级有children所以执行下一步递归
        if (!node.children) return arr.push(node.id)
//一二级有children所以执行这一步递归,将遍历的每一项,再次调用这个函数,函数运行中自己调用自己称为递归
        node.children.forEach(v => getKeys(v, arr))
    }
    getKeys(list, arr)
    console.log(arr);//得到了三级的id 105、1098、1055

但是有的时候后台的数据不是我们想要的就需要自己转化数据结构

 线性转化树形

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>

</body>
<script>
// 线性转化树形
    let list = [{
        name: '一级',
        id: '1',
        pid: null
    }, {
        name: '二级',
        id: '2',
        pid: '1'
    }, {
        name: '三级1',
        id: '3-1',
        pid: '2'
    }, {
        name: '三级2',
        id: '3-2',
        pid: '2'
    }]

    let treeArr = []

    function funtree(arr, tree, id) {
        if (typeof arr == 'undefined') return // 递归结束条件
        arr.forEach(item => {
            if (item.pid == id) {
                tree.push(item) // 一级
                if (!item.children) {
                    item.children = []
                    funtree(arr, item.children, item.id)
                }
            }
        })
    }
    let copyArr = JSON.parse(JSON.stringify(list))
        // 根据第一级的pid去传funtree() 函数的第三个参数
    funtree(copyArr, treeArr, null)

    // 删除空的children
    function del(arr) {
        if (typeof arr == 'undefined') return
        arr.forEach(item => {
            item.children.length != 0 ? del(item.children) : delete item.children;
        })
    }
    del(treeArr)

    console.log(list);
    console.log(treeArr);
</script>

</html>

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

树形转为线性 :

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>

</body>
<script>
    // 树形转为线性
    let treeArr = [{
        name: '一级',
        id: '1',
        children: [{
            name: '二级',
            id: '2',
            children: [{
                name: '三级1',
                id: '3-1',
            }, {
                name: '三级2',
                id: '3-2',
            }]
        }]
    }]

    let list = []

    function fn(arr, list) {
        if (!arr) { // 递归结束条件
            return
        }
        arr.forEach(item => {
            list.push(item)
            if (item.children) { // 如果存在children属性
                fn(item.children, list)
            }
        })
    }

    let copyArr = JSON.parse(JSON.stringify(treeArr))
    fn(copyArr, list)
        // 删除原有的children
    list.forEach(item => {
        if (item.children) {
            delete item.children
        }
    })

    console.log(treeArr);
    console.log(list);
</script>

</html>

 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

----------------------------- 

分配权限确定按钮

  • 思路:就是将全选、半选的权限数组转为字符串,(接口要求),将角色id拼接url,字符串作为post请求体

api接口封装在另一个页面,我是这么写的

//  角色授权
export const roleAuthorizationAjax = (id, data) => request({ method: "POST", url: `roles/${id}/rights`, data });

    // 分配权限确定按钮
    async onClick() {
      //element 定义用ref获取keys
      // console.log(this.$refs.treeRef.getCheckedNodes());
      // 定义一个数组用于合并全选、半选的权限
      const mergeKeys = [
        ...this.$refs.treeRef.getHalfCheckedKeys(),
        ...this.$refs.treeRef.getCheckedKeys(),
      ];
      // console.log(mergeKeys); // [101,104,2033,555,556]
      const idStr = mergeKeys.join(",");
      // console.log(idStr);//  '101,104,2033,555,556'
      // 发起请求
      await roleAuthorizationAjax(this.roleId, { rids: idStr }).then((res) => {
        if (res.meta.status !== 200) {
          return this.$message.error("分配权限失败!");
        }
        this.$message.success("分配权限成功!");
        this.getList();
   // 点击按钮关闭分配权限弹框
        this.dialogVisible = false;
      });
    },

 至此点击分配权限对话框按钮,已完成!

9、添加用户

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

data: 

//   添加用户对话框默认值
      AddRoleDialogVisible: false,

 html:

 <!-- 添加角色对话框 -->
      <el-dialog title="添加角色" :visible.sync="AddRoleDialogVisible" width="40%" @close="addRoleDialogClosed">
        <el-form :model="addRoleForm" ref="addRoleFormRef" :rules="addRoleFormRules" label-width="100px">
          <el-form-item label="角色名称" prop="roleName">
            <el-input v-model="addRoleForm.roleName"></el-input>
          </el-form-item>
          <el-form-item label="角色描述" prop="roleDesc">
            <el-input v-model="addRoleForm.roleDesc"></el-input>
          </el-form-item>
        </el-form>
        <span slot="footer" class="dialog-footer">
          <el-button @click="AddRoleDialogVisible = false">取 消</el-button>
          <el-button type="primary" @click="addRoles">确 定</el-button>
        </span>
      </el-dialog>

 确定按钮是事件:

   // 添加角色
    addRoles() {
      this.$refs.addRoleFormRef.validate(async (valid) => {
        if (!valid) return;
        addRolesAjax(this.addRoleForm).then((res) => {
          console.log(res);
          if (res.meta.status !== 201) {
            this.$message.error("添加角色失败!");
          }
          this.$message.success("添加角色成功!");
          this.AddRoleDialogVisible = false;
          this.getList();
        });
      });
    },
    // 添加角色对话框的关闭
    addRoleDialogClosed() {
      this.$refs.addRoleFormRef.resetFields();
    },

10、编辑用户 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 // 获取编辑角色
    async showEditDialog(id) {
      await ediRolesAjax(id).then((res) => {
        // console.log(res);
        if (res.meta.status !== 200)
          return this.$message.error("查询角色信息失败!");
        this.editRoleForm = res.data;
        this.editRoleDialogVisible = true;
      });
    },

    // 编辑确定按钮
    editRolesBtn() {
      this.$refs.editRoleFormRef.validate(async (valid) => {
        // console.log(valid); // 空就是false 有值就是true
        // 表单非空校验失败,是false就return
        if (!valid) return;
        await ediRoles1Ajax(this.editRoleForm.roleId, {
          roleName: this.editRoleForm.roleName,
          roleDesc: this.editRoleForm.roleDesc,
        }).then((res) => {
          if (res.meta.status !== 200) {
            this.$message.error("更新角色信息失败!");
          }
          // 隐藏编辑角色对话框
          this.editRoleDialogVisible = false;
          this.$message.success("更新角色信息成功!");
          this.getList();
        });
      });
    },

11、删除用户 

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 

 // 删除用户
    removeRoleById(id) {
      this.$confirm("此操作将永久删除该文件, 是否继续?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          deleteRolesAjax(id).then((res) => {
            // console.log(res);
            if (res.meta.status == 200) {     
              this.getList();
              this.$message({
                type: "success",
                message: "删除成功!",
              });
            }
          });
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消删除",
          });
        });
    },

哈哈! 权限列表完!watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16  

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_6,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_6,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_6,color_FFFFFF,t_70,g_se,x_16

 六、商品管理

1、商品分类

效果图: 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16 文档:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

好了需求清楚后,代码写起。。

  • 请求数据渲染表格 
 // 表格所需的数据
      cateList: [],
// 请求接口赋值表格所需的数据
    async getCateList() {
      // 第一种方式 解构data变成res
      // let { data: res } = await getCategoriesListAjax(this.queryInfo);
      // //   console.log(res.total);
      // //  console.log(res.result);
      // this.total = res.total;
      // this.cateList = res.result;

      // 第二种方式 .then
      await getCategoriesListAjax(this.queryInfo).then((res) => {
        this.total = res.data.total;
        this.cateList = res.data.result;
      });
    },

注意:element-ui并没有treeTable控件 ,需要我们自行下载插件

github网址:树形表格控件地址

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

下载依赖:

cnpm i vue-table-with-tree-grid -S

在main.js注册全局组件

import Vue from 'vue'
import ZkTable from 'vue-table-with-tree-grid'

Vue.use(ZkTable);

Api 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 treeTable

 <!-- 表格 -->
      <!-- data数据 必须是树形结构 -->
      <!-- columns 设置属性 -->
      <!-- border边框 -->
      <!-- show-index 显示索引 
         index-text="#" 定义索引文字 
          border 是否显示纵向分割线
         :show-row-hover 鼠标悬停时,是否高亮当前行 
         :expand-type="false" 关闭展开行 
         :selection-type="false" 关闭选择框 -->


      <tree-table class="treeTable" :data="cateList" :columns="columns" 
        :selection-type="false" :expand-type="false" index-text="索引" 
        :show-row-hover="false" show-index border>
     <tree-table >
   // 定义表格字段
      columns: [
        {
          label: "分类名称",
          prop: "cat_name",
        },
        {
          label: "是否有效",
          // 当前列 自定义模板
          type: "template",
          template: "isOk", //模板名字,还要在插槽中用slot='xx'指定 例如:<template slot="isOk" slot-scope="scope">
        },
        {
          label: "排序",
          // 当前列 自定义模板
          type: "template",
          template: "order",
        },
        {
          label: "操作",
          // 当前列 自定义模板
          type: "template",
          template: "opt",
          width: "200", //指定宽度
        },
      ],

 

2、分页: 

 // 查询条件
      queryInfo: {
        type: 3,
        pagenum: 1,
        pagesize: 5,
      },

 


      <!-- 分页 -->
      <el-pagination @size-change="handleSizeChange" 
         @current-change="handleCurrentChange" :current-page="queryInfo.pagenum" 
         :page-sizes="[3, 5, 10, 15]" :page-size="queryInfo.pagesize" 
         layout="total, sizes, prev, pager, next, jumper" :total="total">
</el-pagination>
 // 监听 pageSizeChange
    handleSizeChange(newSize) {
      this.queryInfo.pagesize = newSize;
      this.getCateList();
    },
    // 监听 页数改变
    handleCurrentChange(newPage) {
      // console.log(newPage);
      this.queryInfo.pagenum = newPage;
      this.getCateList();
    },

3、编辑: 

// data
 // 编辑对话框 控制
      editCateDialogVisible: false,
      // 编辑分类表单验证
      editCateFormRules: {
        cat_name: [
          { required: true, message: "请输入分类名称", trigger: "blur" },
        ],
      },
      // 编辑表单 绑定对象
      editCateForm: {},

 

// html 
  <!-- 编辑分类的对话框 -->
      <el-dialog title="编辑分类" :visible.sync="editCateDialogVisible" width="50%">
        <el-form :model="editCateForm" :rules="editCateFormRules" ref="editCateFormRef" label-width="100px">
          <el-form-item label="分类名称:" prop="cat_name">
            <el-input v-model="editCateForm.cat_name"></el-input>
          </el-form-item>
        </el-form>
        <span slot="footer" class="dialog-footer">
          <el-button @click="editCateDialogVisible = false">取 消</el-button>
          <el-button type="primary" @click="editBtn">确 定</el-button>
        </span>
      </el-dialog>

 

// Js

    // 获取编辑内容
    async showEditCateDialog(id) {
      await getEditCategoriesListAjax(id).then((res) => {
        // console.log(res);
        if (res.meta.status !== 200)
          return this.$message.error("获取分类失败!");
        this.editCateForm = res.data;
        this.editCateDialogVisible = true;
      });
    },

    // 编辑确定按钮
    editBtn() {
      this.$refs.editCateFormRef.validate(async (valid) => {
        if (!valid) return;
        await editCategoriesListAjax(this.editCateForm.cat_id, {
          cat_name: this.editCateForm.cat_name,
        }).then((res) => {
          if (res.meta.status !== 200)
            return this.$message.error("更新分类名失败!");
          this.$message.success("更新分类名成功!");
          this.getCateList();
          this.editCateDialogVisible = false;
        });
      });
    },
  },

4、删除: 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

//Js
// 删除
    removeCate(id) {
      this.$confirm("此操作将永久删除该文件, 是否继续?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          deleteCategoriesListAjax(id).then((res) => {
            // console.log(res);
            if (res.meta.status !== 200) {
              return this.$message({
                type: "error", // success error warning
                message: "删除失败",
                duration: 2000,
              });
            }
            this.getCateList();
            this.$message({
              type: "success",
              message: "删除成功!",
            });
          });
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消删除",
          });
        });
    },

 5、添加分类:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 //接口请求的分类的数据
      parentCateList: [],
   // 添加分类
      dialogVisible: false,

    // 添加分类按钮显示对话框
    showAddCateDialog() {
      //思路: 显示对话框之前,获取父级分类的数据,[type可选参数]如果不传递,则默认获取所有级别的分类
      this.getParentCateList();
      this.dialogVisible = true;
    },

    // 获取父级分类的数据
    async getParentCateList() {
      getCategoriesListAjax().then((res) => {
        // console.log(res);
        if (res.meta.status !== 200) {
          return this.$message.error("获取父级分类失败!");
        }
        this.parentCateList = res.data;
      });
    },

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16


      <!-- 添加分类的对话框 -->
      <!-- ref="addCateFormRef"  点x号获取关闭事件-->
      <!--  @close="addCateDialogClosed" 关闭事件 -->
      <!-- this.$refs.addCateFormRef.resetFields(); 可以通过$refs获取dom关闭 elm规定 -->
      <el-dialog title="添加分类" :visible.sync="dialogVisible" width="50%" @close="addCateDialogClosed">
        <el-form :model="addCateForm" :rules="addCateFormRules" ref="addCateFormRef" label-width="100px">
          <el-form-item label="分类名称:" prop="cat_name">
            <el-input v-model="addCateForm.cat_name"></el-input>
          </el-form-item>
          <el-form-item label="父级分类:">
            <!-- :options:数据源 -->
            <!-- :props:指定配置对象有value、children、label... -->
            <!--  v-model=""选中的父级分类id数组,必须指定的是数组,不能是具体的值,因为它是多级分类的数组  -->
            <!-- @change 只要选中的父级分类id数组发生变化就会触发 -->
            <!-- 默认第一级不能选中,可以加这个属性change-on-select -->
            <el-cascader v-model="selectedKeys" :options="parentCateList" 
      :props="cascaderProps" @change="parentCateChanged" 
     change-on-select="true" clearable filterable style="width: 100%"></el-cascader>
          </el-form-item>
        </el-form>

        <span slot="footer" class="dialog-footer">
          <el-button @click="dialogVisible = false">取 消</el-button>
          <el-button type="primary" @click="addCate">确 定</el-button>
        </span>
      </el-dialog>
//data

 // 指定级联选择器的配置对象
      cascaderProps: {
        value: "cat_id", //具体选中的那个值得属性 一般是id
        label: "cat_name", //你所看到的是哪一个属性
        children: "children", //子父级嵌套的属性是什么
        expandTrigger: "hover", // 配置触发选项 hover/click
      },
      // 选中的父级分类id数组
      selectedKeys: [],
 // 添加分类 重置表单 点击X号
    addCateDialogClosed() {
      this.$refs.addCateFormRef.resetFields();
      // 选中的父级分类id数组清空
      this.selectedKeys = [];
      // 清空之后 分类名称/分类父ID,应该也为0
      this.addCateForm.cat_level = 0;
      this.addCateForm.cat_pid = 0;
    },

    //只要选中的父级分类id数组发生变化就会触发, <el-cascader>的@change事件
    parentCateChanged(val) {
      // val形参就是变化后父级分类id数组
      // console.log(val);

      /* 
      思路: 如果分类id数组长度>0,说明选中了父级,因为添加的在后面,它父分类的id是
            数组中最后一项。就将数组最后一项赋值父级id,就得到父分类的id 
            level等级表示: `0`表示一级分类;`1`表示二级分类;`2`表示三级分类, 得出数组长度就是 level等级,
            数组长度不大于0没有选中父级,父级分类的Id和当前分类的等级还是等于0
      */

      // 如果selectKeys 数组的长度>0 说明选中父级分类,反之没有选中父级
      if (this.selectedKeys.length > 0) {
        // 父级分类的Id
        this.addCateForm.cat_pid = this.selectedKeys[
          this.selectedKeys.length - 1
        ];
        // 当前分类的等级
        this.addCateForm.cat_level = this.selectedKeys.length;
        return 0;
      } else {
        /* 数组长度不大于0没有选中父级,父级分类的Id和当前分类的等级还是等于0 */
        // 父级分类的Id
        this.addCateForm.cat_pid = 0;
        // 当前分类的等级
        this.addCateForm.cat_level = 0;
      }
    },

6、商品分类源码: 

<template>
  <div id="">
    <!-- 面包屑导航区 -->
    <el-breadcrumb separator-class="el-icon-arrow-right">
      <el-breadcrumb-item :to="{ path: '/main' }">首页</el-breadcrumb-item>
      <el-breadcrumb-item>商品管理</el-breadcrumb-item>
      <el-breadcrumb-item>商品分类</el-breadcrumb-item>
    </el-breadcrumb>
    <!-- 表格 -->
    <el-card>
      <el-row>
        <el-col>
          <el-button type="primary" @click="showAddCateDialog">添加分类</el-button>
        </el-col>
      </el-row>
      <!-- 表格 -->
      <!-- data数据 必须是树形结构 -->
      <!-- columns 设置属性 -->
      <!-- border边框 -->
      <!-- show-index 显示索引 
         index-text="#" 定义索引文字 
          border 是否显示纵向分割线
         :show-row-hover 鼠标悬停时,是否高亮当前行 
         :expand-type="false" 关闭展开行 
         :selection-type="false" 关闭选择框 -->
      <tree-table class="treeTable" :data="cateList" :columns="columns" :selection-type="false" :expand-type="false" index-text="索引" :show-row-hover="false" show-index border>
        <!-- 是否有效 -->
        <template slot="isOk" slot-scope="scope">
          <i class="el-icon-success" style="color: lightgreen" v-if="scope.row.cat_deleted === false"></i>
        </template>
        <!-- 排序 -->
        <template slot="order" slot-scope="scope">
          <el-tag size="mini" v-if="scope.row.cat_level === 0">一级</el-tag>
          <el-tag size="mini" type="success" v-else-if="scope.row.cat_level === 1">二级</el-tag>
          <el-tag size="mini" type="warning" v-else>三级</el-tag>
        </template>
        <!-- 操作 -->
        <template slot="opt" slot-scope="scope">
          <el-button type="primary" icon="el-icon-edit" size="mini" @click="showEditCateDialog(scope.row.cat_id)">编辑</el-button>
          <el-button type="danger" icon="el-icon-delete" size="mini" @click="removeCate(scope.row.cat_id)">删除</el-button>
        </template>
      </tree-table>

      <!-- 分页 -->
      <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="queryInfo.pagenum" :page-sizes="[3, 5, 10, 15]" :page-size="queryInfo.pagesize" layout="total, sizes, prev, pager, next, jumper" :total="total"></el-pagination>

      <!-- 添加分类的对话框 -->
      <!-- ref="addCateFormRef"  点x号获取关闭事件-->
      <!--  @close="addCateDialogClosed" 关闭事件 -->
      <!-- this.$refs.addCateFormRef.resetFields(); 可以通过$refs获取dom关闭 elm规定 -->
      <el-dialog title="添加分类" :visible.sync="dialogVisible" width="50%" @close="addCateDialogClosed">
        <el-form :model="addCateForm" :rules="addCateFormRules" ref="addCateFormRef" label-width="100px">
          <el-form-item label="分类名称:" prop="cat_name">
            <el-input v-model="addCateForm.cat_name"></el-input>
          </el-form-item>
          <el-form-item label="父级分类:">
            <!-- :options:数据源 -->
            <!-- :props:指定配置对象有value、children、label... -->
            <!--  v-model=""选中的父级分类id数组,必须指定的是数组,不能是具体的值,因为它是多级分类的数组  -->
            <!-- @change 只要选中的父级分类id数组发生变化就会触发 -->
            <!-- 默认第一级不能选中,可以加这个属性change-on-select -->
            <el-cascader v-model="selectedKeys" :options="parentCateList" :props="cascaderProps" @change="parentCateChanged" change-on-select="true" clearable filterable style="width: 100%"></el-cascader>
          </el-form-item>
        </el-form>

        <span slot="footer" class="dialog-footer">
          <el-button @click="dialogVisible = false">取 消</el-button>
          <el-button type="primary" @click="addCate">确 定</el-button>
        </span>
      </el-dialog>

      <!-- 编辑分类的对话框 -->
      <el-dialog title="编辑分类" :visible.sync="editCateDialogVisible" width="50%">
        <el-form :model="editCateForm" :rules="editCateFormRules" ref="editCateFormRef" label-width="100px">
          <el-form-item label="分类名称:" prop="cat_name">
            <el-input v-model="editCateForm.cat_name"></el-input>
          </el-form-item>
        </el-form>
        <span slot="footer" class="dialog-footer">
          <el-button @click="editCateDialogVisible = false">取 消</el-button>
          <el-button type="primary" @click="editBtn">确 定</el-button>
        </span>
      </el-dialog>

    </el-card>

  </div>
</template>

<script>
import {
  getCategoriesListAjax,
  addCategoriesListAjax,
  editCategoriesListAjax,
  getEditCategoriesListAjax,
  deleteCategoriesListAjax,
} from "../api";
export default {
  name: "", //对应路由的name,params传参用路由的name 没有 /
  data() {
    return {
      total: 0,
      // 定义表格字段
      columns: [
        {
          label: "分类名称",
          prop: "cat_name",
        },
        {
          label: "是否有效",
          // 当前列 自定义模板
          type: "template",
          template: "isOk", //模板名字,还要在插槽中用slot='xx'指定 例如:<template slot="isOk" slot-scope="scope">
        },
        {
          label: "排序",
          // 当前列 自定义模板
          type: "template",
          template: "order",
        },
        {
          label: "操作",
          // 当前列 自定义模板
          type: "template",
          template: "opt",
          width: "200", //指定宽度
        },
      ],
      // 查询条件
      queryInfo: {
        type: 3,
        pagenum: 1,
        pagesize: 5,
      },
     
      // 表格所需的数据
      cateList: [],
      // 编辑对话框 控制
      editCateDialogVisible: false,
      // 编辑分类表单验证
      editCateFormRules: {
        cat_name: [
          { required: true, message: "请输入分类名称", trigger: "blur" },
        ],
      },
      // 编辑表单 绑定对象
      editCateForm: {},
      //  添加用户对话框
      dialogVisible: false,
      // 添加分类
      dialogVisible: false,
      // 添加分类对象,后端参数
      addCateForm: {
        // 将要添加分类名称
        cat_name: "",
        // 分类父级id
        cat_pid: 0,
        // 分类等级:`0`表示一级分类;`1`表示二级分类;`2`表示三级分类
        cat_level: 0,
      },
      // 添加分类表单的验证规则
      addCateFormRules: {
        cat_name: [
          { required: true, message: "请输入分类名称", trigger: "blur" },
        ],
      },
      //接口请求的分类的数据
      parentCateList: [],
      // 指定级联选择器的配置对象
      cascaderProps: {
        value: "cat_id", //具体选中的那个值得属性 一般是id
        label: "cat_name", //你所看到的是哪一个属性
        children: "children", //子父级嵌套的属性是什么
        expandTrigger: "hover", // 配置触发选项 hover/click
      },
      // 选中的父级分类id数组
      selectedKeys: [],
    };
  },
  methods: {
    // 监听 pageSizeChange
    handleSizeChange(newSize) {
      this.queryInfo.pagesize = newSize;
      this.getCateList();
    },
    // 监听 页数改变
    handleCurrentChange(newPage) {
      // console.log(newPage);
      this.queryInfo.pagenum = newPage;
      this.getCateList();
    },

    // 请求接口赋值表格所需的数据
    async getCateList() {
      // 第一种方式 解构data变成res
      // let { data: res } = await getCategoriesListAjax(this.queryInfo);
      // //   console.log(res.total);
      // //  console.log(res.result);
      // this.total = res.total;
      // this.cateList = res.result;

      // 第二种方式 .then
      await getCategoriesListAjax(this.queryInfo).then((res) => {
        this.total = res.data.total;
        this.cateList = res.data.result;
      });
    },

    // 添加分类按钮显示对话框
    showAddCateDialog() {
      //思路: 显示对话框之前,获取父级分类的数据,[type可选参数]如果不传递,则默认获取所有级别的分类
      this.getParentCateList();
      this.dialogVisible = true;
    },

    // 获取父级分类的数据
    async getParentCateList() {
      getCategoriesListAjax().then((res) => {
        // console.log(res);
        if (res.meta.status !== 200) {
          return this.$message.error("获取父级分类失败!");
        }
        this.parentCateList = res.data;
      });
    },

    // 添加分类 重置表单 点击X号
    addCateDialogClosed() {
      this.$refs.addCateFormRef.resetFields();
      // 选中的父级分类id数组清空
      this.selectedKeys = [];
      // 清空之后 分类名称/分类父ID,应该也为0
      this.addCateForm.cat_level = 0;
      this.addCateForm.cat_pid = 0;
    },

    //只要选中的父级分类id数组发生变化就会触发, <el-cascader>的@change事件
    parentCateChanged(val) {
      // val形参就是变化后父级分类id数组
      // console.log(val);

      /* 
      思路: 如果分类id数组长度>0,说明选中了父级,因为添加的在后面,它父分类的id是
            数组中最后一项。就将数组最后一项赋值父级id,就得到父分类的id 
            level等级表示: `0`表示一级分类;`1`表示二级分类;`2`表示三级分类, 得出数组长度就是 level等级,
            数组长度不大于0没有选中父级,父级分类的Id和当前分类的等级还是等于0
      */

      // 如果selectKeys 数组的长度>0 说明选中父级分类,反之没有选中父级
      if (this.selectedKeys.length > 0) {
        // 父级分类的Id
        this.addCateForm.cat_pid = this.selectedKeys[
          this.selectedKeys.length - 1
        ];
        // 当前分类的等级
        this.addCateForm.cat_level = this.selectedKeys.length;
        return 0;
      } else {
        /* 数组长度不大于0没有选中父级,父级分类的Id和当前分类的等级还是等于0 */
        // 父级分类的Id
        this.addCateForm.cat_pid = 0;
        // 当前分类的等级
        this.addCateForm.cat_level = 0;
      }
    },

    // 添加分类
    addCate() {
      // 测试添加的值,父分类id是否是数组最后一项,
      // this.addCateForm 是 添加分类对象,后端参数有:cat_level分类层级、cat_name分类名称、cat_pid分类父 ID
      // console.log(this.addCateForm);
      this.$refs.addCateFormRef.validate(async (valid) => {
        if (!valid) return;
        await addCategoriesListAjax(this.addCateForm).then((res) => {
          // console.log(res);
          if (res.meta.status !== 201) {
            return this.$message.error("添加分类失败!");
          }
          this.$message.success("添加分类成功!");
          this.getCateList();
          this.dialogVisible = false;
        });
      });
    },

    // 删除
    removeCate(id) {
      this.$confirm("此操作将永久删除该文件, 是否继续?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          deleteCategoriesListAjax(id).then((res) => {
            // console.log(res);
            if (res.meta.status !== 200) {
              return this.$message({
                type: "error", // success error warning
                message: "删除失败",
                duration: 2000,
              });
            }
            this.getCateList();
            this.$message({
              type: "success",
              message: "删除成功!",
            });
          });
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消删除",
          });
        });
    },

    // 获取编辑内容
    async showEditCateDialog(id) {
      await getEditCategoriesListAjax(id).then((res) => {
        // console.log(res);
        if (res.meta.status !== 200)
          return this.$message.error("获取分类失败!");
        this.editCateForm = res.data;
        this.editCateDialogVisible = true;
      });
    },

    // 编辑确定按钮
    editBtn() {
      this.$refs.editCateFormRef.validate(async (valid) => {
        if (!valid) return;
        await editCategoriesListAjax(this.editCateForm.cat_id, {
          cat_name: this.editCateForm.cat_name,
        }).then((res) => {
          if (res.meta.status !== 200)
            return this.$message.error("更新分类名失败!");
          this.$message.success("更新分类名成功!");
          this.getCateList();
          this.editCateDialogVisible = false;
        });
      });
    },
  },

  created() {
    this.getCateList();
  },
};
</script>


<style lang="scss" scoped >
</style>

七、分类参数

文档: 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

效果图:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 1、渲染级联选择器和面包屑和el-alert

//html
  <!-- 卡片视图 -->
    <el-card>
      <!-- 警告区域 -->
      <!-- :closable="false" 关闭X号 注意要加:因为他是布尔值不是字符串 
           不加会报错:Expected Boolean, got String with value "false". -->
      <!-- show-icon 显示图标 -->
      <el-alert title="注意:只允许为第三级分类设置相关参数!" type="warning" show-icon :closable="false"></el-alert>
      <!-- 选择商品分类区域 -->
      <el-row class="cat_opt">
        <el-col>
          <span>选择商品分类:</span>
          <!-- 商品分类的级联选择框 -->
          <el-cascader v-model="selectedCateKeys" :options="cateList" :props="cateProps" @change="handleChange"></el-cascader>
        </el-col>
      </el-row>

    </el-card>

 

//JS

<script>
import { getCategoriesListAjax } from "../api";
export default {
  name: "Params", //对应路由的name,params传参用路由的name 没有 /
  created() {
    this.getCateList();
  },
  data() {
    return {
      //   级联选择框双向绑定数组
      selectedCateKeys: [],
      // 商品分类列表
      cateList: [],
      //级联选择框的配置对象
      cateProps: {
        expandTrigger: "hover",
        value: "cat_id",
        label: "cat_name",
        children: "children",
      },
    };
  },
  methods: {
    //   获取所有的商品分类列表
    async getCateList() {
      await getCategoriesListAjax().then((res) => {
        // console.log(res);
        if (res.meta.status !== 200) {
          return this.$message.error("获取商品数据列表失败!");
        }
        this.cateList = res.data;
      });
    },
    // change监听的是v-model数组的变化
    handleChange(val) {
      // 里面有默认形参val,val就是级联选择框双向绑定数组
      console.log(val);
    },
  },
  components: {},
};
</script>


<style lang="scss" scoped >
.cat_opt {
  margin: 15px 0;
}
</style>

 2、只允许获取三级分类

  • 监听级联选择器数组的长度等于3就是三级分类

 // change监听的是v-model数组的变化
    handleChange(val) {
      // 里面有默认形参val,val就是级联选择框双向绑定数组
      // console.log(val);
      // 只允许选中三级分类思路:监听数组的长度等于3就是三级分类
      if (this.selectedCateKeys.length !== 3) {
        this.selectedCateKeys = [];
        return;
      }
      // 是三级分类执行以下代码

    },

3、实现没有选中三级分类添加按钮就不显示

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 computed: {
    //思路: 根据级联选择数组长度判断是否是3级,(length长度),返回true or false,在用按钮disable自定义属性绑定这个函数
    isDisable() {
      if (this.selectedCateKeys.length !== 3) return true;
      return false;
    },
  },

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 4、参数获取

 文档:watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

拿到三级分类id


  computed: {
    //添加按钮是否禁用思路:
    // 根据级联选择数组长度判断是否是3级,(length长度),返回true or false,在用按钮disable自定义属性绑定这个函数
    isDisable() {
      if (this.selectedCateKeys.length !== 3) return true;
      return false;
    },

    // 当前选中的三级分类Id
    getCateId() {
      // 如果数长度是3说明选中了三级分类
      if (this.selectedCateKeys.length === 3) {
        // return 数组中最后一项
        return this.selectedCateKeys[2];
      }
      // 没有选中三级分类return null
      return null;
    },
  },

明确了参数后,就可以发起接口请求了,注意:在级联选择器需要获取数据,在切换tabs也需要获取数据,所以将获取数据封装成函数而且不能直接赋值,因为有两个表格,所以要判断是那个表格


  methods: {
    //   获取所有的商品分类列表
    async getCateList() {
      await getCategoriesListAjax().then((res) => {
        // console.log(res);
        if (res.meta.status !== 200) {
          return this.$message.error("获取商品数据列表失败!");
        }
        this.cateList = res.data;
      });
    },

    // change监听的是v-model数组的变化
    handleChange(val) {
      // 里面有默认形参val,val就是级联选择框双向绑定数组
      // console.log(val);
      // 只允许选中三级分类思路:监听数组的长度等于3就是三级分类
      if (this.selectedCateKeys.length !== 3) {
        this.selectedCateKeys = [];
        return;
      }
      // 是三级分类执行以下代码
      this.getParamsData(); //调用函数
    },

    // tabs
    handleClick(val) {
      // console.log(this.activeName);
      this.getParamsData();
    },

    // 获取动态参数和静态参数封装函数
    getParamsData() {
      getParamsListAjax(this.getCateId, { sel: this.activeName }).then(
        (res) => {
          // console.log(res);
          if (res.meta.status !== 200) {
            return this.$message.error("获取参数列表失败!");
          }
          // 不能直接赋值,因为有两个表格,所以要判断是那个表格
          if (this.activeName === "many") {
            this.manyTableData = res.data;
          } else {
            this.onlyTableData = res.data;
          }
        }
      );
    },
  },

 5、渲染 动态参数表格、静态属性表格:


          <!-- 动态参数表格 start-->
          <el-table :data="manyTableData" border stripe>
            
            <!-- 展开列 -->
            <el-table-column type="expand">
              1
            </el-table-column>

            <!-- 索引列 -->
            <el-table-column type="index"></el-table-column>
            <el-table-column label="参数名称" prop="attr_name"></el-table-column>
            <el-table-column>
              <template slot-scope="scope">
                <el-button type="primary" icon="el-icon-edit" size="mini">编辑</el-button>
                <el-button type="danger" icon="el-icon-delete" size="mini">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
          <!-- 动态参数表格 end -->
  <!-- 静态属性表格 start-->
          <el-table :data="onlyTableData" border stripe>
            <!-- 展开列 -->
            <!-- 展开列 -->
            <el-table-column type="expand">

            </el-table-column>
            <!-- 索引列 -->
            <el-table-column type="index"></el-table-column>
            <el-table-column label="属性名称" prop="attr_name"></el-table-column>
            <el-table-column>
              <template slot-scope="scope">
                <el-button type="primary" icon="el-icon-edit" size="mini">编辑</el-button>
                <el-button type="danger" icon="el-icon-delete" size="mini">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
          <!-- 静态属性表格 end-->

 图例:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 6、添加参数:

文档:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 

//  computed
//动态参数和静态属性公用一个对话框,所以对话框title不能写死
    getTitleText() {
      if (this.activeName === "many") {
        return "动态参数";
      }
      return "静态属性";
    },
//data
 
//   控制添加参数对话框的显示与隐藏
      addDialogVisible: false,
      //   添加表单的数据对象
      addFrom: {
        attr_name: "",
      },
      //   添加表单的验证规则
      addFromRules: {
        attr_name: [
          { required: true, message: "请输入参数名称", trigger: "blur" },
        ],
      },
//html

    <!-- 添加参数对话框 动态参数和静态属性公用一个-->
    <el-dialog :title=" '添加' + getTitleText" :visible.sync="addDialogVisible" width="50%">
      <el-form :model="addFrom" :rules="addFromRules" ref="addFromRef" @close="addDialogClosed" label-width="100px">
        <el-form-item :label="getTitleText" prop="attr_name">
          <el-input v-model="addFrom.attr_name"></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="addParams">确 定</el-button>
      </span>
    </el-dialog>

 

// JS

    // 监听对话框的关闭事件
    addDialogClosed() {
      this.$refs.addFromRef.resetFields();
    },
    
    // 添加参数
   async addParams() {
      this.$refs.addFromRef.validate(async (valid) => {
        if (!valid) return;
       await addParamsListAjax(this.getCateId,{attr_name: this.addFrom.attr_name,
            attr_sel: this.activeName,}).then(res=>{
         
        if (res.meta.status !== 201) {
          return this.$message.error("添加参数失败!");
        }
        this.$message.success("添加参数成功!");
        this.addDialogVisible = false;
        // 调用封装函数 获取动态参数和静态参数,因为渲染不同表格是在这个封装函数中写的
        this.getParamsData();
       });
       
      });
    },

  7、修改参数:

动态属性、静态参数都要添加事件,公用一个对话框,只是title不一样,先查询获取,在确定提交

删除、编辑、提交编辑一般都是点击事件传id,编辑先获取,在回填,在提交

文档:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

// data 
 //   编辑对话框
      editDialogVisible: false,
      //   修改表单数据对象
      editFrom: {},
      //   修改表单验证规则
      editFromRules: {
        attr_name: [
          { required: true, message: "请输入参数名称", trigger: "blur" },
        ],
      },

 

//html

 <!-- 编辑参数对话框 -->
    <el-dialog :title=" '修改' + getTitleText" :visible.sync="editDialogVisible" width="50%" @close="editDialogClosed">
      <el-form :model="editFrom" :rules="editFromRules" ref="editFromRef" label-width="100px">
        <el-form-item :label="getTitleText" prop="attr_name">
          <el-input v-model="editFrom.attr_name"></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="editParams">确 定</el-button>
      </span>
    </el-dialog>
// JS 
    // 显示编辑对话框 并 请求编辑数据,回填
    async showEditDialogEv(attrId) {
      //  当前属性id
      //  console.log(attrId);
      await getEditParamsListAjax(this.getCateId, attrId, {
        attr_sel: this.activeTabsName,
      }).then((res) => {
        if (res.meta.status !== 200) {
          return this.$message.error("获取分类失败!");
        }
        this.editFrom = res.data;
        this.editDialogVisible = true;
      });
    },
    // 重置修改表单
    editDialogClosed() {
      this.$refs.editFromRef.resetFields();
    },
    // 修改参数
    async editParams() {
      this.$refs.editFromRef.validate(async (valid) => {
        if (!valid) return;

        await EditOkParamsListAjax(this.getCateId, this.editFrom.attr_id, {
          attr_name: this.editFrom.attr_name,
          attr_sel: this.activeName,
        }).then((res) => {
          if (res.meta.status !== 200) {
            return this.$message.error("修改参数失败!");
          }
          this.$message.success("修改参数成功!");
          this.getParamsData();
          this.editDialogVisible = false;
        });
      });
    },

   8、删除参数:

 文档:watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 

//JS

// 根据Id删除对应的参数项
    async removeParams(attrId) {
      const confirmResult = await this.$confirm(
        "此操作将永久删除该参数, 是否继续?",
        "提示",
        {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        }
      ).catch((err) => err);
      if (confirmResult !== "confirm") {
        return this.$message.info("已取消删除!");
      }

      await deleteEditOkParamsListAjax(this.getCateId, attrId).then((res) => {
        if (res.meta.status !== 200) {
          return this.$message.error("删除参数失败!");
        }
        this.$message.success("删除参数成功!");
        this.getParamsData();
      });
    },

添加、编辑公用一个组件,最重要的是这个封装函数,根据不同的tab值(only,many),判断是哪个表格 

调用接口传入不同的值,最后调用这个封装函数,判断tab值,赋值所对应表格,实现公用一个组件,最后渲染不同的值


    // 封装函数: 获取动态参数和静态参数封装函数
    getParamsData() {// getCateId 计算属性三级id ------activeName tab默认选中值
      getParamsListAjax(this.getCateId, { sel: this.activeName }).then(
        (res) => {
          // console.log(res);
          if (res.meta.status !== 200) {
            return this.$message.error("获取参数列表失败!");
          }
          // 不能直接赋值,因为有两个表格,所以要判断是那个表格
          if (this.activeName === "many") {
            this.manyTableData = res.data;
          } else {
            this.onlyTableData = res.data;
          }
        }
      );
    },

    9、表格展开操作:

9.1、渲染tab

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 需要在封装函数中转化一下

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

如果不判断空数组的话直接字符串转化数组,如果数组为空就会渲染出空的tag标签,这不是我们想要的,所以判断数组中是否为空 

控制台打印:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

tag渲染: 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 9.2、input和button 切换

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

         渲染每一个展开行都公用了 inputVisible,所以展开行的tag会联动,不要公用一个inputVisible,因为有多行都用到了tag,解决:在请求获取动态参数和静态参数封装函数的时候,               
 // 给item每一项添加字段inputVisible,默认是否显示input 

  item.inputVisible = false;
  // 给item每一项添加字段inputVisible,文本框的输入值
  item.inputValue = "";
  在使用tag的时候,在表格获取scope.row.inputVisible,这样点击就不会所有行tag被选中、   

//html

<!--
渲染每一个展开行都公用了 inputVisible,所以一所有展开行会联动,不要公用一个inputVisible,因为有多行都用到了tag,解决:在请求获取动态参数和静态参数封装函数的时候,               
 // 给item每一项添加字段inputVisible,默认是否显示input
   item.inputVisible = false;
  // 给item每一项添加字段inputVisible,文本框的输入值
  item.inputValue = "";
  在使用tag的时候,用scope.row.inputVisible,这样点击就不会所有行tag被选中、     
  -->
<el-input class="input-new-tag" v-if="scope.row.inputVisible" 
v-model="scope.row.inputValue" ref="saveTagInput" 
size="small" @keyup.enter.native="handleInputConfirm(scope.row)" 
@blur="handleInputConfirm(scope.row)">
</el-input>

<el-button v-else class="button-new-tag" size="small" @click="showInput(scope.row)">+ New Tag</el-button>

 封装函数:

  • 封装函数中添加字段

    // 封装函数: 获取动态参数和静态参数封装函数
    getParamsData() {
      // getCateId 计算属性三级id ------activeName tab默认选中值
      getParamsListAjax(this.getCateId, { sel: this.activeName }).then(
        (res) => {
          // console.log(res);
          res.data.forEach((item) => {
            //通过三元表达式判断attr_vals是否为空
            item.attr_vals = item.attr_vals ? item.attr_vals.split(" ") : [];
            // 解决点击一个另一个也会变化,不能公用一个
          //防止tag联动 !!!
            // 给item每一项添加字段inputVisible,默认是否显示input
            item.inputVisible = false;
            // 给item每一项添加字段inputVisible,文本框的输入值
            item.inputValue = "";
          });
          if (res.meta.status !== 200) {
            return this.$message.error("获取参数列表失败!");
          }
          // 不能直接赋值,因为有两个表格,所以要判断是那个表格
          if (this.activeName === "many") {
            this.manyTableData = res.data;
          } else {
            this.onlyTableData = res.data;
          }
        }
      );
// JS


    // 文本框失去焦点,或者按下Enter触发
    handleInputConfirm(row) {
      // 输入的内容为空时,清空
      if (row.inputValue.trim().length === 0) {
        row.inputValue = "";
        row.inputVisible = false;
        return;
      }
      // 不为空去掉前后空格,追加到数组中
      row.attr_vals.push(row.inputValue.trim());
      row.inputValue = "";
      row.inputVisible = false;
      // 提交到接口,保存添加的tag
      this.saveAttrVals(row);
    },    

    // 将对attr_vals(Tag) 的操作 调用接口保存到数据库
    async saveAttrVals(row) {
      await EditTagsOkParamsListAjax(this.getCateId, row.attr_id, {
        attr_name: row.attr_name,
        attr_sel: row.attr_sel,
        // 后台的attr_vals是字符串空格隔开的,我们在获取的时候转为数组了,
        // 现在提交数据到接口,还是要再次转为字符串
        attr_vals: row.attr_vals.join(" "),
      }).then((res) => {
        if (res.meta.status !== 200) {
          return this.$message.error("修改参数项失败!");
        }
        this.$message.success("修改参数项成功!");
      });
    },

    // 点击按钮显示输入框
    showInput(row) {
      row.inputVisible = true;
      //   让输入框自动获取焦点
      // $nextTick方法的作用:当页面元素被重新渲染之后,才会至指定回调函数中的代码
      this.$nextTick(() => {
        this.$refs.saveTagInput.$refs.input.focus();
      });
    },

    // 删除对应的参数可选项也需要调用接口,所以将接口封装函数
    handleClose(i, row) {
      // 获取当前行删除一个,调用接口封装
      row.attr_vals.splice(i, 1);
      this.saveAttrVals(row);
    },

10 、优化分类参数bug,选中非三级分类,清空表格

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

分类参数完整源码:

<template>
  <div id="params">
    <!-- 面包屑导航区 -->
    <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>

    <!-- 卡片视图 -->
    <el-card>
      <!-- 警告区域 -->
      <!-- :closable="false" 关闭X号 注意要加:因为他是布尔值不是字符串 
           不加会报错:Expected Boolean, got String with value "false". -->
      <!-- show-icon 显示图标 -->
      <el-alert title="注意:只允许为第三级分类设置相关参数!" type="warning" show-icon :closable="false"></el-alert>

      <!-- 选择商品分类区域 -->
      <el-row class="cat_opt">
        <el-col>
          <span>选择商品分类:</span>
          <!-- 商品分类的级联选择框 -->
          <el-cascader v-model="selectedCateKeys" :options="cateList" :props="cateProps" @change="handleChange" clearable></el-cascader>
        </el-col>
      </el-row>

      <!-- tabs -->
      <el-tabs v-model="activeName" @tab-click="handleClick">
        <el-tab-pane label="动态参数" name="many">
          <el-button type="primary" :disabled="isDisable" @click="addDialogVisible = true">添加属性</el-button>

          <!-- 动态参数表格 start-->
          <el-table :data="manyTableData" border stripe>

            <!-- 展开列 -->
            <el-table-column type="expand">

              <!--插槽自定义列 渲染tag标签 start -->
              <template slot-scope="scope">

                <!-- tag start -->
                <el-tag v-for="(item, i) in scope.row.attr_vals" :key="i" closable @close="handleClose(i, scope.row)">{{item}}</el-tag>
                <!-- tag end -->

                <!-- input与button 输入 start-->
                <el-input class="input-new-tag" v-if="scope.row.inputVisible" v-model="scope.row.inputValue" ref="saveTagInput" size="small" @keyup.enter.native="handleInputConfirm(scope.row)" @blur="handleInputConfirm(scope.row)"></el-input>
                <el-button v-else class="button-new-tag" size="small" @click="showInput(scope.row)">+ New Tag</el-button>
                <!-- input与button 输入 end-->

              </template>
              <!--插槽自定义列 渲染tag标签 start -->

            </el-table-column>

            <!-- 索引列 -->
            <el-table-column type="index"></el-table-column>
            <el-table-column label="参数名称" prop="attr_name"></el-table-column>
            <el-table-column>
              <template slot-scope="scope">
                <el-button type="primary" icon="el-icon-edit" size="mini" @click="showEditDialogEv(scope.row.attr_id)">编辑</el-button>
                <el-button type="danger" icon="el-icon-delete" size="mini" @click="removeParams(scope.row.attr_id)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
          <!-- 动态参数表格 end -->

        </el-tab-pane>
        <el-tab-pane label="静态属性" name="only">
          <el-button type="primary" :disabled="isDisable" @click="addDialogVisible = true">添加属性</el-button>

          <!-- 静态属性表格 start-->
          <el-table :data="onlyTableData" border stripe>
            <!-- 展开列 -->
            <el-table-column type="expand">

              <!--插槽自定义列 渲染tag标签 start -->
              <template slot-scope="scope">

                <!-- tag start -->
                <el-tag v-for="(item, i) in scope.row.attr_vals" :key="i" closable @close="handleClose(i, scope.row)">{{item}}</el-tag>
                <!-- tag end -->

                <!-- input与button 输入 start-->
                <!--                 
                      渲染每一个展开行都公用了 inputVisible,所以展开行的tag会联动,不要公用一个inputVisible,因为有多行都用到了tag,解决:在请求获取动态参数和静态参数封装函数的时候,               
                   // 给item每一项添加字段inputVisible,默认是否显示input           
                    item.inputVisible = false;
                    // 给item每一项添加字段inputVisible,文本框的输入值
                    item.inputValue = "";
                    在使用tag的时候,用scope.row.inputVisible,这样点击就不会所有行tag被选中、   
                                
                -->
                <el-input class="input-new-tag" v-if="scope.row.inputVisible" v-model="scope.row.inputValue" ref="saveTagInput" size="small" @keyup.enter.native="handleInputConfirm(scope.row)" @blur="handleInputConfirm(scope.row)"></el-input>
                <el-button v-else class="button-new-tag" size="small" @click="showInput(scope.row)">+ New Tag</el-button>
                <!-- input与button 输入 end-->

              </template>
              <!--插槽自定义列 渲染tag标签 start -->

            </el-table-column>
            <!-- 索引列 -->
            <el-table-column type="index"></el-table-column>
            <el-table-column label="属性名称" prop="attr_name"></el-table-column>
            <el-table-column label="操作">
              <template slot-scope="scope">
                <el-button type="primary" icon="el-icon-edit" size="mini" @click="showEditDialogEv(scope.row.attr_id)">编辑</el-button>
                <el-button type="danger" icon="el-icon-delete" size="mini" @click="removeParams(scope.row.attr_id)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
          <!-- 静态属性表格 end-->

        </el-tab-pane>
      </el-tabs>
    </el-card>

    <!-- 添加参数对话框 动态参数和静态属性公用一个-->
    <el-dialog :title=" '添加' + getTitleText" :visible.sync="addDialogVisible" width="50%">
      <el-form :model="addFrom" :rules="addFromRules" ref="addFromRef" @close="addDialogClosed" label-width="100px">
        <el-form-item :label="getTitleText" prop="attr_name">
          <el-input v-model="addFrom.attr_name"></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="addParams">确 定</el-button>
      </span>
    </el-dialog>

    <!-- 编辑参数对话框 -->
    <el-dialog :title=" '修改' + getTitleText" :visible.sync="editDialogVisible" width="50%" @close="editDialogClosed">
      <el-form :model="editFrom" :rules="editFromRules" ref="editFromRef" label-width="100px">
        <el-form-item :label="getTitleText" prop="attr_name">
          <el-input v-model="editFrom.attr_name"></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="editParams">确 定</el-button>
      </span>
    </el-dialog>

  </div>
</template>

<script>
import {
  getCategoriesListAjax,
  getParamsListAjax,
  addParamsListAjax,
  getEditParamsListAjax,
  EditOkParamsListAjax,
  deleteEditOkParamsListAjax,
  EditTagsOkParamsListAjax,
} from "../api";
export default {
  name: "Params", //对应路由的name,params传参用路由的name 没有 /
  created() {
    this.getCateList();
  },

  data() {
    return {
      //   级联选择框双向绑定数组
      selectedCateKeys: [],
      // 商品分类列表
      cateList: [],
      //级联选择框的配置对象
      cateProps: {
        expandTrigger: "hover",
        value: "cat_id",
        label: "cat_name",
        children: "children",
      },
      //默认被激活的tabs
      activeName: "many",
      //   动态参数数据
      manyTableData: [],
      //   静态属性数据
      onlyTableData: [],
      //   控制添加参数对话框的显示与隐藏
      addDialogVisible: false,
      //   添加表单的数据对象
      addFrom: {
        attr_name: "",
      },
      //   添加表单的验证规则
      addFromRules: {
        attr_name: [
          { required: true, message: "请输入参数名称", trigger: "blur" },
        ],
      },

      //   编辑对话框
      editDialogVisible: false,
      //   修改表单数据对象
      editFrom: {},
      //   修改表单验证规则
      editFromRules: {
        attr_name: [
          { required: true, message: "请输入参数名称", trigger: "blur" },
        ],
      },
    };
  },

  methods: {
    //   获取所有的商品分类列表
    async getCateList() {
      await getCategoriesListAjax().then((res) => {
        // console.log(res);
        if (res.meta.status !== 200) {
          return this.$message.error("获取商品数据列表失败!");
        }
        this.cateList = res.data;
      });
    },

    // change监听的是v-model数组的变化
    handleChange(val) {
      // 里面有默认形参val,val就是级联选择框双向绑定数组
      // console.log(val);
      // 是三级分类执行以下代码
      this.getParamsData(); //调用函数
    },

    // tabs
    handleClick(val) {
      // console.log(this.activeName);
      this.getParamsData();
    },

    // 封装函数: 获取动态参数和静态参数封装函数
    async getParamsData() {
      // 只允许选择三级分类:不是三级分类清空表格
      // 通过数组的长度判断
      if (this.selectedCateKeys.length !== 3) {
        this.selectedCateKeys = [];
        // 清空表格数据
        this.manyTableData = [];
        this.onlyTableData = [];
        return;
      }
      // getCateId 计算属性三级id,activeName tab默认选中值
      await getParamsListAjax(this.getCateId, { sel: this.activeName }).then(
        (res) => {
          // console.log(res);
          res.data.forEach((item) => {
            //通过三元表达式判断attr_vals是否为空
            item.attr_vals = item.attr_vals ? item.attr_vals.split(" ") : [];
            // 解决点击一个tag另一行的tag也会变化,不能公用一个初始值,在获取接口数据是,添加字段
            // 给item每一项添加字段inputVisible,默认是否显示input
            item.inputVisible = false;
            // 给item每一项添加字段inputVisible,文本框的输入值
            item.inputValue = "";
          });
          if (res.meta.status !== 200) {
            return this.$message.error("获取参数列表失败!");
          }
          // 不能直接赋值,因为有两个表格,所以要判断是那个表格
          if (this.activeName === "many") {
            this.manyTableData = res.data;
          } else {
            this.onlyTableData = res.data;
          }
        }
      );
    },

    // 监听对话框的关闭事件
    addDialogClosed() {
      this.$refs.addFromRef.resetFields();
    },

    // 添加参数
    async addParams() {
      this.$refs.addFromRef.validate(async (valid) => {
        if (!valid) return;
        await addParamsListAjax(this.getCateId, {
          attr_name: this.addFrom.attr_name,
          attr_sel: this.activeName,
        }).then((res) => {
          if (res.meta.status !== 201) {
            return this.$message.error("添加参数失败!");
          }
          this.$message.success("添加参数成功!");
          this.addDialogVisible = false;
          // 调用封装函数 获取动态参数和静态参数,因为渲染不同表格是在这个封装函数中写的
          this.getParamsData();
        });
      });
    },

    // 显示编辑对话框 并 请求编辑数据,回填
    async showEditDialogEv(attrId) {
      //  当前属性id
      //  console.log(attrId);
      await getEditParamsListAjax(this.getCateId, attrId, {
        attr_sel: this.activeTabsName,
      }).then((res) => {
        if (res.meta.status !== 200) {
          return this.$message.error("获取分类失败!");
        }
        this.editFrom = res.data;
        this.editDialogVisible = true;
      });
    },
    // 重置修改表单
    editDialogClosed() {
      this.$refs.editFromRef.resetFields();
    },
    // 修改参数
    async editParams() {
      this.$refs.editFromRef.validate(async (valid) => {
        if (!valid) return;

        await EditOkParamsListAjax(this.getCateId, this.editFrom.attr_id, {
          attr_name: this.editFrom.attr_name,
          attr_sel: this.activeName,
        }).then((res) => {
          if (res.meta.status !== 200) {
            return this.$message.error("修改参数失败!");
          }
          this.$message.success("修改参数成功!");
          this.getParamsData();
          this.editDialogVisible = false;
        });
      });
    },

    // 根据Id删除对应的参数项
    async removeParams(attrId) {
      const confirmResult = await this.$confirm(
        "此操作将永久删除该参数, 是否继续?",
        "提示",
        {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        }
      ).catch((err) => err);
      if (confirmResult !== "confirm") {
        return this.$message.info("已取消删除!");
      }

      await deleteEditOkParamsListAjax(this.getCateId, attrId).then((res) => {
        if (res.meta.status !== 200) {
          return this.$message.error("删除参数失败!");
        }
        this.$message.success("删除参数成功!");
        this.getParamsData();
      });
    },

    // 文本框失去焦点,或者按下Enter触发
    handleInputConfirm(row) {
      // 输入的内容为空时,清空
      if (row.inputValue.trim().length === 0) {
        row.inputValue = "";
        row.inputVisible = false;
        return;
      }
      // 不为空去掉前后空格,追加到数组中
      row.attr_vals.push(row.inputValue.trim());
      row.inputValue = "";
      row.inputVisible = false;
      // 提交到接口,保存添加的tag
      this.saveAttrVals(row);
    },
    // 将对attr_vals(Tag) 的操作 调用接口保存到数据库
    async saveAttrVals(row) {
      await EditTagsOkParamsListAjax(this.getCateId, row.attr_id, {
        attr_name: row.attr_name,
        attr_sel: row.attr_sel,
        // 后台的attr_vals是字符串空格隔开的,我们在获取的时候转为数组了,
        // 现在提交数据到接口,还是要再次转为字符串
        attr_vals: row.attr_vals.join(" "),
      }).then((res) => {
        if (res.meta.status !== 200) {
          return this.$message.error("修改参数项失败!");
        }
        this.$message.success("修改参数项成功!");
      });
    },
    // 点击按钮显示输入框
    showInput(row) {
      row.inputVisible = true;
      //   让输入框自动获取焦点
      // $nextTick方法的作用:当页面元素被重新渲染之后,才会至指定回调函数中的代码
      this.$nextTick(() => {
        this.$refs.saveTagInput.$refs.input.focus();
      });
    },
    // 删除对应的参数可选项也需要调用接口,所以将接口封装函数
    handleClose(i, row) {
      // 获取当前行删除一个,调用接口封装
      row.attr_vals.splice(i, 1);
      this.saveAttrVals(row);
    },
  },

  computed: {
    //添加按钮是否禁用思路:
    // 根据级联选择数组长度判断是否是3级,(length长度),
    //返回true or false,在用按钮disable自定义属性绑定这个函数
    isDisable() {
      if (this.selectedCateKeys.length !== 3) return true;
      return false;
    },

    // 当前选中的三级分类Id !!重要,后面接口都要用
    getCateId() {
      if (this.selectedCateKeys.length === 3) {
        // console.log(this.selectedCateKeys[2]);
        return this.selectedCateKeys[2];
      }
      return null;
    },

    //动态参数和静态属性公用一个对话框,所以对话框title不能写死
    getTitleText() {
      if (this.activeName === "many") {
        return "动态参数";
      }
      return "静态属性";
    },
  },
};
</script>


<style lang="scss" scoped >
.cat_opt {
  margin: 15px 0;
}

.el-tag + .el-tag {
  margin-left: 10px;
}
.button-new-tag {
  margin-left: 10px;
  height: 32px;
  line-height: 30px;
  padding-top: 0;
  padding-bottom: 0;
}
.input-new-tag {
  width: 90px;
  margin-left: 10px;
  vertical-align: bottom;
}
</style>

  分类参数完!watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_6,color_FFFFFF,t_70,g_se,x_16

 

八、商品列表

商品表格

效果图: 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

源码:

<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>
    <!-- 卡片视图 -->
    <el-card>
      <el-row :gutter="20">
        <el-col :span="6">
          <el-input
            placeholder="输入商品名称自动搜索。。。"
            v-model="queryInfo.query"
            clearable
            @input="searchUser"
          ></el-input>
        </el-col>
        <el-col :span="4">
          <el-button type="primary" @click="showDialogBtn">添加商品</el-button>
        </el-col>
      </el-row>
      <!-- 表格数据 -->
      <el-table :data="goodsList" border stripe>
        <el-table-column type="index"></el-table-column>
        <el-table-column label="商品名称" prop="goods_name"></el-table-column>
        <el-table-column label="商品价格(元)" prop="goods_price" width="100px"></el-table-column>
        <el-table-column label="商品重量" prop="goods_weight" width="70px"></el-table-column>
        <el-table-column label="商品数量" prop="goods_number" width="70px"></el-table-column>
        <el-table-column label="创建时间" prop="add_time" width="140px">
          <template slot-scope="scope">{{ scope.row.add_time | formatePrice }}</template>
        </el-table-column>
        <el-table-column label="操作" width="130px">
          <template slot-scope="scope">
            <el-button type="primary" icon="el-icon-edit" size="mini"></el-button>
            <el-button
              type="danger"
              icon="el-icon-delete"
              size="mini"
              @click="removeById(scope.row.goods_id)"
            ></el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- 分页区域 -->
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="queryInfo.pagenum"
        :page-sizes="[5, 10, 15, 20]"
        :page-size="queryInfo.pagesize"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
        background
      ></el-pagination>
    </el-card>
  </div>
</template>

<script>
import _ from "lodash";
import { getGoodsListAjax, deleteGoodsListAjax, addGoodsUserAjax } from "../../api";
export default {
  data() {
    return {
      queryInfo: {
        query: "",
        pagenum: 1,
        pagesize: 10,
      },
      // 商品列表
      goodsList: [],
      // 商品总数
      total: 0,
      // 默认添加变量
      dialogVisible: false,

    };
  },
  created() {
    this.getGoodsList();
  },
  methods: {
    // 根据分页获取对应的商品列表
    async getGoodsList() {
      await getGoodsListAjax(this.queryInfo).then((res) => {
        // console.log(res);
        if (res.meta.status !== 200) {
          return this.$message.error("获取商品列表失败!");
        }
        this.goodsList = res.data.goods;
        //   console.log(this.goodsList)
        this.total = res.data.total;
      });
    },
    handleSizeChange(newSize) {
      this.queryInfo.pagesize = newSize;
      this.getGoodsList();
    },
    handleCurrentChange(newSize) {
      this.queryInfo.pagenum = newSize;
      this.getGoodsList();
    },
    // 通过Id删除商品
    async removeById(id) {
      const confirmResult = await this.$confirm(
        "此操作将永久删除该商品, 是否继续?",
        "提示",
        {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        }
      ).catch((err) => err);
      if (confirmResult !== "confirm") {
        return this.$message.info("已取消删除!");
      }
      await deleteGoodsListAjax(id).then((res) => {
        if (res.meta.status !== 200) {
          return this.$message.error("删除商品失败!");
        }
        this.$message.success("删除商品成功!");
        this.getGoodsList();
      });
    },
    // 添加商品
    showDialogBtn() {
      this.$router.push('/goods/add');

    },
    //点击搜索 输入搜索防抖
    searchUser: _.debounce(function () {
      this.getGoodsList();
    }, 1000),
    // 添加确定按钮
    addBtn() {
      this.dialogVisible = false;
      addGoodsUserAjax(this.ruleForm).then((res) => {
        if (res.meta.status !== 201) {
          return this.$message.error("添加商品失败!");
        }
        this.$message.success("添加商品成功!");
        // 添加成功了就重新渲染
        this.getGoodsList();
      });
    },
    // 点X关闭添加框
    resetAddForm() {
      (this.ruleForm.goods_price = ""),
        (this.ruleForm.goods_name = ""),
        (this.ruleForm.goods_weight = ""),
        (this.ruleForm.goods_number = "");
    },
  },
};
</script>

<style lang="less" scoped>
</style>
  •  点击添加按钮跳转到good/add
  • //router/index
    
              {
                    // 这个是浏览器url显示的路径 !!
                    path: '/goods/add',
                    name: '',
                    meta: {
                        title: "添加商品",
                        requiredPath: true
                    },
                    component: () =>
                        //路径的文件位置
                        import ('../views/main/goods/add.vue'),
    
                }

     

 添加商品

效果图:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_161、面包屑和步骤条:

​​ data() {
    return {
      // 步骤条默认激活 与左侧Tab联动
      activeIndex: "0",
    };
  },

 

// html
 <!-- 面包屑导航区 -->
    <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>
    <!-- 卡片视图 -->
    <el-card>
      <!-- 提示 -->
      <el-alert
        title="添加商品信息"
        type="info"
        center
        show-icon
        :closable="false"
      ></el-alert>
      <!-- 步骤条 -->
      <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>
    </el-card>

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

tab和步骤条实现同步思路:就是让tab绑定activeIndex,el组件提供tab的name属性可以与v-model绑定,但是activeIndex绑定name成字符串了,可以利用隐式转换,字符串与number运算会自动转为数值在比较activeIndex-0,同时注意el-tabs 的子节点只允许是el-tab-pane,所以将form包裹在tabs外

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

2、渲染页面 基本信息form

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 <!--  label-position="top" 指定文字在上面 -->
      <el-form
        :model="addForm"
        :rules="addFormRules"
        label-width="100px"
        label-position="top"
      >
        <!-- 注意el-tabs 的子节点只允许是el-tab-pane,所以将form包裹在tabs外,form的子项包裹在el-tab-pane中 -->
        <!-- 写好了form发现tabs高度不够了,可以将style="height: 200px"删除 -->
        <el-tabs tab-position="left" v-model="activeIndex">
          <el-tab-pane label="基本信息" name="0">
            <el-form-item label="商品名称" prop="goods_name">
              <el-input v-model="addForm.goods_name"></el-input>
            </el-form-item>
            <el-form-item label="商品价格" prop="price">
              <!--  type="number el组件规定只能输入数值 -->
              <el-input v-model="addForm.goods_price" type="number"></el-input>
            </el-form-item>
            <el-form-item label="商品重量" prop="goods_weight">
              <el-input v-model="addForm.goods_weight" type="number"></el-input>
            </el-form-item>
            <el-form-item label="商品数量" prop="goods_number">
              <el-input v-model="addForm.goods_number" type="number"></el-input>
            </el-form-item>
            <el-form-item label="商品分类" prop="goods_cat">
              <el-cascader
                v-model="addForm.goods_cat"
                :options="cateList"
                :props="cascaderProps"
                @change="handleChange"
              ></el-cascader>
            </el-form-item>
          </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>
      </el-form>

 3、请求级联选择器的数据

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

//data
 // 添加商品的表单对象
      addForm: {
        goods_name: "",
        goods_price: 0,
        goods_weight: 0,
        goods_number: 0,
        // 级联选择器的分类数组
        goods_cat: [],
        // 图片的数组
        pics: [],
        // 商品详情描述
        goods_introduce: "",
        attrs: [],
      },
      //验证规则
      addFormRules: {
        goods_name: [
          { required: true, message: "请输入商品名称", trigger: "blur" },
        ],
        goods_price: [
          { required: true, message: "请输入商品价格", trigger: "blur" },
        ],
        goods_weight: [
          { required: true, message: "请输入商品重量", trigger: "blur" },
        ],
        goods_number: [
          { required: true, message: "请输入商品数量", trigger: "blur" },
        ],
        goods_cat: [
          { required: true, message: "请选择商品分类", trigger: "blur" },
        ],
      },
      // 商品列表
      cateList: [],
      // 级联选择器配置
      cascaderProps: {
        expandTrigger: "hover",
        label: "cat_name",
        value: "cat_id",
        children: "children",
      },
// JS
 // 级联选择器选中项变化时触发,会直接绑定到
    handleChange() {
      if (this.addForm.goods_cat.length !== 3) {
        this.addForm.goods_cat = [];
      }
    },

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

  4、实现不选中分类不允许进行下一步

  • 思路:监听tabs的change事件,如果选中三级分类,就可以执行下一步
  • watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 beforeTabLeave (activeName, odlActiveName) {
      // 未选中商品分类阻止Tab标签跳转
      if (odlActiveName === '0' && this.addForm.goods_cat.length !== 3) {
        this.$message.error('请先选择商品分类')
        return false
      }

  5、商品参数  (动态参数)

 文档说明:watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

效果图: 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

tabs的点击事件:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

根据点击激活的tbas激活值,来判断是否点击了商品参数,(activeIndex) 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 将三级分类id设置成计算属性获取:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

@tab-click事件

  •   因为后台返回的attr_vals数据是以逗号分隔的字符串,空字符串转数组是空数组,会渲染空复选框所以要判空
 // Tab标签被选中时触发
    async tabClicked() {
      // 访问动态参数面板
      if (this.activeIndex === "1") {
        await GetAddParamsAjax(this.getCateId, { sel: "many" }).then((res) => {
          if (res.meta.status !== 200) {
            return this.$message.error("获取动态参数列表失败!");
          }
          //因为后台返回的attr_vals数据是以逗号分隔的字符串,空字符串转数组是空数组,会渲染空复选框所以要判空
          res.data.forEach((item) => {
            item.attr_vals =
              item.attr_vals.length === 0 ? [] : item.attr_vals.split(" ");
          });
          this.manyTableData = res.data;
        });
      } else if (this.activeIndex === "2") {
        await GetAddParamsAjax(this.getCateId, { sel: "only" }).then((res) => {
          if (res.meta.status !== 200) {
            return this.$message.error("获取静态态属性列表失败!");
          }
          this.onlyTableData = res.data;
        });
      }
    },

attr_vals字符串转为数组后就可以使用复选框渲染了,但是复选框有默认外边距,在样式中用最高权重中解决

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 

.el-checkbox {
  // 复选框样式 上右下左
  margin: 0 8px 0 0 !important;
}

效果图例:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

   6、商品属性 (静态属性)

文档说明

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

tabs事件 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16 效果图:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

获取好数据后就可以用el-input循环渲染 了

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 成功:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

    7、商品图片 (重要!)

上传图片 

el-upload结构---保存到服务器--将返回的临时路径追加到添加对象的pics数组   完成上传

 效果图:watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 el组件:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 文档: 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

//data
 // 图片上传地址
      uploadURL: "http://127.0.0.1:8888/api/private/v1/upload",
      // 图片上传组件的请求对象
      headerObj: {
        Authorization: JSON.parse(window.sessionStorage.getItem("token")),
      },
//html
 <el-tab-pane label="商品图片" name="3">
            <!-- action: 图片上传的API接口地址 -->
            <!-- 文档接口是upload action指定接口地址,但是不能直接指定upload,
            指定upload是相对浏览器的 相当于:http://localhost:8080/#/goods/add/upload
            但是图片地址不是,所以要指定完整地址,注意还要添加token,因为el-upload不使用我们的axios,
            它自己有自己封装的,但是它也想到这一点所以提供了headers属性,它接受一个对象,属性是Authorization ,值是token字符串
             -->
            <!--on-preview预览事件  on-remove删除事件 list-type指定展示方式 on-success-->
            <el-upload
              :action="uploadURL"
              :on-preview="handlePreview"
              :on-remove="handleRemove"
              :headers="headerObj"
              list-type="picture"
              :on-success="handleSuccess"
            >
              <el-button size="small" type="primary">点击上传</el-button>
            </el-upload>
            </el-tab-pane>

此时服务器已经存储了图片,下一步还要添加到表单中

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

商品文档: watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

代码:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16 将临时路径追加到添加对象的pic数组中,以对象形式

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 // 监听图片上传成功事件
    handleSuccess(response) {
      console.log(response);
      // 1.拼接得到一个图片信息对象 临时路径
      const picInfo = { pic: response.data.tmp_path };
      // 2.将图片信息对象,push到pics数组中
      this.addForm.pics.push(picInfo);
    },

 删除图片

思路:删除事件默认接受file形参,就是要删除的图片临时路径,用findIndex找到pics数组中对应要删除的哪一项,splice方法删除

// 处理移除图片的操作
    handleRemove(file) {
      // console.log(file);
      // 1.获取将要删除图片的临时路径
      const filePath = file.response.data.tmp_path;
      // 2.从pics数组中,找到图片对应的索引值
      //pics数组中的每一项等于要删除的哪一项,就在pics数组中将其删除
      const i = this.addForm.pics.findIndex((v) => v.pic === filePath);
      // 3.调用splice方法,移除图片信息
      this.addForm.splice(i, 1);
    },

 预览图片

思路:在预览事件中得到url真正图片地址,,复制一个对话框用src来展示

html

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16 data

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

//JS 
// 处理图片预览
    handlePreview(file) {
      // console.log(file); //url就是真正路径
      this.picPreviewPath = file.response.data.url;
      this.previewDialogVisible = true;
    },

8、富文本 添加按钮操作

github官方网址

下载

cnpm install vue-quill-editor –save

引入

//main.js

// 导入富文本编辑器
import VueQuillEditor from 'vue-quill-editor'
// 导入富文本编辑器样式
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'

在页面使用

         <!-- 富文本编辑器 -->
            <quill-editor v-model="addForm.goods_introduce"></quill-editor>
            <!-- 添加商品 -->
            <el-button type="primary" class="btnAdd" @click="addGoods"
              >添加商品</el-button>

 

 在公共样式表位置设置富文本的最小高度

  /* 全局富文本样式 */
        
        .ql-editor {
            min-height: 300px!important;
        }

8.1、处理goods_cat 

添加文档:需要将goods_cat字符串,但是级联选择器需要数组,所以只能深拷贝一份相同的 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16 如下图:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16 使用Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。

 完成深拷贝

下载

cnpm i --save lodash

中文官网  建议撸一遍,收获颇多

官方网址icon-default.png?t=M1FBhttp://​ https://www.lodashjs.com/ ​watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

深拷贝完成

 8.2、处理attr参数

添加文档参数: 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 添加文档数据:watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

//data
 addForm: {
        goods_name: "",
        goods_price: 0,
        goods_weight: 0,
        goods_number: 0,
        // 级联选择器的分类数组
        goods_cat: [],
        // 图片的数组
        pics: [],
        // 富文本v-model绑定值,后台提供的参数
        goods_introduce: "",
        // 在页面上操作的静态、动态最终都要追加到attr数组,以对象形式
        attrs: [],
      },

参数列表渲染后的表格其实就是 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

        动态参数的attr_value是一个数组,文档要求传递字符串

 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

静态属性的 attr_value本来就是字符串 

 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

  // 添加商品
    addGoods() {
      // async 是加在 (valid)前的
      this.$refs.addFormRef.validate(async (valid) => {
        if (!valid) return this.$message.error("请填写必要的表单项!");
        // 发送请求前:需对提交的表单进行处理
        // this.addForm.goods_cat = this.addForm.goods_cat.join(',')
        // 以上写法不对:级联选择器绑定的对象goods_cat要求是数组对象 !!
        /* 添加文档,需要将goods_cat转字符串,但是级联选择器需要数组,所以只能深拷贝一份相同的 */
        // 解决办法: 包:lodash 方法(深拷贝):cloneDeep(boj)
          // console.log(this.addForm);
        // 将this.addForm进行深拷贝
        const form = _.cloneDeep(this.addForm);
        // 深拷贝后装换form的数据,form只为添加用,addForm只为级联选择器用
        form.goods_cat = form.goods_cat.join(",");
        // console.log(form);
        // 处理动态参数
        this.manyTableData.forEach((item) => {
          // 后台文档只需要这两个属性就可以
          const newInfo = {
            attr_id: item.attr_id,
            // 动态参数的attr_value是一个数组,文档要求传递字符串
            attr_value: item.attr_vals.join(" "),
          };
          // 将循环后的对象追加addForm
          this.addForm.attrs.push(newInfo);
        });
        // 处理静态属性
        this.onlyTableData.forEach((item) => {
          const newInfo = {
            attr_id: item.attr_id,
            // 静态属性的 attr_value本来就是字符串
            attr_value: item.attr_vals,
          };
          this.addForm.attrs.push(newInfo);
        });
        // 现在addForm.attrs已经有这两个属性了再将它赋值form(深拷贝用于添加)
        form.attrs = this.addForm.attrs;
        // 发起请求添加商品
        // 商品名称必须是唯一的
        await addGoodsUserAjax(form).then((res) => {
          if (res.meta.status !== 201)
            return this.$message.error("添加商品失败!");
          this.$message.success("添加商品成功!");
          this.$router.push("/goods");
        });
      });
    },

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16  

添加 源码:

<template>
  <div id="add">
    <!-- 面包屑导航区 -->
    <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>
    <!-- 卡片视图 -->
    <el-card>
      <!-- 提示 -->
      <el-alert
        title="添加商品信息"
        type="info"
        center
        show-icon
        :closable="false"
      ></el-alert>

      <!-- 步骤条 设置active属性,接受一个Number-->
      <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>

      <!-- tabs -->
      <!-- tab和步骤条实现同步思路:就是让tab绑定activeIndex,tab el组件提供name属性与v-model绑定 
           但是activeIndex绑定到name成字符串了,可以利用隐式转换,字符串与number运算会自动转为数值在比较activeIndex-0,同时注意el-tabs 的子节点只允许是el-tab-pane,所以将form包裹在tabs外
       -->
      <!--  label-position="top" 指定文字在上面 -->
      <el-form
        :model="addForm"
        :rules="addFormRules"
        ref="addFormRef"
        label-width="100px"
        label-position="top"
      >
        <!-- 注意el-tabs 的子节点只允许是el-tab-pane,所以将form包裹在tabs外,form的子项包裹在el-tab-pane中 -->
        <!-- 写好了form发现tabs高度不够了,可以将style="height: 200px"删除 所有数据都在form的双向数据表单中-->
        <el-tabs
          tab-position="left"
          v-model="activeIndex"
          :before-leave="beforeTabLeave"
          @tab-click="tabClicked"
        >
          <el-tab-pane label="基本信息" name="0">
            <el-form-item label="商品名称" prop="goods_name">
              <el-input v-model="addForm.goods_name"></el-input>
            </el-form-item>
            <el-form-item label="商品价格" prop="price">
              <!--  type="number el组件规定只能输入数值 -->
              <el-input v-model="addForm.goods_price" type="number"></el-input>
            </el-form-item>
            <el-form-item label="商品重量" prop="goods_weight">
              <el-input v-model="addForm.goods_weight" type="number"></el-input>
            </el-form-item>
            <el-form-item label="商品数量" prop="goods_number">
              <el-input v-model="addForm.goods_number" type="number"></el-input>
            </el-form-item>
            <el-form-item label="商品分类" prop="goods_cat">
              <el-cascader
                v-model="addForm.goods_cat"
                :options="cateList"
                :props="cascaderProps"
                @change="handleChange"
              ></el-cascader>
            </el-form-item>
          </el-tab-pane>
          <el-tab-pane label="商品参数" name="1">
            <!-- 渲染表单的Item项 指定label值-->
            <el-form-item
              v-for="item in manyTableData"
              :key="item.attr_id"
              :label="item.attr_name"
            >
              <!-- 复选框组 -->
              <el-checkbox-group v-model="item.attr_vals">
                <!-- 复选框 -->
                <el-checkbox
                  :label="cb"
                  v-for="(cb, i) in item.attr_vals"
                  :key="i"
                  border
                ></el-checkbox>
              </el-checkbox-group> </el-form-item
          ></el-tab-pane>
          <el-tab-pane label="商品属性" name="2">
            <el-form-item
              :label="item.attr_name"
              v-for="item in onlyTableData"
              :key="item.attr_id"
            >
              <el-input v-model="item.attr_vals"></el-input>
            </el-form-item>
          </el-tab-pane>
          <el-tab-pane label="商品图片" name="3">
            <!-- action: 图片上传的API接口地址 -->
            <!-- 文档接口是upload action指定接口地址,但是不能直接指定upload,
            指定upload是相对浏览器的 相当于:http://localhost:8080/#/goods/add/upload
            但是图片地址不是,所以要指定完整地址,注意还要添加token,因为el-upload不使用我们的axios,
            它自己有自己封装的,但是它也想到这一点所以提供了headers属性,它接受一个对象,属性是Authorization ,值是token字符串
             -->
            <!--on-preview预览事件  on-remove删除事件 list-type指定展示方式 on-success-->
            <el-upload
              :action="uploadURL"
              :on-preview="handlePreview"
              :on-remove="handleRemove"
              :headers="headerObj"
              list-type="picture"
              :on-success="handleSuccess"
            >
              <el-button size="small" type="primary">点击上传</el-button>
            </el-upload>
          </el-tab-pane>
          <el-tab-pane label="商品内容" name="4">
            <!-- 富文本编辑器 -->
            <quill-editor v-model="addForm.goods_introduce"></quill-editor>
            <!-- 添加商品 -->
            <el-button type="primary" class="btnAdd" @click="addGoods"
              >添加商品</el-button
            >
          </el-tab-pane>
        </el-tabs>
      </el-form>
    </el-card>
    <!-- 预览图片对话框 -->
    <el-dialog
      title="图片预览"
      :visible.sync="previewDialogVisible"
      width="50%"
    >
      <img :src="picPreviewPath" alt="" class="previewImg" />
    </el-dialog>
  </div>
</template>

<script>
import {
  getCategoriesListAjax,
  GetAddParamsAjax,
  addGoodsUserAjax,
} from "../../api";
import _ from "lodash";
export default {
  name: "", //对应路由的name,params传参用路由的name 没有 /
  data() {
    return {
      // 步骤条默认激活 与左侧Tab联动
      activeIndex: "0",
      // 添加商品的表单对象
      addForm: {
        goods_name: "",
        goods_price: 0,
        goods_weight: 0,
        goods_number: 0,
        // 级联选择器的分类数组
        goods_cat: [],
        // 图片的数组
        pics: [],
        // 富文本v-model绑定值,后台提供的参数
        goods_introduce: "",
        // 在页面上操作的静态、动态最终都要追加到attr数组,以对象形式
        attrs: [],
      },
      //验证规则
      addFormRules: {
        goods_name: [
          { required: true, message: "请输入商品名称", trigger: "blur" },
        ],
        goods_price: [
          { required: true, message: "请输入商品价格", trigger: "blur" },
        ],
        goods_weight: [
          { required: true, message: "请输入商品重量", trigger: "blur" },
        ],
        goods_number: [
          { required: true, message: "请输入商品数量", trigger: "blur" },
        ],
        goods_cat: [
          { required: true, message: "请选择商品分类", trigger: "blur" },
        ],
      },
      // 商品列表
      cateList: [],
      // 级联选择器配置
      cascaderProps: {
        expandTrigger: "hover",
        label: "cat_name",
        value: "cat_id",
        children: "children",
      },
      // 动态参数列表数据
      manyTableData: [],
      // 静态属性列表数据
      onlyTableData: [],
      // 图片上传地址
      uploadURL: "http://127.0.0.1:8888/api/private/v1/upload",
      // 图片上传组件的请求对象
      headerObj: {
        Authorization: JSON.parse(window.sessionStorage.getItem("token")),
      },
      // 预览图片默认值
      picPreviewPath: "",
      // 图片预览对话框
      previewDialogVisible: false,
    };
  },
  methods: {
    // 获取商品分类数据列表
    async getCateList() {
      await getCategoriesListAjax().then((res) => {
        if (res.meta.status !== 200) {
          return this.$message.error("获取商品列表失败!");
        }
        this.cateList = res.data;
      });
    },
    // 级联选择器选中项变化时触发,会直接绑定到
    handleChange() {
      if (this.addForm.goods_cat.length !== 3) {
        this.addForm.goods_cat = [];
      }
    },
    // 未选中商品分类阻止Tab标签跳转
    //activeName:当前激活的tabs索引,odlActiveName:之前被激活的索引
    beforeTabLeave(activeName, odlActiveName) {
      // 如果是基本信息tabs并且没有选中三级分类 return false阻止下一步并提示
      if (odlActiveName === "0" && this.addForm.goods_cat.length !== 3) {
        this.$message.error("请先选择商品分类");
        return false; //el 提供 return false阻止下一步
      }
    },
    // Tab标签被选中时触发
    async tabClicked() {
      // 访问动态参数面板
      if (this.activeIndex === "1") {
        await GetAddParamsAjax(this.getCateId, { sel: "many" }).then((res) => {
          if (res.meta.status !== 200) {
            return this.$message.error("获取动态参数列表失败!");
          }
          //因为后台返回的attr_vals数据是以逗号分隔的字符串,空字符串转数组是空数组,会渲染空复选框所以要判空
          res.data.forEach((item) => {
            item.attr_vals =
              item.attr_vals.length === 0 ? [] : item.attr_vals.split(" ");
          });
          this.manyTableData = res.data;
        });
        // 如果点击的是商品静态属性
      } else if (this.activeIndex === "2") {
        await GetAddParamsAjax(this.getCateId, { sel: "only" }).then((res) => {
          if (res.meta.status !== 200) {
            return this.$message.error("获取静态态属性列表失败!");
          }
          this.onlyTableData = res.data;
        });
      }
    },
    // 处理图片预览
    handlePreview(file) {
      // console.log(file); //url就是真正路径
      this.picPreviewPath = file.response.data.url;
      this.previewDialogVisible = true;
    },
    // 处理移除图片的操作
    handleRemove(file) {
      // console.log(file);
      // 1.获取将要删除图片的临时路径
      const filePath = file.response.data.tmp_path;
      // 2.从pics数组中,找到图片对应的索引值
      //pics数组中的每一项等于要删除的哪一项,就在pics数组中将其删除
      const i = this.addForm.pics.findIndex((v) => v.pic === filePath);
      // 3.调用splice方法,移除图片信息
      this.addForm.pics.splice(i, 1);
    },
    // 监听图片上传成功事件
    handleSuccess(response) {
      // console.log(response);
      // 1.拼接得到一个图片信息对象 临时路径
      const picInfo = { pic: response.data.tmp_path };
      // 2.将图片信息对象,push到pics数组中
      this.addForm.pics.push(picInfo);
    },
    // 添加商品
    addGoods() {
      // async 是加在 (valid)前的
      this.$refs.addFormRef.validate(async (valid) => {
        if (!valid) return this.$message.error("请填写必要的表单项!");
        // 发送请求前:需对提交的表单进行处理
        // this.addForm.goods_cat = this.addForm.goods_cat.join(',')
        // 以上写法不对:级联选择器绑定的对象goods_cat要求是数组对象 !!
        /* 添加文档,需要将goods_cat转字符串,但是级联选择器需要数组,所以只能深拷贝一份相同的 */
        // 解决办法: 包:lodash 方法(深拷贝):cloneDeep(boj)
          // console.log(this.addForm);
        // 将this.addForm进行深拷贝
        const form = _.cloneDeep(this.addForm);
        // 深拷贝后装换form的数据,form只为添加用,addForm只为级联选择器用
        form.goods_cat = form.goods_cat.join(",");
        // console.log(form);
        // 处理动态参数
        this.manyTableData.forEach((item) => {
          // 后台文档只需要这两个属性就可以
          const newInfo = {
            attr_id: item.attr_id,
            // 动态参数的attr_value是一个数组,文档要求传递字符串
            attr_value: item.attr_vals.join(" "),
          };
          // 将循环后的对象追加addForm
          this.addForm.attrs.push(newInfo);
        });
        // 处理静态属性
        this.onlyTableData.forEach((item) => {
          const newInfo = {
            attr_id: item.attr_id,
            // 静态属性的 attr_value本来就是字符串
            attr_value: item.attr_vals,
          };
          this.addForm.attrs.push(newInfo);
        });
        // 现在addForm.attrs已经有这两个属性了再将它赋值form(深拷贝用于添加)
        form.attrs = this.addForm.attrs;
        // 发起请求添加商品
        // 商品名称必须是唯一的
        await addGoodsUserAjax(form).then((res) => {
          if (res.meta.status !== 201)
            return this.$message.error("添加商品失败!");
          this.$message.success("添加商品成功!");
          this.$router.push("/goods");
        });
      });
    },
  },

  computed: {
    // 获取三级分类id
    getCateId() {
      if (this.addForm.goods_cat.length === 3) {
        return this.addForm.goods_cat[2];
      }
      return null;
    },
  },
  components: {},
  created() {
    this.getCateList();
  },
};
</script>

<style lang="scss" scoped>
.el-steps {
  margin: 15px 0;
}
.el-checkbox {
  // 复选框样式 上右下左
  margin: 0 8px 0 0 !important;
}
.previewImg {
  width: 100%;
}
.btnAdd {
  margin-top: 15px;
}
</style>

添加商品完!watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_6,color_FFFFFF,t_70,g_se,x_16 

九、商品订单-列表

因为物流属于隐私所以咱们就简单实现样式!定义三级联动的js在页面导入,实现三级联动

注意:需要用到时间线展示物流信息,element 2.6.3版本才有,物流接口不能用,咱没权限访问,所以用了一个json模拟,我将json定义在public里面在使用的页面有axios请求,在main.js引入axiso,并挂载到原型上

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_19,color_FFFFFF,t_70,g_se,x_16watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 

<template>
  <div id="">
    <!-- 面包屑导航区 -->
    <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>
    <!-- 卡片视图 -->
    <el-card>
      <el-row>
        <el-col :span="6">
          <el-input placeholder="请输入内容">
            <el-button slot="append" icon="el-icon-search"></el-button>
          </el-input>
        </el-col>
      </el-row>

      <!-- 订单列表 -->
      <el-table :data="orderList" border stripe>
        <el-table-column type="index" label="#"></el-table-column>
        <el-table-column label="订单编号" prop="order_number"></el-table-column>
        <el-table-column label="订单价格" prop="order_price"></el-table-column>
        <el-table-column label="是否付款">
          <template slot-scope="scope">
            <el-tag type="danger" size="mini" v-if="scope.row.pay_status"
              >未付款</el-tag
            >
            <el-tag type="success" size="mini" v-else>已付款</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="是否发货" prop="is_send"></el-table-column>
        <el-table-column label="下单时间">
          <template slot-scope="scope">
            {{ scope.row.create_time | formatePrice }}
          </template>
        </el-table-column>
        <el-table-column label="操作" width="200">
          <template slot-scope="scope">
            <el-button
              type="primary"
              size="mini"
              icon="el-icon-edit"
              @click="showEditDialog"
            ></el-button>
            <el-button
              type="success"
              size="mini"
              icon="el-icon-location"
              @click="locationVi"
            ></el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- 分页区域 -->
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="queryInfo.pagenum"
        :page-sizes="[5, 10, 15, 20]"
        :page-size="queryInfo.pagesize"
        layout="total, sizes, prev, pager, next, jumper"
        :total="total"
      ></el-pagination>
    </el-card>

    <!-- 编辑对话框 -->
    <el-dialog
      title="修改地址"
      :visible.sync="addressDialogVisible"
      width="50%"
    >
      <!-- form -->
      <el-form
        :model="addressForm"
        :rules="addressFormRules"
        ref="addressFormRef"
        label-width="100px"
      >
        <el-form-item label="省市区/县" prop="address1">
          <!-- cityData 就是城市js -->
          <el-cascader
            v-model="addressForm.address1"
            :options="cityData"
            :props="{ expandTrigger: 'hover' }"
          ></el-cascader>
        </el-form-item>
        <el-form-item label="详细地址" prop="address2">
          <el-input v-model="addressForm.address2"></el-input>
        </el-form-item>
      </el-form>
      <!-- form -->

      <span slot="footer" class="dialog-footer">
        <el-button @click="addressDialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="addressDialogVisible = false"
          >确 定</el-button
        >
      </span> </el-dialog
    >、

    <!-- 展示物流 -->
    <el-dialog title="展示物流" :visible.sync="kDDialogVisible" width="50%">
       <!-- 时间线 -->
      <el-timeline>
        <el-timeline-item
          v-for="(item, index) in kuaidi"
          :key="index"
          :timestamp="item.time"
        >{{item.context}}</el-timeline-item>
      </el-timeline>
      <span slot="footer" class="dialog-footer">
        <el-button @click="kDDialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="kDDialogVisible = false"
          >确 定</el-button
        >
      </span>
    </el-dialog>
  </div>
</template>

<script>
import { GetOrdersAjax } from "../../api.js";
// 导入省市区js
import cityData from "./citydata.js";
export default {
  name: "", //对应路由的name,params传参用路由的name 没有 /
  data() {
    return {
      // 订单列表查询参数
      queryInfo: {
        query: "",
        pagenum: 1,
        pagesize: 10,
      },
      total: 0,
      // 订单列表
      orderList: [],
      // 快递JSON
      kuaidi: [],
      addressDialogVisible: false,
      // 快递对话框
      kDDialogVisible: false,
      addressForm: {
        //  三级联动
        address1: [],
        address2: "",
      },
      addressFormRules: {
        address1: [
          { required: true, message: "请选择省市区县", trigger: "blur" },
        ],
        address2: [
          { required: true, message: "请输入详细地址", trigger: "blur" },
        ],
      },
      cityData,
    };
  },
  methods: {
    async getOrderList() {
      await GetOrdersAjax(this.queryInfo).then((res) => {
        // console.log(res);
        if (res.meta.status !== 200) {
          return this.$message.error("获取订单列表失败!");
        }
        this.total = res.data.total;
        this.orderList = res.data.goods;
      });
    },
    // 分页
    handleSizeChange(newSize) {
      this.queryInfo.pagesize = newSize;
      this.getOrderList();
    },
    handleCurrentChange(newSize) {
      this.queryInfo.pagenum = newSize;
      this.getOrderList();
    },
    showEditDialog() {
      this.addressDialogVisible = true;
    },
    // 位置
    locationVi() {
      // json放在public中
      this.$axios('/kuaidi.json').then((res) => {
        this.kuaidi = res.data.data;
        // console.log(this.kuaidi);
      });
      this.kDDialogVisible = true;
    },
  },

  created() {
    this.getOrderList();
  },
};
</script>

<style lang="scss" scoped>
.el-cascader {
  width: 100%;
}
</style> 

 十、数据统计-ECharts

我们快要结束了 

Apache ECharts官方文档

1、下载依赖 

cnpm install echarts --save 

 2、导入

import echarts from 'echarts'

如果报错请使用这种引入

const echarts = require('echarts')

3、为Echarts准备一个Dom容器定义Echarts宽高,不然它放哪

// 例如:
  <!-- 卡片视图 -->
    <el-card>
        <!-- 2.为Echarts准备一个Dom -->
        <div id="main" style="width: 750px;height:400px;"></div>
    </el-card>

4、初始化 Echarts获取定义它的dom,所以在mounterd初始化

// 例如 
// 4.基于准备好的dom,初始化echarts实例
    var myChart = echarts.init(document.getElementById('main'))

5、准备数据和配置项可以直接在官网粘过来

//指定图表的配置项和数据
    var option = {
      title: {
        text: 'ECharts 入门示例'
      },
      tooltip: {},
      legend: {
        data: ['销量']
      },
      xAxis: {
        data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
      },
      yAxis: {},
      series: [{
        name: '销量',
        type: 'bar',
        data: [5, 20, 36, 10, 10, 20]
      }]
    }

6、展示数据

 // 6.展示数据
    myChart.setOption(option)

 上面是事例,如何快速上手,一般我们的数据来自后台,不会使用官方的数据

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

所以我们借助lodash来完成合并,自己手动合并也可以要递归,直接使用扩展运算符和Object.assign不可以,思路:先将后台的数据定义在data中,再将请求的数据与之合并,将合并后的变量传给setOption

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

使用lodash合并图例:效果图正常

 

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

看下lodash打印后合并的对象 :全部递归了平铺了

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

在看手动合并的对象形式:效果图乱套了,原因是Object.assign()只是单纯的合并没有递归,渲染时找不到准确的数据

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

 解释:Object.assign()

手动合并:

假设现在有两个对象:

var obj1 = { name:'张三' };
var obj2= { age:18 };

我们想要得到obj3 = {name:’张三’,age:18};

合并对象:Object.assign(obj1,obj2,obj3) 是把obj2,obj3,... 合并到obj1中; 返回值是 合并后的obj1;

例如:

let copy = Object.assign({},obj2,obj3)

注意:

Object.assign()方法,如果多个对象具有相同的属性,则后者也会覆盖前者的属性值,栗子: 

var obj1 = { name:'张三' };
var obj2= { name:'李四' };
var obj3 = Object.assign(obj1,obj2);
console.log(obj3);

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMC7mtLvlnKjpo47mtarph4w=,size_20,color_FFFFFF,t_70,g_se,x_16

合并数组:

​let arr = concat(arr,arr1,arr2);

或

​let arr = [...arr1,arr2]

 源码:

<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>
    <!-- 卡片视图 -->
    <el-card>
      <!-- 2.为Echarts准备一个Dom -->
      <div id="main" style="width: 850px; height: 400px"></div>
    </el-card>
  </div>
</template>

<script>
import { reportsAjax } from "../api";

// 1.下载依赖 cnpm install echarts --save 并 导入echarts
// 如果报错请使用这种引入 const echarts = require('echarts')
import * as echarts from "echarts";

// 引入lodash 用于合并后台数据
import _ from "lodash";

export default {
  data() {
    return {
      // 定义后台需要合并的数据
      options: {
        title: {
          text: "用户来源",
        },
        tooltip: {
          trigger: "axis",
          axisPointer: {
            type: "cross",
            label: {
              backgroundColor: "#E9EEF3",
            },
          },
        },
        grid: {
          left: "3%",
          right: "4%",
          bottom: "3%",
          containLabel: true,
        },
        xAxis: [
          {
            boundaryGap: false,
          },
        ],
        yAxis: [
          {
            type: "value",
          },
        ],
      },
    };
  },

  async mounted() {
    // 3.基于准备好的dom,初始化echarts实例,在mounted钩子函数中
    var myChart = echarts.init(document.getElementById("main"));

    await reportsAjax().then((res) => {
      if (res.meta.status !== 200) return this.$message("获取折线图数据失败!");
      // 4.准备数据项和配置项,借用lodash的合并对象方法
      const result = _.merge(res.data, this.options);


      // 5.展示数据
      myChart.setOption(result);
    });
  },
};
</script>

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

 十一、结束 上线部署服务器

手写一个服务器代码将 《vue电商后台管理系统》部署上去 上线、打包_技术前端,忠于热爱-CSDN博客

 结束 ! 感谢一路的耐心,再此说几句煽情的话 ·愿小伙伴都能早早下班、少些bug,多多陪陪家人

2022/3/19 13:15

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

结合 服务器+后端+前端,完成 vue项目 后台管理系统 的相关文章

  • Vuetify - 如何设置背景颜色

    我正在使用带有 Light 主题的 Vuetify 默认情况下 这会将主要内容的背景设置为浅灰色 我需要它是白色的 我想通过修改手写笔变量来覆盖它 但我似乎无法弄清楚哪个变量设置背景颜色 我按照中的所有步骤进行了操作docs https v
  • 在 Vue 组件中定义用户定义的 getter

    我的问题与 Vue 相关 更具体地说是反应性和反应性 getter setter https v2 vuejs org v2 guide reactivity html https v2 vuejs org v2 guide reactiv
  • 使用 Vue.js 确定放置坐标是否位于另一个元素上方?

    我正在使用vue 可拖动 可调整大小 https github com mauricius vue draggable resizable组件将给出 x y 偏移坐标 说明我的元素在页面上的放置位置 一切都很好 但是 我想知道是否有办法确定
  • (Vue.js)我想让图像1在我点击图像1时弹出,我希望图像2在我点击图像2时弹出

    有几个图像 我想让当我点击图片1时弹出图片1 当我点击图片2时我想让图片2弹出 我可以使用类中的索引解决它吗 Carousel vue
  • 导入目录中的所有文件,无需显式指定每个文件

    我想导入文件夹中的所有组件 有没有一种方法可以做到这一点而无需创建index js文件并单独导入然后导出每个组件 有批量的方法吗 文件夹结构 src templates Foo vue Bar vue Baz vue src App vue
  • 如何限制“v-for”中元素的迭代

    我正在构建一个小型应用程序Vuejs 2 0我有大约 15 个迭代元素 我想限制v for仅 5 个元素 并且可以有更多按钮来显示整个列表 有没有可能 你可以试试这个代码 div div div div div div div div
  • v-bind:style 语法在内联样式中不起作用

    我对 Vue 和整个系统都很了解v bind事情让我陷入困境 基本上 我正在尝试实现这种语法 但是使用 Vue 的v bind 因为我不能在内联 CSS 样式中使用变量 div class card style background col
  • Vue.js $children 按组件名称

    我正在尝试按名字访问特定的孩子 目前 由于孩子所在的位置 我这样称呼孩子 this root children 0 没关系 只要那个孩子总是 0 但如果有一种方法可以做类似的事情那就太好了 this root children detail
  • 拖动 Vuetify v-data-table 中的列

    我想在 v data table 中通过标题拖动列 我已经使用指令 sortablejs 取得了很大的进展 但我无法弄清楚以下内容 我不确定当 列数与 列数不匹配时如何解释 使用 colspan someNumber 时可能会发生这种情况
  • splice 不适用于数组行 vue js

    我有一个对象数组 但是当我想从数组列表中删除对象时 仅从末尾删除项目 div class hours 然后我将点击功能放在图标上 div
  • Vue.js 中的 $t 是什么

    第一次与Vue js并且不知道是什么 t 例如我有一个人的代码是这样的 li class category filter back li
  • openlayers 地图未在 vuejs 中显示

    以下代码片段是一个 vue 文件 它不会生成任何错误 但 openlayers 地图不会显示 我尝试了 openlayers 的两个 vue 插件 但似乎没有按照我的需要工作 在 vue 之外 代码可以工作 我使用 npm install
  • 如何在localStorage中存储数组?

    我有一个数据属性 data return playWord baseWord result 并回应 baseWord Amazing 每次我在 api 中发送新请求时 基本词都会发生变化 我想将每个数据存储在本地存储中并将字符串值发送到 a
  • Vue.use() 抛出“无法读取未定义的属性‘use’”

    尝试1 main js import createApp from vue import store from store store import App from App vue Vue config productionTip fal
  • 使用 Webpack 波形符别名时通过 Vim 中的“gf”解析 JavaScript 模块

    我是使用波浪号的 Vue js 项目的新成员 模块导入中的符号 如 import WhateverApi from api whatever 项目存储库包含所有类型的文件 Vagrant 机器 Laravel 后端应用程序 配置文件和 Vu
  • quasar:构建应用程序时出现 vite 错误

    当我做quasar build它给出以下内容 deb2302user deb2302 tmp2303 vue kitty quasar build d88888b d88P Y88b 888 888 888 888 888 888 8888
  • vue/vuetify 模态模式或最佳实践设计

    在我正在开发的应用程序中 我们有很多模态 每个模态包含少量数据 通常是 2 3 个字段 有时是复选框 列表等 问题是当组件关闭时如何从内部重置 销毁组件 造成这种情况的原因有两个 1 不必清除每个模式上的各个数据字段 2 当第二次打开模式时
  • 以编程方式将焦点设置在 Vuetify 中的按钮上

    我试图在每次打开 v 对话框时将操作按钮集中在 v 对话框中 我尝试使用自动对焦 但它只能工作一次 通常在 Vuetify 中 您可以通过添加引用然后调用 focus 函数来将焦点设置在元素上 如下所示
  • 查询 Firestore 中的特定日期

    我正在尝试查询 Firestore 中的指定日期 到目前为止我已经尝试过这段代码 let ref db collection schools doc DglhflywuybkOuCq7tGW let start new Date 2018
  • Prismic - 如何在不暴露访问令牌的情况下进行 API 调用

    我正在构建一个 vue js Web 应用程序 我想对我的 prismic 存储库进行相应的调用 但我不知道如何在不暴露我的访问令牌的情况下执行此操作 我正在使用所示的其余 api 方法here https prismic io docs

随机推荐

  • Arthas监控java程序

    安装 1 解压缩arthas的压缩包 unzip arthas packaging bin zip 2 安装Arthas cd arthas install local sh 启动 以脚本的方式启动as sh 如图为启动成功 监控命令使用
  • 01. Windows基础:Dos命令

    文章目录 本文中的命令示例统一使用 来代表可以替换的内容 在使用时请不要真的输入 谢谢 0 Dos简介 1 目录相关 1 1 进入目录 1 2 进入另一个磁盘分区 1 3 列出当前目录下所有文件及文件夹 2 文件操作 2 1 浏览文件 2
  • 华为OD机试 - 相对开音节(Java)

    题目描述 相对开音节构成的结构为 辅音 元音 aeiou 辅音 r除外 e 常见的单词有bike cake等 给定一个字符串 以空格为分隔符 反转每个单词中的字母 若单词中包含如数字等其他非字母时不进行反转 反转后计算其中含有相对开音节结构
  • 【自然语言处理】潜在语义分析【下】概率潜在语义分析

    有任何的书写错误 排版错误 概念错误等 希望大家包含指正 由于字数限制 分成两篇博客 自然语言处理 潜在语义分析 上 潜在语义分析 自然语言处理 潜在语义分析 下 概率潜在语义分析 2 概率潜在语义分析 概率潜在语义分析 probabili
  • FreeRTOS(任务管理的创建、删除、挂起、恢复)

    目录 一 任务的基本概念 二 任务状态的概念 1 Running 运行态 2 Ready 就绪态 3 Blocked 阻塞态 4 Suspended 挂起态 三 任务状态的切换 四 系统启动 1 vTaskStartScheduler 函数
  • IOS数据管理

    在 iOS 中 没有直接与 Android 中的 SharePreference 相对应的概念 而是使用不同的机制来处理应用程序的持久化数据 在 iOS 中 你可以使用以下几种方法来保存和读取应用程序的数据 UserDefaults 用户默
  • Halcon (64位)无法卸载或者卸载不彻底,没法再次安装?

    以管理员身份 切换到cmd 1 删除安装目录 rmdir S HALCONROOT 2 查询安装的Halcon版本 reg query HKLM SOFTWARE Wow6432Node MVTec HALCON Windows x64 3
  • 面试常用算法归纳

    面试常用算法归纳 算法时间复杂度 二叉查找树的时间复杂度 递归和分治 递归思维 汉诺塔问题 排序算法 最长子串 子序列 一维dp 有断层 最长递增子序列 最大子数组和 无重复字符的最长子串 买卖股票的最佳时机 二维dp 组合 子集 和排列
  • YOLOv5改进算法之添加CA注意力机制模块

    目录 1 CA注意力机制 2 YOLOv5添加注意力机制 送书活动 1 CA注意力机制 CA Coordinate Attention 注意力机制是一种用于加强深度学习模型对输入数据的空间结构理解的注意力机制 CA 注意力机制的核心思想是引
  • Atmel Studio 7.0 快速上手指南(基于ASF)

    Atmel Studio 7 0 快速上手指南 基于ASF 程序员大本营 pianshen com
  • 【Kubernetes部署篇】K8s图形化管理工具Dasboard部署及使用

    文章目录 一 Dashboard简介 二 Dashboard部署安装 三 配置Dashboard登入用户 1 通过Token令牌登入 2 通过kubeconfig文件登入 四 Dashboard创建容器 五 扩展 一 Dashboard简介
  • switch...case...和if...else...区别

    switch 和 if 都是用来处理分支语句的 那么使用的时候 考虑到代码效率问题 就必须先来了解他们有什么区别 先来看看这两个语句的使用格式 if else if 表达式1 语句1 else if 表达式2 语句2 else if 表达式
  • Altium Designer (AD) 元器件出现绿色叉叉报错的解决办法

    出现报错的原因 元器件的安全间距小于设定的安全间距 但通常情况下 这个问题并不严重 可以理解为是一个警告 不去处理也可以 解决办法 点击菜单栏的工具 T 再点击复位错误标志 M 即可解决报错
  • 一个爬虫代码价值 7000 万

    一个爬虫代码价值 7000 亿 这样的代码你听说过吗 这是一个爬取比特币密钥的代码 比特币相信大家都有听说过 尤其最近比特币价格还突破了 5 万美元大关 现在1 枚比特币就价值 35 万人民币 难怪有句说 币圈一天 人间一年 最近朋友圈关于
  • 登录,注册HTML页面,详细过程

    1 页面说明 登录和注册切换按钮 当点击登录按钮时 显示登录表单 当点击注册按钮时 显示注册表单 每个表单都有对应的 JavaScript 校验函数 校验用户名 邮箱和密码是否为空 如果为空 会弹出警告框 2 效果图展示 3 代码部分 3
  • 手把手教你快速上手人体姿态估计(MMPose)

    最近在研究如何快速实现图像中人体姿态的估计 也就是常见的pose estimation任务 花了些时间 实际对比了AlphaPose BlazePose和MMPose BlazePose主要为移动端设计 AlphaPose安装配置比较麻烦
  • 服务器显卡驱动重装系统,windows7旗舰版系统重装显卡驱动的方法

    在windows7旗舰版电脑中 我们都是需要安装显卡驱动 但是如果显卡驱动安装不合适的话 就会容易导致电脑出现问题 所以如果有碰到安装到不合适的显卡驱动的话我们可以通过重装显卡驱动来解决 那么该怎么操作呢 为此小编这就给大家讲解一下wind
  • 图片 url blob base64 互转

    待补充 url to blob export const urlToBlob async url string gt return new Promise resolve gt fetch url then res gt res blob
  • Nginx

    HTTP和反向代理web服务器 Nginx是一个高性能的HTTP和反向代理web服务器 同时也提供了IMAP POP3 SMTP服务 Nginx是一款轻量级的Web服务器反向代理服务器及电子邮件 IMAP POP3 代理服务器 nginx反
  • 结合 服务器+后端+前端,完成 vue项目 后台管理系统

    目录 以上是项目的服务器php 后端 前端 已经可以正常运行 一 登录 登录页进度条 戳这里Vue项目电商后台管理系统 nprogress 进度条 活在风浪里的博客 CSDN博客 二 侧导航 三 列表页源码 四 角色分配 五 权限页面开发