feat: improve theme configuration
@@ -1,2 +1,2 @@
|
||||
registry=https://registry.npmmirror.com/
|
||||
|
||||
public-hoist-pattern[]=umi
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
|
||||
BIN
qiming/src/assets/images/settings/menu_layouts/dual_column.png
Normal file
|
After Width: | Height: | Size: 514 B |
BIN
qiming/src/assets/images/settings/menu_layouts/horizontal.png
Normal file
|
After Width: | Height: | Size: 409 B |
BIN
qiming/src/assets/images/settings/menu_layouts/mixed.png
Normal file
|
After Width: | Height: | Size: 431 B |
BIN
qiming/src/assets/images/settings/menu_layouts/vertical.png
Normal file
|
After Width: | Height: | Size: 439 B |
BIN
qiming/src/assets/images/settings/menu_styles/dark.png
Normal file
|
After Width: | Height: | Size: 292 B |
BIN
qiming/src/assets/images/settings/menu_styles/design.png
Normal file
|
After Width: | Height: | Size: 286 B |
BIN
qiming/src/assets/images/settings/menu_styles/light.png
Normal file
|
After Width: | Height: | Size: 293 B |
BIN
qiming/src/assets/images/settings/theme_styles/dark.png
Normal file
|
After Width: | Height: | Size: 448 B |
BIN
qiming/src/assets/images/settings/theme_styles/light.png
Normal file
|
After Width: | Height: | Size: 416 B |
BIN
qiming/src/assets/images/settings/theme_styles/system.png
Normal file
|
After Width: | Height: | Size: 509 B |
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 的间距边距
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端隐藏收起按钮
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
color: @WHITE;
|
||||
background: @BLANK26;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 0 0 4px;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -210,7 +210,7 @@ each(range(2, 3, 1), {
|
||||
}
|
||||
|
||||
.radius-10 {
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.font-12 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// ---------- 固定列背景(右侧“操作”列) ----------
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
/** 导航风格ID(style1/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;
|
||||
}
|
||||
|
||||
@@ -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 变量
|
||||
|
||||