refactor: Enhance UI layout and styling with responsive design improvements

This commit is contained in:
1808837298@qq.com
2025-03-10 03:25:02 +08:00
parent 3ed50787b3
commit d9cf0885f1
4 changed files with 376 additions and 52 deletions

View File

@@ -19,7 +19,10 @@ import {
IconNoteMoneyStroked,
IconPriceTag,
IconUser,
IconLanguage
IconLanguage,
IconInfoCircle,
IconCreditCard,
IconTerminal
} from '@douyinfe/semi-icons';
import { Avatar, Button, Dropdown, Layout, Nav, Switch, Tag } from '@douyinfe/semi-ui';
import { stringToColor } from '../helpers/render';
@@ -27,6 +30,73 @@ import Text from '@douyinfe/semi-ui/lib/es/typography/text';
import { StyleContext } from '../context/Style/index.js';
import { StatusContext } from '../context/Status/index.js';
// 自定义顶部栏样式
const headerStyle = {
boxShadow: '0 2px 10px rgba(0, 0, 0, 0.1)',
borderBottom: '1px solid var(--semi-color-border)',
background: 'var(--semi-color-bg-0)',
transition: 'all 0.3s ease',
width: '100%'
};
// 自定义顶部栏按钮样式
const headerItemStyle = {
borderRadius: '4px',
margin: '0 4px',
transition: 'all 0.3s ease'
};
// 自定义顶部栏按钮悬停样式
const headerItemHoverStyle = {
backgroundColor: 'var(--semi-color-primary-light-default)',
color: 'var(--semi-color-primary)'
};
// 自定义顶部栏Logo样式
const logoStyle = {
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '0 10px',
height: '100%'
};
// 自定义顶部栏系统名称样式
const systemNameStyle = {
fontWeight: 'bold',
fontSize: '18px',
background: 'linear-gradient(45deg, var(--semi-color-primary), var(--semi-color-secondary))',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
padding: '0 5px'
};
// 自定义顶部栏按钮图标样式
const headerIconStyle = {
fontSize: '18px',
transition: 'all 0.3s ease'
};
// 自定义头像样式
const avatarStyle = {
margin: '4px',
cursor: 'pointer',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
transition: 'all 0.3s ease'
};
// 自定义下拉菜单样式
const dropdownStyle = {
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
overflow: 'hidden'
};
// 自定义主题切换开关样式
const switchStyle = {
margin: '0 8px'
};
const HeaderBar = () => {
const { t, i18n } = useTranslation();
const [userState, userDispatch] = useContext(UserContext);
@@ -52,16 +122,19 @@ const HeaderBar = () => {
text: t('首页'),
itemKey: 'home',
to: '/',
icon: <IconHome style={headerIconStyle} />,
},
{
text: t('控制台'),
itemKey: 'detail',
to: '/',
icon: <IconTerminal style={headerIconStyle} />,
},
{
text: t('定价'),
itemKey: 'pricing',
to: '/pricing',
icon: <IconPriceTag style={headerIconStyle} />,
},
// Only include the docs button if docsLink exists
...(docsLink ? [{
@@ -69,11 +142,13 @@ const HeaderBar = () => {
itemKey: 'docs',
isExternal: true,
externalLink: docsLink,
icon: <IconHelpCircle style={headerIconStyle} />,
}] : []),
{
text: t('关于'),
itemKey: 'about',
to: '/about',
icon: <IconInfoCircle style={headerIconStyle} />,
},
];
@@ -143,6 +218,9 @@ const HeaderBar = () => {
<Nav
className={'topnav'}
mode={'horizontal'}
style={headerStyle}
itemStyle={headerItemStyle}
hoverStyle={headerItemHoverStyle}
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
const routerMap = {
about: '/about',
@@ -224,11 +302,13 @@ const HeaderBar = () => {
),
}:{
logo: (
<img src={logo} alt='logo' />
<div style={logoStyle}>
<img src={logo} alt='logo' style={{ height: '28px' }} />
</div>
),
text: (
<div style={{ position: 'relative', display: 'inline-block' }}>
{systemName}
<span style={systemNameStyle}>{systemName}</span>
{(isSelfUseMode || isDemoSiteMode) && (
<Tag
color={isSelfUseMode ? 'purple' : 'blue'}
@@ -257,7 +337,7 @@ const HeaderBar = () => {
<Dropdown
position='bottomRight'
render={
<Dropdown.Menu>
<Dropdown.Menu style={dropdownStyle}>
<Dropdown.Item onClick={handleNewYearClick}>
Happy New Year!!!
</Dropdown.Item>
@@ -274,6 +354,7 @@ const HeaderBar = () => {
size={styleState.isMobile?'default':'large'}
checked={theme === 'dark'}
uncheckedText='🌙'
style={switchStyle}
onChange={(checked) => {
setTheme(checked);
}}
@@ -282,7 +363,7 @@ const HeaderBar = () => {
<Dropdown
position='bottomRight'
render={
<Dropdown.Menu>
<Dropdown.Menu style={dropdownStyle}>
<Dropdown.Item
onClick={() => handleLanguageChange('zh')}
type={currentLang === 'zh' ? 'primary' : 'tertiary'}
@@ -300,7 +381,7 @@ const HeaderBar = () => {
>
<Nav.Item
itemKey={'language'}
icon={<IconLanguage />}
icon={<IconLanguage style={headerIconStyle} />}
/>
</Dropdown>
{userState.user ? (
@@ -308,7 +389,7 @@ const HeaderBar = () => {
<Dropdown
position='bottomRight'
render={
<Dropdown.Menu>
<Dropdown.Menu style={dropdownStyle}>
<Dropdown.Item onClick={logout}>{t('退出')}</Dropdown.Item>
</Dropdown.Menu>
}
@@ -316,11 +397,11 @@ const HeaderBar = () => {
<Avatar
size='small'
color={stringToColor(userState.user.username)}
style={{ margin: 4 }}
style={avatarStyle}
>
{userState.user.username[0]}
</Avatar>
{styleState.isMobile?null:<Text>{userState.user.username}</Text>}
{styleState.isMobile?null:<Text style={{ marginLeft: '4px', fontWeight: '500' }}>{userState.user.username}</Text>}
</Dropdown>
</>
) : (
@@ -328,7 +409,7 @@ const HeaderBar = () => {
<Nav.Item
itemKey={'login'}
text={!styleState.isMobile?t('登录'):null}
icon={<IconUser />}
icon={<IconUser style={headerIconStyle} />}
/>
{
// Hide register option in self-use mode
@@ -336,7 +417,7 @@ const HeaderBar = () => {
<Nav.Item
itemKey={'register'}
text={t('注册')}
icon={<IconKey />}
icon={<IconKey style={headerIconStyle} />}
/>
)
}

View File

@@ -62,20 +62,57 @@ 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={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<Header>
<Header style={{
padding: 0,
height: 'auto',
lineHeight: 'normal',
position: 'fixed',
width: '100%',
top: 0,
zIndex: 100,
boxShadow: '0 1px 6px rgba(0, 0, 0, 0.08)'
}}>
<HeaderBar />
</Header>
<Layout style={{ flex: 1, overflow: 'hidden' }}>
<Sider>
{styleState.showSider ? <SiderBar /> : null}
</Sider>
<Layout>
<Layout style={{ marginTop: '56px', height: 'calc(100vh - 56px)', overflow: 'hidden' }}>
{styleState.showSider && (
<Sider style={{
height: 'calc(100vh - 56px)',
position: 'fixed',
left: 0,
top: '56px',
zIndex: 90,
overflowY: 'auto',
overflowX: 'hidden',
width: 'auto',
background: 'transparent',
boxShadow: 'none',
border: 'none',
paddingRight: '5px'
}}>
<SiderBar />
</Sider>
)}
<Layout style={{
marginLeft: styleState.showSider ? (isSidebarCollapsed ? '60px' : '200px') : '0',
transition: 'margin-left 0.3s ease'
}}>
<Content
style={{ overflowY: 'auto', padding: styleState.shouldInnerPadding? '24px': '0' }}
style={{
height: '100%',
overflowY: 'auto',
padding: styleState.shouldInnerPadding? '24px': '0'
}}
>
<App />
</Content>

View File

@@ -1,5 +1,5 @@
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Link, useNavigate, useLocation } from 'react-router-dom';
import { UserContext } from '../context/User';
import { StatusContext } from '../context/Status';
import { useTranslation } from 'react-i18next';
@@ -34,7 +34,34 @@ import { stringToColor } from '../helpers/render.js';
import { useSetTheme, useTheme } from '../context/Theme/index.js';
import { StyleContext } from '../context/Style/index.js';
// HeaderBar Buttons
// 自定义侧边栏按钮样式
const navItemStyle = {
borderRadius: '6px',
margin: '4px 8px',
transition: 'all 0.3s ease'
};
// 自定义侧边栏按钮悬停样式
const navItemHoverStyle = {
backgroundColor: 'var(--semi-color-primary-light-default)',
color: 'var(--semi-color-primary)'
};
// 自定义侧边栏按钮选中样式
const navItemSelectedStyle = {
backgroundColor: 'var(--semi-color-primary-light-default)',
color: 'var(--semi-color-primary)',
fontWeight: '600'
};
// 自定义图标样式
const iconStyle = (itemKey, selectedKeys) => {
return {
fontSize: '18px',
color: selectedKeys.includes(itemKey) ? 'var(--semi-color-primary)' : 'var(--semi-color-text-2)',
transition: 'all 0.3s ease'
};
};
const SiderBar = () => {
const { t } = useTranslation();
@@ -46,8 +73,30 @@ const SiderBar = () => {
const [selectedKeys, setSelectedKeys] = useState(['home']);
const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
const [chatItems, setChatItems] = useState([]);
const [openedKeys, setOpenedKeys] = useState([]);
const theme = useTheme();
const setTheme = useSetTheme();
const location = useLocation();
// 预先计算所有可能的图标样式
const allItemKeys = useMemo(() => {
const keys = ['home', 'channel', 'token', 'redemption', 'topup', 'user', 'log', 'midjourney',
'setting', 'about', 'chat', 'detail', 'pricing', 'task', 'playground', 'personal'];
// 添加聊天项的keys
for (let i = 0; i < chatItems.length; i++) {
keys.push('chat' + i);
}
return keys;
}, [chatItems]);
// 使用useMemo一次性计算所有图标样式
const iconStyles = useMemo(() => {
const styles = {};
allItemKeys.forEach(key => {
styles[key] = iconStyle(key, selectedKeys);
});
return styles;
}, [allItemKeys, selectedKeys]);
const routerMap = {
home: '/',
@@ -190,11 +239,14 @@ const SiderBar = () => {
);
useEffect(() => {
let localKey = window.location.pathname.split('/')[1];
if (localKey === '') {
localKey = 'home';
const currentPath = location.pathname;
const matchingKey = Object.keys(routerMap).find(key => routerMap[key] === currentPath);
if (matchingKey) {
setSelectedKeys([matchingKey]);
} else if (currentPath.startsWith('/chat/')) {
setSelectedKeys(['chat']);
}
setSelectedKeys([localKey]);
let chats = localStorage.getItem('chats');
if (chats) {
@@ -222,7 +274,7 @@ const SiderBar = () => {
}
setIsCollapsed(localStorage.getItem('default_collapse_sidebar') === 'true');
}, []);
}, [location.pathname]);
// Custom divider style
const dividerStyle = {
@@ -235,21 +287,54 @@ const SiderBar = () => {
padding: '8px 16px',
color: 'var(--semi-color-text-2)',
fontSize: '12px',
fontWeight: 'normal',
fontWeight: 'bold',
textTransform: 'uppercase',
letterSpacing: '0.5px',
};
return (
<>
<Nav
style={{ maxWidth: 200, height: '100%' }}
className="custom-sidebar-nav"
style={{
width: isCollapsed ? '60px' : '200px',
height: '100%',
boxShadow: '0 1px 6px rgba(0, 0, 0, 0.08)',
borderRight: '1px solid var(--semi-color-border)',
background: 'var(--semi-color-bg-0)',
borderRadius: '0 8px 8px 0',
transition: 'all 0.3s ease',
position: 'relative',
zIndex: 95
}}
defaultIsCollapsed={
localStorage.getItem('default_collapse_sidebar') === 'true'
}
isCollapsed={isCollapsed}
onCollapseChange={(collapsed) => {
setIsCollapsed(collapsed);
localStorage.setItem('default_collapse_sidebar', collapsed);
// 始终保持侧边栏显示,只是宽度不同
styleDispatch({ type: 'SET_SIDER', payload: true });
// 确保在收起侧边栏时有选中的项目,避免不必要的计算
if (selectedKeys.length === 0) {
const currentPath = location.pathname;
const matchingKey = Object.keys(routerMap).find(key => routerMap[key] === currentPath);
if (matchingKey) {
setSelectedKeys([matchingKey]);
} else if (currentPath.startsWith('/chat/')) {
setSelectedKeys(['chat']);
} else {
setSelectedKeys(['home']); // 默认选中首页
}
}
}}
selectedKeys={selectedKeys}
itemStyle={navItemStyle}
hoverStyle={navItemHoverStyle}
selectedStyle={navItemSelectedStyle}
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
let chats = localStorage.getItem('chats');
if (chats) {
@@ -284,8 +369,18 @@ const SiderBar = () => {
} else {
styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
}
// 如果点击的是已经展开的子菜单的父项,则收起子菜单
if (openedKeys.includes(key.itemKey)) {
setOpenedKeys(openedKeys.filter(k => k !== key.itemKey));
}
setSelectedKeys([key.itemKey]);
}}
openKeys={openedKeys}
onOpenChange={(data) => {
setOpenedKeys(data.openKeys);
}}
>
{/* Chat Section - Only show if there are chat items */}
{chatMenuItems.map((item) => {
@@ -295,7 +390,7 @@ const SiderBar = () => {
key={item.itemKey}
itemKey={item.itemKey}
text={item.text}
icon={item.icon}
icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })}
>
{item.items.map((subItem) => (
<Nav.Item
@@ -312,13 +407,12 @@ const SiderBar = () => {
key={item.itemKey}
itemKey={item.itemKey}
text={item.text}
icon={item.icon}
icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })}
/>
);
}
})}
{/* Divider */}
<Divider style={dividerStyle} />
@@ -329,11 +423,30 @@ const SiderBar = () => {
key={item.itemKey}
itemKey={item.itemKey}
text={item.text}
icon={item.icon}
icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })}
className={item.className}
/>
))}
{isAdmin() && (
<>
{/* Divider */}
<Divider style={dividerStyle} />
{/* Admin Section */}
{!isCollapsed && <div style={groupLabelStyle}>{t('管理员')}</div>}
{adminItems.map((item) => (
<Nav.Item
key={item.itemKey}
itemKey={item.itemKey}
text={item.text}
icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })}
className={item.className}
/>
))}
</>
)}
{/* Divider */}
<Divider style={dividerStyle} />
@@ -344,31 +457,18 @@ const SiderBar = () => {
key={item.itemKey}
itemKey={item.itemKey}
text={item.text}
icon={item.icon}
icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })}
className={item.className}
/>
))}
{isAdmin() && (
<>
{/* Divider */}
<Divider style={dividerStyle} />
{/* Admin Section */}
{adminItems.map((item) => (
<Nav.Item
key={item.itemKey}
itemKey={item.itemKey}
text={item.text}
icon={item.icon}
className={item.className}
/>
))}
</>
)}
<Nav.Footer
collapseButton={true}
style={{
borderTop: '1px solid var(--semi-color-border)',
padding: '12px 0',
marginTop: 'auto'
}}
collapseText={(collapsed)=>
{
if(collapsed){

View File

@@ -1,7 +1,7 @@
body {
margin: 0;
padding-top: 55px;
overflow-y: scroll;
padding-top: 0;
overflow: hidden;
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei',
sans-serif;
-webkit-font-smoothing: antialiased;
@@ -15,6 +15,7 @@ body {
#root {
height: 100vh;
flex-direction: column;
overflow: hidden;
}
#root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li > span{
@@ -29,6 +30,15 @@ body {
/*.semi-navigation-sub-wrap .semi-navigation-sub-title, .semi-navigation-item {*/
/* padding: 0 0;*/
/*}*/
.topnav {
padding: 0 8px;
}
.topnav .semi-navigation-item {
margin: 0 1px;
padding: 0 4px;
}
.topnav .semi-navigation-list-wrapper {
max-width: calc(55vw - 20px);
overflow-x: auto;
@@ -120,6 +130,38 @@ code {
margin-bottom: 0;
}
/* 自定义侧边栏按钮悬停效果 */
.semi-navigation-item:hover {
transform: translateX(2px);
box-shadow: 0 2px 8px rgba(var(--semi-color-primary-rgb), 0.2);
}
/* 自定义侧边栏按钮选中效果 */
.semi-navigation-item-selected {
position: relative;
overflow: hidden;
}
.semi-navigation-item-selected::before {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 4px;
background-color: var(--semi-color-primary);
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateY(-100%);
}
to {
transform: translateY(0);
}
}
.semi-navigation-vertical {
/*flex: 0 0 auto;*/
/*display: flex;*/
@@ -147,3 +189,67 @@ code {
display: none !important;
}
}
/* 顶部栏样式 */
.topnav {
padding: 0 16px;
}
.topnav .semi-navigation-item {
border-radius: 4px;
margin: 0 2px;
transition: all 0.3s ease;
}
.topnav .semi-navigation-item:hover {
background-color: var(--semi-color-primary-light-default);
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(var(--semi-color-primary-rgb), 0.2);
}
.topnav .semi-navigation-item-selected {
background-color: var(--semi-color-primary-light-default);
color: var(--semi-color-primary);
font-weight: 600;
}
/* 顶部栏文本样式 */
.header-bar-text {
color: var(--semi-color-text-0);
font-weight: 500;
transition: all 0.3s ease;
}
.header-bar-text:hover {
color: var(--semi-color-primary);
}
/* 自定义滚动条样式 */
.semi-layout-content::-webkit-scrollbar,
.semi-sider::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.semi-layout-content::-webkit-scrollbar-thumb,
.semi-sider::-webkit-scrollbar-thumb {
background: var(--semi-color-tertiary-light-default);
border-radius: 3px;
}
.semi-layout-content::-webkit-scrollbar-thumb:hover,
.semi-sider::-webkit-scrollbar-thumb:hover {
background: var(--semi-color-tertiary);
}
.semi-layout-content::-webkit-scrollbar-track,
.semi-sider::-webkit-scrollbar-track {
background: transparent;
}
/* Custom sidebar shadow */
.custom-sidebar-nav {
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;
-webkit-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;
-moz-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;
}