本章我们在管理会员基本信息的基础上增加管理其个人简历的功能,以展示管理和主数据相关联的明细数据的的做法。

6.1 数据的准备工作

6.1.1 定义数据类型

services/type.d.ts中增加个人简历的数据定义

  1. type WorkExperience = {
  2. id?: number;
  3. memberId?: number;
  4. dateStart?: string;
  5. dateEnd?: string;
  6. company?: string;
  7. title?: string;
  8. }

6.1.2 编写网络请求函数

services/api/member.ts中编写三个新的网络请求函数

  1. export async function queryMemberExperience(
  2. params: {
  3. },
  4. options?: { [key: string]: any },
  5. ) {
  6. return request<TYPE.MemberList>('/api/member/experience/queryAll', {
  7. method: 'GET',
  8. params: {
  9. ...params,
  10. },
  11. ...(options || {}),
  12. });
  13. }
  14. export async function deleteExperience(id: number) {
  15. return request('/api/member/experience/delete', {
  16. method: 'POST',
  17. params: {
  18. id,
  19. }
  20. });
  21. }
  22. export async function saveExperience(data: TYPE.WorkExperience) {
  23. return request('/api/member/experience/save', {
  24. method: 'POST',
  25. data: {
  26. ...data
  27. }
  28. });
  29. }

为配合下文在EditableProTable中的保存功能,我们将更新(update)和创建(create)放在一个请求中,由后端根据数据id是否小于0来判断——id小于0的代表其为新创建的数据。

6.1.3 编写Mock模拟响应函数

