blob: 20d5dd086c10dfadda337120e81dd4935eb7e734 [file] [log] [blame]
<template>
<Navbar />
<div class="torrents-page">
<div class="page-header">
<h1>种子资源</h1>
<div class="header-actions">
<el-button type="primary" :icon="Upload" @click="$router.push('/upload')">
上传种子
</el-button>
</div>
</div>
<!-- 搜索和筛选 -->
<div class="search-section">
<div class="search-bar">
<el-input
v-model="searchQuery"
placeholder="搜索种子..."
:prefix-icon="Search"
size="large"
@keyup.enter="handleSearch"
clearable
/>
<el-button type="primary" size="large" @click="handleSearch">
搜索
</el-button>
</div>
<div class="filters">
<el-select v-model="selectedCategory" placeholder="分类" @change="handleFilter">
<el-option
v-for="category in categoryOptions"
:key="category.slug"
:label="category.name"
:value="category.slug"
/>
</el-select>
</div>
</div>
<!-- 种子列表 -->
<div class="torrents-list">
<div class="list-header">
<span class="results-count">共找到 {{ totalCount }} 个种子</span>
</div>
<!-- <div style="background: yellow; padding: 10px; margin: 10px 0;">
<p>调试信息:</p>
<p>torrents长度: {{ torrents.length }}</p>
<p>totalCount: {{ totalCount }}</p>
<p>loading: {{ loading }}</p>
<pre>{{ JSON.stringify(torrents[0], null, 2) }}</pre>
</div> -->
<el-table
:data="torrents"
v-loading="loading"
@row-click="handleRowClick"
stripe
class="torrents-table"
>
<el-table-column label="分类" width="100">
<template #default="{ row }">
<el-tag
:type="getCategoryType(row.category)"
size="small"
>
{{ row.category?.name || '未分类' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="种子信息" min-width="400">
<template #default="{ row }">
<div class="torrent-info">
<h4 class="torrent-title">{{ row.title }}</h4>
<div class="torrent-subtitle" v-if="row.subTitle">
{{ row.subTitle }}
</div>
<div class="torrent-meta">
<span class="uploader">
<el-icon><User /></el-icon>
{{ row.anonymous ? '匿名用户' : row.user?.username }}
</span>
<span class="upload-time">
<el-icon><Clock /></el-icon>
{{ formatTime(row.createdAt) }}
</span>
<span class="file-size">
<el-icon><Document /></el-icon>
{{ formatSize(row.size) }}
</span>
</div>
<div class="torrent-tags" v-if="row.tag && row.tag.length > 0">
<el-tag
v-for="tag in row.tag"
:key="tag"
size="small"
type="info"
class="tag-item"
>
{{ tag }}
</el-tag>
</div>
</div>
</template>
</el-table-column>
<!-- <el-table-column label="做种" width="80" align="center">
<template #default="{ row }">
<span class="seeders">{{ row.seeders || 0 }}</span>
</template>
</el-table-column> -->
<el-table-column label="下载" width="80" align="center">
<template #default="{ row }">
<span class="downloads">{{ row.downloadCount || 0 }}</span>
</template>
</el-table-column>
<!-- <el-table-column label="完成" width="80" align="center">
<template #default="{ row }">
<span>{{ row.downloads || 0 }}</span>
</template>
</el-table-column> -->
<el-table-column label="操作" width="120" align="center">
<template #default="{ row }">
<el-button
type="primary"
size="small"
:icon="Download"
@click.stop="handleDownload(row)"
>
下载
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10,20, 50, 100]"
:total="totalCount"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted, watch, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
Search,
Upload,
Download,
User,
Clock,
Document
} from '@element-plus/icons-vue'
import { searchTorrents, getCategories } from '@/api/torrent'
import Navbar from '@/components/Navbar.vue'
import axios from 'axios'
export default {
name: 'TorrentsView',
components:{
Navbar
},
setup() {
const router = useRouter()
const route = useRoute()
const loading = ref(false)
const searchQuery = ref('')
const selectedCategory = ref('')
const sortBy = ref('upload_time')
const sortOrder = ref('desc')
const currentPage = ref(1)
const pageSize = ref(20)
const totalCount = ref(0)
const totalPages = ref(0)
const torrents = ref([])
const categories = ref([])
onMounted(() => {
// 从URL参数初始化搜索条件
if (route.query.q) {
searchQuery.value = route.query.q
}
if (route.query.category) {
selectedCategory.value = route.query.category
}
if (route.query.page) {
currentPage.value = parseInt(route.query.page)
}
fetchCategories()
fetchTorrents()
})
const fetchDownloadCount = async (torrent) => {
try {
const response = await axios.get(`/api/torrent/${torrent.infoHash}/downloads`)
if (response.status === 200) {
torrent.downloadCount = response.data
}
} catch (error) {
console.error('获取下载数失败:', error)
torrent.downloadCount = 0
}
}
const fetchTorrents = async () => {
loading.value = true
try {
if (selectedCategory.value) {
const response = await fetch(`/api/torrent/search?category=${selectedCategory.value}`)
.then(res => res.json())
torrents.value = response.torrents || []
totalCount.value = response.totalElements || 0
totalPages.value = response.totalPages || 1
} else {
const searchParams = {
keyword: searchQuery.value || '',
page: currentPage.value - 1,
entriesPerPage: pageSize.value
}
const response = await searchTorrents(searchParams)
torrents.value = response.torrents || []
totalCount.value = response.totalElements || 0
totalPages.value = response.totalPages || 1
}
// 为每个种子获取下载数
for (const torrent of torrents.value) {
await fetchDownloadCount(torrent)
}
} catch (error) {
console.error('获取种子列表失败:', error)
ElMessage.error('获取种子列表失败')
torrents.value = []
totalCount.value = 0
} finally {
loading.value = false
}
}
const fetchCategories = async () => {
try {
const response = await getCategories()
console.log('分类列表响应:', response)
if (response && Array.isArray(response)) {
categories.value = response
console.log('分类列表加载成功:', categories.value)
} else {
console.error('获取分类列表失败: 响应格式错误', response)
ElMessage.warning('分类列表格式不正确')
}
} catch (error) {
console.error('获取分类列表失败:', error)
ElMessage.error('获取分类列表失败')
}
}
const handleSearch = () => {
currentPage.value = 1
updateURL()
fetchTorrents()
}
const handleFilter = () => {
currentPage.value = 1
updateURL()
fetchTorrents()
}
const updateURL = () => {
const query = {}
if (searchQuery.value) query.q = searchQuery.value
if (selectedCategory.value) query.category = selectedCategory.value
if (currentPage.value > 1) query.page = currentPage.value
router.replace({ query })
}
const handleRowClick = (row) => {
router.push(`/torrent/${row.infoHash}`)
}
const handleDownload = async (row) => {
try {
const response = await axios.get(
`/api/torrent/download/${row.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 = `${row.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)
}
}
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
fetchTorrents()
}
const handleCurrentChange = (page) => {
currentPage.value = page
updateURL()
fetchTorrents()
}
const formatTime = (timeString) => {
if (!timeString) return '-'
const date = new Date(timeString)
const now = new Date()
const diff = now - date
const hours = Math.floor(diff / (1000 * 60 * 60))
if (hours < 1) return '刚刚'
if (hours < 24) return `${hours}小时前`
const days = Math.floor(hours / 24)
if (days < 30) return `${days}天前`
return date.toLocaleDateString('zh-CN')
}
const formatSize = (sizeInBytes) => {
if (!sizeInBytes) return '-'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let size = sizeInBytes
let unitIndex = 0
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024
unitIndex++
}
return `${size.toFixed(1)} ${units[unitIndex]}`
}
const getCategoryType = (category) => {
if (!category) return ''
const types = {
'os': 'primary',
'movie': 'success',
'db': 'info',
'music': 'warning',
'software': 'danger'
}
return types[category.slug] || ''
}
const categoryOptions = computed(() => {
return [
{ id: '', name: '全部' },
...categories.value
]
})
return {
loading,
searchQuery,
selectedCategory,
sortBy,
sortOrder,
currentPage,
pageSize,
totalCount,
torrents,
categories,
categoryOptions,
handleSearch,
handleFilter,
handleRowClick,
handleDownload,
handleSizeChange,
handleCurrentChange,
formatTime,
formatSize,
getCategoryType,
Search,
Upload,
Download,
User,
Clock,
Document
}
}
}
</script>
<style lang="scss" scoped>
.torrents-page {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
h1 {
font-size: 28px;
font-weight: 600;
color: #2c3e50;
margin: 0;
}
}
.search-section {
background: #fff;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
.search-bar {
display: flex;
gap: 12px;
margin-bottom: 16px;
.el-input {
flex: 1;
}
}
.filters {
display: flex;
gap: 16px;
flex-wrap: wrap;
align-items: center;
.el-select {
width: 120px;
}
}
}
.torrents-list {
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
.list-header {
margin-bottom: 16px;
.results-count {
font-size: 14px;
color: #909399;
}
}
.torrents-table {
.torrent-info {
.torrent-title {
font-size: 16px;
font-weight: 500;
color: #2c3e50;
margin: 0 0 8px 0;
line-height: 1.4;
cursor: pointer;
&:hover {
color: #409eff;
}
}
.torrent-meta {
display: flex;
gap: 16px;
font-size: 12px;
color: #909399;
span {
display: flex;
align-items: center;
gap: 4px;
}
}
}
.seeders {
color: #67c23a;
font-weight: 600;
}
.leechers {
color: #f56c6c;
font-weight: 600;
}
}
.pagination-wrapper {
margin-top: 24px;
text-align: center;
}
}
@media (max-width: 768px) {
.torrents-page {
padding: 16px;
}
.page-header {
flex-direction: column;
gap: 16px;
align-items: flex-start;
}
.filters {
flex-direction: column;
align-items: flex-start;
.el-select {
width: 100%;
}
}
.torrents-table {
:deep(.el-table__header),
:deep(.el-table__body) {
font-size: 12px;
}
}
}
</style>