feat: 第一次生成

This commit is contained in:
2025-05-04 20:37:40 +08:00
commit 5c01df4a93
42 changed files with 12705 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
+147
View File
@@ -0,0 +1,147 @@
<template>
<div class="app">
<header class="header">
<div class="logo-container">
<img :src="logoImg" alt="百盘搜" class="logo">
<h1 class="site-title">百盘搜</h1>
</div>
<div class="search-container">
<input type="text" placeholder="请输入您要搜索的关键词" v-model="searchKeyword" @keyup.enter="handleSearch">
<button @click="handleSearch" class="search-button">
<img :src="searchIcon" alt="搜索" class="search-icon">
</button>
</div>
</header>
<div class="notice-bar">
<span class="notice-icon">📢</span>
<span>备用站点</span>
<a href="https://www.feizhupan.com" target="_blank">https://www.feizhupan.com</a>
<a href="https://www.dashengpan.xyz" target="_blank">https://www.dashengpan.xyz</a>
<span>请保存到浏览器书签中</span>
</div>
<main class="main-content">
<NuxtPage />
</main>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import logoImg from '~/assets/logo.png';
import searchIcon from '~/assets/search-icon.svg';
const router = useRouter();
const searchKeyword = ref('');
function handleSearch() {
if (searchKeyword.value.trim()) {
router.push({ path: '/', query: { keyword: searchKeyword.value } });
}
}
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: #f5f5f5;
color: #333;
}
.app {
display: flex;
flex-direction: column;
min-height: 100vh;
align-items: center;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background-color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 1200px;
}
.logo-container {
display: flex;
align-items: center;
}
.logo {
width: 40px;
height: 40px;
margin-right: 0.5rem;
}
.site-title {
font-size: 1.5rem;
font-weight: bold;
color: #ff9500;
}
.search-container {
display: flex;
width: 50%;
max-width: 600px;
border: 2px solid #ff9500;
border-radius: 20px;
overflow: hidden;
}
.search-container input {
flex-grow: 1;
padding: 0.6rem 1rem;
border: none;
outline: none;
font-size: 1rem;
}
.search-button {
background-color: #ff9500;
border: none;
padding: 0.6rem 1rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.search-icon {
width: 20px;
height: 20px;
}
.notice-bar {
background-color: #fff8e6;
padding: 0.5rem 2rem;
color: #9e6500;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
width: 100%;
max-width: 1200px;
}
.notice-bar a {
color: #ff9500;
text-decoration: none;
}
.main-content {
flex: 1;
padding: 2rem;
width: 100%;
max-width: 1200px;
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>

After

Width:  |  Height:  |  Size: 278 B

+153
View File
@@ -0,0 +1,153 @@
<template>
<div class="result-item">
<div class="result-icon">
<img :src="getFileTypeIcon(item.fileType)" :alt="item.fileType" />
</div>
<div class="result-content">
<h3 class="result-title">{{ item.title }}</h3>
<div class="result-details">
<div v-if="item.fileList && item.fileList.length" class="file-list">
<div v-for="(file, index) in visibleFiles" :key="index" class="file-item">
<span> {{ file.name }} - {{ formatSize(file.size) }}</span>
</div>
<div v-if="item.fileList.length > maxVisibleFiles" class="more-files">
...还有{{ item.fileList.length - maxVisibleFiles }}个文件
</div>
</div>
<div class="result-meta">
<span class="size">文件大小: {{ formatSize(item.size) }}</span>
<span class="date">更新时间: {{ item.updateTime }}</span>
</div>
</div>
</div>
<div class="result-actions">
<button class="get-link-button" @click="$emit('get-link', item)">
获取链接
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface File {
name: string;
size: number;
}
interface SearchResult {
id: string;
title: string;
fileType: string;
size: number;
updateTime: string;
cloudType: string;
fileList?: File[];
}
const props = defineProps<{ item: SearchResult }>()
const emit = defineEmits<{ (e: 'get-link', item: SearchResult): void }>()
const maxVisibleFiles = 5
const visibleFiles = computed(() => {
if (!props.item.fileList) return []
return props.item.fileList.slice(0, maxVisibleFiles)
})
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 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'
}
</script>
<style scoped>
.result-item {
display: flex;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
}
.result-icon {
margin-right: 16px;
}
.result-icon img {
width: 40px;
height: 40px;
}
.result-content {
flex: 1;
}
.result-title {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
color: #333;
}
.file-list {
margin-bottom: 8px;
}
.file-item {
margin-bottom: 4px;
color: #666;
font-size: 14px;
}
.more-files {
color: #999;
font-style: italic;
font-size: 13px;
margin-top: 2px;
}
.result-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
color: #666;
font-size: 14px;
}
.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;
transition: background-color 0.2s;
}
.get-link-button:hover {
background-color: #e68600;
}
</style>
+5
View File
@@ -0,0 +1,5 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-11-01',
devtools: { enabled: true }
})
+17
View File
@@ -0,0 +1,17 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"nuxt": "^3.17.1",
"vue": "^3.5.13",
"vue-router": "^4.5.1"
}
}
+637
View File
@@ -0,0 +1,637 @@
<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>
+7862
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

+2
View File
@@ -0,0 +1,2 @@
User-Agent: *
Disallow:
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}
+4
View File
@@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}