4.1 在列表页定义更新数据的对话框

本节我们在当前列表页src/pages/MemberList/index.tsx增加一个用来更新数据的对话框。为了聚焦开发的原理,我们仅操作三个字段:姓名、性别、政治面貌。

4.1.1 准备必要的变量和函数

本节主要是定义各种对话框需要的变量和函数。其中变量用到了各种React Hook,如果你还不能理解这些内容,那么你需要重新学习本学院中关于React Hook的资料

  • 首先完成必要的引用

    1. import React, { useState, useRef, useEffect } from 'react';
    2. import { Drawer, message, Button, Modal, Form } from 'antd';
    3. import ProForm, { ModalForm, ProFormText, ProFormSelect } from '@ant-design/pro-form';
  • 因为在对话框的下拉列表中也需要用columns: ProColumns中的那些代码,所以我们先把这些代码显式的定义成变量

    1. const degreeEnum = {
    2. 0: ' ',
    3. 1: '学士学位',
    4. 2: '硕士学位',
    5. 3: '博士学位',
    6. }
    7. const genderEnum = {
    8. 0: '未选',
    9. 1: '男',
    10. 2: '女',
    11. }

    然后修改columns中对应的定义

    1. {
    2. title: '性别',
    3. dataIndex: 'gender',
    4. hideInForm: true,
    5. //改成引用前文定义的变量
    6. valueEnum: genderEnum,
    7. },
    8. {
    9. title: '最高学位',
    10. dataIndex: 'degree',
    11. hideInTable: true,
    12. //改成引用前文定义的变量
    13. valueEnum: degreeEnum,
    14. },
  • 定义一个State Hook变量用来对应对话框显式的状态

    1. const [dataFormVisible, setDataFormVisible] = useState<boolean>(false);
  • 使用useForm获得一个form实例,在下文用来定义对话框中的form

    1. const [formRef] = Form.useForm();
  • 定义一个设置对话框内容初始状态的函数

    1. const initialValues = () => {
    2. return {
    3. realName: currentRow?.realName,
    4. gender: `${currentRow?.gender}`,
    5. degree: `${currentRow?.degree}`,
    6. };
    7. }

    这里有个非常不小的“坑”——尽管我们给valueEnum设置的键值是数字类型,但它的本质还是是被当成字符串的,所有在转换成options以后,每个选项的value都变成成了字符串,这样如果我们直接把代码值放到initialValues里面,那么显示的时候因为找不到匹配的选项,他会直接把数字显示在那里。

    这里我们的做法是把代码值从数字改成了字符串来兼容这个问题。下一节,我们会展示另外一种更好的办法

  • 定义一个依赖对话框状态的Effect Hook,当对话框可见的时候重置所有的内容
    1. useEffect(() => {
    2. if (formRef && dataFormVisible) formRef.resetFields();
    3. }, [dataFormVisible]);

    这里是要填平另外一个非常巨大的“坑”——默认的情况下,已经渲染过的form不会自动销毁和重建,当其再次被显示的时候,也不会自动的重新执行初始化工作。如果不加处理,再次打开这个对话框的时候,我们会看到上次关闭前的状态而不是新的内容。

这个问题通常有两种解法:

  1. 用上面定义的dataFormVisible变量在对话框隐藏以后销毁它(每次需要的时候都重新创建)
  2. 每次使用对话框前显式执行重置对话框内容的动作resetFields

第1种方式效率较低,所以我们倾向于使用第2种方式。通常的想法是在设置控制对话框显示的开关变量(dataFormVisible)为true后调用resetFields。这里我们应用Effect Hook实现同样的控制逻辑,相比之下这种方式看起来更加的优雅。尤其如果有多个途径都可以激活对话框的时候,这种方式更易于维护,也大大降低了出现为人为失误的可能。

4.1.2 为每行数据增加修改数据的链接

colums定义的最后一列(option部分)的render属性函数中增加一个用来显示对话框的链接

  1. <a
  2. key="update"
  3. onClick={() => {
  4. //想想看,这个是为什么?
  5. setShowDetail(false);
  6. setCurrentRow(record);
  7. //用了上面的useEffect,就不用在这里做重置了
  8. //formRef.resetFields()
  9. setDataFormVisible(true)
  10. }}
  11. >修改</a>,

