Skip to content

项目练习

初始化项目

克隆项目

由于此项目是一个二次开发的项目,于是首先克隆项目代码

shell
git clone https://gitee.com/yuonly0528/react-project.git

安装依赖

修改镜像源(可选)

shell
# 腾讯
npm config set registry http://mirrors.cloud.tencent.com/npm
# 淘宝
npm config set registry https://registry.npmmirror.com
# npm 原镜像
npm config set registry https://registry.npmjs.org

# 查看npm配置
npm config list

# 设置npm代理
npm config set proxy http://<username>:<password>@<proxy-server-url>:<port>
npm config set proxy http://127.0.0.1:7890
npm config set https-proxy http://127.0.0.1:7890

# 删除npm代理
npm config delete proxy
npm config delete https-proxy

安装依赖

shell
npm i -f  # -f force 强制安装

启动项目

shell
npm start

可能出现的问题

shell
1. 由于网络原因,导致包没下全。
   1. 删除node_modules : 切换源 重新 npm i -f安装
   2. 叉掉错误:对付用
2. token异常:
   1. 删除本地存储的token,刷新页面重新登录

项目接口地址

测试接口

测试接口也叫对接口,联调接口。

我们可以使用 swagger postman 工具来测试接口。使用swagger来搭建接口测试

测试接口目的:要保证接口是按照接口文档完成的,没有问题。这样未来我们开发时就不用考虑接口出错的原因了。

技术选型

  • react@18 框架
  • 全面使用hook函数组件
  • react-router-dom@6 前端路由
  • redux 集中状态管理方案
  • axios 发送请求函数
  • antd UI 库
  • typescript
  • nprogress 进度条
  • create-react-app 脚手架
  • craco 修改脚手架配置
  • i18next & react-i18next i18n国际化
  • echarts 数据可视化

项目开发流程

  1. 项目立项(管理层)
  2. 项目需求分析(市场部)

产出:项目需求文档( 产品经理 )

  1. 设计产品原型 (关键) (axure)

产出:项目原型图/草图(产品经理)

  1. 产品开会

​ 产品经理召集前端、后端、UI、测试开会。

  • 对于前端来说:

    • 产品经理会给我们介绍项目需求具体情况,我们需要尽可能搞清楚需求,同时需要敲定需求完成时间。

    • UI 提供项目UI原型图/标注图

    • 后端提供接口文档

    • 认识对接后端、UI、测试、产品同事。

  1. 前端开发

    • 根据后端提供开发的接口文档测试接口

    • 按照需求文档要求开发项目功能

  2. 测试

  3. 上线

Demo演示

shell
前台尚医通挂号平台(前台)在线访问地址: 
- http://syt-h5.atguigu.cn/
- http://syt.atguigu.cn/

项目配置

由于react项目为了限制私自更改webpack配置,所以react项目中没有webpack的配置文件webpack.config.js,于是为了修改webpack的配置文件,就需要用到 craco,通过craco配置,然后再引入到webpack配置中实现自定义webpack配置

配置craro.config.js

js
const CracoLessPlugin = require("craco-less");
const CracoAlias = require("craco-alias");

module.exports = {
    plugins: [
        // 自定义主题
        {
            plugin: CracoLessPlugin,
            options: {
                lessLoaderOptions: {
                    lessOptions: {
                        modifyVars: { "@primary-color": "#1DA57A" },
                        javascriptEnabled: true,
                    },
                },
            },
        },
        // 路径别名
        {
            plugin: CracoAlias,
            options: {
                source: "tsconfig",
                baseUrl: "./",
                tsConfigPath: "./tsconfig.extend.json",
            },
        },
    ],
    // 开发服务器配置
    devServer: {
        // 激活代理服务器
        proxy: {
            // 将来以/dev-api开头的请求,就会被开发服务器转发到目标服务器去。
            "/dev-api": {
                // 需要转发的请求前缀
                target: "http://syt-api.atguigu.cn", // 目标服务器地址
                changeOrigin: true, // 允许跨域
                pathRewrite: {
                    // 路径重写
                    "^/dev-api": "",
                },
            },
        },
    },
};

配置package.json

json
"scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test",
    // 这里的eject作用就是把craco的配置引入到webpack中
    "eject": "react-scripts eject"
  },

路径别名

开发时当组件层级太深时,我们引入其他目录下文件需要回退很多层目录,很麻烦。

路径别名则提供另外一种写路径的方式,或者说路径简写,让我们可以从根路径出发直接写路径,简单方便。

配置文件:tsconfig.extend.json

json
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"],
      "@api/*": ["src/api/*"],
      "@assets/*": ["src/assets/*"],
      "@comps/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"],
      "@pages/*": ["src/pages/*"]
    }
  }
}

通过插件将上述配置加载到 craco.config.js , 最终会修改 React 脚手架配置,所以就可以项目中使用上述路径别名。

配置 craco.config.js

js
// 路径别名
{
  plugin: CracoAlias,
  options: {
    source: "tsconfig",
    tsConfigPath: "./tsconfig.extend.json",
  },
},

路径提示

通过 extends 将上述配置加载到 tsconfig.json 中,此时在 VSCode 写代码才会有路径提示

配置tsconfig.json

json
{
	"extends": "./tsconfig.extend.json",
}

我们将来如果要添加新的路径别名,只需要修改 tsconfig.extend.json 即可

设置代理服务器

  • 代理服务器可以解决开发时的跨域问题。
  • React脚手架本身提供的配置方式只支持单个代理
  • 如果开发中需要有多个如何处理? ==> 利用craco工具来配置

代理服务器配置

配置craco.config.js

js
module.exports = {
	
  ...
  ...
  
	// 开发服务器配置
	devServer: {
		// 激活代理服务器
		proxy: {
			// 拦截以/dev-api开头的请求,就会被代理服务器拦截并转发到目标服务器去。
			"/dev-api": { // 需要转发的请求前缀
				target: "http://syt-api.atguigu.cn", // 目标服务器地址
				changeOrigin: true, // 为true时代理在转发时, 会将请求头的host改为target的值
				pathRewrite: { // 路径重写, 在转发请求时自动去除路径的/dev-api
					"^/dev-api": "",
				},
        	// 可以在下面配置多个代理
        	
			},
		},
	},
  
  
};
js
需要注意的是,一旦生产环境打包项目,服务器以及相关配置并不会打包进去,所以如果运行打包后的项目,还会产生跨域问题。

最终还是需要服务端来解决,将来我们会学习 `nginx` 来解决此问题

项目中的baseUrl 需要和nginx中http根目录一致。
设置反向代理

环境变量设置

这里的环境变量是指node在运行时能够使用 process.env属性获取到的变量,这些变量定义在.env.development.env.production中,具体使用哪一个文件中的变量,需要根据webpack.config.jsMode属性的配置(dev or prod)

.env.development文件配置

js
REACT_APP_MOCK_API = '/mock-api'
REACT_APP_API = '/dev-api'

.env.production文件配置

js
REACT_APP_API = '/prod-api'

项目目录介绍

shell
├── public # 公共静态资源目录
   ├── favicon.ico # 网站图标
   ├── index.html # 主页面
├── src # 主目录
   ├── api # 接口文件
   ├── app # redux配置文件
   ├── components # 公共组件
   ├── Loading # loading组件
   ├── Translation # 国际化组件
   └── withAuthorization # 登陆权限校验组件
   ├── layouts # 主要布局组件
   ├── locales # i18n国际化配置
   ├── pages # 路由组件
   ├── routes # 路由配置
   ├── styles # 全局/公共样式
   ├── utils # 工具函数
   └── http # 封装请求函数
   ├── App.tsx # App组件
   ├── index.ts # 主入口
   ├── react-app-env.d.ts # 类型文件,在编译时会引入额外文件
├── .env.development # 开发环境加载的环境变量配置
├── .env.production # 生产环境加载的环境变量配置
├── .gitignore # git忽略文件
├── craco.config.js # react脚手架配置文件
├── package.json # 包文件
├── README.md # 项目说明文件
├── tsconfig.extend.json # 路径别名配置文件
├── tsconfig.json # ts配置文件
└── yarn.lock # yarn下载包的缓存文件

实现功能步骤

shell
1. 创建路由组件
2. 配置路由
3. 创建组件的静态结构
4. 查看接口
   1. 封装 interface 
   2. 封装 api 函数 
5. 调用 api 函数
   1. 事件回调
   2. 钩子函数  
6. 声明状态

医院管理功能实现

创建路由组件

