fix: 优化预测页面

This commit is contained in:
Shu Guang 2025-04-17 22:27:59 +08:00
parent 07776bee0a
commit 326689155f
4 changed files with 556 additions and 112 deletions

View 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%;
}
}
}

View File

@ -12,10 +12,22 @@ import {
message,
Tooltip,
Space,
Tag,
Divider,
} 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 ReactECharts from 'echarts-for-react';
import styles from './index.less';
const { Option } = Select;
const { Dragger } = Upload;
@ -37,12 +49,59 @@ const AnalysisCenter: React.FC = () => {
const messagesEndRef = useRef<null | HTMLDivElement>(null);
const analysisOptions = [
{ value: 'predictive', label: '预测性分析' },
{ value: 'descriptive', label: '描述性统计' },
{ value: 'anomaly', label: '异常检测' },
{ value: 'quality', label: '数据质量分析' },
{ value: 'predictive', label: '预测性分析', icon: <LineChartOutlined />, color: '#1890ff' },
{ value: 'descriptive', label: '描述性统计', icon: <BarChartOutlined />, color: '#52c41a' },
{ value: 'anomaly', label: '异常检测', icon: <PieChartOutlined />, color: '#faad14' },
{ 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 = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
@ -92,72 +151,72 @@ const AnalysisCenter: React.FC = () => {
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 (
<PageContainer>
<Card>
<Space direction="vertical" style={{ width: '100%' }}>
<Select
style={{ width: '100%' }}
value={analysisType}
onChange={setAnalysisType}
placeholder="请选择分析类型"
>
<PageContainer
className={styles.container}
title="智能预测分析"
subTitle="上传数据,获取专业的数据分析见解"
>
<Card bordered={false} className={styles.mainCard}>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<div className={styles.analysisTypeSelector}>
{analysisOptions.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
<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}
</Tag>
</Tooltip>
))}
</Select>
</div>
<Dragger
fileList={fileList}
onChange={({ fileList }) => setFileList(fileList)}
beforeUpload={(file) => {
setFileList([file]);
return false;
}}
>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text"></p>
<p className="ant-upload-hint"> CSVExcel </p>
</Dragger>
<Card className={styles.uploadCard}>
<Dragger
fileList={fileList}
onChange={({ fileList }) => setFileList(fileList)}
beforeUpload={(file) => {
const isExcelOrCsv = /\.(xlsx|xls|csv)$/.test(file.name.toLowerCase());
if (!isExcelOrCsv) {
message.error('只支持 Excel 或 CSV 文件!');
return false;
}
setFileList([file]);
return false;
}}
>
<p className="ant-upload-drag-icon">
<InboxOutlined className={styles.uploadIcon} />
</p>
<p className="ant-upload-text"></p>
<p className="ant-upload-hint"> Excel (.xlsx, .xls) CSV </p>
</Dragger>
</Card>
<div style={{ height: '400px', overflowY: 'auto', marginBottom: '16px' }}>
<div className={styles.chatContainer}>
<List
className={styles.messageList}
itemLayout="horizontal"
dataSource={messages}
renderItem={item => (
<List.Item style={{ justifyContent: item.type === 'user' ? 'flex-end' : 'flex-start' }}>
<Card style={{ maxWidth: '80%' }}>
<List.Item className={item.type === 'user' ? styles.userMessage : styles.assistantMessage}>
<Card className={styles.messageCard}>
<List.Item.Meta
avatar={
<Avatar icon={item.type === 'user' ? <UserOutlined /> : <RobotOutlined />} />
<Avatar
icon={item.type === 'user' ? <UserOutlined /> : <RobotOutlined />}
className={styles.avatar}
/>
}
title={item.type === 'user' ? '你' : 'AI 助手'}
description={item.content}
/>
{item.charts && (
<div style={{ marginTop: '16px' }}>
<ReactECharts option={item.charts} style={{ height: '300px' }} />
<div className={styles.chartContainer}>
<ReactECharts option={item.charts} style={{ height: 300 }} />
</div>
)}
</Card>
@ -167,12 +226,13 @@ const AnalysisCenter: React.FC = () => {
<div ref={messagesEndRef} />
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<div className={styles.inputContainer}>
<TextArea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="请输入你的分析需求..."
placeholder="请描述您的分析需求..."
autoSize={{ minRows: 2, maxRows: 6 }}
className={styles.input}
onPressEnter={(e) => {
if (!e.shiftKey) {
e.preventDefault();
@ -185,9 +245,9 @@ const AnalysisCenter: React.FC = () => {
icon={<SendOutlined />}
onClick={handleSend}
loading={loading}
style={{ alignSelf: 'flex-end' }}
className={styles.sendButton}
>
</Button>
</div>
</Space>

View 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;
}
}
}

View File

@ -1,13 +1,14 @@
import React, { useState } from 'react';
import { PageContainer } from '@ant-design/pro-components';
import { InboxOutlined } from '@ant-design/icons';
import { Upload, Card, Button, Steps, message, Input, Spin,Result } from 'antd';
import { InboxOutlined, FileExcelOutlined, BarChartOutlined, FileWordOutlined } from '@ant-design/icons';
import { Upload, Card, Button, Steps, message, Input, Spin, Result, Space, Progress, Alert, Typography } from 'antd';
import type { UploadFile } from 'antd/es/upload/interface';
import ReactECharts from 'echarts-for-react';
import { DownloadOutlined } from '@ant-design/icons';
import styles from './index.less';
const { Dragger } = Upload;
const { TextArea } = Input;
const { Title, Paragraph } = Typography;
const ReportPage: React.FC = () => {
const [fileList, setFileList] = useState<UploadFile[]>([]);
@ -16,108 +17,236 @@ const ReportPage: React.FC = () => {
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);
// 这里应该调用后端API上传文件
try {
const formData = new FormData();
formData.append('file', file);
// const response = await uploadFile(formData);
message.success('文件上传成功');
setCurrentStep(1);
// 模拟上传进度
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('文件上传成功');
setCurrentStep(1);
setLoading(false);
clearInterval(timer);
}, 2000);
} catch (error) {
message.error('文件上传失败');
setLoading(false);
}
setLoading(false);
};
const generateReport = async () => {
setLoading(true);
try {
// 这里应该调用后端API生成报告
// const response = await generateWordReport({ fileId: fileList[0].uid, goal });
// setWordUrl(response.data.wordUrl);
let progress = 0;
const timer = setInterval(() => {
progress += 20;
if (progress <= 100) {
setProgress(progress);
}
}, 1000);
// 模拟数据
setTimeout(() => {
clearInterval(timer);
setProgress(100);
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('报告生成成功');
setCurrentStep(2);
}, 1500);
setLoading(false);
}, 5000);
} catch (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 = [
{
title: '上传文件',
icon: <FileExcelOutlined />,
content: (
<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 文件!');
<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;
}
setFileList([file]);
handleUpload(file);
return false;
}}
onRemove={() => {
setFileList([]);
setCurrentStep(0);
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>
}}
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 style={{ textAlign: 'center' }}>
<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)}
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>
{loading && (
<div className={styles.progressContainer}>
<Progress percent={progress} status="active" />
<Paragraph type="secondary">...</Paragraph>
</div>
)}
</div>
),
},
{
title: '查看报告',
icon: <FileWordOutlined />,
content: wordUrl && (
<div style={{ textAlign: 'center' }}>
<div className={styles.reportStep}>
<Card>
<Result
status="success"
title="报告生成成功"
subTitle="您可以下载生成的Word报告文档"
subTitle="您可以查看报告预览或下载完整报告"
extra={[
<Button
type="primary"
icon={<DownloadOutlined />}
icon={<FileWordOutlined />}
onClick={() => window.open(wordUrl)}
key="download"
size="large"
>
</Button>,
<Button
onClick={() => {
@ -125,13 +254,27 @@ const ReportPage: React.FC = () => {
setFileList([]);
setGoal('');
setWordUrl('');
setPreviewData(null);
setReportData(null);
}}
key="again"
size="large"
>
</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>
</div>
),
@ -139,10 +282,19 @@ const ReportPage: React.FC = () => {
];
return (
<PageContainer>
<Card>
<Steps current={currentStep} items={steps} style={{ marginBottom: 24 }} />
<Spin spinning={loading}>{steps[currentStep].content}</Spin>
<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>
);