Merge branch 'develop-cy' into 'develop'

🦄 refactor: 【CenterLink】完善中心对接模块物料;

See merge request web-project/zhst-lambo!15
This commit is contained in:
江志雄 2024-04-19 17:02:53 +08:00
commit f2cc70c785
15 changed files with 379 additions and 334 deletions

View File

@ -0,0 +1,65 @@
import React,{useRef} from 'react'
import TerminalForm from './components/TerminalForm'
import WebTerminal from './components/WebTerminal'
import './index.less';
import style from 'packages/meta/src/badge/style';
interface CenterLinkProps{
websocketUrl:string; // websocket服务地址
token:string; // 用户token信息
ip:string; // IP地址
onExportLogs:()=>void, // 导出日志事件
terminalStyle?:React.CSSProperties;
onConnect?:(values:any) => void, // 连接服务器事件
onOpen?: (event: WebSocketEventMap['open'], instance: WebSocket) => void;
onClose?: (event: WebSocketEventMap['close'], instance: WebSocket) => void;
onMessage?: (message: WebSocketEventMap['message'], instance: WebSocket) => void;
onError?: (event: WebSocketEventMap['error'], instance: WebSocket) => void;
style?:React.CSSProperties;
}
const materialName='center-link';
const CenterLink:React.FC<CenterLinkProps>=(props:CenterLinkProps)=>{
const {
websocketUrl,
ip,
token,
terminalStyle,
onConnect,
onExportLogs,
onOpen,
onClose,
onMessage,
onError,
style
}=props;
const webRef=useRef(null);
// 处理开始连接服务器ip的事件
const handleConnectClick=(values:any)=>{
const {ip}=values;
if(ip&&token&&websocketUrl&&webRef.current){
webRef.current.connect();
}
onConnect&&onConnect(values);
}
return (
<div className={materialName} style={style}>
<TerminalForm onConnect={handleConnectClick} onExportLogs={onExportLogs} />
<WebTerminal
ref={webRef}
websocketUrl={websocketUrl}
ip={ip}
token={token}
terminalStyle={terminalStyle}
onOpen={onOpen}
onClose={onClose}
onMessage={onMessage}
onError={onError}
/>
</div>
)
}
export default CenterLink

View File

@ -0,0 +1,68 @@
import React from 'react';
import { Button, Form, Input,FormProps } from '@zhst/meta';
import '../index.less';
const materialName='terminal-form';
export interface TerminalFormProps{
onConnect:FormProps['onFinish']; // 开始连接事件
onExportLogs:()=>void; // 导出日志事件
}
const TerminalForm:React.FC<TerminalFormProps>=(props:TerminalFormProps)=> {
const {
onConnect,
onExportLogs,
}=props;
const [form] = Form.useForm();
return (
<div className={materialName}>
<h1></h1>
<Form form={form} requiredMark={false} layout={'inline'} >
<Form.Item
label="输入中心服务器IP"
name="ip"
rules={[
{ required: true, message: 'ip不能为空' },
{
pattern: new RegExp(
'^(?=(\\b|\\D))(((\\d{1,2})|(1\\d{1,2})|(2[0-4]\\d)|(25[0-5]))\\.){3}((\\d{1,2})|(1\\d{1,2})|(2[0-4]\\d)|(25[0-5]))(?=(\\b|\\D))$',
'g',
),
message: '输入ip格式不正确',
},
{
type: 'string',
whitespace: true,
message: '请输入不包含空格的ip',
},
]}
>
<Input style={{ width: 320, height: 36 }} allowClear />
</Form.Item>
<Form.Item >
<Button
style={{ width: 100, height: 36,background:'#23ACB2' }}
type="primary"
onClick={async()=>{
const values=await form.validateFields();
onConnect&&onConnect(values);
}}
>
</Button>
</Form.Item>
<Form.Item >
<Button
style={{ width: 100, height: 36 }}
onClick={onExportLogs}
>
</Button>
</Form.Item>
</Form>
</div>
)
}
export default TerminalForm;

View File