创建路由组件:src/pages/hospital/hospitalSet/hospitalSet.tsx

tsx
import React from 'react'
import { Button, Card, Form, Input, Space, Table } from 'antd'
import { ColumnsType } from 'antd/lib/table'
export default function hospitalSet() {
    // 定义一个静态数据:列数据
    const columns:ColumnsType<any> = [
        {
            title: '序号'
        },
        {
            title: '医院名称'
        },
        {
            title: '医院编号'
        },
        {
            title: 'api基础路径'
        },
        {
            title: '签名'
        },
        {
            title: '联系人姓名'
        },
        {
            title: '联系人手机'
        },
        {
            title: '操作'
        }
    ]

    return (
        <Card>
            {/* Form、Select不支持className,只能用style写样式 */}
            <Form
                layout='inline'
            >
                <Form.Item>
                    <Input placeholder='医院名称' />
                </Form.Item>
                <Form.Item>
                    <Input placeholder='医院编号' />
                </Form.Item>
                <Form.Item>
                    <Space>
                        <Button type='primary'>查询</Button>
                        <Button>清空</Button>
                    </Space>
                </Form.Item>
            </Form>
            {/* 这里的className:mt 是在 src/style/index.css引入的全局样式 */}
            <Space className='mt'>
                <Button type='primary'>添加</Button>
                <Button disabled>批量删除</Button>
            </Space>

            <Table
                className='mt'
                columns={columns}
            />

        </Card>
    )
}

路由配置

配置路由:src/routes/index.tsx

tsx
// src/routes/index.tsx
import { lazy, Suspense, FC } from "react";
import { useRoutes, Navigate } from "react-router-dom";
import { HomeOutlined, ShopOutlined } from "@ant-design/icons";
import type { XRoutes } from "./types";

// import { Translation } from "react-i18next";

import {
    Layout,
    EmptyLayout,
    // CompLayout
} from "../layouts";
import Loading from "@comps/Loading";

const Login = lazy(() => import("@pages/login"));
const Dashboard = lazy(() => import("@pages/dashboard"));
const NotFound = lazy(() => import("@pages/404"));
const hospitalSet = lazy(() => import("@/pages/hospital/hospitalSet/hospitalSet"));

// 定义load函数:用于懒加载
const load = (Comp: FC) => {
    return (
        // 因为路由懒加载,组件需要一段网络请求时间才能加载并渲染
        // 在组件还未渲染时,fallback就生效,来渲染一个加载进度条效果
        // 当组件渲染完成时,fallback就失效了
        <Suspense fallback={<Loading />}>
            {/* 所有lazy的组件必须包裹Suspense组件,才能实现功能 */}
            <Comp />
        </Suspense>
    );
};

// 路由配置
const routes: XRoutes = [
    {
        path: "/",
        // (带逻辑判断组件:如果登陆过就重定向到当前目录,若未登录重定向到登陆页面)
        element: <EmptyLayout />,
        children: [
            {
                path: "login",
                element: load(Login),
            },
        ],
    },
    {
        path: "/syt",
        // (带逻辑判断组件: 如果登陆过就重定向到dashboard页面, 若未登录重定向到登陆页面)
        element: <Layout />,
        children: [
            {
                // 路由路径
                path: "/syt/dashboard",
                meta: {
                    icon: <HomeOutlined />,
                    title: '首页'
                },
                element: load(Dashboard),
            },
            {
                // 路由路径
                path: "/syt/hospital",
                meta: {
                    icon: <ShopOutlined />,
                    title: '医院管理'
                },
                children: [
                    {
                        path: '/syt/hospital/hospitalSet',
                        meta: {
                            title: '医院设置'
                        },
                        element: load(hospitalSet)
                    }
                ]
            }
        ],
    },

    {
        path: "/404",
        element: load(NotFound),
    },

    {
        path: "*", // 任意路径:除了上面路径以外其他路径
        element: <Navigate to="/404" />,
    },
];

/* 
自定义hook: 注册应用的所有路由
*/
export const useAppRoutes = () => {
    return useRoutes(routes);
};

// 找到要渲染成左侧菜单的路由
export const findSideBarRoutes = () => {
    const currentIndex = routes.findIndex((route) => route.path === "/syt");
    return routes[currentIndex].children as XRoutes;
};

export default routes;

定义响应体类型

shell
查看接口:【请求方式、url、参数、返回值】
封装interface: 返回值
封装api函数:
	方式1. path: request.请求方式<any,响应值类>(url/参数1/参数2...)
	方式2. query: request.请求方式<any,响应值类>(url, 
		{params:参数key:参数值}
	)

定义request:项目文件初始化的时候已经创建了(二次开发)

定义响应体类型:src/api/hospital/model/hospitalType.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:src/api/hospital/hospitalSet.ts

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

/**
 * 医院设置相关api函数
 * @param page 当前页 必填
 * @param limit 每页几条 必填
 * @param hosname 医院名称
 * @param hoscode 医院编号
 * @returns 
 */
export const getHospitalSetList = (page: number, limit: number, hosname?: string, hoscode?: string) => {
    return request.get<any, IHospitalSetListResponse>(`/admin/hosp/hospitalSet/${page}/${limit}`, {
        params: {
            hosname,
            hoscode
        }
    })
}

调用API渲染数据

通过以下两种方式调用API

ts
1. 事件回调
2. 钩子函数:生命周期 useEffect()

渲染数据:设置列数据的dataIndex属性(antd中语法)

ts
1. 定义数据状态
2. 设置状态(修改数据状态)

src/pages/hospital/hospitalSet/HospitalSet.tsx

tsx
import React, { useEffect, useState } from 'react'
import { Button, Card, Form, Input, Space, Table } from 'antd'
import { EditOutlined, DeleteOutlined } from '@ant-design/icons'
import { ColumnsType } from 'antd/lib/table'
import { IHospitalSetList } from '@/api/hospital/model/hospitalSetType'
import { getHospitalSetList } from '@/api/hospital/hospitalSet'
export default function HospitalSet() {
    // 定义一个静态数据:列数据
    const columns: ColumnsType<any> = [
        {
            // 手动计算序号
            title: '序号',
            render(value: any, row: any, index: number) {
                return (current - 1) * pageSize + (index + 1)
            }
        },
        {
            title: '医院名称',
            dataIndex: 'hosname'
        },
        {
            title: '医院编号',
            dataIndex: 'hoscode'
        },
        {
            title: 'api基础路径',
            dataIndex: 'apiUrl'
        },
        {
            title: '签名',
            dataIndex: 'signKey'
        },
        {
            title: '联系人姓名',
            dataIndex: 'contactsName'
        },
        {
            title: '联系人手机',
            dataIndex: 'contactsPhone'
        },
        {
            title: '操作',
            render(row: any) {
                return (
                    <Space>
                        <Button type='primary' icon={<EditOutlined />}>编辑</Button>
                        <Button type='primary' icon={<DeleteOutlined />} danger>删除</Button>
                    </Space>
                )
            }
        }
    ]
    // 数据状态
    // 分页相关状态
    let [current, setCurrent] = useState<number>(1);
    let [pageSize, setPageSize] = useState<number>(3);
    let [total, setTotal] = useState<number>(10);
    // 医院设置列表类型
    let [hospitalSetList, setHospitalSetList] = useState<IHospitalSetList>([]);
    let [hosname, setHosname] = useState<string>();
    let [hoscode, setHoscode] = useState<string>();
    // 定义异步请求
    async function _getHospitalSetList() {
        let { records, total } = await getHospitalSetList(current, pageSize, hosname, hoscode);
        // 设置状态
        setHospitalSetList(records);
        setTotal(total);
    }
    // 声明周期 
    useEffect(() => {
        // 调用异步请求
        _getHospitalSetList();
    }, [current, pageSize])

    return (
        <Card>
            <Form
                layout='inline'
            >
                <Form.Item>
                    <Input placeholder='医院名称' />
                </Form.Item>
                <Form.Item>
                    <Input placeholder='医院编号' />
                </Form.Item>
                <Form.Item>
                    <Space>
                        <Button type='primary'>查询</Button>
                        <Button>清空</Button>
                    </Space>
                </Form.Item>
            </Form>

            {/* 这里的className:mt 是在 src/style/index.css引入的全局样式 */}
            <Space className='mt'>
                <Button type='primary'>添加</Button>
                <Button disabled>批量删除</Button>
            </Space>

            <Table
                className='mt'
                rowKey={'id'}
                columns={columns}
                dataSource={hospitalSetList}
                pagination={{
                    current,
                    pageSize,
                    total,
                    showQuickJumper: true,
                    showSizeChanger: true,
                    pageSizeOptions: [3, 5, 10, 20],
                    onChange: (page: number, pageSize: number) => {
                        setCurrent(page);
                        setPageSize(pageSize)
                    }
                }}
            />
        </Card>
    )
}

