Springboot3 + MyBatis-Plus + MySql + Uniapp 实现商品规格选择sku(附带自设计数据库,最新保姆级教程)

Springboot3 + MyBatis-Plus + MySql + Uniapp 实现商品规格选择sku(附带自设计数据库,最新保姆级教程)

  • 1、效果展示
  • 2、数据库设计
    • [2.1 商品表](#2.1 商品表)
    • [2.2 商品价格和规格中间表](#2.2 商品价格和规格中间表)
    • [2.3 商品规格表](#2.3 商品规格表)
  • 3、后端代码
    • [3.1 model](#3.1 model)
    • [3.2 vo](#3.2 vo)
    • [3.3 mapper、server、serverImp](#3.3 mapper、server、serverImp)
    • [3.4 controller](#3.4 controller)
  • 4、前端代码
    • [4.1 request.js](#4.1 request.js)
    • [4.2 index.js](#4.2 index.js)
    • [4.3 shop-info.vue](#4.3 shop-info.vue)
    • [4.4 ShopBottomButton.vue](#4.4 ShopBottomButton.vue)

1、效果展示


2、数据库设计

2.1 商品表


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: 21/09/2024 12:59:26
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for shop
-- ----------------------------
DROP TABLE IF EXISTS `shop`;
CREATE TABLE `shop`  (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '商品ID',
  `business_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签/自营?',
  `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品简介',
  `sale_number` int NULL DEFAULT NULL COMMENT '已售数量',
  `old_price` decimal(10, 2) NULL DEFAULT NULL COMMENT '旧价格',
  `new_price` decimal(10, 2) NULL DEFAULT NULL COMMENT '现售价',
  `in_price` decimal(10, 2) NULL DEFAULT NULL COMMENT '进货价',
  `inventory_num` int NULL DEFAULT NULL COMMENT '库存',
  `main_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品主图',
  `shop_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品分类',
  `shop_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci 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 = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of shop
-- ----------------------------
INSERT INTO `shop` VALUES (1, '自营', '新货【三只松鼠_量贩碧根果500g】特大健康坚果零食奶油味长寿果', 138, 39.90, 29.90, 19.90, 200, 'https://gw.alicdn.com/imgextra/i3/2218288872763/O1CN01rN6Cn91WHVIflhWLg_!!2218288872763.jpg', '1', '三只松鼠旗舰店', NULL, NULL, 0);
INSERT INTO `shop` VALUES (2, '', '新货【三只松鼠_量贩碧根果1500g】特大健康坚果零食奶油味长寿果', 28, 59.90, 49.90, 39.90, 2000, 'https://gw.alicdn.com/imgextra/i3/2218288872763/O1CN01rN6Cn91WHVIflhWLg_!!2218288872763.jpg', '2', '三只耗子', NULL, NULL, 0);

SET FOREIGN_KEY_CHECKS = 1;

2.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: 21/09/2024 12:59:16
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for shop_specs
-- ----------------------------
DROP TABLE IF EXISTS `shop_specs`;
CREATE TABLE `shop_specs`  (
  `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `goods_id` int NULL DEFAULT NULL COMMENT '规格对应的商品id',
  `price` decimal(10, 2) NULL DEFAULT NULL COMMENT '对应规格的价格',
  `sku1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '对应的规格',
  `sku2` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '对应的规格',
  `sku3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '对应的规格',
  `sku4` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '对应的规格',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
  `is_deleted` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '逻辑删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of shop_specs
-- ----------------------------
INSERT INTO `shop_specs` VALUES (1, 1, 39.90, '1', '2', NULL, NULL, NULL, NULL, '0');
INSERT INTO `shop_specs` VALUES (2, 1, 29.90, '1', '4', NULL, NULL, NULL, NULL, '0');
INSERT INTO `shop_specs` VALUES (3, 1, 59.90, '3', '2', NULL, NULL, NULL, NULL, '0');
INSERT INTO `shop_specs` VALUES (4, 1, 49.00, '3', '4', NULL, NULL, NULL, NULL, '0');

SET FOREIGN_KEY_CHECKS = 1;

2.3 商品规格表

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: 21/09/2024 12:59:04
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for shop_sku
-- ----------------------------
DROP TABLE IF EXISTS `shop_sku`;
CREATE TABLE `shop_sku`  (
  `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `attr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '属性名',
  `attr_value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '属性值',
  `create_time` datetime NULL DEFAULT NULL,
  `update_time` datetime NULL DEFAULT NULL,
  `is_deleted` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of shop_sku
-- ----------------------------
INSERT INTO `shop_sku` VALUES (1, '重量', '1kg', NULL, NULL, '0');
INSERT INTO `shop_sku` VALUES (2, '大小', '大果', NULL, NULL, '0');
INSERT INTO `shop_sku` VALUES (3, '重量', '2kg', NULL, NULL, '0');
INSERT INTO `shop_sku` VALUES (4, '大小', '中果', NULL, NULL, '0');

SET FOREIGN_KEY_CHECKS = 1;

3、后端代码

3.1 model


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;

}

shop 表

java 复制代码
package com.zhong.model.entity.shop;

import java.io.Serializable;
import java.math.BigDecimal;

import com.zhong.model.entity.BaseEntity;
import lombok.Data;

/**
 * 
 * @TableName shop 商品信息
 */
@Data
public class Shop extends BaseEntity {

    /**
     * 标签/自营?
     */
    private String businessName;

    /**
     * 商品简介
     */
    private String title;

    /**
     * 已售数量
     */
    private Integer saleNumber;

    /**
     * 旧价格
     */
    private BigDecimal oldPrice;

    /**
     * 现售价
     */
    private BigDecimal newPrice;

    /**
     * 进货价
     */
    private BigDecimal inPrice;

    /**
     * 库存
     */
    private Integer inventoryNum;

    /**
     * 商品主图
     */
    private String mainImage;

    /**
     * 商品分类
     */
    private String shopType;

    /**
     * 店铺名称
     */
    private String shopName;

    private static final long serialVersionUID = 1L;
}

shop_img_swiper 表 轮播图

java 复制代码
package com.zhong.model.entity.shop;

import java.io.Serializable;

import com.zhong.model.entity.BaseEntity;
import lombok.Data;

/**
 * 
 * @TableName shop_img_swiper
 */
@Data
public class ShopImgSwiper extends BaseEntity {

    /**
     * 图片地址
     */
    private String url;

    /**
     * 商品id
     */
    private Integer fatherId;

    private static final long serialVersionUID = 1L;
}

shop_img_info表 商品详情图片

java 复制代码
package com.zhong.model.entity.shop;

import java.io.Serializable;

import com.zhong.model.entity.BaseEntity;
import lombok.Data;

/**
 * 
 * @TableName shop_img_info
 */
@Data
public class ShopImgInfo extends BaseEntity {


    /**
     * 图片地址
     */
    private String url;

    /**
     * 商品id
     */
    private Integer fatherId;

    private static final long serialVersionUID = 1L;
}

shop_specs 表 商品规格与商品中间表

java 复制代码
package com.zhong.model.entity.shop;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;

import com.zhong.model.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

/**
 * 
 * @TableName shop_specs
 */
@TableName(value ="shop_specs")
@Data
public class ShopSpecs extends BaseEntity {
    @Schema(description = "主键")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * 规格对应的商品id
     */
    private Integer goodsId;

    /**
     * 对应规格的价格
     */
    private BigDecimal price;

    /**
     * 对应的规格
     */
    private String sku1;

    /**
     * 对应的规格
     */
    private String sku2;

    /**
     * 对应的规格
     */
    private String sku3;

    /**
     * 对应的规格
     */
    private String sku4;

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}

shop_sku 商品规格

java 复制代码
package com.zhong.model.entity.shop;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.zhong.model.entity.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

/**
 * 
 * @TableName shop_sku
 */
@TableName(value ="shop_sku")
@Data
public class ShopSku extends BaseEntity {
    @Schema(description = "主键")
    @TableId(value = "id", type = IdType.AUTO)
    @JsonIgnore
    private Long id;
    /**
     * 属性名
     */
    private String attr;

    /**
     * 属性值
     */
    private String attrValue;


    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}

3.2 vo


ShopVo

java 复制代码
package com.zhong.vo.shop;

import com.zhong.model.entity.shop.Shop;
import com.zhong.model.entity.shop.ShopSpecs;
import lombok.Data;

import java.util.List;

/**
 * @ClassName : ShopVo
 * @Description :
 * @Author : zhx
 * @Date: 2024-09-15 18:55
 */
@Data
public class ShopVo extends Shop {
    private List<String> shopImgSwiper;
    private List<String> shopImgInfo;
    private List<ShopSpecs> shopSpecs;
}

ShopInfoVo

java 复制代码
package com.zhong.vo.shop;

import com.zhong.model.entity.shop.Shop;
import com.zhong.model.entity.shop.ShopSku;
import com.zhong.model.entity.shop.ShopSpecs;
import lombok.Data;

import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @ClassName : ShopVo
 * @Description :
 * @Author : zhx
 * @Date: 2024-09-15 18:55
 */
@Data
public class ShopInfoVo extends Shop {
    private List<String> shopImgSwiper;
    private List<String> shopImgInfo;
    // 所有规格信息 方便渲染
    private List<Map<String, Object>> skuGroup;
    // 规格信息对应价格
    private List<ShopSkuVo> shopSpecs;
}

ShopSkuVo

java 复制代码
package com.zhong.vo.shop;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.zhong.model.entity.shop.ShopSku;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.math.BigDecimal;
import java.util.List;

/**
 * @ClassName : ShopSkuVo
 * @Description :
 * @Author : zhx
 * @Date: 2024-09-20 10:46
 */
@Data
public class ShopSkuVo {
    @Schema(description = "主键")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    private BigDecimal price;

    private List<ShopSku> skus;
}

SkuGroupVo

c 复制代码
package com.zhong.vo.shop;

import lombok.Data;


import java.util.List;

/**
 * @ClassName : skuGroupVo
 * @Description :
 * @Author : zhx
 * @Date: 2024-09-20 13:49
 */
@Data
public class SkuGroupVo {
    private String skuKey;
    private List<String> skuValue;
}

3.3 mapper、server、serverImp

依赖mybatis自动生成,生成后将文件剪贴到对应位置即可。model 也可以自动生成 只不过要稍加修改



3.4 controller


ShopController

java 复制代码
package com.zhong.controller.shop;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.zhong.model.entity.shop.Shop;
import com.zhong.result.Result;
import com.zhong.service.ShopService;
import com.zhong.vo.shop.ShopInfoVo;
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.*;

/**
 * @ClassName : ShopController
 * @Description :
 * @Author : zhx
 * @Date: 2024-09-15 18:23
 */

@RestController
@RequestMapping("/app/shop/")
@Tag(name = "商品信息")
public class ShopController {

    @Autowired
    private ShopService service;

    @Operation(summary = "分页获取商品信息")
    @GetMapping("listItem")
    public Result<Page<Shop>> listItem(@RequestParam long current, @RequestParam long size) {
        LambdaQueryWrapper<Shop> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Shop::getIsDeleted, "0");
        Page<Shop> shopPage = new Page<>(current, size);
        Page<Shop> page = service.page(shopPage, queryWrapper);
        return Result.ok(page);
    }

    @Operation(summary = "根据id获取商品详细信息")
    @GetMapping("getDetailById")
    public Result<ShopInfoVo> getDetailById(@RequestParam Long id) {
        ShopInfoVo shopInfoVo = service.getDetailById(id);
        return Result.ok(shopInfoVo);
    }
}

ShopService

c 复制代码
package com.zhong.service;


import com.baomidou.mybatisplus.extension.service.IService;
import com.zhong.model.entity.shop.Shop;
import com.zhong.vo.shop.ShopInfoVo;
import com.zhong.vo.shop.ShopVo;

/**
* @author zhong
* @description 针对表【shop】的数据库操作Service
* @createDate 2024-09-15 18:18:13
*/
public interface ShopService extends IService<Shop> {

    ShopInfoVo getDetailById(Long id);
}

ShopServiceImpl

java 复制代码
package com.zhong.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zhong.mapper.shop.ShopMapper;
import com.zhong.model.entity.shop.Shop;
import com.zhong.model.entity.shop.ShopSku;
import com.zhong.model.entity.shop.ShopSpecs;
import com.zhong.service.ShopService;
import com.zhong.service.ShopSkuService;
import com.zhong.vo.shop.ShopInfoVo;
import com.zhong.vo.shop.ShopSkuVo;
import com.zhong.vo.shop.ShopVo;
import com.zhong.vo.shop.SkuGroupVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

/**
 * @author zhong
 * @description 针对表【shop】的数据库操作Service实现
 * @createDate 2024-09-15 18:18:13
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop>
        implements ShopService {

    @Autowired
    private ShopMapper shopMapper;

    @Autowired
    private ShopSkuService service;

    @Override
    public ShopInfoVo getDetailById(Long id) {
        // 返回的数据vo
        ShopInfoVo shopInfoVo = new ShopInfoVo();
        // 根据商品ID查询信息
        ShopVo detailById = shopMapper.getDetailById(id);
        // 获取查询到的商品信息的规格列表
        List<ShopSpecs> shopSpecs = detailById.getShopSpecs();
        // 新建一个vo对象保存 规格信息对应价格和列表
        List<ShopSkuVo> shopSkuLists = new ArrayList<>();
//        // 新建一个顶层数组 group vo 方便添加
//        List<List<SkuGroupVo>> skuGroupVos = new ArrayList<>();
        // 遍历每一个规格信息
        shopSpecs.forEach(x -> {
//            // 新建一个 二层数组 group vo 方便添加
//            List<SkuGroupVo> skuGroupList = new ArrayList<>();
//            // 新建一个 三层数组 group vo 方便添加
//            SkuGroupVo skuGroup = new SkuGroupVo();

            // 新建一个规格信息 Vo 方便后续添加到 listVo
            ShopSkuVo shopSkuVo = new ShopSkuVo();
            // 新建一个规格详情信息 Vo 方便后续添加到 shopSkuVo
            List<ShopSku> skuArrayList = new ArrayList<>();
            // 获取 skuGroupVo 的 key
            List<String> skuGroupKey = new ArrayList<>();
            // 获取 skuGroupVo 的 value
            List<String> skuGroupValue = new ArrayList<>();

            // 判断是否含有第一种规格
            if (x.getSku1() != null) {
                // 新建一个 shopSku 方便添加到 List<ShopSku>
                ShopSku shopSku = new ShopSku();
                // 根据规格ID获取规格详情
                ShopSku sku = service.getById(x.getSku1());
                // 将规格ID详情添加到 shopSkuVo
                shopSkuVo.setId(x.getId());
                // 将规格价格详情添加到 shopSkuVo
                shopSkuVo.setPrice(x.getPrice());
                // 将规格标题添加到 shopSku
                shopSku.setAttr(sku.getAttr());
                skuGroupKey.add(sku.getAttr());
                skuGroupValue.add(sku.getAttrValue());
                // 将规格值添加到 shopSku
                shopSku.setAttrValue(sku.getAttrValue());
                // 添加到规格详情List  List<ShopSku>
                skuArrayList.add(shopSku);
            }
            // 判断是否含有第二种规格
            if (x.getSku2() != null) {
                // 新建一个 shopSku 方便添加到 List<ShopSku>
                ShopSku shopSku = new ShopSku();
                // 根据规格ID获取规格详情
                ShopSku sku = service.getById(x.getSku2());
                // 将规格ID详情添加到 shopSkuVo
                shopSkuVo.setId(x.getId());
                // 将规格价格详情添加到 shopSkuVo
                shopSkuVo.setPrice(x.getPrice());
                // 将规格标题添加到 shopSku
                shopSku.setAttr(sku.getAttr());
                skuGroupKey.add(sku.getAttr());
                skuGroupValue.add(sku.getAttrValue());
                // 将规格值添加到 shopSku
                shopSku.setAttrValue(sku.getAttrValue());
                // 添加到规格详情List  List<ShopSku>
                skuArrayList.add(shopSku);
            }
            // 判断是否含有第三种规格
            if (x.getSku3() != null) {
                // 新建一个 shopSku 方便添加到 List<ShopSku>
                ShopSku shopSku = new ShopSku();
                // 根据规格ID获取规格详情
                ShopSku sku = service.getById(x.getSku3());
                // 将规格ID详情添加到 shopSkuVo
                shopSkuVo.setId(x.getId());
                // 将规格价格详情添加到 shopSkuVo
                shopSkuVo.setPrice(x.getPrice());
                // 将规格标题添加到 shopSku
                shopSku.setAttr(sku.getAttr());
                skuGroupKey.add(sku.getAttr());
                skuGroupValue.add(sku.getAttrValue());
                // 将规格值添加到 shopSku
                shopSku.setAttrValue(sku.getAttrValue());
                // 添加到规格详情List  List<ShopSku>
                skuArrayList.add(shopSku);
            }
            // 判断是否含有第四种规格
            if (x.getSku4() != null) {
                // 新建一个 shopSku 方便添加到 List<ShopSku>
                ShopSku shopSku = new ShopSku();
                // 根据规格ID获取规格详情
                ShopSku sku = service.getById(x.getSku4());
                // 将规格ID详情添加到 shopSkuVo
                shopSkuVo.setId(x.getId());
                // 将规格价格详情添加到 shopSkuVo
                shopSkuVo.setPrice(x.getPrice());
                // 将规格标题添加到 shopSku
                shopSku.setAttr(sku.getAttr());
                skuGroupKey.add(sku.getAttr());
                skuGroupValue.add(sku.getAttrValue());
                // 将规格值添加到 shopSku
                shopSku.setAttrValue(sku.getAttrValue());
                // 添加到规格详情List  List<ShopSku>
                skuArrayList.add(shopSku);
            }
            shopSkuVo.setSkus(skuArrayList);
            shopSkuLists.add(shopSkuVo);
        });

        BeanUtils.copyProperties(detailById, shopInfoVo);
        shopInfoVo.setShopSpecs(shopSkuLists);

        List<Map<String, Object>> skuGroup = computeSkuGroup(shopSkuLists);
        shopInfoVo.setSkuGroup(skuGroup);
        return shopInfoVo;
    }

    public List<Map<String, Object>> computeSkuGroup(List<ShopSkuVo> shopSpecs) {
        // 用于存放规格结果
        Map<String, Set<String>> resultMap = new LinkedHashMap<>();

        // 遍历每个商品的skus,提取属性和属性值
        for (ShopSkuVo item : shopSpecs) {
            for (ShopSku sku : item.getSkus()) {
                String attr = sku.getAttr();
                String attrValue = sku.getAttrValue();

                // 如果resultMap中不存在该属性,则初始化一个空的Set
                resultMap.putIfAbsent(attr, new LinkedHashSet<>());

                // 将属性值加入对应的Set
                resultMap.get(attr).add(attrValue);
            }
        }

        // 将结果格式化为所需的输出格式
        List<Map<String, Object>> finalResult = new ArrayList<>();
        for (Map.Entry<String, Set<String>> entry : resultMap.entrySet()) {
            Map<String, Object> temp = new HashMap<>();
            temp.put("key", entry.getKey());
            // 将Set转换为List并排序
            List<String> sortedValues = new ArrayList<>(entry.getValue());
            Collections.sort(sortedValues); // 对属性值进行排序

            // 创建一个包含info和isCheck的结构,假设第一个元素被选中
            List<Map<String, Object>> attrValueItems = new ArrayList<>();
            for (int i = 0; i < sortedValues.size(); i++) {
                Map<String, Object> valueMap = new HashMap<>();
                valueMap.put("info", sortedValues.get(i));
                valueMap.put("isCheck", i == 0); // 默认第一个值为true,其他为false
                attrValueItems.add(valueMap);
            }

            temp.put("value", attrValueItems);
            finalResult.add(temp);
        }

        // 输出结果
        System.out.println(finalResult);
        return finalResult;
    }
}

ShopMapper

java 复制代码
package com.zhong.mapper.shop;


import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zhong.model.entity.shop.Shop;
import com.zhong.vo.shop.ShopVo;
import org.apache.ibatis.annotations.Mapper;

/**
* @author zhong
* @description 针对表【shop】的数据库操作Mapper
* @createDate 2024-09-15 18:18:13
* @Entity generator.domain.Shop
*/
@Mapper
public interface ShopMapper extends BaseMapper<Shop> {

    ShopVo getDetailById(Long id);
}

> ShopMapper.xml

```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zhong.mapper.shop.ShopMapper">
    <resultMap id="ShopVoMap" type="com.zhong.vo.shop.ShopVo" autoMapping="true">
        <id column="id" property="id"/>
        <collection property="shopImgSwiper" ofType="string">
            <result column="siu"/>
        </collection>
        <collection property="shopImgInfo" ofType="string" autoMapping="true">
            <result column="ssu"/>
        </collection>
        <collection property="shopSpecs" ofType="com.zhong.model.entity.shop.ShopSpecs" autoMapping="true">
            <id column="sssid" property="id"/>
        </collection>
    </resultMap>
    <select id="getDetailById" resultMap="ShopVoMap">
        select sp.id,
               sp.business_name,
               sp.title,
               sp.sale_number,
               sp.old_price,
               sp.new_price,
               sp.in_price,
               sp.inventory_num,
               sp.main_image,
               sp.shop_type,
               sp.shop_name,
               si.url siu,
               ss.url ssu,
               sss.id sssid,
               sss.sku1,
               sss.sku2,
               sss.sku3,
               sss.sku4,
               sss.price
        from shop sp
                 left join shop_img_info si on sp.id = si.father_id and si.is_deleted = 0
                 left join shop_img_swiper ss on sp.id = ss.father_id and ss.is_deleted = 0
                 left join shop_specs sss on sp.id = sss.goods_id and sss.is_deleted = 0
        where sp.is_deleted = 0
          and sp.id = #{id}
    </select>
</mapper>

4、前端代码

4.1 request.js

js 复制代码
import axios from 'axios'

// 创建 axios 实例
const service = axios.create({
	// baseURL: import.meta.env.PROD ? import.meta.env.VITE_APP_BASE_URL : '/',  // 根据环境设置 baseURL
	baseURL: '/api', // 根据环境设置 baseURL
	// timeout: ResultEnum.TIMEOUT,
	timeout: 10000,
})

/**
 * @description: 请求拦截器
 */
service.interceptors.request.use(
	(config) => {
		// const userStore = useUserStore()
		// const token = userStore.token
		// if (token) {
		//   config.headers['access-token'] = token
		// }
		config.headers['access-token'] =
			"eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzUwMzcyNjUsInN1YiI6IkxPR0lOX1VTRVIiLCJ1c2VySWQiOjgsInVzZXJuYW1lIjoiMTMwMzY0OTc1NjIifQ.BJudKwWdt48h4eQaYRte2aXZWDYFRG7SHopRYSMr7D4"
		return config
	},
	(error) => {
		// 使用 uni.showToast 替换 ElMessage.error
		uni.showToast({
			title: error.message,
			icon: 'none',
			duration: 2000
		})
		return Promise.reject(error)
	}
)

/**
 * @description: 响应拦截器
 */
service.interceptors.response.use(
	(response) => {
		const {
			data
		} = response
		// console.log(data.code);
		// // * 登陆失效
		// if (ResultEnum.EXPIRE.includes(data.code)) {
		// 	RESEETSTORE()
		// 	uni.showToast({
		// 		title: data.message || ResultEnum.ERRMESSAGE,
		// 		icon: 'none',
		// 		duration: 2000
		// 	})
		// 	router.replace(LOGIN_URL)
		// 	return Promise.reject(data)
		// }

		if (data.code && data.code !== 200) {
			uni.showToast({
				title: data.message || ResultEnum.ERRMESSAGE,
				icon: 'none',
				duration: 2000
			})
			return Promise.reject(data)
		}
		return data.data
	},
	(error) => {
		// 处理 HTTP 网络错误
		let message = ''
		const status = error.response?.status
		switch (status) {
		      case 400:
		        message = "请求错误";
		        break;
		      case 401:
		        message = "未授权,请登录";
		        break;
		      case 403:
		        message = "拒绝访问";
		        break;
		      case 404:
		        message = `请求地址出错: ${error.response?.config?.url}`;
		        break;
		      case 408:
		        message = "请求超时";
		        break;
		      case 500:
		        message = "服务器内部错误";
		        break;
		      case 501:
		        message = "服务未实现";
		        break;
		      case 502:
		        message = "网关错误";
		        break;
		      case 503:
		        message = "服务不可用";
		        break;
		      case 504:
		        message = "网关超时";
		        break;
		      case 505:
		        message = "HTTP版本不受支持";
		        break;
		      default:
		        message = "网络连接故障";
		    }

		// 使用 uni.showToast 提示错误信息
		uni.showToast({
			title: message,
			icon: 'none',
			duration: 2000
		})
		return Promise.reject(error)
	}
)

/**
 * @description: 导出封装的请求方法
 */
const http = {
	get(url, params) {
		return service.get(url, params)
	},

	post(url, data) {
		return service.post(url, data)
	},

	put(url, data) {
		return service.put(url, data)
	},

	delete(url, data) {
		return service.delete(url, {
			data
		})
	}
}

export default http

4.2 index.js

js 复制代码
import http from '@/utils/request.js';

export const getAllShopApi = (params) => {
	return http.get(`/app/shop/listItem?current=${params.current}&size=${params.size}`)
}

export const getShopByIdApi = (id) => {
	return http.get(`/app/shop/getDetailById?id=${id}`)
}

4.3 shop-info.vue

js 复制代码
<template>
	<view>
		<template v-for="(item, index) in [data]" :key="index">
			<view class="">
				<up-swiper :list="item.shopImgSwiper" circular :autoplay="true" bgColor="#ffffff" height="360rpx"
					imgMode="auto"></up-swiper>
			</view>

			<view class="connect card card-shadow">
				<view class="price">
					<view class="">
						<up-text mode="price" :text="item.newPrice" color="red" size="24"></up-text>
					</view>
					<view class="">
						<text>已售{{item.saleNumber}}</text>
					</view>
				</view>
				<!-- 标题 -->
				<view class="title">
					<up-row customStyle="margin-bottom: 10px">
						<up-col span="2" v-if="item.businessName">
							<view class="" style="display: flex;">
								<up-tag :text="item.businessName" size="mini" type="error"></up-tag>
							</view>
						</up-col>
						<up-col :span="item.businessName?10 :12">
							<text>{{item.title}}</text>
						</up-col>
					</up-row>
				</view>
				<!-- 发货 -->
				<view class="logistics flex" style=" position: relative;">
					<up-icon name="car"></up-icon>
					<view class="" style="width: 20rpx;"></view>
					<view class="font-lite-size">
						<text>承诺24小时内发货,晚发必赔</text>
					</view>
					<view class="" style="position: absolute;right: 10rpx;">
						<up-icon name="arrow-right"></up-icon>
					</view>
				</view>

				<!-- 破损 -->
				<view class="pock flex" style=" position: relative;">
					<up-icon name="car"></up-icon>
					<view class="" style="width: 20rpx;"></view>
					<view class="font-lite-size">
						<text>破损包退 | 退货运费险 | 极速退款 | 7天无理由退换</text>
					</view>
					<view class="" style="position: absolute;right: 10rpx;">
						<up-icon name="arrow-right" size="16"></up-icon>
					</view>
				</view>
			</view>

			<!-- 评价 -->
			<view class="card card-shadow">
				<ShopCommentVue></ShopCommentVue>
			</view>

			<!-- 店铺信息 -->
			<view class="card card-shadow">
				<StoreInformationVue></StoreInformationVue>
			</view>
			<!-- 商品详情图片 -->
			<view class="bb-info card card-shadow" v-if="data.shopImgInfo.length> 0">
				<ShopInfoImageListVue :imgList="data.shopImgInfo"></ShopInfoImageListVue>
			</view>
			<!-- 提示 -->
			<view class="tips card card-shadow">
				<ShopTipsVue></ShopTipsVue>
			</view>
			<!-- 底部tabbar安全距离 -->
			<view class="" style="height: 140rpx;">

			</view>
		</template>
		<!-- 加入购物车等操作 -->
		<view class="bottom">
			<ShopBottomButtonVue :data="data"></ShopBottomButtonVue>
		</view>
	</view>
</template>

<script setup>
	import {
		reactive,
		ref,
		onMounted
	} from 'vue';

	import ShopCommentVue from '@/pages/components/Home/ShopComment.vue';
	import StoreInformationVue from '@/pages/components/Home/StoreInformation.vue';
	import ShopInfoImageListVue from '@/pages/components/Home/ShopInfoImageList.vue';
	import ShopTipsVue from '@/pages/components/Home/ShopTips.vue';
	import ShopBottomButtonVue from '@/pages/components/Home/ShopBottomButton.vue';
	import {
		onLoad
	} from "@dcloudio/uni-app"
	import {
		getShopByIdApi
	} from "@/pages/api/shop/index.js"
	const shopId = ref();
	const data = ref();
	onLoad((options) => {
		shopId.value = options.id;
	})
	onMounted(async () => {
		console.log(shopId.value);
		let res = await getShopByIdApi(shopId.value);
		data.value = res;
		console.log(res);
	})
	// 父组件中的价格数据
	const price = ref(null);
	// 处理子组件传来的价格更新
	const handlePriceUpdate = (newPrice) => {
	  price.value = newPrice;
	};
</script>

<style lang="less" scoped>
	.card-shadow {
		border-radius: 20rpx;
		box-shadow: 10rpx 10rpx 10rpx 10rpx rgba(0.2, 0.1, 0.2, 0.2);
	}

	.card {
		margin: 20rpx;
		padding: 20rpx;
		background-color: #FFF;
		border-radius: 20rpx;
	}

	.font-lite-size {
		font-size: 26rpx;
	}

	.flex {
		display: flex;
		align-items: center;
	}

	.title {
		margin-top: 20rpx;
	}

	.pock {
		margin: 20rpx 0;
	}

	.price {
		padding-right: 20rpx;
		display: flex;
		justify-content: space-between;
		align-items: center;
	}
</style>

4.4 ShopBottomButton.vue

js 复制代码
<template>
	<view class="mains">
		<view class="connect">
			<view class="letf-connect">
				<up-icon name="gift" size="40rpx"></up-icon>
				<text style="font-size: 26rpx;">店铺</text>
			</view>
			<view class="letf-connect">
				<up-icon name="kefu-ermai" size="40rpx"></up-icon>
				<text style="font-size: 26rpx;">客服</text>
			</view>
			<view class="letf-connect" @click="toShopCart">
				<up-icon name="shopping-cart" size="40rpx"></up-icon>
				<text style="font-size: 26rpx;">购物车</text>
			</view>

			<view class="" style="display: flex;flex: 1;padding-left: 20rpx;">
				<up-button text="加入购物车" type="warning" @click="show=true"></up-button>
				<up-button text="立即购买" type="success"></up-button>
			</view>
		</view>

		<!-- 弹出层选择商品规格 -->
		<up-popup :show="show" mode="bottom" :round="10" @close="close" @open="open">
			<view>
				<view class="top">
					<up-image :src="props.data.mainImage" width="200rpx" height="300rpx" radius="10"></up-image>
					<view style="padding-left: 40rpx;">
						<text style="flex: 1;overflow: hidden;">{{props.data.title}}</text>
						<view style="padding: 20rpx 0;">
							<up-text mode="price" :text="calculatedPrice" color="red" size="20"></up-text>
						</view>
						<view style="display: flex;padding-top: 20rpx;">
							<up-number-box v-model="shopNum" min="1"></up-number-box>
						</view>
					</view>
				</view>

				<!-- 渲染规格 -->
				<view class="">
					<template v-for="(item,index) in resSkuGroup">
						<view style="padding-left: 20rpx;">{{item.key}}</view>
						<view style="display: flex;">
							<template v-for="(tag,i) in item.value" :key="i">
								<view class="" style="display: flex;padding:20rpx;">
									<up-tag :text="tag.info" :plain="!tag.isCheck" :color="tag.isCheck?'#FFF':'#000'"
										:borderColor="tag.isCheck?'#FFF':'#000'" type="error"
										@click="changeTagIsCheckFun(tag,index)"></up-tag>
								</view>
							</template>
						</view>
					</template>
				</view>

				<view class="" style="padding: 20rpx;">
					<up-button text="加入购物车" shape="circle" type="error" @click="addCartFun"></up-button>
				</view>
			</view>
		</up-popup>
	</view>
</template>

<script setup>
	import {
		computed,
		onMounted,
		reactive,
		defineEmits,
		watch,
		ref
	} from 'vue';
	// 创建响应式数据
	const show = ref(false);
	const resData = ref();
	const resSkuData = ref();
	const resSkuGroup = ref();
	const resDataFun = async() => {
		resSkuGroup.value = await props.data.skuGroup;
		resSkuData.value = await props.data.shopSpecs;
		console.log(props.data.shopSpecs);
		console.log( resSkuData.value);
	}
	const changeTagIsCheckFun = (item, index) => {
		resSkuGroup.value[index].value.map(x => {
			if (x.info == item.info) {
				x.isCheck = true;
			} else {
				x.isCheck = false;
			}
		})
		console.log(resSkuGroup.value);
	}

	// 通过 computed 计算选中的属性值
	const checkedAttributes = computed(() => {
	  return resSkuGroup.value.map(option => ({
	    attr: option.key,
	    attrValue: option.value.find(item => item.isCheck)?.info || null
	  }));
	});
	// 商品数量
	const shopNum = ref(1);
	// 根据选中的属性值匹配 SKU,计算出价格
	const calculatedPrice = computed(() => {
		if (resSkuData.value) {
			// 找到与选中属性完全匹配的 SKU
			const matchingSku = resSkuData.value.find(sku => {
			  return sku.skus.every(skuAttr => {
			    return checkedAttributes.value.some(attr => 
			      attr.attr === skuAttr.attr && attr.attrValue === skuAttr.attrValue
			    );
			  });
			});
				
			// 返回匹配的价格,没有匹配的返回 null
			return matchingSku ? matchingSku.price * shopNum.value: null;
		}
	});
	// // 定义 emit
	// const emit = defineEmits(['updatePrice']);
	// // 监听计算的价格变化并向父组件传值
	// watch(calculatedPrice, (newPrice) => {
	
	// 	emit('updatePrice', newPrice);
	// });
	const toCartData = reactive({
		num: 1,
	})
	const props = defineProps({
		data: Object
	});

	onMounted(() => {
		console.log(props.data);
		resDataFun();
	})
	// 定义方法  
	const open = () => {
		// 打开逻辑,比如设置 show 为 true  
		show.value = true;
		// console.log('open');  
	}

	const close = () => {
		// 关闭逻辑,设置 show 为 false  
		show.value = false;
		// console.log('close');  
	}
	const toShopCart = () => {
		uni.navigateTo({
			url: "/pages/src/home/shop-cart/shop-cart"
		})
	}
	const addCartFun = () => {
		close();
	}
</script>

<style lang="scss" scoped>
	.top {
		display: flex;
		padding: 40rpx;
	}

	.mains {
		position: fixed;
		bottom: 0;
		left: 0;
		width: 100%;
		/* 占据全宽 */
		height: 120rpx;
		/* Tabbar 高度 */
		background-color: #FFF;
		border-top: 2rpx solid #7d7e80;
	}

	.connect {
		display: flex;
		justify-content: space-around;
		padding: 20rpx;
		align-items: center;
	}

	.letf-connect {
		padding: 0 10rpx;
		display: flex;
		flex-direction: column;
		align-items: center;
	}
</style>
相关推荐
行十万里人生几秒前
Qt 对象树详解:从原理到运用
开发语言·数据库·qt·华为od·华为·华为云·harmonyos
松岛的枫叶8 分钟前
【缓冲区】数据库备份的衍生问题,缓冲区在哪里?JVMor操作系统?(二)
数据库
obboda36 分钟前
使用haproxy实现MySQL服务器负载均衡
服务器·mysql·负载均衡
鸠摩智首席音效师43 分钟前
解决 ERROR 1130 (HY000): Host is not allowed to connect to this MySQL server
mysql
littlegirll1 小时前
命令行方式安装KFS同步KES到KADB
java·大数据·数据库
Y编程小白2 小时前
MySQL的存储引擎
数据库·mysql
爱老的虎油2 小时前
MySQL零基础教程10—正则表达式搜索(下)
数据库·mysql·正则表达式
️Carrie️2 小时前
6.6.3 SQL数据查询(一)
数据库·sql·select·子查询·连接查询·简单查询·聚集函数
jay丿2 小时前
Django应用的高级配置和管理
数据库·django·sqlite
虎鲸不是鱼2 小时前
【全栈开发】从0开始搭建一个图书管理系统【一】框架搭建
java·spring boot·spring·maven·mybatis