Skip to content

Commit b117317

Browse files
committed
feat: 🚀 添加 catalogue 插件,优化其他插件代码,文章页添加作者、发布时间、面包屑功能,首页添加图标
1 parent 7693a23 commit b117317

File tree

32 files changed

+686
-106
lines changed

32 files changed

+686
-106
lines changed

docs/.vitepress/cache/deps/_metadata.json

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,65 @@
11
{
2-
"hash": "4e9d9a5f",
3-
"configHash": "cd19b2e9",
4-
"lockfileHash": "418faa5b",
5-
"browserHash": "42bca078",
2+
"hash": "bed69f4d",
3+
"configHash": "f2098492",
4+
"lockfileHash": "8ff230ec",
5+
"browserHash": "1bc836ef",
66
"optimized": {
77
"vue": {
88
"src": "../../../../node_modules/.pnpm/[email protected][email protected]/node_modules/vue/dist/vue.runtime.esm-bundler.js",
99
"file": "vue.js",
10-
"fileHash": "c116dd30",
10+
"fileHash": "c4a03ef2",
1111
"needsInterop": false
1212
},
1313
"vitepress > @vue/devtools-api": {
1414
"src": "../../../../node_modules/.pnpm/@[email protected]/node_modules/@vue/devtools-api/dist/index.js",
1515
"file": "vitepress___@vue_devtools-api.js",
16-
"fileHash": "a054176d",
16+
"fileHash": "8a625078",
1717
"needsInterop": false
1818
},
1919
"vitepress > @vueuse/core": {
2020
"src": "../../../../node_modules/.pnpm/@[email protected][email protected]/node_modules/@vueuse/core/index.mjs",
2121
"file": "vitepress___@vueuse_core.js",
22-
"fileHash": "cc67045e",
22+
"fileHash": "81df7691",
2323
"needsInterop": false
2424
},
2525
"vitepress > @vueuse/integrations/useFocusTrap": {
2626
"src": "../../../../node_modules/.pnpm/@[email protected][email protected][email protected][email protected]/node_modules/@vueuse/integrations/useFocusTrap.mjs",
2727
"file": "vitepress___@vueuse_integrations_useFocusTrap.js",
28-
"fileHash": "28c56b15",
28+
"fileHash": "0dae493e",
2929
"needsInterop": false
3030
},
3131
"vitepress > mark.js/src/vanilla.js": {
3232
"src": "../../../../node_modules/.pnpm/[email protected]/node_modules/mark.js/src/vanilla.js",
3333
"file": "vitepress___mark__js_src_vanilla__js.js",
34-
"fileHash": "c2197e44",
34+
"fileHash": "fb61c6c3",
3535
"needsInterop": false
3636
},
3737
"vitepress > minisearch": {
3838
"src": "../../../../node_modules/.pnpm/[email protected]/node_modules/minisearch/dist/es/index.js",
3939
"file": "vitepress___minisearch.js",
40-
"fileHash": "01611d6e",
40+
"fileHash": "235c25ae",
4141
"needsInterop": false
4242
},
4343
"element-plus": {
4444
"src": "../../../../node_modules/.pnpm/[email protected][email protected][email protected]_/node_modules/element-plus/es/index.mjs",
4545
"file": "element-plus.js",
46-
"fileHash": "895ec0d5",
46+
"fileHash": "5887a0fb",
47+
"needsInterop": false
48+
},
49+
"@element-plus/icons-vue": {
50+
"src": "../../../../node_modules/.pnpm/@[email protected][email protected][email protected]_/node_modules/@element-plus/icons-vue/dist/index.js",
51+
"file": "@element-plus_icons-vue.js",
52+
"fileHash": "d4aef368",
4753
"needsInterop": false
4854
}
4955
},
5056
"chunks": {
5157
"chunk-QY2276BV": {
5258
"file": "chunk-QY2276BV.js"
5359
},
60+
"chunk-6QBDH6GL": {
61+
"file": "chunk-6QBDH6GL.js"
62+
},
5463
"chunk-3ZOFCUNA": {
5564
"file": "chunk-3ZOFCUNA.js"
5665
},

docs/01.指南/a.md

Lines changed: 0 additions & 2 deletions
This file was deleted.

docs/32.Spring生态/10.Spring/02.Spring6 - 概述.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ description: aaaas啊实打实的
1515
sticky: true
1616
---
1717

18-
## Spring 是什么?
19-
2018
::: tip
2119
存寿生产厂商
2220
:::

docs/a.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
---
2+
title: 使用 - AS
3+
date: 2025-12-12 19:00:00
4+
permalink: /a
5+
---
6+
7+
## 设计思路
8+
9+
对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验:
10+
11+
- 如果校验通过,则:正常返回数据
12+
- 如果校验未通过,则:抛出异常,告知其需要先进行登录
13+
14+
那么,判断会话是否登录的依据是什么?我们先来简单分析一下登录访问流程:
15+
16+
1. 用户提交 `name` + `password` 参数,调用登录接口
17+
2. 登录成功,返回这个用户的 Token 会话令牌
18+
3. 用户后续的每次请求,都携带上这个 Token
19+
4. 服务器根据 Token 判断此会话是否登录成功
20+
21+
所谓登录认证,指的就是服务器校验账号密码,为用户颁发 Token 会话令牌的过程,这个 Token 也是我们后续判断会话是否登录的关键所在。
22+
23+
## 登录与注销
24+
25+
根据以上思路,我们需要一个会话登录的函数:
26+
27+
```java
28+
// 会话登录:参数填写要登录的账号 id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等
29+
HdHelper.login(Object id);
30+
```
31+
32+
只此一句代码,便可以使会话登录成功,实际上,Hd Security 在背后做了大量的工作,包括但不限于:
33+
34+
1. 检查此账号是否之前已有登录
35+
2. 为账号生成 `Token` 令牌与 `Session` 会话
36+
3. 记录 Token 活跃时间
37+
4. 通知全局侦听器,xx 账号登录成功
38+
5.`Token` 注入到请求上下文
39+
6. ……
40+
41+
你暂时不需要完整了解整个登录过程,你只需要记住关键一点:`Hd Security 为这个账号创建了一个 Token 令牌,且通过 Cookie 上下文返回给了前端`
42+
43+
所以一般情况下,我们的登录接口代码,会大致类似如下:
44+
45+
```java
46+
// 会话登录接口
47+
@RequestMapping("doLogin")
48+
public HdResponse<String> doLogin(String name, String pwd) {
49+
// 第一步:比对前端提交的账号名称、密码
50+
if("Tianke".equals(name) && "123456".equals(pwd)) {
51+
// 第二步:根据账号 id,进行登录
52+
HdHelper.login(10001);
53+
return HdResponse.ok("登录成功");
54+
}
55+
return HdResponse.error("登录失败");
56+
}
57+
```
58+
59+
如果你对以上代码阅读没有压力,你可能会注意到略显奇怪的一点:此处仅仅做了会话登录,但并没有主动向前端返回 token 信息。 是因为不需要吗?严格来讲是需要的,只不过 `HdHelper.login(id)` 方法利用了 Cookie 自动注入的特性,省略了你手写返回 token 的代码。
60+
61+
::: info Cookie 是什么?
62+
63+
如果你对 Cookie 功能还不太了解,也不用担心,我们会在之后的 [ 前后端分离 ] 章节中详细的阐述 Cookie 功能,现在你只需要了解最基本的两点:
64+
65+
- Cookie 可以从后端控制往浏览器中写入 token 值。
66+
- Cookie 会在前端每次发起请求时自动提交 token 值。
67+
68+
因此,在 Cookie 功能的加持下,我们可以仅靠 `HdHelper.login(id)` 一句代码就完成登录认证。
69+
70+
:::
71+
72+
除了登录方法,我们还需要:
73+
74+
```java
75+
// 当前会话注销登录
76+
HdHelper.logout();
77+
78+
// 获取当前会话是否已经登录,返回 true 已登录,false 未登录
79+
HdHelper.isLogin();
80+
81+
// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`HdSecurityLoginException`
82+
HdHelper.checkLogin();
83+
```
84+
85+
## 会话查询
86+
87+
```java
88+
// 获取当前会话账号id, 如果未登录,则返回 null
89+
HdHelper.getLoginId();
90+
91+
// ---------- 指定未登录情形下返回的默认值 ----------
92+
93+
// 获取当前会话账号 id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型)
94+
HdHelper.loginHelper().getLoginId(Object defaultValue);
95+
```
96+
97+
## token 查询
98+
99+
```java
100+
// 获取当前会话的 token 值
101+
HdHelper.getWebToken();
102+
103+
// 获取指定 token 对应的账号 id,如果未登录,则返回 null
104+
HdHelper.getLoginIdByToken(String token);
105+
106+
// 获取当前会话剩余有效期(单位:s,返回 -1 代表永久有效)
107+
HdHelper.tokenHelper().getTokenAndLoginIdExpireTime();
108+
109+
// 获取 Token 的基本信息
110+
HdHelper.getTokenInfo();
111+
```
112+
113+
## 完整 Demo
114+
115+
新建 `LoginController`
116+
117+
```java
118+
/**
119+
* 登录测试
120+
*/
121+
@RestController
122+
@RequestMapping("/login")
123+
public class LoginController {
124+
125+
// 测试登录 ---- http://localhost:8081/login/doLogin?name=zhang&pwd=123456
126+
@RequestMapping("doLogin")
127+
public HdResponse<String> doLogin(String name, String pwd) {
128+
// 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
129+
if("Tianke".equals(name) && "123456".equals(pwd)) {
130+
HdResponse.login(10001);
131+
return HdResponse.okMessage("登录成功");
132+
}
133+
return HdResponse.errorMessage("登录失败");
134+
}
135+
136+
// 查询登录状态 ---- http://localhost:8081/login/isLogin
137+
@RequestMapping("isLogin")
138+
public HdResponse<String> isLogin() {
139+
return HdResponse.okMessage("是否登录:" + Hdhelper.isLogin());
140+
}
141+
142+
// 查询 Token 信息 ---- http://localhost:8081/login/tokenInfo
143+
@RequestMapping("tokenInfo")
144+
public HdResponse<String> tokenInfo() {
145+
return HdResponse.okMessage(Hdhelper.getTokenInfo());
146+
}
147+
148+
// 测试注销 ---- http://localhost:8081/login/logout
149+
@RequestMapping("logout")
150+
public HdResponse<String> logout() {
151+
Hdhelper.logout();
152+
return HdResponse.ok();
153+
}
154+
155+
}
156+
```

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
],
1111
"scripts": {
1212
"preinstall": "npx only-allow pnpm",
13-
"postinstall": "pnpm stub:theme && pnpm stub:build && pnpm stub:sidebar && pnpm stub:permalink && pnpm stub:mdH1",
13+
"postinstall": "pnpm stub:theme && pnpm stub:build && pnpm stub:sidebar && pnpm stub:permalink && pnpm stub:mdH1 && pnpm stub:catalogue",
1414
"docs:dev": "pnpm run -C docs dev",
1515
"docs:build": "pnpm run -C docs build",
1616
"docs:preview": "pnpm run -C docs preview",
@@ -21,6 +21,7 @@
2121
"stub:sidebar": "pnpm run -C plugins/vitepress-plugin-sidebar-resolve stub",
2222
"stub:permalink": "pnpm run -C plugins/vitepress-plugin-permalink stub",
2323
"stub:mdH1": "pnpm run -C plugins/vitepress-plugin-md-h1 stub",
24+
"stub:catalogue": "pnpm run -C plugins/vitepress-plugin-catalogue stub",
2425
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
2526
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,less,scss,vue,html,md}\"",
2627
"lint:eslint": "eslint --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { defineBuildConfig } from "unbuild";
2+
3+
export default defineBuildConfig({
4+
entries: ["src/index"],
5+
clean: true,
6+
declaration: true,
7+
rollup: {
8+
emitCJS: true,
9+
},
10+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "vitepress-plugin-catalogue",
3+
"version": "1.0.0",
4+
"description": "catalogue",
5+
"type": "module",
6+
"main": "./dist/index.cjs",
7+
"module": "./dist/index.mjs",
8+
"types": "./dist/index.d.ts",
9+
"exports": {
10+
".": {
11+
"types": "./dist/index.d.ts",
12+
"import": "./dist/index.mjs",
13+
"require": "./dist/index.cjs"
14+
}
15+
},
16+
"scripts": {
17+
"stub": "unbuild --stub"
18+
},
19+
"devDependencies": {
20+
"gray-matter": "^4.0.3",
21+
"unbuild": "^3.2.0",
22+
"vite": "^6.0.7"
23+
}
24+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { readdirSync, readFileSync, statSync } from "node:fs";
2+
import { basename, extname, join, resolve } from "node:path";
3+
import matter from "gray-matter";
4+
import type { CatalogueOption } from "./types";
5+
6+
// 默认扫描的文件夹列表
7+
export const DEFAULT_INCLUDE_DIR = ["目录页"];
8+
9+
let catalogues: Record<string, string> = {};
10+
11+
export default (option: CatalogueOption = {}) => {
12+
const { base = ".", includeList = [] } = option;
13+
const sourceDir = join(process.cwd(), base);
14+
15+
// 获取指定根目录下的所有目录绝对路径
16+
const dirPaths = readDirPaths(sourceDir, includeList);
17+
18+
// 遍历根目录下的每个子目录
19+
dirPaths.forEach(dirPath => scannerMdFile(dirPath, option, basename(dirPath)));
20+
21+
return catalogues;
22+
};
23+
24+
/**
25+
* 指定根目录下的所有目录绝对路径,win 如 ['D:\docs\01.guide', 'D:\docs\02.design'],linux 如 ['/usr/local/docs/01.guide', '/usr/local/docs/02.design']
26+
* @param sourceDir 指定文件/文件夹的根目录
27+
*/
28+
const readDirPaths = (sourceDir: string, includeList: CatalogueOption["includeList"] = []) => {
29+
const includeListAll = [...DEFAULT_INCLUDE_DIR, ...includeList];
30+
const dirPaths: string[] = [];
31+
// 读取目录,返回数组,成员是 root 下所有的目录名(包含文件夹和文件,不递归)
32+
const secondDirNames = readdirSync(sourceDir);
33+
34+
secondDirNames.forEach(secondDirName => {
35+
// 将路径或路径片段的序列解析为绝对路径,等于使用 cd 命令
36+
const secondDirPath = resolve(sourceDir, secondDirName);
37+
// 是否为文件夹目录,并排除指定文件夹
38+
if (isSome(includeListAll, secondDirName) && statSync(secondDirPath).isDirectory()) {
39+
dirPaths.push(secondDirPath);
40+
}
41+
});
42+
43+
return dirPaths;
44+
};
45+
46+
const scannerMdFile = (root: string, option: CatalogueOption, prefix = "") => {
47+
const { includeList = [] } = option;
48+
const includeListAll = [...DEFAULT_INCLUDE_DIR, ...includeList];
49+
// 读取目录名(文件和文件夹)
50+
let secondDirOrFilenames = readdirSync(root);
51+
52+
secondDirOrFilenames.forEach(dirOrFilename => {
53+
const filePath = resolve(root, dirOrFilename);
54+
55+
if (statSync(filePath).isDirectory()) {
56+
// 是文件夹目录
57+
if (!isSome(includeListAll, dirOrFilename)) return;
58+
59+
scannerMdFile(filePath, option, `${prefix}/${dirOrFilename}`);
60+
} else {
61+
// 是文件
62+
if (!isMdFile(dirOrFilename)) return;
63+
64+
const content = readFileSync(filePath, "utf-8");
65+
66+
// 解析出 front matter 数据
67+
const { data: { catalogue, path = "" } = {} } = matter(content, {});
68+
69+
if (catalogue && path) {
70+
const filename = basename(dirOrFilename, extname(dirOrFilename));
71+
catalogues[`${prefix}/${filename}`] = path;
72+
}
73+
}
74+
});
75+
};
76+
77+
/**
78+
* 判断是否为 md 文件
79+
*
80+
* @param filePath 文件绝对路径
81+
*/
82+
const isMdFile = (filePath: string) => {
83+
const fileExtension = filePath.substring(filePath.lastIndexOf(".") + 1);
84+
return ["md", "MD"].includes(fileExtension);
85+
};
86+
87+
const isSome = (arr: Array<string | RegExp>, name: string) => {
88+
return arr.some(item => name.includes(item as string) || (item instanceof RegExp && item.test(name)));
89+
};

0 commit comments

Comments
 (0)