主题
查询医院与查看医院详情(Day15 2/3)
本页对应 React + TypeScript + 医通项目,共 6 段视频,约 51 分钟。
学习目标
- 能使用 React 状态与 Ant Design Table 的
dataSource、columns、rowKey完成医生列表渲染,并为序号、职称、号源时间、总号源、挂号费、技能列配置正确的dataIndex、宽度与对齐方式,且能通过编程式导航实现返回上一页。 - 能依据“状态数据是否按预期变化”和 Network 面板中的状态码、Request URL、请求参数定位 React 页面渲染或接口调用问题,并能使用 React DevTools 的 Components 面板检查路由组件状态变化。
- 能创建数据字典路由组件,并在与医院管理同级的路由下配置
children、meta(标题、图标)及懒加载,使“数据管理-数据字典”菜单可正常显示和跳转。 - 能调用已封装的数据字典接口获取数据,定义并更新状态,使用 Table 渲染名称、编码、值、创建时间列,并关闭分页。
- 能通过 Table
expandable配置自定义展开图标,在onExpand中根据当前行record.id请求子节点并写入record.children后展开,且能根据children长度与hasChildren判断避免重复请求并处理无子节点占位。
知识点
医生列表渲染:先把状态和表格列接上
为什么需要:医生列表要随日期、医院等条件变化,不能写死;React 的渲染依赖状态数据。怎么想:先把接口返回的医生数据放进状态,再用 Table 的 dataSource 渲染;每一列通过 columns 的 render 或 dataIndex 取字段。怎么做:定义医生列表状态,绑定到表格,再补列配置。
tsx
const [doctorList, setDoctorList] = useState([]);
<Table
dataSource={doctorList}
rowKey="id"
columns={[
{
title: '序号',
render: (_, __, index) => index + 1,
},
{ title: '职称', dataIndex: 'title' },
{ title: '号源时间', dataIndex: 'workDate' },
{ title: '总号源', dataIndex: 'available' },
{ title: '挂号费', dataIndex: 'fee' },
{ title: '科室', dataIndex: 'skill' },
]}
/>转写里特别强调:序号不要用“分页页码 × pageSize”,因为这里的表格分页和业务分页不是一回事,直接 index + 1 即可。日期显示不全时给列加宽度,序号列可配 align 居中。易错点:遍历渲染必须加 rowKey,优先用数据里的 id;否则控制台会提示缺少 key。返回按钮用编程式导航:onClick={() => navigate(-1)},表示回到上一历史记录。
React 调错思路:先看状态,再看 Network
为什么需要:页面不按预期变化时,盲目改代码效率低。怎么想:React 页面渲染高度依赖状态;若涉及请求,还要确认请求是否发出、参数和响应是否正确。怎么做:先在 React DevTools 的 Components 里找对应路由组件,观察操作前后状态有没有按预期变化;若状态变了但数据不对,就到 Network 看请求。
调试时重点看:请求 URL、请求参数、状态码、响应结果。比如切换日期后只有部分列变化,先确认是否重新发请求;在 Network 清空后每点一次看是否新增一条 200 请求,再点进去看 Headers 里的参数。易错点:请求失败大概率先查参数;状态没变则查设置状态的位置是否写错,或选中属性是否配对。这个思路在 Vue 里也相通:先看状态,再看请求。
数据字典路由:与医院管理同级配置
为什么需要:数据字典是独立功能,不应挂在医院管理的子路由下。怎么想:按“创建路由组件 → 配置路由 → 静态布局 → 取数渲染”的顺序做。怎么做:在 pages/CMN/DICT 下创建组件,路由与 hospital 同级,父级只提供菜单和 children,子级才配置 element。菜单需要 meta.title 和图标;组件用 lazy 引入。
易错点:父级“数据管理”通常没有实际页面,不要误配 element;真正展示页面的“数据字典”才配 element。页面先用 Card + Table 搭静态结构,列可先写“名称、编码、值、创建时间”,表格不需要分页时设 pagination={false}。
获取字典数据并渲染表格
为什么需要:字典表格的数据来自按父级 ID 查询的接口,省市区功能已用过类似接口。怎么想:传特殊父级 ID(如 1)可拿到医院等级、证件类型、学历、民族等字典分类;把返回数组存入状态,再绑定表格。怎么做:组件挂载后调用接口,打印响应确认结构,再 setDictList(res);列上配置 dataIndex,如名称取 name、编码取 dicCode,创建时间按接口字段取。
易错点:先打印再渲染,确认每列字段名;某列值为空不一定是渲染错,可能是后端该条数据本身为空。rowKey 仍要用记录里的 id。
展开图标与展开节点处理
为什么需要:字典数据有父子层级,需要点击箭头展开子节点。怎么想:Table 通过 expandable.expandedIcon 自定义图标,通过 expanded 判断当前行是否展开,通过 onExpand(record) 触发展开;record 是当前行对象。怎么做:未展开显示向右图标,展开显示向下图标;点击时用 record.id 请求子节点,把结果挂到 record.children,再调用 onExpand(record)。
tsx
expandedIcon={(expanded, record) => {
if (!record.hasChildren) {
return <span style={{ display: 'inline-block', width: 15 }} />;
}
return expanded ? <DownOutlined /> : <RightOutlined />;
}}
onExpand={(expanded, record) => {
if (expanded && !record.children?.length) {
const children = await getDictList(record.id);
record.children = children;
}
onExpand(record);
}}易错点:重复点击会重复请求,可先判断 record.children?.length;没有子节点时不应显示箭头,可用 record.hasChildren 判断并渲染占位元素,避免行内容错位。这样“用 ID 取 children,挂到 record.children,再展开”的链路就完整了。
医生列表渲染:先定义状态,再交给 Table
为什么需要:表格不是“直接把接口数据打印出来”,而是由状态驱动渲染;没有状态,数据变化时页面不会更新。
怎么想:先定义 doctorList 状态,空数组作为初始值;再把状态接到 dataSource,最后补 columns 决定每列显示什么。
怎么做:核心结构如下,字段名以接口返回为准,转写中明确用到 title、workDate、available、skill 等。
tsx
const [doctorList, setDoctorList] = useState<IDoctorList[]>([]);
<Table
dataSource={doctorList}
rowKey="id"
columns={columns}
/>序号列不要拿分页参数乘 pageSize,因为这里的表格分页不是接口分页;直接 index + 1。职称用 title,号源时间用 workDate,剩余号源看 available,技能看 skill。
易错点:rowKey 没配会报列表需要 key;列宽不够会显示不全,可给号源时间列设约 120,序号列可配宽度和居中。
返回按钮:编程式导航回上一页
为什么需要:列表页的“返回”不一定写死跳到某个路由,回退历史记录更稳。
怎么想:用 useNavigate 拿到导航函数,点击时传 -1。
怎么做:
tsx
const navigate = useNavigate();
// 返回按钮
onClick={() => navigate(-1)}易错点:不要误写成普通跳转路径;-1 表示回退一条历史记录。
React 调错:先看状态,再看 Network
为什么需要:React 页面高度依赖状态数据,功能不对通常要么状态没按预期变,要么请求没成功。
怎么想:先问“我操作后状态变了吗”,再问“请求发了吗、参数对吗、状态码是 200 吗”。
怎么做:用 React DevTools 的 Components 找对应路由组件,观察状态变化;用浏览器 Network 清空后操作,检查是否新增请求,点开看 Headers 里的 Request URL、参数和状态码。
易错点:只盯页面结果,不看状态和请求;请求失败大概率先查参数,状态码不是 200 时先别继续猜渲染逻辑。这个思路在 Vue 里也相通。
数据字典路由:与医院管理同级配置
为什么需要:数据字典属于“数据管理”下的独立模块,不能挂在医院管理的子路由里。
怎么想:按“创建路由组件 → 配置路由 → 布静态页面”三步走。目录可建 pages/CMN/DICT,路由与 hospital 同级;父级只做菜单分组,子级才展示组件。
怎么做:父级配 meta.title: '数据管理' 和图标,children 里配 CMN/DICT,meta.title: '数据字典',element 用 lazy 引入 Dict。页面用 Card + Table,四列:名称、编码、值、创建时间,先 pagination={false}。
易错点:父级菜单没有对应组件时不要硬写 element;子路由路径和 lazy 导入路径要一致。
获取字典数据并渲染:复用按 parentId 查询接口
为什么需要:数据字典要展示省市区、医院等级、证件类型、学历、民族等分类数据。
怎么想:之前省市区已经封装过按 parentId 查询的接口,这里传特殊参数 1 获取字典分类列表。
怎么做:定义异步函数调用接口,useEffect 挂载后执行;请求回来 setDictList(res),Table 的 dataSource 接状态,rowKey 用 id,列上配 dataIndex。
易错点:只请求不设置状态,表格不会更新;列不配 dataIndex 会出现“有行没值”。
Table 展开节点:图标、请求子节点与缓存
为什么需要:字典数据有层级,点击箭头要展开子节点。
怎么想:expandable.expandedIcon 根据是否展开显示向下/向右图标;onExpand(record) 里用当前行 record.id 请求子节点,把结果挂到 record.children,再交给展开函数。
怎么做:先判断 record.children 不存在或长度为 0 才请求,避免重复发请求;record.hasChildren 为 false 时不显示箭头,用占位元素撑住宽度。
tsx
if (!record.children || record.children.length === 0) {
const children = await getDictList(record.id);
record.children = children;
}
onExpand(record);易错点:每次展开都重复请求;没有子节点仍显示箭头;占位元素要用 display: inline-block; width: 15px 之类撑住位置,否则层级会错位。调试时先 console.log(expanded、onExpand、record),确认 expanded 是布尔值、record 是当前行对象,再改逻辑。
医生列表渲染与返回
为什么需要:医生列表要由接口数据驱动,不能只打印数据;表格列也要明确每列取哪个字段。怎么想:先定义 doctorList 状态,再把状态交给 Table 的 dataSource,最后配置 columns。怎么做:序号列用 index + 1,职称取 title,号源时间取 workDate,剩余号源取 available,技能取 skill;号源时间列可设 width: 120,序号可设 align: 'center'。返回按钮用编程式导航 navigate(-1)。
tsx
const [doctorList, setDoctorList] = useState<any[]>([])
const navigate = useNavigate()
const columns = [
{ title: '序号', render: (_: any, __: any, index: number) => index + 1, width: 80, align: 'center' },
{ title: '职称', dataIndex: 'title' },
{ title: '号源时间', dataIndex: 'workDate', width: 120 },
{ title: '剩余号源', dataIndex: 'available' },
{ title: '技能', dataIndex: 'skill' },
]
<Button onClick={() => navigate(-1)}>返回</Button>易错点:序号不要拿表格分页的 page * pageSize 计算,因为这里的表格分页不是接口分页;遍历渲染要加 rowKey,转写稿中用 row.id,否则会出现 key 警告。
React 调错:先看状态再看网络
为什么需要:React 渲染高度依赖状态数据,功能不对时不能盲改。怎么想:先判断“状态有没有按预期变化”,再判断“Ajax 请求有没有成功、参数对不对”。怎么做:在 React DevTools 的 Components 里找对应路由组件,观察状态变化;在 Network 里清空后点击按钮,看是否新增请求,重点看 Request URL、状态码、请求参数。转写稿中点击日期后只有一列变化,是因为后端返回的其他列数据相同,不是前端没更新。易错点:状态没变就查事件里 setState 写错没;状态变了但页面不对,就查请求参数和返回结构。
数据字典路由与静态表格
为什么需要:数据管理是独立模块,不能挂在 hospital 子路由下。怎么想:先创建路由组件,再配同级路由,最后铺静态页面。怎么做:创建 pages/CMN/DICT/DICT.tsx,路由与 hospital 同级,path: 'CMN' 下配 children,path: 'DICT',meta 分别为“数据管理”“数据字典”;数据管理本身没有 element,只有 children。页面用 Card + Table,columns 为名称、编码、值、创建时间,先 pagination={false}。
易错点:把 DICT 配到 hospital 子路由下会层级错误;数据管理没有 element,不要硬塞路由组件。
数据字典数据获取与表格渲染
为什么需要:静态表格只有表头,必须请求字典数据。怎么想:复用已封装的字典接口,传特殊参数 1 获取医院等级、证件类型、学历、民族等数据。怎么做:定义 getDictList,内部 await 调用已封装接口并传 1;useEffect 中调用,拿到结果后 setDictList(res);Table 的 dataSource={dictList},rowKey="id",各列按接口字段配置 dataIndex。易错点:接口字段名要和 dataIndex 对上;空值要允许显示为空,不要误判为渲染失败。
展开图标配置
为什么需要:字典数据有父子层级,需要点击箭头展开子节点。怎么想:用 Table 的 expandable.expandIcon 自定义图标,函数参数有 expanded、onExpand、record。怎么做:先打印三个参数确认含义:expanded 是布尔展开标识,onExpand 是展开函数,record 是当前行对象。未展开显示向右图标,展开显示向下图标;无子节点时返回占位元素。
tsx
expandIcon={({ expanded, onExpand, record }) => {
if (!record.hasChildren) {
return <span style={{ display: 'inline-block', width: 15 }} />
}
return expanded
? <DownOutlined onClick={() => onExpand(record)} />
: <RightOutlined onClick={() => getChildren(record, onExpand)} />
}}易错点:无子节点仍显示箭头;占位用普通 div 可能高度为 0,转写稿改为 display: inline-block; width: 15px 才占住位置。
展开节点:取子集、缓存与占位
为什么需要:点击省、医院等级等节点时,要用当前 record.id 请求子节点,并挂到 record.children 上再展开。怎么想:先打印 record.id,发现省是 86、医院等级是 10000、证件类型是 20000、学历是 30000;请求返回后执行 record.children = children,再 onExpand(record)。怎么做:加缓存判断,只有 !record.children || record.children.length === 0 时才请求,避免重复发请求;用 Network 验证收起再点不会重复请求。易错点:不判断会反复请求;hasChildren 为 false 的节点不应有箭头,只做占位。
课堂画面






