From ef35204bc669857976f839b9e5ab8425070d789f Mon Sep 17 00:00:00 2001 From: zty Date: Tue, 7 Jul 2026 03:35:09 -0400 Subject: [PATCH] feat(web): forum thread detail with like and reply --- .../iapip-web/src/pages/forum/thread.tsx | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 空气质量预测/源码/用户端/iapip-web/src/pages/forum/thread.tsx diff --git a/空气质量预测/源码/用户端/iapip-web/src/pages/forum/thread.tsx b/空气质量预测/源码/用户端/iapip-web/src/pages/forum/thread.tsx new file mode 100644 index 0000000..0804727 --- /dev/null +++ b/空气质量预测/源码/用户端/iapip-web/src/pages/forum/thread.tsx @@ -0,0 +1,120 @@ +import { useEffect, useState, useCallback } from 'react' +import { history, useParams } from '@umijs/max' +import { Button, Input, List, message, Popconfirm, Spin } from 'antd' +import api from '@/services/api' +import type { ForumThread, ForumReply } from '@/services/api/forum' +import { LOC_STOR_KEY } from '@/common/constants' +import PhoneAuthModal from '@/components/phone-auth-modal' + +const hasToken = () => !!localStorage.getItem(LOC_STOR_KEY.AUTH_TOKEN) + +export default function ThreadDetail() { + const params = useParams<{ id: string }>() + const id = Number(params.id) + + const [thread, setThread] = useState(null) + const [replies, setReplies] = useState([]) + const [liked, setLiked] = useState(false) + const [likeCount, setLikeCount] = useState(0) + const [loading, setLoading] = useState(true) + const [replyText, setReplyText] = useState('') + const [authOpen, setAuthOpen] = useState(false) + const [pendingAfterLogin, setPendingAfterLogin] = useState(null) + + const load = useCallback(async () => { + setLoading(true) + try { + const res = await api.forum.getThread(id) + setThread(res.thread) + setReplies(res.replies) + setLikeCount(res.thread.like_count) + if (hasToken()) { + try { const ls = await api.forum.getLikeStatus(id); setLiked(ls.liked); setLikeCount(ls.like_count) } catch { /* ignore */ } + } + } catch { /* 全局提示 */ } finally { setLoading(false) } + }, [id]) + + useEffect(() => { load() }, [load]) + + const doLike = async () => { + if (!hasToken()) { setPendingAfterLogin('like'); setAuthOpen(true); return } + try { + const res = await api.forum.toggleLike(id) + setLiked(res.liked); setLikeCount(res.like_count) + } catch { /* 全局提示 */ } + } + + const doReply = async () => { + if (!replyText.trim()) { message.warning('请输入回帖内容'); return } + if (!hasToken()) { setPendingAfterLogin('reply'); setAuthOpen(true); return } + try { + await api.forum.createReply(id, { content: replyText.trim() }) + setReplyText('') + message.success('回帖成功') + load() + } catch { /* 全局提示 */ } + } + + const onAuthed = () => { + setAuthOpen(false) + const act = pendingAfterLogin + setPendingAfterLogin(null) + load().then(() => { + if (act === 'like') doLike() + else if (act === 'reply') doReply() + }) + } + + const delThread = async () => { + try { await api.forum.deleteThread(id); message.success('已删除'); history.push('/forum') } catch { /* 全局提示 */ } + } + const delReply = async (rid: number) => { + try { await api.forum.deleteReply(rid); message.success('已删除'); load() } catch { /* 全局提示 */ } + } + + if (loading) return
+ if (!thread) return
帖子不存在或已删除。 history.push('/forum')}>返回论坛
+ + return ( +
+ history.push('/forum')} style={{ color: '#888' }}>← 返回论坛 + +
+
{thread.board}
+

{thread.title}

+
{thread.author_name} · {new Date(thread.created_at).toLocaleString()}
+

{thread.content}

+
+ + 💬 {replies.length} + + 删除 + +
+
+ +

全部回帖({replies.length})

+ ( + delReply(r.id)}>删除]}> + #{r.floor} · {r.author_name} · {new Date(r.created_at).toLocaleString()}} + description={{r.content}} + /> + + )} + /> + +
+ setReplyText(e.target.value)} placeholder="写下你的回帖…" maxLength={5000} /> +
+ +
+
+ + { setAuthOpen(false); setPendingAfterLogin(null) }} /> +
+ ) +}