# react-demo **Repository Path**: novire/react-demo ## Basic Information - **Project Name**: react-demo - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-07-28 - **Last Updated**: 2025-08-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README https://www.nowcoder.com/discuss/779019083768508416?sourceSSR=users front tutorial from nowcoder html + css + js + react + ts -- q: ``` 我现在要设计一个界面,我现在描述一下,就是分为三个部分,上中下排列,第一部分就是3列4行的搜索框,搜索框右下侧有四个按钮设置(充值、新增、查询、展开使用伸缩组件),然后下面是一个图表(饼图、线形图)以及单数据展示(来源于搜索结果),最后一部分就是搜索结果,以表格的形式展示,列就是搜索的12个属性,另外最后一列是操作栏,要求固定,有编辑、详情、删除三个操作,点击编辑和详情都要从右侧弹出抽屉组件来,我仅仅设计一个界面是不是不太优雅,那应该设计几个部分呢,如何设计 ``` a: 好的,非常棒!您描述的这个界面是一个非常经典的后台管理系统模块的布局,包含搜索、数据概览和数据列表。您的顾虑非常正确:**将所有逻辑都塞到一个 `App` 组件中确实会变得“不太优雅”,难以维护和扩展。** 为了实现一个优雅、可维护、可扩展的 React 界面,我们应该遵循 **组件化 (Componentization)** 和 **分离关注点 (Separation of Concerns)** 的原则。 ### 推荐的设计部分(组件划分) 我建议将您的界面划分为以下几个核心组件: 1. **`EmployeeListPage` (或 `DashboardPage`)**: 容器组件/页面组件。 * **职责:** 负责整个页面的布局,管理核心数据(所有员工数据 `allEmployees` 和当前显示数据 `filteredEmployees`),协调子组件之间的通信,处理主要的业务逻辑(如搜索、新增、编辑、删除的实际数据操作)。 * **内部状态:** * `allEmployees`: 存储所有原始员工数据。 * `filteredEmployees`: 存储根据搜索条件过滤后的员工数据,用于表格展示。 * `loading`: 表格和搜索的加载状态。 * `drawerVisible`: 抽屉的显示隐藏状态。 * `editingRecord` / `detailingRecord`: 当前正在编辑/查看的员工记录。 * `formMode`: 'add' (新增), 'edit' (编辑), 'detail' (详情)。 * **不渲染 UI 细节:** 它将把渲染具体的 UI 任务委托给子组件。 2. **`SearchForm` (搜索表单组件)** * **职责:** 渲染搜索表单的 UI,管理搜索表单的字段值,处理表单的校验,并向上(给 `EmployeeListPage`)传递搜索条件。 * **内部状态:** * `collapsed`: 控制搜索表单区域的展开/收缩。 * **接收的 Props:** * `onSearch`: (values: SearchFormData) => void; 当点击搜索时,将表单值传给父组件。 * `onReset`: () => void; 点击重置时通知父组件。 * `onAdd`: () => void; 点击新增时通知父组件。 * `loading`: boolean; 控制搜索按钮的加载状态。 * **Ant Design 组件:** `Form`, `Input`, `Select`, `DatePicker`, `Row`, `Col`, `Button`, `Space`, `Card`。 3. **`DataOverview` (数据概览组件)** * **职责:** 渲染图表(饼图、线形图)和单数据展示区域。它只负责数据的可视化。 * **接收的 Props:** * `chartData`: 对应饼图/线形图的数据。 * `summaryData` / `singleMetric`: 用于单数据展示的汇总统计信息。 * **Ant Design 组件:** `Card`, `@ant-design/charts` (Pie, Line), `Statistic` (或普通 `div` + `p` 标签)。 4. **`EmployeeTable` (员工列表表格组件)** * **职责:** 渲染员工数据的表格,处理表格的排序、分页(如果不是父组件统一管理),并提供编辑、详情、删除的操作按钮。 * **接收的 Props:** * `dataSource`: TableDataType[]; 要显示的数据。 * `loading`: boolean; 表格的加载状态。 * `onEdit`: (record: TableDataType) => void; 点击编辑时,将当前记录传给父组件。 * `onDetail`: (record: TableDataType) => void; 点击详情时,将当前记录传给父组件。 * `onDelete`: (key: string) => void; 点击删除时,将记录的 key 传给父组件。 * `pagination`: any; 分页配置。 * `onChange`: (pagination, filters, sorter) => void; 表格变化回调(用于排序、筛选、分页)。 * **Ant Design 组件:** `Table`, `Button`, `Popconfirm`, `Space`。 5. **`EmployeeFormDrawer` (员工表单抽屉组件)** * **职责:** 渲染用于新增、编辑、查看详情的表单 UI,管理抽屉内部的表单状态和校验。 * **内部状态:** (通常没有太多独立状态,主要依赖 props) * **接收的 Props:** * `visible`: boolean; 控制抽屉显示隐藏。 * `onClose`: () => void; 关闭抽屉的回调。 * `record`: TableDataType | null; 如果是编辑/详情模式,传入要显示的记录数据。 * `mode`: 'add' | 'edit' | 'detail'; 告知组件当前是哪种操作模式。 * `onSave`: (values: EditFormData, mode: 'add' | 'edit') => void; 保存数据时的回调。 * **Ant Design 组件:** `Drawer`, `Form`, `Input`, `Select`, `DatePicker`, `Button`。 ### 为什么这样设计更优雅? 1. **分离关注点 (Separation of Concerns):** * `EmployeeListPage`:只关心整体数据流和状态管理。 * `SearchForm`:只关心搜索条件的输入和提交。 * `DataOverview`:只关心数据的可视化展示。 * `EmployeeTable`:只关心表格数据的展示和行级操作的触发。 * `EmployeeFormDrawer`:只关心新增/编辑/详情表单的输入和提交。 * 每个组件都有明确的单一职责,代码更清晰,更容易理解。 2. **可维护性 (Maintainability):** * 当需要修改搜索逻辑时,只需修改 `SearchForm` 和 `EmployeeListPage` 中的搜索处理函数。 * 当需要修改表格列或操作时,只需修改 `EmployeeTable`。 * 修改一个部分不会轻易影响到其他部分,降低了引入 Bug 的风险。 3. **可复用性 (Reusability):** * 例如,`SearchForm` 的部分逻辑(如通用输入框、选择框等)可以被进一步抽象为更小的通用组件。 * `EmployeeTable` 如果设计得通用,甚至可以在其他页面展示类似结构的表格。 * `EmployeeFormDrawer` 也可以用于其他实体的增删改查。 4. **可测试性 (Testability):** * 每个组件都可以独立地进行单元测试,因为它们的输入 (props) 和输出 (回调函数) 都是明确的。 5. **性能优化 (Performance Optimization):** * 通过 `React.memo` (针对函数组件) 可以很容易地优化子组件的渲染。例如,如果 `EmployeeTable` 的 `dataSource` 没有变化,它可以避免不必要的重新渲染。 * `useCallback` 和 `useMemo` 可以用于优化传递给子组件的回调函数和计算值,避免子组件因为引用变化而重新渲染。 ### 界面伪代码结构示例 ```jsx // src/pages/EmployeeListPage.tsx import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { message } from 'antd'; import dayjs from 'dayjs'; // 导入你的子组件 import SearchForm from '../components/SearchForm'; import DataOverview from '../components/DataOverview'; import EmployeeTable from '../components/EmployeeTable'; import EmployeeFormDrawer from '../components/EmployeeFormDrawer'; // 导入类型定义 (可以放在单独的 types.ts 文件中) import { TableDataType, SearchFormData, EditFormData } from '../types'; // 假设你将类型定义放在这里 // 模拟数据 (通常来自 API 请求) const mockTableData: TableDataType[] = [ // ... (同你原代码中的 mockTableData) { key: '1', name: '张三', age: 32, address: '北京市朝阳区', city: '北京', department: '技术部', salary: 15000, joinDate: '2022-01-15', status: '在职', phone: '13800138000', email: 'zhangsan@example.com', position: '高级工程师' }, { key: '2', name: '李四', age: 28, address: '上海市浦东新区', city: '上海', department: '产品部', salary: 12000, joinDate: '2022-03-20', status: '在职', phone: '13800138001', email: 'lisi@example.com', position: '产品经理' }, { key: '3', name: '王五', age: 35, address: '广州市天河区', city: '广州', department: '销售部', salary: 18000, joinDate: '2021-06-10', status: '离职', phone: '13800138002', email: 'wangwu@example.com', position: '销售总监' }, { key: '4', name: '赵六', age: 29, address: '深圳市南山区', city: '深圳', department: '技术部', salary: 16000, joinDate: '2021-09-12', status: '在职', phone: '13800138003', email: 'zhaoliu@example.com', position: '高级工程师' }, { key: '5', name: '钱七', age: 26, address: '杭州市西湖区', city: '杭州', department: '产品部', salary: 11000, joinDate: '2023-02-10', status: '试用期', phone: '13800138004', email: 'qianqi@example.com', position: '产品助理' }, ]; const EmployeeListPage: React.FC = () => { const [allEmployees, setAllEmployees] = useState([]); const [filteredEmployees, setFilteredEmployees] = useState([]); const [loading, setLoading] = useState(false); const [drawerVisible, setDrawerVisible] = useState(false); const [editingRecord, setEditingRecord] = useState(null); const [formMode, setFormMode] = useState<'add' | 'edit' | 'detail'>('add'); // 用于在组件挂载时初始化数据 useEffect(() => { // 模拟数据加载 setLoading(true); setTimeout(() => { setAllEmployees(mockTableData); setFilteredEmployees(mockTableData); setLoading(false); }, 300); }, []); // ---------- 数据过滤逻辑 (仍然放在这里,因为它依赖 allEmployees 和搜索条件) ---------- const filterData = useCallback((item: TableDataType, values: SearchFormData): boolean => { // ... (你原代码中的 filterData 函数逻辑) // 姓名模糊匹配 if (values.name && !item.name.includes(values.name)) { return false; } // ... 其他过滤条件 return true; }, []); // filterData 依赖的外部变量 (allEmployees) 不在此处,所以[] 可以。 // 保存当前的搜索条件,以便新增/编辑后重新应用过滤 const [currentSearchValues, setCurrentSearchValues] = useState({}); // 搜索处理函数 const handleSearch = useCallback((values: SearchFormData) => { setLoading(true); setCurrentSearchValues(values); // 保存当前搜索条件 // 模拟异步搜索 setTimeout(() => { const result = allEmployees.filter(item => filterData(item, values)); setFilteredEmployees(result); setLoading(false); message.success(`搜索完成,找到 ${result.length} 条记录`); }, 500); }, [allEmployees, filterData]); // 依赖 allEmployees 和 filterData // 重置处理函数 const handleReset = useCallback(() => { setCurrentSearchValues({}); // 清空搜索条件 setFilteredEmployees(allEmployees); message.info('已重置搜索条件'); }, [allEmployees]); // 新增按钮点击 const handleAdd = useCallback(() => { setEditingRecord(null); setFormMode('add'); setDrawerVisible(true); }, []); // 编辑按钮点击 (来自 EmployeeTable) const handleEdit = useCallback((record: TableDataType) => { setEditingRecord(record); setFormMode('edit'); setDrawerVisible(true); }, []); // 详情按钮点击 (来自 EmployeeTable) const handleDetail = useCallback((record: TableDataType) => { setEditingRecord(record); // 详情模式也用 editingRecord setFormMode('detail'); setDrawerVisible(true); }, []); // 删除按钮点击 (来自 EmployeeTable) const handleDelete = useCallback((key: string) => { const newData = allEmployees.filter(item => item.key !== key); setAllEmployees(newData); // 更新原始数据 // 重新应用当前过滤,确保表格数据也更新 const reFilteredData = newData.filter(item => filterData(item, currentSearchValues)); setFilteredEmployees(reFilteredData); message.success('删除成功'); }, [allEmployees, filterData, currentSearchValues]); // 抽屉保存处理 (来自 EmployeeFormDrawer) const handleDrawerSave = useCallback((values: EditFormData, mode: 'add' | 'edit') => { const newRecord: TableDataType = { key: mode === 'edit' ? editingRecord!.key : Date.now().toString(), name: values.name, age: values.age, address: values.address, city: values.city, department: values.department, salary: values.salary, joinDate: values.joinDate ? values.joinDate.format('YYYY-MM-DD') : '', status: values.status, phone: values.phone, email: values.email, position: values.position, }; let updatedAllEmployees; if (mode === 'edit') { updatedAllEmployees = allEmployees.map(item => item.key === editingRecord!.key ? newRecord : item ); message.success('编辑成功'); } else { updatedAllEmployees = [...allEmployees, newRecord]; message.success('新增成功'); } setAllEmployees(updatedAllEmployees); // 更新原始数据 // 重新应用当前过滤 const reFilteredData = updatedAllEmployees.filter(item => filterData(item, currentSearchValues)); setFilteredEmployees(reFilteredData); setDrawerVisible(false); // 关闭抽屉 setEditingRecord(null); // 清空编辑记录 }, [allEmployees, editingRecord, filterData, currentSearchValues]); // 抽屉关闭处理 const handleDrawerClose = useCallback(() => { setDrawerVisible(false); setEditingRecord(null); }, []); // 模拟图表数据和单数据展示 const chartData = useMemo(() => { // 这里的图表数据可以根据 filteredEmployees 进行聚合计算 // 例如:统计不同部门的人数,或者按月份统计入职人数等 // 简单示例:展示前几个月的销售额 (与你原代码类似,实际应与员工数据关联) return [ { type: '一月', sales: 38 }, { type: '二月', sales: 52 }, { type: '三月', sales: 61 }, { type: '四月', sales: 145 }, { type: '五月', sales: 48 }, { type: '六月', sales: 38 }, { type: '七月', sales: 61 }, { type: '八月', sales: 145 }, { type: '九月', sales: 48 }, { type: '十月', sales: 52 }, { type: '十一月', sales: 38 }, { type: '十二月', sales: 61 }, ]; }, [filteredEmployees]); // 依赖过滤后的员工数据,确保图表随之更新 const summaryData = useMemo(() => { // 例如:计算当前过滤结果的平均年龄、总薪资等 const totalSalary = filteredEmployees.reduce((sum, emp) => sum + emp.salary, 0); const avgAge = filteredEmployees.length > 0 ? (filteredEmployees.reduce((sum, emp) => sum + emp.age, 0) / filteredEmployees.length).toFixed(1) : 'N/A'; return { totalEmployees: filteredEmployees.length, averageSalary: `¥${(totalSalary / 1000).toFixed(1)}K`, averageAge: avgAge, }; }, [filteredEmployees]); return (
{/* 搜索表单 */} {/* 数据概览 (图表 + 单数据展示) */} {/* 员工列表表格 */} {/* 录入/编辑/详情抽屉 */}
); }; export default EmployeeListPage; ``` --- **子组件的伪代码结构 (示例):** ```jsx // src/components/SearchForm.tsx import React, { useState, useCallback } from 'react'; import { Form, Input, Select, DatePicker, Button, Row, Col, Space, Card } from 'antd'; import { PlusOutlined, UpOutlined, DownOutlined } from '@ant-design/icons'; import type { Dayjs } from 'dayjs'; const { RangePicker } = DatePicker; const { Option } = Select; interface SearchFormProps { onSearch: (values: SearchFormData) => void; onReset: () => void; onAdd: () => void; loading: boolean; } interface SearchFormData { name?: string; age?: string; department?: string; // ... 其他搜索字段 joinDate?: [Dayjs, Dayjs] | null; } const SearchForm: React.FC = ({ onSearch, onReset, onAdd, loading }) => { const [form] = Form.useForm(); const [collapsed, setCollapsed] = useState(false); const handleFinish = useCallback((values: SearchFormData) => { onSearch(values); }, [onSearch]); const handleFormReset = useCallback(() => { form.resetFields(); onReset(); }, [form, onReset]); const handleToggleCollapse = useCallback(() => { setCollapsed(prev => !prev); }, []); return (
{/* ... 其他固定搜索项 */} {!collapsed && ( <> {/* ... 其他可伸缩搜索项 */} )}
); }; export default React.memo(SearchForm); // 使用 React.memo 优化,防止父组件不必要的重新渲染 ``` --- **关键的优化点:** * **`React.memo`:** 应用于 `SearchForm`, `DataOverview`, `EmployeeTable` 等子组件。它会确保如果组件的 `props` 没有发生变化(浅比较),组件就不会重新渲染。 * **`useCallback`:** 用于包装传递给子组件的回调函数 (`handleSearch`, `handleReset`, `handleAdd`, `handleEdit`, `handleDelete`, `handleDrawerSave`, `handleDrawerClose`)。这可以确保这些函数在父组件重新渲染时不会每次都创建新的引用,从而配合 `React.memo` 避免子组件不必要的重新渲染。 * **`useMemo`:** 用于缓存计算结果,例如 `chartData` 和 `summaryData`。只有当其依赖项变化时,才会重新计算。 通过这样的组件划分和 Hook 优化,您的界面不仅在视觉上保持了整体性,在代码结构和维护性上也会变得非常优雅。 ----- 接入api 好的,非常理解!React Query(或称为 TanStack Query)是一个非常强大的数据管理库,但对于初次接触的人来说,它的一些概念和用法确实需要一些时间来消化。 同时,关于 **TypeScript (TS)**,是的,这段代码是使用 TypeScript 编写的。 ### 什么是 TypeScript (TS)? TypeScript 是 JavaScript 的一个超集,它添加了**类型系统**。这意味着你可以在代码中声明变量、函数参数和返回值的类型(比如 `string`、`number`、`boolean`、`TableDataType[]` 等)。 **为什么使用 TS?** 1. **错误检测:** 在代码运行之前就能发现很多潜在的错误(比如把数字赋给字符串变量)。 2. **代码可读性与维护性:** 类型定义让代码意图更清晰,团队协作时更容易理解他人的代码。 3. **智能提示:** IDE(如 VS Code)能够根据类型提供更准确的代码补全和提示,大大提高开发效率。 4. **重构安全:** 修改代码时,类型系统能帮助你确保改动没有破坏其他部分。 虽然刚开始学习 TS 会有一点门槛,但对于中大型项目来说,它带来的好处是巨大的。即使你对 TS 不熟悉,也可以先关注其核心逻辑,然后逐渐学习 TS 的语法。这里我会尽量在解释代码的同时,点出 TS 相关的部分。 --- 现在,我们来逐行详细解释上面那段 **React Query** 的代码。 ### 第一部分:`src/services/employeeApi.ts` 文件详解 这个文件是专门用来封装所有与员工相关的后端 API 请求的。它把网络通信的细节从你的 React 组件中分离出来,使得组件更纯粹,更容易测试和维护。 ```typescript // src/services/employeeApi.ts import axios from 'axios'; // 导入 Axios 库,一个流行的基于 Promise 的 HTTP 客户端 // 定义后端API的基础URL const API_BASE_URL = 'http://localhost:3000/api'; // [TS] 定义从后端获取的员工数据类型 // 后端通常会使用 'id' 作为唯一标识符,而前端表格可能使用 'key' export interface EmployeeApiDataType { id: string; // 后端数据的唯一标识符,通常是字符串形式的UUID或自增ID name: string; age: number; // TS: 明确指定age是数字类型 address: string; city: string; department: string; salary: number; joinDate: string; // TS: 日期通常以字符串形式从后端返回,例如 "YYYY-MM-DD" status: string; phone: string; email: string; position: string; } // [TS] 导入 SearchFormData 类型,它定义了搜索参数的结构 // SearchFormData 应该在你单独的 types.ts 文件中定义,这里假设它已被导入 import { SearchFormData } from '../types'; /** * 异步函数:请求所有员工数据或根据搜索条件过滤的员工数据 * @param params SearchFormData - 搜索参数对象,可选 * @returns Promise - 返回一个Promise,解析为员工数据数组 */ export const fetchEmployees = async (params: SearchFormData): Promise => { try { // 使用 axios 发送 GET 请求到 '/employees' 接口 // { params } 会将 params 对象自动转换为 URL 查询参数 (e.g., ?name=张三&department=技术部) const response = await axios.get(`${API_BASE_URL}/employees`, { params }); // [TS] response.data 是从后端返回的实际数据 // 这里对后端返回的数据进行映射,将其中的 'id' 字段转换为前端表格需要的 'key' 字段 // 这是为了保持前端表格组件和后端数据之间的兼容性 return response.data.map((item: any) => ({ // item: any 表示 item 可以是任何类型,这里只是为了演示,实际应该定义更精确的类型 key: item.id, // 将后端 'id' 映射到前端 'key' ...item, // 展开后端返回的所有其他字段 })); } catch (error) { // 捕获请求过程中发生的错误 console.error('Error fetching employees:', error); // 重新抛出错误,以便在调用此函数的组件中也能捕获和处理 throw error; } }; /** * 异步函数:新增员工 * @param employeeData Omit - 新员工的数据,Omit 表示从 EmployeeApiDataType 中排除 'id' 字段,因为新增时id由后端生成 * @returns Promise - 返回一个Promise,解析为新增成功后的员工数据 */ export const addEmployee = async (employeeData: Omit): Promise => { try { // 使用 axios 发送 POST 请求到 '/employees' 接口,employeeData 作为请求体 const response = await axios.post(`${API_BASE_URL}/employees`, employeeData); // 返回新增的数据,同样将其 id 映射为 key return { ...response.data, key: response.data.id }; } catch (error) { console.error('Error adding employee:', error); throw error; } }; /** * 异步函数:更新员工 * @param id string - 要更新员工的唯一标识符 * @param employeeData Partial - 部分员工数据,Partial 表示所有字段都是可选的,因为更新可能只修改部分字段 * @returns Promise - 返回一个Promise,解析为更新成功后的员工数据 */ export const updateEmployee = async (id: string, employeeData: Partial): Promise => { try { // 使用 axios 发送 PUT 请求到 '/employees/:id' 接口,employeeData 作为请求体 const response = await axios.put(`${API_BASE_URL}/employees/${id}`, employeeData); // 返回更新后的数据,映射 id 到 key return { ...response.data, key: response.data.id }; } catch (error) { console.error('Error updating employee:', error); throw error; } }; /** * 异步函数:删除员工 * @param id string - 要删除员工的唯一标识符 * @returns Promise - 返回一个Promise,解析为void(空),表示没有返回数据 */ export const deleteEmployee = async (id: string): Promise => { try { // 使用 axios 发送 DELETE 请求到 '/employees/:id' 接口 await axios.delete(`${API_BASE_URL}/employees/${id}`); } catch (error) { console.error('Error deleting employee:', error); throw error; } }; ``` **总结 `employeeApi.ts`:** * **职责单一:** 专注于与员工相关的后端通信。 * **Promise 和 `async/await`:** 处理异步操作。 * **错误处理:** 使用 `try...catch` 捕获网络错误,并重新抛出以便上层处理。 * **数据映射:** 负责将后端数据结构转换为前端需要的结构(`id` 到 `key`)。 * **TypeScript 类型:** 明确定义了函数的参数和返回值类型,增强了代码的健壮性和可读性。 --- ### 第二部分:`src/pages/EmployeeListPage.tsx` 文件详解 (React Query 部分) 这个文件是整个员工管理页面的容器组件,它负责协调数据流、管理页面状态以及调用 API 服务。 ```typescript // src/pages/EmployeeListPage.tsx // ---------------------------------------- 1. 导入必要的模块 ---------------------------------------- import React, { useState, useEffect, useCallback, useMemo } from 'react'; // React 核心库和 Hooks import { message } from 'antd'; // Ant Design 的全局消息提示组件 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; // 从 React Query 库中导入核心 Hooks import dayjs from 'dayjs'; // 导入 dayjs 库,用于日期处理 // 导入你定义的子组件 import SearchForm from '../components/SearchForm'; import DataOverview from '../components/DataOverview'; import EmployeeTable from '../components/EmployeeTable'; import EmployeeFormDrawer from '../components/EmployeeFormDrawer'; // 导入 API 服务和类型定义 import { fetchEmployees, addEmployee, updateEmployee, deleteEmployee, EmployeeApiDataType } from '../services/employeeApi'; import { TableDataType, SearchFormData, EditFormData } from '../types'; // 你的前端自定义类型定义 // ---------------------------------------- 2. 主组件定义 ---------------------------------------- const EmployeeListPage: React.FC = () => { // [React Query] 获取 QueryClient 实例。它是 React Query 的核心,用于管理缓存和发起请求。 const queryClient = useQueryClient(); // ---------------------------------------- 3. 组件状态管理 ---------------------------------------- // 用于保存当前的搜索条件,当这个状态改变时,useQuery 会自动重新发起请求 const [currentSearchValues, setCurrentSearchValues] = useState({}); // 控制抽屉(Drawer)的显示/隐藏 const [drawerVisible, setDrawerVisible] = useState(false); // 存储当前正在编辑或查看的员工记录数据 const [editingRecord, setEditingRecord] = useState(null); // 控制抽屉的模式:'add'(新增),'edit'(编辑),'detail'(详情) const [formMode, setFormMode] = useState<'add' | 'edit' | 'detail'>('add'); // ---------------------------------------- 4. 辅助函数:API数据到前端表格数据转换 ---------------------------------------- // 使用 useCallback 记住这个函数,避免在组件重新渲染时重复创建 const mapApiDataToTableData = useCallback((apiData: EmployeeApiDataType[]): TableDataType[] => { return apiData.map(item => ({ key: item.id, // 将后端返回的 'id' 映射为前端表格需要的 'key' ...item, // 展开后端返回的其他所有字段 })); }, []); // 依赖项为空数组,表示这个函数只会在组件初次渲染时创建一次 // ---------------------------------------- 5. React Query Hooks - 数据获取 (GET) ---------------------------------------- // useQuery Hook 用于获取数据。它会自动处理缓存、加载状态、错误状态等。 const { data: employeesData, // [解构赋值] 将 useQuery 返回的数据命名为 employeesData (即你表格需要的数据) isLoading, // [状态] 是否正在加载数据(初次加载或重新获取) isError, // [状态] 是否发生了错误 error // [错误对象] 如果发生错误,错误信息会在这里 } = useQuery({ // [queryKey] 这是一个数组,用于唯一标识这个查询。 // 当 queryKey 的任何元素发生变化时,useQuery 会自动重新执行 queryFn(重新获取数据)。 // 这里包含 'employees' 字符串和 currentSearchValues 对象,意味着搜索条件变化时会触发重新获取。 queryKey: ['employees', currentSearchValues], // [queryFn] 这是一个异步函数,负责实际的数据请求。它必须返回一个 Promise。 queryFn: async () => { // 对搜索参数进行预处理,例如将 dayjs 对象转换为后端期望的日期字符串格式 const paramsToSend = { ...currentSearchValues, // 展开所有搜索条件 joinDate: currentSearchValues.joinDate // 如果有 joinDate (日期范围选择器) ? [ currentSearchValues.joinDate[0]?.format('YYYY-MM-DD'), // 转换为 'YYYY-MM-DD' 格式 currentSearchValues.joinDate[1]?.format('YYYY-MM-DD') ] : undefined, // 如果没有,则为 undefined,不会发送给后端 }; // 调用我们在 employeeApi.ts 中定义的 API 服务函数 return fetchEmployees(paramsToSend); }, // [select] 这是一个可选的转换函数,在数据从 queryFn 返回后,但存储到缓存之前执行。 // 这里我们使用 mapApiDataToTableData 将后端数据格式转换为前端表格需要的格式。 select: mapApiDataToTableData, // [onError] 数据获取失败时的回调函数 onError: (err) => { // Ant Design 的消息提示,显示错误信息 message.error('获取员工数据失败:' + (err as Error).message); // err as Error 是 TS 类型断言 }, // [onSuccess] 数据成功获取时的回调函数 onSuccess: (data) => { // 只有在非初始加载(即通过搜索触发的)时才显示“搜索完成”消息 if (Object.keys(currentSearchValues).length > 0) { message.success(`搜索完成,找到 ${data.length} 条记录`); } else { message.success(`初始加载完成,找到 ${data.length} 条记录`); } } }); // ---------------------------------------- 6. React Query Hooks - 数据修改 (POST/PUT/DELETE) ---------------------------------------- // useMutation Hook 用于执行数据修改操作(新增、编辑、删除)。 // 它提供 mutationFn (实际执行的函数) 和 onSuccess/onError 等回调。 // [新增/编辑] 员工的 Mutation const addOrUpdateMutation = useMutation({ // [mutationFn] 这是一个异步函数,负责实际的修改请求。 // 它接收一个包含 values, mode, recordKey 的对象作为参数。 mutationFn: async ({ values, mode, recordKey }: { values: EditFormData, mode: 'add' | 'edit', recordKey?: string }) => { // 对要发送的数据进行预处理,特别是日期字段 const employeeDataToSend = { ...values, // 展开表单值 joinDate: values.joinDate ? values.joinDate.format('YYYY-MM-DD') : '', // 格式化日期 }; // 根据 mode 判断是调用新增 API 还是更新 API if (mode === 'edit' && recordKey) { return updateEmployee(recordKey, employeeDataToSend); // 调用更新 API } else { return addEmployee(employeeDataToSend); // 调用新增 API } }, // [onSuccess] 数据修改成功时的回调函数 onSuccess: () => { // [关键点] 使特定 queryKey 的缓存失效。 // 这会告诉 React Query:名为 'employees' 的数据现在是旧的了,下次访问它时需要重新获取。 // 由于我们的 useQuery 监听了 'employees' 的 queryKey,它会自动重新发起请求,从而更新表格。 queryClient.invalidateQueries({ queryKey: ['employees'] }); message.success(`${formMode === 'edit' ? '编辑' : '新增'}成功!`); setDrawerVisible(false); // 关闭抽屉 setEditingRecord(null); // 清空编辑中的记录 }, // [onError] 数据修改失败时的回调函数 onError: (err) => { message.error(`${formMode === 'edit' ? '编辑' : '新增'}失败:` + (err as Error).message); }, }); // [删除] 员工的 Mutation const deleteMutation = useMutation({ // [mutationFn] 接收要删除的员工的 key mutationFn: (key: string) => deleteEmployee(key), // 调用删除 API // [onSuccess] 删除成功时的回调 onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['employees'] }); // 同样使 'employees' 查询失效,触发重新获取 message.success('删除成功!'); }, // [onError] 删除失败时的回调 onError: (err) => { message.error('删除失败:' + (err as Error).message); }, }); // ---------------------------------------- 7. 事件处理函数 ---------------------------------------- // 搜索处理函数。使用 useCallback 优化,避免重复创建函数。 const handleSearch = useCallback((values: SearchFormData) => { // [关键点] 更新 currentSearchValues 状态。 // 由于 currentSearchValues 是 useQuery 的 queryKey 的一部分,useQuery 会自动检测到变化并重新发起请求。 setCurrentSearchValues(values); }, []); // 依赖项为空,因为不依赖外部变量变化 // 重置处理函数。 const handleReset = useCallback(() => { setCurrentSearchValues({}); // 将搜索条件清空,这也会触发 useQuery 重新加载所有数据 message.info('已重置搜索条件'); }, []); // 依赖项为空 // 新增按钮点击处理函数 const handleAdd = useCallback(() => { setEditingRecord(null); // 清空编辑记录 setFormMode('add'); // 设置模式为新增 setDrawerVisible(true); // 显示抽屉 }, []); // 依赖项为空 // 编辑按钮点击处理函数 (来自 EmployeeTable 组件的回调) const handleEdit = useCallback((record: TableDataType) => { setEditingRecord(record); // 存储要编辑的记录 setFormMode('edit'); // 设置模式为编辑 setDrawerVisible(true); // 显示抽屉 }, []); // 依赖项为空 // 详情按钮点击处理函数 (来自 EmployeeTable 组件的回调) const handleDetail = useCallback((record: TableDataType) => { setEditingRecord(record); // 存储要查看的记录 setFormMode('detail'); // 设置模式为详情 setDrawerVisible(true); // 显示抽屉 }, []); // 依赖项为空 // 删除按钮点击处理函数 (来自 EmployeeTable 组件的回调) const handleDelete = useCallback((key: string) => { // [关键点] 调用 deleteMutation 的 mutate 方法来触发删除操作。 // useMutation 会自动处理加载状态、错误、并在成功后调用 onSuccess (从而刷新数据)。 deleteMutation.mutate(key); }, [deleteMutation]); // 依赖 deleteMutation 实例,因为它来自 Hook // 抽屉保存处理函数 (来自 EmployeeFormDrawer 组件的回调) const handleDrawerSave = useCallback((values: EditFormData, mode: 'add' | 'edit') => { // [关键点] 调用 addOrUpdateMutation 的 mutate 方法来触发新增或编辑操作。 // useMutation 会自动处理一切:发送请求、更新加载状态、在成功后使数据失效并刷新。 addOrUpdateMutation.mutate({ values, mode, recordKey: editingRecord?.key }); }, [addOrUpdateMutation, editingRecord]); // 依赖 addOrUpdateMutation 和 editingRecord // 抽屉关闭处理函数 const handleDrawerClose = useCallback(() => { setDrawerVisible(false); // 隐藏抽屉 setEditingRecord(null); // 清空编辑记录 }, []); // 依赖项为空 // ---------------------------------------- 8. 派生数据:图表和单数据展示 ---------------------------------------- // 使用 useMemo 优化图表数据的计算,只有当 employeesData 变化时才重新计算 const chartData = useMemo(() => { // 这里的逻辑应该是根据 employeesData (即 useQuery 返回的已过滤和映射后的数据) 进行聚合计算 // 例如:统计不同部门的人数,或者按月份统计入职人数等 // 实际实现时,根据 employeesData 来生成你的饼图、线形图数据 return employeesData ? [] : []; // 如果 employeesData 为 null/undefined,则返回空数组 }, [employeesData]); // 依赖 useQuery 返回的 employeesData // 使用 useMemo 优化汇总数据的计算 const summaryData = useMemo(() => { // 例如:计算当前过滤结果的平均年龄、总薪资等 // 实际实现时,根据 employeesData 来计算你的单数据展示值 return employeesData ? {} : {}; // 如果 employeesData 为 null/undefined,则返回空对象 }, [employeesData]); // 依赖 useQuery 返回的 employeesData // ---------------------------------------- 9. 渲染 UI ---------------------------------------- return (
{/* 搜索表单组件,并传递必要的 props */} {/* 数据概览组件,并传递图表和汇总数据 */} {/* 员工列表表格组件,并传递数据、加载状态和操作回调 */} {/* 录入/编辑/详情抽屉组件,并传递显示状态、数据、模式和保存回调 */}
); }; export default EmployeeListPage; // 导出组件 ``` --- ### React Query 核心概念再回顾 1. **`queryKey` (`['employees', currentSearchValues]`):** * **唯一标识符:** 它是 React Query 用来标识和管理缓存数据的键。 * **依赖数组:** 就像 `useEffect` 的依赖数组一样,当 `queryKey` 中的任何值发生变化时,`useQuery` 会认为数据可能已过期,并自动重新执行 `queryFn` 来获取最新数据。这是实现搜索条件变化时自动刷新表格的关键。 2. **`queryFn` (e.g., `() => fetchEmployees(paramsToSend)`):** * **数据源:** 实际执行数据获取逻辑的函数,必须返回一个 Promise。它只负责从后端获取数据。 3. **`useQuery` 的返回值 (e.g., `data`, `isLoading`, `isError`):** * **自动状态管理:** React Query 会自动为你管理加载状态 (`isLoading`)、错误状态 (`isError`, `error`) 和数据 (`data`),你不需要手动 `useState` 来管理这些。 4. **`useMutation`:** * **数据修改:** 用于执行创建、更新、删除等会改变后端数据的操作。 * **`mutationFn`:** 实际执行数据修改的异步函数。 * **`mutate()`:** 调用 `useMutation` 返回的 `mutate` 方法来触发 `mutationFn` 的执行。 * **`onSuccess` / `onError`:** 在 mutation 成功或失败后执行的副作用,比如显示消息提示。 5. **`queryClient.invalidateQueries({ queryKey: ['employees'] })`:** * **数据刷新:** 这是 React Query 最“魔幻”且强大的功能之一。当你成功新增、编辑或删除一条数据后,你不需要手动去重新调用 `fetchData` 或重新过滤 `allEmployees`。你只需要告诉 React Query:“嘿,我的 `employees` 相关的数据可能已经过期了!” `queryClient.invalidateQueries` 就会标记 `employees` 这个 `queryKey` 对应的缓存数据为“过期”,**当 `useQuery` 再次渲染或被监听时,它会发现数据过期,并自动重新发起请求获取最新数据**,从而自动更新你的表格。 希望这次的详细解释能帮助您更好地理解代码,特别是 React Query 和 TypeScript 的作用!