Skip to content

Commit 37c04e0

Browse files
committed
feat(docs): 为文档中心添加HTMX支持以实现无刷新导航
- 新增 `_get_template_name` 函数,根据 HTMX 请求头动态返回模板 - 将主内容区域和侧边栏菜单拆分为独立模板 `_doc_content.html` 和 `_sidebar_menu.html` - 为所有文档链接添加 `hx-get`、`hx-target` 和 `hx-push-url` 属性以实现局部替换 - 修改 JavaScript 代码以在 HTMX 内容替换后重新初始化高亮和复制按钮功能 - 将模板中的 `title` 上下文变量统一重命名为 `page_title` 以避免冲突
1 parent 70fdfec commit 37c04e0

4 files changed

Lines changed: 207 additions & 154 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
{% load static page_tags %}
2+
3+
<!-- OOB Swap for Sidebar Menu -->
4+
<div id="doc-sidebar-menu" hx-swap-oob="innerHTML">
5+
{% include "django_starter/docs/_sidebar_menu.html" %}
6+
</div>
7+
8+
<!-- Main Content (Target of HTMX) -->
9+
<!-- Page Header (Breadcrumbs) -->
10+
{% page_header page_title breadcrumbs %}
11+
12+
<!-- Mobile Toggle -->
13+
<div class="lg:hidden px-4 mb-4">
14+
<label for="docs-drawer" class="btn btn-primary drawer-button w-full">
15+
<i class="fas fa-bars mr-2"></i> 目录导航
16+
</label>
17+
</div>
18+
19+
<!-- Content Container -->
20+
<div class="flex flex-col xl:flex-row px-0 lg:px-4 py-6">
21+
<!-- Doc Content -->
22+
<div class="flex-1 min-w-0 px-4 lg:px-8">
23+
<!-- Search -->
24+
<div class="mb-8 max-w-2xl">
25+
<form method="get" action="{% url 'djs_docs:index' %}" hx-boost="true" hx-target="#doc-main-content" hx-push-url="true">
26+
<div class="join w-full shadow-sm">
27+
<input name="q" value="{{ search_query }}" placeholder="搜索文档、关键字..." class="input input-bordered join-item w-full focus:outline-none" />
28+
<button class="btn btn-primary join-item px-6"><i class="fas fa-search"></i></button>
29+
</div>
30+
</form>
31+
</div>
32+
33+
{% if search_results %}
34+
<div class="mb-8">
35+
<h3 class="font-bold text-lg mb-4">搜索结果: "{{ search_query }}"</h3>
36+
<div class="grid gap-4">
37+
{% for result in search_results %}
38+
<a href="{% url 'djs_docs:detail' slug=result.slug %}"
39+
hx-get="{% url 'djs_docs:detail' slug=result.slug %}"
40+
hx-target="#doc-main-content"
41+
hx-push-url="true"
42+
class="card bg-base-200 hover:bg-base-300 transition-colors compact border border-base-300">
43+
<div class="card-body">
44+
<h2 class="card-title text-base">{{ result.title }}</h2>
45+
<p class="text-sm opacity-70">{{ result.summary }}</p>
46+
</div>
47+
</a>
48+
{% endfor %}
49+
{% if not search_results %}
50+
<div class="alert">
51+
<i class="fas fa-info-circle"></i>
52+
<span>未找到相关文档,请尝试更换关键词。</span>
53+
</div>
54+
{% endif %}
55+
</div>
56+
<div class="divider"></div>
57+
</div>
58+
{% endif %}
59+
60+
<article class="prose prose-base lg:prose-lg max-w-none prose-pre:p-0 prose-pre:bg-transparent prose-img:rounded-xl">
61+
{% if active_page and not search_results %}
62+
<div class="mb-8 not-prose">
63+
<h1 class="text-3xl lg:text-4xl font-extrabold mb-2">{{ active_page.title }}</h1>
64+
<p class="text-xl text-base-content/70">{{ active_page.summary }}</p>
65+
</div>
66+
{% endif %}
67+
68+
{{ active_html|safe }}
69+
</article>
70+
71+
<!-- Prev/Next Nav -->
72+
<div class="flex flex-col sm:flex-row justify-between mt-12 gap-4 pt-8 border-t border-base-200">
73+
{% if prev_page %}
74+
<a href="{% url 'djs_docs:detail' slug=prev_page.slug %}"
75+
hx-get="{% url 'djs_docs:detail' slug=prev_page.slug %}"
76+
hx-target="#doc-main-content"
77+
hx-push-url="true"
78+
class="group flex-1 p-4 rounded-xl border border-base-300 hover:border-primary hover:bg-base-200 transition-all text-left">
79+
<div class="text-xs text-base-content/50 mb-1 group-hover:text-primary">上一篇</div>
80+
<div class="font-bold text-lg flex items-center gap-2">
81+
<i class="fas fa-arrow-left text-sm transition-transform group-hover:-translate-x-1"></i>
82+
{{ prev_page.title }}
83+
</div>
84+
</a>
85+
{% else %}
86+
<div class="flex-1 hidden sm:block"></div>
87+
{% endif %}
88+
89+
{% if next_page %}
90+
<a href="{% url 'djs_docs:detail' slug=next_page.slug %}"
91+
hx-get="{% url 'djs_docs:detail' slug=next_page.slug %}"
92+
hx-target="#doc-main-content"
93+
hx-push-url="true"
94+
class="group flex-1 p-4 rounded-xl border border-base-300 hover:border-primary hover:bg-base-200 transition-all text-right">
95+
<div class="text-xs text-base-content/50 mb-1 group-hover:text-primary">下一篇</div>
96+
<div class="font-bold text-lg flex items-center justify-end gap-2">
97+
{{ next_page.title }}
98+
<i class="fas fa-arrow-right text-sm transition-transform group-hover:translate-x-1"></i>
99+
</div>
100+
</a>
101+
{% else %}
102+
<div class="flex-1 hidden sm:block"></div>
103+
{% endif %}
104+
</div>
105+
</div>
106+
107+
<!-- Right Sidebar (TOC) -->
108+
{% if toc %}
109+
<aside class="hidden xl:block w-64 shrink-0 pl-6 border-l border-base-200">
110+
<div class="sticky top-6">
111+
<div class="text-xs font-bold mb-4 uppercase text-base-content/50 tracking-wider">本页目录</div>
112+
<div class="prose prose-sm prose-ul:list-none prose-ul:pl-0 prose-li:pl-0 prose-a:no-underline prose-a:text-base-content/70 hover:prose-a:text-primary">
113+
{{ toc|safe }}
114+
</div>
115+
</div>
116+
</aside>
117+
{% endif %}
118+
</div>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!-- Mobile Header -->
2+
<div class="lg:hidden mb-6 px-2 flex items-center justify-between">
3+
<span class="font-bold text-lg">文档导航</span>
4+
<label for="docs-drawer" class="btn btn-sm btn-ghost btn-circle"><i class="fas fa-times"></i></label>
5+
</div>
6+
7+
{% for item in categories %}
8+
<details open class="group">
9+
<summary class="font-bold opacity-60 hover:opacity-100 group-open:text-primary transition-colors">{{ item.category.title }}</summary>
10+
<ul>
11+
{% for page in item.pages %}
12+
<li>
13+
<a href="{% url 'djs_docs:detail' slug=page.slug %}"
14+
hx-get="{% url 'djs_docs:detail' slug=page.slug %}"
15+
hx-target="#doc-main-content"
16+
hx-push-url="true"
17+
hx-swap="innerHTML"
18+
class="{% if active_page and page.slug == active_page.slug %}active font-medium{% endif %} border-l-2 border-transparent {% if active_page and page.slug == active_page.slug %}border-primary{% endif %}">
19+
{{ page.title }}
20+
</a>
21+
</li>
22+
{% endfor %}
23+
</ul>
24+
</details>
25+
{% endfor %}
26+
27+
<div class="divider my-4"></div>
28+
<ul class="menu-title px-2">快捷链接</ul>
29+
<li><a href="/api/docs" target="_blank"><i class="fas fa-code w-4"></i> API 文档</a></li>
30+
<li><a href="/admin" target="_blank"><i class="fas fa-cogs w-4"></i> 管理后台</a></li>
Lines changed: 48 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -1,145 +1,30 @@
11
{% extends "_base.html" %}
22
{% load static page_tags %}
33

