Skip to content

渲染数据与点击查询获取分页数据(Day12 3/3) ​

本页对应 React + TypeScript + 医通项目,共 6 段视频,约 1 小时 16 分。

学习目标 ​

  1. 能够说明项目 src 目录下 api、pages、routes、store、components、layout、utils 等目录的职责,并准确定位医院设置功能应修改的文件位置。
  2. 能够解释 craco.config.js 的作用,说明路径别名、跨域代理、环境变量与 request 二次封装之间的配合关系。
  3. 能够独立创建医院设置路由组件,并在路由表中完成父子路由、懒加载组件、meta.title 与菜单图标的配置。
  4. 能够使用 Ant Design 完成医院设置页面的静态结构,包括查询表单、按钮区域、表格列配置及基础样式布局。
  5. 能够根据接口文档定义医院设置相关 TypeScript 类型,并封装获取医院设置分页列表的 API 函数。
  6. 能够调用分页列表 API,声明并更新列表、总数、当前页、每页条数等状态,完成表格数据渲染、序号计算及操作列按钮展示。

知识点 ​

本节目标:把静态结构变成动态列表 ​

本节对应笔记里的两条:1. 定义状态;2. 设置状态。前一节已经把医院设置页面的静态结构搭出来:上面是 Form 和按钮,下面是 Table。现在要让 Table 显示服务器返回的医院设置列表。为什么需要状态?因为请求回来的 records、总条数 total、当前页 current、每页条数 pageSize 都会变化,不能写死;只有把数据放进 state,状态一变,组件才会重新渲染。怎么想?先声明列表和分页相关状态,再在挂载后发请求,最后用 setXxx 把接口结果写回状态。怎么做?在 pages/hospital/hospitalSet 的路由组件中写:

tsx
const [current, setCurrent] = useState<number>(1)
const [pageSize, setPageSize] = useState<number>(3)
const [total, setTotal] = useState<number>(10)
const [hospitalSetList, setHospitalSetList] = useState<IHospitalSetList>([])
const [hosname, setHosname] = useState<string>()
const [hoscode, setHoscode] = useState<string>()

易错点:hospitalSetList 的默认值要是空数组,不能是 undefined,否则 Table 的 dataSource 容易出问题;current、pageSize、total 要明确是 number;医院名称、医院编号是选填参数,可以不传,所以用可选的字符串状态。

挂载后请求:首屏数据为什么放在 useEffect ​

为什么需要?进入医院设置页时,用户没有点击任何按钮,但页面就应该有数据,这属于“首屏数据获取”。怎么想?生命周期钩子对应函数组件里的 useEffect,挂载完成后执行,依赖数组给空数组,表示只执行一次。怎么做?在组件内定义异步函数并调用:

tsx
const getList = async () => {
  const res = await getHospitalSetList(current, pageSize, hosname, hoscode)
  const { records, total } = res
  setHospitalSetList(records)
  setTotal(total)
}

useEffect(() => {
  getList()
}, [])

转写稿的调试过程是:先打印请求结果,确认接口确实返回数据;再观察真正需要的是 records 和 total,所以不要拿整个 res 去渲染,而是解构。易错点:useEffect 的回调不能直接写成 async,要在里面定义 async 函数再调用;依赖数组为空只代表首屏执行,后续如果分页参数变化要重新请求,需要另补依赖或事件调用,不能误以为空数组会跟着 current 变化。

设置状态:请求结果如何进入表格 ​

为什么需要“设置状态”?接口数据只是临时变量,不 set 到 state,Table 不会重新渲染。怎么想?请求成功后做两件事:把 records 设置成医院设置列表,把接口返回的总条数设置成 total。怎么做?

tsx
setHospitalSetList(records)
setTotal(total)

然后把状态接到 Table 上:

tsx
<Table
  rowKey="id"
  dataSource={hospitalSetList}
  columns={columns}
/>

转写稿里刷新后列表能出来,但控制台报 key 错误,原因是遍历渲染缺少唯一 key;加上 rowKey="id" 后错误消失。易错点:dataSource 要绑状态,不要绑请求函数里的临时变量;rowKey 要用唯一不重复的 id;columns 里每一项要通过 dataIndex 对应字段,如医院名称、医院编号、API 基础路径、签名、联系人姓名、联系人手机等。

序号列的调试:为什么第二页不能还是 1、2、3 ​

为什么需要单独推导?转写稿发现第一页序号是 1、2、3,但第二页如果还写死索引,仍会从 1 开始,这是错的。怎么想?序号由当前页、每页条数和当前行索引共同决定。怎么做?用渲染函数的第三个参数 index:

tsx
{
  title: '序号',
  render: (_: any, __: any, index: number) =>
    (current - 1) * pageSize + index + 1
}

推导过程:current=1 时 (1-1)*3+index+1 得到 1、2、3;current=2 时 (2-1)*3+index+1 得到 4、5、6。易错点:index 从 0 开始,所以最后要 +1;不要直接把 index 当序号;本节虽然定义了分页状态,但转写稿结尾明确说分页功能还没接,所以这里先把序号公式和列表渲染做对。

操作列与常见易错点 ​

操作列需要显示“编辑”“删除”两个按钮,转写稿用 Space 包裹并给按钮加图标:

tsx
{
  title: '操作',
  render: () => (
    <Space>
      <Button type="primary" icon={<EditOutlined />}>编辑</Button>
      <Button type="primary" danger icon={<DeleteOutlined />}>删除</Button>
    </Space>
  )
}

易错点:图标组件要手动从 Ant Design 图标库导入;删除按钮用 danger;按钮之间用 Space 留间距;columns 配置要写在 Table 的 columns 属性上,不要把配置直接塞进 Table 标签内部。最后检查顺序:定义状态 → 挂载请求 → 解构 records/total → 设置状态 → dataSource 绑状态 → rowKey → 序号公式 → 操作列。这样“渲染数据”这条线就闭合了。

本节要完成什么 ​

本节目标很明确:在“医院设置”列表页中,文本框输入医院名称、医院编号后,点击“查询”,重新获取分页数据。转写稿里已经完成了列表首屏请求、分页状态声明和表格渲染,本节是在这个基础上补上“查询条件”这一环。整体链路是:Form 收集输入 → 查询按钮提交表单 → onFinish 拿到 values → 把 hosname、hoscode 写入状态 → useEffect 监听到状态变化 → 重新调用分页 API。

