diff --git a/GVSite b/GVSite index 6b7d14c6..2c50a807 160000 --- a/GVSite +++ b/GVSite @@ -1 +1 @@ -Subproject commit 6b7d14c6143949ed358e144851675a46ab4d5dac +Subproject commit 2c50a8070b42f106e25d8ee542f3123bc912bb35 diff --git a/TempLate/Document/assets/img/doc.png b/TempLate/Document/assets/img/doc.png deleted file mode 100644 index 73b00b0c..00000000 Binary files a/TempLate/Document/assets/img/doc.png and /dev/null differ diff --git a/TempLate/Document/index.html b/TempLate/Document/index.html deleted file mode 100644 index 8f28ef72..00000000 --- a/TempLate/Document/index.html +++ /dev/null @@ -1,659 +0,0 @@ - - - - - - 平滑阅读 - - - - - - - - - - - -
- -
-
-
-

发现知识的乐趣

-

doc.gvsds.com 提供高质量的技术文档、教程和资源,帮助你平滑阅读和学习。

-

用户可以在 manage.gvsds.com 自由上传文档,因此,请识别链接来源并遵守相关法律法规。同时我们也会做好审核工作

- -
- -
-
- - -
-
-

探索分类

-
- - -
- - -
-
-

精选文档

-
- -
- -
-
- Python高级编程技巧 -
- 编程开发 -
-
-
-
- 2025-06-15 - • - 10 分钟阅读 -
-

Python高级编程技巧与最佳实践

-

探索Python高级编程技巧,包括装饰器、生成器、元类和异步编程。了解如何编写更简洁、高效和可维护的Python代码...

-
-
- 作者头像 - 张教授 -
-
- - -
-
-
-
- - -
-
- 数据库性能优化 -
- 数据库 -
-
-
-
- 2025-06-10 - • - 15 分钟阅读 -
-

MySQL数据库性能优化实战指南

-

深入了解MySQL数据库性能优化的核心原则和实践方法。从查询优化、索引设计到配置调优,全面提升数据库性能...

-
-
- 作者头像 - 李工程师 -
-
- - -
-
-
-
- - -
-
- 前端框架对比 -
- 前端开发 -
-
-
-
- 2025-06-05 - • - 20 分钟阅读 -
-

2025年前端框架全面对比:React、Vue、Angular

-

详细对比三大主流前端框架的优缺点、适用场景和性能表现。从生态系统、学习曲线到实际项目应用,助你做出最佳选择...

-
-
- 作者头像 - 王架构师 -
-
- - -
-
-
-
-
-
- - -
-
-
-

享受平滑阅读体验

-

我们专注于打造舒适的阅读环境,让你可以更专注于内容本身。支持多种阅读模式和自定义选项,满足你的个性化需求。

- -
-
-
- -
-
-

护眼模式

-

多种背景色和字体设置,减轻眼睛疲劳,长时间阅读也舒适。

-
-
- -
-
- -
-
-

智能排版

-

自动优化文章排版,段落间距适中,字体大小合适,提升阅读体验。

-
-
- -
-
- -
-
-

Markdown 阅读

-

支持 Markdown 格式文档,实现流程图、公式、代码的查看。

-
-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
-
Python装饰器详解.md
-
- -
-

Python装饰器详解

-

装饰器是Python中一种强大而灵活的工具,用于修改或扩展函数或类的行为。它们允许你在不修改原始代码的情况下,增加额外的功能。

- -

什么是装饰器?

-

装饰器本质上是一个函数,它接受另一个函数作为参数,并返回一个新的函数。这个新的函数通常会包含原始函数的功能,同时增加一些额外的功能。

- -
-
def my_decorator(func):
-    def wrapper():
-        print("在函数执行前做一些事情")
-        func()
-        print("在函数执行后做一些事情")
-    return wrapper
-
-@my_decorator
-def say_hello():
-    print("Hello, World!")
-
- -

上面的代码定义了一个装饰器`my_decorator`,它会在调用被装饰的函数前后打印一些信息。使用`@my_decorator`语法糖可以很方便地应用装饰器。

- -

装饰器的应用场景

-
    -
  • 日志记录
  • -
  • 性能测试
  • -
  • 权限验证
  • -
  • 缓存
  • -
  • 事务处理
  • -
