import { useCallback, useMemo, useState } from "react" import { FileText, FolderClosed, Loader2, RefreshCw } from "lucide-react" import { FileTargetMenu } from "~/components/app/file-target-menu" import { Badge } from "~/components/ui/badge" import { Button } from "~/components/ui/button" import { Checkbox } from "~/components/ui/checkbox" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "~/components/ui/dialog" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table" import { api } from "~/lib/api" import type { FolderDiffItem, MappingInfo } from "~/lib/api" import type { FileActionId, FileTarget } from "~/lib/tfs" import { makeFileTarget, statusBadgeClass, statusLabel } from "~/lib/tfs" import { cn } from "~/lib/utils" /** * 目录对比弹窗:对已映射目录执行本地与服务端对比, * 默认隐藏 upToDate,支持状态筛选、刷新重比,结果项走统一右键菜单。 */ // 在对比弹窗内直接执行、完成后需要刷新对比结果的动作。 const ACTIONS_NEED_RECOMPARE: FileActionId[] = ["checkout", "getLatest", "undo", "delete", "add"] export function CompareDialog({ serverPath, mappings, onClose, onFileAction, }: { serverPath: string mappings: MappingInfo[] onClose: () => void onFileAction: (target: FileTarget, action: FileActionId) => void | Promise }) { // 先选项后对比:setup 阶段确认选项,result 阶段展示对比结果。 const [phase, setPhase] = useState<"setup" | "result">("setup") const [diffs, setDiffs] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [showUpToDate, setShowUpToDate] = useState(false) // 忽略仅本地存在的项(如 node_modules):勾选后只对比两端都有的文件,并跳过本地全量扫描。 const [ignoreLocalOnly, setIgnoreLocalOnly] = useState(false) // 状态筛选:空集合表示不过滤(除 upToDate 开关外)。 const [statusFilter, setStatusFilter] = useState>(new Set()) /** * 执行(或重新执行)目录对比;忽略仅本地存在的项时由后端直接跳过本地扫描。 */ const runCompare = useCallback(async () => { setLoading(true) setError(null) const result = await api.compareFolder({ serverPath, recursive: true, includeLocalOnly: !ignoreLocalOnly, }) setLoading(false) if (!result.ok) { setError(result.errorMessage ?? "目录对比失败") return } setDiffs(result.data?.diffs ?? []) }, [serverPath, ignoreLocalOnly]) /** * 转发右键动作:签出 / 获取 / 撤销等就地执行的动作完成后自动重新对比, * 让结果状态(如 签出编辑)立即反映最新事实。 */ const handleAction = useCallback( async (target: FileTarget, action: FileActionId) => { await onFileAction(target, action) if (ACTIONS_NEED_RECOMPARE.includes(action)) { await runCompare() } }, [onFileAction, runCompare], ) // 结果中出现过的状态(不含 upToDate),用于生成筛选 chips。 const presentStatuses = useMemo(() => { const set = new Set() for (const diff of diffs) { if (diff.status !== "upToDate") { set.add(diff.status) } } return [...set] }, [diffs]) const visibleDiffs = useMemo( () => diffs.filter((diff) => { if (diff.status === "upToDate") { return showUpToDate && statusFilter.size === 0 } return statusFilter.size === 0 || statusFilter.has(diff.status) }), [diffs, showUpToDate, statusFilter], ) /** * 切换某个状态筛选 chip。 */ const toggleStatus = useCallback((status: string) => { setStatusFilter((prev) => { const next = new Set(prev) if (next.has(status)) { next.delete(status) } else { next.add(status) } return next }) }, []) // 选项确认阶段:先确定对比方式再执行,避免反复触发全量对比。 if (phase === "setup") { return ( !open && onClose()}> 目录对比 {serverPath}
) } return ( !open && onClose()}> 目录对比{ignoreLocalOnly ? "(已忽略仅本地存在的项)" : ""} {serverPath}
{presentStatuses.map((status) => ( ))}
{loading ? (
正在对比目录…
) : error ? (
{error}
) : visibleDiffs.length === 0 ? (
{diffs.length === 0 ? "本地与服务端一致,没有差异" : "没有符合筛选条件的差异项"}
) : ( 名称 状态 本地版本 最新版本 服务端路径 {visibleDiffs.map((diff) => { const target = makeFileTarget({ source: "compare", folder: diff.folder, serverPath: diff.serverPath, mappings, pendingStatus: diff.status.startsWith("pending") ? diff.status : null, compareStatus: diff.status, }) return ( void handleAction(menuTarget, action)} > {diff.folder ? ( ) : ( )} {diff.name} {statusLabel(diff.status)} {diff.localVersion > 0 ? diff.localVersion : "—"} {diff.latestVersion > 0 ? diff.latestVersion : "—"} {diff.serverPath} ) })}
)}

{visibleDiffs.length} / {diffs.length} 项 · 差异项操作通过右键菜单触发

) }