feat: add state grid open app themes

This commit is contained in:
Codex
2026-05-29 14:47:53 +08:00
parent bfd67a0f2c
commit 4ed69b0c78
6 changed files with 315 additions and 33 deletions

View File

@@ -20,6 +20,10 @@ import { FIRST_MENU_WIDTH_STYLE2 } from './layout.constants';
* 定义可选择的主题色选项
*/
export const THEME_COLOR_CONFIGS = [
{
color: '#006569',
name: '国网绿',
},
{
color: '#5147ff',
name: dict('PC.Constants.Theme.colorBlue'),

View File

@@ -5,38 +5,37 @@
padding: @paddingXs;
margin-top: @marginXxs;
cursor: pointer;
color: @navSecondMenuTextSecondary;
color: var(--open-app-sidebar-text-secondary, @navSecondMenuTextSecondary);
border-radius: @borderRadius;
transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
&.first {
margin-top: 0;
}
.name {
color: @navSecondMenuTextSecondary;
color: var(--open-app-sidebar-text-secondary, @navSecondMenuTextSecondary);
font-weight: 400;
font-size: @fontSize;
}
&.active {
background-color: rgba(0, 0, 0, 5%);
color: rgba(0, 0, 0, 90%);
border-radius: @borderRadius;
box-shadow: @boxShadowTertiary;
background-color: var(--open-app-sidebar-active-bg, rgba(0, 0, 0, 5%));
color: var(--open-app-sidebar-active-text, rgba(0, 0, 0, 90%));
box-shadow: var(--open-app-sidebar-control-shadow, @boxShadowTertiary);
.name {
color: rgba(0, 0, 0, 90%);
color: var(--open-app-sidebar-active-text, rgba(0, 0, 0, 90%));
}
}
&:hover:not(.active) {
background-color: rgba(0, 0, 0, 5%);
border-radius: @borderRadius;
box-shadow: @boxShadowTertiary;
color: @colorTextSecondary;
background-color: var(--open-app-sidebar-hover, rgba(0, 0, 0, 5%));
color: var(--open-app-sidebar-text, @colorTextSecondary);
}
.status-text {
font-size: @fontSizeSm;
color: @colorTextTertiary;
color: var(--open-app-sidebar-muted, @colorTextTertiary);
}
}

View File

@@ -3,6 +3,83 @@
.container {
display: flex;
height: 100%;
--open-app-sidebar-bg: #ffffff;
--open-app-sidebar-border: rgba(0, 101, 105, 12%);
--open-app-sidebar-text: #183638;
--open-app-sidebar-text-secondary: rgba(24, 54, 56, 68%);
--open-app-sidebar-muted: rgba(24, 54, 56, 52%);
--open-app-sidebar-hover: rgba(0, 101, 105, 8%);
--open-app-sidebar-active-bg: rgba(0, 101, 105, 12%);
--open-app-sidebar-active-text: #006569;
--open-app-sidebar-accent: #006569;
--open-app-sidebar-accent-2: #00a67a;
--open-app-sidebar-control-bg: #ffffff;
--open-app-sidebar-control-border: rgba(0, 101, 105, 20%);
--open-app-sidebar-control-shadow: 0 8px 24px rgba(0, 101, 105, 10%);
--open-app-sidebar-footer-bg: rgba(0, 101, 105, 6%);
--open-app-sidebar-gradient-end: #ffffff;
--open-app-theme-switch-bg: rgba(0, 101, 105, 8%);
--open-app-theme-switch-active-bg: #006569;
--open-app-theme-switch-active-text: #ffffff;
}
.theme-light {
--open-app-sidebar-bg: #fbfffd;
--open-app-sidebar-border: rgba(0, 101, 105, 16%);
--open-app-sidebar-text: #183638;
--open-app-sidebar-text-secondary: rgba(24, 54, 56, 68%);
--open-app-sidebar-muted: rgba(24, 54, 56, 52%);
--open-app-sidebar-hover: rgba(0, 101, 105, 8%);
--open-app-sidebar-active-bg: rgba(0, 101, 105, 12%);
--open-app-sidebar-active-text: #006569;
--open-app-sidebar-control-bg: #ffffff;
--open-app-sidebar-control-border: rgba(0, 101, 105, 22%);
--open-app-sidebar-control-shadow: 0 10px 26px rgba(0, 101, 105, 9%);
--open-app-sidebar-footer-bg: rgba(0, 101, 105, 6%);
--open-app-sidebar-gradient-end: #fbfffd;
--open-app-theme-switch-bg: rgba(0, 101, 105, 8%);
--open-app-theme-switch-active-bg: #006569;
--open-app-theme-switch-active-text: #ffffff;
}
.theme-dispatch {
--open-app-sidebar-bg: #003f42;
--open-app-sidebar-border: rgba(128, 236, 208, 26%);
--open-app-sidebar-text: #f2fffc;
--open-app-sidebar-text-secondary: rgba(242, 255, 252, 76%);
--open-app-sidebar-muted: rgba(242, 255, 252, 56%);
--open-app-sidebar-hover: rgba(255, 255, 255, 10%);
--open-app-sidebar-active-bg: rgba(0, 166, 122, 22%);
--open-app-sidebar-active-text: #ffffff;
--open-app-sidebar-accent: #00a67a;
--open-app-sidebar-accent-2: #7ee8c4;
--open-app-sidebar-control-bg: rgba(255, 255, 255, 10%);
--open-app-sidebar-control-border: rgba(126, 232, 196, 32%);
--open-app-sidebar-control-shadow: 0 18px 42px rgba(0, 22, 24, 32%);
--open-app-sidebar-footer-bg: rgba(0, 34, 36, 48%);
--open-app-sidebar-gradient-end: #003f42;
--open-app-theme-switch-bg: rgba(255, 255, 255, 10%);
--open-app-theme-switch-active-bg: #00a67a;
--open-app-theme-switch-active-text: #003f42;
}
.theme-portal {
--open-app-sidebar-bg: #f4fbf8;
--open-app-sidebar-border: rgba(0, 101, 105, 20%);
--open-app-sidebar-text: #113436;
--open-app-sidebar-text-secondary: rgba(17, 52, 54, 70%);
--open-app-sidebar-muted: rgba(17, 52, 54, 54%);
--open-app-sidebar-hover: rgba(0, 166, 122, 10%);
--open-app-sidebar-active-bg: #ffffff;
--open-app-sidebar-active-text: #006569;
--open-app-sidebar-control-bg: #ffffff;
--open-app-sidebar-control-border: rgba(0, 101, 105, 18%);
--open-app-sidebar-control-shadow: 0 12px 30px rgba(0, 101, 105, 12%);
--open-app-sidebar-footer-bg: #ffffff;
--open-app-sidebar-gradient-end: #f4fbf8;
--open-app-theme-switch-bg: #ffffff;
--open-app-theme-switch-active-bg: #006569;
--open-app-theme-switch-active-text: #ffffff;
}
// 应用智能体侧边栏容器
@@ -10,8 +87,9 @@
width: 250px;
height: 100%;
padding: @paddingSm @paddingXs;
border-right: @colorBorderSecondary 1px solid;
background-color: @colorBgContainerDisabled;
border-right: 1px solid var(--open-app-sidebar-border);
background: var(--open-app-sidebar-bg);
color: var(--open-app-sidebar-text);
display: flex;
flex-direction: column;
gap: @paddingSm;
@@ -43,7 +121,8 @@
width: 26px;
height: 26px;
border-radius: @borderRadius;
background-color: @colorBgContainer;
background-color: var(--open-app-sidebar-control-bg);
border: 1px solid var(--open-app-sidebar-control-border);
overflow: hidden;
flex-shrink: 0;
@@ -59,6 +138,7 @@
height: 16px;
margin-left: auto;
border-radius: 50%;
color: var(--open-app-sidebar-text-secondary);
}
}
@@ -66,9 +146,9 @@
.newSessionBtn {
width: 100%;
height: @controlHeightLg;
border: @lineWidth solid @colorBorder;
border: @lineWidth solid var(--open-app-sidebar-control-border);
border-radius: @borderRadiusLg;
background-color: @colorBgContainer;
background: var(--open-app-sidebar-control-bg);
padding: 0 @paddingSm;
display: flex;
align-items: center;
@@ -77,14 +157,15 @@
transition: box-shadow 0.2s ease-in-out, background-color 0.2s ease-in-out;
&:hover {
box-shadow: @boxShadowSecondary;
box-shadow: var(--open-app-sidebar-control-shadow);
border-color: var(--open-app-sidebar-accent);
}
.newSessionText {
display: inline-flex;
align-items: center;
gap: 8px;
color: @colorText;
color: var(--open-app-sidebar-text);
font-size: @fontSize;
font-weight: 400;
user-select: none;
@@ -97,12 +178,44 @@
height: 22px;
padding: 0 @paddingXs;
border-radius: @borderRadiusOuter;
background: @colorBgLayout;
color: @colorTextDescription;
background: var(--open-app-sidebar-hover);
color: var(--open-app-sidebar-text-secondary);
font-size: @fontSizeSm;
}
}
.themeSwitcher {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2px;
padding: 3px;
border: 1px solid var(--open-app-sidebar-control-border);
border-radius: @borderRadiusLg;
background: var(--open-app-theme-switch-bg);
}
.themeOption {
min-width: 0;
height: 28px;
border: 0;
border-radius: @borderRadius;
background: transparent;
color: var(--open-app-sidebar-text-secondary);
font-size: @fontSizeSm;
cursor: pointer;
transition: color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
&:hover {
color: var(--open-app-sidebar-text);
}
&.themeOptionActive {
background: var(--open-app-theme-switch-active-bg);
color: var(--open-app-theme-switch-active-text);
box-shadow: 0 6px 14px rgba(0, 0, 0, 12%);
}
}
// ========================== 导航 ==========================
.pageNavList {
display: flex;
@@ -119,6 +232,7 @@
align-items: center;
gap: 8px;
color: @colorText;
color: var(--open-app-sidebar-text);
cursor: pointer;
user-select: none;
transition: background-color 0.2s ease, color 0.2s ease;
@@ -129,11 +243,24 @@
}
&:hover {
background-color: #e6e7e8;
background-color: var(--open-app-sidebar-hover);
}
&.page-nav-item-active {
background-color: #e6e7e8;
position: relative;
background-color: var(--open-app-sidebar-active-bg);
color: var(--open-app-sidebar-active-text);
box-shadow: var(--open-app-sidebar-control-shadow);
&::before {
position: absolute;
left: 5px;
width: 3px;
height: 16px;
border-radius: 999px;
background: var(--open-app-sidebar-accent-2);
content: '';
}
}
}
@@ -156,6 +283,7 @@
align-items: center;
gap: 8px;
color: @colorText;
color: var(--open-app-sidebar-text);
font-size: @fontSize;
font-weight: 400;
}
@@ -164,7 +292,7 @@
.more-conversation {
line-height: @lineHeight;
padding: 0;
color: @navSecondMenuTextSecondary;
color: var(--open-app-sidebar-text-secondary);
font-size: @fontSizeSm;
cursor: pointer;
transition: color 0.3s ease;
@@ -172,7 +300,7 @@
user-select: none;
&:hover {
color: @navSecondMenuTextSecondary;
color: var(--open-app-sidebar-text);
}
}
}
@@ -182,7 +310,7 @@
.no-used {
margin-left: 6px;
color: @colorTextDescription;
color: var(--open-app-sidebar-muted);
}
}
@@ -196,7 +324,7 @@
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0%) 0%,
#f9fafb 80.65%
var(--open-app-sidebar-gradient-end) 80.65%
);
pointer-events: none;
z-index: 1;
@@ -206,12 +334,13 @@
.user-area {
padding: @paddingXxs @paddingXs;
border-radius: @borderRadiusLg;
background: var(--open-app-sidebar-footer-bg);
cursor: pointer;
transition: box-shadow 0.2s ease-in-out, background-color 0.2s ease-in-out;
&:hover {
background-color: @colorBgLayout;
box-shadow: @boxShadowDrawerUp;
background-color: var(--open-app-sidebar-hover);
box-shadow: var(--open-app-sidebar-control-shadow);
}
// 用户头像
@@ -231,15 +360,46 @@
// 用户名称
.user-name {
color: @colorTextSecondary;
color: var(--open-app-sidebar-text-secondary);
}
.active-icon-container {
color: @navFirstMenuTextSecondary;
color: var(--open-app-sidebar-text-secondary);
padding: @paddingXs;
}
}
.theme-dispatch {
.agentSidebarContainer {
background:
linear-gradient(180deg, rgba(0, 166, 122, 12%) 0%, rgba(0, 63, 66, 0%) 34%),
var(--open-app-sidebar-bg);
}
.sidebarTop {
.logo-container {
box-shadow: 0 0 0 3px rgba(126, 232, 196, 18%);
}
}
.newSessionBtn {
backdrop-filter: blur(10px);
}
}
.theme-portal {
.agentSidebarContainer {
border-top: 4px solid var(--open-app-sidebar-accent);
}
.sidebarTop {
padding: 0 @paddingXs;
border-radius: @borderRadiusLg;
background: #ffffff;
box-shadow: 0 8px 20px rgba(0, 101, 105, 8%);
}
}
.messageContainer {
left: 13px !important;
bottom: 53px !important;

View File

@@ -37,6 +37,12 @@ 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);
@@ -96,6 +102,9 @@ const BaseTemplate: React.FC = () => {
// 底部渐变显示状态
const [showFooterTopGradient, setShowFooterTopGradient] =
useState<boolean>(false);
const [openAppTheme, setOpenAppTheme] = useState<OpenAppTheme>(
getInitialOpenAppTheme,
);
/** 手机端且侧栏展开时收起侧栏(用于点主内容区、新建会话、历史项等) */
const closeSidebarIfMobileOpen = useCallback(() => {
@@ -164,6 +173,10 @@ const BaseTemplate: React.FC = () => {
document.documentElement.style.minWidth = 'unset';
}, [agentId]);
useEffect(() => {
localStorage.setItem(OPEN_APP_THEME_STORAGE_KEY, openAppTheme);
}, [openAppTheme]);
// 已发布的智能体详情接口
const { run: runDetail } = useRequest(apiPublishedAgentInfo, {
manual: true,
@@ -376,7 +389,6 @@ const BaseTemplate: React.FC = () => {
zIndex: 999,
pointerEvents: isAppSidebarVisible ? 'auto' : 'none',
transform: isAppSidebarVisible ? 'translateX(0)' : 'translateX(-100%)',
backgroundColor: '#f5f5f5',
};
}
return {
@@ -405,7 +417,14 @@ const BaseTemplate: React.FC = () => {
const showAppSidebarLoading = appAgentDetailLoading || !appAgentDetail;
return (
<div className={cx('flex', 'h-full', styles.container)}>
<div
className={cx(
'flex',
'h-full',
styles.container,
styles[`theme-${openAppTheme}`],
)}
>
{/* 侧边菜单栏区域 */}
<div
className={cx(styles.agentSidebarContainer, {
@@ -493,6 +512,23 @@ const BaseTemplate: React.FC = () => {
</div>
</div>
<div className={styles.themeSwitcher}>
{openAppThemes.map((theme) => (
<button
key={theme.key}
type="button"
className={cx(styles.themeOption, {
[styles.themeOptionActive]: openAppTheme === theme.key,
})}
title={theme.title}
aria-pressed={openAppTheme === theme.key}
onClick={() => setOpenAppTheme(theme.key)}
>
{theme.label}
</button>
))}
</div>
{/* 页面导航 */}
<ConditionRender
condition={appAgentDetail?.customPageMenus?.length > 0}

View File

@@ -0,0 +1,38 @@
export const OPEN_APP_THEME_STORAGE_KEY = 'qiming-open-app-theme';
export const openAppThemes = [
{
key: 'light',
label: '纯白',
title: '纯白国网绿轻量版',
},
{
key: 'dispatch',
label: '调度',
title: '深绿调度台版',
},
{
key: 'portal',
label: '门户',
title: '绿白政企门户版',
},
] as const;
export type OpenAppTheme = (typeof openAppThemes)[number]['key'];
const DEFAULT_OPEN_APP_THEME: OpenAppTheme = 'dispatch';
export const isOpenAppTheme = (value: string): value is OpenAppTheme => {
return openAppThemes.some((theme) => theme.key === value);
};
export const getInitialOpenAppTheme = (): OpenAppTheme => {
if (typeof window === 'undefined') {
return DEFAULT_OPEN_APP_THEME;
}
const storedTheme = window.localStorage.getItem(OPEN_APP_THEME_STORAGE_KEY);
return storedTheme && isOpenAppTheme(storedTheme)
? storedTheme
: DEFAULT_OPEN_APP_THEME;
};

View File

@@ -0,0 +1,45 @@
import {
OPEN_APP_THEME_STORAGE_KEY,
getInitialOpenAppTheme,
isOpenAppTheme,
openAppThemes,
} from '@/pages/OpenApp/BaseTemplate/openAppTheme';
describe('openAppTheme', () => {
beforeEach(() => {
localStorage.clear();
});
test('defines the three State Grid green OpenApp themes', () => {
expect(openAppThemes.map((theme) => theme.key)).toEqual([
'light',
'dispatch',
'portal',
]);
expect(openAppThemes.map((theme) => theme.label)).toEqual([
'纯白',
'调度',
'门户',
]);
});
test('recognizes valid theme keys only', () => {
expect(isOpenAppTheme('light')).toBe(true);
expect(isOpenAppTheme('dispatch')).toBe(true);
expect(isOpenAppTheme('portal')).toBe(true);
expect(isOpenAppTheme('dark')).toBe(false);
expect(isOpenAppTheme('')).toBe(false);
});
test('uses dispatch as the default visual variant', () => {
expect(getInitialOpenAppTheme()).toBe('dispatch');
});
test('restores a valid stored theme and ignores invalid storage values', () => {
localStorage.setItem(OPEN_APP_THEME_STORAGE_KEY, 'portal');
expect(getInitialOpenAppTheme()).toBe('portal');
localStorage.setItem(OPEN_APP_THEME_STORAGE_KEY, 'unknown');
expect(getInitialOpenAppTheme()).toBe('dispatch');
});
});