news 2026/5/17 4:14:40

猪齿鱼table子级树使用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
猪齿鱼table子级树使用

一、需求描述

1、table中的操作列点击'修改':

(1)子级数显示,操作列的'修改'按钮隐藏,变成'保存'和'取消';

(2)子级中每列的'是否发放'字段可修改;

2、点击返点人数,子级列表展开,可查看子级树。

二、效果图

(1)点击操作列‘修改’

(2)点击返点人数

三、代码实现

(1)main.tsx

// main.tsx import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Button, DataSet, message, Modal, Select, Table, } from 'choerodon-ui/pro'; import { Popconfirm, Tag } from 'choerodon-ui'; import { ColumnLock, SelectionMode, TableAutoHeightType, } from 'choerodon-ui/pro/lib/table/enum'; import { useSelector, shallowEqual } from 'react-redux'; import formatterCollections from 'hzero-front/lib/utils/intl/formatterCollections'; import { Button as PermissionButton } from 'components/Permission'; import { ButtonColor, FuncType } from 'choerodon-ui/pro/lib/button/enum'; import { commonModelPrompt, prmRcTemCode, TABLE_COLUMN_OPERATION, COMMON_IMPORT, languageConfig, NO_DATA, COMMON_SAVE, COMMON_CANCEL_OPERATION_TIPS, STATUS_YES, STATUS_NO, COMMON_CANCEL, } from '@/language/language'; import { LabelLayout } from 'choerodon-ui/pro/lib/form/enum'; import { QueryBar } from '@ino/ltc-component-paas'; import CommonImport from 'components/CommonImport'; import { renderSerialNumber } from '@/public'; import { postUpdateIsIssueRebate } from '@/api/rebatePersonnelInformation'; import { SearchParams, StatusItems, } from '@/interface/rebatePersonnelInformation'; import { importProps } from '@/utils/utils'; import { RootState } from '@/interface'; import commonStyles from '@/assets/styles/global.less'; import '@/assets/styles/c7n.less'; import styles from './main.less'; import { childrenList, permissionList, queryConfig, queryList, renderAmount, tableList, } from './store'; const RebatePersonnelInformationList = () => { /** 当前tab页 */ const activeTabKey = useSelector( (state: RootState) => state?.global?.activeTabKey, shallowEqual, ); useEffect(() => { if (activeTabKey === '/prm/rc/rebatePersonnelInformation') { tableDs.query(); } }, [activeTabKey]); /** 处理表格高度 */ const handleRef = useRef<HTMLDivElement | null>(null); const [handleHeight, setHandleHeight] = useState<number>(0); useEffect(() => { (async function() { const height = handleRef?.current?.offsetHeight ?? 0; setHandleHeight(height); })(); }, []); /** 导入 */ const handleImport = () => { const data = { code: 'LTC.PRM_RC_REBATE_PERSONAL', pathKey: 'rebatePersonnelInformation', }; Modal.open({ title: COMMON_IMPORT(), className: commonStyles.prmRcSelectCustomerModal, closable: true, destroyOnClose: true, maskClosable: true, children: <CommonImport {...importProps(data)} />, autoCenter: true, style: { width: 1200 }, onOk: () => { tableDs.query(); }, }); }; /** 操作按钮 */ const tableButtons = useMemo(() => { return [ <PermissionButton key="import" type="c7n-pro" funcType={FuncType.raised} color={ButtonColor.default} permissionList={permissionList.import} onClick={handleImport} > <div className={commonStyles.prmRc_button}> <div className={commonStyles.prmRc_button_export}></div> {COMMON_IMPORT()} </div> </PermissionButton>, ]; }, []); /** 子级数据组件 */ const childrenDsMap = useRef<Record<string | number, DataSet>>({}); const ChildrenTable: React.FC<{ parentId: string | number; parentIsEditing: boolean; // 新增:父级是否处于编辑状态 }> = ({ parentId, parentIsEditing }) => { // useMemo 在子组件顶层,合法!依赖 parentId 变化才重新创建 DataSet // const childrenTableDs = useMemo(() => new DataSet(childrenList(parentId)), [ // parentId, // ]); // 修改时需要获取子级数据:创建子表格 DataSet const childrenTableDs = useMemo(() => { const ds = new DataSet(childrenList(parentId)); childrenDsMap.current[parentId] = ds; // 将 DataSet 实例存入映射表 return ds; }, [parentId]); const childrenColumns = useMemo(() => { return [ { name: 'personalName' }, { name: 'identityNumber' }, { name: 'certificationType' }, { name: 'certificationLevel' }, { name: 'evaluationCoefficient' }, { name: 'baseNumber', renderer: renderAmount }, { name: 'rebateAmount', renderer: renderAmount }, { name: 'isIssueRebate', header: record => ( <div> {parentIsEditing && <span style={{ color: '#FB4242' }}>*</span>} {record.title} </div> ), // 根据父级编辑状态控制 editor: parentIsEditing ? () => <Select clearButton={true} /> : false, }, ]; }, []); useEffect(() => { childrenTableDs.query(); }, [parentId]); // 组件卸载时,从映射表中移除当前子 DataSet(避免内存泄漏) useEffect(() => { return () => { delete childrenDsMap.current[parentId]; }; }, [parentId]); return ( <div className={styles.prmRcPersonalInformation_table}> <Table dataSet={childrenTableDs} columns={childrenColumns} pagination={false} selectionMode={SelectionMode.click} renderEmpty={() => { return <div>{NO_DATA()}</div>; }} /> </div> ); }; /** 渲染:子级数据 */ const expandedRowRenderer = ({ record }) => { const { id } = record.toData(); const parentIsEditing = record.getState('editing'); // 获取父级编辑状态 return <ChildrenTable parentId={id} parentIsEditing={parentIsEditing} />; }; /** 查询 ds */ const querySearchList = async (data: SearchParams) => { const queryParameters = { annual: data.annual, calculateNo: data.calculateNo, protocolCode: data.protocolCode, channelName: data.channelName, rebateProductType: data.rebateProductType, status: data.status, }; Object.entries(queryParameters).forEach(([key, value]): void => { tableDs.setQueryParameter(key, value); }); await tableDs.query(); }; const onChange = async ({ record }) => querySearchList(record.toData()); const queryListDs = useMemo(() => new DataSet(queryList(onChange)), []); /** table ds */ const tableDs = useMemo(() => new DataSet(tableList()), []); const columns = useMemo(() => { return [ ...renderSerialNumber(), { name: 'annual', width: 80 }, { name: 'calculateNo', minWidth: 140 }, { name: 'protocolCode' }, { name: 'channelCode' }, { name: 'channelName' }, { name: 'rebateProductType' }, { name: 'ratioNumber' }, { name: 'rebateNumber', renderer: ({ text }) => { return <a className="expand_target">{text}</a>; }, }, { name: 'status', width: 140, renderer: ({ value, record }) => { const statusMap: StatusItems = record?.getField('status')?.getLookupData(value, record) || {}; if (!value) return null; return ( <Tag color={statusMap.tag} style={{ color: statusMap.description }}> {statusMap?.meaning} </Tag> ); }, }, { header: TABLE_COLUMN_OPERATION(), lock: ColumnLock.right, command: ({ record }) => { const { status } = record.toData(); const isEditing = record.getState('editing'); // 判断当前record是否处于编辑状态 if (isEditing) { return [ <Button key="save" funcType={FuncType.link} color={ButtonColor.primary} onClick={async e => { e.stopPropagation(); // 仅阻止当前按钮点击的冒泡 handleSave(record); }} className="prm_rc_save_btn" > {COMMON_SAVE()} </Button>, <Popconfirm key="cancel" title={COMMON_CANCEL_OPERATION_TIPS()} okText={STATUS_YES()} cancelText={STATUS_NO()} onConfirm={e => { e.stopPropagation(); handleConfirmCancel(record); }} onCancel={e => { e.stopPropagation(); }} > <Button funcType={FuncType.link} color={ButtonColor.red} className="prm_rc_cancel_btn" onClick={e => e.stopPropagation()} > {COMMON_CANCEL()} </Button> </Popconfirm>, ]; } return [ status !== 'pushed' && ( <PermissionButton key="edit" type="text" permissionList={permissionList.edit} onClick={e => { e.stopPropagation(); handleEdit(record); }} > <span className={'expand_target'}> {languageConfig('common.change', '修改')} </span> </PermissionButton> ), ]; }, }, ]; }, []); /** 编辑操作 */ const handleEdit = record => { record.setState('editing', true); // 设置当前行为编辑状态 record.isExpanded = true; // 展开子级数据 }; /** 保存 */ const handleSave = async record => { const { id } = record.toData(); console.log('id', id); // 1、从映射表中取出当前父行对应的子表格 DataSet const childrenTableDs = childrenDsMap.current[id]; if (!childrenTableDs) { message.error('子表格数据不存在', undefined, undefined, 'top'); return; } // 2、校验子表格数据 const isValid = await childrenTableDs.validate(); if (!isValid) { message.error( '子表格数据校验失败,请检查必填项', undefined, undefined, 'top', ); return; } // 3、获取子表格的所有数据 const childrenData = childrenTableDs.toData(); // 4、TODO:提交数据 const res = await postUpdateIsIssueRebate([...childrenData]); if (res.failed) { message.error(res.message, undefined, undefined, 'top'); return; } // 5、保存成功后的处理 record.setState('editing', false); // 退出编辑状态 await tableDs.query(); // 刷新父表格数据 }; /** 取消 */ const handleConfirmCancel = async record => { record.setState('editing', false); // 重置编辑状态 record.reset(); // 重置数据 record.isExpanded = false; // 关闭展开框 await tableDs.query(); // 刷新页面数据 }; /** */ const tableRef = useRef<HTMLDivElement>(null); useEffect(() => { const handleClick = (e: Event) => { const target = e.target as HTMLElement; // 1. 定义允许触发冒泡的元素(需要触发行展开/收缩的元素) const isExpandTarget = target.closest('.expand_target'); // 2. 定义需要正常交互的元素(不阻止其事件) const isInteractiveElement = target.closest( 'button, .c7n-pro-select, .c7n-pro-table-cell-command', ); // const isSaveButton = target.closest('.prm_rc_save_btn'); // const isCancelButton = target.closest('.prm_rc_cancel_btn'); // 3. 只在「不是允许冒泡的元素」且「不是交互元素」时,阻止冒泡 // 目的:仅阻止误触发行展开的点击,不影响其他正常交互 if (!isExpandTarget && !isInteractiveElement) { e.stopPropagation(); } }; const currentTableRef = tableRef.current; if (currentTableRef) { // 移除第三个参数true(默认冒泡阶段触发,避免提前拦截) currentTableRef.addEventListener('click', handleClick); } return () => { if (currentTableRef) { currentTableRef.removeEventListener('click', handleClick); } }; }, []); return ( <div className="ltc-c7n-style" style={{ overflow: 'auto', margin: '16px', borderRadius: '8px', height: '100%', }} > <div className={commonStyles.prmRc}> <div ref={handleRef}> <QueryBar formConfig={{ labelLayout: LabelLayout.float }} onQuery={querySearchList} queryBarDs={queryListDs} queryConfig={queryConfig} /> </div> <div ref={tableRef} className={styles.prmRcPersonalInformation} style={{ height: `calc(100% - ${handleHeight}px)` }} > <Table dataSet={tableDs} columns={columns} buttons={tableButtons} selectionMode={SelectionMode.click} autoHeight={{ type: TableAutoHeightType.maxHeight, diff: 30 }} expandRowByClick={true} // 通过点击行来展开子行 expandIcon={() => <></>} expandedRowRenderer={expandedRowRenderer} /> </div> </div> </div> ); }; export default formatterCollections({ code: [prmRcTemCode, commonModelPrompt], })(RebatePersonnelInformationList);

