主题
批量删除医院与医院管理 - 医院列表(Day14 2/3)
本页对应 React + TypeScript + 医通项目,共 5 段视频,约 39 分钟。
学习目标
- 能够通过获取表单数据、维护查询状态并监听状态变化,实现医院列表查询,并验证筛选条件变化时重新发起请求。
- 能够实现医院列表清空功能,重置表单数据、查询状态和当前页码,并验证清空后列表恢复默认查询结果。
- 能够创建医院详情路由组件,配置带动态参数的路由,并通过编程式导航从医院列表跳转到详情页。
- 能够使用描述列表组件完成医院详情页静态布局,正确设置列跨度、标签样式及返回按钮。
- 能够改造并定义医院详情相关 TypeScript 类型,封装通过 ID 获取医院详情的 API 函数,并验证类型与接口调用无报错。
知识点
批量删除的入口:按钮禁用条件
为什么需要:医院列表里如果只能逐条删除,选中多条后还要反复点删除,效率低且容易误操作。批量删除要先保证“没有选中数据时不能点”,否则会发出无意义的请求。
怎么想:按钮是否可用,取决于 selectedIds 是否为空。笔记中直接用了 disabled={selectedIds.length===0},即选中数量为 0 时禁用,选中至少一条时启用。
怎么做:在按钮上同时绑定 disabled 和 onClick,并保留危险按钮样式:
tsx
<Button className="ml" type="primary" danger disabled={selectedIds.length===0}
onClick={batchRemoveHospitalList}>批量删除</Button>易错点:selectedIds 必须是响应式状态,否则按钮禁用状态不会随选择变化;删除请求参数应是 ID 数组,不要误传成单个 ID 或对象。笔记未展开 selectedIds 的来源,实际应确认它来自表格多选。
批量删除函数:先请求、再提示、后刷新
为什么需要:删除成功后,后端数据已经变化,如果只弹提示不刷新列表,页面仍会显示已删除的医院,用户会以为删除失败。
怎么想:批量删除是异步操作,顺序应是“发请求 → 成功后提示 → 重新获取最新列表”。转写稿里查询和清空功能也反复强调:状态或数据变化后要重新发请求,批量删除同样要回到列表刷新这一条主线。
怎么做:笔记中的函数如下:
tsx
/*
批量删除
*/
const batchRemoveHospitalList = async () => {
// 请求批量删除
await reqBatchRemoveHospitalList(selectedIds);
// 提示成功
message.success("批量删除成功");
// 重新获取最新医院列表
getHospitalList(current, pageSize);
};这里 await 很关键:要等删除请求完成后再提示和刷新。刷新时继续使用当前页 current 和每页条数 pageSize,保持用户所在分页位置。
易错点:不写 await 可能导致删除还没完成就刷新列表;刷新时参数不要漏掉 current、pageSize;如果接口失败,笔记代码没有展示错误处理,实际项目应结合请求封装补充失败提示。转写稿中查询功能调试时曾出现“当前页在第二页导致查不到数据”的问题,并因此把 current 设回 1;批量删除后如果当前页被删空,也要注意页码是否还有数据,不能只刷新不检查。
完整代码片段与调试过程
为什么需要:把入口按钮和删除函数放在一起看,才能确认事件、参数、刷新三者是否闭环。
怎么想:先确认按钮只在有选中时可用,再确认点击后调用的是批量删除函数,最后确认函数内部完成了请求、提示、刷新。
怎么做:笔记中相关代码集中在 src/pages/hospital/hospitalSet/index.tsx:
tsx
import { reqBatchRemoveHospitalList } from "@/api/hospital/hospitalSet";
...
...
/*
批量删除
*/
const batchRemoveHospitalList = async () => {
// 请求批量删除
await reqBatchRemoveHospitalList(selectedIds);
// 提示成功
message.success("批量删除成功");
// 重新获取最新医院列表
getHospitalList(current, pageSize);
};
...
...
<Button className="ml" type="primary" danger disabled={selectedIds.length===0}
onClick={batchRemoveHospitalList}>批量删除</Button>调试时可按这个顺序看:先勾选多条医院,观察按钮是否从禁用变为可点;点击后在 Network 中确认请求参数是选中 ID 数组;请求成功后看是否出现“批量删除成功”;最后看列表是否重新请求并更新。转写稿里查询、清空功能的调试思路也是“操作后重新发请求并观察列表”,批量删除应沿用同样的验证方式。
易错点:不要把 onClick={batchRemoveHospitalList} 写成立即执行;不要忘记引入 message 和删除 API;删除后若列表没有变化,优先检查 getHospitalList 是否被调用、参数是否正确。
医院列表页面与本节目标

