blob: 170e31323aa56f10d35789af64c52bf612be15a2 [file] [log] [blame]
Xing Jinwenff16b1e2025-06-05 00:29:26 +08001<template>
2 <div class="topic-detail-page">
208159515458d95702025-06-09 14:46:58 +08003 <Navbar />
Xing Jinwenff16b1e2025-06-05 00:29:26 +08004 <div class="page-container">
5 <!-- 面包屑导航 -->
6 <div class="breadcrumb">
7 <el-breadcrumb separator="/">
8 <el-breadcrumb-item :to="{ path: '/forum' }">论坛首页</el-breadcrumb-item>
9 <el-breadcrumb-item :to="{ path: `/forum/section/${topic.sectionId}` }">
10 {{ topic.sectionName }}
11 </el-breadcrumb-item>
12 <el-breadcrumb-item>{{ topic.title }}</el-breadcrumb-item>
13 </el-breadcrumb>
14 </div>
15
16 <!-- 主题信息 -->
17 <div class="topic-header">
18 <div class="topic-info">
19 <div class="topic-title-row">
20 <h1 class="topic-title">{{ topic.title }}</h1>
21 <div class="topic-status">
22 <el-tag v-if="topic.pinned" type="warning" size="small">置顶</el-tag>
23 <el-tag v-if="topic.hot" type="danger" size="small">热门</el-tag>
24 <el-tag v-if="topic.closed" type="info" size="small">已关闭</el-tag>
25 </div>
26 </div>
27
28 <div class="topic-tags">
29 <el-tag
30 v-for="tag in topic.tags"
31 :key="tag"
32 size="small"
33 type="info"
34 effect="plain"
35 >
36 {{ tag }}
37 </el-tag>
38 </div>
39
40 <div class="topic-meta">
41 <div class="author-info">
42 <el-avatar :size="32">{{ topic.author.charAt(0) }}</el-avatar>
43 <div class="author-details">
44 <span class="author-name">{{ topic.author }}</span>
208159515458d95702025-06-09 14:46:58 +080045
Xing Jinwenff16b1e2025-06-05 00:29:26 +080046 </div>
47 </div>
48
49 <div class="topic-stats">
50 <div class="stat-item">
51 <el-icon><View /></el-icon>
52 <span>{{ topic.views }} 浏览</span>
53 </div>
54 <div class="stat-item">
55 <el-icon><Comment /></el-icon>
56 <span>{{ topic.replies }} 回复</span>
57 </div>
58 </div>
59 </div>
60 </div>
61
62 <div class="topic-actions">
63 <el-button
64 v-if="!topic.closed"
65 type="primary"
66 :icon="Edit"
67 @click="showReplyDialog = true"
68 >
69 回复主题
70 </el-button>
71 <el-dropdown @command="handleTopicAction">
72 <el-button :icon="More">
73 更多 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
74 </el-button>
75 <template #dropdown>
76 <el-dropdown-menu>
Xing Jinwenff16b1e2025-06-05 00:29:26 +080077 <el-dropdown-item command="share">分享主题</el-dropdown-item>
78 <el-dropdown-item command="report" divided>举报主题</el-dropdown-item>
79 </el-dropdown-menu>
80 </template>
81 </el-dropdown>
82 </div>
83 </div>
84
85 <!-- 主题内容和回复列表 -->
86 <div class="posts-container">
87 <!-- 主楼 -->
88 <div class="post-item main-post">
89 <div class="post-header">
90 <div class="floor-number">#1</div>
91 <div class="post-author">
92 <el-avatar :size="48">{{ topic.author.charAt(0) }}</el-avatar>
93 <div class="author-info">
94 <span class="author-name">{{ topic.author }}</span>
95 <span class="author-title">{{ topic.authorTitle || '会员' }}</span>
96 <div class="author-stats">
97 <span>帖子: {{ topic.authorPosts || 0 }}</span>
98 <span>声望: {{ topic.authorReputation || 0 }}</span>
99 </div>
100 </div>
101 </div>
208159515458d95702025-06-09 14:46:58 +0800102
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800103 </div>
104
105 <div class="post-content">
106 <div class="content-text" v-html="formatContent(topic.content)"></div>
107 </div>
108
109 <div class="post-actions">
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800110 <el-button type="text" size="small" @click="quotePost(topic)">
111 <el-icon><ChatDotRound /></el-icon>
112 引用
113 </el-button>
114 <el-button type="text" size="small" @click="reportPost(topic.id)">
115 <el-icon><Flag /></el-icon>
116 举报
117 </el-button>
118 </div>
119 </div>
120
121 <!-- 回复列表 -->
122 <div
123 v-for="(reply, index) in replies"
124 :key="reply.id"
125 class="post-item reply-post"
126 >
127 <div class="post-header">
128 <div class="floor-number">#{{ index + 2 }}</div>
129 <div class="post-author">
130 <el-avatar :size="48">{{ reply.author.charAt(0) }}</el-avatar>
131 <div class="author-info">
132 <span class="author-name">{{ reply.author }}</span>
133 <span class="author-title">{{ reply.authorTitle || '会员' }}</span>
134 <div class="author-stats">
135 <span>帖子: {{ reply.authorPosts || 0 }}</span>
136 <span>声望: {{ reply.authorReputation || 0 }}</span>
137 </div>
138 </div>
139 </div>
208159515458d95702025-06-09 14:46:58 +0800140
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800141 </div>
142
143 <div class="post-content">
144 <div v-if="reply.quotedPost" class="quoted-content">
145 <div class="quote-header">
146 <el-icon><ChatDotRound /></el-icon>
208159515458d95702025-06-09 14:46:58 +0800147 <span>{{ reply.quotedPost.author }}</span>
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800148 </div>
149 <div class="quote-text">{{ reply.quotedPost.content }}</div>
150 </div>
151 <div class="content-text" v-html="formatContent(reply.content)"></div>
152 </div>
153
154 <div class="post-actions">
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800155 <el-button type="text" size="small" @click="quotePost(reply)">
156 <el-icon><ChatDotRound /></el-icon>
157 引用
158 </el-button>
159 <el-button type="text" size="small" @click="reportPost(reply.id)">
160 <el-icon><Flag /></el-icon>
161 举报
162 </el-button>
163 </div>
164 </div>
165 </div>
166
167 <!-- 分页 -->
168 <div class="pagination-wrapper">
169 <el-pagination
170 v-model:current-page="currentPage"
171 v-model:page-size="pageSize"
172 :page-sizes="[10, 20, 50]"
173 :total="totalReplies"
174 layout="total, sizes, prev, pager, next, jumper"
175 @size-change="handleSizeChange"
176 @current-change="handleCurrentChange"
177 />
178 </div>
179
180 <!-- 快速回复 -->
181 <div v-if="!topic.closed" class="quick-reply">
182 <h3>快速回复</h3>
183 <el-input
184 v-model="quickReplyContent"
185 type="textarea"
186 :rows="4"
187 placeholder="输入你的回复..."
188 maxlength="2000"
189 show-word-limit
190 />
191 <div class="quick-reply-actions">
192 <el-button @click="clearQuickReply">清空</el-button>
193 <el-button type="primary" @click="submitQuickReply" :loading="submittingReply">
194 发表回复
195 </el-button>
196 </div>
197 </div>
198 </div>
199
200 <!-- 回复对话框 -->
201 <el-dialog
202 v-model="showReplyDialog"
203 title="回复主题"
204 width="700px"
205 :before-close="handleCloseReplyDialog"
206 >
207 <el-form
208 ref="replyFormRef"
209 :model="replyForm"
210 :rules="replyRules"
211 label-width="80px"
212 >
213 <el-form-item v-if="quotedContent" label="引用内容">
214 <div class="quoted-preview">
215 <div class="quote-header">
216 <span>{{ quotedContent.author }}</span>
217 </div>
218 <div class="quote-content">{{ quotedContent.content }}</div>
219 <el-button type="text" size="small" @click="clearQuote">
220 清除引用
221 </el-button>
222 </div>
223 </el-form-item>
224
225 <el-form-item label="回复内容" prop="content">
226 <el-input
227 v-model="replyForm.content"
228 type="textarea"
229 :rows="8"
230 placeholder="请输入回复内容..."
231 maxlength="5000"
232 show-word-limit
233 />
234 </el-form-item>
235 </el-form>
236
237 <template #footer>
238 <el-button @click="handleCloseReplyDialog">取消</el-button>
239 <el-button type="primary" @click="submitReply" :loading="submittingReply">
240 发表回复
241 </el-button>
242 </template>
243 </el-dialog>
244 </div>
245</template>
246
247<script>
248import { ref, reactive, onMounted } from 'vue'
249import { useRoute, useRouter } from 'vue-router'
250import { ElMessage, ElMessageBox } from 'element-plus'
208159515458d95702025-06-09 14:46:58 +0800251import axios from 'axios'
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800252import {
253 Edit,
254 More,
255 View,
256 Comment,
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800257 ChatDotRound,
258 Flag,
259 ArrowDown
260} from '@element-plus/icons-vue'
208159515458d95702025-06-09 14:46:58 +0800261import Navbar from "@/components/Navbar.vue";
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800262
263export default {
264 name: 'ForumTopicView',
208159515458d95702025-06-09 14:46:58 +0800265 components: {Navbar},
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800266 setup() {
267 const route = useRoute()
268 const router = useRouter()
269 const replyFormRef = ref(null)
208159515458d95702025-06-09 14:46:58 +0800270
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800271 const showReplyDialog = ref(false)
272 const submittingReply = ref(false)
208159515458d95702025-06-09 14:46:58 +0800273 const loading = ref(false)
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800274 const currentPage = ref(1)
275 const pageSize = ref(20)
276 const totalReplies = ref(0)
277 const quickReplyContent = ref('')
278 const quotedContent = ref(null)
208159515458d95702025-06-09 14:46:58 +0800279
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800280 const topic = ref({
208159515458d95702025-06-09 14:46:58 +0800281 id: null,
282 title: '',
283 sectionId: null,
284 sectionName: '',
285 author: '',
286 authorTitle: '',
287 authorPosts: 0,
288 authorReputation: 0,
289 createTime: '',
290 content: '',
291 views: 0,
292 replies: 0,
293 likes: 0,
294 tags: [],
295 pinned: false,
296 hot: false,
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800297 closed: false
298 })
208159515458d95702025-06-09 14:46:58 +0800299
300 const replies = ref([])
301
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800302 const replyForm = reactive({
303 content: ''
304 })
208159515458d95702025-06-09 14:46:58 +0800305
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800306 const replyRules = {
307 content: [
308 { required: true, message: '请输入回复内容', trigger: 'blur' },
309 { min: 5, max: 5000, message: '内容长度在 5 到 5000 个字符', trigger: 'blur' }
310 ]
311 }
208159515458d95702025-06-09 14:46:58 +0800312
313 // 初始化数据
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800314 onMounted(() => {
315 const topicId = route.params.id
316 fetchTopicDetail(topicId)
208159515458d95702025-06-09 14:46:58 +0800317 fetchReplies(topicId)
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800318 })
208159515458d95702025-06-09 14:46:58 +0800319
320 // 获取主题详情
321 const fetchTopicDetail = async (topicId) => {
322 loading.value = true
323 console.log(`[fetchTopicDetail] 请求主题详情: /api/posts/${topicId}`)
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800324 try {
208159515458d95702025-06-09 14:46:58 +0800325 const response = await axios.get(`/api/posts/${topicId}`)
326 console.log('[fetchTopicDetail] 响应数据:', response.data)
327 topic.value = {
328 ...response.data,
329 tags: response.data.tags ? response.data.tags.split(',') : [],
330 authorTitle: response.data.authorTitle || '会员'
331 }
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800332 } catch (error) {
208159515458d95702025-06-09 14:46:58 +0800333 console.error('[fetchTopicDetail] 请求失败:', error)
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800334 ElMessage.error('获取主题详情失败')
335 router.back()
208159515458d95702025-06-09 14:46:58 +0800336 } finally {
337 loading.value = false
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800338 }
339 }
208159515458d95702025-06-09 14:46:58 +0800340
341
342 // 获取主题的所有回复(平铺结构)
343 const fetchReplies = async (topicId) => {
344 loading.value = true
345 const url = `/api/posts/topic/${topicId}`
346 const params = { page: currentPage.value, size: pageSize.value }
347 console.log(`[fetchReplies] 请求回复列表: ${url}`, params)
348 try {
349 const response = await axios.get(url, { params })
350 console.log('[fetchReplies] 响应数据:', response.data)
351 replies.value = response.data.posts.map(post => ({
352 id: post.id,
353 author: post.author,
354 authorTitle: post.authorTitle || '会员',
355 authorPosts: post.authorPosts || 0,
356 authorReputation: post.authorReputation || 0,
357 createTime: post.createTime,
358 content: post.content,
359 likes: post.likes || 0,
360 quotedPost: post.quotedPost ? {
361 author: post.quotedPost.author,
362 time: post.quotedPost.time,
363 content: post.quotedPost.content
364 } : null
365 }))
366 totalReplies.value = response.data.total
367 } catch (error) {
368 console.error('[fetchReplies] 请求失败:', error)
369 ElMessage.error('获取回复列表失败')
370 } finally {
371 loading.value = false
372 }
373 }
374
375
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800376 const formatDateTime = (dateString) => {
377 const date = new Date(dateString)
378 return date.toLocaleString('zh-CN', {
379 year: 'numeric',
380 month: '2-digit',
381 day: '2-digit',
382 hour: '2-digit',
383 minute: '2-digit'
384 })
385 }
208159515458d95702025-06-09 14:46:58 +0800386
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800387 const formatContent = (content) => {
388 return content.replace(/\n/g, '<br>')
389 }
208159515458d95702025-06-09 14:46:58 +0800390
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800391 const handleTopicAction = (command) => {
392 switch (command) {
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800393 case 'share':
394 navigator.clipboard.writeText(window.location.href)
395 ElMessage.success('链接已复制到剪贴板')
396 break
397 case 'report':
398 reportPost(topic.value.id)
399 break
400 }
401 }
208159515458d95702025-06-09 14:46:58 +0800402
403
404
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800405 const quotePost = (post) => {
406 quotedContent.value = {
407 author: post.author,
408 content: post.content.replace(/<[^>]*>/g, '').substring(0, 100) + '...',
409 time: post.createTime
410 }
411 showReplyDialog.value = true
412 }
208159515458d95702025-06-09 14:46:58 +0800413
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800414 const reportPost = async (postId) => {
415 try {
208159515458d95702025-06-09 14:46:58 +0800416 const { value } = await ElMessageBox.prompt('请说明举报原因', '举报内容', {
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800417 confirmButtonText: '提交举报',
418 cancelButtonText: '取消',
419 inputType: 'textarea',
420 inputPlaceholder: '请详细说明举报原因...'
421 })
208159515458d95702025-06-09 14:46:58 +0800422 // 假设有举报API,实际需根据后端实现
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800423 ElMessage.success('举报已提交,我们会尽快处理')
424 } catch {
425 // 用户取消
426 }
427 }
208159515458d95702025-06-09 14:46:58 +0800428
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800429 const clearQuote = () => {
430 quotedContent.value = null
431 }
208159515458d95702025-06-09 14:46:58 +0800432
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800433 const handleCloseReplyDialog = () => {
434 if (replyForm.content) {
435 ElMessageBox.confirm(
208159515458d95702025-06-09 14:46:58 +0800436 '确定要关闭吗?未保存的内容将会丢失。',
437 '提示',
438 {
439 confirmButtonText: '确定',
440 cancelButtonText: '取消',
441 type: 'warning'
442 }
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800443 ).then(() => {
444 resetReplyForm()
445 showReplyDialog.value = false
446 }).catch(() => {
447 // 用户取消
448 })
449 } else {
450 resetReplyForm()
451 showReplyDialog.value = false
452 }
453 }
208159515458d95702025-06-09 14:46:58 +0800454
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800455 const submitReply = async () => {
456 try {
457 await replyFormRef.value?.validate()
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800458 submittingReply.value = true
208159515458d95702025-06-09 14:46:58 +0800459
460 const postData = {
461 topicId: topic.value.id,
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800462 content: replyForm.content,
208159515458d95702025-06-09 14:46:58 +0800463 quotedPost: quotedContent.value ? {
464 author: quotedContent.value.author,
465 time: quotedContent.value.time,
466 content: quotedContent.value.content
467 } : null,
468 author: localStorage.getItem('username') || '用户'
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800469 }
208159515458d95702025-06-09 14:46:58 +0800470
471 const response = await axios.post('/api/posts', postData)
472 replies.value.push({
473 id: response.data.id,
474 author: response.data.author,
475 authorTitle: response.data.authorTitle || '会员',
476 authorPosts: response.data.authorPosts || 0,
477 authorReputation: response.data.authorReputation || 0,
478 createTime: response.data.createTime,
479 content: response.data.content,
480 likes: response.data.likes || 0,
481 quotedPost: response.data.quotedPost
482 })
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800483 topic.value.replies += 1
208159515458d95702025-06-09 14:46:58 +0800484
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800485 ElMessage.success('回复发表成功!')
486 resetReplyForm()
487 showReplyDialog.value = false
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800488 } catch (error) {
208159515458d95702025-06-09 14:46:58 +0800489 console.error('表单验证或提交失败:', error)
490 ElMessage.error('发表回复失败')
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800491 } finally {
492 submittingReply.value = false
493 }
494 }
208159515458d95702025-06-09 14:46:58 +0800495
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800496 const submitQuickReply = async () => {
497 if (!quickReplyContent.value.trim()) {
498 ElMessage.warning('请输入回复内容')
499 return
500 }
208159515458d95702025-06-09 14:46:58 +0800501
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800502 submittingReply.value = true
503 try {
208159515458d95702025-06-09 14:46:58 +0800504 const postData = {
505 topicId: topic.value.id,
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800506 content: quickReplyContent.value,
208159515458d95702025-06-09 14:46:58 +0800507 author: localStorage.getItem('username') || '用户'
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800508 }
208159515458d95702025-06-09 14:46:58 +0800509
510 const response = await axios.post('/api/posts', postData)
511 replies.value.push({
512 id: response.data.id,
513 author: response.data.author,
514 authorTitle: response.data.authorTitle || '会员',
515 authorPosts: response.data.authorPosts || 0,
516 authorReputation: response.data.authorReputation || 0,
517 createTime: response.data.createTime,
518 content: response.data.content,
519 likes: response.data.likes || 0
520 })
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800521 topic.value.replies += 1
522 quickReplyContent.value = ''
208159515458d95702025-06-09 14:46:58 +0800523
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800524 ElMessage.success('回复发表成功!')
525 } catch (error) {
526 ElMessage.error('发表回复失败')
527 } finally {
528 submittingReply.value = false
529 }
530 }
208159515458d95702025-06-09 14:46:58 +0800531
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800532 const clearQuickReply = () => {
533 quickReplyContent.value = ''
534 }
208159515458d95702025-06-09 14:46:58 +0800535
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800536 const resetReplyForm = () => {
537 replyFormRef.value?.resetFields()
538 replyForm.content = ''
539 quotedContent.value = null
540 }
208159515458d95702025-06-09 14:46:58 +0800541
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800542 const handleSizeChange = (size) => {
543 pageSize.value = size
544 currentPage.value = 1
208159515458d95702025-06-09 14:46:58 +0800545 fetchReplies(topic.value.id)
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800546 }
208159515458d95702025-06-09 14:46:58 +0800547
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800548 const handleCurrentChange = (page) => {
549 currentPage.value = page
208159515458d95702025-06-09 14:46:58 +0800550 fetchReplies(topic.value.id)
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800551 }
208159515458d95702025-06-09 14:46:58 +0800552
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800553 return {
554 showReplyDialog,
555 submittingReply,
208159515458d95702025-06-09 14:46:58 +0800556 loading,
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800557 currentPage,
558 pageSize,
559 totalReplies,
560 quickReplyContent,
561 quotedContent,
562 topic,
563 replies,
564 replyForm,
565 replyRules,
566 replyFormRef,
567 formatDateTime,
568 formatContent,
569 handleTopicAction,
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800570 quotePost,
571 reportPost,
572 clearQuote,
573 handleCloseReplyDialog,
574 submitReply,
575 submitQuickReply,
576 clearQuickReply,
577 handleSizeChange,
578 handleCurrentChange,
579 Edit,
580 More,
581 View,
582 Comment,
Xing Jinwenff16b1e2025-06-05 00:29:26 +0800583 ChatDotRound,
584 Flag,
585 ArrowDown
586 }
587 }
588}
589</script>
590
591<style lang="scss" scoped>
592.topic-detail-page {
593 max-width: 1000px;
594 margin: 0 auto;
595 padding: 24px;
596 background: #f5f5f5;
597 min-height: 100vh;
598}
599
600.breadcrumb {
601 margin-bottom: 16px;
602}
603
604.topic-header {
605 background: #fff;
606 border-radius: 12px;
607 padding: 24px;
608 margin-bottom: 24px;
609 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
610
611 display: flex;
612 justify-content: space-between;
613 align-items: flex-start;
614 gap: 24px;
615
616 .topic-info {
617 flex: 1;
618
619 .topic-title-row {
620 display: flex;
621 align-items: center;
622 gap: 12px;
623 margin-bottom: 12px;
624
625 .topic-title {
626 font-size: 24px;
627 font-weight: 600;
628 color: #2c3e50;
629 margin: 0;
630 flex: 1;
631 }
632
633 .topic-status {
634 .el-tag {
635 margin-left: 8px;
636 }
637 }
638 }
639
640 .topic-tags {
641 margin-bottom: 16px;
642
643 .el-tag {
644 margin-right: 8px;
645 }
646 }
647
648 .topic-meta {
649 display: flex;
650 justify-content: space-between;
651 align-items: center;
652
653 .author-info {
654 display: flex;
655 align-items: center;
656 gap: 12px;
657
658 .author-details {
659 .author-name {
660 display: block;
661 font-weight: 600;
662 color: #2c3e50;
663 font-size: 14px;
664 }
665
666 .post-time {
667 display: block;
668 font-size: 12px;
669 color: #909399;
670 }
671 }
672 }
673
674 .topic-stats {
675 display: flex;
676 gap: 16px;
677
678 .stat-item {
679 display: flex;
680 align-items: center;
681 gap: 4px;
682 font-size: 14px;
683 color: #7f8c8d;
684 }
685 }
686 }
687 }
688
689 .topic-actions {
690 display: flex;
691 gap: 12px;
692 flex-shrink: 0;
693 }
694}
695
696.posts-container {
697 .post-item {
698 background: #fff;
699 border-radius: 12px;
700 margin-bottom: 16px;
701 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
702 overflow: hidden;
703
704 &.main-post {
705 border-left: 4px solid #409eff;
706 }
707
708 .post-header {
709 background: #f8f9fa;
710 padding: 16px 24px;
711 display: flex;
712 align-items: center;
713 gap: 16px;
714 border-bottom: 1px solid #f0f0f0;
715
716 .floor-number {
717 background: #409eff;
718 color: white;
719 padding: 4px 8px;
720 border-radius: 4px;
721 font-size: 12px;
722 font-weight: 600;
723 min-width: 32px;
724 text-align: center;
725 }
726
727 .post-author {
728 display: flex;
729 align-items: center;
730 gap: 12px;
731 flex: 1;
732
733 .author-info {
734 .author-name {
735 display: block;
736 font-weight: 600;
737 color: #2c3e50;
738 font-size: 14px;
739 }
740
741 .author-title {
742 display: block;
743 font-size: 12px;
744 color: #67c23a;
745 margin-bottom: 4px;
746 }
747
748 .author-stats {
749 font-size: 11px;
750 color: #909399;
751
752 span {
753 margin-right: 12px;
754 }
755 }
756 }
757 }
758
759 .post-time {
760 font-size: 12px;
761 color: #909399;
762 }
763 }
764
765 .post-content {
766 padding: 24px;
767
768 .quoted-content {
769 background: #f5f7fa;
770 border-left: 4px solid #e4e7ed;
771 padding: 12px 16px;
772 margin-bottom: 16px;
773 border-radius: 0 4px 4px 0;
774
775 .quote-header {
776 display: flex;
777 align-items: center;
778 gap: 8px;
779 font-size: 12px;
780 color: #909399;
781 margin-bottom: 8px;
782 }
783
784 .quote-text {
785 font-size: 14px;
786 color: #606266;
787 line-height: 1.5;
788 }
789 }
790
791 .content-text {
792 line-height: 1.6;
793 color: #2c3e50;
794
795 :deep(h3) {
796 color: #2c3e50;
797 font-size: 18px;
798 font-weight: 600;
799 margin: 20px 0 12px 0;
800 }
801
802 :deep(p) {
803 margin-bottom: 12px;
804 }
805
806 :deep(ul), :deep(ol) {
807 margin: 12px 0;
808 padding-left: 20px;
809
810 li {
811 margin-bottom: 8px;
812 }
813 }
814 }
815 }
816
817 .post-actions {
818 padding: 12px 24px;
819 border-top: 1px solid #f0f0f0;
820 background: #fafafa;
821
822 .el-button {
823 margin-right: 16px;
824
825 .el-icon {
826 margin-right: 4px;
827 }
828 }
829 }
830 }
831}
832
833.pagination-wrapper {
834 text-align: center;
835 margin: 24px 0;
836}
837
838.quick-reply {
839 background: #fff;
840 border-radius: 12px;
841 padding: 24px;
842 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
843
844 h3 {
845 font-size: 18px;
846 font-weight: 600;
847 color: #2c3e50;
848 margin: 0 0 16px 0;
849 }
850
851 .quick-reply-actions {
852 margin-top: 12px;
853 text-align: right;
854
855 .el-button {
856 margin-left: 12px;
857 }
858 }
859}
860
861.quoted-preview {
862 background: #f5f7fa;
863 border: 1px solid #e4e7ed;
864 border-radius: 4px;
865 padding: 12px;
866
867 .quote-header {
868 font-size: 12px;
869 color: #909399;
870 margin-bottom: 8px;
871 }
872
873 .quote-content {
874 font-size: 14px;
875 color: #606266;
876 margin-bottom: 8px;
877 line-height: 1.5;
878 }
879}
880
881@media (max-width: 768px) {
882 .topic-detail-page {
883 padding: 16px;
884 }
885
886 .topic-header {
887 flex-direction: column;
888 align-items: flex-start;
889
890 .topic-actions {
891 width: 100%;
892 justify-content: flex-end;
893 }
894 }
895
896 .post-header {
897 flex-direction: column;
898 align-items: flex-start;
899 gap: 12px;
900
901 .floor-number {
902 align-self: flex-start;
903 }
904 }
905
906 .post-content {
907 padding: 16px;
908 }
909
910 .post-actions {
911 padding: 12px 16px;
912
913 .el-button {
914 margin-right: 8px;
915 margin-bottom: 8px;
916 }
917 }
918}
xingjinwend652cc62025-06-04 19:52:19 +0800919</style>