From 5a7043fa1ce570274f5aabd46776e0922aecdaac Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Thu, 12 Feb 2026 16:54:16 +0800 Subject: [PATCH] forum --- .../__pycache__/serializers.cpython-312.pyc | Bin 15125 -> 15141 bytes backend/shop/__pycache__/urls.cpython-312.pyc | Bin 2018 -> 2038 bytes .../shop/__pycache__/views.cpython-312.pyc | Bin 63156 -> 63677 bytes backend/shop/serializers.py | 4 +- backend/shop/urls.py | 2 +- backend/shop/views.py | 11 ++ frontend/src/api.js | 10 +- frontend/src/context/AuthContext.jsx | 52 +++++++-- frontend/src/pages/ForumDetail.jsx | 19 +++- miniprogram/README.md | 5 + miniprogram/config/index.js | 9 +- miniprogram/src/pages/forum/index.scss | 105 +++++++++++++++++- miniprogram/src/pages/forum/index.tsx | 86 ++++++++++++-- miniprogram/src/pages/services/index.scss | 33 ++++++ miniprogram/src/pages/services/index.tsx | 10 ++ .../src/subpackages/forum/create/index.tsx | 58 ++++++++-- .../src/subpackages/forum/detail/index.tsx | 31 +++++- 17 files changed, 384 insertions(+), 51 deletions(-) diff --git a/backend/shop/__pycache__/serializers.cpython-312.pyc b/backend/shop/__pycache__/serializers.cpython-312.pyc index a0437bfed9bd83019011a1876871f2d89ac5f724..e314e882dc8151ebfba9c3fd89728636b9c870eb 100644 GIT binary patch delta 1534 zcmZvcZA_b06vulCJd{E=6lkHm*78>^up|AlQl%WJYu~^dpT^oNFL6~pP(uIWd0)2442sp zX+JmF`!i>-|1IDIU$U3dyuM+tAx|706;n5um&=4D5C{(UMWeyc@Ngh-k;J>w8zqTW z;l%!!0@oECqL}P`fuAlukiJ(s+r$x`D?Y8Mc^_j(i9?>@nUYRAt8+;=#Z{yCkX;45 z0pLt7lXx5UDTzc{M1+GOkq*a6U=BD9;CL=g)WCiRcvw(fO&wH|fiirKB~Oc_$zoqf z#CV|WR{a9<-hYt%;M#)WTa?&()g?HWIPUP%l77=+A^AT~Rn>E;vw(uUYPNEx(@7t4 z)VVIF=PEBJzwRxsAq|5TBbco=Ce6tjo+>Y0aAj#L2ngP&Xwk-DE^u~biS{wf4>~<9KmAq7|P+gQL zUqe(~IN3=ZWrc@a`)EbZu|VH&9VQKf6cgN5kEt*`hi>?;7i1STb3bQwi879QTL?V!)}q9>+FTZdHl>h($ delta 1520 zcmZvcZA_b06vum@JkZiIHr`4Z3|64jRuQw1tsoT*>_s*v%q=MFNi7Z3@KSDhap+({ zGMN)_9GNp_AyF|-#Ed-Rrue}n8b4@^e$tdEU;MI|nW#%N`o;UdT{c2|lKy(`IrpA( z?{ogoT^ybtwtsE6TTSw@3`|G5R_yPmYF3dW$NXjL@f~Jm3cnnS8lZO0mdgjysH0shzOqX2U7^}GyjPQOY==WKP<%mLed^f3Q!&ni2NnLw%h z#cUFjroWPsQ|hpfy2LgPs}yUtn^en-_D(uxSTn{c{REa`ndm?&tp#QaFK?@*7P(^3 zIGJfC3L2s9dQw)K40@8qQ}Qovj)eOAL_5!xGKsaQ)7w zX-L+7j~fb~NUfL7Mlr&d3uiRd>2a~r`NdSBDq=|E| z&r8Hpq9PIsi&QvX23`TC0c_8s2`}tf;9f#?G__Gp28!{VN$eJjMhjcRVwgvYf7x>Z z>)yD#`0laA9DFRX`Ka@7&hueXxA;ZZs_fpr z(o$+SPL_H}L!(76daFgHIqK!<(p{YCv1uP5GQjI)0WAje0vDGTY45^Jv~~gJ0(X>m z(}I%RM)@*5Cd(eZQ}VXkMIW-GVs%p$it^2hz19J&-7Rs110_58^Bi-oP*s#F!-%RA zCn_yryu_oP{j?+-nQyFnhDbvv<;yzm_0R~P@y=(Yar zogy5Ii3#rawHAJYdHKL~;FQEMPm-v>z|R}LCNd1){bQtk1kY){`am~*%K4Q6I?rvD z?mKSdY~|lcHF$qg7MLmP4F(4zAwAL?>MMffR8xF^N=>6iGBOsZ`MvX`q-V)M}t5j43jy zf+@0S3guG8Q{>Ua6jC`;6w$<#Cbux@Gb?MVY~IGSg^}kLS9xl3Mq){6aca@#EEX*$ hE?uw(85oKkCeLGA&AuV0f&U7F^JE|PNi5nxDFA?II+p+d delta 271 zcmeyy|A?RWG%qg~0}!N^^k$x!$ScX%wNZU0WBqC-kRSsCBLfpdDsu{7DsKw^8iCc! z5K*w0U@BjV5Q-RMif}4_iU^vRXsSSp7@C-Psz{0inwVs&Xo?h6%oS)4&=STJ=~TfK z88n5msp2VeXkzlIoGA)uVv3VHnDm*IG?h2+V%oyExr#-LiAx9UCkBRMo5}0gRx@8= O(3zaTK8ZyOC=38Y95Sc? diff --git a/backend/shop/__pycache__/views.cpython-312.pyc b/backend/shop/__pycache__/views.cpython-312.pyc index 114fcab313dfecefb7a5ca8f8494d44cb8a73ec6..97bf4b11eb420d183a8773291df969d1319abe52 100644 GIT binary patch delta 902 zcmYjPZAep57(VYkyLVr@*0x(4tXx^)ipmHz)QA-PQ5i*FB7{V{+Jao2ZA3Zf(`c1R zy;4Cz^`|Tom7@tkUsP5UlxQg$CX{Am(6agyMCVXJFWiUcc|Q&h?|a)n)7vTPc$S`S zBO=Z;)P*y~+8p&)>=q%Z_$Cq*BbN#>`7yvvSCa~!x~5wa=7c?A+dd2L&vsMq1(jdF z7K03xwE`imOiVFDumLR`t*AD3vqeG#-MFM_yHZB*V$&hG#WQb2A>eS;S0$_ZUEY3| zuixW6TY9QAW!H3Vf@o>xFGRzZ=EXiI>2g|23TT&aYDq5b$_B}E)sh1Kv-uUkSKi%% zfc`qGVxm0Kc3i>xC2oAir(kCLe5wkUyC%DFO}h!qxS{=?)%?#U`a1lYn>7l*xVBcu zH64Z0U$TQ~)I>-$#^L62(Egaj<()IrCRwC6i}YsUk|ZKgew{4X$moHAtgksAY{H_X_7|~5wiZ*lfK6mv}40inR0~SqFz6qcoKjF zPI{~3>SB93jr|=WL|n5q?%%hL1l71#JTrE(bEN*v$ZZ@+)_rJb96j3k{_f?`11;=; zD6*~co*@axgbbS4X36ER!VXC$S0}RhGI2ygw#?QFh>8`EfC#o!kO5_fvDdT7DBiXu zwjpXT=f<$Hdz}$mu)`=)MRSqZl?Ml(x*YwwuTRhF*0UNtJ^BKqy_tCDrIn8ll>+FP zKkSE-$XKh;}aTYZy!l5V&;P;UkFaxKK7QuR6KAHe<;LbNXj(^CU?-Jci zmk^XNAiXY=Al)&i%t^r*hz<$os=gsN%=6oYUq!=#z GQTrS5xEq21 delta 588 zcmXw#T}YE*6vv>?t& zGE2`zU3gI>l1wvsre3W^!omteMk>vyB(fHYFAMV`qGv?s;_&}-{s(^NBn_HaqSn_I zi;3`Bf9rPW(bo~{oqjy}LlrJiY=5wied!+%BqdfHGhvO*NnO1nOATCwO``r32+>=4 zdoPE=&26nwy@s0#4Nt`{Lf7~Gm`PIJ#WT=xRq0YjCXa-K+LWravMGqlSs zk+h^XRMWI|V?z~9Z-auak{YVn<|pp}47N6mfb&rmma&cVOplxpuzsB#OA>AjBq}92 zXzP`+Cy^3mj)Z4NJ+h`h=J(*q4kt#QdvSQQ6f>jkvLF%?zQT@-)dAie_n9q9fki2> zC@eXiDT3pyJ5{AXoF!kL79h>ES2jVOF06%cD!Z4pyy>CvnXSGZqwt#zWo9T?aWZ>= zP0UInY-N+#O8|Xr=LZi!7e3S*v1_8jM=wH99#rDA@MS`RMK?+OpCo6@*VGCViXWR; z=2HN`j=lvC9Ax_!Iwj33@ZXwR+9P_nwK)_F>m@k9=z~)Hvse#}tnOP3z=F@d*UEOD z)XNZZ9(j+%Y0u%7+%eXf%L!1 api.post('/course-enrollments/', data); export const sendSms = (data) => api.post('/auth/send-sms/', data); export const queryMyOrders = (data) => api.post('/orders/my_orders/', data); export const phoneLogin = (data) => api.post('/auth/phone-login/', data); -export const getUserInfo = () => { - // 如果没有获取用户信息的接口,可以暂时从本地解析或依赖 update_user_info 的返回 - // 但后端有 /wechat/update/ 可以返回用户信息,或者我们可以加一个 /auth/me/ - // 目前 phone_login 返回了用户信息,前端可以保存。 - // 如果需要刷新,可以复用 update_user_info(虽然名字叫update,但传空通常返回当前信息,需确认后端逻辑) - // 查看后端逻辑:update_user_info 是 patch 更新,如果 data 为空,update 不会执行但会返回 serializer.data - return api.post('/wechat/update/', {}); -}; - +export const getUserInfo = () => api.get('/users/me/'); export const updateUserInfo = (data) => api.post('/wechat/update/', data); export const uploadUserAvatar = (data) => { // 使用 axios 直接请求外部接口,避免 base URL 和拦截器干扰 diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index 973540c..7180a6a 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -1,5 +1,7 @@ import React, { createContext, useState, useEffect, useContext } from 'react'; +import { getUserInfo } from '../api'; + const AuthContext = createContext(null); export const AuthProvider = ({ children }) => { @@ -7,16 +9,46 @@ export const AuthProvider = ({ children }) => { const [loading, setLoading] = useState(true); useEffect(() => { - const storedUser = localStorage.getItem('user'); - if (storedUser) { - try { - setUser(JSON.parse(storedUser)); - } catch (e) { - console.error("Failed to parse user from storage", e); - localStorage.removeItem('user'); - } - } - setLoading(false); + const initAuth = async () => { + const storedToken = localStorage.getItem('token'); + const storedUser = localStorage.getItem('user'); + + if (storedToken) { + try { + // 1. 优先尝试从本地获取 + if (storedUser) { + try { + const parsedUser = JSON.parse(storedUser); + // 如果本地数据包含 ID,直接使用 + if (parsedUser.id) { + setUser(parsedUser); + } else { + // 如果没有 ID,标记为需要刷新 + throw new Error("Missing ID in stored user"); + } + } catch (e) { + // 解析失败或数据不完整,继续从服务器获取 + } + } + + // 2. 总是尝试从服务器获取最新信息(或作为兜底) + // 这样可以确保 ID 存在,且信息是最新的 + const res = await getUserInfo(); + if (res.data) { + setUser(res.data); + localStorage.setItem('user', JSON.stringify(res.data)); + } + } catch (error) { + console.error("Failed to fetch user info:", error); + // 如果 token 失效,可能需要登出? + // 暂时不强制登出,只清除无效的本地 user + if (!user) localStorage.removeItem('user'); + } + } + setLoading(false); + }; + + initAuth(); }, []); const login = (userData) => { diff --git a/frontend/src/pages/ForumDetail.jsx b/frontend/src/pages/ForumDetail.jsx index 3a108e3..cce658d 100644 --- a/frontend/src/pages/ForumDetail.jsx +++ b/frontend/src/pages/ForumDetail.jsx @@ -167,7 +167,14 @@ const ForumDetail = () => { 返回列表 - {user && topic.author === user.id && ( + {/* Debug Info: Remove in production */} + {/*
+ User ID: {user?.id} ({typeof user?.id})
+ Topic Author: {topic.author} ({typeof topic.author})
+ Match: {String(topic.author) === String(user?.id) ? 'Yes' : 'No'} +
*/} + + {user && String(topic.author) === String(user.id) && ( diff --git a/miniprogram/README.md b/miniprogram/README.md index a80ba37..952d911 100644 --- a/miniprogram/README.md +++ b/miniprogram/README.md @@ -20,6 +20,11 @@ Taro + React + TypeScript 微信小程序项目,对接 Django 后端,支持 ## 快速开始 +小程序id + +wxdf2ca73e6c0929f0 + + ### 1. 环境准备 确保已安装 Node.js (>=16) 和 npm。 diff --git a/miniprogram/config/index.js b/miniprogram/config/index.js index 2d4ff5e..a2d29ea 100644 --- a/miniprogram/config/index.js +++ b/miniprogram/config/index.js @@ -20,9 +20,14 @@ const config = { } }, framework: 'react', - compiler: 'webpack5', + compiler: { + type: 'webpack5', + prebundle: { + enable: false + } + }, cache: { - enable: true // Enable cache for better build performance + enable: false // Disable cache to fix prebundle error }, mini: { postcss: { diff --git a/miniprogram/src/pages/forum/index.scss b/miniprogram/src/pages/forum/index.scss index 8465aaa..66937e2 100644 --- a/miniprogram/src/pages/forum/index.scss +++ b/miniprogram/src/pages/forum/index.scss @@ -65,20 +65,123 @@ } } + .section-container { + margin: 0 10px 15px; + background: #141414; /* Darker card background */ + border-radius: 12px; + padding: 16px; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + + .section-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + + .section-title { + font-size: 16px; + font-weight: bold; + color: #fff; + } + } + + .announcement-swiper { + height: 40px; + + .announcement-item { + height: 40px; + display: flex; + align-items: center; + background: rgba(255, 255, 255, 0.05); + padding: 0 12px; + border-radius: 6px; + + .item-text { + font-size: 14px; + color: #ccc; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + } + + .star-users-scroll { + white-space: nowrap; + width: 100%; + + .star-user-card { + display: inline-flex; + flex-direction: column; + align-items: center; + margin-right: 20px; + width: 80px; + background: rgba(255, 255, 255, 0.05); + padding: 12px 8px; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.05); + + .user-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + border: 2px solid #ffd700; + margin-bottom: 8px; + } + + .user-name { + font-size: 12px; + font-weight: bold; + color: #fff; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 2px; + } + + .user-title { + font-size: 10px; + color: #888; + } + } + } + } + .tabs-wrapper { background-color: #000; - + margin-bottom: 10px; + + /* Override Taro UI default white background */ + .at-tabs { + background-color: transparent; + height: auto; + } + + .at-tabs__header { + background-color: transparent; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + text-align: left; + } + .at-tabs__item { color: #888; + font-size: 14px; + padding: 12px 24px; &--active { color: #00b96b; font-weight: bold; + font-size: 16px; } } .at-tabs__item-underline { background-color: #00b96b; + bottom: 0; } } diff --git a/miniprogram/src/pages/forum/index.tsx b/miniprogram/src/pages/forum/index.tsx index e529459..057f27d 100644 --- a/miniprogram/src/pages/forum/index.tsx +++ b/miniprogram/src/pages/forum/index.tsx @@ -1,13 +1,14 @@ import React, { useState, useEffect } from 'react' import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro' -import { View, Text, Image, Button } from '@tarojs/components' -import { AtSearchBar, AtTabs, AtIcon, AtActivityIndicator, AtFab } from 'taro-ui' -import { getTopics } from '../../api' -import { useLogin } from '../../utils/hooks' // Assuming a hook or just use Taro.getStorageSync +import { View, Text, Image, Swiper, SwiperItem, ScrollView } from '@tarojs/components' +import { AtSearchBar, AtTabs, AtIcon, AtActivityIndicator } from 'taro-ui' +import { getTopics, getAnnouncements, getStarUsers } from '../../api' import './index.scss' const ForumList = () => { const [topics, setTopics] = useState([]) + const [announcements, setAnnouncements] = useState([]) + const [starUsers, setStarUsers] = useState([]) const [loading, setLoading] = useState(false) const [hasMore, setHasMore] = useState(true) const [page, setPage] = useState(1) @@ -15,13 +16,26 @@ const ForumList = () => { const [currentTab, setCurrentTab] = useState(0) const categories = [ - { title: '全部', key: 'all' }, - { title: '讨论', key: 'discussion' }, - { title: '求助', key: 'help' }, - { title: '分享', key: 'share' }, - { title: '公告', key: 'notice' }, + { title: '全部话题', key: 'all' }, + { title: '技术讨论', key: 'discussion' }, + { title: '求助问答', key: 'help' }, + { title: '经验分享', key: 'share' }, + { title: '官方公告', key: 'notice' }, ] + const fetchExtraData = async () => { + try { + const [announceRes, starRes] = await Promise.all([ + getAnnouncements(), + getStarUsers() + ]) + setAnnouncements(announceRes.results || announceRes.data || []) + setStarUsers(starRes.data || []) + } catch (err) { + console.error('Fetch extra data failed', err) + } + } + const fetchList = async (reset = false) => { if (loading) return if (!reset && !hasMore) return @@ -62,10 +76,12 @@ const ForumList = () => { useEffect(() => { fetchList(true) + fetchExtraData() }, [currentTab]) usePullDownRefresh(() => { fetchList(true) + fetchExtraData() }) useReachBottom(() => { @@ -127,7 +143,7 @@ const ForumList = () => { - Quant Speed Community + Quant Speed Developer Community 技术交流 · 硬件开发 · 官方支持 @@ -137,11 +153,59 @@ const ForumList = () => { onChange={handleSearch} onActionClick={onSearchConfirm} onConfirm={onSearchConfirm} - placeholder='搜索话题...' + placeholder='搜索感兴趣的话题...' /> + + + 发布新帖 + + {/* Announcements Section */} + {announcements.length > 0 && ( + + + + 社区公告 + + + {announcements.map(item => ( + + + {item.title} + + + ))} + + + )} + + {/* Star Users Section */} + {starUsers.length > 0 && ( + + + + 技术专家榜 + + + {starUsers.map(user => ( + + + {user.nickname} + {user.title || '专家'} + + ))} + + + )} + AI 全栈解决方案 从数据处理到模型部署,我们为您提供一站式 AI 基础设施服务。 + + + + diff --git a/miniprogram/src/subpackages/forum/create/index.tsx b/miniprogram/src/subpackages/forum/create/index.tsx index ab83383..62a0175 100644 --- a/miniprogram/src/subpackages/forum/create/index.tsx +++ b/miniprogram/src/subpackages/forum/create/index.tsx @@ -1,10 +1,13 @@ -import React, { useState } from 'react' -import Taro from '@tarojs/taro' +import React, { useState, useEffect } from 'react' +import Taro, { useRouter } from '@tarojs/taro' import { View, Text, Input, Textarea, Button, Picker } from '@tarojs/components' -import { createTopic, uploadMedia } from '../../../api' +import { createTopic, updateTopic, getTopicDetail, uploadMedia } from '../../../api' import './create.scss' const CreateTopic = () => { + const router = useRouter() + const { id } = router.params + const [title, setTitle] = useState('') const [content, setContent] = useState('') const [categoryIndex, setCategoryIndex] = useState(0) @@ -16,6 +19,30 @@ const CreateTopic = () => { { key: 'share', label: '经验分享' }, ] + useEffect(() => { + if (id) { + const fetchDetail = async () => { + Taro.showLoading({ title: '加载中...' }) + try { + const res = await getTopicDetail(Number(id)) + const topic = res.data + setTitle(topic.title) + setContent(topic.content) + const idx = categories.findIndex(c => c.key === topic.category) + if (idx !== -1) setCategoryIndex(idx) + + Taro.setNavigationBarTitle({ title: '编辑话题' }) + } catch (error) { + console.error(error) + Taro.showToast({ title: '加载失败', icon: 'none' }) + } finally { + Taro.hideLoading() + } + } + fetchDetail() + } + }, [id]) + const handleCategoryChange = (e) => { setCategoryIndex(e.detail.value) } @@ -68,19 +95,28 @@ const CreateTopic = () => { setLoading(true) try { - const res = await createTopic({ - title, - content, - category: categories[categoryIndex].key - }) + if (id) { + await updateTopic(Number(id), { + title, + content, + category: categories[categoryIndex].key + }) + Taro.showToast({ title: '更新成功', icon: 'success' }) + } else { + await createTopic({ + title, + content, + category: categories[categoryIndex].key + }) + Taro.showToast({ title: '发布成功', icon: 'success' }) + } - Taro.showToast({ title: '发布成功', icon: 'success' }) setTimeout(() => { Taro.navigateBack() }, 1500) } catch (error) { console.error(error) - Taro.showToast({ title: '发布失败', icon: 'none' }) + Taro.showToast({ title: id ? '更新失败' : '发布失败', icon: 'none' }) } finally { setLoading(false) } @@ -127,7 +163,7 @@ const CreateTopic = () => { onClick={handleSubmit} disabled={loading} > - {loading ? '发布中...' : '发布话题'} + {loading ? (id ? '更新中...' : '发布中...') : (id ? '更新话题' : '发布话题')} ) diff --git a/miniprogram/src/subpackages/forum/detail/index.tsx b/miniprogram/src/subpackages/forum/detail/index.tsx index 253ea6b..a93d11a 100644 --- a/miniprogram/src/subpackages/forum/detail/index.tsx +++ b/miniprogram/src/subpackages/forum/detail/index.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react' -import Taro, { useRouter, useShareAppMessage } from '@tarojs/taro' -import { View, Text, Image, Video, RichText, Input, Button } from '@tarojs/components' +import Taro, { useRouter, useShareAppMessage, useDidShow } from '@tarojs/taro' +import { View, Text, Image, Video, RichText, Input, ScrollView } from '@tarojs/components' import { AtActivityIndicator, AtIcon } from 'taro-ui' import { getTopicDetail, createReply, uploadMedia } from '../../../api' import { marked } from 'marked' @@ -15,6 +15,7 @@ const ForumDetail = () => { const [replyContent, setReplyContent] = useState('') const [sending, setSending] = useState(false) const [htmlContent, setHtmlContent] = useState('') + const [userInfo, setUserInfo] = useState(null) const fetchDetail = async () => { try { @@ -37,11 +38,20 @@ const ForumDetail = () => { } useEffect(() => { + const info = Taro.getStorageSync('userInfo') + if (info) setUserInfo(info) + if (id) { fetchDetail() } }, [id]) + useDidShow(() => { + if (id && !loading) { + fetchDetail() + } + }) + useShareAppMessage(() => { return { title: topic?.title || '技术社区', @@ -53,6 +63,12 @@ const ForumDetail = () => { setReplyContent(e.detail.value) } + const handleEdit = () => { + Taro.navigateTo({ + url: `/subpackages/forum/create/index?id=${id}` + }) + } + const handleUpload = async () => { try { const res = await Taro.chooseMedia({ @@ -125,6 +141,8 @@ const ForumDetail = () => { return ( + + {topic.is_pinned && 置顶} @@ -140,6 +158,13 @@ const ForumDetail = () => { {new Date(topic.created_at).toLocaleDateString()} {topic.view_count} 阅读 + + {userInfo && topic.author === userInfo.id && ( + + + 编辑 + + )} @@ -177,6 +202,8 @@ const ForumDetail = () => { ))} + +