feat: 插件机制与第一个内置插件webpanel

pull/13/head
moonrailgun 4 years ago
parent 1e7e8318f1
commit cd125e9c9c

@ -36,6 +36,8 @@ export { useRafState } from './hooks/useRafState';
export { useUpdateRef } from './hooks/useUpdateRef'; export { useUpdateRef } from './hooks/useUpdateRef';
// manager // manager
export { buildRegList } from './manager/buildRegList';
export { buildRegMap } from './manager/buildRegMap';
export { getStorage, setStorage, useStorage } from './manager/storage'; export { getStorage, setStorage, useStorage } from './manager/storage';
export { setTokenGetter } from './manager/request'; export { setTokenGetter } from './manager/request';
export { setServiceUrl } from './manager/service'; export { setServiceUrl } from './manager/service';
@ -64,6 +66,7 @@ export {
getGroupBasicInfo, getGroupBasicInfo,
applyGroupInvite, applyGroupInvite,
modifyGroupField, modifyGroupField,
createGroupPanel,
} from './model/group'; } from './model/group';
export type { GroupPanel, GroupInfo, GroupBasicInfo } from './model/group'; export type { GroupPanel, GroupInfo, GroupBasicInfo } from './model/group';
export type { ChatMessage } from './model/message'; export type { ChatMessage } from './model/message';

@ -0,0 +1,13 @@
/**
*
*
*/
export function buildRegList<T>(): [T[], (item: T) => void] {
const list: T[] = [];
const reg = (item: T) => {
list.push(item);
};
return [list, reg];
}

@ -0,0 +1,20 @@
/**
* Mapping
*
*/
export function buildRegMap<T>(): [
Record<string, T>,
(name: string, item: T) => void
] {
const mapping: Record<string, T> = {};
const reg = (name: string, item: T) => {
if (mapping[name]) {
console.warn('[buildRegMap] 重复注册:', name);
}
mapping[name] = item;
};
return [mapping, reg];
}

