diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f02714ff..19e48dec 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -379,7 +379,12 @@ importers:
specifiers: {}
web/plugins/com.msgbyte.webview:
- specifiers: {}
+ specifiers:
+ js-base64: ^3.7.2
+ xss: ^1.0.11
+ dependencies:
+ js-base64: 3.7.2
+ xss: 1.0.11
packages:
@@ -3897,7 +3902,6 @@ packages:
/commander/2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
- dev: true
/commander/6.2.1:
resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npm.taobao.org/commander/download/commander-6.2.1.tgz}
@@ -4134,6 +4138,10 @@ packages:
engines: {node: '>= 6'}
dev: true
+ /cssfilter/0.0.10:
+ resolution: {integrity: sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==}
+ dev: false
+
/cssnano-preset-default/4.0.8:
resolution: {integrity: sha1-kgYisfwelaNOiDggPxOXpQTy0/8=, registry: https://registry.npm.taobao.org/, tarball: https://registry.npm.taobao.org/cssnano-preset-default/download/cssnano-preset-default-4.0.8.tgz}
engines: {node: '>=6.9.0'}
@@ -6102,7 +6110,7 @@ packages:
engines: {node: '>=10'}
/js-base64/3.7.2:
- resolution: {integrity: sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==, registry: https://registry.npm.taobao.org/, tarball: js-base64/download/js-base64-3.7.2.tgz}
+ resolution: {integrity: sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==}
dev: false
/js-sha3/0.8.0:
@@ -10166,6 +10174,15 @@ packages:
engines: {node: '>=0.4.0'}
dev: false
+ /xss/1.0.11:
+ resolution: {integrity: sha512-EimjrjThZeK2MO7WKR9mN5ZC1CSqivSl55wvUK5EtU6acf0rzEE1pN+9ZDrFXJ82BRp3JL38pPE6S4o/rpp1zQ==}
+ engines: {node: '>= 0.10.0'}
+ hasBin: true
+ dependencies:
+ commander: 2.20.3
+ cssfilter: 0.0.10
+ dev: false
+
/xtend/4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
diff --git a/web/plugins/com.msgbyte.webview/package.json b/web/plugins/com.msgbyte.webview/package.json
index f8e54e87..b564ee35 100644
--- a/web/plugins/com.msgbyte.webview/package.json
+++ b/web/plugins/com.msgbyte.webview/package.json
@@ -3,5 +3,8 @@
"main": "src/index.tsx",
"version": "0.0.0",
"private": true,
- "dependencies": {}
+ "dependencies": {
+ "js-base64": "^3.7.2",
+ "xss": "^1.0.11"
+ }
}
diff --git a/web/plugins/com.msgbyte.webview/src/group/GroupCustomWebPanelRender.tsx b/web/plugins/com.msgbyte.webview/src/group/GroupCustomWebPanelRender.tsx
new file mode 100644
index 00000000..33b56ca3
--- /dev/null
+++ b/web/plugins/com.msgbyte.webview/src/group/GroupCustomWebPanelRender.tsx
@@ -0,0 +1,43 @@
+import React, { useMemo } from 'react';
+import { encode } from 'js-base64';
+import { isValidStr } from '@capital/common';
+import { Translate } from '../translate';
+import { FilterXSS, getDefaultWhiteList } from 'xss';
+import _mapValues from 'lodash/mapValues';
+
+const xss = new FilterXSS({
+ // 允许style存在
+ whiteList: {
+ ..._mapValues(getDefaultWhiteList(), (v) => [...v, 'style']),
+ style: [],
+ },
+ css: false,
+});
+
+const GroupCustomWebPanelRender: React.FC<{ panelInfo: any }> = (props) => {
+ const panelInfo = props.panelInfo;
+
+ if (!panelInfo) {
+ return
{Translate.notfound}
;
+ }
+
+ const html = panelInfo?.meta?.html;
+ const src = useMemo(() => {
+ if (isValidStr(html)) {
+ try {
+ return `data:text/html;charset=utf8;base64,${encode(
+ xss.process(html)
+ )}`;
+ } catch (e) {
+ return undefined;
+ }
+ } else {
+ return undefined;
+ }
+ }, [html]);
+
+ return ;
+};
+GroupCustomWebPanelRender.displayName = 'GroupCustomWebPanelRender';
+
+export default GroupCustomWebPanelRender;
diff --git a/web/plugins/com.msgbyte.webview/src/group/GroupWebPanelRender.tsx b/web/plugins/com.msgbyte.webview/src/group/GroupWebPanelRender.tsx
new file mode 100644
index 00000000..fa99bb8b
--- /dev/null
+++ b/web/plugins/com.msgbyte.webview/src/group/GroupWebPanelRender.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { Translate } from '../translate';
+
+const GroupWebPanelRender: React.FC<{ panelInfo: any }> = (props) => {
+ const panelInfo = props.panelInfo;
+
+ if (!panelInfo) {
+ return {Translate.notfound}
;
+ }
+
+ const url = panelInfo?.meta?.url;
+
+ return (
+
+ );
+};
+GroupWebPanelRender.displayName = 'GroupWebPanelRender';
+
+export default GroupWebPanelRender;
diff --git a/web/plugins/com.msgbyte.webview/src/index.tsx b/web/plugins/com.msgbyte.webview/src/index.tsx
index fcf82be8..7fbef36e 100644
--- a/web/plugins/com.msgbyte.webview/src/index.tsx
+++ b/web/plugins/com.msgbyte.webview/src/index.tsx
@@ -1,27 +1,24 @@
-import React from 'react';
-import { regGroupPanel } from '@capital/common';
+import { Loadable, regGroupPanel } from '@capital/common';
import { Translate } from './translate';
const PLUGIN_NAME = 'com.msgbyte.webview';
-const GroupWebPanelRender: React.FC<{ panelInfo: any }> = (props) => {
- const panelInfo = props.panelInfo;
-
- if (!panelInfo) {
- return {Translate.notfound}
;
- }
-
- const url = panelInfo?.meta?.url;
-
- return (
-
- );
-};
-
regGroupPanel({
name: `${PLUGIN_NAME}/grouppanel`,
label: Translate.webpanel,
provider: PLUGIN_NAME,
extraFormMeta: [{ type: 'text', name: 'url', label: Translate.website }],
- render: GroupWebPanelRender,
+ render: Loadable(() => import('./group/GroupWebPanelRender')),
+});
+
+regGroupPanel({
+ name: `${PLUGIN_NAME}/customwebpanel`,
+ label: Translate.customwebpanel,
+ provider: PLUGIN_NAME,
+ extraFormMeta: [
+ { type: 'textarea', name: 'html', label: Translate.htmlcode },
+ ],
+ render: Loadable(() => import('./group/GroupCustomWebPanelRender'), {
+ componentName: 'com.msgbyte.webview:GroupCustomWebPanelRender',
+ }),
});
diff --git a/web/plugins/com.msgbyte.webview/src/translate.ts b/web/plugins/com.msgbyte.webview/src/translate.ts
index 8572d0ab..582eae17 100644
--- a/web/plugins/com.msgbyte.webview/src/translate.ts
+++ b/web/plugins/com.msgbyte.webview/src/translate.ts
@@ -2,6 +2,10 @@ import { localTrans } from '@capital/common';
export const Translate = {
webpanel: localTrans({ 'zh-CN': '网页面板', 'en-US': 'Webview Panel' }),
+ customwebpanel: localTrans({
+ 'zh-CN': '自定义网页面板',
+ 'en-US': 'Custom Webview Panel',
+ }),
notfound: localTrans({
'zh-CN': '加载失败, 面板信息不存在',
'en-US': 'Loading failed, panel info does not exist',
@@ -10,4 +14,8 @@ export const Translate = {
'zh-CN': '网址',
'en-US': 'Website',
}),
+ htmlcode: localTrans({
+ 'zh-CN': 'HTML代码',
+ 'en-US': 'HTML Code',
+ }),
};
diff --git a/web/src/components/Loadable.tsx b/web/src/components/Loadable.tsx
index bfbd49e0..76bc347b 100644
--- a/web/src/components/Loadable.tsx
+++ b/web/src/components/Loadable.tsx
@@ -6,6 +6,27 @@ import loadable, {
} from '@loadable/component';
import pMinDelay from 'p-min-delay';
import { LoadingSpinner } from './LoadingSpinner';
+import { isValidStr } from 'tailchat-shared';
+
+function promiseUsage(p: Promise, name: string): Promise {
+ const start = new Date().valueOf();
+
+ return p.then((r) => {
+ const end = new Date().valueOf();
+
+ console.debug(`[Loadable] load ${name} usage: ${end - start}ms`);
+
+ return r;
+ });
+}
+
+interface LoadableOptions extends OptionsWithoutResolver
{
+ /**
+ * 组件名, 如果传入则会记录组件加载用时
+ * 用于权衡组件大小
+ */
+ componentName?: string;
+}
/**
* 用法: Loadable(() => import('xxxxxx'))
@@ -13,10 +34,22 @@ import { LoadingSpinner } from './LoadingSpinner';
*/
export function Loadable(
loadFn: (props: Props) => Promise>,
- options?: OptionsWithoutResolver
+ options?: LoadableOptions
): LoadableComponent {
- return loadable((props) => pMinDelay(loadFn(props), 200), {
- fallback: ,
- ...options,
- });
+ return loadable(
+ (props) => {
+ let p = loadFn(props);
+
+ if (isValidStr(options?.componentName)) {
+ // 增加promise加载用时统计
+ p = promiseUsage(p, String(options?.componentName));
+ }
+
+ return pMinDelay(p, 200);
+ },
+ {
+ fallback: ,
+ ...options,
+ }
+ );
}