feat: 新增论坛&代码分析&数据分析&报告中心&个人中心

This commit is contained in:
Shu Guang 2025-04-17 21:58:14 +08:00
parent 3646d5ec93
commit df7c2bb696
12 changed files with 1120 additions and 403 deletions

View File

@ -7,10 +7,24 @@ export default [
{ path: '/home', name :"首页", icon: "PieChartOutlined", component: './HomePage' },
{ path: '/add_chart', name :"智能分析", icon: "barChart", component: './AddChart' },
{ path: '/add_async', name :"智能分析(异步)", icon: "DotChartOutlined", component: './AddChartAsync' },
{ path: '/add_async', name: "智能分析(异步)", icon: "DotChartOutlined", component: './AddChartAsync' },
{ path: '/report', name: "报告中心", icon: "commentOutlined", component: './Report' },
{ path: '/code', name: "代码分析", icon: "GithubOutlined", component: './Code' },
{ path: '/Forecast', name :"分析中心", icon: "ApiOutlined", component: './Forecast' },
{ path: '/my_chart', name: "我的图表", icon: "PictureOutlined", component: './MyChart' },
{ path: '/open_platform', name :"开放中心", icon: "ApiOutlined", component: './OpenPlatform' },
{ path: '/forum', name :"交流论坛", icon: "CrownOutlined", component: './Forum' },
{ path: '/open_platform', name :"开放中心", icon: "ApiOutlined", component: './OpenPlatform' },
{ path: '/forum', name: "交流论坛", icon: "CrownOutlined", component: './Forum' },
{ path: '/user/center', name: "个人中心", icon: "UserOutlined", component: './User/Info' },
{
path: '/forum',
component: '@/pages/Forum',
routes: [
{ path: '/forum', redirect: '/forum/list' },
{ path: '/forum/list', component: '@/pages/Forum/List' },
{ path: '/forum/detail/:id', component: '@/pages/Forum/Detail' },
{ path: '/forum/publish', component: '@/pages/Forum/Publish' },
],
},
{
path: '/admin',
name: '管理页',

View File

@ -48,6 +48,7 @@
"dependencies": {
"@ant-design/icons": "^4.8.1",
"@ant-design/pro-components": "^2.6.48",
"@monaco-editor/react": "^4.7.0",
"@umijs/route-utils": "^2.2.2",
"antd": "5.24.5",
"antd-style": "^3.6.1",
@ -55,6 +56,7 @@
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2",
"lodash": "^4.17.21",
"mermaid": "^11.6.0",
"moment": "^2.30.1",
"omit.js": "^2.0.2",
"querystring": "^0.2.1",

View File

@ -0,0 +1,30 @@
import React, { useEffect, useRef } from 'react';
import mermaid from 'mermaid';
interface MermaidProps {
chart: string;
}
const Mermaid: React.FC<MermaidProps> = ({ chart }) => {
const elementRef = useRef<HTMLDivElement>(null);
useEffect(() => {
mermaid.initialize({
startOnLoad: true,
theme: 'default',
securityLevel: 'loose',
});
if (elementRef.current) {
mermaid.render('mermaid-diagram', chart).then((result) => {
if (elementRef.current) {
elementRef.current.innerHTML = result.svg;
}
});
}
}, [chart]);
return <div ref={elementRef} />;
};
export default Mermaid;

View File

@ -0,0 +1,28 @@
import React from 'react';
import Editor from '@monaco-editor/react';
interface MonacoEditorProps {
language: string;
value: string;
onChange: (value: string) => void;
height?: string;
}
const MonacoEditor: React.FC<MonacoEditorProps> = ({ language, value, onChange, height = '300px' }) => {
return (
<Editor
height={height}
language={language}
value={value}
onChange={(value) => onChange(value || '')}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
automaticLayout: true,
}}
/>
);
};
export default MonacoEditor;

178
src/pages/Code/index.tsx Normal file
View File

@ -0,0 +1,178 @@
import React, { useState } from 'react';
import { PageContainer } from '@ant-design/pro-components';
import { InboxOutlined, CopyOutlined, DownloadOutlined } from '@ant-design/icons';
import { Upload, Card, Button, Tabs, message, Input, Spin, Radio, Space, Tooltip } from 'antd';
import type { UploadFile } from 'antd/es/upload/interface';
import ReactECharts from 'echarts-for-react';
import Mermaid from '@/components/Mermaid';
import MonacoEditor from '@/components/MonacoEditor';
const { Dragger } = Upload;
const { TabPane } = Tabs;
const { TextArea } = Input;
const CodeAnalysisPage: React.FC = () => {
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [loading, setLoading] = useState(false);
const [analysisType, setAnalysisType] = useState<'er' | 'module'>('er');
const [analysisResult, setAnalysisResult] = useState<string>('');
const [diagramCode, setDiagramCode] = useState<string>('');
const [codeContent, setCodeContent] = useState<string>('');
const [activeTab, setActiveTab] = useState<string>('editor');
// 处理代码编辑
const handleCodeChange = (value: string) => {
setCodeContent(value);
};
// 处理代码分析
const handleAnalyze = async () => {
if (!codeContent.trim()) {
message.warning('请输入或上传代码');
return;
}
setLoading(true);
try {
// 这里应该调用后端API进行分析
// const response = await analyzeCode({ content: codeContent, type: analysisType });
// 模拟API响应
setTimeout(() => {
if (analysisType === 'er') {
setDiagramCode(`
erDiagram
USER ||--o{ POST : creates
POST ||--|{ COMMENT : has
USER {
string username
string email
timestamp created_at
}
POST {
string title
text content
timestamp published_at
}
COMMENT {
text content
timestamp created_at
}
`);
} else {
setDiagramCode(`
graph TD
A[] --> B[]
A --> C[]
B --> D[]
C --> E[]
`);
}
setAnalysisResult('分析完成,建议优化数据库索引结构,添加适当的外键约束。');
setActiveTab('result');
message.success('分析成功');
}, 1500);
} catch (error) {
message.error('分析失败');
}
setLoading(false);
};
// 处理文件上传
const handleUpload = async (file: File) => {
setLoading(true);
try {
// 读取文件内容
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result as string;
setCodeContent(content);
setActiveTab('editor');
};
reader.readAsText(file);
} catch (error) {
message.error('文件读取失败');
}
setLoading(false);
};
return (
<PageContainer>
<Card>
<Space direction="vertical" style={{ width: '100%' }}>
<Radio.Group
value={analysisType}
onChange={(e) => {
setAnalysisType(e.target.value);
setFileList([]);
setDiagramCode('');
setAnalysisResult('');
}}
>
<Radio.Button value="er">ER图生成</Radio.Button>
<Radio.Button value="module"></Radio.Button>
</Radio.Group>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab="代码编辑器" key="editor">
<Card>
<MonacoEditor
language={analysisType === 'er' ? 'sql' : 'typescript'}
value={codeContent}
onChange={handleCodeChange}
height="400px"
/>
<div style={{ marginTop: 16, textAlign: 'right' }}>
<Space>
<Dragger
showUploadList={false}
beforeUpload={(file) => {
handleUpload(file);
return false;
}}
style={{ display: 'inline-block' }}
>
<Button></Button>
</Dragger>
<Button type="primary" onClick={handleAnalyze} loading={loading}>
</Button>
</Space>
</div>
</Card>
</TabPane>
<TabPane tab="分析结果" key="result">
{diagramCode && (
<Card>
<div style={{ marginBottom: 16 }}>
<h3></h3>
<p>{analysisResult}</p>
</div>
<Card title="可视化图表" extra={
<Space>
<Tooltip title="复制图表代码">
<Button
icon={<CopyOutlined />}
onClick={() => {
navigator.clipboard.writeText(diagramCode);
message.success('复制成功');
}}
/>
</Tooltip>
<Tooltip title="导出图表">
<Button icon={<DownloadOutlined />} />
</Tooltip>
</Space>
}>
<Mermaid chart={diagramCode} />
</Card>
</Card>
)}
</TabPane>
</Tabs>
</Space>
</Card>
</PageContainer>
);
};
export default CodeAnalysisPage;

