fix: expose open app theme switch in settings
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
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 = [
|
||||
{
|
||||
@@ -36,3 +37,16 @@ export const getInitialOpenAppTheme = (): OpenAppTheme => {
|
||||
? storedTheme
|
||||
: 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,
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
import BackgroundImagePanel from '@/components/business-component/ThemeConfig/BackgroundImagePanel';
|
||||
import NavigationStylePanel from '@/components/business-component/ThemeConfig/NavigationStylePanel';
|
||||
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 { useUnifiedTheme } from '@/hooks/useUnifiedTheme';
|
||||
import { dict } from '@/services/i18nRuntime';
|
||||
@@ -21,7 +27,7 @@ import { ThemeLayoutColorStyle } from '@/types/enums/theme';
|
||||
import { TenantThemeConfig } from '@/types/tenant';
|
||||
import { message } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import styles from './ThemeSwitchPanel.less';
|
||||
|
||||
const cx = classNames.bind(styles);
|
||||
@@ -56,6 +62,9 @@ const ThemeSwitchPanel: React.FC<ThemeSwitchPanelProps> = ({
|
||||
updateNavigationStyle,
|
||||
toggleNavigationTheme,
|
||||
} = useUnifiedTheme();
|
||||
const [openAppTheme, setOpenAppTheme] = useState<OpenAppTheme>(
|
||||
getInitialOpenAppTheme,
|
||||
);
|
||||
|
||||
// 转换背景配置为组件需要的格式
|
||||
const backgroundImages: BackgroundImage[] = useMemo(() => {
|
||||
@@ -183,6 +192,12 @@ const ThemeSwitchPanel: React.FC<ThemeSwitchPanelProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenAppThemeChange = (theme: OpenAppTheme) => {
|
||||
setOpenAppTheme(theme);
|
||||
setOpenAppThemePreference(theme);
|
||||
setHasUserSwitchThemeFlag();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cx(styles.container)}>
|
||||
<div className={cx(styles.title)}>
|
||||
@@ -209,6 +224,40 @@ const ThemeSwitchPanel: React.FC<ThemeSwitchPanelProps> = ({
|
||||
/>
|
||||
</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)}>
|
||||
<BackgroundImagePanel
|
||||
backgroundImages={backgroundImages}
|
||||
|
||||
@@ -6,6 +6,13 @@ import ConditionRender from '@/components/ConditionRender';
|
||||
import TooltipIcon from '@/components/custom/TooltipIcon';
|
||||
import { EVENT_TYPE } from '@/constants/event.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 User from '@/layouts/DynamicMenusLayout/User';
|
||||
import Message from '@/layouts/Message';
|
||||
@@ -37,12 +44,6 @@ import {
|
||||
} from 'umi';
|
||||
import ConversationItem from './ConversationItem';
|
||||
import styles from './index.less';
|
||||
import {
|
||||
OPEN_APP_THEME_STORAGE_KEY,
|
||||
OpenAppTheme,
|
||||
getInitialOpenAppTheme,
|
||||
openAppThemes,
|
||||
} from './openAppTheme';
|
||||
|
||||
// 绑定 classNames,便于动态样式组合
|
||||
const cx = classNames.bind(styles);
|
||||
@@ -174,8 +175,23 @@ const BaseTemplate: React.FC = () => {
|
||||
}, [agentId]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(OPEN_APP_THEME_STORAGE_KEY, openAppTheme);
|
||||
}, [openAppTheme]);
|
||||
const handleOpenAppThemeChange = (event: Event) => {
|
||||
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, {
|
||||
@@ -522,7 +538,10 @@ const BaseTemplate: React.FC = () => {
|
||||
})}
|
||||
title={theme.title}
|
||||
aria-pressed={openAppTheme === theme.key}
|
||||
onClick={() => setOpenAppTheme(theme.key)}
|
||||
onClick={() => {
|
||||
setOpenAppTheme(theme.key);
|
||||
setOpenAppThemePreference(theme.key);
|
||||
}}
|
||||
>
|
||||
{theme.label}
|
||||
</button>
|
||||
|
||||
@@ -3,7 +3,8 @@ import {
|
||||
getInitialOpenAppTheme,
|
||||
isOpenAppTheme,
|
||||
openAppThemes,
|
||||
} from '@/pages/OpenApp/BaseTemplate/openAppTheme';
|
||||
setOpenAppThemePreference,
|
||||
} from '@/constants/openAppTheme.constants';
|
||||
|
||||
describe('openAppTheme', () => {
|
||||
beforeEach(() => {
|
||||
@@ -42,4 +43,11 @@ describe('openAppTheme', () => {
|
||||
localStorage.setItem(OPEN_APP_THEME_STORAGE_KEY, 'unknown');
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user