6437b96e54
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m17s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m8s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m20s
CI/CD Pipeline / Unit Tests (push) Failing after 5m11s
CI/CD Pipeline / Integration Tests (push) Successful in 3m7s
CI/CD Pipeline / Frontend Lint (push) Successful in 55s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m8s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m32s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m27s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m42s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m33s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 8m18s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 11m0s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 28s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
308 lines
9.8 KiB
TypeScript
308 lines
9.8 KiB
TypeScript
/**
|
||
* 视频库页面 — V21 设计系统
|
||
* 两栏布局:左侧视频库列表(260px)+ 右侧素材网格
|
||
* 使用 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 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 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"
|
||
import { useAssetSelection } from "@/pages/assets/hooks/useAssetSelection"
|
||
import { useAssetOperations } from "@/pages/assets/hooks/useAssetOperations"
|
||
import "./assets.css"
|
||
|
||
/* ============================================================
|
||
* 主组件
|
||
* ============================================================ */
|
||
const AssetLibrary: React.FC = () => {
|
||
/* ── 数据查询与筛选 ── */
|
||
const {
|
||
libraries,
|
||
libLoading,
|
||
activeLibId,
|
||
setActiveLibId,
|
||
effectiveLibId,
|
||
assetsLoading,
|
||
assetsError,
|
||
assetsErrorObj,
|
||
refetchAssets,
|
||
searchText,
|
||
setSearchText,
|
||
filterType,
|
||
setFilterType,
|
||
filterTime,
|
||
setFilterTime,
|
||
filteredAssets,
|
||
} = useAssetsData()
|
||
|
||
/* ── 视频库管理 ── */
|
||
const {
|
||
createModalOpen,
|
||
setCreateModalOpen,
|
||
newLibName,
|
||
setNewLibName,
|
||
newLibKind,
|
||
setNewLibKind,
|
||
isCreating,
|
||
handleCreateLibrary,
|
||
handleDeleteLibrary,
|
||
} = useLibraryManagement({
|
||
libraries,
|
||
activeLibId,
|
||
setActiveLibId,
|
||
effectiveLibId,
|
||
})
|
||
|
||
/* ── 上传 ── */
|
||
const { uploading, uploadProgress, handleUpload } = useAssetUpload({ effectiveLibId })
|
||
|
||
/* ── 选中态管理 ── */
|
||
const { selectedIds, setSelectedIds, toggleSelect, selectAll, deselectAll } = useAssetSelection({
|
||
filteredAssets,
|
||
})
|
||
|
||
/* ── 素材操作 ── */
|
||
const {
|
||
diagnosingId,
|
||
handleDiagnose,
|
||
handleSingleDelete,
|
||
batchLoading,
|
||
tagModalOpen,
|
||
setTagModalOpen,
|
||
batchTagInput,
|
||
setBatchTagInput,
|
||
batchTags,
|
||
setBatchTags,
|
||
tagMode,
|
||
setTagMode,
|
||
handleBatchTag,
|
||
handleTagInputKeyDown,
|
||
removeBatchTag,
|
||
classifyModalOpen,
|
||
setClassifyModalOpen,
|
||
batchCategory,
|
||
setBatchCategory,
|
||
handleBatchClassify,
|
||
markModalOpen,
|
||
setMarkModalOpen,
|
||
batchSmartView,
|
||
setBatchSmartView,
|
||
handleBatchMark,
|
||
handleBatchDelete,
|
||
resultDrawerOpen,
|
||
operationResult,
|
||
operationTitle,
|
||
handleResultDrawerClose,
|
||
} = useAssetOperations({ selectedIds, setSelectedIds })
|
||
|
||
/* ── 视频播放 ── */
|
||
const [playingAsset, setPlayingAsset] = useState<AssetItem | null>(null)
|
||
|
||
// ── Loading 状态 ──
|
||
if (libLoading) {
|
||
return (
|
||
<div className="xx-assets-page">
|
||
<div className="xx-assets-skeleton-grid">
|
||
{Array.from({ length: 8 }).map((_, i) => (
|
||
<SkeletonCard key={i} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="xx-assets-page">
|
||
{/* ─── 上传进度弹窗 ─── */}
|
||
<UploadProgressModal open={uploading} progress={uploadProgress} />
|
||
|
||
{/* 两栏布局 */}
|
||
<div className="xx-assets-layout">
|
||
{/* ─── 左侧:视频库列表 ─── */}
|
||
<LibrarySidebar
|
||
libraries={libraries}
|
||
activeLibId={effectiveLibId}
|
||
onSelect={setActiveLibId}
|
||
onDelete={handleDeleteLibrary}
|
||
onCreateClick={() => setCreateModalOpen(true)}
|
||
/>
|
||
|
||
{/* ─── 右侧:内容区 ─── */}
|
||
<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>
|
||
|
||
{/* 筛选栏 */}
|
||
<AssetFilterBar
|
||
searchText={searchText}
|
||
onSearchChange={setSearchText}
|
||
filterType={filterType}
|
||
onFilterTypeChange={setFilterType}
|
||
filterTime={filterTime}
|
||
onFilterTimeChange={setFilterTime}
|
||
onSelectAll={selectAll}
|
||
totalCount={filteredAssets.length}
|
||
/>
|
||
|
||
{/* 批量操作栏 */}
|
||
{selectedIds.size > 0 && (
|
||
<BatchOperationBar
|
||
selectedCount={selectedIds.size}
|
||
onDeselectAll={deselectAll}
|
||
onTagClick={() => setTagModalOpen(true)}
|
||
onClassifyClick={() => setClassifyModalOpen(true)}
|
||
onMarkClick={() => setMarkModalOpen(true)}
|
||
onBatchDelete={handleBatchDelete}
|
||
/>
|
||
)}
|
||
|
||
{/* 素材网格 */}
|
||
{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>
|
||
|
||
{/* ─── 新建视频库弹窗 ─── */}
|
||
<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}
|
||
onCancel={() => {
|
||
setTagModalOpen(false)
|
||
setBatchTags([])
|
||
setBatchTagInput("")
|
||
}}
|
||
onOk={handleBatchTag}
|
||
tags={batchTags}
|
||
onTagInputChange={setBatchTagInput}
|
||
onTagInputKeyDown={handleTagInputKeyDown}
|
||
onRemoveTag={removeBatchTag}
|
||
tagInput={batchTagInput}
|
||
tagMode={tagMode}
|
||
onTagModeChange={setTagMode}
|
||
confirmLoading={batchLoading}
|
||
/>
|
||
|
||
{/* ─── 批量改分类弹窗 ─── */}
|
||
<BatchClassifyModal
|
||
open={classifyModalOpen}
|
||
selectedCount={selectedIds.size}
|
||
onCancel={() => {
|
||
setClassifyModalOpen(false)
|
||
setBatchCategory("")
|
||
}}
|
||
onOk={handleBatchClassify}
|
||
category={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}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default AssetLibrary
|