diff options
Diffstat (limited to 'frontend/src/components/DiffView.svelte')
| -rw-r--r-- | frontend/src/components/DiffView.svelte | 71 |
1 files changed, 71 insertions, 0 deletions
diff --git a/frontend/src/components/DiffView.svelte b/frontend/src/components/DiffView.svelte new file mode 100644 index 0000000..cf0c1be --- /dev/null +++ b/frontend/src/components/DiffView.svelte @@ -0,0 +1,71 @@ +<script lang="ts"> + import { diffLines } from 'diff'; + + interface Props { + before: string; + after: string; + beforeLabel?: string; + afterLabel?: string; + maxHeight?: number; + } + let { before, after, beforeLabel = '原版本', afterLabel = '新版本', maxHeight = 480 }: Props = $props(); + + const parts = $derived(diffLines(before ?? '', after ?? '')); + + const lines = $derived.by(() => { + const out: { t: 'a' | 'd' | 'c'; text: string }[] = []; + for (const p of parts) { + const pieces = p.value.replace(/\n$/, '').split('\n'); + for (const piece of pieces) { + if (p.added) out.push({ t: 'a', text: piece }); + else if (p.removed) out.push({ t: 'd', text: piece }); + else out.push({ t: 'c', text: piece }); + } + } + return out; + }); +</script> + +<div class="diff"> + <div class="legend"> + <span><i class="sw a"></i>新增({afterLabel})</span> + <span><i class="sw d"></i>删除({beforeLabel})</span> + </div> + <!-- svelte-ignore a11y_no_noninteractive_tabindex --> + <div class="body" style="max-height:{maxHeight}px" tabindex="0" aria-label="代码差异" role="region"> + {#each lines as l, i (i)} + {#if l.t === 'a'} + <span class="ln add" aria-hidden="true"></span><span class="code add">+{l.text}</span> + {:else if l.t === 'd'} + <span class="ln del" aria-hidden="true"></span><span class="code del">−{l.text}</span> + {:else} + <span class="ln" aria-hidden="true"> </span><span class="code">{l.text}</span> + {/if} + {/each} + </div> +</div> + +<style> + .diff .legend { + color: var(--text-3); + font-size: 0.8rem; + margin-bottom: 0.4rem; + display: flex; + gap: 1rem; + } + .sw { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 4px; } + .sw.a { background: #c7f0e6; } + .sw.d { background: #fbd4d0; } + .body { + overflow: auto; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.5rem 0.7rem; + } + .body .code { font-family: var(--mono); font-size: 12.5px; white-space: pre-wrap; word-break: break-all; display: inline-block; width: 100%; } + .add { background: #eafaf5; color: #14532d; } + .del { background: #fdf0ee; color: #7f1d1d; } + .ln { user-select: none; } + .code.add::before, .code.del::before { opacity: 0.6; } +</style> |
