feat(doc): Document 系列表重设计(可空系列 UUID + DocFormat 双格式)与 doc 子域动态 API,删除旧静态阅读页

This commit is contained in:
TermiNexus committed 2026-08-09 01:50:24 +08:00
1 parent 6c2eb5cdb7
commit d4558fbacb
16 files changed
+234 -1643

No files matched your search

+1 -1
Submodule GVSite updated: 6b7d14c614...2c50a8070b.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

-659
View File
@@ -1,659 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>平滑阅读</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.gvsds.com/font-awesome/css/all.min.css" rel="stylesheet" media="print" onload="this.media='all'">
<link href="https://cdn.gvsds.com/font-awesome/css/font-awesome.min.css" rel="stylesheet" media="print" onload="this.media='all'">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#165DFF',
secondary: '#36D399',
neutral: {
100: '#F9FAFB',
200: '#F3F4F6',
300: '#E5E7EB',
400: '#D1D5DB',
500: '#9CA3AF',
600: '#6B7280',
700: '#4B5563',
800: '#374151',
900: '#1F2937',
}
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
serif: ['Merriweather', 'Georgia', 'serif'],
},
boxShadow: {
'card': '0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.03)',
'card-hover': '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
}
},
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.text-balance {
text-wrap: balance;
}
.reading-mode {
@apply bg-amber-50 text-neutral-800;
}
.reading-mode .article-card {
@apply bg-white shadow-lg;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.transition-all-300 {
transition: all 300ms ease-in-out;
}
}
</style>
</head>
<body class="bg-neutral-100 font-sans text-neutral-800 transition-all duration-300">
<!-- 导航栏 -->
<header id="navbar" class="fixed top-0 left-0 w-full bg-white/90 backdrop-blur-md shadow-sm z-50 transition-all duration-300">
<div class="container mx-auto px-4 py-3 flex items-center justify-between">
<div class="flex items-center space-x-2">
<i class="fa fa-book text-primary text-2xl"></i>
<h1 class="text-xl font-bold text-primary">doc<span class="text-neutral-800">.gvsds</span><span class="text-primary">.com</span></h1>
</div>
<!-- 桌面端导航 -->
<nav class="hidden md:flex items-center space-x-8">
<!--a href="#" class="font-medium text-primary border-b-2 border-primary pb-1">首页</a>
<a href="#" class="font-medium text-neutral-600 hover:text-primary transition-colors">分类</a>
<a href="#" class="font-medium text-neutral-600 hover:text-primary transition-colors">收藏</a>
<a href="#" class="font-medium text-neutral-600 hover:text-primary transition-colors">历史</a>
<a href="#" class="font-medium text-neutral-600 hover:text-primary transition-colors">关于</a-->
</nav>
<!-- 搜索栏 --
<div class="hidden md:flex items-center relative">
<input type="text" placeholder="搜索文档..." class="w-64 py-2 pl-10 pr-4 rounded-full border border-neutral-300 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all">
<i class="fa fa-search absolute left-4 text-neutral-400"></i>
</div>
<-- 用户菜单 --
<div class="flex items-center space-x-4">
<button id="theme-toggle" class="p-2 rounded-full hover:bg-neutral-200 transition-colors">
<i class="fa fa-moon-o text-neutral-700"></i>
</button>
<button id="reading-mode" class="p-2 rounded-full hover:bg-neutral-200 transition-colors">
<i class="fa fa-eye text-neutral-700"></i>
</button>
<button id="search-toggle" class="md:hidden p-2 rounded-full hover:bg-neutral-200 transition-colors">
<i class="fa fa-search text-neutral-700"></i>
</button>
<button id="menu-toggle" class="md:hidden p-2 rounded-full hover:bg-neutral-200 transition-colors">
<i class="fa fa-bars text-neutral-700"></i>
</button>
<a href="#" class="hidden md:block">
<img src="https://picsum.photos/id/1005/40/40" alt="用户头像" class="w-8 h-8 rounded-full border-2 border-white shadow-sm">
</a>
</div-->
</div>
<!-- 移动端搜索框 -->
<div id="mobile-search" class="hidden px-4 pb-3">
<!--div class="relative">
<input type="text" placeholder="搜索文档..." class="w-full py-2 pl-10 pr-4 rounded-full border border-neutral-300 focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary">
<i class="fa fa-search absolute left-4 top-3 text-neutral-400"></i>
</div-->
</div>
<!-- 移动端导航菜单 -->
<div id="mobile-menu" class="hidden px-4 pb-4 md:hidden">
<!--nav class="flex flex-col space-y-3">
<a href="#" class="font-medium text-primary py-2">首页</a>
<a href="#" class="font-medium text-neutral-600 py-2 hover:text-primary transition-colors">分类</a>
<a href="#" class="font-medium text-neutral-600 py-2 hover:text-primary transition-colors">收藏</a>
<a href="#" class="font-medium text-neutral-600 py-2 hover:text-primary transition-colors">历史</a>
<a href="#" class="font-medium text-neutral-600 py-2 hover:text-primary transition-colors">关于</a>
<a href="#" class="flex items-center space-x-2 font-medium text-neutral-600 py-2 hover:text-primary transition-colors">
<img src="https://picsum.photos/id/1005/40/40" alt="用户头像" class="w-8 h-8 rounded-full border-2 border-white shadow-sm">
<span>登录/注册</span>
</a>
</nav-->
</div>
</header>
<!-- 主内容区 -->
<main class="container mx-auto px-4 pt-24 pb-16">
<!-- 欢迎横幅 -->
<section class="mb-12 bg-gradient-to-r from-primary/90 to-primary rounded-2xl overflow-hidden shadow-lg">
<div class="flex flex-col md:flex-row items-center">
<div class="p-8 md:p-12 text-white">
<h2 class="text-[clamp(1.5rem,4vw,2.5rem)] font-bold mb-4">发现知识的乐趣</h2>
<p class="text-white/90 text-lg mb-6 max-w-xl">doc.gvsds.com 提供高质量的技术文档、教程和资源,帮助你平滑阅读和学习。</p>
<p class="text-white/90 text-lg mb-6 max-w-xl">用户可以在 manage.gvsds.com 自由上传文档,因此,请识别链接来源并遵守相关法律法规。同时我们也会做好审核工作</p>
<div class="flex flex-wrap gap-4">
<a href="https://doc.gvsds.com/42be05d96dc33dd781930925fba827c7" class="px-6 py-3 bg-white text-primary font-medium rounded-full hover:bg-neutral-100 transition-colors shadow-md hover:shadow-lg transform hover:-translate-y-0.5 duration-300" target="_blank">
阅读示例 (API 文档)
</a>
</div>
</div>
<div class="hidden md:block w-1/2 h-full ml-auto">
<img src="https://picsum.photos/id/1/600/400" alt="阅读场景" class="w-full h-full object-cover">
</div>
</div>
</section>
<!-- 分类导航 -->
<section class="mb-12">
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold">探索分类</h2>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<a class="category-card bg-white rounded-xl p-5 shadow-card hover:shadow-card-hover transition-all-300 hover:-translate-y-1 flex flex-col items-center text-center">
<div class="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center mb-3">
<i class="fa fa-code text-blue-500 text-xl"></i>
</div>
<h3 class="font-medium">编程开发</h3>
</a>
<a class="category-card bg-white rounded-xl p-5 shadow-card hover:shadow-card-hover transition-all-300 hover:-translate-y-1 flex flex-col items-center text-center">
<div class="w-12 h-12 rounded-full bg-green-100 flex items-center justify-center mb-3">
<i class="fa fa-database text-green-500 text-xl"></i>
</div>
<h3 class="font-medium">数据库</h3>
</a>
<a class="category-card bg-white rounded-xl p-5 shadow-card hover:shadow-card-hover transition-all-300 hover:-translate-y-1 flex flex-col items-center text-center">
<div class="w-12 h-12 rounded-full bg-purple-100 flex items-center justify-center mb-3">
<i class="fa fa-mobile text-purple-500 text-xl"></i>
</div>
<h3 class="font-medium">移动开发</h3>
</a>
<a class="category-card bg-white rounded-xl p-5 shadow-card hover:shadow-card-hover transition-all-300 hover:-translate-y-1 flex flex-col items-center text-center">
<div class="w-12 h-12 rounded-full bg-orange-100 flex items-center justify-center mb-3">
<i class="fa fa-cloud text-orange-500 text-xl"></i>
</div>
<h3 class="font-medium">云计算</h3>
</a>
<a class="category-card bg-white rounded-xl p-5 shadow-card hover:shadow-card-hover transition-all-300 hover:-translate-y-1 flex flex-col items-center text-center">
<div class="w-12 h-12 rounded-full bg-red-100 flex items-center justify-center mb-3">
<i class="fa fa-robot text-red-500 text-xl"></i>
</div>
<h3 class="font-medium">人工智能</h3>
</a>
<a class="category-card bg-white rounded-xl p-5 shadow-card hover:shadow-card-hover transition-all-300 hover:-translate-y-1 flex flex-col items-center text-center">
<div class="w-12 h-12 rounded-full bg-yellow-100 flex items-center justify-center mb-3">
<i class="fa fa-server text-yellow-500 text-xl"></i>
</div>
<h3 class="font-medium">运维部署</h3>
</a>
</div>
</section>
<!-- 精选文档 -->
<section class="mb-12">
<div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold">精选文档</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- 文章卡片 1 -->
<article class="article-card bg-white rounded-xl overflow-hidden shadow-card hover:shadow-card-hover transition-all-300 hover:-translate-y-1">
<div class="relative h-48 overflow-hidden">
<img src="https://picsum.photos/id/0/600/400" alt="Python高级编程技巧" class="w-full h-full object-cover transition-transform duration-500 hover:scale-110">
<div class="absolute top-3 left-3">
<span class="bg-primary text-white text-xs font-medium px-2.5 py-0.5 rounded-full">编程开发</span>
</div>
</div>
<div class="p-5">
<div class="flex items-center text-sm text-neutral-500 mb-3">
<span>2025-06-15</span>
<span class="mx-2">•</span>
<span>10 分钟阅读</span>
</div>
<h3 class="text-xl font-bold mb-2 hover:text-primary transition-colors">Python高级编程技巧与最佳实践</h3>
<p class="text-neutral-600 mb-4 line-clamp-3">探索Python高级编程技巧,包括装饰器、生成器、元类和异步编程。了解如何编写更简洁、高效和可维护的Python代码...</p>
<div class="flex items-center justify-between">
<div class="flex items-center">
<img src="https://picsum.photos/id/1001/30/30" alt="作者头像" class="w-7 h-7 rounded-full">
<span class="ml-2 text-sm font-medium">张教授</span>
</div>
<div class="flex items-center space-x-3 text-neutral-500">
<button class="hover:text-primary transition-colors">
<i class="fa fa-bookmark-o"></i>
</button>
<button class="hover:text-primary transition-colors">
<i class="fa fa-share-alt"></i>
</button>
</div>
</div>
</div>
</article>
<!-- 文章卡片 2 -->
<article class="article-card bg-white rounded-xl overflow-hidden shadow-card hover:shadow-card-hover transition-all-300 hover:-translate-y-1">
<div class="relative h-48 overflow-hidden">
<img src="https://picsum.photos/id/180/600/400" alt="数据库性能优化" class="w-full h-full object-cover transition-transform duration-500 hover:scale-110">
<div class="absolute top-3 left-3">
<span class="bg-green-500 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">数据库</span>
</div>
</div>
<div class="p-5">
<div class="flex items-center text-sm text-neutral-500 mb-3">
<span>2025-06-10</span>
<span class="mx-2">•</span>
<span>15 分钟阅读</span>
</div>
<h3 class="text-xl font-bold mb-2 hover:text-primary transition-colors">MySQL数据库性能优化实战指南</h3>
<p class="text-neutral-600 mb-4 line-clamp-3">深入了解MySQL数据库性能优化的核心原则和实践方法。从查询优化、索引设计到配置调优,全面提升数据库性能...</p>
<div class="flex items-center justify-between">
<div class="flex items-center">
<img src="https://picsum.photos/id/1002/30/30" alt="作者头像" class="w-7 h-7 rounded-full">
<span class="ml-2 text-sm font-medium">李工程师</span>
</div>
<div class="flex items-center space-x-3 text-neutral-500">
<button class="hover:text-primary transition-colors">
<i class="fa fa-bookmark-o"></i>
</button>
<button class="hover:text-primary transition-colors">
<i class="fa fa-share-alt"></i>
</button>
</div>
</div>
</div>
</article>
<!-- 文章卡片 3 -->
<article class="article-card bg-white rounded-xl overflow-hidden shadow-card hover:shadow-card-hover transition-all-300 hover:-translate-y-1">
<div class="relative h-48 overflow-hidden">
<img src="https://picsum.photos/id/160/600/400" alt="前端框架对比" class="w-full h-full object-cover transition-transform duration-500 hover:scale-110">
<div class="absolute top-3 left-3">
<span class="bg-purple-500 text-white text-xs font-medium px-2.5 py-0.5 rounded-full">前端开发</span>
</div>
</div>
<div class="p-5">
<div class="flex items-center text-sm text-neutral-500 mb-3">
<span>2025-06-05</span>
<span class="mx-2">•</span>
<span>20 分钟阅读</span>
</div>
<h3 class="text-xl font-bold mb-2 hover:text-primary transition-colors">2025年前端框架全面对比:React、Vue、Angular</h3>
<p class="text-neutral-600 mb-4 line-clamp-3">详细对比三大主流前端框架的优缺点、适用场景和性能表现。从生态系统、学习曲线到实际项目应用,助你做出最佳选择...</p>
<div class="flex items-center justify-between">
<div class="flex items-center">
<img src="https://picsum.photos/id/1003/30/30" alt="作者头像" class="w-7 h-7 rounded-full">
<span class="ml-2 text-sm font-medium">王架构师</span>
</div>
<div class="flex items-center space-x-3 text-neutral-500">
<button class="hover:text-primary transition-colors">
<i class="fa fa-bookmark-o"></i>
</button>
<button class="hover:text-primary transition-colors">
<i class="fa fa-share-alt"></i>
</button>
</div>
</div>
</div>
</article>
</div>
</section>
<!-- 平滑阅读体验展示 -->
<section class="mb-12 bg-white rounded-2xl shadow-lg overflow-hidden">
<div class="flex flex-col md:flex-row">
<div class="p-8 md:p-12 md:w-1/2 flex flex-col justify-center">
<h2 class="text-2xl md:text-3xl font-bold mb-4">享受平滑阅读体验</h2>
<p class="text-neutral-600 mb-6">我们专注于打造舒适的阅读环境,让你可以更专注于内容本身。支持多种阅读模式和自定义选项,满足你的个性化需求。</p>
<div class="space-y-4 mb-8">
<div class="flex items-start">
<div class="mt-1 mr-4 w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
<i class="fa fa-check text-primary"></i>
</div>
<div>
<h3 class="font-medium text-lg mb-1">护眼模式</h3>
<p class="text-neutral-600">多种背景色和字体设置,减轻眼睛疲劳,长时间阅读也舒适。</p>
</div>
</div>
<div class="flex items-start">
<div class="mt-1 mr-4 w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
<i class="fa fa-check text-primary"></i>
</div>
<div>
<h3 class="font-medium text-lg mb-1">智能排版</h3>
<p class="text-neutral-600">自动优化文章排版,段落间距适中,字体大小合适,提升阅读体验。</p>
</div>
</div>
<div class="flex items-start">
<div class="mt-1 mr-4 w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
<i class="fa fa-check text-primary"></i>
</div>
<div>
<h3 class="font-medium text-lg mb-1">Markdown 阅读</h3>
<p class="text-neutral-600">支持 Markdown 格式文档,实现流程图、公式、代码的查看。</p>
</div>
</div>
</div>
</div>
<div class="md:w-1/2 bg-neutral-100 p-6 flex items-center justify-center">
<div class="relative w-full max-w-md">
<div class="absolute -top-4 -right-4 w-20 h-20 bg-secondary/20 rounded-full"></div>
<div class="absolute -bottom-8 -left-8 w-32 h-32 bg-primary/10 rounded-full"></div>
<div class="relative bg-white rounded-xl shadow-lg overflow-hidden z-10">
<div class="h-12 bg-neutral-200 flex items-center px-4">
<div class="flex space-x-2">
<div class="w-3 h-3 rounded-full bg-red-400"></div>
<div class="w-3 h-3 rounded-full bg-yellow-400"></div>
<div class="w-3 h-3 rounded-full bg-green-400"></div>
</div>
<div class="ml-4 text-sm text-neutral-600">Python装饰器详解.md</div>
</div>
<div class="p-6 h-96 overflow-y-auto scrollbar-hide">
<h1 class="text-2xl font-bold mb-4">Python装饰器详解</h1>
<p class="mb-4 text-neutral-700">装饰器是Python中一种强大而灵活的工具,用于修改或扩展函数或类的行为。它们允许你在不修改原始代码的情况下,增加额外的功能。</p>
<h2 class="text-xl font-bold mt-6 mb-3">什么是装饰器?</h2>
<p class="mb-4 text-neutral-700">装饰器本质上是一个函数,它接受另一个函数作为参数,并返回一个新的函数。这个新的函数通常会包含原始函数的功能,同时增加一些额外的功能。</p>
<div class="bg-neutral-100 p-4 rounded-lg my-4">
<pre class="text-sm text-neutral-800 font-mono">def my_decorator(func):
def wrapper():
print("在函数执行前做一些事情")
func()
print("在函数执行后做一些事情")
return wrapper
@my_decorator
def say_hello():
print("Hello, World!")</pre>
</div>
<p class="mb-4 text-neutral-700">上面的代码定义了一个装饰器`my_decorator`,它会在调用被装饰的函数前后打印一些信息。使用`@my_decorator`语法糖可以很方便地应用装饰器。</p>
<h2 class="text-xl font-bold mt-6 mb-3">装饰器的应用场景</h2>
<ul class="list-disc pl-5 mb-4 space-y-2 text-neutral-700">
<li>日志记录</li>
<li>性能测试</li>
<li>权限验证</li>
<li>缓存</li>
<li>事务处理</li>
</ul>
<p class="text-neutral-700">装饰器是Python中一种非常强大的元编程技术,可以帮助你写出更加简洁、优雅和可维护的代码。</p>
</div>
<div class="bg-neutral-200 h-10 flex items-center justify-between px-4">
<div class="flex items-center space-x-3">
<button class="text-neutral-600 hover:text-primary transition-colors">
<i class="fa fa-font"></i>
</button>
<button class="text-neutral-600 hover:text-primary transition-colors">
<i class="fa fa-text-height"></i>
</button>
<button class="text-neutral-600 hover:text-primary transition-colors">
<i class="fa fa-paint-brush"></i>
</button>
</div>
<div class="flex items-center space-x-3">
<button class="text-neutral-600 hover:text-primary transition-colors">
<i class="fa fa-chevron-left"></i>
</button>
<span class="text-sm text-neutral-600">(1/1) FULL</span>
<button class="text-neutral-600 hover:text-primary transition-colors">
<i class="fa fa-chevron-right"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 热门标签 --
<section class="mb-12">
<h2 class="text-2xl font-bold mb-6">热门标签</h2>
<div class="flex flex-wrap gap-3">
<a href="#" class="px-4 py-2 bg-neutral-200 hover:bg-primary hover:text-white rounded-full transition-colors text-sm font-medium">Python</a>
<a href="#" class="px-4 py-2 bg-neutral-200 hover:bg-primary hover:text-white rounded-full transition-colors text-sm font-medium">JavaScript</a>
<a href="#" class="px-4 py-2 bg-neutral-200 hover:bg-primary hover:text-white rounded-full transition-colors text-sm font-medium">React</a>
<a href="#" class="px-4 py-2 bg-neutral-200 hover:bg-primary hover:text-white rounded-full transition-colors text-sm font-medium">Vue</a>
<a href="#" class="px-4 py-2 bg-neutral-200 hover:bg-primary hover:text-white rounded-full transition-colors text-sm font-medium">机器学习</a>
<a href="#" class="px-4 py-2 bg-neutral-200 hover:bg-primary hover:text-white rounded-full transition-colors text-sm font-medium">数据结构</a>
<a href="#" class="px-4 py-2 bg-neutral-200 hover:bg-primary hover:text-white rounded-full transition-colors text-sm font-medium">算法</a>
<a href="#" class="px-4 py-2 bg-neutral-200 hover:bg-primary hover:text-white rounded-full transition-colors text-sm font-medium">Node.js</a>
<a href="#" class="px-4 py-2 bg-neutral-200 hover:bg-primary hover:text-white rounded-full transition-colors text-sm font-medium">微服务</a>
<a href="#" class="px-4 py-2 bg-neutral-200 hover:bg-primary hover:text-white rounded-full transition-colors text-sm font-medium">Docker</a>
<a href="#" class="px-4 py-2 bg-neutral-200 hover:bg-primary hover:text-white rounded-full transition-colors text-sm font-medium">Kubernetes</a>
<a href="#" class="px-4 py-2 bg-neutral-200 hover:bg-primary hover:text-white rounded-full transition-colors text-sm font-medium">TypeScript</a>
</div>
</section>
<-- 订阅区域 --
<section class="bg-gradient-to-r from-primary/90 to-primary rounded-2xl p-8 md:p-12 shadow-lg">
<div class="max-w-2xl mx-auto text-center">
<h2 class="text-2xl md:text-3xl font-bold text-white mb-4">订阅我们的更新</h2>
<p class="text-white/90 mb-8">获取最新的技术文章、教程和行业动态,直接发送到你的邮箱。</p>
<form class="flex flex-col sm:flex-row gap-3 max-w-lg mx-auto">
<input type="email" placeholder="输入你的邮箱地址" class="flex-grow px-4 py-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-white/30">
<button type="submit" class="bg-white text-primary font-medium px-6 py-3 rounded-lg hover:bg-neutral-100 transition-colors shadow-md hover:shadow-lg transform hover:-translate-y-0.5 duration-300">
订阅
</button>
</form>
<p class="text-white/70 text-sm mt-4">我们尊重你的隐私,绝不会向第三方分享你的邮箱地址。</p>
</div>
</section-->
</main>
<!-- 页脚 -->
<footer class="bg-neutral-900 text-white py-12">
<div class="container mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<div>
<div class="flex items-center space-x-2 mb-4">
<i class="fa fa-book text-primary text-2xl"></i>
<h3 class="text-xl font-bold">doc<span class="text-white">.gvsds</span><span class="text-primary">.com</span></h3>
</div>
<p class="text-neutral-400 mb-4">提供高质量的技术文档和学习资源,帮助开发者平滑阅读和成长。</p>
</div>
<!--div>
<h4 class="text-lg font-medium mb-4">快速链接</h4>
<ul class="space-y-2">
<li><a href="#" class="text-neutral-400 hover:text-primary transition-colors">首页</a></li>
<li><a href="#" class="text-neutral-400 hover:text-primary transition-colors">分类</a></li>
<li><a href="#" class="text-neutral-400 hover:text-primary transition-colors">最新文章</a></li>
<li><a href="#" class="text-neutral-400 hover:text-primary transition-colors">热门标签</a></li>
<li><a href="#" class="text-neutral-400 hover:text-primary transition-colors">关于我们</a></li>
</ul>
</div>
<div>
<h4 class="text-lg font-medium mb-4">支持</h4>
<ul class="space-y-2">
<li><a href="#" class="text-neutral-400 hover:text-primary transition-colors">帮助中心</a></li>
<li><a href="#" class="text-neutral-400 hover:text-primary transition-colors">联系我们</a></li>
<li><a href="#" class="text-neutral-400 hover:text-primary transition-colors">常见问题</a></li>
<li><a href="#" class="text-neutral-400 hover:text-primary transition-colors">隐私政策</a></li>
<li><a href="#" class="text-neutral-400 hover:text-primary transition-colors">使用条款</a></li>
</ul>
</div-->
<div>
<h4 class="text-lg font-medium mb-4">联系我们</h4>
<ul class="space-y-3">
<li class="flex items-start">
<i class="fa fa-map-marker text-primary mt-1 mr-3"></i>
<span class="text-neutral-400">天津市武清区</span>
</li>
<li class="flex items-center">
<i class="fa fa-envelope text-primary mr-3"></i>
<a href="mailto:contact@doc.gvsds.com" class="text-neutral-400 hover:text-primary transition-colors">support@gvsds.com</a>
</li>
<li class="flex items-center">
<i class="fa fa-phone text-primary mr-3"></i>
<span class="text-neutral-400">+86 182 2215 2539</span>
</li>
</ul>
</div>
</div>
<div class="border-t border-neutral-800 mt-10 pt-6 flex flex-col md:flex-row justify-between items-center">
<p class="text-neutral-500 text-sm">© 2025 银河锋锐软件开发工作室. 保留所有权利。</p>
<div class="flex space-x-6 mt-4 md:mt-0">
<a href="https://doc.gvsds.com/fc084ab20a283208adb19033ca4de173" class="text-neutral-500 hover:text-primary text-sm transition-colors">隐私政策</a>
<a href="https://doc.gvsds.com/1f07d3b8cec43a2ab9892a15b150bdd6" class="text-neutral-500 hover:text-primary text-sm transition-colors">服务条款</a>
</div>
</div>
</div>
</footer>
<!-- 返回顶部按钮 -->
<button id="back-to-top" class="fixed bottom-6 right-6 bg-primary text-white w-12 h-12 rounded-full flex items-center justify-center shadow-lg opacity-0 invisible transition-all duration-300 hover:bg-primary/90 transform hover:-translate-y-1">
<i class="fa fa-arrow-up"></i>
</button>
<!-- JavaScript -->
<script>
// 导航栏滚动效果
const navbar = document.getElementById('navbar');
const backToTopBtn = document.getElementById('back-to-top');
window.addEventListener('scroll', function() {
if (window.scrollY > 100) {
navbar.classList.add('py-2', 'shadow-md');
navbar.classList.remove('py-3', 'shadow-sm');
backToTopBtn.classList.remove('opacity-0', 'invisible');
backToTopBtn.classList.add('opacity-100', 'visible');
} else {
navbar.classList.add('py-3', 'shadow-sm');
navbar.classList.remove('py-2', 'shadow-md');
backToTopBtn.classList.add('opacity-0', 'invisible');
backToTopBtn.classList.remove('opacity-100', 'visible');
}
});
// 返回顶部功能
backToTopBtn.addEventListener('click', function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
// 移动端菜单切换
const menuToggle = document.getElementById('menu-toggle');
const mobileMenu = document.getElementById('mobile-menu');
menuToggle.addEventListener('click', function() {
mobileMenu.classList.toggle('hidden');
});
// 移动端搜索框切换
const searchToggle = document.getElementById('search-toggle');
const mobileSearch = document.getElementById('mobile-search');
searchToggle.addEventListener('click', function() {
mobileSearch.classList.toggle('hidden');
});
// 主题切换
const themeToggle = document.getElementById('theme-toggle');
const body = document.body;
themeToggle.addEventListener('click', function() {
body.classList.toggle('bg-neutral-900');
body.classList.toggle('text-white');
const isDark = body.classList.contains('bg-neutral-900');
themeToggle.innerHTML = isDark ?
'<i class="fa fa-sun-o text-yellow-400"></i>' :
'<i class="fa fa-moon-o text-neutral-700"></i>';
// 更新文章卡片样式
const articleCards = document.querySelectorAll('.article-card');
articleCards.forEach(card => {
if (isDark) {
card.classList.remove('bg-white');
card.classList.add('bg-neutral-800');
} else {
card.classList.add('bg-white');
card.classList.remove('bg-neutral-800');
}
});
});
// 阅读模式切换
const readingModeBtn = document.getElementById('reading-mode');
readingModeBtn.addEventListener('click', function() {
body.classList.toggle('reading-mode');
});
// 文章轮播
const prevArticleBtn = document.getElementById('prev-article');
const nextArticleBtn = document.getElementById('next-article');
// 模拟文章轮播功能
let currentArticleIndex = 0;
const totalArticles = 3; // 假设有3篇文章
prevArticleBtn.addEventListener('click', function() {
currentArticleIndex = (currentArticleIndex - 1 + totalArticles) % totalArticles;
updateArticleDisplay();
});
nextArticleBtn.addEventListener('click', function() {
currentArticleIndex = (currentArticleIndex + 1) % totalArticles;
updateArticleDisplay();
});
function updateArticleDisplay() {
// 这里可以实现文章轮播的实际逻辑
console.log(`切换到文章索引: ${currentArticleIndex}`);
}
// 平滑滚动
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href');
if (targetId === '#') return;
const targetElement = document.querySelector(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: 'smooth'
});
}
});
});
</script>
</body>
</html>
-622
View File
@@ -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 = `
<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;
-297
View File
@@ -1,297 +0,0 @@
<link href="https://cdn.gvsds.com/font-awesome/css/all.min.css" rel="stylesheet" media="print" onload="this.media='all'">
<script src="https://cdn.gvsds.com/marked.js/src/marked.min.js"></script>
<link rel="stylesheet" href="https://cdn.gvsds.com/highlight.js/styles/github.min.css" media="print" onload="this.media='all'">
<script src="https://cdn.gvsds.com/highlight.js/highlight.min.js"></script>
<!--script src="https://cdn.gvsds.com/KaTeX/0.16.9/katex.min.js"></script>
<link rel="stylesheet" href="https://cdn.gvsds.com/KaTeX/0.16.9/katex.min.css">
<script src="https://cdn.gvsds.com/mermaid/11.5.0/mermaid.min.js"></script>
<script defer src="https://cdn.gvsds.com/KaTeX/0.16.9/contrib/auto-render.min.js"></script-->
<!-- 自定义工具类 -->
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.sidebar-shadow {
box-shadow: 0 0 20px rgba(0, 0, 0, 0.08);
}
.article-shadow {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
}
.transition-all-300 {
transition: all 0.3s ease;
}
.markdown-content h2 {
@apply text-2xl font-bold text-gray-800 mt-8 mb-4 border-b border-gray-200 pb-2;
}
.markdown-content h3 {
@apply text-xl font-bold text-gray-800 mt-6 mb-3;
}
.markdown-content p {
@apply text-gray-700 mb-4 leading-relaxed;
}
.markdown-content a {
@apply text-primary hover:underline;
}
.markdown-content pre {
@apply bg-gray-50 rounded-lg p-4 my-4 overflow-x-auto relative border border-gray-200;
}
.markdown-content pre::before {
content: '';
}
.code-title {
position: relative;
top: 0;
left: 0;
font-size: 0.8rem;
color: #999;
background-color: rgba(255, 255, 255, 0.8);
padding: 0.1rem 0.3rem;
border-radius: 0.2rem 0.2rem 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.markdown-content code {
@apply font-mono text-sm;
}
.markdown-content ul {
@apply list-disc pl-5 mb-4 text-gray-700;
}
.markdown-content ol {
@apply list-decimal pl-5 mb-4 text-gray-700;
}
.markdown-content blockquote {
@apply border-l-4 border-primary pl-4 italic my-4 text-gray-600;
}
.scrollbar-thin {
scrollbar-width: thin;
}
.scrollbar-thin::-webkit-scrollbar {
width: 5px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 20px;
}
.copy-btn,
.demo-btn {
background-color: rgba(255, 255, 255, 0.8);
padding: 0.1rem 0.3rem;
border-radius: 0.2rem;
cursor: pointer;
margin-left: 0.2rem;
}
.loading-spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #165DFF;
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 表格边框样式 */
.markdown-content table {
@apply border-collapse border border-gray-300 w-full;
}
.markdown-content th,
.markdown-content td {
@apply border border-gray-300 p-2;
}
/* 更多内嵌HTML卡片样式 */
.html-card {
@apply bg-gray-100 p-4 rounded-md mb-4 shadow-md;
}
.html-card-blue {
@apply bg-blue-100 p-4 rounded-md mb-4 shadow-md;
}
.html-card-green {
@apply bg-green-100 p-4 rounded-md mb-4 shadow-md;
}
}
</style>
<style>
.progress-bar {
width: 100%;
background-color: #f3f3f3;
border-radius: 5px;
overflow: hidden;
}
.progress {
width: 0%;
height: 30px;
background-color: #4CAF50;
text-align: center;
line-height: 30px;
color: white;
}
/* 加载动画样式 */
.loading-spinner {
width: 60px;
height: 60px;
position: relative;
}
.spinner {
width: 100%;
height: 100%;
border: 3px solid #f3f3f3;
border-radius: 50%;
border-top: 3px solid #3498db;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 搜索结果样式 */
.search-result-item {
background-color: white;
border-color: #e2e8f0;
}
/* 代码块样式 */
.code-title {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f8fafc;
color: #475569;
padding: 4px 10px;
border-radius: 4px 4px 0 0;
font-family: monospace;
font-size: 0.9rem;
border-bottom: 1px solid #e2e8f0;
}
.copy-btn, .demo-btn {
background: none;
border: none;
cursor: pointer;
color: #64748b;
transition: color 0.2s;
}
.copy-btn:hover, .demo-btn:hover {
color: #3b82f6;
}
pre {
margin-top: 0 !important;
border-radius: 0 0 4px 4px !important;
}
.iframe-content {
width: 100%;
height: 100%;
border: none;
color: #333;
overflow: hidden;
}
</style>
<div class="font-inter bg-white text-gray-800 min-h-screen flex flex-col transition-all duration-300">
<header class="bg-white border-b border-gray-200 shadow-sm sticky z-30 transition-all duration-300 w-full top-0" style="position: fixed;">
<div class="container mx-auto px-4 py-3 flex items-center justify-between">
<div class="flex items-center space-x-2">
<button id="sidebar-toggle" class="lg:hidden p-2 rounded-md hover:bg-gray-100 transition-all-300">
<i class="fa fa-bars text-lg"></i>
</button>
<h1 class="text-xl font-bold text-primary flex items-center">
<i class="fa fa-book mr-2"></i><span id="header-title">平滑阅读</span>
</h1>
</div>
<div class="flex items-center space-x-4">
<div class="relative hidden md:block">
<input id="global-search" type="text" placeholder="全局搜索..."
class="bg-gray-100 rounded-full py-1.5 pl-10 pr-4 w-64 focus:outline-none focus:ring-2 focus:ring-primary/50 transition-all-300">
<i class="fa fa-search absolute left-3 top-2 text-gray-400"></i>
<button id="search-button" class="absolute right-3 top-1/2 transform -translate-y-1/2 p-1 rounded-full hover:bg-gray-200 transition-all-300">
<i class="fa fa-search text-gray-400"></i>
</button>
<button id="theme-toggle" class="p-2 rounded-full hover:bg-gray-100 transition-all-300">
<i class="fa fa-moon dark:hidden text-lg"></i>
<i class="fa fa-sun hidden dark:inline-block text-lg"></i>
</button>
</div>
</div>
</div>
</header>
<div class="flex flex-1 h-[calc(100vh-3.5rem)]">
<!-- 侧边栏 -->
<aside id="sidebar" class="w-64 bg-white border-r border-gray-200 sidebar-shadow fixed h-[calc(100vh-3.5rem)] left-0 top-[3.5rem] z-20 transform -translate-x-full lg:translate-x-0 transition-transform duration-300 ease-in-out overflow-y-auto scrollbar-thin">
<div class="p-4">
<!-- 侧边栏搜索 -->
<div class="relative mb-6">
<input id="sidebar-search" type="text" placeholder="搜索章节..."
class="bg-gray-100 rounded-lg py-2 pl-10 pr-4 w-full focus:outline-none focus:ring-2 focus:ring-primary/50 transition-all-300 mt-7">
<i class="fa fa-search absolute left-3 top-2.5 text-gray-400 mt-7"></i>
</div>
<!-- 章节列表 -->
<nav id="sidebar-nav" class="space-y-1"></nav>
</div>
</aside>
<!-- 遮罩层 -->
<div id="overlay" class="fixed inset-0 bg-black bg-opacity-50 z-10 lg:hidden hidden transition-opacity duration-300"></div>
<!-- 主内容区 -->
<main class="flex-1 lg:ml-64 overflow-y-auto scrollbar-thin">
<div class="max-w-5xl mx-auto p-6 md:p-8">
<div id="article-container" class="markdown-content bg-white rounded-xl article-shadow p-6 md:p-8 transition-all duration-300">
<div class="flex flex-col items-center justify-center h-64 text-gray-500">
<i class="fa fa-file-text-o text-5xl mb-4"></i>
<p>点选章节以开始阅读</p>
</div>
</div>
</div>
</main>
</div>
<script src="/lib/script.js"></script>
{{ NoFooter }}
+7 -7
View File
@@ -5,16 +5,16 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GERP 管理系统</title>
<script type="module" crossorigin src="/assets/index-C8rUiY35.js"></script>
<script type="module" crossorigin src="/assets/index-DI9-hmlZ.js"></script>
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
<link rel="modulepreload" crossorigin href="/assets/element-plus-DpxT9tdZ.js">
<link rel="modulepreload" crossorigin href="/assets/element-plus-BsWRg887.js">
<link rel="modulepreload" crossorigin href="/assets/utils-C0vl14oO.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-D8wgd3LJ.js">
<link rel="modulepreload" crossorigin href="/assets/vxe-table-CuIll8tr.js">
<link rel="modulepreload" crossorigin href="/assets/formkit-od3HK-2F.js">
<link rel="modulepreload" crossorigin href="/assets/vue-vendor-CXHjmsgU.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-BvUFXbAx.js">
<link rel="modulepreload" crossorigin href="/assets/vxe-table-rjTifMWQ.js">
<link rel="modulepreload" crossorigin href="/assets/formkit-sTHkhPfk.js">
<link rel="modulepreload" crossorigin href="/assets/vue-vendor-r2EuHewM.js">
<link rel="modulepreload" crossorigin href="/assets/serverScope-C6yhSV7-.js">
<link rel="stylesheet" crossorigin href="/assets/style-JPWZeKDS.css">
<link rel="stylesheet" crossorigin href="/assets/style-4vmlf1rA.css">
</head>
<body>
<div id="app"></div>
+2 -2
View File
@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>银河锋锐软件开发工作室</title>
<link rel="icon" href="/assets/icon/ICON.png">
<script type="module" crossorigin src="/GVSite/index-CTt_c2Ho.js"></script>
<link rel="stylesheet" crossorigin href="/GVSite/index-Oui0rNpm.css">
<script type="module" crossorigin src="/GVSite/index-CNA2BKwV.js"></script>
<link rel="stylesheet" crossorigin href="/GVSite/index-PmELqBcz.css">
</head>
<body>
<div id="app"></div>
+89 -1
View File
@@ -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/<uuid> → 文章 JSON
# /API/GetSerieDirectory/<uuid> → 系列目录 / 单文档标记 JSON
# ============================================================================
def _doc_api(file_path):
"""doc 子域 JSON API:API/GetDoc/<uuid> 与 API/GetSerieDirectory/<uuid>。"""
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/<uuid> → 文章 JSON
# doc.gvsds.com/API/GetSerieDirectory/<uuid> → 目录 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
+22 -16
View File
@@ -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()
@@ -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'),
),
]
@@ -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='内容格式'),
),
]
+12 -10
View File
@@ -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'
+7 -7
View File
@@ -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):
+8 -2
View File
@@ -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': '添加成功',
+4 -8
View File
@@ -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):
+28 -11
View File
@@ -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):
'</body></html>'
)
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': '删除成功'})