feat: 增加开放平台的基础框架

pull/81/head
moonrailgun 3 years ago
parent 6ee8a0a9f2
commit 246733bf75

@ -0,0 +1,22 @@
import { DependencyList, useEffect, useReducer } from 'react';
import type { FunctionReturningPromise } from '../types';
import { useAsyncFn } from './useAsyncFn';
export function useAsyncRefresh<T extends FunctionReturningPromise>(
fn: T,
deps: DependencyList = []
) {
const [state, callback] = useAsyncFn(fn, deps, {
loading: true,
});
const [inc, refresh] = useReducer((i) => i + 1, 0);
useEffect(() => {
callback();
}, [callback, inc]);
return {
...state,
refresh,
};
}

@ -67,6 +67,7 @@ export { useLanguage } from './i18n/language';
export { useUsernames } from './hooks/model/useUsernames';
export { useAsync } from './hooks/useAsync';
export { useAsyncFn } from './hooks/useAsyncFn';
export { useAsyncRefresh } from './hooks/useAsyncRefresh';
export { useAsyncRequest } from './hooks/useAsyncRequest';
export { useDebounce } from './hooks/useDebounce';
export { useMountedState } from './hooks/useMountedState';

@ -0,0 +1,9 @@
{
"label": "开放平台插件",
"name": "com.msgbyte.openapi",
"url": "/plugins/com.msgbyte.openapi/index.js",
"version": "0.0.0",
"author": "msgbyte",
"description": "为应用提供开放平台的操作能力",
"requireRestart": true
}

@ -0,0 +1,9 @@
{
"name": "@plugins/com.msgbyte.openapi",
"main": "src/index.ts",
"version": "0.0.0",
"private": true,
"dependencies": {
"shepherd.js": "^8.3.1"
}
}

@ -0,0 +1,8 @@
import React from 'react';
const Bot: React.FC = React.memo(() => {
return <div></div>;
});
Bot.displayName = 'Bot';
export default Bot;

@ -0,0 +1,8 @@
import React from 'react';
const Profile: React.FC = React.memo(() => {
return <div></div>;
});
Profile.displayName = 'Profile';
export default Profile;

@ -0,0 +1,16 @@
import { useOpenAppInfo } from '../context';
import React from 'react';
const Summary: React.FC = React.memo(() => {
const { appId, appName } = useOpenAppInfo();
return (
<div>
<div>{appId}</div>
<div>{appName}</div>
</div>
);
});
Summary.displayName = 'Summary';
export default Summary;

@ -0,0 +1,8 @@
import React from 'react';
const Webpage: React.FC = React.memo(() => {
return <div></div>;
});
Webpage.displayName = 'Webpage';
export default Webpage;

@ -0,0 +1,3 @@
.plugin-openapi-app-info {
display: flex;
}

@ -0,0 +1,38 @@
import React, { useState } from 'react';
import { Menu } from '@capital/component';
import { Loadable } from '@capital/common';
import { useOpenAppInfo } from '../context';
import './index.less';
const menuRouteMap: Record<string, React.ComponentType> = {
summary: Loadable(() => import('./Summary')),
profile: Loadable(() => import('./Profile')),
bot: Loadable(() => import('./Bot')),
webpage: Loadable(() => import('./Webpage')),
};
const AppInfo: React.FC = React.memo(() => {
const [menu, setMenu] = useState('summary');
return (
<div className="plugin-openapi-app-info">
<Menu
style={{ width: 256 }}
selectedKeys={[menu]}
onSelect={({ key }) => setMenu(key)}
>
<Menu.Item key="summary"></Menu.Item>
<Menu.Item key="profile"></Menu.Item>
<Menu.Item key="bot"></Menu.Item>
<Menu.Item key="webpage"></Menu.Item>
</Menu>
<div>
{menuRouteMap[menu] ? React.createElement(menuRouteMap[menu]) : <div />}
</div>
</div>
);
});
AppInfo.displayName = 'AppInfo';
export default AppInfo;

@ -0,0 +1,17 @@
import React, { useContext } from 'react';
import { OpenApp } from './types';
const OpenAppInfoContext = React.createContext<OpenApp>(null);
OpenAppInfoContext.displayName = 'OpenAppInfoContext';
export const OpenAppInfoProvider: React.FC<{ appInfo: OpenApp }> = (props) => {
return (
<OpenAppInfoContext.Provider value={props.appInfo}>
{props.children}
</OpenAppInfoContext.Provider>
);
};
export function useOpenAppInfo() {
return useContext(OpenAppInfoContext);
}

@ -0,0 +1,3 @@
.plugin-openapi-main-panel {
// keep
}

