更改目录结构

This commit is contained in:
bjdgyc 2021-03-01 15:46:08 +08:00
parent 3464d1d10e
commit 0f91c779e3
105 changed files with 29099 additions and 96 deletions

View File

@ -1,4 +1,5 @@
ignore: ignore:
- "conf" - "screenshot"
- "down_files" - "web"
- "screenshot" - "server/conf"
- "server/files"

View File

@ -13,26 +13,29 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set up Go 1.x - name: Set up Go 1.x
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: 1.15 go-version: 1.15
id: go id: go
- name: Check out code into the Go module directory - name: Switch path
uses: actions/checkout@v2 run: cd server
- name: Get dependencies - name: Check out code into the Go module directory
run: | uses: actions/checkout@v2
go get -v -t -d ./...
- name: Build - name: Get dependencies
run: | run: |
go build -v -o anylink -ldflags "-X main.COMMIT_ID=`git rev-parse HEAD`" go get -v -t -d ./...
./anylink -rev
- name: Build
- name: Test coverage run: |
run: go test -race -coverprofile=coverage.txt -covermode=atomic -v ./... go build -v -o anylink -ldflags "-X main.COMMIT_ID=`git rev-parse HEAD`"
./anylink -rev
- name: Upload coverage to Codecov
run: bash <(curl -s https://codecov.io/bash) - name: Test coverage
run: go test -race -coverprofile=coverage.txt -covermode=atomic -v ./...
- name: Upload coverage to Codecov
run: bash <(curl -s https://codecov.io/bash)

18
.gitignore vendored
View File

@ -1,19 +1,3 @@
# Binaries for programs and plugins # Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
vendor/
ui/
.idea/ .idea/
anylink anylink-deploy

View File

@ -34,7 +34,7 @@ AnyLink 服务端仅在CentOS 7、Ubuntu 18.04测试通过,如需要安装在
git clone https://github.com/bjdgyc/anylink.git git clone https://github.com/bjdgyc/anylink.git
cd anylink cd anylink
sh deploy.sh sh build.sh
# 注意使用root权限运行 # 注意使用root权限运行
cd anylink-deploy cd anylink-deploy
@ -73,7 +73,7 @@ sudo ./anylink -conf="conf/server.toml"
./anylink -secret ./anylink -secret
``` ```
[conf/server.toml](https://github.com/bjdgyc/anylink/blob/master/conf/server.toml) [conf/server.toml](server/conf/server.toml)
## Setting ## Setting

35
build.sh Normal file
View File

@ -0,0 +1,35 @@
#!/usr/bin/env bash
#当前目录
cpath=$(pwd)
echo "编译二进制文件"
cd $cpath/server
go build -o anylink -ldflags "-X main.COMMIT_ID=$(git rev-parse HEAD)"
echo "编译前端项目"
cd $cpath/web
#国内可替换源加快速度
npm install --registry=https://registry.npm.taobao.org
npm run build --registry=https://registry.npm.taobao.org
#npm install
#npm run build
cd $cpath
echo "整理部署文件"
deploy="anylink-deploy"
rm -rf $deploy
mkdir $deploy
mkdir $deploy/log
cp -r server/anylink $deploy
cp -r server/conf $deploy
cp -r server/files $deploy
cp -r server/bridge-init.sh $deploy
cp -r web/ui $deploy
#注意使用root权限运行
#cd anylink-deploy
#sudo ./anylink -conf="conf/server.toml"

View File

@ -1,29 +0,0 @@
#!/usr/bin/env bash
#编译二进制文件
go build -o anylink -ldflags "-X main.COMMIT_ID=`git rev-parse HEAD`"
#编译前端项目
git clone https://github.com/bjdgyc/anylink-web.git
cd anylink-web
#国内可替换源加快速度
#npm install --registry=https://registry.npm.taobao.org
#npm run build --registry=https://registry.npm.taobao.org
npm install
npm run build
cd ../
#整理部署文件
mkdir anylink-deploy
mkdir anylink-deploy/log
cp -r anylink anylink-deploy
cp -r anylink-web/ui anylink-deploy
cp -r conf anylink-deploy
cp -r down_files anylink-deploy
#注意使用root权限运行
#cd anylink-deploy
#sudo ./anylink -conf="conf/server.toml"

19
server/.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
vendor/
ui/
.idea/
anylink

View File

@ -2,5 +2,5 @@ package base
const ( const (
APP_NAME = "AnyLink" APP_NAME = "AnyLink"
APP_VER = "0.1.5" APP_VER = "0.1.6"
) )

View File

@ -16,13 +16,14 @@ type User struct {
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
Email string `json:"email"` Email string `json:"email"`
// Password string `json:"password"` // Password string `json:"password"`
PinCode string `json:"pin_code"` PinCode string `json:"pin_code"`
OtpSecret string `json:"otp_secret"` OtpSecret string `json:"otp_secret"`
Groups []string `json:"groups"` DisableOtp bool `json:"disable_otp"` // 禁用otp
Status int8 `json:"status"` // 1正常 Groups []string `json:"groups"`
SendEmail bool `json:"send_email"` Status int8 `json:"status"` // 1正常
CreatedAt time.Time `json:"created_at"` SendEmail bool `json:"send_email"`
UpdatedAt time.Time `json:"updated_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
} }
func SetUser(v *User) error { func SetUser(v *User) error {
@ -39,7 +40,7 @@ func SetUser(v *User) error {
v.PinCode = planPass v.PinCode = planPass
if v.OtpSecret == "" { if v.OtpSecret == "" {
v.OtpSecret = gotp.RandomSecret(24) v.OtpSecret = gotp.RandomSecret(32)
} }
// 判断组是否有效 // 判断组是否有效
@ -85,19 +86,16 @@ func CheckUser(name, pwd, group string) error {
} }
// 判断otp信息 // 判断otp信息
otp := pwd[pl-6:] if !v.DisableOtp {
if !checkOtp(name, otp) { pwd = pwd[:pl-6]
return fmt.Errorf("%s %s", name, "动态码错误") otp := pwd[pl-6:]
} if !checkOtp(name, otp, v.OtpSecret) {
totp := gotp.NewDefaultTOTP(v.OtpSecret) return fmt.Errorf("%s %s", name, "动态码错误")
unix := time.Now().Unix() }
verify := totp.Verify(otp, int(unix))
if !verify {
return fmt.Errorf("%s %s", name, "动态码错误")
} }
pinCode := pwd[:pl-6] // 判断用户密码
if pinCode != v.PinCode { if pwd != v.PinCode {
return fmt.Errorf("%s %s", name, "密码错误") return fmt.Errorf("%s %s", name, "密码错误")
} }
@ -126,18 +124,23 @@ func init() {
}() }()
} }
// 令牌只能使用一次 // 判断令牌信息
func checkOtp(username, otp string) bool { func checkOtp(name, otp, secret string) bool {
key := fmt.Sprintf("%s:%s", username, otp) key := fmt.Sprintf("%s:%s", name, otp)
userOtpMux.Lock() userOtpMux.Lock()
defer userOtpMux.Unlock() defer userOtpMux.Unlock()
// 令牌只能使用一次
if _, ok := userOtp[key]; ok { if _, ok := userOtp[key]; ok {
// 已经存在 // 已经存在
return false return false
} }
userOtp[key] = time.Now() userOtp[key] = time.Now()
return true
totp := gotp.NewDefaultTOTP(secret)
unix := time.Now().Unix()
verify := totp.Verify(otp, int(unix))
return verify
} }

3
web/.browserslistrc Normal file
View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

17
web/.eslintrc.js Normal file
View File

@ -0,0 +1,17 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/essential',
'eslint:recommended'
],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}

24
web/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
.DS_Store
node_modules
/dist
/ui
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

21
web/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 bjdgyc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

30
web/README.md Normal file
View File

@ -0,0 +1,30 @@
# anylink-web
## Repo
> github: https://github.com/bjdgyc/anylink-web
> gitee: https://gitee.com/bjdgyc/anylink-web
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
web/babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

26204
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
web/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "anylink-web",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.20.0",
"core-js": "^3.6.5",
"echarts": "^4.9.0",
"element-ui": "^2.4.5",
"vue": "^2.6.11",
"vue-count-to": "^1.0.13",
"vue-router": "^3.4.6"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"vue-cli-plugin-element": "~1.0.1",
"vue-template-compiler": "^2.6.11"
}
}

BIN
web/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

17
web/public/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>AnyLink</title>
</head>
<body>
<noscript>
<strong>We're sorry but AnyLink doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

84
web/src/App.vue Normal file
View File

@ -0,0 +1,84 @@
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'app',
components: {},
created() {
const token = sessionStorage.getItem('jwtToken');
console.log("App created", token)
if (token) {
this.$root.isLogin = true
}
},
mounted() {
console.log("App mounted")
},
data() {
return {}
},
computed: {
currentComponent: function () {
var isLogin = this.$root.isLogin
console.log("App isLogin", isLogin)
if (isLogin) {
return "layout";
}
return "login";
},
},
}
</script>
<style>
html, body {
height: 100%;
margin: 0;
}
#app {
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/*color: #2c3e50;*/
/*border: 1px solid red;*/
height: 100%;
/*width:100%;*/
/*box-sizing: border-box;*/
/*padding: 4px;*/
}
.hide {
display: none;
}
/*space vertical*/
.sh-10 {
height: 10px;
}
.sh-20 {
height: 20px;
}
/*space horizontal*/
.sw-10 {
height: 1px;
width: 10px;
}
.sw-20 {
height: 1px;
width: 20px;
}
.m-left-10 {
margin-left: 10px;
}
</style>

BIN
web/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,37 @@
<template>
<div>
<div class="monitor">
<div class="monitor-left">{{ left }}</div>
<div class="monitor-right">{{ right }}</div>
</div>
<el-divider v-if="divider"></el-divider>
</div>
</template>
<script>
export default {
name: "Cell",
props: {
left: {},
right: {},
divider: {type: Boolean}
},
}
</script>
<style scoped>
.monitor {
display: flex;
justify-content: space-between;
align-items: center;
}
.monitor-left {
font-size: 14px;
}
.monitor-right {
font-size: 12px;
color: #909399;
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<div id="line-chart" :style="{height:height,width:width}"/>
</template>
<script>
import echarts from 'echarts'
export default {
name: 'LineChart',
props: {
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '350px'
},
// title,xAxis,series
chartData: {
type: Object,
required: true
}
},
data() {
return {}
},
mounted() {
this.initChart()
},
beforeDestroy() {
},
methods: {
initChart() {
let chart = echarts.init(this.$el)
const option = {
title: {
text: this.chartData.title || '折线图'
},
tooltip: {
trigger: 'axis'
},
legend: {},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
// toolbox: {
// feature: {
// saveAsImage: {}
// }
// },
xAxis: {
type: 'category',
boundaryGap: false,
data: this.chartData.xname
},
yAxis: {
type: 'value'
},
series: [],
};
let xdata = this.chartData.xdata
for (let key in xdata) {
// window.console.log(key);
let a = {
name: key,
type: 'line',
data: xdata[key]
};
option.series.push(a)
}
chart.setOption(option)
},
}
}
</script>

63
web/src/layout/Layout.vue Normal file
View File

@ -0,0 +1,63 @@
<template>
<el-container style="height: 100%;">
<!--侧边栏菜单-->
<el-aside :width="is_active?'200':'64'">
<LayoutAside :is_active="is_active" :route_path="route_path"/>
</el-aside>
<el-container>
<!--正文头部内容-->
<el-header>
<!--监听子组件的变量事件-->
<LayoutHeader :is_active.sync="is_active" :route_name="route_name"/>
</el-header>
<!--正文内容-->
<!--style="background-color: rgb(240, 242, 245);"-->
<el-main style="background-color: #fbfbfb">
<!-- 对应的组件内容渲染到router-view中 -->
<!--子组件上报route信息-->
<router-view :route_path.sync="route_path" :route_name.sync="route_name"></router-view>
</el-main>
</el-container>
</el-container>
</template>
<script>
import LayoutAside from "@/layout/LayoutAside";
import LayoutHeader from "@/layout/LayoutHeader";
export default {
name: "Layout",
components: {LayoutHeader, LayoutAside},
data() {
return {
is_active: true,
route_path: '/index',
route_name: ['首页'],
}
},
watch: {
route_path: function (val) {
// var w = document.getElementById('layout-menu').clientWidth;
window.console.log('is_active', val)
},
},
created() {
window.console.log('layout-route', this.$route)
},
}
</script>
<style>
.el-header {
background-color: #fff;
/*box-shadow: 0 1px 4px rgba(0, 21, 41, .08);*/
color: #333;
line-height: 60px;
/*width: 100%;*/
border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
}
</style>

View File

@ -0,0 +1,78 @@
<template>
<!--background-color="#304156"-->
<!--text-color="#bfcbd9"-->
<!--active-text-color="#409EFF"-->
<!--:unique-opened="false"-->
<!--<div class="layout-aside" :style="aside_style">-->
<el-menu :collapse="!is_active"
:default-active="route_path"
:style="is_active?'width:200px':''"
router
class="layout-menu"
:collapse-transition="false"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<el-menu-item index="/admin/home">
<i class="el-icon-s-home"></i>
<span slot="title">首页</span>
</el-menu-item>
<el-submenu index="1">
<template slot="title">
<i class="el-icon-menu"></i>
<span slot="title">基础信息</span>
</template>
<el-menu-item index="/admin/set/system">系统信息</el-menu-item>
<el-menu-item index="/admin/set/soft">软件配置</el-menu-item>
<el-menu-item index="/admin/set/other">其他设置</el-menu-item>
</el-submenu>
<el-submenu index="2">
<template slot="title">
<i class="el-icon-s-custom"></i>
<span slot="title">用户信息</span>
</template>
<el-menu-item index="/admin/user/list">用户列表</el-menu-item>
<el-menu-item index="/admin/user/online">在线用户</el-menu-item>
<el-menu-item index="/admin/user/ip_map">IP映射</el-menu-item>
</el-submenu>
<el-submenu index="3">
<template slot="title">
<i class="el-icon-s-order"></i>
<span slot="title">用户组信息</span>
</template>
<el-menu-item index="/admin/group/list">用户组列表</el-menu-item>
</el-submenu>
</el-menu>
</template>
<script>
export default {
name: "LayoutAside",
data() {
return {}
},
props: ['is_active', 'route_path'],
mounted() {
}
}
</script>
<style scoped>
.layout-menu {
height: 100%;
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<div class="layout-header">
<div>
<i @click="toggleClick" :class="is_active ? 'el-icon-s-fold' : 'el-icon-s-unfold'" class="toggle-icon"
style="font-size: 26px;"></i>
<el-breadcrumb separator="/" class="app-breadcrumb">
<el-breadcrumb-item v-for="(item, index) in route_name" :key="index">{{ item }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<el-dropdown trigger="click" @command="handleCommand">
<i class="el-icon-setting" style="margin-right: 15px"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="logout">退出</el-dropdown-item>
</el-dropdown-menu>
<span style="font-size: 12px;">{{ admin_user }}</span>
</el-dropdown>
</div>
</template>
<script>
import {getUser, removeToken} from "@/plugins/token";
export default {
name: "Layoutheader",
props: ['route_name'],
data() {
return {
is_active: true
}
},
computed: {
admin_user() {
return getUser();
},
},
methods: {
//
toggleClick() {
this.is_active = !this.is_active
// ,
this.$emit('update:is_active', this.is_active)
},
handleCommand() {
console.log("handleCommand")
// 退
removeToken()
this.$router.push("/login");
},
}
}
</script>
<style scoped>
.layout-header {
display: flex;
justify-content: space-between;
align-items: center
}
.toggle-icon {
cursor: pointer;
transition: background .3s;
-webkit-tap-highlight-color: transparent;
}
.toggle-icon:hover {
background: rgba(0, 0, 0, .025)
}
.app-breadcrumb {
display: inline-block;
font-size: 14px;
/*line-height: 20;*/
margin-left: 20px;
}
</style>

22
web/src/main.js Normal file
View File

@ -0,0 +1,22 @@
import Vue from 'vue'
import App from './App.vue'
import './plugins/element'
import "./plugins/mixin";
import request from './plugins/request'
import router from "./plugins/router";
//TODO
Vue.config.productionTip = false
const vm = new Vue({
data: {
// 判断是否登录
isLogin: false,
},
router,
render: h => h(App),
}).$mount('#app')
request(vm)

160
web/src/pages/Home.vue Normal file
View File

@ -0,0 +1,160 @@
<template>
<div class="home">
<el-row :gutter="40" class="panel-group">
<el-col :span="6" class="card-panel-col">
<div class="card-panel">
<i class="el-icon-user-solid" style="font-size:50px;color: #f4516c;"></i>
<div class="card-panel-description">
<div class="card-panel-text">在线数</div>
<countTo :startVal='0' :endVal='counts.online' :duration='2000' class="panel-num"></countTo>
</div>
</div>
</el-col>
<el-col :span="6" class="card-panel-col">
<div class="card-panel">
<i class="el-icon-user-solid" style="font-size:50px;color: #36a3f7"></i>
<div class="card-panel-description">
<div class="card-panel-text">用户数</div>
<countTo :startVal='0' :endVal='counts.user' :duration='2000' class="panel-num"></countTo>
</div>
</div>
</el-col>
<el-col :span="6" class="card-panel-col">
<div class="card-panel">
<i class="el-icon-wallet" style="font-size:50px;color:#34bfa3"></i>
<div class="card-panel-description">
<div class="card-panel-text">用户组数</div>
<countTo :startVal='0' :endVal='counts.group' :duration='2000' class="panel-num"></countTo>
</div>
</div>
</el-col>
<el-col :span="6" class="card-panel-col">
<div class="card-panel">
<i class="el-icon-s-order" style="font-size:50px;color:#40c9c6"></i>
<div class="card-panel-description">
<div class="card-panel-text">IP映射数</div>
<countTo :startVal='0' :endVal='counts.ip_map' :duration='2000' class="panel-num"></countTo>
</div>
</div>
</el-col>
</el-row>
<el-row class="line-chart">
<LineChart :chart-data="lineChartUser"/>
</el-row>
<el-row class="line-chart">
<LineChart :chart-data="lineChartOrder"/>
</el-row>
</div>
</template>
<script>
import countTo from 'vue-count-to';
import LineChart from "@/components/LineChart";
import axios from "axios";
const lineChartUser = {
title: '每日在线统计',
xname: ['2019-12-13', '2019-12-14', '2019-12-15', '2019-12-16', '2019-12-17', '2019-12-18', '2019-12-19'],
xdata: {
'test1': [10, 120, 11, 134, 105, 10, 15],
'test2': [10, 82, 91, 14, 162, 10, 15]
}
}
const lineChartOrder = {
title: '每日流量统计',
xname: ['2019-12-13', '2019-12-14', '2019-12-15', '2019-12-16', '2019-12-17', '2019-12-18', '2019-12-19'],
xdata: {
'test1': [100, 120, 161, 134, 105, 160, 165],
'test2': [120, 82, 91, 154, 162, 140, 145]
}
}
export default {
name: "Home",
components: {
LineChart,
countTo,
},
data() {
return {
counts: {
online: 0,
user: 0,
group: 0,
ip_map: 0,
},
lineChartUser: lineChartUser,
lineChartOrder: lineChartOrder,
}
},
created() {
this.$emit('update:route_path', this.$route.path)
this.$emit('update:route_name', ['首页'])
},
mounted() {
this.getData()
},
methods: {
getData() {
axios.get('/set/home').then(resp => {
var data = resp.data.data
console.log(data);
this.counts = data.counts
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
},
}
</script>
<style scoped>
.card-panel {
display: flex;
justify-content: space-around;
border: 1px solid red;
padding: 30px 0;
color: #666;
background: #fff;
/*box-shadow: 4px 4px 40px rgba(0, 0, 0, .05);*/
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
border-color: rgba(0, 0, 0, .05);
}
.card-panel-description {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.card-panel-text {
line-height: 18px;
color: rgba(0, 0, 0, .45);
font-size: 16px;
}
.panel-num {
font-size: 20px;
font-weight: 700;
}
.line-chart {
background: #fff;
padding: 0 16px;
margin-top: 40px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
border-color: rgba(0, 0, 0, .05);
}
</style>

118
web/src/pages/Login.vue Normal file
View File

@ -0,0 +1,118 @@
<template>
<div class="login">
<el-card style="width: 550px;">
<div class="issuer">AnyLink SSL VPN管理后台</div>
<el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm">
<el-form-item label="管理用户名" prop="admin_user">
<el-input v-model="ruleForm.admin_user"></el-input>
</el-form-item>
<el-form-item label="管理密码" prop="admin_pass">
<el-input type="password" v-model="ruleForm.admin_pass" autocomplete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="isLoading" @click="submitForm('ruleForm')">登录</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import axios from "axios";
import qs from "qs";
import {setToken, setUser} from "@/plugins/token";
export default {
name: "Login",
mounted() {
// login
console.log("login created")
//
window.addEventListener('keydown', this.keyDown);
},
destroyed(){
window.removeEventListener('keydown',this.keyDown,false);
},
data() {
return {
ruleForm: {},
rules: {
admin_user: [
{required: true, message: '请输入用户名', trigger: 'blur'},
{max: 50, message: '长度小于 50 个字符', trigger: 'blur'}
],
admin_pass: [
{required: true, message: '请输入密码', trigger: 'blur'},
{min: 6, message: '长度大于 6 个字符', trigger: 'blur'}
],
},
}
},
methods: {
keyDown(e) {
//
if (e.keyCode === 13) {
this.submitForm('ruleForm');
}
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (!valid) {
console.log('error submit!!');
return false;
}
this.isLoading = true
// alert('submit!');
axios.post('/base/login', qs.stringify(this.ruleForm)).then(resp => {
var rdata = resp.data
if (rdata.code === 0) {
this.$message.success(rdata.msg);
setToken(rdata.data.token)
setUser(rdata.data.admin_user)
this.$router.push("/home");
} else {
this.$message.error(rdata.msg);
}
console.log(rdata);
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
}).finally(() => {
this.isLoading = false
}
);
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
},
},
}
</script>
<style scoped>
.login {
/*border: 1px solid red;*/
height: 100%;
/*margin: 0 auto;*/
text-align: center;
display: flex;
justify-content: center;
align-items: center;
}
.issuer {
font-size: 26px;
font-weight: bold;
margin-bottom: 50px;
}
</style>

View File

@ -0,0 +1,447 @@
<template>
<div>
<el-card>
<el-form :inline="true">
<el-form-item>
<el-button
size="small"
type="primary"
icon="el-icon-plus"
@click="handleEdit('')">添加
</el-button>
</el-form-item>
</el-form>
<el-table
ref="multipleTable"
:data="tableData"
border>
<el-table-column
sortable="true"
prop="id"
label="ID"
width="60">
</el-table-column>
<el-table-column
prop="name"
label="组名">
</el-table-column>
<el-table-column
prop="note"
label="备注">
</el-table-column>
<el-table-column
prop="allow_lan"
label="本地网络">
<template slot-scope="scope">
<el-switch
v-model="scope.row.allow_lan"
disabled>
</el-switch>
</template>
</el-table-column>
<el-table-column
prop="bandwidth"
label="带宽限制">
</el-table-column>
<el-table-column
prop="client_dns"
label="客户端DNS"
width="160">
<template slot-scope="scope">
<el-row v-for="(item,inx) in scope.row.client_dns" :key="inx">{{ item.val }}</el-row>
</template>
</el-table-column>
<el-table-column
prop="route_include"
label="路由包含"
width="200">
<template slot-scope="scope">
<el-row v-for="(item,inx) in scope.row.route_include" :key="inx">{{ item.val }}</el-row>
</template>
</el-table-column>
<el-table-column
prop="route_exclude"
label="路由排除"
width="200">
<template slot-scope="scope">
<el-row v-for="(item,inx) in scope.row.route_exclude" :key="inx">{{ item.val }}</el-row>
</template>
</el-table-column>
<el-table-column
prop="link_acl"
label="LINK-ACL"
min-width="200">
<template slot-scope="scope">
<el-row v-for="(item,inx) in scope.row.link_acl" :key="inx">
{{ item.action }} => {{ item.val }} : {{ item.port }}
</el-row>
</template>
</el-table-column>
<el-table-column
prop="status"
label="状态"
width="70">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === 1" type="success">可用</el-tag>
<el-tag v-else type="danger">停用</el-tag>
</template>
</el-table-column>
<el-table-column
prop="updated_at"
label="更新时间"
:formatter="tableDateFormat">
</el-table-column>
<el-table-column
label="操作"
width="150">
<template slot-scope="scope">
<el-button
size="mini"
type="primary"
@click="handleEdit(scope.row)">编辑
</el-button>
<el-popconfirm
style="margin-left: 10px"
@onConfirm="handleDel(scope.row)"
title="确定要删除用户吗?">
<el-button
slot="reference"
size="mini"
type="danger">删除
</el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div class="sh-20"></div>
<el-pagination
background
layout="prev, pager, next"
:pager-count="11"
@current-change="pageChange"
:current-page="page"
:total="count">
</el-pagination>
</el-card>
<!--新增修改弹出框-->
<el-dialog
:close-on-click-modal="false"
title="用户组"
:visible.sync="user_edit_dialog"
width="750px"
center>
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm">
<el-form-item label="用户组ID" prop="id">
<el-input v-model="ruleForm.id" disabled></el-input>
</el-form-item>
<el-form-item label="组名" prop="name">
<el-input v-model="ruleForm.name" :disabled="ruleForm.id > 0"></el-input>
</el-form-item>
<el-form-item label="备注" prop="note">
<el-input v-model="ruleForm.note"></el-input>
</el-form-item>
<el-form-item label="带宽限制" prop="bandwidth">
<el-input v-model.number="ruleForm.bandwidth">
<template slot="append">BYTE</template>
</el-input>
</el-form-item>
<el-form-item label="本地网络" prop="allow_lan">
<el-switch
v-model="ruleForm.allow_lan">
</el-switch>
</el-form-item>
<el-form-item label="客户端DNS" prop="client_dns">
<el-row class="msg-info">
<el-col :span="20">输入IP格式如: 192.168.0.10</el-col>
<el-col :span="4">
<el-button size="mini" type="success" icon="el-icon-plus" circle
@click.prevent="addDomain(ruleForm.client_dns)"></el-button>
<el-button size="mini" type="danger" icon="el-icon-minus" circle
@click.prevent="removeDomain(ruleForm.client_dns)"></el-button>
</el-col>
</el-row>
<el-row v-for="(item,index) in ruleForm.client_dns"
:key="index" style="margin-bottom: 5px" gutter="10">
<el-col :span="10">
<el-input v-model="item.val"></el-input>
</el-col>
<el-col :span="14">
<el-input v-model="item.note" placeholder="备注"></el-input>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="包含路由" prop="route_include">
<el-row class="msg-info">
<el-col :span="20">输入CIDR格式如: 192.168.1.0/24</el-col>
<el-col :span="4">
<el-button size="mini" type="success" icon="el-icon-plus" circle
@click.prevent="addDomain(ruleForm.route_include)"></el-button>
<el-button size="mini" type="danger" icon="el-icon-minus" circle
@click.prevent="removeDomain(ruleForm.route_include)"></el-button>
</el-col>
</el-row>
<el-row v-for="(item,index) in ruleForm.route_include"
:key="index" style="margin-bottom: 5px" gutter="10">
<el-col :span="10">
<el-input v-model="item.val"></el-input>
</el-col>
<el-col :span="14">
<el-input v-model="item.note" placeholder="备注"></el-input>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="排除路由" prop="route_exclude">
<el-row class="msg-info">
<el-col :span="20">输入CIDR格式如: 192.168.2.0/24</el-col>
<el-col :span="4">
<el-button size="mini" type="success" icon="el-icon-plus" circle
@click.prevent="addDomain(ruleForm.route_exclude)"></el-button>
<el-button size="mini" type="danger" icon="el-icon-minus" circle
@click.prevent="removeDomain(ruleForm.route_exclude)"></el-button>
</el-col>
</el-row>
<el-row v-for="(item,index) in ruleForm.route_exclude"
:key="index" style="margin-bottom: 5px" gutter="10">
<el-col :span="10">
<el-input v-model="item.val"></el-input>
</el-col>
<el-col :span="14">
<el-input v-model="item.note" placeholder="备注"></el-input>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="权限控制" prop="link_acl">
<el-row class="msg-info">
<el-col :span="20">输入CIDR格式如: 192.168.3.0/24 端口0表示所有端口</el-col>
<el-col :span="4">
<el-button size="mini" type="success" icon="el-icon-plus" circle
@click.prevent="addDomain(ruleForm.link_acl)"></el-button>
<el-button size="mini" type="danger" icon="el-icon-minus" circle
@click.prevent="removeDomain(ruleForm.link_acl)"></el-button>
</el-col>
</el-row>
<el-row v-for="(item,index) in ruleForm.link_acl"
:key="index" style="margin-bottom: 5px" gutter="5">
<el-col :span="11">
<el-input placeholder="请输入CIDR地址" v-model="item.val">
<el-select v-model="item.action" slot="prepend">
<el-option label="允许" value="allow"></el-option>
<el-option label="禁止" value="deny"></el-option>
</el-select>
</el-input>
</el-col>
<el-col :span="3">
<el-input v-model.number="item.port" placeholder="端口"></el-input>
</el-col>
<el-col :span="10">
<el-input v-model="item.note" placeholder="备注"></el-input>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="ruleForm.status">
<el-radio :label="1" border>启用</el-radio>
<el-radio :label="0" border>停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">保存</el-button>
<el-button @click="disVisible">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "List",
components: {},
mixins: [],
created() {
this.$emit('update:route_path', this.$route.path)
this.$emit('update:route_name', ['用户组信息', '用户组列表'])
},
mounted() {
this.getData(1)
},
data() {
return {
page: 1,
tableData: [],
count: 10,
ruleForm: {
bandwidth: 0,
status: 1,
allow_lan: true,
client_dns: [{val: '114.114.114.114'}],
route_include: [],
route_exclude: [],
link_acl: [],
},
rules: {
name: [
{required: true, message: '请输入用户名', trigger: 'blur'},
{max: 30, message: '长度小于 30 个字符', trigger: 'blur'}
],
bandwidth: [
{required: true, message: '请输入用户姓名', trigger: 'blur'},
{type: 'number', message: '年龄必须为数字值'}
],
email: [
{required: true, message: '请输入用户邮箱', trigger: 'blur'},
{type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change']}
],
status: [
{required: true}
],
},
}
},
methods: {
handleDel(row) {
axios.post('/group/del?id=' + row.id).then(resp => {
const rdata = resp.data;
if (rdata.code === 0) {
this.$message.success(rdata.msg);
this.getData(1);
} else {
this.$message.error(rdata.msg);
}
console.log(rdata);
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
handleEdit(row) {
!this.$refs['ruleForm'] || this.$refs['ruleForm'].resetFields();
console.log(row)
this.user_edit_dialog = true
if (!row) {
return;
}
axios.get('/group/detail', {
params: {
id: row.id,
}
}).then(resp => {
this.ruleForm = resp.data.data
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
pageChange(p) {
this.getData(p)
},
getData(page) {
this.page = page
axios.get('/group/list', {
params: {
page: page,
}
}).then(resp => {
const rdata = resp.data.data;
console.log(rdata);
this.tableData = rdata.datas;
this.count = rdata.count
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
removeDomain(arr, item) {
console.log(item)
// let index = arr.indexOf(item);
// if (index !== -1 && arr.length > 1) {
// arr.splice(index, 1)
// }
arr.pop()
},
addDomain(arr) {
arr.push({val: "", action: "allow", port: 0});
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (!valid) {
console.log('error submit!!');
return false;
}
axios.post('/group/set', this.ruleForm).then(resp => {
const rdata = resp.data;
if (rdata.code === 0) {
this.$message.success(rdata.msg);
this.getData(1);
} else {
this.$message.error(rdata.msg);
}
console.log(rdata);
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
},
}
</script>
<style scoped>
.msg-info {
background-color: #f4f4f5;
color: #909399;
padding: 0 5px;
margin: 0;
box-sizing: border-box;
border-radius: 4px;
font-size: 12px;
}
.el-select {
width: 80px;
}
</style>

183
web/src/pages/set/Other.vue Normal file
View File

@ -0,0 +1,183 @@
<template>
<el-card>
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane label="邮件配置" name="dataSmtp">
<el-form :model="dataSmtp" ref="dataSmtp" :rules="rules" label-width="100px" class="tab-one">
<el-form-item label="服务器地址" prop="host">
<el-input v-model="dataSmtp.host"></el-input>
</el-form-item>
<el-form-item label="服务器端口" prop="port">
<el-input v-model.number="dataSmtp.port"></el-input>
</el-form-item>
<el-form-item label="用户名" prop="username">
<el-input v-model="dataSmtp.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="dataSmtp.password"></el-input>
</el-form-item>
<el-form-item label="启用SSL" prop="use_ssl">
<el-switch v-model="dataSmtp.use_ssl"></el-switch>
</el-form-item>
<el-form-item label="邮件from" prop="from">
<el-input v-model="dataSmtp.from"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('dataSmtp')">保存</el-button>
<el-button @click="resetForm('dataSmtp')">重置</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
<el-tab-pane label="其他设置" name="dataOther">
<el-form :model="dataOther" ref="dataOther" :rules="rules" label-width="100px" class="tab-one">
<el-form-item label="Banner信息" prop="banner">
<el-input
type="textarea"
:rows="5"
placeholder="请输入内容"
v-model="dataOther.banner">
</el-input>
</el-form-item>
<el-form-item label="账户开通邮件" prop="account_mail">
<el-input
type="textarea"
:rows="10"
placeholder="请输入内容"
v-model="dataOther.account_mail">
</el-input>
</el-form-item>
<el-form-item label="邮件展示">
<iframe
width="500px"
height="300px"
:srcdoc="dataOther.account_mail">
</iframe>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('dataOther')">保存</el-button>
<el-button @click="resetForm('dataOther')">重置</el-button>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</el-card>
</template>
<script>
import axios from "axios";
export default {
name: "Other",
created() {
this.$emit('update:route_path', this.$route.path)
this.$emit('update:route_name', ['基础信息', '其他设置'])
},
mounted() {
this.getSmtp()
},
data() {
return {
activeName: 'dataSmtp',
dataSmtp: {},
dataOther: {},
rules: {
host: {required: true, message: '请输入服务器地址', trigger: 'blur'},
port: [
{required: true, message: '请输入服务器端口', trigger: 'blur'},
{type: 'number', message: '请输入正确的服务器端口', trigger: ['blur', 'change']}
],
issuer: {required: true, message: '请输入系统名称', trigger: 'blur'},
},
};
},
methods: {
handleClick(tab, event) {
window.console.log(tab.name, event);
switch (tab.name) {
case "dataSmtp":
this.getSmtp()
break
case "dataOther":
this.getOther()
break
}
},
getSmtp() {
axios.get('/set/other/smtp').then(resp => {
let rdata = resp.data
console.log(rdata)
if (rdata.code !== 0) {
this.$message.error(rdata.msg);
return;
}
this.dataSmtp = rdata.data
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
getOther() {
axios.get('/set/other').then(resp => {
let rdata = resp.data
console.log(rdata)
if (rdata.code !== 0) {
this.$message.error(rdata.msg);
return;
}
this.dataOther = rdata.data
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (!valid) {
alert('error submit!');
}
switch (formName) {
case "dataSmtp":
axios.post('/set/other/smtp/edit', this.dataSmtp).then(resp => {
var rdata = resp.data
console.log(rdata);
if (rdata.code === 0) {
this.$message.success(rdata.msg);
} else {
this.$message.error(rdata.msg);
}
})
break;
case "dataOther":
axios.post('/set/other/edit', this.dataOther).then(resp => {
var rdata = resp.data
console.log(rdata);
if (rdata.code === 0) {
this.$message.success(rdata.msg);
} else {
this.$message.error(rdata.msg);
}
})
break;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
},
}
</script>
<style scoped>
.tab-one {
width: 600px;
}
</style>

View File

@ -0,0 +1,64 @@
<template>
<el-card>
<el-table
:data="soft_data"
border>
<el-table-column
prop="info"
label="信息"
width="260">
</el-table-column>
<el-table-column
prop="name"
label="配置"
width="200">
</el-table-column>
<el-table-column
prop="data"
label="数据">
<template slot-scope="scope">
{{ scope.row.data }}
</template>
</el-table-column>
</el-table>
</el-card>
</template>
<script>
import axios from "axios";
export default {
name: "Soft",
created() {
this.$emit('update:route_path', this.$route.path)
this.$emit('update:route_name', ['基础信息', '软件配置'])
},
mounted() {
this.getSoftInfo()
},
data() {
return {soft_data: []}
},
methods: {
getSoftInfo() {
axios.get('/set/soft', {}).then(resp => {
var data = resp.data
console.log(data);
this.soft_data = data.data;
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
}
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,133 @@
<template>
<div>
<el-row :gutter="10" class="mb10">
<el-col :span="8">
<el-card v-if="system.cpu" body-style="text-align: center;">
<div slot="header">
<span>CPU使用率</span>
</div>
<el-progress type="circle" :percentage="system.cpu.percent" style="margin-bottom: 20px"/>
<Cell left="CPU主频" :right="system.cpu.ghz" divider/>
<Cell left="系统负载" :right="system.sys.load"/>
</el-card>
</el-col>
<el-col :span="8">
<el-card v-if="system.mem" body-style="text-align: center;">
<div slot="header">
<span>内存使用率</span>
</div>
<el-progress type="circle" :percentage="system.mem.percent" style="margin-bottom: 20px"/>
<Cell left="总内存" :right="system.mem.total" divider/>
<Cell left="剩余内存" :right="system.mem.free"/>
</el-card>
</el-col>
<el-col :span="8">
<el-card v-if="system.disk" body-style="text-align: center;">
<div slot="header">
<span>磁盘信息</span>
</div>
<el-progress type="circle" :percentage="system.disk.percent" style="margin-bottom: 20px"/>
<Cell left="总存储" :right="system.disk.total" divider/>
<Cell left="剩余存储" :right="system.disk.free"/>
</el-card>
</el-col>
</el-row>
<el-card v-if="system.sys" style="margin-top: 10px">
<div slot="header">
<span>go运行环境</span>
</div>
<Cell left="GO版本" :right="system.sys.goOs" divider/>
<Cell left="GoArch" :right="system.sys.goArch" divider/>
<Cell left="GoVersion" :right="system.sys.goVersion" divider/>
<Cell left="Goroutine" :right="system.sys.goroutine"/>
</el-card>
<el-card v-if="system.sys" style="margin-top: 10px">
<div slot="header">
<span>服务器信息</span>
</div>
<Cell left="机器名称" :right="system.sys.hostname" divider/>
<Cell left="操作系统" :right="system.sys.platform" divider/>
<Cell left="内核版本" :right="system.sys.kernel" divider/>
<Cell left="CPU核心" :right="system.cpu.core" divider/>
<Cell left="CPU" :right="system.cpu.modelName"/>
</el-card>
</div>
</template>
<script>
import Cell from "@/components/Cell";
import axios from "axios";
export default {
name: 'Monitor',
components: {Cell},
created() {
this.$emit('update:route_path', this.$route.path)
this.$emit('update:route_name', ['基础信息', '系统信息'])
},
mounted() {
this.getData();
// const chatTimer = setInterval(() => {
// this.getData();
// }, 2000);
//
// this.$once('hook:beforeDestroy', () => {
// clearInterval(chatTimer);
// });
},
data() {
return {system: {}}
},
methods: {
getData() {
axios.get('/set/system', {}).then(resp => {
var data = resp.data.data
console.log(data);
this.system = data;
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
}
}
}
</script>
<style scoped>
.monitor {
display: flex;
justify-content: space-between;
align-items: center;
}
.monitor-left {
font-size: 14px;
}
.monitor-right {
font-size: 12px;
color: #909399;
}
</style>

View File

@ -0,0 +1,273 @@
<template>
<div>
<el-card>
<el-form :inline="true">
<el-form-item>
<el-button
size="small"
type="primary"
icon="el-icon-plus"
@click="handleEdit('')">添加
</el-button>
</el-form-item>
</el-form>
<el-table
ref="multipleTable"
:data="tableData"
border>
<el-table-column
sortable="true"
prop="id"
label="ID"
width="60">
</el-table-column>
<el-table-column
prop="ip_addr"
label="IP地址">
</el-table-column>
<el-table-column
prop="mac_addr"
label="MAC地址">
</el-table-column>
<el-table-column
prop="username"
label="用户名">
</el-table-column>
<el-table-column
prop="keep"
label="IP保留">
<template slot-scope="scope">
<!-- <el-tag v-if="scope.row.keep" type="success">保留</el-tag>-->
<el-switch
disabled
v-model="scope.row.keep"
active-color="#13ce66">
</el-switch>
</template>
</el-table-column>
<el-table-column
prop="note"
label="备注">
</el-table-column>
<el-table-column
prop="last_login"
label="最后登陆时间"
:formatter="tableDateFormat">
</el-table-column>
<el-table-column
label="操作"
width="150">
<template slot-scope="scope">
<el-button
size="mini"
type="primary"
@click="handleEdit(scope.row)">编辑
</el-button>
<el-popconfirm
class="m-left-10"
@onConfirm="handleDel(scope.row)"
title="确定要删除IP映射吗">
<el-button
slot="reference"
size="mini"
type="danger"
@click="handleDelete(scope.row)">删除
</el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div class="sh-20"></div>
<el-pagination
background
layout="prev, pager, next"
:pager-count="11"
@current-change="pageChange"
:total="count">
</el-pagination>
</el-card>
<!--新增修改弹出框-->
<el-dialog
title="提示"
:close-on-click-modal="false"
:visible="user_edit_dialog"
@close="disVisible"
width="600px"
center>
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm">
<el-form-item label="ID" prop="id">
<el-input v-model="ruleForm.id" disabled></el-input>
</el-form-item>
<el-form-item label="IP地址" prop="ip_addr">
<el-input v-model="ruleForm.ip_addr"></el-input>
</el-form-item>
<el-form-item label="MAC地址" prop="mac_addr">
<el-input v-model="ruleForm.mac_addr"></el-input>
</el-form-item>
<el-form-item label="用户名" prop="username">
<el-input v-model="ruleForm.username"></el-input>
</el-form-item>
<el-form-item label="备注" prop="note">
<el-input v-model="ruleForm.note"></el-input>
</el-form-item>
<el-form-item label="IP保留" prop="keep">
<el-switch
v-model="ruleForm.keep"
active-color="#13ce66">
</el-switch>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">保存</el-button>
<el-button @click="disVisible">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "IpMap",
components: {},
mixins:[],
created() {
this.$emit('update:route_path', this.$route.path)
this.$emit('update:route_name', ['用户信息', 'IP映射'])
},
mounted() {
this.getData(1)
},
data() {
return {
tableData: [],
count: 10,
nowIndex: 0,
ruleForm: {
status: 1,
groups: [],
},
rules: {
username: [
{required: false, message: '请输入用户名', trigger: 'blur'},
{max: 50, message: '长度小于 50 个字符', trigger: 'blur'}
],
mac_addr: [
{required: true, message: '请输入mac地址', trigger: 'blur'}
],
ip_addr: [
{required: true, message: '请输入ip地址', trigger: 'blur'}
],
status: [
{required: true}
],
},
}
},
methods: {
getData(p) {
axios.get('/user/ip_map/list', {
params: {
page: p,
}
}).then(resp => {
var data = resp.data.data
console.log(data);
this.tableData = data.datas;
this.count = data.count
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
pageChange(p) {
this.getData(p)
},
handleEdit(row) {
!this.$refs['ruleForm'] || this.$refs['ruleForm'].resetFields();
console.log(row)
this.user_edit_dialog = true
if (!row) {
return;
}
axios.get('/user/ip_map/detail', {
params: {
id: row.id,
}
}).then(resp => {
this.ruleForm = resp.data.data
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
handleDel(row) {
axios.post('/user/ip_map/del?id=' + row.id).then(resp => {
var rdata = resp.data
if (rdata.code === 0) {
this.$message.success(rdata.msg);
this.getData(1);
} else {
this.$message.error(rdata.msg);
}
console.log(rdata);
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (!valid) {
console.log('error submit!!');
return false;
}
// alert('submit!');
axios.post('/user/ip_map/set', this.ruleForm).then(resp => {
var rdata = resp.data
if (rdata.code === 0) {
this.$message.success(rdata.msg);
this.getData(1);
} else {
this.$message.error(rdata.msg);
}
console.log(rdata);
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
});
},
},
}
</script>
<style scoped>
</style>

410
web/src/pages/user/List.vue Normal file
View File

@ -0,0 +1,410 @@
<template>
<div>
<el-card>
<el-form :inline="true">
<el-form-item>
<el-button
size="small"
type="primary"
icon="el-icon-plus"
@click="handleEdit('')">添加
</el-button>
</el-form-item>
<el-form-item label="用户名:">
<el-input size="small" v-model="searchData" placeholder="请输入内容"></el-input>
</el-form-item>
<el-form-item>
<el-button
size="small"
type="primary"
icon="el-icon-search"
@click="handleSearch()">搜索
</el-button>
<el-button
size="small"
icon="el-icon-refresh"
@click="searchData=''">重置搜索
</el-button>
</el-form-item>
</el-form>
<el-table
ref="multipleTable"
:data="tableData"
border>
<el-table-column
sortable="true"
prop="id"
label="ID"
width="60">
</el-table-column>
<el-table-column
prop="username"
label="用户名"
width="150">
</el-table-column>
<el-table-column
prop="nickname"
label="姓名"
width="100">
</el-table-column>
<el-table-column
prop="email"
label="邮箱">
</el-table-column>
<el-table-column
prop="otp_secret"
label="OTP密钥"
width="110">
<template slot-scope="scope">
<el-button
v-if="!scope.row.disable_otp"
type="text"
icon="el-icon-view"
@click="getOtpImg(scope.row)">
{{ scope.row.otp_secret.substring(0, 6) }}
</el-button>
</template>
</el-table-column>
<el-table-column
prop="groups"
label="用户组">
<template slot-scope="scope">
<el-row v-for="item in scope.row.groups" :key="item">{{ item }}</el-row>
</template>
</el-table-column>
<el-table-column
prop="status"
label="状态"
width="70">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === 1" type="success">可用</el-tag>
<el-tag v-else type="danger">停用</el-tag>
</template>
</el-table-column>
<el-table-column
prop="updated_at"
label="更新时间"
:formatter="tableDateFormat">
</el-table-column>
<el-table-column
label="操作"
width="210">
<template slot-scope="scope">
<el-button
size="mini"
type="primary"
@click="handleEdit(scope.row)">编辑
</el-button>
<!-- <el-popconfirm
class="m-left-10"
@onConfirm="handleClick('reset',scope.row)"
title="确定要重置用户密码和密钥吗?">
<el-button
slot="reference"
size="mini"
type="warning">重置
</el-button>
</el-popconfirm>-->
<el-popconfirm
class="m-left-10"
@onConfirm="handleDel(scope.row)"
title="确定要删除用户吗?">
<el-button
slot="reference"
size="mini"
type="danger">删除
</el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div class="sh-20"></div>
<el-pagination
background
layout="prev, pager, next"
:pager-count="11"
@current-change="pageChange"
:current-page="page"
:total="count">
</el-pagination>
</el-card>
<el-dialog
title="OTP密钥"
:visible.sync="otpImgData.visible"
width="350px"
center>
<div style="text-align: center">{{ otpImgData.username }} : {{ otpImgData.nickname }}</div>
<img :src="otpImgData.base64Img" alt="otp-img"/>
</el-dialog>
<!--新增修改弹出框-->
<el-dialog
:close-on-click-modal="false"
title="用户"
:visible="user_edit_dialog"
@close="disVisible"
width="650px"
center>
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="ruleForm">
<el-form-item label="用户ID" prop="id">
<el-input v-model="ruleForm.id" disabled></el-input>
</el-form-item>
<el-form-item label="用户名" prop="username">
<el-input v-model="ruleForm.username" :disabled="ruleForm.id > 0"></el-input>
</el-form-item>
<el-form-item label="姓名" prop="nickname">
<el-input v-model="ruleForm.nickname"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="ruleForm.email"></el-input>
</el-form-item>
<el-form-item label="PIN码" prop="pin_code">
<el-input v-model="ruleForm.pin_code" placeholder="不填由系统自动生成"></el-input>
</el-form-item>
<el-form-item label="禁用OTP" prop="disable_otp">
<el-switch
v-model="ruleForm.disable_otp">
</el-switch>
</el-form-item>
<el-form-item label="OTP密钥" prop="otp_secret" v-if="!ruleForm.disable_otp">
<el-input v-model="ruleForm.otp_secret" placeholder="不填由系统自动生成"></el-input>
</el-form-item>
<el-form-item label="用户组" prop="groups">
<el-checkbox-group v-model="ruleForm.groups">
<el-checkbox v-for="(item) in grouNames" :key="item" :label="item" :name="item"></el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="发送邮件" prop="send_email">
<el-switch
v-model="ruleForm.send_email">
</el-switch>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="ruleForm.status">
<el-radio :label="1" border>启用</el-radio>
<el-radio :label="0" border>停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">保存</el-button>
<!-- <el-button @click="resetForm('ruleForm')">重置</el-button>-->
<el-button @click="disVisible">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "List",
components: {},
mixins: [],
created() {
this.$emit('update:route_path', this.$route.path)
this.$emit('update:route_name', ['用户信息', '用户列表'])
},
mounted() {
this.getGroups();
this.getData(1)
},
data() {
return {
page: 1,
grouNames: [],
tableData: [],
count: 10,
searchData: '',
otpImgData: {visible: false, username: '', nickname: '', base64Img: ''},
ruleForm: {
send_email: true,
status: 1,
groups: [],
},
rules: {
username: [
{required: true, message: '请输入用户名', trigger: 'blur'},
{max: 50, message: '长度小于 50 个字符', trigger: 'blur'}
],
nickname: [
{required: true, message: '请输入用户姓名', trigger: 'blur'}
],
email: [
{required: true, message: '请输入用户邮箱', trigger: 'blur'},
{type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change']}
],
password: [
{min: 6, message: '长度大于 6 个字符', trigger: 'blur'}
],
pin_code: [
{min: 6, message: 'PIN码大于 6 个字符', trigger: 'blur'}
],
date1: [
{type: 'date', required: true, message: '请选择日期', trigger: 'change'}
],
groups: [
{type: 'array', required: true, message: '请至少选择一个组', trigger: 'change'}
],
status: [
{required: true}
],
},
}
},
methods: {
getOtpImg(row) {
// this.base64Img = Buffer.from(data).toString('base64');
this.otpImgData.visible = true
axios.get('/user/otp_qr', {
params: {
id: row.id,
b64: '1',
}
}).then(resp => {
var rdata = resp.data;
// console.log(resp);
this.otpImgData.username = row.username;
this.otpImgData.nickname = row.nickname;
this.otpImgData.base64Img = 'data:image/png;base64,' + rdata
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
handleDel(row) {
axios.post('/user/del?id=' + row.id).then(resp => {
var rdata = resp.data
if (rdata.code === 0) {
this.$message.success(rdata.msg);
this.getData(1);
} else {
this.$message.error(rdata.msg);
}
console.log(rdata);
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
handleEdit(row) {
!this.$refs['ruleForm'] || this.$refs['ruleForm'].resetFields();
console.log(row)
this.user_edit_dialog = true
if (!row) {
return;
}
axios.get('/user/detail', {
params: {
id: row.id,
}
}).then(resp => {
var data = resp.data.data
//
data.send_email = false
this.ruleForm = data
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
handleSearch() {
console.log(this.searchData)
this.getData(1, this.searchData)
},
pageChange(p) {
this.getData(p)
},
getData(page, prefix) {
this.page = page
axios.get('/user/list', {
params: {
page: page,
prefix: prefix || '',
}
}).then(resp => {
var data = resp.data.data
console.log(data);
this.tableData = data.datas;
this.count = data.count
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
getGroups() {
axios.get('/group/names', {}).then(resp => {
var data = resp.data.data
console.log(data.datas);
this.grouNames = data.datas;
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (!valid) {
console.log('error submit!!');
return false;
}
// alert('submit!');
axios.post('/user/set', this.ruleForm).then(resp => {
var data = resp.data
if (data.code === 0) {
this.$message.success(data.msg);
this.getData(1);
} else {
this.$message.error(data.msg);
}
console.log(data);
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,196 @@
<template>
<div>
<el-card>
<el-table
ref="multipleTable"
:data="tableData"
border>
<el-table-column
sortable="true"
type="index"
label="序号"
width="50">
</el-table-column>
<el-table-column
prop="username"
label="用户名">
</el-table-column>
<el-table-column
prop="group"
label="登陆组">
</el-table-column>
<el-table-column
prop="mac_addr"
label="MAC地址">
</el-table-column>
<el-table-column
prop="ip"
label="IP地址"
width="140">
</el-table-column>
<el-table-column
prop="remote_addr"
label="远端地址">
</el-table-column>
<el-table-column
prop="tun_name"
label="虚拟网卡">
</el-table-column>
<el-table-column
prop="mtu"
label="MTU">
</el-table-column>
<el-table-column
prop="is_mobile"
label="客户端">
<template slot-scope="scope">
<i v-if="scope.row.client === 'mobile'" class="el-icon-mobile-phone" style="font-size: 20px;color: red"></i>
<i v-else class="el-icon-s-platform" style="font-size: 20px;color: blue"></i>
</template>
</el-table-column>
<el-table-column
prop="status"
label="实时 上行/下行"
width="220">
<template slot-scope="scope">
<el-tag type="success">{{ scope.row.bandwidth_up }}</el-tag>
<el-tag class="m-left-10">{{ scope.row.bandwidth_down }}</el-tag>
</template>
</el-table-column>
<el-table-column
prop="status"
label="总量 上行/下行"
width="200">
<template slot-scope="scope">
<el-tag effect="dark" type="success">{{ scope.row.bandwidth_up_all }}</el-tag>
<el-tag class="m-left-10" effect="dark">{{ scope.row.bandwidth_down_all }}</el-tag>
</template>
</el-table-column>
<el-table-column
prop="last_login"
label="登陆时间"
:formatter="tableDateFormat">
</el-table-column>
<el-table-column
label="操作"
width="150">
<template slot-scope="scope">
<el-button
size="mini"
type="primary"
@click="handleReline(scope.row)">重连
</el-button>
<el-popconfirm
class="m-left-10"
@onConfirm="handleOffline(scope.row)"
title="确定要下线用户吗?">
<el-button
slot="reference"
size="mini"
type="danger">下线
</el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "Online",
components: {},
mixins: [],
created() {
this.$emit('update:route_path', this.$route.path)
this.$emit('update:route_name', ['用户信息', '在线用户'])
},
mounted() {
this.getData();
const chatTimer = setInterval(() => {
this.getData();
}, 2000);
this.$once('hook:beforeDestroy', () => {
clearInterval(chatTimer);
})
},
data() {
return {
tableData: [],
}
},
methods: {
handleOffline(row) {
axios.post('/user/offline?token=' + row.token).then(resp => {
var data = resp.data
if (data.code === 0) {
this.$message.success(data.msg);
this.getData();
} else {
this.$message.error(data.msg);
}
console.log(data);
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
handleReline(row) {
axios.post('/user/reline?token=' + row.token).then(resp => {
var data = resp.data
if (data.code === 0) {
this.$message.success(data.msg);
this.getData();
} else {
this.$message.error(data.msg);
}
console.log(data);
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
handleEdit(a, row) {
console.log(a, row)
},
getData() {
axios.get('/user/online').then(resp => {
var data = resp.data.data
console.log(data);
this.tableData = data.datas;
this.count = data.count
}).catch(error => {
this.$message.error('哦,请求出错');
console.log(error);
});
},
},
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,5 @@
import Vue from 'vue'
import Element from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(Element)

Some files were not shown because too many files have changed in this diff Show More