feat(web): G1 骨架屏取代載入中 + S8 完整提交 — Sprint 5R
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
- G1: PulseSkeleton + CardSkeleton 元件 - 首頁所有 LobsterLoading 替換為 PulseSkeleton/CardSkeleton - Tab 2/4 載入狀態用 CardSkeleton - 活躍事件載入用 PulseSkeleton Sprint 5R Phase 1B+1C 全部完成: S1(KPI卡片) S2(FlowPipeline OpenClaw) S3(AI提案) S4(環形圖) S5(時間線) S6(Terminal) S7(待審批) S8(拓撲群組+主機) S9(AI模型) S10(監控3×2) S11(Tab修復) S12(頁面修復) G1(骨架屏) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,7 @@ import { OpenClawPanel } from '@/components/ai/openclaw-panel'
|
||||
import { HostGrid, type HostInfo, type HostService } from '@/components/infra/host-grid'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { PageTabs, type TabConfig } from '@/components/layout/page-tabs'
|
||||
import { LobsterLoading } from '@/components/shared/lobster-loading'
|
||||
import { PulseSkeleton, CardSkeleton } from '@/components/shared/pulse-skeleton'
|
||||
import { ServiceTopology } from '@/components/topology'
|
||||
import { BarChart3, Flame, Telescope, FlaskConical, Activity, GitBranch } from 'lucide-react'
|
||||
import { DispositionMini } from '@/components/shared/disposition-mini'
|
||||
@@ -56,7 +56,7 @@ function AlertsAndApprovalsTab() {
|
||||
}).finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
if (loading) return <LobsterLoading />
|
||||
if (loading) return <CardSkeleton lines={4} />
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 20, padding: '16px 20px', flex: 1, overflow: 'hidden' }}>
|
||||
@@ -172,7 +172,7 @@ function DispositionTab() {
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
if (loading) return <LobsterLoading />
|
||||
if (loading) return <CardSkeleton lines={4} />
|
||||
|
||||
const s = data?.summary
|
||||
if (!s || s.total === 0) return (
|
||||
@@ -730,7 +730,7 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
|
||||
<div style={{ padding: '14px' }}>
|
||||
{isIncidentsLoading ? (
|
||||
<LobsterLoading size="sm" />
|
||||
<PulseSkeleton count={3} height={12} />
|
||||
) : incidentsError ? (
|
||||
<div style={{ padding: 16, fontSize: 13, color: '#cc2200' }}>{incidentsError}</div>
|
||||
) : (incidents?.length ?? 0) === 0 ? (
|
||||
|
||||
50
apps/web/src/components/shared/pulse-skeleton.tsx
Normal file
50
apps/web/src/components/shared/pulse-skeleton.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* PulseSkeleton — 脈動骨架屏 (取代「載入中...」文字)
|
||||
* Sprint 5R G1: Gemini 建議 — AI 呼吸感知
|
||||
* @created 2026-04-09 Claude Opus 4.6 Asia/Taipei
|
||||
*/
|
||||
|
||||
export function PulseSkeleton({ width = '100%', height = 16, borderRadius = 4, count = 1 }: {
|
||||
width?: string | number
|
||||
height?: number
|
||||
borderRadius?: number
|
||||
count?: number
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
@keyframes pulse-skeleton {
|
||||
0%, 100% { opacity: 0.4; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
`}</style>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<div key={i} style={{
|
||||
width, height, borderRadius,
|
||||
background: '#e8e5dc',
|
||||
animation: 'pulse-skeleton 1.5s ease-in-out infinite',
|
||||
animationDelay: `${i * 0.15}s`,
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** 卡片級骨架屏 — 模擬 .card 結構 */
|
||||
export function CardSkeleton({ lines = 3 }: { lines?: number }) {
|
||||
return (
|
||||
<div style={{
|
||||
background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 10,
|
||||
overflow: 'hidden', padding: 14,
|
||||
}}>
|
||||
<PulseSkeleton width="40%" height={14} borderRadius={4} />
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<PulseSkeleton count={lines} height={12} borderRadius={3} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user