本章我们完成一个独立的数据查询功能的设计

9.1 准备工作

9.1.1 程序文件和路由

首先,我们把src/pages/MemberList/index.tsx复制成src/pages/MemberList/components/query.tsx(可以直接在Visual Studio里面复制并改名)。然后在route.ts中把关于会员列表部分的设置改成如下的内容

  1. {
  2. path: '/member',
  3. name: '管理会员',
  4. icon: 'table',
  5. routes: [
  6. {
  7. name: '会员列表',
  8. icon: 'table',
  9. path: '/member/memberlist',
  10. component: './MemberList',
  11. },
  12. {
  13. name: '数据查询',
  14. icon: 'smile',
  15. path: '/member/dataquery',
  16. component: './MemberList/components/query.tsx',
  17. },
  18. ],
  19. },

image.png
对query.tsx做如下修改

  1. -import DataForm from './components/MemberDataForm';
  2. +import DataForm from './MemberDataForm';
  1. -import './index.less'
  2. +import '../index.less'

修改变量名称(其实不改也没问题)

  1. -const MemberList: React.FC = () => {
  2. +const MemberQuery: React.FC = () => {
  1. -export default MemberList;
  2. +export default MemberQuery;

9.1.2 修改列表属性

删除用于处理列状态的State Hook

  1. - const [columnsStateMap, setColumnsStateMap] = useState<{ [key: string]: ColumnsState; }>({
  2. - email: {
  3. - show: false,
  4. - },
  5. - });

按照如下的内容修改query.tsx中ProTable的属性

  1. + manualRequest
  2. + options={false}
  3. + params={queryParam}
  4. - headerTitle={'会员列表'}
  5. - sortDirections={['ascend', 'descend', 'ascend']}
  6. - columnsStateMap={columnsStateMap}
  7. - onColumnsStateChange={(map) => setColumnsStateMap(map)}
  8. - toolBarRender={() => [
  9. - <Button
  10. - type="primary"
  11. - key="primary"
  12. - onClick={() => {
  13. - setFormState({
  14. - showModal: true, operation: 'create', record: {
  15. - nationality: 0,
  16. - education: 6,
  17. - degree: 1,
  18. - party: 0,
  19. - email: '',
  20. - }
  21. - })
  22. - }}
  23. - >
  24. - <PlusOutlined /> 新建
  25. - </Button>,
  26. - ]}

删除和小说有关的变量和列定义

  1. - const novel = [
  2. - '却听得杨过朗声说道:“今番良晤,豪兴不浅,他日江湖相逢,再当杯酒言欢。咱们就此别过。”',
  3. - '说着袍袖一拂,携着小龙女之手,与神雕并肩下山。',
  4. - '其时明月在天,清风吹叶,树巅乌鸦呀啊而鸣,郭襄再也忍耐不住,泪珠夺眶而出。',
  5. - ]
  1. - {
  2. - title: '神雕',
  3. - dataIndex: 'id',
  4. - ellipsis: true,
  5. - copyable: true,
  6. - tooltip: '飞雪连天射白鹿,笑书神侠倚碧鸳',
  7. - renderText: (val: number) => novel[val % 3],
  8. - },

让邮件地址彻底不再列表中显示

  1. {
  2. title: '邮件地址',
  3. dataIndex: 'email',
  4. + hideInTable: true,
  5. },

删除之前做的所有和排序、筛选有关的定义

  1. {
  2. title: '身份证号',
  3. dataIndex: 'identityNumber',
  4. - sorter: {
  5. - multiple: 2,
  6. - },
  7. - copyable: true,
  8. renderText: (val: string) =>
  9. `${val.substr(0, 3)}***${val.substr(val.length - 3, 3)}`,
  10. },
  11. {
  12. title: '性别',
  13. dataIndex: 'gender',
  14. valueEnum: genderEnum,
  15. - filters: true,
  16. - onFilter: true,
  17. },
  1. {
  2. title: '出生日期',
  3. dataIndex: 'birthday',
  4. valueType: 'date',
  5. - sorter: {
  6. - multiple: 1,
  7. - compare: (a, b, sortOrder) => {
  8. - const prev = moment(a.birthday).toDate().valueOf()
  9. - const next = moment(b.birthday).toDate().valueOf()
  10. -
  11. - return prev - next
  12. - },
  13. - },
  14. - defaultSortOrder: 'descend',
  15. - showSorterTooltip: false,
  16. },

让学历和学位在表格中显示

  1. {
  2. title: '学历',
  3. dataIndex: 'education',
  4. - hideInTable: true,
  5. valueEnum: educationEnum,
  6. },
  7. {
  8. title: '最高学位',
  9. dataIndex: 'degree',
  10. - hideInTable: true,
  11. valueEnum: degreeEnum,
  12. },

image.png

9.1.3 定义与列表关联的空表单

  1. import ProForm, { ProFormProps } from '@ant-design/pro-form';
  1. const [queryParam, setQueryParam] = useState()
  2. const FormProps: ProFormProps = {
  3. onFinish: async (value:any) => {
  4. setQueryParam({
  5. ...value,
  6. //ProTable会监视params的值,当发生变化时附加上params自动发起网络请求
  7. //这里这么设计是临时性的,用来确保每次提交都会产生请求,设计完成后会删除这一条
  8. timestamp: new Date()
  9. })
  10. },
  11. }
  12. const pageContent = (
  13. <ProForm {...FormProps}>
  14. </ProForm>
  15. )
  1. - <PageContainer>
  2. + <PageContainer
  3. + title='查询会员信息'
  4. + content={pageContent}
  5. + >
  6. <ProTable<TYPE.Member, TYPE.PageParams>
  7. + params={queryParam}

现在新的页面显式如下
image.png
点击“提交”按钮可以无条件获得全部数据信息。

9.2 完整的查询表单

9.2.1 必要的引用和变量定义

我们需要下面的引用和State Hook

  1. import { getOptionsFormValueEnum } from '@/utils/utils'
  2. import { fieldRuls, fidleNormalizes } from '@/utils/form-validator'
  1. const [partyListVisible, setPartyListVisible] = useState(false);
  2. const [minorityListVisible, setMinorityListVisible] = useState(false);

9.2.2 查询表单的属性

修改ProForm中按钮的文字,并且实现民主党派和少数民族选择控件的逻辑(详细解释见10.3)

  1. const FormProps: ProFormProps = {
  2. layout: "horizontal",
  3. submitter: {
  4. searchConfig: {
  5. submitText: '查询',
  6. },
  7. },
  8. onValuesChange(changedValus) {
  9. if(changedValus['party'] != undefined) {
  10. if(changedValus['party'] === 2)
  11. setPartyListVisible(true)
  12. else
  13. setPartyListVisible(false)
  14. }
  15. if(changedValus['nationality'] != undefined) {
  16. if(changedValus['nationality'] === 2)
  17. setMinorityListVisible(true)
  18. else
  19. setMinorityListVisible(false)
  20. }
  21. },
  22. onFinish: async (formData: any) => {
  23. const { birthday = [null, null], party=0, partyList=0,
  24. nationality, nationalityList=[], ...rest } = formData
  25. const param = { ...rest }
  26. if(birthday[0] != null )
  27. param.startDate = birthday[0]
  28. if(birthday[1] != null)
  29. param.endDate = birthday[1]
  30. if(party === 1)
  31. param.party = 1
  32. else if(party === 2) {
  33. if(partyList ==0 )
  34. param.party = -1
  35. else
  36. param.party = partyList
  37. }
  38. if(nationality === 1)
  39. param.nationality = 1
  40. else if(nationality === 2) {
  41. if(nationalityList.length ==0 )
  42. param.nationality = -1
  43. else {
  44. param.nationality = -2
  45. param.nationalityList = nationalityList
  46. }
  47. }
  48. setDataLoaded(false)
  49. setQueryParam({
  50. ...param,
  51. timestamp: new Date(),
  52. })
  53. },
  54. }

9.2.3 查询表单的内容

下面是ProForm中的完整内容,详细的解释见10.3。

  1. <ProForm {...FormProps}>
  2. <ProForm.Group>
  3. <ProFormText name="realName" label="会员姓名" placeholder="姓名模糊查询"
  4. tooltip="可以输入不完整的姓名进行模糊搜索"
  5. fieldProps={{ autoComplete: "off" }} width="sm" />
  6. <ProFormCheckbox.Group name="gender" label="性别"
  7. options={getOptionsFormValueEnum(genderEnum).filter((item) => item.value > 0)} />
  8. <ProFormRadio.Group name="party" label="政治面貌" radioType="button"
  9. options={[
  10. { value: 0, label: '任意' },
  11. { value: 1, label: '中共党员' },
  12. { value: 2, label: '民主党派' },
  13. ]} />
  14. <ProFormSelect name="partyList" label="" placeholder="单项选择民主党派" width="sm"
  15. options={getOptionsFormValueEnum(partyEnum).filter((item) => item.value != 1)}
  16. hidden={!partyListVisible} allowClear={false} />
  17. </ProForm.Group>
  18. <ProForm.Group>
  19. <ProFormText name="identityNumber" label="身份证号" placeholder="必须完整以精确匹配" width="sm"
  20. rules={[...fieldRuls['identity']]}
  21. normalize={fidleNormalizes['identity']}
  22. tooltip="身份证号必须完整规范以进行精确匹配"
  23. fieldProps={{ autoComplete: "off" }} />
  24. <ProFormCheckbox.Group name="degree" label="学位"
  25. options={getOptionsFormValueEnum(degreeEnum)
  26. .filter((item) => item.value > 0)} />
  27. <ProFormSelect name="education" label="学历" placeholder="可以多选" width="md"
  28. options={getOptionsFormValueEnum(educationEnum)}
  29. mode="multiple" allowClear={true} />
  30. </ProForm.Group>
  31. <ProForm.Group>
  32. <ProFormText name="mobile" label="手机号码" placeholder="手机号码模糊查询" width="sm"
  33. normalize={fidleNormalizes['mobile']}
  34. tooltip="可以输入不完整的手机号码进行模糊搜索"
  35. fieldProps={{ autoComplete: "off", maxLength: 11 }} />
  36. <ProFormDateRangePicker name="birthday" label="生日" width="md"
  37. fieldProps={{ autoComplete: "off", allowEmpty: [true, true], }}
  38. tooltip="可以选择开始或结束一边的日期"
  39. />
  40. <ProFormText name="email" label="&nbsp;&nbsp;&nbsp;邮件地址" placeholder="模糊搜索" width="sm"
  41. fieldProps={{ autoComplete: "off" }}
  42. />
  43. </ProForm.Group>
  44. <ProForm.Group>
  45. <ProFormRadio.Group name="nationality" label="个人民族" radioType="button"
  46. tooltip="既可以搜索特定的一个或多个少数民族,也可以搜索全部少数民族"
  47. options={[
  48. { value: 0, label: '任 意' },
  49. { value: 1, label: '汉 族' },
  50. { value: 2, label: '少数民族' },
  51. ]}
  52. />
  53. <ProFormSelect name="minorityList" label="" placeholder="不选代表全部,也可组合选择" width="xl"
  54. showSearch mode="multiple" hidden={!minorityListVisible}
  55. options={getOptionsFormValueEnum(nationalityEnum).filter((item) => item.value > 1)}
  56. allowClear={true} />
  57. </ProForm.Group>
  58. </ProForm>
  1. -import ProForm, { ProFormProps } from '@ant-design/pro-form';
  2. +import ProForm, { ProFormCheckbox, ProFormDateRangePicker, ProFormProps, ProFormRadio, ProFormSelect, ProFormText } from '@ant-design/pro-form';

image.png

9.3 查询表各项内容的解释

  • 姓名

姓名做查询条件的时候需要支持任意位置的模糊搜索

  • 性别

性别做查询条件的时候需要以复选的形式出现,即可以不选、仅选择男、仅选择女、或男女都选

  • 政治面貌

我们在数据库中存储的政治面貌信息是把所有的民主党派分别标记的,但查询的时候应该给用户查询全部民主党派的方法。也就是说用户可以不选择、选择中国党员、选择全部民主党派、或者选择某个具体的民主党派。所以当用户选择“民主党派”后,软件设计出现一个具体的党派清单供选择(默认是不选任何内容)。另外,一旦用户点击了单选组件中的内容,无法恢复完全未选的状态,因此需要提供一个表示“任意”的选项。如果在实际的业务场景中不区分民主党派的明细,那么就不需要后面那个列表的设计了。

  • 身份证号

用户把身份证号做查询条件的时候通常是做精确的搜索,因此这里要求必须输入符合编码规则的完整的身份证号。

  • 学位

学位的选项只有3个,并且可以并列组合,因此用复选框组件。

  • 学历

选择的可选项比较多,并且可以并列组合,因此用支持复选的下拉选择组件。

  • 手机号码

用户把手机号码做查询条件的时候,有可能要做精确匹配,也可能是要做模糊查询,比如以查一下以1234结尾或者包含它的手机号,所以这里仅要求输入内容为长度不超过11的数字。

  • 生日

在实际的业务场景中,类似生日这样的日期(或时间型)数据,用户往往更倾向于给出某个日期(或时间)范围进行查询,并且范围条件的端不一定封闭,也就是可能是某个时间段之内、某个时间以后、或某个时间以前,因此我们需要提供一个两端都可以为空的日期(或时间)选择组件。

  • 邮件地址

在实际的业务场景中,用户很少把类似邮件地址这种文字信息作为查询条件,即便对这类信息进行查询,也更多是模糊查询。

  • 个人民族

以民族作为查询条件的时候,用户可能会关注汉族、所有的少数民族、某几个少数民族,这里的逻辑和政治面貌那里有一部分近似,但少数民族要提供多选的条件。

9.4 在Mock中响应查询条件

下面是修改后的Moak响应函数,具体逻辑请自行阅读代码理解。

  1. function isRequired(data:TYPE.Member, keys: string[], query: any) : boolean {
  2. for (let key of keys) {
  3. const queryValue = query[key]
  4. if(key === 'timestamp' || key === 'nationalityList')
  5. continue;
  6. if(queryValue != '' ) {
  7. switch(key) {
  8. case 'realName':
  9. case 'mobile':
  10. case 'email':
  11. if(!data[key]?.includes(queryValue as string))
  12. return false
  13. break;
  14. case 'party':
  15. const party = parseInt(queryValue)
  16. if(party > 0 && data.party !== party)
  17. return false
  18. if(party === -1 && (data?.party as number) <= 1 )
  19. return false
  20. break;
  21. case 'nationality':
  22. const nationality = parseInt(queryValue)
  23. if(nationality === 1 && data.nationality !== 1)
  24. return false
  25. if(nationality === -1 && (data?.nationality as number) <= 1)
  26. return false
  27. if(nationality === -2 && !(query['nationalityList'] as Array<string>)?.includes( data.nationality+'' ))
  28. return false
  29. break;
  30. case 'startDate':
  31. case 'endDate':
  32. const startDate = query['startDte'] !== ''? new Date(query['startDate']+'') : null;
  33. const endDate = query['endDate'] !== ''? new Date(query['endDate']+'') : null;
  34. const theDate = new Date(data['birthday']+'')
  35. if(startDate != null && endDate!= null)
  36. return startDate <= theDate && theDate <= endDate;
  37. if(startDate != null && theDate < startDate)
  38. return false
  39. if(endDate!= null && endDate < theDate)
  40. return false
  41. break;
  42. default:
  43. if(typeof queryValue === 'string') {
  44. if(data[key] != queryValue)
  45. return false
  46. } else {
  47. if(!(queryValue as Array<string>)?.includes( data[key]+'' ))
  48. return false
  49. }
  50. break;
  51. }
  52. }
  53. }
  54. return true
  55. }
  56. function queryAll(req: Request, res: Response, u: string) {
  57. const { current = 1, pageSize = 10, sort='', filter='', ...query} = req.query;
  58. const queryKeys = Object.keys(query)
  59. let quertResult = memberListDataSource
  60. console.log(req.query)
  61. if(queryKeys.length > 0) {
  62. quertResult = memberListDataSource.filter( (data) => isRequired(data, queryKeys, req.query))
  63. }
  64. let dataSource = quertResult.slice(
  65. ((current as number) - 1) * (pageSize as number),
  66. (current as number) * (pageSize as number),
  67. );
  68. const result = {
  69. data: dataSource,
  70. total: quertResult.length,
  71. success: true,
  72. pageSize,
  73. current,
  74. };
  75. return res.json(result);
  76. }

9.5 把结果导出为Excel文件

完成上文的程序设计以后,我们已经有了想对完整的查询数据的功能,对查询结果可以做查看、修改和删除的操作。除此以外,用户还经常会希望能够吧查询的结果保存起来,本节我们就在前端程序中实现生成并保存Excel文件的功能。

9.5.1 关于前后端和文件类型的哲学思考

生成Excel XLS格式的文件还是WPS ET格式的文件?在前端生成还是后端生成?这是两个哲学问题。

关于文件格式,我们还是倾向于生成Excel文件,原因是它的适用性更广泛,基本上常见的电子表格软件(包括WPS的)都能够打开它。

关于前端还是后端,这个各有利弊。前端生成的优点是既减少了对数据库服务器资源的消耗(少一次查询)又减少了对应用服务器资源的消耗,因不存在网络传输而节省带宽消耗、避免了网络延迟,缺点自然是消耗了较多的浏览器资源。但既然是前端教程,如果在后端生成,我们就不用写他了。

9.5.2 前端Excel类库选择

通过网络检索可以找到很多前端操作(导入、导出)Exce的开源项目,经过比较我们决定使用SheetJS的 js-xlsx在前端处理Excel文件(他们也有前端处理Word的项目)。这个项目在GitHub上获得了25,254个★下面是它的网址:

通过下面的命令可以把他加到当前项目中

  1. $ tyarn add xlsx
  2. $ cnpm install --save xlsx

9.5.3 封装几个功能函数

在src/utils/utils.ts中增加如下的内容

  1. import { ProColumns } from '@ant-design/pro-table';
  2. import * as Excel from 'xlsx'
  1. /*
  2. * 把数据中的字段名称(英文)换成标题(一般是中文)
  3. *
  4. * @param {any[]} data 待转换的JSON数据,一般是一个对象数组,每个对象是表中的一行数据
  5. * @param {ProColumns[]} columns ProTable的列定义数组
  6. *
  7. * @return {any[]} 把对象键名换成更表格标题的结果
  8. */
  9. export function converToReadable(data: any[], columns: ProColumns[]): any[] {
  10. const readableData = data.map((row: any) => {
  11. const keys = Object.keys(row)
  12. const result = {}
  13. for(let key of keys) {
  14. const columnDefine = columns.find( (col) => col.dataIndex === key)
  15. if(columnDefine)
  16. result[columnDefine.title + ''] = columnDefine.valueEnum? columnDefine.valueEnum[row[key]] : row[key]
  17. else
  18. result[key] = row[key]
  19. }
  20. return result
  21. })
  22. return readableData;
  23. }
  24. /*
  25. * 触发浏览器自动下载前端生成的文件
  26. *
  27. * @param {Blob} blobData 准备保存的文件内容数据
  28. * @param {string} fileName 文件名
  29. *
  30. * @return {void}
  31. */
  32. export function saveAs(blobData:Blob, fileName:string){//导出功能实现
  33. var tmpa = document.createElement("a");
  34. tmpa.download = fileName || "下载";
  35. tmpa.href = URL.createObjectURL(blobData); //绑定a标签
  36. tmpa.click(); //模拟点击实现下载
  37. setTimeout(function () { //延时释放
  38. URL.revokeObjectURL(blobData as any); //用URL.revokeObjectURL()来释放这个object URL
  39. }, 100);
  40. }
  41. /*
  42. * 内部用函数,把字符串转字符流
  43. *
  44. * @param {sring} s 待转换的字符串
  45. *
  46. * @return {ArrayBuffer} ArrayBuffer格式的转换结果
  47. */
  48. function s2ab(s:string){ //字符串转字符流
  49. var buf = new ArrayBuffer(s.length);
  50. var view = new Uint8Array(buf);
  51. for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
  52. return buf;
  53. }
  54. /*
  55. * 触发浏览器自动下载前端生成的非Blob类型数据
  56. *
  57. * @param {any} data 准备保存的文件内容数据(非Blob)
  58. * @param {string} fileName 文件名
  59. *
  60. * @return {void}
  61. */
  62. export function exportFile(data: any, fileName: string) {
  63. const tmpDown = new Blob([s2ab(data)], { type: "" });
  64. saveAs(tmpDown, fileName);
  65. }
  66. /*
  67. * 根据对象数组(或者说JSON数据)生成XLSX格式的Excel文件并且触发浏览器自动下载
  68. *
  69. * @param {any} jsonData 处理好的对象数组
  70. * @param {string} fileName 文件名
  71. *
  72. * @return {void}
  73. */
  74. export function exportAsExcel(jsonData: any, fileName: string) {
  75. const worksheet = Excel.utils.json_to_sheet(jsonData);
  76. const workbook = Excel.utils.book_new();
  77. Excel.utils.book_append_sheet(workbook, worksheet, "查询结果");
  78. const fileObject = Excel.write(workbook,{
  79. bookType: 'xlsx',
  80. bookSST: false,
  81. type: 'binary',
  82. compression: true,
  83. });
  84. exportFile(fileObject, fileName)
  85. }

9.5.4 设计导出为Excel文件的功能

在src/pages/MemberList/components/query.tsx中增加新的引用

  1. import { exportAsExcel, converToReadable } from '@/utils/utils'

增加两个State Hook

  1. const [tableData,setTableData] = useState<any[]>([])

给ProTable增加onLoad响应函数

  1. onLoad={(dataSource: any[]) => {
  2. if(dataSource.length >0 )
  3. setDataLoaded(true)
  4. setTableData(dataSource)
  5. }}

在Form中增加一个按钮

  1. const FormProps: ProFormProps = {
  2. layout: "horizontal",
  3. submitter: {
  4. searchConfig: {
  5. submitText: '查询',
  6. },
  7. + render: (props, defaultDoms) => {
  8. + return [
  9. + ...defaultDoms,
  10. + <Button
  11. + hidden={!dataLoaded}
  12. + key="export"
  13. + onClick={() => {
  14. + exportAsExcel(converToReadable(tableData, columns),'人员信息.xlsx')
  15. + }}
  16. + >
  17. + 导出
  18. + </Button>,
  19. + ];
  20. + },
  21. },

注意,封装完的函数用起来很简单,和导出Excel直接相关的其实只有一行代码

  1. exportAsExcel(converToReadable(tableData, columns),'人员信息.xlsx')

9.6 清理工作

尽管不是必须,但作为一个良好的习惯,应该根据Visual Studio Code中的警告信息删除不再需要的引用定义。

9.7 重要说明

本章涉及的的内容非常多,讲解的又不像之前的章节那么细致,所以最好按照本章的内容逐条输入语句,并且在输入的时候自己思考,确保理解每条语句的逻辑,一定不要简单的大段复制粘贴

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