import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Checkbox } from "@/components/ui/checkbox"; import { Textarea } from "@/components/ui/textarea"; import { ArrowBigDownDash, ArrowBigUpDash, Brackets, CaseSensitive, Filter, Languages, LocateFixed, MoreVertical, PencilLine, Trash2 } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type React from "react"; import { TableVirtuoso, type TableVirtuosoHandle } from "react-virtuoso"; import { toast } from "sonner"; import { useClipboard } from "@/hooks/use-clipboard"; import { useTableOptionState } from "./use-table-option-state"; import type { FlatEntry } from "@/lib/i18n-structure"; import type { TranslationInlineEditHandlers } from "@/hooks/biz/use-translation-inline-edit"; interface EditorTableProps { entries: FlatEntry[]; languages: string[]; selected: Set; setSelected: React.Dispatch>>; inlineEdit: TranslationInlineEditHandlers; onOpenAddEntry: (path: string, position: "above" | "below") => void; onOpenAiTranslate: (path: string) => void; onOpenBulkAiTranslate: () => void; onOpenRenameEntry: (path: string) => void; onMoveEntry: (path: string, offset: number) => Promise | void; onDeleteEntry: (path: string) => Promise | void; onDeleteSelected: (paths: string[]) => Promise | void; maxAiItems?: number; } export default function EditorTable({ entries, languages, selected, setSelected, inlineEdit, onOpenAddEntry, onOpenAiTranslate, onOpenBulkAiTranslate, onOpenRenameEntry, onMoveEntry, onDeleteEntry, onDeleteSelected, maxAiItems = 50, }: EditorTableProps) { const { copy } = useClipboard(); const [visibleLangs, setVisibleLangs] = useState>(new Set()); useEffect(() => { setVisibleLangs(new Set(languages)); }, [languages]); const virtuosoRef = useRef(null); const scrollerRootRef = useRef(null); const { query, setQuery, caseSensitive, setCaseSensitive, fullMatch, setFullMatch, } = useTableOptionState(); const [moveCountUp, setMoveCountUp] = useState(1); const [moveCountDown, setMoveCountDown] = useState(1); const allSelected = useMemo(() => entries.length > 0 && selected.size === entries.length, [entries, selected]); const MAX_AI_ITEMS = maxAiItems; const highlightRow = useCallback((index: number) => { const tryFindAndAnimate = (attempt = 0) => { const root = scrollerRootRef.current as HTMLElement | null; if (!root) return; const row = root.querySelector(`tr[data-item-index="${index}"]`) as HTMLTableRowElement | null; if (row) { row.animate( [ { backgroundColor: "rgba(250, 204, 21, 0.6)" }, { backgroundColor: "transparent" }, { backgroundColor: "rgba(250, 204, 21, 0.6)" }, { backgroundColor: "transparent" }, ], { duration: 1200, easing: "ease-in-out" } ); return; } if (attempt < 10) { setTimeout(() => tryFindAndAnimate(attempt + 1), 50); } }; requestAnimationFrame(() => tryFindAndAnimate(0)); }, []); const scrollToQuery = useCallback((ev: React.FormEvent) => { ev.preventDefault(); ev.stopPropagation(); if (!query) return; const idx = entries.findIndex((e) => { const hay = caseSensitive ? e.path : e.path.toLowerCase(); const needle = caseSensitive ? query : query.toLowerCase(); return fullMatch ? hay === needle : hay.includes(needle); }); if (idx >= 0) { virtuosoRef.current?.scrollIntoView({ index: idx, align: "center", done: () => highlightRow(idx) }); } }, [caseSensitive, entries, fullMatch, highlightRow, query]); const displayedLanguages = useMemo(() => languages.filter((l) => visibleLangs.has(l)), [languages, visibleLangs]); const tableWidth = useMemo(() => (displayedLanguages.length * 200) + 36 + 60 + 300, [displayedLanguages.length]); const virtuosoComponents = useMemo(() => ({ Table: ({ style, ...props }: React.TableHTMLAttributes) => , TableHead: (props: React.HTMLAttributes) => , TableRow: (props: React.HTMLAttributes) => , TableBody: (props: React.HTMLAttributes) => , TableFoot: (props: React.HTMLAttributes) => , }), [tableWidth]); const headerContent = useCallback(() => ( {displayedLanguages.map((lang) => ( ))} ), [allSelected, displayedLanguages, entries, setSelected]); const renderItemContent = useCallback((_idx: number, entry: FlatEntry) => { const handleCopy = () => { copy(entry.path); toast.success("复制成功"); }; return ( <> {displayedLanguages.map((lang) => { const isEditing = inlineEdit.isEditingCell(entry.path, lang); const isSaving = inlineEdit.isSavingCell(entry.path, lang); const displayValue = inlineEdit.getDisplayValue(entry.path, lang); return (
{ const next = new Set(); if (checked) { for (const en of entries) next.add(en.path); } setSelected(next); }} /> 翻译条目名称{lang}操作
{ setSelected((prev) => { const next = new Set(prev); if (checked) next.add(entry.path); else next.delete(entry.path); return next; }); }} /> {isEditing ? (