设置表格loading效果

src/pages/hospital/hospitalSet/HospitalSet.tsx

tsx
...
// 设置loading状态
	let [loading,setLoading] = useState<boolean>(false);

// 定义异步请求
    async function _getHospitalSetList() {
        setLoading(true);
        let { records, total } = await getHospitalSetList(current, pageSize, hosname, hoscode);
        // 设置状态
        setHospitalSetList(records);
        setTotal(total);
        setLoading(false);
    }
...
<Table
    className='mt'
	...
    loading={loading}
    ...
/>

列宽度列固定滚动条

可以查阅 antd官方文档中的Table组件中API查看更多属性

src/pages/hospital/hospitalSet/hospitalSet.tsx

tsx

const columns: ColumnsType<any> = [
    {
        // 手动计算序号
        title: '序号',
        // 设置宽度
        width:60,
        align:'center',
        render(value: any, row: any, index: number) {
            return (current - 1) * pageSize + (index + 1)
        }
    },
    {
        title: '操作',
        width:120,
        // 设置固定定位
        fixed:'right',
        render(row: any) {
            return (
                <Space>
                    <Button type='primary' icon={<EditOutlined />}>编辑</Button>
                    <Button type='primary' icon={<DeleteOutlined />} danger>删除</Button>
                </Space>
            )
        }
    }
]

return (
    <Table
        className='mt'
        rowKey={'id'}
        columns={columns}
        dataSource={hospitalSetList}
        loading={loading}
        // 设置宽度
        scroll={{x:1300}}
        pagination={{
            current,
            pageSize,
            total,
            showQuickJumper: true,
            showSizeChanger: true,
            pageSizeOptions: [3, 5, 10, 20],
            onChange: (page: number, pageSize: number) => {
                setCurrent(page);
                setPageSize(pageSize)
                console.log(hospitalSetList)
            }
        }}
    />
)

点击查询获取分页数据

shell
描述: 做了什么,产生了什么结果?
	文本框输入内容,点击查询,重新获取分页数据!

拆分步骤(实现步骤):
1. 获取文本框输入内容
   1. 参数:values
   2. form对象:
      1. const [form] = Form.useForm()
      2. 绑定form对象
      3. form.getFieldsValue获取
      4. Form.Item 要有name属性 hosname  hoscode
2. 给查询按钮绑定单击事件
   1. button   htmlType='submit'
   2. Form     onFinish ==> search 函数
3. 根据文本框内容发送ajax请求
   1. 在useEffect 的监听中 监听 hosname hoscode
   2. 在 onFinish的事件回调中,重新设置 hosname 和 hoscode的状态

src/pages/hospital/hospitalSet/hospitalSet.tsx

tsx
 // 创建form对象
let [form] = Form.useForm();

// 搜索方法
const search = () => {
    // 获取form表单中的数据
    let { hosname, hoscode } = form.getFieldsValue();
    // 设置数据状态
    setHosname(hosname);
    setHoscode(hoscode);
    // 检索从第一页开始查看
    current !== 1 && setCurrent(1);
}

// 声明周期 
useEffect(() => {
    // 调用异步请求
    _getHospitalSetList();
}, [current, pageSize, hosname, hoscode])

<Form
    layout='inline'
    onFinish={search}
    form={form}
>
    <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>

清空搜索条件

src/pages/hospital/hospitalSet/hospitalSet.tsx

tsx

// 清空方法
const clear = () => {
    // 1. 重置表单 hosname 和 hoscode ==>界面
    // 2. 重置 状态 hosname 和 hoscode  ===> 重发请求的
    form.resetFields();
    setHosname(undefined);
    setHoscode(undefined);
    setCurrent(1);
}

<Form.Item>
<Space>
    <Button type='primary' htmlType='submit'>查询</Button>
    <Button onClick={clear} disabled={hosname === undefined && hoscode === undefined}>清空</Button>
</Space>
</Form.Item>

添加医院功能实现

封装接口

tsx
1. 请求方式、url、参数、响应结果
2. 封装 interface
3. 封装api函数

实现添加

shell
描述:点击保存,将表单中的数据添加到医院列表
步骤:
 1. 绑定事件
    1. 给Form绑定 onFinish 事件
    2. Button组件 添加 htmlType='submit'
 2. 获取表单数据
    通过form获取
    0. Form.Item 组件需要有 name属性,并赋值
    1. 创建form: const [form] = Form.useForm()
    2. 给Form组件绑定form属性
    3. 通过form.getFieldsValue获取表单数据
 3. 发送ajax请求
    调用api函数,发送请求

src/pages/hospital/hospitalSet/HospitalSet.tsx

tsx
 {/* 这里的className:mt 是在 src/style/index.css引入的全局样式 */}
<Space className='mt'>
<Button type='primary' onClick={() => navigate('/syt/hospital/hospitalSet/add')}>添加</Button>
<Button disabled>批量删除</Button>
</Space>

src/pages/hospital/hospitalSet/components/AddOrUpdate.tex

tsx
import { addHospitalSet } from '@/api/hospital/hospitalSet'
import { Button, Card, Form, Input, message, Space } from 'antd'
import React from 'react'
import {  useNavigate } from 'react-router-dom'

export default function AddOrUpdate() {
    // 定义form对象
    const [form] = Form.useForm()
    const navigate = useNavigate()
    const onFinish = async () => {
        let data = form.getFieldsValue()
        try{
            await addHospitalSet(data)
            message.success('添加成功')
            navigate('/syt/hospital/hospitalSet')
        }catch(e){
            message.error('添加失败')
        }
    }
    return (
        <Card>
            <Form
                name="basic"
                // 设置标签宽度
                labelCol={{ span: 2 }}
                // 设置输入框的宽度
                wrapperCol={{ span: 22 }}
                onFinish={onFinish}
                form={form}

            >
                <Form.Item
                    label="医院名称"
                    name="hosname"
                    rules={[{ required: true, message: '请输入医院名称!' }]}
                >
                    <Input />
                </Form.Item>

                <Form.Item
                    label="医院编号"
                    name="hoscode"
                    rules={[{ required: true, message: '请输入医院编号!' }]}
                >
                    <Input />
                </Form.Item>
                <Form.Item
                    label="api基础路径"
                    name="apiUrl"
                    rules={[{ required: true, message: '请输入api基础路径!' }]}
                >
                    <Input />
                </Form.Item>
                <Form.Item
                    label="联系人姓名"
                    name="contactsName"
                    rules={[{ required: true, message: '请输入联系人姓名!' }]}
                >
                    <Input />
                </Form.Item>
                <Form.Item
                    label="联系人手机"
                    name="contactsPhone"
                    rules={[{ required: true, message: '请输入联系人手机!' }]}
                >
                    <Input />
                </Form.Item>


                <Form.Item wrapperCol={{ offset: 2, span: 16 }}>
                    <Space>
                        <Button type="primary" htmlType="submit">
                            保存
                        </Button>
                        <Button>返回</Button>
                    </Space>
                </Form.Item>
            </Form>
        </Card>
    )
}

定义路由:src/routes/index.tex

tsx
{
    // 路由路径
    path: "/syt/hospital",
    meta: {
        icon: <ShopOutlined />,
        title: '医院管理'
    },
    children: [
        {
            path: '/syt/hospital/hospitalSet',
            meta: {
                title: '医院设置'
            },
            element: load(HospitalSet)
        },
        {
            path: '/syt/hospital/hospitalSet/add',
            meta: {
                title: '添加医院'
            },
            hidden:true, // 隐藏菜单栏
            element: load(AddOrUpdate)
        }
    ]
}

定义请求体类型 src/api/model/hospitalSetType.ts

ts
// 添加医院 请求体类型
export interface IHospitalSetData{
    apiUrl: string;
    contactsName: string;
    contactsPhone: string;
    hoscode: string;
    hosname: string;
}

封装接口 src/api/hospitalSet.ts

ts
 * 添加医院设置
 * @param data 请求体数据
 * @returns null
 */
export const addHospitalSet = (data:IHospitalSetData)=>{
    return request.post<any,null>('/admin/hosp/hospitalSet/save', data)
}

