Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7d814cbc8 | |||
| e99f80f333 | |||
| d85b181654 | |||
| f5509ffc27 | |||
| dd7452953e |
@@ -1,25 +1,27 @@
|
||||
/**
|
||||
* 视频库页面 — V21 设计系统
|
||||
* 两栏布局:左侧视频库列表(260px)+ 右侧素材网格
|
||||
* 使用 useQuery 对接后端真实 API(api/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
|
||||
Reference in New Issue
Block a user