diff --git a/apps/web/src/pages/assets/AssetLibrary.tsx b/apps/web/src/pages/assets/AssetLibrary.tsx index de1193f9d..64f5f09f5 100644 --- a/apps/web/src/pages/assets/AssetLibrary.tsx +++ b/apps/web/src/pages/assets/AssetLibrary.tsx @@ -4,36 +4,8 @@ * 使用 useQuery 对接后端真实 API(api/assets.ts) */ import React, { useMemo, useState } from "react" -import { - Upload, - Modal as AntModal, - message, - Popconfirm, - Drawer, - Tag, - Input as AntInput, - Radio, - Select as AntSelect, -} from "antd" -import { - PlusOutlined, - SearchOutlined, - InboxOutlined, - VideoCameraOutlined, - PictureOutlined, - PlayCircleOutlined, - CheckOutlined, - DeleteOutlined, - ExperimentOutlined, - LoadingOutlined, - ExclamationCircleOutlined, - TagsOutlined, - FolderOutlined, - ThunderboltOutlined, - CheckCircleOutlined, - CloseCircleOutlined, - AudioOutlined, -} from "@ant-design/icons" +import { Upload, message } from "antd" +import { InboxOutlined, PictureOutlined, ExclamationCircleOutlined } from "@ant-design/icons" import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { getAssetLibraries, @@ -51,170 +23,24 @@ import { type AssetItem as ApiAssetItem, type BatchOperationResult, } from "@/api/assets" -import { Button, Input, Select } from "@/components/ui" -import { - type AssetItem, - type AssetKind, - type StatusType, - mapLibrary, - mapAsset, -} from "@/pages/assets/types" -import { - MAX_FILE_SIZE, - LARGE_FILE_THRESHOLD, - TYPE_FILTER_OPTIONS, - TIME_FILTER_OPTIONS, - LIBRARY_KIND_OPTIONS, - CATEGORY_OPTIONS, -} from "@/pages/assets/constants" -import { kindLabel, thumbGradient } from "@/pages/assets/utils/asset" +import { Button } from "@/components/ui" +import { type AssetItem, type AssetKind, mapLibrary, mapAsset } from "@/pages/assets/types" +import { MAX_FILE_SIZE, LARGE_FILE_THRESHOLD } from "@/pages/assets/constants" +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 type { SmartViewType } from "@/pages/assets/components/BatchMarkModal" +import ResultDrawer from "@/pages/assets/components/ResultDrawer" +import UploadProgressModal from "@/pages/assets/components/UploadProgressModal" import "./assets.css" -/* ============================================================ - * 工具函数(UI 相关) - * ============================================================ */ -const kindIcon = (kind: AssetKind) => { - switch (kind) { - case "video": - return - case "image": - return - case "voice": - return - } -} - -/* ============================================================ - * StatusPill 组件 - * ============================================================ */ -const StatusPill: React.FC<{ status: StatusType; label: string }> = ({ status, label }) => ( - {label} -) - -/* ============================================================ - * SkeletonCard — 骨架屏卡片(素材列表加载时占位) - * ============================================================ */ -const SkeletonCard: React.FC = () => ( -
-
-
-
-
-
-
-
-) - -/* ============================================================ - * AssetCard 组件 - * ============================================================ */ -const AssetCard: React.FC<{ - asset: AssetItem - selected: boolean - diagnosing?: boolean - onToggle: () => void - onDiagnose: () => void - onPlay: () => void - onDelete: () => void -}> = ({ asset, selected, diagnosing, onToggle, onDiagnose, onPlay, onDelete }) => ( -
- {/* 缩略图区 */} -
- {asset.thumbUrl ? ( - {asset.name} - ) : ( - - {asset.loading ? : kindIcon(asset.kind)} - - )} - - {/* 处理中遮罩 */} - {asset.loading && ( -
- - 处理中 -
- )} - - {/* 失败状态标识 */} - {asset.status === "bad" && asset.statusLabel === "处理失败" && ( -
- - 处理失败 -
- )} - - {/* 视频/配音类显示播放按钮(处理中/失败不显示) */} - {asset.kind === "video" && !asset.loading && asset.status !== "bad" && ( - { - e.stopPropagation() - onPlay() - }} - > - - - )} - - {/* 删除按钮 */} - { - e?.stopPropagation() - onDelete() - }} - onCancel={(e) => e?.stopPropagation()} - okText="删除" - cancelText="取消" - okButtonProps={{ danger: true }} - > - e.stopPropagation()}> - - - - - {/* 选中态勾选 */} - {selected && ( - - - - )} -
- - {/* 信息区 */} -
-

