This commit is contained in:
lblblong
2021-10-13 15:55:10 +08:00
parent 1da3199db2
commit e503d77cf5
42 changed files with 16720 additions and 13029 deletions
+8
View File
@@ -25,6 +25,14 @@ const Help: FC = () => {
+
访
</p>
<h4></h4>
<p>
<ul>
<li>穿</li>
<li></li>
<li>HOSTlocalhost上时</li>
</ul>
</p>
<h4></h4>
<p>
<ul>
+13 -9
View File
@@ -1,13 +1,13 @@
import { Popups, useController } from 'lbl-popups'
import { Observer, useLocalStore } from 'mobx-react'
import { FC } from 'react'
import { Nat } from 'src/model/nat'
import styles from './index.module.scss'
type Data = Pick<Nat, 'port' | 'subdomain' | 'local_host'>
interface Props {
defaultValue?: {
port: number
subdomain?: string
}
defaultValue?: Data
}
const NatEdit: FC<Props> = (props) => {
@@ -16,6 +16,7 @@ const NatEdit: FC<Props> = (props) => {
return {
port: props.defaultValue?.port || '',
subdomian: props.defaultValue?.subdomain || '',
local_host: props.defaultValue?.local_host || '',
bindOnChange: (key: string) => {
return (e: any) => {
localStore[key] = e.target.value
@@ -40,12 +41,18 @@ const NatEdit: FC<Props> = (props) => {
value={localStore.subdomian}
onChange={localStore.bindOnChange('subdomian')}
/>
<input
placeholder="自定义HOST(可选)"
value={localStore.local_host}
onChange={localStore.bindOnChange('local_host')}
/>
<div
className={styles.btn}
onClick={() => {
ctl.close({
port: localStore.port,
subdomain: localStore.subdomian,
local_host: localStore.local_host,
})
}}
>
@@ -57,13 +64,10 @@ const NatEdit: FC<Props> = (props) => {
)
}
export function openNatEdit(props?: Props): Promise<{
port: number
subdomain?: string
}> {
export function openNatEdit(props?: Props): Promise<Data> {
return Popups.open({
el: NatEdit,
position: 'center',
props
props,
})
}
+3 -9
View File
@@ -5,20 +5,14 @@ import ReactDOM from 'react-dom'
import './assets/css/vs.css'
import './assets/icon/fonts/icon.css'
import { IndexPage } from './pages/manager'
import reportWebVitals from './react/reportWebVitals'
configure({
enforceActions: 'never',
})
ReactDOM.render(
<React.StrictMode>
<>
<Popups />
<IndexPage />
</React.StrictMode>,
document.getElementById('root')
</>,
document.getElementById('root'),
)
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()
+90 -6
View File
@@ -1,5 +1,6 @@
import { EventEmitter } from 'events'
import { makeAutoObservable } from 'mobx'
import { openAlert } from 'src/components/alert'
export enum NatState {
on,
@@ -10,18 +11,101 @@ export enum NatState {
export class Nat {
constructor(it: Partial<Nat> = {}) {
makeAutoObservable(this)
if (it.subdomain === '') {
delete it.subdomain
}
this.id = Date.now()
Object.assign(this, it)
if (!it.port) throw Error('请传入端口号')
this.id = it.id || Date.now()
this.subdomain = it.subdomain || undefined
this.local_host = it.local_host || undefined
this.port = it.port
}
id: number
state = NatState.off
subdomain?: string
local_host?: string
port = 6600
state = NatState.off
tunnel?: EventEmitter & { url: string; close: () => void }
url?: string
logs: string[] = []
async start() {
try {
if (this.state === NatState.loading) return
this.state = NatState.loading
this.log('启动中')
this.tunnel = await window.localtunnel({
port: this.port,
subdomain: this.subdomain,
local_host: this.local_host,
})
if (this.state !== NatState.loading) throw Error('主动关闭')
this.url = this.tunnel.url
this.state = NatState.on
this.log(`启动成功:${this.url}`)
const onRequest = (req: any) => {
this.log(`${req.method} ${req.path}`)
}
const onError = (err: Error) => {
console.log(this.id, err)
this.log(err.message)
}
this.tunnel?.once('close', () => {
this.tunnel?.off('error', onError)
this.tunnel?.off('request', onRequest)
this.reset()
this.log('已关闭')
})
this.tunnel?.on('error', onError)
this.tunnel?.on('request', onRequest)
} catch (err) {
this.state = NatState.off
this.log('启动失败:' + err.message)
openAlert({
title: '启动失败',
content: err.message,
})
throw err
}
}
async stop() {
if (this.state === NatState.off) return
if (this.tunnel) {
return new Promise<void>((ok) => {
this.tunnel?.once('close', () => {
ok()
})
this.tunnel?.close()
})
} else {
this.reset()
}
}
reset() {
this.state = NatState.off
this.tunnel = undefined
this.url = ''
}
log(log: string) {
this.logs.push(log)
if (this.logs.length >= 1000) {
this.logs = this.logs.slice(100)
}
}
toJSON() {
return {
id: this.id,
subdomain: this.subdomain,
local_host: this.local_host,
port: this.port,
}
}
}
+57 -53
View File
@@ -32,36 +32,65 @@
.list {
padding-bottom: 100px;
.item,
.item > .lbox,
.item > .rbox {
display: flex;
align-items: center;
}
.item + .item {
border-top: 1px solid #efefef;
}
.item {
line-height: 1;
line-height: 1.3;
padding: 24px 16px;
color: #999;
&:hover {
background: rgba($color: var(--color-primary), $alpha: 0.09);
background: rgba($color: #5d9cec, $alpha: 0.06);
}
.lbox {
flex: 1;
> * + * {
margin-left: 16px;
$state-width: calc(14px + 16px);
.state {
width: 14px;
height: 14px;
background-color: #dfdfdf;
border-radius: 50%;
}
.top {
display: flex;
align-items: center;
> * {
display: flex;
align-items: center;
}
.state {
width: 14px;
height: 14px;
background-color: #dfdfdf;
border-radius: 50%;
.lbox {
width: $state-width;
}
.name {
font-weight: bold;
.cbox {
flex: 1;
.name {
font-weight: bold;
}
}
.rbox {
margin: -8px;
button {
border: none;
background: none;
padding: 0;
color: var(--color-primary);
margin: 8px;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.del {
color: var(--color-third);
}
button:disabled {
color: #999;
}
}
}
.bottom {
padding-left: $state-width;
padding-top: 16px;
.url {
cursor: pointer;
overflow: hidden;
@@ -72,46 +101,21 @@
}
}
}
}
.rbox {
margin: -8px;
button {
border: none;
background: none;
padding: 0;
color: var(--color-primary);
margin: 8px;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.del {
color: var(--color-third);
}
button:disabled {
color: #999;
}
.loading {
.state {
animation-name: blink;
animation-duration: 0.5s;
animation-iteration-count: infinite;
transition: all 0.5s;
}
}
.active {
color: #333;
.lbox {
.state {
background-color: var(--color-primary);
}
}
}
.loading {
.lbox {
.state {
animation-name: blink;
animation-duration: 0.5s;
animation-iteration-count: infinite;
transition: all 0.5s;
}
.state {
background-color: var(--color-primary);
}
}
}
+66 -45
View File
@@ -1,6 +1,7 @@
import classNames from 'classnames'
import { Observer } from 'mobx-react'
import React from 'react'
import { openAlert } from 'src/components/alert'
import { Icon } from '../../components/icon'
import { NatState } from '../../model/nat'
import { StoreServer } from '../../store/server'
@@ -16,13 +17,13 @@ export const IndexPage = () => {
className={styles.action}
onClick={() => utools.shellOpenExternal('https://github.com/lblblong/nat-utools')}
>
<Icon value="github-line" />
<Icon value='github-line' />
</div>
<div className={styles.action} onClick={() => StoreServer.openHelp()}>
<Icon value="question-mark" />
<Icon value='question-mark' />
</div>
<div className={styles.action} onClick={() => StoreServer.add()}>
<Icon value="add-line" />
<Icon value='add-line' />
</div>
</div>
<div className={styles.list}>
@@ -30,59 +31,79 @@ export const IndexPage = () => {
return (
<div
className={classNames(styles.item, {
[styles.active]: it.state === NatState.on,
[styles.loading]: it.state === NatState.loading,
[styles.active]: it.state === NatState.on,
})}
key={it.id}
>
<div className={styles.lbox}>
<span className={styles.state}></span>
<span className={styles.name}>
{it.port}
{it.subdomain ? ` - ${it.subdomain}` : ''}
</span>
<div className={styles.top}>
<div className={styles.lbox}>
<span className={styles.state}></span>
</div>
<div className={styles.cbox}>
<span className={styles.name}>
{it.local_host || 'localhost'}:{it.port}
{it.subdomain ? `${it.subdomain}` : ''}
</span>
</div>
<div className={styles.rbox}>
<button className={classNames(styles.btn, styles.del)} onClick={() => StoreServer.del(it.id)}>
</button>
<button
disabled={it.state === NatState.loading}
className={classNames(styles.btn)}
onClick={() => StoreServer.edit(it.id)}
>
</button>
<button className={classNames(styles.btn)} onClick={() => StoreServer.showLog(it.id)}>
</button>
<button
disabled={!it.url}
className={classNames(styles.btn)}
onClick={() => {
utools.copyText(it.url!)
utools.showNotification('地址已成功复制到剪切板')
}}
>
</button>
{/* {it.state === NatState.off && (
<button className={styles.btn} onClick={() => StoreServer.start(it.id)}>
启动服务
</button>
)} */}
<button
className={styles.btn}
onClick={() => {
it.state !== NatState.off ? StoreServer.stop(it.id) : StoreServer.start(it.id)
}}
>
{it.state !== NatState.off ? '关闭服务' : '启动服务'}
</button>
</div>
</div>
<div className={styles.bottom}>
<span
className={styles.url}
onClick={() => {
utools.shellOpenExternal(it.url!)
if (it.url) {
utools.shellOpenExternal(it.url)
} else {
openAlert({
title: '提示',
content: '请先启动服务',
})
}
}}
>
{it.url}
{it.url || `-`}
</span>
</div>
<div className={styles.rbox}>
<button className={classNames(styles.btn, styles.del)} onClick={() => StoreServer.del(it.id)}>
</button>
<button
disabled={it.state === NatState.loading}
className={classNames(styles.btn)}
onClick={() => StoreServer.edit(it.id)}
>
</button>
<button className={classNames(styles.btn)} onClick={() => StoreServer.showLog(it.id)}>
</button>
<button
disabled={!it.url}
className={classNames(styles.btn)}
onClick={() => {
utools.copyText(it.url!)
utools.showNotification('地址已成功复制到剪切板')
}}
>
</button>
<button
disabled={it.state === NatState.loading}
className={styles.btn}
onClick={() => StoreServer.toggle(it.id)}
>
{it.state === NatState.off ? '启动服务' : '关闭服务'}
</button>
</div>
</div>
)
})}
-15
View File
@@ -1,15 +0,0 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
-5
View File
@@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
+21 -57
View File
@@ -1,3 +1,4 @@
import { proxy } from 'ajax-hook'
import localtunnel from 'localtunnel'
import { makeAutoObservable } from 'mobx'
import { openAlert } from 'src/components/alert'
@@ -13,6 +14,19 @@ declare global {
}
}
proxy({
onRequest: (config, handler) => {
if (config) config.headers['Bypass-Tunnel-Reminder'] = 'true'
handler.next(config)
},
onError: (err, handler) => {
handler.next(err)
},
onResponse: (response, handler) => {
handler.next(response)
},
})
function getStorageID() {
return utools.getLocalId() + '_servers'
}
@@ -47,37 +61,8 @@ class Store {
async start(id: number) {
const nat = this.getNatById(id)
if (nat.state === NatState.loading) return
nat.state = NatState.loading
nat.logs.push('启动中')
nat.tunnel = await window.localtunnel({
port: nat.port,
subdomain: nat.subdomain,
})
nat.url = nat.tunnel.url
nat.state = NatState.on
nat.logs.push('启动成功')
const onRequest = (req: any) => {
nat.logs.push(`${req.method} ${req.path}`)
}
const onError = (err: Error) => {
console.log(nat.id, err)
nat.logs.push(err.message)
}
nat.tunnel?.once('close', () => {
nat.tunnel?.off('error', onError)
nat.tunnel?.off('request', onRequest)
nat.state = NatState.off
nat.tunnel = undefined
nat.url = ''
nat.logs.push('已关闭')
})
nat.tunnel?.on('error', onError)
nat.tunnel?.on('request', onRequest)
if (nat.subdomain && nat.url.indexOf('//' + nat.subdomain) === -1) {
await nat.start()
if (nat.subdomain && nat.url!.indexOf('//' + nat.subdomain) === -1) {
openAlert({
title: '提示',
content: '子域名被占用,已为您分配其他子域名',
@@ -87,14 +72,7 @@ class Store {
async stop(id: number) {
const nat = this.getNatById(id)
if (nat.state === NatState.off) return
return new Promise<void>((ok) => {
nat.tunnel?.once('close', () => {
ok()
})
nat.tunnel?.close()
})
await nat.stop()
}
async del(id: number) {
@@ -111,13 +89,11 @@ class Store {
await this.stop(id)
}
const data = await openNatEdit({
defaultValue: {
port: nat.port,
subdomain: nat.subdomain,
},
defaultValue: nat.toJSON(),
})
nat.port = data.port
nat.subdomain = data.subdomain
nat.local_host = data.local_host
this.flushDb()
if (originState === NatState.on) {
this.start(id)
@@ -151,13 +127,7 @@ class Store {
}[]
>(getStorageID())
if (!it || !it.data) return
this.natList = it.data.map((d) => {
return new Nat({
id: d.id,
subdomain: d.subdomain,
port: d.port,
})
})
this.natList = it.data.map((d) => new Nat(d))
} catch (err) {
alert(err)
}
@@ -172,13 +142,7 @@ class Store {
}[]
>(getStorageID())
const data = this.natList.map((d) => {
return {
id: d.id,
subdomain: d.subdomain,
port: d.port,
}
})
const data = this.natList.map((d) => d.toJSON())
if (!it) {
it = {