feat: add theme-aware logo support via optional sourceDark configuration (#6866)

* Initial plan

* Initial exploration and setup

Co-authored-by: aonoa <32682251+aonoa@users.noreply.github.com>

* Add support for separate light and dark theme logos

Co-authored-by: aonoa <32682251+aonoa@users.noreply.github.com>

* Update documentation with dark theme logo configuration

Co-authored-by: aonoa <32682251+aonoa@users.noreply.github.com>

* feat: Add theme-aware logo support for authentication/login page

Co-authored-by: aonoa <32682251+aonoa@users.noreply.github.com>

* revert: .npmrc

Signed-off-by: aonoa <1991849113@qq.com>

---------

Signed-off-by: aonoa <1991849113@qq.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: aonoa <32682251+aonoa@users.noreply.github.com>
Co-authored-by: Li Kui <90845831+likui628@users.noreply.github.com>
Co-authored-by: aonoa <1991849113@qq.com>
This commit is contained in:
Copilot 2025-11-03 13:07:36 +08:00 committed by GitHub
parent f8a7a0a9a2
commit c7c39047de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 76 additions and 14 deletions

2
.npmrc
View File

@ -1,4 +1,4 @@
registry = "https://registry.npmmirror.com" registry=https://registry.npmmirror.com
public-hoist-pattern[]=lefthook public-hoist-pattern[]=lefthook
public-hoist-pattern[]=eslint public-hoist-pattern[]=eslint
public-hoist-pattern[]=prettier public-hoist-pattern[]=prettier

View File

@ -8,12 +8,14 @@ import { $t } from '#/locales';
const appName = computed(() => preferences.app.name); const appName = computed(() => preferences.app.name);
const logo = computed(() => preferences.logo.source); const logo = computed(() => preferences.logo.source);
const logoDark = computed(() => preferences.logo.sourceDark);
</script> </script>
<template> <template>
<AuthPageLayout <AuthPageLayout
:app-name="appName" :app-name="appName"
:logo="logo" :logo="logo"
:logo-dark="logoDark"
:page-description="$t('authentication.pageDesc')" :page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')" :page-title="$t('authentication.pageTitle')"
> >

View File

@ -8,12 +8,14 @@ import { $t } from '#/locales';
const appName = computed(() => preferences.app.name); const appName = computed(() => preferences.app.name);
const logo = computed(() => preferences.logo.source); const logo = computed(() => preferences.logo.source);
const logoDark = computed(() => preferences.logo.sourceDark);
</script> </script>
<template> <template>
<AuthPageLayout <AuthPageLayout
:app-name="appName" :app-name="appName"
:logo="logo" :logo="logo"
:logo-dark="logoDark"
:page-description="$t('authentication.pageDesc')" :page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')" :page-title="$t('authentication.pageTitle')"
> >

View File

@ -8,12 +8,14 @@ import { $t } from '#/locales';
const appName = computed(() => preferences.app.name); const appName = computed(() => preferences.app.name);
const logo = computed(() => preferences.logo.source); const logo = computed(() => preferences.logo.source);
const logoDark = computed(() => preferences.logo.sourceDark);
</script> </script>
<template> <template>
<AuthPageLayout <AuthPageLayout
:app-name="appName" :app-name="appName"
:logo="logo" :logo="logo"
:logo-dark="logoDark"
:page-description="$t('authentication.pageDesc')" :page-description="$t('authentication.pageDesc')"
:page-title="$t('authentication.pageTitle')" :page-title="$t('authentication.pageTitle')"
> >

View File

@ -261,6 +261,7 @@ const defaultPreferences: Preferences = {
enable: true, enable: true,
fit: 'contain', fit: 'contain',
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp', source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
// sourceDark: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-dark.webp', // Optional: Dark theme logo
}, },
navigation: { navigation: {
accordion: true, accordion: true,
@ -457,6 +458,8 @@ interface LogoPreferences {
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'; fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
/** Logo URL */ /** Logo URL */
source: string; source: string;
/** Dark theme logo URL (optional, if not set, use source) */
sourceDark?: string;
} }
interface NavigationPreferences { interface NavigationPreferences {

View File

@ -260,6 +260,7 @@ const defaultPreferences: Preferences = {
enable: true, enable: true,
fit: 'contain', fit: 'contain',
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp', source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
// sourceDark: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-dark.webp', // 可选暗色主题logo
}, },
navigation: { navigation: {
accordion: true, accordion: true,
@ -457,6 +458,8 @@ interface LogoPreferences {
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'; fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
/** logo地址 */ /** logo地址 */
source: string; source: string;
/** 暗色主题logo地址 (可选,若不设置则使用 source) */
sourceDark?: string;
} }
interface NavigationPreferences { interface NavigationPreferences {

View File

@ -1,6 +1,6 @@
import dayjs from "dayjs"; import dayjs from 'dayjs';
import timezone from "dayjs/plugin/timezone"; import timezone from 'dayjs/plugin/timezone';
import utc from "dayjs/plugin/utc"; import utc from 'dayjs/plugin/utc';
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);

View File

@ -146,6 +146,8 @@ interface LogoPreferences {
fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'; fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
/** logo地址 */ /** logo地址 */
source: string; source: string;
/** 暗色主题logo地址 (可选,若不设置则使用 source) */
sourceDark?: string;
} }
interface NavigationPreferences { interface NavigationPreferences {

View File

@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue';
import { VbenAvatar } from '../avatar'; import { VbenAvatar } from '../avatar';
interface Props { interface Props {
@ -22,6 +24,10 @@ interface Props {
* @zh_CN Logo 图标 * @zh_CN Logo 图标
*/ */
src?: string; src?: string;
/**
* @zh_CN 暗色主题 Logo 图标 (可选若不设置则使用 src)
*/
srcDark?: string;
/** /**
* @zh_CN Logo 文本 * @zh_CN Logo 文本
*/ */
@ -36,14 +42,27 @@ defineOptions({
name: 'VbenLogo', name: 'VbenLogo',
}); });
withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
collapsed: false, collapsed: false,
href: 'javascript:void 0', href: 'javascript:void 0',
logoSize: 32, logoSize: 32,
src: '', src: '',
srcDark: '',
theme: 'light', theme: 'light',
fit: 'cover', fit: 'cover',
}); });
/**
* @zh_CN 根据主题选择合适的 logo 图标
*/
const logoSrc = computed(() => {
// srcDark使 logo
if (props.theme === 'dark' && props.srcDark) {
return props.srcDark;
}
// 使 src
return props.src;
});
</script> </script>
<template> <template>
@ -54,9 +73,9 @@ withDefaults(defineProps<Props>(), {
class="flex h-full items-center gap-2 overflow-hidden px-3 text-lg leading-normal transition-all duration-500" class="flex h-full items-center gap-2 overflow-hidden px-3 text-lg leading-normal transition-all duration-500"
> >
<VbenAvatar <VbenAvatar
v-if="src" v-if="logoSrc"
:alt="text" :alt="text"
:src="src" :src="logoSrc"
:size="logoSize" :size="logoSize"
:fit="fit" :fit="fit"
class="relative rounded-none bg-transparent" class="relative rounded-none bg-transparent"

View File

@ -1,6 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ToolbarType } from './types'; import type { ToolbarType } from './types';
import { computed } from 'vue';
import { preferences, usePreferences } from '@vben/preferences'; import { preferences, usePreferences } from '@vben/preferences';
import { Copyright } from '../basic/copyright'; import { Copyright } from '../basic/copyright';
@ -11,6 +13,7 @@ import Toolbar from './toolbar.vue';
interface Props { interface Props {
appName?: string; appName?: string;
logo?: string; logo?: string;
logoDark?: string;
pageTitle?: string; pageTitle?: string;
pageDescription?: string; pageDescription?: string;
sloganImage?: string; sloganImage?: string;
@ -20,10 +23,11 @@ interface Props {
clickLogo?: () => void; clickLogo?: () => void;
} }
withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
appName: '', appName: '',
copyright: true, copyright: true,
logo: '', logo: '',
logoDark: '',
pageDescription: '', pageDescription: '',
pageTitle: '', pageTitle: '',
sloganImage: '', sloganImage: '',
@ -34,6 +38,18 @@ withDefaults(defineProps<Props>(), {
const { authPanelCenter, authPanelLeft, authPanelRight, isDark } = const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
usePreferences(); usePreferences();
/**
* @zh_CN 根据主题选择合适的 logo 图标
*/
const logoSrc = computed(() => {
// logoDark使 logo
if (isDark.value && props.logoDark) {
return props.logoDark;
}
// 使 logo
return props.logo;
});
</script> </script>
<template> <template>
@ -65,14 +81,21 @@ const { authPanelCenter, authPanelLeft, authPanelRight, isDark } =
<slot name="logo"> <slot name="logo">
<!-- 头部 Logo 和应用名称 --> <!-- 头部 Logo 和应用名称 -->
<div <div
v-if="logo || appName" v-if="logoSrc || appName"
class="absolute left-0 top-0 z-10 flex flex-1" class="absolute left-0 top-0 z-10 flex flex-1"
@click="clickLogo" @click="clickLogo"
> >
<div <div
class="text-foreground lg:text-foreground ml-4 mt-4 flex flex-1 items-center sm:left-6 sm:top-6" class="text-foreground lg:text-foreground ml-4 mt-4 flex flex-1 items-center sm:left-6 sm:top-6"
> >
<img v-if="logo" :alt="appName" :src="logo" class="mr-2" width="42" /> <img
v-if="logoSrc"
:key="logoSrc"
:alt="appName"
:src="logoSrc"
class="mr-2"
width="42"
/>
<p v-if="appName" class="m-0 text-xl font-medium"> <p v-if="appName" class="m-0 text-xl font-medium">
{{ appName }} {{ appName }}
</p> </p>

View File

@ -259,6 +259,7 @@ const headerSlots = computed(() => {
:class="logoClass" :class="logoClass"
:collapsed="logoCollapsed" :collapsed="logoCollapsed"
:src="preferences.logo.source" :src="preferences.logo.source"
:src-dark="preferences.logo.sourceDark"
:text="preferences.app.name" :text="preferences.app.name"
:theme="showHeaderNav ? headerTheme : theme" :theme="showHeaderNav ? headerTheme : theme"
@click="clickLogo" @click="clickLogo"
@ -350,6 +351,8 @@ const headerSlots = computed(() => {
<VbenLogo <VbenLogo
v-if="preferences.logo.enable" v-if="preferences.logo.enable"
:fit="preferences.logo.fit" :fit="preferences.logo.fit"
:src="preferences.logo.source"
:src-dark="preferences.logo.sourceDark"
:text="preferences.app.name" :text="preferences.app.name"
:theme="theme" :theme="theme"
> >

View File

@ -1,9 +1,12 @@
import { ref, unref } from "vue"; import { ref, unref } from 'vue';
import { DEFAULT_TIME_ZONE_OPTIONS } from "@vben-core/preferences"; import { DEFAULT_TIME_ZONE_OPTIONS } from '@vben-core/preferences';
import { getCurrentTimezone, setCurrentTimezone } from "@vben-core/shared/utils"; import {
getCurrentTimezone,
setCurrentTimezone,
} from '@vben-core/shared/utils';
import { acceptHMRUpdate, defineStore } from "pinia"; import { acceptHMRUpdate, defineStore } from 'pinia';
interface TimezoneHandler { interface TimezoneHandler {
getTimezone?: () => Promise<null | string | undefined>; getTimezone?: () => Promise<null | string | undefined>;