(2)store.ts

import { FieldType } from 'choerodon-ui/dataset/data-set/enum'; import { COMMON_IMPORT, languageConfig } from '@/language/language'; import { DataSetProps } from 'choerodon-ui/dataset/data-set/DataSet'; import moment from 'moment'; import { rebatePersonnelInformationList, rebatePersonnelInformationListChild, } from '@/api/rebatePersonnelInformation'; import { Select, TextField, YearPicker } from 'choerodon-ui/pro'; import { handleFormatAmount } from '@/public/store'; /** 公用:金额格式化渲染函数(编辑态显示原始值,非编辑态格式化金额)*/ export const renderAmount = ({ value, record }) => { if (record?.getState('editing')) { return value; } if (!value) return; return handleFormatAmount(value); }; // 权限 export const permissionList = { import: [ { code: 'prmRc.rebatePersonnelInformation.import.btn', type: 'button', meaning: COMMON_IMPORT(), }, ], edit: [ { code: 'prmRc.rebatePersonnelInformation.change.btn', type: 'text', meaning: languageConfig('common.change', '修改'), }, ], }; // 查询(字段) export const queryConfig = [ { name: 'annual', dom: YearPicker, clearButton: true, }, { name: 'calculateNo', dom: TextField, clearButton: true, }, { name: 'protocolCode', dom: TextField, clearButton: true, }, { name: 'channelName', dom: TextField, clearButton: true, }, { name: 'rebateProductType', dom: Select, clearButton: true, // 只显示技术相关的:返点类型 optionsFilter: record => { const hasTechnology = record .get('tag') ?.split(',') ?.includes('technology'); return hasTechnology; }, }, { name: 'status', dom: Select, clearButton: true, }, ]; // 列表(字段) export const parentFields = [ { name: 'annual', type: FieldType.string, label: languageConfig('rebatePersonnelInformation.table.annual', '年度'), }, { name: 'calculateNo', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.calculateNo', '返点核算单号', ), }, { name: 'protocolCode', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.protocolCode', '协议编码', ), }, { name: 'channelCode', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.channelCode', '渠道编码', ), }, { name: 'channelName', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.channelName', '渠道名称', ), }, { name: 'rebateProductType', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.rebateProductType', '返点产品类型', ), lookupCode: 'PRM_RC_REBATEPRODUCTTYPE', }, { name: 'ratioNumber', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.ratioNumber', '人员配比', ), }, { name: 'rebateNumber', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.rebateNumber', '返点人数', ), }, { name: 'status', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.status', '核算单状态', ), lookupCode: 'PRM_RC_ACCOUNT_INFORMATION_STATUS', }, ]; // 子级树:人员配比(字段) export const childLevelFields = [ { name: 'personalName', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.personalName', '认证人员', ), }, { name: 'identityNumber', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.identityNumber', '身份证', ), }, { name: 'certificationType', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.certificationType', '认证类型', ), lookupCode: 'PRM_AE_CERTIFICATION_LEVEL_2', }, { name: 'certificationLevel', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.certificationLevel', '认证等级', ), lookupCode: 'PRM_AE_CERTIFICATION_LEVEL_3', }, { name: 'evaluationCoefficient', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.evaluationCoefficient', '年度贡献评价', ), }, { name: 'baseNumber', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.baseNumber', '等级基数', ), }, { name: 'rebateAmount', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.rebateAmount', '返点金额', ), }, { name: 'isIssueRebate', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.isIssueRebate', '是否发放', ), lookupCode: 'LTC_YES_OR_NO', required: true, }, ]; /** 列表 ds */ export const queryList = (onChange): DataSetProps => { return { autoCreate: true, fields: [ { name: 'annual', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.annual', '年度', ), placeholder: languageConfig( 'rebatePersonnelInformation.table.annual', '年度', ), transformRequest: data => { if (data) return `${moment(data).format('YYYY')}`; }, }, { name: 'calculateNo', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.calculateNo', '返点核算单号', ), placeholder: languageConfig( 'rebatePersonnelInformation.table.calculateNo', '返点核算单号', ), }, { name: 'protocolCode', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.protocolCode', '协议编码', ), placeholder: languageConfig( 'rebatePersonnelInformation.table.protocolCode', '协议编码', ), }, { name: 'channelName', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.channelName', '渠道名称', ), placeholder: languageConfig( 'rebatePersonnelInformation.table.channelName', '渠道名称', ), }, { name: 'rebateProductType', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.rebateProductType', '返点产品类型', ), placeholder: languageConfig( 'rebatePersonnelInformation.table.rebateProductType', '返点产品类型', ), lookupCode: 'PRM_RC_REBATEPRODUCTTYPE', }, { name: 'status', type: FieldType.string, label: languageConfig( 'rebatePersonnelInformation.table.status', '核算单状态', ), placeholder: languageConfig( 'rebatePersonnelInformation.table.status', '核算单状态', ), lookupCode: 'PRM_RC_ACCOUNT_INFORMATION_STATUS', }, ], events: { update: onChange, }, }; }; export const tableList = () => { return { autoQuery: true, fields: parentFields, transport: { read: config => { const params = { page: config.params.page, size: config.params.size, ...config?.data, }; return { ...rebatePersonnelInformationList(), }; }, }, }; }; /** 子级 ds */ export const childrenList = (id: string | number) => { return { autoQuery: true, fields: childLevelFields, transport: { read: config => { const params = { initialId: id, }; return { ...rebatePersonnelInformationListChild(params), transformResponse: data => { const result = JSON.parse(data); return result ?? []; }, }; }, }, }; };

