@bbki.ng/site 5.5.14 → 5.5.15
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/CHANGELOG.md +8 -0
- package/package.json +2 -2
- package/public/fonts/xwy.woff2 +0 -0
- package/src/blog/app.tsx +2 -3
- package/src/blog/hooks/use_plugins.ts +27 -0
- package/src/blog/hooks/use_posts.ts +40 -17
- package/src/plugins/fontstyler/index.ts +132 -0
- package/src/plugins/manifest.ts +6 -0
- package/src/types/slots.ts +1 -1
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bbki.ng/site",
|
|
3
|
-
"version": "5.5.
|
|
3
|
+
"version": "5.5.15",
|
|
4
4
|
"description": "code behind bbki.ng",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"react-dom": "^18.0.0",
|
|
15
15
|
"react-router-dom": "6",
|
|
16
16
|
"swr": "^2.2.5",
|
|
17
|
-
"@bbki.ng/ui": "0.2.
|
|
17
|
+
"@bbki.ng/ui": "0.2.11"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@eslint/compat": "^1.0.0",
|
|
Binary file
|
package/src/blog/app.tsx
CHANGED
|
@@ -13,8 +13,7 @@ import { BotRedirect } from '@/pages/bot';
|
|
|
13
13
|
import { BBContext } from '@/context/bbcontext';
|
|
14
14
|
import { useDynamicLogo } from './hooks/use_dynamic_logo';
|
|
15
15
|
import { Slot } from '../core/components/SlotComp';
|
|
16
|
-
import {
|
|
17
|
-
import { useSticker } from './hooks/use_sticker';
|
|
16
|
+
import { usePlugins } from './hooks/use_plugins';
|
|
18
17
|
|
|
19
18
|
const Layout = () => {
|
|
20
19
|
const paths = usePaths();
|
|
@@ -60,7 +59,7 @@ const Layout = () => {
|
|
|
60
59
|
};
|
|
61
60
|
|
|
62
61
|
export const App = () => {
|
|
63
|
-
|
|
62
|
+
usePlugins(['sticker', 'fontstyler']);
|
|
64
63
|
|
|
65
64
|
return (
|
|
66
65
|
<SWR>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { pluginManager } from '#/core/pluginManager';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 通用插件管理 hook
|
|
6
|
+
* 根据传入的插件ID列表加载和卸载插件
|
|
7
|
+
*
|
|
8
|
+
* @param pluginIds - 要加载的插件ID列表
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* usePlugins(['sticker', 'fontstyler']);
|
|
12
|
+
*/
|
|
13
|
+
export const usePlugins = (pluginIds: string[]) => {
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
// 加载指定的插件
|
|
16
|
+
pluginIds.forEach(id => {
|
|
17
|
+
pluginManager.loadPlugin(id);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return () => {
|
|
21
|
+
// 卸载所有指定的插件
|
|
22
|
+
pluginIds.forEach(id => {
|
|
23
|
+
pluginManager.disablePlugin(id);
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
}, [pluginIds]);
|
|
27
|
+
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import useSWR from 'swr';
|
|
2
|
-
import { useContext, useEffect } from 'react';
|
|
2
|
+
import { useContext, useEffect, useState } from 'react';
|
|
3
3
|
import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
|
|
4
4
|
import { baseFetcher } from '@/utils';
|
|
5
5
|
import { API_ENDPOINT } from '@/constants/routes';
|
|
6
|
+
import { registry } from '#/core/registry';
|
|
6
7
|
|
|
7
8
|
// In dev, use /api prefix to leverage Vite proxy to localhost:8787
|
|
8
9
|
const isProd = true;
|
|
@@ -12,6 +13,13 @@ const POSTS_API = !isProd ? '/api/posts' : `${API_ENDPOINT}/posts`;
|
|
|
12
13
|
// Use baseFetcher for full URLs, cfApiFetcher is for relative paths
|
|
13
14
|
const postsFetcher = (resource: string) => baseFetcher(resource);
|
|
14
15
|
|
|
16
|
+
export interface TitleListItem {
|
|
17
|
+
name: string;
|
|
18
|
+
to: string;
|
|
19
|
+
children: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
export const usePosts = (name: string = '', suspense?: boolean) => {
|
|
16
24
|
const { data: response, error } = useSWR(POSTS_API, postsFetcher, {
|
|
17
25
|
revalidateOnFocus: false,
|
|
@@ -21,27 +29,42 @@ export const usePosts = (name: string = '', suspense?: boolean) => {
|
|
|
21
29
|
// Extract posts array from API response { status: "success", data: [...] }
|
|
22
30
|
const data = response?.data;
|
|
23
31
|
|
|
32
|
+
const [titleList, setTitleList] = useState<TitleListItem[]>([]);
|
|
24
33
|
let isLoading = !data && !error;
|
|
25
34
|
const { setIsLoading } = useContext(GlobalLoadingContext);
|
|
26
|
-
const titleList =
|
|
27
|
-
isLoading || error || !data
|
|
28
|
-
? []
|
|
29
|
-
: [
|
|
30
|
-
...data.map((p: any) => ({
|
|
31
|
-
name: p.title,
|
|
32
|
-
to: p.title,
|
|
33
|
-
children: p.title,
|
|
34
|
-
})),
|
|
35
|
-
{
|
|
36
|
-
name: 'cd ~',
|
|
37
|
-
to: '/',
|
|
38
|
-
children: 'cd ~',
|
|
39
|
-
},
|
|
40
|
-
];
|
|
41
35
|
|
|
42
36
|
useEffect(() => {
|
|
43
37
|
setIsLoading(isLoading);
|
|
44
|
-
}, [isLoading]);
|
|
38
|
+
}, [isLoading, setIsLoading]);
|
|
39
|
+
|
|
40
|
+
// Apply middleware to transform titleList
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const transformTitleList = async () => {
|
|
43
|
+
if (isLoading || error || !data) {
|
|
44
|
+
setTitleList([]);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const baseTitleList: TitleListItem[] = [
|
|
49
|
+
...data.map((p: any) => ({
|
|
50
|
+
name: p.title,
|
|
51
|
+
to: p.title,
|
|
52
|
+
children: p.title,
|
|
53
|
+
})),
|
|
54
|
+
{
|
|
55
|
+
name: 'cd ~',
|
|
56
|
+
to: '/',
|
|
57
|
+
children: 'cd ~',
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// Run middleware to allow plugins to transform the title list
|
|
62
|
+
const transformedList = await registry.runMiddleware('transformTitleList', baseTitleList);
|
|
63
|
+
setTitleList(transformedList);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
transformTitleList();
|
|
67
|
+
}, [data, isLoading, error]);
|
|
45
68
|
|
|
46
69
|
const posts =
|
|
47
70
|
isLoading || name == '' || error || !data ? data : data.find((p: any) => p.title == name);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { IHostContext } from '#/types/hostApi';
|
|
2
|
+
import { IPlugin } from '#/types/plugin';
|
|
3
|
+
|
|
4
|
+
export interface TitleListItem {
|
|
5
|
+
name: string;
|
|
6
|
+
to: string;
|
|
7
|
+
children: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface FontConfig {
|
|
12
|
+
name: string;
|
|
13
|
+
src: string;
|
|
14
|
+
format: 'woff2' | 'woff' | 'ttf' | 'otf';
|
|
15
|
+
fontWeight?: string;
|
|
16
|
+
fontStyle?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FontRule {
|
|
20
|
+
match: string | RegExp;
|
|
21
|
+
fontFamily: string;
|
|
22
|
+
fontConfig?: FontConfig;
|
|
23
|
+
extraCls?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class FontStylerPlugin implements IPlugin {
|
|
27
|
+
id = 'fontstyler';
|
|
28
|
+
name = 'Font Styler';
|
|
29
|
+
description = 'Apply custom fonts to specific post titles';
|
|
30
|
+
version = '1.0.0';
|
|
31
|
+
author = 'bbki.ng';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 配置:标题匹配规则 → 字体配置
|
|
35
|
+
* 支持精确字符串匹配或正则表达式匹配
|
|
36
|
+
*
|
|
37
|
+
* 示例:
|
|
38
|
+
* - 精确匹配: { match: '某篇文章标题', fontFamily: 'CustomFont1', fontConfig: {...} }
|
|
39
|
+
* - 正则匹配: { match: /^示例/, fontFamily: 'CustomFont2', fontConfig: {...} }
|
|
40
|
+
*/
|
|
41
|
+
private fontRules: FontRule[] = [
|
|
42
|
+
{
|
|
43
|
+
match: '小乌鸦合集',
|
|
44
|
+
fontFamily: 'xwy',
|
|
45
|
+
fontConfig: {
|
|
46
|
+
name: 'xwy',
|
|
47
|
+
src: '/fonts/xwy.woff2',
|
|
48
|
+
format: 'woff2',
|
|
49
|
+
},
|
|
50
|
+
extraCls: 'text-2xl',
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
private loadedFonts = new Set<string>();
|
|
55
|
+
|
|
56
|
+
onInstall = (ctx: IHostContext) => {
|
|
57
|
+
// 加载所有需要的字体文件
|
|
58
|
+
this.fontRules.forEach(rule => {
|
|
59
|
+
if (rule.fontConfig && !this.loadedFonts.has(rule.fontConfig.name)) {
|
|
60
|
+
this.loadFont(rule.fontConfig);
|
|
61
|
+
this.loadedFonts.add(rule.fontConfig.name);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
ctx.api.registerMiddleware(
|
|
66
|
+
'transformTitleList',
|
|
67
|
+
this.transformTitleList,
|
|
68
|
+
this.id,
|
|
69
|
+
10 // weight
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 动态加载字体文件
|
|
75
|
+
* 通过创建 style 标签添加 @font-face 规则
|
|
76
|
+
*/
|
|
77
|
+
private loadFont = (config: FontConfig) => {
|
|
78
|
+
const styleId = `font-${config.name}`;
|
|
79
|
+
if (document.getElementById(styleId)) return;
|
|
80
|
+
|
|
81
|
+
const style = document.createElement('style');
|
|
82
|
+
style.id = styleId;
|
|
83
|
+
style.textContent = `
|
|
84
|
+
@font-face {
|
|
85
|
+
font-family: '${config.name}';
|
|
86
|
+
src: url('${config.src}') format('${config.format}');
|
|
87
|
+
font-weight: ${config.fontWeight || 'normal'};
|
|
88
|
+
font-style: ${config.fontStyle || 'normal'};
|
|
89
|
+
font-display: swap;
|
|
90
|
+
}
|
|
91
|
+
.font-${config.name} {
|
|
92
|
+
font-family: '${config.name}', monospace;
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
document.head.appendChild(style);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 转换标题列表,为匹配的标题添加字体类名
|
|
100
|
+
*/
|
|
101
|
+
private transformTitleList = (titleList: TitleListItem[]): TitleListItem[] => {
|
|
102
|
+
return titleList.map(item => {
|
|
103
|
+
for (const rule of this.fontRules) {
|
|
104
|
+
const match =
|
|
105
|
+
typeof rule.match === 'string' ? item.name === rule.match : rule.match.test(item.name);
|
|
106
|
+
|
|
107
|
+
if (match) {
|
|
108
|
+
return {
|
|
109
|
+
...item,
|
|
110
|
+
className: `font-${rule.fontFamily} ${rule.extraCls || ''}`.trim(),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return item;
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
onDisable = async () => {
|
|
119
|
+
// 清理加载的字体样式
|
|
120
|
+
this.loadedFonts.forEach(fontName => {
|
|
121
|
+
const styleEl = document.getElementById(`font-${fontName}`);
|
|
122
|
+
if (styleEl) {
|
|
123
|
+
styleEl.remove();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
this.loadedFonts.clear();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
onDestroy = () => {};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default new FontStylerPlugin();
|
package/src/plugins/manifest.ts
CHANGED
package/src/types/slots.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export type SlotName = 'leftCol' | 'rightCol' | 'articleActionRow';
|
|
2
|
-
export type HookPoint = 'filterPosts' | 'transformPostContent';
|
|
2
|
+
export type HookPoint = 'filterPosts' | 'transformPostContent' | 'transformTitleList';
|