feat(parent,auth,onboarding,files,notifications,adaptive-practice,ai): add module updates
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
This commit is contained in:
132
src/modules/files/hooks/use-file-batch-operations.ts
Normal file
132
src/modules/files/hooks/use-file-batch-operations.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
"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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user