mock/member.ts中编写模拟响应函数

  1. let workExperience: TYPE.WorkExperience[] = [] ;
  2. function queryExperience(req: Request, res: Response, u: string) {
  3. const { memberId = 0 } = req.query;
  4. const dataSource = workExperience.filter( data => data.memberId == memberId);
  5. const result = {
  6. data: dataSource,
  7. total: dataSource.length,
  8. success: true,
  9. };
  10. return res.json(result);
  11. }
  12. function deleteExperience(req: Request, res: Response, u: string) {
  13. const { id } = req.query;
  14. const result = {
  15. success: true,
  16. }
  17. workExperience = workExperience.filter( data => data.id != id);
  18. return res.json(result);
  19. }
  20. function saveExperience(req: Request, res: Response, u: string) {
  21. const { id } = req.body
  22. const result = {
  23. success: true,
  24. errorCode: -1,
  25. }
  26. const index = id > 0
  27. ? workExperience.findIndex(experience => experience.id === id )
  28. : -1
  29. //有两种情况index会小于0————前端传递小于0的id过来用于创新新的数据,或者前端的传递的id不存在
  30. if(index >= 0) {
  31. Object.assign(workExperience[index],req.body)
  32. } else {
  33. const record = {...req.body}
  34. let max = 0
  35. for( let experience of workExperience){
  36. const { id=0 } = experience;
  37. max = (max < id)? id : max
  38. }
  39. record.id = max + 99
  40. workExperience.push(record)
  41. }
  42. return res.json(result);
  43. }
  44. export default {
  45. 'GET /api/member/experience/queryAll': queryExperience,
  46. 'POST /api/member/experience/delete': deleteExperience,
  47. 'POST /api/member/experience/save': saveExperience,

6.2 明细数据增删改

我们使用附着在主数据更新界面的EditableProTable来实现明细数据的增删改。之所以只在主数据的更新界面管理明细数据,是因为明细数据依赖于主数据而存在,如果在创建主数据的界面中也置入管理明细数据的功能,那么就需要改变下文中即刻保存的逻辑,将EditableProTable存于页面数据中,直到整个对话框数据保存时,才真正保存到数据中。在页面保存数据的范例参见 EditableProTable - 可编辑表格 官方文档

本节的代码都在MemberList/components/MemberDataForm.tsx中完成。

6.2.1 引用和控制变量定义

下面是我们需要的引用

  1. import { useRef } from 'react';
  2. import { ProColumns, EditableProTable, ActionType } from "@ant-design/pro-table";
  3. import {
  4. queryMemberExperience,
  5. deleteExperience,
  6. saveExperience,
  7. } from '@/services/api/member'
  8. import {
  9. DeleteOutlined,
  10. EditOutlined,
  11. CheckOutlined,
  12. CloseOutlined,
  13. LoadingOutlined,
  14. } from '@ant-design/icons';
  15. import set from 'rc-util/lib/utils/set';

下面是我们需要的React Hook控制变量

  1. const experienceRef = useRef<ActionType>();
  2. const [lineSaving, setLineSaving] = useState<boolean>(false);
  3. //这个Hook变量是编辑状态的时候用来判断是编辑新增加的数据还是现有数据
  4. //新增加的数据不应该出现删除的图标
  5. const [addEditLine, setAddEditLine] = useState<boolean>(false);

6.2.2 可编辑表格的列定义

下面定义的列用于描述、约束可编辑表格各列的内容和行为

  1. const columns: ProColumns<TYPE.WorkExperience>[] = [
  2. {
  3. title: '开始日期',
  4. dataIndex: 'dateStart',
  5. valueType: 'date',
  6. width: '20%',
  7. //下面两个属性名字很相近,但是有不同的用途,不要搞串了
  8. fieldProps: {
  9. allowClear: false,
  10. },
  11. formItemProps: {
  12. //必填的日期型数据的校验规则必须指定类型
  13. //如果忘了就回空看一下5.3.3的说明
  14. rules: fieldRuls['requiredDate'],
  15. },
  16. },
  17. {
  18. title: '结束日期',
  19. dataIndex: 'dateEnd',
  20. valueType: 'date',
  21. width: '20%',
  22. formItemProps: {
  23. rules: fieldRuls['requiredDate'],
  24. }
  25. },
  26. {
  27. title: '单位或学校名称',
  28. dataIndex: 'company',
  29. width: '35%',
  30. formItemProps: {
  31. rules: fieldRuls['required'],
  32. }
  33. },
  34. {
  35. title: '工作职务',
  36. dataIndex: 'title',
  37. width: '15%',
  38. },
  39. {
  40. title: '操作',
  41. valueType: 'option',
  42. width: '10%',
  43. render: (text, experience, _, action) => [
  44. <a
  45. key="editable"
  46. onClick={() => {
  47. //startEditable是可编辑表格的API
  48. action?.startEditable?.(experience.id? experience.id: 0);
  49. }}
  50. >
  51. <EditOutlined />
  52. </a>,
  53. <a
  54. key="deleteItem"
  55. onClick={() => {
  56. try {
  57. deleteExperience(experience.id as number);
  58. experienceRef.current?.reload();
  59. } catch (e) {
  60. }
  61. }}
  62. >
  63. <DeleteOutlined />
  64. </a>,
  65. ],
  66. },
  67. ];

6.2.3 保存数据的响应函数

  1. const onSaveExperience = async (key:any, row:any) => {
  2. const data = {
  3. memberId: record.id,
  4. ... row,
  5. }
  6. try {
  7. saveExperience(data)
  8. experienceRef.current?.reload();
  9. } catch (error) {
  10. }
  11. }

6.2.4 表格的完整定义

把下面的表格定义放在对话框中现有组件(<ProFormText name="email"...)的下面

  1. <EditableProTable<TYPE.WorkExperience>
  2. rowKey="id"
  3. headerTitle="个人简历"
  4. actionRef={experienceRef}
  5. maxLength={5}
  6. columns={columns}
  7. request={async () => queryMemberExperience({
  8. memberId: record.id
  9. })}
  10. recordCreatorProps={{
  11. creatorButtonText: '增加一条简历',
  12. record: {
  13. id: -1,
  14. },
  15. onClick: () => setAddEditLine(true)
  16. }}
  17. editable={{
  18. type: 'multiple',
  19. onlyAddOneLineAlertMessage: '一次只能新增一行',
  20. onSave: onSaveExperience,
  21. actionRender: (row, config,defaultDoms) => {
  22. const { editorType, recordKey, form, onSave, newLineConfig } = config
  23. return [
  24. <a
  25. key="save"
  26. onClick={async () => {
  27. //下面的代码参考了GitHub上位于pro-components/packages/utils/src/useEditableArray/index.tsx
  28. //中的源码。官方没有提供自定义文字和图标的手段。文档中给的介绍也非常的简略。
  29. try {
  30. const isMapEditor = editorType === 'Map';
  31. const namePath = Array.isArray(recordKey) ? recordKey : [recordKey];
  32. setLineSaving(true)
  33. // @ts-expect-error
  34. await form.validateFields(namePath, {
  35. recursive: true,
  36. });
  37. const fields = form.getFieldValue(namePath);
  38. const experience = isMapEditor ? set(row, namePath, fields) : { ...row, ...fields };
  39. const res = await onSave?.(recordKey, experience, newLineConfig);
  40. setAddEditLine(false)
  41. setLineSaving(false)
  42. return res;
  43. } catch (e) {
  44. // eslint-disable-next-line no-console
  45. console.log(e);
  46. setLineSaving(false)
  47. return null;
  48. }
  49. }}
  50. >
  51. {lineSaving? <LoadingOutlined /> : <CheckOutlined /> }
  52. </a>,
  53. <a
  54. key="cancel"
  55. onClick={() => {
  56. setAddEditLine(false)
  57. config.cancelEditable(config.recordKey)
  58. }
  59. }
  60. ><CloseOutlined /></a>,
  61. !addEditLine &&
  62. <a
  63. key="deleteNow"
  64. onClick={() => {
  65. setAddEditLine(false)
  66. config.cancelEditable(config.recordKey)
  67. try {
  68. deleteExperience(row.id as number);
  69. experienceRef.current?.reload();
  70. } catch(e) {
  71. }
  72. }}
  73. >
  74. <DeleteOutlined />
  75. </a>,
  76. ]
  77. }
  78. }}
  79. />

上面的代码通过maxLength={5}来限制最多允许5条数据,通常我们并不需要限制明细数据的的数量,这里只是为了展现组件的能力而故意为之。

完成这些工作以后,我们就可以在编辑会员信息的对话框中看到管理个人简历的功能
image.png
下面是编辑的界面
image.png

6.2.5 完善日期逻辑校验

我们在7.2.1中约定其实日期和结束日期都是必填项,这在很多实际的场景中是不合适的,因为有尚未结束的情况。另外,和日期相关的还有如下两个逻辑需要考虑:

  1. 所有的日期都不能是尚未到来的
  2. 其实日期要早于结束日期

本节我们就来实现这些逻辑的检验和控制。

增加2个新的引用

  1. import moment from "moment";
  2. import { Alert } from 'antd';

定义新的Hook控制变量

  1. const [showWarning, setShowWarning] = useState<boolean>(false);
  2. const [alertMessage, setAlertMessage] = useState<string>('');

定义一个用来判断日期是否可选的函数

  1. const disabledDate = (current: any) => current > moment().endOf('day')

修改开始日期和结束日期的列定义

  1. {
  2. title: '开始日期',
  3. dataIndex: 'dateStart',
  4. valueType: 'date',
  5. width: '20%',
  6. fieldProps: {
  7. allowClear: false,
  8. disabledDate: disabledDate,
  9. },
  10. formItemProps: {
  11. rules: fieldRuls['requiredDate'],
  12. },
  13. },
  14. {
  15. title: '结束日期',
  16. dataIndex: 'dateEnd',
  17. valueType: 'date',
  18. width: '20%',
  19. fieldProps: {
  20. disabledDate: disabledDate,
  21. },
  22. },

在上面的定义中,我们给两个日期都加上是否可选的判断,去掉了结束日期的必选约束并且恢复了清空数据的功能。

EditableProTable的工具条属性中增加一个错误提示

  1. toolBarRender={() => [
  2. showWarning && <Alert
  3. message={alertMessage}
  4. type="error"
  5. />
  6. ]}

修改数据保存按钮的响应函数,增加比较两个日期的逻辑

  1. <a
  2. key="save"
  3. onClick={async () => {
  4. try {
  5. .....
  6. .....
  7. const fields = form.getFieldValue(namePath);
  8. const experience = isMapEditor ? set(row, namePath, fields) : { ...row, ...fields };
  9. if(moment(experience.dateEnd).diff(moment(experience.dateStart),'days') < 0) {
  10. setAlertMessage('结束日期必须晚于开始日期')
  11. setShowWarning(true)
  12. setLineSaving(false)
  13. } else {
  14. setShowWarning(false)
  15. const res = await onSave?.(recordKey, experience, newLineConfig);
  16. setAddEditLine(false)
  17. setLineSaving(false)
  18. return res;
  19. }
  20. return null;
  21. } catch(e) {
  22. ....

点击取消或删除的时候关闭警告对话框

  1. <a
  2. key="cancel"
  3. onClick={() => {
  4. + setShowWarning(false)
  5. setAddEditLine(false)
  6. config.cancelEditable(config.recordKey)
  7. }
  8. }
  9. ><CloseOutlined /></a>,
  10. !addEditLine &&
  11. <a
  12. key="deleteNow"
  13. onClick={() => {
  14. + setShowWarning(false)
  15. setAddEditLine(false)
  16. config.cancelEditable(config.recordKey)
  17. deleteExperience(row.id as number);
  18. experienceRef.current?.reload();
  19. }}
  20. >
  21. <DeleteOutlined />
  22. </a>,

现在我们打开日期选择控件会发现无法选择当天以后的日期,并且在两个日期逻辑关系不对的时候执行保存会得到错误信息
image.png

6.2.6 修改默认的风格定义

增加了管理明细数据的功能以后,对话框比以前搞了很多,而在默认的风格定义中,模态对话框的顶部是在100px的位置,为了改善视觉效果,我们重载一下默认的定义。新建一个文件components/MemberDataForm.less,置入如下内容

  1. .customSelect {
  2. :global {
  3. .ant-modal {
  4. top: 20px;
  5. }
  6. }
  7. }

MemberDataForm.tsx中引入

  1. import styles from './MemberDataForm.less';

在对画框上使用这个风格定义

  1. <ModalForm
  2. className={styles.customSelect}

这也的写法可以确保被重载的风格定义仅仅用户指定的组件而不影响其他的。

现在我再增加一条定义

  1. :global {
  2. .ant-cascader-menu {
  3. height: 240px;
  4. }
  5. }

这样的写法会影响当前所有使用它的组件

现在刷新页面(有时会需要清除浏览器缓存),执行新增或修改操作,可以看到对话框的顶部提高了,并且级联选择框的下拉菜单的高度也增加了。
image.png

6.3 数据展示中显示明细

本节我们回到MemberList/index.tsx,给数据展示中加入明细数据

首先引入查询函数

  1. import { queryMemberExperience } from '@/services/api/member'

然后定义显示用的数据列信息

  1. const experienceColumns: ProColumns<TYPE.WorkExperience>[] = [
  2. {
  3. title: '开始日期',
  4. dataIndex: 'dateStart',
  5. valueType: 'date',
  6. width: '20%',
  7. },
  8. {
  9. title: '结束年月',
  10. dataIndex: 'dateEnd',
  11. valueType: 'date',
  12. width: '20%',
  13. },
  14. {
  15. title: '单位或学校名称',
  16. dataIndex: 'company',
  17. width: '40%',
  18. },
  19. {
  20. title: '工作职务',
  21. dataIndex: 'title',
  22. width: '20%',
  23. },
  24. ];

最后在Drawer组件内部最后放置一个ProTable

  1. {currentRow?.id && (
  2. <ProTable
  3. headerTitle={'个人简历'}
  4. search={false}
  5. options={false}
  6. pagination={false}
  7. columns={experienceColumns}
  8. request={async () => queryMemberExperience({
  9. memberId: currentRow?.id
  10. })}
  11. />
  12. )}

顺便删除年收入列

现在我们看到的就是下面这样了
image.png

6.4 重要说明

本章展现的设计逻辑是在管理明细数据的完整功能嵌入在主数据的更新界面。明细数据在界面依附于主数据,但他的管理操作(增删改)与主数据界面的工作无关。也就是说对话框的三个按钮——取消、保存、重置都还只是影响主数据的内容,并不会影响到明细数据。

Ant Design ProComponents的文档很粗糙,经常需要对照Ant Design的文档来研究,而且有些内容在文档中也没有说明,需要自己去尝试甚至于去翻阅GitHub上的源码

EditableProTable组件推出的时间不长,还欠缺很多功能,比如没有提供很好的自定义编辑状态的链接文本或图标的功能,需要自己去参照源码来实现。

此外,我们要经常关注Ant Deisgn官方GitHub的各pro-components/packages/中否有重大的更新。

本章涉及的的内容非常多,所以一定要按照本章的内容逐步完成每个步骤,并且确实理解每条语句的作用,一定不要简单的复制粘贴

版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。