feat: 添加透明主题支持及背景图片轮换功能

- 在 App.vue 中引入 API 获取背景图片,并实现背景图片的轮换功能。
- 更新主题切换逻辑,支持透明主题,并在主题变化时更新 HTML 属性。
- 在样式中添加透明主题的特定样式,确保各个组件在透明主题下的显示效果。
This commit is contained in:
jxxghp
2025-04-18 13:47:39 +08:00
parent 01c8304c8b
commit 476d2f7e81
11 changed files with 317 additions and 43 deletions

View File

@@ -195,11 +195,10 @@ onMounted(() => {
<VIcon :icon="getThemeIcon" />
</IconBtn>
</template>
<VList class="theme-switcher-list pt-0">
<VList class="theme-switcher-list pt-0 overflow-hidden">
<VCardItem class="theme-switcher-header">
<VCardTitle class="font-weight-medium text-primary">主题选择</VCardTitle>
</VCardItem>
<div class="theme-switcher-options px-2">
<VListItem
v-for="theme in props.themes"
@@ -271,8 +270,7 @@ onMounted(() => {
}
.theme-switcher-options {
max-block-size: 300px;
overflow-y: auto;
overflow-y: hidden;
}
.theme-option {

View File

@@ -2,6 +2,7 @@
import { useTheme } from 'vuetify'
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
import { ensureRenderComplete, removeEl } from './@core/utils/dom'
import api from '@/api'
// 生效主题
const { global: globalTheme } = useTheme()
@@ -9,9 +10,61 @@ let themeValue = localStorage.getItem('theme') || 'light'
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
// 更新data-theme属性以便CSS选择器能正确匹配
function updateHtmlThemeAttribute(themeName) {
document.documentElement.setAttribute('data-theme', themeName)
// 确保body元素也有相同的主题属性以便更好地选择弹出窗口
document.body.setAttribute('data-theme', themeName)
}
// 显示状态
const show = ref(false)
// 背景图片
const backgroundImages = ref<string[]>([])
const activeImageIndex = ref(0)
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
let backgroundRotationTimer: NodeJS.Timeout | null = null
// 获取背景图片
async function fetchBackgroundImages() {
try {
backgroundImages.value = await api.get('/login/wallpapers')
console.log('获取背景图片成功:', backgroundImages.value)
} catch (e) {
console.error('获取背景图片失败:', e)
}
}
// 开始背景图片轮换
function startBackgroundRotation() {
if (backgroundRotationTimer) clearInterval(backgroundRotationTimer)
if (backgroundImages.value.length > 1) {
backgroundRotationTimer = setInterval(() => {
activeImageIndex.value = (activeImageIndex.value + 1) % backgroundImages.value.length
}, 10000) // 每10秒切换一次
}
}
// 监听主题变化
watch(
() => globalTheme.name.value,
async newTheme => {
// 更新HTML属性
updateHtmlThemeAttribute(newTheme)
if (newTheme === 'transparent' && backgroundImages.value.length === 0) {
await fetchBackgroundImages()
startBackgroundRotation()
} else if (newTheme !== 'transparent' && backgroundRotationTimer) {
clearInterval(backgroundRotationTimer)
backgroundRotationTimer = null
}
},
{ immediate: true },
)
// ApexCharts 全局配置
declare global {
interface Window {
@@ -43,6 +96,9 @@ if (window.Apex) {
}
onMounted(() => {
// 初始化data-theme属性
updateHtmlThemeAttribute(globalTheme.name.value)
ensureRenderComplete(() => {
nextTick(() => {
setTimeout(() => {
@@ -56,10 +112,92 @@ onMounted(() => {
})
})
})
onUnmounted(() => {
if (backgroundRotationTimer) {
clearInterval(backgroundRotationTimer)
backgroundRotationTimer = null
}
})
</script>
<template>
<VApp v-show="show">
<RouterView />
</VApp>
<div class="app-wrapper">
<!-- 透明主题背景 -->
<template v-if="isTransparentTheme && backgroundImages.length > 0">
<div class="background-container">
<div
v-for="(imageUrl, index) in backgroundImages"
:key="index"
class="background-image"
:class="{ 'active': index === activeImageIndex }"
:style="{ backgroundImage: `url(${imageUrl})` }"
></div>
<!-- 全局磨砂层 -->
<div class="global-blur-layer"></div>
</div>
</template>
<VApp v-show="show" :class="{ 'transparent-app': isTransparentTheme }">
<RouterView />
</VApp>
</div>
</template>
<style lang="scss">
/* 全局样式 */
.app-wrapper {
position: relative;
inline-size: 100%;
min-block-size: 100vh;
}
.background-container {
position: fixed;
z-index: 0;
overflow: hidden;
block-size: 100%;
inline-size: 100%;
inset-block-start: 0;
inset-inline-start: 0;
}
.background-image {
position: absolute;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
block-size: 100%;
inline-size: 100%;
inset-block-start: 0;
inset-inline-start: 0;
opacity: 0;
transition: opacity 1.5s ease;
&::after {
position: absolute;
background: linear-gradient(rgba(0, 0, 0, 30%) 0%, rgba(0, 0, 0, 60%) 100%);
block-size: 100%;
content: '';
inline-size: 100%;
inset-block-start: 0;
inset-inline-start: 0;
}
&.active {
opacity: 1;
}
}
/* 全局磨砂层 */
.global-blur-layer {
position: absolute;
z-index: 1;
backdrop-filter: blur(10px);
background-color: rgba(0, 0, 0, 30%);
block-size: 100%;
inline-size: 100%;
inset-block-start: 0;
inset-inline-start: 0;
}
</style>

View File

@@ -120,7 +120,7 @@ onMounted(() => {
<template>
<VDialog max-width="35rem" scrollable>
<VCard>
<VCardTitle class="py-3 me-12">
<VCardTitle class="py-4 me-12">
<VIcon icon="mdi-download" class="me-2" />
<span v-if="title">{{ torrent?.site_name }} - {{ title }}</span>
<span v-else>确认下载</span>

View File

@@ -70,7 +70,7 @@ onUnmounted(() => {
})
</script>
<template>
<div class="tab-header">
<div class="tab-header rounded-lg">
<div ref="tabsContainerRef" class="header-tabs" :class="{ 'show-indicator': showTabsScrollIndicator }">
<div
v-for="(item, index) in items"
@@ -94,7 +94,6 @@ onUnmounted(() => {
align-items: center;
justify-content: space-between;
backdrop-filter: blur(10px);
background-color: rgba(var(--v-theme-background), 0.8);
border-block-end: 1px solid rgba(var(--v-theme-on-surface), 0.05);
inset-block-start: 0;
margin-block-end: 16px;
@@ -127,7 +126,7 @@ onUnmounted(() => {
&::after {
position: absolute;
z-index: 1; // Ensure it's above the tabs but below other header elements if needed
background: linear-gradient(to left, rgba(var(--v-theme-background), 1) 30%, transparent);
background: linear-gradient(to left, rgba(var(--v-theme-background), 10.3) 30%, transparent);
content: '';
inline-size: 40px; // Width of the fade effect
inset-block: 0;
@@ -136,11 +135,6 @@ onUnmounted(() => {
pointer-events: none; // Allow interaction with content behind it
transition: opacity 0.2s ease-in-out;
}
// Show the indicator when the class is present
&.show-indicator::after {
opacity: 1;
}
}
.header-tab-icon {

View File

@@ -19,9 +19,14 @@ const themes: ThemeSwitcherTheme[] = [
},
{
name: 'purple',
title: '紫韵幽兰',
title: '紫',
icon: 'mdi-brightness-4',
},
{
name: 'transparent',
title: '透明',
icon: 'mdi-gradient-vertical',
},
]
</script>

View File

@@ -197,7 +197,7 @@ onUnmounted(() => {
</VFadeTransition>
<!-- 精简标题栏 -->
<VCard v-if="isRefreshed" class="search-header d-flex align-center mb-4">
<VCard v-if="isRefreshed" class="search-header d-flex align-center mb-3">
<div class="search-info-container d-flex align-center flex-wrap">
<div class="search-title text-primary">资源搜索结果</div>
<div class="search-tags d-flex flex-wrap">
@@ -225,7 +225,7 @@ onUnmounted(() => {
<!-- 视图切换加载状态 -->
<VFadeTransition>
<div v-if="isRefreshed && isViewChanging" class="view-changing-container">
<div v-if="isRefreshed && isViewChanging" class="view-changing-container rounded-lg">
<div class="view-changing-content">
<div class="pulse-loader">
<div class="pulse-circle"></div>
@@ -327,7 +327,6 @@ onUnmounted(() => {
/* 精简标题栏样式 */
.search-header {
border: 1px solid rgba(var(--v-theme-on-surface), 0.08);
box-shadow: 0 2px 8px rgba(0, 0, 0, 5%);
padding-block: 12px;
padding-inline: 16px;
}
@@ -390,7 +389,6 @@ onUnmounted(() => {
align-items: center;
justify-content: center;
backdrop-filter: blur(8px);
background-color: rgba(var(--v-theme-background), 0.7);
inset: 0;
}

View File

@@ -8,7 +8,7 @@ const theme: VuetifyOptions['theme'] = {
colors: {
'primary': '#9155FD',
'secondary': '#8A8D93',
'on-secondary': '#fff',
'on-secondary': '#FFFFFF',
'success': '#56CA00',
'info': '#16B1FF',
'warning': '#FFB400',
@@ -30,12 +30,12 @@ const theme: VuetifyOptions['theme'] = {
'grey-800': '#424242',
'grey-900': '#212121',
'perfect-scrollbar-thumb': '#DBDADE',
'skin-bordered-background': '#fff',
'skin-bordered-surface': '#fff',
'skin-bordered-background': '#FFFFFF',
'skin-bordered-surface': '#FFFFFF',
},
variables: {
'code-color': '#d400ff',
'code-color': '#D400FF',
'overlay-scrim-background': '#3A3541',
'overlay-scrim-opacity': 0.5,
'hover-opacity': 0.04,
@@ -59,7 +59,7 @@ const theme: VuetifyOptions['theme'] = {
colors: {
'primary': '#6E66ED',
'secondary': '#8A8D93',
'on-secondary': '#fff',
'on-secondary': '#FFFFFF',
'success': '#56CA00',
'info': '#16B1FF',
'warning': '#FFB400',
@@ -109,7 +109,7 @@ const theme: VuetifyOptions['theme'] = {
colors: {
'primary': '#9155FD',
'secondary': '#8A8D93',
'on-secondary': '#fff',
'on-secondary': '#FFFFFF',
'success': '#56CA00',
'info': '#16B1FF',
'warning': '#FFB400',
@@ -155,6 +155,61 @@ const theme: VuetifyOptions['theme'] = {
'shadow-key-ambient-opacity': 'rgba(20, 18, 33, 0.04)',
},
},
transparent: {
dark: true,
colors: {
'primary': '#A370F7',
'secondary': '#B794FF',
'on-secondary': '#FFFFFF',
'success': '#66BB6A',
'info': '#42A5F5',
'warning': '#FFA726',
'error': '#EF5350',
'on-primary': '#FFFFFF',
'on-success': '#FFFFFF',
'on-warning': '#FFFFFF',
'background': '#000000',
'on-background': '#E7E3FC',
'surface': 'rgba(30, 30, 30, 0.3)',
'on-surface': '#E7E3FC',
'surface-variant': 'rgba(30, 30, 30, 0.2)',
'on-surface-variant': 'rgba(255, 255, 255, 0.65)',
'grey-50': 'rgba(42, 46, 66, 0.15)',
'grey-100': 'rgba(71, 67, 96, 0.15)',
'grey-200': 'rgba(74, 80, 114, 0.15)',
'grey-300': 'rgba(94, 102, 146, 0.15)',
'grey-400': 'rgba(121, 131, 187, 0.15)',
'grey-500': 'rgba(134, 146, 208, 0.15)',
'grey-600': 'rgba(170, 179, 222, 0.15)',
'grey-700': 'rgba(182, 190, 227, 0.15)',
'grey-800': 'rgba(207, 211, 236, 0.15)',
'grey-900': 'rgba(231, 233, 246, 0.15)',
'perfect-scrollbar-thumb': 'rgba(158, 158, 190, 0.4)',
'skin-bordered-background': 'rgba(30, 30, 30, 0.3)',
'skin-bordered-surface': 'rgba(30, 30, 30, 0.3)',
'card-background': 'rgba(30, 30, 30, 0.3)',
},
variables: {
'code-color': '#6D9EEB',
'overlay-scrim-background': '0, 0, 0',
'overlay-scrim-opacity': 0.7,
'hover-opacity': 0.1,
'focus-opacity': 0.15,
'selected-opacity': 0.2,
'activated-opacity': 0.15,
'pressed-opacity': 0.2,
'dragged-opacity': 0.15,
'border-color': '#E7E3FC',
'table-header-background': 'rgba(30, 30, 30, 0.3)',
'custom-background': 'rgba(30, 30, 30, 0.3)',
'card-background': 'rgba(30, 30, 30, 0.3)',
// Shadows
'shadow-key-umbra-opacity': 'rgba(0, 0, 0, 0.07)',
'shadow-key-penumbra-opacity': 'rgba(0, 0, 0, 0.1)',
'shadow-key-ambient-opacity': 'rgba(0, 0, 0, 0.05)',
},
},
},
}

View File

@@ -11,6 +11,7 @@ html.v-overlay-scroll-blocked {
@media (width <= 768px){
html.v-overlay-scroll-blocked {
position: relative;
--v-body-scroll-y: 0px !important;
}
}
@@ -251,8 +252,6 @@ html.v-overlay-scroll-blocked {
.card-cover-blurred::before {
position: absolute;
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
background: rgba(29, 39, 59, 48%);
content: '';
@@ -260,21 +259,15 @@ html.v-overlay-scroll-blocked {
}
.v-overlay__content .v-list{
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
background-color: rgb(var(--v-theme-surface), 0.9) !important;
}
.v-overlay__content .v-card:not(.bg-primary){
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
background-color: rgb(var(--v-theme-surface), 0.95) !important;
.v-list, .v-table {
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: none;
backdrop-filter: none;
background-color: transparent !important;
}
@@ -322,3 +315,91 @@ html.v-overlay-scroll-blocked {
.v-infinite-scroll__side {
padding: 0;
}
// 透明主题特殊样式
.transparent-app {
// 先将所有全局组件定义放在前面避免CSS优先级问题
.v-application, .v-layout, .v-main, .layout-page-content {
background: transparent;
}
// 侧边导航栏
.layout-vertical-nav {
backdrop-filter: blur(10px);
background-color: rgba(var(--v-theme-surface), 0.5);
border-inline-end: 1px solid rgba(255, 255, 255, 10%);
}
// 导航栏
.navbar-blur::after {
background: transparent;
}
// 卡片
.v-card:not(.no-blur) {
backdrop-filter: blur(10px);
background-color: rgba(var(--v-theme-surface), 0.3);
}
// 列表
.v-list {
backdrop-filter: blur(10px);
background-color: rgba(var(--v-theme-surface), 0.3);
}
// 工具栏
.v-toolbar {
backdrop-filter: blur(10px);
background-color: rgba(var(--v-theme-surface), 0.3);
}
// 表格
.v-table {
background-color: rgba(var(--v-theme-surface), 0.3);
}
// 页脚
.v-footer {
backdrop-filter: blur(10px);
background-color: rgba(var(--v-theme-surface), 0.3);
}
// 页面容器
.layout-content-wrapper {
background: transparent;
}
// 无内容区域的背景设为透明
.page-content-container {
background: transparent;
}
// 对话框和菜单蒙层样式
.v-overlay__scrim {
background: rgba(var(--v-overlay-scrim-background), var(--v-overlay-scrim-opacity));
}
// 折叠面板
.v-expansion-panel {
backdrop-filter: blur(10px);
background-color: rgba(var(--v-theme-surface), 0.3);
}
}
// 透明主题下的弹出窗口样式
html[data-theme="transparent"] {
.v-overlay__content {
backdrop-filter: blur(10px) !important;
.v-card:not(.bg-primary) {
backdrop-filter: blur(10px);
background-color: rgb(var(--v-theme-surface), 0.5) !important;
}
.v-list {
backdrop-filter: blur(10px);
background-color: rgb(var(--v-theme-surface), 0.5) !important;
}
}
}

View File

@@ -12,6 +12,7 @@ import { isNullOrEmptyObject } from '@/@core/utils'
import { useUserStore } from '@/stores'
import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
import SearchSiteDialog from '@/components/dialog/SearchSiteDialog.vue'
import { useTheme } from 'vuetify'
// 输入参数
const mediaProps = defineProps({
@@ -30,6 +31,9 @@ const userStore = useUserStore()
// 提示框
const $toast = useToast()
// 获取主题信息
const theme = useTheme()
// 媒体详情
const mediaDetail = ref<MediaInfo>({} as MediaInfo)
@@ -72,6 +76,11 @@ const searchType = ref('title')
// 选择站点对话框
const chooseSiteDialog = ref(false)
// 计算主题是否为透明
const isNonTransparentTheme = computed(() => {
return theme.name.value !== 'transparent'
})
// 查询所有站点
async function querySites() {
try {
@@ -519,7 +528,7 @@ onBeforeMount(() => {
<template>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" class="max-w-8xl mx-auto px-4">
<template v-if="getBackdropUrl || getPosterUrl">
<template v-if="(getBackdropUrl || getPosterUrl) && isNonTransparentTheme">
<div class="vue-media-back absolute left-0 top-0 w-full h-96">
<VImg class="h-96" position="top" :src="getBackdropUrl || getPosterUrl" cover />
</div>
@@ -972,7 +981,6 @@ onBeforeMount(() => {
),
linear-gradient(90deg, rgba(var(--v-theme-background), 0) 50%, rgba(var(--v-theme-background), 1) 100%),
linear-gradient(270deg, rgba(var(--v-theme-background), 0) 50%, rgba(var(--v-theme-background), 1) 100%);
box-shadow: 0 0 0 2px rgb(var(--v-theme-background));
margin-block-start: calc(-70px - env(safe-area-inset-top));
}

View File

@@ -387,7 +387,7 @@ function loadMore({ done }: { done: any }) {
</script>
<template>
<div class="search-header d-none d-sm-flex">
<div class="search-header d-none d-sm-flex mb-3">
<!-- 页面头部和筛选栏 -->
<VCard class="view-header rounded-xl">
<div class="d-flex align-center flex-wrap pa-3">
@@ -499,7 +499,7 @@ function loadMore({ done }: { done: any }) {
</div>
<!-- 移动端头部和筛选区域 -->
<VCard class="d-block d-sm-none search-header-mobile">
<VCard class="d-block d-sm-none search-header-mobile mb-3">
<!-- 移动端头部 -->
<div class="view-header">
<div class="d-flex align-center flex-wrap pa-2">
@@ -617,7 +617,6 @@ function loadMore({ done }: { done: any }) {
position: sticky;
z-index: 10;
backdrop-filter: blur(10px);
background-color: rgba(var(--v-theme-background), 0.95);
inset-block-start: 0;
}

View File

@@ -583,7 +583,7 @@ onMounted(() => {
mode="intersect"
side="end"
:items="displayDataList"
class="resource-list"
class="resource-list overflow-hidden"
@load="loadMore"
>
<template #loading />
@@ -607,7 +607,6 @@ onMounted(() => {
position: sticky;
z-index: 10;
backdrop-filter: blur(10px);
background-color: rgba(var(--v-theme-background), 0.95);
inset-block-start: 0;
}
@@ -615,7 +614,6 @@ onMounted(() => {
position: sticky;
z-index: 10;
backdrop-filter: blur(10px);
background-color: rgba(var(--v-theme-background), 0.95);
inset-block-start: 0;
}