Compare commits

..

2 Commits

Author SHA1 Message Date
xiaoxia 25257743a7 Merge branch 'develop' into refactor/editing-planner-api
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 7s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m8s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m15s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m39s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 39s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 41s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m39s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 53s
AI Code Review / AI Code Review (pull_request) Successful in 1m22s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 50s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m24s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 15m18s
CI/CD Pipeline / CI Gate (pull_request) 失败: CI/CD Pipeline / Validate - Code Quality (pull_request) [frontend-only]
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 23s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 16s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1220h39m48s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1221h14m9s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1221h14m10s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1221h14m11s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1221h14m11s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1222h59m20s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1222h59m22s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1222h59m22s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1222h59m24s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1222h59m26s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1224h53m2s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1224h53m6s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1223h32m5s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1225h25m49s
2026-07-27 12:35:01 +08:00
xiaoxia 9566ff5a96 refactor(editing-planner): API层按模块拆分(243行→入口36行, -85%)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 18s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m2s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 57s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 30s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m39s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 25s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m0s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 43s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m26s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 2m2s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 31s
AI Code Review / AI Code Review (pull_request) Successful in 2m1s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 27s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1245h50m53s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1246h6m0s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1246h6m4s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1246h9m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1246h16m53s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1246h16m55s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1246h20m11s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1246h20m15s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1246h20m18s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1246h25m22s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1246h25m22s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1246h25m24s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1246h38m41s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1246h52m52s
将 editing-planner.ts 单文件拆分为目录结构:
- types.ts — 类型定义
- constants.ts — MODE_LABELS / MODE_COLORS
- templates.ts — 模板 CRUD + 分类 + 生成
- index.ts — 统一入口 re-export

