feat(assets): 素材列表加载骨架屏 + 诊断按钮 loading 态
Deploy / Staging E2E Tests (push) Failing after 108h12m17s
Deploy / Deploy Staging (push) Failing after 108h22m20s
CI/CD Pipeline / Frontend Lint (push) Failing after 108h25m16s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 108h25m23s
Deploy / Production Browser E2E (push) Failing after 1766h43m11s
Deploy / Build Production Runtime Images (push) Failing after 1766h50m8s
Deploy / Deploy Production (push) Failing after 1767h14m39s
Deploy / Staging E2E Tests (push) Failing after 108h12m17s
Deploy / Deploy Staging (push) Failing after 108h22m20s
CI/CD Pipeline / Frontend Lint (push) Failing after 108h25m16s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 108h25m23s
Deploy / Production Browser E2E (push) Failing after 1766h43m11s
Deploy / Build Production Runtime Images (push) Failing after 1766h50m8s
Deploy / Deploy Production (push) Failing after 1767h14m39s
- 素材列表加载时显示 8 张骨架屏卡片(脉冲动画),替代空白 loading 提示 - 诊断按钮点击后显示 loading 状态(LoadingOutlined + 禁用),防止重复点击 - 新增 SkeletonCard 组件和骨架屏 CSS 动画样式 Closes: 代码审计 P3-素材库缺 loading 态
This commit is contained in:
@@ -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 }> = ({
|
||||
<span className={`xx-status-pill xx-status-pill-${status}`}>{label}</span>
|
||||
);
|
||||
|
||||
/* ============================================================
|
||||
* SkeletonCard — 骨架屏卡片(素材列表加载时占位)
|
||||
* ============================================================ */
|
||||
const SkeletonCard: React.FC = () => (
|
||||
<div className="xx-asset-card xx-asset-skeleton">
|
||||
<div className="xx-asset-thumb xx-skeleton-pulse" />
|
||||
<div className="xx-asset-info">
|
||||
<div className="xx-skeleton-line xx-skeleton-pulse" style={{ width: "70%" }} />
|
||||
<div className="xx-skeleton-line xx-skeleton-pulse" style={{ width: "40%", marginTop: 8 }} />
|
||||
<div className="xx-skeleton-line xx-skeleton-pulse" style={{ width: "100%", height: 28, marginTop: 8, borderRadius: "var(--radius-xs)" }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
/* ============================================================
|
||||
* 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 }) => (
|
||||
<div
|
||||
className={`xx-asset-card${selected ? " xx-asset-card-selected" : ""}`}
|
||||
onClick={onToggle}
|
||||
@@ -243,14 +259,15 @@ const AssetCard: React.FC<{
|
||||
{asset.duration && <span>{asset.duration}</span>}
|
||||
</div>
|
||||
<button
|
||||
className="xx-asset-diagnose-btn"
|
||||
className={`xx-asset-diagnose-btn${diagnosing ? " xx-asset-diagnose-btn-loading" : ""}`}
|
||||
disabled={diagnosing}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDiagnose();
|
||||
}}
|
||||
>
|
||||
<ExperimentOutlined />
|
||||
诊断
|
||||
{diagnosing ? <LoadingOutlined /> : <ExperimentOutlined />}
|
||||
{diagnosing ? "诊断中..." : "诊断"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -338,6 +355,9 @@ const AssetLibrary: React.FC = () => {
|
||||
/* 视频播放 */
|
||||
const [playingAsset, setPlayingAsset] = useState<AssetItem | null>(null);
|
||||
|
||||
/* 诊断中状态 — 记录正在诊断的素材 ID */
|
||||
const [diagnosingId, setDiagnosingId] = useState<string | null>(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 ? (
|
||||
<div className="xx-assets-empty">
|
||||
<div className="xx-assets-empty-icon">⏳</div>
|
||||
<p>加载素材中...</p>
|
||||
<div className="xx-asset-grid">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<SkeletonCard key={i} />
|
||||
))}
|
||||
</div>
|
||||
) : assetsError ? (
|
||||
<div className="xx-assets-empty">
|
||||
@@ -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)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
空状态
|
||||
============================================================ */
|
||||
|
||||
Reference in New Issue
Block a user