知识点一:用 Form 收集查询条件 ​

为什么需要:查询条件不能靠直接读 DOM,否则输入框一多、重置和校验都会变乱。项目使用 Ant Design 的 Form,让输入项和字段名绑定,提交时统一拿到 values。

怎么想:笔记里写了两条路:form.getFieldsValue() 和 onFinish 的 values。本节查询是“提交表单”场景,优先用 onFinish(values),因为点击提交时 Ant Design 已经把字段值整理好了;getFieldsValue 更适合非提交场景手动取值。

怎么做:先创建 form 对象并绑定到 Form,每个 Form.Item 必须有 name,且 name 要和接口参数一致:hosname、hoscode。

tsx
const [form] = Form.useForm()

<Form form={form} onFinish={search} layout="inline">
  <Form.Item name="hosname">
    <Input placeholder="医院名称" />
  </Form.Item>
  <Form.Item name="hoscode">
    <Input placeholder="医院编号" />
  </Form.Item>
  <Button type="primary" htmlType="submit">查询</Button>
</Form>

易错点:Form.Item 不写 name,values 里就没有这个字段;name 写成 hosName、hosCode 与接口参数不一致,后端收不到;把 form 创建了却没传给 <Form form={form}>,表单实例不生效。

知识点二:让查询按钮触发表单提交 ​

为什么需要:查询按钮不是普通按钮,它要触发整个表单的提交逻辑。用 htmlType="submit" 才能让 Form 的 onFinish 执行。

怎么想:笔记写的是“给查询按钮绑定单击事件”,但更准确的做法是:按钮负责提交,Form 负责收集并回调 search。这样不用在按钮上再写 onClick 手动取值。

怎么做:按钮加 htmlType="submit",Form 上写 onFinish={search}。search 只做一件事:把查询条件写入状态。

tsx
const search = (values: { hosname?: string; hoscode?: string }) => {
  setHosname(values.hosname)
  setHoscode(values.hoscode)
}

易错点:按钮只写 onClick 不写 htmlType="submit",onFinish 不会触发;同时写 onClick 和 onFinish 容易重复请求;search 里直接发请求,会拿到旧的 hosname/hoscode,因为 setState 是异步的,应让 useEffect 响应状态变化。

知识点三:查询条件驱动分页请求 ​

为什么需要:分页请求不仅依赖 current、pageSize,还依赖查询条件。查询条件变化后必须重新请求,否则表格还是旧数据。

怎么想:转写稿里列表页已经声明了 current、pageSize、total、hospitalSetList 等状态,并在 useEffect 中调用 _getHospitalSetList。本节要把 hosname、hoscode 也纳入依赖,让“查询条件变 → 重新请求”自动发生。

怎么做:声明查询条件状态,useEffect 依赖数组补上 current、pageSize、hosname、hoscode,请求函数把四个参数传给封装好的 API。

tsx
const [hosname, setHosname] = useState<string>()
const [hoscode, setHoscode] = useState<string>()

useEffect(() => {
  _getHospitalSetList()
}, [current, pageSize, hosname, hoscode])

const _getHospitalSetList = async () => {
  const res = await getHospitalSetList(current, pageSize, hosname, hoscode)
  setHospitalSetList(res.records)
  setTotal(res.total)
}

易错点:useEffect 依赖数组漏写 hosname/hoscode,点击查询后不会重新请求;请求参数顺序与 API 函数定义不一致;查询后是否重置到第一页要按项目约定处理,本节笔记只要求重新设置 hosname 和 hoscode,不要凭空加逻辑;清空查询时要把两个状态也清掉,否则输入框清了但请求还带旧条件。

调试过程与易错点 ​

转写稿里调试列表时,先打印 res,确认返回结构里有 records 和 total,再设置状态;这一步同样适用于查询。点击查询后可在 search 里打印 values,在 useEffect 里打印 hosname/hoscode,确认“表单值 → 状态 → 请求参数”三段都通。常见同音字纠正:死锥是 string,飞页是分页,声明状态是声明状态。最后检查:输入医院名称点击查询,Network 里分页接口参数应带上 hosname;输入医院编号再查,应带上 hoscode;两个都不填时,参数应为空或按项目约定不传。

本节目标与开发流程 ​

本节以“医院设置”页面为例,走通一个功能从路由到数据的完整流程:创建路由组件 → 配置路由 → 完成静态结构 → 查看接口并封装类型/API → 在生命周期中请求数据 → 声明状态并渲染列表。转写稿反复强调这六步,是因为后续功能都会复用同一套思路;先固定流程,写代码时就不容易漏掉类型、路由或状态。

为什么需要:没有流程会直接堆组件,容易出现“页面有了但路由不显示”“接口通了但类型不对”“数据回来但表格不渲染”。
怎么想:先让页面能访问,再让页面长得像,最后让数据动起来。
怎么做:按六步推进,每步只解决一个问题。
易错点:不要一开始就写接口和表格,先把路由和静态结构跑通。

项目目录与接口文档 ​

转写稿先介绍了项目目录:src/api 放 API 封装,src/pages 放路由组件,src/routes 放路由表,src/components 放公共组件,src/layout 放布局,src/locales 放国际化,src/utils/http 是对 axios 的二次封装。医院管理接口、数据字典接口、登录接口地址要收藏,后面封装 API 会频繁查看。

为什么需要:知道目录才知道代码写在哪里;知道接口文档才知道请求方式、地址、参数和响应结构。
怎么想:看到“医院设置分页列表”,先确认它是 GET 请求,参数包含当前页、每页条数,以及医院名称、医院编号等选填项。
怎么做:打开接口文档,记录请求方式、地址、参数是否必填、响应中的 records 和 total。
易错点:必填参数和选填参数要看清楚;响应类型不定义,TypeScript 后面无法安全使用数据。

craco 扩展配置与环境变量 ​

项目启动命令不是 react-scripts start,而是 craco start,因为 CRA 默认隐藏 webpack 配置,需要借助 @craco/craco 扩展。craco.config.js 中配置了路径别名、主题、跨域代理等。开发环境读取 .env.development 中的 REACT_APP_API,生产环境读取 .env.production,这样打包上线不用手改请求前缀。

