import { useCallback, useEffect, useState } from "react" import { ArrowLeft, CloudDownload, Loader2 } from "lucide-react" import { ConfirmDialog } from "~/components/app/confirm-dialog" import { FileViewDialog } from "~/components/explorer/file-view-dialog" import { Button } from "~/components/ui/button" import { Checkbox } from "~/components/ui/checkbox" import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, } from "~/components/ui/context-menu" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "~/components/ui/dialog" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table" import { api } from "~/lib/api" import type { HistoryEntry } from "~/lib/api" import { formatDateTime } from "~/lib/utils" // 历史弹窗当前查看的目标(支持从 changeset 文件下钻到单文件历史后返回)。 interface HistoryTarget { serverPath: string folder: boolean } // 「获取此版本」待确认信息。 interface VersionConfirm { serverPath: string changeset: number folder: boolean } // 「回滚」待确认信息:single 仅反做该变更集,toVersion 反做其后全部改动。 interface RollbackConfirm { serverPath: string changeset: number mode: "single" | "toVersion" } /** * History 弹窗:展示文件或目录历史(最近记录)。 * 目录历史可下钻 changeset 影响文件;changeset 文件支持右键查看此版本 / * 查看该文件历史(可返回)/ 获取此版本;文件历史可勾选两个版本进入版本对比; * 每条记录支持「获取此版本…」(确认后用该版本覆盖本地)。 */ export function HistoryDialog({ serverPath, folder, onClose, onDiffRevisions, onGetVersion, onRollback, }: { serverPath: string folder: boolean onClose: () => void onDiffRevisions?: (serverPath: string, source: number, target: number) => void onGetVersion?: (serverPath: string, changeset: number, folder: boolean) => Promise onRollback?: (serverPath: string, mode: "single" | "toVersion", changeset: number) => Promise }) { const [target, setTarget] = useState({ serverPath, folder }) // 下钻历史目标栈:从 changeset 文件进入单文件历史后可逐级返回。 const [targetStack, setTargetStack] = useState([]) const [entries, setEntries] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // 目录历史下钻:当前查看的 changeset 文件列表,null 表示在历史主列表。 const [changesetFiles, setChangesetFiles] = useState<{ changeset: number files: HistoryEntry[] } | null>(null) const [filesLoading, setFilesLoading] = useState(false) // 文件历史勾选的 changeset(最多两个),用于版本对比。 const [selected, setSelected] = useState([]) const [confirmVersion, setConfirmVersion] = useState(null) // 「回滚」待确认信息。 const [confirmRollback, setConfirmRollback] = useState(null) // 「查看此版本内容」叠加的文件查看弹窗目标。 const [viewVersion, setViewVersion] = useState<{ serverPath: string; changeset: number } | null>(null) useEffect(() => { let active = true void (async () => { setLoading(true) const result = await api.getHistory({ path: target.serverPath, folder: target.folder }) if (!active) { return } setLoading(false) if (!result.ok) { setError(result.errorMessage ?? "历史加载失败") return } setError(null) setEntries(result.data?.history ?? []) })() return () => { active = false } }, [target]) /** * 目录历史下钻:加载指定 changeset 影响的文件列表。 */ const openChangeset = useCallback(async (changeset: number) => { setFilesLoading(true) const result = await api.getChangesetFiles({ changeset }) setFilesLoading(false) if (!result.ok) { setError(result.errorMessage ?? "Changeset 文件加载失败") return } setError(null) setChangesetFiles({ changeset, files: result.data?.files ?? [] }) }, []) /** * 从 changeset 文件切换到该文件的历史(压栈当前目标,可返回)。 */ const openFileHistory = useCallback( (fileServerPath: string) => { setTargetStack((prev) => [...prev, target]) setTarget({ serverPath: fileServerPath, folder: false }) setChangesetFiles(null) setSelected([]) }, [target], ) /** * 返回上一级历史目标。 */ const backToPreviousTarget = useCallback(() => { setTargetStack((prev) => { const next = [...prev] const previous = next.pop() if (previous) { setTarget(previous) setChangesetFiles(null) setSelected([]) } return next }) }, []) /** * 勾选 / 取消勾选用于对比的 changeset,最多保留两个(先进先出)。 */ const toggleSelected = useCallback((changeset: number) => { setSelected((prev) => { if (prev.includes(changeset)) { return prev.filter((item) => item !== changeset) } const next = [...prev, changeset] return next.length > 2 ? next.slice(next.length - 2) : next }) }, []) const canDiff = onDiffRevisions != null && !target.folder && selected.length === 2 return ( !open && onClose()}> {changesetFiles ? `Changeset ${changesetFiles.changeset} 的文件` : "历史记录"} {target.serverPath} {(changesetFiles || targetStack.length > 0) && (
{changesetFiles && ( )} {!changesetFiles && targetStack.length > 0 && ( )}
)}
{loading || filesLoading ? (
正在加载…
) : error ? (
{error}
) : changesetFiles ? ( setViewVersion({ serverPath: fileServerPath, changeset: changesetFiles.changeset }) } onOpenFileHistory={openFileHistory} onGetVersion={ onGetVersion ? (fileServerPath) => setConfirmVersion({ serverPath: fileServerPath, changeset: changesetFiles.changeset, folder: false, }) : undefined } /> ) : entries.length === 0 ? (
没有历史记录
) : ( {!target.folder && } Changeset 类型 作者 时间 注释 {onGetVersion && } {entries.map((entry) => ( { if (target.folder) { void openChangeset(entry.changeset) } }} > {!target.folder && ( toggleSelected(entry.changeset)} aria-label={`选择 changeset ${entry.changeset}`} /> )} {target.folder ? ( ) : ( entry.changeset )} {entry.changeType} {entry.author} {formatDateTime(entry.date)} {entry.comment || "—"} {onGetVersion && ( )} {(onGetVersion || onRollback) && ( {onGetVersion && ( setConfirmVersion({ serverPath: target.serverPath, changeset: entry.changeset, folder: target.folder, }) } > 获取此版本(C{entry.changeset})… )} {onGetVersion && onRollback && } {onRollback && ( <> setConfirmRollback({ serverPath: target.serverPath, changeset: entry.changeset, mode: "single", }) } > 回滚此变更集(C{entry.changeset})… setConfirmRollback({ serverPath: target.serverPath, changeset: entry.changeset, mode: "toVersion", }) } > 回滚到此变更集(C{entry.changeset})… )} )} ))}
)}

