From eed93dd567e87ad0a3e62d496150ea7677bd5c6c Mon Sep 17 00:00:00 2001
From: xiaoxia
Date: Mon, 31 Aug 2026 00:31:33 +0800
Subject: [PATCH 1/2] =?UTF-8?q?fix(web):=20=E4=B8=8A=E4=BC=A0=E5=8C=BA?=
=?UTF-8?q?=E6=94=B9=E6=8C=89=E9=92=AE=E6=A8=A1=E5=BC=8F+=E5=8D=A1?=
=?UTF-8?q?=E7=89=87=E4=BF=A1=E6=81=AF=E5=8C=BA=E9=87=8D=E6=8E=92+AI?=
=?UTF-8?q?=E9=85=8D=E9=9F=B3=E6=97=B6=E9=95=BF=E6=98=BE=E7=A4=BA+smart-ma?=
=?UTF-8?q?tch=E7=BB=93=E6=9E=84=E5=85=BC=E5=AE=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- AssetUploadZone: 大虚线框 Dragger 改为紧凑「上传素材」按钮(隐藏input多选),
拖拽到内容区仍可上传;删除旧虚线框样式
- AssetCard: 信息区重排,状态标签+时长第一行(两端对齐)、余量标签独占第二行,
标签/余量 pill 缩小适配6列窄卡片,长标签省略号,杜绝文字重叠
- Step5VoiceSelect: duration=0 且 file_size=0 的 AI 音色显示「AI音色/按文本合成」,
不显示 00:00/大小,且不参与时长不足校验(克隆音色按脚本实时合成)
- smartMatchAssets: 兼容后端 {items:[{asset,score,breakdown}]} 包装结构,
统一归一化为 AssetItem[] 并过滤无 id 项
- usePreviewAssets: 过滤 undefined/空 id,杜绝 /assets/undefined 404
- useSmartMatch: matched 再过滤无 id 项后取 id
---
apps/web/src/api/assets/assets.ts | 29 ++++-
apps/web/src/pages/assets/assets.css | 106 ++++++++++++------
.../src/pages/assets/components/AssetCard.tsx | 16 +--
.../assets/components/AssetUploadZone.tsx | 84 +++++++++-----
.../generate/components/Step5VoiceSelect.tsx | 53 +++++----
.../hooks/step2-materials/useSmartMatch.ts | 6 +-
.../pages/generate/hooks/usePreviewAssets.ts | 13 ++-
7 files changed, 208 insertions(+), 99 deletions(-)
diff --git a/apps/web/src/api/assets/assets.ts b/apps/web/src/api/assets/assets.ts
index c436295c5..96a5b0840 100644
--- a/apps/web/src/api/assets/assets.ts
+++ b/apps/web/src/api/assets/assets.ts
@@ -52,12 +52,37 @@ export const getAssetsByKind = async (
/**
* 智能匹配素材(后端 AI 选素材)
* 调用后端 smart-match 端点,由后端根据素材库内容智能选择素材
+ *
+ * 后端返回 items 元素兼容两种结构(过渡期):
+ * - 扁平结构:AssetItem 本身(id 在顶层)
+ * - 包装结构:{ asset: AssetItem, score, breakdown }(id 需从 .asset 取)
+ * 这里统一归一化为 AssetItem[],调用方无需关心包装层。
*/
-export const smartMatchAssets = async (libraryId: string): Promise<{ items: AssetItem[] }> => {
+export interface SmartMatchResult {
+ items: AssetItem[]
+}
+
+interface SmartMatchWrappedItem {
+ asset?: AssetItem
+ id?: string
+ score?: number
+ breakdown?: unknown
+}
+
+export const smartMatchAssets = async (libraryId: string): Promise => {
const response = await apiClient.post("/assets/smart-match", {
library_id: libraryId,
})
- return response.data
+ const rawItems: SmartMatchWrappedItem[] = response.data?.items ?? []
+ const items = rawItems
+ .map((it) =>
+ // 包装结构 { asset: {...} } 优先解包;否则视其本身为扁平 AssetItem
+ it?.asset && typeof it.asset === "object" && "id" in it.asset
+ ? it.asset
+ : (it as unknown as AssetItem),
+ )
+ .filter((it): it is AssetItem => !!it && typeof it.id === "string" && it.id.length > 0)
+ return { items }
}
/** 更新素材(名称、metadata 等) */
diff --git a/apps/web/src/pages/assets/assets.css b/apps/web/src/pages/assets/assets.css
index ca9c0b019..f237ae738 100644
--- a/apps/web/src/pages/assets/assets.css
+++ b/apps/web/src/pages/assets/assets.css
@@ -147,44 +147,52 @@
/* ============================================================
上传区域
============================================================ */
-.xx-asset-upload-zone {
- border: 2px dashed var(--border-color);
- border-radius: var(--radius-lg);
- padding: var(--space-2xl) var(--space-xl);
- text-align: center;
- background: var(--bg-secondary);
- cursor: pointer;
+.xx-asset-upload-entry {
+ display: flex;
+ align-items: center;
+ gap: var(--space-md);
+ flex-wrap: wrap;
+ padding: var(--space-sm) var(--space-md);
+ border: 1px dashed transparent;
+ border-radius: var(--radius-md);
transition: var(--transition-all);
}
-.xx-asset-upload-zone:hover {
+.xx-asset-upload-entry-dragover {
border-color: var(--primary-color);
background: var(--primary-soft);
}
-.xx-asset-upload-zone:active {
- border-style: solid;
- transform: scale(0.99);
- box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.06);
-}
-
-.xx-asset-upload-icon {
- font-size: 40px;
- margin-bottom: var(--space-sm);
- color: var(--primary-color);
-}
-
-.xx-asset-upload-text {
- font-size: var(--font-size-base) !important;
- color: var(--text-primary) !important;
- margin: 0 0 var(--space-xs) !important;
+.xx-asset-upload-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-xs);
+ padding: 6px 16px;
+ font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
+ color: var(--text-inverse);
+ background: var(--primary-color);
+ border: none;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ transition: var(--transition-all);
+ white-space: nowrap;
}
-.xx-asset-upload-hint {
- font-size: var(--font-size-sm) !important;
- color: var(--text-tertiary) !important;
- margin: 0 !important;
+.xx-asset-upload-btn:hover {
+ opacity: 0.9;
+}
+
+.xx-asset-upload-btn:active {
+ transform: scale(0.98);
+}
+
+.xx-asset-upload-status {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-xs);
+ font-size: var(--font-size-xs);
+ color: var(--text-tertiary);
}
/* ============================================================
@@ -397,26 +405,46 @@
display: flex;
justify-content: space-between;
align-items: center;
+ gap: var(--space-xs);
font-size: var(--font-size-xs);
color: var(--text-secondary);
- margin-bottom: var(--space-sm);
+ margin-bottom: 4px;
+ min-width: 0;
}
-/* 状态标签 + 余量角标行 */
-.xx-asset-meta-left {
+/* 状态标签行:标签过长省略 */
+.xx-asset-meta-status {
display: inline-flex;
align-items: center;
- gap: var(--space-xs);
min-width: 0;
}
+.xx-asset-meta-status .xx-status-pill {
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.xx-asset-meta-duration {
+ flex-shrink: 0;
+ color: var(--text-tertiary);
+ font-variant-numeric: tabular-nums;
+}
+
+/* 余量标签独占一行 */
+.xx-asset-meta-usage {
+ justify-content: flex-start;
+ margin-bottom: var(--space-xs);
+}
+
/* 视频素材余量角标(仅状态展示,不影响卡片操作) */
.xx-asset-usage-badge {
display: inline-flex;
align-items: center;
- padding: var(--space-xxs) var(--space-sm);
+ padding: 1px 6px;
border-radius: var(--radius-full);
- font-size: var(--font-size-xs);
+ font-size: 10px;
font-weight: var(--font-weight-medium);
line-height: 1.5;
white-space: nowrap;
@@ -481,12 +509,16 @@
.xx-status-pill {
display: inline-flex;
align-items: center;
- gap: var(--space-xs);
- padding: var(--space-xxs) var(--space-sm);
+ gap: 2px;
+ padding: 1px 6px;
border-radius: var(--radius-full);
- font-size: var(--font-size-xs);
+ font-size: 10px;
font-weight: var(--font-weight-medium);
line-height: 1.5;
+ white-space: nowrap;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.xx-status-pill-ok {
diff --git a/apps/web/src/pages/assets/components/AssetCard.tsx b/apps/web/src/pages/assets/components/AssetCard.tsx
index a89293330..b0b0ea21c 100644
--- a/apps/web/src/pages/assets/components/AssetCard.tsx
+++ b/apps/web/src/pages/assets/components/AssetCard.tsx
@@ -128,16 +128,18 @@ const AssetCard: React.FC = ({
{asset.name}
-
+
- {usageBadge && (
-
- {usageBadge.label}
-
- )}
- {asset.duration && {asset.duration}}
+ {asset.duration && {asset.duration}}
+ {usageBadge && (
+
+
+ {usageBadge.label}
+
+
+ )}