Files
SMANAGE/TempLate/Document/lib/script.js
T
2026-06-16 11:55:14 +08:00

622 lines
24 KiB
JavaScript

window.WebAssembly = undefined;
const url = new URL(window.location.href);
const pathParts = url.pathname.split('/');
const UUID = pathParts.pop() || pathParts.pop();
// DOM元素
const sidebar = document.getElementById('sidebar');
const sidebarToggle = document.getElementById('sidebar-toggle');
const overlay = document.getElementById('overlay');
const sidebarSearch = document.getElementById('sidebar-search');
const themeToggle = document.getElementById('theme-toggle');
const articleContainer = document.getElementById('article-container');
const sidebarNav = document.getElementById('sidebar-nav');
const globalSearch = document.getElementById('global-search');
const searchButton = document.getElementById('search-button');
const headerTitle = document.getElementById('header-title');
// 初始化Marked.js
if (window.marked) marked.setOptions({
highlight: function (code, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value;
}
return hljs.highlightAuto(code).value;
},
breaks: true,
});
// 渲染侧边栏导航
function renderSidebar() {
sidebarNav.innerHTML = '';
tableOfContents.chapters.forEach(chapter => {
const chapterEl = document.createElement('div');
chapterEl.className = 'mb-1';
const chapterHeader = document.createElement('div');
chapterHeader.className = 'flex items-center justify-between p-2 rounded-lg hover:bg-gray-100 cursor-pointer transition-all duration-300';
chapterHeader.innerHTML = `
<span class="font-medium flex items-center">
<i class="fa fa-book mr-2 text-primary"></i>
${chapter.title}
</span>
<i class="fa fa-chevron-down text-xs transition-transform duration-300"></i>
`;
const chapterContent = document.createElement('div');
chapterContent.className = 'pl-4 mt-1 space-y-1 overflow-hidden max-h-0 transition-all duration-300';
// 添加章节链接
const chapterLink = document.createElement('a');
chapterLink.href = `#${chapter.id}`;
chapterLink.className = 'block p-2 rounded-lg hover:bg-gray-100 text-sm transition-all duration-300';
chapterLink.textContent = chapter.title;
chapterLink.addEventListener('click', (e) => {
e.preventDefault();
loadArticleAsync(chapter.id);
closeSidebar();
highlightActiveLink(chapter.id);
});
chapterContent.appendChild(chapterLink);
// 添加子章节链接
if (chapter.sections && chapter.sections.length > 0) {
chapter.sections.forEach(section => {
const sectionLink = document.createElement('a');
sectionLink.href = `#${section.id}`;
sectionLink.className = 'block p-2 rounded-lg hover:bg-gray-100 text-sm transition-all duration-300';
sectionLink.innerHTML = `
<span class="ml-2">${section.title}</span>
`;
sectionLink.addEventListener('click', (e) => {
e.preventDefault();
loadArticleAsync(section.id);
closeSidebar();
highlightActiveLink(section.id);
});
chapterContent.appendChild(sectionLink);
});
}
chapterHeader.addEventListener('click', () => {
const isExpanded = chapterContent.classList.contains('expanded');
if (isExpanded) {
chapterContent.classList.remove('expanded');
chapterContent.style.maxHeight = '0';
chapterHeader.querySelector('.fa-chevron-down').style.transform = 'rotate(0deg)';
} else {
chapterContent.classList.add('expanded');
chapterContent.style.maxHeight = chapterContent.scrollHeight + 'px';
chapterHeader.querySelector('.fa-chevron-down').style.transform = 'rotate(180deg)';
}
});
chapterEl.appendChild(chapterHeader);
chapterEl.appendChild(chapterContent);
sidebarNav.appendChild(chapterEl);
});
}
// 异步加载文章内容
async function loadArticleAsync(id, password = null) {
// 显示现代化加载动画
showLoadingAnimation();
// 找到侧边栏中对应的链接并模拟点击
const link = document.querySelector(`#sidebar-nav a[href="#${id}"]`);
let apiUrl = `/API/GetDoc/${id}`;
if (password) {
apiUrl += `?Pwd=${password}`;
}
try {
// 使用fetch API进行异步请求
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const content = await response.json();
if (content.DocDecrypt === false) {
showPasswordPrompt(content.DocContent, id);
} else if (content) {
renderArticle(content);
} else {
showError("未找到文章内容");
}
} catch (error) {
console.error('Fetch error:', error);
showError("加载文章失败,请稍后再试");
}
}
// 显示加载动画
function showLoadingAnimation() {
articleContainer.innerHTML = `
<div class="flex flex-col items-center justify-center py-16 min-h-[400px]">
<div class="loading-spinner">
<div class="spinner"></div>
</div>
<p class="mt-4 text-gray-500 dark:text-gray-400 text-lg">正在加载文章内容...</p>
</div>
`;
}
// 显示错误信息
function showError(message) {
articleContainer.innerHTML = `
<div class="flex flex-col items-center justify-center py-16 min-h-[400px]">
<i class="fa fa-exclamation-triangle text-5xl text-gray-300 dark:text-gray-600 mb-4"></i>
<h3 class="text-xl font-medium mb-2">${message}</h3>
<p class="text-gray-600 dark:text-gray-400">请尝试刷新页面或联系管理员</p>
</div>
`;
}
function sanitizeInput(input) {
if (typeof input !== 'string') return '';
// 1. 移除 script 标签
let sanitized = input.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, '');
// 2. 移除所有事件处理器(包括 onerror)
sanitized = sanitized.replace(/\bon\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^"'>\s]+)/gi, '');
// 3. 移除 javascript: 和 data: 协议
sanitized = sanitized.replace(/(javascript|data|vbscript):/gi, '');
// 4. 编码特殊字符
sanitized = sanitized
.replace(/\bonerror\s*=\s*"[^"]*"/gi, '')
.replace(/\bonerror\s*=\s*'[^']*'/gi, '')
.replace(/\bonerror\s*=\s*[^"'>\s]+/gi, '');
return sanitized;
}
function sanitizeHTML(input) {
const parser = new DOMParser();
const doc = parser.parseFromString(input, 'text/html');
const elements = doc.body.getElementsByTagName('*');
for (let element of elements) {
const attributes = element.attributes;
for (let i = attributes.length - 1; i >= 0; i--) {
const attr = attributes[i];
if (attr.name.startsWith('on') ||
attr.name.match(/^on[a-z]+/i) ||
['src', 'href', 'xlink:href'].includes(attr.name.toLowerCase())) {
element.removeAttribute(attr.name);
}
}
if (['iframe', 'script', 'object', 'embed'].includes(element.tagName.toLowerCase())) {
element.remove();
}
}
return doc.body.innerHTML;
}
// 渲染文章内容
function renderArticle(content) {
// 解析Markdown并渲染
if (content.DocContent.substring(0, 5) === "<!DOC" && content.DocPermission) {
articleContainer.innerHTML = `<iframe onerror="return false;" id="IFcontent" class="iframe-content"></iframe>`;
const IFcontent = document.getElementById("IFcontent");
IFcontent.srcdoc = content.DocContent;
IFcontent.onload = function () {
IFcontent.style.height = IFcontent.contentWindow.document.body.scrollHeight + 100 + 'px';
IFcontent.contentDocument.body.style.color = document.documentElement.classList.contains('dark') ? '#FFFFFF' : '#000000';
}
} else if (content.DocPermission) {
articleContainer.innerHTML = marked.parse(content.DocContent);
} else {
articleContainer.innerHTML = marked.parse(sanitizeInput(content.DocContent));
}
// 渲染数学公式
document.querySelectorAll('pre code.language-math, pre code.language-katex, pre code.language-latex').forEach((block) => {
const parentPre = block.parentNode;
const displayMode = true;
const mathDiv = document.createElement('div');
mathDiv.className = 'math';
mathDiv.textContent = block.textContent;
katex.render(mathDiv.textContent, mathDiv, {
throwOnError: false,
displayMode
});
parentPre.parentNode.replaceChild(mathDiv, parentPre);
});
// 渲染流程图和序列图
document.querySelectorAll('pre code.language-flow, pre code.language-seq').forEach((block) => {
const parentPre = block.parentNode;
const diagram = document.createElement('div');
diagram.className = 'mermaid';
diagram.textContent = block.textContent;
parentPre.parentNode.replaceChild(diagram, parentPre);
});
if (window.mermaid) mermaid.init();
// 为普通代码块添加行号、标题、复制按钮和演示按钮
document.querySelectorAll('pre code').forEach((block) => {
const lang = block.getAttribute('class')?.replace('hljs', '').trim();
const supportedLanguages = ['javascript', 'html', 'plaintext']; // 定义支持高亮的语言
// 只对支持的语言进行高亮处理
if (supportedLanguages.includes(lang)) {
hljs.highlightElement(block);
}
// 跳过数学公式和图表代码块
if (['math', 'katex', 'latex', 'flow', 'seq'].includes(lang)) {
return;
}
// 添加行号
if (!block.classList.contains('nohighlight')) {
const lines = block.textContent.split('\n').length;
let lineNumbersHTML = '';
for (let i = 0; i < lines; i++) {
lineNumbersHTML += '<span></span>';
}
const lineNumbersWrapper = document.createElement('div');
lineNumbersWrapper.className = 'line-numbers';
lineNumbersWrapper.innerHTML = lineNumbersHTML;
block.className += ' line-numbers-rows';
block.parentNode.insertBefore(lineNumbersWrapper, block);
}
// 添加代码块标题
const codeTitle = document.createElement('div');
codeTitle.className = 'code-title';
codeTitle.textContent = lang || 'plaintext';
// 添加复制按钮
const copyButton = document.createElement('button');
copyButton.className = 'copy-btn';
copyButton.innerHTML = '<i class="fa fa-copy"></i>';
copyButton.addEventListener('click', () => {
const text = block.textContent;
navigator.clipboard.writeText(text).then(() => {
copyButton.innerHTML = '<i class="fa fa-check"></i>';
setTimeout(() => {
copyButton.innerHTML = '<i class="fa fa-copy"></i>';
}, 1000);
}).catch((err) => {
console.error('复制失败: ', err);
});
});
codeTitle.appendChild(copyButton);
// 添加演示按钮
if (lang === 'javascript' || lang === 'html') {
const demoButton = document.createElement('button');
demoButton.className = 'demo-btn';
demoButton.innerHTML = '<i class="fa fa-external-link"></i>';
demoButton.addEventListener('click', () => {
const code = block.textContent;
const newWindow = window.open('', '_blank');
if (lang === 'javascript') {
newWindow.document.write(`<html><body><script>${code}</script></body></html>`);
} else if (lang === 'html') {
newWindow.document.write(code);
}
});
codeTitle.appendChild(demoButton);
}
block.parentNode.insertBefore(codeTitle, block);
});
if (window.renderMathInElement) renderMathInElement(document.body);
// 添加淡入动画
articleContainer.style.opacity = '0';
setTimeout(() => {
articleContainer.style.opacity = '1';
articleContainer.style.transition = 'opacity 0.3s ease';
}, 50);
}
// 高亮活动链接
function highlightActiveLink(id) {
document.querySelectorAll('#sidebar-nav a').forEach(link => {
if (link.getAttribute('href') === `#${id}`) {
link.classList.add('bg-primary/10', 'dark:bg-primary/20', 'text-primary');
} else {
link.classList.remove('bg-primary/10', 'dark:bg-primary/20', 'text-primary');
}
});
}
// 打开侧边栏
function openSidebar() {
sidebar.classList.remove('-translate-x-full');
overlay.classList.remove('hidden');
setTimeout(() => {
overlay.classList.add('opacity-100');
}, 10);
}
// 关闭侧边栏
function closeSidebar() {
sidebar.classList.add('-translate-x-full');
overlay.classList.remove('opacity-100');
setTimeout(() => {
overlay.classList.add('hidden');
}, 300);
}
// 切换主题
function toggleTheme() {
document.documentElement.classList.toggle('dark');
IFcontent = document.getElementById("IFcontent");
if (IFcontent) {
iframeDoc = IFcontent.contentDocument || IFcontent.contentWindow.document;
iframeDoc.body.style.color = document.documentElement.classList.contains('dark') ? '#FFF' : '#000';
iframeDoc.documentElement.classList.toggle('dark');
}
localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light');
}
// 侧边栏搜索功能
function setupSidebarSearch() {
sidebarSearch.addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase().trim();
const links = document.querySelectorAll('#sidebar-nav a');
links.forEach(link => {
const text = link.textContent.toLowerCase();
const isVisible = text.includes(searchTerm);
link.style.display = isVisible ? 'block' : 'none';
// 如果章节下所有子项都不可见,则隐藏章节
const parent = link.closest('div');
const visibleItems = parent.querySelectorAll('a:not([style*="display: none"])').length;
const chapterHeader = parent.previousElementSibling;
if (chapterHeader && visibleItems === 0) {
parent.style.display = 'none';
chapterHeader.style.display = 'none';
} else if (chapterHeader) {
parent.style.display = '';
chapterHeader.style.display = '';
}
});
});
}
// 搜索功能
function setupSearch() {
searchButton.addEventListener('click', performSearch);
globalSearch.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
performSearch();
}
});
}
// 执行搜索
async function performSearch() {
const searchTerm = globalSearch.value.toLowerCase().trim();
if (!searchTerm) return;
showLoadingAnimation();
try {
// 获取所有文章ID
const allIds = [];
tableOfContents.chapters.forEach(chapter => {
allIds.push(chapter.id);
if (chapter.sections) {
chapter.sections.forEach(section => {
allIds.push(section.id);
});
}
});
// 并行请求所有文章内容
const results = await Promise.allSettled(
allIds.map(id => fetch(`/API/GetDoc/${id}`)
.then(response => response.json())
.then(content => ({ id, content }))
)
);
// 处理搜索结果
const searchResults = [];
results.forEach(result => {
if (result.status === 'fulfilled' && result.value.content) {
const { id, content } = result.value;
c = content.DocContent;
if (c.toLowerCase().includes(searchTerm)) {
const title = tableOfContents.chapters.flatMap(chapter => [
{ id: chapter.id, title: chapter.title },
...chapter.sections.map(section => ({ id: section.id, title: section.title }))
]).find(item => item.id === id)?.title || '未知标题';
const excerpt = getExcerpt(c, searchTerm);
searchResults.push({ id, title, excerpt });
}
}
});
// 渲染搜索结果
renderSearchResults(searchResults);
} catch (error) {
console.error('搜索失败:', error);
showError("搜索过程中发生错误");
}
}
// 渲染搜索结果
function renderSearchResults(results) {
if (results.length > 0) {
let resultsHTML = '<div class="search-results">';
resultsHTML += `<h2 class="text-2xl font-bold mb-6">搜索结果: ${results.length} 个匹配项</h2>`;
results.forEach(result => {
resultsHTML += `
<div class="search-result-item mb-6 p-4 rounded-lg border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow">
<h3 class="text-xl font-semibold mb-2">
<a href="#" onclick="loadArticleAsync('${result.id}'); return false;" class="text-primary hover:underline">${result.title}</a>
</h3>
<div class="text-gray-600 dark:text-gray-400 mb-3 text-sm" style="line-height: 1.6">
${result.excerpt}
</div>
<a href="#" onclick="loadArticleAsync('${result.id}'); return false;" class="inline-flex items-center text-sm text-primary hover:text-primary/80">
阅读更多 <i class="fa fa-arrow-right ml-1"></i>
</a>
</div>
`;
});
resultsHTML += '</div>';
articleContainer.innerHTML = resultsHTML;
} else {
showError("未找到匹配的结果");
}
}
// 获取摘要(带转义和搜索词高亮)
function getExcerpt(content, searchTerm) {
// 转义HTML特殊字符
const escapeHtml = (str) => {
return str.replace(/[&<>'"]/g,
tag => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
"'": '&#39;',
'"': '&quot;'
}[tag]));
};
// 转义内容和搜索词
const escapedContent = escapeHtml(content);
const escapedSearchTerm = escapeHtml(searchTerm);
// 查找位置(不区分大小写)
const index = escapedContent.toLowerCase().indexOf(escapedSearchTerm.toLowerCase());
if (index === -1) return ''; // 没找到搜索词
// 计算摘要范围
const start = Math.max(0, index - 100);
const end = Math.min(escapedContent.length, index + escapedSearchTerm.length + 100);
// 构建摘要
let excerpt = escapedContent.substring(start, end);
if (start > 0) excerpt = '...' + excerpt;
if (end < escapedContent.length) excerpt = excerpt + '...';
// 高亮搜索词(使用转义后的搜索词)
const regex = new RegExp(escapedSearchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
excerpt = excerpt.replace(regex, match => `<mark class="bg-yellow-200 dark:bg-yellow-800">${match}</mark>`);
return excerpt;
}
// 展开所有侧边栏章节
function expandAllSidebarSections() {
const chapterContents = document.querySelectorAll('#sidebar-nav .pl-4.mt-1.space-y-1');
const chapterHeaders = document.querySelectorAll('#sidebar-nav .flex.items-center.justify-between.p-2.rounded-lg');
chapterContents.forEach((chapterContent) => {
chapterContent.style.maxHeight = 'none';
const height = chapterContent.scrollHeight * 100;
chapterContent.style.maxHeight = '0';
requestAnimationFrame(() => {
chapterContent.classList.add('expanded');
chapterContent.style.maxHeight = height + 'px';
});
});
chapterHeaders.forEach((chapterHeader) => {
const chevron = chapterHeader.querySelector('.fa-chevron-down');
chevron.style.transform = 'rotate(180deg)';
});
}
// 显示密码输入提示
function showPasswordPrompt(prompt, id) {
articleContainer.innerHTML = `
<div class="flex flex-col items-center justify-center py-16 min-h-[400px]">
<p class="text-gray-600 dark:text-gray-400 mb-4">${prompt}</p>
<input type="password" id="password-input" class="bg-gray-100 dark:bg-gray-700 rounded-lg py-2 px-4 w-full max-w-xs focus:outline-none focus:ring-2 focus:ring-primary/50 transition-all-300 mb-4">
<p id="password-error" class="text-red-500 dark:text-red-400 hidden">密码错误,请重新输入。</p>
<button id="password-submit" class="bg-primary text-white rounded-lg py-2 px-4 hover:bg-primary/80 transition-all-300">提交密码</button>
</div>
`;
const passwordInput = document.getElementById('password-input');
const passwordSubmit = document.getElementById('password-submit');
const passwordError = document.getElementById('password-error');
passwordSubmit.addEventListener('click', () => {
const password = passwordInput.value;
loadArticleAsync(id, password).then(() => {
if (articleContainer.innerHTML.includes('密码错误')) {
passwordError.classList.remove('hidden');
} else {
passwordError.classList.add('hidden');
}
});
});
passwordInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const password = passwordInput.value;
loadArticleAsync(id, password).then(() => {
if (articleContainer.innerHTML.includes('密码错误')) {
passwordError.classList.remove('hidden');
} else {
passwordError.classList.add('hidden');
}
});
}
});
}
// 初始化
function init() {
fetch(`/API/GetSerieDirectory/${UUID}`)
.then(response => response.json())
.then(data => {
window.tableOfContents = data;
headerTitle.innerText = data.title;
document.title = `${data.title} - 章节阅读器`;
renderSidebar();
setupSearch();
setupSidebarSearch();
expandAllSidebarSections();
if (data.document) {
loadArticleAsync(UUID);
// 展开,懒得写了
highlightActiveLink(UUID);
} else {
loadArticleAsync(`${UUID}_index`);
highlightActiveLink(`${UUID}_index`);
}
})
.catch(error => {
// 错误处理保持不变
console.error('获取目录失败:', error);
articleContainer.innerHTML = `
<div class="flex flex-col items-center justify-center py-16">
<i class="fa fa-exclamation-triangle text-5xl text-gray-300 dark:text-gray-600 mb-4"></i>
<h3 class="text-xl font-medium mb-2">加载目录失败</h3>
<p class="text-gray-600 dark:text-gray-400">请检查网络连接或刷新页面</p>
</div>
`;
});
// 恢复主题设置
//const savedTheme = localStorage.getItem('theme');
//if (savedTheme === 'dark') {
// document.documentElement.classList.add('dark');
//}
sidebarToggle.addEventListener('click', openSidebar);
overlay.addEventListener('click', closeSidebar);
themeToggle.addEventListener('click', toggleTheme);
}
window.onload = init;