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