feat(theme): add style switcher and centralize diff logic

Extract duplicated diff rendering logic into shared/diff-page.js
Implement theme switcher component across all templates
This commit is contained in:
2026-04-08 14:39:39 +08:00
parent 90792b276e
commit e9fcd411a7
9 changed files with 642 additions and 1271 deletions

View File

@@ -83,6 +83,27 @@
.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)];
}
.style-switcher {
@apply flex items-center gap-3;
}
.style-switcher-label {
@apply text-xs text-gray-500 tracking-widest;
}
.style-switcher-select {
background: #050505;
border: 1px solid #333;
color: #fcee0a;
padding: 0.25rem 0.75rem;
font-family: "Fira Code", monospace;
font-size: 0.75rem;
letter-spacing: 0.12em;
outline: none;
}
.style-switcher-select:hover,
.style-switcher-select:focus {
border-color: #00f0ff;
box-shadow: 0 0 12px rgba(0, 240, 255, 0.15);
}
/* 面板边框发光效果 */
.cyber-panel {
@@ -313,6 +334,8 @@ function initCyberware() {
UNIFIED
</button>
</div>
<div data-style-switcher></div>
</div>
<!-- 操作按钮 -->
@@ -351,184 +374,40 @@ function initCyberware() {
<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 src="../shared/diff-page.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;
window.initDiffPage({
themeId: "cyberpunk",
currentThemePath: "cyberpunk/index.html",
switcherLabel: "SKIN_",
switcherAriaLabel: "Switch cyberpunk skin",
fileLabels: {
left: "ORIGINAL",
right: "MODIFIED",
},
example: {
left: `function initCyberware() {\n console.log("Booting optics...");\n return "Ready";\n}`,
right: `function initCyberware() {\n console.log("Booting Kiroshi optics v3.0...");\n connectToNetwork();\n return "Online";\n}`,
language: "javascript",
},
messages: {
generateError: (error) =>
`<div class="p-10 text-center font-mono text-cyber-pink tracking-widest">SYS_ERR: ${error.message}</div>`,
renderError: () =>
'<div class="p-10 text-center font-mono text-cyber-pink tracking-widest">RENDER_FAILED</div>',
blankResult:
'<div class="p-10 text-center font-mono text-gray-500 tracking-widest">DATA_MATCH // NO_DIFF_FOUND</div>',
blankStats: "<span class='text-gray-500'>SYNCED</span>",
generateErrorStats: "ERR",
renderErrorStats: "ERR",
},
formatStats: ({ added, deleted, identical }) => {
if (identical) {
return "<span class='text-gray-500'>SYNCED // NO_CHANGES</span>";
}
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);
})();
return `<span class='text-cyber-cyan'>+${added} INS</span> <span class='text-gray-600 mx-2'>|</span> <span class='text-cyber-pink'>-${deleted} DEL</span>`;
},
});
</script>
</body>
</html>