Compare commits

...

14 Commits

Author SHA1 Message Date
coward
72b8473591 🐛导入配置时,获取当前系统的公网IP,避免手动去更改
All checks were successful
continuous-integration/drone/tag Build is passing
2024-09-24 10:23:20 +08:00
coward
13e4006592 🎨加了一些组件数据监听优化
All checks were successful
continuous-integration/drone/tag Build is passing
2024-09-23 17:17:29 +08:00
coward
f2dcb13e0d 🆕导入配置文件完成 2024-09-23 15:58:46 +08:00
coward
edaf9ba770 🆕为实现数据迁移以及备份新增配置导出 2024-09-20 17:26:41 +08:00
coward
c3ef51e87f 🐛errors包导入掉了
All checks were successful
continuous-integration/drone/tag Build is passing
2024-09-04 11:42:01 +08:00
coward
72420f2ede 🐛修复可重复创建相同账号的bug
Some checks failed
continuous-integration/drone/tag Build is failing
2024-09-04 10:06:29 +08:00
coward
a12552a608 🎨去除首页的诗词
All checks were successful
continuous-integration/drone/tag Build is passing
2024-08-27 09:48:48 +08:00
coward
5f200ea989 🎨不想写啦 2024-08-23 11:25:27 +08:00
coward
3f14df72be 📝修改文档 2024-08-23 10:36:21 +08:00
coward
29902afe65 📝添加readme 2024-08-23 10:29:37 +08:00
ddef41dfca 添加 web/.env
All checks were successful
continuous-integration/drone/tag Build is passing
2024-08-22 16:02:01 +08:00
coward
6c6b40593e 🎨优化 2024-08-22 16:01:00 +08:00
coward
45d83da5c7 🎨优化一哈
All checks were successful
continuous-integration/drone/tag Build is passing
2024-08-22 15:37:21 +08:00
coward
4fa123baa8 🎨优化一下子
All checks were successful
continuous-integration/drone/tag Build is passing
2024-08-22 14:16:33 +08:00
46 changed files with 5576 additions and 37 deletions

56
.gitignore vendored
View File

