后端新增全局异常处理,封装通用返回类,封装错误处理,前端优化部分代码

master
barney 1 year ago
parent d1058c8cf2
commit f973d2cf77
  1. 58
      user-center-backend/src/main/java/cc/bnblogs/usercenterbackend/common/BaseResponse.java
  2. 49
      user-center-backend/src/main/java/cc/bnblogs/usercenterbackend/common/ErrorCode.java
  3. 41
      user-center-backend/src/main/java/cc/bnblogs/usercenterbackend/common/ResultUtils.java
  4. 108
      user-center-backend/src/main/java/cc/bnblogs/usercenterbackend/controller/UserController.java
  5. 54
      user-center-backend/src/main/java/cc/bnblogs/usercenterbackend/exception/BusinessException.java
  6. 34
      user-center-backend/src/main/java/cc/bnblogs/usercenterbackend/exception/GlobalExceptionHandler.java
  7. 5
      user-center-backend/src/main/java/cc/bnblogs/usercenterbackend/model/request/UserRegisterRequest.java
  8. 10
      user-center-backend/src/main/java/cc/bnblogs/usercenterbackend/service/UserService.java
  9. 62
      user-center-backend/src/main/java/cc/bnblogs/usercenterbackend/service/impl/UserServiceImpl.java
  10. 5
      user-center-backend/src/main/resources/application.yml
  11. 13
      user-center-backend/src/test/java/cc/bnblogs/usercenterbackend/UserCenterBackendApplicationTests.java
  12. 16
      user-center-frontend/config/config.ts
  13. 593
      user-center-frontend/config/oneapi.json
  14. 22
      user-center-frontend/config/routes.ts
  15. 4
      user-center-frontend/src/access.ts
  16. 50
      user-center-frontend/src/app.tsx
  17. 6
      user-center-frontend/src/components/RightContent/AvatarDropdown.tsx
  18. 1
      user-center-frontend/src/constants/index.ts
  19. 40
      user-center-frontend/src/pages/Admin.tsx
  20. 50
      user-center-frontend/src/pages/Admin/UserManage/index.less
  21. 159
      user-center-frontend/src/pages/Admin/UserManage/index.tsx
  22. 410
      user-center-frontend/src/pages/DashboardWorkplace/_mock.ts
  23. 16
      user-center-frontend/src/pages/DashboardWorkplace/components/EditableLinkGroup/index.less
  24. 47
      user-center-frontend/src/pages/DashboardWorkplace/components/EditableLinkGroup/index.tsx
  25. 79
      user-center-frontend/src/pages/DashboardWorkplace/components/Radar/autoHeight.tsx
  26. 219
      user-center-frontend/src/pages/DashboardWorkplace/components/Radar/index.tsx
  27. 111
      user-center-frontend/src/pages/DashboardWorkplace/data.d.ts
  28. 237
      user-center-frontend/src/pages/DashboardWorkplace/index.tsx
  29. 14
      user-center-frontend/src/pages/DashboardWorkplace/service.ts
  30. 250
      user-center-frontend/src/pages/DashboardWorkplace/style.less
  31. 29
      user-center-frontend/src/pages/user/Login/index.tsx
  32. 0
      user-center-frontend/src/pages/user/Login/{SYSTEM_LOGO}.tsx
  33. 50
      user-center-frontend/src/pages/user/Register/index.less
  34. 163
      user-center-frontend/src/pages/user/Register/index.tsx
  35. 59
      user-center-frontend/src/plugins/globalRequest.ts
  36. 36
      user-center-frontend/src/services/ant-design-pro/api.ts
  37. 66
      user-center-frontend/src/services/ant-design-pro/typings.d.ts

@ -0,0 +1,58 @@
package cc.bnblogs.usercenterbackend.common;
import lombok.Data;
import java.io.Serializable;
/**
* @description: 通用返回类
* @author: zfp@bnblogs.cc
* @date: 2023/3/15 14:34
*/
@Data
public class BaseResponse<T> implements Serializable {
private static final long serialVersionUID = 3082850285091730120L;
/**
* 状态码
*/
private int code;
/**
* 返回的实际数据
*/
private T data;
/**
* 状态码描述
*/
private String message;
/**
* 详情一般在错误中使用
*/
private String description;
public BaseResponse(int code, T data, String message,String description) {
this.code = code;
this.data = data;
this.message = message;
this.description = description;
}
public BaseResponse(int code, T data) {
this.code = code;
this.data = data;
this.message = "";
this.description = "";
}
public BaseResponse(int code, T data, String message) {
this.code = code;
this.data = data;
this.message = message;
this.description = "";
}
public BaseResponse(ErrorCode code) {
this(code.getCode(),null,code.getMessage(),code.getDescription());
}
}

