@bbki.ng/site 5.5.35 → 5.5.37
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 +14 -0
- package/eslint.config.js +7 -0
- package/index.d.ts +0 -2
- package/package.json +2 -2
- package/src/blog/app.tsx +3 -4
- package/src/blog/hooks/index.ts +0 -2
- package/src/blog/hooks/use_plugin_entries.ts +1 -8
- package/src/blog/hooks/use_posts.ts +6 -1
- package/src/blog/pages/cover/index.tsx +0 -4
- package/src/blog/pages/extensions/txt/article.tsx +5 -2
- package/src/blog/pages/index.tsx +1 -1
- package/src/blog/utils/index.ts +15 -10
- package/src/core/const/index.ts +3 -0
- package/src/core/pluginManager.ts +16 -3
- package/src/plugins/manifest.ts +13 -0
- package/src/plugins/now/components/index.tsx +16 -0
- package/src/{blog/pages → plugins/now/components}/streaming/arrow-down.tsx +3 -3
- package/src/{blog/pages → plugins/now/components}/streaming/index.tsx +9 -8
- package/src/{blog/pages → plugins/now/components}/streaming/useScrollBtnVisibility.ts +1 -0
- package/src/plugins/now/context/index.ts +7 -0
- package/src/{blog → plugins/now}/hooks/use_streaming.ts +5 -4
- package/src/plugins/now/index.ts +37 -0
- package/src/{blog → plugins/now}/utils/streaming.ts +7 -3
- package/src/plugins/xwy/index.ts +27 -78
- package/src/plugins/xwy/transformers/index.ts +67 -0
- package/src/plugins/xwy/types/index.ts +18 -0
- package/src/types/hostApi.ts +3 -0
- package/src/types/plugin.ts +11 -1
- package/tsconfig.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @bbki.ng/site
|
|
2
2
|
|
|
3
|
+
## 5.5.37
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 56a7bc7: fix 404 error page
|
|
8
|
+
- Updated dependencies [56a7bc7]
|
|
9
|
+
- @bbki.ng/ui@0.2.18
|
|
10
|
+
|
|
11
|
+
## 5.5.36
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- e6d75b2: move now page to plugin
|
|
16
|
+
|
|
3
17
|
## 5.5.35
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/eslint.config.js
CHANGED
|
@@ -9,6 +9,13 @@ const __dirname = path.dirname(__filename);
|
|
|
9
9
|
export default [
|
|
10
10
|
includeIgnoreFile(path.resolve(__dirname, '.gitignore')),
|
|
11
11
|
...webConfig,
|
|
12
|
+
{
|
|
13
|
+
languageOptions: {
|
|
14
|
+
globals: {
|
|
15
|
+
GLOBAL_COMMIT_HASH: 'readonly',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
12
19
|
{
|
|
13
20
|
rules: {
|
|
14
21
|
// Site specific overrides
|
package/index.d.ts
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.37",
|
|
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.18"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@eslint/compat": "^1.0.0",
|
package/src/blog/app.tsx
CHANGED
|
@@ -11,11 +11,11 @@ import { usePlugins } from '#/core/hooks/use_plugins';
|
|
|
11
11
|
import { SWR } from '@/swr';
|
|
12
12
|
import type { PluginID } from '#/types/plugin';
|
|
13
13
|
|
|
14
|
-
import { Cover
|
|
14
|
+
import { Cover } from './pages';
|
|
15
15
|
import { usePluginEntries } from './hooks/use_plugin_entries';
|
|
16
16
|
import { BaseLayout } from './components/BaseLayout';
|
|
17
17
|
|
|
18
|
-
const APP_PLUGIN_IDS: Array<PluginID> = [/*'sticker',*/ 'xwy', 'extra-cd'
|
|
18
|
+
const APP_PLUGIN_IDS: Array<PluginID> = [/*'sticker',*/ 'xwy', 'extra-cd', 'now'];
|
|
19
19
|
|
|
20
20
|
const AppRoutes = () => {
|
|
21
21
|
usePlugins(APP_PLUGIN_IDS);
|
|
@@ -33,12 +33,11 @@ const AppRoutes = () => {
|
|
|
33
33
|
</Route>
|
|
34
34
|
|
|
35
35
|
<Route path="bot" element={<BotRedirect />} />
|
|
36
|
-
<Route path="now" element={<Streaming />} />
|
|
37
36
|
{pluginEntries?.map(route => (
|
|
38
37
|
<Route key={route.path} path={route.path} element={<Slot name="route" data={route} />} />
|
|
39
38
|
))}
|
|
39
|
+
<Route path="*" element={pluginEntries?.length ? <NotFound /> : null} />
|
|
40
40
|
</Route>
|
|
41
|
-
<Route path="*" element={<NotFound />} />
|
|
42
41
|
</Routes>
|
|
43
42
|
);
|
|
44
43
|
};
|
package/src/blog/hooks/index.ts
CHANGED
|
@@ -3,14 +3,7 @@ import { type PathRouteProps } from 'react-router-dom';
|
|
|
3
3
|
|
|
4
4
|
import { useMiddlewareRunner } from '#/core/hooks';
|
|
5
5
|
|
|
6
|
-
const protectedRoutesSet = new Set<string>([
|
|
7
|
-
'/bot',
|
|
8
|
-
'/now',
|
|
9
|
-
'/',
|
|
10
|
-
'/blog',
|
|
11
|
-
'/blog/:title',
|
|
12
|
-
'default',
|
|
13
|
-
]);
|
|
6
|
+
const protectedRoutesSet = new Set<string>(['/bot', '/', '/blog', '/blog/:title', 'default']);
|
|
14
7
|
|
|
15
8
|
type PluginRoute = Omit<PathRouteProps, 'element'>;
|
|
16
9
|
|
|
@@ -10,6 +10,11 @@ import { useMiddlewareRunner } from '#/core/hooks/useMiddlewareTransData';
|
|
|
10
10
|
const isProd = true;
|
|
11
11
|
const POSTS_API = !isProd ? '/api/posts' : `${API_ENDPOINT}/posts`;
|
|
12
12
|
|
|
13
|
+
interface PostsApiResponse {
|
|
14
|
+
data: IPost[];
|
|
15
|
+
// 可能还有其他字段如 status, message 等
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
export interface TitleListItem {
|
|
14
19
|
name: string;
|
|
15
20
|
to: string;
|
|
@@ -18,7 +23,7 @@ export interface TitleListItem {
|
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export const usePosts = (name: string = '', suspense?: boolean) => {
|
|
21
|
-
const { data: response, error: swrError } = useSWR(POSTS_API, baseFetcher, {
|
|
26
|
+
const { data: response, error: swrError } = useSWR<PostsApiResponse>(POSTS_API, baseFetcher, {
|
|
22
27
|
revalidateOnFocus: false,
|
|
23
28
|
suspense,
|
|
24
29
|
});
|
|
@@ -6,6 +6,7 @@ import { usePosts } from '@/hooks/use_posts';
|
|
|
6
6
|
import { ArticlePage } from '@/components/article';
|
|
7
7
|
import { useBlogScrollReset } from '@/hooks/use_blog_scroll_pos_restoration';
|
|
8
8
|
import { useMiddlewareTransformedData } from '#/core/hooks/useMiddlewareTransData';
|
|
9
|
+
import { IPost } from '#/types/posts';
|
|
9
10
|
|
|
10
11
|
function TxtArticle() {
|
|
11
12
|
const { title } = useParams();
|
|
@@ -13,7 +14,9 @@ function TxtArticle() {
|
|
|
13
14
|
|
|
14
15
|
useBlogScrollReset();
|
|
15
16
|
|
|
16
|
-
const
|
|
17
|
+
const p = posts as IPost;
|
|
18
|
+
|
|
19
|
+
const transformedContent = useMiddlewareTransformedData('transformPostContent', p?.content);
|
|
17
20
|
|
|
18
21
|
if (!title) {
|
|
19
22
|
return <NotFound />;
|
|
@@ -27,7 +30,7 @@ function TxtArticle() {
|
|
|
27
30
|
return null;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
const date =
|
|
33
|
+
const date = p.createdAt ? p.createdAt.split('T')[0] : '';
|
|
31
34
|
|
|
32
35
|
return (
|
|
33
36
|
<ArticlePage title={title} date={date}>
|
package/src/blog/pages/index.tsx
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { Cover } from './cover';
|
|
2
|
-
export { default as Streaming } from '
|
|
2
|
+
export { default as Streaming } from '../../plugins/now/components/streaming';
|
package/src/blog/utils/index.ts
CHANGED
|
@@ -2,33 +2,38 @@ import { API_ENDPOINT } from '@/constants/routes';
|
|
|
2
2
|
|
|
3
3
|
import { getStableDeviceId } from './fingerprints';
|
|
4
4
|
|
|
5
|
-
type
|
|
5
|
+
type RequestInit = globalThis.RequestInit;
|
|
6
|
+
|
|
7
|
+
type Fetcher = <T>(resource: string, init?: RequestInit) => Promise<T>;
|
|
6
8
|
|
|
7
9
|
export const floatNumberToPercentageString = (num: number): string => {
|
|
8
10
|
return `${num * 100}%`;
|
|
9
11
|
};
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
// 2. 显式声明为 Fetcher 类型,init 默认值改为 RequestInit
|
|
14
|
+
export const baseFetcher: Fetcher = async (resource, init = {}) => {
|
|
12
15
|
const headers = new Headers(init.headers || {});
|
|
13
16
|
const fp = await getStableDeviceId();
|
|
14
17
|
headers.set('X-Device-Fingerprint', fp.id);
|
|
15
|
-
|
|
18
|
+
|
|
19
|
+
const response = await fetch(resource, {
|
|
16
20
|
...init,
|
|
17
21
|
headers,
|
|
18
22
|
mode: 'cors',
|
|
19
|
-
}).then(res => {
|
|
20
|
-
if (!res.ok) {
|
|
21
|
-
throw new Error('An error occurred while fetching the data.');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return res.json();
|
|
25
23
|
});
|
|
24
|
+
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error('An error occurred while fetching the data.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return response.json();
|
|
26
30
|
};
|
|
27
31
|
|
|
32
|
+
// 3. 简化 withBBApi 类型定义,确保参数类型一致
|
|
28
33
|
export const withBBApi =
|
|
29
34
|
(fetcher: Fetcher) =>
|
|
30
35
|
(apiEndPoint: string): Fetcher =>
|
|
31
|
-
async (resource
|
|
36
|
+
async (resource, init = {}) =>
|
|
32
37
|
fetcher(`${apiEndPoint}/${resource}`, { ...init, mode: 'cors' });
|
|
33
38
|
|
|
34
39
|
export const cfApiFetcher = withBBApi(baseFetcher)(API_ENDPOINT);
|
|
@@ -2,11 +2,12 @@ import React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { IHostContext } from '#/types/hostApi';
|
|
4
4
|
import type { SlotName, HookPoint, IComPropsRegisteredToSlot } from '#/types/slots';
|
|
5
|
-
import { IPlugin, PluginEvents, PluginID } from '#/types/plugin';
|
|
5
|
+
import { IPlugin, PluginEvents, PluginID, PluginPerm } from '#/types/plugin';
|
|
6
6
|
import { getStableDeviceId } from '@/utils/fingerprints';
|
|
7
7
|
|
|
8
8
|
import { registry } from './registry';
|
|
9
9
|
import { createEventBus } from './utils/eventBus';
|
|
10
|
+
import { AdminPluginIDSet } from './const';
|
|
10
11
|
|
|
11
12
|
const pluginModules = import.meta.glob('../plugins/*/index.ts');
|
|
12
13
|
|
|
@@ -15,9 +16,15 @@ class PluginManager {
|
|
|
15
16
|
|
|
16
17
|
private bus = createEventBus<PluginEvents>();
|
|
17
18
|
|
|
18
|
-
private createHostContext(): IHostContext {
|
|
19
|
+
private createHostContext(perm: PluginPerm = 'guest'): IHostContext {
|
|
20
|
+
const adminOnlyApi = {
|
|
21
|
+
installPlugin: (id: PluginID) => this.loadPlugin(id),
|
|
22
|
+
disablePlugin: (id: PluginID) => this.disablePlugin(id),
|
|
23
|
+
};
|
|
24
|
+
|
|
19
25
|
return {
|
|
20
26
|
api: {
|
|
27
|
+
...(perm === 'admin' ? adminOnlyApi : {}),
|
|
21
28
|
setLoading: (id: PluginID, loading: boolean) => {
|
|
22
29
|
this.bus.emit('plugin:loading:changed', { id, loading });
|
|
23
30
|
},
|
|
@@ -82,7 +89,13 @@ class PluginManager {
|
|
|
82
89
|
|
|
83
90
|
const p: IPlugin = module.default;
|
|
84
91
|
|
|
85
|
-
const
|
|
92
|
+
const perm = (p as IPlugin & { perm?: PluginPerm }).perm || 'guest';
|
|
93
|
+
if (perm === 'admin' && !AdminPluginIDSet.has(pluginId)) {
|
|
94
|
+
console.error(`Plugin ${pluginId} requires admin permission, but it's not trusted.`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const ctx = this.createHostContext(perm);
|
|
86
99
|
|
|
87
100
|
if (p.onInstall) {
|
|
88
101
|
await p.onInstall(ctx);
|
package/src/plugins/manifest.ts
CHANGED
|
@@ -5,6 +5,19 @@ export const PLUGIN_MANIFEST = [
|
|
|
5
5
|
version: '0.1.0',
|
|
6
6
|
description: 'A sticker plugin',
|
|
7
7
|
},
|
|
8
|
+
{
|
|
9
|
+
name: 'store',
|
|
10
|
+
id: 'store',
|
|
11
|
+
version: '0.1.0',
|
|
12
|
+
description: 'A plugin for state management and data persistence.',
|
|
13
|
+
perm: 'admin',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'now',
|
|
17
|
+
id: 'now',
|
|
18
|
+
version: '1.0.0',
|
|
19
|
+
description: 'show real-time site status and logs',
|
|
20
|
+
},
|
|
8
21
|
{
|
|
9
22
|
name: 'extra-cd',
|
|
10
23
|
id: 'extra-cd',
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { PathRouteProps } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
import { IComPropsRegisteredToSlot } from '#/types/slots';
|
|
5
|
+
|
|
6
|
+
import Streaming from './streaming';
|
|
7
|
+
|
|
8
|
+
export const pageNow = ({ data }: IComPropsRegisteredToSlot) => {
|
|
9
|
+
const route = data as Omit<PathRouteProps, 'element'>;
|
|
10
|
+
|
|
11
|
+
if (route.path !== '/now') {
|
|
12
|
+
return <></>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return <Streaming />;
|
|
16
|
+
};
|
|
@@ -5,7 +5,7 @@ export const ArrowDownIcon = ({ show }: { show: boolean }) => (
|
|
|
5
5
|
<svg
|
|
6
6
|
data-testid="geist-icon"
|
|
7
7
|
height="16"
|
|
8
|
-
|
|
8
|
+
strokeLinejoin="round"
|
|
9
9
|
viewBox="0 0 16 16"
|
|
10
10
|
width="16"
|
|
11
11
|
style={{
|
|
@@ -17,8 +17,8 @@ export const ArrowDownIcon = ({ show }: { show: boolean }) => (
|
|
|
17
17
|
})}
|
|
18
18
|
>
|
|
19
19
|
<path
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
fillRule="evenodd"
|
|
21
|
+
clipRule="evenodd"
|
|
22
22
|
d="M8.70711 1.39644C8.31659 1.00592 7.68342 1.00592 7.2929 1.39644L2.21968 6.46966L1.68935 6.99999L2.75001 8.06065L3.28034 7.53032L7.25001 3.56065V14.25V15H8.75001V14.25V3.56065L12.7197 7.53032L13.25 8.06065L14.3107 6.99999L13.7803 6.46966L8.70711 1.39644Z"
|
|
23
23
|
fill="currentColor"
|
|
24
24
|
></path>
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import { formatStreamingData } from '@/utils/streaming';
|
|
4
|
-
import { Button, Panel } from '@bbki.ng/ui';
|
|
5
|
-
import { useScrollBtnVisibility } from './useScrollBtnVisibility';
|
|
6
|
-
import { Link } from '@bbki.ng/ui';
|
|
7
|
-
import { ArrowDownIcon } from './arrow-down';
|
|
2
|
+
import { Panel, Link } from '@bbki.ng/ui';
|
|
8
3
|
import classNames from 'classnames';
|
|
9
4
|
|
|
10
|
-
|
|
5
|
+
import { formatStreamingData } from '#/plugins/now/utils/streaming';
|
|
6
|
+
|
|
7
|
+
import { useStreaming } from '../../hooks/use_streaming';
|
|
8
|
+
|
|
9
|
+
import { useScrollBtnVisibility } from './useScrollBtnVisibility';
|
|
10
|
+
|
|
11
11
|
declare global {
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
12
13
|
namespace JSX {
|
|
13
14
|
interface IntrinsicElements {
|
|
14
15
|
'bb-msg-history': React.DetailedHTMLProps<
|
|
@@ -39,7 +40,7 @@ const Streaming = () => {
|
|
|
39
40
|
|
|
40
41
|
useEffect(() => {
|
|
41
42
|
const el = bbMsgHistoryRef.current;
|
|
42
|
-
let timer:
|
|
43
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
43
44
|
if (!isLoading && el) {
|
|
44
45
|
// 检查自定义元素是否已定义并升级
|
|
45
46
|
if (el.scrollToBottom) {
|
|
@@ -2,14 +2,13 @@ import useSWR from 'swr';
|
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
|
|
4
4
|
import { baseFetcher } from '@/utils';
|
|
5
|
-
import { API_ENDPOINT } from '@/constants/routes';
|
|
6
5
|
|
|
7
|
-
import {
|
|
6
|
+
import { NowCtx } from '../context';
|
|
8
7
|
|
|
9
8
|
// In dev, use /api prefix to leverage Vite proxy to localhost:8787
|
|
10
9
|
// const isProd = typeof window !== 'undefined' && /^https:\/\/bbki.ng/.test(window.location.href);
|
|
11
10
|
const isProd = true;
|
|
12
|
-
const API_BASE = !isProd ? '/api' :
|
|
11
|
+
const API_BASE = !isProd ? '/api' : 'https://cf.bbki.ng';
|
|
13
12
|
|
|
14
13
|
export type StreamingItem = {
|
|
15
14
|
id: string;
|
|
@@ -93,6 +92,8 @@ export function useStreaming(options: UseStreamingOptions = {}) {
|
|
|
93
92
|
|
|
94
93
|
const isLoading = !data && !error;
|
|
95
94
|
|
|
95
|
+
const nowCtx = NowCtx.useCtx();
|
|
96
|
+
|
|
96
97
|
const [, forceUpdate] = useState(0);
|
|
97
98
|
|
|
98
99
|
// make rerender when customElement defined
|
|
@@ -102,7 +103,7 @@ export function useStreaming(options: UseStreamingOptions = {}) {
|
|
|
102
103
|
});
|
|
103
104
|
}, []);
|
|
104
105
|
|
|
105
|
-
|
|
106
|
+
nowCtx.setLoading(isLoading);
|
|
106
107
|
|
|
107
108
|
return {
|
|
108
109
|
streaming: data?.data ?? [],
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { IHostContext } from '#/types/hostApi';
|
|
2
|
+
import { IPlugin } from '#/types/plugin';
|
|
3
|
+
import { buildEntryCreator } from '#/utils';
|
|
4
|
+
|
|
5
|
+
import { pageNow } from './components';
|
|
6
|
+
import { NowCtx } from './context';
|
|
7
|
+
|
|
8
|
+
export class NowPlugin implements IPlugin {
|
|
9
|
+
id: string = 'now';
|
|
10
|
+
name: string = 'Now';
|
|
11
|
+
description?: string | undefined = 'Manages real-time site status and logs';
|
|
12
|
+
version: string = '1.0.0';
|
|
13
|
+
author?: string | undefined = 'bbki.ng';
|
|
14
|
+
|
|
15
|
+
onInstall = (ctx: IHostContext) => {
|
|
16
|
+
const nowEntryCreator = buildEntryCreator(ctx);
|
|
17
|
+
const { withCtx } = NowCtx;
|
|
18
|
+
|
|
19
|
+
nowEntryCreator(
|
|
20
|
+
{
|
|
21
|
+
path: '/now',
|
|
22
|
+
label: 'cd ./now',
|
|
23
|
+
pageComponent: withCtx(
|
|
24
|
+
{
|
|
25
|
+
setLoading: loading => {
|
|
26
|
+
ctx.api.setLoading('now', loading);
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
pageNow
|
|
30
|
+
),
|
|
31
|
+
},
|
|
32
|
+
this.id
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default new NowPlugin();
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
export type StreamingItem = {
|
|
2
|
+
id: string;
|
|
3
|
+
author: string;
|
|
4
|
+
content: string;
|
|
5
|
+
type?: string;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
};
|
|
2
8
|
|
|
3
|
-
// Format: "author: content" (with optional timestamp)
|
|
4
|
-
// Timestamp format: YYYY-mm-dd HH:MM:SS
|
|
5
9
|
export const formatStreamingData = (items: StreamingItem[]): string => {
|
|
6
10
|
if (!items || items.length === 0) {
|
|
7
11
|
return '';
|
package/src/plugins/xwy/index.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { PathObj } from '@bbki.ng/ui';
|
|
2
|
-
|
|
3
1
|
import { IHostContext } from '#/types/hostApi';
|
|
4
2
|
import { IPlugin } from '#/types/plugin';
|
|
5
|
-
import {
|
|
3
|
+
import { HookPoint } from '#/types/slots';
|
|
6
4
|
|
|
5
|
+
import { HookPointTypeMap, Transformer } from './types';
|
|
7
6
|
import { SpecialTitle } from './components/article';
|
|
8
7
|
import { XwyLogo } from './components/logo';
|
|
9
8
|
import { FontRules, LOADING_CLASS } from './const';
|
|
10
9
|
import { loadFont } from './utils';
|
|
10
|
+
import {
|
|
11
|
+
transformBreadcrumbPaths,
|
|
12
|
+
transformTitleList,
|
|
13
|
+
transformPostContent,
|
|
14
|
+
pinTitle,
|
|
15
|
+
} from './transformers';
|
|
11
16
|
|
|
12
17
|
class XwyPlugin implements IPlugin {
|
|
13
18
|
id = 'fontstyler';
|
|
@@ -30,29 +35,7 @@ class XwyPlugin implements IPlugin {
|
|
|
30
35
|
}
|
|
31
36
|
});
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
ctx.api.registerMiddleware(
|
|
35
|
-
'transformTitleList',
|
|
36
|
-
this.transformTitleList,
|
|
37
|
-
this.id,
|
|
38
|
-
10 // weight
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
// 注册中间件,修改面包屑路径显示
|
|
42
|
-
ctx.api.registerMiddleware(
|
|
43
|
-
'transformBreadcrumbPath',
|
|
44
|
-
this.transformBreadcrumbPaths,
|
|
45
|
-
this.id,
|
|
46
|
-
10 // weight
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
// 注册中间件,修改文章内容
|
|
50
|
-
ctx.api.registerMiddleware(
|
|
51
|
-
'transformPostContent',
|
|
52
|
-
this.transformPostContent,
|
|
53
|
-
this.id,
|
|
54
|
-
10 // weight
|
|
55
|
-
);
|
|
38
|
+
this.batchRegisterMiddlewares(ctx);
|
|
56
39
|
|
|
57
40
|
// 注册logo插槽组件
|
|
58
41
|
ctx.api.registerSlot('logo', XwyLogo, this.id);
|
|
@@ -61,6 +44,24 @@ class XwyPlugin implements IPlugin {
|
|
|
61
44
|
ctx.api.registerSlot('articleTitle', SpecialTitle, this.id);
|
|
62
45
|
};
|
|
63
46
|
|
|
47
|
+
private trasformerMap: {
|
|
48
|
+
[K in Extract<HookPoint, keyof HookPointTypeMap>]?: Array<Transformer<K>>;
|
|
49
|
+
} = {
|
|
50
|
+
transformTitleList: [pinTitle, transformTitleList],
|
|
51
|
+
transformBreadcrumbPath: [transformBreadcrumbPaths],
|
|
52
|
+
transformPostContent: [transformPostContent],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
private batchRegisterMiddlewares(ctx: IHostContext) {
|
|
56
|
+
for (const [point, fns] of Object.entries(this.trasformerMap) as Array<
|
|
57
|
+
[HookPoint, Array<(baseData: unknown) => unknown>]
|
|
58
|
+
>) {
|
|
59
|
+
fns.forEach(fn => {
|
|
60
|
+
ctx.api.registerMiddleware(point, fn, this.id, 10);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
64
65
|
private initFontLoadingListener() {
|
|
65
66
|
// listen font loading status and add class to body for styling
|
|
66
67
|
console.log('Initializing font loading listener');
|
|
@@ -70,58 +71,6 @@ class XwyPlugin implements IPlugin {
|
|
|
70
71
|
});
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
private transformPostContent = (content: string = '') => {
|
|
74
|
-
// 在文章内容中替换特定关键词为特殊样式
|
|
75
|
-
return content
|
|
76
|
-
.replace(
|
|
77
|
-
/小乌鸦/g,
|
|
78
|
-
`<span class="font-xwy text-content-special text-[1.6rem] align-middle leading-[1.09]">小乌鸦</span>`
|
|
79
|
-
)
|
|
80
|
-
.replace(
|
|
81
|
-
/公园/,
|
|
82
|
-
`公园 <span class="font-xwy-icon text-[#679867] text-[1.6rem] align-middle leading-[1.09]"></span>`
|
|
83
|
-
);
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
private transformBreadcrumbPaths = (paths: PathObj[]) => {
|
|
87
|
-
const result = paths.map(p => {
|
|
88
|
-
if (p.name === '小乌鸦合集') {
|
|
89
|
-
return {
|
|
90
|
-
...p,
|
|
91
|
-
name: '小乌鸦合集',
|
|
92
|
-
className: 'font-xwy text-content-special text-[1.2rem] align-middle relative top-[1px]',
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return p;
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
console.log('Transformed breadcrumb paths:', result);
|
|
100
|
-
|
|
101
|
-
return result;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* 转换标题列表,为匹配的标题添加字体类名
|
|
106
|
-
*/
|
|
107
|
-
private transformTitleList = (titleList: TitleListItem[]): TitleListItem[] => {
|
|
108
|
-
return titleList.map(item => {
|
|
109
|
-
for (const rule of FontRules) {
|
|
110
|
-
const match =
|
|
111
|
-
typeof rule.match === 'string' ? item.name === rule.match : rule.match.test(item.name);
|
|
112
|
-
|
|
113
|
-
if (match) {
|
|
114
|
-
return {
|
|
115
|
-
...item,
|
|
116
|
-
variant: rule.variant || 'default',
|
|
117
|
-
className: `font-${rule.fontFamily} ${rule.extraCls || ''}`.trim(),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return item;
|
|
122
|
-
});
|
|
123
|
-
};
|
|
124
|
-
|
|
125
74
|
onDisable = async () => {
|
|
126
75
|
this.loadedFonts.forEach(fontName => {
|
|
127
76
|
const styleEl = document.getElementById(`font-${fontName}`);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { PathObj } from '@bbki.ng/ui';
|
|
2
|
+
|
|
3
|
+
import { TitleListItem } from '#/types/posts';
|
|
4
|
+
|
|
5
|
+
import { FontRules } from '../const';
|
|
6
|
+
|
|
7
|
+
export const transformPostContent = (content: string = '') => {
|
|
8
|
+
// 在文章内容中替换特定关键词为特殊样式
|
|
9
|
+
return content
|
|
10
|
+
.replace(
|
|
11
|
+
/小乌鸦/g,
|
|
12
|
+
`<span class="font-xwy text-content-special text-[1.6rem] align-middle leading-[1.09]">小乌鸦</span>`
|
|
13
|
+
)
|
|
14
|
+
.replace(
|
|
15
|
+
/公园/,
|
|
16
|
+
`公园 <span class="font-xwy-icon text-[#679867] text-[1.6rem] align-middle leading-[1.09]"></span>`
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const pinTitle = (titleList: TitleListItem[]) => {
|
|
21
|
+
const pinnedTitle = '小乌鸦合集';
|
|
22
|
+
const index = titleList.findIndex(item => item.name === pinnedTitle);
|
|
23
|
+
if (index > -1) {
|
|
24
|
+
const [item] = titleList.splice(index, 1);
|
|
25
|
+
titleList.unshift(item);
|
|
26
|
+
}
|
|
27
|
+
return titleList;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const transformBreadcrumbPaths = (paths: PathObj[]) => {
|
|
31
|
+
const result = paths.map(p => {
|
|
32
|
+
if (p.name === '小乌鸦合集') {
|
|
33
|
+
return {
|
|
34
|
+
...p,
|
|
35
|
+
name: '小乌鸦合集',
|
|
36
|
+
className: 'font-xwy text-content-special text-[1.2rem] align-middle relative top-[1px]',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return p;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
console.log('Transformed breadcrumb paths:', result);
|
|
44
|
+
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 转换标题列表,为匹配的标题添加字体类名
|
|
50
|
+
*/
|
|
51
|
+
export const transformTitleList = (titleList: TitleListItem[]): TitleListItem[] => {
|
|
52
|
+
return titleList.map(item => {
|
|
53
|
+
for (const rule of FontRules) {
|
|
54
|
+
const match =
|
|
55
|
+
typeof rule.match === 'string' ? item.name === rule.match : rule.match.test(item.name);
|
|
56
|
+
|
|
57
|
+
if (match) {
|
|
58
|
+
return {
|
|
59
|
+
...item,
|
|
60
|
+
variant: rule.variant || 'default',
|
|
61
|
+
className: `font-${rule.fontFamily} ${rule.extraCls || ''}`.trim(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return item;
|
|
66
|
+
});
|
|
67
|
+
};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { PathObj } from '@bbki.ng/ui';
|
|
2
|
+
|
|
3
|
+
import { TitleListItem } from '#/types/posts';
|
|
4
|
+
|
|
1
5
|
export interface FontConfig {
|
|
2
6
|
name: string;
|
|
3
7
|
src: string;
|
|
@@ -15,3 +19,17 @@ export interface FontRule {
|
|
|
15
19
|
extraCls?: string;
|
|
16
20
|
variant?: 'default' | 'special';
|
|
17
21
|
}
|
|
22
|
+
|
|
23
|
+
// 1. 先定义每个 HookPoint 对应的类型映射
|
|
24
|
+
export interface HookPointTypeMap {
|
|
25
|
+
transformTitleList: { input: TitleListItem[]; output: TitleListItem[] };
|
|
26
|
+
transformBreadcrumbPath: { input: PathObj[]; output: PathObj[] };
|
|
27
|
+
transformPostContent: { input: string; output: string };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type HookPoint = keyof HookPointTypeMap;
|
|
31
|
+
|
|
32
|
+
// 2. 定义带类型的 Transformer
|
|
33
|
+
export type Transformer<K extends HookPoint> = (
|
|
34
|
+
baseData: HookPointTypeMap[K]['input']
|
|
35
|
+
) => HookPointTypeMap[K]['output'];
|
package/src/types/hostApi.ts
CHANGED
package/src/types/plugin.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface IPlugin {
|
|
|
16
16
|
onDestroy?: () => void;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export type PluginID = 'sticker' | 'xwy' | 'extra-cd' | 'extra-entry';
|
|
19
|
+
export type PluginID = 'sticker' | 'xwy' | 'extra-cd' | 'extra-entry' | 'store' | 'now';
|
|
20
20
|
|
|
21
21
|
export interface IPluginEntry {
|
|
22
22
|
path: string;
|
|
@@ -32,3 +32,13 @@ export interface IPluginLoadingPayload {
|
|
|
32
32
|
id: PluginID;
|
|
33
33
|
loading: boolean;
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
export type PluginPerm = 'guest' | 'admin';
|
|
37
|
+
|
|
38
|
+
export interface IPluginManifestEntry {
|
|
39
|
+
id: PluginID;
|
|
40
|
+
name: string;
|
|
41
|
+
version: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
perm?: PluginPerm;
|
|
44
|
+
}
|
package/tsconfig.json
CHANGED