删除医院功能

封装接口

封装接口 src/api/hospitalSet.ts

ts
/**
 * 根据id 删除医院设置
 * @param id 
 * @returns null
 */
export const delHospitalSetById = (id:string)=>{
    return request.delete<any,null>(`/admin/hosp/hospitalSet/remove/${id}`)
}

在组件上绑定单机事件: src/pages/hospital/hospitalSet/HospitalSet.tsx

tsx
{
    title: '操作',
    width:120,
    fixed:'right',
    render(row: any) {
        return (
            <Space>
                <Button type='primary' icon={<EditOutlined />}>编辑</Button>
                <Button type='primary' icon={<DeleteOutlined />} onClick={() => { deleteHospitalById(row.id) }} danger>删除</Button>
            </Space>
        )
    }
}

// 删除医院方法
const deleteHospitalById = (id:string) => {

    // 弹窗
    confirm({
        title: '确定删除么?',
        icon: <ExclamationCircleFilled />,
        content: '删除当前记录',
        async onOk() {
            // 发送删除医院请求
            await delHospitalById(id)
            message.success('删除成功')
            // 刷新页面
            _getHospitalSetList()
        },
        onCancel() {
            console.log('Cancel');
        },
    });
}

批量删除功能

添加复选框

src/pages/hospital/hospitalSet/HospitalSet.tsx

tsx
<Table
    className='mt'
    rowKey={'id'}
    columns={columns}
    dataSource={hospitalSetList}
    loading={loading}
    // 设置宽度
    scroll={{x:1300}}
    // 设置复选框
    rowSelection={{
        /**
         * onChange调用时机,列表复选框发生变化的时候
         * @param selectedKeys 选中条id组成的数组
         */
        onChange(selectedKeys: React.Key[]) {
            console.log('selectedKeys: ', selectedKeys)
            setSelectedKeys(selectedKeys);
        }
    }}
    pagination={{
        current,
        pageSize,
        total,
        showQuickJumper: true,
        showSizeChanger: true,
        pageSizeOptions: [3, 5, 10, 20],
        onChange: (page: number, pageSize: number) => {
            setCurrent(page);
            setPageSize(pageSize)
            console.log(hospitalSetList)
        }
    }}
/>

封装批量删除的api接口 src/api/hospitalSet.ts

ts
// 批量删除,请求体的类型是React.Key[]不用再单独封装
/**
 * 
 * @param ids id 的数组
 * @returns null
 */
export const removeBatch = (ids: React.Key[]) => {
    return request.delete<any, null>('/admin/hosp/hospitalSet/batchRemove', {
        data: ids
    })
}

实现批量删除

src/pages/hospital/hospitalSet/HospitalSet.tsx

tsx
import { delHospitalById, getHospitalSetList, removeBatch } from '@/api/hospital/hospitalSet'

// 批量删除医院的方法
const batchRemove = (ids: React.Key[]) => {
    // 弹窗
    confirm({
        title: '确定删除么?',
        icon: <ExclamationCircleFilled />,
        content: '删除当前记录',
        async onOk() {
            // 发送删除医院请求
            await removeBatch(ids)
            // 将selectedKeys状态清空成空数组
            setSelectedKeys([]);
            message.success('删除成功')
            // 刷新页面
            _getHospitalSetList()
        },
        onCancel() {
            console.log('Cancel');
        },
    });
}



{/* 这里的className:mt 是在 src/style/index.css引入的全局样式 */}
<Space className='mt'>
<Button type='primary' onClick={() => navigate('/syt/hospital/hospitalSet/add')}>添加</Button>
<Button disabled={selectedKeys.length === 0} onClick={() => { batchRemove(selectedKeys) }}>批量删除</Button>
</Space>

编辑医院设置

定义路由:src/routes/index.tex

ts
{
    path: '/syt/hospital/hospitalSet/edit/:id',
    meta: {
        title: '编辑医院'
    },
    hidden: true, // 隐藏菜单栏
    element: load(AddOrUpdate)
}

组件上绑定按钮:src/pages/hospital/hospitalSet/HospitalSet.tsx

tsx
<Space>
<Button type='primary' icon={<EditOutlined />} onClick={() => { editHospitalById(row.id) }}>编辑</Button>
<Button type='primary' icon={<DeleteOutlined />} onClick={() => { deleteHospitalById(row.id) }} danger>删除</Button>
</Space>

封装api函数:src/api/hospitalSet.ts

ts
/**
 * 根据id获取医院设置数据
 * @param id 
 * @returns Promise<IHospitalSetData>
 */
export const getHospitalSetById = (id:string)=>{
    return request.get<any,IHospitalSetData>('/admin/hosp/hospitalSet/get/' + id)
}

/**
 * 更新医院设置
 * @param data 
 * @returns null
 */
export const updateHospitalSet = (data:IHospitalSetUpdateData)=>{
    return request.put<any,null>('/admin/hosp/hospitalSet/update', data)
}

封装更新医院请求体类型:src/api/model/hospitalSetType.ts

ts
/**
 * 更新医院设置请求体参数类型
 */
export interface IHospitalSetUpdateData extends IHospitalSetData{
    id:string;
}

医院列表路由组件创建

创建医院列表组件:src/pages/hospital/hospitalList/hospitalList.tsx

tsx
import React from 'react'

export default function HospitalList() {
  return (
    <div>hospitalList</div>
  )
}

编辑医院列表路由:src/routes/index.tsx

tsx
{
    path: '/syt/hospital/hospitalList',
    meta: {
        title: '医院列表'
    },
    hidden: false, // 隐藏菜单栏
    element: load(HospitalList)
}

医院省市区列表

渲染医院列表组件静态:src/pages/hospital/hospitalList/hospitalList.tsx

tsx
import { Button, Card, Form, Input, Select, Space } from 'antd'
import Table, { ColumnsType } from 'antd/lib/table'
import React, { useEffect, useState } from 'react'
import { getDistrictList } from '@/api/hospital/hospitalList'
import { IDistrictList } from '@/api/hospital/model/hospitalListType';

// 解构出选项列表
const { Option } = Select;

export default function HospitalList() {
	const [form] = Form.useForm();
	const columns: ColumnsType<any> = [
		{
			title: '序号'
		},
		{
			title: '医院logo'
		},
		{
			title: '医院名称'
		},
		{
			title: '等级'
		},
		{
			title: '详细地址'
		},
		{
			title: '状态'
		},
		{
			title: '创建时间'
		},
		{
			title: '操作'
		}
	]
	
	// 定义省市区数据状态
	let [provinceList, setProvinceList] = useState<IDistrictList>([])
    let [cityList, setCityList] = useState<IDistrictList>([])
    let [dictList, setDictList] = useState<IDistrictList>([])
	// 定义获取省市区的方法
	// 获取省列表
    const getProvinceList = async () => {
        const provinceList = await getDistrictList(86);
        // 设置省状态数据
        setProvinceList(provinceList);
    }
    // 根据省id 获取是列表并渲染
    const getCityList = async (id: number) => {
		// 将市、区的表单项赋值为undefined
		form.setFieldsValue({
			cityCode:undefined,
			districtCode:undefined
		})
		// 将区的状态数据设置为空数组
        setDictList([]);
		// onSelect=function(value) value的值为选中的value的值
        const cityList = await getDistrictList(id);
        setCityList(cityList);
    }
    // 根据市id 获取区列表并渲染
    const getDictList = async (id: number) => {
		// 将区的表单值设置为undefined
        form.setFieldsValue({
            districtCode:undefined
        })
        const dictList = await getDistrictList(id);
        setDictList(dictList);
    }

	// 定义生命周期函数
	useEffect(() => {
        getProvinceList();
    }, [])

	return (
		<Card>
            <Form 
				layout='inline'
				form={form}
				>
                <Form.Item name='provinceCode'>
                    <Select 
						className='mb' 
						placeholder='请选择省' 
						style={{ width: 180 }}
						onSelect={(value:any)=>{getCityList(value)}}
						>
                        {provinceList.map(province => (
                            <Option value={province.value} key={province.id}>{province.name}</Option>
                        ))}
                        
                    </Select>
                </Form.Item>
                <Form.Item name='cityCode'>
                    <Select 
						placeholder='请选择市' 
						style={{ width: 180 }}
						onSelect={getDictList}
						>
                        {cityList.map(city => (
                            <Option key={city.id} value={city.value}>{city.name}</Option>
                        ))}
                    </Select>
                </Form.Item>
                <Form.Item name='districtCode'>
					<Select placeholder='请选择区' style={{ width: 180 }}>
                        {dictList.map(dict => (
                            <Option key={dict.id} value={dict.value}>{dict.name}</Option>
                        ))}
                    </Select>
                </Form.Item>
                <Form.Item name='hosname'>
                    <Input placeholder='医院名称'/>
                </Form.Item>
                <Form.Item name='hoscode'>
                    <Input placeholder='医院编号'/>
                </Form.Item>
                <Form.Item name='hostype'>
                    <Select placeholder='医院类型' style={{ width: 180 }}>
                        <Option value="beijing">北京</Option>
                        <Option value="beijing">北京</Option>
                        <Option value="beijing">北京</Option>
                    </Select>
                </Form.Item>
                <Form.Item name='status'>
                    <Select placeholder='医院状态' style={{ width: 180 }}>
                        <Option value="beijing">北京</Option>
                        <Option value="beijing">北京</Option>
                        <Option value="beijing">北京</Option>
                    </Select>
                </Form.Item>
                <Form.Item>
                    <Space>
                        <Button type='primary' htmlType='submit'>查询</Button>
                        <Button disabled>清空</Button>
                    </Space>
                </Form.Item>
            </Form>

            <Table
                className='mt'
                columns={columns}
            />
        </Card>
	)
}