@ -0,0 +1,49 @@
package cc.bnblogs.usercenterbackend.common;
/**
* @description: 错误码
* @author: zfp@bnblogs.cc
* @date: 2023/3/15 15:20
*/
public enum ErrorCode {
/**
* 部分业务状态码
*/
SUCCESS(0,"ok",""),
PARAMS_ERROR(40000,"请求参数错误",""),
NULL_ERROR(40001,"返回数据为null",""),
NOT_LOGIN(40100,"用户未登录",""),
NOT_ADMIN(40101,"无权限",""),
SYSTEM_ERROR(50000,"系统内部异常","");
/**
* 状态码
*/
private final int code;
/**
* 状态码描述
*/
private final String message;
/**
* 详情
*/
private final String description;
ErrorCode(int code, String message, String description) {
this.code = code;
this.message = message;
this.description = description;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public String getDescription() {
return description;
}
}

@ -0,0 +1,41 @@
package cc.bnblogs.usercenterbackend.common;
/**
* @description: 返回结果工具类
* @author: zfp@bnblogs.cc
* @date: 2023/3/15 15:03
*/
public class ResultUtils {
/**
* 操作成功
* @param data 返回的实际数据
* @return
* @param <T> 数据的类型
*/
public static <T> BaseResponse<T> success(T data) {
return new BaseResponse<>(0,data,"ok","请求成功");
}
/**
* 操作失败
* @param errorCode 自定义错误码
* @param message 自定义消息
* @param description 自定义详情
* @return
*/
public static BaseResponse error(ErrorCode errorCode,String message,String description) {
return new BaseResponse<>(errorCode.getCode(),null,message,description);
}
/**
* 操作失败
* @param code 错误码
* @param message 信息
* @param description 详情
* @return
*/
public static BaseResponse error(int code, String message, String description) {
return new BaseResponse<>(code,null,message,description);
}
}

@ -1,18 +1,19 @@
package cc.bnblogs.usercenterbackend.controller;
import cc.bnblogs.usercenterbackend.common.BaseResponse;
import cc.bnblogs.usercenterbackend.common.ErrorCode;
import cc.bnblogs.usercenterbackend.common.ResultUtils;
import cc.bnblogs.usercenterbackend.exception.BusinessException;
import cc.bnblogs.usercenterbackend.model.User;
import cc.bnblogs.usercenterbackend.model.request.UserLoginRequest;
import cc.bnblogs.usercenterbackend.model.request.UserRegisterRequest;
import cc.bnblogs.usercenterbackend.service.UserService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@ -26,37 +27,48 @@ import static cc.bnblogs.usercenterbackend.constant.UserConstant.LOGIN_USER;
*/
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Resource
private UserService userService;
/**
* 用户注册
* @param request 用户注册请求体参数
* @return 注册成功的用户id
*/
@PostMapping("/register")
public Long register(@RequestBody UserRegisterRequest request) {
public BaseResponse<Long> register(@RequestBody UserRegisterRequest request) {
if (request == null) {
log.info("请求参数为null");
return null;
throw new BusinessException(ErrorCode.PARAMS_ERROR,"客户端请求为null");
}
String userAccount = request.getUserAccount();
String userPassword = request.getUserPassword();
String checkPassword = request.getCheckPassword();
String planetCode = request.getPlanetCode();
/*
controller层可以对请求参数做一定的校验
*/
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword)) {
log.info("请求的字段中有参数为空");
return null;
if (StringUtils.isAnyBlank(userAccount, userPassword, checkPassword,planetCode)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"请求的字段中有参数为空");
}
return userService.userRegister(userAccount,userPassword,checkPassword);
Long result = userService.userRegister(userAccount,userPassword,checkPassword,planetCode);
return ResultUtils.success(result);
}
/**
* 用户登录
* HttpServletRequest对象代表客户端的请求当客户端通过HTTP协议访问服务器时
* HTTP请求头中的所有信息都封装在这个对象中通过这个对象提供的方法可以获得客户端请求的所有信息
* @param request 用户登录请求体参数
* @param httpServletRequest 客户端请求
* @return 脱敏后的用户信息
*/
@PostMapping("/login")
public User login(@RequestBody UserLoginRequest request, HttpServletRequest httpServletRequest) {
public BaseResponse<User> login(@RequestBody UserLoginRequest request, HttpServletRequest httpServletRequest) {
if (request == null) {
log.info("请求参数为null");
return null;
throw new BusinessException(ErrorCode.PARAMS_ERROR,"客户端请求为null");
}
String userAccount = request.getUserAccount();
String userPassword = request.getUserPassword();
@ -65,36 +77,51 @@ public class UserController {
controller层可以对请求参数做一定的校验
*/
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
log.info("请求的字段中有参数为空");
return null;
throw new BusinessException(ErrorCode.PARAMS_ERROR,"账户名或密码为null或为空");
}
return userService.userLogin(userAccount,userPassword,httpServletRequest);
User user = userService.userLogin(userAccount, userPassword, httpServletRequest);
return ResultUtils.success(user);
}
/**
* 用户注销
* @param request 客户端发起的请求
* @return 注销结果注销成功返回1
*/
@PostMapping("/logout")
public BaseResponse<Integer> login(HttpServletRequest request) {
if (request == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"客户端请求为null");
}
int res = userService.userLogout(request);
return ResultUtils.success(res);
}
/**
* 根据用户名进行模糊查询
* 该接口调用前需要鉴权需要判断用户是否可以调用该接口
* @param username 用户名
* @param request
* @param request 客户端请求
* @return 用户列表
*/
@GetMapping("/search")
public List<User> getUsersByName(String username,
public BaseResponse<List<User>> getUsersByName(String username,
HttpServletRequest request) {
// 仅管理员可使用本接口
if (!isAdmin(request)) {
return new ArrayList<>();
// todo: 这里应该返回空的列表
throw new BusinessException(ErrorCode.NOT_ADMIN,"本接口需要管理员权限");
}
QueryWrapper<User> qw = new QueryWrapper<>();
// 用户名不为空
if (StringUtils.isNotBlank(username)) {
qw.like("username", username);
log.info("username: {}", username);
}
List<User> userList = userService.list(qw);
// 返回脱敏后的用户列表
return userList.stream().map(user -> userService.getSafetyUser(user)).collect(Collectors.toList());
List<User> lists = userList.stream().map(user -> userService.getSafetyUser(user)).collect(Collectors.toList());
return ResultUtils.success(lists);
}
/**
@ -103,23 +130,48 @@ public class UserController {
* @return 删除结果
*/
@DeleteMapping("/delete")
public boolean deleteUserById(@RequestBody Long id,HttpServletRequest request) {
public BaseResponse<Boolean> deleteUserById(@RequestBody Long id,HttpServletRequest request) {
if (!isAdmin(request)) {
return false;
throw new BusinessException(ErrorCode.NOT_ADMIN,"需要管理员权限");
}
if (id <= 0) {
return false;
throw new BusinessException(ErrorCode.PARAMS_ERROR,"删除的用户id不合法");
}
return userService.removeById(id);
/*
注意: 这里是逻辑删除不是物理删除
*/
boolean result = userService.removeById(id);
return ResultUtils.success(result);
}
/**
* 判断是否为管理员
* 返回当前用户的信息
* @param request
* @return
*/
@GetMapping("/current")
public BaseResponse<User> getCurrentUser(HttpServletRequest request) {
Object userObject = request.getSession().getAttribute(LOGIN_USER);
if (userObject == null) {
throw new BusinessException(ErrorCode.NOT_LOGIN,"用户未登录");
}
// 从session中取出用户id,再根据用户id去查询数据库中的用户数据(这样相当于更新了缓存)
User user = (User) userObject;
Long id = user.getId();
// todo: 校验用户是否合法,比如是否被封号了
User originUser = userService.getById(id);
// 返回脱敏后的用户数据
User safetyUser = userService.getSafetyUser(originUser);
return ResultUtils.success(safetyUser);
}
/**
* 判断是否为管理员
* @param request 客户端请求
* @return
*/
public boolean isAdmin(HttpServletRequest request) {
// 从session中获取用户信息
Object userObject = request.getSession().getAttribute(LOGIN_USER);

@ -0,0 +1,54 @@
package cc.bnblogs.usercenterbackend.exception;
import cc.bnblogs.usercenterbackend.common.ErrorCode;
/**
* @description: 自定义异常类相比默认的异常类抛出更多信息同时支持更多自定义构造函数
* @author: zfp@bnblogs.cc
* @date: 2023/3/15 15:44
*/
public class BusinessException extends RuntimeException {
private final int code;
private final String description;
/**
* 状态码和描述信息完全自定义
* @param message
* @param code 状态码
* @param description 详情
*/
public BusinessException(String message, int code, String description) {
super(message);
this.code = code;
this.description = description;
}
/**
* 接收错误码返回异常信息
* @param errorCode
*/
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
this.description = errorCode.getDescription();
}
/**
* 修改错误码默认的详情信息
* @param errorCode 错误业务码
* @param description 详情
*/
public BusinessException(ErrorCode errorCode,String description) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
this.description = description;
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
}

@ -0,0 +1,34 @@
package cc.bnblogs.usercenterbackend.exception;
import cc.bnblogs.usercenterbackend.common.BaseResponse;
import cc.bnblogs.usercenterbackend.common.ErrorCode;
import cc.bnblogs.usercenterbackend.common.ResultUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @description: 全局异常处理器
* @author: zfp@bnblogs.cc
* @date: 2023/3/15 16:15
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 针对我们自定义的异常类BusinessException来处理
* @return
*/
@ExceptionHandler(BusinessException.class)
public BaseResponse businessExceptionHandler(BusinessException e) {
log.error("BusinessException:" + e.getMessage(),e);
return ResultUtils.error(e.getCode(),e.getMessage(),e.getDescription());
}
@ExceptionHandler(RuntimeException.class)
public BaseResponse runtimeExceptionHandler(RuntimeException e) {
log.info("RuntimeException: " + e);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR,e.getMessage(),"系统内部错误");
}
}

@ -27,4 +27,9 @@ public class UserRegisterRequest implements Serializable{
* 二次验证密码
*/
private String checkPassword;
/**
* 星球编号
*/
private String planetCode;
}

@ -16,9 +16,10 @@ public interface UserService extends IService<User> {
* @param userAccount 登录账户名称(不是昵称)
* @param userPassword 用户密码
* @param checkPassword 二次密码
* @param planetCode 星球编号
* @return 新用户id
*/
long userRegister(String userAccount,String userPassword,String checkPassword);
long userRegister(String userAccount,String userPassword,String checkPassword, String planetCode);
/**
* 用户登录
@ -35,4 +36,11 @@ public interface UserService extends IService<User> {
* @return 脱敏的用户信息
*/
User getSafetyUser(User originUser);
/**
* 用户注销
*
* @param request
*/
int userLogout(HttpServletRequest request);
}

@ -1,5 +1,7 @@
package cc.bnblogs.usercenterbackend.service.impl;
import cc.bnblogs.usercenterbackend.common.ErrorCode;
import cc.bnblogs.usercenterbackend.exception.BusinessException;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import cc.bnblogs.usercenterbackend.model.User;
@ -42,31 +44,35 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
private UserMapper userMapper;
@Override
public long userRegister(String userAccount, String userPassword, String checkPassword) {
public long userRegister(String userAccount, String userPassword, String checkPassword,String planetCode) {
// 1.校验字段
// 字段值不能为空
if (StringUtils.isAnyBlank(userAccount,userPassword,checkPassword)) {
return -1;
if (StringUtils.isAnyBlank(userAccount,userPassword,checkPassword,planetCode)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"字段值不能为空");
}
// 账户名不少于4位
if (userAccount.length() < 4) {
return -1;
throw new BusinessException(ErrorCode.PARAMS_ERROR,"账户名不能小于4位");
}
// 账户名只能使用字母数字下划线,且必须字母开头
Matcher matcher = Pattern.compile(ACCOUNT_VALID).matcher(userAccount);
if (!matcher.find()) {
return -1;
throw new BusinessException(ErrorCode.PARAMS_ERROR,"账户名要以英文字母开头");
}
// 星球编号不能过长
if (planetCode.length() > 8) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"星球编号不超过8位");
}
// 密码不少于8位
if (userPassword.length() < 8) {
return -1;
if (userPassword.length() < 8 || checkPassword.length() < 8) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"密码不能少于8位");
}
// 两次输入的密码不一致
if (!userPassword.equals(checkPassword)) {
return -1;
throw new BusinessException(ErrorCode.PARAMS_ERROR,"两次输入的密码不一致");
}
// 账户不能重复, 涉及数据库操作的检验放在靠后的地方
@ -74,7 +80,15 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
qw.eq("userAccount",userAccount);
long count = userMapper.selectCount(qw);
if (count > 0) {
return -1;
throw new BusinessException(ErrorCode.PARAMS_ERROR,"账户名不能重复");
}
// 星球编号不能重复
qw = new QueryWrapper<>();
qw.eq("planetCode",planetCode);
count = userMapper.selectCount(qw);
if (count > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR,"星球编号已存在");
}
//2.对原始密码进行加密
@ -84,7 +98,10 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
//3.存入数据库中
User user = new User();
user.setUserAccount(userAccount);
// 测试阶段: 设置用户名和账户名称相同
user.setUsername(userAccount);
user.setUserPassword(encryptPassword);
user.setPlanetCode(planetCode);
int saveResult = userMapper.insert(user);
// 插入数据失败
if (saveResult == 0) {
@ -98,26 +115,22 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
// 1.校验字段
// 字段值不能为空
if (StringUtils.isAnyBlank(userAccount,userPassword)) {
log.info("有字段值为空");
return null;
throw new BusinessException(ErrorCode.PARAMS_ERROR,"字段值不能为空");
}
// 账户名不少于4位
if (userAccount.length() < 4) {
log.info("账户名小于4位");
return null;
throw new BusinessException(ErrorCode.PARAMS_ERROR,"账户名不能小于4位");
}
// 账户名只能使用字母数字下划线,且必须字母开头
Matcher matcher = Pattern.compile(ACCOUNT_VALID).matcher(userAccount);
if (!matcher.find()) {
log.info("账户中包含特殊字符");
return null;
throw new BusinessException(ErrorCode.PARAMS_ERROR,"账户名要以英文字母开头");
}
// 密码不少于8位
if (userPassword.length() < 8) {
log.info("用户密码过短");
return null;
throw new BusinessException(ErrorCode.PARAMS_ERROR,"密码不能少于8位");
}
// 2.密码加密
@ -132,8 +145,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
// 用户名或密码不匹配
if (user == null) {
log.info("用户名或者密码不匹配");
return null;
throw new BusinessException(ErrorCode.NULL_ERROR,"用户名或者密码不匹配");
}
// 3.用户数据脱敏
@ -154,6 +166,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
*/
@Override
public User getSafetyUser(User originUser) {
if (originUser == null) {
return null;
}
User safetyUser = new User();
safetyUser.setId(originUser.getId());
safetyUser.setUsername(originUser.getUsername());
@ -165,9 +180,18 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
safetyUser.setUserStatus(originUser.getUserStatus());
safetyUser.setCreateTime(originUser.getCreateTime());
safetyUser.setUserRole(originUser.getUserRole());
safetyUser.setPlanetCode(originUser.getPlanetCode());
return safetyUser;
}
@Override
public int userLogout(HttpServletRequest request) {
// 移除session中的登录态
request.getSession().removeAttribute(LOGIN_USER);
log.info("注销成功!");
return 1;
}
}