image.png

4.1.3 实现一个简单的模态数据对话框

这个可以放在Drawer的前面

  1. <ModalForm
  2. form={formRef}
  3. title={'修改会员信息'}
  4. width="400px"
  5. visible={dataFormVisible}
  6. //注意:这里是执行初始化函数获得返回值,不要写成initialValues={initialValues}
  7. initialValues={initialValues()}
  8. onVisibleChange={ (visible) => {
  9. if(!visible)
  10. setCurrentRow(undefined);
  11. setDataFormVisible(visible)
  12. }
  13. }
  14. onFinish={async (value) => {
  15. message.warning('没有实现保存功能');
  16. setCurrentRow(undefined);
  17. setDataFormVisible(false);
  18. }}
  19. >
  20. <ProFormText name="realName" label="会员姓名" />
  21. <ProForm.Group>
  22. <ProFormSelect width="xs" name="gender" label="性别"
  23. valueEnum={genderEnum} allowClear={false} />
  24. <ProFormSelect width="xs" name="degree" label="最高学历"
  25. valueEnum={degreeEnum} allowClear={false} />
  26. </ProForm.Group>
  27. </ModalForm>

这时候在数据后点击修改,就可以看到定义好的对话框(当然,他现只能显示,还不能真正的修改数据)
image.png

4.2 把对话框定义为组件

把对话框的代码和管理列表数据的的代码放到一起增加了代码的复杂度,也没法复用。所以接下来我们定义一个管理会员数据的对话框组件,这样有利于保持列表主程序的代码整洁,也有利于在其他地方复用

4.2.1 将代码定义放到独立的文件中

因为主程序和对话框需要相同的代码定义,为了提高可维护性,减少出错可能,我们在定义组件前先把代码定义放到独立的文件中。

建立新文件src/services/member.enum.ts

  1. export const genderEnum = {
  2. 0: '未选',
  3. 1: '男',
  4. 2: '女',
  5. }
  6. export const degreeEnum = {
  7. 0: ' ',
  8. 1: '学士学位',
  9. 2: '硕士学位',
  10. 3: '博士学位',
  11. }

然后在主程序中删除原来的定义并从这个文件中引用

  1. import { genderEnum, degreeEnum } from '@/services/member.enum';

4.2.2 定义一个通用的数据对话框组件

创建一个目录src/pages/MemberList/components,在该目录新建一个文件MemberDataForm.tsx

  1. import { message, Form } from "antd";
  2. import { useEffect } from "react";
  3. import ProForm, { ModalForm, ProFormText, ProFormSelect } from '@ant-design/pro-form';
  4. import { genderEnum, degreeEnum } from '@/services/member.enum';
  5. const DataForm = (props:{ [key: string]: any }) => {
  6. const { visible, operation, doClose, record } = props;
  7. const [form] = Form.useForm();
  8. useEffect(() => {
  9. //这里我们用了另外一种方式,当对话框可见的时候,显式的设置他的值
  10. if (form && visible) form.setFieldsValue(record);
  11. }, [visible]);
  12. const getOptionsFormValueEnum = (valueEnum: Object) => {
  13. return (Object.entries(valueEnum)).map( ([key, value]) => ({
  14. value: parseInt(key),
  15. label: value,
  16. }))
  17. }
  18. const handleResponse = () => {
  19. message.warning('没有实现保存功能');
  20. doClose();
  21. };
  22. const handleCancel = () => {
  23. doClose();
  24. };
  25. const onFinish = async () => {
  26. handleResponse();
  27. };
  28. const getTitle = () => {
  29. switch (operation) {
  30. case "create":
  31. return "增加会员";
  32. case "edit":
  33. return "修改信息";
  34. default:
  35. return "类型错误";
  36. }
  37. };
  38. return (
  39. <ModalForm
  40. form={form}
  41. title={getTitle()}
  42. visible={visible}
  43. onVisibleChange={(visible) => {
  44. if (!visible) handleCancel();
  45. }}
  46. onFinish={onFinish}
  47. >
  48. <ProFormText name="realName" label="会员姓名" />
  49. <ProForm.Group>
  50. <ProFormSelect width="xs" name="gender" label="性别"
  51. options={getOptionsFormValueEnum(genderEnum)} allowClear={false} />
  52. <ProFormSelect width="xs" name="degree" label="最高学历"
  53. options={getOptionsFormValueEnum(degreeEnum)} allowClear={false} />
  54. </ProForm.Group>
  55. </ModalForm>
  56. );
  57. };
  58. export default DataForm;

