Commit 7c17f380 by t0ng7u

feat(ui): enhance loading states and fix layout issues

- Fix uptime service card bottom spacing by removing flex layout
- Replace IconRotate with IconSend for request count to better represent semantic meaning
- Add skeleton loading placeholders for all dashboard statistics with 500ms minimum duration
- Unify avgRPM and avgTPM calculation with consistent NaN handling
- Standardize skeleton usage across HeaderBar and Detail components with active animations
- Remove unnecessary empty wrapper elements in skeleton implementations
- Remove gradient styling from system name in header

The changes improve user experience with consistent loading states, better semantic icons,
and eliminate visual layout issues in the dashboard cards.
parent 42f38b82
...@@ -221,7 +221,16 @@ const HeaderBar = () => { ...@@ -221,7 +221,16 @@ const HeaderBar = () => {
.fill(null) .fill(null)
.map((_, index) => ( .map((_, index) => (
<div key={index} className={skeletonLinkClasses}> <div key={index} className={skeletonLinkClasses}>
<Skeleton.Title style={{ width: isMobileView ? 100 : 60, height: 16 }} /> <Skeleton
loading={true}
active
placeholder={
<Skeleton.Title
active
style={{ width: isMobileView ? 100 : 60, height: 16 }}
/>
}
/>
</div> </div>
)); ));
} }
...@@ -272,9 +281,22 @@ const HeaderBar = () => { ...@@ -272,9 +281,22 @@ const HeaderBar = () => {
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex items-center p-1 rounded-full bg-semi-color-fill-0 dark:bg-semi-color-fill-1"> <div className="flex items-center p-1 rounded-full bg-semi-color-fill-0 dark:bg-semi-color-fill-1">
<Skeleton.Avatar size="extra-small" className="shadow-sm" /> <Skeleton
loading={true}
active
placeholder={<Skeleton.Avatar active size="extra-small" className="shadow-sm" />}
/>
<div className="ml-1.5 mr-1"> <div className="ml-1.5 mr-1">
<Skeleton.Title style={{ width: styleState.isMobile ? 15 : 50, height: 12 }} /> <Skeleton
loading={true}
active
placeholder={
<Skeleton.Title
active
style={{ width: styleState.isMobile ? 15 : 50, height: 12 }}
/>
}
/>
</div> </div>
</div> </div>
); );
...@@ -448,22 +470,35 @@ const HeaderBar = () => { ...@@ -448,22 +470,35 @@ const HeaderBar = () => {
/> />
</div> </div>
<Link to="/" onClick={() => handleNavLinkClick('home')} className="flex items-center gap-2 group ml-2"> <Link to="/" onClick={() => handleNavLinkClick('home')} className="flex items-center gap-2 group ml-2">
{isLoading ? ( <Skeleton
<Skeleton.Image className="h-7 md:h-8 !rounded-full" style={{ width: 32, height: 32 }} /> loading={isLoading}
) : ( active
placeholder={
<Skeleton.Image
active
className="h-7 md:h-8 !rounded-full"
style={{ width: 32, height: 32 }}
/>
}
>
<img src={logo} alt="logo" className="h-7 md:h-8 transition-transform duration-300 ease-in-out group-hover:scale-105 rounded-full" /> <img src={logo} alt="logo" className="h-7 md:h-8 transition-transform duration-300 ease-in-out group-hover:scale-105 rounded-full" />
)} </Skeleton>
<div className="hidden md:flex items-center gap-2"> <div className="hidden md:flex items-center gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isLoading ? ( <Skeleton
<Skeleton.Title style={{ width: 120, height: 24 }} /> loading={isLoading}
) : ( active
<Typography.Title heading={4} className="!text-lg !font-semibold !mb-0 placeholder={
bg-gradient-to-r from-blue-500 to-purple-500 dark:from-blue-400 dark:to-purple-400 <Skeleton.Title
bg-clip-text text-transparent"> active
style={{ width: 120, height: 24 }}
/>
}
>
<Typography.Title heading={4} className="!text-lg !font-semibold !mb-0">
{systemName} {systemName}
</Typography.Title> </Typography.Title>
)} </Skeleton>
{(isSelfUseMode || isDemoSiteMode) && !isLoading && ( {(isSelfUseMode || isDemoSiteMode) && !isLoading && (
<Tag <Tag
color={isSelfUseMode ? 'purple' : 'blue'} color={isSelfUseMode ? 'purple' : 'blue'}
......
...@@ -18,7 +18,8 @@ import { ...@@ -18,7 +18,8 @@ import {
Timeline, Timeline,
Collapse, Collapse,
Progress, Progress,
Divider Divider,
Skeleton
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
IconRefresh, IconRefresh,
...@@ -449,7 +450,7 @@ const Detail = (props) => { ...@@ -449,7 +450,7 @@ const Detail = (props) => {
// ========== Hooks - Memoized Values ========== // ========== Hooks - Memoized Values ==========
const performanceMetrics = useMemo(() => { const performanceMetrics = useMemo(() => {
const timeDiff = (Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000; const timeDiff = (Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000;
const avgRPM = (times / timeDiff).toFixed(3); const avgRPM = isNaN(times / timeDiff) ? '0' : (times / timeDiff).toFixed(3);
const avgTPM = isNaN(consumeTokens / timeDiff) ? '0' : (consumeTokens / timeDiff).toFixed(3); const avgTPM = isNaN(consumeTokens / timeDiff) ? '0' : (consumeTokens / timeDiff).toFixed(3);
return { avgRPM, avgTPM, timeDiff }; return { avgRPM, avgTPM, timeDiff };
...@@ -627,6 +628,7 @@ const Detail = (props) => { ...@@ -627,6 +628,7 @@ const Detail = (props) => {
const loadQuotaData = useCallback(async () => { const loadQuotaData = useCallback(async () => {
setLoading(true); setLoading(true);
const startTime = Date.now();
try { try {
let url = ''; let url = '';
let localStartTimestamp = Date.parse(start_timestamp) / 1000; let localStartTimestamp = Date.parse(start_timestamp) / 1000;
...@@ -654,7 +656,11 @@ const Detail = (props) => { ...@@ -654,7 +656,11 @@ const Detail = (props) => {
showError(message); showError(message);
} }
} finally { } finally {
const elapsed = Date.now() - startTime;
const remainingTime = Math.max(0, 500 - elapsed);
setTimeout(() => {
setLoading(false); setLoading(false);
}, remainingTime);
} }
}, [start_timestamp, end_timestamp, username, dataExportDefaultTime, isAdminUser]); }, [start_timestamp, end_timestamp, username, dataExportDefaultTime, isAdminUser]);
...@@ -1202,10 +1208,24 @@ const Detail = (props) => { ...@@ -1202,10 +1208,24 @@ const Detail = (props) => {
</Avatar> </Avatar>
<div> <div>
<div className="text-xs text-gray-500">{item.title}</div> <div className="text-xs text-gray-500">{item.title}</div>
<div className="text-lg font-semibold">{item.value}</div> <div className="text-lg font-semibold">
<Skeleton
loading={loading}
active
placeholder={
<Skeleton.Paragraph
active
rows={1}
style={{ width: '65px', height: '24px', marginTop: '4px' }}
/>
}
>
{item.value}
</Skeleton>
</div>
</div> </div>
</div> </div>
{item.trendData && item.trendData.length > 0 && ( {(loading || (item.trendData && item.trendData.length > 0)) && (
<div className="w-24 h-10"> <div className="w-24 h-10">
<VChart <VChart
spec={getTrendSpec(item.trendData, item.trendColor)} spec={getTrendSpec(item.trendData, item.trendColor)}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment