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_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,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user