功能:新增设置页面和海报卡片的导演、主演及简介展示,优化海报加载样式
This commit is contained in:
parent
a9066b9046
commit
3fb7f45789
148
vue/src/App.vue
148
vue/src/App.vue
@ -191,6 +191,10 @@
|
|||||||
<span class="user-menu__icon">⚙️</span>
|
<span class="user-menu__icon">⚙️</span>
|
||||||
<span>TMDB 配置</span>
|
<span>TMDB 配置</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="user-menu__item" @click="handleSettings">
|
||||||
|
<span class="user-menu__icon">🛠️</span>
|
||||||
|
<span>设置</span>
|
||||||
|
</button>
|
||||||
<button type="button" class="user-menu__item" @click="handleExport">
|
<button type="button" class="user-menu__item" @click="handleExport">
|
||||||
<span class="user-menu__icon">📤</span>
|
<span class="user-menu__icon">📤</span>
|
||||||
<span>导出数据</span>
|
<span>导出数据</span>
|
||||||
@ -222,8 +226,12 @@
|
|||||||
v-if="featuredPosters.length"
|
v-if="featuredPosters.length"
|
||||||
:key="activeFeaturedPoster.id"
|
:key="activeFeaturedPoster.id"
|
||||||
class="hero-main"
|
class="hero-main"
|
||||||
:class="{ 'hero-loading': !heroImageLoaded, 'hero-loaded': heroImageLoaded }"
|
:class="{
|
||||||
:style="buildHeroStyle(activeFeaturedPoster.posterUrl)"
|
'hero-loading': !heroImageLoaded,
|
||||||
|
'hero-loaded': heroImageLoaded,
|
||||||
|
'hero-wide': isWidePoster(activeFeaturedPoster.posterUrl)
|
||||||
|
}"
|
||||||
|
:style="buildHeroStyle(activeFeaturedPoster)"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@mouseenter="pauseFeaturedAutoplay"
|
@mouseenter="pauseFeaturedAutoplay"
|
||||||
@mouseleave="resumeFeaturedAutoplay"
|
@mouseleave="resumeFeaturedAutoplay"
|
||||||
@ -232,6 +240,15 @@
|
|||||||
@keydown.left.prevent="showPrevFeaturedPoster"
|
@keydown.left.prevent="showPrevFeaturedPoster"
|
||||||
@keydown.right.prevent="showNextFeaturedPoster"
|
@keydown.right.prevent="showNextFeaturedPoster"
|
||||||
>
|
>
|
||||||
|
<!-- 海报图片层 -->
|
||||||
|
<img
|
||||||
|
v-if="activeFeaturedPoster.posterUrl"
|
||||||
|
:src="activeFeaturedPoster.posterUrl"
|
||||||
|
:alt="activeFeaturedPoster.title"
|
||||||
|
class="hero-bg-image"
|
||||||
|
@load="heroImageLoaded = true"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="hero-main__overlay">
|
<div class="hero-main__overlay">
|
||||||
<div class="hero-main__badge">每日推荐</div>
|
<div class="hero-main__badge">每日推荐</div>
|
||||||
<div class="hero-main__info">
|
<div class="hero-main__info">
|
||||||
@ -541,8 +558,11 @@
|
|||||||
<p>根据网盘管理中的标题与海报地址进行 TMDB 匹配展示,可作为刮削结果预览。</p>
|
<p>根据网盘管理中的标题与海报地址进行 TMDB 匹配展示,可作为刮削结果预览。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="poster-list poster-list--showcase">
|
<div v-if="loadingPosterShowcase" class="poster-list-loading">
|
||||||
<div v-for="item in posterShowcaseRows" :key="item.id" class="poster-card-wrap">
|
正在加载海报数据...
|
||||||
|
</div>
|
||||||
|
<div v-else class="poster-list poster-list--showcase">
|
||||||
|
<div v-for="item in posterShowcaseData" :key="item.id" class="poster-card-wrap">
|
||||||
<PosterCard :item="item" :readonly="true" />
|
<PosterCard :item="item" :readonly="true" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -556,14 +576,14 @@
|
|||||||
<div class="content-toolbar">
|
<div class="content-toolbar">
|
||||||
<div>
|
<div>
|
||||||
<h2>搜索结果</h2>
|
<h2>搜索结果</h2>
|
||||||
<p>找到 {{ posterShowcaseRows.length }} 个匹配“{{ searchKeyword }}”的结果</p>
|
<p>找到 {{ filteredPosterShowcaseData.length }} 个匹配“{{ searchKeyword }}”的结果</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="secondary" @click="hasSearched = false; searchKeyword = ''">
|
<button type="button" class="secondary" @click="hasSearched = false; searchKeyword = ''">
|
||||||
清空搜索
|
清空搜索
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="posterShowcaseRows.length > 0" class="poster-list poster-list--showcase">
|
<div v-if="filteredPosterShowcaseData.length > 0" class="poster-list poster-list--showcase">
|
||||||
<div v-for="item in posterShowcaseRows" :key="item.id" class="poster-card-wrap">
|
<div v-for="item in filteredPosterShowcaseData" :key="item.id" class="poster-card-wrap">
|
||||||
<PosterCard :item="item" :readonly="true" />
|
<PosterCard :item="item" :readonly="true" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -816,6 +836,16 @@
|
|||||||
@cancel="showConfirmDialog = false"
|
@cancel="showConfirmDialog = false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 设置页面 -->
|
||||||
|
<Transition name="fade">
|
||||||
|
<div v-if="showSettingsView" class="settings-overlay" @click.self="showSettingsView = false">
|
||||||
|
<div class="settings-container">
|
||||||
|
<button type="button" class="settings-close" @click="showSettingsView = false" title="关闭设置">✕</button>
|
||||||
|
<SettingsView />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<!-- 网盘标签页悬浮描述提示框(Teleport到body,避免被父容器overflow裁剪) -->
|
<!-- 网盘标签页悬浮描述提示框(Teleport到body,避免被父容器overflow裁剪) -->
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="tooltip-fade">
|
<Transition name="tooltip-fade">
|
||||||
@ -848,6 +878,7 @@ import UserManageDialog from './components/UserManageDialog.vue'
|
|||||||
import DriveManageDialog from './components/DriveManageDialog.vue'
|
import DriveManageDialog from './components/DriveManageDialog.vue'
|
||||||
import ConfirmDialog from './components/ConfirmDialog.vue'
|
import ConfirmDialog from './components/ConfirmDialog.vue'
|
||||||
import IconManageDialog from './components/IconManageDialog.vue'
|
import IconManageDialog from './components/IconManageDialog.vue'
|
||||||
|
import SettingsView from './components/SettingsView.vue'
|
||||||
import {
|
import {
|
||||||
batchScrapeAll,
|
batchScrapeAll,
|
||||||
batchScrapeMovies,
|
batchScrapeMovies,
|
||||||
@ -859,6 +890,7 @@ import {
|
|||||||
deleteMovieInDrive,
|
deleteMovieInDrive,
|
||||||
deleteRecord,
|
deleteRecord,
|
||||||
exportData,
|
exportData,
|
||||||
|
fetchAllMovies,
|
||||||
fetchBatchScrapeStatus,
|
fetchBatchScrapeStatus,
|
||||||
fetchDashboardData,
|
fetchDashboardData,
|
||||||
fetchDriveList,
|
fetchDriveList,
|
||||||
@ -1055,15 +1087,48 @@ const folderTreeData = computed(() => {
|
|||||||
return buildTree(rows)
|
return buildTree(rows)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 海报展示数据(只包含文件类型,排除文件夹,显示所有网盘的文件)
|
// 海报展示数据(从后端获取,支持排序和分页)
|
||||||
const posterShowcaseRows = computed(() => {
|
const posterShowcaseData = ref([]) // 从后端获取的海报展示数据
|
||||||
// 获取所有网盘的所有行
|
const loadingPosterShowcase = ref(false) // 海报展示加载状态
|
||||||
const allSheetsRows = flattenRows(rawData.value?.sheets || [])
|
|
||||||
// 根据搜索关键词过滤
|
// 过滤后的海报展示数据(用于搜索)
|
||||||
const filtered = filterRows(allSheetsRows, searchKeyword.value)
|
const filteredPosterShowcaseData = computed(() => {
|
||||||
// 只保留文件类型,排除文件夹
|
if (!searchKeyword.value.trim()) {
|
||||||
return filtered.filter(item => item.type !== 'folder')
|
return posterShowcaseData.value
|
||||||
|
}
|
||||||
|
const keyword = searchKeyword.value.toLowerCase()
|
||||||
|
return posterShowcaseData.value.filter(item => {
|
||||||
|
const title = (item.title || '').toLowerCase()
|
||||||
|
const originalName = (item.originalName || '').toLowerCase()
|
||||||
|
const description = (item.description || '').toLowerCase()
|
||||||
|
return title.includes(keyword) || originalName.includes(keyword) || description.includes(keyword)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载海报展示数据(从后端获取所有电影)
|
||||||
|
* @param {string} sortBy - 排序字段:createTime/updateTime/rating/title
|
||||||
|
* @param {string} sortOrder - 排序方式:asc/desc
|
||||||
|
*/
|
||||||
|
async function loadPosterShowcase(sortBy = 'updateTime', sortOrder = 'desc') {
|
||||||
|
loadingPosterShowcase.value = true
|
||||||
|
try {
|
||||||
|
const result = await fetchAllMovies({ pageNo: 1, pageSize: 50, sortBy, sortOrder })
|
||||||
|
if (result?.code === 200 && Array.isArray(result?.data?.list)) {
|
||||||
|
posterShowcaseData.value = result.data.list
|
||||||
|
console.log(`海报展示加载成功,共 ${result.data.list.length} 条`)
|
||||||
|
} else {
|
||||||
|
console.warn('海报展示返回数据异常', result)
|
||||||
|
posterShowcaseData.value = []
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载海报展示失败', err)
|
||||||
|
posterShowcaseData.value = []
|
||||||
|
} finally {
|
||||||
|
loadingPosterShowcase.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
@ -1102,16 +1167,36 @@ const hasValidFeaturedPosters = computed(() => {
|
|||||||
return featuredPosters.value.length > 0 && featuredPosters.value.some(p => p.posterUrl && p.posterUrl.trim() !== '')
|
return featuredPosters.value.length > 0 && featuredPosters.value.some(p => p.posterUrl && p.posterUrl.trim() !== '')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断海报是否为横向图片(基于 URL 特征或宽高比)
|
||||||
|
* @param {string|object} poster - 海报 URL 或海报对象
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isWidePoster(poster) {
|
||||||
|
// 如果传入的是对象,取 posterUrl
|
||||||
|
const url = typeof poster === 'object' ? poster?.posterUrl : poster
|
||||||
|
|
||||||
|
// 如果 URL 为空,不是横向海报
|
||||||
|
if (!url || url.trim() === '') return false
|
||||||
|
|
||||||
|
// 如果 URL 包含 backdrop 关键字,通常是横向背景图
|
||||||
|
if (url.toLowerCase().includes('backdrop')) return true
|
||||||
|
|
||||||
|
// 如果 URL 包含 wide、horizontal、landscape 等关键字
|
||||||
|
if (url.toLowerCase().match(/wide|horizontal|landscape|banner/)) return true
|
||||||
|
|
||||||
|
// 默认按竖向海报处理
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建大海报样式(不再使用 background-image,改用 img 标签)
|
||||||
|
* @param {object} poster - 海报对象 { id, title, posterUrl, ... }
|
||||||
|
* @returns {object} Vue 样式对象
|
||||||
|
*/
|
||||||
function buildHeroStyle(poster) {
|
function buildHeroStyle(poster) {
|
||||||
// 当没有有效海报数据时,使用"暂无新片上映"默认海报
|
// 返回空对象,因为图片现在由 <img> 标签处理
|
||||||
if (!poster || poster.trim() === '') {
|
return {}
|
||||||
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) {
|
function buildSubThumbStyle(poster) {
|
||||||
@ -1829,6 +1914,14 @@ function handleTmdbConfig() {
|
|||||||
showTmdbDialog.value = true
|
showTmdbDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置页面
|
||||||
|
const showSettingsView = ref(false)
|
||||||
|
|
||||||
|
function handleSettings() {
|
||||||
|
showUserMenu.value = false
|
||||||
|
showSettingsView.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 修改头像
|
// 修改头像
|
||||||
function handleChangeAvatar() {
|
function handleChangeAvatar() {
|
||||||
showUserMenu.value = false
|
showUserMenu.value = false
|
||||||
@ -2111,9 +2204,11 @@ async function doDeleteNode(node) {
|
|||||||
/**
|
/**
|
||||||
* 切换到海报展示视图
|
* 切换到海报展示视图
|
||||||
*/
|
*/
|
||||||
function switchToPoster() {
|
async function switchToPoster() {
|
||||||
showUserMenu.value = false
|
showUserMenu.value = false
|
||||||
currentView.value = 'poster'
|
currentView.value = 'poster'
|
||||||
|
// 从后端加载海报展示数据
|
||||||
|
await loadPosterShowcase()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2951,10 +3046,11 @@ onMounted(() => {
|
|||||||
window.addEventListener('wangpan:session-expiring', handleSessionExpiring)
|
window.addEventListener('wangpan:session-expiring', handleSessionExpiring)
|
||||||
document.addEventListener('click', handleClickOutside)
|
document.addEventListener('click', handleClickOutside)
|
||||||
|
|
||||||
// 并行加载仪表盘数据和每日推荐
|
// 并行加载仪表盘数据、每日推荐和海报展示数据
|
||||||
Promise.all([
|
Promise.all([
|
||||||
loadDashboard(),
|
loadDashboard(),
|
||||||
loadRecommendMovies(),
|
loadRecommendMovies(),
|
||||||
|
loadPosterShowcase(),
|
||||||
]).catch(err => {
|
]).catch(err => {
|
||||||
console.error('初始化加载失败', err)
|
console.error('初始化加载失败', err)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -475,7 +475,7 @@ export function createMovieInDrive(driveId, payload) {
|
|||||||
* 更新电影信息
|
* 更新电影信息
|
||||||
* @param {string|number} driveId - 网盘ID
|
* @param {string|number} driveId - 网盘ID
|
||||||
* @param {string|number} movieId - 电影ID
|
* @param {string|number} movieId - 电影ID
|
||||||
* @param {Object} payload - 可选字段:title, originalName, subtitle, description, rating, tags, category
|
* @param {Object} payload - 可选字段:title, originalName, subtitle, description, rating, tags, category, directors, casts
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
export function updateMovieInDrive(driveId, movieId, payload) {
|
export function updateMovieInDrive(driveId, movieId, payload) {
|
||||||
@ -487,6 +487,8 @@ export function updateMovieInDrive(driveId, movieId, payload) {
|
|||||||
if (payload.rating !== undefined) body.rating = payload.rating
|
if (payload.rating !== undefined) body.rating = payload.rating
|
||||||
if (payload.tags !== undefined) body.tags = payload.tags
|
if (payload.tags !== undefined) body.tags = payload.tags
|
||||||
if (payload.category !== undefined) body.category = payload.category
|
if (payload.category !== undefined) body.category = payload.category
|
||||||
|
if (payload.directors !== undefined) body.directors = payload.directors
|
||||||
|
if (payload.casts !== undefined) body.casts = payload.casts
|
||||||
|
|
||||||
return request(`${API_BASE_URL}/drive/${driveId}/movies/${movieId}`, {
|
return request(`${API_BASE_URL}/drive/${driveId}/movies/${movieId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@ -720,6 +722,30 @@ export function fetchScraperLogs(params = {}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 获取所有电影(海报展示用) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有电影列表(用于海报展示页)
|
||||||
|
* @param {Object} params - { pageNo, pageSize, sortBy, sortOrder }
|
||||||
|
* @param {number} [params.pageNo=1] - 页码
|
||||||
|
* @param {number} [params.pageSize=50] - 每页条数,最多50
|
||||||
|
* @param {string} [params.sortBy=createTime] - 排序字段:createTime/updateTime/rating/title
|
||||||
|
* @param {string} [params.sortOrder=desc] - 排序方式:asc/desc
|
||||||
|
* @returns {Promise} 返回格式:{ code: 200, data: { total, list: [{ id, title, posterUrl, ... }], pageNo, pageSize } }
|
||||||
|
*/
|
||||||
|
export function fetchAllMovies(params = {}) {
|
||||||
|
const searchParams = new URLSearchParams()
|
||||||
|
if (params.pageNo) searchParams.set('pageNo', String(params.pageNo))
|
||||||
|
if (params.pageSize) searchParams.set('pageSize', String(Math.min(params.pageSize, 50))) // 最多50条
|
||||||
|
if (params.sortBy) searchParams.set('sortBy', params.sortBy)
|
||||||
|
if (params.sortOrder) searchParams.set('sortOrder', params.sortOrder)
|
||||||
|
|
||||||
|
const query = searchParams.toString()
|
||||||
|
return request(`${API_BASE_URL}/movies/all${query ? `?${query}` : ''}`, {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 批量刮削 API ====================
|
// ==================== 批量刮削 API ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -804,3 +830,51 @@ export async function fetchBatchScrapeStatus(movieIds) {
|
|||||||
data: results.filter(Boolean),
|
data: results.filter(Boolean),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== OpenList 绑定 API ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 OpenList 绑定信息
|
||||||
|
* @returns {Promise} 返回格式:{ code: 200, data: { id, openlistUrl, openlistUsername, storageId, mountPath, status, tokenValid, createTime, updateTime } } 或 null
|
||||||
|
*/
|
||||||
|
export function getOpenListInfo() {
|
||||||
|
return request(`${API_BASE_URL}/openlist/info`, {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定 OpenList 后端接口
|
||||||
|
* @param {Object} payload - { url, username, password }
|
||||||
|
* @returns {Promise} 返回格式:{ code: 200, message: "绑定成功" }
|
||||||
|
*/
|
||||||
|
export function bindOpenList(payload) {
|
||||||
|
return request(`${API_BASE_URL}/openlist/bind`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: payload.url,
|
||||||
|
username: payload.username,
|
||||||
|
password: payload.password,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 OpenList 绑定状态
|
||||||
|
* @returns {Promise} 返回格式:{ code: 200, data: { bound: boolean, url?: string, username?: string, boundAt?: string } }
|
||||||
|
*/
|
||||||
|
export function fetchOpenListStatus() {
|
||||||
|
return request(`${API_BASE_URL}/openlist/status`, {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解除 OpenList 绑定
|
||||||
|
* @returns {Promise} 返回格式:{ code: 200, message: "已解除绑定" }
|
||||||
|
*/
|
||||||
|
export function unbindOpenList() {
|
||||||
|
return request(`${API_BASE_URL}/openlist/unbind`, {
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,31 +1,92 @@
|
|||||||
<template>
|
<template>
|
||||||
<article class="poster-card" :class="{ 'is-readonly': readonly }" @click="handleClick">
|
<article class="poster-card" :class="{ 'is-readonly': readonly }" @click="handleClick">
|
||||||
<div class="poster-thumb">
|
<div class="poster-thumb">
|
||||||
<img :src="item.poster" :alt="item.title" loading="lazy" />
|
<img :src="posterImageUrl" :alt="item.title" loading="lazy" />
|
||||||
<span class="score-badge">{{ item.score }}</span>
|
<span class="score-badge">{{ displayScore }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="poster-body">
|
<div class="poster-body">
|
||||||
<div class="poster-meta-row">
|
<div class="poster-meta-row">
|
||||||
<span class="status-pill" :class="statusClass">{{ item.status }}</span>
|
<span class="poster-time">{{ displayFormattedDate }}</span>
|
||||||
<span class="poster-time">{{ item.updatedAt }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<h3>{{ item.title }}</h3>
|
<h3>{{ item.title }}</h3>
|
||||||
<p>{{ item.subtitle }}</p>
|
<p v-if="item.originalName">{{ item.originalName }}</p>
|
||||||
<div class="poster-owner">
|
|
||||||
<span class="poster-sheet">{{ item.sheetName }}</span>
|
<!-- 导演和主演 -->
|
||||||
<span>{{ item.accountName }}</span>
|
<div v-if="item.directors" class="poster-director">
|
||||||
<small>{{ item.owner }}</small>
|
🎬 {{ item.directors }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="item.casts" class="poster-casts">
|
||||||
|
🎭 {{ item.casts }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 简介按钮(点击打开弹窗) -->
|
||||||
|
<div v-if="item.description" class="poster-description">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="description-toggle"
|
||||||
|
@click.stop="showDescriptionDialog = true"
|
||||||
|
>
|
||||||
|
📝 查看详情
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tag-list">
|
<div class="tag-list">
|
||||||
<span v-for="tag in item.tags" :key="tag">{{ tag }}</span>
|
<span v-for="tag in displayTags" :key="tag">{{ tag }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 简介弹窗 -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="dialog-fade">
|
||||||
|
<div v-if="showDescriptionDialog" class="description-overlay" @click.self="showDescriptionDialog = false">
|
||||||
|
<div class="description-dialog">
|
||||||
|
<div class="description-dialog__header">
|
||||||
|
<h3>{{ item.title }} - 详情</h3>
|
||||||
|
<button type="button" class="description-dialog__close" @click="showDescriptionDialog = false">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="description-dialog__body">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<div v-if="item.description" class="detail-section">
|
||||||
|
<h4>📖 简介</h4>
|
||||||
|
<p class="detail-text">{{ item.description }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.directors" class="detail-section">
|
||||||
|
<h4>🎬 导演</h4>
|
||||||
|
<p class="detail-text">{{ item.directors }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.casts" class="detail-section">
|
||||||
|
<h4>🎭 主演</h4>
|
||||||
|
<p class="detail-text">{{ item.casts }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.tags" class="detail-section">
|
||||||
|
<h4>🏷️ 标签</h4>
|
||||||
|
<div class="detail-tags">
|
||||||
|
<span v-for="tag in displayTags" :key="tag" class="detail-tag">{{ tag }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.score || item.rating" class="detail-section">
|
||||||
|
<h4>⭐ 评分</h4>
|
||||||
|
<p class="detail-text">{{ displayScore }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.category" class="detail-section">
|
||||||
|
<h4>📂 分类</h4>
|
||||||
|
<p class="detail-text">{{ item.category }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.status" class="detail-section">
|
||||||
|
<h4>📌 状态</h4>
|
||||||
|
<p class="detail-text">{{ displayStatus }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
item: {
|
item: {
|
||||||
@ -40,10 +101,46 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['edit'])
|
const emit = defineEmits(['edit'])
|
||||||
|
|
||||||
const statusClass = computed(() => {
|
// 简介弹窗显示状态
|
||||||
if (props.item.status === '正常') return 'is-normal'
|
const showDescriptionDialog = ref(false)
|
||||||
if (props.item.status === '待复核') return 'is-warning'
|
|
||||||
return 'is-danger'
|
// 显示海报图片 URL
|
||||||
|
const posterImageUrl = computed(() => {
|
||||||
|
// 兼容多种字段名:poster, posterUrl, backdropUrl
|
||||||
|
return props.item.poster || props.item.posterUrl || props.item.backdropUrl || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 显示评分
|
||||||
|
const displayScore = computed(() => {
|
||||||
|
const score = props.item.score ?? props.item.rating ?? 0
|
||||||
|
return score ? `⭐ ${score}` : '暂无评分'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 格式化日期时间,将 ISO 8601 格式转换为友好格式
|
||||||
|
const displayFormattedDate = computed(() => {
|
||||||
|
const raw = props.item.updatedAt || props.item.updateTime || props.item.createTime || ''
|
||||||
|
if (!raw) return ''
|
||||||
|
|
||||||
|
// 去掉 T 字符,将 2026-06-26T15:55:42 转换为 2026-06-26 15:55:42
|
||||||
|
let formatted = raw.replace('T', ' ')
|
||||||
|
|
||||||
|
// 如果有毫秒和时区信息(如 .000Z 或 +08:00),去掉
|
||||||
|
formatted = formatted.replace(/\.\d+(Z|[+-]\d{2}:\d{2})?$/, '')
|
||||||
|
|
||||||
|
return formatted
|
||||||
|
})
|
||||||
|
|
||||||
|
// 显示状态(用于弹窗)
|
||||||
|
const displayStatus = computed(() => {
|
||||||
|
return props.item.status || '正常'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 显示标签(兼容字符串和数组格式)
|
||||||
|
const displayTags = computed(() => {
|
||||||
|
const tags = props.item.tags
|
||||||
|
if (Array.isArray(tags)) return tags
|
||||||
|
if (typeof tags === 'string' && tags.trim()) return tags.split(',').map(t => t.trim())
|
||||||
|
return []
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
|
|||||||
@ -121,8 +121,28 @@
|
|||||||
<span class="bound-info__value">{{ boundInfo.username }}</span>
|
<span class="bound-info__value">{{ boundInfo.username }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="bound-info__item">
|
<div class="bound-info__item">
|
||||||
<span class="bound-info__label">绑定时间:</span>
|
<span class="bound-info__label">存储ID:</span>
|
||||||
<span class="bound-info__value">{{ boundInfo.boundAt }}</span>
|
<span class="bound-info__value">{{ boundInfo.storageId || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="bound-info__item">
|
||||||
|
<span class="bound-info__label">挂载路径:</span>
|
||||||
|
<span class="bound-info__value">{{ boundInfo.mountPath || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="bound-info__item">
|
||||||
|
<span class="bound-info__label">状态:</span>
|
||||||
|
<span class="bound-info__value" :class="{ 'status-active': boundInfo.status === 'active' }">{{ boundInfo.status === 'active' ? '正常' : (boundInfo.status || '-') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="bound-info__item">
|
||||||
|
<span class="bound-info__label">Token有效:</span>
|
||||||
|
<span class="bound-info__value">{{ boundInfo.tokenValid ? '✅ 是' : '❌ 否' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="bound-info__item">
|
||||||
|
<span class="bound-info__label">创建时间:</span>
|
||||||
|
<span class="bound-info__value">{{ formatTime(boundInfo.createTime) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="bound-info__item">
|
||||||
|
<span class="bound-info__label">更新时间:</span>
|
||||||
|
<span class="bound-info__value">{{ formatTime(boundInfo.updateTime) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@ -140,7 +160,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { bindOpenList, unbindOpenList, fetchOpenListStatus } from '../api'
|
import { bindOpenList, unbindOpenList, getOpenListInfo } from '../api'
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
@ -157,19 +177,34 @@ const boundInfo = ref(null)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载已绑定的 OpenList 信息
|
* 加载已绑定的 OpenList 信息
|
||||||
|
* 后端返回格式:
|
||||||
|
* - null 或未绑定:返回 null
|
||||||
|
* - 已绑定:返回 { code: 200, data: { id, openlistUrl, openlistUsername, storageId, mountPath, status, tokenValid, createTime, updateTime } }
|
||||||
*/
|
*/
|
||||||
async function loadBoundInfo() {
|
async function loadBoundInfo() {
|
||||||
try {
|
try {
|
||||||
const result = await fetchOpenListStatus()
|
const result = await getOpenListInfo()
|
||||||
if (result?.code === 200 && result?.data?.bound) {
|
// result 可能为 null(未绑定)或 { code: 200, data: {...} }
|
||||||
|
if (result?.data) {
|
||||||
|
const d = result.data
|
||||||
boundInfo.value = {
|
boundInfo.value = {
|
||||||
url: result.data.url || '',
|
id: d.id,
|
||||||
username: result.data.username || '',
|
url: d.openlistUrl || '',
|
||||||
boundAt: result.data.boundAt || '-',
|
username: d.openlistUsername || '',
|
||||||
|
storageId: d.storageId || '',
|
||||||
|
mountPath: d.mountPath || '',
|
||||||
|
status: d.status || '',
|
||||||
|
tokenValid: d.tokenValid,
|
||||||
|
createTime: d.createTime || '',
|
||||||
|
updateTime: d.updateTime || '',
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 返回 null 或未绑定,清空已绑定信息
|
||||||
|
boundInfo.value = null
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('加载绑定信息失败:', err)
|
console.warn('加载绑定信息失败:', err)
|
||||||
|
boundInfo.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,6 +294,26 @@ function resetForm() {
|
|||||||
setBindStatus(null, '')
|
setBindStatus(null, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化时间
|
||||||
|
*/
|
||||||
|
function formatTime(timeStr) {
|
||||||
|
if (!timeStr) return '-'
|
||||||
|
try {
|
||||||
|
const date = new Date(timeStr)
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return timeStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置绑定状态提示
|
* 设置绑定状态提示
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -896,16 +896,44 @@ canvas {
|
|||||||
min-height: 520px;
|
min-height: 520px;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
/* 图片加载完成的过渡效果 */
|
/* 图片加载完成的过渡效果 */
|
||||||
transition: background-image 0.5s ease, opacity 0.6s ease;
|
transition: opacity 0.6s ease;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 海报图片层 */
|
||||||
|
.hero-bg-image {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 横向海报适配 - 使用独立容器确保图片完整显示 */
|
||||||
|
.hero-main.hero-wide {
|
||||||
|
overflow: visible;
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-main.hero-wide .hero-bg-image {
|
||||||
|
position: relative;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: center;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 500px;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
/* 海报加载状态 - 渐变动画模拟视频加载 */
|
/* 海报加载状态 - 渐变动画模拟视频加载 */
|
||||||
.hero-main.hero-loading {
|
.hero-main.hero-loading {
|
||||||
background: linear-gradient(110deg, #1e293b 30%, #334155 50%, #1e293b 70%);
|
background: linear-gradient(110deg, #1e293b 30%, #334155 50%, #1e293b 70%);
|
||||||
@ -999,6 +1027,79 @@ canvas {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================== 设置页面遮罩层 ==================== */
|
||||||
|
.settings-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(2, 6, 23, 0.58);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
z-index: 100000;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container {
|
||||||
|
position: relative;
|
||||||
|
width: min(760px, calc(100vw - 24px));
|
||||||
|
animation: settingsSlideIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes settingsSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card {
|
||||||
|
border-radius: 28px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 40px 100px rgba(15, 23, 42, 0.28);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #eff5fb;
|
||||||
|
border: none;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #243247;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-close:hover {
|
||||||
|
background: #dfe7f3;
|
||||||
|
color: #1a2639;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 设置页面过渡动画 */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-main__meta-item {
|
.hero-main__meta-item {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -4618,6 +4719,22 @@ body.dark-mode .tmdb-config__status.ready {
|
|||||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 黑夜模式:设置页面优化 ===== */
|
||||||
|
body.dark-mode .settings-card {
|
||||||
|
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||||
|
border: 1px solid rgba(51, 65, 85, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .settings-close {
|
||||||
|
background: rgba(51, 65, 85, 0.5);
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .settings-close:hover {
|
||||||
|
background: rgba(71, 85, 105, 0.7);
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== 黑夜模式:通用对话框优化 ===== */
|
/* ===== 黑夜模式:通用对话框优化 ===== */
|
||||||
body.dark-mode .simple-dialog,
|
body.dark-mode .simple-dialog,
|
||||||
body.dark-mode .app-dialog {
|
body.dark-mode .app-dialog {
|
||||||
@ -4753,6 +4870,55 @@ body.dark-mode .quick-access-panel__header {
|
|||||||
background: rgba(51, 65, 85, 0.5);
|
background: rgba(51, 65, 85, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 黑夜模式:快捷入口悬浮面板优化 ===== */
|
||||||
|
body.dark-mode .quick-access-panel {
|
||||||
|
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||||
|
border: 1px solid rgba(51, 65, 85, 0.5);
|
||||||
|
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .quick-access-panel__header h3 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .quick-access-panel .quick-icons__item {
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .quick-access-panel .quick-icons__item:hover {
|
||||||
|
background: rgba(99, 102, 241, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .quick-access-panel .quick-icons__thumb {
|
||||||
|
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .quick-access-panel .quick-icons__item:hover .quick-icons__thumb {
|
||||||
|
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .quick-access-panel .quick-icons__label {
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .quick-access-panel .quick-icons__empty {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .quick-access-panel .quick-icons__manage {
|
||||||
|
background: linear-gradient(135deg, #38bdf8 0%, #818cf8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .quick-access-panel .quick-icons__manage:hover {
|
||||||
|
background: linear-gradient(135deg, #0ea5e9 0%, #6366f1 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .quick-access-panel .quick-icons__header h3 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== 黑夜模式:滚动条全局优化 ===== */
|
/* ===== 黑夜模式:滚动条全局优化 ===== */
|
||||||
body.dark-mode ::-webkit-scrollbar {
|
body.dark-mode ::-webkit-scrollbar {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
@ -4902,3 +5068,312 @@ body.dark-mode .btn-export {
|
|||||||
body.dark-mode .btn-export:hover {
|
body.dark-mode .btn-export:hover {
|
||||||
background: linear-gradient(135deg, #38bdf8, #0ea5e9);
|
background: linear-gradient(135deg, #38bdf8, #0ea5e9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== 海报卡片新增字段样式(导演、主演、简介) ===== */
|
||||||
|
.poster-director {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-casts {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-description {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6366f1;
|
||||||
|
background: rgba(99, 102, 241, 0.08);
|
||||||
|
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-toggle:hover {
|
||||||
|
background: rgba(99, 102, 241, 0.15);
|
||||||
|
border-color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 简介展开/收起过渡动画 */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s ease, max-height 0.2s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-to,
|
||||||
|
.fade-leave-from {
|
||||||
|
opacity: 1;
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 黑夜模式:海报卡片新增字段样式 ===== */
|
||||||
|
body.dark-mode .poster-director,
|
||||||
|
body.dark-mode .poster-casts {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .description-toggle {
|
||||||
|
color: #a5b4fc;
|
||||||
|
background: rgba(99, 102, 241, 0.15);
|
||||||
|
border-color: rgba(99, 102, 241, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .description-toggle:hover {
|
||||||
|
background: rgba(99, 102, 241, 0.25);
|
||||||
|
border-color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .description-text {
|
||||||
|
background: rgba(15, 23, 42, 0.4);
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 简介弹窗样式 ===== */
|
||||||
|
.description-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100001;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-dialog {
|
||||||
|
width: min(600px, calc(100% - 40px));
|
||||||
|
max-height: 80vh;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.25);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: dialogSlideIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dialogSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-dialog__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-dialog__header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-dialog__close {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #6b7280;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-dialog__close:hover {
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-dialog__body {
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section h4 {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-text {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #4b5563;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: rgba(99, 102, 241, 0.08);
|
||||||
|
color: #6366f1;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗滚动条样式 */
|
||||||
|
.description-dialog__body::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-dialog__body::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-dialog__body::-webkit-scrollbar-thumb {
|
||||||
|
background: #d1d5db;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-dialog__body::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗过渡动画 */
|
||||||
|
.dialog-fade-enter-active {
|
||||||
|
animation: dialogFadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-fade-leave-active {
|
||||||
|
animation: dialogFadeOut 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dialogFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dialogFadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px) scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 黑夜模式:简介弹窗样式 ===== */
|
||||||
|
body.dark-mode .description-overlay {
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .description-dialog {
|
||||||
|
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||||
|
border: 1px solid rgba(51, 65, 85, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .description-dialog__header {
|
||||||
|
border-bottom-color: rgba(51, 65, 85, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .description-dialog__header h3 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .description-dialog__close {
|
||||||
|
background: rgba(51, 65, 85, 0.5);
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .description-dialog__close:hover {
|
||||||
|
background: rgba(71, 85, 105, 0.7);
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .detail-section h4 {
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .detail-text {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .detail-tag {
|
||||||
|
background: rgba(99, 102, 241, 0.15);
|
||||||
|
color: #a5b4fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .description-dialog__body::-webkit-scrollbar-thumb {
|
||||||
|
background: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .description-dialog__body::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #64748b;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user