Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d391d202f | ||
|
|
b3dd1549db | ||
| 2c6c0c0e2a | |||
|
|
1dc677f3a0 | ||
|
|
2f9a4b5f6c | ||
|
|
d4f9204b24 | ||
|
|
6a71ca56d0 | ||
|
|
9c560ff352 | ||
|
|
06df5fa048 | ||
|
|
9497729378 | ||
|
|
48a58759da | ||
|
|
36f0202c57 | ||
|
|
904907f796 | ||
|
|
9674796f0a | ||
|
|
b245c26515 |
87
.gitea/workflows/ci.yml
Normal file
87
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
env:
|
||||||
|
GO_VERSION: "1.23.0"
|
||||||
|
NODE_VERSION: "18"
|
||||||
|
PNPM_VERSION: "8.6.10"
|
||||||
|
REGISTRY: code.mrx.ltd
|
||||||
|
IMAGE_NAME: code.mrx.ltd/pkg/wireguard-srv
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
backend-check:
|
||||||
|
name: Backend Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Download Go modules
|
||||||
|
run: go mod download
|
||||||
|
|
||||||
|
- name: Run Go test
|
||||||
|
run: go test ./...
|
||||||
|
|
||||||
|
- name: Verify backend build
|
||||||
|
run: go build -o /tmp/wgui .
|
||||||
|
|
||||||
|
frontend-build:
|
||||||
|
name: Frontend Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: web
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: ${{ env.PNPM_VERSION }}
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: web/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
docker-publish:
|
||||||
|
name: Publish Docker Image
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
needs:
|
||||||
|
- backend-check
|
||||||
|
- frontend-build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Login registry
|
||||||
|
run: echo "${{ secrets.DOCKER_PWD }}" | docker login "${{ env.REGISTRY }}" -u "${{ secrets.DOCKER_USER }}" --password-stdin
|
||||||
|
|
||||||
|
- name: Build image
|
||||||
|
run: docker build -t "${{ env.IMAGE_NAME }}:${GITHUB_REF_NAME}" -t "${{ env.IMAGE_NAME }}:latest" .
|
||||||
|
|
||||||
|
- name: Push image
|
||||||
|
run: |
|
||||||
|
docker push "${{ env.IMAGE_NAME }}:${GITHUB_REF_NAME}"
|
||||||
|
docker push "${{ env.IMAGE_NAME }}:latest"
|
||||||
12
Dockerfile
12
Dockerfile
@@ -1,9 +1,9 @@
|
|||||||
# 打包前端
|
# 打包前端
|
||||||
FROM node:18-alpine as build-front
|
FROM node:18-alpine AS build-front
|
||||||
|
|
||||||
WORKDIR front
|
WORKDIR /front
|
||||||
COPY . .
|
COPY . .
|
||||||
WORKDIR web
|
WORKDIR ./web
|
||||||
|
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
RUN corepack prepare pnpm@8.6.10 --activate
|
RUN corepack prepare pnpm@8.6.10 --activate
|
||||||
@@ -13,7 +13,7 @@ RUN pnpm build
|
|||||||
RUN ls -lh && pwd
|
RUN ls -lh && pwd
|
||||||
|
|
||||||
# 前后端集成打包
|
# 前后端集成打包
|
||||||
FROM golang:alpine as build-backend
|
FROM golang:alpine AS build-backend
|
||||||
|
|
||||||
RUN apk add upx
|
RUN apk add upx
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
@@ -23,7 +23,7 @@ COPY --from=build-front /front/web/dist/ /build/dist
|
|||||||
ENV GO111MODULE=on
|
ENV GO111MODULE=on
|
||||||
ENV GOPROXY=https://goproxy.cn,direct
|
ENV GOPROXY=https://goproxy.cn,direct
|
||||||
|
|
||||||
RUN go build -ldflags="-s -w" -o wgui && upx -9 wgui
|
RUN go version && go build -ldflags="-s -w" -o wgui && upx -9 wgui
|
||||||
|
|
||||||
RUN ls -lh && chmod +x ./wgui
|
RUN ls -lh && chmod +x ./wgui
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"wireguard-ui/cli/tui"
|
||||||
|
)
|
||||||
|
|
||||||
func Kernel() error {
|
func Kernel() error {
|
||||||
|
tui.NewApp().Run()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
165
cli/tui/app.go
Normal file
165
cli/tui/app.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
|
"wireguard-ui/component"
|
||||||
|
"wireguard-ui/global/client"
|
||||||
|
"wireguard-ui/global/constant"
|
||||||
|
"wireguard-ui/http/vo"
|
||||||
|
"wireguard-ui/service"
|
||||||
|
"wireguard-ui/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
TokenSecret string // token密钥
|
||||||
|
User *vo.User // 登陆用户
|
||||||
|
Client *ClientComponent // 客户端组件
|
||||||
|
Server *ServerComponent // 服务端组件
|
||||||
|
Setting *SettingComponent // 设置组件
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApp() *App {
|
||||||
|
app := &App{}
|
||||||
|
if _, err := app.Login(); err != nil {
|
||||||
|
fmt.Println("登陆失败: ", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := app.AuthLogin()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("登陆失败: ", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Println("\n=============== 登陆成功 ==============================================================================")
|
||||||
|
app.Client = NewClientComponent(app.User)
|
||||||
|
app.Server = NewServerComponent(app.User)
|
||||||
|
app.Setting = NewSettingComponent(app.User)
|
||||||
|
fmt.Println("=============== 欢迎使用wireguard-tui =================================================================")
|
||||||
|
fmt.Println("=============== 当前用户: ", app.User.Nickname, " ================================================================")
|
||||||
|
fmt.Println("=============== 当前时间: ", time.Now().Format("2006-01-02 15:04:05"), " =======================================================")
|
||||||
|
fmt.Println("=============== 注意事项如下: =========================================================================")
|
||||||
|
fmt.Println("=============== 1. 请确保服务端已经安装wireguard ======================================================")
|
||||||
|
fmt.Println("=============== 2. 请确保服务端和客户端配置文件路径正确 ===============================================")
|
||||||
|
fmt.Println("=============== 3. 请确保服务端和客户端配置文件权限正确 ===============================================")
|
||||||
|
fmt.Println("=============== 4. 请确保服务端和客户端配置文件内容正确 ===============================================")
|
||||||
|
fmt.Println("=============== 5. 请勿泄露配置文件内容 ===============================================================")
|
||||||
|
fmt.Println("=============== 6. 每次修改客户端、服务端配置或者全局配置过后,请使用重启功能重启服务端,以保证生效 ===")
|
||||||
|
fmt.Println("=============== 7. 当使用重启无效时,请手动执行对应命令 ===============================================")
|
||||||
|
fmt.Println("=============== 8. 请勿随意删除客户端,删除后无法恢复 =================================================")
|
||||||
|
fmt.Println("=============== 9. 手动命令 ===========================================================================")
|
||||||
|
fmt.Println("=============== 10. 启动wireguard服务端: wg-quick up wg0 ==============================================")
|
||||||
|
fmt.Println("=============== 11. 停止wireguard服务端: wg-quick down wg0 ============================================")
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Run() {
|
||||||
|
if a == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
|
||||||
|
PrintMenu()
|
||||||
|
chooseMenu := readInput("请选择菜单: ")
|
||||||
|
switch chooseMenu {
|
||||||
|
case "1":
|
||||||
|
a.Client.ConnectList()
|
||||||
|
case "2":
|
||||||
|
a.Client.Menus()
|
||||||
|
case "3":
|
||||||
|
a.Server.Menus()
|
||||||
|
case "4":
|
||||||
|
a.Setting.Menus()
|
||||||
|
case "q":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthLogin
|
||||||
|
// @description: 登陆认证
|
||||||
|
// @receiver a
|
||||||
|
// @return string
|
||||||
|
func (a *App) AuthLogin() error {
|
||||||
|
// 先判断token是否存在
|
||||||
|
tokenStr, err := client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.TUIUserToken, a.User.Id)).Result()
|
||||||
|
if err != nil {
|
||||||
|
// 不存在,去登陆
|
||||||
|
tokenStr, err = a.Login()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存在,不必要再次登陆,解析token
|
||||||
|
claims, err := component.JWT().ParseToken("Bearer "+tokenStr, a.TokenSecret, "tui")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := service.User().GetUserById(claims.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status != constant.Enabled {
|
||||||
|
return errors.New("用户状态异常,请联系管理员处理")
|
||||||
|
}
|
||||||
|
|
||||||
|
a.User = &vo.User{
|
||||||
|
Id: user.Id,
|
||||||
|
Account: user.Account,
|
||||||
|
Nickname: user.Nickname,
|
||||||
|
Avatar: user.Avatar,
|
||||||
|
Contact: user.Contact,
|
||||||
|
IsAdmin: user.IsAdmin,
|
||||||
|
Status: user.Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login
|
||||||
|
// @description: 登陆
|
||||||
|
// @receiver a
|
||||||
|
// @return string
|
||||||
|
func (a *App) Login() (tokenStr string, err error) {
|
||||||
|
fmt.Println("============== 登陆 ==============")
|
||||||
|
|
||||||
|
username := readInput("请输入用户名: ")
|
||||||
|
password := readInput("请输入密码: ")
|
||||||
|
// 验证码正确,查询用户信息
|
||||||
|
user, err := service.User().GetUserByAccount(username)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("用户不存在: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对比密码
|
||||||
|
if !utils.Password().ComparePassword(user.Password, password) {
|
||||||
|
return "", errors.New("密码错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := component.JWT().GenerateSecret(password, uuid.NewString(), time.Now().Local().String())
|
||||||
|
// 生成token
|
||||||
|
token, _, err := component.JWT().GenerateToken(user.Id, secret, "tui")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("登陆失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.User = &vo.User{
|
||||||
|
Id: user.Id,
|
||||||
|
Account: user.Account,
|
||||||
|
Nickname: user.Nickname,
|
||||||
|
Avatar: user.Avatar,
|
||||||
|
Contact: user.Contact,
|
||||||
|
IsAdmin: user.IsAdmin,
|
||||||
|
Status: user.Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
a.TokenSecret = secret
|
||||||
|
|
||||||
|
return "Bearer " + token, nil
|
||||||
|
}
|
||||||
520
cli/tui/client.go
Normal file
520
cli/tui/client.go
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/charmbracelet/bubbles/table"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"wireguard-ui/component"
|
||||||
|
"wireguard-ui/global/constant"
|
||||||
|
"wireguard-ui/http/param"
|
||||||
|
"wireguard-ui/http/vo"
|
||||||
|
"wireguard-ui/service"
|
||||||
|
"wireguard-ui/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientComponent struct {
|
||||||
|
LoginUser *vo.User
|
||||||
|
Menu []string
|
||||||
|
Clients [][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClientComponent(loginUser *vo.User) *ClientComponent {
|
||||||
|
ccp := &ClientComponent{
|
||||||
|
LoginUser: loginUser,
|
||||||
|
Menu: []string{"[1] 客户端列表", "[2] 查看客户端配置", "[3] 添加客户端", "[4] 编辑客户端", "[5] 删除客户端", "[q] 返回上一级菜单"},
|
||||||
|
}
|
||||||
|
|
||||||
|
return ccp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menus
|
||||||
|
// @description: 客户端菜单
|
||||||
|
// @receiver c
|
||||||
|
func (c *ClientComponent) Menus() {
|
||||||
|
fmt.Println("")
|
||||||
|
for _, r := range c.Menu {
|
||||||
|
fmt.Println(" -> " + r)
|
||||||
|
}
|
||||||
|
|
||||||
|
chooseMenu := readInput("\n请选择: ")
|
||||||
|
switch chooseMenu {
|
||||||
|
case "1":
|
||||||
|
c.List(true)
|
||||||
|
case "2":
|
||||||
|
c.ShowConfig()
|
||||||
|
case "3":
|
||||||
|
c.Add()
|
||||||
|
case "4":
|
||||||
|
c.Edit()
|
||||||
|
case "5":
|
||||||
|
c.Delete()
|
||||||
|
case "q":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectList
|
||||||
|
// @description: 客户端链接列表
|
||||||
|
// @receiver c
|
||||||
|
// @return string
|
||||||
|
func (c *ClientComponent) ConnectList() {
|
||||||
|
fmt.Println("\n客户端链接列表")
|
||||||
|
connectList, err := component.Wireguard().GetClients()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("获取客户端链接列表失败: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data [][]string
|
||||||
|
for _, peer := range connectList {
|
||||||
|
// 获取客户端链接信息
|
||||||
|
clientInfo, err := service.Client().GetByPublicKey(peer.PublicKey.String())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var ipAllocation string
|
||||||
|
for _, iaip := range peer.AllowedIPs {
|
||||||
|
ipAllocation += iaip.String() + ","
|
||||||
|
}
|
||||||
|
// 去除一下最右边的逗号
|
||||||
|
if len(ipAllocation) > 0 {
|
||||||
|
ipAllocation = strings.TrimRight(ipAllocation, ",")
|
||||||
|
}
|
||||||
|
var isOnline = "否"
|
||||||
|
if time.Since(peer.LastHandshakeTime).Minutes() < 3 {
|
||||||
|
isOnline = "是"
|
||||||
|
}
|
||||||
|
data = append(data, []string{clientInfo.Name,
|
||||||
|
clientInfo.Email,
|
||||||
|
ipAllocation,
|
||||||
|
isOnline,
|
||||||
|
utils.FlowCalculation().Parse(peer.TransmitBytes),
|
||||||
|
utils.FlowCalculation().Parse(peer.ReceiveBytes),
|
||||||
|
peer.Endpoint.String(),
|
||||||
|
peer.LastHandshakeTime.Format("2006-01-02 15:04:05")})
|
||||||
|
}
|
||||||
|
|
||||||
|
//if len(data) <= 0 {
|
||||||
|
// //data = append(data, []string{"暂无数据"})
|
||||||
|
// // data = append(data, []string{"名称1", "12345678910@qq.com", "192.168.100.1", "是", "10G", "20G", "1.14.30.133:51280", "2024-12-20 15:07:36"}, []string{"名称2", "12345678910@qq.com", "192.168.100.2", "否", "20G", "40G", "1.14.30.133:51280", "2024-12-22 15:07:36"})
|
||||||
|
//}
|
||||||
|
|
||||||
|
title := []table.Column{
|
||||||
|
{
|
||||||
|
Title: "客户端名称",
|
||||||
|
Width: 20,
|
||||||
|
}, {
|
||||||
|
Title: "联系邮箱",
|
||||||
|
Width: 20,
|
||||||
|
}, {
|
||||||
|
Title: "分配的IP",
|
||||||
|
Width: 30,
|
||||||
|
}, {
|
||||||
|
Title: "是否在线",
|
||||||
|
Width: 10,
|
||||||
|
}, {
|
||||||
|
Title: "接收流量",
|
||||||
|
Width: 10,
|
||||||
|
}, {
|
||||||
|
Title: "传输流量",
|
||||||
|
Width: 10,
|
||||||
|
}, {
|
||||||
|
Title: "链接端点",
|
||||||
|
Width: 30,
|
||||||
|
}, {
|
||||||
|
Title: "最后握手时间",
|
||||||
|
Width: 30,
|
||||||
|
}}
|
||||||
|
|
||||||
|
Show(GenerateTable(title, data))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// List
|
||||||
|
// @description: 客户端列表
|
||||||
|
// @receiver c
|
||||||
|
func (c *ClientComponent) List(showMenu bool) {
|
||||||
|
|
||||||
|
title := []table.Column{
|
||||||
|
{
|
||||||
|
Title: "序号",
|
||||||
|
Width: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "ID",
|
||||||
|
Width: 35,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "名称",
|
||||||
|
Width: 25,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "联系邮箱",
|
||||||
|
Width: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "客户端IP",
|
||||||
|
Width: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "状态",
|
||||||
|
Width: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "离线通知",
|
||||||
|
Width: 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
records, _, err := service.Client().List(param.ClientList{
|
||||||
|
Page: param.Page{
|
||||||
|
Current: -1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("获取客户端列表失败: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\n客户端列表")
|
||||||
|
var data [][]string
|
||||||
|
for i, client := range records {
|
||||||
|
var status, offlineNotify string
|
||||||
|
if client.Enabled == 1 {
|
||||||
|
status = "启用"
|
||||||
|
} else {
|
||||||
|
status = "禁用"
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.OfflineMonitoring == 1 {
|
||||||
|
offlineNotify = "开启"
|
||||||
|
} else {
|
||||||
|
offlineNotify = "关闭"
|
||||||
|
}
|
||||||
|
|
||||||
|
data = append(data, []string{
|
||||||
|
cast.ToString(i + 1),
|
||||||
|
client.Id,
|
||||||
|
client.Name,
|
||||||
|
client.Email,
|
||||||
|
client.IpAllocationStr,
|
||||||
|
status,
|
||||||
|
offlineNotify,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Show(GenerateTable(title, data))
|
||||||
|
c.Clients = data
|
||||||
|
if showMenu {
|
||||||
|
c.Menus()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowConfig
|
||||||
|
// @description: 显示配置
|
||||||
|
// @receiver c
|
||||||
|
func (c *ClientComponent) ShowConfig() {
|
||||||
|
c.List(false)
|
||||||
|
|
||||||
|
clientIdx := readInput("请输入客户端序号: ")
|
||||||
|
downloadType := readInput("请输入下载类型(FILE - 文件 | EMAIL - 邮件): ")
|
||||||
|
idx, err := strconv.Atoi(clientIdx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("输入有误: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx < 1 || idx > len(c.Clients) {
|
||||||
|
fmt.Println("输入有误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := c.Clients[idx-1]
|
||||||
|
// 取到id
|
||||||
|
clientID := client[1]
|
||||||
|
// 查询客户端信息
|
||||||
|
clientInfo, err := service.Client().GetByID(clientID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("获取客户端信息失败: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染配置
|
||||||
|
var keys vo.Keys
|
||||||
|
_ = json.Unmarshal([]byte(clientInfo.Keys), &keys)
|
||||||
|
|
||||||
|
globalSet, err := service.Setting().GetWGSetForConfig()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("获取全局配置失败: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serverConf, err := service.Setting().GetWGServerForConfig()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("获取服务器配置失败: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outPath, err := component.Wireguard().GenerateClientFile(clientInfo, serverConf, globalSet)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("生成客户端配置失败: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据不同下载类型执行不同逻辑
|
||||||
|
switch downloadType {
|
||||||
|
case "FILE": // 二维码
|
||||||
|
// 读取文件内容
|
||||||
|
fileContent, err := os.ReadFile(outPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("读取文件失败: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\n#请将以下内容复制到客户端配置文件中【不包含本行提示语】")
|
||||||
|
fmt.Println("\n" + string(fileContent))
|
||||||
|
|
||||||
|
if err = os.Remove(outPath); err != nil {
|
||||||
|
log.Errorf("删除临时文件失败: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
case "EMAIL": // 邮件
|
||||||
|
if clientInfo.Email == "" {
|
||||||
|
fmt.Println("当前客户端并未配置通知邮箱")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取邮箱配置
|
||||||
|
emailConf, err := service.Setting().GetByCode("EMAIL_SMTP")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("获取邮箱配置失败,请先到设置页面的【其他】里面添加code为【EMAIL_SMTP】的具体配置")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = utils.Mail(emailConf).SendMail(clientInfo.Email, fmt.Sprintf("客户端: %s", clientInfo.Name), "请查收附件", outPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("发送邮件失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.Remove(outPath); err != nil {
|
||||||
|
log.Errorf("删除临时文件失败: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("发送邮件成功,请注意查收!")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Menus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add
|
||||||
|
// @description: 添加客户端
|
||||||
|
// @receiver c
|
||||||
|
func (c *ClientComponent) Add() {
|
||||||
|
fmt.Println("\n添加客户端")
|
||||||
|
clientIP, serverIP, err := GenerateIP()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("生成客户端IP失败: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
keys, err := GenerateKeys()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("生成密钥对失败: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var p param.SaveClient
|
||||||
|
p.Name = readInput("请输入客户端名称: ")
|
||||||
|
p.Email = readInput("请输入联系邮箱: ")
|
||||||
|
clientIPIn := readInput("请输入客户端IP(默认自动生成,多个采用 ',' 分割,例如 10.10.0.1/32,10.10.0.2/32): ")
|
||||||
|
if clientIPIn == "" {
|
||||||
|
p.IpAllocation = clientIP
|
||||||
|
} else {
|
||||||
|
p.IpAllocation = strings.Split(clientIPIn, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.AllowedIps = serverIP
|
||||||
|
p.Keys = ¶m.Keys{
|
||||||
|
PrivateKey: keys.PrivateKey,
|
||||||
|
PublicKey: keys.PublicKey,
|
||||||
|
PresharedKey: keys.PresharedKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
var useServerDNS, enabled, offlineNotify constant.Status
|
||||||
|
|
||||||
|
useServerDNSIn := readInput("是否使用服务器DNS(默认不使用 1 - 是 | 0 - 否 ): ")
|
||||||
|
switch useServerDNSIn {
|
||||||
|
case "1":
|
||||||
|
useServerDNS = constant.Enabled
|
||||||
|
case "0":
|
||||||
|
useServerDNS = constant.Disabled
|
||||||
|
default:
|
||||||
|
useServerDNS = constant.Disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledIn := readInput("是否启用(默认启用 1 - 是 | 0 - 否 ): ")
|
||||||
|
switch enabledIn {
|
||||||
|
case "1":
|
||||||
|
enabled = constant.Enabled
|
||||||
|
case "0":
|
||||||
|
enabled = constant.Disabled
|
||||||
|
default:
|
||||||
|
enabled = constant.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
offlineNotifyIn := readInput("是否开启离线通知(默认关闭 1 - 是 | 0 - 否 ): ")
|
||||||
|
switch offlineNotifyIn {
|
||||||
|
case "1":
|
||||||
|
offlineNotify = constant.Enabled
|
||||||
|
case "0":
|
||||||
|
offlineNotify = constant.Disabled
|
||||||
|
default:
|
||||||
|
offlineNotify = constant.Disabled
|
||||||
|
}
|
||||||
|
p.UseServerDns = &useServerDNS
|
||||||
|
p.Enabled = &enabled
|
||||||
|
p.OfflineMonitoring = &offlineNotify
|
||||||
|
|
||||||
|
err = service.Client().SaveClient(p, c.LoginUser)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("添加客户端失败: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("添加客户端成功")
|
||||||
|
c.List(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit
|
||||||
|
// @description: 编辑客户端
|
||||||
|
// @receiver c
|
||||||
|
func (c *ClientComponent) Edit() {
|
||||||
|
fmt.Println("\n编辑客户端")
|
||||||
|
c.List(false)
|
||||||
|
|
||||||
|
clientIdx := readInput("请输入客户端序号: ")
|
||||||
|
idx, err := strconv.Atoi(clientIdx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("输入有误: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx < 1 || idx > len(c.Clients) {
|
||||||
|
fmt.Println("输入有误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := c.Clients[idx-1]
|
||||||
|
// 取到id
|
||||||
|
clientID := client[1]
|
||||||
|
// 查询客户端信息
|
||||||
|
clientInfo, err := service.Client().GetByID(clientID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("获取客户端信息失败: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var p param.SaveClient
|
||||||
|
p.Id = clientID
|
||||||
|
p.Name = readInput("请输入客户端名称[无需改变请回车跳过,下同]: ")
|
||||||
|
p.Email = readInput("请输入联系邮箱: ")
|
||||||
|
clientIPIn := readInput("请输入客户端IP(默认自动生成,多个采用 ',' 分割,例如 10.10.0.1/32,10.10.0.2/32): ")
|
||||||
|
if clientIPIn == "" {
|
||||||
|
p.IpAllocation = strings.Split(clientInfo.IpAllocation, ",")
|
||||||
|
} else {
|
||||||
|
p.IpAllocation = strings.Split(clientIPIn, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.AllowedIps = strings.Split(clientInfo.AllowedIps, ",")
|
||||||
|
var keys *param.Keys
|
||||||
|
_ = json.Unmarshal([]byte(clientInfo.Keys), &keys)
|
||||||
|
|
||||||
|
p.Keys = keys
|
||||||
|
|
||||||
|
var useServerDNS, enabled, offlineNotify constant.Status
|
||||||
|
|
||||||
|
useServerDNSIn := readInput("是否使用服务器DNS(默认不使用 1 - 是 | 0 - 否 ): ")
|
||||||
|
switch useServerDNSIn {
|
||||||
|
case "1":
|
||||||
|
useServerDNS = constant.Enabled
|
||||||
|
case "0":
|
||||||
|
useServerDNS = constant.Disabled
|
||||||
|
default:
|
||||||
|
useServerDNS = clientInfo.UseServerDns
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledIn := readInput("是否启用(默认启用 1 - 是 | 0 - 否 ): ")
|
||||||
|
switch enabledIn {
|
||||||
|
case "1":
|
||||||
|
enabled = constant.Enabled
|
||||||
|
case "0":
|
||||||
|
enabled = constant.Disabled
|
||||||
|
default:
|
||||||
|
enabled = clientInfo.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
offlineNotifyIn := readInput("是否开启离线通知(默认关闭 1 - 是 | 0 - 否 ): ")
|
||||||
|
switch offlineNotifyIn {
|
||||||
|
case "1":
|
||||||
|
offlineNotify = constant.Enabled
|
||||||
|
case "0":
|
||||||
|
offlineNotify = constant.Disabled
|
||||||
|
default:
|
||||||
|
offlineNotify = clientInfo.OfflineMonitoring
|
||||||
|
}
|
||||||
|
p.UseServerDns = &useServerDNS
|
||||||
|
p.Enabled = &enabled
|
||||||
|
p.OfflineMonitoring = &offlineNotify
|
||||||
|
|
||||||
|
err = service.Client().SaveClient(p, c.LoginUser)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("编辑客户端失败: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("编辑客户端成功")
|
||||||
|
c.List(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
// @description: 删除客户端
|
||||||
|
// @receiver c
|
||||||
|
func (c *ClientComponent) Delete() {
|
||||||
|
fmt.Println("\n删除客户端")
|
||||||
|
|
||||||
|
c.List(false)
|
||||||
|
|
||||||
|
clientIdx := readInput("请输入客户端序号: ")
|
||||||
|
idx, err := strconv.Atoi(clientIdx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("输入有误: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx < 1 || idx > len(c.Clients) {
|
||||||
|
fmt.Println("输入有误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := c.Clients[idx-1]
|
||||||
|
// 取到id
|
||||||
|
clientID := client[1]
|
||||||
|
if err := service.Client().Delete(clientID); err != nil {
|
||||||
|
fmt.Println("删除客户端失败: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.List(true)
|
||||||
|
fmt.Println("删除客户端成功")
|
||||||
|
return
|
||||||
|
}
|
||||||
62
cli/tui/server.go
Normal file
62
cli/tui/server.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"wireguard-ui/command"
|
||||||
|
"wireguard-ui/http/vo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerComponent struct {
|
||||||
|
LoginUser *vo.User
|
||||||
|
Menu []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServerComponent(loginUser *vo.User) *ServerComponent {
|
||||||
|
return &ServerComponent{
|
||||||
|
LoginUser: loginUser,
|
||||||
|
Menu: []string{"[1] 启动服务", "[2] 关闭服务", "[3] 重启服务", "[q] 返回上一级菜单"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menus
|
||||||
|
// @description: 服务端菜单
|
||||||
|
// @receiver s
|
||||||
|
func (s *ServerComponent) Menus() {
|
||||||
|
fmt.Println("")
|
||||||
|
for _, r := range s.Menu {
|
||||||
|
fmt.Println(" -> " + r)
|
||||||
|
}
|
||||||
|
|
||||||
|
chooseMenu := readInput("\n请选择: ")
|
||||||
|
switch chooseMenu {
|
||||||
|
case "1":
|
||||||
|
s.Start()
|
||||||
|
case "2":
|
||||||
|
s.Stop()
|
||||||
|
case "3":
|
||||||
|
s.Restart()
|
||||||
|
case "q":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start
|
||||||
|
// @description: 启动服务
|
||||||
|
// @receiver s
|
||||||
|
func (s *ServerComponent) Start() {
|
||||||
|
command.StartWireguard("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop
|
||||||
|
// @description: 停止服务
|
||||||
|
// @receiver s
|
||||||
|
func (s *ServerComponent) Stop() {
|
||||||
|
command.StopWireguard("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart
|
||||||
|
// @description: 重启服务
|
||||||
|
// @receiver s
|
||||||
|
func (s *ServerComponent) Restart() {
|
||||||
|
command.RestartWireguard(false, "")
|
||||||
|
}
|
||||||
440
cli/tui/setting.go
Normal file
440
cli/tui/setting.go
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/charmbracelet/bubbles/table"
|
||||||
|
"github.com/eiannone/keyboard"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
"strings"
|
||||||
|
"wireguard-ui/http/vo"
|
||||||
|
"wireguard-ui/model"
|
||||||
|
"wireguard-ui/service"
|
||||||
|
"wireguard-ui/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SettingComponent struct {
|
||||||
|
LoginUser *vo.User
|
||||||
|
Menu []string
|
||||||
|
Other *OtherSettingComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSettingComponent(loginUser *vo.User) *SettingComponent {
|
||||||
|
return &SettingComponent{
|
||||||
|
LoginUser: loginUser,
|
||||||
|
Menu: []string{"[1] 服务端配置", "[2] 全局设置", "[3] 其他配置", "[q] 返回上一级菜单"},
|
||||||
|
Other: NewOtherSettingComponent(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menus
|
||||||
|
// @description: 设置菜单
|
||||||
|
// @receiver s
|
||||||
|
func (s *SettingComponent) Menus() {
|
||||||
|
fmt.Println("")
|
||||||
|
for _, r := range s.Menu {
|
||||||
|
fmt.Println(" -> " + r)
|
||||||
|
}
|
||||||
|
|
||||||
|
chooseMenu := readInput("\n请选择: ")
|
||||||
|
switch chooseMenu {
|
||||||
|
case "1":
|
||||||
|
s.ServerSetting()
|
||||||
|
case "2":
|
||||||
|
s.GlobalSetting()
|
||||||
|
case "3":
|
||||||
|
s.Other.Menus(s)
|
||||||
|
case "q":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerSetting
|
||||||
|
// @description: 服务端配置
|
||||||
|
// @receiver s
|
||||||
|
func (s *SettingComponent) ServerSetting() {
|
||||||
|
fmt.Println("\n服务端配置")
|
||||||
|
// 先读取一下服务端配置
|
||||||
|
servConf, err := service.Setting().GetByCode("WG_SERVER")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("获取服务端配置失败: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverConf struct {
|
||||||
|
IpScope []string `json:"ipScope"`
|
||||||
|
ListenPort int `json:"listenPort"`
|
||||||
|
PrivateKey string `json:"privateKey"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
PostUpScript string `json:"postUpScript"`
|
||||||
|
PostDownScript string `json:"postDownScript"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析出来好渲染
|
||||||
|
var conf serverConf
|
||||||
|
if err = json.Unmarshal([]byte(servConf.Data), &conf); err != nil {
|
||||||
|
fmt.Println("解析服务端配置失败: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ipScopeIn := readInput(fmt.Sprintf("请输入IP段多个采用 ',[英文逗号]' 分割,不填写默认当前值,下同,当前值[%s] :", strings.Replace(strings.Join(conf.IpScope, ","), " ", "", -1)))
|
||||||
|
listenPortIn := readInput(fmt.Sprintf("请输入监听端口,当前值[%d]: ", conf.ListenPort))
|
||||||
|
privateKeyIn := readInput(fmt.Sprintf("请输入私钥,当前值[%s]: ", conf.PrivateKey))
|
||||||
|
publicKeyIn := readInput(fmt.Sprintf("请输入公钥,当前值[%s]: ", conf.PublicKey))
|
||||||
|
postUpScriptIn := readInput(fmt.Sprintf("请输入PostUp脚本,当前值[%s]: ", conf.PostUpScript))
|
||||||
|
postDownScriptIn := readInput(fmt.Sprintf("请输入PostDown脚本,当前值[%s]: ", conf.PostDownScript))
|
||||||
|
|
||||||
|
if ipScopeIn != "" {
|
||||||
|
conf.IpScope = strings.Split(ipScopeIn, ",")
|
||||||
|
}
|
||||||
|
if listenPortIn != "" {
|
||||||
|
conf.ListenPort = cast.ToInt(listenPortIn)
|
||||||
|
}
|
||||||
|
if privateKeyIn != "" {
|
||||||
|
conf.PrivateKey = privateKeyIn
|
||||||
|
}
|
||||||
|
if publicKeyIn != "" {
|
||||||
|
conf.PublicKey = publicKeyIn
|
||||||
|
}
|
||||||
|
if postUpScriptIn != "" {
|
||||||
|
conf.PostUpScript = postUpScriptIn
|
||||||
|
}
|
||||||
|
if postDownScriptIn != "" {
|
||||||
|
conf.PostDownScript = postDownScriptIn
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := json.Marshal(conf)
|
||||||
|
|
||||||
|
if err := service.Setting().SetData(&model.Setting{
|
||||||
|
Code: "WG_SERVER",
|
||||||
|
Data: string(data),
|
||||||
|
}); err != nil {
|
||||||
|
fmt.Println("保存服务端配置失败: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("修改服务端配置成功")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalSetting
|
||||||
|
// @description: 全局设置
|
||||||
|
// @receiver s
|
||||||
|
func (s *SettingComponent) GlobalSetting() {
|
||||||
|
fmt.Println("\n服务端配置")
|
||||||
|
// 先读取一下服务端配置
|
||||||
|
globalConf, err := service.Setting().GetByCode("WG_SETTING")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("获取服务端配置失败: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type gConf struct {
|
||||||
|
MTU int `json:"MTU"`
|
||||||
|
ConfigFilePath string `json:"configFilePath"`
|
||||||
|
DnsServer []string `json:"dnsServer"`
|
||||||
|
EndpointAddress string `json:"endpointAddress"`
|
||||||
|
FirewallMark string `json:"firewallMark"`
|
||||||
|
PersistentKeepalive int `json:"persistentKeepalive"`
|
||||||
|
Table string `json:"table"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析出来好渲染
|
||||||
|
var conf gConf
|
||||||
|
if err = json.Unmarshal([]byte(globalConf.Data), &conf); err != nil {
|
||||||
|
fmt.Println("解析全局配置失败: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mtu := readInput(fmt.Sprintf("请输入mtu,不填写默认当前值,下同,当前值[%d] :", conf.MTU))
|
||||||
|
configFilePath := readInput(fmt.Sprintf("请输入配置文件地址,当前值[%s]: ", conf.ConfigFilePath))
|
||||||
|
dnsServer := readInput(fmt.Sprintf("请输入dns,多个采用 ',[英文逗号]' 分割,当前值[%s]: ", strings.Replace(strings.Join(conf.DnsServer, ","), " ", "", -1)))
|
||||||
|
endpointAddress := readInput(fmt.Sprintf("请输入公网IP,默认系统自动获取,当前值[%s]: ", conf.EndpointAddress))
|
||||||
|
firewallMark := readInput(fmt.Sprintf("请输入FirewallMark,当前值[%s]: ", conf.FirewallMark))
|
||||||
|
persistentKeepalive := readInput(fmt.Sprintf("请输入PersistentKeepalive,当前值[%d]: ", conf.PersistentKeepalive))
|
||||||
|
tableRule := readInput(fmt.Sprintf("请输入Table,当前值[%s]: ", conf.Table))
|
||||||
|
|
||||||
|
if mtu != "" {
|
||||||
|
conf.MTU = cast.ToInt(mtu)
|
||||||
|
}
|
||||||
|
if configFilePath != "" {
|
||||||
|
conf.ConfigFilePath = configFilePath
|
||||||
|
}
|
||||||
|
if dnsServer != "" {
|
||||||
|
conf.DnsServer = strings.Split(dnsServer, ",")
|
||||||
|
}
|
||||||
|
if endpointAddress != "" {
|
||||||
|
conf.EndpointAddress = endpointAddress
|
||||||
|
} else {
|
||||||
|
conf.EndpointAddress = utils.Network().GetHostPublicIP()
|
||||||
|
}
|
||||||
|
if firewallMark != "" {
|
||||||
|
conf.FirewallMark = firewallMark
|
||||||
|
}
|
||||||
|
if persistentKeepalive != "" {
|
||||||
|
conf.PersistentKeepalive = cast.ToInt(persistentKeepalive)
|
||||||
|
}
|
||||||
|
if tableRule != "" {
|
||||||
|
conf.Table = tableRule
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := json.Marshal(conf)
|
||||||
|
|
||||||
|
if err := service.Setting().SetData(&model.Setting{
|
||||||
|
Code: "WG_SETTING",
|
||||||
|
Data: string(data),
|
||||||
|
}); err != nil {
|
||||||
|
fmt.Println("保存全局配置失败: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("修改全局配置成功")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// OtherSettingComponent
|
||||||
|
// @description: 其他配置杂项
|
||||||
|
type OtherSettingComponent struct {
|
||||||
|
Setting *SettingComponent
|
||||||
|
Menu []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOtherSettingComponent() *OtherSettingComponent {
|
||||||
|
return &OtherSettingComponent{
|
||||||
|
Menu: []string{"[1] 列表", "[2] 添加", "[3] 编辑", "[4] 删除", "[q] 返回上一级菜单"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OtherSettingComponent) Menus(setting *SettingComponent) {
|
||||||
|
if s.Setting == nil {
|
||||||
|
s.Setting = setting
|
||||||
|
}
|
||||||
|
fmt.Println("")
|
||||||
|
for _, r := range s.Menu {
|
||||||
|
fmt.Println(" -> " + r)
|
||||||
|
}
|
||||||
|
|
||||||
|
chooseMenu := readInput("\n请选择: ")
|
||||||
|
switch chooseMenu {
|
||||||
|
case "1":
|
||||||
|
s.List(true)
|
||||||
|
case "2":
|
||||||
|
s.Add()
|
||||||
|
case "3":
|
||||||
|
s.Edit()
|
||||||
|
case "4":
|
||||||
|
s.Delete()
|
||||||
|
|
||||||
|
case "q":
|
||||||
|
s.Setting.Menus()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// List
|
||||||
|
// @description: 其他配置
|
||||||
|
// @receiver s
|
||||||
|
// @param showMenu
|
||||||
|
func (s *OtherSettingComponent) List(showMenu bool) {
|
||||||
|
fmt.Println("\n其他配置列表")
|
||||||
|
// 不查询的配置
|
||||||
|
var blackList = []string{"WG_SETTING", "WG_SERVER"}
|
||||||
|
|
||||||
|
data, err := service.Setting().GetAllSetting(blackList)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("获取配置失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
title := []table.Column{
|
||||||
|
{
|
||||||
|
Title: "序号",
|
||||||
|
Width: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "编码",
|
||||||
|
Width: 40,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "描述",
|
||||||
|
Width: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "创建时间",
|
||||||
|
Width: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "更新时间",
|
||||||
|
Width: 30,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var result [][]string
|
||||||
|
for i, v := range data {
|
||||||
|
result = append(result, []string{
|
||||||
|
cast.ToString(i + 1),
|
||||||
|
v.Code,
|
||||||
|
v.Describe,
|
||||||
|
v.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
|
v.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Show(GenerateTable(title, result))
|
||||||
|
if showMenu {
|
||||||
|
s.Menus(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add
|
||||||
|
// @description: 新增其他配置
|
||||||
|
// @receiver s
|
||||||
|
func (s *OtherSettingComponent) Add() {
|
||||||
|
fmt.Println("\n新增其他配置")
|
||||||
|
|
||||||
|
code := readInput("请输入配置编码,此编码是唯一编码不可重复:")
|
||||||
|
desc := readInput("请输入配置描述:")
|
||||||
|
|
||||||
|
// 监听键盘事件,只监听 + 和 - 和 enter
|
||||||
|
if err := keyboard.Open(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = keyboard.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// + <=> 43 | - <=> 45 | enter <=> 0
|
||||||
|
fmt.Println("请按下 + 或者 - 进行配置项的新增和删除")
|
||||||
|
fmt.Println("每一项配置如此: key=val ")
|
||||||
|
fmt.Println("确认输入完成后 enter[按一次代表当前配置项输入完成,两次代表新增完成]")
|
||||||
|
fmt.Println("首先进入时请输入 + 进行第一个配置项填写")
|
||||||
|
var breakCycle bool
|
||||||
|
var keyVal []string
|
||||||
|
for {
|
||||||
|
char, _, err := keyboard.GetKey()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if breakCycle {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
switch char {
|
||||||
|
case 0:
|
||||||
|
// 收到enter事件触发,执行后面的
|
||||||
|
var dm = make(map[string]any)
|
||||||
|
for _, kv := range keyVal {
|
||||||
|
kvs := strings.Split(kv, "=")
|
||||||
|
key := kvs[0]
|
||||||
|
val := kvs[1]
|
||||||
|
dm[key] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
dms, err := json.Marshal(dm)
|
||||||
|
if err != nil {
|
||||||
|
breakCycle = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = service.Setting().SetData(&model.Setting{
|
||||||
|
Code: code,
|
||||||
|
Data: string(dms),
|
||||||
|
Describe: desc,
|
||||||
|
}); err != nil {
|
||||||
|
breakCycle = true
|
||||||
|
fmt.Println("保存配置失败: ", err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("保存配置成功")
|
||||||
|
s.List(true)
|
||||||
|
|
||||||
|
breakCycle = true
|
||||||
|
case 43:
|
||||||
|
keyVal = append(keyVal, readInput("请输入配置项:"))
|
||||||
|
case 45:
|
||||||
|
keyVal = keyVal[:len(keyVal)-1]
|
||||||
|
fmt.Println("已删除最后一个配置项,当前配置项为:", keyVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit
|
||||||
|
// @description: 编辑
|
||||||
|
// @receiver s
|
||||||
|
func (s *OtherSettingComponent) Edit() {
|
||||||
|
fmt.Println("\n编辑其他配置")
|
||||||
|
|
||||||
|
s.List(false)
|
||||||
|
|
||||||
|
code := readInput("请输入需要编辑的配置编码:")
|
||||||
|
|
||||||
|
// 通过编码查询配置
|
||||||
|
setting, err := service.Setting().GetByCode(code)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("查找["+code+"]配置失败:", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
desc := readInput("请输入需要编辑的配置描述:")
|
||||||
|
if desc != "" {
|
||||||
|
setting.Describe = desc
|
||||||
|
}
|
||||||
|
|
||||||
|
var kvm = make(map[string]any)
|
||||||
|
if err = json.Unmarshal([]byte(setting.Data), &kvm); err != nil {
|
||||||
|
fmt.Println("配置解析失败: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range kvm {
|
||||||
|
valIn := readInput(fmt.Sprintf("请输入配置项值,仅能修改值,当前键值对:%s=%v :", k, v))
|
||||||
|
if valIn != "" {
|
||||||
|
kvm[k] = valIn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理以下数据
|
||||||
|
kvmStr, err := json.Marshal(kvm)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("序列化数据失败: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setting.Data = string(kvmStr)
|
||||||
|
|
||||||
|
if err = service.Setting().SetData(setting); err != nil {
|
||||||
|
fmt.Println("修改配置失败: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("修改配置成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
// @description: 删除
|
||||||
|
// @receiver s
|
||||||
|
func (s *OtherSettingComponent) Delete() {
|
||||||
|
fmt.Println("\n 删除指定配置")
|
||||||
|
|
||||||
|
s.List(false)
|
||||||
|
|
||||||
|
code := readInput("请输入要删除的配置项编码:")
|
||||||
|
// 查询配置是否存在
|
||||||
|
if err := service.Setting().Model(&model.Setting{}).
|
||||||
|
Where("code NOT IN (?)", []string{"WG_SETTING", "WG_SERVER"}).
|
||||||
|
Where("code = ?", code).Delete(&model.Setting{}).Error; err != nil {
|
||||||
|
fmt.Println("删除[" + code + "]配置失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("删除成功")
|
||||||
|
|
||||||
|
s.List(true)
|
||||||
|
|
||||||
|
}
|
||||||
136
cli/tui/utils.go
Normal file
136
cli/tui/utils.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/charmbracelet/bubbles/table"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"wireguard-ui/http/vo"
|
||||||
|
"wireguard-ui/model"
|
||||||
|
"wireguard-ui/service"
|
||||||
|
"wireguard-ui/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var BaseStyle = lipgloss.NewStyle().
|
||||||
|
BorderStyle(lipgloss.NormalBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("240"))
|
||||||
|
|
||||||
|
var menus = []string{"[1] 客户端链接列表", "[2] 客户端", "[3] 服务端", "[4] 设置", "[q] 退出"}
|
||||||
|
|
||||||
|
// PrintMenu
|
||||||
|
// @description: 打印一下菜单
|
||||||
|
func PrintMenu() {
|
||||||
|
fmt.Println("\n菜单:")
|
||||||
|
for _, menu := range menus {
|
||||||
|
fmt.Println(menu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateTable
|
||||||
|
// @description: 生成数据表
|
||||||
|
// @param column
|
||||||
|
// @param rows
|
||||||
|
// @return string
|
||||||
|
func GenerateTable(column []table.Column, rows [][]string) string {
|
||||||
|
var data []table.Row
|
||||||
|
for _, v := range rows {
|
||||||
|
data = append(data, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
tl := table.New(
|
||||||
|
table.WithColumns(column),
|
||||||
|
table.WithRows(data),
|
||||||
|
table.WithHeight(len(data)+1),
|
||||||
|
)
|
||||||
|
|
||||||
|
s := table.DefaultStyles()
|
||||||
|
s.Header = s.Header.
|
||||||
|
BorderStyle(lipgloss.NormalBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("240")).
|
||||||
|
BorderBottom(true).
|
||||||
|
Bold(false)
|
||||||
|
s.Selected = lipgloss.NewStyle()
|
||||||
|
|
||||||
|
tl.SetStyles(s)
|
||||||
|
|
||||||
|
return BaseStyle.Render(tl.View()+"\n") + "\n一共有 【" + fmt.Sprintf("%d", len(data)) + "】 条数据"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateIP
|
||||||
|
// @description: 生成IP
|
||||||
|
// @return clientIPS
|
||||||
|
// @return serverIPS
|
||||||
|
// @return err
|
||||||
|
func GenerateIP() (clientIPS, serverIPS []string, err error) {
|
||||||
|
// 获取一下服务端信息,因为IP分配需要根据服务端的IP制定
|
||||||
|
serverInfo, err := service.Setting().GetWGServerForConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var assignIPS []string
|
||||||
|
// 只获取最新的一个
|
||||||
|
var clientInfo *model.Client
|
||||||
|
if err = service.Client().Order("created_at DESC").Take(&clientInfo).Error; err == nil {
|
||||||
|
// 遍历每一个ip是否可允许再分配
|
||||||
|
for _, ip := range strings.Split(clientInfo.IpAllocation, ",") {
|
||||||
|
if cast.ToInt64(utils.Network().GetIPSuffix(ip)) >= 255 {
|
||||||
|
log.Errorf("IP:[%s]已无法分配新IP", ip)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
assignIPS = append(assignIPS, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ips := utils.Network().GenerateIPByIPS(serverInfo.Address, assignIPS...)
|
||||||
|
|
||||||
|
return ips, serverInfo.Address, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKeys
|
||||||
|
// @description: 生成密钥对
|
||||||
|
// @return keys
|
||||||
|
// @return err
|
||||||
|
func GenerateKeys() (keys *vo.Keys, err error) {
|
||||||
|
// 为空,新增
|
||||||
|
privateKey, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
publicKey := privateKey.PublicKey().String()
|
||||||
|
presharedKey, err := wgtypes.GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("生成预共享密钥失败: %s", err.Error())
|
||||||
|
}
|
||||||
|
keys = &vo.Keys{
|
||||||
|
PrivateKey: privateKey.String(),
|
||||||
|
PublicKey: publicKey,
|
||||||
|
PresharedKey: presharedKey.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show
|
||||||
|
// @description: 展示
|
||||||
|
// @param data
|
||||||
|
func Show(data any) {
|
||||||
|
fmt.Println(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readInput
|
||||||
|
// @description: 读取输入
|
||||||
|
// @param prompt
|
||||||
|
// @return string
|
||||||
|
func readInput(prompt string) string {
|
||||||
|
fmt.Print(prompt)
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
scanner.Scan()
|
||||||
|
return scanner.Text()
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@ func JWT() JwtComponent {
|
|||||||
// @return token
|
// @return token
|
||||||
// @return expireTime
|
// @return expireTime
|
||||||
// @return err
|
// @return err
|
||||||
func (JwtComponent) GenerateToken(userId, secret string, times ...time.Time) (token string, expireTime *jwt.NumericDate, err error) {
|
func (JwtComponent) GenerateToken(userId, secret, source string, times ...time.Time) (token string, expireTime *jwt.NumericDate, err error) {
|
||||||
var notBefore, issuedAt *jwt.NumericDate
|
var notBefore, issuedAt *jwt.NumericDate
|
||||||
if len(times) != 0 {
|
if len(times) != 0 {
|
||||||
expireTime = jwt.NewNumericDate(times[0])
|
expireTime = jwt.NewNumericDate(times[0])
|
||||||
@@ -68,10 +68,19 @@ func (JwtComponent) GenerateToken(userId, secret string, times ...time.Time) (to
|
|||||||
return "", nil, errors.New("生成token失败")
|
return "", nil, errors.New("生成token失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
client.Redis.Set(context.Background(),
|
switch source {
|
||||||
fmt.Sprintf("%s:%s", constant.UserToken, userId),
|
case "http":
|
||||||
token,
|
client.Redis.Set(context.Background(),
|
||||||
time.Duration(expireTime.Sub(time.Now()).Abs().Seconds())*time.Second)
|
fmt.Sprintf("%s:%s", constant.UserToken, userId),
|
||||||
|
token,
|
||||||
|
time.Duration(expireTime.Sub(time.Now()).Abs().Seconds())*time.Second)
|
||||||
|
case "tui":
|
||||||
|
client.Redis.Set(context.Background(),
|
||||||
|
fmt.Sprintf("%s:%s", constant.TUIUserToken, userId),
|
||||||
|
token,
|
||||||
|
time.Duration(expireTime.Sub(time.Now()).Abs().Seconds())*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +90,7 @@ func (JwtComponent) GenerateToken(userId, secret string, times ...time.Time) (to
|
|||||||
// @param token
|
// @param token
|
||||||
// @return *JwtComponent
|
// @return *JwtComponent
|
||||||
// @return error
|
// @return error
|
||||||
func (JwtComponent) ParseToken(token, secret string) (*JwtComponent, error) {
|
func (JwtComponent) ParseToken(token, secret, source string) (*JwtComponent, error) {
|
||||||
tokenStr := strings.Split(token, "Bearer ")[1]
|
tokenStr := strings.Split(token, "Bearer ")[1]
|
||||||
|
|
||||||
t, err := jwt.ParseWithClaims(tokenStr, &JwtComponent{}, func(token *jwt.Token) (any, error) {
|
t, err := jwt.ParseWithClaims(tokenStr, &JwtComponent{}, func(token *jwt.Token) (any, error) {
|
||||||
@@ -89,10 +98,20 @@ func (JwtComponent) ParseToken(token, secret string) (*JwtComponent, error) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if claims, ok := t.Claims.(*JwtComponent); ok && t.Valid {
|
if claims, ok := t.Claims.(*JwtComponent); ok && t.Valid {
|
||||||
userToken, err := client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.UserToken, claims.ID)).Result()
|
var userToken string
|
||||||
if err != nil {
|
switch source {
|
||||||
log.Errorf("缓存中用户[%s]的token查找失败: %v", claims.ID, err.Error())
|
case "http":
|
||||||
return nil, errors.New("token不存在")
|
userToken, err = client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.UserToken, claims.ID)).Result()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("缓存中用户[%s]的token查找失败: %v", claims.ID, err.Error())
|
||||||
|
return nil, errors.New("token不存在")
|
||||||
|
}
|
||||||
|
case "tui":
|
||||||
|
userToken, err = client.Redis.Get(context.Background(), fmt.Sprintf("%s:%s", constant.TUIUserToken, claims.ID)).Result()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("缓存中用户[%s]的token查找失败: %v", claims.ID, err.Error())
|
||||||
|
return nil, errors.New("token不存在")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if userToken != tokenStr {
|
if userToken != tokenStr {
|
||||||
|
|||||||
@@ -3,11 +3,16 @@ package cron
|
|||||||
import (
|
import (
|
||||||
"gitee.ltd/lxh/logger/log"
|
"gitee.ltd/lxh/logger/log"
|
||||||
"github.com/go-co-op/gocron/v2"
|
"github.com/go-co-op/gocron/v2"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
"wireguard-ui/cron/task"
|
"wireguard-ui/cron/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Task() {
|
func Task() {
|
||||||
|
if !cast.ToBool(os.Getenv("ENABLED_CRON")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
sch, err := gocron.NewScheduler(gocron.WithLocation(time.Local))
|
sch, err := gocron.NewScheduler(gocron.WithLocation(time.Local))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("初始化定时任务失败")
|
log.Errorf("初始化定时任务失败")
|
||||||
|
|||||||
1157
document/openapi.yaml
Normal file
1157
document/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,5 +3,6 @@ package constant
|
|||||||
const (
|
const (
|
||||||
Captcha = "captcha"
|
Captcha = "captcha"
|
||||||
UserToken = "token"
|
UserToken = "token"
|
||||||
|
TUIUserToken = "tui:token"
|
||||||
ClientOffline = "client:offline:"
|
ClientOffline = "client:offline:"
|
||||||
)
|
)
|
||||||
|
|||||||
88
go.mod
88
go.mod
@@ -1,21 +1,28 @@
|
|||||||
module wireguard-ui
|
module wireguard-ui
|
||||||
|
|
||||||
go 1.21
|
go 1.22.7
|
||||||
|
|
||||||
|
toolchain go1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitee.ltd/lxh/logger v1.0.18
|
gitee.ltd/lxh/logger v1.0.19
|
||||||
|
github.com/charmbracelet/bubbles v0.20.0
|
||||||
|
github.com/charmbracelet/lipgloss v0.13.0
|
||||||
github.com/cowardmrx/go_aliyun_oss v1.0.7
|
github.com/cowardmrx/go_aliyun_oss v1.0.7
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/fsnotify/fsnotify v1.7.0
|
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
|
||||||
|
github.com/fsnotify/fsnotify v1.8.0
|
||||||
|
github.com/gin-contrib/pprof v1.5.2
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/go-co-op/gocron/v2 v2.12.4
|
github.com/go-co-op/gocron/v2 v2.12.4
|
||||||
github.com/go-playground/locales v0.14.1
|
github.com/go-playground/locales v0.14.1
|
||||||
github.com/go-playground/universal-translator v0.18.1
|
github.com/go-playground/universal-translator v0.18.1
|
||||||
github.com/go-playground/validator/v10 v10.22.0
|
github.com/go-playground/validator/v10 v10.23.0
|
||||||
github.com/go-resty/resty/v2 v2.13.1
|
github.com/go-resty/resty/v2 v2.15.3
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
|
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/mojocn/base64Captcha v1.3.6
|
github.com/mojocn/base64Captcha v1.3.6
|
||||||
@@ -35,28 +42,37 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible // indirect
|
github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bytedance/sonic v1.12.5 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
github.com/caarlos0/env/v6 v6.10.1 // indirect
|
github.com/caarlos0/env/v6 v6.10.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/charmbracelet/bubbletea v1.1.0 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/charmbracelet/x/ansi v0.2.3 // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/dennwc/varint v1.0.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||||
github.com/go-kit/kit v0.12.0 // indirect
|
github.com/go-kit/kit v0.12.0 // indirect
|
||||||
github.com/go-kit/log v0.2.1 // indirect
|
github.com/go-kit/log v0.2.1 // indirect
|
||||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.4 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
|
github.com/grafana/loki-client-go v0.0.0-20240913122146-e119d400c3a5 // indirect
|
||||||
|
github.com/grafana/loki/pkg/push v0.0.0-20240912152814-63e84b476a9a // indirect
|
||||||
|
github.com/grafana/regexp v0.0.0-20220304095617-2e8d9baf4ac2 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
@@ -67,29 +83,37 @@ require (
|
|||||||
github.com/jonboulle/clockwork v0.4.0 // indirect
|
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
github.com/jpillora/backoff v1.0.0 // indirect
|
github.com/jpillora/backoff v1.0.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lixh00/loki-client-go v1.0.1 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||||
github.com/mdlayher/socket v0.4.1 // indirect
|
github.com/mdlayher/socket v0.4.1 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.15.2 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
||||||
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
|
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
|
||||||
github.com/panjf2000/ants/v2 v2.10.0 // indirect
|
github.com/panjf2000/ants/v2 v2.10.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/prometheus/client_golang v1.13.0 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||||
github.com/prometheus/common v0.37.0 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/procfs v0.8.0 // indirect
|
github.com/prometheus/common v0.61.0 // indirect
|
||||||
github.com/prometheus/prometheus v1.8.2-0.20201028100903-3245b3267b24 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
|
github.com/prometheus/prometheus v0.35.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
@@ -97,27 +121,29 @@ require (
|
|||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/afero v1.11.0 // indirect
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/stretchr/testify v1.10.0 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
go.uber.org/atomic v1.10.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/multierr v1.9.0 // indirect
|
go.uber.org/goleak v1.3.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.23.0 // indirect
|
go.uber.org/zap v1.23.0 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
golang.org/x/arch v0.12.0 // indirect
|
||||||
golang.org/x/image v0.18.0 // indirect
|
golang.org/x/image v0.18.0 // indirect
|
||||||
golang.org/x/net v0.33.0 // indirect
|
golang.org/x/net v0.33.0 // indirect
|
||||||
golang.org/x/oauth2 v0.18.0 // indirect
|
golang.org/x/oauth2 v0.24.0 // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.8.0 // indirect
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect
|
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
|
google.golang.org/grpc v1.69.0 // indirect
|
||||||
google.golang.org/grpc v1.62.1 // indirect
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.22.5 // indirect
|
modernc.org/libc v1.22.5 // indirect
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func (LoginApi) Login(c *gin.Context) {
|
|||||||
|
|
||||||
secret := component.JWT().GenerateSecret(p.Password, uuid.NewString(), time.Now().Local().String())
|
secret := component.JWT().GenerateSecret(p.Password, uuid.NewString(), time.Now().Local().String())
|
||||||
// 生成token
|
// 生成token
|
||||||
token, expireAt, err := component.JWT().GenerateToken(user.Id, secret)
|
token, expireAt, err := component.JWT().GenerateToken(user.Id, secret, "http")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("用户[%s]生成token失败: %v", user.Account, err.Error())
|
log.Errorf("用户[%s]生成token失败: %v", user.Account, err.Error())
|
||||||
response.R(c).FailedWithError("登陆失败!")
|
response.R(c).FailedWithError("登陆失败!")
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func (setting) Delete(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := service.Setting().Model(&model.Setting{}).Where("code = ?", code).Delete(&model.Setting{}).Error; err != nil {
|
if err := service.Setting().Model(&model.Setting{}).Where("code NOT IN (?)", []string{"WG_SETTING", "WG_SERVER"}).Where("code = ?", code).Delete(&model.Setting{}).Error; err != nil {
|
||||||
response.R(c).FailedWithError("删除失败")
|
response.R(c).FailedWithError("删除失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -184,6 +184,9 @@ func (setting) Import(c *gin.Context) {
|
|||||||
response.R(c).FailedWithError(err)
|
response.R(c).FailedWithError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = fileBytes.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
var data vo.Export
|
var data vo.Export
|
||||||
if err := json.NewDecoder(fileBytes).Decode(&data); err != nil {
|
if err := json.NewDecoder(fileBytes).Decode(&data); err != nil {
|
||||||
|
|||||||
@@ -140,8 +140,13 @@ func (UserApi) Delete(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loginUser := GetCurrentLoginUser(c)
|
||||||
|
if c.IsAborted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 是不是自己删除自己
|
// 是不是自己删除自己
|
||||||
if id == GetCurrentLoginUser(c).Id && c.IsAborted() {
|
if id == loginUser.Id {
|
||||||
response.R(c).FailedWithError("非法操作")
|
response.R(c).FailedWithError("非法操作")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -178,8 +183,13 @@ func (UserApi) Status(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loginUser := GetCurrentLoginUser(c)
|
||||||
|
if c.IsAborted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 是不是自己删除自己
|
// 是不是自己删除自己
|
||||||
if id == GetCurrentLoginUser(c).Id && c.IsAborted() {
|
if id == loginUser.Id {
|
||||||
response.R(c).FailedWithError("非法操作")
|
response.R(c).FailedWithError("非法操作")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ package http
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"gitee.ltd/lxh/logger/log"
|
"gitee.ltd/lxh/logger/log"
|
||||||
|
"github.com/gin-contrib/pprof"
|
||||||
|
"github.com/spf13/cast"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"wireguard-ui/config"
|
"wireguard-ui/config"
|
||||||
"wireguard-ui/http/router"
|
"wireguard-ui/http/router"
|
||||||
)
|
)
|
||||||
@@ -16,6 +19,10 @@ func Kernel() error {
|
|||||||
handler := router.InitRouter()
|
handler := router.InitRouter()
|
||||||
addr := fmt.Sprintf(":%d", config.Config.Http.Port)
|
addr := fmt.Sprintf(":%d", config.Config.Http.Port)
|
||||||
|
|
||||||
|
if cast.ToBool(os.Getenv("ENABLED_PPROF")) {
|
||||||
|
pprof.Register(handler, "/monitoring")
|
||||||
|
}
|
||||||
|
|
||||||
httpServer := http.Server{
|
httpServer := http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Handler: handler,
|
Handler: handler,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func Authorization() gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userClaims, err := component.JWT().ParseToken(token, hashPassword)
|
userClaims, err := component.JWT().ParseToken(token, hashPassword, "http")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.R(c).AuthorizationFailed("未登陆")
|
response.R(c).AuthorizationFailed("未登陆")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
@@ -70,7 +70,7 @@ func Authorization() gin.HandlerFunc {
|
|||||||
|
|
||||||
// 生成一个新token
|
// 生成一个新token
|
||||||
secret := component.JWT().GenerateSecret(user.Password, uuid.NewString(), time.Now().Local().String())
|
secret := component.JWT().GenerateSecret(user.Password, uuid.NewString(), time.Now().Local().String())
|
||||||
tokenStr, _, err := component.JWT().GenerateToken(user.Id, secret, userClaims.ExpiresAt.Time, time.Now().Local())
|
tokenStr, _, err := component.JWT().GenerateToken(user.Id, secret, "http", userClaims.ExpiresAt.Time, time.Now().Local())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.R(c).AuthorizationFailed("校验失败")
|
response.R(c).AuthorizationFailed("校验失败")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
// @description: 登陆相关API
|
// @description: 登陆相关API
|
||||||
// @param r
|
// @param r
|
||||||
func ClientApi(r *gin.RouterGroup) {
|
func ClientApi(r *gin.RouterGroup) {
|
||||||
client := r.Group("client", middleware.Authorization(), middleware.RequestLog())
|
client := r.Group("client", middleware.RequestLog(), middleware.Authorization())
|
||||||
{
|
{
|
||||||
client.POST("", api.Client().Save) // 新增/编辑客户端
|
client.POST("", api.Client().Save) // 新增/编辑客户端
|
||||||
client.DELETE("/:id", api.Client().Delete) // 删除客户端
|
client.DELETE("/:id", api.Client().Delete) // 删除客户端
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
// @description: 控制台相关接口
|
// @description: 控制台相关接口
|
||||||
// @param r
|
// @param r
|
||||||
func DashboardApi(r *gin.RouterGroup) {
|
func DashboardApi(r *gin.RouterGroup) {
|
||||||
dashboard := r.Group("dashboard", middleware.Authorization(), middleware.RequestLog())
|
dashboard := r.Group("dashboard", middleware.RequestLog(), middleware.Authorization())
|
||||||
{
|
{
|
||||||
dashboard.GET("/request/list", api.Dashboard().List) // 请求日志
|
dashboard.GET("/request/list", api.Dashboard().List) // 请求日志
|
||||||
dashboard.GET("/daily-poetry", api.Dashboard().DailyPoetry) // 每日诗词
|
dashboard.GET("/daily-poetry", api.Dashboard().DailyPoetry) // 每日诗词
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func InitRouter() *gin.Engine {
|
|||||||
// 开启IP 追踪
|
// 开启IP 追踪
|
||||||
r.ForwardedByClientIP = true
|
r.ForwardedByClientIP = true
|
||||||
// 将请求打印至控制台
|
// 将请求打印至控制台
|
||||||
r.Use(gin.Logger())
|
r.Use(gin.Logger(), gin.Recovery())
|
||||||
|
|
||||||
if config.Config.File.Type == "local" {
|
if config.Config.File.Type == "local" {
|
||||||
r.Static("/assets", config.Config.File.Path)
|
r.Static("/assets", config.Config.File.Path)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
// @description: 设置相关API
|
// @description: 设置相关API
|
||||||
// @param r
|
// @param r
|
||||||
func SettingApi(r *gin.RouterGroup) {
|
func SettingApi(r *gin.RouterGroup) {
|
||||||
setting := r.Group("setting", middleware.Authorization(), middleware.RequestLog())
|
setting := r.Group("setting", middleware.RequestLog(), middleware.Authorization())
|
||||||
{
|
{
|
||||||
setting.POST("", api.Setting().Set) // 新增/编辑设置
|
setting.POST("", api.Setting().Set) // 新增/编辑设置
|
||||||
setting.DELETE("/:code", api.Setting().Delete) // 删除配置
|
setting.DELETE("/:code", api.Setting().Delete) // 删除配置
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
// @description: 用户相关API
|
// @description: 用户相关API
|
||||||
// @param r
|
// @param r
|
||||||
func UserApi(r *gin.RouterGroup) {
|
func UserApi(r *gin.RouterGroup) {
|
||||||
userApi := r.Group("user", middleware.Authorization(), middleware.RequestLog())
|
userApi := r.Group("user", middleware.RequestLog(), middleware.Authorization())
|
||||||
{
|
{
|
||||||
userApi.GET("/info", api.User().GetLoginUser) // 获取当前登陆用户信息
|
userApi.GET("/info", api.User().GetLoginUser) // 获取当前登陆用户信息
|
||||||
userApi.POST("", api.User().SaveUser) // 新增/编辑用户
|
userApi.POST("", api.User().SaveUser) // 新增/编辑用户
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
package vo
|
package vo
|
||||||
|
|
||||||
import "wireguard-ui/global/constant"
|
import (
|
||||||
|
"wireguard-ui/global/constant"
|
||||||
|
"wireguard-ui/model"
|
||||||
|
)
|
||||||
|
|
||||||
// SettingItem
|
// SettingItem
|
||||||
// @description: 设置单项
|
// @description: 设置单项
|
||||||
type SettingItem struct {
|
type SettingItem struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Data string `json:"data"`
|
Data string `json:"data"`
|
||||||
Describe string `json:"describe"`
|
Describe string `json:"describe"`
|
||||||
|
CreatedAt model.JsonTime `json:"createdAt"`
|
||||||
|
UpdatedAt model.JsonTime `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Export struct {
|
type Export struct {
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import (
|
|||||||
"gitee.ltd/lxh/logger/log"
|
"gitee.ltd/lxh/logger/log"
|
||||||
"github.com/cowardmrx/go_aliyun_oss"
|
"github.com/cowardmrx/go_aliyun_oss"
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl"
|
"golang.zx2c4.com/wireguard/wgctrl"
|
||||||
@@ -15,6 +17,7 @@ import (
|
|||||||
"gorm.io/driver/postgres"
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
gl "gorm.io/gorm/logger"
|
gl "gorm.io/gorm/logger"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
"wireguard-ui/config"
|
"wireguard-ui/config"
|
||||||
"wireguard-ui/global/client"
|
"wireguard-ui/global/client"
|
||||||
@@ -25,6 +28,7 @@ import (
|
|||||||
func Init() {
|
func Init() {
|
||||||
initLogger() // 初始化日志
|
initLogger() // 初始化日志
|
||||||
initConfig() // 读取配置文件
|
initConfig() // 读取配置文件
|
||||||
|
initEnv() // 加载环境变量文件
|
||||||
initWireguard() // 初始化wireguard客户端
|
initWireguard() // 初始化wireguard客户端
|
||||||
initDatabase() // 初始化数据库
|
initDatabase() // 初始化数据库
|
||||||
initRedis() // 初始化redis
|
initRedis() // 初始化redis
|
||||||
@@ -140,8 +144,22 @@ func initOSS() {
|
|||||||
// initLogger
|
// initLogger
|
||||||
// @description: 初始化日志
|
// @description: 初始化日志
|
||||||
func initLogger() {
|
func initLogger() {
|
||||||
|
|
||||||
|
mode := logger.Dev
|
||||||
|
if os.Getenv("GIN_MODE") == gin.ReleaseMode {
|
||||||
|
mode = logger.Prod
|
||||||
|
}
|
||||||
|
|
||||||
logger.InitLogger(logger.LogConfig{
|
logger.InitLogger(logger.LogConfig{
|
||||||
Mode: logger.Dev,
|
Mode: mode,
|
||||||
FileEnable: true,
|
FileEnable: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initEnv
|
||||||
|
// @description: 初始化环境变量
|
||||||
|
func initEnv() {
|
||||||
|
if err := godotenv.Load(".env"); err != nil {
|
||||||
|
log.Errorf("加载.env文件失败: %v", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
3
main.go
3
main.go
@@ -8,6 +8,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
tui "wireguard-ui/cli"
|
tui "wireguard-ui/cli"
|
||||||
|
"wireguard-ui/cron"
|
||||||
"wireguard-ui/http"
|
"wireguard-ui/http"
|
||||||
"wireguard-ui/initialize"
|
"wireguard-ui/initialize"
|
||||||
"wireguard-ui/script"
|
"wireguard-ui/script"
|
||||||
@@ -18,7 +19,7 @@ func init() {
|
|||||||
if err := script.New().Do(); err != nil {
|
if err := script.New().Do(); err != nil {
|
||||||
log.Errorf("执行脚本失败: %v", err.Error())
|
log.Errorf("执行脚本失败: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
cron.Task()
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
// AuthClient
|
|
||||||
// @description: 认证客户端
|
|
||||||
type AuthClient struct {
|
|
||||||
Base
|
|
||||||
Name string `json:"name" gorm:"type:varchar(255);not null;comment: '客户端名称'"`
|
|
||||||
ClientID string `json:"clientID" gorm:"type:varchar(255);not null;comment: '客户端ID'"`
|
|
||||||
ClientKey string `json:"clientKey" gorm:"type:varchar(255);not null;comment: '客户端key'"`
|
|
||||||
ExpireAt string `json:"expireAt" gorm:"type:varchar(255);not null;comment: '过期时间'"`
|
|
||||||
IsEnabled int `json:"isEnabled" gorm:"type:int(1);not null;comment: '是否启用'"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (AuthClient) TableName() string {
|
|
||||||
return "t_oauth_client"
|
|
||||||
}
|
|
||||||
@@ -22,16 +22,3 @@ type Client struct {
|
|||||||
func (Client) TableName() string {
|
func (Client) TableName() string {
|
||||||
return "t_client"
|
return "t_client"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watcher
|
|
||||||
// @description: 监听日志
|
|
||||||
type Watcher struct {
|
|
||||||
Base
|
|
||||||
ClientId string `json:"clientId" gorm:"type:char(36);not null;comment:'客户端id'"`
|
|
||||||
NotifyResult string `json:"notifyResult" gorm:"type:text;default null;comment:'通知结果'"`
|
|
||||||
IsSend int `json:"isSend" gorm:"type:tinyint(1);default 0;comment:'是否已通知'"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Watcher) TableName() string {
|
|
||||||
return "t_watcher"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ func (s setting) GetWGServerForConfig() (data *render_data.Server, err error) {
|
|||||||
// @return data
|
// @return data
|
||||||
// @return err
|
// @return err
|
||||||
func (s setting) GetAllSetting(blackList []string) (data []vo.SettingItem, err error) {
|
func (s setting) GetAllSetting(blackList []string) (data []vo.SettingItem, err error) {
|
||||||
err = s.Model(&model.Setting{}).Select("code, data, describe").Where("code not in ?", blackList).Find(&data).Error
|
err = s.Model(&model.Setting{}).Select("code, data, describe,created_at,updated_at").Where("code not in ?", blackList).Find(&data).Error
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
72
web/.aiassistant/rules/front_code_rule.md
Normal file
72
web/.aiassistant/rules/front_code_rule.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
apply: 始终
|
||||||
|
---
|
||||||
|
|
||||||
|
# 前端开发全局强制准则 (V1.0 - Multi-Framework Pro)
|
||||||
|
|
||||||
|
## 1. 性能与资产安全 (Performance & Asset Safety)
|
||||||
|
* **加载优化 (Loading)**:
|
||||||
|
* **强制路由懒加载**:所有页面组件必须使用动态 `import()` 导入。
|
||||||
|
* **按需引入**:UI 框架(Naive UI, Element Plus, AntD)必须配置插件实现自动按需引入,严禁全局注册。
|
||||||
|
* **渲染性能 (Rendering)**:
|
||||||
|
* **长列表防御**:展示数据超过 50 条时,必须使用**虚拟列表 (Virtual List)** 或分页加载,禁止直接渲染大批量 DOM。
|
||||||
|
* **资产压缩**:禁止在页面加载超过 500KB 的原始图片,必须使用 WebP 格式或 CDN 缩略图参数。
|
||||||
|
* **内存释放 (Cleanup)**:
|
||||||
|
* 定时器 (`setInterval`)、全局事件监听 (`window.addEventListener`)、WebSocket 必须在组件卸载钩子(`onUnmounted` / `useEffect cleanup`)中明确销毁。
|
||||||
|
|
||||||
|
## 2. 状态管理与数据流 (State & Data Flow)
|
||||||
|
* **状态分层原则**:
|
||||||
|
* **局部优先**:仅在跨页面/跨多级组件共享时才使用 Pinia 或 Redux。
|
||||||
|
* **不可变性 (Immutability)**:严禁直接修改复杂对象的属性,必须使用解构赋值或 `set` 函数确保引用更新。
|
||||||
|
* **数据安全 (Safety)**:
|
||||||
|
* **可选链操作**:渲染后端异步数据必须使用 `?.` 可选链,严禁因 `undefined` 导致页面白屏。
|
||||||
|
* **声明式校验**:表单提交前必须通过 Schema 校验,严禁依赖后端接口反馈作为唯一拦截手段。
|
||||||
|
|
||||||
|
## 3. 组件架构与规模控制 (Architecture & Scale)
|
||||||
|
* **单文件硬指标**:**单个组件文件代码禁止超过 1000 行**。
|
||||||
|
* **重构门禁**:超过 800 行时,AI/开发者必须将业务逻辑抽离至 `Hooks` (Vue3 `useXXX` / React `useXXX`)。
|
||||||
|
* **层级规范**:
|
||||||
|
* 目录嵌套严禁超过 4 层。
|
||||||
|
* **职责分离**:复杂的页面必须拆分为“逻辑容器组件”和“纯 UI 展示组件”。
|
||||||
|
* **通用逻辑封装**:
|
||||||
|
* 业务无关的格式化、正则、计算函数必须存放在 `src/utils`。
|
||||||
|
* 跨组件复用的逻辑必须封装为 `Hooks`。
|
||||||
|
|
||||||
|
## 4. 命名与样式规范 (Naming & CSS)
|
||||||
|
* **精简命名**:
|
||||||
|
* **组件命名**:采用 `PascalCase`(如 `UserCard.vue`),严禁超过 3 个单词。
|
||||||
|
* **方法前缀**:事件处理函数使用 `handle`(如 `handleSearch`),数据获取使用 `fetch` 或 `get`。
|
||||||
|
* **样式隔离 (Scoped CSS)**:
|
||||||
|
* 必须启用 `scoped` 或 `CSS Modules`,严禁定义可能污染全局的类名。
|
||||||
|
* **严禁内联样式**:禁止在 HTML 标签上写大量 `style` 属性。
|
||||||
|
* **变量化**:颜色、字号必须引用预定义的 CSS 变量,严禁在组件内散落十六进制颜色值。
|
||||||
|
|
||||||
|
## 5. 框架专项标准 (Framework Specifics)
|
||||||
|
|
||||||
|
### Vue3 (uniapp / Naive UI)
|
||||||
|
* **语法糖**:必须使用 `<script setup>` 组合式 API。
|
||||||
|
* **响应式选择**:单一基本类型用 `ref`,结构化对象用 `reactive`。
|
||||||
|
* **uniapp 适配**:原生 API(扫码、支付)必须在 `src/utils/uni-tools.js` 中二次封装,禁止页面内直接调用原生接口。
|
||||||
|
|
||||||
|
### React
|
||||||
|
* **依赖闭包**:`useEffect` 和 `useCallback` 必须明确声明所有依赖项,严禁由于闭包陷阱导致状态陈旧。
|
||||||
|
* **组件更新**:优先使用 `React.memo` 优化高频刷新的子组件。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤖 智能体执行指令 (Agent Frontend Instructions)
|
||||||
|
|
||||||
|
1. **预审行数**:AI 生成代码前统计当前行数。若生成后 > 1000 行,AI 必须拒绝直接输出并主动提供逻辑拆分(Hooks 提取)方案。
|
||||||
|
2. **安全性补全**:
|
||||||
|
* 自动为后端返回的数据访问添加 `?.`。
|
||||||
|
* 自动在卸载钩子中生成资源清理代码(如 `clearInterval`)。
|
||||||
|
3. **按需引入校验**:AI 生成的 UI 组件代码必须采用按需导入模式,严禁 `import { ... } from 'ui-lib'` 全量导入。
|
||||||
|
4. **命名自检**:自动精简用户提供的冗长类名或方法名。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 强制执行门禁 (Submission Gaterails)
|
||||||
|
* **ESLint/Prettier**:必须 100% 通过校验。
|
||||||
|
* **TS 约束**:严禁滥用 `any`。所有后端接口返回数据必须定义 `interface` 或 `type`。
|
||||||
|
* **生产环境清理**:严禁残留 `console.log` 和测试注释。
|
||||||
|
* **一票否决**:违反任一强制规则(如白屏隐患、内存泄漏、样式全局污染),评审直接驳回。
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="fade-slide" mode="out-in" appear>
|
<transition name="fade-slide" mode="out-in" appear>
|
||||||
<section class="cus-scroll-y wh-full flex-col bg-[#f5f6fb] p-15 dark:bg-hex-121212">
|
<section class="app-page-shell cus-scroll-y wh-full">
|
||||||
<slot />
|
<div class="app-page-body">
|
||||||
<AppFooter v-if="showFooter" mt-15 />
|
<slot />
|
||||||
|
<AppFooter v-if="showFooter" mt-15 />
|
||||||
|
</div>
|
||||||
<n-back-top :bottom="20" />
|
<n-back-top :bottom="20" />
|
||||||
</section>
|
</section>
|
||||||
</transition>
|
</transition>
|
||||||
@@ -16,3 +18,19 @@ defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.app-page-shell {
|
||||||
|
position: relative;
|
||||||
|
padding: 16px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-page-body {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
min-height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -18,19 +18,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</AppPage>
|
</AppPage>
|
||||||
<n-modal
|
<n-modal
|
||||||
style="width: 25%"
|
class="client-list-header-modal"
|
||||||
|
style="width: min(760px, calc(100vw - 48px))"
|
||||||
v-model:show="addModalShow"
|
v-model:show="addModalShow"
|
||||||
preset="card"
|
preset="card"
|
||||||
title="添加"
|
title="添加"
|
||||||
header-style="margin-left: 40%"
|
header-style="text-align: center"
|
||||||
>
|
>
|
||||||
<n-form
|
<n-form
|
||||||
ref="addModalFormRef"
|
ref="addModalFormRef"
|
||||||
|
class="client-list-header-form"
|
||||||
:rules="addModalFormRules"
|
:rules="addModalFormRules"
|
||||||
:model="addModalForm"
|
:model="addModalForm"
|
||||||
label-placement="left"
|
label-placement="top"
|
||||||
label-width="auto"
|
|
||||||
label-align="left"
|
|
||||||
require-mark-placement="right"
|
require-mark-placement="right"
|
||||||
>
|
>
|
||||||
<n-form-item label="名称" path="name">
|
<n-form-item label="名称" path="name">
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<n-form-item label="邮箱" path="email">
|
<n-form-item label="邮箱" path="email">
|
||||||
<n-input v-model:value="addModalForm.email"/>
|
<n-input v-model:value="addModalForm.email"/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="IP" path="addModalForm.ipAllocation">
|
<n-form-item class="client-list-header-form__full" label="IP" path="addModalForm.ipAllocation">
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="addModalForm.ipAllocation"
|
v-model:value="addModalForm.ipAllocation"
|
||||||
filterable
|
filterable
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
:show="false"
|
:show="false"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="可访问IP" path="allowedIps">
|
<n-form-item class="client-list-header-form__full" label="可访问IP" path="allowedIps">
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="addModalForm.allowedIps"
|
v-model:value="addModalForm.allowedIps"
|
||||||
filterable
|
filterable
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
:show="false"
|
:show="false"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="可访问IP(扩展)" path="extraAllowedIps">
|
<n-form-item class="client-list-header-form__full" label="可访问IP(扩展)" path="extraAllowedIps">
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="addModalForm.extraAllowedIps"
|
v-model:value="addModalForm.extraAllowedIps"
|
||||||
filterable
|
filterable
|
||||||
@@ -72,35 +72,39 @@
|
|||||||
:show="false"
|
:show="false"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="服务端DNS" path="useServerDns">
|
<n-form-item class="client-list-header-form__full" label="服务端DNS" path="useServerDns">
|
||||||
<n-radio value="1" :checked="addModalForm.useServerDns === 1" @change="addModalForm.useServerDns = 1">是</n-radio>
|
<n-radio-group class="client-list-header-form__radio" :value="addModalForm.useServerDns">
|
||||||
<n-radio value="0" :checked="addModalForm.useServerDns === 0" @change="addModalForm.useServerDns = 0">否</n-radio>
|
<n-radio :value="1" :checked="addModalForm.useServerDns === 1" @change="addModalForm.useServerDns = 1">是</n-radio>
|
||||||
|
<n-radio :value="0" :checked="addModalForm.useServerDns === 0" @change="addModalForm.useServerDns = 0">否</n-radio>
|
||||||
|
</n-radio-group>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="公钥" path="keys.publicKey">
|
<n-form-item class="client-list-header-form__full" label="公钥" path="keys.publicKey">
|
||||||
<n-input v-model:value="addModalForm.keys.publicKey"></n-input>
|
<n-input v-model:value="addModalForm.keys.publicKey"></n-input>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="私钥" path="keys.privateKey">
|
<n-form-item class="client-list-header-form__full" label="私钥" path="keys.privateKey">
|
||||||
<n-input v-model:value="addModalForm.keys.privateKey"></n-input>
|
<n-input v-model:value="addModalForm.keys.privateKey"></n-input>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="共享密钥" path="keys.presharedKey">
|
<n-form-item class="client-list-header-form__full" label="共享密钥" path="keys.presharedKey">
|
||||||
<n-input v-model:value="addModalForm.keys.presharedKey"></n-input>
|
<n-input v-model:value="addModalForm.keys.presharedKey"></n-input>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item>
|
<n-form-item class="client-list-header-form__full">
|
||||||
<n-button style="margin-left: 28%" size="small" type="info" @click="generateKeys">生成密钥对</n-button>
|
<n-button size="small" secondary type="primary" @click="generateKeys">生成密钥对</n-button>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="状态" path="editModalForm.enabled">
|
<n-form-item class="client-list-header-form__full" label="状态" path="editModalForm.enabled">
|
||||||
<n-radio-group :value="addModalForm.enabled">
|
<n-radio-group class="client-list-header-form__radio" :value="addModalForm.enabled">
|
||||||
<n-radio :value="1" :checked="addModalForm.enabled === 1" @change="addModalForm.enabled = 1">启用</n-radio>
|
<n-radio :value="1" :checked="addModalForm.enabled === 1" @change="addModalForm.enabled = 1">启用</n-radio>
|
||||||
<n-radio :value="0" :checked="addModalForm.enabled === 0" @change="addModalForm.enabled = 0">禁用</n-radio>
|
<n-radio :value="0" :checked="addModalForm.enabled === 0" @change="addModalForm.enabled = 0">禁用</n-radio>
|
||||||
</n-radio-group>
|
</n-radio-group>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="离线监听" path="offlineMonitoring">
|
<n-form-item class="client-list-header-form__full" label="离线监听" path="offlineMonitoring">
|
||||||
<n-radio-group :value="addModalForm.offlineMonitoring">
|
<n-radio-group class="client-list-header-form__radio" :value="addModalForm.offlineMonitoring">
|
||||||
<n-radio :value="1" :checked="addModalForm.offlineMonitoring === 1" @change="addModalForm.offlineMonitoring = 1">启用</n-radio>
|
<n-radio :value="1" :checked="addModalForm.offlineMonitoring === 1" @change="addModalForm.offlineMonitoring = 1">启用</n-radio>
|
||||||
<n-radio :value="0" :checked="addModalForm.offlineMonitoring === 0" @change="addModalForm.offlineMonitoring = 0">禁用</n-radio>
|
<n-radio :value="0" :checked="addModalForm.offlineMonitoring === 0" @change="addModalForm.offlineMonitoring = 0">禁用</n-radio>
|
||||||
</n-radio-group>
|
</n-radio-group>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-button type="info" style="margin-left: 40%" @click="confirmAddClient()">确认</n-button>
|
<n-form-item class="client-list-header-form__full client-list-header-form__action">
|
||||||
|
<n-button type="primary" @click="confirmAddClient()">确认</n-button>
|
||||||
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
@@ -226,4 +230,74 @@ function refreshList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.client-list-header-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-list-header-form__full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-list-header-form__radio {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 18px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-list-header-form__action {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-list-header-form__action :deep(.n-button) {
|
||||||
|
min-width: 148px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-list-header-form :deep(.n-form-item-label) {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-list-header-form :deep(.n-input),
|
||||||
|
.client-list-header-form :deep(.n-base-selection) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-list-header-form :deep(.n-input .n-input__input-el),
|
||||||
|
.client-list-header-form :deep(.n-base-selection .n-base-selection-label) {
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.client-list-header-modal .n-card) {
|
||||||
|
width: min(760px, calc(100vw - 48px));
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.client-list-header-modal .n-card-header) {
|
||||||
|
padding: 22px 24px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.client-list-header-modal .n-card__content) {
|
||||||
|
padding: 12px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.client-list-header-form {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div flex items-center>
|
<div class="header-left">
|
||||||
<MenuCollapse />
|
<MenuCollapse />
|
||||||
<BreadCrumb ml-15 hidden sm:block />
|
<BreadCrumb class="header-breadcrumb" hidden sm:block />
|
||||||
</div>
|
</div>
|
||||||
<div ml-auto flex items-center v-if="loginUser.account === 'admin'">
|
<div v-if="loginUser.account === 'admin'" class="header-actions">
|
||||||
<Export/>
|
<Export/>
|
||||||
<FullScreen />
|
<FullScreen />
|
||||||
<UserAvatar />
|
<UserAvatar />
|
||||||
</div>
|
</div>
|
||||||
<div ml-auto flex items-center v-else>
|
<div v-else class="header-actions">
|
||||||
<FullScreen />
|
<FullScreen />
|
||||||
<UserAvatar />
|
<UserAvatar />
|
||||||
</div>
|
</div>
|
||||||
@@ -23,3 +23,24 @@ import Export from './components/Export.vue'
|
|||||||
import { useUserStore } from '@/store'
|
import { useUserStore } from '@/store'
|
||||||
const loginUser = useUserStore()
|
const loginUser = useUserStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-breadcrumb {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-link h-60 f-c-c to="/">
|
<router-link class="side-logo" to="/">
|
||||||
<img src="@/assets/images/logo.png" height="42" />
|
<div class="side-logo__mark">
|
||||||
<h2 v-show="!appStore.collapsed" ml-10 max-w-140 flex-shrink-0 text-16 font-bold color-primary>
|
<img src="@/assets/images/logo.png" height="42" />
|
||||||
{{ title }}
|
</div>
|
||||||
</h2>
|
<div v-show="!appStore.collapsed" class="side-logo__text">
|
||||||
|
<p class="side-logo__eyebrow">WireGuard Control</p>
|
||||||
|
<h2 class="side-logo__title">{{ title }}</h2>
|
||||||
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -13,3 +16,43 @@ const title = import.meta.env.VITE_TITLE
|
|||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.side-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding: 10px 8px;
|
||||||
|
border-radius: 14px;
|
||||||
|
color: #0f172a;
|
||||||
|
background: #eef4fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-logo__mark {
|
||||||
|
display: grid;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-logo__eyebrow {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-logo__title {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
max-width: 110px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -102,19 +102,82 @@ function handleMenuSelect(key, item) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.side-menu {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent !important;
|
||||||
|
|
||||||
|
:deep(.n-menu-item-content),
|
||||||
|
:deep(.n-submenu-children .n-menu-item-content),
|
||||||
|
:deep(.n-submenu-header) {
|
||||||
|
margin: 3px 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-menu-item-content) {
|
||||||
|
padding-left: 10px !important;
|
||||||
|
padding-right: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-menu-item-content::before),
|
||||||
|
:deep(.n-submenu-children .n-menu-item-content::before),
|
||||||
|
:deep(.n-submenu-header::before) {
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-menu-item-content-header),
|
||||||
|
:deep(.n-menu-item-content__icon),
|
||||||
|
:deep(.n-menu-item-content__arrow),
|
||||||
|
:deep(.n-menu-item-content__icon svg),
|
||||||
|
:deep(.n-submenu-header),
|
||||||
|
:deep(.n-submenu-header .n-menu-item-content-header),
|
||||||
|
:deep(.n-submenu-header .n-menu-item-content__icon),
|
||||||
|
:deep(.n-submenu-header .n-menu-item-content__arrow),
|
||||||
|
:deep(.n-submenu-header .n-icon) {
|
||||||
|
color: #334155 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-menu-item-content--selected),
|
||||||
|
:deep(.n-menu-item-content:hover),
|
||||||
|
:deep(.n-submenu-header:hover),
|
||||||
|
:deep(.n-submenu.n-submenu--child-active > .n-submenu-header) {
|
||||||
|
background: #eef4fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-menu-item-content--selected) {
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-menu-item-content--selected .n-menu-item-content-header),
|
||||||
|
:deep(.n-menu-item-content--selected .n-menu-item-content__icon),
|
||||||
|
:deep(.n-menu-item-content--selected .n-menu-item-content__icon svg),
|
||||||
|
:deep(.n-submenu.n-submenu--child-active > .n-submenu-header),
|
||||||
|
:deep(.n-submenu.n-submenu--child-active > .n-submenu-header .n-menu-item-content-header),
|
||||||
|
:deep(.n-submenu.n-submenu--child-active > .n-submenu-header .n-menu-item-content__icon),
|
||||||
|
:deep(.n-submenu.n-submenu--child-active > .n-submenu-header .n-icon) {
|
||||||
|
color: #0f172a !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.side-menu:not(.n-menu--collapsed) {
|
.side-menu:not(.n-menu--collapsed) {
|
||||||
.n-menu-item-content {
|
:deep(.n-menu-item-content) {
|
||||||
&::before {
|
&::before {
|
||||||
left: 5px;
|
left: 0;
|
||||||
right: 5px;
|
right: 0;
|
||||||
}
|
}
|
||||||
&.n-menu-item-content--selected,
|
&.n-menu-item-content--selected,
|
||||||
&:hover {
|
&:hover {
|
||||||
&::before {
|
&::before {
|
||||||
border-left: 4px solid var(--primary-color);
|
border-left: 3px solid #3b82f6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.n-submenu-header) {
|
||||||
|
padding-left: 10px !important;
|
||||||
|
padding-right: 10px !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,6 +4,17 @@ import SideMenu from './components/SideMenu.vue'
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SideLogo />
|
<div class="sidebar-shell">
|
||||||
<SideMenu />
|
<SideLogo />
|
||||||
|
<SideMenu />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.sidebar-shell {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px 8px 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ async function handleContextMenu(e, tagItem) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
.n-tag__close {
|
:deep(.n-tag__close) {
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-layout has-sider wh-full>
|
<n-layout class="app-shell" has-sider wh-full>
|
||||||
<n-layout-sider
|
<n-layout-sider
|
||||||
bordered
|
class="app-shell__sider"
|
||||||
collapse-mode="width"
|
collapse-mode="width"
|
||||||
:collapsed-width="64"
|
:collapsed-width="64"
|
||||||
:width="200"
|
:width="200"
|
||||||
@@ -11,19 +11,14 @@
|
|||||||
<SideBar />
|
<SideBar />
|
||||||
</n-layout-sider>
|
</n-layout-sider>
|
||||||
|
|
||||||
<article flex-col flex-1 overflow-hidden>
|
<article class="app-shell__main">
|
||||||
<header
|
<header class="app-shell__header" :style="`height: ${header.height}px`">
|
||||||
border-b="1 solid #eee"
|
|
||||||
class="flex items-center bg-white px-15"
|
|
||||||
dark="bg-dark border-0"
|
|
||||||
:style="`height: ${header.height}px`"
|
|
||||||
>
|
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
</header>
|
</header>
|
||||||
<section v-if="tags.visible" hidden border-b bc-eee sm:block dark:border-0>
|
<section v-if="tags.visible" class="app-shell__tags" hidden sm:block>
|
||||||
<AppTags :style="{ height: `${tags.height}px` }" />
|
<AppTags :style="{ height: `${tags.height}px` }" />
|
||||||
</section>
|
</section>
|
||||||
<section flex-1 overflow-hidden bg-hex-f5f6fb dark:bg-hex-101014>
|
<section class="app-shell__content">
|
||||||
<AppMain />
|
<AppMain />
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
@@ -40,3 +35,53 @@ import { header, tags } from '~/settings'
|
|||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.app-shell {
|
||||||
|
padding: 12px;
|
||||||
|
gap: 12px;
|
||||||
|
background: linear-gradient(180deg, #f3f6fa 0%, #ecf1f7 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell__sider {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--app-shell-border);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: #f8fafc;
|
||||||
|
box-shadow: var(--app-shell-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell__main {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell__header,
|
||||||
|
.app-shell__tags {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--app-shell-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--app-shell-bg);
|
||||||
|
box-shadow: var(--app-shell-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell__header {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell__tags {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell__content {
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import App from './App.vue'
|
|||||||
import { setupNaiveDiscreteApi } from './utils'
|
import { setupNaiveDiscreteApi } from './utils'
|
||||||
import mitt from 'mitt'
|
import mitt from 'mitt'
|
||||||
|
|
||||||
const EventMitt = mitt();
|
const eventBus = mitt()
|
||||||
|
|
||||||
async function setupApp() {
|
async function setupApp() {
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
setupStore(app)
|
setupStore(app)
|
||||||
app.config.globalProperties.$bus = EventMitt;
|
app.config.globalProperties.$bus = eventBus
|
||||||
await setupRouter(app)
|
await setupRouter(app)
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
setupNaiveDiscreteApi()
|
setupNaiveDiscreteApi()
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
color: #162033;
|
||||||
|
background: #eef3f8;
|
||||||
|
font-family: 'Avenir Next', 'PingFang SC', 'Helvetica Neue', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--app-shell-bg: #ffffff;
|
||||||
|
--app-shell-border: rgba(148, 163, 184, 0.14);
|
||||||
|
--app-shell-shadow: 0 10px 24px rgba(15, 23, 42, 0.06);
|
||||||
|
--app-panel-shadow: 0 8px 20px rgba(15, 23, 42, 0.05);
|
||||||
|
--app-panel-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -10,6 +27,15 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-card {
|
||||||
|
border-color: var(--app-shell-border);
|
||||||
|
box-shadow: var(--app-panel-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
/* transition fade-slide */
|
/* transition fade-slide */
|
||||||
.fade-slide-leave-active,
|
.fade-slide-leave-active,
|
||||||
.fade-slide-enter-active {
|
.fade-slide-enter-active {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as NaiveUI from 'naive-ui'
|
import { computed } from 'vue'
|
||||||
|
import { createDiscreteApi, darkTheme } from 'naive-ui'
|
||||||
import { isNullOrUndef } from '@/utils'
|
import { isNullOrUndef } from '@/utils'
|
||||||
import { naiveThemeOverrides as themeOverrides } from '~/settings'
|
import { naiveThemeOverrides as themeOverrides } from '~/settings'
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
@@ -84,10 +85,10 @@ export function setupDialog(NDialog) {
|
|||||||
export function setupNaiveDiscreteApi() {
|
export function setupNaiveDiscreteApi() {
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const configProviderProps = computed(() => ({
|
const configProviderProps = computed(() => ({
|
||||||
theme: appStore.isDark ? NaiveUI.darkTheme : undefined,
|
theme: appStore.isDark ? darkTheme : undefined,
|
||||||
themeOverrides,
|
themeOverrides,
|
||||||
}))
|
}))
|
||||||
const { message, dialog, notification, loadingBar } = NaiveUI.createDiscreteApi(
|
const { message, dialog, notification, loadingBar } = createDiscreteApi(
|
||||||
['message', 'dialog', 'notification', 'loadingBar'],
|
['message', 'dialog', 'notification', 'loadingBar'],
|
||||||
{ configProviderProps }
|
{ configProviderProps }
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppPage>
|
<AppPage>
|
||||||
<n-card>
|
<n-card class="setting-card form-surface" :bordered="false">
|
||||||
<n-tabs default-value="Server" justify-content="space-evenly" type="line" @update-value="tabChange">
|
<n-tabs class="setting-tabs" default-value="Server" justify-content="space-evenly" type="line" @update-value="tabChange">
|
||||||
<n-tab-pane name="Server" tab="服务端">
|
<n-tab-pane name="Server" tab="服务端">
|
||||||
<n-form
|
<n-form
|
||||||
|
class="setting-form"
|
||||||
ref="serverFormRef"
|
ref="serverFormRef"
|
||||||
:model="serverFormModel"
|
:model="serverFormModel"
|
||||||
:rules="serverFormRules"
|
:rules="serverFormRules"
|
||||||
|
label-placement="top"
|
||||||
>
|
>
|
||||||
<n-form-item label="IP段" path="ipScope" :rule="{
|
<n-form-item label="IP段" path="ipScope" :rule="{
|
||||||
required: true,
|
required: true,
|
||||||
@@ -44,34 +46,36 @@
|
|||||||
trigger: ['change','blur']
|
trigger: ['change','blur']
|
||||||
}
|
}
|
||||||
]">
|
]">
|
||||||
<n-input-number :min="1120" :max="65535" v-model:value="serverFormModel.listenPort"/>
|
<n-input-number :min="1120" :max="65535" v-model:value="serverFormModel.listenPort" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="私钥" path="privateKey">
|
<n-form-item label="私钥" path="privateKey">
|
||||||
<n-input v-model:value="serverFormModel.privateKey"/>
|
<n-input v-model:value="serverFormModel.privateKey" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="公钥" path="publicKey">
|
<n-form-item label="公钥" path="publicKey">
|
||||||
<n-input v-model:value="serverFormModel.publicKey"/>
|
<n-input v-model:value="serverFormModel.publicKey" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="上行脚本" path="postUpScript">
|
<n-form-item label="上行脚本" path="postUpScript">
|
||||||
<n-input v-model:value="serverFormModel.postUpScript"/>
|
<n-input v-model:value="serverFormModel.postUpScript" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="下行脚本" path="postDownScript">
|
<n-form-item label="下行脚本" path="postDownScript">
|
||||||
<n-input v-model:value="serverFormModel.postDownScript"/>
|
<n-input v-model:value="serverFormModel.postDownScript" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item>
|
<n-form-item class="setting-form__action">
|
||||||
<n-button type="info" @click="updateServerConf">确认</n-button>
|
<n-button type="primary" @click="updateServerConf">保存服务端配置</n-button>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="Global" tab="全局">
|
<n-tab-pane name="Global" tab="全局">
|
||||||
<n-form
|
<n-form
|
||||||
|
class="setting-form"
|
||||||
ref="globalFormRef"
|
ref="globalFormRef"
|
||||||
:model="globalFormModel"
|
:model="globalFormModel"
|
||||||
:rules="globalFormRules"
|
:rules="globalFormRules"
|
||||||
|
label-placement="top"
|
||||||
>
|
>
|
||||||
<n-form-item label="公网IP" path="endpointAddress" class="pid">
|
<n-form-item label="公网IP" path="endpointAddress" class="setting-form__endpoint">
|
||||||
<n-input v-model:value="globalFormModel.endpointAddress"/>
|
<n-input v-model:value="globalFormModel.endpointAddress" />
|
||||||
<n-button style="margin-top: 5px" size="small" type="warning" @click="getPublicAddr">获取地址</n-button>
|
<n-button class="setting-form__inline-btn" size="small" secondary type="primary" @click="getPublicAddr">获取地址</n-button>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="DNS" path="dnsServer" :rule="{
|
<n-form-item label="DNS" path="dnsServer" :rule="{
|
||||||
required: true,
|
required: true,
|
||||||
@@ -109,7 +113,7 @@
|
|||||||
trigger: ['change','blur']
|
trigger: ['change','blur']
|
||||||
}
|
}
|
||||||
]">
|
]">
|
||||||
<n-input-number :min="100" :max="3000" v-model:value="globalFormModel.MTU"/>
|
<n-input-number :min="100" :max="3000" v-model:value="globalFormModel.MTU" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="persistentKeepalive" path="persistentKeepalive" :rule="[
|
<n-form-item label="persistentKeepalive" path="persistentKeepalive" :rule="[
|
||||||
{
|
{
|
||||||
@@ -131,24 +135,24 @@
|
|||||||
trigger: ['change','blur']
|
trigger: ['change','blur']
|
||||||
}
|
}
|
||||||
]">
|
]">
|
||||||
<n-input-number :min="15" :max="300" v-model:value="globalFormModel.persistentKeepalive"/>
|
<n-input-number :min="15" :max="300" v-model:value="globalFormModel.persistentKeepalive" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="firewallMark" path="firewallMark">
|
<n-form-item label="firewallMark" path="firewallMark">
|
||||||
<n-input v-model:value="globalFormModel.firewallMark"/>
|
<n-input v-model:value="globalFormModel.firewallMark" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="table" path="table">
|
<n-form-item label="table" path="table">
|
||||||
<n-input v-model:value="globalFormModel.table"/>
|
<n-input v-model:value="globalFormModel.table" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="configPath" path="configFilePath">
|
<n-form-item label="configPath" path="configFilePath">
|
||||||
<n-input v-model:value="globalFormModel.configFilePath"/>
|
<n-input v-model:value="globalFormModel.configFilePath" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item>
|
<n-form-item class="setting-form__action">
|
||||||
<n-button type="info" @click="updateGlobalConf">确认</n-button>
|
<n-button type="primary" @click="updateGlobalConf">保存全局配置</n-button>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="Other" tab="其他">
|
<n-tab-pane name="Other" tab="其他">
|
||||||
<n-button style="float:right;margin-bottom: 10px" size="small" type="info" @click="showAddModal = !showAddModal">添加</n-button>
|
<n-button class="setting-other__add" size="small" type="primary" @click="showAddModal = !showAddModal">添加</n-button>
|
||||||
<n-data-table
|
<n-data-table
|
||||||
:columns="tableColumns"
|
:columns="tableColumns"
|
||||||
:data="taleData.data"
|
:data="taleData.data"
|
||||||
@@ -160,15 +164,18 @@
|
|||||||
:title="editFormModel.describe"
|
:title="editFormModel.describe"
|
||||||
v-model:show="showEditModal"
|
v-model:show="showEditModal"
|
||||||
preset="card"
|
preset="card"
|
||||||
style="width: 30%"
|
class="setting-modal"
|
||||||
|
style="width: min(560px, calc(100vw - 48px))"
|
||||||
>
|
>
|
||||||
<n-form
|
<n-form
|
||||||
|
class="setting-form setting-form--modal"
|
||||||
ref="editFormRef"
|
ref="editFormRef"
|
||||||
:model="editFormModel"
|
:model="editFormModel"
|
||||||
|
label-placement="top"
|
||||||
>
|
>
|
||||||
<n-form-item v-for="(item,index) in editFormModel.data" :label="index">
|
<n-form-item v-for="(item,index) in editFormModel.data" :label="index">
|
||||||
<n-input v-if="typeof item === 'string'" v-model:value="editFormModel.data[index]"/>
|
<n-input v-if="typeof item === 'string'" v-model:value="editFormModel.data[index]" />
|
||||||
<n-input-number v-else-if="typeof item === 'number'" v-model:value="editFormModel.data[index]"/>
|
<n-input-number v-else-if="typeof item === 'number'" v-model:value="editFormModel.data[index]" />
|
||||||
<n-radio-group v-else-if="typeof item === 'boolean'" :value="editFormModel.data[index]">
|
<n-radio-group v-else-if="typeof item === 'boolean'" :value="editFormModel.data[index]">
|
||||||
<n-radio :value="true" :checked="editFormModel.data[index] === true" @change="editFormModel.data[index] = true">是</n-radio>
|
<n-radio :value="true" :checked="editFormModel.data[index] === true" @change="editFormModel.data[index] = true">是</n-radio>
|
||||||
<n-radio :value="false" :checked="editFormModel.data[index] === false" @change="editFormModel.data[index] = false">否</n-radio>
|
<n-radio :value="false" :checked="editFormModel.data[index] === false" @change="editFormModel.data[index] = false">否</n-radio>
|
||||||
@@ -177,8 +184,8 @@
|
|||||||
<n-form-item label="配置描述">
|
<n-form-item label="配置描述">
|
||||||
<n-input v-model:value="editFormModel.describe" />
|
<n-input v-model:value="editFormModel.describe" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item>
|
<n-form-item class="setting-form__action">
|
||||||
<n-button type="info" @click="updateSetting">确认</n-button>
|
<n-button type="primary" @click="updateSetting">保存</n-button>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
@@ -186,20 +193,21 @@
|
|||||||
title="添加"
|
title="添加"
|
||||||
v-model:show="showAddModal"
|
v-model:show="showAddModal"
|
||||||
preset="card"
|
preset="card"
|
||||||
style="width: 30%"
|
class="setting-modal"
|
||||||
|
style="width: min(560px, calc(100vw - 48px))"
|
||||||
>
|
>
|
||||||
<n-form :model="addFormModel" ref="addFormRef">
|
<n-form class="setting-form setting-form--modal" :model="addFormModel" ref="addFormRef" label-placement="top">
|
||||||
<n-form-item label="Code">
|
<n-form-item label="Code">
|
||||||
<n-input v-model:value="addFormModel.code"></n-input>
|
<n-input v-model:value="addFormModel.code" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="选项">
|
<n-form-item label="选项">
|
||||||
<n-dynamic-input v-model:value="addFormModel.data" preset="pair" key-placeholder="键" value-placeholder="值"/>
|
<n-dynamic-input v-model:value="addFormModel.data" preset="pair" key-placeholder="键" value-placeholder="值" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="描述">
|
<n-form-item label="描述">
|
||||||
<n-input v-model:value="addFormModel.describe"></n-input>
|
<n-input v-model:value="addFormModel.describe" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item>
|
<n-form-item class="setting-form__action">
|
||||||
<n-button type="info" @click="addSetting">确认</n-button>
|
<n-button type="primary" @click="addSetting">添加配置</n-button>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
@@ -395,12 +403,11 @@ async function getPublicAddr() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取全部配置
|
// 获取全部配置
|
||||||
function allSetting() {
|
async function allSetting() {
|
||||||
api.allSettings().then(res => {
|
const res = await api.allSettings()
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
taleData.value.data = res.data.data
|
taleData.value.data = res.data.data
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// tab切换事件
|
// tab切换事件
|
||||||
@@ -490,7 +497,7 @@ async function addSetting() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$bus.on("refreshSetting",value => {
|
const handleRefreshSetting = (value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
if (tabCode.value === "" || tabCode.value === undefined) {
|
if (tabCode.value === "" || tabCode.value === undefined) {
|
||||||
getServerConfig()
|
getServerConfig()
|
||||||
@@ -505,11 +512,127 @@ $bus.on("refreshSetting",value => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$bus.on("refreshSetting", handleRefreshSetting)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
$bus.off("refreshSetting", handleRefreshSetting)
|
||||||
})
|
})
|
||||||
getServerConfig()
|
getServerConfig()
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style scoped lang="scss">
|
||||||
.pid .n-form-item-blank {
|
.setting-card {
|
||||||
display: inline;
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 18px 45px rgba(15, 23, 42, 0.06);
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
.setting-tabs {
|
||||||
|
:deep(.n-tabs-nav) {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-form--modal {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-form__wide {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-form :deep(.n-form-item) {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-form :deep(.n-input),
|
||||||
|
.setting-form :deep(.n-input-number),
|
||||||
|
.setting-form :deep(.n-base-selection) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-form :deep(.n-form-item-label) {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-form :deep(.n-input .n-input__input-el),
|
||||||
|
.setting-form :deep(.n-base-selection .n-base-selection-label),
|
||||||
|
.setting-form :deep(.n-input-number .n-input__input-el) {
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-form__action {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-form__action :deep(.n-button) {
|
||||||
|
min-width: 148px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-form__inline-btn {
|
||||||
|
min-width: 104px;
|
||||||
|
height: 42px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-other__add {
|
||||||
|
float: right;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-form__endpoint :deep(.n-form-item-blank) {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-form__endpoint :deep(.n-input) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-form :deep(.n-input-number .n-input-number-button) {
|
||||||
|
width: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-form :deep(.n-input-number .n-input-number-button-icon) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.setting-modal) {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.setting-modal .n-card) {
|
||||||
|
width: min(560px, calc(100vw - 48px));
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.setting-modal .n-card-header) {
|
||||||
|
padding: 22px 24px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.setting-modal .n-card__content) {
|
||||||
|
padding: 12px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.setting-form {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -13,8 +13,10 @@
|
|||||||
{{ $route.meta.title }}
|
{{ $route.meta.title }}
|
||||||
</template>
|
</template>
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-button v-if="useUserStore().isAdmin === 1" size="small" type="info" @click="addUser()">添加</n-button>
|
<div class="user-header__actions">
|
||||||
<n-button style="margin-left: 5px" size="small" type="primary" @click="getUserList()">刷新</n-button>
|
<n-button v-if="useUserStore().isAdmin === 1" size="small" type="info" @click="addUser()">添加</n-button>
|
||||||
|
<n-button size="small" type="primary" @click="getUserList()">刷新</n-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<n-data-table
|
<n-data-table
|
||||||
remote
|
remote
|
||||||
@@ -33,33 +35,32 @@
|
|||||||
:title="infoFormModel.nickname || '个人资料'"
|
:title="infoFormModel.nickname || '个人资料'"
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
size="large"
|
size="large"
|
||||||
style="width: 400px"
|
class="user-form-modal"
|
||||||
|
style="width: min(520px, calc(100vw - 48px))"
|
||||||
header-style="text-align: center"
|
header-style="text-align: center"
|
||||||
>
|
>
|
||||||
<n-form
|
<n-form
|
||||||
ref="infoFormRef"
|
ref="infoFormRef"
|
||||||
|
class="user-form"
|
||||||
:rules="infoFormRules"
|
:rules="infoFormRules"
|
||||||
:model="infoFormModel"
|
:model="infoFormModel"
|
||||||
label-placement="left"
|
label-placement="top"
|
||||||
label-width="auto"
|
|
||||||
label-align="right"
|
|
||||||
require-mark-placement="left"
|
|
||||||
>
|
>
|
||||||
<n-form-item label="账号" path="account">
|
<n-form-item label="账号" path="account">
|
||||||
<n-input v-if="infoFormModel.id !== ''" disabled v-model:value="infoFormModel.account"></n-input>
|
<n-input v-if="infoFormModel.id !== ''" disabled v-model:value="infoFormModel.account" />
|
||||||
<n-input v-else v-model:value="infoFormModel.account"></n-input>
|
<n-input v-else v-model:value="infoFormModel.account" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item v-if="infoFormModel.id === ''" label="密码" path="password">
|
<n-form-item v-if="infoFormModel.id === ''" label="密码" path="password">
|
||||||
<n-input type="password" v-model:value="infoFormModel.password"></n-input>
|
<n-input type="password" v-model:value="infoFormModel.password" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="昵称" path="nickname">
|
<n-form-item label="昵称" path="nickname">
|
||||||
<n-input v-model:value="infoFormModel.nickname"></n-input>
|
<n-input v-model:value="infoFormModel.nickname" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="联系方式" path="contact">
|
<n-form-item label="联系方式" path="contact">
|
||||||
<n-input v-model:value="infoFormModel.contact"></n-input>
|
<n-input v-model:value="infoFormModel.contact" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="管理员">
|
<n-form-item class="user-form__full" label="管理员">
|
||||||
<n-space>
|
<n-space class="user-form__radio">
|
||||||
<n-radio
|
<n-radio
|
||||||
:checked="infoFormModel.isAdmin === 1"
|
:checked="infoFormModel.isAdmin === 1"
|
||||||
value="1"
|
value="1"
|
||||||
@@ -76,8 +77,8 @@
|
|||||||
</n-radio>
|
</n-radio>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="状态">
|
<n-form-item class="user-form__full" label="状态">
|
||||||
<n-space>
|
<n-space class="user-form__radio">
|
||||||
<n-radio
|
<n-radio
|
||||||
:checked="infoFormModel.status === 1"
|
:checked="infoFormModel.status === 1"
|
||||||
value="1"
|
value="1"
|
||||||
@@ -94,7 +95,9 @@
|
|||||||
</n-radio>
|
</n-radio>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-button style="margin-left: 20%" type="primary" @click="SaveUser(infoFormModel)">确认</n-button>
|
<n-form-item class="user-form__full user-form__action">
|
||||||
|
<n-button type="primary" @click="SaveUser(infoFormModel)">保存用户</n-button>
|
||||||
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
@@ -403,12 +406,93 @@ function addUser() {
|
|||||||
showInfoModel.value = true
|
showInfoModel.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
$bus.on('refreshUserInfo',value => {
|
const handleRefreshUserInfo = (value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
getUserList();
|
getUserList()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$bus.on('refreshUserInfo', handleRefreshUserInfo)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
$bus.off('refreshUserInfo', handleRefreshUserInfo)
|
||||||
})
|
})
|
||||||
|
|
||||||
getUserList()
|
getUserList()
|
||||||
</script>
|
</script>
|
||||||
<style></style>
|
<style scoped lang="scss">
|
||||||
|
.user-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-form__full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-header__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-form__radio {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 18px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-form__action {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-form__action :deep(.n-button) {
|
||||||
|
min-width: 148px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-form :deep(.n-form-item-label) {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-form :deep(.n-input) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-form :deep(.n-input .n-input__input-el) {
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.user-form-modal) {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.user-form-modal .n-card) {
|
||||||
|
width: min(520px, calc(100vw - 48px));
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.user-form-modal .n-card-header) {
|
||||||
|
padding: 22px 24px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.user-form-modal .n-card__content) {
|
||||||
|
padding: 12px 24px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.user-form {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,46 +1,71 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppPage :show-footer="true">
|
<AppPage :show-footer="true">
|
||||||
<div class="flex">
|
<section class="workbench-overview">
|
||||||
<n-card class="w-30%">
|
<div class="workbench-overview__title">
|
||||||
<div class="flex items-center">
|
<h1>概览</h1>
|
||||||
<n-avatar round :size="60" :src="userStore.avatar" />
|
<p>查看客户端连接和最近操作。</p>
|
||||||
<div class="ml-20 flex-col">
|
</div>
|
||||||
<span class="text-20 opacity-80">Hello, {{ userStore.nickname }}</span>
|
<div class="workbench-overview__stats">
|
||||||
<span class="mt-4 opacity-50">今日事,今日毕。</span>
|
<span>在线客户端 {{ onlineCount }}</span>
|
||||||
</div>
|
<span>客户端总数 {{ connectionCount }}</span>
|
||||||
|
<span>日志 {{ paginate.itemCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="workbench-overview__user">
|
||||||
|
<n-avatar round :size="40" :src="userStore.avatar" />
|
||||||
|
<div>
|
||||||
|
<strong>{{ userStore.nickname }}</strong>
|
||||||
|
<p>{{ dailyPoetry.content || '莫向外求,但从心觅,行有不得,反求诸己。' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<p class="mt-40 text-14 opacity-60">{{ dailyPoetry.content || '莫向外求,但从心觅,行有不得,反求诸己。' }}</p>
|
<section class="workbench-grid">
|
||||||
<p class="mt-32 text-right text-12 opacity-40">—— {{ dailyPoetry.author || '佚名' }}</p>
|
<n-card class="panel-card" :bordered="false">
|
||||||
</n-card>
|
<template #header>
|
||||||
<n-card class="ml-12 w-70%">
|
<div class="panel-card__header">
|
||||||
|
<h3>最近操作日志</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<n-data-table
|
<n-data-table
|
||||||
|
class="dashboard-table"
|
||||||
remote
|
remote
|
||||||
:columns="tableColumns"
|
:columns="tableColumns"
|
||||||
:data="tableData.data"
|
:data="tableData.data"
|
||||||
:pagination="paginate"
|
:pagination="paginate"
|
||||||
/>
|
/>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
|
||||||
<n-card>
|
<n-card class="panel-card" :bordered="false">
|
||||||
<n-data-table
|
<template #header>
|
||||||
remote
|
<div class="panel-card__header">
|
||||||
:columns="connectionsColumns"
|
<h3>客户端连接状态</h3>
|
||||||
:data="connectionsData.data"
|
</div>
|
||||||
:row-props="rowProps"
|
</template>
|
||||||
/>
|
<div
|
||||||
<n-dropdown
|
class="connections-panel"
|
||||||
placement="bottom-start"
|
@click="closeDropdown"
|
||||||
trigger="manual"
|
@contextmenu.prevent="openDropdown"
|
||||||
size="small"
|
>
|
||||||
:x="xRef"
|
<n-data-table
|
||||||
:y="yRef"
|
class="dashboard-table"
|
||||||
:options="rightMenuOpts"
|
remote
|
||||||
@select="rowSelect"
|
:columns="connectionsColumns"
|
||||||
@clickoutside="rowClick"
|
:data="connectionsData.data"
|
||||||
:show="showDropdownRef"
|
/>
|
||||||
/>
|
<n-dropdown
|
||||||
</n-card>
|
placement="bottom-start"
|
||||||
|
trigger="manual"
|
||||||
|
size="small"
|
||||||
|
:x="xRef"
|
||||||
|
:y="yRef"
|
||||||
|
:options="rightMenuOpts"
|
||||||
|
@select="rowSelect"
|
||||||
|
@clickoutside="closeDropdown"
|
||||||
|
:show="showDropdownRef"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</section>
|
||||||
</AppPage>
|
</AppPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -49,8 +74,27 @@ import { useUserStore } from '@/store'
|
|||||||
import api from '@/views/workbench/api'
|
import api from '@/views/workbench/api'
|
||||||
import { debounce, renderIcon } from '@/utils'
|
import { debounce, renderIcon } from '@/utils'
|
||||||
import { NTag } from 'naive-ui'
|
import { NTag } from 'naive-ui'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const tableData = ref({
|
||||||
|
data: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const connectionsData = ref({
|
||||||
|
data: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const dailyPoetry = ref({
|
||||||
|
author: '',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const onlineCount = computed(
|
||||||
|
() => connectionsData.value.data.filter((item) => item.online === true).length
|
||||||
|
)
|
||||||
|
const connectionCount = computed(() => connectionsData.value.data.length)
|
||||||
|
|
||||||
// 表格表头
|
// 表格表头
|
||||||
const tableColumns = [
|
const tableColumns = [
|
||||||
{
|
{
|
||||||
@@ -96,6 +140,7 @@ const tableColumns = [
|
|||||||
titleAlign: 'center'
|
titleAlign: 'center'
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// 链接信息列表
|
// 链接信息列表
|
||||||
const connectionsColumns = [
|
const connectionsColumns = [
|
||||||
{
|
{
|
||||||
@@ -130,15 +175,15 @@ const connectionsColumns = [
|
|||||||
render: (row) => {
|
render: (row) => {
|
||||||
switch (row.online) {
|
switch (row.online) {
|
||||||
case true:
|
case true:
|
||||||
return h(NTag,{
|
return h(NTag, {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
},{
|
}, {
|
||||||
default: () => '在线'
|
default: () => '在线'
|
||||||
})
|
})
|
||||||
case false:
|
case false:
|
||||||
return h(NTag,{
|
return h(NTag, {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
},{
|
}, {
|
||||||
default: () => '离线'
|
default: () => '离线'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -164,77 +209,56 @@ const connectionsColumns = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// 链接列表邮件刷新菜单
|
|
||||||
const rightMenuOpts = [
|
const rightMenuOpts = [
|
||||||
{
|
{
|
||||||
label: () => h('span',{ style: { color: 'green' }}, '刷新'),
|
label: () => h('span', { style: { color: 'green' } }, '刷新'),
|
||||||
key: "refresh",
|
key: 'refresh',
|
||||||
icon: renderIcon('tabler:refresh',{ size: 14 })
|
icon: renderIcon('tabler:refresh', { size: 14 })
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// 右键菜单的设置
|
const showDropdownRef = ref(false)
|
||||||
const showDropdownRef = ref(false);
|
const xRef = ref(0)
|
||||||
const xRef = ref(0);
|
const yRef = ref(0)
|
||||||
const yRef = ref(0);
|
|
||||||
|
|
||||||
// 右键菜单的基本位置逻辑
|
function openDropdown(e) {
|
||||||
function rowProps(row) {
|
showDropdownRef.value = false
|
||||||
return {
|
nextTick().then(() => {
|
||||||
onContextmenu: (e) => {
|
showDropdownRef.value = true
|
||||||
// $message.info(JSON.stringify(row, null, 2));
|
xRef.value = e.clientX
|
||||||
e.preventDefault();
|
yRef.value = e.clientY
|
||||||
showDropdownRef.value = false;
|
})
|
||||||
nextTick().then(() => {
|
|
||||||
showDropdownRef.value = true;
|
|
||||||
xRef.value = e.clientX;
|
|
||||||
yRef.value = e.clientY;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 右键菜单的逻辑
|
function closeDropdown() {
|
||||||
function rowSelect(row) {
|
|
||||||
switch (row) {
|
|
||||||
case "refresh":
|
|
||||||
getClientConnections()
|
|
||||||
showDropdownRef.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function rowClick() {
|
|
||||||
showDropdownRef.value = false
|
showDropdownRef.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表格数据
|
function onWindowKeydown(e) {
|
||||||
const tableData = ref({
|
if (e.key === 'Escape') {
|
||||||
data: []
|
closeDropdown()
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 链接数据
|
function rowSelect(row) {
|
||||||
const connectionsData = ref({
|
switch (row) {
|
||||||
data: []
|
case 'refresh':
|
||||||
})
|
getClientConnections()
|
||||||
|
closeDropdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dailyPoetry = ref({
|
|
||||||
author: '',
|
|
||||||
content: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// 页码控件
|
|
||||||
const paginate = reactive({
|
const paginate = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 2,
|
pageSize: 2,
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
pageCount: 0,
|
pageCount: 0,
|
||||||
onChange: (page) => {
|
onChange: (page) => {
|
||||||
paginate.page = page;
|
paginate.page = page
|
||||||
getLogsList()
|
getLogsList()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取操作日志列表
|
|
||||||
async function getLogsList() {
|
async function getLogsList() {
|
||||||
try {
|
try {
|
||||||
const res = await api.logsList({
|
const res = await api.logsList({
|
||||||
@@ -242,56 +266,153 @@ async function getLogsList() {
|
|||||||
size: paginate.pageSize,
|
size: paginate.pageSize,
|
||||||
})
|
})
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
tableData.value.data = res.data.data.records;
|
tableData.value.data = res.data.data.records
|
||||||
paginate.itemCount = res.data.data.total;
|
paginate.itemCount = res.data.data.total
|
||||||
paginate.pageCount = res.data.data.totalPage;
|
paginate.pageCount = res.data.data.totalPage
|
||||||
}
|
}
|
||||||
}catch (error) {
|
} catch (error) {
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 每日诗词
|
|
||||||
const dailyPoe = debounce(() => {
|
const dailyPoe = debounce(() => {
|
||||||
getDailyPoetry()
|
getDailyPoetry()
|
||||||
},800)
|
}, 800)
|
||||||
|
|
||||||
// 获取每日诗词
|
async function getDailyPoetry() {
|
||||||
function getDailyPoetry() {
|
|
||||||
try {
|
try {
|
||||||
api.dailyPoetry().then(res => {
|
const res = await api.dailyPoetry()
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
dailyPoetry.value.author = res.data.data.author;
|
dailyPoetry.value.author = res.data.data.author
|
||||||
dailyPoetry.value.content = res.data.data.content;
|
dailyPoetry.value.content = res.data.data.content
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
}catch (error) {
|
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectionList = debounce(() => {
|
|
||||||
getClientConnections()
|
|
||||||
},300)
|
|
||||||
|
|
||||||
// 获取客户端链接列表
|
|
||||||
async function getClientConnections() {
|
async function getClientConnections() {
|
||||||
try {
|
try {
|
||||||
const res = await api.clientConnections()
|
const res = await api.clientConnections()
|
||||||
if (res.data.code === 200) {
|
if (res.data.code === 200) {
|
||||||
connectionsData.value.data = res.data.data;
|
connectionsData.value.data = res.data.data
|
||||||
}
|
}
|
||||||
}catch (e) {
|
} catch (e) {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initFunc = debounce(() => {
|
const initFunc = debounce(() => {
|
||||||
getClientConnections()
|
getClientConnections()
|
||||||
// dailyPoe()
|
dailyPoe()
|
||||||
// connectionList()
|
}, 500)
|
||||||
},500)
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('keydown', onWindowKeydown)
|
||||||
|
window.addEventListener('blur', closeDropdown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('keydown', onWindowKeydown)
|
||||||
|
window.removeEventListener('blur', closeDropdown)
|
||||||
|
})
|
||||||
|
|
||||||
getLogsList()
|
getLogsList()
|
||||||
initFunc()
|
initFunc()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.workbench-overview {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 4px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-overview__title h1,
|
||||||
|
.panel-card__header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-overview__title p {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-overview__stats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-overview__stats span {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #475569;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-overview__user {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-overview__user strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-overview__user p {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-card {
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-table:deep(.n-data-table-wrapper) {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connections-panel {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.workbench-overview {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-overview__user {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user