react+antd pro实现【列表可实时行内编辑】的弹窗表单组件
纯列表版效果展示:
① 初始无值,展示为唤醒按钮+文案外链
②点击按钮唤醒弹窗(简易版示意图)
③配置后
可编辑表格组件文档:
https://procomponents.ant.design/components/editable-table
纯列表组件代码:
表单引用:
// antd-pro的高级表单组件 import SchemaForm from '@/components/SchemaForm'; import AssetsTable from './AssetsTable'; --------------------------------------- <SchemaForm<API.AssetsProps> formRef={formRef} dataSource={dataSource.map((item: any) => { if (item.id === 'event_asset') { return { ...item, required: '请配置事件资产', fieldProps: { ...item.fieldProps, fetchListApi }, renderFormItem: (schema: any) => <AssetsTable {...schema.fieldProps} />, formItemProps: { rules: [ { required: true, validator(rule: any, value: any[]) { if (value && value.length) { if (value.length !== accounts?.length) { return Promise.reject(new Error('请为全部账户配置资产事件')); } if (value.find((itemDeep: any) => !itemDeep.deep_bid_type)) { return Promise.reject(new Error('请为全部账户配置深度出价方式')); } return Promise.resolve(); } return Promise.resolve(); }, }, ], }, }; } return item; })} submitter={false} autoFocusFirstInput={false} onValuesChange={(changedFields: any, allFields: any) => { // 表单值改变时的处理逻辑 }} />
组件代码:
import { useEffect, useState } from 'react'; import { Modal, Button, Input, message } from 'antd'; import { isEmpty } from 'lodash'; import type { ProColumns } from '@ant-design/pro-table'; import { EditableProTable } from '@ant-design/pro-table'; import { fetchAssetsList } from './services'; const { Search } = Input; type AssetsProps = { value?: any[]; onChange?: (value?: any[]) => void; /** 列表接口地址 */ fetchListApi?: string; }; /** 弹窗 */ const AssetsTable: React.FC<AssetsProps> = (props) => { const { value, onChange, fetchListApi } = props; const [data, setData] = useState<API.AssetsProps[]>(value || []); // 存储选中的数据 const [visible, setVisible] = useState<boolean>(false); const [dataSource, setDataSource] = useState<API.AssetsProps[]>([]); // 列表数据 const [tableColumns, setTableColumns] = useState<ProColumns[]>([]); const [tableParams, setTableParams] = useState<any>(); const [tableLoading, setTableLoading] = useState<boolean>(false); const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]); const [keys, setKeys] = useState<(string | number)[]>([]); const handleOk = () => { if (onChange) { onChange(data); setVisible(false);} }; const fetchList = async () => { setTableLoading(true); if (fetchListApi) { // 根据资产类型+投放账号调用头条已有的资产作为选择值 try { const res: any = await fetchAssetsList(fetchListApi, tableParams); if (res.result) { setTableColumns([]); const { tableHeaderList = [], tableList = [] } = res.result; // 当有存储值时,覆盖掉接口返回的对应数据 if (data && data.length) { const initObj = data.find((item: any) => item.account_id === id); const newList = tableList.map((item: any) => { if (initObj && initObj.id && item.id === initObj.id) { return initObj; } return item; }); setDataSource(newList); setEditableRowKeys(newList?.map((item: any) => item.id)); } else { setDataSource(tableList); setEditableRowKeys(tableList?.map((item: any) => item.id)); } // 深度优化目标下拉单选 setTableColumns([ ...tableHeaderList.map((item: any) => { if (item.dataIndex === 'deep_external_action') { return { ...item, width: 200, valueType: 'select', valueEnum: (row: any) => { const optionList = row?.deep_goals ?.filter((itemF: any) => itemF.optimization_name) .map((itemO: any) => { return { label: itemO.optimization_name, value: itemO.deep_external_action, }; }); const valueEnumObj = {}; optionList?.forEach((items: any) => { valueEnumObj[items.value] = { text: items.label }; }); return valueEnumObj; }, }; } // 深度出价方式需要动态获取 if (item.dataIndex === 'deep_bid_type') { return { ...item, width: 200, valueType: 'select', valueEnum: (row: any) => { let deepList: any[] = []; const hasChoose = row?.deep_goals?.find( (el: any) => el.deep_external_action === row.deep_external_action, ); if (hasChoose) { deepList = row?.deep_goals.find( (el: any) => el.deep_external_action === row.deep_external_action, )?.deep_bid_type || []; } else { deepList = row?.default_deep_bid_type || []; } const optionList = deepList?.map((itemO: any) => { return { label: itemO.name, value: itemO.id, }; }); const valueEnumObj = {}; optionList?.forEach((items: any) => { valueEnumObj[items.value] = { text: items.label }; }); return valueEnumObj; }, }; } return { ...item, editable: false }; }), ]); } setTableLoading(false); } catch (error) { // } } }; // 关键词搜索 useEffect(() => { if (!isEmpty(tableParams)) fetchList(); }, [tableParams]); useEffect(() => { setData(dataSource || []); }, [dataSource]); useEffect(() => { if (visible) { fetchList(); // 更新data if (value && Array.isArray(value) && value.length) { setData(value); } } }, [visible]); useEffect(() => { // 更新data if (value && Array.isArray(value) && value.length) { setData(value); } }, [value]); return ( <> <div> <Button onClick={() => setVisible(true)}> {data && data.length ? '已' : ''}选择事件资产{data && data.length ? ',点击配置' : ''} </Button> </div> <Modal width={1500} visible={visible} onCancel={() => setVisible(false)} onOk={handleOk}> <div> <Search addonBefore="资产名称" placeholder="请输入关键词搜索" allowClear onSearch={async (valueSearch: string) => setTableParams({ ...tableParams, asset_name: valueSearch }) } style={{ width: 300, marginBottom: '8px' }} /> <EditableProTable rowKey="id" loading={tableLoading} bordered request={async () => ({ data: dataSource, total: dataSource.length, success: true, })} tableAlertRender={false} value={dataSource} onChange={(valueNew: any[]) => { const newTableData = valueNew.map((item: any, index: number) => { // 深度优化目标的值改变时,清空本条数据的深度出价方式的值 if (item.id === dataSource[index].id) { if (item.deep_external_action !== dataSource[index].deep_external_action) { return { ...item, deep_bid_type: undefined }; } return item; } return item; }); setDataSource(newTableData); }} controlled recordCreatorProps={false} editable={{ type: 'multiple', editableKeys, actionRender: undefined, onChange: setEditableRowKeys, }} scroll={{ y: 320, x: 'max-content' }} columns={tableColumns} tableRender={undefined} pagination={false} rowSelection={{ type: 'radio', selectedRowKeys: keys, onChange: (selectedRowKeys: any, selectedRows: any) => { // 存储选中数据逻辑 }, }} /> {dataSource && dataSource.length ? <p>共 {dataSource.length} 条</p> : null} </div> </Modal> </> ); }; export default AssetsTable;
--------------------------------------以上代码可满足按钮唤醒弹窗并对列表数据进行行内修改的需求-------------------------------------
完整版需求效果展示:
其实就是多了左侧账号栏,要求给左侧每个账号进行配置,右侧展示可选列表,单项选择,可配置内容,其中选中项必须配置出价方式;
点击确定时校验是否满足要求,否则弹出提示;
重新点开弹窗时也要保留上次选择与配置的内容。
具体代码:
import { useEffect, useState, useRef, useContext } from 'react'; import { Modal, Button, Input, message } from 'antd'; import { isEmpty } from 'lodash'; import ScrollList from '@/components/ScrollList'; import type { ProColumns } from '@ant-design/pro-table'; import { EditableProTable } from '@ant-design/pro-table'; import { fetchAssetsList } from './services'; import SmartCreationContext from '../../context'; import styles from './Assets.less'; const { Search } = Input; type AssetsProps = { value?: any[]; onChange?: (value?: any[]) => void; /** 事件资产列表接口地址 */ fetchListApi?: string; }; /** 事件资产弹窗 */ const AssetsTable: React.FC<AssetsProps> = (props) => { const { value, onChange, fetchListApi } = props; const { dependentData, values } = useContext(SmartCreationContext); const { accounts = [] } = dependentData || {}; const { create_rules } = values || {}; const adsRef = useRef<any>(); const [data, setData] = useState<API.AssetsProps[]>(value || []); // 存储选中的数据 const [visible, setVisible] = useState<boolean>(false); const [dataSource, setDataSource] = useState<API.AssetsProps[]>([]); // 列表数据 const [tableColumns, setTableColumns] = useState<ProColumns[]>([]); const [tableParams, setTableParams] = useState<any>(); const [tableLoading, setTableLoading] = useState<boolean>(false); const [targetAccount, setTargetAccount] = useState<any>(''); const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]); const [keys, setKeys] = useState<(string | number)[]>([]); const handleOk = () => { if (onChange) { if (data.length === accounts.length) { if (data?.find((itemDeep: any) => !itemDeep.deep_bid_type)) { message.error('请为全部账户配置深度出价方式'); return; } onChange(data); setVisible(false); } else { message.error('请为全部账户配置资产事件'); } } }; const fetchList = async (id: string) => { setTargetAccount(id); setTableLoading(true); const trace_events = JSON.parse(sessionStorage.getItem('ASSETS_FORM') || ''); if (fetchListApi) { // 根据账号调用已有的资产作为选择值 try { const res: any = await fetchAssetsList(fetchListApi, { account_id: id, // 账号 ...tableParams, }); if (res.result) { setTableColumns([]); const { tableHeaderList = [], tableList = [] } = res.result; const dataTable = tableList.map((item: any) => { return { ...item, account_id: id }; }); // 当有存储值时,覆盖掉接口返回的对应数据 if (data && data.length) { const initObj = data.find((item: any) => item.account_id === id); const newList = dataTable.map((item: any) => { if (initObj && initObj.id && item.id === initObj.id) { return initObj; } return item; }); setDataSource(newList); setEditableRowKeys(newList?.map((item: any) => item.id)); } else { setDataSource(dataTable); setEditableRowKeys(dataTable?.map((item: any) => item.id)); } // 深度优化目标下拉单选 setTableColumns([ ...tableHeaderList.map((item: any) => { if (item.dataIndex === 'deep_external_action') { return { ...item, width: 200, valueType: 'select', valueEnum: (row: any) => { const optionList = row?.deep_goals ?.filter((itemF: any) => itemF.optimization_name) .map((itemO: any) => { return { label: itemO.optimization_name, value: itemO.deep_external_action, }; }); const valueEnumObj = {}; optionList?.forEach((items: any) => { valueEnumObj[items.value] = { text: items.label }; }); return valueEnumObj; }, }; } // 深度出价方式需要动态获取 if (item.dataIndex === 'deep_bid_type') { return { ...item, width: 200, valueType: 'select', valueEnum: (row: any) => { let deepList: any[] = []; const hasChoose = row?.deep_goals?.find( (el: any) => el.deep_external_action === row.deep_external_action, ); if (hasChoose) { deepList = row?.deep_goals.find( (el: any) => el.deep_external_action === row.deep_external_action, )?.deep_bid_type || []; } else { deepList = row?.default_deep_bid_type || []; } const optionList = deepList?.map((itemO: any) => { return { label: itemO.name, value: itemO.id, }; }); const valueEnumObj = {}; optionList?.forEach((items: any) => { valueEnumObj[items.value] = { text: items.label }; }); return valueEnumObj; }, }; } return { ...item, width: 200, editable: false, }; }), ]); } setTableLoading(false); } catch (error) { // } } }; // 关键词搜索 useEffect(() => { if (!isEmpty(tableParams)) fetchList(targetAccount); }, [tableParams]); useEffect(() => { if (dataSource.length) { const newList = data?.map((item: any) => { if (item.account_id === targetAccount) { const newObj = dataSource.find((itemList: any) => itemList.id === item.id); return newObj; } return item; }); setData(newList); } else { setData([]); } }, [dataSource]); // 当前选中key useEffect(() => { if (targetAccount) { if (data && data.length) { const idList = data .filter((item) => item.account_id === targetAccount) .map((item: API.AssetsProps) => { return item.id; }); setKeys(idList); } else { setKeys([]); } } }, [targetAccount, data]); // 打开弹窗时,默认左侧广告投放列表选中第一项,并进行列表请求 useEffect(() => { if (accounts && accounts[0]) { adsRef?.current?.selectItem(accounts[0]); setTargetAccount(accounts[0].id); if (visible) { fetchList(accounts[0].id || ''); // 更新data if (value && Array.isArray(value) && value.length) { setData(value); } } } }, [visible]); useEffect(() => { // 更新data if (value && Array.isArray(value) && value.length) { setData(value); } }, [value]); return ( <> <div> <Button onClick={() => { const trace_events = JSON.parse(sessionStorage.getItem('ASSETS_FORM') || ''); if (!trace_events?.app_type) { message.error('请选择资产类型'); return; } setVisible(true); }} > {data && data.length ? '已' : ''}选择事件资产{data && data.length ? ',点击配置' : ''} </Button> {data && data.length ? null : ( <span style={{ marginLeft: '8px' }}> 找不到适用的资产,去 <a href="/" key="createAssets" target="_blank" rel="noreferrer" > 创建资产 </a> ! </span> )} </div> <Modal width={1500} visible={visible} onCancel={() => setVisible(false)} onOk={handleOk}> <div className={styles['existring-ads-list']}> <div style={{ width: '16%', marginRight: '16px' }}> <ScrollList cRef={adsRef} list={accounts} title="账号名称" onChange={fetchList} /> </div> <div style={{ width: '84%' }}> <Search addonBefore="资产名称" placeholder="请输入关键词搜索" allowClear onSearch={async (valueSearch: string) => setTableParams({ ...tableParams, asset_name: valueSearch }) } style={{ width: 300, marginBottom: '8px' }} /> <EditableProTable rowKey="id" loading={tableLoading} bordered request={async () => ({ data: dataSource, total: dataSource.length, success: true, })} tableAlertRender={false} className={styles['exist-table-edit']} value={dataSource} onChange={(valueNew: any[]) => { const newTableData = valueNew.map((item: any, index: number) => { // 深度优化目标的值改变时,清空本条数据的深度出价方式的值 if (item.id === dataSource[index].id) { if (item.deep_external_action !== dataSource[index].deep_external_action) { return { ...item, deep_bid_type: undefined }; } return item; } return item; }); setDataSource(newTableData); }} controlled recordCreatorProps={false} editable={{ type: 'multiple', editableKeys, actionRender: undefined, onChange: setEditableRowKeys, }} scroll={{ y: 320, x: 'max-content' }} columns={tableColumns} tableRender={undefined} pagination={false} rowSelection={{ type: 'radio', selectedRowKeys: keys, onChange: (selectedRowKeys: any, selectedRows: any) => { if (data && data.length) { // hasAccount = 当前账号是否存在 const hasAccount = data.filter((item) => item.account_id === targetAccount); if (hasAccount && hasAccount.length) { const newList = data.map((item: API.AssetsProps) => { if (item.account_id === targetAccount) { return selectedRows[0]; } return item; }); setData(newList); } else { setData([...data, ...selectedRows]); } } else { setData(selectedRows); } }, }} /> {dataSource && dataSource.length ? <p>共 {dataSource.length} 条</p> : null} </div> </div> </Modal> </> ); }; export default AssetsTable;