aigc/src/pages/Report/index.tsx
2025-04-20 22:24:32 +08:00

395 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react';
import { PageContainer } from '@ant-design/pro-components';
import { InboxOutlined, FileExcelOutlined, BarChartOutlined, FileWordOutlined } from '@ant-design/icons';
import { Upload, Card, Button, Steps, message, Input, Spin, Result, Progress, Alert, Typography } from 'antd';
import type { UploadFile } from 'antd/es/upload/interface';
import { marked } from 'marked';
import styles from './index.less';
import { Document, Packer, Paragraph as DocxParagraph, TextRun } from 'docx';
import { saveAs } from 'file-saver';
import * as XLSX from 'xlsx';
import useAIRequest from '@/hooks/useAIRequest';
const { Dragger } = Upload;
const { TextArea } = Input;
const { Title, Paragraph } = Typography;
interface AnalysisResponse {
id: string;
choices: {
message: {
content: string;
};
}[];
}
interface ReportSection {
title: string;
content: string;
}
interface ReportData {
title: string;
summary: string;
sections: ReportSection[];
charts: any[];
markdown: string;
}
const ReportPage: React.FC = () => {
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [currentStep, setCurrentStep] = useState(0);
const [loading, setLoading] = useState(false);
const [goal, setGoal] = useState('');
const [reportData, setReportData] = useState<any>(null);
const [wordUrl, setWordUrl] = useState<string>('');
const [progress, setProgress] = useState(0);
const [previewData, setPreviewData] = useState<any>(null);
const handleUpload = async (file: File) => {
setLoading(true);
try {
const isImage = file.type.startsWith('image/');
const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
file.type === 'application/vnd.ms-excel';
const isCsv = file.type === 'text/csv';
if (isImage) {
// 处理图片文件
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = async () => {
const base64Image = reader.result as string;
await analyzeData({
type: 'image_url',
image_url: {
url: base64Image
}
});
};
} else if (isExcel || isCsv) {
// 处理 Excel/CSV 文件
const reader = new FileReader();
reader.onload = async (e) => {
const data = e.target?.result;
const workbook = XLSX.read(data, { type: 'array' });
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
const jsonData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });
// 将数据转换为字符串
const textContent = jsonData.map(row => row.join('\t')).join('\n');
await analyzeData({
type: 'text',
text: textContent
});
};
reader.readAsArrayBuffer(file);
} else {
message.error('不支持的文件格式');
setLoading(false);
}
} catch (error) {
message.error('文件处理失败');
console.error('文件处理失败:', error);
setLoading(false);
}
};
const { loading: aiLoading, sendRequest } = useAIRequest();
const analyzeData = async (content: any) => {
setLoading(true);
try {
const result = await sendRequest([
{
type: 'text',
text: '请分析这些数据的趋势和关键信息,并给出专业的分析见解。'
},
content
]);
setPreviewData({
columns: ['分析结果'],
data: [[result]]
});
message.success('数据分析成功');
setCurrentStep(1);
} catch (error) {
message.error('数据分析失败');
console.error('API调用失败:', error);
} finally {
setLoading(false);
}
};
const generateReport = async () => {
try {
const result = await sendRequest([
{
type: 'text',
text: `请基于以下分析目标和数据生成一份详细的markdown格式分析报告包含标题、概述、详细分析等章节${goal}\n${previewData?.data[0][0]}`
}
], {
maxTokens: 2000
});
// 解析 markdown 内容
const titleMatch = result.match(/^#\s+(.+)$/m);
const title = titleMatch ? titleMatch[1] : '数据分析报告';
setReportData({
title,
summary: '',
sections: [],
charts: [],
markdown: result
});
setWordUrl('https://example.com/report.docx');
message.success('报告生成成功');
setCurrentStep(2);
} catch (error) {
message.error('报告生成失败');
}
};
const generateWordDocument = async (markdown: string) => {
const doc = new Document({
sections: [
{
properties: {},
children: markdown.split('\n').map(line => {
if (line.startsWith('# ')) {
return new DocxParagraph({
text: line.replace('# ', ''),
heading: 'Heading1'
});
}
if (line.startsWith('## ')) {
return new DocxParagraph({
text: line.replace('## ', ''),
heading: 'Heading2'
});
}
return new DocxParagraph({
children: [
new TextRun({
text: line,
size: 24
})
]
});
})
}
]
});
const blob = await Packer.toBlob(doc);
saveAs(blob, '数据分析报告.docx');
};
const handleDownload = async () => {
if (reportData?.markdown) {
try {
await generateWordDocument(reportData.markdown);
message.success('报告下载成功');
} catch (error) {
message.error('报告下载失败');
console.error('下载失败:', error);
}
}
};
const renderPreview = () => {
if (!previewData) return null;
return (
<Card className={styles.previewCard} title="数据预览">
<table className={styles.previewTable}>
<thead>
<tr>
{previewData.columns.map((col: string) => (
<th key={col}>{col}</th>
))}
</tr>
</thead>
<tbody>
{previewData.data.map((row: any[], index: number) => (
<tr key={index}>
{row.map((cell, cellIndex) => (
<td key={cellIndex}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</Card>
);
};
const renderReport = () => {
if (!reportData?.markdown) return null;
return (
<div
className={styles.reportContent}
dangerouslySetInnerHTML={{ __html: marked(reportData.markdown) }}
/>
);
};
const steps = [
{
title: '上传文件',
icon: <FileExcelOutlined />,
content: (
<div className={styles.uploadStep}>
<Alert
message="支持的文件格式"
description="Excel文件 (.xlsx, .xls) 或 CSV文件 (.csv)"
type="info"
showIcon
style={{ marginBottom: 24 }}
/>
<Dragger
fileList={fileList}
beforeUpload={(file) => {
// const isExcelOrCsv =
// file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
// file.type === 'application/vnd.ms-excel' ||
// file.type === 'text/csv';
// if (!isExcelOrCsv) {
// message.error('只支持上传 Excel 或 CSV 文件!');
// return false;
// }
setFileList([file]);
handleUpload(file);
return false;
}}
onRemove={() => {
setFileList([]);
setCurrentStep(0);
setPreviewData(null);
return true;
}}
>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text"></p>
<p className="ant-upload-hint"> Excel CSV </p>
</Dragger>
{loading && (
<Progress percent={progress} status="active" style={{ marginTop: 24 }} />
)}
{renderPreview()}
</div>
),
},
{
title: '设置目标',
icon: <BarChartOutlined />,
content: (
<div className={styles.goalStep}>
<Title level={4}></Title>
<Paragraph type="secondary">
<ul>
<li></li>
<li></li>
<li></li>
</ul>
</Paragraph>
<TextArea
placeholder="请输入你想要分析的目标,例如:分析销售趋势、客户分布等"
rows={4}
value={goal}
onChange={(e) => setGoal(e.target.value)}
className={styles.goalInput}
/>
<Button
type="primary"
onClick={generateReport}
disabled={!goal}
size="large"
className={styles.generateButton}
>
</Button>
{loading && (
<div className={styles.progressContainer}>
<Progress percent={progress} status="active" />
<Paragraph type="secondary">...</Paragraph>
</div>
)}
</div>
),
},
{
title: '查看报告',
icon: <FileWordOutlined />,
content: wordUrl && (
<div className={styles.reportStep}>
<Card>
<Result
status="success"
title="报告生成成功"
subTitle="您可以查看报告预览或下载完整报告"
extra={[
<Button
type="primary"
icon={<FileWordOutlined />}
onClick={handleDownload}
key="download"
size="large"
>
</Button>,
<Button
onClick={() => {
setCurrentStep(0);
setFileList([]);
setGoal('');
setWordUrl('');
setPreviewData(null);
setReportData(null);
}}
key="again"
size="large"
>
</Button>,
]}
/>
{renderReport()}
</Card>
</div>
),
},
];
return (
<PageContainer
title="智能图表分析"
subTitle="上传数据图表,快速获取专业分析见解"
>
<Card className={styles.container}>
<Steps
current={currentStep}
items={steps}
className={styles.steps}
/>
<div className={styles.content}>
<Spin spinning={loading}>{steps[currentStep].content}</Spin>
</div>
</Card>
</PageContainer>
);
};
export default ReportPage;