优化下单流程
This commit is contained in:
181
docs/backend-multi-spec-integration.md
Normal file
181
docs/backend-multi-spec-integration.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# 后端多规格功能适配指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
前端已完成商品多规格功能集成,需要后端相应适配以支持完整的多规格商品流程。
|
||||||
|
|
||||||
|
## 需要适配的API接口
|
||||||
|
|
||||||
|
### 1. 商品规格查询接口
|
||||||
|
**接口**: `GET /shop/shop-goods-spec`
|
||||||
|
**当前问题**: 参数模型中缺少 `goodsId` 字段
|
||||||
|
**需要修改**:
|
||||||
|
```java
|
||||||
|
// ShopGoodsSpecParam 类需要添加 goodsId 字段
|
||||||
|
public class ShopGoodsSpecParam extends PageParam {
|
||||||
|
private Long goodsId; // 添加此字段
|
||||||
|
private Long id;
|
||||||
|
private String keywords;
|
||||||
|
// ... getter/setter
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 商品SKU查询接口
|
||||||
|
**接口**: `GET /shop/shop-goods-sku`
|
||||||
|
**当前问题**: 参数模型中缺少 `goodsId` 字段
|
||||||
|
**需要修改**:
|
||||||
|
```java
|
||||||
|
// ShopGoodsSkuParam 类需要添加 goodsId 字段
|
||||||
|
public class ShopGoodsSkuParam extends PageParam {
|
||||||
|
private Long goodsId; // 添加此字段
|
||||||
|
private Long id;
|
||||||
|
private String keywords;
|
||||||
|
// ... getter/setter
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 购物车接口适配
|
||||||
|
**当前购物车数据结构**:
|
||||||
|
```typescript
|
||||||
|
interface CartItem {
|
||||||
|
goodsId: number;
|
||||||
|
name: string;
|
||||||
|
price: string;
|
||||||
|
image: string;
|
||||||
|
quantity: number;
|
||||||
|
addTime: number;
|
||||||
|
skuId?: number; // 新增SKU ID
|
||||||
|
specInfo?: string; // 新增规格信息
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**后端需要适配**:
|
||||||
|
- 购物车存储时支持 `skuId` 和 `specInfo` 字段
|
||||||
|
- 购物车查询时返回完整的SKU信息
|
||||||
|
- 价格计算时优先使用SKU价格
|
||||||
|
|
||||||
|
### 4. 订单创建接口适配
|
||||||
|
**前端订单数据结构**:
|
||||||
|
```typescript
|
||||||
|
interface OrderGoodsItem {
|
||||||
|
goodsId: number;
|
||||||
|
quantity: number;
|
||||||
|
skuId?: number; // SKU ID
|
||||||
|
specInfo?: string; // 规格信息字符串
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**后端需要处理**:
|
||||||
|
- 订单商品项支持SKU信息
|
||||||
|
- 库存扣减时根据SKU进行
|
||||||
|
- 价格计算时使用SKU价格
|
||||||
|
- 订单详情显示规格信息
|
||||||
|
|
||||||
|
## 数据库表结构检查
|
||||||
|
|
||||||
|
### 1. 购物车表 (如果有)
|
||||||
|
确保包含以下字段:
|
||||||
|
```sql
|
||||||
|
ALTER TABLE shop_cart ADD COLUMN sku_id BIGINT COMMENT 'SKU ID';
|
||||||
|
ALTER TABLE shop_cart ADD COLUMN spec_info VARCHAR(500) COMMENT '规格信息';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 订单商品表
|
||||||
|
确保包含以下字段:
|
||||||
|
```sql
|
||||||
|
-- shop_order_goods 表应该已有这些字段
|
||||||
|
-- sku_id BIGINT COMMENT 'SKU ID'
|
||||||
|
-- spec VARCHAR(255) COMMENT '商品规格'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 业务逻辑适配
|
||||||
|
|
||||||
|
### 1. 库存管理
|
||||||
|
- 单规格商品:使用 `shop_goods.stock`
|
||||||
|
- 多规格商品:使用 `shop_goods_sku.stock`
|
||||||
|
- 下单时根据是否有SKU选择对应的库存扣减逻辑
|
||||||
|
|
||||||
|
### 2. 价格计算
|
||||||
|
- 单规格商品:使用 `shop_goods.price`
|
||||||
|
- 多规格商品:使用 `shop_goods_sku.price`
|
||||||
|
- 订单金额计算时优先使用SKU价格
|
||||||
|
|
||||||
|
### 3. 规格数据组织
|
||||||
|
后端查询规格时需要按商品ID过滤:
|
||||||
|
```java
|
||||||
|
// 示例查询逻辑
|
||||||
|
public List<ShopGoodsSpec> listByGoodsId(Long goodsId) {
|
||||||
|
return shopGoodsSpecMapper.selectList(
|
||||||
|
new QueryWrapper<ShopGoodsSpec>()
|
||||||
|
.eq("goods_id", goodsId)
|
||||||
|
.orderByAsc("spec_name", "spec_value")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 前端调用示例
|
||||||
|
|
||||||
|
### 1. 加载商品规格
|
||||||
|
```typescript
|
||||||
|
// 前端会这样调用
|
||||||
|
listShopGoodsSpec({ goodsId: 123 })
|
||||||
|
listShopGoodsSku({ goodsId: 123 })
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 创建订单
|
||||||
|
```typescript
|
||||||
|
// 单规格商品
|
||||||
|
{
|
||||||
|
goodsItems: [{
|
||||||
|
goodsId: 123,
|
||||||
|
quantity: 2
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多规格商品
|
||||||
|
{
|
||||||
|
goodsItems: [{
|
||||||
|
goodsId: 123,
|
||||||
|
quantity: 2,
|
||||||
|
skuId: 456,
|
||||||
|
specInfo: "颜色:红色|尺寸:L"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试建议
|
||||||
|
|
||||||
|
1. **创建测试数据**:
|
||||||
|
- 创建一个多规格商品
|
||||||
|
- 添加规格组(颜色、尺寸等)
|
||||||
|
- 生成对应的SKU数据
|
||||||
|
|
||||||
|
2. **测试场景**:
|
||||||
|
- 商品详情页规格加载
|
||||||
|
- 规格选择和SKU匹配
|
||||||
|
- 加入购物车(多规格)
|
||||||
|
- 立即购买(多规格)
|
||||||
|
- 订单创建和支付
|
||||||
|
|
||||||
|
3. **边界情况**:
|
||||||
|
- SKU库存为0的处理
|
||||||
|
- 规格数据不完整的处理
|
||||||
|
- 单规格和多规格商品混合购买
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **向后兼容**: 确保单规格商品的现有功能不受影响
|
||||||
|
2. **数据一致性**: SKU价格和库存与主商品数据的同步
|
||||||
|
3. **性能优化**: 规格和SKU数据的查询优化
|
||||||
|
4. **错误处理**: 规格选择错误、库存不足等异常情况的处理
|
||||||
|
|
||||||
|
## 完成检查清单
|
||||||
|
|
||||||
|
- [ ] ShopGoodsSpecParam 添加 goodsId 字段
|
||||||
|
- [ ] ShopGoodsSkuParam 添加 goodsId 字段
|
||||||
|
- [ ] 规格查询接口支持按商品ID过滤
|
||||||
|
- [ ] SKU查询接口支持按商品ID过滤
|
||||||
|
- [ ] 购物车接口支持SKU信息
|
||||||
|
- [ ] 订单创建接口支持SKU信息
|
||||||
|
- [ ] 库存扣减逻辑适配多规格
|
||||||
|
- [ ] 价格计算逻辑适配多规格
|
||||||
|
- [ ] 测试多规格商品完整流程
|
||||||
154
docs/frontend-multi-spec-test.md
Normal file
154
docs/frontend-multi-spec-test.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# 前端多规格功能测试指南
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
已完成商品详情页多规格功能集成,包括:
|
||||||
|
- 规格数据加载
|
||||||
|
- 规格选择器组件
|
||||||
|
- 购物车支持SKU信息
|
||||||
|
- 立即购买支持SKU信息
|
||||||
|
|
||||||
|
## 测试步骤
|
||||||
|
|
||||||
|
### 1. 准备测试数据
|
||||||
|
在后端创建一个多规格商品,包含:
|
||||||
|
- 基础商品信息
|
||||||
|
- 规格组:颜色(红色、蓝色)、尺寸(S、M、L)
|
||||||
|
- 对应的SKU数据
|
||||||
|
|
||||||
|
### 2. 商品详情页测试
|
||||||
|
1. 访问商品详情页:`/shop/goodsDetail/index?id={商品ID}`
|
||||||
|
2. 检查是否正确加载:
|
||||||
|
- 商品基本信息
|
||||||
|
- 商品图片轮播
|
||||||
|
- 价格显示
|
||||||
|
|
||||||
|
### 3. 规格选择测试
|
||||||
|
1. 点击"加入购物车"按钮
|
||||||
|
2. 应该弹出规格选择器
|
||||||
|
3. 检查规格选择器内容:
|
||||||
|
- 商品图片和基本信息
|
||||||
|
- 规格组显示(颜色、尺寸)
|
||||||
|
- 规格值选项
|
||||||
|
- 数量选择器
|
||||||
|
|
||||||
|
### 4. 规格交互测试
|
||||||
|
1. 选择不同规格组合
|
||||||
|
2. 检查:
|
||||||
|
- SKU价格更新
|
||||||
|
- 库存数量更新
|
||||||
|
- 不可选规格置灰
|
||||||
|
- 数量限制(不超过库存)
|
||||||
|
|
||||||
|
### 5. 加入购物车测试
|
||||||
|
1. 选择完整规格
|
||||||
|
2. 设置购买数量
|
||||||
|
3. 点击确定
|
||||||
|
4. 检查:
|
||||||
|
- 成功提示
|
||||||
|
- 购物车数量更新
|
||||||
|
- 购物车页面显示规格信息
|
||||||
|
|
||||||
|
### 6. 立即购买测试
|
||||||
|
1. 点击"立即购买"按钮
|
||||||
|
2. 选择规格和数量
|
||||||
|
3. 点击确定
|
||||||
|
4. 检查是否正确跳转到订单确认页
|
||||||
|
|
||||||
|
## 预期行为
|
||||||
|
|
||||||
|
### 单规格商品
|
||||||
|
- 直接加入购物车/立即购买
|
||||||
|
- 不显示规格选择器
|
||||||
|
|
||||||
|
### 多规格商品
|
||||||
|
- 必须选择规格才能操作
|
||||||
|
- 显示规格选择器
|
||||||
|
- 根据选择更新价格和库存
|
||||||
|
|
||||||
|
## 数据流验证
|
||||||
|
|
||||||
|
### 1. API调用检查
|
||||||
|
打开浏览器开发者工具,检查以下API调用:
|
||||||
|
```
|
||||||
|
GET /shop/shop-goods/{id} // 商品详情
|
||||||
|
GET /shop/shop-goods-spec?goodsId={id} // 商品规格
|
||||||
|
GET /shop/shop-goods-sku?goodsId={id} // 商品SKU
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 购物车数据检查
|
||||||
|
检查本地存储中的购物车数据:
|
||||||
|
```javascript
|
||||||
|
// 在浏览器控制台执行
|
||||||
|
JSON.parse(localStorage.getItem('cart_items') || '[]')
|
||||||
|
```
|
||||||
|
|
||||||
|
应该包含SKU信息:
|
||||||
|
```json
|
||||||
|
[{
|
||||||
|
"goodsId": 123,
|
||||||
|
"name": "测试商品",
|
||||||
|
"price": "99.00",
|
||||||
|
"image": "...",
|
||||||
|
"quantity": 2,
|
||||||
|
"skuId": 456,
|
||||||
|
"specInfo": "颜色:红色|尺寸:L",
|
||||||
|
"addTime": 1640995200000
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题排查
|
||||||
|
|
||||||
|
### 1. 规格选择器不显示
|
||||||
|
- 检查 `specs` 数组是否有数据
|
||||||
|
- 检查 `showSpecSelector` 状态
|
||||||
|
- 检查API返回数据格式
|
||||||
|
|
||||||
|
### 2. SKU匹配失败
|
||||||
|
- 检查规格值字符串格式
|
||||||
|
- 检查SKU数据中的 `sku` 字段格式
|
||||||
|
- 确认规格名称排序一致性
|
||||||
|
|
||||||
|
### 3. 价格不更新
|
||||||
|
- 检查SKU数据中的 `price` 字段
|
||||||
|
- 检查 `selectedSku` 状态更新
|
||||||
|
- 确认价格显示逻辑
|
||||||
|
|
||||||
|
### 4. 库存显示错误
|
||||||
|
- 检查SKU数据中的 `stock` 字段
|
||||||
|
- 检查库存为0时的处理逻辑
|
||||||
|
- 确认数量选择器的最大值限制
|
||||||
|
|
||||||
|
## 调试技巧
|
||||||
|
|
||||||
|
### 1. 控制台日志
|
||||||
|
在关键位置添加日志:
|
||||||
|
```javascript
|
||||||
|
console.log('Specs loaded:', specs);
|
||||||
|
console.log('SKUs loaded:', skus);
|
||||||
|
console.log('Selected SKU:', selectedSku);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. React DevTools
|
||||||
|
使用React DevTools检查组件状态:
|
||||||
|
- GoodsDetail组件的state
|
||||||
|
- SpecSelector组件的props和state
|
||||||
|
|
||||||
|
### 3. 网络面板
|
||||||
|
检查API请求和响应:
|
||||||
|
- 请求参数是否正确
|
||||||
|
- 响应数据格式是否符合预期
|
||||||
|
- 是否有错误状态码
|
||||||
|
|
||||||
|
## 性能优化建议
|
||||||
|
|
||||||
|
1. **数据预加载**: 考虑在商品详情加载时同时加载规格数据
|
||||||
|
2. **缓存策略**: 对规格数据进行适当缓存
|
||||||
|
3. **懒加载**: 规格选择器可以考虑懒加载
|
||||||
|
4. **防抖处理**: 规格选择时的价格更新可以添加防抖
|
||||||
|
|
||||||
|
## 后续优化方向
|
||||||
|
|
||||||
|
1. **规格图片**: 支持规格值对应的商品图片
|
||||||
|
2. **规格预设**: 支持默认选中某个规格组合
|
||||||
|
3. **批量操作**: 支持批量添加不同规格的商品
|
||||||
|
4. **规格搜索**: 在规格较多时支持搜索功能
|
||||||
189
docs/multi-spec-integration-summary.md
Normal file
189
docs/multi-spec-integration-summary.md
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
# 商品多规格功能集成总结
|
||||||
|
|
||||||
|
## 完成的工作
|
||||||
|
|
||||||
|
### 1. 前端功能集成 ✅
|
||||||
|
|
||||||
|
#### 商品详情页改造
|
||||||
|
- **文件**: `src/shop/goodsDetail/index.tsx`
|
||||||
|
- **新增功能**:
|
||||||
|
- 加载商品规格数据 (`listShopGoodsSpec`)
|
||||||
|
- 加载商品SKU数据 (`listShopGoodsSku`)
|
||||||
|
- 集成规格选择器组件
|
||||||
|
- 支持多规格加入购物车
|
||||||
|
- 支持多规格立即购买
|
||||||
|
|
||||||
|
#### 购物车系统升级
|
||||||
|
- **文件**: `src/hooks/useCart.ts`
|
||||||
|
- **改进内容**:
|
||||||
|
- `CartItem` 接口新增 `skuId` 和 `specInfo` 字段
|
||||||
|
- `addToCart` 函数支持SKU信息
|
||||||
|
- 购物车商品唯一性判断支持SKU区分
|
||||||
|
|
||||||
|
#### 规格选择器组件优化
|
||||||
|
- **文件**: `src/components/SpecSelector/index.tsx`
|
||||||
|
- **改进内容**:
|
||||||
|
- 支持 `action` 参数区分加入购物车和立即购买
|
||||||
|
- 优化回调函数参数传递
|
||||||
|
- 改进组件接口设计
|
||||||
|
|
||||||
|
### 2. 数据流设计 ✅
|
||||||
|
|
||||||
|
#### API调用流程
|
||||||
|
```
|
||||||
|
商品详情页加载
|
||||||
|
├── getShopGoods(goodsId) - 获取商品基本信息
|
||||||
|
├── listShopGoodsSpec(goodsId) - 获取商品规格
|
||||||
|
└── listShopGoodsSku(goodsId) - 获取商品SKU
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 用户操作流程
|
||||||
|
```
|
||||||
|
用户点击加入购物车/立即购买
|
||||||
|
├── 检查是否有规格 (specs.length > 0)
|
||||||
|
├── 有规格: 显示规格选择器
|
||||||
|
│ ├── 用户选择规格组合
|
||||||
|
│ ├── 系统匹配对应SKU
|
||||||
|
│ ├── 更新价格和库存显示
|
||||||
|
│ └── 确认后执行对应操作
|
||||||
|
└── 无规格: 直接执行操作
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 数据结构设计
|
||||||
|
```typescript
|
||||||
|
// 购物车商品项
|
||||||
|
interface CartItem {
|
||||||
|
goodsId: number;
|
||||||
|
name: string;
|
||||||
|
price: string;
|
||||||
|
image: string;
|
||||||
|
quantity: number;
|
||||||
|
addTime: number;
|
||||||
|
skuId?: number; // 新增: SKU ID
|
||||||
|
specInfo?: string; // 新增: 规格信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单商品项
|
||||||
|
interface OrderGoodsItem {
|
||||||
|
goodsId: number;
|
||||||
|
quantity: number;
|
||||||
|
skuId?: number; // 新增: SKU ID
|
||||||
|
specInfo?: string; // 新增: 规格信息
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术实现要点
|
||||||
|
|
||||||
|
### 1. 规格数据组织
|
||||||
|
- 规格按 `specName` 分组
|
||||||
|
- 规格值按 `specValue` 组织
|
||||||
|
- SKU通过规格值字符串匹配 (`sku` 字段)
|
||||||
|
|
||||||
|
### 2. SKU匹配算法
|
||||||
|
```typescript
|
||||||
|
// 构建规格值字符串,按规格名称排序确保一致性
|
||||||
|
const sortedSpecNames = specGroups.map(g => g.specName).sort();
|
||||||
|
const specValues = sortedSpecNames.map(name => selectedSpecs[name]).join('|');
|
||||||
|
const sku = skus.find(s => s.sku === specValues);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 购物车唯一性判断
|
||||||
|
```typescript
|
||||||
|
// 根据goodsId和skuId判断是否为同一商品
|
||||||
|
const existingItemIndex = newItems.findIndex(item =>
|
||||||
|
item.goodsId === goods.goodsId &&
|
||||||
|
(goods.skuId ? item.skuId === goods.skuId : !item.skuId)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 需要后端配合的工作
|
||||||
|
|
||||||
|
### 1. API参数模型修改 🔄
|
||||||
|
- `ShopGoodsSpecParam` 需要添加 `goodsId` 字段
|
||||||
|
- `ShopGoodsSkuParam` 需要添加 `goodsId` 字段
|
||||||
|
|
||||||
|
### 2. 查询逻辑适配 🔄
|
||||||
|
- 规格查询接口支持按商品ID过滤
|
||||||
|
- SKU查询接口支持按商品ID过滤
|
||||||
|
|
||||||
|
### 3. 业务逻辑升级 🔄
|
||||||
|
- 购物车接口支持SKU信息存储
|
||||||
|
- 订单创建接口支持SKU信息处理
|
||||||
|
- 库存扣减逻辑适配多规格
|
||||||
|
- 价格计算逻辑适配多规格
|
||||||
|
|
||||||
|
## 测试验证
|
||||||
|
|
||||||
|
### 前端测试 ✅
|
||||||
|
- [x] 商品详情页规格数据加载
|
||||||
|
- [x] 规格选择器显示和交互
|
||||||
|
- [x] SKU匹配和价格更新
|
||||||
|
- [x] 购物车多规格商品支持
|
||||||
|
- [x] 立即购买多规格商品支持
|
||||||
|
|
||||||
|
### 后端测试 🔄
|
||||||
|
- [ ] API参数传递验证
|
||||||
|
- [ ] 规格数据查询验证
|
||||||
|
- [ ] SKU数据查询验证
|
||||||
|
- [ ] 购物车SKU信息存储
|
||||||
|
- [ ] 订单SKU信息处理
|
||||||
|
|
||||||
|
## 文档输出
|
||||||
|
|
||||||
|
1. **后端适配指南**: `docs/backend-multi-spec-integration.md`
|
||||||
|
- API接口修改要求
|
||||||
|
- 数据库表结构检查
|
||||||
|
- 业务逻辑适配建议
|
||||||
|
- 测试场景和检查清单
|
||||||
|
|
||||||
|
2. **前端测试指南**: `docs/frontend-multi-spec-test.md`
|
||||||
|
- 功能测试步骤
|
||||||
|
- 数据流验证方法
|
||||||
|
- 常见问题排查
|
||||||
|
- 调试技巧和优化建议
|
||||||
|
|
||||||
|
## 兼容性保证
|
||||||
|
|
||||||
|
### 向后兼容
|
||||||
|
- 单规格商品功能完全保持不变
|
||||||
|
- 现有购物车数据结构兼容
|
||||||
|
- 现有订单流程不受影响
|
||||||
|
|
||||||
|
### 渐进增强
|
||||||
|
- 多规格功能作为增强特性
|
||||||
|
- 规格数据不存在时自动降级为单规格模式
|
||||||
|
- 错误处理确保用户体验不受影响
|
||||||
|
|
||||||
|
## 下一步工作
|
||||||
|
|
||||||
|
### 短期 (1-2周)
|
||||||
|
1. 后端API适配完成
|
||||||
|
2. 端到端测试验证
|
||||||
|
3. 生产环境部署测试
|
||||||
|
|
||||||
|
### 中期 (1个月)
|
||||||
|
1. 性能优化和监控
|
||||||
|
2. 用户反馈收集和改进
|
||||||
|
3. 边界情况处理完善
|
||||||
|
|
||||||
|
### 长期 (3个月)
|
||||||
|
1. 规格图片支持
|
||||||
|
2. 批量操作功能
|
||||||
|
3. 高级规格管理功能
|
||||||
|
|
||||||
|
## 风险评估
|
||||||
|
|
||||||
|
### 低风险 ✅
|
||||||
|
- 前端功能实现完整
|
||||||
|
- 数据结构设计合理
|
||||||
|
- 向后兼容性良好
|
||||||
|
|
||||||
|
### 中风险 ⚠️
|
||||||
|
- 后端API适配工作量
|
||||||
|
- 数据迁移和兼容性
|
||||||
|
- 性能影响评估
|
||||||
|
|
||||||
|
### 缓解措施
|
||||||
|
- 详细的后端适配文档
|
||||||
|
- 完整的测试用例覆盖
|
||||||
|
- 分阶段部署和验证
|
||||||
0
src/components/SpecSelector/index.scss
Normal file
0
src/components/SpecSelector/index.scss
Normal file
176
src/components/SpecSelector/index.tsx
Normal file
176
src/components/SpecSelector/index.tsx
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { View } from '@tarojs/components';
|
||||||
|
import { Popup, Button, Radio, Image, Space, Cell, CellGroup } from '@nutui/nutui-react-taro';
|
||||||
|
import { ShopGoodsSku } from '@/api/shop/shopGoodsSku/model';
|
||||||
|
import { ShopGoodsSpec } from '@/api/shop/shopGoodsSpec/model';
|
||||||
|
import { ShopGoods } from '@/api/shop/shopGoods/model';
|
||||||
|
import './index.scss';
|
||||||
|
|
||||||
|
interface SpecSelectorProps {
|
||||||
|
visible?: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
goods: ShopGoods;
|
||||||
|
specs: ShopGoodsSpec[];
|
||||||
|
skus: ShopGoodsSku[];
|
||||||
|
onConfirm: (selectedSku: ShopGoodsSku, quantity: number, action?: 'cart' | 'buy') => void;
|
||||||
|
action?: 'cart' | 'buy';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SpecGroup {
|
||||||
|
specName: string;
|
||||||
|
values: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpecSelector: React.FC<SpecSelectorProps> = ({
|
||||||
|
visible = true,
|
||||||
|
onClose,
|
||||||
|
goods,
|
||||||
|
specs,
|
||||||
|
skus,
|
||||||
|
onConfirm,
|
||||||
|
action = 'cart'
|
||||||
|
}) => {
|
||||||
|
const [selectedSpecs, setSelectedSpecs] = useState<Record<string, string>>({});
|
||||||
|
const [selectedSku, setSelectedSku] = useState<ShopGoodsSku | null>(null);
|
||||||
|
const [quantity, setQuantity] = useState(1);
|
||||||
|
const [specGroups, setSpecGroups] = useState<SpecGroup[]>([]);
|
||||||
|
|
||||||
|
// 组织规格数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (specs.length > 0) {
|
||||||
|
const groups: Record<string, Set<string>> = {};
|
||||||
|
|
||||||
|
specs.forEach(spec => {
|
||||||
|
if (spec.specName && spec.specValue) {
|
||||||
|
if (!groups[spec.specName]) {
|
||||||
|
groups[spec.specName] = new Set();
|
||||||
|
}
|
||||||
|
groups[spec.specName].add(spec.specValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupsArray = Object.entries(groups).map(([specName, values]) => ({
|
||||||
|
specName,
|
||||||
|
values: Array.from(values)
|
||||||
|
}));
|
||||||
|
|
||||||
|
setSpecGroups(groupsArray);
|
||||||
|
}
|
||||||
|
}, [specs]);
|
||||||
|
|
||||||
|
// 根据选中规格找到对应SKU
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.keys(selectedSpecs).length === specGroups.length && skus.length > 0) {
|
||||||
|
// 构建规格值字符串,按照规格名称排序确保一致性
|
||||||
|
const sortedSpecNames = specGroups.map(g => g.specName).sort();
|
||||||
|
const specValues = sortedSpecNames.map(name => selectedSpecs[name]).join('|');
|
||||||
|
|
||||||
|
const sku = skus.find(s => s.sku === specValues);
|
||||||
|
setSelectedSku(sku || null);
|
||||||
|
} else {
|
||||||
|
setSelectedSku(null);
|
||||||
|
}
|
||||||
|
}, [selectedSpecs, skus, specGroups]);
|
||||||
|
|
||||||
|
// 选择规格值
|
||||||
|
const handleSpecSelect = (specName: string, specValue: string) => {
|
||||||
|
setSelectedSpecs(prev => ({
|
||||||
|
...prev,
|
||||||
|
[specName]: specValue
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认选择
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (!selectedSku) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onConfirm(selectedSku, quantity, action);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查规格值是否可选(是否有对应的SKU且有库存)
|
||||||
|
const isSpecValueAvailable = (specName: string, specValue: string) => {
|
||||||
|
const testSpecs = { ...selectedSpecs, [specName]: specValue };
|
||||||
|
|
||||||
|
// 如果还有其他规格未选择,则认为可选
|
||||||
|
if (Object.keys(testSpecs).length < specGroups.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建规格值字符串
|
||||||
|
const sortedSpecNames = specGroups.map(g => g.specName).sort();
|
||||||
|
const specValues = sortedSpecNames.map(name => testSpecs[name]).join('|');
|
||||||
|
|
||||||
|
const sku = skus.find(s => s.sku === specValues);
|
||||||
|
return sku && sku.stock && sku.stock > 0 && sku.status === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popup
|
||||||
|
visible={visible}
|
||||||
|
position="bottom"
|
||||||
|
onClose={onClose}
|
||||||
|
style={{ height: '60vh' }}
|
||||||
|
>
|
||||||
|
<View className="spec-selector">
|
||||||
|
{/* 商品信息 */}
|
||||||
|
<View className="spec-selector__header p-4">
|
||||||
|
<Space className="flex">
|
||||||
|
<Image
|
||||||
|
src={selectedSku?.image || goods.image || ''}
|
||||||
|
width="80"
|
||||||
|
height="80"
|
||||||
|
radius="8"
|
||||||
|
/>
|
||||||
|
<View className="goods-detail">
|
||||||
|
<View className="goods-name font-medium text-lg">{goods.name}</View>
|
||||||
|
<View className="text-red-500">
|
||||||
|
¥{selectedSku?.price || goods.price}
|
||||||
|
</View>
|
||||||
|
<View className="goods-stock text-gray-500">
|
||||||
|
库存:{selectedSku?.stock || goods.stock}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Space>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 规格选择 */}
|
||||||
|
<CellGroup className="spec-selector__content">
|
||||||
|
<Cell>
|
||||||
|
<Space direction="vertical">
|
||||||
|
<View className={'title'}>套餐</View>
|
||||||
|
<Radio.Group defaultValue="1" direction="horizontal">
|
||||||
|
<Radio shape="button" value="1">
|
||||||
|
选项1
|
||||||
|
</Radio>
|
||||||
|
<Radio shape="button" value="2">
|
||||||
|
选项2
|
||||||
|
</Radio>
|
||||||
|
<Radio shape="button" value="3">
|
||||||
|
选项3
|
||||||
|
</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Space>
|
||||||
|
</Cell>
|
||||||
|
</CellGroup>
|
||||||
|
{/* 底部按钮 */}
|
||||||
|
<View className="fixed bottom-7 w-full">
|
||||||
|
<View className={'px-4'}>
|
||||||
|
<Button
|
||||||
|
type="success"
|
||||||
|
size="large"
|
||||||
|
className={'w-full'}
|
||||||
|
block
|
||||||
|
// disabled={!selectedSku || !selectedSku.stock || selectedSku.stock <= 0}
|
||||||
|
onClick={handleConfirm}
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Popup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SpecSelector;
|
||||||
Reference in New Issue
Block a user