为什么需要:开发服务器和生产服务器地址通常不同,写死地址上线前还要手动改,容易出错。
怎么想:请求前缀用环境变量读取,跨域代理用 devServer.proxy 匹配前缀并重写。
怎么做:在 craco.config.js 配代理;在 axios 封装里用 process.env.REACT_APP_API 作为 baseURL。
易错点:用了 craco 配置就必须用 craco start 启动,否则配置不生效;代理的 pathRewrite 要去掉开发前缀。

创建路由组件并配置路由 ​

先建 pages/hospital/hospitalSet/HospitalSet.tsx,再到 routes 中配置。医院管理是一级菜单,本身没有页面组件,下面挂“医院设置”等子路由;meta.title 决定侧边栏文字,meta.icon 决定图标。路由组件用 lazy 懒加载。

tsx
const HospitalSet = lazy(() => import('@/pages/hospital/hospitalSet/HospitalSet'))

{
  path: '/hospital',
  meta: { title: '医院管理', icon: <ShopOutlined /> },
  children: [
    {
      path: '/hospital/hospitalSet',
      meta: { title: '医院设置' },
      element: <HospitalSet />
    }
  ]
}

为什么需要:没有路由组件和路由配置,点击菜单不会显示页面。
怎么想:先看菜单层级,再决定一级路由有没有 element。
怎么做:创建组件、配置 path/element/meta,用 lazy 包裹。
易错点:一级菜单如果没有独立页面,就不要硬写 element;图标要导入对应组件。

静态结构:Form、按钮与 Table ​

页面外层用 Card 形成白底,上面是 Form,设置为 layout="inline" 让表单项横向排列;两个输入框分别填“医院名称”“医院编号”的 placeholder。查询和清空按钮用 Space 包住。下面按钮组用 Space,批量删除按钮设为禁用。表格用 Table,列配置写在 columns 状态中,并给表格加 rowKey="id"。

为什么需要:先有稳定结构,后面接数据才不会边调样式边调逻辑。
怎么想:按“表单区—按钮区—表格区”拆分。
怎么做:Form 用 inline;Table 的 columns 配 title/dataIndex。
易错点:columns 不要写成表格子标签;rowKey 不写会出现 key 警告。

类型与 API 封装 ​

在 api/hospital/model 中定义医院设置项、列表和响应类型;在 api/hospital/hospitalSet.ts 中封装 getHospitalSetList。参数按接口文档写当前页、每页条数、医院名称、医院编号。请求使用二次封装后的 request。

ts
export interface IHospitalSetItem {
  id: number
  hospitalName: string
  hospitalCode: string
  apiUrl: string
  signKey: string
  contactsName: string
  contactsPhone: string
  status: number
}

export interface IHospitalSetListResponse {
  records: IHospitalSetItem[]
  total: number
}

export const getHospitalSetList = (
  page: number,
  limit: number,
  hospitalName?: string,
  hospitalCode?: string
) => {
  return request.get<any, IHospitalSetListResponse>(
    `/admin/hosp/hospitalSet/${page}/${limit}`,
    { params: { hospitalName, hospitalCode } }
  )
}

为什么需要:TS 需要知道响应里有什么字段;API 函数集中管理便于复用。
怎么想:先定义“每一项”,再定义“列表”,最后定义“分页响应”。
怎么做:模型和请求函数分文件,选填参数加 ?。
易错点:不要用 any 一路到底;接口地址和参数名要以文档为准。

useEffect 请求、状态渲染与分页序号 ​

进入页面不点击就要有数据,所以放在 useEffect 中,依赖空数组模拟挂载完成。声明 current、pageSize、total、hospitalSetList 等状态,请求成功后 setHospitalSetList(records)、setTotal(total),表格 dataSource 绑定列表。序号不能直接用 index + 1,否则第二页仍从 1 开始;公式为 (current - 1) * pageSize + index + 1。

tsx
useEffect(() => {
  const _getHospitalSetList = async () => {
    const res = await getHospitalSetList(current, pageSize)
    setHospitalSetList(res.records)
    setTotal(res.total)
  }
  _getHospitalSetList()
}, [current, pageSize])

为什么需要:首屏数据必须自动获取;分页序号要跨页连续。
怎么想:先打印 res,确认 records/total 是否存在,再设置状态。
怎么做:请求函数放 useEffect 内或外均可,成功后设置列表和总条数;序号用公式计算。
易错点:依赖数组漏写会导致分页不刷新;表格必须绑定 rowKey;操作列返回按钮时记得用 Space 包住并导入图标。

查看接口:先看请求方式、URL、参数、响应结果 ​

为什么需要:写列表页之前,必须先知道数据从哪个接口来、要传什么、返回什么。转写稿里明确说,查看接口要看四件事:请求方式、请求地址、请求参数、响应结果。少看一项,后面的类型和函数就容易写错。

怎么想:医院设置是分页列表,所以先在接口文档里展开“医院设置管理”,找到“分页列表”接口。转写稿里操作是:看请求方式是不是 GET,看 URL,看参数表,再点 Try it out 测试。参数里加粗的是必填,未加粗的是选填;这里页码和每页条数必填,医院名称、医院编号选填。

怎么做:在文档里传 page=1、limit=3 测试,观察响应体。转写稿里确认:真正要用的列表数据在 records 里,总条数在 total 里。调试时先把整个响应打印出来,再决定解构谁。

易错点:不要只看 URL 不看参数;不要把 records 当成整个响应;必填参数漏传会直接报错;选填参数在类型里要加问号。

封装 interface:把响应数据结构拆清楚 ​

为什么需要:TS 项目不能靠 any 混过去。转写稿里说得很直白:不定义类型,就没法安全使用响应里的数据。类型来自接口响应,不是拍脑袋写。

怎么想:从外往里拆。响应对象里有 records 和 total;records 是数组;数组每一项才是医院设置对象。所以先定义“每一项”的类型,再定义“列表”类型,最后定义“分页响应”类型。

怎么做:在 api/hospital/model.ts 里写:

ts
export interface IHospitalSetItem {
  id: number
  createTime: string
  hosname: string
  hoscode: string
  apiUrl: string
  signKey: string
  contactsName: string
  contactsPhone: string
  status: number
}

export type IHospitalSetList = IHospitalSetItem[]

export interface IHospitalSetListResponse {
  records: IHospitalSetList
  total: number
}