View File

@ -0,0 +1,199 @@
import React, { useState, useRef } from 'react';
import { PageContainer } from '@ant-design/pro-components';
import {
Card,
Select,
Input,
Button,
List,
Avatar,
Spin,
Upload,
message,
Tooltip,
Space,
} from 'antd';
import { SendOutlined, RobotOutlined, UserOutlined, InboxOutlined } from '@ant-design/icons';
import type { UploadFile } from 'antd/es/upload/interface';
import ReactECharts from 'echarts-for-react';
const { Option } = Select;
const { Dragger } = Upload;
const { TextArea } = Input;
interface Message {
type: 'user' | 'assistant';
content: string;
timestamp: number;
charts?: any;
}
const AnalysisCenter: React.FC = () => {
const [analysisType, setAnalysisType] = useState<string>('predictive');
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [fileList, setFileList] = useState<UploadFile[]>([]);
const messagesEndRef = useRef<null | HTMLDivElement>(null);
const analysisOptions = [
{ value: 'predictive', label: '预测性分析' },
{ value: 'descriptive', label: '描述性统计' },
{ value: 'anomaly', label: '异常检测' },
{ value: 'quality', label: '数据质量分析' },
];
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
const handleSend = async () => {
if (!inputValue.trim()) return;
const userMessage: Message = {
type: 'user',
content: inputValue,
timestamp: Date.now(),
};
setMessages([...messages, userMessage]);
setInputValue('');
setLoading(true);
try {
// 这里应该调用后端API进行分析
// const response = await analyzeData({ type: analysisType, message: inputValue });
// 模拟API响应
setTimeout(() => {
const assistantMessage: Message = {
type: 'assistant',
content: generateMockResponse(analysisType),
timestamp: Date.now(),
charts: generateMockChart(analysisType),
};
setMessages(prev => [...prev, assistantMessage]);
setLoading(false);
scrollToBottom();
}, 1500);
} catch (error) {
message.error('分析请求失败');
setLoading(false);
}
};
const generateMockResponse = (type: string) => {
const responses: { [key: string]: string } = {
predictive: '根据历史数据分析预计未来三个月的销售增长率将达到15%,主要增长点来自新市场的开拓。',
descriptive: '数据集中包含1000条记录平均值为45.6标准差为12.3,分布呈现正态分布特征。',
anomaly: '检测到3个异常值点主要出现在数据的边缘区域建议进行进一步核实。',
quality: '数据完整性为98.5%,存在少量缺失值,建议对缺失数据进行适当的填充处理。',
};
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="请选择分析类型"
>
{analysisOptions.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
<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>
<div style={{ height: '400px', overflowY: 'auto', marginBottom: '16px' }}>
<List
itemLayout="horizontal"
dataSource={messages}
renderItem={item => (
<List.Item style={{ justifyContent: item.type === 'user' ? 'flex-end' : 'flex-start' }}>
<Card style={{ maxWidth: '80%' }}>
<List.Item.Meta
avatar={
<Avatar icon={item.type === 'user' ? <UserOutlined /> : <RobotOutlined />} />
}
title={item.type === 'user' ? '你' : 'AI 助手'}
description={item.content}
/>
{item.charts && (
<div style={{ marginTop: '16px' }}>
<ReactECharts option={item.charts} style={{ height: '300px' }} />
</div>
)}
</Card>
</List.Item>
)}
/>
<div ref={messagesEndRef} />
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<TextArea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="请输入你的分析需求..."
autoSize={{ minRows: 2, maxRows: 6 }}
onPressEnter={(e) => {
if (!e.shiftKey) {
e.preventDefault();
handleSend();
}
}}
/>
<Button
type="primary"
icon={<SendOutlined />}
onClick={handleSend}
loading={loading}
style={{ alignSelf: 'flex-end' }}
>
</Button>
</div>
</Space>
</Card>
</PageContainer>
);
};
export default AnalysisCenter;

View File

@ -0,0 +1,104 @@
import React, { useState } from 'react';
import { useParams } from '@umijs/max';
import {
Card,
Avatar,
Typography,
Space,
Divider,
Button,
Input,
message,
Tag,
} from 'antd';
import {
LikeOutlined,
LikeFilled,
StarOutlined,
StarFilled,
ShareAltOutlined,
} from '@ant-design/icons';
const { Title, Paragraph } = Typography;
const { TextArea } = Input;
const ForumDetail: React.FC = () => {
const { id } = useParams();
const [liked, setLiked] = useState(false);
const [collected, setCollected] = useState(false);
const [comment, setComment] = useState('');
const handleSubmitComment = () => {
if (!comment.trim()) {
message.warning('请输入评论内容');
return;
}
// 提交评论
message.success('评论成功');
setComment('');
};
return (
<Card>
<article>
<header style={{ marginBottom: 24 }}>
<Title level={2}></Title>
<Space split={<Divider type="vertical" />}>
<Space>
<Avatar src="https://joeschmoe.io/api/v1/random" />
<span></span>
</Space>
<span></span>
<Tag color="blue"></Tag>
<span> 1000</span>
</Space>
</header>
<Paragraph>
</Paragraph>
<div style={{ marginTop: 24 }}>
<Space size="large">
<Button
icon={liked ? <LikeFilled /> : <LikeOutlined />}
onClick={() => setLiked(!liked)}
>
</Button>
<Button
icon={collected ? <StarFilled /> : <StarOutlined />}
onClick={() => setCollected(!collected)}
>
</Button>
<Button icon={<ShareAltOutlined />}>
</Button>
</Space>
</div>
<Divider />
<div>
<Title level={4}></Title>
<TextArea
rows={4}
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="写下你的评论..."
style={{ marginBottom: 16 }}
/>
<Button type="primary" onClick={handleSubmitComment}>
</Button>
{/* 评论列表 */}
{/* 这里可以复用你原有的评论列表组件 */}
</div>
</article>
</Card>
);
};
export default ForumDetail;

View File

@ -0,0 +1,97 @@
import React, { useState } from 'react';
import { Card, List, Tag, Space, Input, Button, Select } from 'antd';
import { history } from '@umijs/max';
import { SearchOutlined, PlusOutlined } from '@ant-design/icons';
const { Search } = Input;
const ForumList: React.FC = () => {
const [loading, setLoading] = useState(false);
const [filter, setFilter] = useState('all');
const categories = [
{ value: 'all', label: '全部' },
{ value: 'tech', label: '技术讨论' },
{ value: 'share', label: '经验分享' },
{ value: 'question', label: '问题求助' },
];
const handleSearch = (value: string) => {
console.log('搜索:', value);
};
return (
<Card>
<div style={{ marginBottom: 16 }}>
<Space size="large">
<Search
placeholder="搜索帖子"
onSearch={handleSearch}
style={{ width: 300 }}
/>
<Select
value={filter}
onChange={setFilter}
options={categories}
style={{ width: 120 }}
/>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => history.push('/forum/publish')}
>
</Button>
</Space>
</div>
<List
itemLayout="vertical"
size="large"
pagination={{
pageSize: 10,
}}
dataSource={[]} // 这里需要接入实际数据
renderItem={(item: any) => (
<List.Item
key={item.id}
actions={[
<Space>
<span> {item.views}</span>
<span> {item.comments}</span>
<span> {item.likes}</span>
</Space>,
]}
extra={
item.cover && (
<img
width={272}
alt="cover"
src={item.cover}
/>
)
}
>
<List.Item.Meta
title={
<Space>
<a onClick={() => history.push(`/forum/detail/${item.id}`)}>{item.title}</a>
<Tag color="blue">{item.category}</Tag>
</Space>
}
description={
<Space>
<span>{item.author}</span>
<span>{item.createTime}</span>
</Space>
}
/>
{item.description}
</List.Item>
)}
/>
</Card>
);
};
export default ForumList;

View File

@ -0,0 +1,86 @@
import React, { useState } from 'react';
import { Card, Form, Input, Button, Upload, Select, message } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import type { UploadFile } from 'antd/es/upload/interface';
import { history } from '@umijs/max';
const { TextArea } = Input;
const ForumPublish: React.FC = () => {
const [form] = Form.useForm();
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async (values: any) => {
setSubmitting(true);
try {
// 这里处理表单提交
console.log('提交数据:', values);
message.success('发布成功');
history.push('/forum/list');
} catch (error) {
message.error('发布失败');
}
setSubmitting(false);
};
return (
<Card title="发布帖子">
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
>
<Form.Item
name="title"
label="标题"
rules={[{ required: true, message: '请输入标题' }]}
>
<Input placeholder="请输入帖子标题" />
</Form.Item>
<Form.Item
name="category"
label="分类"
rules={[{ required: true, message: '请选择分类' }]}
>
<Select>
<Select.Option value="tech"></Select.Option>
<Select.Option value="share"></Select.Option>
<Select.Option value="question"></Select.Option>
</Select>
</Form.Item>
<Form.Item
name="content"
label="内容"
rules={[{ required: true, message: '请输入内容' }]}
>
<TextArea rows={8} placeholder="请输入帖子内容..." />
</Form.Item>
<Form.Item label="封面图">
<Upload
listType="picture-card"
fileList={fileList}
onChange={({ fileList }) => setFileList(fileList)}
beforeUpload={() => false}
>
{fileList.length < 1 && <PlusOutlined />}
</Upload>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={submitting}>
</Button>
<Button style={{ marginLeft: 8 }} onClick={() => history.back()}>
</Button>
</Form.Item>
</Form>
</Card>
);
};
export default ForumPublish;

