فهرست منبع

optimize code / user example

x1ongzhu 6 سال پیش
والد
کامیت
fc05c1bedd
35فایلهای تغییر یافته به همراه905 افزوده شده و 462 حذف شده
  1. 38 4
      pom.xml
  2. 3 0
      src/main/java/com/izouma/zhumj/annotations/Searchable.java
  3. 1 0
      src/main/java/com/izouma/zhumj/config/LocalDateTimeSerializerConfig.java
  4. 2 1
      src/main/java/com/izouma/zhumj/config/WebMvcConfig.java
  5. 5 1
      src/main/java/com/izouma/zhumj/domain/BaseEntity.java
  6. 8 3
      src/main/java/com/izouma/zhumj/domain/Menu.java
  7. 8 2
      src/main/java/com/izouma/zhumj/domain/User.java
  8. 2 0
      src/main/java/com/izouma/zhumj/dto/PageQuery.java
  9. 7 0
      src/main/java/com/izouma/zhumj/repo/AuthorityRepo.java
  10. 3 1
      src/main/java/com/izouma/zhumj/repo/MenuRepo.java
  11. 2 1
      src/main/java/com/izouma/zhumj/security/JwtAuthorizationTokenFilter.java
  12. 1 1
      src/main/java/com/izouma/zhumj/security/JwtUserFactory.java
  13. 1 0
      src/main/java/com/izouma/zhumj/security/WebSecurityConfig.java
  14. 16 0
      src/main/java/com/izouma/zhumj/utils/NullAwareBeanUtilsBean.java
  15. 23 0
      src/main/java/com/izouma/zhumj/utils/ObjectUtils.java
  16. 32 0
      src/main/java/com/izouma/zhumj/utils/excel/LocalDateConverter.java
  17. 32 0
      src/main/java/com/izouma/zhumj/utils/excel/LocalDateTimeConverter.java
  18. 1 1
      src/main/java/com/izouma/zhumj/web/AuthenticationController.java
  19. 31 0
      src/main/java/com/izouma/zhumj/web/AuthorityController.java
  20. 14 6
      src/main/java/com/izouma/zhumj/web/MenuController.java
  21. 75 27
      src/main/java/com/izouma/zhumj/web/UserController.java
  22. 65 0
      src/main/resources/logback-spring.xml
  23. 1 1
      src/main/vue/public/index.html
  24. 103 105
      src/main/vue/src/components/CropUpload.vue
  25. 2 2
      src/main/vue/src/components/SysMenu.vue
  26. 84 0
      src/main/vue/src/mixins/pageableTable.js
  27. 0 34
      src/main/vue/src/mixins/pagination.js
  28. 6 2
      src/main/vue/src/plugins/http.js
  29. 1 1
      src/main/vue/src/styles/app.less
  30. 2 3
      src/main/vue/src/views/Admin.vue
  31. 122 22
      src/main/vue/src/views/Login.vue
  32. 61 16
      src/main/vue/src/views/Menus.vue
  33. 56 115
      src/main/vue/src/views/UserEdit.vue
  34. 80 113
      src/main/vue/src/views/UserList.vue
  35. 17 0
      src/test/java/com/izouma/zhumj/CommonTest.java

+ 38 - 4
pom.xml

@@ -17,6 +17,9 @@
     <properties>
         <java.version>1.8</java.version>
         <skipTests>true</skipTests>
+        <poi.verion>3.17</poi.verion>
+        <javawx.version>3.4.0</javawx.version>
+        <aliyun.oss.version>2.8.3</aliyun.oss.version>
     </properties>
 
     <build>
@@ -89,6 +92,13 @@
             <artifactId>commons-pool2</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>commons-beanutils</groupId>
+            <artifactId>commons-beanutils</artifactId>
+            <version>1.9.4</version>
+        </dependency>
+
+
         <dependency>
             <groupId>io.jsonwebtoken</groupId>
             <artifactId>jjwt</artifactId>
@@ -105,25 +115,25 @@
         <dependency>
             <groupId>com.github.binarywang</groupId>
             <artifactId>weixin-java-mp</artifactId>
-            <version>3.4.0</version>
+            <version>${javawx.version}</version>
         </dependency>
 
         <dependency>
             <groupId>com.github.binarywang</groupId>
             <artifactId>weixin-java-pay</artifactId>
-            <version>3.4.0</version>
+            <version>${javawx.version}</version>
         </dependency>
 
         <dependency>
             <groupId>com.github.binarywang</groupId>
             <artifactId>weixin-java-open</artifactId>
-            <version>3.4.0</version>
+            <version>${javawx.version}</version>
         </dependency>
 
         <dependency>
             <groupId>com.aliyun.oss</groupId>
             <artifactId>aliyun-sdk-oss</artifactId>
-            <version>2.8.3</version>
+            <version>${aliyun.oss.version}</version>
         </dependency>
 
         <dependency>
@@ -131,6 +141,30 @@
             <artifactId>http-request</artifactId>
             <version>6.0</version>
         </dependency>
+
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi</artifactId>
+            <version>${poi.verion}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml</artifactId>
+            <version>${poi.verion}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>cglib</groupId>
+            <artifactId>cglib</artifactId>
+            <version>3.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>2.0.4</version>
+        </dependency>
     </dependencies>
 
 </project>

+ 3 - 0
src/main/java/com/izouma/zhumj/annotations/Searchable.java

@@ -1,9 +1,12 @@
 package com.izouma.zhumj.annotations;
 
 import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
 @Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
 public @interface Searchable {
     boolean value() default true;
 }

+ 1 - 0
src/main/java/com/izouma/zhumj/config/LocalDateTimeSerializerConfig.java

@@ -1,5 +1,6 @@
 package com.izouma.zhumj.config;
 
+import com.alibaba.excel.EasyExcel;
 import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
 import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
 import org.springframework.beans.factory.annotation.Value;

+ 2 - 1
src/main/java/com/izouma/zhumj/config/WebMvcConfig.java

@@ -45,6 +45,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
         registry.addMapping("/**")
                 .allowedHeaders("*")
                 .allowCredentials(true)
-                .allowedMethods("HEAD", "GET", "PUT", "POST", "DELETE", "PATCH");
+                .allowedMethods("HEAD", "GET", "PUT", "POST", "DELETE", "PATCH")
+                .exposedHeaders("Content-Disposition");
     }
 }

+ 5 - 1
src/main/java/com/izouma/zhumj/domain/BaseEntity.java

@@ -1,6 +1,8 @@
 package com.izouma.zhumj.domain;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.NoArgsConstructor;
@@ -17,10 +19,12 @@ import java.time.LocalDateTime;
 @MappedSuperclass
 @Audited
 @EntityListeners(AuditingEntityListener.class)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
 public abstract class BaseEntity {
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
-    public Long id;
+    private Long id;
 
     @JsonIgnore
     @CreatedBy

+ 8 - 3
src/main/java/com/izouma/zhumj/domain/Menu.java

@@ -1,9 +1,14 @@
 package com.izouma.zhumj.domain;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonIgnoreType;
+import com.fasterxml.jackson.annotation.JsonInclude;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
+import org.hibernate.annotations.Cascade;
+import org.hibernate.annotations.Where;
 
 import javax.persistence.*;
 import java.io.Serializable;
@@ -11,9 +16,7 @@ import java.util.List;
 
 @Data
 @Entity
-@AllArgsConstructor
-@NoArgsConstructor
-@Builder
+@Where(clause = "active = 1")
 public class Menu extends BaseEntity implements Serializable {
     private String name;
 
@@ -29,6 +32,8 @@ public class Menu extends BaseEntity implements Serializable {
 
     private Boolean enabled;
 
+    private Boolean active;
+
     @OneToMany
     @JoinColumn(name = "parent", insertable = false, updatable = false)
     List<Menu> children;

+ 8 - 2
src/main/java/com/izouma/zhumj/domain/User.java

@@ -1,17 +1,22 @@
 package com.izouma.zhumj.domain;
 
+import com.alibaba.excel.annotation.ExcelIgnore;
 import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
 import com.izouma.zhumj.annotations.Searchable;
 import com.izouma.zhumj.config.Constants;
 import com.izouma.zhumj.security.Authority;
 import lombok.*;
 import org.hibernate.annotations.BatchSize;
+import org.hibernate.annotations.Where;
 
 import javax.persistence.*;
 import javax.validation.constraints.Pattern;
 import javax.validation.constraints.Size;
 import java.io.Serializable;
+import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 @Data
@@ -19,6 +24,7 @@ import java.util.Set;
 @AllArgsConstructor
 @NoArgsConstructor
 @Builder
+@Where(clause = "enabled = 1")
 public class User extends BaseEntity implements Serializable {
 
     @Pattern(regexp = Constants.USERNAME_REGEX)
@@ -38,12 +44,12 @@ public class User extends BaseEntity implements Serializable {
 
     private String avatar;
 
-    @JsonIgnore
     @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.DETACH})
     @JoinTable(
             name = "user_authority",
             joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
             inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "name")})
     @BatchSize(size = 20)
-    private Set<Authority> authorities = new HashSet<>();
+    @ExcelIgnore
+    private List<Authority> authorities = new ArrayList<>();
 }

