@bbki.ng/site 5.8.0 → 5.8.2

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.2
4
+
5
+ ### Patch Changes
6
+
7
+ - b4037f7: udpate notification zIndex
8
+
9
+ ## 5.8.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 0790fd3: add notification plugin
14
+
3
15
  ## 5.8.0
4
16
 
5
17
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbki.ng/site",
3
- "version": "5.8.0",
3
+ "version": "5.8.2",
4
4
  "description": "code behind bbki.ng",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -13,6 +13,7 @@
13
13
  "react": "^18.0.0",
14
14
  "react-dom": "^18.0.0",
15
15
  "react-router-dom": "6",
16
+ "sonner": "^2.0.7",
16
17
  "swr": "^2.2.5",
17
18
  "@bbki.ng/ui": "0.2.19"
18
19
  },
@@ -23,8 +24,8 @@
23
24
  "@mdx-js/react": "^3.0.0",
24
25
  "@mdx-js/rollup": "^3.0.0",
25
26
  "@tailwindcss/postcss": "4.1.17",
26
- "@tailwindcss/vite": "4.1.17",
27
27
  "@tailwindcss/typography": "^0.5.0",
28
+ "@tailwindcss/vite": "4.1.17",
28
29
  "@types/node": "^20.0.0",
29
30
  "@types/react": "^18.0.15",
30
31
  "@types/react-dom": "^18.0.6",
@@ -47,4 +47,10 @@ export const FALLBACK_MANIFEST: Array<IPluginManifestEntry> = [
47
47
  version: '0.1.0',
48
48
  description: '一个简单的博客系统,支持 Markdown 格式的文章',
49
49
  },
50
+ {
51
+ name: '通知',
52
+ id: 'notification',
53
+ version: '0.1.0',
54
+ description: '接收通知',
55
+ },
50
56
  ];
@@ -1,7 +1,6 @@
1
1
  import { IHostContext } from '#/types/hostApi';
2
- import { IPlugin, PluginEvents, PluginID, PluginPerm } from '#/types/plugin';
2
+ import { IPlugin, PluginID, PluginPerm } from '#/types/plugin';
3
3
 
4
- import { createEventBus } from '../utils/eventBus';
5
4
  import { AdminPluginIDSet } from '../const';
6
5
  import { ServiceRegistry } from '../shared-service/service-registry';
7
6
 