Base图片前缀

ts
<Image width={100} src={'data:image/jpg;base64,' + row.logoData}/>

医院列表分页功能

定义api请求体和响应体类型:src/api/hospital/hostpitalType.ts

ts
// 医院列表每一项 类型

export interface IHospitalItem {
   id: string;
   createTime: string; // 创建时间
   param: {
       hostypeString: string; // 医院等级
       fullAddress: string; // 医院地址
   },
   hoscode: string; // 医院编号
   hosname: string; // 医院名
   hostype: string; // 医院类型
   provinceCode: string; // 省
   cityCode: string; // 市
   districtCode: string; // 区
   address: string; // 地址
   logoData: string; // 医院logo base64URL
   route: string;  //乘车路线
   status: number; // 医院状态
   bookingRule: {
       cycle: number; // 预约周期
       releaseTime: string; // 放号时间
       stopTime: string;// 停止挂号时间
       quitDay: number; // 就诊结束日期
       quitTime: string; // 结束时间
       rule: string[]; //取号规则
   }
}

/**
 * 医院列表类型
 */
export type IHospitalList = IHospitalItem[]

/**
 *  请求医院列表响应对象类型
 */
export interface IHospitalListResponse {
    content:IHospitalList;
    totalElements:number;
}

/**
 * 医院列表参数类型
 */
export interface IHospitalListParams {
    page: number;
    limit: number;
    hoscode?: string;
    hosname?: string;
    hostype?: string;
    provinceCode?: string;
    cityCode?: string;
    districtCode?: string;
    status?: number;
}

医院列表api封装:/src/api/hospital/hospitalList.ts

ts
import { request } from "@utils/http"
import { IDistrictList, IHospitalListParams, IHospitalListResponse } from "./model/hospitalListType"
/**
 * 根据id 获取省市区列表
 * @param id  86 省  
 * @returns 
 */
export const getDistrictList = (id: number) => {
    return request.get<any, IDistrictList>('/admin/cmn/dict/findByParentId/' + id)
}

// 获取医院列表分页数据,同时定义请求体的类型和响应体的类型
export const getHospitalList = ({page,limit,hoscode,hosname,hostype,provinceCode,cityCode,districtCode,status}:IHospitalListParams)=>{
    return request.get<any, IHospitalListResponse>(`/admin/hosp/hospital/${page}/${limit}`, {
        params:{
            hoscode,
            hosname,
            hostype,
            provinceCode,
            cityCode,
            districtCode,
            status
        }
    })
}

渲染医院列表组件静态:src/pages/hospital/hospitalList/hospitalList.tsx

tsx
import { Button, Card, Form, Image, Input, Select, Space } from 'antd'
import Table, { ColumnsType } from 'antd/lib/table'
import React, { useEffect, useState } from 'react'
import { getDistrictList, getHospitalList } from '@/api/hospital/hospitalList'
import { IDistrictList, IHospitalList,IHospitalItem } from '@/api/hospital/model/hospitalListType';

// 解构出选项列表
const { Option } = Select;

export default function HospitalList() {
	const [form] = Form.useForm();
	const columns: ColumnsType<any> = [
		{
			title: '序号',
			render(value:any, row:any, index:number){
                return (current - 1) * pageSize + (index + 1)
            }
		},
		{
			title: '医院logo',
            render(row:IHospitalItem){
                return (
                    <Image width={100} src={'data:image/jpg;base64,' + row.logoData}/>
                )
            }
		},
		{
			title: '医院名称',
			dataIndex:'hosname'

		},
		{
			title: '等级',
			render(row:IHospitalItem){
                return row.param.hostypeString
            }
		},
		{
			title: '详细地址',
			render(row:IHospitalItem){
                return row.param.fullAddress
            }
		},
		{
			title: '状态',
            render(row:IHospitalItem){
                return row.status ? '已上线' : '未上线'
            }
		},
		{
			title: '创建时间',
            dataIndex:'createTime'
		},
		{
			render(row:IHospitalItem){
                return (
                    <Space>
                        <Button type='primary'>查看</Button>
                        <Button type='primary'>排班</Button>
                        <Button type='primary'>{row.status ? '下线' : '上线'}</Button>
                    </Space>
                )
            }
		}
	]
	
	// 定义省市区数据状态
    let [hospitalList, setHospitalList] = useState<IHospitalList>([]);
	let [total, setTotal] = useState<number>(10);
	let [current, setCurrent] = useState<number>(1);
    let [pageSize, setPageSize] = useState<number>(3);
	let [loading, setLoading] = useState<boolean>(false);
	let [provinceList, setProvinceList] = useState<IDistrictList>([])
    let [cityList, setCityList] = useState<IDistrictList>([])
    let [dictList, setDictList] = useState<IDistrictList>([])
	let [typeList,setTypeList] = useState<IDistrictList>([])

	// 定义获取省市区的方法
	// 获取省列表
    const getProvinceList = async () => {
        const provinceList = await getDistrictList(86);
        // 设置省状态数据
        setProvinceList(provinceList);
    }
    // 根据省id 获取是列表并渲染
    const getCityList = async (id: number) => {
		// 将市、区的表单项赋值为undefined
		form.setFieldsValue({
			cityCode:undefined,
			districtCode:undefined
		})
		// 将区的状态数据设置为空数组
        setDictList([]);
		// onSelect=function(value) value的值为选中的value的值
        const cityList = await getDistrictList(id);
        setCityList(cityList);
    }
    // 根据市id 获取区列表并渲染
    const getDictList = async (id: number) => {
		// 将区的表单值设置为undefined
        form.setFieldsValue({
            districtCode:undefined
        })
        const dictList = await getDistrictList(id);
        setDictList(dictList);
    }
	// 获取医院等级
	const getDegree = async() => {
		let degree = await getDistrictList(10000)
		setTypeList(degree)
	}

	// 获取医院分页列表数据
	const _getHospitalList = async()=>{
		setLoading(true);
		let {content, totalElements} = await getHospitalList({page:current,limit:pageSize});
		setHospitalList(content);
        setTotal(totalElements);
        setLoading(false);
		console.log(content)
	}
	// 定义生命周期函数
	useEffect(() => {
		// 获取省
        getProvinceList();
		// 获取等级
		getDegree()
    }, [])

	// 定义生命周期函数2
	useEffect(()=>{
        _getHospitalList();
    },[current, pageSize])

	return (
		<Card>
            <Form 
				layout='inline'
				form={form}
				>
                <Form.Item name='provinceCode'>
                    <Select 
						className='mb' 
						placeholder='请选择省' 
						style={{ width: 180 }}
						onSelect={(value:any)=>{getCityList(value)}}
						>
                        {provinceList.map(province => (
                            <Option value={province.value} key={province.id}>{province.name}</Option>
                        ))}
                        
                    </Select>
                </Form.Item>
                <Form.Item name='cityCode'>
                    <Select 
						placeholder='请选择市' 
						style={{ width: 180 }}
						onSelect={getDictList}
						>
                        {cityList.map(city => (
                            <Option key={city.id} value={city.value}>{city.name}</Option>
                        ))}
                    </Select>
                </Form.Item>
                <Form.Item name='districtCode'>
					<Select placeholder='请选择区' style={{ width: 180 }}>
                        {dictList.map(dict => (
                            <Option key={dict.id} value={dict.value}>{dict.name}</Option>
                        ))}
                    </Select>
                </Form.Item>
                <Form.Item name='hosname'>
                    <Input placeholder='医院名称'/>
                </Form.Item>
                <Form.Item name='hoscode'>
                    <Input placeholder='医院编号'/>
                </Form.Item>
                <Form.Item name='hostype'>
                    <Select 
						placeholder='医院类型' 
						style={{ width: 180 }}
						>
							{typeList.map(type=>(
								<Option value={type.value} key={type.id}>{type.name}</Option>
							))}
                        
                    </Select>
                </Form.Item>
                <Form.Item name='status'>
                    <Select placeholder='医院状态' style={{ width: 180 }}>
                        <Option value={0}>未上线</Option>
                        <Option value={1}>已上线</Option>
                        
                    </Select>
                </Form.Item>
                <Form.Item>
                    <Space>
                        <Button type='primary' htmlType='submit'>查询</Button>
                        <Button disabled>清空</Button>
                    </Space>
                </Form.Item>
            </Form>

            <Table
                className='mt'
				rowKey={'id'}
                columns={columns}
				dataSource={hospitalList}
				loading={loading}
                pagination={{
                    current,
                    pageSize,
                    total,
                    onChange(page:number, pageSize:number){
                        setCurrent(page);
                        setPageSize(pageSize);
                    }
                }}
            />
        </Card>
	)
}

