主题
渲染科室列表与排班日期分页数据获取(Day13 2/3)
本页对应 React + TypeScript + 医通项目,共 7 段视频,约 55 分钟。
学习目标
- 能够根据接口文档定义医院新增请求参数类型,并封装新增医院设置的 API 函数,正确传入请求体数据。
- 能够使用表单实例获取表单数据,完成新增医院提交、成功提示、列表页跳转及失败提示处理。
- 能够为单条删除按钮绑定事件,使用确认弹窗获取当前记录 ID,调用删除 API,并在删除成功后提示并刷新列表。
- 能够配置表格行选择功能,获取并维护选中项 ID 数组,根据选中数量控制批量删除按钮的禁用状态,并完成批量删除及状态清空。
- 能够配置带路由参数的编辑路由,为编辑按钮绑定跳转事件并携带当前记录 ID。
- 能够从路由参数中获取 ID,在组件挂载时按条件请求医院详情,并使用表单批量赋值方法将数据回显到表单,同时区分添加与编辑场景。
知识点
一、先明确目标:接口数据要变成 Tree 能用的数据
为什么需要:Tree 组件不直接认识接口返回的科室对象,必须通过字段映射、展开项、禁用项、选中项等配置,才能正确渲染。
怎么想:先把请求回来的 departmentList 打印出来,确认一级科室和 children 二级科室的结构,再决定哪些字段映射成标题、哪些字段作为 key。转写稿里反复强调的套路是“先看接口返回,再写状态和渲染”,这里同样适用。
怎么做:在生命周期里调用 _getDepartmentList,请求成功后先 console.log('departmentList: ', departmentList) 验证数据,再处理禁用、展开、默认选中。
易错点:不要把接口数据直接原样丢给 Tree;没有打印确认结构就写映射,容易出现标题或 key 取错。
二、fieldNames:把 depname/depcode 映射给 Tree
为什么需要:接口字段叫 depname、depcode,而 Tree 默认读取 title、key 等字段,不做映射就显示不出正确文本,节点 key 也不稳定。
怎么想:笔记文字里提到 key、code、children 的映射关系,但以代码为准:title 映射 depname,key 映射 depcode,children 使用默认字段即可。
怎么做:
tsx
fieldNames={{
title: 'depname',
key: 'depcode'
}}易错点:属性名是 fieldNames,不是 fieldsName;key 要写 depcode,不要写成不存在的 code。
三、expandedKeys:默认展开一级科室
为什么需要:科室树默认收起时,用户看不到二级科室,体验差;通常希望一级科室默认展开。
怎么想:expandedKeys 接收的是“要展开节点的 key 数组”。一级科室的 key 是 depcode,所以把所有一级科室的 depcode 收集成数组。
怎么做:
tsx
let expandedKeys = departmentList.map(item => item.depcode);
setExpandedKeys(expandedKeys);易错点:expandedKeys 必须是数组;不要传单个字符串。设置前要确认 departmentList 已经请求成功。
四、disabled:禁用一级科室节点
为什么需要:一级科室只是分组,不应被选中作为实际科室;禁用后用户只能选二级科室。
怎么想:Tree 节点支持 disabled: true。遍历一级科室对象,给每个一级对象加上 disabled,而不是给二级科室加。
怎么做:
tsx
departmentList = departmentList.map(item => {
item.disabled = true;
return item;
});易错点:map 后要返回对象并重新赋值给 departmentList;如果只写 item.disabled = true 不返回,会得到 undefined 数组。
五、selectedKeys:设置默认选中科室
为什么需要:进入页面时右侧通常要显示一个默认科室,不能让用户先猜着点。
怎么想:取第一个一级科室下的第一个二级科室,拿到它的 depname 和 depcode,分别设置名称和选中 key。
怎么做:
tsx
let depname = (departmentList[0].children as IDepartmentList)[0].depname;
let depcode = (departmentList[0].children as IDepartmentList)[0].depcode;
setDepname(depname);
setDepcode(depcode);tsx
selectedKeys={[depcode as string]}易错点:selectedKeys 是数组,不是字符串;children 可能为空,真实项目中要判空,不能假设一定有第一个二级科室。
六、onSelect:点击节点切换科室
为什么需要:用户点击不同二级科室时,页面要同步更新当前科室名称和编码。
怎么想:onSelect 回调里能拿到 info.node,从节点上重新取 depcode、depname,再更新状态。
怎么做:
tsx
onSelect={(selectedKeys: any, info: any) => {
setDepcode(info.node.depcode);
setDepname(info.node.depname);
}}易错点:不要从 selectedKeys 猜名称;名称在 info.node 上。类型不要长期写 any,可后续补 DataNode 等类型。
七、状态与生命周期:请求成功后再 set
为什么需要:科室列表、展开项、选中科室都是会变化的数据,必须用 state 驱动视图。
怎么想:先声明 departmentList、expandedKeys、depname、depcode,请求成功后统一设置;转写稿中“生命周期钩子里调用、打印 data 验证、类型可能 undefined 时做断言”的调试思路可迁移到这里。
怎么做:在 useEffect 中判断 hoscode 存在再请求;请求后依次处理禁用、展开、列表、默认选中。
易错点:笔记中依赖数组写 [],若 hoscode 后续变化不会重新请求;更稳妥是依赖 hoscode。类型断言 as string 只是告诉 TS“我确定有值”,不是运行时兜底。
完整代码
tsx
let [departmentList, setDepartmentList] = useState<IDepartmentList>([]);
let [expandedKeys, setExpandedKeys] = useState<string[]>([]);// 一级科室depcode组成的数组
let [depname, setDepname] = useState<string>();
let [depcode, setDepcode] = useState<string>();
const _getDepartmentList = async ()=>{
let departmentList = await getDepartmentList(hoscode as string);
console.log('departmentList: ', departmentList);
// 禁用一级科室树节点,就是给所有的一级科室对象,添加一个disabled:true
departmentList = departmentList.map(item=>{
item.disabled = true;
return item;
})
// 展开所有的一级科室
// 1. 获取所有一级科室 depcode组成的数组
let expandedKeys = departmentList.map(item=>item.depcode)
console.log('expandedKeys', expandedKeys);
setExpandedKeys(expandedKeys);
setDepartmentList(departmentList);// 设置科室列表状态数据
// 处理默认选中科室
let depname = (departmentList[0].children as IDepartmentList)[0].depname;
let depcode = (departmentList[0].children as IDepartmentList)[0].depcode;
// console.log(depname);
// console.log(depcode);
// 设置科室名和科室code状态
setDepname(depname);
setDepcode(depcode);
}
useEffect(()=>{
hoscode && _getDepartmentList();
}, [])
// tree组件
<Tree
// onSelect={onSelect}
// onCheck={onCheck}
treeData={departmentList as []}
fieldNames={{
title:'depname',
key:'depcode'
}}
expandedKeys={expandedKeys}
selectedKeys={[depcode as string]}
onSelect={(selectedKeys:any,info:any)=>{
// console.log('selectedKeys: ', selectedKeys);
// console.log('info:', info);
// 重新设置depcode和depname
setDepcode(info.node.depcode);
setDepname(info.node.depname);
}}
/>本节要解决的问题
本节笔记只有一句核心描述:排班日期分页数据依赖于 depcode,所以当 depcode 发生改变时,要重新获取该科室的排班日期列表。 这句话看起来短,但它决定了这一块数据的请求时机和状态组织方式。排班日期分页不是“页面一进来请求一次就结束”的数据,而是“跟着科室走”的数据:当前选的是哪个科室,就查哪个科室的排班日期;科室变了,原来的分页数据就不再成立,必须重新查。
为什么需要把 depcode 作为依赖
排班日期列表通常不是孤立的。它描述的是某个科室在某段时间内的排班日期分页信息,因此 depcode 是查询条件,不是普通展示字段。如果把排班日期列表当成页面初始化数据,只在组件挂载时请求一次,那么用户切换科室后,页面仍然显示上一个科室的排班日期,数据与当前科室不一致。更隐蔽的问题是分页:用户可能已经在第 3 页,切换科室后如果还保留原来的页码,就会出现“新科室没有第 3 页”或“页码与结果不匹配”的情况。所以 depcode 变化不仅是“再发一次请求”,还意味着旧的分页上下文要失效。
怎么想:把 depcode 当作查询条件,而不是页面常量
设计时先分清两类状态:一类是查询条件,比如 depcode、页码、每页条数;另一类是查询结果,比如排班日期列表、总数、加载状态。depcode 属于查询条件,它一变,结果就应重新计算。由此可以推导出三个动作:第一,监听 depcode 的变化;第二,变化后重置分页条件,避免沿用旧页码;第三,用新的 depcode 和分页参数发起请求。这里的关键不是“在哪里写请求”,而是“谁决定请求”。本节笔记已经明确:决定因素是 depcode,所以请求逻辑应围绕 depcode 组织,而不是围绕某个按钮或某个生命周期写死。
怎么做:按触发链路落地
实现时可以按以下链路检查。首先,在状态中保留当前 depcode,并确保它来自当前选中的科室,而不是从旧数据里临时拼出来。其次,在 depcode 变化时执行重新获取逻辑:先处理分页重置,再调用排班日期分页查询。第三,请求函数应显式接收 depcode 与分页参数,避免函数内部读取到过期值。第四,请求返回后只更新当前仍有效的查询结果;如果用户快速切换科室,要防止旧请求的响应覆盖新科室数据。第五,加载态和空态要跟着这次查询走,不能把上一个科室的“加载中”或“无数据”直接复用。以上是本节笔记能够确定的推导;笔记没有给出具体接口名、字段名和代码,因此不能补造接口细节。
易错点
第一,只在页面初始化时请求一次,切换科室不重新请求,导致列表与当前科室不一致。第二,depcode 没变也重复请求,造成无意义请求。第三,depcode 改变后没有重置页码,仍带着旧页码查询新科室。第四,异步竞态:先发的旧科室请求后返回,覆盖了新科室结果。第五,depcode 为空或未定义时仍然发请求,得到无意义结果。第六,把 depcode 只放在请求函数内部读取,依赖收集不完整,变化后不触发。第七,分页参数与筛选条件混在一起更新,导致一次变化触发多次请求。排查时可以先打印当前 depcode、页码和请求参数,确认“变化前是什么、变化后是什么、请求发了几次、返回顺序是否颠倒”。
与转写稿的边界说明
需要说明:提供的转写稿主要讲的是医院设置的新增、删除、批量删除、编辑路由和根据 ID 获取医院设置并渲染表单,与本条笔记“排班日期分页数据获取”不是同一主题,转写稿中也没有出现 depcode 相关推导。因此不能把医院设置里的 addHospital、删除接口、批量删除等代码硬套到本节。本节笔记没有附带图片和代码,故教程中不补造图片与代码,只保留笔记能够支撑的依赖关系和触发时机。后续若补充排班日期分页接口定义、请求参数和返回结构,再按“为什么需要 → 怎么想 → 怎么做 → 易错点”继续展开即可。
先看接口:类型定义决定封装方式
本节虽名为“医生列表渲染”,实际落在医院设置列表的增删改与编辑回显上。做任何列表操作前,先查接口:请求方式、URL、参数、响应结果。为什么需要?因为只有看清接口,才知道要不要定义类型、API 函数怎么传参。怎么想?先判断响应结果和请求参数是否复杂:如果返回值是 null,就不定义响应类型;如果请求体是多个字段组成的对象,就定义请求参数类型。怎么做?在 model 中定义类似 IHospitalSetData 的请求体类型,字段按接口文档保留,删掉不需要的字段。易错点:从接口文档复制参数对象后,必须删掉多余字段和最后一个多余逗号;转写中测试“创建失败”就是因为对象末尾多了逗号,删掉后立即成功。
ts
// 请求体参数类型按接口保留所需字段
export interface IHospitalSetData {
contactsName: string
contactsPhone: string
houseCode: string
houseName: string
}封装新增接口与表单取值
为什么需要?新增医院时要提交表单数据,不能手写散乱参数。怎么想?API 层只负责发请求,页面层负责取表单值和提示跳转。怎么做?封装 addHospitalSet(data: IHospitalSetData),内部用 request.post,第二个参数直接放 data。页面里用 Form.useForm() 创建表单实例,绑定到表单,再用 form.getFieldsValue() 一次拿到全部字段;注意是带 s 的 getFieldsValue,不是单个字段的 getFieldValue。提交时加 try/catch,成功 message.success('添加成功') 并 navigate 到医院设置列表;失败提示“添加失败”,不跳转。易错点:按钮要设 htmlType="submit";成功后才跳转;失败时留在当前页。
ts
const data = form.getFieldsValue()
await addHospitalSet(data)
message.success('添加成功')
navigate('/hospital/list')单个删除与列表刷新
为什么需要?列表每行都要能删当前记录。怎么想?删除按钮在 columns 的自定义渲染里,点击时要把当前行 id 传出去,先弹确认框,再调接口。怎么做?封装 deleteHospitalSetById(id: string),用 request.delete,URL 用模板字符串拼 id。按钮绑定 onClick={() => deleteById(record.id)}。在 deleteById 中 Modal.confirm,点确定后 await deleteHospitalSetById(id),成功 message.success('删除成功'),再重新调用获取列表函数刷新。易错点:确认框的确定回调里再发请求;删除后必须刷新列表;系统数据可能不允许删除,失败要提示,不要误以为接口坏了。
ts
const deleteById = (id: string) => {
Modal.confirm({
title: '确定删除当前记录吗?',
onOk: async () => {
await deleteHospitalSetById(id)
message.success('删除成功')
getList()
}
})
}批量删除与选中状态
为什么需要?表格需要多选后批量删除,按钮还要根据是否选中决定禁用/激活。怎么想?Table 的 rowSelection 负责多选,selectedRowKeys 存选中的 ID 数组,onChange 在选中状态变化时触发。怎么做?配置 rowSelection={{ selectedRowKeys, onChange }},用 useState<(string | number)[]>([]) 保存选中 ID;onChange 中 setSelectedRowKeys(keys)。批量删除按钮的 disabled 根据 selectedRowKeys.length === 0 判断。封装 removeBatch(ids: (string | number)[]),注意转写中该接口是 DELETE 请求,但 ID 数组要放在请求体里,所以配置项用 data: ids。删除成功后提示、刷新列表,并把选中状态清空。易错点:删除后不清空 selectedRowKeys,批量删除按钮会一直激活;DELETE 请求体不是 params,要按接口文档放 data。
ts
const [selectedRowKeys, setSelectedRowKeys] = useState<(string | number)[]>([])
<Table
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys)
}}
/>编辑路由与参数传递
为什么需要?点编辑要进入编辑页,并把当前记录 ID 带过去。怎么想?添加和编辑可共用同一个页面组件,因此不必新建静态页面,只补路由配置。怎么做?在路由中配置 /hospital/edit/:id,:id 是路径占位符,表示 params 参数;编辑按钮点击时 navigate('/hospital/edit/' + record.id)。易错点:路径参数要在路由里写占位符;跳转地址最好复制路由配置,避免手写错;编辑路由通常也要隐藏侧边栏,和添加页保持一致。
编辑回显与添加/编辑共用页面
为什么需要?进入编辑页后要把当前 ID 的医院数据回显到表单。怎么想?进页面就发请求,属于生命周期调用,不是按钮交互调用。怎么做?用 useParams() 取路由中的 id,在 useEffect 中调 getHospitalSetById(id),拿到 data 后用 form.setFieldsValue(data) 批量回显。转写中强调:id 可能被推断为 string | undefined,可断言其一定存在。共用页面时,添加操作没有 ID,不应发获取详情请求;因此要判断“ID 存在说明是编辑,发请求;ID 不存在说明是添加,不发请求”。易错点:getFieldsValue 是取值,setFieldsValue 是回显;不要混淆;添加页误发详情请求会带 undefined,要加存在性判断。
说明
本节笔记主题是“项目完成打包上线部署”。转写稿主要记录医院设置相关功能的开发过程,与本节部署主题不一致,因此下面只依据本节笔记扩写部署流程,不把无关功能写入本节。
先构建最终目录
为什么需要:开发环境依赖开发服务器、热更新和代理配置,不能直接把这些文件丢到线上。上线前必须生成浏览器可直接访问的静态资源。
怎么想:先执行构建命令,得到最终产物目录,再把该目录交给静态服务器托管。笔记中构建命令是 npm run build,产物目录是 build。
怎么做:
bash
npm run build执行后检查项目根目录是否生成 build 目录,里面应包含 index.html、JS、CSS、图片等静态资源。
易错点:不要直接部署 src;不要漏掉构建;部署时要以实际产物目录为准。若服务文件与 build 不同级,静态目录不能照抄 ./,应改成 ./build 或把服务文件放进 build 内再运行。
部署需要解决的三类问题
为什么需要:构建后的项目不是双击 index.html 就能完整运行。笔记明确列出三个问题:刷新子路由出现 404、静态资源目录要配置、接口请求要跨域。
怎么想:用 Express 启动一个 Node 服务:先处理前端路由回退,再托管静态资源,最后把 /prod-api 开头的请求代理到目标服务器。
怎么做:安装依赖并编写服务文件:
bash
npm i express connect-history-api-fallback http-proxy-middleware易错点:三个中间件的顺序和路径要匹配;代理前缀必须与前端请求前缀一致;不要把后端地址写错。
用 history 解决刷新 404
为什么需要:前端使用 history 路由时,刷新 /hospital 这类地址,浏览器会向服务器请求该路径。服务器若没有对应文件,就返回 404。
怎么想:所有找不到的页面请求都回退到 index.html,交给前端路由处理。
怎么做:笔记代码如下:
js
const express = require('express');
const history = require('connect-history-api-fallback');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// 0. 所有404的相应,使用 index.html替换
app.use(history());易错点:history() 要放在静态资源托管之前,保持笔记顺序;它主要处理页面请求,不要误以为它能替代接口代理。
配置静态资源目录
为什么需要:浏览器要能访问构建后的 index.html、JS、CSS 等文件,必须由 Express 指定静态目录。
怎么想:把构建产物所在目录交给 express.static。笔记写的是当前目录 ./,说明部署时服务文件与静态资源处于同一可访问根目录。
怎么做:
js
// 1. 配置当前目录为静态资源目录
app.use(express.static('./'));易错点:如果 server.js 在项目根目录、产物在 build,应改为 express.static('./build');否则会访问不到构建文件。
配置代理服务器完成跨域
为什么需要:生产环境前端由 Express 提供,接口在另一台服务器。浏览器直接请求目标域名会跨域。通过同源 Express 转发,可规避浏览器跨域限制。
怎么想:前端统一请求 /prod-api,Express 收到后转发到 http://syt-api.atguigu.cn,并用 pathRewrite 去掉 /prod-api 前缀。
怎么做:
js
// 2. 进行跨域配置
app.use('/prod-api', createProxyMiddleware(
{
target: "http://syt-api.atguigu.cn", // 目标服务器地址
changeOrigin: true, // 允许跨域
pathRewrite: {
// 路径重写
"^/prod-api": "",
},
}
));
app.listen(5000, () => {
console.log('server run at 5000');
})易错点:target 不要带错协议;changeOrigin: true 要保留;pathRewrite 的键要与代理前缀一致;前端请求地址也要以 /prod-api 开头,否则代理不生效。
启动与验证
怎么做:在服务文件目录执行 node 服务文件名.js,浏览器访问 http://localhost:5000。重点验证:首页能打开、刷新子路由不 404、静态资源能加载、接口请求走 /prod-api 并成功返回。
易错点:端口被占用要换端口;修改服务文件后要重启 Node;部署服务器要允许对应端口访问。
类组件复习:state、props、事件与生命周期
为什么需要:类组件是 React 早期的主要写法,复习它有助于理解 state、props、生命周期和事件绑定这些基础模型。怎么想:state 是组件内部可变数据,props 是外部传入数据;事件回调里的 this 容易丢,是因为回调执行时不再由组件实例直接调用。怎么做:state 可用属性直接赋值或构造函数赋值;props 用 this.props 读取;事件绑定写成 <button onClick={事件处理函数}></button>。this 问题常用三种方式处理:包裹箭头函数、bind、直接定义成箭头函数。事件对象是 React 合成事件,已做兼容处理,e.nativeEvent 可拿原生事件;React 默认把事件对象传给第一个形参。
jsx
const click1 = (e)=>{
// e 作为第一个形参,就可以接收到事件对象
}
<button onClick={click1}>点击</button>生命周期要抓住“挂载后、更新后、卸载前”:
js
// componentDidMount 组件挂载后执行, 只会执行一次
// 发送ajax请求,开启定时器,订阅消息,绑定自定义事件
// componentDidUpdate 组件更新执行 render--》componentDidUpdate
// newprops setState forceUpdate ==> render
// 有机会执行多次
// componentWillUnmount 组件卸载前执行 只会执行一次
// 取消定时器、取消订阅、解绑自定义事件易错点:不要在 render 里发请求或改 state;卸载时要清理定时器、订阅和事件;普通函数事件回调要处理 this,否则拿不到组件实例。
JSX 插值表达式:条件渲染只能写表达式
为什么需要:JSX 里要嵌入动态内容,但 JSX 不是普通语句块。怎么想:花括号里放“表达式”,不能直接写 if 语句。怎么做:条件渲染用三元、逻辑与、函数调用。
js
// 条件渲染:在jsx模板中,只能使用三元表达式、逻辑运算
return (
<>
<div>三元表达式:{条件?<div>111</div>:<div>222</div>}</div>
<div>逻辑运算:{条件 && <div>222</div>}</div>
<div>函数调用:{fn()}</div>
</>
)易错点:三元表达式闭合标签要写完整;逻辑与左边为假值时会直接返回假值;不要在 JSX 花括号里写 if。
函数组件与 Hooks:state、props、事件、useEffect
为什么需要:函数组件更简洁,Hooks 让函数组件也能拥有状态和生命周期能力。怎么想:useState 管状态,props 用形参接收,事件没有类组件 this 问题。怎么做:props 可逐个传,也可用扩展运算符一起传。
js
let [user, setUser] = useState({
count:1,
msg:'atguigu'
})
<Child {...user}/>生命周期用 useEffect 模拟:
js
useEffect(()=>{// didMount 组件挂载调用一次
return ()=>{ // willUnmount 调用一次
}
},[])
useEffect(()=>{
})
useEffect(()=>{
},[count])易错点:依赖数组不同,执行时机不同;清理函数写在 return 里;函数组件事件回调中的 this 是 undefined,不要照搬类组件写法。
Redux 与路由复习
为什么需要:中大型页面要把共享状态和页面切换管清楚。怎么想:Redux 用 Provider 注入 store,组件用 useSelector 取数、useDispatch 派发;路由用 BrowserRouter、Link/NavLink、useNavigate、useRoutes 组织。怎么做:
js
<Provider store={}>
</Provider>
useSelector
useDispatch
useDispatch(actionCreator(payload))==> useDispatch(action对象)js
<BrowserRouter></BrowserRouter>
<NavLink >
<Link>
useNavigate 创建编程式导航
路由表:useRoutes(路由表数组) path element children
路由参数:
1. useParams() path
2. useSearchParams() query
3. useLocation() state
BrowserRouter NavLink Link Outlet from 'react-router'易错点:Provider 要包住使用 store 的组件;path 参数、query 参数、state 参数要用对应 Hook 取;路由表里子路由要配 Outlet。
ref、受控与非受控、Redux Toolkit
为什么需要:ref 用于直接拿 DOM 或组件实例;受控/非受控决定表单值由谁管理;Redux Toolkit 简化 Redux 写法。怎么做:根节点用 ReactDOM.createRoot 创建。
js
const root = ReactDOM.createRoot(dom元素)ref 可绑定 DOM 获取元素、模拟 componentDidUpdate、给类组件拿实例、给函数组件配合 React.forwardRef 让父组件拿子组件 DOM。受控组件由状态控制,value/checked 赋值后只读,靠 onChange 更新状态;非受控用 defaultValue/defaultChecked,通过 ref 取真实 DOM 的 .value。Redux Toolkit 先 createSlice,再 configureStore:
js
configureStore({
reducer:{
count:countSlice.reducer
}
})取数据用 store.getState(),改数据用 store.dispatch(actionCreator(payload)),等价于派发 action 对象。易错点:受控组件漏写 onChange 会无法输入;函数组件不能直接绑 ref;切片 name 必填。
项目串联:添加、删除、批量删除、编辑的调试思路
转写稿里的医院项目把复习点串起来了。添加医院时先看接口:请求方式、URL、参数、响应结果;响应为 null 时不必定义响应类型,只定义请求体类型。调试时最后多写一个逗号会导致创建失败,删掉逗号才成功。删除按 ID 删除时,接口是 delete,参数简单、返回 null,可不定义类型;点击删除要传当前行 id,弹确认框,请求成功后提示并刷新列表。批量删除要看 Table 的 rowSelection:用 selectedRowKeys 和 onChange 配合,把选中 ID 存进 state;按钮是否禁用由选中数组长度决定;接口要求把 ID 数组放在请求体,所以配置里用 data 传,成功后要清空选中状态并刷新。编辑与添加常共用页面:路由配 :id 占位符,编辑按钮 navigate 带上 ID;进页面用 useParams 取 ID,在 useEffect 中判断“ID 存在才请求”,否则就是添加操作;请求回来用 form.setFieldsValue(data) 批量回填。易错点:添加时也会进同一页面,不判断 ID 会多发请求;useParams 的 ID 可能为 undefined;批量删除后不清空选中,按钮会一直激活。
先看接口,再决定封装什么

