主题
需求介绍与搭建静态组件(Day14 3/3)
本页对应 React + TypeScript + 医通项目,共 5 段视频,约 1 小时 0 分。
学习目标
- 能够从路由中获取医院 ID,在组件挂载后调用详情接口,定义状态并完成医院名称、编码、地址、图片、预约规则等字段渲染,并对空图片进行条件渲染。
- 能够封装医院上下线状态修改 API,在列表点击事件中根据当前状态取反调用接口,成功后重新获取医院列表刷新数据。
- 能够创建排班路由组件并配置带
houseCode参数的路由,在医院列表点击排班时使用编程式导航完成跳转。 - 能够使用 Card、Grid、Tag、Pagination、Table 等完成医院排班页静态布局,并根据可视区域高度计算左侧滚动区域高度。
- 能够进入排班页获取
houseCode,挂载后请求科室列表并存入状态,通过fieldNames映射 Tree 的title/key/children字段完成科室树渲染。 - 能够为一级科室节点添加禁用属性,生成并绑定
expandedKeys展开一级科室,设置默认选中科室并通过selectKeys实现默认选中。
知识点
需求清单与实现顺序
本节需求包括:医院列表静态组件、分页展示、省市区三级联动搜索、查询医院列表、清空搜索条件、查看医院详情、医院排班、医院上线。转写稿重点落地了详情渲染、上下线切换、排班路由、排班静态布局和科室树渲染;省市区联动与查询清空属于同一业务闭环中的搜索能力,应按“先静态结构、再接口、再交互”的顺序补上。
为什么需要先列清单:医院业务不是单个页面,而是“列表→详情→排班→状态维护”的链路。怎么想:先明确每个功能依赖的数据和路由参数,再决定组件与接口。怎么做:把静态结构、接口封装、状态管理、模板渲染分开推进。易错点:不要先堆交互再补类型;路由参数、接口返回字段要先确认,否则后面到处断言。
医院详情:路由参数与请求时机
为什么需要:详情页必须根据列表传入的 ID 获取数据。怎么想:进入页面先通过 useParams 取路由参数,再在 useEffect 中发请求;没有 ID 不应发无意义请求。怎么做:转写稿中的关键代码可整理为:
tsx
const { id } = useParams();
const [bookingRule, setBookingRule] = useState<IBookingRule>();
const [hospital, setHospital] = useState<IHospitalItem>();
const getHospitalDetail = async (id: string) => {
const res = await api.getHospitalDetail(id);
setBookingRule(res.bookingRule);
setHospital(res.hospital);
};
useEffect(() => {
if (id) getHospitalDetail(id as string);
}, [id]);易错点:id 可能是 undefined,React 会提示联合类型,需要判空或断言;首次渲染时 hospital 还是 undefined,模板读取 hospital.hosname 会报错,应使用 hospital?.hosname。图片也要条件渲染,避免空 src:
tsx
{hospital?.logoData && (
<Image width={100} src={`${IMG_PREFIX}${hospital.logoData}`} />
)}预约规则是数组,渲染时要 map 并加 key:
tsx
{bookingRule?.rule?.map((item, index) => (
<p key={index}>{item}</p>
))}上下线切换:接口成功后刷新列表
为什么需要:列表上的“上线/未上线”必须与服务器状态一致。怎么想:点击时根据当前状态取反,调用状态修改接口,成功后重新获取列表。怎么做:
ts
const changeStatus = (id: string, status: number) =>
request.get(`/hospital/update/status/${id}/${status}`);
const updateStatus = async (row: IHospitalItem) => {
const nextStatus = row.status === 1 ? 0 : 1;
await changeStatus(row.id, nextStatus);
getHospitalList();
};易错点:不能只改本地状态,否则刷新后丢失;参数类型要统一;修改成功后要刷新列表,而不是手动猜新数据。
排班路由与跳转
为什么需要:排班页依赖医院编码。怎么想:列表点击“排班”时用编程式导航带 hoscode 跳转,路由配置动态参数。怎么做:
tsx
navigate(`/hospital/schedule/${row.hoscode}`);路由侧配置 hospital/schedule/:hoscode,排班组件内用 useParams 取 hoscode。易错点:路由参数名必须与 useParams 解构一致;跳转路径最后一段不要漏。
排班静态布局
为什么需要:排班页信息多,需要“上方导航、左侧科室树、右侧表格与分页”的布局。怎么想:用 Card 包整体,Row/Col 分栏,左侧树区域滚动,右侧放标签、表格、分页。怎么做:
tsx
const height = document.documentElement.clientHeight - 180;
<Row gutter={30}>
<Col span={5}>
<div style={{ height, overflowY: "scroll" }}>树区域</div>
</Col>
<Col span={19}>表格区域</Col>
</Row>表格关闭自带分页:pagination={false};分页组件单独放。易错点:gutter 拼写;高度写死不自适应;左侧没有 overflowY 会出现整页滚动。
科室树:字段映射、禁用、展开与选中
为什么需要:接口字段通常不是 Tree 需要的 title/key/children。怎么想:用 fieldNames 做映射,再处理一级科室禁用、默认展开和默认选中。怎么做:
tsx
<Tree
treeData={departmentList as any}
fieldNames={{ title: "depName", key: "depCode", children: "children" }}
expandedKeys={expandedKeys}
selectedKeys={selectedKeys}
/>一级科室加 disabled: true,展开键取所有一级科室 depCode,默认选中第一个一级科室下第一个子科室。易错点:直接传接口数组会字段不匹配;children 可能为空,取子项要断言;disabled、expandedKeys、selectedKeys 要按组件文档使用,转写稿中口述的属性名需以文档纠正。
整体套路仍是:定义类型→封装 API→在 useEffect 中调用→声明状态并设置→模板中用插值表达式渲染,熟练处理列表映射、条件渲染和树组件映射。
本节的目标是先把医院排班页的静态组件搭起来,再逐步接数据。笔记里有一句很关键:可以从官方文档复制组件代码,但更建议从上一个 HospitalSet 组件复制代码,更方便。 因为项目内组件已经包含现成的 Card、表格、分页、按钮和 API 调用风格,复制后改字段比从零拼更稳。
为什么优先复制 HospitalSet
为什么需要:官方示例是通用 Demo,字段、路由、接口封装都不一定符合当前项目;直接照搬容易把示例字段带进来。
怎么想:静态组件阶段先不急着接数据,先保证路由能进、布局能看、表格列和分页位置正确。复制 HospitalSet 后,把标题、列、路由参数名改成排班页需要的。
怎么做:新建 HospitalSchedule.tsx,先保留 Card、Row、Col、Table、Pagination、Tree 的骨架,页面顶部放面包屑/标题,中间左树右表,底部返回按钮。
易错点:分页如果从 Table 的 pagination 属性拆成独立组件,要记得给 Table 加 pagination={false},否则会出现两个分页。
路由参数与详情请求
为什么需要:医院详情页一进入就要根据路由里的 id 发请求,否则页面没有数据可渲染。
怎么想:用 useParams 取参数,在 useEffect 里等组件挂载完成再调用 async 函数;id 可能是 string | undefined,请求前要判空或断言。
怎么做:
tsx
const { id } = useParams()
const [hospital, setHospital] = useState<IHospitalItem>()
const getHospitalDetail = async () => {
if (!id) return
const res = await getHospitalDetailApi(id as string)
setHospital(res)
}
useEffect(() => {
getHospitalDetail()
}, [])易错点:不要直接在组件函数体里发请求;useEffect 依赖别漏;id 联合类型不处理会报红。
状态定义与条件渲染
为什么需要:首次渲染时 hospital 还是 undefined,直接读 hospital.hospitalName 会报错。
怎么想:状态初始值可以为空,模板里用可选链 ?.;图片这类必须要有值的,用条件渲染。
怎么做:
tsx
<h3>{hospital?.hospitalName}</h3>
<img
width={100}
src={hospital?.logoData ? `图片前缀${hospital.logoData}` : ''}
/>易错点:插值表达式里忘记 ?;图片没有 logoData 时仍渲染空图;地址、乘车路线等字段要按接口实际结构取,别凭字段名猜。
上下线切换
为什么需要:点击“上线/下线”不只是改页面文字,还要改服务器状态并刷新列表。
怎么想:封装 changeStatus(id, status),点击时取反原状态,await 成功后重新调 getHospitalList()。
怎么做:
tsx
const changeStatus = async (id: string, status: number) => {
await changeStatusApi(id, status)
getHospitalList()
}
// 点击回调
const nextStatus = row.status === 1 ? 0 : 1
changeStatus(row.id, nextStatus)易错点:只改本地状态会导致刷新后恢复;状态传参保持 number;请求后要刷新列表。
排班页静态布局与 Tree 配置
为什么需要:排班页是左树右表结构,树的数据字段通常和 Tree 默认的 title/key/children 不一致。
怎么想:先用 Row/Col 分栏,左侧树给固定高度和 overflow-y: auto;树用 fieldNames 做字段映射;一级科室要禁用、展开,默认选中某个子科室。
怎么做:
tsx
<Tree
treeData={departmentList as any}
fieldNames={{ title: 'deptName', key: 'deptCode', children: 'children' }}
checkable={false}
expandedKeys={expandedKeys}
selectedKeys={selectedKeys}
/>给一级科室加 disabled: true,展开项取一级 deptCode 数组,默认选中项取子科室 deptCode。departmentList 与 TreeDataNode 类型不一致时,转写稿里的处理是断言成空数组/合适类型,再配合映射,不要硬塞。
易错点:checkable 不关会出现复选框;expandedKeys、selectedKeys 要传数组;子科室 children 可能是空值,取第一项前要断言或判空;Table 的列 columns 要按排班字段改,不要沿用医院列表列。
分页组件与表格分页要分开处理
为什么需要:排班页底部要展示分页,同时中间还要展示表格。AntD 的 Table 默认自带分页,如果直接使用,会出现“表格自带一个分页、页面又放一个分页”的重复问题。
怎么想:分页应当作为独立组件引入,表格只保留表头和数据区域。先搭静态结构,再考虑数据。
怎么做:引入 Pagination,在表格附近放置分页组件;表格通过 pagination={false} 关闭默认分页,并定义 columns 显示表头。转写稿中的思路是:
tsx
import { Pagination, Table } from 'antd'
<Pagination />
<Table
columns={columns}
pagination={false}
/>表头列可按转写稿整理为:
tsx
const columns = [
{ title: '职称', dataIndex: 'title' },
{ title: '号源时间', dataIndex: 'time' },
{ title: '总预约数', dataIndex: 'total' },
{ title: '剩余预约数', dataIndex: 'remain' },
{ title: '擅长技能', dataIndex: 'skill' },
]易错点:只放分页组件却忘记关闭 Table 自带分页;columns 的类型和字段没有按实际数据核对,导致表头或数据错位。
排班页静态布局与高度自适应
为什么需要:排班页上方是路径导航,中间左侧是树形科室,右侧是标签、分页和表格,下方还有返回按钮。布局不稳,后面的分页和表格会挤在一起。
怎么想:外层用 Card,中间用栅格 Row/Col 分成左右两列;左列放树形结构,右列放分页和表格。左右列之间用 gutter 留间隙,左列内容多时出现纵向滚动条。
怎么做:转写稿中用 Row、Col、gutter={30},左列设置 overflow-y: scroll,高度按可视区域计算:
tsx
const height = document.documentElement.clientHeight - 180再把计算出的高度用于左侧容器,避免写死高度。调试时先写死 500,发现两列紧贴,再查 API 加 gutter;滚动条不出现时,检查是否设置了 overflow-y: scroll。
易错点:高度写死导致小屏幕溢出;gutter 写成普通样式而不是栅格属性;滚动容器没有固定高度,overflow-y 不生效。
进入排班路由后获取科室列表
为什么需要:树形科室的数据来自后端,不能一直用临时假数据。进入排班页后,需要根据路由中的 houseCode 请求科室列表。
怎么想:先用 useParams 取 houseCode,在 useEffect 中组件挂载完成后调用异步函数;请求前判断 houseCode 是否存在,避免发无意义请求。
怎么做:转写稿中的流程是定义 getDepartList,传 houseCode,打印返回值确认结构,再设置状态:
tsx
const { houseCode } = useParams()
const [departmentList, setDepartmentList] = useState<IDepartmentList[]>([])
useEffect(() => {
const getDepartList = async () => {
const res = await getDepartmentList(houseCode as string)
setDepartmentList(res)
}
if (houseCode) getDepartList()
}, [houseCode])调试时先 console.log(departmentList),确认返回的是科室数组,再替换树组件的临时数据。houseCode 从路径参数解构出来可能是联合类型,转写稿中通过断言处理。
易错点:组件一渲染就读未定义数据;不判断 houseCode 就请求;忘记把请求结果设置成状态,页面仍然显示假数据。
树形组件要处理字段映射
为什么需要:后台返回的科室字段通常是 departmentCode、departmentName、children,而 Tree 组件默认认识 key、title、children。直接把后台数据塞进去,可能不报错但文字渲染不出来。
怎么想:转写稿中先尝试把 departmentList 传给 treeData,出现类型报错;断言成空数组后不报错了,但页面没有文字。继续排查才发现字段名不一致,需要使用 fieldNames 建立映射。
怎么做:
tsx
<Tree
fieldNames={{
key: 'departmentCode',
title: 'departmentName',
children: 'children',
}}
/>易错点:只做类型断言,不做字段映射;看到不报错就以为渲染成功;children 可能为 null,后续取子项时要断言。
禁用一级科室、展开节点与默认选中
为什么需要:一级科室只是分类,不应该被选中;页面打开时一级科室应默认展开,并默认选中某个子科室的第一项。
怎么想:给所有一级科室对象添加 disabled: true,用 map 生成新数组;展开项取一级科室的 departmentCode 组成数组;默认选中取子科室第一项的 departmentCode。
怎么做:转写稿中的关键处理是:
tsx
const list = departmentList.map((item) => ({
...item,
disabled: true,
}))展开项和选中项分别设置到 Tree 的 expandedKeys、selectedKeys 上。调试时先打印 expandedKeys 是否有 12 个一级科室编码,再确认 selectedKeys 是否选中子科室。
易错点:disabled 写成 false;把 selectedKeys 误写成 checkedKeys;子科室 children 为 null 时直接取 [0] 会报错。
上下线后要刷新列表数据
为什么需要:医院上下线会修改服务器状态,列表和分页数据必须重新获取,不能只改本地显示。
怎么想:点击上下线按钮时,传当前医院 id 和取反后的状态给 changeStatus 接口;请求成功后重新调用医院列表方法。
怎么做:转写稿中的调试是先打印 id 和状态,确认点击后从 0 变 1、从 1 变 0,再调用接口并刷新列表:
tsx
const updateStatus = async (id: string, status: number) => {
await changeStatus(id, status)
getHospitalList()
}易错点:只改前端状态不重新请求;状态取反写错;请求成功后忘记刷新列表,导致分页数据与服务器不一致。
课堂画面





