import { useRef, useState, type FormEvent } from 'react'; import type { FolderInfo } from '@/core-bridge/types'; import { useWorkspace } from '@/store/workspace'; import { Button } from '@/ui/Button'; import { Icon } from '@/ui/Icon'; import { ExplorerFileRow } from './ExplorerFileRow'; import { ExplorerFolderRow } from './ExplorerFolderRow'; import { ExplorerHover } from './ExplorerHover'; import { ExplorerMoveDialog } from './ExplorerMoveDialog'; import { useExplorerInteractions } from './useExplorerInteractions'; import { ContextMenu } from '@/ui/ContextMenu'; import styles from './FileExplorer.module.css'; import { formatSize } from '@/lib/format'; interface Props { scope: string; threadId?: string; threadName: string; fileBytes: number; maxFileBytes?: number; folders: FolderInfo[]; folderId?: string; ids: string[]; selectedId?: string; checked: ReadonlySet; enabled: boolean; structureEnabled: boolean; onFolder: (id?: string) => void; onSelect: (id?: string) => void; onOpen: (id: string) => void; onToggle: (id: string) => void; onSelection: (ids: ReadonlySet) => void; onFiles: (files: File[], folderId?: string) => void; onCreateFolder: (name: string) => Promise; onRenameFolder: (id: string, name: string) => Promise; onDownloadItems: (ids: string[]) => Promise; onDeleteItems: (ids: string[]) => Promise; onMoveItems: (ids: string[], folderId?: string) => Promise; } export function FileExplorer(props: Props) { const input = useRef(null); const [sort, setSort] = useState<'name' | 'size' | 'date'>('name'); const [edit, setEdit] = useState(); const [name, setName] = useState(''); const [busy, setBusy] = useState(false); const [error, setError] = useState(''); const [selectedFolder, setSelectedFolder] = useState(); const byId = useWorkspace.getState().byId; const ids = props.ids.slice().sort((a, b) => { const first = byId[a]; const second = byId[b]; if (first?.kind !== 'file' || second?.kind !== 'file') return 0; return sort === 'size' ? second.size - first.size : sort === 'date' ? second.ts - first.ts : first.name.localeCompare(second.name, 'zh-CN'); }); const path: FolderInfo[] = []; const seen = new Set(); let current = props.folders.find((folder) => folder.id === props.folderId); while (current && !seen.has(current.id)) { seen.add(current.id); path.unshift(current); current = props.folders.find((folder) => folder.id === current?.parentId); } const children = props.folders.filter((folder) => folder.parentId === props.folderId).sort((a, b) => a.name.localeCompare(b.name, 'zh-CN')); const maxFileBytes = props.maxFileBytes; const usedRatio = maxFileBytes && maxFileBytes > 0 ? Math.min(1, props.fileBytes / maxFileBytes) : 0; function beginFolder(id?: string) { const folder = props.folders.find((row) => row.id === id); setEdit(id ?? 'create'); setName(folder?.name ?? ''); setError(''); } const interaction = useExplorerInteractions({ ...props, ids, onDownload: props.onDownloadItems, onDelete: props.onDeleteItems, onMove: props.onMoveItems, onRenameFolder: (id) => beginFolder(id), onCreateFolder: () => beginFolder(), onUpload: () => input.current?.click(), onFolder: navigateFolder }); function navigateFolder(id?: string) { interaction.dismiss(); setSelectedFolder(undefined); props.onFolder(id); } async function submit(event: FormEvent) { event.preventDefault(); if (!edit || busy || !name.trim()) return; setBusy(true); setError(''); try { if (edit === 'create') await props.onCreateFolder(name); else await props.onRenameFolder(edit, name); setEdit(undefined); } catch (cause) { setError(cause instanceof Error ? cause.message : '目录操作失败'); } finally { setBusy(false); } } return
频道文件{props.threadName}
{ if (event.target.files?.length) props.onFiles([...event.target.files], props.folderId); event.target.value = ''; }} />
{edit ?
{ void submit(event); }}> setName(event.target.value)} />
: null}
{ if (!(event.target as HTMLElement).closest('tr,button,input')) { props.onSelection(new Set()); props.onSelect(undefined); setSelectedFolder(undefined); event.currentTarget.focus(); } }} tabIndex={-1}> {children.map((folder) => navigateFolder(folder.id)} onSelect={() => { interaction.dismiss(); setSelectedFolder(folder.id); props.onSelection(new Set()); props.onSelect(undefined); }} onRename={() => beginFolder(folder.id)} onMenu={(x, y) => { setSelectedFolder(folder.id); props.onSelection(new Set()); props.onSelect(undefined); interaction.showFolderMenu(folder.id, folder.name, x, y); }} />)} {ids.map((id) => { setSelectedFolder(undefined); interaction.select(id, event); }} onOpen={props.onOpen} onToggle={props.onToggle} onMenu={(id, x, y) => { setSelectedFolder(undefined); interaction.showFileMenu(id, x, y); }} onHover={interaction.hoverFile} onLeave={interaction.leave} onDrag={interaction.drag} onDragEnd={interaction.dismiss} />)}
{ids.length === 0 && children.length === 0 ?

此文件夹还没有文件

上传文件或将文件拖到窗口中。
: null}
{props.checked.size > 0 ?
已选 {props.checked.size} 项 {props.enabled ? : null}
: null} {error || interaction.error ?

{error || interaction.error}

: null}
{children.length} 个文件夹 · {ids.length} 个文件双击打开 · 右键操作 · 拖动移动
频道文件空间{formatSize(props.fileBytes)} / {maxFileBytes ? formatSize(maxFileBytes) : '读取中'}
= (maxFileBytes ?? Number.POSITIVE_INFINITY) ? styles.quotaFull : undefined} style={{ width: `${usedRatio * 100}%` }} />
{maxFileBytes ? 剩余 {formatSize(Math.max(0, maxFileBytes - props.fileBytes))} : null}
{interaction.menu ? : null} {interaction.hover && ids.includes(interaction.hover.id) && !interaction.menu ? : null} {interaction.moveIds ? interaction.setMoveIds(undefined)} onMove={(folderId) => props.onMoveItems(interaction.moveIds!, folderId)} /> : null}
; }