4-
{% block title %}{{ title }} - DjangoStarter Docs{% endblock %}
4+
{% block title %}{{ page_title }} - DjangoStarter Docs{% endblock %}
55

66
{% block head %}
77
<!-- Highlight.js -->
88
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark-dimmed.min.css">
99
{% endblock %}
1010

1111
{% block content %}
12-
{% page_header title breadcrumbs %}
1312

1413
<div class="drawer lg:drawer-open min-h-[calc(100vh-10rem)] rounded-box border border-base-200 bg-base-100 shadow-sm mb-8">
1514
<input id="docs-drawer" type="checkbox" class="drawer-toggle" />
1615

1716
<!-- Main Content Area -->
18-
<div class="drawer-content flex flex-col xl:flex-row px-0">
19-
<!-- Mobile Toggle -->
20-
<div class="lg:hidden px-4 mb-4">
21-
<label for="docs-drawer" class="btn btn-primary drawer-button w-full">
22-
<i class="fas fa-bars mr-2"></i> 目录导航
23-
</label>
24-
</div>
25-
26-
<!-- Doc Content -->
27-
<div class="flex-1 min-w-0 px-4 lg:px-8">
28-
<!-- Search -->
29-
<div class="mb-8 max-w-2xl">
30-
<form method="get" action="{% url 'djs_docs:index' %}">
31-
<div class="join w-full shadow-sm">
32-
<input name="q" value="{{ search_query }}" placeholder="搜索文档、关键字..." class="input input-bordered join-item w-full focus:outline-none" />
33-
<button class="btn btn-primary join-item px-6"><i class="fas fa-search"></i></button>
34-
</div>
35-
</form>
36-
</div>
37-
38-
{% if search_results %}
39-
<div class="mb-8">
40-
<h3 class="font-bold text-lg mb-4">搜索结果: "{{ search_query }}"</h3>
41-
<div class="grid gap-4">
42-
{% for result in search_results %}
43-
<a href="{% url 'djs_docs:detail' slug=result.slug %}" class="card bg-base-200 hover:bg-base-300 transition-colors compact border border-base-300">
44-
<div class="card-body">
45-
<h2 class="card-title text-base">{{ result.title }}</h2>
46-
<p class="text-sm opacity-70">{{ result.summary }}</p>
47-
</div>
48-
</a>
49-
{% endfor %}
50-
{% if not search_results %}
51-
<div class="alert">
52-
<i class="fas fa-info-circle"></i>
53-
<span>未找到相关文档,请尝试更换关键词。</span>
54-
</div>
55-
{% endif %}
56-
</div>
57-
<div class="divider"></div>
58-
</div>
59-
{% endif %}
60-
61-
<article class="prose prose-base lg:prose-lg max-w-none prose-pre:p-0 prose-pre:bg-transparent prose-img:rounded-xl">
62-
{% if active_page and not search_results %}
63-
<div class="mb-8 not-prose">
64-
<h1 class="text-3xl lg:text-4xl font-extrabold mb-2">{{ active_page.title }}</h1>
65-
<p class="text-xl text-base-content/70">{{ active_page.summary }}</p>
66-
</div>
67-
{% endif %}
68-
69-
{{ active_html|safe }}
70-
</article>
71-
72-
<!-- Prev/Next Nav -->
73-
<div class="flex flex-col sm:flex-row justify-between mt-12 gap-4 pt-8 border-t border-base-200">
74-
{% if prev_page %}
75-
<a href="{% url 'djs_docs:detail' slug=prev_page.slug %}" class="group flex-1 p-4 rounded-xl border border-base-300 hover:border-primary hover:bg-base-200 transition-all text-left">
76-
<div class="text-xs text-base-content/50 mb-1 group-hover:text-primary">上一篇</div>
77-
<div class="font-bold text-lg flex items-center gap-2">
78-
<i class="fas fa-arrow-left text-sm transition-transform group-hover:-translate-x-1"></i>
79-
{{ prev_page.title }}
80-
</div>
81-
</a>
82-
{% else %}
83-
<div class="flex-1 hidden sm:block"></div>
84-
{% endif %}
85-
86-
{% if next_page %}
87-
<a href="{% url 'djs_docs:detail' slug=next_page.slug %}" class="group flex-1 p-4 rounded-xl border border-base-300 hover:border-primary hover:bg-base-200 transition-all text-right">
88-
<div class="text-xs text-base-content/50 mb-1 group-hover:text-primary">下一篇</div>
89-
<div class="font-bold text-lg flex items-center justify-end gap-2">
90-
{{ next_page.title }}
91-
<i class="fas fa-arrow-right text-sm transition-transform group-hover:translate-x-1"></i>
92-
</div>
93-
</a>
94-
{% else %}
95-
<div class="flex-1 hidden sm:block"></div>
96-
{% endif %}
97-
</div>
98-
</div>
99-
100-
<!-- Right Sidebar (TOC) -->
101-
{% if toc %}
102-
<aside class="hidden xl:block w-64 shrink-0 pl-6 border-l border-base-200">
103-
<div class="sticky top-6">
104-
<div class="text-xs font-bold mb-4 uppercase text-base-content/50 tracking-wider">本页目录</div>
105-
<div class="prose prose-sm prose-ul:list-none prose-ul:pl-0 prose-li:pl-0 prose-a:no-underline prose-a:text-base-content/70 hover:prose-a:text-primary">
106-
{{ toc|safe }}
107-
</div>
108-
</div>
109-
</aside>
110-
{% endif %}
17+
<!-- Added id for HTMX target -->
18+
<div id="doc-main-content" class="drawer-content">
19+
{% include "django_starter/docs/_doc_content.html" %}
11120
</div>
11221

