mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-07-05 14:31:31 +08:00
feat(theme): 添加主题定制器的圆角和阴影示例,优化透明主题下的视觉效果
This commit is contained in:
@@ -143,7 +143,7 @@ const dynamicHeaderTab = ref<DynamicHeaderTab | null>(null)
|
||||
const openHorizontalNavGroup = ref<string | null>(null)
|
||||
const pendingHorizontalTab = ref<{ path: string; tab: string } | null>(null)
|
||||
|
||||
// 提供一个方法让其他组件注册动态标签页
|
||||
/** 注册页面动态标签页,并在页面可用后应用待切换的水平导航标签。 */
|
||||
const registerDynamicHeaderTab = (tab: DynamicHeaderTab) => {
|
||||
// 保存注册标签页的路由路径
|
||||
tab.routePath = route.path
|
||||
@@ -152,12 +152,12 @@ const registerDynamicHeaderTab = (tab: DynamicHeaderTab) => {
|
||||
applyPendingHorizontalTab()
|
||||
}
|
||||
|
||||
// 提供一个方法让其他组件取消注册动态标签页
|
||||
/** 取消当前页面注册的动态标签页。 */
|
||||
const unregisterDynamicHeaderTab = () => {
|
||||
dynamicHeaderTab.value = null
|
||||
}
|
||||
|
||||
// 标签页值更新处理
|
||||
/** 更新当前动态标签页选中值,并通知注册页面同步状态。 */
|
||||
const handleTabChange = (newValue: string) => {
|
||||
if (dynamicHeaderTab.value) {
|
||||
dynamicHeaderTab.value.modelValue = newValue
|
||||
@@ -227,7 +227,7 @@ onUnmounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 监听Service Worker消息
|
||||
/** 处理 Service Worker 推送的离线状态消息。 */
|
||||
const handleServiceWorkerMessage = (event: MessageEvent) => {
|
||||
if (event.data && event.data.type === 'OFFLINE_STATUS') {
|
||||
if (event.data.offline) {
|
||||
@@ -238,7 +238,7 @@ const handleServiceWorkerMessage = (event: MessageEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否可以使用下拉手势
|
||||
/** 判断当前页面状态是否允许使用主界面下拉快捷入口手势。 */
|
||||
const canUsePullGesture = () => {
|
||||
// 检查是否在dashboard页面
|
||||
const isDashboard = route.path === '/dashboard' || route.path === '/'
|
||||
@@ -279,7 +279,7 @@ const {
|
||||
},
|
||||
})
|
||||
|
||||
// 根据分类获取菜单列表
|
||||
/** 根据菜单分组标题获取当前用户可见的菜单项。 */
|
||||
const getMenuList = (header: string) => {
|
||||
// 使用国际化菜单
|
||||
const menus = getNavMenus(t)
|
||||
@@ -287,19 +287,22 @@ const getMenuList = (header: string) => {
|
||||
return filteredMenus.filter((item: NavMenu) => item.header === header)
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
/** 返回浏览历史中的上一页。 */
|
||||
function goBack() {
|
||||
history.back()
|
||||
}
|
||||
|
||||
/** 同步主题定制器变更后的布局模式。 */
|
||||
function handleThemeCustomizerChange(event: Event) {
|
||||
themeLayout.value = (event as CustomEvent<ThemeCustomizerSettings>).detail.layout
|
||||
}
|
||||
|
||||
/** 打开主题定制器面板。 */
|
||||
function handleThemeCustomizerOpen() {
|
||||
showThemeCustomizer.value = true
|
||||
}
|
||||
|
||||
/** 判断水平导航菜单项是否匹配当前路由。 */
|
||||
function isHorizontalNavActive(item: NavMenu) {
|
||||
const targetPath = normalizeMenuPath(item.to)
|
||||
if (!targetPath) return false
|
||||
@@ -309,18 +312,22 @@ function isHorizontalNavActive(item: NavMenu) {
|
||||
return currentPath === targetPath || currentPath.startsWith(`${targetPath}/`)
|
||||
}
|
||||
|
||||
/** 判断水平导航分组内是否存在当前路由激活项。 */
|
||||
function isHorizontalNavGroupActive(group: { items: NavMenu[] }) {
|
||||
return group.items.some(isHorizontalNavActive)
|
||||
}
|
||||
|
||||
/** 判断菜单项是否存在可在水平导航中展示的动态标签。 */
|
||||
function hasHorizontalDynamicTabs(item: NavMenu) {
|
||||
return showHorizontalThemeNav.value && getHorizontalNavTabs(item).length > 0
|
||||
}
|
||||
|
||||
/** 判断水平导航动态标签是否为当前选中项。 */
|
||||
function isHorizontalDynamicTabActive(tab: DynamicHeaderTabItem) {
|
||||
return dynamicHeaderTab.value?.modelValue === tab.tab
|
||||
}
|
||||
|
||||
/** 切换水平导航中的动态标签,必要时先跳转到目标页面。 */
|
||||
async function handleHorizontalDynamicTabSelect(item: NavMenu, tab: DynamicHeaderTabItem) {
|
||||
const targetPath = normalizeMenuPath(item.to)
|
||||
const currentPath = normalizeMenuPath(route.path)
|
||||
@@ -336,28 +343,34 @@ async function handleHorizontalDynamicTabSelect(item: NavMenu, tab: DynamicHeade
|
||||
openHorizontalNavGroup.value = null
|
||||
}
|
||||
|
||||
/** 关闭当前展开的水平导航分组菜单。 */
|
||||
function closeHorizontalNavGroup() {
|
||||
openHorizontalNavGroup.value = null
|
||||
}
|
||||
|
||||
/** 读取可能是 Ref 的值,空值时回落到默认值。 */
|
||||
function resolveMaybeRefValue<T>(value: T | ComputedRef<T> | undefined, fallback: T): T {
|
||||
return isRef(value) ? value.value : (value ?? fallback)
|
||||
}
|
||||
|
||||
/** 解析动态头部按钮颜色。 */
|
||||
function resolveHeaderButtonColor(button: DynamicHeaderTabButton) {
|
||||
return resolveMaybeRefValue(button.color, 'gray')
|
||||
}
|
||||
|
||||
/** 解析动态头部按钮加载状态。 */
|
||||
function resolveHeaderButtonLoading(button: DynamicHeaderTabButton) {
|
||||
return resolveMaybeRefValue(button.loading, false)
|
||||
}
|
||||
|
||||
/** 校验权限后执行动态头部按钮动作。 */
|
||||
function handleHeaderButtonClick(button: DynamicHeaderTabButton) {
|
||||
if (!hasItemPermission(button, userPermissions.value)) return
|
||||
|
||||
button.action?.()
|
||||
}
|
||||
|
||||
/** 获取水平导航动态标签图标,不可渲染时使用默认圆点图标。 */
|
||||
function getHorizontalTabIcon(tab: DynamicHeaderTabItem) {
|
||||
const icon = tab.icon?.trim()
|
||||
|
||||
@@ -370,12 +383,14 @@ function getHorizontalTabIcon(tab: DynamicHeaderTabItem) {
|
||||
return icon
|
||||
}
|
||||
|
||||
/** 标准化菜单路径,移除末尾斜杠并保留根路径。 */
|
||||
function normalizeMenuPath(value: unknown) {
|
||||
if (typeof value !== 'string') return ''
|
||||
|
||||
return value.replace(/\/$/, '') || '/'
|
||||
}
|
||||
|
||||
/** 获取水平导航菜单项可展示的标签列表。 */
|
||||
function getHorizontalNavTabs(item: NavMenu): DynamicHeaderTabItem[] {
|
||||
const targetPath = normalizeMenuPath(item.to)
|
||||
|
||||
@@ -386,6 +401,7 @@ function getHorizontalNavTabs(item: NavMenu): DynamicHeaderTabItem[] {
|
||||
return item.tabs ?? []
|
||||
}
|
||||
|
||||
/** 在目标页面注册动态标签后应用此前暂存的标签切换。 */
|
||||
function applyPendingHorizontalTab() {
|
||||
if (!pendingHorizontalTab.value || !hasDynamicHeaderTab.value) return
|
||||
|
||||
@@ -399,16 +415,17 @@ function applyPendingHorizontalTab() {
|
||||
pendingHorizontalTab.value = null
|
||||
}
|
||||
|
||||
// 关闭插件快速访问
|
||||
/** 关闭插件快速访问面板。 */
|
||||
function handleClosePluginQuickAccess() {
|
||||
showPluginQuickAccess.value = false
|
||||
}
|
||||
|
||||
// 点击插件后关闭
|
||||
/** 点击插件入口后关闭插件快速访问面板。 */
|
||||
function handlePluginClick() {
|
||||
showPluginQuickAccess.value = false
|
||||
}
|
||||
|
||||
/** 将插件侧边栏菜单合并到对应的导航分组中。 */
|
||||
function appendPluginSidebarMenus() {
|
||||
for (const { navMenu, section } of filterPluginSidebarNavEntries(
|
||||
pluginSidebarNavStore.items,
|
||||
@@ -474,15 +491,15 @@ onMounted(async () => {
|
||||
<!-- 👉 Pull Down Indicator -->
|
||||
<div
|
||||
v-if="appMode && showPullIndicator"
|
||||
class="pull-indicator"
|
||||
class="app-pull-indicator"
|
||||
:style="{
|
||||
'--pull-indicator-navbar-extra-height': navbarExtraHeight,
|
||||
'--app-pull-indicator-navbar-extra-height': navbarExtraHeight,
|
||||
opacity: indicatorOpacity,
|
||||
transform: indicatorTransform,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="indicator-icon"
|
||||
class="app-pull-indicator__icon"
|
||||
:style="{
|
||||
transform: `scale(${
|
||||
1 + Math.min((pullDistance - PULL_CONFIG.SHOW_INDICATOR) / PULL_CONFIG.MAX_PULL_DISTANCE, 0.5) * 0.3
|
||||
@@ -847,72 +864,4 @@ onMounted(async () => {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.pull-indicator {
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px;
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(20px);
|
||||
background: rgba(var(--v-theme-surface), 0.3);
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 10%),
|
||||
0 1px 3px rgba(0, 0, 0, 6%);
|
||||
inset-block-start: calc(
|
||||
env(safe-area-inset-top, 0px) + 4rem + var(--pull-indicator-navbar-extra-height, 0rem) + 0.75rem
|
||||
);
|
||||
inset-inline-start: 50%;
|
||||
pointer-events: none;
|
||||
transform: translate3d(-50%, 0, 0);
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.indicator-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: rgba(var(--v-theme-primary), 0.08);
|
||||
block-size: 40px;
|
||||
inline-size: 40px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* 透明主题适配 */
|
||||
html[class*='transparent'] .pull-indicator,
|
||||
html[class*='mica'] .pull-indicator,
|
||||
html[class*='acrylic'] .pull-indicator {
|
||||
border: 1px solid rgba(255, 255, 255, 20%);
|
||||
background: rgba(255, 255, 255, 95%);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 12%),
|
||||
0 4px 16px rgba(0, 0, 0, 8%);
|
||||
}
|
||||
|
||||
html[class*='transparent'] .indicator-icon,
|
||||
html[class*='mica'] .indicator-icon,
|
||||
html[class*='acrylic'] .indicator-icon {
|
||||
background: rgba(var(--v-theme-primary), 0.12);
|
||||
}
|
||||
|
||||
html[data-theme='dark'][class*='transparent'] .pull-indicator,
|
||||
html[data-theme='dark'][class*='mica'] .pull-indicator,
|
||||
html[data-theme='dark'][class*='acrylic'] .pull-indicator {
|
||||
border: 1px solid rgba(255, 255, 255, 10%);
|
||||
background: rgba(18, 18, 18, 95%);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 30%),
|
||||
0 4px 16px rgba(0, 0, 0, 20%);
|
||||
}
|
||||
|
||||
html[data-theme='dark'][class*='transparent'] .indicator-icon,
|
||||
html[data-theme='dark'][class*='mica'] .indicator-icon,
|
||||
html[data-theme='dark'][class*='acrylic'] .indicator-icon {
|
||||
background: rgba(var(--v-theme-primary), 0.15);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1527,3 +1527,75 @@ html[data-theme="transparent"].transparent-glass-realtime .v-theme--transparent
|
||||
.v-menu .v-overlay__content {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 主界面下拉入口提示:放在公共样式中,避免 scoped 样式无法响应根节点主题属性。
|
||||
.app-pull-indicator {
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px;
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), var(--app-pull-indicator-border-opacity));
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(var(--app-pull-indicator-blur)) saturate(1.2);
|
||||
background: rgba(var(--v-theme-surface), var(--app-pull-indicator-surface-opacity));
|
||||
box-shadow: var(--app-pull-indicator-shadow);
|
||||
inset-block-start: calc(
|
||||
env(safe-area-inset-top, 0px) + 4rem + var(--app-pull-indicator-navbar-extra-height, 0rem) + 0.75rem
|
||||
);
|
||||
inset-inline-start: 50%;
|
||||
pointer-events: none;
|
||||
transform: translate3d(-50%, 0, 0);
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
will-change: opacity, transform;
|
||||
|
||||
--app-pull-indicator-blur: 20px;
|
||||
--app-pull-indicator-border-opacity: 0.12;
|
||||
--app-pull-indicator-icon-surface-opacity: 0.1;
|
||||
--app-pull-indicator-shadow: 0 8px 24px rgba(0, 0, 0, 12%), 0 2px 8px rgba(0, 0, 0, 8%);
|
||||
--app-pull-indicator-surface-opacity: 0.86;
|
||||
}
|
||||
|
||||
.app-pull-indicator__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: rgba(var(--v-theme-primary), var(--app-pull-indicator-icon-surface-opacity));
|
||||
block-size: 40px;
|
||||
inline-size: 40px;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.2s ease;
|
||||
}
|
||||
|
||||
html[data-theme='light'] .app-pull-indicator {
|
||||
--app-pull-indicator-border-opacity: 0.1;
|
||||
--app-pull-indicator-icon-surface-opacity: 0.1;
|
||||
--app-pull-indicator-shadow: 0 8px 24px rgba(58, 53, 65, 10%), 0 2px 8px rgba(58, 53, 65, 6%);
|
||||
--app-pull-indicator-surface-opacity: 0.92;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .app-pull-indicator,
|
||||
html[data-theme='purple'] .app-pull-indicator {
|
||||
--app-pull-indicator-border-opacity: 0.14;
|
||||
--app-pull-indicator-icon-surface-opacity: 0.16;
|
||||
--app-pull-indicator-shadow: 0 10px 30px rgba(0, 0, 0, 32%), 0 3px 12px rgba(0, 0, 0, 22%);
|
||||
--app-pull-indicator-surface-opacity: 0.78;
|
||||
}
|
||||
|
||||
html[data-theme='transparent'] .app-pull-indicator {
|
||||
backdrop-filter: blur(var(--transparent-blur-heavy, var(--app-pull-indicator-blur))) saturate(1.25);
|
||||
|
||||
--app-pull-indicator-border-opacity: 0.14;
|
||||
--app-pull-indicator-icon-surface-opacity: 0.18;
|
||||
--app-pull-indicator-shadow: 0 10px 32px rgba(0, 0, 0, 30%), 0 4px 16px rgba(0, 0, 0, 20%);
|
||||
--app-pull-indicator-surface-opacity: var(--transparent-opacity-heavy, 0.5);
|
||||
}
|
||||
|
||||
html[data-theme='transparent'].transparent-blur-disabled .app-pull-indicator {
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
@@ -145,6 +145,27 @@ html[data-theme="transparent"] {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
// 主题定制器的圆角/阴影示例沿用框架预览的透明玻璃底,避免透明主题下出现实色黑块。
|
||||
.theme-customizer-radius-scene,
|
||||
.theme-customizer-shadow-slider,
|
||||
.theme-customizer-shadow-slider__sample {
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(var(--v-theme-on-surface), 0.02),
|
||||
rgba(var(--v-theme-on-surface), 0.05)
|
||||
),
|
||||
rgba(var(--v-theme-surface), var(--transparent-opacity-light));
|
||||
}
|
||||
|
||||
.theme-customizer-preview-option.is-active .theme-customizer-radius-scene {
|
||||
background: rgba(var(--v-theme-primary), 0.04);
|
||||
}
|
||||
|
||||
.theme-customizer-radius-scene__card {
|
||||
background: rgba(var(--v-theme-surface), var(--transparent-opacity));
|
||||
}
|
||||
|
||||
// 智能助手面板
|
||||
.agent-assistant-panel {
|
||||
backdrop-filter: blur(var(--transparent-blur-heavy));
|
||||
|
||||
Reference in New Issue
Block a user