parent: - Add parent-student-attendance-detail component auth: - Add actions, data-access, schema, services, types onboarding: - Add parent-children-form and hooks directory files: - Add actions, schema, hooks directory notifications: - Add schema and schema test adaptive-practice: - Add answer-input, answer-result, practice-result-view, practice-starter-with-nav - Add question-card, question-content, lib and services directories ai: - Add context/create-ai-client-service, hooks/use-drag-position, hooks/use-position-persistence
133 lines
3.6 KiB
TypeScript
133 lines
3.6 KiB
TypeScript
"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<string>
|
||
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<void>
|
||
}
|
||
|
||
/**
|
||
* 文件批量操作 hook:封装筛选、选择、批量删除逻辑。
|
||
*
|
||
* 与 UI 解耦,便于在 AdminFilesView 或其他列表场景复用。
|
||
*/
|
||
export function useFileBatchOperations({
|
||
files,
|
||
onAfterDelete,
|
||
}: UseFileBatchOperationsOptions): UseFileBatchOperationsReturn {
|
||
const router = useRouter()
|
||
const t = useTranslations("files.admin")
|
||
const [typeFilter, setTypeFilter] = useState<string>("all")
|
||
const [search, setSearch] = useState<string>("")
|
||
const [selectedIds, setSelectedIds] = useState<Set<string>>(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,
|
||
}
|
||
}
|