四、核心代码

操作列中的,在操作按钮上添加一个class,监听这个class来控制展示开子级树。

<div ref={tableRef} className={styles.prmRcPersonalInformation} style={{ height: `calc(100% - ${handleHeight}px)` }} > <Table dataSet={tableDs} columns={columns} buttons={tableButtons} selectionMode={SelectionMode.click} autoHeight={{ type: TableAutoHeightType.maxHeight, diff: 30 }} // 核心部分 expandRowByClick={true} // 通过点击行来展开子行 expandIcon={() => <></>} expandedRowRenderer={expandedRowRenderer} /> </div> useEffect(() => { const handleClick = (e: Event) => { const target = e.target as HTMLElement; // 1. 定义允许触发冒泡的元素(需要触发行展开/收缩的元素) const isExpandTarget = target.closest('.expand_target'); // 2. 定义需要正常交互的元素(不阻止其事件) const isInteractiveElement = target.closest( 'button, .c7n-pro-select, .c7n-pro-table-cell-command', ); // const isSaveButton = target.closest('.prm_rc_save_btn'); // const isCancelButton = target.closest('.prm_rc_cancel_btn'); // 3. 只在「不是允许冒泡的元素」且「不是交互元素」时,阻止冒泡 // 目的:仅阻止误触发行展开的点击,不影响其他正常交互 if (!isExpandTarget && !isInteractiveElement) { e.stopPropagation(); } }; const currentTableRef = tableRef.current; if (currentTableRef) { // 移除第三个参数true(默认冒泡阶段触发,避免提前拦截) currentTableRef.addEventListener('click', handleClick); } return () => { if (currentTableRef) { currentTableRef.removeEventListener('click', handleClick); } }; }, []);
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!