字段名以接口文档和项目现有类型为准;转写稿里明确保留的字段包括医院名称、医院编号、API 基础路径、签名、联系人姓名、联系人手机、状态、ID,创建时间保留,更新时间等用不上的字段删掉。

易错点:字段名抄错;把 total 漏掉;把可选字段写成必填;把 records 的类型写成 IHospitalSetItem 而不是数组。

封装 API 函数:组件里不直接写 URL ​

为什么需要:接口地址、参数拼接、请求方法都散在组件里,后面改地址或复用时很乱。转写稿的做法是:在 api/hospital 下新建 API 文件,统一导出请求函数。

怎么想:按接口文档签名。页码和每页条数是路径参数,医院名称和医院编号是查询参数;返回值用刚才定义的响应类型。转写稿里还特别提醒:request 从 utils/http 的 index.ts 导出时要解构使用。

怎么做:

ts
import { request } from '@/utils/http'
import type { IHospitalSetListResponse } from './model'

// 地址从接口文档复制,下面用中文占位表示路径参数位置
export const getHospitalSetList = (
  page: number,
  limit: number,
  hosname?: string,
  hoscode?: string
) => {
  return request.get<IHospitalSetListResponse>(
    `接口文档中的医院设置分页地址/${page}/${limit}`,
    { params: { hosname, hoscode } }
  )
}

易错点:request 导入方式写错;选填参数忘加 ?;路径参数和查询参数拼反;泛型写成列表类型而不是分页响应类型。

在组件里调用并调试 ​

为什么需要:函数封装完要验证参数和返回值是否真的通。转写稿的调试过程是:在组件里用 useEffect 调接口,先 console.log(res),确认 records 和 total 都有值,再设置状态。

怎么做:

ts
useEffect(() => {
  const getData = async () => {
    const res = await getHospitalSetList(current, pageSize, hosname, hoscode)
    setHospitalSetList(res.records)
    setTotal(res.total)
  }
  getData()
}, [current, pageSize, hosname, hoscode])

渲染表格时,dataSource 用 hospitalSetList,rowKey 用 id。序号不要直接写 index + 1,转写稿推导出的公式是:(current - 1) * pageSize + index + 1,这样第二页才会从 4 开始。

易错点:useEffect 空依赖只适合首屏;分页变化要加依赖;列表 key 不要用索引;序号公式别漏掉当前页偏移。

先明确“点击保存”要串起哪条链路 ​

为什么需要:表单里填了医院名称、编号等数据,但这些数据只存在于页面状态中,不会自动进入医院列表。点击“保存”必须完成三件事:触发表单提交、拿到表单数据、调用接口把数据提交给服务器。
怎么想:按本节笔记,链路是“Button 触发 submit → Form 的 onFinish 执行 → 通过 form 获取数据 → 调用已封装的 API 函数发送请求”。转写稿里反复强调,做功能要先看接口文档、再封装 API、调用后配合状态渲染;保存功能同样不能绕过接口直接改列表。
怎么做:给 Form 绑定 onFinish,给保存按钮加 htmlType='submit'。
易错点:只在 Button 上写 onClick 不会触发表单校验和 onFinish;htmlType 写成普通 button 也不会走 Form 提交。

用 Form.useForm 建立表单实例 ​

为什么需要:本节笔记要求“通过 form 获取表单数据”,因此要先创建 Form 实例,并让 Form 组件和这个实例建立关联。
怎么想:Form.useForm() 返回一个表单实例,把它绑定到 <Form form={form}> 后,后续才能用这个实例读取字段值。
怎么做:

tsx
const [form] = Form.useForm();

<Form form={form} onFinish={onFinish}>
  {/* 表单项 */}
</Form>

易错点:useForm 要放在组件函数顶层,不要放在条件分支或循环里;不要重复创建多个实例,否则读取到的可能不是当前表单。

给 Form.Item 补 name,数据才有字段名 ​

为什么需要:form.getFieldsValue() 是按 name 收集字段的。没有 name 的 Form.Item 不会进入表单值对象,保存时就会丢字段。
怎么想:每个要提交的输入项都要有唯一 name,并且这个 name 最好与接口参数字段保持一致,减少提交前再转换的成本。
怎么做:

tsx
<Form.Item name="hospitalName" label="医院名称">
  <Input placeholder="医院名称" />
</Form.Item>

易错点:漏写 name 导致 getFieldsValue() 里没有该字段;name 与接口字段拼写不一致,后端收不到有效参数;多个表单项误用同一个 name。

在 onFinish 中获取表单数据 ​

为什么需要:onFinish 是 Form 校验通过后的回调,在这里取数最稳妥。本节笔记要求通过 form.getFieldsValue() 获取表单数据。
怎么想:onFinish(values) 本身就能拿到校验通过的值,但按笔记写法,也可以用 form.getFieldsValue() 再取一次。实际项目里建议统一一种方式,避免“values 和 getFieldsValue 混用”造成误解。
怎么做:

tsx
const onFinish = async () => {
  const data = form.getFieldsValue();
  console.log('保存表单数据', data);
  // 调用项目已封装的保存 API 函数
};

易错点:在 onFinish 外直接调用 getFieldsValue(),可能拿到旧值;表单校验未通过时不会进入 onFinish,不要误以为点击没反应就是代码没绑定。

调用 API 发送保存请求 ​

为什么需要:前端拿到数据后,必须通过接口提交给服务器,才能把新数据添加到医院列表的数据源中。
怎么想:转写稿中的做法是“查看接口 → 封装类型和 API 函数 → 调用 API → 设置状态渲染”。保存时先确认接口的请求方式、地址、参数结构,再调用项目里已封装好的新增/保存 API 函数,不要在这里临时手写 axios 细节。
怎么做:在 onFinish 中调用 API,把表单数据按接口要求传入;保存成功后,按项目现有列表刷新方式重新请求列表,让新数据出现在医院列表中。
易错点:参数名与接口文档不一致;忘记 await 或错误处理;保存成功后没有刷新列表,导致页面看不到新增数据;把表单值原样传给接口,但接口实际需要额外字段或字段转换。

调试顺序:先看事件,再看数据,最后看请求 ​

