chore: initial commit - CloudSearch v0.0.2
This commit is contained in:
351
packages/frontend/HomePage.vue
Executable file
351
packages/frontend/HomePage.vue
Executable file
@@ -0,0 +1,351 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<div class="hero-section">
|
||||
<template v-if="configLoaded">
|
||||
<img v-if="siteLogo" :src="siteLogo" :alt="siteName || 'CloudSearch'" class="logo-img" @error="(e: any) => { (e.target as HTMLElement).style.display='none'; siteLogo='' }" />
|
||||
<div v-else class="logo-text">{{ siteName || 'CloudSearch' }}</div>
|
||||
</template>
|
||||
<div class="search-box">
|
||||
<el-input
|
||||
v-model="query"
|
||||
placeholder="搜索网盘资源,或粘贴视频/网盘链接..."
|
||||
size="large"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" size="large" @click="handleSearch" class="search-btn">
|
||||
搜 索
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="quote-section" v-if="currentQuote">
|
||||
<span class="quote-text">「 {{ currentQuote }} 」</span>
|
||||
<span class="quote-author">---{{ quoteAuthor }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-section">
|
||||
<PromotionBanner v-if="homeBanners.length > 0" :promotions="homeBanners" />
|
||||
|
||||
<div v-if="categories.length > 0" class="rankings-grid">
|
||||
<div
|
||||
v-for="cat in categories"
|
||||
:key="cat.category"
|
||||
class="rank-panel"
|
||||
>
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">{{ getCategoryIcon(cat.category) }} {{ cat.label }}</span>
|
||||
<div class="panel-tabs">
|
||||
<span class="panel-tab" :class="{ active: activeTab[cat.category] === 'hot' }" @click="switchTab(cat.category, 'hot')">热榜</span>
|
||||
<span class="panel-tab" :class="{ active: activeTab[cat.category] === 'newest' }" @click="switchTab(cat.category, 'newest')">最新</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div
|
||||
v-for="(item, idx) in visibleItems(cat)"
|
||||
:key="cat.category + '-' + idx"
|
||||
class="rank-item"
|
||||
@click="searchTag(item.keyword)"
|
||||
>
|
||||
<span class="rank-idx" :class="{ 'top-three': idx < 3 }">{{ idx + 1 }}</span>
|
||||
<span class="rank-name">{{ item.keyword }}</span>
|
||||
<span class="rank-cnt" v-if="item.rating">⭐{{ item.rating }}</span>
|
||||
<span class="rank-cnt" v-else>{{ item.searchCount }}</span>
|
||||
</div>
|
||||
<!-- 展开按钮 -->
|
||||
<div v-if="hasMoreItems(cat)" class="rank-expand" @click="expandCategory(cat.category)">
|
||||
展开全部 ▼
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span v-if="cat.category !== 'hotsite'">数据来源:TMDB</span>
|
||||
<span v-else>本站搜索数据</span>
|
||||
<span class="footer-time">{{ fetchedAt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="siteDisclaimer" class="site-footer">
|
||||
<div class="footer-inner">{{ siteDisclaimer }}</div>
|
||||
<div class="footer-actions">
|
||||
<el-button class="footer-disclaimer-btn" size="small" @click="openDisclaimer">📜 免责声明</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import PromotionBanner from '../components/PromotionBanner.vue'
|
||||
import { getCategorizedRankings, getPromotions, getSiteConfig } from '../api'
|
||||
import type { Promotion } from '../types'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const query = ref('')
|
||||
const categories = ref<any[]>([])
|
||||
const expanded = reactive<Record<string, boolean>>({})
|
||||
const activeTab = reactive<Record<string, string>>({})
|
||||
const homeBanners = ref<Promotion[]>([])
|
||||
const siteLogo = ref('')
|
||||
const siteName = ref('')
|
||||
const siteDisclaimer = ref('')
|
||||
const configLoaded = ref(false)
|
||||
const currentQuote = ref('')
|
||||
const quoteAuthor = ref('')
|
||||
const fetchedAt = ref('')
|
||||
|
||||
const INITIAL_SHOW = 8
|
||||
|
||||
const CATEGORY_ICONS: Record<string, string> = {
|
||||
movie: '🎬', tv: '📺', western_movie: '🎥', western: '🌍',
|
||||
donghua: '🐉', global_anime: '🌐',
|
||||
domestic_variety: '🎤', global_variety: '🎭',
|
||||
niche: '💎', hotsite: '🏆',
|
||||
}
|
||||
|
||||
function getCategoryIcon(cat: string): string {
|
||||
return CATEGORY_ICONS[cat] || '📋'
|
||||
}
|
||||
|
||||
function getItems(cat: any): any[] {
|
||||
const tab = activeTab[cat.category] || 'hot'
|
||||
return tab === 'hot' ? (cat.hot || []) : (cat.newest || [])
|
||||
}
|
||||
|
||||
function visibleItems(cat: any): any[] {
|
||||
const items = getItems(cat)
|
||||
return expanded[cat.category] ? items : items.slice(0, INITIAL_SHOW)
|
||||
}
|
||||
|
||||
function hasMoreItems(cat: any): boolean {
|
||||
const items = getItems(cat)
|
||||
return items.length > INITIAL_SHOW && !expanded[cat.category]
|
||||
}
|
||||
|
||||
function expandCategory(category: string) {
|
||||
expanded[category] = true
|
||||
}
|
||||
|
||||
function switchTab(category: string, tab: string) {
|
||||
activeTab[category] = tab
|
||||
// 切换标签时收起展开
|
||||
expanded[category] = false
|
||||
}
|
||||
|
||||
function openDisclaimer() {
|
||||
window.open('/disclaimer/', '_blank')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 一言 API
|
||||
try {
|
||||
const res = await fetch('https://v1.hitokoto.cn/')
|
||||
const data = await res.json()
|
||||
currentQuote.value = data.hitokoto || ''
|
||||
quoteAuthor.value = data.from_who || data.from || ''
|
||||
} catch {
|
||||
currentQuote.value = '学而时习之,不亦说乎。'
|
||||
quoteAuthor.value = '孔子'
|
||||
}
|
||||
|
||||
try {
|
||||
const [catsData, promos, siteCfg] = await Promise.all([
|
||||
getCategorizedRankings(),
|
||||
getPromotions(),
|
||||
getSiteConfig(),
|
||||
])
|
||||
// 新格式: { fetchedAt, categories }
|
||||
if (catsData.fetchedAt) {
|
||||
fetchedAt.value = catsData.fetchedAt
|
||||
categories.value = catsData.categories || []
|
||||
} else {
|
||||
// 兼容旧格式
|
||||
categories.value = Array.isArray(catsData) ? catsData : []
|
||||
}
|
||||
for (const cat of categories.value) {
|
||||
activeTab[cat.category] = 'hot'
|
||||
expanded[cat.category] = false
|
||||
}
|
||||
homeBanners.value = promos.filter((p) => p.position === 'home_banner' && p.active)
|
||||
if (siteCfg.site_logo) {
|
||||
siteLogo.value = siteCfg.site_logo
|
||||
}
|
||||
if (siteCfg.site_name) {
|
||||
siteName.value = siteCfg.site_name
|
||||
}
|
||||
if (siteCfg.site_disclaimer) {
|
||||
siteDisclaimer.value = siteCfg.site_disclaimer
|
||||
}
|
||||
configLoaded.value = true
|
||||
} catch (e) {
|
||||
console.error('加载首页数据失败', e)
|
||||
}
|
||||
})
|
||||
|
||||
function handleSearch() {
|
||||
const q = query.value.trim()
|
||||
if (q) router.push('/search?q=' + encodeURIComponent(q))
|
||||
}
|
||||
|
||||
function searchTag(tag: string) {
|
||||
router.push('/search?q=' + encodeURIComponent(tag))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-page { min-height: 100vh; display: flex; flex-direction: column; }
|
||||
|
||||
.hero-section {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
padding: 60px 24px 40px;
|
||||
}
|
||||
.logo-text { font-size: 64px; font-weight: 700; color: var(--primary-color); margin-bottom: 32px; letter-spacing: -2px; }
|
||||
.logo-img { max-width: 500px; max-height: 120px; width: auto; height: auto; object-fit: contain; margin-bottom: 32px; }
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%; max-width: 640px;
|
||||
border: 1px solid #dfe1e5;
|
||||
border-radius: 24px;
|
||||
background: #fff;
|
||||
box-shadow: none;
|
||||
transition: box-shadow .2s, border-color .2s;
|
||||
overflow: hidden;
|
||||
}
|
||||
.search-box:focus-within {
|
||||
box-shadow: 0 1px 6px rgba(32,33,36,.28);
|
||||
border-color: rgba(223,225,229,0);
|
||||
}
|
||||
.search-box :deep(.el-input__wrapper) {
|
||||
border: none; box-shadow: none; background: transparent;
|
||||
padding: 4px 20px; border-radius: 0;
|
||||
}
|
||||
.search-box :deep(.el-input__inner) {
|
||||
font-size: 15px;
|
||||
}
|
||||
.search-btn {
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 0 24px;
|
||||
height: 38px;
|
||||
line-height: 38px;
|
||||
margin: 4px;
|
||||
font-size: 14px; font-weight: 600;
|
||||
background: var(--primary-color); color: #fff;
|
||||
cursor: pointer; transition: all .2s;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.search-btn:hover {
|
||||
background: #3a7be0;
|
||||
}
|
||||
.search-btn:active {
|
||||
background: #2d6ccf;
|
||||
}
|
||||
|
||||
.quote-section { margin-top: 18px; max-width: 640px; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.quote-text { font-size: 14px; color: #aab0b8; font-style: italic; letter-spacing: 0.5px; }
|
||||
.quote-author { font-size: 12px; color: #c0c4cc; display: inline-block; margin-left: 4px; }
|
||||
|
||||
.content-section { max-width: 1500px; width: 100%; margin: 0 auto; padding: 0 16px 60px; }
|
||||
|
||||
.rankings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.rank-panel {
|
||||
background: var(--bg-white,#fff);
|
||||
border-radius: 12px; padding: 14px; border: 1px solid #ebeef5;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.04); display: flex; flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding-bottom: 10px; border-bottom: 2px solid #f0f0f0; margin-bottom: 4px;
|
||||
}
|
||||
.panel-title { font-size: 15px; font-weight: 700; color: #303133; white-space: nowrap; }
|
||||
.panel-tabs { display: flex; gap: 2px; background: #f0f2f5; border-radius: 6px; padding: 2px; }
|
||||
.panel-tab {
|
||||
font-size: 11px; padding: 3px 10px; border-radius: 5px; cursor: pointer;
|
||||
color: #909399; font-weight: 500; transition: all .2s; user-select: none;
|
||||
}
|
||||
.panel-tab.active { background: #fff; color: var(--primary-color); font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
|
||||
|
||||
.panel-body { flex: 1; display: flex; flex-direction: column; gap: 1px; }
|
||||
|
||||
.rank-item {
|
||||
display: flex; align-items: center; gap: 8px; padding: 5px 6px;
|
||||
border-radius: 6px; cursor: pointer; transition: background .15s;
|
||||
}
|
||||
.rank-item:hover { background: #f0f5ff; }
|
||||
.rank-item:active { background: #e6f0ff; }
|
||||
|
||||
.rank-idx {
|
||||
width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center;
|
||||
justify-content: center; font-size: 12px; font-weight: 700;
|
||||
color: #909399; background: #f0f0f0; flex-shrink: 0;
|
||||
}
|
||||
.rank-idx.top-three { background: var(--primary-color); color: #fff; }
|
||||
.rank-name { flex: 1; min-width: 0; font-size: 13px; font-weight: 500; color: #303133; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.rank-cnt { font-size: 11px; color: #c0c4cc; white-space: nowrap; flex-shrink: 0; }
|
||||
|
||||
.rank-expand {
|
||||
text-align: center; padding: 6px; margin-top: 2px; font-size: 12px;
|
||||
color: var(--primary-color); cursor: pointer; border-radius: 6px;
|
||||
transition: background .15s; user-select: none;
|
||||
}
|
||||
.rank-expand:hover { background: #ecf5ff; }
|
||||
|
||||
.panel-footer {
|
||||
margin-top: 8px; padding-top: 8px; border-top: 1px solid #f0f0f0;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
font-size: 11px; color: #c0c4cc;
|
||||
}
|
||||
.footer-time { font-family: monospace; font-size: 10px; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero-section { padding: 36px 16px 24px; }
|
||||
.logo-text { font-size: 36px; margin-bottom: 20px; }
|
||||
.logo-img { max-width: 360px; max-height: 80px; margin-bottom: 20px; }
|
||||
.rankings-scroll { gap: 12px; }
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
margin-top: auto;
|
||||
padding: 20px 16px 32px;
|
||||
background: #f9fafb;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
.footer-inner {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.footer-disclaimer-btn {
|
||||
font-size: 12px !important;
|
||||
color: #909399 !important;
|
||||
}
|
||||
.footer-disclaimer-btn:hover {
|
||||
color: #409eff !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user