主题
省市区三级联动与查询医院(Day15 1/3)
本页对应 React + TypeScript + 医通项目,共 6 段视频,约 50 分钟。
学习目标
- 能够为科室树组件绑定选中事件,解析事件参数中的科室
code与name,更新状态并验证科室选中态与面包屑名称同步切换。 - 能够为排班日期分页接口定义日期项、日期列表、响应数据等 TypeScript 类型,并封装接收
current、limit、houseCode、depCode参数的 API 函数。 - 能够在组件中声明分页、排班日期列表、医院名称、选中日期等状态,并通过
useEffect监听科室code与分页参数发送排班日期请求。 - 能够解构排班日期接口响应,更新总数、排班日期列表和医院名称,完成日期列表与分页器渲染,并验证切换页码后数据更新。
- 能够通过
workDate状态控制排班日期的选中样式,点击日期后更新选中状态并同步页面日期显示。 - 能够定义排班医生项与列表类型,封装获取排班医生列表 API,并在
workDate有值时调用接口、打印医生列表以验证请求参数与返回数据。
知识点
说明:本节转写未附图片,且实际内容不是省市区三级联动,而是“科室切换、排班日期分页、医生列表”的联动;以下严格按转写整理,不虚构省市区代码。
点击科室切换选中状态
为什么需要:首页只默认选中一个科室,用户点击其他科室后,列表选中态和面包屑都要跟着变。怎么想:点击是事件,先给树形控件绑定节点点击/选中事件,打印参数确认结构,再把最新的 deptCode、deptName 重置到状态里。怎么做:转写中打印后确认,第一个参数是科室 deptCode,第二个参数对象里 node.deptCode、node.deptName 可用,于是重置这两个状态。易错点:只改 deptCode 不改 deptName,面包屑不会实时更新;参数名不要照抄“A/B”,要改成有意义的名字。
ts
// 按转写参数含义整理
const onSelect = (deptCode: string, info: any) => {
setDeptCode(info.node.deptCode)
setDeptName(info.node.deptName)
}排班日期接口类型与 API 封装
为什么需要:要根据医院 houseCode 和科室 deptCode 获取该科室未来的排班日期分页数据。怎么想:接口有 current、limit、houseCode、deptCode 四个必填参数,先定义“日期项类型 → 日期列表类型 → 响应类型”,再封装 API。怎么做:按转写定义 IBookingScheduleItem、IBookingScheduleList、IScheduleResponse,响应里用到 total、bookingScheduleList、baseMap.houseName。易错点:字段含义不清要查接口文档或问后端,不能长期靠猜;houseCode、deptCode 可能被推断为联合类型,调用前要断言为 string。
ts
export interface IBookingScheduleItem {
workDate: string
dayOfWeek: string
availableNumber: number
reserveNumber: number
}
export type IBookingScheduleList = IBookingScheduleItem[]
export interface IScheduleResponse {
total: number
bookingScheduleList: IBookingScheduleList
baseMap: { houseName: string }
}
export const getScheduleList = (
current: number, pageSize: number,
houseCode: string, deptCode: string
) => request.get<IScheduleResponse>(url, {
params: { current, pageSize, houseCode, deptCode }
})请求时机:监听 deptCode 与分页
为什么需要:组件挂载时 deptCode 还没有值,直接请求会发出无效请求。怎么想:排班日期请求依赖 deptCode,分页变化也要重新请求,所以监听 deptCode、current、pageSize。怎么做:在 useEffect 开头加 if (!deptCode) return,再调用 getScheduleList。易错点:转写中调试时在 Network 看到 deptCode=undefined 的请求,就是没加守卫;它不能和科室列表请求共用一个 useEffect,因为调用时机不同。
ts
useEffect(() => {
if (!deptCode) return
getSchedule()
}, [deptCode, current, pageSize])渲染分页列表与日期选中
为什么需要:日期列表要分页展示,并且默认选中第一项;点击日期后选中态要切换。怎么想:转写先尝试用索引判断选中,但后续点击时索引不能表达“当前选中日期”,所以改用 workDate 状态比较。怎么做:请求回来设置 total、bookingScheduleList、houseName,并把列表第 0 项的 workDate 设为默认;渲染时用 workDate === item.workDate 判断颜色,点击时 setWorkDate(item.workDate)。易错点:key 用不会重复的 workDate;分页 onChange 要同时更新 current 和 pageSize。
tsx
{bookingScheduleList.map(item => (
<div key={item.workDate}
color={workDate === item.workDate ? 'green' : 'gray'}
onClick={() => setWorkDate(item.workDate)}>
{item.workDate} {item.dayOfWeek} 可约{item.availableNumber}
</div>
))}
<Pagination current={current} total={total} pageSize={pageSize}
onChange={(page, size) => { setCurrent(page); setPageSize(size) }} />医生列表 API 与调用时机
为什么需要:选中排班日期后,要展示该医院、科室、日期下的医生列表。怎么想:三个参数 houseCode、deptCode、workDate 中,workDate 最后获得;它一变医生列表就要变,所以只监听 workDate。怎么做:定义 IDoctorItem、IDoctorList,封装 getDoctorList,在 useEffect 里先判断 if (!workDate) return 再请求。易错点:组件挂载后 useEffect 也会执行一次,若不判断会发出 workDate=undefined 的请求;切换科室后排班第一项会重置 workDate,因此不必再额外监听 deptCode。
ts
useEffect(() => {
if (!workDate) return
getDoctorList()
}, [workDate])点击树节点切换科室
为什么需要:首页虽然已经渲染科室列表并默认选中第一个科室,但用户还要能点其他科室,侧边树和面包屑要同步变化。
怎么想:用户点击是事件,应先找树形控件的选中事件;不要猜参数,先把回调参数打印出来看结构。转写稿里先打印 selectedKeys 和事件对象 e,发现 selectedKeys 是科室 ID,e.node.data 里还有 code、name。
怎么做:绑定 select 事件,调试后重置 deptCode 和 deptName。关键代码示意:
js
const handleSelect = (selectedKeys, e) => {
// 调试:先打印,确认参数结构
console.log(selectedKeys, e)
const { code, name } = e.node.data
setDeptCode(code)
setDeptName(name)
}易错点:不要直接把事件对象当科室 ID;deptName 也要重置,因为面包屑要实时显示;字段名以实际 node.data 为准。
排班日期分页列表:类型与 API 封装
为什么需要:切换科室后,要根据 houseCode + deptCode 获取该科室未来的排班日期分页数据。
怎么想:接口有四个必填参数:current、pageSize、houseCode、deptCode。先调一次接口看响应,再按“列表项类型 → 列表类型 → 响应类型”定义。
怎么做:定义 IBookingScheduleItem、IBookingScheduleList、IScheduleResponse,再封装 getScheduleList。字段中 workday、dayOfWeek、availableNumber、reserveNumber 等按响应保留;不确定含义的要问后端。
易错点:houseCode、deptCode 要做 string 断言;分页参数是组件状态;不要凭英文单词硬猜业务含义。
组件中请求排班日期:依赖 deptCode
为什么需要:一进页面 deptCode 还没有值,不能立刻请求排班日期。
怎么想:deptCode 是科室列表请求回来后才设置的,所以排班日期请求要监听 deptCode;页码 current、每页条数 pageSize 变化也要重新请求。
怎么做:
js
useEffect(() => {
if (!deptCode) return
getScheduleList(current, pageSize, houseCode, deptCode)
}, [deptCode, current, pageSize])调试时在 Network 里看到过 deptCode=undefined 的请求,说明挂载时无条件发请求是错的,加 if (!deptCode) return 后才正确。
易错点:不要和科室列表请求共用一个 useEffect,两者时机不同;依赖别漏 current、pageSize;deptCode 为空不发请求。
渲染日期列表与选中切换
为什么需要:响应里有 total、bookingScheduleList、baseMap.houseName,页面要显示医院名、日期标签和分页。
怎么想:默认选中排班日期列表第一项的 workday,用 workDate 状态保存;点击某个日期时更新 workDate,颜色判断也用 workDate 比较。
怎么做:请求后解构并设置状态;渲染 Tag,key 用 workday,颜色用 workDate === item.workday 判断;分页 onChange 更新 current、pageSize。
易错点:key 不要用索引;选中判断不要只用索引,点击后索引不变会出错;医院名来自排班日期响应,某些科室无数据时显示不出来不是渲染代码问题。
排班医生 API 类型与封装
为什么需要:选中日期后,要拿 houseCode、deptCode、workDate 获取医生排班列表。
怎么想:先调接口看医生项结构,再定义 IDoctorItem 和 IDoctorList,最后封装 getDoctorList。
怎么做:
js
const getDoctorList = (houseCode, deptCode, workDate) => {
return request.get(/* 接口地址 */, {
params: { houseCode, deptCode, workDate }
})
}易错点:参数顺序别错;字段类型按响应定义;不用的字段可以删,但不要改错业务字段。
调用医生列表并处理 workDate 初始值
为什么需要:workDate 变化后医生列表要跟着变。
怎么想:三个参数里 workDate 最后获取;deptCode 变化会导致日期重新请求,所以监听 workDate 即可。但组件挂载时 workDate 初始为 undefined,不能直接发请求。
怎么做:useEffect 依赖 [workDate],先判断有值再请求。调试时 Network 里出现 workDate=undefined 的请求,加判断后才会在日期有值时正常返回数据。
易错点:不能只监听 deptCode;workDate 初始为空仍发请求是常见错误;异步请求与挂载同步执行要区分清楚。
课堂画面






