сначала у меня есть корневой компонент, который обрабатывает дерево компонентов,
RootApp.jsx
import { Suspense } from 'react';
import { HashRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import store from '@/redux/store';
import PageLoader from '@/components/PageLoader';
import '@/style/app.css';
import '@/style/index.css';
import '@/style/tailwind.css'
import ERP_SODEOs from '@/apps/IdurarOs';
import { ThemeProvider } from '@/context/ThemeContext/ThemeContext'; // Import ThemeProvider
export default function RoutApp() {
return (
<Router>
<Provider store = {store}>
<ThemeProvider>
<Suspense fallback = {<PageLoader />}>
<ERP_SODEOs />
</Suspense>
</ThemeProvider>
</Provider>
</Router>
);
}
в <ERP_SODEOs> имеется несколько компонентов, основным компонентом, обрабатывающим дочерний компонент, является ErpApp.jsx:
return (
<Layout hasSider>
<Navigation onPathChange = {handlePathChange} />
{isMobile ? (
<Layout style = {{ marginLeft: 0 }}>
<HeaderContent />
<Content
style = {{
margin: '40px auto 30px',
overflow: 'initial',
width: '100%',
padding: '0 25px',
maxWidth: 'none',
}}
>
<AppRouter />
</Content>
</Layout>
) : (
<Layout style = {{ marginLeft: isNavMenuClose ? 100 : 220 }}>
<HeaderContent currentPath = {currentPath} />
<Content
style = {{
margin: '30px auto 30px',
overflow: 'initial',
width: '100%',
padding: '0px 10px 0px 0px',
maxWidth: isNavMenuClose ? 1700 : 1600,
}}
>
<AppRouter />
</Content>
</Layout>
)}
</Layout>
);
}
я пытаюсь реализовать темный режим внутри моего компонента API контекста, который не работает HeaderContent.jsx:
export default function HeaderContent() {
const currentAdmin = useSelector(selectCurrentAdmin);
const translate = useLanguage();
const [hasPhotoprofile, setHasPhotoprofile] = useState(false);
const [activeKey, setActiveKey] = useState(null);
const [isScrolled, setIsScrolled] = useState(false);
const isAdmin = currentAdmin?.role === 'admin';
const { theme, toggleTheme } = useContext(ThemeContext);
useEffect(() => {
function handleScroll() {
const scrollPosition = window.scrollY;
setIsScrolled(scrollPosition > 0);
}
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
useEffect(() => {
async function fetchData() {
try {
const result = await checkImage(BASE_URL + currentAdmin?.photo);
setHasPhotoprofile(result);
} catch (error) {
console.error('Error checking image:', error);
}
}
fetchData();
}, [currentAdmin]);
const srcImgProfile = hasPhotoprofile ? BASE_URL + currentAdmin?.photo : null;
const ProfileDropdown = () => {
const navigate = useNavigate();
return (
<div className = "profileDropdown" onClick = {() => navigate('/profile')}>
<Avatar
size = "large"
className = "last"
src = {srcImgProfile}
style = {{ color: '#f56a00', backgroundColor: !hasPhotoprofile ? '#fde3cf' : '#f9fafc' }}
>
{currentAdmin?.fullname && currentAdmin?.fullname.charAt(0).toUpperCase()}
</Avatar>
<div className = "profileDropdownInfo">
<p className='capitalize font-thin text-sm text-blue-600'>
{currentAdmin?.fullname}
</p>
<p className='font-mono text-[11px] text-sm text-red-600 font-thin'>{currentAdmin?.username}</p>
</div>
</div>
);
};
const DropdownMenu = ({ text }) => <span>{text}</span>;
const items = isAdmin
? [
{
label: <ProfileDropdown className = "headerDropDownMenu" />,
key: 'ProfileDropdown',
},
{ type: 'divider' },
{
icon: <SettingOutlined />,
key: 'settingProfile',
label: (
<Link to = {'/profile'}>
<DropdownMenu text = {translate('profile_settings')} />
</Link>
),
},
{
icon: <SettingOutlined />,
key: 'settingApp',
label: <Link to = {'/settings'}>{translate('app_settings')}</Link>,
},
{ type: 'divider' },
{
icon: <LogoutOutlined />,
key: 'logout',
label: <Link to = {'/logout'}>{translate('logout')}</Link>,
}
]
: [
{
icon: <SettingOutlined />,
key: 'settingProfile',
label: (
<Link to = {'/profile'}>
<DropdownMenu text = {translate('update_password')} />
</Link>
),
},
{
icon: <LogoutOutlined />,
key: 'logout',
label: <Link to = {'/logout'}>{translate('logout')}</Link>,
}
];
const darkModeIcons = [
{
icon: <BsMoonStars />,
key: 'dark',
label: 'Dark',
onClick: () => toggleTheme('dark'),
},
{
icon: <GrSun />,
key: 'light',
label: 'Light',
onClick: () => toggleTheme('light'),
},
{
icon: <MdOutlineComputer />,
key: 'system',
label: 'System',
onClick: () => toggleTheme('system'),
},
];
const darkModeDropdown = (
<Menu className='w-36' selectedKeys = {[activeKey]}>
{darkModeIcons.map((item) => (
<Menu.Item
key = {item.key}
onClick = {() => handleItemClick(item.key)}
style = {{ display: 'flex', alignItems: 'center' }}
>
<div className='flex items-center gap-2.5'>
{item.icon} <span>{item.label}</span>
</div>
</Menu.Item>
))}
</Menu>
);
const handleItemClick = (key) => {
setActiveKey(key);
};
return (
<Header
className = {`sticky top-0 z-50 ${isScrolled ? 'scrolled' : 'bg-white w-[100%] scroll-smooth'} ${theme === 'dark' ? 'bg-gray-900' : 'bg-white'}`}
style = {{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<div className = {`text-[15px] font-thin ${theme === 'dark' ? 'text-white' : 'text-black'}`}>
<span className='uppercase'>Dashboard</span>
</div>
<div className='flex flex-row-reverse gap-3 items-center'>
<Dropdown
overlay = {<Menu items = {items} />}
trigger = {['click']}
placement = "bottomRight"
style = {{ width: '280px', float: 'right' }}
>
<Avatar
className = "last"
src = {srcImgProfile}
style = {{
color: '#f56a00',
backgroundColor: !hasPhotoprofile ? '#fde3cf' : '#f9fafc',
float: 'right',
}}
size = "large"
>
{currentAdmin?.fullname && currentAdmin?.fullname.charAt(0).toUpperCase()}
</Avatar>
</Dropdown>
<Dropdown overlay = {darkModeDropdown} trigger = {['click']} placement='bottomRight'>
<div>
<BsMoonStars className = {`text-[20px] cursor-pointer ${theme === 'dark' ? 'text-white' : 'text-black'}`} />
</div>
</Dropdown>
<SelectLanguage />
</div>
</Header>
);
}
поскольку глобальные файлы CSS импортируются в корневой файл, я также пытался добавить собственный CSS в этот глобальный файл CSS, но он не работал.
в файле headercontent есть раскрывающаяся кнопка, в которой есть 3 варианта изменения темы приложения. Когда пользователь нажимает темную кнопку, цвет заголовка должен измениться. Я также хочу использовать созданный мной контекст, используемый в других компонентах как ну, я также хочу добавить, что я уже изменил файл tsconfig с помощью darkMode: 'class'
Чтобы реализовать функциональность темного режима с помощью Context API и обеспечить его работу во всем вашем приложении, в том числе в компоненте HeaderContent
, вам необходимо выполнить несколько шагов, чтобы правильно настроить и использовать ваш ThemeContext
. Вот подробный подход, который поможет вам устранить неполадки и реализовать эту функциональность:
Шаг 1. Создайте контекст темы
Сначала создайте ThemeContext
с поставщиком и настраиваемым хуком для упрощения использования.
Темеконтекст.js
import React, { createContext, useState, useContext, useEffect } from 'react';
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState(() => {
// Retrieve the stored theme or default to 'light'
const savedTheme = localStorage.getItem('theme');
return savedTheme ? savedTheme : 'light';
});
const toggleTheme = (newTheme) => {
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
};
useEffect(() => {
// Apply the theme to the body class
document.body.className = theme;
}, [theme]);
return (
<ThemeContext.Provider value = {{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => useContext(ThemeContext);
Шаг 2. Оберните свое приложение поставщиком тем
Убедитесь, что ваш ThemeProvider
обертывает ваше приложение, чтобы контекст был доступен всем компонентам.
RootApp.jsx
import { Suspense } from 'react';
import { HashRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import store from '@/redux/store';
import PageLoader from '@/components/PageLoader';
import '@/style/app.css';
import '@/style/index.css';
import '@/style/tailwind.css';
import ERP_SODEOs from '@/apps/IdurarOs';
import { ThemeProvider } from '@/context/ThemeContext/ThemeContext'; // Import ThemeProvider
export default function RootApp() {
return (
<Router>
<Provider store = {store}>
<ThemeProvider>
<Suspense fallback = {<PageLoader />}>
<ERP_SODEOs />
</Suspense>
</ThemeProvider>
</Provider>
</Router>
);
}
Шаг 3. Используйте контекст темы в HeaderContent
Теперь вы можете использовать хук useTheme
для доступа и изменения темы в вашем компоненте HeaderContent
.
HeaderContent.jsx
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { Link, useNavigate } from 'react-router-dom';
import { Avatar, Dropdown, Menu, Layout } from 'antd';
import { SettingOutlined, LogoutOutlined } from '@ant-design/icons';
import { BsMoonStars } from 'react-icons/bs';
import { GrSun } from 'react-icons/gr';
import { MdOutlineComputer } from 'react-icons/md';
import { useTheme } from '@/context/ThemeContext/ThemeContext'; // Import useTheme
import { selectCurrentAdmin } from '@/redux/selectors';
import SelectLanguage from '@/components/SelectLanguage';
import checkImage from '@/utils/checkImage';
const { Header, Content } = Layout;
export default function HeaderContent() {
const currentAdmin = useSelector(selectCurrentAdmin);
const [hasPhotoprofile, setHasPhotoprofile] = useState(false);
const [activeKey, setActiveKey] = useState(null);
const [isScrolled, setIsScrolled] = useState(false);
const { theme, toggleTheme } = useTheme(); // Use the custom hook to access theme context
useEffect(() => {
function handleScroll() {
const scrollPosition = window.scrollY;
setIsScrolled(scrollPosition > 0);
}
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
useEffect(() => {
async function fetchData() {
try {
const result = await checkImage(BASE_URL + currentAdmin?.photo);
setHasPhotoprofile(result);
} catch (error) {
console.error('Error checking image:', error);
}
}
fetchData();
}, [currentAdmin]);
const srcImgProfile = hasPhotoprofile ? BASE_URL + currentAdmin?.photo : null;
const ProfileDropdown = () => {
const navigate = useNavigate();
return (
<div className = "profileDropdown" onClick = {() => navigate('/profile')}>
<Avatar
size = "large"
className = "last"
src = {srcImgProfile}
style = {{ color: '#f56a00', backgroundColor: !hasPhotoprofile ? '#fde3cf' : '#f9fafc' }}
>
{currentAdmin?.fullname && currentAdmin?.fullname.charAt(0).toUpperCase()}
</Avatar>
<div className = "profileDropdownInfo">
<p className='capitalize font-thin text-sm text-blue-600'>
{currentAdmin?.fullname}
</p>
<p className='font-mono text-[11px] text-sm text-red-600 font-thin'>{currentAdmin?.username}</p>
</div>
</div>
);
};
const DropdownMenu = ({ text }) => <span>{text}</span>;
const items = currentAdmin?.role === 'admin'
? [
{
label: <ProfileDropdown className = "headerDropDownMenu" />,
key: 'ProfileDropdown',
},
{ type: 'divider' },
{
icon: <SettingOutlined />,
key: 'settingProfile',
label: (
<Link to = {'/profile'}>
<DropdownMenu text = {translate('profile_settings')} />
</Link>
),
},
{
icon: <SettingOutlined />,
key: 'settingApp',
label: <Link to = {'/settings'}>{translate('app_settings')}</Link>,
},
{ type: 'divider' },
{
icon: <LogoutOutlined />,
key: 'logout',
label: <Link to = {'/logout'}>{translate('logout')}</Link>,
}
]
: [
{
icon: <SettingOutlined />,
key: 'settingProfile',
label: (
<Link to = {'/profile'}>
<DropdownMenu text = {translate('update_password')} />
</Link>
),
},
{
icon: <LogoutOutlined />,
key: 'logout',
label: <Link to = {'/logout'}>{translate('logout')}</Link>,
}
];
const darkModeIcons = [
{
icon: <BsMoonStars />,
key: 'dark',
label: 'Dark',
onClick: () => toggleTheme('dark'),
},
{
icon: <GrSun />,
key: 'light',
label: 'Light',
onClick: () => toggleTheme('light'),
},
{
icon: <MdOutlineComputer />,
key: 'system',
label: 'System',
onClick: () => toggleTheme('system'),
},
];
const darkModeDropdown = (
<Menu className='w-36' selectedKeys = {[activeKey]}>
{darkModeIcons.map((item) => (
<Menu.Item
key = {item.key}
onClick = {() => handleItemClick(item.key)}
style = {{ display: 'flex', alignItems: 'center' }}
>
<div className='flex items-center gap-2.5'>
{item.icon} <span>{item.label}</span>
</div>
</Menu.Item>
))}
</Menu>
);
const handleItemClick = (key) => {
setActiveKey(key);
};
return (
<Header
className = {`sticky top-0 z-50 ${isScrolled ? 'scrolled' : 'bg-white w-[100%] scroll-smooth'} ${theme === 'dark' ? 'bg-gray-900' : 'bg-white'}`}
style = {{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<div className = {`text-[15px] font-thin ${theme === 'dark' ? 'text-white' : 'text-black'}`}>
<span className='uppercase'>Dashboard</span>
</div>
<div className='flex flex-row-reverse gap-3 items-center'>
<Dropdown
overlay = {<Menu items = {items} />}
trigger = {['click']}
placement = "bottomRight"
style = {{ width: '280px', float: 'right' }}
>
<Avatar
className = "last"
src = {srcImgProfile}
style = {{
color: '#f56a00',
backgroundColor: !hasPhotoprofile ? '#fde3cf' : '#f9fafc',
float: 'right',
}}
size = "large"
>
{currentAdmin?.fullname && currentAdmin?.fullname.charAt(0).toUpperCase()}
</Avatar>
</Dropdown>
<Dropdown overlay = {darkModeDropdown} trigger = {['click']} placement='bottomRight'>
<div>
<BsMoonStars className = {`text-[20px] cursor-pointer ${theme === 'dark' ? 'text-white' : 'text-black'}`} />
</div>
</Dropdown>
<SelectLanguage />
</div>
</Header>
);
}
Шаг 4. Добавьте стили для темного режима Наконец, убедитесь, что ваш CSS имеет необходимые стили для темного и светлого режимов. Чтобы справиться с этим, вы можете использовать класс CSS в элементе body.
body.light {
background-color: #ffffff;
color: #000000;
}
body.dark {
background-color: #1a1a1a;
color: #ffffff;
}
При таком подходе функция toggleTheme
устанавливает состояние theme
и обновляет локальное хранилище. Хук useEffect
применяет тему, изменяя класс body
. Компонент HeaderContent
использует хук useTheme
для доступа к текущей теме и соответствующего применения стилей.
Это должно гарантировать, что функциональность темного режима работает должным образом во всем вашем приложении.