@@ -233,33 +233,33 @@ fabric.properties
# vendor/
# Go workspace file
go.work
./go.work
.idea
web/.idea
./.idea
./web/.idea
web/node_modules
web/.DS_Store
web/dist
web/dist-ssr
web/*.local
web/.eslintcache
web/report.html
web/vite.config.*.timestamp*
./web/node_modules
./web/.DS_Store
./web/dist
./web/dist-ssr
./web/*.local
./web/.eslintcache
./web/report.html
./web/vite.config.*.timestamp*
web/yarn.lock
web/npm-debug.log*
web/.pnpm-error.log*
web/.pnpm-debug.log
web/tests/**/coverage/
web/.vscode/
./web/yarn.lock
./web/npm-debug.log*
./web/.pnpm-error.log*
./web/.pnpm-debug.log
./web/tests/**/coverage/
./web/.vscode/
# Editor directories and files
web/*.suo
web/*.ntvs*
web/*.njsproj
web/*.sln
web/tsconfig.tsbuildinfo
./web/*.suo
./web/*.ntvs*
./web/*.njsproj
./web/*.sln
./web/tsconfig.tsbuildinfo
dist/assets
dist/resource
@@ -267,10 +267,10 @@ dist/favicon.png
dist/favicon.svg
dist/index.html
template/tmp/*
logs/*
app.yaml
*.db
.env
*.env
./template/tmp/*
./logs/*
./app.yaml
./*.db
./.env
./*.env

91
README.md Normal file
View File

@@ -0,0 +1,91 @@
# Wireguard-UI
> wireguard的管理面板UI
## 安装(仅提供docker方式)
OS X & Linux & Windows:
```sh
# 先要创建一个配置文件 app.yaml
http:
port: 6687
endpoint: localhost:3100,localhost:6687
database:
driver: sqlite # sqlite时只填写db即可目前仅支持sqlite | mysql | pgsql
host:
port:
user:
password:
db: wg
cache:
type: redis # 缓存类型 暂时仅支持redis
host: 192.168.1.1
port: 6379
password: pGhQKwj7DE7FbFL1
db: 15
file:
type: oss # 文件类型支持本地文件存储与阿里云oss存储
path: test/ # oss填写前缀目录
endpoint: # oss必填
accessId: # oss必填
accessSecret: # oss必填
bucketName: # oss必填
# 邮件设置
mail:
host:
port:
user:
password:
skipTls:
# 一些系统配置
wireguard:
restartMode: DELAY
delayTime: 20
# 其中依赖了redis等自行安装一个即可
```
```sh
# 创建docker-compose.yaml
version: "3"
services:
wg:
image: gitea.mrx.ltd/go-pkg/wireguard-srv:2.1.0
container_name: wg-srv
restart: always
cap_add:
- NET_ADMIN
network_mode: host
logging:
driver: json-file
options:
max-size: 50m
volumes:
- ./app.yaml:/app/app.yaml
- ./db:/app/db
- ./logs:/app/logs
- /etc/wireguard:/etc/wireguard
- /etc/localtime:/etc/localtime
```
```sh
默认账户密码
账户: admin
密码: admin123
```
## 示例
![img.png](img.png)
![img_1.png](img_1.png)
![img_7.png](img_7.png)
![img_8.png](img_8.png)
![img_2.png](img_2.png)
![img_3.png](img_3.png)
![img_4.png](img_4.png)
![img_5.png](img_5.png)
![img_6.png](img_6.png)

View File

@@ -36,6 +36,18 @@ func Error(err error) string {
return errMsg
}
// Validate
// @description: 校验
// @param data
// @return string
func Validate(data any) error {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
return v.Struct(data)
}
return nil
}
// initValidatorTranslator
// @description: 初始化翻译机
// @receiver vli

View File

@@ -97,8 +97,8 @@ func (w WireguardComponent) GenerateClientFile(clientInfo *model.Client, server
var outPath = "/tmp/" + fmt.Sprintf("%s.conf", clientInfo.Name)
var templatePath = "./template/wg.client.conf"
if os.Getenv("GIN_MODE") != "release" {
outPath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\" + fmt.Sprintf("%s.conf", clientInfo.Name)
templatePath = "E:\\Workspace\\Go\\wireguard-dashboard\\template\\wg.client.conf"
outPath = "E:\\Workspace\\Go\\wireguard-ui\\template\\tmp\\" + fmt.Sprintf("%s.conf", clientInfo.Name)
templatePath = "E:\\Workspace\\Go\\wireguard-ui\\template\\conf\\wg.client.conf"
}
err = Template().Execute(templatePath, outPath, execData)

1
cron/cron.go Normal file
View File

@@ -0,0 +1 @@
package cron

View File

@@ -3,6 +3,7 @@ package api
import (
"fmt"
"github.com/gin-gonic/gin"
"strings"
"time"
"wireguard-ui/component"
"wireguard-ui/http/param"
@@ -77,6 +78,10 @@ func (DashboardApi) ConnectionList(c *gin.Context) {
for _, iaip := range peer.AllowedIPs {
ipAllocation += iaip.String() + ","
}
// 去除一下最右边的逗号
if len(ipAllocation) > 0 {
ipAllocation = strings.TrimRight(ipAllocation, ",")
}
connections = append(connections, vo.DataTraffic{
Name: clientInfo.Name,
Email: clientInfo.Email,

33
http/api/remote.go Normal file
View File

@@ -0,0 +1,33 @@
package api
import "github.com/gin-gonic/gin"
type remote struct{}
func Remote() remote {
return remote{}
}
// SaveAuthClient
// @description: 添加授权客户端
// @receiver remote
// @param c
func (remote) SaveAuthClient(c *gin.Context) {
return
}
// DeleteAuthClient
// @description: 删除授权客户端
// @receiver remote
// @param c
func (remote) DeleteAuthClient(c *gin.Context) {
return
}
// GetClientNodes
// @description: 获取客户端节点
// @receiver remote
// @param c
func (remote) GetClientNodes(c *gin.Context) {
return
}

View File

@@ -1,11 +1,17 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"gitee.ltd/lxh/logger/log"
"github.com/gin-gonic/gin"
"os"
"slices"
"wireguard-ui/component"
"wireguard-ui/http/param"
"wireguard-ui/http/response"
"wireguard-ui/http/vo"
"wireguard-ui/model"
"wireguard-ui/script"
"wireguard-ui/service"
@@ -113,3 +119,99 @@ func (setting) GetAllSetting(c *gin.Context) {
func (setting) GetPublicAddr(c *gin.Context) {
response.R(c).OkWithData(utils.Network().GetHostPublicIP())
}
// Export
// @description: 导出配置
// @receiver setting
// @param c
func (setting) Export(c *gin.Context) {
// 获取当前登陆用户
var loginUser *vo.User
if loginUser = GetCurrentLoginUser(c); c.IsAborted() {
return
}
if loginUser.Account != "admin" {
response.R(c).FailedWithError("非法操作,你被捕啦!")
return
}
// 获取配置
data, err := service.Setting().Export()
if err != nil {
response.R(c).FailedWithError(err)
return
}
// 生成配置文件
dataBytes, _ := json.Marshal(data)
filepath, err := utils.FileUtils().GenerateFile("config.json", dataBytes)
if err != nil {
response.R(c).FailedWithError(err)
return
}
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename="+filepath)
c.Header("Content-Transfer-Encoding", "binary")
c.Header("Connection", "keep-alive")
c.File(filepath)
if err = os.Remove(filepath); err != nil {
log.Errorf("删除临时文件失败: %s", err.Error())
}
}
// Import
// @description: 导入配置
// @receiver setting
// @param c
func (setting) Import(c *gin.Context) {
var p param.Import
if err := c.ShouldBind(&p); err != nil {
response.R(c).Validator(err)
return
}
// 校验文件是否合规
if p.File.Filename != "config.json" {
response.R(c).Validator(errors.New("文件名不合规"))
return
}
// 校验文件内容是否符合
fileBytes, err := p.File.Open()
if err != nil {
response.R(c).FailedWithError(err)
return
}
var data vo.Export
if err := json.NewDecoder(fileBytes).Decode(&data); err != nil {
response.R(c).FailedWithError(err)
return
}
// 校验json串是否合规
if err = component.Validate(&data); err != nil {
response.R(c).Validator(err)
return
}
// 获取当前登陆用户
var loginUser *vo.User
if loginUser = GetCurrentLoginUser(c); c.IsAborted() {
return
}
if loginUser.Account != "admin" {
response.R(c).FailedWithError("非法操作,你被捕啦!")
return
}
if err = service.Setting().Import(&data, loginUser); err != nil {
response.R(c).FailedWithError(fmt.Errorf("导入失败: %v", err.Error()))
return
}
response.R(c).OK()
}

View File

@@ -50,13 +50,13 @@ func Authorization() gin.HandlerFunc {
// 查询用户
user, err := service.User().GetUserById(userClaims.ID)
if err != nil {
response.R(c).FailedWithError("用户不存在")
response.R(c).AuthorizationFailed("用户不存在")
c.Abort()
return
}
if user.Status != constant.Enabled {
response.R(c).FailedWithError("用户状态异常,请联系管理员处理!")
response.R(c).AuthorizationFailed("用户状态异常,请联系管理员处理!")
c.Abort()
return
}

View File

@@ -1,5 +1,7 @@
package param
import "mime/multipart"
// SetSetting
// @description: 添加/编辑设置
type SetSetting struct {
@@ -7,3 +9,9 @@ type SetSetting struct {
Data string `json:"data" form:"data" binding:"required"`
Describe string `json:"describe" form:"describe" binding:"omitempty"`
}
// Import
// @description: 导入
type Import struct {
File *multipart.FileHeader `json:"file" form:"file" binding:"required"`
}

View File

@@ -17,5 +17,7 @@ func SettingApi(r *gin.RouterGroup) {
setting.GET("", api.Setting().GetSetting) // 获取指定配置
setting.GET("/all", api.Setting().GetAllSetting) // 获取全部配置
setting.GET("/public-addr", api.Setting().GetPublicAddr) // 获取公网IP
setting.GET("/export", api.Setting().Export) // 导出配置文件
setting.POST("/import", api.Setting().Import) // 导入配置
}
}

View File

@@ -1,5 +1,7 @@
package vo
import "wireguard-ui/global/constant"
// SettingItem
// @description: 设置单项
type SettingItem struct {
@@ -7,3 +9,46 @@ type SettingItem struct {
Data string `json:"data"`
Describe string `json:"describe"`
}
type Export struct {
Global *Global `json:"global" label:"全局配置" binding:"required"`
Server *Server `json:"server" label:"服务端配置" binding:"required"`
Clients []Client `json:"clients" label:"客户端" binding:"omitempty"`
}
type Global struct {
MTU int `json:"MTU" label:"MTU" binding:"required"`
ConfigFilePath string `json:"configFilePath" label:"配置文件路径" binding:"required"`
DnsServer []string `json:"dnsServer" label:"DNS" binding:"omitempty"`
EndpointAddress string `json:"endpointAddress" label:"公网地址" binding:"required"`
FirewallMark string `json:"firewallMark" label:"firewallMark" binding:"omitempty"`
PersistentKeepalive int `json:"persistentKeepalive" label:"persistentKeepalive" binding:"required"`
Table string `json:"table" label:"table" binding:"omitempty"`
}
type Server struct {
IpScope []string `json:"ipScope" label:"ipScope" binding:"min=1,dive,required"`
ListenPort int `json:"listenPort" label:"listenPort" binding:"required"`
PrivateKey string `json:"privateKey" label:"privateKey" binding:"required"`
PublicKey string `json:"publicKey" label:"publicKey" binding:"required"`
PostUpScript string `json:"postUpScript" label:"postUpScript" binding:"omitempty"`
PostDownScript string `json:"postDownScript" label:"postDownScript" binding:"omitempty"`
}
type Client struct {
Name string `json:"name" label:"name" binding:"required"`
Email string `json:"email" label:"email" binding:"omitempty"`
SubnetRange string `json:"subnetRange" label:"subnetRange" binding:"omitempty"`
IpAllocation []string `json:"ipAllocation" label:"ipAllocation" binding:"min=1,dive,required"`
AllowedIps []string `json:"allowedIps" label:"allowedIps" binding:"min=1,dive,required"`
ExtraAllowedIps []string `json:"extraAllowedIps" label:"extraAllowedIps" binding:"omitempty"`
Endpoint string `json:"endpoint" label:"endpoint" binding:"endpoint"`
UseServerDns *constant.Status `json:"useServerDns" label:"useServerDns" binding:"omitempty"`
Keys struct {
PresharedKey string `json:"presharedKey" label:"presharedKey" binding:"required"`
PrivateKey string `json:"privateKey" label:"privateKey" binding:"required"`
PublicKey string `json:"publicKey" label:"publicKey" binding:"required"`
} `json:"keys" label:"keys" binding:"required"`
Enabled *constant.Status `json:"enabled" label:"enabled" binding:"required"`
OfflineMonitoring *constant.Status `json:"offlineMonitoring" label:"offlineMonitoring" binding:"required"`
}

BIN
img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

BIN
img_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
img_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
img_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
img_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
img_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
img_6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
img_7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
img_8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -22,3 +22,16 @@ type Client struct {
func (Client) TableName() string {
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"
}

View File

@@ -2,11 +2,16 @@ package service
import (
"encoding/json"
"fmt"
slog "gitee.ltd/lxh/logger/log"
"gorm.io/gorm"
"strings"
gdb "wireguard-ui/global/client"
"wireguard-ui/http/param"
"wireguard-ui/http/vo"
"wireguard-ui/model"
"wireguard-ui/template/render_data"
"wireguard-ui/utils"
)
type setting struct{ *gorm.DB }
@@ -84,3 +89,124 @@ func (s setting) GetAllSetting(blackList []string) (data []vo.SettingItem, err e
err = s.Model(&model.Setting{}).Select("code, data, describe").Where("code not in ?", blackList).Find(&data).Error
return
}
// Export
// @description: 导出
// @receiver s
// @return data
// @return err
func (s setting) Export() (data vo.Export, err error) {
// 先查询global配置
var gs, ss *model.Setting
if err = s.Model(&model.Setting{}).Where("code = ?", "WG_SETTING").Take(&gs).Error; err != nil {
return
}
if err = json.Unmarshal([]byte(gs.Data), &data.Global); err != nil {
return
}
// 查询server配置
if err = s.Model(&model.Setting{}).Where("code = ?", "WG_SERVER").Take(&ss).Error; err != nil {
return
}
if err = json.Unmarshal([]byte(ss.Data), &data.Server); err != nil {
return
}
// 查询client配置
var clients []vo.ClientItem
if err = s.Model(&model.Client{}).Select("id,name,email,ip_allocation as ip_allocation_str," +
"allowed_ips as allowed_ips_str,extra_allowed_ips as extra_allowed_ips_str," +
"endpoint,use_server_dns,keys as keys_str," +
"enabled,offline_monitoring").Order("created_at DESC").Find(&clients).Error; err != nil {
return
}
for i, v := range clients {
if v.KeysStr != "" {
_ = json.Unmarshal([]byte(v.KeysStr), &clients[i].Keys)
}
if v.IpAllocationStr != "" {
clients[i].IpAllocation = strings.Split(v.IpAllocationStr, ",")
}
if v.AllowedIpsStr != "" {
clients[i].AllowedIps = strings.Split(v.AllowedIpsStr, ",")
} else {
clients[i].AllowedIps = []string{}
}
if v.ExtraAllowedIpsStr != "" {
clients[i].ExtraAllowedIps = strings.Split(v.ExtraAllowedIpsStr, ",")
} else {
clients[i].ExtraAllowedIps = []string{}
}
}
cj, _ := json.Marshal(clients)
_ = json.Unmarshal(cj, &data.Clients)
return
}
// Import
// @description: 导入
// @receiver s
// @param data
// @return err
func (s setting) Import(data *vo.Export, loginUser *vo.User) (err error) {
// 获取导入系统的公网IP地址
pubAddr := utils.Network().GetHostPublicIP()
data.Global.EndpointAddress = pubAddr
// 先更新global配置
gst, _ := json.Marshal(data.Global)
gs := &model.Setting{
Code: "WG_SETTING",
Data: string(gst),
Describe: "服务端全局配置",
}
if err = s.SetData(gs); err != nil {
return
}
st, _ := json.Marshal(data.Server)
ss := &model.Setting{
Code: "WG_SERVER",
Data: string(st),
Describe: "服务端配置",
}
if err = s.SetData(ss); err != nil {
return
}
// 更新client配置
for _, v := range data.Clients {
keys := &param.Keys{
PrivateKey: v.Keys.PrivateKey,
PublicKey: v.Keys.PublicKey,
PresharedKey: v.Keys.PresharedKey,
}
cc := param.SaveClient{
Name: v.Name,
Email: v.Email,
IpAllocation: v.IpAllocation,
AllowedIps: v.AllowedIps,
ExtraAllowedIps: v.ExtraAllowedIps,
UseServerDns: v.UseServerDns,
Keys: keys,
Enabled: v.Enabled,
OfflineMonitoring: v.OfflineMonitoring,
}
if v.Endpoint != "" {
port := strings.Split(v.Endpoint, ":")[1]
endpoint := fmt.Sprintf("%s:%s", pubAddr, port)
cc.Endpoint = endpoint
}
if err := Client().SaveClient(cc, loginUser); err != nil {
slog.Errorf("客户端[%s]导入失败: %v", v.Name, err)
continue
}
}
return nil
}

View File

@@ -1,6 +1,7 @@
package service
import (
"errors"
"gorm.io/gorm"
gdb "wireguard-ui/global/client"
"wireguard-ui/global/constant"
@@ -39,6 +40,11 @@ func (s user) CreateUser(user *model.User) (err error) {
return s.Model(&model.User{}).Where("id = ?", user.Id).Updates(&updates).Error
}
// 判断账号是否已经存在
if _, err = s.GetUserByAccount(user.Account); err == nil {
return errors.New("账号已经存在,请勿重复创建!")
}
defaultPassword := utils.Password().GenerateHashPassword("admin123")
if user.Password == "" { // 没有密码给一个默认密码
user.Password = defaultPassword

28
utils/file.go Normal file
View File

@@ -0,0 +1,28 @@
package utils
import (
"fmt"
"os"
)
type fileUtils struct{}
func FileUtils() fileUtils {
return fileUtils{}
}
// GenerateFile
// @description: 生成文件
// @receiver fileUtils
// @param filename 文件名称
// @param content 文件内容
// @return error
func (fileUtils) GenerateFile(filename string, content []byte) (filepath string, err error) {
path := "/tmp/"
if os.Getenv("GIN_MODE") != "release" {
path = "E:\\Workspace\\Go\\wireguard-ui\\template\\tmp\\"
}
filepath = fmt.Sprintf("%s%s", path, filename)
err = os.WriteFile(filepath, content, 0777)
return filepath, err
}

3
web/.env Normal file
View File

@@ -0,0 +1,3 @@
VITE_TITLE = 'Wireguard-UI'
VITE_PORT = 3100

View File

@@ -32,6 +32,7 @@
},
"dependencies": {
"@unocss/eslint-config": "^0.55.7",
"@vicons/ionicons5": "^0.12.0",
"@vueuse/core": "^10.4.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "5.1.12",
@@ -40,6 +41,7 @@
"echarts": "^5.4.3",
"lodash-es": "^4.17.21",
"md-editor-v3": "^4.7.0",
"mitt": "^3.0.1",
"mockjs": "^1.1.0",
"pinia": "^2.1.6",
"vite": "^4.4.11",

16
web/pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@unocss/eslint-config':
specifier: ^0.55.7
version: 0.55.7(eslint@8.50.0)(typescript@5.2.2)
'@vicons/ionicons5':
specifier: ^0.12.0
version: 0.12.0
'@vueuse/core':
specifier: ^10.4.1
version: 10.4.1(vue@3.3.4)
@@ -35,6 +38,9 @@ importers:
md-editor-v3:
specifier: ^4.7.0
version: 4.7.0(@codemirror/state@6.2.1)(@codemirror/view@6.21.3)(@lezer/common@1.1.0)(vue@3.3.4)
mitt:
specifier: ^3.0.1
version: 3.0.1
mockjs:
specifier: ^1.1.0
version: 1.1.0
@@ -1046,6 +1052,9 @@ packages:
'@vavt/util@1.4.0':
resolution: {integrity: sha512-qLhaokwifMTFqoo4UE2JZUyaxCzX9T4WcIt2KzznbtBrCM4CG119pY/cKqq6jDa3c1phUvPCoIfjWfnF9nj4NA==}
'@vicons/ionicons5@0.12.0':
resolution: {integrity: sha512-Iy1EUVRpX0WWxeu1VIReR1zsZLMc4fqpt223czR+Rpnrwu7pt46nbnC2ycO7ItI/uqDLJxnbcMC7FujKs9IfFA==}
'@vite-plugin-vue-devtools/core@1.0.0-rc.7':
resolution: {integrity: sha512-Tv9JeRZQ6KDwSkOQJvXc5TBcc4fkSazA96GDhi99v4VCthTgXjnhaah41CeZD3hFDKnNS0MHKFFqN+RHAgYDyQ==}
peerDependencies:
@@ -3008,6 +3017,9 @@ packages:
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
mixin-deep@1.3.2:
resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==}
engines: {node: '>=0.10.0'}
@@ -5490,6 +5502,8 @@ snapshots:
'@vavt/util@1.4.0': {}
'@vicons/ionicons5@0.12.0': {}
'@vite-plugin-vue-devtools/core@1.0.0-rc.7(vite@4.4.11(@types/node@20.5.1)(sass@1.69.0)(terser@5.21.0))':
dependencies:
'@babel/parser': 7.23.0
@@ -7591,6 +7605,8 @@ snapshots:
minimist@1.2.8: {}
mitt@3.0.1: {}
mixin-deep@1.3.2:
dependencies:
for-in: 1.0.2

10
web/src/api/setting.js Normal file
View File

@@ -0,0 +1,10 @@
import { request } from '@/utils'
export default {
exportConfig: () => request.get('/setting/export'), // 导出配置
importConfig: (data) => request.post('/setting/import',data,{
headers: {
'Content-Type': 'multipart/form-data'
}
}),
}

View File

@@ -0,0 +1,126 @@
<template>
<n-icon mr-20 size="18" style="cursor: pointer" @click="importConfig()">
<icon-gg-import />
</n-icon>
<n-icon mr-20 size="18" style="cursor: pointer" @click="exportConfig()">
<icon-ph-export-bold />
</n-icon>
<n-modal
v-model:show="showImportUploader"
transform-origin="center"
preset="card"
title="导入配置"
:bordered="false"
size="large"
style="width: 400px"
header-style="text-align: center"
>
<n-upload
ref="uploadRef"
:multiple="false"
directory-dnd
:max="1"
:custom-request="customRequest"
name="file"
accept=".json"
:default-upload="false"
@before-upload="uploadCheck"
>
<n-upload-dragger>
<div style="margin-bottom: 12px">
<n-icon size="48" :depth="3">
<ArchiveIcon />
</n-icon>
</div>
<n-text style="font-size: 16px">
点击或者拖动文件到该区域来上传
</n-text>
<n-p depth="3" style="margin: 8px 0 0 0">
注意上传后将覆盖原有的配置以及客户端数据等请谨慎操作
</n-p>
</n-upload-dragger>
</n-upload>
<n-button type="primary" style="margin-left: 40%" @click="submitUpload">确定</n-button>
</n-modal>
</template>
<script setup>
import api from '@/api/setting'
import { ArchiveOutline as ArchiveIcon } from "@vicons/ionicons5";
import { router } from '@/router'
import event from '@/utils/event/event'
const { $bus } = event();
const showImportUploader = ref(false)
const uploadRef = ref(null)
// 导入
function importConfig() {
showImportUploader.value = true
}
// 导出
function exportConfig() {
$dialog.confirm({
type: 'warning',
title: '导出配置',
content: `是否需要导出系统全部配置?`,
async confirm() {
api.exportConfig().then(response => {
const blob = new Blob([JSON.stringify(response.data)], {
type: "text/plain"
});
const link = document.createElement("a"); // 创建a标签
link.download = "config.json"; // a标签添加属性
link.style.display = "none";
link.href = URL.createObjectURL(blob);
document.body.appendChild(link);
link.click(); // 执行下载
URL.revokeObjectURL(link.href); // 释放url
document.body.removeChild(link); // 释放标签
})
},
})
}
// 上传前检查
function uploadCheck(data) {
if (data.file.file?.name !== "config.json") {
$message.error("导入文件只能是[config.json]");
return false;
}
if (data.file.file?.type !== "application/json") {
$message.error("只能上传json类型文件请重新上传");
return false;
}
return true;
}
// 自定义上传
function customRequest(file) {
api.importConfig({
file: file.file.file,
}).then(response => {
if (response.data.code === 200) {
showImportUploader.value = false;
switch (router.options.history.location) {
case "/client":
$bus.emit('refreshClients',true);
break;
case "/setting":
$bus.emit('refreshSetting',true)
break;
}
} else {
$message.error(response.data.message);
}
})
}
// 点击按钮上传
function submitUpload() {
uploadRef.value?.submit();
}
</script>

View File

@@ -72,8 +72,11 @@
import { useUserStore } from '@/store'
import { renderIcon } from '@/utils'
import api from '@/api/user'
import event from '@/utils/event/event'
import { router } from '@/router'
const userStore = useUserStore()
const { $bus } = event();
const options = [
{
@@ -212,6 +215,9 @@ async function updateInfo() {
infoFormModel.value = ref(null)
showInfoModel.value = false
await useUserStore().getUserInfo()
if (router.options.history.location === '/user') {
$bus.emit('refreshUserInfo',true);
}
}
})
}

View File

@@ -3,7 +3,12 @@
<MenuCollapse />
<BreadCrumb ml-15 hidden sm:block />
</div>
<div ml-auto flex items-center>
<div ml-auto flex items-center v-if="loginUser.account === 'admin'">
<Export/>
<FullScreen />
<UserAvatar />
</div>
<div ml-auto flex items-center v-else>
<FullScreen />
<UserAvatar />
</div>
@@ -14,4 +19,7 @@ import BreadCrumb from './components/BreadCrumb.vue'
import MenuCollapse from './components/MenuCollapse.vue'
import FullScreen from './components/FullScreen.vue'
import UserAvatar from './components/UserAvatar.vue'
import Export from './components/Export.vue'
import { useUserStore } from '@/store'
const loginUser = useUserStore()
</script>

View File

@@ -9,10 +9,14 @@ import { setupRouter } from '@/router'
import { setupStore } from '@/store'
import App from './App.vue'
import { setupNaiveDiscreteApi } from './utils'
import mitt from 'mitt'
const EventMitt = mitt();
async function setupApp() {
const app = createApp(App)
setupStore(app)
app.config.globalProperties.$bus = EventMitt;
await setupRouter(app)
app.mount('#app')
setupNaiveDiscreteApi()

View File

@@ -37,7 +37,6 @@ export async function addDynamicRoutes() {
// 有token的情况
const userStore = useUserStore()
try {
const permissionStore = usePermissionStore()
!userStore.id && (await userStore.getUserInfo())
@@ -49,8 +48,8 @@ export async function addDynamicRoutes() {
router.addRoute(NOT_FOUND_ROUTE)
} catch (error) {
console.error(error)
$message.error('初始化用户信息失败: ' + error)
userStore.logout()
$message.error('初始化用户信息失败: ' + error)
}
}

View File

@@ -0,0 +1,7 @@
import { getCurrentInstance } from "vue";
export default function event() {
const instance = getCurrentInstance();
const globalProperties = instance?.appContext.config.globalProperties;
return { ...globalProperties };
}

View File

@@ -0,0 +1 @@
export * from './event'

View File

@@ -2,3 +2,4 @@ export * from './common'
export * from './storage'
export * from './http'
export * from './auth'
export * from './event'

View File

@@ -335,6 +335,9 @@ import { NButton } from 'naive-ui'
import { debounce, ellipsis } from '@/utils'
import clientApi from '@/views/client/api'
import QueryBar from '@/components/query-bar/QueryBar.vue'
import event from '@/utils/event/event'
const { $bus } = event();
const selOptions = [
{
@@ -658,6 +661,13 @@ function search() {
getClientList()
}
// 监听事件
$bus.on("refreshClients",value => {
if (value) {
getClientList();
}
})
getClientList()
</script>
<style lang="scss">

View File

@@ -6,6 +6,7 @@ export default {
component: Layout,
redirect: '/client',
meta: {
title: '客户端',
order: 2,
},
children: [

View File

@@ -209,6 +209,10 @@ import AppPage from '@/components/page/AppPage.vue'
import api from '@/views/setting/api'
import { NButton } from 'naive-ui'
import { renderIcon } from '@/utils'
import event from '@/utils/event/event'
const { $bus } = event();
const tabCode = ref("")
// 表头
const tableColumns = [
@@ -401,12 +405,15 @@ function tabChange(code) {
switch (code) {
case "Server":
getServerConfig()
tabCode.value = "Server"
break;
case "Global":
getGlobalConfig()
tabCode.value = "Global"
break;
case "Other":
allSetting()
tabCode.value = "Other"
break;
}
}
@@ -480,6 +487,22 @@ async function addSetting() {
}
}
$bus.on("refreshSetting",value => {
if (value) {
if (tabCode.value === "" || tabCode.value === undefined) {
getServerConfig()
} else {
switch (tabCode.value) {
case "Server":
getServerConfig()
break;
case "Global":
getGlobalConfig()
break;
}
}
}
})
getServerConfig()
</script>
<style lang="scss">

View File

@@ -6,6 +6,7 @@ export default {
component: Layout,
redirect: '/setting',
meta: {
title: '设置',
order: 3,
},
children: [

View File

@@ -106,6 +106,9 @@ import userApi from '@/api/user'
import { NAvatar,NTag,NButton } from 'naive-ui'
import { renderIcon } from '@/utils'
import { useUserStore } from '@/store'
import event from '@/utils/event/event'
const { $bus } = event();
const infoFormRef = ref()
@@ -400,6 +403,12 @@ function addUser() {
showInfoModel.value = true
}
$bus.on('refreshUserInfo',value => {
if (value) {
getUserList();
}
})
getUserList()
</script>
<style></style>

View File

@@ -6,6 +6,7 @@ export default {
component: Layout,
redirect: '/user',
meta: {
title: '管理员',
order: 1,
},
children: [

View File

@@ -10,7 +10,7 @@
</div>
</div>
<p class="mt-40 text-14 opacity-60" style="cursor: pointer" @click="dailyPoe">{{ dailyPoetry.content || '莫向外求但从心觅行有不得反求诸己' }}</p>
<p class="mt-40 text-14 opacity-60">{{ dailyPoetry.content || '莫向外求,但从心觅,行有不得,反求诸己。' }}</p>
<p class="mt-32 text-right text-12 opacity-40"> {{ dailyPoetry.author || '佚名' }}</p>
</n-card>
<n-card class="ml-12 w-70%">
@@ -233,7 +233,7 @@ async function getClientConnections() {
const initFunc = debounce(() => {
getClientConnections()
dailyPoe()
// dailyPoe()
// connectionList()
},500)

View File

@@ -6,6 +6,7 @@ export default {
component: Layout,
redirect: '/workbench',
meta: {
title: '工作台',
order: 0,
},
children: [

4838
web/stats.html Normal file

File diff suppressed because one or more lines are too long