为什么需要:保存不生效时,问题可能出在按钮、表单字段或接口任一层,按顺序排查最快。
怎么想:转写稿调试列表时强调打印返回值、检查 records 和总条数;保存也可以沿用“逐层打印”的思路。
怎么做:第一步在 onFinish 里打印,确认按钮是否触发提交;第二步打印 form.getFieldsValue(),确认 name 是否齐全;第三步打印请求参数,确认与接口文档一致;第四步看保存成功后列表是否重新请求。
易错点:控制台没有输出,先查 htmlType='submit' 和 onFinish 是否绑在同一个 Form 上;有输出但字段为空,查 Form.Item 的 name;请求失败,查 API 地址、请求方式和参数结构。

本节范围:先跑通“删除医院设置”的前置数据链路 ​

转写稿实际覆盖的是项目目录、craco 配置、医院设置路由组件、静态结构、类型与 API 封装、列表显示;尚未讲到“删除”接口与交互。因此本节不能编造删除步骤,重点是把删除前必须具备的列表数据链路跑通。

  • 为什么需要:删除按钮要基于医院设置列表数据出现,没有列表就无法定位删除对象。
  • 怎么想:先认清目录与接口,再按“路由组件→路由配置→静态结构→类型/API→调用渲染”推进。
  • 怎么做:接口地址按笔记收藏医院接口文档;目录中 api 放请求函数,pages 放路由组件,routes 放路由表。
  • 易错点:不要把 api 函数写进页面组件;不要手写接口地址,统一从接口文档复制。

craco 扩展配置:跨域与环境变量 ​

  • 为什么需要:React 默认隐藏 webpack 配置,直接用 react-scripts start 无法加载自定义跨域配置。
  • 怎么想:用 craco 扩展配置,启动命令改为 craco start/build/test;开发环境走 /dev-api 代理,生产环境读环境变量。
  • 怎么做:在根目录 craco.config.js 配代理,并用 pathRewrite 去掉 /dev-api 前缀。
js
module.exports = {
  devServer: {
    proxy: {
      '/dev-api': {
        target: '按项目配置的后端地址',
        changeOrigin: true,
        pathRewrite: { '^/dev-api': '' }
      }
    }
  }
}
  • 易错点:只改 baseURL 不改代理会跨域;生产环境代理通常不生效,需用真实接口前缀;不要写死开发地址后直接打包上线。

路径别名与医院设置路由 ​

  • 为什么需要:页面层级变深后,相对路径难维护。
  • 怎么想:在 tsconfig/craco 中配置 @ 指向 src,页面用 @/pages/... 引入。
  • 怎么做:创建 pages/hospital/hospitalSet/index.tsx,在路由表中加父级“医院管理”和子级“医院设置”,组件用 lazy 懒加载。
tsx
const HospitalSet = lazy(() => import('@/pages/hospital/hospitalSet'))

{
  path: '/syt/hospital',
  meta: { title: '医院管理', icon: <ShopOutlined /> },
  children: [
    {
      path: 'set',
      element: <HospitalSet />,
      meta: { title: '医院设置' }
    }
  ]
}
  • 易错点:meta 不是 React Router 自带属性,是项目自定义的菜单标题/图标配置;父级无页面时不要硬写 element。

静态结构:Form、按钮区与 Table ​

  • 为什么需要:先搭静态结构,后续接数据时只改数据源,不重排布局。
  • 怎么想:外层 Card 白底;上部 Form layout="inline";中间 Space 包按钮;下部 Table。
  • 怎么做:表单放医院名称、医院编号输入框和查询/清空按钮;表格先定义 columns,只配 title,数据源暂空。
tsx
const columns: ColumnsType<IHospitalSetItem> = [
  { title: '序号' },
  { title: '医院名称' },
  { title: '医院编号' },
  { title: 'API基础路径' },
  { title: '签名' },
  { title: '联系人姓名' },
  { title: '联系人手机' },
  { title: '操作' }
]
  • 易错点:Table 配置写在属性上,不要写成双标签子内容;类型未知时可用占位类型,后续替换。

类型与 API 函数封装 ​

  • 为什么需要:TS 必须知道响应数据结构,否则无法安全使用 records、total。
  • 怎么想:先定义列表项类型,再定义列表响应类型,最后封装请求函数。
  • 怎么做:在 api/hospital/model.ts 定义 IHospitalSetItem、IHospitalSetList;在 api/hospital/index.ts 封装 getHospitalSetList,参数按接口文档:页码、每页条数、医院名称、医院编号。
ts
export interface IHospitalSetItem {
  id: number
  hospitalName: string
  hospitalCode: string
  apiUrl: string
  sign: string
  contactsName: string
  contactsPhone: string
  status: number
}
export interface IHospitalSetList {
  records: IHospitalSetItem[]
  total: number
}
  • 易错点:选填参数加 ?;请求地址和参数名从接口文档复制,不凭记忆写。

列表加载、状态与序号渲染 ​

  • 为什么需要:进入页面就要展示数据,属于首屏请求。
  • 怎么想:在 useEffect(..., []) 中调用请求函数;声明 current、pageSize、total、list 等状态。
  • 怎么做:请求成功后 setList(records)、setTotal(total);表格 dataSource={list},rowKey="id"。序号不能直接用 index,否则第二页仍从 1 开始。
tsx
render: (_, __, index) => (current - 1) * pageSize + index + 1
  • 易错点:忘记 rowKey 会报 key 警告;序号公式要结合当前页和每页条数;删除功能需等删除接口明确后再实现,不能在本节编造。

一、先定位接口:接口文档与项目目录 ​

为什么需要:写“医院设置”列表前,必须先知道数据从哪个接口来,否则后面封装 API、定义类型都没有依据。
怎么想:转写稿里明确提到三类地址——医院接口文档、数据字典接口、登录接口;本节最常用的是医院接口文档。项目里接口封装放在 src/api,按功能分目录,医院相关放在 api/hospital,类型放 model.ts,请求函数放 hospitalSet.ts。
怎么做:把医院接口地址收藏到笔记;进入医院设置管理下的“分页列表”接口查看详情;在项目中按 api/hospital/model.ts、api/hospital/hospitalSet.ts 的结构建文件。
易错点:不要把所有接口函数塞进一个文件;不要把接口地址写死在组件里;登录接口本节项目未用到,不要混用。

二、查看接口要抓四个信息 ​

