@bbki.ng/site 5.8.1 → 5.8.3

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,17 @@
1
1
  # @bbki.ng/site
2
2
 
3
+ ## 5.8.3
4
+
5
+ ### Patch Changes
6
+
7
+ - b74177a: refactor ui service
8
+
9
+ ## 5.8.2
10
+
11
+ ### Patch Changes
12
+
13
+ - b4037f7: udpate notification zIndex
14
+
3
15
  ## 5.8.1
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbki.ng/site",
3
- "version": "5.8.1",
3
+ "version": "5.8.3",
4
4
  "description": "code behind bbki.ng",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,27 +1,14 @@
1
1
  import React from 'react';
2
2
 
3
- import { useSlotComp } from '../hooks/useSlotComp';
3
+ import { SystemUIService } from '#/core/plugin-system/services/systemUIService';
4
+ import { createSlotComponent } from '#/core/shared-service/factory/createUIServiceReactKit';
5
+
4
6
  import { SystemSlotName } from '../shared-service/contract/IUIService';
5
7
 
6
8
  export interface ISlotProps {
7
9
  name: SystemSlotName;
8
10
  data?: unknown;
9
-
10
11
  placeholder?: React.ReactNode;
11
12
  }
12
13
 
13
- export const Slot: React.FC<ISlotProps> = ({ name, data, placeholder }) => {
14
- const components = useSlotComp(name);
15
-
16
- if (components.length === 0) {
17
- return <>{placeholder}</>;
18
- }
19
-
20
- return (
21
- <>
22
- {components.map((Component, index) => (
23
- <Component key={index} data={data} />
24
- ))}
25
- </>
26
- );
27
- };
14
+ export const Slot = createSlotComponent<SystemSlotName>(SystemUIService.getInstance());
@@ -1,6 +1,6 @@
1
1
  export { useSlotComp } from './useSlotComp';
2
2
  export { useMiddlewareRunner } from './useMiddlewareTransData';
3
3
  export type {
4
- UseMiddlewareTransDataOptions,
5
- UseMiddlewareTransDataResult,
4
+ UseMiddlewareRunnerOptions as UseMiddlewareTransDataOptions,
5
+ UseMiddlewareRunnerResult as UseMiddlewareTransDataResult,
6
6
  } from './useMiddlewareTransData';
@@ -1,82 +1,13 @@
1
- import { useState, useEffect, useCallback, useRef } from 'react';
2
-
3
- import { SystemUIService } from '../plugin-system/services/systemUIService';
4
- import { SystemDataHookPoint } from '../shared-service/contract/IUIService';
5
-
6
- export interface UseMiddlewareTransDataOptions {
7
- hookPoint: SystemDataHookPoint;
8
- onMiddlewareChange?: () => void; // 通知外部,由外部决定是否 run
9
- }
10
-
11
- export interface UseMiddlewareTransDataResult<T> {
12
- loading: boolean;
13
- error: Error | null;
14
- run: (inputData: T) => Promise<T>;
15
- }
16
-
17
- export function useMiddlewareRunner<T>({
18
- hookPoint,
19
- onMiddlewareChange,
20
- }: UseMiddlewareTransDataOptions): UseMiddlewareTransDataResult<T> {
21
- const [loading, setLoading] = useState(false);
22
- const [error, setError] = useState<Error | null>(null);
23
-
24
- const run = useCallback(
25
- async (inputData: T) => {
26
- setLoading(true);
27
- setError(null);
28
- try {
29
- const result = await SystemUIService.getInstance().runMiddlewares(hookPoint, inputData);
30
- return result;
31
- } catch (err) {
32
- const error = err instanceof Error ? err : new Error(String(err));
33
- setError(error);
34
- throw error;
35
- } finally {
36
- setLoading(false);
37
- }
38
- },
39
- [hookPoint]
40
- );
41
-
42
- // 仅通知,不执行
43
- useEffect(() => {
44
- const unsubscribe = SystemUIService.getInstance().subscribeMiddlewareChange(hook => {
45
- if (hook === hookPoint) {
46
- onMiddlewareChange?.();
47
- }
48
- });
49
-
50
- return () => {
51
- unsubscribe();
52
- };
53
- }, [hookPoint, onMiddlewareChange]);
54
-
55
- return { loading, error, run };
56
- }
57
-
58
- export const useMiddlewareTransformedData = <T>(
59
- hookPoint: SystemDataHookPoint,
60
- defaultValue: T
61
- ) => {
62
- const [result, setResult] = useState<T>(defaultValue);
63
-
64
- const runRef = useRef<(input: T) => Promise<T>>(() => Promise.resolve(defaultValue));
65
-
66
- const onMiddlewareChange = useCallback(() => {
67
- runRef.current(defaultValue).then(setResult);
68
- }, [defaultValue]);
69
-
70
- const { run } = useMiddlewareRunner<T>({
71
- hookPoint,
72
- onMiddlewareChange,
73
- });
74
-
75
- runRef.current = run;
76
-
77
- useEffect(() => {
78
- run(defaultValue).then(setResult);
79
- }, [defaultValue, run]);
80
-
81
- return result;
82
- };
1
+ import {
2
+ createMiddlewareRunnerHook,
3
+ createMiddlewareTransformedDataHook,
4
+ } from '#/core/shared-service/factory/createUIServiceReactKit';
5
+ import { SystemUIService } from '#/core/plugin-system/services/systemUIService';
6
+
7
+ export const useMiddlewareRunner = createMiddlewareRunnerHook(SystemUIService.getInstance());
8
+ export const useMiddlewareTransformedData =
9
+ createMiddlewareTransformedDataHook(useMiddlewareRunner);
10
+ export type {
11
+ UseMiddlewareRunnerOptions,
12
+ UseMiddlewareRunnerResult,
13
+ } from '#/core/shared-service/factory/createUIServiceReactKit';
@@ -1,27 +1,4 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import { createSlotHook } from '#/core/shared-service/factory/createUIServiceReactKit';
2
+ import { SystemUIService } from '#/core/plugin-system/services/systemUIService';
2
3
 
3
- import { IComPropsRegisteredToSlot } from '#/types/slots';
4
-
5
- import { SystemUIService } from '../plugin-system/services/systemUIService';
6
- import { SystemSlotName } from '../shared-service/contract/IUIService';
7
-
8
- export const useSlotComp = (slotName: SystemSlotName) => {
9
- const [components, setComponents] = useState<React.ComponentType<IComPropsRegisteredToSlot>[]>(
10
- () => SystemUIService.getInstance().getComponents(slotName)
11
- );
12
-
13
- useEffect(() => {
14
- setComponents(SystemUIService.getInstance().getComponents(slotName));
15
-
16
- const unsubscribe = SystemUIService.getInstance().subscribeSlotChange(() => {
17
- const comps = SystemUIService.getInstance().getComponents(slotName);
18
- setComponents(comps);
19
- });
20
-
21
- return () => {
22
- unsubscribe();
23
- };
24
- }, [slotName]);
25
-
26
- return components;
27
- };
4
+ export const useSlotComp = createSlotHook(SystemUIService.getInstance());
@@ -16,14 +16,15 @@ const usePluginsLoading = () => {
16
16
  }, [isLoading]);
17
17
 
18
18
  useEffect(() => {
19
- let unregister: (() => void) | undefined;
19
+ let unregisters = new Map<string, () => void>();
20
+
20
21
  const unsubscribe = CoreService.getInstance().subscribePluginLoading(payload => {
21
- unregister = setGlobalLoading(payload.id, payload.loading);
22
+ unregisters.set(payload.id, setGlobalLoading(payload.id, payload.loading));
22
23
  });
23
24
 
24
25
  return () => {
25
26
  unsubscribe();
26
- unregister?.();
27
+ unregisters.forEach(unregister => unregister());
27
28
  };
28
29
  }, [setGlobalLoading]);
29
30
  };
@@ -78,8 +78,7 @@ class PluginManager {
78
78
 
79
79
  const perm = p.getMeta().perm || 'guest';
80
80
  if (perm === 'admin' && !AdminPluginIDSet.has(pluginId)) {
81
- console.error(`Plugin ${pluginId} requires admin permission, but it's not trusted.`);
82
- return;
81
+ throw new Error(`Plugin ${pluginId} requires admin permission, but it's not trusted.`);
83
82
  }
84
83
 
85
84
  const ctx = this.createHostContext(perm);
@@ -108,6 +107,10 @@ class PluginManager {
108
107
  await plugin.onDisable();
109
108
  }
110
109
 
110
+ if (plugin.onDestroy) {
111
+ plugin.onDestroy();
112
+ }
113
+
111
114
  this.activePlugins.delete(pluginId);
112
115
 
113
116
  SystemUIService.getInstance().unregisterAllByPluginId(plugin.id);
@@ -2,7 +2,7 @@ import { cfApiFetcher } from '#/app/utils';
2
2
  import { FALLBACK_MANIFEST } from '#/core/plugin-system/manifest';
3
3
  import { IPluginManifestEntry, PluginID } from '#/types/plugin';
4
4
 
5
- const KnownPluginIDSet = new Set<string>(FALLBACK_MANIFEST.map(entry => entry.id));
5
+ // const KnownPluginIDSet = new Set<string>(FALLBACK_MANIFEST.map(entry => entry.id));
6
6
 
7
7
  interface PluginsApiResponse {
8
8
  status: string;
@@ -48,22 +48,6 @@ class PluginManifestService {
48
48
  if (res.status === 'success' && Array.isArray(res.data)) {
49
49
  const validated = res.data
50
50
  .map((item): IPluginManifestEntry | null => {
51
- if (!KnownPluginIDSet.has(item.id)) {
52
- console.warn(`[PluginManifestService] Unknown plugin id from server: ${item.id}`);
53
- return null;
54
- }
55
-
56
- let dependencies: PluginID[] | undefined;
57
- if (item.dependencies) {
58
- try {
59
- dependencies = item.dependencies.filter(p => KnownPluginIDSet.has(p));
60
- } catch {
61
- console.warn(
62
- `[PluginManifestService] Failed to parse dependencies for plugin: ${item.id}`
63
- );
64
- }
65
- }
66
-
67
51
  return {
68
52
  id: item.id as PluginID,
69
53
  name: item.name,
@@ -71,7 +55,7 @@ class PluginManifestService {
71
55
  description: item.description,
72
56
  perm: (item.perm as 'guest' | 'admin' | undefined) || 'guest',
73
57
  icon: item.icon,
74
- dependencies,
58
+ dependencies: item.dependencies || [],
75
59
  };
76
60
  })
77
61
  .filter((item): item is IPluginManifestEntry => item !== null);
@@ -12,6 +12,7 @@ export class PluginStore {
12
12
  this.parse();
13
13
  }
14
14
 
15
+ // TODO: check circular dependency when install plugin, and also check if all dependencies are installed before install a plugin
15
16
  private checkDep = (id: PluginID): boolean => {
16
17
  const manifest = PluginManifestService.getInstance().getPlugin(id);
17
18
  if (!manifest) {
@@ -1,8 +1,6 @@
1
1
  import React from 'react';
2
2
 
3
- import type { SlotName, HookPoint, IComPropsRegisteredToSlot } from '#/types/slots';
4
-
5
- import { createEventBus } from '../utils/eventBus';
3
+ import type { IComPropsRegisteredToSlot } from '#/types/slots';
6
4
 
7
5
  export interface ISlotEntry {
8
6
  id: string;
@@ -17,106 +15,3 @@ export interface IMiddlewareEntry<T> {
17
15
  pluginId: string;
18
16
  weight?: number;
19
17
  }
20
-
21
- type RegistryEvents = {
22
- 'components:changed': void;
23
- 'middleware:changed': HookPoint;
24
- };
25
-
26
- export class Registry {
27
- private slots = new Map<SlotName, ISlotEntry[]>();
28
-
29
- private bus = createEventBus<RegistryEvents>();
30
-
31
- private middlewares = new Map<HookPoint, IMiddlewareEntry<unknown>[]>();
32
-
33
- subscribeMiddleware(hookPoint: HookPoint, listener: () => void) {
34
- return this.bus.on('middleware:changed', point => {
35
- if (point === hookPoint) {
36
- listener();
37
- }
38
- });
39
- }
40
-
41
- subscribe(listener: () => void) {
42
- return this.bus.on('components:changed', listener);
43
- }
44
-
45
- registerComponent(
46
- slot: SlotName,
47
- component: React.ComponentType<IComPropsRegisteredToSlot>,
48
- pluginId: string,
49
- weight = 0
50
- ) {
51
- const existing = this.slots.get(slot) || [];
52
-
53
- const newEntry: ISlotEntry = {
54
- id: `${pluginId}-${component.name || 'comp'}`,
55
- pluginId,
56
- component,
57
- weight,
58
- };
59
-
60
- // 插入并按权重排序(权重大的在前)
61
- const newList = [...existing, newEntry].sort((a, b) => b.weight - a.weight);
62
- this.slots.set(slot, newList);
63
-
64
- this.bus.emit('components:changed', undefined);
65
- }
66
-
67
- unregisterAllByPluginId(pluginId: string) {
68
- // 1. 清理 UI 槽位
69
- this.slots.forEach((entries, slotName) => {
70
- const filtered = entries.filter(entry => entry.pluginId !== pluginId);
71
- this.slots.set(slotName, filtered);
72
- });
73
-
74
- // 2. 清理中间件
75
- this.middlewares.forEach((entries, point) => {
76
- const filtered = entries.filter(entry => entry.pluginId !== pluginId);
77
- this.middlewares.set(point, filtered);
78
- });
79
-
80
- console.log(`[Registry] All resources for plugin "${pluginId}" have been cleared.`);
81
-
82
- this.bus.emit('components:changed', undefined);
83
- }
84
-
85
- registerMiddleware<T>(
86
- hookPoint: HookPoint,
87
- fn: (data: T) => Promise<T> | T,
88
- pluginId: string,
89
- weight = 0
90
- ) {
91
- const existing = this.middlewares.get(hookPoint) || [];
92
-
93
- const newEntry: IMiddlewareEntry<T> = {
94
- id: `${pluginId}-${fn.name || 'middleware'}`,
95
- pluginId,
96
- fn,
97
- weight,
98
- };
99
-
100
- // 插入并按权重排序(权重大的在前)
101
- const newList = [...existing, newEntry as IMiddlewareEntry<unknown>].sort(
102
- (a, b) => (b.weight || 0) - (a.weight || 0)
103
- );
104
-
105
- this.middlewares.set(hookPoint, newList);
106
-
107
- this.bus.emit('middleware:changed', hookPoint);
108
- }
109
-
110
- getComponents(slotName: SlotName): React.ComponentType<IComPropsRegisteredToSlot>[] {
111
- return (this.slots.get(slotName) || []).map(entry => entry.component);
112
- }
113
-
114
- async runMiddleware<T>(point: HookPoint, data: T): Promise<T> {
115
- const fns = (this.middlewares.get(point) || []).map(entry => entry.fn);
116
- let result = data;
117
- for (const fn of fns) {
118
- result = (await fn(result)) as T;
119
- }
120
- return result;
121
- }
122
- }
@@ -1,66 +1,22 @@
1
- import React from 'react';
2
1
  import { PathRouteProps } from 'react-router-dom';
3
2
  import { LinkProps } from '@bbki.ng/ui';
4
3
 
4
+ import { buildEntrySlotCom } from '#/utils';
5
+ import { createUIService } from '#/core/shared-service/factory/createUIService';
5
6
  import {
6
7
  ISystemUIService,
7
8
  SystemDataHookPoint,
8
9
  SystemSlotName,
9
10
  } from '#/core/shared-service/contract/IUIService';
10
11
  import { IPluginEntry, PluginID } from '#/types/plugin';
11
- import { IComPropsRegisteredToSlot } from '#/types/slots';
12
- import { createEventBus } from '#/core/utils/eventBus';
13
- import { buildEntrySlotCom } from '#/utils';
14
12
 
15
- import { IMiddlewareEntry, ISlotEntry } from '../registry';
16
-
17
- type RegistryEvents = {
18
- 'system:slots:changed': void;
19
- 'system:middleware:changed': SystemDataHookPoint;
20
- };
13
+ const baseService = createUIService<SystemSlotName, SystemDataHookPoint>('system');
21
14
 
22
15
  export class SystemUIService implements ISystemUIService {
23
16
  private static instance: SystemUIService;
24
17
 
25
- private slots = new Map<SystemSlotName, Array<ISlotEntry>>();
26
- private middlewares = new Map<SystemDataHookPoint, IMiddlewareEntry<unknown>[]>();
27
-
28
- private bus = createEventBus<RegistryEvents>();
29
-
30
18
  private constructor() {}
31
19
 
32
- subscribeSlotChange(listener: () => void) {
33
- return this.bus.on('system:slots:changed', listener);
34
- }
35
-
36
- registerMiddleware: <T>(
37
- hookPoint: SystemDataHookPoint,
38
- fn: (data: T) => T | Promise<T>,
39
- pluginId: PluginID,
40
- weight?: number
41
- ) => void = <T>(
42
- hookPoint: SystemDataHookPoint,
43
- fn: (data: T) => T | Promise<T>,
44
- pluginId: PluginID,
45
- weight = 0
46
- ) => {
47
- const existing = this.middlewares.get(hookPoint) || [];
48
-
49
- const newEntry: IMiddlewareEntry<T> = {
50
- id: `${pluginId}-${hookPoint}-middleware`,
51
- pluginId,
52
- fn,
53
- weight,
54
- };
55
-
56
- const updated = [...existing, newEntry as IMiddlewareEntry<unknown>].sort(
57
- (a, b) => (b.weight || 0) - (a.weight || 0)
58
- );
59
- this.middlewares.set(hookPoint, updated);
60
-
61
- this.bus.emit('system:middleware:changed', hookPoint);
62
- };
63
-
64
20
  static getInstance(): SystemUIService {
65
21
  if (!SystemUIService.instance) {
66
22
  SystemUIService.instance = new SystemUIService();
@@ -68,6 +24,15 @@ export class SystemUIService implements ISystemUIService {
68
24
  return SystemUIService.instance;
69
25
  }
70
26
 
27
+ registerSlot = baseService.registerSlot.bind(baseService);
28
+ registerMiddleware = baseService.registerMiddleware.bind(baseService);
29
+ unregisterAllByPluginId = baseService.unregisterAllByPluginId.bind(baseService);
30
+ getSlotEntries = baseService.getSlotEntries.bind(baseService);
31
+ getComponents = baseService.getComponents.bind(baseService);
32
+ subscribeSlotChange = baseService.subscribeSlotChange.bind(baseService);
33
+ subscribeMiddlewareChange = baseService.subscribeMiddlewareChange.bind(baseService);
34
+ runMiddlewares = baseService.runMiddlewares.bind(baseService);
35
+
71
36
  registerPluginEntry: (entry: IPluginEntry, id: PluginID) => void = (entry, id) => {
72
37
  this.registerMiddleware(
73
38
  'extendedRoutes',
@@ -102,58 +67,4 @@ export class SystemUIService implements ISystemUIService {
102
67
  10
103
68
  );
104
69
  };
105
-
106
- registerSlot: (
107
- slotName: SystemSlotName,
108
- component: React.ComponentType<IComPropsRegisteredToSlot>,
109
- pluginId: PluginID,
110
- weight?: number
111
- ) => void = (slotName, component, pluginId, weight = 0) => {
112
- const existing = this.slots.get(slotName) || [];
113
-
114
- const newEntry: ISlotEntry = {
115
- id: `${pluginId}-${component.name || 'comp'}`,
116
- pluginId,
117
- component,
118
- weight,
119
- };
120
-
121
- // 插入并按权重排序(权重大的在前)
122
- const updated = [...existing, newEntry].sort((a, b) => b.weight - a.weight);
123
- this.slots.set(slotName, updated);
124
-
125
- this.bus.emit('system:slots:changed', undefined);
126
- };
127
-
128
- unregisterAllByPluginId(pluginId: string) {
129
- this.slots.forEach((entries, slotName) => {
130
- const filtered = entries.filter(entry => entry.pluginId !== pluginId);
131
- this.slots.set(slotName, filtered);
132
- });
133
-
134
- this.middlewares.forEach((entries, point) => {
135
- const filtered = entries.filter(entry => entry.pluginId !== pluginId);
136
- this.middlewares.set(point, filtered);
137
- this.bus.emit('system:middleware:changed', point);
138
- });
139
-
140
- this.bus.emit('system:slots:changed', undefined);
141
- }
142
-
143
- getComponents(slotName: SystemSlotName): React.ComponentType<IComPropsRegisteredToSlot>[] {
144
- return (this.slots.get(slotName) || []).map(entry => entry.component);
145
- }
146
-
147
- subscribeMiddlewareChange(listener: (point: SystemDataHookPoint) => void) {
148
- return this.bus.on('system:middleware:changed', listener);
149
- }
150
-
151
- async runMiddlewares<T>(point: SystemDataHookPoint, data: T): Promise<T> {
152
- const fns = (this.middlewares.get(point) || []).map(entry => entry.fn);
153
- let result = data;
154
- for (const fn of fns) {
155
- result = (await fn(result)) as T;
156
- }
157
- return result;
158
- }
159
70
  }
@@ -1,5 +1,6 @@
1
1
  import type React from 'react';
2
2
 
3
+ import type { ISlotEntry } from '#/core/plugin-system/registry';
3
4
  import { IPluginEntry, PluginID } from '#/types/plugin';
4
5
  import { IComPropsRegisteredToSlot } from '#/types/slots';
5
6
 
@@ -23,6 +24,18 @@ export interface IBaseUIService<T extends string, K extends string> {
23
24
  pluginId: PluginID,
24
25
  weight?: number
25
26
  ) => void;
27
+
28
+ unregisterAllByPluginId(pluginId: string): void;
29
+
30
+ getSlotEntries(slotName: T): ISlotEntry[];
31
+
32
+ getComponents(slotName: T): React.ComponentType<IComPropsRegisteredToSlot>[];
33
+
34
+ subscribeSlotChange(listener: () => void): () => void;
35
+
36
+ subscribeMiddlewareChange(listener: (point: K) => void): () => void;
37
+
38
+ runMiddlewares: <S>(point: K, data: S) => Promise<S>;
26
39
  }
27
40
 
28
41
  export type SystemSlotName = 'leftCol' | 'rightCol' | 'logo' | 'route' | 'pageFooter';
@@ -33,17 +46,5 @@ export type SystemDataHookPoint =
33
46
  | 'transformCoverEntry';
34
47
 
35
48
  export interface ISystemUIService extends IBaseUIService<SystemSlotName, SystemDataHookPoint> {
36
- registerMiddleware: <S>(
37
- hookPoint: SystemDataHookPoint,
38
- fn: (data: S) => S | Promise<S>,
39
- pluginId: PluginID,
40
- weight?: number
41
- ) => void;
42
- registerSlot: (
43
- slotName: SystemSlotName,
44
- component: React.ComponentType<IComPropsRegisteredToSlot>,
45
- pluginId: PluginID,
46
- weight?: number
47
- ) => void;
48
49
  registerPluginEntry: (entry: IPluginEntry, id: PluginID) => void;
49
50
  }
@@ -0,0 +1,121 @@
1
+ import type React from 'react';
2
+
3
+ import type { IMiddlewareEntry, ISlotEntry } from '#/core/plugin-system/registry';
4
+ import { createEventBus } from '#/core/utils/eventBus';
5
+ import type { PluginID } from '#/types/plugin';
6
+ import type { IComPropsRegisteredToSlot } from '#/types/slots';
7
+
8
+ export interface IUIService<TSlot extends string, THook extends string> {
9
+ registerSlot(
10
+ slotName: TSlot,
11
+ component: React.ComponentType<IComPropsRegisteredToSlot>,
12
+ pluginId: PluginID,
13
+ weight?: number
14
+ ): void;
15
+
16
+ registerMiddleware<T>(
17
+ hookPoint: THook,
18
+ fn: (data: T) => T | Promise<T>,
19
+ pluginId: PluginID,
20
+ weight?: number
21
+ ): void;
22
+
23
+ unregisterAllByPluginId(pluginId: string): void;
24
+
25
+ getSlotEntries(slotName: TSlot): ISlotEntry[];
26
+
27
+ getComponents(slotName: TSlot): React.ComponentType<IComPropsRegisteredToSlot>[];
28
+
29
+ subscribeSlotChange(listener: () => void): () => void;
30
+
31
+ subscribeMiddlewareChange(listener: (point: THook) => void): () => void;
32
+
33
+ runMiddlewares<T>(point: THook, data: T): Promise<T>;
34
+ }
35
+
36
+ export function createUIService<TSlot extends string, THook extends string>(
37
+ _: string
38
+ ): IUIService<TSlot, THook> {
39
+ const slots = new Map<TSlot, Array<ISlotEntry>>();
40
+ const middlewares = new Map<THook, Array<IMiddlewareEntry<unknown>>>();
41
+ const bus = createEventBus<{
42
+ slotsChanged: void;
43
+ middlewareChanged: THook;
44
+ }>();
45
+
46
+ return {
47
+ registerSlot(slotName, component, pluginId, weight = 0) {
48
+ const existing = slots.get(slotName) || [];
49
+ const newEntry: ISlotEntry = {
50
+ id: `${pluginId}-${slotName}-${component.name || 'comp'}`,
51
+ pluginId,
52
+ component,
53
+ weight,
54
+ };
55
+ const updated = [...existing, newEntry].sort((a, b) => (b.weight || 0) - (a.weight || 0));
56
+ slots.set(slotName, updated);
57
+ bus.emit('slotsChanged', undefined);
58
+ },
59
+
60
+ registerMiddleware(hookPoint, fn, pluginId, weight = 0) {
61
+ const existing = middlewares.get(hookPoint) || [];
62
+ const newEntry: IMiddlewareEntry<unknown> = {
63
+ id: `${pluginId}-${hookPoint}-middleware`,
64
+ pluginId,
65
+ fn: fn as (data: unknown) => unknown | Promise<unknown>,
66
+ weight,
67
+ };
68
+ const updated = [...existing, newEntry].sort((a, b) => (b.weight || 0) - (a.weight || 0));
69
+ middlewares.set(hookPoint, updated);
70
+ bus.emit('middlewareChanged', hookPoint);
71
+ },
72
+
73
+ unregisterAllByPluginId(pluginId) {
74
+ let slotsChanged = false;
75
+ slots.forEach((entries, slotName) => {
76
+ const filtered = entries.filter(entry => entry.pluginId !== pluginId);
77
+ if (filtered.length !== entries.length) {
78
+ slots.set(slotName, filtered);
79
+ slotsChanged = true;
80
+ }
81
+ });
82
+
83
+ middlewares.forEach((entries, point) => {
84
+ const filtered = entries.filter(entry => entry.pluginId !== pluginId);
85
+ if (filtered.length !== entries.length) {
86
+ middlewares.set(point, filtered);
87
+ bus.emit('middlewareChanged', point);
88
+ }
89
+ });
90
+
91
+ if (slotsChanged) {
92
+ bus.emit('slotsChanged', undefined);
93
+ }
94
+ },
95
+
96
+ getSlotEntries(slotName) {
97
+ return slots.get(slotName) || [];
98
+ },
99
+
100
+ getComponents(slotName) {
101
+ return (slots.get(slotName) || []).map(entry => entry.component);
102
+ },
103
+
104
+ subscribeSlotChange(listener) {
105
+ return bus.on('slotsChanged', listener as (data: unknown) => void);
106
+ },
107
+
108
+ subscribeMiddlewareChange(listener) {
109
+ return bus.on('middlewareChanged', listener as (data: unknown) => void);
110
+ },
111
+
112
+ async runMiddlewares(point, data) {
113
+ const fns = (middlewares.get(point) || []).map(entry => entry.fn);
114
+ let result: unknown = data;
115
+ for (const fn of fns) {
116
+ result = await fn(result);
117
+ }
118
+ return result as typeof data;
119
+ },
120
+ };
121
+ }
@@ -0,0 +1,149 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ import type { IComPropsRegisteredToSlot } from '#/types/slots';
4
+
5
+ import type { IUIService } from './createUIService';
6
+
7
+ export interface UseMiddlewareRunnerOptions<THook extends string> {
8
+ hookPoint: THook;
9
+ onMiddlewareChange?: () => void;
10
+ }
11
+
12
+ export interface UseMiddlewareRunnerResult<T> {
13
+ loading: boolean;
14
+ error: Error | null;
15
+ run: (inputData: T) => Promise<T>;
16
+ }
17
+
18
+ export function createSlotHook<TSlot extends string>(service: IUIService<TSlot, string>) {
19
+ return function useSlotComp(slotName: TSlot) {
20
+ const [components, setComponents] = useState<React.ComponentType<IComPropsRegisteredToSlot>[]>(
21
+ () => service.getComponents(slotName)
22
+ );
23
+
24
+ useEffect(() => {
25
+ setComponents(service.getComponents(slotName));
26
+
27
+ const unsubscribe = service.subscribeSlotChange(() => {
28
+ setComponents(service.getComponents(slotName));
29
+ });
30
+
31
+ return () => {
32
+ unsubscribe();
33
+ };
34
+ }, [slotName]);
35
+
36
+ return components;
37
+ };
38
+ }
39
+
40
+ export function createSlotComponent<TSlot extends string>(service: IUIService<TSlot, string>) {
41
+ return function Slot({
42
+ name,
43
+ data,
44
+ placeholder,
45
+ }: {
46
+ name: TSlot;
47
+ data?: unknown;
48
+ placeholder?: React.ReactNode;
49
+ }) {
50
+ const [entries, setEntries] = useState(() => service.getSlotEntries(name));
51
+
52
+ useEffect(() => {
53
+ setEntries(service.getSlotEntries(name));
54
+
55
+ const unsubscribe = service.subscribeSlotChange(() => {
56
+ setEntries(service.getSlotEntries(name));
57
+ });
58
+
59
+ return () => {
60
+ unsubscribe();
61
+ };
62
+ }, [name]);
63
+
64
+ if (entries.length === 0) {
65
+ return <>{placeholder}</>;
66
+ }
67
+
68
+ return (
69
+ <>
70
+ {entries.map(({ id, component: Component }) => (
71
+ <Component key={id} data={data} />
72
+ ))}
73
+ </>
74
+ );
75
+ };
76
+ }
77
+
78
+ export function createMiddlewareRunnerHook<THook extends string>(
79
+ service: IUIService<string, THook>
80
+ ) {
81
+ return function useMiddlewareRunner<T>({
82
+ hookPoint,
83
+ onMiddlewareChange,
84
+ }: UseMiddlewareRunnerOptions<THook>): UseMiddlewareRunnerResult<T> {
85
+ const [loading, setLoading] = useState(false);
86
+ const [error, setError] = useState<Error | null>(null);
87
+
88
+ const run = useCallback(
89
+ async (inputData: T) => {
90
+ setLoading(true);
91
+ setError(null);
92
+ try {
93
+ const result = await service.runMiddlewares(hookPoint, inputData);
94
+ return result;
95
+ } catch (err) {
96
+ const error = err instanceof Error ? err : new Error(String(err));
97
+ setError(error);
98
+ throw error;
99
+ } finally {
100
+ setLoading(false);
101
+ }
102
+ },
103
+ [hookPoint]
104
+ );
105
+
106
+ useEffect(() => {
107
+ const unsubscribe = service.subscribeMiddlewareChange(hook => {
108
+ if (hook === hookPoint) {
109
+ onMiddlewareChange?.();
110
+ }
111
+ });
112
+
113
+ return () => {
114
+ unsubscribe();
115
+ };
116
+ }, [hookPoint, onMiddlewareChange]);
117
+
118
+ return { loading, error, run };
119
+ };
120
+ }
121
+
122
+ export function createMiddlewareTransformedDataHook<THook extends string>(
123
+ useMiddlewareRunner: <T>(opts: UseMiddlewareRunnerOptions<THook>) => UseMiddlewareRunnerResult<T>
124
+ ) {
125
+ return function useMiddlewareTransformedData<T>(hookPoint: THook, defaultValue: T) {
126
+ const [result, setResult] = useState<T>(defaultValue);
127
+
128
+ const runRef = useRef<(input: T) => Promise<T>>(() => Promise.resolve(defaultValue));
129
+
130
+ const onMiddlewareChange = useCallback(() => {
131
+ runRef.current(defaultValue).then(setResult);
132
+ }, [defaultValue]);
133
+
134
+ const { run } = useMiddlewareRunner<T>({
135
+ hookPoint,
136
+ onMiddlewareChange,
137
+ });
138
+
139
+ useEffect(() => {
140
+ runRef.current = run;
141
+ }, [run]);
142
+
143
+ useEffect(() => {
144
+ run(defaultValue).then(setResult);
145
+ }, [defaultValue, run]);
146
+
147
+ return result;
148
+ };
149
+ }
@@ -1,27 +1,14 @@
1
1
  import React from 'react';
2
2
 
3
- import { useBlogSlotComp } from '../hooks/use_blog_slot_com';
3
+ import { createSlotComponent } from '#/core/shared-service/factory/createUIServiceReactKit';
4
+
5
+ import { BlogUIService } from '../services/BlogUIService';
4
6
  import { BlogSlotName } from '../services/IBlogUIService';
5
7
 
6
8
  export interface ISlotProps {
7
9
  name: BlogSlotName;
8
10
  data?: unknown;
9
-
10
11
  placeholder?: React.ReactNode;
11
12
  }
12
13
 
13
- export const Slot: React.FC<ISlotProps> = ({ name, data, placeholder }) => {
14
- const components = useBlogSlotComp(name);
15
-
16
- if (components.length === 0) {
17
- return <>{placeholder}</>;
18
- }
19
-
20
- return (
21
- <>
22
- {components.map((Component, index) => (
23
- <Component key={index} data={data} />
24
- ))}
25
- </>
26
- );
27
- };
14
+ export const Slot = createSlotComponent<BlogSlotName>(BlogUIService.getInstance());
@@ -1,79 +1,14 @@
1
- import { useState, useEffect, useCallback, useRef } from 'react';
1
+ import {
2
+ createMiddlewareRunnerHook,
3
+ createMiddlewareTransformedDataHook,
4
+ } from '#/core/shared-service/factory/createUIServiceReactKit';
2
5
 
3
6
  import { BlogUIService } from '../services/BlogUIService';
4
- import { BlogDataHookPoint } from '../services/IBlogUIService';
5
7
 
6
- export interface UseMiddlewareTransDataOptions {
7
- hookPoint: BlogDataHookPoint;
8
- onMiddlewareChange?: () => void; // 通知外部,由外部决定是否 run
9
- }
10
-
11
- export interface UseMiddlewareTransDataResult<T> {
12
- loading: boolean;
13
- error: Error | null;
14
- run: (inputData: T) => Promise<T>;
15
- }
16
-
17
- export function useMiddlewareRunner<T>({
18
- hookPoint,
19
- onMiddlewareChange,
20
- }: UseMiddlewareTransDataOptions): UseMiddlewareTransDataResult<T> {
21
- const [loading, setLoading] = useState(false);
22
- const [error, setError] = useState<Error | null>(null);
23
-
24
- const run = useCallback(
25
- async (inputData: T) => {
26
- setLoading(true);
27
- setError(null);
28
- try {
29
- const result = await BlogUIService.getInstance().runMiddlewares(hookPoint, inputData);
30
- return result;
31
- } catch (err) {
32
- const error = err instanceof Error ? err : new Error(String(err));
33
- setError(error);
34
- throw error;
35
- } finally {
36
- setLoading(false);
37
- }
38
- },
39
- [hookPoint]
40
- );
41
-
42
- // 仅通知,不执行
43
- useEffect(() => {
44
- const unsubscribe = BlogUIService.getInstance().subscribeMiddlewareChange(hook => {
45
- if (hook === hookPoint) {
46
- onMiddlewareChange?.();
47
- }
48
- });
49
-
50
- return () => {
51
- unsubscribe();
52
- };
53
- }, [hookPoint, onMiddlewareChange]);
54
-
55
- return { loading, error, run };
56
- }
57
-
58
- export const useMiddlewareTransformedData = <T>(hookPoint: BlogDataHookPoint, defaultValue: T) => {
59
- const [result, setResult] = useState<T>(defaultValue);
60
-
61
- const runRef = useRef<(input: T) => Promise<T>>(() => Promise.resolve(defaultValue));
62
-
63
- const onMiddlewareChange = useCallback(() => {
64
- runRef.current(defaultValue).then(setResult);
65
- }, [defaultValue]);
66
-
67
- const { run } = useMiddlewareRunner<T>({
68
- hookPoint,
69
- onMiddlewareChange,
70
- });
71
-
72
- runRef.current = run;
73
-
74
- useEffect(() => {
75
- run(defaultValue).then(setResult);
76
- }, [defaultValue, run]);
77
-
78
- return result;
79
- };
8
+ export const useMiddlewareRunner = createMiddlewareRunnerHook(BlogUIService.getInstance());
9
+ export const useMiddlewareTransformedData =
10
+ createMiddlewareTransformedDataHook(useMiddlewareRunner);
11
+ export type {
12
+ UseMiddlewareRunnerOptions,
13
+ UseMiddlewareRunnerResult as UseMiddlewareTransformedDataResult,
14
+ } from '#/core/shared-service/factory/createUIServiceReactKit';
@@ -1,27 +1,5 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import { createSlotHook } from '#/core/shared-service/factory/createUIServiceReactKit';
2
2
 
3
- import { IComPropsRegisteredToSlot } from '#/types/slots';
4
-
5
- import { BlogSlotName } from '../services/IBlogUIService';
6
3
  import { BlogUIService } from '../services/BlogUIService';
7
4
 
8
- export const useBlogSlotComp = (slotName: BlogSlotName) => {
9
- const [components, setComponents] = useState<React.ComponentType<IComPropsRegisteredToSlot>[]>(
10
- () => BlogUIService.getInstance().getComponents(slotName)
11
- );
12
-
13
- useEffect(() => {
14
- // setComponents(BlogUIService.getInstance().getComponents(slotName));
15
-
16
- const unsubscribe = BlogUIService.getInstance().subscribeSlotChange(() => {
17
- const comps = BlogUIService.getInstance().getComponents(slotName);
18
- setComponents(comps);
19
- });
20
-
21
- return () => {
22
- unsubscribe();
23
- };
24
- }, [slotName]);
25
-
26
- return components;
27
- };
5
+ export const useBlogSlotComp = createSlotHook(BlogUIService.getInstance());
@@ -15,6 +15,8 @@ export class BlogPlugin extends BBPlugin {
15
15
 
16
16
  private _serviceRegistry?: ServiceRegistry;
17
17
 
18
+ private _unsubscribePluginUninstall?: () => void;
19
+
18
20
  override onInstall = async (ctx: IHostContext) => {
19
21
  ctx.service.register('blog:uiService', BlogUIService.getInstance());
20
22
 
@@ -28,7 +30,9 @@ export class BlogPlugin extends BBPlugin {
28
30
  return;
29
31
  }
30
32
 
31
- coreService.subscribePluginUninstall(this.handlePluginUninstall);
33
+ this._unsubscribePluginUninstall = coreService.subscribePluginUninstall(
34
+ this.handlePluginUninstall
35
+ );
32
36
 
33
37
  systemUIService?.registerPluginEntry(
34
38
  {
@@ -50,6 +54,7 @@ export class BlogPlugin extends BBPlugin {
50
54
  override onDestroy?: (() => void) | undefined = () => {
51
55
  BlogUIService.getInstance().unregisterAllByPluginId(this.id);
52
56
  this._serviceRegistry?.unregister('blog:uiService');
57
+ this._unsubscribePluginUninstall?.();
53
58
  };
54
59
 
55
60
  private handlePluginUninstall = (payload: PluginID) => {
@@ -1,26 +1,14 @@
1
- import type React from 'react';
2
-
3
- import { IMiddlewareEntry, ISlotEntry } from '#/core/plugin-system/registry';
4
- import { createEventBus } from '#/core/utils/eventBus';
5
- import { IComPropsRegisteredToSlot } from '#/types/slots';
1
+ import { createUIService } from '#/core/shared-service/factory/createUIService';
6
2
 
7
3
  import { BlogDataHookPoint, BlogSlotName, IBlogUIService } from './IBlogUIService';
8
4
 
9
- type RegistryEvents = {
10
- 'blog:slots:changed': void;
11
- 'blog:middleware:changed': BlogDataHookPoint;
12
- };
5
+ const baseService = createUIService<BlogSlotName, BlogDataHookPoint>('blog');
13
6
 
14
7
  export class BlogUIService implements IBlogUIService {
15
8
  private static instance: BlogUIService;
16
9
 
17
10
  private constructor() {}
18
11
 
19
- private slots = new Map<BlogSlotName, Array<ISlotEntry>>();
20
- private middlewares = new Map<BlogDataHookPoint, IMiddlewareEntry<unknown>[]>();
21
-
22
- private bus = createEventBus<RegistryEvents>();
23
-
24
12
  static getInstance(): BlogUIService {
25
13
  if (!BlogUIService.instance) {
26
14
  BlogUIService.instance = new BlogUIService();
@@ -28,93 +16,12 @@ export class BlogUIService implements IBlogUIService {
28
16
  return BlogUIService.instance;
29
17
  }
30
18
 
31
- subscribeSlotChange(listener: () => void) {
32
- return this.bus.on('blog:slots:changed', listener);
33
- }
34
-
35
- subscribeMiddlewareChange(listener: (hookPoint: BlogDataHookPoint) => void) {
36
- return this.bus.on('blog:middleware:changed', listener);
37
- }
38
-
39
- registerMiddleware: <S>(
40
- hookPoint: BlogDataHookPoint,
41
- fn: (data: S) => S | Promise<S>,
42
- pluginId: string,
43
- weight?: number
44
- ) => void = <S>(
45
- hookPoint: BlogDataHookPoint,
46
- fn: (data: S) => S | Promise<S>,
47
- pluginId: string,
48
- weight = 0
49
- ) => {
50
- const existing = this.middlewares.get(hookPoint) || [];
51
-
52
- const newEntry: IMiddlewareEntry<S> = {
53
- id: `${pluginId}-${hookPoint}-middleware`,
54
- pluginId,
55
- fn,
56
- weight,
57
- };
58
-
59
- const updated = [...existing, newEntry as IMiddlewareEntry<unknown>].sort(
60
- (a, b) => (b.weight || 0) - (a.weight || 0)
61
- );
62
- this.middlewares.set(hookPoint, updated);
63
-
64
- this.bus.emit('blog:middleware:changed', hookPoint);
65
- };
66
-
67
- registerSlot: (
68
- slotName: BlogSlotName,
69
- component: React.ComponentType<{ data: unknown }>,
70
- pluginId: string,
71
- weight?: number
72
- ) => void = (slotName, component, pluginId, weight = 0) => {
73
- const existing = this.slots.get(slotName) || [];
74
- const newEntry: ISlotEntry = {
75
- id: `${pluginId}-${slotName}-slot`,
76
- pluginId,
77
- component,
78
- weight,
79
- };
80
-
81
- const updated = [...existing, newEntry].sort((a, b) => (b.weight || 0) - (a.weight || 0));
82
- this.slots.set(slotName, updated);
83
-
84
- this.bus.emit('blog:slots:changed', undefined);
85
- };
86
-
87
- unregisterAllByPluginId: (pluginId: string) => void = pluginId => {
88
- // 移除中间件
89
- this.middlewares.forEach((entries, hookPoint) => {
90
- const filtered = entries.filter(entry => entry.pluginId !== pluginId);
91
- if (filtered.length !== entries.length) {
92
- this.middlewares.set(hookPoint, filtered);
93
- this.bus.emit('blog:middleware:changed', hookPoint);
94
- }
95
- });
96
-
97
- // 移除插槽组件
98
- this.slots.forEach((entries, slotName) => {
99
- const filtered = entries.filter(entry => entry.pluginId !== pluginId);
100
- if (filtered.length !== entries.length) {
101
- this.slots.set(slotName, filtered);
102
- }
103
- });
104
-
105
- this.bus.emit('blog:slots:changed', undefined);
106
- };
107
-
108
- getComponents(slotName: BlogSlotName): React.ComponentType<IComPropsRegisteredToSlot>[] {
109
- return (this.slots.get(slotName) || []).map(entry => entry.component);
110
- }
111
-
112
- async runMiddlewares<T>(point: BlogDataHookPoint, data: T): Promise<T> {
113
- const fns = (this.middlewares.get(point) || []).map(entry => entry.fn);
114
- let result = data;
115
- for (const fn of fns) {
116
- result = (await fn(result)) as T;
117
- }
118
- return result;
119
- }
19
+ registerSlot = baseService.registerSlot.bind(baseService);
20
+ registerMiddleware = baseService.registerMiddleware.bind(baseService);
21
+ unregisterAllByPluginId = baseService.unregisterAllByPluginId.bind(baseService);
22
+ getSlotEntries = baseService.getSlotEntries.bind(baseService);
23
+ getComponents = baseService.getComponents.bind(baseService);
24
+ subscribeSlotChange = baseService.subscribeSlotChange.bind(baseService);
25
+ subscribeMiddlewareChange = baseService.subscribeMiddlewareChange.bind(baseService);
26
+ runMiddlewares = baseService.runMiddlewares.bind(baseService);
120
27
  }
@@ -1,5 +1,3 @@
1
- import type React from 'react';
2
-
3
1
  import { type IBaseUIService } from '#/core/shared-service/contract/IUIService';
4
2
 
5
3
  declare module '#/core/shared-service/service-registry' {
@@ -15,17 +13,4 @@ export type BlogDataHookPoint =
15
13
  | 'blog:transformPostContent'
16
14
  | 'blog:transformTitleList';
17
15
 
18
- export interface IBlogUIService extends IBaseUIService<BlogSlotName, BlogDataHookPoint> {
19
- registerMiddleware: <S>(
20
- hookPoint: BlogDataHookPoint,
21
- fn: (data: S) => S | Promise<S>,
22
- pluginId: string,
23
- weight?: number
24
- ) => void;
25
- registerSlot: (
26
- slotName: BlogSlotName,
27
- component: React.ComponentType<{ data: unknown }>,
28
- pluginId: string,
29
- weight?: number
30
- ) => void;
31
- }
16
+ export interface IBlogUIService extends IBaseUIService<BlogSlotName, BlogDataHookPoint> {}
@@ -7,6 +7,10 @@ export const NotifyComp: FC<IComPropsRegisteredToSlot> = _ => {
7
7
  return (
8
8
  <Toaster
9
9
  position="bottom-center"
10
+ style={{
11
+ position: 'fixed',
12
+ zIndex: 998,
13
+ }}
10
14
  toastOptions={{
11
15
  style: {
12
16
  borderRadius: 0,
package/tsconfig.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "extends": "../../tsconfig.base.json",
3
3
  "compilerOptions": {
4
4
  "lib": ["DOM", "DOM.Iterable", "ESNext"],
5
- "jsx": "react",
5
+ "jsx": "react-jsx",
6
6
  "noEmit": true,
7
7
  "moduleResolution": "bundler",
8
8
  "paths": {