添加快捷入口面板和每日推荐功能,更新样式和API

This commit is contained in:
liRQ 2026-06-24 17:44:24 +08:00
parent 7e0f347c9a
commit bacbe1871c
8 changed files with 753 additions and 87 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

@ -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()
}) })

View File

@ -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仅管理员 ====================
/** /**

View File

@ -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>

View File

@ -7,40 +7,46 @@
</header> </header>
<div class="field-grid"> <div class="field-grid">
<label> <label v-if="!localRecord.id">
<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" autofocus />
</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>
</label> </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> </div>
<footer> <footer>

View File

@ -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;
} }

View File

@ -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)
} }

View File

@ -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,
}, },