目录
动态获取数据
参考效果
推荐模块的布局结构是相同的,因此我们可以复用相同的页面及交互,只是所展示的数据不同。
静态结构
新建热门推荐页面文件,并在 pages.json
中添加路由(VS Code 插件自动完成)。
// /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>
获取页面参数
热门推荐页要根据页面参数区分需要获取的是哪种类型的推荐列表,然后再去调用相应的接口,来获取不同的数据,再渲染到页面当中。
项目首页(传递参数)
// src/pages/index/components/HotPanel.vue
<navigator :url="`/pages/hot/hot?type=${item.type}`">
…省略
</navigator>
热门推荐页(获取参数)
<script setup lang="ts">
import { onLoad } from "@dcloudio/uni-app";
// 热门推荐页 标题和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" },
];
const query = defineProps<{
type: string;
}>();
const currUrlMap = hotMap.find((v) => v.type === query.type);
onLoad(() => {
uni.setNavigationBarTitle({ title: currUrlMap!.title });
});
</script>
获取数据
地址参数
不同类型的推荐,需要调用不同的 API 接口:
type | 推荐类型 | 接口路径 |
---|---|---|
1 | 特惠推荐 | /hot/preference |
2 | 爆款推荐 | /hot/inVogue |
3 | 一站买全 | /hot/oneStop |
4 | 新鲜好物 | /hot/new |
接口调用
调用接口获取推荐商品列表的数据,然后再将这些数据渲染出来。
接口地址:见上表
请求方式:GET
请求参数:
Query:
字段名称 | 是否必须 | 默认值 | 备注 |
---|---|---|---|
subType | 否 | 无 | 推荐列表 Tab 项的 id |
page | 否 | 1 | 页码 |
pageSize | 否 | 10 | 每页商品数量 |
请求封装
经过分析,尽管不同类型推荐的请求 url 不同,但请求参数及响应格式都具有一致性,因此可以将接口的调用进行封装,参考代码如下所示:
ts
import { http } from '@/utils/http'
import type { PageParams } from '@/types/global'
type HotParams = PageParams & {
/** Tab 项的 id,默认查询全部 Tab 项的第 1 页数据 */
subType?: string
}
/**
* 通用热门推荐类型
* @param url 请求地址
* @param data 请求参数
*/
export const getHotRecommendAPI = (url: string, data?: HotParams) => {
return http<HotResult>({
method: 'GET',
url,
data,
})
}
类型声明
电商项目较为常见商品展示,商品的类型是可复用的,封装到 src/types/global.d.ts
文件中:
// src/types/global.d.ts
/** 通用商品类型 */
export type GoodsItem = {
/** 商品描述 */
desc: string
/** 商品折扣 */
discount: number
/** id */
id: string
/** 商品名称 */
name: string
/** 商品已下单数量 */
orderNum: number
/** 商品图片 */
picture: string
/** 商品价格 */
price: number
}
import type { PageResult, GoodsItem } from './global'
/** 热门推荐 */
export type HotResult = {
/** id信息 */
id: string
/** 活动图片 */
bannerPicture: string
/** 活动标题 */
title: string
/** 子类选项 */
subTypes: SubTypeItem[]
}
/** 热门推荐-子类选项 */
export type SubTypeItem = {
/** 子类id */
id: string
/** 子类标题 */
title: string
/** 子类对应的商品集合 */
goodsItems: PageResult<GoodsItem>
}
最后,把获取到的数据结合模板语法渲染到页面中。
热门推荐 – 渲染页面和Tab交互
// 推荐封面图
const bannerPicture = ref("");
// 推荐选项
const subTypes = ref<SubTypeItem[]>([]);
// 高亮下标
const activeIndex = ref(0);
const getHotRecommendData = async () => {
const res = await getHotRecommendAPI(currUrlMap!.url);
console.log(res);
// 保存封面图
bannerPicture.value = res.result.bannerPicture;
// 保存推荐选项
subTypes.value = res.result.subTypes;
};
<view class="viewport">
<!-- 推荐封面图 -->
<view class="cover">
<image :src="bannerPicture"></image>
</view>
<!-- 推荐选项 -->
<view class="tabs">
<text
v-for="(item, index) in subTypes"
:key="item.id"
:class="{ active: activeIndex === index }"
class="text"
@tap="activeIndex = index"
>
{{ item.title }}
</text>
</view>
<!-- 推荐列表 -->
<scroll-view
scroll-y
class="scroll-view"
v-show="activeIndex === index"
v-for="(item, index) in subTypes"
:key="item.id"
>
<view class="goods">
<navigator
hover-class="none"
class="navigator"
v-for="goods in item.goodsItems.items"
:key="goods.id"
:url="`/pages/goods/goods?id=`"
>
<image class="thumb" :src="goods.picture" />
<view class="name ellipsis">{{ goods.name }}</view>
<view class="price">
<text class="symbol">¥</text>
<text class="number">{{ goods.price }}</text>
</view>
</navigator>
</view>
<view class="loading-text">正在加载...</view>
</scroll-view>
</view>
热门推荐 – 分页加载
<scroll-view
scroll-y
class="scroll-view"
v-show="activeIndex === index"
v-for="(item, index) in subTypes"
:key="item.id"
@scrolltolower="onScrolltolower"
// 滚动触底
const onScrolltolower = async () => {
// 获取当前选项
const currSubTypes = subTypes.value[activeIndex.value];
console.log(currSubTypes);
// 当前选项页码累加
currSubTypes.goodsItems.page++;
// 调用并传参
const res = await getHotRecommendAPI(currUrlMap!.url, {
subType: currSubTypes.id,
page: currSubTypes.goodsItems.page,
pageSize: currSubTypes.goodsItems.pageSize,
});
// 提取新数据
const newSubTypes = res.result.subTypes[activeIndex.value];
// 当前选项数组追加
currSubTypes.goodsItems.items.push(...newSubTypes.goodsItems.items);
};
热门推荐 – 分页条件
if (currSubTypes.goodsItems.page < currSubTypes.goodsItems.pages) {
// 当前选项页码累加
currSubTypes.goodsItems.page++;
} else {
// 标记已结束
currSubTypes.finish = true;
return uni.showToast({ icon: "none", title: "没有更多数据~" });
}
<view class="loading-text">
{{ item.finish ? "没有更多数据~" : "正在加载..." }}
</view>
const res = await getHotRecommendAPI(currUrlMap!.url, {
page: import.meta.env.DEV ? 34 : 1,
pageSize: 10,
});
type 和 interface 的区别
当谈到TypeScript中的类型定义时,人们常常会遇到两个主要的概念:type
和interface
。虽然它们在某些情况下可以互相替代,但它们在使用和功能上还是存在一些区别。在本文中,我们将探讨type
和interface
之间的区别,以及它们在不同情境下的使用。
type 和 interface 的相似之处
首先,让我们看一下type
和interface
的相似之处。它们都是用来定义 TypeScript 中的自定义类型,从而增强代码的可读性和维护性。无论你选择使用哪种方式,都可以定义属性、方法、函数、联合类型等等。
type 的特点和用途
type
关键字允许你创建一个自定义的类型,这个类型可以是基本类型、联合类型、交叉类型或者任何复杂的类型组合。使用type
可以进行更灵活的类型操作,包括创建类型别名、条件类型和映射类型等。
type Point = {
x: number;
y: number;
};
type Result = number | string;
type Nullable<T> = T | null;
在这里,我们使用type
定义了一个Point
类型,一个Result
类型,以及一个接受泛型参数的Nullable
类型别名。
interface 的特点和用途
interface
关键字用于定义一个对象的结构,通常用于描述类、对象、函数签名等。与type
不同,interface
不能创建类型别名、联合类型和交叉类型,但是它支持扩展和实现。
interface User {
id: number;
username: string;
}
interface Admin extends User {
isAdmin: boolean;
}
interface Calculate {
(x: number, y: number): number;
}
在这里,我们使用interface
定义了一个User
接口,一个扩展了User
的Admin
接口,以及一个函数签名的Calculate
接口。
何时使用 type 和 interface
08月23日 16:29 会话ID:(1155523)
当谈到TypeScript中的类型定义时,人们常常会遇到两个主要的概念:type
和interface
。虽然它们在某些情况下可以互相替代,但它们在使用和功能上还是存在一些区别。在本文中,我们将探讨type
和interface
之间的区别,以及它们在不同情境下的使用。
type 和 interface 的相似之处
首先,让我们看一下type
和interface
的相似之处。它们都是用来定义 TypeScript 中的自定义类型,从而增强代码的可读性和维护性。无论你选择使用哪种方式,都可以定义属性、方法、函数、联合类型等等。
type 的特点和用途
type
关键字允许你创建一个自定义的类型,这个类型可以是基本类型、联合类型、交叉类型或者任何复杂的类型组合。使用type
可以进行更灵活的类型操作,包括创建类型别名、条件类型和映射类型等。
typescript
插入代码复制代码
type Point = { x: number; y: number; }; type Result = number | string; type Nullable<T> = T | null;
在这里,我们使用type
定义了一个Point
类型,一个Result
类型,以及一个接受泛型参数的Nullable
类型别名。
interface 的特点和用途
interface
关键字用于定义一个对象的结构,通常用于描述类、对象、函数签名等。与type
不同,interface
不能创建类型别名、联合类型和交叉类型,但是它支持扩展和实现。
typescript
插入代码复制代码
interface User { id: number; username: string; } interface Admin extends User { isAdmin: boolean; } interface Calculate { (x: number, y: number): number; }
在这里,我们使用interface
定义了一个User
接口,一个扩展了User
的Admin
接口,以及一个函数签名的Calculate
接口。
何时使用 type 和 interface
使用 type
:
- 当你需要创建一个类型别名,或者进行复杂的联合类型、交叉类型操作时,
type
更适合。 - 当你需要在联合类型中使用字符串字面量类型,或者创建条件类型和映射类型时,
type
更具优势。
使用 interface
:
- 当你需要定义类、对象、函数签名等结构化的类型时,
interface
更适合。 - 当你要扩展其他接口或类时,
interface
可以提供更好的继承性。
总的来说,type
和interface
在很多情况下可以互相替代,但在一些特定的使用场景下,它们的差异变得更加明显。在选择使用哪个关键字时,你可以根据你的需求和代码风格来做出决定。
联合类型 交叉类型与基本类型
在 TypeScript 中,基本类型、联合类型和交叉类型是用来描述变量或值的不同类型特征的概念。
-
基本类型: 基本类型也被称为原始类型,它们是构成更复杂类型的基础。TypeScript 提供了几种内置的基本类型,包括:
number
:表示数字,可以是整数或浮点数。string
:表示字符串,可以包含文本或字符序列。boolean
:表示布尔值,可以是true
或false
。null
和undefined
:分别表示空值和未定义的值。symbol
:表示唯一的、不可变的值,通常用作对象属性的键。bigint
:表示大整数,用于处理超出number
类型范围的整数。
-
联合类型: 联合类型允许一个变量拥有多个可能的类型。使用
|
符号将多个类型组合在一起,表示变量可以是这些类型中的任意一个。
let value: number | string;
value = 42; // 合法
value = "hello"; // 合法
value = true; // 错误,不是 number 或 string 类型
在这个例子中,value
可以是 number
类型或 string
类型
3. 交叉类型: 交叉类型用于将多个类型合并为一个新的类型,新类型将拥有所有原始类型的属性和方法。使用 &
符号连接多个类型,表示新类型拥有所有连接的类型的特性。
type Point = { x: number; y: number };
type Color = { color: string };
type ColoredPoint = Point & Color;
const coloredPoint: ColoredPoint = { x: 10, y: 20, color: "red" };
在这个例子中,ColoredPoint
是一个拥有 Point
和 Color
属性的新类型,coloredPoint
是一个符合这个新类型的对象。
总之,基本类型是 TypeScript 中最基础的数据类型,联合类型允许一个变量可以具有多种类型中的一种,而交叉类型则是将多个类型合并为一个新的类型,以便获得所有类型的属性和方法。这些类型概念有助于在 TypeScript 中更准确地定义变量和函数的类型。
!与?的说明
!
和 ?
是用来表示属性或变量的两种不同的修饰符。
!
非空断言操作符: 在 TypeScript 中,某些情况下编译器无法确定一个值是否为null
或undefined
,但你作为开发者确信该值不会为null
或undefined
。为了在这种情况下告诉编译器“我知道这个值不会是空”,你可以使用!
非空断言操作符。
function printLength(input: string | null) {
console.log(input!.length); // 非空断言,告诉编译器 input 不会为 null
}
在上面的例子中,input!.length
表示我们告诉编译器 input
不会为 null
,因此可以安全地访问其 length
属性。
2.?
可选属性操作符: 在 TypeScript 中,你可以使用 ?
可选属性操作符来表示一个对象的属性是可选的,即该属性可以存在,也可以不存在。
interface Person {
name: string;
age?: number; // age 属性是可选的
}
const person1: Person = { name: "Alice" }; // 合法
const person2: Person = { name: "Bob", age: 30 }; // 合法
在上面的例子中,age
属性被标记为可选,因此在创建 Person
对象时可以选择是否提供 age
属性。
总之,!
非空断言操作符用于告诉编译器一个值不会是空,而 ?
可选属性操作符用于表示对象的属性是可选的。这两个操作符都有助于在 TypeScript 中更准确地表达和操作数据的特性。
总结
在 TypeScript 中,type
和interface
都是用来定义自定义类型的关键字。它们有一些相似之处,但也存在一些区别。type
适用于创建类型别名和进行复杂的类型操作,而interface
适用于定义结构化的类型,特别是在类、对象和函数签名等方面。根据你的需求,选择适合的关键字来增强你的代码的可读性和可维护性。