Springboot3 + MyBatis-Plus + MySql + Vue + ProTable + TS 实现后台管理商品分类(最新教程附源码)

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>
相关推荐
梁萌18 分钟前
mysql使用事件做日志表数据转移
数据库·mysql
lThE ANDE19 分钟前
MySQL中的TRUNCATE TABLE命令
数据库·mysql
STER labo1 小时前
mysql配置环境变量——(‘mysql‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件解决办法)
数据库·mysql·adb
dreamZhanglx1 小时前
MySQL进阶
数据库·mysql
xmjd msup1 小时前
MySQL 函数
数据库·mysql
Python私教2 小时前
Pure-Admin-Thin 深度解析:完整版和精简版到底怎么选?
vue.js·人工智能·开源
jefl jxak2 小时前
mysql用户名怎么看
数据库·mysql
unDl IONA2 小时前
mysql之如何获知版本
数据库·mysql
俺不要写代码2 小时前
数据库:约束
数据库·mysql
WL_Aurora3 小时前
MySQL 5 卸载到 MySQL 8 安装完整指南(不踩坑版)
数据库·mysql