+ 2 - 0
src/main/java/com/izouma/zhumj/dto/PageQuery.java

@@ -1,6 +1,7 @@
 package com.izouma.zhumj.dto;
 
 import lombok.Data;
+import org.springframework.data.domain.Sort;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -9,6 +10,7 @@ import java.util.Map;
 public class PageQuery {
     private int                 page  = 0;
     private int                 size  = 20;
+    private String              sort;
     private String              search;
     private Map<String, Object> query = new HashMap<>();
 }

+ 7 - 0
src/main/java/com/izouma/zhumj/repo/AuthorityRepo.java

@@ -0,0 +1,7 @@
+package com.izouma.zhumj.repo;
+
+import com.izouma.zhumj.security.Authority;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface AuthorityRepo extends JpaRepository<Authority, String> {
+}

+ 3 - 1
src/main/java/com/izouma/zhumj/repo/MenuRepo.java

@@ -1,6 +1,7 @@
 package com.izouma.zhumj.repo;
 
 import com.izouma.zhumj.domain.Menu;
+import org.hibernate.annotations.Where;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
 
@@ -9,7 +10,8 @@ import java.util.List;
 public interface MenuRepo extends JpaRepository<Menu, Long> {
     List<Menu> findByRootTrue();
 
-    List<Menu> findByRootTrueAndName(String name);
+    @Where(clause = "enabled = 1")
+    List<Menu> findByNameAndRootTrue(String name);
 
     @Query(nativeQuery = true, value = "SELECT max(sort + 1) FROM menu")
     int nextSort();

+ 2 - 1
src/main/java/com/izouma/zhumj/security/JwtAuthorizationTokenFilter.java

@@ -67,7 +67,8 @@ public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {
             try {
                 userDetails = userDetailsService.loadUserByUsername(username);
             } catch (Exception e) {
-                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
+                //response.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
+                chain.doFilter(request, response);
                 return;
             }
 

+ 1 - 1
src/main/java/com/izouma/zhumj/security/JwtUserFactory.java

@@ -17,7 +17,7 @@ public final class JwtUserFactory {
         return new JwtUser(user, mapToGrantedAuthorities(user.getAuthorities()));
     }
 
-    private static List<GrantedAuthority> mapToGrantedAuthorities(Set<Authority> authorities) {
+    private static List<GrantedAuthority> mapToGrantedAuthorities(List<Authority> authorities) {
         if (authorities != null) {
             return authorities.stream()
                               .map(authority -> new SimpleGrantedAuthority(authority.getName()))

+ 1 - 0
src/main/java/com/izouma/zhumj/security/WebSecurityConfig.java

@@ -65,6 +65,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
                     .antMatchers("/orderNotify/**").permitAll()
                     .antMatchers("/order/logistic").permitAll()
                     .antMatchers("/systemVariable/all").permitAll()
+                    .antMatchers("/**/excel").permitAll()
                     // all other requests need to be authenticated
                     .anyRequest().authenticated().and()
                     // make sure we use stateless session; session won't be used to

+ 16 - 0
src/main/java/com/izouma/zhumj/utils/NullAwareBeanUtilsBean.java

@@ -0,0 +1,16 @@
+package com.izouma.zhumj.utils;
+
+import org.apache.commons.beanutils.BeanUtilsBean;
+
+import java.lang.reflect.InvocationTargetException;
+
+public class NullAwareBeanUtilsBean extends BeanUtilsBean {
+
+    @Override
+    public void copyProperty(Object dest, String name, Object value)
+            throws IllegalAccessException, InvocationTargetException {
+        if (value == null) return;
+        super.copyProperty(dest, name, value);
+    }
+
+}

+ 23 - 0
src/main/java/com/izouma/zhumj/utils/ObjectUtils.java

@@ -0,0 +1,23 @@
+package com.izouma.zhumj.utils;
+
+
+import org.apache.commons.beanutils.BeanUtilsBean;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Objects;
+
+public class ObjectUtils {
+    public static void merge(Object dst, Object src) {
+        Objects.requireNonNull(src);
+        Objects.requireNonNull(dst);
+        if (!src.getClass().equals(dst.getClass())) {
+            throw new RuntimeException("cannot merge different class");
+        }
+        BeanUtilsBean notNull = new NullAwareBeanUtilsBean();
+        try {
+            notNull.copyProperties(dst, src);
+        } catch (IllegalAccessException | InvocationTargetException e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 32 - 0
src/main/java/com/izouma/zhumj/utils/excel/LocalDateConverter.java

@@ -0,0 +1,32 @@
+package com.izouma.zhumj.utils.excel;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.CellData;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+public class LocalDateConverter implements Converter<LocalDate> {
+    @Override
+    public Class supportJavaTypeKey() {
+        return LocalDate.class;
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        return CellDataTypeEnum.STRING;
+    }
+
+    @Override
+    public LocalDate convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
+        return LocalDate.parse(cellData.getStringValue(), DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+    }
+
+    @Override
+    public CellData convertToExcelData(LocalDate localDate, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
+        return new CellData(DateTimeFormatter.ofPattern("yyyy-MM-dd").format(localDate));
+    }
+}

+ 32 - 0
src/main/java/com/izouma/zhumj/utils/excel/LocalDateTimeConverter.java

@@ -0,0 +1,32 @@
+package com.izouma.zhumj.utils.excel;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.CellData;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+public class LocalDateTimeConverter implements Converter<LocalDateTime> {
+    @Override
+    public Class supportJavaTypeKey() {
+        return LocalDateTime.class;
+    }
+
+    @Override
+    public CellDataTypeEnum supportExcelTypeKey() {
+        return CellDataTypeEnum.STRING;
+    }
+
+    @Override
+    public LocalDateTime convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
+        return LocalDateTime.parse(cellData.getStringValue(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+    }
+
+    @Override
+    public CellData convertToExcelData(LocalDateTime localDateTime, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
+        return new CellData(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(localDateTime));
+    }
+}

+ 1 - 1
src/main/java/com/izouma/zhumj/web/AuthenticationController.java

@@ -35,7 +35,7 @@ public class AuthenticationController {
     private UserDetailsService    userDetailsService;
 
     @PostMapping("/login")
-    public String loginByUserPwd(String username, String password) {
+    public String loginByUserPwd(String username, String password, Integer expiration) {
         try {
             authenticate(username, password);
             final UserDetails userDetails = userDetailsService.loadUserByUsername(username);

+ 31 - 0
src/main/java/com/izouma/zhumj/web/AuthorityController.java

@@ -0,0 +1,31 @@
+package com.izouma.zhumj.web;
+
+import com.izouma.zhumj.repo.AuthorityRepo;
+import com.izouma.zhumj.security.Authority;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/authority")
+public class AuthorityController {
+    @Autowired
+    private AuthorityRepo authorityRepo;
+
+    @PreAuthorize("hasRole('ADMIN')")
+    @GetMapping("/all")
+    public List<Authority> all() {
+        return authorityRepo.findAll();
+    }
+
+    @PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/save")
+    public Authority save(Authority authority) {
+        return authorityRepo.save(authority);
+    }
+}

+ 14 - 6
src/main/java/com/izouma/zhumj/web/MenuController.java

@@ -1,7 +1,9 @@
 package com.izouma.zhumj.web;
 
 import com.izouma.zhumj.domain.Menu;
+import com.izouma.zhumj.exception.BusinessException;
 import com.izouma.zhumj.repo.MenuRepo;
+import org.hibernate.annotations.Where;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -9,6 +11,7 @@ import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.util.Comparator;
 import java.util.List;
 
 @RestController
@@ -19,13 +22,17 @@ public class MenuController {
 
     @GetMapping("/userMenu")
     public List<Menu> userMenu(String name) {
-        return menuRepo.findByRootTrueAndName(name);
+        List<Menu> menuList = menuRepo.findByNameAndRootTrue(name);
+        menuList.forEach(this::sortMenu);
+        return menuList;
     }
 
     @PreAuthorize("hasRole('ADMIN')")
     @GetMapping("/all")
     public List<Menu> all() {
-        return menuRepo.findByRootTrue();
+        List<Menu> menuList = menuRepo.findByRootTrue();
+        menuList.forEach(this::sortMenu);
+        return menuList;
     }
 
     @PreAuthorize("hasRole('ADMIN')")
@@ -38,9 +45,10 @@ public class MenuController {
         return menu;
     }
 
-    @PreAuthorize("hasRole('ADMIN')")
-    @PostMapping("/del")
-    public void del(Long id) {
-        menuRepo.deleteById(id);
+    private void sortMenu(Menu menu) {
+        if (menu.getChildren() != null) {
+            menu.getChildren().sort(Comparator.comparingInt(Menu::getSort));
+            menu.getChildren().forEach(this::sortMenu);
+        }
     }
 }

+ 75 - 27
src/main/java/com/izouma/zhumj/web/UserController.java

@@ -1,33 +1,36 @@
 package com.izouma.zhumj.web;
 
+import com.alibaba.excel.EasyExcel;
 import com.izouma.zhumj.annotations.Searchable;
 import com.izouma.zhumj.domain.User;
 import com.izouma.zhumj.dto.PageQuery;
-import com.izouma.zhumj.exception.AuthenticationException;
 import com.izouma.zhumj.exception.BusinessException;
 import com.izouma.zhumj.repo.UserRepo;
 import com.izouma.zhumj.security.Authority;
+import com.izouma.zhumj.utils.ObjectUtils;
 import com.izouma.zhumj.utils.SecurityUtils;
-import lombok.RequiredArgsConstructor;
-import org.apache.commons.beanutils.Converter;
-import org.apache.commons.beanutils.converters.StringConverter;
+import com.izouma.zhumj.utils.excel.LocalDateConverter;
+import com.izouma.zhumj.utils.excel.LocalDateTimeConverter;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageRequest;
-import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
 import org.springframework.data.jpa.domain.Specification;
-import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.web.bind.annotation.*;
 
 import javax.persistence.criteria.Predicate;
-import javax.validation.Valid;
-import javax.validation.constraints.Size;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Field;
-import java.util.*;
-import java.util.function.BiConsumer;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.IntFunction;
 
 @RestController
 @RequestMapping("/user")
@@ -36,8 +39,9 @@ public class UserController {
     private UserRepo userRepo;
 
     @PostMapping("/register")
-    public User register(@RequestParam String username, @RequestParam @Valid @Size(min = 8, max = 18) String password) {
-        Set<Authority> authorities = new HashSet<>();
+    public User register(@RequestParam String username,
+                         @RequestParam String password) {
+        List<Authority> authorities = new ArrayList<>();
         authorities.add(new Authority(Authority.NAMES.ROLE_USER.name()));
         User user = User.builder()
                         .username(username)
@@ -46,8 +50,18 @@ public class UserController {
                         .enabled(true)
                         .authorities(authorities)
                         .build();
-        userRepo.save(user);
-        return user;
+        return userRepo.save(user);
+    }
+
+    @PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/save")
+    public User save(@RequestBody User user) {
+        if (user.getId() != null) {
+            User orig = userRepo.findById(user.getId()).orElseThrow(new BusinessException("无记录"));
+            ObjectUtils.merge(orig, user);
+            return userRepo.save(user);
+        }
+        return userRepo.save(user);
     }
 
     @GetMapping("/my")
@@ -55,9 +69,27 @@ public class UserController {
         return userRepo.findById(SecurityUtils.getAuthenticatedUser().getId()).orElseThrow(new BusinessException("用户不存在"));
     }
 
+    @PreAuthorize("hasRole('ADMIN')")
     @GetMapping("/all")
     public Page<User> all(PageQuery pageQuery) {
+        PageRequest pageRequest;
+        if (StringUtils.isNotEmpty(pageQuery.getSort())) {
+            List<Sort.Order> orders = new ArrayList<>();
+            for (String sortStr : pageQuery.getSort().split(";")) {
+                String direction = "asc";
+                String prop = sortStr;
+                if (sortStr.contains(",asc") || sortStr.contains(",desc")) {
+                    prop = sortStr.split(",")[0];
+                    direction = sortStr.split(",")[1];
+                }
+                orders.add("asc".equals(direction) ? Sort.Order.asc(prop) : Sort.Order.desc(prop));
+            }
+            pageRequest = PageRequest.of(pageQuery.getPage(), pageQuery.getSize(), Sort.by(orders));
+        } else {
+            pageRequest = PageRequest.of(pageQuery.getPage(), pageQuery.getSize());
+        }
         return userRepo.findAll((Specification<User>) (root, criteriaQuery, criteriaBuilder) -> {
+            List<Predicate> and = new ArrayList<>();
             pageQuery.getQuery().forEach((property, value) -> {
                 if (value == null) {
                     return;
@@ -67,27 +99,43 @@ public class UserController {
                         return;
                     }
                 }
-                criteriaBuilder.and(criteriaBuilder.equal(root.get(property), value));
+                and.add(criteriaBuilder.and(criteriaBuilder.equal(root.get(property), value)));
             });
             if (StringUtils.isNotEmpty(pageQuery.getSearch())) {
                 Field[] fields = User.class.getDeclaredFields();
                 List<Predicate> or = new ArrayList<>();
+                or.add(criteriaBuilder.equal(root.get("id"), pageQuery.getSearch()));
                 for (Field field : fields) {
-                    Annotation[] annotations = field.getDeclaredAnnotations();
-                    for (Annotation annotation : annotations) {
-                        if (!annotation.annotationType().equals(Searchable.class)) {
-                            continue;
-                        }
-                        if (!((Searchable) annotation).value()) {
-                            continue;
-                        }
-                        or.add(criteriaBuilder.equal(root.get(field.getName()), pageQuery.getSearch()));
+                    Searchable annotation = field.getAnnotation(Searchable.class);
+                    if (annotation == null) {
+                        continue;
                     }
+                    if (!annotation.value()) {
+                        continue;
+                    }
+                    or.add(criteriaBuilder.like(root.get(field.getName()), "%" + pageQuery.getSearch() + "%"));
                 }
-                criteriaBuilder.and(or.toArray(new Predicate[or.size()]));
+                and.add(criteriaBuilder.or(or.stream().toArray(Predicate[]::new)));
             }
+            return criteriaBuilder.and(and.stream().toArray(Predicate[]::new));
+        }, pageRequest);
+    }
+
+    @GetMapping("/get/{id}")
+    public User get(@PathVariable Long id) {
+        return userRepo.findById(id).orElseThrow(new BusinessException("无记录"));
+    }
 
-            return criteriaBuilder.and();
-        }, PageRequest.of(pageQuery.getPage(), pageQuery.getSize()));
+    @GetMapping("/excel")
+    @ResponseBody
+    public void excel(HttpServletResponse response, PageQuery pageQuery) throws IOException {
+        response.setContentType("application/vnd.ms-excel");
+        response.setCharacterEncoding("utf-8");
+        response.setHeader("Content-Disposition", "attachment;filename=User.xlsx");
+        List<User> data = all(pageQuery).getContent();
+        EasyExcel.write(response.getOutputStream(), User.class).sheet("User")
+                 .registerConverter(new LocalDateConverter())
+                 .registerConverter(new LocalDateTimeConverter())
+                 .doWrite(data);
     }
 }

+ 65 - 0
src/main/resources/logback-spring.xml

@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
+
+    <springProfile name="dev">
+        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+            <encoder>
+                <pattern>${CONSOLE_LOG_PATTERN}</pattern>
+            </encoder>
+        </appender>
+        <root level="INFO">
+            <appender-ref ref="CONSOLE"/>
+        </root>
+        <logger name="org.hibernate.SQL" level="DEBUG"/>
+        <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
+        <logger name="org.springframework.jdbc.core.JdbcTemplate" level="DEBUG"/>
+        <logger name="org.springframework.jdbc.core.StatementCreatorUtils" level="TRACE"/>
+        <logger name="cn.binarywang.wx.miniapp" level="DEBUG"/>
+    </springProfile>
+
+    <springProfile name="test">
+        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+            <encoder>
+                <pattern>${FILE_LOG_PATTERN}</pattern>
+            </encoder>
+            <file>/var/www/walkchinaTest/app.log</file>
+            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+                <fileNamePattern>app.log.%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
+                <maxFileSize>10MB</maxFileSize>
+                <maxHistory>60</maxHistory>
+            </rollingPolicy>
+        </appender>
+        <root level="INFO">
+            <appender-ref ref="FILE"/>
+        </root>
+        <logger name="org.hibernate.SQL" level="DEBUG"/>
+        <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
+        <logger name="org.springframework.jdbc.core.JdbcTemplate" level="DEBUG"/>
+        <logger name="org.springframework.jdbc.core.StatementCreatorUtils" level="TRACE"/>
+        <logger name="cn.binarywang.wx.miniapp" level="DEBUG"/>
+    </springProfile>
+
+    <springProfile name="prod">
+        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+            <encoder>
+                <pattern>${FILE_LOG_PATTERN}</pattern>
+            </encoder>
+            <file>/var/www/walkchina/app.log</file>
+            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+                <fileNamePattern>app.log.%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
+                <maxFileSize>10MB</maxFileSize>
+                <maxHistory>60</maxHistory>
+            </rollingPolicy>
+        </appender>
+        <root level="INFO">
+            <appender-ref ref="FILE"/>
+        </root>
+        <logger name="org.hibernate.SQL" level="DEBUG"/>
+        <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
+        <logger name="org.springframework.jdbc.core.JdbcTemplate" level="DEBUG"/>
+        <logger name="org.springframework.jdbc.core.StatementCreatorUtils" level="TRACE"/>
+        <logger name="cn.binarywang.wx.miniapp" level="DEBUG"/>
+    </springProfile>
+
+</configuration>

+ 1 - 1
src/main/vue/public/index.html

@@ -6,7 +6,7 @@
     <meta name="viewport" content="width=device-width,initial-scale=1.0">
     <link rel="icon" href="<%= BASE_URL %>favicon.ico">
     <script src="<%= BASE_URL %>fontawesome-v5.2.0.js"></script>
-    <title>vue</title>
+    <title>筑梦居</title>
   </head>
   <body>
     <noscript>

+ 103 - 105
src/main/vue/src/components/CropUpload.vue

@@ -3,126 +3,124 @@
         <div id="upload-wrapper" class="upload-wrapper">
             <img v-if="src" :src="src">
             <i v-else class="el-icon-plus avatar-uploader-icon"></i>
-            <div v-if="loading" class="loading"><i class="el-icon-loading"></i></div>
+            <div v-if="loading" class="loading"><i class="el-icon-loading"></i>
+            </div>
         </div>
-        <avatar-cropper
-            ref="cropper"
-            @submit="loading=true"
-            @uploaded="handleUploaded"
-            trigger="#upload-wrapper"
-            :cropper-options="cropperOptions"
-            :output-options="outputOptions"
-            :upload-url="$baseUrl+'/assets/uploadFile'"
-            :labels="{submit: '确定',cancel: '取消'}"/>
+        <avatar-cropper ref="cropper" @submit="loading=true"
+            @uploaded="handleUploaded" trigger="#upload-wrapper"
+            :cropper-options="cropperOptions" :output-options="outputOptions"
+            :upload-url="$baseUrl+'/upload/file'"
+            :labels="{submit: '确定',cancel: '取消'}" :upload-headers="headers" />
     </div>
 </template>
 <script>
-    import AvatarCropper from "vue-avatar-cropper"
+import AvatarCropper from "vue-avatar-cropper";
 
-    export default {
-        props: {
-            value: {},
-            width: {
-                type: Number,
-                default: 350
-            },
-            height: {
-                type: Number,
-                default: 350
-            },
-        },
-        created() {
-            if (this.value) {
-                this.src = this.value
-            }
-        },
-        mounted() {
-            document.body.appendChild(this.$refs.cropper.$el);
+export default {
+    props: {
+        value: {},
+        width: {
+            type: Number,
+            default: 350
         },
-        beforeDestroy() {
-            document.body.removeChild(this.$refs.cropper.$el);
-        },
-        data() {
-            return {
-                src: '',
-                cropperOptions: {
-                    aspectRatio: 1
-                },
-                loading: false
-            }
-        },
-        computed: {
-            outputOptions() {
-                return {width: this.width, height: this.height}
-            }
-        },
-        watch: {
-            value() {
-                if (this.value) {
-                    this.src = this.value
-                }
+        height: {
+            type: Number,
+            default: 350
+        }
+    },
+    created() {
+        if (this.value) {
+            this.src = this.value;
+        }
+    },
+    mounted() {
+        document.body.appendChild(this.$refs.cropper.$el);
+    },
+    beforeDestroy() {
+        document.body.removeChild(this.$refs.cropper.$el);
+    },
+    data() {
+        return {
+            src: "",
+            cropperOptions: {
+                aspectRatio: 1
+            },
+            loading: false,
+            headers: {
+                Authorization: "Bearer " + localStorage.getItem("token")
             }
-        },
-        methods: {
-            handleUploaded(res) {
-                this.loading = false;
-                if (res.success) {
-                    this.src = res.data[0];
-                    this.$emit('input', res.data[0]);
-                }
+        };
+    },
+    computed: {
+        outputOptions() {
+            return { width: this.width, height: this.height };
+        }
+    },
+    watch: {
+        value() {
+            if (this.value) {
+                this.src = this.value;
             }
-        },
-        components: {
-            AvatarCropper
         }
+    },
+    methods: {
+        handleUploaded(res) {
+            this.loading = false;
+            this.src = res;
+            this.$emit("input", res);
+        }
+    },
+    components: {
+        AvatarCropper
     }
+};
 </script>
 <style lang="less" scoped>
-    .upload-wrapper {
-        width: 178px;
-        height: 178px;
-        display: block;
-        border: 1px dashed #d9d9d9;
-        border-radius: 6px;
-        cursor: pointer;
-        position: relative;
-        overflow: hidden;
-
-        img {
-            width: 100%;
-            height: 100%;
-        }
+.upload-wrapper {
+    width: 178px;
+    height: 178px;
+    display: block;
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    cursor: pointer;
+    position: relative;
+    overflow: hidden;
 
-        &:hover {
-            border-color: #409EFF;
-        }
+    img {
+        width: 100%;
+        height: 100%;
     }
 
-    .avatar-uploader-icon {
-        font-size: 28px;
-        color: #8c939d;
-        width: 178px;
-        height: 178px;
-        line-height: 178px;
-        text-align: center;
-        cursor: pointer;
-        position: relative;
-        overflow: hidden;
-        background-color: #fbfdff;
+    &:hover {
+        border-color: #409eff;
     }
+}
 
-    .loading {
-        position: absolute;
-        top: 0;
-        bottom: 0;
-        left: 0;
-        right: 0;
-        margin: auto;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        background: rgba(255, 255, 255, 0.6);
-        color: #333;
-        font-size: 24px;
-    }
+.avatar-uploader-icon {
+    font-size: 28px;
+    color: #8c939d;
+    width: 178px;
+    height: 178px;
+    line-height: 178px;
+    text-align: center;
+    cursor: pointer;
+    position: relative;
+    overflow: hidden;
+    background-color: #fbfdff;
+}
+
+.loading {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    margin: auto;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: rgba(255, 255, 255, 0.6);
+    color: #333;
+    font-size: 24px;
+}
 </style>

+ 2 - 2
src/main/vue/src/components/SysMenu.vue

@@ -1,5 +1,5 @@
 <template>
-    <el-menu-item v-if="isLeaf" :index="''+menu.id" :route="{path:menu.href}">
+    <el-menu-item v-if="isLeaf" :index="''+menu.id" :route="{path:menu.path}">
         <i class="fa-fw" :class="menu.icon" v-if="menu.icon"></i><span slot="title">{{menu.name}}</span>
     </el-menu-item>
     <el-submenu v-else :index="''+menu.id">
@@ -29,7 +29,7 @@
         },
         computed: {
             isLeaf() {
-                return !(this.menu.children instanceof Array)
+                return !(this.menu.children instanceof Array && this.menu.children.length)
             }
         }
     }

+ 84 - 0
src/main/vue/src/mixins/pageableTable.js

@@ -0,0 +1,84 @@
+export default {
+    data() {
+        return {
+            page: 0,
+            pageSize: Number(localStorage.getItem('pageSize') || 20),
+            totalPages: 0,
+            totalElements: 0,
+            tableData: [],
+            sort: null,
+        };
+    },
+    created() {
+        this.page = this.$route.query.page || 0;
+        this.getData();
+    },
+    computed: {
+        sortStr() {
+            let sortStr = '';
+            if (this.sort && this.sort.order) {
+                sortStr = `${this.sort.prop},${this.sort.order === 'descending' ? 'desc' : 'asc'}`;
+            }
+            this.$router
+                .replace({
+                    query: {
+                        ...this.$router.query,
+                        sort: sortStr,
+                    },
+                })
+                .catch(_ => {});
+            return sortStr;
+        },
+    },
+    methods: {
+        getData() {
+            let data = {
+                page: this.page,
+                size: this.pageSize,
+                sort: this.sortStr,
+            };
+            if (this.beforeGetData) {
+                let mergeData = this.beforeGetData();
+                if (mergeData) {
+                    data = { ...data, ...mergeData };
+                }
+            }
+            this.$http
+                .get(this.url, data)
+                .then(res => {
+                    this.tableData = res.content;
+                    this.totalPages = res.totalPages;
+                    this.totalElements = res.totalElements;
+                    if (this.afterGetData) {
+                        this.afterGetData(res);
+                    }
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.$message.error(e.error);
+                });
+        },
+        onSortChange(e) {
+            this.sort = e;
+            this.getData();
+        },
+        onSizeChange(e) {
+            localStorage.setItem('pageSize', e);
+            this.page = 0;
+            this.pageSize = e;
+            this.getData();
+        },
+        onCurrentChange(e) {
+            this.$router
+                .replace({
+                    query: {
+                        ...this.$router.query,
+                        page: e - 1,
+                    },
+                })
+                .catch(_ => {});
+            this.page = e - 1;
+            this.getData();
+        },
+    },
+};

+ 0 - 34
src/main/vue/src/mixins/pagination.js

@@ -1,34 +0,0 @@
-export default {
-    data() {
-        return {
-            page: 0,
-            pageSize: Number(localStorage.getItem('pageSize') || 20),
-            totalPages: 0,
-            totalElements: 0,
-            tableData: [],
-        };
-    },
-    created() {
-        this.getData();
-    },
-    methods: {
-        getData() {},
-        onSizeChange(e) {
-            localStorage.setItem('pageSize', e);
-            this.page = 0;
-            this.pageSize = e;
-            this.getData();
-        },
-        onCurrentChange(e) {
-            this.page = e - 1;
-            this.getData();
-        },
-        handleRes(res) {
-            if (res.success) {
-                this.tableData = res.data.content;
-                this.totalPages = res.data.totalPages;
-                this.totalElements = res.data.totalElements;
-            }
-        },
-    },
-};

+ 6 - 2
src/main/vue/src/plugins/http.js

@@ -46,6 +46,7 @@ axiosInstance.interceptors.response.use(
                         from: router.currentRoute.name,
                     },
                 });
+            }else{
             }
         }
         return Promise.reject(error.response.data);
@@ -76,10 +77,13 @@ export default {
                         });
                 });
             },
-            post(url, body) {
+            post(url, body, options) {
+                options = options || {};
                 body = body || {};
                 if (!(body instanceof FormData)) {
-                    body = qs.stringify(body);
+                    if (options.body !== 'json') {
+                        body = qs.stringify(body);
+                    }
                 }
                 return new Promise((resolve, reject) => {
                     axiosInstance

+ 1 - 1
src/main/vue/src/styles/app.less

@@ -1,7 +1,6 @@
 html {
     width: 100%;
     height: 100%;
-    min-width: 1200px;
     font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
         'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
     -webkit-font-smoothing: antialiased;
@@ -14,6 +13,7 @@ html {
 body {
     width: 100%;
     height: 100%;
+    background: white;
     font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
         'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
     -webkit-font-smoothing: antialiased;

+ 2 - 3
src/main/vue/src/views/Admin.vue

@@ -90,7 +90,7 @@ export default {
             const findActiveMenu = (parents, childMenus) => {
                 childMenus.forEach(i => {
                     let parents_copy = [...parents];
-                    if (i.href === path) {
+                    if (i.path === path) {
                         parents_copy.push(i);
                         this.menuPath = parents_copy.map(i => i.name);
                         this.activeMenu = "" + i.id;
@@ -109,7 +109,7 @@ export default {
                 .get("/menu/userMenu", {
                     name: "系统菜单"
                 })
-                .then(menus => {
+                .then(([{ children: menus }]) => {
                     this.rawMenus = menus;
                     this.menus = menus;
                     this.findActiveMenu();
@@ -204,7 +204,6 @@ export default {
     .avatar {
         width: 40px;
         height: 40px;
-        border: 1px solid #ebebeb;
         border-radius: 50%;
     }
     a {

+ 122 - 22
src/main/vue/src/views/Login.vue

@@ -1,28 +1,60 @@
 <template>
     <div class="container"
         :style="{backgroundImage :'url(' + require('../assets/bg_login.jpg') + ')'}">
-        <div class="login-wrapper" @keyup.enter="login">
-            <div class="title">欢迎登录</div>
-            <el-form :model="userInfo" style="width: 350px" ref="form">
-                <el-form-item prop="username"
-                    :rules="{required: true, message: '请输入用户名', trigger: 'blur'}">
-                    <el-input v-model="userInfo.username" placeholder="用户名">
-                    </el-input>
-                </el-form-item>
-                <el-form-item prop="password"
-                    :rules="{required: true, message: '请输入密码', trigger: 'blur'}">
-                    <el-input v-model="userInfo.password" placeholder="密码"
-                        type="password"></el-input>
-                </el-form-item>
-                <el-form-item label="记住我">
-                    <el-switch v-model="rememberMe"></el-switch>
-                </el-form-item>
-                <el-form-item>
-                    <el-button :loading="loading" @click="login" type="primary"
-                        style="width: 100%;">登录</el-button>
-                </el-form-item>
-            </el-form>
-        </div>
+        <transition :name="`slide-${register?'in':'out'}`">
+            <div class="login-wrapper" @keyup.enter="doRegister" v-if="register"
+                key="register">
+
+                <el-page-header @back="register=false" title="登录"
+                    style="width: 350px;line-height: 60px;">
+                    <div class="register-title" slot="content">注册账号</div>
+                </el-page-header>
+
+                <el-form :model="registerInfo" style="width: 350px"
+                    ref="registerForm">
+                    <el-form-item prop="username"
+                        :rules="{required: true, message: '请输入用户名', trigger: 'blur'}">
+                        <el-input v-model="registerInfo.username"
+                            placeholder="用户名">
+                        </el-input>
+                    </el-form-item>
+                    <el-form-item prop="password"
+                        :rules="{required: true, message: '请输入密码', trigger: 'blur'}">
+                        <el-input v-model="registerInfo.password"
+                            placeholder="密码" type="password"></el-input>
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button :loading="loading" @click="doRegister"
+                            type="primary" style="width: 100%;">注册</el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+            <div class="login-wrapper" @keyup.enter="login" v-else key="login">
+                <div class="title">欢迎登录</div>
+                <el-form :model="userInfo" style="width: 350px" ref="form">
+                    <el-form-item prop="username"
+                        :rules="{required: true, message: '请输入用户名', trigger: 'blur'}">
+                        <el-input v-model="userInfo.username" placeholder="用户名">
+                        </el-input>
+                    </el-form-item>
+                    <el-form-item prop="password"
+                        :rules="{required: true, message: '请输入密码', trigger: 'blur'}">
+                        <el-input v-model="userInfo.password" placeholder="密码"
+                            type="password"></el-input>
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button :loading="loading" @click="login"
+                            type="primary" style="width: 100%;">登录</el-button>
+                    </el-form-item>
+                    <el-form-item label="">
+                        <el-checkbox v-model="rememberMe">7天内免登录</el-checkbox>
+                        <el-button type="text" style="float: right;"
+                            @click="register=true">注册账号
+                        </el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </transition>
     </div>
 </template>
 <script>
@@ -34,6 +66,11 @@ export default {
             userInfo: {
                 username: "",
                 password: ""
+            },
+            register: false,
+            registerInfo: {
+                username: "",
+                password: ""
             }
         };
     },
@@ -66,6 +103,28 @@ export default {
                         });
                 }
             });
+        },
+        doRegister() {
+            this.$refs.registerForm.validate(valid => {
+                if (valid) {
+                    this.loading = true;
+                    this.$http
+                        .post("/user/register", {
+                            username: this.registerInfo.username,
+                            password: this.registerInfo.password
+                        })
+                        .then(res => {
+                            this.loading = false;
+                            this.$message.success("注册成功");
+                            this.register = false;
+                        })
+                        .catch(e => {
+                            console.log(e);
+                            this.loading = false;
+                            this.$message.error(e.error);
+                        });
+                }
+            });
         }
     }
 };
@@ -83,6 +142,7 @@ export default {
     background-position: center;
     background-repeat: no-repeat;
     position: relative;
+    overflow: hidden;
 }
 
 .login-wrapper {
@@ -104,5 +164,45 @@ export default {
         width: 350px;
         line-height: 60px;
     }
+    .register-title {
+        color: #20a0ff;
+        font-weight: bold;
+        font-size: 15px;
+    }
+}
+@media screen and (max-width: 600px) {
+    .login-wrapper {
+        right: 0;
+        left: 0;
+        top: 0;
+        bottom: 0;
+        margin: auto;
+        height: 265px;
+        width: calc(100vw - 30px);
+    }
+}
+.slide-in-enter {
+    opacity: 0;
+    transform: translateX(50%);
+}
+.slide-in-enter-active {
+    transition: all 0.3s;
+}
+.slide-in-leave-active {
+    opacity: 0;
+    transform: translateX(-50%);
+    transition: all 0.3s;
+}
+.slide-out-enter {
+    opacity: 0;
+    transform: translateX(-50%);
+}
+.slide-out-enter-active {
+    transition: all 0.3s;
+}
+.slide-out-leave-active {
+    opacity: 0;
+    transform: translateX(50%);
+    transition: all 0.3s;
 }
 </style>

+ 61 - 16
src/main/vue/src/views/Menus.vue

@@ -3,10 +3,10 @@
     <div style="">
         <el-tree :data="menus" :render-content="renderContent"
             :highlight-current="true" :expand-on-click-node="true" node-key="id"
-            default-expand-all>
+            default-expand-all v-loading="loading">
         </el-tree>
-        <el-button size="mini" type="text" @click="addRootMenu"
-            style="margin-left: 24px">添加</el-button>
+        <el-button type="text" @click="addRootMenu" style="margin-left: 24px">添加
+        </el-button>
         <el-dialog :visible.sync="dialogVisible" title="添加菜单">
             <el-form :model="menu" ref="form" label-position="top">
                 <el-form-item label="菜单名" prop="name"
@@ -53,8 +53,11 @@ export default {
                 path: "",
                 icon: "",
                 root: false,
-                enabled: true
+                active: true
             },
+            parent: null,
+            currentRef: null,
+            edit: false,
             icon: ""
         };
     },
@@ -63,10 +66,11 @@ export default {
             this.menu = {
                 name: "",
                 path: "",
-                enabled: true,
+                active: true,
                 root: true,
                 icon: "bars"
             };
+            this.parent = null;
             this.icon = "bars";
             this.dialogVisible = true;
             setTimeout(() => {
@@ -74,11 +78,13 @@ export default {
             }, 100);
         },
         showAddDialog(node, data) {
+            this.edit = false;
+            this.parent = node.data;
             this.menu = {
                 parent: node.data.id,
                 name: "",
                 path: "",
-                enabled: true,
+                active: true,
                 root: false,
                 icon: null
             };
@@ -89,6 +95,8 @@ export default {
             }, 100);
         },
         showEditDialog(node, data) {
+            this.edit = true;
+            this.currentRef = node.data;
             const getIconName = icon => {
                 let iconName = "";
                 if (icon) {
@@ -121,7 +129,18 @@ export default {
                             this.loading = false;
                             this.$message.success("添加成功");
                             this.dialogVisible = false;
-                            this.getData();
+                            if (this.edit) {
+                                for (let [key, value] of Object.entries(res)) {
+                                    console.log(`${key}: ${value}`);
+                                    this.$set(this.currentRef, key, value);
+                                }
+                            } else if (this.parent) {
+                                this.parent.children =
+                                    this.parent.children || [];
+                                this.parent.children.push(res);
+                            } else {
+                                this.menus.push(res);
+                            }
                         })
                         .catch(e => {
                             console.log(e);
@@ -132,6 +151,7 @@ export default {
             });
         },
         remove(node, data) {
+            console.log(node);
             this.$confirm("确定删除菜单?", "提示", {
                 confirmButtonText: "确定",
                 cancelButtonText: "取消",
@@ -139,10 +159,21 @@ export default {
             })
                 .then(() => {
                     this.$http
-                        .post("/menu/del", { id: data.id })
+                        .post("/menu/save", {
+                            ...data,
+                            active: false,
+                            children: null
+                        })
                         .then(res => {
                             this.$message.success("删除成功");
-                            this.getData();
+                            let index = node.parent.data.children.findIndex(
+                                i => {
+                                    return i.id === data.id;
+                                }
+                            );
+                            if (index > -1) {
+                                node.parent.data.children.splice(index, 1);
+                            }
                         })
                         .catch(e => {
                             console.log(e);
@@ -163,6 +194,7 @@ export default {
                 return formData;
             };
             if (node.previousSibling) {
+                this.loading = true;
                 Promise.all([
                     this.$http.post("/menu/save", {
                         ...node.data,
@@ -175,9 +207,15 @@ export default {
                         sort: node.data.sort
                     })
                 ])
-                    .then(this.getData)
+                    .then(_ => {
+                        this.loading = false;
+                        let tmp = { ...node.previousSibling.data };
+                        node.previousSibling.data = { ...node.data };
+                        node.data = tmp;
+                    })
                     .catch(e => {
                         console.log(e);
+                        this.loading = false;
                         this.$message.erro(e.error);
                     });
             }
@@ -193,6 +231,7 @@ export default {
                 return formData;
             };
             if (node.nextSibling) {
+                this.loading = true;
                 Promise.all([
                     this.$http.post("/menu/save", {
                         ...node.data,
@@ -205,9 +244,15 @@ export default {
                         sort: node.data.sort
                     })
                 ])
-                    .then(this.getData)
+                    .then(_ => {
+                        this.loading = false;
+                        let tmp = { ...node.nextSibling.data };
+                        node.nextSibling.data = { ...node.data };
+                        node.data = tmp;
+                    })
                     .catch(e => {
                         console.log(e);
+                        this.loading = false;
                         this.$message.erro(e.error);
                     });
             }
@@ -230,50 +275,50 @@ export default {
                     <span class="url">{data.path}</span>
                     <span class="opt">
                         <el-button
-                            size="mini"
                             type="text"
                             on-click={e => {
                                 this.moveUp(node, data), e.stopPropagation();
                             }}
                             class="up"
+                            icon="el-icon-top"
                         >
                             上移
                         </el-button>
                         <el-button
-                            size="mini"
                             type="text"
                             on-click={e => {
                                 this.moveDown(node, data), e.stopPropagation();
                             }}
+                            icon="el-icon-bottom"
                         >
                             下移
                         </el-button>
                         <el-button
-                            size="mini"
                             type="text"
                             on-click={e => {
                                 this.showEditDialog(node, data),
                                     e.stopPropagation();
                             }}
+                            icon="el-icon-edit"
                         >
                             编辑
                         </el-button>
                         <el-button
-                            size="mini"
                             type="text"
                             on-click={e => {
                                 this.showAddDialog(node, data),
                                     e.stopPropagation();
                             }}
+                            icon="el-icon-plus"
                         >
                             添加
                         </el-button>
                         <el-button
-                            size="mini"
                             type="text"
                             on-click={e => {
                                 this.remove(node, data), e.stopPropagation();
                             }}
+                            icon="el-icon-delete"
                         >
                             删除
                         </el-button>

+ 56 - 115
src/main/vue/src/views/UserEdit.vue

@@ -2,42 +2,33 @@
     <div>
         <el-form :model="formData" :rules="rules" ref="form" label-width="80px"
             label-position="right" style="max-width: 500px;">
-            <el-form-item prop="icon" label="头像">
-                <crop-upload v-model="formData.icon"></crop-upload>
+            <el-form-item prop="avatar" label="头像">
+                <crop-upload v-model="formData.avatar"></crop-upload>
             </el-form-item>
             <el-form-item prop="username" label="用户名">
                 <el-input v-model="formData.username"></el-input>
             </el-form-item>
-            <el-form-item prop="password" label="密码">
-                <el-button v-if="formData.id" type="primary" plain
-                    @click="resetPassword">重置</el-button>
-                <el-input v-else v-model="formData.password"></el-input>
+            <el-form-item prop="nickname" label="昵称">
+                <el-input v-model="formData.nickname"></el-input>
+            </el-form-item>
+            <el-form-item v-if="formData.id" label="密码">
+                <el-button type="primary" plain @click="resetPassword">重置
+                </el-button>
+            </el-form-item>
+            <el-form-item v-else prop="password" label="密码">
+                <el-input v-model="formData.password"></el-input>
             </el-form-item>
             <el-form-item prop="phone" label="手机">
                 <el-input v-model="formData.phone"></el-input>
             </el-form-item>
-            <el-form-item prop="roleId" label="角色">
-                <el-select v-model="formData.roleId" multiple placeholder="请选择"
-                    style="width: 100%;">
-                    <el-option v-for="item in roles" :key="item.id"
-                        :label="item.name" :value="item.id">
+            <el-form-item prop="authorities" label="角色">
+                <el-select v-model="formData.authorities" multiple
+                    placeholder="请选择" value-key="name">
+                    <el-option v-for="item in authorities" :key="item.name"
+                        :label="item.name" :value="item">
                     </el-option>
                 </el-select>
             </el-form-item>
-            <el-form-item prop="departId" label="部门">
-                <el-cascader :options="departs" v-model="formData.departId">
-                </el-cascader>
-            </el-form-item>
-            <el-form-item prop="birthday" label="生日">
-                <el-date-picker v-model="formData.birthday" format="yyyy-MM-dd"
-                    value-format="timestamp">
-                </el-date-picker>
-            </el-form-item>
-            <el-form-item prop="createTime" label="创建时间">
-                <el-date-picker v-model="formData.createTime" type="datetime"
-                    format="yyyy-MM-dd HH:mm:ss" value-format="timestamp">
-                </el-date-picker>
-            </el-form-item>
             <el-form-item>
                 <el-button @click="onSave" :loading="$store.state.fetchingData"
                     type="primary">保存</el-button>
@@ -54,112 +45,63 @@ export default {
     created() {
         if (this.$route.query.id) {
             this.$http
-                .get({
-                    url: "/userInfo/getOne",
-                    data: {
-                        id: this.$route.query.id
-                    }
-                })
+                .get(`/user/get/${this.$route.query.id}`)
                 .then(res => {
-                    if (res.success) {
-                        res.data.roleId = res.data.roleId
-                            ? res.data.roleId.split(",").map(i => Number(i))
-                            : [];
-                        res.data.departId = res.data.departId
-                            ? res.data.departId.split(",")
-                            : [];
-                        this.formData = res.data;
-                    }
+                    this.formData = res;
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.$message.error(e.error);
                 });
         }
         this.$http
-            .get({
-                url: "/sysRole/all"
-            })
+            .get("/authority/all")
             .then(res => {
-                if (res.success) {
-                    this.roles = res.data;
-                }
-            });
-        this.$http
-            .get({
-                url: "/departInfo/departTree"
+                this.authorities = res;
             })
-            .then(res => {
-                if (res.success) {
-                    const parse = trees => {
-                        trees.sort((a, b) => {
-                            return a.extra.sort - b.extra.sort;
-                        });
-                        return trees.map(i => {
-                            let t = {
-                                value: i.id,
-                                label: i.name,
-                                parentId: i.parentId,
-                                extra: i.extra
-                            };
-                            if (i.children instanceof Array) {
-                                t.children = parse(i.children);
-                            }
-                            return t;
-                        });
-                    };
-                    this.departs = parse(res.data);
-                }
+            .catch(e => {
+                console.log(e);
             });
     },
     data() {
         return {
             saving: false,
             formData: {
-                departId: []
+                avatar:
+                    "https://zhumj.oss-cn-hangzhou.aliyuncs.com/image/user.jpg"
             },
             rules: {
-                icon: [
+                avatar: [
                     { required: true, message: "请上传头像", trigger: "blur" }
                 ],
                 username: [
-                    { required: true, message: "请输入昵称", trigger: "blur" }
-                ],
-                password: [
                     {
-                        validator: (rule, value, callback) => {
-                            if (this.formData.id) {
-                                callback();
-                            } else if (this.formData.password) {
-                                callback();
-                            } else {
-                                callback("请填写密码");
-                            }
-                        },
+                        required: true,
+                        type: "regexp",
+                        regexp: /^[_.@A-Za-z0-9-]*$/,
+                        message: "请上传头像",
                         trigger: "blur"
                     }
                 ],
+                nickname: [
+                    { required: true, message: "请输入昵称", trigger: "blur" }
+                ],
+                password: [
+                    { required: true, message: "请输入密码", trigger: "blur" }
+                ],
                 phone: [
                     {
-                        required: true,
-                        message: "请输入手机号",
-                        trigger: "blur"
-                    },
-                    {
-                        validator: (rule, value, callback) => {
-                            if (!value) {
-                                callback(new Error("请输入手机号"));
-                            } else if (/^1[3-9]\d{9}$/.test(value)) {
-                                callback();
-                            } else {
-                                callback(new Error("请输入正确的手机号"));
-                            }
-                        },
+                        type: "regexp",
+                        regexp: /^1[3-9]\d{9}$/,
+                        message: "请输入正确的手机号",
                         trigger: "blur"
                     }
                 ],
-                roleId: [
+                authorities: [
                     { required: true, message: "请选择角色", trigger: "blur" }
                 ]
             },
-            roles: [],
-            departs: []
+            authorities: []
         };
     },
     methods: {
@@ -173,21 +115,20 @@ export default {
             });
         },
         submit() {
-            var data = JSON.parse(JSON.stringify(this.formData));
             this.$http
-                .post({
-                    url: this.formData.id
-                        ? "/userInfo/update"
-                        : "/userInfo/save",
-                    data: data
-                })
+                .post("/user/save", this.formData, { body: "json" })
                 .then(res => {
-                    if (res.success) {
-                        this.$message.success("成功");
-                        this.$router.go(-1);
-                    } else {
-                        this.$message.warning("失败");
-                    }
+                    this.$message.success("成功");
+                    this.formData = res;
+                    this.$router.replace({
+                        query: {
+                            id: res.id
+                        }
+                    });
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.$message.error(e.error);
                 });
         },
         del() {

+ 80 - 113
src/main/vue/src/views/UserList.vue

@@ -1,64 +1,43 @@
 <template>
-    <div>
+    <div class="pageable-list">
         <div class="filters-container">
-            <el-input placeholder="用户名" v-model="filter1" clearable
+            <el-input placeholder="输入关键字" v-model="search" clearable
                 class="filter-item"></el-input>
-            <el-select placeholder="性别" v-model="filter2" clearable
-                class="filter-item">
-                <el-option label="女" value="item1">
-                </el-option>
-                <el-option label="男" value="item2">
-                </el-option>
-            </el-select>
             <el-button @click="getData" type="primary" icon="el-icon-search"
                 class="filter-item">搜索
             </el-button>
-            <el-button @click="$router.push('/user')" type="primary"
-                icon="el-icon-edit" class="filter-item">添加
+            <el-button @click="addRow" type="primary" icon="el-icon-plus"
+                class="filter-item">添加
+            </el-button>
+            <el-button @click="download" type="primary" icon="el-icon-download"
+                :loading="downloading" class="filter-item">导出EXCEL
             </el-button>
-            <el-dropdown trigger="click" size="medium"
-                class="table-column-filter">
-                <span>
-                    筛选数据<i class="el-icon-arrow-down el-icon--right"></i>
-                </span>
-                <el-dropdown-menu slot="dropdown"
-                    class="table-column-filter-wrapper">
-                    <el-checkbox v-for="item in tableColumns" :key="item.value"
-                        v-model="item.show">{{item.label}}
-                    </el-checkbox>
-                </el-dropdown-menu>
-            </el-dropdown>
         </div>
-        <el-table :data="tableData" :height="tableHeight" row-key="id"
-            ref="table">
+        <el-table :data="tableData" row-key="id" ref="table"
+            @sort-change="onSortChange">
             <el-table-column v-if="multipleMode" align="center" type="selection"
                 width="50">
             </el-table-column>
-            <el-table-column type="index" min-width="50" align="center">
+            <el-table-column prop="id" label="ID" width="100" align="center"
+                sortable="custom">
             </el-table-column>
-            <el-table-column v-if="isColumnShow('username')" prop="username"
-                label="用户名" min-width="300">
+            <el-table-column prop="username" label="用户名" min-width="300"
+                sortable="custom">
             </el-table-column>
-            <el-table-column v-if="isColumnShow('nickname')" prop="nickname"
-                label="昵称" min-width="300">
+            <el-table-column prop="nickname" label="昵称" min-width="300"
+                sortable="custom">
             </el-table-column>
-            <el-table-column v-if="isColumnShow('icon')" label="头像"
-                min-width="300">
-                <template slot-scope="scope">
-                    <img :src="scope.row.icon"
-                        style="width: 32px;height: 32px;border-radius: 50%;vertical-align: middle;" />
+            <el-table-column label="头像" min-width="300">
+                <template slot-scope="{row}">
+                    <el-image style="width: 30px; height: 30px"
+                        :src="row.avatar" fit="cover"
+                        :preview-src-list="[row.avatar]"></el-image>
                 </template>
             </el-table-column>
-            <el-table-column v-if="isColumnShow('sex')" prop="sex" label="性别"
-                min-width="300">
-            </el-table-column>
-            <el-table-column v-if="isColumnShow('openId')" prop="openId"
-                label="openId" min-width="300">
-            </el-table-column>
             <el-table-column label="操作" align="center" fixed="right">
-                <template slot-scope="scope">
-                    <el-button @click="editRow(scope.row)" type="primary"
-                        size="mini" plain>编辑</el-button>
+                <template slot-scope="{row}">
+                    <el-button @click="editRow(row)" type="primary" size="mini"
+                        plain>编辑</el-button>
                 </template>
             </el-table-column>
         </el-table>
@@ -72,59 +51,27 @@
                     <el-button @click="toggleMultipleMode(false)">取消</el-button>
                 </el-button-group>
             </div>
-            <el-pagination background @size-change="pageSizeChange"
-                @current-change="currentPageChange" :current-page="currentPage"
+            <el-pagination background @size-change="onSizeChange"
+                @current-change="onCurrentChange" :current-page="page"
                 :page-sizes="[10, 20, 30, 40, 50]" :page-size="pageSize"
                 layout="total, sizes, prev, pager, next, jumper"
-                :total="totalNumber">
+                :total="totalElements">
             </el-pagination>
         </div>
     </div>
 </template>
 <script>
 import { mapState } from "vuex";
+import pageableTable from "@/mixins/pageableTable";
 
 export default {
-    created() {
-        this.getData();
-    },
+    mixins: [pageableTable],
     data() {
         return {
-            totalNumber: 0,
-            totalPage: 0,
-            currentPage: 1,
-            pageSize: 20,
-            tableData: [],
-            filter1: "",
-            filter2: "",
-            tableColumns: [
-                {
-                    label: "用户名",
-                    value: "username",
-                    show: true
-                },
-                {
-                    label: "昵称",
-                    value: "nickname",
-                    show: true
-                },
-                {
-                    label: "头像",
-                    value: "icon",
-                    show: true
-                },
-                {
-                    label: "性别",
-                    value: "sex",
-                    show: true
-                },
-                {
-                    label: "openId",
-                    value: "openId",
-                    show: true
-                }
-            ],
-            multipleMode: false
+            multipleMode: false,
+            search: "",
+            url: "/user/all",
+            downloading: false
         };
     },
     computed: {
@@ -134,33 +81,10 @@ export default {
         }
     },
     methods: {
-        pageSizeChange(size) {
-            this.pageSize = size;
-            this.getData();
-        },
-        currentPageChange(page) {
-            this.currentPage = page;
-            this.getData();
-        },
-        getData() {
-            this.$http
-                .get({
-                    url: "/userInfo/page",
-                    data: {
-                        currentPage: this.currentPage,
-                        pageNumber: this.pageSize
-                    }
-                })
-                .then(res => {
-                    if (res.success) {
-                        this.totalNumber = res.data.page.totalNumber;
-                        this.tableData = res.data.pp;
-                    }
-                });
-        },
-        isColumnShow(column) {
-            var row = this.tableColumns.find(i => i.value === column);
-            return row ? row.show : false;
+        beforeGetData() {
+            if (this.search) {
+                return { search: this.search };
+            }
         },
         toggleMultipleMode(multipleMode) {
             this.multipleMode = multipleMode;
@@ -168,14 +92,48 @@ export default {
                 this.$refs.table.clearSelection();
             }
         },
+        addRow() {
+            this.$router.push({
+                path: "/userEdit",
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
         editRow(row) {
             this.$router.push({
-                path: "/user",
+                path: "/userEdit",
                 query: {
                     id: row.id
                 }
             });
         },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get("/user/excel", { responseType: "blob" })
+                .then(res => {
+                    console.log(res);
+                    this.downloading = false;
+                    const downloadUrl = window.URL.createObjectURL(
+                        new Blob([res.data])
+                    );
+                    const link = document.createElement("a");
+                    link.href = downloadUrl;
+                    link.setAttribute(
+                        "download",
+                        res.headers["content-disposition"].split("filename=")[1]
+                    );
+                    document.body.appendChild(link);
+                    link.click();
+                    link.remove();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.downloading = false;
+                    this.$message.error(e.error);
+                });
+        },
         operation1() {
             this.$notify({
                 title: "提示",
@@ -189,4 +147,13 @@ export default {
 };
 </script>
 <style lang="less" scoped>
+.pageable-list {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    .el-table {
+        flex-grow: 1;
+        flex-basis: 0;
+    }
+}
 </style>

+ 17 - 0
src/test/java/com/izouma/zhumj/CommonTest.java

@@ -0,0 +1,17 @@
+package com.izouma.zhumj;
+
+import com.izouma.zhumj.domain.User;
+import com.izouma.zhumj.utils.ObjectUtils;
+import org.junit.Test;
+
+public class CommonTest {
+    @Test
+    public void testMergeObject() {
+        User src = new User();
+        User dst = new User();
+        src.setUsername("src");
+        dst.setNickname("dst");
+        ObjectUtils.merge(dst, src);
+        System.out.println(src);
+    }
+}