@ -0,0 +1,133 @@
import React,{useRef,useEffect, useImperativeHandle,
forwardRef,} from 'react';
import { useWebSocket } from '@zhst/hooks';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';
import '../index.less';
export interface WebsocketOptions {
reconnectLimit?: number; // 重试次数
reconnectInterval?: number; // 重试时间间隔ms
onOpen?: (event: WebSocketEventMap['open'], instance: WebSocket) => void; // webSocket 连接成功回调
onClose?: (event: WebSocketEventMap['close'], instance: WebSocket) => void; // webSocket 关闭回调
onMessage?: (message: WebSocketEventMap['message'], instance: WebSocket) => void; // webSocket 收到消息回调
onError?: (event: WebSocketEventMap['error'], instance: WebSocket) => void; // webSocket 错误回调
protocols?: string | string[]; // 子协议
}
enum ReadyState {
Connecting = 0,
Open = 1,
Closing = 2,
Closed = 3,
}
export interface WebsocketResult {
latestMessage?: WebSocketEventMap['message']; // 最新消息
sendMessage: WebSocket['send']; // 发送消息函数
disconnect: () => void; // 手动断开 webSocket 连接
connect: () => void; // 手动连接 webSocket如果当前已有连接则关闭后重新连接
readyState: ReadyState; // 当前 webSocket 连接状态
webSocketIns?: WebSocket; // webSocket 实例
}
interface WebTerminalProps{
websocketUrl:string; // websocket服务地址
token:string; // 用户token信息
ip:string; // IP地址
terminalStyle?:React.CSSProperties;
onOpen?: (event: WebSocketEventMap['open'], instance: WebSocket) => void;
onClose?: (event: WebSocketEventMap['close'], instance: WebSocket) => void;
onMessage?: (message: WebSocketEventMap['message'], instance: WebSocket) => void;
onError?: (event: WebSocketEventMap['error'], instance: WebSocket) => void;
}
const materialName = 'web-terminal'
const WebTerminal:React.FC<WebTerminalProps&WebsocketOptions>=forwardRef((props:WebTerminalProps&WebsocketOptions,ref)=> {
const {
websocketUrl='',
token='',
ip='',
terminalStyle,
onOpen,
onClose,
onMessage,
onError,
}=props;
const { readyState, sendMessage, latestMessage, disconnect, connect }:WebsocketResult = useWebSocket(
`${websocketUrl}?ip=${ip}&Authorization=${token}`,{manual:true,reconnectLimit:0,onOpen,
onClose,
onMessage,
onError}
);
const termRef = useRef(null);
const termClassRef=useRef(null)
// const currLine=useRef(null);
useEffect(()=>{
if(termClassRef?.current){
termClassRef.current.write(latestMessage?.data+'\r\n\x1b[33m$\x1b[0m ');
}
},[latestMessage]);
// terminal初始化
useEffect(()=>{
// 初始化terminal
if(!termRef.current){
return;
}
termClassRef.current=new Terminal({
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
fontWeight: 400,
fontSize: 14,
rows: Math.ceil(
(termRef.current?.clientHeight -
150) /
14,
),
convertEol: true,//控制终端是否自动将 \n 转换为 \r\n。
cursorBlink: true,//指定光标是否闪烁
scrollback: 50, //终端中的回滚量
disableStdin: false, //是否应禁用输入。
cursorStyle: "underline", //光标样式
windowsMode: true, // 根据窗口换行
theme: {
foreground: "#ffffff", //字体
background: "#1a1a1d", //背景色
cursor: "help", //设置光标
}
})
let term=termClassRef.current;
term.open(termRef.current);
term.focus(); // 光标聚集
term.promp=(_)=>{
term.write('\r\n\x1b[33m$\x1b[0m ');
}
const fitAddon=new FitAddon();
term.loadAddon(fitAddon);
fitAddon.fit();
term.promp();
},[]);
// 自定义暴露给父组件的实例
useImperativeHandle(ref,()=>({
readyState,
sendMessage,
latestMessage,
disconnect,
connect
}));
return (
<div className={materialName}>
<div style={terminalStyle} ref={termRef}></div>
</div>
)
})
export default WebTerminal;

View File

