Extract HTML template into separate views file

This commit is contained in:
2026-05-29 14:18:59 +08:00
parent f9e323f70a
commit cf2d5fc77b
15 changed files with 862 additions and 233 deletions
+125 -45
View File
@@ -3,6 +3,7 @@
const DATA = window.__WEBOOK__;
const navTree = DATA.nav || [];
const themes = Array.isArray(DATA.themes) ? DATA.themes : [];
const defaultFile = DATA.defaultFile;
const initialContent = DATA.initialContent;
@@ -11,6 +12,7 @@
const searchInput = document.getElementById('searchInput');
const btnToggleSidebar = document.getElementById('btnToggleSidebar');
const btnToggleTheme = document.getElementById('btnToggleTheme');
const themeSelect = document.getElementById('themeSelect');
const contentBody = document.getElementById('contentBody');
const emptyState = document.getElementById('emptyState');
const markdownContent = document.getElementById('markdownContent');
@@ -22,35 +24,88 @@
const loadingBar = document.getElementById('loadingBar');
const sidebar = document.getElementById('sidebar');
const mobileNavBtn = document.getElementById('mobileNavBtn');
const themeStylesheet = document.getElementById('themeStylesheet');
const themeHighlightStylesheet = document.getElementById('themeHighlightStylesheet');
let currentFile = null;
const fallbackTheme = themes[0] || {
id: 'default',
label: 'Default',
defaultMode: 'light',
stylesheet: '/_webook/themes/default/theme.css',
highlight: {
light: '/_webook/themes/default/highlight-light.css',
dark: '/_webook/themes/default/highlight-dark.css'
}
};
// === Theme ===
function getTheme() {
return localStorage.getItem('webook-theme') || 'light';
function getThemeConfig(themeId) {
return themes.find((item) => item.id === themeId) || fallbackTheme;
}
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('webook-theme', theme);
btnToggleTheme.textContent = theme === 'dark' ? '☀︎' : '☾';
// Swap highlight theme
const hlLink = document.querySelector('.theme-highlight');
if (hlLink) {
hlLink.href = theme === 'dark'
? '/_webook/highlight-dark.css'
: '/_webook/highlight-light.css';
function getStoredThemeId() {
const stored = localStorage.getItem('webook-theme-id');
if (stored && themes.some((item) => item.id === stored)) {
return stored;
}
return DATA.initialThemeId || fallbackTheme.id;
}
function toggleTheme() {
const current = getTheme();
setTheme(current === 'light' ? 'dark' : 'light');
function getStoredColorMode(themeId) {
const stored = localStorage.getItem('webook-color-mode');
if (stored === 'light' || stored === 'dark') {
return stored;
}
const legacy = localStorage.getItem('webook-theme');
if (legacy === 'light' || legacy === 'dark') {
return legacy;
}
const theme = getThemeConfig(themeId);
return theme.defaultMode === 'dark' ? 'dark' : 'light';
}
// Initialize theme
setTheme(getTheme());
function renderThemeOptions() {
if (!themeSelect) return;
themeSelect.innerHTML = themes.map((item) => (
`<option value="${escapeHtml(item.id)}">${escapeHtml(item.label)}</option>`
)).join('');
}
function applyTheme(themeId, colorMode) {
const theme = getThemeConfig(themeId);
const mode = colorMode === 'dark' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme-id', theme.id);
document.documentElement.setAttribute('data-color-mode', mode);
localStorage.setItem('webook-theme-id', theme.id);
localStorage.setItem('webook-color-mode', mode);
localStorage.setItem('webook-theme', mode);
if (themeStylesheet) {
themeStylesheet.href = theme.stylesheet;
}
if (themeHighlightStylesheet) {
themeHighlightStylesheet.href = mode === 'dark' ? theme.highlight.dark : theme.highlight.light;
}
if (themeSelect) {
themeSelect.value = theme.id;
}
btnToggleTheme.textContent = mode === 'dark' ? '☀︎' : '☾';
btnToggleTheme.title = mode === 'dark' ? '切换到浅色模式' : '切换到深色模式';
}
function toggleColorMode() {
const currentThemeId = getStoredThemeId();
const currentMode = getStoredColorMode(currentThemeId);
applyTheme(currentThemeId, currentMode === 'dark' ? 'light' : 'dark');
}
renderThemeOptions();
applyTheme(getStoredThemeId(), getStoredColorMode(getStoredThemeId()));
// === Sidebar Toggle ===
function getSidebarVisible() {
@@ -174,6 +229,10 @@
if (fileItem) fileItem.classList.add('active');
}
function findNavFileItem(filePath) {
return sidebarNav.querySelector(`[data-file-path="${CSS.escape(filePath)}"]`);
}
// === Search ===
searchInput.addEventListener('input', function () {
const query = this.value.trim().toLowerCase();
@@ -270,16 +329,7 @@
articleBody.innerHTML = data.html;
buildTOC();
// Update meta
const created = formatDate(data.meta.created);
const modified = formatDate(data.meta.modified);
const wordCount = data.meta.wordCount;
articleMeta.innerHTML = `
<span class="article-meta-item">📅 创建: ${created}</span>
<span class="article-meta-item">✏️ 修改: ${modified}</span>
<span class="article-meta-item">📝 字数: ${wordCount.toLocaleString()}</span>
`;
renderArticleMeta(data.meta);
// Scroll to top
contentBody.scrollTop = 0;
@@ -329,6 +379,17 @@
// For now, server-side rendering handles highlighting.
}
function renderArticleMeta(meta) {
const created = formatDate(meta.created);
const modified = formatDate(meta.modified);
const wordCount = meta.wordCount;
articleMeta.innerHTML = `
<span class="article-meta-item">创建时间: ${created}</span>
<span class="article-meta-item">修改时间: ${modified}</span>
<span class="article-meta-item">字数统计: ${wordCount.toLocaleString()}</span>
`;
}
function slugifyHeading(text, index) {
return 'toc-' + text
.toLowerCase()
@@ -361,13 +422,20 @@
});
tocNav.innerHTML = items.map((item) => `
<a class="toc-link toc-level-${item.level}" href="#${escapeHtml(item.id)}" data-target="${escapeHtml(item.id)}">
<button class="toc-link toc-level-${item.level}" type="button" data-target="${escapeHtml(item.id)}">
<span class="toc-bullet"></span>
<span class="toc-text">${escapeHtml(item.text)}</span>
</a>
</button>
`).join('');
}
function scrollToHeading(targetId) {
if (!targetId) return;
const target = articleBody.querySelector(`#${CSS.escape(targetId)}`);
if (!target) return;
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// === Initial Load ===
if (initialContent) {
emptyState.style.display = 'none';
@@ -376,14 +444,7 @@
articleBody.innerHTML = initialContent.html;
buildTOC();
const created = formatDate(initialContent.meta.created);
const modified = formatDate(initialContent.meta.modified);
const wordCount = initialContent.meta.wordCount;
articleMeta.innerHTML = `
<span class="article-meta-item">📅 创建: ${created}</span>
<span class="article-meta-item">✏️ 修改: ${modified}</span>
<span class="article-meta-item">📝 字数: ${wordCount.toLocaleString()}</span>
`;
renderArticleMeta(initialContent.meta);
currentFile = defaultFile;
if (defaultFile && history.replaceState) {
@@ -394,22 +455,41 @@
// === Hash-based navigation ===
function handleHashChange() {
const hash = decodeURIComponent(window.location.hash.slice(1));
if (hash && hash !== currentFile) {
if (!hash || hash === currentFile) {
return;
}
const fileItem = findNavFileItem(hash);
if (fileItem) {
loadFile(hash);
// Find and activate the nav item
const fileItem = sidebarNav.querySelector(`[data-file-path="${CSS.escape(hash)}"]`);
if (fileItem) {
setActiveFile(fileItem);
}
setActiveFile(fileItem);
return;
}
if (hash.startsWith('toc-')) {
scrollToHeading(hash);
}
}
window.addEventListener('hashchange', handleHashChange);
// === Event Listeners ===
btnToggleTheme.addEventListener('click', toggleTheme);
btnToggleTheme.addEventListener('click', toggleColorMode);
btnToggleSidebar.addEventListener('click', toggleSidebar);
mobileNavBtn.addEventListener('click', showMobileSidebar);
if (themeSelect) {
themeSelect.addEventListener('change', function () {
applyTheme(this.value, getStoredColorMode(this.value));
});
}
if (tocNav) {
tocNav.addEventListener('click', function (e) {
const link = e.target.closest('[data-target]');
if (!link) return;
e.preventDefault();
scrollToHeading(link.getAttribute('data-target'));
});
}
// === Keyboard Shortcuts ===
document.addEventListener('keydown', function (e) {