Add VitePress site with local login and deployment docs

Enable static site build on port 12100 with Express session auth, auto sidebar, and DEPLOY.md for Ubuntu/NPS setup.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-05 16:12:35 +08:00
parent 2a1dc95253
commit 68d816b3f3
102 changed files with 4419 additions and 7 deletions
+7
View File
@@ -32,3 +32,10 @@ google-services.json
# Android Profiling # Android Profiling
*.hprof *.hprof
# Node / VitePress
node_modules/
.vitepress/dist/
.vitepress/cache/
.vitepress/public/
server/auth.config.json
+47
View File
@@ -0,0 +1,47 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitepress'
import { missingAssetsPlugin } from './missing-assets-plugin.mjs'
import { generateSidebar } from './sidebar.mts'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
export default defineConfig({
title: 'DAO DE JING',
description: '传统文化典籍资料库',
lang: 'zh-CN',
srcDir: '.',
srcExclude: [
'**/node_modules/**',
'**/.vitepress/**',
'**/server/**',
'**/金瓶梅/**',
'**/黄帝内经/**',
'.env.production',
'**/健康学习到150岁 - 人体系统调优不完全指南/**',
'**/梅花/**',
],
cleanUrls: true,
ignoreDeadLinks: true,
themeConfig: {
nav: [
{ text: '首页', link: '/' },
{ text: '五行', link: '/金、木、水、火、土%20-%20五行/' },
{ text: '道德经', link: '/道德经/01' },
{ text: '周易', link: '/周易/' },
{ text: '中医', link: '/中医宝典/' },
],
sidebar: generateSidebar(),
outline: { level: [2, 4] },
search: {
provider: 'local',
},
},
vite: {
plugins: [missingAssetsPlugin()],
server: {
host: '0.0.0.0',
},
publicDir: path.join(root, '.vitepress', 'public'),
},
})
+49
View File
@@ -0,0 +1,49 @@
import fs from 'node:fs'
import path from 'node:path'
const assetPattern = /\.(png|jpe?g|gif|webp|svg|bmp)(\?.*)?$/i
function toPublicUrl(source) {
if (source.startsWith('../images/')) {
return `/images/${source.slice('../images/'.length)}`
}
if (source.startsWith('../assets/')) {
return `/assets/${source.slice('../assets/'.length)}`
}
if (source.startsWith('images/')) {
return `/images/${source.slice('images/'.length)}`
}
if (source.startsWith('assets/')) {
return `/assets/${source.slice('assets/'.length)}`
}
return `/${source.replace(/^\.\//, '')}`
}
export function missingAssetsPlugin() {
return {
name: 'vitepress-missing-assets',
enforce: 'pre',
resolveId(source, importer) {
if (!importer?.endsWith('.md') || !assetPattern.test(source)) {
return null
}
if (/^https?:\/\//.test(source)) {
return null
}
const resolved = path.resolve(path.dirname(importer), source)
if (fs.existsSync(resolved)) {
return null
}
return `\0missing-asset:${toPublicUrl(source)}`
},
load(id) {
if (!id.startsWith('\0missing-asset:')) {
return null
}
const url = id.slice('\0missing-asset:'.length)
return `export default ${JSON.stringify(url)}`
},
}
}
+112
View File
@@ -0,0 +1,112 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const EXCLUDE_DIRS = new Set([
'node_modules',
'.vitepress',
'server',
'assets',
'images',
'.git',
'金瓶梅',
'黄帝内经',
'健康学习到150岁 - 人体系统调优不完全指南',
'梅花',
])
type SidebarItem = {
text: string
link?: string
collapsed?: boolean
items?: SidebarItem[]
}
function naturalCompare(a: string, b: string) {
return a.localeCompare(b, 'zh-CN', { numeric: true, sensitivity: 'base' })
}
function titleFromFilename(name: string) {
const base = name.replace(/\.md$/i, '')
if (/^readme$/i.test(base)) return '导读'
return base
}
function toLink(relativePath: string) {
const normalized = relativePath.replace(/\\/g, '/').replace(/\.md$/i, '')
if (normalized.endsWith('/README')) {
return `/${normalized.slice(0, -'/README'.length)}/`
}
return `/${normalized}`
}
function collectItems(dir: string, relativeDir: string): SidebarItem[] {
const entries = fs.readdirSync(dir, { withFileTypes: true })
const files = entries.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
const subdirs = entries.filter((entry) => entry.isDirectory())
const items: SidebarItem[] = files
.sort((a, b) => naturalCompare(a.name, b.name))
.map((file) => ({
text: titleFromFilename(file.name),
link: toLink(path.join(relativeDir, file.name)),
}))
for (const subdir of subdirs.sort((a, b) => naturalCompare(a.name, b.name))) {
const subRelative = path.join(relativeDir, subdir.name)
const subItems = collectItems(path.join(dir, subdir.name), subRelative)
if (subItems.length === 0) continue
items.push({
text: subdir.name,
collapsed: true,
items: subItems,
})
}
return items
}
export function generateSidebar(): Record<string, SidebarItem[]> {
const groups: SidebarItem[] = []
const rootFiles = fs
.readdirSync(root, { withFileTypes: true })
.filter(
(entry) =>
entry.isFile() &&
entry.name.endsWith('.md') &&
entry.name !== 'README.md' &&
entry.name !== 'index.md',
)
.sort((a, b) => naturalCompare(a.name, b.name))
if (rootFiles.length > 0) {
groups.push({
text: '典籍',
collapsed: false,
items: rootFiles.map((file) => ({
text: titleFromFilename(file.name),
link: toLink(file.name),
})),
})
}
const dirs = fs
.readdirSync(root, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && !EXCLUDE_DIRS.has(entry.name))
.sort((a, b) => naturalCompare(a.name, b.name))
for (const dir of dirs) {
const items = collectItems(path.join(root, dir.name), dir.name)
if (items.length === 0) continue
groups.push({
text: dir.name,
collapsed: false,
items,
})
}
return { '/': groups }
}
+383
View File
@@ -0,0 +1,383 @@
# DAO DE JING 部署文档
本文档说明如何在 **Ubuntu 本地** 部署站点,并通过 **NPS TCP 内网穿透** + **云服务器反向代理** 对外提供访问。
---
## 1. 架构说明
```
用户浏览器
云服务器(Nginx / 宝塔反代,HTTPS
NPS 服务端(TCP 转发)
NPS 客户端(内网 Ubuntu
Express 登录服务(0.0.0.0:12100
VitePress 静态站点(.vitepress/dist
```
| 组件 | 作用 |
|------|------|
| **VitePress** | 将 Markdown 文档构建为静态 HTML |
| **Express** | 提供登录校验,保护全部页面 |
| **NPS TCP** | 将内网 `12100` 端口映射到公网 |
| **云服务器反代** | 绑定域名、HTTPS(由你自行配置) |
> 登录鉴权在 **本地 Express 层** 完成,不依赖宝塔或 Nginx 的访问限制。
---
## 2. 环境要求
| 项目 | 要求 |
|------|------|
| 操作系统 | Ubuntu 20.04+(或其他 Linux |
| Node.js | **18.x 或 20.x**(推荐 LTS |
| npm | 9+ |
| Git LFS | 可选,用于拉取图片资源 |
| 端口 | **12100**Express 服务端口) |
### 2.1 安装 Node.jsUbuntu 示例)
```bash
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v
npm -v
```
### 2.2 安装 Git LFS(可选,建议)
仓库中部分图片使用 Git LFS 存储,部署前建议执行:
```bash
sudo apt-get install -y git-lfs
git lfs install
git lfs pull
```
---
## 3. 获取代码
```bash
git clone https://git.bz121.com/dekun/DAO_DE_JING.git
cd DAO_DE_JING
git lfs pull # 可选
```
---
## 4. 安装与构建
```bash
npm install
npm run build
```
构建完成后,静态文件输出到 `.vitepress/dist/`
### 常用命令
| 命令 | 说明 |
|------|------|
| `npm run build` | 仅构建静态站点 |
| `npm run start` | 启动服务(需先 build) |
| `npm run serve` | **构建 + 启动**(推荐一键部署) |
| `npm run docs:dev` | 开发模式,端口 5173,**无登录** |
---
## 5. 登录配置
首次启动时,若不存在 `server/auth.config.json`,会自动从示例文件复制生成。
```bash
cp server/auth.config.example.json server/auth.config.json
```
编辑 `server/auth.config.json`
```json
{
"username": "你的用户名",
"password": "你的强密码",
"sessionSecret": "随机长字符串,至少32位"
}
```
| 字段 | 说明 |
|------|------|
| `username` | 登录用户名 |
| `password` | 登录密码(明文存储,仅用于本地校验) |
| `sessionSecret` | Session 签名密钥,务必修改为随机字符串 |
> 该文件已加入 `.gitignore`**不会提交到 Git**。请勿修改 `.env.production`。
修改配置后需重启服务:
```bash
npm run start
```
---
## 6. 启动本地服务
```bash
npm run serve
```
启动成功后输出:
```
DAO DE JING 站点已启动: http://0.0.0.0:12100
登录页: /login
```
### 本地验证
```bash
curl -I http://127.0.0.1:12100/
# 应返回 302,跳转到 /login
curl -I http://127.0.0.1:12100/login
# 应返回 200
```
浏览器访问 `http://<内网IP>:12100/login`,使用配置的账号密码登录。
---
## 7. NPS TCP 内网穿透
在内网 Ubuntu 上运行 NPS 客户端,将本地 **12100****TCP 模式** 映射到 NPS 服务端。
### 客户端配置示例(nps.conf
```ini
[common]
server_addr=<NPS服务端IP>
server_port=<NPS服务端端口>
vkey=<你的密钥>
[tcp-12100]
mode=tcp
target_addr=127.0.0.1:12100
server_port=<公网映射端口>
```
映射完成后,云服务器可通过 `127.0.0.1:<公网映射端口>` 访问内网站点。
### 注意事项
- 使用 **TCP 模式**,不要用 HTTP 模式(Express 自行处理 HTTP 与 Session
- 确保 Ubuntu 防火墙放行出站连接
- 确保 `12100` 端口未被其他程序占用:
```bash
ss -tlnp | grep 12100
```
---
## 8. 云服务器反向代理(参考)
在云服务器(宝塔 / Nginx)上,将域名反代到 NPS 映射端口。**无需在宝塔配置额外登录**。
### Nginx 反代示例
```nginx
server {
listen 443 ssl http2;
server_name your-domain.com;
# ssl_certificate ...;
# ssl_certificate_key ...;
location / {
proxy_pass http://127.0.0.1:<NPS映射端口>;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Express 已启用 `trust proxy`HTTPS 反代后 Session Cookie 可正常工作。
---
## 9. 后台常驻运行(systemd
创建 systemd 服务,实现开机自启与崩溃重启。
```bash
sudo nano /etc/systemd/system/dao-de-jing.service
```
```ini
[Unit]
Description=DAO DE JING Site
After=network.target
[Service]
Type=simple
User=你的用户名
WorkingDirectory=/path/to/DAO_DE_JING
ExecStart=/usr/bin/npm run start
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
```
```bash
# 首次需手动构建
cd /path/to/DAO_DE_JING
npm run build
sudo systemctl daemon-reload
sudo systemctl enable dao-de-jing
sudo systemctl start dao-de-jing
sudo systemctl status dao-de-jing
```
查看日志:
```bash
journalctl -u dao-de-jing -f
```
> `ExecStart` 使用 `npm run start`(仅启动,不重新构建)。更新内容后需手动 `npm run build` 再 `systemctl restart dao-de-jing`。
---
## 10. 更新部署
文档内容有变更时:
```bash
cd DAO_DE_JING
git pull
git lfs pull # 如有图片更新
npm install # 依赖有变更时
npm run build
sudo systemctl restart dao-de-jing # 若使用 systemd
```
或一键:
```bash
npm run serve
```
---
## 11. 目录结构(部署相关)
```
DAO_DE_JING/
├── .vitepress/
│ ├── config.mts # VitePress 配置
│ ├── dist/ # 构建产物(npm run build 生成)
│ └── public/ # 构建时自动同步 assets/images
├── server/
│ ├── index.js # Express 服务(端口 12100
│ ├── auth.config.json # 登录配置(本地,不提交 Git)
│ ├── auth.config.example.json
│ └── public/login.html # 登录页
├── scripts/prepare-public.mjs
├── index.md # 站点首页
├── package.json
└── DEPLOY.md # 本文档
```
---
## 12. 常见问题
### Q1:启动报错「未找到构建产物 .vitepress/dist」
先执行构建:
```bash
npm run build
```
### Q2:登录后刷新又跳回登录页
- 检查 `sessionSecret` 是否已修改且重启后保持一致
- 反代是否传递了 `X-Forwarded-Proto`HTTPS 场景)
- 浏览器是否禁用了 Cookie
### Q3:图片不显示
执行 Git LFS 拉取:
```bash
git lfs pull
npm run build
```
部分章节引用的图片若仓库中不存在,页面可正常访问,仅图片为空。
### Q4:端口被占用
```bash
ss -tlnp | grep 12100
# 结束占用进程或修改 server/index.js 中的 PORT 常量
```
### Q5:内网穿透后外网无法访问
1. 确认 Ubuntu 上 `npm run start` 正常运行
2. 确认 NPS 客户端在线,TCP 隧道状态正常
3. 确认云服务器反代指向正确的 NPS 映射端口
4. 确认云服务器安全组 / 防火墙已放行相应端口
---
## 13. 安全建议
1. **立即修改** `server/auth.config.json` 中的默认密码和 `sessionSecret`
2. 不要将 `server/auth.config.json` 提交到 Git 或分享给他人
3. 通过 HTTPS 对外提供服务(在云服务器反代层配置 SSL)
4. 定期更新依赖:`npm update`
5. 限制 NPS 映射端口的访问来源(如云服务器安全组仅允许必要 IP)
---
## 14. 快速部署清单
```bash
# 1. 环境
node -v && npm -v
# 2. 代码
git clone <repo> && cd DAO_DE_JING
git lfs pull
# 3. 依赖与构建
npm install
npm run build
# 4. 登录配置
cp server/auth.config.example.json server/auth.config.json
nano server/auth.config.json
# 5. 启动
npm run start
# 6. 配置 NPS TCP → 12100
# 7. 配置云服务器反代 → NPS 映射端口
# 8. 浏览器访问域名,登录后使用
```
Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 189 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 324 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 132 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 MiB

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 MiB

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

After

Width:  |  Height:  |  Size: 131 B

+40
View File
@@ -0,0 +1,40 @@
---
layout: home
hero:
name: DAO DE JING
text: 传统文化典籍资料库
tagline: 无善无恶心之体,有善有恶意之动。知善知恶是良知,为善去恶是格物 — 王阳明
actions:
- theme: brand
text: 开始阅读
link: /道德经/01
- theme: alt
text: 五行
link: /金、木、水、火、土%20-%20五行/
features:
- title: 道德经
details: 八十一章原文、译文与解读
link: /道德经/01
- title: 周易
details: 易经全文与读音参考
link: /周易/
- title: 中医宝典
details: 名方、药方与养生资料
link: /中医宝典/
- title: 现代医药
details: 实用健康与用药笔记
link: /现代医药/
---
## 快速导航
- [五行](/金、木、水、火、土%20-%20五行/)
- [抱朴子](/抱朴子/)
- [鬼谷子](/鬼谷子/捭阖)
- [风水](/风水/REAME)
- [八字](/八字/九龙道长八字)
- [君主论](/君主论/00%20君主论)
左侧侧边栏可浏览全部目录。
+3439
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"name": "dao-de-jing-site",
"private": true,
"type": "module",
"scripts": {
"docs:dev": "vitepress dev --host 0.0.0.0 --port 5173",
"docs:build": "node scripts/prepare-public.mjs && vitepress build",
"docs:preview": "vitepress preview --host 0.0.0.0 --port 5173",
"build": "node scripts/prepare-public.mjs && vitepress build",
"start": "node server/index.js",
"serve": "npm run build && npm run start"
},
"devDependencies": {
"vitepress": "^1.6.3"
},
"dependencies": {
"cookie-parser": "^1.4.7",
"express": "^4.21.2",
"express-session": "^1.18.1"
}
}
+34
View File
@@ -0,0 +1,34 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const publicDir = path.join(root, '.vitepress', 'public')
function copyDir(source, target) {
if (!fs.existsSync(source)) return
fs.mkdirSync(target, { recursive: true })
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
const from = path.join(source, entry.name)
const to = path.join(target, entry.name)
if (entry.isDirectory()) {
copyDir(from, to)
} else {
fs.copyFileSync(from, to)
}
}
}
function resetPublicDir() {
if (fs.existsSync(publicDir)) {
fs.rmSync(publicDir, { recursive: true, force: true })
}
fs.mkdirSync(publicDir, { recursive: true })
}
resetPublicDir()
copyDir(path.join(root, 'assets'), path.join(publicDir, 'assets'))
copyDir(path.join(root, 'images'), path.join(publicDir, 'images'))
console.log('[prepare-public] assets 与 images 已同步到 .vitepress/public')
+5
View File
@@ -0,0 +1,5 @@
{
"username": "admin",
"password": "change-me",
"sessionSecret": "please-change-this-session-secret"
}
+144
View File
@@ -0,0 +1,144 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import cookieParser from 'cookie-parser'
import express from 'express'
import session from 'express-session'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, '..')
const distPath = path.join(root, '.vitepress', 'dist')
const authExamplePath = path.join(__dirname, 'auth.config.example.json')
const authConfigPath = path.join(__dirname, 'auth.config.json')
const PORT = 12100
function loadAuthConfig() {
if (!fs.existsSync(authConfigPath)) {
fs.copyFileSync(authExamplePath, authConfigPath)
console.warn(
'[auth] 已生成 server/auth.config.json,请修改默认用户名和密码后重启服务。',
)
}
const raw = fs.readFileSync(authConfigPath, 'utf8')
const config = JSON.parse(raw)
if (!config.username || !config.password) {
throw new Error('server/auth.config.json 缺少 username 或 password')
}
if (!config.sessionSecret || config.sessionSecret === 'please-change-this-session-secret') {
console.warn('[auth] 建议在 server/auth.config.json 中设置独立的 sessionSecret')
}
return config
}
const authConfig = loadAuthConfig()
const app = express()
app.set('trust proxy', 1)
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(
session({
name: 'dao_de_jing.sid',
secret: authConfig.sessionSecret || 'dao-de-jing-default-secret',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000,
sameSite: 'lax',
},
}),
)
function isAuthenticated(req) {
return Boolean(req.session?.user)
}
function authGuard(req, res, next) {
if (req.path === '/login' || req.path.startsWith('/api/login')) {
return next()
}
if (isAuthenticated(req)) {
return next()
}
if (req.path.startsWith('/api/')) {
return res.status(401).json({ message: '未登录' })
}
const redirect = encodeURIComponent(req.originalUrl || '/')
return res.redirect(`/login?redirect=${redirect}`)
}
app.get('/login', (req, res) => {
if (isAuthenticated(req)) {
const redirect = req.query.redirect || '/'
return res.redirect(String(redirect))
}
return res.sendFile(path.join(__dirname, 'public', 'login.html'))
})
app.post('/api/login', (req, res) => {
const { username, password } = req.body || {}
if (username === authConfig.username && password === authConfig.password) {
req.session.user = { username }
return res.json({ ok: true })
}
return res.status(401).json({ message: '用户名或密码错误' })
})
app.post('/api/logout', authGuard, (req, res) => {
req.session.destroy(() => {
res.clearCookie('dao_de_jing.sid')
res.json({ ok: true })
})
})
app.get('/api/me', authGuard, (req, res) => {
res.json({ user: req.session.user })
})
app.use(authGuard)
if (!fs.existsSync(distPath)) {
console.error(
'未找到构建产物 .vitepress/dist,请先运行: npm run build',
)
process.exit(1)
}
app.use(express.static(distPath, { index: false }))
app.get('*', (req, res, next) => {
if (req.path.includes('.')) {
return next()
}
const rel = req.path.replace(/^\//, '').replace(/\/$/, '') || 'index'
const candidates = [
path.join(distPath, `${rel}.html`),
path.join(distPath, rel, 'index.html'),
path.join(distPath, '404.html'),
]
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return res.sendFile(candidate)
}
}
return next()
})
app.listen(PORT, '0.0.0.0', () => {
console.log(`DAO DE JING 站点已启动: http://0.0.0.0:${PORT}`)
console.log('登录页: /login')
})
+132
View File
@@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录 · DAO DE JING</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: linear-gradient(145deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
color: #e8e8e8;
}
.card {
width: min(92vw, 380px);
padding: 2rem;
border-radius: 12px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.12);
backdrop-filter: blur(8px);
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.35);
}
h1 {
margin: 0 0 0.25rem;
font-size: 1.35rem;
font-weight: 600;
}
p.sub {
margin: 0 0 1.5rem;
color: #a8b2c1;
font-size: 0.9rem;
}
label {
display: block;
margin-bottom: 0.35rem;
font-size: 0.85rem;
color: #c5cdd8;
}
input {
width: 100%;
margin-bottom: 1rem;
padding: 0.65rem 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 8px;
background: rgba(0, 0, 0, 0.25);
color: #fff;
font-size: 1rem;
}
input:focus {
outline: none;
border-color: #5b8def;
}
button {
width: 100%;
padding: 0.7rem;
border: none;
border-radius: 8px;
background: #3b82f6;
color: #fff;
font-size: 1rem;
cursor: pointer;
}
button:hover { background: #2563eb; }
button:disabled { opacity: 0.6; cursor: not-allowed; }
.error {
display: none;
margin-bottom: 1rem;
padding: 0.6rem 0.75rem;
border-radius: 8px;
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.35);
color: #fca5a5;
font-size: 0.85rem;
}
.error.show { display: block; }
</style>
</head>
<body>
<div class="card">
<h1>DAO DE JING</h1>
<p class="sub">请登录后访问站点</p>
<div id="error" class="error"></div>
<form id="loginForm">
<label for="username">用户名</label>
<input id="username" name="username" autocomplete="username" required />
<label for="password">密码</label>
<input id="password" name="password" type="password" autocomplete="current-password" required />
<button type="submit" id="submitBtn">登录</button>
</form>
</div>
<script>
const form = document.getElementById('loginForm')
const errorEl = document.getElementById('error')
const submitBtn = document.getElementById('submitBtn')
const params = new URLSearchParams(window.location.search)
const redirect = params.get('redirect') || '/'
form.addEventListener('submit', async (event) => {
event.preventDefault()
errorEl.classList.remove('show')
submitBtn.disabled = true
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: form.username.value.trim(),
password: form.password.value,
}),
})
const data = await response.json().catch(() => ({}))
if (!response.ok) {
throw new Error(data.message || '登录失败')
}
window.location.href = redirect
} catch (error) {
errorEl.textContent = error.message || '登录失败'
errorEl.classList.add('show')
} finally {
submitBtn.disabled = false
}
})
</script>
</body>
</html>
+3 -3
View File
@@ -17,6 +17,6 @@
- [现代医药成份作用大全](../现代医药/README.md) - [现代医药成份作用大全](../现代医药/README.md)
## 毒性中药材整理 ## 毒性中药材整理
<img width="420" src="assets/中药大毒.webp"/> <img width="420" src="../assets/中药大毒.webp"/>
<img width="420" src="assets/中药中毒.webp"/> <img width="420" src="../assets/中药中毒.webp"/>
<img width="420" src="assets/中药小毒.webp"/> <img width="420" src="../assets/中药小毒.webp"/>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 613 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 132 B

-4
View File
@@ -1,7 +1,3 @@
<img width="420" src="六十四卦.jpeg"/>
<img width="420" src="序卦传歌诀.jpeg"/>
《周易》64卦读音 《周易》64卦读音
1、乾:qián 1、乾:qián
2、坤:kūn 2、坤:kūn
Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

BIN
View File
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 130 B

Some files were not shown because too many files have changed in this diff Show More