From 64544d0e5ef20de36af97a162ca24b79a0484041 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sun, 26 Jul 2026 18:20:08 +0800 Subject: [PATCH 1/7] refactor(assets): split useAssetOperations into sub-modules - Extract constants (SMART_VIEW_LABELS) to asset-operations/constants.ts - Extract single operations (diagnose + delete) to useSingleOperations.ts - Extract batch helpers (invalidateAssets, showOperationResult) to useSingleOperations.ts - Extract batch operations (delete/tag/classify/mark) to batchOperations.ts - Main hook reduced from 301 to 165 lines (-45%) - Keep import path @/pages/assets/hooks/useAssetOperations unchanged --- .../hooks/asset-operations/batchOperations.ts | 274 ++++++++++++++++++ .../hooks/asset-operations/constants.ts | 8 + .../asset-operations/useSingleOperations.ts | 92 ++++++ .../pages/assets/hooks/useAssetOperations.ts | 266 +++++------------ 4 files changed, 439 insertions(+), 201 deletions(-) create mode 100644 apps/web/src/pages/assets/hooks/asset-operations/batchOperations.ts create mode 100644 apps/web/src/pages/assets/hooks/asset-operations/constants.ts create mode 100644 apps/web/src/pages/assets/hooks/asset-operations/useSingleOperations.ts diff --git a/apps/web/src/pages/assets/hooks/asset-operations/batchOperations.ts b/apps/web/src/pages/assets/hooks/asset-operations/batchOperations.ts new file mode 100644 index 000000000..2f71325ba --- /dev/null +++ b/apps/web/src/pages/assets/hooks/asset-operations/batchOperations.ts @@ -0,0 +1,274 @@ +import { 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 BatchOperationDeps { + selectedIds: Set + invalidateAssets: () => void + showResult: (result: BatchOperationResult, title: string) => void + setLoading: (loading: boolean) => void + queryClient: ReturnType +} + +interface BatchTagState { + batchTags: string[] + setBatchTags: (tags: string[]) => void + tagMode: "add" | "replace" + setTagMode: (mode: "add" | "replace") => void + setTagModalOpen: (open: boolean) => void + setBatchTagInput: (val: string) => void + batchTagInput: string +} + +interface BatchClassifyState { + batchCategory: string + setBatchCategory: (cat: string) => void + setClassifyModalOpen: (open: boolean) => void +} + +interface BatchMarkState { + batchSmartView: SmartViewType + setBatchSmartView: (view: SmartViewType) => void + setMarkModalOpen: (open: boolean) => void +} + +/** + * 批量删除 + */ +export const useBatchDelete = ({ + selectedIds, + invalidateAssets, + showResult, + setLoading, +}: BatchOperationDeps) => { + return useCallback(async () => { + const ids = Array.from(selectedIds) + setLoading(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 { + setLoading(false) + } + }, [selectedIds, invalidateAssets, showResult, setLoading]) +} + +/** + * 批量打标签 + */ +export const useBatchTag = ( + deps: BatchOperationDeps & + BatchTagState & { + queryClient: ReturnType + }, +) => { + const { + selectedIds, + showResult, + setLoading, + batchTags, + tagMode, + setTagModalOpen, + setBatchTags, + setBatchTagInput, + setTagMode, + queryClient, + } = deps + + const handleBatchTag = useCallback(async () => { + if (batchTags.length === 0) { + message.warning("请至少输入一个标签") + return + } + const ids = Array.from(selectedIds) + setLoading(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 { + setLoading(false) + } + }, [ + batchTags, + selectedIds, + tagMode, + queryClient, + showResult, + setLoading, + setTagModalOpen, + setBatchTags, + setBatchTagInput, + setTagMode, + ]) + + const handleTagInputKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" && deps.batchTagInput.trim()) { + e.preventDefault() + const tag = deps.batchTagInput.trim() + if (!batchTags.includes(tag)) { + setBatchTags([...batchTags, tag]) + } + setBatchTagInput("") + } + }, + [deps.batchTagInput, batchTags, setBatchTags, setBatchTagInput], + ) + + const removeBatchTag = useCallback( + (tag: string) => { + setBatchTags(batchTags.filter((t) => t !== tag)) + }, + [batchTags, setBatchTags], + ) + + return { handleBatchTag, handleTagInputKeyDown, removeBatchTag } +} + +/** + * 批量改分类 + */ +export const useBatchClassify = ( + deps: BatchOperationDeps & + BatchClassifyState & { + queryClient: ReturnType + }, +) => { + const { + selectedIds, + showResult, + setLoading, + batchCategory, + setClassifyModalOpen, + setBatchCategory, + queryClient, + } = deps + + return useCallback(async () => { + if (!batchCategory) { + message.warning("请选择分类") + return + } + const ids = Array.from(selectedIds) + setLoading(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 { + setLoading(false) + } + }, [ + batchCategory, + selectedIds, + queryClient, + showResult, + setLoading, + setClassifyModalOpen, + setBatchCategory, + ]) +} + +/** + * 批量智能标记 + */ +export const useBatchMark = ( + deps: BatchOperationDeps & + BatchMarkState & { + queryClient: ReturnType + }, +) => { + const { + selectedIds, + showResult, + setLoading, + batchSmartView, + setMarkModalOpen, + setBatchSmartView, + queryClient, + } = deps + + return useCallback(async () => { + const ids = Array.from(selectedIds) + setLoading(true) + try { + const result = await batchMarkAssets({ + asset_ids: ids, + smart_view: batchSmartView, + }) + queryClient.invalidateQueries({ queryKey: ["assets"] }) + showResult(result, "批量智能标记") + setMarkModalOpen(false) + const label = SMART_VIEW_LABELS[batchSmartView] + if (result.failure_count === 0) { + message.success(`成功将 ${result.success_count} 个素材标记为「${label}」`) + } else { + message.warning( + `智能标记完成:成功 ${result.success_count} 个,失败 ${result.failure_count} 个`, + ) + } + } catch { + message.error("批量智能标记失败,请重试") + } finally { + setLoading(false) + } + }, [ + batchSmartView, + selectedIds, + queryClient, + showResult, + setLoading, + setMarkModalOpen, + setBatchSmartView, + ]) +} diff --git a/apps/web/src/pages/assets/hooks/asset-operations/constants.ts b/apps/web/src/pages/assets/hooks/asset-operations/constants.ts new file mode 100644 index 000000000..d86e3ec72 --- /dev/null +++ b/apps/web/src/pages/assets/hooks/asset-operations/constants.ts @@ -0,0 +1,8 @@ +import type { SmartViewType } from "../../components/BatchMarkModal" + +/** 智能标记 → 中文标签映射 */ +export const SMART_VIEW_LABELS: Record = { + recommended: "推荐", + caution: "慎用", + high_risk: "高风险", +} diff --git a/apps/web/src/pages/assets/hooks/asset-operations/useSingleOperations.ts b/apps/web/src/pages/assets/hooks/asset-operations/useSingleOperations.ts new file mode 100644 index 000000000..17afc9d0b --- /dev/null +++ b/apps/web/src/pages/assets/hooks/asset-operations/useSingleOperations.ts @@ -0,0 +1,92 @@ +import { useCallback } from "react" +import { useQueryClient } from "@tanstack/react-query" +import { message } from "antd" +import { getAssetDiagnosis, deleteAsset, type BatchOperationResult } from "@/api/assets" +import type { AssetItem } from "../../types" + +interface UseSingleOperationsOptions { + selectedIds: Set + setSelectedIds: (ids: Set) => void + invalidateAssets: () => void +} + +/** + * 单个素材操作(诊断 + 删除) + */ +export const useSingleOperations = ({ + selectedIds, + setSelectedIds, + invalidateAssets, +}: UseSingleOperationsOptions) => { + const queryClient = useQueryClient() + + /* 诊断 */ + const handleDiagnose = useCallback( + async (asset: AssetItem, setDiagnosingId: (id: string | null) => void) => { + setDiagnosingId(asset.id) + try { + const result = await getAssetDiagnosis(asset.id) + const score = result.readiness_score ?? "-" + message.success(`"${asset.name}" 诊断完成,就绪分:${score}`) + queryClient.invalidateQueries({ queryKey: ["assets"] }) + } catch { + message.error(`"${asset.name}" 诊断失败`) + } finally { + setDiagnosingId(null) + } + }, + [queryClient], + ) + + /* 单个素材删除 */ + const handleSingleDelete = useCallback( + async (assetId: string) => { + try { + await deleteAsset(assetId) + invalidateAssets() + // 从选中集合中移除 + const next = new Set(selectedIds) + next.delete(assetId) + setSelectedIds(next) + message.success("素材已删除") + } catch { + message.error("删除失败,请重试") + } + }, + [invalidateAssets, selectedIds, setSelectedIds], + ) + + return { handleDiagnose, handleSingleDelete } +} + +/** + * 批量操作通用工具 + */ +export const useBatchHelpers = ( + queryClient: ReturnType, + setSelectedIds: (ids: Set) => void, +) => { + const invalidateAssets = useCallback(() => { + queryClient.invalidateQueries({ queryKey: ["assets"] }) + queryClient.invalidateQueries({ queryKey: ["asset-libraries"] }) + }, [queryClient]) + + const showOperationResult = useCallback( + ( + result: BatchOperationResult, + title: string, + setResult: (r: BatchOperationResult | null) => void, + setTitle: (t: string) => void, + setDrawerOpen: (open: boolean) => void, + clearSelection = true, + ) => { + setResult(result) + setTitle(title) + setDrawerOpen(true) + if (clearSelection) setSelectedIds(new Set()) + }, + [setSelectedIds], + ) + + return { invalidateAssets, showOperationResult } +} diff --git a/apps/web/src/pages/assets/hooks/useAssetOperations.ts b/apps/web/src/pages/assets/hooks/useAssetOperations.ts index d1ec91f29..ce99f80a3 100644 --- a/apps/web/src/pages/assets/hooks/useAssetOperations.ts +++ b/apps/web/src/pages/assets/hooks/useAssetOperations.ts @@ -1,23 +1,15 @@ import { useState, useCallback } from "react" import { useQueryClient } from "@tanstack/react-query" -import { message } from "antd" +import { useSingleOperations, useBatchHelpers } from "./asset-operations/useSingleOperations" import { - deleteAsset, - getAssetDiagnosis, - batchDeleteAssets, - batchTagAssets, - batchClassifyAssets, - batchMarkAssets, - type BatchOperationResult, -} from "@/api/assets" -import type { AssetItem } from "../types" + useBatchDelete, + useBatchTag, + useBatchClassify, + useBatchMark, +} from "./asset-operations/batchOperations" import type { SmartViewType } from "../components/BatchMarkModal" +import type { BatchOperationResult } from "@/api/assets" -/** - * 素材操作 Hook - * 封装素材的诊断、删除、批量打标签、批量改分类、批量智能标记等操作, - * 以及相关弹窗和结果展示的状态管理 - */ interface UseAssetOperationsProps { selectedIds: Set setSelectedIds: (ids: Set) => void @@ -25,6 +17,10 @@ interface UseAssetOperationsProps { export function useAssetOperations({ selectedIds, setSelectedIds }: UseAssetOperationsProps) { const queryClient = useQueryClient() + const { invalidateAssets, showOperationResult: showResultBase } = useBatchHelpers( + queryClient, + setSelectedIds, + ) /* ── 诊断状态 ── */ const [diagnosingId, setDiagnosingId] = useState(null) @@ -53,204 +49,72 @@ export function useAssetOperations({ selectedIds, setSelectedIds }: UseAssetOper /* ── 批量操作 loading ── */ const [batchLoading, setBatchLoading] = useState(false) - /* ── 刷新数据辅助函数 ── */ - const invalidateAssets = useCallback(() => { - queryClient.invalidateQueries({ queryKey: ["assets"] }) - queryClient.invalidateQueries({ queryKey: ["asset-libraries"] }) - }, [queryClient]) - - /* ── 诊断 ── */ - const handleDiagnose = useCallback( - async (asset: AssetItem) => { - setDiagnosingId(asset.id) - try { - const result = await getAssetDiagnosis(asset.id) - const score = result.readiness_score ?? "-" - message.success(`"${asset.name}" 诊断完成,就绪分:${score}`) - queryClient.invalidateQueries({ queryKey: ["assets"] }) - } catch { - message.error(`"${asset.name}" 诊断失败`) - } finally { - setDiagnosingId(null) - } - }, - [queryClient], - ) - - /* ── 单个素材删除 ── */ - const handleSingleDelete = useCallback( - async (assetId: string) => { - try { - await deleteAsset(assetId) - invalidateAssets() - // 从选中集合中移除 - setSelectedIds( - (() => { - const next = new Set(selectedIds) - next.delete(assetId) - return next - })(), - ) - message.success("素材已删除") - } catch { - message.error("删除失败,请重试") - } - }, - [invalidateAssets, selectedIds, setSelectedIds], - ) - - /* ── 显示操作结果 ── */ + /* ── 操作结果展示 ── */ const showOperationResult = useCallback( (result: BatchOperationResult, title: string, clearSelection = true) => { - setOperationResult(result) - setOperationTitle(title) - setResultDrawerOpen(true) - if (clearSelection) setSelectedIds(new Set()) + showResultBase( + result, + title, + setOperationResult, + setOperationTitle, + setResultDrawerOpen, + clearSelection, + ) }, - [setSelectedIds], + [showResultBase], ) + /* ── 单个操作 ── */ + const { handleDiagnose: handleDiagnoseRaw, handleSingleDelete } = useSingleOperations({ + selectedIds, + setSelectedIds, + invalidateAssets, + }) + + const handleDiagnose = useCallback( + (asset: Parameters[0]) => handleDiagnoseRaw(asset, setDiagnosingId), + [handleDiagnoseRaw], + ) + + /* ── 批量操作 deps ── */ + const batchDeps = { + selectedIds, + invalidateAssets, + showResult: showOperationResult, + setLoading: setBatchLoading, + queryClient, + } + /* ── 批量删除 ── */ - const handleBatchDelete = useCallback(async () => { - const ids = Array.from(selectedIds) - setBatchLoading(true) - try { - const result = await batchDeleteAssets(ids) - invalidateAssets() - showOperationResult(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, showOperationResult]) + const handleBatchDelete = useBatchDelete(batchDeps) /* ── 批量打标签 ── */ - 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"] }) - showOperationResult(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, showOperationResult]) - - /* ── 标签输入处理 ── */ - 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], - ) + const { handleBatchTag, handleTagInputKeyDown, removeBatchTag } = useBatchTag({ + ...batchDeps, + batchTags, + setBatchTags, + tagMode, + setTagMode, + setTagModalOpen, + setBatchTagInput, + batchTagInput, + }) /* ── 批量改分类 ── */ - 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"] }) - showOperationResult(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, showOperationResult]) + const handleBatchClassify = useBatchClassify({ + ...batchDeps, + batchCategory, + setBatchCategory, + setClassifyModalOpen, + }) /* ── 批量智能标记 ── */ - 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"] }) - showOperationResult(result, "批量智能标记") - setMarkModalOpen(false) - const labelMap: Record = { - recommended: "推荐", - caution: "慎用", - high_risk: "高风险", - } - if (result.failure_count === 0) { - message.success( - `成功将 ${result.success_count} 个素材标记为「${labelMap[batchSmartView]}」`, - ) - } else { - message.warning( - `智能标记完成:成功 ${result.success_count} 个,失败 ${result.failure_count} 个`, - ) - } - } catch { - message.error("批量智能标记失败,请重试") - } finally { - setBatchLoading(false) - } - }, [batchSmartView, selectedIds, queryClient, showOperationResult]) + const handleBatchMark = useBatchMark({ + ...batchDeps, + batchSmartView, + setBatchSmartView, + setMarkModalOpen, + }) /* ── 关闭结果 Drawer ── */ const handleResultDrawerClose = useCallback(() => { -- 2.54.0 From 4f774c520449db703c4de5b24df27811d3cd98e0 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 08:13:02 +0800 Subject: [PATCH 2/7] fix: remove unused ClipType import --- apps/web/src/pages/editing-planner/types/clipProperties.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/pages/editing-planner/types/clipProperties.ts b/apps/web/src/pages/editing-planner/types/clipProperties.ts index 54a0d15e1..26125a186 100644 --- a/apps/web/src/pages/editing-planner/types/clipProperties.ts +++ b/apps/web/src/pages/editing-planner/types/clipProperties.ts @@ -1,7 +1,7 @@ /** * ClipPropertiesPanel 相关类型定义 */ -import type { ClipData, ClipType } from "@/pages/editing-planner/types" +import type { ClipData } from "@/pages/editing-planner/types" import type { TemplateMode } from "@/api/editing-planner" import type { AssetItem } from "@/api/assets" -- 2.54.0 From c762740c30a602b1936380e9be496c26e8f26c60 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 12:53:29 +0800 Subject: [PATCH 3/7] =?UTF-8?q?refactor(asset-ops):=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=20constants=20=E5=88=B0=E5=AD=90=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/assets/hooks/asset-operations/constants.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/src/pages/assets/hooks/asset-operations/constants.ts b/apps/web/src/pages/assets/hooks/asset-operations/constants.ts index d86e3ec72..f53b5a923 100644 --- a/apps/web/src/pages/assets/hooks/asset-operations/constants.ts +++ b/apps/web/src/pages/assets/hooks/asset-operations/constants.ts @@ -1,6 +1,5 @@ import type { SmartViewType } from "../../components/BatchMarkModal" -/** 智能标记 → 中文标签映射 */ export const SMART_VIEW_LABELS: Record = { recommended: "推荐", caution: "慎用", -- 2.54.0 From e56870b088f6cacc6f10571eab81435ea9a1e2b5 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 12:53:31 +0800 Subject: [PATCH 4/7] =?UTF-8?q?refactor(asset-ops):=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=E5=8D=95=E4=B8=AA=E6=93=8D=E4=BD=9C=20Hook=20=E5=88=B0?= =?UTF-8?q?=E5=AD=90=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asset-operations/useSingleOperations.ts | 62 ++++++++++++------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/apps/web/src/pages/assets/hooks/asset-operations/useSingleOperations.ts b/apps/web/src/pages/assets/hooks/asset-operations/useSingleOperations.ts index 17afc9d0b..f42be42b1 100644 --- a/apps/web/src/pages/assets/hooks/asset-operations/useSingleOperations.ts +++ b/apps/web/src/pages/assets/hooks/asset-operations/useSingleOperations.ts @@ -7,23 +7,22 @@ import type { AssetItem } from "../../types" interface UseSingleOperationsOptions { selectedIds: Set setSelectedIds: (ids: Set) => void - invalidateAssets: () => void } /** - * 单个素材操作(诊断 + 删除) + * 单个素材操作 Hook + * 诊断、单个删除 */ export const useSingleOperations = ({ selectedIds, setSelectedIds, - invalidateAssets, }: UseSingleOperationsOptions) => { const queryClient = useQueryClient() - /* 诊断 */ + /* ── 诊断 ── */ const handleDiagnose = useCallback( - async (asset: AssetItem, setDiagnosingId: (id: string | null) => void) => { - setDiagnosingId(asset.id) + async (asset: AssetItem) => { + // 模拟 loading 状态 try { const result = await getAssetDiagnosis(asset.id) const score = result.readiness_score ?? "-" @@ -31,41 +30,53 @@ export const useSingleOperations = ({ queryClient.invalidateQueries({ queryKey: ["assets"] }) } catch { message.error(`"${asset.name}" 诊断失败`) - } finally { - setDiagnosingId(null) } }, [queryClient], ) - /* 单个素材删除 */ + /* ── 单个素材删除 ── */ const handleSingleDelete = useCallback( async (assetId: string) => { try { await deleteAsset(assetId) - invalidateAssets() + queryClient.invalidateQueries({ queryKey: ["assets"] }) + queryClient.invalidateQueries({ queryKey: ["asset-libraries"] }) // 从选中集合中移除 - const next = new Set(selectedIds) - next.delete(assetId) - setSelectedIds(next) + setSelectedIds( + (() => { + const next = new Set(selectedIds) + next.delete(assetId) + return next + })(), + ) message.success("素材已删除") } catch { message.error("删除失败,请重试") } }, - [invalidateAssets, selectedIds, setSelectedIds], + [queryClient, selectedIds, setSelectedIds], ) - return { handleDiagnose, handleSingleDelete } + return { + handleDiagnose, + handleSingleDelete, + } +} + +interface UseBatchHelpersOptions { + queryClient: ReturnType + setSelectedIds: (ids: Set) => void } /** - * 批量操作通用工具 + * 批量操作辅助函数 + * 刷新数据、显示操作结果 */ -export const useBatchHelpers = ( - queryClient: ReturnType, - setSelectedIds: (ids: Set) => void, -) => { +export const useBatchHelpers = ({ + queryClient, + setSelectedIds, +}: UseBatchHelpersOptions) => { const invalidateAssets = useCallback(() => { queryClient.invalidateQueries({ queryKey: ["assets"] }) queryClient.invalidateQueries({ queryKey: ["asset-libraries"] }) @@ -73,11 +84,11 @@ export const useBatchHelpers = ( const showOperationResult = useCallback( ( - result: BatchOperationResult, - title: string, setResult: (r: BatchOperationResult | null) => void, setTitle: (t: string) => void, - setDrawerOpen: (open: boolean) => void, + setDrawerOpen: (v: boolean) => void, + result: BatchOperationResult, + title: string, clearSelection = true, ) => { setResult(result) @@ -88,5 +99,8 @@ export const useBatchHelpers = ( [setSelectedIds], ) - return { invalidateAssets, showOperationResult } + return { + invalidateAssets, + showOperationResult, + } } -- 2.54.0 From 464b4a392d5335b4f1cae5c6fdd284030d0544ba Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 12:53:33 +0800 Subject: [PATCH 5/7] =?UTF-8?q?refactor(asset-ops):=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E6=93=8D=E4=BD=9C=20Hook=20=E5=88=B0?= =?UTF-8?q?=E5=AD=90=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/asset-operations/batchOperations.ts | 230 +++++++----------- 1 file changed, 93 insertions(+), 137 deletions(-) diff --git a/apps/web/src/pages/assets/hooks/asset-operations/batchOperations.ts b/apps/web/src/pages/assets/hooks/asset-operations/batchOperations.ts index 2f71325ba..b78a2dcde 100644 --- a/apps/web/src/pages/assets/hooks/asset-operations/batchOperations.ts +++ b/apps/web/src/pages/assets/hooks/asset-operations/batchOperations.ts @@ -1,4 +1,4 @@ -import { useCallback } from "react" +import { useState, useCallback } from "react" import { message } from "antd" import { batchDeleteAssets, @@ -10,48 +10,19 @@ import { import type { SmartViewType } from "../../components/BatchMarkModal" import { SMART_VIEW_LABELS } from "./constants" -interface BatchOperationDeps { +/* ── 批量删除 ── */ +interface UseBatchDeleteOptions { selectedIds: Set invalidateAssets: () => void - showResult: (result: BatchOperationResult, title: string) => void - setLoading: (loading: boolean) => void - queryClient: ReturnType + showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void } -interface BatchTagState { - batchTags: string[] - setBatchTags: (tags: string[]) => void - tagMode: "add" | "replace" - setTagMode: (mode: "add" | "replace") => void - setTagModalOpen: (open: boolean) => void - setBatchTagInput: (val: string) => void - batchTagInput: string -} +export const useBatchDelete = ({ selectedIds, invalidateAssets, showResult }: UseBatchDeleteOptions) => { + const [batchLoading, setBatchLoading] = useState(false) -interface BatchClassifyState { - batchCategory: string - setBatchCategory: (cat: string) => void - setClassifyModalOpen: (open: boolean) => void -} - -interface BatchMarkState { - batchSmartView: SmartViewType - setBatchSmartView: (view: SmartViewType) => void - setMarkModalOpen: (open: boolean) => void -} - -/** - * 批量删除 - */ -export const useBatchDelete = ({ - selectedIds, - invalidateAssets, - showResult, - setLoading, -}: BatchOperationDeps) => { - return useCallback(async () => { + const handleBatchDelete = useCallback(async () => { const ids = Array.from(selectedIds) - setLoading(true) + setBatchLoading(true) try { const result = await batchDeleteAssets(ids) invalidateAssets() @@ -66,32 +37,26 @@ export const useBatchDelete = ({ } catch { message.error("批量删除失败,请重试") } finally { - setLoading(false) + setBatchLoading(false) } - }, [selectedIds, invalidateAssets, showResult, setLoading]) + }, [selectedIds, invalidateAssets, showResult]) + + return { batchLoading, handleBatchDelete } } -/** - * 批量打标签 - */ -export const useBatchTag = ( - deps: BatchOperationDeps & - BatchTagState & { - queryClient: ReturnType - }, -) => { - const { - selectedIds, - showResult, - setLoading, - batchTags, - tagMode, - setTagModalOpen, - setBatchTags, - setBatchTagInput, - setTagMode, - queryClient, - } = deps +/* ── 批量打标签 ── */ +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) { @@ -99,7 +64,7 @@ export const useBatchTag = ( return } const ids = Array.from(selectedIds) - setLoading(true) + setBatchLoading(true) try { const result = await batchTagAssets({ asset_ids: ids, @@ -122,71 +87,66 @@ export const useBatchTag = ( } catch { message.error("批量打标签失败,请重试") } finally { - setLoading(false) + setBatchLoading(false) } - }, [ - batchTags, - selectedIds, - tagMode, - queryClient, - showResult, - setLoading, - setTagModalOpen, - setBatchTags, - setBatchTagInput, - setTagMode, - ]) + }, [batchTags, selectedIds, tagMode, queryClient, showResult]) const handleTagInputKeyDown = useCallback( (e: React.KeyboardEvent) => { - if (e.key === "Enter" && deps.batchTagInput.trim()) { + if (e.key === "Enter" && batchTagInput.trim()) { e.preventDefault() - const tag = deps.batchTagInput.trim() + const tag = batchTagInput.trim() if (!batchTags.includes(tag)) { setBatchTags([...batchTags, tag]) } setBatchTagInput("") } }, - [deps.batchTagInput, batchTags, setBatchTags, setBatchTagInput], + [batchTagInput, batchTags], ) const removeBatchTag = useCallback( (tag: string) => { setBatchTags(batchTags.filter((t) => t !== tag)) }, - [batchTags, setBatchTags], + [batchTags], ) - return { handleBatchTag, handleTagInputKeyDown, removeBatchTag } + return { + tagModalOpen, + setTagModalOpen, + batchTagInput, + setBatchTagInput, + batchTags, + setBatchTags, + tagMode, + setTagMode, + batchLoading, + handleBatchTag, + handleTagInputKeyDown, + removeBatchTag, + } } -/** - * 批量改分类 - */ -export const useBatchClassify = ( - deps: BatchOperationDeps & - BatchClassifyState & { - queryClient: ReturnType - }, -) => { - const { - selectedIds, - showResult, - setLoading, - batchCategory, - setClassifyModalOpen, - setBatchCategory, - queryClient, - } = deps +/* ── 批量改分类 ── */ +interface UseBatchClassifyOptions { + selectedIds: Set + queryClient: ReturnType + showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void +} - return useCallback(async () => { +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) - setLoading(true) + setBatchLoading(true) try { const result = await batchClassifyAssets({ asset_ids: ids, @@ -206,41 +166,35 @@ export const useBatchClassify = ( } catch { message.error("批量改分类失败,请重试") } finally { - setLoading(false) + setBatchLoading(false) } - }, [ - batchCategory, - selectedIds, - queryClient, - showResult, - setLoading, + }, [batchCategory, selectedIds, queryClient, showResult]) + + return { + classifyModalOpen, setClassifyModalOpen, + batchCategory, setBatchCategory, - ]) + batchLoading, + handleBatchClassify, + } } -/** - * 批量智能标记 - */ -export const useBatchMark = ( - deps: BatchOperationDeps & - BatchMarkState & { - queryClient: ReturnType - }, -) => { - const { - selectedIds, - showResult, - setLoading, - batchSmartView, - setMarkModalOpen, - setBatchSmartView, - queryClient, - } = deps +/* ── 批量智能标记 ── */ +interface UseBatchMarkOptions { + selectedIds: Set + queryClient: ReturnType + showResult: (result: BatchOperationResult, title: string, clear?: boolean) => void +} - return useCallback(async () => { +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) - setLoading(true) + setBatchLoading(true) try { const result = await batchMarkAssets({ asset_ids: ids, @@ -249,9 +203,10 @@ export const useBatchMark = ( queryClient.invalidateQueries({ queryKey: ["assets"] }) showResult(result, "批量智能标记") setMarkModalOpen(false) - const label = SMART_VIEW_LABELS[batchSmartView] if (result.failure_count === 0) { - message.success(`成功将 ${result.success_count} 个素材标记为「${label}」`) + message.success( + `成功将 ${result.success_count} 个素材标记为「${SMART_VIEW_LABELS[batchSmartView]}」`, + ) } else { message.warning( `智能标记完成:成功 ${result.success_count} 个,失败 ${result.failure_count} 个`, @@ -260,15 +215,16 @@ export const useBatchMark = ( } catch { message.error("批量智能标记失败,请重试") } finally { - setLoading(false) + setBatchLoading(false) } - }, [ - batchSmartView, - selectedIds, - queryClient, - showResult, - setLoading, + }, [batchSmartView, selectedIds, queryClient, showResult]) + + return { + markModalOpen, setMarkModalOpen, + batchSmartView, setBatchSmartView, - ]) + batchLoading, + handleBatchMark, + } } -- 2.54.0 From 646c5cebacb33e93829bec972a09be1ae098b2b2 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 12:53:34 +0800 Subject: [PATCH 6/7] =?UTF-8?q?refactor(asset-ops):=20=E9=87=8D=E5=86=99?= =?UTF-8?q?=E4=B8=BB=E5=85=A5=E5=8F=A3=E4=B8=BA=E7=BB=84=E5=90=88=E5=B1=82?= =?UTF-8?q?=EF=BC=8C=E4=BF=9D=E6=8C=81=E5=AF=BC=E5=87=BA=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/assets/hooks/useAssetOperations.ts | 158 +++++++----------- 1 file changed, 61 insertions(+), 97 deletions(-) diff --git a/apps/web/src/pages/assets/hooks/useAssetOperations.ts b/apps/web/src/pages/assets/hooks/useAssetOperations.ts index ce99f80a3..160ca3d23 100644 --- a/apps/web/src/pages/assets/hooks/useAssetOperations.ts +++ b/apps/web/src/pages/assets/hooks/useAssetOperations.ts @@ -1,3 +1,9 @@ +/** + * 素材操作 Hook(入口) + * 组合各子模块,保持导出不变 + * + * 子模块位于 ./asset-operations/ + */ import { useState, useCallback } from "react" import { useQueryClient } from "@tanstack/react-query" import { useSingleOperations, useBatchHelpers } from "./asset-operations/useSingleOperations" @@ -7,8 +13,8 @@ import { useBatchClassify, useBatchMark, } from "./asset-operations/batchOperations" -import type { SmartViewType } from "../components/BatchMarkModal" import type { BatchOperationResult } from "@/api/assets" +import type { SmartViewType } from "../components/BatchMarkModal" interface UseAssetOperationsProps { selectedIds: Set @@ -17,104 +23,62 @@ interface UseAssetOperationsProps { export function useAssetOperations({ selectedIds, setSelectedIds }: UseAssetOperationsProps) { const queryClient = useQueryClient() - const { invalidateAssets, showOperationResult: showResultBase } = useBatchHelpers( - queryClient, - setSelectedIds, - ) /* ── 诊断状态 ── */ const [diagnosingId, setDiagnosingId] = useState(null) - /* ── 批量操作弹窗状态 ── */ - const [tagModalOpen, setTagModalOpen] = useState(false) - const [classifyModalOpen, setClassifyModalOpen] = useState(false) - const [markModalOpen, setMarkModalOpen] = useState(false) - const [resultDrawerOpen, setResultDrawerOpen] = useState(false) - - /* ── 批量打标签表单 ── */ - const [batchTagInput, setBatchTagInput] = useState("") - const [batchTags, setBatchTags] = useState([]) - const [tagMode, setTagMode] = useState<"add" | "replace">("add") - - /* ── 批量改分类表单 ── */ - const [batchCategory, setBatchCategory] = useState("") - - /* ── 批量智能标记表单 ── */ - const [batchSmartView, setBatchSmartView] = useState("recommended") - /* ── 操作结果 ── */ + const [resultDrawerOpen, setResultDrawerOpen] = useState(false) const [operationResult, setOperationResult] = useState(null) const [operationTitle, setOperationTitle] = useState("") - /* ── 批量操作 loading ── */ - const [batchLoading, setBatchLoading] = useState(false) - - /* ── 操作结果展示 ── */ - const showOperationResult = useCallback( - (result: BatchOperationResult, title: string, clearSelection = true) => { - showResultBase( - result, - title, - setOperationResult, - setOperationTitle, - setResultDrawerOpen, - clearSelection, - ) - }, - [showResultBase], - ) - /* ── 单个操作 ── */ const { handleDiagnose: handleDiagnoseRaw, handleSingleDelete } = useSingleOperations({ selectedIds, setSelectedIds, - invalidateAssets, }) + // 包装一下,加上 diagnosingId 状态 const handleDiagnose = useCallback( - (asset: Parameters[0]) => handleDiagnoseRaw(asset, setDiagnosingId), + async (asset: Parameters[0]) => { + setDiagnosingId(asset.id) + try { + await handleDiagnoseRaw(asset) + } finally { + setDiagnosingId(null) + } + }, [handleDiagnoseRaw], ) - /* ── 批量操作 deps ── */ - const batchDeps = { + /* ── 批量操作辅助 ── */ + const { invalidateAssets } = useBatchHelpers({ queryClient, setSelectedIds }) + + // 包装 showResult 适配子模块的接口 + const showResult = useCallback( + (result: BatchOperationResult, title: string, clearSelection = true) => { + setOperationResult(result) + setOperationTitle(title) + setResultDrawerOpen(true) + if (clearSelection) setSelectedIds(new Set()) + }, + [setSelectedIds], + ) + + /* ── 批量操作 ── */ + const { batchLoading: deleteLoading, handleBatchDelete } = useBatchDelete({ selectedIds, invalidateAssets, - showResult: showOperationResult, - setLoading: setBatchLoading, - queryClient, - } - - /* ── 批量删除 ── */ - const handleBatchDelete = useBatchDelete(batchDeps) - - /* ── 批量打标签 ── */ - const { handleBatchTag, handleTagInputKeyDown, removeBatchTag } = useBatchTag({ - ...batchDeps, - batchTags, - setBatchTags, - tagMode, - setTagMode, - setTagModalOpen, - setBatchTagInput, - batchTagInput, + showResult, }) - /* ── 批量改分类 ── */ - const handleBatchClassify = useBatchClassify({ - ...batchDeps, - batchCategory, - setBatchCategory, - setClassifyModalOpen, - }) + const tagResult = useBatchTag({ selectedIds, queryClient, showResult }) + const classifyResult = useBatchClassify({ selectedIds, queryClient, showResult }) + const markResult = useBatchMark({ selectedIds, queryClient, showResult }) - /* ── 批量智能标记 ── */ - const handleBatchMark = useBatchMark({ - ...batchDeps, - batchSmartView, - setBatchSmartView, - setMarkModalOpen, - }) + // 取任一批量操作的 loading 状态(任意一个在加载都算加载中) + const batchLoading = + deleteLoading || tagResult.batchLoading || classifyResult.batchLoading || markResult.batchLoading /* ── 关闭结果 Drawer ── */ const handleResultDrawerClose = useCallback(() => { @@ -131,29 +95,29 @@ export function useAssetOperations({ selectedIds, setSelectedIds }: UseAssetOper // 批量操作 loading batchLoading, // 批量打标签 - tagModalOpen, - setTagModalOpen, - batchTagInput, - setBatchTagInput, - batchTags, - setBatchTags, - tagMode, - setTagMode, - handleBatchTag, - handleTagInputKeyDown, - removeBatchTag, + tagModalOpen: tagResult.tagModalOpen, + setTagModalOpen: tagResult.setTagModalOpen, + batchTagInput: tagResult.batchTagInput, + setBatchTagInput: tagResult.setBatchTagInput, + batchTags: tagResult.batchTags, + setBatchTags: tagResult.setBatchTags, + tagMode: tagResult.tagMode, + setTagMode: tagResult.setTagMode, + handleBatchTag: tagResult.handleBatchTag, + handleTagInputKeyDown: tagResult.handleTagInputKeyDown, + removeBatchTag: tagResult.removeBatchTag, // 批量改分类 - classifyModalOpen, - setClassifyModalOpen, - batchCategory, - setBatchCategory, - handleBatchClassify, + classifyModalOpen: classifyResult.classifyModalOpen, + setClassifyModalOpen: classifyResult.setClassifyModalOpen, + batchCategory: classifyResult.batchCategory, + setBatchCategory: classifyResult.setBatchCategory, + handleBatchClassify: classifyResult.handleBatchClassify, // 批量智能标记 - markModalOpen, - setMarkModalOpen, - batchSmartView, - setBatchSmartView, - handleBatchMark, + markModalOpen: markResult.markModalOpen, + setMarkModalOpen: markResult.setMarkModalOpen, + batchSmartView: markResult.batchSmartView as SmartViewType, + setBatchSmartView: markResult.setBatchSmartView, + handleBatchMark: markResult.handleBatchMark, // 批量删除 handleBatchDelete, // 操作结果 -- 2.54.0 From 3fb9e13de6e3e3a41b6c90513af19652143d6801 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 12:53:35 +0800 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=20clipProperties?= =?UTF-8?q?=20import=20=E8=B7=AF=E5=BE=84=E4=B8=BA=E7=9B=B8=E5=AF=B9?= =?UTF-8?q?=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/pages/editing-planner/types/clipProperties.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/pages/editing-planner/types/clipProperties.ts b/apps/web/src/pages/editing-planner/types/clipProperties.ts index 26125a186..3f37015f2 100644 --- a/apps/web/src/pages/editing-planner/types/clipProperties.ts +++ b/apps/web/src/pages/editing-planner/types/clipProperties.ts @@ -1,7 +1,7 @@ /** * ClipPropertiesPanel 相关类型定义 */ -import type { ClipData } from "@/pages/editing-planner/types" +import type { ClipData } from "./clip" import type { TemplateMode } from "@/api/editing-planner" import type { AssetItem } from "@/api/assets" -- 2.54.0