| <template> |
| <Navbar /> |
| <div class="torrent-detail-page"> |
| <div class="page-container"> |
| <!-- 加载状态 --> |
| <div v-if="loading" class="loading-container"> |
| <el-skeleton :rows="8" animated /> |
| </div> |
| |
| <!-- 详情内容 --> |
| <div v-else-if="torrentInfo"> |
| <!-- 返回按钮 --> |
| <div class="back-button"> |
| <el-button :icon="ArrowLeft" @click="$router.back()"> |
| 返回列表 |
| </el-button> |
| </div> |
| |
| <!-- 种子基本信息 --> |
| <div class="torrent-header"> |
| <div class="header-content"> |
| <div class="torrent-cover"> |
| <!-- <el-image |
| :src="torrentInfo.coverImage || '/default-cover.jpg'" |
| :alt="torrentInfo.title" |
| fit="cover" |
| class="cover-image" |
| > |
| <template #error> |
| <div class="image-placeholder"> |
| <el-icon size="48"><Picture /></el-icon> |
| <span>暂无封面</span> |
| </div> |
| </template> |
| </el-image> --> |
| <el-image |
| :src="require('@/views/图片.png')" |
| :alt="torrentInfo.title" |
| fit="cover" |
| class="cover-image" |
| > |
| <template #error> |
| <div class="image-placeholder"> |
| <el-icon size="48"><Picture /></el-icon> |
| <span>暂无封面</span> |
| </div> |
| </template> |
| </el-image> |
| </div> |
| |
| <div class="torrent-info"> |
| <div class="category-tag"> |
| <el-tag :type="getCategoryType(torrentInfo.category?.slug)" size="large"> |
| {{ torrentInfo.category?.name || '未分类' }} |
| </el-tag> |
| <el-tag v-if="torrentInfo.subTitle" type="info" size="small"> |
| {{ torrentInfo.subTitle }} |
| </el-tag> |
| </div> |
| |
| <h1 class="torrent-title">{{ torrentInfo.title }}</h1> |
| |
| <div class="torrent-tags" v-if="torrentInfo.tag && torrentInfo.tag.length > 0"> |
| <el-tag |
| v-for="(tag, index) in parsedTags" |
| :key="index" |
| size="small" |
| effect="plain" |
| > |
| {{ tag }} |
| </el-tag> |
| </div> |
| |
| <div class="torrent-meta"> |
| <div class="meta-item"> |
| <el-icon><User /></el-icon> |
| <span>上传者:{{ torrentInfo.user?.username || '匿名用户' }}</span> |
| </div> |
| <div class="meta-item"> |
| <el-icon><Clock /></el-icon> |
| <span>上传时间:{{ formatDateTime(torrentInfo.createdAt) }}</span> |
| </div> |
| <div class="meta-item"> |
| <el-icon><Document /></el-icon> |
| <span>文件大小:{{ formatFileSize(torrentInfo.size) }}</span> |
| </div> |
| <!-- <div class="meta-item"> |
| <el-icon><Files /></el-icon> |
| <span>完成次数:{{ torrentInfo.finishes }} 次</span> |
| </div> --> |
| <div class="meta-item"> |
| <el-icon><Star /></el-icon> |
| <span>推广策略:{{ torrentInfo.promotionPolicy?.displayName || '默认' }}</span> |
| </div> |
| </div> |
| |
| <div class="torrent-stats"> |
| <!-- <div class="stat-item seeders"> |
| <span class="stat-number">{{ peerStats.seeders }}</span> |
| <span class="stat-label">做种</span> |
| </div> --> |
| <!-- <div class="stat-item leechers"> |
| <span class="stat-number">{{ peerStats.leechers }}</span> |
| <span class="stat-label">下载</span> |
| </div> --> |
| <div class="stat-item downloads"> |
| <span class="stat-number">{{ peerStats.downloads }}</span> |
| <span class="stat-label">总下载</span> |
| </div> |
| </div> |
| |
| <div class="action-buttons"> |
| <el-button |
| type="primary" |
| size="large" |
| :icon="Download" |
| @click="handleDownload" |
| :loading="downloading" |
| > |
| {{ downloading ? '准备中...' : '下载种子' }} |
| </el-button> |
| <!-- <el-button |
| type="success" |
| size="large" |
| :icon="Star" |
| @click="handleFavorite" |
| > |
| {{ isFavorited ? '已收藏' : '收藏' }} |
| </el-button> |
| <el-button |
| type="warning" |
| size="large" |
| :icon="Flag" |
| @click="handleReport" |
| > |
| 举报 |
| </el-button> --> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <!-- 详细信息选项卡 --> |
| <div class="detail-tabs"> |
| <el-tabs v-model="activeTab" type="border-card"> |
| <!-- 种子描述 --> |
| <el-tab-pane label="详细描述" name="description"> |
| <div class="description-content"> |
| <div v-if="torrentInfo.description" v-html="formatDescription(torrentInfo.description)"></div> |
| <div v-else class="no-description">暂无详细描述</div> |
| </div> |
| </el-tab-pane> |
| |
| <!-- 文件列表 --> |
| <el-tab-pane label="文件列表" name="files" lazy> |
| <div class="files-list"> |
| <div v-if="torrentFiles.length > 0"> |
| <el-table :data="torrentFiles" stripe> |
| <el-table-column label="文件名" prop="name" min-width="400"> |
| <template #default="{ row }"> |
| <div class="file-name"> |
| <el-icon v-if="row.type === 'folder'"><Folder /></el-icon> |
| <el-icon v-else><Document /></el-icon> |
| <span>{{ row.name }}</span> |
| </div> |
| </template> |
| </el-table-column> |
| <el-table-column label="大小" prop="size" width="120" align="right" /> |
| <el-table-column label="路径" prop="path" min-width="300" /> |
| </el-table> |
| </div> |
| <div v-else class="no-files"> |
| <el-empty description="文件列表加载中..." /> |
| </div> |
| </div> |
| </el-tab-pane> |
| |
| <!-- 用户活动 --> |
| <el-tab-pane label="用户活动" name="activity"> |
| <div class="activity-section"> |
| <div class="activity-stats"> |
| <div class="stats-grid"> |
| <div class="stat-card"> |
| <h3>做种用户</h3> |
| <p class="stat-number">{{ peerStats.seeders }}</p> |
| </div> |
| <div class="stat-card"> |
| <h3>下载用户</h3> |
| <p class="stat-number">{{ peerStats.leechers }}</p> |
| </div> |
| <div class="stat-card"> |
| <h3>完成用户</h3> |
| <p class="stat-number">{{ torrentInfo.finishes }}</p> |
| </div> |
| </div> |
| </div> |
| |
| <div class="user-lists"> |
| <el-tabs v-model="activityTab" type="card"> |
| <el-tab-pane label="做种用户" name="seeders"> |
| <el-table :data="seedersList" max-height="400"> |
| <el-table-column label="用户" prop="username" /> |
| <el-table-column label="上传量" prop="uploaded" /> |
| <el-table-column label="下载量" prop="downloaded" /> |
| <el-table-column label="分享率" prop="ratio" /> |
| <el-table-column label="做种时间" prop="seedTime" /> |
| </el-table> |
| </el-tab-pane> |
| |
| <el-tab-pane label="下载用户" name="leechers"> |
| <el-table :data="leechersList" max-height="400"> |
| <el-table-column label="用户" prop="username" /> |
| <el-table-column label="进度" prop="progress"> |
| <template #default="{ row }"> |
| <el-progress :percentage="row.progress" :stroke-width="6" /> |
| </template> |
| </el-table-column> |
| <el-table-column label="下载速度" prop="downloadSpeed" /> |
| <el-table-column label="剩余时间" prop="eta" /> |
| </el-table> |
| </el-tab-pane> |
| </el-tabs> |
| </div> |
| </div> |
| </el-tab-pane> |
| |
| <!-- 评论区 --> |
| <el-tab-pane label="评论" name="comments"> |
| <div class="comments-section"> |
| <!-- 发表评论 --> |
| <div class="comment-form"> |
| <el-input |
| v-model="newComment" |
| type="textarea" |
| :rows="4" |
| placeholder="发表你的评论..." |
| maxlength="500" |
| show-word-limit |
| /> |
| <div class="comment-actions"> |
| <el-button type="primary" @click="submitComment" :loading="submittingComment"> |
| 发表评论 |
| </el-button> |
| </div> |
| </div> |
| |
| <!-- 评论列表 --> |
| <div class="comments-list"> |
| <div |
| v-for="comment in comments" |
| :key="comment.id" |
| class="comment-item" |
| > |
| <div class="comment-avatar"> |
| <el-avatar :size="40">{{ comment.username.charAt(0) }}</el-avatar> |
| </div> |
| <div class="comment-content"> |
| <div class="comment-header"> |
| <span class="comment-username">{{ comment.username }}</span> |
| <span class="comment-time">{{ formatDateTime(comment.time) }}</span> |
| </div> |
| <div class="comment-text">{{ comment.content }}</div> |
| <div class="comment-actions"> |
| <el-button type="text" size="small" @click="likeComment(comment.id)"> |
| <el-icon><Like /></el-icon> |
| {{ comment.likes || 0 }} |
| </el-button> |
| <el-button type="text" size="small" @click="replyComment(comment.id)"> |
| 回复 |
| </el-button> |
| </div> |
| </div> |
| </div> |
| |
| <div v-if="comments.length === 0" class="no-comments"> |
| 暂无评论,快来发表第一条评论吧! |
| </div> |
| </div> |
| </div> |
| </el-tab-pane> |
| </el-tabs> |
| </div> |
| </div> |
| |
| <!-- 错误状态 --> |
| <div v-else class="error-container"> |
| <el-empty description="种子信息加载失败"> |
| <el-button type="primary" @click="retry">重试</el-button> |
| </el-empty> |
| </div> |
| </div> |
| </div> |
| </template> |
| |
| <script> |
| import { ref, onMounted, computed } from 'vue' |
| import { useRoute, useRouter } from 'vue-router' |
| import { ElMessage, ElMessageBox } from 'element-plus' |
| import { |
| ArrowLeft, |
| Download, |
| Star, |
| Flag, |
| User, |
| Clock, |
| Document, |
| Files, |
| Picture, |
| Folder, |
| Like |
| } from '@element-plus/icons-vue' |
| import axios from 'axios' |
| import Navbar from '@/components/Navbar.vue' |
| |
| export default { |
| name: 'TorrentDetailView', |
| components: { |
| Navbar, |
| ArrowLeft, |
| Download, |
| Star, |
| Flag, |
| User, |
| Clock, |
| Document, |
| Files, |
| Picture, |
| Folder, |
| Like |
| }, |
| setup() { |
| const route = useRoute() |
| const router = useRouter() |
| |
| const loading = ref(true) |
| const activeTab = ref('description') |
| const activityTab = ref('seeders') |
| const downloading = ref(false) |
| const isFavorited = ref(false) |
| const submittingComment = ref(false) |
| const newComment = ref('') |
| |
| const torrentInfo = ref(null) |
| const torrentFiles = ref([]) |
| const peerStats = ref({ |
| seeders: 0, |
| leechers: 0, |
| downloads: 0 |
| }) |
| |
| const seedersList = ref([]) |
| const leechersList = ref([]) |
| const comments = ref([]) |
| |
| // 解析标签 |
| const parsedTags = computed(() => { |
| if (!torrentInfo.value?.tag || !Array.isArray(torrentInfo.value.tag)) { |
| return [] |
| } |
| |
| const tags = [] |
| torrentInfo.value.tag.forEach(tagString => { |
| try { |
| const parsed = JSON.parse(tagString) |
| if (Array.isArray(parsed)) { |
| tags.push(...parsed) |
| } else { |
| tags.push(parsed) |
| } |
| } catch (e) { |
| tags.push(tagString) |
| } |
| }) |
| return tags |
| }) |
| |
| onMounted(() => { |
| const infoHash = route.params.infoHash |
| if (infoHash) { |
| fetchTorrentDetail(infoHash) |
| } else { |
| ElMessage.error('缺少种子标识符') |
| router.back() |
| } |
| }) |
| |
| const fetchTorrentDetail = async (infoHash) => { |
| try { |
| loading.value = true |
| |
| // 获取种子详情 |
| const response = await axios.get(`/api/torrent/view/${infoHash}`) |
| |
| if (response.data.code === 0) { |
| torrentInfo.value = response.data |
| |
| // 获取文件列表(如果有相关API) |
| await fetchTorrentFiles(infoHash) |
| |
| // 获取用户活动数据(如果有相关API) |
| await fetchPeerStats(infoHash) |
| |
| // 获取下载数 |
| await fetchDownloadCount(infoHash) |
| |
| // 获取评论(如果有相关API) |
| await fetchComments(infoHash) |
| } else { |
| throw new Error(response.data.message || '获取种子详情失败') |
| } |
| } catch (error) { |
| console.error('获取种子详情失败:', error) |
| ElMessage.error(error.message || '获取种子详情失败') |
| torrentInfo.value = null |
| } finally { |
| loading.value = false |
| } |
| } |
| |
| const fetchTorrentFiles = async (infoHash) => { |
| try { |
| if (torrentInfo.value?.fileList) { |
| // 将文件列表转换为所需的格式 |
| torrentFiles.value = Object.entries(torrentInfo.value.fileList).map(([path, size]) => ({ |
| name: path.split('/').pop() || path, // 获取文件名 |
| type: 'file', |
| size: formatFileSize(size), |
| path: path.split('/').slice(0, -1).join('/') || '/' // 获取文件路径 |
| })) |
| } else { |
| // 如果是单文件种子,使用种子标题作为文件名 |
| torrentFiles.value = [ |
| { |
| name: torrentInfo.value?.title || 'unknown', |
| type: 'file', |
| size: formatFileSize(torrentInfo.value?.size || 0), |
| path: '/' |
| } |
| ] |
| } |
| } catch (error) { |
| console.error('获取文件列表失败:', error) |
| } |
| } |
| |
| const fetchPeerStats = async (infoHash) => { |
| try { |
| // 这里应该调用获取用户活动数据的API |
| // const response = await axios.get(`http://localhost:8081/api/torrent/${infoHash}/peers`) |
| // peerStats.value = response.data |
| |
| // 临时模拟数据 |
| peerStats.value = { |
| seeders: Math.floor(Math.random() * 50) + 10, |
| leechers: Math.floor(Math.random() * 30) + 5 |
| } |
| |
| // 模拟用户列表 |
| seedersList.value = [ |
| { username: 'SeedMaster', uploaded: '2.5 TB', downloaded: '850 GB', ratio: '3.02', seedTime: '15天' } |
| ] |
| |
| leechersList.value = [ |
| { username: 'NewUser123', progress: 65, downloadSpeed: '15.2 MB/s', eta: '2小时15分' } |
| ] |
| } catch (error) { |
| console.error('获取用户活动数据失败:', error) |
| } |
| } |
| |
| const fetchDownloadCount = async (infoHash) => { |
| try { |
| const response = await axios.get(`/api/torrent/${infoHash}/downloads`) |
| if (response.status === 200) { |
| peerStats.value.downloads = response.data |
| } |
| } catch (error) { |
| console.error('获取下载数失败:', error) |
| peerStats.value.downloads = 0 |
| } |
| } |
| |
| const fetchComments = async (infoHash) => { |
| try { |
| // 这里应该调用获取评论的API |
| // const response = await axios.get(`http://localhost:8081/api/torrent/${infoHash}/comments`) |
| // comments.value = response.data |
| |
| // 临时模拟数据 |
| comments.value = [] |
| } catch (error) { |
| console.error('获取评论失败:', error) |
| } |
| } |
| |
| const formatDateTime = (timestamp) => { |
| if (!timestamp) return '未知' |
| const date = new Date(timestamp) |
| return date.toLocaleString('zh-CN', { |
| year: 'numeric', |
| month: '2-digit', |
| day: '2-digit', |
| hour: '2-digit', |
| minute: '2-digit' |
| }) |
| } |
| |
| const formatFileSize = (bytes) => { |
| if (!bytes || bytes === 0) return '0 B' |
| |
| const k = 1024 |
| const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] |
| const i = Math.floor(Math.log(bytes) / Math.log(k)) |
| |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] |
| } |
| |
| const formatDescription = (description) => { |
| if (!description) return '' |
| return description.replace(/\n/g, '<br>') |
| } |
| |
| const getCategoryType = (categorySlug) => { |
| const types = { |
| 'os': 'primary', |
| 'movie': 'primary', |
| 'tv': 'info', |
| 'music': 'success', |
| 'software': 'warning', |
| 'game': 'danger' |
| } |
| return types[categorySlug] || 'default' |
| } |
| |
| const handleDownload = async () => { |
| if (!torrentInfo.value?.infoHash) return |
| |
| downloading.value = true |
| try { |
| // 调用下载种子文件的API |
| const response = await axios.get( |
| `/api/torrent/download/${torrentInfo.value.infoHash}`, |
| { |
| responseType: 'blob', |
| // 如果需要传递passkey,可以在这里添加params |
| params: { |
| // passkey: userStore.passkey // 如果你有用户store存储passkey |
| } |
| } |
| ) |
| |
| // 检查响应类型是否为JSON(表示发生了错误) |
| const contentType = response.headers['content-type']; |
| if (contentType && contentType.includes('application/json')) { |
| // 将blob转换为json以读取错误信息 |
| const errorText = await response.data.text(); |
| const errorData = JSON.parse(errorText); |
| throw new Error(errorData.message || '下载失败'); |
| } |
| |
| // 从响应头中获取文件名,如果没有则使用默认格式 |
| let fileName = response.headers?.['content-disposition']?.split('filename=')[1] |
| if (!fileName) { |
| // 使用默认的文件名格式 |
| fileName = `${torrentInfo.value.title}.torrent` |
| } else { |
| // 解码文件名 |
| fileName = decodeURIComponent(fileName) |
| } |
| |
| // 创建下载链接 |
| const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/x-bittorrent' })) |
| const link = document.createElement('a') |
| link.href = url |
| link.download = fileName |
| document.body.appendChild(link) |
| link.click() |
| document.body.removeChild(link) |
| window.URL.revokeObjectURL(url) |
| |
| ElMessage.success('种子文件下载完成') |
| } catch (error) { |
| console.error('下载失败:', error) |
| // 根据错误类型显示不同的错误信息 |
| let errorMessage = '下载失败,请稍后重试'; |
| |
| if (error.response) { |
| const status = error.response.status; |
| const data = error.response.data; |
| |
| switch(status) { |
| case 401: |
| errorMessage = '认证失败,请检查登录状态或passkey是否正确'; |
| break; |
| case 403: |
| if (data.message?.includes('share ratio')) { |
| errorMessage = '分享率不足,无法下载'; |
| } else if (data.message?.includes('torrent:download_review')) { |
| errorMessage = '该种子正在审核中,您没有权限下载'; |
| } else { |
| errorMessage = '您没有权限下载此种子'; |
| } |
| break; |
| case 404: |
| if (data.message?.includes('torrent not registered')) { |
| errorMessage = '该种子未在服务器注册'; |
| } else if (data.message?.includes('file are missing')) { |
| errorMessage = '种子文件丢失,请联系管理员'; |
| } else { |
| errorMessage = '种子不存在'; |
| } |
| break; |
| default: |
| errorMessage = data.message || '下载失败,请稍后重试'; |
| } |
| } |
| |
| ElMessage.error(errorMessage) |
| } finally { |
| downloading.value = false |
| } |
| } |
| |
| const handleFavorite = () => { |
| isFavorited.value = !isFavorited.value |
| ElMessage.success(isFavorited.value ? '已添加到收藏' : '已取消收藏') |
| } |
| |
| const handleReport = async () => { |
| try { |
| await ElMessageBox.prompt('请说明举报原因', '举报内容', { |
| confirmButtonText: '提交举报', |
| cancelButtonText: '取消', |
| inputType: 'textarea', |
| inputPlaceholder: '请详细说明举报原因...' |
| }) |
| |
| ElMessage.success('举报已提交,我们会尽快处理') |
| } catch { |
| // 用户取消 |
| } |
| } |
| |
| const submitComment = async () => { |
| if (!newComment.value.trim()) { |
| ElMessage.warning('请输入评论内容') |
| return |
| } |
| |
| submittingComment.value = true |
| try { |
| // 这里应该调用提交评论的API |
| // await axios.post(`http://localhost:8081/api/torrent/${torrentInfo.value.infoHash}/comments`, { |
| // content: newComment.value |
| // }) |
| |
| // 模拟提交 |
| await new Promise(resolve => setTimeout(resolve, 1000)) |
| |
| const comment = { |
| id: Date.now(), |
| username: localStorage.getItem('username') || '用户', |
| content: newComment.value, |
| time: new Date().toISOString(), |
| likes: 0 |
| } |
| |
| comments.value.unshift(comment) |
| newComment.value = '' |
| |
| ElMessage.success('评论发表成功') |
| } catch (error) { |
| console.error('发表评论失败:', error) |
| ElMessage.error('发表评论失败') |
| } finally { |
| submittingComment.value = false |
| } |
| } |
| |
| const likeComment = (commentId) => { |
| const comment = comments.value.find(c => c.id === commentId) |
| if (comment) { |
| comment.likes = (comment.likes || 0) + 1 |
| ElMessage.success('点赞成功') |
| } |
| } |
| |
| const replyComment = (commentId) => { |
| ElMessage.info('回复功能开发中...') |
| } |
| |
| const retry = () => { |
| const infoHash = route.params.infoHash |
| if (infoHash) { |
| fetchTorrentDetail(infoHash) |
| } |
| } |
| |
| return { |
| loading, |
| activeTab, |
| activityTab, |
| downloading, |
| isFavorited, |
| submittingComment, |
| newComment, |
| torrentInfo, |
| torrentFiles, |
| peerStats, |
| parsedTags, |
| seedersList, |
| leechersList, |
| comments, |
| formatDateTime, |
| formatFileSize, |
| formatDescription, |
| getCategoryType, |
| handleDownload, |
| handleFavorite, |
| handleReport, |
| submitComment, |
| likeComment, |
| replyComment, |
| retry, |
| ArrowLeft, |
| Download, |
| Star, |
| Flag, |
| User, |
| Clock, |
| Document, |
| Files, |
| Picture, |
| Folder, |
| Like |
| } |
| } |
| } |
| </script> |
| |
| <style lang="scss" scoped> |
| .torrent-detail-page { |
| max-width: 1200px; |
| margin: 0 auto; |
| padding: 24px; |
| background: #f5f5f5; |
| min-height: 100vh; |
| } |
| |
| .loading-container, .error-container { |
| background: #fff; |
| border-radius: 12px; |
| padding: 40px; |
| text-align: center; |
| } |
| |
| .back-button { |
| margin-bottom: 16px; |
| } |
| |
| .torrent-header { |
| background: #fff; |
| border-radius: 12px; |
| padding: 32px; |
| margin-bottom: 24px; |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); |
| |
| .header-content { |
| display: flex; |
| gap: 32px; |
| |
| .torrent-cover { |
| flex-shrink: 0; |
| |
| .cover-image { |
| width: 200px; |
| height: 280px; |
| border-radius: 8px; |
| object-fit: cover; |
| } |
| |
| .image-placeholder { |
| width: 200px; |
| height: 280px; |
| background: #f5f5f5; |
| border-radius: 8px; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| color: #999; |
| |
| span { |
| margin-top: 8px; |
| font-size: 14px; |
| } |
| } |
| } |
| |
| .torrent-info { |
| flex: 1; |
| |
| .category-tag { |
| margin-bottom: 16px; |
| |
| .el-tag { |
| margin-right: 8px; |
| } |
| } |
| |
| .torrent-title { |
| font-size: 28px; |
| font-weight: 600; |
| color: #2c3e50; |
| margin: 0 0 16px 0; |
| line-height: 1.3; |
| } |
| |
| .torrent-tags { |
| margin-bottom: 20px; |
| |
| .el-tag { |
| margin: 0 8px 8px 0; |
| } |
| } |
| |
| .torrent-meta { |
| margin-bottom: 20px; |
| |
| .meta-item { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| margin-bottom: 8px; |
| color: #7f8c8d; |
| font-size: 14px; |
| |
| .el-icon { |
| color: #909399; |
| } |
| } |
| } |
| |
| .torrent-stats { |
| display: flex; |
| gap: 32px; |
| margin-bottom: 24px; |
| |
| .stat-item { |
| text-align: center; |
| |
| .stat-number { |
| display: block; |
| font-size: 24px; |
| font-weight: 600; |
| margin-bottom: 4px; |
| color: #2c3e50; |
| } |
| |
| .stat-label { |
| font-size: 14px; |
| color: #909399; |
| } |
| |
| &.seeders .stat-number { color: #67c23a; } |
| &.leechers .stat-number { color: #f56c6c; } |
| &.downloads .stat-number { color: #409eff; } |
| } |
| } |
| |
| .action-buttons { |
| display: flex; |
| gap: 12px; |
| flex-wrap: wrap; |
| } |
| } |
| } |
| } |
| |
| .detail-tabs { |
| background: #fff; |
| border-radius: 12px; |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); |
| |
| :deep(.el-tabs__content) { |
| padding: 24px; |
| } |
| |
| .description-content { |
| line-height: 1.6; |
| |
| :deep(h3) { |
| color: #2c3e50; |
| font-size: 18px; |
| font-weight: 600; |
| margin: 24px 0 12px 0; |
| |
| &:first-child { |
| margin-top: 0; |
| } |
| } |
| |
| :deep(p) { |
| margin-bottom: 12px; |
| color: #5a6c7d; |
| } |
| |
| :deep(ul) { |
| margin: 12px 0; |
| padding-left: 20px; |
| |
| li { |
| margin-bottom: 8px; |
| color: #5a6c7d; |
| } |
| } |
| |
| .no-description { |
| text-align: center; |
| color: #909399; |
| padding: 40px 0; |
| } |
| } |
| |
| .files-list { |
| .file-name { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| |
| .el-icon { |
| color: #909399; |
| } |
| } |
| |
| .no-files { |
| padding: 40px 0; |
| } |
| } |
| |
| .activity-section { |
| .activity-stats { |
| margin-bottom: 24px; |
| |
| .stats-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); |
| gap: 16px; |
| |
| .stat-card { |
| background: #f8f9fa; |
| padding: 20px; |
| border-radius: 8px; |
| text-align: center; |
| |
| h3 { |
| font-size: 14px; |
| color: #909399; |
| margin: 0 0 8px 0; |
| } |
| |
| .stat-number { |
| font-size: 24px; |
| font-weight: 600; |
| color: #2c3e50; |
| } |
| } |
| } |
| } |
| } |
| |
| .comments-section { |
| .comment-form { |
| margin-bottom: 32px; |
| |
| .comment-actions { |
| margin-top: 12px; |
| text-align: right; |
| } |
| } |
| |
| .comments-list { |
| .comment-item { |
| display: flex; |
| gap: 16px; |
| margin-bottom: 24px; |
| padding-bottom: 24px; |
| border-bottom: 1px solid #f0f0f0; |
| |
| &:last-child { |
| border-bottom: none; |
| margin-bottom: 0; |
| padding-bottom: 0; |
| } |
| |
| .comment-content { |
| flex: 1; |
| |
| .comment-header { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| margin-bottom: 8px; |
| |
| .comment-username { |
| font-weight: 600; |
| color: #2c3e50; |
| } |
| |
| .comment-time { |
| font-size: 12px; |
| color: #909399; |
| } |
| } |
| |
| .comment-text { |
| color: #5a6c7d; |
| line-height: 1.5; |
| margin-bottom: 12px; |
| } |
| |
| .comment-actions { |
| .el-button { |
| padding: 0; |
| margin-right: 16px; |
| |
| .el-icon { |
| margin-right: 4px; |
| } |
| } |
| } |
| } |
| } |
| |
| .no-comments { |
| text-align: center; |
| color: #909399; |
| padding: 40px 0; |
| } |
| } |
| } |
| } |
| |
| // 响应式设计 |
| @media (max-width: 768px) { |
| .torrent-detail-page { |
| padding: 16px; |
| } |
| |
| .torrent-header .header-content { |
| flex-direction: column; |
| text-align: center; |
| |
| .torrent-cover { |
| align-self: center; |
| } |
| |
| .torrent-stats { |
| justify-content: center; |
| } |
| |
| .action-buttons { |
| justify-content: center; |
| } |
| } |
| |
| .activity-section .stats-grid { |
| grid-template-columns: 1fr; |
| } |
| |
| .comment-item { |
| flex-direction: column; |
| gap: 12px; |
| } |
| } |
| </style> |