@ -0,0 +1,32 @@
import React from 'react';
import { CenterLink } from '@zhst/material';
const demo = () => {
return (
<CenterLink
style={{width:'100%',height:'600px'}}
websocketUrl={'ws://10.0.0.7:50051/active'}
token={'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTM1OTI2NDYsImp0aSI6ImFkbWluIn0._mVU216h0q8va8bZ8PCKfGOKslYJWdRLFvLzUdvGDN4'}
ip={'127.0.0.1'}
terminalStyle={{width:'100%',height:'calc(100% - 180px)'}}
onConnect={(values)=>{
console.log(values,'====> Connecting');
}}
onExportLogs={()=>{console.log('=====> Export Log')}}
onOpen={(event: WebSocketEventMap['open'], instance: WebSocket)=>{
console.log(event,'===>open'); // webSocket 连接成功回调
}}
onClose={(event: WebSocketEventMap['close'], instance: WebSocket)=>{
console.log(event,'===>close'); // webSocket 关闭回调
}}
onMessage={(message: WebSocketEventMap['message'], instance: WebSocket)=>{
console.log(message,'===>message'); // webSocket 消息回调
}}
onError={(event: WebSocketEventMap['error'], instance: WebSocket)=>{
console.log(event,'===>error'); // webSocket 错误回调
}}
/>
);
};
export default demo;

View File

@ -0,0 +1,32 @@
.center-link{
width: 100%;
height: 100%;
padding: 30px;
box-sizing: border-box;
background-color: #E5EAEC;
overflow: hidden;
}
.web-terminal{
width: 100%;
height: 100%;
// padding: 30px;
// box-sizing: border-box;
// background-color: #E5EAEC;
// overflow: hidden;
}
.terminal-form{
width: 100%;
margin-bottom: 30px;
h1{
font-family: SourceHanSansCN, SourceHanSansCN;
font-weight: bold;
font-size:16px;
text-align: left;
line-height: 24px;
margin-bottom: 20px;
color: rgba(0,0,0,80%);
}
}

View File

@ -0,0 +1,30 @@
---
category: Components
title: CenterLink 中心对接
toc: content
group:
title: 通用
order: 2
---
中心对接
## 代码演示
<code src="./demo/basic.tsx">基本用法</code>
## API
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| websocketUrl | websocket地址【必传】 | string | - | - |
| ip | 服务器ip地址【必传】 | string | - | - |
| token | 用户token信息【必传】 | string | - | - |
| terminalStyle | 终端黑盒子样式【可选传】 | React.CSSProperties | - | - |
| style | 整个页面的样式【可选传】 | React.CSSProperties | - | - |
| onExportLogs | 导出日志事件【可选传】 | ()=>void | - | - |
| onConnect | 连接服务器事件【可选传】 | (values:any) => void | - | - |
| onOpen | webSocket 连接成功回调【可选传】 | (event: WebSocketEventMap['open'], instance: WebSocket) => void | - | - |
| onClose | webSocket 关闭回调【可选传】 | (event: WebSocketEventMap['close'], instance: WebSocket) => void | - | - |
| onMessage | webSocket 收到消息回调【可选传】 | (event:WebSocketEventMap['message'], instance: WebSocket) => void | - | - |
| onError | webSocket 错误回调【可选传】 | (event: WebSocketEventMap['error'], instance: WebSocket) => void | - | - |

View File

@ -0,0 +1,3 @@
import CenterLink from "./CenterLink";
export default CenterLink;

View File

@ -3,6 +3,6 @@ export { default as AlgorithmConfig } from './algorithmConfig';
export type { AlgorithmConfigRef, AlgorithmConfigProps } from './algorithmConfig'; export type { AlgorithmConfigRef, AlgorithmConfigProps } from './algorithmConfig';
export { default as Login } from './login'; export { default as Login } from './login';
export { default as Password } from './password'; export { default as Password } from './password';
export { default as Terminal } from './terminal'; export { default as CenterLink } from './centerLink';
export { default as SchemaFormModal } from './algorithmConfig/components/schemaFormModal'; export { default as SchemaFormModal } from './algorithmConfig/components/schemaFormModal';
export * from 'rc-util' export * from 'rc-util'

View File

