fix: 优化预测页面
This commit is contained in:
parent
07776bee0a
commit
326689155f
139
src/pages/Forecast/index.less
Normal file
139
src/pages/Forecast/index.less
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
.container {
|
||||||
|
:global {
|
||||||
|
.ant-pro-page-container-children-content {
|
||||||
|
margin: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainCard {
|
||||||
|
min-height: 80vh;
|
||||||
|
background: linear-gradient(to bottom, #ffffff, #f0f2f5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysisTypeSelector {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 16px 0;
|
||||||
|
|
||||||
|
.analysisTag {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadCard {
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.ant-upload-drag {
|
||||||
|
border: 2px dashed #d9d9d9;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadIcon {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatContainer {
|
||||||
|
height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.messageList {
|
||||||
|
.userMessage {
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.messageCard {
|
||||||
|
background: #e6f7ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistantMessage {
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
.messageCard {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageCard {
|
||||||
|
max-width: 80%;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
background: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartContainer {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
border-radius: 8px;
|
||||||
|
resize: none;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sendButton {
|
||||||
|
align-self: flex-end;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.analysisTypeSelector {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageCard {
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputContainer {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.sendButton {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,10 +12,22 @@ import {
|
|||||||
message,
|
message,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Space,
|
Space,
|
||||||
|
Tag,
|
||||||
|
Divider,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { SendOutlined, RobotOutlined, UserOutlined, InboxOutlined } from '@ant-design/icons';
|
import {
|
||||||
|
SendOutlined,
|
||||||
|
RobotOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
InboxOutlined,
|
||||||
|
LineChartOutlined,
|
||||||
|
BarChartOutlined,
|
||||||
|
PieChartOutlined,
|
||||||
|
AreaChartOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import type { UploadFile } from 'antd/es/upload/interface';
|
import type { UploadFile } from 'antd/es/upload/interface';
|
||||||
import ReactECharts from 'echarts-for-react';
|
import ReactECharts from 'echarts-for-react';
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
const { Dragger } = Upload;
|
const { Dragger } = Upload;
|
||||||
@ -37,12 +49,59 @@ const AnalysisCenter: React.FC = () => {
|
|||||||
const messagesEndRef = useRef<null | HTMLDivElement>(null);
|
const messagesEndRef = useRef<null | HTMLDivElement>(null);
|
||||||
|
|
||||||
const analysisOptions = [
|
const analysisOptions = [
|
||||||
{ value: 'predictive', label: '预测性分析' },
|
{ value: 'predictive', label: '预测性分析', icon: <LineChartOutlined />, color: '#1890ff' },
|
||||||
{ value: 'descriptive', label: '描述性统计' },
|
{ value: 'descriptive', label: '描述性统计', icon: <BarChartOutlined />, color: '#52c41a' },
|
||||||
{ value: 'anomaly', label: '异常检测' },
|
{ value: 'anomaly', label: '异常检测', icon: <PieChartOutlined />, color: '#faad14' },
|
||||||
{ value: 'quality', label: '数据质量分析' },
|
{ value: 'quality', label: '数据质量分析', icon: <AreaChartOutlined />, color: '#722ed1' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const generateMockChart = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'predictive':
|
||||||
|
return {
|
||||||
|
title: { text: '销售趋势预测', left: 'center' },
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
legend: { data: ['历史数据', '预测数据'], bottom: 10 },
|
||||||
|
grid: { top: 50, right: 20, bottom: 60, left: 40 },
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
||||||
|
axisLabel: { interval: 0 }
|
||||||
|
},
|
||||||
|
yAxis: { type: 'value', name: '销售额' },
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '历史数据',
|
||||||
|
type: 'line',
|
||||||
|
data: [150, 230, 224, 218, 135, 147],
|
||||||
|
smooth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '预测数据',
|
||||||
|
type: 'line',
|
||||||
|
data: [null, null, null, 225, 238, 251],
|
||||||
|
smooth: true,
|
||||||
|
lineStyle: { type: 'dashed' },
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
case 'descriptive':
|
||||||
|
return {
|
||||||
|
title: { text: '数据分布情况', left: 'center' },
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
grid: { top: 50, right: 20, bottom: 60, left: 40 },
|
||||||
|
xAxis: { type: 'category', data: ['极小值', '下四分位', '中位数', '上四分位', '极大值'] },
|
||||||
|
yAxis: { type: 'value' },
|
||||||
|
series: [{
|
||||||
|
type: 'boxplot',
|
||||||
|
data: [[10, 25, 35, 50, 70]],
|
||||||
|
itemStyle: { color: '#52c41a' }
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return baseOption;
|
||||||
|
};
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
};
|
};
|
||||||
@ -92,72 +151,72 @@ const AnalysisCenter: React.FC = () => {
|
|||||||
return responses[type] || '分析完成';
|
return responses[type] || '分析完成';
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateMockChart = (type: string) => {
|
|
||||||
const baseOption = {
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
},
|
|
||||||
series: [{
|
|
||||||
data: [150, 230, 224, 218, 135, 147],
|
|
||||||
type: 'line',
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
return baseOption;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer
|
||||||
<Card>
|
className={styles.container}
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
title="智能预测分析"
|
||||||
<Select
|
subTitle="上传数据,获取专业的数据分析见解"
|
||||||
style={{ width: '100%' }}
|
|
||||||
value={analysisType}
|
|
||||||
onChange={setAnalysisType}
|
|
||||||
placeholder="请选择分析类型"
|
|
||||||
>
|
>
|
||||||
|
<Card bordered={false} className={styles.mainCard}>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||||
|
<div className={styles.analysisTypeSelector}>
|
||||||
{analysisOptions.map(option => (
|
{analysisOptions.map(option => (
|
||||||
<Option key={option.value} value={option.value}>
|
<Tooltip key={option.value} title={option.label}>
|
||||||
|
<Tag
|
||||||
|
className={styles.analysisTag}
|
||||||
|
color={analysisType === option.value ? option.color : 'default'}
|
||||||
|
icon={option.icon}
|
||||||
|
onClick={() => setAnalysisType(option.value)}
|
||||||
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
</Option>
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</div>
|
||||||
|
|
||||||
|
<Card className={styles.uploadCard}>
|
||||||
<Dragger
|
<Dragger
|
||||||
fileList={fileList}
|
fileList={fileList}
|
||||||
onChange={({ fileList }) => setFileList(fileList)}
|
onChange={({ fileList }) => setFileList(fileList)}
|
||||||
beforeUpload={(file) => {
|
beforeUpload={(file) => {
|
||||||
|
const isExcelOrCsv = /\.(xlsx|xls|csv)$/.test(file.name.toLowerCase());
|
||||||
|
if (!isExcelOrCsv) {
|
||||||
|
message.error('只支持 Excel 或 CSV 文件!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
setFileList([file]);
|
setFileList([file]);
|
||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="ant-upload-drag-icon">
|
<p className="ant-upload-drag-icon">
|
||||||
<InboxOutlined />
|
<InboxOutlined className={styles.uploadIcon} />
|
||||||
</p>
|
</p>
|
||||||
<p className="ant-upload-text">点击或拖拽文件上传</p>
|
<p className="ant-upload-text">点击或拖拽文件上传</p>
|
||||||
<p className="ant-upload-hint">支持 CSV、Excel 等数据文件格式</p>
|
<p className="ant-upload-hint">支持 Excel (.xlsx, .xls) 或 CSV 文件格式</p>
|
||||||
</Dragger>
|
</Dragger>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div style={{ height: '400px', overflowY: 'auto', marginBottom: '16px' }}>
|
<div className={styles.chatContainer}>
|
||||||
<List
|
<List
|
||||||
|
className={styles.messageList}
|
||||||
itemLayout="horizontal"
|
itemLayout="horizontal"
|
||||||
dataSource={messages}
|
dataSource={messages}
|
||||||
renderItem={item => (
|
renderItem={item => (
|
||||||
<List.Item style={{ justifyContent: item.type === 'user' ? 'flex-end' : 'flex-start' }}>
|
<List.Item className={item.type === 'user' ? styles.userMessage : styles.assistantMessage}>
|
||||||
<Card style={{ maxWidth: '80%' }}>
|
<Card className={styles.messageCard}>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
avatar={
|
avatar={
|
||||||
<Avatar icon={item.type === 'user' ? <UserOutlined /> : <RobotOutlined />} />
|
<Avatar
|
||||||
|
icon={item.type === 'user' ? <UserOutlined /> : <RobotOutlined />}
|
||||||
|
className={styles.avatar}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
title={item.type === 'user' ? '你' : 'AI 助手'}
|
title={item.type === 'user' ? '你' : 'AI 助手'}
|
||||||
description={item.content}
|
description={item.content}
|
||||||
/>
|
/>
|
||||||
{item.charts && (
|
{item.charts && (
|
||||||
<div style={{ marginTop: '16px' }}>
|
<div className={styles.chartContainer}>
|
||||||
<ReactECharts option={item.charts} style={{ height: '300px' }} />
|
<ReactECharts option={item.charts} style={{ height: 300 }} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
@ -167,12 +226,13 @@ const AnalysisCenter: React.FC = () => {
|
|||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
<div className={styles.inputContainer}>
|
||||||
<TextArea
|
<TextArea
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
placeholder="请输入你的分析需求..."
|
placeholder="请描述您的分析需求..."
|
||||||
autoSize={{ minRows: 2, maxRows: 6 }}
|
autoSize={{ minRows: 2, maxRows: 6 }}
|
||||||
|
className={styles.input}
|
||||||
onPressEnter={(e) => {
|
onPressEnter={(e) => {
|
||||||
if (!e.shiftKey) {
|
if (!e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -185,9 +245,9 @@ const AnalysisCenter: React.FC = () => {
|
|||||||
icon={<SendOutlined />}
|
icon={<SendOutlined />}
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
style={{ alignSelf: 'flex-end' }}
|
className={styles.sendButton}
|
||||||
>
|
>
|
||||||
发送
|
分析
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
|
93
src/pages/Report/index.less
Normal file
93
src/pages/Report/index.less
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
.container {
|
||||||
|
min-height: 600px;
|
||||||
|
|
||||||
|
.steps {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadStep {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewCard {
|
||||||
|
margin-top: 24px;
|
||||||
|
|
||||||
|
.previewTable {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #fafafa;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.goalStep {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
.goalInput {
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generateButton {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressContainer {
|
||||||
|
margin-top: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reportStep {
|
||||||
|
.reportPreview {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.chartCard {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.ant-upload-drag {
|
||||||
|
border: 2px dashed #d9d9d9;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-steps-item-icon {
|
||||||
|
background: #fff !important;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,14 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { PageContainer } from '@ant-design/pro-components';
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
import { InboxOutlined } from '@ant-design/icons';
|
import { InboxOutlined, FileExcelOutlined, BarChartOutlined, FileWordOutlined } from '@ant-design/icons';
|
||||||
import { Upload, Card, Button, Steps, message, Input, Spin,Result } from 'antd';
|
import { Upload, Card, Button, Steps, message, Input, Spin, Result, Space, Progress, Alert, Typography } from 'antd';
|
||||||
import type { UploadFile } from 'antd/es/upload/interface';
|
import type { UploadFile } from 'antd/es/upload/interface';
|
||||||
import ReactECharts from 'echarts-for-react';
|
import ReactECharts from 'echarts-for-react';
|
||||||
import { DownloadOutlined } from '@ant-design/icons';
|
import styles from './index.less';
|
||||||
|
|
||||||
const { Dragger } = Upload;
|
const { Dragger } = Upload;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
const { Title, Paragraph } = Typography;
|
||||||
|
|
||||||
const ReportPage: React.FC = () => {
|
const ReportPage: React.FC = () => {
|
||||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||||
@ -16,45 +17,143 @@ const ReportPage: React.FC = () => {
|
|||||||
const [goal, setGoal] = useState('');
|
const [goal, setGoal] = useState('');
|
||||||
const [reportData, setReportData] = useState<any>(null);
|
const [reportData, setReportData] = useState<any>(null);
|
||||||
const [wordUrl, setWordUrl] = useState<string>('');
|
const [wordUrl, setWordUrl] = useState<string>('');
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
const [previewData, setPreviewData] = useState<any>(null);
|
||||||
|
|
||||||
const handleUpload = async (file: File) => {
|
const handleUpload = async (file: File) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// 这里应该调用后端API上传文件
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
// const response = await uploadFile(formData);
|
|
||||||
|
// 模拟上传进度
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setProgress(prev => {
|
||||||
|
if (prev >= 99) {
|
||||||
|
clearInterval(timer);
|
||||||
|
return 99;
|
||||||
|
}
|
||||||
|
return prev + 10;
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
// 模拟预览数据
|
||||||
|
setTimeout(() => {
|
||||||
|
setPreviewData({
|
||||||
|
columns: ['日期', '销售额', '利润'],
|
||||||
|
data: [
|
||||||
|
['2023-01', 1000, 200],
|
||||||
|
['2023-02', 1500, 300],
|
||||||
|
['2023-03', 1200, 250],
|
||||||
|
]
|
||||||
|
});
|
||||||
|
setProgress(100);
|
||||||
message.success('文件上传成功');
|
message.success('文件上传成功');
|
||||||
setCurrentStep(1);
|
setCurrentStep(1);
|
||||||
|
setLoading(false);
|
||||||
|
clearInterval(timer);
|
||||||
|
}, 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('文件上传失败');
|
message.error('文件上传失败');
|
||||||
}
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateReport = async () => {
|
const generateReport = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// 这里应该调用后端API生成报告
|
let progress = 0;
|
||||||
// const response = await generateWordReport({ fileId: fileList[0].uid, goal });
|
const timer = setInterval(() => {
|
||||||
// setWordUrl(response.data.wordUrl);
|
progress += 20;
|
||||||
|
if (progress <= 100) {
|
||||||
|
setProgress(progress);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
// 模拟数据
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
clearInterval(timer);
|
||||||
|
setProgress(100);
|
||||||
setWordUrl('https://example.com/report.docx');
|
setWordUrl('https://example.com/report.docx');
|
||||||
|
setReportData({
|
||||||
|
title: '数据分析报告',
|
||||||
|
summary: '根据您提供的数据,我们生成了详细的分析报告...',
|
||||||
|
charts: [
|
||||||
|
{
|
||||||
|
title: '销售趋势',
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
title: {
|
||||||
|
text: '销售趋势分析'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis'
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: ['1月', '2月', '3月']
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value'
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
name: '销售额',
|
||||||
|
type: 'line',
|
||||||
|
data: [1000, 1500, 1200],
|
||||||
|
smooth: true
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
message.success('报告生成成功');
|
message.success('报告生成成功');
|
||||||
setCurrentStep(2);
|
setCurrentStep(2);
|
||||||
}, 1500);
|
setLoading(false);
|
||||||
|
}, 5000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('报告生成失败');
|
message.error('报告生成失败');
|
||||||
}
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 steps = [
|
const steps = [
|
||||||
{
|
{
|
||||||
title: '上传文件',
|
title: '上传文件',
|
||||||
|
icon: <FileExcelOutlined />,
|
||||||
content: (
|
content: (
|
||||||
|
<div className={styles.uploadStep}>
|
||||||
|
<Alert
|
||||||
|
message="支持的文件格式"
|
||||||
|
description="Excel文件 (.xlsx, .xls) 或 CSV文件 (.csv)"
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 24 }}
|
||||||
|
/>
|
||||||
<Dragger
|
<Dragger
|
||||||
fileList={fileList}
|
fileList={fileList}
|
||||||
beforeUpload={(file) => {
|
beforeUpload={(file) => {
|
||||||
@ -73,6 +172,7 @@ const ReportPage: React.FC = () => {
|
|||||||
onRemove={() => {
|
onRemove={() => {
|
||||||
setFileList([]);
|
setFileList([]);
|
||||||
setCurrentStep(0);
|
setCurrentStep(0);
|
||||||
|
setPreviewData(null);
|
||||||
return true;
|
return true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -82,42 +182,71 @@ const ReportPage: React.FC = () => {
|
|||||||
<p className="ant-upload-text">点击或拖拽文件到此区域上传</p>
|
<p className="ant-upload-text">点击或拖拽文件到此区域上传</p>
|
||||||
<p className="ant-upload-hint">支持 Excel 和 CSV 文件格式</p>
|
<p className="ant-upload-hint">支持 Excel 和 CSV 文件格式</p>
|
||||||
</Dragger>
|
</Dragger>
|
||||||
|
{loading && (
|
||||||
|
<Progress percent={progress} status="active" style={{ marginTop: 24 }} />
|
||||||
|
)}
|
||||||
|
{renderPreview()}
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '设置目标',
|
title: '设置目标',
|
||||||
|
icon: <BarChartOutlined />,
|
||||||
content: (
|
content: (
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div className={styles.goalStep}>
|
||||||
|
<Title level={4}>分析目标设置</Title>
|
||||||
|
<Paragraph type="secondary">
|
||||||
|
请详细描述您的分析需求,例如:
|
||||||
|
<ul>
|
||||||
|
<li>分析销售趋势和影响因素</li>
|
||||||
|
<li>识别客户购买行为模式</li>
|
||||||
|
<li>预测未来销售情况</li>
|
||||||
|
</ul>
|
||||||
|
</Paragraph>
|
||||||
<TextArea
|
<TextArea
|
||||||
placeholder="请输入你想要分析的目标,例如:分析销售趋势、客户分布等"
|
placeholder="请输入你想要分析的目标,例如:分析销售趋势、客户分布等"
|
||||||
rows={4}
|
rows={4}
|
||||||
value={goal}
|
value={goal}
|
||||||
onChange={(e) => setGoal(e.target.value)}
|
onChange={(e) => setGoal(e.target.value)}
|
||||||
style={{ marginBottom: 16 }}
|
className={styles.goalInput}
|
||||||
/>
|
/>
|
||||||
<Button type="primary" onClick={generateReport} disabled={!goal}>
|
<Button
|
||||||
生成报告
|
type="primary"
|
||||||
|
onClick={generateReport}
|
||||||
|
disabled={!goal}
|
||||||
|
size="large"
|
||||||
|
className={styles.generateButton}
|
||||||
|
>
|
||||||
|
开始生成报告
|
||||||
</Button>
|
</Button>
|
||||||
|
{loading && (
|
||||||
|
<div className={styles.progressContainer}>
|
||||||
|
<Progress percent={progress} status="active" />
|
||||||
|
<Paragraph type="secondary">正在生成分析报告,请稍候...</Paragraph>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '查看报告',
|
title: '查看报告',
|
||||||
|
icon: <FileWordOutlined />,
|
||||||
content: wordUrl && (
|
content: wordUrl && (
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div className={styles.reportStep}>
|
||||||
<Card>
|
<Card>
|
||||||
<Result
|
<Result
|
||||||
status="success"
|
status="success"
|
||||||
title="报告生成成功"
|
title="报告生成成功"
|
||||||
subTitle="您可以下载生成的Word报告文档"
|
subTitle="您可以查看报告预览或下载完整报告"
|
||||||
extra={[
|
extra={[
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<DownloadOutlined />}
|
icon={<FileWordOutlined />}
|
||||||
onClick={() => window.open(wordUrl)}
|
onClick={() => window.open(wordUrl)}
|
||||||
key="download"
|
key="download"
|
||||||
|
size="large"
|
||||||
>
|
>
|
||||||
下载报告
|
下载完整报告
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -125,13 +254,27 @@ const ReportPage: React.FC = () => {
|
|||||||
setFileList([]);
|
setFileList([]);
|
||||||
setGoal('');
|
setGoal('');
|
||||||
setWordUrl('');
|
setWordUrl('');
|
||||||
|
setPreviewData(null);
|
||||||
|
setReportData(null);
|
||||||
}}
|
}}
|
||||||
key="again"
|
key="again"
|
||||||
|
size="large"
|
||||||
>
|
>
|
||||||
重新生成
|
重新生成
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
{reportData && (
|
||||||
|
<div className={styles.reportPreview}>
|
||||||
|
<Title level={4}>{reportData.title}</Title>
|
||||||
|
<Paragraph>{reportData.summary}</Paragraph>
|
||||||
|
{reportData.charts.map((chart: any, index: number) => (
|
||||||
|
<Card key={index} title={chart.title} className={styles.chartCard}>
|
||||||
|
<ReactECharts option={chart.data} style={{ height: 300 }} />
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@ -139,10 +282,19 @@ const ReportPage: React.FC = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer
|
||||||
<Card>
|
title="智能报告生成"
|
||||||
<Steps current={currentStep} items={steps} style={{ marginBottom: 24 }} />
|
subTitle="上传数据文件,快速生成专业分析报告"
|
||||||
|
>
|
||||||
|
<Card className={styles.container}>
|
||||||
|
<Steps
|
||||||
|
current={currentStep}
|
||||||
|
items={steps}
|
||||||
|
className={styles.steps}
|
||||||
|
/>
|
||||||
|
<div className={styles.content}>
|
||||||
<Spin spinning={loading}>{steps[currentStep].content}</Spin>
|
<Spin spinning={loading}>{steps[currentStep].content}</Spin>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user