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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
302 lines
9.0 KiB
TypeScript
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,
|
|
}
|
|
}
|