refactor(assets): split useAssetOperations into sub-modules
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 35s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 43s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m19s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m33s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 2m54s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m48s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 2m48s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 36s
AI Code Review / AI Code Review (pull_request) Successful in 2m24s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m13s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 7m38s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 33s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1243h44m25s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1243h47m14s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1243h47m18s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1243h47m20s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1243h56m4s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1243h56m7s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1243h56m9s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1243h56m11s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1243h56m46s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1244h39m6s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1244h39m10s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1244h19m58s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1244h28m47s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1245h11m48s

- 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
This commit is contained in:
2026-07-26 18:20:08 +08:00
parent 5fa915cdad
commit 64544d0e5e
4 changed files with 439 additions and 201 deletions
@@ -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<string>
invalidateAssets: () => void
showResult: (result: BatchOperationResult, title: string) => void
setLoading: (loading: boolean) => void
queryClient: ReturnType<typeof import("@tanstack/react-query").useQueryClient>
}
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<typeof import("@tanstack/react-query").useQueryClient>
},
) => {
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<typeof import("@tanstack/react-query").useQueryClient>
},
) => {
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<typeof import("@tanstack/react-query").useQueryClient>
},
) => {
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,
])
}
@@ -0,0 +1,8 @@
import type { SmartViewType } from "../../components/BatchMarkModal"
/** 智能标记 → 中文标签映射 */
export const SMART_VIEW_LABELS: Record<SmartViewType, string> = {
recommended: "推荐",
caution: "慎用",
high_risk: "高风险",
}
@@ -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<string>
setSelectedIds: (ids: Set<string>) => 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<typeof useQueryClient>,
setSelectedIds: (ids: Set<string>) => 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 }
}
@@ -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<string>
setSelectedIds: (ids: Set<string>) => 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<string | null>(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<typeof handleDiagnoseRaw>[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<SmartViewType, string> = {
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(() => {