代码示例
示例 1:科室树点击切换选中科室(onSelect 事件)
tsx
import { useState } from 'react';
import { Tree } from 'antd';
interface DeptNode {
deptCode: string;
deptName: string;
children?: DeptNode[];
}
export default function DeptTree({ deptList }: { deptList: DeptNode[] }) {
// 当前选中的科室 code / 名称,名称用于面包屑显示
const [deptCode, setDeptCode] = useState<string>('');
const [deptName, setDeptName] = useState<string>('');
// 第一个参数是 selectedKeys,第二个参数是事件对象(里面带 node 节点信息)
const onSelect = (selectedKeys: React.Key[], e: any) => {
// 想要的信息在第二个参数的 node 节点里
setDeptCode(e.node.deptCode);
setDeptName(e.node.deptName);
};
return (
<>
<Tree
treeData={deptList as any}
fieldNames={{ title: 'deptName', key: 'deptCode' }}
selectedKeys={[deptCode]} // 受控:状态一变,树上的高亮就跟着变
onSelect={onSelect}
/>
<div>当前科室:{deptName}</div>
</>
);
}要点与易错点
- 树控件的选中是“受控”的:
selectedKeys={[deptCode]},所以只要setDeptCode一改,选中状态就跟着切换,不需要手动操作 DOM。 onSelect回调有两个形参:第一个是selectedKeys(数组),第二个是事件对象,真正的科室信息在e.node里(deptCode/deptName),不要直接从第一个参数取deptName。- 两个状态必须一起重置:
deptCode决定高亮,deptName用于面包屑等位置实时显示,漏掉deptName会出现“树切换了但面包屑没变”。 - 写事件时容易多打一个字符导致 TS 报错(转写稿里
on select多了个空格/符号),先检查 JSX 属性拼写再看类型错误。
示例 2:排班日期分页列表(类型定义 + API 封装 + 组件内调用)
tsx
import { useState, useEffect } from 'react';
import { Pagination } from 'antd';
import request from '@/utils/request'; // 项目里已有的请求封装
/* ---------- 类型定义:先定义“每一项”,再定义“列表”,最后定义“响应” ---------- */
export interface IBookingScheduleItem {
workDate: string; // 排班日期
dayOfWeek: string; // 星期几
reservedNumber: number; // 已预约人数
totalNumber: number; // 总预约数
availableNumber: number; // 剩余可预约数
}
export type IBookingScheduleList = IBookingScheduleItem[];
export interface IScheduleResponse {
total: number;
bookingScheduleList: IBookingScheduleList;
baseMap: { hospitalName: string }; // 医院信息在这个字段里
}
/* ---------- API 封装 ---------- */
export const getScheduleList = (
current: number,
pageSize: number,
houseCode: string,
deptCode: string
) =>
request.get<IScheduleResponse>(
`/schedule/page?current=${current}&pageSize=${pageSize}&houseCode=${houseCode}&deptCode=${deptCode}`
);
/* ---------- 组件 ---------- */
export default function ScheduleList({
houseCode,
deptCode,
}: {
houseCode: string;
deptCode: string;
}) {
const [current, setCurrent] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(3);
const [total, setTotal] = useState<number>(1);
const [bookingScheduleList, setBookingScheduleList] = useState<IBookingScheduleList>([]);
const [houseName, setHouseName] = useState<string>('');
const [workDate, setWorkDate] = useState<string>('');
const getSchedule = async () => {
const res = await getScheduleList(current, pageSize, houseCode, deptCode);
const { total, bookingScheduleList, baseMap } = res;
setTotal(total);
setBookingScheduleList(bookingScheduleList);
setHouseName(baseMap?.hospitalName || '');
// 每页数据回来后,默认选中第一项
setWorkDate(bookingScheduleList[0]?.workDate || '');
};
// 依赖:科室变了要重新查;翻页、改每页条数也要重新查
useEffect(() => {
if (!deptCode) return; // deptCode 没有值时不发请求
getSchedule();
}, [deptCode, current, pageSize]);
return (
<div>
<div>{houseName}</div>
{bookingScheduleList.map((item) => (
<div
key={item.workDate} // workDate 不会重复,可作 key
className={workDate === item.workDate ? 'tag active' : 'tag'}
onClick={() => setWorkDate(item.workDate)}
>
<p>{item.workDate}</p>
<p>{item.dayOfWeek}</p>
<p>可预约:{item.availableNumber}</p>
</div>
))}
<Pagination
current={current}
pageSize={pageSize}
total={total}
onChange={(page, size) => {
setCurrent(page);
setPageSize(size);
}}
/>
</div>
);
}要点与易错点
deptCode在一进页面时是空的(科室列表接口还没回来),此时不能发请求,否则会请求出deptCode=any find这种无效数据;所以useEffect里先if (!deptCode) return。- 这个请求和“科室列表”请求时机不同,必须写两个独立的
useEffect,不能合并。 houseCode/deptCode在 TS 里可能是联合类型,调用 API 时要断言成字符串(转写稿里的as string),否则类型报错。- 选中态用
workDate === item.workDate判断,不要用索引index === 0:用索引虽然首屏默认选中没问题,但点击切换后索引就不再是 0,高亮会失效。 - 每页数据回来后要
setWorkDate(list[0].workDate)重置,否则翻页后还停在上页的日期。 - 医院名称
hospitalName只能从排班日期接口的baseMap里取,科室列表接口里没有这个字段,别去错接口找。
示例 3:排班医生列表(监听 workDate,有值才请求)
tsx
import { useState, useEffect } from 'react';
import request from '@/utils/request';
/* ---------- 类型定义 ---------- */
export interface IDoctorItem {
id: string;
createTime: string;
updateTime: string;
houseCode: string;
deptCode: string;
skill: string; // 技能/擅长,字段名以接口文档为准
workDate: string;
reservedNumber: number;
totalNumber: number;
availableNumber: number;
}
export type IDoctorList = IDoctorItem[];
/* ---------- API 封装 ---------- */
export const getDoctorList = (houseCode: string, deptCode: string, workDate: string) =>
request.get<IDoctorList>(
`/doctor/list?houseCode=${houseCode}&deptCode=${deptCode}&workDate=${workDate}`
);
/* ---------- 组件 ---------- */
export default function DoctorList({
houseCode,
deptCode,
workDate,
}: {
houseCode: string;
deptCode: string;
workDate: string;
}) {
const [doctorList, setDoctorList] = useState<IDoctorList>([]);
const getDoctor = async () => {
const res = await getDoctorList(houseCode, deptCode, workDate);
setDoctorList(res);
};
// 只监听 workDate:科室变了会先重新取排班日期,日期重置后这里自然会重新请求
useEffect(() => {
if (!workDate) return; // 没有日期就不发请求
getDoctor();
}, [workDate]);
return (
<ul>
{doctorList.map((item) => (
<li key={item.id}>{item.skill}</li>
))}
</ul>
);
}要点与易错点
- 三个参数里
workDate是最后才拿到的,它一有值,houseCode和deptCode必然已有值,所以依赖数组只写[workDate]就够了,不需要再监听deptCode。 - 组件挂载完成时
workDate是空字符串/any find,此时若不加if (!workDate) return,会白发一次无效请求,返回的列表也拿不到数据。 - 字段名不要靠猜:转写稿里明确说“不知道的字段就跟后台沟通”,类型定义中的注释字段务必对照接口文档核对。
useEffect依赖数组漏项会少请求,多写无关依赖会重复请求;这里的“时机”就是workDate变化 + 组件挂载(且有值)这两种。
常见错误
点击科室后选中态不切换
现象:侧边科室列表可点,但高亮和面包屑不变。
原因:只监听了点击事件,没有把最新科室信息写回选中状态。
修法:从事件参数中取node.deptCode、node.deptName,同时更新科室编码和科室名称状态。科室切换后面包屑名称不更新
现象:科室高亮变了,但顶部仍显示旧科室名。
原因:只重置了deptCode,没有同步重置deptName。
修法:切换科室时同时setDeptCode和setDeptName,保证列表选中与面包屑同源。首次进入就发出
deptCode=undefined的排班日期请求
现象:Network 中出现参数为undefined的无效请求。
原因:排班日期请求依赖deptCode,但在组件挂载时deptCode尚未由科室列表接口返回。
修法:监听deptCode,有值后再请求;不要与科室列表请求共用同一执行时机。分页切换后日期列表不刷新
现象:点击页码后当前页变了,但排班日期数据不变。
原因:请求只依赖deptCode,没有监听current、pageSize。
修法:在请求副作用中同时监听deptCode、current、pageSize,变化后重新拉取。日期选中态用索引判断,点击后逻辑失效
现象:默认第一项高亮正常,但点击其他日期后选中态不符合预期。
原因:用index === 0判断选中,只能表达“第一项”,不能表达“当前选中日期”。
修法:维护workDate状态,用workDate === item.workday判断选中,点击时更新workDate。医生列表在
workDate为空时也发请求
现象:页面初始化出现workDate=undefined的医生列表请求。
原因:副作用执行时只监听了变化,没有加“参数齐全”守卫。
修法:请求前判断houseCode && deptCode && workDate,有值再调用 API;通常监听workDate即可,因为它变化会带动医生列表刷新。
练习
- 实现科室点击切换:点击任意科室后,科室高亮、面包屑科室名、后续排班日期请求参数三者同步变化。说明为什么不能只更新
deptCode。 - 为排班日期列表补全请求副作用:避免首次
deptCode为空时发请求,并支持current、pageSize变化后重新请求。 - 实现排班日期按钮选中态:默认选中当前页第一项,点击其他日期后切换高亮,并同步顶部日期显示。要求不用索引作为最终选中依据。
- 封装并调用排班医生列表接口:写出请求前置守卫,确保
houseCode、deptCode、workDate都有值时才发请求,并解释为什么通常只需监听workDate。 - 检查排班日期响应类型:从响应中解构
total、bookingScheduleList、baseMap.houseName,说明houseName为什么不能从科室列表接口直接获得。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 点击切换科室选中状态 | 06:54 |
| 2 | 排班日期分页列表-类型定义及api函数封装 | 10:47 |
| 3 | 在组件中发送请求获取排班日期的数据 | 08:36 |
| 4 | 排班日期分页列表数据渲染及切换完成 | 13:25 |
| 5 | 排班医生api-类型及函数封装 | 05:30 |
| 6 | 排班医生api调用并打印列表 | 05:38 |
小结
- 科室切换的核心是“事件参数取最新值 → 更新状态 → 状态驱动选中态和面包屑”。
- 选中态不要只靠 DOM 或索引,最终应落到可复用的业务状态上,例如
deptCode、workDate。 - 有依赖关系的接口不能机械地放在同一个挂载时机里,要按参数就绪顺序拆分副作用。
- 分页列表请求必须把分页状态纳入依赖,否则页码变化不会触发数据刷新。
- 日期选中态用
workday比较比用索引更稳,因为点击后选中项不一定还是第 0 项。 - 请求副作用要加参数守卫,避免
undefined参数造成无效请求和错误数据。 - 接口响应类型应显式定义,字段含义不明确时及时与后端确认,不要长期靠猜。
- 页面展示所需数据可能来自不同接口,例如医院名称来自排班日期响应的
baseMap,不能假设科室列表里一定有。