Files
xiaoxia-saas/apps/web/src/pages/assets/AssetLibrary.tsx
T
xiaoxia 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
refactor(assets): Phase 3 - extract business hooks (#899)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-07-25 23:55:16 +08:00

308 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 视频库页面 — V21 设计系统
* 两栏布局:左侧视频库列表(260px)+ 右侧素材网格
* 使用 useQuery 对接后端真实 APIapi/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