blob: e65b297303b29e778f1792d8da4739266a466834 [file] [log] [blame]
<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>