Files
xiaoxia-saas/apps/web/src/pages/assets/AssetLibrary.tsx
T
xiaoxia 5c45629495
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
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
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been skipped
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 / Check push changed paths (push) Successful in 4m37s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4m37s
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 / Validate - Type Check (mypy) (push) Successful in 4m50s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 5m5s
AI Code Review / AI Code Review (pull_request) Successful in 7m56s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m40s
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 8m15s
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m37s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m48s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m55s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 10m20s
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 / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) 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 / PR Build Worker Image (pull_request) Successful in 2m1s
CI/CD Pipeline / Retag skipped Staging API Image (push) Successful in 2m18s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1m49s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Successful in 2m16s
CI/CD Pipeline / Integration Tests (push) Successful in 4m27s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m0s
CI/CD Pipeline / Unit Tests (push) Successful in 15m57s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / CI Gate (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) Successful in 2m25s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m10s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Successful in 4m44s
feat(assets): 上传即入库——批量上传队列化(并发3)+真实进度+刷新不丢 (#1557)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-30 17:56:02 +08:00

277 lines
8.5 KiB
TypeScript

/**
* 视频库页面 — V21 设计系统
* 两栏布局:左侧视频库列表(260px)+ 右侧素材网格
*
* 主组件仅保留 Hook 组装与整体布局
* 数据查询 → hooks/useAssetsData
* 库管理 → hooks/useLibraryManagement
* 上传 → hooks/useAssetUpload
* 选中态 → hooks/useAssetSelection
* 素材操作 → hooks/useAssetOperations
* 上传区 → components/AssetUploadZone
* 网格区 → components/AssetGridSection
* 弹窗集合 → components/AssetModals
*/
import React, { useState } from "react"
import type { AssetItem } from "@/pages/assets/types"
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 AssetUploadZone from "@/pages/assets/components/AssetUploadZone"
import UploadQueuePanel from "@/pages/assets/components/UploadQueuePanel"
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"
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 {
uploadItems,
enqueueUploads,
retryUpload,
removeUpload,
clearFinished,
uploading,
activeCount,
pendingCount,
} = useAssetUpload({ effectiveLibId })
/* ── 上传中 asset_id → 进度/状态映射,合并进网格卡片展示真实进度 ── */
const uploadProgressMap = React.useMemo(() => {
const map = new Map<string, { progress: number; uploading: boolean }>()
for (const it of uploadItems) {
if (it.assetId && (it.status === "uploading" || it.status === "ingesting")) {
map.set(it.assetId, {
progress: it.status === "ingesting" ? 100 : it.progress,
uploading: it.status === "uploading",
})
}
}
return map
}, [uploadItems])
/* ── 选中态管理 ── */
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">
{/* 两栏布局 */}
<div className="xx-assets-layout">
{/* ─── 左侧:视频库列表 ─── */}
<LibrarySidebar
libraries={libraries}
activeLibId={effectiveLibId}
onSelect={setActiveLibId}
onDelete={handleDeleteLibrary}
onCreateClick={() => setCreateModalOpen(true)}
/>
{/* ─── 右侧:内容区 ─── */}
<div className="xx-assets-content">
{/* 上传区域 */}
<AssetUploadZone
uploading={uploading}
activeCount={activeCount}
pendingCount={pendingCount}
onUpload={enqueueUploads}
/>
{/* 上传队列:独立进度 + 失败重试/移除 */}
<UploadQueuePanel
items={uploadItems}
onRetry={retryUpload}
onRemove={removeUpload}
onClearFinished={clearFinished}
/>
{/* 筛选栏 */}
<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}
/>
)}
{/* 素材网格 */}
<AssetGridSection
loading={assetsLoading}
error={assetsError}
errorMessage={assetsErrorObj?.message}
assets={filteredAssets}
selectedIds={selectedIds}
diagnosingId={diagnosingId}
uploadProgressMap={uploadProgressMap}
onRetry={refetchAssets}
onToggleSelect={toggleSelect}
onDiagnose={handleDiagnose}
onPlay={setPlayingAsset}
onDelete={handleSingleDelete}
/>
</div>
</div>
{/* ─── 弹窗集合 ─── */}
<AssetModals
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}
onTagCancel={() => {
setTagModalOpen(false)
setBatchTags([])
setBatchTagInput("")
}}
onTagOk={handleBatchTag}
batchTags={batchTags}
batchTagInput={batchTagInput}
onTagInputChange={setBatchTagInput}
onTagInputKeyDown={handleTagInputKeyDown}
onRemoveTag={removeBatchTag}
tagMode={tagMode as "add" | "replace"}
onTagModeChange={setTagMode as (mode: "add" | "replace") => void}
batchLoading={batchLoading}
classifyModalOpen={classifyModalOpen}
onClassifyCancel={() => {
setClassifyModalOpen(false)
setBatchCategory("")
}}
onClassifyOk={handleBatchClassify}
batchCategory={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}
/>
</div>
)
}
export default AssetLibrary