diff --git a/qiming/src/constants/theme.constants.ts b/qiming/src/constants/theme.constants.ts index 5e1abc77..147884af 100644 --- a/qiming/src/constants/theme.constants.ts +++ b/qiming/src/constants/theme.constants.ts @@ -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'), diff --git a/qiming/src/pages/OpenApp/BaseTemplate/ConversationItem/index.less b/qiming/src/pages/OpenApp/BaseTemplate/ConversationItem/index.less index 3e53b0b7..7b60d32d 100644 --- a/qiming/src/pages/OpenApp/BaseTemplate/ConversationItem/index.less +++ b/qiming/src/pages/OpenApp/BaseTemplate/ConversationItem/index.less @@ -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); } } diff --git a/qiming/src/pages/OpenApp/BaseTemplate/index.less b/qiming/src/pages/OpenApp/BaseTemplate/index.less index b7540c1c..6e8527a1 100644 --- a/qiming/src/pages/OpenApp/BaseTemplate/index.less +++ b/qiming/src/pages/OpenApp/BaseTemplate/index.less @@ -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; diff --git a/qiming/src/pages/OpenApp/BaseTemplate/index.tsx b/qiming/src/pages/OpenApp/BaseTemplate/index.tsx index 396f4650..5886e449 100644 --- a/qiming/src/pages/OpenApp/BaseTemplate/index.tsx +++ b/qiming/src/pages/OpenApp/BaseTemplate/index.tsx @@ -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(false); + const [openAppTheme, setOpenAppTheme] = useState( + 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 ( -
+
{/* 侧边菜单栏区域 */}
{
+
+ {openAppThemes.map((theme) => ( + + ))} +
+ {/* 页面导航 */} 0} diff --git a/qiming/src/pages/OpenApp/BaseTemplate/openAppTheme.ts b/qiming/src/pages/OpenApp/BaseTemplate/openAppTheme.ts new file mode 100644 index 00000000..d68d917c --- /dev/null +++ b/qiming/src/pages/OpenApp/BaseTemplate/openAppTheme.ts @@ -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; +}; diff --git a/qiming/tests/openAppTheme.test.ts b/qiming/tests/openAppTheme.test.ts new file mode 100644 index 00000000..0fe0eeb1 --- /dev/null +++ b/qiming/tests/openAppTheme.test.ts @@ -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'); + }); +});