@ -1,65 +0,0 @@
import { Button, Form, Input,FormProps } from '@zhst/meta';
import React from 'react';
import WebTerminal from './components/WebTerminal';
import './index.less';
interface TerminalProps{
onFinish:FormProps['onFinish'];
onExportLog:(filePath:string)=>void;
websocketUrl:string; // websocket地址
token:string; // 用户token信息
ip:string; // ip地址
// filePath:string; // 导出日志的文件地址
}
const materialName = 'zhst-material-terminal'
const Terminal: React.FC<TerminalProps> = (props:TerminalProps) => {
const {onFinish,websocketUrl='',token='',ip='',onExportLog}=props;
const [form] = Form.useForm();
const handleCenterConnect = async () => {
const values = await form.validateFields();
onFinish&&onFinish(values);
};
return (
<div className={materialName} style={{}}>
<h1 ></h1>
<Form form={form} requiredMark={false} layout={'inline'} >
<Form.Item
label="输入中心服务器IP"
name="ip"
rules={[
{ required: true, message: 'ip不能为空' },
{
pattern: new RegExp(
'^(?=(\\b|\\D))(((\\d{1,2})|(1\\d{1,2})|(2[0-4]\\d)|(25[0-5]))\\.){3}((\\d{1,2})|(1\\d{1,2})|(2[0-4]\\d)|(25[0-5]))(?=(\\b|\\D))$',
'g',
),
message: '输入ip格式不正确',
},
{
type: 'string',
whitespace: true,
message: '请输入不包含空格的ip',
},
]}
>
<Input style={{ width: 320, height: 36 }} allowClear />
</Form.Item>
<Form.Item >
<Button style={{ width: 100, height: 36,background:'#23ACB2' }} type="primary" onClick={handleCenterConnect}>
</Button>
</Form.Item>
<Form.Item >
<Button style={{ width: 100, height: 36, }} onClick={onExportLog}>
</Button>
</Form.Item>
</Form>
<WebTerminal websocketUrl={websocketUrl} token={token} ip={ip} />
</div>
);
};
export default Terminal;

View File

@ -1,164 +0,0 @@
// @ts-nocheck
import React, {Component,useRef,useEffect } from 'react';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import WebsocketTerm from './WebsocketTerm';
import 'xterm/css/xterm.css';
// TODO:引入xterm 后续需要和后端在建立websocket连接再次调试
export default class WebTerminal extends Component {
term = null;
websocket = null;
curr_line = '';
websocketUrl=''; // websocket服务地址
token=''; // 用户token信息
ip=''; // IP地址
constructor(props:any){
super(props);
this.websocketUrl = props.websocketUrl;
this.token=props.token;
this.ip=props.ip;
}
componentDidMount() {
let term = this.term;
// term初始化
this.term = new Terminal({
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
fontWeight: 400,
fontSize: 14,
rows: Math.ceil(
(document.getElementsByClassName('container-children')[0].clientHeight -
150) /
14,
),
convertEol: true,//控制终端是否自动将 \n 转换为 \r\n。
cursorBlink: true,//指定光标是否闪烁
scrollback: 50, //终端中的回滚量
disableStdin: false, //是否应禁用输入。
cursorStyle: "underline", //光标样式
windowsMode: true, // 根据窗口换行
theme: {
foreground: "#ffffff", //字体
background: "#1a1a1d", //背景色
cursor: "help", //设置光标
}
});
this.term.open(document.getElementById('terminal'));
this.term.focus(); // 光标聚集
this.term.prompt = (_) => {
this.term.write('\r\n\x1b[33m$\x1b[0m ');
};
// // 换行并输入起始符
// this.term.prompt = (_) => {
// this.term.write("\r\n>>> ")
// }
if(this.ip!==''){
this.term.write('root@'+this.ip);
}
const fitAddon = new FitAddon();
this.term.loadAddon(fitAddon);
fitAddon.fit();
this.term.prompt();
// 添加事件监听器,支持输入方法
this.term.onKey((e) => {
const printable =
!e.domEvent.altKey &&
!e.domEvent.altGraphKey &&
!e.domEvent.ctrlKey &&
!e.domEvent.metaKey;
if (e.domEvent.keyCode === 13) {
this.Send(term, this.curr_line);
this.term.prompt();
this.curr_line = '';
} else if (e.domEvent.keyCode === 8) {
// back 删除的情况
if (this.term._core.buffer.x > 2) {
if (this.curr_line.length) {
this.curr_line = this.curr_line.slice(0, this.curr_line.length - 1);
this.term.write('\b \b');
} else {
}
}
} else if (printable) {
this.curr_line += e.key;
this.term.write(e.key);
}
this.term.focus();
});
this.term.onData((key) => {
// 粘贴的情况
if (key.length > 1) {
this.term.write(key);
this.curr_line += key;
}
});
this.initWebsock();
if(this.websocket){
// 只读属性 readyState 表示连接状态,可以是以下值
// 0 - 表示连接尚未建立。
// 1 - 表示连接已建立,可以进行通信。
// 2 - 表示连接正在进行关闭。
// 3 - 表示连接已经关闭或者连接不能打开
// websocket建立连接时发送ip以及token给后端
if(this.websocket.readyState ===1){
this.Send(this.term,{ip:this.ip,Authorization:this.token})
}
}
}
componentWillUnmount() {
this.term.dispose();
// WebSocket 方法 关闭连接
this.websocket.close();
}
initWebsock = () => {
let term = this.term;
let token = this.token;
let ip = this.ip;
// let preSuffix=location.protocol === 'http:' ? 'ws://' : 'wss://';
let preSuffix='ws://127.0.0.1:50051/active';
// 初始化
this.websocket = new WebSocket('ws://127.0.0.1:50051/active');
// WebSocket 事件
// 连接建立时触发
this.websocket.onopen = function (evt) {
term.write('connect');
};
// 连接关闭时触发
this.websocket.onclose = function (evt) {
term.write('exit');
};
// 客户端接收服务端数据时触发
this.websocket.onmessage = function (evt) {
term.write(evt.data);
};
// 通信发生错误时触发
this.websocket.onerror = function (evt) {
term.write('connect fail err:' + evt.data);
};
};
prompt = (term) => {
this.term.write('\r\n~$ ');
};
// WebSocket 方法 使用连接发送数据
Send = (term, message) => {
this.websocket.send(message);
};
render() {
return (
<div className="container-children">
<div id="terminal"></div>
</div>
);
}
}

