Skip to content

添加医院与修改医院(Day14 1/3) ​

本页对应 React + TypeScript + 医通项目,共 6 段视频,约 50 分钟。

学习目标 ​

  1. 能够定位并修复省市区联动 Bug:在切换省份时重置市、区表单值及区列表状态,在切换城市时重置区表单值,并通过表单 name 与接口参数对齐完成联动清空。
  2. 能够完成医院类型与医院状态的渲染:将医院状态按 0/1 映射为“未上线/已上线”,调用数据字典接口获取医院等级并渲染为医院类型下拉选项。
  3. 能够根据医院列表分页接口定义参数、列表项、列表数据和响应对象的 TypeScript 类型,并封装包含 path 参数与 query 参数的医院列表请求函数。
  4. 能够在组件挂载后的生命周期钩子中调用医院列表 API,并将返回的列表数据与总条数写入状态。
  5. 能够实现医院列表分页渲染:配置序号、图片、医院名称、等级、地址、状态、创建时间及操作按钮的列渲染,处理 Base64 图片头、分页器、页码监听和 loading 效果。
  6. 能够解释并实践多个 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 要同时更新当前页和每页条数。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:省市区联动(切换省 / 市时重置表单值 + 重置区列表状态) ​

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 实际是数组,或残留了后端没有的字段。
  • 原因:从接口文档整体粘贴后未逐字段核对、未删无用字段、必填与选填(?)没区分。
  • 修法:对着文档逐项过一遍,必填不加 ?,选填加 ?,数组/对象类型按实际结构写,无用字段直接删除。

练习 ​

  1. 联动自查:故意把"切市时重置区"的代码注释掉,跑一遍"选河北→唐山→路南区→切到赤峰市",记录下拉框和表单值分别出现什么异常,再写回代码验证。要求能说出"表单值"和"状态数据"各自负责什么。

  2. 请求时机改造:把当前所有请求合并进一个 useEffect,观察翻页时 Network 面板的请求数量;再拆回按需分发的多个 useEffect,对比前后请求次数,用一句话总结拆分依据。

  3. 类型补全:不看视频,只打开 Swagger 的分页列表接口,独立写出 IHospitalListParams、IHospitalItem、IHospitalListResponse 三个类型,然后与笔记中的版本 diff,列出你漏掉或写错的字段。

  4. 分页序号推导:为序号列写 render,要求第 1 页显示 1、2、3,第 2 页接着显示 4、5、6。先手推公式再上机,并把 pageSize 改成 5 验证公式是否仍然成立。

  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
6react开发释疑02:46

小结 ​

  1. 联动下拉框的"清空"是两件事:清表单值(用户看到的)和清选项数据(下拉里能选的),少做一件就出 bug。
  2. 用 useForm 操作表单的前提是三步齐全——创建实例、Form 绑定、Form.Item 有 name,且 name 必须与接口参数名对齐,否则后续提交、重置全是空谈。
  3. useEffect 的拆分依据是执行时机,不是代码行数:一次性的字典数据与随分页变化的数据必须分开,否则会产生无意义请求。
  4. 分页功能的完整链路是:状态(current/pageSize/total)→ 请求参数 → 依赖监听 → 回填状态,任一环写死都会导致"点了没反应"。
  5. 类型定义是这类后台页面的主要工作量,难点不在技术而在耐心:必填/选填、数组/对象、无用字段,都要逐个核对。
  6. 状态渲染(上线/下线、按钮文案)本质是三元表达式与业务语义的映射,写反了不会报错,只会悄悄出错,需要点一遍验证。
  7. 所有异步请求都应包在 try/finally 里收尾,loading 的关闭不能依赖"成功路径",否则一次失败就永久转圈。