feat(web): G1 骨架屏取代載入中 + S8 完整提交 — Sprint 5R
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:
OG T
2026-04-09 18:09:26 +08:00
parent 09c6eb3358
commit 49a15e1ac9
2 changed files with 54 additions and 4 deletions

View File

@@ -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 ? (

View 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>
)
}