Skip to content

首页模块与极速问诊(一) ​

本页对应 首页模块与极速问诊,共 8 段视频,约 1 小时 38 分。

学习目标 ​

  1. 能够搭建首页基础结构,使用 Vant 导航、轮播图与 Tabs 组件实现紧凑模式、默认激活“推荐”及吸顶效果,并在浏览器中验证交互正确。
  2. 能够封装知识卡片与知识列表组件,使用 van-list 的 loading、finished、load 等属性/事件实现加载更多,并通过模拟分页数据验证加载、加载中和无更多数据状态。
  3. 能够定义知识列表相关 TypeScript 类型,包括知识项、知识列表、分页响应、查询参数及知识类型联合类型,并为列表组件 props 添加类型约束。
  4. 能够封装知识列表 API,在 onLoad 中按页码和每页条数请求数据,追加列表数据、判断是否加载完成、更新加载状态,并完成知识卡片渲染及富文本标签、图片变形和单图样式处理。
  5. 能够使用 VueUse 的 useWindowSize 获取响应式窗口宽度,按比例计算推荐关注组件的滑动宽度,并配置 Swiper 关闭指示器与循环滚动以完成适配。
  6. 能够定义医生与关注相关类型及 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 标签

  • 现象:卡片正文直接显示一堆尖括号源码。
  • 原因:接口返回的是爬取来的富文本,直接 &#123;&#123; item.content &#125;&#125; 插值渲染。
  • 修法: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 而不是索引。

练习 ​

  1. 把 knowledge-list 里的分页逻辑抽成一个 useLoadMore 组合式函数(接收一个 fetcher),要求 knowledge-list 和后续任意列表组件都能复用,并在接口报错时页面能提示“加载失败,上滑重试”。

  2. 给推荐医生滑动区做断点适配:375 下每屏显示约 2.5 张卡片,768 下显示约 4.5 张。要求不使用 useWindowSize,只用 CSS(flex-basis + calc)实现,并说明两种方案各自的取舍。

  3. 现在四个 Tab 的知识列表共用一个 knowledge-list 组件实例还是四个实例?滚动切换 Tab 后列表数据会丢失吗?请动手验证并给出结论;如果会丢,设计一个缓存方案(提示:v-show 与 v-if、keep-alive 的差异)。

  4. 为关注按钮加上“防抖 + 乐观更新 + 失败回滚”:点击立即切换 UI 状态,请求失败时回滚并 toast 提示。对比课上“先请求后改状态”的写法,说明各自的适用场景。

  5. 手写实现 useWindowSize 的等价逻辑(onMounted 监听 resize、onUnmounted 解绑),再把它和 @vueuse/core 版本对比:在 SSR、页面隐藏再恢复、快速拖动缩放这三种情况下,原生写法分别会有什么问题?

课件图示 ​

课件第11页:首页模块-搭建推荐关注组件 推荐关注组件 医生卡片组件 Swipe调整

课件第7页:首页模块-知识列表数据类型 响应数据类型 请求参数类型 props类型

课件第9页:首页模块-知识列表加载功能 知识列表API 准备查询参数 加载列表数据 知识卡片

课件第3页:首页模块-搭建页面 组件基础结构 知识列表Tab

课件第15页:首页模块-推荐医生展示 医生TS类型 获取医生API 数据和渲染

课件第17页:首页模块-实现关注医生 关注TS类型 关注API 关注&取消

原视频对照 ​

#视频时长
1首页模块-搭建页面10:05
2首页模块-知识列表加载更多效果15:03
3首页模块-知识列表数据类型08:35
4首页模块-知识列表加载功能18:33
5首页模块-搭建推荐关注组件09:08
6首页模块-适配滑动宽度-采用VueUse14:13
7首页模块-推荐医生展示10:42
8首页模块-关注医生12:18

小结 ​

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