blob: 20d5dd086c10dfadda337120e81dd4935eb7e734 [file] [log] [blame]
vulgar520152afbcf2025-06-07 02:34:46 +08001<template>
vulgar5201c4345b12025-06-09 18:48:06 +08002 <Navbar />
vulgar520152afbcf2025-06-07 02:34:46 +08003 <div class="torrents-page">
4 <div class="page-header">
5 <h1>种子资源</h1>
6 <div class="header-actions">
7 <el-button type="primary" :icon="Upload" @click="$router.push('/upload')">
8 上传种子
9 </el-button>
10 </div>
11 </div>
12
13 <!-- 搜索和筛选 -->
14 <div class="search-section">
15 <div class="search-bar">
16 <el-input
17 v-model="searchQuery"
18 placeholder="搜索种子..."
19 :prefix-icon="Search"
20 size="large"
21 @keyup.enter="handleSearch"
22 clearable
23 />
24 <el-button type="primary" size="large" @click="handleSearch">
25 搜索
26 </el-button>
27 </div>
28
29 <div class="filters">
30 <el-select v-model="selectedCategory" placeholder="分类" @change="handleFilter">
vulgar52015dc93392025-06-07 12:01:09 +080031 <el-option
vulgar52019bf462d2025-06-07 17:54:04 +080032 v-for="category in categoryOptions"
33 :key="category.slug"
vulgar52015dc93392025-06-07 12:01:09 +080034 :label="category.name"
35 :value="category.slug"
36 />
vulgar520152afbcf2025-06-07 02:34:46 +080037 </el-select>
vulgar520152afbcf2025-06-07 02:34:46 +080038 </div>
39 </div>
40
41 <!-- 种子列表 -->
42 <div class="torrents-list">
43 <div class="list-header">
44 <span class="results-count">共找到 {{ totalCount }} 个种子</span>
45 </div>
46
47 <!-- <div style="background: yellow; padding: 10px; margin: 10px 0;">
48 <p>调试信息:</p>
49 <p>torrents长度: {{ torrents.length }}</p>
50 <p>totalCount: {{ totalCount }}</p>
51 <p>loading: {{ loading }}</p>
52 <pre>{{ JSON.stringify(torrents[0], null, 2) }}</pre>
53 </div> -->
54
55 <el-table
56 :data="torrents"
57 v-loading="loading"
58 @row-click="handleRowClick"
59 stripe
60 class="torrents-table"
61 >
62 <el-table-column label="分类" width="100">
63 <template #default="{ row }">
64 <el-tag
65 :type="getCategoryType(row.category)"
66 size="small"
67 >
68 {{ row.category?.name || '未分类' }}
69 </el-tag>
70 </template>
71 </el-table-column>
72
73 <el-table-column label="种子信息" min-width="400">
74 <template #default="{ row }">
75 <div class="torrent-info">
76 <h4 class="torrent-title">{{ row.title }}</h4>
77 <div class="torrent-subtitle" v-if="row.subTitle">
78 {{ row.subTitle }}
79 </div>
80 <div class="torrent-meta">
81 <span class="uploader">
82 <el-icon><User /></el-icon>
83 {{ row.anonymous ? '匿名用户' : row.user?.username }}
84 </span>
85 <span class="upload-time">
86 <el-icon><Clock /></el-icon>
87 {{ formatTime(row.createdAt) }}
88 </span>
89 <span class="file-size">
90 <el-icon><Document /></el-icon>
91 {{ formatSize(row.size) }}
92 </span>
93 </div>
94 <div class="torrent-tags" v-if="row.tag && row.tag.length > 0">
95 <el-tag
96 v-for="tag in row.tag"
97 :key="tag"
98 size="small"
99 type="info"
100 class="tag-item"
101 >
102 {{ tag }}
103 </el-tag>
104 </div>
105 </div>
106 </template>
107 </el-table-column>
108
vulgar5201a01f5ee2025-06-09 22:38:59 +0800109 <!-- <el-table-column label="做种" width="80" align="center">
vulgar520152afbcf2025-06-07 02:34:46 +0800110 <template #default="{ row }">
111 <span class="seeders">{{ row.seeders || 0 }}</span>
112 </template>
vulgar5201a01f5ee2025-06-09 22:38:59 +0800113 </el-table-column> -->
vulgar520152afbcf2025-06-07 02:34:46 +0800114
115 <el-table-column label="下载" width="80" align="center">
116 <template #default="{ row }">
vulgar5201a01f5ee2025-06-09 22:38:59 +0800117 <span class="downloads">{{ row.downloadCount || 0 }}</span>
vulgar520152afbcf2025-06-07 02:34:46 +0800118 </template>
119 </el-table-column>
120
vulgar5201a01f5ee2025-06-09 22:38:59 +0800121 <!-- <el-table-column label="完成" width="80" align="center">
vulgar520152afbcf2025-06-07 02:34:46 +0800122 <template #default="{ row }">
123 <span>{{ row.downloads || 0 }}</span>
124 </template>
vulgar5201a01f5ee2025-06-09 22:38:59 +0800125 </el-table-column> -->
vulgar520152afbcf2025-06-07 02:34:46 +0800126
127 <el-table-column label="操作" width="120" align="center">
128 <template #default="{ row }">
129 <el-button
130 type="primary"
131 size="small"
132 :icon="Download"
133 @click.stop="handleDownload(row)"
134 >
135 下载
136 </el-button>
137 </template>
138 </el-table-column>
139 </el-table>
140
141 <!-- 分页 -->
142 <div class="pagination-wrapper">
143 <el-pagination
144 v-model:current-page="currentPage"
145 v-model:page-size="pageSize"
146 :page-sizes="[10,20, 50, 100]"
147 :total="totalCount"
148 layout="total, sizes, prev, pager, next, jumper"
149 @size-change="handleSizeChange"
150 @current-change="handleCurrentChange"
151 />
152 </div>
153 </div>
154 </div>
155</template>
156
157<script>
vulgar52019bf462d2025-06-07 17:54:04 +0800158import { ref, onMounted, watch, computed } from 'vue'
vulgar520152afbcf2025-06-07 02:34:46 +0800159import { useRouter, useRoute } from 'vue-router'
160import { ElMessage } from 'element-plus'
161import {
162 Search,
163 Upload,
164 Download,
165 User,
166 Clock,
167 Document
168} from '@element-plus/icons-vue'
vulgar52015dc93392025-06-07 12:01:09 +0800169import { searchTorrents, getCategories } from '@/api/torrent'
Xing Jinwenebbccad2025-06-07 21:24:44 +0800170import Navbar from '@/components/Navbar.vue'
vulgar5201a01f5ee2025-06-09 22:38:59 +0800171import axios from 'axios'
vulgar520152afbcf2025-06-07 02:34:46 +0800172
173export default {
174 name: 'TorrentsView',
Xing Jinwenebbccad2025-06-07 21:24:44 +0800175 components:{
176 Navbar
177 },
vulgar520152afbcf2025-06-07 02:34:46 +0800178 setup() {
179 const router = useRouter()
180 const route = useRoute()
181
182 const loading = ref(false)
183 const searchQuery = ref('')
184 const selectedCategory = ref('')
185 const sortBy = ref('upload_time')
186 const sortOrder = ref('desc')
187 const currentPage = ref(1)
188 const pageSize = ref(20)
189 const totalCount = ref(0)
190 const totalPages = ref(0)
191
192 const torrents = ref([])
vulgar52015dc93392025-06-07 12:01:09 +0800193 const categories = ref([])
vulgar520152afbcf2025-06-07 02:34:46 +0800194
195 onMounted(() => {
196 // 从URL参数初始化搜索条件
197 if (route.query.q) {
198 searchQuery.value = route.query.q
199 }
200 if (route.query.category) {
201 selectedCategory.value = route.query.category
202 }
203 if (route.query.page) {
204 currentPage.value = parseInt(route.query.page)
205 }
206
vulgar52015dc93392025-06-07 12:01:09 +0800207 fetchCategories()
vulgar520152afbcf2025-06-07 02:34:46 +0800208 fetchTorrents()
209 })
210
vulgar5201a01f5ee2025-06-09 22:38:59 +0800211 const fetchDownloadCount = async (torrent) => {
212 try {
213 const response = await axios.get(`/api/torrent/${torrent.infoHash}/downloads`)
214 if (response.status === 200) {
215 torrent.downloadCount = response.data
216 }
217 } catch (error) {
218 console.error('获取下载数失败:', error)
219 torrent.downloadCount = 0
220 }
221 }
222
vulgar520152afbcf2025-06-07 02:34:46 +0800223 const fetchTorrents = async () => {
224 loading.value = true
225 try {
vulgar52019bf462d2025-06-07 17:54:04 +0800226 if (selectedCategory.value) {
vulgar52019bf462d2025-06-07 17:54:04 +0800227 const response = await fetch(`/api/torrent/search?category=${selectedCategory.value}`)
vulgar5201a01f5ee2025-06-09 22:38:59 +0800228 .then(res => res.json())
vulgar520152afbcf2025-06-07 02:34:46 +0800229
vulgar520152afbcf2025-06-07 02:34:46 +0800230 torrents.value = response.torrents || []
231 totalCount.value = response.totalElements || 0
232 totalPages.value = response.totalPages || 1
vulgar52019bf462d2025-06-07 17:54:04 +0800233
234 } else {
vulgar52019bf462d2025-06-07 17:54:04 +0800235 const searchParams = {
236 keyword: searchQuery.value || '',
237 page: currentPage.value - 1,
238 entriesPerPage: pageSize.value
239 }
240
241 const response = await searchTorrents(searchParams)
242
243 torrents.value = response.torrents || []
244 totalCount.value = response.totalElements || 0
245 totalPages.value = response.totalPages || 1
vulgar520152afbcf2025-06-07 02:34:46 +0800246 }
vulgar52019bf462d2025-06-07 17:54:04 +0800247
vulgar5201a01f5ee2025-06-09 22:38:59 +0800248 // 为每个种子获取下载数
249 for (const torrent of torrents.value) {
250 await fetchDownloadCount(torrent)
251 }
252
vulgar520152afbcf2025-06-07 02:34:46 +0800253 } catch (error) {
254 console.error('获取种子列表失败:', error)
vulgar52019bf462d2025-06-07 17:54:04 +0800255 ElMessage.error('获取种子列表失败')
vulgar520152afbcf2025-06-07 02:34:46 +0800256 torrents.value = []
257 totalCount.value = 0
258 } finally {
259 loading.value = false
260 }
261 }
262
vulgar52015dc93392025-06-07 12:01:09 +0800263 const fetchCategories = async () => {
264 try {
265 const response = await getCategories()
vulgar52019bf462d2025-06-07 17:54:04 +0800266 console.log('分类列表响应:', response)
267
vulgar52015dc93392025-06-07 12:01:09 +0800268 if (response && Array.isArray(response)) {
269 categories.value = response
vulgar52019bf462d2025-06-07 17:54:04 +0800270 console.log('分类列表加载成功:', categories.value)
vulgar52015dc93392025-06-07 12:01:09 +0800271 } else {
272 console.error('获取分类列表失败: 响应格式错误', response)
vulgar52019bf462d2025-06-07 17:54:04 +0800273 ElMessage.warning('分类列表格式不正确')
vulgar52015dc93392025-06-07 12:01:09 +0800274 }
275 } catch (error) {
276 console.error('获取分类列表失败:', error)
277 ElMessage.error('获取分类列表失败')
278 }
279 }
280
vulgar520152afbcf2025-06-07 02:34:46 +0800281 const handleSearch = () => {
282 currentPage.value = 1
283 updateURL()
284 fetchTorrents()
285 }
286
287 const handleFilter = () => {
288 currentPage.value = 1
289 updateURL()
290 fetchTorrents()
291 }
292
293 const updateURL = () => {
294 const query = {}
295 if (searchQuery.value) query.q = searchQuery.value
296 if (selectedCategory.value) query.category = selectedCategory.value
297 if (currentPage.value > 1) query.page = currentPage.value
298
299 router.replace({ query })
300 }
301
302 const handleRowClick = (row) => {
vulgar52014958b252025-06-08 03:26:43 +0800303 router.push(`/torrent/${row.infoHash}`)
vulgar520152afbcf2025-06-07 02:34:46 +0800304 }
305
vulgar5201a01f5ee2025-06-09 22:38:59 +0800306 const handleDownload = async (row) => {
307 try {
308 const response = await axios.get(
309 `/api/torrent/download/${row.infoHash}`,
310 {
311 responseType: 'blob',
312 // 如果需要传递passkey,可以在这里添加params
313 params: {
314 // passkey: userStore.passkey // 如果你有用户store存储passkey
315 }
316 }
317 )
318
319 // 检查响应类型是否为JSON(表示发生了错误)
320 const contentType = response.headers['content-type'];
321 if (contentType && contentType.includes('application/json')) {
322 // 将blob转换为json以读取错误信息
323 const errorText = await response.data.text();
324 const errorData = JSON.parse(errorText);
325 throw new Error(errorData.message || '下载失败');
326 }
327
328 // 从响应头中获取文件名,如果没有则使用默认格式
329 let fileName = response.headers?.['content-disposition']?.split('filename=')[1]
330 if (!fileName) {
331 // 使用默认的文件名格式
332 fileName = `${row.title}.torrent`
333 } else {
334 // 解码文件名
335 fileName = decodeURIComponent(fileName)
336 }
337
338 // 创建下载链接
339 const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/x-bittorrent' }))
340 const link = document.createElement('a')
341 link.href = url
342 link.download = fileName
343 document.body.appendChild(link)
344 link.click()
345 document.body.removeChild(link)
346 window.URL.revokeObjectURL(url)
347
348 ElMessage.success('种子文件下载完成')
349 } catch (error) {
350 console.error('下载失败:', error)
351 // 根据错误类型显示不同的错误信息
352 let errorMessage = '下载失败,请稍后重试';
353
354 if (error.response) {
355 const status = error.response.status;
356 const data = error.response.data;
357
358 switch(status) {
359 case 401:
360 errorMessage = '认证失败,请检查登录状态或passkey是否正确';
361 break;
362 case 403:
363 if (data.message?.includes('share ratio')) {
364 errorMessage = '分享率不足,无法下载';
365 } else if (data.message?.includes('torrent:download_review')) {
366 errorMessage = '该种子正在审核中,您没有权限下载';
367 } else {
368 errorMessage = '您没有权限下载此种子';
369 }
370 break;
371 case 404:
372 if (data.message?.includes('torrent not registered')) {
373 errorMessage = '该种子未在服务器注册';
374 } else if (data.message?.includes('file are missing')) {
375 errorMessage = '种子文件丢失,请联系管理员';
376 } else {
377 errorMessage = '种子不存在';
378 }
379 break;
380 default:
381 errorMessage = data.message || '下载失败,请稍后重试';
382 }
383 }
384
385 ElMessage.error(errorMessage)
386 }
vulgar520152afbcf2025-06-07 02:34:46 +0800387 }
388
389 const handleSizeChange = (size) => {
390 pageSize.value = size
391 currentPage.value = 1
392 fetchTorrents()
393 }
394
395 const handleCurrentChange = (page) => {
396 currentPage.value = page
397 updateURL()
398 fetchTorrents()
399 }
400
401 const formatTime = (timeString) => {
402 if (!timeString) return '-'
403
404 const date = new Date(timeString)
405 const now = new Date()
406 const diff = now - date
407 const hours = Math.floor(diff / (1000 * 60 * 60))
408
409 if (hours < 1) return '刚刚'
410 if (hours < 24) return `${hours}小时前`
411 const days = Math.floor(hours / 24)
412 if (days < 30) return `${days}天前`
413
414 return date.toLocaleDateString('zh-CN')
415 }
416
417 const formatSize = (sizeInBytes) => {
418 if (!sizeInBytes) return '-'
419
420 const units = ['B', 'KB', 'MB', 'GB', 'TB']
421 let size = sizeInBytes
422 let unitIndex = 0
423
424 while (size >= 1024 && unitIndex < units.length - 1) {
425 size /= 1024
426 unitIndex++
427 }
428
429 return `${size.toFixed(1)} ${units[unitIndex]}`
430 }
431
432 const getCategoryType = (category) => {
vulgar52015dc93392025-06-07 12:01:09 +0800433 if (!category) return ''
vulgar520152afbcf2025-06-07 02:34:46 +0800434 const types = {
435 'os': 'primary',
436 'movie': 'success',
vulgar52015dc93392025-06-07 12:01:09 +0800437 'db': 'info',
vulgar520152afbcf2025-06-07 02:34:46 +0800438 'music': 'warning',
439 'software': 'danger'
440 }
vulgar52015dc93392025-06-07 12:01:09 +0800441 return types[category.slug] || ''
vulgar520152afbcf2025-06-07 02:34:46 +0800442 }
443
vulgar52019bf462d2025-06-07 17:54:04 +0800444 const categoryOptions = computed(() => {
445 return [
446 { id: '', name: '全部' },
447 ...categories.value
448 ]
449 })
450
vulgar520152afbcf2025-06-07 02:34:46 +0800451 return {
452 loading,
453 searchQuery,
454 selectedCategory,
455 sortBy,
456 sortOrder,
457 currentPage,
458 pageSize,
459 totalCount,
460 torrents,
vulgar52015dc93392025-06-07 12:01:09 +0800461 categories,
vulgar52019bf462d2025-06-07 17:54:04 +0800462 categoryOptions,
vulgar520152afbcf2025-06-07 02:34:46 +0800463 handleSearch,
464 handleFilter,
465 handleRowClick,
466 handleDownload,
467 handleSizeChange,
468 handleCurrentChange,
469 formatTime,
470 formatSize,
471 getCategoryType,
472 Search,
473 Upload,
474 Download,
475 User,
476 Clock,
477 Document
478 }
479 }
480}
481</script>
482
483<style lang="scss" scoped>
484.torrents-page {
485 max-width: 1200px;
486 margin: 0 auto;
487 padding: 24px;
488}
489
490.page-header {
491 display: flex;
492 justify-content: space-between;
493 align-items: center;
494 margin-bottom: 24px;
495
496 h1 {
497 font-size: 28px;
498 font-weight: 600;
499 color: #2c3e50;
500 margin: 0;
501 }
502}
503
504.search-section {
505 background: #fff;
506 border-radius: 12px;
507 padding: 24px;
508 margin-bottom: 24px;
509 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
510
511 .search-bar {
512 display: flex;
513 gap: 12px;
514 margin-bottom: 16px;
515
516 .el-input {
517 flex: 1;
518 }
519 }
520
521 .filters {
522 display: flex;
523 gap: 16px;
524 flex-wrap: wrap;
525 align-items: center;
526
527 .el-select {
528 width: 120px;
529 }
530 }
531}
532
533.torrents-list {
534 background: #fff;
535 border-radius: 12px;
536 padding: 24px;
537 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
538
539 .list-header {
540 margin-bottom: 16px;
541
542 .results-count {
543 font-size: 14px;
544 color: #909399;
545 }
546 }
547
548 .torrents-table {
549 .torrent-info {
550 .torrent-title {
551 font-size: 16px;
552 font-weight: 500;
553 color: #2c3e50;
554 margin: 0 0 8px 0;
555 line-height: 1.4;
556 cursor: pointer;
557
558 &:hover {
559 color: #409eff;
560 }
561 }
562
563 .torrent-meta {
564 display: flex;
565 gap: 16px;
566 font-size: 12px;
567 color: #909399;
568
569 span {
570 display: flex;
571 align-items: center;
572 gap: 4px;
573 }
574 }
575 }
576
577 .seeders {
578 color: #67c23a;
579 font-weight: 600;
580 }
581
582 .leechers {
583 color: #f56c6c;
584 font-weight: 600;
585 }
586 }
587
588 .pagination-wrapper {
589 margin-top: 24px;
590 text-align: center;
591 }
592}
593
594@media (max-width: 768px) {
595 .torrents-page {
596 padding: 16px;
597 }
598
599 .page-header {
600 flex-direction: column;
601 gap: 16px;
602 align-items: flex-start;
603 }
604
605 .filters {
606 flex-direction: column;
607 align-items: flex-start;
608
609 .el-select {
610 width: 100%;
611 }
612 }
613
614 .torrents-table {
615 :deep(.el-table__header),
616 :deep(.el-table__body) {
617 font-size: 12px;
618 }
619 }
620}
xingjinwend652cc62025-06-04 19:52:19 +0800621</style>