【Vue.js + Element UI】实现商品管理

1.Tab卡添加商品

在这里插入图片描述

实现整体布局

/src/pages/goods/Index.vue


<template>
  <div>
   <el-button size="small" type="primary" plain @click = "showAddForm">添加</el-button>
   <u-list @edit = "showEditForm"/>
   <u-form ref = "form"/>
  </div>
</template>

<script>
import UList from './components/List'
import UForm from './components/Form'
export default {
  components: {
    UList,
    UForm
  },
  methods: {
    showAddForm () {
      // 显示对话框
      this.$refs.form.dialogFormVisible = true
      // 修改对话框的标题
      this.$refs.form.title = '添加商品'
    },
    showEditForm (data) {
      // console.log(data)
      // 显示菜单对话框
      this.$refs.form.dialogFormVisible = true
      // 修改对话框的标题
      this.$refs.form.title = '修改商品'
      // 把要编辑的菜单的数据复制给对话框
      this.$refs.form.setFormData(data)
    }
  }
}
</script>

<style scoped>

</style>

实现Tab卡布局

/src/pages/goods/components/Form.vue

<template>
  <!-- el-dialog: 弹出对话框组件
       title: 对话框标题
       visible: 是否显示
   -->
  <el-dialog
    :title="title"
    :visible.sync="dialogFormVisible"
    @close="clearForm"
    width="80%"
  >
    <el-form
      :model="form"
      ref="form"
      label-suffix=""
      label-width="120px"
    >
    <el-tabs v-model="activeName">
      <el-tab-pane label="基本信息" name="baseInfo">
        <el-form-item label="一级分类">
          <el-select v-model = "form.first_cateid" placeholder="请选择一级分类"
          @change="onChangeCategory">
            <el-option label="请选择一级分类" :value="0"></el-option>
              <!-- 循环一级分类 -->
            <el-option
            v-for="item of firstCategoryList"
            :key = "item.id"
            :label ="' |- ' + item.catename"
            :value = "item.id"
            >
            </el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="二级分类">
          <el-select v-model = "form.second_cateid" placeholder="请选择二级分类">
            <el-option label="请选择二级分类" :value="0"></el-option>
              <!-- 循环一级分类 -->
            <el-option
            v-for="item of secondCategoryList"
            :key = "item.id"
            :label ="item.catename"
            :value = "item.id"
            >
            </el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="商品名称" prop="goodsname">
           <el-input v-model.trim="form.goodsname" autocomplete="off" placeholder="请输入商品名称"></el-input>
        </el-form-item>
        <el-form-item label="价格" prop="price">
           <el-input v-model.trim="form.price" autocomplete="off" placeholder="请输入价格"></el-input>
        </el-form-item>
        <el-form-item label="市场价格" prop="market_price">
           <el-input v-model.trim="form.market_price" autocomplete="off" placeholder="请输入市场价格"></el-input>
        </el-form-item>
         <el-form-item label="商品规格">
          <el-select v-model = "form.specsid" placeholder="请选择商品规格" @change="onChangeSpecs">
            <el-option label="请选择商品规格" :value="0"></el-option>
              <!-- 循环一级分类 -->
            <el-option
            v-for="item of specsList"
            :key = "item.id"
            :label ="item.specsname"
            :value = "item.id"
            >
            </el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="规格属性">
          <el-select v-model = "form.specsattr" placeholder="请选择规格属性">
            <el-option label="请选择规格属性" value=""></el-option>
              <!-- 循环一级分类 -->
            <el-option v-for="item of attrList" :key = "item" :label ="item"
            :value = "item">
            </el-option>
          </el-select>
        </el-form-item>
      </el-tab-pane>
      <el-tab-pane label="图片信息" name="imageInfo">
        <el-form-item label="图片">
        <!-- 上传图片
            list-type:文件列表的类型 text/picture/picture-card
            auto-upload: 是否自动上传
            limit 允许上传的文件数量
            on-change 文件状态改变时的钩子, 添加文件、上传成功和上传失败时都会被调用
        -->
        <el-upload
          :class = "{upload: hideUploadBtn}"
          action="#"
          list-type="picture-card"
          :limit="imgLimit"
          :file-list="fileList"
          accept="image/png, image/gif, image/jpeg"
          :on-change="uploadImg"
          :auto-upload="false">
          <i slot="default" class="el-icon-plus"></i>
          <div slot="file" slot-scope="{ file }">
            <!-- 预览的小图 -->
            <img
              class="el-upload-list__item-thumbnail"
              :src="file.url"
              alt=""
            />
            <!-- 预览大图片的按钮 -->
            <span class="el-upload-list__item-actions">
              <span
                class="el-upload-list__item-preview"
                @click="handlePictureCardPreview(file)"
              >
                <i class="el-icon-zoom-in"></i>
              </span>
              <!-- 删除图片的按钮 -->
              <span
                class="el-upload-list__item-delete"
                @click="handleRemove(file)"
              >
                <i class="el-icon-delete"></i>
              </span>
            </span>
          </div>
        </el-upload>
        <!-- 展示大图片的对话框
             append-to-body: 是否挂载在body上
        -->
        <el-dialog :visible.sync="dialogVisible" :append-to-body = "true">
          <img width="100%" :src="dialogImageUrl" alt="" />
        </el-dialog>
      </el-form-item>
       <!-- 修改的时候且有图片时显示分类图片-->
      <el-form-item label="商品图片" v-if="form.id > 0 && editDefaultImg != ''">
        <el-image style="width:148px; height: 148px" fit = "fill" :src = "editDefaultImg | recombinationImg" />
      </el-form-item>
      </el-tab-pane>
      <el-tab-pane label="促销信息" name="promoteInfo">
        <el-form-item label="新品">
          <el-switch v-model="form.isnew" :active-value="1" :inactive-value="2">
          </el-switch>
        </el-form-item>
        <el-form-item label="热卖">
          <el-switch v-model="form.ishot" :active-value="1" :inactive-value="2">
          </el-switch>
        </el-form-item>
        <el-form-item label="状态">
          <el-switch v-model="form.status" :active-value="1" :inactive-value="2">
          </el-switch>
        </el-form-item>
      </el-tab-pane>
      <el-tab-pane label="详细信息" name="detailInfo">
        <!-- 富文本
        id: 创建编辑器的唯一标识(必填) -->
        <vue-wangeditor
        id="editor"
        ref = "editor"
        :disabledMenus = "['location','insertcode','video']"
        width = "100%"
        height = "340"
        :value = "form.description"
        >
        </vue-wangeditor>
      </el-tab-pane>
    </el-tabs>
     <el-form-item>
        <el-button @click="dialogFormVisible = false">取 消</el-button>
        <el-button type="primary" @click="onSubmit">确 定</el-button>
     </el-form-item>
    </el-form>
  </el-dialog>
