mirror of
https://github.com/wikihost-opensource/als.git
synced 2025-12-24 12:57:59 +08:00
first commit
This commit is contained in:
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
**/speedtest-static/*
|
||||
**/ip.mmdb
|
||||
**/.DS_Store
|
||||
**/node_modules
|
||||
**/package-lock.json
|
||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM node:lts-alpine
|
||||
ADD ui /app
|
||||
WORKDIR /app
|
||||
RUN npm i && \
|
||||
npm run build
|
||||
|
||||
FROM alpine:3.16
|
||||
LABEL maintainer="samlm0 <i@teddysun.com>"
|
||||
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
|
||||
apk add --no-cache php81 php81-pecl-maxminddb php81-ctype php81-pecl-swoole nginx xz \
|
||||
iperf iperf3 \
|
||||
mtr \
|
||||
traceroute \
|
||||
iputils
|
||||
|
||||
ADD backend/app /app
|
||||
COPY --from=0 /app/dist /app/webspaces
|
||||
|
||||
CMD php81 /app/app.php
|
||||
42
README.md
Normal file
42
README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# ALS - Another Looking-glass Server
|
||||
|
||||
## Quick start
|
||||
```
|
||||
docker run -d --restart always --network host wikihostinc/looking-glass-server
|
||||
```
|
||||
|
||||
## Host Requirements
|
||||
- Can run docker (yes, only docker is required)
|
||||
|
||||
## Image Environment Variables
|
||||
| Key | Example | Default | Description |
|
||||
| ---- | ---- | ---- | ---- |
|
||||
| HTTP_PORT | 80 | 80 | which HTTP port should use |
|
||||
| SPEEDTEST_FILE_LIST | 100MB 1GB | 1MB 10MB 100MB 1GB | size of static test files, separate with space |
|
||||
| PUBLIC_IPV4 | 1.1.1.1 | (fetch from http://ifconfig.co) | The IPv4 address of the server |
|
||||
| PUBLIC_IPV6 | fe80::1 | (fetch from http://ifconfig.co) | The IPv6 address of the server|
|
||||
| DISPLAY_TRAFFIC | true | true | Toggle the streaming traffic graph |
|
||||
| ENABLE_SPEEDTEST | true | true | Toggle the streaming traffic graph |
|
||||
| UTILITIES_PING | true | true | Toggle the ping feature |
|
||||
| UTILITIES_TRACEROUTE | true | true | Toggle the traceroute feature |
|
||||
| UTILITIES_IPERF3 | true | true | Toggle the iperf3 feature |
|
||||
| UTILITIES_IPERF3_PORT_MIN | 30000 | 30000 | iperf3 listen port range - from |
|
||||
| UTILITIES_IPERF3_PORT_MAX | 31000 | 31000 | iperf3 listen port range - to |
|
||||
|
||||
|
||||
## Features
|
||||
- [x] HTML 5 Speed Test
|
||||
- [x] Traceroute - IPv4
|
||||
- [x] Ping - IPv4
|
||||
- [x] iPerf3 server
|
||||
- [x] Streaming traffic graph
|
||||
|
||||
## TO-DO List
|
||||
- [ ] Traceroute - IPv6
|
||||
- [ ] Ping - IPv6
|
||||
|
||||
## License
|
||||
|
||||
Code is licensed under MIT Public License.
|
||||
|
||||
* If you wish to support my efforts, keep the "Powered by LookingGlass" link intact.
|
||||
14
ui/.eslintrc.cjs
Normal file
14
ui/.eslintrc.cjs
Normal file
@@ -0,0 +1,14 @@
|
||||
/* eslint-env node */
|
||||
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||
|
||||
module.exports = {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"@vue/eslint-config-prettier"
|
||||
],
|
||||
"env": {
|
||||
"vue/setup-compiler-macros": true
|
||||
}
|
||||
}
|
||||
28
ui/.gitignore
vendored
Normal file
28
ui/.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
ui/.vscode/extensions.json
vendored
Normal file
3
ui/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
||||
35
ui/README.md
Normal file
35
ui/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# ui
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
||||
8
ui/build.sh
Normal file
8
ui/build.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
BASEDIR=$(dirname "$0")
|
||||
cd $BASEDIR
|
||||
npm run build
|
||||
rm -rf ../scripts/app/webspaces/assets
|
||||
rm -rf ../scripts/app/webspaces/{index.html,favicon.ico,speedtest_worker.min.js}
|
||||
|
||||
cp -r dist/* ../scripts/app/webspaces/
|
||||
16
ui/index.html
Normal file
16
ui/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Looking glass server</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
6241
ui/package-lock.json
generated
Normal file
6241
ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
ui/package.json
Normal file
30
ui/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "ui",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 4173",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"apexcharts": "^3.35.3",
|
||||
"vue": "^3.2.36",
|
||||
"vue3-apexcharts": "^1.4.1",
|
||||
"xterm": "^4.18.0",
|
||||
"xterm-addon-fit": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.1.0",
|
||||
"@vitejs/plugin-vue": "^2.3.3",
|
||||
"@vitejs/plugin-vue-jsx": "^1.3.10",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"eslint": "^8.5.0",
|
||||
"eslint-plugin-vue": "^8.2.0",
|
||||
"naive-ui": "^2.30.4",
|
||||
"prettier": "^2.5.1",
|
||||
"unplugin-vue-components": "^0.19.6",
|
||||
"vfonts": "^0.0.3",
|
||||
"vite": "^2.9.9"
|
||||
}
|
||||
}
|
||||
BIN
ui/public/favicon.ico
Normal file
BIN
ui/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
1
ui/public/speedtest_worker.min.js
vendored
Normal file
1
ui/public/speedtest_worker.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
96
ui/src/App.vue
Normal file
96
ui/src/App.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
|
||||
|
||||
<template>
|
||||
<n-config-provider :theme="darkTheme">
|
||||
<Loading v-show="!isLoaded"></Loading>
|
||||
<div v-show="isLoaded">
|
||||
<n-space vertical>
|
||||
<h2>Looking Glass Server</h2>
|
||||
<Information v-model:wsMessage="wsMessage" v-model:componentConfig="componentConfig"></Information>
|
||||
<Utilities v-model:componentConfig="componentConfig" v-model:ws="ws" v-model:wsMessage="wsMessage"></Utilities>
|
||||
<Speedtest v-model:componentConfig="componentConfig" v-show="componentConfig.display_speedtest"></Speedtest>
|
||||
<TrafficDisplay v-show="componentConfig.display_traffic" v-model:wsMessage="wsMessage"></TrafficDisplay>
|
||||
</n-space>
|
||||
</div>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
<script>
|
||||
import Loading from './components/Loading.vue'
|
||||
import { defineComponent, defineAsyncComponent, reactive } from 'vue'
|
||||
import { darkTheme } from 'naive-ui'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
Information: defineAsyncComponent(() => import('./components/Information.vue')),
|
||||
TrafficDisplay: defineAsyncComponent(() => import('./components/TrafficDisplay.vue')),
|
||||
Speedtest: defineAsyncComponent(() => import('./components/Speedtest.vue')),
|
||||
Utilities: defineAsyncComponent(() => import('./components/Utilities.vue')),
|
||||
},
|
||||
created() {
|
||||
this.initWebsocket();
|
||||
},
|
||||
methods: {
|
||||
initWebsocket() {
|
||||
this.ws = new WebSocket(location.protocol.replace('http', 'ws') + '//' + location.host + '/ws')
|
||||
this.ws.onopen = () => {
|
||||
this.isLoaded = true
|
||||
}
|
||||
this.ws.onmessage = (message) => {
|
||||
this.wsMessage.push(message.data.split('|'))
|
||||
};
|
||||
this.ws.onclose = () => {
|
||||
this.isLoaded = false
|
||||
setTimeout(() => {
|
||||
this.initWebsocket()
|
||||
}, 1000)
|
||||
}
|
||||
this.ws.onerror = () => {
|
||||
this.isLoaded = false
|
||||
setTimeout(() => {
|
||||
this.initWebsocket()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ws: false,
|
||||
wsMessage: reactive([]),
|
||||
isLoaded: false,
|
||||
componentConfig: reactive({})
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
darkTheme
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
@import './assets/base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* @media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 0fr 0fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.loading-screen {
|
||||
min-width: 500px;
|
||||
}
|
||||
} */
|
||||
</style>
|
||||
49
ui/src/assets/base.css
Normal file
49
ui/src/assets/base.css
Normal file
@@ -0,0 +1,49 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition: color 0.5s, background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
1
ui/src/assets/logo.svg
Normal file
1
ui/src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
After Width: | Height: | Size: 308 B |
54
ui/src/components/Information.vue
Normal file
54
ui/src/components/Information.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-card title="Server Information" hoverable>
|
||||
<n-space vertical>
|
||||
<div v-show="location">
|
||||
服务器地点: {{ location }}
|
||||
</div>
|
||||
<div v-show="publicIpv4">
|
||||
公网 IPv4 地址: <n-tag>{{ publicIpv4 }}</n-tag>
|
||||
</div>
|
||||
<div v-show="publicIpv6">
|
||||
公网 IPv6 地址: <n-tag> [{{ publicIpv6 }}]</n-tag>
|
||||
</div>
|
||||
</n-space>
|
||||
<!-- <n-progress type=" line" :percentage="100" :show-indicator="false" processing /> -->
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
export default defineComponent({
|
||||
props: {
|
||||
wsMessage: Array,
|
||||
componentConfig: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
location: false,
|
||||
publicIpv4: false,
|
||||
publicIpv6: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let DataWatcher = this.$watch(() => this.wsMessage, () => {
|
||||
this.wsMessage.forEach((e, i) => {
|
||||
if (e[0] != 1000) return;
|
||||
let data = JSON.parse(e[1])
|
||||
this.wsMessage.splice(i, 1)
|
||||
this.location = data.location
|
||||
this.publicIpv4 = data.public_ipv4
|
||||
this.publicIpv6 = data.public_ipv6
|
||||
this.componentConfig.public_ipv4 = data.public_ipv4
|
||||
this.componentConfig.public_ipv6 = data.public_ipv6
|
||||
this.componentConfig.testFiles = data.testfiles
|
||||
this.componentConfig.display_traffic = data.display_traffic
|
||||
this.componentConfig.display_speedtest = data.display_speedtest
|
||||
DataWatcher()
|
||||
})
|
||||
}, { immediate: true, deep: true });
|
||||
}
|
||||
})
|
||||
</script>
|
||||
13
ui/src/components/Loading.vue
Normal file
13
ui/src/components/Loading.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="loading-screen">
|
||||
<n-card title="Loading...">
|
||||
<n-progress type="line" :percentage="100" :show-indicator="false" processing />
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export {
|
||||
|
||||
}
|
||||
</script>
|
||||
261
ui/src/components/Speedtest.vue
Normal file
261
ui/src/components/Speedtest.vue
Normal file
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<n-card title="Server Speedtest">
|
||||
<div>
|
||||
<n-card title="HTML 5 在线测速" style="margin-bottom: 10px;">
|
||||
<div v-show="h5Download !== '...'">
|
||||
<n-grid x-gap="12" cols="1 s:1 m:1 l:3" responsive="screen">
|
||||
<n-gi span="1 s:1 m:1 l:2">
|
||||
<div>
|
||||
<h4>下行</h4>
|
||||
<h1>{{ h5Download }} Mbps</h1>
|
||||
<apexchart type="line" :options="h5SpeedtestDownloadSpeedChart.chartOptions"
|
||||
height="200px" :series="h5SpeedtestDownloadSpeedChart.series">
|
||||
</apexchart>
|
||||
</div>
|
||||
</n-gi>
|
||||
<n-gi span="1">
|
||||
<div>
|
||||
<h4>上行</h4>
|
||||
<h1>{{ h5Upload }} Mbps</h1>
|
||||
<apexchart type="line" :options="h5SpeedtestUploadSpeedChart.chartOptions"
|
||||
height="200px" :series="h5SpeedtestUploadSpeedChart.series">
|
||||
</apexchart>
|
||||
</div>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</div>
|
||||
<!-- <h3>下行:
|
||||
<n-number-animation :to="h5Download" />
|
||||
</h3>
|
||||
<h3>上行: {{ h5Upload }}</h3> -->
|
||||
<n-button size="large" @click="startOrStopSpeedtest" style="margin-top: 10px;">
|
||||
<n-spin size="small" v-show="h5SpeedWorker !== null" style="margin-right: 10px;" /> {{
|
||||
h5SpeedtestButtonText
|
||||
}}
|
||||
</n-button>
|
||||
</n-card>
|
||||
</div>
|
||||
<div v-show="componentConfig?.testFiles?.length > 0" style="margin-bottom: 10px;">
|
||||
<n-card title="静态文件测速">
|
||||
<div v-for="i in componentConfig.testFiles">
|
||||
<n-button text tag="a" :href="`speedtest-static/${i}.test`" target="_blank" type="primary">
|
||||
{{ i }}
|
||||
</n-button>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, defineAsyncComponent } from 'vue'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
apexchart: defineAsyncComponent(() => import('vue3-apexcharts')),
|
||||
},
|
||||
props: {
|
||||
componentConfig: Object
|
||||
},
|
||||
methods: {
|
||||
startOrStopSpeedtest(force = true) {
|
||||
if (this.h5SpeedWorker !== null) {
|
||||
this.h5SpeedWorker.postMessage('abort')
|
||||
clearInterval(this.h5SpeedWorkerTimer)
|
||||
this.h5SpeedtestButtonText = '开始测速'
|
||||
this.h5SpeedWorker = null
|
||||
if (force) {
|
||||
this.h5Upload = '...'
|
||||
this.h5Download = '...'
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.h5Upload = '...'
|
||||
this.h5Download = '...'
|
||||
this.h5SpeedtestButtonText = '停止测速'
|
||||
this.h5SpeedWorker = new Worker('speedtest_worker.min.js')
|
||||
// this
|
||||
this.h5SpeedWorker.onmessage = (e) => {
|
||||
var nowPointName = (new Date()).getHours().toString().padStart(2, '0') + ':' +
|
||||
(new Date()).getMinutes().toString().padStart(2, '0') + ':' +
|
||||
(new Date()).getSeconds().toString().padStart(2, '0')
|
||||
|
||||
var data = JSON.parse(e.data);
|
||||
var status = data.testState;
|
||||
if (status >= 4) {
|
||||
return this.startOrStopSpeedtest(false)
|
||||
}
|
||||
|
||||
if (status == 1 && data.dlStatus == 0) {
|
||||
this.h5Download = '...'
|
||||
} else {
|
||||
if (data.dlStatus) {
|
||||
if (data.dlStatus != this.h5Download) {
|
||||
this.h5Download = data.dlStatus
|
||||
let ChartData = this.h5SpeedtestDownloadSpeedChart.series[0].data
|
||||
let categories = this.h5SpeedtestDownloadSpeedChart.chartOptions.xaxis.categories
|
||||
ChartData.push(this.h5Download)
|
||||
categories.push(nowPointName)
|
||||
this.h5SpeedtestDownloadSpeedChart.chartOptions.value = {
|
||||
xaxis: { categories: categories }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (status == 1 && data.ulStatus == 0) {
|
||||
this.h5Upload = '...'
|
||||
} else {
|
||||
if (data.ulStatus) {
|
||||
if (data.ulStatus != this.h5Upload) {
|
||||
this.h5Upload = data.ulStatus
|
||||
let ChartData = this.h5SpeedtestUploadSpeedChart.series[0].data
|
||||
let categories = this.h5SpeedtestUploadSpeedChart.chartOptions.xaxis.categories
|
||||
ChartData.push(this.h5Upload)
|
||||
categories.push(nowPointName)
|
||||
this.h5SpeedtestUploadSpeedChart.chartOptions.value = {
|
||||
xaxis: { categories: categories }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.h5SpeedWorker.postMessage('start ' + JSON.stringify({
|
||||
test_order: "D_U",
|
||||
url_dl: 'speedtest/download',
|
||||
url_ul: 'speedtest/upload',
|
||||
url_ping: 'speedtest/upload',
|
||||
}));
|
||||
this.h5SpeedWorkerTimer = setInterval(() => {
|
||||
this.h5SpeedWorker.postMessage('status')
|
||||
}, 200);
|
||||
// this.h5SpeedtestWorking = !this.h5SpeedtestWorking
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
h5SpeedWorker: null,
|
||||
h5SpeedWorkerTimer: null,
|
||||
h5SpeedtestWorking: false,
|
||||
h5SpeedtestButtonText: '开始测速',
|
||||
h5Upload: '...',
|
||||
h5Download: '...',
|
||||
h5SpeedtestDownloadSpeedChart: {
|
||||
chartOptions: {
|
||||
chart: {
|
||||
id: "speedtest-download-chart",
|
||||
height: 200,
|
||||
foreColor: '#e8e8e8',
|
||||
animations: {
|
||||
enabled: true,
|
||||
easing: 'linear',
|
||||
dynamicAnimation: {
|
||||
speed: 300
|
||||
},
|
||||
},
|
||||
zoom: {
|
||||
enabled: false
|
||||
},
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'dark'
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: 'category',
|
||||
categories: [''],
|
||||
labels: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
formatter: (value) => {
|
||||
return value + ' Mbps'
|
||||
}
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
markers: {
|
||||
size: 0
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth'
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
name: 'Receive',
|
||||
data: []
|
||||
}
|
||||
]
|
||||
},
|
||||
h5SpeedtestUploadSpeedChart: {
|
||||
chartOptions: {
|
||||
chart: {
|
||||
id: "speedtest-upload-chart",
|
||||
height: 200,
|
||||
foreColor: '#e8e8e8',
|
||||
animations: {
|
||||
enabled: true,
|
||||
easing: 'linear',
|
||||
dynamicAnimation: {
|
||||
speed: 300
|
||||
},
|
||||
},
|
||||
zoom: {
|
||||
enabled: false
|
||||
},
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'dark'
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: 'category',
|
||||
categories: [''],
|
||||
labels: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
formatter: (value) => {
|
||||
return value + ' Mbps'
|
||||
}
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
markers: {
|
||||
size: 0
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth'
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
name: 'Receive',
|
||||
data: []
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// setInterval(() => {
|
||||
// console.log(this.componentConfig)
|
||||
// }, 1000)
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
207
ui/src/components/TrafficDisplay.vue
Normal file
207
ui/src/components/TrafficDisplay.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-card title="Server Traffic" hoverable>
|
||||
<n-grid x-gap="12" cols="1 s:1 m:1 l:2 xl:2 2xl:2" responsive="screen">
|
||||
<n-gi v-for="(interfaceData, interfaceName) in interfaces">
|
||||
<n-card :title="interfaceName">
|
||||
<n-grid x-gap="12" :cols="2">
|
||||
<n-gi>
|
||||
<h3>已接收</h3>
|
||||
<span class="traffic-display">
|
||||
{{ formatBytes(interfaceData.traffic.receive, 2, true) }} /
|
||||
{{ formatBytes(interfaceData.receive) }}
|
||||
</span>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<h3>已发送</h3>
|
||||
<span class="traffic-display">
|
||||
{{ formatBytes(interfaceData.traffic.send, 2, true) }} /
|
||||
{{ formatBytes(interfaceData.send) }}
|
||||
</span>
|
||||
</n-gi>
|
||||
<n-gi span="2">
|
||||
<apexchart type="line" :options="interfaceData.chartOptions"
|
||||
:series="interfaceData.series">
|
||||
</apexchart>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, defineAsyncComponent } from 'vue'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
apexchart: defineAsyncComponent(() => import('vue3-apexcharts')),
|
||||
},
|
||||
props: {
|
||||
wsMessage: Array
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
traffic: {
|
||||
receive: null,
|
||||
send: null
|
||||
},
|
||||
categories: [],
|
||||
refreshTimer: null,
|
||||
interfaces: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateSeries() {
|
||||
var nowPointName = (new Date()).getHours().toString().padStart(2, '0') + ':' +
|
||||
(new Date()).getMinutes().toString().padStart(2, '0') + ':' +
|
||||
(new Date()).getSeconds().toString().padStart(2, '0')
|
||||
for (let interfaceName in this.interfaces) {
|
||||
let categories = this.interfaces[interfaceName].chartOptions.xaxis.categories
|
||||
let receiveDatas = this.interfaces[interfaceName].series[0].data
|
||||
let sendDatas = this.interfaces[interfaceName].series[1].data
|
||||
let receive = this.interfaces[interfaceName].receive - this.interfaces[interfaceName].lastReceive
|
||||
let send = this.interfaces[interfaceName].send - this.interfaces[interfaceName].lastSend
|
||||
|
||||
this.interfaces[interfaceName].lastReceive = this.interfaces[interfaceName].receive
|
||||
this.interfaces[interfaceName].lastSend = this.interfaces[interfaceName].send
|
||||
this.interfaces[interfaceName].traffic.receive = receive
|
||||
this.interfaces[interfaceName].traffic.send = send
|
||||
receiveDatas.push(receive)
|
||||
sendDatas.push(send)
|
||||
|
||||
categories.push(nowPointName)
|
||||
this.interfaces[interfaceName].chartOptions.value = {
|
||||
xaxis: { categories: categories }
|
||||
}
|
||||
}
|
||||
},
|
||||
formatBytes(bytes, decimals = 2, bandwidth = false) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const bandwidthSizes = ['Bps', 'Kbps', 'Mbps', 'Gbps', 'Tbps', 'Pbs', 'Ebps', 'Zbps', 'Ybps'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
if (bandwidth) {
|
||||
bytes = bytes * 10
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + bandwidthSizes[i];
|
||||
}
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setInterval(() => {
|
||||
this.updateSeries()
|
||||
}, 1000)
|
||||
this.$watch(() => this.wsMessage, () => {
|
||||
this.wsMessage.forEach((e, i) => {
|
||||
if (e[0] != 100) return false
|
||||
let interfaceName = e[1]
|
||||
let receiveTraffic = e[2]
|
||||
let sendTraffic = e[3]
|
||||
this.wsMessage.splice(i, 1)
|
||||
if (!this.interfaces.hasOwnProperty(interfaceName)) {
|
||||
this.interfaces[interfaceName] = {
|
||||
traffic: {
|
||||
receive: null,
|
||||
send: null
|
||||
},
|
||||
receive: receiveTraffic,
|
||||
send: sendTraffic,
|
||||
lastReceive: receiveTraffic,
|
||||
lastSend: sendTraffic,
|
||||
chartOptions: {
|
||||
chart: {
|
||||
id: "interface-" + interfaceName + "-chart",
|
||||
foreColor: '#e8e8e8',
|
||||
animations: {
|
||||
enabled: true,
|
||||
easing: 'linear',
|
||||
dynamicAnimation: {
|
||||
speed: 1000
|
||||
},
|
||||
},
|
||||
zoom: {
|
||||
enabled: false
|
||||
},
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'dark'
|
||||
},
|
||||
},
|
||||
|
||||
xaxis: {
|
||||
range: 10,
|
||||
type: 'category',
|
||||
categories: [''],
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
formatter: (value) => {
|
||||
return this.formatBytes(value, 2, true)
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
x: {
|
||||
format: 'dd MMM yyyy'
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
markers: {
|
||||
size: 0
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth'
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
name: 'Receive',
|
||||
data: []
|
||||
},
|
||||
{
|
||||
type: 'line',
|
||||
name: 'Send',
|
||||
data: []
|
||||
}
|
||||
]
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.interfaces[interfaceName].receive = receiveTraffic
|
||||
this.interfaces[interfaceName].send = sendTraffic
|
||||
})
|
||||
}, { immediate: true, deep: true });
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.traffic-display {
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.apexcharts-tooltip-title,
|
||||
.apexcharts-tooltip-text {
|
||||
color: #181818;
|
||||
}
|
||||
</style>
|
||||
61
ui/src/components/Utilities.vue
Normal file
61
ui/src/components/Utilities.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<n-card title="Network Utilities">
|
||||
<n-space>
|
||||
<n-button @click="activate('ping')">Ping</n-button>
|
||||
<n-button @click="activate('traceroute')">Traceroute</n-button>
|
||||
<n-button @click="activate('iperf3')">iPerf3</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
<n-drawer v-model:show="componentSwitch.ping" :native-scrollbar="true" :width="drawWidth" placement="right">
|
||||
<n-drawer-content title="Ping" :closable="true">
|
||||
<ping v-model:ws="ws" v-model:wsMessage="wsMessage" />
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
<n-drawer v-model:show="componentSwitch.traceroute" :native-scrollbar="true" :width="drawWidth" placement="right">
|
||||
<n-drawer-content title="Traceroute" :closable="true">
|
||||
<traceroute v-model:ws="ws" v-model:wsMessage="wsMessage" />
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
<n-drawer v-model:show="componentSwitch.iperf3" :native-scrollbar="true" :width="drawWidth" placement="right">
|
||||
<n-drawer-content title="iPerf3" :closable="true">
|
||||
<iperf3 v-model:ws="ws" v-model:wsMessage="wsMessage" v-model:componentConfig="componentConfig" />
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, defineAsyncComponent } from 'vue'
|
||||
export default defineComponent({
|
||||
components: {
|
||||
ping: defineAsyncComponent(() => import('./Utilities/Ping.vue')),
|
||||
traceroute: defineAsyncComponent(() => import('./Utilities/Traceroute.vue')),
|
||||
iperf3: defineAsyncComponent(() => import('./Utilities/iPerf3.vue')),
|
||||
},
|
||||
props: {
|
||||
wsMessage: Array,
|
||||
ws: WebSocket,
|
||||
componentConfig: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
drawWidth: 800,
|
||||
componentSwitch: {
|
||||
ping: false,
|
||||
traceroute: false,
|
||||
iperf3: false
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (window.screen.width < 800) {
|
||||
this.drawWidth = window.screen.width
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
activate(args) {
|
||||
this.host = ''
|
||||
this.componentSwitch[args] = true
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
92
ui/src/components/Utilities/Ping.vue
Normal file
92
ui/src/components/Utilities/Ping.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<n-space vertical>
|
||||
<n-input-group>
|
||||
<n-input :disabled="working" v-model:value="host" placeholder="IP Address Or Domain" @keyup.enter="ping" />
|
||||
<n-button :loading="working" type="primary" ghost @click="ping()">
|
||||
Ping
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<n-table v-show="records.length > 0" :bordered="false" :single-line="false">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Host</th>
|
||||
<th>TTL</th>
|
||||
<th>Latency</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="record in records">
|
||||
<td>{{ record.seq }}</td>
|
||||
<td>{{ record.host }}</td>
|
||||
<td>{{ record.ttl }}</td>
|
||||
<td>{{ record.latency }} ms</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, defineAsyncComponent } from 'vue'
|
||||
export default defineComponent({
|
||||
props: {
|
||||
wsMessage: Array,
|
||||
ws: WebSocket
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
host: '',
|
||||
working: false,
|
||||
records: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
ping() {
|
||||
if (this.working) return false;
|
||||
this.records = []
|
||||
this.working = true
|
||||
this.ws.send('1|' + this.host)
|
||||
let ticket = ''
|
||||
let pingProcess = this.$watch(() => this.wsMessage, (e) => {
|
||||
this.wsMessage.forEach((e, i) => {
|
||||
if (e[0] != 1) return true
|
||||
|
||||
if (ticket.length == 0 && e[2] == this.host && e.length == 4) {
|
||||
ticket = e[3]
|
||||
this.wsMessage.splice(i, 1)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ticket == e[1] && e[2] == '0') {
|
||||
this.working = false
|
||||
pingProcess()
|
||||
this.wsMessage.splice(i, 1)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ticket == e[1] && e[2] == '1') {
|
||||
if (e.length == 7) {
|
||||
this.records.push({
|
||||
host: e[3],
|
||||
seq: e[4],
|
||||
ttl: e[5],
|
||||
latency: e[6]
|
||||
})
|
||||
} else {
|
||||
this.records.push({
|
||||
host: '-',
|
||||
seq: this.records.length + 1,
|
||||
ttl: '-',
|
||||
latency: '-'
|
||||
})
|
||||
}
|
||||
this.wsMessage.splice(i, 1)
|
||||
return true;
|
||||
}
|
||||
})
|
||||
}, { immediate: true, deep: true })
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
138
ui/src/components/Utilities/Traceroute.vue
Normal file
138
ui/src/components/Utilities/Traceroute.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<n-space vertical>
|
||||
<n-input-group>
|
||||
<n-input :disabled="working" v-model:value="host" :style="{ width: '90%' }"
|
||||
placeholder="IP Address Or Domain" @keyup.enter="traceroute" />
|
||||
<n-button :loading="working" type="primary" ghost @click="traceroute()">
|
||||
Traceroute
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<n-table v-show="records.length > 0" :bordered="false" :single-line="false">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hop #</th>
|
||||
<th>Host</th>
|
||||
<th>#1</th>
|
||||
<th>#2</th>
|
||||
<th>#3</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="(record, seq) in records">
|
||||
<tr v-if="record">
|
||||
<td>{{ seq }}</td>
|
||||
<td>
|
||||
<n-space vertical>
|
||||
<n-gradient-text v-show="record.host.length > 1" type="info">
|
||||
! 基于流的负载均衡已发现
|
||||
</n-gradient-text>
|
||||
<template v-for="pop in record.host">
|
||||
<span>{{ pop.dns }} ({{ pop.host }}) | {{ pop.geo }}</span>
|
||||
</template>
|
||||
</n-space>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="record.latency[0]">
|
||||
{{ record.latency[0] }} ms
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
</template>
|
||||
</td>
|
||||
<td> <template v-if="record.latency[1]">
|
||||
{{ record.latency[1] }} ms
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
</template>
|
||||
</td>
|
||||
<td> <template v-if="record.latency[2]">
|
||||
{{ record.latency[2] }} ms
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { method } from 'lodash'
|
||||
import { defineComponent, defineAsyncComponent } from 'vue'
|
||||
export default defineComponent({
|
||||
props: {
|
||||
wsMessage: Array,
|
||||
ws: WebSocket
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
host: '',
|
||||
working: false,
|
||||
records: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
traceroute() {
|
||||
if (this.working) return false;
|
||||
this.records = []
|
||||
this.working = true
|
||||
this.ws.send('2|' + this.host)
|
||||
let ticket = ''
|
||||
let traceProcess = this.$watch(() => this.wsMessage, (e) => {
|
||||
this.wsMessage.forEach((e, i) => {
|
||||
if (e[0] != 2) return true
|
||||
|
||||
if (ticket.length == 0 && e[2] == this.host && e.length == 4) {
|
||||
ticket = e[3]
|
||||
this.wsMessage.splice(i, 1)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ticket == e[1] && e[2] == '0') {
|
||||
this.working = false
|
||||
traceProcess()
|
||||
this.wsMessage.splice(i, 1)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ticket == e[1] && e[2] == '1') {
|
||||
if (this.records[e[3]] === undefined) {
|
||||
this.records[e[3]] = {
|
||||
host: [{
|
||||
dns: e[4] == '0' ? '-' : e[4],
|
||||
host: e[5] == '0' ? '-' : e[5],
|
||||
geo: e[7],
|
||||
}],
|
||||
latency: [e[6]]
|
||||
}
|
||||
} else {
|
||||
this.records[e[3]].host.push({
|
||||
dns: e[4] == '0' ? '-' : e[4],
|
||||
host: e[5] == '0' ? '-' : e[5],
|
||||
geo: e[7],
|
||||
})
|
||||
this.records[e[3]].latency.push(e[6])
|
||||
}
|
||||
this.records[e[3]].host.forEach((v1, k1) => {
|
||||
this.records[e[3]].host.forEach((v2, k2) => {
|
||||
if (k1 != k2 && v1.host == v2.host) {
|
||||
this.records[e[3]].host.splice(k2, 1)
|
||||
}
|
||||
})
|
||||
if (v1.dns == '-' && this.records[e[3]].host.length > 1) {
|
||||
this.records[e[3]].host.splice(k1, 1)
|
||||
}
|
||||
})
|
||||
this.wsMessage.splice(i, 1)
|
||||
return true;
|
||||
}
|
||||
})
|
||||
}, { immediate: true, deep: true })
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
102
ui/src/components/Utilities/iPerf3.vue
Normal file
102
ui/src/components/Utilities/iPerf3.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<n-space vertical>
|
||||
<n-button :block="true" :loading="working" type="primary" ghost @click="startServer()">
|
||||
{{ btnText }}
|
||||
</n-button>
|
||||
<n-progress v-show="timeout != 0" style="transform: rotate(180deg)" type="line"
|
||||
:percentage="100 - TimeoutPercentage" :show-indicator="false" />
|
||||
<n-alert v-show="working && port != 0" type="default" :show-icon="false">
|
||||
iPerf3 command:
|
||||
<p v-show="componentConfig.public_ipv4">
|
||||
IPv4:<br />
|
||||
iperf3 -c {{ componentConfig.public_ipv4 }} -p {{ port }}
|
||||
</p>
|
||||
<p v-show="componentConfig.public_ipv6">
|
||||
IPv6:<br />
|
||||
iperf3 -c {{ componentConfig.public_ipv6 }} -p {{ port }}
|
||||
</p>
|
||||
</n-alert>
|
||||
<n-input autosize style="font-family: monospace;" v-show="log.length > 0" row="30" type="textarea" :value="log"
|
||||
placeholder="iPerf3 Server log" disabled />
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { defineComponent, defineAsyncComponent } from 'vue'
|
||||
export default defineComponent({
|
||||
props: {
|
||||
wsMessage: Array,
|
||||
ws: WebSocket,
|
||||
componentConfig: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
btnText: 'Start iPerf3 Server',
|
||||
working: false,
|
||||
log: '',
|
||||
port: 0,
|
||||
timeout: 0,
|
||||
timePass: 0,
|
||||
TimeoutPercentage: 0,
|
||||
timeoutTimer: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
startServer() {
|
||||
if (this.working) return false;
|
||||
this.btnText = 'iPerf3 Server starting...'
|
||||
this.log = ''
|
||||
this.working = true
|
||||
this.ws.send('4')
|
||||
let ticket = ''
|
||||
let iperfProcess = this.$watch(() => this.wsMessage, (e) => {
|
||||
this.wsMessage.forEach((e, i) => {
|
||||
if (e[0] != 4) return true
|
||||
if (ticket.length == 0 && e[1] == '1' && e.length == 3) {
|
||||
ticket = e[2]
|
||||
this.wsMessage.splice(i, 1)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ticket == e[1] && e[2] == '0') {
|
||||
this.working = false
|
||||
this.port = 0
|
||||
this.timeout = 0
|
||||
clearInterval(this.timeoutTimer)
|
||||
this.timePass = 0
|
||||
iperfProcess()
|
||||
this.btnText = 'Start iPerf3 Server'
|
||||
this.wsMessage.splice(i, 1)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ticket == e[1] && e[2] == '1') {
|
||||
this.port = e[3]
|
||||
this.timeout = e[4]
|
||||
this.timeoutTimer = setInterval(() => {
|
||||
this.btnText = 'iPerf3 Server started (' + (this.timeout - this.timePass) + 's left)'
|
||||
this.timePass++
|
||||
this.TimeoutPercentage = Math.floor((this.timePass / this.timeout) * 100)
|
||||
}, 1000)
|
||||
this.wsMessage.splice(i, 1)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ticket == e[1] && e[2] == '2') {
|
||||
console.log(this.log)
|
||||
if (e[3].length > 0) {
|
||||
this.log = this.log + e[3]
|
||||
}
|
||||
this.wsMessage.splice(i, 1)
|
||||
return false;
|
||||
}
|
||||
})
|
||||
|
||||
}, { immediate: true, deep: true })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
</script>
|
||||
4
ui/src/main.js
Normal file
4
ui/src/main.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
36
ui/vite.config.js
Normal file
36
ui/vite.config.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { fileURLToPath, URL } from 'url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
Components({
|
||||
resolvers: [NaiveUiResolver()]
|
||||
})],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/ws': {
|
||||
target: 'ws://127.0.0.1:80',
|
||||
ws: true
|
||||
},
|
||||
'/speedtest-static': {
|
||||
target: 'http://127.0.0.1:80',
|
||||
},
|
||||
'/speedtest/': {
|
||||
target: 'http://127.0.0.1:80',
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user