前后端登录注册连接成功
Change-Id: Ib5f9282fe7217b3363e542ce5c4e1c0d32619dcb
diff --git a/src/views/forum/ForumTopicView.vue b/src/views/forum/ForumTopicView.vue
index 027083f..e65b297 100644
--- a/src/views/forum/ForumTopicView.vue
+++ b/src/views/forum/ForumTopicView.vue
@@ -1,933 +1,933 @@
-<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;
- }
- }
-}
+<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>
\ No newline at end of file