blob: 3621bb5f49b0d18638453b581bf2e278f8b2213f [file] [log] [blame]
vulgar520152afbcf2025-06-07 02:34:46 +08001<template>
2 <div class="torrents-page">
3 <div class="page-header">
4 <h1>种子资源</h1>
5 <div class="header-actions">
6 <el-button type="primary" :icon="Upload" @click="$router.push('/upload')">
7 上传种子
8 </el-button>
9 </div>
10 </div>
11
12 <!-- 搜索和筛选 -->
13 <div class="search-section">
14 <div class="search-bar">
15 <el-input
16 v-model="searchQuery"
17 placeholder="搜索种子..."
18 :prefix-icon="Search"
19 size="large"
20 @keyup.enter="handleSearch"
21 clearable
22 />
23 <el-button type="primary" size="large" @click="handleSearch">
24 搜索
25 </el-button>
26 </div>
27
28 <div class="filters">
29 <el-select v-model="selectedCategory" placeholder="分类" @change="handleFilter">
vulgar52015dc93392025-06-07 12:01:09 +080030 <el-option
vulgar52019bf462d2025-06-07 17:54:04 +080031 v-for="category in categoryOptions"
32 :key="category.slug"
vulgar52015dc93392025-06-07 12:01:09 +080033 :label="category.name"
34 :value="category.slug"
35 />
vulgar520152afbcf2025-06-07 02:34:46 +080036 </el-select>
vulgar520152afbcf2025-06-07 02:34:46 +080037 </div>
38 </div>
39
40 <!-- 种子列表 -->
41 <div class="torrents-list">
42 <div class="list-header">
43 <span class="results-count">共找到 {{ totalCount }} 个种子</span>
44 </div>
45
46 <!-- <div style="background: yellow; padding: 10px; margin: 10px 0;">
47 <p>调试信息:</p>
48 <p>torrents长度: {{ torrents.length }}</p>
49 <p>totalCount: {{ totalCount }}</p>
50 <p>loading: {{ loading }}</p>
51 <pre>{{ JSON.stringify(torrents[0], null, 2) }}</pre>
52 </div> -->
53
54 <el-table
55 :data="torrents"
56 v-loading="loading"
57 @row-click="handleRowClick"
58 stripe
59 class="torrents-table"
60 >
61 <el-table-column label="分类" width="100">
62 <template #default="{ row }">
63 <el-tag
64 :type="getCategoryType(row.category)"
65 size="small"
66 >
67 {{ row.category?.name || '未分类' }}
68 </el-tag>
69 </template>
70 </el-table-column>
71
72 <el-table-column label="种子信息" min-width="400">
73 <template #default="{ row }">
74 <div class="torrent-info">
75 <h4 class="torrent-title">{{ row.title }}</h4>
76 <div class="torrent-subtitle" v-if="row.subTitle">
77 {{ row.subTitle }}
78 </div>
79 <div class="torrent-meta">
80 <span class="uploader">
81 <el-icon><User /></el-icon>
82 {{ row.anonymous ? '匿名用户' : row.user?.username }}
83 </span>
84 <span class="upload-time">
85 <el-icon><Clock /></el-icon>
86 {{ formatTime(row.createdAt) }}
87 </span>
88 <span class="file-size">
89 <el-icon><Document /></el-icon>
90 {{ formatSize(row.size) }}
91 </span>
92 </div>
93 <div class="torrent-tags" v-if="row.tag && row.tag.length > 0">
94 <el-tag
95 v-for="tag in row.tag"
96 :key="tag"
97 size="small"
98 type="info"
99 class="tag-item"
100 >
101 {{ tag }}
102 </el-tag>
103 </div>
104 </div>
105 </template>
106 </el-table-column>
107
108 <el-table-column label="做种" width="80" align="center">
109 <template #default="{ row }">
110 <span class="seeders">{{ row.seeders || 0 }}</span>
111 </template>
112 </el-table-column>
113
114 <el-table-column label="下载" width="80" align="center">
115 <template #default="{ row }">
116 <span class="leechers">{{ row.leechers || 0 }}</span>
117 </template>
118 </el-table-column>
119
120 <el-table-column label="完成" width="80" align="center">
121 <template #default="{ row }">
122 <span>{{ row.downloads || 0 }}</span>
123 </template>
124 </el-table-column>
125
126 <el-table-column label="操作" width="120" align="center">
127 <template #default="{ row }">
128 <el-button
129 type="primary"
130 size="small"
131 :icon="Download"
132 @click.stop="handleDownload(row)"
133 >
134 下载
135 </el-button>
136 </template>
137 </el-table-column>
138 </el-table>
139
140 <!-- 分页 -->
141 <div class="pagination-wrapper">
142 <el-pagination
143 v-model:current-page="currentPage"
144 v-model:page-size="pageSize"
145 :page-sizes="[10,20, 50, 100]"
146 :total="totalCount"
147 layout="total, sizes, prev, pager, next, jumper"
148 @size-change="handleSizeChange"
149 @current-change="handleCurrentChange"
150 />
151 </div>
152 </div>
153 </div>
154</template>
155
156<script>
vulgar52019bf462d2025-06-07 17:54:04 +0800157import { ref, onMounted, watch, computed } from 'vue'
vulgar520152afbcf2025-06-07 02:34:46 +0800158import { useRouter, useRoute } from 'vue-router'
159import { ElMessage } from 'element-plus'
160import {
161 Search,
162 Upload,
163 Download,
164 User,
165 Clock,
166 Document
167} from '@element-plus/icons-vue'
vulgar52015dc93392025-06-07 12:01:09 +0800168import { searchTorrents, getCategories } from '@/api/torrent'
vulgar520152afbcf2025-06-07 02:34:46 +0800169
170export default {
171 name: 'TorrentsView',
172 setup() {
173 const router = useRouter()
174 const route = useRoute()
175
176 const loading = ref(false)
177 const searchQuery = ref('')
178 const selectedCategory = ref('')
179 const sortBy = ref('upload_time')
180 const sortOrder = ref('desc')
181 const currentPage = ref(1)
182 const pageSize = ref(20)
183 const totalCount = ref(0)
184 const totalPages = ref(0)
185
186 const torrents = ref([])
vulgar52015dc93392025-06-07 12:01:09 +0800187 const categories = ref([])
vulgar520152afbcf2025-06-07 02:34:46 +0800188
189 onMounted(() => {
190 // 从URL参数初始化搜索条件
191 if (route.query.q) {
192 searchQuery.value = route.query.q
193 }
194 if (route.query.category) {
195 selectedCategory.value = route.query.category
196 }
197 if (route.query.page) {
198 currentPage.value = parseInt(route.query.page)
199 }
200
vulgar52015dc93392025-06-07 12:01:09 +0800201 fetchCategories()
vulgar520152afbcf2025-06-07 02:34:46 +0800202 fetchTorrents()
203 })
204
205 const fetchTorrents = async () => {
206 loading.value = true
207 try {
vulgar52019bf462d2025-06-07 17:54:04 +0800208 if (selectedCategory.value) {
209 // 使用 GET 请求
210 const response = await fetch(`/api/torrent/search?category=${selectedCategory.value}`)
211 .then(res => res.json())
vulgar520152afbcf2025-06-07 02:34:46 +0800212
vulgar520152afbcf2025-06-07 02:34:46 +0800213 torrents.value = response.torrents || []
214 totalCount.value = response.totalElements || 0
215 totalPages.value = response.totalPages || 1
vulgar52019bf462d2025-06-07 17:54:04 +0800216
217 } else {
218 // 使用 POST 请求(搜索)
219 const searchParams = {
220 keyword: searchQuery.value || '',
221 page: currentPage.value - 1,
222 entriesPerPage: pageSize.value
223 }
224
225 const response = await searchTorrents(searchParams)
226
227 torrents.value = response.torrents || []
228 totalCount.value = response.totalElements || 0
229 totalPages.value = response.totalPages || 1
vulgar520152afbcf2025-06-07 02:34:46 +0800230 }
vulgar52019bf462d2025-06-07 17:54:04 +0800231
vulgar520152afbcf2025-06-07 02:34:46 +0800232 } catch (error) {
233 console.error('获取种子列表失败:', error)
vulgar52019bf462d2025-06-07 17:54:04 +0800234 ElMessage.error('获取种子列表失败')
vulgar520152afbcf2025-06-07 02:34:46 +0800235 torrents.value = []
236 totalCount.value = 0
237 } finally {
238 loading.value = false
239 }
240 }
241
vulgar52015dc93392025-06-07 12:01:09 +0800242 const fetchCategories = async () => {
243 try {
244 const response = await getCategories()
vulgar52019bf462d2025-06-07 17:54:04 +0800245 console.log('分类列表响应:', response)
246
vulgar52015dc93392025-06-07 12:01:09 +0800247 if (response && Array.isArray(response)) {
248 categories.value = response
vulgar52019bf462d2025-06-07 17:54:04 +0800249 console.log('分类列表加载成功:', categories.value)
vulgar52015dc93392025-06-07 12:01:09 +0800250 } else {
251 console.error('获取分类列表失败: 响应格式错误', response)
vulgar52019bf462d2025-06-07 17:54:04 +0800252 ElMessage.warning('分类列表格式不正确')
vulgar52015dc93392025-06-07 12:01:09 +0800253 }
254 } catch (error) {
255 console.error('获取分类列表失败:', error)
256 ElMessage.error('获取分类列表失败')
257 }
258 }
259
vulgar520152afbcf2025-06-07 02:34:46 +0800260 const handleSearch = () => {
261 currentPage.value = 1
262 updateURL()
263 fetchTorrents()
264 }
265
266 const handleFilter = () => {
267 currentPage.value = 1
268 updateURL()
269 fetchTorrents()
270 }
271
272 const updateURL = () => {
273 const query = {}
274 if (searchQuery.value) query.q = searchQuery.value
275 if (selectedCategory.value) query.category = selectedCategory.value
276 if (currentPage.value > 1) query.page = currentPage.value
277
278 router.replace({ query })
279 }
280
281 const handleRowClick = (row) => {
vulgar52014958b252025-06-08 03:26:43 +0800282 router.push(`/torrent/${row.infoHash}`)
vulgar520152afbcf2025-06-07 02:34:46 +0800283 }
284
285 const handleDownload = (row) => {
286 ElMessage.success(`开始下载: ${row.title || row.name}`)
287 // 这里实现下载逻辑
288 }
289
290 const handleSizeChange = (size) => {
291 pageSize.value = size
292 currentPage.value = 1
293 fetchTorrents()
294 }
295
296 const handleCurrentChange = (page) => {
297 currentPage.value = page
298 updateURL()
299 fetchTorrents()
300 }
301
302 const formatTime = (timeString) => {
303 if (!timeString) return '-'
304
305 const date = new Date(timeString)
306 const now = new Date()
307 const diff = now - date
308 const hours = Math.floor(diff / (1000 * 60 * 60))
309
310 if (hours < 1) return '刚刚'
311 if (hours < 24) return `${hours}小时前`
312 const days = Math.floor(hours / 24)
313 if (days < 30) return `${days}天前`
314
315 return date.toLocaleDateString('zh-CN')
316 }
317
318 const formatSize = (sizeInBytes) => {
319 if (!sizeInBytes) return '-'
320
321 const units = ['B', 'KB', 'MB', 'GB', 'TB']
322 let size = sizeInBytes
323 let unitIndex = 0
324
325 while (size >= 1024 && unitIndex < units.length - 1) {
326 size /= 1024
327 unitIndex++
328 }
329
330 return `${size.toFixed(1)} ${units[unitIndex]}`
331 }
332
333 const getCategoryType = (category) => {
vulgar52015dc93392025-06-07 12:01:09 +0800334 if (!category) return ''
vulgar520152afbcf2025-06-07 02:34:46 +0800335 const types = {
336 'os': 'primary',
337 'movie': 'success',
vulgar52015dc93392025-06-07 12:01:09 +0800338 'db': 'info',
vulgar520152afbcf2025-06-07 02:34:46 +0800339 'music': 'warning',
340 'software': 'danger'
341 }
vulgar52015dc93392025-06-07 12:01:09 +0800342 return types[category.slug] || ''
vulgar520152afbcf2025-06-07 02:34:46 +0800343 }
344
vulgar52019bf462d2025-06-07 17:54:04 +0800345 const categoryOptions = computed(() => {
346 return [
347 { id: '', name: '全部' },
348 ...categories.value
349 ]
350 })
351
vulgar520152afbcf2025-06-07 02:34:46 +0800352 return {
353 loading,
354 searchQuery,
355 selectedCategory,
356 sortBy,
357 sortOrder,
358 currentPage,
359 pageSize,
360 totalCount,
361 torrents,
vulgar52015dc93392025-06-07 12:01:09 +0800362 categories,
vulgar52019bf462d2025-06-07 17:54:04 +0800363 categoryOptions,
vulgar520152afbcf2025-06-07 02:34:46 +0800364 handleSearch,
365 handleFilter,
366 handleRowClick,
367 handleDownload,
368 handleSizeChange,
369 handleCurrentChange,
370 formatTime,
371 formatSize,
372 getCategoryType,
373 Search,
374 Upload,
375 Download,
376 User,
377 Clock,
378 Document
379 }
380 }
381}
382</script>
383
384<style lang="scss" scoped>
385.torrents-page {
386 max-width: 1200px;
387 margin: 0 auto;
388 padding: 24px;
389}
390
391.page-header {
392 display: flex;
393 justify-content: space-between;
394 align-items: center;
395 margin-bottom: 24px;
396
397 h1 {
398 font-size: 28px;
399 font-weight: 600;
400 color: #2c3e50;
401 margin: 0;
402 }
403}
404
405.search-section {
406 background: #fff;
407 border-radius: 12px;
408 padding: 24px;
409 margin-bottom: 24px;
410 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
411
412 .search-bar {
413 display: flex;
414 gap: 12px;
415 margin-bottom: 16px;
416
417 .el-input {
418 flex: 1;
419 }
420 }
421
422 .filters {
423 display: flex;
424 gap: 16px;
425 flex-wrap: wrap;
426 align-items: center;
427
428 .el-select {
429 width: 120px;
430 }
431 }
432}
433
434.torrents-list {
435 background: #fff;
436 border-radius: 12px;
437 padding: 24px;
438 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
439
440 .list-header {
441 margin-bottom: 16px;
442
443 .results-count {
444 font-size: 14px;
445 color: #909399;
446 }
447 }
448
449 .torrents-table {
450 .torrent-info {
451 .torrent-title {
452 font-size: 16px;
453 font-weight: 500;
454 color: #2c3e50;
455 margin: 0 0 8px 0;
456 line-height: 1.4;
457 cursor: pointer;
458
459 &:hover {
460 color: #409eff;
461 }
462 }
463
464 .torrent-meta {
465 display: flex;
466 gap: 16px;
467 font-size: 12px;
468 color: #909399;
469
470 span {
471 display: flex;
472 align-items: center;
473 gap: 4px;
474 }
475 }
476 }
477
478 .seeders {
479 color: #67c23a;
480 font-weight: 600;
481 }
482
483 .leechers {
484 color: #f56c6c;
485 font-weight: 600;
486 }
487 }
488
489 .pagination-wrapper {
490 margin-top: 24px;
491 text-align: center;
492 }
493}
494
495@media (max-width: 768px) {
496 .torrents-page {
497 padding: 16px;
498 }
499
500 .page-header {
501 flex-direction: column;
502 gap: 16px;
503 align-items: flex-start;
504 }
505
506 .filters {
507 flex-direction: column;
508 align-items: flex-start;
509
510 .el-select {
511 width: 100%;
512 }
513 }
514
515 .torrents-table {
516 :deep(.el-table__header),
517 :deep(.el-table__body) {
518 font-size: 12px;
519 }
520 }
521}
xingjinwend652cc62025-06-04 19:52:19 +0800522</style>