451 lines
16 KiB
HTML
451 lines
16 KiB
HTML
<!doctype html>
|
|
<html lang="zh">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta
|
|
name="viewport"
|
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.5, user-scalable=yes"
|
|
/>
|
|
<title>轻量 · 代码差异对比器</title>
|
|
|
|
<!-- Highlight.js 主题 (GitHub 风格) -->
|
|
<link
|
|
rel="stylesheet"
|
|
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css"
|
|
/>
|
|
<!-- Diff2Html 核心样式 -->
|
|
<link
|
|
rel="stylesheet"
|
|
href="https://cdn.jsdelivr.net/npm/diff2html@3.4.47/bundles/css/diff2html.min.css"
|
|
/>
|
|
|
|
<!-- 引入 Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
<!-- 配置 Tailwind 字体 -->
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
sans: ['"Segoe UI"', "system-ui", "-apple-system", "sans-serif"],
|
|
mono: ['"JetBrains Mono"', '"Fira Code"', "monospace"],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<!-- 针对动态生成的 DOM 和 JS 强关联类的少量自定义样式 -->
|
|
<style type="text/tailwindcss">
|
|
@layer components {
|
|
/* 视图切换按钮的激活状态 (配合 JS) */
|
|
.view-option.active {
|
|
@apply bg-white text-blue-700 shadow-sm font-semibold;
|
|
}
|
|
/* JS 动态插入的空状态提示 */
|
|
.empty-message {
|
|
@apply p-10 text-center text-slate-500 bg-slate-50/50;
|
|
}
|
|
}
|
|
|
|
/* 优化 diff2html 内部样式 (Tailwind 无法直接控制第三方库的内部 DOM) */
|
|
.d2h-wrapper {
|
|
font-size: 14px;
|
|
}
|
|
.d2h-file-header {
|
|
display: none !important; /* 隐藏文件名条,更清爽 */
|
|
}
|
|
.d2h-code-line-ctn {
|
|
@apply font-mono !important;
|
|
}
|
|
/* 覆盖 diff2html 默认的难看边框 */
|
|
.d2h-file-wrapper {
|
|
border: none !important;
|
|
margin-bottom: 0 !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body
|
|
class="bg-slate-100 font-sans p-4 md:p-6 min-h-screen flex flex-col items-center text-slate-800"
|
|
>
|
|
<div
|
|
class="max-w-[1600px] w-full bg-white rounded-[28px] shadow-xl shadow-slate-200/50 p-6 md:p-8 transition-all"
|
|
>
|
|
<!-- 标题区 -->
|
|
<h1
|
|
class="font-semibold text-2xl md:text-3xl text-slate-900 flex items-center gap-3 mt-1 mb-6 border-b-2 border-slate-100 pb-5"
|
|
>
|
|
⚖️ Diff Checker
|
|
<span
|
|
class="bg-gradient-to-br from-blue-600 to-blue-800 text-white text-sm font-medium px-3 py-1 rounded-full ml-2 shadow-sm"
|
|
>
|
|
代码高亮 · 即时对比
|
|
</span>
|
|
</h1>
|
|
|
|
<!-- 双栏输入区 -->
|
|
<div class="flex flex-col lg:flex-row gap-5 mb-6">
|
|
<!-- 左侧面板 -->
|
|
<div
|
|
class="flex-1 flex flex-col bg-slate-50 rounded-2xl border border-slate-200 overflow-hidden shadow-sm"
|
|
>
|
|
<div
|
|
class="flex items-center justify-between px-4 py-3 bg-slate-100/50 border-b border-slate-200"
|
|
>
|
|
<label class="font-semibold text-slate-700 flex items-center gap-2">
|
|
📄 原始文本
|
|
<span
|
|
class="bg-slate-200 text-slate-600 text-xs px-2.5 py-0.5 rounded-full font-medium"
|
|
>A</span
|
|
>
|
|
</label>
|
|
<button
|
|
id="clearLeftBtn"
|
|
title="清空左侧"
|
|
class="px-3 py-1 text-sm font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-full hover:bg-blue-100 transition-colors"
|
|
>
|
|
清空
|
|
</button>
|
|
</div>
|
|
<textarea
|
|
id="leftTextarea"
|
|
class="w-full flex-1 min-h-[260px] h-[300px] p-4 font-mono text-sm leading-relaxed bg-white border-none outline-none resize-y text-slate-800 placeholder:text-slate-400 placeholder:italic"
|
|
placeholder="粘贴或输入原始代码 / 文本…"
|
|
>
|
|
function hello() {
|
|
console.log("Hello World");
|
|
return "Hi";
|
|
}</textarea
|
|
>
|
|
</div>
|
|
|
|
<!-- 右侧面板 -->
|
|
<div
|
|
class="flex-1 flex flex-col bg-slate-50 rounded-2xl border border-slate-200 overflow-hidden shadow-sm"
|
|
>
|
|
<div
|
|
class="flex items-center justify-between px-4 py-3 bg-slate-100/50 border-b border-slate-200"
|
|
>
|
|
<label class="font-semibold text-slate-700 flex items-center gap-2">
|
|
📝 修改文本
|
|
<span
|
|
class="bg-slate-200 text-slate-600 text-xs px-2.5 py-0.5 rounded-full font-medium"
|
|
>B</span
|
|
>
|
|
</label>
|
|
<button
|
|
id="clearRightBtn"
|
|
title="清空右侧"
|
|
class="px-3 py-1 text-sm font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-full hover:bg-blue-100 transition-colors"
|
|
>
|
|
清空
|
|
</button>
|
|
</div>
|
|
<textarea
|
|
id="rightTextarea"
|
|
class="w-full flex-1 min-h-[260px] h-[300px] p-4 font-mono text-sm leading-relaxed bg-white border-none outline-none resize-y text-slate-800 placeholder:text-slate-400 placeholder:italic"
|
|
placeholder="粘贴或输入修改后的版本…"
|
|
>
|
|
function hello() {
|
|
console.log("Hello, Diff Checker!");
|
|
return "Hey there";
|
|
}</textarea
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 工具栏 -->
|
|
<div class="flex flex-wrap items-center justify-between my-4 gap-4">
|
|
<div class="flex flex-wrap items-center gap-4">
|
|
<!-- 语言选择 -->
|
|
<div
|
|
class="flex items-center gap-2 bg-slate-100 px-4 py-1.5 rounded-full"
|
|
>
|
|
<label class="font-medium text-slate-700 text-sm"
|
|
>🔤 语言高亮</label
|
|
>
|
|
<select
|
|
id="languageSelect"
|
|
class="bg-white border border-slate-300 rounded-full px-3 py-1 text-sm font-medium text-slate-800 outline-none cursor-pointer shadow-sm hover:border-blue-500 transition-colors"
|
|
>
|
|
<option value="javascript" selected>JavaScript</option>
|
|
<option value="typescript">TypeScript</option>
|
|
<option value="html">HTML</option>
|
|
<option value="css">CSS</option>
|
|
<option value="json">JSON</option>
|
|
<option value="python">Python</option>
|
|
<option value="java">Java</option>
|
|
<option value="cpp">C++</option>
|
|
<option value="plaintext">纯文本 (无高亮)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 视图切换 -->
|
|
<div class="flex bg-slate-100 rounded-full p-1" id="viewToggle">
|
|
<button
|
|
class="view-option active px-4 py-1.5 rounded-full text-sm font-medium bg-transparent text-slate-500 hover:text-slate-800 transition-all"
|
|
data-view="side-by-side"
|
|
>
|
|
📊 并排
|
|
</button>
|
|
<button
|
|
class="view-option px-4 py-1.5 rounded-full text-sm font-medium bg-transparent text-slate-500 hover:text-slate-800 transition-all"
|
|
data-view="line-by-line"
|
|
>
|
|
📋 行内
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 操作按钮 -->
|
|
<div class="flex gap-3 flex-wrap">
|
|
<button
|
|
id="swapBtn"
|
|
title="交换左右内容"
|
|
class="inline-flex items-center gap-1.5 bg-white border border-slate-300 rounded-full px-5 py-2 font-medium text-sm text-slate-700 cursor-pointer transition-all shadow-sm hover:-translate-y-px hover:shadow-md hover:border-slate-400"
|
|
>
|
|
🔄 交换
|
|
</button>
|
|
<button
|
|
id="exampleBtn"
|
|
title="重置为示例"
|
|
class="inline-flex items-center gap-1.5 bg-white border border-slate-300 rounded-full px-5 py-2 font-medium text-sm text-slate-700 cursor-pointer transition-all shadow-sm hover:-translate-y-px hover:shadow-md hover:border-slate-400"
|
|
>
|
|
📋 示例
|
|
</button>
|
|
<button
|
|
id="compareBtn"
|
|
class="inline-flex items-center gap-1.5 bg-blue-700 border border-blue-700 text-white rounded-full px-6 py-2 font-medium text-sm cursor-pointer transition-all shadow-md shadow-blue-700/20 hover:bg-blue-600 hover:border-blue-600 hover:-translate-y-px hover:shadow-lg hover:shadow-blue-700/30"
|
|
>
|
|
✨ 对比差异
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 差异结果区域 -->
|
|
<div
|
|
class="mt-6 rounded-2xl border border-slate-200 bg-white overflow-hidden shadow-sm"
|
|
>
|
|
<div
|
|
class="px-5 py-3 bg-slate-50 border-b border-slate-200 font-semibold text-slate-800 flex items-center justify-between"
|
|
>
|
|
<span class="flex items-center gap-2">📌 差异结果</span>
|
|
<span
|
|
id="diffStats"
|
|
class="text-sm font-normal text-slate-500"
|
|
></span>
|
|
</div>
|
|
<div id="diff-output" class="overflow-x-auto bg-white">
|
|
<div class="empty-message">
|
|
点击「对比差异」查看并排/行内对比,支持语法高亮
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-5 text-right text-sm text-slate-400 font-medium">
|
|
⚡ 基于 diff2html · 高亮 by highlight.js
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 依赖库 -->
|
|
<script src="https://cdn.jsdelivr.net/npm/diff@5.1.0/dist/diff.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/diff2html@3.4.47/bundles/js/diff2html.min.js"></script>
|
|
|
|
<!-- 核心逻辑 (保持不变) -->
|
|
<script>
|
|
(function () {
|
|
"use strict";
|
|
|
|
const leftTextarea = document.getElementById("leftTextarea");
|
|
const rightTextarea = document.getElementById("rightTextarea");
|
|
const compareBtn = document.getElementById("compareBtn");
|
|
const swapBtn = document.getElementById("swapBtn");
|
|
const exampleBtn = document.getElementById("exampleBtn");
|
|
const clearLeftBtn = document.getElementById("clearLeftBtn");
|
|
const clearRightBtn = document.getElementById("clearRightBtn");
|
|
const languageSelect = document.getElementById("languageSelect");
|
|
const diffOutput = document.getElementById("diff-output");
|
|
const diffStats = document.getElementById("diffStats");
|
|
const viewOptions = document.querySelectorAll(".view-option");
|
|
|
|
let currentView = "side-by-side";
|
|
|
|
function getSelectedLanguage() {
|
|
return languageSelect.value;
|
|
}
|
|
|
|
function getHighlightConfig() {
|
|
const lang = getSelectedLanguage();
|
|
if (lang === "plaintext") return false;
|
|
return { enabled: true, language: lang };
|
|
}
|
|
|
|
function generateUnifiedDiff(original, modified) {
|
|
return Diff.createTwoFilesPatch(
|
|
"原始",
|
|
"修改",
|
|
original || "",
|
|
modified || "",
|
|
"",
|
|
"",
|
|
{ context: 4 },
|
|
);
|
|
}
|
|
|
|
function renderDiff() {
|
|
const leftText = leftTextarea.value;
|
|
const rightText = rightTextarea.value;
|
|
|
|
let diffString;
|
|
try {
|
|
diffString = generateUnifiedDiff(leftText, rightText);
|
|
} catch (e) {
|
|
diffOutput.innerHTML = `<div class="empty-message" style="color:#ef4444;">生成差异时出错: ${e.message}</div>`;
|
|
diffStats.textContent = "";
|
|
return;
|
|
}
|
|
|
|
const configuration = {
|
|
drawFileList: false,
|
|
matching: "lines",
|
|
outputFormat: currentView,
|
|
highlight: getHighlightConfig(),
|
|
renderNothingWhenEmpty: false,
|
|
};
|
|
|
|
let diffHtml = "";
|
|
try {
|
|
diffHtml = Diff2Html.html(diffString, configuration);
|
|
} catch (e) {
|
|
console.warn(e);
|
|
diffOutput.innerHTML = `<div class="empty-message">渲染差异失败,请检查输入</div>`;
|
|
diffStats.textContent = "";
|
|
return;
|
|
}
|
|
|
|
diffOutput.innerHTML = diffHtml;
|
|
|
|
const selectedLang = getSelectedLanguage();
|
|
if (selectedLang !== "plaintext") {
|
|
const codeBlocks = diffOutput.querySelectorAll("code");
|
|
if (codeBlocks.length > 0 && window.hljs) {
|
|
codeBlocks.forEach((block) => {
|
|
block.classList.forEach((cls) => {
|
|
if (cls.startsWith("language-")) block.classList.remove(cls);
|
|
});
|
|
block.classList.add(`language-${selectedLang}`);
|
|
if (block.dataset.highlighted) {
|
|
delete block.dataset.highlighted;
|
|
}
|
|
try {
|
|
hljs.highlightElement(block);
|
|
} catch (e) {}
|
|
});
|
|
}
|
|
}
|
|
|
|
const addedLines = diffOutput.querySelectorAll(".d2h-ins").length;
|
|
const deletedLines = diffOutput.querySelectorAll(".d2h-del").length;
|
|
if (addedLines === 0 && deletedLines === 0) {
|
|
diffStats.innerHTML =
|
|
"<span class='text-emerald-600 font-medium'>✅ 内容相同</span>";
|
|
} else {
|
|
diffStats.innerHTML = `<span class='text-emerald-600 font-medium'>+${addedLines} 行添加</span> | <span class='text-rose-500 font-medium'>-${deletedLines} 行删除</span>`;
|
|
}
|
|
|
|
if (!diffHtml.trim() || diffOutput.innerText.trim() === "") {
|
|
diffOutput.innerHTML =
|
|
'<div class="empty-message">两个文本完全相同,没有差异</div>';
|
|
diffStats.innerHTML =
|
|
"<span class='text-slate-500'>⚖️ 无差异</span>";
|
|
}
|
|
}
|
|
|
|
function setActiveView(view) {
|
|
currentView = view;
|
|
viewOptions.forEach((btn) => {
|
|
const val = btn.getAttribute("data-view");
|
|
if (val === view) {
|
|
btn.classList.add("active");
|
|
} else {
|
|
btn.classList.remove("active");
|
|
}
|
|
});
|
|
if (
|
|
diffOutput.querySelector(".d2h-wrapper") ||
|
|
diffOutput.children.length > 0
|
|
) {
|
|
renderDiff();
|
|
}
|
|
}
|
|
|
|
function loadExample() {
|
|
leftTextarea.value = `function hello() {\n console.log("Hello World");\n return "Hi";\n}`;
|
|
rightTextarea.value = `function hello() {\n console.log("Hello, Diff Checker!");\n return "Hey there";\n}`;
|
|
languageSelect.value = "javascript";
|
|
renderDiff();
|
|
}
|
|
|
|
function swapTexts() {
|
|
const temp = leftTextarea.value;
|
|
leftTextarea.value = rightTextarea.value;
|
|
rightTextarea.value = temp;
|
|
renderDiff();
|
|
}
|
|
|
|
function clearLeft() {
|
|
leftTextarea.value = "";
|
|
renderDiff();
|
|
}
|
|
function clearRight() {
|
|
rightTextarea.value = "";
|
|
renderDiff();
|
|
}
|
|
|
|
compareBtn.addEventListener("click", renderDiff);
|
|
swapBtn.addEventListener("click", swapTexts);
|
|
exampleBtn.addEventListener("click", loadExample);
|
|
clearLeftBtn.addEventListener("click", clearLeft);
|
|
clearRightBtn.addEventListener("click", clearRight);
|
|
|
|
languageSelect.addEventListener("change", () => {
|
|
if (
|
|
diffOutput.querySelector(".d2h-wrapper") ||
|
|
diffOutput.children.length > 0
|
|
) {
|
|
renderDiff();
|
|
}
|
|
});
|
|
|
|
viewOptions.forEach((btn) => {
|
|
btn.addEventListener("click", (e) => {
|
|
const view = e.currentTarget.getAttribute("data-view");
|
|
setActiveView(view);
|
|
});
|
|
});
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
|
e.preventDefault();
|
|
renderDiff();
|
|
}
|
|
});
|
|
|
|
window.addEventListener("DOMContentLoaded", () => {
|
|
renderDiff();
|
|
});
|
|
|
|
if (
|
|
document.readyState === "complete" ||
|
|
document.readyState === "interactive"
|
|
) {
|
|
setTimeout(renderDiff, 10);
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|