@@ -14,8 +13,6 @@ const pluginModules = import.meta.glob('../../plugins/*/index.ts');
14
13
  class PluginManager {
15
14
  private activePlugins: Map<string, IPlugin> = new Map();
16
15
 
17
- private bus = createEventBus<PluginEvents>();
18
-
19
16
  private constructor() {
20
17
  ServiceRegistry.getInstance().register('core:baseService', CoreService.getInstance());
21
18
  ServiceRegistry.getInstance().register('core:uiService', SystemUIService.getInstance());
@@ -45,14 +42,6 @@ class PluginManager {
45
42
  return PluginManager.instance;
46
43
  }
47
44
 
48
- notifySiteLoadingChanged(loading: boolean) {
49
- this.bus.emit('site:loading:changed', loading);
50
- }
51
-
52
- subscribePluginLoading(listener: (payload: PluginEvents['plugin:loading:changed']) => void) {
53
- return this.bus.on('plugin:loading:changed', listener);
54
- }
55
-
56
45
  // enable abort ctrl
57
46
  async loadPlugin(pluginId: PluginID) {
58
47
  if (this.activePlugins.has(pluginId)) {
@@ -67,7 +56,7 @@ class PluginManager {
67
56
 
68
57
  // try load plugin
69
58
  this.loading.add(pluginId);
70
- this.bus.emit('plugin:loading:changed', { id: pluginId, loading: true });
59
+ CoreService.getInstance().setLoading(pluginId, true);
71
60
 
72
61
  const modulePath = `../../plugins/${pluginId}/index.ts`;
73
62
  const moduleLoader = pluginModules[modulePath];
@@ -75,7 +64,7 @@ class PluginManager {
75
64
  console.error(`Plugin ${pluginId} not found in registry`);
76
65
 
77
66
  this.loading.delete(pluginId);
78
- this.bus.emit('plugin:loading:changed', { id: pluginId, loading: false });
67
+ CoreService.getInstance().setLoading(pluginId, false);
79
68
 
80
69
  return;
81
70
  }
@@ -104,7 +93,7 @@ class PluginManager {
104
93
  console.error(`Failed to load plugin ${pluginId}:`, error);
105
94
  } finally {
106
95
  this.loading.delete(pluginId);
107
- this.bus.emit('plugin:loading:changed', { id: pluginId, loading: false });
96
+ CoreService.getInstance().setLoading(pluginId, false);
108
97
  }
109
98
  }
110
99
 
@@ -1,18 +1,8 @@
1
1
  import { cfApiFetcher } from '#/app/utils';
2
- import { FALLBACK_MANIFEST } from '#/plugins/manifest';
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>([
6
- 'sticker',
7
- 'xwy',
8
- 'extra-cd',
9
- 'extra-entry',
10
- 'store',
11
- 'now',
12
- 'default',
13
- 'fx',
14
- 'blog',
15
- ]);
5
+ const KnownPluginIDSet = new Set<string>(FALLBACK_MANIFEST.map(entry => entry.id));
16
6
 
17
7
  interface PluginsApiResponse {
18
8
  status: string;
@@ -163,16 +163,22 @@ export class PluginStore {
163
163
  };
164
164
  };
165
165
 
166
- installPlugin: (id: PluginID) => Promise<void> = async id => {
166
+ installPlugin: (id: PluginID) => Promise<boolean> = async id => {
167
167
  if (!this.checkDep(id)) {
168
168
  // throw new Error(`Cannot install plugin ${id} due to missing dependencies`);
169
169
  console.warn(`Cannot install plugin ${id} due to missing dependencies`);
170
- return;
170
+ return false;
171
171
  }
172
172
 
173
- await pluginManager.loadPlugin(id);
174
- this.installedSet.add(id);
175
- this.stringify();
173
+ try {
174
+ await pluginManager.loadPlugin(id);
175
+ this.installedSet.add(id);
176
+ this.stringify();
177
+ return true;
178
+ } catch (error) {
179
+ console.error(`Failed to install plugin ${id}:`, error);
180
+ return false;
181
+ }
176
182
  };
177
183
 
178
184
  disablePlugin: (id: PluginID) => Promise<void> = async id => {
@@ -14,7 +14,7 @@ export function createServiceProxy<T extends object, K extends keyof ServiceIden
14
14
  console.warn(
15
15
  `[System] Service ${serviceId} is currently unavailable. Call to ${String(prop)} ignored.`
16
16
  );
17
- return undefined; // 或者返回一个待定 Promise
17
+ return {}; // 或者返回一个待定 Promise
18
18
  };
19
19
  }
20
20
 
@@ -35,7 +35,9 @@ export class ServiceRegistry {
35
35
  return target as ServiceIdentifierMap[T];
36
36
  }
37
37
 
38
- get<T extends keyof ServiceIdentifierMap>(name: T): ServiceIdentifierMap[T] | undefined {
38
+ get<T extends keyof ServiceIdentifierMap>(
39
+ name: T
40
+ ): ServiceIdentifierMap[T] | Record<string, never> {
39
41
  // const target = this.services.get(name);
40
42
  // if (!target) {
41
43
  // console.warn(`Service with name ${name} is not registered.`);
@@ -0,0 +1,22 @@
1
+ import React, { FC } from 'react';
2
+ import { Toaster } from 'sonner';
3
+
4
+ import { IComPropsRegisteredToSlot } from '#/types/slots';
5
+
6
+ export const NotifyComp: FC<IComPropsRegisteredToSlot> = _ => {
7
+ return (
8
+ <Toaster
9
+ position="bottom-center"
10
+ style={{
11
+ position: 'fixed',
12
+ zIndex: 998,
13
+ }}
14
+ toastOptions={{
15
+ style: {
16
+ borderRadius: 0,
17
+ border: 'none',
18
+ },
19
+ }}
20
+ />
21
+ );
22
+ };
@@ -0,0 +1,26 @@
1
+ import { BBPlugin } from '#/core/plugin-system/bbplugin';
2
+ import { IHostContext } from '#/types/hostApi';
3
+ import { PluginID } from '#/types/plugin';
4
+
5
+ import { NotifyComp } from './components';
6
+ import { NotificationService } from './services/NotificationService';
7
+
8
+ export class NotificationPlugin extends BBPlugin {
9
+ id: PluginID = 'notification' as const;
10
+
11
+ private _serviceRegistry?: IHostContext['service'];
12
+
13
+ override onInstall = async (ctx: IHostContext) => {
14
+ this._serviceRegistry = ctx.service;
15
+ this._serviceRegistry.register('notification', NotificationService.getInstance());
16
+
17
+ const coreUIService = ctx.service.get('core:uiService');
18
+ coreUIService.registerSlot('pageFooter', NotifyComp, this.id);
19
+ };
20
+
21
+ override onDestroy = () => {
22
+ this._serviceRegistry?.unregister('notification');
23
+ };
24
+ }
25
+
26
+ export default new NotificationPlugin();
@@ -0,0 +1,19 @@
1
+ declare module '#/core/shared-service/service-registry' {
2
+ interface ServiceIdentifierMap {
3
+ notification: INotificationService;
4
+ }
5
+ }
6
+
7
+ export type NotificationType = 'toast' | 'banner' | 'modal';
8
+
9
+ export interface INotificationOptions {
10
+ type?: NotificationType;
11
+ level?: 'info' | 'success' | 'warning' | 'error';
12
+ message: string;
13
+ duration?: number; // 持续时间,单位为毫秒,默认为4000ms
14
+ }
15
+
16
+ export interface INotificationService {
17
+ notify: (options: INotificationOptions) => string;
18
+ dismiss: (id: string) => void;
19
+ }
@@ -0,0 +1,28 @@
1
+ import { toast } from 'sonner';
2
+
3
+ import { INotificationOptions, INotificationService } from './INotificationService';
4
+
5
+ export class NotificationService implements INotificationService {
6
+ private static instance: NotificationService;
7
+
8
+ private constructor() {}
9
+
10
+ static getInstance(): NotificationService {
11
+ if (!NotificationService.instance) {
12
+ NotificationService.instance = new NotificationService();
13
+ }
14
+ return NotificationService.instance;
15
+ }
16
+
17
+ notify(options: INotificationOptions): string {
18
+ const id = toast(options.message, {
19
+ duration: options.duration || 3000,
20
+ });
21
+
22
+ return id as string;
23
+ }
24
+
25
+ dismiss(id: string): void {
26
+ toast.dismiss(id);
27
+ }
28
+ }
@@ -6,6 +6,7 @@ import { IComPropsRegisteredToSlot } from '#/types/slots';
6
6
  import { IPluginStoreEntry, PluginID } from '#/types/plugin';
7
7
 
8
8
  import { StoreCtx } from '../context';
9
+ import { pluginDepDescForHuman } from '../utils';
9
10
 
10
11
  // 空状态展示
11
12
  const EmptyState = () => (
@@ -25,7 +26,8 @@ const ErrorState = ({ error, onRetry }: { error: Error; onRetry: () => void }) =
25
26
  );
26
27
 
27
28
  export const StorePage = (_: IComPropsRegisteredToSlot) => {
28
- const { list, setLoading, isInstalled, install, uninstall, get } = StoreCtx.useCtx();
29
+ const { list, setLoading, isInstalled, install, uninstall, get, notificationService } =
30
+ StoreCtx.useCtx();
29
31
  const [plugins, setPlugins] = useState<Array<IPluginStoreEntry>>([]);
30
32
  const [error, setError] = useState<Error | null>(null);
31
33
 
@@ -75,15 +77,25 @@ export const StorePage = (_: IComPropsRegisteredToSlot) => {
75
77
  setError(null);
76
78
 
77
79
  try {
78
- await install(id);
79
- await refreshList();
80
+ const success = await install(id);
81
+ if (success) {
82
+ await refreshList();
83
+ } else {
84
+ notificationService.notify({
85
+ level: 'error',
86
+ message: `安装插件失败,依赖缺失或其他错误`,
87
+ });
88
+ }
80
89
  } catch (err) {
81
- setError(err instanceof Error ? err : new Error('安装失败'));
90
+ notificationService.notify({
91
+ level: 'error',
92
+ message: `安装插件失败: ${err instanceof Error ? err.message : String(err)}`,
93
+ });
82
94
  } finally {
83
95
  setLoading(false);
84
96
  }
85
97
  },
86
- [install, refreshList, setLoading]
98
+ [install, refreshList, setLoading, notificationService]
87
99
  );
88
100
 
89
101
  const handleUninstall = useCallback(
@@ -108,7 +120,6 @@ export const StorePage = (_: IComPropsRegisteredToSlot) => {
108
120
  <>
109
121
  <Table.HCell>功能</Table.HCell>
110
122
  <Table.HCell>描述</Table.HCell>
111
- <Table.HCell>依赖</Table.HCell>
112
123
  <Table.HCell style={{ textAlign: 'right' }}>安装/卸载</Table.HCell>
113
124
  </>
114
125
  );
@@ -126,16 +137,13 @@ export const StorePage = (_: IComPropsRegisteredToSlot) => {
126
137
  return (
127
138
  <>
128
139
  <Table.Cell>{plugin.name}</Table.Cell>
129
- <Table.Cell>{plugin.description}</Table.Cell>
130
140
  <Table.Cell>
131
- {plugin.dependencies && plugin.dependencies.length > 0
132
- ? plugin.dependencies
133
- .map(dep => {
134
- const depPlugin = get(dep);
135
- return depPlugin ? depPlugin.name : dep;
136
- })
137
- .join(', ')
138
- : ''}
141
+ {plugin.description}
142
+ {plugin.dependencies && plugin.dependencies.length > 0 && (
143
+ <div className="mt-4 text-xs text-content-secondary">
144
+ {pluginDepDescForHuman(plugin, get)}
145
+ </div>
146
+ )}
139
147
  </Table.Cell>
140
148
  <Table.Cell style={{ textAlign: 'right' }}>
141
149
  {pluginInstalled ? (
@@ -1,13 +1,15 @@
1
1
  import { createPluginCtx } from '#/core/context';
2
+ import { NotificationService } from '#/plugins/notification/services/NotificationService';
2
3
  import { IPluginStoreEntry, PluginID } from '#/types/plugin';
3
4
 
4
5
  export interface IStoreCtx {
5
- install: (pid: PluginID) => Promise<void>;
6
+ install: (pid: PluginID) => Promise<boolean>;
6
7
  uninstall: (pid: PluginID) => Promise<void>;
7
8
  isInstalled: (pid: PluginID) => boolean;
8
9
  list: () => Promise<Array<IPluginStoreEntry>> | Array<IPluginStoreEntry>;
9
10
  get: (pid: PluginID) => IPluginStoreEntry | undefined;
10
11
  setLoading: (loading: boolean) => void;
12
+ notificationService: NotificationService | Record<string, never>;
11
13
  }
12
14
 
13
15
  export const StoreCtx = createPluginCtx<IStoreCtx>();
@@ -26,7 +26,7 @@ export class StorePlugin extends BBPlugin {
26
26
  label: 'cd ./store',
27
27
  pageComponent: StoreCtx.withCtx(
28
28
  {
29
- install: ctx.store?.installPlugin || (async () => {}),
29
+ install: ctx.store?.installPlugin || (async () => false),
30
30
  uninstall: ctx.store?.disablePlugin || (async () => {}),
31
31
  isInstalled: ctx.store?.isPluginInstalled || ((_: PluginID) => false),
32
32
  get: ctx.store?.getPlugin || ((_: PluginID) => undefined),
@@ -36,6 +36,7 @@ export class StorePlugin extends BBPlugin {
36
36
  coreService.setLoading('store', false);
37
37
  return all.filter(({ id }) => id !== this.id);
38
38
  },
39
+ notificationService: ctx.service.get('notification'),
39
40
  setLoading: (loading: boolean) => {
40
41
  coreService.setLoading('store', loading);
41
42
  },
@@ -0,0 +1,26 @@
1
+ import { IPluginStoreEntry, PluginID } from '#/types/plugin';
2
+
3
+ export const pluginDepDescForHuman = (
4
+ plugin: IPluginStoreEntry,
5
+ infoGetter: (id: PluginID) => IPluginStoreEntry | undefined
6
+ ): string => {
7
+ if (!plugin.dependencies || plugin.dependencies.length === 0) {
8
+ return '';
9
+ }
10
+
11
+ const desc =
12
+ plugin.dependencies && plugin.dependencies.length > 0
13
+ ? plugin.dependencies
14
+ .map(dep => {
15
+ const depPlugin = infoGetter(dep);
16
+ return depPlugin ? depPlugin.name : dep;
17
+ })
18
+ .join(', ')
19
+ : '';
20
+
21
+ if (!desc) {
22
+ return '';
23
+ }
24
+
25
+ return `需要先安装${desc}。`;
26
+ };
@@ -22,6 +22,7 @@ export type PluginID =
22
22
  | 'now'
23
23
  | 'default'
24
24
  | 'blog'
25
+ | 'notification'
25
26
  | 'fx';
26
27
 
27
28
  export interface IPluginEntry {