做“添加医院”时,先别急着写页面。为什么需要先看接口?因为要确认请求方式、URL、请求参数和响应结果,才能决定要不要定义类型、API 函数怎么传参。怎么想:打开新增医院接口,发现是 POST,请求体里实际只需要 5 个字段;接口文档里多出来的 apiUrl、signKey 等要删掉。怎么做:在 hospital 的 model 里定义请求体类型,再封装 API。易错点:转写里测试接口时,对象最后一个字段后多了一个逗号,结果“创建失败”;删掉逗号、其他不动就成功了。所以接口测试和代码里都不能多逗号。
ts
export interface IHospitalSetData {
contactName: string
contactPhone: string
houseCode: string
houseName: string
status: number
}
export const addHospitalSet = (data: IHospitalSetData) => {
return request.post('接口地址', data)
}这里响应结果不需要额外定义类型;请求参数类型明确,添加接口就够用了。
添加医院:表单取值与提交
为什么用 Form.useForm()?因为要统一拿到表单值并做校验。怎么想:创建 form,绑定到表单,提交时用 getFieldsValue() 一次性拿对象;getFieldValue('字段') 是单个取值。怎么做:在保存回调里 try/catch 调 API,成功提示并跳转列表,失败只提示、不跳转。易错点:getFieldsValue 带 s,不要写成单个取值;跳转地址最好复制路由配置,避免手写出错。
ts
const [form] = Form.useForm()
const onFinish = async () => {
const data = form.getFieldsValue()
try {
await addHospitalSet(data)
message.success('添加成功')
navigate('/hospital-set')
} catch (e) {
message.error('添加失败')
}
}单个删除:参数简单时可不定义类型
为什么先看删除接口?删除也有“按 ID 删除”和“批量删除”两个接口。怎么想:点击当前行删除,用按 ID 删除;请求方式 DELETE,参数只有 id,响应是 null。怎么做:不需要定义响应类型,也不需要复杂参数类型,直接封装 API。易错点:删除前要 Modal.confirm;系统数据不能删,测试时用自己新增的数据。
ts
export const deleteHospitalSetById = (id: string) => {
return request.delete(`接口地址/${id}`)
}
const deleteById = async (id: string) => {
await deleteHospitalSetById(id)
message.success('删除成功')
getList()
}批量删除:先拿选中 ID,再控制按钮状态
为什么需要 rowSelection?因为表格要出现复选框,并把选中行 ID 组成数组。怎么想:selectedRowKeys 与 onChange 配合;onChange 的参数就是当前选中 ID 数组。怎么做:用状态保存 selectedRowKeys,按钮根据长度决定禁用/激活。易错点:批量删除成功后必须清空 selectedRowKeys,否则按钮仍按旧长度保持激活。
ts
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([])
<Table
rowSelection={{
selectedRowKeys,
onChange: setSelectedRowKeys
}}
/>
<Button disabled={selectedRowKeys.length === 0}>批量删除</Button>批量删除接口:DELETE 也可能带请求体
为什么批量删除要单独封装?因为它传的是 ID 数组,不是单个 ID。怎么想:接口要求 DELETE,并把数组放在请求体里;request.delete 的请求体通常放在配置对象的 data 中。怎么做:封装 removeBatch,成功后提示、刷新列表、清空选中状态。易错点:不要误把数组放到 URL 上;清空状态这一步不能漏。
ts
export const removeBatch = (ids: React.Key[]) => {
return request.delete('接口地址', { data: ids })
}
const onBatchDelete = async () => {
await removeBatch(selectedRowKeys)
message.success('批量删除成功')
setSelectedRowKeys([])
getList()
}编辑:复用添加页面,配置动态路由
为什么编辑不新建页面?因为添加和编辑表单结构一样,可以复用同一个路由组件。怎么想:编辑要携带当前行 ID,且是路径参数,所以路由要配占位符。怎么做:配置 /hospital-set/edit/:id,编辑按钮点击后 navigate 到带 ID 的地址。易错点:路径参数要拼在路径里,不要当查询参数写。
ts
{ path: '/hospital-set/edit/:id', element: <AddOrUpdate /> }
onClick={() => navigate(`/hospital-set/edit/${record.id}`)}编辑回显:有 ID 才请求,再批量设置表单
为什么在生命周期里请求?因为进入编辑路由组件就要拿当前 ID 的数据,不是用户再点按钮才拿。怎么想:useParams 取路径 ID,useEffect 中请求,form.setFieldsValue(data) 批量回显。怎么做:添加和编辑共用页面时,用“有没有 ID”区分操作。易错点:转写里调试发现,点添加也进入同一页面并发了请求,只是 ID 为 undefined;所以要加 if (!id) return。
ts
const { id } = useParams()
useEffect(() => {
if (!id) return
const getData = async () => {
const data = await getHospitalSetById(id)
form.setFieldsValue(data)
}
getData()
}, [id])这样,编辑时回显原数据;添加时没有 ID,就不发获取详情请求。
17. 未来三年:医院设置增删改与批量操作
本节围绕医院设置模块,把“添加、单条删除、批量删除、编辑回显”串成一条完整开发链路。转写中的同音字按上下文纠正为:interface、POST、DELETE、path 参数、try/catch。整体套路是:先看接口的请求方式、URL、参数、响应结果,再判断要不要定义类型,最后封装 API 函数并在交互或生命周期里调用。
添加医院:先看接口,再定类型与 API
为什么需要: 添加功能不能先写页面逻辑,必须先确认接口要什么。转写里点开“新增医院设置”接口后,发现是 POST 请求,界面实际只需要 5 个字段,因此要把接口示例里多余的 apiUrl、时间、houseCode/houseName、ID、签名等不需要项删掉。
怎么想: 返回值是 null,所以不需要定义响应结果类型;但请求体有多个字段,需要定义请求参数类型。类型定义完,再封装 addHospitalSet。
怎么做: 在 model 里定义请求体类型,在 API 文件里写函数,把 data 作为 POST 请求体传入。示意如下:
ts
// 字段按接口只保留需要的 5 个
export interface IHospitalSetData {
// contactName、contactPhone 等以实际接口为准
}
export const addHospitalSet = (data: IHospitalSetData) =>
request.post(url, data)易错点: 转写中专门调试了“对象最后一个逗号”:多留一个逗号时创建失败,删掉后其他不动就成功。所以测试接口和写请求体时,要检查 JSON/对象末尾逗号。
添加医院:表单取值、提交与跳转
为什么需要: 接口封装好后,要在一个具体交互里调用;添加页的入口是“保存”按钮。
怎么想: 先用 useForm 创建表单实例并绑定,提交时用 getFieldsValue() 一次性拿到表单对象,而不是逐个 getFieldValue。发请求可能失败,所以用 try/catch。
怎么做: 点击保存后取表单值,调用 addHospitalSet(data);成功后 message.success 提示并 navigate 回医院设置列表;失败则提示“添加失败”,不跳转。
ts
const [form] = useForm()
const data = form.getFieldsValue()
try {
await addHospitalSet(data)
message.success('添加成功')
navigate('/医院设置列表地址')
} catch (e) {
message.error('添加失败')
}易错点: getFieldsValue() 带 s,拿的是整个表单对象;getFieldValue 是单个字段。转写中先打印 data 确认 5 个值都有,再发请求,这是很实用的调试步骤。
单条删除:按 ID 删除并刷新列表
为什么需要: 列表每行都有删除按钮,点击后要删除当前记录,不能跳新页面。
怎么想: 删除接口是 DELETE,参数只有 ID,返回值是 null,所以不需要定义参数类型和响应类型,直接封装 deleteHospitalSetById(id) 即可。
怎么做: 在列配置的删除按钮上绑定点击事件,把当前行 ID 传进去;弹确认框,确认后发请求,成功提示并重新调用列表查询函数刷新。
易错点: 转写中系统前 10 条数据不能删,点击会提示“系统数据不能操作”,这是业务限制,不是接口写错。删除成功后要刷新列表;取消确认框时不应发请求。
ts
export const deleteHospitalSetById = (id: string | number) =>
request.delete(`/接口地址/${id}`)批量删除:rowSelection、选中 ID 状态与清空
为什么需要: 表格要支持勾选多行后批量删除,需要拿到所有选中行的 ID 数组。
怎么想: 用 Table 的 rowSelection,配置 selectedRowKeys 和 onChange。onChange 的第一个参数就是选中 ID 数组,把它放进状态 selectedKeys,再用长度控制“批量删除”按钮是否禁用。
怎么做: 批量删除接口要求 DELETE 请求体携带 ID 数组,因此封装 removeBatch(ids),请求配置里用 { data: ids }。点击批量删除时弹确认框,成功后提示、刷新列表,并执行 setSelectedKeys([]) 清空选中状态。
ts
export const removeBatch = (ids: React.Key[]) =>
request.delete(url, { data: ids })易错点: 转写中强调:批量删除成功后必须清空 selectedKeys,否则按钮会一直处于激活状态,因为按钮状态是根据选中数组长度判断的。另外 DELETE 一般少用请求体,但本接口明确要求放在 body 里,要按接口说明写 data。
编辑回显:path 参数、useParams 与表单回填
为什么需要: 点击编辑要进入编辑页,并显示当前医院原来的数据。添加和编辑可共用同一个 ADD/UPDATE 页面,所以只需配置编辑路由,不必重新建页面。
怎么想: 编辑路由要携带 ID,属于 path 参数,路由配置需要占位符。进入组件后通过 useParams() 取 ID;在生命周期里请求详情。若 ID 存在说明是编辑,发请求;ID 不存在说明是添加,不发请求。
怎么做: 封装 getHospitalSetById(id),用 useEffect 调用;拿到数据后用 form.setFieldsValue(data) 批量回填表单。
ts
const { id } = useParams()
useEffect(() => {
if (id) {
// 请求详情并 setFieldsValue
}
}, [])易错点: 转写中调试发现:编辑页和添加页共用组件时,如果无条件发详情请求,添加页的 ID 是 undefined,也会发无效请求。因此必须先判断 id 是否存在。另一个易错点是路由参数从地址栏来,类型可能被推断为 string | undefined,必要时按业务断言,但断言前要确保路由一定带 ID。
说明:本节笔记未附图片和独立代码块,以下代码片段按转写稿中的函数名、接口信息整理为“代码要点”,不补造未出现的字段。
先看接口,再决定要不要定义类型
为什么需要:添加医院不是先写页面,而是先确认接口的请求方式、URL、参数和响应结果。只有接口清楚了,才能判断要不要定义请求参数类型、响应结果类型。
怎么想:新增医院大概率是 POST 请求。点开接口后看请求体,页面上实际只提交 5 个数据,而接口示例里给了更多字段,因此要把不需要的字段删掉。转写稿里特别提醒:测试接口时对象最后多了一个逗号,导致“创建失败”;删掉逗号后,其他都没动就成功了。这说明接口调试要先排除 JSON 格式问题。
怎么做:在 model 中定义请求体参数类型,例如 IHospitalSetData,只保留接口需要的字段;响应结果不需要额外定义。然后在 API 文件中封装 addHospitalSet(data),发送 POST 请求,把 data 作为请求体传入。
易错点:不要照抄接口示例里的全部字段;删除字段后要检查最后一个属性后面的逗号;先测接口成功,再写前端调用。
添加医院:表单取值、发请求、提示并跳转
为什么需要:接口封装好后,要在“保存”按钮里拿到表单数据并提交,否则 API 函数没有调用入口。
怎么想:用 useForm 创建表单实例,绑定到表单;提交时用 getFieldsValue() 一次性拿到对象。转写稿中区分了 getFieldValue(name) 和 getFieldsValue():前者按字段取值,后者拿整个表单对象。添加成功后要提示并跳转到医院设置列表页。
怎么做:保存回调中先 try/catch,调用 addHospitalSet(data);成功后 message.success('添加成功'),再 navigate 到列表页;失败则提示“添加失败”,不跳转。请求期间可加 loading 状态。
易错点:getFieldsValue 带 s;跳转地址最好复制路由配置,避免手写错;失败时不要跳转。
单条删除:接口简单,也要确认参数和返回值
为什么需要:列表中的“删除”只删当前记录,必须拿到当前行 ID。
怎么想:删除接口有两个,一个是按 ID 列表批量删除,一个是 remove 按单个 ID 删除。当前按钮应使用单条删除接口。该接口返回 data 为 null,参数只有一个 ID,因此不需要定义响应类型和参数类型,直接封装 API 函数即可。
怎么做:封装 deleteHospitalSetById(id),发送 DELETE 请求,URL 中用模板字符串拼接 ID。表格自定义列里给删除按钮绑定 onClick,传入 row.id;点击后弹 Modal.confirm,确认后调用 API,成功提示并重新获取列表。
易错点:删除前必须确认,避免误操作;不要误用批量删除接口;确认框里要写清“删除当前记录”。
批量删除:rowSelection 与选中 ID 状态
为什么需要:批量删除要先让表格出现复选框,并实时拿到选中的 ID 数组。
怎么想:查看 Table 的 rowSelection,它需要 selectedRowKeys 和 onChange 配合。onChange 的第一个参数就是选中 ID 组成的数组,类型为字符串或数字的联合类型数组。
怎么做:给 Table 配置 rowSelection,用 selectedKeys 状态保存选中 ID;onChange 中 setSelectedKeys(keys)。批量删除按钮用 disabled={selectedKeys.length === 0} 控制禁用/激活。
易错点:selectedRowKeys 和 onChange 要成对使用;状态不更新,按钮激活状态就不会变。
批量删除请求:DELETE 也可能带请求体
为什么需要:批量删除接口要求传 ID 数组,且转写稿明确该参数在请求体 body 中,不是普通查询参数。
怎么想:虽然 DELETE 请求较少带请求体,但要以接口文档为准。返回值是 null,不需要定义响应类型,直接封装 removeBatch(ids)。
怎么做:removeBatch(ids) 中发送 DELETE 请求,把 ids 放到配置的 data 字段。点击批量删除后弹确认框,调用 removeBatch(selectedKeys);成功后提示、刷新列表,并执行 setSelectedKeys([]) 清空选中状态。
易错点:删除成功后必须清空选中 ID,否则批量删除按钮仍可能保持激活;不要想当然把数组拼到 URL 上。
编辑路由与按钮跳转
为什么需要:点击编辑要进入编辑页,并携带当前医院 ID。
怎么想:添加和编辑共用同一个页面组件,因此不用再创建新组件和静态结构,只配置路由即可。编辑路由需要路径参数占位符,例如 /hospital/edit/:id,并隐藏侧边栏、复用添加页组件。
怎么做:在路由中复制添加路由并改为编辑路由;表格编辑按钮的 onClick 中调用 navigate,路径拼接 row.id。
易错点:ID 是路径参数,路由必须配置 :id;跳转地址不要手写错。
编辑页回显与共用页面的小 bug
为什么需要:进入编辑页后,要把当前 ID 的医院数据回显到表单。
怎么想:根据 ID 获取医院设置接口,返回的数据结构之前已定义过,可直接复用类型并封装 getHospitalSetById(id)。进入路由组件属于生命周期时机,不是用户点击,所以用 useEffect;路径参数用 useParams 获取。
怎么做:在 useEffect 中根据 ID 调接口,拿到 data 后调用 form.setFieldsValue(data) 批量回显。转写稿调试发现:添加页和编辑页共用组件时,添加也会发获取数据请求,此时 ID 为 undefined。因此要判断:ID 存在才是编辑,发请求;ID 不存在就是添加,不发请求。
易错点:useParams 拿到的 ID 可能为空,调用前要判断;setFieldsValue 传的是对象,不是单个字段值。
课堂画面






