Browse Source

✨ 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.
t0ng7u 7 months ago
parent
commit
eb59f9c75d
2 changed files with 74 additions and 19 deletions
  1. 49 14
      web/src/components/layout/HeaderBar.js
  2. 25 5
      web/src/pages/Detail/index.js

+ 49 - 14
web/src/components/layout/HeaderBar.js

@@ -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 = () => {
     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 = () => {
               />
               />
             </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.Image className="h-7 md:h-8 !rounded-full" style={{ width: 32, height: 32 }} />
-              ) : (
+              <Skeleton
+                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.Title style={{ width: 120, height: 24 }} />
-                  ) : (
-                    <Typography.Title heading={4} className="!text-lg !font-semibold !mb-0 
-                                                          bg-gradient-to-r from-blue-500 to-purple-500 dark:from-blue-400 dark:to-purple-400
-                                                          bg-clip-text text-transparent">
+                  <Skeleton
+                    loading={isLoading}
+                    active
+                    placeholder={
+                      <Skeleton.Title
+                        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'}

+ 25 - 5
web/src/pages/Detail/index.js

@@ -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) => {
   // ========== 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) => {
 
 
   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) => {
         showError(message);
         showError(message);
       }
       }
     } finally {
     } finally {
-      setLoading(false);
+      const elapsed = Date.now() - startTime;
+      const remainingTime = Math.max(0, 500 - elapsed);
+      setTimeout(() => {
+        setLoading(false);
+      }, remainingTime);
     }
     }
   }, [start_timestamp, end_timestamp, username, dataExportDefaultTime, isAdminUser]);
   }, [start_timestamp, end_timestamp, username, dataExportDefaultTime, isAdminUser]);
 
 
@@ -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)}