可以看到,这个组件的逻辑和上一节的基本一致,但增加了一些更优雅的设计。

4.2.3 对主程序的修改

  • 首先在主程序src/pages/MemberList/index.tsx中引用新的组件

    1. import DataForm from './components/MemberDataForm';
  • 删除上一节定义的各种Hook和初始化函数,增加如下的State Hook

    1. const [dataFormState, setFormState] = useState({
    2. showModal: false,
    3. operation: '',
    4. record: {}
    5. });
  • 把链接的onClick函数改成下面的样子

    1. <a
    2. key="update"
    3. onClick={() => {
    4. setShowDetail(false);
    5. setFormState({ showModal: true, operation: 'edit', record: record })
    6. }}
    7. >修改</a>,
  • 删除之前ModalForm定义,改成新定义的组件

    1. <DataForm
    2. visible={dataFormState.showModal}
    3. record={dataFormState.record}
    4. operation={dataFormState.operation}
    5. doClose={() =>
    6. setFormState({ showModal: false, operation: '', record: {}})
    7. }
    8. />
  • 根据Visual Studio Code的警告信息删除不再使用的引用。

现在看到的对话框应该是这个样子的
image.png

4.3 增加更多的字段

我们在上一节中通过setFieldsValue(record)把数据的所有内容都注入到对话框了,我们在本节把其他的字段也都放到form中。

4.3.1 把对话框的内容补充完整

  • 首先在src/services/member.enum.ts增加其他必要的代码表 ```typescript export const educationEnum = { 0: ‘ ‘, 1: ‘小学及以下’, 2: ‘初中’, 3: ‘高中、技校’, 4: ‘中专’, 5: ‘大专’, 6: ‘大学本科’, 7: ‘硕士研究生’, 8: ‘博士研究生’, }

export const partyEnum = { 0: ‘ ‘, 1: ‘中国共产党’, 2: ‘中国国民党革命委员会’, 3: ‘中国民主同盟’, 4: ‘中国民主建国会’, 5: ‘中国民主促进会’, 6: ‘中国农工民主党’, 7: ‘中国致公党’, 8: ‘九三学社和台湾民主自治同盟’, }

export const nationalityEnum = { 0: ‘汉族’, 1: ‘满族’, 2: ‘蒙古族’, 3: ‘回族’, 4: ‘藏族’, 5: ‘维吾尔族’, 6: ‘苗族’, 7: ‘彝族’, 8: ‘壮族’, }

  1. 为了简化版面,我们这里的代码清单做了部分的精简,在实际的项目中应根据业务要求编写完整。
  2. - `src/pages/MemberList/components/MemberDataForm.tsx`中增加引用
  3. ```typescript
  4. import {
  5. genderEnum,
  6. degreeEnum,
  7. partyEnum,
  8. educationEnum,
  9. nationalityEnum
  10. } from '@/services/member.enum';
  • 把对话框文件中的函数getOptionsFormValueEnum删除后放到src/utils/utils.ts

    1. export function getOptionsFormValueEnum (valueEnum: Object) {
    2. return (Object.entries(valueEnum)).map( ([key, label]) => ({
    3. value: parseInt(key),
    4. label,
    5. }))
    6. }export function getOptionsFormValueEnum (valueEnum: Object) {
    7. return (Object.entries(valueEnum)).map( ([key, label]) => ({
    8. value: parseInt(key),
    9. label,
    10. }))
    11. }
  • 在对话框文件中引用此函数和其他的ProForm组件

    1. import { getOptionsFormValueEnum } from '@/utils/utils'
    2. import
    3. ProForm, {
    4. ModalForm,
    5. ProFormText,
    6. ProFormSelect,
    7. ProFormDatePicker
    8. } from '@ant-design/pro-form';
  • 编写完整的对话框内容

    1. <ModalForm
    2. form={form}
    3. title={getTitle()}
    4. visible={visible}
    5. layout="horizontal"
    6. omitNil={false}
    7. onVisibleChange={(visible) => {
    8. if (!visible) handleCancel();
    9. }}
    10. onFinish={onFinish}
    11. >
    12. <ProForm.Group>
    13. <ProFormText name="realName" label="会员姓名"
    14. placeholder="请输入会员真实姓名" width="sm"
    15. required />
    16. <ProFormDatePicker name="birthday" label="出生日期"
    17. width="sm" allowClear={false}/>
    18. </ProForm.Group>
    19. <ProForm.Group>
    20. <ProFormText name="identityNumber" label="身份证号"
    21. placeholder="请输入身份证号码" width="sm"
    22. required />
    23. <ProFormSelect name="gender" label="性别"
    24. placeholder="选择性别" width={64}
    25. options={getOptionsFormValueEnum(genderEnum)}
    26. allowClear={false} />
    27. <ProFormSelect name="nationality" label="民族"
    28. placeholder="选择民族" width="xs"
    29. options={getOptionsFormValueEnum(nationalityEnum)}
    30. allowClear={false} />
    31. </ProForm.Group>
    32. <ProForm.Group>
    33. <ProFormText name="mobile" label="手机号码"
    34. placeholder="请输入手机号码" width="sm"
    35. required />
    36. <ProFormSelect name="party" label="所在党派"
    37. placeholder="选择党派" width="sm"
    38. options={getOptionsFormValueEnum(partyEnum)}
    39. allowClear={false} />
    40. </ProForm.Group>
    41. <ProForm.Group>
    42. <ProFormSelect name="education" label="&nbsp;&nbsp;&nbsp;最后学历"
    43. placeholder="选择学历" width="sm"
    44. options={getOptionsFormValueEnum(educationEnum)}
    45. allowClear={false} />
    46. <ProFormSelect name="degree" label="最高学位"
    47. placeholder="选择学位" width="sm"
    48. options={getOptionsFormValueEnum(degreeEnum)}
    49. allowClear={false} />
    50. </ProForm.Group>
    51. <ProFormText name="email" label="&nbsp;&nbsp;&nbsp;邮件地址"
    52. placeholder="请输入e-maill地址" width="md" />
    53. </ModalForm>

    注意:上面我们有意忽略了salary字段的内容。

现在对话框应该是这个样子
image.png

4.3.2 按规则校验输入的内容

新建立一个文件src/utils/form-validator.ts,内容如下

  1. import {Rule} from 'rc-field-form/es/interface'
  2. export const fieldRuls:{[key: string]: Rule[]}={
  3. required: [
  4. {
  5. required: true,
  6. whitespace: true,
  7. message: '此项为必填项',
  8. },
  9. ],
  10. requiredDate: [
  11. {
  12. required: true,
  13. whitespace: true,
  14. type: 'date',
  15. message: '此项为必填项',
  16. },
  17. ],
  18. mobile: [
  19. {
  20. required: false,
  21. pattern: new RegExp(/^1[3-9]\d{9}$/, "g"),
  22. message: '请输入正确的手机号码'
  23. },
  24. ],
  25. captcha: [
  26. {
  27. required: false,
  28. pattern: new RegExp(/^\d{6}$/, "g"),
  29. message: '验证码是6位数字'
  30. },
  31. ],
  32. identity: [
  33. {
  34. required: false,
  35. pattern: new RegExp(/^[1-9]\d{5}(19|20)\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/, "g"),
  36. message: '请输入正确的身份证号码'
  37. },
  38. ],
  39. email: [
  40. {
  41. required: false,
  42. pattern: new RegExp(/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/, "g"),
  43. message: '请输入正确的电子邮件地址'
  44. },
  45. ],
  46. }

显然,这些规则是可以在其他模块中复用的通用定义。

在对话框中引用它

  1. import { fieldRuls } from '@/utils/form-validator'

现在给几个需要校验的字段定义规则

  1. <ProFormText name="realName" label="会员姓名"
  2. placeholder="请输入会员真实姓名" width="sm"
  3. + rules={fieldRuls['required']}
  4. - required
  5. />
  6. <ProFormText name="identityNumber" label="身份证号"
  7. placeholder="请输入身份证号码" width="sm"
  8. + rules={[...fieldRuls['required'], ...fieldRuls['identity']]}
  9. - required
  10. />
  11. <ProFormText name="mobile" label="手机号码"
  12. placeholder="请输入手机号码" width="sm"
  13. + rules={[...fieldRuls['required'], ...fieldRuls['mobile']]}
  14. - required
  15. />
  16. <ProFormText name="email" label="&nbsp;&nbsp;&nbsp;邮件地址"
  17. placeholder="请输入e-maill地址" width="md"
  18. + rules={fieldRuls['email']}
  19. />

上面的代码中我们删除了required属性,这是因为如果规则里面定义了required,那么在渲染的时候会自动加上标记。

下面是这些规则工作的效果
image.png

4.3.3 必填日期型数据的校验规则

参照上面的定义方法,我们把出生日期也定义为必填内容,但实际运行会发现总是无法通过校验
image.png
出现这种情况的原因在于没有在校验规则中定义数据类型——校验规则默认数据类型为string,而日期组件的数据类型是date,因此无法通过校验。解决这个问题有两种做法:
1. 直接定义校验规则

  1. rules={[{
  2. required: true,
  3. whitespace: true,
  4. type: 'date',
  5. message: '出生日期为必填项',
  6. }]}
  1. utils/form-validator.ts中专门为日期定义一个通用规则
    1. requiredDate: [
    2. {
    3. required: true,
    4. whitespace: true,
    5. type: 'date',
    6. message: '此项为必填项',
    7. },
    8. ],
    1. <ProFormDatePicker name="birthday" label="出生日期"
    2. width="sm" allowClear={false}
    3. rules={fieldRuls['requiredDate']}
    4. />

    4.3.4 动态限制可以输入的字符

    用规则对内容进行校验的时机是内容被录入(或改变)以后。有些时候我们会希望用户不能录入无效的字符,比如不可以在手机号码输入框中输入字符和多余11个的字符。这个功能可以通过form中各字段的normalize属性定义来实现。

首先在新建立一个文件src/utils/form-validator.ts中定义如下内容

  1. export const fidleNormalizes={
  2. mobile: (value:string, prevValue:string) => {
  3. let nextValue = value?.replace(/[^\d]+/g, '')
  4. if(value?.length > 11)
  5. nextValue = prevValue
  6. return nextValue
  7. },
  8. identity: (value:string, prevValue:string) => {
  9. let nextValue = value?.replace(/[^(\d|x|X)]+/g, '')
  10. if(value?.length > 18)
  11. nextValue = prevValue
  12. return nextValue?.toUpperCase()
  13. },
  14. phone: (value:string, prevValue:string) => {
  15. let nextValue = value?.replace(/[^(\d|\-)]+/g, '')
  16. return nextValue
  17. },
  18. digit: (value:string, prevValue:string) => {
  19. let nextValue = value?.replace(/[^\d]+/g, '')
  20. return nextValue
  21. }
  22. }

增加引用

  1. import { fieldRuls, fidleNormalizes } from '@/utils/form-validator'

然后给几个需要限制的字段加上属性

  1. <ProFormText name="identityNumber" label="身份证号"
  2. placeholder="请输入身份证号码" width="sm"
  3. rules={fieldRuls['identity']}
  4. + normalize={fidleNormalizes['identity']}
  5. required />
  6. <ProFormText name="mobile" label="手机号码"
  7. placeholder="请输入手机号码" width="sm"
  8. rules={fieldRuls['mobile']}
  9. + normalize={fidleNormalizes['mobile']}
  10. required />

完成这些设置以后,在身份证那里就只能输入数字和字母x,在手机号那里只能输入数字,并且两处的最大长度也被限制了。超长或者不允许的字符都会被自动丢弃。

4.4 发起更新数据的网络请求并模拟效果

4.4.1 增加网络请求函数

src/services/api/member.ts中增加执行更新的网络请求函数

  1. export async function updateMember(data: any) {
  2. return request('/api/member/update', {
  3. method: 'POST',
  4. data: {
  5. ...data
  6. }
  7. });
  8. }

4.4.2 在对话框中调用更新函数