View File

@ -1,17 +0,0 @@
import React from 'react';
import { Terminal } from '@zhst/material';
const demo = () => {
return (
<Terminal
onFinish={val => console.log('val', val)}
websocketUrl={'ws://127.0.0.1:30003'}
token={'this is user token'}
ip={'127.0.0.1'}
onExportLog={(url)=>{console.log(url,'====>url');}}
/>
);
};
export default demo;

View File

@ -1,64 +0,0 @@
.zhst-material-terminal{
width: 100%;
height: 100vh;
background-color: #E5EAEC;
box-sizing: border-box;
padding:30px;
overflow: hidden;
h1{
font-family: SourceHanSansCN, SourceHanSansCN;
font-weight: bold;
font-size:16px;
text-align: left;
line-height: 24px;
margin-bottom: 20px;
color: rgba(0,0,0,80%);
}
}
.container-children{
margin-top: 30px;
width: 100%;
height: calc(100vh - 180px);
box-sizing: border-box;
#terminal{
width: 100%;
height: calc(100vh - 180px);
.xterm-screen{
width: 100%;
height: 100%;
background-color: #000;
}
}
}
.zhst-form-item .zhst-form-item-control-input-content{
.zhst-btn-default:not(:disabled):not(.zhst-btn-disabled):hover{
color: #23ACB2;
border-color:#23acb2;
}
.zhst-input-affix-wrapper:focus{
border-color:#23acb2;
}
.zhst-input-affix-wrapper:hover{
border-color:#23acb2;
}
}
*{
margin: 0;
padding: 0;
}

View File

@ -1,20 +0,0 @@
---
category: Components
title: Terminal 中心对接
toc: content
group:
title: 通用
order: 2
---
中心对接
## 代码演示
<code src="./demo/basic.tsx">基本用法</code>
## API
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| onFinish | 提交事件 | FormProps['onFinish'] | - | - |

View File

@ -1,3 +0,0 @@
import Terminal from './Terminal'
export default Terminal;

View File

@ -0,0 +1,15 @@
// 可应用于页面跳转以及文件下载
// 第一个参数:文件的下载路径/要跳转页面的路径(可携带参数)
// 第二个参数是否新打开一个页面true为新开一个页面false是在当前页面进行操作
export const createAElement = (url: string, isBlank: boolean) => {
var newLink = document.createElement('a');
newLink.className = 'create-link';
newLink.href = url;
if (isBlank) {
newLink.target = '_blank';
}
document.body.appendChild(newLink);
newLink.click();
document.body.removeChild(newLink);
};