Commit 425afdff by flyxiaozhu

erp module

parents
Showing with 12766 additions and 0 deletions
.idea
*.iml
target
react-admin/node_modules
/temp.sql
ERP Module
==
提供全后端分离的模板
\ No newline at end of file
FROM java:8-alpine
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo 'Asia/Shanghai' >/etc/timezone
ADD target/original-erp-admin-api-1.0-SNAPSHOT.jar app.jar
ENTRYPOINT ["sh","-c","java -jar /app.jar --spring.profiles.active=prod"]
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>erp</artifactId>
<groupId>com.maile</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>erp-admin-api</artifactId>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<fork>true</fork>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer>
<mainClass>com.maile.erp.admin.AdminApplication</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.hisun.crypt</groupId>
<artifactId>amac</artifactId>
<version>1.0.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/library/hiencrypt.jar</systemPath>
</dependency>
</dependencies>
<properties>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>erp</artifactId>
<groupId>com.maile</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>erp-admin-api</artifactId>
<dependencies>
<dependency>
<groupId>com.maile</groupId>
<artifactId>erp-core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--aop依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.0.3.RELEASE</version>
</dependency>
<!--https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.0.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>runtime</scope>
<version>2.0.3.RELEASE</version>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 如果不设置fork,那么不会restart,devtools热部署不会起作用-->
<fork>true</fork>
<includeSystemScope>true</includeSystemScope>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.maile.erp.admin.AdminApplication</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
package com.maile.erp.admin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableCaching
@EntityScan(basePackages = {"com.maile.erp"})
@ComponentScan(basePackages = {"com.maile.erp"})
@EnableJpaRepositories(basePackages = {"com.maile.erp"})
@EnableAsync
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
}
package com.maile.erp.admin.authorize;
import com.maile.erp.core.entities.AdminUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
public class AdminUserDetails implements UserDetails {
private AdminUser adminUser;
private Collection<? extends GrantedAuthority> authorities;
public AdminUserDetails(AdminUser adminUser, Collection<? extends GrantedAuthority> authorities) {
this.adminUser = adminUser;
this.authorities = authorities;
}
public AdminUser getAdminUser() {
return adminUser;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return adminUser.getPassword();
}
@Override
public String getUsername() {
return adminUser.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return adminUser.getStatus() == 0;
}
}
package com.maile.erp.admin.authorize;
import com.maile.erp.core.entities.AdminUser;
import com.maile.erp.core.entities.Permission;
import com.maile.erp.core.repositories.AdminUserRepository;
import com.maile.erp.core.repositories.PermissionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Service
public class AdminUserDetailsService implements UserDetailsService {
@Autowired
AdminUserRepository adminUserRepository;
@Autowired
PermissionRepository permissionRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AdminUser user = adminUserRepository.findAdminUserByUsername(username);
if (user != null) {
List<Permission> permissions = permissionRepository.findByAdminUserId(user.getId());
Set<GrantedAuthority> authorities = new HashSet<>();
for (Permission permission : permissions) {
if (permission != null && permission.getName() != null) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + permission.getIdentity());
authorities.add(grantedAuthority);
}
}
return new AdminUserDetails(user, authorities);
} else {
throw new UsernameNotFoundException("admin: " + username + " do not exist!");
}
}
}
package com.maile.erp.admin.authorize;
import com.maile.erp.core.entities.AdminUser;
import com.maile.erp.core.utils.CollectionUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class AuthorizeHelper {
public static AdminUserDetails getUserDetails() {
return (AdminUserDetails) SecurityContextHolder.getContext()
.getAuthentication()
.getPrincipal();
}
public static AdminUser getUser() {
return getUserDetails().getAdminUser();
}
public static Collection<String> getPermissions() {
return getPermissions(null);
}
public static Collection<String> getPermissions(UserDetails userDetails) {
if (userDetails == null) {
userDetails = getUserDetails();
}
List<String> permission = CollectionUtils.map(userDetails.getAuthorities(), o -> o.getAuthority().replaceAll("^ROLE_", ""));
Collections.sort(permission);
return permission;
}
}
package com.maile.erp.admin.authorize;
import com.maile.erp.core.entities.Permission;
import com.maile.erp.core.repositories.PermissionRepository;
import com.maile.erp.core.utils.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Component
public class PermissionManager {
private PermissionRepository permissionRepository;
@Autowired
public void setPermissionRepository(PermissionRepository permissionRepository) {
this.permissionRepository = permissionRepository;
}
private Permission buildDefaultPermission(String identify) {
Permission permission = new Permission();
permission.setIdentity(identify);
permission.setName("未定义权限");
return permission;
}
public void init() {
PermissionScanner scanner = new PermissionScanner("com.maile.erp.admin.controllers");
List<Permission> permissions = permissionRepository.findAll();
Set<String> dbPerms = new HashSet<>(CollectionUtils.map(permissions, Permission::getIdentity));
Set<String> realPerms = scanner.getPermissions();
Set<String> newPerms = new HashSet<>(realPerms);
newPerms.removeAll(dbPerms);
Set<String> discardPerms = new HashSet<>(dbPerms);
discardPerms.removeAll(realPerms);
if (discardPerms.size() > 0) {
System.out.println("============无用的权限============");
System.out.println(discardPerms);
}
if (newPerms.size() > 0) {
System.out.println("============新注册权限============");
System.out.println(newPerms);
}
List<Permission> permissiones = new ArrayList<>();
for (String perm : newPerms) {
permissiones.add(buildDefaultPermission(perm));
}
permissionRepository.saveAll(permissiones);
}
}
package com.maile.erp.admin.authorize;
import com.maile.erp.core.libs.ClassScanner;
import com.maile.erp.core.utils.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PermissionScanner extends ClassScanner {
private String basePackage;
private Set<Class<?>> controllers;
public PermissionScanner(String basePackage) {
this.basePackage = basePackage;
}
public Set<Class<?>> getControllers() {
if (controllers == null) {
controllers = new HashSet<>();
Set<Class<?>> clsList = getClasses(basePackage, true);
if (clsList != null && clsList.size() > 0) {
for (Class<?> cls : clsList) {
if (cls.getAnnotation(Controller.class) != null || cls.getAnnotation(RestController.class) != null) {
controllers.add(cls);
}
}
}
}
return controllers;
}
public Set<String> getPermissions() {
Set<String> permissions = new HashSet<>();
for (Class<?> cls : getControllers()) {
Method[] methods = cls.getMethods();
for (Method method : methods) {
Secured securedAnnotation = method.getAnnotation(Secured.class);
if (securedAnnotation != null) {
String[] value = securedAnnotation.value();
permissions.addAll(CollectionUtils.map(value, o -> o.replaceAll("^ROLE_", "")));
} else {
String expression = "";
PreAuthorize preAuthorizeAnnotation = method.getAnnotation(PreAuthorize.class);
if (preAuthorizeAnnotation != null) {
expression = preAuthorizeAnnotation.value();
}
PostAuthorize postAuthorizeAnnotation = method.getAnnotation(PostAuthorize.class);
if (postAuthorizeAnnotation != null) {
expression = postAuthorizeAnnotation.value();
}
if (!StringUtils.isBlank(expression)) {
Pattern pattern = Pattern.compile("ROLE_\\w+");
Matcher matcher = pattern.matcher(expression);
while (matcher.find()) {
permissions.add(matcher.group().replaceAll("^ROLE_", ""));
}
}
}
}
}
return permissions;
}
}
package com.maile.erp.admin.configurations;
import com.maile.erp.core.libs.AppException;
import com.maile.erp.core.libs.ErrorCode;
import com.maile.erp.core.libs.JSONResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.core.serializer.support.SerializationFailedException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Set;
/**
* Description:
*
* @author johnny.lu
* @version 1.0
* @date 2017/12/30 1:06
* Created with IDEA
*/
@ControllerAdvice
public class WebControllerAdvice {
private static final Logger logger = LoggerFactory.getLogger(WebControllerAdvice.class);
@InitBinder
protected void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
}
@ExceptionHandler(value = ConstraintViolationException.class)
@ResponseBody
public JSONResult handleResourceNotFoundException(ConstraintViolationException e) {
logger.debug("constraint violation exception", e);
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
StringBuilder strBuilder = new StringBuilder();
for (ConstraintViolation<?> violation : violations) {
String errorMsg = violation.getMessage();
strBuilder.append(errorMsg + "\n");
}
return new JSONResult(ErrorCode.INVALID_PARAM, strBuilder.toString());
}
@ExceptionHandler(value = BindException.class)
@ResponseBody
protected JSONResult handleBindException(BindException ex) throws IOException {
JSONResult result = new JSONResult(ex.getBindingResult());
return result;
}
@ExceptionHandler(value = MissingServletRequestParameterException.class)
@ResponseBody
protected JSONResult handleMissingServletRequestParameter(MissingServletRequestParameterException ex) throws IOException {
JSONResult result = new JSONResult(ErrorCode.INVALID_PARAM, "Miss parameter " + ex.getParameterName() + ".");
return result;
}
@ExceptionHandler(value = MissingPathVariableException.class)
@ResponseBody
protected JSONResult handleMissingPathVariable(MissingPathVariableException ex) throws IOException {
JSONResult result = new JSONResult(ErrorCode.INVALID_PARAM, "Miss path variable " + ex.getVariableName() + ".");
return result;
}
@ExceptionHandler(value = DataIntegrityViolationException.class)
@ResponseBody
protected JSONResult handleDataIntegrityViolationException(DataIntegrityViolationException ex) {
JSONResult result = new JSONResult(ErrorCode.DUPLICATE_KEY, "数据发生重复");
return result;
}
@ExceptionHandler(value = AppException.class)
@ResponseBody
protected JSONResult handleAppException(AppException ex) {
JSONResult result = new JSONResult(ex.getCode(), ex.getMessage());
return result;
}
@ExceptionHandler(value = AccessDeniedException.class)
@ResponseBody
protected JSONResult handleAppAccessDenied(AccessDeniedException ex) {
JSONResult result = new JSONResult(ErrorCode.FORBIDDEN, "你没有权限进行此操作");
return result;
}
}
package com.maile.erp.admin.configurations;
import com.maile.erp.admin.authorize.AdminUserDetails;
import com.maile.erp.admin.authorize.AdminUserDetailsService;
import com.maile.erp.admin.authorize.AuthorizeHelper;
import com.maile.erp.core.libs.ErrorCode;
import com.maile.erp.core.libs.JSONResult;
import com.maile.erp.core.libs.RenderUtils;
import com.maile.erp.core.repositories.AdminUserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.DigestUtils;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
AdminUserRepository adminUserRepository;
@Bean
UserDetailsService customUserDetailService() {
return new AdminUserDetailsService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailService())
.passwordEncoder(new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()));
}
});
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/", "/home", "/dist/**", "**.ico").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.permitAll()
.successHandler((request, response, auth) -> {
AdminUserDetails userDetails = (AdminUserDetails) auth.getPrincipal();
JSONResult result = new JSONResult()
.put("user", userDetails.getAdminUser())
.put("permission", AuthorizeHelper.getPermissions(userDetails));
RenderUtils.renderJson(response, result);
})
.failureHandler((request, response, exception) -> {
if (exception instanceof BadCredentialsException) {
RenderUtils.renderJson(response, new JSONResult(ErrorCode.BAD_CREDENTIALS, "账号或密码错误"));
} else if (exception instanceof DisabledException) {
RenderUtils.renderJson(response, new JSONResult(ErrorCode.USER_DISABLED, "此账户已被禁用"));
} else {
RenderUtils.renderJson(response, new JSONResult(ErrorCode.UNKNOWN, "未知错误"));
}
})
.and()
.logout()
.logoutSuccessHandler((request, response, auth) -> response.sendRedirect("/"))
.permitAll()
.and()
.exceptionHandling()
.authenticationEntryPoint((request, response, exception) -> {
RenderUtils.renderJson(response, new JSONResult(ErrorCode.NOT_LOGIN, "尚未登录"));
});
}
}
package com.maile.erp.admin.controllers;
import com.maile.erp.core.entities.Permission;
import com.maile.erp.core.libs.JSONResult;
import com.maile.erp.core.libs.SearchSpecification;
import com.maile.erp.core.repositories.PermissionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@RestController
public class PermissionController {
@Autowired
PermissionRepository permissionRepository;
@RequestMapping("/permission/select")
public JSONResult select(HttpServletRequest request, @PageableDefault Pageable pageable) {
Page<Permission> page = permissionRepository.findAll(new SearchSpecification<>(request), pageable);
return new JSONResult(page, pageable.getPageNumber());
}
@RequestMapping("/permission/all")
public JSONResult all(HttpServletRequest request) {
List<Permission> list = permissionRepository.findAll(new SearchSpecification<>(request));
return new JSONResult().put("list", list);
}
@RequestMapping("/permission/update")
public JSONResult update() {
return new JSONResult().put("gg", "f");
}
}
package com.maile.erp.admin.controllers;
import com.maile.erp.core.entities.Role;
import com.maile.erp.core.entities.RolePermission;
import com.maile.erp.core.libs.ErrorCode;
import com.maile.erp.core.libs.JSONResult;
import com.maile.erp.core.libs.SearchSpecification;
import com.maile.erp.core.repositories.RolePermissionRepository;
import com.maile.erp.core.repositories.RoleRepository;
import com.maile.erp.core.utils.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.transaction.Transactional;
import java.util.*;
@RestController
public class RoleController {
@Autowired
RoleRepository roleRepository;
@Autowired
RolePermissionRepository rolePermissionRepository;
@Secured("ROLE_ROLE_SELECT")
@RequestMapping("/role/select")
public JSONResult select(HttpServletRequest request, @PageableDefault Pageable pageable) {
Page<Role> page = roleRepository.findAll(new SearchSpecification<>(request), pageable);
List<Long> rolesId = CollectionUtils.map(page.getContent(), Role::getId);
List<RolePermission> permissions = rolePermissionRepository.findByRoleIdIn(rolesId);
Map<Long, List<Long>> perms = CollectionUtils.group(permissions, new CollectionUtils.GroupFilter<RolePermission, Long, Long>() {
@Override
public Long getKey(RolePermission source) {
return source.getRoleId();
}
@Override
public Long getValue(RolePermission source) {
return source.getPermissionId();
}
});
for (Role role : page.getContent()) {
if (perms.containsKey(role.getId())) {
role.setPerms(perms.get(role.getId()));
}
}
return new JSONResult(page, pageable.getPageNumber());
}
@Secured("ROLE_ROLE_INSERT")
@RequestMapping("/role/insert")
public JSONResult insert(@RequestParam("name") String name,
@RequestParam(value = "perms", required = false) List<Long> perms) {
if (roleRepository.findByName(name) != null) {
return new JSONResult(ErrorCode.NO_DATA, "角色名已存在");
}
Role role = new Role();
role.setName(name);
roleRepository.save(role);
if (perms != null && perms.size() > 0) {
putPerms(role.getId(), perms);
}
role.setPerms(perms);
return new JSONResult().put("model", role);
}
@Secured("ROLE_ROLE_UPDATE")
@RequestMapping("/role/update")
@Transactional
public JSONResult update(@RequestParam("id") Long id,
@RequestParam("name") String name,
@RequestParam(value = "perms", required = false) List<Long> perms) {
Role role = roleRepository.findById(id).orElse(null);
if (role == null) {
return new JSONResult(ErrorCode.NO_DATA, "角色ID错误");
}
Role old = roleRepository.findByName(name);
if (old != null && old.getId() != id) {
return new JSONResult(ErrorCode.NO_DATA, "角色名已存在");
}
role.setName(name);
roleRepository.save(role);
rolePermissionRepository.deleteAllByRoleId(role.getId());
if (perms != null && perms.size() > 0) {
putPerms(role.getId(), perms);
}
role.setPerms(perms);
return new JSONResult().put("model", role).put("count", 1);
}
private void putPerms(Long roleId, Collection<Long> perms) {
for (Long permId : perms) {
RolePermission rp = new RolePermission();
rp.setPermissionId(permId);
rp.setRoleId(roleId);
rolePermissionRepository.save(rp);
}
}
}
package com.maile.erp.admin.controllers;
import com.maile.erp.core.libs.ErrorCode;
import com.maile.erp.core.libs.JSONResult;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SiteController {
// @RequestMapping(value = "/")
// public String index() {
// return "this is Index";
// }
}
package com.maile.erp.admin.controllers;
import com.maile.erp.admin.authorize.AdminUserDetails;
import com.maile.erp.admin.authorize.AuthorizeHelper;
import com.maile.erp.core.entities.AdminRole;
import com.maile.erp.core.entities.AdminUser;
import com.maile.erp.core.libs.ErrorCode;
import com.maile.erp.core.libs.JSONResult;
import com.maile.erp.core.libs.SearchSpecification;
import com.maile.erp.core.repositories.AdminRoleRepository;
import com.maile.erp.core.repositories.AdminUserRepository;
import com.maile.erp.core.utils.BeanUtils;
import com.maile.erp.core.utils.CollectionUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.transaction.Transactional;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@Slf4j
@RestController
public class UserController {
@Autowired
AdminUserRepository adminUserRepository;
@Autowired
AdminRoleRepository adminRoleRepository;
@RequestMapping("/user/getCurrentUser")
public JSONResult getCurrentUser() {
AdminUserDetails userDetails = AuthorizeHelper.getUserDetails();
return new JSONResult()
.put("user", userDetails.getAdminUser())
.put("permission", AuthorizeHelper.getPermissions());
}
@Secured("ROLE_USER_SELECT")
@RequestMapping("/user/select")
public JSONResult getUserList(HttpServletRequest request, @PageableDefault Pageable pageable) {
Page<AdminUser> page = adminUserRepository.findAll(new SearchSpecification<>(request), pageable);
List<Long> userIds = CollectionUtils.map(page.getContent(), AdminUser::getId);
List<AdminRole> roles = adminRoleRepository.findByAdminUserIdIn(userIds);
Map<Long, List<Long>> userIdGroup = CollectionUtils.group(roles, new CollectionUtils.GroupFilter<AdminRole, Long, Long>() {
@Override
public Long getKey(AdminRole source) {
return source.getAdminUserId();
}
@Override
public Long getValue(AdminRole source) {
return source.getRoleId();
}
});
for (AdminUser adminUser : page.getContent()) {
adminUser.setRoleId(userIdGroup.get(adminUser.getId()));
}
return new JSONResult(page, pageable.getPageNumber());
}
@Secured("ROLE_USER_INSERT")
@PostMapping("/user/insert")
public JSONResult addUser(AdminUser adminUser,
@RequestParam(value = "roleId", required = false) List<Long> roleId) {
AdminUser adminUser1 = adminUserRepository.findAdminUserByUsername(StringUtils.trim(adminUser.getUsername()));
if (adminUser1 != null) {
return new JSONResult(ErrorCode.DUPLICATE_KEY, "该用户名已存在");
}
adminUser.setPassword(DigestUtils.md5Hex(adminUser.getTmpPwd()));
adminUserRepository.save(adminUser);
if (roleId != null) {
assigningRoles(adminUser.getId(), roleId);
}
adminUser.setTmpPwd("******");
return new JSONResult().put("model", adminUser);
}
@Transactional
@Secured("ROLE_USER_UPDATE")
@PostMapping("/user/update")
public JSONResult updateUser(AdminUser adminUser,
@RequestParam(value = "roleId", required = false) List<Long> roleId) {
AdminUser user = adminUserRepository.findById(adminUser.getId()).orElse(null);
if (user == null) {
return new JSONResult(ErrorCode.NO_DATA, "该用户不存在");
}
AdminUser user1 = adminUserRepository.findAdminUserByUsername(StringUtils.trim(adminUser.getUsername()));
if (user1 != null && user1.getId() != adminUser.getId()) {
return new JSONResult(ErrorCode.DUPLICATE_KEY, "该用户名已存在");
}
BeanUtils.copyProperties(adminUser, user);
if (!adminUser.getTmpPwd().equals("******")) {
user.setPassword(DigestUtils.md5Hex(adminUser.getTmpPwd()));
}
adminUserRepository.save(user);
adminRoleRepository.deleteByAdminUserId(adminUser.getId());
if (roleId != null) {
assigningRoles(adminUser.getId(), roleId);
}
user.setTmpPwd("******");
return new JSONResult().put("count", 1).put("model", user);
}
private void assigningRoles(Long userId, Collection<Long> roleIds) {
for (Long roleId : roleIds) {
AdminRole ar = new AdminRole();
ar.setAdminUserId(userId);
ar.setRoleId(roleId);
adminRoleRepository.save(ar);
}
}
}
spring.profiles.active=dev
spring.jackson.serialization.indent_output=true
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
spring.datasource.secondary.max-idle=10
spring.datasource.secondary.max-wait=10000
spring.datasource.secondary.min-idle=5
spring.datasource.secondary.initial-size=5
spring.datasource.secondary.validation-query=SELECT 1
spring.datasource.secondary.test-on-borrow=false
spring.datasource.secondary.test-while-idle=true
spring.datasource.secondary.time-between-eviction-runs-millis=18800
server.port=8081
spring.datasource.url=jdbc:mysql://localhost:3306/erp-module?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false&serverTimezone=UTC&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#设置为create时每次都会重新创建表
#需要初始化数据时用create,会自动加载import.sql
spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=true
logging.level.com.maile.erp=DEBUG
#以下是actuator配置
#如访问:http://localhost:8080/manager/health
management.security.enabled=false
#设置为端点的url设置前缀
management.context-path=/manager
\ No newline at end of file
spring.datasource.url=jdbc:mysql://localhost:3306/erp-module?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#设置为create时每次都会重新创建表
#需要初始化数据时用create,会自动加载import.sql
spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=false
logging.level.com.maile.erp=INFO
logging.file=/usr/local/logs/java/admin-api.log
#以下是actuator配置
#如访问:http://localhost:8080/manager/health
management.security.enabled=false
#设置为端点的url设置前缀
management.context-path=/manager
#注意这个初始化脚本只用于开发/测试时使用,上生产时手动执行
set foreign_key_checks=0;
set foreign_key_checks=1;
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
<title>Group 21</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
<g id="Group-21" transform="translate(77.000000, 73.000000)">
<g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
</g>
</g>
<g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
</g>
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
</g>
<g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
</g>
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
</g>
<g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
<g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
</g>
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<!--
2013-9-30: Created.
-->
<svg>
<metadata>
Created by iconfont
</metadata>
<defs>
<font id="iconfont" horiz-adv-x="1024" >
<font-face
font-family="iconfont"
font-weight="500"
font-stretch="normal"
units-per-em="1024"
ascent="896"
descent="-128"
/>
<missing-glyph />
<glyph glyph-name="jingxuanshichang" unicode="&#58880;" d="M627.977626 476.225434c119.09417-1.609626 238.185267-3.234714 357.295821-4.859699-4.320563 15.968666-8.659558 31.970099-12.985242 47.956173-94.425395-72.591258-188.872192-145.212314-283.298611-217.802547-9.083187-6.970778-15.551795-17.022669-11.830886-29.056819 35.275878-113.777357 70.511718-227.553587 105.774285-341.313536 12.613837 9.670246 25.217331 19.389645 37.817856 29.076173-98.232218 67.357082-196.461363 134.754099-294.693581 202.115277-6.971085 4.792115-19.035341 4.890419-25.973555 0L208.141824-43.727667c12.618957-9.687654 25.200947-19.373261 37.821952-29.057843 33.73056 114.214298 67.447808 228.459213 101.16311 342.70423 3.554099 11.964518-2.581914 22.152499-11.829862 29.056819-95.382221 71.316275-190.799155 142.631526-286.198784 213.963059-4.321587-16.00041-8.661606-31.986483-12.985242-47.956173 119.059354 3.234714 238.11881 6.450893 357.196595 9.669222 12.030464 0.334643 21.047194 7.725978 24.833638 18.90048 38.34071 112.771379 76.697907 225.542861 115.056026 338.296832l-49.636478 0c39.850189-112.235213 79.714714-224.469402 119.579238-336.723046 11.026534-31.032832 60.796109-17.659187 49.634406 13.689754-39.846093 112.235213-79.728026 224.47145-119.57719 336.723046-8.614502 24.230912-41.238835 24.733286-49.636454 0-38.35607-112.771379-76.713267-225.541837-115.052954-338.295808 8.260506 6.283059 16.522957 12.582605 24.80087 18.884096-119.077786-3.21833-238.137242-6.434509-357.196595-9.668198-27.38176-0.753152-32.77783-33.195008-13.003674-47.974605 95.414886-71.315251 190.831923-142.630502 286.212096-213.945651-3.9552 9.684582-7.874662 19.371213-11.830886 29.055795-33.732608-114.247066-67.446784-228.458189-101.179494-342.70423-6.049997-20.51113 18.950349-42.393498 37.80352-29.072179 97.058406 68.499046 194.083123 136.983859 291.10569 205.485978-15.284736-10.792858-34.48576 7.003546-12.482765-8.079053 11.142246-7.640986 22.268006-15.282074 33.380557-22.919987 33.763328-23.160525 67.547136-46.316851 101.29408-69.476352 49.132954-33.679053 98.231194-67.35913 147.331379-101.074022 18.783539-12.868096 44.272128 8.344064 37.83936 29.087437-35.254374 113.779405-70.532198 227.520922-105.772237 341.297152-3.956224-9.685606-7.909478-19.371213-11.84727-29.054771 94.426419 72.58921 188.871168 145.194906 283.312947 217.800499 19.222528 14.779597 14.782259 47.586816-13.00265 47.956173-119.110554 1.62601-238.202675 3.250995-357.295821 4.860723C594.746368 528.15319 594.798592 476.677734 627.977626 476.225434" horiz-adv-x="1024" />
<glyph glyph-name="chexingluntan" unicode="&#59100;" d="M1320.606897 546.427586C1313.544828 564.082759 1292.358621 578.206897 1271.172414 578.206897l-176.551724 0L1094.62069 684.137931c0 21.186207-14.124138 35.310345-35.310345 35.310345L529.655172 719.448276c-21.186207 0-35.310345-14.124138-35.310345-35.310345l0 0c0-21.186207 14.124138-35.310345 35.310345-35.310345l494.344828 0 0-600.275862L632.055172 48.551724c-14.124138 60.027586-70.62069 105.931034-137.710345 105.931034s-120.055172-45.903448-137.710345-105.931034L282.482759 48.551724l0 317.793103c0 21.186207-14.124138 35.310345-35.310345 35.310345l0 0c-21.186207 0-35.310345-14.124138-35.310345-35.310345l0-353.103448c0-21.186207 14.124138-35.310345 35.310345-35.310345l109.462069 0c14.124138-60.027586 70.62069-105.931034 137.710345-105.931034s120.055172 45.903448 137.710345 105.931034l399.006897 0c14.124138-60.027586 70.62069-105.931034 137.710345-105.931034s120.055172 45.903448 137.710345 105.931034L1377.103448-22.068966c21.186207 0 35.310345 14.124138 35.310345 35.310345l0 229.517241C1412.413793 331.034483 1320.606897 546.427586 1320.606897 546.427586zM494.344828-57.37931c-38.841379 0-70.62069 31.77931-70.62069 70.62069s31.77931 70.62069 70.62069 70.62069 70.62069-31.77931 70.62069-70.62069S533.186207-57.37931 494.344828-57.37931zM1165.241379-57.37931c-38.841379 0-70.62069 31.77931-70.62069 70.62069s31.77931 70.62069 70.62069 70.62069 70.62069-31.77931 70.62069-70.62069S1204.082759-57.37931 1165.241379-57.37931zM1341.793103 48.551724l-38.841379 0c-14.124138 60.027586-70.62069 105.931034-137.710345 105.931034-24.717241 0-49.434483-7.062069-70.62069-17.655172L1094.62069 507.586207l158.896552 0c0 0 24.717241-17.655172 28.248276-35.310345l-63.558621 0c-21.186207 0-35.310345-14.124138-35.310345-35.310345l0-141.241379c0-21.186207 14.124138-35.310345 35.310345-35.310345L1341.793103 260.413793C1341.793103 260.413793 1341.793103 48.551724 1341.793103 48.551724zM0 860.689655c0 21.186207 17.655172 35.310345 35.310345 35.310345l353.103448 0c21.186207 0 35.310345-14.124138 35.310345-35.310345 0-21.186207-17.655172-35.310345-35.310345-35.310345L35.310345 825.37931C14.124138 825.37931 0 839.503448 0 860.689655zM105.931034 684.137931c0 21.186207 14.124138 35.310345 35.310345 35.310345l247.172414 0c21.186207 0 35.310345-14.124138 35.310345-35.310345 0-21.186207-14.124138-35.310345-35.310345-35.310345L141.241379 648.827586C120.055172 648.827586 105.931034 662.951724 105.931034 684.137931zM211.862069 507.586207c0 21.186207 17.655172 35.310345 35.310345 35.310345l141.241379 0c17.655172 0 35.310345-14.124138 35.310345-35.310345 0-21.186207-17.655172-35.310345-35.310345-35.310345L247.172414 472.275862C225.986207 472.275862 211.862069 486.4 211.862069 507.586207z" horiz-adv-x="1413" />
</font>
</defs></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
<title>Group 21</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
<g id="Group-21" transform="translate(77.000000, 73.000000)">
<g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
</g>
</g>
<g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
</g>
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
</g>
<g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
</g>
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
</g>
<g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
<g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
</g>
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<!--
2013-9-30: Created.
-->
<svg>
<metadata>
Created by iconfont
</metadata>
<defs>
<font id="iconfont" horiz-adv-x="1024" >
<font-face
font-family="iconfont"
font-weight="500"
font-stretch="normal"
units-per-em="1024"
ascent="896"
descent="-128"
/>
<missing-glyph />
<glyph glyph-name="jingxuanshichang" unicode="&#58880;" d="M627.977626 476.225434c119.09417-1.609626 238.185267-3.234714 357.295821-4.859699-4.320563 15.968666-8.659558 31.970099-12.985242 47.956173-94.425395-72.591258-188.872192-145.212314-283.298611-217.802547-9.083187-6.970778-15.551795-17.022669-11.830886-29.056819 35.275878-113.777357 70.511718-227.553587 105.774285-341.313536 12.613837 9.670246 25.217331 19.389645 37.817856 29.076173-98.232218 67.357082-196.461363 134.754099-294.693581 202.115277-6.971085 4.792115-19.035341 4.890419-25.973555 0L208.141824-43.727667c12.618957-9.687654 25.200947-19.373261 37.821952-29.057843 33.73056 114.214298 67.447808 228.459213 101.16311 342.70423 3.554099 11.964518-2.581914 22.152499-11.829862 29.056819-95.382221 71.316275-190.799155 142.631526-286.198784 213.963059-4.321587-16.00041-8.661606-31.986483-12.985242-47.956173 119.059354 3.234714 238.11881 6.450893 357.196595 9.669222 12.030464 0.334643 21.047194 7.725978 24.833638 18.90048 38.34071 112.771379 76.697907 225.542861 115.056026 338.296832l-49.636478 0c39.850189-112.235213 79.714714-224.469402 119.579238-336.723046 11.026534-31.032832 60.796109-17.659187 49.634406 13.689754-39.846093 112.235213-79.728026 224.47145-119.57719 336.723046-8.614502 24.230912-41.238835 24.733286-49.636454 0-38.35607-112.771379-76.713267-225.541837-115.052954-338.295808 8.260506 6.283059 16.522957 12.582605 24.80087 18.884096-119.077786-3.21833-238.137242-6.434509-357.196595-9.668198-27.38176-0.753152-32.77783-33.195008-13.003674-47.974605 95.414886-71.315251 190.831923-142.630502 286.212096-213.945651-3.9552 9.684582-7.874662 19.371213-11.830886 29.055795-33.732608-114.247066-67.446784-228.458189-101.179494-342.70423-6.049997-20.51113 18.950349-42.393498 37.80352-29.072179 97.058406 68.499046 194.083123 136.983859 291.10569 205.485978-15.284736-10.792858-34.48576 7.003546-12.482765-8.079053 11.142246-7.640986 22.268006-15.282074 33.380557-22.919987 33.763328-23.160525 67.547136-46.316851 101.29408-69.476352 49.132954-33.679053 98.231194-67.35913 147.331379-101.074022 18.783539-12.868096 44.272128 8.344064 37.83936 29.087437-35.254374 113.779405-70.532198 227.520922-105.772237 341.297152-3.956224-9.685606-7.909478-19.371213-11.84727-29.054771 94.426419 72.58921 188.871168 145.194906 283.312947 217.800499 19.222528 14.779597 14.782259 47.586816-13.00265 47.956173-119.110554 1.62601-238.202675 3.250995-357.295821 4.860723C594.746368 528.15319 594.798592 476.677734 627.977626 476.225434" horiz-adv-x="1024" />
<glyph glyph-name="chexingluntan" unicode="&#59100;" d="M1320.606897 546.427586C1313.544828 564.082759 1292.358621 578.206897 1271.172414 578.206897l-176.551724 0L1094.62069 684.137931c0 21.186207-14.124138 35.310345-35.310345 35.310345L529.655172 719.448276c-21.186207 0-35.310345-14.124138-35.310345-35.310345l0 0c0-21.186207 14.124138-35.310345 35.310345-35.310345l494.344828 0 0-600.275862L632.055172 48.551724c-14.124138 60.027586-70.62069 105.931034-137.710345 105.931034s-120.055172-45.903448-137.710345-105.931034L282.482759 48.551724l0 317.793103c0 21.186207-14.124138 35.310345-35.310345 35.310345l0 0c-21.186207 0-35.310345-14.124138-35.310345-35.310345l0-353.103448c0-21.186207 14.124138-35.310345 35.310345-35.310345l109.462069 0c14.124138-60.027586 70.62069-105.931034 137.710345-105.931034s120.055172 45.903448 137.710345 105.931034l399.006897 0c14.124138-60.027586 70.62069-105.931034 137.710345-105.931034s120.055172 45.903448 137.710345 105.931034L1377.103448-22.068966c21.186207 0 35.310345 14.124138 35.310345 35.310345l0 229.517241C1412.413793 331.034483 1320.606897 546.427586 1320.606897 546.427586zM494.344828-57.37931c-38.841379 0-70.62069 31.77931-70.62069 70.62069s31.77931 70.62069 70.62069 70.62069 70.62069-31.77931 70.62069-70.62069S533.186207-57.37931 494.344828-57.37931zM1165.241379-57.37931c-38.841379 0-70.62069 31.77931-70.62069 70.62069s31.77931 70.62069 70.62069 70.62069 70.62069-31.77931 70.62069-70.62069S1204.082759-57.37931 1165.241379-57.37931zM1341.793103 48.551724l-38.841379 0c-14.124138 60.027586-70.62069 105.931034-137.710345 105.931034-24.717241 0-49.434483-7.062069-70.62069-17.655172L1094.62069 507.586207l158.896552 0c0 0 24.717241-17.655172 28.248276-35.310345l-63.558621 0c-21.186207 0-35.310345-14.124138-35.310345-35.310345l0-141.241379c0-21.186207 14.124138-35.310345 35.310345-35.310345L1341.793103 260.413793C1341.793103 260.413793 1341.793103 48.551724 1341.793103 48.551724zM0 860.689655c0 21.186207 17.655172 35.310345 35.310345 35.310345l353.103448 0c21.186207 0 35.310345-14.124138 35.310345-35.310345 0-21.186207-17.655172-35.310345-35.310345-35.310345L35.310345 825.37931C14.124138 825.37931 0 839.503448 0 860.689655zM105.931034 684.137931c0 21.186207 14.124138 35.310345 35.310345 35.310345l247.172414 0c21.186207 0 35.310345-14.124138 35.310345-35.310345 0-21.186207-14.124138-35.310345-35.310345-35.310345L141.241379 648.827586C120.055172 648.827586 105.931034 662.951724 105.931034 684.137931zM211.862069 507.586207c0 21.186207 17.655172 35.310345 35.310345 35.310345l141.241379 0c17.655172 0 35.310345-14.124138 35.310345-35.310345 0-21.186207-17.655172-35.310345-35.310345-35.310345L247.172414 472.275862C225.986207 472.275862 211.862069 486.4 211.862069 507.586207z" horiz-adv-x="1413" />
</font>
</defs></svg>
This source diff could not be displayed because it is too large. You can view the blob instead.
{"version":3,"file":"bundle.min.js","sources":["webpack:///bundle.min.js"],"mappings":"AAAA","sourceRoot":""}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
{"version":3,"file":"vendor.min.js","sources":["webpack:///vendor.min.js"],"mappings":"AAAA","sourceRoot":""}
\ No newline at end of file
<!DOCTYPE html><html><head><base href="/"><title>麦乐 Erp 系统</title><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><link rel="shorticon icon" type="image/x-icon" href="/favicon.ico"></head><body><div id="root"></div><script type="text/javascript" src="/dist/vendor.min.js?0cf9658bc86f37e61b6c"></script><script type="text/javascript" src="/dist/bundle.min.js?0cf9658bc86f37e61b6c"></script></body></html>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>erp</artifactId>
<groupId>com.maile</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>erp-core</artifactId>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.0.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.0.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.0.3.RELEASE</version>
</dependency>
<!--<dependency>-->
<!--<groupId>org.springframework.boot</groupId>-->
<!--<artifactId>spring-boot-starter-actuator</artifactId>-->
<!--<version>2.0.3.RELEASE</version>-->
<!--</dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>2.0.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>2.0.3.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<dependency>
<groupId>org.apache.directory.studio</groupId>
<artifactId>org.apache.commons.lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<!--oss依赖-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>2.7.0</version>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
package com.maile.erp.core.entities;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
public class AdminRole {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "int(11)")
private long id;
@Column(columnDefinition = "int(11)")
private long adminUserId;
@Column(columnDefinition = "int(11)")
private long roleId;
}
package com.maile.erp.core.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import javax.persistence.*;
import java.util.List;
/**
* Created by IntelliJ IDEA.
* User: @Faith
* Date: 2018/7/24
* Time: 下午2:16
*/
@Entity
@Data
@DynamicInsert
@DynamicUpdate
@JsonIgnoreProperties({"handler", "hibernateLazyInitializer"})
@Table(indexes = {
@Index(columnList = "username")
})
public class AdminUser extends BaseEntity {
@Column(name = "`username`", columnDefinition = "varchar(64) not null default '' comment '用户名'")
private String username;
@JsonIgnore
@Column(name = "`password`", columnDefinition = "varchar(32) not null default '' comment '密码'")
private String password;
@Column(name = "`realname`", columnDefinition = "varchar(32) not null default '' comment '真实姓名'")
private String realname;
@Column(columnDefinition = "tinyint(11) not null default 0 comment '状态'")
private int status;
@Transient
private String tmpPwd = "******";
@Transient
private List<Long> roleId;
}
package com.maile.erp.core.entities;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
@Data
@MappedSuperclass
class BaseEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "int(11)")
private long id;
@Column(columnDefinition = "timestamp default CURRENT_TIMESTAMP comment '创建时间'", insertable = false, updatable = false)
private String createTime;
}
package com.maile.erp.core.entities;
import lombok.Data;
import javax.persistence.*;
@Data
@Entity
public class IdSequence {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "int(11)")
private long id;
}
package com.maile.erp.core.entities;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import javax.persistence.*;
@Entity
@Data
@DynamicInsert
@DynamicUpdate
@JsonIgnoreProperties({"handler", "hibernateLazyInitializer"})
@NamedNativeQueries({
@NamedNativeQuery(
name = "Permission.findByAdminUserId",
query = "select p.* " +
"from admin_user u " +
"LEFT JOIN admin_role ar on u.id = ar.admin_user_id " +
"LEFT JOIN role r on ar.role_id = r.id " +
"LEFT JOIN role_permission rp on rp.role_id=r.id " +
"LEFT JOIN permission p on p.id =rp.permission_id " +
"where u.id= ?1",
resultSetMapping = "userPermission"
)
})
@SqlResultSetMappings({
@SqlResultSetMapping(
name = "userPermission",
entities = @EntityResult(
entityClass = Permission.class
)
)
})
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "int(11)")
private long id;
@Column(columnDefinition = "int(11) not null default 0 comment '父节点ID'")
private long pid;
@Column(columnDefinition = "varchar(50) not null default '' comment '权限标识'")
private String identity;
@Column(columnDefinition = "varchar(50) not null default '' comment '权限名称'")
private String name;
@Column(columnDefinition = "varchar(50) not null default '' comment '权限别名,配置时显示'")
private String alias;
@Column(columnDefinition = "varchar(255) not null default '' comment '描述'")
private String description;
@Column(columnDefinition = "varchar(255) not null default '' comment '路径'")
private String path;
}
package com.maile.erp.core.entities;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Data
@DynamicInsert
@DynamicUpdate
@JsonIgnoreProperties({"handler", "hibernateLazyInitializer"})
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "int(11)")
private long id;
@Column(columnDefinition = "varchar(50) not null default '' comment '角色名称'")
private String name;
@Transient
List<Long> perms = new ArrayList<>();
}
package com.maile.erp.core.entities;
import lombok.Data;
import javax.persistence.*;
@Entity
@Data
public class RolePermission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "int(11)")
private long id;
@Column(columnDefinition = "int(11)")
private long roleId;
@Column(columnDefinition = "int(11)")
private long permissionId;
}
package com.maile.erp.core.libs;
import lombok.Data;
@Data
public class AppException extends Exception {
int code = 0;
public AppException(int code) {
this(code, "");
}
public AppException(int code, String message) {
super(message);
this.code = code;
}
public AppException(JSONResult result) {
this(result.getCode(), result.getMessage());
}
}
package com.maile.erp.core.libs;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* 类扫描器
*/
public class ClassScanner {
/**
* 从包package中获取所有的Class
*
* @param packageName
* @param recursive
* @return
* @throws Exception
*/
public Set<Class<?>> getClasses(String packageName, boolean recursive) {
// 第一个class类的集合
//List<Class<?>> classes = new ArrayList<Class<?>>();
Set<Class<?>> classes = new HashSet<>();
// 获取包的名字 并进行替换
String packageDirName = packageName.replace('.', '/');
// 定义一个枚举的集合 并进行循环来处理这个目录下的things
Enumeration<URL> dirs;
try {
dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
// 循环迭代下去
while (dirs.hasMoreElements()) {
// 获取下一个元素
URL url = dirs.nextElement();
// 得到协议的名称
String protocol = url.getProtocol();
// 如果是以文件的形式保存在服务器上
if ("file".equals(protocol)) {
// 获取包的物理路径
String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
// 以文件的方式扫描整个包下的文件 并添加到集合中,以下俩种方法都可以
//网上的第一种方法,
findAndAddClassesInPackageByFile(packageName, filePath, recursive, classes);
//网上的第二种方法
//addClass(classes,filePath,packageName);
} else if ("jar".equals(protocol)) {
// 如果是jar包文件
// 定义一个JarFile
JarFile jar;
try {
// 获取jar
jar = ((JarURLConnection) url.openConnection()).getJarFile();
// 从此jar包 得到一个枚举类
Enumeration<JarEntry> entries = jar.entries();
// 同样的进行循环迭代
while (entries.hasMoreElements()) {
// 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件
JarEntry entry = entries.nextElement();
String name = entry.getName();
// 如果是以/开头的
if (name.charAt(0) == '/') {
// 获取后面的字符串
name = name.substring(1);
}
// 如果前半部分和定义的包名相同
if (name.startsWith(packageDirName)) {
int idx = name.lastIndexOf('/');
// 如果以"/"结尾 是一个包
if (idx != -1) {
// 获取包名 把"/"替换成"."
packageName = name.substring(0, idx).replace('/', '.');
}
// 如果可以迭代下去 并且是一个包
if ((idx != -1) || recursive) {
// 如果是一个.class文件 而且不是目录
if (name.endsWith(".class") && !entry.isDirectory()) {
// 去掉后面的".class" 获取真正的类名
String className = name.substring(packageName.length() + 1, name.length() - 6);
try {
// 添加到classes
classes.add(Class.forName(packageName + '.' + className));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
return classes;
}
/**
* 以文件的形式来获取包下的所有Class
*
* @param packageName
* @param packagePath
* @param recursive
* @param classes
*/
public static void findAndAddClassesInPackageByFile(String packageName,
String packagePath, final boolean recursive, Set<Class<?>> classes) {
// 获取此包的目录 建立一个File
File dir = new File(packagePath);
// 如果不存在或者 也不是目录就直接返回
if (!dir.exists() || !dir.isDirectory()) {
// log.warn("用户定义包名 " + packageName + " 下没有任何文件");
return;
}
// 如果存在 就获取包下的所有文件 包括目录
File[] dirfiles = dir.listFiles(new FileFilter() {
// 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件)
public boolean accept(File file) {
return (recursive && file.isDirectory())
|| (file.getName().endsWith(".class"));
}
});
// 循环所有文件
if (dirfiles != null) {
for (File file : dirfiles) {
// 如果是目录 则继续扫描
if (file.isDirectory()) {
findAndAddClassesInPackageByFile(packageName + "."
+ file.getName(), file.getAbsolutePath(), recursive,
classes);
} else {
// 如果是java类文件 去掉后面的.class 只留下类名
String className = file.getName().substring(0,
file.getName().length() - 6);
try {
// 添加到集合中去
//classes.add(Class.forName(packageName + '.' + className));
//经过回复同学的提醒,这里用forName有一些不好,会触发static方法,没有使用classLoader的load干净
classes.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className));
} catch (ClassNotFoundException e) {
// log.error("添加用户自定义视图类错误 找不到此类的.class文件");
e.printStackTrace();
}
}
}
}
}
public void addClass(Set<Class<?>> classes, String filePath, String packageName) throws Exception {
File[] files = new File(filePath).listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory();
}
});
if (files != null) {
for (File file : files) {
String fileName = file.getName();
if (file.isFile()) {
String classsName = fileName.substring(0, fileName.lastIndexOf("."));
if (!StringUtils.isBlank(packageName)) {
classsName = packageName + "." + classsName;
}
doAddClass(classes, classsName);
}
}
}
}
public void doAddClass(Set<Class<?>> classes, final String classsName) throws Exception {
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return super.loadClass(name);
}
};
classes.add(classLoader.loadClass(classsName));
}
}
package com.maile.erp.core.libs;
public class Constants {
// 常量定义
public static final int COMMON_STATUS_NORMAL = 0;
}
package com.maile.erp.core.libs;
public class ErrorCode {
public final static int NO_DATA = 10000; //找不到数据
public final static int INVALID_PARAM = 10001; //参数错误
public final static int DUPLICATE_KEY = 10002; //数据重复
public final static int MISTAKE_PASSWORD = 10003; //缺少密码
public final static int STATUS_ERROR = 10004; //状态错误
public final static int SMS_CODE_ERROR = 10005; //短信验证码错误
public final static int PASSWORD_ERROR = 10006; //密码错误
public final static int NO_ACCESS = 10007; //没有权限
public final static int SMS_LIMIT = 10008; //短信发送频繁
public final static int SIGN_ERROR = 10009; //签名错误
public final static int LOW_STOCKS = 10010; //库存不足
public final static int API_FAILED = 10011; //库存不足
public final static int NOT_LOGIN = 10012; //尚未登录
public final static int BAD_CREDENTIALS = 10013; //账号/密码错误
public final static int USER_DISABLED = 10014; //账号/密码错误
public final static int NO_STOCK = 10015; //库存不足
public final static int FORBIDDEN = 10403; //账号/密码错误
public final static int SYSTEM_SETTING = 99997; //系统配置错误
public final static int SYSTEM_BUSY = 99998; //系统繁忙
public final static int UNKNOWN = 99999; //找不到数据
}
package com.maile.erp.core.libs;
import java.io.IOException;
import java.security.Key;
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import javax.crypto.spec.IvParameterSpec;
import org.apache.commons.lang.StringUtils;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
public class HuyiEncrypt {
public String des3EncodeCBC(String getKey, String getData) throws Exception {
byte[] data = getData.getBytes("UTF-8");
byte[] key = this.append(getKey, getKey.length(), 24).getBytes("UTF-8");
byte[] keyiv = this.substring(getKey, 0, 8).getBytes("UTF-8");
Key deskey = null;
DESedeKeySpec spec = new DESedeKeySpec(key);
SecretKeyFactory keyfactory = SecretKeyFactory.getInstance("desede");
deskey = keyfactory.generateSecret(spec);
Cipher cipher = Cipher.getInstance("desede" + "/CBC/PKCS5Padding");
IvParameterSpec ips = new IvParameterSpec(keyiv);
cipher.init(Cipher.ENCRYPT_MODE, deskey, ips);
byte[] bOut = cipher.doFinal(data);
return new BASE64Encoder().encode(bOut);
}
public String des3DecodeCBC(String getKey, String getData) throws Exception {
byte[] data = new BASE64Decoder().decodeBuffer(getData);
byte[] key = this.append(getKey, getKey.length(), 24).getBytes("UTF-8");
byte[] keyiv = this.substring(getKey, 0, 8).getBytes("UTF-8");
Key deskey = null;
DESedeKeySpec spec = new DESedeKeySpec(key);
SecretKeyFactory keyfactory = SecretKeyFactory.getInstance("desede");
deskey = keyfactory.generateSecret(spec);
Cipher cipher = Cipher.getInstance("desede" + "/CBC/PKCS5Padding");
IvParameterSpec ips = new IvParameterSpec(keyiv);
cipher.init(Cipher.DECRYPT_MODE, deskey, ips);
byte[] bOut = cipher.doFinal(data);
return new String(bOut, "UTF-8");
}
private String substring(String str, int start, int end) throws IOException {
int len = 0;
if (StringUtils.isNotBlank(str)) {
len = str.length();
}
if (len < end) {// ���Ȳ��������� 0
str = append(str, len, end).substring(start, end);
} else {
str = str.substring(start, end);
}
return str;
}
private String append(String str, int start, int end) throws IOException {
StringBuilder sb = new StringBuilder();
sb.append(str);
for (int i = start; i < end; i++) {
sb.append("0");
}
return sb.toString();
}
}
package com.maile.erp.core.libs;
import lombok.Data;
import org.springframework.data.domain.Page;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import java.util.HashMap;
import java.util.Map;
@Data
public class JSONResult {
static private String DEFAULT_MESSAGE_SUCCESS = "success";
static private String DEFAULT_MESSAGE_FAILED = "failed";
private int code = 0;
private Map<String, Object> data = new HashMap<>();
private String message = DEFAULT_MESSAGE_SUCCESS;
public JSONResult() {
}
public JSONResult(int code) {
if (code != 0) {
this.message = DEFAULT_MESSAGE_FAILED;
}
this.code = code;
}
// public JSONResult(Map data) {
// this(0, DEFAULT_MESSAGE_SUCCESS, data);
// }
public JSONResult(int code, String message) {
this.code = code;
this.message = message;
}
public JSONResult(int code, String message, Map<String, Object> data) {
this.code = code;
this.message = message;
this.data = data;
}
public JSONResult(AppException e) {
this.code = e.getCode();
this.message = e.getMessage();
}
public <T> JSONResult(Page<T> page, int currPage) {
this.put("list", page.getContent())
.put("total", page.getTotalElements())
.put("totalPages", page.getTotalPages())
.put("page", currPage);
}
public JSONResult(BindingResult result) {
Map<String, Object> errorMap = new HashMap<>();
for (FieldError err : result.getFieldErrors()) {
errorMap.put(err.getField(), err.getDefaultMessage());
}
this.code = ErrorCode.INVALID_PARAM;
this.message = "invalid params.";
this.data = errorMap;
}
public Boolean getSuccess() {
return code == 0;
}
public JSONResult put(String key, Object value) {
data.put(key, value);
return this;
}
public Object get(String key) {
return data.get(key);
}
}
package com.maile.erp.core.libs;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import org.apache.commons.codec.binary.Base64;
public class JuheEncrypt {
public static void main(String[] args) {
//加密后的内容
String s = "QPBNbRNDohmlvyzLSgCy1iwo+juaDACo";
//密码,用户名前8位,不足补0
String password = "testtest";
//打印解密后的结果
System.out.println(decrypt(s, password));
}
/**
* 解密
*
* @param srcMsg 密文
* @param password 密码
* @return 解密后的明文
*/
public static String decrypt(String srcMsg, String password) {
byte[] bb = Base64.decodeBase64(srcMsg.getBytes());
try {
// DES算法要求有一个可信任的随机数源
SecureRandom random = new SecureRandom();
// 创建一个DESKeySpec对象
DESKeySpec desKey = new DESKeySpec(password.getBytes());
// 创建一个密匙工厂
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
// 将DESKeySpec对象转换成SecretKey对象
SecretKey securekey = keyFactory.generateSecret(desKey);
// Cipher对象实际完成解密操作
Cipher cipher = Cipher.getInstance("DES");
// 用密匙初始化Cipher对象
cipher.init(Cipher.DECRYPT_MODE, securekey, random);
// 真正开始解密操作
byte[] decryResult = cipher.doFinal(bb);
return new String(decryResult);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 加密
*
* @param srcMsg 待加密字符串
* @param password 密码
* @return 加密后的密文
*/
public static String encrypt(String srcMsg, String password) {
try {
SecureRandom random = new SecureRandom();
DESKeySpec desKey = new DESKeySpec(password.getBytes());
// 创建一个密匙工厂,然后用它把DESKeySpec转换成
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey securekey = keyFactory.generateSecret(desKey);
// Cipher对象实际完成加密操作
Cipher cipher = Cipher.getInstance("DES");
// 用密匙初始化Cipher对象
cipher.init(Cipher.ENCRYPT_MODE, securekey, random);
// 现在,获取数据并加密
// 正式执行加密操作
byte[] result = cipher.doFinal(srcMsg.getBytes());
//Base64编码
byte[] resultBase = Base64.encodeBase64(result, true);
return new String(resultBase);
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}
}
package com.maile.erp.core.libs;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class RenderUtils {
public static void renderJson(HttpServletResponse response, Object object) throws IOException {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(object));
response.getWriter().close();
}
}
package com.maile.erp.core.libs;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.List;
public interface SearchBeforeSpecFilter<T> {
void process(List<Predicate> predicates, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}
package com.maile.erp.core.libs;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.List;
public interface SearchSpecFilter<T> {
Predicate process(List<Predicate> predicates, String field, String operator, String value, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}
package com.maile.erp.core.libs;
import org.apache.commons.lang.StringUtils;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.lang.Nullable;
import javax.persistence.Query;
import javax.persistence.criteria.*;
import javax.servlet.http.HttpServletRequest;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class SearchSpecification<T> implements Specification<T> {
private final String SEARCH_PREFIX = "search_";
private HttpServletRequest request;
private Map<String, SearchSpecFilter> filters = new HashMap<>();
private SearchBeforeSpecFilter beforeSpecFilter = null;
public SearchSpecification(HttpServletRequest request) {
this.request = request;
}
public void setBeforeFilter(SearchBeforeSpecFilter filter) {
this.beforeSpecFilter = filter;
}
public void setFilter(String filed, SearchSpecFilter filter) {
filters.put(filed, filter);
}
@Nullable
@Override
@SuppressWarnings("unchecked")
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<>();
Map<String, String[]> params = request.getParameterMap();
if (beforeSpecFilter != null) {
beforeSpecFilter.process(predicates, root, criteriaQuery, cb);
}
for (String name : params.keySet()) {
if (StringUtils.isBlank(params.get(name)[0]) || name.indexOf(SEARCH_PREFIX) != 0) {
continue;
}
String[] parts = name.split("_");
if (parts.length < 2) {
continue;
}
String value = StringUtils.trim(params.get(name)[0]);
String op;
String filed;
if (parts.length == 2) {
op = "EQ";
filed = StringUtils.trim(parts[1]);
} else {
op = StringUtils.trim(parts[1]);
filed = StringUtils.trim(parts[2]);
}
Object v = value;
if (parts.length >= 4) {
v = transformValue(value, parts[3]);
if (v == null) {
continue;
}
// switch (parts[3].toLowerCase()){
// case "begin":
// v1 = v;
// break;
// case "end":
// v2 = v;
// break;
// }
}
Predicate predicate = null;
if (filters.containsKey(name)) {
predicate = filters.get(name).process(predicates, filed, op, value, root, criteriaQuery, cb);
} else {
try {
Path path = root.get(filed);
switch (op.toUpperCase()) {
case "EQ":
predicate = cb.equal(path, v);
break;
case "LIKE":
predicate = cb.like(path, "%" + v + "%");
break;
case "GT":
predicate = cb.greaterThan(path, (Comparable) v);
break;
case "LT":
predicate = cb.lessThan(path, (Comparable) v);
break;
case "GTE":
predicate = cb.greaterThanOrEqualTo(path, (Comparable) v);
break;
case "LTE":
predicate = cb.lessThanOrEqualTo(path, (Comparable) v);
break;
// case "BW":
// if(v1 != null && v2 != null){
// predicate = cb.between(path, (Comparable) v1,(Comparable) v2);
// }
// break;
}
} catch (Exception e) {
continue;
}
}
if (predicate != null) {
predicates.add(predicate);
}
}
if (predicates.size() > 0) {
return cb.and((Predicate[]) predicates.toArray(new Predicate[predicates.size()]));
}
return null;
}
private Object transformValue(String value, String type) {
try {
switch (type.toLowerCase()) {
case "date":
case "time":
String pattern;
if (value.length() >= 19) {
pattern = "yyyy-MM-dd HH:mm:ss";
} else {
pattern = "yyyy-MM-dd";
}
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
return sdf.parse(value);
}
} catch (ParseException e) {
}
return null;
}
}
package com.maile.erp.core.pojos;
import com.maile.erp.core.utils.BeanUtils;
public class BasePojo {
public BasePojo() {
}
public BasePojo(Object source) {
BeanUtils.copyProperties(source, this);
}
}
package com.maile.erp.core.pojos;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GoodsTagsPojo {
private long goodsId;
private long tagsId;
}
package com.maile.erp.core.repositories;
import com.maile.erp.core.entities.AdminRole;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import java.util.Collection;
import java.util.List;
/**
* Created by IntelliJ IDEA.
* User: @Faith
* Date: 2018/8/9
* Time: 下午6:18
*/
public interface AdminRoleRepository extends JpaRepository<AdminRole, Long> {
List<AdminRole> findByAdminUserIdIn(Collection<Long> userIds);
@Modifying
@Query("delete from AdminRole a where a.adminUserId = ?1")
Integer deleteByAdminUserId(Long userId);
}
package com.maile.erp.core.repositories;
import com.maile.erp.core.entities.AdminUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
/**
* Created by IntelliJ IDEA.
* User: @Faith
* Date: 2018/7/24
* Time: 下午2:29
*/
public interface AdminUserRepository extends JpaRepository<AdminUser, Long>, JpaSpecificationExecutor<AdminUser> {
AdminUser findAdminUserByUsername(String username);
}
package com.maile.erp.core.repositories;
import com.maile.erp.core.entities.IdSequence;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import javax.transaction.Transactional;
public interface IdSequenceRepository extends JpaRepository<IdSequence, Long> {
@Query(value = "TRUNCATE TABLE id_sequence", nativeQuery = true)
@Modifying
@Transactional
void resetId();
}
package com.maile.erp.core.repositories;
import com.maile.erp.core.entities.Permission;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface PermissionRepository extends JpaRepository<Permission, Long>, JpaSpecificationExecutor<Permission> {
@Query(nativeQuery = true)
List<Permission> findByAdminUserId(long adminUserId);
}
package com.maile.erp.core.repositories;
import com.maile.erp.core.entities.RolePermission;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import java.util.Collection;
import java.util.List;
public interface RolePermissionRepository extends JpaRepository<RolePermission, Long> {
List<RolePermission> findByRoleIdIn(Collection<Long> rolesId);
List<RolePermission> findByRoleId(Long roleId);
@Modifying
@Query("delete from RolePermission rp where rp.roleId = ?1")
Integer deleteAllByRoleId(Long roleId);
}
package com.maile.erp.core.repositories;
import com.maile.erp.core.entities.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface RoleRepository extends JpaRepository<Role, Long>, JpaSpecificationExecutor<Role> {
Role findByName(String name);
}
package com.maile.erp.core.utils;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import org.springframework.beans.BeansException;
import org.springframework.cglib.beans.BeanMap;
import org.springframework.util.Assert;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
/**
* Created by bls on 2016/6/16.
*/
public class BeanUtils extends org.springframework.beans.BeanUtils {
public static void copyProperties(Object source, Object target) throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
Class<?> actualEditable = target.getClass();
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
for (PropertyDescriptor targetPd : targetPds) {
if (targetPd.getWriteMethod() != null) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null && sourcePd.getReadMethod() != null) {
try {
Method readMethod = sourcePd.getReadMethod();
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
// 这里判断以下value是否为空 当然这里也能进行一些特殊要求的处理 例如绑定时格式转换等等
if (value != null) {
Method writeMethod = targetPd.getWriteMethod();
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
}
} catch (Throwable ex) {
// throw new FatalBeanException("Could not copy properties from source to target", ex);
}
}
}
//
}
}
/**
* 将Bean转成json
*/
public static String BeanToJson(Object bdPushID){
Gson gson = new Gson();
String jsonBDID = gson.toJson(bdPushID);
return jsonBDID;
}
}
\ No newline at end of file
package com.maile.erp.core.utils;
import java.util.*;
public class CollectionUtils {
/**
* 查找列表里面某值的位置
*
* @param collection 列表
* @param filter 比较方法
* @param <T> 对象
* @return int
*/
public static <T> int indexOf(Collection<T> collection, Filter<T> filter) {
Iterator<T> i$ = collection.iterator();
int i = 0;
while (i$.hasNext()) {
if (filter.filter(i$.next())) {
return i;
}
i++;
}
return -1;
}
/**
* 查找列表里面是否含有某值
*
* @param collection 列表
* @param filter 比较方法
* @param <T> 对象
* @return boolean
*/
public static <T> boolean has(Collection<T> collection, Filter<T> filter) {
for (T o : collection) {
if (filter.filter(o)) {
return true;
}
}
return false;
}
/**
* 查找数组里面某值的位置
*
* @param collection 数组
* @param filter 比较方法
* @param <T> 对象
* @return int
*/
public static <T> int indexOf(T[] collection, Filter<T> filter) {
int i = 0;
while (i < collection.length) {
if (filter.filter(collection[i])) {
return i;
}
i++;
}
return -1;
}
/**
* 查找列表里面是否含有某值
*
* @param collection 数组
* @param filter 比较方法
* @param <T> 对象
* @return boolean
*/
public static <T> boolean has(T[] collection, Filter<T> filter) {
int i = 0;
while (i < collection.length) {
if (filter.filter(collection[i])) {
return true;
}
}
return false;
}
/**
* 将列表每一项经过处理后生成新的列表
*
* @param collection 列表
* @param mapper 处理方法
* @param <T> 旧对象
* @param <H> 新对象
* @return List<KeyType>
*/
public static <T, H> List<H> map(Collection<T> collection, Mapper<T, H> mapper) {
List<H> result = new ArrayList<>();
for (T o : collection) {
result.add(mapper.map(o));
}
return result;
}
/**
* 将数组每一项经过处理后生成新的列表
*
* @param collection 数组
* @param mapper 处理方法
* @param <T> 旧对象
* @param <H> 新对象
* @return List<KeyType>
*/
public static <T, H> List<H> map(T[] collection, Mapper<T, H> mapper) {
List<H> result = new ArrayList<>();
for (T o : collection) {
result.add(mapper.map(o));
}
return result;
}
/**
* 列表每一项进行判断,只要其中某一项判断成功返回true
*
* @param collection 列表
* @param filter 判定方法
* @param <T> 对象
* @return boolean
*/
public static <T> boolean every(Collection<T> collection, Filter<T> filter) {
for (T o : collection) {
if (filter.filter(o)) {
return true;
}
}
return false;
}
/**
* 数组每一项进行判断,只要其中某一项判断成功返回true
*
* @param collection 数组
* @param filter 判定方法
* @param <T> 对象
* @return boolean
*/
public static <T> boolean every(T[] collection, Filter<T> filter) {
for (T o : collection) {
if (filter.filter(o)) {
return true;
}
}
return false;
}
/**
* 列表取项的某个字段作为key生成新的map,返回原数据类型
*
* @param collection 列表
* @param filter 取字段方法
* @param <T> 对象
* @param <H> key的类型
* @return Map
*/
public static <T, H> Map<H, T> columnMap(Collection<T> collection, ColumnMapFilter<T, H> filter) {
Map<H, T> result = new HashMap<>();
for (T o : collection) {
result.put(filter.getKey(o), o);
}
return result;
}
public static <T, H, K> Map<H, K> columnMapFull(Collection<T> collection, ColumnMapFilter2<T, H, K> filter) {
Map<H, K> result = new HashMap<>();
for (T o : collection) {
result.put(filter.getKey(o), filter.getValue(o));
}
return result;
}
/**
* 数组取项的某个字段作为key生成新的map
*
* @param collection 数组
* @param filter 取字段方法
* @param <T> 对象
* @param <H> key的类型
* @return Map
*/
public static <T, H> Map<H, T> columnMap(T[] collection, ColumnMapFilter<T, H> filter) {
Map<H, T> result = new HashMap<>();
for (T o : collection) {
result.put(filter.getKey(o), o);
}
return result;
}
public static <T> List<T> filter(Collection<T> collection, Filter<T> filter) {
List<T> result = new ArrayList<>();
for (T o : collection) {
if (filter.filter(o)) {
result.add(o);
}
}
return result;
}
/**
* 拆分字符串,并将拆分的结果进行处理生成新的对象列表
*
* @param commaStr 逗号字符串
* @param regex 拆分的正则模式
* @param filter 处理方法
* @param <T> 新的对象类型
* @return List<SourceType>
*/
public static <T> List<T> parseSplitStr(String commaStr, String regex, SplitFilter<T> filter) {
List<T> result = new ArrayList<>();
String[] parts = commaStr.split(regex);
for (String o : parts) {
if (o.equals("")) {
continue;
}
result.add(filter.filter(o));
}
return result;
}
public static <SourceType, KeyType, TargetType> Map<KeyType, List<TargetType>> group(List<SourceType> list, GroupFilter<SourceType, KeyType, TargetType> filter) {
Map<KeyType, List<TargetType>> result = new HashMap<>();
for (SourceType source : list) {
if (!result.containsKey(filter.getKey(source))) {
result.put(filter.getKey(source), new ArrayList<>());
}
result.get(filter.getKey(source)).add(filter.getValue(source));
}
return result;
}
/**
* 一次性查询根据目标ID查询并生成映射(一对一)
*
* @param collection
* @param filter
* @param <SourceType>
* @param <TargetType>
* @param <TargetType>
* @return
*/
public static <SourceType, TargetType, KeyType> Map<KeyType, TargetType> getTargetMapFormList(Collection<SourceType> collection,
TargetMapFilter<SourceType, TargetType, KeyType> filter) {
Map<KeyType, TargetType> targetMap = new HashMap<>();
Set<KeyType> ids = new HashSet<>();
for (SourceType o : collection) {
KeyType tid = filter.collectTargetId(o);
if (tid != null) {
ids.add(tid);
}
}
List<TargetType> targets = filter.query(ids);
for (TargetType o : targets) {
targetMap.put(filter.targetMapKey(o), o);
}
for (SourceType o : collection) {
KeyType key = filter.collectTargetId(o);
filter.process(o, targetMap.get(key));
}
return targetMap;
}
/**
* 一次性查询根据目标ID查询并生成映射(一对多)
*
* @param collection
* @param filter
* @param <SourceType>
* @param <TargetType>
* @param <KeyType>
* @return
*/
public static <SourceType, TargetType, KeyType> Map<KeyType, TargetType> getTargetMapFormListForMany(Collection<SourceType> collection,
TargetMapFilterForMany<SourceType, TargetType, KeyType> filter) {
Map<KeyType, TargetType> targetMap = new HashMap<>();
Set<KeyType> ids = new HashSet<>();
for (SourceType o : collection) {
Collection<KeyType> tids = filter.collectTargetId(o);
if (tids != null) {
ids.addAll(tids);
}
}
List<TargetType> targets = filter.query(ids);
for (TargetType o : targets) {
targetMap.put(filter.targetMapKey(o), o);
}
for (SourceType o : collection) {
Collection<KeyType> keyList = filter.collectTargetId(o);
if (keyList != null) {
List<TargetType> oList = new ArrayList<>();
for (KeyType key : keyList) {
oList.add(targetMap.get(key));
}
filter.process(o, oList);
} else {
filter.process(o, new ArrayList<>());
}
}
return targetMap;
}
public static <T, H> H reduce(Collection<T> collection, H result, ReduceFilter<T, H> filter) {
for (T o : collection) {
result = filter.process(o, result);
}
return result;
}
public interface SplitFilter<T> {
T filter(String o);
}
public interface ColumnMapFilter<F, H> {
H getKey(F o);
}
public interface ColumnMapFilter2<F, H, T> {
H getKey(F o);
T getValue(F o);
}
public interface Filter<F> {
boolean filter(F o);
}
public interface Mapper<F, H> {
H map(F o);
}
public interface TargetMapFilter<SourceType, TargetType, KeyType> {
KeyType collectTargetId(SourceType source);
List<TargetType> query(Set<KeyType> ids);
KeyType targetMapKey(TargetType target);
void process(SourceType source, TargetType target);
}
public interface TargetMapFilterForMany<SourceType, TargetType, KeyType> {
Collection<KeyType> collectTargetId(SourceType source);
List<TargetType> query(Set<KeyType> ids);
KeyType targetMapKey(TargetType target);
void process(SourceType source, List<TargetType> target);
}
public interface ReduceFilter<T, H> {
H process(T target, H result);
}
public interface GroupFilter<SourceType, KeyType, TargetType> {
KeyType getKey(SourceType source);
TargetType getValue(SourceType source);
}
}
package com.maile.erp.core.utils;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.*;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Created by Nekoing on 2016/6/27.
*/
@Slf4j
public class HttpUtils {
final static public String CHARSET = "UTF-8";
final static private int httpTimeOut = 30000;
static public Object get(String url) throws IOException {
return get(url, null, null, true);
}
static public <T> Object get(String url, Map<String, T> params) throws IOException {
return get(url, params, null, true);
}
static public <T> Object get(String url, Map<String, T> params, Map<String, String> headers, boolean json) throws IOException {
if (params != null && params.size() > 0) {
url = urlCatParams(url, HttpUtils.buildQuery(params, CHARSET));
}
log.debug("http request: " + url);
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
return doQuery(httpClient, httpGet, headers, json);
}
static public <T> Object post(String url, String body, boolean json) throws IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(url);
HttpEntity entity = new StringEntity(body);
httpPost.setEntity(entity);
return doQuery(httpClient, httpPost, json);
}
static public <T> Object post(String url, Map<String, T> params) throws IOException {
return post(url, params, true);
}
static public <T> Object post(String url, Map<String, T> params, boolean json) throws IOException {
log.debug("http request: " + url + "; " + HttpUtils.buildQuery(params, CHARSET));
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(url);
if (params != null && params.size() > 0) {
List<NameValuePair> paramsList = new ArrayList<NameValuePair>();
for (String key : params.keySet()) {
paramsList.add(new BasicNameValuePair(key, String.valueOf(params.get(key))));
}
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramsList, CHARSET);
httpPost.setEntity(entity);
}
return doQuery(httpClient, httpPost, json);
}
static public Object postXml(String url, String xml, boolean json) throws Exception {
RequestConfig defaultRequestConfig = RequestConfig.custom()
.setSocketTimeout(httpTimeOut)
.setConnectTimeout(httpTimeOut)
.setConnectionRequestTimeout(httpTimeOut)
.build();
SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
return true;
}
}).build();
CloseableHttpResponse response = null;
String result = null;
CloseableHttpClient httpClient = HttpClients.custom().setSSLContext(sslContext).
setSSLHostnameVerifier(new NoopHostnameVerifier()).setDefaultRequestConfig(defaultRequestConfig).build();
HttpPost httpPost = new HttpPost(url);
StringEntity entity = new StringEntity(xml);
httpPost.setEntity(entity);
httpPost.setHeader("Content-Type", "text/xml;charset=ISO-8859-1");
response = httpClient.execute(httpPost);
if (response.getStatusLine().getStatusCode() != 200) {
throw new IOException("HttpUtils status error, code: " + response.getStatusLine().getStatusCode());
}
HttpEntity ResponseEntity = response.getEntity();
result = EntityUtils.toString(ResponseEntity, CHARSET);
httpClient.close();
return result;
}
static public String urlCatParams(String url, String paramsStr) {
if (url.indexOf('?') > 0) {
return url + "&" + paramsStr;
} else {
return url + "?" + paramsStr;
}
}
static protected Object doQuery(CloseableHttpClient httpClient, HttpUriRequest request, boolean json) throws IOException {
return doQuery(httpClient, request, null, json);
}
static protected Object doQuery(CloseableHttpClient httpClient, HttpUriRequest request, Map<String, String> headers, boolean json) throws IOException {
CloseableHttpResponse response = null;
String result = null;
try {
if (headers != null) {
for (Map.Entry<String, String> header : headers.entrySet()) {
request.setHeader(header.getKey(), header.getValue());
}
}
// request.setHeader("Content-Type", "text/html;charset=" + CHARSET);
response = httpClient.execute(request);
if (response.getStatusLine().getStatusCode() != 200) {
throw new IOException("HttpUtils status error, code: " + response.getStatusLine().getStatusCode());
}
HttpEntity entity = response.getEntity();
result = EntityUtils.toString(entity, CHARSET);
log.debug("HTTP Response:" + result);
if (json) {
return new Gson().fromJson(result, new TypeToken<HashMap<String, Object>>() {
}.getType());
} else {
return result;
}
} catch (IOException e) {
throw e;
} finally {
try {
if (response != null) {
response.close();
}
} catch (IOException ignore) {
}
}
}
static public <T> String buildQuery(Map<String, T> params) {
return buildQuery(params, "UTF-8", true);
}
static public <T> String buildQuery(Map<String, T> params, boolean encode) {
return buildQuery(params, "UTF-8", encode);
}
static public <T> String buildQuery(Map<String, T> params, String charset) {
return buildQuery(params, charset, true);
}
static public <T> String buildQuery(Map<String, T> params, String charset, boolean encode) {
String result = "";
if (params == null || params.size() == 0) {
return result;
}
for (String key : params.keySet()) {
try {
if (encode) {
result += URLEncoder.encode(key, charset) + "=" + URLEncoder.encode(String.valueOf(params.get(key)), charset) + "&";
} else {
result += key + "=" + String.valueOf(params.get(key)) + "&";
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
result = result.substring(0, result.length() - 1);
return result;
}
}
package com.maile.erp.core.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class LoggerUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
private LoggerUtils() {}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(LoggerUtils.applicationContext == null) {
LoggerUtils.applicationContext = applicationContext;
}
}
//获取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
//通过name获取 Bean.
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
//通过class获取Bean.
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}
//通过name,以及Clazz返回指定的Bean
public static <T> T getBean(String name,Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
}
/**
* 获取客户端ip地址
* @param request
* @return
*/
public static String getCliectIp(HttpServletRequest request)
{
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.trim() == "" || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.trim() == "" || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.trim() == "" || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个路由时,取第一个非unknown的ip
final String[] arr = ip.split(",");
for (final String str : arr) {
if (!"unknown".equalsIgnoreCase(str)) {
ip = str;
break;
}
}
return ip;
}
}
package com.maile.erp.core.utils;
public class MethodType {
public final static String INSERT="新增";
public final static String UPDATE="修改";
public final static String DELETE="删除";
}
package com.maile.erp.core.utils;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PutObjectResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.net.URL;
import java.util.Date;
import java.util.Random;
/**
* OSS工具类
*/
public class OSSClientUtils {
// 本地异常日志
public static final Logger logger = LoggerFactory.getLogger(OSSClientUtils.class);
// 存储空间
private String bucketName;
// 文件存储目录
private String fileDir;
private OSSClient ossClient;
public OSSClientUtils(String endpoint,String accessKeyId,String accessKeySecret,String bucketName,String fileDir) {
ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);
this.bucketName = bucketName;
this.fileDir = fileDir;
}
/**
* 销毁
*/
public void destory() {
ossClient.shutdown();
}
/**
* 上传图片
*
* @param url 图片上传路径
*/
public void uploadImg2OSS(String url) {
File file = new File(url);
FileInputStream fileInputStream;
try {
fileInputStream = new FileInputStream(file);
String[] split = url.split("/");
this.uploadFile2OSS(fileInputStream, split[split.length - 1]);
} catch (FileNotFoundException e) {
logger.error("图片上传失败:" + e.getMessage());
}
}
public String uploadImg2Oss(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
String substring = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
Random random = new Random();
String name = random.nextInt(10000) + System.currentTimeMillis() + substring;
try {
InputStream inputStream = file.getInputStream();
this.uploadFile2OSS(inputStream, name);
} catch (Exception e) {
logger.error("图片上传失败:" + e.getMessage());
}
return name;
}
/**
* 上传到OSS服务器,如果同名文件或覆盖
*
* @param inputStream 文件流
* @param filename 文件名称,包括后缀
* @return 出错返回 "" ,正确返回MD5数字签名
*/
private String uploadFile2OSS(InputStream inputStream, String filename) {
String ret = "";
try {
// 创建上传的Object的Metadata
ObjectMetadata objectMetadata = new ObjectMetadata();
// 上传的文件的长度
objectMetadata.setContentLength(inputStream.available());
// 指定该Object被下载时的网页的缓存行为
objectMetadata.setCacheControl("not-cache");
// 指定该Object下设置Header
objectMetadata.setHeader("Pragma", "no-cache");
//指定该Object被下载时的内容编码格式
objectMetadata.setContentEncoding("utf-8");
//文件的MIME,定义文件的类型及网页编码,决定浏览器将以什么形式、什么编码读取文件。如果用户没有指定则根据Key或文件名的扩展名生成,
//如果没有扩展名则填默认值application/octet-stream
objectMetadata.setContentType(getContentType(filename.substring(filename.lastIndexOf("."))));
//指定该Object被下载时的名称(指示MINME用户代理如何显示附加的文件,打开或下载,及文件名称)
objectMetadata.setContentDisposition("inline;filename=" + filename);
// 上传文件
PutObjectResult putObjectRequest = ossClient.putObject(bucketName, fileDir + filename, inputStream, objectMetadata);
// 解析结果
ret = putObjectRequest.getETag();
} catch (IOException e) {
logger.error("IO异常:" + e.getMessage());
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return ret;
}
/**
* 判断OSS服务文件上传时文件的contentType
*
* @param filenameExtension 文件后缀
* @return String
*/
private static String getContentType(String filenameExtension) {
if (filenameExtension.equalsIgnoreCase("bmp")) {
return "image/bmp";
}
if (filenameExtension.equalsIgnoreCase("gif")) {
return "image/gif";
}
if (filenameExtension.equalsIgnoreCase("jpeg") || filenameExtension.equalsIgnoreCase("jpg")
|| filenameExtension.equalsIgnoreCase("png")) {
return "image/jpeg";
}
if (filenameExtension.equalsIgnoreCase("html")) {
return "text/html";
}
if (filenameExtension.equalsIgnoreCase("txt")) {
return "text/plain";
}
if (filenameExtension.equalsIgnoreCase("vsd")) {
return "application/vnd.visio";
}
if (filenameExtension.equalsIgnoreCase("pptx") || filenameExtension.equalsIgnoreCase("ppt")) {
return "application/vnd.ms-powerpoint";
}
if (filenameExtension.equalsIgnoreCase("docx") || filenameExtension.equalsIgnoreCase("doc")) {
return "application/msword";
}
if (filenameExtension.equalsIgnoreCase("xml")) {
return "text/xml";
}
return "image/jpeg";
}
/**
* 获得图片路径
*
* @param fileUrl
* @return
*/
public String getImgUrl(String fileUrl) {
if (!StringUtils.isEmpty(fileUrl)) {
String[] split = fileUrl.split("/");
return this.getUrl(this.fileDir + split[split.length - 1]);
}
return null;
}
/**
* 获得url链接
*
* @param key
* @return
*/
private String getUrl(String key) {
// 设置URL过期时间为10年 3600l* 1000*24*365*10
Date expiration = new Date(System.currentTimeMillis() + 3600L * 1000 * 24 * 365 * 10);
// 生成URL
URL url = ossClient.generatePresignedUrl(bucketName, key, expiration);
if (url != null) {
return url.toString().split("\\?")[0];
}
return null;
}
}
package com.maile.erp.core.utils;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import org.springframework.cglib.beans.BeanMap;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public class ObjectMapUtils {
/**
* 将对象装换为map
*
* @param bean
* @return
*/
public static <T> Map<String, Object> beanToMap(T bean) {
Map<String, Object> map = Maps.newHashMap();
if (bean != null) {
BeanMap beanMap = BeanMap.create(bean);
for (Object key : beanMap.keySet()) {
map.put(key + "", beanMap.get(key));
}
}
return map;
}
/**
* 将map装换为javabean对象
*
* @param map
* @param bean
* @return
*/
public static <T> T mapToBean(Map<String, Object> map, T bean) {
BeanMap beanMap = BeanMap.create(bean);
beanMap.putAll(map);
return bean;
}
/**
* 将List<T>转换为List<Map<String, Object>>
*
* @param objList
* @return
* @throws JsonGenerationException
* @throws JsonMappingException
* @throws IOException
*/
public static <T> List<Map<String, Object>> objectsToMaps(List<T> objList) {
List<Map<String, Object>> list = Lists.newArrayList();
if (objList != null && objList.size() > 0) {
Map<String, Object> map = null;
T bean = null;
for (int i = 0, size = objList.size(); i < size; i++) {
bean = objList.get(i);
map = beanToMap(bean);
list.add(map);
}
}
return list;
}
/**
* 将List<Map<String,Object>>转换为List<T>
*
* @param maps
* @param clazz
* @return
* @throws InstantiationException
* @throws IllegalAccessException
*/
public static <T> List<T> mapsToObjects(List<Map<String, Object>> maps, Class<T> clazz) throws InstantiationException, IllegalAccessException {
List<T> list = Lists.newArrayList();
if (maps != null && maps.size() > 0) {
Map<String, Object> map = null;
T bean = null;
for (int i = 0, size = maps.size(); i < size; i++) {
map = maps.get(i);
bean = clazz.newInstance();
mapToBean(map, bean);
list.add(bean);
}
}
return list;
}
/**
*将map转成json字符串
*/
public static <T> String mapToJson(Map<String, T> map) {
Gson gson = new Gson();
String jsonStr = gson.toJson(map);
return jsonStr;
}
}
package com.maile.erp.core.utils;
import org.apache.commons.lang.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
public class RequestUtils {
@SuppressWarnings("unchecked")
public static <T> Map<String, T> getParams(HttpServletRequest request) {
Map<String, T> result = new HashMap<>();
Map<String, String[]> ps = request.getParameterMap();
for (String k : ps.keySet()) {
String[] v = ps.get(k);
if (v == null) {
result.put(k, null);
} else {
result.put(k, (T) StringUtils.trim(v[0]));
}
}
return result;
}
}
package com.maile.erp.core.utils;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.logging.log4j.util.Strings;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class SignatureUtils {
public static String getSign(Map<String, ?> params, String secret, String secretName) {
List<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
String signStr = "";
for (String key : keys) {
String value = String.valueOf(params.get(key));
if (Strings.isNotBlank(value)) {
signStr += key + "=" + String.valueOf(params.get(key)) + "&";
}
}
signStr += secretName + "=" + secret;
return DigestUtils.md5Hex(signStr).toLowerCase();
}
}
package com.maile.erp.core.utils;
import com.maile.erp.core.entities.IdSequence;
import com.maile.erp.core.repositories.IdSequenceRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Calendar;
@Service
public class SnUtils {
@Autowired
IdSequenceRepository idSequenceRepository;
private final long MAX_ID = 99900;
/**
* 生成订单号
*
* @param code 业务代码
* @return 订单号
*/
public String buildSn(BusinessCode code) {
return buildSn(code, 12);
}
public String buildSn(BusinessCode code, int length) {
Calendar calendar = Calendar.getInstance();
String orderSn = String.format("%d%02d%02d%02d",
code.value, //长度1
calendar.get(Calendar.YEAR), //长度2
calendar.get(Calendar.MONTH), //长度2
calendar.get(Calendar.DAY_OF_MONTH) //长度2
);
int sLen = length - orderSn.length();
orderSn += String.format("%0" + sLen + "d", getLatestId());
return orderSn;
}
private long getLatestId() {
IdSequence idSequence = new IdSequence();
idSequenceRepository.save(idSequence);
if (idSequence.getId() >= MAX_ID) {
idSequenceRepository.resetId();
}
//每涨到1000个就清空
if (idSequence.getId() % 1000 == 0) {
idSequenceRepository.deleteAll();
}
return idSequence.getId();
}
public enum BusinessCode {
SHOPPING_ORDER(1),
AFTER_SALE_ORDER(2);
int value;
BusinessCode(int index) {
this.value = index;
}
}
}
自定义原生查询结果绑定到指定类上
--
1. 创建接收结果的类(Pojo),设置一个无惨构造函数和一个全参构造函数
2. 在实体类上添加@NamedNativeQuery和@SqlResultSetMapping注解,NamedNativeQuery的名字为“实体类名.Repository方法名”,设置resultSetMapping为体SqlResultSetMapping的name,具体查看案例
3. 在实体类对应的Repository类上添加对应的方法(NamedNativeQuery的名字,不含实体类名),添加@Query注解,设置nativeQuery值为true
4. 返回类型为Pojo类或者Pojo类的列表
案例:
```java
//Pojo类
@JsonIgnoreProperties({"handler", "hibernateLazyInitializer"})
@Data
public class AccessAppPojo {
private String name;
@JsonIgnore
private String desc;
public AccessAppPojo() {
}
public AccessAppPojo(String name, String desc) {
this.name = name;
this.desc = desc;
}
}
```
```java
//实体类
@Entity
@Data
@DynamicInsert
@DynamicUpdate
@JsonIgnoreProperties({"handler", "hibernateLazyInitializer"})
@NamedNativeQuery(
name = Application,
query = "select name, `desc` from access_app as a where a.id = 1 limit 1",
resultSetMapping = "gg"
)
@SqlResultSetMapping(
name = "gg",
classes = @ConstructorResult(
targetClass = AccessAppPojo.class,
columns = {
@ColumnResult(name = "name"),
@ColumnResult(name = "desc")
}
)
)
public class AccessApp extends BaseEntity {
@Column(columnDefinition = "varchar(32) not null default '' comment '应用名称'")
private String name;
@Column(name = "`desc`", columnDefinition = "varchar(255) not null default '' comment '应用描述'")
private String desc;
@Column(name = "`key`", columnDefinition = "varchar(32) not null default '' comment '应用key'")
private String key;
@Column(columnDefinition = "varchar(32) not null default '' comment '应用secret'")
private String secret;
@Column(columnDefinition = "tinyint(11) not null default 0 comment '状态'")
private int status;
}
```
```java
//Repository类
public interface AccessAppRepository extends JpaRepository<AccessApp, Long> {
@Query(nativeQuery = true)
AccessAppPojo test();
}
```
```java
//实际运用
@RestController
public class WelcomeController {
@Autowired
AccessAppRepository accessAppRepository;
@RequestMapping("/")
public JSONResult index() throws AppException {
AccessAppPojo app = accessAppRepository.test();
return new JSONResult().put("app", app);
}
}
```
\ No newline at end of file
#售后系统流程图
![img](afterSale.png)
\ No newline at end of file
#订单系统流程图
![img](order.png)
\ No newline at end of file
框架相关技术文档
--
本目录包含了一些框架上的技术文档
\ No newline at end of file
{
"lockfileVersion": 1
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.maile</groupId>
<artifactId>erp</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>admin-api</module>
<module>core</module>
</modules>
<name>maile-erp</name>
<description>Maile ERP System</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
{
"presets": [
"latest",
"stage-0",
"react"
]
}
\ No newline at end of file
{
// "parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 7, // ECMAScript版本,7为ES7
"sourceType": "module", //默认script,如果代码是ECMAScript模块,设置为module
"ecmaFeatures": { // 使用额外的语言特性
"jsx": true // 启用JSX
}
},
"extends": "airbnb",
"plugins": [
"react"
],
"rules": {
"indent": ["error", 4],
"comma-dangle": ["error", "never"]
}
}
\ No newline at end of file
### 开发环境部署
- nmp i # 安装依赖包
- npm run dev 启动项目
- 访问 http://localhost:8080
<!DOCTYPE html>
<html>
<head>
<base href='/' />
<!-- ejs语法 -->
<title><%= htmlWebpackPlugin.options.title %></title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<!-- 设置favicon -->
<% if (htmlWebpackPlugin.options.favIcon && htmlWebpackPlugin.options.favIcon.length > 0) { %>
<link rel="shorticon icon" type="image/x-icon" href="<%= htmlWebpackPlugin.options.favIcon %>">
<% } %>
</head>
<body>
<div id="root"></div>
</body>
<!-- 是否是开发环境? -->
<% if (htmlWebpackPlugin.options.devMode) { %>
<script src="http://localhost:8080/webpack-dev-server.js"></script>
<% } %>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "react-admin",
"private": true,
"version": "1.0.0",
"description": "React Admin Boilerplate, with React + Ant Design",
"author": "",
"engines": {
"node": ">=5.0 <7",
"npm": ">=3.3 <4"
},
"keywords": [
"ant",
"react",
"admin",
"frontend"
],
"scripts": {
"build": "webpack --progress --colors",
"prod": "cross-env NODE_ENV=production webpack --progress --colors --config webpack.config.prod.js -p",
"clean": "rimraf dist/*bundle* dist/*vendor* dist/*index*",
"dev": "webpack-dev-server --devtool eval --progress --colors --content-base dist --hot --inline",
"eslint": "eslint --ext .js,.jsx src",
"stylelint": "stylelint \"src/**/*.css\"",
"lesshint": "lesshint src/"
},
"devDependencies": {
"antd": "^3.4.3",
"babel-core": "6.26.0",
"babel-eslint": "7.2.3",
"babel-loader": "7.1.2",
"babel-plugin-import": "1.1.0",
"babel-polyfill": "^6.20.0",
"babel-preset-env": "^1.7.0",
"babel-preset-latest": "^6.16.0",
"babel-preset-react": "^6.16.0",
"babel-preset-stage-0": "^6.16.0",
"compression-webpack-plugin": "0.3.2",
"create-react-class": "^15.6.3",
"cross-env": "3.1.4",
"css-loader": "0.28.7",
"eslint": "2.7.0",
"eslint-config-airbnb": "6.x",
"eslint-plugin-react": "4.x",
"extract-text-webpack-plugin": "3.0.2",
"file-loader": "0.9.0",
"html-webpack-plugin": "2.29.0",
"install": "^0.12.1",
"less": "^2.7.3",
"less-loader": "^4.1.0",
"lesshint": "2.0.2",
"prop-types": "^15.6.2",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-hot-loader": "^4.0.0",
"redux": "^3.7.2",
"redux-logger": "2.7.4",
"redux-thunk": "2.1.0",
"rimraf": "2.5.4",
"strip-loader": "0.1.2",
"style-loader": "0.19.0",
"stylelint": "6.6.0",
"stylelint-config-standard": "9.0.0",
"url-loader": "^0.5.9",
"webpack": "3.8.1",
"webpack-dev-server": "2.9.4"
},
"dependencies": {
"braft-editor": "^1.9.8",
"china-division": "^1.1.0",
"classnames": "^2.2.6",
"collect.js": "^4.0.22",
"react-redux": "^4.4.0",
"react-router": "^3.0.0",
"superagent": "2.3.0",
"webpack-dev-server": "^2.9.4"
}
}
import React from 'react';
import {connect} from 'react-redux';
import {Link} from 'react-router';
import {bindActionCreators} from 'redux';
import {Spin, message, Tabs, Icon} from 'antd';
import Header from '../Header';
import Footer from '../Footer';
import Sidebar from '../Sidebar';
import Login from '../Login';
import Breadcrumb from '../Breadcrumb';
import Welcome from '../Welcome';
import './index.less';
import globalConfig from '../../config.js';
import ajax from '../../utils/ajax';
import Logger from '../../utils/Logger';
import sidebarMenu, {headerMenu} from '../../menu.js';
import {loginSuccessCreator} from '../../redux/Login.js';
const TabPane = Tabs.TabPane;
const logger = Logger.getLogger('App');
/**
* App组件
* 定义整个页面的布局
*/
class App extends React.Component {
// App组件还是不要做成PureComponent了, 可能会有bug, 因为无法要求所有子组件都是pure的
// 要清楚登录逻辑:
// 1. 初始化时, 先尝试获取已登录的用户, 因为可能还留着上次登录的cookie
// 2. 如果当前没有登录, 就跳转到Login组件, 手动输入用户名密码重新登录
// 3. Login组件中登录成功后, 会触发一个loginSuccess action, 修改redux中的状态, 进而触发App组件的re-render
state = {
tryingLogin: true, // App组件要尝试登录, 在屏幕正中显示一个正加载的动画
// tab模式相关的状态
currentTabKey: '', // 当前激活的是哪个tab
tabPanes: [] // 当前总共有哪些tab
};
/**
* 组件挂载之前判断是否要更新tab
*/
componentWillMount() {
// 如果不是tab模式直接返回
if (globalConfig.tabMode.enable !== true) {
return;
}
this.tabTitleMap = this.parseTabTitle();
this.updateTab(this.props);
}
/**
* 每次在react-router中切换时也要判断是否更新tab
*/
componentWillReceiveProps(nextProps) {
// 如果不是tab模式直接返回
if (globalConfig.tabMode.enable !== true) {
return;
}
// FIXME: hack, 在react-router中切换时会触发这个方法两次, 据说是和hashHistory有关, 需要手动处理下
const action = this.props.location.action;
if (action === 'PUSH') { // action有PUSH、POP、REPLACE等几种, 不太清楚分别是做什么用的
return;
}
// FIXME: hack, 因为要区分react-router引起的re-render和redux引起的re-render
if (this.props.collapse === nextProps.collapse) {
this.updateTab(nextProps);
}
}
/**
* App组件挂载后要先尝试去服务端获取已登录的用户
*/
async componentDidMount() {
if (!this.props.login) {
//const hide = message.loading('正在获取用户信息...', 0);
try {
// 先去服务端验证下, 说不定已经登录了
const res = await ajax.getCurrentUser();
//hide();
// 注意这里, debug模式下每次刷新都必须重新登录
if (res.success && !globalConfig.debug) {
// 这里不需要setState了, 因为setState的目的是为了re-render, 而下一句会触发redux的状态变化, 也会re-render
// 所以直接修改状态, 就是感觉这么做有点奇怪...
this.state.tryingLogin = false;
// App组件也可能触发loginSuccess action
this.props.handleLoginSuccess(res.data.user, res.data.permission);
} else {
this.setState({tryingLogin: false});
//this.handleLoginError('获取用户信息失败, 请重新登录');
}
} catch (e) {
// 如果网络请求出错, 弹出一个错误提示
logger.error('getCurrentUser error, %o', e);
this.handleLoginError(`网络请求出错: ${e.message}`);
}
}
}
handleLoginError(errorMsg) {
// 如果服务端说没有登录, 就要跳转到sso或者login组件
if (globalConfig.isSSO() && !globalConfig.debug) {
// debug模式不支持调试单点登录
// 因为没有单点登录的地址啊...跳不回来
logger.debug('not login, redirect to SSO login page');
const redirect = encodeURIComponent(window.location.href);
window.location.href = `${globalConfig.login.sso}${redirect}`;
} else {
message.error(errorMsg);
logger.debug('not login, redirect to Login component');
this.setState({tryingLogin: false});
}
}
// 下面开始是tab相关逻辑
/**
* 解析menu.js中的配置, 找到所有叶子节点对应的key和名称
*
* @returns {Map}
*/
parseTabTitle() {
const tabTitleMap = new Map();
const addItem = item => {
if (item.url) { // 对于直接跳转的菜单项, 直接忽略, 只有headerMenu中可能有这种
return;
}
if (item.icon) {
tabTitleMap.set(item.key, <span className="ant-layout-tab-text"><Icon
type={item.icon}/>{item.name}</span>);
} else {
tabTitleMap.set(item.key, <span className="ant-layout-tab-text">{item.name}</span>);
}
};
const browseMenu = item => {
if (item.child) {
item.child.forEach(browseMenu);
} else {
addItem(item);
}
};
// 又是dfs, 每次用js写这种就觉得很神奇...
sidebarMenu.forEach(browseMenu);
headerMenu.forEach(browseMenu);
// 最后要手动增加一个key, 对应于404页面
tabTitleMap.set('*', <span className="ant-layout-tab-text"><Icon type="frown-o"/>Error</span>);
return tabTitleMap;
}
/**
* 根据传入的props决定是否要新增一个tab
*
* @param props
*/
updateTab(props) {
const routes = props.routes;
let key = routes[routes.length - 1].path; // react-router传入的key
// 如果key有问题, 就直接隐藏所有tab, 这样简单点
if (!key || !this.tabTitleMap.has(key)) {
this.state.tabPanes.length = 0;
return;
}
const tabTitle = this.tabTitleMap.get(key);
// 如果允许同一个组件在tab中多次出现, 每次就必须生成唯一的key
if (globalConfig.tabMode.allowDuplicate === true) {
if (!this.tabCounter) {
this.tabCounter = 0;
}
this.tabCounter++;
key = key + this.tabCounter;
}
// 更新当前选中的tab
this.state.currentTabKey = key;
// 当前key对应的tab是否已经在显示了?
let exist = false;
for (const pane of this.state.tabPanes) {
if (pane.key === key) {
exist = true;
break;
}
}
// 如果key不存在就要新增一个tabPane
if (!exist) {
this.state.tabPanes.push({
key,
title: tabTitle,
//content: React.cloneElement(props.children), // 我本来是想clone一下children的, 这样比较保险, 不同tab不会互相干扰, 但发现似乎不clone也没啥bug
content: props.children
});
}
}
/**
* 改变tab时的回调
*/
onTabChange = (activeKey) => {
this.setState({currentTabKey: activeKey});
};
/**
* 关闭tab时的回调
*/
onTabRemove = (targetKey) => {
// 如果关闭的是当前tab, 要激活哪个tab?
// 首先尝试激活左边的, 再尝试激活右边的
let nextTabKey = this.state.currentTabKey;
if (this.state.currentTabKey === targetKey) {
let currentTabIndex = -1;
this.state.tabPanes.forEach((pane, i) => {
if (pane.key === targetKey) {
currentTabIndex = i;
}
});
// 如果当前tab左边还有tab, 就激活左边的
if (currentTabIndex > 0) {
nextTabKey = this.state.tabPanes[currentTabIndex - 1].key;
}
// 否则就激活右边的tab
else if (currentTabIndex === 0 && this.state.tabPanes.length > 1) {
nextTabKey = this.state.tabPanes[currentTabIndex + 1].key;
}
// 其实还有一种情况, 就是只剩最后一个tab, 但这里不用处理
}
// 过滤panes
const newTabPanes = this.state.tabPanes.filter(pane => pane.key !== targetKey);
this.setState({tabPanes: newTabPanes, currentTabKey: nextTabKey});
};
/**
* 渲染界面右侧主要的操作区
*/
renderBody() {
// 我本来是在jsx表达式中判断globalConfig.tabMode.enable的, 比如{globalConfig.tabMode.enable && XXX}
// 后来想会不会拿到外面去判断好些, webpack会不会把这个语句优化掉? 好像有一些类似的机制
// 因为在编译的时候, globalConfig.tabMode.enable的值已经是确定的了, 下面的if-else其实是可以优化的
// 如果是jsx表达式那种写法, 感觉不太可能优化
// tab模式下, 不显示面包屑
if (globalConfig.tabMode.enable === true) {
// 如果没有tab可以显示, 就显示欢迎界面
if (this.state.tabPanes.length === 0) {
return <div className="ant-layout-container"><Welcome/></div>;
} else {
return <Tabs activeKey={this.state.currentTabKey} type="editable-card"
onEdit={this.onTabRemove} onChange={this.onTabChange}
hideAdd className="ant-layout-tab">
{this.state.tabPanes.map(pane => <TabPane tab={pane.title} key={pane.key}
closable={true}>{pane.content}</TabPane>)}
</Tabs>;
}
}
// 非tab模式, 显示面包屑和对应的组件
else {
return <div>
<Breadcrumb routes={this.props.routes}/>
<div className="ant-layout-container">
{this.props.children}
</div>
</div>;
}
}
render() {
// 显示一个加载中
if (this.state.tryingLogin) {
return <div className="center-div"><Spin spinning={true} size="large"/></div>;
}
// 跳转到登录界面
if (!this.props.login) {
return <Login/>;
}
// 正常显示
return (
<div className="ant-layout-base">
{/*整个页面被一个ant-layout-base的div包围, 分为sidebar/header/footer/content等几部分*/}
<Sidebar/>
<div id="main-content-div"
className={this.props.collapse ? 'ant-layout-main-collapse' : 'ant-layout-main'}>
<Header realname={this.props.user.realname}/>
{this.renderBody()}
<Footer/>
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
collapse: state.Sidebar.collapse, // 侧边栏是否折叠
login: state.Login.login, // 是否登录
user: state.Login.user // 登录后的用户名
};
};
const mapDispatchToProps = (dispatch) => {
return {
handleLoginSuccess: bindActionCreators(loginSuccessCreator, dispatch) // loginSuccess事件比较特殊, 不只Login组件会触发, App组件也会触发
};
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
/*定义整体layout的样式, 直接从antd官网copy的, 稍微修改了一些*/
#root {
height: 100%;
}
.ant-layout-base {
position: relative;
height: 100%;
width: 100%;
}
.ant-layout-main-base {
transition: all 0.3s ease;
clear: left;
}
.ant-layout-base .ant-layout-main {
// margin-left: 224px; // 正常侧边栏的宽度
.ant-layout-main-base;
}
.ant-layout-base .ant-layout-main-collapse {
// margin-left: 64px; // 侧边栏折叠时的宽度
.ant-layout-main-base;
}
.ant-layout-base .ant-layout-container {
// margin: 24px 16px;
// padding: 24px;
margin: 0;
padding: .2em;
background: #fff;
}
.ant-layout-base .ant-layout-tab { // tab模式时的样式
margin: 8px 8px;
background: #fff;
}
.ant-layout-base .ant-layout-tab-text {
font-size: 12px;
}
// 为了在屏幕中间显示一个loading
.center-div {
position: absolute; /*绝对定位*/
top: 50%; /* 距顶部50%*/
left: 50%; /* 距左边50%*/
height: 2px;
margin-top: -1px; /*margin-top为height一半的负值*/
width: 2px;
margin-left: -1px; /*margin-left为width一半的负值*/
}
.table-responsive div.ant-table{
overflow-x: auto;
}
@media screen and (max-width: 767px) {
.table-responsive {
width: 100%;
div.ant-table .ant-table-body > table {
> thead, > tbody, > tfoot {
> tr {
> th, > td {
white-space: nowrap;
}
}
}
}
}
}
@media screen and (min-width: 992px) {
.ant-layout-base {
.ant-layout-main, .ant-layout-main-collapse {
clear: none;
height: 100%;
overflow: auto;
}
}
.ant-layout-base .ant-layout-container {
margin: 24px 16px;
padding: 24px;
}
}
.row-thumb {
max-height: 27px;
max-width: 27px;
}
\ No newline at end of file
import React, {PureComponent} from 'react';
import BraftEditor from 'braft-editor';
import 'braft-editor/dist/braft.css';
import globalConfig from '../../config';
class BraftEditorComponent extends PureComponent {
state = {
initialContent: '',
contentId: ''
};
// 商品详情预览
handleEditorPreview = () => {
if (window.previewWindow) {
window.previewWindow.close();
}
window.previewWindow = window.open();
window.previewWindow.document.write(this.buildPreviewHtml());
};
componentWillReceiveProps(nextProps) {
if (this.props.contentId && !this.props.value) {
this.state.contentId = nextProps.contentId;
this.state.initialContent = nextProps.value;
}
}
// 当编辑器内容改变时触发此事件
handleEditorChange = (content) => {
const {onChange} = this.props;
this.state.initialContent = content;
if (onChange) {
onChange(content);
}
};
// 编辑器图上传
handleEditorUpload = (param) => {
const serverURL = globalConfig.getAPIPath() + this.props.uploadUrl;
const xhr = new XMLHttpRequest;
const fd = new FormData();
// 上传成功后调用并传入上传后的文件地址
const successFn = (response) => {
const result = JSON.parse(xhr.responseText);
if (result && result.code === 0) {
param.success({
url: result.data.url // 服务端返回文件上传后的地址
});
} else {
param.error({
msg: '图片上传失败,请明天再试.'
});
}
};
// 上传进度发生变化时调用
const progressFn = (event) => {
param.progress(event.loaded / event.total * 100);
};
// 上传发生错误时调用
const errorFn = (response) => {
param.error({
msg: '图片上传失败,请明天再试.'
});
};
xhr.upload.addEventListener("progress", progressFn, false);
xhr.addEventListener("load", successFn, false);
xhr.addEventListener("error", errorFn, false);
xhr.addEventListener("abort", errorFn, false);
fd.append('file', param.file);
xhr.open('POST', serverURL, true);
xhr.send(fd);
};
buildPreviewHtml = () => {
const htmlContent = this.state.initialContent;
return `
<!Doctype html>
<html>
<head>
<title>商品详情预览</title>
<style>
html,body{
height: 100%;
margin: 0;
padding: 0;
background-color: #f1f2f3;
}
.container{
box-sizing: border-box;
width: 1000px;
max-width: 100%;
min-height: 100%;
margin: 20px auto;
padding: 30px 20px;
background-color: #fff;
border-right: solid 1px #eee;
border-left: solid 1px #eee;
border-radius: 10px;
}
.container img,
.container audio,
.container video{
max-width: 100%;
height: auto;
}
</style>
</head>
<body>
<div class="container">${htmlContent}</div>
</body>
</html>
`;
};
editorProps = () => {
return {
height: 500,
contentFormat: 'html',
initialContent: this.state.initialContent,
contentId: this.state.contentId,
fontSizes: [
12, 14, 16, 18, 20, 24,
28, 30, 32, 36, 40, 48
],
extendControls: [
{
type: 'split'
},
// 自定义预览按钮
{
type: 'button',
text: <span className="braft-control-text">预览</span>,
className: 'preview-button',
onClick: () => this.handleEditorPreview()
}
],
// 配置编辑器可插入的外部网络媒体类型
media: {
externalMedias: {
image: false,
audio: false,
video: false,
embed: false
},
uploadFn: this.handleEditorUpload
// 限制文件上传大小(待定)
// validateFn: (file) => {
// return file.size < 1024 * 100;
// }
},
onChange: this.handleEditorChange
};
};
render() {
// 富文本编辑器相关配置
const editorProps = this.editorProps();
return (
<div>
<BraftEditor {...editorProps} ref={instance => this.editorInstance = instance}/>
</div>
);
}
}
export default BraftEditorComponent;
\ No newline at end of file
import React from 'react';
import {Breadcrumb, Icon} from 'antd';
import sidebarMenu, {headerMenu} from '../../menu.js'; // 注意这种引用方式
import Logger from '../../utils/Logger';
import './index.less';
const Item = Breadcrumb.Item;
const logger = Logger.getLogger('Breadcrumb');
/**
* 定义面包屑导航, 由于和已有的组件重名, 所以改个类名
*/
class Bread extends React.PureComponent {
//static inited = false; // 表示下面两个map是否初始化
//static iconMap = new Map(); // 暂存menu.js中key->icon的对应关系
//static nameMap = new Map(); // 暂存menu.js中key->name的对应关系
// 上面两个map本来是做成static变量的, 后来感觉还是当成普通的成员变量好些
// 如果是static变量, 那就跟react组件的生命周期完全没关系了
// 话说, 虽然constructor和componentWillMount方法作用差不多, 但我还是觉得componentWillMount更好用
// 因为constructor还要super(props), 有点啰嗦
// 虽然react官方推荐constructor, 因为constructor中可以设置初始状态
// 不过实际上初始状态可以直接通过定义成员变量的方式设置, 不一定要在constructor中
componentWillMount() {
// 准备初始化iconMap和nameMap
const iconMap = new Map();
const nameMap = new Map();
// 这是个很有意思的函数, 本质是dfs, 但用js写出来就觉得很神奇
const browseMenu = (item) => {
nameMap.set(item.key, item.name);
logger.debug('nameMap add entry: key=%s, value=%s', item.key, item.name);
iconMap.set(item.key, item.icon);
logger.debug('iconMap add entry: key=%s, value=%s', item.key, item.icon);
if (item.child) {
item.child.forEach(browseMenu);
}
};
sidebarMenu.forEach(browseMenu);
headerMenu.forEach(browseMenu);
this.iconMap = iconMap;
this.nameMap = nameMap;
}
render() {
const itemArray = [];
// 面包屑导航的最开始都是一个home图标, 并且这个图标是可以点击的
itemArray.push(<Item key="systemHome" href="#"><Icon type="home"/> 首页</Item>);
// this.props.routes是react-router传进来的
for (const route of this.props.routes) {
logger.debug('path=%s, route=%o', route.path, route);
const name = this.nameMap.get(route.path);
if (name) {
const icon = this.iconMap.get(route.path);
if (icon) {
itemArray.push(<Item key={name}><Icon type={icon}/> {name}</Item>); // 有图标的话带上图标
} else {
// 这个key属性不是antd需要的, 只是react要求同一个array中各个元素要是不同的, 否则有warning
itemArray.push(<Item key={name}>{name}</Item>);
}
}
}
// 这个面包屑是不可点击的(除了第一级的home图标), 只是给用户一个提示
return (
<div className="ant-layout-breadcrumb">
<Breadcrumb>{itemArray}</Breadcrumb>
</div>
);
}
}
export default Bread;
.ant-layout-breadcrumb {
> .ant-breadcrumb {
.ant-breadcrumb-link {
display: inline-block;
padding: .8em .4em;
}
}
}
import React from 'react';
import {
Icon,
Row,
Col,
Button,
message,
Upload,
notification
} from 'antd';
import globalConfig from '../../config.js';
import moment from 'moment';
import FormSchemaUtils from './InnerFormSchemaUtils';
import Utils from '../../utils';
import Logger from '../../utils/Logger';
// 这个组件的实现还是有点技巧的, 因为对于不同的schema要显示不同的表单项, 不同表的schema肯定是不同的
// 我最开始是将InnerForm做成一个大的组件, 但这意味着必须要在render方法里解析schema, 虽然能实现功能, 但不完美, 效率也会比较差
// 而且antd的form是controlled components, 每输入一个字符都要重新render一次, 意味着每输入一个字符都要重新解析一次schema, 很蛋疼
// 这种实现见我以前的代码
// 在表名不变的情况下, schema也是固定的, 能不能只解析一次, 之后每次复用呢?
// 绞尽脑汁想到一个办法, 将每个表的表单都做成一个单独的组件, 这个组件是根据schema动态生成的, 在InnerForm的render方法中, 根据当前表名选择对应的组件去渲染
// 这样对InnerForm而言, 每个表单都是黑盒了, 不用关心里面的状态了
// 但要生成antd的表单必须配合一个getFieldDecorator函数, 很难搞, 不能简单的做到模版/数据的分离
// 我甚至考虑过是不是在编译期去解决, 根据schema动态生成js文件之类的, 但这样太麻烦, 最好是能在运行时搞定, 也考虑过eval方法之类的
// 后来参考了函数式语言的惰性求值, 终于想到一个解决办法, 解析schema后不返回具体的元素, 而是返回一个回调函数, 这个函数的参数是getFieldDecorator
// 在真正render的时候, 将getFieldDecorator作为参数传进去
// 此外, 还有一些问题, 比如如何动态生成组件, 如何获取表单的值之类的, 最后也都一一找到办法, 真是不容易...
// 应用的一些技巧: 高阶函数/高阶组件/ref/闭包
// 但表单项一多还是会有点卡...
const logger = Logger.getLogger('InnerForm');
/**
* 内部表单组件(条件以及导入导出)
*/
class InnerForm extends React.PureComponent {
// 什么情况会导致InnerForm re-render?
// 1. 这个组件没有状态
// 2. 只有props会导致re-render, 但由于这个组件是pure的, 所以只有表名变化时才会re-render
componentDidMount() {
this.processQueryParams();
}
componentDidUpdate() {
this.processQueryParams();
}
/**
* 处理url参数, 填入表单
*/
processQueryParams() {
const {form} = this.formComponent.props;
this.formComponent = form;
const params = Utils.getAllQueryParams();
if (Object.keys(params).length > 0) {
this.formComponent.setFieldsValue(params);
}
}
/**
* 表单的查询条件不能直接传给后端, 要处理一下
*
* @param oldObj
* @returns {{}}
*/
filterQueryObj(oldObj) {
// 将提交的值中undefined/null去掉
const newObj = {};
for (const key in oldObj) {
if (oldObj[key] !== undefined && oldObj[key] !== null) {
// 对于js的日期类型, 要转换成字符串再传给后端
if (oldObj[key] instanceof Date) {
newObj[key] = oldObj[key].format('yyyy-MM-dd HH:mm:ss');
} else if (moment.isMoment(oldObj[key])) { // 处理moment对象
newObj[key] = oldObj[key].format('YYYY-MM-DD HH:mm:ss');
} else {
newObj[key] = oldObj[key];
}
}
}
logger.debug('old queryObj: %o, new queryObj %o', oldObj, newObj);
return newObj;
}
/**
* 处理表单提交
*
* @param e
*/
handleSubmit = (e) => {
e.preventDefault();
// 这种用法是非官方的, 直接从代码里扒出来的...
// this.formComponent是通过ref方式获取到的一个react组件
const oldObj = this.formComponent.getFieldsValue();
const newObj = this.filterQueryObj(oldObj);
// 还是要交给上层组件处理, 因为要触发table组件的状态变化...
this.props.parentHandleSubmit(newObj);
};
/**
* 清空表单的值
*
* @param e
*/
handleReset = (e) => {
e.preventDefault();
this.formComponent.resetFields();
};
/**
* 处理数据导入
*/
handleImport = (info) => {
logger.debug('upload status: %s', info.file.status);
// 正在导入时显示一个提示信息
if (info.file.status === 'uploading') {
if (!this.hideLoading) {
let hide = message.loading('正在导入...');
this.hideLoading = hide;
}
}
// 导入完成, 无论成功或失败, 必须给出提示, 并且要用户手动关闭
else if (info.file.status === 'error') {
this.hideLoading();
this.hideLoading = undefined;
notification.error({
message: '导入失败',
description: '文件上传失败, 请联系管理员',
duration: 0,
});
}
// done的情况下还要判断返回值
else if (info.file.status === 'done') {
this.hideLoading();
this.hideLoading = undefined;
logger.debug('upload result %o', info.file.response);
if (!info.file.response.success) {
notification.error({
message: '导入失败',
description: ` ${info.file.response.message}`,
duration: 0,
});
} else {
notification.success({
message: '导入成功',
description: info.file.response.data,
duration: 0,
});
}
}
};
/**
* 处理数据导出
* 本质上也是提交表单, 跟handleSubmit有点类似
* 但不用再提交给上层组件处理了, 因为不需要改变表格组件的状态
*/
handleExport = (e) => {
e.preventDefault();
const oldObj = this.formComponent.getFieldsValue();
const newObj = this.filterQueryObj(oldObj);
// 导出前必须选定了一些查询条件, 不允许导出全表
// 防止误操作
if (Object.keys(newObj).length === 0) {
message.warning('导出时查询条件不能为空', 4.5);
return;
}
// ajax是不能处理下载请求的, 必须交给浏览器自己去处理
// 坏处是我就不知道用户的下载是否成功了
const url = `${globalConfig.getAPIPath()}/${this.props.tableName}/export`;
window.open(`${url}?q=${encodeURIComponent(JSON.stringify(newObj))}`); // 注意url编码
};
render() {
const {tableName, schema, tableConfig} = this.props;
// 根据当前的tableName, 获取对应的表单组件
const FormComponent = FormSchemaUtils.getForm(tableName, schema);
// 导入
const uploadProps = {
name: 'file',
action: `${globalConfig.getAPIPath()}/${this.props.tableName}/import`,
showUploadList: false,
onChange: this.handleImport,
};
// 表单的前面是一堆输入框, 最后一行是按钮
return (
<div className="ant-advanced-search-form">
{/*这个渲染组件的方法很有意思, 另外注意这里的ref*/}
<FormComponent wrappedComponentRef={(input) => {
this.formComponent = input;
}}/>
<Row>
<Col span={24} style={{textAlign: 'right'}}>
<Button type="primary" onClick={this.handleSubmit}><Icon type="search"/>查询</Button>
<Button onClick={this.handleReset}><Icon type="cross"/>清除条件</Button>
{tableConfig.showExport ?
<Button onClick={this.handleExport}><Icon
type="export"/>导出</Button> : ''}
{tableConfig.showImport ?
<Upload {...uploadProps}><Button><Icon type="upload"/>导入</Button></Upload> : ''}
</Col>
</Row>
</div>
);
}
}
export default InnerForm;
import React from 'react';
import {
Form,
Input,
Row,
Col,
DatePicker,
Select,
Icon,
Radio,
InputNumber,
Checkbox,
Cascader
} from 'antd';
import TableUtils from './TableUtils.js';
import moment from 'moment';
import Logger from '../../utils/Logger';
import options from '../../utils/cascader-address-options';
import createClass from 'create-react-class';
//const createClass = require('create-react-class');
const FormItem = Form.Item;
const RadioGroup = Radio.Group;
const CheckboxGroup = Checkbox.Group;
const Option = Select.Option;
const logger = Logger.getLogger('InnerFormSchemaUtils');
// TODO: 其实这里缺少对schema的校验
// 暂存每个表对应的schema callback, 解析schema是个代价较大的操作, 应该尽量复用
const schemaMap = new Map();
// 暂存每个表对应的表单组件, key是表名, value是对应的react组件
const formMap = new Map();
/**
* 这是一个工具类, 目的是将parse schema的过程独立出来
*/
const SchemaUtils = {
/**
* 获取某个表单对应的react组件
*
* @param tableName
* @param schema
* @returns {*}
*/
getForm(tableName, schema) {
// 是否要忽略缓存
// 在忽略缓存的情况下, 每次都会重新解析schema
const ignoreCache = TableUtils.shouldIgnoreSchemaCache(tableName);
if (formMap.has(tableName)) {
return formMap.get(tableName);
} else {
const newForm = this.createForm(tableName, schema);
if (!ignoreCache) {
formMap.set(tableName, newForm);
}
return newForm;
}
},
/**
* 动态生成表单
*
* @param tableName
* @param schema
* @returns {*}
*/
createForm(tableName, schema) {
const ignoreCache = TableUtils.shouldIgnoreSchemaCache(tableName);
// 蛋疼的this
const that = this;
// 如何动态生成一个组件? 如果用class的写法, 似乎不行...
// 只能用传统的ES5的写法, 函数式组件应该也可以, 但是我需要生命周期相关方法
const tmpComponent = createClass({
componentWillMount() {
// 组件初始化时读取schema
if (schemaMap.has(tableName)) {
this.schemaCallback = schemaMap.get(tableName);
return;
}
const schemaCallback = that.parse(schema);
if (!ignoreCache) {
schemaMap.set(tableName, schemaCallback);
}
this.schemaCallback = schemaCallback;
},
render() {
// render的时候传入getFieldDecorator, 生成最终的jsx元素
return this.schemaCallback(this.props.form.getFieldDecorator);
},
});
// 注意要再用antd的create()方法包装下
return Form.create()(tmpComponent);
},
/**
* 解析表单的schema
*
* @param schema 直接从tableName.querySchema.js文件中读出来的schema
* @returns {function()} 一个函数, 这个函数的参数是getFieldDecorator, 执行后才会返回真正的jsx元素, 为啥不直接返回jsx元素而要返回函数呢, 因为antd的表单的限制, 想生成最终的元素必须要getFieldDecorator
*/
parse(schema) {
// 用这两个变量去代表一个表单的schema
const rows = [];
let cols = [];
// 参见antd的布局, 每行被分为24个格子
// 普通的字段每个占用8格, between类型的字段每个占用16格
let spaceLeft = 24;
schema.forEach((field) => {
// 当前列需要占用几个格子? 普通的都是8, 只有datetime between是16
let spaceNeed = field.width || 8;
if (field.showType === 'between' && field.dataType === 'datetime') {
spaceNeed = field.width || 16;
}
// 如果当前行空间不足, 就换行
if (spaceLeft < spaceNeed) {
rows.push(cols);
cols = []; // 重置cols
spaceLeft = 24; // 剩余空间重置
}
// 注意, 每个字段transform之后, 返回的也都是一个回调函数, 所以cols其实是一个回调函数的集合
switch (field.showType) {
case 'select':
cols.push(this.transformSelect(field));
break;
case 'radio':
cols.push(this.transformRadio(field));
break;
case 'checkbox':
cols.push(this.transformCheckbox(field));
break;
case 'multiSelect':
cols.push(this.transformMultiSelect(field));
break;
case 'between':
cols.push(this.transformBetween(field));
break;
case 'cascader':
cols.push(this.transformCascader(field));
break;
default:
cols.push(this.transformNormal(field));
}
spaceLeft -= spaceNeed;
});
// 别忘了最后一行
if (cols.length > 0) {
rows.push(cols);
}
// 至此, schema解析完毕, 接下来是回调函数
// 这里有一点闭包的概念
return getFieldDecorator => {
const formRows = []; // 最终的表单中的一行
for (let i = 0; i < rows.length; i++) {
const formCols = []; // 最终的表单中的一列
for (const col of rows[i]) {
formCols.push(col(getFieldDecorator)); // 注意这里的col是一个函数
}
formRows.push(<Row key={i} gutter={16}>{formCols}</Row>);
}
return (<Form layout="horizontal">
{formRows}
</Form>);
};
},
/**
* 辅助函数, 将一个input元素包装下
*
* @param formItem 一个callback, 以getFieldDecorator为参数, 执行后返回对应的表单项, input/select之类的
* @param field schema中的一列
*/
colWrapper(formItem, field) {
return getFieldDecorator => (
<Col key={field.key} sm={field.width || 8}>
<FormItem key={field.key} label={field.title} labelCol={{span: 7}} wrapperCol={{span: 17}}>
{formItem(getFieldDecorator)}
</FormItem>
</Col>
);
},
/**
* 将schema中的一列转换为下拉框
*
* @param field
*/
transformSelect(field) {
logger.debug('transform field %o to Select component', field);
const options = [];
field.options.forEach((option) => {
options.push(<Option key={option.key} value={option.key}>{option.value}</Option>);
});
return this.colWrapper(getFieldDecorator => getFieldDecorator(field.key, {initialValue: field.defaultValue})(
<Select placeholder={field.placeholder || '请选择'} size="default">
{options}
</Select>
), field);
},
/**
* 将schema中的一列转换为单选框
*
* @param field
*/
transformRadio(field) {
logger.debug('transform field %o to Radio component', field);
const options = [];
field.options.forEach((option) => {
options.push(<Radio.Button key={option.key} value={option.key}>{option.value}</Radio.Button>);
});
return this.colWrapper(getFieldDecorator => getFieldDecorator(field.key, {initialValue: field.defaultValue})(
<Radio.Group buttonStyle="solid">
{options}
</Radio.Group>
), field);
},
/**
* 将schema中的一列转换为checkbox
*
* @param field
*/
transformCheckbox(field) {
logger.debug('transform field %o to Checkbox component', field);
const options = [];
field.options.forEach((option) => {
options.push({label: option.value, value: option.key});
});
return this.colWrapper(getFieldDecorator => getFieldDecorator(field.key, {initialValue: field.defaultValue})(
<CheckboxGroup options={options}/>
), field);
},
/**
* 转换为下拉多选框
*
* @param field
* @returns {XML}
*/
transformMultiSelect(field) {
logger.debug('transform field %o to MultipleSelect component', field);
const options = [];
field.options.forEach((option) => {
options.push(<Option key={option.key} value={option.key}>{option.value}</Option>);
});
return this.colWrapper(getFieldDecorator => getFieldDecorator(field.key, {initialValue: field.defaultValue})(
<Select multiple placeholder={field.placeholder || '请选择'} size="default">
{options}
</Select>
), field);
},
/**
* 转换为级联选择
*
* @param field
* @returns {XML}
*/
transformCascader(field) {
logger.debug('transform field %o to Cascader component', field);
return this.colWrapper(getFieldDecorator => getFieldDecorator(field.key, {initialValue: field.defaultValue})(
<Cascader options={options} expandTrigger="hover" placeholder={field.placeholder || '请选择'} size="default"/>
), field);
},
/**
* 将schema中的一列转换为普通输入框
*
* @param field
* @returns {XML}
*/
transformNormal(field) {
switch (field.dataType) {
case 'int':
logger.debug('transform field %o to integer input component', field);
return this.colWrapper(getFieldDecorator => getFieldDecorator(field.key, {initialValue: field.defaultValue})(
<InputNumber size="default" max={field.max} min={field.min} placeholder={field.placeholder}/>
), field);
case 'float':
logger.debug('transform field %o to float input component', field);
return this.colWrapper(getFieldDecorator => getFieldDecorator(field.key, {initialValue: field.defaultValue})(
<InputNumber step={0.01} size="default" max={field.max} min={field.min}
placeholder={field.placeholder}/>
), field);
case 'datetime':
logger.debug('transform field %o to datetime input component', field);
return this.colWrapper(getFieldDecorator => getFieldDecorator(field.key, {initialValue: field.defaultValue ? moment(field.defaultValue) : null})(
<DatePicker showTime format="YYYY-MM-DD HH:mm:ss" placeholder={field.placeholder || '请选择日期'}/>
), field);
default: // 默认就是普通的输入框
logger.debug('transform field %o to varchar input component', field);
return this.colWrapper(getFieldDecorator => getFieldDecorator(field.key, {initialValue: field.defaultValue})(
<Input placeholder={field.placeholder} size="default" addonBefore={field.addonBefore}
addonAfter={field.addonAfter}/>
), field);
}
},
/**
* 也是个辅助函数, 由于是范围查询, 输入的formItem是两个, 一个用于begin一个用于end
*
* @param beginFormItem
* @param endFormItem
* @param field
*/
betweenColWrapper(beginFormItem, endFormItem, field) {
// 布局真是个麻烦事
// col内部又用了一个row做布局
return getFieldDecorator => (
<Col key={`${field.key}Begin`} sm={8}>
<Row>
<Col span={16}>
<FormItem key={`${field.key}Begin`} label={field.title} labelCol={{span: 15}}
wrapperCol={{span: 9}}>
{beginFormItem(getFieldDecorator)}
</FormItem>
</Col>
<Col span={7} offset={1}>
<FormItem key={`${field.key}End`} labelCol={{span: 10}} wrapperCol={{span: 14}}>
{endFormItem(getFieldDecorator)}
</FormItem>
</Col>
</Row>
</Col>
);
},
/**
* between类型比较特殊, 普通元素每个宽度是8, int和float的between元素宽度也是8, 但datetime类型的between元素宽度是16
* 否则排版出来不能对齐, 太丑了, 逼死强迫症
* 而且普通的transform函数返回是一个object, 而这个函数返回是一个array, 就是因为datetime的between要占用两列
*
* @param field
*/
transformBetween(field) {
let beginFormItem;
let endFormItem;
// webstorm的代码格式化有时很奇怪...
switch (field.dataType) {
case 'int':
logger.debug('transform field %o to integer BETWEEN component', field);
beginFormItem = getFieldDecorator => getFieldDecorator(`${field.key}Begin`, {initialValue: field.defaultValueBegin})
(<InputNumber size="default" placeholder={field.placeholderBegin || '最小值'}/>);
endFormItem = getFieldDecorator => getFieldDecorator(`${field.key}End`, {initialValue: field.defaultValueEnd})
(<InputNumber size="default" placeholder={field.placeholderEnd || '最大值'}/>);
return this.betweenColWrapper(beginFormItem, endFormItem, field);
case 'float':
logger.debug('transform field %o to float BETWEEN component', field);
beginFormItem = getFieldDecorator => getFieldDecorator(`${field.key}Begin`, {initialValue: field.defaultValueBegin})
(<InputNumber step={0.01} size="default" placeholder={field.placeholderBegin || '最小值'}/>);
endFormItem = getFieldDecorator => getFieldDecorator(`${field.key}End`, {initialValue: field.defaultValueEnd})
(<InputNumber step={0.01} size="default" placeholder={field.placeholderEnd || '最大值'}/>);
return this.betweenColWrapper(beginFormItem, endFormItem, field);
// datetime类型的between要占用两个Col
// 不写辅助函数了, 直接这里写jsx吧...
case 'datetime':
logger.debug('transform field %o to datetime BETWEEN component', field);
// 只能返回一个顶层元素
return getFieldDecorator => (
<div key={'datetimeBetweenDiv'}>
<Col key={`${field.key}Begin`} sm={8}>
<FormItem key={`${field.key}Begin`} label={field.title} labelCol={{span: 10}}
wrapperCol={{span: 14}}>
{getFieldDecorator(`${field.key}Begin`, {initialValue: field.defaultValueBegin ? moment(field.defaultValueBegin) : null})
(<DatePicker showTime format="YYYY-MM-DD HH:mm:ss"
placeholder={field.placeholderBegin || '开始日期'}/>)}
</FormItem>
</Col>
<Col key={`${field.key}End`} sm={8}>
<FormItem key={`${field.key}End`} labelCol={{span: 10}} wrapperCol={{span: 14}}>
{getFieldDecorator(`${field.key}End`, {initialValue: field.defaultValueEnd ? moment(field.defaultValueEnd) : null})
(<DatePicker showTime format="YYYY-MM-DD HH:mm:ss"
placeholder={field.placeholderEnd || '结束日期'}/>)}
</FormItem>
</Col>
</div>
);
default:
// 理论上来说不会出现这种情况
logger.error('unknown dataType: %s', field.dataType);
}
return null;
},
};
export default SchemaUtils;
import React from 'react';
import {Pagination, Select} from 'antd';
/**
* 内部分页器组件
*/
class InnerPagination extends React.PureComponent {
render() {
// 有些状态要传到父组件中去处理
return (
<div className="db-pagination">
<Pagination
simple={this.props.isSimple}
showQuickJumper
selectComponentClass={Select}
total={this.props.total}
showTotal={(total) => `每页${this.props.pageSize}条, 共 ${total} 条`}
pageSize={this.props.pageSize} defaultCurrent={1}
current={this.props.currentPage}
onChange={this.props.parentHandlePageChange}
//是否显示“每页显示条目数”,对应 antd Pagination组件的showSizeChanger属性
showSizeChanger={this.props.showSizeChanger}
//修改“每页显示条目数”时触发,对应 antd Pagination组件的onShowSizeChange属性
onShowSizeChange={this.props.parentHandleShowPageChange}
pageSizeOptions={this.props.pageSizeOptions}
/>
</div>
);
}
}
export default InnerPagination;
import React from 'react';
import {
Button,
Table,
Icon,
Modal,
message,
notification,
Affix
} from 'antd';
import Logger from '../../utils/Logger';
import Utils from '../../utils';
import ajax from '../../utils/ajax';
import moment from 'moment';
import ImageSlider from '../ImageSlider';
import InnerTableSchemaUtils from './InnerTableSchemaUtils';
import InnerTableRenderUtils, {ACTION_KEY} from './InnerTableRenderUtils';
const logger = Logger.getLogger('InnerTable');
/**
* 内部表格组件(包含新增,修改,删除系列操作)
*/
class InnerTable extends React.PureComponent {
// 虽然传进来的是dataSchema, 但要parse两次, 分别变成表格和表单的schema
state = {
modalVisible: false, // modal是否可见
modalTitle: '新增', // modal标题
modalInsert: true, // 当前modal是用来insert还是update
selectedRowKeys: [], // 当前有哪些行被选中, 这里只保存key
// FIXME: 这里可能会有点问题, 父组件中有一个data, 这里又有一个data, 都表示的是表格中的数据, 两边状态不一致, 可能有潜在的bug
data: [], // 表格中显示的数据
// 图片预览相关状态
previewVisible: false, // 是否显示图片预览modal
previewImages: [], // 要预览的图片
editModalWidth: 550,
// 用户自定义组件modal, 一般用于实现单条记录的更新
componentModalVisible: false
};
setEditorModalWidth(value) {
this.setState({
editModalWidth: value
});
}
/**
* 组件初次挂载时parse schema
*/
componentWillMount() {
this.parseTableSchema(this.props);
this.parseTableData(this.props);
}
/**
* InnerTable组件的re-render有两种情况: 自身状态变化导致的render vs 父组件导致的render
* 正常情况下, 只有父组件导致的render才会触发这个方法, InnerTable自身的变化应该不会触发
*
* 父组件触发这个方法也有两种情况:
* 1. 只有data变化, 比如改变了查询条件/分页等等
* 2. schema和data都变化了, 比如在react router中切换了菜单项
*
* @param nextProps
*/
componentWillReceiveProps(nextProps) {
logger.debug('receive new props and try to render, nextProps=%o', nextProps);
// 之前因为antd的Form组件搞了一些黑盒操作, 表单每输入一次都会触发这个方法, 现在表单独立成一个组件了, 应该好了
// 只有表名变化时才需要重新parse schema
if (this.props.tableName !== nextProps.tableName) {
logger.debug('tableName changed and try to refresh schema');
this.parseTableSchema(nextProps);
this.formComponent = undefined; // 这个别忘了, 如果schema变了, 表单当然也要变
}
// 这里要还原初始状态, 理论上来讲, InnerTable所有自己的状态都应该还原, 但其实也是看情况的
// 比如这里的this.state.data就不用还原, 因为下面的parseTableData方法会更新this.state.data
// 哪些状态做成this.xxx, 哪些做成this.state.xxx, 还是有点迷惑的, 如果全都塞到state里是不是不太好
this.state.modalVisible = false;
this.state.modalTitle = '新增';
this.state.modalInsert = true;
this.state.selectedRowKeys = [];
// 是否要刷新表格中显示的数据? 这个逻辑还有点绕
// 1. 如果schema变化了, 必须刷新数据
// 2. 如果schema没变, 但表格要进入loading状态, 就不要刷新数据, 这样用户体验更好
if (this.props.tableName !== nextProps.tableName || !nextProps.tableLoading) {
this.parseTableData(nextProps);
}
}
/**
* 当前组件unmount时清除render函数缓存
*/
componentWillUnmount() {
logger.debug('InnerTable component unmount and reset RenderUtils');
InnerTableRenderUtils.reset();
}
/**
* 解析表格的schema
*/
parseTableSchema(props) {
const {tableName, schema} = props;
const parseResult = InnerTableSchemaUtils.getTableSchema(tableName, schema);
this.primaryKey = parseResult.primaryKey;
// fieldMap是对原始的dataSchema做了一些处理, 方便查询用的
this.fieldMap = parseResult.fieldMap;
// tableSchema是转换后的Table组件可用的schema
// 对于tableSchema, 即使命中了缓存, 也要重新设置下render函数
this.tableSchema = InnerTableRenderUtils.bindRender(parseResult.tableSchema, tableName, this);
}
/**
* 解析表格要显示的数据
* 后端返回的数据不能直接在table中显示
*/
parseTableData(props) {
// 每行数据都必须有个key属性, 如果指定了主键, 就以主键为key
// 否则直接用个自增数字做key
const newData = [];
let i = 0;
props.data.forEach((obj) => {
const newObj = this.transformRawDataToTable(obj);
if (this.primaryKey) {
newObj.key = obj[this.primaryKey];
} else {
newObj.key = i;
i++;
}
newData.push(newObj);
});
// 在这里, 下面两种写法是等效的, 因为parseTableData方法只会被componentWillReceiveProps调用, 而componentWillReceiveProps的下一步就是判断是否re-render
// 但要注意, 不是任何情况下都等效
//this.setState({data: newData});
this.state.data = newData;
}
/**
* 将后端返回的一条数据转换为前端表格中能显示的一条数据
* 后端返回的往往是数字(比如0表示屏蔽, 1表示正常)
* 而表格中要显示对应的汉字, 跟dataSchema中的配置对应
*/
transformRawDataToTable(obj) {
const newObj = {};
// 这段代码真是好蛋疼...
for (const key in obj) {
if (this.fieldMap.get(key) === undefined) {
continue;
}
if (this.fieldMap.get(key).$$optionMap) {
const optionMap = this.fieldMap.get(key).$$optionMap;
if (obj[key] instanceof Array) {
const newArray = [];
for (const optionKey of obj[key]) {
newArray.push(optionMap[optionKey]);
}
newObj[key] = newArray.join(',');
} else {
newObj[key] = optionMap[obj[key]];
}
} else {
newObj[key] = obj[key];
}
}
newObj.$$rawData = obj; // 原始数据还是要保存下的, 后面update会用到
return newObj;
}
/**
* 将后端返回的一条数据转换为表单中能显示的数据
* 主要是处理日期字段, 必须是moment对象
*/
transformRawDataToForm(obj) {
const newObj = {};
for (const key in obj) {
// rawData中可能有些undefined或null的字段, 过滤掉
if (obj[key] === null || this.fieldMap.get(key) === undefined) {
continue;
}
// 判断是否是日期类型的字段
if (this.fieldMap.get(key).dataType === 'datetime') {
newObj[key] = moment(obj[key]);
} else {
newObj[key] = obj[key];
}
}
return newObj;
}
/**
* 将表格中的一条数据转换为表单中能显示的数据
*/
transformTableDataToForm(obj) {
return this.transformRawDataToForm(obj.$$rawData);
}
/**
* 设置表单要显示的数据
*/
setFormData(data) {
// 注意这里, 由于antd modal的特殊性, this.formComponent可能是undefined, 要判断一下
if (this.formComponent) {
const {form} = this.formComponent.props;
form.resetFields();
if (data) {
form.setFieldsValue(data);
}
} else {
this.formInitData = data;
}
}
/**
* 点击新增按钮, 弹出一个内嵌表单的modal
*
* @param e
*/
onClickInsert = (e) => {
e.preventDefault();
this.setState({
modalVisible: true,
modalTitle: '新增',
modalInsert: true
}, () => this.setFormData({}));
};
/**
* 点击更新按钮, 弹出一个内嵌表单的modal
* 注意区分单条更新和批量更新
*
* @param e
*/
onClickUpdate = (e) => {
e.preventDefault();
// 重置下keysToUpdate, 因为点击表格上方的更新按钮时, 默认是所有字段都可以更新
this.singleRecordKey = undefined;
this.keysToUpdate = undefined;
// 要显示在表单中的值
const newData = {};
const multiSelected = this.state.selectedRowKeys.length > 1; // 是否选择了多项
// 如果只选择了一项, 就把原来的值填到表单里
// 否则就只把要更新的主键填到表单里
if (!multiSelected) {
logger.debug('update single record, and fill original values');
const selectedKey = this.state.selectedRowKeys[0];
for (const record of this.state.data) { // 找到被选择的那条记录
if (record.key === selectedKey) {
Object.assign(newData, this.transformTableDataToForm(record));
break;
}
}
//console.log(this.state.data);
} else {
newData[this.primaryKey] = this.state.selectedRowKeys.join(', ');
logger.debug('update multiple records, keys = %s', newData[this.primaryKey]);
}
// 理论上来说应该先设置好表单的值(setFieldsValue)再显示modal
// 美中不足的是表单的值变化需要一个时间, 显示modal的过程中可能被用户看到"旧值变新值"的过程, 在FileUploader组件上传图片时这个现象很明显
// 跟组件的实现方式有关, 可能是css动画的问题, 也可能是setState异步的问题, 似乎暂时无解...
if (multiSelected && this.props.tableConfig.showMultiSelected) {
this.setState({
modalVisible: true,
modalTitle: '批量更新',
modalInsert: false
});
} else {
this.setState({modalVisible: true, modalTitle: '更新', modalInsert: false});
}
// 删除不需要在表单显示的字段
for (const key in newData) {
if (this.fieldMap.get(key).showInForm === false) {
delete newData[key];
}
}
// 过滤商品分类制表符
if (newData.name) {
newData.name = newData.name.replace(/^.*─ /, '');
}
this.setFormData(newData);
};
/**
* 点击删除按钮, 弹出一个确认对话框
* 注意区分单条删除和批量删除
*
* @param e
*/
onClickDelete = (e) => {
e.preventDefault();
Modal.confirm({
title: this.state.selectedRowKeys.length > 1 ? '确认批量删除' : '确认删除',
content: `当前被选中的行: ${this.state.selectedRowKeys.join(', ')}`,
// 这里注意要用箭头函数, 否则this不生效
onOk: () => {
this.handleDelete();
}
});
};
/**
* 处理表格的选择事件
*
* @param selectedRowKeys
*/
onTableSelectChange = (selectedRowKeys) => {
this.setState({selectedRowKeys});
};
/**
* 隐藏modal
*/
hideModal = () => {
this.setState({modalVisible: false});
};
/**
* 点击modal中确认按钮的回调, 清洗数据并准备传给后端
*/
handleModalOk = () => {
// 提交表单之前, 要先校验下数据
let validated = true;
const {form} = this.formComponent.props;
form.validateFieldsAndScroll((err, values) => validated = err ? false : validated); // 不知道有没有更好的办法
if (!validated) {
logger.debug('validate form error');
return;
}
// 1. 将表单中的undefined去掉
// 2. 转换日期格式
const newObj = {};
const oldObj = form.getFieldsValue(); // 这里的formComponent必定不是undefined
for (const key in oldObj) {
// if (!oldObj[key]) {
// 原来的这种写法是有bug的, 因为空字符串也会被过滤掉, 而有时候空字符串传回后端也是有意义的
// 这里有个问题, 更新的时候, 某个字段后端接收到了null, 到底是忽略这个字段还是将字段更新为null(默认值)? 用过mybatis的应该能明白啥意思
// 这个问题貌似是无解的, 在后端字段只有null/not null两种状态, 而前端可以用3种状态: undefined表示不更新, null表示更新为null, 其他值表示更新为特定的值
// 只能认为undefined/null都对应于后端的null
// 换句话说, 如果DB里某个字段已经有值了, 就不可能再修改为null了, 即使建表时是允许null的. 最多更新成空字符串. 除非跟后端约定一个特殊的值去表示null.
// 一般情况下这不会有什么影响, 但某些corner case里可能有bug...
// 另外, 要理解antd form的取值逻辑. antd的form是controlled components, 只有当FormItem变化时才会取到值(通过onChange方法), 否则对应的key就是undefined
// 例如, 如果有一个输入框, 如果不动它, 然后getFieldsValue, 得到的是undefined; 如果先输入几个字符, 然后再全部删除, 再getFieldsValue, 得到的是空字符串
// 注意下日期类型, 它返回的是一个moment对象, 所以取到的值可能是null
// 如果写自己的FormItem组件, 一定要注意下这个问题
if (oldObj[key] === undefined || oldObj[key] === null) {
continue;
}
// 跟InnerForm中的filterQueryObj方法很相似
if (key === this.primaryKey && typeof oldObj[key] === 'string') { // 我在InnerTableSchemaUtils限制死了, modal中的主键字段必定是个string
// 对于主键而言, 我本来想在这里转换成array, 后来想想不用, this.state.selectedRowKeys中就已经保存着主键了, 可以直接用
// for (const str of oldObj[key].split(', ')) {
// primaryKeyArray.push(str);
// }
// do nothing
} else if (oldObj[key] instanceof Date) {
newObj[key] = oldObj[key].format('yyyy-MM-dd HH:mm:ss');
} else if (moment.isMoment(oldObj[key])) { // 处理moment对象
newObj[key] = oldObj[key].format('YYYY-MM-DD HH:mm:ss');
} else {
newObj[key] = oldObj[key];
}
}
// 至此表单中的数据格式转换完毕
this.hideModal();
logger.debug('click modal OK and the form obj = %o', newObj);
// 将转换后的数据传给后端
if (this.state.modalInsert) {
this.handleInsert(newObj);
} else {
// 这个modal可能是用于表格上方的"修改"按钮, 也可能用于单条记录的更新
// 这里要判断一下
if (this.singleRecordKey) {
const keys = [];
keys.push(this.singleRecordKey);
this.handleUpdate(newObj, keys);
} else {
this.handleUpdate(newObj);
}
}
};
/**
* 点击图片时显示幻灯片
*
* @param text
*/
onClickImage = (text) => {
const newImageArray = [];
if (Utils.isString(text) && text.length > 0) {
newImageArray.push({url: text, alt: '图片加载失败'});
} else if (text instanceof Array) {
for (const tmp of text) {
newImageArray.push({url: tmp, alt: '图片加载失败'});
}
}
// 如果没有图片, 点击就不要显示modal
if (newImageArray.length > 0) {
this.setState({previewVisible: true, previewImages: newImageArray});
}
};
/**
* 隐藏图片预览
*/
cancelPreview = () => {
this.setState({previewVisible: false});
};
/**
* 针对单条记录的更新
*
* @param record 要更新的记录
* @param keysToUpdate 允许更新哪些字段(弹出的modal中显示哪些字段)
*/
onSingleRecordUpdate = (record, keysToUpdate) => {
// 传进来的record是表格中显示的一条数据, 要转换下才能填到表单中
// 比如checkbox在表格中显示的是逗号分隔字符串, 但在表单中还是要还原为key数组的
const transformedRecord = this.transformTableDataToForm(record);
this.singleRecordKey = record[this.primaryKey]; // 要更新哪条记录
if (keysToUpdate) {
this.keysToUpdate = new Set(keysToUpdate);
} else {
this.keysToUpdate = undefined;
}
//this.setFormData(transformedRecord);
// 这里又有一个hack
// 我本来是先setFormData再setState的, 但表单的显示总是有点问题, setFieldsValue设置表单的值有时不生效
// 只要keysToUpdate改变, 表单的值的显示就会有问题
// 换句话说, 一旦表单的schema变化, setFieldsValue就有问题
// 猜测是setFieldsValue(data)方法的实现比较特殊, 它不会收集data中的所有值, 而是只会收集当前schema中有用的值, 姑且叫做collectKeys
// 比如传入的data是{a:1, b:2, c:3}, 而render方法中有用的key是a和b, 调用setFieldsValue时就会忽略c的值
// 每次render的时候才会更新collectKeys, 应该是通过getFieldDecorator方法收集的
// 我碰到的问题, 如果先setFormData, 表单组件只会按当前的schema去收集值
// 而setState时会触发表单组件的render方法, 这个表单的schema其实是根据keysToUpdate动态生成的, 很可能collectKeys跟之前完全不一样
// 所以渲染出来的表单, 没有把值填进去, 虽然我setFieldsValue时传入了完整的一行记录...
// 唉antd的黑盒太难琢磨, 源码还是typescript的, 有点看不懂...
this.setState({
modalVisible: true,
modalTitle: '更新',
modalInsert: false
}, () => this.setFormData(transformedRecord)); // 这种方法可以保证在表单组件render后才setFieldsValue, 就会按新的schema去收集值了
};
/**
* 针对单条记录的删除
*
* @param record
*/
onSingleRecordDelete = (record) => {
const selectedKey = record[this.primaryKey];
Modal.confirm({
title: '确认删除',
content: `当前被选中的行: ${selectedKey}`,
onOk: () => {
const keys = [];
keys.push(selectedKey);
this.handleDelete(keys);
}
});
};
/**
* 自定义组件实现对单条记录的更新
* 可以满足一些定制化的需求, 优化用户体验
*
* @param record 要更新的记录
* @param component 要渲染的组件, 会将对应的组件渲染到modal中
* @param name 显示modal时的标题
*/
onSingleRecordComponent = (record, component, name) => {
// 暂存对应的信息, 后面会用到
this.updateComponent = component; // react组件对应的class, 其实就是个函数
this.updateComponentRecord = record;
this.updateComponentModalTitle = name;
this.setState({componentModalVisible: true});
};
/**
* 隐藏自定义组件modal
*/
handleComponentModalCancel = () => {
this.setState({componentModalVisible: false});
};
/**
* 自定义组件modal点击确认时的回调
*/
handleComponentModalOk = () => {
// 首先关闭modal
this.setState({componentModalVisible: false});
// 自定义的组件正常挂载后, 会以ref的形式暂存
if (!this.updateComponentMounted) { // 正常情况下不会出现这种情况
logger.error('user-defined component does not mount');
return;
}
// 用户是否定义了getFieldsValue函数
if (!this.updateComponentMounted.getFieldsValue) {
logger.debug('user does not define getFieldsValue function');
return;
}
// 获取用户自定义组件的返回值
const data = this.updateComponentMounted.getFieldsValue();
logger.debug('user-defined component getFieldsValue = %o', data);
// 如果组件返回false/undefined, 就什么都不做
if (!data) {
return;
}
// 否则更新对应的记录
const keys = [];
keys.push(this.updateComponentRecord[this.primaryKey]);
this.handleUpdate(data, keys, this.updateComponentMounted.getTable());
// TODO: 其实用户自定义组件不只可以用于更新, 还可以做很多事, e.g. 如果定义了xxx方法就直接跳转某个url之类的
// 本质上来讲是和用户约定好的一套协议
};
/*下面开始才是真正的数据库操作*/
error(errorMsg) {
// 对于错误信息, 要很明显的提示用户, 这个通知框要用户手动关闭
notification.error({
message: '出错啦!',
description: ` ${errorMsg}`,
duration: 0
});
}
/**
* 真正去新增数据
*/
async handleInsert(obj) {
const CRUD = ajax.CRUD(this.props.tableName);
const hide = message.loading('正在新增...', 0);
try {
const res = await CRUD.insert(obj);
hide();
if (res.success) {
notification.success({
message: '新增成功',
description: this.primaryKey ? `新增数据行 主键=${res.data.model[this.primaryKey]}` : '',
duration: 3
});
// 数据变化后, 刷新下表格, 我之前是变化后刷新整个页面的, 想想还是只刷新表格比较好
// 新增的数据放到第一行
const newData = [];
const transformedData = this.transformRawDataToTable(res.data.model);
// 表格中的每条记录都必须有个唯一的key, 否则会有warn, 如果有主键就用主键, 否则只能随便给个
// 如果key有重复的, 会有warn, 显示也会有问题, 所以后端接口要注意下, 如果DB主键都能重复, 也只能呵呵了...
if (this.primaryKey) {
transformedData.key = res.data.model[this.primaryKey];
} else {
transformedData.key = Math.floor(Math.random() * 233333); // MAGIC NUMBER
}
newData.push(transformedData);
for (const record of this.state.data) {
newData.push(record);
}
this.windowReload(this.props.tableName);
this.setState({selectedRowKeys: [], data: newData});
} else {
this.error(res.message);
}
} catch (ex) {
logger.error('insert exception, %o', ex);
hide();
this.error(`网络请求出错: ${ex.message}`);
}
}
windowReload(tableName) {
switch (tableName) {
case 'goodsCategory':
setTimeout(() => {
window.location.reload();
}, 1000);
break;
}
}
/**
* 真正去更新数据
*/
async handleUpdate(obj, keys = this.state.selectedRowKeys, tableName = '') {
const CRUD = ajax.CRUD(tableName ? tableName : this.props.tableName);
const hide = message.loading('正在更新...', 0);
try {
const res = await CRUD.update(keys, obj);
hide();
if (res.success) {
notification.success({
message: '更新成功',
description: `更新${res.data.count}条数据`,
duration: 3
});
// 数据变化后, 刷新下表格
const transformedData = this.transformRawDataToTable(res.data.model); // 变化后的数据
const newData = [];
const keySet = new Set(keys); // array转set
for (const record of this.state.data) {
if (keySet.has(record.key)) { // 是否是被更新的记录
const newRecord = Object.assign({}, record, transformedData); // 这个应该是浅拷贝
newRecord.$$rawData = Object.assign({}, record.$$rawData, transformedData.$$rawData);
logger.debug('newRecord = %o', newRecord);
newData.push(newRecord);
} else {
newData.push(record);
}
}
this.windowReload(this.props.tableName);
this.setState({selectedRowKeys: [], data: newData});
} else {
this.error(res.message);
}
} catch (ex) {
logger.error('update exception, %o', ex);
hide();
this.error(`网络请求出错: ${ex.message}`);
}
}
/**
* 真正去删除数据
*/
async handleDelete(keys = this.state.selectedRowKeys) {
const CRUD = ajax.CRUD(this.props.tableName);
const hide = message.loading('正在删除...', 0);
try {
const res = await CRUD.delete(keys);
hide();
if (res.success) {
notification.success({
message: '删除成功',
description: `删除${res.data.count}条数据`,
duration: 3
});
// 数据变化后, 刷新下表格
const newData = [];
const keySet = new Set(keys); // array转set
for (const record of this.state.data) {
if (!keySet.has(record.key)) { // 是否是被删除的记录
newData.push(record);
}
}
this.setState({selectedRowKeys: [], data: newData});
} else {
this.error(res.message);
}
} catch (ex) {
logger.error('delete exception, %o', ex);
hide();
this.error(`网络请求出错: ${ex.message}`);
}
}
getEditForm() {
const {tableName, schema} = this.props;
const FormComponent = InnerTableSchemaUtils.getForm(tableName, schema);
return (
<FormComponent
wrappedComponentRef={(input) => {
this.formComponent = input;
}}
initData={this.formInitData}
data={this.state.data}
forUpdate={!this.state.modalInsert}
keysToUpdate={this.keysToUpdate}/>
);
}
render() {
const {tableLoading, tableConfig} = this.props;
// 根据当前的tableName, 获取对应的表单组件
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: this.onTableSelectChange
};
const hasSelected = this.state.selectedRowKeys.length > 0; // 是否选择
const multiSelected = this.state.selectedRowKeys.length > 1; // 是否选择了多项
const UpdateComponent = this.updateComponent;
return (
<div>
<div className="db-table-button">
<Affix offsetTop={8} target={() => document.getElementById('main-content-div')}>
<Button.Group>
{tableConfig.showInsert &&
<Button type="primary" onClick={this.onClickInsert}>
<Icon type="plus-circle-o"/> 新增
</Button>}
{/* 注意这里, 如果schema中没有定义主键, 不允许update或delete */}
{tableConfig.showUpdate &&
<Button type="primary"
disabled={!hasSelected || !this.primaryKey || (multiSelected && !tableConfig.showMultiSelected)}
onClick={this.onClickUpdate}>
<Icon type="edit"/> {multiSelected && tableConfig.showMultiSelected ? '批量修改' : '修改'}
</Button>}
{tableConfig.showDelete &&
<Button type="primary" disabled={!hasSelected || !this.primaryKey}
onClick={this.onClickDelete}>
<Icon type="delete"/> {multiSelected ? '批量删除' : '删除'}
</Button>}
</Button.Group>
</Affix>
{/*antd的modal实现中, 如果modal不显示, 那内部的组件是不会mount的, 导致第一次访问this.formComponent会undefined, 而我又需要设置表单的值, 所以新增一个initData属性*/}
<Modal title={this.state.modalTitle} visible={this.state.modalVisible} onOk={this.handleModalOk}
onCancel={this.hideModal} maskClosable={false} width={this.state.editModalWidth}>
{this.getEditForm()}
</Modal>
</div>
{/*用于图片预览的modal*/}
<Modal visible={this.state.previewVisible} footer={null} onCancel={this.cancelPreview}>
<ImageSlider items={this.state.previewImages}/>
</Modal>
{/*用于显示用户自定义组件的modal(需更新)*/}
<Modal title={this.updateComponentModalTitle} visible={this.state.componentModalVisible}
onCancel={this.handleComponentModalCancel}
onOk={this.handleComponentModalOk} maskClosable={false}>
{/*render方法首次调用时, this.updateComponent必定是undefined*/}
{/*https://github.com/ant-design/ant-design/issues/8903*/}
{this.updateComponent &&
<UpdateComponent wrappedComponentRef={(input) => {
this.updateComponentMounted = input;
}}
record={this.updateComponentRecord}/>}
</Modal>
<div className='table-responsive'>
<Table
rowSelection={tableConfig.showCheckbox && rowSelection}
columns={this.tableSchema}
dataSource={this.state.data}
pagination={false}
loading={tableLoading}/>
</div>
</div>
);
}
}
export default InnerTable;
import React from 'react';
import TableUtils from './TableUtils.js';
import Logger from '../../utils/Logger';
import Utils from '../../utils';
const logger = Logger.getLogger('InnerTableRenderUtils');
// 自定义操作字段, 在dataSchema中是用一个特殊的key来标识的
const ACTION_KEY = 'singleRecordActions';
/**
* 表格的render函数有个比较蛋疼的问题, 就是this绑定, 专门写个工具类去处理
*/
const RenderUtils = {
// 这个utils是有状态的
// 用一个set保存目前已经处理过哪些表的render, 已经处理过的就不用再处理了
tableNameSet: new Set(),
/**
* 重置状态, InnerTable组件unmount时调用
* 因为只有组件unmount后才可能需要重新绑定this
*/
reset() {
this.tableNameSet.clear();
},
/**
* 处理表格的schema, 根据情况赋值render函数
*
* @param tableSchema 表格的schema
* @param tableName 表名
* @param innerTableComponent 对应的InnerTable组件, 换句话说, 要绑定的this对象
* @returns {*}
*/
bindRender(tableSchema, tableName, innerTableComponent) {
const {onClickImage, onSingleRecordUpdate, onSingleRecordDelete, onSingleRecordComponent, fieldMap, primaryKey} = innerTableComponent;
// 命中缓存
if (this.tableNameSet.has(tableName)) {
return tableSchema;
}
tableSchema.forEach(col => {
const field = fieldMap.get(col.key);
if (!field) { // 这种情况理论上不会出现
logger.warn('unknown tableSchema col: %o', col);
return;
}
// 用户自己配置的render最优先
if (field.render) {
logger.debug('bind user-defined render for field %o', field);
col.render = field.render.bind(innerTableComponent); // 绑定this
}
// 对于某些showType我会给个默认的render
else if (field.showType === 'image') {
logger.debug('bind image render for field %o', field);
col.render = this.getImageRender()(onClickImage);
} else if (field.showType === 'file') {
logger.debug('bind file render for field %o', field);
col.render = this.getFileRender;
} else if (field.key === ACTION_KEY && field.actions && field.actions.length > 0) {
logger.debug('bind actions render for field %o', field);
col.render = this.getActionRender(field, primaryKey)(onSingleRecordUpdate, onSingleRecordDelete, onSingleRecordComponent);
}
});
const ignoreCache = TableUtils.shouldIgnoreSchemaCache(tableName);
if (!ignoreCache) {
this.tableNameSet.add(tableName);
}
return tableSchema;
},
/**
* 针对image字段的render方法
*
* @returns {function(): function()}
*/
getImageRender() {
return onClickImagePreview => text => {
if (Utils.isString(text)) {
return <img src={text} alt="图片加载失败" style={{width: '100%'}} onClick={e => onClickImagePreview(text)}/>;
} else if (text instanceof Array) {
// 如果是多张图片, 只取第一张图片在表格中显示
return <img src={text[0]} alt="图片加载失败" style={{width: '100%'}}
onClick={e => onClickImagePreview(text)}/>;
}
return null;
};
},
/**
* 针对file字段的render方法
*
* @param text
* @returns {*}
*/
getFileRender(text) {
if (Utils.isString(text) && text.length > 0) {
// 单个文件, 显示为超链接
return <a href={text} target="_blank">{text.substr(text.lastIndexOf('/') + 1)}</a>;
} else if (text instanceof Array) {
if (text.length === 0) {
return null;
}
// 多个文件, 显示为一组超链接
const urlArray = [];
urlArray.push(<a key={0} href={text[0]} target="_blank">{text[0].substr(text[0].lastIndexOf('/') + 1)}</a>);
for (let i = 1; i < text.length; i++) {
urlArray.push(<br key={-1 - i}/>);
urlArray.push(<a key={i} href={text[i]}
target="_blank">{text[i].substr(text[i].lastIndexOf('/') + 1)}</a>);
}
return <div>{urlArray}</div>;
}
return null;
},
/**
* 渲染自定义操作列
*
* @param field
* @param primaryKey
* @returns {function(): function()}
*/
getActionRender(field, primaryKey) {
// 返回一个高阶函数, 输入是3个函数
// 1. singleRecordUpdate用于更新单条记录的函数, 参数是(record:记录本身, updateKeys:要更新哪些字段)
// 2. singleRecordDelete用于删除单条记录, 参数是record
// 3. singleRecordComponent用于自定义组件实现单条记录的更新, 参数是(record:记录本身, component:要渲染的组件, name:在modal中显示时的标题)
return (singleRecordUpdate, singleRecordDelete, singleRecordComponent) => (text, record) => {
let actions;
if (typeof field.actions === 'function') {
actions = field.actions(text, record) || [];
} else {
actions = field.actions || [];
}
const actionArray = [];
// 最后一个push到array中的元素是否是分割符? 为了排版好看要处理下
let lastDivider = false;
for (let i = 0; i < actions.length; i++) {
const action = actions[i];
// visible函数用于控制当前行是否显示某个操作
if (action.visible && !action.visible(record)) {
continue;
}
// 如果没有定义主键, 不允许更新/删除
if (!primaryKey && (action.type === 'update' || action.type === 'delete')) {
continue;
}
// 换行符, 单纯为了美观
if (action.type === 'newLine') {
// 是否要去掉上一个分隔符
if (lastDivider) {
actionArray.pop();
}
actionArray.push(<br key={i}/>);
lastDivider = false;
continue;
}
// 要push到actionArray的元素
let tmp;
switch (action.type) {
// 更新单条记录, 可以控制更新哪些字段
case 'update':
tmp = <a href="#" key={i}
onClick={e => {
e.preventDefault();
singleRecordUpdate(record, action.keys);
}}>
{action.name}
</a>;
break;
// 删除单条记录
case 'delete':
tmp = <a href="#" key={i}
onClick={e => {
e.preventDefault();
singleRecordDelete(record);
}}>
{action.name}
</a>;
break;
// 自定义组件
case 'component':
tmp = <a href="#" key={i}
onClick={e => {
e.preventDefault();
singleRecordComponent(record, action.component, action.name);
}}>
{action.name}
</a>;
break;
default:
// 如果type不是预定义的几种, 就看用户是否自定义了render函数
if (action.render) {
tmp = <span key={i}>{action.render(record)}</span>;
}
}
// 如果还是不行, 那就说明用户定义的action格式有问题, 忽略
if (!tmp) {
continue;
}
actionArray.push(tmp);
actionArray.push(<span key={-1 - i} className="ant-divider"/>); // 分隔符
lastDivider = true;
}
// 去除最后一个分隔符, 为了美观
if (lastDivider) {
actionArray.pop();
}
return <span>{actionArray}</span>;
};
},
};
export default RenderUtils;
export {ACTION_KEY};
import React from 'react';
import {
Form,
Input,
DatePicker,
Select,
Radio,
InputNumber,
Checkbox,
Cascader
} from 'antd';
import TableUtils from './TableUtils.js';
import FileUploader from '../FileUploader';
import moment from 'moment';
import Logger from '../../utils/Logger';
import {ACTION_KEY} from './InnerTableRenderUtils';
import options from '../../utils/cascader-address-options';
import ajax from "../../utils/ajax";
import globalConfig from "../../config";
import createClass from 'create-react-class';
//const createClass = require('create-react-class');
const FormItem = Form.Item;
const RadioGroup = Radio.Group;
const CheckboxGroup = Checkbox.Group;
const Option = Select.Option;
const logger = Logger.getLogger('InnerTableSchemaUtils');
// 跟InnerForm类似, InnerTable也将parse schema的过程独立出来
// FIXME: 这种缓存也许用weak map更合适
const tableSchemaMap = new Map(); // key是tableName, value是表格的schema, 还有一些额外信息
const formSchemaMap = new Map(); // key是tableName, value是表单的schema callback
const formMap = new Map(); // key是tableName, value是对应的react组件
/**
* 跟InnerFormSchemaUtils非常类似, 但不用考虑布局相关的东西了
*/
const SchemaUtils = {
/**
* 解析表格的schema
*
* @param tableName
* @param schema
* @returns {*}
*/
getTableSchema(tableName, schema) {
// 做一层缓存
// 怎么感觉我在到处做缓存啊...工程化风格明显
if (tableSchemaMap.has(tableName)) {
return tableSchemaMap.get(tableName);
}
const toCache = {};
const newCols = [];
const fieldMap = new Map();
schema.forEach((field) => {
// 在表格中显示的时候, 要将radio/checkbox之类的转换为文字
// 比如schema中配置的是{key:1, value:haha}, 后端返回的值是1, 但前端展示时要换成haha
if (field.options) {
// 这样$$的前缀表示是内部的临时变量, 我觉得这种挺蛋疼的, 但没啥好办法...
field.$$optionMap = this.transformOptionMap(field.options, field.showType);
}
// 有点类似索引
fieldMap.set(field.key, field);
// 当前列是否是主键?
if (field.primary) {
toCache.primaryKey = field.key;
}
// 不需要在表格中展示
if (field.showInTable === false) {
return;
}
const col = {};
col.key = field.key;
col.dataIndex = field.key;
col.title = field.title;
col.width = field.width;
col.sorter = field.sorter;
// 我本来想在解析schema的时候配置一下render然后加到缓存里
// 但如果render中使用了this指针就会有问题
// 比如用户先使用DBTable组件, 这时会解析schema并缓存, 然后用户通过侧边栏切换到其他组件, DBTable组件unmount
// 这时render函数中的this, 就指向这个被unmount的组件了, 就算再重新切回DBTable, 也是重新mount的一个新的组件了
// 换句话说, render函数不能缓存, 必须每次解析schema后重新设置render
// js的this是一个很迷的问题...参考:http://bonsaiden.github.io/JavaScript-Garden/zh/#function.this
//if (field.render) {
// col.render = field.render;
//}
newCols.push(col);
});
toCache.tableSchema = newCols;
toCache.fieldMap = fieldMap;
const ignoreCache = TableUtils.shouldIgnoreSchemaCache(tableName);
if (!ignoreCache) {
tableSchemaMap.set(tableName, toCache);
}
return toCache;
},
/**
* 和getTableSchema配合的一个方法, 用于解析optionMap
*
* @param options
* @param showType
* @returns {{}}
*/
transformOptionMap(options, showType) {
const optionMap = {};
// 对于级联选择要特殊处理下
if (showType === 'cascader') {
const browseOption = (item) => { // dfs
optionMap[item.value] = item.label;
if (item.children) {
item.children.forEach(browseOption);
}
};
options.forEach(browseOption);
} else {
for (const option of options) {
optionMap[option.key] = option.value;
}
}
return optionMap;
},
/**
* 获取某个表单对应的react组件
*
* @param tableName
* @param schema
* @returns {*}
*/
getForm(tableName, schema) {
const ignoreCache = TableUtils.shouldIgnoreSchemaCache(tableName);
if (formMap.has(tableName)) {
return formMap.get(tableName);
} else {
const newForm = this.createForm(tableName, schema);
if (!ignoreCache) {
formMap.set(tableName, newForm);
}
return newForm;
}
},
/**
* 动态生成表单
*
* @param tableName
* @param schema
* @returns {*}
*/
createForm(tableName, schema) {
const ignoreCache = TableUtils.shouldIgnoreSchemaCache(tableName);
const that = this;
const tmpComponent = createClass({
componentWillMount() {
if (formSchemaMap.has(tableName)) {
this.schemaCallback = formSchemaMap.get(tableName);
return;
}
const schemaCallback = that.parseFormSchema(schema);
if (!ignoreCache) {
formSchemaMap.set(tableName, schemaCallback);
}
this.schemaCallback = schemaCallback;
},
// 表单挂载后, 给表单一个初始值
componentDidMount() {
if (this.props.initData) { // 这种方法都能想到, 我tm都佩服自己...
this.props.form.setFieldsValue(this.props.initData);
}
},
render() {
return this.schemaCallback(this.props.form.getFieldDecorator, this.props.forUpdate, this.props.keysToUpdate);
}
});
return Form.create()(tmpComponent);
},
/**
* 这是最主要的方法
*
* @param schema
* @returns {function()}
*/
parseFormSchema(schema) {
// 先校验数据的合法性
this.parseValidator(schema);
const rows = [];
schema.forEach((field) => {
// 有一些列不需要在表单中展示
if (field.showInForm === false) {
return;
}
if (field.key === ACTION_KEY) {
return;
}
rows.push(this.transFormField(field));
});
// 返回的schemaCallback有3个参数
// 1. getFieldDecorator, 表单组件对应的getFieldDecorator函数
// 2. forUpdate, 当前表单是用于insert还是update, 影响到校验规则
// 3. keysToUpdate, 允许更新哪些字段, 影响到modal中显示哪些字段, 仅当forUpdate=true时生效
return (getFieldDecorator, forUpdate, keysToUpdate) => {
const formRows = []; // 最终的表单中的一行
for (const row of rows) {
formRows.push(row(getFieldDecorator, forUpdate, keysToUpdate));
}
return (<Form layout="horizontal">
{formRows}
</Form>);
};
},
/**
* 有点蛋疼的一件事, dataSchema定义的表单, 要同时用于insert和update, 但二者需要的校验规则是不同的
* 比如insert时某个字段是必填的, 但update时是不需要填的
*
* @param schema
*/
parseValidator(schema) {
schema.forEach((field) => {
if (!field.validator)
return;
const newRules = [];
for (const rule of field.validator) {
newRules.push(Object.assign({}, rule, {required: true})); // update时所有字段必填
}
// 这种$$开头的变量都被我用作内部变量
field.$$updateValidator = newRules;
});
},
colWrapper(formItem, field) {
return (getFieldDecorator, forUpdate, keysToUpdate) => {
// 表单用于更新时, 可以只显示部分字段
if (forUpdate === true && keysToUpdate && !keysToUpdate.has(field.key)) {
return null;
}
return (<FormItem key={field.key} label={field.title} labelCol={{span: 4}} wrapperCol={{span: 20}}>
{formItem(getFieldDecorator, forUpdate)}
</FormItem>);
};
},
transFormField(field) {
// 对于主键, 直接返回一个不可编辑的textarea, 因为主键一般是数据库自增的
// 如果有特殊情况需要自己指定主键, 再说吧
if (field.primary === true) {
logger.debug('key %o is primary, transform to text area', field);
return this.colWrapper((getFieldDecorator, forUpdate) => getFieldDecorator(field.key)(
<Input type="textarea" autosize={{minRows: 1, maxRows: 10}} disabled size="default"/>
), field);
}
switch (field.showType) {
case 'select':
return this.transformSelect(field);
case 'radio':
return this.transformRadio(field);
case 'checkbox':
return this.transformCheckbox(field);
case 'multiSelect':
return this.transformMultiSelect(field);
case 'textarea':
return this.transformTextArea(field);
case 'image':
return this.transformImage(field);
case 'file':
return this.transformFile(field);
case 'cascader':
return this.transformCascader(field);
default:
return this.transformNormal(field);
}
},
/**
* 将schema中的一列转换为下拉框
*
* @param field
*/
transformSelect(field) {
logger.debug('transform field %o to Select component', field);
const options = [];
// 是否渲染异步数据
if (field.isAsyncData && field.optionFormat) {
let optionFormat = field.optionFormat.split(":");
field.asyncData.forEach((option) => {
options.push(<Option key={option[optionFormat[0]]}
value={option[optionFormat[0]]}>{option[optionFormat[1]]}</Option>);
});
} else {
field.options.forEach((option) => {
options.push(<Option key={option.key} value={option.key}>{option.value}</Option>);
});
}
return this.colWrapper((getFieldDecorator, forUpdate) => getFieldDecorator(field.key, {
initialValue: forUpdate ? undefined : field.defaultValue,
rules: forUpdate ? field.$$updateValidator : field.validator
})(
<Select placeholder={field.placeholder || '请选择'} size="default" disabled={field.disabled}>
{options}
</Select>
), field);
},
/**
* 将schema中的一列转换为单选框
*
* @param field
*/
transformRadio(field) {
logger.debug('transform field %o to Radio component', field);
const options = [];
field.options.forEach((option) => {
options.push(<Radio key={option.key} value={option.key}>{option.value}</Radio>);
});
return this.colWrapper((getFieldDecorator, forUpdate) => getFieldDecorator(field.key, {
initialValue: forUpdate ? undefined : field.defaultValue,
rules: forUpdate ? field.$$updateValidator : field.validator
})(
<RadioGroup disabled={field.disabled}>
{options}
</RadioGroup>
), field);
},
/**
* 将schema中的一列转换为checkbox
*
* @param field
*/
transformCheckbox(field) {
logger.debug('transform field %o to Checkbox component', field);
const options = [];
field.options.forEach((option) => {
options.push({label: option.value, value: option.key});
});
return this.colWrapper((getFieldDecorator, forUpdate) => getFieldDecorator(field.key, {
initialValue: forUpdate ? undefined : field.defaultValue,
rules: forUpdate ? field.$$updateValidator : field.validator
})(
<CheckboxGroup options={options} disabled={field.disabled}/>
), field);
},
/**
* 转换为下拉多选框
*
* @param field
* @returns {XML}
*/
transformMultiSelect(field) {
logger.debug('transform field %o to MultipleSelect component', field);
const options = [];
// 是否渲染异步数据
if (field.isAsyncData && field.optionFormat) {
let optionFormat = field.optionFormat.split(":");
field.asyncData.forEach((option) => {
options.push(<Option key={option[optionFormat[0]]}
value={option[optionFormat[0]]}>{option[optionFormat[1]]}</Option>);
});
} else {
field.options.forEach((option) => {
options.push(<Option key={option.key} value={option.key}>{option.value}</Option>);
});
}
return this.colWrapper((getFieldDecorator, forUpdate) => getFieldDecorator(field.key, {
initialValue: forUpdate ? undefined : field.defaultValue,
rules: forUpdate ? field.$$updateValidator : field.validator
})(
<Select mode="multiple" allowClear={true} placeholder={field.placeholder || '请选择'} size="default"
disabled={field.disabled}>
{options}
</Select>
), field);
},
/**
* 转换为textarea
*
* @param field
* @returns {XML}
*/
transformTextArea(field) {
logger.debug('transform field %o to textarea component', field);
return this.colWrapper((getFieldDecorator, forUpdate) => getFieldDecorator(field.key, {
initialValue: forUpdate ? undefined : field.defaultValue,
rules: forUpdate ? field.$$updateValidator : field.validator
})(
<Input type="textarea" placeholder={field.placeholder || '请输入'} autosize={{minRows: 2, maxRows: 10}}
disabled={field.disabled} size="default"/>
), field);
},
/**
* 转换为图片上传组件
*
* @param field
* @returns {XML}
*/
transformImage(field) {
logger.debug('transform field %o to image component', field);
return this.colWrapper((getFieldDecorator, forUpdate) => getFieldDecorator(field.key, {
initialValue: forUpdate ? undefined : field.defaultValue,
rules: forUpdate ? field.$$updateValidator : field.validator
})(
<FileUploader max={field.max} url={field.url} sizeLimit={field.sizeLimit} accept={field.accept}
placeholder={field.placeholder} type="image"/>
), field);
},
/**
* 转换为文件上传组件
*
* @param field
* @returns {XML}
*/
transformFile(field) {
logger.debug('transform field %o to file component', field);
return this.colWrapper((getFieldDecorator, forUpdate) => getFieldDecorator(field.key, {
initialValue: forUpdate ? undefined : field.defaultValue,
rules: forUpdate ? field.$$updateValidator : field.validator
})(
<FileUploader max={field.max} url={field.url} sizeLimit={field.sizeLimit} accept={field.accept}
placeholder={field.placeholder}/>
), field);
},
/**
* 转换为级联选择
*
* @param field
* @returns {XML}
*/
transformCascader(field) {
logger.debug('transform field %o to Cascader component', field);
return this.colWrapper((getFieldDecorator, forUpdate) => getFieldDecorator(field.key, {
initialValue: forUpdate ? undefined : field.defaultValue,
rules: forUpdate ? field.$$updateValidator : field.validator
})(
<Cascader options={options} expandTrigger="hover" placeholder={field.placeholder || '请选择'} size="default"
disabled={field.disabled}/>
), field);
},
/**
* 将schema中的一列转换为普通输入框
*
* @param field
* @returns {XML}
*/
transformNormal(field) {
switch (field.dataType) {
case 'int':
logger.debug('transform field %o to integer input component', field);
return this.colWrapper((getFieldDecorator, forUpdate) => getFieldDecorator(field.key, {
initialValue: forUpdate ? undefined : field.defaultValue,
rules: forUpdate ? field.$$updateValidator : field.validator
})(
<InputNumber size="default" max={field.max} min={field.min} placeholder={field.placeholder}
disabled={field.disabled}/>
), field);
case 'float':
logger.debug('transform field %o to float input component', field);
return this.colWrapper((getFieldDecorator, forUpdate) => getFieldDecorator(field.key, {
initialValue: forUpdate ? undefined : field.defaultValue,
rules: forUpdate ? field.$$updateValidator : field.validator
})(
<InputNumber step={0.01} size="default" max={field.max} min={field.min}
placeholder={field.placeholder}
disabled={field.disabled}/>
), field);
case 'datetime':
logger.debug('transform field %o to datetime input component', field);
return this.colWrapper((getFieldDecorator, forUpdate) => getFieldDecorator(field.key, {
initialValue: forUpdate ? undefined : (field.defaultValue ? moment(field.defaultValue) : null), // 这个表达式是真的有点蛋疼
rules: forUpdate ? field.$$updateValidator : field.validator
})(
<DatePicker showTime format="YYYY-MM-DD HH:mm:ss" placeholder={field.placeholder || '请选择日期'}
disabled={field.disabled}/>
), field);
default: // 默认就是普通的输入框
logger.debug('transform field %o to varchar input component', field);
return this.colWrapper((getFieldDecorator, forUpdate) => getFieldDecorator(field.key, {
initialValue: forUpdate ? undefined : field.defaultValue,
rules: forUpdate ? field.$$updateValidator : field.validator
})(
<Input placeholder={field.placeholder} size="default" addonBefore={field.addonBefore}
addonAfter={field.addonAfter} disabled={field.disabled}/>
), field);
}
}
};
export default SchemaUtils;
import React from 'react';
import {notification} from 'antd';
import globalConfig from '../../config.js';
import ajax from '../../utils/ajax';
import Logger from '../../utils/Logger';
const logger = Logger.getLogger('TableUtils');
// 缓存, key是tableName, value是{querySchema, dataSchema}
const tableMap = new Map();
// 缓存, key是tableName, value是tableConfig
const configMap = new Map();
/**
* 用于解析表schema的工具类
*/
export default {
// 将getSchema的函数分为3个, 分别用于不同情况
// 其实就是从远程加载schema时比较特殊, 要显示一个loading提示给用户, 必须是async函数, 其他的就是普通的同步函数
/**
* 从缓存中获取schema
*
* @param tableName
* @returns {V}
*/
getCacheSchema(tableName) {
return tableMap.get(tableName);
},
/**
* 从本地的js文件中读取schema, 会更新缓存
*
* @param tableName
* @returns {{querySchema: *, dataSchema: *}}
*/
getLocalSchema(tableName) {
const ignoreCache = this.shouldIgnoreSchemaCache(tableName);
let querySchema, dataSchema;
try {
querySchema = require(`../../schema/${tableName}.querySchema.js`);
// 如果是忽略cache, 每次读取的schema都必须是全新的
if (ignoreCache) {
querySchema = querySchema.map(item => Object.assign({}, item)); // Object.assign是浅拷贝, 不过没啥影响
}
} catch (e) {
logger.error('load query schema error: %o', e);
}
try {
dataSchema = require(`../../schema/${tableName}.dataSchema.js`);
if (ignoreCache) {
dataSchema = dataSchema.map(item => Object.assign({}, item));
}
} catch (e) {
logger.error('load data schema error: %o', e);
}
// 注意这里会更新缓存
const toCache = {querySchema, dataSchema};
if (!ignoreCache) {
tableMap.set(tableName, toCache);
}
return toCache;
},
/**
* 从远程获取某个表的schema, 如果有本地schema的话会合并
* 这个方法会更新缓存
*
* @param tableName
* @returns {{querySchema: *, dataSchema: *}}
*/
async getRemoteSchema(tableName) {
const ignoreCache = this.shouldIgnoreSchemaCache(tableName);
const localSchema = this.getLocalSchema(tableName);
let querySchema, dataSchema;
try {
const res = await ajax.CRUD(tableName).getRemoteSchema();
logger.debug('get remote schema for table %s, res = %o', tableName, res);
if (res.success) {
querySchema = this.merge(localSchema.querySchema, res.data.querySchema);
dataSchema = this.merge(localSchema.dataSchema, res.data.dataSchema);
} else {
logger.error('getRemoteSchema response error: %o', res);
this.error(`请求asyncSchema失败: ${res.message}`);
}
} catch (e) {
logger.error('getRemoteSchema network request error: %o', e);
this.error(`请求asyncSchema时网络失败: ${e.message}`);
}
// 更新缓存
const toCache = {querySchema, dataSchema};
if (!ignoreCache) {
tableMap.set(tableName, toCache);
}
return toCache;
},
/**
* 合并本地的schema和远程的schema, 其实就是合并两个array
*
* @param local 本地schema
* @param remote 远程schema
* @returns {*}
*/
merge(local, remote) {
// 注意local和remote都可能是undefined
// 只有二者都不是undefined时, 才需要merge
if (local && remote) {
const result = local; // 合并后的结果
const map = new Map();
result.forEach(item => map.set(item.key, item));
// 注意合并的逻辑: 如果远程的key本地也有, 就更新; 否则新增, 新增的列都放在最后
remote.forEach(item => {
if (map.has(item.key)) {
// 注意传值vs传引用的区别
Object.assign(map.get(item.key), item);
} else {
result.push(item);
}
});
return result;
} else {
// 注意这个表达式
return local || remote;
}
},
/**
* 弹出一个错误信息提示用户
*
* @param errorMsg
*/
error(errorMsg) {
notification.error({
message: '出错啦!',
description: ` ${errorMsg}`,
duration: 0,
});
},
/**
* 获取某个表的个性化配置, 会合并默认配置
*
* @param tableName
* @returns {*}
*/
getTableConfig(tableName) {
if (configMap.has(tableName)) {
return configMap.get(tableName);
}
let tableConfig;
try {
const tmp = require(`../../schema/${tableName}.config.js`);
tableConfig = Object.assign({}, globalConfig.DBTable.default, tmp);
} catch (e) {
logger.warn('can not find config for table %s, use default instead', tableName);
tableConfig = Object.assign({}, globalConfig.DBTable.default);
}
configMap.set(tableName, tableConfig);
return tableConfig;
},
/**
* 某个表是否应该忽略缓存
*
* @param tableName
* @returns {boolean}
*/
shouldIgnoreSchemaCache(tableName) {
const tableConfig = this.getTableConfig(tableName);
return tableConfig.asyncSchema === true && tableConfig.ignoreSchemaCache === true;
},
};
import React from 'react';
import {message, notification, Spin} from 'antd';
import Error from '../Error';
import InnerForm from './InnerForm.js';
import InnerTable from './InnerTable.js';
import InnerPagination from './InnerPagination.js';
import TableUtils from './TableUtils.js';
import './index.less';
import ajax from '../../utils/ajax';
import Utils from '../../utils';
import globalConfig from '../../config.js';
import Logger from '../../utils/Logger';
const logger = Logger.getLogger('DBTable');
/**
* 操作数据库中的一张表的组件, 又可以分为3个组件: 表单+表格+分页器
*/
class DBTable extends React.PureComponent {
// 父组件要保存子组件的状态...非常蛋疼...
// 破坏了子组件的"封闭"原则
// 但这是官方推荐的做法: https://facebook.github.io/react/docs/lifting-state-up.html
// 注意: 向父组件传状态, 通过回调函数的形式
// 从父组件接收状态, 通过props的形式
state = {
// 本身的状态
loadingSchema: false, // 是否正在从远程加载schema
// 表单组件的状态
queryObj: {}, // 表单中的查询条件
// 表格组件的状态
data: [], // 表格中显示的数据
tableLoading: false, // 表格是否是loading状态
// 分页器的状态
currentPage: 1, // 当前第几页, 注意页码是从1开始的, 以前总是纠结页码从0还是1开始, 这里统一下, 跟显示给用户的一致
pageSize: globalConfig.DBTable.pageSize, // pageSize默认值50, 这个值一旦初始化就是不可变的
showSizeChanger: globalConfig.DBTable.showSizeChanger, // 是否显示修改每页显示数量的选项
pageSizeOptions: globalConfig.DBTable.pageSizeOptions, // 每页面显示数量选项
total: 0, // 总共有多少条数据
breakpoint: 767,
isSimple: document.documentElement.clientWidth <= 767,
};
// 这里有个很有意思的问题, 就是异步操作的局限性, 你没办法控制callback何时被调用
// 我本来的写法是这样的:
// async componentWillMount() {
// // tryFetchSchema方法可能是同步也可能是异步, 跟tableConfig.asyncSchema有关
// // 如果是同步调用, 会直接返回一个resolved状态的promise
// // 如果是异步调用, 会返回一个pending状态的promise
// // 注意, 所有async方法, 直接调用的话, 必然会返回promise, 这是语言特性决定的
// const res = await this.tryFetchSchema(this.props);
// this.updateTableState(res);
// if (this.state.loadingSchema) {
// this.setState({loadingSchema: false}, this.refresh);
// }
// }
// 注意其中的tryFetchSchema可能同步也可能异步
// 我本来期望着如果是同步调用的话, 下面的updateTableState语句会立刻执行, 如果是异步调用, 就等异步操作结束后再执行updateTableState
// 但实际情况是, 即使是同步调用(直接返回一个resolved状态的promise), 下面的代码也不会立刻执行
// 这可能和async函数的特性有关, 即使直接return一个常量, 也会被当作异步操作对待
// async/await语义只保证语句的"执行顺序", 而不保证执行的"间隔"
// 同理, 各种回调都是不能保证事件发生后"立即"被执行的, 这是js event loop的局限, 也许应该说是"特性"?
// 于是我只能改成下面这种普通的callback方式, 手动控制何时执行callback, 不能用async/await了
// 如果是同步操作就立刻执行callback, 否则等异步操作结束再执行callback
// 另一个有意思的问题就是, 如果将react的生命周期方法做成async的会怎样?
// 关键要了解async函数的执行逻辑, 尤其是多个async函数嵌套时, 了解代码执行权的交换过程
// 如果知道async/await的本质就是生成器, 而生成器的本质就是协程, 那就很好理解了
componentWillMount() {
// 处理url参数
this.processQueryParams();
// 组件初始化时尝试获取schema
this.tryFetchSchema(this.props, (res) => {
this.updateTableState(res);
// 这个参数用于判断获取schema是同步还是异步
if (this.state.loadingSchema) {
this.setState({loadingSchema: false}, this.refresh);
}
});
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
/**
* 刚进入页面时触发一次查询
*/
async componentDidMount() {
// 如果是异步获取schema的话, 后面有callback会调用refresh的, 这里就不用调了
if (!this.state.loadingSchema) {
this.refresh();
}
}
// 在react router中切换时, 组件不会重新mount, 只有props会变化
componentWillReceiveProps(nextProps) {
// 普通模式下, 所有的CRUD操作都是通过同一个DBTable组件进行的, 只是传入的tableName不同而已
// 但是在tab模式下, 为了防止不同tab之间的干扰, 每个tab下都必须是一个"独立"的组件, 换句话说有很多不同DBTable组件的实例
// 类似单例和多实例的区别
if (globalConfig.tabMode.enable === true) {
logger.debug('ignore props update under tabMode');
return;
}
// FIXME: hack, 和App组件中componentWillReceiveProps方法类似
const action = this.props.location.action;
if (action === 'PUSH') {
return;
}
logger.debug('receive new props and try to render, nextProps = %o', nextProps);
// 应该只有react router会触发这个方法
if (nextProps.routes) {
// 如果表名不变的话, 没必要重新加载schema/refresh, 直接return
const routes = nextProps.routes;
const nextTableName = routes[routes.length - 1].tableName;
if (nextTableName === this.tableName) {
return;
}
}
// 在表名切换后要做什么?
// 1. 根据新的表名重新获取schema
// 2. 还原初始状态
// 3. 调用一次refresh(), 重新查询数据
// 和组件挂载时类似, 同样注意区分同步/异步
this.tryFetchSchema(nextProps, (res) => {
this.updateTableState(res);
// 处理url参数
this.state.queryObj = {};
this.processQueryParams();
this.setState({
data: [],
tableLoading: false,
currentPage: 1,
total: 0,
loadingSchema: false,
}, this.refresh);
});
}
/**
* 尝试获取schema, 可能是同步也可能是异步
* 获取schema成功后, 调用回调
*
* @param props
* @param callback
* @returns {undefined}
*/
async tryFetchSchema(props, callback) {
const routes = props.routes;
// 这个tableName是路由表配置中传过来的
// 可以用这个方法向组件传值
const tableName = routes.pop().tableName;
if (tableName) {
logger.info('init component DBTable with tableName = %s', tableName);
} else {
logger.error('can not find tableName, check your router config');
this.inited = false; // 是否成功获取schema
this.errorMsg = '找不到表名, 请检查路由配置'; // 如果没能成功获取schema, 错误信息是什么?
return;
}
const tableConfig = TableUtils.getTableConfig(tableName);
// 这里注意, 区分同步/异步
let tmp = TableUtils.getCacheSchema(tableName);
if (!tmp) {
if (tableConfig.asyncSchema === true) {
// 如果是异步的, 必须给用户一个loading提示
this.state.loadingSchema = true;
tmp = await TableUtils.getRemoteSchema(tableName);
} else {
tmp = TableUtils.getLocalSchema(tableName);
}
}
const res = {...tmp, tableName, tableConfig};
//logger.info("res====>", res.dataSchema);
callback(res);
}
/**
* fetch schema后, 更新当前组件的状态, 主要是更新一些this.XXX变量
* 必须和tryFetchSchema方法配合使用
*
* @param input
*/
async updateTableState(input) {
// 其实很多这种this.xxx变量也可以做成状态, 看情况了
// 关键是这些变量变化时, 是否要触发重新render?
// 这两项是肯定会有的
this.tableName = input.tableName;
this.tableConfig = input.tableConfig;
if (input.querySchema) {
this.querySchema = input.querySchema;
} else {
this.inited = false;
this.errorMsg = `加载${input.tableName}表的querySchema出错, 请检查配置`;
return;
}
if (input.dataSchema) {
this.dataSchema = input.dataSchema;
} else {
this.inited = false;
this.errorMsg = `加载${input.tableName}表的dataSchema出错, 请检查配置`;
return;
}
// 如果一切正常, 设置init=true
this.inited = true;
// 检查是否有字段设置了异步加载数据
for (let field in this.dataSchema) {
if (this.dataSchema[field].isAsyncData && this.dataSchema[field].url) {
const result = await ajax.getAsyncData(this.dataSchema[field].url);
this.dataSchema[field].asyncData = result.data.list;
}
}
//console.log("dataSchema====>", this.dataSchema);
}
/**
* 可以在url上加参数, 改变查询条件
*/
processQueryParams() {
// 这个方法可以算作一个后门, 甚至可以传入一些querySchema中没配置的参数, 只要后端能处理就可以
const params = Utils.getAllQueryParams();
// 如果url上有参数
if (Object.keys(params).length > 0) {
this.state.queryObj = Object.assign({}, this.state.queryObj, params);
}
}
/**
* 按当前的查询条件重新查询一次
*/
refresh = async () => {
// 如果加载schema失败, 就不要查询了
if (!this.inited) {
return;
}
const res = await this.select(this.state.queryObj, this.state.currentPage, this.state.pageSize);
//message.success('查询成功');
if (res.success) {
this.setState({
data: res.data.list,
total: res.data.total,
tableLoading: false,
});
} else {
this.error(res.message);
}
};
/**
* 弹出错误信息(需用户手动关闭)
*
* @param errorMsg
*/
error = (errorMsg) => {
notification.error({
message: '出错啦!',
description: ` ${errorMsg}`,
duration: 0,
});
this.setState({tableLoading: false});
};
/**
* 向服务端发送select请求
*
* @param queryObj 包含了form中所有的查询条件, 再加上page和pageSize, 后端就能拼成完整的sql
* @param page
* @param pageSize
* @returns {Promise}
*/
async select(queryObj, page, pageSize) {
// 为啥这个方法不用箭头函数, 但也不会有this的问题呢? 我猜测是因为这个方法都是被其他箭头函数调用的, 所以也会自动bind this
// 同理上面的error函数似乎也不需要是箭头函数
const tmpObj = Object.assign({}, queryObj); // 创建一个新的临时对象, 其实直接修改queryObj也可以
tmpObj.page = page - 1; // springBoot pageable 需要 -1
tmpObj.size = pageSize;
// 每次查询时, 要显示一个提示, 同时table组件也要变为loading状态
const hide = message.loading('正在查询...', 0);
try {
const CRUD = ajax.CRUD(this.tableName);
this.setState({tableLoading: true});
const res = await CRUD.select(tmpObj);
// 请求结束后, 提示消失, 但不要急着还原tableLoading的状态, 让上层调用的方法去还原
hide();
return res;
} catch (ex) { // 统一的异常处理, 上层方法不用关心
logger.error('select exception, %o', ex);
hide();
const res = {}; // 手动构造一个res返回
res.success = false;
res.message = `网络请求出错: ${ex.message}`;
return Promise.resolve(res); // 还是要返回一个promise对象
}
}
/**
* 切换分页时触发查询
*
* @param page
*/
handlePageChange = async (page) => {
logger.debug('handlePageChange, page = %d', page);
const res = await this.select(this.state.queryObj, page, this.state.pageSize);
if (res.success) {
this.setState({
currentPage: page,
data: res.data.list,
total: res.data.total,
tableLoading: false,
});
} else {
this.error(res.message);
}
};
/**
* 切换每页显示数量时触发查询
*
* @param page
*/
handleShowPageChange = async (page, pageSize) => {
logger.debug('handleShowPageSizeChange, page = %d', page);
const res = await this.select(this.state.queryObj, page, pageSize);
if (res.success) {
this.setState({
currentPage: page,
data: res.data.list,
total: res.data.total,
tableLoading: false,
pageSize: pageSize,
});
} else {
this.error(res.message);
}
};
/**
* 点击提交按钮时触发查询
*
* @param queryObj
*/
handleFormSubmit = async (queryObj) => {
logger.debug('handleFormSubmit, queryObj = %o', queryObj);
// 这时查询条件已经变了, 要从第一页开始查
const res = await this.select(queryObj, 1, this.state.pageSize);
if (res.success) {
this.setState({
currentPage: 1,
data: res.data.list,
total: res.data.total,
tableLoading: false,
queryObj: queryObj,
});
} else {
this.error(res.message);
}
};
handleResize = () => {
var w = document.documentElement.clientWidth;
this.setState({
isSimple: w <= this.state.breakpoint
});
};
render() {
// 一段有些tricky的代码, 某些情况下显示一个特殊的loading
// 主要是为了用户第一次进入的时候, 交互更友好
// FIXME: 这段代码非常丑, (!this.inited && !this.errorMsg)这个条件是为了hack一个react-router的问题
// 如果从首页点击侧边栏进入DBTable组件, 会依次触发componentWillMount和componentWillReceiveProps, 而直接从url进入的话则只会触发componentWillMount
if (this.state.loadingSchema && (!this.notFirstRender || (!this.inited && !this.errorMsg))) {
this.notFirstRender = true;
return (
<Spin tip="loading schema..." spinning={this.state.loadingSchema} delay={500}>
<div style={{height: '150px', width: '100%'}}></div>
</Spin>
);
}
this.notFirstRender = true;
// 如果没能成功加载schema, 显示错误信息
// 注意从错误信息切换到另一个表时, 也可能出现loading状态
if (!this.inited) {
return (
<Spin tip="loading schema..." spinning={this.state.loadingSchema} delay={500}>
<Error errorMsg={this.errorMsg}/>
</Spin>
);
}
// 1. 之前传props是直接{...this.state}, 感觉会影响效率, 传很多无用的属性
// 2. 父组件传进去的方法名都是parentHandleXXX
// 3. InnerForm和InnerPagination都是无状态的, 但InnerTable还是要维护自己的一些状态
return (
<Spin spinning={this.state.loadingSchema} delay={500}>
{this.tableConfig.showSearchForm &&
<InnerForm parentHandleSubmit={this.handleFormSubmit} schema={this.querySchema}
tableConfig={this.tableConfig}
tableName={this.tableName}/>}
<InnerTable data={this.state.data} tableLoading={this.state.tableLoading}
schema={this.dataSchema} refresh={this.refresh}
tableConfig={this.tableConfig} tableName={this.tableName}/>
{this.tableConfig.showPagination &&
<InnerPagination currentPage={this.state.currentPage} total={this.state.total}
pageSize={this.tableConfig.pageSize || this.state.pageSize}
parentHandlePageChange={this.handlePageChange} tableConfig={this.tableConfig}
showSizeChanger={this.state.showSizeChanger}
pageSizeOptions={this.state.pageSizeOptions}
parentHandleShowPageChange={this.handleShowPageChange}
tableName={this.tableName} isSimple={this.state.isSimple}/>
}
</Spin>
);
}
}
export default DBTable;
.ant-advanced-search-form {
padding: 16px 8px;
background: #f8f8f8;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
/* 由于输入标签长度不确定,所以需要微调使之看上去居中 */
.ant-advanced-search-form > .ant-row {
position: relative;
left: -6px;
}
.ant-advanced-search-form .ant-btn + .ant-btn {
margin-left: 8px;
}
/* 我讨厌写css... */
.db-pagination {
margin-top: 8px;
margin-bottom: 8px;
}
.db-table-button {
margin-top: 8px;
margin-bottom: 8px;
}
.ant-upload {
margin-left: 4px;
}
import React from 'react';
import {Icon} from 'antd';
import './index.less';
/**
* 显示错误信息
* 可以当404页来用
*/
class Error extends React.PureComponent {
render() {
return (
<div className="not-found">
<div style={{ fontSize:32 }}><Icon type="frown-o"/></div>
<h1>{this.props.errorMsg || '404 Not Found'}</h1>
</div>
);
}
}
export default Error;
.not-found {
color: black;
text-align: center;
}
.not-found h1 {
font-family: "PingFang SC",
}
import React, {Fragment} from 'react';
import {Upload, Icon, Modal, message, Button, Tooltip} from 'antd';
import globalConfig from '../../config.js';
import Utils from '../../utils';
import Logger from '../../utils/Logger.js';
import './index.less';
import PropTypes from 'prop-types';
const logger = Logger.getLogger('FileUploader');
/**
* 文件上传组件, 样式基本是从antd官网抄过来的
* 可以上传图片, 也可以上传普通文件, 样式会不一样, 通过props中传入的type字段判断
*
* 这个组件可以配合antd的FormItem使用, 以后可以参考
*/
class FileUploader extends React.Component {
// 注意这个组件不能做成PureComponent, 会有bug, 因为上传的过程中会不断触发onChange, 进而导致状态不断变化
state = {
previewVisible: false, // 是否显示图片预览modal
previewImage: '', // 要预览的图片
fileList: [], // 已经上传的文件列表
};
componentWillMount() {
const {defaultValue, max, url, type} = this.props;
// 当前是要上传图片还是普通图片? 会影响后续的很多东西
const forImage = type === 'image';
if (forImage) {
this.listType = 'picture-card'; // 对于图片类型的上传, 要显示缩略图
} else {
this.listType = 'text'; // 对于其他类型的上传, 只显示个文件名就可以了
}
// 组件第一次加载的时候, 设置默认值
this.forceUpdateStateByValue(defaultValue, max);
// 是否自定义了图片上传的路径
if (url) {
if (url.startsWith('http')) {
this.uploadUrl = url;
} else {
this.uploadUrl = `${globalConfig.getAPIPath()}${url}`;
}
} else {
this.uploadUrl = `${globalConfig.getAPIPath()}${forImage ? globalConfig.upload.image : globalConfig.upload.file}`; // 默认上传接口
}
// 上传时的文件大小限制
if (this.props.sizeLimit) {
this.sizeLimit = this.props.sizeLimit;
} else {
// 默认的大小限制
if (forImage) {
this.sizeLimit = globalConfig.upload.imageSizeLimit;
} else {
this.sizeLimit = globalConfig.upload.fileSizeLimit;
}
}
// 允许上传的文件类型
if (this.props.accept) {
this.accept = this.props.accept;
} else if (forImage) {
this.accept = '.jpg,.png,.gif,.jpeg'; // 上传图片时有默认的accept
}
logger.debug('type = %s, upload url = %s, sizeLimit = %d, accept = %s', type, this.uploadUrl, this.sizeLimit, this.accept);
this.forImage = forImage;
}
componentWillReceiveProps(nextProps) {
// 如果上层通过props传过来一个value, 要不要根据value更新文件列表?
// 对于普通的controlled-components而言, 是应该更新的, 相当于本身没有状态, 完全被上层控制
// 但这个组件不是完全的controlled-components...只会向外暴露value, 但也有自己的状态
// 传进来的value有两种情况:
// 1. 本身状态变化后, 通过onChange回调向外暴露, 状态又会通过this.props.value的形式回传, 这种情况不需要更新
// 2. 外界直接setFieldValue, 直接改变这个组件的状态, 这种情况下需要更新
if (this.needRender(nextProps) && nextProps.notReRender !== true) {
const {value, max} = nextProps;
this.forceUpdateStateByValue(value, max);
}
}
/**
* 将props中传过来的value跟当前的state相比较, 看是否要更新state
*
* @param nextProps
* @returns {boolean}
*/
needRender(nextProps) {
const {value} = nextProps;
// 如果外界传过来的value是undefined或者空字符串, 需要清空文件上传列表
if (!value) { // 注意空字符串也是false
return true;
}
// 当前已经上传的文件列表
const fileArray = this.state.fileList.filter(file => file.status === 'done');
// 外界传过来一个string
if (Utils.isString(value)) {
if (fileArray.length !== 1 || value !== fileArray[0].url) { // 当前没有上传文件, 或者已经上传的文件和外界传过来的不是同一个文件, 需要替换
return true;
}
}
// 外界传过来一个数组
else if (value instanceof Array) {
// 两个数组对应的文件url必须完全一样, 才认为是同样的数据, 不需更新
if (value.length !== fileArray.length) {
return true;
}
for (let i = 0; i < value.length; i++) {
if (value[i] !== fileArray[i].url) {
return true;
}
}
}
return false;
}
/**
* 强制更新state中的上传文件列表
*
* @param value
* @param max
*/
forceUpdateStateByValue(value, max) {
// 首先清空文件列表
this.state.fileList.length = 0;
// 注意传进来的value可能是个空字符串
if (Utils.isString(value) && value.length > 0) {
this.state.fileList.push({
uid: -1,
name: value.substr(value.lastIndexOf('/') + 1), // 取url中的最后一部分作为文件名
status: 'done',
url: value,
});
} else if (value instanceof Array) {
if (max === 1 && value.length > 0) {
// 如果max=1, 正常情况下value应该是个string的
// 但如果传进来一个数组, 就只取第一个元素
this.state.fileList.push({
uid: -1,
name: value[0].substr(value[0].lastIndexOf('/') + 1),
status: 'done',
url: value[0],
});
} else {
for (let i = 0; i < value.length; i++) {
this.state.fileList.push({
uid: -1 - i,
name: value[i].substr(value[i].lastIndexOf('/') + 1),
status: 'done',
url: value[i],
});
}
}
}
}
/**
* 调用上传接口之前校验一次
*
* @param file
* @returns {boolean}
*/
beforeUpload = (file) => {
if (this.sizeLimit) {
if (file.size / 1024 > this.sizeLimit) {
message.error(`${this.forImage ? '图片' : '文件'}过大,最大只允许${this.sizeLimit}KB`);
return false;
}
}
return true;
};
/**
* 点击预览按钮
*
* @param file
*/
handlePreview = (file) => {
this.setState({
previewImage: file.url || file.thumbUrl,
previewVisible: true,
});
};
/**
* 预览界面点击取消按钮
*/
handleCancel = () => this.setState({previewVisible: false});
/**
* 上传文件时的回调, 注意上传过程中会被调用多次
* 删除图片时也会触发这个方法
* 参考: https://ant.design/components/upload-cn/#onChange
*
* @param fileList
*/
handleChange = ({file, fileList}) => {
// 还要自己处理一下fileList
for (const tmp of fileList) {
if (tmp.status === 'done' && !tmp.url && tmp.response && tmp.response.success) {
tmp.url = tmp.response.data.url; // 服务端返回的url
}
}
// 上传失败
if (file.status === 'error') {
// debug模式下, 上传是必定失败的, 为了测试用, 给一个默认图片
if (globalConfig.debug) {
message.info(`debug模式下使用测试${this.forImage ? '图片' : '文件'}`, 2.5);
fileList.push({
uid: Date.now(),
name: this.forImage ? 'avatar.jpg' : 'mapreduce-osdi04.pdf',
status: 'done',
url: this.forImage ? 'http://jxy.me/about/avatar.jpg' : 'https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/mapreduce-osdi04.pdf',
});
this.notifyFileChange();
} else {
message.error(`${file.name}上传失败`, 2.5);
}
}
// 上传成功 or 删除图片
else if (file.status === 'done' || file.status === 'removed') {
this.notifyFileChange();
}
// 其实还有正在上传(uploading)/错误(error)的状态, 不过这里不关心
// 注意对于controlled components而言, 这步setState必不可少
// 见https://github.com/ant-design/ant-design/issues/2423
this.setState({fileList});
// 其实这里可能有点小问题
// notifyFileChange方法会通知上层组件, 文件列表变化了, 对于antd的FormItem而言, 新的值又会通过props.value的形式回传, 导致re-render
// 也就是说, 在已经调用过notifyFileChange的情况下, 其实不需要上面的手动setState再触发re-render, 有点重复, 效率上可能会受点影响
// 但我还是决定保留setState, 因为props.value其实是antd中受控表单组件的特殊逻辑, 而这个组件可能不只用于FormItem
};
/**
* 文件列表变化后, 通知上层
*/
notifyFileChange = () => {
const {onChange, max} = this.props;
if (onChange) {
// 传给回调的参数可能是个string, 也可能是个array, 要判断一下
let res;
if (max === 1) {
// 这里要判断下状态, 因为文件被删除后状态会变为removed
// 只返回给上层"正确"的图片
if (this.state.fileList.length > 0 && this.state.fileList[0].status === 'done') {
res = this.state.fileList[0].url;
} else {
res = '';
}
} else {
res = this.state.fileList.filter(file => file.status === 'done').map(file => file.url); // 注意先filter再map, 因为map必须是一一对应的
// 如果res是undefined, 那对应的, 后端收到的就是null; 如果res是空的数组, 后端收到的就是一个空的List. 注意这两种区别.
}
// 这个回调配合getValueFromEvent可以定制如何从组件中取值, 很方便, 参考: https://ant.design/components/form-cn/#getFieldDecorator(id,-options)-参数
// 但是我这次没用到, 因为默认的getValueFromEvent已经可以满足需求
onChange(res);
}
};
/**
* 上传按钮的样式, 跟文件类型/当前状态都有关
*/
renderUploadButton() {
const {fileList} = this.state;
const disabled = fileList.length >= this.props.max;
if (this.forImage) {
const button = (<Fragment>
<Icon type="plus"/>
<div className="ant-upload-text">上传图片</div>
</Fragment>);
// 对于图片而言, 如果文件数量达到max, 上传按钮直接消失
if (disabled) {
return null;
}
// 是否有提示语
if (this.props.placeholder) {
return <Tooltip title={this.props.placeholder} mouseLeaveDelay={0}>
{button}
</Tooltip>;
} else {
return button;
}
} else {
// 对于普通文件而言, 如果数量达到max, 上传按钮不可用
const button = <Button disabled={disabled}><Icon type="upload"/> 上传</Button>;
// 是否要有提示语
if (this.props.placeholder && !disabled) {
return <Tooltip title={this.props.placeholder} mouseLeaveDelay={0}>
{button}
</Tooltip>;
} else {
return button;
}
}
}
render() {
const {previewVisible, previewImage, fileList} = this.state;
// 我本来是写成accept="image/*"的, 但chrome下有些bug, 要很久才能弹出文件选择框
// 只能用后缀名的写法了
return (
<Fragment>
<Upload
action={this.uploadUrl}
listType={this.listType}
fileList={fileList}
onPreview={this.forImage ? this.handlePreview : undefined}
onChange={this.handleChange}
beforeUpload={this.beforeUpload}
accept={this.accept}
withCredentials={globalConfig.isCrossDomain()}
>
{this.renderUploadButton()}
</Upload>
{/*只有上传图片时才需要这个预览modal*/}
{this.forImage &&
<Modal visible={previewVisible} footer={null} onCancel={this.handleCancel}>
<img alt="图片加载失败" style={{width: '100%'}} src={previewImage}/>
</Modal>}
</Fragment>
);
}
}
FileUploader.propTypes = {
max: PropTypes.number.isRequired, // 最多可以上传文件数量
sizeLimit: PropTypes.number, // 大小限制, 单位KB
onChange: PropTypes.func, // 上传后的回调函数
defaultValue: PropTypes.oneOfType([ // 默认值, 可以是单个文件, 也可以是一组文件
PropTypes.string,
PropTypes.array,
]),
value: PropTypes.oneOfType([ // 受控组件
PropTypes.string,
PropTypes.array,
]),
url: PropTypes.string, // 自定义上传接口
type: PropTypes.string, // type=image表示上传图片, 否则上传普通文件
accept: PropTypes.string, // 上传时允许选择的文件类型, 例子:".jpg,.png,.gif"
placeholder: PropTypes.string, // 提示语
};
FileUploader.defaultProps = {
max: 1, // 默认只能上传一个文件
};
export default FileUploader;
.ant-upload-select-picture-card i {
font-size: 28px;
color: #999;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
font-size: 12px;
color: #666;
}
// 不知为何需要微调下样式才能居中, 跟antd官网上不太一样
.ant-upload-select-picture-card .ant-upload {
margin-left: 0;
}
import React from 'react';
import {BackTop} from 'antd';
import globalConfig from '../../config.js';
import './index.less';
/**
* 定义Footer组件
*/
class Footer extends React.PureComponent {
state = {
count: 0
};
queryScrollParent = () => {
var width = document.documentElement.clientWidth;
return width >= 992 ? document.getElementById('main-content-div') : window;
};
render() {
const text = globalConfig.footer || 'footer被弄丢啦!';
// backtop如果不设置target会有问题
// footer的字可以有html标签, 有一定XSS的风险, 不过问题不大
return (
<div data-count={this.state.count}>
<BackTop target={this.queryScrollParent} />
<div className="ant-layout-footer" dangerouslySetInnerHTML={{ __html: text }}/>
</div>
);
}
}
export default Footer;
.ant-layout-footer {
height: 64px;
line-height: 64px;
text-align: center;
font-size: 12px;
color: #999;
background: #fff;
border-top: 1px solid #e9e9e9;
width: 100%;
}
.ant-back-top {
right: 2em;
bottom: 6em;
}
\ No newline at end of file
import React from 'react';
import {Link} from 'react-router';
import {Icon, Menu} from 'antd';
import Logger from '../../utils/Logger';
import globalConfig from '../../config';
import './index.less';
import {headerMenu} from '../../menu.js';
const SubMenu = Menu.SubMenu; // 为了使用方便
const MenuItem = Menu.Item;
const MenuItemGroup = Menu.ItemGroup;
const logger = Logger.getLogger('Header');
/**
* 定义Header组件, 包括登录/注销的链接, 以及一些自定义链接
*/
class Header extends React.PureComponent {
// parse菜单的过程和sidebar组件差不多, copy&paste
transFormMenuItem(obj, paths) {
const parentPath = paths.join('/');
logger.debug('transform %o to path %s', obj, parentPath);
return (
<MenuItem key={obj.key}>
{obj.icon && <Icon type={obj.icon}/>}
{obj.url ? <a target="_blank" href={obj.url}>{obj.name}</a> :
<Link to={`/${parentPath}`}>{obj.name}</Link>}
</MenuItem>
);
}
componentWillMount() {
const paths = [];
// 这一项菜单是必须有的, 不需要在配置文件里配置
const logoutMenuItem = <MenuItem key="logout" className="logout">
<Icon type="logout"/>
<a href={`${globalConfig.getAPIPath()}${globalConfig.login.logout}`}>退出登录</a>
</MenuItem>;
// header右侧必须是用户菜单
let userMenuItems = null;
const menu = headerMenu.map((level1) => {
paths.push(level1.key);
let transformedLevel1Menu;
if (level1.child) {
const level2menu = level1.child.map((level2) => {
paths.push(level2.key);
if (level2.child) {
const level3menu = level2.child.map((level3) => {
paths.push(level3.key);
const tmp = this.transFormMenuItem(level3, paths);
paths.pop();
return tmp;
});
paths.pop();
// 与sidebarMenu不同的是这里返回MenuItemGroup
return (
<MenuItemGroup key={level2.key}
title={level2.icon ?
<span><Icon type={level2.icon}/>{` ${level2.name}`}</span> :
<span>{level2.name}</span>}>
<Menu.Divider/>
{level3menu}
</MenuItemGroup>
);
} else {
const tmp = this.transFormMenuItem(level2, paths);
paths.pop();
return tmp;
}
});
paths.pop();
transformedLevel1Menu = (
<SubMenu key={level1.key}
title={level1.icon ? <span><Icon type={level1.icon}/>{level1.name}</span> : level1.name}>
{level2menu}
</SubMenu>
);
} else {
transformedLevel1Menu = this.transFormMenuItem(level1, paths);
paths.pop();
}
// 顶层菜单parse完毕后, 先不要直接返回, 如果用户在config中定义了用户菜单, 要单独处理
if (level1.key === 'userMenu') {
userMenuItems = transformedLevel1Menu.props.children; // 注意这个直接读取props的逻辑
return null;
} else {
return transformedLevel1Menu;
}
});
this.menu = menu;
// 注意用户菜单的最后一项必定是注销
const userMenu = (
<SubMenu title={<span><Icon type="user"/>{this.props.realname}</span>}>
{userMenuItems && userMenuItems[0] ? userMenuItems : null}
<Menu.Divider/>
{logoutMenuItem}
</SubMenu>
);
this.userMenu = userMenu;
}
// FIXME: 这里其实有个bug, 如果菜单名称很长可能会导致overflow, 出现滚动条
// 暂时无法解决..., 怎么调css都不对
render() {
return (
<div className="ant-layout-header">
{/*定义header中的菜单, 从右向左依次是注销/用户菜单/其他自定义菜单*/}
<Menu className="header-menu" mode="horizontal">
{this.userMenu}
{this.menu}
</Menu>
</div>
);
}
}
export default Header;
.logout {
width: 100%;
display: inline-block !important;
}
.logout > a {
display: inline-block !important;
}
.ant-layout-header {
background: #fff;
height: 48px;
border-bottom: 1px solid #e9e9e9;
.header-menu {
&:before, &:after {
display: table;
content: '';
}
&:after {
clear: both;
}
> li {
float: right;
}
a {
display: inline-block;
color: rgba(0, 0, 0, 0.65);
height: 48px;
width: 100%;
transition: all 0.3s ease;
}
a:hover {
color: #1890FF;
transition: all 0.3s ease;
}
// span元素内不换行
span {
white-space: nowrap;
}
.anticon {
display: inline-block;
}
}
}
// 防止最右边的菜单项超出屏幕宽度, 导致出现滚动条
.header-menu li:first-child ul {
right: 1px;
left: auto;
}
import React from 'react';
import './index.less';
import PropTypes from 'prop-types';
/**
* 图片走马灯, 基本是抄的这个: https://github.com/xiaolin/react-image-gallery
* 写样式真是太痛苦了...臣妾真的做不到啊...
*/
class ImageSlider extends React.PureComponent {
state = {
previousIndex: 0,
currentIndex: 0,
};
componentWillMount() {
this.navButton = (
<span key="navigation">
<button type="button" className="image-gallery-left-nav" onClick={this.slideLeft}/>
<button type="button" className="image-gallery-right-nav" onClick={this.slideRight}/>
</span>
);
}
componentWillReceiveProps() {
// 每次从外界传新的图片过来时, 都要还原状态
this.state.currentIndex = 0;
this.state.previousIndex = 0;
}
/**
* 滑动到指定index
*/
slideToIndex(index) {
const {currentIndex} = this.state;
const slideCount = this.props.items.length - 1;
let nextIndex = index;
if (index < 0) {
nextIndex = slideCount;
} else if (index > slideCount) {
nextIndex = 0;
}
this.setState({
previousIndex: currentIndex,
currentIndex: nextIndex,
});
}
slideLeft = () => this.slideToIndex(this.state.currentIndex - 1);
slideRight = () => this.slideToIndex(this.state.currentIndex + 1);
/**
* 将传入的item(一张图片)转换为react元素
*/
renderItem(item) {
return (
<div className="image-gallery-image">
<img src={item.url} alt={item.alt}/>
{item.description && <span className="image-gallery-description">{item.description}</span>}
</div>
);
}
/*下面这3个方法真心是改不动...css真的苦手...*/
_getAlignmentClassName(index) {
// LEFT, and RIGHT alignments are necessary for lazyLoad
let {currentIndex} = this.state;
let alignment = '';
const LEFT = 'left';
const CENTER = 'center';
const RIGHT = 'right';
switch (index) {
case (currentIndex - 1):
alignment = ` ${LEFT}`;
break;
case (currentIndex):
alignment = ` ${CENTER}`;
break;
case (currentIndex + 1):
alignment = ` ${RIGHT}`;
break;
}
if (this.props.items.length >= 3) {
if (index === 0 && currentIndex === this.props.items.length - 1) {
// set first slide as right slide if were sliding right from last slide
alignment = ` ${RIGHT}`;
} else if (index === this.props.items.length - 1 && currentIndex === 0) {
// set last slide as left slide if were sliding left from first slide
alignment = ` ${LEFT}`;
}
}
return alignment;
}
_getTranslateXForTwoSlide(index) {
// For taking care of infinite swipe when there are only two slides
// 这个offsetPercentage本来是state中的, 但其实根本没用到, 固定是0
// 从state中删除后, 不想改这个方法的代码, 索性直接给个默认值
const {currentIndex, offsetPercentage = 0, previousIndex} = this.state;
const baseTranslateX = -100 * currentIndex;
let translateX = baseTranslateX + (index * 100) + offsetPercentage;
// keep track of user swiping direction
if (offsetPercentage > 0) {
this.direction = 'left';
} else if (offsetPercentage < 0) {
this.direction = 'right';
}
// when swiping make sure the slides are on the correct side
if (currentIndex === 0 && index === 1 && offsetPercentage > 0) {
translateX = -100 + offsetPercentage;
} else if (currentIndex === 1 && index === 0 && offsetPercentage < 0) {
translateX = 100 + offsetPercentage;
}
if (currentIndex !== previousIndex) {
// when swiped move the slide to the correct side
if (previousIndex === 0 && index === 0 &&
offsetPercentage === 0 && this.direction === 'left') {
translateX = 100;
} else if (previousIndex === 1 && index === 1 &&
offsetPercentage === 0 && this.direction === 'right') {
translateX = -100;
}
} else {
// keep the slide on the correct slide even when not a swipe
if (currentIndex === 0 && index === 1 &&
offsetPercentage === 0 && this.direction === 'left') {
translateX = -100;
} else if (currentIndex === 1 && index === 0 &&
offsetPercentage === 0 && this.direction === 'right') {
translateX = 100;
}
}
return translateX;
}
_getSlideStyle(index) {
const {currentIndex, offsetPercentage = 0} = this.state;
const {items} = this.props;
const baseTranslateX = -100 * currentIndex;
const totalSlides = items.length - 1;
// calculates where the other slides belong based on currentIndex
let translateX = baseTranslateX + (index * 100) + offsetPercentage;
// adjust zIndex so that only the current slide and the slide were going
// to is at the top layer, this prevents transitions from flying in the
// background when swiping before the first slide or beyond the last slide
let zIndex = 1;
if (index === currentIndex) {
zIndex = 3;
} else if (index === this.state.previousIndex) {
zIndex = 2;
} else if (index === 0 || index === totalSlides) {
zIndex = 0;
}
if (items.length > 2) {
if (currentIndex === 0 && index === totalSlides) {
// make the last slide the slide before the first
translateX = -100 + offsetPercentage;
} else if (currentIndex === totalSlides && index === 0) {
// make the first slide the slide after the last
translateX = 100 + offsetPercentage;
}
}
// Special case when there are only 2 items with infinite on
if (items.length === 2) {
translateX = this._getTranslateXForTwoSlide(index);
}
const translate3d = `translate3d(${translateX}%, 0, 0)`;
return {
WebkitTransform: translate3d,
MozTransform: translate3d,
msTransform: translate3d,
OTransform: translate3d,
transform: translate3d,
zIndex: zIndex,
transition: 'transform 450ms ease-out', // 这个450ms本来是props中传过来的, 这里写死了
};
}
render() {
const {currentIndex} = this.state;
const slides = [];
const bullets = [];
// 准备数据
this.props.items.forEach((item, index) => {
const alignment = this._getAlignmentClassName(index);
const slide = (
<div key={index} className={`image-gallery-slide${alignment}`} style={this._getSlideStyle(index)}>
{this.renderItem(item)}
</div>
);
slides.push(slide);
bullets.push(
<button key={index} type="button"
className={`image-gallery-bullet ${currentIndex === index ? 'active' : ''}`}
onClick={e => this.slideToIndex(index)}/>
);
});
const slideWrapper = (
<div className="image-gallery-slide-wrapper">
{/*左右切换按钮*/}
{this.props.items.length > 1 && this.navButton}
{/*图片*/}
<div className="image-gallery-slides">
{slides}
</div>
{/*下方圆点*/}
{this.props.items.length > 1 &&
<div className="image-gallery-bullets">
<ul className="image-gallery-bullets-container">
{bullets}
</ul>
</div>}
{/*右上角index*/}
{this.props.items.length > 1 &&
<div className="image-gallery-index">
<span className="image-gallery-index-current">{this.state.currentIndex + 1}</span>
<span className="image-gallery-index-separator">{' / '}</span>
<span className="image-gallery-index-total">{this.props.items.length}</span>
</div>}
</div>
);
return (
<section className="image-gallery">
<div className="image-gallery-content">
{slideWrapper}
</div>
</section>
);
}
}
ImageSlider.propTypes = {
// 要显示的图片数组, 例子: [{url:aaa, alt:bbb, description:ccc}]
items: PropTypes.array.isRequired,
};
ImageSlider.defaultProps = {
items: [],
};
export default ImageSlider;
// 稍微修改了一些
.image-gallery-left-nav::before,
.image-gallery-right-nav::before {
display: inline-block;
font-family: "anticon"; // 字体改为antd的, 否则还要引入额外的字体文件
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
text-rendering: auto;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.image-gallery {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.image-gallery-content {
position: relative;
line-height: 0;
top: 0;
}
.image-gallery-slide-wrapper {
position: relative;
}
.image-gallery-slide-wrapper.left, .image-gallery-slide-wrapper.right {
display: inline-block;
width: calc(100% - 113px);
}
// 这个应该是缩放时为了自适应吧, 不知道可不可以删掉
@media (max-width: 768px) {
.image-gallery-slide-wrapper.left, .image-gallery-slide-wrapper.right {
width: calc(100% - 84px);
}
}
.image-gallery-left-nav,
.image-gallery-right-nav {
appearance: none;
background-color: transparent;
border: 0;
cursor: pointer;
outline: none;
position: absolute;
z-index: 4;
}
.image-gallery-left-nav::before,
.image-gallery-right-nav::before {
color: #fff;
line-height: .7;
text-shadow: 0 2px 2px #1a1a1a;
transition: color .2s ease-out;
}
.image-gallery-left-nav:hover::before,
.image-gallery-right-nav:hover::before {
color: #1890ff;
}
@media (max-width: 768px) {
.image-gallery-left-nav:hover::before,
.image-gallery-right-nav:hover::before {
color: #fff;
}
}
.image-gallery-left-nav,
.image-gallery-right-nav {
color: #fff;
font-size: 5em;
padding: 50px 3px;
top: 50%;
transform: translateY(-50%);
}
.image-gallery-left-nav[disabled],
.image-gallery-right-nav[disabled] {
cursor: disabled;
opacity: .6;
pointer-events: none;
}
@media (max-width: 768px) {
.image-gallery-left-nav,
.image-gallery-right-nav {
font-size: 3.4em;
}
}
@media (max-width: 480px) {
.image-gallery-left-nav,
.image-gallery-right-nav {
font-size: 2.4em;
}
}
.image-gallery-left-nav {
left: 0;
}
.image-gallery-left-nav::before {
content: "\E620";
}
.image-gallery-right-nav {
right: 0;
}
.image-gallery-right-nav::before {
content: "\E61F";
}
.image-gallery-slides {
line-height: 0;
overflow: hidden;
position: relative;
white-space: nowrap;
}
.image-gallery-slide {
background: #fff;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
.image-gallery-slide.center {
position: relative;
}
.image-gallery-slide img {
width: 100%;
}
.image-gallery-slide .image-gallery-description {
background: rgba(0, 0, 0, 0.4);
bottom: 70px;
color: #fff;
left: 0;
line-height: 1;
padding: 10px 20px;
position: absolute;
white-space: normal;
}
@media (max-width: 768px) {
.image-gallery-slide .image-gallery-description {
bottom: 45px;
font-size: .8em;
padding: 8px 15px;
}
}
.image-gallery-bullets {
bottom: 20px;
left: 0;
margin: 0 auto;
position: absolute;
right: 0;
width: 80%;
z-index: 4;
}
.image-gallery-bullets .image-gallery-bullets-container {
margin: 0;
padding: 0;
text-align: center;
}
.image-gallery-bullets .image-gallery-bullet {
appearance: none;
background-color: transparent;
border: 1px solid #fff;
border-radius: 50%;
box-shadow: 0 1px 0 #1a1a1a;
cursor: pointer;
display: inline-block;
margin: 0 5px;
outline: none;
padding: 5px;
}
@media (max-width: 768px) {
.image-gallery-bullets .image-gallery-bullet {
margin: 0 3px;
padding: 3px;
}
}
@media (max-width: 480px) {
.image-gallery-bullets .image-gallery-bullet {
padding: 2.7px;
}
}
.image-gallery-bullets .image-gallery-bullet.active {
background: #fff;
}
.image-gallery-index {
background: rgba(0, 0, 0, 0.4);
color: #fff;
line-height: 1;
padding: 10px 20px;
position: absolute;
right: 0;
top: 0;
z-index: 4;
}
@media (max-width: 768px) {
.image-gallery-index {
font-size: .8em;
padding: 5px 10px;
}
}
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import globalConfig from '../../config.js';
import ajax from '../../utils/ajax';
import Logger from '../../utils/Logger';
import {message, Icon, Button, Input} from 'antd';
import './index.less';
import {loginSuccessCreator} from '../../redux/Login.js';
import avatar1 from '../../statics/avatar.gif';
import avatar2 from '../../statics/avatar2.gif';
import loginSvg from '../../statics/login-bg.svg';
const logger = Logger.getLogger('Login');
/**
* 定义Login组件
*/
class Login extends React.PureComponent {
// 这个login样式是直接从网上找的: https://colorlib.com/wp/html5-and-css3-login-forms/
// 一般而言公司内部都会提供基于LDAP的统一登录, 用到这个登录组件的场景应该挺少的
state = {
username: '', // 当前输入的用户名
password: '', // 当前输入的密码
requesting: false // 当前是否正在请求服务端接口
};
// controlled components
handleUsernameInput = (e) => {
this.setState({username: e.target.value});
};
handlePasswordInput = (e) => {
this.setState({password: e.target.value});
};
/**
* 处理表单的submit事件
*
* @param e
*/
handleSubmit = async (e) => { // async可以配合箭头函数
e.preventDefault(); // 这个很重要, 防止跳转
this.setState({requesting: true});
const hide = message.loading('正在验证...', 0);
const username = this.state.username;
const password = this.state.password;
try {
// 服务端验证
const res = await ajax.login(username, password);
hide();
logger.debug('login validate return: result %o', res);
if (res.success) {
message.success('登录成功');
// 如果登录成功, 触发一个loginSuccess的action, payload就是登录后的用户名
this.props.handleLoginSuccess(res.data.user, res.data.permission);
} else {
message.error(`登录失败: ${res.message}, 请联系管理员`);
this.setState({requesting: false});
}
} catch (exception) {
hide();
message.error(`网络请求出错: ${exception.message}`);
logger.error('login error, %o', exception);
this.setState({requesting: false});
}
};
render() {
// 整个组件被一个id="loginDIV"的div包围, 样式都设置到这个div中
return (
<div id="loginDIV" style={{backgroundImage: "url(" + loginSvg + ")"}}>
<div className="login">
<div className="logo">
<img src={(new Date().getHours() + 1) % 2 === 0 ? avatar1 : avatar2}/>
<span>{globalConfig.name}</span>
</div>
<form onSubmit={this.handleSubmit}>
<Input prefix={<Icon type="user"/>} className="login-input" type="text"
value={this.state.username}
onChange={this.handleUsernameInput} placeholder="用户名" required="required"/>
<Input prefix={<Icon type="lock"/>} className="login-input" type="password"
value={this.state.password}
onChange={this.handlePasswordInput} placeholder="密码" required="required"/>
<Button type="primary" htmlType="submit" size="large" disabled={this.state.requesting}>
登录
</Button>
</form>
</div>
<footer>
<a target="_blank" href="http://www.mailejifen.com">麦乐积分</a> All Rights Reserved. © 2018 Powered by
Ant Design
</footer>
</div>
);
}
}
const mapDispatchToProps = (dispatch) => {
return {
handleLoginSuccess: bindActionCreators(loginSuccessCreator, dispatch)
};
};
// 不需要从state中获取什么, 所以传一个null
export default connect(null, mapDispatchToProps)(Login);
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html {
width: 100%;
height: 100%;
}
body {
width: 100%;
height: 100%;
background: #f0f2f5;
}
// 这里我稍微修改了一下
#loginDIV {
height: 100vh;
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
#loginDIV footer {
position: absolute;
bottom: 4%;
left: 50%;
margin-left: -170px;
color: rgba(0,0,0,.45);;
}
.login {
position: absolute;
top: 50%;
left: 50%;
margin: -190px 0 0 -190px;
width: 380px;
height: 380px;
padding: 36px;
button {
width: 100%;
height: 45px;
margin-top: 25px;
}
input {
height: 40px;
font-size: 15px;
margin: 10px 0;
line-height: 1.5;
color: rgba(0, 0, 0, .65);
background-color: #fff;
background-image: none;
border: 1px solid #d9d9d9;
border-radius: 4px;
}
}
.logo {
text-align: center;
height: 40px;
line-height: 40px;
cursor: pointer;
margin-bottom: 50px;
img {
width: 60px;
margin-right: 20px;
border-radius: 50%;
}
span {
vertical-align: text-bottom;
font-size: 24px;
font-family: Exo, sans-serif;
text-transform: uppercase;
display: inline-block;
background-image: -webkit-linear-gradient(left, #3498db, #f47920 10%, #d71345 20%, #f7acbc 30%,
#ffd400 40%, #3498db 50%, #f47920 60%, #d71345 70%, #f7acbc 80%, #ffd400 90%, #3498db);
color: transparent;
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-size: 200% 100%;
animation: masked-animation 4s infinite linear;
}
@keyframes masked-animation {
0% {
background-position: 0 0;
}
100% {
background-position: -100% 0;
}
}
}
import React from 'react';
import globalConfig from '../../config.js';
import './index.less';
/**
* 定义Logo组件
*/
class Logo extends React.PureComponent {
render() {
return (
<div className={this.props.collapse ? "ant-layout-logo-collapse" : "ant-layout-logo-normal"}>
<div className="ant-layout-logo-text">
{/*侧边栏折叠的时候只显示一个字*/}
<a href="#">{this.props.collapse ? globalConfig.name[0] : globalConfig.name}</a>
</div>
</div>
);
}
}
export default Logo;
.ant-layout-logo-base {
height: 32px;
background: #002140;
border-radius: 6px;
}
.ant-layout-logo-normal {
width: 200px;
margin: 16px 8px 16px 12px;
.ant-layout-logo-base;
}
.ant-layout-logo-collapse {
width: 32px;
margin: 16px;
transition: all 0.3s ease;
.ant-layout-logo-base;
}
.ant-layout-logo-text {
text-align: center;
font-family: Lato, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif;
font-size: 22px;
}
import React from 'react';
import {Table, Checkbox} from 'antd';
import collect from 'collect.js';
export default class PermissionEditor extends React.PureComponent {
initial = false;
static defaultProps = {
menus: [],
permissions: []
};
componentWillMount() {
let menus = this.initMenuTree(JSON.parse(JSON.stringify(this.props.menus)));
this.setState({
menus
});
}
componentWillReceiveProps(nextProps) {
if (!this.initial && nextProps.permissions.length > 0 && this.props.permissions.length === 0) {
let menus = this.buildMenusPermissions(nextProps.permissions);
this.initValues(this.props.initialValue, menus);
this.initial = true;
}
if (this.initial) {
//数据发生变化
let a = collect(this.state.value), b = collect(nextProps.value);
if (!a.diff(b).isEmpty() || !b.diff(a).isEmpty()) {
this.initValues(nextProps.value, this.state.menus);
}
}
}
state = {
tableLoading: false,
selectedMenus: [],
value: [],
menus: [],
selectedPermissionMap: {}
};
tableSchema = [{
title: '菜单项名称',
dataIndex: 'name',
width: 300
}, {
title: '操作权限',
render: (text, record, index) => {
if (record.permissions && record.permissions.length > 0) {
return record.permissions.map((p, i) => (
<Checkbox style={{padding: '5px', marginLeft: 0}}
key={p.identity}
checked={this.state.selectedPermissionMap[p.id]}
onChange={e => this.onPermissionChange(p, e, record)}>
{p.alias || p.identity}
</Checkbox>
));
}
}
}];
onMenuSelectAll(selected, selectedRows, changeRows) {
let selectedMenus;
if (selected) {
this.setValues(this.props.permissions.map(o => o.id));
selectedMenus = this.getAllMenusKey();
} else {
this.setValues([]);
selectedMenus = [];
}
this.setState({selectedMenus});
}
onMenuSelect(menu, selected, selectedRows, nativeEvent) {
if (selected) {
this.selectMenu(menu);
} else {
this.unselectMenu(menu);
}
}
/**
* 选择菜单
* @param menu
* @param useChildren 是否同时选中所有子权限
*/
selectMenu(menu) {
let selectedMenus = this.state.selectedMenus;
let childrenKey = this.getMenuChildrenKey(menu);
let parentsKey = this.getMenuParentsKey(menu);
selectedMenus.push(menu.key);
selectedMenus = selectedMenus.concat(childrenKey, parentsKey);
let values = this.getMenuPermissionsId(menu, true);
if (values.length > 0) {
this.addValues(values);
}
selectedMenus = collect(selectedMenus).unique().toArray();
this.setState({selectedMenus});
}
/**
* 取消选择菜单
* @param menu
* @param useChildren
*/
unselectMenu(menu) {
let selectedMenus = this.state.selectedMenus;
let childrenKey = this.getMenuChildrenKey(menu);
selectedMenus.splice(selectedMenus.indexOf(menu.key), 1);
if (childrenKey.length > 0) {
for (let i in childrenKey) {
let index = selectedMenus.indexOf(childrenKey[i]);
if (index >= 0) {
selectedMenus.splice(index, 1);
}
}
}
let values = this.getMenuPermissionsId(menu, true);
if (values.length > 0) {
this.removeValues(values);
}
selectedMenus = collect(selectedMenus).unique().toArray();
this.setState({selectedMenus});
}
initValues(values, menus) {
let v = collect();
let selectMap = {};
if (values instanceof Array) {
v.concat(values);
values.forEach(o => selectMap[o] = true);
} else {
v.push(values);
selectMap[values] = true;
}
let selectedMenus = collect();
this.traversalTree(menus, menu => {
if (menu.permissions && menu.permissions.length > 0) {
if (menu.permissions.some(p => v.contains(p.id))) {
selectedMenus.push(menu.key);
selectedMenus.concat(this.getMenuParentsKey(menu));
}
}
});
selectedMenus = selectedMenus.unique().toArray();
v = v.unique().toArray().sort((a, b) => a - b);
this.setState({
value: v,
selectedMenus: selectedMenus,
selectedPermissionMap: selectMap
});
}
setValues(values) {
const {onChange} = this.props;
let v = collect();
let selectMap = {};
if (values instanceof Array) {
v.concat(values);
values.forEach(o => selectMap[o] = true);
} else {
v.push(values);
selectMap[values] = true;
}
v = v.unique().toArray().sort((a, b) => a - b);
this.setState({
value: v,
selectedPermissionMap: selectMap
});
onChange(v);
return v;
}
addValues(values) {
const {onChange} = this.props;
let v = collect(this.state.value);
let selectMap = Object.assign(this.state.selectedPermissionMap);
if (values instanceof Array) {
v.concat(values);
values.forEach(o => selectMap[o] = true);
} else {
v.push(values);
selectMap[values] = true;
}
v = v.unique().toArray().sort((a, b) => a - b);
this.setState({
value: v,
selectedPermissionMap: selectMap
});
onChange(v);
return v;
}
removeValues(values) {
const {onChange} = this.props;
let v = collect(this.state.value);
let selectMap = Object.assign(this.state.selectedPermissionMap);
if (values instanceof Array) {
v = v.diff(values);
values.forEach(o => selectMap[o] = false);
} else {
let i = v.search(values);
if (i >= 0) {
v.splice(i, 1);
}
selectMap[values] = false;
}
v = v.toArray().sort((a, b) => a - b);
this.setState({
value: v,
selectedPermissionMap: selectMap
});
onChange(v);
return v;
}
onPermissionChange(record, e, menu) {
if (e.target.checked) {
this.addValues(record.id);
let selectedMenus = collect(this.state.selectedMenus);
if (this.state.selectedMenus.indexOf(menu.key) === -1) {
selectedMenus.push(menu.key);
this.getMenuParentsKey(menu).forEach(o => {
selectedMenus.push(o);
});
selectedMenus = selectedMenus.unique().toArray();
this.setState({selectedMenus});
}
} else {
this.removeValues(record.id);
}
}
getMenuChildrenKey(node) {
let result = [];
if (node.children && node.children.length) {
for (let k in node.children) {
let child = node.children[k];
result.push(child.key);
result = result.concat(this.getMenuChildrenKey(child));
}
}
return result;
}
getAllMenusKey() {
let tmp = {
children: this.state.menus
};
return this.getMenuChildrenKey(tmp);
}
buildMenusParent(tree, parent) {
tree = tree || this.state.menus;
parent = parent || null;
for (let k in tree) {
tree[k].parent = parent;
if (tree[k].children && tree[k].children.length) {
this.buildMenusParent(tree[k].children, tree[k]);
}
}
}
initMenuTree(menus, parent) {
menus = menus || this.state.menus;
parent = parent || null;
for (let k in menus) {
menus[k].parent = parent;
menus[k].children = menus[k].child;
delete menus[k].child;
if (menus[k].children && menus[k].children.length) {
this.buildMenusParent(menus[k].children, menus[k]);
}
}
return menus;
}
getMenuParentsKey(node) {
let path = [];
while (node.parent) {
node = node.parent;
path.push(node.key);
}
return path.reverse();
}
getMenuPermissionsId(menu, useChildren) {
let result;
if (menu.permissions && menu.permissions.length > 0) {
result = menu.permissions.map(o => o.id);
} else {
result = [];
}
if (useChildren && menu.children && menu.children.length > 0) {
this.traversalTree(menu.children, o => {
if (o.permissions && o.permissions.length > 0) {
result = result.concat(o.permissions.map(k => k.id));
}
});
}
return result;
}
buildMenusPermissions(permissions) {
let menus = this.state.menus;
let permissionMap = {};
for (let i in permissions) {
permissionMap[permissions[i].identity] = permissions[i];
}
this.traversalTree(menus, menu => {
if (menu.permsKey && menu.permsKey.length > 0) {
let permsKey = menu.permsKey;
menu.permissions = [];
for (let i in permsKey) {
if (permissionMap[permsKey[i]]) {
menu.permissions.push(permissionMap[permsKey[i]]);
}
}
}
});
this.setState({
menus: menus
});
return menus;
}
traversalTree(tree, fn) {
for (let i in tree) {
fn(tree[i]);
if (tree[i].children && tree[i].children.length > 0) {
this.traversalTree(tree[i].children, fn);
}
}
}
render() {
const rowSelection = {
onSelect: this.onMenuSelect.bind(this),
onSelectAll: this.onMenuSelectAll.bind(this),
selectedRowKeys: this.state.selectedMenus
};
return (
<div>
<Table
rowSelection={rowSelection}
rowKey='key'
bordered={true}
defaultExpandAllRows={true}
columns={this.tableSchema}
dataSource={this.state.menus}
pagination={false}
loading={this.state.tableLoading}>
</Table>
</div>
);
}
}
\ No newline at end of file
import React, {Fragment} from 'react';
import {Form, Input, Icon, Button, Row, Divider} from 'antd';
import PermissionEditor from './PermissionEditor';
import menus from '../../menu';
import ajax from '../../utils/ajax';
class RoleEditor extends React.PureComponent {
async componentWillMount() {
this.props.setEditorModalWidth('80%');
let resp = await ajax.getAsyncData('permission/all');
if (resp.code === 0) {
this.setState({
permissions: resp.data.list
});
}
}
state = {
permissions: []
};
render() {
const FormItem = Form.Item;
const getFieldDecorator = this.props.form.getFieldDecorator;
return (
<Fragment>
<Form layout="inline" onSubmit={this.handleSubmit}>
{getFieldDecorator('id', {
initialValue: this.props.forUpdate ? this.props.initData.id : undefined
})(
<Input type='hidden'/>
)}
<Row>
<FormItem label='角色名称'>
{getFieldDecorator('name', {
initialValue: this.props.forUpdate ? this.props.initData.name : undefined,
rules: [{required: true, message: '请输入角色名称'}]
})(
<Input prefix={
<Icon type="user" style={{color: 'rgba(0,0,0,.25)'}}/>
} placeholder="角色名称"/>
)}
</FormItem>
</Row>
<Divider orientation="left" style={{fontSize: '12px'}}>权限设定</Divider>
<Row>
{getFieldDecorator('perms', {
initialValue: this.props.forUpdate ? this.props.initData.perms : []
})(
<PermissionEditor menus={menus} permissions={this.state.permissions}/>
)}
</Row>
</Form>
</Fragment>
);
}
}
export default Form.create()(RoleEditor);
\ No newline at end of file
import React, {Fragment} from 'react';
import InnerTable from '../DBTable/InnerTable';
import RoleEditor from './RoleEditor';
export default class RoleInnerTable extends InnerTable {
constructor() {
super();
this.state = Object.assign(this.state, {
role: {},
permission: [2, 3, 4]
});
}
getEditForm() {
return <RoleEditor
wrappedComponentRef={(input) => {
this.formComponent = input;
}}
initData={this.formInitData}
data={this.state.data}
forUpdate={!this.state.modalInsert}
keysToUpdate={this.keysToUpdate}
setEditorModalWidth={this.setEditorModalWidth.bind(this)}/>;
}
}
import React from 'react';
import {message, notification, Spin} from 'antd';
import RoleInnerTable from "./RoleInnerTable";
import DBTable from "../DBTable";
import InnerPagination from '../DBTable/InnerPagination';
export default class RoleTable extends DBTable {
render() {
if (this.state.loadingSchema && (!this.notFirstRender || (!this.inited && !this.errorMsg))) {
this.notFirstRender = true;
return (
<Spin tip="loading schema..." spinning={this.state.loadingSchema} delay={500}>
<div style={{height: '150px', width: '100%'}}></div>
</Spin>
);
}
this.notFirstRender = true;
if (!this.inited) {
return (
<Spin tip="loading schema..." spinning={this.state.loadingSchema} delay={500}>
<Error errorMsg={this.errorMsg}/>
</Spin>
);
}
return (
<Spin spinning={this.state.loadingSchema} delay={500}>
<RoleInnerTable data={this.state.data} tableLoading={this.state.tableLoading}
schema={this.dataSchema} refresh={this.refresh}
tableConfig={this.tableConfig} tableName={this.tableName}/>
{this.tableConfig.showPagination &&
<InnerPagination currentPage={this.state.currentPage} total={this.state.total}
pageSize={this.tableConfig.pageSize || this.state.pageSize}
parentHandlePageChange={this.handlePageChange} tableConfig={this.tableConfig}
showSizeChanger={this.state.showSizeChanger}
pageSizeOptions={this.state.pageSizeOptions}
parentHandleShowPageChange={this.handleShowPageChange}
tableName={this.tableName} isSimple={this.state.isSimple}/>
}
</Spin>
);
}
// state = {
// data: [],
// tableLoading: false, // 表格是否是loading状态
// queryObj: {}, // 表单中的查询条件
//
// // 分页器的状态
// pagination: {
// currentPage: 1, // 当前第几页, 注意页码是从1开始的, 以前总是纠结页码从0还是1开始, 这里统一下, 跟显示给用户的一致
// pageSize: 20, // pageSize默认值50, 这个值一旦初始化就是不可变的
// showSizeChanger: globalConfig.DBTable.showSizeChanger, // 是否显示修改每页显示数量的选项
// pageSizeOptions: globalConfig.DBTable.pageSizeOptions, // 每页面显示数量选项
// total: 0 // 总共有多少条数据
// }
// };
//
// async componentWillMount() {
// this.setState({
// tableLoading: true
// });
// let res = await ajax.getAsyncData('role/select');
// this.setState({
// data: res.data.list,
// tableLoading: false,
// pagination: {...this.state.pagination, total: res.data.total}
// });
// }
// static attrColumns = [
// {
// title: 'ID',
// dataIndex: 'id',
// key: 'id'
// }, {
// title: '角色名称',
// dataIndex: 'name',
// key: 'roleName'
// }, {
// title: '操作',
// key: 'operation',
// render: (text, record) => <a href="###">编辑</a>
// }
// ];
//
// onInsert() {
//
// }
// render() {
// return (
// <div>
// <Row gutter={24}>
// <Col lg={24} md={24} sm={24} xs={24} style={{margin: 16, textAlign: 'left'}}>
// <Button type="primary"><Icon type="plus-circle-o"/>添加</Button>
// </Col>
// </Row>
// <Table
// rowKey='id'
// attrColumns={Permission.attrColumns}
// dataSource={this.state.data}
// pagination={{...this.state.pagination}}
// />
// <DBTable/>
// </div>
// );
// }
}
\ No newline at end of file
import React from 'react';
import {Link} from 'react-router';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {Menu, Icon} from 'antd';
import Logo from '../Logo';
import Logger from '../../utils/Logger';
import items from '../../menu.js'; // 由于webpack中的设置, 不用写完整路径
import globalConfig from '../../config.js';
import './index.less';
import {sidebarCollapseCreator} from '../../redux/Sidebar.js';
import classnames from 'classnames';
import collect from 'collect.js';
const SubMenu = Menu.SubMenu;
const MenuItem = Menu.Item;
const logger = Logger.getLogger('Sidebar');
/**
* 定义sidebar组件
*/
class Sidebar extends React.PureComponent {
// 尝试把sidebar做成PureComponent, 注意可能的bug
// 注意PureComponent的子组件也应该是pure的
state = {
openKeys: [], // 当前有哪些submenu被展开
isFold: true, // 余下的菜单是否隐藏
};
// 哪些状态组件自己维护, 哪些状态放到redux, 是需要权衡的
/**
* 将菜单项配置转换为对应的MenuItem组件
*
* @param obj sidebar菜单配置项
* @param paths 父级目录, array
* @returns {XML}
*/
transFormMenuItem(obj, paths, isLevel1) {
const parentPath = paths.join('/'); // 将各级父目录组合成完整的路径
logger.debug('transform %o to path %s', obj, parentPath);
// 这个表达式还是有点恶心的...
// JSX虽然方便, 但是很容易被滥用, ES6也是
// 注意这里的样式, 用chrome一点点调出来的...
// 我的css都是野路子, 头痛医头脚痛医脚, 哪里显示有问题就去调一下, 各种inline style
// 估计事后去看的话, 我都忘了为什么要加这些样式...
return (
<MenuItem key={obj.key} style={{margin: '0px'}}>
{obj.icon && <Icon type={obj.icon}/>}
{/*对于level1的菜单项, 如果没有图标, 取第一个字用于折叠时显示*/}
{isLevel1 && !obj.icon && <span className="invisible-nav-text">{obj.name[0]}</span>}
<Link to={`/${parentPath}`} style={{display: 'inline'}}><span
className="nav-text">{obj.name}</span></Link>
</MenuItem>
);
}
// 在每次组件挂载的时候parse一次菜单, 不用每次render都解析
// 其实这个也可以放在constructor里
componentWillMount() {
const paths = []; // 暂存各级路径, 当作stack用
const level1KeySet = new Set(); // 暂存所有顶级菜单的key
const level2KeyMap = new Map(); // 次级菜单与顶级菜单的对应关系
const permission = collect(this.props.permission);
const newMenus = this.buildAvailableMenus(JSON.parse(JSON.stringify(items)), permission);
// 菜单项是从配置中读取的, parse过程还是有点复杂的
// map函数很好用
const menu = newMenus.map((level1) => {
// parse一级菜单
paths.push(level1.key);
level1KeySet.add(level1.key);
if (this.state.openKeys.length === 0) {
this.state.openKeys.push(level1.key); // 默认展开第一个菜单, 直接修改state, 没必要setState
}
// 是否有子菜单?
if (level1.child) {
const level2menu = level1.child.map((level2, index) => {
if (level2.showInSidebar === false) {
level2 = level1.child.splice(index, 1);
} else {
// parse二级菜单
paths.push(level2.key);
level2KeyMap.set(level2.key, level1.key);
if (level2.child) {
const level3menu = level2.child.map((level3) => {
// parse三级菜单, 不能再有子菜单了, 即使有也会忽略
paths.push(level3.key);
const tmp = this.transFormMenuItem(level3, paths);
paths.pop();
return tmp;
});
paths.pop();
return (
<SubMenu key={level2.key}
title={level2.icon ?
<span><Icon type={level2.icon}/>{level2.name}</span> : level2.name}>
{level3menu}
</SubMenu>
);
} else {
const tmp = this.transFormMenuItem(level2, paths);
paths.pop();
return tmp;
}
}
});
paths.pop();
let level1Title;
// 同样, 如果没有图标的话取第一个字
if (level1.icon) {
level1Title =
<span><Icon type={level1.icon}/><span className="nav-text">{level1.name}</span></span>;
} else {
level1Title = <span><span className="invisible-nav-text">{level1.name[0]}</span><span
className="nav-text">{level1.name}</span></span>;
}
return (
<SubMenu key={level1.key} title={level1Title}>
{level2menu}
</SubMenu>
);
}
// 没有子菜单, 直接转换为MenuItem
else {
const tmp = this.transFormMenuItem(level1, paths, true);
paths.pop(); // return之前别忘了pop
return tmp;
}
});
this.menu = menu;
this.level1KeySet = level1KeySet;
this.level2KeyMap = level2KeyMap;
}
/**
* 过滤掉没权限的菜单项
*
* @param menus Array
* @param permission collect
* @returns {Array}
*/
buildAvailableMenus(menus, permission) {
let newMenus = [];
for (let i in menus) {
let item = menus[i];
let isLeaf = !item.child || item.child.length === 0;
if (!isLeaf) {
item.child = this.buildAvailableMenus(item.child, permission);
}
if (!isLeaf) {
if (item.child && item.child.length > 0) { //非叶子节点且有孩子
newMenus.push(item);
}
} else {
if (!item.permsKey || item.permsKey.length === 0) { //叶子节点,但未设定所需权限
newMenus.push(item);
} else if (collect(item.permsKey).intersect(permission).all().length > 0) { //叶子节点且拥有权限
newMenus.push(item);
}
}
}
return newMenus;
}
// 我决定在class里面, 只有在碰到this问题时才使用箭头函数, 否则还是优先使用成员方法的形式定义函数
// 因为用箭头函数ESlint总是提示语句最后少一个分号...
// 事件处理的方法统一命名为handleXXX
/**
* 处理子菜单的展开事件
*
* @param openKeys
*/
handleOpenChange = (openKeys) => {
// 如果当前菜单是折叠状态, 就先展开
if (this.props.collapse) {
this.props.handleClickCollapse();
}
if (!globalConfig.sidebar.autoMenuSwitch) { // 不开启这个功能
this.setState({openKeys});
return;
}
logger.debug('old open keys: %o', openKeys);
const newOpenKeys = [];
// 有没有更优雅的写法
let lastKey = ''; // 找到最近被点击的一个顶级菜单, 跟数组中元素的顺序有关
for (let i = openKeys.length; i >= 0; i--) {
if (this.level1KeySet.has(openKeys[i])) {
lastKey = openKeys[i];
break;
}
}
// 过滤掉不在lastKey下面的所有子菜单
for (const key of openKeys) {
const ancestor = this.level2KeyMap.get(key);
if (ancestor === lastKey) {
newOpenKeys.push(key);
}
}
newOpenKeys.push(lastKey);
logger.debug('new open keys: %o', newOpenKeys);
this.setState({openKeys: newOpenKeys});
};
/**
* 处理"叶子"节点的点击事件
*
* @param key
*/
handleSelect = ({key}) => {
if (this.props.collapse) {
this.props.handleClickCollapse();
}
// 如果是level1级别的菜单触发了这个事件, 说明这个菜单没有子项, 需要把其他所有submenu折叠
if (globalConfig.sidebar.autoMenuSwitch && this.level1KeySet.has(key) && this.state.openKeys.length > 0) {
this.setState({openKeys: []});
}
};
handleClick = () => {
this.setState({isFold: true});
};
handleFold = () => {
this.setState({
isFold: !this.state.isFold
});
};
render() {
return (
<aside className={classnames({
"ant-layout-sidebar-collapse": this.props.collapse,
"ant-layout-sidebar": !this.props.collapse,
"menu-fold": this.state.isFold,
})}
>
<Logo collapse={this.props.collapse}/>
<Menu theme="dark" mode="inline"
onOpenChange={this.handleOpenChange}
onSelect={this.handleSelect}
onClick={this.handleClick}
openKeys={this.props.collapse ? [] : this.state.openKeys}>
{this.menu}
</Menu>
<div className="menu-trigger" onClick={this.handleFold}>
<Icon type={this.state.isFold ? "menu-fold" : "menu-unfold"}/>
</div>
<div className="ant-layout-sidebar-trigger" onClick={this.props.handleClickCollapse}>
<Icon type={this.props.collapse ? "right" : "left"}/>
</div>
</aside>
);
}
}
// 什么时候使用箭头函数?
// 1. 碰到this问题的时候
// 2. 要写function关键字的时候
const mapStateToProps = (state) => {
let item = document.getElementsByClassName("ant-menu-submenu-arrow");
for (let i = 0; i < item.length; i++) {
item[i].style.display = state.Sidebar.collapse ? "none" : "block";
}
return {
collapse: state.Sidebar.collapse,
user: state.Login.user,
permission: state.Login.permission
};
};
const mapDispatchToProps = (dispatch) => {
return {
// 所有处理事件的方法都以handleXXX命名
handleClickCollapse: bindActionCreators(sidebarCollapseCreator, dispatch),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Sidebar);
// 又到了痛苦的写css的时间了啊...
// less虽然引入了变量/函数/流程控制之类的, 更类似编程语言了, 但写法还是有些奇怪的
@sidebarCollapsible: true; // 侧边栏是否可折叠, 这个变量是通过webpack less-loader传进来的
// 下方折叠按钮的样式
.ant-layout-sidebar-trigger {
position: absolute;
text-align: center;
width: inherit;
left: 0;
bottom: 0;
cursor: pointer;
height: 42px;
line-height: 48px;
background: rgba(0, 0, 0, 0.65);
color: #fff;
& when not (@sidebarCollapsible = true) {
display: none;
}
}
.ant-layout-sidebar-base {
background: #001529;
position: relative;
float: left;
overflow: auto;
transition: all 0.3s ease;
// less的if语句: http://stackoverflow.com/questions/14910667/how-to-use-if-statements-in-less
// // 但是没有else语句...
// & when (@sidebarCollapsible = true) {
// // height: calc(~"100% - 42px"); // 这是个很神奇的写法, 要减去下方按钮的高度
// padding-bottom: 42px;
// height: 100%;
// }
// & when not (@sidebarCollapsible = true) {
// height: 100%;
// }
}
.ant-layout-sidebar, .ant-layout-sidebar-collapse {
width: 100%;
.ant-layout-sidebar-base;
&.menu-fold > .ant-menu > li + li {
display: none;
}
}
/* 定义侧边栏正常显示时的样式 - begin */
.ant-layout-sidebar > .ant-menu > .ant-menu-item {
margin: 16px 0;
}
.ant-layout-sidebar > .ant-menu > .ant-menu-item .nav-text {
vertical-align: baseline;
display: inline-block;
}
.ant-layout-sidebar > .ant-menu .invisible-nav-text {
// 不用display:none, 而是设为0px, 动画效果就比较明显
// 这都能想到, 我都佩服自己...
font-size: 0px;
}
.ant-layout-sidebar > .ant-menu > .ant-menu-item > .anticon {
transition: font-size .3s;
}
/* 定义侧边栏正常显示时的样式 - end */
/* 定义侧边栏折叠时的样式 - begin */
.ant-layout-sidebar-collapse > .ant-menu .anticon {
font-size: 16px;
display: inline-block;
}
.ant-layout-sidebar-collapse > .ant-menu .ant-menu-submenu-title:after {
content: ''; // 去除submenu右方的箭头提示
}
.ant-layout-sidebar-collapse > .ant-menu .nav-text {
display: none; // 折叠时菜单项文字隐藏
}
.ant-layout-sidebar-collapse > .ant-menu .invisible-nav-text {
font-size: 16px;
transition: all 0.3s ease;
display: inline-block;
}
.ant-layout-logo-normal, .ant-layout-logo-collapse, .ant-layout-sidebar-trigger {
display: none;
}
.menu-trigger {
position: absolute;
top: 0;
right: 0;
z-index: 4;
line-height: 42px;
padding: 0 16px;
color: white;
}
/* 定义侧边栏折叠时的样式 - end */
@media screen and (min-width: 992px) {
.ant-layout-logo-normal, .ant-layout-logo-collapse, .ant-layout-sidebar-trigger {
display: block;
}
.menu-trigger {
display: none;
}
.ant-layout-sidebar > .ant-menu {
margin-bottom: 20px;
}
.ant-layout-sidebar, .ant-layout-sidebar-collapse {
padding-bottom: 42px;
height: 100%;
&.menu-fold > .ant-menu > li + li {
display: initial;
}
}
.ant-layout-sidebar {
width: 224px; // 正常的侧边栏宽度
}
.ant-layout-sidebar-collapse {
width: 64px; // 侧边栏折叠时的宽度
}
}
\ No newline at end of file
import React from 'react';
import moment from 'moment';
import './index.less';
import ajax from '../../utils/ajax';
import 'moment/locale/zh-cn';
moment.locale('zh-cn');
/**
展示欢迎界面
**/
class Welcome extends React.PureComponent {
render() {
return (
<div className='welcome-page'>
<pre className='doge'>
<br/> iii. ;9ABH,
<br/> SA391, .r9GG35&G
<br/> &#ii13Gh; i3X31i;:,rB1
<br/> iMs,:,i5895, .5G91:,:;:s1:8A
<br/> 33::::,,;5G5, ,58Si,,:::,sHX;iH1
<br/> Sr.,:;rs13BBX35hh11511h5Shhh5S3GAXS:.,,::,,1AG3i,GG
<br/> .G51S511sr;;iiiishS8G89Shsrrsh59S;.,,,,,..5A85Si,h8
<br/> :SB9s:,............................,,,.,,,SASh53h,1G.
<br/> .r18S;..,,,,,,,,,,,,,,,,,,,,,,,,,,,,,....,,.1H315199,rX,
<br/> ;S89s,..,,,,,,,,,,,,,,,,,,,,,,,....,,.......,,,;r1ShS8,;Xi
<br/> i55s:.........,,,,,,,,,,,,,,,,.,,,......,.....,,....r9&5.:X1
<br/> 59;.....,. .,,,,,,,,,,,... .............,..:1;.:&s
<br/> s8,..;53S5S3s. .,,,,,,,.,.. i15S5h1:.........,,,..,,:99
<br/> 93.:39s:rSGB@A; ..,,,,..... .SG3hhh9G&BGi..,,,,,,,,,,,,.,83
<br/> G5.G8 9#@@@@@X. .,,,,,,..... iA9,.S&B###@@Mr...,,,,,,,,..,.;Xh
<br/> Gs.X8 S@@@@@@@B:..,,,,,,,,,,. rA1 ,A@@@@@@@@@H:........,,,,,,.iX:
<br/> ;9. ,8A#@@@@@@#5,.,,,,,,,,,... 9A. 8@@@@@@@@@@M; ....,,,,,,,,S8
<br/> X3 iS8XAHH8s.,,,,,,,,,,...,..58hH@@@@@@@@@Hs ...,,,,,,,:Gs
<br/> r8, ,,,...,,,,,,,,,,..... ,h8XABMMHX3r. .,,,,,,,.rX:
<br/> :9, . .:,..,:;;;::,.,,,,,.. .,,. ..,,,,,,.59
<br/> .Si ,:.i8HBMMMMMB&5,.... . .,,,,,.sMr
<br/> SS :: h@@@@@@@@@@#; . ... . ..,,,,iM5
<br/> 91 . ;:.,1&@@@@@@MXs. . .,,:,:&S
<br/> hS .... .:;,,,i3MMS1;..,..... . . ... ..,:,.99
<br/> ,8; ..... .,:,..,8Ms:;,,,... .,::.83
<br/> s&: .... .sS553B@@HX3s;,. .,;13h. .:::&1
<br/> SXr . ...;s3G99XA&X88Shss11155hi. ,;:h&,
<br/> iH8: . .. ,;iiii;,::,,,,,. .;irHA
<br/> ,8X5; . ....... ,;iihS8Gi
<br/> 1831, .,;irrrrrs&
<br/> ;5A8r. .:;iiiiirrss1H
<br/> :X@H3s....... .,:;iii;iiiiirsrh
<br/> r#h:;,...,,.. .,,:;;;;;:::,... .:;;;;;;iiiirrss1
<br/> ,M8 ..,....,.....,,::::::,,... . .,;;;iiiiiirss11h
<br/> 8B;.,,,,,,,.,..... . .. .:;;;;iirrsss111h
<br/> i@5,:::,,,,,,,,.... . . .:::;;;;;irrrss111111
<br/> 9Bi,:,,,,...... ..r91;;;;;iirrsss1ss1111
<br/>
</pre>
</div>
);
}
}
export default Welcome;
.welcome-page {
// padding: 30px;
> .ant-card {
margin: 2px;
> .ant-card-body {
padding: 1em 0;
> .ant-calendar-picker {
display: block;
margin: .5em;
}
}
}
}
.ant-layout-container {
margin: 0 15px !important;
padding: 0 !important;
}
.welcome-page .doge {
font-size: 12px;
text-align: center;
background-image: -webkit-linear-gradient(left, #3498db, #f47920 10%, #d71345 20%, #f7acbc 30%,
#ffd400 40%, #3498db 50%, #f47920 60%, #d71345 70%, #f7acbc 80%, #ffd400 90%, #3498db);
color: transparent;
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-size: 200% 100%;
animation: masked-animation 4s infinite linear;
}
@keyframes masked-animation {
0% {
background-position: 0 0;
}
100% {
background-position: -100% 0;
}
}
.welcome-text {
color: red;
font-family: Lato, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif;
}
@media screen and (min-width: 992px) {
.welcome-page {
> .ant-card > .ant-card-body {
// copy from ant-design .ant-card-body
padding: 24px;
> .ant-calendar-picker {
display: inline-block;
margin: 0;
}
}
}
}
/**
* 定义整个项目的全局配置
*/
'use strict';
console.log("当前环境:", process.env.NODE_ENV);
module.exports = {
// 项目的名字
name: '麦乐 Erp 系统',
// 设置网页的favicon, 可以是外链, 也可以是本地
favicon: '/favicon.ico',
// footer中显示的字, 可以嵌入html标签
footer: '<a target="_blank" href="http://www.mailejifen.com">麦乐积分</a> All Rights Reserved. © 2018 Powered by Ant Design',
// 是否开启debug模式, 不会请求后端接口, 使用mock的数据
debug: false,
// 打包输出目录
outputPath: '../admin-api/src/main/resources/public',
tabMode: { // tab模式相关配置
enable: false, // 是否开启tab模式
allowDuplicate: false // 同一个菜单项只允许一个tab
},
// 日志配置
log: {
level: 'info', // 日志级别, 目前支持debug/info/warn/error 4种级别
debug: [],
info: [],
warn: [],
error: ['loggerA', 'loggerB']
},
api: {
// 接口地址
host: process.env.NODE_ENV === 'development' ? 'http://localhost:8080' : 'http://erp-admin.mailejifen.com',
// 后端代理地址
proxyHost: 'http://localhost:8081',
// ajax请求的路径前缀
path: '',
// 请求的超时时间, 单位毫秒
timeout: 15000
},
login: {
// 获取当前用户
getCurrentUser: 'user/getCurrentUser',
sso: '',
// 登录
validate: 'login',
// 登出
logout: 'logout'
},
// 图片和文件上传配置
upload: {
image: 'uploadImage', // 默认的上传图片接口
imageSizeLimit: 1000, // 默认的图片大小限制, 单位KB
file: 'uploadFile', // 默认的上传文件的接口
fileSizeLimit: 10240 // 默认的文件大小限制, 单位KB
},
// 侧边栏相关配置
sidebar: {
collapsible: true, // 是否显示折叠侧边栏的按钮
autoMenuSwitch: true // 只展开一个顶级菜单, 其他顶级菜单自动折叠
},
// DBTable组件相关配置
DBTable: {
pageSize: 20, // 表格每页显示多少条数据
showSizeChanger: true, // 是否可以修改每页显示多少条数据
pageSizeOptions: ['10', '20', '50', '100'], // 指定每页可以显示多少条
// 针对每个表格的默认配置
default: {
showExport: true, // 显示导出按钮, 默认true
showImport: true, // 显示导入按钮, 默认true
showInsert: true, // 显示新增按钮, 默认true
showUpdate: true, // 显示修改按钮, 默认true
showDelete: true, // 显示删除按钮, 默认true
showCheckbox: true, // 表格是否显示多选
showMultiSelected: true, // 显示批量修改
showChildTable: false, // 是否显示子表格
showPagination: true, // 是否显示分页
showSearchForm: true, // 是否显示搜索表单
asyncSchema: false, // 是否从服务端加载schema, 默认false
ignoreSchemaCache: false // 是否忽略schema的缓存, 对于异步schema而言, 默认只会请求一次后端接口然后缓存起来
}
},
// 以下一些辅助的函数, 不要修改
// 不能使用箭头函数, 会导致this是undefined
/**
* 是否要跨域请求
*
* @returns {boolean}
*/
isCrossDomain() {
if (this.api.host && this.api.host !== '') {
return true;
} else {
return false;
}
},
/**
* 是否单点登录
*
* @returns {boolean}
*/
isSSO() {
if (this.login.sso && this.login.sso !== '') {
return true;
} else {
return false;
}
},
/**
* 获得api请求的路径
*
* @returns {*}
*/
getAPIPath() {
if (this.tmpApiPath) { // 缓存
return this.tmpApiPath;
}
const paths = [];
if (this.isCrossDomain()) {
// 去除结尾的'/'
const tmp = this.api.host;
let index = tmp.length - 1;
// 如果超出指定的 index 范围,charAt返回一个空字符串
while (tmp.charAt(index) === '/') {
index--;
}
if (index < 0)
paths.push('');
else
paths.push(tmp.substring(0, index + 1));
} else {
paths.push('');
}
if (this.api.path) {
const tmp = this.api.path;
let begin = 0;
let end = tmp.length - 1;
while (tmp.charAt(begin) === '/') {
begin++;
}
while (tmp.charAt(end) === '/') {
end--;
}
if (begin > end)
paths.push('');
else
paths.push(tmp.substring(begin, end + 1));
} else {
paths.push('');
}
const tmpApiPath = paths.join('/');
this.tmpApiPath = tmpApiPath;
return tmpApiPath;
}
};
/**
* 程序的入口, 类似java中的main
*/
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import {Router, Route, IndexRoute, hashHistory} from 'react-router';
import './utils/index.js'; // 引入各种prototype辅助方法
import store from './redux/store.js'; // redux store
// 开始引入各种自定义的组件
import App from './components/App';
import Welcome from './components/Welcome';
import RoleTable from './components/RoleTable';
import Error from './components/Error';
// import DBTable from './components/DBTable';
// 将DBTable组件做成动态路由, 减小bundle size
// 注意不要再import DBTable了, 不然就没意义了
// 一些比较大/不常用的组件, 都可以考虑做成动态路由
const DBTableContainer = (location, cb) => {
require.ensure([], require => {
cb(null, require('./components/DBTable').default);
}, 'DBTable');
};
// 路由表, 只要menu.js中所有的叶子节点配置了路由就可以了
// 我本来想根据menu.js自动生成路由表, 但那样太不灵活了, 还是自己配置好些
const routes = (
<Provider store={store}>
<Router history={hashHistory}>
<Route path="/" component={App}>
/*首页*/
<IndexRoute component={Welcome}/>
/*权限管理*/
<Route path="permission">
<Route path="user" tableName="user" getComponent={DBTableContainer}/>
<Route path="role" tableName="role" component={RoleTable}/>
</Route>
/*错误页面*/
<Route path="*" component={Error}/>
</Route>
</Router>
</Provider>
);
ReactDOM.render(routes, document.getElementById('root'));
/**
* 定义sidebar和header中的菜单项
*
* 一些约定:
* 1.菜单最多3层;
* 2.只有"叶子"节点才能跳转;
* 3.所有的key都不能重复;
*/
// 其实理论上可以嵌套更多层菜单的, 但是我觉得超过3层就不好看了
// 可用的图标见这里: https://ant.design/components/icon-cn/
// 定义siderbar菜单
const sidebarMenu = [
{
key: '',
name: '首页',
icon: 'home'
},
{
key: 'permission',
name: '权限管理',
icon: 'lock',
child: [
{
key: 'role',
name: '角色',
icon: 'bars',
permsKey: [
'ROLE_SELECT',
'ROLE_INSERT',
'ROLE_UPDATE',
]
},
{
key: 'user',
name: '用户',
icon: 'bars',
permsKey: [
'USER_SELECT',
'USER_INSERT',
'USER_UPDATE'
]
},
]
},
// {
// key: 'noiconhaha',
// name: '数据统计',
// icon: 'bar-chart',
// child: [
// {
// key: 'nesnesnes',
// name: 'N64',
// },
// ],
// },
// {
// key: 'daohang',
// name: '日志管理',
// icon: 'code-o',
// child: [
// {
// key: '555',
// name: '选项5',
// },
// {
// key: 'sanji', // 最多只能到三级导航
// name: '三级导航',
// icon: 'laptop',
// child: [
// {
// key: '666',
// name: '选项6',
// icon: 'check',
// },
// {
// key: '777',
// name: '选项7',
// icon: 'close',
// },
// {
// key: '888',
// name: '选项8',
// },
// {
// key: '999',
// name: '选项9',
// },
// ],
// },
// ],
// },
// {
// key: 'test',
// name: '测试',
// icon: 'eye',
// child: [
// {
// key: 'aaa',
// name: '选项a',
// },
// {
// key: 'bbb',
// name: '选项b',
// icon: 'pause',
// },
// {
// key: 'ccc',
// name: '选项c',
// },
// {
// key: 'sanjiaaa', // 最多只能到三级导航
// name: '三级导航aaa',
// child: [
// {
// key: '666aa',
// name: '选项6',
// icon: 'meh',
// },
// ],
// },
// {
// key: 'sanjibbb', // 最多只能到三级导航
// name: '三级导航bbb',
// child: [
// {
// key: '666bb',
// name: '选项6',
// },
// ],
// },
// ],
// },
];
export default sidebarMenu;
// 定义header菜单, 格式和sidebar是一样的
// 特殊的地方在于, 我规定header的最右侧必须是用户相关操作的菜单, 所以定义了一个特殊的key
// 另外注意这个菜单定义的顺序是从右向左的, 因为样式是float:right
export const headerMenu = [
// {
// // 一个特殊的key, 定义用户菜单, 在这个submenu下面设置icon/name不会生效
// key: 'userMenu',
// child: [
// {
// key: 'modifyUser',
// name: '修改用户信息',
// icon: 'bulb',
// // 对于headerMenu的菜单项, 可以让它跳到外部地址, 如果设置了url属性, 就会打开一个新窗口
// // 如果不设置url属性, 行为和sidebarMenu是一样的, 激活特定的组件, 注意在index.js中配置好路由, 否则会404
// url: '###',
// }
// ],
// },
// {
// key: 'headerMenu2',
// name: 'header菜单',
// icon: 'team',
// child: [
// {
// key: 'headerMenu111',
// name: '菜单项1',
// icon: 'windows',
// url: 'http://jxy.me',
// },
// {
// key: '菜单项2',
// name: '短信表管理',
// url: 'http://jxy.me',
// },
// {
// key: '菜单项3',
// name: '选项3',
// icon: 'chrome',
// url: 'http://jxy.me',
// },
// ],
// },
// {
// key: 'headerMenu3',
// name: '我没有子菜单',
// icon: 'setting',
// url: 'http://jxy.me',
// },
// {
// key: 'headerMenu4',
// name: '我也没有子菜单',
// icon: 'shopping-cart',
// },
// {
// key: 'headerMenu5',
// name: '我没有图标',
// child: [
// {
// key: 'headerMenu5000000',
// name: '二级导航无子菜单',
// },
// {
// key: 'headerMenu51111',
// name: '三级导航',
// icon: 'laptop',
// child: [
// {
// key: 'headerMenu51111aa',
// name: '选项6',
// icon: 'meh',
// },
// {
// key: 'headerMenu51111bb',
// name: '选项7',
// icon: 'inbox',
// },
// ],
// },
// {
// key: 'headerMenu52222',
// name: '三级导航无图标',
// child: [
// {
// key: 'headerMenu52222aa',
// name: '选项8',
// },
// {
// key: 'headerMenu52222bb',
// name: '选项9',
// },
// ],
// },
// ],
// },
];
// 登录成功的事件
export const loginSuccessCreator = (user, permission) => {
return {
type: 'LOGIN_SUCCESS',
user: user,
permission: permission
};
};
const initState = {
login: false, // 是否已登录
user: {
userName: '未登录'
},
permission: []
};
const reducer = (state = initState, action = {}) => {
switch (action.type) {
case 'LOGIN_SUCCESS':
return {...state, login: true, user: action.user, permission: action.permission};
default:
return state;
}
};
export default {initState, reducer};
import globalConfig from "../config";
const initState = {
currentPage: 1,
pageSize: globalConfig.DBTable.pageSize
};
// 设置内容
export const setPageInfo = (currentPage,pageSize) => {
return {
type: 'SET_PAGE_INFO',
currentPage: currentPage,
pageSize: pageSize
};
};
const reducer = (state = initState, action) => {
switch (action.type) {
case 'SET_PAGE_INFO':
return {
...state,
currentPage: action.currentPage,
pageSize: action.pageSize
};
default:
return state;
}
};
export default {initState, reducer};
\ No newline at end of file
/**
* 一些约定:
*
* 1. redux相关的action/reducer都放到redux文件夹中, 每个组件一个文件, 文件名和组件名相同
* 2. 在这个文件中, 要先定义action creator, 再定义组件的initState, 最后定义reducer
* 3. 所有action creator function的名称都是XXXCreator, 采用camelCase风格, e.g., "sidebarCollapseCreator"
* 4. 一般而言, creator返回的action的type字段, 跟creator函数的名字是对应的, 全部大写, 下划线风格, e.g., "SIDEBAR_COLLAPSE"
* 5. action的格式采用社区的规范: https://github.com/acdlite/flux-standard-action
* 6. 每个组件只有一个reducer
*/
// https://github.com/acdlite/redux-actions
// 这个库可以用于生成action和reducer, 但我觉得好麻烦...
// 还是手写吧
/* 定义action creator */
export const sidebarCollapseCreator = () => {
return {type: 'SIDEBAR_COLLAPSE'};
};
/* 定义初始状态, 每个组件只需要关心自己的状态 */
const initState = {
collapse: false, // 是否折叠
};
/* 定义reducer, 每个组件只有一个reducer */
const reducer = (state = initState, action = {}) => {
switch (action.type) {
case 'SIDEBAR_COLLAPSE':
return {...state, collapse: !state.collapse};
default: // 注意必须要有default语句
return state;
}
};
/* 导出的字段名称固定, 方便后续的store去处理 */
export default {initState, reducer};
import {applyMiddleware, createStore, compose, combineReducers} from 'redux';
import createLogger from 'redux-logger';
import thunk from 'redux-thunk';
import globalConfig from '../config.js';
import Sidebar from './Sidebar.js';
import Login from './Login.js';
import Page from './Page';
/* 这个文件用于生成store */
// 设置各种中间件
const logger = createLogger();
let middleware;
if (globalConfig.debug) {
middleware = applyMiddleware(logger);
} else {
middleware = applyMiddleware(thunk);
}
// 设置redux dev tools
const composeEnhancers =
process.env.NODE_ENV !== 'production' &&
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const enhancer = composeEnhancers(
middleware,
);
// 整体的初始状态
// 就是把每个组件自己的初始状态组合起来, 注意key的名字和组件名一致
const initState = {
Sidebar: Sidebar.initState,
Login: Login.initState,
Page: Page.initState
};
// 定义reducer
// 每个组件自己的reducer负责维护自己的状态, 注意key的名字和组件名一致
const reducers = {
Sidebar: Sidebar.reducer,
Login: Login.reducer,
Page: Page.reducer
};
// 组合最终的store
const store = createStore(combineReducers(reducers), initState, enhancer);
export default store;
module.exports = {
showExport: false, // 显示导出按钮, 默认true
showImport: false, // 显示导入按钮, 默认true
showDelete: false, // 显示删除按钮, 默认true
showMultiSelected: false
};
import React from 'react';
import {Badge} from 'antd';
import globalConfig from '../config';
module.exports = [
{
key: 'id',
title: 'ID',
dataType: 'int',
primary: true,
showType: 'normal',
showInTable: true,
showInForm: true,
disabled: true,
render: (text, record) => text
},
{
title: '角色名称',
key: 'name',
dataType: 'varchar',
showInTable: true,
showInForm: true
},
{
title: '权限列表',
key: 'perms',
showInTable: false
}
// , {
// title: '操作',
// key: 'operation',
// render: (text, record) => <a href="###">编辑</a>
// }
];
\ No newline at end of file
import React from 'react';
module.exports = [
{
key: 'search_like_name',
title: '应用名称',
dataType: 'varchar'
},
{
key: 'search_eq_status',
title: '状态',
dataType: 'int',
showType: 'select',
options: [{key: "", value: "任意"}, {key: 0, value: "启用"}, {key: 1, value: "禁用"}]
}
];
\ No newline at end of file
import React from 'react';
import {Icon} from 'antd';
// 定义某个表的dataSchema, 结构跟querySchema很相似, 见下面的例子
// 注意: 所有的key不能重复
// 这个配置不只决定了table的schema, 也包括用于新增/删除的表单的schema
module.exports = [
{
key: 'id', // 传递给后端的key
title: 'ID', // 前端显示的名字
// 其实dataType对前端的意义不大, 更重要的是生成后端接口时要用到, 所以要和DB中的类型一致
// 对java而言, int/float/varchar/datetime会映射为Long/Double/String/Date
dataType: 'int', // 数据类型, 目前可用的: int/float/varchar/datetime
// 这一列是否是主键?
// 如果不指定主键, 不能update/delete, 但可以insert
// 如果指定了主键, insert/update时不能填写主键的值;
// 只有int/varchar可以作为主键, 但是实际上主键一般都是自增id
primary: true,
// 可用的showType: normal/radio/select/checkbox/multiSelect/textarea/image/file/cascader
showType: 'normal', // 默认是normal, 就是最普通的输入框
showInTable: true, // 这一列是否要在table中展示, 默认true
disabled: false, // 表单中这一列是否禁止编辑, 默认false
// 扩展接口, 决定了这一列渲染成什么样子
render: (text, record) => text,
},
{
key: 'name',
title: '用户名',
dataType: 'varchar', // 对于普通的input框, 可以设置addonBefore/addonAfter
placeholder: '请输入用户名',
addonBefore: (<Icon type="user"/>),
addonAfter: '切克闹',
defaultValue: 'foolbear', // 默认值, 只在insert时生效, update时不生效
},
{
key: 'weight',
title: '体重(千克)',
dataType: 'int',
min: 10,
defaultValue: 70, // 默认值
disabled: true,
showInForm: false, // 这一列是否要在表单中展示, 默认true. 这种列一般都是不需要用户输入的, DB就会给一个默认值那种
},
{
key: 'gender',
title: '性别',
dataType: 'int',
showType: 'radio',
options: [{key: '1', value: '男'}, {key: '2', value: '女'}],
defaultValue: '1',
disabled: true,
},
{
key: 'marriage',
title: '婚否',
dataType: 'varchar',
showType: 'select',
options: [{key: 'yes', value: '已婚'}, {key: 'no', value: '未婚'}],
// 对于dataSchema可以设置校验规则, querySchema不能设置
// 设置校验规则, 参考https://github.com/yiminghe/async-validator#rules
validator: [{type: 'string', required: true, message: '必须选择婚否!'}],
},
{
key: 'interest',
title: '兴趣爱好',
dataType: 'int',
showType: 'checkbox',
options: [{key: '1', value: '吃饭'}, {key: '2', value: '睡觉'}, {key: '3', value: '打豆豆'}],
defaultValue: ['1', '2'],
validator: [{type: 'array', required: true, message: '请至少选择一项兴趣'}],
width: 120, // 指定这一列的宽度
},
{
key: 'good',
title: '优点',
dataType: 'varchar',
showType: 'multiSelect',
options: [{key: 'lan', value: '懒'}, {key: 'zhai', value: '宅'}],
validator: [{type: 'array', required: true, message: '请选择优点'}],
},
{
key: 'pic1',
title: '头像',
dataType: 'varchar',
showType: 'image', // 后端必须提供图片上传接口
showInTable: false,
},
{
key: 'desc',
title: '个人简介',
dataType: 'varchar',
showType: 'textarea', // 用于编辑大段的文本
showInTable: false,
defaultValue: '个人简介个人简介个人简介',
validator: [{type: 'string', max: 20, message: '最长20个字符'}],
},
{
key: 'score',
title: '分数',
dataType: 'int',
max: 99,
min: 9,
},
{
key: 'gpa',
title: 'GPA',
dataType: 'float',
max: 9.9,
placeholder: '哈哈',
width: 50,
},
{
key: 'birthday',
title: '生日',
// 对于日期类型要注意下, 在js端日期被表示为yyyy-MM-dd HH:mm:ss的字符串, 在java端日期被表示为java.util.Date对象
// fastjson反序列化时可以自动识别
// 序列化倒是不用特别配置, 看自己需求, fastjson会序列化为一个字符串, 前端原样展示
dataType: 'datetime',
// 对于datetime而言, 配置showType是无意义的
placeholder: 'happy!',
},
{
key: 'xxday',
title: 'xx日期',
dataType: 'datetime',
defaultValue: '2017-01-01 11:22:33',
showInTable: false,
showInForm: false, // 这个只是测试下...如果一列在table和form中都不出现, 那就不如直接去掉.
// 另外showInForm=false时, 很多针对form的字段都没意义了, 比如defaultValue/placeholder/validator等等
},
];
import React from 'react';
import {Icon} from 'antd';
// 定义某个表的querySchema
// schema的结构和含义参考下面的例子
// 注意: 所有的key不能重复
module.exports = [
{
key: 'name', // 传递给后端的字段名
title: '用户名', // 前端显示的名称
placeholder: '请输入用户名', // 提示语, 可选
// 数据类型, 前端会根据数据类型展示不同的输入框
// 目前可用的dataType: int/float/varchar/datetime
// 为啥我会把字符串定义为varchar而不是string呢...我也不知道, 懒得改了...
dataType: 'varchar',
// 显示类型, 一些可枚举的字段, 比如type, 可以被显示为单选框或下拉框
// 默认显示类型是normal, 就是一个普通的输入框, 这时可以省略showType字段
// 目前可用的showType: normal/select/radio/between/checkbox/multiSelect/cascader
// between只能用于int/float/datetime, 会显示2个输入框, 用于范围查询
showType: 'normal',
// 有一点要注意, 就算showType是normal, 也不意味是等值查询, 只是说传递给后台的是单独的一个值
// 至于后台用这个值做等值/大于/小于/like, 前端不关心
// 对于varchar类型的字段, 可以设置前置标签和后置标签
addonBefore: (<Icon type="user"/>),
defaultValue: 'foolbear', // 默认值
},
{
key: 'blog',
title: 'BLOG',
placeholder: '请输入网址',
dataType: 'varchar',
showType: 'normal',
addonBefore: 'http://', // 这个前置和后置标签的值不能被传到后端, 其实作用很有限, 也就是美观点而已, antd官方的例子中还有用select做addon的, 感觉也没啥大用...
addonAfter: '.me',
defaultValue: 'jxy',
},
{
key: 'age',
title: '年龄',
placeholder: '请输入年龄',
dataType: 'int',
defaultValue: 18,
// 对于数字类型(int/float), 可以配置max/min
min: 0,
max: 99,
},
{
key: 'weight',
title: '体重(kg)',
dataType: 'float', // 小数会统一保留2位
defaultValue: 50.1,
min: 0,
max: 99.9,
},
{
key: 'type',
title: '类型',
dataType: 'int',
showType: 'select', // 下拉框选择, antd版本升级后, option的key要求必须是string, 否则会有个warning, 后端反序列化时要注意
options: [{key: '1', value: '类型1'}, {key: '2', value: '类型2'}],
defaultValue: '1', // 这个defaultValue必须和options中的key是对应的
},
{
key: 'userType',
title: '用户类型',
dataType: 'varchar', // 理论上来说, 这里的dataType可以是int/float/varchar甚至datetime, 反正对前端而言都是字符串, 只是后端反序列化时有区别
showType: 'radio', // 单选框, 和下拉框schema是一样的, 只是显示时有差别
options: [{key: 'typeA', value: '类型A'}, {key: 'typeB', value: '类型B'}],
defaultValue: 'typeB',
},
{
key: 'score',
title: '分数',
dataType: 'int',
showType: 'between', // 整数范围查询, 对于范围查询, 会自动生成xxBegin/xxEnd两个key传递给后端
defaultValueBegin: 9, // 对于between类型不搞max/min了, 太繁琐
defaultValueEnd: 99,
},
{
key: 'gpa',
title: 'GPA',
dataType: 'float',
showType: 'between', // 小数也可以范围查询, 固定两位小数
placeholderBegin: '哈哈', // 对于范围查询, 可以定义placeholderBegin和placeholderBegin, 用于两个框的提示语
placeholderEnd: '切克闹', // 如果不定义, 对于int/float的范围查询, 提示语是"最小值"/"最大值", 对于日期的范围查询, 提示语是"开始日期"/"结束日期"
defaultValueEnd: 99.9,
},
{
key: 'height',
title: '身高(cm)',
placeholder: '哈哈哈',
dataType: 'float', // 小数等值查询
},
{
key: 'duoxuan1',
title: '多选1',
dataType: 'int', // 跟select一样, 这里的值其实也可以是int/float/varchar/datetime
showType: 'checkbox', // checkbox
options: [{key: '1', value: '类型1'}, {key: '2', value: '类型2'}], // 同样注意, option的key必须是字符串
defaultValue: ['1', '2'], // 多选的defaultValue是个数组
},
{
key: 'duoxuan2',
title: '多选2',
dataType: 'varchar',
showType: 'multiSelect', // 另一种多选
options: [{key: 'type1', value: '类型1'}, {key: 'type2', value: '类型2'}],
defaultValue: ['type1'],
},
{
key: 'work',
title: '工作年限',
dataType: 'int',
min: 3,
},
{
key: 'duoxuan3',
title: '多选3',
dataType: 'varchar',
showType: 'multiSelect',
options: [{key: 'K', value: '开'}, {key: 'F', value: '封'}, {key: 'C', value: '菜'}],
defaultValue: ['K', 'F', 'C'],
},
{
key: 'primarySchool',
title: '入学日期',
dataType: 'datetime', // 日期范围查询, 日期范围查询占用的显示空间会很大, 注意排版
showType: 'between',
defaultValueBegin: '2016-01-01 12:34:56', // 注意日期类型defaultValue的格式
defaultValueEnd: '2016-12-01 22:33:44',
},
{
key: 'birthday',
title: '出生日期',
dataType: 'datetime',
showType: 'between',
defaultValueBegin: '2016-01-01 12:34:56',
},
{
key: 'xxbirthday',
title: 'XX日期',
dataType: 'datetime', // 日期等值查询
},
];
import React from 'react';
import {Link} from 'react-router';
module.exports = [
{
key: 'id',
title: 'ID',
dataType: 'int',
primary: true,
},
{
key: 'name',
title: '姓名',
dataType: 'varchar',
validator: [{type: 'string', max: 10, message: '最多10个字符'}],
},
{
key: 'touxiang',
title: '头像',
dataType: 'varchar',
showType: 'image',
width: 60,
},
{
key: 'desc',
title: '描述',
dataType: 'varchar',
},
{
key: 'score',
title: '分数',
dataType: 'int',
max: 18,
validator: [{required: true, message: '必填'}],
},
{
key: 'gpa',
title: 'GPA',
dataType: 'float',
},
{
key: 'birthday',
title: '生日',
dataType: 'datetime',
},
// 定义针对单条记录的操作
// 常见的针对单条记录的自定义操作有哪些? 无非是更新和删除
// 注意, 如果没有定义主键, 是不允许更新和删除的
{
// 这个key是我预先定义好的, 注意不要冲突
key: 'singleRecordActions',
title: '我是自定义操作', // 列名
width: 300, // 宽度
actions: [
{
name: '更新姓名',
type: 'update', // 更新单条记录
keys: ['name'], // 允许更新哪些字段, 如果不设置keys, 就允许更所有字段
},
{
name: '更新分数和GPA',
type: 'update',
keys: ['score', 'gpa'], // 弹出的modal中只会有这两个字段
},
{
name: '更新生日',
type: 'update',
keys: ['birthday'],
visible: (record) => record.id >= 1010, // 所有action都可以定义visible函数, 返回false则对这行记录不显示这个操作
},
{
name: '更新头像',
type: 'update',
keys: ['touxiang'],
},
{
type: 'newLine', // 换行, 纯粹用于排版的, 更美观一点
},
{
type: 'newLine',
},
{
name: '删除',
type: 'delete', // 删除单条记录
},
{
// 如果不是预定义的type(update/delete/newLine/component), 就检查是否有render函数
// 有render函数就直接执行
render: (record) => <a href={`http://jxy.me?id=${record.id}`} target="_blank">{'跳转url'}</a>,
},
{
// react-router的Link组件
render: (record) => <Link to={`/index/option1?name=${record.id}`}>{'跳转其他组件'}</Link>,
},
{
// 这样写似乎和Link组件是一样的效果
render: (record) => <a href={`/#/index/option1?name=${record.id}`}>{'跳转2'}</a>,
},
{
type: 'newLine',
},
{
type: 'newLine',
},
],
},
];
import React from 'react';
import {Link} from 'react-router';
module.exports = [
{
key: 'id',
title: 'ID',
dataType: 'int',
primary: true,
// 当前列如何渲染
render(text) {
// 只是一个例子, 说明下render函数中可以用this, 甚至可以this.setState之类的
// 我会把this绑定到当前的InnerTable组件上
// 但需要注意, 如果要使用this, render必须是普通的函数, 不能是箭头函数, 因为箭头函数不能手动绑定this
// this不要滥用, 搞出内存泄漏就不好了
// render应该尽量是一个纯函数, 不要有副作用
// console.log(this.props.tableName);
return text;
},
// 表格中根据这一列排序, 排序规则可以配置
sorter: (a, b) => a.id - b.id,
},
{
key: 'avatar',
title: '头像',
dataType: 'varchar',
showType: 'image',
sizeLimit: 500, // 限制图片大小, 单位kb, 如果不设置这个属性, 就使用默认配置, 见config.js中相关配置
max: 1, // 最多可以上传几张图片? 默认1
// 默认值, 可以是string也可以是string array, 跟max有关
defaultValue: 'http://jxy.me/about/avatar.jpg',
width: 100, // 图片在表格中显示时会撑满宽度, 为了美观要自己调整下
accept: '.jpg', // 允许上传的文件类型, 可以省略, 默认值是".jpg,.png,.gif,.jpeg"
placeholder: '请上传jpg格式, 分辨率不要超过200x200', // 提示语
},
{
key: 'photos',
title: '风景照',
dataType: 'varchar',
showType: 'image',
max: 5,
// 图片的上传接口, 可以针对每个上传组件单独配置, 如果不单独配置就使用config.js中的默认值
// 如果这个url是http开头的, 就直接使用这个接口; 否则会根据config.js中的配置判断是否加上host
url: 'http://hahaha/uploadImage',
// max>1时, 默认值是string array
defaultValue: ['http://jxy.me/about/avatar.jpg', 'http://jxy.me/about/avatar.jpg'],
width: 150,
placeholder: '药药切克闹',
},
{
// 文件上传和图片上传其实是很类似的
key: 'jianli',
title: '个人简历',
dataType: 'varchar',
showType: 'file',
accept: '.pdf',
sizeLimit: 20480,
placeholder: '请上传pdf格式, 大小不要超过20M',
validator: [{required: true, message: '必填'}],
},
{
key: 'guanshui',
title: '科研成果',
dataType: 'varchar',
showType: 'file',
accept: '.pdf',
max: 3,
placeholder: '请上传论文, pdf格式, 最多3个',
sorter: (a, b) => a.guanshui.length - b.guanshui.length,
},
{
key: 'url',
title: '个人主页',
dataType: 'varchar',
validator: [{type: 'url', message: '主页有误'}],
// 跳转到外部链接例子, 会打开一个新窗口
// 我本来想要不要加个showType=url, 但考虑了下还是用render去实现吧
// 对于某些showType(比如image)我会有默认的render, 但用户自定义的render是最优先的
render: (text, record) => <a href={`/index/option1?name=${record.id}`}>{text}</a>,
},
{
key: 'mail',
title: '邮箱',
dataType: 'varchar',
validator: [{type: 'email', required: true, message: '邮箱地址有误'}],
// 跳转邮箱地址例子
render: (text) => <a href="mailto:foolbeargm@gmail.com" target="_blank">{'foolbeargm@gmail.com'}</a>,
},
{
key: 'phoneModel',
title: '手机型号',
dataType: 'varchar',
// 跳转其他组件的例子, 可以带参数, 一般用于关联查询之类的
// 其实就是react-router的配置
render: (text, record) => <Link to={`/index/option1?name=${record.id}`}>{'跳转其他组件'}</Link>,
validator: [{type: 'string', pattern: /^[a-zA-Z0-9]+$/, message: '只能是数字+字母'}],
},
{
key: 'experience',
title: '使用经验',
dataType: 'varchar',
validator: [{type: 'string', max: 10, message: '最多10个字符!'}],
},
{
key: 'location',
title: '地理位置',
dataType: 'varchar',
showType: 'cascader',
options: [{
value: 'zhejiang',
label: '浙江',
children: [{
value: 'hangzhou',
label: '杭州',
children: [{
value: 'xihu',
label: '西湖',
}],
}],
}, {
value: 'yuzhou',
label: '宇宙中心',
children: [{
value: 'wudaokou',
label: '五道口',
}],
}],
},
];
// 定义某个表的querySchema
// schema的结构和含义参考下面的例子
// 注意: 所有的key不能重复
module.exports = [
{
key: 'id', // 传递给后端的字段名
title: 'ID', // 前端显示的名称
dataType: 'int',
},
{
key: 'content',
title: '内容',
dataType: 'varchar',
},
{
key: 'phoneModel',
title: '手机型号',
dataType: 'varchar',
},
{
key: 'experience',
title: '使用经验',
dataType: 'varchar',
},
{
key: 'frequency',
title: '使用频率',
dataType: 'varchar',
},
{
key: 'isNative',
title: '是否母语',
dataType: 'varchar',
showType: 'radio',
options: [{key: 'yes', value: '是'}, {key: 'no', value: '否'}],
},
{
key: 'type',
title: '类型',
dataType: 'int',
showType: 'select',
options: [{key: '1', value: '类型1'}, {key: '2', value: '类型2'}],
defaultValue: '1',
},
// 级联选择, 和select很类似
// 同样支持placeholder/defaultValue等属性
{
key: 'location',
title: '地理位置',
dataType: 'varchar', // 一般而言dataType是字符串, 但也可以是数字
showType: 'cascader',
defaultValue: ['zhejiang', 'hangzhou', 'xihu'],
options: [{
value: 'zhejiang', // option的value必须是字符串, 和select类似
label: '浙江',
children: [{
value: 'hangzhou',
label: '杭州',
children: [{
value: 'xihu',
label: '西湖',
}],
}],
}, {
value: 'yuzhou',
label: '宇宙中心',
children: [{
value: 'wudaokou',
label: '五道口',
}],
}],
},
];
module.exports = {
showExport: false, // 显示导出按钮, 默认true
showImport: false, // 显示导入按钮, 默认true
showDelete: false, // 显示删除按钮, 默认true
showMultiSelected: false
};
import React from 'react';
import {Badge} from 'antd';
module.exports = [
{
key: 'id',
title: 'ID',
dataType: 'int',
primary: true,
showType: 'normal',
showInTable: true,
showInForm: true,
disabled: true,
render: (text, record) => text
},
{
key: 'username',
title: '用户名',
dataType: 'varchar',
validator: [{required: true, message: '此项必填'}]
},
{
key: 'tmpPwd',
title: '密码',
dataType: 'varchar',
showInTable: false,
validator: [{required: true, message: '此项必填'}]
},
{
key: 'realname',
title: '真实姓名',
dataType: 'varchar',
validator: [{required: true, message: '此项必填'}]
},
{
key: 'roleId',
title: '角色',
dataType: 'varchar',
showInTable: false,
showType: 'multiSelect',
optionFormat: "id:name",
isAsyncData: true,
url: 'role/select',
// validator: [{required: true, message: '此项必填'}]
},
{
key: 'status',
title: '状态',
dataType: 'int',
showType: 'select',
options: [{key: 0, value: "启用"}, {key: 1, value: "禁用"}],
render: (record) => <Badge status={record === '启用' ? "success" : "error"} text={record}/>
},
{
key: 'createTime',
title: '创建时间',
dataType: 'datetime',
showInForm: false
}
];
\ No newline at end of file
import React from 'react';
module.exports = [
{
key: 'search_eq_username',
title: '用户名',
dataType: 'varchar'
},
{
key: 'search_eq_realname',
title: '真实姓名',
dataType: 'varchar'
}
];
\ No newline at end of file
@font-face {font-family: "iconfont";
src: url('./iconfont.eot?t=1534845673807'); /* IE9*/
src: url('./iconfont.eot?t=1534845673807#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAXgAAsAAAAACIgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADMAAABCsP6z7U9TLzIAAAE8AAAARAAAAFY8d0lQY21hcAAAAYAAAABUAAABhme50M5nbHlmAAAB1AAAAgYAAAJc6DKcWGhlYWQAAAPcAAAALwAAADYT6EvOaGhlYQAABAwAAAAdAAAAJAljBQlobXR4AAAELAAAAAwAAAAMDYUAAGxvY2EAAAQ4AAAACAAAAAgAcAEubWF4cAAABEAAAAAfAAAAIAEUAJNuYW1lAAAEYAAAAUUAAAJtPlT+fXBvc3QAAAWoAAAAOAAAAEruBBRceJxjYGRgYOBikGPQYWB0cfMJYeBgYGGAAJAMY05meiJQDMoDyrGAaQ4gZoOIAgCKIwNPAHicY2BkaWKcwMDKwMHUyXSGgYGhH0IzvmYwYuRgYGBiYGVmwAoC0lxTGByeMTy7w9zwv4EhhrmBoREozAiSAwAXtw1CeJztkLERgDAMA99OSMExCAUDUTF56qyRKA5sge7eOsuuBGxAEpfIYA/G1K3UIk/skef4KXLDG632Dp9LplsJd43CryPm+W4+21rMZltd4AOzzBC2eJxVT89rE0EUnreb3UnS7OxsXnbXpiR0d8luJLg0P9yVxPrjpE3xUEHEk/QQhSBeIlhBUA96Ew8eC1ZvikhF/wH1KOhNsdKToEc9SUHs1NmWHoSZ933z+L7vzSNAyM4j9be6TI4TAo4baQxcTBuhTzUXkzBq0EaSYoBOEqmyqaaZQItU3UkbeuhqWV9KpFCvKNdevzLEd0qVcSg2NFMXm+FIzVPxzYAR6pP29VJLW8n7dHJArNrUhHpppFZNCAxmQtWsquMSzJjUFqvTsKmocxs6K4rntfe53McanC+Y2ue5IpThrFh3pqZhqVwWL7CoeManmnhm2YUPer74tmBbcK721fAUTT1ECJX73dLuqrfJEXKSDMlFcoXcIPfkrjG04HAMgefrtF2Hrmd5MUgeWL0wajOwrYozgDp0ktTbrd1OMi9d4Ie9pONUdG+fpEEMkU+tbh3cwNdrUHHcLhyTkR2HAZWxbi8ZQCZtSfd/D+2gPmO9BOTbvziCRPgpVkrDy6fZInKOyK9OLV5aYPBA9haMUxy1pb9v6s3m0WZTubOHD6PSsBfOroGbO5E5bv55B7YByxaCzcQaxzHyLQvR2pLUZveZLQtHxWflcabnuP007vdjmN/7QWaJB4M4u8h/nPmyvZ6N2T/juA8WTGQ2chkDygUZMps5n+wSfCzBQyT/AOcncrkAAHicY2BkYGAAYiOVhnvx/DZfGbhZGEDg+iLmlwj6fz1rK3MDkMvBwAQSBQAoKArEAHicY2BkYGBu+N/AEMPaygAEQJKRARUwAwBeEwN4AAAABAAAAAQAAAAFhQAAAAAAAABwAS54nGNgZGBgYGZoZ2BnAAEmIOYCQgaG/2A+AwAWawGoAHicZY9NTsMwEIVf+gekEqqoYIfkBWIBKP0Rq25YVGr3XXTfpk6bKokjx63UA3AejsAJOALcgDvwSCebNpbH37x5Y08A3OAHHo7fLfeRPVwyO3INF7gXrlN/EG6QX4SbaONVuEX9TdjHM6bCbXRheYPXuGL2hHdhDx18CNdwjU/hOvUv4Qb5W7iJO/wKt9Dx6sI+5l5XuI1HL/bHVi+cXqnlQcWhySKTOb+CmV7vkoWt0uqca1vEJlODoF9JU51pW91T7NdD5yIVWZOqCas6SYzKrdnq0AUb5/JRrxeJHoQm5Vhj/rbGAo5xBYUlDowxQhhkiMro6DtVZvSvsUPCXntWPc3ndFsU1P9zhQEC9M9cU7qy0nk6T4E9XxtSdXQrbsuelDSRXs1JErJCXta2VELqATZlV44RelzRiT8oZ0j/AAlabsgAAAB4nGNgYoAALgbsgJmRiZGZkYVBICszL72iNDGvOCMzOSMxL503OSO1AiiWU5pXkpjHwAAA3SEMWg==') format('woff'),
url('./iconfont.ttf?t=1534845673807') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url('./iconfont.svg?t=1534845673807#iconfont') format('svg'); /* iOS 4.1- */
}
.iconfont {
font-family:"iconfont" !important;
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-jingxuanshichang:before { content: "\e600"; }
.icon-chexingluntan:before { content: "\e6dc"; }
(function(window){var svgSprite='<svg><symbol id="icon-jingxuanshichang" viewBox="0 0 1024 1024"><path d="M627.977626 419.774566c119.09417 1.609626 238.185267 3.234714 357.295821 4.859699-4.320563-15.968666-8.659558-31.970099-12.985242-47.956173-94.425395 72.591258-188.872192 145.212314-283.298611 217.802547-9.083187 6.970778-15.551795 17.022669-11.830886 29.056819 35.275878 113.777357 70.511718 227.553587 105.774285 341.313536 12.613837-9.670246 25.217331-19.389645 37.817856-29.076173-98.232218-67.357082-196.461363-134.754099-294.693581-202.115277-6.971085-4.792115-19.035341-4.890419-25.973555 0L208.141824 939.727667c12.618957 9.687654 25.200947 19.373261 37.821952 29.057843 33.73056-114.214298 67.447808-228.459213 101.16311-342.70423 3.554099-11.964518-2.581914-22.152499-11.829862-29.056819-95.382221-71.316275-190.799155-142.631526-286.198784-213.963059-4.321587 16.00041-8.661606 31.986483-12.985242 47.956173 119.059354-3.234714 238.11881-6.450893 357.196595-9.669222 12.030464-0.334643 21.047194-7.725978 24.833638-18.90048 38.34071-112.771379 76.697907-225.542861 115.056026-338.296832l-49.636478 0c39.850189 112.235213 79.714714 224.469402 119.579238 336.723046 11.026534 31.032832 60.796109 17.659187 49.634406-13.689754-39.846093-112.235213-79.728026-224.47145-119.57719-336.723046-8.614502-24.230912-41.238835-24.733286-49.636454 0-38.35607 112.771379-76.713267 225.541837-115.052954 338.295808 8.260506-6.283059 16.522957-12.582605 24.80087-18.884096-119.077786 3.21833-238.137242 6.434509-357.196595 9.668198-27.38176 0.753152-32.77783 33.195008-13.003674 47.974605 95.414886 71.315251 190.831923 142.630502 286.212096 213.945651-3.9552-9.684582-7.874662-19.371213-11.830886-29.055795-33.732608 114.247066-67.446784 228.458189-101.179494 342.70423-6.049997 20.51113 18.950349 42.393498 37.80352 29.072179 97.058406-68.499046 194.083123-136.983859 291.10569-205.485978-15.284736 10.792858-34.48576-7.003546-12.482765 8.079053 11.142246 7.640986 22.268006 15.282074 33.380557 22.919987 33.763328 23.160525 67.547136 46.316851 101.29408 69.476352 49.132954 33.679053 98.231194 67.35913 147.331379 101.074022 18.783539 12.868096 44.272128-8.344064 37.83936-29.087437-35.254374-113.779405-70.532198-227.520922-105.772237-341.297152-3.956224 9.685606-7.909478 19.371213-11.84727 29.054771 94.426419-72.58921 188.871168-145.194906 283.312947-217.800499 19.222528-14.779597 14.782259-47.586816-13.00265-47.956173-119.110554-1.62601-238.202675-3.250995-357.295821-4.860723C594.746368 367.84681 594.798592 419.322266 627.977626 419.774566" ></path></symbol><symbol id="icon-chexingluntan" viewBox="0 0 1413 1024"><path d="M1320.606897 349.572414C1313.544828 331.917241 1292.358621 317.793103 1271.172414 317.793103l-176.551724 0L1094.62069 211.862069c0-21.186207-14.124138-35.310345-35.310345-35.310345L529.655172 176.551724c-21.186207 0-35.310345 14.124138-35.310345 35.310345l0 0c0 21.186207 14.124138 35.310345 35.310345 35.310345l494.344828 0 0 600.275862L632.055172 847.448276c-14.124138-60.027586-70.62069-105.931034-137.710345-105.931034s-120.055172 45.903448-137.710345 105.931034L282.482759 847.448276l0-317.793103c0-21.186207-14.124138-35.310345-35.310345-35.310345l0 0c-21.186207 0-35.310345 14.124138-35.310345 35.310345l0 353.103448c0 21.186207 14.124138 35.310345 35.310345 35.310345l109.462069 0c14.124138 60.027586 70.62069 105.931034 137.710345 105.931034s120.055172-45.903448 137.710345-105.931034l399.006897 0c14.124138 60.027586 70.62069 105.931034 137.710345 105.931034s120.055172-45.903448 137.710345-105.931034L1377.103448 918.068966c21.186207 0 35.310345-14.124138 35.310345-35.310345l0-229.517241C1412.413793 564.965517 1320.606897 349.572414 1320.606897 349.572414zM494.344828 953.37931c-38.841379 0-70.62069-31.77931-70.62069-70.62069s31.77931-70.62069 70.62069-70.62069 70.62069 31.77931 70.62069 70.62069S533.186207 953.37931 494.344828 953.37931zM1165.241379 953.37931c-38.841379 0-70.62069-31.77931-70.62069-70.62069s31.77931-70.62069 70.62069-70.62069 70.62069 31.77931 70.62069 70.62069S1204.082759 953.37931 1165.241379 953.37931zM1341.793103 847.448276l-38.841379 0c-14.124138-60.027586-70.62069-105.931034-137.710345-105.931034-24.717241 0-49.434483 7.062069-70.62069 17.655172L1094.62069 388.413793l158.896552 0c0 0 24.717241 17.655172 28.248276 35.310345l-63.558621 0c-21.186207 0-35.310345 14.124138-35.310345 35.310345l0 141.241379c0 21.186207 14.124138 35.310345 35.310345 35.310345L1341.793103 635.586207C1341.793103 635.586207 1341.793103 847.448276 1341.793103 847.448276z" ></path><path d="M0 35.310345c0-21.186207 17.655172-35.310345 35.310345-35.310345l353.103448 0c21.186207 0 35.310345 14.124138 35.310345 35.310345 0 21.186207-17.655172 35.310345-35.310345 35.310345L35.310345 70.62069C14.124138 70.62069 0 56.496552 0 35.310345z" ></path><path d="M105.931034 211.862069c0-21.186207 14.124138-35.310345 35.310345-35.310345l247.172414 0c21.186207 0 35.310345 14.124138 35.310345 35.310345 0 21.186207-14.124138 35.310345-35.310345 35.310345L141.241379 247.172414C120.055172 247.172414 105.931034 233.048276 105.931034 211.862069z" ></path><path d="M211.862069 388.413793c0-21.186207 17.655172-35.310345 35.310345-35.310345l141.241379 0c17.655172 0 35.310345 14.124138 35.310345 35.310345 0 21.186207-17.655172 35.310345-35.310345 35.310345L247.172414 423.724138C225.986207 423.724138 211.862069 409.6 211.862069 388.413793z" ></path></symbol></svg>';var script=function(){var scripts=document.getElementsByTagName("script");return scripts[scripts.length-1]}();var shouldInjectCss=script.getAttribute("data-injectcss");var ready=function(fn){if(document.addEventListener){if(~["complete","loaded","interactive"].indexOf(document.readyState)){setTimeout(fn,0)}else{var loadFn=function(){document.removeEventListener("DOMContentLoaded",loadFn,false);fn()};document.addEventListener("DOMContentLoaded",loadFn,false)}}else if(document.attachEvent){IEContentLoaded(window,fn)}function IEContentLoaded(w,fn){var d=w.document,done=false,init=function(){if(!done){done=true;fn()}};var polling=function(){try{d.documentElement.doScroll("left")}catch(e){setTimeout(polling,50);return}init()};polling();d.onreadystatechange=function(){if(d.readyState=="complete"){d.onreadystatechange=null;init()}}}};var before=function(el,target){target.parentNode.insertBefore(el,target)};var prepend=function(el,target){if(target.firstChild){before(el,target.firstChild)}else{target.appendChild(el)}};function appendSvg(){var div,svg;div=document.createElement("div");div.innerHTML=svgSprite;svgSprite=null;svg=div.getElementsByTagName("svg")[0];if(svg){svg.setAttribute("aria-hidden","true");svg.style.position="absolute";svg.style.width=0;svg.style.height=0;svg.style.overflow="hidden";prepend(svg,document.body)}}if(shouldInjectCss&&!window.__iconfont__svg__cssinject__){window.__iconfont__svg__cssinject__=true;try{document.write("<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>")}catch(e){console&&console.log(e)}}ready(appendSvg)})(window)
\ No newline at end of file
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<!--
2013-9-30: Created.
-->
<svg>
<metadata>
Created by iconfont
</metadata>
<defs>
<font id="iconfont" horiz-adv-x="1024" >
<font-face
font-family="iconfont"
font-weight="500"
font-stretch="normal"
units-per-em="1024"
ascent="896"
descent="-128"
/>
<missing-glyph />
<glyph glyph-name="jingxuanshichang" unicode="&#58880;" d="M627.977626 476.225434c119.09417-1.609626 238.185267-3.234714 357.295821-4.859699-4.320563 15.968666-8.659558 31.970099-12.985242 47.956173-94.425395-72.591258-188.872192-145.212314-283.298611-217.802547-9.083187-6.970778-15.551795-17.022669-11.830886-29.056819 35.275878-113.777357 70.511718-227.553587 105.774285-341.313536 12.613837 9.670246 25.217331 19.389645 37.817856 29.076173-98.232218 67.357082-196.461363 134.754099-294.693581 202.115277-6.971085 4.792115-19.035341 4.890419-25.973555 0L208.141824-43.727667c12.618957-9.687654 25.200947-19.373261 37.821952-29.057843 33.73056 114.214298 67.447808 228.459213 101.16311 342.70423 3.554099 11.964518-2.581914 22.152499-11.829862 29.056819-95.382221 71.316275-190.799155 142.631526-286.198784 213.963059-4.321587-16.00041-8.661606-31.986483-12.985242-47.956173 119.059354 3.234714 238.11881 6.450893 357.196595 9.669222 12.030464 0.334643 21.047194 7.725978 24.833638 18.90048 38.34071 112.771379 76.697907 225.542861 115.056026 338.296832l-49.636478 0c39.850189-112.235213 79.714714-224.469402 119.579238-336.723046 11.026534-31.032832 60.796109-17.659187 49.634406 13.689754-39.846093 112.235213-79.728026 224.47145-119.57719 336.723046-8.614502 24.230912-41.238835 24.733286-49.636454 0-38.35607-112.771379-76.713267-225.541837-115.052954-338.295808 8.260506 6.283059 16.522957 12.582605 24.80087 18.884096-119.077786-3.21833-238.137242-6.434509-357.196595-9.668198-27.38176-0.753152-32.77783-33.195008-13.003674-47.974605 95.414886-71.315251 190.831923-142.630502 286.212096-213.945651-3.9552 9.684582-7.874662 19.371213-11.830886 29.055795-33.732608-114.247066-67.446784-228.458189-101.179494-342.70423-6.049997-20.51113 18.950349-42.393498 37.80352-29.072179 97.058406 68.499046 194.083123 136.983859 291.10569 205.485978-15.284736-10.792858-34.48576 7.003546-12.482765-8.079053 11.142246-7.640986 22.268006-15.282074 33.380557-22.919987 33.763328-23.160525 67.547136-46.316851 101.29408-69.476352 49.132954-33.679053 98.231194-67.35913 147.331379-101.074022 18.783539-12.868096 44.272128 8.344064 37.83936 29.087437-35.254374 113.779405-70.532198 227.520922-105.772237 341.297152-3.956224-9.685606-7.909478-19.371213-11.84727-29.054771 94.426419 72.58921 188.871168 145.194906 283.312947 217.800499 19.222528 14.779597 14.782259 47.586816-13.00265 47.956173-119.110554 1.62601-238.202675 3.250995-357.295821 4.860723C594.746368 528.15319 594.798592 476.677734 627.977626 476.225434" horiz-adv-x="1024" />
<glyph glyph-name="chexingluntan" unicode="&#59100;" d="M1320.606897 546.427586C1313.544828 564.082759 1292.358621 578.206897 1271.172414 578.206897l-176.551724 0L1094.62069 684.137931c0 21.186207-14.124138 35.310345-35.310345 35.310345L529.655172 719.448276c-21.186207 0-35.310345-14.124138-35.310345-35.310345l0 0c0-21.186207 14.124138-35.310345 35.310345-35.310345l494.344828 0 0-600.275862L632.055172 48.551724c-14.124138 60.027586-70.62069 105.931034-137.710345 105.931034s-120.055172-45.903448-137.710345-105.931034L282.482759 48.551724l0 317.793103c0 21.186207-14.124138 35.310345-35.310345 35.310345l0 0c-21.186207 0-35.310345-14.124138-35.310345-35.310345l0-353.103448c0-21.186207 14.124138-35.310345 35.310345-35.310345l109.462069 0c14.124138-60.027586 70.62069-105.931034 137.710345-105.931034s120.055172 45.903448 137.710345 105.931034l399.006897 0c14.124138-60.027586 70.62069-105.931034 137.710345-105.931034s120.055172 45.903448 137.710345 105.931034L1377.103448-22.068966c21.186207 0 35.310345 14.124138 35.310345 35.310345l0 229.517241C1412.413793 331.034483 1320.606897 546.427586 1320.606897 546.427586zM494.344828-57.37931c-38.841379 0-70.62069 31.77931-70.62069 70.62069s31.77931 70.62069 70.62069 70.62069 70.62069-31.77931 70.62069-70.62069S533.186207-57.37931 494.344828-57.37931zM1165.241379-57.37931c-38.841379 0-70.62069 31.77931-70.62069 70.62069s31.77931 70.62069 70.62069 70.62069 70.62069-31.77931 70.62069-70.62069S1204.082759-57.37931 1165.241379-57.37931zM1341.793103 48.551724l-38.841379 0c-14.124138 60.027586-70.62069 105.931034-137.710345 105.931034-24.717241 0-49.434483-7.062069-70.62069-17.655172L1094.62069 507.586207l158.896552 0c0 0 24.717241-17.655172 28.248276-35.310345l-63.558621 0c-21.186207 0-35.310345-14.124138-35.310345-35.310345l0-141.241379c0-21.186207 14.124138-35.310345 35.310345-35.310345L1341.793103 260.413793C1341.793103 260.413793 1341.793103 48.551724 1341.793103 48.551724zM0 860.689655c0 21.186207 17.655172 35.310345 35.310345 35.310345l353.103448 0c21.186207 0 35.310345-14.124138 35.310345-35.310345 0-21.186207-17.655172-35.310345-35.310345-35.310345L35.310345 825.37931C14.124138 825.37931 0 839.503448 0 860.689655zM105.931034 684.137931c0 21.186207 14.124138 35.310345 35.310345 35.310345l247.172414 0c21.186207 0 35.310345-14.124138 35.310345-35.310345 0-21.186207-14.124138-35.310345-35.310345-35.310345L141.241379 648.827586C120.055172 648.827586 105.931034 662.951724 105.931034 684.137931zM211.862069 507.586207c0 21.186207 17.655172 35.310345 35.310345 35.310345l141.241379 0c17.655172 0 35.310345-14.124138 35.310345-35.310345 0-21.186207-17.655172-35.310345-35.310345-35.310345L247.172414 472.275862C225.986207 472.275862 211.862069 486.4 211.862069 507.586207z" horiz-adv-x="1413" />
</font>
</defs></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
<title>Group 21</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
<g id="Group-21" transform="translate(77.000000, 73.000000)">
<g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
</g>
</g>
<g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
</g>
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
</g>
<g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
</g>
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
</g>
<g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
<g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
</g>
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
class Constants {
ORDER_STATUS_CREATED = 0; //已创建
ORDER_STATUS_USER_PAY = 1; //待用户支付
ORDER_STATUS_MERCHANT_PAY = 2; //待商户支付
ORDER_STATUS_MERCHANT_VERIFY = 3; //待商户审核
ORDER_STATUS_VERIFY = 4; //待审核
ORDER_STATUS_DELIVERY = 5; //待发货
ORDER_STATUS_SENT = 6; //待收货
ORDER_STATUS_COMPLELTED = 9; //已完成
ORDER_STATUS_FAILED = 10; //已失败
ORDER_STATUS_REFUND = 20; //已退货
ORDER_STATUS_EXCEPTION = 80; //异常
ORDER_STATUS_CLOSED = 99; //已关闭
ORDER_STATUS_MAP = {
0: '已创建',
1: '待用户支付',
2: '待商户支付',
3: '待商户审核',
4: '待审核',
5: '待发货',
6: '待收货',
9: '已完成',
10: '已失败',
20: '已退货',
80: '异常',
99: '已关闭'
};
GOODS_TYPE_REALITY = 1;
GOODS_TYPE_RECHARGE = 2;
GOODS_TYPE_VIRTUAL = 3;
GOODS_TYPE_MAP = {
1: '实物',
2: '直充',
3: '卡券'
};
PAY_TYPE_WEIXIN = 'weixin';
PAY_TYPE_ALIPAY = 'alipay';
PAY_TYPE_IAPPPAY = 'iapppay';
PAY_TYPE_MAP = {
'weixin': '微信',
'alipay': '支付宝',
'iapppay': '爱贝'
};
GOODS_EXAMINE_NOMARL = 0; //待审核
GOODS_EXAMINE_PASSED = 1; //审核通过
GOODS_EXAMINE_REJECT = 2; //待审拒绝
GOODS_EXAMINE_MAP = {
0: '待审核',
1: '通过',
2: '拒绝'
};
AFTER_SALE_NOMARL = 0; //待受理
AFTER_SALE_SEND = 1; //待用户寄回
AFTER_SALE_PASSED = 2; //待商户收货
AFTER_SALE_COMPLETE = 3; //完成售后
AFTER_SALE_REFUSE = 4; //拒绝受理
AFTER_SALE_CLOSE = 5; //售后取消
AFTER_SALE_STATUS_MAP = {
0: "待受理",
1: "待用户寄回",
2: "待商户收货",
3: "完成售后",
4: "拒绝受理",
5: "售后关闭"
};
CUSTOM_GOODS_NO = false;
CUSTOM_GOODS_YES = true;
CUSTOM_GOODS_MAP = {
false: "否",
true: "是"
};
AFTER_TYPE_EXCHANGE_GOODS = 0; // 仅换货
AFTER_TYPE_REFUND_GOODS = 1; // 退款退货
AFTER_TYPE_MAP = {
0: "换货",
1: "退款退货"
};
EXPRESS_STATUS_ERROR = -1; // 单号或快递公司代码错误
EXPRESS_STATUS_NO_DARA = 0; // 暂无轨迹
EXPRESS_STATUS_WAY = 2; // 在途中
EXPRESS_STATUS_RECEIVED = 3; // 签收
EXPRESS_STATUS_PROBLEM = 4; // 问题件
EXPRESS_STATUS_MAP = {
"-1": "单号或快递公司代码错误",
0: "暂无轨迹",
2: "在途中",
3: "已签收",
4: "问题件"
};
PLATFORM_ALARM_VALUE_MAP = {
"huyi_alarm_value": "互亿无线_报警值",
"juhe_alarm_value": "聚合数据_报警值",
"ejiaofei_alarm_value": "一脚飞_报警值",
"baishitong_alarm_value": "百事通_报警值",
"reality_stock_notice": "实物库存_报警值",
"coupon_stock_notice": "卡劵库存_报警值",
"alarm_notice_Identification": "预警_群组标识",
"goods_edit_Identification": "商品修改_群组标识"
};
REFUND_STATUS_MAP = {
0: "否",
1: "是"
};
}
const c = new Constants();
export default c;
\ No newline at end of file
import globalConfig from '../config';
/**
* 日志工具类 <br/>
*
* <p>
* 工程变大之后, 日志就很重要了, 总不能一直console.log.
* 本来想看看有没有现成的工具, 找到一个log4js的库, 但只能用在node环境下, 浏览器环境下似乎没什么好用的.
* 作为一个用惯了slf4j的人, 干脆自己写个吧, 练练手.
* 目前有以下功能:
* <ul>
* <li>支持常用的日志级别: debug/info/warn/error, 说实话其他的级别极少用到</li>
* <li>支持变量替换: 类似slf4j中的logger.info("a={}",a)这种, 其实console本身已经支持的</li>
* <li>根据日志级别设置样式: debug是黑色, info是默认, warn是黄色, error是红色, 看起来清晰很多</li>
* <li>定义每个logger的名字: 也算是常规功能了吧</li>
* </ul>
* 我是尽量按着slf4j的习惯来设计的, 目前还比较简单.
* 不支持pattern/appender之类的, 但对于二手前端来说, 也算够用了.
*
* 关于变量替换, 参考: https://developers.google.com/web/tools/chrome-devtools/console/console-write#_8
* </p>
*/
class Logger {
// 定义一些预设的日志级别
// 目前只有4种级别
static LOG_LEVEL_DEBUG = 1;
static LOG_LEVEL_INFO = 2;
static LOG_LEVEL_WARN = 3;
static LOG_LEVEL_ERROR = 4;
/*暂存所有logger*/
static loggerMap = new Map();
// 是否为某些logger单独指定了日志级别?
static debugLoggers = new Set();
static infoLoggers = new Set();
static warnLoggers = new Set();
static errorLoggers = new Set();
/*默认的logger*/
static defaultLogger = new Logger(); // 注意这一行代码的位置, 必须在所有Map/Set声明完毕之后
/**
* 获取一个Logger实例
*
* @param name
* @returns {*}
*/
static getLogger(name) {
if (name && name !== '') {
// 缓存
if (Logger.loggerMap.has(name)) {
return Logger.loggerMap.get(name);
}
const logger = new Logger(name);
Logger.loggerMap.set(name, logger);
return logger;
} else {
return Logger.defaultLogger;
}
}
constructor(name) {
this.name = name; // logger的名字
// 是否单独设置了这个logger的日志级别?
if (Logger.debugLoggers.has(name)) {
this.logLevel = Logger.LOG_LEVEL_DEBUG;
return;
}
if (Logger.infoLoggers.has(name)) {
this.logLevel = Logger.LOG_LEVEL_INFO;
return;
}
if (Logger.warnLoggers.has(name)) {
this.logLevel = Logger.LOG_LEVEL_WARN;
return;
}
if (Logger.errorLoggers.has(name)) {
this.logLevel = Logger.LOG_LEVEL_ERROR;
return;
}
// 如果没有单独设置, 就使用root logger level
const configLogLevel = globalConfig.log.level;
if (configLogLevel === 'debug') {
this.logLevel = Logger.LOG_LEVEL_DEBUG;
} else if (configLogLevel === 'info') {
this.logLevel = Logger.LOG_LEVEL_INFO;
} else if (configLogLevel === 'warn') {
this.logLevel = Logger.LOG_LEVEL_WARN;
} else if (configLogLevel === 'error') {
this.logLevel = Logger.LOG_LEVEL_ERROR;
} else {
// 默认都是info级别
this.error('unsupported logLevel: %s, use INFO instead', configLogLevel);
this.logLevel = Logger.LOG_LEVEL_INFO;
}
}
/**
* 设置日志级别, 只有4种级别可选
*
* @param newLogLevel 1~4之间的一个数字
*/
setLogLevel(newLogLevel) {
if (isNaN(newLogLevel)) {
this.error('setLogLevel error, not a number: %s', newLogLevel);
}
if (newLogLevel < 1 || newLogLevel > 4) {
this.error('setLogLevel error, input = %s, must between 1 and 4', newLogLevel);
}
this.logLevel = newLogLevel;
}
/**
* 打印info日志
*
* @param pattern 日志格式, 支持%d/%s等占位符
* @param args 可变参数, 用于替换pattern中的占位符
*/
info(pattern, ...args) {
// 先判断日志级别
if (this.logLevel > Logger.LOG_LEVEL_INFO)
return;
if (this.name)
args.unshift(`${this.name}: ${pattern}`);
else
args.unshift(pattern);
console.log.apply(console, args);
}
/**
* 打印error日志
*
* @param pattern
* @param args
*/
error(pattern, ...args) {
if (this.logLevel > Logger.LOG_LEVEL_ERROR)
return;
args.unshift('background: red; color: #bada55;');
if (this.name)
args.unshift(`%c${this.name}: ${pattern}`);
else
args.unshift(`%c${pattern}`);
console.error.apply(console, args);
}
/**
* 打印debug日志
*
* @param pattern
* @param args
*/
debug(pattern, ...args) {
if (this.logLevel > Logger.LOG_LEVEL_DEBUG)
return;
args.unshift('background: black; color: #bada55;');
if (this.name)
args.unshift(`%c${this.name}: ${pattern}`);
else
args.unshift(`%c${pattern}`);
console.debug.apply(console, args);
}
/**
* 打印warn日志
*
* @param pattern
* @param args
*/
warn(pattern, ...args) {
if (this.logLevel > Logger.LOG_LEVEL_WARN)
return;
args.unshift('background: yellow; color: black;');
if (this.name)
args.unshift(`%c${this.name}: ${pattern}`);
else
args.unshift(`%c${pattern}`);
console.warn.apply(console, args);
}
}
// 初始化Logger类中的一些static变量, 类似java中的static代码块
['debug', 'info', 'warn', 'error'].forEach((level) => {
if (globalConfig.log[level]) {
for (const logger of globalConfig.log[level]) {
Logger[`${level}Loggers`].add(logger);
}
}
});
export default Logger;
import Logger from './Logger';
import {ACTION_KEY} from '../components/DBTable/InnerTableRenderUtils';
const logger = new Logger('mockAjax');
const result = { // 暂存mock的ajax返回, 总共有5个字段
success: true,
code: 0,
message: 'just a mock ;) ',
total: 10000,
data: {},
};
// 模拟统一的延迟, 返回promise对象
const mockPromise = (callback) => {
return new Promise(resolve => {
setTimeout(callback, 2000, resolve);
});
};
// 根据某个表的dataSchema创造mock数据
const mockResult = (tableName, queryObj) => {
logger.debug('begin to mock data for table %s', tableName);
// 尝试加载schema文件
let schema;
try {
schema = require(`../schema/${tableName}.dataSchema.js`);
} catch (e) {
logger.error('can not find dataSchema file for table %s', tableName);
// 设置返回结果为失败
result.success = false;
result.code = 200;
result.message = `can not find dataSchema file for table ${tableName}`;
return;
}
// 一般来说, 传入的查询条件都是肯定会有page/pageSize的, 以防万一
if (!queryObj.page) {
queryObj.page = 1;
}
if (!queryObj.pageSize) {
queryObj.pageSize = 50;
}
const tmp = [];
for (let i = 0; i < queryObj.pageSize; i++) {
const record = {};
// 为了让mock的数据有些区别, 把page算进去
schema.forEach((column) => {
// 对于自定义操作列, 无需mock数据
if (column.key === ACTION_KEY) {
return;
}
// 生成mock数据还是挺麻烦的, 要判断showType和dataType
switch (column.showType) {
case 'select':
record[column.key] = mockOption(column);
break;
case 'radio':
record[column.key] = mockOption(column);
break;
case 'checkbox':
record[column.key] = mockOptionArray(column);
break;
case 'multiSelect':
record[column.key] = mockOptionArray(column);
break;
case 'textarea':
record[column.key] = `mock page=${queryObj.page} ${i}`;
break;
case 'image':
record[column.key] = mockImage(column);
break;
case 'file':
record[column.key] = mockFile(column);
break;
case 'cascader':
record[column.key] = mockCascader(column);
break;
default:
switch (column.dataType) {
case 'int':
record[column.key] = 1000 * queryObj.page + i;
break;
case 'float':
record[column.key] = parseFloat(new Number(2.0 * queryObj.page + i * 0.1).toFixed(2)); // toFixed返回的是个string
break;
case 'varchar':
record[column.key] = `mock page=${queryObj.page} ${i}`;
break;
case 'datetime':
record[column.key] = new Date().plusDays(i).format('yyyy-MM-dd HH:mm:ss');
break;
default:
logger.error('unsupported dataType %s', column.dataType);
}
}
});
tmp.push(record);
}
result.success = true;
result.data = tmp;
};
// 模拟radio/select的数据
const mockOption = (field) => {
const rand = Math.floor(Math.random() * field.options.length);
return field.options[rand].key;
};
// 模拟checkbox/multiSelect的数据
const mockOptionArray = (field) => {
const rand = Math.floor(Math.random() * field.options.length);
const mockResult = [];
for (let i = 0; i <= rand; i++) {
mockResult.push(field.options[i].key);
}
return mockResult;
};
// 测试用的图片, 生成数据时会随机从这里面挑选
const testAvatarArray = [
'http://jxy.me/about/avatar.jpg',
'http://imgtu.5011.net/uploads/content/20170207/4051451486453572.jpg',
'http://dynamic-image.yesky.com/600x-/uploadImages/upload/20140912/upload/201409/322l3se203jjpg.jpg',
];
const testImageArray = [
'http://img.51ztzj.com/upload/image/20140506/dn201405074019_670x419.jpg',
'http://img.51ztzj.com/upload/image/20170311/2017031104_670x419.jpg',
'http://img.51ztzj.com//upload/image/20170311/2017031107_670x419.jpg',
'http://img.51ztzj.com//upload/image/20130218/20130218011_670x419.jpg',
'http://img.51ztzj.com//upload/image/20130218/2013021802_670x419.jpg'
];
// 模拟图片数据
const mockImage = (field) => {
// 返回的是array还是string?
if (field.max > 1) {
const mockResult = [];
const rand = Math.floor(Math.random() * field.max);
for (let i = 0; i <= rand; i++) {
mockResult.push(testImageArray[Math.floor(Math.random() * testImageArray.length)]);
}
return mockResult;
} else {
return testAvatarArray[Math.floor(Math.random() * testAvatarArray.length)];
}
};
// 三驾马车啊, 虽然是十多年前的...
const testFileArray = [
'https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/gfs-sosp2003.pdf',
'https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/mapreduce-osdi04.pdf',
'http://xpgc.vicp.net/course/svt/TechDoc/storagepaper/bigtable-osdi06.pdf',
];
// 模拟文件
const mockFile = (field) => {
// 返回的是array还是string?
if (field.max > 1) {
const mockResult = [];
const rand = Math.floor(Math.random() * field.max);
for (let i = 0; i <= rand; i++) {
mockResult.push(testFileArray[Math.floor(Math.random() * testFileArray.length)]);
}
return mockResult;
} else {
return testFileArray[Math.floor(Math.random() * testFileArray.length)];
}
};
// 模拟级联选择的数据
const mockCascader = (field) => {
const mockResult = [];
const tmp = options => {
const rand = Math.floor(Math.random() * options.length);
mockResult.push(options[rand].value);
if (options[rand].children) {
tmp(options[rand].children);
}
};
tmp(field.options);
return mockResult;
};
/**
* 模拟ajax请求用于调试, 一般而言只需mock业务相关方法
*/
class MockAjax {
tableCache = new Map();
getCurrentUser() {
return mockPromise(resolve => {
result.success = true;
result.data = 'guest';
resolve(result);
});
}
login(username, password) {
return mockPromise(resolve => {
if (username === 'guest' && password === 'guest') {
result.success = true;
result.data = 'guest';
resolve(result);
} else {
result.success = false;
result.code = 100;
result.message = 'invalid username or password';
resolve(result);
}
});
}
CRUD(tableName) {
if (this.tableCache.has(tableName)) {
return this.tableCache.get(tableName);
}
const util = new MockCRUDUtil(tableName);
this.tableCache.set(tableName, util);
return util;
}
}
class MockCRUDUtil {
constructor(tableName) {
this.tableName = tableName;
}
select(queryObj) {
return mockPromise(resolve => {
mockResult(this.tableName, queryObj);
resolve(result);
});
}
insert(dataObj) {
return mockPromise(resolve => {
mockResult(this.tableName, {page: Math.floor(Math.random() * 10000), pageSize: 1}); // 为了生成一个主键, 反正是测试用的
const tmpObj = result.data[0];
Object.assign(tmpObj, dataObj);
result.success = true;
result.data = tmpObj;
resolve(result);
});
}
update(keys = [], dataObj) {
return mockPromise(resolve => {
result.success = true;
result.data = keys.length;
resolve(result);
});
}
delete(keys = []) {
return mockPromise(resolve => {
result.success = true;
result.data = keys.length;
resolve(result);
});
}
getRemoteSchema() {
// 这个counter用于测试ignoreSchemaCache选项, 每次请求得到的服务端schema都是不同的
if (!this.counter) {
this.counter = 0;
}
this.counter++;
return mockPromise(resolve => {
if (this.tableName === 'testAction') {
resolve({
success: true,
data: {
querySchema: [
{
key: 'keyFromServer',
title: `服务端key ${this.counter}`,
},
// 理论上来说服务端可以返回任意schema, 覆盖本地js的配置
{
key: 'type',
options: [{key: '1', value: '来自服务端1'}, {key: '2', value: '来自服务端2'}, {key: '3', value: '来自服务端3'}],
defaultValue: '2',
},
],
dataSchema: [
// 服务端甚至可以修改showType
{
key: 'name',
title: `选择姓名 ${this.counter}`,
showType: 'radio',
options: [{key: 'a', value: 'AA'}, {key: 'b', value: 'BB'}, {key: 'c', value: 'CC'}],
},
],
},
});
} else {
resolve({success: true, data: {}});
}
});
}
}
export default MockAjax;
import Logger from './Logger';
import superagent from 'superagent';
import globalConfig from '../config';
import ajax from "./ajax";
const logger = new Logger('Ajax');
/**
* 封装所有ajax逻辑, 为了配合async/await, 所有ajax请求都要返回promise对象
*/
class Ajax {
// Ajax工具类提供的方法可以分为2种:
// 1. 基础的get/post方法, 这些是通用的
// 2. 在get/post基础上包装的业务方法, 比如getCurrentUser, 这些方法是有业务含义的
// 作为缓存
tableCache = new Map();
/**
* 内部方法, 在superagent api的基础上, 包装一些全局的设置
*
* @param method 要请求的方法
* @param url 要请求的url
* @param params url上的额外参数
* @param data 要发送的数据
* @param headers 额外设置的http header
* @returns {Promise}
*/
requestWrapper(method, url, {params, data, headers} = {}) {
logger.debug('method=%s, url=%s, params=%o, data=%o, headers=%o', method, url, params, data, headers);
return new Promise((resolve, reject) => {
const tmp = superagent(method, url);
// 是否是跨域请求
if (globalConfig.isCrossDomain()) {
tmp.withCredentials();
}
// 设置全局的超时时间
if (globalConfig.api.timeout && !isNaN(globalConfig.api.timeout)) {
tmp.timeout(globalConfig.api.timeout);
}
// 默认的Content-Type和Accept
if (!data || (data && !(data instanceof FormData))) {
tmp.set('Content-Type', 'application/x-www-form-urlencoded');
}
tmp.set('Accept', 'application/json');
// 如果有自定义的header
if (headers) {
tmp.set(headers);
}
// url中是否有附加的参数?
if (params) {
tmp.query(params);
}
// body中发送的数据
if (data) {
tmp.send(data);
}
// 包装成promise
tmp.end((err, res) => {
logger.debug('err=%o, res=%o', err, res);
// 我本来在想, 要不要在这里把错误包装下, 即使请求失败也调用resolve, 这样上层就不用区分"网络请求成功但查询数据失败"和"网络失败"两种情况了
// 但后来觉得这个ajax方法是很底层的, 在这里包装不合适, 应该让上层业务去包装
if (res && res.body) {
resolve(res.body);
} else {
reject(err || res);
}
});
});
}
// 基础的get/post方法
get(url, opts = {}) {
return this.requestWrapper('GET', url, {...opts});
}
post(url, data, opts = {}) {
return this.requestWrapper('POST', url, {...opts, data});
}
getAsyncData(url) {
return ajax.get(`${globalConfig.getAPIPath()}` + url);
}
// 业务方法
/**
* 获取当前登录的用户
*
* @returns {*}
*/
getCurrentUser() {
return this.get(`${globalConfig.getAPIPath()}${globalConfig.login.getCurrentUser}`);
}
/**
* 用户登录
*
* @param username
* @param password
*/
login(username, password) {
const headers = {'Content-Type': 'application/x-www-form-urlencoded'};
return this.post(`${globalConfig.getAPIPath()}${globalConfig.login.validate}`, {username, password}, {headers});
}
/**
* 封装CRUD相关操作
*
* @param tableName 要操作的表名
* @returns {*}
*/
CRUD(tableName) {
if (this.tableCache.has(tableName)) {
return this.tableCache.get(tableName);
}
const util = new CRUDUtil(tableName);
util.ajax = this;
this.tableCache.set(tableName, util);
return util;
}
}
/**
* 封装CRUD相关操作, 有点内部类的感觉
*/
class CRUDUtil {
constructor(tableName) {
this.tableName = tableName;
}
/**
* 查询某个表
*
* @param queryObj 查询条件封装为一个对象
* @returns {*}
*/
select(queryObj) {
return this.ajax.post(`${globalConfig.getAPIPath()}${this.tableName}/select`, queryObj);
}
/**
* 给某个表新增一条数据
*
* @param dataObj 要新增的数据
* @returns {*}
*/
insert(dataObj) {
return this.ajax.post(`${globalConfig.getAPIPath()}${this.tableName}/insert`, dataObj);
}
/**
* 更新某个表的数据, 可以批量, 也可以单条
*
* @param keys 要更新的记录的主键
* @param dataObj 要更新哪些字段
* @returns {*}
*/
update(keys = [], dataObj) {
const tmp = keys.join(',');
return this.ajax.post(`${globalConfig.getAPIPath()}${this.tableName}/update`, dataObj, {params: {keys: tmp}});
}
/**
* 删除某个表的数据, 可以批量, 也可以单条
*
* @param keys 要删除的记录的主键
* @returns {*}
*/
delete(keys = []) {
const tmp = keys.join(',');
return this.ajax.get(`${globalConfig.getAPIPath()}${this.tableName}/delete`, {params: {keys: tmp}});
}
/**
* 从服务端获取某个表的schema, 会merge到本地的schema中
*
* @returns {*}
*/
getRemoteSchema() {
return this.ajax.get(`${globalConfig.getAPIPath()}${this.tableName}/schema`);
}
}
export default Ajax;
import globalConfig from '../config';
import MockAjax from './MockAjax';
import RealAjax from './RealAjax';
const mockAjax = new MockAjax();
const realAjax = new RealAjax();
const tmp = globalConfig.debug === true ? mockAjax : realAjax;
export default tmp;
// 按我之前的写法有些问题, 可能导致import的时候得到一个空对象, 猜测是循环引用的问题
// http://stackoverflow.com/questions/30378226/circular-imports-with-webpack-returning-empty-object
// // 这个写法总觉得有点奇怪...不知道webpack会不会优化掉
// if (globalConfig.debug === true) {
// module.exports = mockAjax;
// } else {
// module.exports = realAjax;
// }
This source diff could not be displayed because it is too large. You can view the blob instead.
// 一些辅助用的工具方法
// 很多都是gross hack, 属于历史遗留问题
// antd从2.x开始引入了moment: http://momentjs.com/docs/
// 这是个好东西, 处理日期方便多了, 简直就是javascript界的joda-time
// 这些prototype的hack基本用不到了
// 不过它format时的pattern和常见的不太一样, 比如要大写的YYYY才代表年份
/** 对Date的扩展,将 Date 转化为指定格式的String * 月(M)、日(d)、12小时(h)、24小时(H)、分(m)、秒(s)、周(E)、季度(q)
* 可以用 1-2 个占位符 * 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)
* eg:
* (new Date()).pattern("yyyy-MM-dd hh:mm:ss.S")==> 2006-07-02 08:09:04.423
* (new Date()).pattern("yyyy-MM-dd E HH:mm:ss") ==> 2009-03-10 二 20:09:04
* (new Date()).pattern("yyyy-MM-dd EE hh:mm:ss") ==> 2009-03-10 周二 08:09:04
* (new Date()).pattern("yyyy-MM-dd EEE hh:mm:ss") ==> 2009-03-10 星期二 08:09:04
* (new Date()).pattern("yyyy-M-d h:m:s.S") ==> 2006-7-2 8:9:4.18
*/
Date.prototype.format = function (fmt) {
var o = {
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"h+": this.getHours() % 12 == 0 ? 12 : this.getHours() % 12, //小时
"H+": this.getHours(), //小时
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
"S": this.getMilliseconds() //毫秒
};
var week = {
"0": "/u65e5",
"1": "/u4e00",
"2": "/u4e8c",
"3": "/u4e09",
"4": "/u56db",
"5": "/u4e94",
"6": "/u516d"
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
}
if (/(E+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, ((RegExp.$1.length > 1) ? (RegExp.$1.length > 2 ? "/u661f/u671f" : "/u5468") : "") + week[this.getDay() + ""]);
}
for (var k in o) {
if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
}
}
return fmt;
};
/**
* 在当前日期的基础上再增加几天
*
* @param num 要增加的天数
*/
Date.prototype.plusDays = function (num) {
var tmp = new Date();
tmp.setDate(this.getDate() + num);
return tmp;
};
// 为了克服js的一些坑...
const Utils = {
isString(s) {
return typeof(s) === 'string' || s instanceof String;
},
// 获取url中的所有参数
getAllQueryParams() {
let str = window.location.href;
if (!str) {
return {};
}
let num = str.indexOf('?');
str = str.substr(num + 1); //取得所有参数
const res = {};
let name;
let value;
const arr = str.split('&'); //各个参数放到数组里
for (let i = 0; i < arr.length; i++) {
num = arr[i].indexOf('=');
if (num > 0) {
name = arr[i].substring(0, num).trim();
value = arr[i].substr(num + 1).trim();
res[name] = value;
}
}
return res;
},
formatMoney(value, useUnit) {
if (value !== +value) {
return value;
}
let r = parseInt(value).toLocaleString() + value.toFixed(2).replace(/^-?\d+/, '');
if (useUnit || useUnit === undefined) {
return r + ' 元';
} else {
return r;
}
},
map2kvArr(value,) {
let a = [];
for (let i in value) {
a.push({
key: i,
value: value[i]
});
}
return a;
},
/**
* 通过属性值查找对象,存在则返回索引,不存在返回-1
* @param arrayToSearch
* @param attr
* @param val
* @returns {number}
*/
findElem(arrayToSearch, attr, val) {
for (let i = 0; i < arrayToSearch.length; i++) {
if (arrayToSearch[i][attr] === val) {
return i;
}
}
return -1;
},
/**
* 通过key对数组对象去重,返回新的数组
* @param songs
* @param key
* @returns {Array}
*/
uniqueByKey(songs, key) {
let result = {};
let finalResult = [];
for (let i = 0; i < songs.length; i++) {
result[songs[i][key]] = songs[i];
}
for (let item in result) {
finalResult.push(result[item]);
}
return finalResult;
},
generateId(length) {
return Number(Math.random().toString().substr(3, length) + Date.now()).toString(36);
}
}
;
export default Utils;
const webpack = require('webpack');
const globalConfig = require('./src/config.js');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
devtool: 'eval-source-map',
entry: [
'webpack-dev-server/client?http://0.0.0.0:8080',
'webpack/hot/dev-server',
'babel-polyfill', // 可以使用完整的ES6特性, 大概增加100KB
// 编译的入口
'./src/index.js',
],
output: {
// 输出的目录和文件名
path: path.resolve(__dirname, "dist"),
filename: 'bundle.js'
},
devServer: {
inline: false,
proxy: {
'!(/|/#/|**/*.js|**/*.css|**/*.ttf|**/*.jpg|**/*.png|**/*.gif|**/*.ico)': {
target: globalConfig.api.proxyHost,
changeOrigin: true,
secure: false
}
}
},
resolve: {
modules: [
"node_modules",
path.resolve(__dirname, "app")
],
extensions: ['*', '.js', '.jsx'],
alias: {
antdcss: 'antd/dist/antd.min.css'
},
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
options: {
plugins: [
['import', [{libraryName: 'antd', style: true}]],
],
cacheDirectory: true,
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', "less-loader"]
},
{
test: /\.(svg|eot|ttf|woff)$/,
use: ['file-loader']
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10000
}
}
]
}
]
},
plugins: [
// 生成文件时加上注释
new webpack.BannerPlugin('This is react admin'),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development'),
__DEV__: JSON.stringify(JSON.parse(process.env.NODE_ENV === 'production' ? 'false' : 'true')), // magic globals, 用于打印一些调试的日志, webpack -p时会删除
}),
// 生成html文件
new HtmlWebpackPlugin({
template: 'index.html',
title: globalConfig.name,
// favicon
favIcon: globalConfig.favicon,
// 这个属性也是我自己定义的, dev模式下要加载一些额外的js
devMode: true,
}),
],
};
const webpack = require('webpack');
const path = require('path');
const globalConfig = require('./src/config.js');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const CompressionPlugin = require("compression-webpack-plugin");
const HtmlWebpackPlugin = require('html-webpack-plugin');
// bundle split, 尝试把这些比较独立的库单独放在一个js文件中
// 注意只有真正"公用"的库才能放这里, 否则会有各种奇怪的问题
// 而且必须在bundle.js之前加载
const vendorLibs = ['react', 'react-router',
'redux', 'react-redux', 'redux-logger', 'redux-thunk', 'redux-promise',
'superagent',
];
module.exports = {
devtool: 'cheap-module-source-map',
entry: [
'babel-polyfill',
'./src/index.js',
],
output: {
path: path.join(__dirname, globalConfig.outputPath + '/dist'), // 用来存放打包后文件的输出目录
publicPath: '/dist/', // 指定资源文件引用的目录
filename: 'bundle.min.js'
},
resolve: {
modules: [
"node_modules",
path.resolve(__dirname, "app")
],
extensions: ['*', '.js', '.jsx']
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
options: {
plugins: [
['import', [{libraryName: 'antd', style: true}]],
],
cacheDirectory: true,
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', "less-loader"]
},
{
test: /\.(svg|eot|ttf|woff)$/,
use: ['file-loader']
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10000
}
}
]
}
]
},
plugins: [
// 代码压缩
new webpack.optimize.UglifyJsPlugin({
sourceMap: true,
minimize: true,
compress: {warnings: false},
output: {comments: false},
}),
new HtmlWebpackPlugin({
template: 'index.html',// 模板文件
filename: '../index.html',// 入口文件
title: globalConfig.name,
favIcon: globalConfig.favicon,
hash: true, // 引入js/css的时候加个hash, 防止cdn的缓存问题
minify: {removeComments: true, collapseWhitespace: true},
}),
// 抽离公共部分, 要了解CommonsChunkPlugin的原理, 首先要搞清楚chunk的概念
// CommonsChunkPlugin做的其实就是把公共模块抽出来, 可以单独生成一个新的文件, 也可以附加到已有的chunk上
// 同时还会加上webpack的runtime相关代码
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'vendor.min.js',
// 这个函数决定哪些模块会被放到vender.min.js中
minChunks: (module) => {
// 得到资源路径
var resource = module.resource;
if (!resource)
return false;
// 坑爹的webpack, for-of里不用能const, 会有bug
for (var libName of vendorLibs) {
if (resource.indexOf(path.resolve(__dirname, 'node_modules', libName)) >= 0)
return true;
}
return false;
},
}),
new webpack.optimize.AggressiveMergingPlugin(),
// 允许错误不打断程序
new webpack.NoEmitOnErrorsPlugin(),
// css单独抽出来
new ExtractTextPlugin('bundle.min.css', {allChunks: false}),
// 压缩成gzip格式
new CompressionPlugin({
asset: "[path].gz[query]",
algorithm: "gzip",
test: /\.js$|\.css$|\.html$/,
threshold: 10240,
minRatio: 0,
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
__DEV__: JSON.stringify(JSON.parse(process.env.NODE_ENV === 'production' ? 'false' : 'true')),
}),
],
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment