This commit is contained in:
2026-04-25 16:36:34 +08:00
commit db90e7579b
1876 changed files with 189777 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
## com.msgbyte.webview
`Tailchat` 增加 `Webview` 能力
### Usage
在 群组设置 -> 创建面板 可以添加网页面板。

View File

@@ -0,0 +1,12 @@
{
"label": "Web Panel Plugin",
"label.zh-CN": "网页面板插件",
"name": "com.msgbyte.webview",
"url": "/plugins/com.msgbyte.webview/index.js",
"version": "0.0.0",
"author": "msgbyte",
"description": "Provides groups with the ability to create web panels",
"description.zh-CN": "为群组提供创建网页面板的功能",
"documentUrl": "/plugins/com.msgbyte.webview/README.md",
"requireRestart": false
}

View File

@@ -0,0 +1,10 @@
{
"name": "@plugins/com.msgbyte.webview",
"main": "src/index.tsx",
"version": "0.0.0",
"private": true,
"dependencies": {
"url-regex": "^5.0.0",
"xss": "^1.0.14"
}
}

View File

@@ -0,0 +1,118 @@
import React, { useEffect, useRef, useState } from 'react';
import { Translate } from '../translate';
import { FilterXSS, getDefaultWhiteList } from 'xss';
import { useWatch } from '@capital/common';
import { GroupExtraDataPanel, NoData, TextArea } from '@capital/component';
import styled from 'styled-components';
const EditModalContent = styled.div`
padding: 10px;
width: 80vw;
height: 80vh;
display: flex;
flex-direction: column;
overflow: hidden;
.main {
flex: 1;
overflow: hidden;
> textarea {
height: 100%;
resize: none;
}
}
`;
const xss = new FilterXSS({
css: false,
whiteList: { ...getDefaultWhiteList(), iframe: ['src', 'style', 'class'] },
onIgnoreTag: function (tag, html, options) {
if (['html', 'body', 'head', 'meta', 'style', 'div'].includes(tag)) {
// 不对其属性列表进行过滤
return html;
}
},
});
function getInjectedStyle() {
try {
// 当前面板文本颜色
const currentTextColor = document.defaultView.getComputedStyle(
document.querySelector('.tc-content-background')
).color;
return `<style>body { color: ${currentTextColor} }</style>`;
} catch (e) {
return '';
}
}
const GroupCustomWebPanelRender: React.FC<{ html: string }> = (props) => {
const ref = useRef<HTMLIFrameElement>(null);
const html = props.html;
useEffect(() => {
if (!ref.current || !html) {
return;
}
const doc = ref.current.contentWindow.document;
doc.open();
doc.writeln(getInjectedStyle(), xss.process(html));
doc.close();
}, [html]);
if (!html) {
return <NoData />;
}
return <iframe ref={ref} className="w-full h-full" />;
};
GroupCustomWebPanelRender.displayName = 'GroupCustomWebPanelRender';
const GroupCustomWebPanelEditor: React.FC<{
initValue: string;
onChange: (html: string) => void;
}> = React.memo((props) => {
const [html, setHtml] = useState(() => props.initValue ?? '');
useWatch([html], () => {
props.onChange(html);
});
return <TextArea value={html} onChange={(e) => setHtml(e.target.value)} />;
});
GroupCustomWebPanelEditor.displayName = 'GroupCustomWebPanelEditor';
const GroupCustomWebPanel: React.FC<{ panelInfo: any }> = (props) => {
return (
<GroupExtraDataPanel
names={['html']}
render={(dataMap: Record<string, string>) => {
return (
<GroupCustomWebPanelRender
html={dataMap['html'] ?? props.panelInfo?.meta?.html ?? ''}
/>
);
}}
renderEdit={(dataMap: Record<string, string>) => {
return (
<EditModalContent>
<div>{Translate.editTip}</div>
<div className="main">
<GroupCustomWebPanelEditor
initValue={dataMap['html'] ?? props.panelInfo?.meta?.html ?? ''}
onChange={(html) => (dataMap['html'] = html)}
/>
</div>
</EditModalContent>
);
}}
/>
);
};
GroupCustomWebPanel.displayName = 'GroupCustomWebPanel';
export default GroupCustomWebPanel;

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { Translate } from '../translate';
import { GroupPanelContainer, WebviewKeepAlive } from '@capital/component';
import urlRegex from 'url-regex';
import { useGroupPanelContext } from '@capital/common';
const GroupWebPanelRender: React.FC<{ panelInfo: any }> = (props) => {
const { groupId, panelId } = useGroupPanelContext();
const panelInfo = props.panelInfo;
if (!panelInfo) {
return <div>{Translate.notfound}</div>;
}
let url = String(panelInfo?.meta?.url);
if (
!url.includes('://') &&
urlRegex({ exact: true, strict: false }).test(url)
) {
// 不包含协议, 但是是个网址
url = 'https://' + url;
}
const background = panelInfo?.meta?.background ?? false;
return (
<GroupPanelContainer groupId={groupId} panelId={panelId}>
<WebviewKeepAlive
key={String(url)}
className={`w-full h-full ${background ? 'bg-white' : ''}`}
url={url}
/>
</GroupPanelContainer>
);
};
GroupWebPanelRender.displayName = 'GroupWebPanelRender';
export default GroupWebPanelRender;

View File

@@ -0,0 +1,44 @@
import { Loadable, regGroupPanel } from '@capital/common';
import { Translate } from './translate';
const PLUGIN_NAME = 'com.msgbyte.webview';
regGroupPanel({
name: `${PLUGIN_NAME}/grouppanel`,
label: Translate.webpanel,
provider: PLUGIN_NAME,
extraFormMeta: [
{
type: 'text',
name: 'url',
label: Translate.website,
},
{
type: 'checkbox',
name: 'background',
label: Translate.addBackground,
},
],
render: Loadable(() => import('./group/GroupWebPanelRender')),
menus: [
{
name: 'openInNewWindow',
label: Translate.openInExtra,
icon: 'mdi:web',
onClick: (panelInfo) => {
if (panelInfo.meta?.url) {
window.open(String(panelInfo.meta?.url));
}
},
},
],
});
regGroupPanel({
name: `${PLUGIN_NAME}/customwebpanel`,
label: Translate.customwebpanel,
provider: PLUGIN_NAME,
render: Loadable(() => import('./group/GroupCustomWebPanelRender'), {
componentName: `${PLUGIN_NAME}:GroupCustomWebPanelRender`,
}),
});

View File

@@ -0,0 +1,33 @@
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',
}),
website: localTrans({
'zh-CN': '网址',
'en-US': 'Website',
}),
addBackground: localTrans({
'zh-CN': '添加背景色',
'en-US': 'Add Background Color',
}),
htmlcode: localTrans({
'zh-CN': 'HTML代码',
'en-US': 'HTML Code',
}),
openInExtra: localTrans({
'zh-CN': '在外部打开',
'en-US': 'Open in extra',
}),
editTip: localTrans({
'zh-CN': '使用html语法编辑, 关闭窗口自动保存',
'en-US': 'Edit with html syntax, close the window and save automatically',
}),
};

View File

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