11322
<!-- Left Sidebar (Drawer Side) -->
11423
<div class="drawer-side z-50 lg:z-auto">
11524
<label for="docs-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
116-
<div class="menu p-4 w-72 min-h-full bg-base-100 lg:bg-transparent text-base-content border-r lg:border-none border-base-200">
117-
<!-- Mobile Header -->
118-
<div class="lg:hidden mb-6 px-2 flex items-center justify-between">
119-
<span class="font-bold text-lg">文档导航</span>
120-
<label for="docs-drawer" class="btn btn-sm btn-ghost btn-circle"><i class="fas fa-times"></i></label>
121-
</div>
122-
123-
{% for item in categories %}
124-
<details open class="group">
125-
<summary class="font-bold opacity-60 hover:opacity-100 group-open:text-primary transition-colors">{{ item.category.title }}</summary>
126-
<ul>
127-
{% for page in item.pages %}
128-
<li>
129-
<a href="{% url 'djs_docs:detail' slug=page.slug %}"
130-
class="{% if active_page and page.slug == active_page.slug %}active font-medium{% endif %} border-l-2 border-transparent {% if active_page and page.slug == active_page.slug %}border-primary{% endif %}">
131-
{{ page.title }}
132-
</a>
133-
</li>
134-
{% endfor %}
135-
</ul>
136-
</details>
137-
{% endfor %}
138-
139-
<div class="divider my-4"></div>
140-
<ul class="menu-title px-2">快捷链接</ul>
141-
<li><a href="/api/docs" target="_blank"><i class="fas fa-code w-4"></i> API 文档</a></li>
142-
<li><a href="/admin" target="_blank"><i class="fas fa-cogs w-4"></i> 管理后台</a></li>
25+
<!-- Added id for HTMX OOB Swap -->
26+
<div id="doc-sidebar-menu" class="menu p-4 w-72 min-h-full bg-base-100 lg:bg-transparent text-base-content border-r lg:border-none border-base-200">
27+
{% include "django_starter/docs/_sidebar_menu.html" %}
14328
</div>
14429
</div>
14530
</div>
@@ -148,35 +33,49 @@ <h1 class="text-3xl lg:text-4xl font-extrabold mb-2">{{ active_page.title }}</h1
14833
{% block scripts %}
14934
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
15035
<script>
151-
// Init Highlight.js
152-
hljs.highlightAll();
36+
function initDocs() {
37+
// Init Highlight.js
38+
hljs.highlightAll();
39+
40+
// Copy Code Button Logic
41+
document.querySelectorAll('pre').forEach((pre) => {
42+
if (pre.parentNode.classList.contains('relative') && pre.parentNode.classList.contains('group')) {
43+
return; // Already initialized
44+
}
45+
46+
// Wrapper for positioning
47+
const wrapper = document.createElement('div');
48+
wrapper.className = 'relative group mb-4';
49+
50+
// Replace pre with wrapper in DOM
51+
pre.parentNode.insertBefore(wrapper, pre);
52+
wrapper.appendChild(pre);
53+
54+
// Styling the pre to look nice with the button
55+
pre.className += ' !bg-[#0d1117] !p-4 !rounded-xl !my-0'; // Force GitHub Dark bg
56+
57+
// Create button
58+
const btn = document.createElement('button');
59+
btn.className = 'btn btn-xs btn-square btn-ghost absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-all bg-base-100/10 hover:bg-base-100/20 text-white';
60+
btn.innerHTML = '<i class="fas fa-copy"></i>';
61+
btn.title = "复制代码";
62+
63+
btn.onclick = () => {
64+
const code = pre.querySelector('code').innerText;
65+
navigator.clipboard.writeText(code);
66+
btn.innerHTML = '<i class="fas fa-check text-success"></i>';
67+
setTimeout(() => btn.innerHTML = '<i class="fas fa-copy"></i>', 2000);
68+
};
69+
wrapper.appendChild(btn);
70+
});
71+
}
15372

