@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 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
@@ -15,5 +15,3 @@ declare global {
15
15
  }
16
16
  }
17
17
  }
18
-
19
- export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbki.ng/site",
3
- "version": "5.5.35",
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"
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, Streaming } from './pages';
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' /*'extra-entry'*/];
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
  };
@@ -1,4 +1,2 @@
1
1
  export { usePaths } from './use_paths';
2
- export { useStreaming } from './use_streaming';
3
- export type { StreamingItem } from './use_streaming';
4
2
  export { useGlobalLoading } from './use_loading';
@@ -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
  });
@@ -11,10 +11,6 @@ export const Cover = (_: { className?: string }) => {
11
11
  to: '/blog',
12
12
  children: 'cd ./blog',
13
13
  },
14
- {
15
- to: '/now',
16
- children: 'cd ./now',
17
- },
18
14
  ],
19
15
  []
20
16
  );
@@ -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 transformedContent = useMiddlewareTransformedData('transformPostContent', posts?.content);
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 = posts.createdAt ? posts.createdAt.split('T')[0] : '';
33
+ const date = p.createdAt ? p.createdAt.split('T')[0] : '';
31
34
 
32
35
  return (
33
36
  <ArticlePage title={title} date={date}>
@@ -1,2 +1,2 @@
1
1
  export { Cover } from './cover';
2
- export { default as Streaming } from './streaming';
2
+ export { default as Streaming } from '../../plugins/now/components/streaming';
@@ -2,33 +2,38 @@ import { API_ENDPOINT } from '@/constants/routes';
2
2
 
3
3
  import { getStableDeviceId } from './fingerprints';
4
4
 
5
- type Fetcher = (resource: string, init?: any) => Promise<any>;
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
- export const baseFetcher = async (resource: string, init: RequestInit = {}) => {
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
- return fetch(resource, {
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: string, init: RequestInit = {}) =>
36
+ async (resource, init = {}) =>
32
37
  fetcher(`${apiEndPoint}/${resource}`, { ...init, mode: 'cors' });
33
38
 
34
39
  export const cfApiFetcher = withBBApi(baseFetcher)(API_ENDPOINT);
@@ -0,0 +1,3 @@
1
+ import { PluginID } from '#/types/plugin';
2
+
3
+ export const AdminPluginIDSet: Set<PluginID> = new Set(['store']);
@@ -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 ctx = this.createHostContext();
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);
@@ -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
- stroke-linejoin="round"
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
- fill-rule="evenodd"
21
- clip-rule="evenodd"
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 { useStreaming, StreamingItem } from '@/hooks/use_streaming';
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
- // Extend JSX IntrinsicElements for the web component
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: NodeJS.Timeout;
43
+ let timer: ReturnType<typeof setTimeout>;
43
44
  if (!isLoading && el) {
44
45
  // 检查自定义元素是否已定义并升级
45
46
  if (el.scrollToBottom) {
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
+
2
3
  import { BbMsgHistoryElement } from '.';
3
4
 
4
5
  export const useScrollBtnVisibility = (ele: BbMsgHistoryElement) => {
@@ -0,0 +1,7 @@
1
+ import { createPluginCtx } from '#/core/context';
2
+
3
+ export interface INowCtx {
4
+ setLoading: (loading: boolean) => void;
5
+ }
6
+
7
+ export const NowCtx = createPluginCtx<INowCtx>();
@@ -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 { useGlobalLoading } from './use_loading';
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' : API_ENDPOINT;
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
- useGlobalLoading('streaming', isLoading);
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
- import { StreamingItem } from '@/hooks/use_streaming';
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 '';
@@ -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 { TitleListItem } from '#/types/posts';
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]">&#xE01A&#xE003</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]">&#xE01A&#xE003</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'];
@@ -20,6 +20,9 @@ export interface IHostApi {
20
20
  pluginId: string,
21
21
  weight?: number
22
22
  ) => void;
23
+
24
+ installPlugin?: (id: PluginID) => void;
25
+ disablePlugin?: (id: PluginID) => void;
23
26
  }
24
27
 
25
28
  export type PluginInitializer = (api: IHostApi) => void;
@@ -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
@@ -10,5 +10,5 @@
10
10
  "#/*": ["./src/*"]
11
11
  }
12
12
  },
13
- "include": ["./src", "vite.config.js", "index.d.ts"]
13
+ "include": ["./src", "vite.config.js", "**/*.d.ts"]
14
14
  }