mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 03:15:04 +08:00
1069 lines
38 KiB
Vue
1069 lines
38 KiB
Vue
<template>
|
||
<div v-if="!systemConfig?.maintenance_mode" class="min-h-screen bg-gray-50 dark:bg-slate-900 text-gray-800 dark:text-slate-100">
|
||
<!-- 主要内容区域 -->
|
||
<div class="flex-1 p-3 sm:p-5">
|
||
<div class="max-w-7xl mx-auto">
|
||
<!-- 头部导航 -->
|
||
<div class="header-container bg-slate-800 dark:bg-slate-800 text-white dark:text-slate-100 rounded-lg shadow-lg p-4 sm:p-8 mb-4 sm:mb-8 text-center relative">
|
||
<h1 class="flex items-center justify-center gap-3">
|
||
<img
|
||
v-if="systemConfig?.site_logo"
|
||
:src="getImageUrl(systemConfig.site_logo)"
|
||
:alt="systemConfig?.site_title || 'Logo'"
|
||
class="h-8 w-auto object-contain"
|
||
@error="handleLogoError"
|
||
/>
|
||
<img
|
||
v-else
|
||
src="/assets/images/logo.webp"
|
||
alt="Logo"
|
||
class="h-8 w-auto object-contain"
|
||
/>
|
||
<NuxtLink to="/" class="text-white hover:text-gray-200 dark:hover:text-gray-300 no-underline text-2xl sm:text-3xl font-bold">
|
||
{{ systemConfig?.site_title || '老九网盘资源数据库' }}
|
||
</NuxtLink>
|
||
</h1>
|
||
|
||
<!-- 右侧导航按钮 -->
|
||
<nav class="mt-4 flex flex-col sm:flex-row justify-center gap-2 sm:gap-2 right-4 top-0 absolute">
|
||
<NuxtLink to="/" class="hidden sm:flex">
|
||
<n-button size="tiny" type="tertiary" round ghost class="!px-2 !py-1 !text-xs !text-white dark:!text-white !border-white/30 hover:!border-white">
|
||
<i class="fas fa-arrow-left text-xs"></i>
|
||
<span class="ml-1">返回首页</span>
|
||
</n-button>
|
||
</NuxtLink>
|
||
</nav>
|
||
</div>
|
||
|
||
<!-- 资源详情内容 -->
|
||
<div v-if="resourcesData" class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||
<!-- 主内容区域 -->
|
||
<div class="lg:col-span-2 space-y-6">
|
||
<!-- 资源主卡片 -->
|
||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg overflow-hidden">
|
||
<div class="flex flex-col sm:flex-row gap-6 p-6">
|
||
<!-- 封面图片 -->
|
||
<div class="flex-shrink-0 mx-auto sm:mx-0">
|
||
<n-image
|
||
:src="getResourceImageUrl(mainResource)"
|
||
:alt="mainResource?.title || '资源图片'"
|
||
width="160"
|
||
class="rounded-xl object-cover border-2 border-gray-200 dark:border-slate-600 shadow-md hover:shadow-xl transition-all duration-300 w-40 h-56"
|
||
@error="handleResourceImageError"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 资源信息 -->
|
||
<div class="flex-1 space-y-4">
|
||
<!-- 标题和操作按钮 -->
|
||
<div class="flex flex-col gap-2">
|
||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||
<h1 class="text-xl sm:text-2xl font-bold text-gray-900 dark:text-gray-100 break-words leading-tight flex-1" v-html="mainResource?.title_highlight || mainResource?.title">
|
||
</h1>
|
||
|
||
<!-- 操作按钮组 -->
|
||
<div class="flex items-center gap-2 flex-shrink-0">
|
||
<button
|
||
class="px-3 py-1.5 text-xs font-medium rounded-lg border border-orange-200 dark:border-orange-400/30 bg-orange-50 dark:bg-orange-500/10 text-orange-600 dark:text-orange-400 hover:bg-orange-100 dark:hover:bg-orange-500/20 transition-colors flex items-center gap-1"
|
||
@click="showReportModal = true"
|
||
title="举报资源失效"
|
||
>
|
||
<i class="fas fa-exclamation-circle"></i>
|
||
<span class="hidden sm:inline">举报</span>
|
||
</button>
|
||
<button
|
||
class="px-3 py-1.5 text-xs font-medium rounded-lg border border-purple-200 dark:border-purple-400/30 bg-purple-50 dark:bg-purple-500/10 text-purple-600 dark:text-purple-400 hover:bg-purple-100 dark:hover:bg-purple-500/20 transition-colors flex items-center gap-1"
|
||
@click="showCopyrightModal = true"
|
||
title="版权申述"
|
||
>
|
||
<i class="fas fa-balance-scale"></i>
|
||
<span class="hidden sm:inline">申述</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 时间和浏览次数 -->
|
||
<div class="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
|
||
<span class="flex items-center gap-1">
|
||
<i class="fas fa-calendar-alt text-blue-500"></i>
|
||
{{ formatDate(mainResource?.updated_at) }}
|
||
</span>
|
||
<span class="flex items-center gap-1">
|
||
<i class="fas fa-eye text-green-500"></i>
|
||
{{ mainResource?.view_count || 0 }}次浏览
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 标签 -->
|
||
<div v-if="mainResource?.tags && mainResource.tags.length > 0" class="flex flex-wrap gap-2">
|
||
<template v-for="tag in mainResource.tags" :key="tag.id">
|
||
<span class="resource-tag inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-blue-50 dark:bg-blue-500/10 text-blue-700 dark:text-blue-100 border border-blue-200 dark:border-blue-400/30">
|
||
<i class="fas fa-tag mr-1 text-blue-500 dark:text-blue-300 text-xs"></i>
|
||
{{ tag.name || '未知标签' }}
|
||
</span>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 描述 -->
|
||
<div v-if="mainResource?.description" class="text-gray-600 dark:text-gray-300 text-sm leading-relaxed bg-gray-50 dark:bg-slate-700/50 rounded-lg p-4 border border-gray-100 dark:border-slate-600" v-html="mainResource.description_highlight || mainResource.description">
|
||
</div>
|
||
|
||
<!-- 基本信息 -->
|
||
<div v-if="mainResource?.file_size || mainResource?.author" class="flex flex-wrap gap-4 text-xs text-gray-500 dark:text-gray-400">
|
||
<span v-if="mainResource?.file_size" class="flex items-center gap-1">
|
||
<i class="fas fa-file text-purple-500"></i>
|
||
{{ mainResource.file_size }}
|
||
</span>
|
||
<span v-if="mainResource?.author" class="flex items-center gap-1">
|
||
<i class="fas fa-user text-orange-500"></i>
|
||
{{ mainResource.author }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 网盘资源链接列表 -->
|
||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg overflow-hidden">
|
||
<div class="p-6">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||
<i class="fas fa-cloud-download-alt text-blue-500"></i>
|
||
网盘资源 ({{ resourcesData?.resources?.length || 0 }})
|
||
</h3>
|
||
|
||
<!-- 分享按钮 -->
|
||
<ShareButtons
|
||
:title="mainResource?.title"
|
||
:description="mainResource?.description"
|
||
:url="getResourceUrl"
|
||
:tags="mainResource?.tags?.map(tag => tag.name)"
|
||
/>
|
||
</div>
|
||
|
||
<div class="space-y-3">
|
||
<div
|
||
v-for="(resource, index) in resourcesData?.resources"
|
||
:key="resource.id"
|
||
class="flex items-center justify-between p-4 border rounded-xl hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors"
|
||
:class="resource.is_valid
|
||
? 'border-gray-200 dark:border-slate-600'
|
||
: 'border-red-200 dark:border-red-400 bg-red-50/50 dark:bg-red-500/5'"
|
||
>
|
||
<!-- 左侧:平台信息 -->
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-10 h-10 rounded-lg bg-gray-100 dark:bg-slate-700 flex items-center justify-center">
|
||
<span v-html="resource.pan?.icon" class="text-lg"></span>
|
||
</div>
|
||
<div>
|
||
<div class="font-medium text-gray-900 dark:text-gray-100">{{ resource.pan?.remark || '未知平台' }}</div>
|
||
<div class="flex items-center gap-2 mt-1">
|
||
<span
|
||
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
||
:class="resource.is_valid
|
||
? 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-100'
|
||
: 'bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-100'"
|
||
>
|
||
<span class="w-1.5 h-1.5 rounded-full mr-1" :class="resource.is_valid ? 'bg-green-500' : 'bg-red-500'"></span>
|
||
{{ resource.is_valid ? '有效' : '无效' }}
|
||
</span>
|
||
<span v-if="resource.save_url" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-100">
|
||
已转存
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:操作按钮 -->
|
||
<div class="flex items-center gap-2">
|
||
<!-- 检测中状态 -->
|
||
<div v-if="isDetecting && !detectionResults[resource.id]" class="flex items-center gap-2 px-4 py-2 text-sm text-gray-500 dark:text-gray-400">
|
||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
|
||
<span>检测中</span>
|
||
</div>
|
||
|
||
<!-- 检测完成后的按钮 -->
|
||
<template v-else>
|
||
<button
|
||
class="px-4 py-2 text-sm font-medium rounded-lg border border-blue-200 dark:border-blue-400/30 bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-500/20 transition-colors flex items-center gap-2 disabled:opacity-50"
|
||
:disabled="resource.forbidden || loadingStates[resource.id]"
|
||
@click="toggleLink(resource)"
|
||
>
|
||
<i class="fas" :class="loadingStates[resource.id] ? 'fa-spinner fa-spin' : 'fa-external-link-alt'"></i>
|
||
{{ resource.forbidden ? '受限' : (loadingStates[resource.id] ? '获取中' : '获取链接') }}
|
||
</button>
|
||
|
||
<button
|
||
v-if="resource.save_url && !resource.forbidden"
|
||
class="p-2 text-sm rounded-lg border border-gray-200 dark:border-slate-600 bg-white dark:bg-slate-700 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-slate-600 transition-colors"
|
||
@click="copyToClipboard(resource.save_url)"
|
||
title="复制转存链接"
|
||
>
|
||
<i class="fas fa-copy"></i>
|
||
</button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 违禁词提示 -->
|
||
<div v-if="resourcesData?.resources?.some(r => r.forbidden)" class="mt-4 p-4 bg-yellow-50 dark:bg-yellow-500/10 rounded-lg border border-yellow-200 dark:border-yellow-400/30">
|
||
<div class="flex items-start gap-2">
|
||
<i class="fas fa-exclamation-triangle text-yellow-600 dark:text-yellow-400 mt-0.5"></i>
|
||
<div class="text-sm text-yellow-800 dark:text-yellow-200">
|
||
部分资源包含受限内容,无法正常访问
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧边栏 -->
|
||
<div class="lg:col-span-1 space-y-6">
|
||
<!-- 相关资源 -->
|
||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg overflow-hidden">
|
||
<div class="p-6">
|
||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center gap-2">
|
||
<i class="fas fa-fire text-orange-500"></i>
|
||
相关资源
|
||
</h3>
|
||
|
||
<!-- 相关资源列表 -->
|
||
<div v-if="isRelatedResourcesLoading" class="space-y-3">
|
||
<div v-for="i in 5" :key="i" class="animate-pulse">
|
||
<div class="flex items-center gap-3 p-2 rounded-lg">
|
||
<div class="w-5 h-5 bg-gray-200 dark:bg-gray-700 rounded-full flex-shrink-0"></div>
|
||
<div class="flex-1 space-y-2">
|
||
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
||
<div class="h-3 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else-if="displayRelatedResources.length > 0" class="space-y-3">
|
||
<a
|
||
v-for="(resource, index) in displayRelatedResources"
|
||
:key="resource.id"
|
||
:href="`/r/${resource.key}`"
|
||
class="group block cursor-pointer"
|
||
@click.prevent="navigateToResource(resource.key)"
|
||
>
|
||
<div class="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors">
|
||
<!-- 序号 -->
|
||
<div class="flex-shrink-0 flex items-center justify-center">
|
||
<div
|
||
class="w-5 h-5 rounded-full flex items-center justify-center text-xs font-medium"
|
||
:class="index < 3
|
||
? 'bg-blue-500 text-white'
|
||
: 'bg-gray-300 dark:bg-gray-600 text-gray-700 dark:text-gray-300'"
|
||
>
|
||
{{ index + 1 }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 资源信息 -->
|
||
<div class="flex-1 min-w-0">
|
||
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 line-clamp-1 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
|
||
{{ resource.title }}
|
||
</h4>
|
||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-1">
|
||
{{ resource.description }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
|
||
<div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||
<i class="fas fa-inbox text-3xl mb-2"></i>
|
||
<p class="text-sm">暂无相关资源</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 热门资源 -->
|
||
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg overflow-hidden">
|
||
<div class="p-6">
|
||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center gap-2">
|
||
<i class="fas fa-trending-up text-red-500"></i>
|
||
热门资源
|
||
</h3>
|
||
|
||
<!-- 热门资源列表 -->
|
||
<div v-if="hotResourcesLoading" class="space-y-3">
|
||
<div v-for="i in 10" :key="i" class="animate-pulse">
|
||
<div class="flex items-center gap-3 p-2 rounded-lg">
|
||
<div class="w-5 h-5 bg-gray-200 dark:bg-gray-700 rounded-full flex-shrink-0"></div>
|
||
<div class="flex-1 space-y-2">
|
||
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
||
<div class="h-3 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else-if="hotResources.length > 0" class="space-y-3">
|
||
<a
|
||
v-for="(resource, index) in hotResources"
|
||
:key="resource.id"
|
||
:href="`/r/${resource.key}`"
|
||
class="group block cursor-pointer"
|
||
@click.prevent="navigateToResource(resource.key)"
|
||
>
|
||
<div class="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors">
|
||
<!-- 排名标识 -->
|
||
<div class="flex-shrink-0 flex items-center justify-center">
|
||
<div
|
||
class="w-5 h-5 rounded-full flex items-center justify-center text-xs font-medium"
|
||
:class="index < 3
|
||
? 'bg-red-500 text-white'
|
||
: 'bg-gray-300 dark:bg-gray-600 text-gray-700 dark:text-gray-300'"
|
||
>
|
||
{{ index + 1 }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 资源信息 -->
|
||
<div class="flex-1 min-w-0">
|
||
<div class="flex items-center justify-between">
|
||
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 line-clamp-1 group-hover:text-red-600 dark:group-hover:text-red-400 transition-colors flex-1">
|
||
{{ resource.title }}
|
||
</h4>
|
||
<!-- 排名皇冠 -->
|
||
<div class="flex items-center gap-1 flex-shrink-0 ml-2">
|
||
<i v-if="index === 0" class="fas fa-crown text-yellow-500 text-xs" title="第一名"></i>
|
||
<i v-else-if="index === 1" class="fas fa-crown text-gray-400 text-xs" title="第二名"></i>
|
||
<i v-else-if="index === 2" class="fas fa-crown text-orange-600 text-xs" title="第三名"></i>
|
||
</div>
|
||
</div>
|
||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-1">
|
||
{{ resource.description }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
|
||
<div v-else class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||
<i class="fas fa-fire-alt text-3xl mb-2"></i>
|
||
<p class="text-sm">暂无热门资源</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 404 状态 -->
|
||
<div v-else-if="!resourcesData" class="flex flex-col items-center justify-center py-20">
|
||
<div class="text-center space-y-4">
|
||
<i class="fas fa-search text-6xl text-gray-300 dark:text-gray-600"></i>
|
||
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">资源不存在</h2>
|
||
<p class="text-gray-600 dark:text-gray-400">抱歉,您访问的资源不存在或已被删除</p>
|
||
<NuxtLink
|
||
to="/"
|
||
class="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-all duration-200"
|
||
>
|
||
<i class="fas fa-home"></i>
|
||
返回首页
|
||
</NuxtLink>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 链接模态框 -->
|
||
<QrCodeModal
|
||
:visible="showLinkModal"
|
||
:url="selectedResource?.url"
|
||
:save_url="selectedResource?.save_url"
|
||
:loading="selectedResource?.loading"
|
||
:linkType="selectedResource?.linkType"
|
||
:platform="selectedResource?.platform"
|
||
:message="selectedResource?.message"
|
||
:error="selectedResource?.error"
|
||
:forbidden="selectedResource?.forbidden"
|
||
:forbidden_words="selectedResource?.forbidden_words"
|
||
@close="showLinkModal = false"
|
||
/>
|
||
|
||
<!-- 举报模态框 -->
|
||
<ReportModal
|
||
:visible="showReportModal"
|
||
:resource-key="resourceKey"
|
||
@close="showReportModal = false"
|
||
@submitted="handleReportSubmitted"
|
||
/>
|
||
|
||
<!-- 版权申述模态框 -->
|
||
<CopyrightModal
|
||
:visible="showCopyrightModal"
|
||
:resource-key="resourceKey"
|
||
@close="showCopyrightModal = false"
|
||
@submitted="handleCopyrightSubmitted"
|
||
/>
|
||
|
||
<!-- 页脚 -->
|
||
<AppFooter />
|
||
|
||
<!-- 悬浮按钮组件 -->
|
||
<FloatButtons />
|
||
</div>
|
||
|
||
<!-- 维护模式 -->
|
||
<div v-if="systemConfig?.maintenance_mode" class="fixed inset-0 z-[1000000] flex items-center justify-center bg-gradient-to-br from-yellow-100/80 via-gray-900/90 to-yellow-200/80 backdrop-blur-sm">
|
||
<div class="bg-white dark:bg-gray-800 rounded-3xl shadow-2xl px-8 py-10 flex flex-col items-center max-w-xs w-full border border-yellow-200 dark:border-yellow-700">
|
||
<i class="fas fa-tools text-yellow-500 text-5xl mb-6 animate-bounce-slow"></i>
|
||
<h3 class="text-2xl font-extrabold text-yellow-600 dark:text-yellow-400 mb-2 tracking-wide drop-shadow">系统维护中</h3>
|
||
<p class="text-base text-gray-600 dark:text-gray-300 mb-6 text-center leading-relaxed">
|
||
我们正在进行系统升级和维护,预计很快恢复服务。<br>
|
||
请稍后再试,感谢您的理解与支持!
|
||
</p>
|
||
<div class="flex space-x-1 mt-2">
|
||
<span class="w-2 h-2 bg-yellow-400 rounded-full animate-blink"></span>
|
||
<span class="w-2 h-2 bg-yellow-500 rounded-full animate-blink delay-200"></span>
|
||
<span class="w-2 h-2 bg-yellow-600 rounded-full animate-blink delay-400"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
// 获取运行时配置
|
||
const config = useRuntimeConfig()
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
|
||
// 获取资源key参数
|
||
const resourceKey = computed(() => route.params.key as string)
|
||
|
||
// 导入API
|
||
import { useResourceApi, usePublicSystemConfigApi } from '~/composables/useApi'
|
||
|
||
const resourceApi = useResourceApi()
|
||
const publicSystemConfigApi = usePublicSystemConfigApi()
|
||
|
||
// 响应式数据
|
||
const showLinkModal = ref(false)
|
||
const showReportModal = ref(false)
|
||
const showCopyrightModal = ref(false)
|
||
const selectedResource = ref<any>(null)
|
||
const loadingStates = ref<Record<number, boolean>>({})
|
||
const isDetecting = ref(false)
|
||
const detectionResults = ref<Record<number, boolean>>({})
|
||
const relatedResources = ref<any[]>([])
|
||
const relatedResourcesLoading = ref(true)
|
||
const hotResources = ref<any[]>([])
|
||
const hotResourcesLoading = ref(true)
|
||
|
||
// 获取系统配置
|
||
const { data: systemConfigData } = await useAsyncData('systemConfig', () => publicSystemConfigApi.getPublicSystemConfig())
|
||
const systemConfig = computed(() => (systemConfigData.value as any)?.data || { site_title: '老九网盘资源数据库' })
|
||
|
||
// 获取资源数据
|
||
const { data: resourcesData, error: resourcesError } = await useAsyncData(
|
||
`resources-${resourceKey.value}`,
|
||
() => resourceApi.getResourcesByKey(resourceKey.value),
|
||
{
|
||
server: true,
|
||
default: () => null
|
||
}
|
||
)
|
||
|
||
// 获取相关资源(服务端渲染,用于SEO优化)
|
||
const { data: relatedResourcesData } = await useAsyncData(
|
||
`related-resources-${resourceKey.value}`,
|
||
() => {
|
||
const params = {
|
||
key: resourceKey.value,
|
||
limit: 5
|
||
}
|
||
return resourceApi.getRelatedResources(params)
|
||
},
|
||
{
|
||
server: true,
|
||
default: () => ({ data: [] })
|
||
}
|
||
)
|
||
|
||
// 主要资源信息
|
||
const mainResource = computed(() => {
|
||
const resources = resourcesData.value?.resources
|
||
return resources && resources.length > 0 ? resources[0] : null
|
||
})
|
||
|
||
// 生成完整的资源URL
|
||
const getResourceUrl = computed(() => {
|
||
const config = useRuntimeConfig()
|
||
const key = mainResource.value?.key
|
||
if (!key) return ''
|
||
|
||
// 优先使用配置中的站点URL,如果未设置则使用当前页面的origin
|
||
let siteUrl = config.public.siteUrl
|
||
if (!siteUrl || siteUrl === 'https://yourdomain.com') {
|
||
// 在客户端使用当前页面的origin
|
||
if (typeof window !== 'undefined') {
|
||
siteUrl = window.location.origin
|
||
} else {
|
||
// 在服务端渲染时,使用默认值(这应该在部署时被环境变量覆盖)
|
||
siteUrl = process.env.NUXT_PUBLIC_SITE_URL || 'https://yourdomain.com'
|
||
}
|
||
}
|
||
|
||
return `${siteUrl}/r/${key}`
|
||
})
|
||
|
||
// 服务端相关资源处理(去重)
|
||
const serverRelatedResources = computed(() => {
|
||
const resources = Array.isArray(relatedResourcesData.value?.data) ? relatedResourcesData.value.data : []
|
||
|
||
// 根据key去重,避免显示重复资源
|
||
const uniqueResources = resources.filter((resource, index, self) =>
|
||
index === self.findIndex((r) => r.key === resource.key)
|
||
)
|
||
|
||
return uniqueResources.slice(0, 5) // 最多显示5个相关资源
|
||
})
|
||
|
||
// 合并服务端和客户端相关资源,优先显示服务端数据,支持SEO
|
||
const displayRelatedResources = computed(() => {
|
||
// 如果有客户端数据(可能是更新的数据),使用客户端数据
|
||
if (relatedResources.value.length > 0) {
|
||
return relatedResources.value
|
||
}
|
||
|
||
// 否则使用服务端数据,确保SEO友好
|
||
return serverRelatedResources.value
|
||
})
|
||
|
||
// 相关资源加载状态
|
||
const isRelatedResourcesLoading = computed(() => {
|
||
// 如果有服务端数据,不显示加载状态
|
||
if (serverRelatedResources.value.length > 0) {
|
||
return false
|
||
}
|
||
|
||
// 否则显示客户端加载状态
|
||
return relatedResourcesLoading.value
|
||
})
|
||
|
||
// 检测状态
|
||
const detectionStatus = computed(() => {
|
||
if (isDetecting.value) {
|
||
return {
|
||
icon: 'fas fa-spinner fa-spin text-blue-600',
|
||
text: 'text-blue-600',
|
||
label: '检测中'
|
||
}
|
||
}
|
||
|
||
const resources = resourcesData.value?.resources
|
||
if (!resources || resources.length === 0) {
|
||
return {
|
||
icon: 'fas fa-question-circle text-gray-400',
|
||
text: 'text-gray-400',
|
||
label: '未知状态'
|
||
}
|
||
}
|
||
|
||
const validCount = resources.filter(r => detectionResults.value[r.id] !== false && (detectionResults.value[r.id] || r.is_valid)).length
|
||
const totalCount = resources.length
|
||
|
||
if (validCount === totalCount) {
|
||
return {
|
||
icon: 'fas fa-check-circle text-green-600',
|
||
text: 'text-green-600',
|
||
label: '全部有效'
|
||
}
|
||
} else if (validCount === 0) {
|
||
return {
|
||
icon: 'fas fa-times-circle text-red-600',
|
||
text: 'text-red-600',
|
||
label: '全部无效'
|
||
}
|
||
} else {
|
||
return {
|
||
icon: 'fas fa-exclamation-triangle text-orange-600',
|
||
text: 'text-orange-600',
|
||
label: `${validCount}/${totalCount} 有效`
|
||
}
|
||
}
|
||
})
|
||
|
||
// 图片URL处理
|
||
const { getImageUrl } = useImageUrl()
|
||
|
||
// Logo错误处理
|
||
const handleLogoError = (event: Event) => {
|
||
const img = event.target as HTMLImageElement
|
||
img.src = '/assets/images/logo.webp'
|
||
}
|
||
|
||
// 获取资源图片URL
|
||
const getResourceImageUrl = (resource: any) => {
|
||
if (!resource) return '/assets/images/cover1.webp'
|
||
|
||
if (resource.image_url) {
|
||
return getImageUrl(resource.image_url)
|
||
}
|
||
|
||
if (resource.cover) {
|
||
return getImageUrl(resource.cover)
|
||
}
|
||
|
||
const randomNum = Math.floor(Math.random() * 8) + 1
|
||
return `/assets/images/cover${randomNum}.webp`
|
||
}
|
||
|
||
// 处理资源图片加载错误
|
||
const handleResourceImageError = (event: Event) => {
|
||
const img = event.target as HTMLImageElement
|
||
const randomNum = Math.floor(Math.random() * 8) + 1
|
||
img.src = `/assets/images/cover${randomNum}.webp`
|
||
}
|
||
|
||
// 格式化日期
|
||
const formatDate = (dateString: string) => {
|
||
if (!dateString) return '未知'
|
||
const date = new Date(dateString)
|
||
return date.toLocaleDateString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
})
|
||
}
|
||
|
||
// 检测资源有效性
|
||
const detectResourceValidity = async () => {
|
||
if (!resourcesData.value?.resources) return
|
||
|
||
isDetecting.value = true
|
||
detectionResults.value = {} // 重置检测结果
|
||
|
||
try {
|
||
// 逐个检测每个资源
|
||
for (const resource of resourcesData.value.resources) {
|
||
try {
|
||
// 这里可以添加实际的检测逻辑
|
||
// const result = await resourceApi.checkResourceValidity(resource.id)
|
||
// detectionResults.value[resource.id] = result.isValid
|
||
|
||
// 暂时使用现有的is_valid字段,但添加随机延迟模拟真实检测
|
||
await new Promise(resolve => setTimeout(resolve, 800 + Math.random() * 700))
|
||
detectionResults.value[resource.id] = resource.is_valid
|
||
} catch (error) {
|
||
console.error(`检测资源 ${resource.id} 失败:`, error)
|
||
detectionResults.value[resource.id] = false
|
||
}
|
||
}
|
||
} finally {
|
||
isDetecting.value = false
|
||
}
|
||
}
|
||
|
||
// 切换链接显示
|
||
const toggleLink = async (resource: any) => {
|
||
if (resource.forbidden) {
|
||
selectedResource.value = {
|
||
...resource,
|
||
forbidden: true,
|
||
error: '该资源包含受限内容,无法访问',
|
||
forbidden_words: resource.forbidden_words || []
|
||
}
|
||
showLinkModal.value = true
|
||
return
|
||
}
|
||
|
||
loadingStates.value[resource.id] = true
|
||
selectedResource.value = { ...resource, loading: true }
|
||
showLinkModal.value = true
|
||
|
||
try {
|
||
const linkData = await resourceApi.getResourceLink(resource.id) as any
|
||
|
||
selectedResource.value = {
|
||
...resource,
|
||
url: linkData.url,
|
||
save_url: linkData.type === 'transferred' ? linkData.url : resource.save_url,
|
||
loading: false,
|
||
linkType: linkData.type,
|
||
platform: linkData.platform,
|
||
message: linkData.message
|
||
}
|
||
} catch (error: any) {
|
||
console.error('获取资源链接失败:', error)
|
||
selectedResource.value = {
|
||
...resource,
|
||
loading: false,
|
||
error: '检测有效性失败,请自行验证'
|
||
}
|
||
} finally {
|
||
loadingStates.value[resource.id] = false
|
||
}
|
||
}
|
||
|
||
// 复制到剪贴板
|
||
const copyToClipboard = async (text: string) => {
|
||
try {
|
||
await navigator.clipboard.writeText(text)
|
||
// 显示复制成功提示
|
||
if (process.client) {
|
||
const notification = useNotification()
|
||
if (notification) {
|
||
notification.success({
|
||
content: '已复制到剪贴板',
|
||
duration: 2000
|
||
})
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('复制失败:', error)
|
||
}
|
||
}
|
||
|
||
// 处理举报提交
|
||
const handleReportSubmitted = () => {
|
||
showReportModal.value = false
|
||
if (process.client) {
|
||
const notification = useNotification()
|
||
if (notification) {
|
||
notification.success({
|
||
content: '举报已提交,感谢您的反馈',
|
||
duration: 3000
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理版权申述提交
|
||
const handleCopyrightSubmitted = () => {
|
||
showCopyrightModal.value = false
|
||
if (process.client) {
|
||
const notification = useNotification()
|
||
if (notification) {
|
||
notification.success({
|
||
content: '版权申述已提交,我们会尽快处理',
|
||
duration: 3000
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取相关资源(客户端更新,用于交互优化)
|
||
const fetchRelatedResources = async () => {
|
||
if (!mainResource.value) return
|
||
|
||
// 如果已经有服务端数据,跳过客户端获取(SEO优化)
|
||
if (serverRelatedResources.value.length > 0) {
|
||
console.log('使用服务端相关资源数据,跳过客户端获取')
|
||
return
|
||
}
|
||
|
||
try {
|
||
relatedResourcesLoading.value = true
|
||
|
||
// 使用新的相关资源API,基于资源key查找相关资源
|
||
const params: any = {
|
||
key: resourceKey.value, // 使用当前资源的key
|
||
limit: 5,
|
||
}
|
||
|
||
const response = await resourceApi.getRelatedResources(params) as any
|
||
|
||
// 处理响应数据
|
||
const resources = Array.isArray(response?.data) ? response.data : []
|
||
|
||
// 根据key去重,避免显示重复资源
|
||
const uniqueResources = resources.filter((resource, index, self) =>
|
||
index === self.findIndex((r) => r.key === resource.key)
|
||
)
|
||
|
||
relatedResources.value = uniqueResources.slice(0, 5) // 最多显示5个相关资源
|
||
|
||
console.log('获取相关资源成功:', {
|
||
source: response?.source,
|
||
count: relatedResources.value.length,
|
||
params: params
|
||
})
|
||
|
||
} catch (error) {
|
||
console.error('获取相关资源失败:', error)
|
||
relatedResources.value = []
|
||
} finally {
|
||
relatedResourcesLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 获取热门资源
|
||
const fetchHotResources = async () => {
|
||
try {
|
||
hotResourcesLoading.value = true
|
||
|
||
// 使用专门的热门资源API,保持10个热门资源
|
||
const params = {
|
||
limit: 10
|
||
}
|
||
|
||
const response = await resourceApi.getHotResources(params) as any
|
||
|
||
// 处理响应数据
|
||
const resources = Array.isArray(response?.data) ? response.data : []
|
||
|
||
hotResources.value = resources.slice(0, 10)
|
||
|
||
console.log('获取热门资源成功:', {
|
||
count: hotResources.value.length,
|
||
params: params
|
||
})
|
||
|
||
} catch (error) {
|
||
console.error('获取热门资源失败:', error)
|
||
hotResources.value = []
|
||
} finally {
|
||
hotResourcesLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 导航到资源详情页
|
||
const navigateToResource = (key: string) => {
|
||
navigateTo(`/r/${key}`)
|
||
}
|
||
|
||
// 页面加载完成后开始检测
|
||
onMounted(() => {
|
||
// 开始检测资源有效性
|
||
nextTick(() => {
|
||
if (resourcesData.value?.resources) {
|
||
detectResourceValidity()
|
||
}
|
||
})
|
||
|
||
// 获取相关资源
|
||
nextTick(() => {
|
||
fetchRelatedResources()
|
||
})
|
||
|
||
// 获取热门资源
|
||
nextTick(() => {
|
||
fetchHotResources()
|
||
})
|
||
})
|
||
|
||
// 设置页面SEO
|
||
const { initSystemConfig, setPageSeo } = useGlobalSeo()
|
||
const { generateOgImageUrl } = useSeo()
|
||
|
||
// 动态生成页面SEO信息
|
||
const pageSeo = computed(() => {
|
||
const resource = mainResource.value
|
||
if (!resource) return null
|
||
|
||
const title = resource.title || '资源详情'
|
||
const description = resource.description
|
||
? resource.description.substring(0, 160)
|
||
: `${resource.title} - 多网盘资源下载,支持百度网盘、阿里云盘、夸克网盘等多个平台`
|
||
|
||
const keywords = [
|
||
...(resource.tags?.map((tag: any) => tag.name) || []),
|
||
resource.title,
|
||
'网盘资源',
|
||
'资源下载',
|
||
...(resource.pan?.name ? [resource.pan.name] : [])
|
||
].join(', ')
|
||
|
||
return { title, description, keywords }
|
||
})
|
||
|
||
// 更新页面SEO的函数
|
||
const updatePageSeo = () => {
|
||
if (!pageSeo.value) return
|
||
|
||
const { title, description, keywords } = pageSeo.value
|
||
|
||
// 设置基本SEO
|
||
setPageSeo(title, {
|
||
description,
|
||
keywords
|
||
})
|
||
|
||
// 设置更详细的HTML元数据
|
||
const baseUrl = config.public.siteUrl || 'https://yourdomain.com'
|
||
const canonicalUrl = `${baseUrl}/r/${resourceKey.value}`
|
||
|
||
// 生成动态OG图片URL(使用新的key参数格式)
|
||
const ogImageUrl = generateOgImageUrl(resourceKey.value, '', 'blue')
|
||
|
||
useHead({
|
||
htmlAttrs: {
|
||
lang: 'zh-CN'
|
||
},
|
||
link: [
|
||
{
|
||
rel: 'canonical',
|
||
href: canonicalUrl
|
||
}
|
||
],
|
||
meta: [
|
||
// Open Graph
|
||
{
|
||
property: 'og:title',
|
||
content: title
|
||
},
|
||
{
|
||
property: 'og:description',
|
||
content: description
|
||
},
|
||
{
|
||
property: 'og:url',
|
||
content: canonicalUrl
|
||
},
|
||
{
|
||
property: 'og:type',
|
||
content: 'website'
|
||
},
|
||
{
|
||
property: 'og:image',
|
||
content: ogImageUrl
|
||
},
|
||
{
|
||
property: 'og:site_name',
|
||
content: systemConfig.value?.site_title || '老九网盘资源数据库'
|
||
},
|
||
// Twitter Card
|
||
{
|
||
name: 'twitter:card',
|
||
content: 'summary_large_image'
|
||
},
|
||
{
|
||
name: 'twitter:title',
|
||
content: title
|
||
},
|
||
{
|
||
name: 'twitter:description',
|
||
content: description
|
||
},
|
||
{
|
||
name: 'twitter:image',
|
||
content: ogImageUrl
|
||
},
|
||
// 其他元数据
|
||
{
|
||
name: 'robots',
|
||
content: 'index, follow'
|
||
},
|
||
{
|
||
name: 'author',
|
||
content: mainResource.value?.author || systemConfig.value?.site_title || '老九网盘资源数据库'
|
||
},
|
||
{
|
||
name: 'revisit-after',
|
||
content: '1 days'
|
||
}
|
||
],
|
||
script: [
|
||
{
|
||
type: 'application/ld+json',
|
||
innerHTML: JSON.stringify({
|
||
"@context": "https://schema.org",
|
||
"@type": "WebPage",
|
||
"name": title,
|
||
"description": description,
|
||
"url": canonicalUrl,
|
||
"image": ogImageUrl,
|
||
"mainEntity": {
|
||
"@type": "SoftwareApplication" || "DigitalDocument",
|
||
"name": title,
|
||
"description": description,
|
||
"author": {
|
||
"@type": "Person" || "Organization",
|
||
"name": mainResource.value?.author || '未知'
|
||
},
|
||
"dateModified": mainResource.value?.updated_at,
|
||
"keywords": keywords,
|
||
"image": ogImageUrl,
|
||
"offers": {
|
||
"@type": "Offer",
|
||
"price": "0",
|
||
"priceCurrency": "CNY"
|
||
}
|
||
},
|
||
"publisher": {
|
||
"@type": "Organization",
|
||
"name": systemConfig.value?.site_title || '老九网盘资源数据库'
|
||
},
|
||
"relatedContent": displayRelatedResources.value.map((resource, index) => ({
|
||
"@type": "SoftwareApplication" || "DigitalDocument",
|
||
"position": index + 1,
|
||
"name": resource.title,
|
||
"description": resource.description?.substring(0, 160) || '',
|
||
"url": `${baseUrl}/r/${resource.key}`,
|
||
"dateModified": resource.updated_at,
|
||
"keywords": resource.tags?.map((tag: any) => tag.name).join(', ') || '',
|
||
"image": generateOgImageUrl(resource.key, '', 'green')
|
||
}))
|
||
})
|
||
}
|
||
]
|
||
})
|
||
}
|
||
|
||
onBeforeMount(async () => {
|
||
await initSystemConfig()
|
||
updatePageSeo()
|
||
})
|
||
|
||
// 监听资源数据变化,更新SEO
|
||
watch([mainResource, systemConfig], () => {
|
||
nextTick(() => {
|
||
updatePageSeo()
|
||
})
|
||
}, { deep: true })
|
||
|
||
// 错误处理
|
||
watch(resourcesError, (error) => {
|
||
if (error) {
|
||
console.error('获取资源数据失败:', error)
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.header-container{
|
||
background: url(/assets/images/banner.webp) center top/cover no-repeat,
|
||
linear-gradient(
|
||
to bottom,
|
||
rgba(0,0,0,0.1) 0%,
|
||
rgba(0,0,0,0.25) 100%
|
||
);
|
||
}
|
||
|
||
.resource-tag {
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.resource-tag:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
@keyframes bounce-slow {
|
||
0%, 100% { transform: translateY(0);}
|
||
50% { transform: translateY(-12px);}
|
||
}
|
||
.animate-bounce-slow {
|
||
animation: bounce-slow 1.6s infinite;
|
||
}
|
||
@keyframes blink {
|
||
0%, 80%, 100% { opacity: 0.2;}
|
||
40% { opacity: 1;}
|
||
}
|
||
.animate-blink {
|
||
animation: blink 1.4s infinite both;
|
||
}
|
||
.animate-blink.delay-200 { animation-delay: 0.2s; }
|
||
.animate-blink.delay-400 { animation-delay: 0.4s; }
|
||
</style> |