核心技术实现拆解
目录
Excel 文件上传功能实现
功能概述
Excel 文件上传是系统三大核心功能的基础数据输入方式之一。用户可以通过浏览器界面选择并上传 .xlsx 或 .xls 格式的 Excel 文件,系统会自动解析文件内容,提取所需的数据工作表。
技术实现流程
1. 前端文件选择组件
系统使用 FileUpload 组件(位于 src/components/FileUpload.tsx)来处理文件选择。该组件基于 Semi Design 的 Upload 组件,但通过 customRequest 函数拦截了默认的网络上传行为,改为直接在前端处理文件。
核心代码逻辑:
// 自定义上传请求,不实际发送网络请求
const customRequest = ({ file, fileInstance, onProgress, onSuccess, onError }) => {
try {
// 获取真实的 File 对象
const realFile = fileInstance || (file as any)?.originFile || (file as any);
// 直接调用父组件传入的回调函数,传递文件对象
onFileSelect(realFile as File);
// 模拟上传进度(用于 UI 反馈)
let loaded = 0;
const total = 100;
timerRef.current = window.setInterval(() => {
loaded = Math.min(loaded + 20, total);
onProgress && onProgress({ total, loaded });
if (loaded === total) {
clearInterval(timerRef.current);
onSuccess && onSuccess({});
}
}, 200);
} catch (e) {
onError && onError({ status: 500 }, e as Event);
}
};
设计要点:
- 无网络上传:文件不会实际发送到服务器,所有处理都在浏览器端完成
- 进度模拟:为了提供良好的用户体验,模拟了上传进度条
- 直接传递:文件对象直接传递给父组件,由父组件决定如何处理
2. 文件读取与解析
当文件选择完成后,父组件(如 GeoAnalysisPage、AudienceSignalPage)会调用相应的解析函数。系统使用 XLSX 库(SheetJS)来解析 Excel 文件。
解析流程:
- 文件读取:使用浏览器的
FileReaderAPI 将文件读取为ArrayBuffer - 工作簿解析:使用
XLSX.read()将ArrayBuffer解析为工作簿对象 - 工作表提取:根据功能需求,提取特定的工作表(如
sublocations、input等) - 数据转换:使用
XLSX.utils.sheet_to_json()将工作表数据转换为 JSON 格式
核心代码示例(以市场机会分析为例):
export const parseExcelFile = (file: File, onProgress: LogCallback): Promise<RawData> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event: ProgressEvent<FileReader>) => {
if (!event.target?.result) {
return reject(new Error("文件读取失败。"));
}
try {
// 1. 将文件内容转换为 Uint8Array
const data = new Uint8Array(event.target.result as ArrayBuffer);
// 2. 使用 XLSX 库解析工作簿
const workbook = XLSX.read(data, { type: 'array', cellDates: true });
// 3. 提取行业名称(从 input 工作表的 B4 单元格)
const inputSheet = workbook.Sheets[workbook.SheetNames[0]];
const industryName = inputSheet['B4']?.v || '';
// 4. 查找目标工作表(如 'sublocations')
const targetSheetName = workbook.SheetNames.find(name =>
name.toLowerCase().includes('sublocation')
) || workbook.SheetNames[1];
// 5. 将工作表数据转换为 JSON
const dataSheet = workbook.Sheets[targetSheetName];
const jsonData: any[] = XLSX.utils.sheet_to_json(dataSheet);
// 6. 数据规范化处理
const locationSheet = jsonData.map(row => ({
// 清理和转换数据字段
category: normalizeCategory(row['品类名称'] || row['Category']),
country: row['国家名称'] || row['Country'],
cpc: parseFloat(String(row['CPC'] || 0)),
// ... 其他字段
}));
resolve({
industryName,
popPeriod: extractPopPeriod(jsonData),
locationSheet,
});
} catch (error) {
reject(new Error(`解析 Excel 文件失败: ${error.message}`));
}
};
reader.onerror = () => {
reject(new Error('文件读取失败'));
};
// 开始读取文件
reader.readAsArrayBuffer(file);
});
};
错误处理:
- 文件格式验证:检查文件扩展名和 MIME 类型
- 工作表存在性检查:确保必需的工作表存在
- 数据完整性验证:检查必需字段是否存在
- 异常捕获:使用 try-catch 捕获所有可能的解析错误