View File

@ -1,405 +1,13 @@
import {LikeFilled,LikeOutlined,MessageOutlined,PlusOutlined,SendOutlined,} from '@ant-design/icons';
import type { UploadFile, UploadProps } from 'antd';
import {Avatar,Button,Card,Divider,Form,Image,Input,List,message,Space,Tooltip,Upload} from 'antd';
import moment from 'moment';
import React, { useState } from 'react';
const { TextArea } = Input;
type CommentType = {
id: string;
author: string;
avatar: string;
content: string;
datetime: string;
likes: number;
liked: boolean;
};
type PostType = {
id: string;
author: string;
avatar: string;
content: string;
images: string[];
datetime: string;
likes: number;
liked: boolean;
comments: CommentType[];
commentVisible: boolean;
};
const Forum: React.FC = () => {
// 当前用户信息
const currentUser = {
name: '当前用户',
avatar: 'https://joeschmoe.io/api/v1/random',
};
// 帖子列表数据
const [posts, setPosts] = useState<PostType[]>([
{
id: '1',
author: '用户1',
avatar: 'https://joeschmoe.io/api/v1/1',
content: '这是一个示例帖子内容,欢迎大家讨论!',
images: ['https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'],
datetime: '2023-05-01 10:00:00',
likes: 5,
liked: false,
comments: [
{
id: '1-1',
author: '用户2',
avatar: 'https://joeschmoe.io/api/v1/2',
content: '这个帖子很有意义!',
datetime: '2023-05-01 10:30:00',
likes: 2,
liked: false,
},
],
commentVisible: false,
},
{
id: '2',
author: '用户3',
avatar: 'https://joeschmoe.io/api/v1/3',
content: '分享一张有趣的图片',
images: [
'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
],
datetime: '2023-05-02 15:00:00',
likes: 10,
liked: true,
comments: [],
commentVisible: false,
},
]);
// 新帖子内容
const [postContent, setPostContent] = useState('');
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState('');
// 处理图片预览
const handlePreview = async (file: UploadFile) => {
if (!file.url && !file.preview) {
file.preview = URL.createObjectURL(file.originFileObj as any);
}
setPreviewImage(file.url || (file.preview as string));
setPreviewOpen(true);
};
// 处理图片变化
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
setFileList(newFileList);
};
// 上传按钮
const uploadButton = (
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}></div>
</div>
);
// 发布新帖子
const handlePostSubmit = () => {
if (!postContent.trim() && fileList.length === 0) {
message.warning('请填写内容或上传图片');
return;
}
const newPost: PostType = {
id: Date.now().toString(),
author: currentUser.name,
avatar: currentUser.avatar,
content: postContent,
images: fileList.map((file) => file.url || (file.preview as string)),
datetime: moment().format('YYYY-MM-DD HH:mm:ss'),
likes: 0,
liked: false,
comments: [],
commentVisible: false,
};
setPosts([newPost, ...posts]);
setPostContent('');
setFileList([]);
message.success('帖子发布成功');
};
// 点赞帖子
const handleLikePost = (postId: string) => {
setPosts(
posts.map((post) => {
if (post.id === postId) {
return {
...post,
likes: post.liked ? post.likes - 1 : post.likes + 1,
liked: !post.liked,
};
}
return post;
}),
);
};
// 切换评论可见性
const toggleCommentVisible = (postId: string) => {
setPosts(
posts.map((post) => {
if (post.id === postId) {
return {
...post,
commentVisible: !post.commentVisible,
};
}
return post;
}),
);
};
// 添加评论
const handleAddComment = (postId: string, content: string) => {
if (!content.trim()) {
message.warning('评论内容不能为空');
return;
}
const newComment: CommentType = {
id: `${postId}-${Date.now()}`,
author: currentUser.name,
avatar: currentUser.avatar,
content: content,
datetime: moment().format('YYYY-MM-DD HH:mm:ss'),
likes: 0,
liked: false,
};
setPosts(
posts.map((post) => {
if (post.id === postId) {
return {
...post,
comments: [...post.comments, newComment],
};
}
return post;
}),
);
};
// 点赞评论
const handleLikeComment = (postId: string, commentId: string) => {
setPosts(
posts.map((post) => {
if (post.id === postId) {
return {
...post,
comments: post.comments.map((comment) => {
if (comment.id === commentId) {
return {
...comment,
likes: comment.liked ? comment.likes - 1 : comment.likes + 1,
liked: !comment.liked,
};
}
return comment;
}),
};
}
return post;
}),
);
};
import React from 'react';
import { PageContainer } from '@ant-design/pro-components';
import { Outlet } from '@umijs/max';
const ForumLayout: React.FC = () => {
return (
<div style={{ maxWidth: 800, margin: '0 auto', padding: 20 }}>
{/* 发布新帖子区域 */}
<Card title="发布新帖子" style={{ marginBottom: 20 }}>
<Form.Item>
<TextArea
rows={4}
value={postContent}
onChange={(e) => setPostContent(e.target.value)}
placeholder="分享你的想法..."
/>
</Form.Item>
<Form.Item>
<Upload
listType="picture-card"
fileList={fileList}
onPreview={handlePreview}
onChange={handleChange}
beforeUpload={() => false} // 阻止自动上传
multiple
>
{fileList.length >= 4 ? null : uploadButton}
</Upload>
</Form.Item>
<Form.Item>
<Button type="primary" onClick={handlePostSubmit} icon={<SendOutlined />}>
</Button>
</Form.Item>
</Card>
{/* 图片预览 */}
{previewImage && (
<Image
wrapperStyle={{ display: 'none' }}
preview={{
visible: previewOpen,
onVisibleChange: (visible) => setPreviewOpen(visible),
afterOpenChange: (visible) => !visible && setPreviewImage(''),
}}
src={previewImage}
/>
)}
{/* 帖子列表 */}
<List
itemLayout="vertical"
size="large"
dataSource={posts}
renderItem={(post) => (
<Card style={{ marginBottom: 20 }}>
<Comment
author={<a>{post.author}</a>}
avatar={<Avatar src={post.avatar} alt={post.author} />}
content={<p>{post.content}</p>}
datetime={
<Tooltip title={post.datetime}>
<span>{moment(post.datetime).fromNow()}</span>
</Tooltip>
}
/>
{/* 帖子图片 */}
{post.images.length > 0 && (
<div style={{ marginTop: 16 }}>
<Image.PreviewGroup>
<Space wrap>
{post.images.map((img, index) => (
<Image
key={index}
width={150}
height={150}
src={img}
style={{ objectFit: 'cover' }}
/>
))}
</Space>
</Image.PreviewGroup>
</div>
)}
{/* 帖子操作 */}
<div style={{ marginTop: 16 }}>
<Space>
<span onClick={() => handleLikePost(post.id)} style={{ cursor: 'pointer' }}>
{post.liked ? <LikeFilled style={{ color: '#1890ff' }} /> : <LikeOutlined />}
<span style={{ paddingLeft: 8 }}>{post.likes}</span>
</span>
<span onClick={() => toggleCommentVisible(post.id)} style={{ cursor: 'pointer' }}>
<MessageOutlined />
<span style={{ paddingLeft: 8 }}>{post.comments.length}</span>
</span>
</Space>
</div>
{/* 评论区域 */}
{post.commentVisible && (
<div style={{ marginTop: 16 }}>
<Divider orientation="left" plain>
</Divider>
{/* 评论列表 */}
<List
dataSource={post.comments}
renderItem={(comment) => (
<Comment
author={<a>{comment.author}</a>}
avatar={<Avatar src={comment.avatar} alt={comment.author} />}
content={<p>{comment.content}</p>}
datetime={
<Tooltip title={comment.datetime}>
<span>{moment(comment.datetime).fromNow()}</span>
</Tooltip>
}
actions={[
<span
key="comment-like"
onClick={() => handleLikeComment(post.id, comment.id)}
style={{ cursor: 'pointer' }}
>
{comment.liked ? (
<LikeFilled style={{ color: '#1890ff' }} />
) : (
<LikeOutlined />
)}
<span style={{ paddingLeft: 8 }}>{comment.likes}</span>
</span>,
]}
/>
)}
/>
{/* 添加评论 */}
<Comment
avatar={<Avatar src={currentUser.avatar} alt={currentUser.name} />}
content={
<Editor
onSubmit={(content) => handleAddComment(post.id, content)}
placeholder="写下你的评论..."
/>
}
/>
</div>
)}
</Card>
)}
/>
</div>
<PageContainer>
<Outlet />
</PageContainer>
);
};
// 评论编辑器组件
const Editor = ({
onSubmit,
placeholder,
}: {
onSubmit: (content: string) => void;
placeholder: string;
}) => {
const [value, setValue] = useState('');
const handleSubmit = () => {
onSubmit(value);
setValue('');
};
return (
<>
<TextArea
rows={2}
onChange={(e) => setValue(e.target.value)}
value={value}
placeholder={placeholder}
/>
<Form.Item style={{ marginTop: 16, marginBottom: 0 }}>
<Button htmlType="submit" onClick={handleSubmit} type="primary" disabled={!value.trim()}>
</Button>
</Form.Item>
</>
);
};
export default Forum;
export default ForumLayout;

