637 lines
16 KiB
Vue
637 lines
16 KiB
Vue
<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> |