import { useEffect, useState } from "react"; import FileViewer from "@file-viewer/react"; import officePreset from "@file-viewer/preset-office"; import Editor from "@monaco-editor/react"; import { X } from "lucide-react"; import { ApiRequestError, type ResourcePreviewPayload } from "~/services/api"; import { useApi, useAuth } from "~/context/AuthContext"; import { EXCEL_PREVIEW_MAX_BYTES, TEXT_PREVIEW_MAX_BYTES, monacoLanguageFromFileName, resourceDisplayName, type DataResourcePreviewTarget, } from "./dataResourcePreview"; const EYEBROW: Record = { excel: "EXCEL PREVIEW", text: "TEXT PREVIEW", table: "TABLE PREVIEW", }; export function DataResourcePreviewDialog({ target, onClose, }: { target: DataResourcePreviewTarget | null; onClose: () => void; }) { const title = target ? resourceDisplayName(target.resourceName, target.fileExtension) : ""; if (!target) return null; return (
event.stopPropagation()} >
{EYEBROW[target.kind]}

{title}

{target.kind === "excel" && ( )} {target.kind === "text" && ( )} {target.kind === "table" && }
); } function StatusOverlay({ loading, error, }: { loading: boolean; error: string | null; }) { if (loading) { return (

正在加载预览…

); } if (error) { return (

{error}

); } return null; } function ExcelPreviewBody({ target, title, }: { target: DataResourcePreviewTarget; title: string; }) { const api = useApi(); const { currentWorkspace } = useAuth(); const [file, setFile] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!currentWorkspace?.workspace_id) return; if (target.sizeBytes > EXCEL_PREVIEW_MAX_BYTES) { setFile(null); setError( `文件过大(${Math.ceil(target.sizeBytes / (1024 * 1024))} MB),暂不支持在线预览`, ); setLoading(false); return; } const controller = new AbortController(); setLoading(true); setError(null); setFile(null); void api .fetchResourceContentFile(target.resourceId, title, controller.signal) .then((loaded) => { if (controller.signal.aborted) return; setFile(loaded); setLoading(false); }) .catch((err: unknown) => { if (controller.signal.aborted) return; setFile(null); setLoading(false); setError(errorMessage(err)); }); return () => controller.abort(); }, [api, currentWorkspace?.workspace_id, target, title]); return ( <> {!loading && !error && file && ( )} ); } function TextPreviewBody({ target, title, }: { target: DataResourcePreviewTarget; title: string; }) { const api = useApi(); const { currentWorkspace } = useAuth(); const [content, setContent] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!currentWorkspace?.workspace_id) return; if (target.sizeBytes > TEXT_PREVIEW_MAX_BYTES) { setContent(null); setError( `文件过大(${Math.ceil(target.sizeBytes / (1024 * 1024))} MB),暂不支持文本预览`, ); setLoading(false); return; } const controller = new AbortController(); setLoading(true); setError(null); setContent(null); void api .fetchResourceContentFile(target.resourceId, title, controller.signal) .then(async (loaded) => { if (controller.signal.aborted) return; const text = await loaded.text(); if (controller.signal.aborted) return; setContent(text); setLoading(false); }) .catch((err: unknown) => { if (controller.signal.aborted) return; setContent(null); setLoading(false); setError(errorMessage(err)); }); return () => controller.abort(); }, [api, currentWorkspace?.workspace_id, target, title]); return ( <> {!loading && !error && content !== null && ( 5000 }, wordWrap: "on", fontSize: 13, automaticLayout: true, renderLineHighlight: "gutter", contextmenu: false, scrollBeyondLastLine: false, }} /> )} ); } function TablePreviewBody({ target }: { target: DataResourcePreviewTarget }) { const api = useApi(); const { currentWorkspace } = useAuth(); const [payload, setPayload] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!currentWorkspace?.workspace_id) return; const controller = new AbortController(); setLoading(true); setError(null); setPayload(null); void api .fetchResourcePreview(target.resourceId, { limit: 100 }, controller.signal) .then((data) => { if (controller.signal.aborted) return; setPayload(data); setLoading(false); }) .catch((err: unknown) => { if (controller.signal.aborted) return; setPayload(null); setLoading(false); setError(errorMessage(err)); }); return () => controller.abort(); }, [api, currentWorkspace?.workspace_id, target.resourceId]); return ( <> {!loading && !error && payload && (

{payload.truncated ? `仅预览前 ${payload.row_count} 行(已截断)` : `共 ${payload.row_count} 行`} {payload.delimiter === "\t" ? " · TSV" : " · CSV"}

{payload.columns.map((column) => ( ))} {payload.rows.map((row, index) => ( {payload.columns.map((_, colIndex) => ( ))} ))}
# {column}
{index + 1} {row[colIndex] ?? ""}
{payload.rows.length === 0 && (

文件没有可预览的数据行

)}
)} ); } function errorMessage(err: unknown): string { if (err instanceof ApiRequestError) return err.message; if (err instanceof Error) return err.message; return "加载预览失败"; }