</template>

<script>
// 引入富文本编辑器插件
import vueWangeditor from 'vue-wangeditor'
// 引入接口方法
import { mapGetters, mapState } from 'vuex'
// 导入规则
import goodsRules from '@/validate/goods'
// 导入公共方法
import { validate } from '@/utils/function'
// 导出所有的非default内容
import * as model from '@/api/goods'
const defaultForm = {
  first_cateid: 0, // 一级分类ID(required)
  second_cateid: 0, // 二级分类ID(required)
  goodsname: '', // 商品名称(required)
  price: '', // 商品价格(required)
  market_price: '', // 市场价格(required)
  img: '', // 商品图片(required)
  description: '', // 商品详情(required)
  specsid: 0, // 规格ID(required)
  specsattr: '', // 规格属性(required)
  isnew: 1, // 是否新品 1是 2不是
  ishot: 1, // 是否热卖推荐 1是 2不是
  status: 1 // 状态 1启用 2禁用
}
export default {
  components: {
    vueWangeditor
  },
  data () {
    return {
      dialogFormVisible: false,
      title: '', // 对话框的标题
      activeName: 'baseInfo', // 点击的tab选项卡
      form: { ...defaultForm },
      secondCategoryList: [], // 二级分类数据
      attrList: [], // 属性列表
      imgLimit: 1,
      fileList: [], // 选择的文件列表
      dialogVisible: false, // 是否展示大图片
      dialogImageUrl: '', //  大图片的地址
      hideUploadBtn: false, // 是否隐藏选择图片的按钮
      editDefaultImg: '' // 存储修改时传入的图片
    }
  },
  computed: {
    ...mapState({
      // 获取分类列表
      categoryList: state => state.category.list,
      // 获取商品规格
      specsList: state => state.specs.allList
    }),
    ...mapGetters({
      // 获取一级分类
      firstCategoryList: 'category/firstCategoryList'
    })
  },
  mounted () {
    // 如果没有一级分类信息, 则重新获取信息
    if (this.categoryList.length === 0) {
      this.$store.dispatch('category/getCategoryList')
    }
    this.$store.dispatch('specs/getAllSpecs')
  },
  methods: {
    // 切换一级分类时触发
    onChangeCategory (id) {
      // 清空表单的second_cateid
      this.form.second_cateid = 0
      // console.log(id, this.categoryList)
      // 如果id为0则清空二级分类
      // 如果id>0则获取当前分类的二级分类
      if (id > 0) {
        const category = this.categoryList.find(item => item.id === id)
        this.secondCategoryList = category.children || []
      } else {
        this.secondCategoryList = []
      }
    },
    // 切换规格属性时触发
    onChangeSpecs (id) {
      // 清空表单的规格属性数据
      this.form.specsattr = ''
      // console.log(id, this.categoryList)
      // 如果id为0则规格属性数据
      // 如果id>0则获取当前商品规格规格属性数据
      if (id > 0) {
        const specs = this.specsList.find(item => item.id === id)
        this.attrList = specs.attrs || []
      } else {
        this.attrList = []
      }
    },
    // 上传图片
    uploadImg (file, fileList) {
      // console.log(file, fileList)
      // 对大小和类型进行限制
      const allowType = ['image/png', 'image/gif', 'image/jpeg']
      if (!allowType.includes(file.raw.type)) {
        // 选择了不允许的类型
        this.$message.error('不是正确的图片格式')
        // 移除当前选择的文件, 即过滤出不是当前的图片地址的文件
        this.fileList = this.fileList.filter(item => item.url !== file.url)
        return
      }
      const allowMaxSize = 1024 * 1024
      if (file.size > allowMaxSize) {
        // 文件超过允许的大小
        this.$message.error('文件超过允许的大小')
        // 移除当前选择的文件, 即过滤出不是当前的图片地址的文件
        this.fileList = this.fileList.filter(item => item.url !== file.url)
        return
      }
      // 当选择的文件的列表等于允许的最大数量时
      // 隐藏选择图片的按钮
      if (fileList.length === this.imgLimit) {
        this.hideUploadBtn = true
      }
      this.fileList = fileList
      // 把文件的资源保存到表单数据中
      this.form.img = file.raw
    },
    // 图片预览(展示大图)
    handlePictureCardPreview (file) {
      // console.log(file)
      // 把file的链接赋值给大图片的src
      this.dialogImageUrl = file.url
      // 显示大图的对话框
      this.dialogVisible = true
    },
    handleRemove (file) {
      // console.log(file, this.fileList)
      // this.fileList = []
      // 从filelist中删除选择的图片
      this.fileList = this.fileList.filter(item => item.url !== file.url)
      // 显示选择图片的按钮
      this.hideUploadBtn = false
      // 如果是添加清空表单的图片数据, 如果是修改要还原修改之前的数据
      this.form.img = this.editDefaultImg
    },
    onSubmit () {
      // 获取富文本编辑器中的数据
      // console.log(this.$refs.editor.getHtml())
      // 把富文本中的纯文本添加到this.form中
      this.form.description = this.$refs.editor.getText()
      const valid = validate(this.form, goodsRules)
      if (valid !== true) {
        this.$message.error(valid)
        return
      }
      // 验证通过之后才处理数据
      // 根据form数据中是否有id属性来判断当前是修改菜单还是添加菜单
      if (this.form.id && this.form.id > 0) {
        // 修改
        this.editGoods('updateGoods')
      } else {
        // 添加
        this.editGoods()
      }
    },
    editGoods (method = 'addGoods') {
      // 如果是修改,删除数据中多余的属性 secondcatename,firstcatename
      if (this.form.id > 0) {
        Reflect.deleteProperty(this.form, 'secondcatename') // delete this.form.secondcatename
        Reflect.deleteProperty(this.form, 'firstcatename')
      }
      // 把富文本中的内容(包含html)添加到this.form中
      this.form.description = this.$refs.editor.getHtml()
      // 有文件的上传, 必须使用FormData()
      const data = new FormData()
      for (const k in this.form) {
        data.append(k, this.form[k])
      }
      // 处理菜单的添加,把表单的数据提交给接口
      model[method](data)
        .then(() => {
          // 添加成功
          // 显示添加成功的信息
          this.$message.success({
            message: method === 'addGoods' ? '添加成功' : '修改成功',
            // 关闭对话框
            onClose: () => {
              this.dialogFormVisible = false
            }
          })
          // 添加时重新获取总数量
          if (method === 'addGoods') {
            this.$store.dispatch('goods/getGoodsTotal')
          }
          // 刷新列表数据
          this.$store.dispatch('goods/getGoodsList')
        })
        .catch((err) => {
          this.$message.error(err.message)
        })
    },
    clearForm () {
      // 把表单数据还原到初始值
      this.form = { ...defaultForm }
      // 清空富文本编辑器数据
      // this.$refs.editor.clear()
      // 还原选项卡
      this.activeName = 'baseInfo'
      // 还原上传组件的数据
      this.hideUploadBtn = false
      this.editDefaultImg = ''
      this.fileList = []
    },
    // 修改时设置表单数据
    setFormData (data) {
      // 根据商品的一级分类设置二级分类的数据
      const category = this.categoryList.find(item => item.id === data.first_cateid)
      this.secondCategoryList = category.children || []
      // 保存商品的图片
      this.editDefaultImg = data.img
      this.form = {...data} // 复制一份数据
    }
  }
}
</script>

<style scoped>
.upload /deep/ .el-upload {
  display:none !important
}
/deep/ .wangEditor-txt{
  height: 280px !important
}
</style>

验证表单数据

validate/goods.js

export default {
  first_cateid (value) {
    value = parseInt(value)
    if (value === 0) {
      return '请选择一级分类'
    }
    return true
  },
  second_cateid (value) {
    value = parseInt(value)
    if (value === 0) {
      return '请选择二级分类'
    }
    return true
  },
  goodsname (value) {
    return value === '' ? '请输入商品名称' : true
  },
  price (value) {
    if (!value) return '请输入价格'
    if (isNaN(value)) return '价格必须为数字'
    if (value < 0) return '价格必须为正数'
    return true
  },
  market_price (value) {
    if (!value) return '请输入市场价格'
    if (isNaN(value)) return '市场价格必须为数字'
    if (value < 0) return '市场价格必须为正数'
    return true
  },
  specsid (value) {
    value = parseInt(value)
    if (value === 0) {
      return '请选择商品规格'
    }
    return true
  },
  specsattr (value) {
    return value === '' ? '请选择规格属性' : true
  },
  img (value) {
    if (!value) return '请上传商品图片'
    return true
  },
  description (value) {
    return value === '' ? '请输入商品详情' : true
  }
}
添加接口文件

/src/api/goods.js

import http from './http'

// 添加商品
export const addGoods = (data) => {
  // 有数据上传, 需要设置headers
  return http.post('/goodsadd', data, {
    headers: {
      'Content-type': 'multipart/form-data'
    }
  })
}

// 修改规格
export const updateGoods = (data) => {
// 判断data中是否包含id属性且大于0
  if (!data.get('id')) {
    return Promise.reject(new Error('缺少ID参数或id参数错误'))
  }
  // 有数据上传, 需要设置headers
  return http.post('/goodsedit', data, {
    headers: {
      'Content-type': 'multipart/form-data'
    }
  })
}

// 分页获取商品数据
export const getPageGoods = (page = 1, size = 4) => {
  return http.get('/goodslist', {
    params: {
      page,
      size
    }
  })
}

// 获取商品总数量
export const getGoodsTotal = () => {
  return http.get('/goodscount')
}

// 删除商品
export const deleteGoods = (id) => {
  return http.post('/goodsdelete', { id })
}

vuex中处理商品数据
// 导入接口文件
import { getPageGoods, getGoodsTotal } from '@/api/goods'

export default {
  namespaced: true,
  state: {
    allList: [], // 所有的规格
    list: [],
    page: 1, // 当前的页码
    size: 4, // 每页显示的条数
    total: 0 // 管理员用户总数
  },
  mutations: {
    SET_LIST (state, list) {
      state.list = list
    },
    SET_TOTAL (state, total) {
      state.total = total
    },
    SET_PAGE (state, page) {
      state.page = page
    },
    SET_ALLLIST (state, list) {
      state.allList = list
    }
  },
  actions: {
    getGoodsList ({commit, state}) {
      getPageGoods(state.page, state.size).then(res => {
        // console.log(res)
        commit('SET_LIST', res)
      })
    },
    getGoodsTotal ({commit}) {
      getGoodsTotal().then(res => {
        // console.log(res)
        commit('SET_TOTAL', res[0].total || 0)
      })
    },
    // 获取所有商品
    async getAllGoods ({commit}) {
      const total = await getGoodsTotal().then(res => res[0].total || 0)
      // console.log(total)
      if (total > 0) {
        const list = await getPageGoods(1, total)
        // console.log(list)
        commit('SET_ALLLIST', list)
      }
    }
  }
}

