feat(doc): Document 系列表重设计(可空系列 UUID + DocFormat 双格式)与 doc 子域动态 API,删除旧静态阅读页
This commit is contained in:
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 |
@@ -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>
|
||||
@@ -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 => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
"'": ''',
|
||||
'"': '"'
|
||||
}[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;
|
||||
@@ -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 }}
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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'
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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': '添加成功',
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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': '删除成功'})
|
||||
|
||||
Reference in new issue
Block a user