Ver código fonte

web/frps: upgrade vue and element-plus (#3310)

fatedier 2 anos atrás
pai
commit
24f0b3afa5
48 arquivos alterados com 3624 adições e 6623 exclusões
  1. BIN
      assets/frps/static/535877f50039c0cb49a6196a5b7517cd.woff
  2. BIN
      assets/frps/static/732389ded34cb9c52dd88271f1345af9.ttf
  3. 0 0
      assets/frps/static/index-4ce77078.css
  4. 0 0
      assets/frps/static/index-cd02d3b4.js
  5. 16 1
      assets/frps/static/index.html
  6. 0 1
      assets/frps/static/manifest.js
  7. 0 0
      assets/frps/static/vendor.js
  8. 0 14
      web/frps/.babelrc
  9. 30 0
      web/frps/.eslintrc.cjs
  10. 26 4
      web/frps/.gitignore
  11. 5 0
      web/frps/.prettierrc.json
  12. 8 2
      web/frps/Makefile
  13. 46 0
      web/frps/README.md
  14. 5 0
      web/frps/auto-imports.d.ts
  15. 35 0
      web/frps/components.d.ts
  16. 1 0
      web/frps/env.d.ts
  17. 14 0
      web/frps/index.html
  18. 27 37
      web/frps/package.json
  19. 0 5
      web/frps/postcss.config.js
  20. 0 0
      web/frps/public/favicon.ico
  21. 77 73
      web/frps/src/App.vue
  22. 22 0
      web/frps/src/assets/custom.css
  23. 0 169
      web/frps/src/components/Overview.vue
  24. 35 142
      web/frps/src/components/ProxiesHttp.vue
  25. 35 137
      web/frps/src/components/ProxiesHttps.vue
  26. 20 110
      web/frps/src/components/ProxiesStcp.vue
  27. 20 110
      web/frps/src/components/ProxiesSudp.vue
  28. 20 118
      web/frps/src/components/ProxiesTcp.vue
  29. 21 120
      web/frps/src/components/ProxiesUdp.vue
  30. 91 0
      web/frps/src/components/ProxyView.vue
  31. 70 0
      web/frps/src/components/ProxyViewExpand.vue
  32. 177 0
      web/frps/src/components/ServerOverview.vue
  33. 27 31
      web/frps/src/components/Traffic.vue
  34. 0 15
      web/frps/src/index.html
  35. 0 48
      web/frps/src/main.js
  36. 12 0
      web/frps/src/main.ts
  37. 0 43
      web/frps/src/router/index.js
  38. 51 0
      web/frps/src/router/index.ts
  39. 0 186
      web/frps/src/utils/chart.js
  40. 293 0
      web/frps/src/utils/chart.ts
  41. 0 22
      web/frps/src/utils/less/custom.less
  42. 0 104
      web/frps/src/utils/proxy.js
  43. 138 0
      web/frps/src/utils/proxy.ts
  44. 8 0
      web/frps/tsconfig.config.json
  45. 16 0
      web/frps/tsconfig.json
  46. 29 0
      web/frps/vite.config.ts
  47. 0 107
      web/frps/webpack.config.js
  48. 2249 5024
      web/frps/yarn.lock

BIN
assets/frps/static/535877f50039c0cb49a6196a5b7517cd.woff


BIN
assets/frps/static/732389ded34cb9c52dd88271f1345af9.ttf


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
assets/frps/static/index-4ce77078.css


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
assets/frps/static/index-cd02d3b4.js


+ 16 - 1
assets/frps/static/index.html

@@ -1 +1,16 @@
-<!doctype html> <html lang=en> <head> <meta charset=utf-8> <title>frps dashboard</title> <link rel="shortcut icon" href="favicon.ico"></head> <body> <div id=app></div> <script type="text/javascript" src="manifest.js?5d154ba4c6b342d8c0c3"></script><script type="text/javascript" src="vendor.js?ddbd1f69fb6e67be4b78"></script></body> </html> 
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="utf-8">
+    <title>frps dashboard</title>
+  <script type="module" crossorigin src="./index-cd02d3b4.js"></script>
+  <link rel="stylesheet" href="./index-4ce77078.css">
+</head>
+
+<body>
+    <div id="app"></div>
+    
+</body>
+
+</html>

+ 0 - 1
assets/frps/static/manifest.js

@@ -1 +0,0 @@
-!function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,u,c){for(var i,a,f,l=0,s=[];l<t.length;l++)a=t[l],o[a]&&s.push(o[a][0]),o[a]=0;for(i in u)Object.prototype.hasOwnProperty.call(u,i)&&(e[i]=u[i]);for(r&&r(t,u,c);s.length;)s.shift()();if(c)for(l=0;l<c.length;l++)f=n(n.s=c[l]);return f};var t={},o={1:0};n.e=function(e){function r(){i.onerror=i.onload=null,clearTimeout(a);var n=o[e];0!==n&&(n&&n[1](new Error("Loading chunk "+e+" failed.")),o[e]=void 0)}var t=o[e];if(0===t)return new Promise(function(e){e()});if(t)return t[2];var u=new Promise(function(n,r){t=o[e]=[n,r]});t[2]=u;var c=document.getElementsByTagName("head")[0],i=document.createElement("script");i.type="text/javascript",i.charset="utf-8",i.async=!0,i.timeout=12e4,n.nc&&i.setAttribute("nonce",n.nc),i.src=n.p+""+e+".js?"+{0:"ddbd1f69fb6e67be4b78"}[e];var a=setTimeout(r,12e4);return i.onerror=i.onload=r,c.appendChild(i),u},n.m=e,n.c=t,n.i=function(e){return e},n.d=function(e,r,t){n.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:t})},n.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(r,"a",r),r},n.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},n.p="",n.oe=function(e){throw console.error(e),e}}([]);

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
assets/frps/static/vendor.js


+ 0 - 14
web/frps/.babelrc

@@ -1,14 +0,0 @@
-{
-    "presets": [
-        ["es2015", { "modules": false }]
-    ],
-    "plugins": [
-        [
-            "component",
-            {
-                "libraryName": "element-ui",
-                "styleLibraryName": "theme-chalk"
-            }
-        ]
-    ]
-}

+ 30 - 0
web/frps/.eslintrc.cjs

@@ -0,0 +1,30 @@
+/* eslint-env node */
+require('@rushstack/eslint-patch/modern-module-resolution')
+
+module.exports = {
+  root: true,
+  extends: [
+    'plugin:vue/vue3-essential',
+    'eslint:recommended',
+    '@vue/eslint-config-typescript',
+    '@vue/eslint-config-prettier',
+  ],
+  parserOptions: {
+    ecmaVersion: 'latest',
+  },
+  rules: {
+    '@typescript-eslint/no-unused-vars': [
+      'warn',
+      {
+        argsIgnorePattern: '^_',
+        varsIgnorePattern: '^_',
+      },
+    ],
+    'vue/multi-word-component-names': [
+      'error',
+      {
+        ignores: ['Traffic'],
+      },
+    ],
+  },
+}

+ 26 - 4
web/frps/.gitignore

@@ -1,6 +1,28 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
 .DS_Store
-node_modules/
-dist/
-npm-debug.log
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
 .idea
-.vscode/settings.json
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 5 - 0
web/frps/.prettierrc.json

@@ -0,0 +1,5 @@
+{
+  "tabWidth": 2,
+  "semi": false,
+  "singleQuote": true
+}

+ 8 - 2
web/frps/Makefile

@@ -1,7 +1,13 @@
-.PHONY: dist build
+.PHONY: dist build preview lint
 
 build:
 	@npm run build
 
-dev: install
+dev:
 	@npm run dev
+
+preview:
+	@npm run preview
+
+lint:
+	@npm run lint

+ 46 - 0
web/frps/README.md

