From 246733bf755569e4cb33fe7d6d83c309ca3c5bfc Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Sat, 29 Jan 2022 19:52:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=BC=80=E6=94=BE?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E7=9A=84=E5=9F=BA=E7=A1=80=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/hooks/useAsyncRefresh.ts | 22 +++++ shared/index.tsx | 1 + web/plugins/com.msgbyte.openapi/manifest.json | 9 ++ web/plugins/com.msgbyte.openapi/package.json | 9 ++ .../src/MainPanel/AppInfo/Bot.tsx | 8 ++ .../src/MainPanel/AppInfo/Profile.tsx | 8 ++ .../src/MainPanel/AppInfo/Summary.tsx | 16 ++++ .../src/MainPanel/AppInfo/Webpage.tsx | 8 ++ .../src/MainPanel/AppInfo/index.less | 3 + .../src/MainPanel/AppInfo/index.tsx | 38 +++++++++ .../src/MainPanel/context.tsx | 17 ++++ .../src/MainPanel/index.less | 3 + .../src/MainPanel/index.tsx | 84 +++++++++++++++++++ .../src/MainPanel/types.ts | 5 ++ web/plugins/com.msgbyte.openapi/src/index.ts | 10 +++ .../src/modals/CreateOpenApp.tsx | 55 ++++++++++++ .../com.msgbyte.openapi/src/translate.ts | 5 ++ web/plugins/com.msgbyte.openapi/tsconfig.json | 10 +++ web/src/plugin/common/index.ts | 44 +++++++++- web/src/plugin/component/index.tsx | 4 +- web/src/styles/antd/dark.less | 4 + 21 files changed, 360 insertions(+), 3 deletions(-) create mode 100644 shared/hooks/useAsyncRefresh.ts create mode 100644 web/plugins/com.msgbyte.openapi/manifest.json create mode 100644 web/plugins/com.msgbyte.openapi/package.json create mode 100644 web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/Bot.tsx create mode 100644 web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/Profile.tsx create mode 100644 web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/Summary.tsx create mode 100644 web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/Webpage.tsx create mode 100644 web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/index.less create mode 100644 web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/index.tsx create mode 100644 web/plugins/com.msgbyte.openapi/src/MainPanel/context.tsx create mode 100644 web/plugins/com.msgbyte.openapi/src/MainPanel/index.less create mode 100644 web/plugins/com.msgbyte.openapi/src/MainPanel/index.tsx create mode 100644 web/plugins/com.msgbyte.openapi/src/MainPanel/types.ts create mode 100644 web/plugins/com.msgbyte.openapi/src/index.ts create mode 100644 web/plugins/com.msgbyte.openapi/src/modals/CreateOpenApp.tsx create mode 100644 web/plugins/com.msgbyte.openapi/src/translate.ts create mode 100644 web/plugins/com.msgbyte.openapi/tsconfig.json diff --git a/shared/hooks/useAsyncRefresh.ts b/shared/hooks/useAsyncRefresh.ts new file mode 100644 index 00000000..6546144b --- /dev/null +++ b/shared/hooks/useAsyncRefresh.ts @@ -0,0 +1,22 @@ +import { DependencyList, useEffect, useReducer } from 'react'; +import type { FunctionReturningPromise } from '../types'; +import { useAsyncFn } from './useAsyncFn'; + +export function useAsyncRefresh( + 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, + }; +} diff --git a/shared/index.tsx b/shared/index.tsx index fc621ddc..9da3742f 100644 --- a/shared/index.tsx +++ b/shared/index.tsx @@ -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'; diff --git a/web/plugins/com.msgbyte.openapi/manifest.json b/web/plugins/com.msgbyte.openapi/manifest.json new file mode 100644 index 00000000..d47acd81 --- /dev/null +++ b/web/plugins/com.msgbyte.openapi/manifest.json @@ -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 +} diff --git a/web/plugins/com.msgbyte.openapi/package.json b/web/plugins/com.msgbyte.openapi/package.json new file mode 100644 index 00000000..00edd569 --- /dev/null +++ b/web/plugins/com.msgbyte.openapi/package.json @@ -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" + } +} diff --git a/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/Bot.tsx b/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/Bot.tsx new file mode 100644 index 00000000..b66e18e5 --- /dev/null +++ b/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/Bot.tsx @@ -0,0 +1,8 @@ +import React from 'react'; + +const Bot: React.FC = React.memo(() => { + return
开发中
; +}); +Bot.displayName = 'Bot'; + +export default Bot; diff --git a/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/Profile.tsx b/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/Profile.tsx new file mode 100644 index 00000000..7e51eb6b --- /dev/null +++ b/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/Profile.tsx @@ -0,0 +1,8 @@ +import React from 'react'; + +const Profile: React.FC = React.memo(() => { + return
开发中
; +}); +Profile.displayName = 'Profile'; + +export default Profile; diff --git a/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/Summary.tsx b/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/Summary.tsx new file mode 100644 index 00000000..c9201e86 --- /dev/null +++ b/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/Summary.tsx @@ -0,0 +1,16 @@ +import { useOpenAppInfo } from '../context'; +import React from 'react'; + +const Summary: React.FC = React.memo(() => { + const { appId, appName } = useOpenAppInfo(); + + return ( +
+
{appId}
+
{appName}
+
+ ); +}); +Summary.displayName = 'Summary'; + +export default Summary; diff --git a/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/Webpage.tsx b/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/Webpage.tsx new file mode 100644 index 00000000..7b195913 --- /dev/null +++ b/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/Webpage.tsx @@ -0,0 +1,8 @@ +import React from 'react'; + +const Webpage: React.FC = React.memo(() => { + return
开发中
; +}); +Webpage.displayName = 'Webpage'; + +export default Webpage; diff --git a/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/index.less b/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/index.less new file mode 100644 index 00000000..52ea0d17 --- /dev/null +++ b/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/index.less @@ -0,0 +1,3 @@ +.plugin-openapi-app-info { + display: flex; +} diff --git a/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/index.tsx b/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/index.tsx new file mode 100644 index 00000000..ff75bd7f --- /dev/null +++ b/web/plugins/com.msgbyte.openapi/src/MainPanel/AppInfo/index.tsx @@ -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 = { + 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 ( +
+ setMenu(key)} + > + 总览 + 基础信息 + 机器人 + 网页 + + +
+ {menuRouteMap[menu] ? React.createElement(menuRouteMap[menu]) :
} +
+
+ ); +}); +AppInfo.displayName = 'AppInfo'; + +export default AppInfo; diff --git a/web/plugins/com.msgbyte.openapi/src/MainPanel/context.tsx b/web/plugins/com.msgbyte.openapi/src/MainPanel/context.tsx new file mode 100644 index 00000000..57eb4ef4 --- /dev/null +++ b/web/plugins/com.msgbyte.openapi/src/MainPanel/context.tsx @@ -0,0 +1,17 @@ +import React, { useContext } from 'react'; +import { OpenApp } from './types'; + +const OpenAppInfoContext = React.createContext(null); +OpenAppInfoContext.displayName = 'OpenAppInfoContext'; + +export const OpenAppInfoProvider: React.FC<{ appInfo: OpenApp }> = (props) => { + return ( + + {props.children} + + ); +}; + +export function useOpenAppInfo() { + return useContext(OpenAppInfoContext); +} diff --git a/web/plugins/com.msgbyte.openapi/src/MainPanel/index.less b/web/plugins/com.msgbyte.openapi/src/MainPanel/index.less new file mode 100644 index 00000000..8fcf8907 --- /dev/null +++ b/web/plugins/com.msgbyte.openapi/src/MainPanel/index.less @@ -0,0 +1,3 @@ +.plugin-openapi-main-panel { + // keep +} diff --git a/web/plugins/com.msgbyte.openapi/src/MainPanel/index.tsx b/web/plugins/com.msgbyte.openapi/src/MainPanel/index.tsx new file mode 100644 index 00000000..12de2bb8 --- /dev/null +++ b/web/plugins/com.msgbyte.openapi/src/MainPanel/index.tsx @@ -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(null); + const { + loading, + value: allApps, + refresh, + } = useAsyncRefresh(async (): Promise => { + const { data } = await postRequest('/openapi/app/all'); + + return data ?? []; + }, []); + + const columns = useMemo( + () => [ + { + title: '名称', + dataIndex: 'appName', + }, + { + title: '操作', + key: 'action', + render: (_, record: OpenApp) => ( + + + + ), + }, + ], + [] + ); + + const handleCreateOpenApp = () => { + const key = openModal( + { + refresh(); + closeModal(key); + }} + /> + ); + }; + + if (loading) { + return ; + } + + return ( +
+ {appInfo ? ( + + + + ) : ( + <> + + + + )} + + ); +}); +OpenApiMainPanel.displayName = 'OpenApiMainPanel'; + +export default OpenApiMainPanel; diff --git a/web/plugins/com.msgbyte.openapi/src/MainPanel/types.ts b/web/plugins/com.msgbyte.openapi/src/MainPanel/types.ts new file mode 100644 index 00000000..84191aad --- /dev/null +++ b/web/plugins/com.msgbyte.openapi/src/MainPanel/types.ts @@ -0,0 +1,5 @@ +export interface OpenApp { + appId: string; + appSecret: string; + appName: string; +} diff --git a/web/plugins/com.msgbyte.openapi/src/index.ts b/web/plugins/com.msgbyte.openapi/src/index.ts new file mode 100644 index 00000000..570c3267 --- /dev/null +++ b/web/plugins/com.msgbyte.openapi/src/index.ts @@ -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')), +}); diff --git a/web/plugins/com.msgbyte.openapi/src/modals/CreateOpenApp.tsx b/web/plugins/com.msgbyte.openapi/src/modals/CreateOpenApp.tsx new file mode 100644 index 00000000..c37564af --- /dev/null +++ b/web/plugins/com.msgbyte.openapi/src/modals/CreateOpenApp.tsx @@ -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 = 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 ( + + + + ); + } +); +CreateOpenApp.displayName = 'CreateOpenApp'; diff --git a/web/plugins/com.msgbyte.openapi/src/translate.ts b/web/plugins/com.msgbyte.openapi/src/translate.ts new file mode 100644 index 00000000..e07a2340 --- /dev/null +++ b/web/plugins/com.msgbyte.openapi/src/translate.ts @@ -0,0 +1,5 @@ +import { localTrans } from '@capital/common'; + +export const Translate = { + openapi: localTrans({ 'zh-CN': '开放平台', 'en-US': 'Open Api' }), +}; diff --git a/web/plugins/com.msgbyte.openapi/tsconfig.json b/web/plugins/com.msgbyte.openapi/tsconfig.json new file mode 100644 index 00000000..465a28b5 --- /dev/null +++ b/web/plugins/com.msgbyte.openapi/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "baseUrl": "./src", + "esModuleInterop": true, + "jsx": "react", + "paths": { + "@capital/*": ["../../../src/plugin/*"], + } + } +} diff --git a/web/src/plugin/common/index.ts b/web/src/plugin/common/index.ts index c77b78d4..6105c65d 100644 --- a/web/src/plugin/common/index.ts +++ b/web/src/plugin/common/index.ts @@ -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'; +/** + * 处理axios的request 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)); +} diff --git a/web/src/plugin/component/index.tsx b/web/src/plugin/component/index.tsx index adcf9e50..b765eda2 100644 --- a/web/src/plugin/component/index.tsx +++ b/web/src/plugin/component/index.tsx @@ -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'; diff --git a/web/src/styles/antd/dark.less b/web/src/styles/antd/dark.less index a6dc45c4..a026965b 100644 --- a/web/src/styles/antd/dark.less +++ b/web/src/styles/antd/dark.less @@ -58,6 +58,10 @@ .ant-input:not(.ant-input-disabled) { background-color: transparent; border-color: var(--antd-primary-dangerous-color); + + &:hover { + background-color: transparent; + } } }