一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第22天,点击查看活动详情。
前言
我们这几天关于商品相关的功能开发的差不多了,现在已经完成了以下功能
- 首页
- 分类搜索
- 搜索引导和商品列表 今天我们来打通商品到购物车或者直接下单的最后一环——商品详情页
商品详情页的需求分析
简单分析一下,有以下几个功能
- 头部切换:商品、评价、详情、推荐区域快速跳转。这里要实现下滑切换头部tab和tab计算对应的滑动窗口位置。总体的逻辑比较难,不在我们今天的探索范围内
- 当前规格的图片显示
- 规格的属性显示
- 切换规格
- 物流、服务。这个地方是京东自有的逻辑,今天暂时不探索这个功能的实现
- 评价列表。实现的逻辑和237一样,只是需要加载数据。本文先略过这个功能的实现
- 商品描述 今天,我们实现一下以上 2、3、4、7的需求。即进入详情页时先加载商品数据,渲染237。在进行4操作的时候,237也随之改变。我们结合上一篇的redux/toolkit来使用状态管理器实现当前规格的存储功能
实现逻辑
根据单一组件单一职责的功能划分,我们将这五个需求分为了4个组件。其中商品描述我们没有单独开发组件,因为它是直接渲染的html元素。
step1 src下新建sub-details-pages文件夹,创建goods-detil文件夹
因为这个详情页面是有可能不用打开用户就能直接下单的,所以我们新建一个用来分所有详情的包:sub-details-pages,在它下面新建goods-detil用来表示商品详情
这一步不要忘记在app.config.ts
中添加分包定义,定义的代码如下
{
root: "sub-detail-pages",
name: "detail-pages",
pages: ["goods-detail/index"],
},
step2 先创建基本的子组件并在index.tsx中引用
这一步我们就是简单的看一下定义的几个组件是否正常引入,在goods-detail下新建index.tsx和index.scss,其他组件分别创建对应的文件夹和index.tsx、index.config.ts和index.scss(这一步所有的组件的index.scss内容都为空)
/goods-detail/index.tsx
import { View, Text } from "@tarojs/components";
import "./index.scss";
import ProductPhoto from "./product-photo";
import ProductProps from "./product-props";
import ProductSelector from "./product-selector";
export default function GoodsDetail() {
return (
<View>
<ProductPhoto />
<ProductProps />
<ProductSelector />
<Text>描述</Text>
</View>
);
}
四个组建的初始化方法都差不多,我拿一个举例。以规格图片为例,ProductPhoto组件定义如下 /goods-detail/product-photo/index.tsx
import { View } from "@tarojs/components";
import "./index.scss";
export default function ProductPhoto() {
return <View></View>;
}
step3 添加跳转入口
我们在首页和商品详情页添加进入商品详情页的入口,在点击对应的商品view时,通过
Taro.redirectTo({
url: `/sub-detail-pages/goods-detail/index?goodsId=${item.id}`,
})
跳转到商品详情页。这里我们之所以采用redirectTo是因为微信小程序在使用navigateTo时会限制最多跳转10层。
step4 定义商品详情的规格状态
因为我们在查看详情的时候,取的大部分是规格信息,我们直接使用状态管理器存储规格。
我们的存储逻辑为:
graph TD
进入页面加载规格状态 --> 点击切换时改变规格状态
点击切换时改变规格状态 --> 购买或者加购物车
这里我不不用做清除状态的操作,因为每次进入商品详情都会初始化状态。下面我们直接上代码来给大家说一下实现的细节
代码实现
我先把实现详情功能需要创建的文件截图贴出来
定义action和state
goodsDetailProductSlice.ts
import { createSlice } from "@reduxjs/toolkit";
export const goodsDetailProductSlice = createSlice({
// 命名空间,在调用action的时候会默认的设置为action的前缀
name: "goodsDetailProduct",
// 初始值
initialState: {
goodsDetailProduct: {
goodsId: "",
desc: "",
photos: [],
},
},
// 这里的属性会自动的导出为actions,在组件中可以直接通过dispatch进行触发
reducers: {
updateGoodsDetailProduct(state, { payload }) {
// 内置了immutable
state.goodsDetailProduct = payload;
},
},
});
// 导出actions
export const { updateGoodsDetailProduct } = goodsDetailProductSlice.actions;
// 导出reducer,在创建store时使用到
export default goodsDetailProductSlice.reducer;
store/index.tsx
import { configureStore } from "@reduxjs/toolkit";
import goodsQueryFilterSlice from "./features/goodsQueryFilterSlice";
import goodsDetailProductSlice from "./features/goodsDetailProductSlice";
// configureStore创建一个redux数据
export default configureStore({
reducer: {
goodsQueryFilter: goodsQueryFilterSlice,
goodsDetailProduct: goodsDetailProductSlice,
},
});
这一步定义了我们的action\
图片显示页面
product-photo/index.tsx
import { View } from "@tarojs/components";
import "./index.scss";
import { useSelector } from "react-redux";
import { Swiper, SwiperItem } from "@tarojs/components";
import { AtImagePicker } from "taro-ui";
export default function ProductPhoto() {
const { goodsDetailProduct } = useSelector(
(state: any) => state.goodsDetailProduct
);
return (
<View>
{goodsDetailProduct.photos && goodsDetailProduct.photos.length > 0 ? (
<Swiper
className="test-h"
indicatorColor="#999"
indicatorActiveColor="#333"
circular
autoplay
indicatorDots
>
<SwiperItem>
<View>
<AtImagePicker
files={[
{
url: goodsDetailProduct.photos[0],
},
]}
showAddBtn={false}
onImageClick={() => {}}
count={1}
onChange={() => {}}
sizeType={["original", "compressed"]}
/>
</View>
</SwiperItem>
<SwiperItem>
<View>
<AtImagePicker
files={[
{
url: goodsDetailProduct.photos[1],
},
]}
showAddBtn={false}
onImageClick={() => {}}
count={1}
onChange={() => {}}
sizeType={["original", "compressed"]}
/>
</View>
</SwiperItem>
<SwiperItem>
<View>
<AtImagePicker
files={[
{
url: goodsDetailProduct.photos[2],
},
]}
showAddBtn={false}
onImageClick={() => {}}
count={1}
onChange={() => {}}
sizeType={["original", "compressed"]}
/>
</View>
</SwiperItem>
</Swiper>
) : null}
</View>
);
}
属性显示页面
product-props/index.tsx
import { View, Text } from "@tarojs/components";
import "./index.scss";
import { useSelector } from "react-redux";
export default function ProductProps() {
const { goodsDetailProduct } = useSelector(
(state: any) => state.goodsDetailProduct
);
return (
<View className="goods-props">
<View>
<Text>{goodsDetailProduct.name}</Text>
</View>
<View className="price">
<Text>{goodsDetailProduct.price}</Text>
</View>
</View>
);
}
product-props/index.tsx
.goods-props {
.price {
color: red;
}
}
切换属性与显示当前选择选项的页面
product-selector/index.tsx
import { View, Text } from "@tarojs/components";
import "./index.scss";
import { useSelector, useDispatch } from "react-redux";
import { AtButton, AtFloatLayout } from "taro-ui";
import { useState } from "react";
import { AtList, AtListItem } from "taro-ui";
import { updateGoodsDetailProduct } from "../../../store/features/goodsDetailProductSlice";
export default function ProductSelector() {
const { goodsDetailProduct } = useSelector(
(state: any) => state.goodsDetailProduct
);
const [showChooseFlag, setShowChooseFlag] = useState(false);
const dispatch = useDispatch();
const bindProdcut = (goodsId) => {
dispatch(
updateGoodsDetailProduct({
goodsId: goodsId,
desc: `<p1>商品id——:${goodsId}——的描述</p1>`,
name: `${goodsId}商品名称`,
price: `${goodsId}商品价格`,
productStr: `${goodsId}的规格描述,内存,颜色等`,
photos: [
"https://storage.360buyimg.com/mtd/home/111543234387022.jpg",
"https://storage.360buyimg.com/mtd/home/221543234387016.jpg",
"https://storage.360buyimg.com/mtd/home/111543234387022.jpg",
"https://storage.360buyimg.com/mtd/home/111543234387022.jpg",
],
})
);
};
return (
<View className="goods-selector">
<View className="selector-prodcut">
已选择:<Text>{goodsDetailProduct.productStr}</Text>
<AtButton
onClick={() => setShowChooseFlag(true)}
type="primary"
size="small"
>
点我切换
</AtButton>
<AtFloatLayout
isOpened={showChooseFlag}
title=""
onClose={() => setShowChooseFlag(false)}
>
<AtList>
<AtListItem
title="点我选择规格1111"
onClick={() => bindProdcut(1111)}
/>
<AtListItem
title="点我选择规格2222"
onClick={() => bindProdcut(2222)}
/>
<AtListItem
title="点我选择规格3333"
onClick={() => bindProdcut(3333)}
/>
<AtListItem
title="点我选择规格4444"
onClick={() => bindProdcut(4444)}
/>
</AtList>
</AtFloatLayout>
</View>
</View>
);
}
product-selector/index.scss
@import "~taro-ui/dist/style/components/float-layout.scss";
@import "~taro-ui/dist/style/components/list.scss";
详情页面首页
index.tsx
import { View, ScrollView } from "@tarojs/components";
import "./index.scss";
import ProductPhoto from "./product-photo";
import ProductProps from "./product-props";
import ProductSelector from "./product-selector";
import { getCurrentInstance } from "@tarojs/taro";
import { updateGoodsDetailProduct } from "../../store/features/goodsDetailProductSlice";
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { AtDivider } from 'taro-ui'
import { AtButton } from "taro-ui";
export default function GoodsDetail() {
const { router } = getCurrentInstance();
const { goodsDetailProduct } = useSelector(
(state: any) => state.goodsDetailProduct
);
const dispatch = useDispatch();
useEffect(() => {
const goodsId = router && router.params.goodsId;
dispatch(
updateGoodsDetailProduct({
goodsId: goodsId,
desc: `<p1>商品id——:${goodsId}——的描述</p1>`,
name: `${goodsId}商品名称`,
price: `${goodsId}商品价格`,
productStr: `${goodsId}的规格描述,内存,颜色等`,
photos: [
"https://storage.360buyimg.com/mtd/home/111543234387022.jpg",
"https://storage.360buyimg.com/mtd/home/221543234387016.jpg",
"https://storage.360buyimg.com/mtd/home/111543234387022.jpg",
"https://storage.360buyimg.com/mtd/home/111543234387022.jpg",
],
})
);
}, []);
return (
<View className="goodsDetail">
<ScrollView scrollY={true} className="goods_detail_scroll">
<ProductPhoto />
<AtDivider content='' />
<ProductProps />
<AtDivider content='' />
<ProductSelector />
<AtDivider content='' />
<View
dangerouslySetInnerHTML={{ __html: goodsDetailProduct.desc }}
></View>
</ScrollView>
<View>
<AtButton type="primary">加入购物车</AtButton>
<AtButton type="secondary">立即购买</AtButton>
</View>
</View>
);
}
index.scss
@import "~taro-ui/dist/style/components/button.scss";
@import "~taro-ui/dist/style/components/divider.scss";
.goods_detail_scroll {
height: 1000px;
}
主要的逻辑说明
我们使用dispatch调度action,通过状态管理器来管理最新的规格信息,在点击切换的时候,这几个页面会实时切换,效果如下
结语
本篇文章我们基于状态管理器实现了商品详情页面的数据展示和切换。
在使用状态管理器的时候,我发现在遍历的时候,可以在控制台中打印出来item,但是无法渲染,这个后面我们看一下找一下原因。
原创不易,欢迎各位多多关注点赞!