为什么需要:接口文档信息不全,类型和函数就会写错。
怎么想:看接口固定看四件事——请求方式、请求地址、请求参数、响应数据结构。医院设置分页列表是 GET;参数中当前页、每页条数是必填,医院名称、医院编号是选填;响应数据重点看 records 和 total。
怎么做:在文档里点“测试”发一次请求,确认返回结构;必填/选填以文档加粗加黑为准;把 records 作为表格数据源,total 作为分页总数。
易错点:只看地址不看响应结构;把选填参数当成必填;把 records 和 total 的层级看错。

三、封装类型:从响应结构倒推 ​

为什么需要:TypeScript 要检查数据,不定义类型就无法安全使用 records 里的字段。
怎么想:先定义“每一项”的类型,再定义“列表”类型,最后定义“分页响应”类型。
怎么做:在 model.ts 中按接口文档字段定义,字段名以文档为准:

ts
export interface IHospitalSetItem {
  id: number
  // 其余字段按接口文档:医院名称、医院编号、API基础路径、签名、联系人姓名、联系人手机、状态
}

export type IHospitalSetList = IHospitalSetItem[]

export interface IHospitalSetListResponse {
  records: IHospitalSetList
  total: number
}

易错点:不要图省事写 any;total 不要漏;字段名不要自己拍脑袋,建议从文档复制。

四、封装 API 函数 ​

为什么需要:组件里直接发请求会重复、难维护,统一封装后调用更清晰。
怎么想:函数参数按接口文档来:current、limit 必填,hospitalName?、hospitalCode? 选填;用项目里二次封装好的 request 发 GET。
怎么做:在 hospitalSet.ts 中写:

ts
import request from '@/utils/http'
import type { IHospitalSetListResponse } from './model'

export const getHospitalSetList = (
  current: number,
  limit: number,
  hospitalName?: string,
  hospitalCode?: string
) => {
  return request.get<IHospitalSetListResponse>(url, {
    params: { hospitalName, hospitalCode }
  })
}

其中 url 按接口文档地址替换,当前页和每页条数用模板字符串拼到地址中。
易错点:request 的引入方式要和项目暴露方式一致;可选参数要加 ?;不要把 current/limit 和 hospitalName/hospitalCode 的传参位置弄反。

五、调用与渲染:首屏数据与序号公式 ​

为什么需要:静态结构有了,但表格没有数据;首屏进入页面就要请求。
怎么想:转写稿对应 componentDidMount 的是 useEffect 空依赖;请求回来把 records 设置给列表状态,把 total 设置给总数状态。
怎么做:先打印 res 验证数据在 records 和 total,再设置状态;表格 dataSource 用列表状态,rowKey="id" 避免 key 报错;序号列用公式:

tsx
{
  title: '序号',
  render: (_: any, __: any, index: number) =>
    (current - 1) * pageSize + index + 1
}

调试时先看第一页是否 1、2、3,再看第二页是否 4、5、6;如果第二页还是 1、2、3,说明序号没有结合 current 和 pageSize。
易错点:忘记设置 total;遍历不写 key;序号直接写 index + 1 导致第二页重复;操作列按钮要放在 Space 里并配置图标与颜色。

删除功能要解决什么问题 ​

列表页里每条医院设置记录后面都有“编辑”“删除”两个操作按钮。删除不是简单把页面上的行删掉,而是要根据当前记录的 id 去调用后端删除接口,删除成功后再重新获取列表数据。否则页面显示和后端数据会不一致。

为什么需要二次确认?因为删除通常不可逆,用户误点一下就把数据删掉,风险很高。所以本节用 Ant Design 的 confirm 模态框做确认:用户点“确定”才真正发请求,点“取消”就什么都不做。

怎么想?可以把删除流程拆成三段:第一段,在操作列拿到当前行 id;第二段,弹出确认框;第三段,确认后调用删除 API,成功后提示并刷新列表。转写稿里前面已经完成了列表渲染、操作列按钮、_getHospitalSetList() 列表加载函数,删除功能就是在这个基础上继续接。

给删除按钮绑定事件并传 id ​

为什么需要传 id?因为表格里每一行都是一条记录,删除按钮必须知道“删哪一条”。不能只写一个统一的删除函数而不带参数,否则后端无法定位记录。

怎么想?删除按钮在 columns 的操作列里渲染,渲染时能拿到当前行数据 record,所以把 record.id 传给事件回调最直接。事件写法要注意:不能写成 onClick={confirm(...)},那样组件渲染时就会执行;要写成箭头函数包一层,点击时才执行。

怎么做?在操作列的删除按钮上绑定单击事件,回调中把当前记录 id 传出去,例如 onClick={() => handleDelete(record.id)}。然后在 handleDelete 中调用 confirm。本节笔记给出的核心代码如下:

ts
confirm({
    title: '确定删除么?',
    icon: <ExclamationCircleFilled />,
    content: '删除当前记录',
    async onOk() {
        await deleteHospitalSetById(id)
        message.success('删除成功');
        // 刷新列表
        _getHospitalSetList();
    },
    onCancel() {
        console.log('Cancel');
    },
});

易错点:id 必须在 confirm 的作用域内可见;deleteHospitalSetById 和 _getHospitalSetList 要已经定义或导入;ExclamationCircleFilled 图标、message 也要按项目实际从 antd 引入。笔记只写了 import {Modal} from 'antd' 和解构 const {confirm} = Modal,但代码里还用了图标和 message.success,实际写代码时不能漏。

用 confirm 做二次确认 ​

为什么用 Modal.confirm?它是 Ant Design 提供的静态确认框,适合“点按钮后先问一句”的场景,不需要自己维护一个受控 Modal 的显示状态。

怎么想?导入 Modal 后解构出 confirm,调用时传入 title、icon、content、onOk、onCancel。其中 onOk 写成 async,因为里面要 await 删除接口。这样确认框会等待异步操作完成,流程更清晰。

怎么做?按笔记写:

ts
import {Modal} from 'antd'
const {confirm} = Modal

然后在删除事件里调用上面的 confirm({...})。onCancel 里可以只打印日志,表示用户取消了删除。

易错点:onOk 里 await deleteHospitalSetById(id) 之后才提示成功和刷新列表;如果删除失败,不应提示“删除成功”。实际项目里要按统一错误处理机制处理异常,不能把失败也当成功。另外,confirm 是静态方法,不要写成组件标签 <confirm />。

调用删除 API 并刷新列表 ​

