mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-09-05 06:04:35 +08:00
feat: 处理冲突
This commit is contained in:
commit
9a0c83aa7e
32 changed files with 457 additions and 79 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -15,3 +15,4 @@
|
|||
# vendor/
|
||||
|
||||
.idea
|
||||
backend/__debug_bin
|
||||
|
|
|
@ -9,5 +9,6 @@ type ApiGroup struct {
|
|||
var ApiGroupApp = new(ApiGroup)
|
||||
|
||||
var (
|
||||
userService = service.ServiceGroupApp.UserService
|
||||
userService = service.ServiceGroupApp.UserService
|
||||
operationService = service.ServiceGroupApp.OperationService
|
||||
)
|
||||
|
|
|
@ -12,23 +12,23 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func GeneratePaginationFromReq(c *gin.Context) (dto.PageInfo, bool) {
|
||||
func GeneratePaginationFromReq(c *gin.Context) (*dto.PageInfo, bool) {
|
||||
p, ok1 := c.GetQuery("page")
|
||||
ps, ok2 := c.GetQuery("pageSize")
|
||||
if !(ok1 && ok2) {
|
||||
return dto.PageInfo{Page: 1, PageSize: 10}, false
|
||||
return nil, false
|
||||
}
|
||||
|
||||
page, err := strconv.Atoi(p)
|
||||
if err != nil {
|
||||
return dto.PageInfo{Page: 1, PageSize: 10}, false
|
||||
return nil, false
|
||||
}
|
||||
pageSize, err := strconv.Atoi(ps)
|
||||
if err != nil {
|
||||
return dto.PageInfo{Page: 1, PageSize: 10}, false
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return dto.PageInfo{Page: page, PageSize: pageSize}, false
|
||||
return &dto.PageInfo{Page: page, PageSize: pageSize}, true
|
||||
}
|
||||
|
||||
func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
||||
|
|
27
backend/app/api/v1/operation_log.go
Normal file
27
backend/app/api/v1/operation_log.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/app/api/v1/helper"
|
||||
"github.com/1Panel-dev/1Panel/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/constant"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (b *BaseApi) GetOperationList(c *gin.Context) {
|
||||
pagenation, isOK := helper.GeneratePaginationFromReq(c)
|
||||
if !isOK {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, constant.ErrPageGenerate)
|
||||
return
|
||||
}
|
||||
|
||||
total, list, err := operationService.Page(pagenation.Page, pagenation.PageSize)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Items: list,
|
||||
Total: total,
|
||||
})
|
||||
}
|
|
@ -36,6 +36,14 @@ func (b *BaseApi) Login(c *gin.Context) {
|
|||
helper.SuccessWithData(c, user)
|
||||
}
|
||||
|
||||
func (b *BaseApi) LogOut(c *gin.Context) {
|
||||
if err := userService.LogOut(c); err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, nil)
|
||||
}
|
||||
|
||||
func (b *BaseApi) Captcha(c *gin.Context) {
|
||||
captcha, err := captcha.CreateCaptcha()
|
||||
if err != nil {
|
||||
|
|
25
backend/app/dto/operation_log.go
Normal file
25
backend/app/dto/operation_log.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type OperationLogBack struct {
|
||||
Group string `json:"group"`
|
||||
Source string `json:"source"`
|
||||
Action string `json:"action"`
|
||||
|
||||
IP string `json:"ip"`
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
Body string `json:"body"`
|
||||
Resp string `json:"resp"`
|
||||
|
||||
Status int `json:"status"`
|
||||
Latency time.Duration `json:"latency"`
|
||||
ErrorMessage string `json:"errorMessage"`
|
||||
|
||||
Detail string `json:"detail"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
|
@ -8,20 +8,18 @@ import (
|
|||
|
||||
type OperationLog struct {
|
||||
gorm.Model
|
||||
Group string `gorm:"type:varchar(64)" json:"type"`
|
||||
Group string `gorm:"type:varchar(64)" json:"group"`
|
||||
Source string `gorm:"type:varchar(64)" json:"source"`
|
||||
Action string `gorm:"type:varchar(64)" json:"action"`
|
||||
|
||||
IP string `gorm:"type:varchar(64)" json:"ip"`
|
||||
Path string `gorm:"type:varchar(64)" json:"path"`
|
||||
Method string `gorm:"type:varchar(64)" json:"method"`
|
||||
UserAgent string `gorm:"type:text(65535)" json:"userAgent"`
|
||||
Body string `gorm:"type:text(65535)" json:"body"`
|
||||
Resp string `gorm:"type:text(65535)" json:"resp"`
|
||||
UserAgent string `gorm:"type:varchar(256)" json:"userAgent"`
|
||||
Body string `gorm:"type:longText" json:"body"`
|
||||
Resp string `gorm:"type:longText" json:"resp"`
|
||||
|
||||
Status int `gorm:"type:varchar(64)" json:"status"`
|
||||
Latency time.Duration `gorm:"type:varchar(64)" json:"latency"`
|
||||
ErrorMessage string `gorm:"type:varchar(256)" json:"errorMessage"`
|
||||
Latency time.Duration `gorm:"type:varchar(64)" json:"latency"`
|
||||
|
||||
Detail string `gorm:"type:longText" json:"detail"`
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ type OperationRepo struct{}
|
|||
|
||||
type IOperationRepo interface {
|
||||
Create(user *model.OperationLog) error
|
||||
Page(limit, offset int, opts ...DBOption) (int64, []model.OperationLog, error)
|
||||
}
|
||||
|
||||
func NewIOperationService() IOperationRepo {
|
||||
|
@ -18,3 +19,15 @@ func NewIOperationService() IOperationRepo {
|
|||
func (u *OperationRepo) Create(user *model.OperationLog) error {
|
||||
return global.DB.Create(user).Error
|
||||
}
|
||||
|
||||
func (u *OperationRepo) Page(page, size int, opts ...DBOption) (int64, []model.OperationLog, error) {
|
||||
var ops []model.OperationLog
|
||||
db := global.DB.Model(&model.OperationLog{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
count := int64(0)
|
||||
db = db.Count(&count)
|
||||
err := db.Limit(size).Offset(size * (page - 1)).Find(&ops).Error
|
||||
return count, ops, err
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import "github.com/1Panel-dev/1Panel/app/repo"
|
|||
|
||||
type ServiceGroup struct {
|
||||
UserService
|
||||
OperationService
|
||||
}
|
||||
|
||||
var ServiceGroupApp = new(ServiceGroup)
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
package service
|
||||
|
||||
import "github.com/1Panel-dev/1Panel/app/model"
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/app/model"
|
||||
"github.com/1Panel-dev/1Panel/constant"
|
||||
"github.com/1Panel-dev/1Panel/global"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type OperationService struct{}
|
||||
|
||||
type IOperationService interface {
|
||||
Page(page, size int) (int64, interface{}, error)
|
||||
Create(operation model.OperationLog) error
|
||||
}
|
||||
|
||||
|
@ -15,3 +25,48 @@ func NewIOperationService() IOperationService {
|
|||
func (u *OperationService) Create(operation model.OperationLog) error {
|
||||
return operationRepo.Create(&operation)
|
||||
}
|
||||
|
||||
func (u *OperationService) Page(page, size int) (int64, interface{}, error) {
|
||||
total, ops, err := operationRepo.Page(page, size, commonRepo.WithOrderBy("created_at desc"))
|
||||
var dtoOps []dto.OperationLogBack
|
||||
for _, op := range ops {
|
||||
var item dto.OperationLogBack
|
||||
if err := copier.Copy(&item, &op); err != nil {
|
||||
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||
}
|
||||
item.Body = filterSensitive(item.Body)
|
||||
var res dto.Response
|
||||
if err := json.Unmarshal([]byte(item.Resp), &res); err != nil {
|
||||
global.LOG.Errorf("unmarshal failed, err: %+v", err)
|
||||
dtoOps = append(dtoOps, item)
|
||||
continue
|
||||
}
|
||||
item.Status = res.Code
|
||||
if item.Status != 200 {
|
||||
item.ErrorMessage = res.Msg
|
||||
}
|
||||
dtoOps = append(dtoOps, item)
|
||||
}
|
||||
return total, dtoOps, err
|
||||
}
|
||||
|
||||
func filterSensitive(vars string) string {
|
||||
var Sensitives = []string{"password", "Password"}
|
||||
ops := make(map[string]string)
|
||||
if err := json.Unmarshal([]byte(vars), &ops); err != nil {
|
||||
return vars
|
||||
}
|
||||
for k := range ops {
|
||||
for _, sen := range Sensitives {
|
||||
if k == sen {
|
||||
delete(ops, k)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
backStr, err := json.Marshal(ops)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(backStr)
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ type IUserService interface {
|
|||
Page(search dto.UserPage) (int64, interface{}, error)
|
||||
Register(userDto dto.UserCreate) error
|
||||
Login(c *gin.Context, info dto.Login) (*dto.UserLoginInfo, error)
|
||||
LogOut(c *gin.Context) error
|
||||
Delete(name string) error
|
||||
Save(req model.User) error
|
||||
Update(upMap map[string]interface{}) error
|
||||
|
@ -33,7 +34,7 @@ func NewIUserService() IUserService {
|
|||
func (u *UserService) Get(name string) (*dto.UserBack, error) {
|
||||
user, err := userRepo.Get(commonRepo.WithByName(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, constant.ErrRecordNotFound
|
||||
}
|
||||
var dtoUser dto.UserBack
|
||||
if err := copier.Copy(&dtoUser, &user); err != nil {
|
||||
|
@ -56,14 +57,13 @@ func (u *UserService) Page(search dto.UserPage) (int64, interface{}, error) {
|
|||
}
|
||||
|
||||
func (u *UserService) Register(userDto dto.UserCreate) error {
|
||||
var user model.User
|
||||
user, _ := userRepo.Get(commonRepo.WithByName(userDto.Name))
|
||||
if user.ID != 0 {
|
||||
return constant.ErrRecordExist
|
||||
}
|
||||
if err := copier.Copy(&user, &userDto); err != nil {
|
||||
return errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||
}
|
||||
_ = global.DB.Where("name = ?", user.Name).First(&user).Error
|
||||
if user.ID != 0 {
|
||||
return errors.Wrap(constant.ErrRecordExist, "data exist")
|
||||
}
|
||||
return userRepo.Create(&user)
|
||||
}
|
||||
|
||||
|
@ -111,6 +111,23 @@ func (u *UserService) Login(c *gin.Context, info dto.Login) (*dto.UserLoginInfo,
|
|||
return &dto.UserLoginInfo{Name: user.Name}, err
|
||||
}
|
||||
|
||||
func (u *UserService) LogOut(c *gin.Context) error {
|
||||
sID, _ := c.Cookie(global.CONF.Session.SessionName)
|
||||
if sID != "" {
|
||||
c.SetCookie(global.CONF.Session.SessionName, "", -1, "", "", false, false)
|
||||
}
|
||||
sessionItem, err := global.SESSION.Get(c.Request, global.CONF.Session.SessionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sessionItem.Options.MaxAge = -1
|
||||
if err := global.SESSION.Save(c.Request, c.Writer, sessionItem); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UserService) Delete(name string) error {
|
||||
return userRepo.Delete(commonRepo.WithByName(name))
|
||||
}
|
||||
|
|
|
@ -33,12 +33,11 @@ func Routers() *gin.Engine {
|
|||
c.JSON(200, "ok")
|
||||
})
|
||||
}
|
||||
PrivateGroup := Router.Group("/api/v1")
|
||||
{
|
||||
systemRouter.InitBaseRouter(PublicGroup) // 注册基础功能路由 不做鉴权
|
||||
}
|
||||
PrivateGroup := Router.Group("")
|
||||
{
|
||||
systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由
|
||||
systemRouter.InitBaseRouter(PrivateGroup)
|
||||
systemRouter.InitUserRouter(PrivateGroup)
|
||||
systemRouter.InitOperationLogRouter(PrivateGroup)
|
||||
}
|
||||
|
||||
return Router
|
||||
|
|
|
@ -2,10 +2,8 @@ package middleware
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -18,26 +16,17 @@ import (
|
|||
func OperationRecord() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var body []byte
|
||||
if c.Request.Method != http.MethodGet {
|
||||
var err error
|
||||
body, err = ioutil.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("read body from request failed, err: %v", err)
|
||||
} else {
|
||||
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
if c.Request.Method == http.MethodGet || strings.Contains(c.Request.URL.Path, "search") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
body, err = ioutil.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("read body from request failed, err: %v", err)
|
||||
} else {
|
||||
query := c.Request.URL.RawQuery
|
||||
query, _ = url.QueryUnescape(query)
|
||||
split := strings.Split(query, "&")
|
||||
m := make(map[string]string)
|
||||
for _, v := range split {
|
||||
kv := strings.Split(v, "=")
|
||||
if len(kv) == 2 {
|
||||
m[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
body, _ = json.Marshal(&m)
|
||||
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
pathInfo := loadLogInfo(c.Request.URL.Path)
|
||||
|
||||
|
@ -62,8 +51,6 @@ func OperationRecord() gin.HandlerFunc {
|
|||
c.Next()
|
||||
|
||||
latency := time.Since(now)
|
||||
record.ErrorMessage = c.Errors.ByType(gin.ErrorTypePrivate).String()
|
||||
record.Status = c.Writer.Status()
|
||||
record.Latency = latency
|
||||
record.Resp = writer.body.String()
|
||||
|
||||
|
@ -90,6 +77,7 @@ type pathInfo struct {
|
|||
}
|
||||
|
||||
func loadLogInfo(path string) pathInfo {
|
||||
path = strings.ReplaceAll(path, "/api/v1", "")
|
||||
if !strings.Contains(path, "/") {
|
||||
return pathInfo{}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package router
|
|||
type RouterGroup struct {
|
||||
BaseRouter
|
||||
UserRouter
|
||||
OperationLogRouter
|
||||
}
|
||||
|
||||
var RouterGroupApp = new(RouterGroup)
|
||||
|
|
19
backend/router/operation_log.go
Normal file
19
backend/router/operation_log.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
v1 "github.com/1Panel-dev/1Panel/app/api/v1"
|
||||
"github.com/1Panel-dev/1Panel/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type OperationLogRouter struct{}
|
||||
|
||||
func (s *OperationLogRouter) InitOperationLogRouter(Router *gin.RouterGroup) {
|
||||
operationRouter := Router.Group("operations")
|
||||
operationRouter.Use(middleware.JwtAuth()).Use(middleware.SessionAuth())
|
||||
baseApi := v1.ApiGroupApp.BaseApi
|
||||
{
|
||||
operationRouter.GET("", baseApi.GetOperationList)
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ func (s *BaseRouter) InitBaseRouter(Router *gin.RouterGroup) (R gin.IRoutes) {
|
|||
baseApi := v1.ApiGroupApp.BaseApi
|
||||
{
|
||||
withRecordRouter.POST("login", baseApi.Login)
|
||||
withRecordRouter.POST("logout", baseApi.LogOut)
|
||||
baseRouter.GET("captcha", baseApi.Captcha)
|
||||
}
|
||||
return baseRouter
|
||||
|
|
|
@ -11,11 +11,8 @@ func Copy(to, from interface{}) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "marshal from data err")
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, to)
|
||||
if err != nil {
|
||||
if err = json.Unmarshal(b, to); err != nil {
|
||||
return errors.Wrap(err, "unmarshal to data err")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
NODE_ENV = 'development'
|
||||
|
||||
# 本地环境接口地址
|
||||
VITE_API_URL = '/api'
|
||||
VITE_API_URL = '/api/v1'
|
|
@ -6,7 +6,14 @@ import { ResultEnum } from '@/enums/http-enum';
|
|||
import { checkStatus } from './helper/check-status';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import router from '@/routers';
|
||||
import i18n from '@/lang';
|
||||
|
||||
/**
|
||||
* pinia 错误使用说明示例
|
||||
* https://github.com/vuejs/pinia/discussions/971
|
||||
* https://github.com/vuejs/pinia/discussions/664#discussioncomment-1329898
|
||||
* https://pinia.vuejs.org/core-concepts/outside-component-usage.html#single-page-applications
|
||||
*/
|
||||
// const globalStore = GlobalStore();
|
||||
|
||||
const axiosCanceler = new AxiosCanceler();
|
||||
|
||||
|
@ -34,10 +41,6 @@ class RequestHttp {
|
|||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @description 响应拦截器
|
||||
* 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
|
||||
*/
|
||||
this.service.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const { data, config } = response;
|
||||
|
@ -59,8 +62,7 @@ class RequestHttp {
|
|||
async (error: AxiosError) => {
|
||||
const { response } = error;
|
||||
tryHideFullScreenLoading();
|
||||
if (error.message.indexOf('timeout') !== -1)
|
||||
ElMessage.error(i18n.global.t('commons.msg.requestTimeout'));
|
||||
if (error.message.indexOf('timeout') !== -1) ElMessage.error('请求超时!请您稍后重试');
|
||||
if (response) checkStatus(response.status);
|
||||
if (!window.navigator.onLine) router.replace({ path: '/500' });
|
||||
return Promise.reject(error);
|
||||
|
@ -68,7 +70,6 @@ class RequestHttp {
|
|||
);
|
||||
}
|
||||
|
||||
// * 常用请求方法封装
|
||||
get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
|
||||
return this.service.get(url, { params, ..._object });
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ export interface Result {
|
|||
}
|
||||
|
||||
// * 请求响应参数(包含data)
|
||||
export interface ResultData<T = any> {
|
||||
export interface ResultData<T> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
|
|
20
frontend/src/api/interface/operation-log.ts
Normal file
20
frontend/src/api/interface/operation-log.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { DateTimeFormats } from '@intlify/core-base';
|
||||
|
||||
export interface ResOperationLog {
|
||||
group: string;
|
||||
source: string;
|
||||
action: string;
|
||||
ip: string;
|
||||
path: string;
|
||||
method: string;
|
||||
userAgent: string;
|
||||
body: string;
|
||||
resp: string;
|
||||
|
||||
status: number;
|
||||
latency: number;
|
||||
errorMessage: string;
|
||||
|
||||
detail: string;
|
||||
createdAt: DateTimeFormats;
|
||||
}
|
|
@ -8,3 +8,7 @@ export const loginApi = (params: Login.ReqLoginForm) => {
|
|||
export const getCaptcha = () => {
|
||||
return http.get<Login.ResCaptcha>(`/auth/captcha`);
|
||||
};
|
||||
|
||||
export const logOutApi = () => {
|
||||
return http.post<any>(`/auth/logout`);
|
||||
};
|
||||
|
|
7
frontend/src/api/modules/operation-log.ts
Normal file
7
frontend/src/api/modules/operation-log.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import http from '@/api';
|
||||
import { ResPage } from '../interface';
|
||||
import { ResOperationLog } from '../interface/operation-log';
|
||||
|
||||
export const getOperationList = (currentPage: number, pageSize: number) => {
|
||||
return http.get<ResPage<ResOperationLog>>(`/operations?page=${currentPage}&pageSize=${pageSize}`);
|
||||
};
|
|
@ -13,9 +13,7 @@
|
|||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<!-- infoDialog -->
|
||||
<InfoDialog ref="infoRef"></InfoDialog>
|
||||
<!-- passwordDialog -->
|
||||
<PasswordDialog ref="passwordRef"></PasswordDialog>
|
||||
</template>
|
||||
|
||||
|
@ -26,32 +24,38 @@ import PasswordDialog from './password-dialog.vue';
|
|||
import { ElMessageBox, ElMessage } from 'element-plus';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { GlobalStore } from '@/store';
|
||||
import { logOutApi } from '@/api/modules/login';
|
||||
import i18n from '@/lang';
|
||||
|
||||
const router = useRouter();
|
||||
const globalStore = GlobalStore();
|
||||
|
||||
// 退出登录
|
||||
const logout = () => {
|
||||
ElMessageBox.confirm('您是否确认退出登录?', '温馨提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
ElMessageBox.confirm(i18n.global.t('commons.msg.sureLogOut'), i18n.global.t('commons.msg.infoTitle'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
systemLogOut();
|
||||
router.push({ name: 'login' });
|
||||
globalStore.setUserInfo('');
|
||||
globalStore.setLogStatus(false);
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '退出登录成功!',
|
||||
message: i18n.global.t('commons.msg.operationSuccess'),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const systemLogOut = async () => {
|
||||
await logOutApi();
|
||||
};
|
||||
|
||||
interface DialogExpose {
|
||||
openDialog: () => void;
|
||||
}
|
||||
const infoRef = ref<null | DialogExpose>(null);
|
||||
const passwordRef = ref<null | DialogExpose>(null);
|
||||
// 打开修改密码和个人信息弹窗
|
||||
|
||||
const openDialog = (refName: string) => {
|
||||
if (refName == 'infoRef') return infoRef.value?.openDialog();
|
||||
passwordRef.value?.openDialog();
|
||||
|
|
|
@ -17,7 +17,7 @@ export const useDeleteData = <P = any, R = any>(
|
|||
confirmType: HandleData.MessageType = 'error',
|
||||
) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
ElMessageBox.confirm(i18n.global.t(`${message}`) + '?', i18n.global.t('commons.msg.title'), {
|
||||
ElMessageBox.confirm(i18n.global.t(`${message}`) + '?', i18n.global.t('commons.msg.deleteTitle'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: confirmType,
|
||||
|
|
|
@ -14,12 +14,13 @@ export default {
|
|||
createdAt: 'Creation Time',
|
||||
updatedAt: 'Update Time',
|
||||
operate: 'Operations',
|
||||
message: 'message',
|
||||
},
|
||||
msg: {
|
||||
delete: 'This operation cannot be rolled back. Do you want to continue',
|
||||
title: 'Delete',
|
||||
deleteSuccess: 'Delete Success',
|
||||
loginSuccss: 'Login Success',
|
||||
loginSuccess: 'Login Success',
|
||||
requestTimeout: 'The request timed out, please try again later',
|
||||
},
|
||||
login: {
|
||||
|
@ -41,6 +42,7 @@ export default {
|
|||
menu: {
|
||||
home: 'Dashboard',
|
||||
demo: 'Demo',
|
||||
operations: 'Operation logs',
|
||||
},
|
||||
home: {
|
||||
welcome: 'Welcome',
|
||||
|
@ -66,4 +68,19 @@ export default {
|
|||
changePassword: 'Change Password',
|
||||
logout: 'Logout',
|
||||
},
|
||||
operations: {
|
||||
detail: {
|
||||
users: 'User',
|
||||
auth: 'User',
|
||||
login: ' login',
|
||||
logout: ' logout',
|
||||
post: ' create',
|
||||
update: ' update',
|
||||
delete: ' delete',
|
||||
},
|
||||
operatoin: 'operatoin',
|
||||
status: 'status',
|
||||
request: 'request',
|
||||
response: 'response',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -12,15 +12,20 @@ export default {
|
|||
table: {
|
||||
name: '名称',
|
||||
createdAt: '创建时间',
|
||||
date: '时间',
|
||||
updatedAt: '更新时间',
|
||||
operate: '操作',
|
||||
message: '信息',
|
||||
},
|
||||
msg: {
|
||||
delete: '此操作不可回滚,是否继续',
|
||||
title: '删除',
|
||||
deleteTitle: '删除',
|
||||
deleteSuccess: '删除成功',
|
||||
loginSuccss: '登陆成功',
|
||||
loginSuccess: '登陆成功',
|
||||
operationSuccess: '操作成功',
|
||||
requestTimeout: '请求超时,请稍后重试',
|
||||
infoTitle: '提示',
|
||||
sureLogOut: '您是否确认退出登录?',
|
||||
},
|
||||
login: {
|
||||
captchaHelper: '请输入验证码',
|
||||
|
@ -41,6 +46,7 @@ export default {
|
|||
menu: {
|
||||
home: '概览',
|
||||
demo: '样例',
|
||||
operations: '操作记录',
|
||||
},
|
||||
|
||||
home: {
|
||||
|
@ -67,4 +73,19 @@ export default {
|
|||
changePassword: '修改密码',
|
||||
logout: '退出登录',
|
||||
},
|
||||
operations: {
|
||||
detail: {
|
||||
users: '用户',
|
||||
auth: '用户',
|
||||
post: '创建',
|
||||
update: '更新',
|
||||
delete: '删除',
|
||||
login: '登录',
|
||||
logout: '退出',
|
||||
},
|
||||
operatoin: '操作',
|
||||
status: '状态',
|
||||
request: '请求',
|
||||
response: '响应',
|
||||
},
|
||||
};
|
||||
|
|
26
frontend/src/routers/modules/operation-log.ts
Normal file
26
frontend/src/routers/modules/operation-log.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { Layout } from '@/routers/constant';
|
||||
|
||||
const operationRouter = {
|
||||
sort: 2,
|
||||
path: '/operations',
|
||||
component: Layout,
|
||||
redirect: '/operation',
|
||||
meta: {
|
||||
title: 'menu.operations',
|
||||
icon: 'notebook',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/operation',
|
||||
name: 'OperationLog',
|
||||
component: () => import('@/views/operation-log/index.vue'),
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
requiresAuth: true,
|
||||
key: 'OperationLog',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default operationRouter;
|
|
@ -65,7 +65,6 @@ export const routes: RouteRecordRaw[] = [
|
|||
},
|
||||
...routerArray,
|
||||
{
|
||||
// 找不到路由重定向到404页面
|
||||
path: '/:pathMatch(.*)',
|
||||
redirect: { name: '404' },
|
||||
},
|
||||
|
|
|
@ -97,7 +97,7 @@ const login = (formEl: FormInstance | undefined) => {
|
|||
globalStore.setUserInfo(res.data.name);
|
||||
globalStore.setLogStatus(true);
|
||||
menuStore.setMenuList([]);
|
||||
ElMessage.success(i18n.global.t('commons.msg.loginSuccss'));
|
||||
ElMessage.success(i18n.global.t('commons.msg.loginSuccess'));
|
||||
router.push({ name: 'home' });
|
||||
} catch (error) {
|
||||
loginVerify();
|
||||
|
|
128
frontend/src/views/operation-log/index.vue
Normal file
128
frontend/src/views/operation-log/index.vue
Normal file
|
@ -0,0 +1,128 @@
|
|||
<template>
|
||||
<LayoutContent :header="$t('menu.operations')">
|
||||
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search">
|
||||
<el-table-column :label="$t('operations.operatoin')" fix>
|
||||
<template #default="{ row }">
|
||||
{{ fmtOperation(row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('operations.status')" prop="status">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.status == '200'" class="ml-2" type="success">{{ row.status }}</el-tag>
|
||||
<div v-else>
|
||||
<el-popover
|
||||
placement="top-start"
|
||||
:title="$t('commons.table.message')"
|
||||
:width="400"
|
||||
trigger="hover"
|
||||
:content="row.errorMessage"
|
||||
>
|
||||
<template #reference>
|
||||
<el-tag class="ml-2" type="warning">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="IP" prop="ip" />
|
||||
<el-table-column align="left" :label="$t('operations.request')" prop="path">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<el-popover :width="500" v-if="row.body" placement="left-start" trigger="click">
|
||||
<div style="word-wrap: break-word; font-size: 12px; white-space: normal">
|
||||
<pre class="pre">{{ fmtBody(row.body) }}</pre>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-icon style="cursor: pointer"><warning /></el-icon>
|
||||
</template>
|
||||
</el-popover>
|
||||
<span v-else>无</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" :label="$t('operations.response')" prop="path">
|
||||
<template #default="{ row }">
|
||||
<div>
|
||||
<el-popover :width="500" v-if="row.resp" placement="left-start" trigger="click">
|
||||
<div style="word-wrap: break-word; font-size: 12px; white-space: normal">
|
||||
<pre class="pre">{{ fmtBody(row.resp) }}</pre>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-icon style="cursor: pointer"><warning /></el-icon>
|
||||
</template>
|
||||
</el-popover>
|
||||
<span v-else>无</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
:label="$t('commons.table.date')"
|
||||
:formatter="dateFromat"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</ComplexTable>
|
||||
</LayoutContent>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LayoutContent from '@/layout/layout-content.vue';
|
||||
import ComplexTable from '@/components/complex-table/index.vue';
|
||||
import { dateFromat } from '@/utils/util';
|
||||
import { getOperationList } from '@/api/modules/operation-log';
|
||||
import { onMounted, reactive, ref } from '@vue/runtime-core';
|
||||
import { ResOperationLog } from '@/api/interface/operation-log';
|
||||
import i18n from '@/lang';
|
||||
|
||||
const data = ref();
|
||||
const paginationConfig = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const search = async () => {
|
||||
const { currentPage, pageSize } = paginationConfig;
|
||||
const res = await getOperationList(currentPage, pageSize);
|
||||
data.value = res.data.items;
|
||||
paginationConfig.total = res.data.total;
|
||||
};
|
||||
|
||||
const fmtOperation = (row: ResOperationLog) => {
|
||||
if (row.source == '' && row.action == '') {
|
||||
return (
|
||||
i18n.global.t('operations.detail.' + row.group.toLocaleLowerCase()) +
|
||||
i18n.global.t('operations.detail.' + row.method.toLocaleLowerCase())
|
||||
);
|
||||
}
|
||||
if (row.action == '') {
|
||||
return (
|
||||
i18n.global.t('operations.detail.' + row.group.toLocaleLowerCase()) +
|
||||
i18n.global.t('operations.detail.' + row.source.toLocaleLowerCase())
|
||||
);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const fmtBody = (value: string) => {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (err) {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
search();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pre {
|
||||
white-space: pre-wrap;
|
||||
white-space: -moz-pre-wrap;
|
||||
white-space: -pre-wrap;
|
||||
white-space: -o-pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
</style>
|
|
@ -41,10 +41,10 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|||
open: viteEnv.VITE_OPEN,
|
||||
// https: false,
|
||||
proxy: {
|
||||
'/api': {
|
||||
'/api/v1': {
|
||||
target: 'http://localhost:9999',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
// rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue