一.概述
将之前的一个项目用spring boot+vue+redis进行了改进.从2023年7月10日开始逐步在CSND博客上逐步分享.如果你查阅的时间距离现在比较久远请注意技术的更迭,
如果想了解前面的内容,你可以点击下面这个链接:
springboot+vue+redis 番茄鲜生电商平台实例一:登录功能
springboot+vue+redis 番茄鲜生电商平台实例二:后台菜单
springboot+vue+redis 番茄鲜生电商平台实例三:路由
springboot+vue+redis 番茄鲜生电商平台实例四:数据显示和搜索
springboot+vue+redis 番茄鲜生电商平台实例五:商品分类的增删改查
二.功能介绍
1.使用computed计算属性实现一级分类和二级分类下拉列表的联动.
2.实现图片上传到服务端.
3.在表格中显示图片
4.完成商品品类新增功能.
三.效果展示
四.代码
goods.vue代码
<template>
<div style="margin-bottom: 15px;" >
<!-- 显示页头 -->
<a-page-header :style="{ background: 'var(--color-bg-2)' }" title="商品品种" :show-back="false">
<template #breadcrumb>
<a-breadcrumb>
<a-breadcrumb-item>商品管理</a-breadcrumb-item>
<a-breadcrumb-item>商品品种</a-breadcrumb-item>
</a-breadcrumb>
</template>
</a-page-header>
</div>
<a-grid cols="2" col-gap="20">
<a-grid-item class="item_box">
<a-form size="medium" :model="goods" @submit="addGoods">
<a-space :size="18" direction="vertical">
<a-form-item label="一级分类">
<a-select v-model="parentId"
:rules="[{min:'1',message:'必须选择一级分类'}]"
:validate-trigger="['change']">
<a-option :value="-1">请选择分类</a-option>
<a-option v-for="item in parentCategory" :key="item.id" :value="item.id">
{{ item.categoryName }}
</a-option>
</a-select>
</a-form-item>
<a-form-item label="二级分类" field="goodsCategoryId">
<a-select v-model="goods.goodsCategoryId"
:rules="[{min:'1',message:'必须选择二级分类'}]"
:validate-trigger="['change']">
<a-option :value="0">请选择分类</a-option>
<a-option v-for="item in childCategory" :key="item.id" :value="item.id">
{{ item.categoryName }}
</a-option>
</a-select>
</a-form-item>
<a-form-item field="goodsName" label="商品名"
:rules="[{required:true,message:'商品名不能为空'}]"
:validate-trigger="['change','input']">
<a-input v-model="goods.goodsName" placeholder="请输入商品名" />
</a-form-item>
<a-form-item label="排序" field="rank">
<a-input-number v-model="goods.rank" placeholder="排序">
</a-input-number>
</a-form-item>
<a-form-item>
<a-upload action="http://localhost:8011/upload/uploadGoodsImg"
:fileList="file ? [file] : []" @success="onSuccess" @error ="onError"
:show-file-list="false" @change="onChange" @progress="onProgress">
<template #upload-button>
<div :class="`arco-upload-list-item${file && file.status === 'error' ? ' arco-upload-list-item-error' : ''}`">
<div class="arco-upload-list-picture custom-upload-avatar" v-if="file && file.url">
<img :src="file.url" />
<div class="arco-upload-list-picture-mask"><IconEdit /></div>
<a-progress
v-if="file.status === 'uploading' && file.percent < 100"
:percent="file.percent" type="circle" size="mini"
:style="{position: 'absolute',left: '50%',top: '50%',
transform: 'translateX(-50%) translateY(-50%)',}"/>
</div>
<div class="arco-upload-picture-card" v-else>
<div class="arco-upload-picture-card-text">
<IconPlus />
<div style="margin-top: 10px; font-weight: 600">Upload</div>
</div>
</div>
</div>
</template>
</a-upload>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" html-type="submit">
<template #icon><icon-plus /></template>新增
</a-button>
<a-alert v-if="isShow" :type="msgType">{{msg}}</a-alert>
</a-space>
</a-form-item>
</a-space>
</a-form>
</a-grid-item>
<a-grid-item>
<a-table :columns="columns" :data="goodsList.data">
<template #img="{ record }">
<a-image width="50" :src="'http://localhost:8011/goodsImg/'+record.img"></a-image>
</template>
<template #goodsCategoryId="{ record }">
{{ getCategory(record.goodsCategoryId) }}
</template>
<template #optional="{ record }">
<a-button @click="updateGoods(record)">更新</a-button>
</template>
</a-table>
</a-grid-item>
</a-grid>
</template>
<script src="../../../js/goods.js"></script>
<style scoped>
.item_box{
background-color: #fff;
padding:20px;
}
</style>
goods.js代码
import axios from 'axios';
import { forEach } from 'lodash-es';
import { computed, onMounted, reactive,ref } from 'vue';
export default{
setup(){
//添加的商品品类对象
const goods = reactive({
id:0,
goodsName:'',
img:'',
goodsCategoryId:0,
rank:0,
});
const goodsList = reactive({data:[]});//商品品类列表
const goodsCategory = reactive({data:[]});//商品分类
const parentCategory = computed(()=>{//计算商品分类中父id为0的一级分类
return goodsCategory.data.filter((category)=>{
return category.parentId==0;
});
});
const parentId = ref(-1);//选择的一级分类id
const childCategory = computed(()=>{//根据一级分类计算二级分类
goods.goodsCategoryId=0;
return goodsCategory.data.filter((category)=>{
return category.parentId==parentId.value;
})
});
onMounted(()=>{//页面加载时,获取必要的数据
//获取分类的数组
axios.get("http://localhost:8011/fresh_mall/goodsCategory/getAll")
.then((res)=>{
goodsCategory.data = res.data.data;
})
//显示所有商品品类
axios.get("http://localhost:8011/fresh_mall/goods/getAll")
.then((res)=>{
goodsList.data =res.data.data;
})
})
//上传商品图片
const file = ref();
const onChange = (_, currentFile) => {
file.value = {
//列举currentFile中的每一项
...currentFile,
url: URL.createObjectURL(currentFile.file),
};
};
const onProgress = (currentFile) => {
file.value = currentFile;
};
const onSuccess = (res)=>{//图片上传成功后
// console.log(res);
goods.img = res.response.data;//记录上传的图片路径
}
//将对象进行格式化,这样才能将对象传递给服务器.
const getformData=(obj)=>{
let formData = new FormData();
for(var key in obj){
formData.append(key,obj[key]);
}
return formData;
}
const msg = ref('');//添加商品品类后显示的信息
const isShow = ref(false);//是否显示提示信息.
const msgType = ref('success');//信息提示类型
//将商品品类数据保存到数据库
const addGoods = ()=>{
if(goods.img==''){
msg.value='必须选择商品图片';
isShow.value=true;
msgType.value='warning';
return;
}
let formData = getformData(goods);
axios.post("http://localhost:8011/fresh_mall/goods/add",formData)
.then((res)=>{
console.log(res.data);
if(res.data.resultCode==6000){//添加成功
//清空表单
goods.id=0;
goods.goodsCategoryId=0;
parentId.value = -1;
goods.goodsName='';
goods.img='';
file.url='';
goods.rank=0;
msg.value="数据添加成功";
isShow.value=true;
msgType.value='success';
}
})
}
//显示右侧商品品类列表
const columns = [{title: '图片',slotName: 'img'}
,{title: '品类名称',slotName: 'goodsName',dataIndex:'goodsName'}
,{title: '分类',slotName: 'goodsCategoryId',dataIndex:'goodsCategoryId'}
,{title: '排序',slotName: 'rank',dataIndex:'rank'}
,{title: '操作',slotName: 'optional'}];
//计算获取分类的名称
const getCategory = (id)=>{
for(var i=0;i<goodsCategory.data.length;i++){
if(goodsCategory.data[i].id==id){
return goodsCategory.data[i].categoryName;
}
}
};
//更新
const updateGoods = (record)=>{
goods.id = record.id;
goods.goodsName = ref(record.goodsName);
goods.img = record.img;
goods.rank = record.rank;
goods.goodsCategoryId = record.goodsCategoryId;
}
return{
goods,
file,
addGoods,
parentCategory,
parentId,
childCategory,
goodsList,
msg,
isShow,
onChange,
onProgress,
onSuccess,
columns,
getCategory,
updateGoods,
}
}
}
服务端代码:略
五.关键代码解释
1. 在上面代码中,使用onMounted(()=>{}) 使用axios从服务器端获取数据.相当于是页面初始化的时候获取数据.
2.两个下拉列表的数据要经过计算之后,再绑定到列表中,所以和使用的是computed计算属性,这里需要注意的是:
1)使用计算属性时,不要有附加操作,仅仅制作计算并返回计算后的结果,包括console.log也算是附加操作.
2)在使用ref定义的常量时,需要使用.value属性才能正确的获取值或者复制.
3)computed计算属性其标答的含义是,在计算属性中的关联值发生变化时,计算属性将重新进行计算,如果没有变化,那么就读取第一次计算的缓存值.但是这里所谓的关联值必须还是常量(由const定义),而不能是由let定义的变量.
3.如果将对象传递到服务器,建议使用axios.post方法,但是在传递对象时需要先格式化.也就是以下面这段代码:
//将对象进行格式化,这样才能将对象传递给服务器.
const getformData=(obj)=>{
let formData = new FormData();
for(var key in obj){
formData.append(key,obj[key]);
}
return formData;
}
4.在table组件显示数据时,需要制定列也就是colums属性,在指定colums时,需要配置相关的属性,例如:
const columns = [{title: '图片',slotName: 'img'}
,{title: '品类名称',slotName: 'goodsName',dataIndex:'goodsName'}
,{title: '分类',slotName: 'goodsCategoryId',dataIndex:'goodsCategoryId'}
,{title: '排序',slotName: 'rank',dataIndex:'rank'}
,{title: '操作',slotName: 'optional'}];
其中title:列头显示的标题,slotName:相当于列的名称,用于在组件中,进行匹配,dataIndex:这个列需要显示的数组中对象的属性值.
5.在vue中
<template #optional="{ record }">
<a-button @click="updateGoods(record)">更新</a-button>
</template>
可以看到有#optional="{record}",其中optional就是在colums指定的slotName,
record就是绑定数据的每一行记录.