@ -0,0 +1,84 @@
import React, { useMemo, useState } from 'react';
import {
postRequest,
useAsyncRefresh,
openModal,
closeModal,
} from '@capital/common';
import { LoadingSpinner, Space, Table, Button } from '@capital/component';
import { OpenApp } from './types';
import AppInfo from './AppInfo';
import { OpenAppInfoProvider } from './context';
import { CreateOpenApp } from '../modals/CreateOpenApp';
import './index.less';
const OpenApiMainPanel: React.FC = React.memo(() => {
const [appInfo, setAppInfo] = useState<OpenApp | null>(null);
const {
loading,
value: allApps,
refresh,
} = useAsyncRefresh(async (): Promise<OpenApp[]> => {
const { data } = await postRequest('/openapi/app/all');
return data ?? [];
}, []);
const columns = useMemo(
() => [
{
title: '名称',
dataIndex: 'appName',
},
{
title: '操作',
key: 'action',
render: (_, record: OpenApp) => (
<Space>
<Button onClick={() => setAppInfo(record)}></Button>
</Space>
),
},
],
[]
);
const handleCreateOpenApp = () => {
const key = openModal(
<CreateOpenApp
onSuccess={() => {
refresh();
closeModal(key);
}}
/>
);
};
if (loading) {
return <LoadingSpinner />;
}
return (
<div className="plugin-openapi-main-panel">
{appInfo ? (
<OpenAppInfoProvider appInfo={appInfo}>
<AppInfo />
</OpenAppInfoProvider>
) : (
<>
<Button
style={{ marginBottom: 4 }}
type="primary"
onClick={handleCreateOpenApp}
>
</Button>
<Table columns={columns} dataSource={allApps} pagination={false} />
</>
)}
</div>
);
});
OpenApiMainPanel.displayName = 'OpenApiMainPanel';
export default OpenApiMainPanel;

@ -0,0 +1,5 @@
export interface OpenApp {
appId: string;
appSecret: string;
appName: string;
}

@ -0,0 +1,10 @@
import { Loadable, regCustomPanel } from '@capital/common';
import { Translate } from './translate';
regCustomPanel({
position: 'setting',
icon: '',
name: 'com.msgbyte.openapi/mainPanel',
label: Translate.openapi,
render: Loadable(() => import('./MainPanel')),
});

@ -0,0 +1,55 @@
import {
createFastFormSchema,
fieldSchema,
ModalWrapper,
postRequest,
showToasts,
showErrorToasts,
} from '@capital/common';
import { WebFastForm } from '@capital/component';
import React from 'react';
const schema = createFastFormSchema({
appName: fieldSchema
.string()
.required('应用名不能为空')
.max(20, '应用名过长'),
appDesc: fieldSchema.string(),
});
const fields = [
{ type: 'text', name: 'appName', label: '应用名' },
{
type: 'textarea',
name: 'appDesc',
label: '应用描述',
},
];
interface CreateOpenAppProps {
onSuccess?: () => void;
}
export const CreateOpenApp: React.FC<CreateOpenAppProps> = React.memo(
(props) => {
const handleSubmit = async (values: any) => {
try {
await postRequest('/openapi/app/create', {
...values,
appIcon: '',
});
showToasts('应用创建成功', 'success');
props.onSuccess?.();
} catch (e) {
showErrorToasts(e);
}
};
return (
<ModalWrapper title="创建应用">
<WebFastForm schema={schema} fields={fields} onSubmit={handleSubmit} />
</ModalWrapper>
);
}
);
CreateOpenApp.displayName = 'CreateOpenApp';

@ -0,0 +1,5 @@
import { localTrans } from '@capital/common';
export const Translate = {
openapi: localTrans({ 'zh-CN': '开放平台', 'en-US': 'Open Api' }),
};

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

@ -3,6 +3,7 @@
*
*/
import _pick from 'lodash/pick';
export * from './reg';
export {
useGroupPanelParams,
@ -26,24 +27,63 @@ export {
sharedEvent,
useAsync,
useAsyncFn,
useAsyncRefresh,
uploadFile,
showToasts,
showErrorToasts,
createFastFormSchema,
fieldSchema,
} from 'tailchat-shared';
/**
* axiosrequest config
*
* jwt
*/
function purgeRequestConfig(config?: RequestConfig) {
if (!config) {
return undefined;
}
return _pick(config, [
'transformRequest',
'transformResponse',
'headers',
'params',
'data',
'timeout',
'withCredentials',
'xsrfCookieName',
'xsrfHeaderName',
]);
}
/**
*
*/
export function createPluginRequest(pluginName: string) {
return {
get(actionName: string, config?: RequestConfig) {
return request.get(`/api/plugin:${pluginName}/${actionName}`, config);
return request.get(
`/api/plugin:${pluginName}/${actionName}`,
purgeRequestConfig(config)
);
},
post(actionName: string, data?: any, config?: RequestConfig) {
return request.post(
`/api/plugin:${pluginName}/${actionName}`,
data,
config
purgeRequestConfig(config)
);
},
};
}
/**
*
*
* plugin
*/
export function postRequest(url: string, data?: any, config?: RequestConfig) {
return request.post(`/api${url}`, data, purgeRequestConfig(config));
}

@ -1,7 +1,9 @@
import { Input } from 'antd';
export { Button, Checkbox, Input, Divider, Space } from 'antd';
export { Button, Checkbox, Input, Divider, Space, Menu, Table } from 'antd';
export const TextArea = Input.TextArea;
export { Image } from '@/components/Image';
export { Icon } from '@iconify/react';
export { PillTabs, PillTabPane } from '@/components/PillTabs';
export { LoadingSpinner } from '@/components/LoadingSpinner';
export { WebFastForm } from '@/components/WebFastForm';

@ -58,6 +58,10 @@
.ant-input:not(.ant-input-disabled) {
background-color: transparent;
border-color: var(--antd-primary-dangerous-color);
&:hover {
background-color: transparent;
}
}
}

Loading…
Cancel
Save