医院列表是医院管理模块的入口页。为什么需要先做列表?因为后续“查看医院”“医院排班”都要从列表行进入,列表的查询条件、分页状态会直接影响详情页和排班页的取数。怎么想?先把列表页的查询、清空做稳,再创建详情路由组件,最后补详情接口类型与函数。怎么做?本节围绕列表查询/清空、详情路由配置、详情静态布局、详情 API 封装展开。易错点:不要一上来写详情页,先保证列表筛选和分页联动正确,否则详情入口的 ID 容易带错。


查询与清空的核心思路
为什么需要这套思路?列表查询条件多,如果每次点击查询都手动拼参数,状态会分散且难同步。怎么想?转写稿里明确:点击查询时用表单对象拿到所有表单值,把最新值设置到 formFields 状态;在 useEffect 中监听 formFields 的每个字段,状态变化后重新发请求。怎么做?查询函数先取表单值,再 setFormFields,并重置 current 为 1。易错点:useEffect 对对象无法直接深度监听,必须把 cityCode、provinceCode、districtCode、hospitalName、hospitalType、state 等字段逐项写进依赖;否则筛选不变时不会重发请求。
ts
const search = () => {
const values = form.getFieldsValue()
setFormFields({
hospitalName: values.hospitalName,
hospitalType: values.hospitalType,
provinceCode: values.provinceCode,
cityCode: values.cityCode,
districtCode: values.districtCode,
state: values.state,
// 其余表单字段同理
})
setCurrent(1)
}
useEffect(() => {
getHospitalList({ page: current, limit: pageSize, ...formFields })
}, [
current,
formFields.hospitalName,
formFields.hospitalType,
formFields.provinceCode,
formFields.cityCode,
formFields.districtCode,
formFields.state
])查询时重置分页的调试
为什么需要重置分页?转写稿调试“北京已上线医院”时发现只有 3 条,但页面还在第二页,所以查询无数据。怎么想?筛选条件变化后,旧页码可能超出新结果范围。怎么做?在查询函数里执行 setCurrent(1)。易错点:只改筛选不重置页码,会出现“明明有数据却空白”的假象;调试时先看当前页是否还停留在第二页。
ts
setCurrent(1)清空按钮的禁用判断
为什么需要禁用判断?表单全空时“清空”没有意义,应禁用。怎么想?把 formFields 的所有属性值取出组成数组,用 every 判断是否都为 undefined。怎么做?转写稿使用 Object.values(formFields).every(item => item === undefined) 作为按钮 disabled 条件。易错点:不要写七个连等式,既冗长又易漏字段;注意字段初始值必须是 undefined,不能混用空字符串导致判断失效。
ts
const isClearDisabled = Object.values(formFields).every(
item => item === undefined
)清空操作的三步
为什么清空要分三步?只清 UI 表单不清请求状态,列表仍会带旧条件;不清页码,可能停在无效页。怎么想?先恢复表单,再恢复 formFields,最后回到第一页。怎么做?调用 resetFields();把 formFields 每项设为 undefined;setCurrent(1)。易错点:清空后要触发列表重新请求,依赖 formFields 和 current 的监听逻辑要能接住这次变化。
ts
const clear = () => {
form.resetFields()
setFormFields({
hospitalName: undefined,
hospitalType: undefined,
provinceCode: undefined,
cityCode: undefined,
districtCode: undefined,
state: undefined
})
setCurrent(1)
}查看医院:详情路由组件与配置
为什么先建路由?列表“查看”要跳详情,没有路由组件就无法承接。怎么想?在 pages/hospital/components 下建 hospitalDetail.tsx,用 lazy 引入,在列表路由下加子路由,路径带 :id 占位符,并设置 hidden: true 隐藏侧边栏。怎么做?查看按钮用 useNavigate 编程式跳转并拼接 ID。易错点:路由参数是 path 参数时,跳转地址必须带 ID;隐藏路由不要误显示在菜单里。
ts
const navigate = useNavigate()
navigate(`/hospital/detail/${record.id}`)详情页静态布局
为什么用 Descriptions?详情字段是键值对,Ant Design 的 Descriptions 正好适配。怎么想?默认一行三列,需要两列时给 Descriptions.Item 设 span={1.5},独占一行设 span={3}。怎么做?外层用 Card,标题“基本信息”“预约规则”,底部加返回按钮;必要时用 labelStyle 调文字宽度。易错点:span 不设置会导致列宽不符合设计;窄屏下文字竖排可通过 labelStyle 调整。
详情接口类型改造与 API 封装
为什么改类型?详情返回值与列表项相似但不完全一致,bookingRule 可能为 null,intro 也可能为空。怎么想?抽出 IBookingRule,让 bookingRule 为 IBookingRule | null,intro 为 string | null,再定义 IHospitalDetail。怎么做?封装 getHospitalDetail(id),地址用 path 参数替换 ID,返回 Promise<IHospitalDetail>。易错点:不要直接复用旧类型导致 null 报错;API 参数别漏 ID,返回值类型要写清。
ts
export interface IHospitalDetail {
bookingRule: IBookingRule | null
hospital: IHospitalItem
}
export const getHospitalDetail = (id: string) => {
return request.get<IHospitalDetail>(`/hospital/detail/${id}`)
}本节要做什么
本节笔记给出医院列表相关需求:
- 医院列表静态组件
- 医院列表数据分页展示
- 省市区搜索条件三级联动
- 查询医院列表
- 清空搜索条件
- 查看医院详情
- 医院排班
- 医院上线
转写稿重点讲了“查询、清空、医院详情路由与静态布局、详情接口类型与函数”。第7、8项只出现在需求清单里,转写稿未展开,下面不编造实现细节。
查询列表:把表单值变成可监听状态
为什么需要:查询条件不能只停在表单里,否则分页、刷新列表时拿不到最新条件。
怎么想:点击查询时先用表单实例取到所有字段值,把它放进一个状态 formFields;再让 useEffect 监听这个状态里的每个字段,字段变化就重新发请求。
怎么做:转写稿中的核心是 form.getFieldsValue()、setFormFields(values)、setCurrent(1),请求参数用 { current, pageSize, ...formFields } 展开。依赖要逐项写,不能只写对象整体:
ts
const search = () => {
const values = form.getFieldsValue();
setFormFields(values);
setCurrent(1);
};
useEffect(() => {
getHospitalList({ current, pageSize, ...formFields });
}, [
formFields.houseCode,
formFields.houseName,
formFields.provinceCode,
formFields.cityCode,
formFields.districtCode,
formFields.houseType,
formFields.status,
]);调试时:选“山西/北京”、输入名称、选类型,点查询能取到值;切到北京后列表只剩北京数据;条件不变再点查询不会重发,因为状态没变;切河北无数据,因为当时只有北京数据。还发现查“已上线”时停在第二页为空,所以查询时必须把 current 重置为 1。
易错点:undefined 不要写成“安迪饭”;useEffect 依赖不能漏;查询按钮要放在 Form 内并配合提交/取值;查询后不重置页码会出现空页。
清空条件:用 every 判断是否全空
为什么需要:没有条件时“清空”按钮应禁用,有条件时才可点。
怎么想:formFields 是对象,用 Object.values(formFields) 取出所有值组成数组,再用 every 判断是否都为 undefined。
怎么做:
ts
const isClearDisabled = Object.values(formFields).every(
(item) => item === undefined
);
const clear = () => {
form.resetFields();
setFormFields({
houseCode: undefined,
houseName: undefined,
provinceCode: undefined,
cityCode: undefined,
districtCode: undefined,
houseType: undefined,
status: undefined,
});
setCurrent(1);
};调试时:有值时按钮激活;刷新后全空则禁用;选北京查询后点清空,表单和列表条件都恢复,页码回第一页。
易错点:用 every 不是 some;清空要同时处理表单、formFields 状态和当前页;只 resetFields 不够,因为请求用的是 formFields。
详情路由:先建组件再配路由
为什么需要:点“查看”要进入独立详情页。
怎么想:详情属于医院列表下的子路由,先建组件,再懒加载并配置带参数路由,最后用编程式导航跳转。
怎么做:创建 HospitalDetail.tsx,用 lazy 引入;路由加 hidden: true 隐藏侧边栏,路径带 :id 占位;按钮里用 useNavigate:
ts
navigate(`/hospital/detail/${record.id}`);调试时点“查看”能跳转,说明路由和参数拼接正确。
易错点:路由路径要带 :id;隐藏路由别忘 hidden;跳转路径要和配置一致。
详情静态布局:Descriptions 的列与 span
为什么需要:详情页要展示基本信息、预约规则等键值对。
怎么想:用 Card 包 Descriptions,默认一行三列;需要半行就设 span={1.5},需要独占一行就设 span={3}。
怎么做:把“基本信息”放 title,医院名称、logo、编码、地址等用 Descriptions.Item;预约规则同样处理;底部加返回按钮。调试时发现医院名称和 logo 要各占一半,就都设 span={1.5};坐车路线、医院简介设 span={3}。
易错点:span 总和要与列数匹配;文字竖排时可用 labelStyle 调宽度;不要把所有项都默认三列。
详情接口与类型:复用后按返回值改造
为什么需要:详情接口返回结构与列表项相似,但 bookingRule 可能是 null。
怎么想:先复用 IHospitalItem,对比后发现 bookingRule、intro 需要调整,再定义 IHospitalDetail。
怎么做:定义 IBookingRule,把 bookingRule 设为 IBookingRule | null,intro 设为 string | null;详情返回类型包含 bookingRule 和 hospital。函数 getHospitalDetail(id) 返回 Promise<IHospitalDetail>,地址用 /hospital/detail/${id}。改造后刷新列表,原功能不报错。
易错点:null 与 undefined 不要混;接口地址参数要替换成真实 id;类型改造后要回归列表功能。
课堂画面





