2145 lines
56 KiB
Vue
Executable File
2145 lines
56 KiB
Vue
Executable File
<template>
|
||
<div class="search-result-page">
|
||
<!-- 顶部固定搜索栏 -->
|
||
<div class="top-search-bar">
|
||
<div class="search-bar-inner">
|
||
<router-link to="/" class="logo-link" title="返回首页">
|
||
<img v-if="siteLogo" :src="siteLogo" :alt="siteName || '首页'" class="logo-img" @error="(e: any) => e.target.style.display='none'" />
|
||
<div v-else-if="siteName" class="logo-text-only">{{ siteName }}</div>
|
||
<div v-else class="logo-icon">
|
||
<svg viewBox="0 0 28 28" width="28" height="28" fill="none">
|
||
<circle cx="14" cy="14" r="13" stroke="var(--primary-color)" stroke-width="2"/>
|
||
<path d="M8 14l4 4 8-8" stroke="var(--primary-color)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
</div>
|
||
</router-link>
|
||
<div class="search-box-inner">
|
||
<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="result-search-btn">搜 索</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 滚动通知条(跑马灯) -->
|
||
<div v-if="siteMarquee" class="marquee-bar">
|
||
<span class="marquee-icon marquee-icon-left">📢</span>
|
||
<div class="marquee-track">
|
||
<span class="marquee-text">{{ siteMarquee }}</span>
|
||
</div>
|
||
<span class="marquee-icon marquee-icon-right">📢</span>
|
||
</div>
|
||
|
||
<div class="result-content">
|
||
<!-- 搜索结果信息栏 -->
|
||
<div v-if="intent === 'SEARCH' && !loading" class="result-info-bar">
|
||
<div class="info-left">
|
||
<span v-if="totalCount > 0" class="info-item info-count">已为您挑选到最符合 {{ totalCount }} 条结果</span>
|
||
<span v-if="cloudTypesCount > 0" class="info-item info-type">📂 {{ cloudTypesCount }} 个网盘</span>
|
||
<span v-if="filteredCount > 0" class="filter-badge">❌ 失效 {{ filteredCount }}</span>
|
||
<span v-if="skippedCount > 0" class="skip-badge">⏭ 跳过 {{ skippedCount }}</span>
|
||
</div>
|
||
<div class="info-right">
|
||
<span v-if="searchTime > 0" class="info-item info-time">⏱ {{ searchTime }}ms</span>
|
||
<span v-if="hasMore" class="info-hasmore">📄 第 {{ currentPage }} 页</span>
|
||
<button class="refresh-btn" @click="handleRefresh" title="强制刷新">🔄 刷新</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 意图标签 -->
|
||
|
||
<!-- 搜索进度条 -->
|
||
<div v-if="loading" class="loading-section">
|
||
<div class="progress-track">
|
||
<div class="progress-bar" :style="{ width: loadingProgress + '%' }"></div>
|
||
</div>
|
||
<div class="progress-label">
|
||
<span v-if="loadingPhase === 'search'">🔍 正在搜索中...</span>
|
||
<span v-else-if="loadingPhase === 'validate'">
|
||
✅ 正在验证链接有效性
|
||
<span v-if="validationTotal > 0" class="validate-count">
|
||
({{ validationDone }} / {{ validationTotal }})
|
||
</span>
|
||
</span>
|
||
<span v-else>⏳ 加载中...</span>
|
||
<span class="progress-time">⏱ {{ searchTime }}ms</span>
|
||
</div>
|
||
<el-skeleton :rows="3" animated class="loading-skeleton" />
|
||
</div>
|
||
|
||
<!-- 网盘分类标签栏 — 仅显示有结果的类型 -->
|
||
<div v-if="intent === 'SEARCH' && visibleTabs.length > 0 && !loading" class="cloud-tabs">
|
||
<div
|
||
v-for="tab in visibleTabs"
|
||
:key="tab.type || 'all'"
|
||
class="cloud-tab"
|
||
:class="{ active: activeCloudTab === (tab.type || '') }"
|
||
@click="activeCloudTab = tab.type || ''"
|
||
>
|
||
<img v-if="tab.icon && (tab.icon.startsWith('data:') || tab.icon.startsWith('http') || tab.icon.startsWith('/'))" :src="tab.icon" class="tab-icon-img" />
|
||
<span v-else-if="tab.icon" class="tab-icon">{{ tab.icon }}</span>
|
||
{{ tab.label }}
|
||
<span v-if="tab.count > 0" class="tab-count">{{ tab.count }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 内容信息(紧凑信息条) -->
|
||
<div v-if="!loading && (contentInfo || contentTags.length > 0) && intent === 'SEARCH'" class="media-strip">
|
||
<a
|
||
v-if="contentInfo?.tmdb_url"
|
||
:href="contentInfo.tmdb_url"
|
||
target="_blank"
|
||
class="media-strip-inner"
|
||
rel="noopener"
|
||
>
|
||
<span v-if="contentInfo?.cover && !coverError" class="strip-thumb">
|
||
<img :src="contentInfo.cover" @error="coverError = true" />
|
||
</span>
|
||
<span v-else class="strip-thumb strip-thumb-fallback">🎬</span>
|
||
<span class="strip-title">{{ contentInfo?.title || query }}</span>
|
||
<span v-if="contentInfo?.year" class="strip-year">{{ contentInfo.year }}</span>
|
||
<span v-if="contentInfo?.rating" class="strip-rating">⭐ {{ contentInfo.rating }}</span>
|
||
<span v-if="contentInfo?.genres?.length" class="strip-genres">
|
||
<span v-for="(g, gi) in contentInfo.genres.slice(0, 3)" :key="gi" class="strip-genre"> {{ g }}</span>
|
||
</span>
|
||
<span v-if="contentTags.length > 0" class="strip-tags">
|
||
<span v-for="tag in contentTags.slice(0, 3)" :key="tag" class="strip-tag">{{ tag }}</span>
|
||
</span>
|
||
<span class="strip-right">信息来源 TMDB · 更多详情 →</span>
|
||
</a>
|
||
<div v-else class="media-strip-inner">
|
||
<span v-if="contentInfo?.cover && !coverError" class="strip-thumb">
|
||
<img :src="contentInfo.cover" @error="coverError = true" />
|
||
</span>
|
||
<span v-else class="strip-thumb strip-thumb-fallback">🎬</span>
|
||
<span class="strip-title">{{ contentInfo?.title || query }}</span>
|
||
<span v-if="contentInfo?.year" class="strip-year">{{ contentInfo.year }}</span>
|
||
<span v-if="contentInfo?.rating" class="strip-rating">⭐ {{ contentInfo.rating }}</span>
|
||
<span v-if="contentInfo?.genres?.length" class="strip-genres">
|
||
<span v-for="(g, gi) in contentInfo.genres.slice(0, 3)" :key="gi" class="strip-genre"> {{ g }}</span>
|
||
</span>
|
||
<span v-if="contentTags.length > 0" class="strip-tags">
|
||
<span v-for="tag in contentTags.slice(0, 3)" :key="tag" class="strip-tag">{{ tag }}</span>
|
||
</span>
|
||
<span class="strip-right">信息来源 TMDB · 更多详情 →</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 结果展示 -->
|
||
<template v-if="!loading && intent === 'SEARCH'">
|
||
<!-- 全部标签:按时间分组 + 卡片网格 -->
|
||
<div v-if="!activeCloudTab && displayedResults.length > 0" class="result-list flat-list">
|
||
<template v-for="(item, ri) in displayedResults" :key="'flat-' + ri">
|
||
<ResultCard
|
||
:data="item"
|
||
:fallbackTags="contentTags"
|
||
:fallbackImage="fallbackImage"
|
||
:loggedIn="false"
|
||
:cloudTypeMap="cloudTypeMap"
|
||
@save="handleSave"
|
||
/>
|
||
</template>
|
||
<!-- 页面内加载更多 -->
|
||
<div v-if="hasMoreLocal" class="load-more-inline">
|
||
<el-button @click="loadMoreLocal" :loading="loadingMore" class="load-more-btn">
|
||
加载更多 (已显示 {{ visibleCount }} / {{ allResultsFlat.length }})
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
<!-- 具体网盘标签:频道分组展示 -->
|
||
<div v-else-if="activeCloudTab && filteredChannels.length > 0" class="result-list channel-list">
|
||
<div
|
||
v-for="(channel, ci) in filteredChannels"
|
||
:key="'ch-' + channel.cloud_type"
|
||
class="channel-section"
|
||
>
|
||
<span class="channel-header">
|
||
<img v-if="channelIcon(channel.cloud_type).startsWith('data:') || channelIcon(channel.cloud_type).startsWith('http') || channelIcon(channel.cloud_type).startsWith('/')" :src="channelIcon(channel.cloud_type)" class="channel-icon-img" />
|
||
<span v-else class="channel-icon">{{ channelIcon(channel.cloud_type) }}</span>
|
||
<span class="channel-label">{{ channel.label }}</span>
|
||
<span class="channel-total-badge">{{ channel.count }} 条资源</span>
|
||
<span v-if="channel.newestTime" class="channel-time">🕐 {{ channel.newestTime }}</span>
|
||
</span>
|
||
<ResultCard
|
||
v-for="(item, ri) in channelVisibleItems(channel)"
|
||
:key="'ch-' + ci + '-' + ri"
|
||
:data="item"
|
||
:fallbackTags="contentTags"
|
||
:fallbackImage="fallbackImage"
|
||
:loggedIn="false"
|
||
:cloudTypeMap="cloudTypeMap"
|
||
@save="handleSave"
|
||
/>
|
||
<div v-if="channelHasMore(channel)" class="channel-load-more" @click="channelLoadMore(channel.cloud_type)">
|
||
<span class="channel-load-more-text">
|
||
展开更多 (已显示 {{ channelVisibleItems(channel).length }} / {{ channel.count }})
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 点击分类标签后无匹配结果 -->
|
||
<div v-else-if="totalCount > 0 && activeCloudTab" class="no-match-tip">
|
||
<span>当前页暂无「{{ getActiveTabLabel() }}」资源</span>
|
||
<el-button size="small" @click="loadMore" :loading="loadingMore" v-if="hasMore">
|
||
加载更多试试
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 视频解析结果 -->
|
||
<div v-else-if="!loading && intent === 'VIDEO_PARSE'" class="result-list">
|
||
<VideoResultCard
|
||
v-for="(item, index) in videoResults"
|
||
:key="index"
|
||
:data="item"
|
||
@save="handleVideoSave"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 空状态(智能分析) -->
|
||
<div v-if="!loading && !loadingMore && totalCount === 0 && results.length === 0" class="empty-wrapper">
|
||
<div class="empty-icon">🔍</div>
|
||
<div class="empty-title">没有找到相关资源</div>
|
||
<div class="empty-hint">{{ emptyHint }}</div>
|
||
<div v-if="emptyTips.length > 0" class="empty-tips">
|
||
<div v-for="(tip, ti) in emptyTips" :key="ti" class="empty-tip-item">💡 {{ tip }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 加载更多 -->
|
||
<div v-if="hasMore && intent === 'SEARCH' && !loading" class="load-more">
|
||
<el-button :loading="loadingMore" @click="loadMore">加载更多 ({{ currentPage }}/{{ totalPages }})</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 保存结果弹窗 -->
|
||
<el-dialog v-model="resultDialogVisible" width="650px" :close-on-click-modal="false" class="save-dialog">
|
||
<template #header>
|
||
<strong class="dialog-title-bold">{{ dialogTitle }}</strong>
|
||
</template>
|
||
<div class="result-dialog-content">
|
||
<!-- 进度流程 -->
|
||
<div v-if="saving" class="progress-flow">
|
||
<div class="progress-step" :class="{ active: progressStep >= 1, done: progressStep > 1 }">
|
||
<div class="step-dot">
|
||
<span v-if="progressStep > 1" class="step-check">✓</span>
|
||
<span v-else class="step-num">1</span>
|
||
</div>
|
||
<div class="step-body">
|
||
<span class="step-title">正在转存到{{ diskLabel }}...</span>
|
||
<span v-if="progressStep === 1" class="step-status loading">进行中</span>
|
||
<span v-else class="step-status done">已完成</span>
|
||
</div>
|
||
</div>
|
||
<div class="progress-step" :class="{ active: progressStep >= 2, done: progressStep > 2 }">
|
||
<div class="step-dot">
|
||
<span v-if="progressStep > 2" class="step-check">✓</span>
|
||
<span v-else class="step-num">2</span>
|
||
</div>
|
||
<div class="step-body">
|
||
<span class="step-title">正在重命名文件(防和谐)...</span>
|
||
<span v-if="progressStep === 2" class="step-status loading">进行中</span>
|
||
<span v-else-if="progressStep > 2" class="step-status done">已完成</span>
|
||
<span v-else class="step-status pending">等待中</span>
|
||
</div>
|
||
</div>
|
||
<div class="progress-step" :class="{ active: progressStep >= 3, done: progressStep > 3 }">
|
||
<div class="step-dot">
|
||
<span v-if="progressStep > 3" class="step-check">✓</span>
|
||
<span v-else class="step-num">3</span>
|
||
</div>
|
||
<div class="step-body">
|
||
<span class="step-title">正在生成分享链接...</span>
|
||
<span v-if="progressStep === 3" class="step-status loading">进行中</span>
|
||
<span v-else-if="progressStep > 3" class="step-status done">已完成</span>
|
||
<span v-else class="step-status pending">等待中</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 保存失败 -->
|
||
<div v-else-if="!saveSuccess" class="save-error">
|
||
<el-alert
|
||
type="error"
|
||
:title="saveResult?.message || (saveResult as any)?.error || '保存失败'"
|
||
show-icon
|
||
:closable="false"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 重命名信息(完成时显示) -->
|
||
<div v-if="saveSuccess && renamedFiles.length > 0 && shareLink" class="rename-info-bar">
|
||
<el-alert type="warning" :closable="false" show-icon>
|
||
<template #title>
|
||
<span style="font-size:13px">已对 {{ renamedFiles.length }} 个文件执行防和谐重命名</span>
|
||
</template>
|
||
<div v-for="r in renamedFiles" :key="r" class="rename-item">{{ r }}</div>
|
||
</el-alert>
|
||
</div>
|
||
|
||
<!-- 成功结果展示 -->
|
||
<div v-if="saveSuccess && shareLink" class="share-result">
|
||
<div class="share-layout">
|
||
<div class="qr-left">
|
||
<canvas ref="qrCanvasRef" class="qr-canvas"></canvas>
|
||
<p class="qr-hint">{{ diskLabel }}APP扫码转存</p>
|
||
<p class="qr-subhint">保存到你自己网盘</p>
|
||
<div class="qr-disclaimer-short">
|
||
<span>⚠️ 本站资源仅供学习交流,请于24h内删除</span>
|
||
</div>
|
||
</div>
|
||
<div class="link-right">
|
||
<div class="success-header">
|
||
<el-icon class="success-icon" :size="20" color="#67c23a"><CircleCheckFilled /></el-icon>
|
||
<span class="success-text">【{{ diskLabel }}】<strong>分享链接已生成!</strong></span>
|
||
</div>
|
||
<div class="link-row">
|
||
<el-input v-model="shareLink" readonly class="share-input" />
|
||
</div>
|
||
<div v-if="sharePwd" class="share-pwd-row">
|
||
<span class="pwd-label">🔑 提取密码:</span>
|
||
<el-tag type="warning">{{ sharePwd }}</el-tag>
|
||
<span class="pwd-hint">打开链接后需输入密码</span>
|
||
</div>
|
||
<div class="share-tip">
|
||
<span class="share-tip-warn">⚠️</span>
|
||
<div class="share-tip-text">
|
||
<strong>请尽快复制链接到浏览器打开</strong> 或 <strong>用{{ diskLabel }}APP扫码</strong><br>
|
||
<strong>转存至您的网盘,以免资源被官方和谐</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 郑重警告 -->
|
||
<div class="warnings-box">
|
||
<p class="warning-item">郑重警告一:网盘内除您所需资源外,不要打开任何不相关内容。</p>
|
||
<p class="warning-item">郑重警告二:网盘内除您所需资源外,不要打开任何不相关内容。</p>
|
||
<p class="warning-item">郑重警告三:网盘内除您所需资源外,不要打开任何不相关内容。</p>
|
||
<p class="warning-item">郑重警告四:以上警告说三遍,你还要明知故犯吗?</p>
|
||
</div>
|
||
<!-- 底部操作 -->
|
||
<div class="dialog-actions">
|
||
<el-button class="disclaimer-btn" @click="openDisclaimer">📜 免责声明</el-button>
|
||
<el-button @click="resultDialogVisible = false">关闭</el-button>
|
||
<el-button type="primary" @click="copyShareLink">一键复制链接</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
</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>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { Search, Loading, CircleCheckFilled } from '@element-plus/icons-vue'
|
||
import QRCode from 'qrcode'
|
||
import ResultCard from '../components/ResultCard.vue'
|
||
import VideoResultCard from '../components/VideoResultCard.vue'
|
||
import { query as searchQuery, saveToCloud, saveVideoToCloud, getSystemConfigs, getCloudTypes, streamSearch, getSiteConfig } from '../api'
|
||
import { ElMessage } from 'element-plus'
|
||
import type { FormInstance } from 'element-plus'
|
||
import type { SearchResult, VideoParseResult, SaveResult, IntentType, CloudType, ChannelGroup } from '../types'
|
||
import { CLOUD_LABELS, CLOUD_ICONS, CLOUD_COLORS } from '../types'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
|
||
const query = ref('')
|
||
const loading = ref(false)
|
||
const loadingMore = ref(false)
|
||
const intent = ref<IntentType | null>(null)
|
||
const searchResults = ref<SearchResult[]>([])
|
||
const videoResults = ref<VideoParseResult[]>([])
|
||
const results = ref<any[]>([])
|
||
const channels = ref<ChannelGroup[]>([])
|
||
const filteredCount = ref(0)
|
||
const skippedCount = ref(0)
|
||
const hasMore = ref(false)
|
||
const currentPage = ref(1)
|
||
const totalCount = ref(0)
|
||
const searchTime = ref(0)
|
||
const activeCloudTab = ref('')
|
||
const loadingProgress = ref(0)
|
||
const loadingPhase = ref<'search' | 'validate' | 'done'>('search')
|
||
const validationTotal = ref(0)
|
||
const validationDone = ref(0)
|
||
const contentInfo = ref<any>(null)
|
||
const contentTags = ref<string[]>([])
|
||
const fallbackImage = computed(() => {
|
||
// 兜底图优先用 search_fallback_image,没有则用 site_logo
|
||
return _fallbackImage.value || siteLogo.value || ''
|
||
})
|
||
const _fallbackImage = ref('')
|
||
const siteLogo = ref('')
|
||
const siteName = ref('')
|
||
const siteDisclaimer = ref('')
|
||
const siteMarquee = ref('')
|
||
const coverError = ref(false)
|
||
|
||
|
||
const allResultsMap = ref(new Map<string, any>())
|
||
const validResults = ref<any[]>([])
|
||
|
||
// 页面内分批加载
|
||
const FLAT_PAGE_SIZE = 30
|
||
const CHANNEL_PAGE_SIZE = 20
|
||
const visibleCount = ref(FLAT_PAGE_SIZE)
|
||
// 每个频道展示的条目数上限
|
||
const channelLimits = ref<Record<string, number>>({})
|
||
|
||
// 网盘图标映射 — 从后端 API 加载
|
||
const cloudTypeMap = ref<Record<string, { label: string; icon: string }>>({})
|
||
function cloudIconByType(t: string): string {
|
||
return cloudTypeMap.value[t]?.icon || CLOUD_ICONS[t] || '📁'
|
||
}
|
||
async function loadCloudTypes() {
|
||
try {
|
||
const res = await getCloudTypes()
|
||
const map: Record<string, { label: string; icon: string }> = {}
|
||
for (const ct of res.types) {
|
||
map[ct.type] = { label: ct.label, icon: ct.icon }
|
||
}
|
||
cloudTypeMap.value = map
|
||
} catch { /* ignore */ }
|
||
}
|
||
|
||
onMounted(async () => {
|
||
// 预加载网站配置(logo/名称/公告等)
|
||
try {
|
||
const sc = await getSiteConfig()
|
||
if (sc.site_logo) siteLogo.value = sc.site_logo
|
||
if (sc.site_name) siteName.value = sc.site_name
|
||
if (sc.site_disclaimer) siteDisclaimer.value = sc.site_disclaimer
|
||
if (sc.site_marquee) siteMarquee.value = sc.site_marquee
|
||
} catch {}
|
||
const q = (route.query.q as string) || ''
|
||
if (q) {
|
||
query.value = q
|
||
doSearch(q)
|
||
}
|
||
loadCloudTypes()
|
||
})
|
||
|
||
|
||
// 网盘分类标签 — 始终展示所有已知云盘类型(0 也显示)
|
||
const cloudTabs = computed(() => {
|
||
const counts: Record<string, number> = {}
|
||
for (const r of searchResults.value) {
|
||
const ct = r.cloud_type || 'others'
|
||
counts[ct] = (counts[ct] || 0) + 1
|
||
}
|
||
const list: { type: string; label: string; count: number; icon: string }[] = []
|
||
list.push({ type: '', label: '全部', count: searchResults.value.length, icon: '📋' })
|
||
|
||
const cloudOrder: Record<string, number> = {
|
||
quark: 1, baidu: 2, aliyun: 3, '115': 4,
|
||
tianyi: 5, '123pan': 6, uc: 7, xunlei: 8,
|
||
pikpak: 9, magnet: 10, ed2k: 11, others: 12,
|
||
}
|
||
const allTypes = Object.keys(CLOUD_LABELS) as CloudType[]
|
||
const sorted = allTypes.sort((a, b) => (cloudOrder[a] ?? 99) - (cloudOrder[b] ?? 99))
|
||
for (const type of sorted) {
|
||
list.push({
|
||
type,
|
||
label: CLOUD_LABELS[type],
|
||
count: counts[type] || 0,
|
||
icon: cloudIconByType(type),
|
||
})
|
||
}
|
||
return list
|
||
})
|
||
|
||
// 只显示有结果的标签(隐藏 0 计数的标签)
|
||
const visibleTabs = computed(() => {
|
||
return cloudTabs.value.filter(t => t.count > 0)
|
||
})
|
||
|
||
function getActiveTabLabel(): string {
|
||
const tab = cloudTabs.value.find(t => t.type === activeCloudTab.value)
|
||
return tab?.label || activeCloudTab.value || ''
|
||
}
|
||
|
||
// 扁平全部结果(按时间排序 + 页面内分批)
|
||
const allResultsFlat = computed(() => {
|
||
const all: any[] = []
|
||
for (const ch of channels.value) {
|
||
all.push(...ch.items)
|
||
}
|
||
// 按 update_time 降序(最新优先)
|
||
return all.sort((a, b) => {
|
||
const ta = a.update_time || a.datetime || ''
|
||
const tb = b.update_time || b.datetime || ''
|
||
if (!ta && !tb) return 0
|
||
if (!ta) return 1
|
||
if (!tb) return -1
|
||
return tb.localeCompare(ta)
|
||
})
|
||
})
|
||
|
||
// 分页显示的切片
|
||
const displayedResults = computed(() => {
|
||
return allResultsFlat.value.slice(0, visibleCount.value)
|
||
})
|
||
|
||
// 是否还有更多本地数据可加载
|
||
const hasMoreLocal = computed(() => {
|
||
return visibleCount.value < allResultsFlat.value.length
|
||
})
|
||
|
||
// 未显示的数量
|
||
const remainingCount = computed(() => {
|
||
return allResultsFlat.value.length - visibleCount.value
|
||
})
|
||
|
||
// ===== 空结果智能提示分析 =====
|
||
const emptyHint = computed(() => {
|
||
const q = query.value.trim()
|
||
if (!q) return '请输入关键词进行搜索'
|
||
if (q.length < 2) return `「${q}」太短了,试试输入更完整的关键词`
|
||
if (q.length > 30) return '关键词太长啦,试试用几个核心词代替整句话'
|
||
const chineseChars = (q.match(/[\u4e00-\u9fff]/g) || []).length
|
||
if (chineseChars === 0) return '网盘资源通常以中文命名,试试用中文搜索'
|
||
return `「${q}」暂时没找到匹配的资源`
|
||
})
|
||
|
||
const emptyTips = computed(() => {
|
||
const q = query.value.trim()
|
||
if (!q) return ['输入电视剧/电影/文件名称试试']
|
||
const tips: string[] = []
|
||
|
||
// 1. 判断关键词长度
|
||
if (q.length < 2) {
|
||
tips.push('输入至少 2 个字符,试试完整的资源名称')
|
||
}
|
||
|
||
// 2. 判断是否过长
|
||
if (q.length > 30) {
|
||
tips.push('缩短到 2-10 个字,用核心关键词搜索更精准')
|
||
}
|
||
|
||
// 3. 没有中文
|
||
const chineseChars = (q.match(/[\u4e00-\u9fff]/g) || []).length
|
||
if (chineseChars === 0) {
|
||
tips.push('国内网盘资源标题大多是中文,试试转换为中文名称')
|
||
}
|
||
|
||
// 4. 中文太少
|
||
if (chineseChars > 0 && chineseChars < q.length * 0.5 && q.length >= 2) {
|
||
tips.push('混合了太多非中文字符,提取核心中文关键词试试')
|
||
}
|
||
|
||
// 5. 包含空格
|
||
if (q.includes(' ') || q.includes(' ')) {
|
||
tips.push('搜索词中包含了空格,试试去掉空格连续输入')
|
||
}
|
||
|
||
// 6. 是否是完整句子
|
||
const stopWords = ['的', '了', '是', '在', '有', '我', '他', '她', '它', '这', '那', '和', '与', '及', '或', '但', '而', '且']
|
||
const stopCount = stopWords.filter(w => q.includes(w)).length
|
||
if (stopCount >= 2 && q.length > 6) {
|
||
tips.push('看起来像是一句话,试着只保留资源核心名称(去掉「的」「了」「我」等词)')
|
||
}
|
||
|
||
// 7. 太具体/特殊字符过多
|
||
const specialChars = (q.match(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~`《》【】!@#¥%……&*()——+|]/g) || []).length
|
||
if (specialChars > 2) {
|
||
tips.push('特殊符号过多,试试只用中英文和数字')
|
||
}
|
||
|
||
// 8. 如果是单字中文
|
||
if (q.length === 1 && chineseChars === 1) {
|
||
tips.push('单个汉字过于宽泛,试试完整的剧名或文件名')
|
||
}
|
||
|
||
// 9. 默认通用建议
|
||
if (tips.length === 0) {
|
||
tips.push('试试更换关键词或减短搜索词')
|
||
tips.push('检查一下是否输入了正确的资源名称')
|
||
}
|
||
|
||
return tips.slice(0, 4)
|
||
})
|
||
|
||
|
||
// 加载更多本地数据
|
||
function loadMoreLocal() {
|
||
visibleCount.value += FLAT_PAGE_SIZE
|
||
}
|
||
|
||
// 获取频道可见条目数
|
||
function channelVisibleItems(channel: any): any[] {
|
||
const max = channelLimits.value[channel.cloud_type] || CHANNEL_PAGE_SIZE
|
||
return (channel.items || []).slice(0, max)
|
||
}
|
||
|
||
// 频道是否还有更多
|
||
function channelHasMore(channel: any): boolean {
|
||
const max = channelLimits.value[channel.cloud_type] || CHANNEL_PAGE_SIZE
|
||
return (channel.items || []).length > max
|
||
}
|
||
|
||
// 频道展开加载更多
|
||
function channelLoadMore(cloudType: string) {
|
||
channelLimits.value = {
|
||
...channelLimits.value,
|
||
[cloudType]: (channelLimits.value[cloudType] || CHANNEL_PAGE_SIZE) + CHANNEL_PAGE_SIZE
|
||
}
|
||
}
|
||
|
||
// 重置频道限制
|
||
function resetChannelLimits() {
|
||
channelLimits.value = {}
|
||
}
|
||
|
||
// 根据当前激活的 cloud tab 过滤 channels
|
||
const filteredChannels = computed<ChannelGroup[]>(() => {
|
||
let list = channels.value
|
||
if (activeCloudTab.value) {
|
||
list = list.filter(ch => ch.cloud_type === activeCloudTab.value)
|
||
}
|
||
return list
|
||
})
|
||
|
||
// 总页数
|
||
const totalPages = computed(() => {
|
||
if (totalCount.value <= 0) return 1
|
||
return Math.ceil(totalCount.value / 20)
|
||
})
|
||
|
||
// 包含结果的网盘类型数
|
||
const cloudTypesCount = computed(() => {
|
||
return cloudTabs.value.filter(t => t.type !== '' && t.count > 0).length
|
||
})
|
||
|
||
// 强制刷新搜索
|
||
function handleRefresh() {
|
||
if (query.value.trim()) {
|
||
doSearch(query.value.trim())
|
||
}
|
||
}
|
||
|
||
// 模拟进度条(实时更新时间)
|
||
function startProgressSimulation() {
|
||
loadingProgress.value = 0
|
||
loadingPhase.value = 'search'
|
||
const startTime = Date.now()
|
||
searchTime.value = 0
|
||
|
||
const interval = setInterval(() => {
|
||
if (!loading.value) {
|
||
loadingProgress.value = 100
|
||
clearInterval(interval)
|
||
return
|
||
}
|
||
// 实时更新时间
|
||
searchTime.value = Date.now() - startTime
|
||
|
||
if (loadingProgress.value < 60) {
|
||
loadingProgress.value += 1 + Math.random() * 3
|
||
} else if (loadingProgress.value < 85) {
|
||
loadingPhase.value = 'validate'
|
||
loadingProgress.value += 0.5 + Math.random() * 1
|
||
} else if (loadingProgress.value < 98) {
|
||
loadingProgress.value += 0.2 + Math.random() * 0.5
|
||
}
|
||
}, 200)
|
||
return interval
|
||
}
|
||
|
||
// 计算频道最新时间
|
||
function getChannelNewestTime(items: any[]): string {
|
||
const times = items
|
||
.map(i => i.update_time || i.datetime || '')
|
||
.filter(Boolean)
|
||
.sort()
|
||
.reverse()
|
||
if (times.length === 0) return ''
|
||
return formatRelativeTime(times[0])
|
||
}
|
||
|
||
|
||
function formatRelativeTime(dateStr: string): string {
|
||
if (!dateStr) return ''
|
||
const now = Date.now()
|
||
const date = new Date(dateStr)
|
||
if (isNaN(date.getTime())) return dateStr.slice(0, 10)
|
||
const diff = now - date.getTime()
|
||
if (diff < 0) return dateStr.slice(0, 10)
|
||
const mins = Math.floor(diff / 60000)
|
||
if (mins < 60) return mins <= 1 ? '刚刚' : `${mins} 分钟前`
|
||
const hours = Math.floor(mins / 60)
|
||
if (hours < 24) return `${hours} 小时前`
|
||
const days = Math.floor(hours / 24)
|
||
if (days < 30) return `${days} 天前`
|
||
const months = Math.floor(days / 30)
|
||
return `${months} 个月前`
|
||
}
|
||
|
||
// 频道图标 — 使用 API 加载的网盘图标
|
||
function channelIcon(ct: string): string {
|
||
return cloudIconByType(ct)
|
||
}
|
||
|
||
watch(
|
||
() => route.query.q,
|
||
(newQ) => {
|
||
if (newQ && newQ !== query.value) {
|
||
query.value = newQ as string
|
||
doSearch(newQ as string)
|
||
}
|
||
}
|
||
)
|
||
|
||
const saving = ref(false)
|
||
const currentSaveItem = ref<any>(null)
|
||
const resultDialogVisible = ref(false)
|
||
const saveSuccess = ref(false)
|
||
const saveResult = ref<SaveResult | null>(null)
|
||
const shareLink = ref('')
|
||
const sharePwd = ref('')
|
||
const renamedFiles = ref<string[]>([])
|
||
const qrCanvasRef = ref<HTMLCanvasElement | null>(null)
|
||
const progressStep = ref(0)
|
||
|
||
// 当前保存的网盘名称(如"夸克网盘"、"百度网盘")
|
||
const diskLabel = computed(() => {
|
||
const ct = currentSaveItem.value?.cloud_type || 'quark'
|
||
return (CLOUD_LABELS as any)[ct] || '夸克网盘'
|
||
})
|
||
// 弹窗标题
|
||
const dialogTitle = computed(() => {
|
||
const title = currentSaveItem.value?.title || ''
|
||
// 取干净标题(去掉【】内容)
|
||
const clean = title.replace(/【[^】]+】/g, '').trim()
|
||
return clean || title || '资源'
|
||
})
|
||
|
||
async function doSearch(q: string) {
|
||
loading.value = true
|
||
const startTime = Date.now()
|
||
visibleCount.value = FLAT_PAGE_SIZE
|
||
currentPage.value = 1
|
||
searchResults.value = []
|
||
videoResults.value = []
|
||
results.value = []
|
||
channels.value = []
|
||
validResults.value = []
|
||
allResultsMap.value = new Map()
|
||
filteredCount.value = 0
|
||
skippedCount.value = 0
|
||
hasMore.value = false
|
||
activeCloudTab.value = ''
|
||
searchTime.value = 0
|
||
coverError.value = false
|
||
siteMarquee.value = ''
|
||
resetChannelLimits()
|
||
|
||
const progressTimer = startProgressSimulation()
|
||
|
||
try {
|
||
intent.value = 'SEARCH'
|
||
let totalValidated = 0
|
||
let totalFiltered = 0
|
||
const validatedMap = new Map<string, boolean>()
|
||
let validationComplete = false
|
||
|
||
await streamSearch(q, {
|
||
onStats: (stats) => {
|
||
searchTime.value = Date.now() - startTime
|
||
totalCount.value = stats.total
|
||
contentInfo.value = stats.content_info || null
|
||
contentTags.value = stats.content_tags || []
|
||
if (stats.fallback_image) {
|
||
_fallbackImage.value = stats.fallback_image
|
||
// 预加载兜底图:成功了缓存,失败了清空引用,避免每个卡片单独试
|
||
const preload = new Image()
|
||
preload.onload = () => {} // 浏览器自动缓存
|
||
preload.onerror = () => { _fallbackImage.value = '' }
|
||
preload.src = stats.fallback_image
|
||
}
|
||
if (stats.site_logo) {
|
||
siteLogo.value = stats.site_logo
|
||
}
|
||
if (stats.site_name) {
|
||
siteName.value = stats.site_name
|
||
}
|
||
if (stats.site_disclaimer) {
|
||
siteDisclaimer.value = stats.site_disclaimer
|
||
}
|
||
if (stats.site_marquee) {
|
||
siteMarquee.value = stats.site_marquee
|
||
}
|
||
loadingPhase.value = 'validate'
|
||
|
||
// 存储所有结果,后续按验证结果逐步展示
|
||
if (stats.channels) {
|
||
const map = new Map<string, any>()
|
||
const all: any[] = []
|
||
for (const ch of stats.channels) {
|
||
for (const item of (ch.items || [])) {
|
||
map.set(item.id, item)
|
||
all.push(item)
|
||
}
|
||
}
|
||
allResultsMap.value = map
|
||
results.value = all
|
||
}
|
||
|
||
if (stats.link_validation) {
|
||
// Now we validate ALL items, so validation total = total count
|
||
validationTotal.value = stats.total
|
||
}
|
||
},
|
||
|
||
onResult: (id, valid) => {
|
||
totalValidated++
|
||
validationDone.value = totalValidated
|
||
searchTime.value = Date.now() - startTime
|
||
|
||
// 验证通过就加入结果列表
|
||
if (valid) {
|
||
const item = allResultsMap.value.get(id)
|
||
if (item) {
|
||
validResults.value.push(item)
|
||
// Incrementally update channels for display
|
||
searchResults.value = [...validResults.value]
|
||
channels.value = buildChannelsFromResults(searchResults.value).map((ch: any) => ({
|
||
...ch,
|
||
newestTime: getChannelNewestTime(ch.items),
|
||
}))
|
||
}
|
||
}
|
||
},
|
||
|
||
onComplete: (data) => {
|
||
searchTime.value = Date.now() - startTime
|
||
// 后端已过滤失效链接,直接使用
|
||
const backendResults = (data.results as SearchResult[]) || []
|
||
|
||
totalCount.value = backendResults.length
|
||
filteredCount.value = (data.filtered || 0)
|
||
skippedCount.value = (data.skipped || 0)
|
||
hasMore.value = false
|
||
validationDone.value = validationTotal.value
|
||
|
||
// Use the complete data's final results
|
||
searchResults.value = backendResults
|
||
channels.value = (data.channels || []).map((ch: any) => ({
|
||
...ch,
|
||
newestTime: getChannelNewestTime(ch.items),
|
||
}))
|
||
// Recalculate channel item lists
|
||
const itemsByType: Record<string, any[]> = {}
|
||
for (const r of backendResults) {
|
||
const ct = r.cloud_type || 'others'
|
||
if (!itemsByType[ct]) itemsByType[ct] = []
|
||
itemsByType[ct].push(r)
|
||
}
|
||
channels.value = channels.value.map(ch => ({
|
||
...ch,
|
||
count: (itemsByType[ch.cloud_type] || []).length,
|
||
items: itemsByType[ch.cloud_type] || [],
|
||
})).filter(ch => ch.count > 0)
|
||
results.value = backendResults
|
||
|
||
loading.value = false
|
||
loadingPhase.value = 'done'
|
||
loadingProgress.value = 100
|
||
clearInterval(progressTimer)
|
||
},
|
||
|
||
onError: (err) => {
|
||
console.error('搜索失败', err)
|
||
loading.value = false
|
||
loadingPhase.value = 'done'
|
||
loadingProgress.value = 100
|
||
clearInterval(progressTimer)
|
||
},
|
||
})
|
||
} catch (e) {
|
||
console.error('搜索异常', e)
|
||
loading.value = false
|
||
loadingPhase.value = 'done'
|
||
loadingProgress.value = 100
|
||
clearInterval(progressTimer)
|
||
}
|
||
}
|
||
|
||
function buildChannelsFromResults(results: any[]): ChannelGroup[] {
|
||
const groups: Record<string, any[]> = {}
|
||
const order: Record<string, number> = {
|
||
quark: 1, baidu: 2, aliyun: 3, '115': 4,
|
||
tianyi: 5, '123pan': 6, uc: 7, xunlei: 8,
|
||
pikpak: 9, magnet: 10, ed2k: 11, others: 12,
|
||
}
|
||
for (const r of results) {
|
||
const ct = r.cloud_type || 'others'
|
||
if (!groups[ct]) groups[ct] = []
|
||
groups[ct].push(r)
|
||
}
|
||
return Object.entries(groups)
|
||
.sort((a, b) => (order[a[0]] ?? 99) - (order[b[0]] ?? 99))
|
||
.map(([cloud_type, items]) => ({
|
||
cloud_type,
|
||
label: CLOUD_LABELS[cloud_type as CloudType] || cloud_type,
|
||
color: CLOUD_COLORS[cloud_type as CloudType] || '#95a5a6',
|
||
count: items.length,
|
||
items,
|
||
newestTime: getChannelNewestTime(items),
|
||
}))
|
||
}
|
||
|
||
async function loadMore() {
|
||
loadingMore.value = true
|
||
currentPage.value++
|
||
try {
|
||
const res = await searchQuery(query.value, currentPage.value)
|
||
const newResults = res.results as SearchResult[]
|
||
searchResults.value.push(...newResults)
|
||
totalCount.value = res.total // 更新总数
|
||
hasMore.value = res.total > searchResults.value.length
|
||
filteredCount.value += res.filtered || 0
|
||
channels.value = buildChannelsFromResults(searchResults.value)
|
||
} catch (e) {
|
||
console.error('加载更多失败', e)
|
||
} finally {
|
||
loadingMore.value = false
|
||
}
|
||
}
|
||
|
||
function handleSearch() {
|
||
const q = query.value.trim()
|
||
if (q) {
|
||
router.replace('/search?q=' + encodeURIComponent(q))
|
||
doSearch(q)
|
||
}
|
||
}
|
||
|
||
async function handleSave(data: SearchResult) {
|
||
currentSaveItem.value = data
|
||
// 清除上一次保存的结果状态,避免新旧结果同时显示
|
||
saveSuccess.value = false
|
||
saveResult.value = null
|
||
shareLink.value = ''
|
||
sharePwd.value = ''
|
||
renamedFiles.value = []
|
||
saving.value = true
|
||
resultDialogVisible.value = true
|
||
progressStep.value = 1
|
||
const cloudType = data.cloud_type || 'quark'
|
||
try {
|
||
const result: SaveResult = await saveToCloud({
|
||
type: 'search',
|
||
source: data,
|
||
target_cloud: cloudType,
|
||
})
|
||
saveResult.value = result
|
||
saveSuccess.value = result.success
|
||
|
||
if (result.success) {
|
||
// Populate rename info from backend response
|
||
if ((result as any).renamed?.length > 0) {
|
||
renamedFiles.value = (result as any).renamed
|
||
}
|
||
|
||
// Step 2: 重命名文件(防和谐)
|
||
progressStep.value = 2
|
||
await new Promise(r => setTimeout(r, 600))
|
||
|
||
// Step 3: 生成分享链接
|
||
progressStep.value = 3
|
||
await new Promise(r => setTimeout(r, 400))
|
||
|
||
if (result.share_url) {
|
||
shareLink.value = result.share_url
|
||
sharePwd.value = (result as any).share_pwd || (result as any).sharePwd || ''
|
||
await new Promise(r => setTimeout(r, 300))
|
||
progressStep.value = 4
|
||
} else {
|
||
progressStep.value = 4
|
||
}
|
||
}
|
||
} catch (e: any) {
|
||
saveResult.value = {
|
||
success: false,
|
||
share_url: '',
|
||
file_name: '',
|
||
file_size: '',
|
||
message: e.message || '保存请求失败',
|
||
}
|
||
saveSuccess.value = false
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
async function handleVideoSave(data: VideoParseResult) {
|
||
currentSaveItem.value = data
|
||
// 清除上一次保存的结果状态
|
||
saveSuccess.value = false
|
||
saveResult.value = null
|
||
shareLink.value = ''
|
||
sharePwd.value = ''
|
||
renamedFiles.value = []
|
||
saving.value = true
|
||
resultDialogVisible.value = true
|
||
try {
|
||
const result = await saveVideoToCloud({
|
||
video_url: data.video_url,
|
||
title: data.title,
|
||
target_cloud: 'quark',
|
||
})
|
||
saveResult.value = result
|
||
saveSuccess.value = result.success
|
||
if (result.success && result.share_url) {
|
||
shareLink.value = result.share_url
|
||
}
|
||
} catch (e: any) {
|
||
saveResult.value = { success: false, share_url: '', file_name: '', file_size: '', message: e.message || '保存请求失败' }
|
||
saveSuccess.value = false
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
// 当保存完成且 shareLink 更新时,生成二维码
|
||
watch([shareLink, saving], async ([newLink, isSaving]) => {
|
||
if (newLink && !isSaving && resultDialogVisible.value) {
|
||
await nextTick()
|
||
if (qrCanvasRef.value) {
|
||
QRCode.toCanvas(qrCanvasRef.value, newLink, { width: 180, margin: 1 })
|
||
}
|
||
}
|
||
})
|
||
|
||
function copyShareLink() {
|
||
if (!shareLink.value) return
|
||
const text = shareLink.value
|
||
// Try modern clipboard API first
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
ElMessage.success('链接已复制')
|
||
}).catch(() => {
|
||
fallbackCopy(text)
|
||
})
|
||
} else {
|
||
fallbackCopy(text)
|
||
}
|
||
}
|
||
|
||
function openDisclaimer() {
|
||
window.open('/disclaimer/', '_blank')
|
||
}
|
||
|
||
function fallbackCopy(text: string) {
|
||
const textarea = document.createElement('textarea')
|
||
textarea.value = text
|
||
textarea.style.position = 'fixed'
|
||
textarea.style.left = '-9999px'
|
||
textarea.style.top = '-9999px'
|
||
textarea.style.opacity = '0'
|
||
document.body.appendChild(textarea)
|
||
textarea.select()
|
||
try {
|
||
document.execCommand('copy')
|
||
ElMessage.success('链接已复制')
|
||
} catch {
|
||
ElMessage.warning('复制失败,请手动复制链接')
|
||
}
|
||
document.body.removeChild(textarea)
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.search-result-page {
|
||
min-height: 100vh;
|
||
background: var(--bg-color, #f5f5f5);
|
||
}
|
||
|
||
.top-search-bar {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 50;
|
||
background: #fff;
|
||
border-bottom: 1px solid #e8e8e8;
|
||
padding: 12px 24px;
|
||
}
|
||
|
||
.search-bar-inner {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.search-box-inner {
|
||
display: flex;
|
||
align-items: center;
|
||
flex: 1;
|
||
border: 1px solid #dfe1e5;
|
||
border-radius: 24px;
|
||
background: #fff;
|
||
box-shadow: none;
|
||
transition: box-shadow .2s, border-color .2s;
|
||
overflow: hidden;
|
||
}
|
||
.search-box-inner:focus-within {
|
||
box-shadow: 0 1px 6px rgba(32,33,36,.28);
|
||
border-color: rgba(223,225,229,0);
|
||
}
|
||
|
||
/* ---- 跑马灯通知条 ---- */
|
||
.marquee-bar {
|
||
max-width: 680px;
|
||
margin: 0 auto;
|
||
padding: 6px 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
overflow: hidden;
|
||
}
|
||
.marquee-icon {
|
||
flex-shrink: 0;
|
||
font-size: 15px;
|
||
line-height: 1;
|
||
}
|
||
.marquee-track {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
}
|
||
.marquee-text {
|
||
display: inline-block;
|
||
font-size: 13px;
|
||
color: #e6a23c;
|
||
animation: marquee-scroll 20s linear infinite;
|
||
padding-left: 100%;
|
||
}
|
||
@keyframes marquee-scroll {
|
||
0% {
|
||
transform: translateX(0);
|
||
}
|
||
100% {
|
||
transform: translateX(-100%);
|
||
}
|
||
}
|
||
.search-box-inner :deep(.el-input__wrapper) {
|
||
border: none; box-shadow: none; background: transparent;
|
||
padding: 4px 20px; border-radius: 0;
|
||
}
|
||
.search-box-inner :deep(.el-input__inner) {
|
||
font-size: 15px;
|
||
}
|
||
|
||
/* 右上角用户信息 - 独立于搜索栏 */
|
||
.top-right-user {
|
||
position: absolute;
|
||
top: 12px;
|
||
right: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
z-index: 100;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.user-badge {
|
||
font-size: 13px;
|
||
color: var(--primary-color, #409eff);
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.login-error {
|
||
color: #f56c6c;
|
||
font-size: 13px;
|
||
text-align: center;
|
||
margin: 0;
|
||
}
|
||
|
||
.logo-link {
|
||
text-decoration: none;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.logo-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 8px;
|
||
background: rgba(64, 158, 255, 0.08);
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.logo-icon:hover {
|
||
background: rgba(64, 158, 255, 0.15);
|
||
}
|
||
|
||
.logo-text-only {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: var(--primary-color);
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.logo-img {
|
||
width: auto;
|
||
height: 40px;
|
||
max-width: 160px;
|
||
object-fit: contain;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.result-search-btn {
|
||
height: 38px !important;
|
||
padding: 0 22px !important;
|
||
border: none !important;
|
||
border-radius: 999px !important;
|
||
margin: 4px;
|
||
font-size: 14px !important;
|
||
font-weight: 600 !important;
|
||
letter-spacing: 0.5px;
|
||
flex-shrink: 0;
|
||
background: var(--primary-color);
|
||
color: #fff;
|
||
transition: all .2s;
|
||
}
|
||
.result-search-btn:hover {
|
||
background: #3a7be0;
|
||
}
|
||
.result-search-btn:active {
|
||
background: #2d6ccf;
|
||
}
|
||
|
||
.share-input {
|
||
flex: 1;
|
||
}
|
||
.share-pwd-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
}
|
||
.pwd-label {
|
||
font-size: 13px;
|
||
color: var(--text-secondary, #909399);
|
||
}
|
||
.pwd-hint {
|
||
font-size: 12px;
|
||
color: var(--text-secondary, #909399);
|
||
}
|
||
/* 结果信息栏 */
|
||
.result-info-bar {
|
||
max-width: 800px;
|
||
margin: 16px auto 0;
|
||
padding: 0 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
font-size: 13px;
|
||
color: #909399;
|
||
}
|
||
|
||
.info-left, .info-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.info-item {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.info-count {
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
.info-type {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
background: #f4f4f5;
|
||
padding: 1px 7px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.info-time {
|
||
font-family: monospace;
|
||
background: #f4f4f5;
|
||
padding: 1px 7px;
|
||
border-radius: 4px;
|
||
color: #909399;
|
||
}
|
||
|
||
.info-hasmore {
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.filter-badge {
|
||
background: #fef0f0;
|
||
color: #f56c6c;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.skip-badge {
|
||
background: #fdf6ec;
|
||
color: #e6a23c;
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.refresh-btn {
|
||
background: none;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 6px;
|
||
padding: 2px 10px;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.refresh-btn:hover {
|
||
color: #409eff;
|
||
border-color: #409eff;
|
||
background: rgba(64,158,255,0.05);
|
||
}
|
||
|
||
/* 意图标签 */
|
||
.intent-badge {
|
||
max-width: 800px;
|
||
margin: 12px auto 0;
|
||
padding: 0 24px;
|
||
}
|
||
|
||
.intent-tag {
|
||
font-size: 13px;
|
||
color: #909399;
|
||
background: #f0f2f5;
|
||
padding: 2px 10px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
/* 加载进度 */
|
||
.loading-section {
|
||
max-width: 800px;
|
||
margin: 24px auto;
|
||
padding: 0 24px;
|
||
}
|
||
|
||
.progress-track {
|
||
width: 100%;
|
||
height: 4px;
|
||
background: #e8e8e8;
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #409eff, #67c23a);
|
||
border-radius: 2px;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.progress-label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
font-size: 13px;
|
||
color: #909399;
|
||
}
|
||
|
||
.progress-time {
|
||
margin-left: auto;
|
||
font-family: monospace;
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.validate-count {
|
||
font-family: monospace;
|
||
color: #409eff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.loading-skeleton {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
/* 网盘分类标签栏 — 跟资源展示等宽(继承父容器) */
|
||
.cloud-tabs {
|
||
margin: 16px auto 0;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
|
||
.cloud-tab {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 6px 14px;
|
||
border-radius: 20px;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.5px;
|
||
color: #606266;
|
||
background: #f0f2f5;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
user-select: none;
|
||
}
|
||
|
||
.cloud-tab:hover {
|
||
background: #e4e7ed;
|
||
color: #303133;
|
||
}
|
||
|
||
.cloud-tab.active {
|
||
background: rgba(64, 158, 255, 0.1);
|
||
color: #409eff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.tab-icon {
|
||
font-size: 15px;
|
||
line-height: 1;
|
||
}
|
||
.tab-icon-img {
|
||
width: 16px;
|
||
height: 16px;
|
||
vertical-align: middle;
|
||
margin-right: 2px;
|
||
}
|
||
|
||
.tab-count {
|
||
font-size: 11px;
|
||
color: #c0c4cc;
|
||
margin-left: 2px;
|
||
}
|
||
|
||
/* 结果列表 */
|
||
.result-content {
|
||
max-width: 1080px;
|
||
margin: 0 auto;
|
||
padding: 0 24px 48px;
|
||
}
|
||
|
||
/* 宽屏下搜索栏保持800px,结果容器保持1080px,网格保持3列 */
|
||
@media (min-width: 1280px) {
|
||
.result-content {
|
||
max-width: 1080px;
|
||
}
|
||
.search-bar-inner {
|
||
max-width: 800px;
|
||
}
|
||
}
|
||
|
||
.result-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
/* 无匹配提示 */
|
||
.no-match-tip {
|
||
margin-top: 32px;
|
||
text-align: center;
|
||
color: #909399;
|
||
font-size: 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* 频道分组 */
|
||
.channel-section {
|
||
background: #fff;
|
||
border-radius: 12px;
|
||
padding: 14px 16px;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||
border: 1px solid #ebeef5;
|
||
}
|
||
|
||
.channel-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 10px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.channel-icon {
|
||
font-size: 16px;
|
||
line-height: 1;
|
||
}
|
||
.channel-icon-img {
|
||
width: 18px;
|
||
height: 18px;
|
||
vertical-align: middle;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.channel-label {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
.channel-total-badge {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
background: #f4f4f5;
|
||
padding: 1px 8px;
|
||
border-radius: 10px;
|
||
margin-left: 2px;
|
||
}
|
||
|
||
.channel-time {
|
||
margin-left: auto;
|
||
font-size: 12px;
|
||
color: #b8860b;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 全部标签 — 卡片网格,3列 */
|
||
.flat-list {
|
||
display: grid !important;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 14px;
|
||
}
|
||
|
||
/* 窄屏降为1列,平板降为2列 */
|
||
@media (max-width: 720px) {
|
||
.flat-list {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.channel-section {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
@media (min-width: 721px) and (max-width: 1079px) {
|
||
.flat-list {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
.channel-section {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
}
|
||
|
||
/* 频道列表 — 每个频道区块也3列 */
|
||
.channel-list {
|
||
gap: 14px;
|
||
}
|
||
|
||
.channel-section {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 14px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.channel-section .channel-header {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.channel-section .channel-load-more {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.channel-load-more {
|
||
margin-top: 8px;
|
||
text-align: center;
|
||
padding: 8px;
|
||
border: 1px dashed #dcdfe6;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
background: #fafafa;
|
||
}
|
||
.channel-load-more:hover {
|
||
background: #f0f2f5;
|
||
border-color: #c0c4cc;
|
||
}
|
||
.channel-load-more-text {
|
||
font-size: 13px;
|
||
color: #909399;
|
||
}
|
||
|
||
/* 加载更多 */
|
||
.load-more {
|
||
text-align: center;
|
||
margin-top: 24px;
|
||
}
|
||
|
||
/* 内联加载更多("全部"标签的分批加载) */
|
||
.load-more-inline {
|
||
text-align: center;
|
||
margin-top: 8px;
|
||
}
|
||
.load-more-btn {
|
||
width: 100%;
|
||
border-radius: 8px;
|
||
padding: 10px;
|
||
border: 1px dashed #dcdfe6;
|
||
background: #fafafa;
|
||
color: #909399;
|
||
font-size: 13px;
|
||
transition: all 0.2s;
|
||
}
|
||
.load-more-btn:hover {
|
||
background: #f0f2f5;
|
||
border-color: #c0c4cc;
|
||
color: #606266;
|
||
}
|
||
|
||
/* 保存弹窗 */
|
||
.save-dialog :deep(.el-dialog__title) {
|
||
font-weight: 700;
|
||
font-size: 16px;
|
||
}
|
||
.save-dialog {
|
||
width: 650px !important;
|
||
}
|
||
/* 修复dialog被el-dialog默认样式覆盖 */
|
||
.save-dialog :deep(.el-dialog) {
|
||
--el-dialog-width: 650px !important;
|
||
}
|
||
|
||
.dialog-title-bold {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: #303133;
|
||
line-height: 1.4;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
display: block;
|
||
}
|
||
|
||
/* ---- 保存弹窗 — 进度流程 ---- */
|
||
.result-dialog-content {
|
||
min-height: 80px;
|
||
}
|
||
|
||
.progress-flow {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.progress-step {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
opacity: 0.4;
|
||
transition: all 0.3s ease;
|
||
}
|
||
.progress-step.active {
|
||
opacity: 1;
|
||
}
|
||
.progress-step.done {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.step-dot {
|
||
flex-shrink: 0;
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
background: #e4e7ed;
|
||
color: #909399;
|
||
transition: all 0.3s;
|
||
}
|
||
.progress-step.active .step-dot {
|
||
background: #409eff;
|
||
color: #fff;
|
||
box-shadow: 0 0 0 4px rgba(64,158,255,0.2);
|
||
}
|
||
.progress-step.done .step-dot {
|
||
background: #67c23a;
|
||
color: #fff;
|
||
}
|
||
|
||
.step-check {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.step-body {
|
||
flex: 1;
|
||
padding-top: 3px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.step-title {
|
||
font-size: 14px;
|
||
color: #303133;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.step-status {
|
||
font-size: 12px;
|
||
padding: 1px 8px;
|
||
border-radius: 10px;
|
||
white-space: nowrap;
|
||
}
|
||
.step-status.loading {
|
||
background: #ecf5ff;
|
||
color: #409eff;
|
||
}
|
||
.step-status.done {
|
||
background: #f0f9eb;
|
||
color: #67c23a;
|
||
}
|
||
.step-status.pending {
|
||
background: #f4f4f5;
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
/* 保存弹窗 — 分享结果展示(左二维码 + 右链接) */
|
||
.share-result {
|
||
padding: 8px 0;
|
||
}
|
||
|
||
.share-layout {
|
||
display: flex;
|
||
gap: 24px;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.qr-left {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 8px;
|
||
background: #fafafa;
|
||
border-radius: 12px;
|
||
border: 1px solid #ebeef5;
|
||
align-self: stretch;
|
||
}
|
||
|
||
.qr-canvas {
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.qr-hint {
|
||
margin: 4px 0 0;
|
||
font-size: 12px;
|
||
color: #409eff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.qr-subhint {
|
||
margin: 0;
|
||
font-size: 11px;
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.qr-disclaimer-short {
|
||
margin-top: 8px;
|
||
padding: 4px 8px;
|
||
background: #fff7e6;
|
||
border: 1px solid #ffe7ba;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
color: #d46b08;
|
||
text-align: center;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.link-right {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.success-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.success-text {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: #303133;
|
||
}
|
||
|
||
.link-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.share-input :deep(.el-input__wrapper) {
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
.share-tip {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
background: #fdf6ec;
|
||
padding: 8px 10px;
|
||
border-radius: 6px;
|
||
text-align: left;
|
||
display: flex;
|
||
gap: 6px;
|
||
align-items: flex-start;
|
||
}
|
||
.share-tip strong {
|
||
color: #d46b08;
|
||
font-weight: 700;
|
||
}
|
||
.share-tip-warn {
|
||
font-size: 18px;
|
||
line-height: 1.5;
|
||
flex-shrink: 0;
|
||
display: inline-flex;
|
||
align-items: flex-start;
|
||
padding-top: 1px;
|
||
}
|
||
.share-tip-text {
|
||
flex: 1;
|
||
min-width: 0;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* 底部操作按钮 */
|
||
.dialog-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
margin-top: auto;
|
||
padding-top: 8px;
|
||
}
|
||
|
||
.disclaimer-btn {
|
||
margin-right: auto !important;
|
||
font-size: 12px !important;
|
||
color: #909399 !important;
|
||
}
|
||
.disclaimer-btn:hover {
|
||
color: #409eff !important;
|
||
}
|
||
|
||
/* 郑重警告 */
|
||
.warnings-box {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
background: #fff2f0;
|
||
border: 1px solid #ffccc7;
|
||
border-radius: 8px;
|
||
padding: 8px 10px;
|
||
}
|
||
.warning-item {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
line-height: 1.8;
|
||
font-weight: 700;
|
||
white-space: nowrap;
|
||
}
|
||
.warning-item:nth-child(odd) {
|
||
color: #cf1322;
|
||
}
|
||
.warning-item:nth-child(even) {
|
||
color: #d46b08;
|
||
}
|
||
.warning-item:last-child {
|
||
color: #b71c1c;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.save-error {
|
||
padding: 8px 0;
|
||
}
|
||
|
||
/* ===== 内容信息(紧凑信息条) ===== */
|
||
.media-strip {
|
||
margin: 10px auto 0;
|
||
width: 100%;
|
||
}
|
||
.media-strip-inner {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px 14px;
|
||
background: linear-gradient(135deg, #f8faff, #f0f5ff);
|
||
border: 1px solid #e8f0fe;
|
||
border-radius: 10px;
|
||
text-decoration: none;
|
||
color: inherit;
|
||
font-size: 13px;
|
||
line-height: 1.4;
|
||
transition: all 0.2s;
|
||
}
|
||
.media-strip-inner:hover {
|
||
border-color: #c0d9ff;
|
||
background: linear-gradient(135deg, #f0f7ff, #e6f0ff);
|
||
}
|
||
.strip-thumb {
|
||
flex-shrink: 0;
|
||
width: 32px;
|
||
height: 44px;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
background: #e8ecf1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.strip-thumb img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
.strip-thumb-fallback {
|
||
font-size: 18px;
|
||
opacity: 0.5;
|
||
}
|
||
.strip-title {
|
||
font-weight: 700;
|
||
color: #1a1a2e;
|
||
font-size: 14px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
max-width: 200px;
|
||
}
|
||
.strip-year {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
.strip-rating {
|
||
color: #e6a23c;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
flex-shrink: 0;
|
||
}
|
||
.strip-genres {
|
||
display: flex;
|
||
gap: 2px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.strip-genre {
|
||
font-size: 11px;
|
||
color: #67c23a;
|
||
background: #f0f9eb;
|
||
padding: 1px 7px;
|
||
border-radius: 4px;
|
||
}
|
||
.strip-tags {
|
||
display: flex;
|
||
gap: 3px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.strip-tag {
|
||
font-size: 11px;
|
||
color: #409eff;
|
||
background: #ecf5ff;
|
||
padding: 1px 7px;
|
||
border-radius: 4px;
|
||
}
|
||
.strip-right {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-left: auto;
|
||
flex-shrink: 0;
|
||
white-space: nowrap;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
}
|
||
|
||
/* 重命名信息 */
|
||
.rename-info-bar {
|
||
margin-bottom: 12px;
|
||
}
|
||
.rename-item {
|
||
font-size: 11px;
|
||
color: #909399;
|
||
margin-top: 4px;
|
||
word-break: break-all;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
/* ===== 响应式 ===== */
|
||
@media (max-width: 768px) {
|
||
.save-dialog {
|
||
width: 96vw !important;
|
||
}
|
||
.save-dialog :deep(.el-dialog) {
|
||
--el-dialog-width: 96vw !important;
|
||
margin: 5vh auto !important;
|
||
}
|
||
.save-dialog :deep(.el-dialog__body) {
|
||
padding: 12px;
|
||
}
|
||
.share-layout {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
.qr-left {
|
||
width: 100%;
|
||
align-self: auto;
|
||
}
|
||
.link-right {
|
||
width: 100%;
|
||
}
|
||
/* 手机版警告区域水平滚动 */
|
||
.warnings-box {
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.search-result-page {
|
||
}
|
||
.top-search-bar {
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.top-search-bar .search-bar-inner {
|
||
width: 100%;
|
||
}
|
||
.top-search-bar .search-bar-inner .el-input {
|
||
min-width: 0;
|
||
}
|
||
.top-right-user {
|
||
position: static;
|
||
align-self: flex-end;
|
||
}
|
||
.result-info-bar {
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.info-left,
|
||
.info-right {
|
||
flex-wrap: wrap;
|
||
}
|
||
.cloud-tabs {
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
gap: 4px;
|
||
}
|
||
.cloud-tab {
|
||
flex-shrink: 0;
|
||
font-size: 12px;
|
||
padding: 4px 10px;
|
||
}
|
||
}
|
||
|
||
/* ===== 空结果智能提示 ===== */
|
||
.empty-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 60px 20px 40px;
|
||
text-align: center;
|
||
}
|
||
.empty-icon {
|
||
font-size: 48px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.empty-title {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: #1a1a2e;
|
||
margin-bottom: 8px;
|
||
}
|
||
.empty-hint {
|
||
font-size: 14px;
|
||
color: #909399;
|
||
margin-bottom: 20px;
|
||
max-width: 360px;
|
||
line-height: 1.5;
|
||
}
|
||
.empty-tips {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.empty-tip-item {
|
||
font-size: 13px;
|
||
color: #606266;
|
||
background: #f4f8ff;
|
||
padding: 8px 16px;
|
||
border-radius: 8px;
|
||
border: 1px solid #e8f0fe;
|
||
line-height: 1.4;
|
||
max-width: 400px;
|
||
}
|
||
|
||
.site-footer {
|
||
margin-top: 40px;
|
||
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>
|