@@ -0,0 +1,46 @@
+# frps-dashboard
+
+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).
+
+## Type Support for `.vue` Imports in TS
+
+TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
+
+If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
+
+1. Disable the built-in TypeScript Extension
+    1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
+    2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
+2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
+
+## 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
+```
+
+### Type-Check, Compile and Minify for Production
+
+```sh
+npm run build
+```
+
+### Lint with [ESLint](https://eslint.org/)
+
+```sh
+npm run lint
+```

+ 5 - 0
web/frps/auto-imports.d.ts

@@ -0,0 +1,5 @@
+// Generated by 'unplugin-auto-import'
+export {}
+declare global {
+
+}

+ 35 - 0
web/frps/components.d.ts

@@ -0,0 +1,35 @@
+// generated by unplugin-vue-components
+// We suggest you to commit this file into source control
+// Read more: https://github.com/vuejs/core/pull/3399
+import '@vue/runtime-core'
+
+export {}
+
+declare module '@vue/runtime-core' {
+  export interface GlobalComponents {
+    ElButton: typeof import('element-plus/es')['ElButton']
+    ElCol: typeof import('element-plus/es')['ElCol']
+    ElForm: typeof import('element-plus/es')['ElForm']
+    ElFormItem: typeof import('element-plus/es')['ElFormItem']
+    ElMenu: typeof import('element-plus/es')['ElMenu']
+    ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
+    ElPopover: typeof import('element-plus/es')['ElPopover']
+    ElRow: typeof import('element-plus/es')['ElRow']
+    ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
+    ElTable: typeof import('element-plus/es')['ElTable']
+    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+    ElTag: typeof import('element-plus/es')['ElTag']
+    ProxiesHTTP: typeof import('./src/components/ProxiesHTTP.vue')['default']
+    ProxiesHTTPS: typeof import('./src/components/ProxiesHTTPS.vue')['default']
+    ProxiesSTCP: typeof import('./src/components/ProxiesSTCP.vue')['default']
+    ProxiesSUDP: typeof import('./src/components/ProxiesSUDP.vue')['default']
+    ProxiesTCP: typeof import('./src/components/ProxiesTCP.vue')['default']
+    ProxiesUDP: typeof import('./src/components/ProxiesUDP.vue')['default']
+    ProxyView: typeof import('./src/components/ProxyView.vue')['default']
+    ProxyViewExpand: typeof import('./src/components/ProxyViewExpand.vue')['default']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+    ServerOverview: typeof import('./src/components/ServerOverview.vue')['default']
+    Traffic: typeof import('./src/components/Traffic.vue')['default']
+  }
+}

+ 1 - 0
web/frps/env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 14 - 0
web/frps/index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="utf-8">
+    <title>frps dashboard</title>
+</head>
+
+<body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+</body>
+
+</html>

+ 27 - 37
web/frps/package.json

@@ -1,48 +1,38 @@
 {
   "name": "frps-dashboard",
-  "description": "A dashboard for frp server.",
-  "author": "fatedier",
+  "version": "0.0.1",
   "private": true,
   "scripts": {
-    "dev": "webpack-dev-server -d --inline --hot --env.dev",
-    "build": "rimraf dist && webpack -p --progress --hide-modules"
+    "dev": "vite",
+    "build": "run-p type-check build-only",
+    "preview": "vite preview",
+    "build-only": "vite build",
+    "type-check": "vue-tsc --noEmit",
+    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
   },
   "dependencies": {
-    "bootstrap": "^3.3.7",
-    "echarts": "^3.5.0",
-    "element-ui": "^2.3.8",
+    "@types/humanize-plus": "^1.8.0",
+    "echarts": "^5.4.1",
+    "element-plus": "^2.2.28",
     "humanize-plus": "^1.8.2",
-    "vue": "^2.5.16",
-    "vue-resource": "^1.2.1",
-    "vue-router": "^2.3.0",
-    "whatwg-fetch": "^2.0.3"
-  },
-  "engines": {
-    "node": ">=6"
+    "vue": "^3.2.45",
+    "vue-router": "^4.1.6"
   },
   "devDependencies": {
-    "autoprefixer": "^6.6.0",
-    "babel-core": "^6.21.0",
-    "babel-eslint": "^7.1.1",
-    "babel-loader": "^6.4.0",
-    "babel-plugin-component": "^1.1.1",
-    "babel-preset-es2015": "^6.13.2",
-    "css-loader": "^0.27.0",
-    "eslint": "^3.12.2",
-    "eslint-config-enough": "^0.2.2",
-    "eslint-loader": "^1.6.3",
-    "file-loader": "^0.10.1",
-    "html-loader": "^0.4.5",
-    "html-webpack-plugin": "^2.24.1",
-    "less": "^3.0.4",
-    "less-loader": "^4.1.0",
-    "postcss-loader": "^1.3.3",
-    "rimraf": "^2.5.4",
-    "style-loader": "^0.13.2",
-    "url-loader": "^1.0.1",
-    "vue-loader": "^15.0.10",
-    "vue-template-compiler": "^2.1.8",
-    "webpack": "^2.2.0-rc.4",
-    "webpack-dev-server": "^3.1.4"
+    "@rushstack/eslint-patch": "^1.1.4",
+    "@types/node": "^18.11.12",
+    "@vitejs/plugin-vue": "^4.0.0",
+    "@vue/eslint-config-prettier": "^7.0.0",
+    "@vue/eslint-config-typescript": "^11.0.0",
+    "@vue/tsconfig": "^0.1.3",
+    "eslint": "^8.22.0",
+    "eslint-plugin-vue": "^9.3.0",
+    "npm-run-all": "^4.1.5",
+    "prettier": "^2.7.1",
+    "typescript": "~4.7.4",
+    "unplugin-auto-import": "^0.13.0",
+    "unplugin-vue-components": "^0.23.0",
+    "vite": "^4.0.4",
+    "vue-tsc": "^1.0.12"
   }
 }

+ 0 - 5
web/frps/postcss.config.js

@@ -1,5 +0,0 @@
-module.exports = {
-    plugins: [
-        require('autoprefixer')()
-    ]
-}

+ 0 - 0
web/frps/src/assets/favicon.ico → web/frps/public/favicon.ico


+ 77 - 73
web/frps/src/App.vue

@@ -1,81 +1,85 @@
 <template>
-    <div id="app">
-        <header class="grid-content header-color">
-            <el-row>
-                <a class="brand" href="#">frp</a>
-            </el-row>
-        </header>
-        <section>
-            <el-row>
-                <el-col id="side-nav" :xs="24" :md="4">
-                    <el-menu default-active="1" mode="vertical" theme="light" router="false" @select="handleSelect">
-                        <el-menu-item index="/">Overview</el-menu-item>
-                        <el-submenu index="/proxies">
-                            <template slot="title">Proxies</template>
-                            <el-menu-item index="/proxies/tcp">TCP</el-menu-item>
-                            <el-menu-item index="/proxies/udp">UDP</el-menu-item>
-                            <el-menu-item index="/proxies/http">HTTP</el-menu-item>
-                            <el-menu-item index="/proxies/https">HTTPS</el-menu-item>
-                            <el-menu-item index="/proxies/stcp">STCP</el-menu-item>
-                            <el-menu-item index="/proxies/sudp">SUDP</el-menu-item>
-                        </el-submenu>
-                        <el-menu-item index="">Help</el-menu-item>
-                    </el-menu>
-				</el-col>
+  <div id="app">
+    <header class="grid-content header-color">
+      <el-row>
+        <a class="brand" href="#">frp</a>
+      </el-row>
+    </header>
+    <section>
+      <el-row>
+        <el-col id="side-nav" :xs="24" :md="4">
+          <el-menu
+            default-active="/"
+            mode="vertical"
+            theme="light"
+            router="false"
+            @select="handleSelect"
+          >
+            <el-menu-item index="/">Overview</el-menu-item>
+            <el-sub-menu index="/proxies">
+              <template #title>
+                <span>Proxies</span>
+              </template>
+              <el-menu-item index="/proxies/tcp">TCP</el-menu-item>
+              <el-menu-item index="/proxies/udp">UDP</el-menu-item>
+              <el-menu-item index="/proxies/http">HTTP</el-menu-item>
+              <el-menu-item index="/proxies/https">HTTPS</el-menu-item>
+              <el-menu-item index="/proxies/stcp">STCP</el-menu-item>
+              <el-menu-item index="/proxies/sudp">SUDP</el-menu-item>
+            </el-sub-menu>
+            <el-menu-item index="">Help</el-menu-item>
+          </el-menu>
+        </el-col>
 
-				<el-col :xs="24" :md="20">
-                    <div id="content">
-                    <router-view></router-view>
-                    </div>
-				</el-col>
-		</el-row>
-	</section>
-	<footer></footer>
-</div>
+        <el-col :xs="24" :md="20">
+          <div id="content">
+            <router-view></router-view>
+          </div>
+        </el-col>
+      </el-row>
+    </section>
+    <footer></footer>
+  </div>
 </template>
 
-<script>
-    export default {
-        methods: {
-            handleSelect(key, path) {
-                if (key == '') {
-                    window.open("https://github.com/fatedier/frp")
-                }
-            }
-        }
-    }
+<script setup lang="ts">
+const handleSelect = (key: string) => {
+  if (key == '') {
+    window.open('https://github.com/fatedier/frp')
+  }
+}
 </script>
 
 <style>
-    body {
-        background-color: #fafafa;
-        margin: 0px;
-        font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,sans-serif;
-    }
-    
-    header {
-        width: 100%;
-        height: 60px;
-    }
-    
-    .header-color {
-        background: #58B7FF;
-    }
-    
-    #content {
-        margin-top: 20px;
-        padding-right: 40px;
-    }
-    
-    .brand {
-        color: #fff;
-        background-color: transparent;
-        margin-left: 20px;
-        float: left;
-        line-height: 25px;
-        font-size: 25px;
-        padding: 15px 15px;
-        height: 30px;
-        text-decoration: none;
-    }
+body {
+  background-color: #fafafa;
+  margin: 0px;
+  font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, sans-serif;
+}
+
+header {
+  width: 100%;
+  height: 60px;
+}
+
+.header-color {
+  background: #58b7ff;
+}
+
+#content {
+  margin-top: 20px;
+  padding-right: 40px;
+}
+
+.brand {
+  color: #fff;
+  background-color: transparent;
+  margin-left: 20px;
+  float: left;
+  line-height: 25px;
+  font-size: 25px;
+  padding: 15px 15px;
+  height: 30px;
+  text-decoration: none;
+}
 </style>

+ 22 - 0
web/frps/src/assets/custom.css

@@ -0,0 +1,22 @@
+.el-form-item span {
+  margin-left: 15px;
+}
+
+.proxy-table-expand {
+  font-size: 0;
+}
+
+.proxy-table-expand .el-form-item__label{
+  width: 90px;
+  color: #99a9bf;
+}
+
+.proxy-table-expand .el-form-item {
+  margin-right: 0;
+  margin-bottom: 0;
+  width: 50%;
+}
+
+.el-table .el-table__expanded-cell {
+  padding: 20px 50px;
+}

+ 0 - 169
web/frps/src/components/Overview.vue

@@ -1,169 +0,0 @@
-<template>
-    <div>
-        <el-row>
-            <el-col :md="12">
-                <div class="source">
-                    <el-form label-position="left" class="server_info">
-                        <el-form-item label="Version">
-                          <span>{{ version }}</span>
-                        </el-form-item>
-                        <el-form-item label="BindPort">
-                          <span>{{ bind_port }}</span>
-                        </el-form-item>
-                        <el-form-item label="BindUdpPort">
-                          <span>{{ bind_udp_port }}</span>
-                        </el-form-item>
-                        <el-form-item label="Http Port">
-                          <span>{{ vhost_http_port }}</span>
-                        </el-form-item>
-                        <el-form-item label="Https Port">
-                          <span>{{ vhost_https_port }}</span>
-                        </el-form-item>
-                        <el-form-item label="Subdomain Host">
-                          <span>{{ subdomain_host }}</span>
-                        </el-form-item>
-                        <el-form-item label="Max PoolCount">
-                          <span>{{ max_pool_count }}</span>
-                        </el-form-item>
-                        <el-form-item label="Max Ports Per Client">
-                          <span>{{ max_ports_per_client }}</span>
-                        </el-form-item>
-                        <el-form-item label="HeartBeat Timeout">
-                          <span>{{ heart_beat_timeout }}</span>
-                        </el-form-item>
-                        <el-form-item label="Client Counts">
-                          <span>{{ client_counts }}</span>
-                        </el-form-item>
-                        <el-form-item label="Current Connections">
-                          <span>{{ cur_conns }}</span>
-                        </el-form-item>
-                        <el-form-item label="Proxy Counts">
-                          <span>{{ proxy_counts }}</span>
-                        </el-form-item>
-                    </el-form>
-                </div>
-            </el-col>
-            <el-col :md="12">
-                <div id="traffic" style="width: 400px;height:250px;margin-bottom: 30px;"></div>
-                <div id="proxies" style="width: 400px;height:250px;"></div>
-            </el-col>
-        </el-row>
-    </div>
-</template>
-
-<script>
-    import {DrawTrafficChart, DrawProxyChart} from '../utils/chart.js'
-    export default {
-        data() {
-            return {
-                version: '',
-                bind_port: '',
-                bind_udp_port: '',
-                vhost_http_port: '',
-                vhost_https_port: '',
-                subdomain_host: '',
-                max_pool_count: '',
-                max_ports_per_client: '',
-                heart_beat_timeout: '',
-                client_counts: '',
-                cur_conns: '',
-                proxy_counts: ''
-            }
-        },
-        created() {
-            this.fetchData()
-        },
-        watch: {
-            '$route': 'fetchData'
-        },
-        methods: {
-            fetchData() {
-                fetch('../api/serverinfo', {credentials: 'include'})
-              .then(res => {
-                return res.json()
-              }).then(json => {
-                this.version = json.version
-                this.bind_port = json.bind_port
-                this.bind_udp_port = json.bind_udp_port
-                if (this.bind_udp_port == 0) {
-                    this.bind_udp_port = "disable"
-                }
-                this.vhost_http_port = json.vhost_http_port
-                if (this.vhost_http_port == 0) {
-                    this.vhost_http_port = "disable"
-                }
-                this.vhost_https_port = json.vhost_https_port
-                if (this.vhost_https_port == 0) {
-                    this.vhost_https_port = "disable"
-                }
-                this.subdomain_host = json.subdomain_host
-                this.max_pool_count = json.max_pool_count
-                this.max_ports_per_client = json.max_ports_per_client
-                if (this.max_ports_per_client == 0) {
-                    this.max_ports_per_client = "no limit"
-                }
-                this.heart_beat_timeout = json.heart_beat_timeout
-                this.client_counts = json.client_counts
-                this.cur_conns = json.cur_conns
-                this.proxy_counts = 0
-                if (json.proxy_type_count != null) {
-                    if (json.proxy_type_count.tcp != null) {
-                        this.proxy_counts += json.proxy_type_count.tcp
-                    }
-                    if (json.proxy_type_count.udp != null) {
-                        this.proxy_counts += json.proxy_type_count.udp
-                    }
-                    if (json.proxy_type_count.http != null) {
-                        this.proxy_counts += json.proxy_type_count.http
-                    }
-                    if (json.proxy_type_count.https != null) {
-                        this.proxy_counts += json.proxy_type_count.https
-                    }
-                    if (json.proxy_type_count.stcp != null) {
-                        this.proxy_counts += json.proxy_type_count.stcp
-                    }
-                    if (json.proxy_type_count.sudp != null) {
-                        this.proxy_counts += json.proxy_type_count.sudp
-                    }
-                    if (json.proxy_type_count.xtcp != null) {
-                        this.proxy_counts += json.proxy_type_count.xtcp
-                    }
-                }
-                DrawTrafficChart('traffic', json.total_traffic_in, json.total_traffic_out)
-                DrawProxyChart('proxies', json)
-              }).catch( err => {
-                  this.$message({
-                      showClose: true,
-                      message: 'Get server info from frps failed!',
-                      type: 'warning'
-                    })
-              })
-            }
-        }
-    }
-</script>
-
-<style>
-.source {
-    border: 1px solid #eaeefb;
-    border-radius: 4px;
-    transition: .2s;
-    padding: 24px;
-}
-
-.server_info {
-    margin-left: 40px;
-    font-size: 0px;
-}
-
-.server_info label {
-    width: 150px;
-    color: #99a9bf;
-}
-
-.server_info .el-form-item {
-    margin-right: 0;
-    margin-bottom: 0;
-    width: 100%;
-}
-</style>

+ 35 - 142
web/frps/src/components/ProxiesHttp.vue

@@ -1,148 +1,41 @@
 <template>
-  <div>
-    <el-table :data="proxies" :default-sort="{prop: 'name', order: 'ascending'}" style="width: 100%">
-      <el-table-column type="expand">
-        <template slot-scope="props">
-          <el-popover
-            ref="popover4"
-            placement="right"
-            width="600"
-  		  style="margin-left:0px"
-            trigger="click">
-            <my-traffic-chart :proxy_name="props.row.name"></my-traffic-chart>
-          </el-popover>
-  
-          <el-button v-popover:popover4 type="primary" size="small" icon="view" style="margin-bottom:10px">Traffic Statistics</el-button>
-  
-          <el-form label-position="left" inline class="demo-table-expand">
-            <el-form-item label="Name">
-              <span>{{ props.row.name }}</span>
-            </el-form-item>
-            <el-form-item label="Type">
-              <span>{{ props.row.type }}</span>
-            </el-form-item>
-            <el-form-item label="Domains">
-              <span>{{ props.row.custom_domains }}</span>
-            </el-form-item>
-            <el-form-item label="SubDomain">
-              <span>{{ props.row.subdomain }}</span>
-            </el-form-item>
-            <el-form-item label="locations">
-              <span>{{ props.row.locations }}</span>
-            </el-form-item>
-            <el-form-item label="HostRewrite">
-              <span>{{ props.row.host_header_rewrite }}</span>
-            </el-form-item>
-            <el-form-item label="Encryption">
-              <span>{{ props.row.encryption }}</span>
-            </el-form-item>
-            <el-form-item label="Compression">
-              <span>{{ props.row.compression }}</span>
-            </el-form-item>
-            <el-form-item label="Last Start">
-              <span>{{ props.row.last_start_time }}</span>
-            </el-form-item>
-            <el-form-item label="Last Close">
-              <span>{{ props.row.last_close_time }}</span>
-            </el-form-item>
-        </el-form>
-    </template>
-    </el-table-column>
-    <el-table-column
-      label="Name"
-      prop="name"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Port"
-      prop="port"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Connections"
-      prop="conns"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Traffic In"
-      prop="traffic_in"
-      :formatter="formatTrafficIn"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Traffic Out"
-      prop="traffic_out"
-      :formatter="formatTrafficOut"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="status"
-      prop="status"
-      sortable>
-      <template slot-scope="scope">
-        <el-tag type="success" v-if="scope.row.status === 'online'">{{ scope.row.status }}</el-tag>
-        <el-tag type="danger" v-else>{{ scope.row.status }}</el-tag>
-      </template>
-    </el-table-column>
-</el-table>
-</div>
+  <ProxyView :proxies="proxies" proxyType="http" />
 </template>
 
-<script>
-  import Humanize from 'humanize-plus';
-  import Traffic from './Traffic.vue'
-  import {
-    HttpProxy
-  } from '../utils/proxy.js'
-  export default {
-    data() {
-      return {
-        proxies: new Array(),
-        vhost_http_port: "",
-        subdomain_host: ""
-      }
-    },
-    created() {
-      this.fetchData()
-    },
-    watch: {
-      '$route': 'fetchData'
-    },
-    methods: {
-      formatTrafficIn(row, column) {
-        return Humanize.fileSize(row.traffic_in)
-      },
-      formatTrafficOut(row, column) {
-        return Humanize.fileSize(row.traffic_out)
-      },
-      fetchData() {
-        fetch('../api/serverinfo', {credentials: 'include'})
-          .then(res => {
-            return res.json()
-          }).then(json => {
-            this.vhost_http_port = json.vhost_http_port
-            this.subdomain_host = json.subdomain_host
-            if (this.vhost_http_port == null || this.vhost_http_port == 0) {
-              return
-            } else {
-              fetch('../api/proxy/http', {credentials: 'include'})
-                .then(res => {
-                  return res.json()
-                }).then(json => {
-                  this.proxies = new Array()
-                  for (let proxyStats of json.proxies) {
-                    this.proxies.push(new HttpProxy(proxyStats, this.vhost_http_port, this.subdomain_host))
-                  }
-                })
-            }
-          })
+<script setup lang="ts">
+import { ref } from 'vue'
+import { HTTPProxy } from '../utils/proxy.js'
+import ProxyView from './ProxyView.vue'
+
+let proxies = ref<HTTPProxy[]>([])
+
+const fetchData = () => {
+  let vhost_http_port: number
+  let subdomain_host: string
+  fetch('../api/serverinfo', { credentials: 'include' })
+    .then((res) => {
+      return res.json()
+    })
+    .then((json) => {
+      vhost_http_port = json.vhost_http_port
+      subdomain_host = json.subdomain_host
+      if (vhost_http_port == null || vhost_http_port == 0) {
+        return
       }
-    },
-    components: {
-        'my-traffic-chart': Traffic
-    }
-  }
+      fetch('../api/proxy/http', { credentials: 'include' })
+        .then((res) => {
+          return res.json()
+        })
+        .then((json) => {
+          for (let proxyStats of json.proxies) {
+            proxies.value.push(
+              new HTTPProxy(proxyStats, vhost_http_port, subdomain_host)
+            )
+          }
+        })
+    })
+}
+fetchData()
 </script>
 
-<style>
-</style>
+<style></style>

+ 35 - 137
web/frps/src/components/ProxiesHttps.vue

@@ -1,143 +1,41 @@
 <template>
-  <div>
-    <el-table :data="proxies" :default-sort="{prop: 'name', order: 'ascending'}" style="width: 100%">
-      <el-table-column type="expand">
-        <template slot-scope="props">
-          <el-popover
-            ref="popover4"
-            placement="right"
-            width="600"
-  		  style="margin-left:0px"
-            trigger="click">
-            <my-traffic-chart :proxy_name="props.row.name"></my-traffic-chart>
-          </el-popover>
-  
-          <el-button v-popover:popover4 type="primary" size="small" icon="view" style="margin-bottom:10px">Traffic Statistics</el-button>
-  
-          <el-form label-position="left" inline class="demo-table-expand">
-            <el-form-item label="Name">
-              <span>{{ props.row.name }}</span>
-            </el-form-item>
-            <el-form-item label="Type">
-              <span>{{ props.row.type }}</span>
-            </el-form-item>
-            <el-form-item label="Domains">
-              <span>{{ props.row.custom_domains }}</span>
-            </el-form-item>
-            <el-form-item label="SubDomain">
-              <span>{{ props.row.subdomain }}</span>
-            </el-form-item>
-            <el-form-item label="Encryption">
-              <span>{{ props.row.encryption }}</span>
-            </el-form-item>
-            <el-form-item label="Compression">
-              <span>{{ props.row.compression }}</span>
-            </el-form-item>
-            <el-form-item label="Last Start">
-              <span>{{ props.row.last_start_time }}</span>
-            </el-form-item>
-            <el-form-item label="Last Close">
-              <span>{{ props.row.last_close_time }}</span>
-            </el-form-item>
-        </el-form>
-    </template>
-    </el-table-column>
-    <el-table-column
-      label="Name"
-      prop="name"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Port"
-      prop="port"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Connections"
-      prop="conns"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Traffic In"
-      prop="traffic_in"
-      :formatter="formatTrafficIn"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Traffic Out"
-      prop="traffic_out"
-      :formatter="formatTrafficOut"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="status"
-      prop="status"
-      sortable>
-      <template slot-scope="scope">
-        <el-tag type="success" v-if="scope.row.status === 'online'">{{ scope.row.status }}</el-tag>
-        <el-tag type="danger" v-else>{{ scope.row.status }}</el-tag>
-      </template>
-    </el-table-column>
-</el-table>
-
-</div>
+  <ProxyView :proxies="proxies" proxyType="https" />
 </template>
 
-<script>
-  import Humanize from 'humanize-plus';
-  import Traffic from './Traffic.vue'
-  import {
-    HttpsProxy
-  } from '../utils/proxy.js'
-  export default {
-    data() {
-      return {
-        proxies: new Array(),
-        vhost_https_port: '',
-        subdomain_host: ''
-      }
-    },
-    created() {
-      this.fetchData()
-    },
-    watch: {
-      '$route': 'fetchData'
-    },
-    methods: {
-      formatTrafficIn(row, column) {
-        return Humanize.fileSize(row.traffic_in)
-      },
-      formatTrafficOut(row, column) {
-        return Humanize.fileSize(row.traffic_out)
-      },
-      fetchData() {
-        fetch('../api/serverinfo', {credentials: 'include'})
-          .then(res => {
-            return res.json()
-          }).then(json => {
-            this.vhost_https_port = json.vhost_https_port
-            this.subdomain_host = json.subdomain_host
-            if (this.vhost_https_port == null || this.vhost_https_port == 0) {
-              return
-            } else {
-              fetch('../api/proxy/https', {credentials: 'include'})
-                .then(res => {
-                  return res.json()
-                }).then(json => {
-                  this.proxies = new Array()
-                  for (let proxyStats of json.proxies) {
-                    this.proxies.push(new HttpsProxy(proxyStats, this.vhost_https_port, this.subdomain_host))
-                  }
-                })
-            }
-          })
+<script setup lang="ts">
+import { ref } from 'vue'
+import { HTTPSProxy } from '../utils/proxy.js'
+import ProxyView from './ProxyView.vue'
+
+let proxies = ref<HTTPSProxy[]>([])
+
+const fetchData = () => {
+  let vhost_https_port: number
+  let subdomain_host: string
+  fetch('../api/serverinfo', { credentials: 'include' })
+    .then((res) => {
+      return res.json()
+    })
+    .then((json) => {
+      vhost_https_port = json.vhost_https_port
+      subdomain_host = json.subdomain_host
+      if (vhost_https_port == null || vhost_https_port == 0) {
+        return
       }
-    },
-    components: {
-        'my-traffic-chart': Traffic
-    }
-  }
+      fetch('../api/proxy/https', { credentials: 'include' })
+        .then((res) => {
+          return res.json()
+        })
+        .then((json) => {
+          for (let proxyStats of json.proxies) {
+            proxies.value.push(
+              new HTTPSProxy(proxyStats, vhost_https_port, subdomain_host)
+            )
+          }
+        })
+    })
+}
+fetchData()
 </script>
 
-<style>
-</style>
+<style></style>

+ 20 - 110
web/frps/src/components/ProxiesStcp.vue

@@ -1,116 +1,26 @@
 <template>
-  <div>
-    <el-table :data="proxies" :default-sort="{prop: 'name', order: 'ascending'}" style="width: 100%">
-      <el-table-column type="expand">
-        <template slot-scope="props">
-          <el-popover
-            ref="popover4"
-            placement="right"
-            width="600"
-  		  style="margin-left:0px"
-            trigger="click">
-            <my-traffic-chart :proxy_name="props.row.name"></my-traffic-chart>
-          </el-popover>
-  
-          <el-button v-popover:popover4 type="primary" size="small" icon="view" :name="props.row.name" style="margin-bottom:10px" @click="fetchData2">Traffic Statistics</el-button>
-  
-          <el-form label-position="left" inline class="demo-table-expand">
-            <el-form-item label="Name">
-              <span>{{ props.row.name }}</span>
-            </el-form-item>
-            <el-form-item label="Type">
-              <span>{{ props.row.type }}</span>
-            </el-form-item>
-            <el-form-item label="Encryption">
-              <span>{{ props.row.encryption }}</span>
-            </el-form-item>
-            <el-form-item label="Compression">
-              <span>{{ props.row.compression }}</span>
-            </el-form-item>
-            <el-form-item label="Last Start">
-              <span>{{ props.row.last_start_time }}</span>
-            </el-form-item>
-            <el-form-item label="Last Close">
-              <span>{{ props.row.last_close_time }}</span>
-            </el-form-item>
-        </el-form>
-        </template>
-    </el-table-column>
-    <el-table-column
-      label="Name"
-      prop="name"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Connections"
-      prop="conns"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Traffic In"
-      prop="traffic_in"
-      :formatter="formatTrafficIn"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Traffic Out"
-      prop="traffic_out"
-      :formatter="formatTrafficOut"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="status"
-      prop="status"
-      sortable>
-      <template slot-scope="scope">
-        <el-tag type="success" v-if="scope.row.status === 'online'">{{ scope.row.status }}</el-tag>
-        <el-tag type="danger" v-else>{{ scope.row.status }}</el-tag>
-      </template>
-    </el-table-column>
-  </el-table>
-</div>
+  <ProxyView :proxies="proxies" proxyType="stcp" />
 </template>
 
-<script>
-  import Humanize from 'humanize-plus'
-  import Traffic from './Traffic.vue'
-  import { StcpProxy } from '../utils/proxy.js'
-  export default {
-    data() {
-      return {
-        proxies: new Array(),
-      }
-    },
-    created() {
-      this.fetchData()
-    },
-    watch: {
-      '$route': 'fetchData'
-    },
-    methods: {
-      formatTrafficIn(row, column) {
-        return Humanize.fileSize(row.traffic_in)
-      },
-      formatTrafficOut(row, column) {
-        return Humanize.fileSize(row.traffic_out)
-      },
-      fetchData() {
-        fetch('../api/proxy/stcp', {credentials: 'include'})
-          .then(res => {
-            return res.json()
-          }).then(json => {
-            this.proxies = new Array()
-            for (let proxyStats of json.proxies) {
-              this.proxies.push(new StcpProxy(proxyStats))
-            }
-          })
+<script setup lang="ts">
+import { ref } from 'vue'
+import { STCPProxy } from '../utils/proxy.js'
+import ProxyView from './ProxyView.vue'
+
+let proxies = ref<STCPProxy[]>([])
+
+const fetchData = () => {
+  fetch('../api/proxy/stcp', { credentials: 'include' })
+    .then((res) => {
+      return res.json()
+    })
+    .then((json) => {
+      for (let proxyStats of json.proxies) {
+        proxies.value.push(new STCPProxy(proxyStats))
       }
-    },
-    components: {
-        'my-traffic-chart': Traffic
-    }
-  }
+    })
+}
+fetchData()
 </script>
 
-<style>
-</style>
+<style></style>

+ 20 - 110
web/frps/src/components/ProxiesSudp.vue

@@ -1,116 +1,26 @@
 <template>
-  <div>
-    <el-table :data="proxies" :default-sort="{prop: 'name', order: 'ascending'}" style="width: 100%">
-      <el-table-column type="expand">
-        <template slot-scope="props">
-          <el-popover
-            ref="popover4"
-            placement="right"
-            width="600"
-  		  style="margin-left:0px"
-            trigger="click">
-            <my-traffic-chart :proxy_name="props.row.name"></my-traffic-chart>
-          </el-popover>
-  
-          <el-button v-popover:popover4 type="primary" size="small" icon="view" :name="props.row.name" style="margin-bottom:10px" @click="fetchData2">Traffic Statistics</el-button>
-  
-          <el-form label-position="left" inline class="demo-table-expand">
-            <el-form-item label="Name">
-              <span>{{ props.row.name }}</span>
-            </el-form-item>
-            <el-form-item label="Type">
-              <span>{{ props.row.type }}</span>
-            </el-form-item>
-            <el-form-item label="Encryption">
-              <span>{{ props.row.encryption }}</span>
-            </el-form-item>
-            <el-form-item label="Compression">
-              <span>{{ props.row.compression }}</span>
-            </el-form-item>
-            <el-form-item label="Last Start">
-              <span>{{ props.row.last_start_time }}</span>
-            </el-form-item>
-            <el-form-item label="Last Close">
-              <span>{{ props.row.last_close_time }}</span>
-            </el-form-item>
-        </el-form>
-        </template>
-    </el-table-column>
-    <el-table-column
-      label="Name"
-      prop="name"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Connections"
-      prop="conns"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Traffic In"
-      prop="traffic_in"
-      :formatter="formatTrafficIn"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Traffic Out"
-      prop="traffic_out"
-      :formatter="formatTrafficOut"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="status"
-      prop="status"
-      sortable>
-      <template slot-scope="scope">
-        <el-tag type="success" v-if="scope.row.status === 'online'">{{ scope.row.status }}</el-tag>
-        <el-tag type="danger" v-else>{{ scope.row.status }}</el-tag>
-      </template>
-    </el-table-column>
-  </el-table>
-</div>
+  <ProxyView :proxies="proxies" proxyType="sudp" />
 </template>
 
-<script>
-  import Humanize from 'humanize-plus'
-  import Traffic from './Traffic.vue'
-  import { SudpProxy } from '../utils/proxy.js'
-  export default {
-    data() {
-      return {
-        proxies: new Array(),
-      }
-    },
-    created() {
-      this.fetchData()
-    },
-    watch: {
-      '$route': 'fetchData'
-    },
-    methods: {
-      formatTrafficIn(row, column) {
-        return Humanize.fileSize(row.traffic_in)
-      },
-      formatTrafficOut(row, column) {
-        return Humanize.fileSize(row.traffic_out)
-      },
-      fetchData() {
-        fetch('../api/proxy/sudp', {credentials: 'include'})
-          .then(res => {
-            return res.json()
-          }).then(json => {
-            this.proxies = new Array()
-            for (let proxyStats of json.proxies) {
-              this.proxies.push(new SudpProxy(proxyStats))
-            }
-          })
+<script setup lang="ts">
+import { ref } from 'vue'
+import { SUDPProxy } from '../utils/proxy.js'
+import ProxyView from './ProxyView.vue'
+
+let proxies = ref<SUDPProxy[]>([])
+
+const fetchData = () => {
+  fetch('../api/proxy/sudp', { credentials: 'include' })
+    .then((res) => {
+      return res.json()
+    })
+    .then((json) => {
+      for (let proxyStats of json.proxies) {
+        proxies.value.push(new SUDPProxy(proxyStats))
       }
-    },
-    components: {
-        'my-traffic-chart': Traffic
-    }
-  }
+    })
+}
+fetchData()
 </script>
 
-<style>
-</style>
+<style></style>

+ 20 - 118
web/frps/src/components/ProxiesTcp.vue

@@ -1,124 +1,26 @@
 <template>
-  <div>
-    <el-table :data="proxies" :default-sort="{prop: 'name', order: 'ascending'}" style="width: 100%">
-      <el-table-column type="expand">
-        <template slot-scope="props">
-          <el-popover
-            ref="popover4"
-            placement="right"
-            width="600"
-  		  style="margin-left:0px"
-            trigger="click">
-            <my-traffic-chart :proxy_name="props.row.name"></my-traffic-chart>
-          </el-popover>
-  
-          <el-button v-popover:popover4 type="primary" size="small" icon="view" :name="props.row.name" style="margin-bottom:10px" @click="fetchData2">Traffic Statistics</el-button>
-  
-          <el-form label-position="left" inline class="demo-table-expand">
-            <el-form-item label="Name">
-              <span>{{ props.row.name }}</span>
-            </el-form-item>
-            <el-form-item label="Type">
-              <span>{{ props.row.type }}</span>
-            </el-form-item>
-            <el-form-item label="Addr">
-              <span>{{ props.row.addr }}</span>
-            </el-form-item>
-            <el-form-item label="Encryption">
-              <span>{{ props.row.encryption }}</span>
-            </el-form-item>
-            <el-form-item label="Compression">
-              <span>{{ props.row.compression }}</span>
-            </el-form-item>
-            <el-form-item label="Last Start">
-              <span>{{ props.row.last_start_time }}</span>
-            </el-form-item>
-            <el-form-item label="Last Close">
-              <span>{{ props.row.last_close_time }}</span>
-            </el-form-item>
-        </el-form>
-        </template>
-    </el-table-column>
-    <el-table-column
-      label="Name"
-      prop="name"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Port"
-      prop="port"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Connections"
-      prop="conns"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Traffic In"
-      prop="traffic_in"
-      :formatter="formatTrafficIn"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Traffic Out"
-      prop="traffic_out"
-      :formatter="formatTrafficOut"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="status"
-      prop="status"
-      sortable>
-      <template slot-scope="scope">
-        <el-tag type="success" v-if="scope.row.status === 'online'">{{ scope.row.status }}</el-tag>
-        <el-tag type="danger" v-else>{{ scope.row.status }}</el-tag>
-      </template>
-    </el-table-column>
-  </el-table>
-</div>
+  <ProxyView :proxies="proxies" proxyType="tcp" />
 </template>
 
-<script>
-  import Humanize from 'humanize-plus'
-  import Traffic from './Traffic.vue'
-  import { TcpProxy } from '../utils/proxy.js'
-  export default {
-    data() {
-      return {
-        proxies: new Array(),
-      }
-    },
-    created() {
-      this.fetchData()
-    },
-    watch: {
-      '$route': 'fetchData'
-    },
-    methods: {
-      formatTrafficIn(row, column) {
-        return Humanize.fileSize(row.traffic_in)
-      },
-      formatTrafficOut(row, column) {
-        return Humanize.fileSize(row.traffic_out)
-      },
-      fetchData() {
-        fetch('../api/proxy/tcp', {credentials: 'include'})
-          .then(res => {
-            return res.json()
-          }).then(json => {
-            this.proxies = new Array()
-            for (let proxyStats of json.proxies) {
-              this.proxies.push(new TcpProxy(proxyStats))
-            }
-          })
+<script setup lang="ts">
+import { ref } from 'vue'
+import { TCPProxy } from '../utils/proxy.js'
+import ProxyView from './ProxyView.vue'
+
+let proxies = ref<TCPProxy[]>([])
+
+const fetchData = () => {
+  fetch('../api/proxy/tcp', { credentials: 'include' })
+    .then((res) => {
+      return res.json()
+    })
+    .then((json) => {
+      for (let proxyStats of json.proxies) {
+        proxies.value.push(new TCPProxy(proxyStats))
       }
-    },
-    components: {
-        'my-traffic-chart': Traffic
-    }
-  }
+    })
+}
+fetchData()
 </script>
 
-<style>
-</style>
+<style></style>

+ 21 - 120
web/frps/src/components/ProxiesUdp.vue

@@ -1,126 +1,27 @@
 <template>
-  <div>
-    <el-table :data="proxies" :default-sort="{prop: 'name', order: 'ascending'}" style="width: 100%">
-      <el-table-column type="expand">
-        <template slot-scope="props">
-          <el-popover
-            ref="popover4"
-            placement="right"
-            width="600"
-  		  style="margin-left:0px"
-            trigger="click">
-            <my-traffic-chart :proxy_name="props.row.name"></my-traffic-chart>
-          </el-popover>
-  
-          <el-button v-popover:popover4 type="primary" size="small" icon="view" style="margin-bottom:10px">Traffic Statistics</el-button>
-  
-          <el-form label-position="left" inline class="demo-table-expand">
-            <el-form-item label="Name">
-              <span>{{ props.row.name }}</span>
-            </el-form-item>
-            <el-form-item label="Type">
-              <span>{{ props.row.type }}</span>
-            </el-form-item>
-            <el-form-item label="Addr">
-              <span>{{ props.row.addr }}</span>
-            </el-form-item>
-            <el-form-item label="Encryption">
-              <span>{{ props.row.encryption }}</span>
-            </el-form-item>
-            <el-form-item label="Compression">
-              <span>{{ props.row.compression }}</span>
-            </el-form-item>
-            <el-form-item label="Last Start">
-              <span>{{ props.row.last_start_time }}</span>
-            </el-form-item>
-            <el-form-item label="Last Close">
-              <span>{{ props.row.last_close_time }}</span>
-            </el-form-item>
-        </el-form>
-    </template>
-    </el-table-column>
-    <el-table-column
-      label="Name"
-      prop="name"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Port"
-      prop="port"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Connections"
-      prop="conns"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Traffic In"
-      prop="traffic_in"
-      :formatter="formatTrafficIn"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="Traffic Out"
-      prop="traffic_out"
-      :formatter="formatTrafficOut"
-      sortable>
-    </el-table-column>
-    <el-table-column
-      label="status"
-      prop="status"
-      sortable>
-      <template slot-scope="scope">
-        <el-tag type="success" v-if="scope.row.status === 'online'">{{ scope.row.status }}</el-tag>
-        <el-tag type="danger" v-else>{{ scope.row.status }}</el-tag>
-      </template>
-    </el-table-column>
-  </el-table>
-</div>
+  <ProxyView :proxies="proxies" proxyType="udp" />
 </template>
 
-<script>
-  import Humanize from 'humanize-plus';
-  import Traffic from './Traffic.vue'
-  import {
-    UdpProxy
-  } from '../utils/proxy.js'
-  export default {
-    data() {
-      return {
-        proxies: new Array(),
-      }
-    },
-    created() {
-      this.fetchData()
-    },
-    watch: {
-      '$route': 'fetchData'
-    },
-    methods: {
-      formatTrafficIn(row, column) {
-        return Humanize.fileSize(row.traffic_in)
-      },
-      formatTrafficOut(row, column) {
-        return Humanize.fileSize(row.traffic_out)
-      },
-      fetchData() {
-        fetch('../api/proxy/udp', {credentials: 'include'})
-          .then(res => {
-            return res.json()
-          }).then(json => {
-            this.proxies = new Array()
-            for (let proxyStats of json.proxies) {
-              this.proxies.push(new UdpProxy(proxyStats))
-            }
-          })
+<script setup lang="ts">
+import { ref } from 'vue'
+import { UDPProxy } from '../utils/proxy.js'
+import ProxyView from './ProxyView.vue'
+
+let proxies = ref<UDPProxy[]>([])
+
+const fetchData = () => {
+  fetch('../api/proxy/udp', { credentials: 'include' })
+    .then((res) => {
+      return res.json()
+    })
+    .then((json) => {
+      for (let proxyStats of json.proxies) {
+        proxies.value.push(new UDPProxy(proxyStats))
       }
-    },
-    components: {
-        'my-traffic-chart': Traffic
-    }
-  }
+    })
+}
+
+fetchData()
 </script>
 
-<style>
-</style>
+<style></style>

+ 91 - 0
web/frps/src/components/ProxyView.vue

@@ -0,0 +1,91 @@
+<template>
+  <div>
+    <el-table
+      :data="proxies"
+      :default-sort="{ prop: 'name', order: 'ascending' }"
+      style="width: 100%"
+    >
+      <el-table-column type="expand">
+        <template #default="props">
+          <el-popover
+            ref="popoverTraffic"
+            :virtual-ref="buttonTraffic"
+            placement="right"
+            width="600"
+            style="margin-left: 0px"
+            trigger="click"
+            virtual-triggering
+          >
+            <Traffic :proxy_name="props.row.name" />
+          </el-popover>
+
+          <el-button
+            ref="buttonTraffic"
+            type="primary"
+            size="large"
+            :name="props.row.name"
+            style="margin-bottom: 10px"
+            v-click-outside="onClickTrafficStats"
+            >Traffic Statistics
+          </el-button>
+
+          <ProxyViewExpand :row="props.row" :proxyType="proxyType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="Name" prop="name" sortable> </el-table-column>
+      <el-table-column label="Port" prop="port" sortable> </el-table-column>
+      <el-table-column label="Connections" prop="conns" sortable>
+      </el-table-column>
+      <el-table-column
+        label="Traffic In"
+        prop="traffic_in"
+        :formatter="formatTrafficIn"
+        sortable
+      >
+      </el-table-column>
+      <el-table-column
+        label="Traffic Out"
+        prop="traffic_out"
+        :formatter="formatTrafficOut"
+        sortable
+      >
+      </el-table-column>
+      <el-table-column label="status" prop="status" sortable>
+        <template #default="scope">
+          <el-tag v-if="scope.row.status === 'online'" type="success">{{
+            scope.row.status
+          }}</el-tag>
+          <el-tag v-else type="danger">{{ scope.row.status }}</el-tag>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, unref } from 'vue'
+import * as Humanize from 'humanize-plus'
+import type { TableColumnCtx } from 'element-plus'
+import type { BaseProxy } from '../utils/proxy.js'
+import ProxyViewExpand from './ProxyViewExpand.vue'
+
+defineProps<{
+  proxies: BaseProxy[]
+  proxyType: string
+}>()
+
+const formatTrafficIn = (row: BaseProxy, _: TableColumnCtx<BaseProxy>) => {
+  return Humanize.fileSize(row.traffic_in)
+}
+
+const formatTrafficOut = (row: BaseProxy, _: TableColumnCtx<BaseProxy>) => {
+  return Humanize.fileSize(row.traffic_out)
+}
+
+const buttonTraffic = ref()
+const popoverTraffic = ref()
+
+const onClickTrafficStats = () => {
+  unref(popoverTraffic).popoverTraffic?.delayHide?.()
+}
+</script>

+ 70 - 0
web/frps/src/components/ProxyViewExpand.vue

@@ -0,0 +1,70 @@
+<template>
+  <el-form
+    label-position="left"
+    inline
+    class="proxy-table-expand"
+    v-if="proxyType === 'http' || proxyType === 'https'"
+  >
+    <el-form-item label="Name">
+      <span>{{ row.name }}</span>
+    </el-form-item>
+    <el-form-item label="Type">
+      <span>{{ row.type }}</span>
+    </el-form-item>
+    <el-form-item label="Domains">
+      <span>{{ row.custom_domains }}</span>
+    </el-form-item>
+    <el-form-item label="SubDomain">
+      <span>{{ row.subdomain }}</span>
+    </el-form-item>
+    <el-form-item label="locations">
+      <span>{{ row.locations }}</span>
+    </el-form-item>
+    <el-form-item label="HostRewrite">
+      <span>{{ row.host_header_rewrite }}</span>
+    </el-form-item>
+    <el-form-item label="Encryption">
+      <span>{{ row.encryption }}</span>
+    </el-form-item>
+    <el-form-item label="Compression">
+      <span>{{ row.compression }}</span>
+    </el-form-item>
+    <el-form-item label="Last Start">
+      <span>{{ row.last_start_time }}</span>
+    </el-form-item>
+    <el-form-item label="Last Close">
+      <span>{{ row.last_close_time }}</span>
+    </el-form-item>
+  </el-form>
+
+  <el-form label-position="left" inline class="proxy-table-expand" v-else>
+    <el-form-item label="Name">
+      <span>{{ row.name }}</span>
+    </el-form-item>
+    <el-form-item label="Type">
+      <span>{{ row.type }}</span>
+    </el-form-item>
+    <el-form-item label="Addr">
+      <span>{{ row.addr }}</span>
+    </el-form-item>
+    <el-form-item label="Encryption">
+      <span>{{ row.encryption }}</span>
+    </el-form-item>
+    <el-form-item label="Compression">
+      <span>{{ row.compression }}</span>
+    </el-form-item>
+    <el-form-item label="Last Start">
+      <span>{{ row.last_start_time }}</span>
+    </el-form-item>
+    <el-form-item label="Last Close">
+      <span>{{ row.last_close_time }}</span>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+defineProps<{
+  row: any
+  proxyType: string
+}>()
+</script>

+ 177 - 0
web/frps/src/components/ServerOverview.vue

@@ -0,0 +1,177 @@
+<template>
+  <div>
+    <el-row>
+      <el-col :md="12">
+        <div class="source">
+          <el-form
+            label-position="left"
+            label-width="160px"
+            class="server_info"
+          >
+            <el-form-item label="Version">
+              <span>{{ data.version }}</span>
+            </el-form-item>
+            <el-form-item label="BindPort">
+              <span>{{ data.bind_port }}</span>
+            </el-form-item>
+            <el-form-item label="BindUdpPort">
+              <span>{{ data.bind_udp_port }}</span>
+            </el-form-item>
+            <el-form-item label="Http Port">
+              <span>{{ data.vhost_http_port }}</span>
+            </el-form-item>
+            <el-form-item label="Https Port">
+              <span>{{ data.vhost_https_port }}</span>
+            </el-form-item>
+            <el-form-item label="Subdomain Host">
+              <span>{{ data.subdomain_host }}</span>
+            </el-form-item>
+            <el-form-item label="Max PoolCount">
+              <span>{{ data.max_pool_count }}</span>
+            </el-form-item>
+            <el-form-item label="Max Ports Per Client">
+              <span>{{ data.max_ports_per_client }}</span>
+            </el-form-item>
+            <el-form-item label="HeartBeat Timeout">
+              <span>{{ data.heart_beat_timeout }}</span>
+            </el-form-item>
+            <el-form-item label="Client Counts">
+              <span>{{ data.client_counts }}</span>
+            </el-form-item>
+            <el-form-item label="Current Connections">
+              <span>{{ data.cur_conns }}</span>
+            </el-form-item>
+            <el-form-item label="Proxy Counts">
+              <span>{{ data.proxy_counts }}</span>
+            </el-form-item>
+          </el-form>
+        </div>
+      </el-col>
+      <el-col :md="12">
+        <div
+          id="traffic"
+          style="width: 400px; height: 250px; margin-bottom: 30px"
+        ></div>
+        <div id="proxies" style="width: 400px; height: 250px"></div>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import { DrawTrafficChart, DrawProxyChart } from '../utils/chart'
+
+let data = ref({
+  version: '',
+  bind_port: '',
+  bind_udp_port: '',
+  vhost_http_port: '',
+  vhost_https_port: '',
+  subdomain_host: '',
+  max_pool_count: '',
+  max_ports_per_client: '',
+  heart_beat_timeout: '',
+  client_counts: '',
+  cur_conns: '',
+  proxy_counts: 0,
+})
+
+const fetchData = () => {
+  fetch('../api/serverinfo', { credentials: 'include' })
+    .then((res) => res.json())
+    .then((json) => {
+      data.value.version = json.version
+      data.value.bind_port = json.bind_port
+      data.value.bind_udp_port = json.bind_udp_port
+      if (data.value.bind_udp_port == '0') {
+        data.value.bind_udp_port = 'disable'
+      }
+      data.value.vhost_http_port = json.vhost_http_port
+      if (data.value.vhost_http_port == '0') {
+        data.value.vhost_http_port = 'disable'
+      }
+      data.value.vhost_https_port = json.vhost_https_port
+      if (data.value.vhost_https_port == '0') {
+        data.value.vhost_https_port = 'disable'
+      }
+      data.value.subdomain_host = json.subdomain_host
+      data.value.max_pool_count = json.max_pool_count
+      data.value.max_ports_per_client = json.max_ports_per_client
+      if (data.value.max_ports_per_client == '0') {
+        data.value.max_ports_per_client = 'no limit'
+      }
+      data.value.heart_beat_timeout = json.heart_beat_timeout
+      data.value.client_counts = json.client_counts
+      data.value.cur_conns = json.cur_conns
+      data.value.proxy_counts = 0
+      if (json.proxy_type_count != null) {
+        if (json.proxy_type_count.tcp != null) {
+          data.value.proxy_counts += json.proxy_type_count.tcp
+        }
+        if (json.proxy_type_count.udp != null) {
+          data.value.proxy_counts += json.proxy_type_count.udp
+        }
+        if (json.proxy_type_count.http != null) {
+          data.value.proxy_counts += json.proxy_type_count.http
+        }
+        if (json.proxy_type_count.https != null) {
+          data.value.proxy_counts += json.proxy_type_count.https
+        }
+        if (json.proxy_type_count.stcp != null) {
+          data.value.proxy_counts += json.proxy_type_count.stcp
+        }
+        if (json.proxy_type_count.sudp != null) {
+          data.value.proxy_counts += json.proxy_type_count.sudp
+        }
+        if (json.proxy_type_count.xtcp != null) {
+          data.value.proxy_counts += json.proxy_type_count.xtcp
+        }
+      }
+
+      // draw chart
+      DrawTrafficChart('traffic', json.total_traffic_in, json.total_traffic_out)
+      DrawProxyChart('proxies', json)
+    })
+    .catch(() => {
+      ElMessage({
+        showClose: true,
+        message: 'Get server info from frps failed!',
+        type: 'warning',
+      })
+    })
+}
+fetchData()
+</script>
+
+<style>
+.source {
+  border: 1px solid #eaeefb;
+  border-radius: 4px;
+  transition: 0.2s;
+  padding: 24px;
+}
+
+.server_info {
+  margin-left: 40px;
+  font-size: 0px;
+}
+
+.server_info .el-form-item__label {
+  color: #99a9bf;
+  height: 40px;
+  line-height: 40px;
+}
+
+.server_info .el-form-item__content {
+  height: 40px;
+  line-height: 40px;
+}
+
+.server_info .el-form-item {
+  margin-right: 0;
+  margin-bottom: 0;
+  width: 100%;
+}
+</style>

+ 27 - 31
web/frps/src/components/Traffic.vue

@@ -1,36 +1,32 @@
 <template>
-    <div :id="proxy_name" style="width: 600px;height:400px;"></div>
+  <div :id="proxy_name" style="width: 600px; height: 400px"></div>
 </template>
 
-<script>
-import {DrawProxyTrafficChart} from '../utils/chart.js'
-export default {
-    props: ['proxy_name'],
-    created() {
-        this.fetchData()
-    },
-    //watch: {
-        //'$route': 'fetchData'
-    //},
-    methods: {
-        fetchData() {
-            let url = '../api/traffic/' + this.proxy_name
-            fetch(url, {credentials: 'include'})
-              .then(res => {
-                return res.json()
-              }).then(json => {
-                DrawProxyTrafficChart(this.proxy_name, json.traffic_in, json.traffic_out)
-              }).catch( err => {
-                  this.$message({
-                      showClose: true,
-                      message: 'Get server info from frps failed!' + err,
-                      type: 'warning'
-                    })
-              })
-        }
-    }
+<script setup lang="ts">
+import { ElMessage } from 'element-plus'
+import { DrawProxyTrafficChart } from '../utils/chart.js'
+
+const props = defineProps<{
+  proxy_name: string
+}>()
+
+const fetchData = () => {
+  let url = '../api/traffic/' + props.proxy_name
+  fetch(url, { credentials: 'include' })
+    .then((res) => {
+      return res.json()
+    })
+    .then((json) => {
+      DrawProxyTrafficChart(props.proxy_name, json.traffic_in, json.traffic_out)
+    })
+    .catch((err) => {
+      ElMessage({
+        showClose: true,
+        message: 'Get traffic info failed!' + err,
+        type: 'warning',
+      })
+    })
 }
+fetchData()
 </script>
-
-<style>
-</style>
+<style></style>

+ 0 - 15
web/frps/src/index.html

@@ -1,15 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
-    <meta charset="utf-8">
-    <title>frps dashboard</title>
-</head>
-
-<body>
-    <div id="app"></div>
-    <!--<script src="https://code.jquery.com/jquery-3.2.0.min.js"></script>-->
-    <!--<script src="//cdn.bootcss.com/echarts/3.4.0/echarts.min.js"></script>-->
-</body>
-
-</html>

+ 0 - 48
web/frps/src/main.js

@@ -1,48 +0,0 @@
-import Vue from 'vue'
-//import ElementUI from 'element-ui'
-import {
-    Button,
-    Form,
-    FormItem,
-    Row,
-    Col,
-    Table,
-    TableColumn,
-    Popover,
-    Menu,
-    Submenu,
-    MenuItem,
-    Tag
-} from 'element-ui'
-import lang from 'element-ui/lib/locale/lang/en'
-import locale from 'element-ui/lib/locale'
-import 'element-ui/lib/theme-chalk/index.css'
-import './utils/less/custom.less'
-
-import App from './App.vue'
-import router from './router'
-import 'whatwg-fetch'
-
-locale.use(lang)
-
-Vue.use(Button)
-Vue.use(Form)
-Vue.use(FormItem)
-Vue.use(Row)
-Vue.use(Col)
-Vue.use(Table)
-Vue.use(TableColumn)
-Vue.use(Popover)
-Vue.use(Menu)
-Vue.use(Submenu)
-Vue.use(MenuItem)
-Vue.use(Tag)
-
-Vue.config.productionTip = false
-
-new Vue({
-    el: '#app',
-    router,
-    template: '<App/>',
-    components: { App }
-})

+ 12 - 0
web/frps/src/main.ts

@@ -0,0 +1,12 @@
+import { createApp } from 'vue'
+import 'element-plus/dist/index.css'
+import App from './App.vue'
+import router from './router'
+
+import './assets/custom.css'
+
+const app = createApp(App)
+
+app.use(router)
+
+app.mount('#app')

+ 0 - 43
web/frps/src/router/index.js

@@ -1,43 +0,0 @@
-import Vue from 'vue'
-import Router from 'vue-router'
-import Overview from '../components/Overview.vue'
-import ProxiesTcp from '../components/ProxiesTcp.vue'
-import ProxiesUdp from '../components/ProxiesUdp.vue'
-import ProxiesHttp from '../components/ProxiesHttp.vue'
-import ProxiesHttps from '../components/ProxiesHttps.vue'
-import ProxiesStcp from '../components/ProxiesStcp.vue'
-import ProxiesSudp from '../components/ProxiesSudp.vue'
-
-Vue.use(Router)
-
-export default new Router({
-    routes: [{
-        path: '/',
-        name: 'Overview',
-        component: Overview
-    }, {
-        path: '/proxies/tcp',
-        name: 'ProxiesTcp',
-        component: ProxiesTcp
-    }, {
-        path: '/proxies/udp',
-        name: 'ProxiesUdp',
-        component: ProxiesUdp
-    }, {
-        path: '/proxies/http',
-        name: 'ProxiesHttp',
-        component: ProxiesHttp
-    }, {
-        path: '/proxies/https',
-        name: 'ProxiesHttps',
-        component: ProxiesHttps
-    }, {
-        path: '/proxies/stcp',
-        name: 'ProxiesStcp',
-        component: ProxiesStcp
-    }, {
-        path: '/proxies/sudp',
-        name: 'ProxiesSudp',
-        component: ProxiesSudp
-    }]
-})

+ 51 - 0
web/frps/src/router/index.ts

@@ -0,0 +1,51 @@
+import { createRouter, createWebHashHistory } from 'vue-router'
+import ServerOverview from '../components/ServerOverview.vue'
+import ProxiesTCP from '../components/ProxiesTCP.vue'
+import ProxiesUDP from '../components/ProxiesUDP.vue'
+import ProxiesHTTP from '../components/ProxiesHTTP.vue'
+import ProxiesHTTPS from '../components/ProxiesHTTPS.vue'
+import ProxiesSTCP from '../components/ProxiesSTCP.vue'
+import ProxiesSUDP from '../components/ProxiesSUDP.vue'
+
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes: [
+    {
+      path: '/',
+      name: 'ServerOverview',
+      component: ServerOverview,
+    },
+    {
+      path: '/proxies/tcp',
+      name: 'ProxiesTCP',
+      component: ProxiesTCP,
+    },
+    {
+      path: '/proxies/udp',
+      name: 'ProxiesUDP',
+      component: ProxiesUDP,
+    },
+    {
+      path: '/proxies/http',
+      name: 'ProxiesHTTP',
+      component: ProxiesHTTP,
+    },
+    {
+      path: '/proxies/https',
+      name: 'ProxiesHTTPS',
+      component: ProxiesHTTPS,
+    },
+    {
+      path: '/proxies/stcp',
+      name: 'ProxiesSTCP',
+      component: ProxiesSTCP,
+    },
+    {
+      path: '/proxies/sudp',
+      name: 'ProxiesSUDP',
+      component: ProxiesSUDP,
+    },
+  ],
+})
+
+export default router

+ 0 - 186
web/frps/src/utils/chart.js

@@ -1,186 +0,0 @@
-import Humanize from "humanize-plus"
-import echarts from "echarts/lib/echarts"
-
-import "echarts/theme/macarons"
-import "echarts/lib/chart/bar"
-import "echarts/lib/chart/pie"
-import "echarts/lib/component/tooltip"
-import "echarts/lib/component/title"
-
-function DrawTrafficChart(elementId, trafficIn, trafficOut) {
-    let myChart = echarts.init(document.getElementById(elementId), 'macarons');
-    myChart.showLoading()
-
-    let option = {
-        title: {
-            text: 'Network Traffic',
-            subtext: 'today',
-            x: 'center'
-        },
-        tooltip: {
-            trigger: 'item',
-            formatter: function(v) {
-                return Humanize.fileSize(v.data.value) + " (" + v.percent + "%)"
-            }
-        },
-        series: [{
-            type: 'pie',
-            radius: '55%',
-            center: ['50%', '60%'],
-            data: [{
-                value: trafficIn,
-                name: 'Traffic In'
-            }, {
-                value: trafficOut,
-                name: 'Traffic Out'
-            }, ],
-            itemStyle: {
-                emphasis: {
-                    shadowBlur: 10,
-                    shadowOffsetX: 0,
-                    shadowColor: 'rgba(0, 0, 0, 0.5)'
-                }
-            }
-        }]
-    };
-    myChart.setOption(option);
-    myChart.hideLoading()
-}
-
-function DrawProxyChart(elementId, serverInfo) {
-    let myChart = echarts.init(document.getElementById(elementId), 'macarons')
-    myChart.showLoading()
-
-    let option = {
-        title: {
-            text: 'Proxies',
-            subtext: 'now',
-            x: 'center'
-        },
-        tooltip: {
-            trigger: 'item',
-            formatter: function(v) {
-                return v.data.value
-            }
-        },
-        series: [{
-            type: 'pie',
-            radius: '55%',
-            center: ['50%', '60%'],
-            data: [],
-            itemStyle: {
-                emphasis: {
-                    shadowBlur: 10,
-                    shadowOffsetX: 0,
-                    shadowColor: 'rgba(0, 0, 0, 0.5)'
-                }
-            }
-        }]
-    };
-
-    if (serverInfo.proxy_type_count.tcp != null && serverInfo.proxy_type_count.tcp != 0) {
-        option.series[0].data.push({value: serverInfo.proxy_type_count.tcp, name: 'TCP'})
-    }
-    if (serverInfo.proxy_type_count.udp != null && serverInfo.proxy_type_count.udp != 0) {
-        option.series[0].data.push({value: serverInfo.proxy_type_count.udp, name: 'UDP'})
-    }
-    if (serverInfo.proxy_type_count.http != null && serverInfo.proxy_type_count.http != 0) {
-        option.series[0].data.push({value: serverInfo.proxy_type_count.http, name: 'HTTP'})
-    }
-    if (serverInfo.proxy_type_count.https != null && serverInfo.proxy_type_count.https != 0) {
-        option.series[0].data.push({value: serverInfo.proxy_type_count.https, name: 'HTTPS'})
-    }
-    if (serverInfo.proxy_type_count.stcp != null && serverInfo.proxy_type_count.stcp != 0) {
-        option.series[0].data.push({value: serverInfo.proxy_type_count.stcp, name: 'STCP'})
-    }
-    if (serverInfo.proxy_type_count.sudp != null && serverInfo.proxy_type_count.sudp != 0) {
-        option.series[0].data.push({value: serverInfo.proxy_type_count.sudp, name: 'SUDP'})
-    }
-    if (serverInfo.proxy_type_count.xtcp != null && serverInfo.proxy_type_count.xtcp != 0) {
-        option.series[0].data.push({value: serverInfo.proxy_type_count.xtcp, name: 'XTCP'})
-    }
-
-    myChart.setOption(option);
-    myChart.hideLoading()
-}
-
-// 7 days
-function DrawProxyTrafficChart(elementId, trafficInArr, trafficOutArr) {
-    let params = {
-        width: '600px',
-        height: '400px'
-    }
-
-    let myChart = echarts.init(document.getElementById(elementId), 'macarons', params);
-    myChart.showLoading()
-
-    trafficInArr = trafficInArr.reverse()
-    trafficOutArr = trafficOutArr.reverse()
-    let now = new Date()
-    now = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6)
-    let dates = new Array()
-    for (let i = 0; i < 7; i++) {
-        dates.push(now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate())
-        now = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1)
-    }
-
-    let option = {
-        tooltip: {
-            trigger: 'axis',
-            axisPointer: {
-                type: 'shadow'
-            },
-            formatter: function(data) {
-                let html = ''
-                if (data.length > 0) {
-                    html += data[0].name + '<br/>'
-                }
-                for (let v of data) {
-                    let colorEl = '<span style="display:inline-block;margin-right:5px;' +
-                        'border-radius:10px;width:9px;height:9px;background-color:' + v.color + '"></span>';
-                    html += colorEl + v.seriesName + ': ' + Humanize.fileSize(v.value) + '<br/>'
-                }
-                return html
-            }
-        },
-        legend: {
-            data: ['Traffic In', 'Traffic Out']
-        },
-        grid: {
-            left: '3%',
-            right: '4%',
-            bottom: '3%',
-            containLabel: true
-        },
-        xAxis: [{
-            type: 'category',
-            data: dates
-        }],
-        yAxis: [{
-            type: 'value',
-            axisLabel: {
-                formatter: function(value) {
-                    return Humanize.fileSize(value)
-                }
-            }
-        }],
-        series: [{
-            name: 'Traffic In',
-            type: 'bar',
-            data: trafficInArr
-        }, {
-
-            name: 'Traffic Out',
-            type: 'bar',
-            data: trafficOutArr
-        }]
-    };
-    myChart.setOption(option);
-    myChart.hideLoading()
-}
-
-export {
-    DrawTrafficChart,
-    DrawProxyChart,
-    DrawProxyTrafficChart
-}

+ 293 - 0
web/frps/src/utils/chart.ts

@@ -0,0 +1,293 @@
+import * as Humanize from 'humanize-plus'
+import * as echarts from 'echarts/core'
+import { PieChart, BarChart } from 'echarts/charts'
+import { CanvasRenderer } from 'echarts/renderers'
+import { LabelLayout } from 'echarts/features'
+
+import {
+  TitleComponent,
+  TooltipComponent,
+  LegendComponent,
+  GridComponent,
+} from 'echarts/components'
+
+echarts.use([
+  PieChart,
+  BarChart,
+  CanvasRenderer,
+  LabelLayout,
+  TitleComponent,
+  TooltipComponent,
+  LegendComponent,
+  GridComponent,
+])
+
+function DrawTrafficChart(
+  elementId: string,
+  trafficIn: number,
+  trafficOut: number
+) {
+  const myChart = echarts.init(
+    document.getElementById(elementId) as HTMLElement,
+    'macarons'
+  )
+  myChart.showLoading()
+
+  const option = {
+    title: {
+      text: 'Network Traffic',
+      subtext: 'today',
+      left: 'center',
+    },
+    tooltip: {
+      trigger: 'item',
+      formatter: function (v: any) {
+        return Humanize.fileSize(v.data.value) + ' (' + v.percent + '%)'
+      },
+    },
+    legend: {
+      orient: 'vertical',
+      left: 'left',
+      data: ['Traffic In', 'Traffic Out'],
+    },
+    series: [
+      {
+        type: 'pie',
+        radius: '55%',
+        center: ['50%', '60%'],
+        data: [
+          {
+            value: trafficIn,
+            name: 'Traffic In',
+          },
+          {
+            value: trafficOut,
+            name: 'Traffic Out',
+          },
+        ],
+        emphasis: {
+          itemStyle: {
+            shadowBlur: 10,
+            shadowOffsetX: 0,
+            shadowColor: 'rgba(0, 0, 0, 0.5)',
+          },
+        },
+      },
+    ],
+  }
+  myChart.setOption(option)
+  myChart.hideLoading()
+}
+
+function DrawProxyChart(elementId: string, serverInfo: any) {
+  const myChart = echarts.init(
+    document.getElementById(elementId) as HTMLElement,
+    'macarons'
+  )
+  myChart.showLoading()
+
+  const option = {
+    title: {
+      text: 'Proxies',
+      subtext: 'now',
+      left: 'center',
+    },
+    tooltip: {
+      trigger: 'item',
+      formatter: function (v: any) {
+        return String(v.data.value)
+      },
+    },
+    legend: {
+      orient: 'vertical',
+      left: 'left',
+      data: <string[]>[],
+    },
+    series: [
+      {
+        type: 'pie',
+        radius: '55%',
+        center: ['50%', '60%'],
+        data: <any[]>[],
+        emphasis: {
+          itemStyle: {
+            shadowBlur: 10,
+            shadowOffsetX: 0,
+            shadowColor: 'rgba(0, 0, 0, 0.5)',
+          },
+        },
+      },
+    ],
+  }
+
+  if (
+    serverInfo.proxy_type_count.tcp != null &&
+    serverInfo.proxy_type_count.tcp != 0
+  ) {
+    option.series[0].data.push({
+      value: serverInfo.proxy_type_count.tcp,
+      name: 'TCP',
+    })
+    option.legend.data.push('TCP')
+  }
+  if (
+    serverInfo.proxy_type_count.udp != null &&
+    serverInfo.proxy_type_count.udp != 0
+  ) {
+    option.series[0].data.push({
+      value: serverInfo.proxy_type_count.udp,
+      name: 'UDP',
+    })
+    option.legend.data.push('UDP')
+  }
+  if (
+    serverInfo.proxy_type_count.http != null &&
+    serverInfo.proxy_type_count.http != 0
+  ) {
+    option.series[0].data.push({
+      value: serverInfo.proxy_type_count.http,
+      name: 'HTTP',
+    })
+    option.legend.data.push('HTTP')
+  }
+  if (
+    serverInfo.proxy_type_count.https != null &&
+    serverInfo.proxy_type_count.https != 0
+  ) {
+    option.series[0].data.push({
+      value: serverInfo.proxy_type_count.https,
+      name: 'HTTPS',
+    })
+    option.legend.data.push('HTTPS')
+  }
+  if (
+    serverInfo.proxy_type_count.stcp != null &&
+    serverInfo.proxy_type_count.stcp != 0
+  ) {
+    option.series[0].data.push({
+      value: serverInfo.proxy_type_count.stcp,
+      name: 'STCP',
+    })
+    option.legend.data.push('STCP')
+  }
+  if (
+    serverInfo.proxy_type_count.sudp != null &&
+    serverInfo.proxy_type_count.sudp != 0
+  ) {
+    option.series[0].data.push({
+      value: serverInfo.proxy_type_count.sudp,
+      name: 'SUDP',
+    })
+    option.legend.data.push('SUDP')
+  }
+  if (
+    serverInfo.proxy_type_count.xtcp != null &&
+    serverInfo.proxy_type_count.xtcp != 0
+  ) {
+    option.series[0].data.push({
+      value: serverInfo.proxy_type_count.xtcp,
+      name: 'XTCP',
+    })
+    option.legend.data.push('XTCP')
+  }
+
+  myChart.setOption(option)
+  myChart.hideLoading()
+}
+
+// 7 days
+function DrawProxyTrafficChart(
+  elementId: string,
+  trafficInArr: number[],
+  trafficOutArr: number[]
+) {
+  const params = {
+    width: '600px',
+    height: '400px',
+  }
+
+  const myChart = echarts.init(
+    document.getElementById(elementId) as HTMLElement,
+    'macarons',
+    params
+  )
+  myChart.showLoading()
+
+  trafficInArr = trafficInArr.reverse()
+  trafficOutArr = trafficOutArr.reverse()
+  let now = new Date()
+  now = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6)
+  const dates: Array<string> = []
+  for (let i = 0; i < 7; i++) {
+    dates.push(
+      now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate()
+    )
+    now = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1)
+  }
+
+  const option = {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+      },
+      formatter: function (data: any) {
+        let html = ''
+        if (data.length > 0) {
+          html += data[0].name + '<br/>'
+        }
+        for (const v of data) {
+          const colorEl =
+            '<span style="display:inline-block;margin-right:5px;' +
+            'border-radius:10px;width:9px;height:9px;background-color:' +
+            v.color +
+            '"></span>'
+          html +=
+            colorEl + v.seriesName + ': ' + Humanize.fileSize(v.value) + '<br/>'
+        }
+        return html
+      },
+    },
+    legend: {
+      data: ['Traffic In', 'Traffic Out'],
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true,
+    },
+    xAxis: [
+      {
+        type: 'category',
+        data: dates,
+      },
+    ],
+    yAxis: [
+      {
+        type: 'value',
+        axisLabel: {
+          formatter: function (value: number) {
+            return Humanize.fileSize(value)
+          },
+        },
+      },
+    ],
+    series: [
+      {
+        name: 'Traffic In',
+        type: 'bar',
+        data: trafficInArr,
+      },
+      {
+        name: 'Traffic Out',
+        type: 'bar',
+        data: trafficOutArr,
+      },
+    ],
+  }
+  myChart.setOption(option)
+  myChart.hideLoading()
+}
+
+export { DrawTrafficChart, DrawProxyChart, DrawProxyTrafficChart }

+ 0 - 22
web/frps/src/utils/less/custom.less

@@ -1,22 +0,0 @@
-@color: red;
-
-.el-form-item {
-    span {
-        margin-left: 15px;
-    }
-}
-
-.demo-table-expand {
-    font-size: 0;
-
-    label {
-        width: 90px;
-        color: #99a9bf;
-    }
-
-    .el-form-item {
-        margin-right: 0;
-        margin-bottom: 0;
-        width: 50%;
-    }
-}

+ 0 - 104
web/frps/src/utils/proxy.js

@@ -1,104 +0,0 @@
-class BaseProxy {
-    constructor(proxyStats) {
-        this.name = proxyStats.name
-        if (proxyStats.conf != null) {
-            this.encryption = proxyStats.conf.use_encryption
-            this.compression = proxyStats.conf.use_compression
-        } else {
-            this.encryption = ""
-            this.compression = ""
-        }
-        this.conns = proxyStats.cur_conns
-        this.traffic_in = proxyStats.today_traffic_in
-        this.traffic_out = proxyStats.today_traffic_out
-        this.last_start_time = proxyStats.last_start_time
-        this.last_close_time = proxyStats.last_close_time
-        this.status = proxyStats.status
-    }
-}
-
-class TcpProxy extends BaseProxy {
-    constructor(proxyStats) {
-        super(proxyStats)
-        this.type = "tcp"
-        if (proxyStats.conf != null) {
-            this.addr = ":" + proxyStats.conf.remote_port
-            this.port = proxyStats.conf.remote_port
-        } else {
-            this.addr = ""
-            this.port = ""
-        }
-    }
-}
-
-class UdpProxy extends BaseProxy {
-    constructor(proxyStats) {
-        super(proxyStats)
-        this.type = "udp"
-        if (proxyStats.conf != null) {
-            this.addr = ":" + proxyStats.conf.remote_port
-            this.port = proxyStats.conf.remote_port
-        } else {
-            this.addr = ""
-            this.port = ""
-        }
-    }
-}
-
-class HttpProxy extends BaseProxy {
-    constructor(proxyStats, port, subdomain_host) {
-        super(proxyStats)
-        this.type = "http"
-        this.port = port
-        if (proxyStats.conf != null) {
-            this.custom_domains = proxyStats.conf.custom_domains
-            this.host_header_rewrite = proxyStats.conf.host_header_rewrite
-            this.locations = proxyStats.conf.locations
-            if (proxyStats.conf.subdomain != "") {
-                this.subdomain = proxyStats.conf.subdomain + "." + subdomain_host
-            } else {
-                this.subdomain = ""
-            }
-        } else {
-            this.custom_domains = ""
-            this.host_header_rewrite = ""
-            this.subdomain = ""
-            this.locations = ""
-        }
-    }
-}
-
-class HttpsProxy extends BaseProxy {
-    constructor(proxyStats, port, subdomain_host) {
-        super(proxyStats)
-        this.type = "https"
-        this.port = port
-        if (proxyStats.conf != null) {
-            this.custom_domains = proxyStats.conf.custom_domains
-            if (proxyStats.conf.subdomain != "") {
-                this.subdomain = proxyStats.conf.subdomain + "." + subdomain_host
-            } else {
-                this.subdomain = ""
-            }
-        } else {
-            this.custom_domains = ""
-            this.subdomain = ""
-        }
-    }
-}
-
-class StcpProxy extends BaseProxy {
-    constructor(proxyStats) {
-        super(proxyStats)
-        this.type = "stcp"
-    }
-}
-
-class SudpProxy extends BaseProxy {
-    constructor(proxyStats) {
-        super(proxyStats)
-        this.type = "sudp"
-    }
-}
-
-export {BaseProxy, TcpProxy, UdpProxy, HttpProxy, HttpsProxy, StcpProxy, SudpProxy}

+ 138 - 0
web/frps/src/utils/proxy.ts

@@ -0,0 +1,138 @@
+class BaseProxy {
+  name: string
+  type: string
+  encryption: boolean
+  compression: boolean
+  conns: number
+  traffic_in: number
+  traffic_out: number
+  last_start_time: string
+  last_close_time: string
+  status: string
+  addr: string
+  port: number
+
+  custom_domains: string
+  host_header_rewrite: string
+  locations: string
+  subdomain: string
+
+  constructor(proxyStats: any) {
+    this.name = proxyStats.name
+    this.type = ''
+    if (proxyStats.conf != null) {
+      this.encryption = proxyStats.conf.use_encryption
+      this.compression = proxyStats.conf.use_compression
+    } else {
+      this.encryption = false
+      this.compression = false
+    }
+    this.conns = proxyStats.cur_conns
+    this.traffic_in = proxyStats.today_traffic_in
+    this.traffic_out = proxyStats.today_traffic_out
+    this.last_start_time = proxyStats.last_start_time
+    this.last_close_time = proxyStats.last_close_time
+    this.status = proxyStats.status
+
+    this.addr = ''
+    this.port = 0
+    this.custom_domains = ''
+    this.host_header_rewrite = ''
+    this.locations = ''
+    this.subdomain = ''
+  }
+}
+
+class TCPProxy extends BaseProxy {
+  constructor(proxyStats: any) {
+    super(proxyStats)
+    this.type = 'tcp'
+    if (proxyStats.conf != null) {
+      this.addr = ':' + proxyStats.conf.remote_port
+      this.port = proxyStats.conf.remote_port
+    } else {
+      this.addr = ''
+      this.port = 0
+    }
+  }
+}
+
+class UDPProxy extends BaseProxy {
+  constructor(proxyStats: any) {
+    super(proxyStats)
+    this.type = 'udp'
+    if (proxyStats.conf != null) {
+      this.addr = ':' + proxyStats.conf.remote_port
+      this.port = proxyStats.conf.remote_port
+    } else {
+      this.addr = ''
+      this.port = 0
+    }
+  }
+}
+
+class HTTPProxy extends BaseProxy {
+  constructor(proxyStats: any, port: number, subdomain_host: string) {
+    super(proxyStats)
+    this.type = 'http'
+    this.port = port
+    if (proxyStats.conf != null) {
+      this.custom_domains = proxyStats.conf.custom_domains
+      this.host_header_rewrite = proxyStats.conf.host_header_rewrite
+      this.locations = proxyStats.conf.locations
+      if (proxyStats.conf.subdomain != '') {
+        this.subdomain = proxyStats.conf.subdomain + '.' + subdomain_host
+      } else {
+        this.subdomain = ''
+      }
+    } else {
+      this.custom_domains = ''
+      this.host_header_rewrite = ''
+      this.subdomain = ''
+      this.locations = ''
+    }
+  }
+}
+
+class HTTPSProxy extends BaseProxy {
+  constructor(proxyStats: any, port: number, subdomain_host: string) {
+    super(proxyStats)
+    this.type = 'https'
+    this.port = port
+    if (proxyStats.conf != null) {
+      this.custom_domains = proxyStats.conf.custom_domains
+      if (proxyStats.conf.subdomain != '') {
+        this.subdomain = proxyStats.conf.subdomain + '.' + subdomain_host
+      } else {
+        this.subdomain = ''
+      }
+    } else {
+      this.custom_domains = ''
+      this.subdomain = ''
+    }
+  }
+}
+
+class STCPProxy extends BaseProxy {
+  constructor(proxyStats: any) {
+    super(proxyStats)
+    this.type = 'stcp'
+  }
+}
+
+class SUDPProxy extends BaseProxy {
+  constructor(proxyStats: any) {
+    super(proxyStats)
+    this.type = 'sudp'
+  }
+}
+
+export {
+  BaseProxy,
+  TCPProxy,
+  UDPProxy,
+  HTTPProxy,
+  HTTPSProxy,
+  STCPProxy,
+  SUDPProxy,
+}

+ 8 - 0
web/frps/tsconfig.config.json

@@ -0,0 +1,8 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.node.json",
+  "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
+  "compilerOptions": {
+    "composite": true,
+    "types": ["node"]
+  }
+}

+ 16 - 0
web/frps/tsconfig.json

@@ -0,0 +1,16 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.web.json",
+  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+
+  "references": [
+    {
+      "path": "./tsconfig.config.json"
+    }
+  ]
+}

+ 29 - 0
web/frps/vite.config.ts

@@ -0,0 +1,29 @@
+import { fileURLToPath, URL } from 'node:url'
+
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import AutoImport from 'unplugin-auto-import/vite'
+import Components from 'unplugin-vue-components/vite'
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  base: '',
+  plugins: [
+    vue(),
+    AutoImport({
+      resolvers: [ElementPlusResolver()],
+    }),
+    Components({
+      resolvers: [ElementPlusResolver()],
+    }),
+  ],
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url)),
+    },
+  },
+  build: {
+    assetsDir: '',
+  },
+})

+ 0 - 107
web/frps/webpack.config.js

@@ -1,107 +0,0 @@
-const path = require('path')
-var webpack = require('webpack')
-var HtmlWebpackPlugin = require('html-webpack-plugin')
-var VueLoaderPlugin = require('vue-loader/lib/plugin')
-var url = require('url')
-var publicPath = ''
-
-module.exports = (options = {}) => ({
-    entry: {
-        vendor: './src/main'
-    },
-    output: {
-        path: path.resolve(__dirname, 'dist'),
-        filename: options.dev ? '[name].js' : '[name].js?[chunkhash]',
-        chunkFilename: '[id].js?[chunkhash]',
-        publicPath: options.dev ? '/assets/' : publicPath
-    },
-    resolve: {
-        extensions: ['.js', '.vue', '.json'],
-        alias: {
-            'vue$': 'vue/dist/vue.esm.js',
-            '@': path.resolve(__dirname, 'src'),
-        }
-    },
-    module: {
-        rules: [{
-            test: /\.vue$/,
-            loader: 'vue-loader'
-        }, {
-            test: /\.js$/,
-            use: ['babel-loader'],
-            exclude: /node_modules/
-        }, {
-            test: /\.html$/,
-            use: [{
-                loader: 'html-loader',
-                options: {
-                    root: path.resolve(__dirname, 'src'),
-                    attrs: ['img:src', 'link:href']
-                }
-            }]
-        }, {
-            test: /\.less$/,
-            loader: 'style-loader!css-loader!postcss-loader!less-loader'
-        }, {
-            test: /\.css$/,
-            use: ['style-loader', 'css-loader', 'postcss-loader']
-        }, {
-            test: /favicon\.png$/,
-            use: [{
-                loader: 'file-loader',
-                options: {
-                    name: '[name].[ext]?[hash]'
-                }
-            }]
-        }, {
-            test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/,
-            exclude: /favicon\.png$/,
-            use: [{
-                loader: 'url-loader',
-                options: {
-                    limit: 10000
-                }
-            }]
-        }]
-    },
-    plugins: [
-        new webpack.optimize.CommonsChunkPlugin({
-            names: ['vendor', 'manifest']
-        }),
-        new HtmlWebpackPlugin({
-            favicon: 'src/assets/favicon.ico',
-            template: 'src/index.html'
-        }),
-        new webpack.NormalModuleReplacementPlugin(/element-ui[\/\\]lib[\/\\]locale[\/\\]lang[\/\\]zh-CN/, 'element-ui/lib/locale/lang/en'),
-        new webpack.DefinePlugin({
-            'process.env': {
-                NODE_ENV: '"production"'
-            }
-        }),
-        new webpack.optimize.UglifyJsPlugin({
-            sourceMap: false,
-            comments: false,
-            compress: {
-                warnings: false
-            }
-        }),
-        new VueLoaderPlugin()
-    ],
-    devServer: {
-        host: '127.0.0.1',
-        port: 8010,
-        proxy: {
-            '/api/': {
-                target: 'http://127.0.0.1:8080',
-                changeOrigin: true,
-                pathRewrite: {
-                    '^/api': ''
-                }
-            }
-        },
-        historyApiFallback: {
-            index: url.parse(options.dev ? '/assets/' : publicPath).pathname
-        }
-    }//,
-    //devtool: options.dev ? '#eval-source-map' : '#source-map'
-})

Diferenças do arquivo suprimidas por serem muito extensas
+ 2249 - 5024
web/frps/yarn.lock


Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff