<template> | |
<div class="topic-detail-page"> | |
<div class="page-container"> | |
<!-- 面包屑导航 --> | |
<div class="breadcrumb"> | |
<el-breadcrumb separator="/"> | |
<el-breadcrumb-item :to="{ path: '/forum' }">论坛首页</el-breadcrumb-item> | |
<el-breadcrumb-item :to="{ path: `/forum/section/${topic.sectionId}` }"> | |
{{ topic.sectionName }} | |
</el-breadcrumb-item> | |
<el-breadcrumb-item>{{ topic.title }}</el-breadcrumb-item> | |
</el-breadcrumb> | |
</div> | |
<!-- 主题信息 --> | |
<div class="topic-header"> | |
<div class="topic-info"> | |
<div class="topic-title-row"> | |
<h1 class="topic-title">{{ topic.title }}</h1> | |
<div class="topic-status"> | |
<el-tag v-if="topic.pinned" type="warning" size="small">置顶</el-tag> | |
<el-tag v-if="topic.hot" type="danger" size="small">热门</el-tag> | |
<el-tag v-if="topic.closed" type="info" size="small">已关闭</el-tag> | |
</div> | |
</div> | |
<div class="topic-tags"> | |
<el-tag | |
v-for="tag in topic.tags" | |
:key="tag" | |
size="small" | |
type="info" | |
effect="plain" | |
> | |
{{ tag }} | |
</el-tag> | |
</div> | |
<div class="topic-meta"> | |
<div class="author-info"> | |
<el-avatar :size="32">{{ topic.author.charAt(0) }}</el-avatar> | |
<div class="author-details"> | |
<span class="author-name">{{ topic.author }}</span> | |
<span class="post-time">发表于 {{ formatDateTime(topic.createTime) }}</span> | |
</div> | |
</div> | |
<div class="topic-stats"> | |
<div class="stat-item"> | |
<el-icon><View /></el-icon> | |
<span>{{ topic.views }} 浏览</span> | |
</div> | |
<div class="stat-item"> | |
<el-icon><Comment /></el-icon> | |
<span>{{ topic.replies }} 回复</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="topic-actions"> | |
<el-button | |
v-if="!topic.closed" | |
type="primary" | |
:icon="Edit" | |
@click="showReplyDialog = true" | |
> | |
回复主题 | |
</el-button> | |
<el-dropdown @command="handleTopicAction"> | |
<el-button :icon="More"> | |
更多 <el-icon class="el-icon--right"><ArrowDown /></el-icon> | |
</el-button> | |
<template #dropdown> | |
<el-dropdown-menu> | |
<el-dropdown-item command="favorite"> | |
{{ isFavorited ? '取消收藏' : '收藏主题' }} | |
</el-dropdown-item> | |
<el-dropdown-item command="share">分享主题</el-dropdown-item> | |
<el-dropdown-item command="report" divided>举报主题</el-dropdown-item> | |
</el-dropdown-menu> | |
</template> | |
</el-dropdown> | |
</div> | |
</div> | |
<!-- 主题内容和回复列表 --> | |
<div class="posts-container"> | |
<!-- 主楼 --> | |
<div class="post-item main-post"> | |
<div class="post-header"> | |
<div class="floor-number">#1</div> | |
<div class="post-author"> | |
<el-avatar :size="48">{{ topic.author.charAt(0) }}</el-avatar> | |
<div class="author-info"> | |
<span class="author-name">{{ topic.author }}</span> | |
<span class="author-title">{{ topic.authorTitle || '会员' }}</span> | |
<div class="author-stats"> | |
<span>帖子: {{ topic.authorPosts || 0 }}</span> | |
<span>声望: {{ topic.authorReputation || 0 }}</span> | |
</div> | |
</div> | |
</div> | |
<div class="post-time"> | |
{{ formatDateTime(topic.createTime) }} | |
</div> | |
</div> | |
<div class="post-content"> | |
<div class="content-text" v-html="formatContent(topic.content)"></div> | |
</div> | |
<div class="post-actions"> | |
<el-button type="text" size="small" @click="likePost(topic.id)"> | |
<el-icon><Like /></el-icon> | |
{{ topic.likes || 0 }} | |
</el-button> | |
<el-button type="text" size="small" @click="quotePost(topic)"> | |
<el-icon><ChatDotRound /></el-icon> | |
引用 | |
</el-button> | |
<el-button type="text" size="small" @click="reportPost(topic.id)"> | |
<el-icon><Flag /></el-icon> | |
举报 | |
</el-button> | |
</div> | |
</div> | |
<!-- 回复列表 --> | |
<div | |
v-for="(reply, index) in replies" | |
:key="reply.id" | |
class="post-item reply-post" | |
> | |
<div class="post-header"> | |
<div class="floor-number">#{{ index + 2 }}</div> | |
<div class="post-author"> | |
<el-avatar :size="48">{{ reply.author.charAt(0) }}</el-avatar> | |
<div class="author-info"> | |
<span class="author-name">{{ reply.author }}</span> | |
<span class="author-title">{{ reply.authorTitle || '会员' }}</span> | |
<div class="author-stats"> | |
<span>帖子: {{ reply.authorPosts || 0 }}</span> | |
<span>声望: {{ reply.authorReputation || 0 }}</span> | |
</div> | |
</div> | |
</div> | |
<div class="post-time"> | |
{{ formatDateTime(reply.createTime) }} | |
</div> | |
</div> | |
<div class="post-content"> | |
<div v-if="reply.quotedPost" class="quoted-content"> | |
<div class="quote-header"> | |
<el-icon><ChatDotRound /></el-icon> | |
<span>{{ reply.quotedPost.author }} 发表于 {{ formatDateTime(reply.quotedPost.time) }}</span> | |
</div> | |
<div class="quote-text">{{ reply.quotedPost.content }}</div> | |
</div> | |
<div class="content-text" v-html="formatContent(reply.content)"></div> | |
</div> | |
<div class="post-actions"> | |
<el-button type="text" size="small" @click="likePost(reply.id)"> | |
<el-icon><Like /></el-icon> | |
{{ reply.likes || 0 }} | |
</el-button> | |
<el-button type="text" size="small" @click="quotePost(reply)"> | |
<el-icon><ChatDotRound /></el-icon> | |
引用 | |
</el-button> | |
<el-button type="text" size="small" @click="reportPost(reply.id)"> | |
<el-icon><Flag /></el-icon> | |
举报 | |
</el-button> | |
</div> | |
</div> | |
</div> | |
<!-- 分页 --> | |
<div class="pagination-wrapper"> | |
<el-pagination | |
v-model:current-page="currentPage" | |
v-model:page-size="pageSize" | |
:page-sizes="[10, 20, 50]" | |
:total="totalReplies" | |
layout="total, sizes, prev, pager, next, jumper" | |
@size-change="handleSizeChange" | |
@current-change="handleCurrentChange" | |
/> | |
</div> | |
<!-- 快速回复 --> | |
<div v-if="!topic.closed" class="quick-reply"> | |
<h3>快速回复</h3> | |
<el-input | |
v-model="quickReplyContent" | |
type="textarea" | |
:rows="4" | |
placeholder="输入你的回复..." | |
maxlength="2000" | |
show-word-limit | |
/> | |
<div class="quick-reply-actions"> | |
<el-button @click="clearQuickReply">清空</el-button> | |
<el-button type="primary" @click="submitQuickReply" :loading="submittingReply"> | |
发表回复 | |
</el-button> | |
</div> | |
</div> | |
</div> | |
<!-- 回复对话框 --> | |
<el-dialog | |
v-model="showReplyDialog" | |
title="回复主题" | |
width="700px" | |
:before-close="handleCloseReplyDialog" | |
> | |
<el-form | |
ref="replyFormRef" | |
:model="replyForm" | |
:rules="replyRules" | |
label-width="80px" | |
> | |
<el-form-item v-if="quotedContent" label="引用内容"> | |
<div class="quoted-preview"> | |
<div class="quote-header"> | |
<span>{{ quotedContent.author }}</span> | |
</div> | |
<div class="quote-content">{{ quotedContent.content }}</div> | |
<el-button type="text" size="small" @click="clearQuote"> | |
清除引用 | |
</el-button> | |
</div> | |
</el-form-item> | |
<el-form-item label="回复内容" prop="content"> | |
<el-input | |
v-model="replyForm.content" | |
type="textarea" | |
:rows="8" | |
placeholder="请输入回复内容..." | |
maxlength="5000" | |
show-word-limit | |
/> | |
</el-form-item> | |
</el-form> | |
<template #footer> | |
<el-button @click="handleCloseReplyDialog">取消</el-button> | |
<el-button type="primary" @click="submitReply" :loading="submittingReply"> | |
发表回复 | |
</el-button> | |
</template> | |
</el-dialog> | |
</div> | |
</template> | |
<script> | |
import { ref, reactive, onMounted } from 'vue' | |
import { useRoute, useRouter } from 'vue-router' | |
import { ElMessage, ElMessageBox } from 'element-plus' | |
import { | |
Edit, | |
More, | |
View, | |
Comment, | |
Like, | |
ChatDotRound, | |
Flag, | |
ArrowDown | |
} from '@element-plus/icons-vue' | |
export default { | |
name: 'ForumTopicView', | |
setup() { | |
const route = useRoute() | |
const router = useRouter() | |
const replyFormRef = ref(null) | |
const showReplyDialog = ref(false) | |
const submittingReply = ref(false) | |
const isFavorited = ref(false) | |
const currentPage = ref(1) | |
const pageSize = ref(20) | |
const totalReplies = ref(0) | |
const quickReplyContent = ref('') | |
const quotedContent = ref(null) | |
const topic = ref({ | |
id: 1, | |
title: '2024年度最佳PT站点推荐与对比分析', | |
sectionId: 1, | |
sectionName: '站务讨论', | |
author: 'PTExpert', | |
authorTitle: '资深会员', | |
authorPosts: 1256, | |
authorReputation: 2890, | |
createTime: '2025-06-01T10:30:00', | |
content: ` | |
<p>大家好,作为一个使用PT站点多年的老用户,我想和大家分享一下2024年各大PT站点的使用体验和对比分析。</p> | |
<h3>评测标准</h3> | |
<ul> | |
<li>资源丰富度:种子数量、更新速度、稀有资源</li> | |
<li>用户体验:界面设计、功能完善度、响应速度</li> | |
<li>社区氛围:用户活跃度、互帮互助程度</li> | |
<li>规则友好性:考核难度、分享率要求、保种要求</li> | |
</ul> | |
<h3>推荐站点</h3> | |
<p>经过综合评测,以下几个站点值得推荐:</p> | |
<ol> | |
<li><strong>站点A</strong>:资源最全,更新最快,适合影视爱好者</li> | |
<li><strong>站点B</strong>:音乐资源丰富,无损居多,音质发烧友首选</li> | |
<li><strong>站点C</strong>:软件资源全面,更新及时,开发者必备</li> | |
</ol> | |
<p>具体的详细评测报告我会在后续回复中逐一介绍,欢迎大家讨论和补充!</p> | |
`, | |
views: 2856, | |
replies: 147, | |
likes: 89, | |
tags: ['PT站点', '推荐', '对比'], | |
pinned: true, | |
hot: true, | |
closed: false | |
}) | |
const replies = ref([ | |
{ | |
id: 2, | |
author: 'MovieLover88', | |
authorTitle: '影视达人', | |
authorPosts: 567, | |
authorReputation: 1234, | |
createTime: '2025-06-01T11:15:00', | |
content: '感谢楼主的详细分析!特别期待站点A的详细评测,最近正在寻找好的影视资源站点。', | |
likes: 12 | |
}, | |
{ | |
id: 3, | |
author: 'TechGuru', | |
authorTitle: '技术专家', | |
authorPosts: 890, | |
authorReputation: 2156, | |
createTime: '2025-06-01T12:30:00', | |
content: '站点C确实不错,软件资源很全面。不过楼主能不能也评测一下游戏类的PT站点?', | |
likes: 8, | |
quotedPost: { | |
author: 'PTExpert', | |
time: '2025-06-01T10:30:00', | |
content: '站点C:软件资源全面,更新及时,开发者必备' | |
} | |
} | |
]) | |
const replyForm = reactive({ | |
content: '' | |
}) | |
const replyRules = { | |
content: [ | |
{ required: true, message: '请输入回复内容', trigger: 'blur' }, | |
{ min: 5, max: 5000, message: '内容长度在 5 到 5000 个字符', trigger: 'blur' } | |
] | |
} | |
onMounted(() => { | |
const topicId = route.params.id | |
fetchTopicDetail(topicId) | |
}) | |
const fetchTopicDetail = async (id) => { | |
try { | |
console.log('获取主题详情:', id) | |
totalReplies.value = 147 | |
} catch (error) { | |
ElMessage.error('获取主题详情失败') | |
router.back() | |
} | |
} | |
const formatDateTime = (dateString) => { | |
const date = new Date(dateString) | |
return date.toLocaleString('zh-CN', { | |
year: 'numeric', | |
month: '2-digit', | |
day: '2-digit', | |
hour: '2-digit', | |
minute: '2-digit' | |
}) | |
} | |
const formatContent = (content) => { | |
return content.replace(/\n/g, '<br>') | |
} | |
const handleTopicAction = (command) => { | |
switch (command) { | |
case 'favorite': | |
isFavorited.value = !isFavorited.value | |
ElMessage.success(isFavorited.value ? '已收藏' : '已取消收藏') | |
break | |
case 'share': | |
navigator.clipboard.writeText(window.location.href) | |
ElMessage.success('链接已复制到剪贴板') | |
break | |
case 'report': | |
reportPost(topic.value.id) | |
break | |
} | |
} | |
const likePost = (postId) => { | |
if (postId === topic.value.id) { | |
topic.value.likes = (topic.value.likes || 0) + 1 | |
} else { | |
const reply = replies.value.find(r => r.id === postId) | |
if (reply) { | |
reply.likes = (reply.likes || 0) + 1 | |
} | |
} | |
ElMessage.success('点赞成功') | |
} | |
const quotePost = (post) => { | |
quotedContent.value = { | |
author: post.author, | |
content: post.content.replace(/<[^>]*>/g, '').substring(0, 100) + '...', | |
time: post.createTime | |
} | |
showReplyDialog.value = true | |
} | |
const reportPost = async (postId) => { | |
try { | |
await ElMessageBox.prompt('请说明举报原因', '举报内容', { | |
confirmButtonText: '提交举报', | |
cancelButtonText: '取消', | |
inputType: 'textarea', | |
inputPlaceholder: '请详细说明举报原因...' | |
}) | |
ElMessage.success('举报已提交,我们会尽快处理') | |
} catch { | |
// 用户取消 | |
} | |
} | |
const clearQuote = () => { | |
quotedContent.value = null | |
} | |
const handleCloseReplyDialog = () => { | |
if (replyForm.content) { | |
ElMessageBox.confirm( | |
'确定要关闭吗?未保存的内容将会丢失。', | |
'提示', | |
{ | |
confirmButtonText: '确定', | |
cancelButtonText: '取消', | |
type: 'warning' | |
} | |
).then(() => { | |
resetReplyForm() | |
showReplyDialog.value = false | |
}).catch(() => { | |
// 用户取消 | |
}) | |
} else { | |
resetReplyForm() | |
showReplyDialog.value = false | |
} | |
} | |
const submitReply = async () => { | |
try { | |
await replyFormRef.value?.validate() | |
submittingReply.value = true | |
await new Promise(resolve => setTimeout(resolve, 1500)) | |
const newReply = { | |
id: Date.now(), | |
author: localStorage.getItem('username') || '用户', | |
authorTitle: '会员', | |
authorPosts: 0, | |
authorReputation: 0, | |
createTime: new Date().toISOString(), | |
content: replyForm.content, | |
likes: 0, | |
quotedPost: quotedContent.value | |
} | |
replies.value.push(newReply) | |
topic.value.replies += 1 | |
ElMessage.success('回复发表成功!') | |
resetReplyForm() | |
showReplyDialog.value = false | |
} catch (error) { | |
console.error('表单验证失败:', error) | |
} finally { | |
submittingReply.value = false | |
} | |
} | |
const submitQuickReply = async () => { | |
if (!quickReplyContent.value.trim()) { | |
ElMessage.warning('请输入回复内容') | |
return | |
} | |
submittingReply.value = true | |
try { | |
await new Promise(resolve => setTimeout(resolve, 1000)) | |
const newReply = { | |
id: Date.now(), | |
author: localStorage.getItem('username') || '用户', | |
authorTitle: '会员', | |
authorPosts: 0, | |
authorReputation: 0, | |
createTime: new Date().toISOString(), | |
content: quickReplyContent.value, | |
likes: 0 | |
} | |
replies.value.push(newReply) | |
topic.value.replies += 1 | |
quickReplyContent.value = '' | |
ElMessage.success('回复发表成功!') | |
} catch (error) { | |
ElMessage.error('发表回复失败') | |
} finally { | |
submittingReply.value = false | |
} | |
} | |
const clearQuickReply = () => { | |
quickReplyContent.value = '' | |
} | |
const resetReplyForm = () => { | |
replyFormRef.value?.resetFields() | |
replyForm.content = '' | |
quotedContent.value = null | |
} | |
const handleSizeChange = (size) => { | |
pageSize.value = size | |
currentPage.value = 1 | |
} | |
const handleCurrentChange = (page) => { | |
currentPage.value = page | |
} | |
return { | |
showReplyDialog, | |
submittingReply, | |
isFavorited, | |
currentPage, | |
pageSize, | |
totalReplies, | |
quickReplyContent, | |
quotedContent, | |
topic, | |
replies, | |
replyForm, | |
replyRules, | |
replyFormRef, | |
formatDateTime, | |
formatContent, | |
handleTopicAction, | |
likePost, | |
quotePost, | |
reportPost, | |
clearQuote, | |
handleCloseReplyDialog, | |
submitReply, | |
submitQuickReply, | |
clearQuickReply, | |
handleSizeChange, | |
handleCurrentChange, | |
Edit, | |
More, | |
View, | |
Comment, | |
Like, | |
ChatDotRound, | |
Flag, | |
ArrowDown | |
} | |
} | |
} | |
</script> | |
<style lang="scss" scoped> | |
.topic-detail-page { | |
max-width: 1000px; | |
margin: 0 auto; | |
padding: 24px; | |
background: #f5f5f5; | |
min-height: 100vh; | |
} | |
.breadcrumb { | |
margin-bottom: 16px; | |
} | |
.topic-header { | |
background: #fff; | |
border-radius: 12px; | |
padding: 24px; | |
margin-bottom: 24px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); | |
display: flex; | |
justify-content: space-between; | |
align-items: flex-start; | |
gap: 24px; | |
.topic-info { | |
flex: 1; | |
.topic-title-row { | |
display: flex; | |
align-items: center; | |
gap: 12px; | |
margin-bottom: 12px; | |
.topic-title { | |
font-size: 24px; | |
font-weight: 600; | |
color: #2c3e50; | |
margin: 0; | |
flex: 1; | |
} | |
.topic-status { | |
.el-tag { | |
margin-left: 8px; | |
} | |
} | |
} | |
.topic-tags { | |
margin-bottom: 16px; | |
.el-tag { | |
margin-right: 8px; | |
} | |
} | |
.topic-meta { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
.author-info { | |
display: flex; | |
align-items: center; | |
gap: 12px; | |
.author-details { | |
.author-name { | |
display: block; | |
font-weight: 600; | |
color: #2c3e50; | |
font-size: 14px; | |
} | |
.post-time { | |
display: block; | |
font-size: 12px; | |
color: #909399; | |
} | |
} | |
} | |
.topic-stats { | |
display: flex; | |
gap: 16px; | |
.stat-item { | |
display: flex; | |
align-items: center; | |
gap: 4px; | |
font-size: 14px; | |
color: #7f8c8d; | |
} | |
} | |
} | |
} | |
.topic-actions { | |
display: flex; | |
gap: 12px; | |
flex-shrink: 0; | |
} | |
} | |
.posts-container { | |
.post-item { | |
background: #fff; | |
border-radius: 12px; | |
margin-bottom: 16px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); | |
overflow: hidden; | |
&.main-post { | |
border-left: 4px solid #409eff; | |
} | |
.post-header { | |
background: #f8f9fa; | |
padding: 16px 24px; | |
display: flex; | |
align-items: center; | |
gap: 16px; | |
border-bottom: 1px solid #f0f0f0; | |
.floor-number { | |
background: #409eff; | |
color: white; | |
padding: 4px 8px; | |
border-radius: 4px; | |
font-size: 12px; | |
font-weight: 600; | |
min-width: 32px; | |
text-align: center; | |
} | |
.post-author { | |
display: flex; | |
align-items: center; | |
gap: 12px; | |
flex: 1; | |
.author-info { | |
.author-name { | |
display: block; | |
font-weight: 600; | |
color: #2c3e50; | |
font-size: 14px; | |
} | |
.author-title { | |
display: block; | |
font-size: 12px; | |
color: #67c23a; | |
margin-bottom: 4px; | |
} | |
.author-stats { | |
font-size: 11px; | |
color: #909399; | |
span { | |
margin-right: 12px; | |
} | |
} | |
} | |
} | |
.post-time { | |
font-size: 12px; | |
color: #909399; | |
} | |
} | |
.post-content { | |
padding: 24px; | |
.quoted-content { | |
background: #f5f7fa; | |
border-left: 4px solid #e4e7ed; | |
padding: 12px 16px; | |
margin-bottom: 16px; | |
border-radius: 0 4px 4px 0; | |
.quote-header { | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
font-size: 12px; | |
color: #909399; | |
margin-bottom: 8px; | |
} | |
.quote-text { | |
font-size: 14px; | |
color: #606266; | |
line-height: 1.5; | |
} | |
} | |
.content-text { | |
line-height: 1.6; | |
color: #2c3e50; | |
:deep(h3) { | |
color: #2c3e50; | |
font-size: 18px; | |
font-weight: 600; | |
margin: 20px 0 12px 0; | |
} | |
:deep(p) { | |
margin-bottom: 12px; | |
} | |
:deep(ul), :deep(ol) { | |
margin: 12px 0; | |
padding-left: 20px; | |
li { | |
margin-bottom: 8px; | |
} | |
} | |
} | |
} | |
.post-actions { | |
padding: 12px 24px; | |
border-top: 1px solid #f0f0f0; | |
background: #fafafa; | |
.el-button { | |
margin-right: 16px; | |
.el-icon { | |
margin-right: 4px; | |
} | |
} | |
} | |
} | |
} | |
.pagination-wrapper { | |
text-align: center; | |
margin: 24px 0; | |
} | |
.quick-reply { | |
background: #fff; | |
border-radius: 12px; | |
padding: 24px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); | |
h3 { | |
font-size: 18px; | |
font-weight: 600; | |
color: #2c3e50; | |
margin: 0 0 16px 0; | |
} | |
.quick-reply-actions { | |
margin-top: 12px; | |
text-align: right; | |
.el-button { | |
margin-left: 12px; | |
} | |
} | |
} | |
.quoted-preview { | |
background: #f5f7fa; | |
border: 1px solid #e4e7ed; | |
border-radius: 4px; | |
padding: 12px; | |
.quote-header { | |
font-size: 12px; | |
color: #909399; | |
margin-bottom: 8px; | |
} | |
.quote-content { | |
font-size: 14px; | |
color: #606266; | |
margin-bottom: 8px; | |
line-height: 1.5; | |
} | |
} | |
@media (max-width: 768px) { | |
.topic-detail-page { | |
padding: 16px; | |
} | |
.topic-header { | |
flex-direction: column; | |
align-items: flex-start; | |
.topic-actions { | |
width: 100%; | |
justify-content: flex-end; | |
} | |
} | |
.post-header { | |
flex-direction: column; | |
align-items: flex-start; | |
gap: 12px; | |
.floor-number { | |
align-self: flex-start; | |
} | |
} | |
.post-content { | |
padding: 16px; | |
} | |
.post-actions { | |
padding: 12px 16px; | |
.el-button { | |
margin-right: 8px; | |
margin-bottom: 8px; | |
} | |
} | |
} | |
</style> |