mirror of
https://github.com/blossom-editor/blossom
synced 2024-11-17 14:39:21 +08:00
feat:用户管理
This commit is contained in:
parent
88c01f5fbb
commit
f83660a738
@ -1,9 +1,12 @@
|
||||
package com.blossom.backend.base.auth;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.blossom.backend.base.auth.pojo.LoginReq;
|
||||
import com.blossom.backend.base.auth.annotation.AuthIgnore;
|
||||
import com.blossom.backend.base.auth.annotation.AuthUserType;
|
||||
import com.blossom.backend.base.auth.pojo.AccessToken;
|
||||
import com.blossom.backend.base.auth.pojo.KickOutReq;
|
||||
import com.blossom.backend.base.auth.pojo.LoginReq;
|
||||
import com.blossom.backend.base.user.UserTypeEnum;
|
||||
import com.blossom.common.base.exception.XzException400;
|
||||
import com.blossom.common.base.pojo.R;
|
||||
import lombok.AllArgsConstructor;
|
||||
@ -52,6 +55,18 @@ public class AuthController {
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 踢出用户
|
||||
*
|
||||
* @since 1.13.0
|
||||
*/
|
||||
@AuthUserType(UserTypeEnum.ADMIN)
|
||||
@PostMapping("kickout")
|
||||
public R<?> kickout(@RequestBody KickOutReq req) {
|
||||
authService.kickout(req.getUserId());
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 Token 状态
|
||||
*/
|
||||
|
@ -46,6 +46,7 @@ public class AuthService extends AbstractAuthService {
|
||||
AuthException.throwBy(StrUtil.isBlank(login.getPassword()), AuthRCode.USERNAME_OR_PWD_FAULT);
|
||||
UserEntity user = userService.selectByUsername(login.getUsername());
|
||||
AuthException.throwBy(ObjUtil.isNull(user), AuthRCode.USERNAME_OR_PWD_FAULT);
|
||||
AuthException.throwBy(user.getDelTime() == null || !user.getDelTime().equals(0L), AuthRCode.USER_NOT_ENABLED);
|
||||
AuthException.throwBy(!passwordEncoder.matches(login.getPassword() + user.getSalt(), user.getPassword()), AuthRCode.USERNAME_OR_PWD_FAULT);
|
||||
fillUserDetail(accessToken, user);
|
||||
}
|
||||
@ -59,6 +60,15 @@ public class AuthService extends AbstractAuthService {
|
||||
tokenRepository.remove(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 踢出用户的所有令牌
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
public void kickout(Long userId) {
|
||||
tokenRepository.removeAll(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
|
@ -0,0 +1,19 @@
|
||||
package com.blossom.backend.base.auth.annotation;
|
||||
|
||||
import com.blossom.backend.base.user.UserTypeEnum;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 接口校验用户类型
|
||||
*/
|
||||
@Documented
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface AuthUserType {
|
||||
|
||||
/**
|
||||
* 用户的类型, 接口只允许该类型用户调用
|
||||
*/
|
||||
UserTypeEnum value();
|
||||
}
|
@ -13,6 +13,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@ -42,7 +43,7 @@ public class CaffeineTokenRepository implements TokenRepository {
|
||||
.initialCapacity(1000)
|
||||
.expireAfterWrite(client.getDuration(), TimeUnit.SECONDS)
|
||||
.removalListener((String key, AccessToken value, RemovalCause cause) ->
|
||||
log.info("Token [" + key + "] 被删除")
|
||||
log.info("Token [" + key + "] has been deleted")
|
||||
)
|
||||
.build();
|
||||
|
||||
@ -50,9 +51,10 @@ public class CaffeineTokenRepository implements TokenRepository {
|
||||
.initialCapacity(1000)
|
||||
.expireAfterWrite(client.getDuration(), TimeUnit.SECONDS)
|
||||
.removalListener((String userId, String token, RemovalCause cause) ->
|
||||
log.info("Unique Token(userId) [" + userId + "] 被删除")
|
||||
log.info("Unique Token(userId) [" + userId + "] has been deleted")
|
||||
)
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -78,6 +80,17 @@ public class CaffeineTokenRepository implements TokenRepository {
|
||||
tokenCache.invalidate(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAll(Long userId) {
|
||||
uniqueTokenCache.invalidate(userId);
|
||||
Map<String, AccessToken> maps = tokenCache.asMap();
|
||||
maps.forEach((k, t) -> {
|
||||
if (t.getUserId().equals(userId)) {
|
||||
tokenCache.invalidate(k);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveUniqueToken(AccessToken accessToken) {
|
||||
if (accessToken == null || StrUtil.isBlank(accessToken.getToken())) {
|
||||
|
@ -27,7 +27,7 @@ public enum AuthRCode implements IRCode {
|
||||
*/
|
||||
USERNAME_OR_PWD_FAULT ("AUTH-40004", "用户名或密码错误","用户名或密码错误, 或用户名不存在。"),
|
||||
CAPTCHA_FAULT ("AUTH-40005", "验证码错误","验证码错误, 或手机号不存在。"),
|
||||
USER_NOT_ENABLED ("AUTH-40010", "用户已禁用, 暂时无法登录","用户已禁用, 暂时无法登录。"),
|
||||
USER_NOT_ENABLED ("AUTH-40010", "您的账户已被已禁用, 暂时无法登录","您的账户已被已禁用, 暂时无法登录。"),
|
||||
|
||||
/**
|
||||
* 401: 未经过认证
|
||||
|
@ -2,8 +2,10 @@ package com.blossom.backend.base.auth.interceptor;
|
||||
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
import com.blossom.backend.base.auth.annotation.AuthIgnore;
|
||||
import com.blossom.backend.base.auth.annotation.AuthUserType;
|
||||
import com.blossom.backend.base.auth.exception.AuthException;
|
||||
import com.blossom.backend.base.auth.exception.AuthRCode;
|
||||
import com.blossom.backend.base.user.UserTypeEnum;
|
||||
import com.blossom.common.base.exception.XzException400;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
@ -14,7 +16,7 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 判断用户的类型, 只读用户只允许发送 get 请求
|
||||
* 接口对于用户的校验
|
||||
*/
|
||||
@Slf4j
|
||||
public class UserTypeInterceptor implements HandlerInterceptor {
|
||||
@ -28,7 +30,7 @@ public class UserTypeInterceptor implements HandlerInterceptor {
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
// 忽略静态资源处理器
|
||||
if(handler instanceof ResourceHttpRequestHandler) {
|
||||
if (handler instanceof ResourceHttpRequestHandler) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -40,12 +42,25 @@ public class UserTypeInterceptor implements HandlerInterceptor {
|
||||
if (isIgnore) {
|
||||
return true;
|
||||
}
|
||||
// GET请求校验
|
||||
if (HttpMethod.GET.name().equals(request.getMethod())) {
|
||||
return true;
|
||||
|
||||
// 校验接口允许的用户类型
|
||||
boolean isCheckUserType = handlerMethod.hasMethodAnnotation(AuthUserType.class);
|
||||
if (isCheckUserType) {
|
||||
AuthUserType userType = handlerMethod.getMethodAnnotation(AuthUserType.class);
|
||||
if (userType != null) {
|
||||
UserTypeEnum type = userType.value();
|
||||
if (!type.getType().equals(AuthContext.getType())) {
|
||||
throw new AuthException(AuthRCode.PERMISSION_DENIED);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 只读账号不非 GET 请求
|
||||
if (UserTypeEnum.READONLY.getType().equals(AuthContext.getType()) && !HttpMethod.GET.name().equals(request.getMethod())) {
|
||||
throw new AuthException(AuthRCode.PERMISSION_DENIED.getCode(), "您的账号为只读账号, 无法使用该功能");
|
||||
}
|
||||
|
||||
XzException400.throwBy(UserTypeEnum.READONLY.getType().equals(AuthContext.getType()), "您的账号为只读账号, 无法使用该功能");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,10 @@ public class JWTTokenRepository implements TokenRepository {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAll(Long userId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveUniqueToken(AccessToken accessToken) {
|
||||
|
||||
|
@ -0,0 +1,15 @@
|
||||
package com.blossom.backend.base.auth.pojo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 踢出用户
|
||||
*/
|
||||
@Data
|
||||
public class KickOutReq {
|
||||
|
||||
@NotNull(message = "userId 为必填项")
|
||||
private Long userId;
|
||||
}
|
@ -31,6 +31,13 @@ public interface TokenRepository {
|
||||
*/
|
||||
void remove(String token);
|
||||
|
||||
/**
|
||||
* 删除某个用户的所有 token
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
void removeAll(Long userId);
|
||||
|
||||
/**
|
||||
* 保存唯一生效的 token 对象
|
||||
*
|
||||
|
@ -1,7 +1,11 @@
|
||||
package com.blossom.backend.base.paramu;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
import com.blossom.backend.base.auth.annotation.AuthUserType;
|
||||
import com.blossom.backend.base.paramu.pojo.UserParamUpdReq;
|
||||
import com.blossom.backend.base.user.UserTypeEnum;
|
||||
import com.blossom.common.base.exception.XzException400;
|
||||
import com.blossom.common.base.pojo.R;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -45,6 +49,20 @@ public class UserParamController {
|
||||
return R.ok(baseService.selectMap(AuthContext.getUserId(), true, UserParamEnum.values()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户参数
|
||||
*
|
||||
* @apiNote 需要管理员权限
|
||||
*/
|
||||
@AuthUserType(UserTypeEnum.ADMIN)
|
||||
@PostMapping("/upd/admin")
|
||||
public R<Map<String, String>> updByAdmin(@Validated @RequestBody UserParamUpdReq req) {
|
||||
XzException400.throwBy(ObjUtil.isNull(req.getUserId()), "用户ID为必填项");
|
||||
baseService.update(req);
|
||||
baseService.refresh();
|
||||
return R.ok(baseService.selectMap(AuthContext.getUserId(), true, UserParamEnum.values()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新用户配置
|
||||
*/
|
||||
|
@ -2,6 +2,7 @@ package com.blossom.backend.base.sys;
|
||||
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
import com.blossom.backend.base.auth.annotation.AuthIgnore;
|
||||
import com.blossom.backend.base.auth.annotation.AuthUserType;
|
||||
import com.blossom.backend.base.param.ParamEnum;
|
||||
import com.blossom.backend.base.param.ParamService;
|
||||
import com.blossom.backend.base.param.pojo.ParamUpdReq;
|
||||
@ -63,6 +64,7 @@ public class SysController {
|
||||
/**
|
||||
* 修改系统参数
|
||||
*/
|
||||
@AuthUserType(UserTypeEnum.ADMIN)
|
||||
@PostMapping("/param/upd")
|
||||
public R<Map<String, String>> upd(@Validated @RequestBody ParamUpdReq req) {
|
||||
if (!UserTypeEnum.ADMIN.getType().equals(AuthContext.getType())) {
|
||||
@ -76,6 +78,7 @@ public class SysController {
|
||||
/**
|
||||
* 刷新系统配置
|
||||
*/
|
||||
@AuthUserType(UserTypeEnum.ADMIN)
|
||||
@PostMapping("/param/refresh")
|
||||
public R<?> paramRefresh() {
|
||||
paramService.refresh();
|
||||
|
@ -2,7 +2,9 @@ package com.blossom.backend.base.user;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
import com.blossom.backend.base.auth.AuthService;
|
||||
import com.blossom.backend.base.auth.annotation.AuthIgnore;
|
||||
import com.blossom.backend.base.auth.annotation.AuthUserType;
|
||||
import com.blossom.backend.base.param.ParamEnum;
|
||||
import com.blossom.backend.base.param.ParamService;
|
||||
import com.blossom.backend.base.paramu.UserParamEnum;
|
||||
@ -12,14 +14,15 @@ import com.blossom.backend.base.user.pojo.*;
|
||||
import com.blossom.backend.config.BlConstants;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleStatRes;
|
||||
import com.blossom.backend.server.article.stat.ArticleStatService;
|
||||
import com.blossom.common.base.exception.XzException400;
|
||||
import com.blossom.common.base.exception.XzException404;
|
||||
import com.blossom.common.base.exception.XzException500;
|
||||
import com.blossom.common.base.pojo.R;
|
||||
import com.blossom.common.base.util.spring.SpringUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@ -32,24 +35,84 @@ import java.util.Map;
|
||||
@AllArgsConstructor
|
||||
public class UserController {
|
||||
private final UserService userService;
|
||||
private final AuthService authService;
|
||||
private final ArticleStatService articleService;
|
||||
private final SysService sysService;
|
||||
private final ParamService paramService;
|
||||
private final UserParamService userParamService;
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*
|
||||
* @apiNote 需要管理员权限
|
||||
* @since 1.13.0
|
||||
*/
|
||||
@AuthUserType(UserTypeEnum.ADMIN)
|
||||
@GetMapping("/list")
|
||||
public R<List<UserListRes>> list() {
|
||||
return R.ok(userService.listAll(), UserListRes.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @apiNote 根据用户ID获取用户信息, 需要管理员权限
|
||||
*/
|
||||
@AuthUserType(UserTypeEnum.ADMIN)
|
||||
@GetMapping("/info/admin")
|
||||
public R<BlossomUserRes> user(@RequestParam("id") Long id) {
|
||||
return getUserById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增用户
|
||||
*
|
||||
* @apiNote 需要管理员权限
|
||||
*/
|
||||
@AuthUserType(UserTypeEnum.ADMIN)
|
||||
@PostMapping("/add")
|
||||
public R<?> add(@Validated @RequestBody UserAddReq req) {
|
||||
userService.insert(req);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户
|
||||
*
|
||||
* @apiNote 需要管理员权限
|
||||
*/
|
||||
@AuthUserType(UserTypeEnum.ADMIN)
|
||||
@PostMapping("/upd/admin")
|
||||
public R<?> updateImportant(@Validated @RequestBody UserUpdAdminReq req) {
|
||||
UserEntity user = req.to(UserEntity.class);
|
||||
userService.updById(user);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用启用
|
||||
*/
|
||||
@AuthUserType(UserTypeEnum.ADMIN)
|
||||
@PostMapping("/disabled")
|
||||
public R<?> disabled(@Validated @RequestBody UserDisabledReq req) {
|
||||
if (req.getId().equals(AuthContext.getUserId())) {
|
||||
throw new XzException500("不能禁用自己");
|
||||
}
|
||||
UserEntity user = req.to(UserEntity.class);
|
||||
userService.updById(user);
|
||||
authService.kickout(req.getId());
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*
|
||||
* @apiNote 当前登录用户的用户信息
|
||||
*/
|
||||
@GetMapping("/info")
|
||||
public R<BlossomUserRes> user() {
|
||||
BlossomUserRes user = userService.selectById(AuthContext.getUserId()).to(BlossomUserRes.class);
|
||||
user.setOsRes(sysService.getOsConfig());
|
||||
Map<String, String> paramMap = paramService.selectMap(true, ParamEnum.values());
|
||||
user.setParams(paramMap);
|
||||
paramMap.put("SERVER_VERSION", SpringUtil.get("project.base.version"));
|
||||
Map<String, String> userParamMap = userParamService.selectMap(AuthContext.getUserId(), true, UserParamEnum.values());
|
||||
user.setUserParams(userParamMap);
|
||||
return R.ok(user);
|
||||
return getUserById(AuthContext.getUserId());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -96,15 +159,19 @@ public class UserController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增用户
|
||||
* 获取用户信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
public R<?> add(@Validated @RequestBody UserAddReq req) {
|
||||
UserEntity curUser = userService.getById(AuthContext.getUserId());
|
||||
if (curUser == null || !UserTypeEnum.ADMIN.getType().equals(curUser.getType())) {
|
||||
throw new XzException400("您没有权限添加用户");
|
||||
}
|
||||
userService.insert(req);
|
||||
return R.ok();
|
||||
private R<BlossomUserRes> getUserById(Long userId) {
|
||||
BlossomUserRes user = userService.selectById(userId).to(BlossomUserRes.class);
|
||||
user.setOsRes(sysService.getOsConfig());
|
||||
Map<String, String> paramMap = paramService.selectMap(true, ParamEnum.values());
|
||||
user.setParams(paramMap);
|
||||
paramMap.put("SERVER_VERSION", SpringUtil.get("project.base.version"));
|
||||
Map<String, String> userParamMap = userParamService.selectMap(AuthContext.getUserId(), true, UserParamEnum.values());
|
||||
user.setUserParams(userParamMap);
|
||||
return R.ok(user);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@ -56,6 +57,14 @@ public class BlossomUserRes extends AbstractPOJO implements Serializable {
|
||||
* 文章字数
|
||||
*/
|
||||
private Integer articleWords;
|
||||
/**
|
||||
* 创建日期
|
||||
*/
|
||||
private Date creTime;
|
||||
/**
|
||||
* 逻辑删除, 目前用于禁用用户, 而不是删除
|
||||
*/
|
||||
private Long delTime;
|
||||
|
||||
/**
|
||||
* 对象存储信息, 非登录状态不返回该字段
|
||||
|
@ -0,0 +1,20 @@
|
||||
package com.blossom.backend.base.user.pojo;
|
||||
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
public class UserDisabledReq extends AbstractPOJO {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@NotNull(message = "用户ID为必填项")
|
||||
private Long id;
|
||||
/**
|
||||
* 禁用状态
|
||||
*/
|
||||
private Long delTime;
|
||||
}
|
@ -8,6 +8,7 @@ import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 用户
|
||||
@ -66,5 +67,13 @@ public class UserEntity extends AbstractPOJO implements Serializable {
|
||||
* 用户的位置
|
||||
*/
|
||||
private String location;
|
||||
/**
|
||||
* 创建日期
|
||||
*/
|
||||
private Date creTime;
|
||||
/**
|
||||
* 逻辑删除, 目前用于禁用用户, 而不是删除
|
||||
*/
|
||||
private Long delTime;
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
package com.blossom.backend.base.user.pojo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class UserListRes {
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 用户头像
|
||||
*/
|
||||
private String avatar;
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
private String nickName;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer type;
|
||||
/**
|
||||
* 创建日期
|
||||
*/
|
||||
private Date creTime;
|
||||
/**
|
||||
* 逻辑删除, 目前用于禁用用户, 而不是删除
|
||||
*/
|
||||
private Long delTime;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package com.blossom.backend.base.user.pojo;
|
||||
|
||||
import com.blossom.common.base.pojo.AbstractEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 用户请求
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class UserUpdAdminReq extends AbstractEntity {
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 和风天气的位置
|
||||
*/
|
||||
private String location;
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private Integer type;
|
||||
}
|
@ -37,6 +37,5 @@ public class UserUpdReq extends AbstractEntity {
|
||||
/**
|
||||
* 用户头像
|
||||
*/
|
||||
@NotBlank(message = "用户头像为必填项")
|
||||
private String avatar;
|
||||
}
|
||||
|
@ -24,6 +24,8 @@
|
||||
<if test="avatar != null">avatar = #{avatar},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
<if test="location != null">location = #{location},</if>
|
||||
<if test="delTime != null">del_time = #{delTime},</if>
|
||||
<if test="type != null">type = #{type},</if>
|
||||
upd_time = now()
|
||||
</set>
|
||||
where id = #{id}
|
||||
|
@ -18,10 +18,18 @@ export const checkApi = (): Promise<R<any>> => {
|
||||
return rq.get<R<any>>('/check', {})
|
||||
}
|
||||
|
||||
export const userListApi = (params?: object): Promise<R<any>> => {
|
||||
return rq.get<R<any>>('/user/list', { params })
|
||||
}
|
||||
|
||||
export const userinfoApi = (params?: object): Promise<R<any>> => {
|
||||
return rq.get<R<any>>('/user/info', { params })
|
||||
}
|
||||
|
||||
export const userinfoAdminApi = (params?: object): Promise<R<any>> => {
|
||||
return rq.get<R<any>>('/user/info/admin', { params })
|
||||
}
|
||||
|
||||
export const userUpdApi = (data?: object): Promise<R<any>> => {
|
||||
return rq.post<any>('/user/upd', data)
|
||||
}
|
||||
@ -30,6 +38,14 @@ export const userUpdPwdApi = (data?: object): Promise<R<any>> => {
|
||||
return rq.post<any>('/user/upd/pwd', data)
|
||||
}
|
||||
|
||||
export const userUpdAdminApi = (data?: object): Promise<R<any>> => {
|
||||
return rq.post<any>('/user/upd/admin', data)
|
||||
}
|
||||
|
||||
export const userAddApi = (data?: object): Promise<R<any>> => {
|
||||
return rq.post<any>('/user/add', data)
|
||||
}
|
||||
|
||||
export const userDisabledApi = (data?: object): Promise<R<any>> => {
|
||||
return rq.post<any>('/user/disabled', data)
|
||||
}
|
||||
|
@ -40,13 +40,21 @@ export const userParamListApi = (): Promise<R<any>> => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改系统参数
|
||||
* 修改用户参数
|
||||
* @returns
|
||||
*/
|
||||
export const userParamUpdApi = (data: object): Promise<R<any>> => {
|
||||
return rq.post<R<any>>('/user/param/upd', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员修改用户参数
|
||||
* @returns
|
||||
*/
|
||||
export const userParamUpdAdminApi = (data: object): Promise<R<any>> => {
|
||||
return rq.post<R<any>>('/user/param/upd/admin', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新系统参数缓存
|
||||
* @param data 文件 form
|
||||
@ -304,6 +312,15 @@ export const articleWordsApi = (params?: object): Promise<R<any>> => {
|
||||
return rq.get<R<any>>('/article/stat/words', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定用户的文章数和文章字数统计
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export const articleWordsUserApi = (params?: object): Promise<R<any>> => {
|
||||
return rq.get<R<any>>('/article/stat/words/user', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章数和文章字数统计
|
||||
* @param params
|
||||
@ -543,6 +560,15 @@ export const pictureStatApi = (params?: object): Promise<R<any>> => {
|
||||
return rq.get<R<any>>('/picture/stat', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除图片
|
||||
* @param params {id:id}
|
||||
* @returns
|
||||
*/
|
||||
export const pictureStatUserApi = (params?: object): Promise<R<any>> => {
|
||||
return rq.get<R<any>>('/picture/stat/user', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 转移文件
|
||||
* @param data
|
||||
|
@ -174,8 +174,6 @@ const copyIcon = (icon: string) => {
|
||||
@include box(143px, 110px);
|
||||
padding: 15px 10px 10px 10px;
|
||||
font-size: 35px;
|
||||
// border-top: 1px solid var(--el-border-color);
|
||||
// border-left: 1px solid var(--el-border-color);
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
transition: 0.3s;
|
||||
|
@ -48,7 +48,7 @@ const initAuth = () => {
|
||||
/**
|
||||
* 默认用户信息
|
||||
*/
|
||||
const DEFAULT_USER_INFO = {
|
||||
export const DEFAULT_USER_INFO = {
|
||||
id: '',
|
||||
type: 2,
|
||||
username: '暂未登录',
|
||||
@ -58,6 +58,8 @@ const DEFAULT_USER_INFO = {
|
||||
articleCount: 0,
|
||||
articleWords: 0,
|
||||
location: '',
|
||||
creTime: '',
|
||||
delTime: '',
|
||||
osRes: {
|
||||
osType: '',
|
||||
bucketName: '',
|
||||
|
@ -204,7 +204,7 @@ import Notify from '@renderer/scripts/notify'
|
||||
import { useDraggable } from '@renderer/scripts/draggable'
|
||||
import type { shortcutFunc } from '@renderer/scripts/shortcut-register'
|
||||
import { treeToInfo, provideKeyDocInfo, provideKeyCurArticleInfo, isArticle } from '@renderer/views/doc/doc'
|
||||
import { TempTextareaKey, ArticleReference, DocsEditorStyle, parseTocAsync } from './scripts/article'
|
||||
import { TempTextareaKey, ArticleReference, parseTocAsync } from './scripts/article'
|
||||
import type { Toc } from './scripts/article'
|
||||
import { beforeUpload, onError, picCacheWrapper, picCacheRefresh, uploadForm, uploadDate } from '@renderer/views/picture/scripts/picture'
|
||||
import { useResize } from './scripts/editor-preview-resize'
|
||||
|
@ -21,19 +21,24 @@
|
||||
<ConfigAddUser></ConfigAddUser>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="实时访问流量" name="flow" v-if="userStore.isLogin && userStore.userinfo.type === 1" :lazy="true">
|
||||
<SentinelResources></SentinelResources>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { useUserStore } from '@renderer/stores/user'
|
||||
import ConfigUserinfo from './SettingConfigUserinfo.vue'
|
||||
import ConfigUpdPwd from './SettingConfigUpdPwd.vue'
|
||||
import ConfigAddUser from './SettingConfigAddUser.vue'
|
||||
import ConfigClient from './SettingConfigClient.vue'
|
||||
import ConfigServer from './SettingConfigServer.vue'
|
||||
import ConfigBlog from './SettingConfigBlog.vue'
|
||||
import { useUserStore } from '@renderer/stores/user'
|
||||
import SentinelResources from '@renderer/views/statistic/SentinelResources.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const curTab = ref('client')
|
||||
@ -63,6 +68,7 @@ const handleChange = (name: string) => {
|
||||
|
||||
:deep(.el-tabs__item.is-left.is-active) {
|
||||
border-right-color: #ffffff00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="config-root" v-loading="!userStore.isLogin" element-loading-spinner="none" element-loading-text="请登录后查看...">
|
||||
<div class="title">博客配置</div>
|
||||
<div class="desc">博客各项参数配置,若无内容请点击右侧刷新。<el-button @click="refreshParam" text bg>刷新</el-button></div>
|
||||
<div class="desc">博客各项参数配置,若无内容请点击右侧刷新。<el-button @click="refreshParam" text bg>刷新参数</el-button></div>
|
||||
|
||||
<el-form :model="userParamForm" label-position="right" label-width="130px" style="max-width: 800px">
|
||||
<el-form-item label="文章查看地址" :required="true">
|
||||
@ -109,7 +109,7 @@ const getParamList = () => {
|
||||
|
||||
const refreshParam = () => {
|
||||
userParamRefreshApi().then((_) => {
|
||||
Notify.success('刷新参数成功', '刷新成功')
|
||||
Notify.success('', '刷新成功')
|
||||
getParamList()
|
||||
userStore.getUserinfo()
|
||||
})
|
||||
|
@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div class="config-root" v-loading="!userStore.isLogin" element-loading-spinner="none" element-loading-text="请登录后查看...">
|
||||
<div class="title">服务器配置</div>
|
||||
<div class="desc">服务器各项参数配置,若无内容请点击右侧刷新。<el-button @click="refreshParam" text bg>刷新</el-button></div>
|
||||
<div class="desc" style="margin-bottom: 0">服务器各项参数配置,若无内容请点击下方刷新。只有管理员用户具有操作服务器配置的权限。</div>
|
||||
<div class="desc">
|
||||
<el-button @click="refreshParam" text bg><span class="iconbl bl-refresh-line"></span>刷新参数</el-button>
|
||||
<el-button @click="showUserListDialog" text bg><span class="iconbl bl-user-line"></span>用户管理</el-button>
|
||||
</div>
|
||||
|
||||
<el-form :model="serverParamForm" label-position="right" label-width="130px" style="max-width: 800px">
|
||||
<el-form-item label="文件访问地址" :required="true">
|
||||
@ -100,21 +104,35 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="server-config">
|
||||
<!-- <div class="server-config">
|
||||
{{ userStore.userinfo }}
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 自定义临时访问链接 -->
|
||||
<el-dialog
|
||||
v-model="isShowUserListDialog"
|
||||
class="bl-dialog-fixed-body"
|
||||
width="710"
|
||||
style="height: 70%"
|
||||
:align-center="true"
|
||||
:append-to-body="true"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="true">
|
||||
<UserListSetting></UserListSetting>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useServerStore } from '@renderer/stores/server'
|
||||
import { KEY_BLOSSOM_OBJECT_STORAGE_DOMAIN, useUserStore } from '@renderer/stores/user'
|
||||
import { paramListApi, paramUpdApi, paramRefreshApi } from '@renderer/api/blossom'
|
||||
import { getDateTimeFormat, betweenDay } from '@renderer/assets/utils/util'
|
||||
import Notify from '@renderer/scripts/notify'
|
||||
import dayjs from 'dayjs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import UserListSetting from './setting/UserListSetting.vue'
|
||||
|
||||
const serverStore = useServerStore()
|
||||
const userStore = useUserStore()
|
||||
@ -177,7 +195,7 @@ const getParamList = () => {
|
||||
|
||||
const refreshParam = () => {
|
||||
paramRefreshApi().then((_) => {
|
||||
Notify.success('刷新参数成功', '刷新成功')
|
||||
Notify.success('', '刷新成功')
|
||||
getParamList()
|
||||
userStore.getUserinfo()
|
||||
})
|
||||
@ -202,6 +220,13 @@ const autuUpdBlossomOSDomain = () => {
|
||||
})
|
||||
}
|
||||
|
||||
//#region ----------------------------------------< 菜单 >--------------------------------------
|
||||
const isShowUserListDialog = ref(false)
|
||||
const showUserListDialog = () => {
|
||||
isShowUserListDialog.value = true
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const reload = () => {
|
||||
getParamList()
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="config-root" v-loading="!userStore.isLogin" element-loading-spinner="none" element-loading-text="请登录后查看...">
|
||||
<div class="title">修改用户信息</div>
|
||||
<div class="desc">用户的个人信息,若无内容请点击右侧刷新。<el-button @click="refreshUserinfo" text bg>刷新</el-button></div>
|
||||
<div class="desc">用户的个人信息,若无内容请点击右侧刷新。<el-button @click="refreshUserinfo" text bg>刷新信息</el-button></div>
|
||||
<el-form :model="userinfoForm" :rules="rules" label-position="right" label-width="130px" style="max-width: 800px" ref="UserinfoFormRef">
|
||||
<el-form-item label="ID" prop="id">
|
||||
<el-input v-model="userinfoForm.id" size="default" disabled>
|
||||
@ -99,8 +99,7 @@ const rules = ref<FormRules<UserinfoForm>>({
|
||||
id: [{ required: true, message: '请填写用户ID', trigger: 'blur' }],
|
||||
username: [{ required: true, message: '请填写用户名', trigger: 'blur' }],
|
||||
nickName: [{ required: true, message: '请填写昵称', trigger: 'blur' }],
|
||||
remark: [{ required: true, message: '请填写备注', trigger: 'blur' }],
|
||||
avatar: [{ required: true, message: '请填写头像', trigger: 'blur' }]
|
||||
remark: [{ required: true, message: '请填写备注', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const getUserinfo = () => {
|
||||
@ -112,7 +111,7 @@ const getUserinfo = () => {
|
||||
const refreshUserinfo = () => {
|
||||
userinfoApi().then((resp) => {
|
||||
userinfoForm.value = resp.data
|
||||
Notify.success('刷新用户信息成功', '刷新成功')
|
||||
Notify.success('', '刷新成功')
|
||||
})
|
||||
}
|
||||
|
||||
@ -121,7 +120,7 @@ const save = async (formEl: FormInstance | undefined) => {
|
||||
await formEl.validate((valid, _fields) => {
|
||||
if (valid) {
|
||||
userUpdApi(userinfoForm.value).then((_resp) => {
|
||||
Notify.success('个人信息修改成功')
|
||||
Notify.success('您的个人信息已变更', '修改成功')
|
||||
userStore.checkToken(
|
||||
() => {},
|
||||
() => {}
|
||||
|
@ -14,9 +14,6 @@
|
||||
<el-tab-pane label="关于" name="about">
|
||||
<SettingAboutVue></SettingAboutVue>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="访问流量" :lazy="true" name="flow">
|
||||
<SentinelResources></SentinelResources>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div class="version">
|
||||
@ -32,7 +29,6 @@ import { isNotBlank } from '@renderer/assets/utils/obj'
|
||||
import SettingLogin from './SettingLogin.vue'
|
||||
import SettingConfig from './SettingConfig.vue'
|
||||
import SettingAboutVue from './SettingAbout.vue'
|
||||
import SentinelResources from '@renderer/views/statistic/SentinelResources.vue'
|
||||
import CONFIG from '@renderer/assets/constants/system'
|
||||
|
||||
const userStore = useUserStore()
|
||||
@ -40,7 +36,6 @@ const route = useRoute()
|
||||
|
||||
onMounted(() => {
|
||||
let actTab = route.query.activeTab as string
|
||||
console.log(actTab)
|
||||
if (isNotBlank(actTab)) {
|
||||
activeTab.value = actTab
|
||||
} else {
|
||||
@ -70,7 +65,11 @@ const getServerVersion = () => {
|
||||
height: 100%;
|
||||
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
background-color: var(--el-color-primary-light-8);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
:deep(el-tabs__nav-scroll::after) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
|
@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<div class="userinfo-setting-root">
|
||||
<div class="info-title">
|
||||
<div class="info" style="width: 350px; font-size: 17px">{{ curUser?.nickName }}</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div :class="curUser.delTime !== '0' ? 'warn-bg-zebra' : ''">
|
||||
<bl-row class="info-group">
|
||||
<bl-col width="80px">
|
||||
<img v-if="curUser.avatar != ''" class="avatar" :src="curUser.avatar" />
|
||||
<img v-else class="avatar" src="@renderer/assets/imgs/default_user_avatar.jpg" />
|
||||
</bl-col>
|
||||
<bl-col width="300px" height="100px" just="space-around" align="flex-start" style="padding-left: 20px">
|
||||
<div class="info">ID {{ curUser.id }}</div>
|
||||
<div class="info">昵称 {{ curUser.nickName }}</div>
|
||||
<div class="info">用户名 {{ curUser.username }}</div>
|
||||
<bl-row just="space-between">
|
||||
<div>创建于 {{ curUser.creTime }}</div>
|
||||
<div :class="['type', curUser.type === 1 ? 'admin' : curUser.type === 2 ? 'normal' : 'read']">
|
||||
<span v-if="curUser.type === 1" class="iconbl bl-a-Securitypermissions-line"></span
|
||||
>{{ curUser.type === 1 ? '管理员' : curUser.type === 2 ? '普通' : '只读' }}
|
||||
</div>
|
||||
</bl-row>
|
||||
</bl-col>
|
||||
</bl-row>
|
||||
|
||||
<bl-row class="info-group stat-group" just="space-around">
|
||||
<div class="iconbl bl-a-texteditorhighlightcolor-line"></div>
|
||||
<bl-col align="flex-end" width="30%">
|
||||
<div style="margin-bottom: 5px">{{ formartNumber(articleStat.articleCount) }} 篇文章</div>
|
||||
<div>{{ formartNumber(articleStat.articleWords) }} 字</div>
|
||||
</bl-col>
|
||||
<div class="iconbl bl-image--line"></div>
|
||||
<bl-col align="flex-end" width="30%">
|
||||
<div style="margin-bottom: 5px">{{ formartNumber(pictureStat.pictureCount) }} 个文件</div>
|
||||
<div>{{ formatFileSize(pictureStat.pictureSize) }}</div>
|
||||
</bl-col>
|
||||
</bl-row>
|
||||
</div>
|
||||
|
||||
<el-divider style="margin: 0"></el-divider>
|
||||
|
||||
<div class="info-group">
|
||||
<div class="row">用户类型</div>
|
||||
<el-select class="row" size="default" v-model="curUser.type" style="width: 100%">
|
||||
<el-option :value="1" label="管理员">
|
||||
<span style="float: left">管理员</span>
|
||||
<span style="float: right; color: var(--bl-text-color-light)">允许管理用户,修改服务器配置</span>
|
||||
</el-option>
|
||||
<el-option :value="2" label="普通用户">
|
||||
<span style="float: left">普通用户</span>
|
||||
<span style="float: right; color: var(--bl-text-color-light)">除管理员特权外的所有功能</span>
|
||||
</el-option>
|
||||
<el-option :value="3" label="只读用户">
|
||||
<span style="float: left">只读用户</span>
|
||||
<span style="float: right; color: var(--bl-text-color-light)">只有查看权限,无法修改与删除</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<div class="row">和风天气地区</div>
|
||||
<el-input class="row" size="default" v-model="curUser.location">
|
||||
<template #append>
|
||||
<el-button @click="openExtenal('https://github.com/qwd/LocationList/blob/master/China-City-List-latest.csv')">查看城市代码</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<div class="row">博客地址</div>
|
||||
<el-input type="textarea" class="row" size="default" :row="2" resize="none" v-model="curUser.userParams.WEB_ARTICLE_URL">
|
||||
<template #prefix>
|
||||
<div class="iconbl bl-blog"></div>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<el-divider style="margin: 0"></el-divider>
|
||||
|
||||
<bl-row just="space-between" class="info-group">
|
||||
<el-button-group>
|
||||
<el-button size="default" text bg type="danger">删除</el-button>
|
||||
<el-button size="default" text bg type="warning" @click="disabled">
|
||||
{{ curUser.delTime === '0' ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
<el-button size="default" type="primary" @click="upd">保存</el-button>
|
||||
</bl-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, onMounted, ref } from 'vue'
|
||||
import { userinfoAdminApi, userDisabledApi, userUpdAdminApi } from '@renderer/api/auth'
|
||||
import { articleWordsUserApi, pictureStatUserApi, userParamUpdAdminApi } from '@renderer/api/blossom'
|
||||
import { DEFAULT_USER_INFO } from '@renderer/stores/user'
|
||||
import type { Userinfo } from '@renderer/stores/user'
|
||||
import { openExtenal } from '@renderer/assets/utils/electron'
|
||||
import { formartNumber, formatFileSize } from '@renderer/assets/utils/util'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { emit } from 'process'
|
||||
|
||||
onMounted(() => {
|
||||
getUserInfo()
|
||||
})
|
||||
|
||||
//#region ----------------------------------------< 用户列表 >--------------------------------------
|
||||
|
||||
const props = defineProps({
|
||||
id: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const articleStat = ref({ articleCount: 0, articleWords: 0 })
|
||||
const pictureStat = ref({ pictureCount: 0, pictureSize: 0 })
|
||||
const curUser = ref<Userinfo>(DEFAULT_USER_INFO)
|
||||
const getUserInfo = () => {
|
||||
userinfoAdminApi({ id: props.id }).then((resp) => {
|
||||
curUser.value = resp.data
|
||||
console.log(curUser.value)
|
||||
})
|
||||
articleWordsUserApi({ id: props.id }).then((resp) => {
|
||||
articleStat.value = resp.data
|
||||
})
|
||||
pictureStatUserApi({ id: props.id }).then((resp) => {
|
||||
pictureStat.value = resp.data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用启用用户
|
||||
*/
|
||||
const disabled = () => {
|
||||
if (curUser.value.delTime === '0') {
|
||||
ElMessageBox.confirm(
|
||||
`禁用后该用户将无法再次登录, 是否禁用?<br/>
|
||||
<div style="width:100%;text-align:center;font-size:14px">${curUser.value.nickName} (${curUser.value.id})</div>`,
|
||||
{
|
||||
confirmButtonText: '确定禁用',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
dangerouslyUseHTMLString: true,
|
||||
draggable: true
|
||||
}
|
||||
).then(() => {
|
||||
userDisabledApi({ id: props.id, delTime: Date.now() }).then((_resp) => {
|
||||
emits('saved')
|
||||
})
|
||||
})
|
||||
} else {
|
||||
userDisabledApi({ id: props.id, delTime: 0 }).then((_resp) => {
|
||||
emits('saved')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户信息
|
||||
*/
|
||||
const upd = () => {
|
||||
ElMessageBox.confirm(
|
||||
`修改后各项配置会在用户重新登录后生效, 是否保存用户信息?<br/>
|
||||
<div style="width:100%;text-align:center;font-size:14px">${curUser.value.nickName} (${curUser.value.id})</div>`,
|
||||
{
|
||||
confirmButtonText: '确定保存',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info',
|
||||
dangerouslyUseHTMLString: true,
|
||||
draggable: true
|
||||
}
|
||||
).then(() => {
|
||||
userUpdAdminApi({
|
||||
id: props.id,
|
||||
type: curUser.value.type,
|
||||
location: curUser.value.location
|
||||
}).then((_resp) => {
|
||||
userParamUpdAdminApi({
|
||||
userId: props.id,
|
||||
paramName: 'WEB_ARTICLE_URL',
|
||||
paramValue: curUser.value.userParams.WEB_ARTICLE_URL
|
||||
}).then((_resp) => {
|
||||
emits('saved')
|
||||
ElMessage.info('用户信息已更新')
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const emits = defineEmits(['saved'])
|
||||
|
||||
//#endregion
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@renderer/assets/styles/bl-dialog-info';
|
||||
@import '@renderer/views/index/styles/user-setting';
|
||||
.userinfo-setting-root {
|
||||
@include box(100%, 100%);
|
||||
@include font(13px, 300);
|
||||
|
||||
.info {
|
||||
@include ellipsis();
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
@include box(100%, calc(100% - 50px));
|
||||
position: relative;
|
||||
|
||||
.info-group {
|
||||
padding: 10px;
|
||||
|
||||
.row {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-group {
|
||||
font-size: 12px;
|
||||
.bl-a-texteditorhighlightcolor-line,
|
||||
.bl-image--line {
|
||||
@include themeText(2px 4px 5px rgba(134, 134, 134, 0.3), 2px 4px 7px #222222);
|
||||
@include themeColor(#8a8a8a, #656565);
|
||||
position: absolute;
|
||||
font-size: 30px;
|
||||
}
|
||||
.bl-a-texteditorhighlightcolor-line {
|
||||
left: 30px;
|
||||
}
|
||||
|
||||
.bl-image--line {
|
||||
left: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
@include box(80px, 80px);
|
||||
@include themeBrightness();
|
||||
@include themeShadow(2px 4px 7px 2px rgba(134, 134, 134, 0.3), 2px 4px 7px 2px #000000);
|
||||
@include themeBorder(2px, #a8abb2, #707070);
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<div class="userlist-setting-root">
|
||||
<div class="info-title">
|
||||
<div class="iconbl bl-user-line"></div>
|
||||
用户管理
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<bl-row class="stat"
|
||||
>共
|
||||
<div>{{ users.length }}</div>
|
||||
名用户,管理员
|
||||
<div class="admin">{{ userStatComputed.admin }}</div>
|
||||
名,普通用户
|
||||
<div class="normal">{{ userStatComputed.normal }}</div>
|
||||
名,只读用户
|
||||
<div class="read">{{ userStatComputed.read }}</div>
|
||||
名。</bl-row
|
||||
>
|
||||
<bl-row class="search" just="space-between">
|
||||
<div>
|
||||
<el-input size="default" placeholder="搜索用户名或昵称" style="width: 200px" v-model="userSearch"></el-input>
|
||||
<el-select size="default" placeholder="用户名类型" style="width: 120px; margin-left: 10px" v-model="userTypeSearch" clearable>
|
||||
<el-option :value="1" label="管理员" />
|
||||
<el-option :value="2" label="普通用户" />
|
||||
<el-option :value="3" label="只读用户" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-button size="default" class="iconbl bl-refresh-line" @click="getUserList"></el-button>
|
||||
</bl-row>
|
||||
<div class="user-container">
|
||||
<div :class="['user-item', user.delTime !== '0' ? 'warn-bg-zebra' : '']" v-for="user in usersComputed" @click="showDetailDialog(user.id)">
|
||||
<img v-if="user.avatar != ''" class="avatar" :src="user.avatar" />
|
||||
<img v-else class="avatar" src="@renderer/assets/imgs/default_user_avatar.jpg" />
|
||||
<div class="nickname">{{ user.nickName }}</div>
|
||||
<div class="username">{{ user.username }}</div>
|
||||
<bl-row class="time" just="space-between">{{ user.creTime }}</bl-row>
|
||||
<div class="id">{{ user.id }}</div>
|
||||
<div :class="['type', user.type === 1 ? 'admin' : user.type === 2 ? 'normal' : 'read']">
|
||||
<span v-if="user.type === 1" class="iconbl bl-a-Securitypermissions-line"></span
|
||||
>{{ user.type === 1 ? '管理员' : user.type === 2 ? '普通' : '只读' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="isShowDetailDialog"
|
||||
class="bl-dialog-fixed-body"
|
||||
width="400"
|
||||
style="height: 535px"
|
||||
:align-center="true"
|
||||
:append-to-body="true"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="true">
|
||||
<UserInfoSetting :id="curUserId" @saved="infoSaved"></UserInfoSetting>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { userListApi } from '@renderer/api/auth'
|
||||
import { isNotNull, isNull } from '@renderer/assets/utils/obj'
|
||||
import { computed } from 'vue'
|
||||
import UserInfoSetting from './UserInfoSetting.vue'
|
||||
|
||||
onMounted(() => {
|
||||
getUserList()
|
||||
})
|
||||
|
||||
//#region ----------------------------------------< 用户列表 >--------------------------------------
|
||||
|
||||
type UserList = {
|
||||
id: string
|
||||
avatar: string
|
||||
nickName: string
|
||||
username: string
|
||||
creTime: string
|
||||
delTime: string
|
||||
type: number
|
||||
}
|
||||
const userSearch = ref()
|
||||
const userTypeSearch = ref()
|
||||
const users = ref<UserList[]>([])
|
||||
|
||||
/**
|
||||
* 用户搜索
|
||||
*/
|
||||
const usersComputed = computed(() => {
|
||||
return users.value
|
||||
.filter((user: UserList) => {
|
||||
if (isNull(userSearch.value) && isNull(userTypeSearch.value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
let isName: boolean = true
|
||||
if (isNotNull(userSearch.value)) {
|
||||
isName =
|
||||
user.username.toLowerCase().includes(userSearch.value.toLowerCase()) || user.nickName.toLowerCase().includes(userSearch.value.toLowerCase())
|
||||
}
|
||||
|
||||
let isType: boolean = true
|
||||
if (isNotNull(userTypeSearch.value)) {
|
||||
isType = user.type === userTypeSearch.value
|
||||
}
|
||||
|
||||
if (isName && isType) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
.reverse()
|
||||
})
|
||||
|
||||
/**
|
||||
* 统计用户数
|
||||
*/
|
||||
const userStatComputed = computed(() => {
|
||||
let stat = {
|
||||
admin: 0,
|
||||
normal: 0,
|
||||
read: 0
|
||||
}
|
||||
for (let i = 0; i < users.value.length; i++) {
|
||||
const u = users.value[i]
|
||||
if (u.type === 1) {
|
||||
stat.admin++
|
||||
}
|
||||
if (u.type === 2) {
|
||||
stat.normal++
|
||||
}
|
||||
if (u.type === 3) {
|
||||
stat.read++
|
||||
}
|
||||
}
|
||||
return stat
|
||||
})
|
||||
|
||||
const getUserList = () => {
|
||||
userListApi().then((resp) => {
|
||||
users.value = resp.data
|
||||
})
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region ----------------------------------------< 用户列表 >--------------------------------------
|
||||
const isShowDetailDialog = ref(false)
|
||||
const curUserId = ref<string>()
|
||||
const showDetailDialog = (id: string) => {
|
||||
isShowDetailDialog.value = true
|
||||
curUserId.value = id
|
||||
}
|
||||
const infoSaved = () => {
|
||||
getUserList()
|
||||
isShowDetailDialog.value = false
|
||||
}
|
||||
//#endregion
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@renderer/assets/styles/bl-dialog-info';
|
||||
@import '@renderer/views/index/styles/user-setting';
|
||||
.userlist-setting-root {
|
||||
@include box(100%, 100%);
|
||||
|
||||
.content {
|
||||
@include box(100%, calc(100% - 50px));
|
||||
padding: 10px 10px 20px;
|
||||
|
||||
.stat {
|
||||
height: 25px;
|
||||
font-size: 13px;
|
||||
|
||||
div {
|
||||
padding: 0 3px;
|
||||
margin: 0 2px;
|
||||
font-size: 13px;
|
||||
transition: all 0.3s;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.admin:hover {
|
||||
background-color: $admin-bg;
|
||||
color: $admin-color;
|
||||
}
|
||||
|
||||
.normal:hover {
|
||||
background-color: $normal-bg;
|
||||
color: $normal-color;
|
||||
}
|
||||
|
||||
.read:hover {
|
||||
background-color: $read-bg;
|
||||
color: $read-color;
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
height: 30px;
|
||||
margin: 10px 0 10px 0;
|
||||
}
|
||||
|
||||
.user-container {
|
||||
@include box(100%, calc(100% - 75px));
|
||||
@include flex(row, flex-start, flex-start);
|
||||
align-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: scroll;
|
||||
padding: 10px 5px 10px 10px;
|
||||
|
||||
.user-item {
|
||||
@include font(13px, 300);
|
||||
@include themeBg(#f5f5f5, #252525);
|
||||
width: 150px;
|
||||
margin-bottom: 25px;
|
||||
margin-right: 15px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
transition:
|
||||
box-shadow 0.2s,
|
||||
transform 0.3s;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
@include themeShadow(0 3px 5px 0 rgb(190, 190, 190), 0 3px 5px 0 rgb(20, 20, 20));
|
||||
transform: translateY(-5px);
|
||||
|
||||
.download-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
@include box(50px, 50px);
|
||||
@include themeBrightness();
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
@include font(15px, 300);
|
||||
@include ellipsis();
|
||||
}
|
||||
|
||||
.username {
|
||||
@include font(12px, 300);
|
||||
@include ellipsis();
|
||||
color: var(--bl-text-color-light);
|
||||
}
|
||||
|
||||
.time {
|
||||
@include font(11px, 300);
|
||||
color: var(--bl-text-color-light);
|
||||
}
|
||||
|
||||
.id {
|
||||
@include font(11px, 300);
|
||||
color: var(--bl-text-color-light);
|
||||
position: absolute;
|
||||
top: 35px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.type {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -45,6 +45,11 @@
|
||||
line-height: 30px;
|
||||
margin-bottom: 30px;
|
||||
color: var(--bl-text-color-light);
|
||||
|
||||
.iconbl {
|
||||
@include font(12px, 300);
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
|
@ -0,0 +1,66 @@
|
||||
$admin-bg: #ca9100;
|
||||
$admin-color: var(--bl-html-color);
|
||||
$normal-bg: var(--el-color-primary);
|
||||
$normal-color: var(--bl-html-color);
|
||||
$read-bg: var(--bl-bg-color);
|
||||
$read-color: #939393;
|
||||
|
||||
.bg-zebra {
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-size: 20px 20px;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.warn-bg-zebra {
|
||||
@extend .bg-zebra;
|
||||
|
||||
&:before {
|
||||
background-image: linear-gradient(
|
||||
-45deg,
|
||||
#ca910021 0%,
|
||||
#ca910021 25%,
|
||||
transparent 25%,
|
||||
transparent 50%,
|
||||
#ca910021 50%,
|
||||
#ca910021 75%,
|
||||
transparent 75%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.type {
|
||||
@include font(11px, 500);
|
||||
@include themeShadow(0 2px 5px 0 rgb(190, 190, 190), 0 2px 3px 0 rgb(20, 20, 20));
|
||||
border-radius: 4px;
|
||||
padding: 2px 5px;
|
||||
margin-left: 10px;
|
||||
.iconbl {
|
||||
font-size: 12px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.type.admin {
|
||||
background-color: $admin-bg;
|
||||
color: $admin-color;
|
||||
}
|
||||
|
||||
.type.normal {
|
||||
background-color: $normal-bg;
|
||||
color: $normal-color;
|
||||
}
|
||||
|
||||
.type.read {
|
||||
background-color: $read-bg;
|
||||
color: $read-color;
|
||||
}
|
@ -4,14 +4,13 @@
|
||||
<div class="table-operator">
|
||||
<bl-row class="component-title">资源列表</bl-row>
|
||||
<bl-row style="margin-top: 10px">
|
||||
<!-- 资源搜索框 -->
|
||||
<el-input v-model="dataSearch" placeholder="搜索资源名" />
|
||||
<el-button type="primary" :icon="Refresh" @click="getResource()" style="margin-left: 10px">刷新</el-button>
|
||||
<el-button type="primary" :icon="Refresh" @click="getResource()" style="margin-left: 10px"></el-button>
|
||||
</bl-row>
|
||||
</div>
|
||||
<div class="table">
|
||||
<el-table border height="100%" tooltip-effect="dark" v-loading="tableLoading" :data="filterTableDatas" @row-click="rowClick">
|
||||
<el-table-column sortable prop="resource" min-width="250" :show-overflow-tooltip="true" label="资源名称(24小时内)" />
|
||||
<el-table-column sortable prop="resource" min-width="170" :show-overflow-tooltip="true" label="资源名称(24小时内)" />
|
||||
<el-table-column sortable prop="success" width="80" align="right" label="请求数" />
|
||||
<el-table-column sortable prop="avgRt" width="90" align="right" label="avgRT">
|
||||
<template #default="scope">
|
||||
@ -35,14 +34,14 @@
|
||||
|
||||
<div class="chart-line">
|
||||
<bl-row class="component-title" just="space-between">
|
||||
<div>
|
||||
资源折线图:
|
||||
<div class="title">
|
||||
流量折线图:
|
||||
<span style="font-size: 12px"
|
||||
>查询最近{{ lineSearchParam.interval }}的流量信息,按每{{ lineSearchParam.customInterval }}分钟聚合,资源:
|
||||
{{ lineSearchParam.resource }}</span
|
||||
>
|
||||
</div>
|
||||
<el-button-group>
|
||||
<el-button-group style="width: 195px">
|
||||
<el-button round @click="intervalClick('5m', 1)">5M</el-button>
|
||||
<el-button round @click="intervalClick('10m', 1)">10M</el-button>
|
||||
<el-button round @click="intervalClick('1h', 1)">1H</el-button>
|
||||
@ -127,16 +126,23 @@ const rowClick = (row: ResourceRow) => {
|
||||
padding: 0 30px 30px 0;
|
||||
|
||||
.component-title {
|
||||
width: 100%;
|
||||
color: var(--bl-text-color);
|
||||
border: 1px solid var(--el-border-color);
|
||||
background-color: var(--bl-bg-color);
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
height: 36px;
|
||||
|
||||
.title {
|
||||
width: calc(100% - 195px);
|
||||
overflow: hidden;
|
||||
@include ellipsis();
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
@include box(530px, 100%);
|
||||
@include box(420px, 100%);
|
||||
|
||||
.table-operator {
|
||||
@include box(100%, 80px);
|
||||
@ -163,7 +169,7 @@ const rowClick = (row: ResourceRow) => {
|
||||
}
|
||||
|
||||
.chart-line {
|
||||
@include box(calc(100% - 545px), 100%);
|
||||
@include box(calc(100% - 420px), 100%);
|
||||
max-height: 500px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user