From f00cc9a28e89e142d7f57927b8baf2510996fbf2 Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Thu, 12 Feb 2026 14:20:03 +0800 Subject: [PATCH] new --- .../migrations/0005_topic_category.py | 18 ++ backend/community/models.py | 9 + backend/db.sqlite3 | Bin 393216 -> 397312 bytes .../shop/__pycache__/models.cpython-312.pyc | Bin 27346 -> 27594 bytes .../__pycache__/serializers.cpython-312.pyc | Bin 15100 -> 15125 bytes ...027_wechatuser_is_star_wechatuser_title.py | 23 ++ backend/shop/models.py | 5 + backend/shop/serializers.py | 4 +- frontend/src/App.jsx | 4 + frontend/src/api.js | 14 ++ frontend/src/components/CreateTopicModal.jsx | 77 +++++++ frontend/src/components/Layout.jsx | 7 +- frontend/src/pages/ForumDetail.jsx | 205 ++++++++++++++++++ frontend/src/pages/ForumList.jsx | 190 ++++++++++++++++ 14 files changed, 553 insertions(+), 3 deletions(-) create mode 100644 backend/community/migrations/0005_topic_category.py create mode 100644 backend/shop/migrations/0027_wechatuser_is_star_wechatuser_title.py create mode 100644 frontend/src/components/CreateTopicModal.jsx create mode 100644 frontend/src/pages/ForumDetail.jsx create mode 100644 frontend/src/pages/ForumList.jsx 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 7c09cf05a082ff2c547d5100f6c2e69b1b3d5775..ba9ff15a30fa8ed25e3f477f1c36c156cd235ffb 100644 GIT binary patch delta 957 zcmZuv-)~cO6#xF@w!6Lewp|8WG}~p@vazo4-R*wZjfFPL*aHYY%;jmE(06^jy2msJs=nlg>U&cRBgn*MCF@429loqVR`J&)W5cm_E zugrKG;rv0Jf~L1(*`t|h#`FQ>L~`P2B6mESoHbHeBb!UiKA*~^a+67&>HQ(5hkA6C z^$+$$27B3_0lhyGi465n&=!tOWTvK$r&GB(##o<`%S@*xjEO`p`EF)*?mx|9y_*g6 z4n!($|6daJ%BPEh+DcE$%W}Hni@q&y_Xly%_3zO7z{XHzWlsV^eSv+*;3`9`$}sWT zWEwsFe$gT>Bwqr`EAl00#96t?WXKPKKx#9wOOm8m3}~(Z|1pggciuabzqkCwZ`X@= zKd+h${!dvtgBCr{mz8g;)i@TSwrb(umBJPO`Xl09BQ+d<(E~Lg9rTVxjM#R){t6K9xMQe<_kF%*&U_g6$8Ghl8+T4aNxzxO;(!o&Z^sKJq;F2;x{n zQ7E_{xJT(N^h78)wuE0U&UnY|lNOmr;;C^nAnXVR;TeNRRieQ@zEdO3+*%^k&R!(P zkl3xZZ=IgU&Y>N4yh>}3?_VPKdF>c>bK@JjPUNqFj|Yq7D}M7jU9+cePz!TygHy*K5REA-A6zo1Uzi^a4V1Y5o6e7n^8AEQl&oeRExRyAATRFcx#1fcEm&q z;;&w#oh=TrX98_k-^pYq)x$@|)x)nHI>g^g!dAN544J$6w=3i&9@(Jn9G6JPX4J^f zZ%_t1`TG_b<)4;F8}GV7$8gB(*sMMP$M~BYbQkX`krB9yr%PlQ_Vdq5WZXFgayk1F zL`{zw7WmvG9)ulyCP*6j%}G4$(9rA4|9p-5R%mF?Y delta 841 zcma)4OK1~O6rDHkzD%Z*nf%mLs_i6GNo`c7Xhf{BDIyBGP;j9XOzH>x+v-AE6*E?) zD@_}kYorvdkbo}K20GYA8br{A3%eF=5JB-9bf<#8wmaRpIE!=7;c__lrf$}zQnl$i zZ$%JWEf8OhUGY|(F#qU`&rQnxmok@R{p zT=d3IX^ucMjryC5Bdad&$ z?Sg^#Hqt>Zkbi`IZFDf-C6)(YKmn4XrwEl_4dY2F#Z{4SUds{XSi@I;a*tKPQ-Cr} zE|Hm6P`A0RD$|M~ZFW3ytY)Lk3u$hS4zw@Cs19H3cNus_T+4aBWm2G}gGtfhd(&`1%%nVRxH$&}+X%vO3d(EplB-+s6QAJ#!u!9n zktcK6u0;MP+biVR`mk{Xh7@b1<(C5cBgEQ+J94lfTVfG>KEc8$mzW<9mYC`ENr9c* z5S=sNgAxnjxk=W5-%4!l*nQAv+fIyD^?aP^q^3wJzZ=HNFn@$5mGuZ7u}^Rx(5BV5 zYNM-5d8~Nl6V9hj-I1hzht4YOrr5_{rbA?w%W9Oc=NMg2)?jx8{P^@3jVYJ*S-KR6 zL`eTxtlG@LlT*xzP5q(|H#UP}@3q7a1Va<7$;l5TgaaK>3?3%6%87$#5{c7?PR1+) z(R&aj)GbMV+UVC~7$^f;$g7%RdEGITE{5F}bs_w6< jWV|!MLS#3lbFh?GH^=?xm}DlNPs3e1a|6;eUuN+?KIr)x diff --git a/backend/shop/__pycache__/models.cpython-312.pyc b/backend/shop/__pycache__/models.cpython-312.pyc index 50cf5ab16f6e1737eb90a57da304ca79605f47d5..183b20640c1c620c834af1919cc8ece9557dd82a 100644 GIT binary patch delta 5678 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 L+kO?44wL;4_;Pn# diff --git a/backend/shop/__pycache__/serializers.cpython-312.pyc b/backend/shop/__pycache__/serializers.cpython-312.pyc index f41f618df36f7ece3372de7da3e84b76f984e49d..a0437bfed9bd83019011a1876871f2d89ac5f724 100644 GIT binary patch delta 1560 zcmaKsUrgIo6vum@|4SWz)&lM5C@qvyER#4Ch9H`C6U8A_2@LWJV^x~KV86nMxPcOa z%xy3@HqJzYG3pDtg#2LCX-rH^e9#!351PhgF~<0`7?ve!^uhbR#j)Vx@8Q#P&OPUz zdwak4{ymmFW&hG{x9Y@aIe8)e{HlH0#OFh1p0XJDO_Q6foHDig(x!MqNsRWXX~SrI zbTAgws}513HUI~JMh;t@yw`kRLB4QjbU);}<_zh)$vI=%eUJytkL2GYc__-ygz}O74nXg%^shK}B7ju#AQ)-v( zZ`dm72s?AC=pmWfhSWCh&hb#2Otoc4`InreL2VQx{Ib1|p4NW0Pm!%t&JXm;$_|(v zJiDurI%MT_?@{f6Ts={r)|jVcg}X#TFk9^wKWTlufB1yjgOVzNM3z8lsp>PK zv^HAw8qqXcTyARRa@X;F)2NsQ<^<9fMd=^xOC%H}VHAETzLzqKnHT|-hUKD161Rl=RjA3YuU3-oiU~a8vfjuQpSzAOPqK*r)V%m3GH&;rc zG{shrS1hyIbA(>kPJ5oADcR`~rV9Ww0G1szt7l=K6G$6I)OdePH6idSa2~h-VD&+x z8iKt5Y)>e6FIRroEtf=N>M8b>(HL?J82Ijq7DK7QN;*PmhY#NyjI?9Odw;C z|19?y--nrz!7|KcuB#ZJW!W}W@gY6R&js2=(~UqqUFOZey7&Kjh#4lrTpDojkCh(L z?61lyhioRr9YX`T_e{eT_$wR@9x}khIcsadF*4$i#lKe*3eq@tg_ewG5SZl6(6i!v z4_8NXKSH1wNC7YKN_7`$n&I9_GOi(VhR=ov=qld~H`6(;tqE*}HKpc{EHm~XG_b}> ztv@0q6nf;K?1LQeVLcE=15Gm6g%hTZrxummRTf zg<0-U*p6<~I`N2YUp4h2zg<^I^IEEIoI0hf)-jj7S2eJ%03QRN0NZ!%bGTmsQ1#uF zw~CQfo_%nGR=KJE#di7fwIM1`P`(uMdP6*Ud&39+0NP7(3IG5A delta 1504 zcmZvce@t6d6vul?`&!yQ>;kk<3beKqo@HwwlMFhjI%S*7I+Sc~mQkD1KuHJWwxDif z6@P5(2Rb>ZBswyqCX;PR@rj%IkI}?`8i)SnteMedf0*naG&dah%!+$?-Jd+G9$uAl#1KNR-LG9Dh^EyS3ZyTIo#H)tK4Xl#y zVjcLK*@i)5OO>53U)Ef=I;Qo74a3vYb*Jn6rf+LnFl&6oZV|!1v?co1-!&ZUeoAh^ z8}4fLwy^^okI}lDu$@pv*g@DS8XPn=!8SZ@>V`-08&j3kNuf@>Wom{_DuNg6Wf(H& zAk4RS!EOv#9)KYJB9}MpqOAlkf6}W5>7s*l6%VpY)VOH*)lkr9l9^Nj=j9kYsos&Z zVBSXsD4F$9L8*t#9z1v7R_Nh*88vLx0X(DjZis5j_w%4q!Vd7if-V^!9bpm7HdI0% z{#bq+_Tx&041cb%KJ-&VH=-c+$}FJwiJ zwcN#xUOyk{Fq1(dw&{ zJ|MF=&jm6UaJO#=7KGYXe3#*2-nHj$$D55FxQxc8e`s{jiph>k{{iy0b*KOU 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 ( + +
+ + + + + +