This commit is contained in:
jeremygan2021
2026-02-12 14:23:36 +08:00
parent f00cc9a28e
commit b4ac97c3c2
7 changed files with 201 additions and 82 deletions

View File

@@ -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);
}}
/>