Download src/components/editor/MonacoDiffViewer.tsx from Leon4gr45/Velo: direct link, hf CLI and curl.
- Browser
- Download file 5.48 kB
-
https://huggingface.co/spaces/Leon4gr45/Velo/resolve/main/src/components/editor/MonacoDiffViewer.tsx
- Command line
-
hf download hf://spaces/Leon4gr45/Velo/src/components/editor/MonacoDiffViewer.tsx
-
curl -L -o MonacoDiffViewer.tsx https://huggingface.co/spaces/Leon4gr45/Velo/resolve/main/src/components/editor/MonacoDiffViewer.tsx
5.48 kB
| import { useState, useRef, useEffect } from 'react' | |
| import { DiffEditor, type DiffOnMount } from '@monaco-editor/react' | |
| import { Check, X, Columns2, AlignJustify } from 'lucide-react' | |
| import { useTranslation } from 'react-i18next' | |
| import { useThemeStore } from '../../stores/theme-store' | |
| interface MonacoDiffViewerProps { | |
| /** 原始文本 */ | |
| original: string | |
| /** 修改后文本 */ | |
| modified: string | |
| /** 原始文本标签 */ | |
| originalLabel?: string | |
| /** 修改后文本标签 */ | |
| modifiedLabel?: string | |
| /** 接受修改回调 */ | |
| onAccept?: (text: string) => void | |
| /** 拒绝修改回调 */ | |
| onReject?: () => void | |
| } | |
| /** | |
| * Monaco Diff 对比查看器 | |
| * 使用 VS Code 同款 Monaco Editor 的 DiffEditor 组件 | |
| * 支持并排和内联两种视图模式 | |
| */ | |
| export default function MonacoDiffViewer({ | |
| original, | |
| modified, | |
| originalLabel, | |
| modifiedLabel, | |
| onAccept, | |
| onReject, | |
| }: MonacoDiffViewerProps) { | |
| const { t } = useTranslation('editors') | |
| const [inline, setInline] = useState(false) | |
| const theme = useThemeStore((s) => s.theme) | |
| const diffEditorRef = useRef<Parameters<DiffOnMount>[0] | null>(null) | |
| // 统计变更 | |
| const [stats, setStats] = useState({ additions: 0, deletions: 0 }) | |
| useEffect(() => { | |
| let mounted = true | |
| // 简单统计行差异 | |
| const origLines = original.split('\n').length | |
| const modLines = modified.split('\n').length | |
| const additions = Math.max(0, modLines - origLines) | |
| const deletions = Math.max(0, origLines - modLines) | |
| Promise.resolve().then(() => { | |
| if (mounted) { | |
| setStats({ additions: additions || Math.ceil(modLines * 0.1), deletions: deletions || Math.ceil(origLines * 0.05) }) | |
| } | |
| }) | |
| return () => { mounted = false } | |
| }, [original, modified]) | |
| const handleMount: DiffOnMount = (editor) => { | |
| diffEditorRef.current = editor | |
| } | |
| /** 获取修改后的内容 */ | |
| const getModifiedContent = (): string => { | |
| if (diffEditorRef.current) { | |
| return diffEditorRef.current.getModifiedEditor().getValue() | |
| } | |
| return modified | |
| } | |
| return ( | |
| <div className="flex flex-col h-full"> | |
| {/* 工具栏 */} | |
| <div | |
| className="no-select flex items-center justify-between px-4 h-9 flex-shrink-0" | |
| style={{ | |
| borderBottom: '1px solid var(--color-border)', | |
| backgroundColor: 'var(--color-sidebar)', | |
| }} | |
| > | |
| <div className="flex items-center gap-3"> | |
| <span className="text-xs font-medium" style={{ color: 'var(--color-text)' }}> | |
| {t('diffViewer.title')} | |
| </span> | |
| <span className="text-xs" style={{ color: 'var(--color-text-muted)' }}> | |
| {originalLabel ?? t('diffViewer.original')} → {modifiedLabel ?? t('diffViewer.revised')} | |
| </span> | |
| <span className="text-xs"> | |
| <span style={{ color: 'var(--color-success)' }}>+{stats.additions}</span> | |
| {' '} | |
| <span style={{ color: 'var(--color-error)' }}>-{stats.deletions}</span> | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| {/* 视图切换 */} | |
| <button | |
| onClick={() => setInline(!inline)} | |
| className="flex items-center gap-1 px-2 py-0.5 rounded text-xs" | |
| style={{ | |
| backgroundColor: 'var(--color-hover)', | |
| color: 'var(--color-text-secondary)', | |
| border: '1px solid var(--color-border)', | |
| }} | |
| title={inline ? t('diffViewer.toggleSideBySide') : t('diffViewer.toggleInline')} | |
| > | |
| {inline ? <Columns2 size={12} /> : <AlignJustify size={12} />} | |
| {inline ? t('diffViewer.sideBySideShort') : t('diffViewer.inlineShort')} | |
| </button> | |
| {/* 操作按钮 */} | |
| {onReject && ( | |
| <button | |
| onClick={onReject} | |
| className="flex items-center gap-1 px-2 py-0.5 rounded text-xs" | |
| style={{ color: 'var(--color-error)', border: '1px solid var(--color-border)' }} | |
| > | |
| <X size={12} /> {t('diffViewer.reject')} | |
| </button> | |
| )} | |
| {onAccept && ( | |
| <button | |
| onClick={() => onAccept(getModifiedContent())} | |
| className="flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium" | |
| style={{ | |
| backgroundColor: 'var(--color-accent)', | |
| color: '#fff', | |
| }} | |
| > | |
| <Check size={12} /> {t('diffViewer.acceptChanges')} | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| {/* Monaco Diff Editor */} | |
| <div className="flex-1"> | |
| <DiffEditor | |
| original={original} | |
| modified={modified} | |
| language="markdown" | |
| theme={theme === 'dark' ? 'vs-dark' : 'light'} | |
| onMount={handleMount} | |
| options={{ | |
| readOnly: false, | |
| renderSideBySide: !inline, | |
| originalEditable: false, | |
| fontSize: 14, | |
| lineHeight: 24, | |
| wordWrap: 'on', | |
| minimap: { enabled: false }, | |
| scrollBeyondLastLine: false, | |
| renderOverviewRuler: false, | |
| automaticLayout: true, | |
| padding: { top: 8, bottom: 8 }, | |
| lineNumbers: 'on', | |
| glyphMargin: false, | |
| folding: false, | |
| lineDecorationsWidth: 0, | |
| diffWordWrap: 'on', | |
| }} | |
| /> | |
| </div> | |
| </div> | |
| ) | |
| } | |