主题
添加医院与修改医院(Day14 1/3)
本页对应 React + TypeScript + 医通项目,共 6 段视频,约 50 分钟。
学习目标
- 能够定位并修复省市区联动 Bug:在切换省份时重置市、区表单值及区列表状态,在切换城市时重置区表单值,并通过表单
name与接口参数对齐完成联动清空。 - 能够完成医院类型与医院状态的渲染:将医院状态按 0/1 映射为“未上线/已上线”,调用数据字典接口获取医院等级并渲染为医院类型下拉选项。
- 能够根据医院列表分页接口定义参数、列表项、列表数据和响应对象的 TypeScript 类型,并封装包含 path 参数与 query 参数的医院列表请求函数。
- 能够在组件挂载后的生命周期钩子中调用医院列表 API,并将返回的列表数据与总条数写入状态。
- 能够实现医院列表分页渲染:配置序号、图片、医院名称、等级、地址、状态、创建时间及操作按钮的列渲染,处理 Base64 图片头、分页器、页码监听和 loading 效果。
- 能够解释并实践多个
useEffect按不同执行时机拆分:页面加载类请求只执行一次,分页请求监听当前页和每页条数变化,避免不合理重复请求。
知识点
省市区联动的重置逻辑
为什么需要:切换省份后,市、区下拉框的可选列表已经变了,但表单里仍保留旧的市、区值,用户会看到“山西省 + 石家庄市”这种不合常识的组合。切换城市后,区也存在同样问题。
怎么想:联动不只是“换列表”,还要“清表单”。省变时清空市、区表单值;市变时清空区表单值。另外,区列表本身是状态数据,省变后如果不清空,用户点开区下拉框仍会看到上一个省/市的区。
怎么做:给表单绑定 form,字段名按医院分页接口参数命名,如 provinceCode、cityCode、districtCode。在省切换回调中:
tsx
const [form] = Form.useForm();
const onProvinceChange = () => {
form.setFieldsValue({
cityCode: undefined,
districtCode: undefined,
});
setDistrictList([]);
};
const onCityChange = () => {
form.setFieldsValue({
districtCode: undefined,
});
};易错点:只清表单不清区列表状态,区下拉仍会出现旧数据;只清区不清市,切换省后市值还在;字段名必须和接口参数一致,否则查询时取不到值。调试时先切换省,观察表单值和 districtList 是否同时变化。
医院状态与医院类型渲染
为什么需要:医院状态是有限枚举,可以直接写死;医院类型(三级甲等、二级甲等)来自数据字典接口,不能手写。
怎么想:状态用 status 判断,0 未上线、1 已上线。类型复用省市区字典接口,传固定参数 10000 获取医院等级。
怎么做:
tsx
const getHospitalType = async () => {
const res = await getDicList(10000);
setHospitalTypes(res);
};
<Option key={item.id} value={item.id}>
{item.name}
</Option>易错点:类型接口与省市区接口是同一个 API 函数,只是参数不同;不要把医院类型写成静态数组。渲染时注意 Option 的 value 与表单字段类型一致。
医院列表接口类型定义与函数封装
为什么需要:医院列表参数多、响应对象大,直接写 any 容易在渲染时取错字段。
怎么想:按“参数类型 → 列表项类型 → 列表类型 → 响应类型”逐层定义。参数中 page、limit 必填,其余如 provinceCode、cityCode、districtCode、hospitalName、hospitalCode、hospitalType、status 为可选。
怎么做:
ts
interface IHospitalListParams {
page: number;
limit: number;
provinceCode?: string;
cityCode?: string;
districtCode?: string;
hospitalName?: string;
hospitalCode?: string;
hospitalType?: string;
status?: number;
}
interface IHospitalListResponse {
content: IHospitalListItem[];
totalElements: number;
}
const getHospitalList = (params: IHospitalListParams) => {
const { page, limit, ...query } = params;
return request.get<IHospitalListResponse>(
`/hospital/list/${page}/${limit}`,
{ params: query }
);
};易错点:page、limit 拼在路径里,其余参数放 params;字段名要跟接口一致,如 totalElements 不是 total;类型定义繁琐但能避免渲染阶段反复猜字段。
生命周期钩子中调用接口
为什么需要:页面加载后要拿到分页数据;页码或每页条数变化后要重新请求。
怎么想:初始化请求和分页监听分开。省、医院类型只需挂载后取一次,医院列表要监听 current、pageSize。
怎么做:
tsx
useEffect(() => {
getHospitalListData();
}, [current, pageSize]);
const getHospitalListData = async () => {
setLoading(true);
const { content, totalElements } = await getHospitalList({
page: current,
limit: pageSize,
});
setHospitalList(content);
setTotal(totalElements);
setLoading(false);
};易错点:转写中调试发现“页码变了但没重新发请求”,原因是请求参数里 page、limit 写死了。必须用状态值。多个 useEffect 不要合并,否则页码变化会重复请求省和医院类型。
分页数据渲染与图片头处理
为什么需要:表格要显示序号、医院 logo、医院名、等级、地址、状态、创建时间和操作按钮。
怎么想:dataSource 绑 hospitalList,rowKey 用 id;需要计算或判断的列用 render;分页配置 current、pageSize、total、onChange。
怎么做:
tsx
<Table
rowKey="id"
dataSource={hospitalList}
loading={loading}
pagination={{
current,
pageSize,
total,
onChange: (page, size) => {
setCurrent(page);
setPageSize(size);
},
}}
columns={columns}
/>序号列:(current - 1) * pageSize + index + 1。状态列:record.status ? '已上线' : '未上线'。按钮文字与状态相反:已上线显示“下线”,未上线显示“上线”。logo 是 base64,必须补图片头:
tsx
<Image src={`data:image/png;base64,${record.logoData}`} width={100} />易错点:base64 不加 data:image/png;base64, 前缀图片不显示;render 里不要直接写死当前页;分页 onChange 要同时更新页码和每页条数。调试时先看接口返回 content、totalElements,再看表格列字段是否对应。
为什么需要多个 useEffect
为什么需要:省、医院类型只在挂载后请求一次;医院列表要随分页变化反复请求。
怎么想:把不同时机拆成不同 useEffect。列表请求监听 current、pageSize;初始化数据用空依赖或单独挂载钩子。
怎么做:一个 useEffect 取省和医院类型,一个 useEffect 监听分页并取医院列表。
易错点:把省、类型请求塞进分页监听里,页码一变就重复请求;React 组件化开发要利用 useEffect 的依赖监听灵活实现,而不是把所有逻辑堆在一个函数里。
从“修改医院”回调看新增与更新分流
本节的核心代码是表单校验通过后的 onFinish。它把“新增医院”和“修改医院”复用在同一个表单里:有 id 走更新,没有 id 走新增。
tsx
...
...
/*
表单校验通过后调用的回调
*/
const onFinish = async (values: ReqAddHospitalParams) => {
try {
setLoading(true)
if (id) { // 更新
await reqUpdateHospital({
...values,
id: +id, // params中的参数类型是string,要将其转化为number
});
} else { // 添加
await reqAddHospital(values);
}
setLoading(false)
// 提示成功
message.success(`${id ? "更新" : "添加"}医院成功`);
// 跳转到医院设置路由
navigate("/syt/hospital/hospitalSet", {replace: true});
} catch (error) {
setLoading(false)
}
};为什么需要:新增和修改的表单字段几乎一致,如果写两套页面,维护成本高。用 id 判断可以复用同一套校验和提交逻辑。
怎么想:先看路由参数里有没有 id;有就说明是编辑,需要把 id 一起提交;没有就是新增。提交期间用 loading 防止重复点击。
怎么做:await 请求成功后提示成功,再用 navigate(..., { replace: true }) 回到列表页。replace 可以避免用户点后退又回到已提交的表单。
易错点:路由参数 id 是字符串,而接口要 number,所以代码里写了 id: +id;另外 catch 里也要 setLoading(false),否则请求失败后按钮会一直转圈。
省市区联动:表单值和列表状态要一起重置
转写稿里先调试了一个典型 bug:从河北切到山西后,省变了,但“市”“区”输入框还显示旧值;更隐蔽的是,切换省后区列表状态没清空,点开区下拉仍能看到上一个省的区。
为什么需要:省变化后,市、区已经不再属于当前省,必须回到“请选择市/请选择区”的状态。
怎么想:切换省会触发获取市列表的方法,所以在这个方法里做两件事:把市、区的表单值设为 undefined,把区列表状态设为空数组。切换市时,再把区的表单值设为 undefined。
怎么做:先用 Form.useForm() 拿到 form,给 Form 绑定 form,并给表单项配置与接口一致的 name,如 provinceCode、cityCode、districtCode。重置时用 form.setFieldsValue 把对应字段设为 undefined,同时 setDistrictList([])。
易错点:只清表单不清列表,会出现“表单显示请选择区,但下拉里还有旧区”的情况;转写中还提到把区列表重置放到请求前,界面响应会更快。
医院类型与状态:状态写死,类型走字典接口
医院状态下拉比较简单,转写稿里直接写死:0 表示未上线,1 表示已上线。医院类型则不是写死的,而是调用数据字典接口,并传固定参数 10000 获取“三级甲等、三级乙等、二级甲等……”等选项。
为什么需要:医院类型属于字典数据,后续可能调整,走接口比硬编码更合理。
怎么想:既然接口已经封装过,就复用同一个数据字典请求,只是参数固定为 10000;组件挂载后请求一次,把结果存到 hospitalTypes 状态。
怎么做:在 useEffect 中调用字典接口,拿到数组后渲染 option,key 和 value 用字典项 id,显示文本用 name。
易错点:不要把医院类型和医院状态混在一起;状态是简单枚举,类型是接口数据;option 的字段名要和接口返回一致。
医院列表 API:类型多,但套路不变
医院列表接口参数较多:pageNumber、limit 必填,hospitalCode、hospitalName、hospitalType、provinceCode、cityCode、districtCode、status 等为选填。转写稿中把参数定义成 IHospitalListParams,列表每一项定义成 IHospitalItem,响应定义成 IHospitalListResponse,最终取 content 和 totalElements。
为什么需要:参数多、返回对象大,不定义类型很容易写错字段。
怎么想:先看接口文档的请求方式、URL、参数和响应;再把分页参数拼到 URL,其余查询参数放到 params 对象里。
怎么做:封装 getHospitalList,函数参数用 IHospitalListParams,请求泛型用 IHospitalListResponse。
易错点:必填和选填要区分;字段名必须和接口一致,例如 provinceCode 不是 province;分页参数写死会导致翻页不生效。
分页渲染与 useEffect 拆分
列表状态包括 current、pageSize、total、hospitalList、loading。表格通过 rowKey、dataSource、columns 渲染;序号列用 render 计算 (current - 1) * pageSize + index + 1;医院 logo 是 base64,需要补上图片头;状态列用三元表达式显示“已上线/未上线”;操作列根据状态显示“上线/下线”。
为什么需要:分页、首屏字典、省市区列表的请求时机不同。
怎么想:省市和医院类型只需挂载后请求一次;医院列表要监听 current 和 pageSize 变化。转写稿特别强调,不要把分页监听和省市/类型请求塞进同一个 useEffect,否则翻页会重复请求字典数据。
怎么做:拆多个 useEffect;列表请求前 setLoading(true),请求后关闭;表格加 pagination 并绑定 current、pageSize、total、onChange。
易错点:翻页没反应时先检查请求参数是否仍写死为第一页;图片不显示时检查 base64 前是否缺少图片头。整体逻辑和医院设置列表一致,只是查询项更多。
本节目标与调试主线
本节从省市区联动的 bug 入手,解决“切换省/市后表单值未重置、区列表状态未清空”的问题;随后渲染医院类型、医院状态,封装医院列表分页 API,并在生命周期钩子中请求、渲染分页数据。转写稿里反复出现的“案地范/安y范”应纠正为 undefined,“内幕”是 name,“好斯”是 house 相关字段。
省市区联动:切省要重置市和区
为什么需要:从河北切到山西后,市、区下拉框列表虽然变了,但文本框仍显示“石家庄市/长安区”,用户会看到与当前省不匹配的旧值。
怎么想:表单值要跟着省变化清空;同时区的下拉列表状态也要清空,否则切换省后点区仍可能出现上一个省的数据。
怎么做:先给表单绑定 form,给各表单项补 name,名称与医院列表分页接口参数对齐,如 provinceCode、cityCode、districtCode。切换省时在 getCity 回调中重置市、区表单值,并把区列表状态设为空数组:
js
form.setFieldsValue({
cityCode: undefined,
districtCode: undefined,
});
setDistrictList([]);切换市时只重置区表单值:
js
form.setFieldsValue({ districtCode: undefined });转写稿调试时发现:只清表单值还不够,区的状态数据仍是旧数组;把 setDistrictList([]) 补上后 bug 才真正解决。
易错点:重置逻辑若写在请求之后,会显得慢;应放在请求前。表单项没有 name 时,form 无法按字段操作。
医院类型与医院状态渲染
为什么需要:医院类型(三级甲等、二级甲等)和状态(已上线/未上线)要显示在下拉框中。
怎么想:状态简单,直接写死 0 未上线、1 已上线;类型来自数据字典接口,和省市区的 API 是同一个函数,只是传固定参数 10000。
怎么做:定义获取医院等级的方法,调用字典接口并把结果存入 hospitalTypes 状态,再用 map 渲染 option;状态用两个固定选项渲染。
易错点:不要把医院类型当成本地常量;转写稿明确它是请求接口得到的。字段名要对齐接口,如 id、value、name。
医院列表 API:类型与函数封装
为什么需要:医院列表分页参数多、返回对象大,不定义类型容易写错字段。
怎么想:按“请求方式、URL、参数、响应结果”四方向看接口;参数分必填 page、limit 和可选查询参数;响应主要取 content 与 totalElements。
怎么做:定义 IHospitalListParams、IHospitalItem、IHospitalListResponse,再封装 getHospitalList。请求地址中页码和每页条数走路径参数,其余走 params。
易错点:转写稿中分页第二页没变化,原因是请求参数把页码写死了;应改为使用 current、pageSize 状态。content 不要写成 countent。
生命周期、分页渲染与 loading
为什么需要:页面挂载后要取列表;页码或每页条数变化后要重新请求。
怎么想:省列表、医院类型只需挂载后取一次;分页请求要监听 current 和 pageSize。因此用多个 useEffect,不要把所有逻辑塞进一个。
怎么做:定义 hospitalList、current、pageSize、total、loading;请求前开 loading,请求后关 loading 并设置列表和总数;表格配置 rowKey、dataSource、pagination,列中用 render 处理序号、图片、状态和操作按钮。图片是 base64 时,src 要补图片头;转写稿现场查格式后拼接才显示。
易错点:useEffect 合并会导致页码变化时省数据、医院类型也重复请求,不合理;分页 onChange 要同时更新当前页和每页条数。
课堂画面






