blob: 046c00b018a18ad79ae48dd9d93c24108a2a5239 [file] [log] [blame]
vulgar52019469e5c2025-06-09 01:34:47 +08001<template>
2 <div class="torrent-detail-page">
3 <div class="page-container">
4 <!-- 加载状态 -->
5 <div v-if="loading" class="loading-container">
6 <el-skeleton :rows="8" animated />
7 </div>
8
9 <!-- 详情内容 -->
10 <div v-else-if="torrentInfo">
11 <!-- 返回按钮 -->
12 <div class="back-button">
13 <el-button :icon="ArrowLeft" @click="$router.back()">
14 返回列表
15 </el-button>
16 </div>
17
18 <!-- 种子基本信息 -->
19 <div class="torrent-header">
20 <div class="header-content">
21 <div class="torrent-cover">
208159515458d95702025-06-09 14:46:58 +080022 <!-- <el-image
vulgar52019469e5c2025-06-09 01:34:47 +080023 :src="torrentInfo.coverImage || '/default-cover.jpg'"
24 :alt="torrentInfo.title"
25 fit="cover"
26 class="cover-image"
27 >
28 <template #error>
29 <div class="image-placeholder">
30 <el-icon size="48"><Picture /></el-icon>
31 <span>暂无封面</span>
32 </div>
33 </template>
208159515458d95702025-06-09 14:46:58 +080034 </el-image> -->
35 <el-image
36 :src="require('@/views/图片.png')"
37 :alt="torrentInfo.title"
38 fit="cover"
39 class="cover-image"
40 >
41 <template #error>
42 <div class="image-placeholder">
43 <el-icon size="48"><Picture /></el-icon>
44 <span>暂无封面</span>
45 </div>
46 </template>
vulgar52019469e5c2025-06-09 01:34:47 +080047 </el-image>
48 </div>
49
50 <div class="torrent-info">
51 <div class="category-tag">
52 <el-tag :type="getCategoryType(torrentInfo.category?.slug)" size="large">
53 {{ torrentInfo.category?.name || '未分类' }}
54 </el-tag>
55 <el-tag v-if="torrentInfo.subTitle" type="info" size="small">
56 {{ torrentInfo.subTitle }}
57 </el-tag>
58 </div>
59
60 <h1 class="torrent-title">{{ torrentInfo.title }}</h1>
61
62 <div class="torrent-tags" v-if="torrentInfo.tag && torrentInfo.tag.length > 0">
63 <el-tag
64 v-for="(tag, index) in parsedTags"
65 :key="index"
66 size="small"
67 effect="plain"
68 >
69 {{ tag }}
70 </el-tag>
71 </div>
72
73 <div class="torrent-meta">
74 <div class="meta-item">
75 <el-icon><User /></el-icon>
76 <span>上传者:{{ torrentInfo.user?.username || '匿名用户' }}</span>
77 </div>
78 <div class="meta-item">
79 <el-icon><Clock /></el-icon>
80 <span>上传时间:{{ formatDateTime(torrentInfo.createdAt) }}</span>
81 </div>
82 <div class="meta-item">
83 <el-icon><Document /></el-icon>
84 <span>文件大小:{{ formatFileSize(torrentInfo.size) }}</span>
85 </div>
208159515458d95702025-06-09 14:46:58 +080086 <!-- <div class="meta-item">
vulgar52019469e5c2025-06-09 01:34:47 +080087 <el-icon><Files /></el-icon>
88 <span>完成次数:{{ torrentInfo.finishes }} 次</span>
208159515458d95702025-06-09 14:46:58 +080089 </div> -->
vulgar52019469e5c2025-06-09 01:34:47 +080090 <div class="meta-item">
91 <el-icon><Star /></el-icon>
92 <span>推广策略:{{ torrentInfo.promotionPolicy?.displayName || '默认' }}</span>
93 </div>
94 </div>
95
96 <div class="torrent-stats">
97 <div class="stat-item seeders">
98 <span class="stat-number">{{ peerStats.seeders }}</span>
99 <span class="stat-label">做种</span>
100 </div>
101 <div class="stat-item leechers">
102 <span class="stat-number">{{ peerStats.leechers }}</span>
103 <span class="stat-label">下载</span>
104 </div>
105 <div class="stat-item downloads">
106 <span class="stat-number">{{ torrentInfo.finishes }}</span>
107 <span class="stat-label">完成</span>
108 </div>
109 </div>
110
111 <div class="action-buttons">
112 <el-button
113 type="primary"
114 size="large"
115 :icon="Download"
116 @click="handleDownload"
117 :loading="downloading"
118 >
119 {{ downloading ? '准备中...' : '下载种子' }}
120 </el-button>
208159515458d95702025-06-09 14:46:58 +0800121 <!-- <el-button
vulgar52019469e5c2025-06-09 01:34:47 +0800122 type="success"
123 size="large"
124 :icon="Star"
125 @click="handleFavorite"
126 >
127 {{ isFavorited ? '已收藏' : '收藏' }}
128 </el-button>
129 <el-button
130 type="warning"
131 size="large"
132 :icon="Flag"
133 @click="handleReport"
134 >
135 举报
208159515458d95702025-06-09 14:46:58 +0800136 </el-button> -->
vulgar52019469e5c2025-06-09 01:34:47 +0800137 </div>
138 </div>
139 </div>
140 </div>
141
142 <!-- 详细信息选项卡 -->
143 <div class="detail-tabs">
144 <el-tabs v-model="activeTab" type="border-card">
145 <!-- 种子描述 -->
146 <el-tab-pane label="详细描述" name="description">
147 <div class="description-content">
148 <div v-if="torrentInfo.description" v-html="formatDescription(torrentInfo.description)"></div>
149 <div v-else class="no-description">暂无详细描述</div>
150 </div>
151 </el-tab-pane>
152
153 <!-- 文件列表 -->
154 <el-tab-pane label="文件列表" name="files" lazy>
155 <div class="files-list">
156 <div v-if="torrentFiles.length > 0">
157 <el-table :data="torrentFiles" stripe>
158 <el-table-column label="文件名" prop="name" min-width="400">
159 <template #default="{ row }">
160 <div class="file-name">
161 <el-icon v-if="row.type === 'folder'"><Folder /></el-icon>
162 <el-icon v-else><Document /></el-icon>
163 <span>{{ row.name }}</span>
164 </div>
165 </template>
166 </el-table-column>
167 <el-table-column label="大小" prop="size" width="120" align="right" />
168 <el-table-column label="路径" prop="path" min-width="300" />
169 </el-table>
170 </div>
171 <div v-else class="no-files">
172 <el-empty description="文件列表加载中..." />
173 </div>
174 </div>
175 </el-tab-pane>
176
177 <!-- 用户活动 -->
178 <el-tab-pane label="用户活动" name="activity">
179 <div class="activity-section">
180 <div class="activity-stats">
181 <div class="stats-grid">
182 <div class="stat-card">
183 <h3>做种用户</h3>
184 <p class="stat-number">{{ peerStats.seeders }}</p>
185 </div>
186 <div class="stat-card">
187 <h3>下载用户</h3>
188 <p class="stat-number">{{ peerStats.leechers }}</p>
189 </div>
190 <div class="stat-card">
191 <h3>完成用户</h3>
192 <p class="stat-number">{{ torrentInfo.finishes }}</p>
193 </div>
194 </div>
195 </div>
196
197 <div class="user-lists">
198 <el-tabs v-model="activityTab" type="card">
199 <el-tab-pane label="做种用户" name="seeders">
200 <el-table :data="seedersList" max-height="400">
201 <el-table-column label="用户" prop="username" />
202 <el-table-column label="上传量" prop="uploaded" />
203 <el-table-column label="下载量" prop="downloaded" />
204 <el-table-column label="分享率" prop="ratio" />
205 <el-table-column label="做种时间" prop="seedTime" />
206 </el-table>
207 </el-tab-pane>
208
209 <el-tab-pane label="下载用户" name="leechers">
210 <el-table :data="leechersList" max-height="400">
211 <el-table-column label="用户" prop="username" />
212 <el-table-column label="进度" prop="progress">
213 <template #default="{ row }">
214 <el-progress :percentage="row.progress" :stroke-width="6" />
215 </template>
216 </el-table-column>
217 <el-table-column label="下载速度" prop="downloadSpeed" />
218 <el-table-column label="剩余时间" prop="eta" />
219 </el-table>
220 </el-tab-pane>
221 </el-tabs>
222 </div>
223 </div>
224 </el-tab-pane>
225
226 <!-- 评论区 -->
227 <el-tab-pane label="评论" name="comments">
228 <div class="comments-section">
229 <!-- 发表评论 -->
230 <div class="comment-form">
231 <el-input
232 v-model="newComment"
233 type="textarea"
234 :rows="4"
235 placeholder="发表你的评论..."
236 maxlength="500"
237 show-word-limit
238 />
239 <div class="comment-actions">
240 <el-button type="primary" @click="submitComment" :loading="submittingComment">
241 发表评论
242 </el-button>
243 </div>
244 </div>
245
246 <!-- 评论列表 -->
247 <div class="comments-list">
248 <div
249 v-for="comment in comments"
250 :key="comment.id"
251 class="comment-item"
252 >
253 <div class="comment-avatar">
254 <el-avatar :size="40">{{ comment.username.charAt(0) }}</el-avatar>
255 </div>
256 <div class="comment-content">
257 <div class="comment-header">
258 <span class="comment-username">{{ comment.username }}</span>
259 <span class="comment-time">{{ formatDateTime(comment.time) }}</span>
260 </div>
261 <div class="comment-text">{{ comment.content }}</div>
262 <div class="comment-actions">
263 <el-button type="text" size="small" @click="likeComment(comment.id)">
264 <el-icon><Like /></el-icon>
265 {{ comment.likes || 0 }}
266 </el-button>
267 <el-button type="text" size="small" @click="replyComment(comment.id)">
268 回复
269 </el-button>
270 </div>
271 </div>
272 </div>
273
274 <div v-if="comments.length === 0" class="no-comments">
275 暂无评论,快来发表第一条评论吧!
276 </div>
277 </div>
278 </div>
279 </el-tab-pane>
280 </el-tabs>
281 </div>
282 </div>
283
284 <!-- 错误状态 -->
285 <div v-else class="error-container">
286 <el-empty description="种子信息加载失败">
287 <el-button type="primary" @click="retry">重试</el-button>
288 </el-empty>
289 </div>
290 </div>
291 </div>
292</template>
293
294<script>
295import { ref, onMounted, computed } from 'vue'
296import { useRoute, useRouter } from 'vue-router'
297import { ElMessage, ElMessageBox } from 'element-plus'
298import {
299 ArrowLeft,
300 Download,
301 Star,
302 Flag,
303 User,
304 Clock,
305 Document,
306 Files,
307 Picture,
308 Folder,
309 Like
310} from '@element-plus/icons-vue'
311import axios from 'axios'
312
313export default {
314 name: 'TorrentDetailView',
315 components: {
316 ArrowLeft,
317 Download,
318 Star,
319 Flag,
320 User,
321 Clock,
322 Document,
323 Files,
324 Picture,
325 Folder,
326 Like
327 },
328 setup() {
329 const route = useRoute()
330 const router = useRouter()
331
332 const loading = ref(true)
333 const activeTab = ref('description')
334 const activityTab = ref('seeders')
335 const downloading = ref(false)
336 const isFavorited = ref(false)
337 const submittingComment = ref(false)
338 const newComment = ref('')
339
340 const torrentInfo = ref(null)
341 const torrentFiles = ref([])
342 const peerStats = ref({
343 seeders: 0,
344 leechers: 0
345 })
346
347 const seedersList = ref([])
348 const leechersList = ref([])
349 const comments = ref([])
350
351 // 解析标签
352 const parsedTags = computed(() => {
353 if (!torrentInfo.value?.tag || !Array.isArray(torrentInfo.value.tag)) {
354 return []
355 }
356
357 const tags = []
358 torrentInfo.value.tag.forEach(tagString => {
359 try {
360 const parsed = JSON.parse(tagString)
361 if (Array.isArray(parsed)) {
362 tags.push(...parsed)
363 } else {
364 tags.push(parsed)
365 }
366 } catch (e) {
367 tags.push(tagString)
368 }
369 })
370 return tags
371 })
372
373 onMounted(() => {
374 const infoHash = route.params.infoHash
375 if (infoHash) {
376 fetchTorrentDetail(infoHash)
377 } else {
378 ElMessage.error('缺少种子标识符')
379 router.back()
380 }
381 })
382
383 const fetchTorrentDetail = async (infoHash) => {
384 try {
385 loading.value = true
386
387 // 获取种子详情
388 const response = await axios.get(`/api/torrent/view/${infoHash}`)
389
390 if (response.data.code === 0) {
391 torrentInfo.value = response.data
392
393 // 获取文件列表(如果有相关API)
394 await fetchTorrentFiles(infoHash)
395
396 // 获取用户活动数据(如果有相关API)
397 await fetchPeerStats(infoHash)
398
399 // 获取评论(如果有相关API)
400 await fetchComments(infoHash)
401 } else {
402 throw new Error(response.data.message || '获取种子详情失败')
403 }
404 } catch (error) {
405 console.error('获取种子详情失败:', error)
406 ElMessage.error(error.message || '获取种子详情失败')
407 torrentInfo.value = null
408 } finally {
409 loading.value = false
410 }
411 }
412
413 const fetchTorrentFiles = async (infoHash) => {
414 try {
415 if (torrentInfo.value?.fileList) {
416 // 将文件列表转换为所需的格式
417 torrentFiles.value = Object.entries(torrentInfo.value.fileList).map(([path, size]) => ({
418 name: path.split('/').pop() || path, // 获取文件名
419 type: 'file',
420 size: formatFileSize(size),
421 path: path.split('/').slice(0, -1).join('/') || '/' // 获取文件路径
422 }))
423 } else {
424 // 如果是单文件种子,使用种子标题作为文件名
425 torrentFiles.value = [
426 {
427 name: torrentInfo.value?.title || 'unknown',
428 type: 'file',
429 size: formatFileSize(torrentInfo.value?.size || 0),
430 path: '/'
431 }
432 ]
433 }
434 } catch (error) {
435 console.error('获取文件列表失败:', error)
436 }
437 }
438
439 const fetchPeerStats = async (infoHash) => {
440 try {
441 // 这里应该调用获取用户活动数据的API
442 // const response = await axios.get(`http://localhost:8081/api/torrent/${infoHash}/peers`)
443 // peerStats.value = response.data
444
445 // 临时模拟数据
446 peerStats.value = {
447 seeders: Math.floor(Math.random() * 50) + 10,
448 leechers: Math.floor(Math.random() * 30) + 5
449 }
450
451 // 模拟用户列表
452 seedersList.value = [
453 { username: 'SeedMaster', uploaded: '2.5 TB', downloaded: '850 GB', ratio: '3.02', seedTime: '15天' }
454 ]
455
456 leechersList.value = [
457 { username: 'NewUser123', progress: 65, downloadSpeed: '15.2 MB/s', eta: '2小时15分' }
458 ]
459 } catch (error) {
460 console.error('获取用户活动数据失败:', error)
461 }
462 }
463
464 const fetchComments = async (infoHash) => {
465 try {
466 // 这里应该调用获取评论的API
467 // const response = await axios.get(`http://localhost:8081/api/torrent/${infoHash}/comments`)
468 // comments.value = response.data
469
470 // 临时模拟数据
471 comments.value = []
472 } catch (error) {
473 console.error('获取评论失败:', error)
474 }
475 }
476
477 const formatDateTime = (timestamp) => {
478 if (!timestamp) return '未知'
479 const date = new Date(timestamp)
480 return date.toLocaleString('zh-CN', {
481 year: 'numeric',
482 month: '2-digit',
483 day: '2-digit',
484 hour: '2-digit',
485 minute: '2-digit'
486 })
487 }
488
489 const formatFileSize = (bytes) => {
490 if (!bytes || bytes === 0) return '0 B'
491
492 const k = 1024
493 const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
494 const i = Math.floor(Math.log(bytes) / Math.log(k))
495
496 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
497 }
498
499 const formatDescription = (description) => {
500 if (!description) return ''
501 return description.replace(/\n/g, '<br>')
502 }
503
504 const getCategoryType = (categorySlug) => {
505 const types = {
506 'os': 'primary',
507 'movie': 'primary',
508 'tv': 'info',
509 'music': 'success',
510 'software': 'warning',
511 'game': 'danger'
512 }
513 return types[categorySlug] || 'default'
514 }
515
516 const handleDownload = async () => {
517 if (!torrentInfo.value?.infoHash) return
518
519 downloading.value = true
520 try {
521 // 调用下载种子文件的API
522 const response = await axios.get(
523 `/api/torrent/download/${torrentInfo.value.infoHash}`,
524 {
525 responseType: 'blob',
526 // 如果需要传递passkey,可以在这里添加params
527 params: {
528 // passkey: userStore.passkey // 如果你有用户store存储passkey
529 }
530 }
531 )
532
533 // 从响应头中获取文件名,如果没有则使用默认格式
534 let fileName = response.headers?.['content-disposition']?.split('filename=')[1]
535 if (!fileName) {
536 // 使用默认的文件名格式
537 fileName = `${torrentInfo.value.title}.torrent`
538 } else {
539 // 解码文件名
540 fileName = decodeURIComponent(fileName)
541 }
542
543 // 创建下载链接
544 const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/x-bittorrent' }))
545 const link = document.createElement('a')
546 link.href = url
547 link.download = fileName
548 document.body.appendChild(link)
549 link.click()
550 document.body.removeChild(link)
551 window.URL.revokeObjectURL(url)
552
553 ElMessage.success('种子文件下载完成')
554 } catch (error) {
555 console.error('下载失败:', error)
556 ElMessage.error(error.response?.data?.message || '下载失败,请稍后重试')
557 } finally {
558 downloading.value = false
559 }
560 }
561
562 const handleFavorite = () => {
563 isFavorited.value = !isFavorited.value
564 ElMessage.success(isFavorited.value ? '已添加到收藏' : '已取消收藏')
565 }
566
567 const handleReport = async () => {
568 try {
569 await ElMessageBox.prompt('请说明举报原因', '举报内容', {
570 confirmButtonText: '提交举报',
571 cancelButtonText: '取消',
572 inputType: 'textarea',
573 inputPlaceholder: '请详细说明举报原因...'
574 })
575
576 ElMessage.success('举报已提交,我们会尽快处理')
577 } catch {
578 // 用户取消
579 }
580 }
581
582 const submitComment = async () => {
583 if (!newComment.value.trim()) {
584 ElMessage.warning('请输入评论内容')
585 return
586 }
587
588 submittingComment.value = true
589 try {
590 // 这里应该调用提交评论的API
591 // await axios.post(`http://localhost:8081/api/torrent/${torrentInfo.value.infoHash}/comments`, {
592 // content: newComment.value
593 // })
594
595 // 模拟提交
596 await new Promise(resolve => setTimeout(resolve, 1000))
597
598 const comment = {
599 id: Date.now(),
600 username: localStorage.getItem('username') || '用户',
601 content: newComment.value,
602 time: new Date().toISOString(),
603 likes: 0
604 }
605
606 comments.value.unshift(comment)
607 newComment.value = ''
608
609 ElMessage.success('评论发表成功')
610 } catch (error) {
611 console.error('发表评论失败:', error)
612 ElMessage.error('发表评论失败')
613 } finally {
614 submittingComment.value = false
615 }
616 }
617
618 const likeComment = (commentId) => {
619 const comment = comments.value.find(c => c.id === commentId)
620 if (comment) {
621 comment.likes = (comment.likes || 0) + 1
622 ElMessage.success('点赞成功')
623 }
624 }
625
626 const replyComment = (commentId) => {
627 ElMessage.info('回复功能开发中...')
628 }
629
630 const retry = () => {
631 const infoHash = route.params.infoHash
632 if (infoHash) {
633 fetchTorrentDetail(infoHash)
634 }
635 }
636
637 return {
638 loading,
639 activeTab,
640 activityTab,
641 downloading,
642 isFavorited,
643 submittingComment,
644 newComment,
645 torrentInfo,
646 torrentFiles,
647 peerStats,
648 parsedTags,
649 seedersList,
650 leechersList,
651 comments,
652 formatDateTime,
653 formatFileSize,
654 formatDescription,
655 getCategoryType,
656 handleDownload,
657 handleFavorite,
658 handleReport,
659 submitComment,
660 likeComment,
661 replyComment,
662 retry,
663 ArrowLeft,
664 Download,
665 Star,
666 Flag,
667 User,
668 Clock,
669 Document,
670 Files,
671 Picture,
672 Folder,
673 Like
674 }
675 }
676}
677</script>
678
679<style lang="scss" scoped>
680.torrent-detail-page {
681 max-width: 1200px;
682 margin: 0 auto;
683 padding: 24px;
684 background: #f5f5f5;
685 min-height: 100vh;
686}
687
688.loading-container, .error-container {
689 background: #fff;
690 border-radius: 12px;
691 padding: 40px;
692 text-align: center;
693}
694
695.back-button {
696 margin-bottom: 16px;
697}
698
699.torrent-header {
700 background: #fff;
701 border-radius: 12px;
702 padding: 32px;
703 margin-bottom: 24px;
704 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
705
706 .header-content {
707 display: flex;
708 gap: 32px;
709
710 .torrent-cover {
711 flex-shrink: 0;
712
713 .cover-image {
714 width: 200px;
715 height: 280px;
716 border-radius: 8px;
717 object-fit: cover;
718 }
719
720 .image-placeholder {
721 width: 200px;
722 height: 280px;
723 background: #f5f5f5;
724 border-radius: 8px;
725 display: flex;
726 flex-direction: column;
727 align-items: center;
728 justify-content: center;
729 color: #999;
730
731 span {
732 margin-top: 8px;
733 font-size: 14px;
734 }
735 }
736 }
737
738 .torrent-info {
739 flex: 1;
740
741 .category-tag {
742 margin-bottom: 16px;
743
744 .el-tag {
745 margin-right: 8px;
746 }
747 }
748
749 .torrent-title {
750 font-size: 28px;
751 font-weight: 600;
752 color: #2c3e50;
753 margin: 0 0 16px 0;
754 line-height: 1.3;
755 }
756
757 .torrent-tags {
758 margin-bottom: 20px;
759
760 .el-tag {
761 margin: 0 8px 8px 0;
762 }
763 }
764
765 .torrent-meta {
766 margin-bottom: 20px;
767
768 .meta-item {
769 display: flex;
770 align-items: center;
771 gap: 8px;
772 margin-bottom: 8px;
773 color: #7f8c8d;
774 font-size: 14px;
775
776 .el-icon {
777 color: #909399;
778 }
779 }
780 }
781
782 .torrent-stats {
783 display: flex;
784 gap: 32px;
785 margin-bottom: 24px;
786
787 .stat-item {
788 text-align: center;
789
790 .stat-number {
791 display: block;
792 font-size: 24px;
793 font-weight: 600;
794 margin-bottom: 4px;
795 color: #2c3e50;
796 }
797
798 .stat-label {
799 font-size: 14px;
800 color: #909399;
801 }
802
803 &.seeders .stat-number { color: #67c23a; }
804 &.leechers .stat-number { color: #f56c6c; }
805 &.downloads .stat-number { color: #409eff; }
806 }
807 }
808
809 .action-buttons {
810 display: flex;
811 gap: 12px;
812 flex-wrap: wrap;
813 }
814 }
815 }
816}
817
818.detail-tabs {
819 background: #fff;
820 border-radius: 12px;
821 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
822
823 :deep(.el-tabs__content) {
824 padding: 24px;
825 }
826
827 .description-content {
828 line-height: 1.6;
829
830 :deep(h3) {
831 color: #2c3e50;
832 font-size: 18px;
833 font-weight: 600;
834 margin: 24px 0 12px 0;
835
836 &:first-child {
837 margin-top: 0;
838 }
839 }
840
841 :deep(p) {
842 margin-bottom: 12px;
843 color: #5a6c7d;
844 }
845
846 :deep(ul) {
847 margin: 12px 0;
848 padding-left: 20px;
849
850 li {
851 margin-bottom: 8px;
852 color: #5a6c7d;
853 }
854 }
855
856 .no-description {
857 text-align: center;
858 color: #909399;
859 padding: 40px 0;
860 }
861 }
862
863 .files-list {
864 .file-name {
865 display: flex;
866 align-items: center;
867 gap: 8px;
868
869 .el-icon {
870 color: #909399;
871 }
872 }
873
874 .no-files {
875 padding: 40px 0;
876 }
877 }
878
879 .activity-section {
880 .activity-stats {
881 margin-bottom: 24px;
882
883 .stats-grid {
884 display: grid;
885 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
886 gap: 16px;
887
888 .stat-card {
889 background: #f8f9fa;
890 padding: 20px;
891 border-radius: 8px;
892 text-align: center;
893
894 h3 {
895 font-size: 14px;
896 color: #909399;
897 margin: 0 0 8px 0;
898 }
899
900 .stat-number {
901 font-size: 24px;
902 font-weight: 600;
903 color: #2c3e50;
904 }
905 }
906 }
907 }
908 }
909
910 .comments-section {
911 .comment-form {
912 margin-bottom: 32px;
913
914 .comment-actions {
915 margin-top: 12px;
916 text-align: right;
917 }
918 }
919
920 .comments-list {
921 .comment-item {
922 display: flex;
923 gap: 16px;
924 margin-bottom: 24px;
925 padding-bottom: 24px;
926 border-bottom: 1px solid #f0f0f0;
927
928 &:last-child {
929 border-bottom: none;
930 margin-bottom: 0;
931 padding-bottom: 0;
932 }
933
934 .comment-content {
935 flex: 1;
936
937 .comment-header {
938 display: flex;
939 align-items: center;
940 gap: 12px;
941 margin-bottom: 8px;
942
943 .comment-username {
944 font-weight: 600;
945 color: #2c3e50;
946 }
947
948 .comment-time {
949 font-size: 12px;
950 color: #909399;
951 }
952 }
953
954 .comment-text {
955 color: #5a6c7d;
956 line-height: 1.5;
957 margin-bottom: 12px;
958 }
959
960 .comment-actions {
961 .el-button {
962 padding: 0;
963 margin-right: 16px;
964
965 .el-icon {
966 margin-right: 4px;
967 }
968 }
969 }
970 }
971 }
972
973 .no-comments {
974 text-align: center;
975 color: #909399;
976 padding: 40px 0;
977 }
978 }
979 }
980}
981
982// 响应式设计
983@media (max-width: 768px) {
984 .torrent-detail-page {
985 padding: 16px;
986 }
987
988 .torrent-header .header-content {
989 flex-direction: column;
990 text-align: center;
991
992 .torrent-cover {
993 align-self: center;
994 }
995
996 .torrent-stats {
997 justify-content: center;
998 }
999
1000 .action-buttons {
1001 justify-content: center;
1002 }
1003 }
1004
1005 .activity-section .stats-grid {
1006 grid-template-columns: 1fr;
1007 }
1008
1009 .comment-item {
1010 flex-direction: column;
1011 gap: 12px;
1012 }
1013}
1014</style>