From 32bf103f15cd212372b3eeaf96e2ae961986cf06 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Mon, 16 Feb 2026 23:51:23 +0800 Subject: [PATCH] fix(ui): add batch bar exit animation and chunked diff cards --- src/components/config/DiffModal.module.scss | 113 ++++++++++++------ src/components/config/DiffModal.tsx | 124 +++++++++++--------- src/pages/AuthFilesPage.tsx | 32 ++++- 3 files changed, 175 insertions(+), 94 deletions(-) diff --git a/src/components/config/DiffModal.module.scss b/src/components/config/DiffModal.module.scss index 7089c54..020ac1c 100644 --- a/src/components/config/DiffModal.module.scss +++ b/src/components/config/DiffModal.module.scss @@ -12,57 +12,92 @@ .content { display: flex; flex-direction: column; - gap: $spacing-sm; height: 70vh; min-height: 420px; } -.columnLabels { +.emptyState { + flex: 1; + border: 1px dashed var(--border-color); + border-radius: $radius-md; + background: var(--bg-secondary); + color: var(--text-secondary); + font-size: 14px; + display: grid; + place-items: center; +} + +.diffList { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + gap: $spacing-sm; + overflow: auto; + padding-right: 2px; +} + +.diffCard { + border: 1px solid var(--border-color); + border-radius: $radius-md; + background: var(--bg-secondary); + overflow: hidden; +} + +.diffCardHeader { + padding: 8px 10px; + border-bottom: 1px dashed var(--border-color); + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + background: color-mix(in srgb, var(--bg-primary) 92%, transparent); +} + +.diffColumns { display: grid; grid-template-columns: 1fr 1fr; gap: $spacing-sm; - font-size: 12px; - color: var(--text-secondary); - font-weight: 600; - - span { - padding: 0 2px; - } + padding: $spacing-sm; } -.mergeRoot { - flex: 1; - min-height: 0; +.diffColumn { + min-width: 0; border: 1px solid var(--border-color); - border-radius: $radius-md; + border-radius: $radius-sm; overflow: hidden; + background: var(--bg-primary); +} + +.diffColumnHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: $spacing-sm; + padding: 8px 10px; + border-bottom: 1px solid var(--border-color); + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); background: var(--bg-secondary); +} - :global(.cm-mergeView) { - height: 100%; - overflow: auto !important; - -webkit-overflow-scrolling: touch; - background: var(--bg-primary); - } +.lineRange { + font-size: 11px; + color: var(--text-tertiary); + font-family: 'Consolas', 'Monaco', 'Menlo', monospace; +} - :global(.cm-mergeViewEditors) { - min-height: 100%; - } - - :global(.cm-mergeViewEditor) { - height: 100%; - } - - :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); - } +.codeBlock { + margin: 0; + padding: 10px; + font-size: 12px; + line-height: 1.5; + color: var(--text-primary); + white-space: pre-wrap; + word-break: break-word; + font-family: 'Consolas', 'Monaco', 'Menlo', monospace; + overflow: auto; + max-height: 240px; } @include mobile { @@ -70,4 +105,8 @@ height: 65vh; min-height: 360px; } + + .diffColumns { + grid-template-columns: 1fr; + } } diff --git a/src/components/config/DiffModal.tsx b/src/components/config/DiffModal.tsx index 7665083..f771323 100644 --- a/src/components/config/DiffModal.tsx +++ b/src/components/config/DiffModal.tsx @@ -1,12 +1,9 @@ -import { useEffect, useRef } from 'react'; +import { useMemo } 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 { Text } from '@codemirror/state'; +import { Chunk } 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 = { @@ -18,6 +15,35 @@ type DiffModalProps = { loading?: boolean; }; +type DiffChunkCard = { + id: string; + currentLines: string; + modifiedLines: string; + currentText: string; + modifiedText: string; +}; + +const clampPos = (doc: Text, pos: number) => Math.max(0, Math.min(pos, doc.length)); + +const getLineRangeLabel = (doc: Text, from: number, to: number): string => { + const start = clampPos(doc, from); + const end = clampPos(doc, to); + if (start === end) { + const linePos = Math.min(start, doc.length); + return String(doc.lineAt(linePos).number); + } + const startLine = doc.lineAt(start).number; + const endLine = doc.lineAt(Math.max(start, end - 1)).number; + return startLine === endLine ? String(startLine) : `${startLine}-${endLine}`; +}; + +const getChunkText = (doc: Text, from: number, to: number): string => { + const start = clampPos(doc, from); + const end = clampPos(doc, to); + if (start >= end) return ''; + return doc.sliceString(start, end).trimEnd(); +}; + export function DiffModal({ open, original, @@ -27,50 +53,20 @@ export function DiffModal({ loading = false }: DiffModalProps) { const { t } = useTranslation(); - const resolvedTheme = useThemeStore((state) => state.resolvedTheme); - const mergeContainerRef = useRef(null); - const mergeViewRef = useRef(null); - useEffect(() => { - if (!open || !mergeContainerRef.current) return; + const diffCards = useMemo(() => { + const currentDoc = Text.of(original.split('\n')); + const modifiedDoc = Text.of(modified.split('\n')); + const chunks = Chunk.build(currentDoc, modifiedDoc); - 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 chunks.map((chunk, index) => ({ + id: `${index}-${chunk.fromA}-${chunk.toA}-${chunk.fromB}-${chunk.toB}`, + currentLines: getLineRangeLabel(currentDoc, chunk.fromA, chunk.toA), + modifiedLines: getLineRangeLabel(modifiedDoc, chunk.fromB, chunk.toB), + currentText: getChunkText(currentDoc, chunk.fromA, chunk.toA), + modifiedText: getChunkText(modifiedDoc, chunk.fromB, chunk.toB) + })); + }, [modified, original]); return (
-
- {t('config_management.diff.current')} - {t('config_management.diff.modified')} -
-
+ {diffCards.length === 0 ? ( +
{t('config_management.diff.no_changes')}
+ ) : ( +
+ {diffCards.map((card, index) => ( +
+
#{index + 1}
+
+
+
+ {t('config_management.diff.current')} + L{card.currentLines} +
+
{card.currentText || '-'}
+
+
+
+ {t('config_management.diff.modified')} + L{card.modifiedLines} +
+
{card.modifiedText || '-'}
+
+
+
+ ))} +
+ )}
); diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index 3e8cd8a..f2eb340 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -57,8 +57,10 @@ export function AuthFilesPage() { const [detailModalOpen, setDetailModalOpen] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [viewMode, setViewMode] = useState<'diagram' | 'list'>('list'); + const [batchActionBarVisible, setBatchActionBarVisible] = useState(false); const floatingBatchActionsRef = useRef(null); const previousSelectionCountRef = useRef(0); + const selectionCountRef = useRef(0); const { keyStats, usageDetails, loadKeyStats } = useAuthFilesStats(); const { @@ -331,24 +333,46 @@ export function AuthFilesPage() { window.removeEventListener('resize', updatePadding); document.documentElement.style.removeProperty('--auth-files-action-bar-height'); }; + }, [batchActionBarVisible, selectionCount]); + + useEffect(() => { + selectionCountRef.current = selectionCount; + if (selectionCount > 0) { + setBatchActionBarVisible(true); + } }, [selectionCount]); useLayoutEffect(() => { + if (!batchActionBarVisible) return; const currentCount = selectionCount; const previousCount = previousSelectionCountRef.current; const actionsEl = floatingBatchActionsRef.current; + if (!actionsEl) return; - if (currentCount > 0 && previousCount === 0 && actionsEl) { - gsap.killTweensOf(actionsEl); + gsap.killTweensOf(actionsEl); + + if (currentCount > 0 && previousCount === 0) { gsap.fromTo( actionsEl, { y: 56, autoAlpha: 0 }, { y: 0, autoAlpha: 1, duration: 0.28, ease: 'power3.out' } ); + } else if (currentCount === 0 && previousCount > 0) { + gsap.to(actionsEl, { + y: 56, + autoAlpha: 0, + duration: 0.22, + ease: 'power2.in', + onComplete: () => { + if (selectionCountRef.current === 0) { + setBatchActionBarVisible(false); + } + } + }); } previousSelectionCountRef.current = currentCount; - }, [selectionCount]); + }, [batchActionBarVisible, selectionCount]); const renderFilterTags = () => (
@@ -584,7 +608,7 @@ export function AuthFilesPage() { onChange={handlePrefixProxyChange} /> - {selectionCount > 0 && typeof document !== 'undefined' + {batchActionBarVisible && typeof document !== 'undefined' ? createPortal(