feat(web): forum thread detail with like and reply

This commit is contained in:
zty 2026-07-07 03:35:09 -04:00
parent fd84b0b0ff
commit ef35204bc6
1 changed files with 120 additions and 0 deletions

View File

@ -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<ForumThread | null>(null)
const [replies, setReplies] = useState<ForumReply[]>([])
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 | 'like' | 'reply'>(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 <div style={{ textAlign: 'center', padding: 80 }}><Spin /></div>
if (!thread) return <div style={{ textAlign: 'center', padding: 80 }}><a onClick={() => history.push('/forum')}></a></div>
return (
<div style={{ maxWidth: 900, margin: '0 auto', padding: '24px 16px' }}>
<a onClick={() => history.push('/forum')} style={{ color: '#888' }}> </a>
<div style={{ marginTop: 16, padding: 20, border: '1px solid #eee', borderRadius: 8 }}>
<div style={{ color: '#B43533', fontSize: 12 }}>{thread.board}</div>
<h1 style={{ fontSize: 22, margin: '8px 0' }}>{thread.title}</h1>
<div style={{ color: '#999', fontSize: 13 }}>{thread.author_name} · {new Date(thread.created_at).toLocaleString()}</div>
<p style={{ marginTop: 16, whiteSpace: 'pre-wrap', lineHeight: 1.7 }}>{thread.content}</p>
<div style={{ marginTop: 16, display: 'flex', gap: 12, alignItems: 'center' }}>
<Button type={liked ? 'primary' : 'default'} onClick={doLike}>👍 {likeCount}</Button>
<span style={{ color: '#999' }}>💬 {replies.length}</span>
<Popconfirm title="确定删除这篇帖子?" onConfirm={delThread}>
<a style={{ marginLeft: 'auto', color: '#999' }}></a>
</Popconfirm>
</div>
</div>
<h3 style={{ marginTop: 24 }}>{replies.length}</h3>
<List
dataSource={replies}
locale={{ emptyText: '还没有回帖,来抢沙发' }}
renderItem={r => (
<List.Item actions={[<Popconfirm key="d" title="删除这条回帖?" onConfirm={() => delReply(r.id)}><a style={{ color: '#bbb' }}></a></Popconfirm>]}>
<List.Item.Meta
title={<span style={{ fontSize: 13, color: '#999' }}>#{r.floor} · {r.author_name} · {new Date(r.created_at).toLocaleString()}</span>}
description={<span style={{ color: '#333', whiteSpace: 'pre-wrap' }}>{r.content}</span>}
/>
</List.Item>
)}
/>
<div style={{ marginTop: 16 }}>
<Input.TextArea rows={3} value={replyText} onChange={e => setReplyText(e.target.value)} placeholder="写下你的回帖…" maxLength={5000} />
<div style={{ textAlign: 'right', marginTop: 8 }}>
<Button type="primary" onClick={doReply}></Button>
</div>
</div>
<PhoneAuthModal open={authOpen} onSuccess={onAuthed} onCancel={() => { setAuthOpen(false); setPendingAfterLogin(null) }} />
</div>
)
}