diff --git a/apps/web/src/pages/assets/AssetLibrary.tsx b/apps/web/src/pages/assets/AssetLibrary.tsx index e95ec7f47..aebf04b63 100644 --- a/apps/web/src/pages/assets/AssetLibrary.tsx +++ b/apps/web/src/pages/assets/AssetLibrary.tsx @@ -16,6 +16,7 @@ import { CheckOutlined, DeleteOutlined, ExperimentOutlined, + LoadingOutlined, } from "@ant-design/icons"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { @@ -185,16 +186,31 @@ const StatusPill: React.FC<{ status: StatusType; label: string }> = ({ {label} ); +/* ============================================================ + * SkeletonCard — 骨架屏卡片(素材列表加载时占位) + * ============================================================ */ +const SkeletonCard: React.FC = () => ( +
+
+
+
+
+
+
+
+); + /* ============================================================ * AssetCard 组件 * ============================================================ */ const AssetCard: React.FC<{ asset: AssetItem; selected: boolean; + diagnosing?: boolean; onToggle: () => void; onDiagnose: () => void; onPlay: () => void; -}> = ({ asset, selected, onToggle, onDiagnose, onPlay }) => ( +}> = ({ asset, selected, diagnosing, onToggle, onDiagnose, onPlay }) => (
{asset.duration}}
@@ -338,6 +355,9 @@ const AssetLibrary: React.FC = () => { /* 视频播放 */ const [playingAsset, setPlayingAsset] = useState(null); + /* 诊断中状态 — 记录正在诊断的素材 ID */ + const [diagnosingId, setDiagnosingId] = useState(null); + /* 派生数据 */ const filteredAssets = useMemo(() => { let list = assets; @@ -449,8 +469,9 @@ const AssetLibrary: React.FC = () => { } }; - /* 诊断 — 调用真实 API */ + /* 诊断 — 调用真实 API,带 loading 状态 */ const handleDiagnose = async (asset: AssetItem) => { + setDiagnosingId(asset.id); try { const result = await getAssetDiagnosis(asset.id); const score = result.readiness_score ?? "-"; @@ -458,6 +479,8 @@ const AssetLibrary: React.FC = () => { queryClient.invalidateQueries({ queryKey: ["assets"] }); } catch { message.error(`"${asset.name}" 诊断失败`); + } finally { + setDiagnosingId(null); } }; @@ -647,9 +670,10 @@ const AssetLibrary: React.FC = () => { {/* 素材网格 */} {assetsLoading ? ( -
-
⏳
-

加载素材中...

+
+ {Array.from({ length: 8 }).map((_, i) => ( + + ))}
) : assetsError ? (
@@ -670,6 +694,7 @@ const AssetLibrary: React.FC = () => { key={asset.id} asset={asset} selected={selectedIds.has(asset.id)} + diagnosing={diagnosingId === asset.id} onToggle={() => toggleSelect(asset.id)} onDiagnose={() => handleDiagnose(asset)} onPlay={() => setPlayingAsset(asset)} diff --git a/apps/web/src/pages/assets/assets.css b/apps/web/src/pages/assets/assets.css index a51ad36b5..3dd72c6d9 100644 --- a/apps/web/src/pages/assets/assets.css +++ b/apps/web/src/pages/assets/assets.css @@ -357,6 +357,39 @@ color: var(--info-color); } +/* ============================================================ + 骨架屏(素材列表加载占位) + ============================================================ */ +@keyframes xx-skeleton-pulse { + 0% { opacity: 1; } + 50% { opacity: 0.4; } + 100% { opacity: 1; } +} + +.xx-asset-skeleton { + pointer-events: none; +} + +.xx-asset-skeleton .xx-asset-thumb { + background: var(--bg-tertiary, #e5e7eb); +} + +.xx-skeleton-pulse { + animation: xx-skeleton-pulse 1.5s ease-in-out infinite; +} + +.xx-skeleton-line { + height: 14px; + border-radius: var(--radius-xs); + background: var(--bg-tertiary, #e5e7eb); +} + +/* 诊断按钮 loading 态 */ +.xx-asset-diagnose-btn-loading { + opacity: 0.7; + cursor: not-allowed; +} + /* ============================================================ 空状态 ============================================================ */