feat: 新增论坛&代码分析&数据分析&报告中心&个人中心
This commit is contained in:
parent
3646d5ec93
commit
df7c2bb696
@ -7,10 +7,24 @@ export default [
|
|||||||
|
|
||||||
{ path: '/home', name :"首页", icon: "PieChartOutlined", component: './HomePage' },
|
{ path: '/home', name :"首页", icon: "PieChartOutlined", component: './HomePage' },
|
||||||
{ path: '/add_chart', name :"智能分析", icon: "barChart", component: './AddChart' },
|
{ 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: '/my_chart', name: "我的图表", icon: "PictureOutlined", component: './MyChart' },
|
||||||
{ path: '/open_platform', name :"开放中心", icon: "ApiOutlined", component: './OpenPlatform' },
|
{ path: '/open_platform', name :"开放中心", icon: "ApiOutlined", component: './OpenPlatform' },
|
||||||
{ path: '/forum', name :"交流论坛", icon: "CrownOutlined", component: './Forum' },
|
{ 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',
|
path: '/admin',
|
||||||
name: '管理页',
|
name: '管理页',
|
||||||
|
@ -48,6 +48,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^4.8.1",
|
"@ant-design/icons": "^4.8.1",
|
||||||
"@ant-design/pro-components": "^2.6.48",
|
"@ant-design/pro-components": "^2.6.48",
|
||||||
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@umijs/route-utils": "^2.2.2",
|
"@umijs/route-utils": "^2.2.2",
|
||||||
"antd": "5.24.5",
|
"antd": "5.24.5",
|
||||||
"antd-style": "^3.6.1",
|
"antd-style": "^3.6.1",
|
||||||
@ -55,6 +56,7 @@
|
|||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts-for-react": "^3.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"mermaid": "^11.6.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"omit.js": "^2.0.2",
|
"omit.js": "^2.0.2",
|
||||||
"querystring": "^0.2.1",
|
"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 React from 'react';
|
||||||
import type { UploadFile, UploadProps } from 'antd';
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
import {Avatar,Button,Card,Divider,Form,Image,Input,List,message,Space,Tooltip,Upload} from 'antd';
|
import { Outlet } from '@umijs/max';
|
||||||
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;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const ForumLayout: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<div style={{ maxWidth: 800, margin: '0 auto', padding: 20 }}>
|
<PageContainer>
|
||||||
{/* 发布新帖子区域 */}
|
<Outlet />
|
||||||
<Card title="发布新帖子" style={{ marginBottom: 20 }}>
|
</PageContainer>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 评论编辑器组件
|
export default ForumLayout;
|
||||||
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;
|
|
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