代码示例
一、渲染科室列表(Tree 组件 + 状态数据)
tsx
import { useEffect, useState } from 'react';
import { Tree } from 'antd';
import { getDepartmentList } from '@/api/hospital'; // 科室列表接口
interface IDepartment {
depcode: string;
depname: string;
disabled?: boolean;
children?: IDepartment[];
}
type IDepartmentList = IDepartment[];
const DepartmentList = ({ hoscode }: { hoscode: string }) => {
const [departmentList, setDepartmentList] = useState<IDepartmentList>([]);
const [expandedKeys, setExpandedKeys] = useState<string[]>([]); // 一级科室 depcode 数组
const [depname, setDepname] = useState<string>();
const [depcode, setDepcode] = useState<string>();
const _getDepartmentList = async () => {
let departmentList: IDepartmentList = await getDepartmentList(hoscode as string);
console.log('departmentList: ', departmentList);
// 禁用一级科室树节点:给所有一级科室对象添加 disabled: true
departmentList = departmentList.map((item) => {
item.disabled = true;
return item;
});
// 展开所有一级科室:取所有一级科室 depcode 组成的数组
const expandedKeys = departmentList.map((item) => item.depcode);
console.log('expandedKeys', expandedKeys);
setExpandedKeys(expandedKeys);
setDepartmentList(departmentList);
// 处理默认选中科室:第一个一级科室下的第一个二级科室
const first = (departmentList[0].children as IDepartmentList)[0];
setDepname(first.depname);
setDepcode(first.depcode);
};
useEffect(() => {
hoscode && _getDepartmentList();
}, []);
return (
<Tree
treeData={departmentList as []}
fieldNames={{ title: 'depname', key: 'depcode' }}
expandedKeys={expandedKeys}
selectedKeys={[depcode as string]}
onSelect={(_selectedKeys: any, info: any) => {
// 点击切换树节点,重新设置 depcode 和 depname
setDepcode(info.node.depcode);
setDepname(info.node.depname);
}}
/>
);
};
export default DepartmentList;关键行与易错点
fieldNames={{ title: 'depname', key: 'depcode' }}:把后端字段映射成 Tree 需要的title/key(还有children),不配置树就渲染不出来。item.disabled = true只加在一级科室上,二级科室保持可点击;map里必须return item,漏掉返回值列表会变成undefined。expandedKeys必须是受控状态:先用departmentList.map取出所有一级depcode再setExpandedKeys,否则树默认是收起的。selectedKeys={[depcode as string]}是数组形式;onSelect里取值用info.node.depcode,不是selectedKeys(那是 key 数组)。useEffect依赖数组为空 +hoscode &&判断,避免hoscode还没拿到就发请求。
二、排班日期分页数据获取(依赖 depcode)
tsx
import { useEffect, useState } from 'react';
import { getScheduleList } from '@/api/hospital';
interface ScheduleItem {
workDate: string;
availableNumber: number;
}
const ScheduleList = ({ hoscode, depcode }: { hoscode: string; depcode?: string }) => {
const [scheduleList, setScheduleList] = useState<ScheduleItem[]>([]);
const [total, setTotal] = useState(0);
const [current, setCurrent] = useState(1);
const [pageSize] = useState(5);
const [loading, setLoading] = useState(false);
const _getScheduleList = async () => {
setLoading(true);
try {
const res = await getScheduleList({
page: current,
limit: pageSize,
hoscode,
depcode: depcode as string,
});
setScheduleList(res.bookingScheduleList);
setTotal(res.total);
} finally {
setLoading(false);
}
};
// depcode 发生变化 → 重新获取该科室的排班日期列表
useEffect(() => {
depcode && _getScheduleList();
}, [depcode, current]);
return (
<>
{/* 日期列表 + 分页渲染 */}
<ul>
{scheduleList.map((item) => (
<li key={item.workDate}>
{item.workDate} 剩余 {item.availableNumber}
</li>
))}
</ul>
<button disabled={current === 1} onClick={() => setCurrent(current - 1)}>
上一页
</button>
<span>
{current} / {Math.ceil(total / pageSize)}
</span>
<button
disabled={current >= Math.ceil(total / pageSize)}
onClick={() => setCurrent(current + 1)}
>
下一页
</button>
</>
);
};
export default ScheduleList;关键行与易错点
- 排班数据依赖
depcode:useEffect依赖数组必须写[depcode, current],只写[]时切换科室不会重新请求。 depcode && _getScheduleList():科室未选中时depcode是undefined,直接请求会带上空参数报错。- 分页参数(
page/limit)和depcode放在同一个useEffect里,切科室时current应重置为 1,否则可能停在不存在的页码上。 finally里关loading,避免请求失败后 loading 永远转圈。- 总页数用
Math.ceil(total / pageSize)计算,别用total / pageSize直接比。
三、打包上线部署(Express 静态资源 + history 回退 + 代理跨域)
js
// server.js —— npm run build 之后,把 build 目录作为站点根目录启动
const express = require('express');
const history = require('connect-history-api-fallback');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// 0. 所有 404 的响应,使用 index.html 替换(解决刷新子路由 404)
app.use(history());
// 1. 配置当前目录为静态资源目录
app.use(express.static('./'));
// 2. 进行跨域配置
app.use(
'/prod-api',
createProxyMiddleware({
target: 'http://syt-api.atguigu.cn', // 目标服务器地址
changeOrigin: true, // 允许跨域
pathRewrite: {
// 路径重写
'^/prod-api': '',
},
})
);
app.listen(5000, () => {
console.log('server run at 5000');
});关键行与易错点
npm run build生成build目录后,把server.js放进build里再启动,express.static('./')才能正确读到index.html和静态资源。app.use(history())必须放在静态资源中间件之前(按笔记顺序),否则刷新/hospital/setting这类前端路由会直接 404。pathRewrite把^/prod-api重写为空,因此前端请求 baseURL 必须写成/prod-api,两边不一致就会 404 或打到错误路径。changeOrigin: true不能省,否则服务端拿到的Host不匹配会被目标服务器拒绝。- 代理中间件挂在
/prod-api上,注意它要写在history()之后,否则接口请求可能被 index.html 拦截。
常见错误
- 新增医院接口测试失败/创建失败 → 直接复制接口文档示例,保留了
apiUrl、tenantId、signKey等多余字段,或对象末尾多逗号 → 只保留业务需要的 5 个字段,删除末尾逗号,并用IHospitalSetData约束请求体。 - 保存后提示成功但页面不跳转,或失败也跳转 →
navigate写在try/catch外层或catch中也执行 → 成功分支内先message.success再navigate;catch只提示失败,不跳转。 - 单个删除/批量删除后列表不刷新、按钮仍激活 → 删除成功后未重新拉列表,批量删除后未清空
selectedKeys→ 成功回调里重新获取列表;批量删除成功执行setSelectedKeys([]),禁用态由选中数量派生。 - 新增页一进入就请求医院详情,控制台出现
id undefined→ 添加/编辑共用组件,useEffect无条件调用getHospitalSetById(id)→useEffect内先判断id是否存在,无id直接return,有id才请求详情。 - Tree 科室默认展开/选中异常,切换科室右侧不更新 →
fieldNames映射错、selectedKeys未与depcode同步,或排班请求useEffect漏依赖depcode→ 固定title:'depname'、key:'depcode';selectedKeys={[depcode]};onSelect同步depcode/depname;排班请求以depcode为依赖并重置页码。 - 部署后刷新 404、接口跨域或静态资源 404 → 未启用 history fallback、静态目录配置不对、代理前缀或
pathRewrite不匹配 → 按history()→express.static()→/prod-api代理的顺序配置,pathRewrite与前端请求前缀保持一致。
练习
- 补全“新增医院保存”流程:从表单整体取值、调用新增 API、成功提示并跳转列表、失败只提示不跳转。写出关键代码顺序。
- 现有删除按钮点击后直接删除且列表不刷新。改为
Modal.confirm确认后删除,成功后提示并刷新列表,失败提示错误。 - 批量删除按钮在未选中时仍可点击,删除后仍保持激活。指出需要调整的状态、派生条件和删除成功后的清理动作。
- 编辑页与新增页共用组件,新增时仍请求详情并报
id undefined。给出useEffect的修正方案,并说明如何区分新增/编辑。 - 科室树切换后右侧医生列表和排班日期不更新。检查并修正
depcode同步、selectedKeys和请求依赖。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 添加医院接口类型定义及api函数封装 | 06:38 |
| 2 | 添加医院功能实现 | 04:52 |
| 3 | 根据id删除医院设置 | 12:11 |
| 4 | 批量删除id状态获取及设置 | 08:01 |
| 5 | 批量删除实现 | 07:26 |
| 6 | 编辑按钮事件绑定-及路由配置 | 03:21 |
| 7 | 根据id获取医院设置并渲染到表单 | 12:45 |
小结
- 接口封装前先确认请求方式、URL、参数和响应,再决定是否需要定义请求/响应类型;简单参数不必强行建类型。
- 新增/编辑共用页面时,
id就是模式开关:有id走编辑并拉详情,无id走新增且不拉详情。 - 删除类操作统一按“确认 → 请求 → 提示 → 刷新/清状态”收口,批量删除必须清空选中 keys。
- 树、列表、分页联动的核心是依赖状态:
depcode变化触发排班/医生请求,selectedKeys与depcode保持同步。 - 表单整体取值用
getFieldsValue(),单字段取值用getFieldValue(),详情回填用setFieldsValue()。 - 成功跳转与失败提示必须分离,
catch中不要继续执行navigate。 - 路由参数可能为
undefined,共用组件中先判空再请求,断言只能用于确定存在的场景。 - 部署重点检查三件事:history fallback 解决刷新 404、静态目录托管 build 产物、代理与
pathRewrite解决跨域。