151
src/pages/Report/index.tsx Normal file
View File

@ -0,0 +1,151 @@
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 type { UploadFile } from 'antd/es/upload/interface';
import ReactECharts from 'echarts-for-react';
import { DownloadOutlined } from '@ant-design/icons';
const { Dragger } = Upload;
const { TextArea } = Input;
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 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);
} catch (error) {
message.error('文件上传失败');
}
setLoading(false);
};
const generateReport = async () => {
setLoading(true);
try {
// 这里应该调用后端API生成报告
// const response = await generateWordReport({ fileId: fileList[0].uid, goal });
// setWordUrl(response.data.wordUrl);
// 模拟数据
setTimeout(() => {
setWordUrl('https://example.com/report.docx');
message.success('报告生成成功');
setCurrentStep(2);
}, 1500);
} catch (error) {
message.error('报告生成失败');
}
setLoading(false);
};
const steps = [
{
title: '上传文件',
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 文件!');
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>
),
},
{
title: '设置目标',
content: (
<div style={{ textAlign: 'center' }}>
<TextArea
placeholder="请输入你想要分析的目标,例如:分析销售趋势、客户分布等"
rows={4}
value={goal}
onChange={(e) => setGoal(e.target.value)}
style={{ marginBottom: 16 }}
/>
<Button type="primary" onClick={generateReport} disabled={!goal}>
</Button>
</div>
),
},
{
title: '查看报告',
content: wordUrl && (
<div style={{ textAlign: 'center' }}>
<Card>
<Result
status="success"
title="报告生成成功"
subTitle="您可以下载生成的Word报告文档"
extra={[
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={() => window.open(wordUrl)}
key="download"
>
</Button>,
<Button
onClick={() => {
setCurrentStep(0);
setFileList([]);
setGoal('');
setWordUrl('');
}}
key="again"
>
</Button>,
]}
/>
</Card>
</div>
),
},
];
return (
<PageContainer>
<Card>
<Steps current={currentStep} items={steps} style={{ marginBottom: 24 }} />
<Spin spinning={loading}>{steps[currentStep].content}</Spin>
</Card>
</PageContainer>
);
};
export default ReportPage;

