diff --git a/backend/community/migrations/0005_topic_category.py b/backend/community/migrations/0005_topic_category.py
new file mode 100644
index 0000000..f101bac
--- /dev/null
+++ b/backend/community/migrations/0005_topic_category.py
@@ -0,0 +1,18 @@
+# Generated by Django 6.0.1 on 2026-02-12 06:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('community', '0004_activity_banner_url_alter_activity_banner'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='topic',
+ name='category',
+ field=models.CharField(choices=[('discussion', '技术讨论'), ('help', '求助问答'), ('share', '经验分享'), ('notice', '官方公告')], default='discussion', max_length=20, verbose_name='分类'),
+ ),
+ ]
diff --git a/backend/community/models.py b/backend/community/models.py
index 1c756f4..bbdd21d 100644
--- a/backend/community/models.py
+++ b/backend/community/models.py
@@ -71,6 +71,15 @@ class Topic(models.Model):
论坛帖子/主题
"""
title = models.CharField(max_length=200, verbose_name="标题")
+
+ CATEGORY_CHOICES = (
+ ('discussion', '技术讨论'),
+ ('help', '求助问答'),
+ ('share', '经验分享'),
+ ('notice', '官方公告'),
+ )
+ category = models.CharField(max_length=20, choices=CATEGORY_CHOICES, default='discussion', verbose_name="分类")
+
content = models.TextField(verbose_name="内容", help_text="支持Markdown格式,支持插入图片")
author = models.ForeignKey(WeChatUser, on_delete=models.CASCADE, related_name='topics', verbose_name="作者")
diff --git a/backend/db.sqlite3 b/backend/db.sqlite3
index 7c09cf0..ba9ff15 100644
Binary files a/backend/db.sqlite3 and b/backend/db.sqlite3 differ
diff --git a/backend/shop/__pycache__/models.cpython-312.pyc b/backend/shop/__pycache__/models.cpython-312.pyc
index 50cf5ab..183b206 100644
Binary files a/backend/shop/__pycache__/models.cpython-312.pyc and b/backend/shop/__pycache__/models.cpython-312.pyc differ
diff --git a/backend/shop/__pycache__/serializers.cpython-312.pyc b/backend/shop/__pycache__/serializers.cpython-312.pyc
index f41f618..a0437bf 100644
Binary files a/backend/shop/__pycache__/serializers.cpython-312.pyc and b/backend/shop/__pycache__/serializers.cpython-312.pyc differ
diff --git a/backend/shop/migrations/0027_wechatuser_is_star_wechatuser_title.py b/backend/shop/migrations/0027_wechatuser_is_star_wechatuser_title.py
new file mode 100644
index 0000000..9fdc0ae
--- /dev/null
+++ b/backend/shop/migrations/0027_wechatuser_is_star_wechatuser_title.py
@@ -0,0 +1,23 @@
+# Generated by Django 6.0.1 on 2026-02-12 06:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('shop', '0026_wechatuser_phone_number'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='wechatuser',
+ name='is_star',
+ field=models.BooleanField(default=False, verbose_name='是否明星技术用户'),
+ ),
+ migrations.AddField(
+ model_name='wechatuser',
+ name='title',
+ field=models.CharField(blank=True, default='技术专家', max_length=50, verbose_name='专家头衔'),
+ ),
+ ]
diff --git a/backend/shop/models.py b/backend/shop/models.py
index f2e5d88..d0a30c0 100644
--- a/backend/shop/models.py
+++ b/backend/shop/models.py
@@ -20,6 +20,11 @@ class WeChatUser(models.Model):
country = models.CharField(max_length=64, verbose_name="国家", blank=True)
province = models.CharField(max_length=64, verbose_name="省份", blank=True)
city = models.CharField(max_length=64, verbose_name="城市", blank=True)
+
+ # 明星技术用户/专家标识
+ is_star = models.BooleanField(default=False, verbose_name="是否明星技术用户")
+ title = models.CharField(max_length=50, default="技术专家", verbose_name="专家头衔", blank=True)
+
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
diff --git a/backend/shop/serializers.py b/backend/shop/serializers.py
index 61ea115..7715f9d 100644
--- a/backend/shop/serializers.py
+++ b/backend/shop/serializers.py
@@ -22,8 +22,8 @@ class CommissionLogSerializer(serializers.ModelSerializer):
class WeChatUserSerializer(serializers.ModelSerializer):
class Meta:
model = WeChatUser
- fields = ['id', 'nickname', 'avatar_url', 'gender', 'country', 'province', 'city', 'phone_number']
- read_only_fields = ['id', 'phone_number']
+ fields = ['id', 'nickname', 'avatar_url', 'gender', 'country', 'province', 'city', 'phone_number', 'is_star', 'title']
+ read_only_fields = ['id', 'phone_number', 'is_star', 'title']
class DistributorSerializer(serializers.ModelSerializer):
user_info = WeChatUserSerializer(source='user', read_only=True)
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 0095f11..b218974 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -10,6 +10,8 @@ import ServiceDetail from './pages/ServiceDetail';
import VCCourses from './pages/VCCourses';
import VCCourseDetail from './pages/VCCourseDetail';
import MyOrders from './pages/MyOrders';
+import ForumList from './pages/ForumList';
+import ForumDetail from './pages/ForumDetail';
import 'antd/dist/reset.css';
import './App.css';
@@ -24,6 +26,8 @@ function App() {
} />
} />
} />
+ } />
+ } />
} />
} />
} />
diff --git a/frontend/src/api.js b/frontend/src/api.js
index e6aa74e..fa0f3c1 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -47,4 +47,18 @@ export const getUserInfo = () => {
return api.post('/wechat/update/', {});
};
+// Community / Forum API
+export const getTopics = (params) => api.get('/topics/', { params });
+export const getTopicDetail = (id) => api.get(`/topics/${id}/`);
+export const createTopic = (data) => api.post('/topics/', data);
+export const getReplies = (params) => api.get('/replies/', { params });
+export const createReply = (data) => api.post('/replies/', data);
+export const uploadMedia = (data) => {
+ return api.post('/media/', data, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ }
+ });
+};
+
export default api;
diff --git a/frontend/src/components/CreateTopicModal.jsx b/frontend/src/components/CreateTopicModal.jsx
new file mode 100644
index 0000000..9cd27e4
--- /dev/null
+++ b/frontend/src/components/CreateTopicModal.jsx
@@ -0,0 +1,77 @@
+import React, { useState } from 'react';
+import { Modal, Form, Input, Button, message, Upload } from 'antd';
+import { InboxOutlined } from '@ant-design/icons';
+import { createTopic } from '../api';
+
+const { TextArea } = Input;
+const { Dragger } = Upload;
+
+const CreateTopicModal = ({ visible, onClose, onSuccess }) => {
+ const [form] = Form.useForm();
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (values) => {
+ setLoading(true);
+ try {
+ await createTopic(values);
+ message.success('发布成功');
+ form.resetFields();
+ if (onSuccess) onSuccess();
+ onClose();
+ } catch (error) {
+ console.error(error);
+ message.error('发布失败: ' + (error.response?.data?.detail || '网络错误'));
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default CreateTopicModal;
diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx
index 06d2592..3297921 100644
--- a/frontend/src/components/Layout.jsx
+++ b/frontend/src/components/Layout.jsx
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Layout as AntLayout, Menu, ConfigProvider, theme, Drawer, Button, Avatar, Dropdown } from 'antd';
-import { RobotOutlined, MenuOutlined, AppstoreOutlined, EyeOutlined, SearchOutlined, UserOutlined, LogoutOutlined, WechatOutlined } from '@ant-design/icons';
+import { RobotOutlined, MenuOutlined, AppstoreOutlined, EyeOutlined, SearchOutlined, UserOutlined, LogoutOutlined, WechatOutlined, TeamOutlined } from '@ant-design/icons';
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
import ParticleBackground from './ParticleBackground';
import LoginModal from './LoginModal';
@@ -59,6 +59,11 @@ const Layout = ({ children }) => {
icon: ,
label: 'VC 课程',
},
+ {
+ key: '/forum',
+ icon: ,
+ label: '技术论坛',
+ },
{
key: '/my-orders',
icon: ,
diff --git a/frontend/src/pages/ForumDetail.jsx b/frontend/src/pages/ForumDetail.jsx
new file mode 100644
index 0000000..6703378
--- /dev/null
+++ b/frontend/src/pages/ForumDetail.jsx
@@ -0,0 +1,205 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { Typography, Card, Avatar, Tag, Space, Button, Divider, Input, message, Breadcrumb, Tooltip } from 'antd';
+import { UserOutlined, ClockCircleOutlined, EyeOutlined, CheckCircleFilled, LeftOutlined, LikeOutlined } from '@ant-design/icons';
+import { getTopicDetail, createReply } from '../api';
+import { useAuth } from '../context/AuthContext';
+import LoginModal from '../components/LoginModal';
+
+const { Title, Text, Paragraph } = Typography;
+const { TextArea } = Input;
+
+const ForumDetail = () => {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const { user } = useAuth();
+
+ const [loading, setLoading] = useState(true);
+ const [topic, setTopic] = useState(null);
+ const [replyContent, setReplyContent] = useState('');
+ const [submitting, setSubmitting] = useState(false);
+ const [loginModalVisible, setLoginModalVisible] = useState(false);
+
+ const fetchTopic = async () => {
+ try {
+ const res = await getTopicDetail(id);
+ setTopic(res.data);
+ } catch (error) {
+ console.error(error);
+ message.error('加载失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchTopic();
+ }, [id]);
+
+ const handleSubmitReply = async () => {
+ if (!user) {
+ setLoginModalVisible(true);
+ return;
+ }
+ if (!replyContent.trim()) {
+ message.warning('请输入回复内容');
+ return;
+ }
+
+ setSubmitting(true);
+ try {
+ await createReply({
+ topic: id,
+ content: replyContent
+ });
+ message.success('回复成功');
+ setReplyContent('');
+ fetchTopic(); // Refresh to show new reply
+ } catch (error) {
+ console.error(error);
+ message.error('回复失败');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ if (loading) return
Loading...
;
+ if (!topic) return Topic not found
;
+
+ return (
+
+
}
+ style={{ color: '#fff', marginBottom: 20 }}
+ onClick={() => navigate('/forum')}
+ >
+ 返回列表
+
+
+ {/* Topic Content */}
+
+
+ {topic.is_pinned &&
置顶}
+ {topic.product_info &&
{topic.product_info.name}}
+
{topic.title}
+
+
+
+ } />
+ {topic.author_info?.nickname}
+ {topic.is_verified_owner && (
+
+
+
+ )}
+
+
+
+ {new Date(topic.created_at).toLocaleString()}
+
+
+
+ {topic.view_count} 阅读
+
+
+
+
+
+
+
+ {topic.content}
+
+
+
+ {/* Replies List */}
+
+
+ {topic.replies?.length || 0} 条回复
+
+
+ {topic.replies?.map((reply, index) => (
+
+
+
} />
+
+
+
+ {reply.author_info?.nickname}
+ {new Date(reply.created_at).toLocaleString()}
+
+ #{index + 1}
+
+
{reply.content}
+
+
+
+ ))}
+
+
+ {/* Reply Form */}
+
+ 发表回复
+ {user ? (
+ <>
+
+
+
setLoginModalVisible(false)}
+ onLoginSuccess={() => {}}
+ />
+
+ );
+};
+
+export default ForumDetail;
diff --git a/frontend/src/pages/ForumList.jsx b/frontend/src/pages/ForumList.jsx
new file mode 100644
index 0000000..792f817
--- /dev/null
+++ b/frontend/src/pages/ForumList.jsx
@@ -0,0 +1,190 @@
+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 { useNavigate } from 'react-router-dom';
+import { motion } from 'framer-motion';
+import { getTopics } from '../api';
+import { useAuth } from '../context/AuthContext';
+import CreateTopicModal from '../components/CreateTopicModal';
+import LoginModal from '../components/LoginModal';
+
+const { Title, Text, Paragraph } = Typography;
+
+const ForumList = () => {
+ const navigate = useNavigate();
+ const { user } = useAuth();
+
+ const [loading, setLoading] = useState(true);
+ const [topics, setTopics] = useState([]);
+ const [searchText, setSearchText] = useState('');
+ const [createModalVisible, setCreateModalVisible] = useState(false);
+ const [loginModalVisible, setLoginModalVisible] = useState(false);
+
+ const fetchTopics = async (search = '') => {
+ setLoading(true);
+ try {
+ const params = {};
+ if (search) params.search = search;
+ const res = await getTopics(params);
+ setTopics(res.data.results || res.data); // Support pagination result or list
+ } catch (error) {
+ console.error(error);
+ message.error('获取帖子列表失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchTopics();
+ }, []);
+
+ const handleSearch = (value) => {
+ setSearchText(value);
+ fetchTopics(value);
+ };
+
+ const handleCreateClick = () => {
+ if (!user) {
+ setLoginModalVisible(true);
+ return;
+ }
+ setCreateModalVisible(true);
+ };
+
+ return (
+
+ {/* Hero Section */}
+
+
+
+ Quant Speed Developer Community
+
+
+ 技术交流 · 硬件开发 · 官方支持
+
+
+
+
+ }
+ style={{ borderRadius: 8, background: 'rgba(255,255,255,0.1)', border: '1px solid #333', color: '#fff' }}
+ onChange={(e) => setSearchText(e.target.value)}
+ onPressEnter={(e) => handleSearch(e.target.value)}
+ />
+ }
+ onClick={handleCreateClick}
+ style={{ height: 'auto', borderRadius: 8 }}
+ >
+ 发布新帖
+
+
+
+
+ {/* Content Section */}
+
+
(
+
+ navigate(`/forum/${item.id}`)}
+ >
+
+
+
+ {item.is_pinned && 置顶}
+ {item.is_verified_owner && (
+
+ } color="#00b96b">认证用户
+
+ )}
+
+ {item.title}
+
+
+
+
+ {item.content.replace(/[#*`]/g, '')} {/* Simple markdown strip */}
+
+
+
+
+ } size="small" />
+ {item.author_info?.nickname || '匿名用户'}
+
+ •
+ {new Date(item.created_at).toLocaleDateString()}
+ {item.product_info && (
+ {item.product_info.name}
+ )}
+
+
+
+
+
+
+
{item.replies?.length || 0}
+
+
+
+
{item.view_count || 0}
+
+
+
+
+
+ )}
+ locale={{ emptyText: 暂无帖子,来发布第一个吧!
}}
+ />
+
+
+
setCreateModalVisible(false)}
+ onSuccess={() => fetchTopics(searchText)}
+ />
+
+ setLoginModalVisible(false)}
+ onLoginSuccess={() => {
+ // 登录成功后自动打开发布框
+ setCreateModalVisible(true);
+ }}
+ />
+
+ );
+};
+
+export default ForumList;