代码示例
示例一:省市区联动(切换省 / 市时重置表单值 + 重置区列表状态)
tsx
import { useEffect, useState } from 'react'
import { Form, Select } from 'antd'
import { reqGetProvince, reqGetCity, reqGetDistrict } from '@/api/hospital'
export default function HospitalSet() {
// 1. 从 Form 里取出 form 实例
const [form] = Form.useForm()
const [provinceList, setProvinceList] = useState<any[]>([])
const [cityList, setCityList] = useState<any[]>([])
const [districtList, setDistrictList] = useState<any[]>([])
useEffect(() => {
getProvinceList()
}, [])
const getProvinceList = async () => {
const res = await reqGetProvince()
setProvinceList(res)
}
// 切换省:清空市、区的表单值 + 清空区列表状态
const getCityList = async (provinceCode: string) => {
// 先重置状态再发请求,页面响应更快
setDistrictList([])
// 2. 用 form 实例批量重置表单值
form.setFieldsValue({ cityCode: undefined, districtCode: undefined })
const res = await reqGetCity(provinceCode)
setCityList(res)
}
// 切换市:清空区的表单值
const getDistrictList = async (cityCode: string) => {
form.setFieldsValue({ districtCode: undefined })
const res = await reqGetDistrict(cityCode)
setDistrictList(res)
}
return (
// 3. form 实例绑定到 Form 上
<Form form={form} layout="inline">
<Form.Item name="provinceCode">
<Select
placeholder="请选择省"
onSelect={getCityList}
options={provinceList.map((item) => ({ value: item.value, label: item.name }))}
/>
</Form.Item>
<Form.Item name="cityCode">
<Select
placeholder="请选择市"
onSelect={getDistrictList}
options={cityList.map((item) => ({ value: item.value, label: item.name }))}
/>
</Form.Item>
<Form.Item name="districtCode">
<Select
placeholder="请选择区"
options={districtList.map((item) => ({ value: item.value, label: item.name }))}
/>
</Form.Item>
</Form>
)
}要点解释 / 易错点
- 想用
form.setFieldsValue操作表单项,前提是Form上绑定了form={form},并且每个Form.Item都必须有name,否则设置不生效。 name的值要和后端列表接口的参数名保持一致:provinceCode、cityCode、districtCode,这样点查询时form.getFieldsValue()拿到的就是接口参数。- 切换省时只重置了表单值是不够的:区的下拉列表状态数据也要清空(
setDistrictList([])),否则虽然输入框显示“请选择区”,点开下拉还能看到上一个省的区。 - 重置顺序建议放在发请求之前(先
setDistrictList([])再await),否则网络慢时会看到旧数据。 - 切换市时同样要
form.setFieldsValue({ districtCode: undefined }),否则市变了、区还停在旧值。
示例二:医院类型(字典接口取)与医院状态(写死)渲染
tsx
import { useEffect, useState } from 'react'
import { Form, Select } from 'antd'
import { getDictList } from '@/api/hospital'
export default function HospitalSet() {
const [form] = Form.useForm()
// 医院类型(等级)列表
const [hostypeList, setHostypeList] = useState<any[]>([])
// 医院类型和省市一样,是组件挂载后取一次就行,放在单独的 useEffect 里
useEffect(() => {
getHostypeList()
}, [])
const getHostypeList = async () => {
// 复用省市区的字典接口,只是传固定参数 10000
const res = await getDictList('10000')
setHostypeList(res)
}
return (
<Form form={form} layout="inline">
{/* 医院类型:字典接口渲染 */}
<Form.Item name="hostype">
<Select
placeholder="请选择医院类型"
options={hostypeList.map((item) => ({ value: item.value, label: item.name }))}
/>
</Form.Item>
{/* 医院状态:直接写死两个值 */}
<Form.Item name="status">
<Select placeholder="请选择医院状态">
<Select.Option value={0}>未上线</Select.Option>
<Select.Option value={1}>已上线</Select.Option>
</Select>
</Form.Item>
</Form>
)
}要点解释 / 易错点
- 医院等级(三级甲等、二级乙等……)不是写死的,它和省市一样走同一个字典接口,只是传固定参数
10000,返回值就是医院等级列表。 - 医院状态是纯前端写死的:
0未上线、1已上线,不要再去请求接口。 - 这段
useEffect必须和“分页请求”的useEffect分开写:分页的useEffect依赖current/pageSize,页码一变就会重新执行,如果把字典请求也塞进去,翻页时会白白重复请求省列表和医院等级。 useEffect可以定义多个,执行时机不同就应该拆开,这是 React 组件化开发里useEffect的核心用法。
示例三:医院列表接口类型定义、请求封装与表格渲染分页
ts
// ---------- 类型定义 ----------
export interface IHospitalListParams {
page: number // 必填
limit: number // 必填
hoscode?: string
hosname?: string
hostype?: string
provinceCode?: string
cityCode?: string
districtCode?: string
status?: number
}
export interface IHospitalItem {
id: string
createTime: string
updateTime: string
isDeleted: boolean
param: {
hostype: string
provinceCode: string
cityCode: string
districtCode: string
address: string
logoData: string
intro: string
route: string
status: number
bookingRule: {
cycle: number
releaseTime: string
stopTime: string
quitDay: number
quitTime: string
rule: string[]
}
}
hoscode: string
hosname: string
hostype: string
provinceCode: string
cityCode: string
districtCode: string
address: string
logoData: string
intro: string
route: string
status: number
}
export type IHospitalList = IHospitalItem[]
export interface IHospitalListResponse {
content: IHospitalList
totalElements: number
}
// ---------- 请求函数封装 ----------
import request from '@/utils/request'
export const reqGetHospitalList = ({ page, limit, ...rest }: IHospitalListParams) => {
return request.get<any, IHospitalListResponse>(
`/admin/hosp/hospital/${page}/${limit}`, // 路径参数
{ params: rest } // 其余都是 query 参数
)
}tsx
// ---------- 组件中使用 ----------
import { useEffect, useState } from 'react'
import { Table, Tag, Button, Space, Image } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { reqGetHospitalList } from '@/api/hospital'
import type { IHospitalList, IHospitalItem } from '@/api/hospital'
export default function HospitalList() {
const [hospitalList, setHospitalList] = useState<IHospitalList>([])
const [total, setTotal] = useState<number>(0)
const [current, setCurrent] = useState<number>(1)
const [pageSize, setPageSize] = useState<number>(3)
const [loading, setLoading] = useState<boolean>(false)
const getHospitalList = async () => {
setLoading(true)
const { content, totalElements } = await reqGetHospitalList({
page: current,
limit: pageSize,
})
setHospitalList(content)
setTotal(totalElements)
setLoading(false)
}
// 挂载 + current/pageSize 变化时重新请求
useEffect(() => {
getHospitalList()
}, [current, pageSize])
const columns: ColumnsType<IHospitalItem> = [
{
title: '序号',
render: (_: any, __: any, index: number) => (current - 1) * pageSize + index + 1,
},
{
title: '医院logo',
render: (row: IHospitalItem) => (
<Image width={100} src={'data:image/jpeg;base64,' + row.param.logoData} />
),
},
{ title: '医院名称', dataIndex: 'hosname' },
{
title: '医院等级',
render: (row: IHospitalItem) => row.param.hostype,
},
{
title: '详细地址',
render: (row: IHospitalItem) => row.param.address,
},
{
title: '状态',
render: (row: IHospitalItem) => (row.status ? <Tag color="green">已上线</Tag> : <Tag>未上线</Tag>),
},
{ title: '创建时间', dataIndex: 'createTime' },
{
title: '操作',
render: (row: IHospitalItem) => (
<Space>
<Button type="primary">查看</Button>
<Button>排班</Button>
<Button>{row.status ? '下线' : '上线'}</Button>
</Space>
),
},
]
return (
<Table
rowKey="id"
loading={loading}
columns={columns}
dataSource={hospitalList}
pagination={{
current,
pageSize,
total,
showSizeChanger: true,
onChange: (page, size) => {
setCurrent(page)
setPageSize(size)
},
}}
/>
)
}要点解释 / 易错点
reqGetHospitalList里page、limit是路径参数(拼在 URL 上),其余筛选条件是 query 参数(放进params对象),两者不能混。- 请求函数里务必用解构出来的
page、limit,如果写死1、3,翻页时请求参数永远不变,看起来“点了第二页没反应”。 content是列表数组,totalElements是总条数,这两个才是页面真正要用的字段,类型定义时只需覆盖它们及列表项。- 图片是 base64,
src必须拼上data:image/jpeg;base64,前缀,否则图片不显示。 Table必须配置rowKey="id",否则会有 key 警告;dataSource直接给hospitalList。- 分页依赖
current、pageSize两个状态,useEffect的依赖数组要同时监听它们;pagination.onChange里更新这两个状态即可触发重新请求。 loading在请求前true、请求后false,绑到Table的loading属性上,注意try/finally或异常时也要关闭。
常见错误
1. 切换省份后,市/区下拉框仍显示上一次选中的值
- 现象:从河北切到山西,省变了,但"市""区"输入框里还挂着"石家庄市/长安区"。
- 原因:只更新了
provinceCode的表单值,没有用form.setFieldsValue({ cityCode: undefined, districtCode: undefined })把联动项清空。 - 修法:在
getCity回调(省onSelect)里同时重置市、区两个字段值,缺一不可。
2. 表单值已清空,但点开"区"下拉仍是上一个省/市的区列表
- 现象:切到内蒙古后点区,列表里还是"新荣区"这类旧数据。
- 原因:只重置了表单值,没重置区列表的状态数据
districtList;表单值和选项数据是两套东西。 - 修法:切省时
setDistrictList([]);切市时同样重置区表单值 + 区列表,二者配对处理。
3. form.setFieldsValue 调用无效、validateFields 取不到值
- 现象:代码写了重置,页面上毫无反应;或提交时 values 是空对象。
- 原因:
Form没有form={form}绑定useForm()实例;或Form.Item缺name(name 必须与接口参数名一致,如provinceCode、houseType)。 - 修法:先
const [form] = Form.useForm()→<Form form={form}>→ 每个Form.Item按接口字段补齐name,三步顺序不能漏。
4. 点击第二页数据不刷新,永远是第一页内容
- 现象:分页器页码变了,表格内容纹丝不动。
- 原因:请求函数的
page/limit被写死成1和3,没接current、pageSize状态;或useEffect依赖数组里漏了这两个状态。 - 修法:请求时传
current、pageSize,并在useEffect依赖中显式列出,页码/条数变化即重新请求。
5. 省市区、医院等级数据在翻页时被重复请求
- 现象:每点一次下一页,字典接口就多发一次,Network 面板请求翻倍。
- 原因:把所有请求塞进同一个
useEffect,依赖里带上了分页状态。 - 修法:按执行时机拆成多个
useEffect:字典类(省/市/区/等级)依赖数组为空,只在挂载时跑一次;列表请求单独一个,依赖current、pageSize及查询条件。
6. 表格图片列空白(只显示破图)
- 现象:
logoData有值,<Image src={record.logoData} />却不显示。 - 原因:后端返回的是裸 base64 字符串,
src缺 MIME 头。 - 修法:拼接成
data:image/png;base64,${record.logoData}(注意逗号不能少)。
7. 接口类型定义照抄出错,TS 不报错但运行/取值异常
- 现象:某个字段类型写成
string实际是数组,或残留了后端没有的字段。 - 原因:从接口文档整体粘贴后未逐字段核对、未删无用字段、必填与选填(
?)没区分。 - 修法:对着文档逐项过一遍,必填不加
?,选填加?,数组/对象类型按实际结构写,无用字段直接删除。
练习
联动自查:故意把"切市时重置区"的代码注释掉,跑一遍"选河北→唐山→路南区→切到赤峰市",记录下拉框和表单值分别出现什么异常,再写回代码验证。要求能说出"表单值"和"状态数据"各自负责什么。
请求时机改造:把当前所有请求合并进一个
useEffect,观察翻页时 Network 面板的请求数量;再拆回按需分发的多个useEffect,对比前后请求次数,用一句话总结拆分依据。类型补全:不看视频,只打开 Swagger 的分页列表接口,独立写出
IHospitalListParams、IHospitalItem、IHospitalListResponse三个类型,然后与笔记中的版本 diff,列出你漏掉或写错的字段。分页序号推导:为序号列写
render,要求第 1 页显示 1、2、3,第 2 页接着显示 4、5、6。先手推公式再上机,并把pageSize改成 5 验证公式是否仍然成立。状态按钮闭环:让"上线/下线"按钮的文案与
status严格对应(0 显示"上线"、1 显示"下线"),点击后调用接口并刷新列表,同时保证请求期间按钮loading、请求失败时loading能正确关闭(提示:try/finally)。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 省市区联动-bug解决 | 11:20 |
| 2 | 医院类型-医院状态-渲染 | 05:46 |
| 3 | 医院列表api-类型及函数封装 | 15:09 |
| 4 | 医院列表api函数-声明周期钩子中调用 | 02:23 |
| 5 | 医院列表分页数据渲染 | 12:45 |
| 6 | react开发释疑 | 02:46 |
小结
- 联动下拉框的"清空"是两件事:清表单值(用户看到的)和清选项数据(下拉里能选的),少做一件就出 bug。
- 用
useForm操作表单的前提是三步齐全——创建实例、Form绑定、Form.Item有name,且name必须与接口参数名对齐,否则后续提交、重置全是空谈。 useEffect的拆分依据是执行时机,不是代码行数:一次性的字典数据与随分页变化的数据必须分开,否则会产生无意义请求。- 分页功能的完整链路是:状态(current/pageSize/total)→ 请求参数 → 依赖监听 → 回填状态,任一环写死都会导致"点了没反应"。
- 类型定义是这类后台页面的主要工作量,难点不在技术而在耐心:必填/选填、数组/对象、无用字段,都要逐个核对。
- 状态渲染(上线/下线、按钮文案)本质是三元表达式与业务语义的映射,写反了不会报错,只会悄悄出错,需要点一遍验证。
- 所有异步请求都应包在
try/finally里收尾,loading的关闭不能依赖"成功路径",否则一次失败就永久转圈。