@ -16,6 +16,9 @@ export interface GroupPanel {
name: string; name: string;
parentId?: string; parentId?: string;
type: GroupPanelType; type: GroupPanelType;
provider?: string; // 面板提供者
pluginPanelName?: string; // 插件面板名
meta?: Record<string, unknown>;
} }
export interface GroupInfo { export interface GroupInfo {
@ -146,6 +149,7 @@ export async function createGroupPanel(
type: number; type: number;
parentId?: string; parentId?: string;
provider?: string; provider?: string;
pluginPanelName?: string;
meta?: Record<string, unknown>; meta?: Record<string, unknown>;
} }
) { ) {

@ -15,11 +15,11 @@ export function useGroupInfo(groupId: string): GroupInfo | undefined {
export function useGroupPanel( export function useGroupPanel(
groupId: string, groupId: string,
panelId: string panelId: string
): GroupPanel | undefined { ): GroupPanel | null {
const groupInfo = useGroupInfo(groupId); const groupInfo = useGroupInfo(groupId);
return useMemo( return useMemo(
() => groupInfo?.panels.find((p) => p.id === panelId), () => groupInfo?.panels.find((p) => p.id === panelId) ?? null,
[groupInfo, panelId] [groupInfo, panelId]
); );
} }

@ -8,7 +8,9 @@
"private": true, "private": true,
"scripts": { "scripts": {
"build": "cross-env TS_NODE_PROJECT='tsconfig.node.json' webpack", "build": "cross-env TS_NODE_PROJECT='tsconfig.node.json' webpack",
"dev": "cross-env TS_NODE_PROJECT='tsconfig.node.json' NODE_ENV=development webpack serve" "dev": "cross-env TS_NODE_PROJECT='tsconfig.node.json' NODE_ENV=development webpack serve",
"plugins:all": "ministar buildPlugin all",
"plugins:watch": "ministar watchPlugin all"
}, },
"dependencies": { "dependencies": {
"@iconify/iconify": "^2.0.2", "@iconify/iconify": "^2.0.2",

@ -1,6 +1,6 @@
{ {
"name": "@plugins/com.msgbyte.webpanel", "name": "@plugins/com.msgbyte.webpanel",
"main": "src/index.ts", "main": "src/index.tsx",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"dependencies": {} "dependencies": {}

@ -1 +0,0 @@
console.log('Hello World!');

@ -0,0 +1,24 @@
import React from 'react';
import { regGroupPanel, useCurrentGroupPanelInfo } from '@capital/common';
const PLUGIN_NAME = 'com.msgbyte.webpanel';
const GroupWebPanelRender = () => {
const groupPanelInfo = useCurrentGroupPanelInfo();
if (!groupPanelInfo) {
return <div>, </div>;
}
return (
<iframe className="w-full h-full bg-white" src={groupPanelInfo.meta?.url} />
);
};
regGroupPanel({
name: `${PLUGIN_NAME}/grouppanel`,
label: '网页面板',
provider: PLUGIN_NAME,
extraFormMeta: [{ type: 'text', name: 'url', label: '网址' }],
render: GroupWebPanelRender,
});

@ -1,6 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"rootDir": "./src", "rootDir": "./src",
"baseUrl": "./src" "baseUrl": "./src",
"esModuleInterop": true,
"jsx": "react",
"paths": {
"@capital/*": ["../../../src/plugin/*"],
}
} }
} }

@ -0,0 +1,57 @@
import { findPluginPanelInfoByName } from '@/utils/plugin-helper';
import { Alert } from 'antd';
import React, { useMemo } from 'react';
import { isValidStr, useGroupPanel } from 'tailchat-shared';
interface GroupPluginPanelProps {
groupId: string;
panelId: string;
}
/**
*
*/
export const GroupPluginPanel: React.FC<GroupPluginPanelProps> = React.memo(
(props) => {
const panelInfo = useGroupPanel(props.groupId, props.panelId);
if (!panelInfo) {
return (
<Alert className="w-full text-center" message="无法获取面板信息" />
);
}
if (typeof panelInfo.provider !== 'string') {
return (
<Alert className="w-full text-center" message="未找到插件的提供者" />
);
}
// 从已安装插件注册的群组面板中查找对应群组的面板配置
const pluginPanelInfo = useMemo(() => {
if (!isValidStr(panelInfo.pluginPanelName)) {
return null;
}
return findPluginPanelInfoByName(panelInfo.pluginPanelName);
}, [panelInfo.name]);
if (!pluginPanelInfo) {
// TODO: 如果没有安装, 引导用户安装插件
return (
<Alert
className="w-full text-center"
message={`该面板由插件提供, 插件未安装: ${panelInfo.provider}`}
/>
);
}
const Component = pluginPanelInfo.render;
if (!Component) {
return null;
}
return <Component />;
}
);
GroupPluginPanel.displayName = 'GroupPluginPanel';

@ -1,25 +1,28 @@
import React from 'react'; import { PluginGroupPanel, pluginGroupPanel } from '@/plugin/common';
import { findPluginPanelInfoByName } from '@/utils/plugin-helper';
import React, { useMemo, useState } from 'react';
import { import {
FastFormFieldMeta, FastFormFieldMeta,
GroupPanelType, GroupPanelType,
t, t,
useAsyncRequest, useAsyncRequest,
createGroupPanel,
} from 'tailchat-shared'; } from 'tailchat-shared';
import { createGroupPanel } from '../../../../shared/model/group';
import { ModalWrapper } from '../Modal'; import { ModalWrapper } from '../Modal';
import { WebFastForm } from '../WebFastForm'; import { WebFastForm } from '../WebFastForm';
type Values = { interface Values {
name: string; name: string;
type: string; type: string;
}; [key: string]: unknown;
}
const baseFields: FastFormFieldMeta[] = [ const baseFields: FastFormFieldMeta[] = [
{ type: 'text', name: 'name', label: t('面板名') }, { type: 'text', name: 'name', label: t('面板名') },
{ {
type: 'select', type: 'select',
name: 'type', name: 'type',
label: t('类型'), label: t('类型'), // 如果为插件则存储插件面板的名称
options: [ options: [
{ {
label: t('聊天频道'), label: t('聊天频道'),
@ -29,6 +32,10 @@ const baseFields: FastFormFieldMeta[] = [
label: t('面板分组'), label: t('面板分组'),
value: GroupPanelType.GROUP, value: GroupPanelType.GROUP,
}, },
...pluginGroupPanel.map((pluginPanel) => ({
label: pluginPanel.label,
value: pluginPanel.name,
})),
], ],
}, },
]; ];
@ -40,13 +47,24 @@ export const ModalCreateGroupPanel: React.FC<{
groupId: string; groupId: string;
onCreateSuccess: () => void; onCreateSuccess: () => void;
}> = React.memo((props) => { }> = React.memo((props) => {
const [currentValues, setValues] = useState<Partial<Values>>({});
const [, handleSubmit] = useAsyncRequest( const [, handleSubmit] = useAsyncRequest(
async (values: Values) => { async (values: Values) => {
const { name, type } = values; const { name, type, ...meta } = values;
let panelType: number; let panelType: number;
let provider: string | undefined = undefined;
let pluginPanelName: string | undefined = undefined;
if (typeof type === 'string') { if (typeof type === 'string') {
// 创建一个来自插件的面板
const panelName = type;
panelType = GroupPanelType.PLUGIN; panelType = GroupPanelType.PLUGIN;
const pluginPanelInfo = findPluginPanelInfoByName(panelName);
if (pluginPanelInfo) {
provider = pluginPanelInfo.provider;
pluginPanelName = pluginPanelInfo.name;
}
} else { } else {
panelType = type; panelType = type;
} }
@ -54,15 +72,35 @@ export const ModalCreateGroupPanel: React.FC<{
await createGroupPanel(props.groupId, { await createGroupPanel(props.groupId, {
name, name,
type: panelType, type: panelType,
provider,
pluginPanelName,
meta,
}); });
props.onCreateSuccess(); props.onCreateSuccess();
}, },
[props.groupId, props.onCreateSuccess] [props.groupId, props.onCreateSuccess]
); );
const field = useMemo(() => {
if (typeof currentValues.type === 'string') {
// 如果当前选择的面板类型为插件类型
// 需要从插件信息中获取额外的字段
const panelInfo = findPluginPanelInfoByName(currentValues.type);
if (panelInfo) {
return [...baseFields, ...panelInfo.extraFormMeta];
}
}
return baseFields;
}, [currentValues]);
return ( return (
<ModalWrapper title={t('创建群组面板')} style={{ width: 440 }}> <ModalWrapper title={t('创建群组面板')} style={{ width: 440 }}>
<WebFastForm fields={baseFields} onSubmit={handleSubmit} /> <WebFastForm
fields={field}
onChange={setValues}
onSubmit={handleSubmit}
/>
</ModalWrapper> </ModalWrapper>
); );
}); });

@ -3,7 +3,7 @@ import './dev';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { App } from './App'; import { App } from './App';
import { initMiniStar } from 'mini-star'; import { initPlugins } from './plugin/loader';
import 'antd/dist/antd.css'; import 'antd/dist/antd.css';
import './styles/antd/index.less'; import './styles/antd/index.less';
@ -11,13 +11,6 @@ import 'tailwindcss/tailwind.css';
import './styles/global.less'; import './styles/global.less';
// 先加载插件再开启应用 // 先加载插件再开启应用
initMiniStar({ initPlugins().then(() => {
plugins: [
{
name: 'com.msgbyte.webpanel',
url: '/plugins/com.msgbyte.webpanel/index.js',
},
],
}).then(() => {
ReactDOM.render(<App />, document.querySelector('#app')); ReactDOM.render(<App />, document.querySelector('#app'));
}); });

@ -0,0 +1,10 @@
/**
*
*
*/
export * from './reg';
export {
useGroupPanelParams,
useCurrentGroupPanelInfo,
} from '@/routes/Main/Content/Group/utils';

@ -0,0 +1,34 @@
import { buildRegList, FastFormFieldMeta } from 'tailchat-shared';
/**
*
*/
export interface PluginGroupPanel {
/**
*
* @example com.msgbyte.webpanel/grouppanel
*/
name: string;
/**
*
*/
label: string;
/**
* ,
*/
provider: string;
/**
* , 使
*/
extraFormMeta: FastFormFieldMeta[];
/**
*
*/
render: React.ComponentType;
}
export const [pluginGroupPanel, regGroupPanel] =
buildRegList<PluginGroupPanel>();

@ -0,0 +1,26 @@
import { initMiniStar, regDependency, regSharedModule } from 'mini-star';
/**
*
*/
export function initPlugins(): Promise<void> {
registerDependencies();
registerModules();
return initMiniStar({
plugins: [
{
name: 'com.msgbyte.webpanel',
url: '/plugins/com.msgbyte.webpanel/index.js',
},
],
});
}
function registerDependencies() {
regDependency('react', () => import('react'));
}
function registerModules() {
regSharedModule('@capital/common', () => import('./common/index'));
}

@ -1,25 +1,30 @@
import { GroupPluginPanel } from '@/components/Panel/group/PluginPanel';
import { TextPanel } from '@/components/Panel/group/TextPanel'; import { TextPanel } from '@/components/Panel/group/TextPanel';
import { Alert } from 'antd'; import { Alert } from 'antd';
import React from 'react'; import React from 'react';
import { useParams } from 'react-router'; import { GroupPanelType, t, useGroupPanel } from 'tailchat-shared';
import { GroupPanelType, useGroupPanel } from 'tailchat-shared'; import { useGroupPanelParams } from './utils';
export const GroupPanelRender: React.FC = React.memo(() => { export const GroupPanelRender: React.FC = React.memo(() => {
const { groupId, panelId } = useParams<{ const { groupId, panelId } = useGroupPanelParams();
groupId: string;
panelId: string;
}>();
const panelInfo = useGroupPanel(groupId, panelId); const panelInfo = useGroupPanel(groupId, panelId);
if (panelInfo === undefined) { if (panelInfo === null) {
return null; return null;
} }
if (panelInfo.type === GroupPanelType.TEXT) { if (panelInfo.type === GroupPanelType.TEXT) {
return <TextPanel groupId={groupId} panelId={panelInfo.id} />; return <TextPanel groupId={groupId} panelId={panelInfo.id} />;
} else if (panelInfo.type === GroupPanelType.PLUGIN) {
return <GroupPluginPanel groupId={groupId} panelId={panelInfo.id} />;
} }
return <Alert message="未知的面板类型" />; return (
<Alert
className="w-full text-center"
type="error"
message={t('未知的面板类型')}
/>
);
}); });
GroupPanelRender.displayName = 'GroupPanelRender'; GroupPanelRender.displayName = 'GroupPanelRender';

@ -0,0 +1,16 @@
import React, { useContext } from 'react';
interface GroupPanelContextValue {
groupId: string;
panelId: string;
}
export const GroupPanelContext =
React.createContext<GroupPanelContextValue | null>(null);
GroupPanelContext.displayName = 'GroupPanelContext';
/**
*
*/
export function useGroupPanelContext(): GroupPanelContextValue | null {
return useContext(GroupPanelContext);
}

@ -0,0 +1,24 @@
import { useParams } from 'react-router';
import { GroupPanel, useGroupPanel } from 'tailchat-shared';
/**
*
*/
export function useGroupPanelParams(): {
groupId: string;
panelId: string;
} {
const { groupId, panelId } = useParams<{
groupId: string;
panelId: string;
}>();
return { groupId, panelId };
}
export function useCurrentGroupPanelInfo(): GroupPanel | null {
const { groupId, panelId } = useGroupPanelParams();
const panelInfo = useGroupPanel(groupId, panelId);
return panelInfo;
}

@ -0,0 +1,11 @@
import { pluginGroupPanel, PluginGroupPanel } from '@/plugin/common';
/**
*
* @param pluginPanelName
*/
export function findPluginPanelInfoByName(
pluginPanelName: string
): PluginGroupPanel | undefined {
return pluginGroupPanel.find((p) => p.name === pluginPanelName);
}
Loading…
Cancel
Save