• 尚品汇后台管理项目(Vue)


    简介

    1:什么是后台管理系统项目?
    注意:前端领域当中,开发后台管理系统项目,并非是java、php等后台语言项目。

    • 在前面课程当中,我们已经开发了一个项目【尚品汇电商平台项目】,这个项目主要针对的是用户(游客),可以让用户在平台当中购买产品。但是你需要想明白一件事情,用户购买产品信息从何而来呀?
    • 比如:前台项目当中的数据来源于卖家(公司),但是需要注意的时候,卖家它不会数据库操作。对于卖家而言,需要把产品的信息上传于服务器,写入数据库。
    • 卖家并非程序员,不会数据库操作(增删改查)。导致卖家,找了一个程序员,开发一个产品,可以让我进行可视化操作数据库(增删改查)
    • 卖家(公司):组成,老板、员工
    • 老板:开发这个项目,对于老板而言,什么都可以操作。【产品的上架、产品的下架、查看员工的个人业绩、其他等等】 员工:可能就是查看个人业绩
    • 后台管理系统:可以让用户通过一个可视化工具,可以实现对于数据库进行增删改查的操作。
    • 而且需要注意,根据不同的角色(老板、员工),看到的、操作内容是不同的。对于后台管理系统项目,一般而言,是不需要注册的。

    模板介绍

    简洁版
    加强版

    模块的文件与文件夹的认知

    build

    • index.js webpack配置文件【很少修改这个文件】

    mock

    • mock数据的文件夹【模拟一些假的数据mockjs实现的】,因为咱们实际开发的时候,利用的是真是接口

    node_modules

    • 项目依赖的模块

    public

    • ico图标,静态页面,publick文件夹里面经常放置一些静态资源,而且在项目打包的时候webpack不会编译这个文件夹,原封不动的打包到dist文件夹里面

    src - 程序员源代码存储的地方

    ------api文件夹:涉及请求相关的
    ------assets文件夹:里面放置一些静态资源(一般共享的),放在aseets文件夹里面静态资源,在webpack打包的时候,会进行编译
    ------components文件夹:一般放置非路由组件获取全局组件
    ------icons这个文件夹的里面放置了一些svg矢量图
    ------layout文件夹:他里面放置一些组件与混入
    ------router文件夹:与路由相关的
    -----store文件夹:一定是与vuex先关的
    -----style文件夹:与样式先关的
    ------utils文件夹:request.js是axios二次封装文件****
    ------views文件夹:里面放置的是路由组件
    
    -------------------------------------------
    App.vue:根组件
    main.js:入口文件
    permission.js:与导航守卫先关、
    settings:项目配置项文件
    .env.development
    .env.producation
    

    后台管理系统API接口在线文档:

    在线接口文档1
    在线接口文档2

    完成登录业务 –

    1. 静态组件书写完成
      如:书写相应的样式,换登录页面的背景图片,使用background,还可以调节登录页面的背景图片大小
    background: url(~@/assets/deadlift.jpg);
    background-size: 100% 100%;
    
    1. 书写API (store -> modules -> user.js)
      一般是先在vuex中书写相应的api如:actions,commit等 然后在具体想用的组件中进行dispatch操作

    2. axios二次封装
      二次封装的时候,有些内容我们也要进行修改,如服务器返回的接口,有时候返回真实的结果可能是200,也可能是20000

    if (res.code !== 20000 && res.code != 200) { ... }
    
    1. 换成真实接口之后需要解决代理跨域问题
    // 引入axios(axios进行了二次封装操作)
    import request from '@/utils/request'
    
    // 对外暴露接口接口函数
    export function login(data) {
      return request({
        url: '/admin/acl/index/login',
        method: 'post',
        data
      })
    }
    
    // 对外暴露用户信息的函数
    export function getInfo(token) {
      return request({
        url: '/admin/acl/index/info',
        method: 'get',
        params: { token }
      })
    }
    
    export function logout() {
      return request({
        url: '/admin/acl/index/logout',
        method: 'post'
      })
    }
    

    换成真实的url接口,以及method方法也是需要进行更改操作

    在换入接口以后还需要解决跨域问题 – 在vue.config.js文件中进行配置

    devServer: {
       port: port,
       open: true,
       overlay: {
         warnings: false,
         errors: true
       },
       proxy: {
         '/dev-api': {
           // 数据来自于哪一台代理服务器 就使用哪一台代理服务器即可
           target: 'http://39.98.123.211:8170',
           pathRewrite: { '^/dev-api': '' }
         },
       }
       // before: require('./mock/mock-server.js')
       // 配置代理跨域
     },
    
    • 在devServer属性中间进行相应配置操作,里面配置proxy,里面具体的配置需要根据项目进行修改操作
    proxy: {
         '/dev-api': {
           // 数据来自于哪一台代理服务器 就使用哪一台代理服务器即可
           target: 'http://39.98.123.211:8170',
           pathRewrite: { '^/dev-api': '' }
         },
       }
    

    退出登录业务

    • 可以去google中打开vue开发者工具,查看退出登录是在哪个组件当中,–> 在Navbar.vue组件当中
    • 需要的保留,修改相应的中文,不需要的进行相应的删除操作

    项目路由的搭建

    整个路由的项目框架都是依赖于Layout外部框架

    /* 整个项目的框架 */
    import Layout from '@/layout'
    

    路由组件的配置是通过懒加载的形式进行的 - component:箭头函数的形式

    component: () => import('@/views/login/index'),
    

    需要注意的是,二级路由是放在Children属性下面的

    export const constantRoutes = [
      {
        path: '/login',
        component: () => import('@/views/login/index'),
        hidden: true
      },
    
      {
        path: '/404',
        component: () => import('@/views/404'),
        hidden: true
      },
    
      {
        path: '/',
        component: Layout,
        // 访问 / 会进入到整个大的一个结构,但是它会立马重定向到它的二级路由
        redirect: '/dashboard',
        children: [{
          path: 'dashboard',
          name: 'Dashboard',
          component: () => import('@/views/dashboard/index'),
          meta: { title: 'Dashboard', icon: 'dashboard' }
        }]
      },
    
      /* 
        在搭建路由的时候,在总体框架 component: Layout,中书写完全以后,
        组件的内容就在其中了
      */
      {
        path: '/product',
        // 我们在进行搭建组件的时候都是在layout组件下面进行的
        component: Layout,
        name: 'Product',
        meta: { title: '商品管理', icon: 'el-icon-goods' }, 
        children: [
          {
            path: 'trademark',
            name: 'Trademark',
            component: () => import('@/views/product/tradeMark'),
            meta: { title: '品牌管理' }
          }, 
          {
            path: 'attr',
            name: 'Attr',
            component: () => import('@/views/product/Attr'),
            meta: { title: '平台属性管理' }
          }, 
          {
            path: 'spu',
            name: 'Spu',
            component: () => import('@/views/product/Spu'),
            meta: { title: 'Spu管理' }
          }, 
          {
            path: 'sku',
            name: 'Sku',
            component: () => import('@/views/product/Sku'),
            meta: { title: 'Sku管理' }
          }
        ]
      }, 
    
      // 404 page must be placed at the end !!!
      { path: '*', redirect: '/404', hidden: true }
    ]
    

    然后对外暴露一个箭头函数,而且这个箭头函数一致性会返回一个路由器对象

    const createRouter = () => new Router({
      // mode: 'history', // require service support
      scrollBehavior: () => ({ y: 0 }),
      routes: constantRoutes
    })
    '
    运行
    • 其中constantRoutes 被放置在这个路由器对象中,而后new实例, 对外进行暴露即可

    重置路由操作,了解

    // 重置路由操作
    export function resetRouter() {
      const newRouter = createRouter()
      router.matcher = newRouter.matcher // reset router
    }
    
    export default router // 对外暴露
    

    完成品牌管理静态组件

    首先项目图,product下应该有很多的二级路由 先创建二级路由组件
    在这里插入图片描述

    tradeMark组件

    • 从上至下:按钮
    <!-- 像element-ui组件库里面的内容可以直接书写 -->
    <el-button type="primary" icon="el-icon-plus" style="margin: 10px 0px"
      >添加</el-button
    >
    

    其中,任何标签中还可以书写相应的样式 style标签,形式如下

    style="margin: 10px 0px"
    '
    运行
    • 表格组件 el-table
    <!-- 
        表格组件
            - data:表格组件将来要展示的数据 --- 数组类型
            - border:表格的边框
            - el-table-column :代表一列
                - label:显示的标题
                - width:对应列的宽度
                - align:标题的对齐方式
    -->
    <el-table :data="data" style="width: 100%" border>
      <el-table-column prop="prop" label="序号" width="80px" align="center">
      </el-table-column>
      <el-table-column prop="prop" label="品牌名称" width="width">
      </el-table-column>
      <el-table-column prop="prop" label="品牌Logo" width="width">
      </el-table-column>
      <el-table-column prop="prop" label="操作" width="width">
      </el-table-column>
    </el-table>
    
    • 分页器组件 - el-pagination
    <!-- 
        分页器
            当前第几页、数据总条数、每一页展示的条数、连续页码数
            @size-change="handleSizeChange"
            @current-change="6"
    
        - current-page 当前第几页
        - total 代表分页器一共需要展示数据条数
        - page-size 代表每一页需要展示多少条数据
        - page-sizes 可以设置每一页展示多少条数据
        - layout 可以实现分页器的布局
        - pager-count 按钮的数量 如果9 连续页码数是7 
     - 都是el-pagination里面的属性内容 是包裹在el-pagination标签内部的,注意结束位置
    -->
    <el-pagination
        style="margin-top:20px;textAlign:center"
        :current-page="6"
        :total="99"
        :pager-count='9'
        :page-size="3"
        :page-sizes="[3, 5, 10]"
        layout="prev, pager, next, jumper, ->, sizes, total">
    </el-pagination>
    
    • 其中,分页器标签里面的属性,或者一些方法都是放置在 中的
    • 其中layout属性是调节 分页器的布局的,调换里面的顺序得到的是不同的分页器样式显示数据

    完成品牌管理列表的展示

    书写相关的API接口

    • 因为有四个二级路由组件,所以在书写API接口方法的时候,也可以将其分为4个不同的部分,但最后可以集中在index.js中
      在这里插入图片描述
    • 在index.js中,对外进行统一暴露操作
    /* 
        将四个模块请求的接口统一对外暴露
    */
    import * as trademark from './product/tradeMark'
    import * as attr from './product/attr'
    import * as spu from './product/spu'
    import * as sku from './product/sku'
    
    // 对外暴露 kv一致
    export default {
        trademark, 
        attr, 
        spu, 
        sku
    }
    
    • 然后在可以在main.js入口文件中进行统一的引入,引入以后在项目中的任何文件中都可以使用
    /* 
      引入相关API请求接口
      组件实例的原型的原型指向的是Vue.prototype
      任意组件可以使用API相关的接口
    */
    import API from '@/api'
    Vue.prototype.$API = API
    
    • 还有一个重要的点就是:该项目中针对不同的结构和要求,使用了不同的接口,我们也需要解决跨域问题
    • Vue.config.js 文件,对devServer属性进行配置
    proxy: {
          /* '/dev-api': {
            // 数据来自于哪一台代理服务器 就使用哪一台代理服务器即可
            target: 'http://39.98.123.211:8170',
            pathRewrite: { '^/dev-api': '' }
          }, */
          '/dev-api/admin/acl': {
            // 数据来自于哪一台代理服务器 就使用哪一台代理服务器即可
            target: 'http://39.98.123.211:8170',
            pathRewrite: { '^/dev-api': '' }
          },
          '/dev-api/admin/product': {
            // 数据来自于哪一台代理服务器 就使用哪一台代理服务器即可
            // 配置了多个跨域问题
            target: 'http://39.98.123.211:8510',
            pathRewrite: { '^/dev-api': '' }
          },
        }
    

    书写tradeMark下的组件

    获取数据在组件内部静态展示

    • mounted - 组件挂载完毕以后执行,但是因为创建获取数据的函数,需要多次调用,一般书写在methods中
    • 对于分页器,以及分页需要用到的内容数据,一开始就需要在data属性中先配置好
    data() {
        return {
          // 分页器第几页
          page: 1,
          // 当前页数展示数据条数
          limit: 3,
          // 总共数据条数
          total: 0,
          // 列表展示的数据
          list: [],
        };
      },
    
    • getPageList() 的书写
      ① 在调用接口函数时候,需要用到的数据可以先从this中解构出来
    // 解构出参数
    const { page, limit } = this;
    
    async getPageList(pager = 1) {
        this.page = pager
        // 解构出参数
        const { page, limit } = this;
        // 获取品牌列表的接口,需要带两个参数(在data中声明)
        let result = await this.$API.trademark.reqTradeMarkList(page, limit);
        // console.log(result);
        if(result.code == 200) {
          this.total = result.data.total
          this.list = result.data.records
        }
      },
    
    • handleCurrentChange() 的书写 - 其实上述getPageList已经解决了
    handleCurrentChange(pager) {
          // 修改参数
          this.page = pager
          this.getPageList() 
        }
    
    • handleSizeChange() 的书写 - 类似于handleCurrentChange,就是对参数进行了相应的修改操作
    // 当分页器某一页需要展示数据条数发生变化的时候出现
    handleSizeChange(limit) {
      // console.log(limit)
      // 整理参数,再次发送请求
      this.limit = limit
      this.getPageList() 
    }
    
    添加按钮的操作 showDialog
    // 点击添加品牌的按钮
    showDialog() {
      // 显示对话框
      this.dialogFormVisible = true;
      // 为了防止用于点击取消,然后点进去又有可以每次进去都清楚数据
      this.tmForm = {
        tmName: "",
        logoUrl: "",
      };
    },
    
    • 首先点击进去就需要显示出我们的对话框,
    this.dialogFormVisible = true;
    
    • 在el-dialog组件中,我们是通过 :visible.sync 来控制对话框的显示与隐藏用的
    
    
    添加按钮(添加品牌 | 修改品牌)addOrUpdateTradeMark
    async addOrUpdateTradeMark() {
      this.dialogFormVisible = false
      // 发请求(添加品牌|修改品牌)
      let result = await this.$API.trademark.reqAddOrUpdateTradeMark(this.tmForm)
      if(result.code == 200) {
        // 弹出一个信息框:添加品牌成功、修改品牌成功
        this.$message(this.tmForm.id ? '修改品牌成功' : '添加品牌成功')
        // 成功以后,将数据进行展示即可
        this.getPageList() 
      }
    }
    
    • vue框架中弹出一个对话框的用法
    this.$message() 
    
    • 在很多时候,我们处理完逻辑以后,都需要进行再次获取 数据展示的操作
    修改品牌
    • 修改品牌需要将品牌相应的信息传入到点击事件的方法中
    @click="updateTradeMark(row)"
    
    • 修改品牌方法的书写
    updateTradeMark(row) {
      // console.log(row) row是整个品牌的对象 - 用于选中的品牌的信息
      // 显示对话框
      this.dialogFormVisible = true;
      /* 
        将已有的品牌信息赋值给tmForm进行展示
        将服务器返回的品牌信息,直接赋值给了tmForm进行展示
        也就是说tmForm存储的即为服务器返回的品牌信息
          获取到的对象,进行浅拷贝操作 {...row}
      */
      // this.tmForm = row;
      this.tmForm = { ...row };
    },
    

    需要注意的是: 有时候获取到的对象是和v-model的属性绑定了的 需要用到浅拷贝,这样修改此处不会影响到别的地方

    品牌的表单验证
    • 表单验证规则同样是去elementUI中进行查找,且表单验证规则是写在data配置项中供给使用
    // 表单验证规则
    rules: {
      /* 
        required 必须要验证字段
        message 提示信息
        trigger 用户行为设置(事件的设置blur change)
        min max 字段长度
      */
      // 品牌名称的验证规则
      tmName: [
        { required: true, message: "请输入品牌名称", trigger: "blur" },
        {
          min: 2,
          max: 10,
          message: "长度在 2 到 10 个字符",
          trigger: "change",
        },
      ],
      // 品牌logo的验证规则
      logoUrl: [
        {
          required: true,
          message: "请选择品牌的图片区域",
          trigger: "change",
        },
      ],
    },
    
    • el-form表单最外面需要包含
    <!-- form表单  
    :model属性 - 这个属性的作用是 把表单的数据收集到那个对象身上,将来表单验证,也需要这个属性
    -->
    <el-form style="width: 80%" :model="tmForm" :rules="rules" ref="ruleForm">
    
    • 正常的项目逻辑是,当所有的表单验证成功通过以后,才能够点击确定按钮,进行添加按钮(添加品牌 | 修改品牌)的操作
    addOrUpdateTradeMark() {
      // 当全部验证字段通过 再去书写业务逻辑
      this.$refs.ruleForm.validate(async (success) => {
        if (success) {
          // 如果全部字段符合条件
          this.dialogFormVisible = false;
          // 发请求(添加品牌|修改品牌)
          let result = await this.$API.trademark.reqAddOrUpdateTradeMark(
            this.tmForm
          );
          if (result.code == 200) {
            // 弹出一个信息框:添加品牌成功、修改品牌成功
            // this.$message(this.tmForm.id ? "修改品牌成功" : "添加品牌成功");
            this.$message({
              type: "success",
              message: this.tmForm.id ? "修改品牌成功" : "添加品牌成功",
            });
            // 成功以后,将数据进行展示即可
            // 如果添加品牌,停留在第一页,修改品牌应该停留在当前页面
            this.getPageList(this.tmForm.id ? this.page : 1);
          }
        } else {
          console.log("Submit Error!!!");
          return false;
        }
      });
    },
    
    删除品牌操作
    • 同样的删除品牌也要书写API接口,告诉服务器我们已经将商品信息进行删除,然后通过服务器来更新商品列表
    /* 
        删除品牌的信息
        删除品牌 /admin/product/baseTrademark/remove/{id}  delete
        在书写方法的时候 中间是有参数的
    */
    export const reqDeleteTradeMark = (id) => request({
        url: `/admin/product/baseTrademark/remove/${id} `,
        method: 'delete'
    })
    
    • 删除商品信息,同样需要传入row告诉服务器我们删除的是哪一个商品信息
    @click="deleteTradeMark(row)"
    
    // 删除品牌的操作
    deleteTradeMark(row) {
      this.$confirm(`你确定删除${row.tmName}?`, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(async () => {
          // 当用户点击确定按钮的时候会触发
          // 向服务器发请求
          let result = await this.$API.trademark.reqDeleteTradeMark(row.id);
          if (result.code == 200) {
            this.$message({
              type: "success",
              message: "删除成功!",
            });
            // 再次获取列表数据 但是展示数据的页码数需要发生变化
            this.getPageList(this.list.length>1 ? this.page : this.page-1)
          }
        })
        .catch(() => {
          // 当用户点击取消按钮的时候会触发
          this.$message({
            type: "info",
            message: "已取消删除",
          });
        });
    },
    
    • 其中删除商品信息展示列表数据的时候,需要判断是应该展示当前页还是上一页 (用户可以拥有更好的体验)
    this.getPageList(this.list.length>1 ? this.page : this.page-1)
    

    商品管理之三级联动静态组件

    三级联动静态组件

    • 该静态组件因为在多个地方都需要用到,所以可以将其设置为全局静态组件 - 放置在components文件夹下面
    • 该静态组件在创建以后取名字,因为全局静态组件需要进行相应的注册
    • 在main.js文件中进行全局路由组件的注册
    import CategorySelect from '@/components/CategorySelect'
    // 注册全局组件 因为一开始名字中用到了,所以直接用名字来进行注册操作
    Vue.component(CategorySelect.name, CategorySelect)
    
    • 在Attr文件下引入相应的子组件
    
    

    注意: 子给父传数据我们用的是自定义事件

    子组件(全局组件CategorySelect的书写)

    • 其中效果我们用到的是el-form表单
      在这里插入图片描述
      
    获取分类函数方法的书写
    /* 
        平台属性管理模块请求文件
    */
    import request from "@/utils/request"
    /* 
        获取一级分类数据接口
        /admin/product/getCategory1  get
    */
    export const reqCategory1List = () =>request({
        url:'/admin/product/getCategory1', 
        method:'get',
    })
    
    /* 
        获取二级分类数据接口
        /admin/product/getCategory2/{category1Id}  get
    */
    export const reqCategory2List = (category1Id) =>request({
        url:`/admin/product/getCategory2/${category1Id}`, 
        method:'get',
    })
    
    /* 
        获取三级分类数据接口
        /admin/product/getCategory2/{category1Id}  get
    */
    export const reqCategory3List = (category2Id) =>request({
        url:`/admin/product/getCategory3/${category2Id}`, 
        method:'get',
    })
    
    • 其中二级、三级分类的接口在获取数据的时候需要携带相应的id
    组件数据配置

    因为组件内部需要收集到相应的数据进行展示 - 且数据收集时候的初始值都是[] 分类的id数量有多个可以放在一个对象中

    data() {
      return {
        // 一级分类的数据
        list1: [],
        // 二级分类的数据
        list2: [],
        // 三级分类的数据
        list3: [],
        // 收集相应的一级二级三级分类的id
        cForm: {
          category1Id: "",
          category2Id: "",
          category3Id: "",
        },
      };
    },
    
    获取一级分类、二级分类、三级分类数据方法的回调函数
    • 需要注意的是:在书写一级分类方法时,如果用户选择的属性发生改变的时候需要一开始就清除掉list2和list3中的方法,以及他们的id
    • 同理:在书写二级分类方法也是如此
      // 组件挂载完毕,向服务器发请求,获取相应的一级分类的数据
      mounted() {
        // 获取一级分类的数据的方法
        this.getCategory1List();
      },
      methods: {
        // 获取一级分类数据的方法
        async getCategory1List() {
          // 获取一级分类的请求,不需要携带参数
          let result = await this.$API.attr.reqCategory1List();
          // console.log(result)
          // 不使用vuex来操作数据的话,就需要通过在data属性中设置相应的数据来进行·保存
          if (result.code == 200) {
            this.list1 = result.data;
          }
        },
        /* 
            一级分类的select事件的回调
            - 当一级分类的option发生变化的时候获取相应二级分类的数据
        */
        async handler1() {
          this.list2 = [];
          this.list3 = [];
          this.cForm.category2Id = "";
          this.cForm.category3Id = "";
          // console.log(111)
          // 解构出一级分类的id
          const { category1Id } = this.cForm;
          /* 为了区分开传的到底是几id 可以进行相应的打标第操作 */
          // this.$emit("getCategoryId", category1Id);
          this.$emit("getCategoryId", { categoryId: category1Id, level: 1 });
          // 通过一级分类的id 获取二级分类的数据
          let result = await this.$API.attr.reqCategory2List(category1Id);
          // console.log(result);
          if (result.code == 200) {
            this.list2 = result.data;
          }
        },
        /* 
            二级分类的select事件的回调
            - 当二级分类的option发生变化的时候获取相应三级分类的数据
        */
        async handler2() {
          this.list2 = [];
          this.cForm.category3Id = "";
          const { category2Id } = this.cForm;
          this.$emit("getCategoryId", { categoryId: category2Id, level: 2 });
          let result = await this.$API.attr.reqCategory3List(category2Id);
          if (result.code == 200) {
            this.list3 = result.data;
          }
        },
        // 三级分类的事件回调
        handler3() {
          const { category3Id } = this.cForm;
          this.$emit("getCategoryId", { categoryId: category3Id, level: 3 });
        },
      },
    

    父组件需要用到子组件中的数据来进行相应展示 - 父组件发送请求,主要是需要用到子组件传递过来的三级分类的id
    因为书写API接口方法需要用到,因为在子组件中触发自定义函数用到了level来区分具体是哪一个级别分类的id

    父组件Attr

    methods: {
      /* 
        自定义事件的回调
      */
      getCategoryId({ categoryId, level }) {
        // 通过level来区别是几级id
        if (level == 1) {
          this.category1Id = categoryId;
          this.category2Id = "";
          this.category3Id = "";
        } else if (level == 2) {
          this.category2Id = categoryId;
          this.category3Id = "";
        } else {
          // 代表三级分类已经有了
          this.category3Id = categoryId;
          // 发请求获取品牌属性
          this.getAttrList() 
        }
      },
      // 获取平台属性的数据
      getAttrList() {
        console.log('发请求')
      }
    },
    
    平台属性管理动态展示属性
    • 动态展示属性数据的时候,首先应该书写相应的API方法获取数据
    /* 
        获取平台属性的接口
        /admin/product/attrInfoList/{category1Id}/{category2Id}/{category3Id}
    */
    export const reqAttrList = (category1Id, category2Id, category3Id) => request({
        url: `/admin/product/attrInfoList/${category1Id}/${category2Id}/${category3Id}`,
        method: 'get'
    })
    

    获取数据来进行展示

    async getAttrList() {
      // console.log('发请求')
      // 获取分类的id
      const { category1Id, category2Id, category3Id } = this;
      let result = await this.$API.attr.reqAttrList(
        category1Id,
        category2Id,
        category3Id
      );
      // console.log(result)
      if (result.code == 200) {
        this.attrList = result.data;
      }
    },
    
    • 一样的操作,因为是在组件内部进行书写API,所以在data中需要配置相应的属性
    // 接收平台属性的字段
    attrList: [],
    

    将上述获取到的属性 展示到 el-card组件当中即可

        
          
    添加属性
    添加属性值 取消 保存 取消

    **el-table中是以列属性来进行展示的,**其中el-table-column是用来展示每一列的属性的

    平台属性之添加属性与修改属性

    **点击添加或者修改按钮以后,**会跳转展示到另一个界面当中, 使用 v-show来完成即可 且通过isShowTable来控制完成

     

    同样的有内部有内容框 每一个el-table-column中有el-input时候就需要用到作用域插槽来实现该效果

    
      
    
    
    收集属性名的操作

    收集属性名首先也是一样的,需要先书写接口函数 - 当有多个接口的时候,我们可以用一个data对象来进行表示

    /* 
        添加属性与属性值接口
        /admin/product/saveAttrInfo   post  
        写data,用一个对象来表示数据即可
        data:
            "attrName":"" 属性名
            "attrValueList":[  属性名中属性值,因为属性值可以是多个,因此需要的是数组
                {
                    "attrId":0,  // 属性的ID
                    "valueName":'string'  // 相应的属性值
                }
            ], 
            "categoryId":0, category3Id
            "categoryLevel":3 
    */
    export const reqAddAttr = (data) => request({
        url: "/admin/product/saveAttrInfo",
        method: 'post',
        data
    })
    

    在组件中进行调用的时候,不用vuex以后就需要在data中设置相应的数据来进行接收

    attrInfo: {
      attrName: "", // 属性名
      attrValueList: [
        // 属性值
        /* {
          attrId: 0, // 相应的属性名的id
          valueName: "",
        }, */
      ],
      categoryId: 0, // 三级分类的id
      categoryLevel: 3, // 因为服务器也需要区分是几级Id
    },
    

    一开始是不应该有内容的,只有每次点击了才会加上相应的内容,所以在data中,一开始attrValueList中是不应该有内容的
    添加属性值

    // 添加属性值的按钮
    addAttrValue() {
      // 向属性值的数组里面添加元素
      this.attrInfo.attrValueList.push({
        attrId:undefined, 
        valueName:'', 
      })
    }
    
    解决返回按钮数据回显问题

    **点击取消以后,我们需要对数据进行取消操作 ** 清除数据,this.attrInfo直接进行赋值操作即可,里面相应的内容设为空即可

    但是我们在添加商品以后,需要设置商品的id即可

    // 清除数据
    this.attrInfo = {
      attrName: "", // 属性名
      attrValueList: [
        // 属性值
        /* {
          attrId: 0, // 相应的属性名的id
          valueName: "",
        }, */
      ],
      // categoryId: 0, // 三级分类的id  点击的那一刻 3级分类的id是可以拿到的
      categoryId: this.category3Id,
      categoryLevel: 3, // 因为服务器也需要区分是几级Id
    };
    
    修改属性操作

    **修改某一个属性的时候,先切换页面 在将attrInfo的数据进行相应的替换操作

    • 但是在数据里面的结构当中,当存在对象里面套数组,数组里面套对象等操作,结构相对复杂一点的可以使用深拷贝来解决
    // 修改某一个属性
    updateAttr(row) {
      // console.log(row)
      this.isShowTable = false;
      // 将选中的属性,赋值给attrInfo
      // 由于数据结构当中存在对象里面套数组 数组里面套对象 因此需要使用深拷贝解决这类问题
      // 深拷贝 浅拷贝在面试的时候出现的频率很高,切记达到手写深拷贝和浅拷贝
      this.attrInfo = cloneDeep(row);
    },
    
    查看模式与修改模式切换
    • 为了控制每一个输入框的里面是查看还是修改模式的状态,可以在添加属性的时候,就添加一个flag属性来控制这一点
     // 添加属性值的按钮
     addAttrValue() {
       // 向属性值的数组里面添加元素
       this.attrInfo.attrValueList.push({
         // attrId: undefined,
         /* 
           对于修改某一个属性的时候
           可以在已有的属性值基础之上新增新的属性值
             - 新增属性值的时候,需要把已有的属性的id带上
           此处直接解决了两个地方,添加的时候没有id就undefined
           修改的时候有id 就直接添加上即可
         */
         attrId: this.attrInfo.id,
         valueName: "",
         /* 
           flag属性
           - 给每一个属性值添加一个标记flag
           - 用户切换查看模式与编辑模式
           - 好处:每一个属性值可以控制自己的模式切换
           - 当前的flag属性是响应式数据(数据变化视图跟着变化)
           - 因为flag本质上是attrValueList上面的,在data中有定义过
         */
         flag: true,
       });
     },
    

    el-input与span之间的显示与隐藏通过 v-if 以及 v-else来解决处理即可

    
    

    失去焦点的事件

    // 失去焦点的事件,切换为查看模式 显示span
    toLook(row) {
      row.flag = false
    }
    
    查看模式与编辑模式注意事项
    • 失去焦点的事件,切换为查看模式,显示span
    • 但是在这样进行相应切换的时候,我们有自己需要注意的点:尤其是用户在input框里面进行输入的时候
    • 有时候用户在进行输入的时候,输入的内容会有相应的偏差,比如说用户输入为空,可以发生一个提示信息
    // 如果属性值为空 不能作为新的属性值 需要给用户提示 让他输入一个其他属性值
    if (row.valueName.trim() == "") {
      this.$message("请输入一个正常的属性值:");
      return;
    }
    
    • 新增的属性值,不能与已有的属性值进行重复,重复的话直接进行return操作即可
    // 失去焦点的事件,切换为查看模式 显示span
    toLook(row) {
      // 如果属性值为空 不能作为新的属性值 需要给用户提示 让他输入一个其他属性值
      if (row.valueName.trim() == "") {
        this.$message("请输入一个正常的属性值:");
        return;
      }
      // 新增的属性值,不能与已有的属性值重复
      let isRepat = this.attrInfo.attrValueList.some((item) => {
        // 需要将row从数组里面判断的时候去除
        /* 
          row是最新的属性值【数组的最后一项元素】
          判断的时候,需要把已有的数组当中新增的这个属性值去除
    
        */
        if (row !== item) {
          return row.valueName == item.valueName;
        }
      });
    
      // console.log(isRepat)
      if (isRepat) return;
      // row是当前用户添加的最新的属性值
      // console.log(row)
      // 当前的编辑模式变为查看模式【让input消失,显示span】
      row.flag = false;
    },
    
    修改属性的查看与编辑模式切换
    • 我们在进行修改的时候,点击修改的时候,因为是一开始没有定义flag属性,所以没有办法切换查看模式和编辑模式
    • 遍历每一个this.attrInfo.attrValueList.forEach((item) => {} 将里面的元素的flag属性都设置为 false的话,在内部是探测不到的
    • **原因:**因为vue无法探测普通的新增property,这样书写的属性并非响应式属性
    • 我们可以使用 Vue.set 来添加响应式属性
    this.attrInfo.attrValueList.forEach((item) => {
      // 这样书写也可以给属性值添加flag属性,但是会发现视图不会跟着变化(因为flag不是响应式数据)
      // item.flag = false 之前用put是可以探测到的,但是这里是给一个对象进行操作
      /* 
        上述:因为vue无法探测普通的新增property,这样书写的属性并非响应式属性
        响应式属性(数据变化视图跟着变化)
        Vue.set 向响应式
        参数一:对象
        参数二:添加新的响应式属性
        参数三:添加新的响应式属性的属性值
      */
      this.$set(item, "flag", false);
    });
    
    表单元素自动聚焦的实现

    点击span自动变成编辑模式

    @click="toEdit(row, $index)"
    
    // 点击span的回调,变为编辑模式
    toEdit(row, index) {
      row.flag = true;
      // 获取input节点 实现自动聚焦
      // console.log(this.$refs[index])
      /* 
        - 获取input节点,实现自动聚焦
        - 需要注意:点击span的时候,切换为input变为编辑模式 
                   但是需要注意:对于浏览器而言页面的重绘会会消耗时间
        - 点击span的时候,重绘一个input是需要耗费时间的,不可能一点击span就立马获取到input
      */
      this.$nextTick(() => {
        // 获取相应的input表单元素实现聚焦
        this.$refs[index].focus();
      });
    },
    
    • 当然在我们每一次点击一个添加按钮的时候,同样需要在输入框中立马出现自动聚焦的操作,
    • 因为每次点击添加操作应该都是在 列表 的最后一个元素当中
    // 添加添加按钮以后 自动聚焦操作
    this.$nextTick(() => {
      this.$refs[this.attrInfo.attrValueList.length - 1].focus();
    });
    
    • 需要用到ref,所以我们需要在 el-input中进行相应的定义操作,定义ref并将上述的$index进行传入即可
    
    

    删除属性的操作

    其中删除属性我们用的是 el-popconfirm 标签

    
      
    
    

    其中确认删除也是有固定的 点击事件 onConfirm – 因为我们确认删除是需要知道具体删除的是哪一个事件

    书写相应的点击事件 – 需要传入相应的参数,即为点击属性的相应索引值

    deleteAttrValue(index) {
      // alert(111)
      // 当前删除属性值的操作是不需要发请求的
      this.attrInfo.attrValueList.splice(index, 1);
    },
    

    保存操作

    当用户在输入框中输入完毕以后,我们需要将数据进行保存的操作

    • 过滤掉的属性值不能是空的
    • 删除掉flag属性
    • 通过调用·方法向服务器中保存相应的属性,因为需要知道返回成功和返回失败等信息,可以使用try…catch
    // 保存按钮 进行添加属性或修改属性的操作
    async addOrUpdateAttr() {
      // alert(111)
      /* 
        整理参数
          - 1、如果用户添加很多属性值,且属性值为空的不应该提交给服务器
          - 2、提交给服务器数据中不应该出现flag字段
      */
      this.attrInfo.attrValueList = this.attrInfo.attrValueList.filter(
        (item) => {
          // 过滤掉属性值不是空的
          if (item.valueName != "") {
            // 删除掉flag属性
            delete item.flag;
            return true;
          }
        }
      );
      // 向服务器发送请求来保存数据
      // 成功干什么 失败干什么 可以使用try...catch
      try {
        // 发请求
        await this.$API.attr.reqAddOrUpdateAttr(this.attrInfo);
        // 展示平台属性的信号量进行切换
        this.isShowTable = true;
        // 提示消息
        this.$message({
          type: "success",
          message: "保存成功",
        });
        // 再次调用服务器全新的数据来进行调用展示全新的数据
        this.getAttrList();
      } catch (error) {
        /* 
          有很多数据是服务器提供的数据,是不支持进行修改的
          系统数据进行修改会发生报错
        */
        this.$message("保存失败");
      }
    },
    

    按钮与三级联动的可操作性

    父亲给儿子传递事件直接使用props进行操作即可

    通过下面属性值的输入,来控制上面三级联动的效果, 只需传入isShowTable属性即可

    
    
    

    子组件CategorySelect直接进行接受即可 然后在每一个下拉框中加上disabled属性即可

    
    

    Spu管理模块(静态)

    • 最上面是一样的也是三级联动组件,直接调用全局组件即可,放在el-card当中
    
      
      
    
    
    • 但是和Attr组件不同的是,这下面区域有三种状态,所以我们必须使用数值来代替情况,在data中定义scene字段,表示三种状态
    /* 
      0代表展示SPU列表数据,1
      1添加SPU|修改SPU
      2添加SKU
    */
    scene: 2,
    

    动态展示Spu列表

    • 根据上面所说的Spu列表的展示其实也算是一种状态的展示,其状态可以用 scene == 0 来进行表示
    • 书写获取Spu列表数据的方法 getSpuList
    书写获取Spu列表的API接口方法

    参数 需要三个,其中如果参数个数在路径中没有体现出来的话,可以通过params参数来进行携带操作

    // 1、先引入我们二次封装的axios
    import request from '@/utils/request'
    
    // 2、获取SPU列表数据的接口
    /* 
        /admin/product/{page}/{limit}  get操作
        参数:page、limit、category3Id
    */
    export const reqSpuList = (page, limit, category3Id) => request({
        url: `/admin/product/${page}/${limit}`,
        method: 'get',
        // 还有一个参数在路径中没有体现出来,可以通过params参数进行携带
        params: { category3Id }
    })
    
    书写获取Spu列表数据的方法 getSpuList
    async getSpuList() {
      // 解构出函数需要的三个参数
      const { page, limit, category3Id } = this;
      /* 
        携带三个参数
            1、page 第几页
            2、limit 每一页需要展示多少条数据
            3、三级分类id
      */
      let result = await this.$API.spu.reqSpuList(page, limit, category3Id);
      // console.log(result);
      if (result.code == 200) {
        this.total = result.data.total;
        this.records = result.data.records;
      }
    },
    

    在从服务器获取数据的时候,像Spu列表数据,总条数等都需要在data中预先定义出来

    data() {
      return {
        // 分类的id
        category1Id: "",
        category2Id: "",
        category3Id: "",
        // 控制三级联动的可是测试性
        show: true,
        page: 1, // 分页器当前第几页
        limit: 3, // 分页器每一页需要展示多少条数据
        records: [], // spu列表的数据
        total: 0, // 分页器一共需要展示数据的条数
        /* 
          0代表展示SPU列表数据,1
          1添加SPU|修改SPU
          2添加SKU
        */
        scene: 2,
      };
    },
    
    分页器两个方法的书写
    1. 点击分页器第几页按钮的回调,其实就是将函数的参数进行了相应的修改,然后再次调用函数来返回相应的参数即可
    handleCurrentChange(page) {
      this.page = page;
      this.getSpuList();
    },
    
    1. 当分页器某一个展示数据条数发生变化的回调
    handleSizeChange(limit) {
      // 修改参数
      this.limit = limit;
      // 再次发送请求
      this.getSpuList();
    },
    
    Spu管理内容的切换

    因为此处有多个状态需要进行展示,为了方便出来可以拆分为子组件
    在这里插入图片描述
    然后在父组件中,通过v-show里面的scene值来决定是否进行展示该组件状态

    
    
    

    自己封装button组件

    有很多按钮的时候,我们可以封装一个hint-button全局组件

    
    
    
    
    
    
    • 在main.js入口文件中,进行全局注册即可
    import HintButton from '@/components/HintButton'
    Vue.component(HintButton.name, HintButton)
    

    然后在Spu组件中进行使用即可

    
    

    想要鼠标放上去显示相应的内容,传入自己想要的title值即可

    SpuForm静态组件完成

    • SpuForm中由一个一个的el-form-item来进行组成,
    • 在el-form-item中也可以自己选择el-input 、 el-option 等内容来供自己选择操作 非常关键
    
      
        
      
      
        
          
        
      
      
        
      
      
        
          
        
        
          
        
      
      
        
          
        
        添加销售属性
        
          
          
          
          
          
          
          
          
        
      
      
        保存
        取消
      
    
    

    spuForm请求业务的分析

    • v-show只是控制spuForm子组件的显示与隐藏,子组件并没有卸载
    • 我们在一个父组件中想要获取到子组件中的内容 比如说子组件中的数据和方法,可以使用ref来进行父子组件之间的通信
    
    
    • 然后我们在父组件中点击修改按钮,就可以调用子组件中的方法,我们发4个请求是在子组件中发请求的
    // 修改某一个Spu
    updateSpu(row) {
      this.scene = 1
      // 在修改某一个子组件的时候,完全可以获取一个子组件的相关内容
      // console.log(this.$refs.spu) // VueComponent
      // 在父组件中可以获取到子组件,那么对于子组件的数据和方法都可以拿到
      // 在父组件中可以通过$ref获取子组件等等
      this.$refs.spu.initSpuData(row) 
    }, 
    

    四大请求的书写

    /* 
        3、获取Spu信息
        /admin/product/getSpuById/{SpuId} get
    */
    export const reqSpu = (spuId) => request({
        url: `/admin/product/getSpuById/${spuId}`,
        method: 'get'
    })
    
    /* 
        4、获取品牌的信息
        /admin/product/baseTrademark/getTrademarkList
    */
    export const reqTradeMarkList = () => request({
        url: "/admin/product/baseTrademark/getTrademarkList",
        method: 'get',
    })
    
    /* 
        5、获取Spu图片的请求
        /admin/product/spuImageList/{spuId} get
    */
    export const reqSpuImageList = (spuId) => request({
        url: `/admin/product/spuImageList/${spuId}`,
        method: 'get'
    })
    
    /* 
        6、获取平台全部的销售属性 整个平台销售属性一共就三个
        /admin/product/baseSaleAttrList 
    */
    export const reqBaseSaleAttrList = () => request({
        url:"/admin/product/baseSaleAttrList", 
        method:"get", 
    })
    

    在子组件spuForm中获取数据

    • 先书写相应的API接口方法
    /* 
        3、获取Spu信息
        /admin/product/getSpuById/{SpuId} get
    */
    export const reqSpu = (spuId) => request({
        url: `/admin/product/getSpuById/${spuId}`,
        method: 'get'
    })
    
    /* 
        4、获取品牌的信息
        /admin/product/baseTrademark/getTrademarkList
    */
    export const reqTradeMarkList = () => request({
        url: "/admin/product/baseTrademark/getTrademarkList",
        method: 'get',
    })
    
    /* 
        5、获取Spu图片的请求
        /admin/product/spuImageList/{spuId} get
    */
    export const reqSpuImageList = (spuId) => request({
        url: `/admin/product/spuImageList/${spuId}`,
        method: 'get'
    })
    
    /* 
        6、获取平台全部的销售属性 整个平台销售属性一共就三个
        /admin/product/baseSaleAttrList 
    */
    export const reqBaseSaleAttrList = () => request({
        url:"/admin/product/baseSaleAttrList", 
        method:"get", 
    })
    
    • 初始化数据,获取需要在子组件中展示的数据
    // 初始化SpuForm数据
    async initSpuData(spu) {
      // console.log('发数据', spu)
      // 获取Spu信息的数据
      let spuResult = await this.$API.spu.reqSpu(spu.id);
      // console.log(spuResult);
      if(spuResult.code == 200) {
        this.spu = spuResult.data
      }
      // 获取品牌的信息
      let tradeMarkResult = await this.$API.spu.reqTradeMarkList() 
      // console.log(tradeMarkResult)
      if(tradeMarkResult.code == 200) {
        this.tradeMarkList = tradeMarkResult.data
      }
      // 获取spu图片的数据
      let spuImageResult = await this.$API.spu.reqSpuImageList(spu.id) 
      // console.log(spuImageResult)
      if(spuImageResult.code == 200) {
        this.spuImageList = spuImageResult.data
      }
      // 获取平台全部的销售属性
      let saleResult = await this.$API.spu.reqBaseSaleAttrList() 
      if(saleResult.code == 200) {
        this.saleAttrList = saleResult.data
      }
    },
    
    • 在data中需要配置相应的默认初始值进行接收
    data() {
      return {
        dialogImageUrl: "",
        dialogVisible: false,
        spu: {}, // 存储SPU信息属性
        tradeMarkList:[], // 存储品牌信息
        spuImageList:[], // 存储Spu图片的数据
        saleAttrList:[], // 销售属性的数据
      };
    },
    

    SpuForm销售属性的数据展示

    我们在展示数据的时候,还需要将数据同步收集

    • v-model不仅可以展示数据,也可以用于收集数据,是双向的
    • spuForm组件中用到了照片墙技术,使用到了el-upload 进行上传,需要注意的是照片墙技术是有样式的,在引入的时候需要将样式进行引入,
    
      
      
        
      
      
        
      
    
    
    • 需要注意的一点是:很多elementui提供的组件在进行使用的时候,里面的属性都有自己特定的字段。比如里面规定是name、url字段
    • 我们就需要将自己获取到的名字和url字段赋值到一个全新的数组,在进行相应的赋值操作
    // 获取spu图片的数据
    let spuImageResult = await this.$API.spu.reqSpuImageList(spu.id);
    // console.log(spuImageResult)
    if (spuImageResult.code == 200) {
      // this.spuImageList = spuImageResult.data;
      // 可以将数据进行处理,然后在返回给数组进行接收操作
      let listArr = spuImageResult.data;
      /* 
        由于照片墙显示图片的数据需要数组,
        数组里面的元素需要有name和url字段
        需要把服务器返回的数据进行修改再赋予新的值
      */
      listArr.forEach((item) => {
        item.name = item.imgName;
        item.url = item.imgUrl;
      });
      // 把整理好的数据赋值给一开始设置好的初始数据
      this.spuImageList = listArr;
    }
    
    • 尤其是有多个属性进行展示的,比如说下拉框等,我们有多个数据的时候,需要使用到v-for来进行遍历
    • 比如说在子组件当中的,使用el-option,很多数据需要展示的时候,我们就需要用到v-for来进行遍历操作
    
      
      
    
    

    el-tag数据进行展示
    在这里插入图片描述
    这一类用到el-tag组件,这一类内容都用到作用域插槽来进行

    
    

    同时我们还需要很注意,我们在进行收集用户输入的数据的时候,需要在data中创建初始数据的形态

    • 我们在spu对象内容的时候 在初始化的时候是一个空对象 在修改spu的时候 会向服务器发请求,返回SPU信息(对象)
    • 在修改的时候可以利用服务器返回的这个对象收集最新的数据提交给服务器, 添加Spu 如果是添加Spu的时候,并没有向服务器发请求,数据收集到哪里呀【SPU】收集数据的时候有哪些属性字段,需要看文档
    spu: {
      //三级分类的id
      category3Id: 0,
      //描述
      description: "",
      //spu名称
      spuName: "",
      //平台的id
      tmId: 0,
      //收集SPU图片的信息
      spuImageList: [],
      //平台属性与属性值收集
      spuSaleAttrList: [],
    },
    

    销售属性的计算

    在这里插入图片描述

    /* 
      整个平台的销售属性就三个
        - 颜色、尺寸、版本 saleAttrList
        - 当前Spu拥有的属于自己的销售属性 Spu.spuSaleAttrList -- 颜色
        - 数组过滤的方法,可以从已有的数据当中过滤出用户需要的元素,并返回一个新的数据
          - 对于filter而言,我们需要返回的是boolean值
    */
    
    复习JavaScript中的语法

    filter - 返回真表示 过滤出来的元素,我们是需要的
    every- 数组的过滤方法::可以从已有的数组当中过滤出用户需要的元素,并返回一个新的数据

    过滤出来的结果需要用一个全新的字段来进行接收

    • 计算出来的结果供给我们使用 可以用到vue2中的计算属性
    // 计算出还未选择的销售属性
    unSelectSaleAttr() {
      /* 
        整个平台的销售属性就三个
          - 颜色、尺寸、版本 saleAttrList
          - 当前Spu拥有的属于自己的销售属性 Spu.spuSaleAttrList -- 颜色
          - 数组过滤的方法,可以从已有的数据当中过滤出用户需要的元素,并返回一个新的数据
            - 对于filter而言,我们需要返回的是boolean值
      */
      let result = this.saleAttrList.filter((item) => {
        // filter 返回真表示 过滤出来的元素,我们是需要的
        // every数组的过滤方法:可以从已有的数组当中过滤出用户需要的元素,并返回一个新的数据
        // 过滤出来的结果就是一个数组
        return this.spu.spuSaleAttrList.every(item1 => {
          return item.name != item1.saleAttrName
        })
      })
      // return '二哈'
      // 对于一个计算属性肯定要有一个返回值
      return result 
    }
    

    完成spuForm照片墙图片的收集

    照片墙何时收集数据

    • 预览照片墙的时候,显示大的图片的时候,需要收集数据吗? —不需要收集的【数据已经有了】
    • 照片墙在删除图片的时候,需要收集数据 || 照片墙在添加图片的时候,需要收集数据的。

    照片墙进行上传的时候,需要用到我们特定的地址

    action="/dev-api/admin/product/fileUpload"
    '
    运行

    file-list属性是用于存放照片墙需要展示的数据 照片墙需要展示的数据有name和url属性

    :file-list="spuImageList"
    

    同时对于照片墙我们有一个照片预览和移除的功能,我们需要写上相应的回调

    :on-preview="handlePictureCardPreview"
    :on-remove="handleRemove"
    

    书写回调的时候,有三个方法,其中还有一个是照片墙上传成功的回调

    1. 照片墙删除的时候会触发
    handleRemove(file, fileList) {
      /* 
        file:代表删除的那张图片
        fileList:照片墙删除某一张图片以后,剩余其他的图片
      */
      // console.log(file, fileList);
      /* 
        收集照片墙图片的数据
        对于已有的图片【照片墙中显示的图片,有name、url字段】
        因为照片墙显示数据务必要有这两个属性
        而对服务器而言,不需要name和url字段
        将来对于有的图片的数据在提交给服务器的时候,需要进行处理
      */
    
      this.spuImageList = fileList;
    },
    
    1. 照片墙图片预览的回调
    handlePictureCardPreview(file) {
      // 将图片地址赋值给这个属性
      this.dialogImageUrl = file.url;
      // 对话框显示
      this.dialogVisible = true;
    },
    
    1. 照片墙图片上传成功的回调(将收集文件的属性fileList赋值给我们一开始就定义好的属性)
    handlerSuccess(response, file, fileList) {
      this.spuImageList = fileList;
    },
    

    销售属性添加的操作

    • 我们在收集销售属性的时候,不仅需要收集销售属性的名字,还有收集其所对应的id
    :value="`${unselect.id}:${unselect.name}`"
    
    • 收集到了在书写方法的时候,分割出来 split,而且数据进行存储的话,需要存储到spu中,添加完全以后还需要将数据进行清空
    // 添加新的销售属性
    addSaleAttr() {
      // 已经收集到了需要添加的销售属性的信息
      // 把收集到的销售属性的数据进行分割
      const [baseSaleAttrId, saleAttrName] = this.attrIdAndAttrName.split(":");
      // 向SPU对象的spuSaleAttrList属性里面添加新的销售属性
      let newSaleAttr = { baseSaleAttrId, saleAttrName, spuSaleAttrList: [] };
      // 添加新的销售属性
      /* 
        收集数据可以收集到很多位置,但是收集到spu可以直接进行展示
      */
      this.spu.spuSaleAttrList.push(newSaleAttr);
      // 将数据进行清空操作
      this.attrIdAndAttrName = "";
    },
    
    • 在el-input和button添加框中书写相应的回调,通过inputVisible来控制显示与隐藏,但是该数据不能放在data中,放在data中大家公用不能单独控制每一行里面的元素,
    • 应该在点击添加按钮的时候,通过响应式数据来添加一个inputVisible属性
    // 添加按钮的回调,其中row是当前点击的这一行销售属性
    addSaleAttrValue(row) {
      // 点击销售属性中的添加按钮,需要将button变为input,通过当前销售属性的inputVisible控制
      // 挂载在销售属性身上的响应式数据inputVisible控制这button与input切换
      this.$set(row, "inputVisible", true);
      // 通过响应式数据inputValue字段收集新增的销售属性值
      this.$set(row, "inputValue", "");
    },
    
    • 失去焦点的事件,在失去焦点的时候,我们应该将相应的数据收集起来,并到时候进行相应的展示操作

    在进行添加或者过滤方法的时候,总是报错说push方法或者啥filter未undefined 一般都是前面的调用者不是一个数组,
    我们可以这样书写

    let arr = row.spuSaleAttrValueList || []
    // 因为可能就一开始没有获取到相应的内容
    let result = arr.every((item) => item.saleAttrValueName != inputValue)
    if(!result) return 
    
    • 不过后续我们需要将arr赋值给我们项目定义的属性内容,要不然不能进行匹配添加
    row.spuSaleAttrValueList = arr
    
    • 失去焦点事件书写
    // el-input 失去焦点的事件
    handleInputConfirm(row) {
      console.log(row)
      /* 
        新增的销售属性值需要收集的字段
        baseSaleAttrId
        saleAttrValueName
        应该往当前的销售属性的属性值添加新的属性才行
        需要解构出销售属性当中收集数据
      */
      const { baseSaleAttrId, inputValue } = row;
      // console.log(baseSaleAttrId, inputValue)
      if(inputValue.trim() == "") {
        this.$message('属性值不能为空')
        return 
      }
      let arr = row.spuSaleAttrValueList || []
      // 直接书写下面
      // let result = row.spuSaleAttrValueList.every((item) => item.saleAttrValueName != inputValue)
      let result = arr.every((item) => item.saleAttrValueName != inputValue)
      if(!result) return 
      // 新增的销售属性值
      let newSaleAttrValue = { baseSaleAttrId, saleAttrValueName: inputValue };
      row.spuSaleAttrValueList = arr
      arr.push(newSaleAttrValue);
      // 因为上面添加的inputVisible为响应式的,修改其值为false就显示button了
      row.inputVisible = false;
    },
    

    删除销售属性与属性值的操作

    删除销售属性值的话,其实在删除的时候,是将一行整个属性进行删除 - 用到数组里面的splice,索引在这个时候就很有作用了,我们就是通过索引来进行删除操作,删除往往是获取到数据以后跟根据数组的索引从数组当中删除数据

    @click="spu.spuSaleAttrList.splice($index, 1)"
    

    完成修改Spu的保存操作

    • 先书写保存操作的API数据操作方法,其中修改Spu和添加Spu的方法都是一样的,差别在于修改Spu的操作需要携带相关的Id,两个方法类似,都需要在放松请求的时候携带相应的spuInfo信息,我们可以根据是否携带有相应的id来区分是修改Spu还是添加Spu
    
    /* 
        7、修改Spu || 添加Spu
        对于修改或者添加,携带给服务器参数大致是一样的,
        唯一的区别就是携带的参数是否携带id
    */
    export const reqAddOrUpdateSpu = (spuInfo) => {
        // 携带的参数带有id -- 修改spu
        if (spuInfo.id) {
            return request({
                url: "/admin/product/updateSpuInfo",
                method: 'post',
                data: spuInfo
            })
        } else {
            // 携带的参数不带有id --- 添加Spu
            return request({
                url: "/admin/product/saveSpuInfo",
                method: 'post',
                data: spuInfo
            })
        }
    }
    
    • 在外形框架中书写相应点击事件的名字,点击该按钮就会触发addOrUpdateSpu 保存或者修改的按钮,进行操作
    <el-button type="primary" @click="addOrUpdateSpu">保存</el-button>
    

    保存按钮相应的回调

    • 整理参数,需要整理照片墙的数据,对于对于图片,需要携带imageName和imageUrl字段
    • 之前为了展示将图片设置为 name和url字段,但是在数据当中需要设置为imageName和imageUrl字段才行
    • 在通知父组件回到场景0的时候,可以通过flag字段来标记是修改还是进行添加操作
    • 最后在进行清除数据的时候,有一个常用技巧
    // 保存按钮的回调
    async addOrUpdateSpu() {
      /* 
        整理参数,需要整理照片墙的数据
        携带参数,对于图片,需要携带imageName和imageUrl字段   
        map方法会返回一个全新的对象     
      */
      this.spu.spuImageList = this.spuImageList.map((item) => {
        return {
          imageName: item.name,
          // 新图放在response当中,老图放在url当中
          // 数组的map会返回一个新数组,我们可以返回给我们spuImageList字段
          imageUrl: (item.response && item.response.data) || item.url,
        };
      });
      // 发请求
      // 在进行修改保存的时候,有可能会失败,可能是修改到了官方的数据
      let result = await this.$API.spu.reqAddOrUpdateSpu(this.spu);
      // console.log(result)
      if (result.code == 200) {
        // 提示
        this.$message({
          type: "success",
          message: "保存成功",
        });
        // 也是一样的,通知父组件回到场景0那里即可
        // this.$emit("changeScene", 0);
        this.$emit("changeScene", {
          scene: 0,
          flag: this.spu.id ? "修改" : "添加",
        });
      }
      // 清除数据
      Object.assign(this._data, this.$options.data());
    },
    

    其中 this._data 可以操作data中的响应式数据,this.$options可以获取配置对象,配置对象的data函数执行,进行将响应式中的数据进行清零操作

    取消按钮的操作

    • 点击取消按钮,通知父亲来进行切换场景的操作,同样也需要进行清除数据的操作
    cancel() {
      // 取消按钮的回调,通知父亲切换场景
      this.$emit("changeScene", { scene: 0, flag: "" });
      /* 
        清理数据
        Object.assign:es6中新增的方法 可以合并对象
        组件实例 this._data 可以操作data当中的响应式数据
        this.$options可以获取配置对象,配置对象的data函数执行,
          返回的响应式数据为空的
      */
      Object.assign(this._data, this.$options.data());
    },
    

    完成添加Spu的操作

    • 需要注意的一点是,在子组件中是获取不到category3Id的, 但是在父组件通过refs与子组件通信的时候,可以将category3Id传递给子组件
    • 父组件
    // 添加Spu按钮的回调
    addSpu() {
      // 切换场景为1
      this.scene = 1
      // 通知子组件spuForm发请求 -- 两个
      // 因为在子组件当中是不能有这几个数据的,在传入的时候,可以通过父组件传送过去,因为父组件这个时候是有的
      this.$refs.spu.addSpuData(this.category3Id) 
    }, 
    
    • 同样的,相关的调用接口的方法都可以放在子组件当中
    • 点击添加Spu的操作需要调用两个方法:获取到品牌和销售属性
    // 点击添加SPU按钮的时候,发请求的函数
    async addSpuData(category3Id) {
      // 添加Spu的时候收集三级分类的id 收集到spu的category3Id大当中
      this.category3Id = category3Id;
    
      // console.log("tianjia")
      // 获取品牌的信息
      let tradeMarkResult = await this.$API.spu.reqTradeMarkList();
      // console.log(tradeMarkResult)
      if (tradeMarkResult.code == 200) {
        this.tradeMarkList = tradeMarkResult.data;
      }
      // 获取平台全部的销售属性
      let saleResult = await this.$API.spu.reqBaseSaleAttrList();
      if (saleResult.code == 200) {
        this.saleAttrList = saleResult.data;
      }
    },
    
    • 而后自己进行相关输入,点击保存按钮可以将数据进行保存
    // 点击添加SPU按钮的时候,发请求的函数
    async addSpuData(category3Id) {
      // 添加Spu的时候收集三级分类的id 收集到spu的category3Id大当中
      this.category3Id = category3Id;
    
      // console.log("tianjia")
      // 获取品牌的信息
      let tradeMarkResult = await this.$API.spu.reqTradeMarkList();
      // console.log(tradeMarkResult)
      if (tradeMarkResult.code == 200) {
        this.tradeMarkList = tradeMarkResult.data;
      }
      // 获取平台全部的销售属性
      let saleResult = await this.$API.spu.reqBaseSaleAttrList();
      if (saleResult.code == 200) {
        this.saleAttrList = saleResult.data;
      }
    },
    
    • 但是进行修改操作和添加操作成功以后,返回父组件展示的时候,定位到之前的页面不一样,这个时候就需要用到page属性和flag属性
    // 自定义事件回调(Spu)修改场景值
    changeScene({scene, flag}) {
      // flag 是为了区分保存按钮是添加还是修改
      // console.log(scene)
      this.scene = scene
      if(flag == "修改") {
        this.getSpuList(this.page)
      } else {
        this.getSpuList() 
      }
      // 修改了具体哪一页,在后面进行返回的时候,需要停留在当前页才行
      // this.getSpuList(this.page) 
    }
    
    • 在进行调试的时候,如果数据对不太上,可以使用google调试工具,看看数据是否收集到了,只有用户收集到了数据才能

    删除Spu的操作

    • 在进行删除的时候,为了美观引用了pop插件,其可以弹出一个提示框,用于提示
    • 在进行删除的时候,需要将删除的对象,直接传入进去,比如这一块删除的对象就是row ,
    • 其中组件pop中的确定删除事件是固定的,固定为onConfirm 事件
    <el-popconfirm title="这是一段内容确定删除吗?" @onConfirm="deleteSpu(row)">
    
    删除Spu的API的书写
    • 既然要删除某个内容,最好的办法就是传入id
    /* 
        8、删除Spu
        /admin/product/deleteSpu/{spuId}
    */
    export const reqDeleteSpu = (spuId) => request({
        url: `/admin/product/deleteSpu/${spuId}`,
        method: 'delete',
    })
    
    • 同样的在删除成功以后,需要重新调用数据进行展示,但是展示的页数还是有讲究的,当页数大于1的时候,展示当前页,否则展示前一页
    // 删除spu按钮的回调
    async deleteSpu(row) {
      // 删除Spu肯定是要去发请求的
      // alert(111)
      let result = await this.$API.spu.reqDeleteSpu(row.id)
      if(result.code == 200) {
        this.$message({
          type:'success', 
          message:"删除成功"
        })
        // 提示成功,删除成功以后还需要回调数据
        this.getSpuList(this.records.length>1 ? this.page : this.page-1) 
      }
    },
    

    完成SKU静态组件

    • Sku组件其实和Spu组件是很相似的,大致都是使用el-form-item组件标签来进行操作使用
    • 在el-table当中,每一列进行展示的时候,使用el-table-column,其中列当中勾选按钮使用 type=“selection” 来进行表示
    <div>
      <el-form ref="form" label-width="80px">
        <el-form-item label="SPU名称"> 海绵宝宝 </el-form-item>
        <el-form-item label="SKU名称">
          <el-input placeholder="SKU名称"></el-input>
        </el-form-item>
        <el-form-item label="价格(元)">
          <el-input placeholder="价格(元素)"></el-input>
        </el-form-item>
        <el-form-item label="重量(千克)">
          <el-input placeholder="重量(千克)"></el-input>
        </el-form-item>
        <el-form-item label="规格描述">
          <el-input type="textarea" rows="4"></el-input>
        </el-form-item>
        <el-form-item label="平台属性">
          <el-form :inline="true" ref="form" label-width="80px">
            <el-form-item label="屏幕尺寸">
              <el-select placeholder="请选择" value="">
                <el-option label="label" value="value"></el-option>
              </el-select>
            </el-form-item>
          </el-form>
        </el-form-item>
        <el-form-item label="销售属性">
          <el-form :inline="true" ref="form" label-width="80px">
            <el-form-item label="屏幕尺寸">
              <el-select placeholder="请选择" value="">
                <el-option label="label" value="value"></el-option>
              </el-select>
            </el-form-item>
          </el-form>
        </el-form-item>
        <el-form-item label="图片列表">
          <el-table style="width: 100%" border>
            <el-table-column
              prop="prop"
              label="label"
              width="width"
              type="selection"
            ></el-table-column>
            <el-table-column prop="prop" label="图片" width="width">
            </el-table-column>
            <el-table-column prop="prop" label="名称" width="width">
            </el-table-column>
            <el-table-column prop="prop" label="操作" width="width">
            </el-table-column>
          </el-table>
        </el-form-item>
        <el-form-item>
          <el-button type="primary">保存</el-button>
          <el-button>取消</el-button>
        </el-form-item>
      </el-form>
    </div>
    

    获取SkuForm数据进行展示

    • 在获取SkuForm数据进行展示的时候,点击添加的那一刻需要发送3个请求操作,
    • 书写三个发送请求的API方法,获取图片数据、获取销售属性数据、获取平台属性的数据
    // 1、先引入我们二次封装的axios
    import request from '@/utils/request'
    
    /* 
        1、获取图片的数据
        /admin/product/spuImageList/{spuId} get
    */
    export const reqSpuImageList = (spuId) => request({
        url: `/admin/product/spuImageList/${spuId}`,
        method: 'get'
    })
    
    /* 
        2、获取销售属性的数据
        /admin/product/spuSaleAttrList/{spuId} get
    */
    export const reqSpuSaleAttrList = (spuId) => request({
        url: `/admin/product/spuSaleAttrList/${spuId}`,
        method: 'get'
    })
    
    /* 
        3、获取平台属性的数据
        /admin/product/attrInfoList/{category1Id}/{category2Id}/{category3Id}  get 
    */
    export const reqAttrInfoList = (category1Id, category2Id, category3Id) => request({
        url: `/admin/product/attrInfoList/${category1Id}/${category2Id}/${category3Id}`,
        method: 'get'
    })
    
    • 在父组件中书写点击添加sku按钮回调,父组件中可以收集到相关的分类的id,可以传送到子组件当中
    // 添加Sku按钮的回调
    addSku(row) {
      // 切换场景为2
      this.scene = 2
      // 通过父组件调用子组件的方法,让子组件发请求  -- 三个请求
      // 父组件身上有的参数,可以直接传送过去
      this.$refs.sku.getData(this.category1Id, this.category2Id, row) 
    }
    
    • 在SkuForm子组件当中书写相应的回调操作
    • 没有使用到vuex所以相关的数据都需要在data当中进行初始化
    data() {
      return {
        // 有需要获取到数据的时候,设置一个初始属性进行相应接收才行
        // 存储图片的信息
        spuImageList: [],
        // 存储销售的属性
        spuSaleAttrList: [],
        // 存储平台属性的数据
        reqAttrInfoList: [],
      };
    },
    
    • 书写相应的获取数据的方法
    methods: {
      // 获取SkuForm数据
      async getData(category1Id, category2Id, spu) {
        // console.log("获取数据")
        // 获取图片的数据
        let result = await this.$API.sku.reqSpuImageList(spu.id);
        // console.log(result);
        if (result.code == 200) {
          this.spuImageList = result.data;
        }
        // 获取销售属性的数据
        let result1 = await this.$API.sku.reqSpuSaleAttrList(spu.id);
        // console.log(result1)
        if (result1.code == 200) {
          this.spuSaleAttrList = result1.data;
        }
        // 获取平台属性的数据
        let result2 = await this.$API.sku.reqAttrInfoList(
          category1Id,
          category2Id,
          spu.category3Id
        );
        // console.log(result2);
        if(result2.code == 200) {
          this.reqAttrInfoList = result2.data
        }
      },
    },
    

    展示Sku数据与收集Sku数据

    • 前面我们通过发送3个请求来获取Sku相关的数据,存储在下面当中
    // 存储图片的信息
    spuImageList: [],
    // 存储销售的属性
    spuSaleAttrList: [],
    // 存储平台属性的数据
    attrInfoList: [],
    
    • 但是我们还需要将数据收集到skuInfo当中,用于向服务器发送请求的操作,用一个大的对象来进行表示
    skuInfo: {
      // 我们是添加sku,已有的才有id
      // id: 0,
      // isSale: 0,
      // 第一类收集的数据:父组件给的数据,
      category3Id: 0,
      spuId: 0,
      tmId: 0,
      // 第二类:需要通过数据双向绑定v-model收集
      skuName: "",
      price: 0,
      weight: "",
      skuDesc: "",
      // 第三类:需要自己书写代码
      // 平台属性
      skuAttrValueList: [
        {
          attrId: 0,
          valueId: 0,
          // attrName: "string",
          // id: 0,
          // skuId: 0,
          // valueName: "string",
        },
      ],
      // 设置默认图片
      skuDefaultImg: "",
      // 收集图片的字段,默认为一个空数组
      skuImageList: [
        // {
        //   id: 0,
        //   imgName: "string",
        //   imgUrl: "string",
        //   isDefault: "string",
        //   skuId: 0,
        //   spuImgId: 0,
        // },
      ],
      // 销售属性
      skuSaleAttrValueList: [
        // {
        //   id: 0,
        //   saleAttrId: 0,
        //   saleAttrName: "string",
        //   saleAttrValueId: 0,
        //   saleAttrValueName: "string",
        //   skuId: 0,
        //   spuId: 0,
        // },
      ],
    },
    
    • skuInfo中的字段,我们在获取SkuForm中的数据的时候,也可以先进行收集存储
    // 收集父组件给予的数据
    this.skuInfo.category3Id = spu.category3Id;
    this.skuInfo.spuId = spu.id;
    this.skuInfo.tmId = spu.tmId;
    
    this.spu = spu;
    
    • 尤其是对于图片的操作,我们需要对图片列表,加上相应的id,在最初的获取操作,加上我们想要的默认属性字段
    • 图片显示区域有一个默认字段和显示两种状态的互显示,这种排他的数据,只能在数据内容中单个显示,不可在data中设置显示
    // 将列表中的每个对象加上一个默认属性字段
    list.forEach((item) => {
      // 给每一个图片的信息,加上这个需要的字段即可
      // 0代表显示设置默认,1代表默认
      item.isDefault = 0;
    });
    // 这种往默认属性字段中添加了相应的属性,乃至属性值,其实就是对原有数组进行了改变
    // 将数组赋值到原有数组进行存储
    this.spuImageList = list;
    
    图片列表显示的业务
    • 选中第一列当中的复选框, 这是elementui提供的包装方法,选中的参数就是该张图片的信息
    // table表格复选框按钮的事件
    handleSelectionChange(params) {
      /* 
        获取到用户选中图片的信息数据
        但是需要注意,当前收集的数据当中,缺少isDefault字段
        现在收集到的数据是不完整的,所以不能存储在skuImageList当中,
        因为这一块将来是需要提交给服务器的
      */
      this.imageList = params;
    },
    
    • 图片的显示设置默认和默认的操作, 即排他的操作, 即两种状态只能显示其中一种
    • 结构样式:el-table-column中,通过作用域插槽,传入两个按钮,通过v-if和v-else来实现互斥操作,
    • 通过 @click=“changeDefault(row)” 来改变default属性值

    排他的操作

    • 先将图片字段中的所有的isDefault字段变为0,然后将你点击的那个字段变为1,收集一下默认图片的地址
    // 排他的操作
    changeDefault(row) {
      // 图片列表的isDefault字段变为0,只有你点击的那个变为1
      this.spuImageList.forEach((item) => {
        // 所有的isDefault字段变为0
        item.isDefault = 0;
      });
      // 点击的那个图片的数据变为1
      row.isDefault = 1;
      // 收集一下默认图片的地址
      this.skuInfo.skuDefaultImg = row.imgUrl;
    },
    

    其他的内容数据收集操作都是通过v-model来进行收集信息 - 实现数据的交互操作

    完成添加Sku保存操作

    先完成取消操作

    • 点击取消之后,我们需要通知父组件改变scene的值,来进行切换场景操作
    • 触发切换场景以后,还需要将数据进行清除操作
    cancel() {
      // 自定义事件,让父组件切换场景为0
      this.$emit("changeScenes", 0);
      // 清除数据
      Object.assign(this._data, this.$options.data());
    },
    

    保存按钮的操作

    • 1、整理平台属性
    • 2、整理好数据以后,进行发请求的操作
    • 3、发送成功以后,给予提示信息,并提示父组件进行切换场景的操作
    整理平台属性的数据方式一(forEach)
    // 整理平台属性的数据方式一
    // 新建一个数组
    let arr = []
    // 把收集到的数据整理一下
    attrInfoList.forEach(item => {
      // 当前平台属性用户进行了选择
      if(item.attrIdAndValueId) {
        const [attrId, valueId] = item.attrIdAndValueId.split(":")
        // 携带给服务器的参数,应该是一个对象
        let obj = {attrId, valueId}
        arr.push(obj)
      }
    })
    // 将整理好的参数字段赋值给skuInfo.skuAttrValueList
    skuInfo.skuAttrValueList = arr
    
    整理平台属性的数据方式二(reduce)
    /* 
      prev初始值为[] 初始值为一个空数组
      item为遍历到的每一个元素
    
      常用于:求数组累加和、最大值、当前情况
    */
    // 返回的是最后一次执行的结果
    skuInfo.skuAttrValueList = attrInfoList.reduce((prev, item) => {
      // 用户已经选择了
      if (item.attrIdAndValueId) {
        const [attrId, valueId] = item.attrIdAndValueId.split(":");
        prev.push({ attrId, valueId });
      }
      // 最后一次返回执行的结果 reduce需要将下一次的结果返回,当做下一次的
      return prev;
    }, []);
    

    整理销售属性

    // 整理销售属性 spuSaleAttrList
    skuInfo.skuSaleAttrValueList = spuSaleAttrList.reduce((prev, item) => {
      if (item.attrIdAndValueId) {
        const [saleAttrId, saleAttrValueId] =
          item.attrIdAndValueId.split(":");
        prev.push({ saleAttrId, saleAttrValueId });
      }
      return prev;
    }, []);
    

    整理图片的数据 - 利用已有的图片数据来映射出一个新的数据出来

    // 整理图片的数据 利用已有的图片数据来映射出一个新的数据出来
    // map是映射出一个新的数组,赋值给它即可
    skuInfo.skuImageList = imageList.map((item) => {
      return {
        imgName: item.imgName,
        imgUrl: item.imgUrl,
        isDefault: item.isDefault,
        spuImgId: item.id,
      };
    });
    

    整理好的数据都放在了skuInfo当中 通过发送请求,成功以后弹出提示信息,然后提示父组件进行场景调换即可

    // 整理好了数据以后,发送请求即可
    let result4 = await this.$API.sku.reqAddSku(skuInfo)
    // console.log(result4)
    if(result4.code == 200) {
      this.$message({
        type:"success", 
        message:"添加Sku成功"
      })
      this.$emit('changeScenes', 0)
    }
    

    对于map的测试

    • 若arr的形式是这种
    let arr = [
        // 对象里面肯定是放键值对冒号的形式出来
        { imgName: 1 },
        { imgUrl: 2 },
        { isDefault: 3 },
        { id: 4 }
    ]
    
    • 则使用map对里面arr中的每个对象进行赋值操作得到的结果是
    let arr = [
        // 对象里面肯定是放键值对冒号的形式出来
        { imgName: 1 },
        { imgUrl: 2 },
        { isDefault: 3 },
        { id: 4 }
    ]
    
    let skuInfo = arr.map(item => {
        return {
            imgName: item.imgName,
            imgUrl: item.imgUrl,
            isDefault: item.isDefault,
            spuImgId: item.id,
        }
    })
    
    console.log(skuInfo)
    // 结果
    [
      {
        imgName: 1,
        imgUrl: undefined,
        isDefault: undefined,
        spuImgId: undefined
      },
      {
        imgName: undefined,
        imgUrl: 2,
        isDefault: undefined,
        spuImgId: undefined
      },
      {
        imgName: undefined,
        imgUrl: undefined,
        isDefault: 3,
        spuImgId: undefined
      },
      {
        imgName: undefined,
        imgUrl: undefined,
        isDefault: undefined,
        spuImgId: 4
      }
    ]
    
    
    let arr = [
        // 对象里面肯定是放键值对冒号的形式出来
        {
            imgName: 1,
            imgUrl: 2,
            isDefault: 3,
            id: 4
        }
    ]
    
    // 对数组里面的每个对象进行操作
    let skuInfo = arr.map(item => {
        return {
            imgName: item.imgName,
            imgUrl: item.imgUrl,
            isDefault: item.isDefault,
            spuImgId: item.id,
        }
    })
    
    console.log(skuInfo) 
    // 结果 非常关键
    [ { imgName: 1, imgUrl: 2, isDefault: 3, spuImgId: 4 } ]
    

    对于reduce的测试

    • reduce接收两个参数:① 执行归并操作的函数,任务:将两个值归并或组合为一个值并返回这个值 ② 可选的,是传给归并函数的初始值
    let a = [1, 2, 3, 4, 5]
    console.log(a.reduce((x, y) => x + y, 0)) // 15
    console.log(a.reduce((x, y) => x * y, 1)) // 120
    console.log(a.reduce((x, y) => (x > y) ? x : y)) // 5
    
    let a = [1, 2, 3, 4, 5]
    b = a.reduce((x, y) => {
    	// 完整的写法,工作中用的多
        x = x + y
        return x  // 每次要将x前一个参数进行返回,作为下一次运算的参数
    }, 0)
    console.log(b) // 15
    

    Sku列表的展示

    • 想要查看具体内容的话,像本文是用到了el-dialog插件来进行的
    • 像这种样式,随便放在哪一个位置,因为它有专门的属性来控制对话框的显示与隐藏
    • 里面也是用el-table来进行展示操作的
    <!-- 像这种点击一下就进行显示的操作,放在随便哪一个位置都可以,弄成动态的才行 -->
    <el-dialog
      :title="`${spu.spuName}的sku列表`"
      :visible.sync="dialogTableVisible"
      :before-close="close"
    >
      <el-table :data="skuList" style="width: 100%" border v-loading="loading">
        <!-- prop当中展示相应的字段操作 -->
        <el-table-column prop="skuName" label="名称" width="width">
        </el-table-column>
        <el-table-column prop="price" label="价格" width="width">
        </el-table-column>
        <el-table-column prop="weight" label="重量" width="width">
        </el-table-column>
        <el-table-column label="默认图片" width="width">
          <template slot-scope="{ row, $index }">
            <img
              :src="row.skuDefaultImg"
              alt=""
              style="width: 100px; height: 100px"
            />
          </template>
        </el-table-column>
      </el-table>
    </el-dialog>
    
    • 上方点击按钮的操作的回调
    @click="handler(row)"
    
    • 书写相应的回调操作
    async handler(spu) {
      // 点击按钮的时候,对话框应该是可见的
      this.dialogTableVisible = true;
      // 保存spu的信息,因为我们想要在别的地方进行展示,保存到data当中
      this.spu = spu;
      // 刚才写了相应的参数,我们需要获取sku列表的数据进行展示
      let result = await this.$API.sku.reqSkuList(spu.id);
      // console.log(result);
      if (result.code == 200) {
        this.skuList = result.data;
        // 将loading进行隐藏
        this.loading = false 
      }
    },
    
    • 在对话框中,:before-close=“close” 常在这类函数中书写相关的逻辑,比如加载等
    // 关闭对话框的回调
    close(done) {
      // loading属性再次变为真
      this.loading = true
      // 清除sku列表的数据,每次展示前将数据进行清除操作
      this.skuList = []
      // 关闭对话框
      done() 
    }
    

    Sku模块数据展示与分页功能

    • 同样的套路,没有了vuex,要进行数据展示,必须在data中一开始就对数据进行声明,到时候获取到了在进行存储
    • 对于分页器有几个常用的属性,① 当前第几页 ② 当前页面显示的数据的条数 ③ 总共需要展示多少条数据
    • 对于别的数据的展示,我们也需要弄出来, 直接弄一个初始状态即可
    data() {
      return {
        // 代表当前第几页
        page: 1,
        // 代表当前页面有几条数据
        limit: 10,
        // 存储Sku列表的数据
        records: [],
        // 存储分页器一共展示的数据
        total: 0,
        // 存储sku的信息的
        skuInfo: {},
        show: false,
      };
    },
    
    • 因为发请求需要在多个地方请求,所以发送数据的回调方法一般不放在mounted当中,而是放在methods中,到时候进行调用的时候比较方便

    获取sku列表数据的接口

    /* 
        6、sku列表的接口
        /admin/product/list/{page}/{limit}
    */
    export const reqSkuList2 = (page, limit) => request({
        url: `/admin/product/list/${page}/${limit}`,
        method: 'get'
    })
    

    获取sku列表数据的方法

    async getSkuList(pages = 1) {
      this.page = pages;
      // 需要用到哪些参数,我们后续需要解构出来
      const { page, limit } = this;
      let result = await this.$API.sku.reqSkuList2(page, limit);
      // console.log(result)
      if (result.code == 200) {
        this.total = result.data.total;
        this.records = result.data.records;
      }
    },
    

    在结构进行展示的时候 尤其是在el-table中展示的时候,直接将要展示的数据,放入到:data中,
    而el-table-column中的数据进行展示的时候,直接使用:data 的字段即可

    <!-- 表格 -->
    <el-table style="width: 100%" border :data="records">
      <el-table-column type="index" label="序号" width="80" align="center">
      </el-table-column>
      <!-- 里面在进行展示的时候,直接进行展示即可,没有多余的操作 -->
      <el-table-column prop="skuName" label="名称" width="width">
      </el-table-column>
      <el-table-column prop="skuDesc" label="描述" width="width">
      </el-table-column>
      <el-table-column prop="prop" label="默认图片" width="110">
        <template slot-scope="{ row, $index }">
          <img
            :src="row.skuDefaultImg"
            alt=""
            style="width: 80px; height: 80px"
          />
        </template>
      </el-table-column>
      <el-table-column prop="weight" label="重量" width="80"> </el-table-column>
      <el-table-column prop="price" label="价格" width="80"> </el-table-column>
      <el-table-column prop="prop" label="操作" width="width">
        <template slot-scope="{ row, $index }">
          <el-button
            type="success"
            icon="el-icon-sort-down"
            size="mini"
            v-if="row.isSale == 0"
            @click="sale(row)"
          ></el-button>
          <el-button
            type="success"
            icon="el-icon-sort-up"
            size="mini"
            v-else
            @click="cancel(row)"
          ></el-button>
          <el-button
            type="primary"
            icon="el-icon-edit"
            size="mini"
            @click="edit"
          ></el-button>
          <el-button
            type="info"
            icon="el-icon-info"
            size="mini"
            @click="getSkuInfo(row)"
          ></el-button>
          <el-button
            type="danger"
            icon="el-icon-delete"
            size="mini"
          ></el-button>
        </template>
      </el-table-column>
    </el-table>
    

    分页的操作

    <el-pagination
      style="text-align: center"
      :current-page="page"
      :page-sizes="[3, 5, 10]"
      :page-size="limit"
      layout="prev, pager, next, jumper, ->, sizes, total"
      :total="total"
      @current-change="getSkuList"
      @size-change="handleSizeChange"
    >
    

    两个回调方法

    @current-change="getSkuList"
    
    // 可以复用上述的getSkuList的方法,可以都是获取pages页进行数据展示,不过之前的固定是page = 1
    // 获取sku列表数据的方法
    async getSkuList(pages = 1) {
      this.page = pages;
      // 需要用到哪些参数,我们后续需要解构出来
      const { page, limit } = this;
      let result = await this.$API.sku.reqSkuList2(page, limit);
      // console.log(result)
      if (result.code == 200) {
        this.total = result.data.total;
        this.records = result.data.records;
      }
    },
    
    @size-change="handleSizeChange"
    // 修改携带的参数然后再进行展示
    handleSizeChange(limit) {
      // 修改参数,然后发送请求即可
      this.limit = limit;
      this.getSkuList();
    },
    

    sku上架与下架的操作

    上架与下架操作 这两个按钮为互斥操作,即这两个按钮也只能展示其中一个

    • 只能展示其中一个使用v-if v-else 来进行操作,需要控制某一个属性来控制
    • 注意某一行数据当中是有isSale属性,来控制sku产品的上架与下架操作
    <el-button
      type="success"
      icon="el-icon-sort-down"
      size="mini"
      v-if="row.isSale == 0"
      @click="sale(row)"
    ></el-button>
    <el-button
      type="success"
      icon="el-icon-sort-up"
      size="mini"
      v-else
      @click="cancel(row)"
    ></el-button>
    
    • 书写上架与下架的API
    /* 
        7、商品的上架操作
        /admin/product/onSale/{skuId}
    */
    export const reqSale = (skuId) => request({
        url: `/admin/product/onSale/${skuId}`,
        method: 'get',
    })
    
    /* 
        8、商品的下架操作
        /admin/product/cancelSale/{skuId}
    */
    export const reqCancel = (skuId) => request({
        url: `/admin/product/cancelSale/${skuId}`,
        method: 'get',
    })
    

    书写上架操作和下架操作的方法的回调

    • 上下架操作其实就是让后台进行展示,我们只需要切换页面,然后弹出成功的提示信息即可
    // 上架的业务
    async sale(row) {
      let result = await this.$API.sku.reqSale(row.id);
      if (result.code == 200) {
        // 上架以后,将isSale字段进行相应的修改操作
        row.isSale = 1;
        this.$message({
          type: "success",
          message: "上架成功",
        });
      }
    },
    // 下架的业务
    async cancel(row) {
      let result = await this.$API.sku.reqCancel(row.id);
      if (result.code == 200) {
        row.isSale = 0;
        this.$message({
          type: "success",
          message: "下架成功",
        });
      }
    },
    // edit
    edit() {
      this.$message("正在开发中");
    },
    

    sku详情查看完成

    获取详情数据的api书写

    /* 
        9、获取sku详情的接口
        /admin/product/getSkuById/{skuId} get
    */
    export const reqSkuById = (skuId) => request({
        url: `/admin/product/getSkuById/${skuId}`,
        method: 'get'
    })
    
    // 获取sku详情的方法
    async getSkuInfo(sku) {
      // 展示抽屉
      this.show = true;
      // 获取Sku的数据
      let result = await this.$API.sku.reqSkuById(sku.id);
      if (result.code == 200) {
        this.skuInfo = result.data;
      }
    },
    
    • 将数据存储在skuInfo属性字段中,这一点很关键
    • 通过抽屉插件来进行sku数据的展示
    • 在进行数据的样式的布局,我们还可以通过el-row || el-col来进行布局
    • 轮播图使用el-carousel标签来使用
    <el-drawer
      title="我是标题"
      :visible.sync="show"
      :before-close="handleClose"
      :show-close="false"
      size="50%"
    >
      <el-row>
        <el-col :span="5">名称</el-col>
        <el-col :span="16">{{ skuInfo.skuName }}</el-col>
      </el-row>
      <el-row>
        <el-col :span="5">描述</el-col>
        <el-col :span="16">{{ skuInfo.skuDesc }}</el-col>
      </el-row>
      <el-row>
        <el-col :span="5">价格</el-col>
        <el-col :span="16">{{ skuInfo.price }}</el-col>
      </el-row>
      <el-row>
        <el-col :span="5">平台属性</el-col>
        <el-col :span="16">
          <template>
            <el-tag
              type="success"
              v-for="(attr, index) in skuInfo.skuAttrValueList"
              :key="attr.id"
              style="margin-right: 10px"
            >
            // 展示的话,还是使用{{}}进行展示操作
              {{ attr.attrId }}-{{ attr.valueId }}
            </el-tag>
          </template>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="5">商品图片</el-col>
        <el-col :span="16">
          <el-carousel height="150px">
         	// data中定义的数据是可以直接获取展示的
            <el-carousel-item
              v-for="item in skuInfo.skuImageList"
              :key="item.id"
            >
              <img :src="item.imgUrl" alt="" />
            </el-carousel-item>
          </el-carousel>
        </el-col>
      </el-row>
    </el-drawer>
    

    深度选择器

    • 为了让父组件中style中的样式书写了scoped属性以后还能影响到子组件我们可以使用深度选择器来实现
    >>>  一般用于原生CSS
    
    /deep/ 一般用于less
    
     ::v-deep 一般用户scss
    
    • 一般的我们在父组件中书写了样式,并在style中添加了scoped属性,我们只能在子组件中的div那一层拥有和父组件一样的样式

    数据可视化

    数据可视化简介

    • 就是服务器返回的数据,是以视图的形式进行展示【饼图、折线图,K线图】
    echarts:vue、react
    v-chart:vue
    d3.js:vue、react
    hightchart:vue、react
    echarts:基本使用
    

    canvas绘制线段

    canvas画布

    • 是HTML5中新增的一个特性,双闭合标签
    • 该标签默认具有宽度与高度 300 * 150
    • 浏览器认为canvas标签是一张图片,可以另存为
    • 给canvas画布添加文本内容没有任何意义
    • 给canvas添加子节点也是没有任何意义
    • 你想操作canvas画布:画布当中绘制图形,显示一个文字,都必须通过JS完成
    • canvas标签的w|h务必通过canvas标签属性width||height设置
    • 切记不能通过样式去设置画布的宽度与高度

    注意点:

    /* 
        - canvas标签任何操作务必通过JS完成
        - 通过“JS” 当中的“笔”去完成
    
        - 该图形是由像素点组成的像素群
    */
    '
    运行

    使用canvas绘制线段

    <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>canvas的基本使用</title>
        <style>
            * {
                /* 书写的时候,每次结束用分号隔开 */
                margin: 0;
                padding: 0;
            }
    
            canvas {
                border: 1px solid black;
            }
        </style>
    </head>
    
    <body>
        <!-- 
            canvas画布
            -HTML5中新增的一个特性,双闭合标签
            - 该标签默认具有宽度与高度 300 * 150
            - 浏览器认为canvas标签是一张图片,可以另存为
            - 给canvas画布添加文本内容没有任何意义
            - 给canvas添加子节点也是没有任何意义
            - 你想操作canvas画布:画布当中绘制图形,显示一个文字,都必须通过JS完成
            - canvas标签的w|h务必通过canvas标签属性width||height设置
            - 切记不能通过样式去设置画布的宽度与高度
        -->
        <canvas width="600" height="400"></canvas>
    </body>
    
    <script>
        /* 
            - canvas标签任何操作务必通过JS完成
            - 通过“JS” 当中的“笔”去完成
    
            - 该图形是由像素点组成的像素群
        */
        let canvas = document.querySelector('canvas')
        // 获取画布的笔【上下文】
        let ctx = canvas.getContext('2d')
        // console.log(ctx)
        // 绘制线段:绘制线段的起点的设置
        ctx.moveTo(100, 100)
        // 其他点的设置(可以有多个)
        ctx.lineTo(100, 200)
        ctx.lineTo(200, 100)
        // 设置图形填充的颜色
        ctx.fillStyle = "red"
        ctx.fill() 
        // 设置图型的线段的颜色与宽度
        ctx.strokeStyle = "purple"
        ctx.lineWidth = "20"
        // 可以设置起点与最终的结束点连接在一起
        ctx.closePath() 
        // 调用stroke方法去绘制线段
        ctx.stroke() 
    
    </script>
    

    使用canvas绘制矩形

    • 绘制矩形使用:strokeRect() 其有四个参数,分别代表左上角距离x的宽度和y的宽度,以及所要绘制矩形的宽高
    
        
        
        
        Document
        
    
    
    
        
    
    
    
    

    使用canvas绘制圆形

    • 绘制圆形使用的方式是arc() 方法,其有六个参数,分别代表: x, y, r, 起始的弧度, 结束的弧度, 是否逆时针绘制(x, y 是圆心距离水平轴和y轴)
    
        
        
        
        Document
        
    
    
    
        
    
    
    

    画布清除与绘制文字

    **清除画布使用clearRect()**其有四个参数
    **绘制文字使用fillText()**其有三个参数

    
        
        
        
        Document
        
    
    
    
        
        
    
    
    

    绘制柱状图

    所要实现效果图
    在这里插入图片描述
    实现代码

    
        
        
        
        Document
        
    
    
    
        
    
    
    
    
    • 其是通过一条条线来进行绘制完成的,然后在响应的位置加上字,最后绘制矩形,填充颜色即可

    svg的基本使用

    1. svg双闭合标签
    2. 默认宽度与高度为300 * 150
    3. svg绘制图形务必在svg标签内部使用绘制图形
    4. svg标签内部有很多的属性,可以通过这些内部标签属性来绘制图形
    
        
        
        
        Document
        
    
    
    
        
        
            
            
            
    
            
            
    
            
            
            
    
                    
            
    
            
            
    
            
            
    
            
            
        
    
    

    echarts的基本使用

    • 首先echarts的使用要先准备一个容器
    • 然后在获取配置项与数据,根据配置项和数据来进行绘制表格的操作
    
        
        
        
        Document
        
        
        
    
    
    
        
        

    echarts展示多个表格

    既然要展示多个表格,就需要准备多个容器,然后创建多个实例 - 然后根据配置项和数据来创建相应的表格

    
        
        
        
        Document
        
        
        
    
    
    
        
        

    echarts数据集dataset的使用

    • 即可以不用在series中声明各种类型图的时候把对应的数据传入进去,而是在外面用配置项data进行声明,而在图表的配置项与数据当中使用数据源进行引入,series中用encode进行声明
    <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>
        <!-- 引入echarts依赖包 -->
        <script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.3.3/echarts.common.js"></script>
        <style>
            * {
                margin: 0px;
                padding: 0px;
            }
    
            div {
                width: 800px;
                height: 400px;
            }
        </style>
    </head>
    
    <body>
        <!-- 准备一个容器:容器就是显示图标的区域 -->
        <div></div>
    </body>
    
    <script>
        // 基于准备好的dom初始化一个echart实例
        let dom = document.querySelector('div')
        // 创建echarts实例
        let mycharts = echarts.init(dom)
        let data = [
            ["衣服", 10, 22, 'x', 10],
            ["直播", 12, 55, 'y', 60],
            ["游戏", 16, 44, 'z', 50],
            ["电影", 19, 32, 't', 70],
        ]
        // 准备指定图表的配置项与数据
        mycharts.setOption({
            // 设置字符集
            dataset: {
                // 数据源
                source: data,
            },
            // 图标的标题
            title: {
                // 主标题的设置
                text: "数据可视化",
                // 子标题
                subtext: "echarts的基本使用",
                // 主标题的颜色
                textStyle: {
                    color: "cyan"
                },
                // 设置标题位置
                left: "center"
            },
            // x轴的配置项
            xAxis: {
                // 数据
                data: ["衣服", "直播", "游戏", "电影"]
            },
            // y轴的配置项
            yAxis: {
                // 显示Y轴的线条
                axisLine: {
                    show: true,
                },
                // 显示Y轴的刻度
                axisTick: {
                    show: true,
                }
            },
            // 系列的设置
            series: [
                // 柱状图
                {
                    // 图表类型的设置
                    type: "bar",
                    // 图表的数据
                    // data: [10, 20, 30, 40],
                    // 颜色
                    color: "red",
                    encode: {
                        y: 1,
                    }
                },
                // 折线图
                {
                    // 图表类型的设置
                    type: "line",
                    // 图表的数据
                    // data: [10, 20, 30, 40],
                    color: "pink",
                    encode: {
                        y: 2,
                    }
                },
                // 饼图
                {
                    type: "pie",
                    // 想要展示文字和数字可以data里面配置对象
                    // 饼图你可以显示文字,data写法如下
                    /* data: [
                        { name: 'x', value: 10 },
                        { name: 'y', value: 20 },
                        { name: 'z', value: 30 },
                        { name: 't', value: 40 },
                    ], */
                    // 饼图的宽度与高度
                    width: 250,
                    height: 250,
                    // 饼图的位置
                    left: 150,
                    top: 100,
                    // 饼图的半径
                    radius: 25,
                    encode: {
                        // 饼图旁边的文字,
                        itemName: 3,
                        value: 4,
                    }
                }
            ]
        })
    
    </script>
    

    echarts内置组件的使用

    • echarts内部有很多的组件,比如提示组件、内置切换组件等,需要使用的话,可以进行引入
    <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>
        <script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.3.3/echarts.common.js"></script>
        <style>
            * {
                margin: 0px;
                padding: 0px;
            }
    
            .box {
                width: 100%;
                height: 400px;
                /* border: 1px solid black; */
            }
        </style>
    </head>
    
    <body>
        <!-- 准备容器 -->
        <div class="box"></div>
    </body>
    
    <script>
        // 初始化echarts实例
        // 获取容器
        let dom = document.querySelector('.box')
        let myCharts = echarts.init(dom)
        // 配置数据
        myCharts.setOption({
            dataZoom: {},
            // 标题
            title: {
                text: "echarts组件",
            },
            xAxis: {
                data: ['游戏', '电影', '直播', '娱乐'],
            },
            yAxis: {},
            series: [
                {
                    name: "柱状图",
                    type: "bar",
                    data: [10, 20, 30, 40]
                },
                {
                    name: "折线图",
                    type: "line",
                    data: [30, 40, 50, 60]
                }
            ],
            // 提示组件
            tooltip: {
                // 提示框文字的颜色
                textStyle: {
                    color: 'red',
                }
            },
            // 系列切换组件
            legend: {
                data: ['柱状图', '折线图']
            },
            toolbox: {
                show: true,
                feature: {
                    dataZoom: {
                        yAxisIndex: 'none'
                    },
                    dataView: { readOnly: false },
                    magicType: { type: ['line', 'bar'] },
                    restore: {},
                    saveAsImage: {}
                }
            },
            // 调整图表的布局
            grid: {
                left: 30,
                right: 0,
            }
        })
    
    </script>
    

    echarts坐标体系

    ## echarts坐标体系之一个坐标体系

    例子:散点图

    <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>
        <!-- 引入echarts依赖包 -->
        <script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.3.3/echarts.common.js"></script>
        <style>
            * {
                margin: 0px;
                padding: 0px;
            }
    
            div {
                width: 100%;
                height: 400px;
            }
        </style>
    </head>
    
    <body>
        <!-- 准备一个容器:容器就是显示图标的区域 -->
        <div></div>
    </body>
    
    <script>
        // 基于准备好的dom初始化一个echart实例
        let dom = document.querySelector('div')
        // 创建echarts实例
        let mycharts = echarts.init(dom)
        // 准备指定图表的配置项与数据
        mycharts.setOption({
            // 标题
            title: {
                text: "一个坐标系",
            },
            // X轴和Y轴 的使用 
            xAxis: {
                type: "category"
            },
            yAxis: {},
            // 散点图
            series: [
                {
                    type: "scatter",
                    // 散点图的数据 是一个二维数组
                    data: [
                        [10, 20],
                        [13, 66],
                        [50, 9],
                        [44, 22],
                        [15, 10]
                    ]
                }
            ]
        })
    
    </script>
    

    echarts坐标体系之多个坐标体系

    • series中在每个对象中设置自己的属性即可(yAxisIndex)
    <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>
        <!-- 引入echarts依赖包 -->
        <script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.3.3/echarts.common.js"></script>
        <style>
            * {
                margin: 0px;
                padding: 0px;
            }
    
            div {
                width: 100%;
                height: 400px;
            }
        </style>
    </head>
    
    <body>
        <!-- 准备一个容器:容器就是显示图标的区域 -->
        <div></div>
    </body>
    
    <script>
        // 基于准备好的dom初始化一个echart实例
        let dom = document.querySelector('div')
        // 创建echarts实例
        let mycharts = echarts.init(dom)
        // 准备指定图表的配置项与数据
        mycharts.setOption({
            // 标题
            title: {
                text: "双坐标",
            },
            // X轴和Y轴 的使用 
            xAxis: {
                data: ['游戏', '直播', '经济', '娱乐'],
            },
            yAxis: [
                {
                    // 显示Y轴的线条
                    axisLine: {
                        show: true,
                    },
                    // 显示Y轴的刻度
                    axisTick: {
                        show: true,
                    },
                },
                {
                    // 显示Y轴的线条
                    axisLine: {
                        show: true,
                    },
                    // 显示Y轴的刻度
                    axisTick: {
                        show: true,
                    },
                }
            ],
            // 散点图
            series: [
                {
                    type: "line",
                    data: [10, 20, 30, 40],
                    yAxisIndex: 0
                },
                {
                    type: "bar",
                    data: [6, 10, 80, 20],
                    yAxisIndex: 1
                }
            ]
        })
    
    </script>
    

    Home首页Card静态组件

    • 页面布局当中有一个静态布局,就是el-row和el-col 其中el-row占的总数是24,要进行均分的话,el-col占6份即可
    • 然后在每一个el-col中放一个el-card即可,因为想要阴影效果,所以使用card
    • 为了更好操纵,我们在card组件下面又定义了4个子组件
      在这里插入图片描述
    • 在子组件中书写相应的样式即可,
    • 其中子组件在进行书写样式的时候,把相应的结构,弄出来,父组件给子组件传递数据的时候使用props进行传递即可
    • 对于每一个子组件中,有的部分需用到插槽的内容,在子组件中占位置,父组件中使用template书写结构
    • 比如Detail子组件
    <template>
      <div>
        <div class="card-header">
          <span>{{ title }}</span>
          <svg
            t="1663466055240"
            class="icon"
            viewBox="0 0 1024 1024"
            version="1.1"
            xmlns="http://www.w3.org/2000/svg"
            p-id="2421"
            width="20"
            height="20"
          >
            <path
              d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
              p-id="2422"
            ></path>
            <path
              d="M512 336m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z"
              p-id="2423"
            ></path>
            <path
              d="M536 448h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z"
              p-id="2424"
            ></path>
          </svg>
        </div>
        <div class="card-content">{{ count }}</div>
        <div class="card-charts">
          <slot name="charts"> </slot>
        </div>
        <div class="card-footer">
          <slot name="footer"> </slot>
        </div>
      </div>
    </template>
    
    • 父组件传递相应的数据,并使用template定义相应的结构
    <el-col :span="6">
      <!-- 想要有阴影的效果,可以外面放上el-card -->
      <el-card>
        <!-- 父组件进行传递,子组件进行接收的时候,不需要动态进行传递 -->
        <Detail title="总销售额" count="¥126560">
          <template slot="charts">
            <span>周同比&nbsp;&nbsp;56.67%</span
            ><svg
              t="1663467150019"
              class="icon"
              viewBox="0 0 1024 1024"
              version="1.1"
              xmlns="http://www.w3.org/2000/svg"
              p-id="3501"
              width="16"
              height="16"
            >
              <path
                d="M786.295467 485.000533l-6.126934 2.082134v-18.295467h6.024534l0.1024 16.213333zM221.5424 485.239467h-8.9088l-0.085333-16.247467 8.994133-0.512v16.759467z"
                fill="#996E28"
                p-id="3502"
              ></path>
              <path
                d="M784.469333 480.290133L504.456533 178.978133a6.826667 6.826667 0 0 0-10.001066 0L214.459733 480.290133a6.826667 6.826667 0 0 0 5.922134 11.4176l158.1056-21.435733L358.4 853.333333v16.162134a6.826667 6.826667 0 0 0 6.826667 7.168h268.356266a6.826667 6.826667 0 0 0 6.826667-7.168l-0.119467-16.3328-19.848533-382.8736 158.122667 21.418666a6.826667 6.826667 0 0 0 5.922133-11.4176z"
                fill="#996E28"
                p-id="3503"
              ></path>
              <path
                d="M191.3344 734.6176l103.031467 72.789333 103.048533-72.789333a127.197867 127.197867 0 0 0-206.097067 0z"
                fill="#D5382E"
                p-id="3504"
              ></path>
              <path
                d="M294.382933 681.984a127.010133 127.010133 0 0 1 103.048534 52.6336l-103.048534 72.789333-103.031466-72.789333a127.010133 127.010133 0 0 1 103.048533-52.6336m0-10.24a137.915733 137.915733 0 0 0-111.342933 56.849067 10.24 10.24 0 0 0 2.269866 14.2848l0.136534 0.1024L288.426667 815.786667a10.24 10.24 0 0 0 11.810133 0l103.031467-72.772267a10.24 10.24 0 0 0 2.4064-14.3872 137.915733 137.915733 0 0 0-111.342934-56.849067z"
                fill="#E8D4AB"
                p-id="3505"
              ></path>
              <path
                d="M384.1536 444.142933h1.5872l-20.48 409.6h268.2368l-20.48-409.6"
                fill="#D5382E"
                p-id="3506"
              ></path>
              <path
                d="M633.4976 860.5696H365.226667a6.826667 6.826667 0 0 1-6.826667-7.168l20.48-409.6 5.239467 0.256v-6.826667h1.5872a6.826667 6.826667 0 0 1 6.826666 7.168l-20.1216 402.432H626.346667l-20.1216-402.432 13.653333-0.682666 20.48 409.6a6.826667 6.826667 0 0 1-6.826667 7.168z"
                fill="#E8D4AB"
                p-id="3507"
              ></path>
              <path
                d="M613.0176 446.276267l166.365867 22.545066L499.387733 167.509333 219.374933 468.821333l164.778667-22.545066"
                fill="#D5382E"
                p-id="3508"
              ></path>
              <path
                d="M633.4976 860.5696H365.226667a6.826667 6.826667 0 0 1-6.826667-7.168l19.968-399.240533-158.122667 21.435733a6.826667 6.826667 0 0 1-5.922133-11.4176L494.370133 162.8672a6.826667 6.826667 0 0 1 10.001067 0L784.384 464.213333a6.826667 6.826667 0 0 1-5.922133 11.4176l-158.122667-21.418666L640.3072 853.333333a6.826667 6.826667 0 0 1-6.826667 7.168z m-261.12-13.653333H626.346667L606.3104 446.634667a6.826667 6.826667 0 0 1 7.7312-7.099734l147.3536 19.950934L499.370667 177.493333 237.3632 459.485867l147.3536-19.968a6.826667 6.826667 0 0 1 7.7312 7.099733z"
                fill="#E8D4AB"
                p-id="3509"
              ></path>
              <path
                d="M557.3632 617.5744l149.5552 105.659733 149.572267-105.659733a184.644267 184.644267 0 0 0-299.1616 0z"
                fill="#D5382E"
                p-id="3510"
              ></path>
              <path
                d="M706.935467 541.184a184.32 184.32 0 0 1 149.572266 76.3904l-149.572266 105.659733-149.5552-105.659733a184.32 184.32 0 0 1 149.572266-76.3904m0-10.24a194.56 194.56 0 0 0-157.866666 80.622933 10.24 10.24 0 0 0 2.184533 14.2336l0.221867 0.1536 149.5552 105.659734a10.24 10.24 0 0 0 11.810133 0l149.5552-105.642667a10.24 10.24 0 0 0 2.4064-14.3872 194.56 194.56 0 0 0-157.866667-80.622933z"
                fill="#E8D4AB"
                p-id="3511"
              ></path>
            </svg>
            &nbsp;&nbsp;
            <span>日同比&nbsp;&nbsp;19.96%</span
            ><svg
              t="1663467232683"
              class="icon"
              viewBox="0 0 1024 1024"
              version="1.1"
              xmlns="http://www.w3.org/2000/svg"
              p-id="4715"
              width="16"
              height="16"
            >
              <path
                d="M252.484267 566.749867h11.776v16.6912h-11.776zM815.684267 566.8352h10.461866v16.9984h-10.461866z"
                fill="#996E28"
                p-id="4716"
              ></path>
              <path
                d="M825.361067 580.642133a6.826667 6.826667 0 0 0-6.980267-3.618133l-158.0032 21.418667L680.2432 201.386667a6.826667 6.826667 0 0 0 0-1.058134v-1.109333a6.826667 6.826667 0 0 0-6.826667-7.168H405.1968a6.826667 6.826667 0 0 0-6.826667 7.168v1.109333a6.826667 6.826667 0 0 0 0 1.058134l19.848534 397.038933-158.0032-21.4016a6.826667 6.826667 0 0 0-5.922134 11.4176l280.0128 301.277867a6.826667 6.826667 0 0 0 10.001067 0L824.32 588.424533a6.826667 6.826667 0 0 0 1.041067-7.7824z"
                fill="#996E28"
                p-id="4717"
              ></path>
              <path
                d="M339.780267 523.7248H103.6288a17.066667 17.066667 0 1 1 0-34.133333h236.151467a13.380267 13.380267 0 1 0 0-26.760534h-33.467734a44.782933 44.782933 0 1 1 0.631467-89.565866h238.250667a17.066667 17.066667 0 0 1 0 34.133333h-238.250667a11.093333 11.093333 0 0 0-11.229867 9.5744 10.717867 10.717867 0 0 0 10.5984 11.7248h33.467734a47.5136 47.5136 0 1 1 0 95.0272z"
                fill="#D5382E"
                p-id="4718"
              ></path>
              <path
                d="M545.194667 373.282133a17.066667 17.066667 0 0 1 0 34.133334h-238.250667a11.093333 11.093333 0 0 0-11.229867 9.5744 10.717867 10.717867 0 0 0 10.5984 11.7248h33.467734a47.5136 47.5136 0 1 1 0 95.0272H103.6288a17.066667 17.066667 0 1 1 0-34.133334h236.151467a13.380267 13.380267 0 1 0 0-26.760533h-33.467734a44.782933 44.782933 0 1 1 0.631467-89.565867h238.250667m0-10.24h-238.250667a55.022933 55.022933 0 1 0-0.631467 110.045867h33.467734a3.140267 3.140267 0 1 1 0 6.280533H103.6288a27.306667 27.306667 0 1 0 0 54.613334h236.151467a57.7536 57.7536 0 1 0 0-115.5072h-33.467734a0.512 0.512 0 0 1-0.290133-0.170667 0.3584 0.3584 0 0 1-0.119467-0.221867 1.416533 1.416533 0 0 1 1.041067-0.426666h238.250667a27.306667 27.306667 0 0 0 0-54.613334z"
                fill="#E8D4AB"
                p-id="4719"
              ></path>
              <path
                d="M654.523733 591.854933h-1.5872l20.48-409.6H405.1968l20.48 409.6"
                fill="#AF3131"
                p-id="4720"
              ></path>
              <path
                d="M654.523733 593.5616h-1.5872l20.48-409.6H405.1968l20.48 409.6"
                fill="#D5382E"
                p-id="4721"
              ></path>
              <path
                d="M654.523733 600.388267h-1.5872a6.826667 6.826667 0 0 1-6.826666-7.168l20.1216-402.432H412.3648l20.1216 402.432-13.653333 0.682666-20.48-409.6a6.826667 6.826667 0 0 1 6.826666-7.168h268.253867a6.826667 6.826667 0 0 1 6.826667 7.168l-20.48 409.6-5.239467-0.256z"
                fill="#E8D4AB"
                p-id="4722"
              ></path>
              <path
                d="M425.6768 589.704533l-166.365867-22.528L539.306667 868.471467l279.995733-301.294934-164.778667 22.528"
                fill="#D5382E"
                p-id="4723"
              ></path>
              <path
                d="M425.6768 591.4112l-166.365867-22.528L539.306667 870.178133l279.995733-301.294933-164.778667 22.528"
                fill="#D5382E"
                p-id="4724"
              ></path>
              <path
                d="M539.306667 874.837333a6.826667 6.826667 0 0 1-5.000534-2.184533L254.293333 571.357867a6.826667 6.826667 0 0 1 5.922134-11.4176l158.122666 21.418666-19.968-399.223466a6.826667 6.826667 0 0 1 6.826667-7.168h268.2368a6.826667 6.826667 0 0 1 6.826667 7.168l-19.968 399.240533 158.122666-21.435733a6.826667 6.826667 0 0 1 5.922134 11.4176L544.3072 872.6528a6.826667 6.826667 0 0 1-5.000533 2.184533zM277.2992 576.034133L539.306667 857.975467l262.007466-281.941334-147.3536 19.968a6.826667 6.826667 0 0 1-7.7312-7.099733l20.0192-400.2816H412.3648l20.0192 400.264533a6.826667 6.826667 0 0 1-7.7312 7.099734z"
                fill="#E8D4AB"
                p-id="4725"
              ></path>
              <path
                d="M780.7488 226.338133h105.540267a17.066667 17.066667 0 0 1 0 34.133334h-105.540267a13.380267 13.380267 0 1 0 0 26.760533h8.0384a44.782933 44.782933 0 1 1 0.631467 89.565867H503.7056a17.066667 17.066667 0 0 1 0-34.133334h285.730133a10.717867 10.717867 0 0 0 10.5984-11.7248 11.076267 11.076267 0 0 0-11.229866-9.5744h-8.0384a47.5136 47.5136 0 1 1 0-95.0272z"
                fill="#D5382E"
                p-id="4726"
              ></path>
              <path
                d="M886.289067 226.338133a17.066667 17.066667 0 0 1 0 34.133334h-105.540267a13.380267 13.380267 0 1 0 0 26.760533h8.0384a44.782933 44.782933 0 1 1 0.631467 89.565867H503.7056a17.066667 17.066667 0 0 1 0-34.133334h285.730133a10.717867 10.717867 0 0 0 10.5984-11.7248 11.076267 11.076267 0 0 0-11.229866-9.5744h-8.0384a47.5136 47.5136 0 1 1 0-95.0272h105.540266m0-10.24h-105.557333a57.7536 57.7536 0 1 0 0 115.5072h8.0384a1.416533 1.416533 0 0 1 1.041067 0.426667 0.341333 0.341333 0 0 1-0.119467 0.221867 0.802133 0.802133 0 0 1-0.221867 0.170666H503.7056a27.306667 27.306667 0 0 0 0 54.613334h285.730133a55.022933 55.022933 0 1 0-0.631466-110.045867h-8.0384a3.140267 3.140267 0 1 1 0-6.280533h105.540266a27.306667 27.306667 0 0 0 0-54.613334z"
                fill="#E8D4AB"
                p-id="4727"
              ></path>
            </svg>
          </template>
          <template slot="footer">
            <span>日销售额¥12423</span>
          </template>
        </Detail>
      </el-card>
    </el-col>
    

    折线图的绘制

    • 同样是定义出来一个子组件然后在父组件中相应的位置放置即可
    
    
    
    
    
    

    柱状图和进度条完成

  • 相关阅读:
    13 套接字Socket
    网络安全自学手册
    IP行业查询API:为用户分析提供帮助
    flask-sqlalchemy连接数据库
    二叉树进阶——手撕二叉搜索树
    使用nacos实现简单的动态化线程池
    hardhat 教程及 hardhat-deploy 插件使用
    18.备忘录模式(Memento)
    基于SpringBoot+Vue的家具销售电商平台设计与实现
    中值滤波算法及例程
  • 原文地址:https://blog.csdn.net/A13526_/article/details/126520175