@ -23,8 +23,13 @@ mybatis-plus:
# 关闭自动驼峰命名规则(camel case)映射
# 这样sql语句就不会自动将驼峰式变量转为下滑线式变量
map-underscore-to-camel-case: false
# 显示sql日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除的实体字段名: isDelete
logic-delete-value: 1 # 已逻辑删除设置该字段为1
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

@ -28,35 +28,36 @@ class UserCenterBackendApplicationTests {
String userAccount = "aaaa";
String userPassword = "12345678";
String checkPassword = "";
String planetCode = "22";
// 测试字段为空
long result = userService.userRegister(userAccount, userPassword, checkPassword);
long result = userService.userRegister(userAccount, userPassword, checkPassword,planetCode);
Assertions.assertEquals(-1, result);
// 测试账户名长度
userAccount = "aaa";
result = userService.userRegister(userAccount, userPassword, checkPassword);
result = userService.userRegister(userAccount, userPassword, checkPassword,planetCode);
Assertions.assertEquals(-1, result);
// 测试账户名是否包含特殊字符
userAccount = "hjsdu$ ";
result = userService.userRegister(userAccount, userPassword, checkPassword);
result = userService.userRegister(userAccount, userPassword, checkPassword,planetCode);
Assertions.assertEquals(-1, result);
// 测试密码不少于8位
userAccount = "aaaa";
userPassword = "123456";
result = userService.userRegister(userAccount, userPassword, checkPassword);
result = userService.userRegister(userAccount, userPassword, checkPassword,planetCode);
Assertions.assertEquals(-1, result);
// 测试两次密码输入是否一致
userPassword = "12345678";
checkPassword = "123456789";
result = userService.userRegister(userAccount, userPassword, checkPassword);
result = userService.userRegister(userAccount, userPassword, checkPassword,planetCode);
Assertions.assertEquals(-1, result);
// 正确插入
userPassword = "12345678";
checkPassword = "12345678";
result = userService.userRegister(userAccount, userPassword, checkPassword);
result = userService.userRegister(userAccount, userPassword, checkPassword,planetCode);
Assertions.assertTrue(result > 0);

@ -3,6 +3,7 @@ import { defineConfig } from 'umi';
import defaultSettings from './defaultSettings';
import proxy from './proxy';
import routes from './routes';
import {join} from "path";
const { REACT_APP_ENV } = process.env;
export default defineConfig({
hash: true,
@ -22,6 +23,21 @@ export default defineConfig({
targets: {
ie: 11,
},
openAPI: [
{
requestLibPath: "import { request } from 'umi'",
// 或者使用在线的版本
// schemaPath: "https://gw.alipayobjects.com/os/antfincdn/M%24jrzTTYJN/oneapi.json"
schemaPath: join(__dirname, 'oneapi.json'),
projectName: 'oneapi', // 增加这一行
mock: false,
},
{
requestLibPath: "import { request } from 'umi'",
schemaPath: 'https://gw.alipayobjects.com/os/antfincdn/CA1dOm%2631B/openapi.json',
projectName: 'swagger',
},
],
// umi routes: https://umijs.org/docs/routing
routes,
access: {},

@ -0,0 +1,593 @@
{
"openapi": "3.0.1",
"info": {
"title": "Ant Design Pro",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:8000/"
},
{
"url": "https://localhost:8000/"
}
],
"paths": {
"/api/currentUser": {
"get": {
"tags": ["api"],
"description": "获取当前的用户",
"operationId": "currentUser",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CurrentUser"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"x-swagger-router-controller": "api"
},
"/api/login/captcha": {
"post": {
"description": "发送验证码",
"operationId": "getFakeCaptcha",
"tags": ["login"],
"parameters": [
{
"name": "phone",
"in": "query",
"description": "手机号",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FakeCaptcha"
}
}
}
}
}
}
},
"/api/login/outLogin": {
"post": {
"description": "登录接口",
"operationId": "outLogin",
"tags": ["login"],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"x-swagger-router-controller": "api"
},
"/api/login/account": {
"post": {
"tags": ["login"],
"description": "登录接口",
"operationId": "login",
"requestBody": {
"description": "登录系统",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoginParams"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoginResult"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
},
"x-codegen-request-body-name": "body"
},
"x-swagger-router-controller": "api"
},
"/api/notices": {
"summary": "getNotices",
"description": "NoticeIconItem",
"get": {
"tags": ["api"],
"operationId": "getNotices",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NoticeIconList"
}
}
}
}
}
}
},
"/api/rule": {
"get": {
"tags": ["rule"],
"description": "获取规则列表",
"operationId": "rule",
"parameters": [
{
"name": "current",
"in": "query",
"description": "当前的页码",
"schema": {
"type": "number"
}
},
{
"name": "pageSize",
"in": "query",
"description": "页面的容量",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RuleList"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"post": {
"tags": ["rule"],
"description": "新建规则",
"operationId": "addRule",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RuleListItem"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"put": {
"tags": ["rule"],
"description": "新建规则",
"operationId": "updateRule",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RuleListItem"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"delete": {
"tags": ["rule"],
"description": "删除规则",
"operationId": "removeRule",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"x-swagger-router-controller": "api"
},
"/swagger": {
"x-swagger-pipe": "swagger_raw"
}
},
"components": {
"schemas": {
"CurrentUser": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"avatar": {
"type": "string"
},
"userid": {
"type": "string"
},
"email": {
"type": "string"
},
"signature": {
"type": "string"
},
"title": {
"type": "string"
},
"group": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"label": {
"type": "string"
}
}
}
},
"notifyCount": {
"type": "integer",
"format": "int32"
},
"unreadCount": {
"type": "integer",
"format": "int32"
},
"country": {
"type": "string"
},
"access": {
"type": "string"
},
"geographic": {
"type": "object",
"properties": {
"province": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"key": {
"type": "string"
}
}
},
"city": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"key": {
"type": "string"
}
}
}
}
},
"address": {
"type": "string"
},
"phone": {
"type": "string"
}
}
},
"LoginResult": {
"type": "object",
"properties": {
"status": {
"type": "string"
},
"type": {
"type": "string"
},
"currentAuthority": {
"type": "string"
}
}
},
"PageParams": {
"type": "object",
"properties": {
"current": {
"type": "number"
},
"pageSize": {
"type": "number"
}
}
},
"RuleListItem": {
"type": "object",
"properties": {
"key": {
"type": "integer",
"format": "int32"
},
"disabled": {
"type": "boolean"
},
"href": {
"type": "string"
},
"avatar": {
"type": "string"
},
"name": {
"type": "string"
},
"owner": {
"type": "string"
},
"desc": {
"type": "string"
},
"callNo": {
"type": "integer",
"format": "int32"
},
"status": {
"type": "integer",
"format": "int32"
},
"updatedAt": {
"type": "string",
"format": "datetime"
},
"createdAt": {
"type": "string",
"format": "datetime"
},
"progress": {
"type": "integer",
"format": "int32"
}
}
},
"RuleList": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RuleListItem"
}
},
"total": {
"type": "integer",
"description": "列表的内容总数",
"format": "int32"
},
"success": {
"type": "boolean"
}
}
},
"FakeCaptcha": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"status": {
"type": "string"
}
}
},
"LoginParams": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"autoLogin": {
"type": "boolean"
},
"type": {
"type": "string"
}
}
},
"ErrorResponse": {
"required": ["errorCode"],
"type": "object",
"properties": {
"errorCode": {
"type": "string",
"description": "业务约定的错误码"
},
"errorMessage": {
"type": "string",
"description": "业务上的错误信息"
},
"success": {
"type": "boolean",
"description": "业务上的请求是否成功"
}
}
},
"NoticeIconList": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NoticeIconItem"
}
},
"total": {
"type": "integer",
"description": "列表的内容总数",
"format": "int32"
},
"success": {
"type": "boolean"
}
}
},
"NoticeIconItemType": {
"title": "NoticeIconItemType",
"description": "已读未读列表的枚举",
"type": "string",
"properties": {},
"enum": ["notification", "message", "event"]
},
"NoticeIconItem": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"extra": {
"type": "string",
"format": "any"
},
"key": { "type": "string" },
"read": {
"type": "boolean"
},
"avatar": {
"type": "string"
},
"title": {
"type": "string"
},
"status": {
"type": "string"
},
"datetime": {
"type": "string",
"format": "date"
},
"description": {
"type": "string"
},
"type": {
"extensions": {
"x-is-enum": true
},
"$ref": "#/components/schemas/NoticeIconItemType"
}
}
}
}
}
}

