纯前端实现 导入/导出/模板下载功能
项目开发过程中,有时侯需要前端自己去实现导入/导出和模板下载功能;
目前已react x项目为例;实现如下图功能操作项;
每个对应的tab都有属于自己的功能;
导入功能支持JSON格式和CSV格式(模板下载配置也是一样的/导出也一样)。
测重代码
// 导出当前配置const handleExport = (itemInfo: any) => {const currentTabData = currentLayoutMap;const blob = new Blob([JSON.stringify(currentTabData, null, 2)], {type: 'application/json',});const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = `${itemInfo.id}-layout-config.json`;document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);};const handleExportToCSV = (itemInfo: any) => {// 准备表头和数据const headers = ['Key','priority','roleDesc','dataSource','roleDescDetail','isUsedForCreative','useDemandTag','saveToElem','expressSet','fixedField','isNeedHightLight',];
let csvContent = [headers.join(',')]; // 初始化 CSV 内容,加入表头
Object.entries(currentLayoutMap).forEach(([key, value]) => {const row = [key,value.priority,value.roleDesc,value.dataSource,value.roleDescDetail,value.isUsedForCreative,value.useDemandTag,value.saveToElem,value.expressSet,value.fixedField,value.isNeedHightLight,];csvContent.push(row.map((item) => `"${String(item).replace(/"/g, '""')}"`).join(','),); // 处理可能存在的逗号或引号
});
// 添加 BOM 头部,确保 Excel 正确识别 UTF-8 编码
const bom = new Uint8Array([0xef, 0xbb, 0xbf]);
// 创建 Blob 并触发下载
const blob = new Blob([bom, csvContent.join('\n')], {type: 'text/csv;charset=utf-8;',
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${itemInfo.id}-layout-config.csv`; // 设置默认文件名
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);};const handleImportedData = (content: Record<string, LayoutConfig>,itemInfo: any,currentLayoutMap: any,) => {const keys = Object.keys(content);// 检查是否只有一个 keyif (keys.length !== 1) {message.error('仅支持单个配置项的导入');return;}
const importKey = keys[0];
// 检查 key 是否存在于 filteredElementConfigList 中(白名单)
const isValidKey = filteredElementConfigList.some((config) => config.tplField === importKey,
);
if (!isValidKey) {message.error(`字段名:${importKey}不允许导入;该值在已知配置项中不存在`);return;
}
// 检查 key 是否已存在
if (currentLayoutMap.hasOwnProperty(importKey)) {message.error(`字段 ${importKey} 已存在,不能重复导入`);return;
}
setNewId(itemInfo.id);
const updatedLayoutPlanMap = {...currentLayoutMap,...content,
};
const newImportedKeys = Object.keys(content);
setCurrentLayoutMap(updatedLayoutPlanMap);
setNewlyAddedKeys((prevKeys) => [...prevKeys, ...newImportedKeys]);
onUpdate?.(itemInfo.id, updatedLayoutPlanMap);
message.success(`导入成功`);
setFileList([]);
return false;};const getImportProps = (itemInfo: any, currentLayoutMap: any) => ({accept: '.json,.csv',name: 'file',multiple: false,fileList: fileList, // 控制上传列表beforeUpload(file: any) {const reader = new FileReader();reader.onload = (event: any) => {try {let content: Record<string, LayoutConfig> = {};if (file.name.endsWith('.json')) {content = JSON.parse(event.target.result as string);} else if (file.name.endsWith('.csv')) {// 使用 PapaParse 解析 CSV(推荐)Papa.parse(event.target.result as string, {header: true,skipEmptyLines: true,complete: (results: any) => {const csvData = results.data as Array<Record<string, any>>;if (csvData.length === 0) {message.error('CSV文件内容为空');return;}// 转换为 layoutPlanMap 结构const importedMap: Record<string, LayoutConfig> = {};csvData.forEach((row) => {const key = row['Key'];if (!key) {message.error('CSV中缺少 Key 字段');return;}importedMap[key] = {priority: parseInt(row['priority']) || 0,roleDesc: row['roleDesc'] || '',dataSource: row['dataSource'] || '',roleDescDetail: row['roleDescDetail'] || '',isUsedForCreative: row['isUsedForCreative'] === 'true',useDemandTag: row['useDemandTag'] === 'true',saveToElem: row['saveToElem'] === 'true',expressSet: row['expressSet'] || '',fixedField: row['fixedField'] || '',isNeedHightLight: row['isNeedHightLight'] === 'true',};});handleImportedData(importedMap, itemInfo, currentLayoutMap);},});} else {message.error('仅支持 .json 或 .csv 格式的文件');return;}if (file.name.endsWith('.json')) {handleImportedData(content, itemInfo, currentLayoutMap);}} catch (error) {message.error('文件格式错误,请检查文件内容');}};reader.readAsText(file);return false;
},onChange(info: any) {if (info.file.status === 'error') {message.error(`${info.file.name} 文件导入失败`);}
},});const showModal = (itemInfo: any, actionType: 'export' | 'template') => {setFormatData(itemInfo);setCurrentExportAction(actionType); // 设置当前操作类型formatOpenForm.setFieldsValue({ radioGroup: exportType }); // 同步初始值setIsModalOpen(true);};const downloadJsonTemplate = () => {const template = {title: {priority: 1,roleDesc: '',dataSource: '',roleDescDetail: '',isUsedForCreative: true,useDemandTag: false,saveToElem: false,expressSet: '',isNeedHightLight: false,fixedField: '',},};const dataStr = JSON.stringify(template, null, 2);const blob = new Blob([dataStr], { type: 'application/json' });const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = 'template-layout-config.json';a.click();URL.revokeObjectURL(url);};const downloadCsvTemplate = () => {const headers = ['Key','priority','roleDesc','dataSource','roleDescDetail','isUsedForCreative','useDemandTag','saveToElem','expressSet','fixedField','isNeedHightLight',];const csvContent = [headers.join(','),'title,1,,,,true,false,false,,,"false"',].join('\n');const bom = new Uint8Array([0xef, 0xbb, 0xbf]);const blob = new Blob([bom, csvContent], {type: 'text/csv;charset=utf-8;',});const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = 'template-layout-config.csv';a.click();URL.revokeObjectURL(url);};const handleOkExport = async () => {try {const values = await formatOpenForm.validateFields();const selectedType = values.radioGroup;if (currentExportAction === 'export') {// 正常导出当前 tab 的配置if (selectedType === 'json') {handleExport(formatData);} else {handleExportToCSV(formatData);}} else if (currentExportAction === 'template') {// 下载模板if (selectedType === 'json') {downloadJsonTemplate();} else {downloadCsvTemplate();}}} finally {// 关闭弹窗前重置表单和状态setIsModalOpen(false);formatOpenForm.resetFields(); // 重置表单formatOpenForm.setFieldsValue({ radioGroup: 'json' }); // 设置默认值setExportType('json'); // 更新本地状态}};const handleCancelExport = () => {setIsModalOpen(false);formatOpenForm.resetFields(); // 重置表单formatOpenForm.setFieldsValue({ radioGroup: 'json' }); // 设置默认值setExportType('json'); // 更新本地状态};<divstyle={{ display: 'flex', justifyContent: 'flex-end' }}><Popoverplacement="topLeft"title={'导入布局配置提示'}content={content2}><Upload{...getImportProps(itemInfo, currentLayoutMap)}style={{ marginBottom: 10, marginTop: 10 }}><Buttonstyle={{ marginBottom: 10, marginTop: 10 }}type="primary"disabled={filteredElementConfigList.length === 0}icon={<UploadOutlined />}>导入布局配置</Button></Upload></Popover><Buttonstyle={buttonStyle}type="primary"onClick={() => showModal(itemInfo, 'export')}disabled={!showFlag}>导出当前布局方案</Button><Buttonstyle={buttonStyle}onClick={() => showModal(itemInfo, 'template')}>模板下载</Button></div>
完整代码如下:
import { insmacCreativeTplSchemaLayoutPlanFacadeCreate } from '@/needleServices/generativeCard';
import { CopyOutlined, UploadOutlined } from '@ant-design/icons';
import {Card,Button,Checkbox,Collapse,Descriptions,Form,Input,message,Modal,Popconfirm,Popover,Radio,Spin,Switch,Tabs,Tooltip,Upload,Select,
} from 'antd';
import dayjs from 'dayjs';
import { isEqual } from 'lodash';
import Papa from 'papaparse';
import React, {forwardRef,useEffect,useImperativeHandle,useState,
} from 'react';
import AddOrCopy from './AddOrCopy';const { TextArea } = Input;
interface LayoutConfig {priority: number; // 优先级roleDesc: string; // 角色描述dataSource: string; // 数据源roleDescDetail: string; // 角色详细描述isUsedForCreative: boolean; // 是否用于生成创意useDemandTag: boolean; // 是否使用需求标签saveToElem: boolean; // 保存到创意元素中expressSet: string; // 表达式集合isNeedHightLight: boolean; // 是否需要高亮fixedField: string; // 固定字段值
}
const LayoutPlanModuleTabs = forwardRef((props: any, ref: any) => {const {initialValueData,layoutPlanList,onUpdate,pushFacadeUpdate,saveFieldLoading,} = props;const [activeKey, setActiveKey] = useState('0'); // 当前激活的 Tabconst [currentLayoutMap, setCurrentLayoutMap] = useState<Record<string, LayoutConfig>>({}); // 当前 Tab 的 layoutPlanMapconst [newAddLayoutOpen, setNewAddLayoutOpen] = useState(false);const [newLayoutForm] = Form.useForm();const [addcopyType, setAddcopyType] = useState('add'); // 新增/复制const [addNewLayoutOpen, setAddNewLayoutOpen] = useState(false); // 新增配置弹框const [addNewLayoutOpenForm] = Form.useForm(); // 新增布局配置const [isConfirmDisabled, setIsConfirmDisabled] = useState(true); // 控制确认按钮的禁用状态const [newId, setNewId] = useState<any>(null);const [initialCollapseData, setInitialCollapseData] = useState<Record<string, LayoutConfig>>({}); // 初始 Collapse 数据const [copyItem, setCopyItem] = useState<any>({});const [newlyAddedKeys, setNewlyAddedKeys] = useState<string[]>([]); // 当前 Tab 下新增的字段 keyconst [filteredElementConfigList, setFilteredElementConfigList] = useState<any[]>([]); // 过滤新增配置下拉数据const optionsWithDisabled = [{ label: '用于创意 (isUsedForCreative)', value: 'isUsedForCreative' },{ label: '使用需求标签 (useDemandTag)', value: 'useDemandTag' },];const [fileList, setFileList] = useState<any[]>([]);const [isModalOpen, setIsModalOpen] = useState(false);const [exportType, setExportType] = useState<string>('json'); // 默认为 JSONconst [formatOpenForm] = Form.useForm(); // 新增布局配置const [formatData, setFormatData] = useState<any>(null);const [currentExportAction, setCurrentExportAction] = useState<'export' | 'template'>('export');useImperativeHandle(ref, () => ({reset: () => {setNewlyAddedKeys([]); // 清空新增字段状态// 重置当前 Tab 的 layoutPlanMap 为初始值if (layoutPlanList.length > 0) {const initialMap = layoutPlanList[0]?.layoutPlanMap || {};setCurrentLayoutMap(initialMap);setInitialCollapseData(initialMap); // 同步初始数据}setActiveKey('0'); // 恢复到第一个 Tab},}));// 确认/复制方案const handleOkNewAddLayoutOpen = () => {try {newLayoutForm.validateFields().then(async (values) => {let newcode = values.code;const isDuplicate = layoutPlanList.some((item: any) =>item.templateCode === newcode || item.code === newcode,);if (isDuplicate) {return message.warning('方案code不要和该模板下面已有的code重复,请修改',);} else {let params = {...values,isDeleted: 'N',layoutPlan:addcopyType === 'add' ? {} : { ...copyItem?.layoutPlanMap },};await insmacCreativeTplSchemaLayoutPlanFacadeCreate(params);setNewAddLayoutOpen(false);message.success(addcopyType === 'add' ? '新增布局方案成功' : '复制布局方案成功',);newLayoutForm?.resetFields();props.handleConfirm();}}).catch((e: any) => {console.log(e, '失败');setNewAddLayoutOpen(true);});} catch (error) {console.error('报错了:', error);setNewAddLayoutOpen(false);}};// 取消方案const handleCancelNewAddLayoutOpen = () => {setNewAddLayoutOpen(false);newLayoutForm?.resetFields();};const getFilteredElementConfigList = (layoutPlanMap: any) => {const uniqueElementConfigList = Array.from(new Map(initialValueData?.elementConfigList?.map((item: any) => [item.tplField,item,]),).values(),);const layoutPlanKeys = Object.keys(layoutPlanMap || {});return uniqueElementConfigList.filter((item: any) => !layoutPlanKeys.includes(item.tplField),);};useEffect(() => {// 初始化时设置第一个 Tab 的 layoutPlanMap 和初始数据if (layoutPlanList.length > 0) {const initialMap =layoutPlanList[parseInt(activeKey)]?.layoutPlanMap || {};setCurrentLayoutMap(initialMap);setInitialCollapseData(initialMap); // 初始化 Collapse 数据}// 每个tab新增配置过滤项const currentTab = layoutPlanList[parseInt(activeKey)];if (currentTab) {const initialMap = currentTab.layoutPlanMap || {};setCurrentLayoutMap(initialMap);const filteredList = getFilteredElementConfigList(initialMap);setFilteredElementConfigList(filteredList);}setNewlyAddedKeys([]); // 清空新增字段状态}, [layoutPlanList, activeKey]);// 更新当前数据const updateField = (key: string, field: string, value: any) => {// 定义布尔字段的列表const booleanFields = ['isUsedForCreative','useDemandTag','saveToElem','isNeedHightLight',];setCurrentLayoutMap((prevData) => ({...prevData,[key]: {...prevData[key],[field]:field === 'priority'? !isNaN(Number(value))? Number(value): 1: booleanFields.includes(field)? Boolean(value): value,},}));};// 复制当前 Tab 的内容const handleCopyTabContent = (item: any, index: number) => {setAddcopyType('copy');setCopyItem(item);setActiveKey(index.toString());newLayoutForm.setFieldsValue({...item,});setNewAddLayoutOpen(true);};const add = () => {setAddcopyType('add');setNewAddLayoutOpen(true);newLayoutForm.setFieldsValue({templateCode: initialValueData.templateCode,}); // 设置表单值};const onEdit = (targetKey: React.MouseEvent | React.KeyboardEvent | string,action: 'add' | 'remove',) => {if (action === 'add') {add();}};const content = (<div><div style={{ color: '#f1bb0b' }}><p>新增配置前请先确认是否存在已编辑的数据或已新增但未修改的数据,</p><p>若存在请先保存后再进行新增配置操作,否则数据将无法保存。</p></div><p>若不存在请忽略该提示。</p></div>);const content2 = (<div><div style={{ color: '#f1bb0b' }}><p> 目前暂不支持批量导入; </p><p> 导入之前请先确认是否存在已编辑的数据 或 已新增但未修改的数据,</p><p> 若存在请先保存后再进行导入操作,否则数据将无法保存。</p><p>若不存在请忽略该提示。</p></div><p>导入后请编辑该数据进行保存, 否则数据无法真实保存</p></div>);const buttonStyle = {marginBottom: 10,marginTop: 10,marginLeft: 10,};// 支持配置项多选const handleOkAddNewLayoutOpen = async () => {try {const values = await addNewLayoutOpenForm.validateFields();let selectedKeys: string[] = [];if (typeof values.dynamicConfiguration === 'string') {selectedKeys = values.dynamicConfiguration.split(',')?.map((key: any) => key.trim());} else if (Array.isArray(values.dynamicConfiguration)) {selectedKeys = values.dynamicConfiguration;} else {message.error('配置项格式无效');return;}const newConfigs: Record<string, LayoutConfig> = {};for (const key of selectedKeys) {if (layoutPlanList?.layoutPlanMap?.[key]) {message.error(`配置项 "${key}" 已存在,请勿重复添加`);return;}const newConfig: LayoutConfig = {priority: 1, // 默认优先级roleDesc: '', // 默认角色描述dataSource: '', // 默认数据源roleDescDetail: '', // 默认详细描述isUsedForCreative: true, // 默认不用于创意useDemandTag: false, // 默认不使用需求标签saveToElem: false, // 保存到创意元素中expressSet: '', // 默认表达式集合isNeedHightLight: false, // 默认不需要高亮fixedField: '', // 固定字段值};// 将新配置存储到 newConfigsnewConfigs[key] = newConfig;}const updatedLayoutPlanMap = {...currentLayoutMap,...newConfigs,};// 更新本地状态setCurrentLayoutMap(updatedLayoutPlanMap);setNewlyAddedKeys((prevKeys) => [...prevKeys,...Object.keys(newConfigs),]);// 通知父组件数据已更新,并传递当前 Tab ID 和更新后的数据onUpdate?.(newId, updatedLayoutPlanMap);setIsConfirmDisabled(true);setAddNewLayoutOpen(false);addNewLayoutOpenForm.resetFields();message.success('新增配置成功,请编辑或直接保存当前字段');} catch (error) {console.error('新增配置验证失败:', error);}};// 保存此字段const handleSaveField = (formattedData: any) => {const currentTabIndex = parseInt(activeKey);const currentTabInfo = layoutPlanList[currentTabIndex]; // 获取当前 Tab 的信息const [key, itemInfo] = Object.entries(formattedData)[0]; // 获取当前字段的 key 和值const initialData = initialCollapseData[key] || {}; // 获取该字段的初始值const isNewlyAdded = newlyAddedKeys.includes(key); // 判断是否是新增的数据// 判断是否是已更改的数据,并在深比较前,确保字段类型一致const normalizeValue = (data: any) => ({...data,priority: Number(data.priority), // 确保 priority 是数字类型isUsedForCreative: Boolean(data.isUsedForCreative), // 确保布尔字段是布尔类型useDemandTag: Boolean(data.useDemandTag),saveToElem: Boolean(data.saveToElem),isNeedHightLight: Boolean(data.isNeedHightLight),});const normalizedItemInfo = normalizeValue(itemInfo);const normalizedInitialData = normalizeValue(initialData);const isValueChanged = !isEqual(normalizedItemInfo, normalizedInitialData); // 使用 lodash 的 isEqual 深比较if (isNewlyAdded && !isValueChanged) {message.info('新增的数据未修改,不需保存');return;}if (!isNewlyAdded && !isValueChanged) {message.info('当前内容无更改,不需保存');return;}const params = {id: currentTabInfo.id || null,layoutPlan: { ...formattedData },};pushFacadeUpdate(params);};// 处理取消逻辑const handleCancel = (key: string) => {const initialData = initialCollapseData[key] || {};setCurrentLayoutMap((prevData) => ({...prevData,[key]: initialData, // 只还原当前 Collapse 的数据}));message.info('已取消更改');};const showFlag = Object.entries(currentLayoutMap).length > 0;// 导出当前配置const handleExport = (itemInfo: any) => {const currentTabData = currentLayoutMap;const blob = new Blob([JSON.stringify(currentTabData, null, 2)], {type: 'application/json',});const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = `${itemInfo.id}-layout-config.json`;document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);};const handleExportToCSV = (itemInfo: any) => {// 准备表头和数据const headers = ['Key','priority','roleDesc','dataSource','roleDescDetail','isUsedForCreative','useDemandTag','saveToElem','expressSet','fixedField','isNeedHightLight',];let csvContent = [headers.join(',')]; // 初始化 CSV 内容,加入表头Object.entries(currentLayoutMap).forEach(([key, value]) => {const row = [key,value.priority,value.roleDesc,value.dataSource,value.roleDescDetail,value.isUsedForCreative,value.useDemandTag,value.saveToElem,value.expressSet,value.fixedField,value.isNeedHightLight,];csvContent.push(row.map((item) => `"${String(item).replace(/"/g, '""')}"`).join(','),); // 处理可能存在的逗号或引号});// 添加 BOM 头部,确保 Excel 正确识别 UTF-8 编码const bom = new Uint8Array([0xef, 0xbb, 0xbf]);// 创建 Blob 并触发下载const blob = new Blob([bom, csvContent.join('\n')], {type: 'text/csv;charset=utf-8;',});const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = `${itemInfo.id}-layout-config.csv`; // 设置默认文件名document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);};const handleImportedData = (content: Record<string, LayoutConfig>,itemInfo: any,currentLayoutMap: any,) => {const keys = Object.keys(content);// 检查是否只有一个 keyif (keys.length !== 1) {message.error('仅支持单个配置项的导入');return;}const importKey = keys[0];// 检查 key 是否存在于 filteredElementConfigList 中(白名单)const isValidKey = filteredElementConfigList.some((config) => config.tplField === importKey,);if (!isValidKey) {message.error(`字段名:${importKey}不允许导入;该值在已知配置项中不存在`);return;}// 检查 key 是否已存在if (currentLayoutMap.hasOwnProperty(importKey)) {message.error(`字段 ${importKey} 已存在,不能重复导入`);return;}setNewId(itemInfo.id);const updatedLayoutPlanMap = {...currentLayoutMap,...content,};const newImportedKeys = Object.keys(content);setCurrentLayoutMap(updatedLayoutPlanMap);setNewlyAddedKeys((prevKeys) => [...prevKeys, ...newImportedKeys]);onUpdate?.(itemInfo.id, updatedLayoutPlanMap);message.success(`导入成功`);setFileList([]);return false;};const getImportProps = (itemInfo: any, currentLayoutMap: any) => ({accept: '.json,.csv',name: 'file',multiple: false,fileList: fileList, // 控制上传列表beforeUpload(file: any) {const reader = new FileReader();reader.onload = (event: any) => {try {let content: Record<string, LayoutConfig> = {};if (file.name.endsWith('.json')) {content = JSON.parse(event.target.result as string);} else if (file.name.endsWith('.csv')) {// 使用 PapaParse 解析 CSV(推荐)Papa.parse(event.target.result as string, {header: true,skipEmptyLines: true,complete: (results: any) => {const csvData = results.data as Array<Record<string, any>>;if (csvData.length === 0) {message.error('CSV文件内容为空');return;}// 转换为 layoutPlanMap 结构const importedMap: Record<string, LayoutConfig> = {};csvData.forEach((row) => {const key = row['Key'];if (!key) {message.error('CSV中缺少 Key 字段');return;}importedMap[key] = {priority: parseInt(row['priority']) || 0,roleDesc: row['roleDesc'] || '',dataSource: row['dataSource'] || '',roleDescDetail: row['roleDescDetail'] || '',isUsedForCreative: row['isUsedForCreative'] === 'true',useDemandTag: row['useDemandTag'] === 'true',saveToElem: row['saveToElem'] === 'true',expressSet: row['expressSet'] || '',fixedField: row['fixedField'] || '',isNeedHightLight: row['isNeedHightLight'] === 'true',};});handleImportedData(importedMap, itemInfo, currentLayoutMap);},});} else {message.error('仅支持 .json 或 .csv 格式的文件');return;}if (file.name.endsWith('.json')) {handleImportedData(content, itemInfo, currentLayoutMap);}} catch (error) {message.error('文件格式错误,请检查文件内容');}};reader.readAsText(file);return false;},onChange(info: any) {if (info.file.status === 'error') {message.error(`${info.file.name} 文件导入失败`);}},});const showModal = (itemInfo: any, actionType: 'export' | 'template') => {setFormatData(itemInfo);setCurrentExportAction(actionType); // 设置当前操作类型formatOpenForm.setFieldsValue({ radioGroup: exportType }); // 同步初始值setIsModalOpen(true);};const downloadJsonTemplate = () => {const template = {title: {priority: 1,roleDesc: '',dataSource: '',roleDescDetail: '',isUsedForCreative: true,useDemandTag: false,saveToElem: false,expressSet: '',isNeedHightLight: false,fixedField: '',},};const dataStr = JSON.stringify(template, null, 2);const blob = new Blob([dataStr], { type: 'application/json' });const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = 'template-layout-config.json';a.click();URL.revokeObjectURL(url);};const downloadCsvTemplate = () => {const headers = ['Key','priority','roleDesc','dataSource','roleDescDetail','isUsedForCreative','useDemandTag','saveToElem','expressSet','fixedField','isNeedHightLight',];const csvContent = [headers.join(','),'title,1,,,,true,false,false,,,"false"',].join('\n');const bom = new Uint8Array([0xef, 0xbb, 0xbf]);const blob = new Blob([bom, csvContent], {type: 'text/csv;charset=utf-8;',});const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = 'template-layout-config.csv';a.click();URL.revokeObjectURL(url);};const handleOkExport = async () => {try {const values = await formatOpenForm.validateFields();const selectedType = values.radioGroup;if (currentExportAction === 'export') {// 正常导出当前 tab 的配置if (selectedType === 'json') {handleExport(formatData);} else {handleExportToCSV(formatData);}} else if (currentExportAction === 'template') {// 下载模板if (selectedType === 'json') {downloadJsonTemplate();} else {downloadCsvTemplate();}}} finally {// 关闭弹窗前重置表单和状态setIsModalOpen(false);formatOpenForm.resetFields(); // 重置表单formatOpenForm.setFieldsValue({ radioGroup: 'json' }); // 设置默认值setExportType('json'); // 更新本地状态}};const handleCancelExport = () => {setIsModalOpen(false);formatOpenForm.resetFields(); // 重置表单formatOpenForm.setFieldsValue({ radioGroup: 'json' }); // 设置默认值setExportType('json'); // 更新本地状态};return (<><Card><Spin spinning={saveFieldLoading}><Tabstype="editable-card"activeKey={activeKey}onChange={(key) => setActiveKey(key)}onEdit={onEdit}items={layoutPlanList?.map((itemInfo: any, index: any) => ({key: index.toString(),label: (<div style={{ display: 'flex', alignItems: 'center' }}><span>{itemInfo?.name}</span><Tooltipplacement="bottom"title={'复制此布局方案'}arrow={true}><Button type="link" style={{ marginLeft: 8 }}><CopyOutlinedonClick={(e) => {e.stopPropagation();handleCopyTabContent(itemInfo, index);}}/></Button></Tooltip></div>),children: (<><div><div style={{ fontSize: 16 }}><div>模板代码: {itemInfo?.templateCode} </div><div>当前方案Code: {itemInfo?.code} </div><div>创建时间:{dayjs(itemInfo?.gmtCreate).format('YYYY-MM-DD HH:mm:ss',)}</div></div><divstyle={{display: 'flex',justifyContent: 'space-between',}}>{showFlag ? (<div style={{ fontSize: 22, marginTop: 10 }}>编辑当前布局方案</div>) : (<div></div>)}<divstyle={{ display: 'flex', justifyContent: 'flex-end' }}><Popoverplacement="topLeft"title={'导入布局配置提示'}content={content2}><Upload{...getImportProps(itemInfo, currentLayoutMap)}style={{ marginBottom: 10, marginTop: 10 }}><Buttonstyle={{ marginBottom: 10, marginTop: 10 }}type="primary"disabled={filteredElementConfigList.length === 0}icon={<UploadOutlined />}>导入布局配置</Button></Upload></Popover><Buttonstyle={buttonStyle}type="primary"onClick={() => showModal(itemInfo, 'export')}disabled={!showFlag}>导出当前布局方案</Button><Buttonstyle={buttonStyle}onClick={() => showModal(itemInfo, 'template')}>模板下载</Button></div></div></div>{showFlag ? (<Collapse accordion>{Object.entries(currentLayoutMap).map(([key, itemInfo]) => (<Collapse.Panelkey={key}header={`${key}${itemInfo.roleDesc ? `(${itemInfo.roleDesc})` : ''}`}><Descriptions column={1} layout="vertical"><Descriptions.Item label="优先级 (priority)"><Inputvalue={itemInfo.priority}onChange={(e) =>updateField(key, 'priority', e.target.value)}placeholder="请填写优先级"/></Descriptions.Item><Descriptions.Item label="角色描述 (roleDesc)"><Inputvalue={itemInfo.roleDesc}onChange={(e) =>updateField(key, 'roleDesc', e.target.value)}placeholder="请填写角色描述"/></Descriptions.Item><Descriptions.Item label="数据源 (dataSource)"><Inputvalue={itemInfo.dataSource}onChange={(e) =>updateField(key,'dataSource',e.target.value,)}placeholder="请填写数据源"/></Descriptions.Item><Descriptions.Item label="角色详细描述 (roleDescDetail)"><div><TextAreastyle={{ width: 668 }}value={itemInfo.roleDescDetail}placeholder="请填写角色详细描述"autoSize={{ minRows: 3, maxRows: 5 }}onChange={(e) =>updateField(key,'roleDescDetail',e.target.value,)}/><Checkbox.Groupstyle={{marginTop: 15,display: 'flex',flexDirection: 'column',}}options={optionsWithDisabled}value={[itemInfo.isUsedForCreative? 'isUsedForCreative': undefined,itemInfo.useDemandTag? 'useDemandTag': undefined,].filter((val) => val !== undefined)}onChange={(checkedValues) => {const isUsedForCreative =checkedValues.includes('isUsedForCreative',);const useDemandTag =checkedValues.includes('useDemandTag');updateField(key,'isUsedForCreative',isUsedForCreative,);updateField(key,'useDemandTag',useDemandTag,);}}/></div></Descriptions.Item><Descriptions.Item label="表达式集合 (expressSet)"><div><TextAreavalue={itemInfo.expressSet}placeholder="请填写表达式集合"autoSize={{ minRows: 3, maxRows: 5 }}onChange={(e) =>updateField(key,'expressSet',e.target.value,)}style={{ width: 668 }}/><div style={{ marginTop: 15 }}><SwitchcheckedChildren="开启"unCheckedChildren="关闭"checked={itemInfo.isNeedHightLight? itemInfo.isNeedHightLight: false}onChange={(checked) => {updateField(key,'isNeedHightLight',checked,);}}/><span style={{ marginLeft: '8px' }}>需要高亮 (isNeedHightLight)</span></div></div></Descriptions.Item><Descriptions.Item label="固定字段值 (fixedField)"><div><TextAreastyle={{ width: 668 }}value={itemInfo.fixedField}autoSize={{ minRows: 3, maxRows: 5 }}onChange={(e) =>updateField(key,'fixedField',e.target.value,)}placeholder="请填写固定字段值"/><div style={{ marginTop: 15 }}><SwitchcheckedChildren="开启"unCheckedChildren="取消"checked={itemInfo.saveToElem? itemInfo.saveToElem: false}onChange={(checked) => {updateField(key, 'saveToElem', checked);}}/><span style={{ marginLeft: '8px' }}>保存到创意元素中 (saveToElem)</span></div></div></Descriptions.Item></Descriptions><divstyle={{display: 'flex',justifyContent: 'flex-end',marginTop: '16px',}}><Tooltipplacement="top"title={'默认初始数据,请谨慎操作'}arrow={true}><Buttonstyle={{ marginRight: 10 }}onClick={() => handleCancel(key)}>取消</Button></Tooltip><Popconfirmplacement="top"title={'保存当前操作'}description={'请确认当前更改的内容是否完善'}onConfirm={() => {const currentPanelData =currentLayoutMap[key];handleSaveField({ [key]: currentPanelData });}}okText="确认"cancelText="取消"><Buttontype="primary"loading={saveFieldLoading}>保存此字段</Button></Popconfirm></div></Collapse.Panel>),)}</Collapse>) : (<><Card><divstyle={{textAlign: 'center',padding: '20px',color: '#999',}}>当前方案暂无数据-请手动新增配置信息</div></Card></>)}<div style={{ display: 'flex', justifyContent: 'flex-end' }}><Popoverplacement="topLeft"title={'新增配置'}content={content}><Buttonstyle={{ marginBottom: 10, marginTop: 10 }}type="primary"onClick={async (e) => {e.stopPropagation();setNewId(itemInfo?.id);setAddNewLayoutOpen(true);}}hidden={filteredElementConfigList.length === 0}>新增配置</Button></Popover></div></>),closable: false,}))}/></Spin></Card><Modalwidth={680}title={addcopyType === 'add' ? '新增布局方案' : '复制布局方案'}open={newAddLayoutOpen}onOk={handleOkNewAddLayoutOpen}onCancel={handleCancelNewAddLayoutOpen}okText={addcopyType === 'add' ? '确认' : '确认复制'}footer={[<Button key="cancel" onClick={handleCancelNewAddLayoutOpen}>取消</Button>,<Buttonkey="submit"type="primary"onClick={handleOkNewAddLayoutOpen}>确认</Button>,]}><Formform={newLayoutForm}labelCol={{ span: 6 }}wrapperCol={{ span: 16 }}style={{ marginTop: '20px' }}initialValues={initialValueData}layout="horizontal"submitter={false}onFinish={handleOkNewAddLayoutOpen}params={{}}request={async () => {return {useMode: 'chapter',};}}><AddOrCopyformref={newLayoutForm}initialValueData={initialValueData}addcopyType={addcopyType}/></Form></Modal>{/* 新增布局配置 */}<Modalstyle={{ marginTop: 120 }}title="新增配置"open={addNewLayoutOpen}onOk={handleOkAddNewLayoutOpen}onCancel={() => {setAddNewLayoutOpen(false);setIsConfirmDisabled(true);addNewLayoutOpenForm?.resetFields();}}okButtonProps={{ disabled: isConfirmDisabled }} // 动态控制确认按钮的禁用状态><Formform={addNewLayoutOpenForm}labelCol={{ span: 6 }}wrapperCol={{ span: 16 }}style={{ marginTop: '40px' }}initialValues={{dynamicConfiguration: [], // 初始化为空数组,适配多选模式}}onValuesChange={(changedValues, allValues) => {const selectedValues = allValues.dynamicConfiguration;setIsConfirmDisabled(!selectedValues || selectedValues.length === 0,);}}><Selectname="dynamicConfiguration"label="配置项"options={filteredElementConfigList?.map((item: any) => ({label: item.tplField, // tplFieldName,value: item.tplField,}))}placeholder="请选择配置项"/></Form></Modal>{/* {导出/模板格式选择} */}<Modaltitle={currentExportAction === 'export' ? '导出格式选择' : '模版格式选择'}open={isModalOpen}onOk={handleOkExport}onCancel={handleCancelExport}><Formstyle={{ maxWidth: 600 }}form={formatOpenForm}initialValues={{ radioGroup: 'json' }}><Form.Item name="radioGroup" label="请选择格式"><Radio.Groupvalue={exportType}onChange={(e) => {const val = e.target.value;setExportType(val);formatOpenForm.setFieldsValue({ radioGroup: val });}}><Radio value="json">JSON</Radio><Radio value="csv">CSV</Radio></Radio.Group></Form.Item></Form></Modal></>);
});
export default LayoutPlanModuleTabs;
配置文件
// src/types/papaparse.d.tsdeclare module 'papaparse' {const Papa: {parse: (file: File | string,config?: {header?: boolean;skipEmptyLines?: boolean;complete?: (results: {data: Record<string, any>[];errors: any[];meta: any;}) => void;[key: string]: any;},) => void;// 可选导出其他方法unparse?: (data: any[], config?: object) => string;};export default Papa;}