feat: 新增论坛&代码分析&数据分析&报告中心&个人中心
This commit is contained in:
parent
3646d5ec93
commit
df7c2bb696
@ -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: '管理页',
|
||||
|
@ -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",
|
||||
|
30
src/components/Mermaid/index.tsx
Normal file
30
src/components/Mermaid/index.tsx
Normal 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;
|
28
src/components/MonacoEditor/index.tsx
Normal file
28
src/components/MonacoEditor/index.tsx
Normal 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
178
src/pages/Code/index.tsx
Normal 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;
|
199
src/pages/Forecast/index.tsx
Normal file
199
src/pages/Forecast/index.tsx
Normal 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">支持 CSV、Excel 等数据文件格式</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;
|
104
src/pages/Forum/Detail/index.tsx
Normal file
104
src/pages/Forum/Detail/index.tsx
Normal 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;
|
97
src/pages/Forum/List/index.tsx
Normal file
97
src/pages/Forum/List/index.tsx
Normal 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;
|
86
src/pages/Forum/Publish/index.tsx
Normal file
86
src/pages/Forum/Publish/index.tsx
Normal 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;
|
@ -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
151
src/pages/Report/index.tsx
Normal 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;
|
220
src/pages/User/Info/index.tsx
Normal file
220
src/pages/User/Info/index.tsx
Normal 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;
|
Loading…
x
Reference in New Issue
Block a user