导入路径 @/api/editing-planner 保持向后兼容。
2026-07-26 16:54:05 +08:00
9 changed files with 402 additions and 623 deletions
-243
View File
@@ -1,243 +0,0 @@
/**
* 模板编辑器 API
* 对接后端 /api/v1/templates 路由
*/
import apiClient from "./client"
import type {
WatermarkConfig,
IntroOutroConfig,
PipConfig,
FilterConfig,
ChromaKeyConfig,
StickerConfig,
CoverConfig,
} from "@/pages/editing-planner/types"
/* ──────────── 类型定义 ──────────── */
/** 模板模式(后端枚举值) */
export type TemplateMode = "pip" | "voice_over" | "one_take" | "voice_pip"
/** 模式显示名称映射 */
export const MODE_LABELS: Record<TemplateMode, string> = {
pip: "混剪",
voice_over: "人物口播",
one_take: "一镜到底",
voice_pip: "口播+混剪",
}
/** 模式颜色映射 */
export const MODE_COLORS: Record<TemplateMode, string> = {
pip: "blue",
voice_over: "green",
one_take: "orange",
voice_pip: "purple",
}
/** 标题配置 */
export interface TitleConfig {
ai_auto_select: boolean
content: string
font_preset: string
font_color: string
font_size: number
position: string
}
/** 字幕配置 */
export interface SubtitleConfig {
enabled: boolean
position: string
font: string
color: string
size: number
animation: string
}
/** BGM 配置 */
export interface BgmConfig {
enabled: boolean
music_id: string
}
/** 模板片段 */
export interface TemplateSegment {
id?: string
segment_order: number
duration_min: number
duration_max: number
material_type: string | null
}
/** 剪辑模板 */
export interface EditingTemplate {
id: string
name: string
mode: TemplateMode
category: string
tags: string[]
title_config: TitleConfig
subtitle_config: SubtitleConfig
bgm_config: BgmConfig
estimated_duration: number
segments: TemplateSegment[]
/** 水印配置(后端就绪后启用) */
watermark_config?: WatermarkConfig
/** 片头片尾配置(后端就绪后启用) */
intro_outro_config?: IntroOutroConfig
/** 混剪配置 */
pip_config?: PipConfig
/** 滤镜调色配置 */
filter_config?: FilterConfig
/** 绿幕抠像配置 */
green_screen_config?: ChromaKeyConfig
/** 贴纸配置 */
sticker_config?: StickerConfig
/** 封面配置 */
cover_config?: CoverConfig
is_active?: boolean
created_at: string
updated_at: string
}
/** 模板分类 */
export interface TemplateCategory {
id: string
name: string
created_at?: string
}
/** 创建/更新模板请求体 */
export interface SaveTemplatePayload {
name: string
mode: TemplateMode
category: string
tags: string[]
title_config: TitleConfig
subtitle_config: SubtitleConfig
bgm_config: BgmConfig
estimated_duration: number
segments: Omit<TemplateSegment, "id">[]
/** 水印配置(后端就绪后启用) */
watermark_config?: WatermarkConfig
/** 片头片尾配置(后端就绪后启用) */
intro_outro_config?: IntroOutroConfig
/** 混剪配置 */
pip_config?: PipConfig
/** 滤镜调色配置 */
filter_config?: FilterConfig
/** 绿幕抠像配置 */
green_screen_config?: ChromaKeyConfig
/** 贴纸配置 */
sticker_config?: StickerConfig
/** 封面配置 */
cover_config?: CoverConfig
}
/** 使用模板生成请求体 */
export interface GenerateFromTemplatePayload {
voiceover_duration: number
}
/** 验证警告详情 */
export interface ValidationWarningDetails {
/** 相关字段名 */
field?: string
/** 期望值 */
expected?: string | number
/** 实际值 */
actual?: string | number
/** 建议值 */
suggested?: string | number
}
/** 验证/生成响应 */
export interface ValidateWarning {
code: string
message: string
details?: ValidationWarningDetails
}
/** 使用模板生成响应 */
export interface GenerateFromTemplateResponse {
template: EditingTemplate
warnings: ValidateWarning[]
}
/** 列表响应(带分页) */
export interface ListTemplatesResponse {
items: EditingTemplate[]
total: number
}
/** 分类列表响应 */
export interface ListCategoriesResponse {
items: TemplateCategory[]
}
// ============ API 函数 ============
/** 获取模板列表 */
export const getEditingTemplates = async (params?: {
category?: string
tag?: string
skip?: number
limit?: number
}): Promise<EditingTemplate[]> => {
const response = await apiClient.get<ListTemplatesResponse>("/templates", {
params: {
skip: params?.skip ?? 0,
limit: params?.limit ?? 50,
},
})
let list = response.data.items
if (params?.category) list = list.filter((t) => t.category === params.category)
if (params?.tag) list = list.filter((t) => t.tags.includes(params.tag!))
return list
}
/** 获取模板详情 */
export const getEditingTemplate = async (id: string): Promise<EditingTemplate> => {
const response = await apiClient.get<EditingTemplate>(`/templates/${id}`)
return response.data
}
/** 创建模板 */
export const createEditingTemplate = async (
data: SaveTemplatePayload,
): Promise<EditingTemplate> => {
const response = await apiClient.post<EditingTemplate>("/templates", data)
return response.data
}
/** 更新模板 */
export const updateEditingTemplate = async (
id: string,
data: SaveTemplatePayload,
): Promise<EditingTemplate> => {
const response = await apiClient.patch<EditingTemplate>(`/templates/${id}`, data)
return response.data
}
/** 删除模板 */
export const deleteEditingTemplate = async (id: string): Promise<void> => {
await apiClient.delete(`/templates/${id}`)
}
/** 获取模板分类列表 */
export const getTemplateCategories = async (): Promise<TemplateCategory[]> => {
const response = await apiClient.get<ListCategoriesResponse>("/templates/categories/list")
return response.data.items
}
/** 使用模板生成视频(调用 validate 端点) */
export const generateFromTemplate = async (
templateId: string,
data: GenerateFromTemplatePayload,
): Promise<GenerateFromTemplateResponse> => {
const response = await apiClient.post<GenerateFromTemplateResponse>(
`/templates/${templateId}/validate`,
data,
)
return response.data
}
@@ -0,0 +1,20 @@
/**
* 模板编辑器常量
*/
import type { TemplateMode } from "./types"
/** 模式显示名称映射 */
export const MODE_LABELS: Record<TemplateMode, string> = {
pip: "混剪",
voice_over: "人物口播",
one_take: "一镜到底",
voice_pip: "口播+混剪",
}
/** 模式颜色映射 */
export const MODE_COLORS: Record<TemplateMode, string> = {
pip: "blue",
voice_over: "green",
one_take: "orange",
voice_pip: "purple",
}
+36
View File
@@ -0,0 +1,36 @@
/**
* 模板编辑器 API — 目录化入口
* 保持与原 editing-planner.ts 相同导出,向后兼容
*/
// 类型
export type {
TemplateMode,
TitleConfig,
SubtitleConfig,
BgmConfig,
TemplateSegment,
EditingTemplate,
TemplateCategory,
SaveTemplatePayload,
GenerateFromTemplatePayload,
ValidationWarningDetails,
ValidateWarning,
GenerateFromTemplateResponse,
ListTemplatesResponse,
ListCategoriesResponse,
} from "./types"
// 常量
export { MODE_LABELS, MODE_COLORS } from "./constants"
// API 函数
export {
getEditingTemplates,
getEditingTemplate,
createEditingTemplate,
updateEditingTemplate,
deleteEditingTemplate,
getTemplateCategories,
generateFromTemplate,
} from "./templates"
@@ -0,0 +1,78 @@
/**
* 模板编辑器 API 函数
*/
import apiClient from "../client"
import type {
EditingTemplate,
TemplateCategory,
SaveTemplatePayload,
GenerateFromTemplatePayload,
GenerateFromTemplateResponse,
ListTemplatesResponse,
ListCategoriesResponse,
} from "./types"
/** 获取模板列表 */
export const getEditingTemplates = async (params?: {
category?: string
tag?: string
skip?: number
limit?: number
}): Promise<EditingTemplate[]> => {
const response = await apiClient.get<ListTemplatesResponse>("/templates", {
params: {
skip: params?.skip ?? 0,
limit: params?.limit ?? 50,
},
})
let list = response.data.items
if (params?.category) list = list.filter((t) => t.category === params.category)
if (params?.tag) list = list.filter((t) => t.tags.includes(params.tag!))
return list
}
/** 获取模板详情 */
export const getEditingTemplate = async (id: string): Promise<EditingTemplate> => {
const response = await apiClient.get<EditingTemplate>(`/templates/${id}`)
return response.data
}
/** 创建模板 */
export const createEditingTemplate = async (
data: SaveTemplatePayload,
): Promise<EditingTemplate> => {
const response = await apiClient.post<EditingTemplate>("/templates", data)
return response.data
}
/** 更新模板 */
export const updateEditingTemplate = async (
id: string,
data: SaveTemplatePayload,
): Promise<EditingTemplate> => {
const response = await apiClient.patch<EditingTemplate>(`/templates/${id}`, data)
return response.data
}
/** 删除模板 */
export const deleteEditingTemplate = async (id: string): Promise<void> => {
await apiClient.delete(`/templates/${id}`)
}
/** 获取模板分类列表 */
export const getTemplateCategories = async (): Promise<TemplateCategory[]> => {
const response = await apiClient.get<ListCategoriesResponse>("/templates/categories/list")
return response.data.items
}
/** 使用模板生成视频(调用 validate 端点) */
export const generateFromTemplate = async (
templateId: string,
data: GenerateFromTemplatePayload,
): Promise<GenerateFromTemplateResponse> => {
const response = await apiClient.post<GenerateFromTemplateResponse>(
`/templates/${templateId}/validate`,
data,
)
return response.data
}
+138
View File
@@ -0,0 +1,138 @@
/**
* 模板编辑器类型定义
*/
import type {
WatermarkConfig,
IntroOutroConfig,
PipConfig,
FilterConfig,
ChromaKeyConfig,
StickerConfig,
CoverConfig,
} from "@/pages/editing-planner/types"
/** 模板模式(后端枚举值) */
export type TemplateMode = "pip" | "voice_over" | "one_take" | "voice_pip"
/** 标题配置 */
export interface TitleConfig {
ai_auto_select: boolean
content: string
font_preset: string
font_color: string
font_size: number
position: string
}
/** 字幕配置 */
export interface SubtitleConfig {
enabled: boolean
position: string
font: string
color: string
size: number
animation: string
}
/** BGM 配置 */
export interface BgmConfig {
enabled: boolean
music_id: string
}
/** 模板片段 */
export interface TemplateSegment {
id?: string
segment_order: number
duration_min: number
duration_max: number
material_type: string | null
}
/** 剪辑模板 */
export interface EditingTemplate {
id: string
name: string
mode: TemplateMode
category: string
tags: string[]
title_config: TitleConfig
subtitle_config: SubtitleConfig
bgm_config: BgmConfig
estimated_duration: number
segments: TemplateSegment[]
watermark_config?: WatermarkConfig
intro_outro_config?: IntroOutroConfig
pip_config?: PipConfig
filter_config?: FilterConfig
green_screen_config?: ChromaKeyConfig
sticker_config?: StickerConfig
cover_config?: CoverConfig
is_active?: boolean
created_at: string
updated_at: string
}
/** 模板分类 */
export interface TemplateCategory {
id: string
name: string
created_at?: string
}
/** 创建/更新模板请求体 */
export interface SaveTemplatePayload {
name: string
mode: TemplateMode
category: string
tags: string[]
title_config: TitleConfig
subtitle_config: SubtitleConfig
bgm_config: BgmConfig
estimated_duration: number
segments: Omit<TemplateSegment, "id">[]
watermark_config?: WatermarkConfig
intro_outro_config?: IntroOutroConfig
pip_config?: PipConfig
filter_config?: FilterConfig
green_screen_config?: ChromaKeyConfig
sticker_config?: StickerConfig
cover_config?: CoverConfig
}
/** 使用模板生成请求体 */
export interface GenerateFromTemplatePayload {
voiceover_duration: number
}
/** 验证警告详情 */
export interface ValidationWarningDetails {
field?: string
expected?: string | number
actual?: string | number
suggested?: string | number
}
/** 验证警告 */
export interface ValidateWarning {
code: string
message: string
details?: ValidationWarningDetails
}
/** 使用模板生成响应 */
export interface GenerateFromTemplateResponse {
template: EditingTemplate
warnings: ValidateWarning[]
}
/** 列表响应(带分页) */
export interface ListTemplatesResponse {
items: EditingTemplate[]
total: number
}
/** 分类列表响应 */
export interface ListCategoriesResponse {
items: TemplateCategory[]
}
+130 -67
View File
@@ -1,27 +1,25 @@
/**
* 视频库页面 — V21 设计系统
* 两栏布局:左侧视频库列表(260px)+ 右侧素材网格
*
* 主组件仅保留 Hook 组装与整体布局
* 数据查询 → hooks/useAssetsData
* 库管理 → hooks/useLibraryManagement
* 上传 → hooks/useAssetUpload
* 选中态 → hooks/useAssetSelection
* 素材操作 → hooks/useAssetOperations
* 上传区 → components/AssetUploadZone
* 网格区 → components/AssetGridSection
* 弹窗集合 → components/AssetModals
* 使用 useQuery 对接后端真实 API(api/assets.ts)
*/
import React, { useState } from "react"
import { Upload } from "antd"
import { InboxOutlined, PictureOutlined, ExclamationCircleOutlined } from "@ant-design/icons"
import { Button } from "@/components/ui"
import type { AssetItem } from "@/pages/assets/types"
import type { SmartViewType } from "@/pages/assets/components/BatchMarkModal"
import AssetCard from "@/pages/assets/components/AssetCard"
import { SkeletonCard } from "@/pages/assets/components/AssetSkeleton"
import LibrarySidebar from "@/pages/assets/components/LibrarySidebar"
import AssetFilterBar from "@/pages/assets/components/AssetFilterBar"
import BatchOperationBar from "@/pages/assets/components/BatchOperationBar"
import AssetUploadZone from "@/pages/assets/components/AssetUploadZone"
import AssetGridSection from "@/pages/assets/components/AssetGridSection"
import AssetModals from "@/pages/assets/components/AssetModals"
import CreateLibraryModal from "@/pages/assets/components/CreateLibraryModal"
import PlayModal from "@/pages/assets/components/PlayModal"
import BatchTagModal from "@/pages/assets/components/BatchTagModal"
import BatchClassifyModal from "@/pages/assets/components/BatchClassifyModal"
import BatchMarkModal from "@/pages/assets/components/BatchMarkModal"
import ResultDrawer from "@/pages/assets/components/ResultDrawer"
import UploadProgressModal from "@/pages/assets/components/UploadProgressModal"
import { useAssetsData } from "@/pages/assets/hooks/useAssetsData"
import { useLibraryManagement } from "@/pages/assets/hooks/useLibraryManagement"
import { useAssetUpload } from "@/pages/assets/hooks/useAssetUpload"
@@ -29,6 +27,9 @@ import { useAssetSelection } from "@/pages/assets/hooks/useAssetSelection"
import { useAssetOperations } from "@/pages/assets/hooks/useAssetOperations"
import "./assets.css"
/* ============================================================
* 主组件
* ============================================================ */
const AssetLibrary: React.FC = () => {
/* ── 数据查询与筛选 ── */
const {
@@ -128,6 +129,9 @@ const AssetLibrary: React.FC = () => {
return (
<div className="xx-assets-page">
{/* ─── 上传进度弹窗 ─── */}
<UploadProgressModal open={uploading} progress={uploadProgress} />
{/* 两栏布局 */}
<div className="xx-assets-layout">
{/* ─── 左侧:视频库列表 ─── */}
@@ -142,11 +146,25 @@ const AssetLibrary: React.FC = () => {
{/* ─── 右侧:内容区 ─── */}
<div className="xx-assets-content">
{/* 上传区域 */}
<AssetUploadZone
uploading={uploading}
uploadProgress={uploadProgress}
onUpload={handleUpload}
/>
<Upload.Dragger
beforeUpload={(file) => {
handleUpload(file as File)
return false
}}
showUploadList={false}
multiple
accept="video/*,image/*"
>
<div className="xx-asset-upload-zone">
<p className="xx-asset-upload-icon">
<InboxOutlined />
</p>
<p className="xx-asset-upload-text">
{uploading ? "上传中..." : "点击或拖拽文件到此区域上传"}
</p>
<p className="xx-asset-upload-hint">支持视频、图片,单文件不超过 2GB</p>
</div>
</Upload.Dragger>
{/* 筛选栏 */}
<AssetFilterBar
@@ -173,69 +191,114 @@ const AssetLibrary: React.FC = () => {
)}
{/* 素材网格 */}
<AssetGridSection
loading={assetsLoading}
error={assetsError}
errorMessage={assetsErrorObj?.message}
assets={filteredAssets}
selectedIds={selectedIds}
diagnosingId={diagnosingId}
onRetry={refetchAssets}
onToggleSelect={toggleSelect}
onDiagnose={handleDiagnose}
onPlay={setPlayingAsset}
onDelete={handleSingleDelete}
/>
{assetsLoading ? (
<div className="xx-asset-grid">
{Array.from({ length: 8 }).map((_, i) => (
<SkeletonCard key={i} />
))}
</div>
) : assetsError ? (
<div className="xx-assets-empty">
<div className="xx-assets-empty-icon">
<ExclamationCircleOutlined />
</div>
<p className="xx-assets-empty-title">{assetsErrorObj?.message || "加载失败"}</p>
<Button buttonType="primary" buttonSize="sm" onClick={() => refetchAssets()}>
重新加载
</Button>
</div>
) : filteredAssets.length > 0 ? (
<div className="xx-asset-grid">
{filteredAssets.map((asset) => (
<AssetCard
key={asset.id}
asset={asset}
selected={selectedIds.has(asset.id)}
diagnosing={diagnosingId === asset.id}
onToggle={() => toggleSelect(asset.id)}
onDiagnose={() => handleDiagnose(asset)}
onPlay={() => setPlayingAsset(asset)}
onDelete={() => handleSingleDelete(asset.id)}
/>
))}
</div>
) : (
<div className="xx-assets-empty">
<div className="xx-assets-empty-icon">
<PictureOutlined />
</div>
<p className="xx-assets-empty-title">暂无素材,请上传或切换视频库</p>
</div>
)}
</div>
</div>
{/* ─── 弹窗集合 ─── */}
<AssetModals
uploading={uploading}
uploadProgress={uploadProgress}
createModalOpen={createModalOpen}
onCreateModalCancel={() => setCreateModalOpen(false)}
onCreateModalOk={handleCreateLibrary}
newLibName={newLibName}
onNewLibNameChange={setNewLibName}
newLibKind={newLibKind}
onNewLibKindChange={setNewLibKind}
createLoading={isCreating}
playingAsset={playingAsset}
onPlayClose={() => setPlayingAsset(null)}
tagModalOpen={tagModalOpen}
{/* ─── 新建视频库弹窗 ─── */}
<CreateLibraryModal
open={createModalOpen}
onCancel={() => setCreateModalOpen(false)}
onOk={handleCreateLibrary}
name={newLibName}
onNameChange={setNewLibName}
kind={newLibKind}
onKindChange={setNewLibKind}
confirmLoading={isCreating}
/>
{/* ─── 视频/音频播放弹窗 ─── */}
<PlayModal open={!!playingAsset} asset={playingAsset} onClose={() => setPlayingAsset(null)} />
{/* ─── 批量打标签弹窗 ─── */}
<BatchTagModal
open={tagModalOpen}
selectedCount={selectedIds.size}
onTagCancel={() => {
onCancel={() => {
setTagModalOpen(false)
setBatchTags([])
setBatchTagInput("")
}}
onTagOk={handleBatchTag}
batchTags={batchTags}
batchTagInput={batchTagInput}
onOk={handleBatchTag}
tags={batchTags}
onTagInputChange={setBatchTagInput}
onTagInputKeyDown={handleTagInputKeyDown}
onRemoveTag={removeBatchTag}
tagMode={tagMode as "add" | "replace"}
onTagModeChange={setTagMode as (mode: "add" | "replace") => void}
batchLoading={batchLoading}
classifyModalOpen={classifyModalOpen}
onClassifyCancel={() => {
tagInput={batchTagInput}
tagMode={tagMode}
onTagModeChange={setTagMode}
confirmLoading={batchLoading}
/>
{/* ─── 批量改分类弹窗 ─── */}
<BatchClassifyModal
open={classifyModalOpen}
selectedCount={selectedIds.size}
onCancel={() => {
setClassifyModalOpen(false)
setBatchCategory("")
}}
onClassifyOk={handleBatchClassify}
batchCategory={batchCategory}
onOk={handleBatchClassify}
category={batchCategory}
onCategoryChange={setBatchCategory}
markModalOpen={markModalOpen}
onMarkCancel={() => setMarkModalOpen(false)}
onMarkOk={handleBatchMark}
batchSmartView={batchSmartView as SmartViewType}
onSmartViewChange={setBatchSmartView as (val: SmartViewType) => void}
resultDrawerOpen={resultDrawerOpen}
operationTitle={operationTitle}
operationResult={operationResult}
onResultDrawerClose={handleResultDrawerClose}
confirmLoading={batchLoading}
/>
{/* ─── 批量智能标记弹窗 ─── */}
<BatchMarkModal
open={markModalOpen}
selectedCount={selectedIds.size}
onCancel={() => setMarkModalOpen(false)}
onOk={handleBatchMark}
smartView={batchSmartView}
onSmartViewChange={setBatchSmartView}
confirmLoading={batchLoading}
/>
{/* ─── 操作结果 Drawer ─── */}
<ResultDrawer
open={resultDrawerOpen}
title={operationTitle}
result={operationResult}
onClose={handleResultDrawerClose}
/>
</div>
)
@@ -1,93 +0,0 @@
/**
* AssetLibrary 素材网格区域(含加载/错误/空状态)
*/
import React from "react"
import { PictureOutlined, ExclamationCircleOutlined } from "@ant-design/icons"
import { Button } from "@/components/ui"
import type { AssetItem } from "../types"
import AssetCard from "./AssetCard"
import { SkeletonCard } from "./AssetSkeleton"
export interface AssetGridSectionProps {
loading: boolean
error: boolean
errorMessage?: string
assets: AssetItem[]
selectedIds: Set<string>
diagnosingId: string | null
onRetry?: () => void
onToggleSelect: (id: string) => void
onDiagnose: (asset: AssetItem) => void
onPlay: (asset: AssetItem) => void
onDelete: (id: string) => void
}
export const AssetGridSection: React.FC<AssetGridSectionProps> = ({
loading,
error,
errorMessage,
assets,
selectedIds,
diagnosingId,
onRetry,
onToggleSelect,
onDiagnose,
onPlay,
onDelete,
}) => {
if (loading) {
return (
<div className="xx-asset-grid">
{Array.from({ length: 8 }).map((_, i) => (
<SkeletonCard key={i} />
))}
</div>
)
}
if (error) {
return (
<div className="xx-assets-empty">
<div className="xx-assets-empty-icon">
<ExclamationCircleOutlined />
</div>
<p className="xx-assets-empty-title">{errorMessage || "加载失败"}</p>
{onRetry && (
<Button buttonType="primary" buttonSize="sm" onClick={onRetry}>
重新加载
</Button>
)}
</div>
)
}
if (assets.length > 0) {
return (
<div className="xx-asset-grid">
{assets.map((asset) => (
<AssetCard
key={asset.id}
asset={asset}
selected={selectedIds.has(asset.id)}
diagnosing={diagnosingId === asset.id}
onToggle={() => onToggleSelect(asset.id)}
onDiagnose={() => onDiagnose(asset)}
onPlay={() => onPlay(asset)}
onDelete={() => onDelete(asset.id)}
/>
))}
</div>
)
}
return (
<div className="xx-assets-empty">
<div className="xx-assets-empty-icon">
<PictureOutlined />
</div>
<p className="xx-assets-empty-title">暂无素材,请上传或切换视频库</p>
</div>
)
}
export default AssetGridSection
@@ -1,179 +0,0 @@
/**
* AssetLibrary 弹窗集合
*/
import React from "react"
import type { AssetItem, AssetKind } from "../types"
import type { SmartViewType } from "./BatchMarkModal"
import type { BatchOperationResult } from "@/api/assets"
import CreateLibraryModal from "./CreateLibraryModal"
import PlayModal from "./PlayModal"
import BatchTagModal from "./BatchTagModal"
import BatchClassifyModal from "./BatchClassifyModal"
import BatchMarkModal from "./BatchMarkModal"
import ResultDrawer from "./ResultDrawer"
import UploadProgressModal from "./UploadProgressModal"
export interface AssetModalsProps {
/* 上传进度 */
uploading: boolean
uploadProgress: number
/* 新建视频库 */
createModalOpen: boolean
onCreateModalCancel: () => void
onCreateModalOk: () => void
newLibName: string
onNewLibNameChange: (name: string) => void
newLibKind: AssetKind
onNewLibKindChange: (kind: AssetKind) => void
createLoading: boolean
/* 播放弹窗 */
playingAsset: AssetItem | null
onPlayClose: () => void
/* 批量打标签 */
tagModalOpen: boolean
selectedCount: number
onTagCancel: () => void
onTagOk: () => void
batchTags: string[]
batchTagInput: string
onTagInputChange: (val: string) => void
onTagInputKeyDown: (e: React.KeyboardEvent) => void
onRemoveTag: (tag: string) => void
tagMode: "add" | "replace"
onTagModeChange: (mode: "add" | "replace") => void
batchLoading: boolean
/* 批量改分类 */
classifyModalOpen: boolean
onClassifyCancel: () => void
onClassifyOk: () => void
batchCategory: string
onCategoryChange: (val: string) => void
/* 批量智能标记 */
markModalOpen: boolean
onMarkCancel: () => void
onMarkOk: () => void
batchSmartView: SmartViewType
onSmartViewChange: (val: SmartViewType) => void
/* 操作结果 Drawer */
resultDrawerOpen: boolean
operationTitle: string
operationResult: BatchOperationResult | null
onResultDrawerClose: () => void
}
export const AssetModals: React.FC<AssetModalsProps> = ({
uploading,
uploadProgress,
createModalOpen,
onCreateModalCancel,
onCreateModalOk,
newLibName,
onNewLibNameChange,
newLibKind,
onNewLibKindChange,
createLoading,
playingAsset,
onPlayClose,
tagModalOpen,
selectedCount,
onTagCancel,
onTagOk,
batchTags,
batchTagInput,
onTagInputChange,
onTagInputKeyDown,
onRemoveTag,
tagMode,
onTagModeChange,
batchLoading,
classifyModalOpen,
onClassifyCancel,
onClassifyOk,
batchCategory,
onCategoryChange,
markModalOpen,
onMarkCancel,
onMarkOk,
batchSmartView,
onSmartViewChange,
resultDrawerOpen,
operationTitle,
operationResult,
onResultDrawerClose,
}) => {
return (
<>
{/* 上传进度弹窗 */}
<UploadProgressModal open={uploading} progress={uploadProgress} />
{/* 新建视频库弹窗 */}
<CreateLibraryModal
open={createModalOpen}
onCancel={onCreateModalCancel}
onOk={onCreateModalOk}
name={newLibName}
onNameChange={onNewLibNameChange}
kind={newLibKind}
onKindChange={onNewLibKindChange}
confirmLoading={createLoading}
/>
{/* 视频/音频播放弹窗 */}
<PlayModal open={!!playingAsset} asset={playingAsset} onClose={onPlayClose} />
{/* 批量打标签弹窗 */}
<BatchTagModal
open={tagModalOpen}
selectedCount={selectedCount}
onCancel={onTagCancel}
onOk={onTagOk}
tags={batchTags}
onTagInputChange={onTagInputChange}
onTagInputKeyDown={onTagInputKeyDown}
onRemoveTag={onRemoveTag}
tagInput={batchTagInput}
tagMode={tagMode}
onTagModeChange={onTagModeChange}
confirmLoading={batchLoading}
/>
{/* 批量改分类弹窗 */}
<BatchClassifyModal
open={classifyModalOpen}
selectedCount={selectedCount}
onCancel={onClassifyCancel}
onOk={onClassifyOk}
category={batchCategory}
onCategoryChange={onCategoryChange}
confirmLoading={batchLoading}
/>
{/* 批量智能标记弹窗 */}
<BatchMarkModal
open={markModalOpen}
selectedCount={selectedCount}
onCancel={onMarkCancel}
onOk={onMarkOk}
smartView={batchSmartView}
onSmartViewChange={onSmartViewChange}
confirmLoading={batchLoading}
/>
{/* 操作结果 Drawer */}
<ResultDrawer
open={resultDrawerOpen}
title={operationTitle}
result={operationResult}
onClose={onResultDrawerClose}
/>
</>
)
}
export default AssetModals
@@ -1,41 +0,0 @@
/**
* AssetLibrary 上传拖拽区域
*/
import React from "react"
import { Upload } from "antd"
import { InboxOutlined } from "@ant-design/icons"
export interface AssetUploadZoneProps {
uploading: boolean
uploadProgress: number
onUpload: (file: File) => void
}
export const AssetUploadZone: React.FC<AssetUploadZoneProps> = ({
uploading,
onUpload,
}) => {
return (
<Upload.Dragger
beforeUpload={(file) => {
onUpload(file as File)
return false
}}
showUploadList={false}
multiple
accept="video/*,image/*"
>
<div className="xx-asset-upload-zone">
<p className="xx-asset-upload-icon">
<InboxOutlined />
</p>
<p className="xx-asset-upload-text">
{uploading ? "上传中..." : "点击或拖拽文件到此区域上传"}
</p>
<p className="xx-asset-upload-hint">支持视频、图片,单文件不超过 2GB</p>
</div>
</Upload.Dragger>
)
}
export default AssetUploadZone