代码示例
代码段 1:医院列表——查询 / 清空 / 批量删除
tsx
// src/pages/hospital/hospitalSet/index.tsx
import { useEffect, useState } from "react";
import { Button, Form, Input, Select, Table, message } from "antd";
import {
reqGetHospitalList,
reqBatchRemoveHospitalList,
} from "@/api/hospital/hospitalSet";
// 表单字段类型:只包含搜索条件,不含 page / limit
interface FormFields {
hoscode?: string;
hosname?: string;
provinceCode?: string;
cityCode?: string;
districtCode?: string;
hostype?: string;
status?: number;
}
const emptyFields: FormFields = {
hoscode: undefined,
hosname: undefined,
provinceCode: undefined,
cityCode: undefined,
districtCode: undefined,
hostype: undefined,
status: undefined,
};
export default function HospitalSet() {
const [current, setCurrent] = useState(1);
const [pageSize, setPageSize] = useState(3);
const [total, setTotal] = useState(0);
const [hospitalList, setHospitalList] = useState<any[]>([]);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [formFields, setFormFields] = useState<FormFields>(emptyFields);
const [form] = Form.useForm();
/* 获取医院列表 */
const getHospitalList = async (page = current, limit = pageSize) => {
const res = await reqGetHospitalList({ page, limit, ...formFields });
setHospitalList(res.content);
setTotal(res.totalElements);
};
/* 监听分页与搜索条件,任一变化重新发请求 */
useEffect(() => {
getHospitalList();
}, [
current,
pageSize,
formFields.provinceCode,
formFields.cityCode,
formFields.districtCode,
formFields.hosname,
formFields.hoscode,
formFields.hostype,
formFields.status,
]);
/* 查询:取表单最新值 -> 重置状态 -> 回到第一页 */
const search = async () => {
const values = await form.validateFields();
setFormFields({ ...formFields, ...values });
setCurrent(1);
};
/* 清空:表单重置 + 状态重置 + 回到第一页 */
const clear = () => {
form.resetFields();
setFormFields({ ...emptyFields });
setCurrent(1);
};
/* 批量删除 */
const batchRemoveHospitalList = async () => {
await reqBatchRemoveHospitalList(selectedIds);
message.success("批量删除成功");
getHospitalList(current, pageSize);
};
const columns = [
{
title: "序号",
render: (_: any, __: any, index: number) =>
(current - 1) * pageSize + index + 1,
},
{ title: "医院名称", dataIndex: "hosname" },
{ title: "医院编码", dataIndex: "hoscode" },
{ title: "状态", dataIndex: "status" },
// 操作列:查看 / 排班 / 上线 / 删除(见代码段 2)
];
return (
<>
<Form form={form} layout="inline">
{/* 省市区三级联动 + 医院名称 / 编码 / 类型 / 状态等表单项 */}
<Form.Item label="医院名称" name="hosname">
<Input />
</Form.Item>
<Form.Item label="医院编码" name="hoscode">
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" onClick={search}>
查询
</Button>
<Button
className="ml"
disabled={Object.values(formFields).every(
(item) => item === undefined
)}
onClick={clear}
>
清空
</Button>
</Form.Item>
</Form>
<Table
rowKey="id"
dataSource={hospitalList}
columns={columns}
rowSelection={{
selectedRowKeys: selectedIds,
onChange: (keys) => setSelectedIds(keys as string[]),
}}
pagination={{
current,
pageSize,
total,
showSizeChanger: true,
pageSizeOptions: [3, 5, 10],
onChange: (page, size) => {
setCurrent(page);
setPageSize(size);
},
}}
/>
<Button
className="ml"
type="primary"
danger
disabled={selectedIds.length === 0}
onClick={batchRemoveHospitalList}
>
批量删除
</Button>
</>
);
}关键行与易错点
- 查询与清空的核心思路:
search先取表单最新值,再setFormFields重置状态;useEffect监听formFields中的各个字段,状态一变就重新发请求。请求参数用...formFields展开,这样搜索条件才能带到接口里。 useEffect依赖数组必须逐项列出formFields.xxx。直接写[formFields]做不了深度监听,对象内部字段变化不会触发;字段少一个,该条件查询就不生效。- 查询和清空都要
setCurrent(1)。否则停在第二页查询,接口返回空数据会让人误以为查询坏了。 - 清空按钮的禁用条件是
Object.values(formFields).every(item => item === undefined):全为undefined才禁用,只要有一个条件有值就可用。不要用七个if连等去写。 setFormFields({ ...emptyFields })每次传新对象。若直接传同一个常量对象引用,React 认为状态没变,可能不重新渲染。- 批量删除成功后要
getHospitalList(current, pageSize)刷新列表;按钮disabled={selectedIds.length === 0}保证没勾选时不可点。
代码段 2:医院详情路由组件创建、路由配置与查看跳转
tsx
// ① 创建路由组件:src/pages/hospital/hospitalList/components/hospitalDetail.tsx
// (组件内容见代码段 3,这里先建文件、默认导出组件)
// ② 路由配置:src/router/index.tsx
import { lazy, Suspense } from "react";
import { Spin } from "antd";
const HospitalList = lazy(() => import("@/pages/hospital/hospitalList"));
const HospitalDetail = lazy(
() => import("@/pages/hospital/hospitalList/components/hospitalDetail")
);
export const routes = [
// ……其他路由
{
path: "/syt/hospital",
element: <Hospital />, // 医院管理布局
children: [
{
path: "list",
element: (
<Suspense fallback={<Spin />}>
<HospitalList />
</Suspense>
),
},
{
// 带 params 占位符,详情页靠 useParams 拿到 id
path: "detail/:id",
element: (
<Suspense fallback={<Spin />}>
<HospitalDetail />
</Suspense>
),
hidden: true, // 不在左侧菜单中显示
},
],
},
];tsx
// ③ 列表页「查看」按钮:编程式导航跳转
// src/pages/hospital/hospitalList/index.tsx
import { useNavigate } from "react-router-dom";
import { Button } from "antd";
const navigate = useNavigate();
const columns = [
// ……
{
title: "操作",
render: (row: any) => (
<>
<Button
type="link"
onClick={() => navigate("/syt/hospital/detail/" + row.id)}
>
查看
</Button>
</>
),
},
];关键行与易错点
- 详情页是列表页的子组件,放在
hospitalList/components/下;路由上用lazy+Suspense按需加载。 - 详情路由必须写成
detail/:id这种带占位符的形式,因为组件里要用useParams取id;漏了:id就取不到参数。 hidden: true用于让详情页不出现在菜单里,但它依然属于医院管理的子路由,所以保留布局、没有侧边栏干扰。- 跳转用编程式导航
navigate("/syt/hospital/detail/" + row.id),路径要和路由配置完全对应(前缀 +detail+ id),少写一段会 404。
代码段 3:医院详情页静态布局(Descriptions)
tsx
// src/pages/hospital/hospitalList/components/hospitalDetail.tsx
import { Button, Card, Descriptions } from "antd";
import { useNavigate } from "react-router-dom";
export default function HospitalDetail() {
const navigate = useNavigate();
return (
<Card>
<Descriptions title="基本信息">
<Descriptions.Item label="医院名称" span={1.5}>
北京人民医院
</Descriptions.Item>
<Descriptions.Item label="医院logo" span={1.5}>
图片
</Descriptions.Item>
<Descriptions.Item label="医院编码" span={1.5}>
10010
</Descriptions.Item>
<Descriptions.Item label="医院地址" span={1.5}>
北京市
</Descriptions.Item>
<Descriptions.Item label="坐车路线" span={3}>
地铁 1 号线
</Descriptions.Item>
<Descriptions.Item label="医院简介" span={3}>
简介内容
</Descriptions.Item>
</Descriptions>
<Descriptions title="预约规则">
<Descriptions.Item label="预约周期" span={1.5}>
7 天
</Descriptions.Item>
<Descriptions.Item label="放号时间" span={1.5}>
08:30
</Descriptions.Item>
<Descriptions.Item label="停挂时间" span={1.5}>
22:00
</Descriptions.Item>
<Descriptions.Item label="退号时间" span={1.5}>
就诊前一天 15:00
</Descriptions.Item>
<Descriptions.Item label="预约规则" span={3}>
规则内容
</Descriptions.Item>
</Descriptions>
<Button className="mt" onClick={() => navigate(-1)}>
返回
</Button>
</Card>
);
}关键行与易错点
- 整个详情页外层用
Card提供白色背景,内层用 antd 的Descriptions(描述列表)做键值对展示。 Descriptions默认一行显示 3 列(三个 key-value)。要让某行只显示两列,就给Descriptions.Item加span={1.5},两个 1.5 正好占满一行;要独占一行则用span={3}(如“坐车路线”“医院简介”“预约规则”)。- 文字(label)和内容分属不同区域,想调整文字宽度可以用
labelStyle;屏幕窄时文字竖排,也可以用labelStyle设宽度解决。 - 返回按钮加
className="mt"拉开与上方表格的距离,navigate(-1)是回退到上一页。
常见错误
1. 点「查询」没反应,或查询后表格空白
- 现象:选完条件点查询,列表不刷新;或筛选后跳到第 2 页直接没数据。
- 原因:
useEffect依赖只写了formFields这个对象本身(引用未变不触发);且查询时没把current重置为 1。 - 修法:依赖项拆成
formFields.hospitalName、formFields.provinceCode等每一个字段;search内部先setCurrent(1)再发请求。
2. 清空按钮的禁用状态判断写反 / 写残
- 现象:表单全空时按钮仍可点;或只填了医院名称按钮还是灰的。
- 原因:用
some代替了every,或手写 7 个=== undefined的&&漏掉某个字段。 - 修法:
disabled={Object.values(formFields).every(v => v === undefined)},用「全部为空才禁用」这一条规则覆盖所有字段。
3. 清空后仍停留在原页码,列表空白
- 现象:在第 3 页点清空,列表变成空表格。
- 原因:只做了
resetFields()和setFormFields(初始值),漏了页码归位;或只重置了状态没重置表单(输入框里文字还在)。 - 修法:三件事一起做——
resetFields()清 UI、setFormFields(...)清状态、setCurrent(1)归页码,缺一不可。
4. 详情页跳转后左侧菜单消失/出现,或拿不到 id
- 现象:进详情页侧边栏还在;或
useParams()解构出undefined,请求 404。 - 原因:路由配置漏了
hidden: true;或path没写:id占位符,跳转时用字符串拼接却对不上参数名。 - 修法:路由项加
hidden: true,path: '/hospital/detail/:id',跳转用/hospital/detail/${record.id},详情页const { id } = useParams()。
5. Descriptions 排布错乱,本该两列变三列
- 现象:医院名称/logo、编码/地址挤成一行三个,坐车路线没独占一行。
- 原因:没理解
span是「栅格份数」而非「列数」,默认一行 3 份。 - 修法:一行两个用
span={1.5}(两个 1.5 凑满 3),独占整行用span={3},改完对照设计图逐行核对。
6. 复用 IHospitalItem 后 TS 报错或运行时崩
- 现象:详情接口返回
bookingRule: null,页面读bookingRule.releaseTime直接白屏。 - 原因:把
bookingRule从可空改成了必填的IBookingRule。 - 修法:抽出的
IBookingRule单独定义,字段声明为bookingRule: IBookingRule | null,intro同理保留string | null;改造完刷新列表页回归验证旧功能。
7. 批量删除成功后勾选状态没清
- 现象:删完表格刷新了,但按钮仍可点、选中项还在。
- 原因:只重新拉列表,没重置
selectedIds。 - 修法:
await请求成功 → 提示 →setSelectedIds([])→ 重新getHospitalList;同时注意删完当前页可能空了,需回退页码。
练习
- 给「查询」按钮补一个防抖(500ms),并说明为什么依赖数组里拆字段和防抖不冲突。
- 现在只做了「清空」,请再实现一个「重置并回到默认排序」的扩展逻辑,要求不新增多余状态。
- 把详情页
Descriptions的labelStyle抽成常量,解决窄屏下标签竖排换行难看的问题,并验证不影响其他行。 - 详情接口若返回
bookingRule: null,页面应如何降级展示?给出处理方案并说明改在哪一层(组件 / API 类型)。 - 批量删除后如果当前页只剩 0 条,请补全页码回退逻辑,并列出需要改动的位置。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 医院列表查询功能实现 | 09:31 |
| 2 | 清空功能实现 | 08:50 |
| 3 | 医院详情路由组件创建及路由配置 | 04:35 |
| 4 | 医院详情页静态布局 | 10:10 |
| 5 | 医院详情api类型及函数定义 | 05:54 |
小结
- 查询的核心是「表单值 → 状态 →
useEffect监听 → 重发请求」,链路断在任意一环都表现为「点了没反应」。 - 对象型状态做依赖必须拆到叶子字段,React 的浅比较不会替你深入对象内部。
- 任何筛选条件变化都要把
current归 1,这是分页 + 搜索组合场景的固定动作。 - 按钮禁用状态优先用
Object.values(...).every(...)表达规则,而不是堆砌&&,可读性和可维护性都更好。 - 清空 = 表单 UI 重置 + 状态重置 + 页码归位,三件事缺一即出 bug。
- 详情页属于列表的子路由,务必配
hidden: true与:id占位符,跳转与取参要对齐。 Descriptions的span是栅格份数(一行 3 份),不是列数,这是布局错乱的高频根源。- 类型复用时优先「抽取 + 联合类型」,把接口可能返回
null的字段显式声明出来,比事后加可选链更可靠。