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

View File

@@ -2,9 +2,14 @@ import { theme } from 'antd'
import zh_CN from 'antd/locale/zh_CN'
import BaseStyles from '@/assets/css/base.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 { init } from '@/util/common'
import { getThemeMode, init } from '@/util/common'
import FullscreenLoadingMask from '@/components/common/FullscreenLoadingMask'
export const AppContext = createContext({
@@ -17,19 +22,38 @@ const App = () => {
const [notificationInstance, notificationHolder] = notification.useNotification()
const [modalInstance, modalHolder] = AntdModal.useModal()
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(() => {
init(messageInstance, notificationInstance, modalInstance)
const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)')
setIsDarkMode(darkThemeMq.matches)
const listener = (ev: MediaQueryListEvent) => {
setIsDarkMode(ev.matches)
setIsSystemDarkMode(darkThemeMq.matches)
const darkThemeMqChangeListener = (ev: MediaQueryListEvent) => {
setIsSystemDarkMode(ev.matches)
}
darkThemeMq.addEventListener('change', listener)
darkThemeMq.addEventListener('change', darkThemeMqChangeListener)
const themeModeChangeListener = () => {
setThemeMode(getThemeMode())
}
window.addEventListener('localStorageChange', themeModeChangeListener)
return () => {
darkThemeMq.removeEventListener('change', listener)
darkThemeMq.removeEventListener('change', darkThemeMqChangeListener)
window.removeEventListener('localStorageChange', themeModeChangeListener)
}
}, [])
@@ -37,7 +61,7 @@ const App = () => {
<AntdConfigProvider
theme={{
cssVar: true,
algorithm: isDarkMode ? theme.darkAlgorithm : undefined,
algorithm: getIsDark() ? theme.darkAlgorithm : theme.defaultAlgorithm,
token: {
colorPrimary: COLOR_PRODUCTION,
colorLinkHover: COLOR_PRODUCTION
@@ -57,7 +81,7 @@ const App = () => {
refreshRouter: () => {
setRouterState(getRouter())
},
isDarkMode
isDarkMode: getIsDark()
}}
>
<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 }) => {
const collapsedExit = cx(css`
opacity: 0;
@@ -29,7 +18,6 @@ export default createStyles(({ cx, css, token }) => {
padding-left: ${token.paddingXS}px;
left: 100%;
z-index: 1000;
animation: ${slideOut} 0.1s ease;
transform: translateX(-100%);
`)

View File

@@ -63,6 +63,7 @@ export default createStyles(({ token }) => ({
header: {
justifyContent: 'space-between',
alignItems: 'center',
'> *': {
flex: '0 0 auto'
@@ -70,7 +71,7 @@ export default createStyles(({ token }) => ({
},
title: {
fontSize: token.fontSizeXL,
fontSize: token.fontSizeHeading3,
fontWeight: 'bolder'
},
@@ -90,8 +91,9 @@ export default createStyles(({ token }) => ({
},
row: {
alignItems: 'center',
justifyContent: 'space-between',
alignItems: 'center',
padding: `0 ${token.paddingLG}px`,
'> *': {
flex: '0 0 auto'
@@ -99,7 +101,7 @@ export default createStyles(({ token }) => ({
},
label: {
fontSize: token.fontSize,
fontSize: token.fontSizeLG,
fontWeight: 'bolder',
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 useStyles from '@/assets/css/components/common/sidebar/footer.style'
import { SidebarContext } from '@/components/common/Sidebar/index'
import { notification } from '@/util/common'
import { THEME_DARK, THEME_FOLLOW_SYSTEM, THEME_LIGHT } from '@/constants/common.constants'
import { getThemeMode, notification, setThemeMode, ThemeMode } from '@/util/common'
import { getRedirectUrl } from '@/util/route'
import { getAvatar, getLoginStatus, getNickname, removeToken } from '@/util/auth'
import { navigateToLogin, navigateToUser } from '@/util/navigation'
import { r_auth_logout } from '@/services/auth'
import { SidebarContext } from '@/components/common/Sidebar/index'
const Footer = () => {
const { styles, theme, cx } = useStyles()
@@ -77,6 +78,31 @@ const Footer = () => {
</NavLink>
</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
hidden={!getLoginStatus()}
className={cx(styles.text, isCollapse ? styles.collapsedText : '')}

View File

@@ -1,7 +1,7 @@
import { PropsWithChildren, ReactNode } from 'react'
import Icon from '@ant-design/icons'
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 ItemList from '@/components/common/Sidebar/ItemList'
import Scroll from '@/components/common/Sidebar/Scroll'
@@ -20,12 +20,10 @@ interface SidebarProps extends PropsWithChildren {
const Sidebar = (props: SidebarProps) => {
const { styles, cx } = useStyles()
const [isCollapseSidebar, setIsCollapseSidebar] = useState(
getLocalStorage('COLLAPSE_SIDEBAR') === 'true'
)
const [isCollapseSidebar, setIsCollapseSidebar] = useState(getSidebarCollapse())
const switchSidebar = () => {
setLocalStorage('COLLAPSE_SIDEBAR', !isCollapseSidebar ? 'true' : 'false')
setSidebarCollapse(!isCollapseSidebar)
setIsCollapseSidebar(!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_USER_INFO_KEY = 'USER_INFO'
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 THEME_FOLLOW_SYSTEM = 'FOLLOW_SYSTEM'
export const THEME_LIGHT = 'LIGHT'
export const THEME_DARK = 'DARK'
/**
* Response code

View File

@@ -1,11 +1,14 @@
import Icon from '@ant-design/icons'
import useStyles from '@/assets/css/pages/user/index.style'
import {
THEME_DARK,
THEME_FOLLOW_SYSTEM,
THEME_LIGHT,
DATABASE_UPDATE_SUCCESS,
PERMISSION_ACCESS_DENIED,
PERMISSION_LOGIN_USERNAME_PASSWORD_ERROR
} 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 { getUserInfo, removeToken } from '@/util/auth'
import { r_sys_user_info_change_password, r_sys_user_info_update } from '@/services/system'
@@ -553,6 +556,21 @@ const User = () => {
</FlexBox>
<div className={styles.divider} />
<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'}>
<div className={styles.label}> IP</div>
<div className={styles.input}>

View File

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

View File

@@ -1,12 +1,21 @@
import { createRoot } from 'react-dom/client'
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 FullscreenLoadingMask from '@/components/common/FullscreenLoadingMask'
import { MessageInstance } from 'antd/es/message/interface'
import { NotificationInstance } from 'antd/es/notification/interface'
import { HookAPI } from 'antd/es/modal/useModal'
export type ThemeMode = typeof THEME_FOLLOW_SYSTEM | typeof THEME_LIGHT | typeof THEME_DARK
let message: MessageInstance
let notification: NotificationInstance
let modal: HookAPI
@@ -212,3 +221,24 @@ export const omitTextByByte = (text: string, length: number) => {
}
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)
}