new
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Typography, Input, Button, List, Tag, Avatar, Card, Space, Spin, message, Badge, Tooltip } from 'antd';
|
||||
import { SearchOutlined, PlusOutlined, UserOutlined, MessageOutlined, EyeOutlined, CheckCircleFilled, FireOutlined } from '@ant-design/icons';
|
||||
import { Typography, Input, Button, List, Tag, Avatar, Card, Space, Spin, message, Badge, Tooltip, Tabs, Row, Col } from 'antd';
|
||||
import { SearchOutlined, PlusOutlined, UserOutlined, MessageOutlined, EyeOutlined, CheckCircleFilled, FireOutlined, StarFilled, QuestionCircleOutlined, ShareAltOutlined, SoundOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { getTopics } from '../api';
|
||||
@@ -17,14 +17,17 @@ const ForumList = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [topics, setTopics] = useState([]);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [category, setCategory] = useState('all');
|
||||
const [createModalVisible, setCreateModalVisible] = useState(false);
|
||||
const [loginModalVisible, setLoginModalVisible] = useState(false);
|
||||
|
||||
const fetchTopics = async (search = '') => {
|
||||
const fetchTopics = async (search = '', cat = '') => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = {};
|
||||
if (search) params.search = search;
|
||||
if (cat && cat !== 'all') params.category = cat;
|
||||
|
||||
const res = await getTopics(params);
|
||||
setTopics(res.data.results || res.data); // Support pagination result or list
|
||||
} catch (error) {
|
||||
@@ -36,12 +39,12 @@ const ForumList = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTopics();
|
||||
}, []);
|
||||
fetchTopics(searchText, category);
|
||||
}, [category]);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
setSearchText(value);
|
||||
fetchTopics(value);
|
||||
fetchTopics(value, category);
|
||||
};
|
||||
|
||||
const handleCreateClick = () => {
|
||||
@@ -52,6 +55,32 @@ const ForumList = () => {
|
||||
setCreateModalVisible(true);
|
||||
};
|
||||
|
||||
const getCategoryIcon = (cat) => {
|
||||
switch(cat) {
|
||||
case 'help': return <QuestionCircleOutlined style={{ color: '#faad14' }} />;
|
||||
case 'share': return <ShareAltOutlined style={{ color: '#1890ff' }} />;
|
||||
case 'notice': return <SoundOutlined style={{ color: '#ff4d4f' }} />;
|
||||
default: return <MessageOutlined style={{ color: '#00b96b' }} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryLabel = (cat) => {
|
||||
switch(cat) {
|
||||
case 'help': return '求助';
|
||||
case 'share': return '分享';
|
||||
case 'notice': return '公告';
|
||||
default: return '讨论';
|
||||
}
|
||||
};
|
||||
|
||||
const items = [
|
||||
{ key: 'all', label: '全部话题' },
|
||||
{ key: 'discussion', label: '技术讨论' },
|
||||
{ key: 'help', label: '求助问答' },
|
||||
{ key: 'share', label: '经验分享' },
|
||||
{ key: 'notice', label: '官方公告' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', paddingBottom: 60 }}>
|
||||
{/* Hero Section */}
|
||||
@@ -95,91 +124,153 @@ const ForumList = () => {
|
||||
</div>
|
||||
|
||||
{/* Content Section */}
|
||||
<div style={{ maxWidth: 1000, margin: '0 auto', padding: '0 20px' }}>
|
||||
<List
|
||||
loading={loading}
|
||||
itemLayout="vertical"
|
||||
dataSource={topics}
|
||||
renderItem={(item) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<Card
|
||||
hoverable
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
background: 'rgba(20,20,20,0.6)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
backdropFilter: 'blur(10px)'
|
||||
}}
|
||||
bodyStyle={{ padding: '20px 24px' }}
|
||||
onClick={() => navigate(`/forum/${item.id}`)}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
{item.is_pinned && <Tag color="red">置顶</Tag>}
|
||||
{item.is_verified_owner && (
|
||||
<Tooltip title="已验证购买过相关产品">
|
||||
<Tag icon={<CheckCircleFilled />} color="#00b96b">认证用户</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Text style={{ color: '#fff', fontSize: 18, fontWeight: 'bold', cursor: 'pointer' }}>
|
||||
{item.title}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Paragraph
|
||||
ellipsis={{ rows: 2 }}
|
||||
style={{ color: '#aaa', marginBottom: 12, fontSize: 14 }}
|
||||
>
|
||||
{item.content.replace(/[#*`]/g, '')} {/* Simple markdown strip */}
|
||||
</Paragraph>
|
||||
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '0 20px' }}>
|
||||
<Row gutter={24}>
|
||||
<Col xs={24} md={18}>
|
||||
<Tabs
|
||||
defaultActiveKey="all"
|
||||
items={items}
|
||||
onChange={setCategory}
|
||||
tabBarStyle={{ color: '#fff' }}
|
||||
/>
|
||||
<List
|
||||
loading={loading}
|
||||
itemLayout="vertical"
|
||||
dataSource={topics}
|
||||
renderItem={(item) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<Card
|
||||
hoverable
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
background: 'rgba(20,20,20,0.6)',
|
||||
border: item.is_pinned ? '1px solid rgba(0, 185, 107, 0.4)' : '1px solid rgba(255,255,255,0.1)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
boxShadow: item.is_pinned ? '0 0 10px rgba(0, 185, 107, 0.1)' : 'none'
|
||||
}}
|
||||
bodyStyle={{ padding: '20px 24px' }}
|
||||
onClick={() => navigate(`/forum/${item.id}`)}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ marginBottom: 8, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
{item.is_pinned && <Tag color="red" icon={<FireOutlined />}>置顶</Tag>}
|
||||
<Tag icon={getCategoryIcon(item.category)} style={{ background: 'transparent', color: '#fff', border: '1px solid #444' }}>
|
||||
{getCategoryLabel(item.category)}
|
||||
</Tag>
|
||||
{item.is_verified_owner && (
|
||||
<Tooltip title="已验证购买过相关产品">
|
||||
<Tag icon={<CheckCircleFilled />} color="#00b96b" style={{ margin: 0 }}>认证用户</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Text style={{ color: '#fff', fontSize: 18, fontWeight: 'bold', cursor: 'pointer' }}>
|
||||
{item.title}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Paragraph
|
||||
ellipsis={{ rows: 2 }}
|
||||
style={{ color: '#aaa', marginBottom: 12, fontSize: 14 }}
|
||||
>
|
||||
{item.content.replace(/[#*`]/g, '')} {/* Simple markdown strip */}
|
||||
</Paragraph>
|
||||
|
||||
<Space size="middle" style={{ color: '#666', fontSize: 13 }}>
|
||||
<Space>
|
||||
<Avatar src={item.author_info?.avatar_url} icon={<UserOutlined />} size="small" />
|
||||
<Text style={{ color: '#888' }}>{item.author_info?.nickname || '匿名用户'}</Text>
|
||||
</Space>
|
||||
<span>•</span>
|
||||
<span>{new Date(item.created_at).toLocaleDateString()}</span>
|
||||
{item.product_info && (
|
||||
<Tag color="blue" style={{ marginLeft: 8 }}>{item.product_info.name}</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
<Space size="middle" style={{ color: '#666', fontSize: 13 }}>
|
||||
<Space>
|
||||
<Avatar src={item.author_info?.avatar_url} icon={<UserOutlined />} size="small" />
|
||||
<Text style={{ color: item.author_info?.is_star ? '#ffd700' : '#888', fontWeight: item.author_info?.is_star ? 'bold' : 'normal' }}>
|
||||
{item.author_info?.nickname || '匿名用户'}
|
||||
</Text>
|
||||
{item.author_info?.is_star && (
|
||||
<Tooltip title={item.author_info.title || "技术专家"}>
|
||||
<StarFilled style={{ color: '#ffd700' }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
<span>•</span>
|
||||
<span>{new Date(item.created_at).toLocaleDateString()}</span>
|
||||
{item.product_info && (
|
||||
<Tag color="blue" style={{ marginLeft: 8 }}>{item.product_info.name}</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 8, minWidth: 80 }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<MessageOutlined style={{ fontSize: 16, color: '#00b96b' }} />
|
||||
<div style={{ color: '#fff', fontWeight: 'bold' }}>{item.replies?.length || 0}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 8, minWidth: 80 }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<MessageOutlined style={{ fontSize: 16, color: '#00b96b' }} />
|
||||
<div style={{ color: '#fff', fontWeight: 'bold' }}>{item.replies?.length || 0}</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', marginTop: 5 }}>
|
||||
<EyeOutlined style={{ fontSize: 16, color: '#666' }} />
|
||||
<div style={{ color: '#888', fontSize: 12 }}>{item.view_count || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
locale={{ emptyText: <div style={{ color: '#666', padding: 40 }}>暂无帖子,来发布第一个吧!</div> }}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col xs={0} md={6}>
|
||||
<Card
|
||||
title={<Space><StarFilled style={{ color: '#ffd700' }} /><span style={{ color: '#fff' }}>技术专家榜</span></Space>}
|
||||
style={{ background: 'rgba(20,20,20,0.6)', border: '1px solid rgba(255,255,255,0.1)', backdropFilter: 'blur(10px)' }}
|
||||
headStyle={{ borderBottom: '1px solid rgba(255,255,255,0.1)' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', padding: '20px 0', color: '#666' }}>
|
||||
{/* 这里可以通过 API 获取专家列表,目前先做静态展示或从帖子中提取 */}
|
||||
<div style={{ marginBottom: 15, display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<Avatar size="large" src="https://api.dicebear.com/7.x/avataaars/svg?seed=Expert1" />
|
||||
<div style={{ textAlign: 'left' }}>
|
||||
<div style={{ color: '#fff', fontWeight: 'bold' }}>QuantMaster <StarFilled style={{ color: '#ffd700', fontSize: 12 }} /></div>
|
||||
<div style={{ color: '#666', fontSize: 12 }}>官方技术支持</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 15, display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<Avatar size="large" src="https://api.dicebear.com/7.x/avataaars/svg?seed=Expert2" />
|
||||
<div style={{ textAlign: 'left' }}>
|
||||
<div style={{ color: '#fff', fontWeight: 'bold' }}>AI_Wizard <StarFilled style={{ color: '#ffd700', fontSize: 12 }} /></div>
|
||||
<div style={{ color: '#666', fontSize: 12 }}>社区贡献者</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', marginTop: 5 }}>
|
||||
<EyeOutlined style={{ fontSize: 16, color: '#666' }} />
|
||||
<div style={{ color: '#888', fontSize: 12 }}>{item.view_count || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
locale={{ emptyText: <div style={{ color: '#666', padding: 40 }}>暂无帖子,来发布第一个吧!</div> }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title={<Space><SoundOutlined style={{ color: '#ff4d4f' }} /><span style={{ color: '#fff' }}>社区公告</span></Space>}
|
||||
style={{ marginTop: 20, background: 'rgba(20,20,20,0.6)', border: '1px solid rgba(255,255,255,0.1)', backdropFilter: 'blur(10px)' }}
|
||||
headStyle={{ borderBottom: '1px solid rgba(255,255,255,0.1)' }}
|
||||
>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={[
|
||||
'欢迎来到 Quant Speed 开发者社区',
|
||||
'发帖前请阅读社区规范',
|
||||
'如何获取“认证用户”标识?'
|
||||
]}
|
||||
renderItem={item => <List.Item style={{ color: '#aaa', padding: '8px 0', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{item}</List.Item>}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<CreateTopicModal
|
||||
visible={createModalVisible}
|
||||
onClose={() => setCreateModalVisible(false)}
|
||||
onSuccess={() => fetchTopics(searchText)}
|
||||
onSuccess={() => fetchTopics(searchText, category)}
|
||||
/>
|
||||
|
||||
<LoginModal
|
||||
visible={loginModalVisible}
|
||||
onClose={() => setLoginModalVisible(false)}
|
||||
onLoginSuccess={() => {
|
||||
// 登录成功后自动打开发布框
|
||||
setCreateModalVisible(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user