parent
d1058c8cf2
commit
f973d2cf77
37 changed files with 1621 additions and 1545 deletions
@ -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); |
||||
} |
||||
} |
@ -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(),"系统内部错误"); |
||||
} |
||||
|
||||
} |
@ -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" |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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 +1,2 @@ |
||||
export const SYSTEM_LOGO = "https://pic.code-nav.cn/user_avatar/1610518142000300034/YeedIoq3-logo.png"; |
||||
export const ADMIN_USER = 1; |
||||
|
@ -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> |
||||
|
||||
<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%; |
||||
} |
||||
} |
||||
} |
@ -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; |
Loading…
Reference in new issue