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';
// manager
export { buildRegList } from './manager/buildRegList';
export { buildRegMap } from './manager/buildRegMap';
export { getStorage, setStorage, useStorage } from './manager/storage';
export { setTokenGetter } from './manager/request';
export { setServiceUrl } from './manager/service';
@ -64,6 +66,7 @@ export {
getGroupBasicInfo,
applyGroupInvite,
modifyGroupField,
createGroupPanel,
} from './model/group';
export type { GroupPanel, GroupInfo, GroupBasicInfo } from './model/group';
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;
parentId?: string;
type: GroupPanelType;
provider?: string; // 面板提供者
pluginPanelName?: string; // 插件面板名
meta?: Record<string, unknown>;
}
export interface GroupInfo {
@ -146,6 +149,7 @@ export async function createGroupPanel(
type: number;
parentId?: string;
provider?: string;
pluginPanelName?: string;
meta?: Record<string, unknown>;
}
) {

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

@ -8,7 +8,9 @@
"private": true,
"scripts": {
"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": {
"@iconify/iconify": "^2.0.2",

@ -1,7 +1,7 @@
{
"name": "@plugins/com.msgbyte.webpanel",
"main": "src/index.ts",
"main": "src/index.tsx",
"version": "0.0.0",
"private": true,
"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": {
"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 {
FastFormFieldMeta,
GroupPanelType,
t,
useAsyncRequest,
createGroupPanel,
} from 'tailchat-shared';
import { createGroupPanel } from '../../../../shared/model/group';
import { ModalWrapper } from '../Modal';
import { WebFastForm } from '../WebFastForm';
type Values = {
interface Values {
name: string;
type: string;
};
[key: string]: unknown;
}
const baseFields: FastFormFieldMeta[] = [
{ type: 'text', name: 'name', label: t('面板名') },
{
type: 'select',
name: 'type',
label: t('类型'),
label: t('类型'), // 如果为插件则存储插件面板的名称
options: [
{
label: t('聊天频道'),
@ -29,6 +32,10 @@ const baseFields: FastFormFieldMeta[] = [
label: t('面板分组'),
value: GroupPanelType.GROUP,
},
...pluginGroupPanel.map((pluginPanel) => ({
label: pluginPanel.label,
value: pluginPanel.name,
})),
],
},
];
@ -40,13 +47,24 @@ export const ModalCreateGroupPanel: React.FC<{
groupId: string;
onCreateSuccess: () => void;
}> = React.memo((props) => {
const [currentValues, setValues] = useState<Partial<Values>>({});
const [, handleSubmit] = useAsyncRequest(
async (values: Values) => {
const { name, type } = values;
const { name, type, ...meta } = values;
let panelType: number;
let provider: string | undefined = undefined;
let pluginPanelName: string | undefined = undefined;
if (typeof type === 'string') {
// 创建一个来自插件的面板
const panelName = type;
panelType = GroupPanelType.PLUGIN;
const pluginPanelInfo = findPluginPanelInfoByName(panelName);
if (pluginPanelInfo) {
provider = pluginPanelInfo.provider;
pluginPanelName = pluginPanelInfo.name;
}
} else {
panelType = type;
}
@ -54,15 +72,35 @@ export const ModalCreateGroupPanel: React.FC<{
await createGroupPanel(props.groupId, {
name,
type: panelType,
provider,
pluginPanelName,
meta,
});
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 (
<ModalWrapper title={t('创建群组面板')} style={{ width: 440 }}>
<WebFastForm fields={baseFields} onSubmit={handleSubmit} />
<WebFastForm
fields={field}
onChange={setValues}
onSubmit={handleSubmit}
/>
</ModalWrapper>
);
});

@ -3,7 +3,7 @@ import './dev';
import React from 'react';
import ReactDOM from 'react-dom';
import { App } from './App';
import { initMiniStar } from 'mini-star';
import { initPlugins } from './plugin/loader';
import 'antd/dist/antd.css';
import './styles/antd/index.less';
@ -11,13 +11,6 @@ import 'tailwindcss/tailwind.css';
import './styles/global.less';
// 先加载插件再开启应用
initMiniStar({
plugins: [
{
name: 'com.msgbyte.webpanel',
url: '/plugins/com.msgbyte.webpanel/index.js',
},
],
}).then(() => {
initPlugins().then(() => {
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 { Alert } from 'antd';
import React from 'react';
import { useParams } from 'react-router';
import { GroupPanelType, useGroupPanel } from 'tailchat-shared';
import { GroupPanelType, t, useGroupPanel } from 'tailchat-shared';
import { useGroupPanelParams } from './utils';
export const GroupPanelRender: React.FC = React.memo(() => {
const { groupId, panelId } = useParams<{
groupId: string;
panelId: string;
}>();
const { groupId, panelId } = useGroupPanelParams();
const panelInfo = useGroupPanel(groupId, panelId);
if (panelInfo === undefined) {
if (panelInfo === null) {
return null;
}
if (panelInfo.type === GroupPanelType.TEXT) {
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';

@ -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