mirror of https://github.com/msgbyte/tailchat
feat: 增加开放平台的基础框架
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,
|
||||||
|
};
|
||||||
|
}
|
@ -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/*"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
import { Input } from 'antd';
|
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 const TextArea = Input.TextArea;
|
||||||
export { Image } from '@/components/Image';
|
export { Image } from '@/components/Image';
|
||||||
export { Icon } from '@iconify/react';
|
export { Icon } from '@iconify/react';
|
||||||
export { PillTabs, PillTabPane } from '@/components/PillTabs';
|
export { PillTabs, PillTabPane } from '@/components/PillTabs';
|
||||||
|
export { LoadingSpinner } from '@/components/LoadingSpinner';
|
||||||
|
export { WebFastForm } from '@/components/WebFastForm';
|
||||||
|
Loading…
Reference in New Issue