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 ( + +
+ + + + + +