diff --git a/apps/web/src/pages/assets/hooks/asset-operations/batch-operations/index.ts b/apps/web/src/pages/assets/hooks/asset-operations/batch-operations/index.ts new file mode 100755 index 000000000..fb51f0659 --- /dev/null +++ b/apps/web/src/pages/assets/hooks/asset-operations/batch-operations/index.ts @@ -0,0 +1,4 @@ +export { useBatchDelete } from "./useBatchDelete" +export { useBatchTag } from "./useBatchTag" +export { useBatchClassify } from "./useBatchClassify" +export { useBatchMark } from "./useBatchMark" diff --git a/apps/web/src/pages/assets/hooks/asset-operations/batch-operations/useBatchClassify.ts b/apps/web/src/pages/assets/hooks/asset-operations/batch-operations/useBatchClassify.ts new file mode 100755 index 000000000..5ba8d6d37 --- /dev/null +++ b/apps/web/src/pages/assets/hooks/asset-operations/batch-operations/useBatchClassify.ts @@ -0,0 +1,58 @@ +import { useState, useCallback } from "react" +import { message } from "antd" +import { batchClassifyAssets, type BatchOperationResult } from "@/api/assets" + +interface UseBatchClassifyOptions { + selectedIds: Set + queryClient: ReturnType + showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void +} + +export const useBatchClassify = ({ + selectedIds, + queryClient, + showResult, +}: UseBatchClassifyOptions) => { + const [classifyModalOpen, setClassifyModalOpen] = useState(false) + const [batchCategory, setBatchCategory] = useState("") + const [batchLoading, setBatchLoading] = useState(false) + + const handleBatchClassify = useCallback(async () => { + if (!batchCategory) { + message.warning("请选择分类") + return + } + const ids = Array.from(selectedIds) + setBatchLoading(true) + try { + const result = await batchClassifyAssets({ + asset_ids: ids, + category: batchCategory, + }) + queryClient.invalidateQueries({ queryKey: ["assets"] }) + showResult(result, "批量改分类") + setClassifyModalOpen(false) + setBatchCategory("") + if (result.failure_count === 0) { + message.success(`成功将 ${result.success_count} 个素材改为「${batchCategory}」`) + } else { + message.warning( + `改分类完成:成功 ${result.success_count} 个,失败 ${result.failure_count} 个`, + ) + } + } catch { + message.error("批量改分类失败,请重试") + } finally { + setBatchLoading(false) + } + }, [batchCategory, selectedIds, queryClient, showResult]) + + return { + classifyModalOpen, + setClassifyModalOpen, + batchCategory, + setBatchCategory, + batchLoading, + handleBatchClassify, + } +} diff --git a/apps/web/src/pages/assets/hooks/asset-operations/batch-operations/useBatchDelete.ts b/apps/web/src/pages/assets/hooks/asset-operations/batch-operations/useBatchDelete.ts new file mode 100755 index 000000000..b1243e1df --- /dev/null +++ b/apps/web/src/pages/assets/hooks/asset-operations/batch-operations/useBatchDelete.ts @@ -0,0 +1,40 @@ +import { useState, useCallback } from "react" +import { message } from "antd" +import { batchDeleteAssets, type BatchOperationResult } from "@/api/assets" + +interface UseBatchDeleteOptions { + selectedIds: Set + invalidateAssets: () => void + showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void +} + +export const useBatchDelete = ({ + selectedIds, + invalidateAssets, + showResult, +}: UseBatchDeleteOptions) => { + const [batchLoading, setBatchLoading] = useState(false) + + const handleBatchDelete = useCallback(async () => { + const ids = Array.from(selectedIds) + setBatchLoading(true) + try { + const result = await batchDeleteAssets(ids) + invalidateAssets() + showResult(result, "批量删除") + if (result.failure_count === 0) { + message.success(`成功删除 ${result.success_count} 个素材`) + } else { + message.warning( + `删除完成:成功 ${result.success_count} 个,失败 ${result.failure_count} 个`, + ) + } + } catch { + message.error("批量删除失败,请重试") + } finally { + setBatchLoading(false) + } + }, [selectedIds, invalidateAssets, showResult]) + + return { batchLoading, handleBatchDelete } +} diff --git a/apps/web/src/pages/assets/hooks/asset-operations/batch-operations/useBatchMark.ts b/apps/web/src/pages/assets/hooks/asset-operations/batch-operations/useBatchMark.ts new file mode 100755 index 000000000..296888b1a --- /dev/null +++ b/apps/web/src/pages/assets/hooks/asset-operations/batch-operations/useBatchMark.ts @@ -0,0 +1,53 @@ +import { useState, useCallback } from "react" +import { message } from "antd" +import { batchMarkAssets, type BatchOperationResult } from "@/api/assets" +import type { SmartViewType } from "../../../components/BatchMarkModal" +import { SMART_VIEW_LABELS } from "../constants" + +interface UseBatchMarkOptions { + selectedIds: Set + queryClient: ReturnType + showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void +} + +export const useBatchMark = ({ selectedIds, queryClient, showResult }: UseBatchMarkOptions) => { + const [markModalOpen, setMarkModalOpen] = useState(false) + const [batchSmartView, setBatchSmartView] = useState("recommended") + const [batchLoading, setBatchLoading] = useState(false) + + const handleBatchMark = useCallback(async () => { + const ids = Array.from(selectedIds) + setBatchLoading(true) + try { + const result = await batchMarkAssets({ + asset_ids: ids, + smart_view: batchSmartView, + }) + queryClient.invalidateQueries({ queryKey: ["assets"] }) + showResult(result, "批量智能标记") + setMarkModalOpen(false) + if (result.failure_count === 0) { + message.success( + `成功将 ${result.success_count} 个素材标记为「${SMART_VIEW_LABELS[batchSmartView]}」`, + ) + } else { + message.warning( + `智能标记完成:成功 ${result.success_count} 个,失败 ${result.failure_count} 个`, + ) + } + } catch { + message.error("批量智能标记失败,请重试") + } finally { + setBatchLoading(false) + } + }, [batchSmartView, selectedIds, queryClient, showResult]) + + return { + markModalOpen, + setMarkModalOpen, + batchSmartView, + setBatchSmartView, + batchLoading, + handleBatchMark, + } +} diff --git a/apps/web/src/pages/assets/hooks/asset-operations/batch-operations/useBatchTag.ts b/apps/web/src/pages/assets/hooks/asset-operations/batch-operations/useBatchTag.ts new file mode 100755 index 000000000..6f13ad359 --- /dev/null +++ b/apps/web/src/pages/assets/hooks/asset-operations/batch-operations/useBatchTag.ts @@ -0,0 +1,86 @@ +import { useState, useCallback } from "react" +import { message } from "antd" +import { batchTagAssets, type BatchOperationResult } from "@/api/assets" + +interface UseBatchTagOptions { + selectedIds: Set + queryClient: ReturnType + showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void +} + +export const useBatchTag = ({ selectedIds, queryClient, showResult }: UseBatchTagOptions) => { + const [tagModalOpen, setTagModalOpen] = useState(false) + const [batchTagInput, setBatchTagInput] = useState("") + const [batchTags, setBatchTags] = useState([]) + const [tagMode, setTagMode] = useState<"add" | "replace">("add") + const [batchLoading, setBatchLoading] = useState(false) + + const handleBatchTag = useCallback(async () => { + if (batchTags.length === 0) { + message.warning("请至少输入一个标签") + return + } + const ids = Array.from(selectedIds) + setBatchLoading(true) + try { + const result = await batchTagAssets({ + asset_ids: ids, + tags: batchTags, + mode: tagMode, + }) + queryClient.invalidateQueries({ queryKey: ["assets"] }) + showResult(result, "批量打标签") + setTagModalOpen(false) + setBatchTags([]) + setBatchTagInput("") + setTagMode("add") + if (result.failure_count === 0) { + message.success(`成功为 ${result.success_count} 个素材打标签`) + } else { + message.warning( + `打标签完成:成功 ${result.success_count} 个,失败 ${result.failure_count} 个`, + ) + } + } catch { + message.error("批量打标签失败,请重试") + } finally { + setBatchLoading(false) + } + }, [batchTags, selectedIds, tagMode, queryClient, showResult]) + + const handleTagInputKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" && batchTagInput.trim()) { + e.preventDefault() + const tag = batchTagInput.trim() + if (!batchTags.includes(tag)) { + setBatchTags([...batchTags, tag]) + } + setBatchTagInput("") + } + }, + [batchTagInput, batchTags], + ) + + const removeBatchTag = useCallback( + (tag: string) => { + setBatchTags(batchTags.filter((t) => t !== tag)) + }, + [batchTags], + ) + + return { + tagModalOpen, + setTagModalOpen, + batchTagInput, + setBatchTagInput, + batchTags, + setBatchTags, + tagMode, + setTagMode, + batchLoading, + handleBatchTag, + handleTagInputKeyDown, + removeBatchTag, + } +} diff --git a/apps/web/src/pages/assets/hooks/asset-operations/batchOperations.ts b/apps/web/src/pages/assets/hooks/asset-operations/batchOperations.ts deleted file mode 100644 index 41e585758..000000000 --- a/apps/web/src/pages/assets/hooks/asset-operations/batchOperations.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { useState, useCallback } from "react" -import { message } from "antd" -import { - batchDeleteAssets, - batchTagAssets, - batchClassifyAssets, - batchMarkAssets, - type BatchOperationResult, -} from "@/api/assets" -import type { SmartViewType } from "../../components/BatchMarkModal" -import { SMART_VIEW_LABELS } from "./constants" - -/* ── 批量删除 ── */ -interface UseBatchDeleteOptions { - selectedIds: Set - invalidateAssets: () => void - showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void -} - -export const useBatchDelete = ({ - selectedIds, - invalidateAssets, - showResult, -}: UseBatchDeleteOptions) => { - const [batchLoading, setBatchLoading] = useState(false) - - const handleBatchDelete = useCallback(async () => { - const ids = Array.from(selectedIds) - setBatchLoading(true) - try { - const result = await batchDeleteAssets(ids) - invalidateAssets() - showResult(result, "批量删除") - if (result.failure_count === 0) { - message.success(`成功删除 ${result.success_count} 个素材`) - } else { - message.warning( - `删除完成:成功 ${result.success_count} 个,失败 ${result.failure_count} 个`, - ) - } - } catch { - message.error("批量删除失败,请重试") - } finally { - setBatchLoading(false) - } - }, [selectedIds, invalidateAssets, showResult]) - - return { batchLoading, handleBatchDelete } -} - -/* ── 批量打标签 ── */ -interface UseBatchTagOptions { - selectedIds: Set - queryClient: ReturnType - showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void -} - -export const useBatchTag = ({ selectedIds, queryClient, showResult }: UseBatchTagOptions) => { - const [tagModalOpen, setTagModalOpen] = useState(false) - const [batchTagInput, setBatchTagInput] = useState("") - const [batchTags, setBatchTags] = useState([]) - const [tagMode, setTagMode] = useState<"add" | "replace">("add") - const [batchLoading, setBatchLoading] = useState(false) - - const handleBatchTag = useCallback(async () => { - if (batchTags.length === 0) { - message.warning("请至少输入一个标签") - return - } - const ids = Array.from(selectedIds) - setBatchLoading(true) - try { - const result = await batchTagAssets({ - asset_ids: ids, - tags: batchTags, - mode: tagMode, - }) - queryClient.invalidateQueries({ queryKey: ["assets"] }) - showResult(result, "批量打标签") - setTagModalOpen(false) - setBatchTags([]) - setBatchTagInput("") - setTagMode("add") - if (result.failure_count === 0) { - message.success(`成功为 ${result.success_count} 个素材打标签`) - } else { - message.warning( - `打标签完成:成功 ${result.success_count} 个,失败 ${result.failure_count} 个`, - ) - } - } catch { - message.error("批量打标签失败,请重试") - } finally { - setBatchLoading(false) - } - }, [batchTags, selectedIds, tagMode, queryClient, showResult]) - - const handleTagInputKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === "Enter" && batchTagInput.trim()) { - e.preventDefault() - const tag = batchTagInput.trim() - if (!batchTags.includes(tag)) { - setBatchTags([...batchTags, tag]) - } - setBatchTagInput("") - } - }, - [batchTagInput, batchTags], - ) - - const removeBatchTag = useCallback( - (tag: string) => { - setBatchTags(batchTags.filter((t) => t !== tag)) - }, - [batchTags], - ) - - return { - tagModalOpen, - setTagModalOpen, - batchTagInput, - setBatchTagInput, - batchTags, - setBatchTags, - tagMode, - setTagMode, - batchLoading, - handleBatchTag, - handleTagInputKeyDown, - removeBatchTag, - } -} - -/* ── 批量改分类 ── */ -interface UseBatchClassifyOptions { - selectedIds: Set - queryClient: ReturnType - showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void -} - -export const useBatchClassify = ({ - selectedIds, - queryClient, - showResult, -}: UseBatchClassifyOptions) => { - const [classifyModalOpen, setClassifyModalOpen] = useState(false) - const [batchCategory, setBatchCategory] = useState("") - const [batchLoading, setBatchLoading] = useState(false) - - const handleBatchClassify = useCallback(async () => { - if (!batchCategory) { - message.warning("请选择分类") - return - } - const ids = Array.from(selectedIds) - setBatchLoading(true) - try { - const result = await batchClassifyAssets({ - asset_ids: ids, - category: batchCategory, - }) - queryClient.invalidateQueries({ queryKey: ["assets"] }) - showResult(result, "批量改分类") - setClassifyModalOpen(false) - setBatchCategory("") - if (result.failure_count === 0) { - message.success(`成功将 ${result.success_count} 个素材改为「${batchCategory}」`) - } else { - message.warning( - `改分类完成:成功 ${result.success_count} 个,失败 ${result.failure_count} 个`, - ) - } - } catch { - message.error("批量改分类失败,请重试") - } finally { - setBatchLoading(false) - } - }, [batchCategory, selectedIds, queryClient, showResult]) - - return { - classifyModalOpen, - setClassifyModalOpen, - batchCategory, - setBatchCategory, - batchLoading, - handleBatchClassify, - } -} - -/* ── 批量智能标记 ── */ -interface UseBatchMarkOptions { - selectedIds: Set - queryClient: ReturnType - showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void -} - -export const useBatchMark = ({ selectedIds, queryClient, showResult }: UseBatchMarkOptions) => { - const [markModalOpen, setMarkModalOpen] = useState(false) - const [batchSmartView, setBatchSmartView] = useState("recommended") - const [batchLoading, setBatchLoading] = useState(false) - - const handleBatchMark = useCallback(async () => { - const ids = Array.from(selectedIds) - setBatchLoading(true) - try { - const result = await batchMarkAssets({ - asset_ids: ids, - smart_view: batchSmartView, - }) - queryClient.invalidateQueries({ queryKey: ["assets"] }) - showResult(result, "批量智能标记") - setMarkModalOpen(false) - if (result.failure_count === 0) { - message.success( - `成功将 ${result.success_count} 个素材标记为「${SMART_VIEW_LABELS[batchSmartView]}」`, - ) - } else { - message.warning( - `智能标记完成:成功 ${result.success_count} 个,失败 ${result.failure_count} 个`, - ) - } - } catch { - message.error("批量智能标记失败,请重试") - } finally { - setBatchLoading(false) - } - }, [batchSmartView, selectedIds, queryClient, showResult]) - - return { - markModalOpen, - setMarkModalOpen, - batchSmartView, - setBatchSmartView, - batchLoading, - handleBatchMark, - } -} diff --git a/apps/web/src/pages/assets/hooks/useAssetOperations.ts b/apps/web/src/pages/assets/hooks/useAssetOperations.ts index 84b78e4b5..87ade2bd0 100644 --- a/apps/web/src/pages/assets/hooks/useAssetOperations.ts +++ b/apps/web/src/pages/assets/hooks/useAssetOperations.ts @@ -12,7 +12,7 @@ import { useBatchTag, useBatchClassify, useBatchMark, -} from "./asset-operations/batchOperations" +} from "./asset-operations/batch-operations" import type { BatchOperationResult } from "@/api/assets" import type { SmartViewType } from "../components/BatchMarkModal" diff --git a/apps/web/src/test/pages/assets/smoke.test.tsx b/apps/web/src/test/pages/assets/smoke.test.tsx index bf1468e9b..0a514c1ce 100644 --- a/apps/web/src/test/pages/assets/smoke.test.tsx +++ b/apps/web/src/test/pages/assets/smoke.test.tsx @@ -36,6 +36,11 @@ import "@/pages/assets/hooks/useLibraryManagement" import "@/pages/assets/hooks/useAssetUpload" import "@/pages/assets/hooks/useAssetSelection" import "@/pages/assets/hooks/useAssetOperations" +import "@/pages/assets/hooks/asset-operations/batch-operations" +import "@/pages/assets/hooks/asset-operations/batch-operations/useBatchDelete" +import "@/pages/assets/hooks/asset-operations/batch-operations/useBatchTag" +import "@/pages/assets/hooks/asset-operations/batch-operations/useBatchClassify" +import "@/pages/assets/hooks/asset-operations/batch-operations/useBatchMark" describe("AssetLibrary module smoke test", () => { it("should load all asset modules", () => {