Files
xiaoxia-saas/apps/web/src/pages/assets/hooks/useAssetOperations.ts
T
xiaoxia 6437b96e54
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m17s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m8s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m20s
CI/CD Pipeline / Unit Tests (push) Failing after 5m11s
CI/CD Pipeline / Integration Tests (push) Successful in 3m7s
CI/CD Pipeline / Frontend Lint (push) Successful in 55s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m8s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m32s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m27s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m42s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m33s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 8m18s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 11m0s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 28s
refactor(assets): Phase 3 - extract business hooks (#899)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-07-25 23:55:16 +08:00

302 lines
9.0 KiB
TypeScript

import { useState, useCallback } from "react"
import { useQueryClient } from "@tanstack/react-query"
import { message } from "antd"
import {
deleteAsset,
getAssetDiagnosis,
batchDeleteAssets,
batchTagAssets,
batchClassifyAssets,
batchMarkAssets,
type BatchOperationResult,
} from "@/api/assets"
import type { AssetItem } from "../types"
import type { SmartViewType } from "../components/BatchMarkModal"
/**
* 素材操作 Hook
* 封装素材的诊断、删除、批量打标签、批量改分类、批量智能标记等操作,
* 以及相关弹窗和结果展示的状态管理
*/
interface UseAssetOperationsProps {
selectedIds: Set<string>
setSelectedIds: (ids: Set<string>) => void
}
export function useAssetOperations({ selectedIds, setSelectedIds }: UseAssetOperationsProps) {
const queryClient = useQueryClient()
/* ── 诊断状态 ── */
const [diagnosingId, setDiagnosingId] = useState<string | null>(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<string[]>([])
const [tagMode, setTagMode] = useState<"add" | "replace">("add")
/* ── 批量改分类表单 ── */
const [batchCategory, setBatchCategory] = useState("")
/* ── 批量智能标记表单 ── */
const [batchSmartView, setBatchSmartView] = useState<SmartViewType>("recommended")
/* ── 操作结果 ── */
const [operationResult, setOperationResult] = useState<BatchOperationResult | null>(null)
const [operationTitle, setOperationTitle] = useState("")
/* ── 批量操作 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())
},
[setSelectedIds],
)
/* ── 批量删除 ── */
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 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 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 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])
/* ── 关闭结果 Drawer ── */
const handleResultDrawerClose = useCallback(() => {
setResultDrawerOpen(false)
setOperationResult(null)
}, [])
return {
// 诊断
diagnosingId,
handleDiagnose,
// 单个操作
handleSingleDelete,
// 批量操作 loading
batchLoading,
// 批量打标签
tagModalOpen,
setTagModalOpen,
batchTagInput,
setBatchTagInput,
batchTags,
setBatchTags,
tagMode,
setTagMode,
handleBatchTag,
handleTagInputKeyDown,
removeBatchTag,
// 批量改分类
classifyModalOpen,
setClassifyModalOpen,
batchCategory,
setBatchCategory,
handleBatchClassify,
// 批量智能标记
markModalOpen,
setMarkModalOpen,
batchSmartView,
setBatchSmartView,
handleBatchMark,
// 批量删除
handleBatchDelete,
// 操作结果
resultDrawerOpen,
operationResult,
operationTitle,
handleResultDrawerClose,
}
}