- {asset.name} -

-
- - {asset.duration && {asset.duration}} -
- -
-
-) - /* ============================================================ * 主组件 * ============================================================ */ @@ -328,9 +154,7 @@ const AssetLibrary: React.FC = () => { const [batchCategory, setBatchCategory] = useState("") /* 批量智能标记 */ - const [batchSmartView, setBatchSmartView] = useState<"recommended" | "caution" | "high_risk">( - "recommended", - ) + const [batchSmartView, setBatchSmartView] = useState("recommended") /* 操作结果 */ const [operationResult, setOperationResult] = useState(null) @@ -474,7 +298,6 @@ const AssetLibrary: React.FC = () => { } } - /* 批量删除 */ /* 单个素材删除 */ const handleSingleDelete = async (assetId: string) => { try { @@ -493,6 +316,7 @@ const AssetLibrary: React.FC = () => { } } + /* 批量删除 */ const handleBatchDelete = async () => { const ids = Array.from(selectedIds) setBatchLoading(true) @@ -604,7 +428,7 @@ const AssetLibrary: React.FC = () => { setMarkModalOpen(false) queryClient.invalidateQueries({ queryKey: ["assets"] }) setSelectedIds(new Set()) - const labelMap = { + const labelMap: Record = { recommended: "推荐", caution: "慎用", high_risk: "高风险", @@ -656,94 +480,19 @@ const AssetLibrary: React.FC = () => { return (
- {/* ─── 上传进度弹窗(圆形动画 + 百分比) ─── */} - -
- - {/* 背景圆环 */} - - {/* 进度圆弧 */} - - -
- {uploadProgress}% - 上传中… -
-
-
+ {/* ─── 上传进度弹窗 ─── */} + {/* 两栏布局 */}
{/* ─── 左侧:视频库列表 ─── */} -
- {libraries.map((lib) => ( -
setActiveLibId(lib.id)} - > -
-

- {kindIcon(lib.kind)} {lib.name} -

- { - e?.stopPropagation() - handleDeleteLibrary(lib.id) - }} - onCancel={(e) => e?.stopPropagation()} - okText="删除" - cancelText="取消" - > - - -
- - {kindLabel(lib.kind)} · {lib.count} 个素材 - -
- ))} - - {/* 新建视频库 */} -
setCreateModalOpen(true)}> - - 新建视频库 -
-
+ setCreateModalOpen(true)} + /> {/* ─── 右侧:内容区 ─── */}
@@ -771,79 +520,27 @@ const AssetLibrary: React.FC = () => { {/* 筛选栏 */} -
-
- } - value={searchText} - onChange={(e) => setSearchText(e.target.value)} - allowClear - style={{ width: 220 }} - /> - -
-
- - 共 {filteredAssets.length} 个素材 -
-
+ {/* 批量操作栏 */} {selectedIds.size > 0 && ( -
- 已选 {selectedIds.size} 项 - - - - - - - -
+ setTagModalOpen(true)} + onClassifyClick={() => setClassifyModalOpen(true)} + onMarkClick={() => setMarkModalOpen(true)} + onBatchDelete={handleBatchDelete} + /> )} {/* 素材网格 */} @@ -890,231 +587,75 @@ const AssetLibrary: React.FC = () => {
{/* ─── 新建视频库弹窗 ─── */} - setCreateModalOpen(false)} onOk={handleCreateLibrary} - okText="创建" - cancelText="取消" - destroyOnClose + name={newLibName} + onNameChange={setNewLibName} + kind={newLibKind} + onKindChange={setNewLibKind} confirmLoading={createLibMutation.isPending} - > -
-
-
名称
- setNewLibName(e.target.value)} - maxLength={50} - /> -
-
-
类型
- } + value={searchText} + onChange={(e) => onSearchChange(e.target.value)} + allowClear + style={{ width: 220 }} + /> + +
+
+ + 共 {totalCount} 个素材 +
+
+) + +export default AssetFilterBar diff --git a/apps/web/src/pages/assets/components/AssetSkeleton.tsx b/apps/web/src/pages/assets/components/AssetSkeleton.tsx new file mode 100644 index 000000000..8af74816a --- /dev/null +++ b/apps/web/src/pages/assets/components/AssetSkeleton.tsx @@ -0,0 +1,36 @@ +import React from "react" +import type { StatusType } from "@/pages/assets/types" + +/* ============================================================ + * StatusPill — 状态标签 + * ============================================================ */ +export interface StatusPillProps { + status: StatusType + label: string +} + +export const StatusPill: React.FC = ({ status, label }) => ( + {label} +) + +/* ============================================================ + * SkeletonCard — 骨架屏卡片(素材列表加载时占位) + * ============================================================ */ +export const SkeletonCard: React.FC = () => ( +
+
+
+
+
+
+
+
+) diff --git a/apps/web/src/pages/assets/components/BatchClassifyModal.tsx b/apps/web/src/pages/assets/components/BatchClassifyModal.tsx new file mode 100644 index 000000000..fef1673ae --- /dev/null +++ b/apps/web/src/pages/assets/components/BatchClassifyModal.tsx @@ -0,0 +1,49 @@ +import React from "react" +import { Modal as AntModal, Select as AntSelect } from "antd" +import { CATEGORY_OPTIONS } from "@/pages/assets/constants" + +/* ============================================================ + * BatchClassifyModal — 批量改分类弹窗 + * ============================================================ */ +export interface BatchClassifyModalProps { + open: boolean + selectedCount: number + onCancel: () => void + onOk: () => void + category: string + onCategoryChange: (value: string) => void + confirmLoading?: boolean +} + +const BatchClassifyModal: React.FC = ({ + open, + selectedCount, + onCancel, + onOk, + category, + onCategoryChange, + confirmLoading, +}) => ( + +
+

将选中的 {selectedCount} 个素材统一修改为以下分类:

+ onCategoryChange(v)} + placeholder="请选择分类" + style={{ width: "100%" }} + options={CATEGORY_OPTIONS} + /> +
+
+) + +export default BatchClassifyModal diff --git a/apps/web/src/pages/assets/components/BatchMarkModal.tsx b/apps/web/src/pages/assets/components/BatchMarkModal.tsx new file mode 100644 index 000000000..95a61628f --- /dev/null +++ b/apps/web/src/pages/assets/components/BatchMarkModal.tsx @@ -0,0 +1,67 @@ +import React from "react" +import { Modal as AntModal, Tag, Radio } from "antd" + +/* ============================================================ + * BatchMarkModal — 批量智能标记弹窗 + * ============================================================ */ +export type SmartViewType = "recommended" | "caution" | "high_risk" + +export interface BatchMarkModalProps { + open: boolean + selectedCount: number + onCancel: () => void + onOk: () => void + smartView: SmartViewType + onSmartViewChange: (value: SmartViewType) => void + confirmLoading?: boolean +} + +const BatchMarkModal: React.FC = ({ + open, + selectedCount, + onCancel, + onOk, + smartView, + onSmartViewChange, + confirmLoading, +}) => ( + +
+

将选中的 {selectedCount} 个素材标记为:

+ onSmartViewChange(e.target.value)} + className="xx-batch-mark-options" + > +
+ + 推荐 + 质量优良,可直接用于生产 + +
+
+ + 慎用 + 存在一定问题,需人工审核后再使用 + +
+
+ + 高风险 + 存在严重问题,不建议使用 + +
+
+
+
+) + +export default BatchMarkModal diff --git a/apps/web/src/pages/assets/components/BatchOperationBar.tsx b/apps/web/src/pages/assets/components/BatchOperationBar.tsx new file mode 100644 index 000000000..f420d4a29 --- /dev/null +++ b/apps/web/src/pages/assets/components/BatchOperationBar.tsx @@ -0,0 +1,58 @@ +import React from "react" +import { + TagsOutlined, + FolderOutlined, + ThunderboltOutlined, + DeleteOutlined, +} from "@ant-design/icons" +import { Popconfirm } from "antd" +import { Button } from "@/components/ui" + +/* ============================================================ + * BatchOperationBar — 批量操作栏 + * ============================================================ */ +export interface BatchOperationBarProps { + selectedCount: number + onDeselectAll: () => void + onTagClick: () => void + onClassifyClick: () => void + onMarkClick: () => void + onBatchDelete: () => void +} + +const BatchOperationBar: React.FC = ({ + selectedCount, + onDeselectAll, + onTagClick, + onClassifyClick, + onMarkClick, + onBatchDelete, +}) => ( +
+ 已选 {selectedCount} 项 + + + + + + + +
+) + +export default BatchOperationBar diff --git a/apps/web/src/pages/assets/components/BatchTagModal.tsx b/apps/web/src/pages/assets/components/BatchTagModal.tsx new file mode 100644 index 000000000..74db258ee --- /dev/null +++ b/apps/web/src/pages/assets/components/BatchTagModal.tsx @@ -0,0 +1,81 @@ +import React from "react" +import { Modal as AntModal, Tag, Radio, Input as AntInput } from "antd" +import { ExclamationCircleOutlined } from "@ant-design/icons" + +/* ============================================================ + * BatchTagModal — 批量打标签弹窗 + * ============================================================ */ +export interface BatchTagModalProps { + open: boolean + selectedCount: number + onCancel: () => void + onOk: () => void + tags: string[] + onTagInputChange: (value: string) => void + onTagInputKeyDown: (e: React.KeyboardEvent) => void + onRemoveTag: (tag: string) => void + tagInput: string + tagMode: "add" | "replace" + onTagModeChange: (mode: "add" | "replace") => void + confirmLoading?: boolean +} + +const BatchTagModal: React.FC = ({ + open, + selectedCount, + onCancel, + onOk, + tags, + onTagInputChange, + onTagInputKeyDown, + onRemoveTag, + tagInput, + tagMode, + onTagModeChange, + confirmLoading, +}) => ( + +
+
+ 模式: + onTagModeChange(e.target.value)}> + 追加标签 + 替换全部标签 + +
+
+ onTagInputChange(e.target.value)} + onKeyDown={onTagInputKeyDown} + style={{ flex: 1 }} + /> +
+ {tags.length > 0 && ( +
+ {tags.map((tag) => ( + onRemoveTag(tag)} color="blue"> + {tag} + + ))} +
+ )} + {tagMode === "replace" && tags.length > 0 && ( +
+ 替换模式将清除素材原有全部标签 +
+ )} +
+
+) + +export default BatchTagModal diff --git a/apps/web/src/pages/assets/components/CreateLibraryModal.tsx b/apps/web/src/pages/assets/components/CreateLibraryModal.tsx new file mode 100644 index 000000000..d66693bc7 --- /dev/null +++ b/apps/web/src/pages/assets/components/CreateLibraryModal.tsx @@ -0,0 +1,64 @@ +import React from "react" +import { Modal as AntModal } from "antd" +import { Input, Select } from "@/components/ui" +import type { AssetKind } from "@/pages/assets/types" +import { LIBRARY_KIND_OPTIONS } from "@/pages/assets/constants" + +/* ============================================================ + * CreateLibraryModal — 新建视频库弹窗 + * ============================================================ */ +export interface CreateLibraryModalProps { + open: boolean + onCancel: () => void + onOk: () => void + name: string + onNameChange: (value: string) => void + kind: AssetKind + onKindChange: (value: AssetKind) => void + confirmLoading?: boolean +} + +const CreateLibraryModal: React.FC = ({ + open, + onCancel, + onOk, + name, + onNameChange, + kind, + onKindChange, + confirmLoading, +}) => ( + +
+
+
名称
+ onNameChange(e.target.value)} + maxLength={50} + /> +
+
+
类型
+