1. 热门推荐
1.1 热门推荐-准备工作
内容不同,但结构相同,所以要复用热门推荐组件,再动态传值渲染
1.2 静态结构
// /src/pages/hot/hot.vue
<script setup lang="ts">
// 热门推荐页 标题和url
const hotMap = [
{ type: '1', title: '特惠推荐', url: '/hot/preference' },
{ type: '2', title: '爆款推荐', url: '/hot/inVogue' },
{ type: '3', title: '一站买全', url: '/hot/oneStop' },
{ type: '4', title: '新鲜好物', url: '/hot/new' },
]
</script>
<template>
<view class="viewport">
<!-- 推荐封面图 -->
<view class="cover">
<image
src="http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-05-20/84abb5b1-8344-49ae-afc1-9cb932f3d593.jpg"
></image>
</view>
<!-- 推荐选项 -->
<view class="tabs">
<text class="text active">抢先尝鲜</text>
<text class="text">新品预告</text>
</view>
<!-- 推荐列表 -->
<scroll-view scroll-y class="scroll-view">
<view class="goods">
<navigator
hover-class="none"
class="navigator"
v-for="goods in 10"
:key="goods"
:url="`/pages/goods/goods?id=`"
>
<image
class="thumb"
src="https://yanxuan-item.nosdn.127.net/5e7864647286c7447eeee7f0025f8c11.png"
></image>
<view class="name ellipsis">不含酒精,使用安心爽肤清洁湿巾</view>
<view class="price">
<text class="symbol">¥</text>
<text class="number">29.90</text>
</view>
</navigator>
</view>
<view class="loading-text">正在加载...</view>
</scroll-view>
</view>
</template>
<style lang="scss">
page {
height: 100%;
background-color: #f4f4f4;
}
.viewport {
display: flex;
flex-direction: column;
height: 100%;
padding: 180rpx 0 0;
position: relative;
}
.cover {
width: 750rpx;
height: 225rpx;
border-radius: 0 0 40rpx 40rpx;
overflow: hidden;
position: absolute;
left: 0;
top: 0;
}
.scroll-view {
flex: 1;
}
.tabs {
display: flex;
justify-content: space-evenly;
height: 100rpx;
line-height: 90rpx;
margin: 0 20rpx;
font-size: 28rpx;
border-radius: 10rpx;
box-shadow: 0 4rpx 5rpx rgba(200, 200, 200, 0.3);
color: #333;
background-color: #fff;
position: relative;
z-index: 9;
.text {
margin: 0 20rpx;
position: relative;
}
.active {
&::after {
content: '';
width: 40rpx;
height: 4rpx;
transform: translate(-50%);
background-color: #27ba9b;
position: absolute;
left: 50%;
bottom: 24rpx;
}
}
}
.goods {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 20rpx 20rpx;
.navigator {
width: 345rpx;
padding: 20rpx;
margin-top: 20rpx;
border-radius: 10rpx;
background-color: #fff;
}
.thumb {
width: 305rpx;
height: 305rpx;
}
.name {
height: 88rpx;
font-size: 26rpx;
}
.price {
line-height: 1;
color: #cf4444;
font-size: 30rpx;
}
.symbol {
font-size: 70%;
}
.decimal {
font-size: 70%;
}
}
.loading-text {
text-align: center;
font-size: 28rpx;
color: #666;
padding: 20rpx 0 50rpx;
}
</style>
1.3 给热门组件绑定跳转到hot页面
1.3.1 传参
1.3.2 顶部传参结果
1.4 获取数据-获取热门推荐的数据
四个接口除路径,其他都相同,所以做个统一封装,提高代码复用
1.4.1 封装四个接口的通用结构
使用交叉类型‘&’拓展字段
1.4.2 在热门页面中获取数据并调用
1.4.3 控制台输出的结果:
1.5 热门推荐-定义类型
返回的数据结构和类型与先前的‘猜你喜欢’差不多,所以可以复用‘猜你喜欢’
返回的结构为:
1.5.1 通用类型的定义
item:
1.5.2 因为热门推荐和‘猜你喜欢’的数据类型完全一致,所以直接复用
1.5.3 封装热门推荐的数据类型(有复用之前的数据类型)
HotResult:
subTypes:
之前封装的通用分页类型:
最终的代码展示:
1.5.4 使用定义好的HotResult方法
1.6 热门推荐-渲染页面和Tab交互
给渲染作为参考的后端数据:
1.6.1 使用ref,定义bannerpicture接收res。reslut。bannerpicture,再动态传值给对应的页面标签
1.6.2 bannerpicture成功渲染
1.6.3 使用同样的方法更改hot页面的两个title
1.6.4 数据渲染成功
1.7 解决字段都有active导致的所有字段高亮问题(使用动态绑定样式)
v-for自动循环activeIndex,先默认选中第一个activeindex(下标为0)。index是tap事件获得的,如果activeIndex===index,就给对应的activeindex添加高亮,
1.7.1结果:
1.8 列表内容的渲染
(点击tap实现内容的切换)(这里使用v-show;不用v-if,因为v-if就是在不断的销毁和创建,很消耗性能)
1.8.1 结果:
1.9 列表的分页加载
当页面滚动触底的时候加载页面数据
1.9.1 给滚动容器添加一个滚动触底的事件
滚动tab触底,输出的结果:
1.9.2 基于对应的id,触底才传goodsitem(用于区别tab)
- getHotRecommendAPI 被调用时传入的就是 currHot?.url,也就是那一个匹配项的 url。所以只会返回哪一个页面的goodsitem
发现只有滚动的对应的tabs才会有goodsitem
1.9.3 将获取到的新的后端数据添加到列表
1.9.4 实现了页码的累加和数组的追加
1.10 分页条件
1.10.1 使用条件语句判断是否能继续加载,否则轻提示
页面全都加载完,触发轻提示
小技巧:如果在开发环境,则设置为30,便于测试。否则为1
2.商品分类
2.1 静态结构-商品分类页属于tabBar页
<script setup lang="ts">
//
</script>
<template>
<view class="viewport">
<!-- 搜索框 -->
<view class="search">
<view class="input">
<text class="icon-search">女靴</text>
</view>
</view>
<!-- 分类 -->
<view class="categories">
<!-- 左侧:一级分类 -->
<scroll-view class="primary" scroll-y>
<view v-for="(item, index) in 10" :key="item" class="item" :class="{ active: index === 0 }">
<text class="name"> 居家 </text>
</view>
</scroll-view>
<!-- 右侧:二级分类 -->
<scroll-view class="secondary" scroll-y>
<!-- 焦点图 -->
<XtxSwiper class="banner" :list="[]" />
<!-- 内容区域 -->
<view class="panel" v-for="item in 3" :key="item">
<view class="title">
<text class="name">宠物用品</text>
<navigator class="more" hover-class="none">全部</navigator>
</view>
<view class="section">
<navigator
v-for="goods in 4"
:key="goods"
class="goods"
hover-class="none"
:url="`/pages/goods/goods?id=`"
>
<image
class="image"
src="https://yanxuan-item.nosdn.127.net/674ec7a88de58a026304983dd049ea69.jpg"
></image>
<view class="name ellipsis">木天蓼逗猫棍</view>
<view class="price">
<text class="symbol">¥</text>
<text class="number">16.00</text>
</view>
</navigator>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<style lang="scss">
page {
height: 100%;
overflow: hidden;
}
.viewport {
height: 100%;
display: flex;
flex-direction: column;
}
.search {
padding: 0 30rpx 20rpx;
background-color: #fff;
.input {
display: flex;
align-items: center;
justify-content: space-between;
height: 64rpx;
padding-left: 26rpx;
color: #8b8b8b;
font-size: 28rpx;
border-radius: 32rpx;
background-color: #f3f4f4;
}
}
.icon-search {
&::before {
margin-right: 10rpx;
}
}
/* 分类 */
.categories {
flex: 1;
min-height: 400rpx;
display: flex;
}
/* 一级分类 */
.primary {
overflow: hidden;
width: 180rpx;
flex: none;
background-color: #f6f6f6;
.item {
display: flex;
justify-content: center;
align-items: center;
height: 96rpx;
font-size: 26rpx;
color: #595c63;
position: relative;
&::after {
content: '';
position: absolute;
left: 42rpx;
bottom: 0;
width: 96rpx;
border-top: 1rpx solid #e3e4e7;
}
}
.active {
background-color: #fff;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 8rpx;
height: 100%;
background-color: #27ba9b;
}
}
}
.primary .item:last-child::after,
.primary .active::after {
display: none;
}
/* 二级分类 */
.secondary {
background-color: #fff;
.carousel {
height: 200rpx;
margin: 0 30rpx 20rpx;
border-radius: 4rpx;
overflow: hidden;
}
.panel {
margin: 0 30rpx 0rpx;
}
.title {
height: 60rpx;
line-height: 60rpx;
color: #333;
font-size: 28rpx;
border-bottom: 1rpx solid #f7f7f8;
.more {
float: right;
padding-left: 20rpx;
font-size: 24rpx;
color: #999;
}
}
.more {
&::after {
font-family: 'erabbit' !important;
content: '\e6c2';
}
}
.section {
width: 100%;
display: flex;
flex-wrap: wrap;
padding: 20rpx 0;
.goods {
width: 150rpx;
margin: 0rpx 30rpx 20rpx 0;
&:nth-child(3n) {
margin-right: 0;
}
image {
width: 150rpx;
height: 150rpx;
}
.name {
padding: 5rpx;
font-size: 22rpx;
color: #333;
}
.price {
padding: 5rpx;
font-size: 18rpx;
color: #cf4444;
}
.number {
font-size: 24rpx;
margin-left: 2rpx;
}
}
}
}
</style>
2.2 获取轮播图数据
首页轮播图和这里的轮播可以复用
结果:
2.3 商品分类-渲染一级分类和Tab交互
2.3.1 封装请求API
2.3.2 在catrgory页面加载并使用API返回的数据
2.3.3 定义一下分类的类型声明文件
2.3.4 给API绑上数据类型
2.3.5 获取的数据页绑上对应的数据类型
2.3.6 页面标签绑定数据实现数据渲染
2.3.7 实现Tab交互
结果:
2.4 商品分类-二级分类和商品渲染
2.4.1 基于一级分类获取到的下标(高亮)获取对应的二级分类
2.4.1 渲染拿到的数据
2.4.2 结果:
2.5 商品分类-骨架图
2.5.1 设置一个变量isFinsh标记数据是否加载完毕
2.5.2 在标签中使用v-if和v-else显示为互斥
3. 商品详情页-准备工作
3.1 商品详情页准备
3.1.1 新建商品详情页
3.1.2 获取传来的商品id
3.1.3 封装一下接口
3.1.4 页面初始化的时候调用一下API
3.1.5 成功获取点击商品的后端数据
3.2 商品详情-页面渲染
3.2.1 编写类型声明文件
import type { GoodsItem } from './global'
/** 商品信息 */
export type GoodsResult = {
/** id */
id: string
/** 商品名称 */
name: string
/** 商品描述 */
desc: string
/** 当前价格 */
price: number
/** 原价 */
oldPrice: number
/** 商品详情: 包含详情属性 + 详情图片 */
details: Details
/** 主图图片集合[ 主图图片链接 ] */
mainPictures: string[]
/** 同类商品[ 商品信息 ] */
similarProducts: GoodsItem[]
/** sku集合[ sku信息 ] */
skus: SkuItem[]
/** 可选规格集合备注[ 可选规格信息 ] */
specs: SpecItem[]
/** 用户地址列表[ 地址信息 ] */
userAddresses: AddressItem[]
}
/** 商品详情: 包含详情属性 + 详情图片 */
export type Details = {
/** 商品属性集合[ 属性信息 ] */
properties: DetailsPropertyItem[]
/** 商品详情图片集合[ 图片链接 ] */
pictures: string[]
}
/** 属性信息 */
export type DetailsPropertyItem = {
/** 属性名称 */
name: string
/** 属性值 */
value: string
}
/** sku信息 */
export type SkuItem = {
/** id */
id: string
/** 库存 */
inventory: number
/** 原价 */
oldPrice: number
/** sku图片 */
picture: string
/** 当前价格 */
price: number
/** sku编码 */
skuCode: string
/** 规格集合[ 规格信息 ] */
specs: SkuSpecItem[]
}
/** 规格信息 */
export type SkuSpecItem = {
/** 规格名称 */
name: string
/** 可选值名称 */
valueName: string
}
/** 可选规格信息 */
export type SpecItem = {
/** 规格名称 */
name: string
/** 可选值集合[ 可选值信息 ] */
values: SpecValueItem[]
}
/** 可选值信息 */
export type SpecValueItem = {
/** 是否可售 */
available: boolean
/** 可选值备注 */
desc: string
/** 可选值名称 */
name: string
/** 可选值图片链接 */
picture: string
}
/** 地址信息 */
export type AddressItem = {
/** 收货人姓名 */
receiver: string
/** 联系方式 */
contact: string
/** 省份编码 */
provinceCode: string
/** 城市编码 */
cityCode: string
/** 区/县编码 */
countyCode: string
/** 详细地址 */
address: string
/** 默认地址,1为是,0为否 */
isDefault: number
/** 收货地址 id */
id: string
/** 省市区 */
fullLocation: string
}
3.2.2 保存获取到的数据
3.2.3 使用获取到的数据渲染
结果:
3.3 轮播图交互
3.3.1 绑定事件,获取对应下标
3.3.2 定义参数currentIndex用来保存当前的索引
3.3.3 给对应标签传参
3.3.4 实现点击商品图预览
使用微信小程序官方的放大API,其中current和urls是必传参数,否则报错
3.3.5 成功实现
3.4 商品详情-弹出层
uni-popup官方文档:uni-popup 弹出层 | uni-app官网
3.4.1 弹出层
3.4.2 传参
3.4.3 绑定popup。open方法
3.5 商品详情-弹出层交互
3.5.1 组件静态代码
3.5.2 引入组件
3.5.3 但是现在一点击弹出层,两个面板都弹出来了。所以我们要实现点击哪个面板,再弹出对应面板
3.6 使用子调父实现在组件内的‘x’(关闭按钮)关闭goods(父页面)的面板
这里的“子调父”指的是子组件 AddressPanel 内部通过 emit('close') 通知它的父组件“我要关闭弹窗”这个动作,父组件接收到这个事件后再去执行实际的关闭逻辑(调用 popup?.close())。