Feat(Theme): Support switch theme mode

This commit is contained in:
2024-10-28 17:54:56 +08:00
parent e97b932cf5
commit a2dcf45f84
13 changed files with 230 additions and 142 deletions

View File

@@ -35,18 +35,17 @@ interface IMatcher {
const matchComponents: IMatcher[] = [ const matchComponents: IMatcher[] = [
{ {
pattern: /^Avatar/, pattern: /^Anchor/,
styleDir: 'avatar' styleDir: 'anchor'
}, },
{ {
pattern: /^AutoComplete/, pattern: /^AutoComplete/,
styleDir: 'auto-complete' styleDir: 'auto-complete'
}, },
{ {
pattern: /^Anchor/, pattern: /^Avatar/,
styleDir: 'anchor' styleDir: 'avatar'
}, },
{ {
pattern: /^Badge/, pattern: /^Badge/,
styleDir: 'badge' styleDir: 'badge'
@@ -59,14 +58,18 @@ const matchComponents: IMatcher[] = [
pattern: /^Button/, pattern: /^Button/,
styleDir: 'button' styleDir: 'button'
}, },
{
pattern: /^Checkbox/,
styleDir: 'checkbox'
},
{ {
pattern: /^Card/, pattern: /^Card/,
styleDir: 'card' styleDir: 'card'
}, },
{
pattern: /^CheckableTag/,
styleDir: 'tag'
},
{
pattern: /^Checkbox/,
styleDir: 'checkbox'
},
{ {
pattern: /^Collapse/, pattern: /^Collapse/,
styleDir: 'collapse' styleDir: 'collapse'
@@ -75,76 +78,30 @@ const matchComponents: IMatcher[] = [
pattern: /^Descriptions/, pattern: /^Descriptions/,
styleDir: 'descriptions' styleDir: 'descriptions'
}, },
{
pattern: /^RangePicker|^WeekPicker|^MonthPicker/,
styleDir: 'date-picker'
},
{ {
pattern: /^Dropdown/, pattern: /^Dropdown/,
styleDir: 'dropdown' styleDir: 'dropdown'
}, },
{ {
pattern: /^Form/, pattern: /^Form/,
styleDir: 'form' styleDir: 'form'
}, },
{
pattern: /^Image/,
styleDir: 'image'
},
{ {
pattern: /^InputNumber/, pattern: /^InputNumber/,
styleDir: 'input-number' styleDir: 'input-number'
}, },
{
pattern: /^Input|^Textarea/,
styleDir: 'input'
},
{
pattern: /^Statistic/,
styleDir: 'statistic'
},
{
pattern: /^CheckableTag/,
styleDir: 'tag'
},
{
pattern: /^TimeRangePicker/,
styleDir: 'time-picker'
},
{ {
pattern: /^Layout/, pattern: /^Layout/,
styleDir: 'layout' styleDir: 'layout'
}, },
{
pattern: /^Menu|^SubMenu/,
styleDir: 'menu'
},
{
pattern: /^Table/,
styleDir: 'table'
},
{
pattern: /^TimePicker|^TimeRangePicker/,
styleDir: 'time-picker'
},
{
pattern: /^Radio/,
styleDir: 'radio'
},
{
pattern: /^Image/,
styleDir: 'image'
},
{ {
pattern: /^List/, pattern: /^List/,
styleDir: 'list' styleDir: 'list'
}, },
{
pattern: /^Tab/,
styleDir: 'tabs'
},
{ {
pattern: /^Mentions/, pattern: /^Mentions/,
styleDir: 'mentions' styleDir: 'mentions'
@@ -154,37 +111,72 @@ const matchComponents: IMatcher[] = [
styleDir: 'qr-code' styleDir: 'qr-code'
}, },
{ {
pattern: /^Step/, pattern: /^Radio/,
styleDir: 'steps' styleDir: 'radio'
}, },
{
pattern: /^Skeleton/,
styleDir: 'skeleton'
},
{ {
pattern: /^Select/, pattern: /^Select/,
styleDir: 'select' styleDir: 'select'
}, },
{ {
pattern: /^TreeSelect/, pattern: /^Skeleton/,
styleDir: 'tree-select' styleDir: 'skeleton'
}, },
{ {
pattern: /^Tree|^DirectoryTree/, pattern: /^Statistic/,
styleDir: 'tree' styleDir: 'statistic'
}, },
{ {
pattern: /^Typography/, pattern: /^Step/,
styleDir: 'typography' styleDir: 'steps'
},
{
pattern: /^Tab/,
styleDir: 'tabs'
},
{
pattern: /^Table/,
styleDir: 'table'
}, },
{ {
pattern: /^Timeline/, pattern: /^Timeline/,
styleDir: 'timeline' styleDir: 'timeline'
}, },
{
pattern: /^TimeRangePicker/,
styleDir: 'time-picker'
},
{
pattern: /^Typography/,
styleDir: 'typography'
},
{
pattern: /^TreeSelect/,
styleDir: 'tree-select'
},
{ {
pattern: /^Upload/, pattern: /^Upload/,
styleDir: 'upload' styleDir: 'upload'
},
{
pattern: /^Input|^Textarea/,
styleDir: 'input'
},
{
pattern: /^Menu|^SubMenu/,
styleDir: 'menu'
},
{
pattern: /^Tree|^DirectoryTree/,
styleDir: 'tree'
},
{
pattern: /^MonthPicker|^RangePicker|^WeekPicker/,
styleDir: 'date-picker'
},
{
pattern: /^TimePicker|^TimeRangePicker/,
styleDir: 'time-picker'
} }
] ]
@@ -257,12 +249,12 @@ const getSideEffects = (compName: string, options: AntDesignResolverOptions): Si
const primitiveNames = [ const primitiveNames = [
'Affix', 'Affix',
'Alert',
'Anchor', 'Anchor',
'AnchorLink', 'AnchorLink',
'AutoComplete', 'AutoComplete',
'AutoCompleteOptGroup', 'AutoCompleteOptGroup',
'AutoCompleteOption', 'AutoCompleteOption',
'Alert',
'Avatar', 'Avatar',
'AvatarGroup', 'AvatarGroup',
'BackTop', 'BackTop',
@@ -277,105 +269,106 @@ const primitiveNames = [
'Card', 'Card',
'CardGrid', 'CardGrid',
'CardMeta', 'CardMeta',
'Collapse',
'CollapsePanel',
'Carousel', 'Carousel',
'Cascader', 'Cascader',
'CheckableTag',
'Checkbox', 'Checkbox',
'CheckboxGroup', 'CheckboxGroup',
'Col', 'Col',
'Collapse',
'CollapsePanel',
'Comment', 'Comment',
'ConfigProvider', 'ConfigProvider',
'DatePicker', 'DatePicker',
'MonthPicker',
'WeekPicker',
'RangePicker',
'QuarterPicker',
'Descriptions', 'Descriptions',
'DescriptionsItem', 'DescriptionsItem',
'DirectoryTree',
'Divider', 'Divider',
'Drawer',
'Dropdown', 'Dropdown',
'DropdownButton', 'DropdownButton',
'Drawer',
'Empty', 'Empty',
'FloatButton', 'FloatButton',
'Form', 'Form',
'FormItem', 'FormItem',
'FormItemRest', 'FormItemRest',
'Grid', 'Grid',
'Input',
'InputGroup',
'InputPassword',
'InputSearch',
'Textarea',
'Image', 'Image',
'ImagePreviewGroup', 'ImagePreviewGroup',
'Input',
'InputGroup',
'InputNumber', 'InputNumber',
'InputPassword',
'InputSearch',
'Layout', 'Layout',
'LayoutContent',
'LayoutFooter',
'LayoutHeader', 'LayoutHeader',
'LayoutSider', 'LayoutSider',
'LayoutFooter',
'LayoutContent',
'List', 'List',
'ListItem', 'ListItem',
'ListItemMeta', 'ListItemMeta',
'LocaleProvider',
'Mentions',
'MentionsOption',
'Menu', 'Menu',
'MenuDivider', 'MenuDivider',
'MenuItem', 'MenuItem',
'MenuItemGroup', 'MenuItemGroup',
'SubMenu',
'Mentions',
'MentionsOption',
'Modal', 'Modal',
'Statistic', 'MonthPicker',
'StatisticCountdown',
'PageHeader', 'PageHeader',
'Pagination', 'Pagination',
'Popconfirm', 'Popconfirm',
'Popover', 'Popover',
'Progress', 'Progress',
'QRCode',
'QuarterPicker',
'Radio', 'Radio',
'RadioButton', 'RadioButton',
'RadioGroup', 'RadioGroup',
'RangePicker',
'Rate', 'Rate',
'Result', 'Result',
'Row', 'Row',
'QRCode', 'Segmented',
'Select', 'Select',
'SelectOptGroup', 'SelectOptGroup',
'SelectOption', 'SelectOption',
'Skeleton', 'Skeleton',
'SkeletonButton',
'SkeletonAvatar', 'SkeletonAvatar',
'SkeletonInput', 'SkeletonButton',
'SkeletonImage', 'SkeletonImage',
'SkeletonInput',
'Slider', 'Slider',
'Space', 'Space',
'Spin', 'Spin',
'Steps', 'Statistic',
'StatisticCountdown',
'Step', 'Step',
'Steps',
'SubMenu',
'Switch', 'Switch',
'Table', 'Table',
'TableColumn', 'TableColumn',
'TableColumnGroup', 'TableColumnGroup',
'TableSummary', 'TableSummary',
'TableSummaryRow',
'TableSummaryCell', 'TableSummaryCell',
'TableSummaryRow',
'TabPane',
'Tabs',
'Tag',
'Textarea',
'Timeline',
'TimelineItem',
'TimePicker',
'TimeRangePicker',
'Tooltip',
'Transfer', 'Transfer',
'Tree', 'Tree',
'TreeNode', 'TreeNode',
'DirectoryTree',
'TreeSelect', 'TreeSelect',
'TreeSelectNode', 'TreeSelectNode',
'Tabs',
'TabPane',
'Tag',
'CheckableTag',
'TimePicker',
'TimeRangePicker',
'Timeline',
'TimelineItem',
'Tooltip',
'Typography', 'Typography',
'TypographyLink', 'TypographyLink',
'TypographyParagraph', 'TypographyParagraph',
@@ -383,7 +376,7 @@ const primitiveNames = [
'TypographyTitle', 'TypographyTitle',
'Upload', 'Upload',
'UploadDragger', 'UploadDragger',
'LocaleProvider' 'WeekPicker'
] ]
const prefix = 'Antd' const prefix = 'Antd'

View File

@@ -2,9 +2,14 @@ import { theme } from 'antd'
import zh_CN from 'antd/locale/zh_CN' import zh_CN from 'antd/locale/zh_CN'
import BaseStyles from '@/assets/css/base.style' import BaseStyles from '@/assets/css/base.style'
import CommonStyles from '@/assets/css/common.style' import CommonStyles from '@/assets/css/common.style'
import { COLOR_PRODUCTION } from '@/constants/common.constants' import {
COLOR_PRODUCTION,
THEME_DARK,
THEME_FOLLOW_SYSTEM,
THEME_LIGHT
} from '@/constants/common.constants'
import { getRouter } from '@/router' import { getRouter } from '@/router'
import { init } from '@/util/common' import { getThemeMode, init } from '@/util/common'
import FullscreenLoadingMask from '@/components/common/FullscreenLoadingMask' import FullscreenLoadingMask from '@/components/common/FullscreenLoadingMask'
export const AppContext = createContext({ export const AppContext = createContext({
@@ -17,19 +22,38 @@ const App = () => {
const [notificationInstance, notificationHolder] = notification.useNotification() const [notificationInstance, notificationHolder] = notification.useNotification()
const [modalInstance, modalHolder] = AntdModal.useModal() const [modalInstance, modalHolder] = AntdModal.useModal()
const [routerState, setRouterState] = useState(getRouter) const [routerState, setRouterState] = useState(getRouter)
const [isDarkMode, setIsDarkMode] = useState(false) const [themeMode, setThemeMode] = useState(getThemeMode())
const [isSystemDarkMode, setIsSystemDarkMode] = useState(false)
const getIsDark = () => {
switch (themeMode) {
case THEME_FOLLOW_SYSTEM:
return isSystemDarkMode
case THEME_LIGHT:
return false
case THEME_DARK:
return true
}
}
useEffect(() => { useEffect(() => {
init(messageInstance, notificationInstance, modalInstance) init(messageInstance, notificationInstance, modalInstance)
const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)') const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)')
setIsDarkMode(darkThemeMq.matches) setIsSystemDarkMode(darkThemeMq.matches)
const listener = (ev: MediaQueryListEvent) => { const darkThemeMqChangeListener = (ev: MediaQueryListEvent) => {
setIsDarkMode(ev.matches) setIsSystemDarkMode(ev.matches)
} }
darkThemeMq.addEventListener('change', listener) darkThemeMq.addEventListener('change', darkThemeMqChangeListener)
const themeModeChangeListener = () => {
setThemeMode(getThemeMode())
}
window.addEventListener('localStorageChange', themeModeChangeListener)
return () => { return () => {
darkThemeMq.removeEventListener('change', listener) darkThemeMq.removeEventListener('change', darkThemeMqChangeListener)
window.removeEventListener('localStorageChange', themeModeChangeListener)
} }
}, []) }, [])
@@ -37,7 +61,7 @@ const App = () => {
<AntdConfigProvider <AntdConfigProvider
theme={{ theme={{
cssVar: true, cssVar: true,
algorithm: isDarkMode ? theme.darkAlgorithm : undefined, algorithm: getIsDark() ? theme.darkAlgorithm : theme.defaultAlgorithm,
token: { token: {
colorPrimary: COLOR_PRODUCTION, colorPrimary: COLOR_PRODUCTION,
colorLinkHover: COLOR_PRODUCTION colorLinkHover: COLOR_PRODUCTION
@@ -57,7 +81,7 @@ const App = () => {
refreshRouter: () => { refreshRouter: () => {
setRouterState(getRouter()) setRouterState(getRouter())
}, },
isDarkMode isDarkMode: getIsDark()
}} }}
> >
<Suspense fallback={<FullscreenLoadingMask />}> <Suspense fallback={<FullscreenLoadingMask />}>

View File

@@ -11,17 +11,6 @@ const slideIn = keyframes`
} }
` `
const slideOut = keyframes`
0% {
transform: translateX(0);
opacity: 1;
}
100% {
transform: translateX(-10px);
opacity: 0;
}
`
export default createStyles(({ cx, css, token }) => { export default createStyles(({ cx, css, token }) => {
const collapsedExit = cx(css` const collapsedExit = cx(css`
opacity: 0; opacity: 0;
@@ -29,7 +18,6 @@ export default createStyles(({ cx, css, token }) => {
padding-left: ${token.paddingXS}px; padding-left: ${token.paddingXS}px;
left: 100%; left: 100%;
z-index: 1000; z-index: 1000;
animation: ${slideOut} 0.1s ease;
transform: translateX(-100%); transform: translateX(-100%);
`) `)

View File

@@ -63,6 +63,7 @@ export default createStyles(({ token }) => ({
header: { header: {
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center',
'> *': { '> *': {
flex: '0 0 auto' flex: '0 0 auto'
@@ -70,7 +71,7 @@ export default createStyles(({ token }) => ({
}, },
title: { title: {
fontSize: token.fontSizeXL, fontSize: token.fontSizeHeading3,
fontWeight: 'bolder' fontWeight: 'bolder'
}, },
@@ -90,8 +91,9 @@ export default createStyles(({ token }) => ({
}, },
row: { row: {
alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center',
padding: `0 ${token.paddingLG}px`,
'> *': { '> *': {
flex: '0 0 auto' flex: '0 0 auto'
@@ -99,7 +101,7 @@ export default createStyles(({ token }) => ({
}, },
label: { label: {
fontSize: token.fontSize, fontSize: token.fontSizeLG,
fontWeight: 'bolder', fontWeight: 'bolder',
flex: 1 flex: 1
}, },

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M853.824 512c0 195.84-160.224 356.064-356.064 356.064-148.352 0-274.976-91.008-328.384-217.6h11.872c195.84 0 356.064-160.224 356.064-356.064a360.64 360.64 0 0 0-27.68-138.464c191.872 5.92 344.192 164.192 344.192 356.064zM509.632 76.8c-25.728 0-49.44 11.872-65.28 33.632s-17.792 51.424-7.904 75.168c13.856 35.616 21.76 71.2 21.76 108.8 0 152.32-124.64 276.96-276.96 276.96h-11.872c-25.728 0-49.44 11.872-65.28 33.632s-17.792 51.424-7.904 75.168C165.44 842.368 321.696 947.2 497.76 947.2c239.36 0 435.2-195.84 435.2-435.2A433.664 433.664 0 0 0 511.616 76.8h-1.984z"/></svg>

After

Width:  |  Height:  |  Size: 645 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M469.333333 128a42.666667 42.666667 0 0 1 85.333334 0v85.333333a42.666667 42.666667 0 0 1-85.333334 0V128z m0 682.666667a42.666667 42.666667 0 0 1 85.333334 0v85.333333a42.666667 42.666667 0 0 1-85.333334 0v-85.333333z m42.666667-85.333334a213.333333 213.333333 0 1 1 0-426.666666 213.333333 213.333333 0 0 1 0 426.666666z m0-85.333333a128 128 0 1 0 0-256 128 128 0 0 0 0 256z m-384-85.333333a42.666667 42.666667 0 0 1 0-85.333334h85.333333a42.666667 42.666667 0 0 1 0 85.333334H128z m682.666667 0a42.666667 42.666667 0 0 1 0-85.333334h85.333333a42.666667 42.666667 0 0 1 0 85.333334h-85.333333z m-30.165334-371.498667a42.666667 42.666667 0 0 1 60.330667 60.330667l-67.456 67.456a42.666667 42.666667 0 0 1-60.330667-60.330667l67.413334-67.456zM243.498667 840.832a42.666667 42.666667 0 1 1-60.330667-60.330667l67.456-67.456a42.666667 42.666667 0 1 1 60.330667 60.330667l-67.413334 67.456z m-60.330667-597.333333a42.666667 42.666667 0 0 1 60.330667-60.330667l67.456 67.456a42.666667 42.666667 0 0 1-60.330667 60.330667l-67.456-67.413334z m657.664 537.002666a42.666667 42.666667 0 0 1-60.330667 60.330667l-67.456-67.456a42.666667 42.666667 0 0 1 60.330667-60.330667l67.456 67.413334z" /></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M513.12 11.136c-42.784 0-84.336 5.376-123.872 15.456-2.352 0.56-4.816 1.232-7.168 1.904-9.296 2.464-18.48 5.264-27.552 8.288-1.12 0.336-2.24 0.784-3.472 1.12-5.264 1.792-10.64 3.696-15.792 5.712-1.904 0.672-3.808 1.456-5.712 2.24-13.216 5.264-26.208 10.976-38.976 17.36l-5.376 2.688c-19.824 10.192-38.864 21.504-57.008 34.16-1.68 1.12-3.248 2.352-4.928 3.472-16.352 11.648-32.032 24.192-46.816 37.744l-4.368 4.032a465.472 465.472 0 0 0-29.568 29.904c-1.344 1.456-2.688 3.024-4.032 4.48-78.4 88.256-126 204.624-126 332.08 0 276.528 224.112 500.64 500.64 500.64s500.64-224.112 500.64-500.64-224.112-500.64-500.64-500.64zM810.144 808.8c-38.64 38.64-83.552 68.88-133.504 90.048-51.184 21.616-105.504 32.704-161.616 32.928v-840c56.112 0.224 110.432 11.312 161.616 32.928 50.064 21.168 94.976 51.408 133.504 90.048 38.64 38.64 68.88 83.552 90.048 133.504 21.84 51.744 32.928 106.736 32.928 163.52s-11.088 111.776-32.928 163.52c-21.168 49.952-51.408 94.864-90.048 133.504z" /></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,11 +1,12 @@
import Icon from '@ant-design/icons' import Icon from '@ant-design/icons'
import useStyles from '@/assets/css/components/common/sidebar/footer.style' import useStyles from '@/assets/css/components/common/sidebar/footer.style'
import { SidebarContext } from '@/components/common/Sidebar/index' import { THEME_DARK, THEME_FOLLOW_SYSTEM, THEME_LIGHT } from '@/constants/common.constants'
import { notification } from '@/util/common' import { getThemeMode, notification, setThemeMode, ThemeMode } from '@/util/common'
import { getRedirectUrl } from '@/util/route' import { getRedirectUrl } from '@/util/route'
import { getAvatar, getLoginStatus, getNickname, removeToken } from '@/util/auth' import { getAvatar, getLoginStatus, getNickname, removeToken } from '@/util/auth'
import { navigateToLogin, navigateToUser } from '@/util/navigation' import { navigateToLogin, navigateToUser } from '@/util/navigation'
import { r_auth_logout } from '@/services/auth' import { r_auth_logout } from '@/services/auth'
import { SidebarContext } from '@/components/common/Sidebar/index'
const Footer = () => { const Footer = () => {
const { styles, theme, cx } = useStyles() const { styles, theme, cx } = useStyles()
@@ -77,6 +78,31 @@ const Footer = () => {
</NavLink> </NavLink>
</span> </span>
{!getLoginStatus() && !isCollapse && (
<AntdSegmented<ThemeMode>
options={[
{
icon: <Icon component={IconOxygenThemeSystem} />,
title: '跟随系统',
value: THEME_FOLLOW_SYSTEM
},
{
label: <Icon component={IconOxygenThemeLight} />,
title: '亮色',
value: THEME_LIGHT
},
{
label: <Icon component={IconOxygenThemeDark} />,
title: '深色',
value: THEME_DARK
}
]}
defaultValue={getThemeMode()}
onChange={setThemeMode}
size={'small'}
block
/>
)}
<span <span
hidden={!getLoginStatus()} hidden={!getLoginStatus()}
className={cx(styles.text, isCollapse ? styles.collapsedText : '')} className={cx(styles.text, isCollapse ? styles.collapsedText : '')}

View File

@@ -1,7 +1,7 @@
import { PropsWithChildren, ReactNode } from 'react' import { PropsWithChildren, ReactNode } from 'react'
import Icon from '@ant-design/icons' import Icon from '@ant-design/icons'
import useStyles from '@/assets/css/components/common/sidebar/index.style' import useStyles from '@/assets/css/components/common/sidebar/index.style'
import { getLocalStorage, setLocalStorage } from '@/util/browser' import { getSidebarCollapse, setSidebarCollapse } from '@/util/common'
import Item from '@/components/common/Sidebar/Item' import Item from '@/components/common/Sidebar/Item'
import ItemList from '@/components/common/Sidebar/ItemList' import ItemList from '@/components/common/Sidebar/ItemList'
import Scroll from '@/components/common/Sidebar/Scroll' import Scroll from '@/components/common/Sidebar/Scroll'
@@ -20,12 +20,10 @@ interface SidebarProps extends PropsWithChildren {
const Sidebar = (props: SidebarProps) => { const Sidebar = (props: SidebarProps) => {
const { styles, cx } = useStyles() const { styles, cx } = useStyles()
const [isCollapseSidebar, setIsCollapseSidebar] = useState( const [isCollapseSidebar, setIsCollapseSidebar] = useState(getSidebarCollapse())
getLocalStorage('COLLAPSE_SIDEBAR') === 'true'
)
const switchSidebar = () => { const switchSidebar = () => {
setLocalStorage('COLLAPSE_SIDEBAR', !isCollapseSidebar ? 'true' : 'false') setSidebarCollapse(!isCollapseSidebar)
setIsCollapseSidebar(!isCollapseSidebar) setIsCollapseSidebar(!isCollapseSidebar)
props.onSidebarSwitch?.(isCollapseSidebar) props.onSidebarSwitch?.(isCollapseSidebar)
} }

View File

@@ -2,7 +2,12 @@ export const PRODUCTION_NAME = 'Oxygen Toolbox'
export const STORAGE_TOKEN_KEY = 'JWT_TOKEN' export const STORAGE_TOKEN_KEY = 'JWT_TOKEN'
export const STORAGE_USER_INFO_KEY = 'USER_INFO' export const STORAGE_USER_INFO_KEY = 'USER_INFO'
export const STORAGE_TOOL_MENU_ITEM_KEY = 'TOOL_MENU_ITEM' export const STORAGE_TOOL_MENU_ITEM_KEY = 'TOOL_MENU_ITEM'
export const STORAGE_COLLAPSE_SIDEBAR_KEY = 'COLLAPSE_SIDEBAR'
export const STORAGE_THEME_MODE_KEY = 'THEME_MODE'
export const COLOR_PRODUCTION = '#4E47BB' export const COLOR_PRODUCTION = '#4E47BB'
export const THEME_FOLLOW_SYSTEM = 'FOLLOW_SYSTEM'
export const THEME_LIGHT = 'LIGHT'
export const THEME_DARK = 'DARK'
/** /**
* Response code * Response code

View File

@@ -1,11 +1,14 @@
import Icon from '@ant-design/icons' import Icon from '@ant-design/icons'
import useStyles from '@/assets/css/pages/user/index.style' import useStyles from '@/assets/css/pages/user/index.style'
import { import {
THEME_DARK,
THEME_FOLLOW_SYSTEM,
THEME_LIGHT,
DATABASE_UPDATE_SUCCESS, DATABASE_UPDATE_SUCCESS,
PERMISSION_ACCESS_DENIED, PERMISSION_ACCESS_DENIED,
PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR
} from '@/constants/common.constants' } from '@/constants/common.constants'
import { message, notification, modal } from '@/util/common' import { message, notification, modal, getThemeMode, ThemeMode, setThemeMode } from '@/util/common'
import { utcToLocalTime } from '@/util/datetime' import { utcToLocalTime } from '@/util/datetime'
import { getUserInfo, removeToken } from '@/util/auth' import { getUserInfo, removeToken } from '@/util/auth'
import { r_sys_user_info_change_password, r_sys_user_info_update } from '@/services/system' import { r_sys_user_info_change_password, r_sys_user_info_update } from '@/services/system'
@@ -553,6 +556,21 @@ const User = () => {
</FlexBox> </FlexBox>
<div className={styles.divider} /> <div className={styles.divider} />
<FlexBox className={styles.list}> <FlexBox className={styles.list}>
<FlexBox className={styles.row} direction={'horizontal'}>
<div className={styles.label}></div>
<div className={styles.input}>
<AntdSegmented<ThemeMode>
options={[
{ label: '跟随系统', value: THEME_FOLLOW_SYSTEM },
{ label: '亮色', value: THEME_LIGHT },
{ label: '深色', value: THEME_DARK }
]}
defaultValue={getThemeMode()}
onChange={setThemeMode}
block
/>
</div>
</FlexBox>
<FlexBox className={styles.row} direction={'horizontal'}> <FlexBox className={styles.row} direction={'horizontal'}>
<div className={styles.label}> IP</div> <div className={styles.label}> IP</div>
<div className={styles.input}> <div className={styles.input}>

View File

@@ -48,6 +48,7 @@ export const removeCookie = (name: string) => {
export const setLocalStorage = (name: string, value: string) => { export const setLocalStorage = (name: string, value: string) => {
localStorage.setItem(name, value) localStorage.setItem(name, value)
window.dispatchEvent(new Event('localStorageChange'))
} }
export const getLocalStorage = (name: string) => { export const getLocalStorage = (name: string) => {

View File

@@ -1,12 +1,21 @@
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { floor } from 'lodash' import { floor } from 'lodash'
import { STORAGE_TOOL_MENU_ITEM_KEY } from '@/constants/common.constants' import {
STORAGE_COLLAPSE_SIDEBAR_KEY,
STORAGE_THEME_MODE_KEY,
STORAGE_TOOL_MENU_ITEM_KEY,
THEME_DARK,
THEME_FOLLOW_SYSTEM,
THEME_LIGHT
} from '@/constants/common.constants'
import { getLocalStorage, setLocalStorage } from '@/util/browser' import { getLocalStorage, setLocalStorage } from '@/util/browser'
import FullscreenLoadingMask from '@/components/common/FullscreenLoadingMask' import FullscreenLoadingMask from '@/components/common/FullscreenLoadingMask'
import { MessageInstance } from 'antd/es/message/interface' import { MessageInstance } from 'antd/es/message/interface'
import { NotificationInstance } from 'antd/es/notification/interface' import { NotificationInstance } from 'antd/es/notification/interface'
import { HookAPI } from 'antd/es/modal/useModal' import { HookAPI } from 'antd/es/modal/useModal'
export type ThemeMode = typeof THEME_FOLLOW_SYSTEM | typeof THEME_LIGHT | typeof THEME_DARK
let message: MessageInstance let message: MessageInstance
let notification: NotificationInstance let notification: NotificationInstance
let modal: HookAPI let modal: HookAPI
@@ -212,3 +221,24 @@ export const omitTextByByte = (text: string, length: number) => {
} }
return `${substringByByte(text, 0, length)}...` return `${substringByByte(text, 0, length)}...`
} }
export const getSidebarCollapse = () => getLocalStorage(STORAGE_COLLAPSE_SIDEBAR_KEY) === 'true'
export const setSidebarCollapse = (isCollapse: boolean) => {
setLocalStorage(STORAGE_COLLAPSE_SIDEBAR_KEY, isCollapse ? 'true' : 'false')
}
export const getThemeMode = (): ThemeMode => {
switch (getLocalStorage(STORAGE_THEME_MODE_KEY)) {
case THEME_FOLLOW_SYSTEM:
case THEME_LIGHT:
case THEME_DARK:
return getLocalStorage(STORAGE_THEME_MODE_KEY) as ThemeMode
default:
return THEME_FOLLOW_SYSTEM
}
}
export const setThemeMode = (themeMode: ThemeMode) => {
setLocalStorage(STORAGE_THEME_MODE_KEY, themeMode)
}