- -

装饰器是Python中一种非常强大的元编程技术,可以帮助你写出更加简洁、优雅和可维护的代码。

-
- -
-
- - - -
-
- - (1/1) FULL - -
-
-
-
-
-
-
- - -
- - - - - - - - - - - \ No newline at end of file diff --git a/TempLate/Document/lib/script.js b/TempLate/Document/lib/script.js deleted file mode 100644 index b0db6f9b..00000000 --- a/TempLate/Document/lib/script.js +++ /dev/null @@ -1,622 +0,0 @@ -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 = ` - - - ${chapter.title} - - - `; - - 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 = ` - ${section.title} - `; - 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 = ` -
-
-
-
-

正在加载文章内容...

-
- `; -} - -// 显示错误信息 -function showError(message) { - articleContainer.innerHTML = ` -
- -

${message}

-

请尝试刷新页面或联系管理员

-
- `; -} - -function sanitizeInput(input) { - if (typeof input !== 'string') return ''; - // 1. 移除 script 标签 - let sanitized = input.replace(/]*>([\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) === "`; - 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 += ''; - } - - 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 = ''; - copyButton.addEventListener('click', () => { - const text = block.textContent; - navigator.clipboard.writeText(text).then(() => { - copyButton.innerHTML = ''; - setTimeout(() => { - copyButton.innerHTML = ''; - }, 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 = ''; - demoButton.addEventListener('click', () => { - const code = block.textContent; - const newWindow = window.open('', '_blank'); - if (lang === 'javascript') { - newWindow.document.write(``); - } 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 = '
'; - resultsHTML += `

搜索结果: ${results.length} 个匹配项

`; - - results.forEach(result => { - resultsHTML += ` -
-

- ${result.title} -

-
- ${result.excerpt} -
- - 阅读更多 - -
- `; - }); - - resultsHTML += '
'; - articleContainer.innerHTML = resultsHTML; - } else { - showError("未找到匹配的结果"); - } -} - -// 获取摘要(带转义和搜索词高亮) -function getExcerpt(content, searchTerm) { - // 转义HTML特殊字符 - const escapeHtml = (str) => { - return str.replace(/[&<>'"]/g, - tag => ({ - '&': '&', - '<': '<', - '>': '>', - "'": ''', - '"': '"' - }[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 => `${match}`); - - 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 = ` -
-

${prompt}

- - - -
- `; - 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 = ` -
- -

加载目录失败

-

请检查网络连接或刷新页面

-
- `; - }); - // 恢复主题设置 - //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; \ No newline at end of file diff --git a/TempLate/Document/template.html b/TempLate/Document/template.html deleted file mode 100644 index 2e9b5da8..00000000 --- a/TempLate/Document/template.html +++ /dev/null @@ -1,297 +0,0 @@ - - - - - - - - - - - -
- -
-
-
- -

- 平滑阅读 -

-
- -
- -
-
-
- -
- - - - - - - -
-
-
-
- -

点选章节以开始阅读

-
-
-
-
-
- - -{{ NoFooter }} \ No newline at end of file diff --git a/TempLate/GERP/index.html b/TempLate/GERP/index.html index b631795d..e76bf349 100644 --- a/TempLate/GERP/index.html +++ b/TempLate/GERP/index.html @@ -5,16 +5,16 @@ GERP 管理系统 - + - + - - - - + + + + - +
diff --git a/TempLate/index.html b/TempLate/index.html index c19aa279..5c5602ba 100644 --- a/TempLate/index.html +++ b/TempLate/index.html @@ -5,8 +5,8 @@ 银河锋锐软件开发工作室 - - + +
diff --git a/gvsds/apps/core/views.py b/gvsds/apps/core/views.py index a4d9e12c..af6f27a6 100644 --- a/gvsds/apps/core/views.py +++ b/gvsds/apps/core/views.py @@ -43,7 +43,7 @@ SUBDOMAIN_CONFIG = { '3crb': {'dir': 'CCCRB', 'root_is_index': True}, 'ccgo': {'dir': 'CCGO', 'root_is_index': True}, 'invite': {'dir': '', 'root_is_index': True}, - 'doc': {'dir': 'Document', 'root_is_index': True}, + 'doc': {'dir': '', 'root_is_index': True}, 't4': {'dir': None, 'root_is_index': False}, 'verify': {'dir': 'Verify', 'root_is_index': True}, 'verifox': {'dir': 'Verifox', 'root_is_index': True}, @@ -129,6 +129,86 @@ def _serve_file(full_path, subdomain=None): return response +# ============================================================================ +# doc 子域 JSON API(页面由 GVSite SPA 渲染) +# /API/GetDoc/ → 文章 JSON +# /API/GetSerieDirectory/ → 系列目录 / 单文档标记 JSON +# ============================================================================ + + +def _doc_api(file_path): + """doc 子域 JSON API:API/GetDoc/ 与 API/GetSerieDirectory/。""" + from gvsds.apps.doc.manager import DocManager + from gvsdsdk.model_utils import UUIDObj + + parts = file_path.split('/') + if len(parts) < 3 or parts[0] != 'API': + return None + action = parts[1] + uuid_text = parts[2].strip('/') + try: + uuid_bytes = UUIDObj(uuid_text).bytes + except (ValueError, TypeError, AttributeError): + return JsonResponse({'error': 'invalid_uuid'}, status=404) + + if action == 'GetDoc': + doc = DocManager.GetDocument(uuid_bytes) + if doc is None: + return JsonResponse({'error': 'not_found'}, status=404) + return JsonResponse({ + 'DocTitle': doc.DocTitle or '未命名文档', + 'DocContent': doc.DocContent or '', + 'DocFormat': doc.DocFormat, + 'DocPermission': 1, + 'DocDecrypt': True, + }) + + if action == 'GetSerieDirectory': + from gvsds.apps.doc.models import CommunityDocument, CommunitySeries + try: + serie = CommunitySeries.objects.get(DocSerieUUID=uuid_bytes) + except CommunitySeries.DoesNotExist: + serie = None + if serie is not None: + docs = CommunityDocument.objects.filter(DocSerieUUID=uuid_bytes) + chapters = [ + { + 'id': UUIDObj(d.DocUUID).str.replace('-', ''), + 'title': d.DocTitle or '未命名文档', + 'sections': [], + } + for d in docs + ] + return JsonResponse({ + 'title': serie.DocSerieName or '未命名系列', + 'document': False, + 'chapters': chapters, + }) + # 非系列 UUID:单文档模式,返回所属系列的全部文章供侧边栏展示 + doc = DocManager.GetDocument(uuid_bytes) + if doc is None: + return JsonResponse({'error': 'not_found'}, status=404) + chapters = [] + if doc.DocSerieUUID: + serie_docs = CommunityDocument.objects.filter(DocSerieUUID=doc.DocSerieUUID) + chapters = [ + { + 'id': UUIDObj(d.DocUUID).str.replace('-', ''), + 'title': d.DocTitle or '未命名文档', + 'sections': [], + } + for d in serie_docs + ] + return JsonResponse({ + 'title': doc.DocTitle or '未命名文档', + 'document': True, + 'id': UUIDObj(doc.DocUUID).str.replace('-', ''), + 'chapters': chapters, + }) + + return None + + class SubdomainView(View): def get(self, request, file_path=''): subdomain = _get_subdomain(request) @@ -137,6 +217,14 @@ class SubdomainView(View): if config.get('dir') is None: raise Http404 + # doc 子域:动态阅读 JSON API(页面由 GVSite SPA 渲染,走下方通用静态/SPA fallback) + # doc.gvsds.com/API/GetDoc/ → 文章 JSON + # doc.gvsds.com/API/GetSerieDirectory/ → 目录 JSON + if subdomain == 'doc' and file_path and file_path.startswith('API/'): + api_resp = _doc_api(file_path) + if api_resp: + return api_resp + # ugm 子域:未登录则跳转登录页,已登录则跳转 gerp if subdomain == 'ugm': # JWT Cookie 认证:检查 AccessToken 或 RefreshToken diff --git a/gvsds/apps/doc/manager.py b/gvsds/apps/doc/manager.py index 242f36cb..c8d6d628 100644 --- a/gvsds/apps/doc/manager.py +++ b/gvsds/apps/doc/manager.py @@ -17,7 +17,7 @@ class DocManager: @staticmethod def ListDocuments(Keyword=''): - """获取文档列表(不含 DocContent 全文,仅标题与创建时间)""" + """获取文档列表(不含 DocContent 全文,仅标题/系列/创建时间/格式)""" qs = CommunityDocument.objects.all() if Keyword: qs = qs.filter(DocTitle__icontains=Keyword) @@ -25,7 +25,9 @@ class DocManager: for d in qs: data.append({ 'DocUUID': UUIDObj(d.DocUUID).str, + 'DocSerieUUID': UUIDObj(d.DocSerieUUID).str if d.DocSerieUUID else None, 'DocTitle': d.DocTitle, + 'DocFormat': d.DocFormat, 'DocCreateTime': d.DocCreateTime.isoformat() if d.DocCreateTime else None, }) return data @@ -39,23 +41,24 @@ class DocManager: return None @staticmethod - def AddDocument(Title, Content='', SerieUUID=None): - """新建文档(可选绑定系列)""" + def AddDocument(Title, Content='', SerieUUID=None, Format='markdown'): + """新建文档(可选绑定系列;DocSerieUUID 为可空 UUID 列,直接存 bytes) + + Format: 'markdown' 或 'html'(DocFormat 字段)。 + """ create_data = { 'DocUUID': uuid.uuid4().bytes, 'DocTitle': Title, 'DocContent': Content, + 'DocFormat': Format if Format in ('markdown', 'html') else 'markdown', } - if SerieUUID: - serie = DocManager.GetSerie(SerieUUID) - if serie is not None: - # DocSerieUUID 为 ForeignKey:传实例 - create_data['DocSerieUUID'] = serie + if SerieUUID and DocManager.GetSerie(SerieUUID) is not None: + create_data['DocSerieUUID'] = UUIDObj(SerieUUID).bytes return CommunityDocument.objects.create(**create_data) @staticmethod def ModifyDocument(DocUUID, Data): - """修改文档标题与内容 + """修改文档标题/内容/格式 Returns: (成功?, 错误消息 or None) @@ -63,7 +66,7 @@ class DocManager: doc = DocManager.GetDocument(DocUUID) if doc is None: return False, '文档不存在' - for field in ('DocTitle', 'DocContent'): + for field in ('DocTitle', 'DocContent', 'DocFormat'): if field in Data: setattr(doc, field, Data[field]) doc.save() @@ -86,12 +89,12 @@ class DocManager: @staticmethod def ListSeries(): - """获取系列列表""" + """获取系列列表(统一 DocSerie 前缀字段名)""" data = [] for s in CommunitySeries.objects.all(): data.append({ - 'SerieUUID': UUIDObj(s.DocSerieUUID).str, - 'SerieName': s.DocSerieName, + 'DocSerieUUID': UUIDObj(s.DocSerieUUID).str, + 'DocSerieName': s.DocSerieName, }) return data @@ -152,7 +155,10 @@ class DocManager: @staticmethod def SerieHasDocuments(DocSerieUUID): """判断系列下是否还有文档(防删除)""" - serie = DocManager.GetSerie(DocSerieUUID) - if serie is None: + try: + serie_bytes = UUIDObj(DocSerieUUID).bytes + except (ValueError, AttributeError): return False - return CommunityDocument.objects.filter(DocSerieUUID=serie).exists() + if DocManager.GetSerie(DocSerieUUID) is None: + return False + return CommunityDocument.objects.filter(DocSerieUUID=serie_bytes).exists() diff --git a/gvsds/apps/doc/migrations/0002_doc_redesign.py b/gvsds/apps/doc/migrations/0002_doc_redesign.py new file mode 100644 index 00000000..6ffee531 --- /dev/null +++ b/gvsds/apps/doc/migrations/0002_doc_redesign.py @@ -0,0 +1,34 @@ +"""Redesign CommunityDocument + +系列字段 DocSerieUUID 由 FK 改为可空 UUIDField(非 FK,消除列名不一致); +删除冗余且从未使用的 DocPermission / DocPwd / DocPwdDesc 字段。 +""" +import gvsds.apps.core.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('doc', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='communitydocument', + name='doc_permission', + ), + migrations.RemoveField( + model_name='communitydocument', + name='doc_pwd', + ), + migrations.RemoveField( + model_name='communitydocument', + name='doc_pwd_desc', + ), + migrations.AlterField( + model_name='communitydocument', + name='doc_serie_uuid', + field=gvsds.apps.core.models.UUIDField(blank=True, db_column='DocSerieUUID', null=True, serialize=False, verbose_name='所属系列UUID'), + ), + ] diff --git a/gvsds/apps/doc/migrations/0003_doc_format.py b/gvsds/apps/doc/migrations/0003_doc_format.py new file mode 100644 index 00000000..f188786e --- /dev/null +++ b/gvsds/apps/doc/migrations/0003_doc_format.py @@ -0,0 +1,20 @@ +"""Add CommunityDocument.DocFormat + +内容格式字段:markdown(默认,兼容旧内容经 marked 渲染)或 html(富文本)。 +""" +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('doc', '0002_doc_redesign'), + ] + + operations = [ + migrations.AddField( + model_name='communitydocument', + name='doc_format', + field=models.CharField(choices=[('markdown', 'Markdown'), ('html', '富文本 HTML')], db_column='DocFormat', default='markdown', max_length=16, verbose_name='内容格式'), + ), + ] diff --git a/gvsds/apps/doc/models.py b/gvsds/apps/doc/models.py index b59c76b5..d013dff5 100644 --- a/gvsds/apps/doc/models.py +++ b/gvsds/apps/doc/models.py @@ -8,21 +8,23 @@ from gvsds.apps.core.models import UUIDField class CommunityDocument(QModel): - """社区文档表——文档内容与权限管理""" + """社区文档表——文档内容管理 + + DocSerieUUID 为可空 UUIDField(非 FK):系列仅是逻辑分组, + 避免 FK + to_field 导致列名(DocSerieUUID_id)与迁移不一致的坑。 + """ DocUUID = UUIDField(primary_key=True, verbose_name='文档UUID') - DocSerieUUID = models.ForeignKey( - 'CommunitySeries', on_delete=models.CASCADE, - to_field='DocSerieUUID', - related_name='+', - verbose_name='所属系列', - ) + DocSerieUUID = UUIDField(null=True, blank=True, verbose_name='所属系列UUID') DocTitle = models.CharField(max_length=256, verbose_name='文档标题') DocContent = models.TextField(verbose_name='文档内容') + DocFormat = models.CharField( + max_length=16, + default='markdown', + choices=(('markdown', 'Markdown'), ('html', '富文本 HTML')), + verbose_name='内容格式', + ) DocCreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='文档创建时间') - DocPermission = models.IntegerField(default=0, verbose_name='文档权限') - DocPwd = UUIDField(verbose_name='文档密码') - DocPwdDesc = models.CharField(max_length=256, verbose_name='密码描述') class Meta: db_table = 'Document' diff --git a/gvsds/apps/doc/serializers.py b/gvsds/apps/doc/serializers.py index 45f5c9c5..ff7c8cc6 100644 --- a/gvsds/apps/doc/serializers.py +++ b/gvsds/apps/doc/serializers.py @@ -10,7 +10,7 @@ class CommunityDocumentSerializer(PascalCaseModelSerializer): class Meta: model = CommunityDocument - fields = ['DocUUID', 'DocSerieUUID', 'DocTitle', 'DocContent', 'DocCreateTime'] + fields = ['DocUUID', 'DocSerieUUID', 'DocTitle', 'DocContent', 'DocFormat', 'DocCreateTime'] def get_DocUUID(self, obj): raw = obj.DocUUID @@ -19,14 +19,12 @@ class CommunityDocumentSerializer(PascalCaseModelSerializer): return str(raw) if raw else None def get_DocSerieUUID(self, obj): - raw = obj.DocSerieUUID_id if hasattr(obj, 'DocSerieUUID_id') else obj.DocSerieUUID + # DocSerieUUID 现为可空 UUIDField(非 FK),直接转字符串 + raw = obj.DocSerieUUID + if raw is None: + return None if isinstance(raw, bytes) and len(raw) == 16: return str(uuid.UUID(bytes=raw)) - if isinstance(raw, CommunitySeries): - raw2 = raw.DocSerieUUID - if isinstance(raw2, bytes) and len(raw2) == 16: - return str(uuid.UUID(bytes=raw2)) - return str(raw2) if raw2 else None return str(raw) if raw else None @@ -47,12 +45,14 @@ class CommunitySeriesSerializer(PascalCaseModelSerializer): class DocAddSerializer(PascalCaseSerializer): DocTitle = serializers.CharField(max_length=256) DocSerieUUID = serializers.CharField(required=False) + DocFormat = serializers.ChoiceField(choices=('markdown', 'html'), required=False) class DocModifySerializer(PascalCaseSerializer): DocUUID = serializers.CharField() DocTitle = serializers.CharField(max_length=256, required=False) DocContent = serializers.CharField(required=False) + DocFormat = serializers.ChoiceField(choices=('markdown', 'html'), required=False) class SerieAddSerializer(PascalCaseSerializer): diff --git a/gvsds/apps/doc/views.py b/gvsds/apps/doc/views.py index 03a142db..58318487 100644 --- a/gvsds/apps/doc/views.py +++ b/gvsds/apps/doc/views.py @@ -40,7 +40,7 @@ class CommunityDocumentViewSet(viewsets.ModelViewSet): for d in qs: data.append({ 'DocUUID': UUIDObj(d.DocUUID).str, - 'DocSerieUUID': UUIDObj(d.DocSerieUUID.pk if hasattr(d, 'DocSerieUUID_id') else d.DocSerieUUID).str, + 'DocSerieUUID': UUIDObj(d.DocSerieUUID).str if d.DocSerieUUID else None, 'DocTitle': d.DocTitle, 'DocCreateTime': d.DocCreateTime.isoformat() if d.DocCreateTime else None, }) @@ -62,6 +62,7 @@ class CommunityDocumentViewSet(viewsets.ModelViewSet): 'DocUUID': UUIDObj(doc.DocUUID).str, 'DocTitle': doc.DocTitle, 'DocContent': doc.DocContent or '', + 'DocFormat': doc.DocFormat, 'DocCreateTime': doc.DocCreateTime.isoformat() if doc.DocCreateTime else None, }) @@ -79,7 +80,12 @@ class CommunityDocumentViewSet(viewsets.ModelViewSet): if DocManager.GetSerie(serie_uuid_str) is None: return Response({'code': 1, 'msg': '系列不存在'}, status=400) - doc = DocManager.AddDocument(Title=data['DocTitle'], Content='', SerieUUID=serie_uuid_str) + doc = DocManager.AddDocument( + Title=data['DocTitle'], + Content='', + SerieUUID=serie_uuid_str, + Format=data.get('DocFormat', 'markdown'), + ) return Response({ 'code': 0, 'msg': '添加成功', diff --git a/gvsds/apps/gerp/serializers.py b/gvsds/apps/gerp/serializers.py index 255929cd..c09ebdbc 100644 --- a/gvsds/apps/gerp/serializers.py +++ b/gvsds/apps/gerp/serializers.py @@ -275,7 +275,7 @@ class FinanceShowSerializer(PascalCaseSerializer): class DocAddSerializer(PascalCaseSerializer): DocTitle = serializers.CharField(max_length=256) DocContent = serializers.CharField(required=False, allow_blank=True, default='') - SerieUUID = serializers.CharField(required=False) + DocSerieUUID = serializers.CharField(required=False) DocTags = serializers.JSONField(default=list) @@ -287,16 +287,12 @@ class DocModifySerializer(PascalCaseSerializer): class SerieAddSerializer(PascalCaseSerializer): - SerieName = serializers.CharField(max_length=256) - SerieDescription = serializers.CharField(default='') - SerieTags = serializers.JSONField(default=list) + DocSerieName = serializers.CharField(max_length=256) class SerieModifySerializer(PascalCaseSerializer): - SerieUUID = serializers.CharField() - SerieName = serializers.CharField(max_length=256, required=False) - SerieDescription = serializers.CharField(required=False) - SerieTags = serializers.JSONField(required=False) + DocSerieUUID = serializers.CharField() + DocSerieName = serializers.CharField(max_length=256, required=False) class WorkOrderCreateSerializer(PascalCaseSerializer): diff --git a/gvsds/apps/gerp/views/doc.py b/gvsds/apps/gerp/views/doc.py index fe5a0c82..d44197b1 100644 --- a/gvsds/apps/gerp/views/doc.py +++ b/gvsds/apps/gerp/views/doc.py @@ -74,10 +74,13 @@ class DocView(ELTAuthenticatedAPIView): return Response({'code': 1, 'msg': '文档不存在'}, status=404) return Response({ 'code': 0, - 'DocUUID': UUIDObj(doc.DocUUID).str, - 'DocTitle': doc.DocTitle, - 'DocContent': doc.DocContent or '', - 'DocCreateTime': doc.DocCreateTime.isoformat() if doc.DocCreateTime else None, + 'data': { + 'DocUUID': UUIDObj(doc.DocUUID).str, + 'DocTitle': doc.DocTitle, + 'DocContent': doc.DocContent or '', + 'DocFormat': doc.DocFormat, + 'DocCreateTime': doc.DocCreateTime.isoformat() if doc.DocCreateTime else None, + }, }) def _AddDoc(self, request): @@ -86,7 +89,12 @@ class DocView(ELTAuthenticatedAPIView): ser.IsValid(raise_exception=True) data = ser.ValidatedData from gvsds.apps.doc.manager import DocManager - doc = DocManager.AddDocument(data['DocTitle'], data.get('DocContent', '')) + doc = DocManager.AddDocument( + data['DocTitle'], + data.get('DocContent', ''), + SerieUUID=data.get('DocSerieUUID'), + Format=data.get('DocFormat', 'markdown'), + ) return Response({ 'code': 0, 'msg': '添加成功', @@ -171,7 +179,9 @@ class DocView(ELTAuthenticatedAPIView): buffer.getvalue(), content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document', ) - response['Content-Disposition'] = f'attachment; filename*=UTF-8\'\'{__import__("urllib.parse", fromlist=["quote"]).quote(filename)}' + # 同时提供 ASCII filename 与 RFC 5987 filename*(兼容各浏览器下载文件名) + quoted = __import__('urllib.parse', fromlist=['quote']).quote(filename) + response['Content-Disposition'] = f"attachment; filename=\"document.docx\"; filename*=UTF-8''{quoted}" return response @staticmethod @@ -186,7 +196,8 @@ class DocView(ELTAuthenticatedAPIView): '' ) response = HttpResponse(html, content_type='text/html; charset=utf-8') - response['Content-Disposition'] = f'attachment; filename*=UTF-8\'\'{__import__("urllib.parse", fromlist=["quote"]).quote(filename)}' + quoted = __import__('urllib.parse', fromlist=['quote']).quote(filename) + response['Content-Disposition'] = f"attachment; filename=\"document.html\"; filename*=UTF-8''{quoted}" return response @@ -204,19 +215,25 @@ class SerieView(ELTAuthenticatedAPIView): if action == 'AddSerie': ser = SerieAddSerializer(data=request.data) ser.IsValid(raise_exception=True) - DocManager.AddSeries(ser.ValidatedData) + data = ser.ValidatedData + DocManager.AddSeries({ + 'DocSerieName': data['DocSerieName'], + 'DocAuthorUserUUID': getattr(request.user, 'UserUUID', None), + }) return Response({'code': 0, 'msg': '添加成功'}, status=201) elif action == 'ModifySerieSettings': ser = SerieModifySerializer(data=request.data) ser.IsValid(raise_exception=True) data = ser.ValidatedData - ok, err = DocManager.ModifySerie(data['SerieUUID'], data) + ok, err = DocManager.ModifySerie(data['DocSerieUUID'], {'DocSerieName': data.get('DocSerieName')}) if not ok: return Response({'code': 1, 'msg': err or '系列不存在'}, status=404) return Response({'code': 0, 'msg': '修改成功'}) elif action == 'DeleteSerie': - serie_uuid = request.data.get('SerieUUID') - ok, err = DocManager.DeleteSerie(serie_uuid) + doc_serie_uuid = request.data.get('DocSerieUUID') + if not doc_serie_uuid: + return Response({'code': 1, 'msg': '缺少DocSerieUUID'}, status=400) + ok, err = DocManager.DeleteSerie(doc_serie_uuid) if not ok: return Response({'code': 1, 'msg': err or '系列不存在'}, status=404) return Response({'code': 0, 'msg': '删除成功'})