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;
+}
+
/* ============================================================
空状态
============================================================ */