From f9e090125c7c1a27341105dc7affe6b4caf6b83e Mon Sep 17 00:00:00 2001 From: baiyanyun Date: Thu, 4 Jun 2026 16:48:47 +0800 Subject: [PATCH] feat: improve theme configuration --- qiming/.npmrc | 2 +- qiming/src/app.tsx | 88 +- .../settings/menu_layouts/dual_column.png | Bin 0 -> 514 bytes .../settings/menu_layouts/horizontal.png | Bin 0 -> 409 bytes .../images/settings/menu_layouts/mixed.png | Bin 0 -> 431 bytes .../images/settings/menu_layouts/vertical.png | Bin 0 -> 439 bytes .../images/settings/menu_styles/dark.png | Bin 0 -> 292 bytes .../images/settings/menu_styles/design.png | Bin 0 -> 286 bytes .../images/settings/menu_styles/light.png | Bin 0 -> 293 bytes .../images/settings/theme_styles/dark.png | Bin 0 -> 448 bytes .../images/settings/theme_styles/light.png | Bin 0 -> 416 bytes .../images/settings/theme_styles/system.png | Bin 0 -> 509 bytes qiming/src/components/ButtonToggle/index.less | 52 +- .../src/components/WorkspaceLayout/index.less | 121 ++- .../components/base/MenuListItem/index.less | 54 +- .../components/base/SecondMenuItem/index.less | 71 +- .../components/custom/SelectList/index.less | 50 + qiming/src/constants/layout.constants.ts | 12 +- qiming/src/constants/theme.constants.ts | 299 ++++-- qiming/src/global.less | 277 +++++- qiming/src/hooks/useUnifiedTheme.ts | 42 + .../CollapseButton/index.less | 38 +- .../CollapseButton/index.tsx | 22 +- .../DynamicSecondMenu/index.tsx | 15 +- .../DynamicTabs/TabItem/index.less | 31 +- .../DynamicTabs/TabItem/index.tsx | 6 +- .../DynamicMenusLayout/DynamicTabs/index.less | 9 +- .../DynamicMenusLayout/Header/index.less | 11 +- .../User/UserAction/index.less | 17 +- .../User/UserAvatar/index.less | 15 +- .../DynamicMenusLayout/User/index.less | 47 +- .../UserOperateArea/index.less | 35 +- .../src/layouts/DynamicMenusLayout/index.less | 642 ++++++++++++- .../src/layouts/DynamicMenusLayout/index.tsx | 876 ++++++++++++++++-- .../layouts/Message/MessageItem/index.less | 2 +- .../src/layouts/Setting/ThemeSwitchPanel.less | 4 +- qiming/src/layouts/Setting/index.less | 22 +- qiming/src/layouts/Setting/index.tsx | 5 + qiming/src/layouts/index.less | 241 ++++- qiming/src/layouts/index.tsx | 298 +++++- .../SystemConfig/ThemeConfig/index.less | 765 ++++++++++++++- .../SystemConfig/ThemeConfig/index.tsx | 722 ++++++++++----- qiming/src/services/tenant.ts | 2 +- qiming/src/services/unifiedThemeService.ts | 203 +++- qiming/src/styles/color.less | 2 +- qiming/src/styles/custom.less | 2 +- qiming/src/styles/pro-components.less | 6 +- qiming/src/styles/teamSetting.less | 4 +- qiming/src/styles/token.less | 12 +- qiming/src/types/enums/theme.ts | 50 + qiming/src/types/interfaces/systemManage.ts | 27 + qiming/src/utils/styleInitializer.ts | 32 +- 52 files changed, 4490 insertions(+), 741 deletions(-) create mode 100644 qiming/src/assets/images/settings/menu_layouts/dual_column.png create mode 100644 qiming/src/assets/images/settings/menu_layouts/horizontal.png create mode 100644 qiming/src/assets/images/settings/menu_layouts/mixed.png create mode 100644 qiming/src/assets/images/settings/menu_layouts/vertical.png create mode 100644 qiming/src/assets/images/settings/menu_styles/dark.png create mode 100644 qiming/src/assets/images/settings/menu_styles/design.png create mode 100644 qiming/src/assets/images/settings/menu_styles/light.png create mode 100644 qiming/src/assets/images/settings/theme_styles/dark.png create mode 100644 qiming/src/assets/images/settings/theme_styles/light.png create mode 100644 qiming/src/assets/images/settings/theme_styles/system.png diff --git a/qiming/.npmrc b/qiming/.npmrc index 8f46dd25..7d574b12 100644 --- a/qiming/.npmrc +++ b/qiming/.npmrc @@ -1,2 +1,2 @@ registry=https://registry.npmmirror.com/ - +public-hoist-pattern[]=umi diff --git a/qiming/src/app.tsx b/qiming/src/app.tsx index 687bc40d..4d1d47a9 100644 --- a/qiming/src/app.tsx +++ b/qiming/src/app.tsx @@ -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(); + + 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 && ( +
+ )} + {themeData.basicConfig.watermarkVisible && ( + + )} + + ); +}; + 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 }> = ({ <> {/* 只有用户已登录时才启动事件轮询 */} + {children} ); diff --git a/qiming/src/assets/images/settings/menu_layouts/dual_column.png b/qiming/src/assets/images/settings/menu_layouts/dual_column.png new file mode 100644 index 0000000000000000000000000000000000000000..9b868cac6915d2784493b0792f18870820f17c5e GIT binary patch literal 514 zcmV+d0{#7oP)k(S^u+pLnXS z1G2mrm-5={`kzSGKnEH|cY%?h`=$GsNxIjWq=SKE0Heb|VvNq%I)hPl#sSZ|>mGRS z9pG7K5y!nlg!r4Ybwh;Ev3uuRXWvkD#sISJf`Q$-7w!VYZru?suhL1kbVN%hoph0N z{LS~L!!nGHf4)OkR|mrqhRV7^U1gmW>P&U2BkI^AnyTYiuxbZk;ix#?ROoDvTLN|q(DUvOk=*HBOFHWGF($AMKDj(5FhxM>J?hO|@ zUI)Wk3>$Pr-3FZzb;jvN-@4JaF0ggv5zW?d7a-5NxTfnUo$3m89&fm;<8^_kJHU;f zbUS~XlukP7PJgQ0y$=6kmlKQy-Np$1?FS@Vd6?;N^O8@`>07*qoM6N<$ Ef{6$HX8-^I literal 0 HcmV?d00001 diff --git a/qiming/src/assets/images/settings/menu_layouts/horizontal.png b/qiming/src/assets/images/settings/menu_layouts/horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..ca779bc78ad5bf45e4b26e4e75e6481295383369 GIT binary patch literal 409 zcmV;K0cQS*P) zvbn(=z`lWa1n(axjo^q~;4t&bS@^%KH`vFc7`p2Q0I=EJ`>gf&x{iCB)gIPy+H$?# zI&Rh@j<_p{ds>?h{E0Z?i2HPk&-@4Ch&w}E!NafkSzG;R2LRxg$z)yzam_;Ieh_DF zDlv-FwX;E-%i6gQ+qsC>eh{Sfe@zd^TDtroDA!A5hii^ozT^kui2GyQ`9Fk7vFhZSNmiXsv;oD2I1{Wo zId{pb(`jB}R@*LWXIIUn~YTl0Pqhc#kP}ccEGly z+kj$coC&s_oYRhWw4>dQcKbJ=%iasfw_w`=03i1PMtw+J%=}$J00000NkvXXu0mjf DJf+bZ literal 0 HcmV?d00001 diff --git a/qiming/src/assets/images/settings/menu_layouts/mixed.png b/qiming/src/assets/images/settings/menu_layouts/mixed.png new file mode 100644 index 0000000000000000000000000000000000000000..c82b58038fcbdab260ef4df85cd0bf53fe232ecf GIT binary patch literal 431 zcmV;g0Z{&lP)Yy$(XK&WqF=)bm5 z&;o+?zlB7C)K}fi`)JgA{>Jxlc!KXfgb6|jAx2Z&sveg*c(ShsoP#IVp>AiIo`88@ zr(tz#nx24pU$**en#2z4P8E+W)>TaYs*4?@TZnE@z)$QW={$9Pq+5t?5s4t(vUL8d zk>BD6MzrHvYjymk9q8Cd*CO3GT_5SB%UpMi1j=u9n@Hh+b*4E&dq@{atWMW0ODCOl z4eTM7=ZGc+P_FT6@YfY7)!;zX6{(>H2c9mqfCEieq?Y>qVa~er;Qc|m&m&qoqK^6@ zVBN$3Du2{%NO?+K9&mf!tI@d6TsrBbTaC_bh#7oGv|&;J<@f7bI_ad#=vP1p(n&`P zAWTV(SImLAJYY(!i%iy}>sF+bPPzt{A(q{UW(H7(@yY~TT`{;`%(2Y&+R=+kRYC}n ZKX+7z8kqwQRWJYm002ovPDHLkV1jqY&3ym> literal 0 HcmV?d00001 diff --git a/qiming/src/assets/images/settings/menu_layouts/vertical.png b/qiming/src/assets/images/settings/menu_layouts/vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..16e942b0e9f3919eb199134692e32004544b280c GIT binary patch literal 439 zcmV;o0Z9IdP)(# zD2kaKe_n4u2iGeYpx=`+HyKd(2SbJL9}%}U{@8`CedQuyi89i?#7jp@$(zBsZj{qeqluEicf z*6Rj#SRgdoBZzCSqT|}D=pbE-bUD@21=xhdeC5x5a>aJJ;D*%S|aFn{?|8j z&_So{5lo1xtA1;g<9us`n+K#rx>Kg(1@+SDdjvb0&iU3z)8W<#y8VnK9qURScO3WW h%8Q3N0002Cig literal 0 HcmV?d00001 diff --git a/qiming/src/assets/images/settings/menu_styles/dark.png b/qiming/src/assets/images/settings/menu_styles/dark.png new file mode 100644 index 0000000000000000000000000000000000000000..e1653b7a657edd6e2e83b8b479bb1b4af4620e15 GIT binary patch literal 292 zcmeAS@N?(olHy`uVBq!ia0vp^cYt^j3p0qNwsRej;t%i%asBk^tGc?@&!0c-?cF|q z{-!dsl@Ta)!PCVtB;(%On+JK17;v}*%3nHAmJs>!0B^+Y`oPQsN=8b26YP2SNGmV+ z(<1Wuz(Ft&-I2P^pJ{r#^(-^z!+)>dS^035-TET1?9HMwt^CXP@rda`@oSCvV)pRD9`Ic+V>RN|cZ9-Q}41PonSK>R4CZA96Tw@1diy fo(~{Er;M1%fByXW@#Dw4 zckfEI`&$F0PIK_}Tq&4}k`{J&n3>JYDATM6ZXdH1D1|rwka( a+00#Z%j6q-R9HLER}7x6elF{r5}E*&a(=A< literal 0 HcmV?d00001 diff --git a/qiming/src/assets/images/settings/menu_styles/light.png b/qiming/src/assets/images/settings/menu_styles/light.png new file mode 100644 index 0000000000000000000000000000000000000000..3007b99ea227f5f203ad309d28358d95aa40aad2 GIT binary patch literal 293 zcmeAS@N?(olHy`uVBq!ia0vp^cYt^j3p0qNwsRej5(@AMasBk^>;M1%fByXW@#Dw4 zckhOVhNk`~V+6{c^K@|x$+-9SW+HE^0gvlLR`&CZGg~7o#6IM+GMaerh)K|XF7e~v z*2wU>xm~svV6cO!eAexM#~%n4hWb7dn;9*-?$zE?M-Ku)rRC!eF^lI+S&sX@aO&C)xGbBzUl*1H|~cH`T*7S|L8+xoqd^+pNbuX$T2rdEOtd33n=<}rH(r4Bw?X0 z19j9Zq$jx5yQq&=Rv*u@&uV zZv5wD2VJi=A#|57GsIK59E1=t&(Kn59-)r9I&_O8$G{R>i|A%Y2O}3yS41Z&E$H;i qY~iWsL1-c78CvShBh*n>SoaHFuY$So!<9_{0000$3b*O46wRy83EweL3E-p zTDPTC^?=rG?@;xC)#cYaJXSqGb<2^89-z9fBYo%rs(T&jp$Dk$Kk9+HS#|cPcrJon zbI(i6T+zM{&>wNHId#;{s7qL=Yd{@!)cq1mSEY~blI|(J>{oO}dReaMR_P^Q(MdlO z>i~~A@u&cbFmcTrMB15WsH1KUUF~df!d~+osw?&ewtya>4AqH=XPy0+o=3$lg6Eny zh_o}$P)FSyx+-_rpi9hVN3Lj_1$lsMGW5LKxX|5xOc$eay9h37-XPM>JVPCIbLeVE zkAX{UAEGNAI~aL@@(`VV>$4IoA-;gkJ&1+Cgm8pmlOdPn{^_RrAUFTa z?Nwrlv;qeX95`^`z<~n?4ji~TxhJFJb(>akZP2?200000NkvXXu0mjfdG+rO literal 0 HcmV?d00001 diff --git a/qiming/src/components/ButtonToggle/index.less b/qiming/src/components/ButtonToggle/index.less index 0fd8ea5a..4ff8ebd5 100644 --- a/qiming/src/components/ButtonToggle/index.less +++ b/qiming/src/components/ButtonToggle/index.less @@ -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; } } diff --git a/qiming/src/components/WorkspaceLayout/index.less b/qiming/src/components/WorkspaceLayout/index.less index 3afc4b7c..b6a76943 100644 --- a/qiming/src/components/WorkspaceLayout/index.less +++ b/qiming/src/components/WorkspaceLayout/index.less @@ -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); + } } } diff --git a/qiming/src/components/base/MenuListItem/index.less b/qiming/src/components/base/MenuListItem/index.less index e0fba4a9..5f6636a4 100644 --- a/qiming/src/components/base/MenuListItem/index.less +++ b/qiming/src/components/base/MenuListItem/index.less @@ -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; diff --git a/qiming/src/components/base/SecondMenuItem/index.less b/qiming/src/components/base/SecondMenuItem/index.less index 1c8bf433..348e5556 100644 --- a/qiming/src/components/base/SecondMenuItem/index.less +++ b/qiming/src/components/base/SecondMenuItem/index.less @@ -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; diff --git a/qiming/src/components/custom/SelectList/index.less b/qiming/src/components/custom/SelectList/index.less index 00b95dea..d4459f7a 100644 --- a/qiming/src/components/custom/SelectList/index.less +++ b/qiming/src/components/custom/SelectList/index.less @@ -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; + } } diff --git a/qiming/src/constants/layout.constants.ts b/qiming/src/constants/layout.constants.ts index 64bc9fb0..1650f630 100644 --- a/qiming/src/constants/layout.constants.ts +++ b/qiming/src/constants/layout.constants.ts @@ -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 的间距边距 diff --git a/qiming/src/constants/theme.constants.ts b/qiming/src/constants/theme.constants.ts index 147884af..691e48da 100644 --- a/qiming/src/constants/theme.constants.ts +++ b/qiming/src/constants/theme.constants.ts @@ -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 = { '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 = { '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 = { '--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 = { '--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 = { // 品牌主色 - 项目主色调 - colorPrimary: '#5147ff', + colorPrimary: '#5D87FF', // 功能色 colorSuccess: '#3bb346', @@ -281,7 +304,7 @@ export const themeTokens: Partial = { colorBgBase: '#ffffff', // 超链接颜色 - colorLink: '#5147ff', + colorLink: '#5D87FF', // 字体配置 fontFamily: @@ -332,12 +355,12 @@ export const themeTokens: Partial = { 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 = { 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; diff --git a/qiming/src/global.less b/qiming/src/global.less index c396211e..4be37c5a 100644 --- a/qiming/src/global.less +++ b/qiming/src/global.less @@ -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; } } diff --git a/qiming/src/hooks/useUnifiedTheme.ts b/qiming/src/hooks/useUnifiedTheme.ts index 146e4516..085ac43c 100644 --- a/qiming/src/hooks/useUnifiedTheme.ts +++ b/qiming/src/hooks/useUnifiedTheme.ts @@ -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; + updateMenuLayout: ( + layout: ThemeMenuLayoutType, + options?: UpdateOptions, + ) => Promise; + updateMenuStyle: ( + style: ThemeMenuStyleType, + options?: UpdateOptions, + ) => Promise; updateLayoutStyle: ( style: ThemeLayoutColorStyle, options?: UpdateOptions, @@ -57,6 +68,10 @@ interface UseUnifiedThemeReturn { language: 'zh-CN' | 'en-US', options?: UpdateOptions, ) => Promise; + updateBasicConfig: ( + config: Partial, + options?: UpdateOptions, + ) => Promise; // 便捷方法 toggleAntdTheme: () => Promise; @@ -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, + 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, diff --git a/qiming/src/layouts/DynamicMenusLayout/CollapseButton/index.less b/qiming/src/layouts/DynamicMenusLayout/CollapseButton/index.less index 9056aaac..71740fa2 100644 --- a/qiming/src/layouts/DynamicMenusLayout/CollapseButton/index.less +++ b/qiming/src/layouts/DynamicMenusLayout/CollapseButton/index.less @@ -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; + } } // 移动端隐藏收起按钮 diff --git a/qiming/src/layouts/DynamicMenusLayout/CollapseButton/index.tsx b/qiming/src/layouts/DynamicMenusLayout/CollapseButton/index.tsx index a4a187e8..c325c1e4 100644 --- a/qiming/src/layouts/DynamicMenusLayout/CollapseButton/index.tsx +++ b/qiming/src/layouts/DynamicMenusLayout/CollapseButton/index.tsx @@ -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, }} > = ({ onToggle={() => toggleExpand(menuCode)} /> {/* 递归渲染子菜单 */} - {isExpanded && - menu.children?.map((child) => renderMenuItem(child, level + 1))} +
+ {menu.children?.map((child) => renderMenuItem(child, level + 1))} +
); }, diff --git a/qiming/src/layouts/DynamicMenusLayout/DynamicTabs/TabItem/index.less b/qiming/src/layouts/DynamicMenusLayout/DynamicTabs/TabItem/index.less index 5d5957a8..7396c2db 100644 --- a/qiming/src/layouts/DynamicMenusLayout/DynamicTabs/TabItem/index.less +++ b/qiming/src/layouts/DynamicMenusLayout/DynamicTabs/TabItem/index.less @@ -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; diff --git a/qiming/src/layouts/DynamicMenusLayout/DynamicTabs/TabItem/index.tsx b/qiming/src/layouts/DynamicMenusLayout/DynamicTabs/TabItem/index.tsx index 3e023189..f17fe3df 100644 --- a/qiming/src/layouts/DynamicMenusLayout/DynamicTabs/TabItem/index.tsx +++ b/qiming/src/layouts/DynamicMenusLayout/DynamicTabs/TabItem/index.tsx @@ -35,12 +35,12 @@ const TabItem: React.FC = ({ const navStyle: React.CSSProperties = useMemo(() => { return isStyle2 ? { - width: '64px', + width: '88px', height: '64px', } : { - width: '40px', - height: '40px', + width: '58px', + height: '58px', padding: 0, }; }, [isStyle2]); diff --git a/qiming/src/layouts/DynamicMenusLayout/DynamicTabs/index.less b/qiming/src/layouts/DynamicMenusLayout/DynamicTabs/index.less index 68146717..b5b2fb20 100644 --- a/qiming/src/layouts/DynamicMenusLayout/DynamicTabs/index.less +++ b/qiming/src/layouts/DynamicMenusLayout/DynamicTabs/index.less @@ -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; } diff --git a/qiming/src/layouts/DynamicMenusLayout/Header/index.less b/qiming/src/layouts/DynamicMenusLayout/Header/index.less index 84b3a8b7..195dc2e8 100644 --- a/qiming/src/layouts/DynamicMenusLayout/Header/index.less +++ b/qiming/src/layouts/DynamicMenusLayout/Header/index.less @@ -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; } diff --git a/qiming/src/layouts/DynamicMenusLayout/User/UserAction/index.less b/qiming/src/layouts/DynamicMenusLayout/User/UserAction/index.less index c9a4acda..fd875ae7 100644 --- a/qiming/src/layouts/DynamicMenusLayout/User/UserAction/index.less +++ b/qiming/src/layouts/DynamicMenusLayout/User/UserAction/index.less @@ -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; } } diff --git a/qiming/src/layouts/DynamicMenusLayout/User/UserAvatar/index.less b/qiming/src/layouts/DynamicMenusLayout/User/UserAvatar/index.less index c47ca8a5..9e8be405 100644 --- a/qiming/src/layouts/DynamicMenusLayout/User/UserAvatar/index.less +++ b/qiming/src/layouts/DynamicMenusLayout/User/UserAvatar/index.less @@ -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%; diff --git a/qiming/src/layouts/DynamicMenusLayout/User/index.less b/qiming/src/layouts/DynamicMenusLayout/User/index.less index 379c6deb..274d7096 100644 --- a/qiming/src/layouts/DynamicMenusLayout/User/index.less +++ b/qiming/src/layouts/DynamicMenusLayout/User/index.less @@ -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); } } diff --git a/qiming/src/layouts/DynamicMenusLayout/UserOperateArea/index.less b/qiming/src/layouts/DynamicMenusLayout/UserOperateArea/index.less index 3c56bd2f..e3a20486 100644 --- a/qiming/src/layouts/DynamicMenusLayout/UserOperateArea/index.less +++ b/qiming/src/layouts/DynamicMenusLayout/UserOperateArea/index.less @@ -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; } } diff --git a/qiming/src/layouts/DynamicMenusLayout/index.less b/qiming/src/layouts/DynamicMenusLayout/index.less index 8c6f4f94..df966234 100644 --- a/qiming/src/layouts/DynamicMenusLayout/index.less +++ b/qiming/src/layouts/DynamicMenusLayout/index.less @@ -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 { diff --git a/qiming/src/layouts/DynamicMenusLayout/index.tsx b/qiming/src/layouts/DynamicMenusLayout/index.tsx index bcf9c913..cb97d1e5 100644 --- a/qiming/src/layouts/DynamicMenusLayout/index.tsx +++ b/qiming/src/layouts/DynamicMenusLayout/index.tsx @@ -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 = ({ 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 = ({ // 是否点击了新对话菜单,特殊处理,用于显示title时使用 const [isClickNewConversation, setIsClickNewConversation] = useState(false); + const [verticalExpandedMenus, setVerticalExpandedMenus] = useState( + [], + ); // 创建智能体会话 const { handleCreateConversation } = useConversation(); const { tenantConfigInfo } = useModel('tenantConfigInfo'); + const effectiveMenuLayout = isMobile ? ThemeMenuLayoutType.DUAL : menuLayout; + const isEnableSubscription = tenantConfigInfo?.enableSubscription !== 0; // 是否点击菜单 const isClickMenu = useRef(false); @@ -401,7 +427,7 @@ const DynamicMenusLayout: React.FC = ({ runEdit({ size: 5, }); - }, []); + }, [runEdit]); /** * 递归查找第一个有 path 的子菜单 @@ -428,6 +454,246 @@ const DynamicMenusLayout: React.FC = ({ [], ); + 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; + 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(); + + 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 = ({ 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 = ({ 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 = ({ 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; + + 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 = ({ '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 = ({ return ; }, [activeTab, overrideContainerStyle]); - return ( -
- {/* 一级导航菜单栏 */} -
-
- {/* 动态一级菜单 */} - { + if ( + menu.icon && + (menu.icon.includes('.png') || + menu.icon.includes('.jpg') || + menu.icon.includes('.jpeg')) + ) { + return ; + } + + return ; + }; + + const renderBrandLogo = (compact = false) => ( + <> + {tenantConfigInfo?.siteLogo ? ( + - {/* 用户操作区域 */} - - {/* 用户头像 */} - + ) : null} + {!compact ? ( + + {tenantConfigInfo?.siteName || 'Art Design Pro'} + + ) : 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 ( +
+ + {hasChildren ? ( +
+ {children.map((child) => + renderTopDropdownItem(child, level + 1, parentCode), + )} +
+ ) : null} +
+ ); + }; + + const renderTopMenuItem = (menu: MenuItemDto) => { + const children = getVerticalMenuChildren(menu).filter( + isVerticalMenuVisible, + ); + const hasChildren = children.length > 0; + const content = ( + + ); + + if (!hasChildren) { + return content; + } + + return ( + + {children.map((child) => + renderTopDropdownItem(child, 0, menu.code || ''), + )} +
+ } + > + {content} + + ); + }; + + const renderTopNavigation = () => ( +
+
{renderBrandLogo()}
+
+ {firstLevelMenus.map((menu) => renderTopMenuItem(menu))} +
+
+ {basicConfig.showRefreshButton && ( + + )} + {basicConfig.showFastEnter && + otherMenus.map((menu) => ( + + ))} + +
+
+ ); + + const renderSecondMenuPanel = () => ( +
+
+ +
+ +
+ + {currentTitle} + +
+
+ + {renderSecondMenu} +
+
- {/* 二级导航菜单栏 */} -
-
- + +
+
+ ); + + const renderIconRail = () => ( +
+
+ + {basicConfig.showFastEnter && ( + + )} + {basicConfig.showRefreshButton && ( +
+ ); + + 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 ( +
+ + {hasChildren ? ( +
-
- {/* 标题 */} - -
- - {currentTitle} - -
-
- - {/* 二级/三级菜单 */} - {renderSecondMenu} -
- -
- - {/* 积分相关入口:放到二级导航栏底部固定展示 */} -
- -
+ {children.map((child) => + renderVerticalMenuItem( + child, + level + 1, + topCode, + nextAncestorCodes, + ), + )} +
+ ) : null}
+ ); + }; - {/* 收起/展开按钮 */} - + const renderVerticalActionButton = (menu: MenuItemDto) => { + const content = ( + + ); + + if (menu.code === 'notification' && unreadCount > 0) { + return ( + + {content} + + ); + } + + return content; + }; + + const renderVerticalMenu = () => ( + + ); + + if (region === 'top') { + return renderTopNavigation(); + } + + if (region === 'side' && effectiveMenuLayout === ThemeMenuLayoutType.MIXED) { + return ( +
+ {renderSecondMenuPanel()} +
+ ); + } + + if (effectiveMenuLayout === ThemeMenuLayoutType.HORIZONTAL) { + return renderTopNavigation(); + } + + if (effectiveMenuLayout === ThemeMenuLayoutType.VERTICAL) { + return
{renderVerticalMenu()}
; + } + + return ( +
+ {renderIconRail()} + {renderSecondMenuPanel()} + {basicConfig.showMenuButton && }
); }; diff --git a/qiming/src/layouts/Message/MessageItem/index.less b/qiming/src/layouts/Message/MessageItem/index.less index 1becd7ae..d7f098ec 100644 --- a/qiming/src/layouts/Message/MessageItem/index.less +++ b/qiming/src/layouts/Message/MessageItem/index.less @@ -44,7 +44,7 @@ color: @WHITE; background: @BLANK26; padding: 10px; - border-radius: 10px; + border-radius: 8px; overflow-x: auto; margin: 0 0 4px; diff --git a/qiming/src/layouts/Setting/ThemeSwitchPanel.less b/qiming/src/layouts/Setting/ThemeSwitchPanel.less index 41ad70a1..2baaa0cd 100644 --- a/qiming/src/layouts/Setting/ThemeSwitchPanel.less +++ b/qiming/src/layouts/Setting/ThemeSwitchPanel.less @@ -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; } diff --git a/qiming/src/layouts/Setting/index.less b/qiming/src/layouts/Setting/index.less index 953c1536..f044a76d 100644 --- a/qiming/src/layouts/Setting/index.less +++ b/qiming/src/layouts/Setting/index.less @@ -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; diff --git a/qiming/src/layouts/Setting/index.tsx b/qiming/src/layouts/Setting/index.tsx index a1d3918d..0771b16c 100644 --- a/qiming/src/layouts/Setting/index.tsx +++ b/qiming/src/layouts/Setting/index.tsx @@ -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( @@ -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) => (
  • (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(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([]); // 移除对 @@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 | React.KeyboardEvent, + 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 (
    - {/* 侧边菜单栏及弹窗区域 */} -
    - {/* 菜单栏 */} + {hasTopMenu && ( + )} - {/* 悬浮菜单 */} - +
    + {/* 侧边菜单栏及弹窗区域 */} + {hasSideMenu && ( +
    + {/* 菜单栏 */} + - {/* 消息弹窗 */} - + {/* 悬浮菜单 */} + {menuLayout !== 'vertical' && } - {/* 设置弹窗 */} - + {/* 消息弹窗 */} + - {/* 移动端菜单按钮和遮罩层 */} - {isMobile && ( - + {/* 设置弹窗 */} + + + {/* 移动端菜单按钮和遮罩层 */} + {isMobile && ( + + )} +
    )} -
    - {/* 主内容区 */} -
    - + {!hasSideMenu && ( + <> + + + + )} + + {/* 主内容区 */} +
    + {basicConfig.showWorkTab && workTabs.length > 0 && ( +
    +
    + {workTabs.map((tab) => ( + + ))} +
    +
    + )} + + {basicConfig.showCrumbs && breadcrumbItems.length > 0 && ( +
    + ({ title: item.title }))} + /> +
    + )} + +
    + +
    +
    ); diff --git a/qiming/src/pages/SystemManagement/SystemConfig/ThemeConfig/index.less b/qiming/src/pages/SystemManagement/SystemConfig/ThemeConfig/index.less index a56866f1..5b895813 100644 --- a/qiming/src/pages/SystemManagement/SystemConfig/ThemeConfig/index.less +++ b/qiming/src/pages/SystemManagement/SystemConfig/ThemeConfig/index.less @@ -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; + } + } +} diff --git a/qiming/src/pages/SystemManagement/SystemConfig/ThemeConfig/index.tsx b/qiming/src/pages/SystemManagement/SystemConfig/ThemeConfig/index.tsx index 69363997..3207e6ea 100644 --- a/qiming/src/pages/SystemManagement/SystemConfig/ThemeConfig/index.tsx +++ b/qiming/src/pages/SystemManagement/SystemConfig/ThemeConfig/index.tsx @@ -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(menuLayout); + const [previewMenuStyle, setPreviewMenuStyle] = + useState(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( + 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[0], + ) => { + await unifiedThemeService.updateData(updates, { saveToStorage: false }); + }; + + const handleBasicConfigChange = async ( + updates: Partial, + ) => { + 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 ( - + <> +
    +