2x
This commit is contained in:
@@ -25,6 +25,14 @@ const Help: FC = () => {
|
||||
点击右下角 +
|
||||
号按钮,输入你本地服务监听的端口号,如果需要自定义子域名则输入自定义的子域名,然后点击确定,即可获得可被外网访问的地址
|
||||
</p>
|
||||
<h4>参数说明:</h4>
|
||||
<p>
|
||||
<ul>
|
||||
<li>端口号:需要通过内网穿透公开的本地端口号</li>
|
||||
<li>子域名:自定义的子域名,过于火热的域名通常会被占用</li>
|
||||
<li>自定义HOST:当你需要公开的服务不在localhost上时,你会需要它</li>
|
||||
</ul>
|
||||
</p>
|
||||
<h4>状态说明:</h4>
|
||||
<p>
|
||||
<ul>
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user