论坛,聊天室前后端对接

Change-Id: I90740329ab40dc050e8a791a382ab187900d673a
diff --git a/src/views/forum/ForumSectionView.vue b/src/views/forum/ForumSectionView.vue
index d0b42ec..ccb88f5 100644
--- a/src/views/forum/ForumSectionView.vue
+++ b/src/views/forum/ForumSectionView.vue
@@ -1,5 +1,6 @@
 <template>

   <div class="section-page">

+    <Navbar />

     <div class="page-container">

       <!-- 面包屑导航 -->

       <div class="breadcrumb">

@@ -25,14 +26,6 @@
                 <el-icon><ChatDotRound /></el-icon>

                 <span>{{ sectionInfo.topics }} 主题</span>

               </div>

-              <div class="stat-item">

-                <el-icon><Comment /></el-icon>

-                <span>{{ sectionInfo.replies }} 回复</span>

-              </div>

-              <div class="stat-item">

-                <el-icon><User /></el-icon>

-                <span>{{ sectionInfo.members }} 成员</span>

-              </div>

             </div>

           </div>

         </div>

@@ -101,8 +94,8 @@
               

               <div class="topic-meta">

                 <div class="author-info">

-                  <el-avatar :size="24">{{ topic.author.charAt(0) }}</el-avatar>

-                  <span class="author-name">{{ topic.author }}</span>

+                  <el-avatar :size="24">{{ topic.user?.username ? topic.user.username.charAt(0) : 'A' }}</el-avatar>

+                  <span class="author-name">{{ topic.user?.username || '匿名' }}</span>

                   <span class="create-time">{{ formatTime(topic.createTime) }}</span>

                 </div>

                 

@@ -166,8 +159,8 @@
               

               <div class="topic-meta">

                 <div class="author-info">

-                  <el-avatar :size="24">{{ topic.author.charAt(0) }}</el-avatar>

-                  <span class="author-name">{{ topic.author }}</span>

+                  <el-avatar :size="24">{{ topic.user?.username ? topic.user.username.charAt(0) : 'A' }}</el-avatar>

+                  <span class="author-name">{{ topic.user?.username || '匿名' }}</span>

                   <span class="create-time">{{ formatTime(topic.createTime) }}</span>

                 </div>

                 

@@ -201,8 +194,8 @@
         <!-- 分页 -->

         <div class="pagination-wrapper">

           <el-pagination

-            v-model:current-page="currentPage"

-            v-model:page-size="pageSize"

+            v-model="currentPage"

+            :page-size="pageSize"

             :page-sizes="[20, 50, 100]"

             :total="totalTopics"

             layout="total, sizes, prev, pager, next, jumper"

@@ -313,9 +306,12 @@
   Bell,

   QuestionFilled

 } from '@element-plus/icons-vue'

+import { getTopicsByForum, createTopic } from '@/api/topic'

+import Navbar from "@/components/Navbar.vue";

 

 export default {

   name: 'ForumSectionView',

+  components: {Navbar},

   setup() {

     const route = useRoute()

     const router = useRouter()

@@ -381,60 +377,12 @@
       }

     ])

     

-    const topics = ref([

-      {

-        id: 2,

-        title: '2024年度最佳科幻电影推荐榜单',

-        author: 'SciFiFan',

-        createTime: '2025-06-03T10:30:00',

-        views: 1234,

-        replies: 45,

-        hot: true,

-        featured: false,

-        closed: false,

-        hasNewReplies: true,

-        lastReply: {

-          author: 'MovieLover',

-          time: '2025-06-03T14:25:00'

-        }

-      },

-      {

-        id: 3,

-        title: '阿凡达2:水之道 观影感受分享',

-        author: 'Avatar2Fan',

-        createTime: '2025-06-02T16:45:00',

-        views: 892,

-        replies: 67,

-        hot: false,

-        featured: true,

-        closed: false,

-        hasNewReplies: false,

-        lastReply: {

-          author: 'CinemaExpert',

-          time: '2025-06-03T12:10:00'

-        }

-      },

-      {

-        id: 4,

-        title: '求推荐几部好看的悬疑电影',

-        author: 'SuspenseLover',

-        createTime: '2025-06-01T09:20:00',

-        views: 456,

-        replies: 23,

-        hot: false,

-        featured: false,

-        closed: false,

-        hasNewReplies: true,

-        lastReply: {

-          author: 'ThrillerFan',

-          time: '2025-06-03T11:45:00'

-        }

-      }

-    ])

