Springboot3 + MyBatis-Plus + MySql + Vue + ProTable + TS 实现后台管理商品分类(最新教程附源码)
Springboot3 + MyBatis-Plus + MySql + Uniapp 商品加入购物车功能实现(针对上一篇sku)
- 1、效果展示
- 2、数据库设计
- 3、后端源码
- 3.1 application.yml 方便 AliOssUtil.java 读取
- 3.2 model 层
- 3.2.1 BaseEntity
- 3.2.1 GoodsType
- 3.2.3 GoodsTypeSonVo
- 3.3 Controller 层
- 3.3.1 FileUploadController.java 后端上传图片到阿里云OSS
- 3.3.2 AliOssUtil.java 后端上传图片到阿里云OSS的工具类
- 3.3.3 GoodsTypeController.java 商品分类接口
- 3.4 GoodsTypeService 层
- 3.5 GoodsTypeServiceImpl 层
- 4、前端代码
- 4.1 type.Vue
- 4.2 type.Vue
- 4.3 addOrEditType.Vue
- 4.4 supportTypeDialog.Vue
1、效果展示
QQ2024930-15058

2、数据库设计
/*Navicat Premium Data TransferSource Server : Test1Source Server Type : MySQLSource Server Version : 80200Source Host : localhost:3306Source Schema : yunshangshequTarget Server Type : MySQLTarget Server Version : 80200File Encoding : 65001Date: 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 读取
server:port: 8080
spring:servlet:multipart:max-file-size: 40MBmax-request-size: 40MBdatasource:type: com.zaxxer.hikari.HikariDataSourceurl: jdbc:mysql://localhost:3306/yunshangshequ?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT%2b8username: rootpassword: 123456hikari:connection-test-query: SELECT 1 # 自动检测连接connection-timeout: 60000 #数据库连接超时时间,默认30秒idle-timeout: 500000 #空闲连接存活最大时间,默认600000(10分钟)max-lifetime: 540000 #此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟maximum-pool-size: 12 #连接池最大连接数,默认是10minimum-idle: 10 #最小空闲连接数量pool-name: SPHHikariPool # 连接池名称jackson:time-zone: GMT+8data:redis:host: localhostport: 6379database: 0
#用于打印框架生成的sql语句,便于调试
mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImplglobal-config:db-config:logic-delete-field: isDeleted # 全局逻辑删除的实体字段名logic-delete-value: 1 # 逻辑已删除的值 默认为 1logic-not-delete-value: 0 # 逻辑未删除的值 默认为 0minio:endpoint: http://localhost:9000access-key: minioadminsecret-key: minioadminbucket-name: yunshangshequ
springdoc:default-flat-param-object: truealioss: # 阿里云配置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
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)@JsonIgnoreprivate Date createTime;@Schema(description = "更新时间")@TableField(value = "update_time", fill = FieldFill.UPDATE)@JsonIgnoreprivate Date updateTime;@Schema(description = "逻辑删除")@TableField("is_deleted")@JsonIgnoreprivate Byte isDeleted;}
3.2.1 GoodsType
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
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
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 {@Autowiredprivate 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的工具类
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/18SimpleDateFormat 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 商品分类接口
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 {@Autowiredprivate 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 层
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 层
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 {@Autowiredprivate GoodsTypeMapper mapper;@Overridepublic 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);}@Overridepublic 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
<template><el-uploadv-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
<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-switchactive-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
<!--* @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: stringname: stringdescription: stringtypeSort: stringstatus: stringimg: 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
<!--* @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>相关文章:
Springboot3 + MyBatis-Plus + MySql + Vue + ProTable + TS 实现后台管理商品分类(最新教程附源码)
Springboot3 MyBatis-Plus MySql Uniapp 商品加入购物车功能实现(针对上一篇sku) 1、效果展示2、数据库设计3、后端源码3.1 application.yml 方便 AliOssUtil.java 读取3.2 model 层3.2.1 BaseEntity3.2.1 GoodsType3.2.3 GoodsTypeSonVo3.3 Controll…...
消费电子制造企业如何使用SAP系统提升运营效率与竞争力
在当今这个日新月异的消费电子市场中,企业面临着快速变化的需求、激烈的竞争以及不断攀升的成本压力。为了在这场竞赛中脱颖而出,消费电子制造企业纷纷寻求数字化转型的突破点,其中,SAP系统作为业界领先的企业资源规划(ERP)解决方…...
算法记录——树
二叉树 3.1二叉树的最大深度 思路:二叉树的最大深度 根节点的最大高度。因此本题可以转换为求二叉树的最大高度。 而求高度的时候应该采用后序遍历。遍历顺序为:左右中。每次遍历的节点按后序遍历顺序,先收集左右孩子的最大高度,…...
单片机在控制和自动化任务中的应用场景广泛
单片机在控制和自动化任务中的应用场景广泛,以下是一些具体示例: 1. 家电控制 洗衣机:单片机用于控制洗衣周期、温度和水位。微波炉:控制加热时间、功率和用户界面。 2. 工业自动化 生产线监控:单片机用于控制传送…...
UEFI EDK2框架学习(三)——protocol
一、Protocol协议 搜索支持特定Protocol的设备,获取其Handle gBS->LocateHandleBuffer 将内存中的Driver绑定到给定的ControllerHandle gBS->OpenProtocol 二、代码实现 Protocol.c #include <Uefi.h> #include <Library/UefiLib.h> #includ…...
PostgreSQL的字段存储类型了解
PostgreSQL的字段存储类型了解 在 PostgreSQL 中,每个字段(列)都有其存储类型,这些存储类型决定了数据库如何存储和处理该字段的数据。了解和适当地利用这些存储类型,可以提高数据库的性能和存储效率。 主要的存储类…...
CTFshow 命令执行 web29~web36(正则匹配绕过)
目录 web29 方法一:include伪协议包含文件读取 方法二:写入文件 方法三:通识符 web30 方法一:filter伪协议文件包含读取 方法二:命令执行函数绕过 方法三:写入文件 web31 方法一:filter伪…...
【顺序表使用练习】发牌游戏
【顺序表使用练习】发牌游戏 1. 介绍游戏2. 实现52张牌3. 实现洗牌4. 实现发牌5. 效果展示 1. 介绍游戏 首先先为大家介绍一下设计要求 实现52张牌(这里排除大小王)洗牌——打乱牌的顺序发牌——3个人,1人5张牌 2. 实现52张牌 创建Code对象创…...
1.7 编码与调制
欢迎大家订阅【计算机网络】学习专栏,开启你的计算机网络学习之旅! 文章目录 前言前言1 基本术语2 常用的编码方法2.1 不归零编码2.2 归零编码2.3 反向归零编码2.4 曼彻斯特编码2.5 差分曼彻斯特编码 3 常用的调制方法3.1 调幅(AM)…...
004集—— txt格式坐标写入cad(CAD—C#二次开发入门)
如图所示原始坐标格式,xy按空格分开,将坐标按顺序在cad中画成多段线: 坐标xy分开并按行重新输入txt,效果如下: 代码如下 : using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.Runtime; us…...
CSS中的font-variation-settings:探索字体的可变性
随着Web字体的发展,设计师们不再局限于传统的字体样式。现代Web字体支持可变字体(Variable Fonts),这种字体允许开发者在单一的字体文件中包含多种字形样式。通过使用CSS中的font-variation-settings属性,我们可以控制…...
组合优化与凸优化 学习笔记5 对偶拉格朗日函数
有的时候约束条件有点难搞,我们可以把它放到目标函数里面。 记得之前凸函数的时候的结论吗?一大堆函数,每一段都取最大的,最后会得到一个凸函数。同理,每一段都取最小的,得到的是一个凹函数。就这样&#x…...
监控易监测对象及指标之:Exchange邮件服务器监测
在现代企业运营中,邮件服务器的作用至关重要,它不仅承载着企业内外的信息传递,还是协同工作的重要工具。为了确保邮件服务器的稳定运行,以及邮件的顺畅收发,采用高效的监控系统是不可或缺的。监控易作为一款专业的监控…...
【机器学习基础】Transformer学习
Transformer学习 梯度消失FeedForward层激活函数的主要作用是在网络中加入非线性变换 梯度消失 梯度爆炸 FeedForward层 Transformer结构: Transformer结构主要分为两大部分: 一是Encoder层结构:Encoder 的输入由 Input Embedding 和 Positional Embedding 求和输入Multi…...
mysql如何不使用窗口函数,去统计出入库情况
mysql如何不使用窗口函数,去统计出入库情况 你把这个表看做 进出库表,每个物料把时间正序后 依次累加数量 ,看这个物料的时间线上 是否会出现负数,1号进货5个 2号出库3个 3号你不能出库3个 最多俩个 不然就是负库存,…...
uni-app canvas文本自动换行
封装 支持单行文本超出换行。多行文本顺位排版 // 填充自动换行的文本function fillFeedText({ctx, text, x, y, maxWidth, lineHeight, color, size}) {// 文本配置ctx.setFontSize(size);ctx.setFillStyle(color);// 计算文本换行宽高,换行逻辑const words text…...
【设计模式-职责链】
定义 职责链模式是一种行为设计模式,**它通过将请求发送给链上的多个处理者来避免请求发送者与处理者之间的紧密耦合。每个处理者可以选择处理请求或将其传递给链中的下一个处理者。**这样,可以将处理请求的责任链式组织,从而实现更灵活的请…...
Prompt:在AI时代,提问比答案更有价值
你好,我是三桥君 随着AI技术的飞速发展,我们进入了一个信息爆炸的时代。在这个时代,只要你会提问,AI就能为你提供满意的答案。这种现象让很多人开始思考:在这个答案触手可及的时代,答案的价值是否还像以前…...
whatis命令:关于命令的简短描述
一、命令简介 whatis 命令用于查询命令、函数、文件等的基本用途,查询结果只是一句简短的描述。 例如 $ whatis ls ls (1) - list directory contents返回关于 ls 命令的简短描述。这个结果实质是来自于man手册的一个章节,在较新的L…...
ICM20948 DMP代码详解(54)
接前一篇文章:ICM20948 DMP代码详解(53) 上一回解析了inv_icm20948_compass_dmp_cal函数的大部分代码,本回继续讲解inv_icm20948_compass_dmp_cal函数的余下内容。为了便于理解和回顾,再次贴出inv_icm20948_compass_dmp_cal函数代码,在EMD-Core\sources\Invn\Devices\Dri…...
OkHttp 中实现断点续传 demo
在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range 请求头指定下载范围: 实现原理 Range 请求头:向服务器请求文件的特定字节范围(如 Range: bytes1024-) 本地文件记录:保存已…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
2023赣州旅游投资集团
单选题 1.“不登高山,不知天之高也;不临深溪,不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...
DingDing机器人群消息推送
文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人,点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置,详见说明文档 成功后,记录Webhook 2 API文档说明 点击设置说明 查看自…...
MySQL JOIN 表过多的优化思路
当 MySQL 查询涉及大量表 JOIN 时,性能会显著下降。以下是优化思路和简易实现方法: 一、核心优化思路 减少 JOIN 数量 数据冗余:添加必要的冗余字段(如订单表直接存储用户名)合并表:将频繁关联的小表合并成…...
Golang——9、反射和文件操作
反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一:使用Read()读取文件2.3、方式二:bufio读取文件2.4、方式三:os.ReadFile读取2.5、写…...
OD 算法题 B卷【正整数到Excel编号之间的转换】
文章目录 正整数到Excel编号之间的转换 正整数到Excel编号之间的转换 excel的列编号是这样的:a b c … z aa ab ac… az ba bb bc…yz za zb zc …zz aaa aab aac…; 分别代表以下的编号1 2 3 … 26 27 28 29… 52 53 54 55… 676 677 678 679 … 702 703 704 705;…...
redis和redission的区别
Redis 和 Redisson 是两个密切相关但又本质不同的技术,它们扮演着完全不同的角色: Redis: 内存数据库/数据结构存储 本质: 它是一个开源的、高性能的、基于内存的 键值存储数据库。它也可以将数据持久化到磁盘。 核心功能: 提供丰…...
