♻️ 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:
Apple\Apple
2025-06-02 04:16:48 +08:00
parent 90d4e0e41c
commit cc3f3cf033
15 changed files with 261 additions and 190 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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',

View File

@@ -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));

View File

@@ -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 ? (

View File

@@ -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>

View File

@@ -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

View File

@@ -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>
);
}

View File

@@ -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%',