支持git冲突时弹出窗口合并代码解决冲突
This commit is contained in:
@@ -347,3 +347,173 @@
|
||||
background-color: rgba(248, 81, 73, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Merge editor: hunk type decorations (IDEA-style) */
|
||||
|
||||
/* Left pane (ours) & Right pane (theirs) — diff highlights */
|
||||
.monaco-editor .merge-hunk-added-bg {
|
||||
background-color: rgba(46, 160, 67, 0.12);
|
||||
}
|
||||
|
||||
.monaco-editor .merge-hunk-modified-bg {
|
||||
background-color: rgba(31, 111, 235, 0.12);
|
||||
}
|
||||
|
||||
.monaco-editor .merge-hunk-removed-bg {
|
||||
background-color: rgba(128, 128, 128, 0.15);
|
||||
}
|
||||
|
||||
/* Center pane — conflict regions */
|
||||
.monaco-editor .merge-hunk-conflict-bg {
|
||||
background-color: rgba(248, 81, 73, 0.12);
|
||||
}
|
||||
|
||||
/* Center pane — applied hunk */
|
||||
.monaco-editor .merge-hunk-applied-bg {
|
||||
background-color: rgba(46, 160, 67, 0.08);
|
||||
}
|
||||
|
||||
/* Center pane — pending non-conflict hunk */
|
||||
.monaco-editor .merge-hunk-pending-bg {
|
||||
background-color: rgba(234, 179, 8, 0.06);
|
||||
}
|
||||
|
||||
/* Dark mode overrides */
|
||||
.dark .monaco-editor .merge-hunk-added-bg {
|
||||
background-color: rgba(63, 185, 80, 0.18);
|
||||
}
|
||||
|
||||
.dark .monaco-editor .merge-hunk-modified-bg {
|
||||
background-color: rgba(56, 139, 253, 0.18);
|
||||
}
|
||||
|
||||
.dark .monaco-editor .merge-hunk-removed-bg {
|
||||
background-color: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
.dark .monaco-editor .merge-hunk-conflict-bg {
|
||||
background-color: rgba(248, 81, 73, 0.18);
|
||||
}
|
||||
|
||||
.dark .monaco-editor .merge-hunk-applied-bg {
|
||||
background-color: rgba(63, 185, 80, 0.1);
|
||||
}
|
||||
|
||||
.dark .monaco-editor .merge-hunk-pending-bg {
|
||||
background-color: rgba(234, 179, 8, 0.08);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not(.light) .monaco-editor .merge-hunk-added-bg {
|
||||
background-color: rgba(63, 185, 80, 0.18);
|
||||
}
|
||||
|
||||
:root:not(.light) .monaco-editor .merge-hunk-modified-bg {
|
||||
background-color: rgba(56, 139, 253, 0.18);
|
||||
}
|
||||
|
||||
:root:not(.light) .monaco-editor .merge-hunk-removed-bg {
|
||||
background-color: rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
:root:not(.light) .monaco-editor .merge-hunk-conflict-bg {
|
||||
background-color: rgba(248, 81, 73, 0.18);
|
||||
}
|
||||
|
||||
:root:not(.light) .monaco-editor .merge-hunk-applied-bg {
|
||||
background-color: rgba(63, 185, 80, 0.1);
|
||||
}
|
||||
|
||||
:root:not(.light) .monaco-editor .merge-hunk-pending-bg {
|
||||
background-color: rgba(234, 179, 8, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
/* Merge arrow gutter columns */
|
||||
.merge-gutter-column {
|
||||
position: relative;
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.merge-gutter-arrow-btn {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 18px;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
opacity: 0.75;
|
||||
transition: opacity 0.15s, background-color 0.15s;
|
||||
}
|
||||
|
||||
.merge-gutter-arrow-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.merge-gutter-arrow-accept {
|
||||
background-color: rgba(46, 160, 67, 0.25);
|
||||
color: #2ea043;
|
||||
}
|
||||
|
||||
.merge-gutter-arrow-accept:hover {
|
||||
background-color: rgba(46, 160, 67, 0.45);
|
||||
}
|
||||
|
||||
.merge-gutter-arrow-conflict {
|
||||
background-color: rgba(248, 81, 73, 0.2);
|
||||
color: #f85149;
|
||||
}
|
||||
|
||||
.merge-gutter-arrow-conflict:hover {
|
||||
background-color: rgba(248, 81, 73, 0.4);
|
||||
}
|
||||
|
||||
.dark .merge-gutter-arrow-accept {
|
||||
background-color: rgba(63, 185, 80, 0.2);
|
||||
color: #3fb950;
|
||||
}
|
||||
|
||||
.dark .merge-gutter-arrow-accept:hover {
|
||||
background-color: rgba(63, 185, 80, 0.4);
|
||||
}
|
||||
|
||||
.dark .merge-gutter-arrow-conflict {
|
||||
background-color: rgba(248, 81, 73, 0.15);
|
||||
color: #f85149;
|
||||
}
|
||||
|
||||
.dark .merge-gutter-arrow-conflict:hover {
|
||||
background-color: rgba(248, 81, 73, 0.35);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not(.light) .merge-gutter-arrow-accept {
|
||||
background-color: rgba(63, 185, 80, 0.2);
|
||||
color: #3fb950;
|
||||
}
|
||||
|
||||
:root:not(.light) .merge-gutter-arrow-accept:hover {
|
||||
background-color: rgba(63, 185, 80, 0.4);
|
||||
}
|
||||
|
||||
:root:not(.light) .merge-gutter-arrow-conflict {
|
||||
background-color: rgba(248, 81, 73, 0.15);
|
||||
color: #f85149;
|
||||
}
|
||||
|
||||
:root:not(.light) .merge-gutter-arrow-conflict:hover {
|
||||
background-color: rgba(248, 81, 73, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
128
src/app/merge/page.tsx
Normal file
128
src/app/merge/page.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
"use client"
|
||||
|
||||
import { Suspense, useCallback, useEffect, useState } from "react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { MergeWorkspace } from "@/components/merge/merge-workspace"
|
||||
import { AppTitleBar } from "@/components/layout/app-title-bar"
|
||||
import { AppToaster } from "@/components/ui/app-toaster"
|
||||
import { getFolder } from "@/lib/tauri"
|
||||
import type { FolderDetail } from "@/lib/types"
|
||||
|
||||
const TOAST_DURATION_MS = 6000
|
||||
|
||||
interface FolderLoadState {
|
||||
loadedId: number | null
|
||||
folder: FolderDetail | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
function MergePageInner() {
|
||||
const t = useTranslations("MergePage")
|
||||
const searchParams = useSearchParams()
|
||||
const [state, setState] = useState<FolderLoadState>({
|
||||
loadedId: null,
|
||||
folder: null,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const folderId = Number(searchParams.get("folderId") ?? "0")
|
||||
const operation = searchParams.get("operation") ?? "merge"
|
||||
const normalizedFolderId = Number.isFinite(folderId) ? folderId : 0
|
||||
const hasValidFolderId = normalizedFolderId > 0
|
||||
const loading = hasValidFolderId && state.loadedId !== normalizedFolderId
|
||||
const folder = state.loadedId === normalizedFolderId ? state.folder : null
|
||||
const error = state.loadedId === normalizedFolderId ? state.error : null
|
||||
|
||||
const closeWindow = useCallback(() => {
|
||||
getCurrentWindow()
|
||||
.close()
|
||||
.catch((err) => {
|
||||
console.error("[MergePage] failed to close window:", err)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasValidFolderId) return
|
||||
|
||||
let cancelled = false
|
||||
|
||||
getFolder(normalizedFolderId)
|
||||
.then((detail) => {
|
||||
if (!cancelled) {
|
||||
setState({
|
||||
loadedId: normalizedFolderId,
|
||||
folder: detail,
|
||||
error: null,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) {
|
||||
setState({
|
||||
loadedId: normalizedFolderId,
|
||||
folder: null,
|
||||
error: String(err),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [hasValidFolderId, normalizedFolderId])
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col overflow-hidden bg-background text-foreground">
|
||||
<AppTitleBar
|
||||
center={
|
||||
<div className="text-sm font-semibold tracking-tight">
|
||||
{t("title")}
|
||||
{hasValidFolderId && folder ? ` · ${folder.name}` : ""}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<main className="flex-1 min-h-0 p-3">
|
||||
{!hasValidFolderId ? (
|
||||
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
{t("invalidFolderId")}
|
||||
</div>
|
||||
) : loading ? (
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{t("loadingRepo")}
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
) : folder ? (
|
||||
<MergeWorkspace
|
||||
folderId={normalizedFolderId}
|
||||
folderPath={folder.path}
|
||||
operation={operation}
|
||||
onCompleted={closeWindow}
|
||||
onAborted={closeWindow}
|
||||
/>
|
||||
) : null}
|
||||
</main>
|
||||
|
||||
<AppToaster
|
||||
position="bottom-right"
|
||||
duration={TOAST_DURATION_MS}
|
||||
closeButton
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MergePage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<MergePageInner />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user