154-
// Copy Code Button Logic
155-
document.querySelectorAll('pre').forEach((pre) => {
156-
// Wrapper for positioning
157-
const wrapper = document.createElement('div');
158-
wrapper.className = 'relative group mb-4';
159-
160-
// Replace pre with wrapper in DOM
161-
pre.parentNode.insertBefore(wrapper, pre);
162-
wrapper.appendChild(pre);
73+
// Initial load
74+
initDocs();
16375

164-
// Styling the pre to look nice with the button
165-
pre.className += ' !bg-[#0d1117] !p-4 !rounded-xl !my-0'; // Force GitHub Dark bg
166-
167-
// Create button
168-
const btn = document.createElement('button');
169-
btn.className = 'btn btn-xs btn-square btn-ghost absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-all bg-base-100/10 hover:bg-base-100/20 text-white';
170-
btn.innerHTML = '<i class="fas fa-copy"></i>';
171-
btn.title = "复制代码";
172-
173-
btn.onclick = () => {
174-
const code = pre.querySelector('code').innerText;
175-
navigator.clipboard.writeText(code);
176-
btn.innerHTML = '<i class="fas fa-check text-success"></i>';
177-
setTimeout(() => btn.innerHTML = '<i class="fas fa-copy"></i>', 2000);
178-
};
179-
wrapper.appendChild(btn);
76+
// Re-init on HTMX swap
77+
document.body.addEventListener('htmx:afterSwap', function(evt) {
78+
initDocs();
18079
});
18180
</script>
18281
{% endblock %}

0 commit comments

Comments
 (0)