eed93dd567
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3m23s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m25s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m42s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m45s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 4m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m46s
AI Code Review / AI Code Review (pull_request) Successful in 6m11s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 2m50s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m1s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 3m10s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 6m40s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 2m58s
- 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
112 lines
3.6 KiB
TypeScript
112 lines
3.6 KiB
TypeScript
/**
|
||
* 素材 CRUD API
|
||
*/
|
||
import apiClient from "../client"
|
||
import type { AssetItem, AssetMetadata } from "./types"
|
||
|
||
/** 获取素材库下的所有素材 */
|
||
export const getAssets = async (
|
||
libraryId: string,
|
||
options?: {
|
||
status?: string
|
||
page?: number
|
||
page_size?: number
|
||
},
|
||
): Promise<{ items: AssetItem[]; total: number }> => {
|
||
const params: Record<string, string | number> = { library_id: libraryId }
|
||
if (options?.status) params.status = options.status
|
||
if (options?.page) params.page = options.page
|
||
if (options?.page_size) params.page_size = options.page_size
|
||
const response = await apiClient.get("/assets", { params })
|
||
const data = response.data || {}
|
||
const items: AssetItem[] = data.items || []
|
||
const total: number = typeof data.total === "number" ? data.total : items.length
|
||
return { items, total }
|
||
}
|
||
|
||
/** 按类型获取素材(如 voice/video/image),支持可选筛选 */
|
||
export const getAssetsByKind = async (
|
||
kind: string,
|
||
filters?: {
|
||
keyword?: string
|
||
gender?: string
|
||
style?: string
|
||
tag_ids?: string[]
|
||
limit?: number
|
||
page?: number
|
||
page_size?: number
|
||
},
|
||
): Promise<AssetItem[]> => {
|
||
const params: Record<string, string | number> = { kind }
|
||
if (filters?.keyword) params.keyword = filters.keyword
|
||
if (filters?.gender) params.gender = filters.gender
|
||
if (filters?.style) params.style = filters.style
|
||
if (filters?.tag_ids?.length) params.tag_ids = filters.tag_ids.join(",")
|
||
if (filters?.limit) params.limit = filters.limit
|
||
if (filters?.page) params.page = filters.page
|
||
if (filters?.page_size) params.page_size = filters.page_size
|
||
const response = await apiClient.get("/assets", { params })
|
||
return response.data.items || []
|
||
}
|
||
|
||
/**
|
||
* 智能匹配素材(后端 AI 选素材)
|
||
* 调用后端 smart-match 端点,由后端根据素材库内容智能选择素材
|
||
*
|
||
* 后端返回 items 元素兼容两种结构(过渡期):
|
||
* - 扁平结构:AssetItem 本身(id 在顶层)
|
||
* - 包装结构:{ asset: AssetItem, score, breakdown }(id 需从 .asset 取)
|
||
* 这里统一归一化为 AssetItem[],调用方无需关心包装层。
|
||
*/
|
||
export interface SmartMatchResult {
|
||
items: AssetItem[]
|
||
}
|
||
|
||
interface SmartMatchWrappedItem {
|
||
asset?: AssetItem
|
||
id?: string
|
||
score?: number
|
||
breakdown?: unknown
|
||
}
|
||
|
||
export const smartMatchAssets = async (libraryId: string): Promise<SmartMatchResult> => {
|
||
const response = await apiClient.post("/assets/smart-match", {
|
||
library_id: libraryId,
|
||
})
|
||
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 等) */
|
||
export const updateAsset = async (
|
||
assetId: string,
|
||
data: { name?: string; metadata?: AssetMetadata },
|
||
): Promise<AssetItem> => {
|
||
const response = await apiClient.put(`/assets/${assetId}`, data)
|
||
return response.data
|
||
}
|
||
|
||
/** 更新素材审核状态 */
|
||
export const updateAssetReviewStatus = async (
|
||
assetId: string,
|
||
reviewStatus: "pending_review" | "approved" | "rejected",
|
||
): Promise<AssetItem> => {
|
||
const response = await apiClient.patch(`/assets/${assetId}/review`, {
|
||
review_status: reviewStatus,
|
||
})
|
||
return response.data
|
||
}
|
||
|
||
/** 删除素材 */
|
||
export const deleteAsset = async (assetId: string): Promise<void> => {
|
||
await apiClient.delete(`/assets/${assetId}`)
|
||
}
|