增加引用

  1. import { updateMember } from "@/services/api/member";

修改handleResponseonFinish函数

  1. const handleResponse = (sucess:boolean) => {
  2. doClose(sucess);
  3. };
  4. const onFinish = async (value:any) => {
  5. let result = {
  6. id: record.id,
  7. ...value,
  8. }
  9. try {
  10. await updateMember(result);
  11. message.success('保存成功,即将刷新',1);
  12. handleResponse(true);
  13. } catch (error) {
  14. handleResponse(false);
  15. }
  16. };

4.4.3 模拟更新数据

mock/member.ts中模拟完整更新数据

  1. function update(req: Request, res: Response, u: string) {
  2. const index = memberListDataSource.findIndex((member) => {
  3. return member.id === req.body.id
  4. } )
  5. const result = {
  6. success: true,
  7. errorCode: -1,
  8. }
  9. if(index >= 0) {
  10. Object.assign(memberListDataSource[index],req.body)
  11. } else {
  12. result.success = false
  13. result.errorCode = 2
  14. }
  15. return res.json(result);
  16. }
  17. export default {
  18. 'POST /api/member/update': update,

4.4.4 在成功更新后重新加载对话框

修改src/pages/MemberList/index.tsx中的doClose函数,当成功更新后重新加载对话框以反应最新的修改

  1. doClose={(needReload=false) => {
  2. setFormState({ showModal: false, operation: '', record: {}})
  3. if(needReload) {
  4. actionRef.current?.reloadAndRest?.();
  5. }
  6. }}

现在应该可以看到完整的更新数据内容的效果。

4.5 不保存未修改过的数据

到这里我们基本上完成了更新数据的功能,但还有一个需要优化的地方,那就是当用户没有做任何修改而直接点击确定的时候,不应该发起网络请求。本节就来解决这个问题

首先在对话框中增加对stateHook的引用

  1. import { useEffect, useState } from "react";

然后在对话框组件中定义一个State Hook

  1. const [dataChanged,setDataChanged] = useState<boolean>(false)

接下来修改onFinish函数的定义

  1. const onFinish = async (value:any) => {
  2. if(dataChanged) {
  3. let result = {
  4. id: record.id,
  5. ...value,
  6. }
  7. try {
  8. await updateMember(result);
  9. message.success('保存成功,即将刷新',1);
  10. handleResponse(true);
  11. } catch (error) {
  12. handleResponse(false);
  13. }
  14. } else {
  15. message.success('没有改动,直接退出了',1);
  16. handleResponse(false);
  17. }
  18. };

最后给ModalForm增加onValuesChange属性

  1. <ModalForm
  2. onValuesChange={() => {
  3. if(!dataChanged)
  4. setDataChanged(true)
  5. }}

现在如果不改动内容而点击确定,对话框会直接关闭并且给出提示。

4.6 增加重置功能

我们来给对话框增加一个“重置”按钮,点击它的时候把全部数据重置为修改前的内容,并且清除修改标记。

首先引用antd的Button

  1. import { message, Form, Button } from "antd";

然后修改默认按钮的文字并给对话框增加属性

  1. <ModalForm
  2. submitter={{
  3. searchConfig: {
  4. submitText: '保存',
  5. resetText: '取消',
  6. },
  7. render: (props, defaultDoms) => {
  8. return [
  9. ...defaultDoms,
  10. <Button
  11. key="extra-reset"
  12. onClick={() => {
  13. const { form } = props
  14. form?.setFieldsValue(record)
  15. setDataChanged(false)
  16. }}
  17. >
  18. 重置
  19. </Button>,
  20. ];
  21. },
  22. }}

4.7 关闭烦人的自动完成功能

默认情况下,浏览器会给所有输入框打开“自动完成”(autoComplete)功能,这个貌似出于友好理由的功能其实会给录入数据的人带来很大的麻烦,因此在实际的项目中最好声明关闭这个功能。具体的做法是给所有的输入框加上autoComplete = ‘off’的属性,根据组件的不同,方法有所区别:

  1. <Input autoComplete="off">
  2. <ProFormText fieldProps ={{ autoComplete:"off" }}>

4.8 小结

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

下面是和本章相关的重要链接:

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