// src/components/UnifiedCsvMerger.jsx
import React, { useState, useRef } from "react";
import Papa from "papaparse";
// MUI Imports
import { Box, Typography, Button, TextField, Paper, Table, TableHead, TableRow, TableCell, TableBody, TableContainer, Radio, RadioGroup, FormControlLabel, FormControl, FormLabel, List, ListItem, ListItemText, IconButton, Divider, Alert, CircularProgress, Grid } from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
// 工具函数(保持不变)
import { processDataList, generateTotalStats, filterPlayIdWithSuccessPriority } from "./utils";
const UnifiedCsvMerger = () => {
const fileInputRef = useRef(null);
const [selectedFiles, setSelectedFiles] = useState([]);
const [mergedPreview, setMergedPreview] = useState([]);
// 缓存处理后的数据
const [processedData, setProcessedData] = useState([]);
const [statsSummary, setStatsSummary] = useState([]);
const [statsSummaryUserImpact, setStatsSummaryUserImpact] = useState([]);
const [totalRows, setTotalRows] = useState(null);
const [isParsing, setIsParsing] = useState(false);
const [error, setError] = useState(null);
// 表单状态
const [formValues, setFormValues] = useState({
startTime: "",
endTime: "",
excludeErrorCodes: "-71114\n-71101\n-52208\n-1007\n-81607\n-80301\n-52405\n-1033\n-1029\n2\n-52407",
targetEid: "Liveview.Video.StartPlay",
});
const targetEidOptions = [
{ value: "Liveview.Video.StartPlay", label: "直播" },
{ value: "Playback.Video.StartPlay", label: "回放" },
];
// 单选选项
const options = [
{ value: "res", label: "明细数据 (res)" },
{ value: "err_summary", label: "错误统计汇总 (err_summary)" },
{ value: "err_summary_user_impact", label: "错误统计汇总 (用户感知维度)" },
];
const [selectedOption, setSelectedOption] = useState("res");
// 添加文件
const handleAddFiles = () => {
const files = fileInputRef.current?.files;
if (!files || files.length === 0) return;
const newFiles = Array.from(files).filter((file) => /\.(csv)$/i.test(file.name));
if (newFiles.length === 0) {
alert("请选择有效的 CSV 文件");
return;
}
setSelectedFiles((prev) => [...prev, ...newFiles]);
if (fileInputRef.current) fileInputRef.current.value = "";
};
// 删除文件
const handleRemoveFile = (index) => {
setSelectedFiles((prev) => prev.filter((_, i) => i !== index));
};
// 处理表单输入
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormValues((prev) => ({
...prev,
[name]: value,
}));
};
// 解析所有文件
const handleParseAll = () => {
if (selectedFiles.length === 0) {
setError("请先添加至少一个 CSV 文件");
return;
}
setIsParsing(true);
setError(null);
setMergedPreview([]);
setTotalRows(0);
const promises = selectedFiles.map(
(file) =>
new Promise((resolve) => {
const rows = [];
Papa.parse(file, {
worker: false,
header: true,
skipEmptyLines: true,
transform: (value) => value.trim(),
step: (result) => {
rows.push(result.data);
},
complete: () => resolve(rows),
error: () => resolve([]),
});
})
);
Promise.all(promises)
.then((allRows) => {
const mergedData = allRows.flat();
const config = {
start_time: formValues.startTime.trim() || undefined,
end_time: formValues.endTime.trim() || undefined,
exclude_error_codes: formValues.excludeErrorCodes
.split(/[\n,;,\s]+/)
.map((code) => code.trim())
.filter((code) => code !== ""),
target_eid: formValues.targetEid.trim() || "Liveview.Video.StartPlay",
};
const res = processDataList(mergedData, config);
const err_summary = generateTotalStats(res);
const err_summary_user_impact = generateTotalStats(filterPlayIdWithSuccessPriority(res));
setProcessedData(res);
setStatsSummary(err_summary);
setStatsSummaryUserImpact(err_summary_user_impact);
// 默认显示前100条明细
setMergedPreview(res.slice(0, 100));
setTotalRows(mergedData.length);
})
.catch((err) => {
const message = err instanceof Error ? err.message : "未知错误";
setError(`解析过程中发生错误: ${message}`);
})
.finally(() => {
setIsParsing(false);
});
};
// 切换预览内容
const handleRadioChange = (e) => {
const value = e.target.value;
setSelectedOption(value);
if (value === "res") {
setMergedPreview(processedData.slice(0, 100));
} else if (value === "err_summary") {
setMergedPreview(statsSummary);
} else if (value === "err_summary_user_impact") {
setMergedPreview(statsSummaryUserImpact);
}
};
return (
<Box sx={{ padding: 3 }}>
<Paper elevation={3} sx={{ padding: 3, borderRadius: 2 }}>
<Typography variant="h5" gutterBottom>
📁 解析多个 CSV 文件(统一格式)
</Typography>
{/* 添加文件 */}
<Box mt={2}>
<input ref={fileInputRef} type="file" accept=".csv" multiple onChange={handleAddFiles} style={{ display: "none" }} id="upload-csv" />
<label htmlFor="upload-csv">
<Button component="span" variant="contained" color="primary" disabled={isParsing} startIcon={<span>📁</span>}>
添加文件
</Button>
</label>
</Box>
{/* 已选文件列表 */}
{selectedFiles.length > 0 && (
<Box mt={3}>
<Typography variant="h6">📎 已选文件 ({selectedFiles.length}):</Typography>
<List dense>
{selectedFiles.map((file, index) => (
<React.Fragment key={index}>
<ListItem
secondaryAction={
<IconButton edge="end" onClick={() => handleRemoveFile(index)} disabled={isParsing}>
<DeleteIcon />
</IconButton>
}
>
<ListItemText primary={file.name} secondary={`${(file.size / 1024).toFixed(1)} KB`} />
</ListItem>
<Divider />
</React.Fragment>
))}
</List>
</Box>
)}
{/* 过滤条件表单 */}
<Box mt={3}>
<Typography variant="h6" gutterBottom>
⚙️ 数据处理配置
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<TextField label="开始时间(可选)" type="datetime-local" name="startTime" value={formValues.startTime} onChange={handleInputChange} fullWidth InputLabelProps={{ shrink: true }} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField label="结束时间(可选)" type="datetime-local" name="endTime" value={formValues.endTime} onChange={handleInputChange} fullWidth InputLabelProps={{ shrink: true }} />
</Grid>
<Grid item xs={12}>
<TextField label="排除的错误码" name="excludeErrorCodes" value={formValues.excludeErrorCodes} onChange={handleInputChange} multiline rows={6} placeholder="每行或用逗号/空格分隔,例如:-71114 -1007" fullWidth />
</Grid>
<Grid item xs={12}>
<FormControl component="fieldset">
<FormLabel component="legend">目标事件类型 (target_eid)</FormLabel>
<RadioGroup row name="targetEid" value={formValues.targetEid} onChange={handleInputChange}>
{targetEidOptions.map((option) => (
<FormControlLabel key={option.value} value={option.value} control={<Radio size="small" />} label={option.label} />
))}
</RadioGroup>
</FormControl>
</Grid>
</Grid>
</Box>
{/* 解析按钮 */}
<Box mt={3}>
<Button variant="contained" color="secondary" onClick={handleParseAll} disabled={isParsing || selectedFiles.length === 0} startIcon={isParsing ? <CircularProgress size={20} /> : "🚀"} size="large">
{isParsing ? "解析中..." : "解析并合并所有文件"}
</Button>
</Box>
{/* 错误提示 */}
{error && (
<Box mt={2}>
<Alert severity="error">{error}</Alert>
</Box>
)}
{/* 合并结果展示 */}
{!isParsing && totalRows !== null && (
<Box mt={4}>
<Typography variant="h6">请选择预览内容:</Typography>
<FormControl component="fieldset" sx={{ mb: 2 }}>
<RadioGroup value={selectedOption} onChange={handleRadioChange}>
{options.map((option) => (
<FormControlLabel key={option.value} value={option.value} control={<Radio />} label={option.label} />
))}
</RadioGroup>
</FormControl>
<Typography variant="body1" color="textSecondary" mb={1}>
当前显示: <strong>{options.find((o) => o.value === selectedOption)?.label}</strong>
</Typography>
<Typography variant="body2" color="textSecondary" mb={2}>
<strong>原始共 {totalRows.toLocaleString()} 行数据</strong>,{selectedOption === "res" ? " 展示过滤后明细数据前100条" : " 展示错误统计汇总"}
</Typography>
<TableContainer component={Paper} variant="outlined" sx={{ maxHeight: 500 }}>
<Table stickyHeader size="small">
<TableHead>
<TableRow>
{Object.keys(mergedPreview[0] || {}).map((header) => (
<TableCell key={header} sx={{ fontWeight: "bold" }}>
{header}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{mergedPreview.length > 0 ? (
mergedPreview.map((row, idx) => (
<TableRow key={idx}>
{Object.values(row).map((cell, j) => (
<TableCell key={j}>{String(cell)}</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={Object.keys(mergedPreview[0] || {}).length || 1} align="center">
暂无数据
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</Box>
)}
</Paper>
</Box>
);
};
export default UnifiedCsvMerger;
新增四个表格pullStreamStats err_daily_weekly_monthly err_device_cov err_device_cov_daily_weekly_monthly,还是使用之前的单选切换显示,内容都是generateTotalStats(res);
最新发布