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>
@@ -32,3 +32,10 @@ google-services.json
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
# Node / VitePress
|
||||
node_modules/
|
||||
.vitepress/dist/
|
||||
.vitepress/cache/
|
||||
.vitepress/public/
|
||||
server/auth.config.json
|
||||
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
})
|
||||
@@ -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)}`
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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.js(Ubuntu 示例)
|
||||
|
||||
```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. 浏览器访问域名,登录后使用
|
||||
```
|
||||
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 213 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 283 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 6.3 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 3.1 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 4.1 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 4.4 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 3.4 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 3.8 MiB After Width: | Height: | Size: 132 B |
|
Before Width: | Height: | Size: 544 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 316 KiB After Width: | Height: | Size: 131 B |
@@ -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君主论)
|
||||
|
||||
左侧侧边栏可浏览全部目录。
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "change-me",
|
||||
"sessionSecret": "please-change-this-session-secret"
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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>
|
||||
@@ -17,6 +17,6 @@
|
||||
- [现代医药成份作用大全](../现代医药/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"/>
|
||||
|
Before Width: | Height: | Size: 404 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 613 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 132 B |
@@ -1,7 +1,3 @@
|
||||
<img width="420" src="六十四卦.jpeg"/>
|
||||
|
||||
<img width="420" src="序卦传歌诀.jpeg"/>
|
||||
|
||||
《周易》64卦读音
|
||||
1、乾:qián
|
||||
2、坤:kūn
|
||||
|
||||
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 131 B |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 130 B |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 130 B |