♻️ refactor(StyleContext): modernize context architecture and eliminate route transition flicker
## Breaking Changes - Remove backward compatibility layer for old action types - StyleContext is no longer exported, use useStyle hook instead ## Major Improvements - **Architecture**: Replace useState with useReducer for complex state management - **Performance**: Add debounced resize handling and batch updates via BATCH_UPDATE action - **DX**: Export useStyle hook and styleActions for type-safe usage - **Memory**: Use useMemo to cache context value and prevent unnecessary re-renders ## Bug Fixes - **UI**: Eliminate padding flicker when navigating to /console/chat* and /console/playground routes - **Logic**: Remove redundant localStorage operations and state synchronization ## Implementation Details - Define ACTION_TYPES and ROUTE_PATTERNS constants for better maintainability - Add comprehensive JSDoc documentation for all functions - Extract custom hooks: useWindowResize, useRouteChange, useMobileSiderAutoHide - Calculate shouldInnerPadding directly in PageLayout based on pathname to prevent async updates - Integrate localStorage saving logic into SET_SIDER_COLLAPSED reducer case - Remove SET_INNER_PADDING action as it's no longer needed ## Updated Components - PageLayout.js: Direct padding calculation based on route - HeaderBar.js: Use new useStyle hook and styleActions - SiderBar.js: Remove redundant localStorage calls - LogsTable.js: Remove unused StyleContext import - Playground/index.js: Migrate to new API ## Performance Impact - Reduced component re-renders through optimized context structure - Eliminated unnecessary effect dependencies and state updates - Improved route transition smoothness with synchronous padding calculation
This commit is contained in:
@@ -31,13 +31,13 @@ import {
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { stringToColor } from '../helpers/render';
|
||||
import { StatusContext } from '../context/Status/index.js';
|
||||
import { StyleContext } from '../context/Style/index.js';
|
||||
import { useStyle, styleActions } from '../context/Style/index.js';
|
||||
|
||||
const HeaderBar = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const { state: styleState, dispatch: styleDispatch } = useStyle();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
let navigate = useNavigate();
|
||||
const [currentLang, setCurrentLang] = useState(i18n.language);
|
||||
@@ -152,8 +152,7 @@ const HeaderBar = () => {
|
||||
|
||||
const handleNavLinkClick = (itemKey) => {
|
||||
if (itemKey === 'home') {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
|
||||
styleDispatch({ type: 'SET_SIDER', payload: false });
|
||||
styleDispatch(styleActions.setSider(false));
|
||||
}
|
||||
setMobileMenuOpen(false);
|
||||
};
|
||||
@@ -383,7 +382,7 @@ const HeaderBar = () => {
|
||||
onClick={() => {
|
||||
if (isConsoleRoute) {
|
||||
// 控制侧边栏的显示/隐藏,无论是否移动设备
|
||||
styleDispatch({ type: 'TOGGLE_SIDER' });
|
||||
styleDispatch(styleActions.toggleSider());
|
||||
} else {
|
||||
// 控制HeaderBar自己的移动菜单
|
||||
setMobileMenuOpen(!mobileMenuOpen);
|
||||
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
} from '../helpers/render';
|
||||
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
|
||||
import { getLogOther } from '../helpers/other.js';
|
||||
import { StyleContext } from '../context/Style/index.js';
|
||||
import {
|
||||
IconRefresh,
|
||||
IconSetting,
|
||||
@@ -765,7 +764,6 @@ const LogsTable = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [expandData, setExpandData] = useState({});
|
||||
const [showStat, setShowStat] = useState(false);
|
||||
|
||||
@@ -5,7 +5,7 @@ import App from '../App.js';
|
||||
import FooterBar from './Footer.js';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { StyleContext } from '../context/Style/index.js';
|
||||
import { useStyle } from '../context/Style/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, getLogo, getSystemName, showError } from '../helpers/index.js';
|
||||
import { setStatusData } from '../helpers/data.js';
|
||||
@@ -17,11 +17,15 @@ const { Sider, Content, Header, Footer } = Layout;
|
||||
const PageLayout = () => {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const { state: styleState } = useStyle();
|
||||
const { i18n } = useTranslation();
|
||||
const location = useLocation();
|
||||
|
||||
const isPlaygroundRoute = location.pathname === '/console/playground';
|
||||
const shouldHideFooter = location.pathname === '/console/playground' || location.pathname.startsWith('/console/chat');
|
||||
|
||||
const shouldInnerPadding = location.pathname.includes('/console') &&
|
||||
!location.pathname.startsWith('/console/chat') &&
|
||||
location.pathname !== '/console/playground';
|
||||
|
||||
const loadUser = () => {
|
||||
let user = localStorage.getItem('user');
|
||||
@@ -65,15 +69,8 @@ const PageLayout = () => {
|
||||
if (savedLang) {
|
||||
i18n.changeLanguage(savedLang);
|
||||
}
|
||||
|
||||
// 默认显示侧边栏
|
||||
styleDispatch({ type: 'SET_SIDER', payload: true });
|
||||
}, [i18n]);
|
||||
|
||||
// 获取侧边栏折叠状态
|
||||
const isSidebarCollapsed =
|
||||
localStorage.getItem('default_collapse_sidebar') === 'true';
|
||||
|
||||
return (
|
||||
<Layout
|
||||
style={{
|
||||
@@ -99,8 +96,8 @@ const PageLayout = () => {
|
||||
</Header>
|
||||
<Layout
|
||||
style={{
|
||||
marginTop: '56px',
|
||||
height: 'calc(100vh - 56px)',
|
||||
marginTop: '64px',
|
||||
height: 'calc(100vh - 64px)',
|
||||
overflow: styleState.isMobile ? 'visible' : 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -111,11 +108,11 @@ const PageLayout = () => {
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
top: '56px',
|
||||
top: '64px',
|
||||
zIndex: 99,
|
||||
border: 'none',
|
||||
paddingRight: '0',
|
||||
height: 'calc(100vh - 56px)',
|
||||
height: 'calc(100vh - 64px)',
|
||||
}}
|
||||
>
|
||||
<SiderBar />
|
||||
@@ -141,14 +138,14 @@ const PageLayout = () => {
|
||||
flex: '1 0 auto',
|
||||
overflowY: styleState.isMobile ? 'visible' : 'auto',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
padding: styleState.shouldInnerPadding ? '24px' : '0',
|
||||
padding: shouldInnerPadding ? '24px' : '0',
|
||||
position: 'relative',
|
||||
marginTop: styleState.isMobile ? '2px' : '0',
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
</Content>
|
||||
{!isPlaygroundRoute && (
|
||||
{!shouldHideFooter && (
|
||||
<Layout.Footer
|
||||
style={{
|
||||
flex: '0 0 auto',
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
import { setStatusData } from '../helpers/data.js';
|
||||
import { stringToColor } from '../helpers/render.js';
|
||||
import { useSetTheme, useTheme } from '../context/Theme/index.js';
|
||||
import { StyleContext } from '../context/Style/index.js';
|
||||
import { useStyle, styleActions } from '../context/Style/index.js';
|
||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||
|
||||
// 自定义侧边栏按钮样式
|
||||
@@ -95,13 +95,11 @@ const routerMap = {
|
||||
|
||||
const SiderBar = () => {
|
||||
const { t } = useTranslation();
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const { state: styleState, dispatch: styleDispatch } = useStyle();
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
const defaultIsCollapsed =
|
||||
localStorage.getItem('default_collapse_sidebar') === 'true';
|
||||
|
||||
const [selectedKeys, setSelectedKeys] = useState(['home']);
|
||||
const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
|
||||
const [isCollapsed, setIsCollapsed] = useState(styleState.siderCollapsed);
|
||||
const [chatItems, setChatItems] = useState([]);
|
||||
const [openedKeys, setOpenedKeys] = useState([]);
|
||||
const theme = useTheme();
|
||||
@@ -270,7 +268,7 @@ const SiderBar = () => {
|
||||
|
||||
if (Array.isArray(chats) && chats.length > 0) {
|
||||
for (let i = 0; i < chats.length; i++) {
|
||||
newRouterMap['chat' + i] = '/chat/' + i;
|
||||
newRouterMap['chat' + i] = '/console/chat/' + i;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +289,7 @@ const SiderBar = () => {
|
||||
for (let key in chats[i]) {
|
||||
chat.text = key;
|
||||
chat.itemKey = 'chat' + i;
|
||||
chat.to = '/chat/' + i;
|
||||
chat.to = '/console/chat/' + i;
|
||||
}
|
||||
chatItems.push(chat);
|
||||
}
|
||||
@@ -315,7 +313,7 @@ const SiderBar = () => {
|
||||
);
|
||||
|
||||
// Handle chat routes
|
||||
if (!matchingKey && currentPath.startsWith('/chat/')) {
|
||||
if (!matchingKey && currentPath.startsWith('/console/chat/')) {
|
||||
const chatIndex = currentPath.split('/').pop();
|
||||
if (!isNaN(chatIndex)) {
|
||||
matchingKey = 'chat' + chatIndex;
|
||||
@@ -365,15 +363,11 @@ const SiderBar = () => {
|
||||
overflowY: 'auto',
|
||||
WebkitOverflowScrolling: 'touch', // Improve scrolling on iOS devices
|
||||
}}
|
||||
defaultIsCollapsed={
|
||||
localStorage.getItem('default_collapse_sidebar') === 'true'
|
||||
}
|
||||
defaultIsCollapsed={styleState.siderCollapsed}
|
||||
isCollapsed={isCollapsed}
|
||||
onCollapseChange={(collapsed) => {
|
||||
setIsCollapsed(collapsed);
|
||||
// styleDispatch({ type: 'SET_SIDER', payload: true });
|
||||
styleDispatch({ type: 'SET_SIDER_COLLAPSED', payload: collapsed });
|
||||
localStorage.setItem('default_collapse_sidebar', collapsed);
|
||||
styleDispatch(styleActions.setSiderCollapsed(collapsed));
|
||||
|
||||
// 确保在收起侧边栏时有选中的项目,避免不必要的计算
|
||||
if (selectedKeys.length === 0) {
|
||||
@@ -384,7 +378,7 @@ const SiderBar = () => {
|
||||
|
||||
if (matchingKey) {
|
||||
setSelectedKeys([matchingKey]);
|
||||
} else if (currentPath.startsWith('/chat/')) {
|
||||
} else if (currentPath.startsWith('/console/chat/')) {
|
||||
setSelectedKeys(['chat']);
|
||||
} else {
|
||||
setSelectedKeys(['detail']); // 默认选中首页
|
||||
@@ -406,12 +400,6 @@ const SiderBar = () => {
|
||||
);
|
||||
}}
|
||||
onSelect={(key) => {
|
||||
if (key.itemKey.toString().startsWith('chat')) {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
|
||||
} else {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
|
||||
}
|
||||
|
||||
// 如果点击的是已经展开的子菜单的父项,则收起子菜单
|
||||
if (openedKeys.includes(key.itemKey)) {
|
||||
setOpenedKeys(openedKeys.filter((k) => k !== key.itemKey));
|
||||
|
||||
@@ -38,8 +38,9 @@ const ChatArea = ({
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="!rounded-2xl h-full"
|
||||
bodyStyle={{ padding: 0, height: 'calc(100vh - 108px)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
||||
className="h-full"
|
||||
bordered={false}
|
||||
bodyStyle={{ padding: 0, height: 'calc(100vh - 66px)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
||||
>
|
||||
{/* 聊天头部 */}
|
||||
{styleState.isMobile ? (
|
||||
|
||||
@@ -217,8 +217,7 @@ const ConfigManager = ({
|
||||
theme="borderless"
|
||||
type="danger"
|
||||
onClick={handleReset}
|
||||
className="!rounded-lg !text-xs !h-7 !px-2"
|
||||
style={{ minWidth: 'auto' }}
|
||||
className="!rounded-full !text-xs !px-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -85,7 +85,8 @@ const DebugPanel = ({
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="!rounded-2xl h-full flex flex-col"
|
||||
className="h-full flex flex-col"
|
||||
bordered={false}
|
||||
bodyStyle={{
|
||||
padding: styleState.isMobile ? '16px' : '24px',
|
||||
height: '100%',
|
||||
@@ -159,7 +160,7 @@ const DebugPanel = ({
|
||||
<TabPane tab={
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap size={16} />
|
||||
{t('响应内容')}
|
||||
{t('响应')}
|
||||
</div>
|
||||
} itemKey="response">
|
||||
<CodeViewer
|
||||
|
||||
@@ -136,18 +136,10 @@ const MessageContent = ({
|
||||
!finalExtractedThinkingContent &&
|
||||
(!finalDisplayableFinalContent || finalDisplayableFinalContent.trim() === '')) {
|
||||
return (
|
||||
<div className={`${className} flex items-center gap-2 sm:gap-4 p-4 sm:p-6 bg-gradient-to-r from-purple-50 to-indigo-50 rounded-xl sm:rounded-2xl`}>
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg">
|
||||
<div className={`${className} flex items-center gap-2 sm:gap-4 bg-gradient-to-r from-purple-50 to-indigo-50 rounded-xl sm:rounded-2xl`}>
|
||||
<div className="w-4 h-4 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg">
|
||||
<Loader2 className="animate-spin text-white" size={styleState.isMobile ? 16 : 20} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Typography.Text strong className="text-gray-800 text-sm sm:text-base">
|
||||
{t('正在思考...')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="text-gray-500 text-xs sm:text-sm">
|
||||
AI 正在分析您的问题
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,8 @@ const SettingsPanel = ({
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`!rounded-2xl h-full flex flex-col ${styleState.isMobile ? 'rounded-none border-none shadow-none' : ''}`}
|
||||
className={`h-full flex flex-col ${styleState.isMobile ? 'rounded-none border-none shadow-none' : ''}`}
|
||||
bordered={false}
|
||||
bodyStyle={{
|
||||
padding: styleState.isMobile ? '24px' : '24px 24px 16px 24px',
|
||||
height: '100%',
|
||||
|
||||
Reference in New Issue
Block a user