Files
bps/frontend/pages/index.vue
T
2025-05-04 20:37:40 +08:00

637 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="search-page">
<div class="filter-bar">
<div>
筛选
<div class="filter-group">
<span>模式</span>
<select v-model="filterMode">
<option value="standard">精准搜索</option>
<option value="fuzzy">模糊搜索</option>
</select>
</div>
<div class="filter-group">
<span>文件类型</span>
<select v-model="fileType">
<option value="">全部</option>
<option value="document">文档</option>
<option value="video">视频</option>
<option value="audio">音频</option>
<option value="image">图片</option>
</select>
</div>
<div class="filter-group">
<span>文件大小</span>
<select v-model="fileSize">
<option value="">全部</option>
<option value="small">(< 10MB)</option>
<option value="medium">(10MB-100MB)</option>
<option value="large">(100MB-1GB)</option>
<option value="huge">超大(> 1GB)</option>
</select>
</div>
<div class="filter-group">
<span>文件时间</span>
<select v-model="fileTime">
<option value="">全部</option>
<option value="today">今天</option>
<option value="week">本周</option>
<option value="month">本月</option>
<option value="year">今年</option>
</select>
</div>
<div class="filter-group">
<button class="clear-button" @click="clearFilters">
<span>清除</span>
</button>
</div>
</div>
</div>
<div class="cloud-filter">
<div>
网盘
<button
v-for="drive in driveTypes"
:key="drive.value"
:class="['cloud-button', { active: selectedDrives.includes(drive.value) }]"
@click="toggleDrive(drive.value)"
>
<img :src="drive.icon" :alt="drive.label" class="cloud-icon" />
<span>{{ drive.label }}</span>
</button>
</div>
</div>
<div class="search-tips" v-if="!searched">
<h3>搜索小技巧</h3>
<ol>
<li>可以灵活选用精准搜索模糊搜索</li>
<li>搜索资源时可以通过资源类型」、「时间等进行筛选</li>
<li>增加文件来源百度网盘阿里云盘等进行筛选</li>
<li>搜索关键次尽量不要包含」「等无关助词只包含关键词即可</li>
<li>如搜索过于频繁被屏蔽请发送UID信息给shakanamo945@gmail.com申请解封</li>
<li>如有希望我们重点关注或收录的资源网站来源限免费开放式网站),请发送邮件至zhuangbee8287@163.com</li>
<li>相关版权方对于搜索关键词屏蔽需求请通过发送邮件至shakanamo945@gmail.com;</li>
<li>坚决抵制劣质违规隐私风险版权其他问题资源请大家发现一律通过页面举报</li>
<li>备用站点 <a href="https://www.feizhupan.com">https://www.feizhupan.com</a><a href="https://www.dashengpan.xyz">https://www.dashengpan.xyz</a>;</li>
</ol>
</div>
<div v-if="searched">
<div v-if="results.length === 0" class="no-results">
<p>没有找到相关结果请尝试其他关键词或筛选条件</p>
</div>
<div v-else class="results-container">
<div v-for="(group, index) in groupedResults" :key="index" class="result-group">
<div class="cloud-header">
<img :src="getCloudIcon(group.cloudType)" :alt="group.cloudType" class="cloud-icon" />
<span class="cloud-name">{{ getCloudName(group.cloudType) }}</span>
<span class="result-count">共搜出 {{ group.items.length }} 条结果</span>
</div>
<div class="result-list">
<div v-for="item in group.items" :key="item.id" class="result-item">
<div class="result-icon">
<img :src="getFileTypeIcon(item.fileType)" :alt="item.fileType" />
</div>
<div class="result-content">
<div class="result-title">{{ item.title }}</div>
<div class="result-details">
<div v-if="item.fileList && item.fileList.length" class="file-list">
<div v-for="file in item.fileList" :key="file.name" class="file-item">
<span> {{ file.name }} - {{ formatSize(file.size) }}</span>
</div>
<div v-if="item.fileList.length > 5" class="more-files">
...
</div>
</div>
<div class="result-meta">
<span>文件大小: {{ formatSize(item.size) }}</span>
<span>更新时间: {{ formatDate(item.updateTime) }}</span>
</div>
</div>
</div>
<div class="result-actions">
<button class="get-link-button" @click="getLink(item)">
获取链接
</button>
</div>
</div>
</div>
</div>
</div>
<div class="pagination" v-if="totalPages > 1">
<button :disabled="page === 1" @click="changePage(page - 1)">上一页</button>
<div class="page-numbers">
<button
v-for="p in displayedPages"
:key="p"
:class="['page-number', { active: p === page }]"
@click="changePage(p)"
>
{{ p }}
</button>
</div>
<button :disabled="page === totalPages" @click="changePage(page + 1)">下一页</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const keyword = ref('')
const filterMode = ref('standard')
const fileType = ref('')
const fileSize = ref('')
const fileTime = ref('')
const selectedDrives = ref<string[]>(['baidu', 'aliyun', 'quark', 'xunlei'])
const page = ref(1)
const totalPages = ref(1)
const results = ref<any[]>([])
const searched = ref(false)
const loading = ref(false)
const driveTypes = [
{ value: 'baidu', label: '百度网盘', icon: '/images/baidu.png' },
{ value: 'aliyun', label: '阿里云盘', icon: '/images/aliyun.png' },
{ value: 'quark', label: '夸克网盘', icon: '/images/quark.png' },
{ value: 'xunlei', label: '迅雷网盘', icon: '/images/xunlei.png' },
]
// 模拟数据
const mockResults = [
{
id: '1',
title: 'Deepseek',
fileType: 'folder',
size: 312 * 1024 * 1024 * 1024,
updateTime: '2025-04-15',
cloudType: 'quark',
fileList: [
{ name: 'deepseek.apk', size: 8.8 * 1024 * 1024 },
{ name: 'DeepSeek.dmg', size: 14.6 * 1024 * 1024 },
{ name: 'DeepSeek_x64.msi', size: 6.1 * 1024 * 1024 },
{ name: 'DeepSeek15天指导手册.pdf', size: 1.3 * 1024 * 1024 },
{ name: 'DeepSeek_x86_64.deb', size: 8.2 * 1024 * 1024 },
]
},
{
id: '2',
title: 'Deepseek',
fileType: 'folder',
size: 311.8 * 1024 * 1024 * 1024,
updateTime: '2025-03-07',
cloudType: 'quark',
fileList: [
{ name: 'deepseek.apk', size: 8.8 * 1024 * 1024 },
{ name: 'DeepSeek.dmg', size: 14.6 * 1024 * 1024 },
{ name: 'DeepSeek_x64.msi', size: 6.1 * 1024 * 1024 },
{ name: 'DeepSeek15天指导手册.pdf', size: 1.3 * 1024 * 1024 },
{ name: 'DeepSeek_x86_64.deb', size: 8.2 * 1024 * 1024 },
]
}
]
const groupedResults = computed(() => {
const groups: Record<string, { cloudType: string, items: any[] }> = {}
results.value.forEach(result => {
if (!groups[result.cloudType]) {
groups[result.cloudType] = {
cloudType: result.cloudType,
items: []
}
}
groups[result.cloudType].items.push(result)
})
return Object.values(groups)
})
const displayedPages = computed(() => {
const totalDisplayed = 5
const currentPage = page.value
const total = totalPages.value
if (total <= totalDisplayed) {
return Array.from({ length: total }, (_, i) => i + 1)
}
let start = Math.max(currentPage - Math.floor(totalDisplayed / 2), 1)
let end = start + totalDisplayed - 1
if (end > total) {
end = total
start = Math.max(end - totalDisplayed + 1, 1)
}
return Array.from({ length: end - start + 1 }, (_, i) => start + i)
})
onMounted(() => {
const queryKeyword = route.query.keyword as string
if (queryKeyword) {
keyword.value = queryKeyword
onSearch()
}
})
function toggleDrive(drive: string) {
const index = selectedDrives.value.indexOf(drive)
if (index === -1) {
selectedDrives.value.push(drive)
} else {
selectedDrives.value.splice(index, 1)
}
if (searched.value) {
fetchResults()
}
}
function clearFilters() {
filterMode.value = 'standard'
fileType.value = ''
fileSize.value = ''
fileTime.value = ''
selectedDrives.value = ['baidu', 'aliyun', 'quark', 'xunlei']
if (searched.value) {
fetchResults()
}
}
async function onSearch() {
if (!keyword.value.trim()) return
page.value = 1
await fetchResults()
}
async function fetchResults() {
loading.value = true
searched.value = true
try {
// 构建API请求参数
const params = new URLSearchParams();
if (keyword.value) params.append('keyword', keyword.value);
selectedDrives.value.forEach(drive => {
params.append('types', drive);
});
params.append('page', page.value.toString());
if (fileType.value) params.append('fileType', fileType.value);
if (fileSize.value) params.append('fileSize', fileSize.value);
if (fileTime.value) params.append('fileTime', fileTime.value);
params.append('mode', filterMode.value);
// 调用后端API
const response = await fetch(`/api/search?${params.toString()}`);
const responseData = await response.json();
if (responseData.success) {
results.value = responseData.data;
totalPages.value = responseData.totalPages || 1;
} else {
console.error('搜索失败:', responseData.message);
results.value = [];
}
// 模拟数据(当后端未启动时)
if (results.value.length === 0) {
results.value = mockResults;
totalPages.value = 5;
}
} catch (error) {
console.error('搜索失败', error);
// 使用模拟数据
results.value = mockResults;
totalPages.value = 5;
} finally {
loading.value = false;
}
}
function changePage(newPage: number) {
page.value = newPage
// 更新URL但不触发路由变化
router.replace({
query: {
...route.query,
page: newPage.toString()
}
})
fetchResults()
}
async function getLink(item: any) {
try {
// 调用后端生成链接API
const response = await fetch(`/api/link/${item.id}`);
const data = await response.json();
if (data.success && data.data.link) {
// 显示链接
alert(`获取链接成功: ${data.data.link}\n有效期: ${data.data.expiresIn}秒`);
} else {
alert(`获取链接失败: ${data.message || '未知错误'}`);
}
} catch (error) {
console.error('获取链接失败', error);
alert(`获取链接失败: 服务异常,请稍后再试`);
}
}
function getCloudIcon(cloudType: string) {
const drive = driveTypes.find(d => d.value === cloudType)
return drive?.icon || '/icons/default-cloud.png'
}
function getCloudName(cloudType: string) {
const drive = driveTypes.find(d => d.value === cloudType)
return drive?.label || cloudType
}
function getFileTypeIcon(fileType: string) {
const icons: Record<string, string> = {
folder: '/icons/folder.png',
document: '/icons/document.png',
video: '/icons/video.png',
audio: '/icons/audio.png',
image: '/icons/image.png',
}
return icons[fileType] || '/icons/file.png'
}
function formatSize(bytes: number) {
if (bytes === 0) return '0 B'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(1024))
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i]
}
function formatDate(date: string) {
return date
}
</script>
<style scoped>
.search-page {
max-width: 1200px;
margin: 0 auto;
}
.filter-bar, .cloud-filter {
background-color: #fff;
padding: 12px 16px;
border-radius: 4px;
margin-bottom: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
display: flex;
flex-wrap: wrap;
}
.filter-group {
display: inline-flex;
align-items: center;
margin-right: 16px;
margin-bottom: 8px;
}
.filter-group span {
margin-right: 8px;
font-size: 14px;
color: #666;
}
.filter-group select {
padding: 6px 8px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
}
.clear-button {
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
}
.cloud-button {
display: inline-flex;
align-items: center;
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
margin-right: 12px;
cursor: pointer;
}
.cloud-button.active {
background-color: #e6f7ff;
border-color: #1890ff;
color: #1890ff;
}
.cloud-icon {
width: 16px;
height: 16px;
margin-right: 6px;
}
.search-tips {
background-color: #fff;
padding: 20px;
border-radius: 4px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.search-tips h3 {
margin-bottom: 12px;
font-size: 16px;
color: #333;
}
.search-tips ol {
padding-left: 24px;
}
.search-tips li {
margin-bottom: 8px;
line-height: 1.5;
color: #555;
}
.search-tips a {
color: #1890ff;
text-decoration: none;
}
.no-results {
background-color: #fff;
padding: 40px;
text-align: center;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.results-container {
margin-bottom: 20px;
}
.result-group {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
overflow: hidden;
}
.cloud-header {
padding: 12px 16px;
background-color: #f9f9f9;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
}
.cloud-name {
font-weight: 500;
margin-right: 12px;
}
.result-count {
color: #ff9500;
font-size: 14px;
}
.result-list {
padding: 0 16px;
}
.result-item {
display: flex;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
}
.result-item:last-child {
border-bottom: none;
}
.result-icon {
margin-right: 16px;
}
.result-icon img {
width: 40px;
height: 40px;
}
.result-content {
flex: 1;
}
.result-title {
font-weight: 500;
margin-bottom: 8px;
font-size: 16px;
}
.result-details {
font-size: 14px;
color: #666;
}
.file-list {
margin-bottom: 8px;
}
.file-item {
margin-bottom: 4px;
color: #888;
}
.more-files {
color: #888;
padding-left: 16px;
}
.result-meta {
display: flex;
gap: 16px;
}
.result-actions {
margin-left: 16px;
display: flex;
align-items: center;
}
.get-link-button {
background-color: #ff9500;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
font-weight: 500;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
margin: 20px 0;
}
.pagination button {
padding: 8px 16px;
border: 1px solid #ddd;
background-color: white;
cursor: pointer;
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-numbers {
display: flex;
margin: 0 8px;
}
.page-number {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 4px;
}
.page-number.active {
background-color: #ff9500;
color: white;
border-color: #ff9500;
}
</style>