View File

@ -0,0 +1,220 @@
import React, { useState } from 'react';
import { PageContainer, ProCard } from '@ant-design/pro-components';
import {
Avatar,
Button,
Card,
Descriptions,
Form,
Input,
Modal,
Space,
Tabs,
Tag,
Upload,
message,
} from 'antd';
import {
UserOutlined,
MailOutlined,
PhoneOutlined,
KeyOutlined,
UploadOutlined,
} from '@ant-design/icons';
import { useModel } from '@umijs/max';
import type { RcFile, UploadProps } from 'antd/es/upload';
const { TabPane } = Tabs;
const UserInfo: React.FC = () => {
const { initialState } = useModel('@@initialState');
const { currentUser } = initialState || {};
const [passwordVisible, setPasswordVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
// 处理头像上传
const handleAvatarUpload: UploadProps['onChange'] = async (info) => {
if (info.file.status === 'uploading') {
setLoading(true);
return;
}
if (info.file.status === 'done') {
message.success('头像更新成功');
setLoading(false);
}
};
// 处理密码修改
const handlePasswordChange = async (values: any) => {
try {
// 这里应该调用修改密码的API
// await updatePassword(values);
message.success('密码修改成功');
setPasswordVisible(false);
form.resetFields();
} catch (error) {
message.error('密码修改失败');
}
};
// 处理基本信息更新
const handleInfoUpdate = async (values: any) => {
try {
// 这里应该调用更新用户信息的API
// await updateUserInfo(values);
message.success('信息更新成功');
} catch (error) {
message.error('信息更新失败');
}
};
return (
<PageContainer>
<ProCard split="vertical">
<ProCard colSpan="30%">
<div style={{ textAlign: 'center', padding: '24px' }}>
<Upload
name="avatar"
showUploadList={false}
onChange={handleAvatarUpload}
>
<Space direction="vertical" size="large">
<Avatar
size={120}
src={currentUser?.avatar}
icon={<UserOutlined />}
/>
<Button icon={<UploadOutlined />} loading={loading}>
</Button>
</Space>
</Upload>
<div style={{ marginTop: '16px' }}>
<h2>{currentUser?.name}</h2>
<Tag color="blue">{currentUser?.role || '普通用户'}</Tag>
</div>
</div>
</ProCard>
<ProCard>
<Tabs defaultActiveKey="1">
<TabPane tab="基本信息" key="1">
<Form
layout="vertical"
initialValues={currentUser}
onFinish={handleInfoUpdate}
>
<Form.Item
label="用户名"
name="username"
rules={[{ required: true }]}
>
<Input prefix={<UserOutlined />} />
</Form.Item>
<Form.Item
label="邮箱"
name="email"
rules={[{ required: true, type: 'email' }]}
>
<Input prefix={<MailOutlined />} />
</Form.Item>
<Form.Item label="手机" name="phone">
<Input prefix={<PhoneOutlined />} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
</Button>
</Form.Item>
</Form>
</TabPane>
<TabPane tab="账号安全" key="2">
<Card>
<Descriptions>
<Descriptions.Item label="账号密码">
<Button
type="link"
onClick={() => setPasswordVisible(true)}
>
</Button>
</Descriptions.Item>
<Descriptions.Item label="手机验证"></Descriptions.Item>
<Descriptions.Item label="邮箱验证"></Descriptions.Item>
</Descriptions>
</Card>
</TabPane>
<TabPane tab="使用统计" key="3">
<Card>
<Descriptions column={2}>
<Descriptions.Item label="注册时间">
{currentUser?.registerTime || '-'}
</Descriptions.Item>
<Descriptions.Item label="最后登录">
{currentUser?.lastLogin || '-'}
</Descriptions.Item>
<Descriptions.Item label="API调用次数">
{currentUser?.apiCalls || 0}
</Descriptions.Item>
<Descriptions.Item label="分析次数">
{currentUser?.analysisCounts || 0}
</Descriptions.Item>
</Descriptions>
</Card>
</TabPane>
</Tabs>
</ProCard>
</ProCard>
<Modal
title="修改密码"
open={passwordVisible}
onCancel={() => setPasswordVisible(false)}
footer={null}
>
<Form form={form} onFinish={handlePasswordChange}>
<Form.Item
name="oldPassword"
rules={[{ required: true, message: '请输入原密码' }]}
>
<Input.Password prefix={<KeyOutlined />} placeholder="原密码" />
</Form.Item>
<Form.Item
name="newPassword"
rules={[{ required: true, message: '请输入新密码' }]}
>
<Input.Password prefix={<KeyOutlined />} placeholder="新密码" />
</Form.Item>
<Form.Item
name="confirmPassword"
dependencies={['newPassword']}
rules={[
{ required: true, message: '请确认新密码' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('newPassword') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('两次输入的密码不一致'));
},
}),
]}
>
<Input.Password prefix={<KeyOutlined />} placeholder="确认新密码" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" block>
</Button>
</Form.Item>
</Form>
</Modal>
</PageContainer>
);
};
export default UserInfo;