主题
首页模块与极速问诊(一)
本页对应 首页模块与极速问诊,共 8 段视频,约 1 小时 38 分。
学习目标
- 能够搭建首页基础结构,使用 Vant 导航、轮播图与 Tabs 组件实现紧凑模式、默认激活“推荐”及吸顶效果,并在浏览器中验证交互正确。
- 能够封装知识卡片与知识列表组件,使用
van-list的loading、finished、load等属性/事件实现加载更多,并通过模拟分页数据验证加载、加载中和无更多数据状态。 - 能够定义知识列表相关 TypeScript 类型,包括知识项、知识列表、分页响应、查询参数及知识类型联合类型,并为列表组件
props添加类型约束。 - 能够封装知识列表 API,在
onLoad中按页码和每页条数请求数据,追加列表数据、判断是否加载完成、更新加载状态,并完成知识卡片渲染及富文本标签、图片变形和单图样式处理。 - 能够使用 VueUse 的
useWindowSize获取响应式窗口宽度,按比例计算推荐关注组件的滑动宽度,并配置 Swiper 关闭指示器与循环滚动以完成适配。 - 能够定义医生与关注相关类型及 API,完成推荐医生列表请求与渲染,并实现关注/取消关注的按钮 loading、状态取反及异常后状态恢复。
知识点
本节目标与页面拆分
本节只做“首页模块-搭建页面”,不展开后续知识列表加载。页面分两部分:先搭头部、导航、轮播图等基础结构,再用 Vant 的 Tabs 组件实现知识列表的选项卡切换。转写稿里反复出现的“want/w”应纠正为 Vant,“swift”应理解为 Swipe,“thinkk”是 shrink,“tickly”是 sticky。本节笔记未附页面截图和完整源码,下面只复现转写稿中明确出现的组件、属性和调试过程,不虚构截图与代码。
基础结构:头部、导航、轮播图
为什么需要:首页最先要能把页面骨架撑起来,否则后面的列表、关注组件没有落点。
怎么想:按视觉区块拆成头部容器、导航、轮播图三块,先把笔记里准备好的结构复制进 Home 页面,再逐块核对。
怎么做:到 VS Code 中用笔记里的首页基本结构替换原代码。头部对应页面顶部;导航使用 Vant 栅格系统,按 24 等分划分,三等份时每列占 8 份;轮播图使用 Vant 的 Swipe 组件,每张图放一个 van-swipe-item。关键结构可按转写稿整理为:
vue
<van-swipe>
<van-swipe-item v-for="item in bannerList" :key="item.id">
<img :src="item.url" alt="首页轮播图" />
</van-swipe-item>
</van-swipe>易错点:组件名和属性名不要按同音字写错;Swipe 里“一张图一个 item”,不要漏掉 item;导航栅格要按 24 列计算,不要凭感觉写宽度。
Tabs 选项卡:先跑通切换,再做紧凑与吸顶
为什么需要:知识列表要按“关注、推荐、减脂、饮食”等分类展示,选项卡是最直接的切换入口。
怎么想:先查 Vant 官方文档的基础用法,用 van-tabs 包住多个 van-tab,每个 van-tab 的 title 是选项卡标题,内部放对应内容。
怎么做:在轮播图下方写四个选项卡,标题分别为关注、推荐、减脂、饮食。刷新页面后默认激活“关注”,点击“推荐”切到第 2 项,说明结构可用。随后对照设计图发现默认 Tabs 偏宽,加上 shrink 让选项卡更紧凑、靠左对齐;再发现下方小横条偏宽,把笔记中准备好的 van-tabs 覆盖样式放进全局 main.scss,因为项目里所有 Tabs 都要统一调整。关键写法:
vue
<van-tabs v-model:active="active" shrink sticky>
<van-tab title="关注">关注内容</van-tab>
<van-tab title="推荐">推荐内容</van-tab>
<van-tab title="减脂">减脂内容</van-tab>
<van-tab title="饮食">饮食内容</van-tab>
</van-tabs>易错点:shrink 解决的是选项卡紧凑问题,不是横条样式;横条样式要放全局样式文件,不要只写在当前页面里;sticky 需要页面内容足够长才能看出吸顶效果。
默认激活推荐与吸顶调试
为什么需要:设计图默认展示“推荐”,而不是索引 0 的“关注”;页面滚动时选项卡还要吸顶,方便切换。
怎么想:Vant Tabs 支持 v-model:active,active 表示当前选项卡索引,索引从 0 开始。关注是 0,推荐是 1,所以把响应式数据初始值设为 1。
怎么做:用 ref 定义 active,初值设为 1,绑定到 v-model:active。刷新后默认激活“推荐”。验证吸顶时,先在第二个选项卡下用 v-for 渲染约 100 条内容撑开盒子,滚动页面发现 Tabs 被顶走,再给 van-tabs 加 sticky,滚动后选项卡吸在顶部。
易错点:active 是索引不是标题文字;不要忘记导入 ref;内容太少时看不到吸顶,不是 sticky 失效;样式覆盖后要刷新确认,避免缓存误判。
小结
本节完成首页基础结构:头部、栅格导航、Swipe 轮播图;完成 Tabs 的四个选项卡、紧凑模式、全局横条样式、默认激活推荐和吸顶效果。提交代码时可记为“首页模块-页面搭建”。下一节才进入知识列表加载更多,不在本节展开。
先明确目标:为什么要做“知识列表加载更多”
首页每个 tab 下方都会展示文章列表,这个列表在业务里就是“知识列表”。一页不可能把所有文章一次性渲染出来,所以需要滚动到底部再加载下一页。这里的“加载更多”不是自己手写滚动监听,而是借助 Wot Design Uni 的 w-list 组件完成。
怎么想:先把“一条文章”抽成知识卡片组件,再把“一组文章”抽成知识列表组件,最后把列表组件放到每个 tab 下方。这样列表负责加载逻辑,卡片只负责展示,职责清晰。
怎么做:在 home/components 下新建 knowledge-card.vue 和 knowledge-list.vue;首页的每个 tab 里使用 knowledge-list。列表组件里先用 w-list 包住若干 knowledge-card。易错点:组件写完忘记 import;只在第一个 tab 放了列表,其他 tab 没放;卡片和列表的职责混在一起,后面接真实接口会很难改。
用 w-list 接住加载状态和触发事件
为什么需要 w-list:它已经提供了加载中、加载完成、触底加载这几个关键能力。怎么想:v-model:loading 控制“加载中”,finished 控制“是否还有更多”,finished-text 是加载完后的提示,@load 是滚动到底部时触发的事件。
怎么做:先在列表组件里定义响应式数据:
vue
<w-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<knowledge-card
v-for="(item, index) in list"
:key="index"
/>
</w-list>ts
import { ref } from 'vue'
const loading = ref(false)
const finished = ref(false)
const onLoad = () => {
console.log('load')
}调试时可以在浏览器 Vue 开发工具里观察 loading:默认是 false,滚动到底部触发 @load 后变成 true。易错点:loading 初始值不要设成 true,否则可能一开始就处于加载中;请求结束后必须把 loading 改回 false;finished 改成 true 后不会再触发加载更多;finished-text 要和 finished 配合使用。
模拟数据:先把加载更多的交互跑通
为什么先模拟:真实接口还没接时,先用假数据验证“触底 → 加载中 → 追加数据 → 加载完成”的完整链路。怎么想:用一个 list 存数据,在 onLoad 里用 setTimeout 模拟 2 秒网络延迟,每次追加 5 条;超过 20 条就认为没有更多数据。
怎么做:
ts
const list = ref<number[]>([])
const onLoad = () => {
setTimeout(() => {
const data = [1, 2, 3, 4, 5]
list.value.push(...data)
if (list.value.length > 20) {
finished.value = true
}
loading.value = false
}, 2000)
}模板里用 v-for 遍历 list。调试过程:刷新页面后滚动到底部,看到“加载中”;2 秒后新增 5 条;继续滚动会继续追加;到 25 条时显示“没有更多了”。易错点:追加数据要用 list.value.push(...data),不要直接覆盖;判断长度要写 list.value.length;模拟延迟期间不要重复触发加载;loading 一定要在追加完成后关闭。
从模拟到真实加载时要守住的边界
真实接口接入后,list 的类型要从 number[] 改成知识列表类型;onLoad 里改为 await 请求,拿到分页数据后追加 res.data.list;用当前页码和总页数判断是否 finished = true,否则页码加一;无论成功失败,最后都要把 loading 改回 false。易错点:不要用接口返回的当前页数据直接覆盖整个列表;请求失败也要关闭 loading;finished 和 finished-text 要成对处理,否则用户不知道是否还有数据。
先明确本节要解决什么问题
上一节做的是“模拟加载更多”,数据是前端自己造的,所以类型要求不高。到了真正请求后台时,问题就出现了:后台返回的文章字段很多,不同 tab 下请求的知识列表也不一样,而封装好的知识列表组件还要被“关注、推荐、减脂、饮食”四个 tab 复用。于是本节的核心不是写请求,而是先把类型边界划清楚。
按转写稿的推导,知识列表数据类型要定义三个地方:第一,响应数据类型;第二,请求参数类型;第三,知识列表组件的 props 类型。先把这三件事做好,下一节写 getKnowledgePage 和 onLoad 时才不会到处补类型。
响应数据类型:单篇文章、文章列表、分页响应要分开
为什么需要?因为后台返回的不是“一个文章对象”,而是带分页信息的列表数据。列表组件拿到后要追加到 list,还要判断总页数、总条数、当前页是否加载完。如果只写一个模糊类型,后面 res.data.list、pageTotal、total 很容易写错。
怎么想?按接口文档拆成三层:Knowledge 表示一篇文章/一条知识信息;KnowledgeList 表示文章数组;分页响应类型里包含总页数、总条数和 list,其中 list 指向 KnowledgeList。转写稿里明确说这些字段较多,笔记中已准备好,直接拷贝到 consult.d.ts 中,并保留注释,暂时不逐字段解释。
怎么做?在类型声明文件里新增 consult.d.ts,把响应类型粘贴进去,检查 list 是否引用文章列表类型。易错点是:不要把单篇文章类型直接当成列表类型;不要自己猜字段名;分页字段的含义要分清,“总页数”和“总条数”不是一回事。
请求参数类型:用字面量联合类型约束 type
为什么需要?不同 tab 请求不同列表,必须告诉后台“我要关注、推荐、减脂还是饮食”。如果 type 只写成普通 string,拼写错误编辑器发现不了,运行时才暴露。
怎么想?查询参数对象至少包含 type、current、pageSize。其中 type 不是任意字符串,而是从四个值里选一个,所以用字面量联合类型,例如关注是 like、推荐是 recommend、饮食是 food,减脂按接口文档中的对应值选择。转写稿中特别强调“这是一组数据当中去选一个”,这就是联合类型的意义。
怎么做?在 consult.d.ts 下方定义查询参数类型,把 type 声明为知识类型,current 表示当前页码,pageSize 表示一页多少条。易错点:不要把 type 写成 string;页码和每页条数不要传反;类型值必须和接口文档一致,不能凭中文含义自己造。
给知识列表组件加 props,让复用组件知道请求哪类列表
为什么需要?knowledge-list 被四个 tab 共用。组件内部发请求时,必须知道当前是关注还是推荐。最直接的办法是暴露一个 type prop,由使用方传入。
怎么想?在组件里用 defineProps 声明 type: KnowledgeType。这样使用组件时就有类型提示,也必须传值。转写稿里的调试过程很典型:props 加好后,index.vue 原来使用了四次列表组件,立刻报错,因为四个地方都没传 type。于是逐个补:关注传 like,推荐传 recommend,减脂按文档选对应值,饮食传 food。补完后报错消失。
怎么做?在 knowledge-list 中定义 props,并在后续初始化查询参数时通过 props.type 取到类型。使用方在四个 tab 下分别写 :type="..."。易错点:漏传 type;把四个 tab 的值传串;以为 props 定义完就自动有默认值——本节没有说默认值,仍应显式传。
检查清单与提交
本节完成后应检查:consult.d.ts 是否有文章、文章列表、分页响应三段;查询参数是否包含 type/current/pageSize;knowledge-list 是否声明 type prop;index.vue 四个列表组件是否都补了 type。转写稿最后提交信息是“首页模块 知识列表类型”,可用 feat: 首页模块 知识列表类型。注意本节只定义类型,真正请求和渲染在下一节完成,不要提前把模拟逻辑删乱。
代码示例
示例 1:views/home/index.vue —— 首页基础结构 + Tabs 切换(含吸顶、默认激活推荐)
vue
<script setup lang="ts">
import { ref } from 'vue'
import KnowledgeList from './components/KnowledgeList.vue'
import FollowDoctor from './components/FollowDoctor.vue'
// 默认激活「推荐」选项卡,其索引为 1(关注=0)
const active = ref(1)
</script>
<template>
<div class="home-page">
<!-- 1. 头部容器 -->
<div class="home-header">
<h3>推荐</h3>
</div>
<!-- 2. 导航:Vant 栅格把一行分成 24 份,三等分即每列 span=8 -->
<div class="home-nav">
<van-row>
<van-col span="8">快速问诊</van-col>
<van-col span="8">特需门诊</van-col>
<van-col span="8">名医直播</van-col>
</van-row>
</div>
<!-- 3. 轮播图:每张图一个 van-swipe-item -->
<div class="home-banner">
<van-swipe :autoplay="3000">
<van-swipe-item>图片1</van-swipe-item>
<van-swipe-item>图片2</van-swipe-item>
<van-swipe-item>图片3</van-swipe-item>
</van-swipe>
</div>
<!-- 4. Tabs:shrink 让选项卡更紧凑,sticky 实现吸顶,v-model:active 双向绑定索引 -->
<van-tabs v-model:active="active" shrink sticky>
<van-tab title="关注">
<!-- 关注选项卡下多一个「推荐关注」组件 -->
<follow-doctor />
<knowledge-list type="like" />
</van-tab>
<van-tab title="推荐">
<knowledge-list type="recommend" />
</van-tab>
<van-tab title="减脂">
<knowledge-list type="fat" />
</van-tab>
<van-tab title="饮食">
<knowledge-list type="food" />
</van-tab>
</van-tabs>
</div>
</template>关键行与易错点
v-model:active="active":active就是选项卡的索引,0 是关注、1 是推荐;想默认显示推荐就把它初始化为1,不是写"推荐"。shrink:让选项卡收缩紧凑、左对齐,不加时选项卡会均分整屏宽度,和设计图不符。sticky:滚动到页面顶部时吸顶。注意内容太少时看不出效果,需要先用v-for="i in 100"之类的循环把页面撑开再验证。- 选项卡下方的小横条样式需要在
styles/main.scss里做全局覆盖(笔记已提供),因为项目里所有van-tab都要统一改,所以不能写在单个组件里。 type的取值来自接口文档的四种文章类型,四个列表必须传不同的type,否则后台返回的是同一份数据。
示例 2:views/home/components/KnowledgeList.vue —— 知识列表加载更多(van-list)
vue
<script setup lang="ts">
import { ref } from 'vue'
import type { KnowledgeList, KnowledgeParams, KnowledgeType } from '@/types/consult'
import { getKnowledgePage } from '@/services/consult'
import KnowledgeCard from './KnowledgeCard.vue'
// props:告诉组件当前要加载哪一类知识列表
const props = defineProps<{ type: KnowledgeType }>()
const list = ref<KnowledgeList>([])
const loading = ref(false) // 加载中状态,控制列表底部「加载中…」
const finished = ref(false) // 是否已全部加载完毕
// 查询参数:当前第几页、每页几条
const params = ref<KnowledgeParams>({
type: props.type,
current: 1,
pageSize: 5
})
// 滚动到底部触发
const onLoad = async () => {
const res = await getKnowledgePage(params.value)
// 追加数据,注意是 ... 展开后 push
list.value.push(...res.data.rows)
// 当前页码 >= 总页数,说明没有更多数据了
if (params.value.current >= res.data.pageTotal) {
finished.value = true
} else {
params.value.current++
}
// 不管成功失败,都要把 loading 改回 false
loading.value = false
}
</script>
<template>
<div class="knowledge-list">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<knowledge-card v-for="item in list" :key="item.id" :item="item" />
</van-list>
</div>
</template>关键行与易错点
v-model:loading:滚动到底部时 Vant 会自动把它置为true并触发@load,你在请求结束后必须手动改回false,否则列表会一直卡在「加载中」。finished+finished-text必须配合使用:finished = true时底部显示finished-text的文字,只改finished不写文字,用户看不到任何提示。list.value.push(...res.data.rows):用展开运算符追加,直接list.value = res.data.rows会把之前加载的页覆盖掉。- 页码判断用
>=而不是>,并且current++要写在else分支里,否则最后一页还会继续请求。 - 类型定义在
types/consult.ts:Knowledge(单篇文章)、KnowledgeList(文章数组)、KnowledgePage(带分页信息的响应体,含pageTotal/total/rows)、KnowledgeParams(请求参数)。 - 通用分页参数
PageParams(current+pageSize)应抽出来,KnowledgeParams用交叉类型PageParams & { type: KnowledgeType }复用。
示例 3:views/home/components/KnowledgeCard.vue —— 知识卡片渲染(富文本、图片适配)
vue
<script setup lang="ts">
import type { Knowledge } from '@/types/consult'
// 接收父组件传进来的单篇文章数据
defineProps<{ item: Knowledge }>()
// 文章内容是爬取来的富文本,只展示纯文字:把 <xxx> 标签整体替换成空串
const stripHtml = (content: string) => content.replace(/<[^>]+>/g, '')
</script>
<template>
<div class="knowledge-card">
<!-- 医生信息 -->
<div class="head">
<van-image round :src="item.creatorAvatar" />
<div class="info">
<p>{{ item.creatorName }}</p>
<p>
{{ item.creatorHospatalName }}
{{ item.creatorDep }}
{{ item.creatorTitles }}
</p>
</div>
<!-- likeFlag 为 1 表示已关注 -->
<van-button size="small" round>
{{ item.likeFlag === 1 ? '已关注' : '加关注' }}
</van-button>
</div>
<!-- 标题 -->
<p class="title">{{ item.title }}</p>
<!-- 标签 -->
<div class="tags">
<van-tag v-for="(tag, i) in item.topics" :key="i">{{ tag }}</van-tag>
</div>
<!-- 内容:必须过滤 HTML 标签 -->
<p class="content">{{ stripHtml(item.content) }}</p>
<!-- 图片:一张图时加 large 类名放大 -->
<div class="images" :class="{ large: item.coverUrl.length === 1 }">
<van-image
v-for="(url, i) in item.coverUrl"
:key="i"
:src="url"
fit="cover"
/>
</div>
<!-- 底部数据 -->
<div class="footer">
<span>{{ item.collectionNumber }} 收藏</span>
<span>{{ item.commentNumber }} 评论</span>
</div>
</div>
</template>关键行与易错点
fit="cover":不加这个属性,图片会被拉伸变形;cover是铺满容器、保持比例裁剪。content.replace(/<[^>]+>/g, ''):<[^>]+>表示「尖括号 + 中间任意非>字符 + 尖括号」,g是全局替换;漏写g只会替换掉第一个标签,漏写[^>]会把两个标签之间的正文一起吃掉。:class="{ large: item.coverUrl.length === 1 }":一张图时放大、三张图时平分一行,large的样式在笔记里已准备好,注意是动态绑定 class,不能写死。item.likeFlag === 1:后端用数字 1/0 表示关注状态,不是布尔值,用=== 1判断最稳。v-for的key用索引在标签、图片这类静态渲染里没问题,但列表渲染(KnowledgeList里)必须用item.id。- 字段名(
creatorAvatar、creatorHospatalName、creatorDep、creatorTitles、topics、coverUrl、collectionNumber、commentNumber)以接口文档为准,鼠标悬停即可看到注释提示。
常见错误
1. 滑动卡片宽度写死 150px,换到 414 设备上卡片之间没间距
- 现象:375 模拟器下正常,切到 iPhone Plus 后卡片紧贴、滑动一次跨度过小或过大。
- 原因:150px 是按 375 设计稿量出来的绝对值,而卡片本身随 rem/vw 等比缩放,容器宽度没跟着换算。
- 修法:按
150 / 375 * 当前设备宽度计算,当前宽度用useWindowSize()的width实时取,不要用window.innerWidth一次性取值。
2. 列表一直卡在“加载中”,再也触发不了 load
- 现象:滚到底转圈不停,或者第二次滚动没有反应。
- 原因:
v-model:loading只在请求成功分支里置回false,接口报错或提前 return 时状态没复位。 - 修法:
loading的复位必须放在finally(或请求外层),与成功失败无关。
3. 最后一批数据加载完后仍继续发请求 / 不显示“没有更多”
- 现象:明明只有 2 页,滚到底还在请求第三页;或
finished已置true但页面没有提示文案。 - 原因:分页判断写成
current > total(应为>=),或只加了v-model:finished忘了finished-text。 - 修法:判断改为
currentPage >= res.data.pageTotal,同时补finished-text="没有更多了";未结束时才执行currentPage++。
4. 文章内容里露出 <p>、<img> 等 HTML 标签
- 现象:卡片正文直接显示一堆尖括号源码。
- 原因:接口返回的是爬取来的富文本,直接
{{ item.content }}插值渲染。 - 修法:
item.content.replace(/<[^>]+>/g, '')去标签后再展示;确实需要富文本再用v-html并做安全过滤。
5. 关注按钮请求失败后一直转圈,无法再次点击
- 现象:弱网/接口 500 后按钮 loading 不消失,用户点不动。
- 原因:
loading = false写在await之后的正常流程里,异常直接跳出函数。 - 修法:
try { ... } finally { loading.value = false },同时前端like_flag取反也要放在成功分支内,避免失败时状态与后台不一致。
6. 组件加了必填 props 后页面报类型错误 / 渲染空白
- 现象:
knowledge-list使用处标红,或doctor-card里item为 undefined。 - 原因:
defineProps里type声明为必填,但四个 Tab 下的 list 组件没逐个传;或父组件v-for时忘了把item传进去。 - 修法:使用处补齐
:type="'like' | 'recommend' | 'diet' | 'food'",v-for中显式:item="item",:key用item.id而不是索引。
练习
把
knowledge-list里的分页逻辑抽成一个useLoadMore组合式函数(接收一个 fetcher),要求knowledge-list和后续任意列表组件都能复用,并在接口报错时页面能提示“加载失败,上滑重试”。给推荐医生滑动区做断点适配:375 下每屏显示约 2.5 张卡片,768 下显示约 4.5 张。要求不使用
useWindowSize,只用 CSS(flex-basis+calc)实现,并说明两种方案各自的取舍。现在四个 Tab 的知识列表共用一个
knowledge-list组件实例还是四个实例?滚动切换 Tab 后列表数据会丢失吗?请动手验证并给出结论;如果会丢,设计一个缓存方案(提示:v-show与v-if、keep-alive的差异)。为关注按钮加上“防抖 + 乐观更新 + 失败回滚”:点击立即切换 UI 状态,请求失败时回滚并 toast 提示。对比课上“先请求后改状态”的写法,说明各自的适用场景。
手写实现
useWindowSize的等价逻辑(onMounted监听 resize、onUnmounted解绑),再把它和@vueuse/core版本对比:在 SSR、页面隐藏再恢复、快速拖动缩放这三种情况下,原生写法分别会有什么问题?
课件图示






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 首页模块-搭建页面 | 10:05 |
| 2 | 首页模块-知识列表加载更多效果 | 15:03 |
| 3 | 首页模块-知识列表数据类型 | 08:35 |
| 4 | 首页模块-知识列表加载功能 | 18:33 |
| 5 | 首页模块-搭建推荐关注组件 | 09:08 |
| 6 | 首页模块-适配滑动宽度-采用VueUse | 14:13 |
| 7 | 首页模块-推荐医生展示 | 10:42 |
| 8 | 首页模块-关注医生 | 12:18 |
小结
- 移动端一切尺寸都要落在“比例”上,凡是从设计稿量出来的绝对值,都要过一遍
当前宽度 / 设计宽度的换算,否则换机型必翻车。 - 列表加载更多的核心不是“怎么加载”,而是“三个状态机”——
loading、finished、currentPage的一致性;状态复位必须走finally,不能依赖 happy path。 - 富文本字段是前端渲染的高发区:插值渲染会露标签,
v-html会带 XSS 风险,两者都要在拿到数据的第一时间处理,而不是在模板里补救。 - 图片的三个高频坑(变形、单图过小、多图排版)都可以用组件属性 + 动态 class 解决,不必手写 CSS hack。
- 组件 props 一旦声明为必填,就是在用类型系统做“调用方契约检查”;报错不是麻烦,是提前暴露了漏传参数。
- 能用成熟组合式工具库解决的通用能力(窗口尺寸、滚动位置、可视区、倒计时、复制),不要自己写生命周期 + 事件监听,原生写法在解绑、SSR、边界场景上更容易留坑。
- 每个功能点的收尾动作都是“提交 + 在真机/多机型上验证”,只在 375 模拟器上点通不算完成。