为什么删除后要刷新列表?因为当前表格数据是之前请求回来的状态,删除成功后如果只在前端删一行,分页总数、当前页数据、序号都可能不准。重新调用 _getHospitalSetList() 让后端返回最新列表最稳妥。

怎么想?删除接口按 id 删除,成功后先 message.success('删除成功'),再刷新列表。刷新函数沿用前面列表加载逻辑,不另起一套。转写稿里列表数据通过状态保存,分页有 current、pageSize、total 等状态,刷新列表时仍走原来的加载函数,能保持分页和列表状态一致。

怎么做?确认框的 onOk 中按顺序执行:await deleteHospitalSetById(id) → message.success('删除成功') → _getHospitalSetList()。如果当前页删空后需要回到上一页,这属于分页边界处理,本节笔记没有展开,不要自行编造;先保证删除后重新请求列表。

易错点:删除成功后忘记刷新列表;把刷新写在 onCancel 里;await 漏写导致提示和刷新早于删除完成;删除按钮事件里传错 id,例如传了索引而不是记录主键。索引会随分页变化,主键 id 才是删除依据。

调试顺序与检查点 ​

调试时先确认删除按钮点击后能弹出 confirm,再确认 onOk 里的 id 是当前行 id,最后看 Network 中删除请求是否发出、响应是否成功,以及成功后列表是否重新请求。转写稿中反复强调“调用 API 函数—声明状态—渲染—刷新”的开发顺序,删除功能同样遵循这个顺序:先接事件,再接确认框,再接删除 API,最后接列表刷新。这样出错时容易定位是哪一段没接上。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

代码一:医院设置列表 —— 定义状态、请求数据、渲染表格(含序号公式)

tsx
// pages/hospital/hospitalSet/index.tsx
import { useEffect, useState } from 'react'
import { Table } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { getHospitalSetList } from '@/api/hospital/hospitalSet'
import type { IHospitalSetList } from '@/api/hospital/model/hospitalSetTypes'

export default function HospitalSet() {
  // 1. 分页相关状态
  const [current, setCurrent] = useState<number>(1)      // 当前页
  const [pageSize, setPageSize] = useState<number>(3)    // 每页条数
  const [total, setTotal] = useState<number>(0)          // 总条数

  // 2. 医院设置列表状态(要渲染的数据源)
  const [hospitalSetList, setHospitalSetList] = useState<IHospitalSetList>([])

  // 3. 查询条件状态(不传就是 undefined,相当于没传该参数)
  const [hosname, setHosname] = useState<string>()
  const [hoscode, setHoscode] = useState<string>()

  // 4. 封装获取列表的函数
  const _getHospitalSetList = async () => {
    const { records, total } = await getHospitalSetList(current, pageSize, hosname, hoscode)
    // 设置状态
    setHospitalSetList(records)
    setTotal(total)
  }

  // 5. 首屏加载 + 分页/查询条件变化时重新请求
  useEffect(() => {
    _getHospitalSetList()
  }, [current, pageSize, hosname, hoscode])

  const columns: ColumnsType<any> = [
    {
      title: '序号',
      // 序号公式:(当前页 - 1) * 每页条数 + 索引 + 1
      render: (_: any, __: any, index: number) => (current - 1) * pageSize + index + 1
    },
    { title: '医院名称', dataIndex: 'hosname' },
    { title: '医院编号', dataIndex: 'hoscode' },
    { title: 'API基础路径', dataIndex: 'apiUrl' },
    { title: '签名', dataIndex: 'signKey' },
    { title: '联系人姓名', dataIndex: 'contactsName' },
    { title: '联系人手机', dataIndex: 'contactsPhone' }
  ]

  return (
    <Table
      rowKey="id"
      columns={columns}
      dataSource={hospitalSetList}
      pagination={{
        current,
        pageSize,
        total,
        onChange: (page, size) => {
          setCurrent(page)
          setPageSize(size)
        }
      }}
    />
  )
}

要点解释 / 易错点:

  • 状态一定要“先声明、再渲染”:hospitalSetList 是 Table 的 dataSource,total 是分页的总条数,current/pageSize 是请求参数。
  • useEffect 的依赖数组写 [current, pageSize, hosname, hoscode],任何一个变化都会重新发请求,这是“点击查询重新获取分页数据”的核心。
  • 序号不能直接用 index + 1,否则第二页又从 1 开始;必须 (current - 1) * pageSize + index + 1。
  • Table 必须写 rowKey="id",否则控制台报 key 重复/缺失警告。
  • dataIndex 的字段名要和接口返回(records 里每一项)一致,写错就是空白列。

代码二:点击查询获取分页数据 —— Form + useForm + onFinish

tsx
// pages/hospital/hospitalSet/index.tsx(在代码一基础上补充查询表单)
import { useEffect, useState } from 'react'
import { Button, Form, Input, Space } from 'antd'

export default function HospitalSet() {
  // 1. 创建 form 对象
  const [form] = Form.useForm()

  const [current, setCurrent] = useState<number>(1)
  const [pageSize, setPageSize] = useState<number>(3)
  const [hosname, setHosname] = useState<string>()
  const [hoscode, setHoscode] = useState<string>()

  // 2. 表单提交(点击查询按钮触发)
  const search = () => {
    // 通过 form 获取文本框内容
    const { hosname, hoscode } = form.getFieldsValue()
    // 重新设置状态,触发 useEffect 重新发请求
    setHosname(hosname)
    setHoscode(hoscode)
    setCurrent(1) // 查询后回到第一页,避免停留在越界页码
  }

  useEffect(() => {
    // 根据 hosname、hoscode 发送 ajax 请求
    // _getHospitalSetList()
  }, [current, pageSize, hosname, hoscode])

  return (
    <Form form={form} layout="inline" onFinish={search}>
      {/* Form.Item 必须有 name,才能被 form.getFieldsValue 拿到 */}
      <Form.Item name="hosname">
        <Input placeholder="医院名称" />
      </Form.Item>
      <Form.Item name="hoscode">
        <Input placeholder="医院编号" />
      </Form.Item>
      <Form.Item>
        <Space>
          <Button type="primary" htmlType="submit">查询</Button>
          <Button>清空</Button>
        </Space>
      </Form.Item>
    </Form>
  )
}

