2210 lines
58 KiB
Vue
Executable File
2210 lines
58 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 class="top-right-user">
|
||
<template v-if="userInfo">
|
||
<span class="user-badge">{{ userInfo.username }}</span>
|
||
<el-button size="small" text @click="handleLogout">退出</el-button>
|
||
</template>
|
||
<el-button v-else size="small" @click="showLogin = true">登录</el-button>
|
||
</div>
|
||
</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 || ''"
|
||
>
|
||
<span v-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>
|
||
|
||
<!-- 内容信息(来自TMDB,全面展示) -->
|
||
<div v-if="!loading && (contentInfo || contentTags.length > 0) && intent === 'SEARCH'" class="content-info-section">
|
||
<a
|
||
v-if="contentInfo?.tmdb_url"
|
||
:href="contentInfo.tmdb_url"
|
||
target="_blank"
|
||
class="media-card media-card-link"
|
||
rel="noopener"
|
||
>
|
||
<div class="media-poster-wrap">
|
||
<img v-if="contentInfo?.cover && !coverError" :src="contentInfo.cover" class="media-poster" @error="coverError = true" />
|
||
<div v-else class="media-poster-placeholder">🎬</div>
|
||
<span v-if="contentInfo?.rating" class="media-rating-badge">⭐ {{ contentInfo.rating }}</span>
|
||
</div>
|
||
<div class="media-body">
|
||
<div class="media-title">{{ contentInfo?.title || query }}<span v-if="contentInfo?.year" class="media-year"> ({{ contentInfo.year }})</span></div>
|
||
|
||
<!-- 评分和来源 -->
|
||
<div class="media-meta-row">
|
||
<span v-if="contentInfo?.rating" class="media-meta-rating">
|
||
⭐ <strong>{{ contentInfo.rating }}</strong>
|
||
<span v-if="contentInfo?.rating_count" class="rating-count">/10 · {{ contentInfo.rating_count }}人评价</span>
|
||
</span>
|
||
<span class="media-source-tag">{{ contentInfo?.source === 'tmdb' ? 'TMDB' : '缓存' }}</span>
|
||
</div>
|
||
|
||
<!-- 导演 -->
|
||
<div v-if="contentInfo?.directors" class="media-meta-line">
|
||
<span class="meta-label">导演</span>
|
||
<span class="meta-value">{{ contentInfo.directors }}</span>
|
||
</div>
|
||
|
||
<!-- 演员 -->
|
||
<div v-if="contentInfo?.actors" class="media-meta-line">
|
||
<span class="meta-label">演员</span>
|
||
<span class="meta-value meta-actors">{{ contentInfo.actors }}</span>
|
||
</div>
|
||
|
||
<!-- 类型/地区/片长 -->
|
||
<div v-if="contentInfo?.genres?.length || contentInfo?.region || contentInfo?.duration" class="media-meta-line">
|
||
<span class="meta-label">类型</span>
|
||
<span class="meta-value">
|
||
<span v-if="contentInfo?.genres?.length" class="meta-genres">
|
||
<span v-for="(g, gi) in contentInfo.genres" :key="gi" class="meta-genre-tag">{{ g }}</span>
|
||
</span>
|
||
<span v-if="contentInfo?.region" class="meta-region"> / {{ contentInfo.region }}</span>
|
||
<span v-if="contentInfo?.duration" class="meta-duration"> / {{ contentInfo.duration }}</span>
|
||
</span>
|
||
</div>
|
||
|
||
<!-- 描述 -->
|
||
<div v-if="contentInfo?.description" class="media-subtitle">{{ contentInfo.description }}</div>
|
||
|
||
<!-- 标签 -->
|
||
<div v-if="contentTags.length > 0" class="media-tags">
|
||
<span v-for="tag in contentTags" :key="tag" class="media-tag" :class="'media-tag-' + mediaTagColor(tag)">{{ tag }}</span>
|
||
</div>
|
||
<div class="media-hint">查看TMDB详情 →</div>
|
||
</div>
|
||
</a>
|
||
<div v-else class="media-card">
|
||
<div class="media-poster-wrap">
|
||
<img v-if="contentInfo?.cover && !coverError" :src="contentInfo.cover" class="media-poster" @error="coverError = true" />
|
||
<div v-else class="media-poster-placeholder">🎬</div>
|
||
<span v-if="contentInfo?.rating" class="media-rating-badge">⭐ {{ contentInfo.rating }}</span>
|
||
<span class="media-source-badge">{{ contentInfo?.source === 'tmdb' ? 'TMDB' : '缓存' }}</span>
|
||
</div>
|
||
<div class="media-body">
|
||
<div class="media-title">{{ contentInfo?.title || query }}<span v-if="contentInfo?.year"> ({{ contentInfo.year }})</span></div>
|
||
<div v-if="contentInfo?.rating" class="media-meta-row">
|
||
<span class="media-meta-rating">⭐ <strong>{{ contentInfo.rating }}</strong><span v-if="contentInfo?.rating_count" class="rating-count">/10 · {{ contentInfo.rating_count }}人评价</span></span>
|
||
</div>
|
||
<div v-if="contentInfo?.directors" class="media-meta-line">
|
||
<span class="meta-label">导演</span>
|
||
<span class="meta-value">{{ contentInfo.directors }}</span>
|
||
</div>
|
||
<div v-if="contentInfo?.actors" class="media-meta-line">
|
||
<span class="meta-label">演员</span>
|
||
<span class="meta-value meta-actors">{{ contentInfo.actors }}</span>
|
||
</div>
|
||
<div v-if="contentInfo?.genres?.length || contentInfo?.region || contentInfo?.duration" class="media-meta-line">
|
||
<span class="meta-label">类型</span>
|
||
<span class="meta-value">
|
||
<span v-if="contentInfo?.genres?.length" class="meta-genres">
|
||
<span v-for="(g, gi) in contentInfo.genres" :key="gi" class="meta-genre-tag">{{ g }}</span>
|
||
</span>
|
||
<span v-if="contentInfo?.region"> / {{ contentInfo.region }}</span>
|
||
<span v-if="contentInfo?.duration"> / {{ contentInfo.duration }}</span>
|
||
</span>
|
||
</div>
|
||
<div v-if="contentInfo?.description" class="media-subtitle">{{ contentInfo.description }}</div>
|
||
<div v-if="contentTags.length > 0" class="media-tags">
|
||
<span v-for="tag in contentTags" :key="tag" class="media-tag" :class="'media-tag-' + mediaTagColor(tag)">{{ tag }}</span>
|
||
</div>
|
||
</div>
|
||
</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"
|
||
@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"
|
||
>
|
||
<div class="channel-header">
|
||
<span 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>
|
||
</div>
|
||
<ResultCard
|
||
v-for="(item, ri) in channelVisibleItems(channel)"
|
||
:key="'ch-' + ci + '-' + ri"
|
||
:data="item"
|
||
:fallbackTags="contentTags"
|
||
:fallbackImage="fallbackImage"
|
||
@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>
|
||
|
||
<!-- 空状态 -->
|
||
<el-empty v-if="!loading && !loadingMore && totalCount === 0 && results.length === 0" description="未找到相关资源" />
|
||
|
||
<!-- 加载更多 -->
|
||
<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>
|
||
|
||
<!-- 登录弹窗 -->
|
||
<el-dialog v-model="showLogin" title="登录" width="380px" :close-on-click-modal="false" top="25vh">
|
||
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" label-width="0" @keyup.enter="handleLogin">
|
||
<el-form-item prop="username">
|
||
<el-input v-model="loginForm.username" placeholder="用户名" prefix-icon="User" />
|
||
</el-form-item>
|
||
<el-form-item prop="password">
|
||
<el-input v-model="loginForm.password" type="password" placeholder="密码" prefix-icon="Lock" show-password />
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" :loading="loginLoading" style="width: 100%" @click="handleLogin">登录</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<p v-if="loginError" class="login-error">{{ loginError }}</p>
|
||
</el-dialog>
|
||
<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, streamSearch, adminLogin, getMe } 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_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 = ref('')
|
||
const siteLogo = ref('')
|
||
const siteName = ref('')
|
||
const siteDisclaimer = ref('')
|
||
const coverError = ref(false)
|
||
|
||
// 登录状态
|
||
const userInfo = ref<{ username: string } | null>(null)
|
||
const showLogin = ref(false)
|
||
const loginFormRef = ref<FormInstance>()
|
||
const loginForm = reactive({ username: '', password: '' })
|
||
const loginLoading = ref(false)
|
||
const loginError = ref('')
|
||
const loginRules = {
|
||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||
}
|
||
|
||
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>>({})
|
||
|
||
// 网盘图标映射
|
||
const CLOUD_ICONS: Record<string, string> = {
|
||
quark: '☁️',
|
||
baidu: '🔵',
|
||
aliyun: '🟠',
|
||
'115': '🟣',
|
||
tianyi: '🔷',
|
||
'123pan': '🔴',
|
||
uc: '🟡',
|
||
xunlei: '🟢',
|
||
pikpak: '🟤',
|
||
magnet: '🧲',
|
||
ed2k: '🔗',
|
||
others: '📁',
|
||
}
|
||
|
||
onMounted(async () => {
|
||
const q = (route.query.q as string) || ''
|
||
if (q) {
|
||
query.value = q
|
||
doSearch(q)
|
||
}
|
||
// 检查登录状态
|
||
const me: any = await getMe().catch(() => ({ loggedIn: false }))
|
||
if (me.loggedIn && me.username) {
|
||
userInfo.value = { username: me.username }
|
||
}
|
||
})
|
||
|
||
async function handleLogin() {
|
||
const valid = await loginFormRef.value?.validate().catch(() => false)
|
||
if (!valid) return
|
||
loginLoading.value = true
|
||
loginError.value = ''
|
||
try {
|
||
const res = await adminLogin(loginForm.username, loginForm.password)
|
||
localStorage.setItem('admin_token', res.token)
|
||
userInfo.value = { username: loginForm.username }
|
||
showLogin.value = false
|
||
loginForm.password = ''
|
||
ElMessage.success('登录成功')
|
||
} catch (e: any) {
|
||
loginError.value = e?.response?.data?.error || e?.message || '登录失败'
|
||
} finally {
|
||
loginLoading.value = false
|
||
}
|
||
}
|
||
|
||
function handleLogout() {
|
||
localStorage.removeItem('admin_token')
|
||
userInfo.value = null
|
||
ElMessage.success('已退出')
|
||
}
|
||
|
||
// 网盘分类标签 — 始终展示所有已知云盘类型(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: CLOUD_ICONS[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
|
||
})
|
||
|
||
|
||
// 加载更多本地数据
|
||
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 mediaTagColor(tag: string): string {
|
||
const genre = ['动画', '动漫', '国漫', '奇幻', '玄幻', '仙侠', '动作', '剧情', '喜剧', '悬疑', '科幻', '冒险', '爱情', '古装']
|
||
if (genre.includes(tag)) return 'genre'
|
||
const type = ['电影', '剧集', '电视剧', '纪录片', '短片', '真人秀', '综艺']
|
||
if (type.includes(tag)) return 'type'
|
||
const quality = ['剧场版', '年番', '总集篇', 'OVA', 'SP']
|
||
if (quality.includes(tag)) return 'quality'
|
||
return 'default'
|
||
}
|
||
|
||
|
||
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} 个月前`
|
||
}
|
||
|
||
// 频道图标
|
||
function channelIcon(ct: string): string {
|
||
const icons: Record<string, string> = {
|
||
quark: '☁️', baidu: '🔵', aliyun: '🟠', '115': '🟣',
|
||
tianyi: '🔷', '123pan': '🔴', uc: '🟡', xunlei: '🟢',
|
||
pikpak: '🟤', magnet: '🧲', ed2k: '🔗', others: '📁',
|
||
}
|
||
return icons[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
|
||
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, {
|
||
onSearching: () => {
|
||
searchTime.value = Date.now() - startTime
|
||
loadingPhase.value = 'search'
|
||
},
|
||
|
||
onSaved: (data) => {
|
||
searchTime.value = Date.now() - startTime
|
||
// Add DB cached results to display immediately
|
||
const saved = (data.results || []).map((item: any) => ({
|
||
...item,
|
||
id: item.id || ('saved_' + Math.random()),
|
||
}))
|
||
// Add to the map so validation doesn't re-add them
|
||
for (const item of saved) {
|
||
allResultsMap.value.set(item.id, item)
|
||
}
|
||
validResults.value.push(...saved)
|
||
searchResults.value = [...validResults.value]
|
||
channels.value = buildChannelsFromResults(searchResults.value).map((ch: any) => ({
|
||
...ch,
|
||
newestTime: getChannelNewestTime(ch.items),
|
||
}))
|
||
totalCount.value = searchResults.value.length
|
||
loadingPhase.value = 'validate'
|
||
},
|
||
|
||
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
|
||
}
|
||
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
|
||
}
|
||
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
|
||
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
|
||
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);
|
||
}
|
||
.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: 1px;
|
||
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 {
|
||
max-width: 800px;
|
||
margin: 16px auto 0;
|
||
padding: 0 24px;
|
||
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;
|
||
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-count {
|
||
font-size: 11px;
|
||
color: #c0c4cc;
|
||
margin-left: 2px;
|
||
}
|
||
|
||
/* 结果列表 */
|
||
.result-content {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
padding: 0 24px 48px;
|
||
}
|
||
|
||
/* 宽屏下搜索结果容器加宽以容纳3列 */
|
||
@media (min-width: 1280px) {
|
||
.result-content {
|
||
max-width: 1200px;
|
||
}
|
||
}
|
||
|
||
.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-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;
|
||
}
|
||
|
||
/* 全部标签 — 卡片网格 */
|
||
.flat-list {
|
||
display: grid !important;
|
||
grid-template-columns: 1fr;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* 响应式:平板 2 列 */
|
||
@media (min-width: 768px) {
|
||
.flat-list {
|
||
grid-template-columns: 1fr 1fr;
|
||
}
|
||
}
|
||
|
||
/* 响应式:桌面 3 列 */
|
||
@media (min-width: 1280px) {
|
||
.flat-list {
|
||
grid-template-columns: 1fr 1fr 1fr;
|
||
}
|
||
}
|
||
|
||
/* 频道列表保持单列 */
|
||
.channel-list {
|
||
gap: 12px;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* ---- 保存弹窗 — 进度流程 ---- */
|
||
.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;
|
||
}
|
||
|
||
/* 内容信息(豆瓣)全面展示 */
|
||
.content-info-section {
|
||
max-width: 800px;
|
||
margin: 16px auto 0;
|
||
padding: 0 24px;
|
||
}
|
||
|
||
.media-card {
|
||
display: flex;
|
||
gap: 20px;
|
||
background: #fff;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||
border: 1px solid #ebeef5;
|
||
text-decoration: none;
|
||
color: inherit;
|
||
transition: box-shadow 0.2s;
|
||
}
|
||
.media-card-link:hover {
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||
border-color: #c0c4cc;
|
||
}
|
||
.media-card-link:hover .media-hint {
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
.media-hint {
|
||
font-size: 12px;
|
||
color: #c0c4cc;
|
||
margin-top: auto;
|
||
transition: color 0.2s;
|
||
}
|
||
|
||
.media-poster-wrap {
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
width: 120px;
|
||
height: 170px;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.media-poster {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.media-poster-placeholder {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||
font-size: 40px;
|
||
color: rgba(255,255,255,0.7);
|
||
}
|
||
|
||
/* 评分 + 来源角标 */
|
||
.media-rating-badge {
|
||
position: absolute;
|
||
bottom: 6px;
|
||
left: 6px;
|
||
padding: 2px 8px;
|
||
background: rgba(0, 0, 0, 0.65);
|
||
color: #ffd700;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
border-radius: 4px;
|
||
backdrop-filter: blur(4px);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.media-source-badge {
|
||
position: absolute;
|
||
top: 6px;
|
||
right: 6px;
|
||
padding: 2px 7px;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
color: #fff;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
border-radius: 4px;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.media-body {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
padding-top: 4px;
|
||
}
|
||
|
||
.media-title {
|
||
font-size: 20px;
|
||
font-weight: 800;
|
||
color: #1a1a2e;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.media-year {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #909399;
|
||
}
|
||
|
||
/* 评分行 */
|
||
.media-meta-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.media-meta-rating {
|
||
font-size: 14px;
|
||
color: #e6a23c;
|
||
}
|
||
.media-meta-rating strong {
|
||
font-size: 16px;
|
||
color: #f56c6c;
|
||
}
|
||
.rating-count {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.media-source-tag {
|
||
font-size: 11px;
|
||
padding: 1px 8px;
|
||
background: #ecf5ff;
|
||
color: #409eff;
|
||
border-radius: 10px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 元信息行:导演 / 演员 / 类型 */
|
||
.media-meta-line {
|
||
display: flex;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.meta-label {
|
||
flex-shrink: 0;
|
||
color: #909399;
|
||
width: 2.5em;
|
||
text-align: right;
|
||
}
|
||
|
||
.meta-value {
|
||
color: #606266;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 2px;
|
||
align-items: center;
|
||
}
|
||
|
||
.meta-actors {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
}
|
||
|
||
.meta-genres {
|
||
display: inline-flex;
|
||
gap: 4px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.meta-genre-tag {
|
||
display: inline-block;
|
||
padding: 1px 8px;
|
||
background: #fef0f0;
|
||
color: #f56c6c;
|
||
border-radius: 10px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.meta-region,
|
||
.meta-duration {
|
||
color: #606266;
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* 描述 */
|
||
.media-subtitle {
|
||
font-size: 13px;
|
||
color: #909399;
|
||
line-height: 1.5;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 3;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.media-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 5px;
|
||
margin-top: auto;
|
||
}
|
||
|
||
.media-tag {
|
||
display: inline-block;
|
||
padding: 3px 12px;
|
||
border-radius: 14px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
transition: transform 0.15s;
|
||
}
|
||
.media-tag:hover {
|
||
transform: scale(1.05);
|
||
}
|
||
.media-tag-genre {
|
||
background: #f0f9eb;
|
||
color: #67c23a;
|
||
}
|
||
.media-tag-type {
|
||
background: #ecf5ff;
|
||
color: #409eff;
|
||
}
|
||
.media-tag-quality {
|
||
background: #fdf6ec;
|
||
color: #e6a23c;
|
||
}
|
||
.media-tag-default {
|
||
background: #f4f4f5;
|
||
color: #909399;
|
||
}
|
||
|
||
/* 重命名信息 */
|
||
.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;
|
||
}
|
||
}
|
||
|
||
.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>
|