feat(web): forum thread detail with like and reply
This commit is contained in:
parent
fd84b0b0ff
commit
ef35204bc6
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue