import { useEffect, useState } from 'react' import { Check, GripVertical, Loader2, Pencil, Plus, Trash2, X } from 'lucide-react' import toast from 'react-hot-toast' import { confirmAction } from './confirmAction' import { useDragReorder } from '../hooks/useDragReorder' import { MAX_BOOK_GROUPS, bookGroupNameError, normalizeBookGroupName } from '../utils/readerBookGroups' import type { BookGroup } from '../api/reader' export type ManageBookGroupsDialogProps = { groups: BookGroup[] /** 分组名 → 当前书架里实际属于该组的书籍数(不统计已失效的 ID)。 */ bookCounts: Record saving: boolean onCreate: (name: string) => Promise onRename: (from: string, to: string) => Promise onRemove: (name: string) => Promise /** 按给定分组名顺序重排(拖拽排序的落点)。 */ onReorder: (orderedNames: string[]) => Promise onClose: () => void } /** * 分组管理对话框(对齐影视模块的管理标签弹窗,去掉成员分配那半边): * 新建 / 重命名 / 删除分组,拖拽调整分组栏顺序。 * * 书籍归组不在这里做,而是在书架卡片上的「分组」按钮里逐个/批量指定 —— 书可能有上千本, * 逐个列在弹窗里既慢又难找。 */ export function ManageBookGroupsDialogView({ groups, bookCounts, saving, onCreate, onRename, onRemove, onReorder, onClose, }: ManageBookGroupsDialogProps) { const [draftName, setDraftName] = useState('') const [renaming, setRenaming] = useState('') const [renameDraft, setRenameDraft] = useState('') const { draggingId, dragOverId, dragProps } = useDragReorder( groups.map((group) => group.name), onReorder, ) useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') onClose() } window.addEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown) }, [onClose]) const submitCreate = () => { const name = normalizeBookGroupName(draftName) if (!name) return const err = bookGroupNameError(draftName, groups) if (err) { toast.error(err) return } setDraftName('') void onCreate(name) } const submitRename = (from: string) => { const name = normalizeBookGroupName(renameDraft) setRenaming('') setRenameDraft('') if (!name || name === from) return const err = bookGroupNameError(renameDraft, groups, from) if (err) { toast.error(err) return } void onRename(from, name) } const handleRemove = async (name: string) => { const ok = await confirmAction({ title: '删除分组', message: `删除「${name}」后,组内的书会回到「未分组」,书籍本身不会被移除。确定吗?`, confirmText: '删除', danger: true, }) if (!ok) return void onRemove(name) } return (
event.stopPropagation()} >

管理分组

拖拽调整顺序 · 最多 {MAX_BOOK_GROUPS} 个分组

setDraftName(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') { event.preventDefault() submitCreate() } }} maxLength={24} placeholder="新分组名" className="h-9 min-w-0 flex-1 rounded-xl border border-[var(--app-border)] bg-[var(--app-panel-soft)] px-3 text-xs font-semibold text-[var(--app-text)] outline-none placeholder:text-[var(--app-muted)] focus:border-brand-500" />
{groups.length === 0 ? (

还没有分组,先在上方新建一个,再到书籍卡片上用「分组」按钮把书放进去

) : (
{groups.map((group) => { const isRenaming = renaming === group.name const dragging = draggingId === group.name const over = dragOverId === group.name return (
{isRenaming ? ( <> setRenameDraft(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') { event.preventDefault() submitRename(group.name) } if (event.key === 'Escape') { setRenaming('') setRenameDraft('') } }} maxLength={24} className="h-7 min-w-0 flex-1 rounded-lg border border-[var(--app-border)] bg-[var(--app-panel-soft)] px-2 text-xs font-semibold text-[var(--app-text)] outline-none focus:border-brand-500" /> ) : ( <> {group.name} {bookCounts[group.name] ?? 0} 本 )}
) })}
)}
{saving && (
正在保存…
)}
) }