summaryrefslogtreecommitdiff
path: root/frontend/src/components/DiffView.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components/DiffView.svelte')
-rw-r--r--frontend/src/components/DiffView.svelte71
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>