代码示例
示例一:医生列表渲染(Table + columns + 返回按钮)
tsx
import { useEffect, useState } from 'react'
import { Table, Button } from 'antd'
import { useNavigate } from 'react-router-dom'
import type { ColumnsType } from 'antd/es/table'
interface IDoctor {
id: string
title: string
workDate: string
available: number
fee: number
skill: string
}
export default function DoctorList() {
const navigate = useNavigate()
// 先定状态,再渲染
const [doctorList, setDoctorList] = useState<IDoctor[]>([])
useEffect(() => {
// 已有逻辑:请求医生列表后 setDoctorList(res.data)
}, [])
const columns: ColumnsType<IDoctor> = [
{
title: '序号',
width: 80,
align: 'center',
// 第三个参数才是 index
render: (_: any, __: any, index: number) => index + 1,
},
{ title: '职称', dataIndex: 'title' },
{ title: '号源时间', dataIndex: 'workDate', width: 120 },
{ title: '总号源', dataIndex: 'available' },
{ title: '挂号费', dataIndex: 'fee' },
{ title: '擅长技能', dataIndex: 'skill' },
]
return (
<>
<Button onClick={() => navigate(-1)}>返回</Button>
<Table rowKey="id" columns={columns} dataSource={doctorList} />
</>
)
}要点:
- 渲染表格前必须先定义
doctorList状态,dataSource绑定的就是它,没有状态数据表格永远是空的。 - 序号用
render的第三个参数index + 1,不能用分页的current * pageSize,那个分页不是表格分页。 rowKey="id"必须加,否则控制台报“列表需要 key”的警告(接口数据里是有id的)。- 返回按钮用
navigate(-1)做编程式导航,等价于浏览器后退,回到上一条历史记录。 - 列宽不够(比如号源时间)用
width: 120撑开,序号列可再加align: 'center'居中。
示例二:数据字典路由配置 + 表格渲染
tsx
// ------- 路由配置(片段):数据管理与医院管理同级 -------
import { lazy } from 'react'
import { ListOutlined } from '@ant-design/icons'
const Dict = lazy(() => import('@/pages/cmn/dict/Dict'))
export const routes = [
// ... 医院管理 /hospital 等已有同级路由
{
path: '/cmn',
// 父级只有菜单信息,没有 element
meta: { title: '数据管理', icon: <ListOutlined /> },
children: [
{
path: '/cmn/dict',
meta: { title: '数据字典' },
element: <Dict />,
},
],
},
]
// ------- pages/cmn/dict/Dict.tsx -------
import { useEffect, useState } from 'react'
import { Card, Table } from 'antd'
import type { ColumnsType } from 'antd/es/table'
// 省市区时已封装的接口函数,这里直接复用
import { getDictList } from '@/api/hospital'
import type { IDictList } from '@/api/hospital'
export default function Dict() {
const [dictList, setDictList] = useState<IDictList[]>([])
const getDictListData = async () => {
// 传 1 拿的是医院类型/等级/证件类型/学历/民族那一批字典
const res = await getDictList(1)
setDictList(res.data)
}
useEffect(() => {
getDictListData()
}, [])
const columns: ColumnsType<IDictList> = [
{ title: '名称', dataIndex: 'name' },
{ title: '编码', dataIndex: 'dicCode' },
{ title: '值', dataIndex: 'dicValue' },
{ title: '创建时间', dataIndex: 'createTime' },
]
return (
<Card>
<Table
rowKey="id"
columns={columns}
dataSource={dictList}
pagination={false}
/>
</Card>
)
}要点:
- 数据管理是跟医院管理同级的路由,不是
/hospital的子路由;父级只配meta(标题 + 图标),真正的组件放在children里的/cmn/dict。 - 组件用
lazy懒加载,别忘import图标,否则菜单图标处报错。 - 接口函数
getDictList在省市区那节已经封装过,这里只是换个参数调用,不要重复写一套类型。 - 表格不想要分页就配
pagination={false}。 - 列只写
title不写dataIndex时单元格是空的,四个列都要对应上接口字段(字段名以接口返回为准)。
示例三:表格展开图标 + 子节点按需加载
tsx
import { Card, Table } from 'antd'
import { DownOutlined, RightOutlined } from '@ant-design/icons'
import { getDictList } from '@/api/hospital'
// …… columns、dictList 与示例二相同
<Table
rowKey="id"
columns={columns}
dataSource={dictList}
pagination={false}
expandable={{
expandIcon: ({ expanded, onExpand, record }) => {
// 没有子节点:不显示箭头,但要占位,否则下一层会被挤到左边
if (!record.hasChildren) {
return <span style={{ display: 'inline-block', width: 15 }} />
}
// 已展开 -> 向下箭头
if (expanded) {
return <DownOutlined onClick={e => onExpand(record, e)} />
}
// 未展开 -> 向右箭头,点击时先取子节点再展开
return (
<RightOutlined
onClick={async e => {
// 已有 children 就不重复请求
if (!record.children || record.children.length === 0) {
const res = await getDictList(record.id)
;(record as any).children = res.data
}
onExpand(record, e)
}}
/>
)
},
}}
/>要点:
expandIcon的三个参数含义:expanded是当前行是否展开的布尔值,onExpand是展开函数,record是当前行对象(record.id就是发请求要用的父级 ID,如 86 / 10000 / 20000)。- 箭头方向用
expanded判断,展开给向下、未展开给向右,不要写成没有return的简写三元,否则函数返回undefined。 - 子节点是“先请求、挂到
record.children、再onExpand(record, e)”,antd 会自动把有children的行展开。 if (!record.children || record.children.length === 0)这个判断是防重复请求:第一次点发一次,收起再点不会重复发(可在 Network 里验证)。record.hasChildren === false的节点(如天津)不显示箭头,用display: 'inline-block'; width: 15的span占位,直接给div不设inline-block会把后面的内容挤下去。- TS 下给
record.children赋值可能报类型错误,用(record as any)断言即可。
常见错误
- 现象:医生列表表格能出现但列内容空白。原因:只设置了
dataSource,columns未正确配置dataIndex/render,或字段名与接口不一致。修法:逐列对照接口字段配置dataIndex,序号列用render(index)返回index + 1,日期列补width,挂号费/剩余号源/职称/技能逐项核对。 - 现象:控制台出现列表
key警告。原因:Table 遍历数据时未指定rowKey。修法:给 Table 增加rowKey="id"或rowKey={record => record.id}。 - 现象:点击“返回”无反应或跳转错误。原因:未创建编程式导航函数,或点击事件未绑定
navigate(-1)。修法:在组件内const navigate = useNavigate(),返回按钮绑定onClick={() => navigate(-1)}。 - 现象:功能不符合预期时直接改代码,越改越乱。原因:未先判断是状态没变还是请求失败。修法:先用 React DevTools 的 Components 看相关 state 是否按预期变化;若状态正常再看 Network 的 Request URL、状态码、请求参数,二分定位问题。
- 现象:数据管理菜单不出现或点开无页面。原因:路由层级配错,把
CMN/DICT配到hospital子路由下,或漏配meta.title、element、懒加载导入路径。修法:将CMN与hospital配为同级,CMN下配children,DICT配meta.title和element,并检查lazy导入路径。 - 现象:数据字典表格展开异常、重复发请求或无子节点仍显示箭头。原因:未用当前行
record.id请求子节点并回填record.children,未判断已有children,未用hasChildren控制图标。修法:onExpand中用record.id请求子节点并挂到record.children后展开;已有children且长度大于 0 时不再请求;hasChildren === false时返回占位元素而非展开图标。
练习
- 完成医生列表表格:补齐
columns的字段映射、序号render、日期列宽度、rowKey,并验证切换日期后列表数据变化。 - 用 Components 与 Network 排查一次“点击切换后页面未更新”的问题,记录:哪个 state 是否变化、请求是否发出、状态码与参数是否正确。
- 按“创建路由组件→配置路由→静态布局”的步骤新增“数据管理/数据字典”页面,要求菜单可显示、可跳转、懒加载正常。
- 实现数据字典表格展开:点击省/医院等级/证件类型等行按
record.id请求子节点并展开;验证重复展开不重复请求;无子节点不显示箭头且保留占位。 - 给展开逻辑增加一次验收:在 Network 中确认首次展开发请求、收起再展开不发重复请求、无子节点行不请求。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 医生列表渲染完成 | 06:04 |
| 2 | react调错思路分析 | 07:56 |
| 3 | 数据字典路由组件创建-路由配置 | 06:52 |
| 4 | 获取数据字典数据并渲染table表格 | 05:32 |
| 5 | table-展开图标配置 | 06:19 |
| 6 | table展开节点处理完毕 | 19:12 |
小结
- 列表页要先保证状态数据正确,再让
columns成为接口字段与页面展示之间的明确契约。 - 调错顺序应是“先状态、后请求”:Components 定位状态问题,Network 定位接口问题。
- 路由功能按层级、菜单元信息、懒加载组件三处同时验收,任何一处漏配都会表现为“菜单不见”或“页面打不开”。
- 表格展开的核心不是图标本身,而是当前行数据、子节点数据与展开回调之间的协同。
- 子节点请求必须以当前行 ID 为依据,回填到行数据后再触发展开,才能形成递归展开效果。
- 重复请求用已有
children判断,无子节点用hasChildren控制图标与占位,最终以 Network 请求次数和交互效果验收。