blob: 8b1c8fdd1eb039afb09d78825c6a6d17866c0d89 [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">
vulgar5201a01f5ee2025-06-09 22:38:59 +080098 <!-- <div class="stat-item seeders">
vulgar52019469e5c2025-06-09 01:34:47 +080099 <span class="stat-number">{{ peerStats.seeders }}</span>
100 <span class="stat-label">做种</span>
vulgar5201a01f5ee2025-06-09 22:38:59 +0800101 </div> -->
102 <!-- <div class="stat-item leechers">
vulgar52019469e5c2025-06-09 01:34:47 +0800103 <span class="stat-number">{{ peerStats.leechers }}</span>
104 <span class="stat-label">下载</span>
vulgar5201a01f5ee2025-06-09 22:38:59 +0800105 </div> -->
vulgar52019469e5c2025-06-09 01:34:47 +0800106 <div class="stat-item downloads">
vulgar5201a01f5ee2025-06-09 22:38:59 +0800107 <span class="stat-number">{{ peerStats.downloads }}</span>
108 <span class="stat-label">总下载</span>
vulgar52019469e5c2025-06-09 01:34:47 +0800109 </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,
vulgar5201a01f5ee2025-06-09 22:38:59 +0800347 leechers: 0,
348 downloads: 0
vulgar52019469e5c2025-06-09 01:34:47 +0800349 })
350
351 const seedersList = ref([])
352 const leechersList = ref([])
353 const comments = ref([])
354
355 // 解析标签
356 const parsedTags = computed(() => {
357 if (!torrentInfo.value?.tag || !Array.isArray(torrentInfo.value.tag)) {
358 return []
359 }
360
361 const tags = []
362 torrentInfo.value.tag.forEach(tagString => {
363 try {
364 const parsed = JSON.parse(tagString)
365 if (Array.isArray(parsed)) {
366 tags.push(...parsed)
367 } else {
368 tags.push(parsed)
369 }
370 } catch (e) {
371 tags.push(tagString)
372 }
373 })
374 return tags
375 })
376
377 onMounted(() => {
378 const infoHash = route.params.infoHash
379 if (infoHash) {
380 fetchTorrentDetail(infoHash)
381 } else {
382 ElMessage.error('缺少种子标识符')
383 router.back()
384 }
385 })
386
387 const fetchTorrentDetail = async (infoHash) => {
388 try {
389 loading.value = true
390
391 // 获取种子详情
392 const response = await axios.get(`/api/torrent/view/${infoHash}`)
393
394 if (response.data.code === 0) {
395 torrentInfo.value = response.data
396
397 // 获取文件列表(如果有相关API)
398 await fetchTorrentFiles(infoHash)
399
400 // 获取用户活动数据(如果有相关API)
401 await fetchPeerStats(infoHash)
402
vulgar5201a01f5ee2025-06-09 22:38:59 +0800403 // 获取下载数
404 await fetchDownloadCount(infoHash)
405
vulgar52019469e5c2025-06-09 01:34:47 +0800406 // 获取评论(如果有相关API)
407 await fetchComments(infoHash)
408 } else {
409 throw new Error(response.data.message || '获取种子详情失败')
410 }
411 } catch (error) {
412 console.error('获取种子详情失败:', error)
413 ElMessage.error(error.message || '获取种子详情失败')
414 torrentInfo.value = null
415 } finally {
416 loading.value = false
417 }
418 }
419
420 const fetchTorrentFiles = async (infoHash) => {
421 try {
422 if (torrentInfo.value?.fileList) {
423 // 将文件列表转换为所需的格式
424 torrentFiles.value = Object.entries(torrentInfo.value.fileList).map(([path, size]) => ({
425 name: path.split('/').pop() || path, // 获取文件名
426 type: 'file',
427 size: formatFileSize(size),
428 path: path.split('/').slice(0, -1).join('/') || '/' // 获取文件路径
429 }))
430 } else {
431 // 如果是单文件种子,使用种子标题作为文件名
432 torrentFiles.value = [
433 {
434 name: torrentInfo.value?.title || 'unknown',
435 type: 'file',
436 size: formatFileSize(torrentInfo.value?.size || 0),
437 path: '/'
438 }
439 ]
440 }
441 } catch (error) {
442 console.error('获取文件列表失败:', error)
443 }
444 }
445
446 const fetchPeerStats = async (infoHash) => {
447 try {
448 // 这里应该调用获取用户活动数据的API
449 // const response = await axios.get(`http://localhost:8081/api/torrent/${infoHash}/peers`)
450 // peerStats.value = response.data
451
452 // 临时模拟数据
453 peerStats.value = {
454 seeders: Math.floor(Math.random() * 50) + 10,
455 leechers: Math.floor(Math.random() * 30) + 5
456 }
457
458 // 模拟用户列表
459 seedersList.value = [
460 { username: 'SeedMaster', uploaded: '2.5 TB', downloaded: '850 GB', ratio: '3.02', seedTime: '15天' }
461 ]
462
463 leechersList.value = [
464 { username: 'NewUser123', progress: 65, downloadSpeed: '15.2 MB/s', eta: '2小时15分' }
465 ]
466 } catch (error) {
467 console.error('获取用户活动数据失败:', error)
468 }
469 }
470
vulgar5201a01f5ee2025-06-09 22:38:59 +0800471 const fetchDownloadCount = async (infoHash) => {
472 try {
473 const response = await axios.get(`/api/torrent/${infoHash}/downloads`)
474 if (response.status === 200) {
475 peerStats.value.downloads = response.data
476 }
477 } catch (error) {
478 console.error('获取下载数失败:', error)
479 peerStats.value.downloads = 0
480 }
481 }
482
vulgar52019469e5c2025-06-09 01:34:47 +0800483 const fetchComments = async (infoHash) => {
484 try {
485 // 这里应该调用获取评论的API
486 // const response = await axios.get(`http://localhost:8081/api/torrent/${infoHash}/comments`)
487 // comments.value = response.data
488
489 // 临时模拟数据
490 comments.value = []
491 } catch (error) {
492 console.error('获取评论失败:', error)
493 }
494 }
495
496 const formatDateTime = (timestamp) => {
497 if (!timestamp) return '未知'
498 const date = new Date(timestamp)
499 return date.toLocaleString('zh-CN', {
500 year: 'numeric',
501 month: '2-digit',
502 day: '2-digit',
503 hour: '2-digit',
504 minute: '2-digit'
505 })
506 }
507
508 const formatFileSize = (bytes) => {
509 if (!bytes || bytes === 0) return '0 B'
510
511 const k = 1024
512 const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
513 const i = Math.floor(Math.log(bytes) / Math.log(k))
514
515 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
516 }
517
518 const formatDescription = (description) => {
519 if (!description) return ''
520 return description.replace(/\n/g, '<br>')
521 }
522
523 const getCategoryType = (categorySlug) => {
524 const types = {
525 'os': 'primary',
526 'movie': 'primary',
527 'tv': 'info',
528 'music': 'success',
529 'software': 'warning',
530 'game': 'danger'
531 }
532 return types[categorySlug] || 'default'
533 }
534
535 const handleDownload = async () => {
536 if (!torrentInfo.value?.infoHash) return
537
538 downloading.value = true
539 try {
540 // 调用下载种子文件的API
541 const response = await axios.get(
542 `/api/torrent/download/${torrentInfo.value.infoHash}`,
543 {
544 responseType: 'blob',
545 // 如果需要传递passkey,可以在这里添加params
546 params: {
547 // passkey: userStore.passkey // 如果你有用户store存储passkey
548 }
549 }
550 )
551
vulgar5201a01f5ee2025-06-09 22:38:59 +0800552 // 检查响应类型是否为JSON(表示发生了错误)
553 const contentType = response.headers['content-type'];
554 if (contentType && contentType.includes('application/json')) {
555 // 将blob转换为json以读取错误信息
556 const errorText = await response.data.text();
557 const errorData = JSON.parse(errorText);
558 throw new Error(errorData.message || '下载失败');
559 }
560
vulgar52019469e5c2025-06-09 01:34:47 +0800561 // 从响应头中获取文件名,如果没有则使用默认格式
562 let fileName = response.headers?.['content-disposition']?.split('filename=')[1]
563 if (!fileName) {
564 // 使用默认的文件名格式
565 fileName = `${torrentInfo.value.title}.torrent`
566 } else {
567 // 解码文件名
568 fileName = decodeURIComponent(fileName)
569 }
570
571 // 创建下载链接
572 const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/x-bittorrent' }))
573 const link = document.createElement('a')
574 link.href = url
575 link.download = fileName
576 document.body.appendChild(link)
577 link.click()
578 document.body.removeChild(link)
579 window.URL.revokeObjectURL(url)
580
581 ElMessage.success('种子文件下载完成')
582 } catch (error) {
583 console.error('下载失败:', error)
vulgar5201a01f5ee2025-06-09 22:38:59 +0800584 // 根据错误类型显示不同的错误信息
585 let errorMessage = '下载失败,请稍后重试';
586
587 if (error.response) {
588 const status = error.response.status;
589 const data = error.response.data;
590
591 switch(status) {
592 case 401:
593 errorMessage = '认证失败,请检查登录状态或passkey是否正确';
594 break;
595 case 403:
596 if (data.message?.includes('share ratio')) {
597 errorMessage = '分享率不足,无法下载';
598 } else if (data.message?.includes('torrent:download_review')) {
599 errorMessage = '该种子正在审核中,您没有权限下载';
600 } else {
601 errorMessage = '您没有权限下载此种子';
602 }
603 break;
604 case 404:
605 if (data.message?.includes('torrent not registered')) {
606 errorMessage = '该种子未在服务器注册';
607 } else if (data.message?.includes('file are missing')) {
608 errorMessage = '种子文件丢失,请联系管理员';
609 } else {
610 errorMessage = '种子不存在';
611 }
612 break;
613 default:
614 errorMessage = data.message || '下载失败,请稍后重试';
615 }
616 }
617
618 ElMessage.error(errorMessage)
vulgar52019469e5c2025-06-09 01:34:47 +0800619 } finally {
620 downloading.value = false
621 }
622 }
623
624 const handleFavorite = () => {
625 isFavorited.value = !isFavorited.value
626 ElMessage.success(isFavorited.value ? '已添加到收藏' : '已取消收藏')
627 }
628
629 const handleReport = async () => {
630 try {
631 await ElMessageBox.prompt('请说明举报原因', '举报内容', {
632 confirmButtonText: '提交举报',
633 cancelButtonText: '取消',
634 inputType: 'textarea',
635 inputPlaceholder: '请详细说明举报原因...'
636 })
637
638 ElMessage.success('举报已提交,我们会尽快处理')
639 } catch {
640 // 用户取消
641 }
642 }
643
644 const submitComment = async () => {
645 if (!newComment.value.trim()) {
646 ElMessage.warning('请输入评论内容')
647 return
648 }
649
650 submittingComment.value = true
651 try {
652 // 这里应该调用提交评论的API
653 // await axios.post(`http://localhost:8081/api/torrent/${torrentInfo.value.infoHash}/comments`, {
654 // content: newComment.value
655 // })
656
657 // 模拟提交
658 await new Promise(resolve => setTimeout(resolve, 1000))
659
660 const comment = {
661 id: Date.now(),
662 username: localStorage.getItem('username') || '用户',
663 content: newComment.value,
664 time: new Date().toISOString(),
665 likes: 0
666 }
667
668 comments.value.unshift(comment)
669 newComment.value = ''
670
671 ElMessage.success('评论发表成功')
672 } catch (error) {
673 console.error('发表评论失败:', error)
674 ElMessage.error('发表评论失败')
675 } finally {
676 submittingComment.value = false
677 }
678 }
679
680 const likeComment = (commentId) => {
681 const comment = comments.value.find(c => c.id === commentId)
682 if (comment) {
683 comment.likes = (comment.likes || 0) + 1
684 ElMessage.success('点赞成功')
685 }
686 }
687
688 const replyComment = (commentId) => {
689 ElMessage.info('回复功能开发中...')
690 }
691
692 const retry = () => {
693 const infoHash = route.params.infoHash
694 if (infoHash) {
695 fetchTorrentDetail(infoHash)
696 }
697 }
698
699 return {
700 loading,
701 activeTab,
702 activityTab,
703 downloading,
704 isFavorited,
705 submittingComment,
706 newComment,
707 torrentInfo,
708 torrentFiles,
709 peerStats,
710 parsedTags,
711 seedersList,
712 leechersList,
713 comments,
714 formatDateTime,
715 formatFileSize,
716 formatDescription,
717 getCategoryType,
718 handleDownload,
719 handleFavorite,
720 handleReport,
721 submitComment,
722 likeComment,
723 replyComment,
724 retry,
725 ArrowLeft,
726 Download,
727 Star,
728 Flag,
729 User,
730 Clock,
731 Document,
732 Files,
733 Picture,
734 Folder,
735 Like
736 }
737 }
738}
739</script>
740
741<style lang="scss" scoped>
742.torrent-detail-page {
743 max-width: 1200px;
744 margin: 0 auto;
745 padding: 24px;
746 background: #f5f5f5;
747 min-height: 100vh;
748}
749
750.loading-container, .error-container {
751 background: #fff;
752 border-radius: 12px;
753 padding: 40px;
754 text-align: center;
755}
756
757.back-button {
758 margin-bottom: 16px;
759}
760
761.torrent-header {
762 background: #fff;
763 border-radius: 12px;
764 padding: 32px;
765 margin-bottom: 24px;
766 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
767
768 .header-content {
769 display: flex;
770 gap: 32px;
771
772 .torrent-cover {
773 flex-shrink: 0;
774
775 .cover-image {
776 width: 200px;
777 height: 280px;
778 border-radius: 8px;
779 object-fit: cover;
780 }
781
782 .image-placeholder {
783 width: 200px;
784 height: 280px;
785 background: #f5f5f5;
786 border-radius: 8px;
787 display: flex;
788 flex-direction: column;
789 align-items: center;
790 justify-content: center;
791 color: #999;
792
793 span {
794 margin-top: 8px;
795 font-size: 14px;
796 }
797 }
798 }
799
800 .torrent-info {
801 flex: 1;
802
803 .category-tag {
804 margin-bottom: 16px;
805
806 .el-tag {
807 margin-right: 8px;
808 }
809 }
810
811 .torrent-title {
812 font-size: 28px;
813 font-weight: 600;
814 color: #2c3e50;
815 margin: 0 0 16px 0;
816 line-height: 1.3;
817 }
818
819 .torrent-tags {
820 margin-bottom: 20px;
821
822 .el-tag {
823 margin: 0 8px 8px 0;
824 }
825 }
826
827 .torrent-meta {
828 margin-bottom: 20px;
829
830 .meta-item {
831 display: flex;
832 align-items: center;
833 gap: 8px;
834 margin-bottom: 8px;
835 color: #7f8c8d;
836 font-size: 14px;
837
838 .el-icon {
839 color: #909399;
840 }
841 }
842 }
843
844 .torrent-stats {
845 display: flex;
846 gap: 32px;
847 margin-bottom: 24px;
848
849 .stat-item {
850 text-align: center;
851
852 .stat-number {
853 display: block;
854 font-size: 24px;
855 font-weight: 600;
856 margin-bottom: 4px;
857 color: #2c3e50;
858 }
859
860 .stat-label {
861 font-size: 14px;
862 color: #909399;
863 }
864
865 &.seeders .stat-number { color: #67c23a; }
866 &.leechers .stat-number { color: #f56c6c; }
867 &.downloads .stat-number { color: #409eff; }
868 }
869 }
870
871 .action-buttons {
872 display: flex;
873 gap: 12px;
874 flex-wrap: wrap;
875 }
876 }
877 }
878}
879
880.detail-tabs {
881 background: #fff;
882 border-radius: 12px;
883 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
884
885 :deep(.el-tabs__content) {
886 padding: 24px;
887 }
888
889 .description-content {
890 line-height: 1.6;
891
892 :deep(h3) {
893 color: #2c3e50;
894 font-size: 18px;
895 font-weight: 600;
896 margin: 24px 0 12px 0;
897
898 &:first-child {
899 margin-top: 0;
900 }
901 }
902
903 :deep(p) {
904 margin-bottom: 12px;
905 color: #5a6c7d;
906 }
907
908 :deep(ul) {
909 margin: 12px 0;
910 padding-left: 20px;
911
912 li {
913 margin-bottom: 8px;
914 color: #5a6c7d;
915 }
916 }
917
918 .no-description {
919 text-align: center;
920 color: #909399;
921 padding: 40px 0;
922 }
923 }
924
925 .files-list {
926 .file-name {
927 display: flex;
928 align-items: center;
929 gap: 8px;
930
931 .el-icon {
932 color: #909399;
933 }
934 }
935
936 .no-files {
937 padding: 40px 0;
938 }
939 }
940
941 .activity-section {
942 .activity-stats {
943 margin-bottom: 24px;
944
945 .stats-grid {
946 display: grid;
947 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
948 gap: 16px;
949
950 .stat-card {
951 background: #f8f9fa;
952 padding: 20px;
953 border-radius: 8px;
954 text-align: center;
955
956 h3 {
957 font-size: 14px;
958 color: #909399;
959 margin: 0 0 8px 0;
960 }
961
962 .stat-number {
963 font-size: 24px;
964 font-weight: 600;
965 color: #2c3e50;
966 }
967 }
968 }
969 }
970 }
971
972 .comments-section {
973 .comment-form {
974 margin-bottom: 32px;
975
976 .comment-actions {
977 margin-top: 12px;
978 text-align: right;
979 }
980 }
981
982 .comments-list {
983 .comment-item {
984 display: flex;
985 gap: 16px;
986 margin-bottom: 24px;
987 padding-bottom: 24px;
988 border-bottom: 1px solid #f0f0f0;
989
990 &:last-child {
991 border-bottom: none;
992 margin-bottom: 0;
993 padding-bottom: 0;
994 }
995
996 .comment-content {
997 flex: 1;
998
999 .comment-header {
1000 display: flex;
1001 align-items: center;
1002 gap: 12px;
1003 margin-bottom: 8px;
1004
1005 .comment-username {
1006 font-weight: 600;
1007 color: #2c3e50;
1008 }
1009
1010 .comment-time {
1011 font-size: 12px;
1012 color: #909399;
1013 }
1014 }
1015
1016 .comment-text {
1017 color: #5a6c7d;
1018 line-height: 1.5;
1019 margin-bottom: 12px;
1020 }
1021
1022 .comment-actions {
1023 .el-button {
1024 padding: 0;
1025 margin-right: 16px;
1026
1027 .el-icon {
1028 margin-right: 4px;
1029 }
1030 }
1031 }
1032 }
1033 }
1034
1035 .no-comments {
1036 text-align: center;
1037 color: #909399;
1038 padding: 40px 0;
1039 }
1040 }
1041 }
1042}
1043
1044// 响应式设计
1045@media (max-width: 768px) {
1046 .torrent-detail-page {
1047 padding: 16px;
1048 }
1049
1050 .torrent-header .header-content {
1051 flex-direction: column;
1052 text-align: center;
1053
1054 .torrent-cover {
1055 align-self: center;
1056 }
1057
1058 .torrent-stats {
1059 justify-content: center;
1060 }
1061
1062 .action-buttons {
1063 justify-content: center;
1064 }
1065 }
1066
1067 .activity-section .stats-grid {
1068 grid-template-columns: 1fr;
1069 }
1070
1071 .comment-item {
1072 flex-direction: column;
1073 gap: 12px;
1074 }
1075}
1076</style>