Files
new-api/web/src/components/layout/components/SkeletonWrapper.jsx
t0ng7u d9f37d16f7 🎨 fix: sidebar skeleton background and icon spacing consistency
- Set sidebar skeleton background to use theme variable (--semi-color-bg-0) instead of hardcoded white for better dark mode compatibility
- Apply consistent background to both collapsed and expanded skeleton states
- Ensure sidebar container uses theme background when skeleton is loading
- Remove duplicate margin-right classes from skeleton wrapper components that conflicted with CSS definitions
- Simplify navigation text structure by removing unnecessary div wrappers to improve text truncation
- Add proper text layout styles for better truncation handling when menu items have long names
- Standardize icon-to-text spacing across all sidebar navigation items
2025-09-02 17:07:01 +08:00

395 lines
10 KiB
JavaScript

/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import { Skeleton } from '@douyinfe/semi-ui';
const SkeletonWrapper = ({
loading = false,
type = 'text',
count = 1,
width = 60,
height = 16,
isMobile = false,
className = '',
collapsed = false,
showAdmin = true,
children,
...props
}) => {
if (!loading) {
return children;
}
// 导航链接骨架屏
const renderNavigationSkeleton = () => {
const skeletonLinkClasses = isMobile
? 'flex items-center gap-1 p-1 w-full rounded-md'
: 'flex items-center gap-1 p-2 rounded-md';
return Array(count)
.fill(null)
.map((_, index) => (
<div key={index} className={skeletonLinkClasses}>
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Title
active
style={{ width: isMobile ? 40 : width, height }}
/>
}
/>
</div>
));
};
// 用户区域骨架屏 (头像 + 文本)
const renderUserAreaSkeleton = () => {
return (
<div
className={`flex items-center p-1 rounded-full bg-semi-color-fill-0 dark:bg-semi-color-fill-1 ${className}`}
>
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Avatar active size='extra-small' className='shadow-sm' />
}
/>
<div className='ml-1.5 mr-1'>
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Title
active
style={{ width: isMobile ? 15 : width, height: 12 }}
/>
}
/>
</div>
</div>
);
};
// Logo图片骨架屏
const renderImageSkeleton = () => {
return (
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Image
active
className={`absolute inset-0 !rounded-full ${className}`}
style={{ width: '100%', height: '100%' }}
/>
}
/>
);
};
// 系统名称骨架屏
const renderTitleSkeleton = () => {
return (
<Skeleton
loading={true}
active
placeholder={<Skeleton.Title active style={{ width, height: 24 }} />}
/>
);
};
// 通用文本骨架屏
const renderTextSkeleton = () => {
return (
<div className={className}>
<Skeleton
loading={true}
active
placeholder={<Skeleton.Title active style={{ width, height }} />}
/>
</div>
);
};
// 按钮骨架屏(支持圆角)
const renderButtonSkeleton = () => {
return (
<div className={className}>
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Title
active
style={{ width, height, borderRadius: 9999 }}
/>
}
/>
</div>
);
};
// 侧边栏导航项骨架屏 (图标 + 文本)
const renderSidebarNavItemSkeleton = () => {
return Array(count)
.fill(null)
.map((_, index) => (
<div
key={index}
className={`flex items-center p-2 mb-1 rounded-md ${className}`}
>
{/* 图标骨架屏 */}
<div className='sidebar-icon-container flex-shrink-0'>
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Avatar active size='extra-small' shape='square' />
}
/>
</div>
{/* 文本骨架屏 */}
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Title
active
style={{ width: width || 80, height: height || 14 }}
/>
}
/>
</div>
));
};
// 侧边栏组标题骨架屏
const renderSidebarGroupTitleSkeleton = () => {
return (
<div className={`mb-2 ${className}`}>
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Title
active
style={{ width: width || 60, height: height || 12 }}
/>
}
/>
</div>
);
};
// 完整侧边栏骨架屏 - 1:1 还原,去重实现
const renderSidebarSkeleton = () => {
const NAV_WIDTH = 164;
const NAV_HEIGHT = 30;
const COLLAPSED_WIDTH = 44;
const COLLAPSED_HEIGHT = 44;
const ICON_SIZE = 16;
const TITLE_HEIGHT = 12;
const TEXT_HEIGHT = 16;
const renderIcon = () => (
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Avatar
active
shape='square'
style={{ width: ICON_SIZE, height: ICON_SIZE }}
/>
}
/>
);
const renderLabel = (labelWidth) => (
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Title
active
style={{ width: labelWidth, height: TEXT_HEIGHT }}
/>
}
/>
);
const NavRow = ({ labelWidth }) => (
<div
className='flex items-center p-2 mb-1 rounded-md'
style={{
width: `${NAV_WIDTH}px`,
height: `${NAV_HEIGHT}px`,
margin: '3px 8px',
}}
>
<div className='sidebar-icon-container flex-shrink-0'>
{renderIcon()}
</div>
{renderLabel(labelWidth)}
</div>
);
const CollapsedRow = ({ keyPrefix, index }) => (
<div
key={`${keyPrefix}-${index}`}
className='flex items-center justify-center'
style={{
width: `${COLLAPSED_WIDTH}px`,
height: `${COLLAPSED_HEIGHT}px`,
margin: '0 8px 4px 8px',
}}
>
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Avatar
active
shape='square'
style={{ width: ICON_SIZE, height: ICON_SIZE }}
/>
}
/>
</div>
);
if (collapsed) {
return (
<div className={`w-full ${className}`} style={{ paddingTop: '12px' }}>
{Array(2)
.fill(null)
.map((_, i) => (
<CollapsedRow keyPrefix='c-chat' index={i} />
))}
{Array(5)
.fill(null)
.map((_, i) => (
<CollapsedRow keyPrefix='c-console' index={i} />
))}
{Array(2)
.fill(null)
.map((_, i) => (
<CollapsedRow keyPrefix='c-personal' index={i} />
))}
{Array(5)
.fill(null)
.map((_, i) => (
<CollapsedRow keyPrefix='c-admin' index={i} />
))}
</div>
);
}
const sections = [
{ key: 'chat', titleWidth: 32, itemWidths: [54, 32], wrapper: 'section' },
{ key: 'console', titleWidth: 48, itemWidths: [64, 64, 64, 64, 64] },
{ key: 'personal', titleWidth: 64, itemWidths: [64, 64] },
...(showAdmin
? [{ key: 'admin', titleWidth: 48, itemWidths: [64, 64, 80, 64, 64] }]
: []),
];
return (
<div className={`w-full ${className}`} style={{ paddingTop: '12px' }}>
{sections.map((sec, idx) => (
<React.Fragment key={sec.key}>
{sec.wrapper === 'section' ? (
<div className='sidebar-section'>
<div
className='sidebar-group-label'
style={{ padding: '4px 15px 8px' }}
>
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Title
active
style={{ width: sec.titleWidth, height: TITLE_HEIGHT }}
/>
}
/>
</div>
{sec.itemWidths.map((w, i) => (
<NavRow key={`${sec.key}-${i}`} labelWidth={w} />
))}
</div>
) : (
<div>
<div
className='sidebar-group-label'
style={{ padding: '4px 15px 8px' }}
>
<Skeleton
loading={true}
active
placeholder={
<Skeleton.Title
active
style={{ width: sec.titleWidth, height: TITLE_HEIGHT }}
/>
}
/>
</div>
{sec.itemWidths.map((w, i) => (
<NavRow key={`${sec.key}-${i}`} labelWidth={w} />
))}
</div>
)}
</React.Fragment>
))}
</div>
);
};
// 根据类型渲染不同的骨架屏
switch (type) {
case 'navigation':
return renderNavigationSkeleton();
case 'userArea':
return renderUserAreaSkeleton();
case 'image':
return renderImageSkeleton();
case 'title':
return renderTitleSkeleton();
case 'sidebarNavItem':
return renderSidebarNavItemSkeleton();
case 'sidebarGroupTitle':
return renderSidebarGroupTitleSkeleton();
case 'sidebar':
return renderSidebarSkeleton();
case 'button':
return renderButtonSkeleton();
case 'text':
default:
return renderTextSkeleton();
}
};
export default SkeletonWrapper;