+    const topics = ref([])

     

     onMounted(() => {

       const sectionId = route.params.id

       fetchSectionData(sectionId)

+      fetchTopics()

     })

     

     const fetchSectionData = async (id) => {

@@ -457,9 +405,7 @@
         sectionInfo.value = {

           id: parseInt(id),

           ...sectionData,

-          topics: 3256,

-          replies: 18934,

-          members: 1234

+          topics: 0  // 初始化为0,会在fetchTopics中更新为真实数量

         }

         

         totalTopics.value = 156

@@ -507,9 +453,12 @@
     const fetchTopics = async () => {

       loading.value = true

       try {

-        // 模拟API调用

-        await new Promise(resolve => setTimeout(resolve, 500))

-        console.log('获取主题列表:', { searchQuery: searchQuery.value, sortBy: sortBy.value, filterType: filterType.value })

+        // 调用后端API获取主题列表

+        const res = await getTopicsByForum(sectionInfo.value.id)

+        topics.value = res.data || res // 兼容不同返回结构

+        totalTopics.value = topics.value.length

+        // 同时更新顶部显示的主题数量

+        sectionInfo.value.topics = topics.value.length

       } catch (error) {

         ElMessage.error('获取主题列表失败')

       } finally {

@@ -579,8 +528,20 @@
         

         submitting.value = true

         

-        // 模拟提交过程

-        await new Promise(resolve => setTimeout(resolve, 1500))

+        // 构建主题数据

+        const topicData = {

+          title: newTopic.title,

+          content: newTopic.content,

+          forumId: sectionInfo.value.id, // 使用当前版块ID

+          tags: newTopic.tags,

+          isPinned: newTopic.options.includes('hot'),

+          isLocked: false

+        }

+        

+        console.log('提交主题数据:', topicData)

+        

+        // 调用API创建主题

+        const response = await createTopic(topicData)

         

         ElMessage.success('主题发布成功!')

         resetForm()

@@ -590,7 +551,8 @@
         fetchTopics()

         

       } catch (error) {

-        console.error('表单验证失败:', error)

+        console.error('发布主题失败:', error)

+        ElMessage.error('发布主题失败,请重试')

       } finally {

         submitting.value = false

       }

diff --git a/src/views/forum/ForumTopicView.vue b/src/views/forum/ForumTopicView.vue
index e65b297..170e313 100644
--- a/src/views/forum/ForumTopicView.vue
+++ b/src/views/forum/ForumTopicView.vue
@@ -1,5 +1,6 @@
 <template>

   <div class="topic-detail-page">

+    <Navbar />

     <div class="page-container">

       <!-- 面包屑导航 -->

       <div class="breadcrumb">

@@ -41,7 +42,7 @@
               <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>

             

@@ -73,9 +74,6 @@
             </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>

@@ -101,9 +99,7 @@
                 </div>

               </div>

             </div>

-            <div class="post-time">

-              {{ formatDateTime(topic.createTime) }}

-            </div>

+

           </div>

           

           <div class="post-content">

@@ -111,10 +107,6 @@
           </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>

               引用

@@ -145,16 +137,14 @@
                 </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>

+                <span>{{ reply.quotedPost.author }}</span>

               </div>

               <div class="quote-text">{{ reply.quotedPost.content }}</div>

             </div>

@@ -162,10 +152,6 @@
           </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>

               引用

@@ -262,127 +248,131 @@
 import { ref, reactive, onMounted } from 'vue'

 import { useRoute, useRouter } from 'vue-router'

 import { ElMessage, ElMessageBox } from 'element-plus'

+import axios from 'axios'

 import {

   Edit,

   More,

   View,

   Comment,

-  Like,

   ChatDotRound,

   Flag,

   ArrowDown

 } from '@element-plus/icons-vue'

+import Navbar from "@/components/Navbar.vue";

 

 export default {

   name: 'ForumTopicView',

+  components: {Navbar},

   setup() {

     const route = useRoute()

     const router = useRouter()

     const replyFormRef = ref(null)

-    

+

     const showReplyDialog = ref(false)

     const submittingReply = ref(false)

-    const isFavorited = ref(false)

+    const loading = 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,

+      id: null,

+      title: '',

+      sectionId: null,

+      sectionName: '',

+      author: '',

+      authorTitle: '',

+      authorPosts: 0,

+      authorReputation: 0,

+      createTime: '',

+      content: '',

+      views: 0,

+      replies: 0,

+      likes: 0,

+      tags: [],

+      pinned: false,

+      hot: false,

       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 replies = ref([])

+

     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)

+      fetchReplies(topicId)

     })

-    

-    const fetchTopicDetail = async (id) => {

+

+    // 获取主题详情

+    const fetchTopicDetail = async (topicId) => {

+      loading.value = true

+      console.log(`[fetchTopicDetail] 请求主题详情: /api/posts/${topicId}`)

       try {

-        console.log('获取主题详情:', id)

-        totalReplies.value = 147

+        const response = await axios.get(`/api/posts/${topicId}`)

+        console.log('[fetchTopicDetail] 响应数据:', response.data)

+        topic.value = {

+          ...response.data,

+          tags: response.data.tags ? response.data.tags.split(',') : [],

+          authorTitle: response.data.authorTitle || '会员'

+        }

       } catch (error) {

+        console.error('[fetchTopicDetail] 请求失败:', error)

         ElMessage.error('获取主题详情失败')

         router.back()

+      } finally {

+        loading.value = false

       }

     }

-    

+

+

+    // 获取主题的所有回复(平铺结构)

+    const fetchReplies = async (topicId) => {

+      loading.value = true

+      const url = `/api/posts/topic/${topicId}`

+      const params = { page: currentPage.value, size: pageSize.value }

+      console.log(`[fetchReplies] 请求回复列表: ${url}`, params)

+      try {

+        const response = await axios.get(url, { params })

+        console.log('[fetchReplies] 响应数据:', response.data)

+        replies.value = response.data.posts.map(post => ({

+          id: post.id,

+          author: post.author,

+          authorTitle: post.authorTitle || '会员',

+          authorPosts: post.authorPosts || 0,

+          authorReputation: post.authorReputation || 0,

+          createTime: post.createTime,

+          content: post.content,

+          likes: post.likes || 0,

+          quotedPost: post.quotedPost ? {

+            author: post.quotedPost.author,

+            time: post.quotedPost.time,

+            content: post.quotedPost.content

+          } : null

+        }))

+        totalReplies.value = response.data.total

+      } catch (error) {

+        console.error('[fetchReplies] 请求失败:', error)

+        ElMessage.error('获取回复列表失败')

+      } finally {

+        loading.value = false

+      }

+    }

+

+

     const formatDateTime = (dateString) => {

       const date = new Date(dateString)

       return date.toLocaleString('zh-CN', {

@@ -393,17 +383,13 @@
         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('链接已复制到剪贴板')

@@ -413,19 +399,9 @@
           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,

@@ -434,36 +410,36 @@
       }

       showReplyDialog.value = true

     }

-    

+

     const reportPost = async (postId) => {

       try {

-        await ElMessageBox.prompt('请说明举报原因', '举报内容', {

+        const { value } = await ElMessageBox.prompt('请说明举报原因', '举报内容', {

           confirmButtonText: '提交举报',

           cancelButtonText: '取消',

           inputType: 'textarea',

           inputPlaceholder: '请详细说明举报原因...'

         })

-        

+        // 假设有举报API,实际需根据后端实现

         ElMessage.success('举报已提交,我们会尽快处理')

       } catch {

         // 用户取消

       }

     }

-    

+

     const clearQuote = () => {

       quotedContent.value = null

     }

-    

+

     const handleCloseReplyDialog = () => {

       if (replyForm.content) {

         ElMessageBox.confirm(

-          '确定要关闭吗?未保存的内容将会丢失。',

-          '提示',

-          {

-            confirmButtonText: '确定',

-            cancelButtonText: '取消',

-            type: 'warning'

-          }

+            '确定要关闭吗?未保存的内容将会丢失。',

+            '提示',

+            {

+              confirmButtonText: '确定',

+              cancelButtonText: '取消',

+              type: 'warning'

+            }

         ).then(() => {

           resetReplyForm()

           showReplyDialog.value = false

@@ -475,66 +451,76 @@
         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(),

+

+        const postData = {

+          topicId: topic.value.id,

           content: replyForm.content,

-          likes: 0,

-          quotedPost: quotedContent.value

+          quotedPost: quotedContent.value ? {

+            author: quotedContent.value.author,

+            time: quotedContent.value.time,

+            content: quotedContent.value.content

+          } : null,

+          author: localStorage.getItem('username') || '用户'

         }

-        

-        replies.value.push(newReply)

+

+        const response = await axios.post('/api/posts', postData)

+        replies.value.push({

+          id: response.data.id,

+          author: response.data.author,

+          authorTitle: response.data.authorTitle || '会员',

+          authorPosts: response.data.authorPosts || 0,

+          authorReputation: response.data.authorReputation || 0,

+          createTime: response.data.createTime,

+          content: response.data.content,

+          likes: response.data.likes || 0,

+          quotedPost: response.data.quotedPost

+        })

         topic.value.replies += 1

-        

+

         ElMessage.success('回复发表成功!')

         resetReplyForm()

         showReplyDialog.value = false

-        

       } catch (error) {

-        console.error('表单验证失败:', error)

+        console.error('表单验证或提交失败:', error)

+        ElMessage.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(),

+        const postData = {

+          topicId: topic.value.id,

           content: quickReplyContent.value,

-          likes: 0

+          author: localStorage.getItem('username') || '用户'

         }

-        

-        replies.value.push(newReply)

+

+        const response = await axios.post('/api/posts', postData)

+        replies.value.push({

+          id: response.data.id,

+          author: response.data.author,

+          authorTitle: response.data.authorTitle || '会员',

+          authorPosts: response.data.authorPosts || 0,

+          authorReputation: response.data.authorReputation || 0,

+          createTime: response.data.createTime,

+          content: response.data.content,

+          likes: response.data.likes || 0

+        })

         topic.value.replies += 1

         quickReplyContent.value = ''

-        

+

         ElMessage.success('回复发表成功!')

       } catch (error) {

         ElMessage.error('发表回复失败')

@@ -542,30 +528,32 @@
         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

+      fetchReplies(topic.value.id)

     }

-    

+

     const handleCurrentChange = (page) => {

       currentPage.value = page

+      fetchReplies(topic.value.id)

     }

-    

+

     return {

       showReplyDialog,

       submittingReply,

-      isFavorited,

+      loading,

       currentPage,

       pageSize,

       totalReplies,

@@ -579,7 +567,6 @@
       formatDateTime,

       formatContent,

       handleTopicAction,

-      likePost,

       quotePost,

       reportPost,

       clearQuote,

@@ -593,7 +580,6 @@
       More,

       View,

       Comment,

-      Like,

       ChatDotRound,

       Flag,

       ArrowDown

diff --git a/src/views/forum/ForumView.vue b/src/views/forum/ForumView.vue
index 28cdff2..703e4f5 100644
--- a/src/views/forum/ForumView.vue
+++ b/src/views/forum/ForumView.vue
@@ -67,10 +67,10 @@
             <div class="section-info">

               <h3 class="section-name">{{ section.name }}</h3>

               <p class="section-description">{{ section.description }}</p>

-              <div class="section-stats">

-                <span class="stat">{{ section.topics }} 主题</span>

-                <span class="stat">{{ section.replies }} 回复</span>

-              </div>

+<!--              <div class="section-stats">-->

+<!--                <span class="stat">{{ section.topics }} 主题</span>-->

+<!--                <span class="stat">{{ section.replies }} 回复</span>-->

+<!--              </div>-->

             </div>

             <div class="section-latest">

               <div v-if="section.latestTopic" class="latest-topic">

@@ -270,13 +270,21 @@
   QuestionFilled,

   Bell

 } from '@element-plus/icons-vue'

-import Navbar from '@/components/Navbar.vue'

+import {

+  getAllTopics,

+  getTopicsByForum,

+  createTopic,

+  searchTopics

+} from '@/api/topic'

+import {

+  getAllForums,

+  getForumById

+} from '@/api/forum'

+import Navbar from "@/components/Navbar.vue";

 

 export default {

   name: 'ForumView',

-  components:{

-    Navbar

-  },

+  components: {Navbar},

   setup() {

     const router = useRouter()

     const topicFormRef = ref(null)

@@ -288,10 +296,10 @@
     const tagInputValue = ref('')

     

     const forumStats = reactive({

-      totalTopics: '15,268',

-      totalReplies: '89,456',

-      activeUsers: '2,341',

-      todayPosts: '156'

+      totalTopics: 0,

+      totalReplies: 0,

+      activeUsers: 0,

+      todayPosts: 0

     })

     

     const newTopic = reactive({

@@ -315,156 +323,83 @@
       ]

     }

     

-    const forumSections = ref([

-      {

-        id: 1,

-        name: '电影讨论',

-        description: '分享和讨论电影资源,交流观影心得',

-        icon: 'Film',

-        color: '#409eff',

-        topics: 3256,

-        replies: 18934,

-        latestTopic: {

-          title: '2024年最佳科幻电影推荐',

-          author: 'MovieFan',

-          time: '2025-06-03T14:30:00'

-        }

-      },

-      {

-        id: 2,

-        name: '音乐分享',

-        description: '音乐资源分享,音乐制作技术交流',

-        icon: 'Headphones',

-        color: '#67c23a',

-        topics: 1892,

-        replies: 9567,

-        latestTopic: {

-          title: '无损音乐格式对比分析',

-          author: 'AudioExpert',

-          time: '2025-06-03T13:45:00'

-        }

-      },

-      {

-        id: 3,

-        name: '软件技术',

-        description: '软件资源分享,技术问题讨论',

-        icon: 'Monitor',

-        color: '#e6a23c',

-        topics: 2134,

-        replies: 12456,

-        latestTopic: {

-          title: 'Adobe 2025 新功能体验分享',

-          author: 'TechGuru',

-          time: '2025-06-03T12:20:00'

-        }

-      },

-      {

-        id: 4,

-        name: '游戏天地',

-        description: '游戏资源分享,游戏攻略讨论',

-        icon: 'GamePad',

-        color: '#f56c6c',

-        topics: 1567,

-        replies: 8234,

-        latestTopic: {

-          title: '年度游戏大作盘点',

-          author: 'GameMaster',

-          time: '2025-06-03T11:50:00'

-        }

-      },

-      {

-        id: 5,

-        name: '站务公告',

-        description: '网站公告,规则说明,意见建议',

-        icon: 'Bell',

-        color: '#909399',

-        topics: 234,

-        replies: 1567,

-        latestTopic: {

-          title: '网站维护通知',

-          author: 'Admin',

-          time: '2025-06-03T10:00:00'

-        }

-      },

-      {

-        id: 6,

-        name: '新手求助',

-        description: '新手问题解答,使用教程分享',

-        icon: 'QuestionFilled',

-        color: '#606266',

-        topics: 456,

-        replies: 2890,

-        latestTopic: {

-          title: '新手如何提高分享率?',

-          author: 'Newbie123',

-          time: '2025-06-03T09:30:00'

-        }

-      }

-    ])

+    const forumSections = ref([])

     

-    const hotTopics = ref([

-      {

-        id: 1,

-        title: '2024年度最佳PT站点推荐与对比分析',

-        author: 'PTExpert',

-        views: 2856,

-        replies: 147,

-        lastReply: '2025-06-03T14:25:00',

-        tags: ['PT站点', '推荐', '对比'],

-        pinned: true,

-        hot: true

-      },

-      {

-        id: 2,

-        title: '如何安全高效地使用BT下载工具',

-        author: 'SafeDownloader',

-        views: 1932,

-        replies: 89,

-        lastReply: '2025-06-03T13:50:00',

-        tags: ['BT工具', '安全', '教程'],

-        hot: true

-      },

-      {

-        id: 3,

-        title: '分享率提升技巧与经验总结',

-        author: 'SeedMaster',

-        views: 1654,

-        replies: 76,

-        lastReply: '2025-06-03T12:40:00',

-        tags: ['分享率', '技巧', '经验']

-      }

-    ])

+    const hotTopics = ref([])

+    const recentReplies = ref([])

     

-    const recentReplies = ref([

-      {

-        id: 1,

-        author: 'MovieLover88',

-        topicId: 1,

-        topicTitle: '阿凡达2观影感受分享',

-        content: '画面效果确实震撼,特别是水下的场景...',

-        time: '2025-06-03T14:45:00'

-      },

-      {

-        id: 2,

-        author: 'TechEnthusiast',

-        topicId: 2,

-        topicTitle: '最新版Photoshop使用技巧',

-        content: '新的AI功能确实很强大,大大提高了工作效率...',

-        time: '2025-06-03T14:30:00'

-      },

-      {

-        id: 3,

-        author: 'GameFan2024',

-        topicId: 3,

-        topicTitle: '赛博朋克2077最新更新体验',

-        content: '修复了很多bug,现在游戏体验好多了...',

-        time: '2025-06-03T14:15:00'

+    const loadForumData = async () => {

+      try {

+        // 获取所有论坛

+        const forums = await getAllForums()

+        forumSections.value = forums.map(forum => ({

+          id: forum.id,

+          name: forum.name,

+          description: forum.description,

+          icon: getForumIcon(forum.name),

+          color: getForumColor(forum.name),

+          topics: forum.topicCount || 0,

+          replies: forum.replyCount || 0,

+          latestTopic: forum.latestTopic ? {

+            title: forum.latestTopic.title,

+            author: forum.latestTopic.user?.username || '匿名用户',

+            time: forum.latestTopic.createTime

+          } : null

+        }))

+        

+        // 获取所有主题

+        const topics = await getAllTopics()

+        hotTopics.value = topics.slice(0, 3).map(topic => ({

+          id: topic.id,

+          title: topic.title,

+          author: topic.user?.username || '匿名用户',

+          views: topic.views || 0,

+          replies: topic.replies || 0,

+          lastReply: topic.lastReplyTime,

+          tags: topic.tags || [],

+          pinned: topic.pinned || false,

+          hot: topic.views > 1000

+        }))

+        

+        // 更新统计数据

+        forumStats.totalTopics = topics.length

+        forumStats.totalReplies = topics.reduce((sum, topic) => sum + (topic.replies || 0), 0)

+        forumStats.activeUsers = new Set(topics.map(topic => topic.user?.id)).size

+        forumStats.todayPosts = topics.filter(topic => {

+          const topicDate = new Date(topic.createTime)

+          const today = new Date()

+          return topicDate.toDateString() === today.toDateString()

+        }).length

+        

+      } catch (error) {

+        console.error('加载论坛数据失败:', error)

+        ElMessage.error('加载论坛数据失败')

       }

-    ])

+    }

     

-    onMounted(() => {

-      // 初始化论坛数据

-    })

+    const getForumIcon = (name) => {

+      const iconMap = {

+        '电影讨论': 'Film',

+        '音乐分享': 'Headphones',

+        '软件技术': 'Monitor',

+        '游戏天地': 'GamePad',

+        '站务公告': 'Bell',

+        '新手求助': 'QuestionFilled'

+      }

+      return iconMap[name] || 'ChatLineRound'

+    }

+    

+    const getForumColor = (name) => {

+      const colorMap = {

+        '电影讨论': '#409eff',

+        '音乐分享': '#67c23a',

+        '软件技术': '#e6a23c',

+        '游戏天地': '#f56c6c',

+        '站务公告': '#909399',

+        '新手求助': '#606266'

+      }

+      return colorMap[name] || '#409eff'

+    }

     

     const formatTime = (timeString) => {

       const date = new Date(timeString)

@@ -537,18 +472,28 @@
         

         submitting.value = true

         

-        // 模拟提交过程

-        await new Promise(resolve => setTimeout(resolve, 1500))

+        const topicData = {

+          title: newTopic.title,

+          content: newTopic.content,

+          forumId: newTopic.sectionId,

+          tags: newTopic.tags

+        }

+        

+        const response = await createTopic(topicData)

         

         ElMessage.success('主题发布成功!')

         resetForm()

         showNewTopicDialog.value = false

         

+        // 刷新论坛数据

+        await loadForumData()

+        

         // 跳转到新创建的主题页面

-        router.push('/forum/topic/new')

+        router.push(`/forum/topic/${response.id}`)

         

       } catch (error) {

-        console.error('表单验证失败:', error)

+        console.error('发布主题失败:', error)

+        ElMessage.error('发布主题失败')

       } finally {

         submitting.value = false

       }

@@ -562,6 +507,10 @@
       newTopic.tags = []

     }

     

+    onMounted(() => {

+      loadForumData()

+    })

+    

     return {

       showNewTopicDialog,

       submitting,

@@ -594,7 +543,10 @@
       Monitor,

       GamePad,

       Bell,

-      QuestionFilled

+      QuestionFilled,

+      loadForumData,

+      getForumIcon,

+      getForumColor

     }

   }

 }

diff --git a/src/views/forum/TopicView.vue b/src/views/forum/TopicView.vue
new file mode 100644
index 0000000..c79bfa8
--- /dev/null
+++ b/src/views/forum/TopicView.vue
@@ -0,0 +1,666 @@
+<template>
+  <div class="topic-page">
+    <Navbar />
+    <div class="page-container">
+      <!-- 话题头部 -->
+      <div class="topic-header">
+        <div class="header-content">
+          <div class="topic-title-section">
+            <h1>{{ topic.title }}</h1>
+            <div class="topic-meta">
+              <span class="author">
+                <el-avatar :size="24">{{ topic.user?.username ? topic.user.username.charAt(0) : 'A' }}</el-avatar>
+                <span class="author-name">{{ topic.user?.username || '匿名' }}</span>
+              </span>
+              <span class="time">{{ formatTime(topic.createTime) }}</span>
+              <span class="views">
+                <el-icon><View /></el-icon>
+                {{ topic.views }}
+              </span>
+              <el-button
+                :icon="isSubscribed ? StarFilled : Star"
+                :type="isSubscribed ? 'warning' : 'default'"
+                @click="handleSubscribe"
+              >
+                {{ isSubscribed ? '已订阅' : '订阅' }}
+              </el-button>
+            </div>
+          </div>
+          <div class="topic-actions" v-if="isAuthor">
+            <el-button type="primary" @click="showEditDialog = true">编辑</el-button>
+            <el-button type="danger" @click="handleDelete">删除</el-button>
+          </div>
+        </div>
+      </div>
+
+      <!-- 话题内容 -->
+      <div class="topic-content">
+        <div class="content-card">
+          <div class="content-body" v-html="topic.content"></div>
+          <div class="content-tags">
+            <el-tag
+              v-for="tag in topic.tags"
+              :key="tag.id"
+              :color="tag.color"
+              effect="light"
+            >
+              {{ tag.name }}
+            </el-tag>
+          </div>
+        </div>
+      </div>
+
+
+
+      <!-- 回复列表 -->
+      <div class="replies-section">
+        <h2>回复 ({{ getTotalReplyCount() }})</h2>
+        <div class="replies-list">
+          <!-- 使用递归组件显示所有回复 -->
+          <ReplyTree
+            :replies="replies"
+            :level="0"
+            @reply="replyToPost"
+            @edit="editReply"
+            @delete="deleteReply"
+          />
+        </div>
+      </div>
+
+      <!-- 回复框 -->
+      <div class="reply-box">
+        <h3>{{ replyingTo ? `回复 @${replyingTo.author}` : '发表回复' }}</h3>
+        <div v-if="replyingTo" class="replying-to">
+          <div class="original-content">{{ replyingTo.content }}</div>
+          <el-button size="small" @click="cancelReply">取消回复</el-button>
+        </div>
+        <el-input
+          v-model="newReply"
+          type="textarea"
+          :rows="4"
+          placeholder="请输入回复内容..."
+        />
+        <div class="reply-actions">
+          <el-button type="primary" @click="submitReply" :loading="submitting">
+            发表回复
+          </el-button>
+        </div>
+      </div>
+    </div>
+
+    <!-- 编辑话题对话框 -->
+    <el-dialog
+      v-model="showEditDialog"
+      title="编辑话题"
+      width="600px"
+    >
+      <el-form
+        ref="editFormRef"
+        :model="editForm"
+        :rules="editRules"
+        label-width="80px"
+      >
+        <el-form-item label="标题" prop="title">
+          <el-input v-model="editForm.title" />
+        </el-form-item>
+        <el-form-item label="内容" prop="content">
+          <el-input
+            v-model="editForm.content"
+            type="textarea"
+            :rows="8"
+          />
+        </el-form-item>
+        <el-form-item label="标签">
+          <div class="tags-input">
+            <el-tag
+              v-for="tag in editForm.tags"
+              :key="tag"
+              closable
+              @close="removeTag(tag)"
+            >
+              {{ tag }}
+            </el-tag>
+            <el-input
+              v-if="tagInputVisible"
+              ref="tagInputRef"
+              v-model="tagInputValue"
+              size="small"
+              @keyup.enter="addTag"
+              @blur="addTag"
+              style="width: 100px;"
+            />
+            <el-button
+              v-else
+              size="small"
+              @click="showTagInput"
+            >
+              + 添加标签
+            </el-button>
+          </div>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="showEditDialog = false">取消</el-button>
+        <el-button type="primary" @click="submitEdit" :loading="submitting">
+          保存
+        </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { ref, reactive, onMounted, computed } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { View, Star, StarFilled } from '@element-plus/icons-vue'
+import {
+  getTopicById,
+  updateTopic,
+  deleteTopic
+} from '@/api/topic'
+import {
+  getPostsByTopic,
+  getPostTreeByTopic,
+  createPost,
+  updatePost,
+  deletePost
+} from '@/api/post'
+
+import {
+  getTopicTags,
+  assignTagsToTopic
+} from '@/api/forumTag'
+import {
+  recordTopicView,
+  getUserViewHistory
+} from '@/api/topicView'
+import {
+  subscribeTopic,
+  unsubscribeTopic,
+  checkSubscription,
+  getSubscriptionList
+} from '@/api/subscription'
+import Navbar from "@/components/Navbar.vue";
+import ReplyTree from "@/components/ReplyTree.vue";
+
+export default {
+  name: 'TopicView',
+  components: {Navbar, ReplyTree},
+  setup() {
+    const route = useRoute()
+    const router = useRouter()
+    const topicId = computed(() => route.params.id)
+    
+    const topic = ref({})
+    const replies = ref([])
+    const newReply = ref('')
+    const submitting = ref(false)
+    const showEditDialog = ref(false)
+    const tagInputVisible = ref(false)
+    const tagInputValue = ref('')
+    const replyingTo = ref(null)
+    
+    const editForm = reactive({
+      title: '',
+      content: '',
+      tags: []
+    })
+    
+    const editRules = {
+      title: [
+        { required: true, message: '请输入标题', trigger: 'blur' },
+        { min: 5, max: 100, message: '标题长度在 5 到 100 个字符', trigger: 'blur' }
+      ],
+      content: [
+        { required: true, message: '请输入内容', trigger: 'blur' },
+        { min: 10, max: 5000, message: '内容长度在 10 到 5000 个字符', trigger: 'blur' }
+      ]
+    }
+    
+    const isAuthor = computed(() => {
+      // 这里需要根据实际用户系统来判断
+      return true
+    })
+    
+    const isSubscribed = ref(false)
+    
+    const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
+    const token = localStorage.getItem('token')
+    
+    const loadTopicData = async () => {
+      try {
+        // 获取话题详情
+        const response = await getTopicById(topicId.value)
+        topic.value = response
+        editForm.title = response.title
+        editForm.content = response.content
+        
+        // 获取话题标签
+        const tags = await getTopicTags(topicId.value)
+        editForm.tags = tags.map(tag => tag.name)
+        
+        // 获取树形回复结构
+        const posts = await getPostTreeByTopic(topicId.value)
+        replies.value = posts
+        
+
+        
+        // 检查订阅状态
+        if (token && userInfo.id) {
+          const subscriptionStatus = await checkSubscription({
+            topicId: topicId.value,
+            userId: userInfo.id
+          })
+          isSubscribed.value = subscriptionStatus
+        }
+        
+        // 记录浏览
+        if (token && userInfo.id) {
+          await recordTopicView({
+            topicId: topicId.value,
+            userId: userInfo.id
+          })
+        }
+      } catch (error) {
+        console.error('加载话题数据失败:', error)
+        ElMessage.error('加载话题数据失败')
+      }
+    }
+    
+
+    
+    const handleSubscribe = async () => {
+      try {
+        if (isSubscribed.value) {
+          await unsubscribeTopic({ topicId: topicId.value })
+          isSubscribed.value = false
+          ElMessage.success('已取消订阅')
+        } else {
+          await subscribeTopic({ topicId: topicId.value })
+          isSubscribed.value = true
+          ElMessage.success('已订阅话题')
+        }
+      } catch (error) {
+        console.error('订阅操作失败:', error)
+        ElMessage.error('订阅操作失败')
+      }
+    }
+    
+    const formatTime = (timeString) => {
+      const date = new Date(timeString)
+      const now = new Date()
+      const diff = now - date
+      const hours = Math.floor(diff / (1000 * 60 * 60))
+      
+      if (hours < 1) return '刚刚'
+      if (hours < 24) return `${hours}小时前`
+      const days = Math.floor(hours / 24)
+      return `${days}天前`
+    }
+    
+    const handleDelete = async () => {
+      try {
+        await ElMessageBox.confirm(
+          '确定要删除这个话题吗?此操作不可恢复。',
+          '警告',
+          {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning'
+          }
+        )
+        
+        await deleteTopic(topicId.value)
+        ElMessage.success('话题已删除')
+        router.push('/forum')
+      } catch (error) {
+        if (error !== 'cancel') {
+          console.error('删除话题失败:', error)
+          ElMessage.error('删除话题失败')
+        }
+      }
+    }
+    
+    const submitEdit = async () => {
+      try {
+        submitting.value = true
+        const updatedTopic = {
+          ...topic.value,
+          title: editForm.title,
+          content: editForm.content
+        }
+        
+        await updateTopic(topicId.value, updatedTopic)
+        
+        // 更新标签
+        await assignTagsToTopic(topicId.value, editForm.tags)
+        
+        ElMessage.success('话题已更新')
+        showEditDialog.value = false
+        await loadTopicData()
+      } catch (error) {
+        console.error('更新话题失败:', error)
+        ElMessage.error('更新话题失败')
+      } finally {
+        submitting.value = false
+      }
+    }
+    
+    const submitReply = async () => {
+      if (!newReply.value.trim()) {
+        ElMessage.warning('请输入回复内容')
+        return
+      }
+      
+      try {
+        submitting.value = true
+        const reply = {
+          topicId: topicId.value,
+          content: newReply.value,
+          parentId: replyingTo.value ? replyingTo.value.id : null
+        }
+        
+        await createPost(reply)
+        ElMessage.success('回复已发布')
+        newReply.value = ''
+        replyingTo.value = null
+        await loadTopicData()
+      } catch (error) {
+        console.error('发布回复失败:', error)
+        ElMessage.error('发布回复失败')
+      } finally {
+        submitting.value = false
+      }
+    }
+
+    const replyToPost = (post) => {
+      replyingTo.value = {
+        id: post.id,
+        author: post.user?.username || '匿名',
+        content: post.content.substring(0, 100) + (post.content.length > 100 ? '...' : '')
+      }
+      // 滚动到回复框
+      document.querySelector('.reply-box').scrollIntoView({ behavior: 'smooth' })
+    }
+
+    const cancelReply = () => {
+      replyingTo.value = null
+    }
+
+    // 递归计算所有回复的总数(包括嵌套回复)
+    const countReplies = (posts) => {
+      let count = 0
+      for (const post of posts) {
+        count += 1
+        if (post.replies && post.replies.length > 0) {
+          count += countReplies(post.replies)
+        }
+      }
+      return count
+    }
+
+    const getTotalReplyCount = () => {
+      return countReplies(replies.value)
+    }
+
+
+    
+    const editReply = (reply) => {
+      // 实现编辑回复的逻辑
+    }
+    
+    const deleteReply = async (replyId) => {
+      try {
+        await ElMessageBox.confirm(
+          '确定要删除这条回复吗?',
+          '警告',
+          {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning'
+          }
+        )
+        
+        await softDeletePost(replyId)
+        ElMessage.success('回复已删除')
+        await loadTopicData()
+      } catch (error) {
+        if (error !== 'cancel') {
+          console.error('删除回复失败:', error)
+          ElMessage.error('删除回复失败')
+        }
+      }
+    }
+    
+    const showTagInput = () => {
+      tagInputVisible.value = true
+      nextTick(() => {
+        tagInputRef.value?.focus()
+      })
+    }
+    
+    const addTag = () => {
+      const tag = tagInputValue.value.trim()
+      if (tag && !editForm.tags.includes(tag)) {
+        editForm.tags.push(tag)
+      }
+      tagInputVisible.value = false
+      tagInputValue.value = ''
+    }
+    
+    const removeTag = (tag) => {
+      const index = editForm.tags.indexOf(tag)
+      if (index > -1) {
+        editForm.tags.splice(index, 1)
+      }
+    }
+    
+    onMounted(() => {
+      loadTopicData()
+    })
+    
+          return {
+      topic,
+      replies,
+      newReply,
+      submitting,
+      showEditDialog,
+      tagInputVisible,
+      tagInputValue,
+      replyingTo,
+      editForm,
+      editRules,
+      isAuthor,
+      formatTime,
+      handleDelete,
+      submitEdit,
+      submitReply,
+      replyToPost,
+      cancelReply,
+      getTotalReplyCount,
+      editReply,
+      deleteReply,
+      showTagInput,
+      addTag,
+      removeTag,
+      View,
+      Star,
+      StarFilled,
+      isSubscribed,
+      handleSubscribe
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.topic-page {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 24px;
+  background: #f5f5f5;
+  min-height: 100vh;
+}
+
+.topic-header {
+  background: #fff;
+  border-radius: 12px;
+  padding: 24px;
+  margin-bottom: 24px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+  
+  .header-content {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    
+    .topic-title-section {
+      h1 {
+        font-size: 24px;
+        font-weight: 600;
+        color: #2c3e50;
+        margin: 0 0 16px 0;
+      }
+      
+      .topic-meta {
+        display: flex;
+        align-items: center;
+        gap: 16px;
+        color: #7f8c8d;
+        font-size: 14px;
+        
+        .author {
+          display: flex;
+          align-items: center;
+          gap: 8px;
+        }
+        
+        .views {
+          display: flex;
+          align-items: center;
+          gap: 4px;
+        }
+      }
+    }
+    
+    .topic-actions {
+      display: flex;
+      gap: 12px;
+    }
+  }
+}
+
+.topic-content {
+  margin-bottom: 24px;
+  
+  .content-card {
+    background: #fff;
+    border-radius: 12px;
+    padding: 24px;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+    
+    .content-body {
+      font-size: 16px;
+      line-height: 1.6;
+      color: #2c3e50;
+      margin-bottom: 24px;
+    }
+    
+    .content-tags {
+      display: flex;
+      gap: 8px;
+      flex-wrap: wrap;
+    }
+  }
+}
+
+
+
+.replies-section {
+  background: #fff;
+  border-radius: 12px;
+  padding: 24px;
+  margin-bottom: 24px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+  
+  h2 {
+    font-size: 20px;
+    font-weight: 600;
+    color: #2c3e50;
+    margin: 0 0 20px 0;
+  }
+}
+
+.reply-box {
+  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;
+  }
+  
+  .replying-to {
+    background: #f8f9fa;
+    border-left: 4px solid #3498db;
+    padding: 12px 16px;
+    margin-bottom: 16px;
+    border-radius: 4px;
+    
+    .original-content {
+      color: #666;
+      font-size: 14px;
+      margin-bottom: 8px;
+      font-style: italic;
+    }
+  }
+  
+  .reply-actions {
+    margin-top: 16px;
+    display: flex;
+    justify-content: flex-end;
+  }
+}
+
+.tags-input {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  align-items: center;
+  
+  .el-tag {
+    margin: 0;
+  }
+}
+
+
+
+.reply-actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+@media (max-width: 768px) {
+  .topic-page {
+    padding: 16px;
+  }
+  
+  .topic-header {
+    .header-content {
+      flex-direction: column;
+      
+      .topic-actions {
+        margin-top: 16px;
+        width: 100%;
+        justify-content: flex-end;
+      }
+    }
+  }
+}
+</style> 
\ No newline at end of file