要点解释 / 易错点:

  • 三步不能漏:const [form] = Form.useForm() → <Form form={form}> 绑定 → form.getFieldsValue() 取值。
  • Form.Item 一定要写 name="hosname"、name="hoscode",否则 getFieldsValue() 里根本没有这两个字段。
  • 查询按钮必须 htmlType="submit",这样才会触发 Form 的 onFinish;如果只写 onClick,回车提交和表单校验都会失效。
  • onFinish 回调里要做的是“重新设置状态”,真正发请求交给 useEffect 监听,不要在这里再直接调一次接口(会重复请求)。
  • 查询时建议把 current 重置为 1,否则在第 5 页查询一个只有 1 条数据的关键字会出现空列表。

代码三:删除医院设置 —— confirm 模态框 + 刷新列表

tsx
// pages/hospital/hospitalSet/index.tsx(操作列 + 删除逻辑)
import { Button, Modal, Space, message } from 'antd'
import { ExclamationCircleFilled, EditOutlined, DeleteOutlined } from '@ant-design/icons'
import { deleteHospitalSetById } from '@/api/hospital/hospitalSet'

// 1. 解构出 confirm
const { confirm } = Modal

export default function HospitalSet() {
  // 2. 删除回调:传入当前记录的 id
  const deleteHandler = (id: number) => {
    confirm({
      title: '确定删除么?',
      icon: <ExclamationCircleFilled />,
      content: '删除当前记录',
      async onOk() {
        // 3. 根据 id 调用 api 函数
        await deleteHospitalSetById(id)
        message.success('删除成功')
        // 4. 刷新列表
        _getHospitalSetList()
      },
      onCancel() {
        console.log('Cancel')
      }
    })
  }

  const columns = [
    // ...其它列
    {
      title: '操作',
      render: (row: any) => (
        <Space>
          <Button type="primary" icon={<EditOutlined />}>编辑</Button>
          <Button
            type="primary"
            danger
            icon={<DeleteOutlined />}
            onClick={() => deleteHandler(row.id)}
          >
            删除
          </Button>
        </Space>
      )
    }
  ]

  return null // 省略表格渲染,见代码一
}

要点解释 / 易错点:

  • import { Modal } from 'antd' 后必须 const { confirm } = Modal,不能直接写 <Modal.confirm /> 组件形式。
  • 删除按钮的 onClick 要写成箭头函数 () => deleteHandler(row.id);写成 deleteHandler(row.id) 会在渲染时立刻执行,点击反而不生效。
  • onOk 里是异步删除:await 完接口再 message.success 并调用 _getHospitalSetList() 刷新列表,顺序反了会刷出旧数据。
  • 若接口返回结构是 { code, data, message } 之类,记得在 deleteHospitalSetById 内部判断 code 再决定是否提示成功,不要无条件 message.success。
  • 表格列里的 row 来自 dataSource,前提是 rowKey="id" 正确,否则 row.id 可能取不到。

常见错误 ​

  1. 现象:点击“查询”后列表没变化。
    原因:Form 没绑定 onFinish,或按钮没设 htmlType="submit",提交事件根本没触发。
    修法:检查 Form onFinish={search} 与查询按钮 htmlType="submit" 是否成对存在。

  2. 现象:form.getFieldsValue() 拿到的是空对象。
    原因:Form.Item 缺少 name,或 Form 没绑定 form={form}。
    修法:给 hosname、hoscode 对应的 Form.Item 补 name,并确认 const [form] = Form.useForm() 已绑定到 Form。

  3. 现象:分页第二页序号仍从 1 开始。
    原因:序号直接用了 index + 1,没把 current 和 pageSize 算进去。
    修法:使用 (current - 1) * pageSize + index + 1,并确认 current/pageSize 已进入请求参数和分页状态。

  4. 现象:删除成功但页面还显示旧数据。
    原因:只调了删除接口,没有重新获取列表。
    修法:在 confirm 的 onOk 中 await 删除接口后,立即调用列表查询函数刷新。

  5. 现象:接口返回数据但 TS 报错、渲染取不到字段。
    原因:响应类型与实际结构不一致,或 records/total 解构层级写错。
    修法:按接口文档定义 IHospitalSetListResponse,确认数据在 data.records、data.total,再设置状态。

  6. 现象:请求地址多出 /dev-api 或跨域配置不生效。
    原因:baseURL 环境变量、craco 代理前缀和 pathRewrite 不匹配。
    修法:核对 .env.development、.env.production 与 craco.config.js 中代理前缀,确保请求前缀被正确重写。

练习 ​

  1. 查询表单联调:为医院设置页补全 Form、Form.Item name、查询按钮 htmlType,实现输入医院名称/编号后点击查询能重新请求第一页数据。
  2. 分页序号修正:把表格“序号”列改为跨页连续编号,并验证第 1、2、3 页分别显示 1-3、4-6、7-9。
  3. 删除确认闭环:给删除按钮加 Modal.confirm,要求删除成功后提示、刷新列表;删除失败时保留原列表并提示错误。
  4. 类型与 API 封装:根据接口文档补齐医院设置列表项、列表响应类型和 getHospitalSetList 参数类型,要求选填参数用 ? 标记。
  5. 首屏请求检查:在 useEffect 中完成首屏列表请求,要求只请求一次,并说明如果依赖写错会出现什么现象。

原视频对照 ​

#视频时长
1项目目录结构介绍12:56
2carco扩展配置react项目13:29
3创建医院设置路由组件-完成路由配置10:36
4医院设置组件静态结构完成09:21
5医院设置api类型及函数封装13:06
6医院设置列表显示17:21

小结 ​

  1. 查询功能的核心闭环是:表单取值 → 更新查询条件状态 → 触发列表请求。
  2. Form.Item 的 name、Form 的 form 绑定、按钮的 htmlType 是表单提交链路的三处关键点。
  3. 分页数据必须同时维护 current、pageSize、total,序号显示要结合当前页计算。
  4. 删除类操作不能只关心接口成功,还要处理确认框、提示和列表刷新。
  5. API 封装应先对齐接口文档,再定义类型,最后写请求函数,避免“先写页面后补类型”导致返工。
  6. 首屏数据放在挂载生命周期中获取,查询/分页/删除后的刷新则由事件回调主动触发。
  7. 环境变量和代理配置要成对检查,开发环境与生产环境的请求前缀不能混用。