import { useEffect, useMemo, useState } from "react" import { Loader2 } from "lucide-react" import { Badge } from "~/components/ui/badge" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "~/components/ui/dialog" import { Input } from "~/components/ui/input" import { api } from "~/lib/api" import type { FileContent } from "~/lib/api" import { cn } from "~/lib/utils" // 查看来源:本地映射文件或服务器 latest。 type ViewSource = "local" | "server" /** * 把字节数格式化为可读大小。 */ function formatSize(size: number): string { if (size < 1024) { return `${size} B` } if (size < 1024 * 1024) { return `${(size / 1024).toFixed(1)} KB` } return `${(size / 1024 / 1024).toFixed(1)} MB` } /** * 文件查看弹窗:只读展示本地、服务器 latest 或指定 changeset 版本内容, * 支持来源切换、行号与搜索高亮;二进制 / 超大 / 非映射路径给出明确提示。 */ export function FileViewDialog({ serverPath, localPath, changeset, onClose, }: { serverPath: string localPath: string | null /** 指定 changeset 时固定展示该版本内容,不提供来源切换。 */ changeset?: number onClose: () => void }) { const [source, setSource] = useState(localPath && changeset == null ? "local" : "server") const [content, setContent] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [search, setSearch] = useState("") // 加载所选来源的文件内容;本地内容仅在已映射时可用;指定版本固定按 changeset 读取。 useEffect(() => { let active = true setLoading(true) setError(null) void (async () => { const result = changeset != null ? await api.getFileContent({ serverPath, changeset }) : source === "local" && localPath ? await api.getFileContent({ localPath }) : await api.getFileContent({ serverPath }) if (!active) { return } setLoading(false) if (!result.ok || !result.data) { setContent(null) setError(result.errorMessage ?? "文件内容加载失败") return } setContent(result.data.content) })() return () => { active = false } }, [source, serverPath, localPath, changeset]) const lines = useMemo( () => (content && !content.binary && !content.tooLarge ? content.content.split(/\r?\n/) : []), [content], ) const keyword = search.trim().toLowerCase() const matchCount = useMemo( () => keyword.length === 0 ? 0 : lines.filter((line) => line.toLowerCase().includes(keyword)).length, [lines, keyword], ) return ( !open && onClose()}> 查看文件 {serverPath}
{changeset != null ? ( 版本 C{changeset} ) : (
setSource("local")} /> setSource("server")} />
)} {content && ( {formatSize(content.size)} {content.changeset > 0 && ` · 版本 ${content.changeset}`} )} setSearch(event.target.value)} placeholder="搜索内容…" className="ml-auto h-7 w-48 text-xs" /> {keyword.length > 0 && ( {matchCount} 行命中 )}
{loading ? (
正在加载内容…
) : error ? (
{error}
) : content?.binary ? ( ) : content?.tooLarge ? ( ) : (
              {lines.map((line, index) => {
                const hit = keyword.length > 0 && line.toLowerCase().includes(keyword)
                return (
                  
{index + 1} {line}
) })}
)}

只读查看 · 来源: {changeset != null ? `changeset ${changeset} 版本` : source === "local" ? `本地 ${localPath ?? ""}` : "服务器 latest"}

) } /** * 来源切换小标签按钮。 */ function SourceTab({ label, active, disabled, disabledReason, onClick, }: { label: string active: boolean disabled?: boolean disabledReason?: string onClick: () => void }) { return ( ) } /** * 内容区占位提示(二进制 / 超大文件)。 */ function Notice({ text }: { text: string }) { return (
{text}
) }