@ -4,9 +4,15 @@ export default [
layout: false,
routes: [
{
name: '登录',
path: '/user/login',
component: './user/Login',
},
{
name: '注册',
path: '/user/register',
component: './user/Register',
},
{
component: './404',
},
@ -14,18 +20,21 @@ export default [
},
{
path: '/welcome',
name: '欢迎',
icon: 'smile',
component: './Welcome',
},
{
path: '/admin',
icon: 'crown',
name: '管理页',
access: 'canAdmin',
routes: [
{
path: '/admin/sub-page',
name: '用户管理',
path: '/admin/user-manage',
icon: 'smile',
component: './Welcome',
component: './Admin/UserManage',
},
{
component: './404',
@ -33,6 +42,7 @@ export default [
],
},
{
name: '查询表格',
icon: 'table',
path: '/list',
component: './TableList',
@ -41,13 +51,7 @@ export default [
path: '/',
redirect: '/welcome',
},
{
name: '工作台',
icon: 'smile',
path: '/dashboardworkplace',
component: './DashboardWorkplace',
},
{
component: './404',
},
}
];

@ -1,9 +1,11 @@
import {ADMIN_USER} from "@/constants";
/**
* @see https://umijs.org/zh-CN/plugins/plugin-access
* */
export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {
const { currentUser } = initialState ?? {};
return {
canAdmin: currentUser && currentUser.access === 'admin',
canAdmin: currentUser && currentUser.userRole === ADMIN_USER,
};
}

@ -1,32 +1,39 @@
import Footer from '@/components/Footer';
import RightContent from '@/components/RightContent';
import { BookOutlined, LinkOutlined } from '@ant-design/icons';
import type { Settings as LayoutSettings } from '@ant-design/pro-components';
import { PageLoading, SettingDrawer } from '@ant-design/pro-components';
import type { RunTimeLayoutConfig } from 'umi';
import { history, Link } from 'umi';
import {BookOutlined, LinkOutlined} from '@ant-design/icons';
import type {Settings as LayoutSettings} from '@ant-design/pro-components';
import {PageLoading, SettingDrawer} from '@ant-design/pro-components';
import type {RunTimeLayoutConfig} from 'umi';
import {history, Link} from 'umi';
import defaultSettings from '../config/defaultSettings';
import { currentUser as queryCurrentUser } from './services/ant-design-pro/api';
import {RequestConfig} from "@@/plugin-request/request";
import {currentUser as queryCurrentUser} from './services/ant-design-pro/api';
import type {RequestConfig} from "@@/plugin-request/request";
const isDev = process.env.NODE_ENV === 'development';
const loginPath = '/user/login';
/**
* 访
*/
const NO_NEED_LOGIN_WHITE_LIST = ['/user/register',loginPath];
/** 获取用户信息比较慢的时候会展示一个 loading */
export const initialStateConfig = {
loading: <PageLoading />,
};
/***
*
*/
export const request: RequestConfig = {
// timeout: 1000
}
timeout: 1000000,
};
/**
* @see https://umijs.org/zh-CN/plugins/plugin-initial-state
* */
/*
getInitialState: 返回一些全局变量
*/
export async function getInitialState(): Promise<{
settings?: Partial<LayoutSettings>;
currentUser?: API.CurrentUser;
@ -35,15 +42,15 @@ export async function getInitialState(): Promise<{
}> {
const fetchUserInfo = async () => {
try {
const msg = await queryCurrentUser();
return msg.data;
return await queryCurrentUser();
} catch (error) {
// 如果没有获取到用户信息,会自动跳转到登录页面
history.push(loginPath);
}
return undefined;
};
// 如果不是登录页面,执行
if (history.location.pathname !== loginPath) {
// 如果不是登录或者注册页面(说明已经成功登录),获取当前用户信息
if (!NO_NEED_LOGIN_WHITE_LIST.includes(history.location.pathname)) {
const currentUser = await fetchUserInfo();
return {
fetchUserInfo,
@ -63,13 +70,20 @@ export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) =
rightContentRender: () => <RightContent />,
disableContentMargin: false,
waterMarkProps: {
content: initialState?.currentUser?.name,
content: initialState?.currentUser?.avatarUrl,
},
footerRender: () => <Footer />,
onPageChange: () => {
const { location } = history;
// 如果没有登录,重定向到 login
if (!initialState?.currentUser && location.pathname !== loginPath) {
// 修改一下页面跳转逻辑
// 该为在白名单中的链接可以直接跳转
// 注册页面不用拦截,直接返回
if (NO_NEED_LOGIN_WHITE_LIST.includes(location.pathname)) {
return;
}
if (!initialState?.currentUser) {
history.push(loginPath);
}
},

@ -65,7 +65,7 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
const { currentUser } = initialState;
if (!currentUser || !currentUser.name) {
if (!currentUser || !currentUser.username) {
return loading;
}
@ -101,8 +101,8 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
return (
<HeaderDropdown overlay={menuHeaderDropdown}>
<span className={`${styles.action} ${styles.account}`}>
<Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" />
<span className={`${styles.name} anticon`}>{currentUser.name}</span>
<Avatar size="small" className={styles.avatar} src={currentUser.avatarUrl} alt="avatarUrl" />
<span className={`${styles.name} anticon`}>{currentUser.username}</span>
</span>
</HeaderDropdown>
);

@ -1 +1,2 @@
export const SYSTEM_LOGO = "https://pic.code-nav.cn/user_avatar/1610518142000300034/YeedIoq3-logo.png";
export const ADMIN_USER = 1;

@ -1,42 +1,10 @@
import { HeartTwoTone, SmileTwoTone } from '@ant-design/icons';
import { PageHeaderWrapper } from '@ant-design/pro-components';
import { Alert, Card, Typography } from 'antd';
import React from 'react';
const Admin: React.FC = () => {
const Admin: React.FC = (props) => {
const { children } = props;
return (
<PageHeaderWrapper content={' 这个页面只有 admin 权限才能查看'}>
<Card>
<Alert
message={'更快更强的重型组件,已经发布。'}
type="success"
showIcon
banner
style={{
margin: -12,
marginBottom: 48,
}}
/>
<Typography.Title
level={2}
style={{
textAlign: 'center',
}}
>
<SmileTwoTone /> Ant Design Pro <HeartTwoTone twoToneColor="#eb2f96" /> You
</Typography.Title>
</Card>
<p
style={{
textAlign: 'center',
marginTop: 24,
}}
>
Want to add more pages? Please refer to{' '}
<a href="https://pro.ant.design/docs/block-cn" target="_blank" rel="noopener noreferrer">
use block
</a>
</p>
<PageHeaderWrapper>
{children}
</PageHeaderWrapper>
);
};

@ -0,0 +1,50 @@
@import (reference) '~antd/es/style/themes/index';
.container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
background: @layout-body-background;
}
.lang {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
:global(.ant-dropdown-trigger) {
margin-right: 24px;
}
}
.content {
flex: 1;
padding: 32px 0;
}
@media (min-width: @screen-md-min) {
.container {
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
.content {
padding: 32px 0 24px;
}
}
.icon {
margin-left: 8px;
color: rgba(0, 0, 0, 0.2);
font-size: 24px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: @primary-color;
}
}

@ -0,0 +1,159 @@
import React, { useRef } from 'react';
import type { ProColumns, ActionType } from '@ant-design/pro-table';
import ProTable, { TableDropdown } from '@ant-design/pro-table';
import { searchUsers } from "@/services/ant-design-pro/api";
import {Image} from "antd";
const columns: ProColumns<API.CurrentUser>[] = [
{
dataIndex: 'id',
valueType: 'indexBorder',
width: 48,
},
{
title: '用户名',
dataIndex: 'username',
copyable: true,
},
{
title: '用户账户',
dataIndex: 'userAccount',
copyable: true,
},
{
title: '头像',
dataIndex: 'avatarUrl',
render: (_, record) => (
<div>
<Image src={record.avatarUrl} width={100} />
</div>
),
},
{
title: '性别',
dataIndex: 'gender',
valueType: 'select',
valueEnum: {
0: { text: '女', status: 'Error'},
1: {
text: '男',
status: 'Success',
},
2: { text: '保密', status: 'Default'}
},
},
{
title: '电话',
dataIndex: 'phone',
copyable: true,
},
{
title: '邮件',
dataIndex: 'email',
copyable: true,
},
{
title: '状态',
dataIndex: 'userStatus',
valueType: 'select',
valueEnum: {
0: { text: '正常', status: 'Success'},
1: {
text: '其他',
status: 'Info',
},
},
},
{
title: '星球编号',
dataIndex: 'planetCode',
},
{
title: '角色',
dataIndex: 'userRole',
valueType: 'select',
valueEnum: {
0: { text: '普通用户', status: 'Default' },
1: {
text: '管理员',
status: 'Success',
},
},
},
{
title: '创建时间',
dataIndex: 'createTime',
valueType: 'dateTime',
},
{
title: '操作',
valueType: 'option',
render: (text, record, _, action) => [
<a
key="editable"
onClick={() => {
action?.startEditable?.(record.id);
}}
>
</a>,
<a href={record.url} target="_blank" rel="noopener noreferrer" key="view">
</a>,
<TableDropdown
key="actionGroup"
onSelect={() => action?.reload()}
menus={[
{ key: 'copy', name: '复制' },
{ key: 'delete', name: '删除' },
]}
/>,
],
},
];
export default () => {
const actionRef = useRef<ActionType>();
return (
<ProTable<API.CurrentUser>
columns={columns}
actionRef={actionRef}
cardBordered
request={async (params = {}, sort, filter) => {
console.log(sort, filter);
const userList = await searchUsers();
return {
data: userList
}
}}
editable={{
type: 'multiple',
}}
columnsState={{
persistenceKey: 'pro-table-singe-demos',
persistenceType: 'localStorage',
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
form={{
// 由于配置了 transform,提交的参与与定义的不同这里需要转化一下
syncToUrl: (values, type) => {
if (type === 'get') {
return {
...values,
created_at: [values.startTime, values.endTime],
};
}
return values;
},
}}
pagination={{
pageSize: 5,
}}
dateFormatter="string"
headerTitle="用户列表"
/>
);
};

@ -1,410 +0,0 @@
import moment from 'moment';
import type { Request, Response } from 'express';
import type { SearchDataType, OfflineDataType, DataItem } from './data.d';
// mock data
const visitData: DataItem[] = [];
const beginDay = new Date().getTime();
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
for (let i = 0; i < fakeY.length; i += 1) {
visitData.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: fakeY[i],
});
}
const visitData2: DataItem[] = [];
const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
for (let i = 0; i < fakeY2.length; i += 1) {
visitData2.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
y: fakeY2[i],
});
}
const salesData: DataItem[] = [];
for (let i = 0; i < 12; i += 1) {
salesData.push({
x: `${i + 1}`,
y: Math.floor(Math.random() * 1000) + 200,
});
}
const searchData: SearchDataType[] = [];
for (let i = 0; i < 50; i += 1) {
searchData.push({
index: i + 1,
keyword: `搜索关键词-${i}`,
count: Math.floor(Math.random() * 1000),
range: Math.floor(Math.random() * 100),
status: Math.floor((Math.random() * 10) % 2),
});
}
const salesTypeData = [
{
x: '家用电器',
y: 4544,
},
{
x: '食用酒水',
y: 3321,
},
{
x: '个护健康',
y: 3113,
},
{
x: '服饰箱包',
y: 2341,
},
{
x: '母婴产品',
y: 1231,
},
{
x: '其他',
y: 1231,
},
];
const salesTypeDataOnline = [
{
x: '家用电器',
y: 244,
},
{
x: '食用酒水',
y: 321,
},
{
x: '个护健康',
y: 311,
},
{
x: '服饰箱包',
y: 41,
},
{
x: '母婴产品',
y: 121,
},
{
x: '其他',
y: 111,
},
];
const salesTypeDataOffline = [
{
x: '家用电器',
y: 99,
},
{
x: '食用酒水',
y: 188,
},
{
x: '个护健康',
y: 344,
},
{
x: '服饰箱包',
y: 255,
},
{
x: '其他',
y: 65,
},
];
const offlineData: OfflineDataType[] = [];
for (let i = 0; i < 10; i += 1) {
offlineData.push({
name: `Stores ${i}`,
cvr: Math.ceil(Math.random() * 9) / 10,
});
}
const offlineChartData: DataItem[] = [];
for (let i = 0; i < 20; i += 1) {
offlineChartData.push({
x: new Date().getTime() + 1000 * 60 * 30 * i,
y1: Math.floor(Math.random() * 100) + 10,
y2: Math.floor(Math.random() * 100) + 10,
});
}
const titles = [
'Alipay',
'Angular',
'Ant Design',
'Ant Design Pro',
'Bootstrap',
'React',
'Vue',
'Webpack',
];
const avatars = [
'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
];
const avatars2 = [
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png',
'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png',
'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png',
'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png',
'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png',
'https://gw.alipayobjects.com/zos/rmsportal/psOgztMplJMGpVEqfcgF.png',
'https://gw.alipayobjects.com/zos/rmsportal/ZpBqSxLxVEXfcUNoPKrz.png',
'https://gw.alipayobjects.com/zos/rmsportal/laiEnJdGHVOhJrUShBaJ.png',
'https://gw.alipayobjects.com/zos/rmsportal/UrQsqscbKEpNuJcvBZBu.png',
];
const getNotice = (_: Request, res: Response) => {
res.json({
data: [
{
id: 'xxx1',
title: titles[0],
logo: avatars[0],
description: '那是一种内在的东西,他们到达不了,也无法触及的',
updatedAt: new Date(),
member: '科学搬砖组',
href: '',
memberLink: '',
},
{
id: 'xxx2',
title: titles[1],
logo: avatars[1],
description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
updatedAt: new Date('2017-07-24'),
member: '全组都是吴彦祖',
href: '',
memberLink: '',
},
{
id: 'xxx3',
title: titles[2],
logo: avatars[2],
description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
updatedAt: new Date(),
member: '中二少女团',
href: '',
memberLink: '',
},
{
id: 'xxx4',
title: titles[3],
logo: avatars[3],
description: '那时候我只会想自己想要什么,从不想自己拥有什么',
updatedAt: new Date('2017-07-23'),
member: '程序员日常',
href: '',
memberLink: '',
},
{
id: 'xxx5',
title: titles[4],
logo: avatars[4],
description: '凛冬将至',
updatedAt: new Date('2017-07-23'),
member: '高逼格设计天团',
href: '',
memberLink: '',
},
{
id: 'xxx6',
title: titles[5],
logo: avatars[5],
description: '生命就像一盒巧克力,结果往往出人意料',
updatedAt: new Date('2017-07-23'),
member: '骗你来学计算机',
href: '',
memberLink: '',
},
],
});
};
const getActivities = (_: Request, res: Response) => {
res.json({
data: [
{
id: 'trend-1',
updatedAt: new Date(),
user: {
name: '曲丽丽',
avatar: avatars2[0],
},
group: {
name: '高逼格设计天团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-2',
updatedAt: new Date(),
user: {
name: '付小小',
avatar: avatars2[1],
},
group: {
name: '高逼格设计天团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-3',
updatedAt: new Date(),
user: {
name: '林东东',
avatar: avatars2[2],
},
group: {
name: '中二少女团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-4',
updatedAt: new Date(),
user: {
name: '周星星',
avatar: avatars2[4],
},
project: {
name: '5 月日常迭代',
link: 'http://github.com/',
},
template: '将 @{project} 更新至已发布状态',
},
{
id: 'trend-5',
updatedAt: new Date(),
user: {
name: '朱偏右',
avatar: avatars2[3],
},
project: {
name: '工程效能',
link: 'http://github.com/',
},
comment: {
name: '留言',
link: 'http://github.com/',
},
template: '在 @{project} 发布了 @{comment}',
},
{
id: 'trend-6',
updatedAt: new Date(),
user: {
name: '乐哥',
avatar: avatars2[5],
},
group: {
name: '程序员日常',
link: 'http://github.com/',
},
project: {
name: '品牌迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
],
});
};
const radarOriginData = [
{
name: '个人',
ref: 10,
koubei: 8,
output: 4,
contribute: 5,
hot: 7,
},
{
name: '团队',
ref: 3,
koubei: 9,
output: 6,
contribute: 3,
hot: 1,
},
{
name: '部门',
ref: 4,
koubei: 1,
output: 6,
contribute: 5,
hot: 7,
},
];
const radarData: any[] = [];
const radarTitleMap = {
ref: '引用',
koubei: '口碑',
output: '产量',
contribute: '贡献',
hot: '热度',
};
radarOriginData.forEach((item) => {
Object.keys(item).forEach((key) => {
if (key !== 'name') {
radarData.push({
name: item.name,
label: radarTitleMap[key],
value: item[key],
});
}
});
});
const getChartData = (_: Request, res: Response) => {
res.json({
data: {
visitData,
visitData2,
salesData,
searchData,
offlineData,
offlineChartData,
salesTypeData,
salesTypeDataOnline,
salesTypeDataOffline,
radarData,
},
});
};
export default {
'GET /api/project/notice': getNotice,
'GET /api/activities': getActivities,
'GET /api/fake_workplace_chart_data': getChartData,
};

@ -1,16 +0,0 @@
@import '~antd/es/style/themes/default.less';
.linkGroup {
padding: 20px 0 8px 24px;
font-size: 0;
& > a {
display: inline-block;
width: 25%;
margin-bottom: 13px;
color: @text-color;
font-size: @font-size-base;
&:hover {
color: @primary-color;
}
}
}

@ -1,47 +0,0 @@
import React, { createElement } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import styles from './index.less';
export type EditableLink = {
title: string;
href: string;
id?: string;
};
type EditableLinkGroupProps = {
onAdd: () => void;
links: EditableLink[];
linkElement: any;
};
const EditableLinkGroup: React.FC<EditableLinkGroupProps> = (props) => {
const { links, linkElement, onAdd } = props;
return (
<div className={styles.linkGroup}>
{links.map((link) =>
createElement(
linkElement,
{
key: `linkGroup-item-${link.id || link.title}`,
to: link.href,
href: link.href,
},
link.title,
),
)}
<Button size="small" type="primary" ghost onClick={onAdd}>
<PlusOutlined />
</Button>
</div>
);
};
EditableLinkGroup.defaultProps = {
links: [],
onAdd: () => {},
linkElement: 'a',
};
export default EditableLinkGroup;

@ -1,79 +0,0 @@
import React from 'react';
export type IReactComponent<P = any> =
| React.StatelessComponent<P>
| React.ComponentClass<P>
| React.ClassicComponentClass<P>;
function computeHeight(node: HTMLDivElement) {
const { style } = node;
style.height = '100%';
const totalHeight = parseInt(`${getComputedStyle(node).height}`, 10);
const padding =
parseInt(`${getComputedStyle(node).paddingTop}`, 10) +
parseInt(`${getComputedStyle(node).paddingBottom}`, 10);
return totalHeight - padding;
}
function getAutoHeight(n: HTMLDivElement | undefined) {
if (!n) {
return 0;
}
const node = n;
let height = computeHeight(node);
const parentNode = node.parentNode as HTMLDivElement;
if (parentNode) {
height = computeHeight(parentNode);
}
return height;
}
type AutoHeightProps = {
height?: number;
};
function autoHeight() {
return <P extends AutoHeightProps>(
WrappedComponent: React.ComponentClass<P> | React.FC<P>,
): React.ComponentClass<P> => {
class AutoHeightComponent extends React.Component<P & AutoHeightProps> {
state = {
computedHeight: 0,
};
root: HTMLDivElement | undefined = undefined;
componentDidMount() {
const { height } = this.props;
if (!height) {
let h = getAutoHeight(this.root);
this.setState({ computedHeight: h });
if (h < 1) {
h = getAutoHeight(this.root);
this.setState({ computedHeight: h });
}
}
}
handleRoot = (node: HTMLDivElement) => {
this.root = node;
};
render() {
const { height } = this.props;
const { computedHeight } = this.state;
const h = height || computedHeight;
return (
<div ref={this.handleRoot}>
{h > 0 && <WrappedComponent {...this.props} height={h} />}
</div>
);
}
}
return AutoHeightComponent;
};
}
export default autoHeight;

@ -1,219 +0,0 @@
import { Axis, Chart, Coord, Geom, Tooltip } from 'bizcharts';
import { Col, Row } from 'antd';
import React, { Component } from 'react';
import autoHeight from './autoHeight';
import styles from './index.less';
export type RadarProps = {
title?: React.ReactNode;
height?: number;
padding?: [number, number, number, number];
hasLegend?: boolean;
data: {
name: string;
label: string;
value: string | number;
}[];
colors?: string[];
animate?: boolean;
forceFit?: boolean;
tickCount?: number;
style?: React.CSSProperties;
};
type RadarState = {
legendData: {
checked: boolean;
name: string;
color: string;
percent: number;
value: string;
}[];
};
/* eslint react/no-danger:0 */
class Radar extends Component<RadarProps, RadarState> {
state: RadarState = {
legendData: [],
};
chart: G2.Chart | undefined = undefined;
node: HTMLDivElement | undefined = undefined;
componentDidMount() {
this.getLegendData();
}
componentDidUpdate(preProps: RadarProps) {
const { data } = this.props;
if (data !== preProps.data) {
this.getLegendData();
}
}
getG2Instance = (chart: G2.Chart) => {
this.chart = chart;
};
// for custom lengend view
getLegendData = () => {
if (!this.chart) return;
const geom = this.chart.getAllGeoms()[0]; // 获取所有的图形
if (!geom) return;
const items = (geom as any).get('dataArray') || []; // 获取图形对应的
const legendData = items.map((item: { color: any; _origin: any }[]) => {
// eslint-disable-next-line no-underscore-dangle
const origins = item.map((t) => t._origin);
const result = {
name: origins[0].name,
color: item[0].color,
checked: true,
value: origins.reduce((p, n) => p + n.value, 0),
};
return result;
});
this.setState({
legendData,
});
};
handleRef = (n: HTMLDivElement) => {
this.node = n;
};
handleLegendClick = (
item: {
checked: boolean;
name: string;
},
i: string | number,
) => {
const newItem = item;
newItem.checked = !newItem.checked;
const { legendData } = this.state;
legendData[i] = newItem;
const filteredLegendData = legendData.filter((l) => l.checked).map((l) => l.name);
if (this.chart) {
this.chart.filter('name', (val) => filteredLegendData.indexOf(`${val}`) > -1);
this.chart.repaint();
}
this.setState({
legendData,
});
};
render() {
const defaultColors = [
'#1890FF',
'#FACC14',
'#2FC25B',
'#8543E0',
'#F04864',
'#13C2C2',
'#fa8c16',
'#a0d911',
];
const {
data = [],
height = 0,
title,
hasLegend = false,
forceFit = true,
tickCount = 5,
padding = [35, 30, 16, 30] as [number, number, number, number],
animate = true,
colors = defaultColors,
} = this.props;
const { legendData } = this.state;
const scale = {
value: {
min: 0,
tickCount,
},
};
const chartHeight = height - (hasLegend ? 80 : 22);
return (
<div className={styles.radar} style={{ height }}>
{title && <h4>{title}</h4>}
<Chart
scale={scale}
height={chartHeight}
forceFit={forceFit}
data={data}
padding={padding}
animate={animate}
onGetG2Instance={this.getG2Instance}
>
<Tooltip />
<Coord type="polar" />
<Axis
name="label"
line={undefined}
tickLine={undefined}
grid={{
lineStyle: {
lineDash: undefined,
},
hideFirstLine: false,
}}
/>
<Axis
name="value"
grid={{
type: 'polygon',
lineStyle: {
lineDash: undefined,
},
}}
/>
<Geom type="line" position="label*value" color={['name', colors]} size={1} />
<Geom
type="point"
position="label*value"
color={['name', colors]}
shape="circle"
size={3}
/>
</Chart>
{hasLegend && (
<Row className={styles.legend}>
{legendData.map((item, i) => (
<Col
span={24 / legendData.length}
key={item.name}
onClick={() => this.handleLegendClick(item, i)}
>
<div className={styles.legendItem}>
<p>
<span
className={styles.dot}
style={{
backgroundColor: !item.checked ? '#aaa' : item.color,
}}
/>
<span>{item.name}</span>
</p>
<h6>{item.value}</h6>
</div>
</Col>
))}
</Row>
)}
</div>
);
}
}
export default autoHeight()(Radar);

@ -1,111 +0,0 @@
import { DataItem } from '@antv/g2plot/esm/interface/config';
export { DataItem };
export interface TagType {
key: string;
label: string;
}
export type SearchDataType = {
index: number;
keyword: string;
count: number;
range: number;
status: number;
};
export type OfflineDataType = {
name: string;
cvr: number;
};
export interface RadarData {
name: string;
label: string;
value: number;
}
export type AnalysisData = {
visitData: VisitDataType[];
visitData2: VisitDataType[];
salesData: VisitDataType[];
searchData: SearchDataType[];
offlineData: OfflineDataType[];
offlineChartData: OfflineChartData[];
salesTypeData: VisitDataType[];
salesTypeDataOnline: VisitDataType[];
salesTypeDataOffline: VisitDataType[];
radarData: DataItem[];
};
export type GeographicType = {
province: {
label: string;
key: string;
};
city: {
label: string;
key: string;
};
};
export type NoticeType = {
id: string;
title: string;
logo: string;
description: string;
updatedAt: string;
member: string;
href: string;
memberLink: string;
};
export type CurrentUser = {
name: string;
avatar: string;
userid: string;
notice: NoticeType[];
email: string;
signature: string;
title: string;
group: string;
tags: TagType[];
notifyCount: number;
unreadCount: number;
country: string;
geographic: GeographicType;
address: string;
phone: string;
};
export type Member = {
avatar: string;
name: string;
id: string;
};
export type ActivitiesType = {
id: string;
updatedAt: string;
user: {
name: string;
avatar: string;
};
group: {
name: string;
link: string;
};
project: {
name: string;
link: string;
};
template: string;
};
export type RadarDataType = {
label: string;
name: string;
value: number;
};

@ -1,237 +0,0 @@
import type { FC } from 'react';
import { Avatar, Card, Col, List, Skeleton, Row, Statistic } from 'antd';
import { Radar } from '@ant-design/charts';
import { Link, useRequest } from 'umi';
import { PageContainer } from '@ant-design/pro-layout';
import moment from 'moment';
import EditableLinkGroup from './components/EditableLinkGroup';
import styles from './style.less';
import type { ActivitiesType, CurrentUser } from './data.d';
import { queryProjectNotice, queryActivities, fakeChartData } from './service';
const links = [
{
title: '操作一',
href: '',
},
{
title: '操作二',
href: '',
},
{
title: '操作三',
href: '',
},
{
title: '操作四',
href: '',
},
{
title: '操作五',
href: '',
},
{
title: '操作六',
href: '',
},
];
const PageHeaderContent: FC<{ currentUser: Partial<CurrentUser> }> = ({ currentUser }) => {
const loading = currentUser && Object.keys(currentUser).length;
if (!loading) {
return <Skeleton avatar paragraph={{ rows: 1 }} active />;
}
return (
<div className={styles.pageHeaderContent}>
<div className={styles.avatar}>
<Avatar size="large" src={currentUser.avatar} />
</div>
<div className={styles.content}>
<div className={styles.contentTitle}>
{currentUser.name}
</div>
<div>
{currentUser.title} |{currentUser.group}
</div>
</div>
</div>
);
};
const ExtraContent: FC<Record<string, any>> = () => (
<div className={styles.extraContent}>
<div className={styles.statItem}>
<Statistic title="项目数" value={56} />
</div>
<div className={styles.statItem}>
<Statistic title="团队内排名" value={8} suffix="/ 24" />
</div>
<div className={styles.statItem}>
<Statistic title="项目访问" value={2223} />
</div>
</div>
);
const DashboardWorkplace: FC = () => {
const { loading: projectLoading, data: projectNotice = [] } = useRequest(queryProjectNotice);
const { loading: activitiesLoading, data: activities = [] } = useRequest(queryActivities);
const { data } = useRequest(fakeChartData);
const renderActivities = (item: ActivitiesType) => {
const events = item.template.split(/@\{([^{}]*)\}/gi).map((key) => {
if (item[key]) {
return (
<a href={item[key].link} key={item[key].name}>
{item[key].name}
</a>
);
}
return key;
});
return (
<List.Item key={item.id}>
<List.Item.Meta
avatar={<Avatar src={item.user.avatar} />}
title={
<span>
<a className={styles.username}>{item.user.name}</a>
&nbsp;
<span className={styles.event}>{events}</span>
</span>
}
description={
<span className={styles.datetime} title={item.updatedAt}>
{moment(item.updatedAt).fromNow()}
</span>
}
/>
</List.Item>
);
};
return (
<PageContainer
content={
<PageHeaderContent
currentUser={{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
name: '吴彦祖',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
title: '交互专家',
group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
}}
/>
}
extraContent={<ExtraContent />}
>
<Row gutter={24}>
<Col xl={16} lg={24} md={24} sm={24} xs={24}>
<Card
className={styles.projectList}
style={{ marginBottom: 24 }}
title="进行中的项目"
bordered={false}
extra={<Link to="/"></Link>}
loading={projectLoading}
bodyStyle={{ padding: 0 }}
>
{projectNotice.map((item) => (
<Card.Grid className={styles.projectGrid} key={item.id}>
<Card bodyStyle={{ padding: 0 }} bordered={false}>
<Card.Meta
title={
<div className={styles.cardTitle}>
<Avatar size="small" src={item.logo} />
<Link to={item.href}>{item.title}</Link>
</div>
}
description={item.description}
/>
<div className={styles.projectItemContent}>
<Link to={item.memberLink}>{item.member || ''}</Link>
{item.updatedAt && (
<span className={styles.datetime} title={item.updatedAt}>
{moment(item.updatedAt).fromNow()}
</span>
)}
</div>
</Card>
</Card.Grid>
))}
</Card>
<Card
bodyStyle={{ padding: 0 }}
bordered={false}
className={styles.activeCard}
title="动态"
loading={activitiesLoading}
>
<List<ActivitiesType>
loading={activitiesLoading}
renderItem={(item) => renderActivities(item)}
dataSource={activities}
className={styles.activitiesList}
size="large"
/>
</Card>
</Col>
<Col xl={8} lg={24} md={24} sm={24} xs={24}>
<Card
style={{ marginBottom: 24 }}
title="快速开始 / 便捷导航"
bordered={false}
bodyStyle={{ padding: 0 }}
>
<EditableLinkGroup onAdd={() => {}} links={links} linkElement={Link} />
</Card>
<Card
style={{ marginBottom: 24 }}
bordered={false}
title="XX 指数"
loading={data?.radarData?.length === 0}
>
<div className={styles.chart}>
<Radar
height={343}
data={data?.radarData || []}
seriesField="name"
xField="label"
yField="value"
point={{}}
legend={{
position: 'bottom',
}}
/>
</div>
</Card>
<Card
bodyStyle={{ paddingTop: 12, paddingBottom: 12 }}
bordered={false}
title="团队"
loading={projectLoading}
>
<div className={styles.members}>
<Row gutter={48}>
{projectNotice.map((item) => (
<Col span={12} key={`members-item-${item.id}`}>
<Link to={item.href}>
<Avatar src={item.logo} size="small" />
<span className={styles.member}>{item.member}</span>
</Link>
</Col>
))}
</Row>
</div>
</Card>
</Col>
</Row>
</PageContainer>
);
};
export default DashboardWorkplace;

@ -1,14 +0,0 @@
import { request } from 'umi';
import type { NoticeType, ActivitiesType, AnalysisData } from './data';
export async function queryProjectNotice(): Promise<{ data: NoticeType[] }> {
return request('/api/project/notice');
}
export async function queryActivities(): Promise<{ data: ActivitiesType[] }> {
return request('/api/activities');
}
export async function fakeChartData(): Promise<{ data: AnalysisData }> {
return request('/api/fake_workplace_chart_data');
}

@ -1,250 +0,0 @@
@import '~antd/es/style/themes/default.less';
.textOverflow() {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
word-break: break-all;
}
// mixins for clearfix
// ------------------------
.clearfix() {
zoom: 1;
&::before,
&::after {
display: table;
content: ' ';
}
&::after {
clear: both;
height: 0;
font-size: 0;
visibility: hidden;
}
}
.activitiesList {
padding: 0 24px 8px 24px;
.username {
color: @text-color;
}
.event {
font-weight: normal;
}
}
.pageHeaderContent {
display: flex;
.avatar {
flex: 0 1 72px;
& > span {
display: block;
width: 72px;
height: 72px;
border-radius: 72px;
}
}
.content {
position: relative;
top: 4px;
flex: 1 1 auto;
margin-left: 24px;
color: @text-color-secondary;
line-height: 22px;
.contentTitle {
margin-bottom: 12px;
color: @heading-color;
font-weight: 500;
font-size: 20px;
line-height: 28px;
}
}
}
.extraContent {
.clearfix();
float: right;
white-space: nowrap;
.statItem {
position: relative;
display: inline-block;
padding: 0 32px;
> p:first-child {
margin-bottom: 4px;
color: @text-color-secondary;
font-size: @font-size-base;
line-height: 22px;
}
> p {
margin: 0;
color: @heading-color;
font-size: 30px;
line-height: 38px;
> span {
color: @text-color-secondary;
font-size: 20px;
}
}
&::after {
position: absolute;
top: 8px;
right: 0;
width: 1px;
height: 40px;
background-color: @border-color-split;
content: '';
}
&:last-child {
padding-right: 0;
&::after {
display: none;
}
}
}
}
.members {
a {
display: block;
height: 24px;
margin: 12px 0;
color: @text-color;
transition: all 0.3s;
.textOverflow();
.member {
margin-left: 12px;
font-size: @font-size-base;
line-height: 24px;
vertical-align: top;
}
&:hover {
color: @primary-color;
}
}
}
.projectList {
:global {
.ant-card-meta-description {
height: 44px;
overflow: hidden;
color: @text-color-secondary;
line-height: 22px;
}
}
.cardTitle {
font-size: 0;
a {
display: inline-block;
height: 24px;
margin-left: 12px;
color: @heading-color;
font-size: @font-size-base;
line-height: 24px;
vertical-align: top;
&:hover {
color: @primary-color;
}
}
}
.projectGrid {
width: 33.33%;
}
.projectItemContent {
display: flex;
height: 20px;
margin-top: 8px;
overflow: hidden;
font-size: 12px;
line-height: 20px;
.textOverflow();
a {
display: inline-block;
flex: 1 1 0;
color: @text-color-secondary;
.textOverflow();
&:hover {
color: @primary-color;
}
}
.datetime {
flex: 0 0 auto;
float: right;
color: @disabled-color;
}
}
}
.datetime {
color: @disabled-color;
}
@media screen and (max-width: @screen-xl) and (min-width: @screen-lg) {
.activeCard {
margin-bottom: 24px;
}
.members {
margin-bottom: 0;
}
.extraContent {
margin-left: -44px;
.statItem {
padding: 0 16px;
}
}
}
@media screen and (max-width: @screen-lg) {
.activeCard {
margin-bottom: 24px;
}
.members {
margin-bottom: 0;
}
.extraContent {
float: none;
margin-right: 0;
.statItem {
padding: 0 16px;
text-align: left;
&::after {
display: none;
}
}
}
}
@media screen and (max-width: @screen-md) {
.extraContent {
margin-left: -16px;
}
.projectList {
.projectGrid {
width: 50%;
}
}
}
@media screen and (max-width: @screen-sm) {
.pageHeaderContent {
display: block;
.content {
margin-left: 0;
}
}
.extraContent {
.statItem {
float: none;
}
}
}
@media screen and (max-width: @screen-xs) {
.projectList {
.projectGrid {
width: 100%;
}
}
}

@ -47,20 +47,21 @@ const Login: React.FC = () => {
type,
});
if (user) {
const defaultLoginSuccessMessage = '登录成功!';
message.success(defaultLoginSuccessMessage);
await fetchUserInfo();
/** 此方法会跳转到 redirect 参数所在的位置 */
if (!history) return;
const { query } = history.location;
const { redirect } = query as {
redirect: string;
};
history.push(redirect || '/');
return;
}
// 如果失败去设置用户错误信息
setUserLoginState(user);
const defaultLoginSuccessMessage = '登录成功!';
message.success(defaultLoginSuccessMessage);
await fetchUserInfo();
/** 此方法会跳转到 redirect 参数所在的位置 */
if (!history) return;
const {query} = history.location;
const {redirect} = query as {
redirect: string;
};
history.push(redirect || '/');
return;
}
return error;
} catch (error) {
const defaultLoginFailureMessage = '登录失败,请重试!';
message.error(defaultLoginFailureMessage);

@ -0,0 +1,50 @@
@import (reference) '~antd/es/style/themes/index';
.container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
background: @layout-body-background;
}
.lang {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
:global(.ant-dropdown-trigger) {
margin-right: 24px;
}
}
.content {
flex: 1;
padding: 32px 0;
}
@media (min-width: @screen-md-min) {
.container {
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
.content {
padding: 32px 0 24px;
}
}
.icon {
margin-left: 8px;
color: rgba(0, 0, 0, 0.2);
font-size: 24px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: @primary-color;
}
}

@ -0,0 +1,163 @@
import Footer from '@/components/Footer';
import {register} from '@/services/ant-design-pro/api';
import {SYSTEM_LOGO} from "@/constants";
import {
LockOutlined,
UserOutlined,
} from '@ant-design/icons';
import {
LoginForm,
ProFormText,
} from '@ant-design/pro-components';
import { message, Tabs} from 'antd';
import React, {useState} from 'react';
import {history, useModel} from 'umi';
import styles from './index.less';
const Register: React.FC = () => {
const [userLoginState] = useState<API.LoginResult>({});
const [type, setType] = useState<string>('account');
const {initialState, setInitialState} = useModel('@@initialState');
const fetchUserInfo = async () => {
const userInfo = await initialState?.fetchUserInfo?.();
if (userInfo) {
await setInitialState((s) => ({
...s,
currentUser: userInfo,
}));
}
};
// 表单提交
const handleSubmit = async (values: API.RegisterParams) => {
// 注册之前进行一定的校验
const {userPassword,checkPassword} = values;
if (userPassword !== checkPassword) {
message.error('两次输入的密码不一致');
return;
}
try {
// 注册逻辑
const id = await register(values)
if(id) {
const defaultLoginSuccessMessage = '注册成功!';
message.success(defaultLoginSuccessMessage);
await fetchUserInfo();
/** 此方法会跳转到 redirect 参数所在的位置 */
if (!history) return;
const {query} = history.location;
history.push({
pathname: '/user/login', // 登录成功redirect到访问的页面
query,
});
return;
}
} catch (error: any) {
const defaultLoginFailureMessage = '注册失败,请重试!';
message.error(defaultLoginFailureMessage);
}
};
const {status, type: loginType} = userLoginState;
return (
<div className={styles.container}>
<div className={styles.content}>
<LoginForm
submitter={{
searchConfig: {
submitText: '注册'
}
}}
logo={<img alt="logo" src={SYSTEM_LOGO}/>}
title="用户中心测试版"
subTitle={<a href="https://git.bnblogs.cc/zfp/user-center" target="_blank"
rel="noreferrer"></a>}
initialValues={{
autoLogin: true,
}}
onFinish={async (values) => {
await handleSubmit(values as API.RegisterParams);
}}
>
<Tabs activeKey={type} onChange={setType}>
<Tabs.TabPane key="account" tab={'账号密码注册'}/>
</Tabs>
{status === 'error' && loginType === 'account' }
{type === 'account' && (
<>
<ProFormText
name="userAccount"
fieldProps={{
size: 'large',
prefix: <UserOutlined className={styles.prefixIcon}/>,
}}
placeholder={'请输入您的账号'}
rules={[
{
required: true,
message: '账号是必填项!',
},
]}
/>
<ProFormText.Password
name="userPassword"
fieldProps={{
size: 'large',
prefix: <LockOutlined className={styles.prefixIcon}/>,
}}
placeholder={'请输入您的密码'}
rules={[
{
required: true,
message: '密码是必填项!',
}, {
min: 8,
type: 'string',
message: '长度不能小于8'
}
]}
/>
<ProFormText.Password
name="checkPassword"
fieldProps={{
size: 'large',
prefix: <LockOutlined className={styles.prefixIcon}/>,
}}
placeholder={'请再次输入您的密码'}
rules={[
{
required: true,
message: '确认密码是必填项!',
}, {
min: 8,
type: 'string',
message: '长度不能小于8'
}
]}
/>
<ProFormText
name="planetCode"
fieldProps={{
size: 'large',
prefix: <UserOutlined className={styles.prefixIcon}/>,
}}
placeholder={'请输入您的星球编号'}
rules={[
{
required: true,
message: '星球编号是必填项!',
},
]}
/>
</>
)}
</LoginForm>
</div>
<Footer/>
</div>
);
};
export default Register;

@ -0,0 +1,59 @@
/**
* request
* api 文档: https://github.com/umijs/umi-request
*/
import {extend} from 'umi-request';
import {message} from "antd";
import {history} from "@@/core/history";
import {stringify} from "querystring";
/**
* request请求时的默认参数
*/
const request = extend({
credentials: 'include', // 默认请求是否带上cookie
prefix: process.env.NODE_ENV === 'production' ? 'http://user-backend.code-nav.cn' : undefined
// requestType: 'form',
});
/**
*
*/
request.interceptors.request.use((url, options): any => {
console.log(`do request url = ${url}`)
return {
url,
options: {
...options,
headers: {},
},
};
});
/**
*
*/
request.interceptors.response.use(async (response, options): Promise<any> => {
const res = await response.clone().json();
console.log(res);
if (res.code === 0) {
return res.data;
}
// 用户未登录重定向到登录页面
if (res.code === 40100) {
message.error('请先登录');
history.replace({
pathname: '/user/login',
search: stringify({
redirect: location.pathname,
}),
});
} else {
message.error(res.description)
}
return res.data;
});
export default request;

@ -1,28 +1,39 @@
// @ts-ignore
/* eslint-disable */
import { request } from 'umi';
import request from "@/plugins/globalRequest";
/** 获取当前的用户 GET /api/currentUser */
export async function currentUser(options?: { [key: string]: any }) {
return request<{
data: API.CurrentUser;
}>('/api/currentUser', {
return request<API.BaseResponse<API.CurrentUser>>('/api/user/current', {
method: 'GET',
...(options || {}),
});
}
/** 退出登录接口 POST /api/login/outLogin */
/** 退出登录接口 POST /api/user/logout */
export async function outLogin(options?: { [key: string]: any }) {
return request<Record<string, any>>('/api/login/outLogin', {
return request<API.BaseResponse<number>>('/api/user/logout', {
method: 'POST',
...(options || {}),
});
}
/** 登录接口 POST /api/login/account */
/** 登录接口 POST /api/user/login */
export async function login(body: API.LoginParams, options?: { [key: string]: any }) {
return request<API.LoginResult>('/api/user/login', {
return request<API.BaseResponse<API.LoginResult>>('/api/user/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 注册接口 POST /api/user/register */
export async function register(body: API.RegisterParams, options?: { [key: string]: any }) {
return request<API.BaseResponse<API.RegisterResult>>('/api/user/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -32,6 +43,15 @@ export async function login(body: API.LoginParams, options?: { [key: string]: an
});
}
/** 搜索用户 GET /api/user/search */
export async function searchUsers(options?: { [key: string]: any }) {
return request<API.BaseResponse<API.CurrentUser[]>>('/api/user/search', {
method: 'GET',
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /api/notices */
export async function getNotices(options?: { [key: string]: any }) {
return request<API.NoticeIconList>('/api/notices', {

@ -3,32 +3,44 @@
declare namespace API {
type CurrentUser = {
name?: string;
avatar?: string;
userid?: string;
email?: string;
signature?: string;
title?: string;
group?: string;
tags?: { key?: string; label?: string }[];
notifyCount?: number;
unreadCount?: number;
country?: string;
access?: string;
geographic?: {
province?: { label?: string; key?: string };
city?: { label?: string; key?: string };
};
address?: string;
phone?: string;
};
id: number,
username: string,
userAccount: string,
avatarUrl?: string
gender: number,
phone: string,
email: string,
userStatus: number,
createTime: Date,
userRole: number,
planetCode: string
}
/*
*/
type LoginResult = {
status?: string;
type?: string;
currentAuthority?: string;
};
/**
*
*/
type BaseResponse<T> = {
code: number,
data: T,
message: string,
description: string,
}
/*
id
*/
type RegisterResult = number;
type PageParams = {
current?: number;
pageSize?: number;
@ -61,12 +73,28 @@ declare namespace API {
status?: string;
};
/*
*/
type LoginParams = {
userAccount?: string;
userPassword?: string;
autoLogin?: boolean;
type?: string;
};
/*
*/
type RegisterParams = {
userAccount?: string;
userPassword?: string;
checkPassword?: string;
planetCode?: string;
type?: string;
};
type ErrorResponse = {
/** 业务约定的错误码 */

Loading…
Cancel
Save