feat(config): add YAML diff confirmation before save

This commit is contained in:
Supra4E8C
2026-02-16 22:04:55 +08:00
parent 470ff51579
commit b7794a91b4
8 changed files with 262 additions and 6 deletions
@@ -0,0 +1,72 @@
@use '../../styles/variables' as *;
@use '../../styles/mixins' as *;
.diffModal {
:global(.modal-body) {
padding: $spacing-md $spacing-lg;
max-height: none;
}
}
.content {
display: flex;
flex-direction: column;
gap: $spacing-sm;
height: 70vh;
min-height: 420px;
}
.columnLabels {
display: grid;
grid-template-columns: 1fr 1fr;
gap: $spacing-sm;
font-size: 12px;
color: var(--text-secondary);
font-weight: 600;
span {
padding: 0 2px;
}
}
.mergeRoot {
flex: 1;
min-height: 0;
border: 1px solid var(--border-color);
border-radius: $radius-md;
overflow: hidden;
background: var(--bg-secondary);
:global(.cm-mergeView) {
height: 100%;
background: var(--bg-primary);
}
:global(.cm-mergeViewEditors),
:global(.cm-mergeViewEditor),
:global(.cm-editor) {
height: 100%;
}
:global(.cm-scroller) {
overflow: auto;
}
:global(.cm-gutters) {
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
}
:global(.cm-mergeGap) {
background: var(--bg-secondary);
border-left: 1px solid var(--border-color);
border-right: 1px solid var(--border-color);
}
}
@include mobile {
.content {
height: 65vh;
min-height: 360px;
}
}
+103
View File
@@ -0,0 +1,103 @@
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { yaml } from '@codemirror/lang-yaml';
import { MergeView } from '@codemirror/merge';
import { Modal } from '@/components/ui/Modal';
import { Button } from '@/components/ui/Button';
import { useThemeStore } from '@/stores';
import styles from './DiffModal.module.scss';
type DiffModalProps = {
open: boolean;
original: string;
modified: string;
onConfirm: () => void;
onCancel: () => void;
loading?: boolean;
};
export function DiffModal({
open,
original,
modified,
onConfirm,
onCancel,
loading = false
}: DiffModalProps) {
const { t } = useTranslation();
const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
const mergeContainerRef = useRef<HTMLDivElement>(null);
const mergeViewRef = useRef<MergeView | null>(null);
useEffect(() => {
if (!open || !mergeContainerRef.current) return;
const mountEl = mergeContainerRef.current;
mountEl.innerHTML = '';
const commonExtensions = [
yaml(),
EditorState.readOnly.of(true),
EditorView.editable.of(false),
EditorView.lineWrapping,
EditorView.theme(
{
'&': { height: '100%' },
'.cm-scroller': {
fontFamily: "'Consolas', 'Monaco', 'Menlo', monospace"
}
},
{ dark: resolvedTheme === 'dark' }
)
];
const view = new MergeView({
parent: mountEl,
a: { doc: original, extensions: commonExtensions },
b: { doc: modified, extensions: commonExtensions },
orientation: 'a-b',
highlightChanges: true,
gutter: true
});
mergeViewRef.current = view;
return () => {
view.destroy();
if (mergeViewRef.current === view) {
mergeViewRef.current = null;
}
mountEl.innerHTML = '';
};
}, [modified, open, original, resolvedTheme]);
return (
<Modal
open={open}
title={t('config_management.diff.title')}
onClose={onCancel}
width="min(1200px, 90vw)"
className={styles.diffModal}
closeDisabled={loading}
footer={
<>
<Button variant="secondary" onClick={onCancel} disabled={loading}>
{t('common.cancel')}
</Button>
<Button onClick={onConfirm} loading={loading} disabled={loading}>
{t('config_management.diff.confirm')}
</Button>
</>
}
>
<div className={styles.content}>
<div className={styles.columnLabels}>
<span>{t('config_management.diff.current')}</span>
<span>{t('config_management.diff.modified')}</span>
</div>
<div className={styles.mergeRoot} ref={mergeContainerRef} />
</div>
</Modal>
);
}