fix: expose open app theme switch in settings

This commit is contained in:
Codex
2026-05-29 15:03:17 +08:00
parent 4ed69b0c78
commit 45202121a5
5 changed files with 231 additions and 11 deletions

View File

@@ -1,4 +1,5 @@
export const OPEN_APP_THEME_STORAGE_KEY = 'qiming-open-app-theme'; export const OPEN_APP_THEME_STORAGE_KEY = 'qiming-open-app-theme';
export const OPEN_APP_THEME_CHANGED_EVENT = 'qiming-open-app-theme-changed';
export const openAppThemes = [ export const openAppThemes = [
{ {
@@ -36,3 +37,16 @@ export const getInitialOpenAppTheme = (): OpenAppTheme => {
? storedTheme ? storedTheme
: DEFAULT_OPEN_APP_THEME; : DEFAULT_OPEN_APP_THEME;
}; };
export const setOpenAppThemePreference = (theme: OpenAppTheme) => {
if (typeof window === 'undefined') {
return;
}
window.localStorage.setItem(OPEN_APP_THEME_STORAGE_KEY, theme);
window.dispatchEvent(
new CustomEvent<OpenAppTheme>(OPEN_APP_THEME_CHANGED_EVENT, {
detail: theme,
}),
);
};

View File

@@ -34,3 +34,133 @@
} }
} }
} }
.panelTitle {
margin: @padding 0;
font-size: @fontSize;
font-weight: @fontWeightStrong;
color: @colorText;
}
.openAppThemePanel {
h4 {
margin: 0 0 @marginSm 0;
font-size: @fontSizeSm;
color: @colorTextSecondary;
font-weight: normal;
}
}
.openAppThemeOptions {
display: grid;
grid-template-columns: repeat(3, minmax(120px, 172px));
gap: @paddingSm;
}
.openAppThemeOption {
display: flex;
flex-direction: column;
align-items: center;
gap: @marginXxs;
min-width: 0;
padding: @paddingXs;
border: 2px solid transparent;
border-radius: @borderRadiusLg;
background: transparent;
color: @colorTextSecondary;
cursor: pointer;
transition: border-color 0.3s @motionEaseOut, color 0.3s @motionEaseOut;
&:hover,
&.active {
border-color: @colorPrimary;
color: @colorText;
}
}
.openAppThemePreview {
display: flex;
width: 116px;
height: 64px;
overflow: hidden;
border: 1px solid rgba(0, 101, 105, 18%);
border-radius: @borderRadiusSm;
background: #fff;
}
.previewRail {
width: 28px;
background: #006569;
}
.previewBody {
flex: 1;
padding: @paddingXs;
background: #fbfffd;
span {
display: block;
width: 46px;
height: 7px;
margin-bottom: 6px;
border-radius: 999px;
background: rgba(0, 101, 105, 22%);
&:first-child {
width: 60px;
background: rgba(0, 101, 105, 42%);
}
}
}
.dispatch {
.openAppThemePreview {
border-color: rgba(126, 232, 196, 36%);
background: #003f42;
}
.previewRail {
background: #002d30;
}
.previewBody {
background: linear-gradient(180deg, rgba(0, 166, 122, 22%), #003f42);
span {
background: rgba(242, 255, 252, 28%);
&:first-child {
background: #00a67a;
}
}
}
}
.light {
}
.portal {
.openAppThemePreview {
border-top: 4px solid #006569;
background: #f4fbf8;
}
.previewRail {
background: #ffffff;
border-right: 1px solid rgba(0, 101, 105, 18%);
}
.previewBody {
background: #f4fbf8;
}
}
.openAppThemeLabel {
max-width: 132px;
overflow: hidden;
font-size: @fontSizeSm;
line-height: @lineHeight;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@@ -12,6 +12,12 @@
import BackgroundImagePanel from '@/components/business-component/ThemeConfig/BackgroundImagePanel'; import BackgroundImagePanel from '@/components/business-component/ThemeConfig/BackgroundImagePanel';
import NavigationStylePanel from '@/components/business-component/ThemeConfig/NavigationStylePanel'; import NavigationStylePanel from '@/components/business-component/ThemeConfig/NavigationStylePanel';
import ThemeColorPanel from '@/components/business-component/ThemeConfig/ThemeColorPanel'; import ThemeColorPanel from '@/components/business-component/ThemeConfig/ThemeColorPanel';
import {
OpenAppTheme,
getInitialOpenAppTheme,
openAppThemes,
setOpenAppThemePreference,
} from '@/constants/openAppTheme.constants';
import { backgroundConfigs, STORAGE_KEYS } from '@/constants/theme.constants'; import { backgroundConfigs, STORAGE_KEYS } from '@/constants/theme.constants';
import { useUnifiedTheme } from '@/hooks/useUnifiedTheme'; import { useUnifiedTheme } from '@/hooks/useUnifiedTheme';
import { dict } from '@/services/i18nRuntime'; import { dict } from '@/services/i18nRuntime';
@@ -21,7 +27,7 @@ import { ThemeLayoutColorStyle } from '@/types/enums/theme';
import { TenantThemeConfig } from '@/types/tenant'; import { TenantThemeConfig } from '@/types/tenant';
import { message } from 'antd'; import { message } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import styles from './ThemeSwitchPanel.less'; import styles from './ThemeSwitchPanel.less';
const cx = classNames.bind(styles); const cx = classNames.bind(styles);
@@ -56,6 +62,9 @@ const ThemeSwitchPanel: React.FC<ThemeSwitchPanelProps> = ({
updateNavigationStyle, updateNavigationStyle,
toggleNavigationTheme, toggleNavigationTheme,
} = useUnifiedTheme(); } = useUnifiedTheme();
const [openAppTheme, setOpenAppTheme] = useState<OpenAppTheme>(
getInitialOpenAppTheme,
);
// 转换背景配置为组件需要的格式 // 转换背景配置为组件需要的格式
const backgroundImages: BackgroundImage[] = useMemo(() => { const backgroundImages: BackgroundImage[] = useMemo(() => {
@@ -183,6 +192,12 @@ const ThemeSwitchPanel: React.FC<ThemeSwitchPanelProps> = ({
} }
}; };
const handleOpenAppThemeChange = (theme: OpenAppTheme) => {
setOpenAppTheme(theme);
setOpenAppThemePreference(theme);
setHasUserSwitchThemeFlag();
};
return ( return (
<div className={cx(styles.container)}> <div className={cx(styles.container)}>
<div className={cx(styles.title)}> <div className={cx(styles.title)}>
@@ -209,6 +224,40 @@ const ThemeSwitchPanel: React.FC<ThemeSwitchPanelProps> = ({
/> />
</div> </div>
<div className={cx(styles.configItem)}>
<div className={cx(styles.openAppThemePanel)}>
<h3 className={cx(styles.panelTitle)}></h3>
<h4>绿</h4>
<div className={cx(styles.openAppThemeOptions)}>
{openAppThemes.map((theme) => (
<button
key={theme.key}
type="button"
className={cx(styles.openAppThemeOption, {
[styles.active]: openAppTheme === theme.key,
[styles[theme.key]]: true,
})}
title={theme.title}
aria-pressed={openAppTheme === theme.key}
onClick={() => handleOpenAppThemeChange(theme.key)}
>
<span className={cx(styles.openAppThemePreview)}>
<span className={cx(styles.previewRail)} />
<span className={cx(styles.previewBody)}>
<span />
<span />
<span />
</span>
</span>
<span className={cx(styles.openAppThemeLabel)}>
{theme.title}
</span>
</button>
))}
</div>
</div>
</div>
<div className={cx(styles.configItem)}> <div className={cx(styles.configItem)}>
<BackgroundImagePanel <BackgroundImagePanel
backgroundImages={backgroundImages} backgroundImages={backgroundImages}

View File

@@ -6,6 +6,13 @@ import ConditionRender from '@/components/ConditionRender';
import TooltipIcon from '@/components/custom/TooltipIcon'; import TooltipIcon from '@/components/custom/TooltipIcon';
import { EVENT_TYPE } from '@/constants/event.constants'; import { EVENT_TYPE } from '@/constants/event.constants';
import { ANIMATION_DURATION } from '@/constants/layout.constants'; import { ANIMATION_DURATION } from '@/constants/layout.constants';
import {
OPEN_APP_THEME_CHANGED_EVENT,
OpenAppTheme,
getInitialOpenAppTheme,
openAppThemes,
setOpenAppThemePreference,
} from '@/constants/openAppTheme.constants';
import useSubscription from '@/hooks/useSubscription'; import useSubscription from '@/hooks/useSubscription';
import User from '@/layouts/DynamicMenusLayout/User'; import User from '@/layouts/DynamicMenusLayout/User';
import Message from '@/layouts/Message'; import Message from '@/layouts/Message';
@@ -37,12 +44,6 @@ import {
} from 'umi'; } from 'umi';
import ConversationItem from './ConversationItem'; import ConversationItem from './ConversationItem';
import styles from './index.less'; import styles from './index.less';
import {
OPEN_APP_THEME_STORAGE_KEY,
OpenAppTheme,
getInitialOpenAppTheme,
openAppThemes,
} from './openAppTheme';
// 绑定 classNames便于动态样式组合 // 绑定 classNames便于动态样式组合
const cx = classNames.bind(styles); const cx = classNames.bind(styles);
@@ -174,8 +175,23 @@ const BaseTemplate: React.FC = () => {
}, [agentId]); }, [agentId]);
useEffect(() => { useEffect(() => {
localStorage.setItem(OPEN_APP_THEME_STORAGE_KEY, openAppTheme); const handleOpenAppThemeChange = (event: Event) => {
}, [openAppTheme]); const nextTheme = (event as CustomEvent<OpenAppTheme>).detail;
setOpenAppTheme(nextTheme);
};
window.addEventListener(
OPEN_APP_THEME_CHANGED_EVENT,
handleOpenAppThemeChange,
);
return () => {
window.removeEventListener(
OPEN_APP_THEME_CHANGED_EVENT,
handleOpenAppThemeChange,
);
};
}, []);
// 已发布的智能体详情接口 // 已发布的智能体详情接口
const { run: runDetail } = useRequest(apiPublishedAgentInfo, { const { run: runDetail } = useRequest(apiPublishedAgentInfo, {
@@ -522,7 +538,10 @@ const BaseTemplate: React.FC = () => {
})} })}
title={theme.title} title={theme.title}
aria-pressed={openAppTheme === theme.key} aria-pressed={openAppTheme === theme.key}
onClick={() => setOpenAppTheme(theme.key)} onClick={() => {
setOpenAppTheme(theme.key);
setOpenAppThemePreference(theme.key);
}}
> >
{theme.label} {theme.label}
</button> </button>

View File

@@ -3,7 +3,8 @@ import {
getInitialOpenAppTheme, getInitialOpenAppTheme,
isOpenAppTheme, isOpenAppTheme,
openAppThemes, openAppThemes,
} from '@/pages/OpenApp/BaseTemplate/openAppTheme'; setOpenAppThemePreference,
} from '@/constants/openAppTheme.constants';
describe('openAppTheme', () => { describe('openAppTheme', () => {
beforeEach(() => { beforeEach(() => {
@@ -42,4 +43,11 @@ describe('openAppTheme', () => {
localStorage.setItem(OPEN_APP_THEME_STORAGE_KEY, 'unknown'); localStorage.setItem(OPEN_APP_THEME_STORAGE_KEY, 'unknown');
expect(getInitialOpenAppTheme()).toBe('dispatch'); expect(getInitialOpenAppTheme()).toBe('dispatch');
}); });
test('stores theme preference through the shared setter', () => {
setOpenAppThemePreference('light');
expect(localStorage.getItem(OPEN_APP_THEME_STORAGE_KEY)).toBe('light');
expect(getInitialOpenAppTheme()).toBe('light');
});
}); });