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>