代码示例
示例 1:医院详情页——数据渲染
tsx
// src/pages/hospital/HospitalDetail/index.tsx
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Image, Descriptions } from 'antd';
import { getHospitalDetail } from '@/api/hospital';
import type { IHospitalItem, IBookingRule } from '@/types/hospital';
export default function HospitalDetail() {
// 1. 拿到路由参数 id
const { id } = useParams();
// 2. 声明两个状态,初始值都是 undefined
const [hospital, setHospital] = useState<IHospitalItem>();
const [bookingRule, setBookingRule] = useState<IBookingRule>();
const getDetail = async (id: string) => {
const res = await getHospitalDetail(id);
setBookingRule(res.bookingRule);
setHospital(res.hospital);
};
// 3. 组件挂载完成后发请求
useEffect(() => {
if (id) getDetail(id as string);
}, []);
return (
<div>
<h1>{hospital?.hosname}</h1>
{/* 4. 图片前缀 + 条件渲染,避免首次渲染出现空图 */}
{hospital?.logoData && (
<Image width={100} src={'data:image/png;base64,' + hospital.logoData} />
)}
<Descriptions column={1}>
<Descriptions.Item label="医院编码">{hospital?.hoscode}</Descriptions.Item>
<Descriptions.Item label="医院地址">
{hospital?.param?.fullAddress}
</Descriptions.Item>
<Descriptions.Item label="坐车路线">{hospital?.route}</Descriptions.Item>
<Descriptions.Item label="医院简介">{hospital?.intro}</Descriptions.Item>
</Descriptions>
{/* 5. 预约规则是一个字符串数组,要 map 渲染 */}
<div>
{bookingRule?.rule?.map((item, index) => (
<p key={index}>{item}</p>
))}
</div>
</div>
);
}要点解释
useParams()取到的id类型是string | undefined,直接传给 API 会报红色类型错误,需要id as string断言。useEffect依赖数组传[],表示只在挂载完成后执行一次;里面要加if (id)判断,没 id 发请求没意义。hospital?.hosname的可选链是关键:页面第一次渲染时hospital还是undefined,不加?直接读.hosname会报错;挂载完成拿到数据后第二次渲染才有值。- 图片必须先判断
hospital?.logoData再渲染,否则第一次渲染会渲染出一个空图片。 - 预约规则
rule是数组,用map渲染时每一项要给key。
示例 2:医院上下线切换
ts
// src/api/hospital.ts
import request from '@/utils/request';
// 修改医院上下线状态:不需要定义返回值类型,两个参数也很简单
export const changeStatus = (id: string, status: number) => {
return request(`/hospital/${id}/status/${status}`);
};tsx
// src/pages/hospital/HospitalList/index.tsx(片段)
import { Button } from 'antd';
import { changeStatus } from '@/api/hospital';
// 点击上下线按钮触发的事件回调
const updateStatus = async (id: string, status: number) => {
// status 是「最终要变成的值」:原来是 1 改成 0,原来是 0 改成 1
await changeStatus(id, status);
// 服务器数据变了,列表数据也要重新拉一次
getHospitalList();
};
// 表格操作列
<Button
type="link"
onClick={() => updateStatus(item.id!, item.status === 1 ? 0 : 1)}
>
{item.status === 1 ? '下线' : '上线'}
</Button>要点解释
- 接口路径用模板字符串拼接
`/hospital/${id}/status/${status}`,注意${}不要写漏$。 status传的是修改后的值,不是当前值:item.status === 1 ? 0 : 1。- 修改成功后必须调用
getHospitalList()重新获取列表,否则页面显示的还是旧状态。 - 事件回调要加
async,否则await不生效。
示例 3:医院排班页——科室树渲染(映射 / 禁用 / 展开 / 默认选中)
tsx
// src/pages/hospital/HospitalSchedule/index.tsx
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Tree } from 'antd';
import { getDepartList } from '@/api/hospital';
import type { IDepartmentList } from '@/types/hospital';
export default function HospitalSchedule() {
const { hoscode } = useParams();
const [departmentList, setDepartmentList] = useState<IDepartmentList>([]);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]); // 一级科室 depCode 组成的数组
const [selectedKeys, setSelectedKeys] = useState<string[]>([]); // 默认选中的科室
const getDepart = async (hoscode: string) => {
const list = await getDepartList(hoscode);
// 1. 给所有一级科室对象加 disabled: true
const newList = list.map((item) => ({ ...item, disabled: true }));
setDepartmentList(newList as IDepartmentList);
// 2. 展开所有一级科室:把一级科室的 depCode 抽成一个新数组
setExpandedKeys(newList.map((item) => item.depCode));
// 3. 默认选中:第一个一级科室的第一个子科室
const firstChild = (newList[0].children as IDepartmentList)[0];
setSelectedKeys([firstChild.depCode]);
};
useEffect(() => {
if (hoscode) getDepart(hoscode as string);
}, []);
return (
<Tree
treeData={departmentList as any}
// 后端字段和 Tree 组件字段不一致,用 fieldNames 做映射
fieldNames={{ title: 'depName', key: 'depCode', children: 'children' }}
expandedKeys={expandedKeys}
selectedKeys={selectedKeys}
onSelect={(keys) => setSelectedKeys(keys as string[])}
/>
);
}要点解释
treeData直接传后端返回的departmentList会报类型错误(后端字段是depName/depCode,Tree 要title/key),用fieldNames做映射,同时用as any断言绕过 TS 检查。disabled不是类型里原有的属性,要在IDepartmentList的类型定义里补一个可选属性disabled?: boolean,否则item.disabled会报错。- 展开一级科室用
expandedKeys,默认选中用selectedKeys(转写里试过checkedKeys,是不对的),两者都是字符串数组,元素是节点的depCode。 newList[0].children是联合类型,可能为undefined,取第 0 项前要断言as IDepartmentList。useEffect里同样要if (hoscode)判断后再发请求,hoscode需要as string断言。
常见错误
1. 详情页首屏白屏 / 控制台报 Cannot read properties of undefined (reading 'xxx')
- 现象:一进详情页就红屏,或图片位置出现一个空框、裂图。
- 原因:
hospital初始值是undefined,而useEffect要等挂载完成才发请求,所以页面必然先渲染一次空数据;<Image src={...logoData}>在无值时也被渲染出来了。 - 修法:取值一律走可选链
hospital?.hospitalName;图片、科室等依赖异步数据的节点用条件渲染包一层(有值才渲染),而不是只加问号就完事。
2. useParams() 取到的 id / hosCode 报类型错误,或请求带上了 undefined
- 现象:TS 标红提示
string | undefined不能赋给string;网络面板里出现/hospital/detail/undefined。 - 原因:路由参数在类型层面允许不存在,TS 只能推断成联合类型;而
useEffect里没做存在性判断就无条件发请求。 - 修法:断言
as string消除类型报错,同时在useEffect内加if (!hosCode) return;守卫,保证「有参数才请求」。
3. 点击上下线后,按钮文案和列表状态没变
- 现象:接口返回成功,但列表里那条记录还是原来的状态,刷新页面才变。
- 原因:只把新状态提交给了服务端,本地列表数据仍是被修改前的旧快照。
- 修法:
await changeStatus(id, targetStatus)成功后立刻重新调用getHospitalList()拉取列表;注意目标状态要取反原状态,别写成常量。
4. 科室树渲染不出来,或渲染出来但节点没有文字
- 现象:
Tree不报错但一片空白,或节点全是空的、展开后只有箭头。 - 原因:后台字段是
departCode / departName,而Tree默认读key / title,字段名对不上;直接用departmentList还会因类型不一致标红。 - 修法:给
Tree配fieldNames做字段映射(title→departName、key→departCode);类型不匹配时按组件要求的数组类型断言,先让结构对齐再谈渲染。
5. 一级科室「禁用 / 展开 / 默认选中」三件事总有一件不生效
- 现象:禁用加了却还能点;
expandedKeys写了没反应;默认选中死活选不上。 - 原因:给
map出来的item挂disable属性时没在接口类型里声明该可选字段(TS 报错后随手any掩盖);expandedKeys/ 选中项是写死的常量而非受控状态;选中属性误用成checkedKeys(复选框语义),实际应为selectedKeys。 - 修法:接口类型补可选字段;用
map返回新对象并setState;把展开项、选中项都定义成 state,先算出departCode数组再赋值;属性名以组件文档/实测为准,逐个验证。
6. 左侧栏高度写死,换个屏幕就出现双滚动条或留白
- 现象:小屏时内容被截断,大屏时底部一大片空白。
- 原因:
height: 500px这类硬编码没有跟随可视区变化。 - 修法:按可视区高度减去顶部导航 + 面包屑 + 分页等固定高度动态计算,配合
overflow-y: auto只在容器内滚动;两列之间用Row的gutter留间隙,别用 margin 硬怼。
练习
首屏健壮性改造:把医院详情页所有插值渲染逐个检查一遍,列出哪些必须加可选链、哪些必须加条件渲染,并说明两者的区别(各举一个本页的例子)。完成后在 Network 面板限速到 Slow 3G,确认首屏不红屏、不裂图。
状态切换的幂等性:给上下线按钮加
loading防重复点击,并在请求失败时给出提示且不改动本地数据。思考:如果两次快速点击都发出去了,最终服务端状态和你以为的状态可能不一致,怎么避免?树组件属性验证:新建一个最小
Treedemo,分别传入checkedKeys、selectedKeys、expandedKeys,观察三者视觉表现与交互差异,写下一句话结论区分它们。再把fieldNames去掉,确认渲染确实变空,验证你对错误 4 的判断。字段映射的通用化:把「一级科室加
disable」和「提取一级科室departCode生成展开数组」两段map逻辑抽成两个纯函数,输入输出都是数组,不依赖组件状态。抽完后回答:为什么这两步必须在setState之前做,而不能在渲染函数里做?布局自适应验收:把左侧栏高度改成动态计算后,分别在 1366×768、1920×1080 以及浏览器缩放 80%/125% 下检查:是否只有左侧栏内部滚动、是否出现页面级双滚动条、分页是否始终可见。记录不通过的场景并修正。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 医院详情-数据渲染 | 11:54 |
| 2 | 医院上下线切换 | 06:17 |
| 3 | 排班路由组件创建及路由配置 | 02:36 |
| 4 | 医院排班静态页面布局完成 | 14:58 |
| 5 | 科室列表渲染完毕 | 24:15 |
小结
- 详情页这类「进页面就取数」的组件,首次渲染必然拿不到数据,可选链是防崩、条件渲染是防丑,两者缺一不可,不能只靠一个问号糊过去。
- 路由参数天然是「可能不存在」的,类型断言解决编译器,存在性判断解决运行时,只做前者等于把风险留到线上。
- 凡是修改服务端数据的操作,改完必须重新同步列表,否则用户看到的是自己的错觉;这类操作还应加防重复提交。
- 第三方组件(Tree、Table、Pagination)接入的通用套路是:先对齐数据结构(fieldNames / columns),再谈受控属性(expandedKeys / selectedKeys),最后才是样式,顺序颠倒会反复返工。
- 受控类属性必须是 state,写成常量就等于写死,交互必然失灵;属性名以文档和实测为准,
checkedKeys与selectedKeys是两回事。 - 后端字段名永远不等于 UI 组件期望的字段名,映射层要么放在组件配置里,要么放在数据转换里,不要指望改后端。
- 布局高度、间距这类「看起来能跑」的硬编码,是响应式问题的最大来源;能算就别写死,能用栅格
gutter就别用 margin。 - 整个模块从详情到排班,走的都是同一套流程:定类型 → 封装 API → 选时机调用 → 声明并设置状态 → 插值渲染,把这套流程练成肌肉记忆,比记住某个组件的 API 更有价值。