Compare commits

..

5 Commits

Author SHA1 Message Date
xiaoxia c7d814cbc8 fix: 修复 AssetModals newLibKind 类型为 AssetKind
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 8s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m55s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m19s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 33s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m17s
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 / PR Build Worker Image (pull_request) Successful in 28s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 54s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m11s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 37s
AI Code Review / AI Code Review (pull_request) Successful in 1m59s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 59s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 3m7s
CI/CD Pipeline / CI Gate (pull_request) 失败: CI/CD Pipeline / Validate - Code Quality (pull_request), CI/CD Pipeline / Frontend Lint (pull_request) [frontend-only]
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
CI/CD Pipeline / Unit 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 / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 19s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 12s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
2026-07-27 13:19:19 +08:00
xiaoxia e99f80f333 fix: 修复 AssetLibrary 类型不匹配(tagMode/smartView)
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 43s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 43s
2026-07-27 13:17:38 +08:00
xiaoxia d85b181654 fix: 修复 AssetModals 类型不匹配(tagMode/smartView/result)
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Successful in 42s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 40s
2026-07-27 13:17:24 +08:00
xiaoxia f5509ffc27 Merge branch 'develop' into refactor/asset-library-deepen
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 28s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m22s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m9s
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 / Validate - Migration (alembic) (pull_request) Successful in 1m33s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 41s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 2m31s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 57s
AI Code Review / AI Code Review (pull_request) Successful in 1m27s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m16s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 37s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 2m37s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 8m33s
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
2026-07-27 12:34:31 +08:00
xiaoxia dd7452953e refactor(asset-library): 深化 AssetLibrary 拆分,抽离上传区/网格区/弹窗集合
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 35s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 45s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m19s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m7s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m37s
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 / PR Build Worker Image (pull_request) Successful in 44s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m5s
AI Code Review / AI Code Review (pull_request) Successful in 1m22s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 42s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 53s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m58s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 2m49s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 27s
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 / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
- 抽 AssetUploadZone 组件: 上传拖拽区域
- 抽 AssetGridSection 组件: 素材网格 + 加载/错误/空状态
- 抽 AssetModals 组件: 7 个弹窗 + Drawer 集合
- 主文件 307→243 行 (-21%),仅保留 Hook 组装与整体布局
2026-07-27 00:45:58 +08:00
9 changed files with 664 additions and 482 deletions
+67 -130
View File
@@ -1,25 +1,27 @@
/**
* 视频库页面 — V21 设计系统
* 两栏布局:左侧视频库列表(260px)+ 右侧素材网格
* 使用 useQuery 对接后端真实 APIapi/assets.ts
*
* 主组件仅保留 Hook 组装与整体布局
* 数据查询 → hooks/useAssetsData
* 库管理 → hooks/useLibraryManagement
* 上传 → hooks/useAssetUpload
* 选中态 → hooks/useAssetSelection
* 素材操作 → hooks/useAssetOperations
* 上传区 → components/AssetUploadZone
* 网格区 → components/AssetGridSection
* 弹窗集合 → components/AssetModals
*/
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 AssetCard from "@/pages/assets/components/AssetCard"
import type { SmartViewType } from "@/pages/assets/components/BatchMarkModal"
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 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 AssetUploadZone from "@/pages/assets/components/AssetUploadZone"
import AssetGridSection from "@/pages/assets/components/AssetGridSection"
import AssetModals from "@/pages/assets/components/AssetModals"
import { useAssetsData } from "@/pages/assets/hooks/useAssetsData"
import { useLibraryManagement } from "@/pages/assets/hooks/useLibraryManagement"
import { useAssetUpload } from "@/pages/assets/hooks/useAssetUpload"
@@ -27,9 +29,6 @@ import { useAssetSelection } from "@/pages/assets/hooks/useAssetSelection"
import { useAssetOperations } from "@/pages/assets/hooks/useAssetOperations"
import "./assets.css"
/* ============================================================
* 主组件
* ============================================================ */
const AssetLibrary: React.FC = () => {
/* ── 数据查询与筛选 ── */
const {
@@ -129,9 +128,6 @@ const AssetLibrary: React.FC = () => {
return (
<div className="xx-assets-page">
{/* ─── 上传进度弹窗 ─── */}
<UploadProgressModal open={uploading} progress={uploadProgress} />
{/* 两栏布局 */}
<div className="xx-assets-layout">
{/* ─── 左侧:视频库列表 ─── */}
@@ -146,25 +142,11 @@ const AssetLibrary: React.FC = () => {
{/* ─── 右侧:内容区 ─── */}
<div className="xx-assets-content">
{/* 上传区域 */}
<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>
<AssetUploadZone
uploading={uploading}
uploadProgress={uploadProgress}
onUpload={handleUpload}
/>
{/* 筛选栏 */}
<AssetFilterBar
@@ -191,114 +173,69 @@ const AssetLibrary: React.FC = () => {
)}
{/* 素材网格 */}
{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>
)}
<AssetGridSection
loading={assetsLoading}
error={assetsError}
errorMessage={assetsErrorObj?.message}
assets={filteredAssets}
selectedIds={selectedIds}
diagnosingId={diagnosingId}
onRetry={refetchAssets}
onToggleSelect={toggleSelect}
onDiagnose={handleDiagnose}
onPlay={setPlayingAsset}
onDelete={handleSingleDelete}
/>
</div>
</div>
{/* ─── 新建视频库弹窗 ─── */}
<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}
{/* ─── 弹窗集合 ─── */}
<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}
selectedCount={selectedIds.size}
onCancel={() => {
onTagCancel={() => {
setTagModalOpen(false)
setBatchTags([])
setBatchTagInput("")
}}
onOk={handleBatchTag}
tags={batchTags}
onTagOk={handleBatchTag}
batchTags={batchTags}
batchTagInput={batchTagInput}
onTagInputChange={setBatchTagInput}
onTagInputKeyDown={handleTagInputKeyDown}
onRemoveTag={removeBatchTag}
tagInput={batchTagInput}
tagMode={tagMode}
onTagModeChange={setTagMode}
confirmLoading={batchLoading}
/>
{/* ─── 批量改分类弹窗 ─── */}
<BatchClassifyModal
open={classifyModalOpen}
selectedCount={selectedIds.size}
onCancel={() => {
tagMode={tagMode as "add" | "replace"}
onTagModeChange={setTagMode as (mode: "add" | "replace") => void}
batchLoading={batchLoading}
classifyModalOpen={classifyModalOpen}
onClassifyCancel={() => {
setClassifyModalOpen(false)
setBatchCategory("")
}}
onOk={handleBatchClassify}
category={batchCategory}
onClassifyOk={handleBatchClassify}
batchCategory={batchCategory}
onCategoryChange={setBatchCategory}
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}
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}
/>
</div>
)
@@ -0,0 +1,93 @@
/**
* 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
@@ -0,0 +1,179 @@
/**
* 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
@@ -0,0 +1,41 @@
/**
* 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
+284 -3
View File
@@ -1,5 +1,286 @@
/**
* LayerConfig 入口(向后兼容)
* 实际实现位于 ./layer-config/ 目录
* 混剪单图层配置区
*/
export { default } from "./layer-config"
import React from "react"
import type {
PipLayer,
PipAnimType,
PipSlideDirection,
PipGridPosition,
} from "@/pages/editing-planner/types"
import {
GRID_POSITIONS,
ANIM_OPTIONS,
SLIDE_DIR_OPTIONS,
LAYER_COLORS,
} from "@/pages/editing-planner/constants/pipConfig"
interface LayerConfigProps {
layer: PipLayer | null
layers: PipLayer[]
totalDuration: number
onUpdate: (id: string, partial: Partial<PipLayer>) => void
onGridClick: (pos: PipGridPosition) => void
onWidthChange: (val: number) => void
onHeightChange: (val: number) => void
}
const LayerConfig: React.FC<LayerConfigProps> = ({
layer,
layers,
totalDuration,
onUpdate,
onGridClick,
onWidthChange,
onHeightChange,
}) => {
if (!layer) {
return (
<div className="pip-config-area">
<div className="pip-config-empty"></div>
</div>
)
}
return (
<div className="pip-config-area">
{/* ── 迷你预览 ── */}
<div className="pip-preview-box">
{layers.map((l, idx) => (
<div
key={l.id}
className={`pip-preview-layer${layer.id === l.id ? " selected" : ""}`}
style={{
left: `${l.x}%`,
top: `${l.y}%`,
width: `${l.width}%`,
height: `${l.height}%`,
background: LAYER_COLORS[idx % LAYER_COLORS.length],
opacity: l.opacity / 100,
borderRadius: `${l.border_radius}%`,
}}
>
<span className="pip-preview-label">{l.name}</span>
</div>
))}
</div>
{/* ── 素材类型 ── */}
<div className="pip-field">
<label className="pip-field-label"></label>
<div className="pip-type-btns">
<button
className={`pip-type-btn${layer.material_type === "image" ? " active" : ""}`}
onClick={() => onUpdate(layer.id, { material_type: "image" })}
>
🖼
</button>
<button
className={`pip-type-btn${layer.material_type === "video" ? " active" : ""}`}
onClick={() => onUpdate(layer.id, { material_type: "video" })}
>
🎬
</button>
</div>
</div>
{/* ── 素材 URL ── */}
<div className="pip-field">
<label className="pip-field-label">
{layer.material_type === "image" ? "图片" : "视频"} URL
</label>
<input
className="pip-input"
type="text"
placeholder={
layer.material_type === "image"
? "https://example.com/image.png"
: "https://example.com/video.mp4"
}
value={layer.material_url}
onChange={(e) => onUpdate(layer.id, { material_url: e.target.value })}
/>
</div>
{/* ── 位置:九宫格 + 坐标 ── */}
<div className="pip-field">
<label className="pip-field-label"></label>
<div style={{ display: "flex", gap: 16, alignItems: "flex-start" }}>
<div className="pip-grid">
{GRID_POSITIONS.map((pos) => (
<button
key={pos}
className={`pip-grid-btn${layer.grid_position === pos ? " active" : ""}`}
onClick={() => onGridClick(pos)}
>
<span className="pip-grid-dot" />
</button>
))}
</div>
<div className="pip-field-row" style={{ flex: 1 }}>
<div>
<label className="pip-field-label">X (%)</label>
<input
className="pip-number"
type="number"
min={0}
max={100}
value={layer.x}
onChange={(e) => onUpdate(layer.id, { x: Number(e.target.value) })}
/>
</div>
<div>
<label className="pip-field-label">Y (%)</label>
<input
className="pip-number"
type="number"
min={0}
max={100}
value={layer.y}
onChange={(e) => onUpdate(layer.id, { y: Number(e.target.value) })}
/>
</div>
</div>
</div>
</div>
{/* ── 尺寸 ── */}
<div className="pip-field">
<label className="pip-field-label"></label>
<div className="pip-slider-row">
<span style={{ fontSize: 12, color: "#999", width: 20 }}></span>
<input
className="pip-slider"
type="range"
min={10}
max={80}
value={layer.width}
onChange={(e) => onWidthChange(Number(e.target.value))}
/>
<span className="pip-slider-value">{layer.width}%</span>
</div>
<div className="pip-slider-row" style={{ marginTop: 6 }}>
<span style={{ fontSize: 12, color: "#999", width: 20 }}></span>
<input
className="pip-slider"
type="range"
min={10}
max={80}
value={layer.height}
onChange={(e) => onHeightChange(Number(e.target.value))}
/>
<span className="pip-slider-value">{layer.height}%</span>
</div>
<div
className="pip-lock-row"
style={{ marginTop: 6 }}
onClick={() => onUpdate(layer.id, { aspect_lock: !layer.aspect_lock })}
>
<span className="pip-lock-icon">{layer.aspect_lock ? "🔒" : "🔓"}</span>
<span>{layer.aspect_lock ? "已锁定比例" : "锁定宽高比"}</span>
</div>
</div>
{/* ── 圆角 ── */}
<div className="pip-field">
<label className="pip-field-label"></label>
<div className="pip-slider-row">
<input
className="pip-slider"
type="range"
min={0}
max={50}
value={layer.border_radius}
onChange={(e) => onUpdate(layer.id, { border_radius: Number(e.target.value) })}
/>
<span className="pip-slider-value">{layer.border_radius}%</span>
</div>
</div>
{/* ── 透明度 ── */}
<div className="pip-field">
<label className="pip-field-label"></label>
<div className="pip-slider-row">
<input
className="pip-slider"
type="range"
min={0}
max={100}
value={layer.opacity}
onChange={(e) => onUpdate(layer.id, { opacity: Number(e.target.value) })}
/>
<span className="pip-slider-value">{layer.opacity}%</span>
</div>
</div>
{/* ── 时间 ── */}
<div className="pip-field">
<label className="pip-field-label"></label>
<div className="pip-field-row">
<div>
<label className="pip-field-label"> (s)</label>
<input
className="pip-number"
type="number"
min={0}
max={totalDuration || 999}
step={0.1}
value={layer.start_time}
onChange={(e) => onUpdate(layer.id, { start_time: Number(e.target.value) })}
/>
</div>
<div>
<label className="pip-field-label"> (s)</label>
<input
className="pip-number"
type="number"
min={0.1}
max={totalDuration || 999}
step={0.1}
value={layer.duration}
onChange={(e) => onUpdate(layer.id, { duration: Number(e.target.value) })}
/>
</div>
</div>
</div>
{/* ── 入场动画 ── */}
<div className="pip-field">
<label className="pip-field-label"></label>
<select
className="pip-select"
value={layer.animation}
onChange={(e) => onUpdate(layer.id, { animation: e.target.value as PipAnimType })}
>
{ANIM_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
{/* 滑入方向(仅 slide_in 时显示) */}
{layer.animation === "slide_in" && (
<div className="pip-field">
<label className="pip-field-label"></label>
<select
className="pip-select"
value={layer.slide_direction}
onChange={(e) =>
onUpdate(layer.id, { slide_direction: e.target.value as PipSlideDirection })
}
>
{SLIDE_DIR_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
)}
</div>
)
}
export default LayerConfig
@@ -1,172 +0,0 @@
import React from "react"
import type { PipLayer, PipGridPosition } from "@/pages/editing-planner/types"
import { GRID_POSITIONS } from "@/pages/editing-planner/constants/pipConfig"
interface LayerPositionSizeProps {
layer: PipLayer
onUpdate: (id: string, partial: Partial<PipLayer>) => void
onGridClick: (pos: PipGridPosition) => void
onWidthChange: (val: number) => void
onHeightChange: (val: number) => void
}
/**
* 图层位置与尺寸配置面板
*/
export const LayerPositionSize: React.FC<LayerPositionSizeProps> = ({
layer,
onUpdate,
onGridClick,
onWidthChange,
onHeightChange,
}) => (
<>
{/* 素材类型 */}
<div className="pip-field">
<label className="pip-field-label"></label>
<div className="pip-type-btns">
<button
className={`pip-type-btn${layer.material_type === "image" ? " active" : ""}`}
onClick={() => onUpdate(layer.id, { material_type: "image" })}
>
🖼
</button>
<button
className={`pip-type-btn${layer.material_type === "video" ? " active" : ""}`}
onClick={() => onUpdate(layer.id, { material_type: "video" })}
>
🎬
</button>
</div>
</div>
{/* 素材 URL */}
<div className="pip-field">
<label className="pip-field-label">
{layer.material_type === "image" ? "图片" : "视频"} URL
</label>
<input
className="pip-input"
type="text"
placeholder={
layer.material_type === "image"
? "https://example.com/image.png"
: "https://example.com/video.mp4"
}
value={layer.material_url}
onChange={(e) => onUpdate(layer.id, { material_url: e.target.value })}
/>
</div>
{/* 位置:九宫格 + 坐标 */}
<div className="pip-field">
<label className="pip-field-label"></label>
<div style={{ display: "flex", gap: 16, alignItems: "flex-start" }}>
<div className="pip-grid">
{GRID_POSITIONS.map((pos) => (
<button
key={pos}
className={`pip-grid-btn${layer.grid_position === pos ? " active" : ""}`}
onClick={() => onGridClick(pos)}
>
<span className="pip-grid-dot" />
</button>
))}
</div>
<div className="pip-field-row" style={{ flex: 1 }}>
<div>
<label className="pip-field-label">X (%)</label>
<input
className="pip-number"
type="number"
min={0}
max={100}
value={layer.x}
onChange={(e) => onUpdate(layer.id, { x: Number(e.target.value) })}
/>
</div>
<div>
<label className="pip-field-label">Y (%)</label>
<input
className="pip-number"
type="number"
min={0}
max={100}
value={layer.y}
onChange={(e) => onUpdate(layer.id, { y: Number(e.target.value) })}
/>
</div>
</div>
</div>
</div>
{/* 尺寸 */}
<div className="pip-field">
<label className="pip-field-label"></label>
<div className="pip-slider-row">
<span style={{ fontSize: 12, color: "#999", width: 20 }}></span>
<input
className="pip-slider"
type="range"
min={10}
max={80}
value={layer.width}
onChange={(e) => onWidthChange(Number(e.target.value))}
/>
<span className="pip-slider-value">{layer.width}%</span>
</div>
<div className="pip-slider-row" style={{ marginTop: 6 }}>
<span style={{ fontSize: 12, color: "#999", width: 20 }}></span>
<input
className="pip-slider"
type="range"
min={10}
max={80}
value={layer.height}
onChange={(e) => onHeightChange(Number(e.target.value))}
/>
<span className="pip-slider-value">{layer.height}%</span>
</div>
<div
className="pip-lock-row"
style={{ marginTop: 6 }}
onClick={() => onUpdate(layer.id, { aspect_lock: !layer.aspect_lock })}
>
<span className="pip-lock-icon">{layer.aspect_lock ? "🔒" : "🔓"}</span>
<span>{layer.aspect_lock ? "已锁定比例" : "锁定宽高比"}</span>
</div>
</div>
{/* 圆角 */}
<div className="pip-field">
<label className="pip-field-label"></label>
<div className="pip-slider-row">
<input
className="pip-slider"
type="range"
min={0}
max={50}
value={layer.border_radius}
onChange={(e) => onUpdate(layer.id, { border_radius: Number(e.target.value) })}
/>
<span className="pip-slider-value">{layer.border_radius}%</span>
</div>
</div>
{/* 透明度 */}
<div className="pip-field">
<label className="pip-field-label"></label>
<div className="pip-slider-row">
<input
className="pip-slider"
type="range"
min={0}
max={100}
value={layer.opacity}
onChange={(e) => onUpdate(layer.id, { opacity: Number(e.target.value) })}
/>
<span className="pip-slider-value">{layer.opacity}%</span>
</div>
</div>
</>
)
@@ -1,87 +0,0 @@
import React from "react"
import type { PipLayer, PipAnimType, PipSlideDirection } from "@/pages/editing-planner/types"
import { ANIM_OPTIONS, SLIDE_DIR_OPTIONS } from "@/pages/editing-planner/constants/pipConfig"
interface LayerTimingAnimationProps {
layer: PipLayer
totalDuration: number
onUpdate: (id: string, partial: Partial<PipLayer>) => void
}
/**
* 图层时间与动画配置面板
*/
export const LayerTimingAnimation: React.FC<LayerTimingAnimationProps> = ({
layer,
totalDuration,
onUpdate,
}) => (
<>
{/* 时间 */}
<div className="pip-field">
<label className="pip-field-label"></label>
<div className="pip-field-row">
<div>
<label className="pip-field-label"> (s)</label>
<input
className="pip-number"
type="number"
min={0}
max={totalDuration || 999}
step={0.1}
value={layer.start_time}
onChange={(e) => onUpdate(layer.id, { start_time: Number(e.target.value) })}
/>
</div>
<div>
<label className="pip-field-label"> (s)</label>
<input
className="pip-number"
type="number"
min={0.1}
max={totalDuration || 999}
step={0.1}
value={layer.duration}
onChange={(e) => onUpdate(layer.id, { duration: Number(e.target.value) })}
/>
</div>
</div>
</div>
{/* 入场动画 */}
<div className="pip-field">
<label className="pip-field-label"></label>
<select
className="pip-select"
value={layer.animation}
onChange={(e) => onUpdate(layer.id, { animation: e.target.value as PipAnimType })}
>
{ANIM_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
{/* 滑入方向(仅 slide_in 时显示) */}
{layer.animation === "slide_in" && (
<div className="pip-field">
<label className="pip-field-label"></label>
<select
className="pip-select"
value={layer.slide_direction}
onChange={(e) =>
onUpdate(layer.id, { slide_direction: e.target.value as PipSlideDirection })
}
>
{SLIDE_DIR_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
)}
</>
)
@@ -1,33 +0,0 @@
import React from "react"
import type { PipLayer } from "@/pages/editing-planner/types"
import { LAYER_COLORS } from "@/pages/editing-planner/constants/pipConfig"
interface PipPreviewProps {
layers: PipLayer[]
selectedId: string
}
/**
* PIP 图层迷你预览组件
*/
export const PipPreview: React.FC<PipPreviewProps> = ({ layers, selectedId }) => (
<div className="pip-preview-box">
{layers.map((l, idx) => (
<div
key={l.id}
className={`pip-preview-layer${selectedId === l.id ? " selected" : ""}`}
style={{
left: `${l.x}%`,
top: `${l.y}%`,
width: `${l.width}%`,
height: `${l.height}%`,
background: LAYER_COLORS[idx % LAYER_COLORS.length],
opacity: l.opacity / 100,
borderRadius: `${l.border_radius}%`,
}}
>
<span className="pip-preview-label">{l.name}</span>
</div>
))}
</div>
)
@@ -1,57 +0,0 @@
/**
* 混剪单图层配置区
*/
import React from "react"
import type { PipLayer, PipGridPosition } from "@/pages/editing-planner/types"
import { PipPreview } from "./PipPreview"
import { LayerPositionSize } from "./LayerPositionSize"
import { LayerTimingAnimation } from "./LayerTimingAnimation"
interface LayerConfigProps {
layer: PipLayer | null
layers: PipLayer[]
totalDuration: number
onUpdate: (id: string, partial: Partial<PipLayer>) => void
onGridClick: (pos: PipGridPosition) => void
onWidthChange: (val: number) => void
onHeightChange: (val: number) => void
}
const LayerConfig: React.FC<LayerConfigProps> = ({
layer,
layers,
totalDuration,
onUpdate,
onGridClick,
onWidthChange,
onHeightChange,
}) => {
if (!layer) {
return (
<div className="pip-config-area">
<div className="pip-config-empty"></div>
</div>
)
}
return (
<div className="pip-config-area">
{/* 迷你预览 */}
<PipPreview layers={layers} selectedId={layer.id} />
{/* 位置与尺寸 */}
<LayerPositionSize
layer={layer}
onUpdate={onUpdate}
onGridClick={onGridClick}
onWidthChange={onWidthChange}
onHeightChange={onHeightChange}
/>
{/* 时间与动画 */}
<LayerTimingAnimation layer={layer} totalDuration={totalDuration} onUpdate={onUpdate} />
</div>
)
}
export default LayerConfig