Springboot3 + MyBatis-Plus + MySql + Uniapp 商品加入购物车功能实现(针对上一篇sku)
- 1、效果展示
- 2、数据库设计
- 3、后端源码
-
- [3.1 application.yml 方便 AliOssUtil.java 读取](#3.1 application.yml 方便 AliOssUtil.java 读取)
- [3.2 model 层](#3.2 model 层)
- [3.2.1 BaseEntity](#3.2.1 BaseEntity)
- [3.2.1 GoodsType](#3.2.1 GoodsType)
- [3.2.3 GoodsTypeSonVo](#3.2.3 GoodsTypeSonVo)
- [3.3 Controller 层](#3.3 Controller 层)
-
- [3.3.1 FileUploadController.java 后端上传图片到阿里云OSS](#3.3.1 FileUploadController.java 后端上传图片到阿里云OSS)
- [3.3.2 AliOssUtil.java 后端上传图片到阿里云OSS的工具类](#3.3.2 AliOssUtil.java 后端上传图片到阿里云OSS的工具类)
- [3.3.3 GoodsTypeController.java 商品分类接口](#3.3.3 GoodsTypeController.java 商品分类接口)
- [3.4 GoodsTypeService 层](#3.4 GoodsTypeService 层)
- [3.5 GoodsTypeServiceImpl 层](#3.5 GoodsTypeServiceImpl 层)
- 4、前端代码
-
- [4.1 type.Vue](#4.1 type.Vue)
- [4.2 type.Vue](#4.2 type.Vue)
- [4.3 addOrEditType.Vue](#4.3 addOrEditType.Vue)
- [4.4 supportTypeDialog.Vue](#4.4 supportTypeDialog.Vue)
1、效果展示
QQ2024930-15058
2、数据库设计
sql
/*
Navicat Premium Data Transfer
Source Server : Test1
Source Server Type : MySQL
Source Server Version : 80200
Source Host : localhost:3306
Source Schema : yunshangshequ
Target Server Type : MySQL
Target Server Version : 80200
File Encoding : 65001
Date: 30/09/2024 15:08:28
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for goods_type
-- ----------------------------
DROP TABLE IF EXISTS `goods_type`;
CREATE TABLE `goods_type` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分类名称',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分类描述',
`img` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分类图标',
`father_id` int NULL DEFAULT NULL COMMENT '如果没有 father_id 则是一级分类',
`status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '是否启用 1 启用 0禁用',
`type_sort` int NULL DEFAULT NULL COMMENT '排序',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`is_deleted` tinyint NULL DEFAULT NULL COMMENT '逻辑删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of goods_type
-- ----------------------------
INSERT INTO `goods_type` VALUES (1, '水果', '所有水果哈哈哈', 'https://yunshangshequ.cn-chengdu.oss.aliyuncs.com/2024.09.30/b648e907-12e1-406d-89cd-d4948b3ed317.png', NULL, '1', 999, NULL, '2024-09-30 15:01:39', 0);
INSERT INTO `goods_type` VALUES (2, '零食', '所有零食', NULL, NULL, '1', 9999, NULL, '2024-09-30 14:48:53', 0);
INSERT INTO `goods_type` VALUES (3, '西瓜', '非常好吃的西瓜', 'https://yunshangshequ.oss-cn-chengdu.aliyuncs.com/icons/icon_3212wanjasr/xiguarang.png', 1, '1', 999999, NULL, '2024-09-30 14:42:36', 0);
INSERT INTO `goods_type` VALUES (4, '葡萄', '非常好吃的葡萄', 'https://yunshangshequ.oss-cn-chengdu.aliyuncs.com/icons/icon_3212wanjasr/putao.png', 1, '1', 20, NULL, NULL, 0);
INSERT INTO `goods_type` VALUES (5, '芒果', '非常好吃的芒果12', 'https://yunshangshequ.oss-cn-chengdu.aliyuncs.com/icons/icon_3212wanjasr/mangguo.png', 1, '1', 5, NULL, '2024-09-30 15:01:17', 1);
INSERT INTO `goods_type` VALUES (6, '坚果', '非常好吃的坚果', NULL, 2, '1', 100, NULL, NULL, 0);
INSERT INTO `goods_type` VALUES (7, '辣条', '非常好吃的辣条', NULL, 2, '1', 20, NULL, NULL, 0);
INSERT INTO `goods_type` VALUES (10, '橘子', '非常好吃的橘子', 'https://yunshangshequ.oss-cn-chengdu.aliyuncs.com/icons/icon_3212wanjasr/juzi.png', 1, '1', 999, '2024-09-30 13:49:02', '2024-09-30 14:36:23', 0);
INSERT INTO `goods_type` VALUES (11, '香蕉', '非常非常好吃的香蕉', 'https://yunshangshequ.cn-chengdu.oss.aliyuncs.com/2024.09.30/404a678d-aae3-4e5b-844f-ba27643d9209.png', 1, '1', 2222, '2024-09-30 13:57:13', '2024-09-30 14:38:53', 1);
INSERT INTO `goods_type` VALUES (12, '火龙果', '非常好吃的火龙果', 'https://yunshangshequ.cn-chengdu.oss.aliyuncs.com/2024.09.30/3c18869e-f0bb-48a7-8c3e-8fd02318bfd5.png', 1, '1', 99999, '2024-09-30 13:58:54', '2024-09-30 14:25:13', 1);
INSERT INTO `goods_type` VALUES (13, '芒果', '非常好吃的芒果', 'https://yunshangshequ.cn-chengdu.oss.aliyuncs.com/2024.09.30/928d56f0-7062-4eaf-a89b-52ba43199322.png', 1, '1', 9999999, '2024-09-30 15:01:33', NULL, 0);
SET FOREIGN_KEY_CHECKS = 1;
3、后端源码
3.1 application.yml 方便 AliOssUtil.java 读取
yml
server:
port: 8080
spring:
servlet:
multipart:
max-file-size: 40MB
max-request-size: 40MB
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://localhost:3306/yunshangshequ?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT%2b8
username: root
password: 123456
hikari:
connection-test-query: SELECT 1 # 自动检测连接
connection-timeout: 60000 #数据库连接超时时间,默认30秒
idle-timeout: 500000 #空闲连接存活最大时间,默认600000(10分钟)
max-lifetime: 540000 #此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
maximum-pool-size: 12 #连接池最大连接数,默认是10
minimum-idle: 10 #最小空闲连接数量
pool-name: SPHHikariPool # 连接池名称
jackson:
time-zone: GMT+8
data:
redis:
host: localhost
port: 6379
database: 0
#用于打印框架生成的sql语句,便于调试
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: isDeleted # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除的值 默认为 1
logic-not-delete-value: 0 # 逻辑未删除的值 默认为 0
minio:
endpoint: http://localhost:9000
access-key: minioadmin
secret-key: minioadmin
bucket-name: yunshangshequ
springdoc:
default-flat-param-object: true
alioss: # 阿里云配置
endpoint: "https://cn-chengdu.oss.aliyuncs.com" # Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
bucketName: "" # 填写Bucket名称,例如examplebucket。
access_key: "" # 点击头像->Accesskey管理查看 秘钥
access_key_secret: "" # 密码
3.2 model 层
3.2.1 BaseEntity
java
package com.zhong.model.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class BaseEntity implements Serializable {
@Schema(description = "主键")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "创建时间")
@TableField(value = "create_time", fill = FieldFill.INSERT)
@JsonIgnore
private Date createTime;
@Schema(description = "更新时间")
@TableField(value = "update_time", fill = FieldFill.UPDATE)
@JsonIgnore
private Date updateTime;
@Schema(description = "逻辑删除")
@TableField("is_deleted")
@JsonIgnore
private Byte isDeleted;
}
3.2.1 GoodsType
java
package com.zhong.model.entity.goods;
import com.zhong.model.entity.BaseEntity;
import lombok.Data;
/**
* 商品分类
* @TableName shop_type
*/
@Data
public class GoodsType extends BaseEntity {
/**
/**
* 分类名称
*/
private String name;
/**
* 分类描述
*/
private String description;
/**
* 排序值
*/
private Long typeSort;
/**
* 分类图标
*/
private String img;
/**
* 如果没有 father_id 则是一级分类
*/
private Integer fatherId;
/**
* 是否启用 1 启用 0禁用
*/
private Integer status;
private static final long serialVersionUID = 1L;
}
3.2.3 GoodsTypeSonVo
java
package com.zhong.vo.small;
import com.zhong.model.entity.goods.GoodsType;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* @ClassName : GoodsTypeVo
* @Description :
* @Author : zhx
* @Date: 2024-09-29 15:08
*/
@Data
public class GoodsTypeSonVo {
List<GoodsType> fatherType;
List<GoodsType> sonType;
}
3.3 Controller 层
3.3.1 FileUploadController.java 后端上传图片到阿里云OSS
java
package com.zhong.controller.apartment;
import com.zhong.result.Result;
import com.zhong.utils.AliOssUtil;
import io.minio.errors.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;
@Tag(name = "文件管理")
@RequestMapping("/admin/file")
@RestController
public class FileUploadController {
@Autowired
private AliOssUtil ossUtil;
@Operation(summary = "上传文件")
@PostMapping("/upload")
public Result<String> upload(@RequestParam MultipartFile file) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
// 获取文件原名
String originalFilename = file.getOriginalFilename();
// 防止重复上传文件名重复
String fileName = null;
if (originalFilename != null) {
fileName = UUID.randomUUID() + originalFilename.substring(originalFilename.indexOf("."));
}
// 把文件储存到本地磁盘
// file.transferTo(new File("E:\\SpringBootBase\\ProjectOne\\big-event\\src\\main\\resources\\flies\\" + fileName));
String url = ossUtil.uploadFile(fileName, file.getInputStream());
System.out.println();
return Result.ok(url);
}
}
3.3.2 AliOssUtil.java 后端上传图片到阿里云OSS的工具类
java
package com.zhong.utils;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import com.zhong.result.Result;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @ClassName : AliOssUtil
* @Description : 阿里云上传服务
* @Author : zhx
* @Date: 2024-03-1 20:29
*/
@Component
@Service
public class AliOssUtil {
@Value("${alioss.endpoint}")
private String ENDPOINT;
@Value("${alioss.bucketName}")
private String BUCKETNAME;
@Value("${alioss.access_key}")
private String ACCESS_KEY;
@Value("${alioss.access_key_secret}")
private String ACCESS_KEY_SECRET;
public String uploadFile(String objectName, InputStream inputStream) {
String url = "";
// 创建OSSClient实例。
System.out.println(ACCESS_KEY);
OSS ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY, ACCESS_KEY_SECRET);
try {
// 创建PutObjectRequest对象。
// 生成日期文件夹路径,例如:2022/04/18
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd");
String dateStr = dateFormat.format(new Date());
PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKETNAME, dateStr + "/" + objectName, inputStream);
// 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
// ObjectMetadata metadata = new ObjectMetadata();
// metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
// metadata.setObjectAcl(CannedAccessControlList.Private);
// putObjectRequest.setMetadata(metadata);
// 上传文件。
PutObjectResult result = ossClient.putObject(putObjectRequest);
url = "https://" + BUCKETNAME + "." + ENDPOINT.substring(ENDPOINT.lastIndexOf("/") + 1) + "/" + dateStr + "/" + objectName;
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return url;
}
public Result deleteFile(String objectName) {
System.out.println(objectName);
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY, ACCESS_KEY_SECRET);
try {
// 删除文件。
System.out.println(objectName);
System.out.println(objectName.replace(",", "/"));
ossClient.deleteObject(BUCKETNAME, objectName.replace(",", "/"));
return Result.ok("删除成功!");
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return Result.fail(555,"上传失败!");
}
}
3.3.3 GoodsTypeController.java 商品分类接口
java
package com.zhong.controller.small;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.zhong.model.entity.enums.BaseStatus;
import com.zhong.model.entity.goods.GoodsType;
import com.zhong.result.Result;
import com.zhong.service.small.GoodsTypeService;
import com.zhong.vo.small.GoodsTypeSonVo;
import com.zhong.vo.small.GoodsTypeVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @ClassName : ShopSpecsController
* @Description : 商品分类
* @Author : zhx
* @Date: 2024-09-28 15:07
*/
@Slf4j
@RestController
@Tag(name = "后台商品分类管理")
@RequestMapping("/admin/goods/type")
public class GoodsTypeController {
@Autowired
private GoodsTypeService service;
@Operation(summary = "分页获商品分类信息")
@GetMapping("page")
private Result<IPage<GoodsType>> page(@RequestParam long current, @RequestParam long size, GoodsTypeVo goodsTypeVo) {
Page<GoodsType> goodsTypePage = new Page<>(current, size);
Page<GoodsType> page = service.pageItem(goodsTypePage, goodsTypeVo);
return Result.ok(page);
}
@Operation(summary = "获所有商品二级分类信息")
@GetMapping("page/son")
private Result<GoodsTypeSonVo> pageSon(@RequestParam long id) {
GoodsTypeSonVo page = service.list(id);
return Result.ok(page);
}
@Operation(summary = "获所有商品分类信息")
@GetMapping("list")
private Result<List<GoodsType>> page() {
List<GoodsType> page = service.list();
return Result.ok(page);
}
@Operation(summary = "根据ID修改商品分类")
@PostMapping("saveOrUpdate")
public Result saveOrUpdateType(@RequestBody GoodsType goodsType) {
goodsType.setIsDeleted((byte) 0);
service.saveOrUpdate(goodsType);
return Result.ok();
}
@DeleteMapping("deleteById")
@Operation(summary = "根据id删除商品分类")
public Result removeById(@RequestParam Long id) {
service.removeById(id);
return Result.ok();
}
}
3.4 GoodsTypeService 层
java
package com.zhong.service.small;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.zhong.model.entity.goods.GoodsType;
import com.zhong.vo.small.GoodsTypeSonVo;
import com.zhong.vo.small.GoodsTypeVo;
import java.util.List;
/**
* @author zhong
* @description 针对表【shop_type(商品分类)】的数据库操作Service
* @createDate 2024-09-15 18:18:13
*/
public interface GoodsTypeService extends IService<GoodsType> {
Page<GoodsType> pageItem(Page<GoodsType> goodsTypePage, GoodsTypeVo goodsTypeVo);
GoodsTypeSonVo list(long id);
}
3.5 GoodsTypeServiceImpl 层
java
package com.zhong.service.small.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zhong.mapper.small.GoodsTypeMapper;
import com.zhong.model.entity.goods.GoodsSku;
import com.zhong.model.entity.goods.GoodsType;
import com.zhong.service.small.GoodsTypeService;
import com.zhong.vo.small.GoodsTypeSonVo;
import com.zhong.vo.small.GoodsTypeVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author zhong
* @description 针对表【shop_type(商品分类)】的数据库操作Service实现
* @createDate 2024-09-15 18:18:13
*/
@Service
public class GoodsTypeServiceImpl extends ServiceImpl<GoodsTypeMapper, GoodsType>
implements GoodsTypeService {
@Autowired
private GoodsTypeMapper mapper;
@Override
public Page<GoodsType> pageItem(Page<GoodsType> goodsTypePage, GoodsTypeVo goodsTypeVo) {
LambdaQueryWrapper<GoodsType> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.isNull(GoodsType::getFatherId)
.eq(GoodsType::getIsDeleted, 0);
if (goodsTypeVo.getName() != null) {
queryWrapper.like(GoodsType::getName, goodsTypeVo.getName());
}
if (goodsTypeVo.getId() != null) {
queryWrapper.like(GoodsType::getId, goodsTypeVo.getId());
}
queryWrapper.orderByDesc(GoodsType::getTypeSort);
return mapper.selectPage(goodsTypePage, queryWrapper);
}
@Override
public GoodsTypeSonVo list(long id) {
GoodsTypeSonVo goodsTypeSonVo = new GoodsTypeSonVo();
// 获取父分类
LambdaQueryWrapper<GoodsType> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(GoodsType::getIsDeleted, 0)
.eq(GoodsType::getId, id);
List<GoodsType> goodsTypes = mapper.selectList(queryWrapper);
goodsTypeSonVo.setFatherType(goodsTypes);
// 获取子分类
LambdaQueryWrapper<GoodsType> sonQueryWrapper = new LambdaQueryWrapper<>();
sonQueryWrapper.eq(GoodsType::getIsDeleted, 0)
.eq(GoodsType::getFatherId, id)
.orderByDesc(GoodsType::getTypeSort);
List<GoodsType> sonGoodsTypes = mapper.selectList(sonQueryWrapper);
goodsTypeSonVo.setSonType(sonGoodsTypes);
return goodsTypeSonVo;
}
}
4、前端代码
4.1 type.Vue
ts
<template>
<el-upload
v-bind="$attrs"
:action="UPLOAD_IMG_URL"
:on-preview="handlePictureCardPreview"
:headers="{ 'access-token': useUserStore().token }"
>
<el-icon>
<Plus />
</el-icon>
</el-upload>
<el-dialog v-model="dialogVisible">
<el-image :src="dialogImageUrl" fit="fill" />
</el-dialog>
</template>
<script setup lang="ts">
import { UPLOAD_IMG_URL } from '@/api/upload'
import { ref } from 'vue'
import { UploadFile } from 'element-plus/es/components/upload/src/upload'
import { useUserStore } from '@/store/modules/user'
const dialogImageUrl = ref('')
const dialogVisible = ref(false)
function handlePictureCardPreview(uploadFile: UploadFile) {
dialogImageUrl.value = uploadFile.url!
dialogVisible.value = true
}
</script>
<style scoped lang="scss"></style>
4.2 type.Vue
ts
<template>
<div>
<ProTable ref="proTable" :dataCallback="dataCallback" :columns="columns" :requestApi="getTypeListPageApi"
:initParam="initParam">
<template #tableHeader>
<el-button type="primary" icon="Plus" @click="addHandle">
添加
</el-button>
</template>
<!-- 表格操作 -->
<template #operation="scope">
<el-button type="primary" link icon="Edit" @click="editHandle(scope.row)">
编辑
</el-button>
<el-button type="primary" link icon="Delete" @click="handleDelete(scope.row)">
删除
</el-button>
</template>
</ProTable>
<PostDialog ref="DialogRef" />
</div>
</template>
<script setup lang="tsx">
import { reactive, ref } from 'vue'
import { ColumnProps } from '@/components/ProTable/src/types'
import { useHandleData } from '@/hooks/useHandleData'
import { useRouter } from 'vue-router'
const router = useRouter();
import {
getTypeListPageApi,
// addGoodsTypeApi,
// // updateGoodsTypeStatusApi,
// updateGoodsTypeApi
} from '@/api/goodsType'
import { PostInterfacesRes } from '@/api/goodsType/types'
// *获取 ProTable 元素,调用其获取刷新数据方法
const proTable = ref()
// *查询参数
const initParam = reactive({})
// 处理返回的数据格式
const dataCallback = (data: any) => {
return {
list: data?.records,
total: data?.total,
}
}
// 新增商品
// 新增
const addHandle = () => {
router.push({
path: '/small/type/addOrEditGoods',
})
}
// 查看
const editHandle = (row: PostInterfacesRes) => {
console.log(row)
router.push({
path: '/small/type/addOrEditGoods',
query: {
id: row.id,
},
})
}
// *根据id删除用户
const handleDelete = async (row: PostInterfacesRes) => {
// await useHandleData(deleteSysPostById, row.id, `删除${row.id}`)
proTable.value?.getTableList()
}
// *表格配置项
const columns: ColumnProps[] = [
{ type: 'index', label: '序号', width: 100 },
{
prop: 'id',
label: '分类ID',
search: { el: 'input', props: { placeholder: '请输入分类编码' } },
},
{
prop: 'name',
label: '分类名称',
search: { el: 'input', props: { placeholder: '请输入分类名称' } },
},
{
prop: 'description',
label: '分类描述',
},
{
prop: 'typeSort',
label: '分类排序',
},
{
prop: 'status',
label: '状态',
width: 100,
enum: [
{ label: '正常', value: 1 },
{ label: '停用', value: 0 },
],
render: ({ row }) => {
return (
<el-switch
active-value={1}
inactive-value={0}
v-model={row.status}
onChange={() => updateGoodsTypeStatusApi(row.id, row.status)}
></el-switch>
)
},
},
{ prop: 'operation', label: '操作', fixed: 'right', width: 280 },
]
</script>
4.3 addOrEditType.Vue
ts
<!--
* @Date: 2024-08-17 14:18:14
* @LastEditors: zhong
* @LastEditTime: 2024-09-30 14:57:38
* @FilePath: \admin\src\views\small\type\components\addOrEditType.vue
-->
<template>
<el-card>
<template #header>
<div class="card-header">
<span>{{ formData.id ? '修改' : '新增' }}商品分类</span>
</div>
</template>
<el-card>
<template #header>
<div class="card-header">
<span>{{ formData.id ? '修改' : '新增' }}商品一级分类</span>
</div>
</template>
<div style="display: flex;align-items: center;">
<div style="flex: 1;">
<el-form ref="typeFormRef" :model="formData" :rules="rules" label-width="120px" style="max-width: 660px"
status-icon>
<el-form-item label="分类名称" prop="name">
<el-input v-model="formData.name" />
</el-form-item>
<el-form-item label="分类简介" prop="description">
<el-input type="textarea" v-model="formData.description" />
</el-form-item>
<el-form-item label="分类排序" prop="typeSort">
<el-input v-model="formData.typeSort" />
</el-form-item>
<el-form-item label="是否启用" prop="status">
<el-radio-group v-model="formData.status" class="ml-4">
<el-radio :label="UserStatus.DISABLED">
{{
getLabelByValue(
UserStatusMap,
UserStatus.DISABLED,
)
}}
</el-radio>
<el-radio :label="UserStatus.NORMAL">
{{
getLabelByValue(
UserStatusMap,
UserStatus.NORMAL,
)
}}
</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</div>
<!-- 上传操作 -->
<div style="width: 200px;display: flex;flex-direction: column;padding-bottom: 60px;">
<text label="商品主图" />
<upload-img v-model:file-list="formData.img" :on-success="uploadSuccessHandle" :on-remove="uploadRemoveHandle"
list-type="picture-card" :limit="1" :class="listLengthTag === 1 ? 'hide_box' : ''"></upload-img>
</div>
</div>
</el-card>
<el-card class="m-t-20">
<template #header>
<div class="card-header">
<span>{{ formData.id ? '修改' : '新增' }}商品二级分类</span>
</div>
</template>
<el-row class="container">
<el-col :span="2" class="text-center">二级分类</el-col>
<el-col :span="22" class="item-container">
<el-popconfirm v-for="item in goodsTypeList || []" :key="item.id" width="220" confirm-button-text="删除"
cancel-button-text="修改" cancel-button-type="warning" confirm-button-type="danger"
@confirm="deleteFacilityHandle(item)" @cancel="editTypeHandle(item)" :title="`修改或直接删除${item.name}`">
<template #reference>
<div class="item m-r-10 m-t-10 pointer">
<el-image style="width: 30px; height: 30px" :src="item.img" />
<span>{{ item.name }}</span>
</div>
</template>
</el-popconfirm>
<el-icon class="item m-r-10 m-t-5 pointer" :size="35" color="#567722" @click="addTypeDialog(formData)">
<CirclePlus />
</el-icon>
</el-col>
</el-row>
<!-- 商品二级分类信息修改弹窗管理-->
<SupportTypeDialog ref="supportTypeDialog" :updateFacility="getTypeListHandle"></SupportTypeDialog>
</el-card>
<!-- -->
<!-- 底部保存或取消按钮 -->
<div class="flex-center m-t-20">
<el-button style="width: 150px" type="info" @click="router.back()">
取消
</el-button>
<el-button style="width: 150px" type="primary" @click="submitHandle">
{{ formData.id ? '保存' : '新增' }}
</el-button>
</div>
</el-card>
</template>
<script setup lang="ts">
import { onMounted, ref, reactive, computed } from 'vue'
import { FacilityInfoInterface } from '@/api/apartmentManagement/types'
// import { PageResponseInterface } from '@/api/type/types'
import {
deleteGoodsTypeByIdApi,
getTypeSonListApi,
saveOrUpdateGoodsTypeApi,
} from '@/api/goodsType'
import {
getLabelByValue,
UserStatus,
UserStatusMap
} from '@/enums/constEnums'
import { UploadFile } from 'element-plus/es/components/upload/src/upload'
import UploadImg from '@/components/uploadImg/uploadImg.vue'
import SupportTypeDialog from "./supportTypeDialog.vue"
import { ElMessage, FormInstance, UploadFiles } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
type typeFromData = {
id: string
name: string
description: string
typeSort: string
status: string
img: any[]
}
// 父类分类信息
const formData = ref<typeFromData>({
id: "",
name: "",
description: "",
typeSort: "",
status: "",
img: []
})
const typeFormRef = ref<FormInstance>()
// 清空表单数据的函数
const clearFormData = () => {
formData.value.id = "";
formData.value.name = "";
formData.value.description = "";
formData.value.typeSort = "";
formData.value.status = "";
formData.value.img = [];
};
// 表单验证规则
const rules = reactive({
name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }],
description: [
{ required: true, message: '请输入分类介绍', trigger: 'blur' },
],
typeSort: [{ required: true, message: '请输入分类排序数值越大越靠前', trigger: 'blur' }],
})
// 图片上传成功
function uploadSuccessHandle(
response: any,
uploadFile: UploadFile,
uploadFiles: UploadFiles,
) {
console.log(uploadFile);
formData.value.img = uploadFiles?.map((item) => {
return {
...item,
url: (item?.response as any)?.data.url || item.url,
}
})
console.log(formData.value.img);
}
// TODO 控制上传图片数量隐藏上传按钮
const listLengthTag = computed(() => {
return formData.value.img.length;
});
// 移除图片
const uploadRemoveHandle = (uploadFiles: UploadFiles, uploadFile: UploadFile) => {
console.log(uploadFiles);
console.log(uploadFile);
}
const supportTypeDialog = ref<any>()
// 修改后刷新数据
const getTypeListHandle = () => {
if (route.query?.id) {
getGoodsInfoList(route.query.id as string)
}
}
// 子类分类信息
const goodsTypeList = ref<any>([])
// 获取分类信息
async function getGoodsInfoList(id: string | number) {
try {
const { data } = await getTypeSonListApi(id);
console.log(data);
formData.value = data.fatherType?.[0];
formData.value.img = [{ name: formData.value.name, url: formData.value.img }];
console.log(formData.value);
goodsTypeList.value = data.sonType;
} catch (error) {
console.log(error)
}
}
// 删除分类
const deleteFacilityHandle = async (item: any) => {
console.log('删除分类', item)
try {
await deleteGoodsTypeByIdApi(item.id)
await getTypeListHandle()
ElMessage.success('操作成功')
} catch (error) {
console.log(error)
}
}
// 编辑分类
const editTypeHandle = (item: typeFromData) => {
console.log('编辑分类', item)
supportTypeDialog.value?.show(item)
}
// 添加分类
const addTypeDialog = (item: any) => {
console.log('添加分类', item)
supportTypeDialog.value?.show({ type: item.id })
}
// 新增或更新商品分类信息
async function addOrUpdateTypeHandle() {
try {
console.log(formData.value);
let res = JSON.parse(JSON.stringify(formData.value));
// 判断图片改变没有
if (res.img?.[0]?.response) {
res.img = res.img[0].response.data;
} else {
res.img = res.img[0].url;
}
await saveOrUpdateGoodsTypeApi(res);
clearFormData();
ElMessage.success('操作成功')
router.back()
} catch (error) {
console.log(error)
}
}
// 提交
function submitHandle() {
typeFormRef.value?.validate(async (valid) => {
if (valid) {
await addOrUpdateTypeHandle()
} else {
ElMessage.error('表单填写有误,请检查')
return false
}
})
}
onMounted(() => {
if (route.query?.id) {
getGoodsInfoList(route.query.id as string)
}
})
</script>
<style scoped lang="scss">
::v-deep(.hide_box .el-upload.el-upload--picture-card) {
display: none !important;
}
.card-header {
font-size: 18px;
font-weight: bold;
}
.text-center {
display: flex;
align-items: center;
justify-content: flex-start;
text-align: center;
}
.container:not(:first-child) {
margin-top: 20px;
}
.item-container {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
width: 100%;
padding: 10px 15px;
background-color: #efefef;
border-radius: 20px;
.item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
// width: 50px;
}
}
</style>
4.4 supportTypeDialog.Vue
js
<!--
* @Date: 2024-09-29 19:45:25
* @LastEditors: zhong
* @LastEditTime: 2024-09-30 15:00:22
* @FilePath: \admin\src\views\small\type\components\supportTypeDialog.vue
-->
<template>
<el-dialog v-model="dialogFormVisible" :close-on-press-escape="true" :destroy-on-close="true" :title="title"
style="max-width: 700px">
<div style="display: flex;align-items: center;">
<div style="flex: 1;">
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" style="max-width: 660px"
status-icon>
<el-form-item label="分类名称" prop="name">
<el-input v-model="formData.name" />
</el-form-item>
<el-form-item label="分类简介" prop="description">
<el-input type="textarea" v-model="formData.description" />
</el-form-item>
<el-form-item label="分类排序" prop="typeSort">
<el-input v-model="formData.typeSort" />
</el-form-item>
<el-form-item label="是否上架" prop="status">
<el-radio-group v-model="formData.status" class="ml-4">
<el-radio :label="UserStatus.DISABLED">
{{
getLabelByValue(
UserStatusMap,
UserStatus.DISABLED,
)
}}
</el-radio>
<el-radio :label="UserStatus.NORMAL">
{{
getLabelByValue(
UserStatusMap,
UserStatus.NORMAL,
)
}}
</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</div>
<!-- 上传操作 -->
<div style="width: 200px;display: flex;flex-direction: column;padding-left: 60px;padding-bottom: 70px;">
<text label="商品主图" prop="mainImage" />
<upload-img v-model:file-list="formData.img" :on-success="uploadSuccessHandle" :on-remove="uploadRemoveHandle"
list-type="picture-card" :limit="1" :class="listLengthTag === 1 ? 'hide_box' : ''"></upload-img>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="submitHandle">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import ids from 'virtual:svg-icons-names'
import { computed, ref } from 'vue'
import { FacilityInfoInterface } from '@/api/apartmentManagement/types'
import { ElMessage, FormInstance, FormRules, UploadFiles } from 'element-plus'
import { saveOrUpdateFacilityInfo } from '@/api/apartmentManagement'
import { UploadFile } from 'element-plus/es/components/upload/src/upload'
import UploadImg from '@/components/uploadImg/uploadImg.vue'
import {
getLabelByValue,
UserStatus,
UserStatusMap
} from '@/enums/constEnums'
import {
saveOrUpdateGoodsTypeApi,
} from '@/api/goodsType'
const props = defineProps({
updateFacility: {
type: Function,
default: () => ({}),
},
})
const defaultFormData = {
id: '',
description: '',
name: '',
status: "",
fatherId: "",
typeSort: "",
img: [],
}
const formRef = ref<FormInstance>()
const dialogFormVisible = ref(false)
const formData = ref<any>({
...defaultFormData,
})
// 清空表单数据的函数
const clearFormData = () => {
formData.value.id = "";
formData.value.name = "";
formData.value.description = "";
formData.value.typeSort = "";
formData.value.status = "";
formData.value.img = [];
};
// 表单验证规则
const rules = ref<FormRules>({
name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }],
description: [{ required: true, message: '请选择分类描述', trigger: 'change' }],
typeSort: [{ required: true, message: '请输入分类排序数值越大越靠前', trigger: 'change' }],
})
const title = computed(() => {
return (
(formData.value.id ? '修改' : '新增') + formData.value.name + '二级分类详情'
)
})
// 图片上传成功
// TODO 控制上传图片数量隐藏上传按钮
const listLengthTag = computed(() => {
return formData.value.img.length;
});
function uploadSuccessHandle(
response: any,
uploadFile: UploadFile,
uploadFiles: UploadFiles,
) {
console.log(uploadFile);
formData.value.img = uploadFiles?.map((item) => {
return {
...item,
url: (item?.response as any)?.data.url || item.url,
}
})
console.log(formData.value.img);
}
// 移除图片
const uploadRemoveHandle = (uploadFiles: UploadFiles, uploadFile: UploadFile) => {
console.log(uploadFiles);
console.log(uploadFile);
}
const isEdit = ref(false);
// 展示方法
const show = (data: Partial<any> = defaultFormData) => {
console.log('show', data)
if (data.fatherId != null) {
formData.value = Object.assign({}, defaultFormData, data)
formData.value.img = [{ name: data.name, url: data.img }];
} else {
formData.value.fatherId = data.type;
formData.value.status = 1;
isEdit.value = true;
}
dialogFormVisible.value = true
}
// 关闭方法
const close = () => {
dialogFormVisible.value = false
}
// 提交方法
const submitHandle = () => {
formRef.value?.validate(async (valid) => {
if (valid) {
let res = JSON.parse(JSON.stringify(formData.value));
if (isEdit && res.img?.[0]?.response) {
res.img = res.img[0].response.data;
} else {
res.img = res.img[0].url;
}
await saveOrUpdateGoodsTypeApi(res)
clearFormData();
await props.updateFacility()
ElMessage.success('操作成功')
close()
} else {
ElMessage.error('表单填写有误,请检查')
return false
}
})
}
// 对外暴露
defineExpose({
show,
close,
})
</script>
<style scoped lang="scss">
::v-deep(.hide_box .el-upload.el-upload--picture-card) {
display: none !important;
}
.icon-option {
display: flex;
align-items: center;
}
</style>