@claudelaw/taichu 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +13 -0
- package/Dockerfile +51 -0
- package/LICENSE +21 -0
- package/README.md +208 -0
- package/docker-compose.yml +42 -0
- package/docs/ROADMAP.md +101 -0
- package/docs/api/README.md +102 -0
- package/docs/architecture/001-zero-dependency-core.md +61 -0
- package/docs/architecture/002-structured-content-model.md +70 -0
- package/docs/architecture/003-hook-based-extension.md +82 -0
- package/docs/architecture/004-api-first-architecture.md +122 -0
- package/docs/architecture/README.md +24 -0
- package/docs/logo.svg +40 -0
- package/docs/research/ai-era-cms-user-research.md +247 -0
- package/docs/zh/README.md +81 -0
- package/docs/zh/guides/deploy.md +75 -0
- package/docs/zh/guides/mcp.md +84 -0
- package/docs/zh/guides/promotion.md +51 -0
- package/marketplace.json +78 -0
- package/package.json +60 -0
- package/packages/core/src/auth.js +158 -0
- package/packages/core/src/content-type.js +244 -0
- package/packages/core/src/core.test.js +406 -0
- package/packages/core/src/errors.js +60 -0
- package/packages/core/src/hooks.js +104 -0
- package/packages/core/src/index.js +16 -0
- package/packages/core/src/server.test.js +149 -0
- package/packages/core/src/sm-crypto.js +31 -0
- package/packages/core/src/sqlite-store.js +354 -0
- package/packages/core/src/store.js +174 -0
- package/packages/core/src/tokenizer.js +89 -0
- package/packages/core/src/vector-index.js +131 -0
- package/packages/llm-providers/src/index.js +181 -0
- package/packages/mcp/src/index.js +355 -0
- package/packages/server/public/admin/assets/index-DApxOVTx.js +191 -0
- package/packages/server/public/admin/assets/index-DtMvdQm9.css +1 -0
- package/packages/server/public/admin/index.html +28 -0
- package/packages/server/public/aurora/style.css +1173 -0
- package/packages/server/public/favicon.svg +46 -0
- package/packages/server/public/theme/index.html +288 -0
- package/packages/server/public/theme/style.css +133 -0
- package/packages/server/public/theme-minimal/index.html +223 -0
- package/packages/server/public/theme-minimal/style.css +109 -0
- package/packages/server/public/ws-test.html +106 -0
- package/packages/server/src/activitypub.js +228 -0
- package/packages/server/src/audit.js +104 -0
- package/packages/server/src/auth-provider.js +76 -0
- package/packages/server/src/body-parser.js +52 -0
- package/packages/server/src/bootstrap.js +272 -0
- package/packages/server/src/collab.js +154 -0
- package/packages/server/src/config.js +136 -0
- package/packages/server/src/context.js +86 -0
- package/packages/server/src/email.js +317 -0
- package/packages/server/src/index.js +195 -0
- package/packages/server/src/logger.js +78 -0
- package/packages/server/src/media-store.js +213 -0
- package/packages/server/src/middleware/auth.js +203 -0
- package/packages/server/src/middleware/cors.js +15 -0
- package/packages/server/src/middleware/error-handler.js +49 -0
- package/packages/server/src/middleware/rate-limit.js +118 -0
- package/packages/server/src/multipart.js +150 -0
- package/packages/server/src/notify.js +126 -0
- package/packages/server/src/pipeline.js +206 -0
- package/packages/server/src/plugin-installer.js +139 -0
- package/packages/server/src/plugin-manager.js +165 -0
- package/packages/server/src/relationships.js +217 -0
- package/packages/server/src/revisions.js +114 -0
- package/packages/server/src/router.js +194 -0
- package/packages/server/src/routes/activitypub.js +140 -0
- package/packages/server/src/routes/api.js +363 -0
- package/packages/server/src/routes/audit.js +222 -0
- package/packages/server/src/routes/auth.js +205 -0
- package/packages/server/src/routes/collab.js +90 -0
- package/packages/server/src/routes/export.js +77 -0
- package/packages/server/src/routes/graphql.js +344 -0
- package/packages/server/src/routes/media.js +169 -0
- package/packages/server/src/routes/plugin-marketplace.js +171 -0
- package/packages/server/src/routes/relationships.js +133 -0
- package/packages/server/src/routes/rss.js +92 -0
- package/packages/server/src/routes/sso.js +211 -0
- package/packages/server/src/routes/theme.js +119 -0
- package/packages/server/src/routes/webhook.js +94 -0
- package/packages/server/src/routes/wechat.js +115 -0
- package/packages/server/src/routes/workflow.js +157 -0
- package/packages/server/src/scheduler.js +96 -0
- package/packages/server/src/search.js +100 -0
- package/packages/server/src/server.test.js +295 -0
- package/packages/server/src/sso-analytics.js +78 -0
- package/packages/server/src/static.js +70 -0
- package/packages/server/src/theme-engine.js +119 -0
- package/packages/server/src/webhook.js +192 -0
- package/packages/server/src/websocket.js +308 -0
- package/scripts/cli.js +90 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
2
|
+
<defs>
|
|
3
|
+
<!-- 3D sphere highlight -->
|
|
4
|
+
<radialGradient id="sphere-hi" cx="0.36" cy="0.30" r="0.68" fx="0.36" fy="0.30">
|
|
5
|
+
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.40"/>
|
|
6
|
+
<stop offset="35%" stop-color="#ffffff" stop-opacity="0.10"/>
|
|
7
|
+
<stop offset="100%" stop-color="#000000" stop-opacity="0.20"/>
|
|
8
|
+
</radialGradient>
|
|
9
|
+
<!-- Rim shadow for depth -->
|
|
10
|
+
<radialGradient id="rim" cx="0.5" cy="0.5" r="0.5">
|
|
11
|
+
<stop offset="82%" stop-color="#000000" stop-opacity="0"/>
|
|
12
|
+
<stop offset="100%" stop-color="#000000" stop-opacity="0.22"/>
|
|
13
|
+
</radialGradient>
|
|
14
|
+
<!-- Subtle ambient glow -->
|
|
15
|
+
<filter id="softglow" x="-10%" y="-10%" width="120%" height="120%">
|
|
16
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation="6" result="blur"/>
|
|
17
|
+
<feMerge>
|
|
18
|
+
<feMergeNode in="blur"/>
|
|
19
|
+
<feMergeNode in="SourceGraphic"/>
|
|
20
|
+
</feMerge>
|
|
21
|
+
</filter>
|
|
22
|
+
<clipPath id="ball">
|
|
23
|
+
<circle cx="256" cy="256" r="232"/>
|
|
24
|
+
</clipPath>
|
|
25
|
+
</defs>
|
|
26
|
+
|
|
27
|
+
<!-- Yin-yang ball -->
|
|
28
|
+
<g clip-path="url(#ball)" filter="url(#softglow)">
|
|
29
|
+
<!-- Dark half — full circle fill -->
|
|
30
|
+
<circle cx="256" cy="256" r="232" fill="#1E1B4B"/>
|
|
31
|
+
<!-- Light half — left semicircle -->
|
|
32
|
+
<path d="M256,24 A232,232 0 0,0 256,488" fill="#EDE9FE"/>
|
|
33
|
+
<!-- Upper bulge: light extends right -->
|
|
34
|
+
<circle cx="256" cy="140" r="116" fill="#EDE9FE"/>
|
|
35
|
+
<!-- Lower bulge: dark extends left -->
|
|
36
|
+
<circle cx="256" cy="372" r="116" fill="#1E1B4B"/>
|
|
37
|
+
<!-- Upper eye: dark dot in light area -->
|
|
38
|
+
<circle cx="256" cy="140" r="34" fill="#1E1B4B"/>
|
|
39
|
+
<!-- Lower eye: light dot in dark area -->
|
|
40
|
+
<circle cx="256" cy="372" r="34" fill="#EDE9FE"/>
|
|
41
|
+
</g>
|
|
42
|
+
|
|
43
|
+
<!-- 3D sphere shading -->
|
|
44
|
+
<circle cx="256" cy="256" r="232" fill="url(#sphere-hi)"/>
|
|
45
|
+
<circle cx="256" cy="256" r="232" fill="url(#rim)"/>
|
|
46
|
+
</svg>
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title data-taichu="siteName">Taichu CMS</title>
|
|
7
|
+
<meta name="description" content="">
|
|
8
|
+
<link rel="stylesheet" href="/theme/style.css">
|
|
9
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
10
|
+
<link rel="alternate" type="application/rss+xml" href="/rss.xml" title="RSS Feed">
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<header class="site-header">
|
|
14
|
+
<div class="header-inner">
|
|
15
|
+
<a href="/" class="site-logo">Taichu</a>
|
|
16
|
+
<nav class="site-nav" id="nav"></nav>
|
|
17
|
+
</div>
|
|
18
|
+
</header>
|
|
19
|
+
|
|
20
|
+
<main class="site-main" id="main"></main>
|
|
21
|
+
|
|
22
|
+
<footer class="site-footer">
|
|
23
|
+
<div class="footer-inner">
|
|
24
|
+
<p>Powered by <a href="https://github.com/Caludelaw/Taichu" target="_blank">Taichu CMS</a></p>
|
|
25
|
+
<p class="icp" id="icp"></p>
|
|
26
|
+
</div>
|
|
27
|
+
</footer>
|
|
28
|
+
|
|
29
|
+
<script>
|
|
30
|
+
(function() {
|
|
31
|
+
var G = window.__TAICHU__ || {};
|
|
32
|
+
|
|
33
|
+
// ── Init ──────────────────────────────────────────────────
|
|
34
|
+
document.title = G.site?.name || 'Taichu CMS';
|
|
35
|
+
document.documentElement.lang = G.site?.language || 'zh-CN';
|
|
36
|
+
document.querySelectorAll('[data-taichu=siteName]').forEach(function(el){ el.textContent = G.site?.name || 'Taichu' });
|
|
37
|
+
|
|
38
|
+
// ICP
|
|
39
|
+
var icpEl = document.getElementById('icp');
|
|
40
|
+
if (icpEl && G.site?.icp) {
|
|
41
|
+
icpEl.innerHTML = '<a href="https://beian.miit.gov.cn" target="_blank">'+G.site.icp+'</a>' + (G.site.gongan ? ' | '+G.site.gongan : '');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Theme CSS vars
|
|
45
|
+
var theme = G.theme || {};
|
|
46
|
+
var rs = document.documentElement.style;
|
|
47
|
+
if (theme.primaryColor) rs.setProperty('--accent', theme.primaryColor);
|
|
48
|
+
if (theme.bgColor) rs.setProperty('--bg', theme.bgColor);
|
|
49
|
+
if (theme.textColor) rs.setProperty('--text', theme.textColor);
|
|
50
|
+
if (theme.fontFamily) rs.setProperty('--font', theme.fontFamily);
|
|
51
|
+
if (theme.fontSize) rs.setProperty('--font-size', theme.fontSize);
|
|
52
|
+
if (theme.maxWidth) rs.setProperty('--max-width', theme.maxWidth);
|
|
53
|
+
if (theme.customCSS) { var s = document.createElement('style'); s.textContent = theme.customCSS; document.head.appendChild(s); }
|
|
54
|
+
|
|
55
|
+
// ── API ───────────────────────────────────────────────────
|
|
56
|
+
function api(path) { return fetch('/api'+path).then(function(r){ return r.json() }); }
|
|
57
|
+
function getPage() { var m = location.search.match(/page=(\d+)/); return m ? parseInt(m[1]) : 1; }
|
|
58
|
+
var PAGE_SIZE = 10;
|
|
59
|
+
|
|
60
|
+
// ── Pagination Controls ───────────────────────────────────
|
|
61
|
+
function paginationHTML(page, total, linkPrefix) {
|
|
62
|
+
var pages = Math.ceil(total / PAGE_SIZE);
|
|
63
|
+
if (pages <= 1) return '';
|
|
64
|
+
var prefix = linkPrefix || '?';
|
|
65
|
+
var html = '<nav class="pagination">';
|
|
66
|
+
if (page > 1) html += '<a href="' + prefix + 'page=' + (page-1) + '" class="btn-page">« 上一页</a>';
|
|
67
|
+
else html += '<span class="btn-page disabled">« 上一页</span>';
|
|
68
|
+
html += '<span class="page-info">第 ' + page + ' / ' + pages + ' 页 (共 ' + total + ' 篇)</span>';
|
|
69
|
+
if (page < pages) html += '<a href="' + prefix + 'page=' + (page+1) + '" class="btn-page">下一页 »</a>';
|
|
70
|
+
else html += '<span class="btn-page disabled">下一页 »</span>';
|
|
71
|
+
html += '</nav>';
|
|
72
|
+
return html;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Router ────────────────────────────────────────────────
|
|
76
|
+
function route() {
|
|
77
|
+
var p = location.pathname;
|
|
78
|
+
if (p === '/' || p === '') return renderHome();
|
|
79
|
+
if (p.startsWith('/post/')) return renderPost(p.split('/post/')[1]);
|
|
80
|
+
if (p.startsWith('/page/')) return renderPage(p.split('/page/')[1]);
|
|
81
|
+
if (p.startsWith('/category/')) return renderCategory(p.split('/category/')[1]);
|
|
82
|
+
if (p === '/search') return renderSearch();
|
|
83
|
+
render404();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Navigation (from CMS navigation type + categories) ───
|
|
87
|
+
function buildNav(navItems, categories, pages) {
|
|
88
|
+
var nav = document.getElementById('nav');
|
|
89
|
+
var html = '<a href="/">首页</a>';
|
|
90
|
+
(navItems||[]).forEach(function(item){
|
|
91
|
+
html += '<a href="'+escAttr(item.data?.url||'#')+'"'+(item.data?.target==='_blank'?' target="_blank"':'')+'>'+escHtml(item.data?.title||'')+'</a>';
|
|
92
|
+
});
|
|
93
|
+
if (!navItems || !navItems.length) {
|
|
94
|
+
(categories||[]).forEach(function(c){
|
|
95
|
+
html += '<a href="/category/'+(c.data?.slug||c.data?.name||c.id)+'">'+escHtml(c.data?.name||c.data?.title||c.id)+'</a>';
|
|
96
|
+
});
|
|
97
|
+
(pages||[]).forEach(function(p){
|
|
98
|
+
html += '<a href="/page/'+(p.data?.slug||p.id)+'">'+escHtml(p.data?.title||p.id)+'</a>';
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
html += '<a href="/search">🔍</a>';
|
|
102
|
+
html += '<a href="/admin/">管理</a>';
|
|
103
|
+
nav.innerHTML = html;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Home ──────────────────────────────────────────────────
|
|
107
|
+
function renderHome() {
|
|
108
|
+
var main = document.getElementById('main');
|
|
109
|
+
var page = getPage();
|
|
110
|
+
var offset = (page - 1) * PAGE_SIZE;
|
|
111
|
+
main.innerHTML = '<div class="loading">加载中...</div>';
|
|
112
|
+
|
|
113
|
+
Promise.all([
|
|
114
|
+
api('/content/article?status=published&limit=' + PAGE_SIZE + '&offset=' + offset),
|
|
115
|
+
api('/content/article?status=published&limit=1'),
|
|
116
|
+
api('/content/navigation?limit=20'),
|
|
117
|
+
api('/content/category?limit=20')
|
|
118
|
+
]).then(function(res){
|
|
119
|
+
var articles = res[0].docs || [];
|
|
120
|
+
var total = res[1].total || 0;
|
|
121
|
+
var navItems = (res[2].docs||[]).sort(function(a,b){ return (a.data?.order||0)-(b.data?.order||0) });
|
|
122
|
+
var categories = res[3].docs || [];
|
|
123
|
+
|
|
124
|
+
buildNav(navItems, categories, []);
|
|
125
|
+
|
|
126
|
+
if (!articles.length && page === 1) {
|
|
127
|
+
main.innerHTML = '<div class="empty-page"><h1>👋 欢迎</h1><p>还没有文章,去 <a href="/admin/">管理后台</a> 写第一篇。</p></div>';
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
var html = '<div class="post-list">';
|
|
131
|
+
articles.forEach(function(a){
|
|
132
|
+
var ex = excerpt(a.data?.body);
|
|
133
|
+
html += '<article class="post-card"><h2><a href="/post/'+(a.data?.slug||a.id)+'">'+escHtml(a.data?.title||'Untitled')+'</a></h2><time>'+fmtDate(a.updatedAt)+'</time><p>'+ex+'</p></article>';
|
|
134
|
+
});
|
|
135
|
+
html += '</div>';
|
|
136
|
+
html += paginationHTML(page, total, '?');
|
|
137
|
+
main.innerHTML = html;
|
|
138
|
+
}).catch(function(e){ main.innerHTML = '<p class="error">加载失败: '+e.message+'</p>'; });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Single Post ───────────────────────────────────────────
|
|
142
|
+
function renderPost(slug) {
|
|
143
|
+
var main = document.getElementById('main');
|
|
144
|
+
main.innerHTML = '<div class="loading">加载中...</div>';
|
|
145
|
+
|
|
146
|
+
api('/content/article?limit=100').then(function(data){
|
|
147
|
+
var docs = data.docs || [];
|
|
148
|
+
var doc = docs.find(function(d){ return d.data?.slug === slug });
|
|
149
|
+
if (!doc) { render404(); return; }
|
|
150
|
+
main.innerHTML = '<article class="post-single"><h1>'+escHtml(doc.data?.title||'')+'</h1><time>'+fmtDate(doc.updatedAt)+'</time><div class="post-body">'+renderBody(doc.data?.body)+'</div></article>';
|
|
151
|
+
document.title = (doc.data?.title||'') + ' — ' + (G.site?.name||'Taichu');
|
|
152
|
+
// CTA
|
|
153
|
+
if (doc.data?.status !== 'published') {
|
|
154
|
+
main.innerHTML += '<p class="cta-note">此内容未发布。</p>';
|
|
155
|
+
}
|
|
156
|
+
}).catch(function(e){ main.innerHTML = '<p class="error">加载失败</p>'; });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Static Page ───────────────────────────────────────────
|
|
160
|
+
function renderPage(slug) {
|
|
161
|
+
var main = document.getElementById('main');
|
|
162
|
+
main.innerHTML = '<div class="loading">加载中...</div>';
|
|
163
|
+
|
|
164
|
+
api('/content/page?limit=100').then(function(data){
|
|
165
|
+
var docs = data.docs || [];
|
|
166
|
+
var doc = docs.find(function(d){ return d.data?.slug === slug });
|
|
167
|
+
if (!doc) { render404(); return; }
|
|
168
|
+
main.innerHTML = '<article class="post-single"><h1>'+escHtml(doc.data?.title||'')+'</h1><div class="post-body">'+renderBody(doc.data?.body)+'</div></article>';
|
|
169
|
+
document.title = (doc.data?.title||'') + ' — ' + (G.site?.name||'Taichu');
|
|
170
|
+
}).catch(function(e){ main.innerHTML = '<p class="error">加载失败</p>'; });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Category ──────────────────────────────────────────────
|
|
174
|
+
function renderCategory(slug) {
|
|
175
|
+
var main = document.getElementById('main');
|
|
176
|
+
main.innerHTML = '<div class="loading">加载中...</div>';
|
|
177
|
+
|
|
178
|
+
api('/content/article?status=published&limit=50').then(function(data){
|
|
179
|
+
var articles = data.docs || [];
|
|
180
|
+
var name = slug;
|
|
181
|
+
|
|
182
|
+
var cats = api('/content/category?limit=50').then(function(cdata){
|
|
183
|
+
var c = (cdata.docs||[]).find(function(x){ return (x.data?.slug||'') === slug });
|
|
184
|
+
if (c) name = c.data?.name || slug;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
cats.then(function(){
|
|
188
|
+
var filtered = articles.filter(function(a){
|
|
189
|
+
var tags = a.data?.tags || [];
|
|
190
|
+
return tags.some(function(t){ return t.toLowerCase() === slug.toLowerCase() }) || a.data?.categoryId;
|
|
191
|
+
});
|
|
192
|
+
var html = '<h1 class="page-title">分类: '+escHtml(name)+'</h1><div class="post-list">';
|
|
193
|
+
filtered.forEach(function(a){
|
|
194
|
+
var ex = excerpt(a.data?.body);
|
|
195
|
+
html += '<article class="post-card"><h2><a href="/post/'+(a.data?.slug||a.id)+'">'+escHtml(a.data?.title||'')+'</a></h2><time>'+fmtDate(a.updatedAt)+'</time><p>'+ex+'</p></article>';
|
|
196
|
+
});
|
|
197
|
+
html += '</div>';
|
|
198
|
+
main.innerHTML = html;
|
|
199
|
+
});
|
|
200
|
+
}).catch(function(){ main.innerHTML = '<p class="error">加载失败</p>'; });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Search ────────────────────────────────────────────────
|
|
204
|
+
function renderSearch() {
|
|
205
|
+
var main = document.getElementById('main');
|
|
206
|
+
var q = (new URL(location.href)).searchParams.get('q') || '';
|
|
207
|
+
main.innerHTML = '<h1 class="page-title">🔍 搜索</h1><form onsubmit="event.preventDefault();location.href=\'/search?q=\'+encodeURIComponent(this.q.value)" class="search-form"><input name="q" value="'+escAttr(q)+'" class="search-input" placeholder="输入关键词..." autofocus><button class="btn-primary">搜索</button></form><div id="search-results"></div>';
|
|
208
|
+
|
|
209
|
+
if (!q) return;
|
|
210
|
+
document.getElementById('search-results').innerHTML = '<div class="loading">搜索中...</div>';
|
|
211
|
+
|
|
212
|
+
api('/search?q='+encodeURIComponent(q)).then(function(data){
|
|
213
|
+
var docs = data.docs || [];
|
|
214
|
+
var results = document.getElementById('search-results');
|
|
215
|
+
if (!docs.length) { results.innerHTML = '<p class="empty">未找到相关结果</p>'; return; }
|
|
216
|
+
var html = '<div class="post-list">';
|
|
217
|
+
docs.forEach(function(d){
|
|
218
|
+
var ex = excerpt(d.data?.body);
|
|
219
|
+
html += '<article class="post-card"><h2><a href="/post/'+(d.data?.slug||d.id)+'">'+escHtml(d.data?.title||'')+' <small class="score">'+Math.round(d._score*100)+'%</small></a></h2><p>'+ex+'</p></article>';
|
|
220
|
+
});
|
|
221
|
+
html += '</div>';
|
|
222
|
+
results.innerHTML = html;
|
|
223
|
+
}).catch(function(e){ document.getElementById('search-results').innerHTML = '<p class="error">搜索失败</p>'; });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── 404 ───────────────────────────────────────────────────
|
|
227
|
+
function render404() {
|
|
228
|
+
document.getElementById('main').innerHTML = '<div class="empty-page"><h1>404</h1><p>页面未找到</p><a href="/">返回首页</a></div>';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Body Renderer (TipTap JSON → HTML) ────────────────────
|
|
232
|
+
function renderBody(body) {
|
|
233
|
+
if (!body) return '';
|
|
234
|
+
if (typeof body === 'string') return '<p>'+escHtml(body)+'</p>';
|
|
235
|
+
if (body.type === 'doc' && Array.isArray(body.content)) {
|
|
236
|
+
return body.content.map(renderNode).join('');
|
|
237
|
+
}
|
|
238
|
+
if (body.text) return '<p>'+escHtml(String(body.text||body))+'</p>';
|
|
239
|
+
return '';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function renderNode(node) {
|
|
243
|
+
if (!node) return '';
|
|
244
|
+
var text = '';
|
|
245
|
+
if (node.content) text = node.content.map(renderNode).join('');
|
|
246
|
+
else if (node.text) text = escHtml(String(node.text));
|
|
247
|
+
if (node.marks) {
|
|
248
|
+
node.marks.forEach(function(m){
|
|
249
|
+
if (m.type === 'bold') text = '<strong>'+text+'</strong>';
|
|
250
|
+
if (m.type === 'italic') text = '<em>'+text+'</em>';
|
|
251
|
+
if (m.type === 'code') text = '<code>'+text+'</code>';
|
|
252
|
+
if (m.type === 'link') text = '<a href="'+escAttr(m.attrs?.href||'#')+'" target="_blank">'+text+'</a>';
|
|
253
|
+
if (m.type === 'strike') text = '<s>'+text+'</s>';
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
switch (node.type) {
|
|
257
|
+
case 'paragraph': return '<p>'+text+'</p>';
|
|
258
|
+
case 'heading': return '<h'+(node.attrs?.level||2)+'>'+text+'</h'+(node.attrs?.level||2)+'>';
|
|
259
|
+
case 'bulletList': return '<ul>'+text+'</ul>';
|
|
260
|
+
case 'orderedList': return '<ol>'+text+'</ol>';
|
|
261
|
+
case 'listItem': return '<li>'+text+'</li>';
|
|
262
|
+
case 'blockquote': return '<blockquote>'+text+'</blockquote>';
|
|
263
|
+
case 'codeBlock': return '<pre><code>'+text+'</code></pre>';
|
|
264
|
+
case 'horizontalRule': return '<hr>';
|
|
265
|
+
case 'image': return '<img src="'+escAttr(node.attrs?.src||'')+'" alt="'+escAttr(node.attrs?.alt||'')+'" loading="lazy">';
|
|
266
|
+
default: return text;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Utils ─────────────────────────────────────────────────
|
|
271
|
+
function excerpt(body) {
|
|
272
|
+
if (!body) return '';
|
|
273
|
+
if (typeof body === 'string') return body.replace(/<[^>]+>/g,'').substring(0,200)+(body.length>200?'...':'');
|
|
274
|
+
if (body.text) return escHtml(String(body.text)).substring(0,200);
|
|
275
|
+
if (body.content) return body.content.map(function(n){ return n.text||'' }).join(' ').substring(0,200)+(JSON.stringify(body).length>200?'...':'');
|
|
276
|
+
return '';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function fmtDate(d) { if (!d) return ''; return new Date(d).toLocaleDateString('zh-CN',{year:'numeric',month:'long',day:'numeric'}); }
|
|
280
|
+
function escHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
281
|
+
function escAttr(s) { return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,'''); }
|
|
282
|
+
|
|
283
|
+
// ── Boot ──────────────────────────────────────────────────
|
|
284
|
+
route();
|
|
285
|
+
})();
|
|
286
|
+
</script>
|
|
287
|
+
</body>
|
|
288
|
+
</html>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/* Taichu Default Theme — Editorial Blog */
|
|
2
|
+
:root {
|
|
3
|
+
--accent: #10B981;
|
|
4
|
+
--bg: #ffffff;
|
|
5
|
+
--text: #111827;
|
|
6
|
+
--text-secondary: #6B7280;
|
|
7
|
+
--border: #E5E7EB;
|
|
8
|
+
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", sans-serif;
|
|
9
|
+
--font-size: 16px;
|
|
10
|
+
--max-width: 800px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
14
|
+
body {
|
|
15
|
+
font-family: var(--font);
|
|
16
|
+
font-size: var(--font-size);
|
|
17
|
+
color: var(--text);
|
|
18
|
+
background: var(--bg);
|
|
19
|
+
line-height: 1.7;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Header */
|
|
23
|
+
.site-header {
|
|
24
|
+
border-bottom: 1px solid var(--border);
|
|
25
|
+
background: var(--bg);
|
|
26
|
+
position: sticky; top: 0; z-index: 10;
|
|
27
|
+
}
|
|
28
|
+
.header-inner, .footer-inner {
|
|
29
|
+
max-width: var(--max-width);
|
|
30
|
+
margin: 0 auto;
|
|
31
|
+
padding: 16px 24px;
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: space-between;
|
|
35
|
+
flex-wrap: wrap;
|
|
36
|
+
gap: 12px;
|
|
37
|
+
}
|
|
38
|
+
.site-logo {
|
|
39
|
+
font-size: 20px;
|
|
40
|
+
font-weight: 700;
|
|
41
|
+
color: var(--accent);
|
|
42
|
+
text-decoration: none;
|
|
43
|
+
letter-spacing: -0.5px;
|
|
44
|
+
}
|
|
45
|
+
.site-nav { display: flex; gap: 20px; flex-wrap: wrap; }
|
|
46
|
+
.site-nav a {
|
|
47
|
+
color: var(--text-secondary);
|
|
48
|
+
text-decoration: none;
|
|
49
|
+
font-size: 14px;
|
|
50
|
+
font-weight: 500;
|
|
51
|
+
transition: color 0.15s;
|
|
52
|
+
}
|
|
53
|
+
.site-nav a:hover { color: var(--accent); }
|
|
54
|
+
|
|
55
|
+
/* Main */
|
|
56
|
+
.site-main {
|
|
57
|
+
max-width: var(--max-width);
|
|
58
|
+
margin: 0 auto;
|
|
59
|
+
padding: 48px 24px;
|
|
60
|
+
min-height: 60vh;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Post List */
|
|
64
|
+
.post-list { display: flex; flex-direction: column; gap: 40px; }
|
|
65
|
+
.post-card h2 { font-size: 22px; line-height: 1.3; margin-bottom: 8px; }
|
|
66
|
+
.post-card h2 a { color: var(--text); text-decoration: none; font-weight: 700; }
|
|
67
|
+
.post-card h2 a:hover { color: var(--accent); }
|
|
68
|
+
.post-card time { font-size: 13px; color: var(--text-secondary); }
|
|
69
|
+
.post-card p { margin-top: 8px; color: var(--text-secondary); font-size: 15px; line-height: 1.6; }
|
|
70
|
+
|
|
71
|
+
/* Post Single */
|
|
72
|
+
.post-single h1 { font-size: 32px; line-height: 1.2; margin-bottom: 12px; font-weight: 800; letter-spacing: -0.5px; }
|
|
73
|
+
.post-single time { color: var(--text-secondary); font-size: 14px; margin-bottom: 32px; display: block; }
|
|
74
|
+
.post-body p { margin-bottom: 20px; font-size: 17px; line-height: 1.8; }
|
|
75
|
+
.post-body h2, .post-body h3 { margin: 32px 0 12px; font-weight: 700; }
|
|
76
|
+
.post-body h2 { font-size: 24px; }
|
|
77
|
+
.post-body h3 { font-size: 20px; }
|
|
78
|
+
.post-body ul, .post-body ol { margin: 12px 0 20px 24px; }
|
|
79
|
+
.post-body li { margin-bottom: 6px; }
|
|
80
|
+
.post-body blockquote {
|
|
81
|
+
border-left: 3px solid var(--accent);
|
|
82
|
+
padding: 12px 20px;
|
|
83
|
+
margin: 24px 0;
|
|
84
|
+
background: #F9FAFB;
|
|
85
|
+
color: var(--text-secondary);
|
|
86
|
+
font-style: italic;
|
|
87
|
+
}
|
|
88
|
+
.post-body pre { background: #1F2937; color: #E5E7EB; padding: 16px 20px; border-radius: 8px; overflow-x: auto; font-size: 14px; margin: 20px 0; }
|
|
89
|
+
.post-body code { background: #F3F4F6; padding: 2px 6px; border-radius: 4px; font-size: 14px; }
|
|
90
|
+
.post-body pre code { background: none; padding: 0; }
|
|
91
|
+
.post-body img { max-width: 100%; border-radius: 8px; margin: 16px 0; }
|
|
92
|
+
.post-body hr { border: none; border-top: 1px solid var(--border); margin: 32px 0; }
|
|
93
|
+
.post-body a { color: var(--accent); }
|
|
94
|
+
|
|
95
|
+
/* Footer */
|
|
96
|
+
.site-footer { border-top: 1px solid var(--border); padding: 32px 0; margin-top: 64px; }
|
|
97
|
+
.footer-inner { display: block; text-align: center; }
|
|
98
|
+
.footer-inner p { font-size: 13px; color: var(--text-secondary); margin-bottom: 4px; }
|
|
99
|
+
.footer-inner a { color: var(--accent); text-decoration: none; }
|
|
100
|
+
.icp { color: var(--text-secondary); font-size: 12px; }
|
|
101
|
+
.icp a { color: var(--text-secondary); }
|
|
102
|
+
|
|
103
|
+
/* States */
|
|
104
|
+
.loading { text-align: center; padding: 80px 0; color: var(--text-secondary); }
|
|
105
|
+
.error { color: #EF4444; padding: 40px; text-align: center; }
|
|
106
|
+
.empty-page { text-align: center; padding: 80px 0; }
|
|
107
|
+
.empty-page h1 { font-size: 64px; color: var(--text-secondary); margin-bottom: 8px; }
|
|
108
|
+
.empty-page p { color: var(--text-secondary); margin-bottom: 24px; }
|
|
109
|
+
.empty-page a { color: var(--accent); font-weight: 600; }
|
|
110
|
+
|
|
111
|
+
.page-title { font-size: 28px; margin-bottom: 32px; font-weight: 700; }
|
|
112
|
+
|
|
113
|
+
/* Pagination */
|
|
114
|
+
.pagination {
|
|
115
|
+
display: flex; justify-content: center; align-items: center; gap: 16px;
|
|
116
|
+
margin-top: 48px; padding: 24px 0; border-top: 1px solid var(--border);
|
|
117
|
+
}
|
|
118
|
+
.btn-page {
|
|
119
|
+
padding: 8px 20px; background: #F9FAFB; border: 1px solid var(--border);
|
|
120
|
+
border-radius: 8px; font-size: 14px; color: var(--text-secondary);
|
|
121
|
+
text-decoration: none; cursor: pointer; transition: all 0.15s;
|
|
122
|
+
}
|
|
123
|
+
.btn-page:hover { border-color: var(--accent); color: var(--accent); background: #F0FDF4; }
|
|
124
|
+
.btn-page.disabled { opacity: 0.3; cursor: default; }
|
|
125
|
+
.page-info { font-size: 13px; color: var(--text-secondary); }
|
|
126
|
+
|
|
127
|
+
/* Mobile */
|
|
128
|
+
@media (max-width: 768px) {
|
|
129
|
+
.site-main { padding: 32px 16px; }
|
|
130
|
+
.post-single h1 { font-size: 24px; }
|
|
131
|
+
.post-card h2 { font-size: 18px; }
|
|
132
|
+
.header-inner { flex-direction: column; align-items: flex-start; }
|
|
133
|
+
}
|