展示数据

在这里插入图片描述
/src/pages/goods/components/list.vue

<template>
<div>
  <el-table
    :data = "list"
    style="width: 100%; margin: 20px 0;"
    row-key="id"
    border>
    <!-- el-table-column:表格的列组件
        prop: 菜单数据中对应的数据
        label:表头
         -->
    <el-table-column
      prop="goodsname"
      label="商品名称">
    </el-table-column>
     <el-table-column
      prop="price"
      label="商品价格">
    </el-table-column>
    <el-table-column
      prop="market_price"
      label="市场价格">
    </el-table-column>
    <el-table-column
      label="图片">
    <template #default = "props">
      <img v-if = "props.row.img !== ''" :src="props.row.img | recombinationImg" width="80" height="80">
    </template>
    </el-table-column>
    <el-table-column
      label="是否新品">
    <template #default = "props">
      <el-tag v-if = "props.row.isnew === 1">是</el-tag>
      <el-tag v-else type="danger"></el-tag>
    </template>
    </el-table-column>
     <el-table-column
      label="是否热卖">
    <template #default = "props">
      <el-tag v-if = "props.row.ishot === 1">是</el-tag>
      <el-tag v-else type="danger"></el-tag>
    </template>
    </el-table-column>
    <el-table-column
      label="状态">
    <template #default = "props">
      <el-tag v-if = "props.row.status === 1">启用</el-tag>
      <el-tag v-else type="danger">禁用</el-tag>
    </template>
    </el-table-column>
    <el-table-column
      label="操作">
    <template #default = "props">
       <el-button type="primary" size = "mini" @click="onEdit(props.row)"><i class="el-icon-edit"></i> 编辑</el-button><br>
       <el-button type="danger" size = "mini" @click="onDelete(props.row)" style="margin-top:10px"><i class="el-icon-delete"></i> 删除</el-button>
    </template>
    </el-table-column>
  </el-table>
  <!-- 分页组件
    layout 组件布局
    total: 总数量
    page-size: 每页显示的数量
    current-change: 当前页码发生改变时触发
    至少有2页时才显示分页组件
    -->
   <el-pagination
   @current-change = "onCurrentChange"
   class="page"
   background
   layout="prev, pager, next"
   :page-size="size"
   :total="total"
   v-if = "total > size"
   >
   </el-pagination>
</div>
</template>

<script>
import { mapState } from 'vuex'
import { deleteGoods } from '@/api/goods'
export default {
  computed: {
    ...mapState({
      list: state => state.goods.list,
      size: state => state.goods.size,
      page: state => state.goods.page,
      total: state => state.goods.total
    })
  },
  mounted () {
    this.$store.dispatch('goods/getGoodsList')
    this.$store.dispatch('goods/getGoodsTotal')
  },
  methods: {
    onCurrentChange (page) {
      // console.log(page)
      // 更改vuex中的当前页
      this.$store.commit('goods/SET_PAGE', page)
      // 重新获取当前页的商品数据
      this.$store.dispatch('goods/getGoodsList')
    },
    onEdit (data) {
      // 触发编辑按钮
      // 通知父组件显示编辑菜单的对话框, 把当前编辑的数据传递过去
      this.$emit('edit', data)
    },
    // id为1的管理员不允许删除
    onDelete (data) {
      // element-ui的弹出框this.$comfirm(显示的信息[,标题,其他的配置项目])
      this.$confirm('确定要删除吗?', '提示', { type: 'error' }).then(() => {
        // 完成删除功能
        // 调用接口删除角色
        // console.log('删除')
        deleteGoods(data.id).then(() => {
          // 刷新列表数据
          this.$message.success({
            message: '删除成功',
            onClose: () => {
              // 关闭对话框
              this.dialogFormVisible = false
              // 重新获取总数量
              this.$store.dispatch('specs/getSpecsTotal')
              // 如果当前页的数据已经全部删除, 就修改page
              if (this.list.length === 1) {
                let page = 0
                if (this.page === 1) {
                  page = 1
                } else {
                  page = this.page - 1
                }
                this.$store.commit('goods/SET_PAGE', page)
              }
              // 刷新列表数据
              this.$store.dispatch('goods/getGoodsList')
            }
          })
        }).catch(err => {
          this.$message.error(err.message)
        })
        // 调用接口删除菜单
        // 刷新列表数据
      }).catch(() => {})
    }
  }
}
</script>

<style>
.page {
  float: right;
}
</style>

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值