医院列表查询功能

定义参数类型:src/api/hospital/hostpitalType.ts

ts
/**
 * 医院列表参数类型
 */
export interface IFormFields{
    hoscode?: string;
    hosname?: string;
    hostype?: string;
    provinceCode?: string;
    cityCode?: string;
    districtCode?: string;
    status?: number;
}
export interface IHospitalListParams extends IFormFields{
    page: number;
    limit: number;
}

渲染医院列表组件静态:src/pages/hospital/hospitalList/hospitalList.tsx

tsx
// 定义查询params
let [formFields, setFormFields] = useState<IFormFields>({
    hoscode: undefined,
    hosname: undefined,
    provinceCode: undefined,
    cityCode: undefined,
    districtCode: undefined,
    status: undefined,
    hostype: undefined
})

// 获取医院分页列表数据
const _getHospitalList = async()=>{
    setLoading(true);
    let {content, totalElements} = await getHospitalList({ page: current, limit: pageSize, ...formFields });
    setHospitalList(content);
    setTotal(totalElements);
    setLoading(false);
    console.log(content)
}

// 清空功能
const clear = () => {
    // 清空form表单的数据
    // 清空 formFields 状态的值都为 undefined
    // 当前页设置为 1
    form.resetFields();
    setFormFields({
        hoscode: undefined,
        hosname: undefined,
        provinceCode: undefined,
        cityCode: undefined,
        districtCode: undefined,
        status: undefined,
        hostype: undefined
    })
    setCurrent(1);
}

// 定义生命周期函数2
	useEffect(()=>{
        _getHospitalList();
    },[current, pageSize, formFields.hoscode, formFields.hosname, formFields.cityCode, formFields.provinceCode, formFields.districtCode, formFields.hostype, formFields.status])

<Form 
    layout='inline'
    form={form}
    onFinish={search}
    >
        <Button disabled={Object.values(formFields).every(item => item === undefined)} onClick={clear}>清空</Button>

医院详情页面

配置路由:src/route/index.ts

ts
const HospitalDetail = lazy(() => import("@/pages/hospital/hospitalList/components/HospitalDetail"));
{
    path: '/syt/hospital/hospitalList/show/:id',
    meta: {
        title: '医院详情'
    },
    hidden: true, // 隐藏菜单栏
    element: load(HospitalDetail)
}
tsx

渲染医院列表组件传参:src/pages/hospital/hospitalList/hospitalList.tsx

tsx
const navigate = useNavigate()

<Space>
    <Button type='primary' onClick={()=>{navigate('/syt/hospital/hospitalList/show/' + row.id)}}>查看</Button>
    <Button type='primary'>排班</Button>
    <Button type='primary'>{row.status ? '下线' : '上线'}</Button>
</Space>

定义医院详情请求响应体类型:src/api/hospital/hostpitalType.ts

ts
// 医院详情类型返回值类型
export interface IHospitalDetail{
    bookingRule:IBookingRule
    hospital:IHospitalItem
}

// 医院列表每一项类型
export interface IHospitalItem {
   id: string;
   createTime: string; // 创建时间
   param: {
       hostypeString: string; // 医院等级
       fullAddress: string; // 医院地址
   },
   hoscode: string; // 医院编号
   hosname: string; // 医院名
   hostype: string; // 医院类型
   provinceCode: string; // 省
   cityCode: string; // 市
   districtCode: string; // 区
   address: string; // 地址
   logoData: string; // 医院logo base64URL
   route: string;  //乘车路线
   status: number; // 医院状态
   intro: string | null; // 联合类型
   bookingRule: IBookingRule|null
}

// 医院预约类型
export interface IBookingRule {
    cycle: number; // 预约周期
    releaseTime: string; // 放号时间
    stopTime: string;// 停止挂号时间
    quitDay: number; // 就诊结束日期
    quitTime: string; // 结束时间
    rule: string[]; //取号规则
}

封装医院详情请求api:/src/api/hospital/hospitalList.ts

ts
// 获取医院详情返回值api
export const getHospitalDetail = (id:string) => {
    return request.get<any,IHospitalDetail>(`/admin/hosp/hospital/show/${id}`)
}

渲染页面布局:src/pages/hospitalList/components/HospitalDetail.tsx

tsx
import { getHospitalDetail } from '@/api/hospital/hospitalList';
import { IBookingRule, IHospitalItem } from '@/api/hospital/model/hospitalListType';
import { Button, Card, Descriptions, Image } from 'antd'
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom';

export default function HospitalDetail() {
    // 获取path参数
    let {id} = useParams();
    // 定义数据状态
    let [hospital, setHospital] = useState<IHospitalItem>();
    let [bookingRule, setBookingRule] = useState<IBookingRule>();
    // 定义方法
    const _getHospitalDetail = async ()=>{
        let {bookingRule,hospital} = await getHospitalDetail(id as string);
        setHospital(hospital);
        setBookingRule(bookingRule);
    }
    useEffect(()=>{
        id && _getHospitalDetail();
    }, [])
    return (
        <Card>
            <Descriptions title="基本信息" bordered>
                <Descriptions.Item labelStyle={{ width: 200 }} label="医院名称" span={1.5}>{hospital?.hosname}</Descriptions.Item>
                <Descriptions.Item label="医院logo" span={1.5}>
                    {hospital?.logoData && <Image width={100} src={'data:image/jpg;base64,' + hospital?.logoData} />}
                </Descriptions.Item>
                <Descriptions.Item label="医院编码" span={1.5}>{hospital?.hoscode}</Descriptions.Item>
                <Descriptions.Item label="医院地址" span={1.5}>{hospital?.param.fullAddress}</Descriptions.Item>
                <Descriptions.Item label="坐车路线" span={3}>
                    {hospital?.route}
                </Descriptions.Item>
                <Descriptions.Item label="医院简介" span={3}>
                    {hospital?.intro}
                </Descriptions.Item>
            </Descriptions>

            <Descriptions title="预约规则" bordered className='mt'>
                <Descriptions.Item label="预约周期" span={1.5}>{bookingRule?.cycle}</Descriptions.Item>
                <Descriptions.Item label="放号时间" span={1.5}>{bookingRule?.releaseTime}</Descriptions.Item>
                <Descriptions.Item label="停挂时间" span={1.5}>{bookingRule?.quitDay}</Descriptions.Item>
                <Descriptions.Item label="退号时间" span={1.5}>{bookingRule?.quitTime}</Descriptions.Item>
                <Descriptions.Item label="预约规则" span={3}>
                    {bookingRule?.rule.map((item,index)=>(
                        <div key={index}>{item}</div>
                    ))}
                </Descriptions.Item>
            </Descriptions>

            <Button className='mt'>返回</Button>
        </Card>
    )
}

医院上下线管理

src/pages/hospital/hospitalList/hospitalList.tsx

tsx
<Space>
    <Button type='primary' onClick={()=>{navigate('/syt/hospital/hospitalList/show/' + row.id)}}>查看</Button>
    <Button type='primary'>排班</Button>
    <Button type='primary' onClick={()=>updateStatus(row.id, row.status ? 0: 1)}>{row.status ? '下线' : '上线'}</Button>
