initial commit
This commit is contained in:
534
cyberpunk/index.html
Normal file
534
cyberpunk/index.html
Normal file
@@ -0,0 +1,534 @@
|
||||
<!doctype html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CYBER_DIFF // 2077</title>
|
||||
|
||||
<!-- Highlight.js 主题 (Atom One Dark - 适合暗黑模式) -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css"
|
||||
/>
|
||||
<!-- Diff2Html 核心样式 -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/diff2html@3.4.47/bundles/css/diff2html.min.css"
|
||||
/>
|
||||
|
||||
<!-- 引入赛博朋克风格字体 -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Orbitron:wght@400;700;900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- 引入 Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- 配置 Tailwind -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
cyber: {
|
||||
bg: "#050505",
|
||||
panel: "#0c0c0c",
|
||||
cyan: "#00f0ff",
|
||||
pink: "#ff003c",
|
||||
yellow: "#fcee0a",
|
||||
dark: "#111",
|
||||
border: "#333",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
cyber: ['"Orbitron"', "sans-serif"],
|
||||
mono: ['"Fira Code"', "monospace"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style type="text/tailwindcss">
|
||||
@layer components {
|
||||
/* 赛博朋克按钮 */
|
||||
.cyber-btn {
|
||||
@apply relative px-6 py-2 font-cyber font-bold uppercase tracking-widest text-sm transition-all duration-200 border-2 bg-transparent;
|
||||
clip-path: polygon(
|
||||
10px 0,
|
||||
100% 0,
|
||||
100% calc(100% - 10px),
|
||||
calc(100% - 10px) 100%,
|
||||
0 100%,
|
||||
0 10px
|
||||
);
|
||||
}
|
||||
.cyber-btn-cyan {
|
||||
@apply border-cyber-cyan text-cyber-cyan hover:bg-cyber-cyan hover:text-black hover:shadow-[0_0_15px_#00f0ff];
|
||||
}
|
||||
.cyber-btn-pink {
|
||||
@apply border-cyber-pink text-cyber-pink hover:bg-cyber-pink hover:text-black hover:shadow-[0_0_15px_#ff003c];
|
||||
}
|
||||
.cyber-btn-yellow {
|
||||
@apply border-cyber-yellow bg-cyber-yellow text-black hover:bg-transparent hover:text-cyber-yellow hover:shadow-[0_0_15px_#fcee0a];
|
||||
}
|
||||
|
||||
/* 视图切换按钮 */
|
||||
.view-option {
|
||||
@apply px-4 py-1 font-cyber text-xs tracking-wider text-gray-500 border border-transparent hover:text-cyber-cyan transition-colors;
|
||||
}
|
||||
.view-option.active {
|
||||
@apply text-cyber-cyan border-cyber-cyan bg-cyber-cyan/10 shadow-[inset_0_0_10px_rgba(0,240,255,0.2)];
|
||||
}
|
||||
|
||||
/* 面板边框发光效果 */
|
||||
.cyber-panel {
|
||||
@apply bg-cyber-panel border border-cyber-border relative;
|
||||
}
|
||||
.cyber-panel::before {
|
||||
content: "";
|
||||
@apply absolute top-0 left-0 w-2 h-2 border-t-2 border-l-2 border-cyber-cyan pointer-events-none;
|
||||
}
|
||||
.cyber-panel::after {
|
||||
content: "";
|
||||
@apply absolute bottom-0 right-0 w-2 h-2 border-b-2 border-r-2 border-cyber-pink pointer-events-none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
强制覆盖 Diff2Html 为暗黑模式 (核心)
|
||||
========================================== */
|
||||
.d2h-wrapper {
|
||||
font-family: "Fira Code", monospace !important;
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
.d2h-file-header {
|
||||
display: none !important;
|
||||
}
|
||||
.d2h-file-wrapper {
|
||||
border: none !important;
|
||||
background: #0c0c0c !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.d2h-code-line-ctn {
|
||||
@apply font-mono text-[14px] !important;
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
/* 差异行背景色 (暗黑适配) */
|
||||
.d2h-ins {
|
||||
background-color: rgba(0, 240, 255, 0.15) !important;
|
||||
border-color: #00f0ff !important;
|
||||
}
|
||||
.d2h-del {
|
||||
background-color: rgba(255, 0, 60, 0.15) !important;
|
||||
border-color: #ff003c !important;
|
||||
}
|
||||
|
||||
/* 行号区域暗黑适配 */
|
||||
.d2h-code-linenumber {
|
||||
background-color: #111 !important;
|
||||
border-color: #333 !important;
|
||||
color: #666 !important;
|
||||
}
|
||||
.d2h-info {
|
||||
background-color: #1a1a1a !important;
|
||||
color: #888 !important;
|
||||
border-color: #333 !important;
|
||||
}
|
||||
.d2h-emptyplaceholder {
|
||||
background-color: #0c0c0c !important;
|
||||
border-color: #333 !important;
|
||||
}
|
||||
tbody {
|
||||
border-color: #333 !important;
|
||||
}
|
||||
td {
|
||||
border-color: #333 !important;
|
||||
}
|
||||
|
||||
/* 滚动条赛博化 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #050505;
|
||||
border-left: 1px solid #1f2937;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #333;
|
||||
border-radius: 0;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #00f0ff;
|
||||
}
|
||||
|
||||
/* 文本框选中颜色 */
|
||||
::selection {
|
||||
background: #ff003c;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body
|
||||
class="bg-cyber-bg font-cyber p-4 md:p-8 min-h-screen flex flex-col items-center text-gray-300"
|
||||
>
|
||||
<!-- 顶部装饰线 -->
|
||||
<div
|
||||
class="fixed top-0 left-0 w-full h-1 bg-gradient-to-r from-cyber-cyan via-cyber-pink to-cyber-yellow z-50"
|
||||
></div>
|
||||
|
||||
<div class="max-w-[1600px] w-full flex flex-col gap-6">
|
||||
<!-- 标题区 -->
|
||||
<header
|
||||
class="flex items-end justify-between border-b border-cyber-border pb-4 relative"
|
||||
>
|
||||
<div>
|
||||
<h1
|
||||
class="font-black text-3xl md:text-4xl text-white tracking-widest flex items-center gap-3 drop-shadow-[0_0_10px_rgba(255,255,255,0.3)]"
|
||||
>
|
||||
CYBER<span class="text-cyber-pink">_</span>DIFF
|
||||
</h1>
|
||||
<p class="text-cyber-cyan text-xs tracking-[0.3em] mt-2 opacity-80">
|
||||
NEURAL_NETWORK_CODE_COMPARISON_SYS_V2.0
|
||||
</p>
|
||||
</div>
|
||||
<div class="hidden md:block text-right">
|
||||
<div class="text-cyber-yellow text-xs tracking-widest animate-pulse">
|
||||
SYS.STATUS // ONLINE
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 双栏输入区 -->
|
||||
<div class="flex flex-col lg:flex-row gap-6">
|
||||
<!-- 左侧面板 -->
|
||||
<div class="flex-1 flex flex-col cyber-panel">
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-2 bg-cyber-dark border-b border-cyber-border"
|
||||
>
|
||||
<label
|
||||
class="text-cyber-cyan text-sm tracking-widest flex items-center gap-2"
|
||||
>
|
||||
<span
|
||||
class="w-2 h-2 bg-cyber-cyan rounded-full animate-pulse"
|
||||
></span>
|
||||
INPUT_A // ORIGINAL
|
||||
</label>
|
||||
<button
|
||||
id="clearLeftBtn"
|
||||
class="text-xs text-gray-500 hover:text-cyber-pink transition-colors tracking-widest"
|
||||
>
|
||||
[ CLEAR ]
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="leftTextarea"
|
||||
class="w-full flex-1 min-h-[260px] h-[300px] p-4 font-mono text-[14px] leading-relaxed bg-transparent border-none outline-none resize-y text-gray-300 placeholder:text-gray-700 focus:shadow-[inset_0_0_20px_rgba(0,240,255,0.05)] transition-shadow"
|
||||
spellcheck="false"
|
||||
>
|
||||
function initCyberware() {
|
||||
console.log("Booting optics...");
|
||||
return "Ready";
|
||||
}</textarea
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 右侧面板 -->
|
||||
<div class="flex-1 flex flex-col cyber-panel">
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-2 bg-cyber-dark border-b border-cyber-border"
|
||||
>
|
||||
<label
|
||||
class="text-cyber-pink text-sm tracking-widest flex items-center gap-2"
|
||||
>
|
||||
<span
|
||||
class="w-2 h-2 bg-cyber-pink rounded-full animate-pulse"
|
||||
></span>
|
||||
INPUT_B // MODIFIED
|
||||
</label>
|
||||
<button
|
||||
id="clearRightBtn"
|
||||
class="text-xs text-gray-500 hover:text-cyber-cyan transition-colors tracking-widest"
|
||||
>
|
||||
[ CLEAR ]
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="rightTextarea"
|
||||
class="w-full flex-1 min-h-[260px] h-[300px] p-4 font-mono text-[14px] leading-relaxed bg-transparent border-none outline-none resize-y text-gray-300 placeholder:text-gray-700 focus:shadow-[inset_0_0_20px_rgba(255,0,60,0.05)] transition-shadow"
|
||||
spellcheck="false"
|
||||
>
|
||||
function initCyberware() {
|
||||
console.log("Booting Kiroshi optics v3.0...");
|
||||
connectToNetwork();
|
||||
return "Online";
|
||||
}</textarea
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-between gap-4 bg-cyber-dark p-4 border border-cyber-border relative"
|
||||
>
|
||||
<!-- 装饰角 -->
|
||||
<div class="absolute top-0 left-0 w-1 h-1 bg-cyber-yellow"></div>
|
||||
<div class="absolute bottom-0 right-0 w-1 h-1 bg-cyber-yellow"></div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-6">
|
||||
<!-- 语言选择 -->
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-xs text-gray-500 tracking-widest">LANG_</label>
|
||||
<select
|
||||
id="languageSelect"
|
||||
class="bg-cyber-bg border border-cyber-border text-cyber-cyan px-3 py-1 text-sm font-mono outline-none cursor-pointer hover:border-cyber-cyan transition-colors focus:border-cyber-cyan appearance-none"
|
||||
>
|
||||
<option value="javascript" selected>JS</option>
|
||||
<option value="typescript">TS</option>
|
||||
<option value="html">HTML</option>
|
||||
<option value="css">CSS</option>
|
||||
<option value="json">JSON</option>
|
||||
<option value="python">PY</option>
|
||||
<option value="java">JAVA</option>
|
||||
<option value="cpp">CPP</option>
|
||||
<option value="plaintext">TXT</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 视图切换 -->
|
||||
<div
|
||||
class="flex items-center gap-1 bg-cyber-bg border border-cyber-border p-1"
|
||||
id="viewToggle"
|
||||
>
|
||||
<button class="view-option active" data-view="side-by-side">
|
||||
SPLIT
|
||||
</button>
|
||||
<button class="view-option" data-view="line-by-line">
|
||||
UNIFIED
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
<button id="swapBtn" class="cyber-btn cyber-btn-cyan">SWAP</button>
|
||||
<button id="exampleBtn" class="cyber-btn cyber-btn-pink">DEMO</button>
|
||||
<button id="compareBtn" class="cyber-btn cyber-btn-yellow">
|
||||
RUN_DIFF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 差异结果区域 -->
|
||||
<div class="cyber-panel flex flex-col">
|
||||
<div
|
||||
class="px-4 py-3 bg-cyber-dark border-b border-cyber-border flex items-center justify-between"
|
||||
>
|
||||
<span class="text-white tracking-widest text-sm"
|
||||
>OUTPUT // DIFF_RESULT</span
|
||||
>
|
||||
<span id="diffStats" class="text-xs font-mono tracking-wider"></span>
|
||||
</div>
|
||||
<div
|
||||
id="diff-output"
|
||||
class="overflow-x-auto bg-cyber-panel min-h-[150px]"
|
||||
>
|
||||
<div class="p-10 text-center font-mono text-gray-600 tracking-widest">
|
||||
AWAITING EXECUTION...
|
||||
</div>
|
||||
</div>
|
||||
</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",
|
||||
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="p-10 text-center font-mono text-cyber-pink tracking-widest">SYS_ERR: ${e.message}</div>`;
|
||||
diffStats.textContent = "ERR";
|
||||
return;
|
||||
}
|
||||
|
||||
const configuration = {
|
||||
drawFileList: false,
|
||||
matching: "lines",
|
||||
outputFormat: currentView,
|
||||
highlight: getHighlightConfig(),
|
||||
renderNothingWhenEmpty: false,
|
||||
};
|
||||
|
||||
let diffHtml = "";
|
||||
try {
|
||||
diffHtml = Diff2Html.html(diffString, configuration);
|
||||
} catch (e) {
|
||||
diffOutput.innerHTML = `<div class="p-10 text-center font-mono text-cyber-pink tracking-widest">RENDER_FAILED</div>`;
|
||||
diffStats.textContent = "ERR";
|
||||
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-gray-500'>SYNCED // NO_CHANGES</span>";
|
||||
} else {
|
||||
diffStats.innerHTML = `<span class='text-cyber-cyan'>+${addedLines} INS</span> <span class='text-gray-600 mx-2'>|</span> <span class='text-cyber-pink'>-${deletedLines} DEL</span>`;
|
||||
}
|
||||
|
||||
if (!diffHtml.trim() || diffOutput.innerText.trim() === "") {
|
||||
diffOutput.innerHTML =
|
||||
'<div class="p-10 text-center font-mono text-gray-500 tracking-widest">DATA_MATCH // NO_DIFF_FOUND</div>';
|
||||
diffStats.innerHTML = "<span class='text-gray-500'>SYNCED</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 initCyberware() {\n console.log("Booting optics...");\n return "Ready";\n}`;
|
||||
rightTextarea.value = `function initCyberware() {\n console.log("Booting Kiroshi optics v3.0...");\n connectToNetwork();\n return "Online";\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) => {
|
||||
setActiveView(e.currentTarget.getAttribute("data-view"));
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
renderDiff();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("DOMContentLoaded", renderDiff);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
507
exe/index.html
Normal file
507
exe/index.html
Normal file
@@ -0,0 +1,507 @@
|
||||
<!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>DIFF_CHECKER.EXE</title>
|
||||
|
||||
<!-- Highlight.js 主题 (复古终端风格) -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs.min.css"
|
||||
/>
|
||||
<!-- Diff2Html 核心样式 -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/diff2html@3.4.47/bundles/css/diff2html.min.css"
|
||||
/>
|
||||
|
||||
<!-- 引入复古像素字体 -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=VT323&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- 引入 Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- 配置 Tailwind -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
win: {
|
||||
bg: "#c0c0c0",
|
||||
title: "#000080",
|
||||
desktop: "#008080",
|
||||
borderLight: "#ffffff",
|
||||
borderDark: "#000000",
|
||||
borderGray: "#808080",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
retro: ['"VT323"', "monospace"],
|
||||
mono: ['"Courier New"', "Courier", "monospace"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style type="text/tailwindcss">
|
||||
@layer components {
|
||||
/* 经典 Win95 凸起边框 (外边框) */
|
||||
.win-outset {
|
||||
@apply bg-win-bg border-[2px] border-t-win-borderLight border-l-win-borderLight border-b-win-borderDark border-r-win-borderDark;
|
||||
}
|
||||
/* 经典 Win95 凹陷边框 (输入框/内容区) */
|
||||
.win-inset {
|
||||
@apply bg-white border-[2px] border-t-win-borderDark border-l-win-borderDark border-b-win-borderLight border-r-win-borderLight;
|
||||
}
|
||||
/* 经典按钮 */
|
||||
.win-btn {
|
||||
@apply win-outset px-4 py-1 font-retro text-lg text-black cursor-pointer active:border-t-win-borderDark active:border-l-win-borderDark active:border-b-win-borderLight active:border-r-win-borderLight active:pt-[5px] active:pb-[3px] active:pl-[17px] active:pr-[15px] focus:outline-none focus:ring-1 focus:ring-black focus:ring-offset-1 focus:ring-offset-win-bg;
|
||||
}
|
||||
/* 视图切换按钮的激活状态 (凹陷) */
|
||||
.view-option.active {
|
||||
@apply border-t-win-borderDark border-l-win-borderDark border-b-win-borderLight border-r-win-borderLight pt-[5px] pb-[3px] pl-[17px] pr-[15px] bg-[url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABZJREFUeNpi2rVrdz8DAwMDMwIGAAgwAEYQAWtO1y8gAAAAAElFTkSuQmCC')] /* 经典棋盘格背景 */;
|
||||
}
|
||||
/* JS 动态插入的空状态提示 */
|
||||
.empty-message {
|
||||
@apply p-8 text-center font-retro text-xl text-gray-600 bg-white;
|
||||
}
|
||||
}
|
||||
|
||||
/* 覆盖 diff2html 默认样式,使其复古化 */
|
||||
.d2h-wrapper {
|
||||
font-family: "Courier New", Courier, monospace !important;
|
||||
}
|
||||
.d2h-file-header {
|
||||
display: none !important;
|
||||
}
|
||||
.d2h-code-line-ctn {
|
||||
@apply font-mono text-[14px] !important;
|
||||
}
|
||||
.d2h-file-wrapper {
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.d2h-code-line {
|
||||
padding: 0 0.5em !important;
|
||||
}
|
||||
/* 强化差异颜色,去掉现代的柔和感 */
|
||||
.d2h-ins {
|
||||
background-color: #a6ffb2 !important;
|
||||
border-color: #008000 !important;
|
||||
}
|
||||
.d2h-del {
|
||||
background-color: #ffb2b2 !important;
|
||||
border-color: #800000 !important;
|
||||
}
|
||||
|
||||
/* 复古滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #dfdfdf;
|
||||
border-left: 1px solid #fff;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c0c0c0;
|
||||
border: 2px solid;
|
||||
border-top-color: #fff;
|
||||
border-left-color: #fff;
|
||||
border-bottom-color: #000;
|
||||
border-right-color: #000;
|
||||
}
|
||||
::-webkit-scrollbar-button {
|
||||
background: #c0c0c0;
|
||||
border: 2px solid;
|
||||
border-top-color: #fff;
|
||||
border-left-color: #fff;
|
||||
border-bottom-color: #000;
|
||||
border-right-color: #000;
|
||||
display: block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body
|
||||
class="bg-win-desktop font-retro p-2 md:p-8 min-h-screen flex flex-col items-center text-black selection:bg-win-title selection:text-white"
|
||||
>
|
||||
<!-- 主窗口 -->
|
||||
<div
|
||||
class="max-w-[1600px] w-full win-outset p-[2px] flex flex-col shadow-2xl"
|
||||
>
|
||||
<!-- 标题栏 -->
|
||||
<div
|
||||
class="bg-win-title text-white px-2 py-1 flex items-center justify-between mb-2"
|
||||
>
|
||||
<h1 class="font-retro text-xl tracking-wider flex items-center gap-2">
|
||||
💾 DIFF_CHECKER.EXE
|
||||
</h1>
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
class="win-outset w-6 h-6 flex items-center justify-center text-black font-bold text-sm leading-none active:border-t-win-borderDark active:border-l-win-borderDark active:border-b-win-borderLight active:border-r-win-borderLight"
|
||||
>
|
||||
_
|
||||
</button>
|
||||
<button
|
||||
class="win-outset w-6 h-6 flex items-center justify-center text-black font-bold text-sm leading-none active:border-t-win-borderDark active:border-l-win-borderDark active:border-b-win-borderLight active:border-r-win-borderLight"
|
||||
>
|
||||
□
|
||||
</button>
|
||||
<button
|
||||
class="win-outset w-6 h-6 flex items-center justify-center text-black font-bold text-sm leading-none active:border-t-win-borderDark active:border-l-win-borderDark active:border-b-win-borderLight active:border-r-win-borderLight"
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 菜单栏 (装饰) -->
|
||||
<div
|
||||
class="flex gap-4 px-2 pb-2 text-lg border-b border-win-borderGray mb-3"
|
||||
>
|
||||
<span class="cursor-pointer hover:bg-win-title hover:text-white px-1"
|
||||
><u>F</u>ile</span
|
||||
>
|
||||
<span class="cursor-pointer hover:bg-win-title hover:text-white px-1"
|
||||
><u>E</u>dit</span
|
||||
>
|
||||
<span class="cursor-pointer hover:bg-win-title hover:text-white px-1"
|
||||
><u>V</u>iew</span
|
||||
>
|
||||
<span class="cursor-pointer hover:bg-win-title hover:text-white px-1"
|
||||
><u>H</u>elp</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="px-3 pb-3 flex-1 flex flex-col">
|
||||
<!-- 双栏输入区 -->
|
||||
<div class="flex flex-col lg:flex-row gap-4 mb-4">
|
||||
<!-- 左侧面板 -->
|
||||
<div class="flex-1 flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-lg tracking-wide">A:\ORIGINAL.TXT</label>
|
||||
<button id="clearLeftBtn" class="win-btn text-sm py-0 px-2">
|
||||
CLEAR
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="leftTextarea"
|
||||
class="win-inset w-full flex-1 min-h-[260px] h-[300px] p-2 font-mono text-[15px] leading-tight resize-y focus:outline-none"
|
||||
spellcheck="false"
|
||||
>
|
||||
function hello() {
|
||||
console.log("Hello World");
|
||||
return "Hi";
|
||||
}</textarea
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 右侧面板 -->
|
||||
<div class="flex-1 flex flex-col gap-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-lg tracking-wide">B:\MODIFIED.TXT</label>
|
||||
<button id="clearRightBtn" class="win-btn text-sm py-0 px-2">
|
||||
CLEAR
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="rightTextarea"
|
||||
class="win-inset w-full flex-1 min-h-[260px] h-[300px] p-2 font-mono text-[15px] leading-tight resize-y focus:outline-none"
|
||||
spellcheck="false"
|
||||
>
|
||||
function hello() {
|
||||
console.log("Hello, Diff Checker!");
|
||||
return "Hey there";
|
||||
}</textarea
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div
|
||||
class="win-outset p-2 mb-4 flex flex-wrap items-center justify-between gap-4"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-6">
|
||||
<!-- 语言选择 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-lg">LANG:</label>
|
||||
<select
|
||||
id="languageSelect"
|
||||
class="win-inset px-2 py-1 text-lg font-retro outline-none cursor-pointer"
|
||||
>
|
||||
<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">PLAIN_TEXT</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 视图切换 -->
|
||||
<div class="flex items-center gap-2" id="viewToggle">
|
||||
<label class="text-lg">VIEW:</label>
|
||||
<button
|
||||
class="win-btn view-option active"
|
||||
data-view="side-by-side"
|
||||
>
|
||||
SPLIT
|
||||
</button>
|
||||
<button class="win-btn view-option" data-view="line-by-line">
|
||||
UNIFIED
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button id="swapBtn" class="win-btn">SWAP</button>
|
||||
<button id="exampleBtn" class="win-btn">DEMO</button>
|
||||
<button id="compareBtn" class="win-btn font-bold border-[3px]">
|
||||
EXECUTE_DIFF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 差异结果区域 -->
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-lg">C:\OUTPUT.LOG</span>
|
||||
<span
|
||||
id="diffStats"
|
||||
class="text-lg bg-black text-[#00ff00] px-2 font-mono"
|
||||
></span>
|
||||
</div>
|
||||
<div class="win-inset flex-1 overflow-hidden flex flex-col bg-white">
|
||||
<div id="diff-output" class="overflow-x-auto flex-1">
|
||||
<div class="empty-message">PRESS [EXECUTE_DIFF] TO START...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<div
|
||||
class="win-inset border-t-win-borderDark border-l-win-borderDark border-b-win-borderLight border-r-win-borderLight bg-win-bg px-2 py-1 mt-1 flex justify-between text-sm font-retro"
|
||||
>
|
||||
<span>STATUS: READY</span>
|
||||
<span>POWERED BY DIFF2HTML & HIGHLIGHT.JS</span>
|
||||
</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",
|
||||
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:red;">ERROR: ${e.message}</div>`;
|
||||
diffStats.textContent = "ERR";
|
||||
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">RENDER FAILED.</div>`;
|
||||
diffStats.textContent = "ERR";
|
||||
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 = "NO_CHANGES_DETECTED";
|
||||
} else {
|
||||
// 复古终端风格的统计输出
|
||||
diffStats.innerHTML = `[ +${addedLines} INS | -${deletedLines} DEL ]`;
|
||||
}
|
||||
|
||||
if (!diffHtml.trim() || diffOutput.innerText.trim() === "") {
|
||||
diffOutput.innerHTML =
|
||||
'<div class="empty-message">FILES ARE IDENTICAL.</div>';
|
||||
diffStats.innerHTML = "NO_CHANGES";
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
450
index.html
Normal file
450
index.html
Normal file
@@ -0,0 +1,450 @@
|
||||
<!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>
|
||||
598
macos/index.html
Normal file
598
macos/index.html
Normal file
@@ -0,0 +1,598 @@
|
||||
<!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>Diff Checker - macOS Glass</title>
|
||||
|
||||
<!-- Highlight.js 主题 (替换为 Xcode 风格) -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/xcode.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: [
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
'"SF Pro Text"',
|
||||
'"Segoe UI"',
|
||||
"Roboto",
|
||||
"sans-serif",
|
||||
],
|
||||
mono: [
|
||||
'"SF Mono"',
|
||||
'"Menlo"',
|
||||
'"Monaco"',
|
||||
'"Courier New"',
|
||||
"monospace",
|
||||
],
|
||||
},
|
||||
animation: {
|
||||
blob: "blob 10s infinite",
|
||||
},
|
||||
keyframes: {
|
||||
blob: {
|
||||
"0%": { transform: "translate(0px, 0px) scale(1)" },
|
||||
"33%": { transform: "translate(30px, -50px) scale(1.1)" },
|
||||
"66%": { transform: "translate(-20px, 20px) scale(0.9)" },
|
||||
"100%": { transform: "translate(0px, 0px) scale(1)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style type="text/tailwindcss">
|
||||
@layer components {
|
||||
/* 毛玻璃面板基础类 */
|
||||
.glass-panel {
|
||||
@apply bg-white/40 backdrop-blur-xl border border-white/60 shadow-[0_8px_32px_0_rgba(31,38,135,0.07)];
|
||||
}
|
||||
/* 毛玻璃输入框 */
|
||||
.glass-input {
|
||||
@apply bg-white/50 backdrop-blur-md border border-white/50 focus:bg-white/80 focus:border-white/80 focus:shadow-[0_0_0_4px_rgba(255,255,255,0.3)] transition-all duration-300 outline-none;
|
||||
}
|
||||
/* 毛玻璃按钮 */
|
||||
.glass-btn {
|
||||
@apply bg-white/50 backdrop-blur-md border border-white/60 text-slate-700 font-medium hover:bg-white/80 hover:shadow-sm hover:-translate-y-[0.5px] active:translate-y-[0.5px] transition-all duration-200;
|
||||
}
|
||||
/* 视图切换按钮激活状态 */
|
||||
.view-option {
|
||||
@apply px-4 py-1.5 rounded-lg text-sm font-medium text-slate-500 hover:text-slate-800 transition-all;
|
||||
}
|
||||
.view-option.active {
|
||||
@apply bg-white/80 text-blue-600 shadow-sm border border-white/80;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
覆盖 Diff2Html 样式,使其融入毛玻璃背景
|
||||
========================================== */
|
||||
.d2h-wrapper {
|
||||
font-family: "SF Mono", Menlo, Monaco, monospace !important;
|
||||
}
|
||||
.d2h-file-header {
|
||||
display: none !important;
|
||||
}
|
||||
.d2h-file-wrapper {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.d2h-code-line-ctn {
|
||||
@apply font-mono text-[13px] !important;
|
||||
color: #334155 !important;
|
||||
}
|
||||
|
||||
/* 差异行背景色 (柔和的苹果风色彩) */
|
||||
.d2h-ins {
|
||||
background-color: rgba(52, 199, 89, 0.15) !important;
|
||||
border-color: rgba(52, 199, 89, 0.3) !important;
|
||||
}
|
||||
.d2h-del {
|
||||
background-color: rgba(255, 59, 48, 0.12) !important;
|
||||
border-color: rgba(255, 59, 48, 0.3) !important;
|
||||
}
|
||||
|
||||
/* 行号和空行区域透明化 */
|
||||
.d2h-code-linenumber {
|
||||
background-color: rgba(255, 255, 255, 0.3) !important;
|
||||
border-color: rgba(255, 255, 255, 0.4) !important;
|
||||
color: #94a3b8 !important;
|
||||
}
|
||||
.d2h-info {
|
||||
background-color: rgba(255, 255, 255, 0.4) !important;
|
||||
color: #64748b !important;
|
||||
border-color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
.d2h-emptyplaceholder {
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
border-color: rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
tbody {
|
||||
border-color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
td {
|
||||
border-color: rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
/* macOS 风格滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 10px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
/* 选中文本颜色 */
|
||||
::selection {
|
||||
background: rgba(10, 132, 255, 0.3);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body
|
||||
class="font-sans min-h-screen flex flex-col items-center p-4 md:p-8 text-slate-800 relative overflow-x-hidden selection:bg-blue-200"
|
||||
>
|
||||
<!-- 动态弥散渐变背景 (macOS Big Sur 风格) -->
|
||||
<div class="fixed inset-0 z-[-1] bg-[#f4f7fb] overflow-hidden">
|
||||
<div
|
||||
class="absolute top-[-10%] left-[-10%] w-[40rem] h-[40rem] bg-purple-300/60 rounded-full mix-blend-multiply filter blur-[100px] animate-blob"
|
||||
></div>
|
||||
<div
|
||||
class="absolute top-[20%] right-[-10%] w-[35rem] h-[35rem] bg-blue-300/60 rounded-full mix-blend-multiply filter blur-[100px] animate-blob"
|
||||
style="animation-delay: 2s"
|
||||
></div>
|
||||
<div
|
||||
class="absolute bottom-[-20%] left-[20%] w-[45rem] h-[45rem] bg-pink-300/60 rounded-full mix-blend-multiply filter blur-[100px] animate-blob"
|
||||
style="animation-delay: 4s"
|
||||
></div>
|
||||
<div
|
||||
class="absolute bottom-[10%] right-[20%] w-[30rem] h-[30rem] bg-teal-200/60 rounded-full mix-blend-multiply filter blur-[100px] animate-blob"
|
||||
style="animation-delay: 6s"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- 主窗口 -->
|
||||
<div
|
||||
class="max-w-[1600px] w-full glass-panel rounded-[2rem] p-2 flex flex-col"
|
||||
>
|
||||
<!-- macOS 标题栏 -->
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-3 border-b border-white/40"
|
||||
>
|
||||
<!-- 红黄绿按钮 -->
|
||||
<div class="flex gap-2 w-20">
|
||||
<div
|
||||
class="w-3.5 h-3.5 rounded-full bg-[#FF5F56] border border-[#E0443E] shadow-inner"
|
||||
></div>
|
||||
<div
|
||||
class="w-3.5 h-3.5 rounded-full bg-[#FFBD2E] border border-[#DEA123] shadow-inner"
|
||||
></div>
|
||||
<div
|
||||
class="w-3.5 h-3.5 rounded-full bg-[#27C93F] border border-[#1AAB29] shadow-inner"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- 标题 -->
|
||||
<h1
|
||||
class="font-semibold text-slate-700 text-sm tracking-wide flex-1 text-center flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-slate-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
||||
></path>
|
||||
</svg>
|
||||
Diff Checker
|
||||
</h1>
|
||||
|
||||
<div class="w-20"></div>
|
||||
<!-- 占位,保持标题居中 -->
|
||||
</div>
|
||||
|
||||
<div class="p-4 md:p-6 flex-1 flex flex-col gap-6">
|
||||
<!-- 双栏输入区 -->
|
||||
<div class="flex flex-col lg:flex-row gap-6">
|
||||
<!-- 左侧面板 -->
|
||||
<div class="flex-1 flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between px-2">
|
||||
<label
|
||||
class="text-sm font-semibold text-slate-600 flex items-center gap-2"
|
||||
>
|
||||
<span class="w-2 h-2 rounded-full bg-slate-400"></span>
|
||||
Original File
|
||||
</label>
|
||||
<button
|
||||
id="clearLeftBtn"
|
||||
class="text-xs font-medium text-slate-400 hover:text-slate-700 transition-colors"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="leftTextarea"
|
||||
class="glass-input w-full flex-1 min-h-[260px] h-[300px] p-5 rounded-2xl font-mono text-[13px] leading-relaxed resize-y text-slate-700 placeholder:text-slate-400"
|
||||
spellcheck="false"
|
||||
>
|
||||
function hello() {
|
||||
console.log("Hello World");
|
||||
return "Hi";
|
||||
}</textarea
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 右侧面板 -->
|
||||
<div class="flex-1 flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between px-2">
|
||||
<label
|
||||
class="text-sm font-semibold text-slate-600 flex items-center gap-2"
|
||||
>
|
||||
<span class="w-2 h-2 rounded-full bg-blue-400"></span>
|
||||
Modified File
|
||||
</label>
|
||||
<button
|
||||
id="clearRightBtn"
|
||||
class="text-xs font-medium text-slate-400 hover:text-slate-700 transition-colors"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="rightTextarea"
|
||||
class="glass-input w-full flex-1 min-h-[260px] h-[300px] p-5 rounded-2xl font-mono text-[13px] leading-relaxed resize-y text-slate-700 placeholder:text-slate-400"
|
||||
spellcheck="false"
|
||||
>
|
||||
function hello() {
|
||||
console.log("Hello, Diff Checker!");
|
||||
return "Hey there";
|
||||
}</textarea
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div
|
||||
class="glass-panel rounded-2xl p-3 flex flex-wrap items-center justify-between gap-4"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<!-- 语言选择 -->
|
||||
<div
|
||||
class="flex items-center gap-2 bg-white/40 px-3 py-1.5 rounded-xl border border-white/50"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 text-slate-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
|
||||
></path>
|
||||
</svg>
|
||||
<select
|
||||
id="languageSelect"
|
||||
class="bg-transparent text-sm font-medium text-slate-700 outline-none cursor-pointer appearance-none pr-4"
|
||||
>
|
||||
<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">Plain Text</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 视图切换 -->
|
||||
<div
|
||||
class="flex items-center bg-white/40 p-1 rounded-xl border border-white/50"
|
||||
id="viewToggle"
|
||||
>
|
||||
<button class="view-option active" data-view="side-by-side">
|
||||
Split
|
||||
</button>
|
||||
<button class="view-option" data-view="line-by-line">
|
||||
Unified
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-3 flex-wrap">
|
||||
<button
|
||||
id="swapBtn"
|
||||
class="glass-btn px-5 py-2 rounded-xl text-sm flex items-center gap-2"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"
|
||||
></path>
|
||||
</svg>
|
||||
Swap
|
||||
</button>
|
||||
<button
|
||||
id="exampleBtn"
|
||||
class="glass-btn px-5 py-2 rounded-xl text-sm"
|
||||
>
|
||||
Demo
|
||||
</button>
|
||||
<button
|
||||
id="compareBtn"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white shadow-[0_4px_14px_0_rgba(59,130,246,0.39)] hover:shadow-[0_6px_20px_rgba(59,130,246,0.23)] px-6 py-2 rounded-xl text-sm font-medium transition-all duration-200 hover:-translate-y-[0.5px] active:translate-y-[0.5px] flex items-center gap-2 border border-blue-400/50"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
Compare
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 差异结果区域 -->
|
||||
<div class="glass-input rounded-2xl flex flex-col overflow-hidden">
|
||||
<div
|
||||
class="px-5 py-3 border-b border-white/50 flex items-center justify-between bg-white/30"
|
||||
>
|
||||
<span class="text-sm font-semibold text-slate-700"
|
||||
>Diff Output</span
|
||||
>
|
||||
<span
|
||||
id="diffStats"
|
||||
class="text-xs font-medium text-slate-500 bg-white/50 px-3 py-1 rounded-full border border-white/60"
|
||||
></span>
|
||||
</div>
|
||||
<div id="diff-output" class="overflow-x-auto min-h-[150px]">
|
||||
<div class="p-12 text-center text-slate-400 text-sm">
|
||||
Ready to compare files.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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",
|
||||
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="p-12 text-center text-red-500 text-sm">Error: ${e.message}</div>`;
|
||||
diffStats.textContent = "Error";
|
||||
return;
|
||||
}
|
||||
|
||||
const configuration = {
|
||||
drawFileList: false,
|
||||
matching: "lines",
|
||||
outputFormat: currentView,
|
||||
highlight: getHighlightConfig(),
|
||||
renderNothingWhenEmpty: false,
|
||||
};
|
||||
|
||||
let diffHtml = "";
|
||||
try {
|
||||
diffHtml = Diff2Html.html(diffString, configuration);
|
||||
} catch (e) {
|
||||
diffOutput.innerHTML = `<div class="p-12 text-center text-red-500 text-sm">Render Failed</div>`;
|
||||
diffStats.textContent = "Error";
|
||||
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 = "No Changes";
|
||||
} else {
|
||||
diffStats.innerHTML = `<span class='text-green-600'>+${addedLines}</span> | <span class='text-red-500'>-${deletedLines}</span>`;
|
||||
}
|
||||
|
||||
if (!diffHtml.trim() || diffOutput.innerText.trim() === "") {
|
||||
diffOutput.innerHTML =
|
||||
'<div class="p-12 text-center text-slate-400 text-sm">Files are identical.</div>';
|
||||
diffStats.innerHTML = "Identical";
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
setActiveView(e.currentTarget.getAttribute("data-view"));
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
renderDiff();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("DOMContentLoaded", renderDiff);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
602
matrix/index.html
Normal file
602
matrix/index.html
Normal file
@@ -0,0 +1,602 @@
|
||||
<!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>MATRIX // DIFF_PROTOCOL</title>
|
||||
|
||||
<!-- Highlight.js 主题 (暗黑基础) -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"
|
||||
/>
|
||||
<!-- Diff2Html 核心样式 -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/diff2html@3.4.47/bundles/css/diff2html.min.css"
|
||||
/>
|
||||
|
||||
<!-- 引入极客等宽字体 -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- 引入 Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- 配置 Tailwind -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
matrix: {
|
||||
bg: "#000000",
|
||||
green: "#00FF41",
|
||||
darkGreen: "#008F11",
|
||||
darkestGreen: "#003B00",
|
||||
red: "#FF003C",
|
||||
darkRed: "#3B0000",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
mono: ['"Share Tech Mono"', "monospace", "Courier New"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style type="text/tailwindcss">
|
||||
@layer components {
|
||||
/* 矩阵面板 */
|
||||
.matrix-panel {
|
||||
@apply bg-black/85 backdrop-blur-sm border border-matrix-darkGreen shadow-[0_0_10px_rgba(0,143,17,0.2)] relative;
|
||||
}
|
||||
/* 矩阵输入框 */
|
||||
.matrix-input {
|
||||
@apply bg-transparent text-matrix-green border-none outline-none resize-y placeholder:text-matrix-darkGreen/50 focus:shadow-[inset_0_0_20px_rgba(0,255,65,0.05)];
|
||||
text-shadow: 0 0 2px rgba(0, 255, 65, 0.4);
|
||||
}
|
||||
/* 矩阵按钮 */
|
||||
.matrix-btn {
|
||||
@apply bg-transparent text-matrix-green border border-matrix-darkGreen px-4 py-1 font-mono uppercase tracking-widest hover:bg-matrix-green hover:text-black hover:shadow-[0_0_15px_#00FF41] transition-all duration-200 cursor-pointer;
|
||||
}
|
||||
/* 视图切换按钮 */
|
||||
.view-option {
|
||||
@apply px-3 py-1 text-matrix-darkGreen hover:text-matrix-green transition-colors;
|
||||
}
|
||||
.view-option.active {
|
||||
@apply text-black bg-matrix-green shadow-[0_0_10px_#00FF41];
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
全局 CRT 扫描线滤镜
|
||||
========================================== */
|
||||
body::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background:
|
||||
linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%),
|
||||
linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 0, 0, 0.06),
|
||||
rgba(0, 255, 0, 0.02),
|
||||
rgba(0, 0, 255, 0.06)
|
||||
);
|
||||
z-index: 999;
|
||||
background-size:
|
||||
100% 2px,
|
||||
3px 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
覆盖 Diff2Html 样式 (黑客帝国化)
|
||||
========================================== */
|
||||
.d2h-wrapper {
|
||||
font-family: "Share Tech Mono", monospace !important;
|
||||
color: #00ff41 !important;
|
||||
}
|
||||
.d2h-file-header {
|
||||
display: none !important;
|
||||
}
|
||||
.d2h-file-wrapper {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.d2h-code-line-ctn {
|
||||
@apply font-mono text-[15px] !important;
|
||||
color: #00ff41 !important;
|
||||
text-shadow: 0 0 2px rgba(0, 255, 65, 0.3);
|
||||
}
|
||||
|
||||
/* 差异行背景色 */
|
||||
.d2h-ins {
|
||||
background-color: rgba(0, 59, 0, 0.8) !important;
|
||||
border-color: #00ff41 !important;
|
||||
color: #00ff41 !important;
|
||||
}
|
||||
.d2h-del {
|
||||
background-color: rgba(59, 0, 0, 0.8) !important;
|
||||
border-color: #ff003c !important;
|
||||
color: #ff003c !important;
|
||||
text-shadow: 0 0 2px rgba(255, 0, 60, 0.5);
|
||||
}
|
||||
|
||||
/* 行号区域 */
|
||||
.d2h-code-linenumber {
|
||||
background-color: #000 !important;
|
||||
border-color: #008f11 !important;
|
||||
color: #008f11 !important;
|
||||
}
|
||||
.d2h-info {
|
||||
background-color: #001100 !important;
|
||||
color: #008f11 !important;
|
||||
border-color: #008f11 !important;
|
||||
}
|
||||
.d2h-emptyplaceholder {
|
||||
background-color: #000 !important;
|
||||
border-color: #008f11 !important;
|
||||
}
|
||||
tbody {
|
||||
border-color: #008f11 !important;
|
||||
}
|
||||
td {
|
||||
border-color: #008f11 !important;
|
||||
}
|
||||
|
||||
/* 覆盖 highlight.js 的默认颜色,强制偏绿 (保留部分语法高亮但统一色调) */
|
||||
.hljs-keyword,
|
||||
.hljs-built_in {
|
||||
color: #fff !important;
|
||||
text-shadow: 0 0 5px #fff;
|
||||
}
|
||||
.hljs-string,
|
||||
.hljs-title {
|
||||
color: #00ff41 !important;
|
||||
}
|
||||
.hljs-comment {
|
||||
color: #008f11 !important;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 矩阵滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #000;
|
||||
border-left: 1px solid #008f11;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #003b00;
|
||||
border: 1px solid #008f11;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #00ff41;
|
||||
}
|
||||
|
||||
/* 选中文本颜色 */
|
||||
::selection {
|
||||
background: #00ff41;
|
||||
color: #000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body
|
||||
class="bg-matrix-bg font-mono p-4 md:p-8 min-h-screen flex flex-col items-center text-matrix-green selection:bg-matrix-green selection:text-black relative overflow-x-hidden"
|
||||
>
|
||||
<!-- HTML5 Canvas 矩阵数字雨背景 -->
|
||||
<canvas id="matrix-canvas" class="fixed inset-0 z-[-1] opacity-40"></canvas>
|
||||
|
||||
<!-- 主窗口 -->
|
||||
<div class="max-w-[1600px] w-full flex flex-col gap-6 z-10">
|
||||
<!-- 标题区 -->
|
||||
<header
|
||||
class="border-b-2 border-matrix-darkGreen pb-4 flex justify-between items-end"
|
||||
>
|
||||
<div>
|
||||
<h1
|
||||
class="text-3xl md:text-4xl font-bold tracking-widest drop-shadow-[0_0_8px_#00FF41]"
|
||||
>
|
||||
WAKE_UP_NEO <span class="animate-pulse">_</span>
|
||||
</h1>
|
||||
<p class="text-matrix-darkGreen mt-2 tracking-[0.2em] text-sm">
|
||||
SYSTEM.DIFF_PROTOCOL // V.1.0.0
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="hidden md:block text-right text-xs text-matrix-darkGreen tracking-widest"
|
||||
>
|
||||
CONNECTION: SECURE<br />
|
||||
ENCRYPTION: RSA-4096
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 双栏输入区 -->
|
||||
<div class="flex flex-col lg:flex-row gap-6">
|
||||
<!-- 左侧面板 -->
|
||||
<div class="flex-1 flex flex-col matrix-panel">
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-2 border-b border-matrix-darkGreen bg-matrix-darkestGreen/50"
|
||||
>
|
||||
<label class="text-sm tracking-widest flex items-center gap-2">
|
||||
> SOURCE_FILE.TXT
|
||||
</label>
|
||||
<button
|
||||
id="clearLeftBtn"
|
||||
class="text-xs text-matrix-darkGreen hover:text-matrix-green transition-colors tracking-widest"
|
||||
>
|
||||
[ PURGE ]
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="leftTextarea"
|
||||
class="matrix-input w-full flex-1 min-h-[260px] h-[300px] p-4 text-[15px] leading-relaxed"
|
||||
spellcheck="false"
|
||||
>
|
||||
function enterMatrix() {
|
||||
console.log("Follow the white rabbit.");
|
||||
return "Ignorance is bliss.";
|
||||
}</textarea
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 右侧面板 -->
|
||||
<div class="flex-1 flex flex-col matrix-panel">
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-2 border-b border-matrix-darkGreen bg-matrix-darkestGreen/50"
|
||||
>
|
||||
<label class="text-sm tracking-widest flex items-center gap-2">
|
||||
> TARGET_FILE.TXT
|
||||
</label>
|
||||
<button
|
||||
id="clearRightBtn"
|
||||
class="text-xs text-matrix-darkGreen hover:text-matrix-green transition-colors tracking-widest"
|
||||
>
|
||||
[ PURGE ]
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="rightTextarea"
|
||||
class="matrix-input w-full flex-1 min-h-[260px] h-[300px] p-4 text-[15px] leading-relaxed"
|
||||
spellcheck="false"
|
||||
>
|
||||
function enterMatrix() {
|
||||
console.log("There is no spoon.");
|
||||
decodeConstruct();
|
||||
return "I know Kung Fu.";
|
||||
}</textarea
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div
|
||||
class="matrix-panel p-4 flex flex-wrap items-center justify-between gap-4"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-6">
|
||||
<!-- 语言选择 -->
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-sm tracking-widest text-matrix-darkGreen"
|
||||
>PARSER:</label
|
||||
>
|
||||
<select
|
||||
id="languageSelect"
|
||||
class="bg-black border border-matrix-darkGreen text-matrix-green px-3 py-1 text-sm outline-none cursor-pointer hover:border-matrix-green focus:border-matrix-green focus:shadow-[0_0_10px_#00FF41] transition-all appearance-none"
|
||||
>
|
||||
<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">RAW_TEXT</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 视图切换 -->
|
||||
<div
|
||||
class="flex items-center border border-matrix-darkGreen p-0.5"
|
||||
id="viewToggle"
|
||||
>
|
||||
<button class="view-option active" data-view="side-by-side">
|
||||
SPLIT
|
||||
</button>
|
||||
<button class="view-option" data-view="line-by-line">
|
||||
UNIFIED
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
<button id="swapBtn" class="matrix-btn">[ INVERT ]</button>
|
||||
<button id="exampleBtn" class="matrix-btn">
|
||||
[ LOAD_SIMULATION ]
|
||||
</button>
|
||||
<button
|
||||
id="compareBtn"
|
||||
class="matrix-btn font-bold bg-matrix-darkestGreen hover:bg-matrix-green hover:text-black shadow-[0_0_10px_rgba(0,255,65,0.2)]"
|
||||
>
|
||||
[ EXECUTE_DIFF ]
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 差异结果区域 -->
|
||||
<div class="matrix-panel flex flex-col">
|
||||
<div
|
||||
class="px-4 py-2 border-b border-matrix-darkGreen bg-matrix-darkestGreen/50 flex items-center justify-between"
|
||||
>
|
||||
<span class="tracking-widest text-sm">> ANALYSIS_RESULT</span>
|
||||
<span id="diffStats" class="text-xs tracking-wider"></span>
|
||||
</div>
|
||||
<div id="diff-output" class="overflow-x-auto min-h-[150px]">
|
||||
<div
|
||||
class="p-12 text-center text-matrix-darkGreen tracking-widest animate-pulse"
|
||||
>
|
||||
WAITING FOR COMMAND...
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- 矩阵数字雨 Canvas 脚本 -->
|
||||
<script>
|
||||
const canvas = document.getElementById("matrix-canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
|
||||
const katakana =
|
||||
"アァカサタナハマヤャラワガザダバパイィキシチニヒミリヰギジヂビピウゥクスツヌフムユュルグズブヅプエェケセテネヘメレゲゼデベペオォコソトノホモヨョロゴゾドボポヴッン";
|
||||
const latin = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const nums = "0123456789";
|
||||
const alphabet = katakana + latin + nums;
|
||||
|
||||
const fontSize = 16;
|
||||
const columns = canvas.width / fontSize;
|
||||
|
||||
const rainDrops = [];
|
||||
for (let x = 0; x < columns; x++) {
|
||||
rainDrops[x] = 1;
|
||||
}
|
||||
|
||||
const draw = () => {
|
||||
ctx.fillStyle = "rgba(0, 0, 0, 0.05)";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.fillStyle = "#0F0";
|
||||
ctx.font = fontSize + "px monospace";
|
||||
|
||||
for (let i = 0; i < rainDrops.length; i++) {
|
||||
const text = alphabet.charAt(
|
||||
Math.floor(Math.random() * alphabet.length),
|
||||
);
|
||||
ctx.fillText(text, i * fontSize, rainDrops[i] * fontSize);
|
||||
|
||||
if (
|
||||
rainDrops[i] * fontSize > canvas.height &&
|
||||
Math.random() > 0.975
|
||||
) {
|
||||
rainDrops[i] = 0;
|
||||
}
|
||||
rainDrops[i]++;
|
||||
}
|
||||
};
|
||||
|
||||
setInterval(draw, 30);
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
});
|
||||
</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(
|
||||
"SOURCE",
|
||||
"TARGET",
|
||||
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="p-12 text-center text-matrix-red tracking-widest">FATAL_ERROR: ${e.message}</div>`;
|
||||
diffStats.textContent = "ERR";
|
||||
return;
|
||||
}
|
||||
|
||||
const configuration = {
|
||||
drawFileList: false,
|
||||
matching: "lines",
|
||||
outputFormat: currentView,
|
||||
highlight: getHighlightConfig(),
|
||||
renderNothingWhenEmpty: false,
|
||||
};
|
||||
|
||||
let diffHtml = "";
|
||||
try {
|
||||
diffHtml = Diff2Html.html(diffString, configuration);
|
||||
} catch (e) {
|
||||
diffOutput.innerHTML = `<div class="p-12 text-center text-matrix-red tracking-widest">RENDER_FAILURE</div>`;
|
||||
diffStats.textContent = "ERR";
|
||||
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-matrix-darkGreen'>MATCH_FOUND // NO_ANOMALIES</span>";
|
||||
} else {
|
||||
diffStats.innerHTML = `<span class='text-matrix-green'>+${addedLines} INJECTED</span> <span class='text-matrix-darkGreen mx-2'>|</span> <span class='text-matrix-red'>-${deletedLines} PURGED</span>`;
|
||||
}
|
||||
|
||||
if (!diffHtml.trim() || diffOutput.innerText.trim() === "") {
|
||||
diffOutput.innerHTML =
|
||||
'<div class="p-12 text-center text-matrix-darkGreen tracking-widest">DATA_STREAMS_ARE_IDENTICAL.</div>';
|
||||
diffStats.innerHTML =
|
||||
"<span class='text-matrix-darkGreen'>SYNCED</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 enterMatrix() {\n console.log("Follow the white rabbit.");\n return "Ignorance is bliss.";\n}`;
|
||||
rightTextarea.value = `function enterMatrix() {\n console.log("There is no spoon.");\n decodeConstruct();\n return "I know Kung Fu.";\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) => {
|
||||
setActiveView(e.currentTarget.getAttribute("data-view"));
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
renderDiff();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("DOMContentLoaded", renderDiff);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
513
notion/index.html
Normal file
513
notion/index.html
Normal file
@@ -0,0 +1,513 @@
|
||||
<!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>Diff Checker - Notion Style</title>
|
||||
|
||||
<!-- Highlight.js 主题 (GitHub 风格最接近 Notion 的代码块) -->
|
||||
<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"
|
||||
/>
|
||||
|
||||
<!-- 引入 Inter 字体 (Notion 默认英文字体) -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- 引入 Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- 配置 Tailwind -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
notion: {
|
||||
text: "#37352f" /* Notion 默认文字黑 */,
|
||||
gray: "#787774" /* Notion 默认次要文字灰 */,
|
||||
lightGray: "#9a9a97" /* 更浅的灰 */,
|
||||
bg: "#ffffff" /* 纯白背景 */,
|
||||
hover: "#efefed" /* 悬浮浅灰背景 */,
|
||||
codeBg: "#f7f7f5" /* 代码块背景灰 */,
|
||||
border: "#e9e9e7" /* 极细边框灰 */,
|
||||
blue: "#2383e2" /* Notion 链接/按钮蓝 */,
|
||||
diffAdd: "#edf3ec" /* 低饱和度绿 */,
|
||||
diffDel: "#fbe4e4" /* 低饱和度红 */,
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [
|
||||
'"Inter"',
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
'"Segoe UI"',
|
||||
"sans-serif",
|
||||
],
|
||||
mono: [
|
||||
'"SFMono-Regular"',
|
||||
"Menlo",
|
||||
"Monaco",
|
||||
"Consolas",
|
||||
'"Liberation Mono"',
|
||||
'"Courier New"',
|
||||
"monospace",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style type="text/tailwindcss">
|
||||
@layer components {
|
||||
/* Notion 风格按钮 */
|
||||
.notion-btn {
|
||||
@apply px-3 py-1.5 rounded-[4px] text-sm font-medium transition-colors duration-200 flex items-center gap-1.5 cursor-pointer select-none;
|
||||
}
|
||||
.notion-btn-ghost {
|
||||
@apply text-notion-text bg-transparent hover:bg-notion-hover active:bg-[#e1e1df];
|
||||
}
|
||||
.notion-btn-primary {
|
||||
@apply text-white bg-[#2f2f2f] hover:bg-[#454545] active:bg-[#1a1a1a];
|
||||
}
|
||||
|
||||
/* 视图切换按钮 */
|
||||
.view-option {
|
||||
@apply px-2.5 py-1 rounded-[4px] text-sm text-notion-gray hover:bg-notion-hover transition-colors cursor-pointer select-none;
|
||||
}
|
||||
.view-option.active {
|
||||
@apply text-notion-text font-medium bg-notion-hover;
|
||||
}
|
||||
|
||||
/* Notion 代码块输入框 */
|
||||
.notion-input-wrapper {
|
||||
@apply bg-notion-codeBg rounded-[4px] border border-transparent focus-within:border-notion-border transition-colors flex flex-col;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
深度定制 Diff2Html 样式 (Notion 极简风)
|
||||
========================================== */
|
||||
.d2h-wrapper {
|
||||
font-family:
|
||||
"SFMono-Regular", Menlo, Monaco, Consolas, monospace !important;
|
||||
color: #37352f !important;
|
||||
}
|
||||
.d2h-file-header {
|
||||
display: none !important;
|
||||
}
|
||||
.d2h-file-wrapper {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.d2h-code-line-ctn {
|
||||
@apply font-mono text-[13px] !important;
|
||||
color: #37352f !important;
|
||||
}
|
||||
|
||||
/* 移除所有表格边框 */
|
||||
tbody,
|
||||
td,
|
||||
.d2h-code-linenumber,
|
||||
.d2h-info,
|
||||
.d2h-emptyplaceholder {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* 差异行背景色 (Notion 低饱和度色彩) */
|
||||
.d2h-ins {
|
||||
background-color: #edf3ec !important;
|
||||
}
|
||||
.d2h-del {
|
||||
background-color: #fbe4e4 !important;
|
||||
}
|
||||
|
||||
/* 行号区域极简处理 */
|
||||
.d2h-code-linenumber {
|
||||
background-color: #f7f7f5 !important;
|
||||
color: rgba(55, 53, 47, 0.4) !important;
|
||||
padding-right: 12px !important;
|
||||
}
|
||||
.d2h-info {
|
||||
background-color: #ffffff !important;
|
||||
color: rgba(55, 53, 47, 0.4) !important;
|
||||
}
|
||||
.d2h-emptyplaceholder {
|
||||
background-color: #f7f7f5 !important;
|
||||
}
|
||||
|
||||
/* 滚动条极简处理 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #d3d1cb;
|
||||
border-radius: 4px;
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #aeaca6;
|
||||
}
|
||||
|
||||
/* 选中文本颜色 */
|
||||
::selection {
|
||||
background: rgba(35, 131, 226, 0.28);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body
|
||||
class="bg-notion-bg text-notion-text font-sans min-h-screen flex flex-col items-center selection:bg-[#cce2ff]"
|
||||
>
|
||||
<!-- 页面容器 (限制最大宽度,类似 Notion 页面) -->
|
||||
<div class="max-w-[1100px] w-full px-6 py-12 md:py-20 flex flex-col gap-8">
|
||||
<!-- 标题区 (大字号,带 Emoji) -->
|
||||
<header class="flex flex-col gap-2">
|
||||
<h1 class="text-4xl font-bold tracking-tight flex items-center gap-3">
|
||||
<span class="text-4xl">⚖️</span> Diff Checker
|
||||
</h1>
|
||||
<p class="text-notion-gray text-base mt-2">
|
||||
Paste your code below to compare changes. Supports syntax
|
||||
highlighting.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- 双栏输入区 -->
|
||||
<div class="flex flex-col lg:flex-row gap-6">
|
||||
<!-- 左侧面板 -->
|
||||
<div class="flex-1 notion-input-wrapper">
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-2 border-b border-notion-border/50"
|
||||
>
|
||||
<label class="text-sm text-notion-gray font-medium">Original</label>
|
||||
<button
|
||||
id="clearLeftBtn"
|
||||
class="text-xs text-notion-lightGray hover:text-notion-text transition-colors"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="leftTextarea"
|
||||
class="w-full flex-1 min-h-[260px] h-[300px] p-4 bg-transparent font-mono text-[13px] leading-relaxed resize-y outline-none placeholder:text-notion-lightGray"
|
||||
spellcheck="false"
|
||||
>
|
||||
function hello() {
|
||||
console.log("Hello World");
|
||||
return "Hi";
|
||||
}</textarea
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 右侧面板 -->
|
||||
<div class="flex-1 notion-input-wrapper">
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-2 border-b border-notion-border/50"
|
||||
>
|
||||
<label class="text-sm text-notion-gray font-medium">Modified</label>
|
||||
<button
|
||||
id="clearRightBtn"
|
||||
class="text-xs text-notion-lightGray hover:text-notion-text transition-colors"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="rightTextarea"
|
||||
class="w-full flex-1 min-h-[260px] h-[300px] p-4 bg-transparent font-mono text-[13px] leading-relaxed resize-y outline-none placeholder:text-notion-lightGray"
|
||||
spellcheck="false"
|
||||
>
|
||||
function hello() {
|
||||
console.log("Hello, Diff Checker!");
|
||||
return "Hey there";
|
||||
}</textarea
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 (极简分割线上下) -->
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-between gap-4 py-2 border-y border-notion-border"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<!-- 语言选择 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-notion-gray">Language</span>
|
||||
<select
|
||||
id="languageSelect"
|
||||
class="bg-transparent hover:bg-notion-hover px-2 py-1 rounded-[4px] text-sm text-notion-text outline-none cursor-pointer transition-colors appearance-none"
|
||||
>
|
||||
<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">Plain Text</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="w-[1px] h-4 bg-notion-border hidden md:block"></div>
|
||||
|
||||
<!-- 视图切换 -->
|
||||
<div class="flex items-center gap-1" id="viewToggle">
|
||||
<button class="view-option active" data-view="side-by-side">
|
||||
Split view
|
||||
</button>
|
||||
<button class="view-option" data-view="line-by-line">
|
||||
Unified view
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button id="swapBtn" class="notion-btn notion-btn-ghost">
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"
|
||||
></path>
|
||||
</svg>
|
||||
Swap
|
||||
</button>
|
||||
<button id="exampleBtn" class="notion-btn notion-btn-ghost">
|
||||
Demo
|
||||
</button>
|
||||
<button id="compareBtn" class="notion-btn notion-btn-primary ml-2">
|
||||
Compare
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 差异结果区域 -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between py-1">
|
||||
<h2 class="text-lg font-semibold">Result</h2>
|
||||
<span id="diffStats" class="text-sm text-notion-gray"></span>
|
||||
</div>
|
||||
|
||||
<div class="notion-input-wrapper overflow-hidden">
|
||||
<div id="diff-output" class="overflow-x-auto min-h-[100px]">
|
||||
<div class="p-8 text-center text-notion-lightGray text-sm">
|
||||
Click "Compare" to see the differences.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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",
|
||||
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="p-8 text-center text-[#eb5757] text-sm">Error: ${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) {
|
||||
diffOutput.innerHTML = `<div class="p-8 text-center text-[#eb5757] text-sm">Render Failed</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 = "No changes";
|
||||
} else {
|
||||
// Notion 风格的极简统计文本
|
||||
diffStats.innerHTML = `<span style="color:#448361">${addedLines} additions</span>, <span style="color:#e03e3e">${deletedLines} deletions</span>`;
|
||||
}
|
||||
|
||||
if (!diffHtml.trim() || diffOutput.innerText.trim() === "") {
|
||||
diffOutput.innerHTML =
|
||||
'<div class="p-8 text-center text-notion-lightGray text-sm">Files are identical.</div>';
|
||||
diffStats.innerHTML = "Identical";
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
setActiveView(e.currentTarget.getAttribute("data-view"));
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
renderDiff();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("DOMContentLoaded", renderDiff);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
617
synthwave/index.html
Normal file
617
synthwave/index.html
Normal file
@@ -0,0 +1,617 @@
|
||||
<!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>V A P O R _ D I F F</title>
|
||||
|
||||
<!-- Highlight.js 主题 (Shades of Purple - 完美契合蒸汽波) -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/shades-of-purple.min.css"
|
||||
/>
|
||||
<!-- Diff2Html 核心样式 -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/diff2html@3.4.47/bundles/css/diff2html.min.css"
|
||||
/>
|
||||
|
||||
<!-- 引入蒸汽波风格字体 -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Righteous&family=
|
||||
|
||||
@message_part::reasoning-1_2
|
||||
|
||||
Space+Mono:ital,wght@0,400;0,700;1,400&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- 引入 Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- 配置 Tailwind -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
vapor: {
|
||||
bg: "#120458" /* 深邃夜空蓝 */,
|
||||
dark: "#2a0a4a" /* 暗紫色面板底色 */,
|
||||
pink: "#ff71ce" /* 迈阿密粉 */,
|
||||
cyan: "#01cdfe" /* 霓虹青 */,
|
||||
purple: "#b967ff" /* 亮紫色 */,
|
||||
yellow: "#fffb96" /* 落日黄 */,
|
||||
panel: "rgba(42, 10, 74, 0.75)" /* 半透明面板 */,
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
vapor: ['"Righteous"', "cursive"],
|
||||
mono: ['"Space Mono"', "monospace"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style type="text/tailwindcss">
|
||||
@layer components {
|
||||
/* 蒸汽波半透明发光面板 */
|
||||
.vapor-panel {
|
||||
@apply bg-vapor-panel backdrop-blur-md border-2 border-vapor-purple shadow-[0_0_20px_rgba(185,103,255,0.3)] relative overflow-hidden;
|
||||
}
|
||||
|
||||
/* 蒸汽波输入框 */
|
||||
.vapor-input {
|
||||
@apply bg-transparent text-vapor-cyan border-none outline-none resize-y placeholder:text-vapor-cyan/40 focus:shadow-[inset_0_0_30px_rgba(1,205,254,0.15)] transition-all;
|
||||
text-shadow: 0 0 4px rgba(1, 205, 254, 0.4);
|
||||
}
|
||||
|
||||
/* 蒸汽波霓虹按钮 */
|
||||
.vapor-btn {
|
||||
@apply bg-transparent border-2 px-5 py-2 font-vapor tracking-widest uppercase transition-all duration-300 cursor-pointer;
|
||||
}
|
||||
.vapor-btn-cyan {
|
||||
@apply border-vapor-cyan text-vapor-cyan hover:bg-vapor-cyan hover:text-vapor-bg shadow-[0_0_10px_rgba(1,205,254,0.5)] hover:shadow-[0_0_25px_rgba(1,205,254,0.8)];
|
||||
}
|
||||
.vapor-btn-pink {
|
||||
@apply border-vapor-pink text-vapor-pink hover:bg-vapor-pink hover:text-vapor-bg shadow-[0_0_10px_rgba(255,113,206,0.5)] hover:shadow-[0_0_25px_rgba(255,113,206,0.8)];
|
||||
}
|
||||
|
||||
/* 视图切换按钮 */
|
||||
.view-option {
|
||||
@apply px-3 py-1 font-vapor text-sm tracking-wider text-vapor-purple hover:text-vapor-pink transition-colors;
|
||||
}
|
||||
.view-option.active {
|
||||
@apply text-vapor-bg bg-vapor-purple shadow-[0_0_15px_#b967ff];
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
纯 CSS 绘制:80年代落日与 3D 透视网格
|
||||
========================================== */
|
||||
body {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
#0f023b 0%,
|
||||
#2a0a4a 60%,
|
||||
#ff71ce 100%
|
||||
);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
/* 霓虹落日 */
|
||||
.vapor-sun {
|
||||
position: fixed;
|
||||
top: 15%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: linear-gradient(to bottom, #fffb96 0%, #ff71ce 100%);
|
||||
border-radius: 50%;
|
||||
z-index: -2;
|
||||
box-shadow:
|
||||
0 0 80px #ff71ce,
|
||||
0 0 150px #fffb96;
|
||||
/* 百叶窗切割效果 */
|
||||
-webkit-mask-image: repeating-linear-gradient(
|
||||
to bottom,
|
||||
black 0%,
|
||||
black 6%,
|
||||
transparent 6%,
|
||||
transparent 8%
|
||||
);
|
||||
mask-image: repeating-linear-gradient(
|
||||
to bottom,
|
||||
black 0%,
|
||||
black 6%,
|
||||
transparent 6%,
|
||||
transparent 8%
|
||||
);
|
||||
}
|
||||
|
||||
/* 3D 赛博网格地板 */
|
||||
.vapor-grid {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 40vh;
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to right,
|
||||
rgba(1, 205, 254, 0.6) 2px,
|
||||
transparent 2px
|
||||
),
|
||||
linear-gradient(to top, rgba(1, 205, 254, 0.6) 2px, transparent 2px);
|
||||
background-size: 50px 50px;
|
||||
transform: perspective(600px) rotateX(70deg);
|
||||
z-index: -1;
|
||||
animation: gridMove 3s linear infinite;
|
||||
/* 让网格向远处渐隐 */
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
black 100%
|
||||
);
|
||||
}
|
||||
@keyframes gridMove {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 0 50px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
覆盖 Diff2Html 样式 (蒸汽波化)
|
||||
========================================== */
|
||||
.d2h-wrapper {
|
||||
font-family: "Space Mono", monospace !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.d2h-file-header {
|
||||
display: none !important;
|
||||
}
|
||||
.d2h-file-wrapper {
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.d2h-code-line-ctn {
|
||||
@apply font-mono text-[14px] !important;
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
/* 差异行背景色 (青色代表新增,粉色代表删除) */
|
||||
.d2h-ins {
|
||||
background-color: rgba(1, 205, 254, 0.2) !important;
|
||||
border-color: #01cdfe !important;
|
||||
color: #01cdfe !important;
|
||||
text-shadow: 0 0 5px rgba(1, 205, 254, 0.5);
|
||||
}
|
||||
.d2h-del {
|
||||
background-color: rgba(255, 113, 206, 0.2) !important;
|
||||
border-color: #ff71ce !important;
|
||||
color: #ff71ce !important;
|
||||
text-shadow: 0 0 5px rgba(255, 113, 206, 0.5);
|
||||
}
|
||||
|
||||
/* 行号区域 */
|
||||
.d2h-code-linenumber {
|
||||
background-color: rgba(18, 4, 88, 0.8) !important;
|
||||
border-color: #b967ff !important;
|
||||
color: #b967ff !important;
|
||||
}
|
||||
.d2h-info {
|
||||
background-color: rgba(42, 10, 74, 0.9) !important;
|
||||
color: #fffb96 !important;
|
||||
border-color: #b967ff !important;
|
||||
}
|
||||
.d2h-emptyplaceholder {
|
||||
background-color: rgba(18, 4, 88, 0.5) !important;
|
||||
border-color: #b967ff !important;
|
||||
}
|
||||
tbody {
|
||||
border-color: #b967ff !important;
|
||||
}
|
||||
td {
|
||||
border-color: #b967ff !important;
|
||||
}
|
||||
|
||||
/* 滚动条霓虹化 */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #120458;
|
||||
border-left: 1px solid #b967ff;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #ff71ce;
|
||||
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #01cdfe;
|
||||
}
|
||||
|
||||
/* 选中文本颜色 */
|
||||
::selection {
|
||||
background: #ff71ce;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body
|
||||
class="font-mono p-4 md:p-8 min-h-screen flex flex-col items-center text-white relative overflow-x-hidden"
|
||||
>
|
||||
<!-- 纯 CSS 背景动画 -->
|
||||
<div class="vapor-sun"></div>
|
||||
<div class="vapor-grid"></div>
|
||||
|
||||
<!-- 主窗口 -->
|
||||
<div class="max-w-[1600px] w-full flex flex-col gap-6 z-10">
|
||||
<!-- 标题区 -->
|
||||
<header class="text-center mb-4">
|
||||
<h1
|
||||
class="font-vapor text-5xl md:text-7xl tracking-widest text-transparent bg-clip-text bg-gradient-to-b from-vapor-cyan to-vapor-purple drop-shadow-[0_0_15px_rgba(1,205,254,0.8)]"
|
||||
style="-webkit-text-stroke: 1px #fff"
|
||||
>
|
||||
V A P O R _ D I F F
|
||||
</h1>
|
||||
<p
|
||||
class="font-vapor text-vapor-pink mt-2 tracking-[0.5em] text-sm drop-shadow-[0_0_8px_#ff71ce]"
|
||||
>
|
||||
A E S T H E T I C S // 1 9 8 4
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- 双栏输入区 -->
|
||||
<div class="flex flex-col lg:flex-row gap-6">
|
||||
<!-- 左侧面板 -->
|
||||
<div class="flex-1 flex flex-col vapor-panel rounded-xl">
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-3 border-b-2 border-vapor-purple bg-vapor-bg/50"
|
||||
>
|
||||
<label
|
||||
class="font-vapor text-vapor-cyan tracking-widest flex items-center gap-2 drop-shadow-[0_0_5px_#01cdfe]"
|
||||
>
|
||||
[ CASSETTE_A ]
|
||||
</label>
|
||||
<button
|
||||
id="clearLeftBtn"
|
||||
class="font-vapor text-xs text-vapor-purple hover:text-vapor-pink transition-colors tracking-widest"
|
||||
>
|
||||
EJECT
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="leftTextarea"
|
||||
class="vapor-input w-full flex-1 min-h-[260px] h-[300px] p-5 text-[15px] leading-relaxed"
|
||||
spellcheck="false"
|
||||
>
|
||||
function playSynthwave() {
|
||||
console.log("Loading VHS tape...");
|
||||
return "Nostalgia";
|
||||
}</textarea
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 右侧面板 -->
|
||||
<div class="flex-1 flex flex-col vapor-panel rounded-xl">
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-3 border-b-2 border-vapor-purple bg-vapor-bg/50"
|
||||
>
|
||||
<label
|
||||
class="font-vapor text-vapor-pink tracking-widest flex items-center gap-2 drop-shadow-[0_0_5px_#ff71ce]"
|
||||
>
|
||||
[ CASSETTE_B ]
|
||||
</label>
|
||||
<button
|
||||
id="clearRightBtn"
|
||||
class="font-vapor text-xs text-vapor-purple hover:text-vapor-cyan transition-colors tracking-widest"
|
||||
>
|
||||
EJECT
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="rightTextarea"
|
||||
class="vapor-input w-full flex-1 min-h-[260px] h-[300px] p-5 text-[15px] leading-relaxed"
|
||||
spellcheck="false"
|
||||
>
|
||||
function playSynthwave() {
|
||||
console.log("Loading LaserDisc...");
|
||||
enableNeonGlow();
|
||||
return "A E S T H E T I C S";
|
||||
}</textarea
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div
|
||||
class="vapor-panel rounded-xl p-4 flex flex-wrap items-center justify-between gap-4"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-6">
|
||||
<!-- 语言选择 -->
|
||||
<div class="flex items-center gap-3">
|
||||
<label
|
||||
class="font-vapor text-sm tracking-widest text-vapor-yellow drop-shadow-[0_0_5px_#fffb96]"
|
||||
>FORMAT:</label
|
||||
>
|
||||
<select
|
||||
id="languageSelect"
|
||||
class="bg-vapor-bg border-2 border-vapor-purple text-vapor-cyan px-3 py-1.5 font-vapor text-sm outline-none cursor-pointer hover:border-vapor-cyan focus:border-vapor-cyan focus:shadow-[0_0_10px_#01cdfe] transition-all appearance-none"
|
||||
>
|
||||
<option value="javascript" selected>JS_WAVE</option>
|
||||
<option value="typescript">TS_WAVE</option>
|
||||
<option value="html">HTML_84</option>
|
||||
<option value="css">CSS_NEON</option>
|
||||
<option value="json">JSON_DATA</option>
|
||||
<option value="python">PY_SYNTH</option>
|
||||
<option value="plaintext">RAW_TEXT</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 视图切换 -->
|
||||
<div
|
||||
class="flex items-center border-2 border-vapor-purple p-1 rounded-md bg-vapor-bg/50"
|
||||
id="viewToggle"
|
||||
>
|
||||
<button
|
||||
class="view-option active rounded-sm"
|
||||
data-view="side-by-side"
|
||||
>
|
||||
STEREO
|
||||
</button>
|
||||
<button class="view-option rounded-sm" data-view="line-by-line">
|
||||
MONO
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
<button id="swapBtn" class="vapor-btn vapor-btn-cyan rounded-md">
|
||||
REMIX
|
||||
</button>
|
||||
<button id="exampleBtn" class="vapor-btn vapor-btn-cyan rounded-md">
|
||||
DEMO
|
||||
</button>
|
||||
<button
|
||||
id="compareBtn"
|
||||
class="vapor-btn vapor-btn-pink rounded-md bg-vapor-pink/10"
|
||||
>
|
||||
SYNC_TRACKS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 差异结果区域 -->
|
||||
<div class="vapor-panel rounded-xl flex flex-col">
|
||||
<div
|
||||
class="px-5 py-3 border-b-2 border-vapor-purple bg-vapor-bg/50 flex items-center justify-between"
|
||||
>
|
||||
<span
|
||||
class="font-vapor tracking-widest text-vapor-yellow drop-shadow-[0_0_5px_#fffb96]"
|
||||
>> PLAYBACK_RESULT</span
|
||||
>
|
||||
<span id="diffStats" class="font-vapor text-sm tracking-wider"></span>
|
||||
</div>
|
||||
<div
|
||||
id="diff-output"
|
||||
class="overflow-x-auto min-h-[150px] bg-vapor-bg/80"
|
||||
>
|
||||
<div
|
||||
class="p-12 text-center font-vapor text-vapor-purple tracking-widest text-xl animate-pulse"
|
||||
>
|
||||
PRESS [ SYNC_TRACKS ] TO PLAY...
|
||||
</div>
|
||||
</div>
|
||||
</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() {
|
||||
let val = languageSelect.value;
|
||||
// 映射回真实的语言名称
|
||||
if (val === "javascript") return "javascript";
|
||||
if (val === "typescript") return "typescript";
|
||||
if (val === "html") return "html";
|
||||
if (val === "css") return "css";
|
||||
if (val === "json") return "json";
|
||||
if (val === "python") return "python";
|
||||
return "plaintext";
|
||||
}
|
||||
|
||||
function getHighlightConfig() {
|
||||
const lang = getSelectedLanguage();
|
||||
if (lang === "plaintext") return false;
|
||||
return { enabled: true, language: lang };
|
||||
}
|
||||
|
||||
function generateUnifiedDiff(original, modified) {
|
||||
return Diff.createTwoFilesPatch(
|
||||
"CASSETTE_A",
|
||||
"CASSETTE_B",
|
||||
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="p-12 text-center font-vapor text-vapor-pink tracking-widest text-xl">GLITCH_ERROR: ${e.message}</div>`;
|
||||
diffStats.textContent = "ERR";
|
||||
return;
|
||||
}
|
||||
|
||||
const configuration = {
|
||||
drawFileList: false,
|
||||
matching: "lines",
|
||||
outputFormat: currentView,
|
||||
highlight: getHighlightConfig(),
|
||||
renderNothingWhenEmpty: false,
|
||||
};
|
||||
|
||||
let diffHtml = "";
|
||||
try {
|
||||
diffHtml = Diff2Html.html(diffString, configuration);
|
||||
} catch (e) {
|
||||
diffOutput.innerHTML = `<div class="p-12 text-center font-vapor text-vapor-pink tracking-widest text-xl">TAPE_JAMMED</div>`;
|
||||
diffStats.textContent = "ERR";
|
||||
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-vapor-cyan drop-shadow-[0_0_5px_#01cdfe]'>VIBES_SYNCED</span>";
|
||||
} else {
|
||||
diffStats.innerHTML = `<span class='text-vapor-cyan drop-shadow-[0_0_5px_#01cdfe]'>+${addedLines} UPLOADED</span> <span class='text-vapor-purple mx-2'>|</span> <span class='text-vapor-pink drop-shadow-[0_0_5px_#ff71ce]'>-${deletedLines} ERASED</span>`;
|
||||
}
|
||||
|
||||
if (!diffHtml.trim() || diffOutput.innerText.trim() === "") {
|
||||
diffOutput.innerHTML =
|
||||
'<div class="p-12 text-center font-vapor text-vapor-cyan tracking-widest text-xl">TRACKS_ARE_IDENTICAL</div>';
|
||||
diffStats.innerHTML = "<span class='text-vapor-cyan'>SYNCED</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 playSynthwave() {\n console.log("Loading VHS tape...");\n return "Nostalgia";\n}`;
|
||||
rightTextarea.value = `function playSynthwave() {\n console.log("Loading LaserDisc...");\n enableNeonGlow();\n return "A E S T H E T I C S";\n}`;
|
||||
// 恢复下拉框的显示值
|
||||
languageSelect.selectedIndex = 0;
|
||||
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) => {
|
||||
setActiveView(e.currentTarget.getAttribute("data-view"));
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
renderDiff();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("DOMContentLoaded", renderDiff);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user