diff --git a/backend/community/serializers.py b/backend/community/serializers.py index b3c6a0c..78d4eba 100644 --- a/backend/community/serializers.py +++ b/backend/community/serializers.py @@ -41,7 +41,7 @@ class TopicSerializer(serializers.ModelSerializer): class Meta: model = Topic fields = [ - 'id', 'title', 'content', 'author', 'author_info', + 'id', 'title', 'category', 'content', 'author', 'author_info', 'related_product', 'product_info', 'related_service', 'service_info', 'related_course', 'course_info', diff --git a/backend/community/views.py b/backend/community/views.py index db6e013..d23380d 100644 --- a/backend/community/views.py +++ b/backend/community/views.py @@ -1,4 +1,5 @@ -from rest_framework import viewsets, status, mixins, parsers +from rest_framework import viewsets, status, mixins, parsers, filters +from django_filters.rest_framework import DjangoFilterBackend from rest_framework.decorators import action from rest_framework.response import Response from rest_framework import serializers @@ -68,6 +69,11 @@ class TopicViewSet(viewsets.ModelViewSet): """ queryset = Topic.objects.all() serializer_class = TopicSerializer + filter_backends = [filters.SearchFilter, filters.OrderingFilter, DjangoFilterBackend] + search_fields = ['title', 'content'] + filterset_fields = ['category', 'is_pinned'] + ordering_fields = ['created_at', 'view_count'] + ordering = ['-is_pinned', '-created_at'] def perform_create(self, serializer): user = get_current_wechat_user(self.request) diff --git a/backend/db.sqlite3 b/backend/db.sqlite3 index ba9ff15..974d747 100644 Binary files a/backend/db.sqlite3 and b/backend/db.sqlite3 differ diff --git a/backend/populate_db.py b/backend/populate_db.py index 10f9386..f0e9863 100644 --- a/backend/populate_db.py +++ b/backend/populate_db.py @@ -7,9 +7,14 @@ django.setup() from shop.models import ESP32Config def populate(): + # 检查数据库是否已有数据,如果有则跳过,避免每次构建都重置 + if ESP32Config.objects.exists(): + print("ESP32Config data already exists, skipping population.") + return + # 清除旧数据,避免重复累积 # 注意:在生产环境中慎用 delete - ESP32Config.objects.all().delete() + # ESP32Config.objects.all().delete() configs = [ { diff --git a/frontend/src/api.js b/frontend/src/api.js index fa0f3c1..3892199 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -60,5 +60,9 @@ export const uploadMedia = (data) => { } }); }; +// 获取明星技术用户 (目前暂无专门接口,通过 /wechat/login 返回的 token 获取当前用户信息,或者通过 filter 获取用户列表如果后端开放) +// 由于没有专门的用户列表接口,我们暂时不实现 getStarUsers API,或者在 ForumList 中模拟或请求特定的 Top 榜单接口。 +// 为了演示,我们假设后端开放一个 user list 接口,或者我们修改 Topic 列表返回 author_info 时前端自行筛选。 +// 最好的方式是后端提供一个 star_users 接口。我们暂时跳过,只在 ForumList 中处理。 export default api; diff --git a/frontend/src/components/CreateTopicModal.jsx b/frontend/src/components/CreateTopicModal.jsx index 9cd27e4..1a8d36d 100644 --- a/frontend/src/components/CreateTopicModal.jsx +++ b/frontend/src/components/CreateTopicModal.jsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; -import { Modal, Form, Input, Button, message, Upload } from 'antd'; +import { Modal, Form, Input, Button, message, Upload, Select } from 'antd'; import { InboxOutlined } from '@ant-design/icons'; import { createTopic } from '../api'; const { TextArea } = Input; -const { Dragger } = Upload; +const { Option } = Select; const CreateTopicModal = ({ visible, onClose, onSuccess }) => { const [form] = Form.useForm(); @@ -39,6 +39,7 @@ const CreateTopicModal = ({ visible, onClose, onSuccess }) => { layout="vertical" onFinish={handleSubmit} style={{ marginTop: 20 }} + initialValues={{ category: 'discussion' }} > { + + + + { 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 ; + case 'share': return ; + case 'notice': return ; + default: return ; + } + }; + + 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 (
{/* Hero Section */} @@ -95,91 +124,153 @@ const ForumList = () => {
{/* Content Section */} -
- ( - - navigate(`/forum/${item.id}`)} - > -
-
-
- {item.is_pinned && 置顶} - {item.is_verified_owner && ( - - } color="#00b96b">认证用户 - - )} - - {item.title} - -
- - - {item.content.replace(/[#*`]/g, '')} {/* Simple markdown strip */} - +
+ + + + ( + + navigate(`/forum/${item.id}`)} + > +
+
+
+ {item.is_pinned && }>置顶} + + {getCategoryLabel(item.category)} + + {item.is_verified_owner && ( + + } color="#00b96b" style={{ margin: 0 }}>认证用户 + + )} + + {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} - )} - -
+ + + } size="small" /> + + {item.author_info?.nickname || '匿名用户'} + + {item.author_info?.is_star && ( + + + + )} + + + {new Date(item.created_at).toLocaleDateString()} + {item.product_info && ( + {item.product_info.name} + )} + +
-
-
- -
{item.replies?.length || 0}
+
+
+ +
{item.replies?.length || 0}
+
+
+ +
{item.view_count || 0}
+
+
+
+ + + )} + locale={{ emptyText:
暂无帖子,来发布第一个吧!
}} + /> + + + + 技术专家榜} + 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)' }} + > +
+ {/* 这里可以通过 API 获取专家列表,目前先做静态展示或从帖子中提取 */} +
+ +
+
QuantMaster
+
官方技术支持
+
+
+
+ +
+
AI_Wizard
+
社区贡献者
+
+
-
- -
{item.view_count || 0}
-
-
-
- - - )} - locale={{ emptyText:
暂无帖子,来发布第一个吧!
}} - /> + + + 社区公告} + 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)' }} + > + {item}} + /> + + +
setCreateModalVisible(false)} - onSuccess={() => fetchTopics(searchText)} + onSuccess={() => fetchTopics(searchText, category)} /> setLoginModalVisible(false)} onLoginSuccess={() => { - // 登录成功后自动打开发布框 setCreateModalVisible(true); }} />