</Space>

// 改变医院状态函数
	const updateStatus = async (id:string, status:number)=>{
        
        await changeStatus(id, status);
        // 重新获取列表
        _getHospitalList();
    }

封装医院详情请求api:/src/api/hospital/hospitalList.ts

tsx
// 调整医院上下线
export const changeStatus = (id:string, status:number)=>{
    return request.get<any,null>(`/admin/hosp/hospital/updateStatus/${id}/${status}`);
}

排班功能

定义路由页面:src/pages/hospital/hospitalList/components/hospitalSchedule.tsx

tsx
import { Button, Card, Col, Pagination, Row, Table, Tag, Tree } from 'antd'
import React from 'react'

export default function HospitalSchedule() {
    return (
        <Card>
            123
        </Card>
    )
}

配置路由:src/route/index.ts

ts
const HospitalSchedule = lazy(() => import("@/pages/hospital/hospitalList/components/HospitalSchedule"));
{
    path: '/syt/hospital/hospitalList/schedule/:hoscode',
    meta: {
        title: '医院排班'
    },
    hidden: true, // 隐藏菜单栏
    element: load(HospitalSchedule)
}

绑定事件跳转:src/pages/hospital/hospitalList/hospitalList.tsx

tsx
<Button type='primary' onClick={()=>{navigate('/syt/hospital/hospitalList/schedule/' + row.hoscode)}}>排班</Button>

定义类型:src/api/hospital/model/hospitalListType.ts

ts
export interface IDepartmentItem {
    depcode: string;
    depname: string;
    children: IDepartmentList | null;
    disabled?:boolean;
}
//科室列表类型
export type IDepartmentList = IDepartmentItem[];

封装api:src/api/hospital/hospitalList.ts

ts
// 根据医院编号 hoscode 获取医院科室列表
export const getDepartmentList = (hoscode:string)=>{
    return request.get<any, IDepartmentList>(`/admin/hosp/department/${hoscode}`)
}

静态页面布局 src/pages/hospital/hospitalList/components/hospitalSchedule.tsx

tsx
import { Button, Card, Col, Pagination, Row, Table, Tag, Tree } from 'antd'
import { ColumnsType } from 'antd/lib/table'
import React from 'react'

// 获取视口的高度
let height = document.documentElement.clientHeight - 180

export default function HospitalSchedule() {
    // 列数据
    const columns: ColumnsType<any> = [
        {
            title: '序号'
        },
        {
            title: '职称'
        },
        {
            title: '号源时间'
        },
        {
            title: '总预约数'
        },
        {
            title: '剩余预约数'
        },
        {
            title: '挂号费(元)'
        },
        {
            title: '擅长技能'
        }
    ]
    return (
        <Card>
            <div>选择:北京人民医院 / 多发性硬化专科门诊 / 2023-07-28</div>
            {/* gutter 栅格间隙 这里组件来自 antd中的栅格组件 */}
            <Row className='mt' gutter={30}>
                <Col span={5}>
                    <div style={{ border: '1px solid #ddd', height, overflowY: 'scroll' }}>
                        <Tree
                            // // onSelect={onSelect}
                            // // onCheck={onCheck}
                            // treeData={departmentList as []}
                            // fieldNames={{
                            //     title:'depname',
                            //     key:'depcode'
                            // }}
                            // expandedKeys={expandedKeys}
                            // selectedKeys={[depcode as string]}
                        />
                    </div>
                </Col>
                <Col span={19}>
                    <Tag color="green">
                        <div>2023-07-28 周五</div>
                        <div>38 / 100</div>
                    </Tag>
                    <Tag>
                        <div>2023-07-28 周五</div>
                        <div>38 / 100</div>
                    </Tag>
                    <Tag>
                        <div>2023-07-28 周五</div>
                        <div>38 / 100</div>
                    </Tag>

                    <Pagination
                        defaultCurrent={6}
                        total={500}
                        className='mt'
                    />

                    <Table
                        className='mt'
                        pagination={false}
                        columns={columns}
                    />
                    
                    <Button className='mt'>返回</Button>
                </Col>

            </Row>
        </Card>
    )
}

获取数据渲染页面 src/pages/hospital/hospitalList/components/hospitalSchedule.tsx

tsx
import { getDepartmentList } from '@/api/hospital/hospitalList'
import { IDepartmentList } from '@/api/hospital/model/hospitalListType'
import { Button, Card, Col, Pagination, Row, Table, Tag, Tree } from 'antd'
import { ColumnsType } from 'antd/lib/table'
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'

// 获取视口的高度
let height = document.documentElement.clientHeight - 180

export default function HospitalSchedule() {
    // 列数据
    const columns: ColumnsType<any> = [
        {
            title: '序号'
        },
        {
            title: '职称'
        },
        {
            title: '号源时间'
        },
        {
            title: '总预约数'
        },
        {
            title: '剩余预约数'
        },
        {
            title: '挂号费(元)'
        },
        {
            title: '擅长技能'
        }
    ]

    // 获取参数
    const {hoscode} = useParams()
    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)
        // 禁用一级科室树节点,就是给所有的一级科室对象,添加一个disabled:true
        departmentList = departmentList.map(item=>{
            item.disabled = true;
            return item;
        })
        setDepartmentList(departmentList);// 设置科室列表状态数据
        // 展开所有的一级科室
        // 1. 获取所有一级科室 depcode组成的数组
        let expandedKeys = departmentList.map(item=>item.depcode)
        setExpandedKeys(expandedKeys);
        // 2. 处理默认选中科室
        let depname = (departmentList[0].children as IDepartmentList)[0].depname;
        let depcode = (departmentList[0].children as IDepartmentList)[0].depcode;
        // 设置科室名和科室code状态
        setDepname(depname);
        setDepcode(depcode);
    }
    // 生命周期
    useEffect(()=>{
        hoscode && _getDepartmentList();
    }, [])
    return (
        <Card>
            <div>选择:北京人民医院 / 多发性硬化专科门诊 / 2023-07-28</div>
            {/* gutter 栅格间隙 这里组件来自 antd中的栅格组件 */}
            <Row className='mt' gutter={30}>
                <Col span={5}>
                    <div style={{ border: '1px solid #ddd', height, overflowY: 'scroll' }}>
                        <Tree
                            // 设置数据源
                            treeData={departmentList as []}
                            // 自定义节点 title、key、children 的字段
                            fieldNames={{
                                title:'depname',
                                key:'depcode'
                            }}
                            // (受控)展开指定的树节点
                            expandedKeys={expandedKeys}
                            // (受控)设置选中的树节点
                            selectedKeys={[depcode as string]}
                        />
                    </div>
                </Col>
                <Col span={19}>
                    <Tag color="green">
                        <div>2023-07-28 周五</div>
                        <div>38 / 100</div>
                    </Tag>
                    <Tag>
                        <div>2023-07-28 周五</div>
                        <div>38 / 100</div>
                    </Tag>
                    <Tag>
                        <div>2023-07-28 周五</div>
                        <div>38 / 100</div>
                    </Tag>

                    <Pagination
                        defaultCurrent={6}
                        total={500}
                        className='mt'
                    />

                    <Table
                        className='mt'
                        pagination={false}
                        columns={columns}
                    />

                    <Button className='mt'>返回</Button>
                </Col>

            </Row>
        </Card>
    )
}

点击切换科室

在科室的页面绑定事件 src/pages/hospital/hospitalList/components/hospitalSchedule.tsx

tsx
<Tree
    // 设置数据源
    treeData={departmentList as []}
    // 自定义节点 title、key、children 的字段
    fieldNames={{
        title:'depname',
        key:'depcode'
    }}
    // (受控)展开指定的树节点
    expandedKeys={expandedKeys}
    // (受控)设置选中的树节点
    selectedKeys={[depcode as string]}
    onSelect={(selectedKeys: any,info: any)=>{ // a 是hoscode b是事件对象event
        setDepcode(info.node.depcode);
        setDepname(info.node.depname);
    }}
/>

排班日期分页

定义数据类型:src/api/hospital/model/hospitalListType.ts

ts

// 请求排班日期分页数据响应结果的类型
export interface IScheduleResponse {
    total: number;
    bookingScheduleList: IBookingScheduleList;
    baseMap: {
        hosname: string;
    }
}
// 排班日期每一项的类型
export interface IBookingScheduleItem {
    workDate: string;// 排班日期
    dayOfWeek: string;//星期几
    docCount: number;// 已预约人数
    reservedNumber: number;//总预约数
    availableNumber: number;// 剩余预约数
}
// 排班日期列表类型
export type IBookingScheduleList = IBookingScheduleItem[]

封装请求:src/api/hospital/hospitalList.ts

ts
// 获取医院科室排班日期分页列表数据
export const getScheduleList = (page: number, limit: number, hoscode: string, depcode: string) => {
    return request.get<any, IScheduleResponse>(`/admin/hosp/schedule/getScheduleRule/${page}/${limit}/${hoscode}/${depcode}`)
}

src/pages/hospital/hospitalList/components/HospitalSchedule.tsx

tsx


let [current, setCurrent] = useState<number>(1);
let [pageSize, setPageSize] = useState<number>(3);
let [total, setTotal] = useState<number>(10);
let [bookingScheduleList, setBookingScheduleList] = useState<IBookingScheduleList>([]);
let [hosname, setHosname] = useState<string>();
let [workDate, setWorkDate] = useState<string>();

//获取医院科室 排班日期数据
const _getScheduleList = async () => {
    let { total, bookingScheduleList, baseMap: { hosname } } = await getScheduleList(current, pageSize, hoscode as string, depcode as string);
    setTotal(total)
    setBookingScheduleList(bookingScheduleList)
    setHosname(hosname)
    // 设置排班日期状态
    setWorkDate(bookingScheduleList[0].workDate);
}

useEffect(() => {
    depcode && _getScheduleList();// 获取排班日期分页列表数据
}, [depcode, current, pageSize]);

<Col span={19}>
    {bookingScheduleList.map((item,index)=>(
        <Tag 
            color={workDate===item.workDate ? 'green':''} 
            key={item.workDate}
            onClick={()=>{setWorkDate(item.workDate)}}
            >
                <div>{item.workDate} {item.dayOfWeek}</div>
                <div>{item.availableNumber} / {item.reservedNumber}</div>
        </Tag>
    ))}

    <Pagination
        current={current}
        total={total}
        className='mt'
        pageSize={pageSize}
        onChange={(page:number, pageSize)=>{
            setCurrent(page);
            setPageSize(pageSize);
        }}
    />

    <Table
        className='mt'
        pagination={false}
        columns={columns}
    />

    <Button className='mt'>返回</Button>
</Col>

排班医生

定义数据类型:src/api/hospital/model/hospitalListType.ts

ts
/**
 * 排班医生对象类型
 */
export interface IDoctorItem {
    id: string;
    createTime: string;
    param: {
        dayOfWeek: string;
        depname: string;
        hosname:string;
    },
    hoscode:string;
    depcode: string;
    title: string;
    skill: string;
    workDate: string;
    reservedNumber: number;
    availableNumber: number;
    amount: number;
    status: number;
}

//排班医生列表类型
export type IDoctorList = IDoctorItem[];

封装请求:src/api/hospital/hospitalList.ts

ts
// 获取排班医生列表
export const getDoctorList = (hoscode: string, depcode: string, workDate: string) => {
    return request.get<any,IDoctorList>(`/admin/hosp/schedule/findScheduleList/${hoscode}/${depcode}/${workDate}`);
}

数据渲染:src/pages/hospital/hospitalList/components/HospitalSchedule.tsx

tsx
const columns: ColumnsType<any> = [
        {
            title: '序号',
            render(value:any, row:any, index:number){
                return (index + 1);
            }
        },
        {
            title: '职称',
            dataIndex:'title'
        },
        {
            title: '号源时间',
            width:120,
            dataIndex:'workDate'
        },
        {
            title: '总预约数',
            dataIndex:'reservedNumber'
        },
        {
            title: '剩余预约数',
            dataIndex:'availableNumber'

        },
        {
            title: '挂号费(元)',
            dataIndex:'amount'
        },
        {
            title: '擅长技能',
            dataIndex:'skill'
        }
    ]

// 医生列表
    let [doctorList, setDoctorList] = useState<IDoctorList>([]);

// 获取排班医生列表数据
const _getDoctorList = async ()=>{
let doctorList = await getDoctorList(hoscode as string, depcode as string, workDate as string);
// console.log('doctorList: ', doctorList);
setDoctorList(doctorList);
}

useEffect(()=>{// 组件挂载完成之后执行 + workDate变化后执行
    workDate && _getDoctorList();
}, [workDate]);

<Table
    className='mt'
    pagination={false}
    columns={columns}
    dataSource={doctorList}
    rowKey={'id'}
/>

数据字典

创建数据字典路由

创建数据字典组件:src/pages/cmn/dict/Dict.tsx

tsx
import { Card } from 'antd'
import Table, { ColumnsType } from 'antd/lib/table'
import React from 'react'

export default function Dict() {
    const columns:ColumnsType<any> = [
        {
            title:'名称'
        },
        {
            title:'编码'
        },
        {
            title:'值'
        },
        {
            title:'创建时间'
        }
    ]
    return (
        <Card>
            <Table
                columns={columns}
                pagination={false}
            />
        </Card>
    )
}

配置路由:src/route/index.ts

ts
{
    path:'/syt/cmn/',
    meta:{
        "title":"数据管理",
        "icon":<UnorderedListOutlined />
    },
    children:[
        {
            path:"/syt/cmn/dict",
            meta:{
                "title":"数据字典"
            },
            element:load(Dict)
        }
    ]
}

获取数据并渲染

创建数据字典组件:src/pages/cmn/dict/Dict.tsx

tsx
import { getDistrictList } from '@/api/hospital/hospitalList';
import { IDistrictList } from '@/api/hospital/model/hospitalListType'
import { RightOutlined, DownOutlined } from '@ant-design/icons'
import { Card } from 'antd'
import Table, { ColumnsType } from 'antd/lib/table'
import React, { useEffect, useState } from 'react'

export default function Dict() {
    const columns:ColumnsType<any> = [
        {
            title:'名称',
            dataIndex: 'name'
        },
        {
            title:'编码',
            dataIndex: 'dictCode'
        },
        {
            title:'值',
            dataIndex: 'value'
        },
        {
            title:'创建时间',
            dataIndex: 'createTime'
        }
    ]
    // 定义数据类型
    let [dictList, setDictList] = useState<IDistrictList>([]);
    // 获取省市区方法
    const _getDictList = async () => {
        let dictList = await getDistrictList(1);
        setDictList(dictList);
    }
    // 生命周期函数
    useEffect(() => {
        _getDictList();
    }, [])

    return (
        <Card>
            <Table
                rowKey={'id'}
                columns={columns}
                pagination={false}
                dataSource={dictList}
                expandable={{
                    expandIcon: ({ expanded, onExpand, record }) => {
                        // console.log('expanded: ', expanded);// 展开标识 boolean
                        // console.log('onExpand: ', onExpand);// 展开函数
                        // console.log('record: ', record);// 当前行对象
                        
                        // 首先判断当前行对象中是否有子节点
                        if(!record.hasChildren){
                            // 返回一个空标签
                            return <div style={{display:'inline-block',width:15}}></div>
                        }
                        return expanded ? (
                            <DownOutlined onClick={e => onExpand(record, e)} />
                        ) : (
                            <RightOutlined onClick={async e => {
                                // 获取当前行记录的子节点,加入record,进行展开
                                // 请求子节点的数据
                                if (!record.children?.length) {// record没有children属性或者record.children 数组的长度是 0,匹配逻辑判断
                                    let children = await getDistrictList(record.id);
                                    // 把这些子节点的信息添加到record对象children属性,就可以使用onExpand展开
                                    record.children = children;
                                }
                                onExpand(record, e)

                            }} />
                        )
                    }
                }}
            />
        </Card>
    )
}

打包上线部署

shell
1. 构建最终目录: npm run build ===> build
2. 部署需要解决的问题:
   1. 404 ==》 index.html
   2. 配置静态资源目录
   3. 设置代理服务器完成跨域

使用express配置http服务

ts
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());
// 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');
})

前端发送请求过程

image-20241022111503295

经验总结:

ts
未来三年 
学习:
找工作:跟编码水平无关、跟记忆力、脸皮、韧性、心态关系重大
工作前三个月:这里是带薪实训基地【60窝囊废 + 40 技术】
三个月:
1. 行业经验:
2. 真实的项目经验:
3. 编码能力,同行同事
半年后
1. 学历:本科  买一个 学信网可查的 成人教育  2-3年   2. 3 ==1
2. 年终奖: