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 获取专家列表,目前先做静态展示或从帖子中提取 */}
+
+
-
-
-
{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);
}}
/>