{changesetFiles ? `${changesetFiles.files.length} 个文件 · 右键文件可查看此版本 / 历史 / 获取` : target.folder ? "双击记录或点击 changeset 编号查看影响文件" : "勾选两个版本可进行对比"}

{!target.folder && onDiffRevisions && ( )}
{confirmVersion && onGetVersion && (

将把 {confirmVersion.serverPath}{" "} 的本地内容替换为 changeset {confirmVersion.changeset} 的版本。

本地未签入的修改会被覆盖,且无法恢复。确定继续吗?

} confirmLabel="获取此版本" danger onConfirm={async () => { await onGetVersion( confirmVersion.serverPath, confirmVersion.changeset, confirmVersion.folder, ) setConfirmVersion(null) }} onClose={() => setConfirmVersion(null)} /> )} {confirmRollback && onRollback && (

将对 {confirmRollback.serverPath}{" "} {confirmRollback.mode === "single" ? `反做变更集 C${confirmRollback.changeset} 的改动。` : `反做 C${confirmRollback.changeset} 之后的全部改动(回到该变更集时刻的内容)。`}

回滚只会产生挂起更改,不会直接写入服务器;可在挂起更改面板审查、撤销,确认无误后签入。

} confirmLabel="执行回滚" danger onConfirm={async () => { await onRollback( confirmRollback.serverPath, confirmRollback.mode, confirmRollback.changeset, ) setConfirmRollback(null) }} onClose={() => setConfirmRollback(null)} /> )} {viewVersion && ( setViewVersion(null)} /> )}
) } /** * Changeset 影响文件列表(目录历史下钻视图),行级右键菜单提供对象操作。 */ function ChangesetFilesTable({ files, changeset, onViewVersion, onOpenFileHistory, onGetVersion, }: { files: HistoryEntry[] changeset: number onViewVersion: (serverPath: string) => void onOpenFileHistory: (serverPath: string) => void onGetVersion?: (serverPath: string) => void }) { if (files.length === 0) { return (
该 changeset 没有文件记录
) } return ( 类型 服务端路径 {files.map((file) => ( {file.changeType} {file.serverPath} onViewVersion(file.serverPath)}> 查看此版本内容 onOpenFileHistory(file.serverPath)}> 查看该文件历史 {onGetVersion && ( <> onGetVersion(file.serverPath)} > 获取此版本(C{changeset})… )} ))}
) }