Skip to content

批量删除医院与医院管理 - 医院列表(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}`)
}

本节要做什么 ​

本节笔记给出医院列表相关需求:

  1. 医院列表静态组件
  2. 医院列表数据分页展示
  3. 省市区搜索条件三级联动
  4. 查询医院列表
  5. 清空搜索条件
  6. 查看医院详情
  7. 医院排班
  8. 医院上线

转写稿重点讲了“查询、清空、医院详情路由与静态布局、详情接口类型与函数”。第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

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

代码段 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;同时注意删完当前页可能空了,需回退页码。

练习 ​

  1. 给「查询」按钮补一个防抖(500ms),并说明为什么依赖数组里拆字段和防抖不冲突。
  2. 现在只做了「清空」,请再实现一个「重置并回到默认排序」的扩展逻辑,要求不新增多余状态。
  3. 把详情页 Descriptions 的 labelStyle 抽成常量,解决窄屏下标签竖排换行难看的问题,并验证不影响其他行。
  4. 详情接口若返回 bookingRule: null,页面应如何降级展示?给出处理方案并说明改在哪一层(组件 / API 类型)。
  5. 批量删除后如果当前页只剩 0 条,请补全页码回退逻辑,并列出需要改动的位置。

原视频对照 ​

#视频时长
1医院列表查询功能实现09:31
2清空功能实现08:50
3医院详情路由组件创建及路由配置04:35
4医院详情页静态布局10:10
5医院详情api类型及函数定义05:54

小结 ​

  1. 查询的核心是「表单值 → 状态 → useEffect 监听 → 重发请求」,链路断在任意一环都表现为「点了没反应」。
  2. 对象型状态做依赖必须拆到叶子字段,React 的浅比较不会替你深入对象内部。
  3. 任何筛选条件变化都要把 current 归 1,这是分页 + 搜索组合场景的固定动作。
  4. 按钮禁用状态优先用 Object.values(...).every(...) 表达规则,而不是堆砌 &&,可读性和可维护性都更好。
  5. 清空 = 表单 UI 重置 + 状态重置 + 页码归位,三件事缺一即出 bug。
  6. 详情页属于列表的子路由,务必配 hidden: true 与 :id 占位符,跳转与取参要对齐。
  7. Descriptions 的 span 是栅格份数(一行 3 份),不是列数,这是布局错乱的高频根源。
  8. 类型复用时优先「抽取 + 联合类型」,把接口可能返回 null 的字段显式声明出来,比事后加可选链更可靠。