添加快捷入口面板和每日推荐功能,更新样式和API
This commit is contained in:
parent
7e0f347c9a
commit
bacbe1871c
BIN
vue/public/暂无新片上映海报生成.png
Normal file
BIN
vue/public/暂无新片上映海报生成.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
383
vue/src/App.vue
383
vue/src/App.vue
@ -42,6 +42,73 @@
|
|||||||
<button type="button" @click="handleHeroSearch">搜索</button>
|
<button type="button" @click="handleHeroSearch">搜索</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 快捷入口按钮和悬浮面板 -->
|
||||||
|
<div class="quick-access-wrapper" ref="quickAccessWrapperRef" @mouseenter="handleQuickAccessEnter" @mouseleave="handleQuickAccessWrapperLeave">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="quick-access-btn"
|
||||||
|
:class="{ active: showQuickAccessPanel }"
|
||||||
|
@click.stop="toggleQuickAccessPanel"
|
||||||
|
title="快捷入口"
|
||||||
|
>
|
||||||
|
<svg class="quick-access-btn__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="7" height="7" rx="1"/>
|
||||||
|
<rect x="14" y="3" width="7" height="7" rx="1"/>
|
||||||
|
<rect x="3" y="14" width="7" height="7" rx="1"/>
|
||||||
|
<rect x="14" y="14" width="7" height="7" rx="1"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 快捷入口悬浮面板 -->
|
||||||
|
<Transition name="quick-access-fade">
|
||||||
|
<div v-if="showQuickAccessPanel" class="quick-access-panel" @mouseenter="handleQuickAccessPanelEnter" @mouseleave="handleQuickAccessPanelLeave" @click.stop>
|
||||||
|
<div class="quick-access-panel__header">
|
||||||
|
<h3>快捷入口</h3>
|
||||||
|
<div class="quick-icons__actions">
|
||||||
|
<!-- 内外网切换 -->
|
||||||
|
<div class="network-toggle" title="切换内外网链接">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="['network-toggle__btn', { 'network-toggle__btn--active': networkMode === 'external' }]"
|
||||||
|
@click="networkMode = 'external'"
|
||||||
|
>外网</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="['network-toggle__btn', { 'network-toggle__btn--active': networkMode === 'internal' }]"
|
||||||
|
@click="networkMode = 'internal'"
|
||||||
|
>内网</button>
|
||||||
|
</div>
|
||||||
|
<button v-if="!hasSearched" type="button" class="quick-icons__manage" @click="showIconManageDialog = true" title="管理图标">⚙️</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="iconList.length" class="quick-icons__grid">
|
||||||
|
<a
|
||||||
|
v-for="icon in iconList"
|
||||||
|
:key="icon.id"
|
||||||
|
:href="getIconLink(icon)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="quick-icons__item"
|
||||||
|
:title="icon.description || icon.name"
|
||||||
|
>
|
||||||
|
<div class="quick-icons__thumb">
|
||||||
|
<img
|
||||||
|
:src="icon.logoUrl || getIconFavicon(icon.url)"
|
||||||
|
:alt="icon.name"
|
||||||
|
@error="(e) => { e.target.style.display = 'none'; e.target.nextElementSibling.style.display = 'flex' }"
|
||||||
|
/>
|
||||||
|
<span class="quick-icons__fallback" style="display:none">{{ (icon.name || '?')[0] }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="quick-icons__label">{{ icon.name }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div v-else class="quick-icons__empty">
|
||||||
|
<span>暂无快捷图标,点击 ⚙️ 添加</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 个人中心下拉菜单 -->
|
<!-- 个人中心下拉菜单 -->
|
||||||
<div class="user-menu" ref="userMenuRef">
|
<div class="user-menu" ref="userMenuRef">
|
||||||
<button type="button" class="user-menu__trigger" @click="showUserMenu = !showUserMenu">
|
<button type="button" class="user-menu__trigger" @click="showUserMenu = !showUserMenu">
|
||||||
@ -77,6 +144,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-menu__divider"></div>
|
<div class="user-menu__divider"></div>
|
||||||
|
<button type="button" class="user-menu__item" @click="switchToPoster">
|
||||||
|
<span class="user-menu__icon">🎬</span>
|
||||||
|
<span>海报展示</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="user-menu__item" @click="handleSwitchToManage">
|
||||||
|
<span class="user-menu__icon">📋</span>
|
||||||
|
<span>控制台</span>
|
||||||
|
</button>
|
||||||
<button type="button" class="user-menu__item" @click="handleTmdbConfig">
|
<button type="button" class="user-menu__item" @click="handleTmdbConfig">
|
||||||
<span class="user-menu__icon">⚙️</span>
|
<span class="user-menu__icon">⚙️</span>
|
||||||
<span>TMDB 配置</span>
|
<span>TMDB 配置</span>
|
||||||
@ -103,33 +178,17 @@
|
|||||||
<strong>{{ currentView === 'manage' ? '控制台' : '海报展示' }}</strong>
|
<strong>{{ currentView === 'manage' ? '控制台' : '海报展示' }}</strong>
|
||||||
<span>{{ currentView === 'manage' ? '管理网盘资源与账号数据' : '根据网盘内容进行 TMDB 海报刮削展示' }}</span>
|
<span>{{ currentView === 'manage' ? '管理网盘资源与账号数据' : '根据网盘内容进行 TMDB 海报刮削展示' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="view-switch" role="tablist" aria-label="视图切换">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="view-switch__btn"
|
|
||||||
:class="{ active: currentView === 'poster' }"
|
|
||||||
@click="currentView = 'poster'"
|
|
||||||
>
|
|
||||||
海报展示
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="view-switch__btn"
|
|
||||||
:class="{ active: currentView === 'manage' }"
|
|
||||||
@click="handleSwitchToManage"
|
|
||||||
>
|
|
||||||
控制台
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section v-if="currentView === 'poster' && !hasSearched && !isSearchFocused" class="hero-carousel" aria-label="每日推荐海报">
|
<section v-if="currentView === 'poster' && !hasSearched && !isSearchFocused" class="hero-carousel" aria-label="每日推荐海报">
|
||||||
|
<!-- 主海报横幅 -->
|
||||||
<Transition name="hero-fade" mode="out-in">
|
<Transition name="hero-fade" mode="out-in">
|
||||||
<article
|
<article
|
||||||
v-if="featuredPosters.length"
|
v-if="featuredPosters.length"
|
||||||
:key="activeFeaturedPoster.id"
|
:key="activeFeaturedPoster.id"
|
||||||
class="hero-slide hero-slide--single"
|
class="hero-main"
|
||||||
:style="buildHeroStyle(activeFeaturedPoster.poster)"
|
:class="{ 'hero-loading': !heroImageLoaded, 'hero-loaded': heroImageLoaded }"
|
||||||
|
:style="buildHeroStyle(activeFeaturedPoster.posterUrl)"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@mouseenter="pauseFeaturedAutoplay"
|
@mouseenter="pauseFeaturedAutoplay"
|
||||||
@mouseleave="resumeFeaturedAutoplay"
|
@mouseleave="resumeFeaturedAutoplay"
|
||||||
@ -138,22 +197,35 @@
|
|||||||
@keydown.left.prevent="showPrevFeaturedPoster"
|
@keydown.left.prevent="showPrevFeaturedPoster"
|
||||||
@keydown.right.prevent="showNextFeaturedPoster"
|
@keydown.right.prevent="showNextFeaturedPoster"
|
||||||
>
|
>
|
||||||
<div class="hero-slide__overlay">
|
<div class="hero-main__overlay">
|
||||||
<div class="hero-slide__topbar">
|
<div class="hero-main__badge">每日推荐</div>
|
||||||
<span class="hero-slide__badge">每日推荐</span>
|
<div class="hero-main__info">
|
||||||
</div>
|
<h2 class="hero-main__title">{{ activeFeaturedPoster.title }}</h2>
|
||||||
<div class="hero-slide__content">
|
<p class="hero-main__desc">{{ activeFeaturedPoster.subtitle || activeFeaturedPoster.description || '根据网盘资源内容自动匹配海报展示。' }}</p>
|
||||||
<h2>{{ activeFeaturedPoster.title }}</h2>
|
<div class="hero-main__meta">
|
||||||
<p>{{ activeFeaturedPoster.subtitle || activeFeaturedPoster.description || '根据网盘资源内容自动匹配海报展示。' }}</p>
|
<span v-if="activeFeaturedPoster.accountName" class="hero-main__meta-item">📁 {{ activeFeaturedPoster.accountName }}</span>
|
||||||
<div v-if="activeFeaturedPoster.accountName || activeFeaturedPoster.owner || activeFeaturedPoster.status" class="hero-slide__meta">
|
<span v-if="activeFeaturedPoster.owner" class="hero-main__meta-item">👤 {{ activeFeaturedPoster.owner }}</span>
|
||||||
<span v-if="activeFeaturedPoster.accountName">{{ activeFeaturedPoster.accountName }}</span>
|
<span v-if="activeFeaturedPoster.status" class="hero-main__meta-item">{{ activeFeaturedPoster.status === '已删除' ? '🗑️' : '✅' }} {{ activeFeaturedPoster.status }}</span>
|
||||||
<span v-if="activeFeaturedPoster.owner">{{ activeFeaturedPoster.owner }}</span>
|
|
||||||
<span v-if="activeFeaturedPoster.status">{{ activeFeaturedPoster.status }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
|
<!-- 小海报卡片 - 悬浮于大海报右下角 -->
|
||||||
|
<div class="hero-sub" :class="{ 'hero-sub--above': featuredPosters.length }">
|
||||||
|
<div
|
||||||
|
v-for="(poster, index) in featuredPosters"
|
||||||
|
:key="poster.id"
|
||||||
|
class="hero-sub__card"
|
||||||
|
:class="{ active: activeFeaturedPoster.id === poster.id }"
|
||||||
|
@click="setFeaturedPoster(index)"
|
||||||
|
>
|
||||||
|
<div class="hero-sub__thumb" :style="buildSubThumbStyle(poster.posterUrl)">
|
||||||
|
<span class="hero-sub__title">{{ poster.title }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-if="isLoading" class="loading-panel">
|
<section v-if="isLoading" class="loading-panel">
|
||||||
@ -177,7 +249,7 @@
|
|||||||
|
|
||||||
<template v-if="!isLoading && currentView === 'manage' && !isSearchFocused">
|
<template v-if="!isLoading && currentView === 'manage' && !isSearchFocused">
|
||||||
<!-- 快捷图标区域 -->
|
<!-- 快捷图标区域 -->
|
||||||
<section v-if="iconList.length || !hasSearched" class="quick-icons">
|
<section ref="quickIconsRef" v-if="iconList.length || !hasSearched" class="quick-icons">
|
||||||
<div class="quick-icons__header">
|
<div class="quick-icons__header">
|
||||||
<h3>快捷入口</h3>
|
<h3>快捷入口</h3>
|
||||||
<div class="quick-icons__actions">
|
<div class="quick-icons__actions">
|
||||||
@ -591,6 +663,7 @@ import {
|
|||||||
fetchIconList,
|
fetchIconList,
|
||||||
fetchLoginLogs,
|
fetchLoginLogs,
|
||||||
fetchMovieDuplicates,
|
fetchMovieDuplicates,
|
||||||
|
fetchRecommendMovies,
|
||||||
fetchTmdbConfig,
|
fetchTmdbConfig,
|
||||||
requestState,
|
requestState,
|
||||||
resetData,
|
resetData,
|
||||||
@ -630,6 +703,7 @@ const showFolderDialog = ref(false)
|
|||||||
const editingFolder = ref(null) // 编辑时传入的文件夹对象,null 表示新建
|
const editingFolder = ref(null) // 编辑时传入的文件夹对象,null 表示新建
|
||||||
const folderDialogParentId = ref(0) // 新建时的父文件夹ID
|
const folderDialogParentId = ref(0) // 新建时的父文件夹ID
|
||||||
const folderTreeContainerRef = ref(null) // 文件夹树容器引用(用于保持滚动位置)
|
const folderTreeContainerRef = ref(null) // 文件夹树容器引用(用于保持滚动位置)
|
||||||
|
const quickIconsRef = ref(null) // 快捷入口区域引用(用于滚动定位)
|
||||||
const expandedFolderIds = reactive(new Set()) // 全局展开状态追踪,防止 buildTree 重建丢失展开
|
const expandedFolderIds = reactive(new Set()) // 全局展开状态追踪,防止 buildTree 重建丢失展开
|
||||||
|
|
||||||
// 确认弹窗状态
|
// 确认弹窗状态
|
||||||
@ -667,6 +741,10 @@ const iconList = ref([]) // 快捷图标列表
|
|||||||
const networkMode = ref('external') // 网络模式:'external' | 'internal'
|
const networkMode = ref('external') // 网络模式:'external' | 'internal'
|
||||||
const showUserMenu = ref(false)
|
const showUserMenu = ref(false)
|
||||||
const userMenuRef = ref(null)
|
const userMenuRef = ref(null)
|
||||||
|
const showQuickAccessPanel = ref(false)
|
||||||
|
const quickAccessWrapperRef = ref(null)
|
||||||
|
let quickAccessTimer = null // 悬浮延迟定时器(显示面板)
|
||||||
|
let quickAccessCloseTimer = null // 面板关闭延迟定时器
|
||||||
const editingRecord = ref(null)
|
const editingRecord = ref(null)
|
||||||
const featuredPosterIndex = ref(0)
|
const featuredPosterIndex = ref(0)
|
||||||
|
|
||||||
@ -715,6 +793,14 @@ let featuredAutoplayTimer = 0
|
|||||||
const successMessage = ref('')
|
const successMessage = ref('')
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
// 每日推荐电影数据(从后端接口获取)
|
||||||
|
const recommendMovies = ref([])
|
||||||
|
const loadingRecommend = ref(false)
|
||||||
|
|
||||||
|
// 海报图片加载状态
|
||||||
|
const heroImageLoaded = ref(false)
|
||||||
|
let heroImagePreloadTimer = null
|
||||||
|
|
||||||
// 当前用户信息
|
// 当前用户信息
|
||||||
const currentUser = reactive({
|
const currentUser = reactive({
|
||||||
username: authSession?.username || '',
|
username: authSession?.username || '',
|
||||||
@ -758,26 +844,69 @@ const posterShowcaseRows = computed(() => {
|
|||||||
const loginLogs = computed(() => rawData.value?.loginLogs || [])
|
const loginLogs = computed(() => rawData.value?.loginLogs || [])
|
||||||
|
|
||||||
const logIntegrity = computed(() => loginLogs.value.length > 0)
|
const logIntegrity = computed(() => loginLogs.value.length > 0)
|
||||||
|
|
||||||
|
// 每日推荐海报池(优先使用后端返回的推荐电影数据)
|
||||||
const posterPool = computed(() => {
|
const posterPool = computed(() => {
|
||||||
const rows = flattenRows(rawData.value?.sheets || [])
|
// 如果有后端返回的推荐电影数据,使用它
|
||||||
return rows.length
|
if (recommendMovies.value && recommendMovies.value.length > 0) {
|
||||||
? rows
|
return recommendMovies.value
|
||||||
: [createRecord({ title: '今日推荐', subtitle: '暂无海报数据', accountName: '默认资源库', owner: '系统', status: '正常' }, '默认分类')]
|
}
|
||||||
|
// 否则使用降级默认数据(包含 posterUrl 字段以确保 buildHeroStyle 能正确判断)
|
||||||
|
return [{
|
||||||
|
id: 'default-poster',
|
||||||
|
title: '每日推荐',
|
||||||
|
subtitle: '暂无新片上映',
|
||||||
|
description: '当前没有可推荐的海报,请稍后再试。',
|
||||||
|
posterUrl: '', // 空字符串会触发 buildHeroStyle 显示默认海报
|
||||||
|
status: '正常',
|
||||||
|
}]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 精选海报(从 posterPool 中取前 3 条)
|
||||||
const featuredPosters = computed(() => {
|
const featuredPosters = computed(() => {
|
||||||
const rows = posterPool.value
|
const rows = posterPool.value
|
||||||
const seed = new Date().toISOString().slice(0, 10)
|
// 如果只有默认数据(1条),直接返回
|
||||||
const startIndex = Array.from(seed).reduce((sum, char) => sum + char.charCodeAt(0), 0) % rows.length
|
if (rows.length <= 1) {
|
||||||
return Array.from({ length: Math.min(3, rows.length) }, (_, index) => rows[(startIndex + index) % rows.length])
|
return rows
|
||||||
|
}
|
||||||
|
// 从后端数据中取前 3 条
|
||||||
|
return rows.slice(0, 3)
|
||||||
})
|
})
|
||||||
const activeFeaturedPoster = computed(() => featuredPosters.value[featuredPosterIndex.value] || featuredPosters.value[0] || createRecord())
|
const activeFeaturedPoster = computed(() => featuredPosters.value[featuredPosterIndex.value] || featuredPosters.value[0] || createRecord())
|
||||||
|
|
||||||
|
// 判断是否有有效的海报数据
|
||||||
|
const hasValidFeaturedPosters = computed(() => {
|
||||||
|
return featuredPosters.value.length > 0 && featuredPosters.value.some(p => p.posterUrl && p.posterUrl.trim() !== '')
|
||||||
|
})
|
||||||
|
|
||||||
function buildHeroStyle(poster) {
|
function buildHeroStyle(poster) {
|
||||||
|
// 当没有有效海报数据时,使用"暂无新片上映"默认海报
|
||||||
|
if (!poster || poster.trim() === '') {
|
||||||
|
return {
|
||||||
|
backgroundImage: `url('/暂无新片上映海报生成.png')`,
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
backgroundImage: `linear-gradient(180deg, rgba(8, 15, 29, 0.1), rgba(8, 15, 29, 0.82)), url(${poster})`,
|
backgroundImage: `linear-gradient(180deg, rgba(8, 15, 29, 0.1), rgba(8, 15, 29, 0.82)), url(${poster})`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildSubThumbStyle(poster) {
|
||||||
|
// 当没有有效海报数据时,使用默认海报
|
||||||
|
if (!poster || poster.trim() === '') {
|
||||||
|
return {
|
||||||
|
backgroundImage: `linear-gradient(180deg, rgba(8, 15, 29, 0.3), rgba(8, 15, 29, 0.7)), url('/暂无新片上映海报生成.png')`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
backgroundImage: `linear-gradient(180deg, rgba(8, 15, 29, 0.3), rgba(8, 15, 29, 0.7)), url(${poster})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showPrevFeaturedPoster() {
|
function showPrevFeaturedPoster() {
|
||||||
if (!featuredPosters.value.length) return
|
if (!featuredPosters.value.length) return
|
||||||
featuredPosterIndex.value = (featuredPosterIndex.value - 1 + featuredPosters.value.length) % featuredPosters.value.length
|
featuredPosterIndex.value = (featuredPosterIndex.value - 1 + featuredPosters.value.length) % featuredPosters.value.length
|
||||||
@ -1357,6 +1486,75 @@ function handleClickOutside(event) {
|
|||||||
if (userMenuRef.value && !userMenuRef.value.contains(event.target)) {
|
if (userMenuRef.value && !userMenuRef.value.contains(event.target)) {
|
||||||
showUserMenu.value = false
|
showUserMenu.value = false
|
||||||
}
|
}
|
||||||
|
// 点击外部关闭快捷入口面板
|
||||||
|
if (quickAccessWrapperRef.value && !quickAccessWrapperRef.value.contains(event.target)) {
|
||||||
|
showQuickAccessPanel.value = false
|
||||||
|
clearQuickAccessTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换快捷入口面板显示/隐藏
|
||||||
|
*/
|
||||||
|
function toggleQuickAccessPanel() {
|
||||||
|
showQuickAccessPanel.value = !showQuickAccessPanel.value
|
||||||
|
clearQuickAccessTimers()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标悬浮进入快捷入口区域 - 1秒后显示面板
|
||||||
|
*/
|
||||||
|
function handleQuickAccessEnter() {
|
||||||
|
clearQuickAccessTimers()
|
||||||
|
quickAccessTimer = window.setTimeout(() => {
|
||||||
|
showQuickAccessPanel.value = true
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标悬浮进入面板 - 清除关闭定时器,保持面板显示
|
||||||
|
*/
|
||||||
|
function handleQuickAccessPanelEnter() {
|
||||||
|
clearQuickAccessCloseTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标悬浮离开面板 - 延迟关闭面板
|
||||||
|
*/
|
||||||
|
function handleQuickAccessPanelLeave() {
|
||||||
|
quickAccessCloseTimer = window.setTimeout(() => {
|
||||||
|
showQuickAccessPanel.value = false
|
||||||
|
}, 2000) // 2秒后关闭
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 鼠标离开整个包装器(按钮+面板)- 延迟关闭面板
|
||||||
|
*/
|
||||||
|
function handleQuickAccessWrapperLeave() {
|
||||||
|
// 只有当面板正在显示时才启动关闭定时器
|
||||||
|
if (showQuickAccessPanel.value) {
|
||||||
|
quickAccessCloseTimer = window.setTimeout(() => {
|
||||||
|
showQuickAccessPanel.value = false
|
||||||
|
}, 2000) // 2秒后关闭
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除快捷入口定时器(显示和关闭)
|
||||||
|
*/
|
||||||
|
function clearQuickAccessTimers() {
|
||||||
|
clearQuickAccessTimer()
|
||||||
|
clearQuickAccessCloseTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除快捷入口关闭定时器
|
||||||
|
*/
|
||||||
|
function clearQuickAccessCloseTimer() {
|
||||||
|
if (quickAccessCloseTimer) {
|
||||||
|
window.clearTimeout(quickAccessCloseTimer)
|
||||||
|
quickAccessCloseTimer = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAddRecord() {
|
function openAddRecord() {
|
||||||
@ -1535,6 +1733,25 @@ async function doDeleteNode(node) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换到海报展示视图
|
||||||
|
*/
|
||||||
|
function switchToPoster() {
|
||||||
|
showUserMenu.value = false
|
||||||
|
currentView.value = 'poster'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换海报展示/控制台视图
|
||||||
|
*/
|
||||||
|
function toggleView() {
|
||||||
|
currentView.value = currentView.value === 'poster' ? 'manage' : 'poster'
|
||||||
|
// 如果切换到控制台,需要加载数据
|
||||||
|
if (currentView.value === 'manage') {
|
||||||
|
handleSwitchToManage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换到网盘管理视图
|
* 切换到网盘管理视图
|
||||||
* 每次切换时都从后端重新加载最新的网盘列表数据
|
* 每次切换时都从后端重新加载最新的网盘列表数据
|
||||||
@ -1909,6 +2126,43 @@ async function syncTmdbConfigFromServer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载每日推荐电影(从后端接口获取)
|
||||||
|
*/
|
||||||
|
async function loadRecommendMovies() {
|
||||||
|
loadingRecommend.value = true
|
||||||
|
try {
|
||||||
|
const result = await fetchRecommendMovies()
|
||||||
|
|
||||||
|
if (result?.code === 200 && result?.data?.list && result.data.list.length > 0) {
|
||||||
|
// 将后端返回的数据转换为前端使用的格式
|
||||||
|
recommendMovies.value = result.data.list.map(movie => ({
|
||||||
|
id: movie.id,
|
||||||
|
title: movie.title || '未知标题',
|
||||||
|
subtitle: movie.subtitle || '',
|
||||||
|
description: movie.description || '',
|
||||||
|
posterUrl: movie.posterUrl || '',
|
||||||
|
rating: movie.rating || 0,
|
||||||
|
category: movie.category || '',
|
||||||
|
tags: typeof movie.tags === 'string' ? movie.tags.split(',') : movie.tags || [],
|
||||||
|
createTime: movie.createTime || '',
|
||||||
|
}))
|
||||||
|
console.log(`成功加载 ${recommendMovies.value.length} 条每日推荐`)
|
||||||
|
} else {
|
||||||
|
console.warn('每日推荐返回数据异常', result)
|
||||||
|
// 后端无数据,保持空数组,使用默认海报
|
||||||
|
recommendMovies.value = []
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载每日推荐失败', err)
|
||||||
|
// 接口报错,保持空数组,使用默认海报
|
||||||
|
recommendMovies.value = []
|
||||||
|
setErrorMessage(err?.message || '加载每日推荐失败')
|
||||||
|
} finally {
|
||||||
|
loadingRecommend.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
featuredPosters,
|
featuredPosters,
|
||||||
(value) => {
|
(value) => {
|
||||||
@ -1925,6 +2179,47 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 监听当前选中的海报变化,预加载图片
|
||||||
|
watch(
|
||||||
|
() => activeFeaturedPoster.value?.posterUrl,
|
||||||
|
(newPosterUrl) => {
|
||||||
|
if (heroImagePreloadTimer) clearTimeout(heroImagePreloadTimer)
|
||||||
|
|
||||||
|
// 重置加载状态
|
||||||
|
heroImageLoaded.value = false
|
||||||
|
|
||||||
|
// 如果没有海报URL,立即显示默认图片
|
||||||
|
if (!newPosterUrl || newPosterUrl.trim() === '') {
|
||||||
|
heroImageLoaded.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟一小段时间再加载,避免频繁切换时闪烁
|
||||||
|
heroImagePreloadTimer = setTimeout(() => {
|
||||||
|
preloadImage(newPosterUrl)
|
||||||
|
}, 100)
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
// 预加载图片函数
|
||||||
|
function preloadImage(url) {
|
||||||
|
if (!url || url.trim() === '') {
|
||||||
|
heroImageLoaded.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = new Image()
|
||||||
|
img.onload = () => {
|
||||||
|
heroImageLoaded.value = true
|
||||||
|
}
|
||||||
|
img.onerror = () => {
|
||||||
|
// 图片加载失败,显示默认海报
|
||||||
|
heroImageLoaded.value = true
|
||||||
|
}
|
||||||
|
img.src = url
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => rawData.value?.sheets,
|
() => rawData.value?.sheets,
|
||||||
() => syncActiveSheet(),
|
() => syncActiveSheet(),
|
||||||
@ -1948,7 +2243,15 @@ onMounted(() => {
|
|||||||
window.addEventListener('wangpan:forbidden', handleForbidden)
|
window.addEventListener('wangpan:forbidden', handleForbidden)
|
||||||
window.addEventListener('wangpan:session-expiring', handleSessionExpiring)
|
window.addEventListener('wangpan:session-expiring', handleSessionExpiring)
|
||||||
document.addEventListener('click', handleClickOutside)
|
document.addEventListener('click', handleClickOutside)
|
||||||
loadDashboard()
|
|
||||||
|
// 并行加载仪表盘数据和每日推荐
|
||||||
|
Promise.all([
|
||||||
|
loadDashboard(),
|
||||||
|
loadRecommendMovies(),
|
||||||
|
]).catch(err => {
|
||||||
|
console.error('初始化加载失败', err)
|
||||||
|
})
|
||||||
|
|
||||||
resumeFeaturedAutoplay()
|
resumeFeaturedAutoplay()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -598,6 +598,18 @@ export function uploadIconLogo(id, file) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 每日推荐 API ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询每日推荐电影
|
||||||
|
* @returns {Promise} 返回 { code, data: { list: [...], count: N } }
|
||||||
|
*/
|
||||||
|
export function fetchRecommendMovies() {
|
||||||
|
return request(`${API_BASE_URL}/movies/recommend`, {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 用户管理 API(仅管理员) ====================
|
// ==================== 用户管理 API(仅管理员) ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -132,6 +132,9 @@
|
|||||||
<div class="form-message" role="alert">{{ message }}</div>
|
<div class="form-message" role="alert">{{ message }}</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
<footer class="page-footer">
|
||||||
|
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">Copyright © 2013 - 2026 浙ICP备2024131314号-3</a>
|
||||||
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -295,3 +298,25 @@ onMounted(() => {
|
|||||||
refreshCaptcha()
|
refreshCaptcha()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 8px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-footer a {
|
||||||
|
color: rgba(223, 215, 255, 0.45);
|
||||||
|
font-size: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-footer a:hover {
|
||||||
|
color: rgba(223, 215, 255, 0.8);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -7,17 +7,22 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="field-grid">
|
<div class="field-grid">
|
||||||
|
<label v-if="!localRecord.id">
|
||||||
|
<span>文件名</span>
|
||||||
|
<input v-model.trim="localRecord.title" type="text" placeholder="请输入电影文件名,如 xxx.mp4" autofocus />
|
||||||
|
</label>
|
||||||
|
<template v-else>
|
||||||
<label>
|
<label>
|
||||||
<span>文件名</span>
|
<span>文件名</span>
|
||||||
<input v-model.trim="localRecord.title" type="text" placeholder="请输入电影文件名,如 xxx.mp4" />
|
<input v-model.trim="localRecord.title" type="text" placeholder="请输入电影文件名,如 xxx.mp4" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>原始文件名</span>
|
<span>原始文件名</span>
|
||||||
<input v-model.trim="localRecord.originalName" type="text" placeholder="用于 TMDB 刮削纠错" :disabled="!localRecord.id" />
|
<input v-model.trim="localRecord.originalName" type="text" placeholder="用于 TMDB 刮削纠错" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>所属网盘</span>
|
<span>所属网盘</span>
|
||||||
<select v-model="localRecord.sheetId" :disabled="Boolean(localRecord.id)">
|
<select v-model="localRecord.sheetId">
|
||||||
<option v-for="sheet in sheets" :key="sheet.id" :value="sheet.id">{{ sheet.name }}</option>
|
<option v-for="sheet in sheets" :key="sheet.id" :value="sheet.id">{{ sheet.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@ -27,20 +32,21 @@
|
|||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>副标题</span>
|
<span>副标题</span>
|
||||||
<input v-model.trim="localRecord.subtitle" type="text" placeholder="可选" :disabled="!localRecord.id" />
|
<input v-model.trim="localRecord.subtitle" type="text" placeholder="可选" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>分类标签</span>
|
<span>分类标签</span>
|
||||||
<input v-model.trim="localRecord.category" type="text" placeholder="如 科幻 / 动作" :disabled="!localRecord.id" />
|
<input v-model.trim="localRecord.category" type="text" placeholder="如 科幻 / 动作" />
|
||||||
</label>
|
</label>
|
||||||
<label class="field-span-2">
|
<label class="field-span-2">
|
||||||
<span>标签</span>
|
<span>标签</span>
|
||||||
<input v-model="tagText" type="text" placeholder="多个标签用逗号分隔" :disabled="!localRecord.id" />
|
<input v-model="tagText" type="text" placeholder="多个标签用逗号分隔" />
|
||||||
</label>
|
</label>
|
||||||
<label class="field-span-2">
|
<label class="field-span-2">
|
||||||
<span>描述</span>
|
<span>描述</span>
|
||||||
<textarea v-model.trim="localRecord.description" rows="4" placeholder="可选" :disabled="!localRecord.id"></textarea>
|
<textarea v-model.trim="localRecord.description" rows="4" placeholder="可选"></textarea>
|
||||||
</label>
|
</label>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
|
|||||||
@ -504,6 +504,132 @@ canvas {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 快捷入口按钮 */
|
||||||
|
.quick-access-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0;
|
||||||
|
background: #fff;
|
||||||
|
border: 2px solid var(--line);
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-access-btn:hover {
|
||||||
|
border-color: var(--brand);
|
||||||
|
background: rgba(99, 102, 241, 0.05);
|
||||||
|
color: var(--brand);
|
||||||
|
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-access-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-access-btn__icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 快捷入口按钮激活状态 */
|
||||||
|
.quick-access-btn.active {
|
||||||
|
border-color: var(--brand);
|
||||||
|
background: rgba(99, 102, 241, 0.1);
|
||||||
|
color: var(--brand);
|
||||||
|
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 快捷入口按钮包裹器(相对定位,用于面板定位) */
|
||||||
|
.quick-access-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 快捷入口悬浮面板 */
|
||||||
|
.quick-access-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 12px);
|
||||||
|
right: 0;
|
||||||
|
width: min(520px, calc(100vw - 40px));
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15), 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
padding: 20px;
|
||||||
|
z-index: 10000;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-access-panel__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-access-panel__header h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-access-panel .quick-icons__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-access-panel .quick-icons__empty {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 快捷入口面板过渡动画 */
|
||||||
|
.quick-access-fade-enter-active {
|
||||||
|
animation: quickAccessPanelIn 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-access-fade-leave-active {
|
||||||
|
animation: quickAccessPanelOut 0.2s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes quickAccessPanelIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px) scale(0.96);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes quickAccessPanelOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-8px) scale(0.96);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 个人中心菜单 */
|
/* 个人中心菜单 */
|
||||||
.user-menu {
|
.user-menu {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -759,9 +885,211 @@ canvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero-carousel {
|
.hero-carousel {
|
||||||
|
position: relative;
|
||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================== 主海报横幅 ==================== */
|
||||||
|
.hero-main {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 520px;
|
||||||
|
border-radius: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
/* 图片加载完成的过渡效果 */
|
||||||
|
transition: background-image 0.5s ease, opacity 0.6s ease;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 海报加载状态 - 渐变动画模拟视频加载 */
|
||||||
|
.hero-main.hero-loading {
|
||||||
|
background: linear-gradient(110deg, #1e293b 30%, #334155 50%, #1e293b 70%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: hero-shimmer 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-main.hero-loaded {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hero-shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 海报图片预加载指示器 */
|
||||||
|
.hero-main::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 24px;
|
||||||
|
background: radial-gradient(ellipse at center, transparent 0%, rgba(0, 0, 0, 0.3) 100%);
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-main.hero-loaded::before {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 海报内容层 */
|
||||||
|
.hero-main__overlay {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-main__overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 48px;
|
||||||
|
background: linear-gradient(180deg, rgba(8, 15, 29, 0.05) 0%, rgba(8, 15, 29, 0.75) 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-main__badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
width: fit-content;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-main__info {
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-main__title {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: clamp(28px, 3.2vw, 42px);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-main__desc {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.7;
|
||||||
|
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-main__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-main__meta-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.14);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 上方小海报卡片(悬浮于大海报之上,与每日推荐同级) ==================== */
|
||||||
|
.hero-sub {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(99, 102, 241, 0.3) transparent;
|
||||||
|
max-width: 520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub::-webkit-scrollbar {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(99, 102, 241, 0.3);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当有大海报时,将缩略图定位到大海报上层右侧(右下角) */
|
||||||
|
.hero-sub--above {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
z-index: 20;
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub__card {
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub__card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub__thumb {
|
||||||
|
position: relative;
|
||||||
|
width: 140px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: linear-gradient(135deg, #1e293b, #334155);
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub__card.active .hero-sub__thumb {
|
||||||
|
box-shadow: 0 0 0 2px #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub__card:hover .hero-sub__thumb {
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub__title {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(0deg, rgba(0, 0, 0, 0.75) 0%, transparent 100%);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-slide {
|
.hero-slide {
|
||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -163,27 +163,19 @@ export function mapMovieToRecord(movie = {}, fallbackSheetName = '') {
|
|||||||
return createRecord({
|
return createRecord({
|
||||||
id: movie.id,
|
id: movie.id,
|
||||||
title: movie.title,
|
title: movie.title,
|
||||||
subtitle: movie.subtitle || movie.description || '',
|
subtitle: movie.subtitle || '',
|
||||||
accountName: movie.originalName || movie.minioUrl || '',
|
accountName: movie.resourceLibrary || '',
|
||||||
owner: movie.category || '',
|
owner: movie.maintainer || '',
|
||||||
updatedAt: movie.updateTime || movie.createTime || '',
|
updatedAt: movie.updateTime || '',
|
||||||
status: '正常',
|
status: movie.status === 'deleted' ? '已删除' : '正常',
|
||||||
poster: movie.posterUrl || '',
|
poster: movie.posterUrl || '',
|
||||||
tags: movie.tags,
|
tags: movie.tags,
|
||||||
score: Number(movie.rating) || 0,
|
score: Number(movie.rating) || 0,
|
||||||
sheetName: fallbackSheetName,
|
sheetName: fallbackSheetName,
|
||||||
parentId: movie.folderId || null,
|
parentId: movie.folderId || null,
|
||||||
type: 'file',
|
type: 'file',
|
||||||
originalName: movie.originalName,
|
|
||||||
description: movie.description,
|
|
||||||
category: movie.category,
|
category: movie.category,
|
||||||
driveId: movie.driveId,
|
description: movie.description || '',
|
||||||
folderId: movie.folderId,
|
|
||||||
minioUrl: movie.minioUrl,
|
|
||||||
mimeType: movie.mimeType,
|
|
||||||
extension: movie.extension,
|
|
||||||
fileSize: movie.fileSize,
|
|
||||||
createdAt: movie.createTime,
|
|
||||||
}, fallbackSheetName)
|
}, fallbackSheetName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export default defineConfig({
|
|||||||
strictPort: true, // ✅ 强制使用指定端口,如果被占用则报错而不是自动切换
|
strictPort: true, // ✅ 强制使用指定端口,如果被占用则报错而不是自动切换
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://192.168.1.18:8888',
|
target: 'http://localhost:8888',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user