From 44595b351f9888709958e979f983de630da95571 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Wed, 19 Apr 2023 20:49:27 +0800 Subject: [PATCH] feat: add fe ui in wxpusher plugin --- client/web/src/plugin/component/index.tsx | 1 + client/web/tailchat.d.ts | 8 + ...her.service.dev.ts => wxpusher.service.ts} | 4 +- .../src/SettingsPanel.tsx | 92 +++ .../com.msgbyte.wxpusher/src/index.tsx | 11 + .../com.msgbyte.wxpusher/src/translate.ts | 24 + .../com.msgbyte.wxpusher/types/tailchat.d.ts | 534 +++++++++++++++++- 7 files changed, 670 insertions(+), 4 deletions(-) rename server/plugins/com.msgbyte.wxpusher/services/{wxpusher.service.dev.ts => wxpusher.service.ts} (97%) create mode 100644 server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/SettingsPanel.tsx create mode 100644 server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/translate.ts diff --git a/client/web/src/plugin/component/index.tsx b/client/web/src/plugin/component/index.tsx index 17ebe87a..3fe703d0 100644 --- a/client/web/src/plugin/component/index.tsx +++ b/client/web/src/plugin/component/index.tsx @@ -56,6 +56,7 @@ export { GroupPanelSelector } from '@/components/GroupPanelSelector'; export { Emoji } from '@/components/Emoji'; export { PortalAdd, PortalRemove } from '@/components/Portal'; export { ErrorBoundary } from '@/components/ErrorBoundary'; +export { ErrorView } from '@/components/ErrorView'; export { UserAvatar } from '@/components/UserAvatar'; export { UserName } from '@/components/UserName'; export { Markdown } from '@/components/Markdown'; diff --git a/client/web/tailchat.d.ts b/client/web/tailchat.d.ts index 41f990e9..dcc8499f 100644 --- a/client/web/tailchat.d.ts +++ b/client/web/tailchat.d.ts @@ -410,6 +410,8 @@ declare module '@capital/component' { export const MessageAckContainer: any; + export const GroupExtraDataPanel: any; + export const Image: any; export const IconBtn: React.FC<{ @@ -497,6 +499,10 @@ declare module '@capital/component' { export const ErrorBoundary: any; + export const ErrorView: React.FC<{ + error: Error; + }>; + export const UserAvatar: React.FC<{ userId: string; className?: string; @@ -512,6 +518,8 @@ declare module '@capital/component' { export const Markdown: any; + export const MarkdownEditor: any; + export const Webview: any; export const WebviewKeepAlive: any; diff --git a/server/plugins/com.msgbyte.wxpusher/services/wxpusher.service.dev.ts b/server/plugins/com.msgbyte.wxpusher/services/wxpusher.service.ts similarity index 97% rename from server/plugins/com.msgbyte.wxpusher/services/wxpusher.service.dev.ts rename to server/plugins/com.msgbyte.wxpusher/services/wxpusher.service.ts index 5ad17c58..ee3b1822 100644 --- a/server/plugins/com.msgbyte.wxpusher/services/wxpusher.service.dev.ts +++ b/server/plugins/com.msgbyte.wxpusher/services/wxpusher.service.ts @@ -114,9 +114,9 @@ class WxpusherService extends TcService { return null; } - const uid = user.wxpusherUserId; + const wxpusherUserId = user.wxpusherUserId; - return uid; + return wxpusherUserId; } /** diff --git a/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/SettingsPanel.tsx b/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/SettingsPanel.tsx new file mode 100644 index 00000000..a0ced86b --- /dev/null +++ b/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/SettingsPanel.tsx @@ -0,0 +1,92 @@ +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { createPluginRequest, useAsync } from '@capital/common'; +import { LoadingSpinner, ErrorView } from '@capital/component'; +import { Translate } from './translate'; + +const request = createPluginRequest('com.msgbyte.wxpusher'); + +const SettingsPanel: React.FC = React.memo(() => { + const [wxpusherUserId, setWxpusherUserId] = useState(''); + const { loading, error } = useAsync(async () => { + const { data } = await request.get('getWXPusherUserId'); + + setWxpusherUserId(data); + }, []); + + if (loading) { + return ; + } + + if (error) { + return ; + } + + return wxpusherUserId ? ( +
+
{Translate.binded}
+
+ {Translate.currentWXPusherId}: {wxpusherUserId} +
+
+ ) : ( + { + setWxpusherUserId(wxpusherUserId); + }} + /> + ); +}); +SettingsPanel.displayName = 'SettingsPanel'; + +export const QRCode: React.FC<{ + onBindSuccess: (wxpusherUserId: string) => void; +}> = React.memo((props) => { + const { + loading, + error, + value: url, + } = useAsync(async () => { + const { data } = await request.post('createQRCode'); + return data.data.url; + }, []); + + const onBindSuccessRef = useRef(props.onBindSuccess); + onBindSuccessRef.current = props.onBindSuccess; + useLayoutEffect(() => { + let timer: number; + async function loop() { + const { data: wxpusherUserId } = await request.get('getWXPusherUserId'); + if (wxpusherUserId) { + onBindSuccessRef.current(wxpusherUserId); + } else { + timer = window.setTimeout(loop, 4 * 1000); // 4s loop + } + } + + loop(); + + return () => { + if (timer) { + window.clearTimeout(timer); + } + }; + }, []); + + if (loading) { + return ; + } + + if (error) { + return ; + } + + return ( +
+
{Translate.useWechatBindTip}
+ +
+ ); +}); +QRCode.displayName = 'QRCode'; + +export default SettingsPanel; diff --git a/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/index.tsx b/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/index.tsx index 04ecbf7d..60fb6e73 100644 --- a/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/index.tsx +++ b/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/index.tsx @@ -1 +1,12 @@ +import { regCustomPanel } from '@capital/common'; +import { Loadable } from '@capital/component'; + console.log('Plugin wxpusher is loaded'); + +regCustomPanel({ + position: 'setting', + icon: '', + name: 'com.msgbyte.wxpusher/settings', + label: 'WxPusher', + render: Loadable(() => import('./SettingsPanel')), +}); diff --git a/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/translate.ts b/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/translate.ts new file mode 100644 index 00000000..405f7380 --- /dev/null +++ b/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/src/translate.ts @@ -0,0 +1,24 @@ +import { localTrans } from '@capital/common'; + +export const Translate = { + loadingState: localTrans({ + 'zh-CN': '正在检查绑定状态', + 'en-US': 'Checking binding status', + }), + binded: localTrans({ + 'zh-CN': '已绑定', + 'en-US': 'Binded', + }), + currentWXPusherId: localTrans({ + 'zh-CN': '当前 wxpusher ID', + 'en-US': 'Current wxpusher uid', + }), + loadingQRCode: localTrans({ + 'zh-CN': '正在加载绑定二维码', + 'en-US': 'Binding QR code is loading', + }), + useWechatBindTip: localTrans({ + 'zh-CN': '使用微信扫码绑定 wxpusher', + 'en-US': 'Use wechat scan QRCode to bind wxpusher', + }), +}; diff --git a/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/types/tailchat.d.ts b/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/types/tailchat.d.ts index 49f524ae..aece80b9 100644 --- a/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/types/tailchat.d.ts +++ b/server/plugins/com.msgbyte.wxpusher/web/plugins/com.msgbyte.wxpusher/types/tailchat.d.ts @@ -1,2 +1,532 @@ -declare module '@capital/common'; -declare module '@capital/component'; +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/// + +/** + * 该文件由 Tailchat 自动生成 + * 用于插件的类型声明 + * 生成命令: pnpm run plugins:declaration:generate + */ + +/** + * Tailchat 通用 + */ +declare module '@capital/common' { + export const useGroupPanelParams: any; + + /** + * 打开模态框 + * @deprecated 请从 @capital/component 引入 + */ + export const openModal: ( + content: React.ReactNode, + + props?: { + /** + * 是否显示右上角的关闭按钮 + * @default false + */ + closable?: boolean; + + /** + * 遮罩层是否可关闭 + */ + maskClosable?: boolean; + + /** + * 关闭modal的回调 + */ + onCloseModal?: () => void; + } + ) => number; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const closeModal: any; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const ModalWrapper: any; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const useModalContext: any; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const openConfirmModal: any; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const openReconfirmModal: any; + + /** + * @deprecated 请从 @capital/component 引入 + */ + export const Loadable: any; + + export const getGlobalState: any; + + export const useGlobalSocketEvent: ( + eventName: string, + callback: (data: T) => void + ) => void; + + export const getJWTUserInfo: () => Promise<{ + _id?: string; + nickname?: string; + email?: string; + avatar?: string; + }>; + + export const dataUrlToFile: any; + + export const urlSearchStringify: any; + + export const urlSearchParse: any; + + export const appendUrlSearch: any; + + export const getServiceWorkerRegistration: any; + + export const getServiceUrl: () => string; + + export const getCachedUserInfo: ( + userId: string, + refetch?: boolean + ) => Promise<{ + _id: string; + email: string; + nickname: string; + discriminator: string; + avatar: string | null; + temporary: boolean; + }>; + + export const getCachedConverseInfo: any; + + export const getCachedBaseGroupInfo: any; + + export const getCachedUserSettings: any; + + /** + * 本地翻译 + * @example + * localTrans({'zh-CN': '你好', 'en-US': 'Hello'}); + * + * @param trans 翻译对象 + */ + export const localTrans: (trans: Record<'zh-CN' | 'en-US', string>) => string; + + export const getLanguage: any; + + export const sharedEvent: any; + + export const useAsync: Promise>( + fn: T, + deps?: React.DependencyList + ) => { loading: boolean; value?: any; error?: Error }; + + export const useAsyncFn: Promise>( + fn: T, + deps?: React.DependencyList + ) => [{ loading: boolean; value?: any; error?: Error }, T]; + + export const useAsyncRefresh: Promise>( + fn: T, + deps?: React.DependencyList + ) => { loading: boolean; value?: any; error?: Error; refresh: () => void }; + + export const useAsyncRequest: Promise>( + fn: T, + deps?: React.DependencyList + ) => [{ loading: boolean; value?: any }, T]; + + export const uploadFile: any; + + export const showToasts: ( + message: string, + type?: 'info' | 'success' | 'error' | 'warning' + ) => void; + + export const showSuccessToasts: any; + + export const showErrorToasts: (error: any) => void; + + export const fetchAvailableServices: any; + + export const isValidStr: (str: any) => str is string; + + export const useGroupPanelInfo: any; + + export const sendMessage: any; + + export const showMessageTime: any; + + export const joinArray: any; + + export const navigate: any; + + export const useLocation: any; + + export const useNavigate: any; + + /** + * @deprecated please use createMetaFormSchema from @capital/component + */ + export const createFastFormSchema: any; + + /** + * @deprecated please use metaFormFieldSchema from @capital/component + */ + export const fieldSchema: any; + + export const useCurrentUserInfo: any; + + export const createPluginRequest: (pluginName: string) => { + get: (actionName: string, config?: any) => Promise; + post: (actionName: string, data?: any, config?: any) => Promise; + }; + + export const postRequest: any; + + export const pluginCustomPanel: any; + + export const regCustomPanel: (info: { + position: + | 'personal' + | 'setting' + | 'groupdetail' + | 'navbar-more' + | 'navbar-group' + | 'navbar-personal'; + icon: string; + name: string; + label: string; + render: React.ComponentType; + }) => void; + + export const pluginGroupPanel: any; + + export const regGroupPanel: any; + + export const messageInterpreter: { + name?: string; + explainMessage: (message: string) => React.ReactNode; + }[]; + + export const regMessageInterpreter: (interpreter: { + name?: string; + explainMessage: (message: string) => React.ReactNode; + }) => void; + + export const getMessageRender: (message: string) => React.ReactNode; + + export const regMessageRender: ( + render: (message: string) => React.ReactNode + ) => void; + + export const getMessageTextDecorators: any; + + export const regMessageTextDecorators: any; + + export const ChatInputActionContextProps: any; + + export const pluginChatInputActions: any; + + export const regChatInputAction: any; + + export const regSocketEventListener: (item: { + eventName: string; + eventFn: (...args: any[]) => void; + }) => void; + + export const pluginColorScheme: any; + + export const regPluginColorScheme: any; + + export const pluginInspectServices: any; + + export const regInspectService: any; + + export const pluginMessageExtraParsers: any; + + export const regMessageExtraParser: any; + + export const pluginRootRoute: any; + + export const regPluginRootRoute: any; + + export const pluginPanelActions: any; + + export const regPluginPanelAction: ( + action: + | { + name: string; + label: string; + icon: string; + position: 'group'; + onClick: (ctx: { groupId: string; panelId: string }) => void; + } + | { + name: string; + label: string; + icon: string; + position: 'dm'; + onClick: (ctx: { converseId: string }) => void; + } + ) => void; + + export const pluginPermission: any; + + export const regPluginPermission: (permission: { + /** + * 权限唯一key, 用于写入数据库 + * 如果为插件则权限点应当符合命名规范, 如: plugin.com.msgbyte.github.manage + */ + key: string; + /** + * 权限点显示名称 + */ + title: string; + /** + * 权限描述 + */ + desc: string; + /** + * 是否默认开启 + */ + default: boolean; + /** + * 是否依赖其他权限点 + */ + required?: string[]; + }) => void; + + export const pluginGroupPanelBadges: any; + + export const regGroupPanelBadge: any; + + export const pluginGroupTextPanelExtraMenus: any; + + export const regPluginGroupTextPanelExtraMenu: any; + + export const pluginUserExtraInfo: any; + + export const regUserExtraInfo: any; + + export const pluginSettings: any; + + export const regPluginSettings: any; + + export const pluginInboxItemMap: any; + + export const regPluginInboxItemMap: any; + + export const useGroupIdContext: () => string; + + export const useGroupPanelContext: () => { + groupId: string; + panelId: string; + } | null; + + export const useSocketContext: any; +} + +/** + * Tailchat 组件 + */ +declare module '@capital/component' { + export const Button: any; + + export const Checkbox: any; + + export const Input: any; + + export const Divider: any; + + export const Space: any; + + export const Menu: any; + + export const Table: any; + + export const Switch: any; + + export const Tooltip: any; + + /** + * @link https://ant.design/components/notification-cn/ + */ + export const notification: any; + + export const Empty: React.FC< + React.PropsWithChildren<{ + prefixCls?: string; + className?: string; + style?: React.CSSProperties; + imageStyle?: React.CSSProperties; + image?: React.ReactNode; + description?: React.ReactNode; + }> + >; + + export const TextArea: any; + + export const Avatar: any; + + export const SensitiveText: React.FC<{ className?: string; text: string }>; + + export const Icon: React.FC<{ icon: string } & React.SVGProps>; + + export const CopyableText: React.FC<{ + className?: string; + style?: React.CSSProperties; + config?: + | boolean + | { + text?: string; + onCopy?: (event?: React.MouseEvent) => void; + icon?: React.ReactNode; + tooltips?: boolean | React.ReactNode; + format?: 'text/plain' | 'text/html'; + }; + }>; + + export const WebFastForm: any; + + export const WebMetaForm: any; + + export const createMetaFormSchema: any; + + export const metaFormFieldSchema: any; + + export const Link: any; + + export const MessageAckContainer: any; + + export const Image: any; + + export const IconBtn: React.FC<{ + icon: string; + className?: string; + iconClassName?: string; + size?: 'small' | 'middle' | 'large'; + shape?: 'circle' | 'square'; + title?: string; + danger?: boolean; + active?: boolean; + disabled?: boolean; + onClick?: React.MouseEventHandler; + }>; + + export const PillTabs: any; + + export const PillTabPane: any; + + export const LoadingSpinner: React.FC<{ tip?: string }>; + + export const FullModalField: any; + + export const DefaultFullModalInputEditorRender: any; + + export const DefaultFullModalTextAreaEditorRender: any; + + export const openModal: ( + content: React.ReactNode, + + props?: { + /** + * 是否显示右上角的关闭按钮 + * @default false + */ + closable?: boolean; + + /** + * 遮罩层是否可关闭 + */ + maskClosable?: boolean; + + /** + * 关闭modal的回调 + */ + onCloseModal?: () => void; + } + ) => number; + + export const closeModal: any; + + export const ModalWrapper: any; + + export const useModalContext: any; + + export const openConfirmModal: any; + + export const openReconfirmModal: any; + + export const Loadable: any; + + export const Loading: React.FC<{ + spinning: boolean; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; + }>; + + export const LoadingOnFirst: React.FC<{ + spinning: boolean; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; + }>; + + export const SidebarView: any; + + export const GroupPanelSelector: any; + + export const Emoji: any; + + export const PortalAdd: any; + + export const PortalRemove: any; + + export const ErrorBoundary: any; + + export const ErrorView: React.FC<{ + error: Error; + }>; + + export const UserAvatar: React.FC<{ + userId: string; + className?: string; + style?: React.CSSProperties; + size?: 'large' | 'small' | 'default' | number; + }>; + + export const UserName: React.FC<{ + userId: string; + className?: string; + style?: React.CSSProperties; + }>; + + export const Markdown: any; + + export const Webview: any; + + export const WebviewKeepAlive: any; + + export const Card: any; + + export const Problem: any; + + export const JumpToButton: any; + + export const JumpToGroupPanelButton: any; + + export const JumpToConverseButton: any; +}