添加快捷入口面板和每日推荐功能,更新样式和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>
|
||||
</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">
|
||||
<button type="button" class="user-menu__trigger" @click="showUserMenu = !showUserMenu">
|
||||
@ -77,6 +144,14 @@
|
||||
</button>
|
||||
</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">
|
||||
<span class="user-menu__icon">⚙️</span>
|
||||
<span>TMDB 配置</span>
|
||||
@ -103,33 +178,17 @@
|
||||
<strong>{{ currentView === 'manage' ? '控制台' : '海报展示' }}</strong>
|
||||
<span>{{ currentView === 'manage' ? '管理网盘资源与账号数据' : '根据网盘内容进行 TMDB 海报刮削展示' }}</span>
|
||||
</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>
|
||||
|
||||
<section v-if="currentView === 'poster' && !hasSearched && !isSearchFocused" class="hero-carousel" aria-label="每日推荐海报">
|
||||
<!-- 主海报横幅 -->
|
||||
<Transition name="hero-fade" mode="out-in">
|
||||
<article
|
||||
v-if="featuredPosters.length"
|
||||
:key="activeFeaturedPoster.id"
|
||||
class="hero-slide hero-slide--single"
|
||||
:style="buildHeroStyle(activeFeaturedPoster.poster)"
|
||||
class="hero-main"
|
||||
:class="{ 'hero-loading': !heroImageLoaded, 'hero-loaded': heroImageLoaded }"
|
||||
:style="buildHeroStyle(activeFeaturedPoster.posterUrl)"
|
||||
tabindex="0"
|
||||
@mouseenter="pauseFeaturedAutoplay"
|
||||
@mouseleave="resumeFeaturedAutoplay"
|
||||
@ -138,22 +197,35 @@
|
||||
@keydown.left.prevent="showPrevFeaturedPoster"
|
||||
@keydown.right.prevent="showNextFeaturedPoster"
|
||||
>
|
||||
<div class="hero-slide__overlay">
|
||||
<div class="hero-slide__topbar">
|
||||
<span class="hero-slide__badge">每日推荐</span>
|
||||
</div>
|
||||
<div class="hero-slide__content">
|
||||
<h2>{{ activeFeaturedPoster.title }}</h2>
|
||||
<p>{{ activeFeaturedPoster.subtitle || activeFeaturedPoster.description || '根据网盘资源内容自动匹配海报展示。' }}</p>
|
||||
<div v-if="activeFeaturedPoster.accountName || activeFeaturedPoster.owner || activeFeaturedPoster.status" class="hero-slide__meta">
|
||||
<span v-if="activeFeaturedPoster.accountName">{{ activeFeaturedPoster.accountName }}</span>
|
||||
<span v-if="activeFeaturedPoster.owner">{{ activeFeaturedPoster.owner }}</span>
|
||||
<span v-if="activeFeaturedPoster.status">{{ activeFeaturedPoster.status }}</span>
|
||||
<div class="hero-main__overlay">
|
||||
<div class="hero-main__badge">每日推荐</div>
|
||||
<div class="hero-main__info">
|
||||
<h2 class="hero-main__title">{{ activeFeaturedPoster.title }}</h2>
|
||||
<p class="hero-main__desc">{{ activeFeaturedPoster.subtitle || activeFeaturedPoster.description || '根据网盘资源内容自动匹配海报展示。' }}</p>
|
||||
<div class="hero-main__meta">
|
||||
<span v-if="activeFeaturedPoster.accountName" class="hero-main__meta-item">📁 {{ activeFeaturedPoster.accountName }}</span>
|
||||
<span v-if="activeFeaturedPoster.owner" class="hero-main__meta-item">👤 {{ activeFeaturedPoster.owner }}</span>
|
||||
<span v-if="activeFeaturedPoster.status" class="hero-main__meta-item">{{ activeFeaturedPoster.status === '已删除' ? '🗑️' : '✅' }} {{ activeFeaturedPoster.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</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 v-if="isLoading" class="loading-panel">
|
||||
@ -177,7 +249,7 @@
|
||||
|
||||
<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">
|
||||
<h3>快捷入口</h3>
|
||||
<div class="quick-icons__actions">
|
||||
@ -591,6 +663,7 @@ import {
|
||||
fetchIconList,
|
||||
fetchLoginLogs,
|
||||
fetchMovieDuplicates,
|
||||
fetchRecommendMovies,
|
||||
fetchTmdbConfig,
|
||||
requestState,
|
||||
resetData,
|
||||
@ -630,6 +703,7 @@ const showFolderDialog = ref(false)
|
||||
const editingFolder = ref(null) // 编辑时传入的文件夹对象,null 表示新建
|
||||
const folderDialogParentId = ref(0) // 新建时的父文件夹ID
|
||||
const folderTreeContainerRef = ref(null) // 文件夹树容器引用(用于保持滚动位置)
|
||||
const quickIconsRef = ref(null) // 快捷入口区域引用(用于滚动定位)
|
||||
const expandedFolderIds = reactive(new Set()) // 全局展开状态追踪,防止 buildTree 重建丢失展开
|
||||
|
||||
// 确认弹窗状态
|
||||
@ -667,6 +741,10 @@ const iconList = ref([]) // 快捷图标列表
|
||||
const networkMode = ref('external') // 网络模式:'external' | 'internal'
|
||||
const showUserMenu = ref(false)
|
||||
const userMenuRef = ref(null)
|
||||
const showQuickAccessPanel = ref(false)
|
||||
const quickAccessWrapperRef = ref(null)
|
||||
let quickAccessTimer = null // 悬浮延迟定时器(显示面板)
|
||||
let quickAccessCloseTimer = null // 面板关闭延迟定时器
|
||||
const editingRecord = ref(null)
|
||||
const featuredPosterIndex = ref(0)
|
||||
|
||||
@ -715,6 +793,14 @@ let featuredAutoplayTimer = 0
|
||||
const successMessage = ref('')
|
||||
const errorMessage = ref('')
|
||||
|
||||
// 每日推荐电影数据(从后端接口获取)
|
||||
const recommendMovies = ref([])
|
||||
const loadingRecommend = ref(false)
|
||||
|
||||
// 海报图片加载状态
|
||||
const heroImageLoaded = ref(false)
|
||||
let heroImagePreloadTimer = null
|
||||
|
||||
// 当前用户信息
|
||||
const currentUser = reactive({
|
||||
username: authSession?.username || '',
|
||||
@ -758,26 +844,69 @@ const posterShowcaseRows = computed(() => {
|
||||
const loginLogs = computed(() => rawData.value?.loginLogs || [])
|
||||
|
||||
const logIntegrity = computed(() => loginLogs.value.length > 0)
|
||||
|
||||
// 每日推荐海报池(优先使用后端返回的推荐电影数据)
|
||||
const posterPool = computed(() => {
|
||||
const rows = flattenRows(rawData.value?.sheets || [])
|
||||
return rows.length
|
||||
? rows
|
||||
: [createRecord({ title: '今日推荐', subtitle: '暂无海报数据', accountName: '默认资源库', owner: '系统', status: '正常' }, '默认分类')]
|
||||
// 如果有后端返回的推荐电影数据,使用它
|
||||
if (recommendMovies.value && recommendMovies.value.length > 0) {
|
||||
return recommendMovies.value
|
||||
}
|
||||
// 否则使用降级默认数据(包含 posterUrl 字段以确保 buildHeroStyle 能正确判断)
|
||||
return [{
|
||||
id: 'default-poster',
|
||||
title: '每日推荐',
|
||||
subtitle: '暂无新片上映',
|
||||
description: '当前没有可推荐的海报,请稍后再试。',
|
||||
posterUrl: '', // 空字符串会触发 buildHeroStyle 显示默认海报
|
||||
status: '正常',
|
||||
}]
|
||||
})
|
||||
|
||||
// 精选海报(从 posterPool 中取前 3 条)
|
||||
const featuredPosters = computed(() => {
|
||||
const rows = posterPool.value
|
||||
const seed = new Date().toISOString().slice(0, 10)
|
||||
const startIndex = Array.from(seed).reduce((sum, char) => sum + char.charCodeAt(0), 0) % rows.length
|
||||
return Array.from({ length: Math.min(3, rows.length) }, (_, index) => rows[(startIndex + index) % rows.length])
|
||||
// 如果只有默认数据(1条),直接返回
|
||||
if (rows.length <= 1) {
|
||||
return rows
|
||||
}
|
||||
// 从后端数据中取前 3 条
|
||||
return rows.slice(0, 3)
|
||||
})
|
||||
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) {
|
||||
// 当没有有效海报数据时,使用"暂无新片上映"默认海报
|
||||
if (!poster || poster.trim() === '') {
|
||||
return {
|
||||
backgroundImage: `url('/暂无新片上映海报生成.png')`,
|
||||
}
|
||||
}
|
||||
return {
|
||||
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() {
|
||||
if (!featuredPosters.value.length) return
|
||||
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)) {
|
||||
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() {
|
||||
@ -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(
|
||||
featuredPosters,
|
||||
(value) => {
|
||||
@ -1925,6 +2179,47 @@ watch(
|
||||
{ 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(
|
||||
() => rawData.value?.sheets,
|
||||
() => syncActiveSheet(),
|
||||
@ -1948,7 +2243,15 @@ onMounted(() => {
|
||||
window.addEventListener('wangpan:forbidden', handleForbidden)
|
||||
window.addEventListener('wangpan:session-expiring', handleSessionExpiring)
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
loadDashboard()
|
||||
|
||||
// 并行加载仪表盘数据和每日推荐
|
||||
Promise.all([
|
||||
loadDashboard(),
|
||||
loadRecommendMovies(),
|
||||
]).catch(err => {
|
||||
console.error('初始化加载失败', err)
|
||||
})
|
||||
|
||||
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(仅管理员) ====================
|
||||
|
||||
/**
|
||||
|
||||
@ -132,6 +132,9 @@
|
||||
<div class="form-message" role="alert">{{ message }}</div>
|
||||
</form>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@ -295,3 +298,25 @@ onMounted(() => {
|
||||
refreshCaptcha()
|
||||
})
|
||||
</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,40 +7,46 @@
|
||||
</header>
|
||||
|
||||
<div class="field-grid">
|
||||
<label>
|
||||
<label v-if="!localRecord.id">
|
||||
<span>文件名</span>
|
||||
<input v-model.trim="localRecord.title" type="text" placeholder="请输入电影文件名,如 xxx.mp4" />
|
||||
</label>
|
||||
<label>
|
||||
<span>原始文件名</span>
|
||||
<input v-model.trim="localRecord.originalName" type="text" placeholder="用于 TMDB 刮削纠错" :disabled="!localRecord.id" />
|
||||
</label>
|
||||
<label>
|
||||
<span>所属网盘</span>
|
||||
<select v-model="localRecord.sheetId" :disabled="Boolean(localRecord.id)">
|
||||
<option v-for="sheet in sheets" :key="sheet.id" :value="sheet.id">{{ sheet.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>评分</span>
|
||||
<input v-model.number="localRecord.score" type="number" min="0" max="100" placeholder="0-100" />
|
||||
</label>
|
||||
<label>
|
||||
<span>副标题</span>
|
||||
<input v-model.trim="localRecord.subtitle" type="text" placeholder="可选" :disabled="!localRecord.id" />
|
||||
</label>
|
||||
<label>
|
||||
<span>分类标签</span>
|
||||
<input v-model.trim="localRecord.category" type="text" placeholder="如 科幻 / 动作" :disabled="!localRecord.id" />
|
||||
</label>
|
||||
<label class="field-span-2">
|
||||
<span>标签</span>
|
||||
<input v-model="tagText" type="text" placeholder="多个标签用逗号分隔" :disabled="!localRecord.id" />
|
||||
</label>
|
||||
<label class="field-span-2">
|
||||
<span>描述</span>
|
||||
<textarea v-model.trim="localRecord.description" rows="4" placeholder="可选" :disabled="!localRecord.id"></textarea>
|
||||
<input v-model.trim="localRecord.title" type="text" placeholder="请输入电影文件名,如 xxx.mp4" autofocus />
|
||||
</label>
|
||||
<template v-else>
|
||||
<label>
|
||||
<span>文件名</span>
|
||||
<input v-model.trim="localRecord.title" type="text" placeholder="请输入电影文件名,如 xxx.mp4" />
|
||||
</label>
|
||||
<label>
|
||||
<span>原始文件名</span>
|
||||
<input v-model.trim="localRecord.originalName" type="text" placeholder="用于 TMDB 刮削纠错" />
|
||||
</label>
|
||||
<label>
|
||||
<span>所属网盘</span>
|
||||
<select v-model="localRecord.sheetId">
|
||||
<option v-for="sheet in sheets" :key="sheet.id" :value="sheet.id">{{ sheet.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>评分</span>
|
||||
<input v-model.number="localRecord.score" type="number" min="0" max="100" placeholder="0-100" />
|
||||
</label>
|
||||
<label>
|
||||
<span>副标题</span>
|
||||
<input v-model.trim="localRecord.subtitle" type="text" placeholder="可选" />
|
||||
</label>
|
||||
<label>
|
||||
<span>分类标签</span>
|
||||
<input v-model.trim="localRecord.category" type="text" placeholder="如 科幻 / 动作" />
|
||||
</label>
|
||||
<label class="field-span-2">
|
||||
<span>标签</span>
|
||||
<input v-model="tagText" type="text" placeholder="多个标签用逗号分隔" />
|
||||
</label>
|
||||
<label class="field-span-2">
|
||||
<span>描述</span>
|
||||
<textarea v-model.trim="localRecord.description" rows="4" placeholder="可选"></textarea>
|
||||
</label>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
|
||||
@ -504,6 +504,132 @@ canvas {
|
||||
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 {
|
||||
position: relative;
|
||||
@ -759,9 +885,211 @@ canvas {
|
||||
}
|
||||
|
||||
.hero-carousel {
|
||||
position: relative;
|
||||
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 {
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
@ -163,27 +163,19 @@ export function mapMovieToRecord(movie = {}, fallbackSheetName = '') {
|
||||
return createRecord({
|
||||
id: movie.id,
|
||||
title: movie.title,
|
||||
subtitle: movie.subtitle || movie.description || '',
|
||||
accountName: movie.originalName || movie.minioUrl || '',
|
||||
owner: movie.category || '',
|
||||
updatedAt: movie.updateTime || movie.createTime || '',
|
||||
status: '正常',
|
||||
subtitle: movie.subtitle || '',
|
||||
accountName: movie.resourceLibrary || '',
|
||||
owner: movie.maintainer || '',
|
||||
updatedAt: movie.updateTime || '',
|
||||
status: movie.status === 'deleted' ? '已删除' : '正常',
|
||||
poster: movie.posterUrl || '',
|
||||
tags: movie.tags,
|
||||
score: Number(movie.rating) || 0,
|
||||
sheetName: fallbackSheetName,
|
||||
parentId: movie.folderId || null,
|
||||
type: 'file',
|
||||
originalName: movie.originalName,
|
||||
description: movie.description,
|
||||
category: movie.category,
|
||||
driveId: movie.driveId,
|
||||
folderId: movie.folderId,
|
||||
minioUrl: movie.minioUrl,
|
||||
mimeType: movie.mimeType,
|
||||
extension: movie.extension,
|
||||
fileSize: movie.fileSize,
|
||||
createdAt: movie.createTime,
|
||||
description: movie.description || '',
|
||||
}, fallbackSheetName)
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ export default defineConfig({
|
||||
strictPort: true, // ✅ 强制使用指定端口,如果被占用则报错而不是自动切换
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://192.168.1.18:8888',
|
||||
target: 'http://localhost:8888',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user