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