"use client" import { useCallback, useMemo, useState } from "react" import { useRouter } from "next/navigation" import { toast } from "sonner" import { useTranslations } from "next-intl" import type { FileAttachment } from "../types" export interface UseFileBatchOperationsOptions { files: FileAttachment[] onAfterDelete?: () => void } export interface UseFileBatchOperationsReturn { selectedIds: Set typeFilter: string search: string deleting: boolean setTypeFilter: (v: string) => void setSearch: (v: string) => void filteredFiles: FileAttachment[] allSelected: boolean someSelected: boolean toggleAll: () => void toggleOne: (id: string) => void handleBatchDelete: () => Promise } /** * 文件批量操作 hook:封装筛选、选择、批量删除逻辑。 * * 与 UI 解耦,便于在 AdminFilesView 或其他列表场景复用。 */ export function useFileBatchOperations({ files, onAfterDelete, }: UseFileBatchOperationsOptions): UseFileBatchOperationsReturn { const router = useRouter() const t = useTranslations("files.admin") const [typeFilter, setTypeFilter] = useState("all") const [search, setSearch] = useState("") const [selectedIds, setSelectedIds] = useState>(new Set()) const [deleting, setDeleting] = useState(false) const filteredFiles = useMemo(() => { return files.filter((f) => { if (typeFilter !== "all") { if (typeFilter.endsWith("/")) { if (!f.mimeType.startsWith(typeFilter)) return false } else if (f.mimeType !== typeFilter) { return false } } if (search.trim()) { const kw = search.trim().toLowerCase() if ( !f.originalName.toLowerCase().includes(kw) && !f.filename.toLowerCase().includes(kw) ) { return false } } return true }) }, [files, typeFilter, search]) const allSelected = filteredFiles.length > 0 && selectedIds.size === filteredFiles.length const someSelected = selectedIds.size > 0 && !allSelected const toggleAll = useCallback(() => { if (allSelected) { setSelectedIds(new Set()) } else { setSelectedIds(new Set(filteredFiles.map((f) => f.id))) } }, [allSelected, filteredFiles]) const toggleOne = useCallback((id: string) => { setSelectedIds((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) }, []) const handleBatchDelete = useCallback(async () => { if (selectedIds.size === 0) return const ids = Array.from(selectedIds) setDeleting(true) try { const res = await fetch("/api/files/batch-delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids }), }) const body = (await res.json().catch(() => null)) as { success?: boolean message?: string deletedCount?: number } | null if (!res.ok || !body?.success) { toast.error(body?.message || t("selection.deleteFailed")) return } toast.success(t("selection.deleted", { count: body.deletedCount ?? 0 })) setSelectedIds(new Set()) onAfterDelete?.() router.refresh() } catch { toast.error(t("selection.deleteFailed")) } finally { setDeleting(false) } }, [selectedIds, onAfterDelete, router, t]) return { selectedIds, typeFilter, search, deleting, setTypeFilter, setSearch, filteredFiles, allSelected, someSelected, toggleAll, toggleOne, handleBatchDelete, } }