feat: improve theme configuration

This commit is contained in:
baiyanyun
2026-06-04 16:48:47 +08:00
parent a01fc93594
commit f9e090125c
52 changed files with 4490 additions and 741 deletions

View File

@@ -1,2 +1,2 @@
registry=https://registry.npmmirror.com/
public-hoist-pattern[]=umi

View File

@@ -1,6 +1,6 @@
import { RequestConfig } from '@@/plugin-request/request';
import { Modal, theme as antdTheme } from 'antd';
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { history, useAntdConfigSetter } from 'umi';
import { SUCCESS_CODE } from './constants/codes.constants';
import { ACCESS_TOKEN } from './constants/home.constants';
@@ -8,7 +8,11 @@ import {
applyOpenAppThemeToDocument,
getInitialOpenAppTheme,
} from './constants/openAppTheme.constants';
import { darkThemeTokens, themeTokens } from './constants/theme.constants';
import {
darkThemeTokens,
getComponentThemes,
themeTokens,
} from './constants/theme.constants';
import { APP_NAME, APP_VERSION } from './constants/version';
import useEventPolling from './hooks/useEventPolling';
import { request as requestCommon } from './services/common';
@@ -68,6 +72,59 @@ const GlobalEventPolling: React.FC = () => {
return contextHolder; // 返回 contextHolder 以支持 Modal 的动态主题
};
const GlobalThemeEffects: React.FC = () => {
const [themeData, setThemeData] = useState(() =>
unifiedThemeService.getCurrentData(),
);
const [progressVisible, setProgressVisible] = useState(false);
const progressTimerRef = useRef<number>();
useEffect(() => {
const handleThemeChange = () => {
setThemeData(unifiedThemeService.getCurrentData());
};
unifiedThemeService.addListener(handleThemeChange);
return () => {
unifiedThemeService.removeListener(handleThemeChange);
};
}, []);
useEffect(() => {
if (!themeData.basicConfig.showNprogress) return;
const unlisten = history.listen(() => {
setProgressVisible(true);
window.clearTimeout(progressTimerRef.current);
progressTimerRef.current = window.setTimeout(() => {
setProgressVisible(false);
}, 420);
});
return () => {
unlisten();
window.clearTimeout(progressTimerRef.current);
};
}, [themeData.basicConfig.showNprogress]);
return (
<>
{themeData.basicConfig.showNprogress && (
<div
className={`xagi-top-progress ${
progressVisible ? 'xagi-top-progress-active' : ''
}`}
/>
)}
{themeData.basicConfig.watermarkVisible && (
<div className="xagi-global-watermark" aria-hidden="true">
<span>{APP_NAME}</span>
</div>
)}
</>
);
};
const AppContainer: React.FC<{ children: React.ReactElement }> = ({
children,
}) => {
@@ -222,6 +279,7 @@ const AppContainer: React.FC<{ children: React.ReactElement }> = ({
const signature = JSON.stringify({
mode: darkMode ? 'dark' : 'light',
language: data.language || getCurrentLang(),
tokens,
});
if (signature === lastAppliedRef.current) return;
@@ -231,11 +289,10 @@ const AppContainer: React.FC<{ children: React.ReactElement }> = ({
theme: {
algorithm,
token: tokens as any,
components: {
Segmented: {
itemSelectedColor: data.primaryColor,
},
},
components: getComponentThemes({
darkMode,
primaryColor: data.primaryColor,
}),
cssVar: { prefix: 'xagi' },
},
locale: getAntdLocale(data.language || getCurrentLang()),
@@ -252,6 +309,22 @@ const AppContainer: React.FC<{ children: React.ReactElement }> = ({
'data-nav-style',
data.navigationStyle === 'style1' ? 'compact' : 'expanded',
);
document.documentElement.setAttribute(
'data-menu-layout',
data.menuLayout,
);
document.documentElement.setAttribute(
'data-menu-style',
data.menuStyle,
);
document.documentElement.setAttribute(
'data-tab-style',
data.basicConfig.tabStyle,
);
document.documentElement.setAttribute(
'data-page-transition',
data.basicConfig.pageTransition || 'none',
);
unifiedThemeService.updateData(data, {
immediate: true,
@@ -294,6 +367,7 @@ const AppContainer: React.FC<{ children: React.ReactElement }> = ({
<>
{/* 只有用户已登录时才启动事件轮询 */}
<GlobalEventPolling />
<GlobalThemeEffects />
{children}
</>
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

View File

@@ -16,36 +16,56 @@
// 默认状态
&:not(.selected) {
background-color: @colorBgContainer;
border: none;
color: @colorText;
border: none !important;
background-color: var(--xagi-layout-bg-input, #fff) !important;
color: var(--xagi-layout-text-primary, #1f2337) !important;
box-shadow: none !important;
&:hover:not(.disabled) {
background-color: @colorBgTextHover;
border-color: @colorPrimaryHover;
color: @colorPrimaryHover;
border-color: transparent !important;
background-color: rgba(93, 135, 255, 0.1) !important;
background-color: color-mix(
in srgb,
var(--xagi-color-primary, #5d87ff) 10%,
#fff
) !important;
color: var(--xagi-color-primary, #5d87ff) !important;
}
}
// 选中状态
&.selected {
background-color: @colorPrimaryBg;
border: none;
color: @colorPrimary;
border: none !important;
background-color: rgba(93, 135, 255, 0.12) !important;
background-color: color-mix(
in srgb,
var(--xagi-color-primary, #5d87ff) 16%,
#fff
) !important;
color: var(--xagi-color-primary, #5d87ff) !important;
font-weight: @fontWeightStrong;
box-shadow: none;
box-shadow: none !important;
&:hover:not(.disabled) {
background-color: @colorPrimaryBg;
border: none;
color: @colorPrimaryHover;
border: none !important;
background-color: rgba(93, 135, 255, 0.16) !important;
background-color: color-mix(
in srgb,
var(--xagi-color-primary, #5d87ff) 20%,
#fff
) !important;
color: var(--xagi-color-primary, #5d87ff) !important;
}
&:focus:not(.disabled) {
background-color: @colorPrimaryBg;
border: none;
color: @colorPrimaryActive;
border: none !important;
background-color: rgba(93, 135, 255, 0.16) !important;
background-color: color-mix(
in srgb,
var(--xagi-color-primary, #5d87ff) 20%,
#fff
) !important;
color: var(--xagi-color-primary, #5d87ff) !important;
}
}

View File

@@ -2,12 +2,15 @@
@import '@/styles/token';
.container {
min-height: 100%;
background: var(--xagi-layout-bg-secondary, #f7f8fb);
.title {
line-height: 32px;
font-size: @fontSizeLg;
line-height: 60px;
font-size: 16px;
font-weight: 600;
color: @BLANK04;
margin-right: 20px;
color: var(--xagi-layout-text-primary, #1f2337);
margin: 0 20px 0 0;
display: flex;
align-items: center;
gap: 4px;
@@ -16,16 +19,22 @@
.tips-icon {
font-size: 14px;
cursor: pointer;
transition: color 0.3s;
color: #8b92a6;
transition: color 0.24s ease;
&:hover {
color: @BLANK04;
color: @colorPrimary;
}
}
.icon-back {
color: @BLANK17;
color: #8b92a6;
font-size: 16px;
transition: color 0.24s ease;
&:hover {
color: @colorPrimary;
}
}
.header-area {
@@ -35,7 +44,12 @@
align-items: center;
flex-wrap: wrap;
gap: 12px;
padding: 15px 15px 10px 24px;
min-height: 60px;
padding: 0 20px 0 24px;
border-bottom: 1px solid
var(--xagi-layout-border-secondary, rgba(91, 105, 135, 0.1));
background: var(--xagi-layout-bg-container, #fff);
box-shadow: 0 1px 0 rgba(32, 45, 84, 0.02);
.header-left {
display: flex;
@@ -80,9 +94,41 @@
.content {
overflow-x: hidden;
overflow-y: auto;
height: 100%;
padding: 0 16px 16px 24px;
background-color: transparent;
flex: 1;
min-height: 0;
height: auto;
padding: 16px 18px 18px 24px;
background: var(--xagi-layout-bg-secondary, #f7f8fb);
:global {
.ant-card,
.ant-pro-card,
.ant-table-wrapper,
.ant-form,
.ant-pro-table {
border-radius: 8px;
}
.ant-card,
.ant-pro-card,
.ant-table-wrapper .ant-table,
.ant-pro-table .ant-pro-table-search {
border: 1px solid
var(--xagi-layout-border-secondary, rgba(91, 105, 135, 0.1));
background: var(--xagi-layout-bg-container, #fff);
box-shadow: 0 8px 18px rgba(32, 45, 84, 0.04);
}
.ant-table-thead > tr > th {
background: #f4f7fb !important;
color: #5d6378 !important;
font-weight: 600;
}
.ant-btn-primary {
box-shadow: 0 8px 16px rgba(93, 135, 255, 0.18);
}
}
// hideScroll内容区占满页头以下剩余高度由页面内部自行分配滚动
&.scroll-container-hide {
@@ -94,6 +140,57 @@
}
.extra-container {
padding: 0 16px 16px 24px;
padding: 0 18px 18px 24px;
background: var(--xagi-layout-bg-secondary, #f7f8fb);
}
}
:global(html[data-theme='dark']) {
.container {
background: var(--xagi-layout-bg-primary, #080a0f);
.tips-icon,
.icon-back {
color: var(--xagi-layout-text-tertiary, rgba(255, 255, 255, 0.65));
}
.header-area {
border-bottom-color: var(
--xagi-layout-border-secondary,
rgba(255, 255, 255, 0.08)
);
background: var(--xagi-layout-bg-container, #111620);
box-shadow: none;
}
.content {
background: var(--xagi-layout-bg-primary, #080a0f);
:global {
.ant-card,
.ant-pro-card,
.ant-table-wrapper .ant-table,
.ant-pro-table .ant-pro-table-search {
border-color: var(
--xagi-layout-border-secondary,
rgba(255, 255, 255, 0.08)
);
background: var(--xagi-layout-bg-container, #111620);
box-shadow: none;
}
.ant-table-thead > tr > th {
background: rgba(255, 255, 255, 0.04) !important;
color: var(
--xagi-layout-text-secondary,
rgba(255, 255, 255, 0.85)
) !important;
}
}
}
.extra-container {
background: var(--xagi-layout-bg-primary, #080a0f);
}
}
}

View File

@@ -2,32 +2,44 @@
@import '@/styles/token';
.row {
padding: @paddingXs;
min-height: 42px;
padding: 0 12px;
cursor: default;
color: @navSecondMenuTextSecondary;
color: var(
--xagi-layout-second-menu-text-color-secondary,
@navSecondMenuTextSecondary
);
border-radius: 6px;
transition: transform 0.24s ease, background 0.24s ease, color 0.24s ease,
box-shadow 0.24s ease;
&.menuItem {
cursor: pointer;
.name {
color: @navSecondMenuTextSecondary;
font-size: @fontSize;
line-height: @fontHeightLg;
color: var(
--xagi-layout-second-menu-text-color-secondary,
@navSecondMenuTextSecondary
);
font-size: 14px;
line-height: 42px;
white-space: nowrap;
}
&.has-icon {
.name {
margin-left: @marginXs;
margin-left: 12px;
}
}
}
&.active {
background-color: @colorBgContainer;
background-color: var(--xagi-menu-item-active-bg, #fff);
color: @colorPrimary;
border-radius: @borderRadius;
box-shadow: @boxShadowTertiary;
box-shadow: var(
--xagi-menu-item-active-shadow,
0 10px 20px rgba(32, 45, 84, 0.1)
);
.name {
color: @colorPrimary;
@@ -35,13 +47,16 @@
}
&:hover {
background-color: @colorBgContainer;
border-radius: @borderRadius;
box-shadow: @boxShadowTertiary;
color: @colorTextSecondary;
background-color: var(--xagi-menu-item-hover-bg, rgba(255, 255, 255, 0.72));
box-shadow: var(
--xagi-menu-item-hover-shadow,
0 8px 16px rgba(32, 45, 84, 0.06)
);
color: @colorPrimary;
transform: translateX(2px);
.name {
color: @colorTextSecondary;
color: @colorPrimary;
}
.collectIcon {
@@ -52,9 +67,13 @@
.icon-box {
font-size: 20px;
border-radius: @borderRadiusSm;
border-radius: 6px;
overflow: hidden;
flex-shrink: 0;
color: var(
--xagi-layout-second-menu-text-color-secondary,
@navSecondMenuTextSecondary
);
.icon-image {
width: 20px;
@@ -63,7 +82,10 @@
}
.collectIcon {
color: @navSecondMenuTextSecondary;
color: var(
--xagi-layout-second-menu-text-color-secondary,
@navSecondMenuTextSecondary
);
visibility: hidden;
transition: background-color ease-in-out 0.2s;

View File

@@ -2,13 +2,24 @@
@import '@/styles/token';
.row {
padding: @paddingXs;
min-height: 42px;
padding: 0 12px;
cursor: default;
color: @navSecondMenuTextSecondary;
color: var(
--xagi-layout-second-menu-text-color-secondary,
@navSecondMenuTextSecondary
);
border-radius: 6px;
transition: transform 0.24s ease, background 0.24s ease, color 0.24s ease,
box-shadow 0.24s ease;
.name {
color: @navSecondMenuTextSecondary;
font-size: @fontSize !important;
color: var(
--xagi-layout-second-menu-text-color-secondary,
@navSecondMenuTextSecondary
);
font-size: 14px !important;
line-height: 42px;
white-space: nowrap;
}
@@ -17,18 +28,24 @@
:global {
.ant-typography {
margin-left: @marginXs;
font-size: @fontSizeLg;
line-height: @fontHeightLg;
color: var(
--xagi-layout-second-menu-text-color-secondary,
@navSecondMenuTextSecondary
);
margin-left: 12px;
font-size: 14px;
line-height: 42px;
}
}
}
&.active {
background-color: @colorBgContainer;
background-color: var(--xagi-menu-item-active-bg, #fff);
color: @colorPrimary;
border-radius: @borderRadius;
box-shadow: @boxShadowTertiary;
box-shadow: var(
--xagi-menu-item-active-shadow,
0 10px 20px rgba(32, 45, 84, 0.1)
);
:global {
.ant-typography {
@@ -37,19 +54,22 @@
}
.icon-dropdown {
color: @colorTextSecondary;
color: var(--xagi-layout-text-tertiary, @colorTextSecondary);
}
}
&:hover {
background-color: @colorBgContainer;
border-radius: @borderRadius;
box-shadow: @boxShadowTertiary;
color: @colorTextSecondary;
background-color: var(--xagi-menu-item-hover-bg, rgba(255, 255, 255, 0.72));
box-shadow: var(
--xagi-menu-item-hover-shadow,
0 8px 16px rgba(32, 45, 84, 0.06)
);
color: @colorPrimary;
transform: translateX(2px);
:global {
.ant-typography {
color: @colorTextSecondary;
color: @colorPrimary;
}
}
@@ -58,7 +78,7 @@
}
.icon-dropdown {
color: @colorTextSecondary;
color: var(--xagi-layout-text-tertiary, @colorTextSecondary);
}
}
@@ -68,9 +88,13 @@
.icon-box {
font-size: 20px;
border-radius: @borderRadiusSm;
border-radius: 6px;
overflow: hidden;
flex-shrink: 0;
color: var(
--xagi-layout-second-menu-text-color-secondary,
@navSecondMenuTextSecondary
);
.icon-image {
width: 20px;
@@ -80,7 +104,11 @@
.icon-dropdown {
font-size: @fontSizeSm;
color: @navSecondMenuTextSecondary;
color: var(
--xagi-layout-second-menu-text-color-secondary,
@navSecondMenuTextSecondary
);
transition: transform 0.24s ease, color 0.24s ease;
}
&.open {
@@ -90,7 +118,10 @@
}
.collectIcon {
color: @colorTextSecondary;
color: var(
--xagi-layout-second-menu-text-color-secondary,
@colorTextSecondary
);
visibility: hidden;
transition: background-color ease-in-out 0.2s;

View File

@@ -3,6 +3,28 @@
// 仅选择器rootClassName 挂在这层):默认宽度;外部传入的 className 也合并在此,只影响触发区域
.container {
width: 120px;
:global(.ant-select-selector) {
border-color: var(
--xagi-layout-border-primary,
rgba(91, 105, 135, 0.14)
) !important;
background: var(
--xagi-layout-bg-input,
rgba(255, 255, 255, 0.88)
) !important;
color: var(--xagi-layout-text-primary, #1f2337) !important;
}
:global(.ant-select-selection-item) {
color: var(--xagi-layout-text-primary, #1f2337) !important;
}
:global(.ant-select-selection-placeholder),
:global(.ant-select-arrow),
:global(.ant-select-clear) {
color: var(--xagi-layout-text-tertiary, rgba(31, 35, 55, 0.52)) !important;
}
}
// 以下类名用在 portal 内的 optionRender 上,不能嵌套在 .container 下否则选择器匹配不到
@@ -64,10 +86,38 @@
// 解绑选择器宽度rc-select 在 popupMatchSelectWidth={false} 时仍会用 rc-trigger 把下拉的 min-width 对齐到选择器;
// 用 max-content 覆盖,使下拉最小宽度由选项内容决定,而不随 styles.container / className 的宽度变化
min-width: max-content !important;
background: var(--xagi-layout-bg-container, #fff) !important;
:global(.ant-select-item-option-content) {
// 避免 flex 默认 min-width:auto 卡住子项宽度测量
min-width: 0;
width: max-content;
}
:global(.ant-select-item) {
color: var(--xagi-layout-text-primary, #1f2337) !important;
}
:global(
.ant-select-item-option-active:not(.ant-select-item-option-disabled)
) {
background: rgba(93, 135, 255, 0.08) !important;
background: color-mix(
in srgb,
var(--xagi-color-primary, #5d87ff) 8%,
#fff
) !important;
}
:global(
.ant-select-item-option-selected:not(.ant-select-item-option-disabled)
) {
background: rgba(93, 135, 255, 0.12) !important;
background: color-mix(
in srgb,
var(--xagi-color-primary, #5d87ff) 14%,
#fff
) !important;
color: var(--xagi-color-primary, #5d87ff) !important;
}
}

View File

@@ -8,12 +8,12 @@ import { ThemeNavigationStyleType } from '@/types/enums/theme';
export const NAVIGATION_LAYOUT_SIZES = {
// 一级菜单宽度配置
FIRST_MENU_WIDTH: {
STYLE1: 60, // 紧凑模式:无文字导航
STYLE2: 80, // 展开模式:有文字导航
STYLE1: 88, // Art 风格:窄图标栏
STYLE2: 112, // Art 风格:图标+文字栏
},
// 二级菜单宽度配置
SECOND_MENU_WIDTH: 240,
SECOND_MENU_WIDTH: 230,
// 菜单总宽度计算
getTotalMenuWidth: (navigationStyle: string) => {
@@ -26,13 +26,13 @@ export const NAVIGATION_LAYOUT_SIZES = {
// 页面容器边距配置
PAGE_CONTAINER_MARGIN: {
STYLE1: 16, // 紧凑模式:有外边距
STYLE1: 0, // Art 风格:内容区贴合主布局
STYLE2: 0, // 展开模式:无外边距
},
// 页面容器圆角配置
PAGE_CONTAINER_BORDER_RADIUS: {
STYLE1: 12, // 紧凑模式:有圆角
STYLE1: 0, // Art 风格:由内容卡片控制圆角
STYLE2: 0, // 展开模式:无圆角
},
} as const;
@@ -75,7 +75,7 @@ export const getTotalMenuWidth = (
// 常量定义
export const MOBILE_BREAKPOINT = 768; // 移动端断点
export const ANIMATION_DURATION = 300; // 动画持续时间
export const MOBILE_MENU_TOP_PADDING = 32; // 移动端菜单顶部间距
export const MOBILE_MENU_TOP_PADDING = 20; // 移动端菜单顶部间距
/**
* 通用表格全局样式:主要用于弹窗中 Table 的间距边距

View File

@@ -4,7 +4,13 @@
*/
import { dict } from '@/services/i18nRuntime';
import { ThemeLayoutColorStyle } from '@/types/enums/theme';
import {
ThemeLayoutColorStyle,
ThemeMenuLayoutType,
ThemeMenuStyleType,
ThemePageTransitionType,
ThemeTabStyleType,
} from '@/types/enums/theme';
import {
ThemeBackgroundConfig,
ThemeStyleConfig,
@@ -21,19 +27,17 @@ import { FIRST_MENU_WIDTH_STYLE2 } from './layout.constants';
*/
export const THEME_COLOR_CONFIGS = [
{
color: '#006569',
name: '国网绿',
},
{
color: '#5147ff',
color: '#5D87FF',
name: dict('PC.Constants.Theme.colorBlue'),
isDefault: true,
},
{ color: '#B48DF3', name: dict('PC.Constants.Theme.colorPurple') },
{ color: '#1D84FF', name: dict('PC.Constants.Theme.colorBlue') },
{ color: '#60C041', name: dict('PC.Constants.Theme.colorGreen') },
{ color: '#38C0FC', name: dict('PC.Constants.Theme.colorBlue') },
{ color: '#F9901F', name: dict('PC.Constants.Theme.colorOrange') },
{ color: '#FF80C8', name: dict('PC.Constants.Theme.colorPink') },
{ color: '#ff4d4f', name: dict('PC.Constants.Theme.colorRed') },
{ color: '#fa8c16', name: dict('PC.Constants.Theme.colorOrange') },
{ color: '#52c41a', name: dict('PC.Constants.Theme.colorGreen') },
{ color: '#722ed1', name: dict('PC.Constants.Theme.colorPurple') },
{ color: '#eb2f96', name: dict('PC.Constants.Theme.colorPink') },
] as const;
/**
@@ -59,12 +63,30 @@ export const STORAGE_KEYS = {
* 统一管理所有默认值,确保各模块间的一致性
*/
export const DEFAULT_THEME_CONFIG = {
PRIMARY_COLOR: '#5147ff',
PRIMARY_COLOR: '#5D87FF',
BACKGROUND_ID: '',
NAVIGATION_STYLE: 'style1',
MENU_LAYOUT: ThemeMenuLayoutType.DUAL,
MENU_STYLE: ThemeMenuStyleType.DESIGN,
LAYOUT_STYLE: 'light',
THEME: 'light',
LANGUAGE: 'zh-CN',
BASIC_CONFIG: {
showWorkTab: true,
uniqueOpened: true,
showMenuButton: true,
showFastEnter: true,
showRefreshButton: true,
showCrumbs: true,
showLanguage: true,
showNprogress: false,
colorWeak: false,
watermarkVisible: false,
menuOpenWidth: 180,
tabStyle: ThemeTabStyleType.DEFAULT,
pageTransition: ThemePageTransitionType.NONE,
customRadius: '0.75',
},
} as const;
/**
@@ -139,25 +161,25 @@ export const STYLE_CONFIGS: Record<string, ThemeStyleConfig> = {
'light-style1': {
layout: {
'--xagi-layout-text-primary': '#000000',
'--xagi-layout-text-secondary': 'rgba(0, 0, 0, 0.65)',
'--xagi-layout-text-tertiary': 'rgba(0, 0, 0, 0.65)',
'--xagi-layout-text-secondary': 'rgba(31, 35, 55, 0.72)',
'--xagi-layout-text-tertiary': 'rgba(31, 35, 55, 0.52)',
'--xagi-layout-text-disabled': 'rgba(0, 0, 0, 0.25)',
'--xagi-layout-second-menu-text-color': 'rgba(0, 0, 0, 0.88)',
'--xagi-layout-second-menu-text-color-secondary': 'rgba(0, 0, 0, 0.65)',
'--xagi-layout-bg-primary': 'rgba(255, 255, 255, 0.95)',
'--xagi-layout-bg-secondary': 'rgba(255, 255, 255, 0.85)',
'--xagi-layout-bg-card': 'rgba(255, 255, 255, 0.65)',
'--xagi-layout-bg-input': 'rgba(255, 255, 255, 0.45)',
'--xagi-layout-border-primary': 'rgba(0, 0, 0, 0.15)',
'--xagi-layout-border-secondary': 'rgba(0, 0, 0, 0.1)',
'--xagi-layout-shadow': 'rgba(0, 0, 0, 0.1)',
'--xagi-layout-overlay': 'rgba(255, 255, 255, 0.7)',
'--xagi-layout-bg-container': 'rgba(255, 255, 255, 0.95)',
'--xagi-layout-second-menu-text-color': '#1f2337',
'--xagi-layout-second-menu-text-color-secondary': '#5d6378',
'--xagi-layout-bg-primary': '#f4f7fb',
'--xagi-layout-bg-secondary': '#f2f5fb',
'--xagi-layout-bg-card': 'rgba(255, 255, 255, 0.92)',
'--xagi-layout-bg-input': 'rgba(255, 255, 255, 0.88)',
'--xagi-layout-border-primary': 'rgba(91, 105, 135, 0.14)',
'--xagi-layout-border-secondary': 'rgba(91, 105, 135, 0.1)',
'--xagi-layout-shadow': '0 8px 20px rgba(32, 45, 84, 0.08)',
'--xagi-layout-overlay': 'rgba(255, 255, 255, 0.78)',
'--xagi-layout-bg-container': '#ffffff',
},
navigation: {
'--xagi-nav-first-menu-width': '60px',
'--xagi-page-container-margin': '12px',
'--xagi-page-container-border-radius': '12px',
'--xagi-nav-first-menu-width': '88px',
'--xagi-page-container-margin': '0',
'--xagi-page-container-border-radius': '0',
'--xagi-page-container-border-color': 'transparent',
},
},
@@ -165,20 +187,20 @@ export const STYLE_CONFIGS: Record<string, ThemeStyleConfig> = {
'light-style2': {
layout: {
'--xagi-layout-text-primary': '#000000',
'--xagi-layout-text-secondary': 'rgba(0, 0, 0, 0.65)',
'--xagi-layout-text-tertiary': 'rgba(0, 0, 0, 0.65)',
'--xagi-layout-text-secondary': 'rgba(31, 35, 55, 0.72)',
'--xagi-layout-text-tertiary': 'rgba(31, 35, 55, 0.52)',
'--xagi-layout-text-disabled': 'rgba(0, 0, 0, 0.25)',
'--xagi-layout-second-menu-text-color': 'rgba(0, 0, 0, 0.88)',
'--xagi-layout-second-menu-text-color-secondary': 'rgba(0, 0, 0, 0.65)',
'--xagi-layout-bg-primary': 'rgba(255, 255, 255, 0.95)',
'--xagi-layout-bg-secondary': 'rgba(255, 255, 255, 0.85)',
'--xagi-layout-bg-card': 'rgba(255, 255, 255, 0.65)',
'--xagi-layout-bg-input': 'rgba(255, 255, 255, 0.45)',
'--xagi-layout-border-primary': 'rgba(0, 0, 0, 0.15)',
'--xagi-layout-border-secondary': 'rgba(0, 0, 0, 0.1)',
'--xagi-layout-shadow': 'rgba(0, 0, 0, 0.1)',
'--xagi-layout-overlay': 'rgba(255, 255, 255, 0.7)',
'--xagi-layout-bg-container': 'rgba(255, 255, 255, 0.95)',
'--xagi-layout-second-menu-text-color': '#1f2337',
'--xagi-layout-second-menu-text-color-secondary': '#5d6378',
'--xagi-layout-bg-primary': '#f4f7fb',
'--xagi-layout-bg-secondary': '#f2f5fb',
'--xagi-layout-bg-card': 'rgba(255, 255, 255, 0.92)',
'--xagi-layout-bg-input': 'rgba(255, 255, 255, 0.88)',
'--xagi-layout-border-primary': 'rgba(91, 105, 135, 0.14)',
'--xagi-layout-border-secondary': 'rgba(91, 105, 135, 0.1)',
'--xagi-layout-shadow': '0 8px 20px rgba(32, 45, 84, 0.08)',
'--xagi-layout-overlay': 'rgba(255, 255, 255, 0.78)',
'--xagi-layout-bg-container': '#ffffff',
},
navigation: {
'--xagi-nav-first-menu-width': `${FIRST_MENU_WIDTH_STYLE2}px`,
@@ -197,20 +219,20 @@ export const STYLE_CONFIGS: Record<string, ThemeStyleConfig> = {
'--xagi-layout-second-menu-text-color': 'rgba(255, 255, 255, 1)',
'--xagi-layout-second-menu-text-color-secondary':
'rgba(255, 255, 255, 0.8)',
'--xagi-layout-bg-primary': 'rgba(0, 0, 0, 0.85)',
'--xagi-layout-bg-secondary': 'rgba(0, 0, 0, 0.65)',
'--xagi-layout-bg-card': 'rgba(0, 0, 0, 0.45)',
'--xagi-layout-bg-input': 'rgba(0, 0, 0, 0.25)',
'--xagi-layout-bg-primary': '#080a0f',
'--xagi-layout-bg-secondary': '#0d1117',
'--xagi-layout-bg-card': 'rgba(20, 24, 33, 0.9)',
'--xagi-layout-bg-input': 'rgba(255, 255, 255, 0.06)',
'--xagi-layout-border-primary': 'rgba(255, 255, 255, 0.12)',
'--xagi-layout-border-secondary': 'rgba(255, 255, 255, 0.08)',
'--xagi-layout-shadow': 'rgba(0, 0, 0, 0.6)',
'--xagi-layout-shadow': '0 12px 32px rgba(0, 0, 0, 0.45)',
'--xagi-layout-overlay': 'rgba(0, 0, 0, 0.7)',
'--xagi-layout-bg-container': '#ffffff',
'--xagi-layout-bg-container': '#111620',
},
navigation: {
'--xagi-nav-first-menu-width': '60px',
'--xagi-page-container-margin': '12px',
'--xagi-page-container-border-radius': '12px',
'--xagi-nav-first-menu-width': '88px',
'--xagi-page-container-margin': '0',
'--xagi-page-container-border-radius': '0',
'--xagi-page-container-border-color': 'transparent',
},
},
@@ -221,17 +243,18 @@ export const STYLE_CONFIGS: Record<string, ThemeStyleConfig> = {
'--xagi-layout-text-secondary': 'rgba(255, 255, 255, 0.85)',
'--xagi-layout-text-tertiary': 'rgba(255, 255, 255, 0.65)',
'--xagi-layout-text-disabled': 'rgba(255, 255, 255, 0.25)',
'--xagi-layout-second-menu-text-color': 'rgba(0, 0, 0, 0.88)',
'--xagi-layout-second-menu-text-color-secondary': 'rgba(0, 0, 0, 0.65)',
'--xagi-layout-bg-primary': 'rgba(0, 0, 0, 0.85)',
'--xagi-layout-bg-secondary': 'rgba(0, 0, 0, 0.85)',
'--xagi-layout-bg-card': 'rgba(0, 0, 0, 0.45)',
'--xagi-layout-bg-input': 'rgba(0, 0, 0, 0.25)',
'--xagi-layout-second-menu-text-color': 'rgba(255, 255, 255, 0.95)',
'--xagi-layout-second-menu-text-color-secondary':
'rgba(255, 255, 255, 0.78)',
'--xagi-layout-bg-primary': '#080a0f',
'--xagi-layout-bg-secondary': '#0d1117',
'--xagi-layout-bg-card': 'rgba(20, 24, 33, 0.9)',
'--xagi-layout-bg-input': 'rgba(255, 255, 255, 0.06)',
'--xagi-layout-border-primary': 'rgba(255, 255, 255, 0.12)',
'--xagi-layout-border-secondary': 'rgba(255, 255, 255, 0.08)',
'--xagi-layout-shadow': 'rgba(0, 0, 0, 0.6)',
'--xagi-layout-shadow': '0 12px 32px rgba(0, 0, 0, 0.45)',
'--xagi-layout-overlay': 'rgba(0, 0, 0, 0.7)',
'--xagi-layout-bg-container': '#ffffff',
'--xagi-layout-bg-container': '#111620',
},
navigation: {
'--xagi-nav-first-menu-width': `${FIRST_MENU_WIDTH_STYLE2}px`,
@@ -268,7 +291,7 @@ export type StyleConfigKey = keyof typeof STYLE_CONFIGS;
*/
export const themeTokens: Partial<AliasToken> = {
// 品牌主色 - 项目主色调
colorPrimary: '#5147ff',
colorPrimary: '#5D87FF',
// 功能色
colorSuccess: '#3bb346',
@@ -281,7 +304,7 @@ export const themeTokens: Partial<AliasToken> = {
colorBgBase: '#ffffff',
// 超链接颜色
colorLink: '#5147ff',
colorLink: '#5D87FF',
// 字体配置
fontFamily:
@@ -332,12 +355,12 @@ export const themeTokens: Partial<AliasToken> = {
motion: true,
// 预设颜色
blue: '#1890ff',
purple: '#722ed1',
cyan: '#13c2c2',
green: '#52c41a',
magenta: '#eb2f96',
pink: '#eb2f96',
blue: '#5D87FF',
purple: '#B48DF3',
cyan: '#38C0FC',
green: '#60C041',
magenta: '#FF80C8',
pink: '#FF80C8',
red: '#f5222d',
orange: '#fa8c16',
yellow: '#fadb14',
@@ -357,8 +380,13 @@ export const themeTokens: Partial<AliasToken> = {
export const darkThemeTokens = {
...themeTokens,
colorBgBase: '#000',
colorBgBase: '#080a0f',
colorTextBase: '#fff',
colorBgLayout: '#080a0f',
colorBgContainer: '#111620',
colorBgElevated: '#171c26',
colorBorder: 'rgba(255, 255, 255, 0.12)',
colorBorderSecondary: 'rgba(255, 255, 255, 0.08)',
// TODO 填充颜色 深色 缺少
// 导航深色主题适配
@@ -413,7 +441,7 @@ export const componentThemes: ThemeConfig['components'] = {
paddingBlock: 4,
activeBorderColor: themeTokens.colorPrimary,
hoverBorderColor: '#7B6EFF',
activeShadow: '0 0 0 2px rgba(81, 71, 255, 0.2)',
activeShadow: '0 0 0 2px rgba(93, 135, 255, 0.18)',
errorActiveShadow: '0 0 0 2px rgba(255, 77, 79, 0.2)',
warningActiveShadow: '0 0 0 2px rgba(255, 140, 0, 0.2)',
} as InputToken,
@@ -424,8 +452,8 @@ export const componentThemes: ThemeConfig['components'] = {
borderRadius: themeTokens.borderRadius,
controlHeight: themeTokens.controlHeight,
fontSize: themeTokens.fontSize,
optionSelectedBg: 'rgba(81, 71, 255, 0.1)',
optionActiveBg: 'rgba(81, 71, 255, 0.05)',
optionSelectedBg: 'rgba(93, 135, 255, 0.12)',
optionActiveBg: 'rgba(93, 135, 255, 0.07)',
optionSelectedColor: themeTokens.colorPrimary,
optionPadding: '5px 12px',
showArrowPaddingInlineEnd: 18,
@@ -440,9 +468,9 @@ export const componentThemes: ThemeConfig['components'] = {
headerSortActiveBg: '#f0f0f0',
headerSortHoverBg: '#f5f5f5',
bodySortBg: '#fafafa',
rowHoverBg: 'rgba(81, 71, 255, 0.03)',
rowSelectedBg: 'rgba(81, 71, 255, 0.05)',
rowSelectedHoverBg: 'rgba(81, 71, 255, 0.08)',
rowHoverBg: 'rgba(93, 135, 255, 0.04)',
rowSelectedBg: 'rgba(93, 135, 255, 0.08)',
rowSelectedHoverBg: 'rgba(93, 135, 255, 0.1)',
rowExpandedBg: '#fbfbfb',
cellPaddingBlock: 16,
cellPaddingInline: 16,
@@ -505,8 +533,8 @@ export const componentThemes: ThemeConfig['components'] = {
borderRadius: themeTokens.borderRadius,
controlHeight: themeTokens.controlHeight,
fontSize: themeTokens.fontSize,
cellActiveWithRangeBg: 'rgba(81, 71, 255, 0.1)',
cellHoverWithRangeBg: 'rgba(81, 71, 255, 0.05)',
cellActiveWithRangeBg: 'rgba(93, 135, 255, 0.12)',
cellHoverWithRangeBg: 'rgba(93, 135, 255, 0.07)',
cellRangeBorderColor: 'transparent',
cellBgDisabled: '#f5f5f5',
timeColumnWidth: 56,
@@ -534,9 +562,9 @@ export const componentThemes: ThemeConfig['components'] = {
itemColor: '#000000d9',
itemHoverBg: 'rgba(0, 0, 0, 0.06)',
itemHoverColor: '#000000d9',
itemSelectedBg: 'rgba(81, 71, 255, 0.1)',
itemSelectedBg: 'rgba(93, 135, 255, 0.12)',
itemSelectedColor: themeTokens.colorPrimary,
itemActiveBg: 'rgba(81, 71, 255, 0.15)',
itemActiveBg: 'rgba(93, 135, 255, 0.16)',
subMenuItemBg: 'transparent',
itemMarginBlock: 4,
itemMarginInline: 4,
@@ -558,6 +586,125 @@ export const componentThemes: ThemeConfig['components'] = {
} as SegmentedToken,
};
export const getComponentThemes = ({
darkMode = false,
primaryColor = themeTokens.colorPrimary,
}: {
darkMode?: boolean;
primaryColor?: string;
} = {}): ThemeConfig['components'] => {
const primary = primaryColor || themeTokens.colorPrimary;
const primarySelectedBg = 'rgba(93, 135, 255, 0.12)';
const primaryActiveBg = 'rgba(93, 135, 255, 0.16)';
if (!darkMode) {
return {
...componentThemes,
Button: {
...(componentThemes.Button || {}),
colorPrimary: primary,
} as ButtonToken,
Input: {
...(componentThemes.Input || {}),
colorPrimary: primary,
activeBorderColor: primary,
} as InputToken,
Select: {
...(componentThemes.Select || {}),
colorPrimary: primary,
optionSelectedColor: primary,
} as SelectToken,
DatePicker: {
...(componentThemes.DatePicker || {}),
colorPrimary: primary,
} as DatePickerToken,
Menu: {
...(componentThemes.Menu || {}),
itemSelectedColor: primary,
} as MenuToken,
Segmented: {
...(componentThemes.Segmented || {}),
itemSelectedColor: primary,
} as SegmentedToken,
};
}
return {
...componentThemes,
Button: {
...(componentThemes.Button || {}),
colorPrimary: primary,
} as ButtonToken,
Input: {
...(componentThemes.Input || {}),
colorPrimary: primary,
activeBorderColor: primary,
hoverBorderColor: primary,
activeShadow: '0 0 0 2px rgba(93, 135, 255, 0.2)',
} as InputToken,
Select: {
...(componentThemes.Select || {}),
colorPrimary: primary,
optionSelectedBg: primarySelectedBg,
optionActiveBg: 'rgba(255, 255, 255, 0.06)',
optionSelectedColor: '#fff',
} as SelectToken,
Table: {
...(componentThemes.Table || {}),
headerBg: 'rgba(255, 255, 255, 0.04)',
headerColor: 'rgba(255, 255, 255, 0.85)',
headerSortActiveBg: 'rgba(255, 255, 255, 0.08)',
headerSortHoverBg: 'rgba(255, 255, 255, 0.06)',
bodySortBg: 'rgba(255, 255, 255, 0.03)',
rowHoverBg: 'rgba(255, 255, 255, 0.05)',
rowSelectedBg: primarySelectedBg,
rowSelectedHoverBg: primaryActiveBg,
rowExpandedBg: 'rgba(255, 255, 255, 0.03)',
} as TableToken,
Card: {
...(componentThemes.Card || {}),
actionsBg: 'rgba(255, 255, 255, 0.03)',
} as CardToken,
Modal: {
...(componentThemes.Modal || {}),
contentBg: '#171c26',
headerBg: '#171c26',
footerBg: 'transparent',
maskBg: 'rgba(0, 0, 0, 0.55)',
} as ModalToken,
Message: {
...(componentThemes.Message || {}),
contentBg: '#171c26',
} as MessageToken,
DatePicker: {
...(componentThemes.DatePicker || {}),
colorPrimary: primary,
cellActiveWithRangeBg: primarySelectedBg,
cellHoverWithRangeBg: 'rgba(255, 255, 255, 0.06)',
cellBgDisabled: 'rgba(255, 255, 255, 0.04)',
} as DatePickerToken,
Form: {
...(componentThemes.Form || {}),
labelColor: 'rgba(255, 255, 255, 0.85)',
} as FormToken,
Menu: {
...(componentThemes.Menu || {}),
itemColor: 'rgba(255, 255, 255, 0.85)',
itemHoverBg: 'rgba(255, 255, 255, 0.06)',
itemHoverColor: '#fff',
itemSelectedBg: primarySelectedBg,
itemSelectedColor: primary,
itemActiveBg: primaryActiveBg,
} as MenuToken,
Segmented: {
...(componentThemes.Segmented || {}),
itemSelectedBg: 'rgba(255, 255, 255, 0.1)',
itemSelectedColor: primary,
trackBg: 'rgba(255, 255, 255, 0.04)',
} as SegmentedToken,
};
};
// 向后兼容的导出
export const backgroundConfigs = THEME_BACKGROUND_CONFIGS;
export type BackgroundConfig = ThemeBackgroundConfig;

View File

@@ -6,7 +6,7 @@ html {
width: 100%;
height: 100%;
min-width: 1200px; //TODO 这个要和UI 确认
background-color: #fff;
background-color: var(--xagi-layout-bg-secondary, #f4f7fb);
}
#root {
@@ -22,6 +22,8 @@ body {
margin: 0;
padding: 0;
transform: translate3d(0, 0, 0);
background: var(--xagi-layout-bg-secondary, #f4f7fb);
color: var(--xagi-layout-text-primary, #1f2337);
ul,
li {
@@ -48,6 +50,50 @@ body {
}
}
body.xagi-color-weak {
filter: invert(80%);
}
.xagi-top-progress {
position: fixed;
top: 0;
left: 0;
z-index: 9999;
width: 0;
height: 2px;
background: var(--xagi-color-primary, #5d87ff);
box-shadow: 0 0 10px var(--xagi-color-primary, #5d87ff);
opacity: 0;
transition: width 0.42s ease, opacity 0.18s ease;
pointer-events: none;
}
.xagi-top-progress-active {
width: 100%;
opacity: 1;
}
.xagi-global-watermark {
position: fixed;
inset: 0;
z-index: 9998;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 72px 36px;
overflow: hidden;
padding: 72px 40px;
color: rgba(91, 105, 135, 0.14);
font-size: 18px;
font-weight: 600;
pointer-events: none;
span {
display: block;
transform: rotate(-24deg);
user-select: none;
}
}
// 滚动条
::-webkit-scrollbar {
width: 8px;
@@ -60,13 +106,13 @@ body {
}
// 滚动条滑块
::-webkit-scrollbar-thumb {
background: @GRAY19;
background: rgba(91, 105, 135, 0.28);
border-radius: 6px; /* 滚动条滑块圆角 */
transition: background-color 0.3s ease-in-out; /* 添加过渡动画 */
}
// 滚动条滑块悬停效果
::-webkit-scrollbar-thumb:hover {
background: @GRAY; /* 鼠标悬停时颜色加深 */
background: rgba(91, 105, 135, 0.48); /* 鼠标悬停时颜色加深 */
}
@media (max-width: 768px) {
@@ -93,7 +139,7 @@ body {
/* 滚动条的外层滑轨背景颜色 */
.scrollbar::-webkit-scrollbar-thumb {
background: @GRAY19;
background: rgba(91, 105, 135, 0.28);
border-radius: 6px;
transition: background-color 0.3s ease-in-out; /* 添加过渡动画 */
@@ -102,7 +148,7 @@ body {
/* 滚动条的内层滑块悬停效果 */
.scrollbar::-webkit-scrollbar-thumb:hover {
background: @GRAY; /* 鼠标悬停时颜色进一步加深 */
background: rgba(91, 105, 135, 0.48); /* 鼠标悬停时颜色进一步加深 */
}
/* 滚动条的内层滑块颜色 */
@@ -150,12 +196,223 @@ body {
.ant-table thead.ant-table-thead .ant-table-cell-fix-right,
.ant-table-container thead.ant-table-thead > tr > th,
.ant-table-content thead.ant-table-thead > tr > th {
background-color: #f0f2f5 !important;
background-color: #f4f7fb !important;
color: #5d6378 !important;
font-weight: 600;
}
// 表格分页下拉框边框颜色
.ant-pagination-options .ant-select-selector {
border-color: #f0f2f5 !important;
border-color: rgba(91, 105, 135, 0.14) !important;
}
.ant-modal-content,
.ant-popover-inner,
.ant-dropdown-menu,
.ant-select-dropdown,
.ant-picker-dropdown .ant-picker-panel-container {
border-radius: 8px !important;
box-shadow: 0 16px 42px rgba(32, 45, 84, 0.12) !important;
}
.ant-btn,
.ant-input,
.ant-input-affix-wrapper,
.ant-select-selector,
.ant-picker,
.ant-segmented {
border-radius: 6px !important;
}
.ant-card,
.ant-pro-card,
.ant-table-wrapper .ant-table,
.ant-pro-table .ant-pro-table-search {
border-radius: 8px !important;
}
body.xagi-layout-light {
.ant-input,
.ant-input-affix-wrapper,
.ant-select:not(.ant-select-customize-input) .ant-select-selector,
.ant-picker {
border-color: var(
--xagi-layout-border-primary,
rgba(91, 105, 135, 0.14)
) !important;
background: var(
--xagi-layout-bg-input,
rgba(255, 255, 255, 0.88)
) !important;
color: var(--xagi-layout-text-primary, #1f2337) !important;
}
.ant-input::placeholder,
.ant-input-affix-wrapper .ant-input::placeholder,
.ant-select-selection-placeholder {
color: var(--xagi-layout-text-tertiary, rgba(31, 35, 55, 0.52)) !important;
}
.ant-input-prefix,
.ant-input-suffix,
.ant-select-arrow,
.ant-select-clear,
.ant-picker-suffix {
color: var(--xagi-layout-text-tertiary, rgba(31, 35, 55, 0.52)) !important;
}
.ant-input-affix-wrapper:hover,
.ant-input:hover,
.ant-select:not(.ant-select-disabled):not(.ant-select-customize-input)
.ant-select-selector:hover,
.ant-picker:hover {
border-color: var(--xagi-color-primary, #5d87ff) !important;
}
.ant-input-affix-wrapper-focused,
.ant-input:focus,
.ant-select-focused:not(.ant-select-disabled):not(.ant-select-customize-input)
.ant-select-selector,
.ant-picker-focused {
border-color: var(--xagi-color-primary, #5d87ff) !important;
box-shadow: 0 0 0 2px rgba(93, 135, 255, 0.14) !important;
}
.ant-btn-default:not(.ant-btn-dangerous):not(.ant-btn-color-dangerous),
.ant-btn-color-default.ant-btn-variant-outlined {
border-color: var(
--xagi-layout-border-primary,
rgba(91, 105, 135, 0.14)
) !important;
background: var(
--xagi-layout-bg-input,
rgba(255, 255, 255, 0.88)
) !important;
color: var(--xagi-layout-text-primary, #1f2337) !important;
box-shadow: none !important;
}
.ant-btn-default:not(.ant-btn-dangerous):not(.ant-btn-color-dangerous):not(
:disabled
):not(.ant-btn-disabled):hover,
.ant-btn-color-default.ant-btn-variant-outlined:not(:disabled):not(
.ant-btn-disabled
):hover {
border-color: var(--xagi-color-primary, #5d87ff) !important;
background: rgba(93, 135, 255, 0.1) !important;
background: color-mix(
in srgb,
var(--xagi-color-primary, #5d87ff) 10%,
#fff
) !important;
color: var(--xagi-color-primary, #5d87ff) !important;
}
}
body.xagi-layout-light {
.ant-modal-content,
.ant-popover-inner,
.ant-dropdown-menu,
.ant-select-dropdown,
.ant-picker-dropdown .ant-picker-panel-container {
background: var(--xagi-layout-bg-container, #fff) !important;
color: var(--xagi-layout-text-primary, #1f2337) !important;
}
.ant-select-item,
.ant-dropdown-menu-item,
.ant-dropdown-menu-submenu-title {
color: var(--xagi-layout-text-primary, #1f2337) !important;
}
.ant-select-item-option-active:not(.ant-select-item-option-disabled),
.ant-dropdown-menu-item-active,
.ant-dropdown-menu-submenu-title-active {
background: rgba(93, 135, 255, 0.08) !important;
background: color-mix(
in srgb,
var(--xagi-color-primary, #5d87ff) 8%,
#fff
) !important;
}
.ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
background: rgba(93, 135, 255, 0.12) !important;
background: color-mix(
in srgb,
var(--xagi-color-primary, #5d87ff) 14%,
#fff
) !important;
color: var(--xagi-color-primary, #5d87ff) !important;
}
}
html[data-theme='dark'] {
background-color: var(--xagi-layout-bg-primary, #080a0f);
body {
background: var(--xagi-layout-bg-primary, #080a0f);
color: var(--xagi-layout-text-primary, #fff);
}
::-webkit-scrollbar-thumb,
.scrollbar::-webkit-scrollbar-thumb,
.x-pro-table .ant-table-body::-webkit-scrollbar-thumb,
.x-pro-table .ant-table-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2) !important;
}
::-webkit-scrollbar-thumb:hover,
.scrollbar::-webkit-scrollbar-thumb:hover,
.x-pro-table .ant-table-body::-webkit-scrollbar-thumb:hover,
.x-pro-table .ant-table-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.34) !important;
}
.scrollbar::-webkit-scrollbar-track,
.scrollbar::-webkit-scrollbar-track-piece,
.scrollbar::-webkit-scrollbar-button {
background-color: transparent;
}
.ant-pagination-options .ant-select-selector,
.ant-table thead.ant-table-thead > tr > th,
.ant-table thead.ant-table-thead .ant-table-cell-fix-left,
.ant-table thead.ant-table-thead .ant-table-cell-fix-right,
.ant-table-container thead.ant-table-thead > tr > th,
.ant-table-content thead.ant-table-thead > tr > th {
background-color: rgba(255, 255, 255, 0.04) !important;
color: var(
--xagi-layout-text-secondary,
rgba(255, 255, 255, 0.85)
) !important;
}
.ant-pagination-options .ant-select-selector {
border-color: var(
--xagi-layout-border-primary,
rgba(255, 255, 255, 0.12)
) !important;
}
.ant-modal-content,
.ant-popover-inner,
.ant-dropdown-menu,
.ant-select-dropdown,
.ant-picker-dropdown .ant-picker-panel-container {
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.48) !important;
}
.ant-card,
.ant-pro-card,
.ant-table-wrapper .ant-table,
.ant-pro-table .ant-pro-table-search {
border-color: var(
--xagi-layout-border-secondary,
rgba(255, 255, 255, 0.08)
) !important;
background: var(--xagi-layout-bg-container, #111620) !important;
}
}
// XProTable 表格滚动条样式:显示滑块,隐藏轨道
@@ -175,17 +432,17 @@ body {
// 显示滚动条滑块
&::-webkit-scrollbar-thumb {
background: @GRAY19 !important;
background: rgba(91, 105, 135, 0.28) !important;
border-radius: 6px !important;
}
&::-webkit-scrollbar-thumb:hover {
background: @GRAY !important;
background: rgba(91, 105, 135, 0.48) !important;
}
// 针对 Firefox - 使用 thin 模式保留滑块
scrollbar-width: thin !important;
scrollbar-color: @GRAY19 transparent !important;
scrollbar-color: rgba(91, 105, 135, 0.28) transparent !important;
}
}

View File

@@ -8,11 +8,14 @@
*/
import {
UnifiedThemeBasicConfig,
UnifiedThemeData,
unifiedThemeService,
} from '@/services/unifiedThemeService';
import {
ThemeLayoutColorStyle,
ThemeMenuLayoutType,
ThemeMenuStyleType,
ThemeNavigationStyleType,
} from '@/types/enums/theme';
import { useCallback, useEffect, useMemo, useState } from 'react';
@@ -45,6 +48,14 @@ interface UseUnifiedThemeReturn {
style: ThemeNavigationStyleType,
options?: UpdateOptions,
) => Promise<void>;
updateMenuLayout: (
layout: ThemeMenuLayoutType,
options?: UpdateOptions,
) => Promise<void>;
updateMenuStyle: (
style: ThemeMenuStyleType,
options?: UpdateOptions,
) => Promise<void>;
updateLayoutStyle: (
style: ThemeLayoutColorStyle,
options?: UpdateOptions,
@@ -57,6 +68,10 @@ interface UseUnifiedThemeReturn {
language: 'zh-CN' | 'en-US',
options?: UpdateOptions,
) => Promise<void>;
updateBasicConfig: (
config: Partial<UnifiedThemeBasicConfig>,
options?: UpdateOptions,
) => Promise<void>;
// 便捷方法
toggleAntdTheme: () => Promise<void>;
@@ -69,9 +84,12 @@ interface UseUnifiedThemeReturn {
primaryColor: string;
antdTheme: 'light' | 'dark';
navigationStyle: ThemeNavigationStyleType;
menuLayout: ThemeMenuLayoutType;
menuStyle: ThemeMenuStyleType;
layoutStyle: ThemeLayoutColorStyle;
backgroundId: string;
language: 'zh-CN' | 'en-US';
basicConfig: UnifiedThemeBasicConfig;
isDarkMode: boolean;
isNavigationDark: boolean;
isNavigationExpanded: boolean;
@@ -133,6 +151,12 @@ export const useUnifiedTheme = (): UseUnifiedThemeReturn => {
const updateNavigationStyle = createUpdateMethod(
unifiedThemeService.updateNavigationStyle.bind(unifiedThemeService),
);
const updateMenuLayout = createUpdateMethod(
unifiedThemeService.updateMenuLayout.bind(unifiedThemeService),
);
const updateMenuStyle = createUpdateMethod(
unifiedThemeService.updateMenuStyle.bind(unifiedThemeService),
);
const updateLayoutStyle = createUpdateMethod(
unifiedThemeService.updateLayoutStyle.bind(unifiedThemeService),
);
@@ -142,6 +166,18 @@ export const useUnifiedTheme = (): UseUnifiedThemeReturn => {
const updateLanguage = createUpdateMethod(
unifiedThemeService.updateLanguage.bind(unifiedThemeService),
);
const updateBasicConfig = createUpdateMethod(
async (
config: Partial<UnifiedThemeBasicConfig>,
options?: UpdateOptions,
) => {
const current = unifiedThemeService.getCurrentData().basicConfig;
await unifiedThemeService.updateData(
{ basicConfig: { ...current, ...config } },
options,
);
},
);
// 便捷切换方法
const toggleAntdTheme = useCallback(async () => {
@@ -196,9 +232,12 @@ export const useUnifiedTheme = (): UseUnifiedThemeReturn => {
updatePrimaryColor,
updateAntdTheme,
updateNavigationStyle,
updateMenuLayout,
updateMenuStyle,
updateLayoutStyle,
updateBackground,
updateLanguage,
updateBasicConfig,
// 便捷方法
toggleAntdTheme,
@@ -211,9 +250,12 @@ export const useUnifiedTheme = (): UseUnifiedThemeReturn => {
primaryColor: data.primaryColor,
antdTheme: data.antdTheme,
navigationStyle: data.navigationStyle,
menuLayout: data.menuLayout,
menuStyle: data.menuStyle,
layoutStyle: data.layoutStyle,
backgroundId: data.backgroundId,
language: data.language,
basicConfig: data.basicConfig,
isDarkMode,
isNavigationDark,
isNavigationExpanded,

View File

@@ -3,41 +3,37 @@
.collapse-button {
position: absolute;
bottom: 44px;
width: 20px;
height: 20px;
bottom: 34px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background-color: @colorBgElevated;
border: @lineWidth solid @colorSplit;
background-color: rgba(255, 255, 255, 0.96);
border: 1px solid rgba(91, 105, 135, 0.14);
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease-in-out;
transition: transform 0.24s ease, border-color 0.24s ease,
box-shadow 0.24s ease, color 0.24s ease;
z-index: 999;
transform: translateX(-50%);
box-shadow: 0 10px 20px rgba(32, 45, 84, 0.1);
&:hover {
transform: translateX(-50%) scale(1.1);
border-color: @colorPrimary;
transform: translateX(-50%) scale(1.08);
box-shadow: 0 14px 24px rgba(32, 45, 84, 0.14);
}
.icon {
color: @colorTextSecondary;
transition: color 0.2s ease-in-out;
color: #5d6378;
font-size: 12px;
transition: color 0.24s ease;
}
//&:hover .icon {
// color: @colorText;
//}
//
//// 收起状态时的样式调整
//&.collapsed {
// &:hover {
// .icon {
// color: @colorText;
// }
// }
//}
&:hover .icon {
color: @colorPrimary;
}
}
// 移动端隐藏收起按钮

View File

@@ -2,7 +2,10 @@ import SvgIcon from '@/components/base/SvgIcon';
import { NAVIGATION_LAYOUT_SIZES } from '@/constants/layout.constants';
import { useUnifiedTheme } from '@/hooks/useUnifiedTheme';
import { dict } from '@/services/i18nRuntime';
import { ThemeNavigationStyleType } from '@/types/enums/theme';
import {
ThemeMenuLayoutType,
ThemeNavigationStyleType,
} from '@/types/enums/theme';
import { Tooltip } from 'antd';
import classNames from 'classnames';
import React, { useEffect } from 'react';
@@ -18,7 +21,7 @@ const cx = classNames.bind(styles);
const CollapseButton: React.FC = () => {
const { isSecondMenuCollapsed, setIsSecondMenuCollapsed } =
useModel('layout');
const { navigationStyle } = useUnifiedTheme();
const { navigationStyle, menuLayout } = useUnifiedTheme();
const [searchParams] = useSearchParams();
const location = useLocation();
@@ -84,6 +87,19 @@ const CollapseButton: React.FC = () => {
: NAVIGATION_LAYOUT_SIZES.FIRST_MENU_WIDTH.STYLE1;
const menuTotalWidth =
NAVIGATION_LAYOUT_SIZES.getTotalMenuWidth(navigationStyle);
const collapsedLeft =
menuLayout === ThemeMenuLayoutType.MIXED ? 0 : firstMenuWidth;
const expandedLeft =
menuLayout === ThemeMenuLayoutType.MIXED
? NAVIGATION_LAYOUT_SIZES.SECOND_MENU_WIDTH
: menuTotalWidth;
if (
menuLayout === ThemeMenuLayoutType.HORIZONTAL ||
menuLayout === ThemeMenuLayoutType.VERTICAL
) {
return null;
}
// 处理点击事件保存用户操作到localStorage
const handleToggleCollapse = () => {
@@ -115,7 +131,7 @@ const CollapseButton: React.FC = () => {
})}
onClick={handleToggleCollapse}
style={{
left: isSecondMenuCollapsed ? firstMenuWidth : menuTotalWidth,
left: isSecondMenuCollapsed ? collapsedLeft : expandedLeft,
}}
>
<SvgIcon

View File

@@ -697,8 +697,19 @@ const DynamicSecondMenu: React.FC<DynamicSecondMenuProps> = ({
onToggle={() => toggleExpand(menuCode)}
/>
{/* 递归渲染子菜单 */}
{isExpanded &&
menu.children?.map((child) => renderMenuItem(child, level + 1))}
<div
className="flex flex-col gap-4"
style={{
maxHeight: isExpanded ? 720 : 0,
opacity: isExpanded ? 1 : 0,
overflow: 'hidden',
transform: isExpanded ? 'translateY(0)' : 'translateY(-4px)',
transition:
'max-height 260ms cubic-bezier(0.4, 0, 0.2, 1), opacity 220ms ease, transform 220ms ease',
}}
>
{menu.children?.map((child) => renderMenuItem(child, level + 1))}
</div>
</div>
);
},

View File

@@ -2,18 +2,18 @@
@import '@/styles/token';
.box {
margin-bottom: @marginXs;
margin-bottom: 0;
color: @navFirstMenuTextSecondary;
transition: background-color 0.3s, color 0.3s;
transition: transform 0.26s ease, color 0.26s ease;
user-select: none; /* 禁止选中 */
.active-icon-container {
font-size: 20px;
font-size: 24px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
border-radius: @borderRadius;
border-radius: 8px;
.icon-image {
width: 20px;
@@ -22,8 +22,9 @@
}
.active-box {
border-radius: @borderRadius;
transition: all 0.3s ease;
border-radius: 14px;
transition: transform 0.26s ease, background 0.26s ease,
box-shadow 0.26s ease, color 0.26s ease;
display: flex;
flex-direction: column;
align-items: center;
@@ -31,11 +32,12 @@
}
&:hover {
color: @colorTextSecondary;
color: @colorPrimary;
transform: translateY(-1px);
.active-box {
background: @navItemHoverBg;
box-shadow: @navShadow;
background: rgba(255, 255, 255, 0.7);
box-shadow: 0 10px 18px rgba(32, 45, 84, 0.08);
}
}
@@ -43,17 +45,18 @@
color: @colorPrimary;
.active-box {
background: @navItemActiveBg;
box-shadow: @navShadow;
background: #fff;
box-shadow: 0 12px 22px rgba(32, 45, 84, 0.1);
}
}
// 支持导航风格2的文字显示
.text {
width: 100%;
margin-top: @marginXxs;
font-size: @navFirstMenuFontSize;
transition: all 0.3s ease;
margin-top: 4px;
font-size: 12px;
line-height: 18px;
transition: all 0.26s ease;
text-align: center;
white-space: nowrap;
overflow: hidden;

View File

@@ -35,12 +35,12 @@ const TabItem: React.FC<TabItemProps & { isSecondMenuCollapsed?: boolean }> = ({
const navStyle: React.CSSProperties = useMemo(() => {
return isStyle2
? {
width: '64px',
width: '88px',
height: '64px',
}
: {
width: '40px',
height: '40px',
width: '58px',
height: '58px',
padding: 0,
};
}, [isStyle2]);

View File

@@ -1,8 +1,11 @@
.dynamic-tabs-container {
align-items: flex-end;
padding-right: 2px;
align-items: center;
gap: 12px;
padding: 10px 0 18px;
}
.style-two {
align-items: flex-end;
align-items: center;
gap: 10px;
padding: 10px 8px 18px;
}

View File

@@ -1,9 +1,10 @@
@import '@/styles/color';
.logo {
width: 32px;
height: 32px;
border-radius: 8px;
width: 48px;
height: 48px;
border-radius: 14px;
box-shadow: 0 10px 22px rgba(93, 135, 255, 0.28);
}
.add-agent {
@@ -19,9 +20,9 @@
}
.logo-container {
height: 52px;
height: 58px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
margin-bottom: 18px;
}

View File

@@ -1,12 +1,19 @@
@import '@/styles/token';
.row {
height: 36px;
padding: @paddingSm;
gap: @paddingSm;
color: @colorTextSecondary;
height: 42px;
padding: 0 12px;
gap: 10px;
color: #5d6378;
border-radius: 6px;
transition: background 0.24s ease, color 0.24s ease, transform 0.24s ease;
&:hover {
transform: translateX(2px);
}
span {
font-size: @fontSize;
font-size: 14px;
line-height: 42px;
}
}

View File

@@ -2,14 +2,19 @@
@import '@/styles/token';
.user {
width: 32px;
height: 32px;
margin-top: 5px;
margin-bottom: 5px;
width: 42px;
height: 42px;
margin: 0;
border-radius: 50%;
overflow: hidden;
box-sizing: content-box;
border: @lineWidth solid @colorBorder;
border: 2px solid #fff;
box-shadow: 0 8px 18px rgba(32, 45, 84, 0.12);
transition: transform 0.26s ease;
&:hover {
transform: scale(1.04);
}
img {
width: 100%;

View File

@@ -2,53 +2,56 @@
@import '@/styles/token';
.container {
width: 140px;
padding-top: 6px;
background: @navFirstMenuBg;
color: @navFirstMenuText;
border: 1px solid @navBorderColor;
border-radius: @borderRadius;
box-shadow: @navShadow;
width: 184px;
padding: 8px;
background: rgba(255, 255, 255, 0.96);
color: #1f2337;
border: 1px solid rgba(91, 105, 135, 0.12);
border-radius: 8px;
box-shadow: 0 16px 42px rgba(32, 45, 84, 0.12);
backdrop-filter: blur(16px);
.cursor-default {
cursor: default;
}
.divider-horizontal {
position: absolute;
left: 0;
right: 0;
bottom: 50px;
border-bottom: 1px solid @navDividerColor;
margin: 6px 4px 0;
border-bottom: 1px solid rgba(91, 105, 135, 0.1);
}
.log-out {
height: 51px;
color: @colorTextSecondary;
height: 42px;
color: #5d6378;
&:hover {
background: @colorBgElevated;
background: rgba(249, 57, 32, 0.08);
color: @colorError;
}
}
// 每个菜单项的默认样式
.item {
transition: all 0.3s ease;
transition: all 0.24s ease;
&:hover {
background: @colorBgElevated;
color: @colorTextSecondary;
background: rgba(93, 135, 255, 0.1);
color: @colorPrimary;
}
}
}
.user-avatar-container {
margin-bottom: 32px;
margin-top: 24px;
transition: all 0.3s ease;
margin-top: 22px;
margin-bottom: 24px;
padding: 6px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.58);
transition: transform 0.26s ease, background 0.26s ease, box-shadow 0.26s ease;
&:hover {
transform: scale(1.05);
background: #fff;
box-shadow: 0 12px 22px rgba(32, 45, 84, 0.1);
transform: translateY(-1px);
}
}

View File

@@ -2,30 +2,32 @@
@import '@/styles/token';
.user-icon {
margin-bottom: @marginXs;
margin-bottom: 12px;
color: @navFirstMenuTextSecondary;
transition: background-color 0.3s, color 0.3s;
transition: transform 0.26s ease, color 0.26s ease;
user-select: none;
.active-icon-container {
color: @navFirstMenuTextSecondary;
display: flex;
width: 40px;
height: 40px;
padding: @paddingXs;
width: 58px;
height: 58px;
padding: 0;
justify-content: center;
align-items: center;
border-radius: @borderRadius;
transition: all 0.3s ease;
border-radius: 14px;
font-size: 24px;
transition: background 0.26s ease, box-shadow 0.26s ease, color 0.26s ease;
}
&:hover {
color: @colorTextSecondary;
color: @colorPrimary;
transform: translateY(-1px);
.active-icon-container {
color: @colorTextSecondary;
background: @navItemHoverBg;
box-shadow: @navShadow;
color: @colorPrimary;
background: #fff;
box-shadow: 0 12px 22px rgba(32, 45, 84, 0.1);
}
}
@@ -35,11 +37,12 @@
:global {
.ant-badge-count {
top: 10px;
right: 10px;
top: 8px;
right: 8px;
background: @colorError;
color: @colorWhite;
border: @lineWidth solid @navBorderColor;
border: 2px solid #fff;
box-shadow: 0 4px 10px rgba(32, 45, 84, 0.12);
}
sup {
@@ -53,8 +56,8 @@
.ant-badge-multiple-words {
//line-height: 12px;
padding: 0 2px !important;
top: 8px;
right: 8px;
top: 6px;
right: 6px;
font-size: 10px;
}
}

View File

@@ -2,21 +2,24 @@
@import '@/styles/token';
.mobile-container {
box-shadow: 12px 0 24px -4px rgba(0, 0, 0, 12%);
border-radius: 0 @borderRadiusLg @borderRadiusLg 0;
box-shadow: 12px 0 28px rgba(32, 45, 84, 12%);
border-radius: 0 8px 8px 0;
}
.container {
overflow: hidden;
// background-color: @colorBgLayout;
background: var(--xagi-layout-bg-secondary, #f2f5fb);
border-right: @lineWidth solid var(--xagi-layout-border-primary);
height: 100%;
.first-menus {
// 使用动态导航宽度token
width: @navFirstMenuWidth;
padding-top: 16px;
padding-top: 22px;
background: @navFirstMenuBg;
color: @navFirstMenuText;
box-shadow: @navFirstMenuShadow;
border-right: @lineWidth solid var(--xagi-layout-border-primary);
transition: width 0.3s ease, background-color 0.3s ease;
// 风格2适配
@@ -29,16 +32,16 @@
display: flex;
flex-direction: column;
height: 100%;
padding-top: @padding;
padding-top: 22px;
// width: 200px;
box-sizing: border-box;
border-left: @lineWidth solid var(--xagi-layout-border-primary);
border-left: 0;
transition: width 0.3s ease-in-out, opacity 0.3s ease-in-out,
padding-left 0.3s ease-in-out, background-color 0.3s ease;
overflow: hidden;
position: relative;
background: @navSecondMenuBg;
border-radius: @borderRadiusLg 0 0 @borderRadiusLg;
border-radius: 0;
.nav-menus-scroll {
flex: 1;
@@ -46,20 +49,639 @@
}
.integral-footer {
padding: @marginSm 0 0;
margin-right: @margin;
padding: @marginSm 0 18px;
margin-right: 18px;
}
.menu-title {
font-size: 20px !important;
line-height: 28px !important;
font-weight: 600 !important;
color: @navSecondMenuText;
}
// 风格2下的二级导航背景适配
.xagi-nav-style2 & {
background: var(--xagi-color-bg-container);
background: var(--xagi-layout-bg-container, #ffffff);
box-shadow: 10px 0 24px rgba(32, 45, 84, 0.05);
}
}
}
.topMenuBar {
position: relative;
z-index: 2;
display: flex;
align-items: center;
width: 100%;
height: 64px;
min-height: 64px;
padding: 0 20px;
border-bottom: @lineWidth solid var(--xagi-layout-border-primary);
background: var(--xagi-layout-bg-container, #fff);
box-shadow: 0 1px 0 rgba(32, 45, 84, 0.03);
}
.topLogo,
.verticalLogo {
display: flex;
align-items: center;
flex-shrink: 0;
}
.topLogo {
min-width: 226px;
padding-right: 18px;
.brandLogo {
width: 36px;
height: 36px;
border-radius: 9px;
}
.brandText {
margin-left: 10px;
font-size: 18px !important;
font-weight: 600;
line-height: 24px;
letter-spacing: 0;
}
}
.brandLogo {
width: 44px;
height: 44px;
flex-shrink: 0;
object-fit: contain;
border-radius: 10px;
}
.brandText {
margin-left: 12px;
color: var(--xagi-layout-text-primary, #25283f) !important;
font-size: 20px !important;
font-weight: 600;
line-height: 28px;
white-space: nowrap;
}
.topMenuScroll {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
height: 100%;
gap: 6px;
overflow: hidden;
}
.topMenuItem,
.topActionButton,
.verticalMenuItem {
border: 0;
background: transparent;
color: var(--xagi-layout-text-secondary, #4d5876);
cursor: pointer;
transition: color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease,
transform 0.2s ease;
}
.topMenuItem {
position: relative;
display: flex;
align-items: center;
height: 40px;
max-width: 168px;
padding: 0 10px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
line-height: 20px;
white-space: nowrap;
&:hover,
&.active {
color: @colorPrimary;
background: rgba(93, 135, 255, 0.08);
}
&.active::after {
content: '';
position: absolute;
left: 18px;
right: 18px;
bottom: -12px;
height: 2px;
border-radius: 2px;
background: @colorPrimary;
}
}
.topMenuIcon,
.verticalMenuIcon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 22px;
}
.topMenuIcon {
width: 18px;
font-size: 18px;
}
.verticalMenuIcon {
width: 18px;
font-size: 18px;
}
.topMenuText,
.verticalMenuText {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.topMenuText {
margin-left: 8px;
}
.topMenuArrow,
.verticalMenuArrow {
flex-shrink: 0;
margin-left: 7px;
font-size: 12px;
color: var(--xagi-layout-text-tertiary, #7f8aa8);
}
.topMenuArrow {
font-size: 11px;
}
.verticalMenuArrow {
font-size: 11px;
color: var(--xagi-layout-text-tertiary, #8a92a6);
}
.topActions {
display: flex;
align-items: center;
flex-shrink: 0;
gap: 8px;
padding-left: 16px;
}
.topActionButton {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
font-size: 19px;
&:hover,
&.active {
color: @colorPrimary;
background: rgba(93, 135, 255, 0.1);
}
}
.topDropdownMenu {
width: 236px;
max-height: 68vh;
padding: 8px;
overflow: hidden auto;
border: @lineWidth solid var(--xagi-layout-border-secondary);
border-radius: 8px;
background: var(--xagi-layout-bg-container, #fff);
box-shadow: 0 16px 42px rgba(32, 45, 84, 0.14);
}
.topDropdownGroup {
display: flex;
flex-direction: column;
margin-bottom: 2px;
&:last-child {
margin-bottom: 0;
}
}
.topDropdownItem {
display: flex;
align-items: center;
width: 100%;
height: 36px;
padding: 0 10px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--xagi-layout-text-secondary, #626a80);
cursor: pointer;
font-size: 14px;
font-weight: 500;
line-height: 20px;
text-align: left;
transition: color 0.2s ease, background 0.2s ease;
&:hover,
&.active {
color: @colorPrimary;
background: rgba(93, 135, 255, 0.08);
}
&.open {
color: var(--xagi-layout-text-secondary, #626a80);
.topDropdownArrow {
transform: rotate(180deg);
}
}
}
.topDropdownLevel1,
.topDropdownLevel2 {
height: 32px;
color: var(--xagi-layout-text-secondary, #677086);
font-weight: 400;
}
.topDropdownLevel1 {
padding-left: 34px;
}
.topDropdownLevel2 {
padding-left: 46px;
}
.topDropdownIcon {
display: flex;
align-items: center;
justify-content: center;
width: 18px;
flex-shrink: 0;
font-size: 18px;
}
.topDropdownText {
flex: 1;
min-width: 0;
margin-left: 9px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.topDropdownBullet {
width: 3px;
height: 3px;
flex-shrink: 0;
border-radius: 50%;
background: currentColor;
opacity: 0.45;
}
.topDropdownArrow {
flex-shrink: 0;
margin-left: 8px;
color: var(--xagi-layout-text-tertiary, #8a92a6);
font-size: 11px;
transition: transform 0.2s ease, color 0.2s ease;
}
.topDropdownChildren {
padding: 2px 0 4px;
}
.verticalMenu {
display: flex;
flex-direction: column;
width: 252px;
height: 100%;
padding: 18px 12px 14px;
border-right: @lineWidth solid var(--xagi-layout-border-primary);
background: var(--xagi-layout-bg-container, #fff);
}
.verticalLogo {
height: 42px;
margin-bottom: 18px;
.brandLogo {
width: 40px;
height: 40px;
border-radius: 10px;
}
.brandText {
margin-left: 12px;
color: var(--xagi-layout-text-primary, #25283f) !important;
font-size: 18px !important;
font-weight: 600;
line-height: 24px;
letter-spacing: 0;
}
}
.verticalMenuList {
flex: 1;
min-height: 0;
padding-right: 2px;
overflow: hidden auto;
}
.verticalMenuGroup {
display: flex;
flex-direction: column;
margin-bottom: 2px;
}
.verticalMenuItem {
display: flex;
align-items: center;
width: 100%;
height: 40px;
padding: 0 12px;
margin: 0;
border-radius: 8px;
text-align: left;
font-size: 14px;
font-weight: 500;
line-height: 20px;
color: var(--xagi-layout-text-secondary, #626a80);
&:hover,
&.active {
color: @colorPrimary;
background: var(--xagi-menu-item-active-bg, rgba(93, 135, 255, 0.09));
box-shadow: var(--xagi-menu-item-active-shadow, none);
}
&.open {
color: @colorPrimary;
.verticalMenuArrow {
transform: rotate(180deg);
}
}
}
.verticalMenuLevel1,
.verticalMenuLevel2,
.verticalMenuLevel3 {
height: 34px;
color: var(--xagi-layout-text-secondary, #677086);
font-size: 14px;
font-weight: 400;
line-height: 20px;
&:hover,
&.active {
background: var(--xagi-menu-item-hover-bg, rgba(93, 135, 255, 0.07));
}
}
.verticalMenuLevel1 {
padding-left: 42px;
}
.verticalMenuLevel2 {
padding-left: 54px;
}
.verticalMenuLevel3 {
padding-left: 66px;
}
.verticalMenuText {
flex: 1;
margin-left: 10px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.verticalMenuLevel1,
.verticalMenuLevel2,
.verticalMenuLevel3 {
.verticalMenuText {
margin-left: 10px;
}
}
.verticalSubMenu {
overflow: hidden;
padding: 4px 0 2px;
transition: max-height 0.24s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease;
}
.verticalMenuBullet {
width: 3px;
height: 3px;
flex-shrink: 0;
border-radius: 50%;
background: currentColor;
opacity: 0.45;
}
.verticalFooter {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding-top: 12px;
margin-top: 12px;
border-top: @lineWidth solid var(--xagi-layout-border-secondary);
}
.verticalActions {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.verticalActionButton {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
flex-shrink: 0;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--xagi-layout-text-secondary, #5d6378);
cursor: pointer;
transition: color 0.2s ease, background 0.2s ease;
&:hover,
&.active {
color: @colorPrimary;
background: rgba(93, 135, 255, 0.11);
}
}
.verticalActionButton {
font-size: 20px;
}
.verticalUserAvatar {
width: 36px;
height: 36px;
flex-shrink: 0;
> div {
width: 36px;
height: 36px;
border-width: 1px;
box-shadow: 0 8px 18px rgba(32, 45, 84, 0.1);
}
}
.menuIconImg {
width: 22px;
height: 22px;
object-fit: contain;
}
.topMenuIcon .menuIconImg,
.topDropdownIcon .menuIconImg {
width: 18px;
height: 18px;
}
.verticalMenuIcon .menuIconImg {
width: 18px;
height: 18px;
}
.mixedSide {
.nav-menus {
border-right: @lineWidth solid var(--xagi-layout-border-primary);
}
}
.menu-layout-horizontal {
border-right: 0;
}
.menu-layout-vertical {
border-right: 0;
}
.railReloadButton {
width: 36px;
height: 36px;
margin-bottom: 8px;
color: var(--xagi-layout-text-secondary, #626a80);
border-radius: 8px !important;
&:hover {
color: @colorPrimary !important;
background: var(
--xagi-menu-item-hover-bg,
rgba(93, 135, 255, 0.07)
) !important;
}
}
:global(html[data-theme='light'][data-menu-style='design']) {
.menu-layout-vertical .verticalMenu,
.mixedSide .nav-menus {
--xagi-layout-text-primary: #1f2337;
--xagi-layout-text-secondary: #626a80;
--xagi-layout-text-tertiary: #8a92a6;
--xagi-layout-second-menu-text-color: #1f2337;
--xagi-layout-second-menu-text-color-secondary: #626a80;
--xagi-menu-item-active-bg: #ffffff;
--xagi-menu-item-hover-bg: rgba(255, 255, 255, 0.72);
--xagi-menu-item-active-shadow: 0 10px 20px rgba(32, 45, 84, 0.1);
--xagi-menu-item-hover-shadow: 0 8px 16px rgba(32, 45, 84, 0.06);
background: #f2f5fb;
border-color: rgba(91, 105, 135, 0.12);
}
}
:global(html[data-theme='light'][data-menu-style='light']) {
.menu-layout-vertical .verticalMenu,
.mixedSide .nav-menus {
--xagi-layout-text-primary: #1f2337;
--xagi-layout-text-secondary: #5d6378;
--xagi-layout-text-tertiary: #8790a5;
--xagi-layout-second-menu-text-color: #1f2337;
--xagi-layout-second-menu-text-color-secondary: #5d6378;
--xagi-menu-item-active-bg: rgba(93, 135, 255, 0.1);
--xagi-menu-item-hover-bg: rgba(93, 135, 255, 0.07);
--xagi-menu-item-active-shadow: none;
--xagi-menu-item-hover-shadow: none;
background: #ffffff;
border-color: rgba(91, 105, 135, 0.12);
box-shadow: 10px 0 24px rgba(32, 45, 84, 0.04);
}
}
:global(html[data-theme='light'][data-menu-style='dark']) {
.menu-layout-vertical .verticalMenu,
.mixedSide .nav-menus {
--xagi-layout-text-primary: rgba(255, 255, 255, 0.95);
--xagi-layout-text-secondary: rgba(255, 255, 255, 0.78);
--xagi-layout-text-tertiary: rgba(255, 255, 255, 0.56);
--xagi-layout-second-menu-text-color: rgba(255, 255, 255, 0.95);
--xagi-layout-second-menu-text-color-secondary: rgba(255, 255, 255, 0.78);
--xagi-menu-item-active-bg: rgba(93, 135, 255, 0.2);
--xagi-menu-item-hover-bg: rgba(255, 255, 255, 0.08);
--xagi-menu-item-active-shadow: none;
--xagi-menu-item-hover-shadow: none;
background: #111827;
border-color: rgba(255, 255, 255, 0.08);
box-shadow: none;
}
.menu-layout-vertical .verticalFooter {
border-color: rgba(255, 255, 255, 0.08);
}
}
:global(html[data-theme='dark']) {
.topMenuBar,
.verticalMenu {
background: var(--xagi-layout-bg-container, #111620);
border-color: var(
--xagi-layout-border-secondary,
rgba(255, 255, 255, 0.08)
);
box-shadow: none;
}
.topMenuItem,
.topActionButton,
.topDropdownItem,
.verticalMenuItem {
color: var(--xagi-layout-text-secondary, rgba(255, 255, 255, 0.85));
&:hover,
&.active {
color: @colorPrimary;
background: rgba(93, 135, 255, 0.18);
}
}
.topDropdownMenu {
background: var(--xagi-layout-bg-container, #111620);
border-color: var(
--xagi-layout-border-secondary,
rgba(255, 255, 255, 0.08)
);
box-shadow: 0 16px 42px rgba(0, 0, 0, 0.24);
}
}
// 风格1下的二级导航样式适配
:global(.xagi-nav-style1) {
.container {

View File

@@ -8,12 +8,13 @@
* 3. 复用现有的 Header、User、UserOperateArea、TabItem、SecondMenuItem 等组件
*/
import HoverScrollbar from '@/components/base/HoverScrollbar';
import SvgIcon from '@/components/base/SvgIcon';
import ConditionRender from '@/components/ConditionRender';
import { NAVIGATION_LAYOUT_SIZES } from '@/constants/layout.constants';
import { useUnifiedTheme } from '@/hooks/useUnifiedTheme';
import { dict } from '@/services/i18nRuntime';
import type { MenuItemDto } from '@/types/interfaces/menu';
import { theme, Typography } from 'antd';
import { Badge, Button, Popover, theme, Typography } from 'antd';
import classNames from 'classnames';
import React, {
useCallback,
@@ -27,9 +28,11 @@ import DynamicSecondMenu from './DynamicSecondMenu';
import DynamicTabs from './DynamicTabs';
// 复用原有组件
import CreditsBalance from '@/components/business-component/CreditsBalance';
import { ReloadOutlined } from '@ant-design/icons';
import CollapseButton from './CollapseButton';
import Header from './Header';
import User from './User';
import UserAvatar from './User/UserAvatar';
import UserOperateArea from './UserOperateArea';
// 复用原有样式
import { PATH_URL } from '@/constants/home.constants';
@@ -41,13 +44,24 @@ import {
OTHER_MENU_CODES,
} from '@/constants/menus.constants';
import useConversation from '@/hooks/useConversation';
import { ThemeNavigationStyleType } from '@/types/enums/theme';
import { RoleEnum } from '@/types/enums/common';
import { AllowDevelopEnum, SpaceTypeEnum } from '@/types/enums/space';
import {
ThemeLayoutColorStyle,
ThemeMenuLayoutType,
ThemeMenuStyleType,
ThemeNavigationStyleType,
} from '@/types/enums/theme';
import EcosystemMarketSection from './EcosystemMarketSection';
import HomeSection from './HomeSection';
import styles from './index.less';
import SpaceSection from './SpaceSection';
import SquareSection from './SquareSection';
import { handleOpenUrl, normalizeMenuPathname } from './utils';
import {
handleOpenUrl,
normalizeMenuPathname,
updatePathUrlToLocalStorage,
} from './utils';
const cx = classNames.bind(styles);
@@ -56,6 +70,8 @@ export interface DynamicMenusLayoutProps {
overrideContainerStyle?: React.CSSProperties;
/** 是否为移动端 */
isMobile?: boolean;
/** 顶部/侧边渲染区域 */
region?: 'auto' | 'top' | 'side';
}
/**
@@ -64,20 +80,25 @@ export interface DynamicMenusLayoutProps {
const DynamicMenusLayout: React.FC<DynamicMenusLayoutProps> = ({
overrideContainerStyle,
isMobile = false,
region = 'auto',
}) => {
const location = useLocation();
const params = useParams();
const { token } = theme.useToken();
const { navigationStyle, layoutStyle } = useUnifiedTheme();
const { navigationStyle, layoutStyle, menuLayout, menuStyle, basicConfig } =
useUnifiedTheme();
const {
// showHoverMenu,
isSecondMenuCollapsed,
setOpenMessage,
handleCloseMobileMenu,
unreadCount,
} = useModel('layout');
const { firstLevelMenus, otherMenus } = useModel('menuModel');
const { firstLevelMenus, otherMenus, getSecondLevelMenus } =
useModel('menuModel');
const { refreshUserInfo } = useModel('userInfo');
const { refreshUserInfo, userInfo } = useModel('userInfo');
const { currentSpaceInfo, spaceList } = useModel('spaceModel');
// 工作空间下的最近编辑和开发收藏
const { runEdit } = useModel('devCollectAgent');
@@ -88,10 +109,15 @@ const DynamicMenusLayout: React.FC<DynamicMenusLayoutProps> = ({
// 是否点击了新对话菜单特殊处理用于显示title时使用
const [isClickNewConversation, setIsClickNewConversation] =
useState<boolean>(false);
const [verticalExpandedMenus, setVerticalExpandedMenus] = useState<string[]>(
[],
);
// 创建智能体会话
const { handleCreateConversation } = useConversation();
const { tenantConfigInfo } = useModel('tenantConfigInfo');
const effectiveMenuLayout = isMobile ? ThemeMenuLayoutType.DUAL : menuLayout;
const isEnableSubscription = tenantConfigInfo?.enableSubscription !== 0;
// 是否点击菜单
const isClickMenu = useRef<boolean>(false);
@@ -401,7 +427,7 @@ const DynamicMenusLayout: React.FC<DynamicMenusLayoutProps> = ({
runEdit({
size: 5,
});
}, []);
}, [runEdit]);
/**
* 递归查找第一个有 path 的子菜单
@@ -428,6 +454,246 @@ const DynamicMenusLayout: React.FC<DynamicMenusLayoutProps> = ({
[],
);
const getVerticalMenuChildren = useCallback(
(menu: MenuItemDto): MenuItemDto[] => {
const modelChildren = menu.code ? getSecondLevelMenus(menu.code) : [];
const children = modelChildren.length
? modelChildren
: menu.children || [];
if (menu.code === 'workspace' && !isEnableSubscription) {
return children.filter((child) => child.code !== 'resource_pricing');
}
return children;
},
[getSecondLevelMenus, isEnableSubscription],
);
const isVerticalMenuVisible = useCallback(
(menu: MenuItemDto): boolean => {
if (
(currentSpaceInfo?.type === SpaceTypeEnum.Personal ||
currentSpaceInfo?.currentUserRole === RoleEnum.User) &&
menu.code === 'member_setting'
) {
return false;
}
if (
currentSpaceInfo?.currentUserRole === RoleEnum.User &&
currentSpaceInfo?.allowDevelop === AllowDevelopEnum.Not_Allow &&
(menu.code === 'agent_dev' || menu.code === 'component_lib_dev')
) {
return false;
}
return true;
},
[currentSpaceInfo],
);
const extractSpaceIdFromPath = useCallback((path: string): string | null => {
if (!path) return null;
const match = path.split('?')[0].match(/\/space\/([^/]+)/);
return match ? match[1] : null;
}, []);
const resolveVerticalMenuPath = useCallback(
(path: string, parentCode?: string): string => {
if (!path || !path.includes(':')) {
return path;
}
if (parentCode === 'workspace') {
let spaceId: string | null = null;
try {
const pathUrl = localStorage.getItem(PATH_URL);
if (pathUrl) {
const pathUrlObj = JSON.parse(pathUrl) as Record<string, string>;
const workspacePath = pathUrlObj.workspace;
if (workspacePath) {
spaceId = extractSpaceIdFromPath(workspacePath);
}
}
} catch {}
if (!spaceId && params?.spaceId) {
spaceId = String(params.spaceId);
}
if (!spaceId && spaceList?.length) {
spaceId = String(spaceList[0].id);
}
if (spaceId) {
return path.replace(/:spaceId/g, spaceId);
}
}
return path.replace(/:(\w+)/g, (_match, key) => {
const value = params?.[key];
return value ? String(value) : `:${key}`;
});
},
[extractSpaceIdFromPath, params, spaceList],
);
const toggleVerticalMenu = useCallback(
(code?: string, ancestorCodes: string[] = []) => {
if (!code) return;
setVerticalExpandedMenus((prev) => {
if (prev.includes(code)) {
return prev.filter((item) => item !== code);
}
if (basicConfig.uniqueOpened) {
return [...ancestorCodes, code].filter(Boolean);
}
return [...prev, code];
});
},
[basicConfig.uniqueOpened],
);
const getTopMenuCodeByChildCode = useCallback(
(menuCode?: string): string | undefined => {
if (!menuCode) return undefined;
return findFirstLevelCodeByMenuCode(menuCode) || menuCode;
},
[findFirstLevelCodeByMenuCode],
);
const handleVerticalMenuClick = useCallback(
(menu: MenuItemDto, parentCode?: string, ancestorCodes: string[] = []) => {
const children = getVerticalMenuChildren(menu).filter(
isVerticalMenuVisible,
);
const hasChildren = children.length > 0;
const topCode = parentCode || getTopMenuCodeByChildCode(menu.code);
if (topCode) {
setActiveTab(topCode);
}
if (menu.code === 'workspace' || topCode === 'workspace') {
handleRefreshEditAndCollect();
}
if (hasChildren) {
toggleVerticalMenu(menu.code, ancestorCodes);
return;
}
handleCloseMobileMenu();
setIsClickNewConversation(false);
if (menu.path?.includes('http')) {
handleOpenUrl(menu, topCode);
return;
}
if (!menu.path) {
if (menu.code === 'new_conversation') {
handlerClick();
setIsClickNewConversation(true);
handleNewConversation();
}
return;
}
const resolvedPath = resolveVerticalMenuPath(menu.path, topCode);
if (!resolvedPath || resolvedPath.includes(':')) {
if (topCode === 'workspace') {
history.push('/space', { _t: Date.now(), menuCode: topCode });
}
return;
}
if (topCode) {
updatePathUrlToLocalStorage(topCode, resolvedPath);
}
history.push(resolvedPath, { _t: Date.now(), menuCode: menu.code });
},
[
getVerticalMenuChildren,
isVerticalMenuVisible,
getTopMenuCodeByChildCode,
handleRefreshEditAndCollect,
toggleVerticalMenu,
handleCloseMobileMenu,
resolveVerticalMenuPath,
handlerClick,
handleNewConversation,
],
);
const findVerticalMenuCodePath = useCallback(
(menus: MenuItemDto[], targetCode?: string): string[] => {
if (!targetCode) return [];
for (const menu of menus) {
const menuCode = menu.code || '';
if (menuCode === targetCode) {
return [menuCode];
}
const children = getVerticalMenuChildren(menu).filter(
isVerticalMenuVisible,
);
if (children.length) {
const childPath = findVerticalMenuCodePath(children, targetCode);
if (childPath.length) {
return [menuCode, ...childPath].filter(Boolean);
}
}
}
return [];
},
[getVerticalMenuChildren, isVerticalMenuVisible],
);
useEffect(() => {
if (effectiveMenuLayout !== ThemeMenuLayoutType.VERTICAL) return;
const routeMenuCode = params?.menuCode || location.state?.menuCode;
const nextExpandedCodes = new Set<string>();
if (activeTab) {
nextExpandedCodes.add(activeTab);
}
const activePath = findVerticalMenuCodePath(firstLevelMenus, routeMenuCode);
activePath.slice(0, -1).forEach((code) => nextExpandedCodes.add(code));
if (!nextExpandedCodes.size) return;
setVerticalExpandedMenus((prev) => {
const merged = [...prev];
let changed = false;
nextExpandedCodes.forEach((code) => {
if (!merged.includes(code)) {
merged.push(code);
changed = true;
}
});
return changed ? merged : prev;
});
}, [
activeTab,
effectiveMenuLayout,
findVerticalMenuCodePath,
firstLevelMenus,
location.state?.menuCode,
params?.menuCode,
]);
/**
* 点击一级菜单
*/
@@ -598,10 +864,22 @@ const DynamicMenusLayout: React.FC<DynamicMenusLayoutProps> = ({
if (isMobile) {
return NAVIGATION_LAYOUT_SIZES.FIRST_MENU_WIDTH.STYLE1;
}
if (effectiveMenuLayout === ThemeMenuLayoutType.VERTICAL) {
return basicConfig.menuOpenWidth;
}
return navigationStyle === 'style2'
? NAVIGATION_LAYOUT_SIZES.FIRST_MENU_WIDTH.STYLE2
: NAVIGATION_LAYOUT_SIZES.FIRST_MENU_WIDTH.STYLE1;
}, [navigationStyle, isMobile]);
}, [
basicConfig.menuOpenWidth,
effectiveMenuLayout,
navigationStyle,
isMobile,
]);
const handleReloadPage = useCallback(() => {
window.location.reload();
}, []);
/**
* 一级导航背景
@@ -610,7 +888,7 @@ const DynamicMenusLayout: React.FC<DynamicMenusLayoutProps> = ({
if (isMobile) {
return `var(--xagi-background-image) ${token.colorBgContainer}`;
}
return 'transparent';
return 'var(--xagi-layout-bg-secondary, #f2f5fb)';
}, [isMobile, token.colorBgContainer]);
/**
@@ -621,10 +899,64 @@ const DynamicMenusLayout: React.FC<DynamicMenusLayoutProps> = ({
return token.colorBgContainer;
}
return navigationStyle === 'style2'
? 'var(--xagi-layout-bg-container, rgba(255, 255, 255, 0.95))'
: 'transparent';
? 'var(--xagi-layout-bg-container, #ffffff)'
: 'var(--xagi-layout-bg-secondary, #f2f5fb)';
}, [isMobile, navigationStyle, token.colorBgContainer]);
const isMixedSideNavigation =
region === 'side' && effectiveMenuLayout === ThemeMenuLayoutType.MIXED;
const secondMenuVisualStyle = useMemo(() => {
const style = {
backgroundColor: secondaryBackgroundColor,
} as React.CSSProperties & Record<string, string>;
if (!isMixedSideNavigation) {
return style;
}
if (layoutStyle === ThemeLayoutColorStyle.DARK) {
style.backgroundColor =
layoutStyle === ThemeLayoutColorStyle.DARK
? 'var(--xagi-layout-bg-container, #111620)'
: '#111827';
style['--xagi-layout-second-menu-text-color'] =
'rgba(255, 255, 255, 0.95)';
style['--xagi-layout-second-menu-text-color-secondary'] =
'rgba(255, 255, 255, 0.78)';
style['--xagi-layout-text-tertiary'] = 'rgba(255, 255, 255, 0.56)';
style['--xagi-menu-item-active-bg'] = 'rgba(93, 135, 255, 0.2)';
style['--xagi-menu-item-hover-bg'] = 'rgba(255, 255, 255, 0.08)';
style['--xagi-menu-item-active-shadow'] = 'none';
style['--xagi-menu-item-hover-shadow'] = 'none';
return style;
}
if (menuStyle === ThemeMenuStyleType.LIGHT) {
style.backgroundColor = '#ffffff';
style['--xagi-layout-second-menu-text-color'] = '#1f2337';
style['--xagi-layout-second-menu-text-color-secondary'] = '#5d6378';
style['--xagi-layout-text-tertiary'] = '#8790a5';
style['--xagi-menu-item-active-bg'] = 'rgba(93, 135, 255, 0.1)';
style['--xagi-menu-item-hover-bg'] = 'rgba(93, 135, 255, 0.07)';
style['--xagi-menu-item-active-shadow'] = 'none';
style['--xagi-menu-item-hover-shadow'] = 'none';
return style;
}
style.backgroundColor = '#f2f5fb';
style['--xagi-layout-second-menu-text-color'] = '#1f2337';
style['--xagi-layout-second-menu-text-color-secondary'] = '#626a80';
style['--xagi-layout-text-tertiary'] = '#8a92a6';
style['--xagi-menu-item-active-bg'] = '#ffffff';
style['--xagi-menu-item-hover-bg'] = 'rgba(255, 255, 255, 0.72)';
style['--xagi-menu-item-active-shadow'] =
'0 10px 20px rgba(32, 45, 84, 0.1)';
style['--xagi-menu-item-hover-shadow'] =
'0 8px 16px rgba(32, 45, 84, 0.06)';
return style;
}, [isMixedSideNavigation, layoutStyle, menuStyle, secondaryBackgroundColor]);
/**
* 导航容器样式类名
*/
@@ -634,9 +966,10 @@ const DynamicMenusLayout: React.FC<DynamicMenusLayoutProps> = ({
'flex',
`xagi-layout-${layoutStyle}`,
`xagi-nav-${navigationStyle}`,
styles[`menu-layout-${effectiveMenuLayout}`],
isMobile && styles['mobile-container'],
);
}, [layoutStyle, navigationStyle, isMobile]);
}, [layoutStyle, navigationStyle, effectiveMenuLayout, isMobile]);
/**
* 渲染二级菜单
@@ -675,92 +1008,455 @@ const DynamicMenusLayout: React.FC<DynamicMenusLayoutProps> = ({
return <DynamicSecondMenu parentCode={activeTab} />;
}, [activeTab, overrideContainerStyle]);
return (
<div className={navigationClassName}>
{/* 一级导航菜单栏 */}
<div
className={cx(
styles['first-menus'],
'flex',
'flex-col',
'items-center',
)}
style={{
width: firstMenuWidth,
background: firstMenuBackground,
}}
>
<Header />
{/* 动态一级菜单 */}
<DynamicTabs
isStyleOne={
navigationStyle === ThemeNavigationStyleType.STYLE1 || isMobile
}
menus={firstLevelMenus}
activeTab={activeTab}
onClick={handleTabClick}
const renderMenuIcon = (menu: MenuItemDto) => {
if (
menu.icon &&
(menu.icon.includes('.png') ||
menu.icon.includes('.jpg') ||
menu.icon.includes('.jpeg'))
) {
return <img src={menu.icon} alt="" className={cx(styles.menuIconImg)} />;
}
return <SvgIcon name={menu.icon || 'icons-nav-task-time'} />;
};
const renderBrandLogo = (compact = false) => (
<>
{tenantConfigInfo?.siteLogo ? (
<img
src={tenantConfigInfo.siteLogo}
alt=""
className={cx(styles.brandLogo)}
/>
{/* 用户操作区域 */}
<UserOperateArea onClick={handleUserClick} menus={otherMenus} />
{/* 用户头像 */}
<User />
) : null}
{!compact ? (
<Typography.Text className={cx(styles.brandText)}>
{tenantConfigInfo?.siteName || 'Art Design Pro'}
</Typography.Text>
) : null}
</>
);
const renderTopDropdownItem = (
menu: MenuItemDto,
level = 0,
parentCode?: string,
): React.ReactNode => {
if (!isVerticalMenuVisible(menu)) {
return null;
}
const children = getVerticalMenuChildren(menu).filter(
isVerticalMenuVisible,
);
const hasChildren = children.length > 0;
const isActive = isVerticalMenuActive(menu);
return (
<div key={menu.code || menu.name} className={cx(styles.topDropdownGroup)}>
<button
type="button"
className={cx(
styles.topDropdownItem,
styles[`topDropdownLevel${Math.min(level, 2)}`],
{
[styles.active]: isActive,
[styles.open]: hasChildren,
},
)}
onClick={() => {
if (!hasChildren) {
handleVerticalMenuClick(menu, parentCode);
}
}}
title={menu.name}
>
{level === 0 ? (
<span className={cx(styles.topDropdownIcon)}>
{renderMenuIcon(menu)}
</span>
) : (
<span className={cx(styles.topDropdownBullet)} />
)}
<span className={cx(styles.topDropdownText)}>{menu.name}</span>
{hasChildren ? (
<SvgIcon
name="icons-common-caret_down"
className={cx(styles.topDropdownArrow)}
/>
) : null}
</button>
{hasChildren ? (
<div className={cx(styles.topDropdownChildren)}>
{children.map((child) =>
renderTopDropdownItem(child, level + 1, parentCode),
)}
</div>
) : null}
</div>
);
};
const renderTopMenuItem = (menu: MenuItemDto) => {
const children = getVerticalMenuChildren(menu).filter(
isVerticalMenuVisible,
);
const hasChildren = children.length > 0;
const content = (
<button
key={menu.code}
type="button"
className={cx(styles.topMenuItem, {
[styles.active]: activeTab === menu.code,
})}
onClick={() => {
if (hasChildren) {
setActiveTab(menu.code || '');
if (menu.code === 'workspace') {
handleRefreshEditAndCollect();
}
return;
}
handleTabClick(menu);
}}
title={menu.name}
>
<span className={cx(styles.topMenuIcon)}>{renderMenuIcon(menu)}</span>
<span className={cx(styles.topMenuText)}>{menu.name}</span>
{hasChildren ? (
<SvgIcon
name="icons-common-caret_down"
className={cx(styles.topMenuArrow)}
/>
) : null}
</button>
);
if (!hasChildren) {
return content;
}
return (
<Popover
key={menu.code}
trigger={['hover', 'click']}
placement="bottomLeft"
arrow={false}
styles={{ body: { padding: 0 } }}
content={
<div className={cx(styles.topDropdownMenu)}>
{children.map((child) =>
renderTopDropdownItem(child, 0, menu.code || ''),
)}
</div>
}
>
{content}
</Popover>
);
};
const renderTopNavigation = () => (
<div className={cx(styles.topMenuBar, 'noselect')}>
<div className={cx(styles.topLogo)}>{renderBrandLogo()}</div>
<div className={cx(styles.topMenuScroll)}>
{firstLevelMenus.map((menu) => renderTopMenuItem(menu))}
</div>
<div className={cx(styles.topActions)}>
{basicConfig.showRefreshButton && (
<button
type="button"
className={cx(styles.topActionButton)}
onClick={handleReloadPage}
title="重载页面"
>
<ReloadOutlined />
</button>
)}
{basicConfig.showFastEnter &&
otherMenus.map((menu) => (
<button
key={menu.code}
type="button"
className={cx(styles.topActionButton, {
[styles.active]: activeTab === menu.code,
})}
onClick={() => handleUserClick(menu)}
title={menu.name}
>
{renderMenuIcon(menu)}
</button>
))}
<User placement="bottomRight" />
</div>
</div>
);
const renderSecondMenuPanel = () => (
<div
className={cx(styles['nav-menus'], 'noselect')}
style={{
width: isSecondMenuCollapsed
? 0
: NAVIGATION_LAYOUT_SIZES.SECOND_MENU_WIDTH,
paddingLeft: isSecondMenuCollapsed ? 0 : token.padding,
opacity: isSecondMenuCollapsed ? 0 : 1,
...secondMenuVisualStyle,
}}
>
<div className={cx(styles['nav-menus-scroll'])}>
<HoverScrollbar
className={cx('w-full', 'h-full')}
bodyWidth={
NAVIGATION_LAYOUT_SIZES.SECOND_MENU_WIDTH - token.padding * 2
}
style={{
padding: `${token.paddingSM}px 0`,
}}
>
<div
className={cx('flex', 'flex-col', 'h-full')}
style={{ minHeight: 0 }}
>
<ConditionRender condition={isShowTitle && currentTitle}>
<div style={{ padding: '0 12px 12px' }}>
<Typography.Title
level={5}
style={{ marginBottom: 0 }}
className={cx(styles['menu-title'])}
>
{currentTitle}
</Typography.Title>
</div>
</ConditionRender>
{renderSecondMenu}
</div>
</HoverScrollbar>
</div>
{/* 二级导航菜单栏 */}
<div
className={cx(styles['nav-menus'], 'noselect')}
style={{
width: isSecondMenuCollapsed
? 0
: NAVIGATION_LAYOUT_SIZES.SECOND_MENU_WIDTH,
paddingLeft: isSecondMenuCollapsed ? 0 : token.padding,
opacity: isSecondMenuCollapsed ? 0 : 1,
backgroundColor: secondaryBackgroundColor,
}}
>
<div className={cx(styles['nav-menus-scroll'])}>
<HoverScrollbar
className={cx('w-full', 'h-full')}
bodyWidth={
NAVIGATION_LAYOUT_SIZES.SECOND_MENU_WIDTH - token.padding * 2
}
<div className={cx(styles['integral-footer'])}>
<CreditsBalance />
</div>
</div>
);
const renderIconRail = () => (
<div
className={cx(styles['first-menus'], 'flex', 'flex-col', 'items-center')}
style={{
width: firstMenuWidth,
background: firstMenuBackground,
}}
>
<Header />
<DynamicTabs
isStyleOne={
navigationStyle === ThemeNavigationStyleType.STYLE1 || isMobile
}
menus={firstLevelMenus}
activeTab={activeTab}
onClick={handleTabClick}
/>
{basicConfig.showFastEnter && (
<UserOperateArea onClick={handleUserClick} menus={otherMenus} />
)}
{basicConfig.showRefreshButton && (
<Button
type="text"
className={cx(styles.railReloadButton)}
icon={<ReloadOutlined />}
onClick={handleReloadPage}
title="重载页面"
/>
)}
<User />
</div>
);
const isVerticalMenuActive = (menu: MenuItemDto) => {
const routeMenuCode = params?.menuCode || location.state?.menuCode;
if (routeMenuCode && menu.code === routeMenuCode) {
return true;
}
if (!menu.path) {
return menu.code === activeTab;
}
const resolvedPath = resolveVerticalMenuPath(
menu.path,
getTopMenuCodeByChildCode(menu.code),
);
if (!resolvedPath || resolvedPath.includes(':')) {
return false;
}
return isPathMatch(resolvedPath, location.pathname);
};
const renderVerticalMenuItem = (
menu: MenuItemDto,
level = 0,
parentCode?: string,
ancestorCodes: string[] = [],
): React.ReactNode => {
if (!isVerticalMenuVisible(menu)) {
return null;
}
const menuCode = menu.code || `${menu.name}-${level}`;
const topCode = parentCode || menu.code;
const children = getVerticalMenuChildren(menu).filter(
isVerticalMenuVisible,
);
const hasChildren = children.length > 0;
const isExpanded = verticalExpandedMenus.includes(menuCode);
const nextAncestorCodes = [...ancestorCodes, menuCode].filter(Boolean);
const isTopLevel = level === 0;
const isActive = isTopLevel
? activeTab === menu.code
: isVerticalMenuActive(menu);
return (
<div key={menuCode} className={cx(styles.verticalMenuGroup)}>
<button
type="button"
className={cx(
styles.verticalMenuItem,
styles[`verticalMenuLevel${Math.min(level, 3)}`],
{
[styles.active]: isActive,
[styles.open]: isExpanded,
},
)}
onClick={() => handleVerticalMenuClick(menu, topCode, ancestorCodes)}
title={menu.name}
>
{isTopLevel ? (
<span className={cx(styles.verticalMenuIcon)}>
{renderMenuIcon(menu)}
</span>
) : (
<span className={cx(styles.verticalMenuBullet)} />
)}
<span className={cx(styles.verticalMenuText)}>{menu.name}</span>
{hasChildren ? (
<SvgIcon
name="icons-common-caret_down"
className={cx(styles.verticalMenuArrow)}
/>
) : null}
</button>
{hasChildren ? (
<div
className={cx(styles.verticalSubMenu)}
style={{
padding: `${token.paddingSM}px 0`,
maxHeight: isExpanded ? 720 : 0,
opacity: isExpanded ? 1 : 0,
}}
>
<div
className={cx('flex', 'flex-col', 'h-full')}
style={{
minHeight: 0,
}}
>
{/* 标题 */}
<ConditionRender condition={isShowTitle && currentTitle}>
<div style={{ padding: '0 12px 12px' }}>
<Typography.Title
level={5}
style={{ marginBottom: 0 }}
className={cx(styles['menu-title'])}
>
{currentTitle}
</Typography.Title>
</div>
</ConditionRender>
{/* 二级/三级菜单 */}
{renderSecondMenu}
</div>
</HoverScrollbar>
</div>
{/* 积分相关入口:放到二级导航栏底部固定展示 */}
<div className={cx(styles['integral-footer'])}>
<CreditsBalance />
</div>
{children.map((child) =>
renderVerticalMenuItem(
child,
level + 1,
topCode,
nextAncestorCodes,
),
)}
</div>
) : null}
</div>
);
};
{/* 收起/展开按钮 */}
<CollapseButton />
const renderVerticalActionButton = (menu: MenuItemDto) => {
const content = (
<button
key={menu.code}
type="button"
className={cx(styles.verticalActionButton, {
[styles.active]: activeTab === menu.code,
})}
onClick={() => handleUserClick(menu)}
title={menu.name}
>
{renderMenuIcon(menu)}
</button>
);
if (menu.code === 'notification' && unreadCount > 0) {
return (
<Badge key={menu.code} count={unreadCount} size="small">
{content}
</Badge>
);
}
return content;
};
const renderVerticalMenu = () => (
<aside
className={cx(styles.verticalMenu, 'noselect')}
style={{ width: firstMenuWidth }}
>
<div className={cx(styles.verticalLogo)}>{renderBrandLogo()}</div>
<div className={cx(styles.verticalMenuList)}>
{firstLevelMenus.map((menu) => renderVerticalMenuItem(menu))}
</div>
<div className={cx(styles.verticalFooter)}>
<div className={cx(styles.verticalActions)}>
{basicConfig.showRefreshButton && (
<button
type="button"
className={cx(styles.verticalActionButton)}
onClick={handleReloadPage}
title="重载页面"
>
<ReloadOutlined />
</button>
)}
{basicConfig.showFastEnter &&
otherMenus.map((menu) => renderVerticalActionButton(menu))}
</div>
<User placement="topRight">
<div className={cx(styles.verticalUserAvatar)}>
<UserAvatar avatar={userInfo?.avatar} onClick={() => undefined} />
</div>
</User>
</div>
</aside>
);
if (region === 'top') {
return renderTopNavigation();
}
if (region === 'side' && effectiveMenuLayout === ThemeMenuLayoutType.MIXED) {
return (
<div className={cx(navigationClassName, styles.mixedSide)}>
{renderSecondMenuPanel()}
</div>
);
}
if (effectiveMenuLayout === ThemeMenuLayoutType.HORIZONTAL) {
return renderTopNavigation();
}
if (effectiveMenuLayout === ThemeMenuLayoutType.VERTICAL) {
return <div className={navigationClassName}>{renderVerticalMenu()}</div>;
}
return (
<div className={navigationClassName}>
{renderIconRail()}
{renderSecondMenuPanel()}
{basicConfig.showMenuButton && <CollapseButton />}
</div>
);
};

View File

@@ -44,7 +44,7 @@
color: @WHITE;
background: @BLANK26;
padding: 10px;
border-radius: 10px;
border-radius: 8px;
overflow-x: auto;
margin: 0 0 4px;

View File

@@ -90,7 +90,7 @@
.previewRail {
width: 28px;
background: #006569;
background: #5d87ff;
}
.previewBody {
@@ -141,7 +141,7 @@
.portal {
.openAppThemePreview {
border-top: 4px solid #006569;
border-top: 4px solid #5d87ff;
background: #f4fbf8;
}

View File

@@ -37,7 +37,7 @@
line-height: 38px;
padding: 0 10px;
margin-bottom: 10px;
border-radius: 10px;
border-radius: 8px;
transition: background-color 0.3s;
&:hover:not(.checked) {
@@ -67,23 +67,23 @@
.left {
background: #f4fbf8;
border-right: 1px solid rgba(0, 101, 105, 0.12);
border-right: 1px solid rgba(93, 135, 255, 0.14);
h3 {
color: #006569;
color: #5d87ff;
}
ul .item {
color: #395254;
&:hover:not(.checked) {
background-color: rgba(0, 101, 105, 0.08);
background-color: rgba(93, 135, 255, 0.1);
}
&.checked {
background-color: #ffffff;
color: #006569;
box-shadow: inset 3px 0 0 #006569;
color: #5d87ff;
box-shadow: inset 3px 0 0 #5d87ff;
}
}
}
@@ -139,10 +139,10 @@
.left {
background: #eef8f5;
border-right: 1px solid #cde5df;
border-right: 1px solid rgba(93, 135, 255, 0.18);
h3 {
color: #00575a;
color: #5d87ff;
}
ul .item {
@@ -150,11 +150,11 @@
border-radius: 6px;
&:hover:not(.checked) {
background-color: rgba(0, 101, 105, 0.08);
background-color: rgba(93, 135, 255, 0.1);
}
&.checked {
background-color: #006569;
background-color: #5d87ff;
color: #ffffff;
}
}
@@ -163,7 +163,7 @@
.right {
background: linear-gradient(
90deg,
rgba(0, 101, 105, 0.05) 0 1px,
rgba(93, 135, 255, 0.05) 0 1px,
transparent 1px
),
#ffffff;

View File

@@ -4,6 +4,7 @@ import {
OPEN_APP_THEME_CHANGED_EVENT,
OpenAppTheme,
} from '@/constants/openAppTheme.constants';
import { useUnifiedTheme } from '@/hooks/useUnifiedTheme';
import { dict } from '@/services/i18nRuntime';
import { getTenantThemeConfig } from '@/services/tenant';
import { SettingActionEnum } from '@/types/enums/menus';
@@ -26,6 +27,7 @@ const cx = classNames.bind(styles);
const Setting: React.FC = () => {
const { openSetting, setOpenSetting, isMobile } = useModel('layout');
const { basicConfig } = useUnifiedTheme();
const { tenantConfigInfo } = useModel('tenantConfigInfo');
const isEnableSubscription = tenantConfigInfo?.enableSubscription !== 0;
const [action, setAction] = useState<SettingActionEnum>(
@@ -158,6 +160,9 @@ const Setting: React.FC = () => {
if (item.type === SettingActionEnum.Developer_Profile) {
return isEnableSubscription;
}
if (item.type === SettingActionEnum.Language_Switch) {
return basicConfig.showLanguage;
}
return true;
}).map((item) => (
<li

View File

@@ -1,7 +1,9 @@
@import '@/styles/token';
.container {
// background-color: @colorBgLayout;
display: flex;
flex-direction: column;
background-color: var(--xagi-layout-bg-secondary, #f2f5fb);
position: relative;
// 背景图片样式
@@ -13,6 +15,7 @@
width: 100%;
height: 100%;
z-index: 0;
background-color: var(--xagi-layout-bg-secondary, #f2f5fb);
background-image: @backgroundImage;
background-size: cover;
background-position: center;
@@ -22,21 +25,49 @@
}
}
.layout-body {
position: relative;
z-index: 1;
display: flex;
flex: 1;
min-height: 0;
}
.menu-layout-vertical,
.menu-layout-dual {
flex-direction: row;
.layout-body {
flex: 1;
width: 100%;
}
}
.menu-layout-horizontal,
.menu-layout-mixed {
flex-direction: column;
.layout-body {
flex: 1;
width: 100%;
}
}
// 移动端菜单容器隐藏时的样式
.mobile-menu-container-hidden {
width: 0 !important;
}
.page-container {
background: @pageContainerBg;
background: var(--xagi-layout-bg-secondary, #f7f8fb);
border-left: 0 solid @pageContainerBorderColor;
margin: @marginXs;
margin: @pageContainerMargin;
margin-left: 0;
border-radius: @pageContainerBorderRadius;
box-shadow: 0 8px 24px -4px rgba(0, 0, 0, 8%);
box-shadow: none;
z-index: 1;
// 确保内容区域有半透明背景,提高可读性
transition: all 0.3s ease;
transition: margin 0.3s ease, border-radius 0.3s ease, background 0.3s ease;
position: relative;
// 风格2的适配
@@ -47,7 +78,7 @@
// background: var(--xagi-color-bg-container);
&.xagi-layout-collapsed {
border-left-width: 0;
border-radius: 12px 0 0 12px;
border-radius: 0;
}
&.xagi-layout-expanded {
@@ -56,3 +87,201 @@
}
}
}
.workTabsBar {
flex-shrink: 0;
padding: 10px 16px 0;
background: var(--xagi-layout-bg-secondary, #f2f5fb);
}
.workTabsScroller {
display: flex;
align-items: center;
gap: 8px;
min-height: 36px;
overflow: auto hidden;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.workTab {
display: inline-flex;
align-items: center;
flex-shrink: 0;
max-width: 168px;
height: 34px;
padding: 0 10px;
border: @lineWidth solid var(--xagi-layout-border-secondary);
border-radius: var(--xagi-custom-radius, 0.75rem);
background: var(--xagi-layout-bg-container, #fff);
color: var(--xagi-layout-text-secondary, #626a80);
cursor: pointer;
font-size: 14px;
line-height: 20px;
transition: color 0.2s ease, background 0.2s ease, border-color 0.2s ease,
box-shadow 0.2s ease;
&:hover,
&.active {
color: var(--xagi-color-primary, #5d87ff);
border-color: rgba(93, 135, 255, 0.28);
background: var(--xagi-layout-bg-container, #fff);
}
&.active {
box-shadow: 0 8px 18px rgba(32, 45, 84, 0.08);
}
}
.work-tab-tab-card {
border-radius: var(--xagi-custom-radius, 0.75rem)
var(--xagi-custom-radius, 0.75rem) 4px 4px;
&.active {
border-bottom-color: var(--xagi-layout-bg-container, #fff);
}
}
.work-tab-tab-google {
height: 32px;
border-radius: 999px;
background: rgba(93, 135, 255, 0.08);
border-color: transparent;
&.active {
background: var(--xagi-color-primary, #5d87ff);
color: #fff;
box-shadow: 0 8px 18px rgba(93, 135, 255, 0.22);
}
}
.workTabTitle {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.workTabClose {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
flex-shrink: 0;
margin-left: 6px;
border-radius: 50%;
color: currentColor;
font-size: 11px;
opacity: 0.62;
&:hover {
background: rgba(91, 105, 135, 0.12);
opacity: 1;
}
}
.breadcrumbBar {
flex-shrink: 0;
padding: 8px 18px 0;
color: var(--xagi-layout-text-tertiary, #8790a5);
:global(.ant-breadcrumb) {
font-size: 13px;
}
}
.pageTransition {
min-height: 100%;
}
.pageTransition-none {
animation: none;
}
.pageTransition-fade {
animation: xagiPageFade 0.22s ease both;
}
.pageTransition-slide-left {
animation: xagiPageSlideLeft 0.24s ease both;
}
.pageTransition-slide-bottom {
animation: xagiPageSlideBottom 0.24s ease both;
}
.pageTransition-slide-top {
animation: xagiPageSlideTop 0.24s ease both;
}
@keyframes xagiPageFade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes xagiPageSlideLeft {
from {
opacity: 0;
transform: translateX(16px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes xagiPageSlideBottom {
from {
opacity: 0;
transform: translateY(-14px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes xagiPageSlideTop {
from {
opacity: 0;
transform: translateY(14px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
:global(html[data-theme='dark']) {
.container {
background-color: var(--xagi-layout-bg-primary, #080a0f);
&::before {
background-color: var(--xagi-layout-bg-primary, #080a0f);
}
}
.page-container {
background: var(--xagi-layout-bg-primary, #080a0f);
}
.workTabsBar {
background: var(--xagi-layout-bg-primary, #080a0f);
}
.workTab {
background: var(--xagi-layout-bg-container, #111620);
}
}

View File

@@ -5,11 +5,20 @@ import {
} from '@/constants/layout.constants';
import useCategory from '@/hooks/useCategory';
import { useUnifiedTheme } from '@/hooks/useUnifiedTheme';
import { theme } from 'antd';
import type { MenuItemDto } from '@/types/interfaces/menu';
import { CloseOutlined } from '@ant-design/icons';
import { Breadcrumb, theme } from 'antd';
import classNames from 'classnames';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { Outlet, useModel } from 'umi';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { history, Outlet, useLocation, useModel } from 'umi';
import DynamicMenusLayout from './DynamicMenusLayout';
import { normalizeMenuPathname } from './DynamicMenusLayout/utils';
import HoverMenu from './HoverMenu';
import styles from './index.less';
import Message from './Message';
@@ -19,6 +28,75 @@ import Setting from './Setting';
// 绑定 classNames便于动态样式组合
const cx = classNames.bind(styles);
interface WorkTabItem {
key: string;
path: string;
title: string;
}
interface LayoutBreadcrumbItem {
key: string;
title: string;
}
const getMenuPathWithoutQuery = (path?: string) => (path || '').split('?')[0];
const isMenuPathMatch = (menuPath: string | undefined, pathname: string) => {
const normalizedPathname = normalizeMenuPathname(pathname);
const cleanMenuPath = getMenuPathWithoutQuery(menuPath);
if (!cleanMenuPath) return false;
if (cleanMenuPath === '/homepage') {
return normalizedPathname === '/home' || normalizedPathname === '/';
}
if (cleanMenuPath === '/space') {
return normalizedPathname.startsWith('/space');
}
if (cleanMenuPath.includes(':')) {
const pattern = cleanMenuPath
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
.replace(/:(\w+)/g, '[^/]+');
return new RegExp(`^${pattern}(/.*)?$`).test(normalizedPathname);
}
return (
normalizedPathname === cleanMenuPath ||
normalizedPathname.startsWith(`${cleanMenuPath}/`)
);
};
const findMenuTrail = (
menus: MenuItemDto[] = [],
pathname: string,
parents: LayoutBreadcrumbItem[] = [],
): LayoutBreadcrumbItem[] => {
for (const menu of menus) {
const menuKey = menu.code || menu.path || menu.name || '';
const currentTrail = menu.name
? [...parents, { key: menuKey, title: menu.name }]
: parents;
if (isMenuPathMatch(menu.path, pathname)) {
return currentTrail;
}
if (menu.children?.length) {
const childTrail = findMenuTrail(menu.children, pathname, currentTrail);
if (childTrail.length) {
return childTrail;
}
}
}
return [];
};
const getFallbackRouteTitle = (pathname: string) => {
if (pathname === '/' || pathname === '/home') return '首页';
const lastSegment = pathname.split('/').filter(Boolean).pop();
return lastSegment || '当前页面';
};
// 开发环境判断
// const isDev = process.env.NODE_ENV === 'development';
// const isDevOrTest = isDev || process.env.CI;
@@ -28,6 +106,7 @@ const cx = classNames.bind(styles);
* 负责响应式菜单、历史会话、消息、设置弹窗的布局与展示
*/
const Layout: React.FC = () => {
const location = useLocation();
// 使用 useRef 避免重复获取 DOM 元素
const mobileMenuContainerRef = useRef<HTMLDivElement>(null);
// 全局主题与语言(已在 app.tsx 统一注入,这里仅保留使用场景需要时可读取)
@@ -45,7 +124,8 @@ const Layout: React.FC = () => {
const { runQueryCategory } = useCategory();
// 导航风格管理(使用统一主题系统)
const { navigationStyle, layoutStyle } = useUnifiedTheme();
const { navigationStyle, layoutStyle, menuLayout, basicConfig } =
useUnifiedTheme();
const { isSecondMenuCollapsed } = useModel('layout');
const { token } = theme.useToken();
@@ -61,7 +141,8 @@ const Layout: React.FC = () => {
} = useModel('layout');
const { asyncSpaceListFun } = useModel('spaceModel');
const { loadMenus } = useModel('menuModel');
const { firstLevelMenus, loadMenus } = useModel('menuModel');
const [workTabs, setWorkTabs] = useState<WorkTabItem[]>([]);
// 移除对 @@initialState 的依赖,统一由 useGlobalSettings 管理全局配置
@@ -214,6 +295,73 @@ const Layout: React.FC = () => {
() => (isMobile ? { paddingTop: MOBILE_MENU_TOP_PADDING } : {}),
[isMobile],
);
const currentPath = `${location.pathname}${location.search || ''}`;
const breadcrumbItems = useMemo(
() => findMenuTrail(firstLevelMenus, location.pathname),
[firstLevelMenus, location.pathname],
);
const currentPageTitle =
breadcrumbItems[breadcrumbItems.length - 1]?.title ||
getFallbackRouteTitle(location.pathname);
const transitionType = basicConfig.pageTransition || 'none';
useEffect(() => {
if (!basicConfig.showWorkTab || location.pathname.startsWith('/login')) {
return;
}
setWorkTabs((prev) => {
const currentTab = {
key: currentPath,
path: currentPath,
title: currentPageTitle,
};
const exists = prev.some((item) => item.key === currentTab.key);
const nextTabs = exists
? prev.map((item) =>
item.key === currentTab.key
? { ...item, title: currentTab.title }
: item,
)
: [...prev, currentTab];
return nextTabs.slice(-12);
});
}, [
basicConfig.showWorkTab,
currentPageTitle,
currentPath,
location.pathname,
]);
const handleWorkTabClick = (tab: WorkTabItem) => {
if (tab.key !== currentPath) {
history.push(tab.path);
}
};
const handleWorkTabClose = (
event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
tab: WorkTabItem,
) => {
event.stopPropagation();
const nextTabs = workTabs.filter((item) => item.key !== tab.key);
setWorkTabs(nextTabs);
if (tab.key === currentPath) {
const fallbackTab = nextTabs[nextTabs.length - 1];
history.push(fallbackTab?.path || '/home');
}
};
const workTabClassName = useCallback(
(tab: WorkTabItem) =>
cx(styles.workTab, styles[`work-tab-${basicConfig.tabStyle}`], {
[styles.active]: tab.key === currentPath,
}),
[basicConfig.tabStyle, currentPath],
);
/**
* 主容器样式类名(使用独立的布局风格类)
* 包含导航风格和布局风格类
@@ -224,12 +372,16 @@ const Layout: React.FC = () => {
'flex',
'h-full',
styles.container,
styles[`menu-layout-${menuLayout}`],
`xagi-layout-${layoutStyle}`, // 布局风格类独立于Ant Design
`xagi-nav-${navigationStyle}`, // 导航风格类
),
[layoutStyle, navigationStyle],
[layoutStyle, navigationStyle, menuLayout],
);
const hasTopMenu = !isMobile && ['horizontal', 'mixed'].includes(menuLayout);
const hasSideMenu = isMobile || menuLayout !== 'horizontal';
/**
* 页面容器样式类名(使用独立的布局风格类)
* 根据导航风格动态调整
@@ -250,48 +402,118 @@ const Layout: React.FC = () => {
return (
<div className={mainContainerClassName}>
{/* 侧边菜单栏及弹窗区域 */}
<div
ref={mobileMenuContainerRef}
className={containerClassName}
id="mobile-menu-container"
style={sidebarStyle}
>
{/* 菜单栏 */}
{hasTopMenu && (
<DynamicMenusLayout
overrideContainerStyle={menuOverrideStyle}
isMobile={isMobile}
region="top"
/>
)}
{/* 悬浮菜单 */}
<HoverMenu />
<div className={styles['layout-body']}>
{/* 侧边菜单栏及弹窗区域 */}
{hasSideMenu && (
<div
ref={mobileMenuContainerRef}
className={containerClassName}
id="mobile-menu-container"
style={sidebarStyle}
>
{/* 菜单栏 */}
<DynamicMenusLayout
overrideContainerStyle={menuOverrideStyle}
isMobile={isMobile}
region={hasTopMenu ? 'side' : 'auto'}
/>
{/* 消息弹窗 */}
<Message />
{/* 悬浮菜单 */}
{menuLayout !== 'vertical' && <HoverMenu />}
{/* 设置弹窗 */}
<Setting />
{/* 消息弹窗 */}
<Message />
{/* 移动端菜单按钮和遮罩层 */}
{isMobile && (
<MobileMenu
isOpen={fullMobileMenu}
onToggle={toggleFullMobileMenu}
menuWidth={getCurrentMenuWidth()}
/>
{/* 设置弹窗 */}
<Setting />
{/* 移动端菜单按钮和遮罩层 */}
{isMobile && (
<MobileMenu
isOpen={fullMobileMenu}
onToggle={toggleFullMobileMenu}
menuWidth={getCurrentMenuWidth()}
/>
)}
</div>
)}
</div>
{/* 主内容区 */}
<div
className={cx(pageContainerClassName, [
{ 'scroll-container': !isMobile },
// 移动端模式下宽度100%
{ 'w-full': isMobile },
])}
id="page-container-selector"
>
<Outlet />
{!hasSideMenu && (
<>
<Message />
<Setting />
</>
)}
{/* 主内容区 */}
<div
className={cx(pageContainerClassName, [
{ 'scroll-container': !isMobile },
// 移动端模式下宽度100%
{ 'w-full': isMobile },
])}
id="page-container-selector"
>
{basicConfig.showWorkTab && workTabs.length > 0 && (
<div className={cx(styles.workTabsBar)}>
<div className={cx(styles.workTabsScroller)}>
{workTabs.map((tab) => (
<button
key={tab.key}
type="button"
className={workTabClassName(tab)}
onClick={() => handleWorkTabClick(tab)}
title={tab.title}
>
<span className={cx(styles.workTabTitle)}>{tab.title}</span>
{workTabs.length > 1 && (
<span
role="button"
tabIndex={0}
className={cx(styles.workTabClose)}
onClick={(event) => handleWorkTabClose(event, tab)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
handleWorkTabClose(event, tab);
}
}}
aria-label={`关闭${tab.title}`}
>
<CloseOutlined />
</span>
)}
</button>
))}
</div>
</div>
)}
{basicConfig.showCrumbs && breadcrumbItems.length > 0 && (
<div className={cx(styles.breadcrumbBar)}>
<Breadcrumb
items={breadcrumbItems.map((item) => ({ title: item.title }))}
/>
</div>
)}
<div
key={location.pathname}
className={cx(
styles.pageTransition,
styles[`pageTransition-${transitionType}`],
)}
>
<Outlet />
</div>
</div>
</div>
</div>
);

View File

@@ -1,62 +1,737 @@
@import '@/styles/token';
.container {
width: 100%;
.headerActions {
display: flex;
align-items: center;
gap: 10px;
}
.content {
padding: @paddingLg;
.artSettingsPage {
min-height: 100%;
padding: 12px 24px 24px;
background: transparent;
}
.configContainer {
margin-bottom: @paddingXl;
.settingsPanel {
display: grid;
align-items: start;
width: 100%;
max-width: 1280px;
min-height: auto;
margin: 0 auto;
padding: 0;
overflow: visible;
background: transparent;
box-shadow: none;
backdrop-filter: none;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: 28px;
row-gap: 18px;
}
.settingSection {
margin-bottom: 0;
}
.sectionTitle {
display: flex;
align-items: center;
margin-bottom: 10px;
color: #2b3150;
font-size: 15px;
font-weight: 600;
line-height: 22px;
&::before,
&::after {
content: '';
flex: 1;
height: 1px;
background: #e5e8ef;
}
.configItem {
margin-bottom: 24px;
padding: 0;
span {
padding: 0 14px;
white-space: nowrap;
}
}
&:last-child {
margin-bottom: 0;
.settingBoxWrap {
display: grid;
width: 100%;
margin-bottom: 0;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px 12px;
}
.settingItem {
box-sizing: border-box;
width: 100%;
margin-right: 0;
padding: 0;
border: 0;
background: transparent;
text-align: center;
cursor: pointer;
&.disabled,
&:disabled {
cursor: not-allowed;
opacity: 0.42;
.previewBox {
border-color: #edf1f7 !important;
box-shadow: none;
}
}
}
// 响应式设计
// @media (max-width: @screenLg) {
// .content {
// .configItem {
// margin-bottom: @paddingMd;
// }
// }
// }
.previewBox {
position: relative;
box-sizing: border-box;
display: block;
height: 42px;
overflow: hidden;
border: 2px solid #edf1f7;
border-radius: 7px;
background: #f6f8fc;
box-shadow: 0 0 8px 0 rgb(0 0 0 / 10%);
transition: border-color 0.12s ease, box-shadow 0.12s ease;
// @media (max-width: @screenMd) {
// .title {
// padding: 16px;
// }
&.active {
border-color: @colorPrimary;
}
// .content {
// padding: 16px;
// padding-top: 70px; // 调整移动端标题高度
// padding-bottom: 70px; // 调整移动端底部高度
&.withTopMargin {
margin-top: 0;
}
// .configItem {
// margin-bottom: @paddingSm;
// padding: 0;
// }
// }
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
// .footer {
// display: flex;
// justify-content: flex-start;
// align-items: center;
// flex-direction: column;
// gap: @paddingMd;
// padding: 16px;
i,
b,
em {
position: absolute;
display: block;
border-radius: 4px;
}
}
// :global(.ant-btn) {
// width: 100%;
// max-width: 200px;
// }
// }
// }
.settingName {
display: block;
margin-top: 4px;
color: #111;
font-size: 13px;
line-height: 18px;
text-align: center;
}
.theme-light {
background: #f7f8fb;
i {
left: 12px;
top: 10px;
width: 18px;
height: 32px;
background: #fff;
}
b,
em {
left: 42px;
height: 5px;
background: #fff;
}
b {
top: 13px;
width: 72px;
}
em {
top: 25px;
width: 48px;
}
}
.theme-dark {
background: #30323b;
i {
left: 10px;
top: 8px;
width: 24px;
height: 36px;
background: #40424d;
}
b,
em {
left: 48px;
height: 5px;
background: #555867;
}
b {
top: 15px;
width: 76px;
}
em {
top: 28px;
width: 48px;
}
}
.theme-system {
background: linear-gradient(90deg, #f7f8fb 0 50%, #30323b 50% 100%);
i {
left: 12px;
top: 10px;
width: 24px;
height: 32px;
background: #fff;
}
b,
em {
left: 48px;
height: 5px;
background: rgb(255 255 255 / 55%);
}
b {
top: 15px;
width: 72px;
}
em {
top: 28px;
width: 46px;
}
}
.layout-vertical,
.layout-horizontal,
.layout-mixed,
.layout-dual,
.menu-design,
.menu-light,
.menu-dark {
background: #f7f9fd;
i,
b,
em {
background: #fff;
}
}
.layout-vertical {
i {
left: 8px;
top: 7px;
width: 18px;
height: 38px;
}
b,
em {
left: 38px;
height: 7px;
}
b {
top: 13px;
width: 74px;
}
em {
top: 29px;
width: 54px;
}
}
.layout-horizontal {
i {
left: 10px;
top: 8px;
width: calc(100% - 20px);
height: 12px;
}
b,
em {
left: 18px;
height: 6px;
}
b {
top: 28px;
width: 92px;
}
em {
top: 40px;
width: 56px;
}
}
.layout-mixed {
i {
left: 8px;
top: 7px;
width: 18px;
height: 38px;
}
b {
left: 34px;
top: 8px;
width: calc(100% - 44px);
height: 12px;
}
em {
left: 40px;
top: 30px;
width: 62px;
height: 7px;
}
}
.layout-dual {
i {
left: 8px;
top: 7px;
width: 14px;
height: 38px;
}
b {
left: 28px;
top: 7px;
width: 18px;
height: 38px;
}
em {
left: 58px;
top: 14px;
width: 72px;
height: 8px;
}
}
.menu-design {
background: #eef2fb;
i {
left: 8px;
top: 7px;
width: 18px;
height: 38px;
background: #dfe5f2;
}
b,
em {
left: 38px;
height: 7px;
background: #fff;
}
b {
top: 13px;
width: 74px;
}
em {
top: 29px;
width: 54px;
}
}
.menu-light {
i {
left: 8px;
top: 7px;
width: 18px;
height: 38px;
}
b,
em {
left: 38px;
height: 7px;
background: #eef2f8;
}
b {
top: 13px;
width: 74px;
}
em {
top: 29px;
width: 54px;
}
}
.menu-dark {
background: #30323b;
i {
left: 8px;
top: 7px;
width: 18px;
height: 38px;
background: #20222a;
}
b,
em {
left: 38px;
height: 7px;
background: #555867;
}
b {
top: 13px;
width: 74px;
}
em {
top: 29px;
width: 54px;
}
}
.colorGrid {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.colorDot {
width: 30px;
height: 30px;
padding: 0;
border: 2px solid #fff;
border-radius: 50%;
color: #fff;
cursor: pointer;
box-shadow: 0 0 0 1px #dfe5ee;
transition: transform 0.15s ease, box-shadow 0.15s ease;
&:hover,
&.active {
transform: scale(1.05);
box-shadow: 0 0 0 3px rgb(93 135 255 / 18%);
}
}
.colorPicker {
width: 30px;
height: 30px;
:global(.ant-color-picker-trigger) {
width: 30px;
height: 30px;
min-width: 30px;
padding: 2px;
border-radius: 50%;
}
}
.switchRow {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
min-height: 56px;
padding: 0;
border: 0;
background: transparent;
cursor: pointer;
span {
display: flex;
flex-direction: column;
text-align: left;
}
strong {
color: #111;
font-size: 15px;
font-weight: 500;
}
small {
margin-top: 4px;
color: #8b92a6;
font-size: 12px;
}
}
.backgroundGrid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.backgroundItem {
min-width: 0;
padding: 6px;
border: 2px solid #edf1f7;
border-radius: 8px;
background: transparent;
cursor: pointer;
transition: border-color 0.12s ease, box-shadow 0.12s ease;
&:hover,
&.active {
border-color: @colorPrimary;
}
img {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 6px;
}
span {
display: block;
margin-top: 6px;
color: #111;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.settingActions {
display: flex;
grid-column: 1 / -1;
gap: 8px;
justify-content: flex-end;
margin-top: 0;
padding-top: 12px;
border-top: 1px solid #e5e8ef;
background: transparent;
:global(.ant-btn) {
flex: 0 0 112px;
height: 32px;
border-radius: 6px !important;
}
}
.basicConfigSection {
grid-column: 1 / -1;
}
.basicConfigList {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px 28px;
}
.basicConfigItem {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 30px;
gap: 12px;
:global(.ant-switch) {
min-width: 40px;
height: 22px;
background: #d8dce6;
}
:global(.ant-switch .ant-switch-handle) {
top: 3px;
width: 16px;
height: 16px;
}
:global(.ant-switch.ant-switch-checked) {
background: #5d87ff;
}
:global(.ant-switch.ant-switch-checked .ant-switch-handle) {
inset-inline-start: calc(100% - 19px);
}
}
.basicConfigLabel {
min-width: 0;
color: #111;
font-size: 13px;
font-weight: 400;
line-height: 20px;
}
.basicConfigControl {
width: 132px !important;
:global(.ant-input-number-input),
:global(.ant-select-selection-item),
:global(.ant-select-selection-placeholder) {
font-size: 13px;
}
:global(.ant-input-number),
:global(.ant-select-selector) {
height: 32px !important;
border-radius: 8px !important;
}
:global(.ant-select-selector) {
align-items: center;
}
}
:global(html[data-theme='dark']) {
.artSettingsPage {
background: transparent;
}
.settingsPanel {
background: transparent;
box-shadow: none;
}
.sectionTitle {
color: var(--xagi-layout-text-primary, #fff);
&::before,
&::after {
background: var(
--xagi-layout-border-secondary,
rgba(255, 255, 255, 0.08)
);
}
}
.previewBox {
border-color: var(--xagi-layout-border-primary, rgba(255, 255, 255, 0.12));
background: rgba(255, 255, 255, 0.04);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.32);
&.active {
border-color: @colorPrimary;
box-shadow: 0 0 0 3px rgba(93, 135, 255, 0.18);
}
}
.settingName,
.basicConfigLabel,
.switchRow strong,
.backgroundItem span {
color: var(--xagi-layout-text-primary, #fff);
}
.switchRow small {
color: var(--xagi-layout-text-tertiary, rgba(255, 255, 255, 0.65));
}
.colorDot {
border-color: rgba(255, 255, 255, 0.18);
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.12);
&:hover,
&.active {
box-shadow: 0 0 0 3px rgba(93, 135, 255, 0.24);
}
}
.colorPicker {
:global(.ant-color-picker-trigger) {
border-color: var(
--xagi-layout-border-primary,
rgba(255, 255, 255, 0.12)
);
background: rgba(255, 255, 255, 0.04);
}
}
.backgroundItem {
border-color: var(--xagi-layout-border-primary, rgba(255, 255, 255, 0.12));
background: rgba(255, 255, 255, 0.03);
&:hover,
&.active {
border-color: @colorPrimary;
}
}
.settingActions {
border-top-color: var(
--xagi-layout-border-secondary,
rgba(255, 255, 255, 0.08)
);
background: transparent;
}
}
@media (max-width: 1180px) {
.settingsPanel {
max-width: 760px;
grid-template-columns: 1fr;
}
.basicConfigSection,
.settingActions {
grid-column: auto;
}
.basicConfigList {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.headerActions {
width: 100%;
}
.artSettingsPage {
padding: 14px 16px 24px;
}
.settingsPanel {
width: 100%;
min-height: auto;
margin-left: 0;
row-gap: 16px;
}
.settingBoxWrap {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.basicConfigList {
grid-template-columns: 1fr;
}
.settingActions {
justify-content: stretch;
:global(.ant-btn) {
flex: 1;
}
}
}

View File

@@ -1,25 +1,34 @@
import WorkspaceLayout from '@/components/WorkspaceLayout';
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 menuLayoutDual from '@/assets/images/settings/menu_layouts/dual_column.png';
import menuLayoutHorizontal from '@/assets/images/settings/menu_layouts/horizontal.png';
import menuLayoutMixed from '@/assets/images/settings/menu_layouts/mixed.png';
import menuLayoutVertical from '@/assets/images/settings/menu_layouts/vertical.png';
import menuStyleDesign from '@/assets/images/settings/menu_styles/design.png';
import menuStyleLight from '@/assets/images/settings/menu_styles/light.png';
import themeStyleDark from '@/assets/images/settings/theme_styles/dark.png';
import themeStyleLight from '@/assets/images/settings/theme_styles/light.png';
import themeStyleSystem from '@/assets/images/settings/theme_styles/system.png';
import {
backgroundConfigs,
DEFAULT_THEME_CONFIG,
THEME_COLOR_CONFIGS,
} from '@/constants/theme.constants';
import { useUnifiedTheme } from '@/hooks/useUnifiedTheme';
import { t } from '@/services/i18nRuntime';
import { apiSystemConfigUpdate } from '@/services/systemManage';
import { unifiedThemeService } from '@/services/unifiedThemeService';
import {
reloadConfiguration,
unifiedThemeService,
} from '@/services/unifiedThemeService';
import { BackgroundImage } from '@/types/background';
import { ThemeLayoutColorStyle } from '@/types/enums/theme';
ThemeLayoutColorStyle,
ThemeMenuLayoutType,
ThemeMenuStyleType,
ThemePageTransitionType,
ThemeTabStyleType,
} from '@/types/enums/theme';
import { ThemeConfigData } from '@/types/interfaces/systemManage';
import { App, Button } from 'antd';
import { CheckOutlined } from '@ant-design/icons';
import { App, Button, ColorPicker, InputNumber, Select, Switch } from 'antd';
import type { Color } from 'antd/es/color-picker';
import classNames from 'classnames';
import React, { useEffect, useMemo, useState } from 'react';
import { useLocation, useModel } from 'umi';
import React, { useMemo, useState } from 'react';
import { useModel } from 'umi';
import styles from './index.less';
// 使用统一的存储键名
@@ -28,13 +37,13 @@ const cx = classNames.bind(styles);
/**
* 主题配置页面(重构版本)
* 提供主题色、导航栏风格和背景图片的配置功能
* 提供主题色、导航栏风格和菜单布局的配置功能
*
* 重构特点:
* - 使用统一的主题数据管理服务
* - 所有切换都是临时预览效果,不会立即保存到本地缓存
* - Users must click the "Save Config" button to persist changes to backend and local cache
* - 支持完整的自定义功能(自定义颜色、背景图片上传等)
* - 支持完整的自定义功能(自定义颜色等)
* - 配置优先级:用户设置 > 租户信息设置 > 默认配置
*/
const ThemeConfig: React.FC = () => {
@@ -43,45 +52,176 @@ const ThemeConfig: React.FC = () => {
// 使用统一主题管理
const {
primaryColor,
backgroundId,
navigationStyle,
menuLayout,
menuStyle,
layoutStyle,
isNavigationDark,
antdTheme,
basicConfig,
extraColors,
updatePrimaryColor,
updateBackground,
updateNavigationStyle,
updateLayoutStyle,
} = useUnifiedTheme();
// 预览状态管理(临时预览,不立即保存)
const [previewPrimaryColor, setPreviewPrimaryColor] = useState(primaryColor);
const [previewBackgroundId, setPreviewBackgroundId] = useState(backgroundId);
const [previewNavigationStyle, setPreviewNavigationStyle] =
useState(navigationStyle);
const [previewMenuLayout, setPreviewMenuLayout] =
useState<ThemeMenuLayoutType>(menuLayout);
const [previewMenuStyle, setPreviewMenuStyle] =
useState<ThemeMenuStyleType>(menuStyle);
const [previewLayoutStyle, setPreviewLayoutStyle] = useState(layoutStyle);
const [previewIsNavigationDark, setPreviewIsNavigationDark] =
useState(isNavigationDark);
const location = useLocation();
const [previewAntdTheme, setPreviewAntdTheme] = useState<'light' | 'dark'>(
antdTheme,
);
const [previewBasicConfig, setPreviewBasicConfig] = useState(basicConfig);
// 转换背景配置为组件需要的格式
const backgroundImages: BackgroundImage[] = useMemo(() => {
return backgroundConfigs.map((config) => ({
id: config.id,
name: config.name,
path: config.url,
preview: config.url,
}));
}, []);
const themeStyleOptions = useMemo(
() => [
{ id: 'light', name: '浅色', img: themeStyleLight },
{ id: 'dark', name: '深色', img: themeStyleDark },
{ id: 'system', name: '系统', img: themeStyleSystem },
],
[],
);
// 根据背景图片ID获取对应的布局风格
const getLayoutStyleByBackgroundId = (
backgroundId: string,
): ThemeLayoutColorStyle => {
const backgroundConfig = backgroundConfigs.find(
(config) => config.id === backgroundId,
const menuLayoutOptions = useMemo(
() => [
{
id: ThemeMenuLayoutType.VERTICAL,
name: '垂直',
navigationStyle: 'style1',
img: menuLayoutVertical,
},
{
id: ThemeMenuLayoutType.HORIZONTAL,
name: '水平',
navigationStyle: 'style2',
img: menuLayoutHorizontal,
},
{
id: ThemeMenuLayoutType.MIXED,
name: '混合',
navigationStyle: 'style2',
img: menuLayoutMixed,
},
{
id: ThemeMenuLayoutType.DUAL,
name: '双列',
navigationStyle: 'style1',
img: menuLayoutDual,
},
],
[],
);
const menuStyleOptions = useMemo(
() => [
{ id: ThemeMenuStyleType.DESIGN, name: '设计风格', img: menuStyleDesign },
{ id: ThemeMenuStyleType.LIGHT, name: '浅色菜单', img: menuStyleLight },
],
[],
);
const basicSwitchOptions = useMemo(
() => [
{ key: 'showWorkTab', label: '开启多标签栏' },
{ key: 'uniqueOpened', label: '侧边栏开启手风琴模式' },
{ key: 'showMenuButton', label: '显示折叠侧边栏按钮' },
{ key: 'showFastEnter', label: '显示快速入口' },
{ key: 'showRefreshButton', label: '显示重载页面按钮' },
{ key: 'showCrumbs', label: '显示全局面包屑导航' },
{ key: 'showLanguage', label: '显示多语言选择' },
{ key: 'showNprogress', label: '显示顶部进度条' },
{ key: 'colorWeak', label: '色弱模式' },
{ key: 'watermarkVisible', label: '全局水印' },
],
[],
);
const tabStyleOptions = useMemo(
() => [
{ value: ThemeTabStyleType.DEFAULT, label: '默认' },
{ value: ThemeTabStyleType.CARD, label: '卡片' },
{ value: ThemeTabStyleType.GOOGLE, label: '谷歌' },
],
[],
);
const pageTransitionOptions = useMemo(
() => [
{ value: ThemePageTransitionType.NONE, label: '请选择' },
{ value: ThemePageTransitionType.FADE, label: '淡入淡出' },
{ value: ThemePageTransitionType.SLIDE_LEFT, label: '向左滑动' },
{ value: ThemePageTransitionType.SLIDE_BOTTOM, label: '向下滑动' },
{ value: ThemePageTransitionType.SLIDE_TOP, label: '向上滑动' },
],
[],
);
const customRadiusOptions = useMemo(
() => [
{ value: '0', label: '0' },
{ value: '0.25', label: '0.25' },
{ value: '0.5', label: '0.5' },
{ value: '0.75', label: '0.75' },
{ value: '1', label: '1' },
],
[],
);
const [previewThemeStyle, setPreviewThemeStyle] = useState<string>(
layoutStyle === ThemeLayoutColorStyle.DARK ? 'dark' : 'light',
);
const isDarkThemePreview = previewAntdTheme === 'dark';
const isMenuStyleChangeDisabled =
isDarkThemePreview ||
![ThemeMenuLayoutType.VERTICAL, ThemeMenuLayoutType.MIXED].includes(
previewMenuLayout,
);
return backgroundConfig?.layoutStyle || ThemeLayoutColorStyle.LIGHT;
const getAntdThemeByLayoutStyle = (
nextLayoutStyle: ThemeLayoutColorStyle,
): 'light' | 'dark' => {
return nextLayoutStyle === ThemeLayoutColorStyle.DARK ? 'dark' : 'light';
};
const getSystemLayoutStyle = (): ThemeLayoutColorStyle => {
if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
return ThemeLayoutColorStyle.DARK;
}
return ThemeLayoutColorStyle.LIGHT;
};
const isMenuStyleSupported = (
nextAntdTheme: 'light' | 'dark',
nextMenuLayout: ThemeMenuLayoutType,
) => {
return (
nextAntdTheme !== 'dark' &&
[ThemeMenuLayoutType.VERTICAL, ThemeMenuLayoutType.MIXED].includes(
nextMenuLayout,
)
);
};
const previewThemeData = async (
updates: Parameters<typeof unifiedThemeService.updateData>[0],
) => {
await unifiedThemeService.updateData(updates, { saveToStorage: false });
};
const handleBasicConfigChange = async (
updates: Partial<typeof previewBasicConfig>,
) => {
const nextBasicConfig = { ...previewBasicConfig, ...updates };
setPreviewBasicConfig(nextBasicConfig);
try {
await previewThemeData({ basicConfig: nextBasicConfig });
} catch (error) {
console.warn('Failed to preview basic config:', error);
}
};
// 处理主题色变更(临时预览,但实时显示效果)
@@ -96,6 +236,87 @@ const ThemeConfig: React.FC = () => {
}
};
const handleCustomColorChange = (_color: Color, hex: string) => {
handleColorChange(hex);
};
const handleThemeStyleChange = async (mode: string) => {
setPreviewThemeStyle(mode);
const nextLayoutStyle =
mode === 'dark'
? ThemeLayoutColorStyle.DARK
: mode === 'system'
? getSystemLayoutStyle()
: ThemeLayoutColorStyle.LIGHT;
const nextAntdTheme = getAntdThemeByLayoutStyle(nextLayoutStyle);
const nextMenuStyle = isMenuStyleSupported(nextAntdTheme, previewMenuLayout)
? previewMenuStyle
: ThemeMenuStyleType.DESIGN;
setPreviewLayoutStyle(nextLayoutStyle);
setPreviewAntdTheme(nextAntdTheme);
setPreviewMenuStyle(nextMenuStyle);
try {
await previewThemeData({
layoutStyle: nextLayoutStyle,
antdTheme: nextAntdTheme,
menuStyle: nextMenuStyle,
});
} catch (error) {
console.warn('Failed to preview theme style:', error);
}
};
const handleMenuLayoutChange = async (
layoutId: ThemeMenuLayoutType,
styleId: string,
) => {
setPreviewMenuLayout(layoutId);
setPreviewNavigationStyle(styleId as any);
const nextMenuStyle = isMenuStyleSupported(previewAntdTheme, layoutId)
? previewMenuStyle
: ThemeMenuStyleType.DESIGN;
setPreviewMenuStyle(nextMenuStyle);
try {
await previewThemeData({
menuLayout: layoutId,
menuStyle: nextMenuStyle,
navigationStyle: styleId as any,
layoutStyle: previewLayoutStyle,
antdTheme: previewAntdTheme,
});
} catch (error) {
console.warn('Failed to preview menu layout:', error);
}
};
const handleMenuStyleChange = async (styleId: ThemeMenuStyleType) => {
if (isDarkThemePreview) {
message.warning('深色主题下不可修改菜单风格');
return;
}
if (
![ThemeMenuLayoutType.VERTICAL, ThemeMenuLayoutType.MIXED].includes(
previewMenuLayout,
)
) {
message.warning('当前菜单布局不支持修改菜单风格,仅垂直和混合布局可用');
return;
}
setPreviewMenuStyle(styleId);
try {
await previewThemeData({
menuStyle: styleId,
});
} catch (error) {
console.warn('Failed to preview menu style:', error);
}
};
// 处理导航风格变更(临时预览,但实时显示效果)
const handleNavigationStyleChange = async (styleId: string) => {
setPreviewNavigationStyle(styleId as any);
@@ -103,126 +324,42 @@ const ThemeConfig: React.FC = () => {
// 立即应用导航风格预览效果不保存到localStorage
// 使用批量更新确保导航风格和布局风格同步应用
try {
// 导入 unifiedThemeService 进行批量更新
const { unifiedThemeService } = await import(
'@/services/unifiedThemeService'
);
// 批量更新导航风格和布局风格确保CSS变量同步
await unifiedThemeService.updateData(
{
navigationStyle: styleId as any,
layoutStyle: previewLayoutStyle,
},
{ saveToStorage: false },
);
await previewThemeData({
navigationStyle: styleId as any,
layoutStyle: previewLayoutStyle,
antdTheme: previewAntdTheme,
});
} catch (error) {
console.warn('Failed to preview navigation style:', error);
}
};
// 背景图片切换处理(临时预览,但实时显示效果)
const handleBackgroundChange = async (backgroundId: string) => {
// 设置背景图片(临时预览)
setPreviewBackgroundId(backgroundId);
// 根据背景图片自动切换导航栏深浅色(临时预览)
const newLayoutStyle = getLayoutStyleByBackgroundId(backgroundId);
setPreviewLayoutStyle(newLayoutStyle);
setPreviewIsNavigationDark(newLayoutStyle === ThemeLayoutColorStyle.DARK);
// 立即应用背景图预览效果不保存到localStorage
try {
await updateBackground(backgroundId, { saveToStorage: false });
} catch (error) {
console.warn('Failed to preview background image:', error);
}
// 显示联动提示
const backgroundConfig = backgroundConfigs.find(
(config) => config.id === backgroundId,
);
if (backgroundConfig) {
message.info(
t(
'PC.Pages.SystemThemeConfig.autoSwitchedNavModePreview',
newLayoutStyle === ThemeLayoutColorStyle.DARK
? t('PC.Pages.SystemThemeConfig.darkMode')
: t('PC.Pages.SystemThemeConfig.lightMode'),
),
);
}
};
// 切换导航栏深浅色(临时预览,但实时显示效果)
const handleNavigationThemeToggle = async () => {
const newLayoutStyle =
previewLayoutStyle === ThemeLayoutColorStyle.DARK
? ThemeLayoutColorStyle.LIGHT
: ThemeLayoutColorStyle.DARK;
setPreviewLayoutStyle(newLayoutStyle);
setPreviewIsNavigationDark(newLayoutStyle === ThemeLayoutColorStyle.DARK);
// 立即应用布局风格预览效果不保存到localStorage
try {
await updateLayoutStyle(newLayoutStyle, { saveToStorage: false });
} catch (error) {
console.warn('Failed to preview layout style:', error);
}
// 检查当前背景是否与新的导航栏深浅色匹配
const currentBackgroundLayoutStyle =
getLayoutStyleByBackgroundId(previewBackgroundId);
if (currentBackgroundLayoutStyle !== newLayoutStyle) {
// 当前背景不匹配,自动切换到匹配的背景(临时预览)
const matchingBackground = backgroundConfigs.find(
(config) => config.layoutStyle === newLayoutStyle,
);
if (matchingBackground) {
// 切换背景但不触发布局风格联动(避免循环)
setPreviewBackgroundId(matchingBackground.id);
// 立即应用背景图预览效果不保存到localStorage
try {
await updateBackground(matchingBackground.id, {
saveToStorage: false,
});
} catch (error) {
console.warn('Failed to preview background image:', error);
}
// 显示背景自动匹配提示
message.info(
t(
'PC.Pages.SystemThemeConfig.autoSwitchedBackgroundPreview',
matchingBackground.name,
newLayoutStyle === ThemeLayoutColorStyle.DARK
? t('PC.Pages.SystemThemeConfig.darkMode')
: t('PC.Pages.SystemThemeConfig.lightMode'),
),
);
}
}
};
// 保存配置到后端
const handleSave = async () => {
try {
// 首先使用统一主题服务保存预览的配置
await updatePrimaryColor(previewPrimaryColor);
await updateBackground(previewBackgroundId);
await updateNavigationStyle(previewNavigationStyle);
await updateLayoutStyle(previewLayoutStyle);
await unifiedThemeService.updateData({
primaryColor: previewPrimaryColor,
backgroundId: '',
navigationStyle: previewNavigationStyle,
menuLayout: previewMenuLayout,
menuStyle: previewMenuStyle,
basicConfig: previewBasicConfig,
layoutStyle: previewLayoutStyle,
antdTheme: previewAntdTheme,
});
// 构建后端所需的数据格式
const themeConfig: ThemeConfigData = {
primaryColor: previewPrimaryColor,
backgroundId: previewBackgroundId,
antdTheme: DEFAULT_THEME_CONFIG.THEME, // 目前没有支持切换Ant Design主题
backgroundId: '',
antdTheme: previewAntdTheme,
layoutStyle: previewLayoutStyle, // 导航栏深浅色
navigationStyle: previewNavigationStyle, // 导航风格 ID
menuLayout: previewMenuLayout,
menuStyle: previewMenuStyle,
basicConfig: previewBasicConfig,
timestamp: Date.now(),
};
@@ -244,86 +381,251 @@ const ThemeConfig: React.FC = () => {
};
// 重置为默认配置
const handleReset = () => {
setPreviewPrimaryColor('#5147ff'); // 默认蓝色
setPreviewBackgroundId('bg-variant-1'); // 默认背景
const handleReset = async () => {
setPreviewPrimaryColor(DEFAULT_THEME_CONFIG.PRIMARY_COLOR); // 默认蓝色
setPreviewNavigationStyle('style1' as any);
setPreviewThemeStyle('light');
setPreviewMenuLayout(ThemeMenuLayoutType.DUAL);
setPreviewMenuStyle(DEFAULT_THEME_CONFIG.MENU_STYLE);
setPreviewBasicConfig({ ...DEFAULT_THEME_CONFIG.BASIC_CONFIG });
setPreviewLayoutStyle(ThemeLayoutColorStyle.LIGHT);
setPreviewIsNavigationDark(false);
setPreviewAntdTheme('light');
try {
await previewThemeData({
primaryColor: DEFAULT_THEME_CONFIG.PRIMARY_COLOR,
backgroundId: '',
navigationStyle: 'style1' as any,
menuLayout: ThemeMenuLayoutType.DUAL,
menuStyle: DEFAULT_THEME_CONFIG.MENU_STYLE,
basicConfig: { ...DEFAULT_THEME_CONFIG.BASIC_CONFIG },
layoutStyle: ThemeLayoutColorStyle.LIGHT,
antdTheme: 'light',
});
} catch (error) {
console.warn('Failed to preview reset theme:', error);
}
message.info(t('PC.Pages.SystemThemeConfig.resetPreview'));
};
// 恢复到已保存状态(页面加载状态)
const handleRestore = React.useCallback(() => {
reloadConfiguration(); // 重新加载存储中的配置
const savedData = unifiedThemeService.getCurrentData(); // 获取重新加载后的数据
setPreviewPrimaryColor(savedData.primaryColor);
setPreviewBackgroundId(savedData.backgroundId);
setPreviewNavigationStyle(savedData.navigationStyle);
setPreviewLayoutStyle(savedData.layoutStyle);
setPreviewIsNavigationDark(
savedData.layoutStyle === ThemeLayoutColorStyle.DARK,
);
// 使用 setTimeout 确保在 remount 后消息能正常显示
// setTimeout(() => {
// message.info('Reset to default config (preview mode)');
// }, 200);
}, [message]);
// 监听 location.state 变化
useEffect(() => {
const state = location.state as any;
if (state?._t) {
handleRestore();
}
}, [location.state, handleRestore]);
return (
<WorkspaceLayout
title={t('PC.Pages.SystemThemeConfig.pageTitle')}
extraContent={
<div style={{ padding: '12px 6px' }}>
<>
<div className={cx(styles.artSettingsPage)}>
<aside className={cx(styles.settingsPanel)}>
<div className={cx(styles.settingSection)}>
<div className={cx(styles.sectionTitle)}>
<span></span>
</div>
<div className={cx(styles.settingBoxWrap)}>
{themeStyleOptions.map((item) => (
<button
key={item.id}
type="button"
className={cx(styles.settingItem)}
onClick={() => handleThemeStyleChange(item.id)}
>
<span
className={cx(styles.previewBox, {
[styles.active]: previewThemeStyle === item.id,
})}
>
<img src={item.img} alt={item.name} />
</span>
<span className={cx(styles.settingName)}>{item.name}</span>
</button>
))}
</div>
</div>
<div className={cx(styles.settingSection)}>
<div className={cx(styles.sectionTitle)}>
<span></span>
</div>
<div className={cx(styles.settingBoxWrap)}>
{menuLayoutOptions.map((item, index) => (
<button
key={item.id}
type="button"
className={cx(styles.settingItem)}
onClick={() =>
handleMenuLayoutChange(item.id, item.navigationStyle)
}
title={item.name}
>
<span
className={cx(
styles.previewBox,
index > 2 && styles.withTopMargin,
{ [styles.active]: previewMenuLayout === item.id },
)}
>
<img src={item.img} alt={item.name} />
</span>
<span className={cx(styles.settingName)}>{item.name}</span>
</button>
))}
</div>
</div>
<div className={cx(styles.settingSection)}>
<div className={cx(styles.sectionTitle)}>
<span></span>
</div>
<div className={cx(styles.settingBoxWrap)}>
{menuStyleOptions.map((item) => (
<button
key={item.id}
type="button"
className={cx(styles.settingItem, {
[styles.disabled]: isMenuStyleChangeDisabled,
})}
onClick={() => handleMenuStyleChange(item.id)}
title={
isMenuStyleChangeDisabled
? isDarkThemePreview
? '深色主题下不可修改菜单风格'
: '当前菜单布局不支持修改菜单风格,仅垂直和混合布局可用'
: item.name
}
>
<span
className={cx(styles.previewBox, {
[styles.active]: previewMenuStyle === item.id,
})}
>
<img src={item.img} alt={item.name} />
</span>
<span className={cx(styles.settingName)}>{item.name}</span>
</button>
))}
</div>
</div>
<div className={cx(styles.settingSection)}>
<div className={cx(styles.sectionTitle)}>
<span></span>
</div>
<div className={cx(styles.colorGrid)}>
{[
...THEME_COLOR_CONFIGS,
...extraColors.map((color) => ({ color, name: '自定义' })),
].map((item) => (
<button
key={item.color}
type="button"
className={cx(styles.colorDot, {
[styles.active]: previewPrimaryColor === item.color,
})}
style={{ backgroundColor: item.color }}
title={item.name}
onClick={() => handleColorChange(item.color)}
>
{previewPrimaryColor === item.color && <CheckOutlined />}
</button>
))}
<ColorPicker
value={previewPrimaryColor}
format="hex"
onChange={handleCustomColorChange}
className={cx(styles.colorPicker)}
/>
</div>
</div>
<div className={cx(styles.settingSection, styles.basicConfigSection)}>
<div className={cx(styles.sectionTitle)}>
<span></span>
</div>
<div className={cx(styles.basicConfigList)}>
{basicSwitchOptions.map((item) => (
<div key={item.key} className={cx(styles.basicConfigItem)}>
<span className={cx(styles.basicConfigLabel)}>
{item.label}
</span>
<Switch
checked={
previewBasicConfig[
item.key as keyof typeof previewBasicConfig
] as boolean
}
onChange={(checked) =>
handleBasicConfigChange({ [item.key]: checked })
}
/>
</div>
))}
<div className={cx(styles.basicConfigItem)}>
<span className={cx(styles.basicConfigLabel)}></span>
<InputNumber
min={180}
max={320}
step={10}
value={previewBasicConfig.menuOpenWidth}
onChange={(value) =>
handleBasicConfigChange({
menuOpenWidth: Number(value || 180),
})
}
className={cx(styles.basicConfigControl)}
/>
</div>
<div className={cx(styles.basicConfigItem)}>
<span className={cx(styles.basicConfigLabel)}></span>
<Select
value={previewBasicConfig.tabStyle}
options={tabStyleOptions}
onChange={(value) =>
handleBasicConfigChange({ tabStyle: value })
}
className={cx(styles.basicConfigControl)}
/>
</div>
<div className={cx(styles.basicConfigItem)}>
<span className={cx(styles.basicConfigLabel)}>
</span>
<Select
value={previewBasicConfig.pageTransition}
options={pageTransitionOptions}
onChange={(value) =>
handleBasicConfigChange({ pageTransition: value })
}
className={cx(styles.basicConfigControl)}
placeholder="请选择"
/>
</div>
<div className={cx(styles.basicConfigItem)}>
<span className={cx(styles.basicConfigLabel)}></span>
<Select
value={previewBasicConfig.customRadius}
options={customRadiusOptions}
onChange={(value) =>
handleBasicConfigChange({ customRadius: value })
}
className={cx(styles.basicConfigControl)}
/>
</div>
</div>
</div>
{hasPermission('system_theme_config_save') && (
<>
<div className={cx(styles.settingActions)}>
<Button type="primary" onClick={handleSave}>
{t('PC.Pages.SystemThemeConfig.saveConfig')}
</Button>
<Button style={{ marginLeft: 12 }} onClick={handleReset}>
<Button danger ghost onClick={handleReset}>
{t('PC.Pages.SystemThemeConfig.resetDefault')}
</Button>
</>
</div>
)}
</div>
}
>
<div className={cx(styles.configContainer)}>
<div className={cx(styles.configItem)}>
<ThemeColorPanel
currentColor={previewPrimaryColor}
onColorChange={handleColorChange}
extraColors={extraColors}
/>
</div>
<div className={cx(styles.configItem)}>
<NavigationStylePanel
isNavigationDarkMode={previewIsNavigationDark}
onNavigationThemeToggle={handleNavigationThemeToggle}
onNavigationStyleChange={handleNavigationStyleChange}
currentNavigationStyle={previewNavigationStyle}
/>
</div>
<div className={cx(styles.configItem)}>
<BackgroundImagePanel
backgroundImages={backgroundImages}
currentBackground={previewBackgroundId}
onBackgroundChange={handleBackgroundChange}
enableCustomUpload={false} //先关闭后期考虑开启
/>
</div>
</aside>
</div>
</WorkspaceLayout>
</>
);
};

View File

@@ -48,7 +48,7 @@ export const getTenantInfo = async (): Promise<TenantInfo> => {
},
],
// 使用统一主题数据,如果没有则使用默认值
defaultThemeColor: currentData.primaryColor || '#5147ff',
defaultThemeColor: currentData.primaryColor || '#5D87FF',
defaultBackgroundId: currentBackground || 'variant-1',
defaultNavigationStyleId:
currentNavStyle === 'style2' ? 'style2' : 'style1',

View File

@@ -17,13 +17,33 @@ import {
STORAGE_KEYS,
STYLE_CONFIGS,
THEME_COLOR_CONFIGS,
backgroundConfigs,
} from '@/constants/theme.constants';
import {
ThemeLayoutColorStyle,
ThemeMenuLayoutType,
ThemeMenuStyleType,
ThemeNavigationStyleType,
ThemePageTransitionType,
ThemeTabStyleType,
} from '@/types/enums/theme';
export interface UnifiedThemeBasicConfig {
showWorkTab: boolean;
uniqueOpened: boolean;
showMenuButton: boolean;
showFastEnter: boolean;
showRefreshButton: boolean;
showCrumbs: boolean;
showLanguage: boolean;
showNprogress: boolean;
colorWeak: boolean;
watermarkVisible: boolean;
menuOpenWidth: number;
tabStyle: ThemeTabStyleType;
pageTransition: ThemePageTransitionType;
customRadius: string;
}
/**
* 统一主题配置接口
*/
@@ -34,6 +54,8 @@ export interface UnifiedThemeData {
// 导航配置
navigationStyle: ThemeNavigationStyleType;
menuLayout: ThemeMenuLayoutType;
menuStyle: ThemeMenuStyleType;
layoutStyle: ThemeLayoutColorStyle;
// 背景配置
@@ -42,6 +64,9 @@ export interface UnifiedThemeData {
// 语言配置
language: 'zh-CN' | 'en-US';
// 基础配置
basicConfig: UnifiedThemeBasicConfig;
// 元数据
timestamp: number;
source: 'user' | 'tenant' | 'default';
@@ -148,14 +173,64 @@ class UnifiedThemeService {
antdTheme: DEFAULT_THEME_CONFIG.THEME as 'light' | 'dark',
navigationStyle:
DEFAULT_THEME_CONFIG.NAVIGATION_STYLE as ThemeNavigationStyleType,
menuLayout: DEFAULT_THEME_CONFIG.MENU_LAYOUT as ThemeMenuLayoutType,
menuStyle: DEFAULT_THEME_CONFIG.MENU_STYLE as ThemeMenuStyleType,
layoutStyle: DEFAULT_THEME_CONFIG.LAYOUT_STYLE as ThemeLayoutColorStyle,
backgroundId: DEFAULT_THEME_CONFIG.BACKGROUND_ID,
language: DEFAULT_THEME_CONFIG.LANGUAGE as 'zh-CN' | 'en-US',
basicConfig: { ...DEFAULT_THEME_CONFIG.BASIC_CONFIG },
timestamp: Date.now(),
source: 'default',
};
}
private normalizeMenuStyle(
menuStyle?: ThemeMenuStyleType,
): ThemeMenuStyleType {
if (
menuStyle === ThemeMenuStyleType.DESIGN ||
menuStyle === ThemeMenuStyleType.LIGHT
) {
return menuStyle;
}
return DEFAULT_THEME_CONFIG.MENU_STYLE as ThemeMenuStyleType;
}
private normalizeBasicConfig(config?: any): UnifiedThemeBasicConfig {
const defaults = DEFAULT_THEME_CONFIG.BASIC_CONFIG;
const menuOpenWidth = Number(config?.menuOpenWidth);
const customRadius = String(config?.customRadius ?? defaults.customRadius);
const tabStyleValues = Object.values(ThemeTabStyleType);
const pageTransitionValues = Object.values(ThemePageTransitionType);
return {
showWorkTab: config?.showWorkTab ?? defaults.showWorkTab,
uniqueOpened: config?.uniqueOpened ?? defaults.uniqueOpened,
showMenuButton: config?.showMenuButton ?? defaults.showMenuButton,
showFastEnter: config?.showFastEnter ?? defaults.showFastEnter,
showRefreshButton:
config?.showRefreshButton ?? defaults.showRefreshButton,
showCrumbs: config?.showCrumbs ?? defaults.showCrumbs,
showLanguage: config?.showLanguage ?? defaults.showLanguage,
showNprogress: config?.showNprogress ?? defaults.showNprogress,
colorWeak: config?.colorWeak ?? defaults.colorWeak,
watermarkVisible: config?.watermarkVisible ?? defaults.watermarkVisible,
menuOpenWidth: Number.isFinite(menuOpenWidth)
? Math.min(320, Math.max(180, menuOpenWidth))
: defaults.menuOpenWidth,
tabStyle: tabStyleValues.includes(config?.tabStyle)
? config.tabStyle
: defaults.tabStyle,
pageTransition: pageTransitionValues.includes(config?.pageTransition)
? config.pageTransition
: defaults.pageTransition,
customRadius: ['0', '0.25', '0.5', '0.75', '1'].includes(customRadius)
? customRadius
: defaults.customRadius,
};
}
/**
* 标准化用户配置格式
*/
@@ -165,9 +240,12 @@ class UnifiedThemeService {
primaryColor: config.selectedThemeColor || defaults.primaryColor,
antdTheme: config.antdTheme || defaults.antdTheme,
navigationStyle: config.navigationStyleId || defaults.navigationStyle,
menuLayout: config.menuLayout || defaults.menuLayout,
menuStyle: this.normalizeMenuStyle(config.menuStyle),
layoutStyle: config.navigationStyle || defaults.layoutStyle,
backgroundId: config.selectedBackgroundId || defaults.backgroundId,
backgroundId: '',
language: config.language || defaults.language,
basicConfig: this.normalizeBasicConfig(config.basicConfig),
timestamp: config.timestamp || Date.now(),
source: 'user',
};
@@ -182,9 +260,12 @@ class UnifiedThemeService {
primaryColor: settings.primaryColor || defaults.primaryColor,
antdTheme: settings.theme || defaults.antdTheme,
navigationStyle: defaults.navigationStyle,
menuLayout: settings.menuLayout || defaults.menuLayout,
menuStyle: this.normalizeMenuStyle(settings.menuStyle),
layoutStyle: defaults.layoutStyle,
backgroundId: settings.backgroundImageId || defaults.backgroundId,
backgroundId: '',
language: settings.language || defaults.language,
basicConfig: this.normalizeBasicConfig(settings.basicConfig),
timestamp: Date.now(),
source: 'user',
};
@@ -199,9 +280,12 @@ class UnifiedThemeService {
primaryColor: config.selectedThemeColor || defaults.primaryColor,
antdTheme: config.antdTheme || defaults.antdTheme,
navigationStyle: config.navigationStyleId || defaults.navigationStyle,
menuLayout: config.menuLayout || defaults.menuLayout,
menuStyle: this.normalizeMenuStyle(config.menuStyle),
layoutStyle: config.navigationStyle || defaults.layoutStyle,
backgroundId: config.selectedBackgroundId || defaults.backgroundId,
backgroundId: '',
language: config.language || defaults.language,
basicConfig: this.normalizeBasicConfig(config.basicConfig),
timestamp: config.timestamp || Date.now(),
source: 'tenant',
};
@@ -277,6 +361,26 @@ class UnifiedThemeService {
await this.updateData({ navigationStyle: style }, options);
}
/**
* 更新菜单布局
*/
async updateMenuLayout(
menuLayout: ThemeMenuLayoutType,
options: UpdateOptions = {},
): Promise<void> {
await this.updateData({ menuLayout }, options);
}
/**
* 更新菜单视觉风格
*/
async updateMenuStyle(
menuStyle: ThemeMenuStyleType,
options: UpdateOptions = {},
): Promise<void> {
await this.updateData({ menuStyle }, options);
}
/**
* 更新布局风格(导航栏深浅色)
*/
@@ -294,19 +398,7 @@ class UnifiedThemeService {
backgroundId: string,
options: UpdateOptions = {},
): Promise<void> {
// 根据背景图自动设置布局风格
const backgroundConfig = backgroundConfigs.find(
(bg) => bg.id === backgroundId,
);
const layoutStyle = backgroundConfig?.layoutStyle;
const updates: Partial<UnifiedThemeData> = { backgroundId };
if (layoutStyle) {
updates.layoutStyle = layoutStyle;
}
await this.updateData(updates, options);
await this.updateData({ backgroundId: '' }, options);
}
/**
@@ -336,6 +428,13 @@ class UnifiedThemeService {
this.currentData = {
...this.currentData,
...(updates || {}),
backgroundId: '',
basicConfig: this.normalizeBasicConfig(
updates?.basicConfig || this.currentData.basicConfig,
),
menuStyle: this.normalizeMenuStyle(
updates?.menuStyle || this.currentData.menuStyle,
),
timestamp: Date.now(),
source: 'user', // 用户操作都标记为用户来源
};
@@ -365,10 +464,13 @@ class UnifiedThemeService {
// 保存到用户主题配置
const userThemeConfig = {
selectedThemeColor: this.currentData.primaryColor,
selectedBackgroundId: this.currentData.backgroundId,
selectedBackgroundId: '',
antdTheme: this.currentData.antdTheme,
navigationStyle: this.currentData.layoutStyle,
navigationStyleId: this.currentData.navigationStyle,
menuLayout: this.currentData.menuLayout,
menuStyle: this.currentData.menuStyle,
basicConfig: this.currentData.basicConfig,
language: this.currentData.language,
timestamp: this.currentData.timestamp,
};
@@ -383,7 +485,10 @@ class UnifiedThemeService {
theme: this.currentData.antdTheme,
language: this.currentData.language,
primaryColor: this.currentData.primaryColor,
backgroundImageId: this.currentData.backgroundId,
backgroundImageId: '',
menuLayout: this.currentData.menuLayout,
menuStyle: this.currentData.menuStyle,
basicConfig: this.currentData.basicConfig,
};
localStorage.setItem(
@@ -408,22 +513,7 @@ class UnifiedThemeService {
this.currentData.primaryColor,
);
// 设置背景图
if (this.currentData.backgroundId) {
// 查找背景图配置获取正确的URL
const backgroundConfig = backgroundConfigs.find(
(bg) => bg.id === this.currentData.backgroundId,
);
const backgroundUrl =
backgroundConfig?.url || `/bg/${this.currentData.backgroundId}.png`;
// 设置CSS变量
root.style.setProperty(
'--xagi-background-image',
`url(${backgroundUrl})`,
);
}
root.style.removeProperty('--xagi-background-image');
// 根据布局风格和导航风格应用完整的CSS变量
const layoutStyleKey =
@@ -465,6 +555,32 @@ class UnifiedThemeService {
? 'compact'
: 'expanded',
);
root.setAttribute('data-menu-layout', this.currentData.menuLayout);
root.setAttribute('data-menu-style', this.currentData.menuStyle);
root.setAttribute(
'data-tab-style',
this.currentData.basicConfig.tabStyle,
);
root.setAttribute(
'data-page-transition',
this.currentData.basicConfig.pageTransition || 'none',
);
root.setAttribute(
'data-color-weak',
String(this.currentData.basicConfig.colorWeak),
);
root.setAttribute(
'data-watermark-visible',
String(this.currentData.basicConfig.watermarkVisible),
);
root.style.setProperty(
'--xagi-basic-menu-open-width',
`${this.currentData.basicConfig.menuOpenWidth}px`,
);
root.style.setProperty(
'--xagi-custom-radius',
`${this.currentData.basicConfig.customRadius}rem`,
);
this.updateBodyClasses();
} catch (error) {
console.error('Failed to apply theme data to DOM:', error);
@@ -481,11 +597,28 @@ class UnifiedThemeService {
'xagi-layout-dark',
'xagi-nav-style1',
'xagi-nav-style2',
'xagi-menu-layout-vertical',
'xagi-menu-layout-horizontal',
'xagi-menu-layout-mixed',
'xagi-menu-layout-dual',
'xagi-menu-style-design',
'xagi-menu-style-light',
'xagi-menu-style-dark',
'xagi-color-weak',
);
// 添加当前样式对应的类名
document.body.classList.add(`xagi-layout-${this.currentData.layoutStyle}`);
document.body.classList.add(`xagi-nav-${this.currentData.navigationStyle}`);
document.body.classList.add(
`xagi-menu-layout-${this.currentData.menuLayout}`,
);
document.body.classList.add(
`xagi-menu-style-${this.currentData.menuStyle}`,
);
if (this.currentData.basicConfig.colorWeak) {
document.body.classList.add('xagi-color-weak');
}
}
/**

View File

@@ -108,7 +108,7 @@
@BLUE10: #0256ff;
@BLUE11: #edf3ff;
@BLUE12: rgba(150, 159, 255, 0.45);
@BLUE13: #5147ff;
@BLUE13: #5d87ff;
@GREEN: #008000;
@GREEN01: rgba(103, 229, 146, 0.2);

View File

@@ -210,7 +210,7 @@ each(range(2, 3, 1), {
}
.radius-10 {
border-radius: 10px;
border-radius: 8px;
}
.font-12 {

View File

@@ -17,7 +17,7 @@
// ---------- 表格滚动条:只保留滑块 ----------
// Firefox
scrollbar-color: @GRAY19 transparent;
scrollbar-color: rgba(91, 105, 135, 0.28) transparent;
scrollbar-width: thin;
// Chrome / Safari / Edge
@@ -36,12 +36,12 @@
}
&::-webkit-scrollbar-thumb {
background: @GRAY19;
background: rgba(91, 105, 135, 0.28);
border-radius: 6px;
}
&::-webkit-scrollbar-thumb:hover {
background: @GRAY;
background: rgba(91, 105, 135, 0.48);
}
// ---------- 固定列背景(右侧“操作”列) ----------

View File

@@ -11,7 +11,7 @@
img {
width: 50px;
height: 50px;
border-radius: 10px;
border-radius: 8px;
margin-right: 15px;
}
@@ -29,7 +29,7 @@
background-color: #f6f4dd;
text-align: center;
padding: 10px 0;
border-radius: 10px;
border-radius: 8px;
}
.add-member-modal-content {

View File

@@ -488,12 +488,12 @@
// 导航尺寸配置直接使用动态CSS变量
@navFirstMenuWidth: var(--xagi-nav-first-menu-width, 60px);
@navFirstMenuWidthStyle2: 88px; // 展开模式宽度
@navFirstMenuWidthStyle2: 112px; // 展开模式宽度
@navFirstMenuFontSize: @fontSizeSm;
// 导航背景色使用Ant Design的动态颜色
@navFirstMenuBg: var(--xagi-color-bg-container, rgba(255, 255, 255, 0.95));
@navSecondMenuBg: var(--xagi-color-bg-layout, rgba(255, 255, 255, 0.85));
@navFirstMenuBg: var(--xagi-layout-bg-secondary, #f2f5fb);
@navSecondMenuBg: var(--xagi-layout-bg-secondary, #f2f5fb);
// 页面容器相关Token
@pageContainerMargin: var(--xagi-page-container-margin, 12px);
@@ -524,9 +524,9 @@
);
// 导航交互状态颜色(使用主题色)
@navItemHoverBg: #ffffff;
@navItemHoverBg: rgba(255, 255, 255, 0.66);
@navItemActiveBg: #ffffff;
@navItemSelectedBg: #ffffff;
@navItemSelectedBg: var(--xagi-color-primary-bg, rgba(93, 135, 255, 0.1));
// 导航边框和分割线(使用独立的布局风格变量)
@navBorderColor: var(--xagi-layout-border-primary, rgba(0, 0, 0, 0.15));
@@ -538,7 +538,7 @@
);
// 导航阴影(使用独立的布局风格变量)
@navShadow: var(--xagi-layout-shadow, 0 2px 8px rgba(0, 0, 0, 0.15));
@navShadow: var(--xagi-layout-shadow, 0 8px 20px rgba(32, 45, 84, 0.08));
@navFirstMenuShadow: none;
@navSecondMenuShadow: none;

View File

@@ -24,3 +24,53 @@ export enum ThemeNavigationStyleType {
/** 展开模式 */
STYLE2 = 'style2',
}
/**
* 菜单布局类型
* 对应 Art Design Pro 的 Left / Top / TopLeft / DualMenu 四类结构
*/
export enum ThemeMenuLayoutType {
/** 左侧垂直菜单 */
VERTICAL = 'vertical',
/** 顶部水平菜单 */
HORIZONTAL = 'horizontal',
/** 顶部一级菜单 + 左侧二级菜单 */
MIXED = 'mixed',
/** 左侧图标栏 + 右侧二级菜单 */
DUAL = 'dual',
}
/**
* 菜单视觉风格类型
* 只用于侧边菜单外观,不改变主题模式和菜单布局
*/
export enum ThemeMenuStyleType {
/** Art Design Pro 风格 */
DESIGN = 'design',
/** 浅色菜单 */
LIGHT = 'light',
/** 深色菜单 */
DARK = 'dark',
}
/**
* 标签页风格类型
* 对齐 Art Design Pro 的 tab-default / tab-card / tab-google
*/
export enum ThemeTabStyleType {
DEFAULT = 'tab-default',
CARD = 'tab-card',
GOOGLE = 'tab-google',
}
/**
* 页面切换动画类型
* 对齐 Art Design Pro 的 transition 配置
*/
export enum ThemePageTransitionType {
NONE = '',
FADE = 'fade',
SLIDE_LEFT = 'slide-left',
SLIDE_BOTTOM = 'slide-bottom',
SLIDE_TOP = 'slide-top',
}

View File

@@ -7,6 +7,12 @@ import {
UserRoleEnum,
UserStatusEnum,
} from '@/types/enums/systemManage';
import {
ThemeMenuLayoutType,
ThemeMenuStyleType,
ThemePageTransitionType,
ThemeTabStyleType,
} from '@/types/enums/theme';
import { PublishStatusEnum } from '../enums/common';
import {
KnowledgeDataTypeEnum,
@@ -437,6 +443,27 @@ export interface ThemeConfigData {
layoutStyle: 'light' | 'dark';
/** 导航风格IDstyle1/style2 */
navigationStyle: 'style1' | 'style2';
/** 菜单布局(垂直/水平/混合/双列) */
menuLayout?: ThemeMenuLayoutType;
/** 菜单视觉风格 */
menuStyle?: ThemeMenuStyleType;
/** 基础配置 */
basicConfig?: {
showWorkTab: boolean;
uniqueOpened: boolean;
showMenuButton: boolean;
showFastEnter: boolean;
showRefreshButton: boolean;
showCrumbs: boolean;
showLanguage: boolean;
showNprogress: boolean;
colorWeak: boolean;
watermarkVisible: boolean;
menuOpenWidth: number;
tabStyle: ThemeTabStyleType;
pageTransition: ThemePageTransitionType;
customRadius: string;
};
/** 时间戳 */
timestamp: number;
}

View File

@@ -101,29 +101,29 @@ export class StyleInitializer {
// 设置默认的导航相关 CSS 变量
const defaultNavVars = {
'--xagi-nav-first-menu-width': '60px',
'--xagi-page-container-margin': '16px',
'--xagi-page-container-border-radius': '12px',
'--xagi-nav-first-menu-width': '88px',
'--xagi-page-container-margin': '0',
'--xagi-page-container-border-radius': '0',
'--xagi-page-container-border-color': 'transparent',
};
// 设置默认的布局相关 CSS 变量
const defaultLayoutVars = {
'--xagi-layout-text-primary': '#000000',
'--xagi-layout-text-secondary': 'rgba(0, 0, 0, 0.65)',
'--xagi-layout-text-tertiary': 'rgba(0, 0, 0, 0.65)',
'--xagi-layout-text-secondary': 'rgba(31, 35, 55, 0.72)',
'--xagi-layout-text-tertiary': 'rgba(31, 35, 55, 0.52)',
'--xagi-layout-text-disabled': 'rgba(0, 0, 0, 0.25)',
'--xagi-layout-second-menu-text-color': 'rgba(0, 0, 0, 0.88)',
'--xagi-layout-second-menu-text-color-secondary': 'rgba(0, 0, 0, 0.65)',
'--xagi-layout-bg-primary': 'rgba(255, 255, 255, 0.95)',
'--xagi-layout-bg-secondary': 'rgba(255, 255, 255, 0.85)',
'--xagi-layout-bg-card': 'rgba(255, 255, 255, 0.65)',
'--xagi-layout-bg-input': 'rgba(255, 255, 255, 0.45)',
'--xagi-layout-border-primary': 'rgba(0, 0, 0, 0.15)',
'--xagi-layout-border-secondary': 'rgba(0, 0, 0, 0.1)',
'--xagi-layout-shadow': 'rgba(0, 0, 0, 0.1)',
'--xagi-layout-overlay': 'rgba(255, 255, 255, 0.7)',
'--xagi-layout-bg-container': 'rgba(255, 255, 255, 0.95)',
'--xagi-layout-second-menu-text-color': '#1f2337',
'--xagi-layout-second-menu-text-color-secondary': '#5d6378',
'--xagi-layout-bg-primary': '#f4f7fb',
'--xagi-layout-bg-secondary': '#f2f5fb',
'--xagi-layout-bg-card': 'rgba(255, 255, 255, 0.92)',
'--xagi-layout-bg-input': 'rgba(255, 255, 255, 0.88)',
'--xagi-layout-border-primary': 'rgba(91, 105, 135, 0.14)',
'--xagi-layout-border-secondary': 'rgba(91, 105, 135, 0.1)',
'--xagi-layout-shadow': '0 8px 20px rgba(32, 45, 84, 0.08)',
'--xagi-layout-overlay': 'rgba(255, 255, 255, 0.78)',
'--xagi-layout-bg-container': '#ffffff',
};
// 设置默认主题色相关 CSS 变量