Răsfoiți Sursa

Merge branch '筹建端' into dev

xiongzhu 5 ani în urmă
părinte
comite
e37f8113f6
35 a modificat fișierele cu 4550 adăugiri și 10 ștergeri
  1. 53 0
      src/main/java/com/izouma/zhumj/domain/Construction.java
  2. 54 0
      src/main/java/com/izouma/zhumj/domain/ConstructionProcess.java
  3. 39 0
      src/main/java/com/izouma/zhumj/domain/ConstructionProcessLog.java
  4. 18 0
      src/main/java/com/izouma/zhumj/enums/ProcessStatus.java
  5. 8 0
      src/main/java/com/izouma/zhumj/repo/ConstructionProcessLogRepo.java
  6. 11 0
      src/main/java/com/izouma/zhumj/repo/ConstructionProcessRepo.java
  7. 8 0
      src/main/java/com/izouma/zhumj/repo/ConstructionRepo.java
  8. 33 0
      src/main/java/com/izouma/zhumj/service/ConstructionProcessLogService.java
  9. 134 0
      src/main/java/com/izouma/zhumj/service/ConstructionProcessService.java
  10. 14 0
      src/main/java/com/izouma/zhumj/service/ConstructionService.java
  11. 58 0
      src/main/java/com/izouma/zhumj/web/ConstructionController.java
  12. 62 0
      src/main/java/com/izouma/zhumj/web/ConstructionProcessController.java
  13. 66 0
      src/main/java/com/izouma/zhumj/web/ConstructionProcessLogController.java
  14. 1 0
      src/main/resources/genjson/Construction.json
  15. 0 0
      src/main/resources/genjson/ConstructionProcess.json
  16. 1 0
      src/main/resources/genjson/ConstructionProcessLog.json
  17. 2 0
      src/main/vue/package.json
  18. 118 0
      src/main/vue/src/components/wl-gantt/components/wl-contextmenu.vue
  19. 280 0
      src/main/vue/src/components/wl-gantt/css/index.less
  20. 0 0
      src/main/vue/src/components/wl-gantt/css/index.min.css
  21. 276 0
      src/main/vue/src/components/wl-gantt/css/index.scss
  22. 7 0
      src/main/vue/src/components/wl-gantt/index.js
  23. 1478 0
      src/main/vue/src/components/wl-gantt/index.vue
  24. 3 0
      src/main/vue/src/main.js
  25. 57 0
      src/main/vue/src/router.js
  26. 536 0
      src/main/vue/src/views/ConstructionGantt.vue
  27. 381 0
      src/main/vue/src/views/ConstructionList.vue
  28. 214 0
      src/main/vue/src/views/ConstructionProcessEdit.vue
  29. 285 0
      src/main/vue/src/views/ConstructionProcessList.vue
  30. 112 0
      src/main/vue/src/views/ConstructionProcessLogEdit.vue
  31. 169 0
      src/main/vue/src/views/ConstructionProcessLogList.vue
  32. 2 2
      src/main/vue/src/views/Operation/RoomStatus.vue
  33. 1 2
      src/main/vue/src/views/customer/ContractListForCustomer.vue
  34. 67 0
      src/main/vue/yarn.lock
  35. 2 6
      src/test/java/com/izouma/zhumj/CommonTest.java

+ 53 - 0
src/main/java/com/izouma/zhumj/domain/Construction.java

@@ -0,0 +1,53 @@
+package com.izouma.zhumj.domain;
+
+import com.izouma.zhumj.enums.ProcessStatus;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import java.time.LocalDate;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@ApiModel("施工项目")
+public class Construction extends BaseEntity {
+
+    @ApiModelProperty("门店")
+    private Long storeId;
+
+    @ApiModelProperty("项目名称")
+    private String name;
+
+    @ApiModelProperty("店长")
+    private String manager;
+
+    @ApiModelProperty("工程监理")
+    private String supervisor;
+
+    private LocalDate planStart;
+
+    private LocalDate planEnd;
+
+    @ApiModelProperty("开工日期")
+    private LocalDate start;
+
+    @ApiModelProperty("竣工日期")
+    private LocalDate end;
+
+    private int days;
+
+    @Enumerated(EnumType.STRING)
+    private ProcessStatus status;
+
+    @ApiModelProperty("进度")
+    private int progress;
+}

+ 54 - 0
src/main/java/com/izouma/zhumj/domain/ConstructionProcess.java

@@ -0,0 +1,54 @@
+package com.izouma.zhumj.domain;
+
+import com.izouma.zhumj.enums.ProcessStatus;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import java.time.LocalDate;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@ApiModel("工序")
+public class ConstructionProcess extends BaseEntity {
+    @ApiModelProperty("项目")
+    private Long constructionId;
+
+    @ApiModelProperty("工序名称")
+    private String name;
+
+    @ApiModelProperty("计划开工")
+    private LocalDate planStart;
+
+    @ApiModelProperty("计划结束")
+    private LocalDate planEnd;
+
+    @ApiModelProperty("工期")
+    private int days;
+
+    @ApiModelProperty("负责人")
+    private String principal;
+
+    @ApiModelProperty("开工时间")
+    private LocalDate start;
+
+    @ApiModelProperty("结束时间")
+    private LocalDate end;
+
+    @ApiModelProperty("备注")
+    private String remark;
+
+    @Enumerated(EnumType.STRING)
+    @ApiModelProperty("状态")
+    private ProcessStatus status;
+}

+ 39 - 0
src/main/java/com/izouma/zhumj/domain/ConstructionProcessLog.java

@@ -0,0 +1,39 @@
+package com.izouma.zhumj.domain;
+
+import com.izouma.zhumj.enums.ProcessStatus;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@ApiModel("工序")
+public class ConstructionProcessLog extends BaseEntity {
+    @ApiModelProperty("项目")
+    private Long constructionId;
+
+    @ApiModelProperty("工序名称")
+    private Long constructionProcessId;
+
+    @ApiModelProperty("备注")
+    private String remark;
+
+    @ApiModelProperty("操作时间")
+    private LocalDateTime time;
+
+    @ApiModelProperty("操作人")
+    private String operator;
+
+}

+ 18 - 0
src/main/java/com/izouma/zhumj/enums/ProcessStatus.java

@@ -0,0 +1,18 @@
+package com.izouma.zhumj.enums;
+
+public enum ProcessStatus {
+    PENDING("未开工"),
+    IN_PROGRESS("已开工"),
+    PAUSE("已暂停"),
+    FINISH("已完工");
+
+    private final String description;
+
+    ProcessStatus(String description) {
+        this.description = description;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+}

+ 8 - 0
src/main/java/com/izouma/zhumj/repo/ConstructionProcessLogRepo.java

@@ -0,0 +1,8 @@
+package com.izouma.zhumj.repo;
+
+import com.izouma.zhumj.domain.ConstructionProcessLog;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+public interface ConstructionProcessLogRepo extends JpaRepository<ConstructionProcessLog, Long>, JpaSpecificationExecutor<ConstructionProcessLog> {
+}

+ 11 - 0
src/main/java/com/izouma/zhumj/repo/ConstructionProcessRepo.java

@@ -0,0 +1,11 @@
+package com.izouma.zhumj.repo;
+
+import com.izouma.zhumj.domain.ConstructionProcess;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+import java.util.List;
+
+public interface ConstructionProcessRepo extends JpaRepository<ConstructionProcess, Long>, JpaSpecificationExecutor<ConstructionProcess> {
+    List<ConstructionProcess> findByConstructionId(Long constructionId);
+}

+ 8 - 0
src/main/java/com/izouma/zhumj/repo/ConstructionRepo.java

@@ -0,0 +1,8 @@
+package com.izouma.zhumj.repo;
+
+import com.izouma.zhumj.domain.Construction;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+public interface ConstructionRepo extends JpaRepository<Construction, Long>, JpaSpecificationExecutor<Construction> {
+}

+ 33 - 0
src/main/java/com/izouma/zhumj/service/ConstructionProcessLogService.java

@@ -0,0 +1,33 @@
+package com.izouma.zhumj.service;
+
+import com.izouma.zhumj.domain.ConstructionProcess;
+import com.izouma.zhumj.domain.ConstructionProcessLog;
+import com.izouma.zhumj.exception.BusinessException;
+import com.izouma.zhumj.repo.ConstructionProcessLogRepo;
+import com.izouma.zhumj.repo.ConstructionProcessRepo;
+import com.izouma.zhumj.utils.SecurityUtils;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+
+@Service
+@AllArgsConstructor
+public class ConstructionProcessLogService {
+
+    private ConstructionProcessLogRepo constructionProcessLogRepo;
+    private ConstructionProcessRepo    constructionProcessRepo;
+
+    public ConstructionProcessLog saveLog(Long processId, String remark) {
+        ConstructionProcess process = constructionProcessRepo.findById(processId)
+                .orElseThrow(new BusinessException("无记录"));
+        return constructionProcessLogRepo.save(ConstructionProcessLog.builder()
+                .constructionProcessId(processId)
+                .constructionId(process.getConstructionId())
+                .remark(remark)
+                .time(LocalDateTime.now())
+                .operator(SecurityUtils.getAuthenticatedUser().getNickname())
+                .build());
+    }
+
+}

+ 134 - 0
src/main/java/com/izouma/zhumj/service/ConstructionProcessService.java

@@ -0,0 +1,134 @@
+package com.izouma.zhumj.service;
+
+import com.izouma.zhumj.domain.Construction;
+import com.izouma.zhumj.domain.ConstructionProcess;
+import com.izouma.zhumj.domain.ConstructionProcessLog;
+import com.izouma.zhumj.enums.ProcessStatus;
+import com.izouma.zhumj.exception.BusinessException;
+import com.izouma.zhumj.repo.ConstructionProcessLogRepo;
+import com.izouma.zhumj.repo.ConstructionProcessRepo;
+import com.izouma.zhumj.repo.ConstructionRepo;
+import com.izouma.zhumj.utils.DateTimeUtils;
+import com.izouma.zhumj.utils.ObjUtils;
+import com.izouma.zhumj.utils.SecurityUtils;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.text.DateFormat;
+import java.text.MessageFormat;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+@AllArgsConstructor
+public class ConstructionProcessService {
+    private ConstructionRepo           constructionRepo;
+    private ConstructionProcessRepo    constructionProcessRepo;
+    private ConstructionProcessLogRepo constructionProcessLogRepo;
+
+    public ConstructionProcess create(ConstructionProcess record) {
+        record = constructionProcessRepo.save(record);
+        updateConstruction(record.getConstructionId());
+        return record;
+    }
+
+    public ConstructionProcess update(ConstructionProcess record) {
+        ConstructionProcess orig = constructionProcessRepo.findById(record.getId())
+                .orElseThrow(new BusinessException("无记录"));
+        ObjUtils.merge(orig, record);
+        record = constructionProcessRepo.save(orig);
+        updateConstruction(record.getConstructionId());
+        return record;
+    }
+
+    public void del(Long id) {
+        constructionProcessRepo.findById(id).ifPresent(constructionProcess -> {
+            constructionProcessRepo.delete(constructionProcess);
+            updateConstruction(constructionProcess.getConstructionId());
+        });
+    }
+
+    public ConstructionProcess updateStatus(Long id, ProcessStatus status) {
+        LocalDateTime time = LocalDateTime.now();
+        ConstructionProcess process = constructionProcessRepo.findById(id)
+                .orElseThrow(new BusinessException("无记录"));
+        process.setStatus(status);
+        if (status == ProcessStatus.IN_PROGRESS) {
+            process.setStart(time.toLocalDate());
+        }
+        if (status == ProcessStatus.FINISH) {
+            process.setEnd(time.toLocalDate());
+        }
+        process = constructionProcessRepo.save(process);
+
+        String operator = SecurityUtils.getAuthenticatedUser().getNickname();
+
+        constructionProcessLogRepo.save(ConstructionProcessLog.builder()
+                .constructionId(process.getConstructionId())
+                .constructionProcessId(process.getId())
+                .time(time)
+                .operator(operator)
+                .remark(MessageFormat
+                        .format("{0}将工序【{1}】标记为{2},时间为{3}", operator, process.getName(), status
+                                .getDescription(), DateTimeUtils.format(time, DateTimeUtils.DATE_TIME_FORMAT)))
+                .build());
+        updateConstruction(process.getConstructionId());
+        return process;
+    }
+
+    private void updateConstruction(Long id) {
+        Construction construction = constructionRepo.findById(id).orElseThrow(new BusinessException("无记录"));
+        List<ConstructionProcess> processes = constructionProcessRepo.findByConstructionId(id);
+        LocalDate planStart = null;
+        LocalDate planEnd = null;
+        LocalDate start = null;
+        LocalDate end = null;
+        Map<ProcessStatus, Integer> map = new HashMap<>();
+        for (ProcessStatus value : ProcessStatus.values()) {
+            map.put(value, 0);
+        }
+        for (ConstructionProcess process : processes) {
+            if (planStart == null || (process.getPlanStart() != null && process.getPlanStart().isBefore(planStart))) {
+                planStart = process.getPlanStart();
+            }
+            if (planEnd == null || (process.getPlanEnd() != null && process.getPlanEnd().isAfter(planStart))) {
+                planEnd = process.getPlanEnd();
+            }
+            if (start == null || (process.getStart() != null && process.getStart().isBefore(start))) {
+                start = process.getStart();
+            }
+            if (end == null || (process.getEnd() != null && process.getEnd().isAfter(end))) {
+                end = process.getEnd();
+            }
+            map.put(process.getStatus(), map.get(process.getStatus()) + 1);
+        }
+        construction.setPlanStart(planStart);
+        construction.setPlanEnd(planEnd);
+        construction.setStart(start);
+        construction.setEnd(end);
+        if (planStart != null && planEnd != null) {
+            construction.setDays((int) (ChronoUnit.DAYS.between(planStart, planEnd) + 1));
+        }
+        if (map.get(ProcessStatus.IN_PROGRESS) + map.get(ProcessStatus.PAUSE) + map.get(ProcessStatus.FINISH) == 0) {
+            construction.setStatus(ProcessStatus.PENDING);
+        } else if (map.get(ProcessStatus.PENDING) + map.get(ProcessStatus.IN_PROGRESS)
+                + map.get(ProcessStatus.PAUSE) == 0) {
+            construction.setStatus(ProcessStatus.FINISH);
+        } else {
+            construction.setStatus(ProcessStatus.IN_PROGRESS);
+        }
+        if (processes.isEmpty()) {
+            construction.setProgress(0);
+        } else {
+            construction.setProgress(map.get(ProcessStatus.FINISH) * 100 / processes.size());
+        }
+        if (construction.getStatus() != ProcessStatus.FINISH) {
+            construction.setEnd(null);
+        }
+        constructionRepo.save(construction);
+    }
+}

+ 14 - 0
src/main/java/com/izouma/zhumj/service/ConstructionService.java

@@ -0,0 +1,14 @@
+package com.izouma.zhumj.service;
+
+import com.izouma.zhumj.domain.Construction;
+import com.izouma.zhumj.repo.ConstructionRepo;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@AllArgsConstructor
+public class ConstructionService {
+
+    private ConstructionRepo constructionRepo;
+
+}

+ 58 - 0
src/main/java/com/izouma/zhumj/web/ConstructionController.java

@@ -0,0 +1,58 @@
+package com.izouma.zhumj.web;
+import com.izouma.zhumj.domain.Construction;
+import com.izouma.zhumj.service.ConstructionService;
+import com.izouma.zhumj.dto.PageQuery;
+import com.izouma.zhumj.exception.BusinessException;
+import com.izouma.zhumj.repo.ConstructionRepo;
+import com.izouma.zhumj.utils.ObjUtils;
+import com.izouma.zhumj.utils.excel.ExcelUtils;
+import lombok.AllArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+
+@RestController
+@RequestMapping("/construction")
+@AllArgsConstructor
+public class ConstructionController extends BaseController {
+    private ConstructionService constructionService;
+    private ConstructionRepo constructionRepo;
+
+    //@PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/save")
+    public Construction save(@RequestBody Construction record) {
+        if (record.getId() != null) {
+            Construction orig = constructionRepo.findById(record.getId()).orElseThrow(new BusinessException("无记录"));
+            ObjUtils.merge(orig, record);
+            return constructionRepo.save(orig);
+        }
+        return constructionRepo.save(record);
+    }
+
+    @GetMapping("/all")
+    public Page<Construction> all(PageQuery pageQuery) {
+        return constructionRepo.findAll(toSpecification(pageQuery,Construction.class), toPageRequest(pageQuery));
+    }
+
+    @GetMapping("/get/{id}")
+    public Construction get(@PathVariable Long id) {
+        return constructionRepo.findById(id).orElseThrow(new BusinessException("无记录"));
+    }
+
+    @PostMapping("/del/{id}")
+    public void del(@PathVariable Long id) {
+        constructionRepo.deleteById(id);
+    }
+
+    @GetMapping("/excel")
+    @ResponseBody
+    public void excel(HttpServletResponse response, PageQuery pageQuery) throws IOException {
+        List<Construction> data = all(pageQuery).getContent();
+        ExcelUtils.export(response, data);
+    }
+}
+

+ 62 - 0
src/main/java/com/izouma/zhumj/web/ConstructionProcessController.java

@@ -0,0 +1,62 @@
+package com.izouma.zhumj.web;
+
+import com.izouma.zhumj.domain.ConstructionProcess;
+import com.izouma.zhumj.enums.ProcessStatus;
+import com.izouma.zhumj.service.ConstructionProcessService;
+import com.izouma.zhumj.dto.PageQuery;
+import com.izouma.zhumj.exception.BusinessException;
+import com.izouma.zhumj.repo.ConstructionProcessRepo;
+import com.izouma.zhumj.utils.excel.ExcelUtils;
+import lombok.AllArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+
+@RestController
+@RequestMapping("/constructionProcess")
+@AllArgsConstructor
+public class ConstructionProcessController extends BaseController {
+    private ConstructionProcessService constructionProcessService;
+    private ConstructionProcessRepo    constructionProcessRepo;
+
+    //@PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/save")
+    public ConstructionProcess save(@RequestBody ConstructionProcess record) {
+        if (record.getId() != null) {
+            return constructionProcessService.update(record);
+        }
+        return constructionProcessService.create(record);
+    }
+
+    @GetMapping("/all")
+    public Page<ConstructionProcess> all(PageQuery pageQuery) {
+        return constructionProcessRepo
+                .findAll(toSpecification(pageQuery, ConstructionProcess.class), toPageRequest(pageQuery));
+    }
+
+    @GetMapping("/get/{id}")
+    public ConstructionProcess get(@PathVariable Long id) {
+        return constructionProcessRepo.findById(id).orElseThrow(new BusinessException("无记录"));
+    }
+
+    @PostMapping("/del/{id}")
+    public void del(@PathVariable Long id) {
+        constructionProcessService.del(id);
+    }
+
+    @GetMapping("/excel")
+    @ResponseBody
+    public void excel(HttpServletResponse response, PageQuery pageQuery) throws IOException {
+        List<ConstructionProcess> data = all(pageQuery).getContent();
+        ExcelUtils.export(response, data);
+    }
+
+    @PostMapping("/updateStatus")
+    public ConstructionProcess updateStatus(@RequestParam Long id, @RequestParam ProcessStatus status) {
+        return constructionProcessService.updateStatus(id, status);
+    }
+}
+

+ 66 - 0
src/main/java/com/izouma/zhumj/web/ConstructionProcessLogController.java

@@ -0,0 +1,66 @@
+package com.izouma.zhumj.web;
+
+import com.izouma.zhumj.domain.ConstructionProcessLog;
+import com.izouma.zhumj.service.ConstructionProcessLogService;
+import com.izouma.zhumj.dto.PageQuery;
+import com.izouma.zhumj.exception.BusinessException;
+import com.izouma.zhumj.repo.ConstructionProcessLogRepo;
+import com.izouma.zhumj.utils.ObjUtils;
+import com.izouma.zhumj.utils.excel.ExcelUtils;
+import lombok.AllArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+
+@RestController
+@RequestMapping("/constructionProcessLog")
+@AllArgsConstructor
+public class ConstructionProcessLogController extends BaseController {
+    private ConstructionProcessLogService constructionProcessLogService;
+    private ConstructionProcessLogRepo    constructionProcessLogRepo;
+
+    //@PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/save")
+    public ConstructionProcessLog save(@RequestBody ConstructionProcessLog record) {
+        if (record.getId() != null) {
+            ConstructionProcessLog orig = constructionProcessLogRepo.findById(record.getId())
+                    .orElseThrow(new BusinessException("无记录"));
+            ObjUtils.merge(orig, record);
+            return constructionProcessLogRepo.save(orig);
+        }
+        return constructionProcessLogRepo.save(record);
+    }
+
+    @GetMapping("/all")
+    public Page<ConstructionProcessLog> all(PageQuery pageQuery) {
+        return constructionProcessLogRepo
+                .findAll(toSpecification(pageQuery, ConstructionProcessLog.class), toPageRequest(pageQuery));
+    }
+
+    @GetMapping("/get/{id}")
+    public ConstructionProcessLog get(@PathVariable Long id) {
+        return constructionProcessLogRepo.findById(id).orElseThrow(new BusinessException("无记录"));
+    }
+
+    @PostMapping("/del/{id}")
+    public void del(@PathVariable Long id) {
+        constructionProcessLogRepo.deleteById(id);
+    }
+
+    @GetMapping("/excel")
+    @ResponseBody
+    public void excel(HttpServletResponse response, PageQuery pageQuery) throws IOException {
+        List<ConstructionProcessLog> data = all(pageQuery).getContent();
+        ExcelUtils.export(response, data);
+    }
+
+    @PostMapping("/saveLog")
+    public ConstructionProcessLog saveLog(@RequestParam Long processId, @RequestParam String remark) {
+        return constructionProcessLogService.saveLog(processId, remark);
+    }
+}
+

+ 1 - 0
src/main/resources/genjson/Construction.json

@@ -0,0 +1 @@
+{"tableName":"Construction","className":"Construction","remark":"工程管理","genTable":true,"genClass":true,"genList":true,"genForm":true,"genRouter":true,"javaPath":"/Users/drew/Projects/Java/zhumj/src/main/java/com/izouma/zhumj","viewPath":"/Users/drew/Projects/Java/zhumj/src/main/vue/src/views","routerPath":"/Users/drew/Projects/Java/zhumj/src/main/vue/src","resourcesPath":"/Users/drew/Projects/Java/zhumj/src/main/resources","dataBaseType":"Mysql","fields":[{"name":"storeId","modelName":"storeId","remark":"门店","showInList":true,"showInForm":true,"formType":"select","required":true,"apiFlag":"2","optionsMethod":"/storeInfo/all","optionsValue":"id","optionsLabel":"name"},{"name":"name","modelName":"name","remark":"项目名称","showInList":true,"showInForm":true,"formType":"singleLineText","required":true},{"name":"manager","modelName":"manager","remark":"店长","showInList":true,"showInForm":true,"formType":"singleLineText","required":true},{"name":"supervisor","modelName":"supervisor","remark":"工程监理","showInList":true,"showInForm":true,"formType":"singleLineText","required":true},{"name":"start","modelName":"start","remark":"开工日期","showInList":true,"showInForm":true,"formType":"date","required":true},{"name":"end","modelName":"end","remark":"竣工日期","showInList":true,"showInForm":true,"formType":"date","required":true},{"name":"progress","modelName":"progress","remark":"进度","showInList":true,"showInForm":true,"formType":"number","required":false}],"readTable":false,"dataSourceCode":"dataSource","genJson":"","subtables":[],"update":false,"basePackage":"com.izouma.zhumj","tablePackage":"com.izouma.zhumj.domain.Construction"}

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
src/main/resources/genjson/ConstructionProcess.json


+ 1 - 0
src/main/resources/genjson/ConstructionProcessLog.json

@@ -0,0 +1 @@
+{"tableName":"ConstructionProcessLog","className":"ConstructionProcessLog","remark":"施工纪录","genTable":true,"genClass":true,"genList":true,"genForm":true,"genRouter":true,"javaPath":"/Users/drew/Projects/Java/zhumj/src/main/java/com/izouma/zhumj","viewPath":"/Users/drew/Projects/Java/zhumj/src/main/vue/src/views","routerPath":"/Users/drew/Projects/Java/zhumj/src/main/vue/src","resourcesPath":"/Users/drew/Projects/Java/zhumj/src/main/resources","dataBaseType":"Mysql","fields":[{"name":"constructionId","modelName":"constructionId","remark":"项目","showInList":true,"showInForm":true,"formType":"number"},{"name":"constructionProcessId","modelName":"constructionProcessId","remark":"工序名称","showInList":true,"showInForm":true,"formType":"singleLineText"},{"name":"remark","modelName":"remark","remark":"备注","showInList":true,"showInForm":true,"formType":"singleLineText"},{"name":"time","modelName":"time","remark":"操作时间","showInList":true,"showInForm":true,"formType":"datetime"},{"name":"operator","modelName":"operator","remark":"操作人","showInList":true,"showInForm":true,"formType":"singleLineText"}],"readTable":false,"dataSourceCode":"dataSource","genJson":"","subtables":[],"update":false,"basePackage":"com.izouma.zhumj","tablePackage":"com.izouma.zhumj.domain.ConstructionProcessLog"}

+ 2 - 0
src/main/vue/package.json

@@ -19,6 +19,7 @@
     "clipboard": "^2.0.4",
     "core-js": "^2.6.5",
     "date-fns": "^2.3.0",
+    "dayjs": "^1.10.1",
     "element-ui": "^2.13.1",
     "ip": "^1.1.5",
     "mathjs": "^6.6.0",
@@ -38,6 +39,7 @@
     "vue-i18n": "^8.14.1",
     "vue-router": "^3.0.3",
     "vuex": "^3.0.1",
+    "wl-gantt": "^1.0.5",
     "xlsx": "^0.15.4"
   },
   "devDependencies": {

+ 118 - 0
src/main/vue/src/components/wl-gantt/components/wl-contextmenu.vue

@@ -0,0 +1,118 @@
+<template>
+    <div class="ft-context-menu" v-if="flag" :style="style">
+        <ul class="menu-liet" v-if="useDefault">
+            <li class="menu-item" v-for="(item, index) of menuList" :key="index" @click="handleMenuItem(item)">
+                <div class="memu-item-icon" :class="item.icon"></div>
+                <div class="memu-item-title">{{ item.label }}</div>
+                <div class="memu-item-value">{{ item.value }}</div>
+            </li>
+            <li class="menu-item" v-if="menuList.length === 0" @click="flag = false">
+                <span class="memu-item-title">暂无菜单</span>
+            </li>
+        </ul>
+        <slot />
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'ft-contextmenu',
+    props: {
+        visible: {
+            type: Boolean,
+            default: false
+        }, // 是否打开上下文菜单
+        x: {
+            type: Number,
+            default: 0
+        }, // 菜单打开坐标x轴
+        y: {
+            type: Number,
+            default: 0
+        }, // 菜单打开坐标y轴
+        useDefault: {
+            type: Boolean,
+            default: true
+        }, // 是否使用内置菜单样式
+        menuList: {
+            type: Array,
+            default: () => []
+        } // 使用内置菜单样式是,菜单列表
+    },
+    computed: {
+        flag: {
+            get() {
+                if (this.visible) {
+                    // 注册全局监听事件 [ 目前只考虑鼠标解除触发 ]
+                    window.addEventListener('mousedown', this.watchContextmenu);
+                }
+                return this.visible;
+            },
+            set(newVal) {
+                this.$emit('update:visible', newVal);
+            }
+        },
+        style() {
+            return {
+                left: this.x + 'px',
+                top: this.y + 'px',
+                display: this.visible ? 'block' : 'none '
+            };
+        }
+    },
+    methods: {
+        // 菜单点击事件
+        handleMenuItem(item) {
+            this.$emit('rowClick', item);
+        },
+        watchContextmenu(event) {
+            if (!this.$el.contains(event.target) || event.button !== 0) this.flag = false;
+            window.removeEventListener('mousedown', this.watchContextmenu);
+            return false;
+        }
+    },
+    mounted() {
+        // 将菜单放置到body下
+        document.querySelector('body').appendChild(this.$el);
+    }
+};
+</script>
+
+<style lang="less">
+.ft-context-menu {
+    position: absolute;
+    padding: 5px 0;
+    z-index: 2018;
+    background: #fff;
+    border: 1px solid #cfd7e5;
+    border-radius: 4px;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+
+    .menu-liet {
+        min-width: 260px;
+    }
+    .menu-item {
+        display: flex;
+        padding: 8px 20px 8px 15px;
+        margin: 0;
+        font-size: 14px;
+        color: #606266;
+        cursor: pointer;
+        &:hover {
+            background: #ecf5ff;
+            color: #66b1ff;
+        }
+        .memu-item-icon {
+            font-size: 16px;
+            font-weight: 600;
+        }
+        .memu-item-title {
+            width: 80px;
+            margin-left: 10px;
+        }
+        .memu-item-value {
+            flex: 1;
+        }
+    }
+}
+</style>

+ 280 - 0
src/main/vue/src/components/wl-gantt/css/index.less

@@ -0,0 +1,280 @@
+.wl-gantt .wl-gantt-header > th {
+    text-align: center;
+}
+
+.wl-gantt .h-full {
+    height: 100%;
+}
+
+.wl-gantt .wl-gantt-item {
+    position: relative;
+    transition: all 0.3s;
+}
+
+.wl-gantt .wl-gantt-item > .cell {
+    padding: 0;
+}
+
+.wl-gantt .u-full.el-input {
+    width: 100%;
+}
+
+.wl-gantt .wl-item-on {
+    position: absolute;
+    top: 50%;
+    left: 0;
+    right: -1px;
+    margin-top: -8px;
+    height: 16px;
+    background: @primary;
+    transition: all 0.4s;
+}
+
+.wl-gantt .wl-item-start {
+    left: 50%;
+}
+
+.wl-gantt .wl-item-start:after {
+    position: absolute;
+    top: 16px;
+    left: 0;
+    z-index: 1;
+    content: '';
+    width: 0;
+    height: 0;
+    border-color: @primary transparent transparent;
+    border-width: 6px 6px 6px 0;
+    border-style: solid;
+}
+
+.wl-gantt .wl-item-end {
+    right: 50%;
+}
+
+.wl-gantt .wl-item-end:after {
+    position: absolute;
+    top: 16px;
+    right: 0;
+    z-index: 1;
+    content: '';
+    width: 0;
+    height: 0;
+    border-color: transparent @primary;
+    border-width: 0 6px 6px 0;
+    border-style: solid;
+}
+
+.wl-gantt .wl-item-full {
+    left: 0;
+    right: 0;
+}
+
+.wl-gantt .wl-item-full:before {
+    position: absolute;
+    top: 16px;
+    left: 0;
+    z-index: 1;
+    content: '';
+    width: 0;
+    height: 0;
+    border-color: @primary transparent transparent;
+    border-width: 6px 6px 6px 0;
+    border-style: solid;
+}
+
+.wl-gantt .wl-item-full:after {
+    position: absolute;
+    top: 16px;
+    right: 0;
+    z-index: 1;
+    content: '';
+    width: 0;
+    height: 0;
+    border-color: transparent @primary;
+    border-width: 0 6px 6px 0;
+    border-style: solid;
+}
+
+.wl-gantt .wl-real-on {
+    position: absolute;
+    top: 70%;
+    left: 0;
+    right: -1px;
+    margin-top: -8px;
+    height: 16px;
+    background: #409eff;
+    transition: all 0.4s;
+}
+
+.wl-gantt .wl-real-start {
+    left: 50%;
+}
+
+.wl-gantt .wl-real-start:after {
+    position: absolute;
+    top: 16px;
+    left: 0;
+    z-index: 1;
+    content: '';
+    width: 0;
+    height: 0;
+    border-color: #409eff transparent transparent;
+    border-width: 6px 6px 6px 0;
+    border-style: solid;
+}
+
+.wl-gantt .wl-real-end {
+    right: 50%;
+}
+
+.wl-gantt .wl-real-end:after {
+    position: absolute;
+    top: 16px;
+    right: 0;
+    z-index: 1;
+    content: '';
+    width: 0;
+    height: 0;
+    border-color: transparent #409eff;
+    border-width: 0 6px 6px 0;
+    border-style: solid;
+}
+
+.wl-gantt .wl-real-full {
+    left: 0;
+    right: 0;
+}
+
+.wl-gantt .wl-real-full:before {
+    position: absolute;
+    top: 16px;
+    left: 0;
+    z-index: 1;
+    content: '';
+    width: 0;
+    height: 0;
+    border-color: #409eff transparent transparent;
+    border-width: 6px 6px 6px 0;
+    border-style: solid;
+}
+
+.wl-gantt .wl-real-full:after {
+    position: absolute;
+    top: 16px;
+    right: 0;
+    z-index: 1;
+    content: '';
+    width: 0;
+    height: 0;
+    border-color: transparent #409eff;
+    border-width: 0 6px 6px 0;
+    border-style: solid;
+}
+
+.wl-gantt .name-col {
+    position: relative;
+}
+
+.wl-gantt .name-col:hover .name-col-edit {
+    display: inline-block;
+}
+
+.wl-gantt .name-col .name-col-edit {
+    display: none;
+    position: absolute;
+    right: 0;
+}
+
+.wl-gantt .name-col .name-col-icon {
+    padding: 6px 3px;
+    cursor: pointer;
+    font-size: 16px;
+}
+
+.wl-gantt .name-col .task-remove {
+    color: #f56c6c;
+}
+
+.wl-gantt .name-col .task-add {
+    color: @primary;
+}
+
+.year-and-month .wl-item-start {
+    left: 5%;
+}
+
+.year-and-month .wl-item-start:after {
+    position: absolute;
+    top: 16px;
+    left: 0;
+    z-index: 1;
+    content: '';
+    width: 0;
+    height: 0;
+    border-color: @primary transparent transparent;
+    border-width: 6px 6px 6px 0;
+    border-style: solid;
+}
+
+.year-and-month .wl-item-end {
+    right: 5%;
+}
+
+.year-and-month .wl-item-end:after {
+    position: absolute;
+    top: 16px;
+    right: 0;
+    z-index: 1;
+    content: '';
+    width: 0;
+    height: 0;
+    border-color: transparent @primary;
+    border-width: 0 6px 6px 0;
+    border-style: solid;
+}
+
+.year-and-month .wl-item-full {
+    left: 5%;
+    right: 5%;
+}
+
+.year-and-month .wl-item-full:before {
+    position: absolute;
+    top: 16px;
+    left: 0;
+    z-index: 1;
+    content: '';
+    width: 0;
+    height: 0;
+    border-color: @primary transparent transparent;
+    border-width: 6px 6px 6px 0;
+    border-style: solid;
+}
+
+.year-and-month .wl-item-full:after {
+    position: absolute;
+    top: 16px;
+    right: 0;
+    z-index: 1;
+    content: '';
+    width: 0;
+    height: 0;
+    border-color: transparent @primary;
+    border-width: 0 6px 6px 0;
+    border-style: solid;
+}
+
+.wl-info-card {
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 99;
+    padding: 10px;
+    width: 360px;
+    min-height: 120px;
+    transition: all 0.4s ease-out;
+    transform: translate(-100%, -120%);
+    border-radius: 4px;
+    box-shadow: inset 0px 0px 11px 0px #e6e6e6;
+    background: #fff;
+}

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
src/main/vue/src/components/wl-gantt/css/index.min.css


+ 276 - 0
src/main/vue/src/components/wl-gantt/css/index.scss

@@ -0,0 +1,276 @@
+$gantt_item: 16px;
+$gantt_item_half: 8px;
+
+.wl-gantt {
+  .wl-gantt-header > th {
+    text-align: center;
+  }
+
+  .h-full {
+    height: 100%;
+  }
+
+  .wl-gantt-item {
+    position: relative;
+    transition: all 0.3s;
+    > .cell {
+      padding: 0;
+    }
+  }
+
+  .u-full.el-input {
+    width: 100%;
+  }
+
+  // 计划时间gantt开始
+  .wl-item-on {
+    position: absolute;
+    top: 50%;
+    left: 0;
+    right: -1px;
+    margin-top: -$gantt_item_half;
+    height: $gantt_item;
+    background: #409eff;
+    transition: all 0.4s;
+  }
+
+  .wl-item-start {
+    left: 50%;
+    &:after {
+      position: absolute;
+      top: $gantt_item;
+      left: 0;
+      z-index: 1;
+      content: "";
+      width: 0;
+      height: 0;
+      border-color: #409eff transparent transparent;
+      border-width: 6px 6px 6px 0;
+      border-style: solid;
+    }
+  }
+
+  .wl-item-end {
+    right: 50%;
+    &:after {
+      position: absolute;
+      top: $gantt_item;
+      right: 0;
+      z-index: 1;
+      content: "";
+      width: 0;
+      height: 0;
+      border-color: transparent #409eff;
+      border-width: 0 6px 6px 0;
+      border-style: solid;
+    }
+  }
+
+  .wl-item-full {
+    left: 0;
+    right: 0;
+    &:before {
+      position: absolute;
+      top: $gantt_item;
+      left: 0;
+      z-index: 1;
+      content: "";
+      width: 0;
+      height: 0;
+      border-color: #409eff transparent transparent;
+      border-width: 6px 6px 6px 0;
+      border-style: solid;
+    }
+    &:after {
+      position: absolute;
+      top: $gantt_item;
+      right: 0;
+      z-index: 1;
+      content: "";
+      width: 0;
+      height: 0;
+      border-color: transparent #409eff;
+      border-width: 0 6px 6px 0;
+      border-style: solid;
+    }
+  }
+  // 计划时间gantt结束
+
+  // 实际时间gantt开始
+  .wl-real-on {
+    position: absolute;
+    top: 70%;
+    left: 0;
+    right: -1px;
+    margin-top: -$gantt_item_half;
+    height: $gantt_item;
+    background: #faa792; //rgba(250, 167, 146, .6);
+    transition: all 0.4s;
+  }
+  .wl-real-start {
+    left: 50%;
+    &:after {
+      position: absolute;
+      top: $gantt_item;
+      left: 0;
+      z-index: 1;
+      content: "";
+      width: 0;
+      height: 0;
+      border-color: #faa792 transparent transparent;
+      border-width: 6px 6px 6px 0;
+      border-style: solid;
+    }
+  }
+
+  .wl-real-end {
+    right: 50%;
+    &:after {
+      position: absolute;
+      top: $gantt_item;
+      right: 0;
+      z-index: 1;
+      content: "";
+      width: 0;
+      height: 0;
+      border-color: transparent #faa792;
+      border-width: 0 6px 6px 0;
+      border-style: solid;
+    }
+  }
+
+  .wl-real-full {
+    left: 0;
+    right: 0;
+    &:before {
+      position: absolute;
+      top: $gantt_item;
+      left: 0;
+      z-index: 1;
+      content: "";
+      width: 0;
+      height: 0;
+      border-color: #faa792 transparent transparent;
+      border-width: 6px 6px 6px 0;
+      border-style: solid;
+    }
+    &:after {
+      position: absolute;
+      top: $gantt_item;
+      right: 0;
+      z-index: 1;
+      content: "";
+      width: 0;
+      height: 0;
+      border-color: transparent #faa792;
+      border-width: 0 6px 6px 0;
+      border-style: solid;
+    }
+  }
+  // 实际时间gantt结束
+
+  // 名称列
+  .name-col {
+    position: relative;
+    &:hover .name-col-edit {
+      display: inline-block;
+    }
+
+    .name-col-edit {
+      display: none;
+      position: absolute;
+      right: 0;
+    }
+
+    .name-col-icon {
+      padding: 6px 3px;
+      cursor: pointer;
+      font-size: 16px;
+    }
+
+    .task-remove {
+      color: #f56c6c;
+    }
+    .task-add {
+      color: #409eff;
+    }
+  }
+}
+
+.year-and-month {
+  .wl-item-start {
+    left: 5%;
+    &:after {
+      position: absolute;
+      top: $gantt_item;
+      left: 0;
+      z-index: 1;
+      content: "";
+      width: 0;
+      height: 0;
+      border-color: #409eff transparent transparent;
+      border-width: 6px 6px 6px 0;
+      border-style: solid;
+    }
+  }
+
+  .wl-item-end {
+    right: 5%;
+    &:after {
+      position: absolute;
+      top: $gantt_item;
+      right: 0;
+      z-index: 1;
+      content: "";
+      width: 0;
+      height: 0;
+      border-color: transparent #409eff;
+      border-width: 0 6px 6px 0;
+      border-style: solid;
+    }
+  }
+
+  .wl-item-full {
+    left: 5%;
+    right: 5%;
+    &:before {
+      position: absolute;
+      top: $gantt_item;
+      left: 0;
+      z-index: 1;
+      content: "";
+      width: 0;
+      height: 0;
+      border-color: #409eff transparent transparent;
+      border-width: 6px 6px 6px 0;
+      border-style: solid;
+    }
+    &:after {
+      position: absolute;
+      top: $gantt_item;
+      right: 0;
+      z-index: 1;
+      content: "";
+      width: 0;
+      height: 0;
+      border-color: transparent #409eff;
+      border-width: 0 6px 6px 0;
+      border-style: solid;
+    }
+  }
+}
+
+.wl-info-card {
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 99;
+  padding: 10px;
+  width: 360px;
+  min-height: 120px;
+  transition: all 0.4s ease-out;
+  transform: translate(-100%, -120%);
+  border-radius: 4px;
+  box-shadow: inset 0px 0px 11px 0px #e6e6e6;
+  background: #fff;
+}

+ 7 - 0
src/main/vue/src/components/wl-gantt/index.js

@@ -0,0 +1,7 @@
+import wlGantt from './index.vue';
+
+wlGantt.install = function(Vue) {
+    Vue.component(wlGantt.name, wlGantt);
+};
+
+export default wlGantt;

+ 1478 - 0
src/main/vue/src/components/wl-gantt/index.vue

@@ -0,0 +1,1478 @@
+<template>
+    <div class="wl-gantt" id="wl-gantt">
+        <!-- 甘特图区 -->
+        <el-table
+            ref="wl-gantt"
+            class="wl-gantt-table"
+            :fit="fit"
+            :size="size"
+            :load="load"
+            :lazy="lazy"
+            :border="border"
+            :data="selfData"
+            :stripe="stripe"
+            :height="height"
+            :row-key="rowKey"
+            :row-style="rowStyle"
+            :class="dateTypeClass"
+            :cell-style="cellStyle"
+            :max-height="maxHeight"
+            :tree-props="selfProps"
+            :current-row-key="rowKey"
+            :row-class-name="rowClassName"
+            :cell-class-name="cellClassName"
+            :expand-row-keys="expandRowKeys"
+            :header-row-style="headerRowStyle"
+            :header-cell-style="headerCellStyle"
+            :default-expand-all="defaultExpandAll"
+            :header-row-class-name="headerRowClassName"
+            :highlight-current-row="highlightCurrentRow"
+            :header-cell-class-name="headerCellClassName"
+            @header-contextmenu="handleHeaderContextMenu"
+            @selection-change="handleSelectionChange"
+            @row-contextmenu="handleRowContextMenu"
+            @contextmenu.native="handleContextmenu"
+            @current-change="handleCurrentChange"
+            @cell-mouse-enter="handleMouseEnter"
+            @cell-mouse-leave="handleMouseLeave"
+            @expand-change="handleExpandChange"
+            @filter-change="handleFilterChange"
+            @cell-dblclick="handleCellDbClick"
+            @header-click="handleHeaderClick"
+            @row-dblclick="handleRowDbClick"
+            @sort-change="handleSortChange"
+            @cell-click="handleCellClick"
+            @select-all="handleSelectAll"
+            @row-click="handleRowClick"
+            @select="handleSelect"
+        >
+            <template v-if="!ganttOnly">
+                <slot name="prv"></slot>
+                <el-table-column
+                    v-if="useCheckColumn"
+                    fixed
+                    type="selection"
+                    width="55"
+                    align="center"
+                ></el-table-column>
+                <el-table-column v-if="useIndexColumn" fixed type="index" width="50" label="序号"></el-table-column>
+                <slot></slot>
+            </template>
+            <!-- year and mouth gantt -->
+            <template v-if="self_date_type === 'yearAndMonth'">
+                <el-table-column :resizable="false" v-for="year in ganttTitleDate" :label="year.name" :key="year.id">
+                    <el-table-column
+                        class-name="wl-gantt-item"
+                        v-for="month in year.children"
+                        :resizable="false"
+                        :key="month.id"
+                        :label="month.name"
+                    >
+                        <template slot-scope="scope">
+                            <div :class="dayGanttType(scope.row, month.full_date, 'months')"></div>
+                            <div
+                                v-if="useRealTime"
+                                :class="realDayGanttType(scope.row, month.full_date, 'months')"
+                            ></div>
+                        </template>
+                    </el-table-column>
+                </el-table-column>
+            </template>
+            <!-- year and week gantt -->
+            <template v-else-if="self_date_type === 'yearAndWeek'">
+                <el-table-column :resizable="false" v-for="i in ganttTitleDate" :label="i.full_date" :key="i.id">
+                    <el-table-column
+                        class-name="wl-gantt-item"
+                        v-for="t in i.children"
+                        :resizable="false"
+                        :key="t.id"
+                        :label="t.name"
+                    >
+                        <template slot-scope="scope">
+                            <div :class="dayGanttType(scope.row, t.full_date, 'week')"></div>
+                            <div v-if="useRealTime" :class="realDayGanttType(scope.row, t.full_date, 'week')"></div>
+                        </template>
+                    </el-table-column>
+                </el-table-column>
+            </template>
+            <!-- mouth and day gantt -->
+            <template v-else>
+                <el-table-column :resizable="false" v-for="i in ganttTitleDate" :label="i.full_date" :key="i.id">
+                    <el-table-column
+                        class-name="wl-gantt-item"
+                        v-for="t in i.children"
+                        :resizable="false"
+                        :key="t.id"
+                        :label="t.name"
+                        width="50"
+                    >
+                        <template slot-scope="scope">
+                            <div :class="dayGanttType(scope.row, t.full_date)"></div>
+                            <div v-if="useRealTime" :class="realDayGanttType(scope.row, t.full_date)"></div>
+                        </template>
+                    </el-table-column>
+                </el-table-column>
+            </template>
+        </el-table>
+        <!-- 组件区 -->
+        <!-- 右键菜单 -->
+        <context-menu
+            :visible.sync="contextMenu.show"
+            :x="contextMenu.x"
+            :y="contextMenu.y"
+            :menuList="contextMenu.data"
+        ></context-menu>
+        <!-- hover 看板 -->
+        <div v-show="infoCard.show" class="wl-info-card" :style="infoCardStyle">
+            <slot
+                name="info-card"
+                :row="infoCard.row"
+                :column="infoCard.column"
+                :cell="infoCard.cell"
+                :event="infoCard.event"
+            ></slot>
+        </div>
+    </div>
+</template>
+
+<script>
+import dayjs from 'dayjs'; // 导入日期js
+const uuidv4 = require('uuid/v4'); // 导入uuid生成插件
+import isBetween from 'dayjs/plugin/isBetween';
+dayjs.extend(isBetween);
+import { flattenDeep, getMax, flattenDeepParents, regDeepParents } from 'wl-core'; // 导入数组操作函数
+import ContextMenu from './components/wl-contextmenu';
+
+export default {
+    name: 'WlGantt',
+    components: { ContextMenu },
+    data() {
+        return {
+            self_start_date: '', // 项目开始时间
+            self_end_date: '', // 项目结束时间
+            self_data_list: [], // 一维化后的gantt数据
+            self_date_type: '', // 自身日期类型
+            self_id: 1, // 自增id
+            self_cell_edit: null, // 正在编辑的单元格
+            self_dependent_store: [], // 自身依赖库
+            multipleSelection: [], // 多选数据
+            currentRow: null, // 单选数据
+            pre_options: [], // 可选前置节点
+            name_show_tooltip: true, // 名称列是否开启超出隐藏
+            update: true, // 更新视图
+            selectionList: [], // 多选选中数据
+            contextMenu: {
+                show: false, // 显示
+                x: 0, // 坐标点
+                y: 0, // 坐标点
+                data: [] // 整理后要显示的数据
+            }, // 右键菜单配置项
+            infoCard: {
+                show: false,
+                x: 0,
+                y: 0,
+                row: {},
+                column: {},
+                cell: null,
+                event: {},
+                timer: null
+            } // 看板信息
+        };
+    },
+    props: {
+        useCard: {
+            type: Boolean,
+            default: false
+        }, // 是否使用hover看板
+        /**
+         * @name 右键扩展菜单
+         * @param {String} label 展示名称
+         * @param {String} prop 绑定的字段
+         * @param {String} icon 可选 字体图标class
+         */
+        contextMenuOptions: Array,
+        // gantt数据
+        data: {
+            type: Array,
+            default: () => {
+                return [];
+            }
+        },
+        // 日期类型
+        dateType: {
+            type: String,
+            default: 'yearAndMonth' // monthAndDay,yearAndMonth,yearAndWeek
+        },
+        // 树表配置项
+        props: Object,
+        // 开始日期
+        startDate: {
+            type: [String, Object],
+            required: true
+        },
+        // 结束时间
+        endDate: {
+            type: [String, Object],
+            required: true
+        },
+        // 是否使用实际开始时间、实际结束时间
+        useRealTime: {
+            type: Boolean,
+            default: false
+        },
+        // 是否检查源数据符合规则,默认开启,如果源数据已遵循project规则,可设置为false以提高性能
+        checkSource: {
+            type: Boolean,
+            default: true
+        },
+        // 废弃:反而会因为频繁的判断而影响性能
+        // 是否生成自增id并组成parents来满足树链的查询条件,如果数据本身已有自增id,可设置为false以提高性能
+        // 如果设置为false,则数据内必须包含自增id字段:identityId,parents字段必须以`,`分割。
+        // 字段名可通过props配置,自增id必须唯一并尽可能的短,如1,2,3...,parents应为祖先自增id通过`,`拼接直至父级
+        recordParents: {
+            type: Boolean,
+            default: true
+        },
+        // 是否使用id来作为自增id,如果是请保证id本来就简短的数字型而不是较长的字符串或guid
+        treatIdAsIdentityId: {
+            type: Boolean,
+            default: false
+        },
+        // 自动变化gantt标题日期模式
+        autoGanttDateType: {
+            type: Boolean,
+            default: true
+        },
+        nameFormatter: Function, // 名称列的格式化内容函数
+        // 是否使用内置前置任务列
+        usePreColumn: {
+            type: Boolean,
+            default: false
+        },
+        // 是否使用复选框列
+        useCheckColumn: {
+            type: Boolean,
+            default: false
+        },
+        // 是否使用序号列
+        useIndexColumn: {
+            type: Boolean,
+            default: false
+        },
+        // 是否可编辑
+        edit: {
+            type: Boolean,
+            default: true
+        },
+        // 复选框是否父子关联
+        parentChild: {
+            type: Boolean,
+            default: true
+        },
+        // 是否开启前置任务多选 如果开启多选则pre字段必须是Array,否则可以是Number\String
+        preMultiple: {
+            type: Boolean,
+            default: true
+        },
+        preFormatter: Function, // 前置任务列的格式化内容函数
+        // 空单元格占位符
+        emptyCellText: {
+            type: String,
+            default: '-'
+        },
+        // 多选时,是否可以点击行快速选中复选框
+        /* quickCheck: {
+      type: Boolean,
+      default: false
+    }, */
+        ganttOnly: {
+            type: Boolean,
+            default: false
+        }, // 是否只显示图形
+        // ---------------------------------------------以下为el-table Attributes--------------------------------------------
+        defaultExpandAll: {
+            type: Boolean,
+            default: false
+        }, // 是否全部展开
+        rowKey: {
+            type: String,
+            default: 'id'
+        }, // 必须指定key来渲染树形数据
+        height: [String, Number], // 列表高度
+        maxHeight: [String, Number], // 列表最大高度
+        stripe: {
+            type: Boolean,
+            default: false
+        }, // 是否为斑马纹
+        highlightCurrentRow: {
+            type: Boolean,
+            default: false
+        }, // 是否要高亮当前行
+        border: {
+            type: Boolean,
+            default: true
+        }, // 是否带有纵向边框
+        fit: {
+            type: Boolean,
+            default: true
+        }, // 列的宽度是否自撑开
+        size: String, // Table 的尺寸
+        rowClassName: Function, // 行的 className 的回调方法
+        rowStyle: Function, // 行的 style 的回调方法
+        cellClassName: Function, // 单元格的 className 的回调方法
+        cellStyle: Function, // 单元格的 style 的回调方法
+        headerRowClassName: {
+            type: [Function, String],
+            default: 'wl-gantt-header'
+        }, // 表头行的 className 的回调方法
+        headerRowStyle: [Function, Object], // 表头行的 style 的回调方法
+        headerCellClassName: [Function, String], // 表头单元格的 className 的回调方法
+        headerCellStyle: [Function, Object], // 表头单元格的 style 的回调方法
+        expandRowKeys: Array, // 可以通过该属性设置 Table 目前的展开行
+        lazy: {
+            type: Boolean,
+            default: false
+        }, // 是否懒加载子节点数据
+        load: Function // 加载子节点数据的函数,lazy 为 true 时生效
+        // 是否使用一维数据组成树
+        /* arrayToTree: {
+      type: Boolean,
+      default: false
+    } */
+    },
+    computed: {
+        // 甘特图标题日期分配
+        ganttTitleDate() {
+            // 分解开始和结束日期
+            let start_date_spilt = dayjs(this.self_start_date)
+                .format('YYYY-M-D')
+                .split('-');
+            let end_date_spilt = dayjs(this.self_end_date)
+                .format('YYYY-M-D')
+                .split('-');
+            let start_year = Number(start_date_spilt[0]);
+            let start_mouth = Number(start_date_spilt[1]);
+            let end_year = Number(end_date_spilt[0]);
+            let end_mouth = Number(end_date_spilt[1]);
+            // 自动更新日期类型以适应任务时间范围跨度
+            if (this.autoGanttDateType) {
+                // 计算日期跨度
+                let mouth_diff = this.timeDiffTime(this.self_start_date, this.self_end_date, 'months');
+                if (mouth_diff > 12) {
+                    // 12个月以上的分到yearAndMouth
+                    this.setDataType('yearAndMonth');
+                } else if (mouth_diff > 2) {
+                    // 2个月以上的分到yearAndWeek
+                    this.setDataType('yearAndWeek');
+                } else {
+                    this.setDataType('monthAndDay');
+                }
+            }
+            // 不自动更新日期类型,以dateType固定展示
+            if (this.self_date_type === 'yearAndWeek') {
+                return this.yearAndWeekTitleDate(start_year, start_mouth, end_year, end_mouth);
+            } else if (this.self_date_type === 'monthAndDay') {
+                return this.mouthAndDayTitleDate(start_year, start_mouth, end_year, end_mouth);
+            } else {
+                return this.yearAndMouthTitleDate(start_year, start_mouth, end_year, end_mouth);
+            }
+        },
+        // 数据
+        selfData() {
+            let _data = this.data || [];
+            // 生成一维数据
+            this.setListData();
+            // 处理源数据合法性
+            this.handleData(_data);
+            // 处理前置依赖
+            this.handleDependentStore();
+            return _data;
+        },
+        // 树表配置项
+        selfProps() {
+            return {
+                hasChildren: 'hasChildren', // 字段来指定哪些行是包含子节点
+                children: 'children', // children字段来表示有子节点
+                name: 'name', // 任务名称字段
+                id: 'id', // id字段
+                pid: 'pid', // pid字段
+                startDate: 'startDate', // 开始时间字段
+                realStartDate: 'realStartDate', // 实际开始时间字段
+                endDate: 'endDate', // 结束时间字段
+                realEndDate: 'realEndDate', // 实际结束时间字段
+                identityId: 'identityId',
+                parents: 'parents',
+                pre: 'pre', // 前置任务字段【注意:如果使用recordParents,则pre值应是目标对象的identityId字段(可配置)】
+                ...this.props
+            };
+        },
+        // 根据日期类型改样式
+        dateTypeClass() {
+            if (this.self_date_type === 'yearAndMonth') {
+                return 'year-and-month';
+            } else if (this.self_date_type === 'monthAndDay') {
+                return 'month-and-day';
+            } else if (this.self_date_type === 'yearAndWeek') {
+                return 'year-and-week';
+            }
+            return '';
+        },
+        // 看板样式
+        infoCardStyle() {
+            return {
+                left: this.infoCard.x + 'px',
+                top: this.infoCard.y + 'px'
+            };
+        }
+    },
+    methods: {
+        // 设置dateType
+        setDataType(type) {
+            this.self_date_type = type;
+        },
+        // 生成一维数据
+        setListData() {
+            this.self_data_list = flattenDeep(this.data, this.selfProps.children);
+        },
+        /**
+         * 开始时间改变
+         * row: object 当前行数据
+         */
+        startDateChange(row) {
+            // 如果将开始时间后移,结束时间也应后移
+            let _delay = this.timeIsBefore(row._oldStartDate, row[this.selfProps.startDate]);
+            if (_delay) {
+                row[this.selfProps.endDate] = this.timeAdd(row[this.selfProps.endDate], row._cycle);
+            }
+            // 如果开始早于项目开始,则把项目开始提前
+            let _early_project_start = this.timeIsBefore(row[this.selfProps.startDate], this.self_start_date);
+            if (_early_project_start) {
+                this.self_start_date = row[this.selfProps.startDate];
+            }
+            this.emitTimeChange(row);
+        },
+        /**
+         * 结束时间改变
+         * row: object 当前行数据
+         */
+        endDateChange(row) {
+            this.emitTimeChange(row);
+            // 如果开始晚于结束,提示
+            /* if (
+        this.timeIsBefore(
+          row[this.selfProps.endDate],
+          row[this.selfProps.startDate]
+        )
+      ) {
+        row[this.selfProps.startDate] = row._oldStartDate;
+        this.$message({
+          showClose: true,
+          message: "开始时间不可以晚于结束时间",
+          type: "error"
+        });
+        return;
+      } */
+        },
+        /**
+         * 前置任务改变
+         * row: object 当前行数据
+         */
+        preChange(row) {
+            this.emitTimeChange(row);
+            this.self_cell_edit = null;
+            // 如果开始晚于结束,提示
+            /* if (
+        this.timeIsBefore(
+          row[this.selfProps.endDate],
+          row[this.selfProps.startDate]
+        )
+      ) {
+        row[this.selfProps.startDate] = row._oldStartDate;
+        this.$message({
+          showClose: true,
+          message: "开始时间不可以晚于结束时间",
+          type: "error"
+        });
+        return;
+      } */
+        },
+        /**
+         * 前置任务内容格式化函数
+         * data:[String, Array] 前置任务
+         */
+        preFormat(row) {
+            // 自定义格式化前置任务列函数
+            if (this.preFormatter) {
+                return this.preFormatter(row);
+            }
+            let data = row[this.selfProps.pre];
+            if (!data) return this.emptyCellText;
+            if (Array.isArray(data)) {
+                if (data.length === 0) return this.emptyCellText;
+                let _pre_text = '';
+                data.forEach(i => {
+                    let _act = this.self_data_list.find(t => t[this.selfProps.id] === i);
+                    if (_act) _pre_text += `${_act[this.selfProps.name]},`;
+                });
+                return _pre_text;
+            }
+            let _act = this.self_data_list.find(t => t[this.selfProps.id] === data);
+            return _act ? _act[this.selfProps.name] : this.emptyCellText;
+        },
+        // 前置下拉框失去焦点事件,change会触发blur,如果不延时则chang失效,如果延时则也只是延迟触发,会造成选一次就关闭无法多选
+        /* preEditBlur(){
+      setTimeout(()=>{
+        this.self_cell_edit = null
+      },500)
+    }, */
+        /**
+         * 前置任务编辑
+         */
+        preCellEdit(row, key, ref) {
+            /* let _parents = row._parents.split(","); // 父祖节点不可选
+      let _children = row._all_children.map(i => i._identityId); // 子孙节点不可选
+      let _self = row[this.selfProps.id]; // 自己不可选
+      let _parents_and_children = _children.concat(_parents, [_self]);
+      let filter_options = this.self_data_list.filter(
+        i => !_parents_and_children.some(t => t == i._identityId)
+      );
+      this.pre_options = filter_options; */
+            if (!this.edit) return;
+            this.pre_options = [];
+            this.self_data_list.forEach(i => {
+                if (i[this.selfProps.id] !== row[this.selfProps.id]) {
+                    this.pre_options.push({ ...i, [this.selfProps.children]: null });
+                }
+            });
+            // 再剔除所有前置链涉及到的节点
+            this.deepFindToSelf(row);
+            // 调用单元格编辑
+            this.cellEdit(key, ref);
+        },
+        /**
+         * 找出to为当前元素的form,并将form作为to继续查找
+         * item: Object 当前元素
+         * targets: Array 需要过滤的数据(废弃)
+         */
+        deepFindToSelf(item) {
+            let _parents = item._parents.split(','); // 父祖节点不可选
+            let _children = item._all_children.map(i => i._identityId); // 子孙节点不可选
+            let _parents_and_children = _children.concat(_parents);
+            this.pre_options = this.pre_options.filter(i => !_parents_and_children.some(t => t == i._identityId));
+            this.self_dependent_store.forEach(i => {
+                let _tag = this.preMultiple
+                    ? i.to.some(t => t[this.selfProps.id] === item[this.selfProps.id])
+                    : i.to[this.selfProps.id] === item[this.selfProps.id];
+                if (_tag) {
+                    this.pre_options = this.pre_options.filter(t => t[this.selfProps.id] !== i.form[this.selfProps.id]);
+                    this.deepFindToSelf(i.form);
+                }
+            });
+        },
+        /**
+         * 单元格编辑
+         * key: string 需要操作的单元格key
+         * ref:object 需要获取焦点的dom
+         */
+        cellEdit(key, ref) {
+            if (!this.edit) return;
+            if (ref === 'wl-name') {
+                this.name_show_tooltip = false;
+            }
+            this.self_cell_edit = key;
+            this.$nextTick(() => {
+                this.$refs[ref].focus();
+            });
+        },
+        // 名称编辑事件
+        nameChange(row) {
+            this.self_cell_edit = null;
+            this.name_show_tooltip = true;
+            this.emitNameChange(row);
+        },
+        // 名称列编辑输入框blur事件
+        nameBlur() {
+            this.self_cell_edit = null;
+            this.name_show_tooltip = true;
+        },
+        // 以下是表格-日期-gantt生成函数----------------------------------------生成gantt表格-------------------------------------
+        /**
+         * 年-月模式gantt标题
+         * start_year: 起始年
+         * start_mouth:起始月
+         * end_year:结束年
+         * end_mouth:结束月
+         */
+        yearAndMouthTitleDate(start_year, start_mouth, end_year, end_mouth) {
+            // 日期数据盒子
+            let dates = [
+                {
+                    name: `${start_year}年`,
+                    date: start_year,
+                    id: uuidv4(),
+                    children: []
+                }
+            ];
+            // 处理年份
+            let year_diff = end_year - start_year;
+            // 年间隔小于一年
+            if (year_diff === 0) {
+                let isLeap = this.isLeap(start_year); // 是否闰年
+                let mouths = this.generationMonths(start_year, start_mouth, end_mouth + 1, isLeap, false); // 处理月份
+                dates[0].children = mouths;
+                return dates;
+            }
+            // 处理开始月份
+            let startIsLeap = this.isLeap(start_year);
+            let start_mouths = this.generationMonths(start_year, start_mouth, 13, startIsLeap, false);
+            // 处理结束月份
+            let endIsLeap = this.isLeap(end_year);
+            let end_mouths = this.generationMonths(end_year, 1, end_mouth + 1, endIsLeap, false);
+            // 年间隔等于一年
+            if (year_diff === 1) {
+                dates[0].children = start_mouths;
+                dates.push({
+                    name: `${end_year}年`,
+                    date: end_year,
+                    children: end_mouths,
+                    id: uuidv4()
+                });
+                return dates;
+            }
+            // 年间隔大于1年
+            if (year_diff > 1) {
+                dates[0].children = start_mouths;
+                for (let i = 1; i < year_diff; i++) {
+                    let item_year = start_year + i;
+                    let isLeap = this.isLeap(item_year);
+                    let month_and_day = this.generationMonths(item_year, 1, 13, isLeap, false);
+                    dates.push({
+                        name: `${item_year}年`,
+                        date: item_year,
+                        id: uuidv4(),
+                        children: month_and_day
+                    });
+                }
+                dates.push({
+                    name: `${end_year}年`,
+                    date: end_year,
+                    children: end_mouths,
+                    id: uuidv4()
+                });
+                return dates;
+            }
+        },
+        /**
+         * 年-周模式gantt标题
+         * start_year: 起始年
+         * start_mouth:起始月
+         * end_year:结束年
+         * end_mouth:结束月
+         */
+        yearAndWeekTitleDate(start_year, start_mouth, end_year, end_mouth) {
+            // 处理年份
+            let year_diff = end_year - start_year;
+            // 只存在同年或前后年的情况
+            if (year_diff === 0) {
+                // 年间隔为同一年
+                let isLeap = this.isLeap(start_year); // 是否闰年
+                let mouths = this.generationMonths(start_year, start_mouth, end_mouth + 1, isLeap, true, true); // 处理月份
+                return mouths;
+            }
+            // 处理开始月份
+            let startIsLeap = this.isLeap(start_year);
+            let start_mouths = this.generationMonths(start_year, start_mouth, 13, startIsLeap, true, true);
+            // 处理结束月份
+            let endIsLeap = this.isLeap(end_year);
+            let end_mouths = this.generationMonths(end_year, 1, end_mouth + 1, endIsLeap, true, true);
+            return start_mouths.concat(end_mouths);
+        },
+        /**
+         * 月-日模式gantt标题
+         * start_year: 起始年
+         * start_mouth:起始月
+         * end_year:结束年
+         * end_mouth:结束月
+         */
+        mouthAndDayTitleDate(start_year, start_mouth, end_year, end_mouth) {
+            // 处理年份
+            let year_diff = end_year - start_year;
+            // 只存在同年或前后年的情况
+            if (year_diff === 0) {
+                // 年间隔为同一年
+                let isLeap = this.isLeap(start_year); // 是否闰年
+                let mouths = this.generationMonths(start_year, start_mouth, end_mouth + 1, isLeap); // 处理月份
+                return mouths;
+            }
+            // 处理开始月份
+            let startIsLeap = this.isLeap(start_year);
+            let start_mouths = this.generationMonths(start_year, start_mouth, 13, startIsLeap);
+            // 处理结束月份
+            let endIsLeap = this.isLeap(end_year);
+            let end_mouths = this.generationMonths(end_year, 1, end_mouth + 1, endIsLeap);
+            return start_mouths.concat(end_mouths);
+        },
+        /**
+         * 生成月份函数
+         * year: Number 当前年份
+         * start_num: Number 开始月分
+         * end_num:Number 结束月份
+         * isLeap: Boolean 是否闰年
+         * insert_days: Boolean 是否需要插入 日
+         * week: 是否以周的间隔
+         */
+        generationMonths(year, start_num = 1, end_num = 13, isLeap = false, insert_days = true, week = false) {
+            let months = [];
+            if (insert_days) {
+                // 无需 日 的模式
+                for (let i = start_num; i < end_num; i++) {
+                    // 需要 日 的模式
+                    let days = this.generationDays(year, i, isLeap, week);
+                    months.push({
+                        name: `${i}月`,
+                        date: i,
+                        full_date: `${year}-${i}`,
+                        children: days,
+                        id: uuidv4()
+                    });
+                }
+                return months;
+            }
+            for (let i = start_num; i < end_num; i++) {
+                // 需要 日 的模式
+                months.push({
+                    name: `${i}月`,
+                    date: i,
+                    full_date: `${year}-${i}`,
+                    id: uuidv4()
+                });
+            }
+            return months;
+        },
+        /**
+         * 生成日期函数
+         * year: Number 当前年份
+         * month: Number 当前月份
+         * isLeap: Boolean 是否闰年
+         * week: Boolean 是否间隔一周
+         */
+        generationDays(year, month, isLeap = false, week = false) {
+            let big_month = [1, 3, 5, 7, 8, 10, 12].includes(month);
+            let small_month = [4, 6, 9, 11].includes(month);
+            let dates_num = big_month ? 32 : small_month ? 31 : isLeap ? 30 : 29;
+            let days = [];
+            if (week) {
+                let _day = 1; // 从周日开始
+                let _start_day_inweek = this.timeInWeek(`${year}-${month}-1`);
+                if (_start_day_inweek !== 0) {
+                    _day = 8 - _start_day_inweek;
+                }
+                for (let i = _day; i < dates_num; i += 7) {
+                    days.push({
+                        date: i,
+                        name: `${i}日`,
+                        id: uuidv4(),
+                        full_date: `${year}-${month}-${i}`
+                    });
+                }
+            } else {
+                for (let i = 1; i < dates_num; i++) {
+                    days.push({
+                        date: i,
+                        name: `${i}日`,
+                        id: uuidv4(),
+                        full_date: `${year}-${month}-${i}`
+                    });
+                }
+            }
+            return days;
+        },
+        /**
+         * 是否闰年函数
+         * year: Number 当前年份
+         */
+        isLeap(year) {
+            return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
+        },
+        /**
+         * 当前日期gantt状态
+         * row: object 当前行信息
+         * date: string 当前格子日期
+         * unit: string 时间单位,以天、月、年计算
+         */
+        dayGanttType(row, date, unit = 'days') {
+            let start_date = row[this.selfProps.startDate];
+            let end_date = row[this.selfProps.endDate];
+            let between = dayjs(date).isBetween(start_date, end_date, unit);
+            if (between) {
+                return 'wl-item-on';
+            }
+            let start = dayjs(start_date).isSame(date, unit);
+            let end = dayjs(end_date).isSame(date, unit);
+            if (start && end) {
+                return 'wl-item-on wl-item-full';
+            }
+            if (start) {
+                return 'wl-item-on wl-item-start';
+            }
+            if (end) {
+                return 'wl-item-on wl-item-end';
+            }
+        },
+        /**
+         * 实际日期gantt状态
+         * row: object 当前行信息
+         * date: string 当前格子日期
+         * unit: string 时间单位,以天、月、年计算
+         */
+        realDayGanttType(row, date, unit = 'days') {
+            let start_date = row[this.selfProps.realStartDate];
+            let end_date = row[this.selfProps.realEndDate];
+            if (!start_date || !end_date) {
+                return '';
+            }
+            let between = dayjs(date).isBetween(start_date, end_date, unit);
+            if (between) {
+                return 'wl-real-on';
+            }
+            let start = dayjs(start_date).isSame(date, unit);
+            let end = dayjs(end_date).isSame(date, unit);
+            if (start && end) {
+                return 'wl-real-on wl-real-full';
+            }
+            if (start) {
+                return 'wl-real-on wl-real-start';
+            }
+            if (end) {
+                return 'wl-real-on wl-real-end';
+            }
+        },
+        // 以下是时间计算类函数 ------------------------------------------------------时间计算---------------------------------------
+        /**
+         * 计算时差
+         * startDate:开始时间
+         * endDate:结束时间
+         * unit:单位 days、months、yesrs
+         */
+        timeDiffTime(startDate, endDate, unit = 'days') {
+            return dayjs(endDate).diff(startDate, unit);
+        },
+        /**
+         * 比较时间,是否之前
+         * startDate:开始时间
+         * endDate:结束时间
+         * unit:单位 days、months、yesrs
+         */
+        timeIsBefore(startDate, endDate, unit = 'days') {
+            return dayjs(startDate).isBefore(endDate, unit);
+        },
+        /**
+         * 时间加计算函数
+         * date:原时间
+         * num:需要增加的时间数量
+         * nuit:增加时间的单位 day year
+         */
+        timeAdd(date, num = 1, nuit = 'day', format = 'YYYY-MM-DD') {
+            return dayjs(date)
+                .add(num, nuit)
+                .format(format);
+        },
+        /**
+         * 时间格式化函数
+         * date 需要格式化的数据
+         * format 格式化的格式
+         */
+        timeFormat(date, format = 'YYYY-MM-DD') {
+            return date ? dayjs(date).format(format) : this.emptyCellText;
+        },
+        /**
+         * 查询时间是周几
+         */
+        timeInWeek(date) {
+            return dayjs(date).day();
+        },
+        // 以下为输出数据函数 --------------------------------------------------------------输出数据------------------------------------
+        // 删除任务
+        emitTaskRemove(item) {
+            this.$emit('taskRemove', item);
+        },
+        // 添加任务
+        emitTaskAdd(item) {
+            this.$emit('taskAdd', item);
+        },
+        // 任务名称更改
+        emitNameChange(item) {
+            this.$emit('nameChange', item);
+        },
+        // 任务时间更改
+        emitTimeChange(item) {
+            this.$emit('timeChange', item);
+            this.$nextTick(() => {
+                this.$set(item, '_oldStartDate', item[this.selfProps.startDate]);
+                this.$set(item, '_oldEndDate', item[this.selfProps.endDate]);
+            });
+        },
+        /**
+         * 前置任务更改
+         * item: Object 发生更改的行数据
+         * oldval: [String, Array] 修改前数据
+         * handle: Boolean true为操作选择框修改 false为源数据不符合规范的修正更改
+         */
+        emitPreChange(item, oldval, handle = false) {
+            this.$emit('preChange', item, oldval, handle);
+        },
+        // 处理外部数据 ---------------------------------------------------------------原始数据处理-------------------------------------
+        handleData(data, parent = null, level = 0) {
+            level++;
+            data.forEach(i => {
+                this.$set(i, '_parent', parent); // 添加父级字段
+                this.$set(i, '_level', level); // 添加层级字段
+                if (!i._oldStartDate) {
+                    this.$set(i, '_oldStartDate', i[this.selfProps.startDate]);
+                }
+                if (!i._oldEndDate) {
+                    this.$set(i, '_oldEndDate', i[this.selfProps.endDate]);
+                }
+                // 当结束时间早于开始时间时,自动处理结束时间为开始时间延后一天
+                let _end_early_start = this.timeIsBefore(i[this.selfProps.endDate], i[this.selfProps.startDate]);
+                if (_end_early_start) {
+                    this.$set(i, this.selfProps.endDate, i[this.selfProps.startDate]);
+                    this.$set(i, '_cycle', 1); // 添加工期字段
+                    this.emitTimeChange(i); // 将发生时间更新的数据输出
+                } else {
+                    let _time_diff = this.timeDiffTime(i[this.selfProps.startDate], i[this.selfProps.endDate]);
+                    this.$set(i, '_cycle', _time_diff + 1); // 添加工期字段
+                } // 添加工期字段
+                // 添加自增id字段及树链组成的parents字段
+                this.recordIdentityIdAndParents(i);
+                // 处理前置任务
+                // this.handlePreTask(i);
+                // 如果当前节点的开始时间早于父节点的开始时间,则将开始时间与父节点相同
+                this.parentStartDateToChild(i);
+                // 校验结束时间是否晚于子节点,如不则将节点结束时间改为最晚子节点
+                this.childEndDateToParent(i);
+                if (Array.isArray(i[this.selfProps.children])) {
+                    this.$set(i, '_isLeaf', false); // 添加是否叶子节点字段
+                    let _all_children = flattenDeep(i[this.selfProps.children], this.selfProps.children);
+                    this.$set(i, '_all_children', _all_children); // 添加全部子节点字段
+                    this.handleData(i[this.selfProps.children], i, level);
+                } else {
+                    this.$set(i, '_isLeaf', true); // 添加是否叶子节点字段
+                    this.$set(i, '_all_children', []); // 添加全部子节点字段
+                }
+            });
+        },
+        // 取父节点开始时间给早于父节点开始时间的子节点
+        parentStartDateToChild(item) {
+            if (!item._parent) return;
+            // 如果子节点时间早于父节点,则将子节点开始时间后移至父节点开始时间,并将结束时间平移【即工期不变】
+            let _child_early_parent = this.timeIsBefore(
+                item[this.selfProps.startDate],
+                item._parent[this.selfProps.startDate]
+            );
+            if (_child_early_parent) {
+                // 修正子节点开始时间
+                this.$set(item, this.selfProps.startDate, item._parent[this.selfProps.startDate]);
+                // 修正子节点结束时间
+                let _to_endDate = this.timeAdd(item[this.selfProps.startDate], item._cycle);
+                this.$set(item, this.selfProps.endDate, _to_endDate);
+                this.emitTimeChange(item); // 将发生时间更新的数据输出
+            }
+        },
+        // 取数组结束时间最大值,如果最大值比父级结束时间大,更新父级结束时间
+        childEndDateToParent(item) {
+            if (!Array.isArray(item[this.selfProps.children])) return;
+            let _child_max = getMax(item[this.selfProps.children], this.selfProps.endDate, true); // 取子节点中最晚的结束时间
+            let _parent_end = dayjs(item[this.selfProps.endDate]).valueOf();
+            if (_child_max > _parent_end) {
+                // 如果子节点结束时间比父节点晚,则将父节点结束时间退后
+                this.$set(item, this.selfProps.endDate, this.timeFormat(_child_max));
+                this.emitTimeChange(item); // 将发生时间更新的数据输出
+            }
+        },
+        // 处理前置任务节点    /// ---- 此使前置任务校验处理还没开始,因此出错,前置处理后手动调用vue视图更新试试
+        handlePreTask(item) {
+            // 统一在一维化数据中处理前置任务
+            let _pre_target = this.self_dependent_store.find(
+                i => i.form[this.selfProps.id] === item[this.selfProps.id]
+            );
+            if (!_pre_target) return;
+            let _pre_end_date = this.preMultiple
+                ? getMax(_pre_target.to, this.selfProps.endDate, true) // 取前置点中最晚的结束时间
+                : _pre_target.to[this.selfProps.endDate];
+            /* 在数据循环中处理前置
+      let pres = item[this.selfProps.pre];
+      if(!pres) return;
+      let _pre_target = null, _pre_end_date = null;
+      if(this.preMultiple){
+        if(!Array.isArray(pres) || pres.length ===0) return;
+        _pre_target = this.self_data_list.filter(i => pres.includes(i[this.selfProps.id]));
+        _pre_end_date = getMax(_pre_target, this.selfProps.endDate, true);
+      }else{
+        _pre_target = this.self_data_list.find(i => i[this.selfProps.id] === pres);
+        if(!_pre_target) return;
+        _pre_end_date = _pre_target[this.selfProps.endDate]
+      } */
+            // 查看是否需要根据前置时间,如果不符合规则,更新后置时间
+            let _start_early_prvend = this.timeIsBefore(item[this.selfProps.startDate], _pre_end_date);
+            if (_start_early_prvend) {
+                let _cycle = item._cycle - 1;
+                let _to_startDate = this.timeAdd(_pre_end_date, 1);
+                let _to_endDate = this.timeAdd(_to_startDate, _cycle);
+                this.$set(item, this.selfProps.startDate, _to_startDate);
+                this.$set(item, this.selfProps.endDate, _to_endDate);
+            }
+        },
+        /**
+         * 检查前置任务合法性
+         * !!已废弃:改为从一维数据列收集form、to并校验,不再在递归中检查 -> handleDependentStore
+         */
+        checkPreTaskValidity(item) {
+            // 没有前置任务退出
+            if (!item[this.selfProps.pre]) return false;
+            // 多前置任务模式
+            if (this.preMultiple) {
+                let _pres = item[this.selfProps.pre];
+                // 不是数组退出
+                if (!Array.isArray(_pres)) {
+                    this.emitPreChange(item, item[this.selfProps.pre]);
+                    this.$set(item, this.selfProps.pre, []);
+                    return false;
+                }
+                // 数组为空退出
+                if (_pres.length === 0) return false;
+                // 前置任务有自己时,剔除自己
+                let _net_self_pres = _pres.filter(i => i !== item[this.selfProps.id]);
+                if (_net_self_pres.length !== _pres.length) {
+                    this.emitPreChange(item, item[this.selfProps.pre]);
+                    this.$set(item, this.selfProps.pre, _net_self_pres);
+                }
+                // 剔除前置任务找不到目标数据的元素
+                let _pre_exist = _net_self_pres.filter(i => this.targetInAllData(i));
+                if (_pre_exist.length !== _net_self_pres.length) {
+                    this.emitPreChange(item, item[this.selfProps.pre]);
+                    this.$set(item, this.selfProps.pre, _pre_exist);
+                }
+                let _no_par_chi = []; // 声明非父、祖、子、孙节点的盒子
+                for (let i of _pre_exist) {
+                    let _pre_target = this.self_data_list.find(t => t[this.selfProps.id] === i);
+                    if (!_pre_target) continue;
+                    let _pre_par_chi = this.targetInParentsOrChildren(item, _pre_target);
+                    _pre_par_chi || _no_par_chi.push(i);
+                }
+                // 前置任务是自己的父祖或子孙节点, 剔除此前置
+                if (_no_par_chi.length !== _pre_exist.length) {
+                    this.emitPreChange(item, item[this.selfProps.pre]);
+                    this.$set(item, this.selfProps.pre, _no_par_chi);
+                }
+                // 处理前置任务链链中产生了回环【A->B,B->C,C->D,D->B】即前置链中形成了相互前置的节点,剔除其错误前置数据
+                this.targetLinkLoopback(item);
+                return true;
+            }
+            // 单前置任务模式
+            if (item[this.selfProps.pre] === item[this.selfProps.id]) {
+                this.$set(item, this.selfProps.pre, null);
+                return false;
+            } // 前置任务是自己退出
+            // 找到前置目标节点
+            let _pre_target = this.self_data_list.find(i => i[this.selfProps.id] == item[this.selfProps.pre]);
+            // 没找到前置任务节点数据退出
+            if (!_pre_target) {
+                this.$set(item, this.selfProps.pre, null);
+                return false;
+            }
+            // 前置任务是自己的父祖或子孙节点退出
+            let is_pre_standard = this.targetInParentsOrChildren(item, _pre_target);
+            if (is_pre_standard) {
+                this.$set(item, this.selfProps.pre, null);
+                return false;
+            }
+            // 处理前置任务链链中产生了回环【A->B,B->C,C->D,D->B】即前置链中形成了相互前置的节点,剔除其错误前置数据
+            this.targetLinkLoopback(item);
+            return true;
+        },
+        // 处理数据生成自增id和树链parents
+        recordIdentityIdAndParents(item) {
+            // if (!this.recordParents) return;
+            if (this.treatIdAsIdentityId) {
+                let _parents = item._parent ? item._parent._parents + ',' + item._parent[this.selfProps.id] : '';
+                this.$set(item, '_parents', _parents);
+                this.$set(item, '_identityId', item[this.selfProps.id]);
+                return;
+            }
+            // 添加自增id
+            this.$set(item, '_identityId', this.self_id);
+            this.self_id++;
+            // 添加parents字段
+            let _parents = item._parent ? item._parent._parents + ',' + item._parent._identityId : '';
+            this.$set(item, '_parents', _parents);
+        },
+        /**
+         * 查询目标是否在父级链或者全部子集中
+         * item 当前节点
+         * pre 前置节点
+         */
+        targetInParentsOrChildren(item, pre) {
+            let _parents = item._parents.split(',');
+            let _children = item._all_children.map(i => i._identityId);
+            return _children.concat(_parents).some(i => i == pre._identityId);
+        },
+        // 查询目标节点是否在数据中存在,并返回数据
+        targetInAllData(target_id) {
+            return this.self_data_list.find(i => i[this.selfProps.id] === target_id);
+        },
+        /**
+         * 处理前置任务链链中产生了回环【A->B,B->C,C->D,D->B】即前置链中形成了相互前置的节点,剔除其错误前置数据
+         * item: Object 当前节点数据
+         * pre_tesk: Array 前置链上所有id
+         * !!已废弃:下方尝试改成form to结构收集起来处理,不再循环中反复循环处理 -> terseTargetLinkLoopback
+         */
+        targetLinkLoopback(item, pre_tesk = []) {
+            pre_tesk.push(item[this.selfProps.id]);
+            let _pres = item[this.selfProps.pre];
+            let _legal_pres = _pres.filter(i => !pre_tesk.includes(i));
+            if (this.preMultiple) {
+                if (_legal_pres.length !== _pres.length) {
+                    this.emitPreChange(item, item[this.selfProps.pre]);
+                    this.$set(item, this.selfProps.pre, _legal_pres);
+                }
+                _legal_pres.forEach(i => {
+                    let _pre_target = this.self_data_list.find(t => t[this.selfProps.id] === i);
+                    if (
+                        _pre_target &&
+                        Array.isArray(_pre_target[this.selfProps.pre]) &&
+                        _pre_target[this.selfProps.pre].length > 0
+                    ) {
+                        this.targetLinkLoopback(_pre_target, pre_tesk);
+                    }
+                });
+            } else {
+                if (pre_tesk.includes(_pres)) {
+                    this.emitPreChange(item, item[this.selfProps.pre]);
+                    this.$set(item, this.selfProps.pre, _legal_pres);
+                }
+                let _pre_target = this.self_data_list.find(t => t[this.selfProps.id] === item[this.selfProps.id]);
+                if (_pre_target) {
+                    this.targetLinkLoopback(_pre_target, pre_tesk);
+                }
+            }
+        },
+        /**
+         * 处理前置任务链链中产生了回环【A->B,B->C,C->D,D->B】即前置链中形成了相互前置的节点,剔除其错误前置数据
+         * item: Object 当前节点数据
+         * flow_pre_tesk: Array 前置链上所有id
+         */
+        terseTargetLinkLoopback(item, flow_pre_tesk = []) {
+            flow_pre_tesk.push(item.form[this.selfProps.id]);
+            if (this.preMultiple) {
+                let _legal_pre = [], // 收集合法数据
+                    _next_form = []; // 收集所有前置的前置
+                for (let i of item.to) {
+                    let _to_id = i[this.selfProps.id];
+                    if (flow_pre_tesk.includes(_to_id)) continue;
+                    _legal_pre.push(_to_id);
+                    flow_pre_tesk.push(_to_id);
+                    let _store_next_form = this.self_dependent_store.filter(t => t.form[this.selfProps.id] === _to_id);
+                    _next_form = _next_form.concat(_store_next_form);
+                }
+                // 剔除不合法前置
+                if (_legal_pre.length !== item.to.length) {
+                    this.emitPreChange(item.form, item.form[this.selfProps.pre]);
+                    this.$set(item.form, this.selfProps.pre, _legal_pre);
+                }
+                // 向前置的前置递归
+                _next_form.forEach(t => {
+                    this.terseTargetLinkLoopback(t, flow_pre_tesk);
+                });
+            } else {
+                let _to_id = item.to[this.selfProps.id];
+                if (flow_pre_tesk.includes(_to_id)) {
+                    this.emitPreChange(item.form, item.form[this.selfProps.pre]);
+                    this.$set(item.form, this.selfProps.pre, null);
+                    return;
+                }
+                let _next_form = this.self_dependent_store.find(t => t.form[this.selfProps.id] === _to_id);
+                if (!_next_form) return;
+                this.terseTargetLinkLoopback(_next_form, flow_pre_tesk);
+            }
+        },
+        // 简洁处理数据
+        terseHandleData(data, parent = null, level = 0) {
+            level++;
+            data.forEach(i => {
+                this.$set(i, '_parent', parent); // 添加父级字段
+                this.$set(i, '_level', level); // 添加层级字段
+                let _time_diff = this.timeDiffTime(i[this.selfProps.startDate], i[this.selfProps.endDate]);
+                i._cycle = _time_diff + 1;
+                if (!i._oldStartDate) {
+                    // 添加开始时间字段
+                    this.$set(i, '_oldStartDate', i[this.selfProps.startDate]);
+                }
+                if (!i._oldEndDate) {
+                    // 添加结束字段时间
+                    this.$set(i, '_oldEndDate', i[this.selfProps.endDate]);
+                }
+                // 添加自增id字段及树链组成的parents字段
+                this.recordIdentityIdAndParents(i);
+                if (Array.isArray(i[this.selfProps.children])) {
+                    this.$set(i, '_isLeaf', false); // 添加是否叶子节点字段
+                    let _all_children = flattenDeep(i[this.selfProps.children], this.selfProps.children);
+                    this.$set(i, '_all_children', _all_children); // 添加全部子节点字段
+                    this.terseHandleData(i[this.selfProps.children], i, level);
+                } else {
+                    this.$set(i, '_isLeaf', true); // 添加是否叶子节点字段
+                }
+                // 处理前置任务
+                // this.handlePreTask(i);
+            });
+        },
+        // 生成前置依赖库, 校验前置合法性并剔除不合法数据
+        handleDependentStore() {
+            this.self_dependent_store = [];
+            // 多选前置模式
+            if (this.preMultiple) {
+                for (let i of this.self_data_list) {
+                    let _pres = i[this.selfProps.pre];
+                    if (!_pres) continue;
+                    // 不是数组退出
+                    if (!Array.isArray(_pres)) {
+                        this.emitPreChange(i, i[this.selfProps.pre]);
+                        this.$set(i, this.selfProps.pre, []);
+                        continue;
+                    }
+                    // 数组为空退出
+                    if (_pres.length === 0) continue;
+                    // 查询不到数据的不收集,是父、祖、子、孙节点的不收集
+                    let _pre_exist_node = [],
+                        _pre_exist_id = [];
+                    for (let t of _pres) {
+                        let target_node = this.targetInAllData(t);
+                        if (!target_node) continue; // 查询不到数据的不收集
+                        let in_per_chi = this.targetInParentsOrChildren(i, target_node);
+                        if (in_per_chi) continue; // 是父、祖、子、孙节点的不收集
+                        _pre_exist_node.push(target_node);
+                        _pre_exist_id.push(target_node[this.selfProps.id]);
+                    }
+                    if (_pre_exist_node.length !== _pres.length) {
+                        this.emitPreChange(i, i[this.selfProps.pre]);
+                        this.$set(i, this.selfProps.pre, _pre_exist_id);
+                    }
+                    this.self_dependent_store.push({
+                        form: i,
+                        to: _pre_exist_node
+                    });
+                }
+            } else {
+                // 单选前置模式
+                for (let i of this.self_data_list) {
+                    if (!i[this.selfProps.pre]) continue;
+                    let _pre_target = this.targetInAllData(i[this.selfProps.pre]);
+                    // 处理前置任务找不到的情况
+                    if (!_pre_target) {
+                        this.emitPreChange(i, i[this.selfProps.pre]);
+                        this.$set(i, this.selfProps.pre, null);
+                        continue;
+                    }
+                    // 处理前置任务是父祖子孙节点的情况
+                    let in_per_chi = this.targetInParentsOrChildren(i, _pre_target);
+                    if (in_per_chi) {
+                        this.emitPreChange(i, i[this.selfProps.pre]);
+                        this.$set(i, this.selfProps.pre, null);
+                        continue;
+                    }
+                    this.self_dependent_store.push({
+                        form: i,
+                        to: _pre_target
+                    });
+                }
+            }
+            // 处理合格前置任务
+            this.self_dependent_store.forEach(i => {
+                this.terseTargetLinkLoopback(i);
+            });
+            // 处理前置依赖
+            this.self_data_list.forEach(i => {
+                this.handlePreTask(i);
+            });
+            // 暂时强制更新视图
+            if (this.update) {
+                this.update = false;
+                this.selfData.sort();
+            }
+        },
+        // 父子关联
+        tableSelect(val, row) {
+            if (!this.parentChild) return;
+            // 选中
+            if (val.some(item => item[this.selfProps.id] == row[this.selfProps.id])) {
+                // 父元素选中全选所有子孙元素
+                // for (let item of val) {
+                row._all_children.forEach(i => {
+                    this.$refs['wl-gantt'].toggleRowSelection(i, true);
+                });
+                // }
+                // 子元素全选向上查找所有满足条件的祖先元素
+                regDeepParents(row, '_parent', parents => {
+                    let reg =
+                        parents &&
+                        parents[this.selfProps.children].every(item => {
+                            return val.some(it => it[this.selfProps.id] == item[this.selfProps.id]);
+                        });
+                    if (reg) this.$refs['wl-gantt'].toggleRowSelection(parents, true);
+                });
+            } else {
+                // 非选中将所有子孙元素及支线上祖先元素清除
+                let cancel_data = [...row._all_children, ...flattenDeepParents([row], '_parent')];
+                for (let item of cancel_data) {
+                    this.$refs['wl-gantt'].toggleRowSelection(item, false);
+                }
+            }
+        },
+        // el-table事件----------------------------------------------以下为原el-table事件输出------------------------------------------------
+        handleSelectionChange(val) {
+            this.$emit('selection-change', val);
+            this.multipleSelection = val;
+        }, // 当选择项发生变化时会触发该事件
+        handleCurrentChange(val, oldVal) {
+            this.$emit('current-change', val, oldVal);
+            this.currentRow = val;
+        }, // 当表格的当前行发生变化的时候会触发该事件
+        handleSelectAll(val) {
+            let is_check = val.length > 0;
+            this.self_data_list.forEach(i => {
+                this.$refs['wl-gantt'].toggleRowSelection(i, is_check);
+            });
+            this.$emit('select-all', val);
+        }, // 当用户手动勾选全选 Checkbox 时触发的事件
+        handleSelect(selection, row) {
+            this.tableSelect(selection, row);
+            let _is_add = selection.some(i => i[this.rowKey] === row[this.rowKey]);
+            this.selectionList = selection;
+            this.$emit('select', selection, row, _is_add);
+        }, // 当用户手动勾选全选 Checkbox 时触发的事件
+        handleMouseEnter(row, column, cell, event) {
+            if (this.useCard) {
+                this.infoCard.show = true;
+                this.infoCard.x = event.screenX;
+                this.infoCard.y = event.screenY;
+                this.infoCard.row = { ...row };
+                this.infoCard.column = column;
+                this.infoCard.cell = cell;
+                this.infoCard.event = event;
+                this.infoCard.timer && clearTimeout(this.infoCard.timer);
+            }
+            this.$emit('cell-mouse-enter', row, column, cell, event);
+        }, // 当单元格 hover 进入时会触发该事件
+        handleMouseLeave(row, column, cell, event) {
+            if (this.useCard) {
+                this.infoCard.timer = setTimeout(() => {
+                    this.infoCard.show = false;
+                    clearTimeout(this.infoCard.timer);
+                    this.infoCard.timer = null;
+                }, 500);
+            }
+            this.$emit('cell-mouse-leave', row, column, cell, event);
+        }, // 当单元格 hover 退出时会触发该事件
+        handleCellClick(row, column, cell, event) {
+            this.$emit('cell-click', row, column, cell, event);
+        }, // 当某个单元格被点击时会触发该事件
+        handleCellDbClick(row, column, cell, event) {
+            this.$emit('cell-dblclick', row, column, cell, event);
+        }, // 当某个单元格被双击击时会触发该事件
+        handleRowClick(row, column, event) {
+            /* if (this.useCheckColumn && this.quickCheck) {
+        let is_check = this.selectionList.some(
+          i => i[this.rowKey] == row[this.rowKey]
+        );
+        this.$refs["wl-gantt"].toggleRowSelection(row, !is_check);
+        this.$nextTick(() => {
+          this.handleSelect(this.selectionList, row, !is_check);
+        });
+      } */
+            this.$emit('row-click', row, column, event);
+        }, // 当某一行被点击时会触发该事件
+        handleRowContextMenu(row, column, event) {
+            this.$emit('row-contextmenu', row, column, event);
+            // 处理右键菜单浮窗
+            if (!Array.isArray(this.contextMenuOptions)) return;
+            this.contextMenu.data = [];
+            this.contextMenuOptions.forEach(i => {
+                let _item = {
+                    label: i.label,
+                    icon: i.icon,
+                    value: row[i.prop]
+                };
+                this.contextMenu.data.push(_item);
+            });
+            this.contextMenu.x = event.x;
+            this.contextMenu.y = event.y;
+            this.contextMenu.show = true;
+        }, // 当某一行被鼠标右键点击时会触发该事件
+        handleContextmenu() {
+            event.preventDefault();
+            event.stopPropagation();
+        }, // 右键菜单事件
+        handleRowDbClick(row, column, event) {
+            this.$emit('row-dblclick', row, column, event);
+        }, // 当某一行被双击时会触发该事件
+        handleHeaderClick(column, event) {
+            this.$emit('header-click', column, event);
+        }, // 当某一列的表头被点击时会触发该事件
+        handleHeaderContextMenu(column, event) {
+            this.$emit('header-contextmenu', column, event);
+        }, // 当某一列的表头被鼠标右键点击时触发该事件
+        handleSortChange(e) {
+            this.$emit('sort-change', e);
+        }, // 当表格的排序条件发生变化的时候会触发该事件
+        handleFilterChange(filters) {
+            this.$emit('filter-change', filters);
+        }, // 当表格的筛选条件发生变化的时候会触发该事件
+        handleExpandChange(row, expanded) {
+            this.$emit('expand-change', row, expanded);
+        }, // 当表格的筛选条件发生变化的时候会触发该事件
+        // ------------------------------------------- 以下为提供方法 ------------------------------------
+        /**
+         * 手动调用树表懒加载
+         * row 要展开的行信息
+         */
+        loadTree(row) {
+            this.$refs['tableRef'].store.loadOrToggle(row);
+        },
+        /**
+         * 更新树表懒加载后的子节点
+         * 要更新的节点id
+         * 要添加的节点list
+         */
+        loadTreeAdd(id, list) {
+            let _children = this.$refs['wl-gantt'].store.states.lazyTreeNodeMap[id] || [];
+            this.$set(this.$refs['wl-gantt'].store.states.lazyTreeNodeMap, id, list.concat(_children));
+        },
+        /**
+         * 更新树表懒加载后的子节点
+         * 要更新的节点id
+         * 要删掉的字节的rowKey
+         */
+        loadTreeRemove(id, key) {
+            let _children = this.$refs['wl-gantt'].store.states.lazyTreeNodeMap[id];
+            let _new_children = _children.filter(i => i[this.rowKey] != key);
+            this.$set(this.$refs['wl-gantt'].store.states.lazyTreeNodeMap, id, _new_children);
+        }
+    },
+    created() {
+        this.self_date_type = this.dateType;
+        this.self_start_date = this.startDate;
+        this.self_end_date = this.endDate;
+    },
+    beforeDestroy() {
+        if (this.infoCard.timer) {
+            clearTimeout(this.infoCard.timer);
+            this.infoCard.timer = null;
+        }
+    }
+};
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style lang="less">
+@import './css/index.less';
+</style>

+ 3 - 0
src/main/vue/src/main.js

@@ -19,12 +19,15 @@ import 'normalize.css/normalize.css';
 import '@/styles/theme/index.css';
 import VueAMap from 'vue-amap';
 import AreaChoose from '@/components/AreaChoose';
+import wlGantt from '@/components/wl-gantt/index';
 
 Vue.config.productionTip = false;
 Vue.use(ElementUI, { size: 'small' });
 Vue.use(http);
 Vue.use(math);
 Vue.use(VueAMap);
+Vue.use(wlGantt);
+
 VueAMap.initAMapApiLoader({
     key: 'bf91055058a47a7dc387e40ab6256a5f',
     plugin: [

+ 57 - 0
src/main/vue/src/router.js

@@ -1626,6 +1626,63 @@ const router = new Router({
                     meta: {
                         title: '合同列表'
                     }
+                },
+                {
+                    path: '/constructionGantt',
+                    name: 'ConstructionGantt',
+                    component: () =>
+                        import(/* webpackChunkName: "constructionGantt" */ '@/views/ConstructionGantt.vue'),
+                    meta: {
+                        title: '施工详情'
+                    }
+                },
+                {
+                    path: '/constructionList',
+                    name: 'ConstructionList',
+                    component: () => import(/* webpackChunkName: "constructionList" */ '@/views/ConstructionList.vue'),
+                    meta: {
+                        title: '工程管理'
+                    }
+                },
+                {
+                    path: '/constructionProcessEdit',
+                    name: 'ConstructionProcessEdit',
+                    component: () =>
+                        import(/* webpackChunkName: "constructionProcessEdit" */ '@/views/ConstructionProcessEdit.vue'),
+                    meta: {
+                        title: '工序管理编辑'
+                    }
+                },
+                {
+                    path: '/constructionProcessList',
+                    name: 'ConstructionProcessList',
+                    component: () =>
+                        import(/* webpackChunkName: "constructionProcessList" */ '@/views/ConstructionProcessList.vue'),
+                    meta: {
+                        title: '工序管理'
+                    }
+                },
+                {
+                    path: '/constructionProcessLogEdit',
+                    name: 'ConstructionProcessLogEdit',
+                    component: () =>
+                        import(
+                            /* webpackChunkName: "constructionProcessLogEdit" */ '@/views/ConstructionProcessLogEdit.vue'
+                        ),
+                    meta: {
+                        title: '施工纪录编辑'
+                    }
+                },
+                {
+                    path: '/constructionProcessLogList',
+                    name: 'ConstructionProcessLogList',
+                    component: () =>
+                        import(
+                            /* webpackChunkName: "constructionProcessLogList" */ '@/views/ConstructionProcessLogList.vue'
+                        ),
+                    meta: {
+                        title: '施工纪录'
+                    }
                 }
                 /**INSERT_LOCATION**/
             ]

+ 536 - 0
src/main/vue/src/views/ConstructionGantt.vue

@@ -0,0 +1,536 @@
+<template>
+    <div class="list-view construction-gantt">
+        <wl-gantt
+            ref="wl-gantt-demo"
+            :end-date="endDate"
+            :start-date="startDate"
+            :data="list"
+            :contextMenuOptions="contextMenuOptions"
+            :edit="false"
+            use-real-time
+            @row-dblclick="rowDbClick"
+            :props="treeProps"
+            v-if="renderGantt"
+        >
+            <el-table-column width="150" label="名称" prop="name" fixed></el-table-column>
+            <el-table-column width="90" label="开始日期" prop="planStart" align="center" fixed></el-table-column>
+            <el-table-column width="50" label="工期" prop="days" align="center" fixed></el-table-column>
+            <el-table-column width="60" label="状态" prop="status" align="center" :formatter="statusFormatter" fixed>
+                <template v-slot="{ row }">
+                    <div class="gantt-status-tag" :class="row.status">
+                        {{ statusFormatter(null, null, row.status) }}
+                    </div>
+                </template>
+            </el-table-column>
+            <el-table-column width="55" align="center" fixed>
+                <div slot="header">
+                    <div @click="addRow" class="gantt-cell-edit">
+                        <i class="el-icon-plus"></i>
+                    </div>
+                </div>
+                <template v-slot="{ row }">
+                    <el-dropdown trigger="click" @command="editRow">
+                        <div class="gantt-cell-edit">
+                            <i class="el-icon-more"></i>
+                        </div>
+                        <el-dropdown-menu slot="dropdown">
+                            <el-dropdown-item :command="{ action: 'detail', data: row }">详情</el-dropdown-item>
+                            <el-dropdown-item :command="{ action: 'edit', data: row }">编辑任务</el-dropdown-item>
+                            <el-dropdown-item
+                                :command="{ action: 'updateStatus', data: row, status: 'IN_PROGRESS' }"
+                                v-if="row.status === 'PENDING' || row.status === 'FINISH'"
+                            >
+                                标记开工
+                            </el-dropdown-item>
+                            <el-dropdown-item
+                                :command="{ action: 'updateStatus', data: row, status: 'FINISH' }"
+                                v-if="row.status === 'IN_PROGRESS'"
+                            >
+                                标记完工
+                            </el-dropdown-item>
+                            <el-dropdown-item :command="{ action: 'del', data: row }">删除任务</el-dropdown-item>
+                        </el-dropdown-menu>
+                    </el-dropdown>
+                </template>
+            </el-table-column>
+        </wl-gantt>
+        <el-dialog :visible.sync="showEditDialog" :title="formData.id ? '编辑' : '添加'" width="600px">
+            <el-form
+                :model="formData"
+                :rules="rules"
+                ref="form"
+                label-width="80px"
+                label-position="right"
+                size="small"
+                style="max-width: 500px;"
+            >
+                <el-form-item prop="name" label="工序名称">
+                    <el-input v-model="formData.name"></el-input>
+                </el-form-item>
+                <el-form-item prop="planStart" label="计划开工">
+                    <el-date-picker
+                        v-model="formData.planStart"
+                        type="date"
+                        value-format="yyyy-MM-dd"
+                        placeholder="选择日期"
+                    >
+                    </el-date-picker>
+                </el-form-item>
+                <el-form-item prop="planEnd" label="计划结束">
+                    <el-date-picker
+                        v-model="formData.planEnd"
+                        type="date"
+                        value-format="yyyy-MM-dd"
+                        placeholder="选择日期"
+                    >
+                    </el-date-picker>
+                </el-form-item>
+                <el-form-item prop="principal" label="负责人">
+                    <el-input v-model="formData.principal"></el-input>
+                </el-form-item>
+                <el-form-item prop="remark" label="备注">
+                    <el-input v-model="formData.remark"></el-input>
+                </el-form-item>
+            </el-form>
+            <div slot="footer">
+                <el-button @click="showEditDialog = false" :disabled="saving">取消</el-button>
+                <el-button @click="onSave" :loading="saving" type="primary">保存</el-button>
+            </div>
+        </el-dialog>
+
+        <el-dialog :visible.sync="showDetailDialog" title="详情" width="600px">
+            <el-tabs v-model="tab">
+                <el-tab-pane label="基本信息" name="1" class="detail-tab">
+                    <div class="detail-form">
+                        <div class="form-item l">
+                            <div class="label">名称:</div>
+                            <div class="value">{{ currentRow.name }}</div>
+                        </div>
+                        <div class="form-item">
+                            <div class="label">计划开工时间:</div>
+                            <div class="value">{{ currentRow.planStart }}</div>
+                        </div>
+                        <div class="form-item">
+                            <div class="label">计划结束时间:</div>
+                            <div class="value">{{ currentRow.planEnd }}</div>
+                        </div>
+                        <div class="form-item">
+                            <div class="label">工期:</div>
+                            <div class="value">{{ currentRow.days }}</div>
+                        </div>
+                        <div class="form-item">
+                            <div class="label">负责人:</div>
+                            <div class="value">{{ currentRow.principal }}</div>
+                        </div>
+                        <div class="form-item">
+                            <div class="label">状态:</div>
+                            <div class="value gantt-status-tag" :class="currentRow.status">
+                                {{ statusFormatter(null, null, currentRow.status) }}
+                            </div>
+                        </div>
+                        <div class="form-item"></div>
+                        <div class="form-item">
+                            <div class="label">开工时间:</div>
+                            <div class="value">{{ currentRow.start }}</div>
+                        </div>
+                        <div class="form-item">
+                            <div class="label">结束时间:</div>
+                            <div class="value">{{ currentRow.end }}</div>
+                        </div>
+                        <div class="form-item">
+                            <div class="label">备注:</div>
+                            <div class="value">{{ currentRow.remark }}</div>
+                        </div>
+                    </div>
+                </el-tab-pane>
+                <el-tab-pane label="施工纪录" name="2" class="detail-tab">
+                    <div class="btn-add">
+                        <el-button icon="el-icon-plus" @click="onAddLog">添加施工纪录</el-button>
+                    </div>
+                    <div class="log-list">
+                        <div class="log-item" v-for="item in logs" :key="item.id">
+                            <div class="remark">{{ item.remark }}</div>
+                            <div class="info">
+                                <div class="name">{{ item.operator }}</div>
+                                <div class="time">{{ item.time }}</div>
+                            </div>
+                        </div>
+                    </div>
+                </el-tab-pane>
+            </el-tabs>
+        </el-dialog>
+
+        <el-dialog width="600px" title="添加施工纪录" :visible.sync="showAddLogDialog">
+            <el-form :model="addLogForm" ref="addLogForm" :rules="addLogRules">
+                <el-form-item prop="remark">
+                    <el-input type="textarea" v-model="addLogForm.remark" placeholder="请输入施工纪录"></el-input>
+                </el-form-item>
+            </el-form>
+            <div slot="footer">
+                <el-button :disabled="savingLog" @click="showAddLogDialog = false">取消</el-button>
+                <el-button type="primary" :loading="savingLog" @click="onSaveLog"> 保存</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+<script>
+import zhCN from 'dayjs/locale/zh-cn';
+import dayjs from 'dayjs';
+import duration from 'dayjs/plugin/duration';
+dayjs.extend(duration);
+export default {
+    name: 'ConstructionGantt',
+    created() {
+        this.getData();
+    },
+    mounted() {
+        let el = document.querySelector('.list-view');
+        this.tableHeight = el.getBoundingClientRect().height - 40 + 'px';
+        console.log(this.tableHeight);
+    },
+    data() {
+        return {
+            tableHeight: null,
+            showEditDialog: false,
+            saving: false,
+            formData: {
+                status: 'PENDING'
+            },
+            rules: {
+                constructionId: [
+                    {
+                        required: true,
+                        message: '请输入项目',
+                        trigger: 'blur'
+                    }
+                ],
+                name: [
+                    {
+                        required: true,
+                        message: '请输入工序名称',
+                        trigger: 'blur'
+                    }
+                ],
+                planStart: [
+                    {
+                        required: true,
+                        message: '请输入计划开工',
+                        trigger: 'blur'
+                    }
+                ],
+                planEnd: [
+                    {
+                        required: true,
+                        message: '请输入计划结束',
+                        trigger: 'blur'
+                    }
+                ],
+                days: [
+                    {
+                        required: true,
+                        message: '请输入工期',
+                        trigger: 'blur'
+                    }
+                ],
+                principal: [
+                    {
+                        required: true,
+                        message: '请输入负责人',
+                        trigger: 'blur'
+                    }
+                ],
+                status: [
+                    {
+                        required: true,
+                        message: '请输入状态',
+                        trigger: 'blur'
+                    }
+                ]
+            },
+            constructionIdOptions: [],
+            statusOptions: [
+                { label: '未开工', value: 'PENDING' },
+                { label: '已开工', value: 'IN_PROGRESS' },
+                { label: '已暂停', value: 'PAUSE' },
+                { label: '已完工', value: 'FINISH' }
+            ],
+            list: [], // 数据
+            selected: [], // 选中数据
+            contextMenuOptions: [
+                { label: '任务名称', prop: 'name' },
+                { label: '开始时间', prop: 'startDate' },
+                { label: '结束时间', prop: 'endDate' }
+            ],
+            renderGantt: true,
+            treeProps: { startDate: 'planStart', endDate: 'planEnd', realStartDate: 'start', realEndDate: 'end' },
+            startDate: '2021-01-01',
+            endDate: '2021-01-31',
+            currentRow: {},
+            showDetailDialog: false,
+            tab: '1',
+            logs: [],
+            showAddLogDialog: false,
+            addLogForm: {},
+            addLogRules: { remark: [{ required: true, message: '请输入内容' }] },
+            savingLog: false
+        };
+    },
+    methods: {
+        getData() {
+            this.$http
+                .get('/constructionProcess/all', { query: { constructionId: this.$route.query.id }, size: 1000 })
+                .then(res => {
+                    this.list = res.content;
+                    let startDate = dayjs();
+                    let endDate = dayjs();
+                    res.content.forEach(i => {
+                        let planStart = dayjs(i.planStart, 'YYYY-MM-DD');
+                        let start = dayjs(i.start, 'YYYY-MM-DD');
+                        let planEnd = dayjs(i.planEnd, 'YYYY-MM-DD');
+                        let end = dayjs(i.end, 'YYYY-MM-DD');
+                        if (planStart.isBefore(startDate)) {
+                            startDate = planStart;
+                        }
+                        if (start.isBefore(startDate)) {
+                            startDate = start;
+                        }
+                        if (planEnd.isAfter(endDate)) {
+                            endDate = planEnd;
+                        }
+                        if (end.isAfter(endDate)) {
+                            endDate = end;
+                        }
+                    });
+                    this.startDate = startDate.format('YYYY-MM-DD');
+                    this.endDate = endDate.format('YYYY-MM-DD');
+                    this.renderGantt = false;
+                    this.$nextTick(() => {
+                        this.renderGantt = true;
+                    });
+                });
+        },
+        onSave() {
+            this.$refs.form.validate(valid => {
+                if (valid) {
+                    this.submit();
+                } else {
+                    return false;
+                }
+            });
+        },
+        statusFormatter(cell, column, value, index) {
+            return (this.statusOptions.find(i => i.value === value) || {}).label || '';
+        },
+        addRow() {
+            if (this.$refs.form) {
+                this.$refs.form.clearValidate();
+            }
+            this.formData = { status: 'PENDING' };
+            this.showEditDialog = true;
+        },
+        editRow(params) {
+            console.log(params);
+            if (params.action === 'detail') {
+                this.rowDbClick(params.data);
+            } else if (params.action === 'edit') {
+                if (this.$refs.form) {
+                    this.$refs.form.clearValidate();
+                }
+                this.formData = { ...params.data };
+                this.showEditDialog = true;
+            } else if (params.action === 'updateStatus') {
+                this.$confirm(
+                    `是否确认标记为${(this.statusOptions.find(i => i.value === params.status) || {}).label || ''}?`,
+                    '提示',
+                    {
+                        confirmButtonText: '确定',
+                        cancelButtonText: '取消',
+                        type: 'warning'
+                    }
+                )
+                    .then(() => {
+                        return this.$http.post('/constructionProcess/updateStatus', {
+                            id: params.data.id,
+                            status: params.status
+                        });
+                    })
+                    .then(res => {
+                        this.$message.success('操作成功');
+                        this.getData();
+                    })
+                    .catch(e => {
+                        if ('cancel' !== e) {
+                            this.$message.error(e.error || '操作失败');
+                        }
+                    });
+            } else if (params.action === 'del') {
+                this.delRow(params.data);
+            }
+        },
+        submit() {
+            let data = { ...this.formData, constructionId: this.$route.query.id };
+            data.days =
+                dayjs.duration(dayjs(data.planEnd, 'YYYY-MM-DD').diff(dayjs(data.planStart, 'YYYY-MM-DD'))).days() + 1;
+            this.saving = true;
+            this.$http
+                .post('/constructionProcess/save', data, { body: 'json' })
+                .then(res => {
+                    this.saving = false;
+                    this.$message.success('成功');
+                    this.showEditDialog = false;
+                    this.getData();
+                })
+                .catch(e => {
+                    this.saving = false;
+                    this.$message.error(e.error);
+                });
+        },
+        delRow(row) {
+            this.$confirm('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/constructionProcess/del/${row.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        console.log(e);
+                        this.$message.error(e.error);
+                    }
+                });
+        },
+        rowDbClick(row) {
+            this.tab = '1';
+            this.currentRow = row;
+            this.showDetailDialog = true;
+            this.logs = [];
+            this.getLogs();
+        },
+        getLogs() {
+            this.$http
+                .get('/constructionProcessLog/all', {
+                    size: 1000,
+                    sort: 'createdAt,desc',
+                    query: { constructionProcessId: this.currentRow.id }
+                })
+                .then(res => {
+                    this.logs = res.content;
+                });
+        },
+        onAddLog() {
+            this.addLogForm = {};
+            if (this.$refs.addLogForm) {
+                this.$refs.addLogForm.clearValidate();
+            }
+            this.showAddLogDialog = true;
+        },
+        onSaveLog() {
+            this.$refs.addLogForm
+                .validate()
+                .then(() => {
+                    this.savingLog = true;
+                    return this.$http.post('/constructionProcessLog/saveLog', {
+                        processId: this.currentRow.id,
+                        remark: this.addLogForm.remark
+                    });
+                })
+                .then(res => {
+                    this.savingLog = false;
+                    this.showAddLogDialog = false;
+                    this.$message.success('保存成功');
+                    this.getLogs();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.savingLog = false;
+                    if (e !== false) {
+                        this.$message.error(e.error || '保存失败');
+                    }
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+.detail-form {
+    display: flex;
+    flex-wrap: wrap;
+    margin-top: 15px;
+    .form-item {
+        width: 50%;
+        display: flex;
+        align-content: center;
+        height: 40px;
+        line-height: 40px;
+        color: #666;
+        &.l {
+            width: 100%;
+        }
+        .label {
+            width: 120px;
+            text-align: right;
+            font-size: 13px;
+            font-weight: bold;
+            color: #333;
+        }
+        .value {
+            font-size: 14px;
+            margin-left: 20px;
+        }
+    }
+}
+.btn-add {
+    margin: 10px 15px 0 15px;
+    .el-button {
+        width: 100%;
+    }
+}
+.log-list {
+    max-height: 50vh;
+    overflow: auto;
+    padding: 0 15px;
+    .log-item {
+        padding: 20px 0;
+        border-bottom: 1px solid #f2f4f5;
+        .remark {
+            color: #333;
+            font-size: 14px;
+        }
+        .info {
+            margin-top: 10px;
+            display: flex;
+            align-items: center;
+            font-size: 12px;
+            color: #999999;
+            .name {
+            }
+            .time {
+                flex-grow: 1;
+                text-align: right;
+            }
+        }
+    }
+}
+</style>
+<style lang="less">
+.construction-gantt {
+    .el-tabs__header {
+        margin-bottom: 0;
+    }
+    .gantt-cell-edit {
+        text-align: center;
+        cursor: pointer;
+        font-size: 15px;
+    }
+    .gantt-status-tag {
+        &.IN_PROGRESS {
+            color: @primary;
+        }
+        &.FINISH {
+            color: @success;
+        }
+    }
+}
+</style>

+ 381 - 0
src/main/vue/src/views/ConstructionList.vue

@@ -0,0 +1,381 @@
+<template>
+    <div class="list-view">
+        <div class="filters-container">
+            <el-input placeholder="输入关键字" v-model="search" clearable class="filter-item"></el-input>
+            <el-button @click="getData" type="primary" icon="el-icon-search" class="filter-item">搜索 </el-button>
+            <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>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+            :height="tableHeight"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column
+                prop="storeId"
+                label="门店"
+                :formatter="storeFormatter"
+                min-width="100"
+                show-overflow-tooltip
+            >
+            </el-table-column>
+            <el-table-column prop="name" label="项目名称" min-width="150" show-overflow-tooltip> </el-table-column>
+            <el-table-column prop="manager" label="店长" min-width="80"> </el-table-column>
+            <el-table-column prop="supervisor" label="工程监理" min-width="80"> </el-table-column>
+            <el-table-column prop="planStart" label="计划开工日期" min-width="100" align="center"> </el-table-column>
+            <el-table-column prop="planEnd" label="计划竣工日期" min-width="100" align="center"> </el-table-column>
+            <el-table-column prop="start" label="实际开工日期" min-width="100" align="center"> </el-table-column>
+            <el-table-column prop="end" label="实际竣工日期" min-width="100" align="center"> </el-table-column>
+            <el-table-column width="80" label="状态" prop="status" align="center" :formatter="statusFormatter">
+                <template v-slot="{ row }">
+                    <div class="gantt-status-tag" :class="row.status">
+                        {{ statusFormatter(null, null, row.status) }}
+                    </div>
+                </template>
+            </el-table-column>
+            <el-table-column prop="progress" label="进度" width="150">
+                <template v-slot="{ row }">
+                    <el-progress
+                        :percentage="row.progress"
+                        :status="row.progress === 100 ? 'success' : null"
+                    ></el-progress>
+                </template>
+            </el-table-column>
+            <el-table-column prop="days" label="总工期" min-width="80"> </el-table-column>
+            <el-table-column
+                prop="delayDays"
+                label="超期天数"
+                min-width="80"
+                :formatter="delayDaysFormatter"
+            ></el-table-column>
+            <el-table-column label="操作" align="center" fixed="right" min-width="250">
+                <template slot-scope="{ row }">
+                    <el-button @click="editRow(row)" type="primary" size="mini" plain>编辑</el-button>
+                    <el-button @click="detail(row)" type="primary" size="mini" plain>施工详情</el-button>
+                    <el-button @click="deleteRow(row)" type="danger" size="mini" plain>删除</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <!-- <div class="multiple-mode-wrapper">
+                <el-button v-if="!multipleMode" @click="toggleMultipleMode(true)">批量编辑</el-button>
+                <el-button-group v-else>
+                    <el-button @click="operation1">批量操作1</el-button>
+                    <el-button @click="operation2">批量操作2</el-button>
+                    <el-button @click="toggleMultipleMode(false)">取消</el-button>
+                </el-button-group>
+            </div> -->
+            <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="totalElements"
+            >
+            </el-pagination>
+        </div>
+        <el-dialog
+            :visible.sync="showEditDialog"
+            title="编辑"
+            width="600px"
+            :close-on-click-modal="!saving"
+            :close-on-press-escape="!saving"
+            :show-close="!saving"
+        >
+            <el-form
+                :model="formData"
+                :rules="rules"
+                ref="form"
+                label-width="80px"
+                label-position="right"
+                size="small"
+                style="max-width: 500px;"
+            >
+                <el-form-item prop="storeId" label="门店">
+                    <el-select v-model="formData.storeId" clearable filterable placeholder="请选择">
+                        <el-option
+                            v-for="item in storeIdOptions"
+                            :key="item.value"
+                            :label="item.label"
+                            :value="item.value"
+                        >
+                        </el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item prop="name" label="项目名称">
+                    <el-input v-model="formData.name"></el-input>
+                </el-form-item>
+                <el-form-item prop="manager" label="店长">
+                    <el-input v-model="formData.manager"></el-input>
+                </el-form-item>
+                <el-form-item prop="supervisor" label="工程监理">
+                    <el-input v-model="formData.supervisor"></el-input>
+                </el-form-item>
+                <!-- <el-form-item prop="start" label="开工日期">
+                    <el-date-picker
+                        v-model="formData.start"
+                        type="date"
+                        value-format="yyyy-MM-dd"
+                        placeholder="选择日期"
+                    >
+                    </el-date-picker>
+                </el-form-item>
+                <el-form-item prop="end" label="竣工日期">
+                    <el-date-picker v-model="formData.end" type="date" value-format="yyyy-MM-dd" placeholder="选择日期">
+                    </el-date-picker>
+                </el-form-item>
+                <el-form-item prop="progress" label="进度">
+                    <el-input-number type="number" v-model="formData.progress"></el-input-number>
+                </el-form-item> -->
+            </el-form>
+            <div slot="footer">
+                <el-button @click="showEditDialog = false" :disabled="saving">取消</el-button>
+                <el-button @click="onSave" :loading="saving" type="primary">保存</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+import { format, parse, isAfter, isBefore, differenceInDays } from 'date-fns';
+
+export default {
+    name: 'ConstructionList',
+    mixins: [pageableTable],
+    created() {
+        this.getData();
+        this.$http
+            .get('/storeInfo/all')
+            .then(res => {
+                if (res.content.length > 0) {
+                    res.content.forEach(item => {
+                        this.storeIdOptions.push({
+                            label: item.storeName,
+                            value: item.id
+                        });
+                    });
+                }
+            })
+            .catch(e => {
+                console.log(e);
+                this.$message.error(e.error);
+            });
+    },
+    data() {
+        return {
+            sortStr: 'createdAt,desc',
+            multipleMode: false,
+            search: '',
+            url: '/construction/all',
+            downloading: false,
+            saving: false,
+            formData: {},
+            rules: {
+                storeId: [
+                    {
+                        required: true,
+                        message: '请输入门店',
+                        trigger: 'blur'
+                    }
+                ],
+                name: [
+                    {
+                        required: true,
+                        message: '请输入项目名称',
+                        trigger: 'blur'
+                    }
+                ],
+                manager: [
+                    {
+                        required: true,
+                        message: '请输入店长',
+                        trigger: 'blur'
+                    }
+                ],
+                supervisor: [
+                    {
+                        required: true,
+                        message: '请输入工程监理',
+                        trigger: 'blur'
+                    }
+                ],
+                start: [
+                    {
+                        required: true,
+                        message: '请输入开工日期',
+                        trigger: 'blur'
+                    }
+                ],
+                end: [
+                    {
+                        required: true,
+                        message: '请输入竣工日期',
+                        trigger: 'blur'
+                    }
+                ]
+            },
+            storeIdOptions: [],
+            showEditDialog: false,
+            statusOptions: [
+                { label: '未开工', value: 'PENDING' },
+                { label: '已开工', value: 'IN_PROGRESS' },
+                { label: '已暂停', value: 'PAUSE' },
+                { label: '已完工', value: 'FINISH' }
+            ]
+        };
+    },
+    computed: {
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        }
+    },
+    methods: {
+        beforeGetData() {
+            if (this.search) {
+                return { search: this.search };
+            }
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        addRow() {
+            this.formData = {};
+            if (this.$refs.form) {
+                this.$refs.form.clearValidate();
+            }
+            this.showEditDialog = true;
+        },
+        editRow(row) {
+            this.formData = { ...row };
+            if (this.$refs.form) {
+                this.$refs.form.clearValidate();
+            }
+            this.showEditDialog = true;
+        },
+        detail(row) {
+            this.$router.push({
+                path: '/constructionGantt',
+                query: {
+                    id: row.id
+                }
+            });
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/construction/excel', {
+                    responseType: 'blob',
+                    params: { size: 10000 }
+                })
+                .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);
+                });
+        },
+        deleteRow(row) {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/construction/del/${row.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        this.$message.error((e || {}).error || '删除失败');
+                    }
+                });
+        },
+        onSave() {
+            this.$refs.form.validate(valid => {
+                if (valid) {
+                    this.submit();
+                } else {
+                    return false;
+                }
+            });
+        },
+        submit() {
+            let data = { ...this.formData };
+            this.saving = true;
+            this.$http
+                .post('/construction/save', data, { body: 'json' })
+                .then(res => {
+                    this.$message.success('成功');
+                    this.saving = false;
+                    this.showEditDialog = false;
+                    this.getData();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.$message.error(e.error);
+                    this.saving = false;
+                });
+        },
+        storeFormatter(row, column, value, index) {
+            return (this.storeIdOptions.find(i => i.value === value) || {}).label;
+        },
+        statusFormatter(row, column, value, index) {
+            return (this.statusOptions.find(i => i.value === value) || {}).label || '';
+        },
+        delayDaysFormatter(row, column, value, index) {
+            if (row.planEnd && row.end) {
+                let planEnd = parse(row.planEnd, 'yyyy-MM-dd', new Date());
+                let end = parse(row.end, 'yyyy-MM-dd', new Date());
+                if (isAfter(end, planEnd)) {
+                    return differenceInDays(end, planEnd);
+                }
+            }
+            return '';
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+.gantt-status-tag {
+    font-weight: bold;
+    &.IN_PROGRESS {
+        color: @primary;
+    }
+    &.FINISH {
+        color: @success;
+    }
+    &.error {
+        color: #f56c6c;
+    }
+}
+</style>

+ 214 - 0
src/main/vue/src/views/ConstructionProcessEdit.vue

@@ -0,0 +1,214 @@
+<template>
+    <div class="edit-view">
+        <el-form
+            :model="formData"
+            :rules="rules"
+            ref="form"
+            label-width="80px"
+            label-position="right"
+            size="small"
+            style="max-width: 500px;"
+        >
+            <el-form-item prop="constructionId" label="项目">
+                <el-select v-model="formData.constructionId" clearable filterable placeholder="请选择">
+                    <el-option
+                        v-for="item in constructionIdOptions"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value"
+                    >
+                    </el-option>
+                </el-select>
+            </el-form-item>
+            <el-form-item prop="name" label="工序名称">
+                <el-input v-model="formData.name"></el-input>
+            </el-form-item>
+            <el-form-item prop="planStart" label="计划开工">
+                <el-date-picker
+                    v-model="formData.planStart"
+                    type="date"
+                    value-format="yyyy-MM-dd"
+                    placeholder="选择日期"
+                >
+                </el-date-picker>
+            </el-form-item>
+            <el-form-item prop="planEnd" label="计划结束">
+                <el-date-picker v-model="formData.planEnd" type="date" value-format="yyyy-MM-dd" placeholder="选择日期">
+                </el-date-picker>
+            </el-form-item>
+            <el-form-item prop="days" label="工期">
+                <el-input v-model="formData.days"></el-input>
+            </el-form-item>
+            <el-form-item prop="principal" label="负责人">
+                <el-input v-model="formData.principal"></el-input>
+            </el-form-item>
+            <el-form-item prop="start" label="开工时间">
+                <el-date-picker v-model="formData.start" type="date" value-format="yyyy-MM-dd" placeholder="选择日期">
+                </el-date-picker>
+            </el-form-item>
+            <el-form-item prop="end" label="结束时间">
+                <el-date-picker v-model="formData.end" type="date" value-format="yyyy-MM-dd" placeholder="选择日期">
+                </el-date-picker>
+            </el-form-item>
+            <el-form-item prop="remark" label="模版">
+                <el-input v-model="formData.remark"></el-input>
+            </el-form-item>
+            <el-form-item prop="status" label="状态">
+                <el-select v-model="formData.status" clearable filterable placeholder="请选择">
+                    <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value">
+                    </el-option>
+                </el-select>
+            </el-form-item>
+            <el-form-item>
+                <el-button @click="onSave" :loading="$store.state.fetchingData" type="primary">保存</el-button>
+                <el-button @click="onDelete" :loading="$store.state.fetchingData" type="danger" v-if="formData.id"
+                    >删除
+                </el-button>
+                <el-button @click="$router.go(-1)">取消</el-button>
+            </el-form-item>
+        </el-form>
+    </div>
+</template>
+<script>
+export default {
+    name: 'ConstructionProcessEdit',
+    created() {
+        if (this.$route.query.id) {
+            this.$http
+                .get('constructionProcess/get/' + this.$route.query.id)
+                .then(res => {
+                    this.formData = res;
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.$message.error(e.error);
+                });
+        }
+        this.$http
+            .get('/construction/all')
+            .then(res => {
+                if (res.content.length > 0) {
+                    res.content.forEach(item => {
+                        this.constructionIdOptions.push({
+                            label: item.name,
+                            value: item.id
+                        });
+                    });
+                }
+            })
+            .catch(e => {
+                console.log(e);
+                this.$message.error(e.error);
+            });
+    },
+    data() {
+        return {
+            saving: false,
+            formData: {},
+            rules: {
+                constructionId: [
+                    {
+                        required: true,
+                        message: '请输入项目',
+                        trigger: 'blur'
+                    }
+                ],
+                name: [
+                    {
+                        required: true,
+                        message: '请输入工序名称',
+                        trigger: 'blur'
+                    }
+                ],
+                planStart: [
+                    {
+                        required: true,
+                        message: '请输入计划开工',
+                        trigger: 'blur'
+                    }
+                ],
+                planEnd: [
+                    {
+                        required: true,
+                        message: '请输入计划结束',
+                        trigger: 'blur'
+                    }
+                ],
+                days: [
+                    {
+                        required: true,
+                        message: '请输入工期',
+                        trigger: 'blur'
+                    }
+                ],
+                principal: [
+                    {
+                        required: true,
+                        message: '请输入负责人',
+                        trigger: 'blur'
+                    }
+                ],
+                status: [
+                    {
+                        required: true,
+                        message: '请输入状态',
+                        trigger: 'blur'
+                    }
+                ]
+            },
+            constructionIdOptions: [],
+            statusOptions: [
+                { label: '未开工', value: 'PENDING' },
+                { label: '已开工', value: 'IN_PROGRESS' },
+                { label: '已暂停', value: 'PAUSE' },
+                { label: '已完工', value: 'FINISH' }
+            ]
+        };
+    },
+    methods: {
+        onSave() {
+            this.$refs.form.validate(valid => {
+                if (valid) {
+                    this.submit();
+                } else {
+                    return false;
+                }
+            });
+        },
+        submit() {
+            let data = { ...this.formData };
+
+            this.$store.commit('updateFetchingData', true);
+            this.$http
+                .post('/constructionProcess/save', data, { body: 'json' })
+                .then(res => {
+                    this.$store.commit('updateFetchingData', false);
+                    this.$message.success('成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    this.$store.commit('updateFetchingData', false);
+                    console.log(e);
+                    this.$message.error(e.error);
+                });
+        },
+        onDelete() {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/constructionProcess/del/${this.formData.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        console.log(e);
+                        this.$message.error(e.error);
+                    }
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped></style>

+ 285 - 0
src/main/vue/src/views/ConstructionProcessList.vue

@@ -0,0 +1,285 @@
+<template>
+    <div class="list-view" v-loading="tableLoading">
+        <div class="filters-container">
+            <div class="filter-item">
+                <span class="radio-label">计划开工时间:</span>
+                <el-radio-group v-model="planStartRadio" size="small">
+                    <el-radio label="1" border>全部</el-radio>
+                    <el-radio label="2" border>今天</el-radio>
+                    <el-radio label="3" border>昨天</el-radio>
+                    <el-radio label="4" border>本周</el-radio>
+                    <el-radio label="5" border>本月</el-radio>
+                    <el-radio label="6" border>自定义时间</el-radio>
+                </el-radio-group>
+                <el-date-picker
+                    value-format="yyyy-MM-dd"
+                    type="daterange"
+                    v-model="planStart"
+                    style="margin-left:10px;position:relative;top:1px;"
+                    :disabled="planStartRadio !== '6'"
+                    start-placeholder="请选择"
+                    end-placeholder="请选择"
+                ></el-date-picker>
+            </div>
+            <div class="filter-item">
+                <span class="radio-label">计划完工时间:</span>
+                <el-radio-group v-model="planEndRadio" size="small">
+                    <el-radio label="1" border>全部</el-radio>
+                    <el-radio label="2" border>今天</el-radio>
+                    <el-radio label="3" border>昨天</el-radio>
+                    <el-radio label="4" border>本周</el-radio>
+                    <el-radio label="5" border>本月</el-radio>
+                    <el-radio label="6" border>自定义时间</el-radio>
+                </el-radio-group>
+                <el-date-picker
+                    value-format="yyyy-MM-dd"
+                    type="daterange"
+                    v-model="planEnd"
+                    d
+                    style="margin-left:10px;position:relative;top:1px;"
+                    :disabled="planEndRadio !== '6'"
+                    start-placeholder="请选择"
+                    end-placeholder="请选择"
+                ></el-date-picker>
+            </div>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+            :height="tableHeight"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column prop="constructionId" label="项目" :formatter="constructionFormatter"> </el-table-column>
+            <el-table-column prop="name" label="工序名称"> </el-table-column>
+            <el-table-column prop="planStart" label="计划开工"> </el-table-column>
+            <el-table-column prop="planEnd" label="计划结束"> </el-table-column>
+            <el-table-column prop="days" label="工期"> </el-table-column>
+            <el-table-column prop="principal" label="负责人"> </el-table-column>
+            <el-table-column prop="start" label="开工时间"> </el-table-column>
+            <el-table-column prop="end" label="结束时间"> </el-table-column>
+            <el-table-column prop="remark" label="备注"> </el-table-column>
+            <el-table-column prop="status" label="状态" :formatter="statusFormatter"> </el-table-column>
+            <el-table-column label="操作" align="center" fixed="right" width="150">
+                <template slot-scope="{ row }">
+                    <el-button @click="editRow(row)" type="primary" size="mini" plain>查看</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <!-- <div class="multiple-mode-wrapper">
+                <el-button v-if="!multipleMode" @click="toggleMultipleMode(true)">批量编辑</el-button>
+                <el-button-group v-else>
+                    <el-button @click="operation1">批量操作1</el-button>
+                    <el-button @click="operation2">批量操作2</el-button>
+                    <el-button @click="toggleMultipleMode(false)">取消</el-button>
+                </el-button-group>
+            </div> -->
+            <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="totalElements"
+            >
+            </el-pagination>
+        </div>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+import { format, parse, startOfWeek, endOfWeek, startOfMonth, endOfMonth, addDays } from 'date-fns';
+import { ca } from 'date-fns/locale';
+const f = (d, fmt) => {
+    return format(d, fmt || 'yyyy-MM-dd');
+};
+const p = (s, fmt) => {
+    return parse(s, fmt || 'yyyy-MM-dd', new Date());
+};
+export default {
+    name: 'ConstructionProcessList',
+    mixins: [pageableTable],
+    created() {
+        this.getData();
+        this.$http.get('/construction/all', { size: 10000 }).then(res => {
+            this.constructions = res.content;
+        });
+    },
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            sortStr: 'createdAt,desc',
+            url: '/constructionProcess/all',
+            downloading: false,
+            statusOptions: [
+                { label: '未开工', value: 'PENDING' },
+                { label: '已开工', value: 'IN_PROGRESS' },
+                { label: '已暂停', value: 'PAUSE' },
+                { label: '已完工', value: 'FINISH' }
+            ],
+            constructions: [],
+            planStartRadio: '1',
+            planEndRadio: '1',
+            planStart: [],
+            planEnd: []
+        };
+    },
+    computed: {
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        }
+    },
+    methods: {
+        statusFormatter(row, column, cellValue, index) {
+            let selectedOption = this.statusOptions.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        beforeGetData() {
+            let data = { search: this.search, query: {} };
+            if (this.planStartRadio !== '1') {
+                data.query.planStart = this.planStart.join();
+            }
+            if (this.planEndRadio !== '1') {
+                data.query.planEnd = this.planEnd.join();
+            }
+            return data;
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        addRow() {
+            this.$router.push({
+                path: '/constructionProcessEdit',
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: '/constructionGantt',
+                query: {
+                    id: row.constructionId
+                }
+            });
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/constructionProcess/excel', {
+                    responseType: 'blob',
+                    params: { size: 10000 }
+                })
+                .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: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        deleteRow(row) {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/constructionProcess/del/${row.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.getData();
+                })
+                .catch(action => {
+                    if (action === 'cancel') {
+                        this.$message.info('删除取消');
+                    } else {
+                        this.$message.error('删除失败');
+                    }
+                });
+        },
+        constructionFormatter(row, column, value, index) {
+            return (this.constructions.find(i => i.id === value) || {}).name;
+        },
+        getRange(type) {
+            let d = new Date();
+            switch (type) {
+                case '1':
+                    return [];
+                case '2':
+                    return [f(d), f(d)];
+                case '3':
+                    d = addDays(d, -1);
+                    return [f(d), f(d)];
+                case '4':
+                    return [f(startOfWeek(d)), f(endOfWeek(d))];
+                case '5':
+                    return [f(startOfMonth(d)), f(endOfMonth(d))];
+            }
+        }
+    },
+    watch: {
+        planStartRadio(val) {
+            if (val !== '6') {
+                this.planStart = this.getRange(val);
+            }
+        },
+        planEndRadio(val) {
+            if (val !== '6') {
+                this.planEnd = this.getRange(val);
+            }
+        },
+        planStart(val) {
+            if ((val && val.length === 2) || this.planStartRadio === '1') {
+                this.getData();
+            }
+        },
+        planEnd(val) {
+            if ((val && val.length === 2) || this.planEndRadio === '1') {
+                this.getData();
+            }
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+.radio-label {
+    color: #666;
+    font-weight: bold;
+    font-size: 13px;
+}
+.el-radio.is-bordered {
+    margin-right: 0;
+}
+</style>

+ 112 - 0
src/main/vue/src/views/ConstructionProcessLogEdit.vue

@@ -0,0 +1,112 @@
+<template>
+    <div class="edit-view">
+        <el-form
+            :model="formData"
+            :rules="rules"
+            ref="form"
+            label-width="80px"
+            label-position="right"
+            size="small"
+            style="max-width: 500px;"
+        >
+            <el-form-item prop="constructionId" label="项目">
+                <el-input-number type="number" v-model="formData.constructionId"></el-input-number>
+            </el-form-item>
+            <el-form-item prop="constructionProcessId" label="工序名称">
+                <el-input v-model="formData.constructionProcessId"></el-input>
+            </el-form-item>
+            <el-form-item prop="remark" label="备注">
+                <el-input v-model="formData.remark"></el-input>
+            </el-form-item>
+            <el-form-item prop="time" label="操作时间">
+                <el-date-picker
+                    v-model="formData.time"
+                    type="datetime"
+                    value-format="yyyy-MM-dd HH:mm:ss"
+                    placeholder="选择日期时间"
+                >
+                </el-date-picker>
+            </el-form-item>
+            <el-form-item prop="operator" label="操作人">
+                <el-input v-model="formData.operator"></el-input>
+            </el-form-item>
+            <el-form-item>
+                <el-button @click="onSave" :loading="$store.state.fetchingData" type="primary">保存</el-button>
+                <el-button @click="onDelete" :loading="$store.state.fetchingData" type="danger" v-if="formData.id"
+                    >删除
+                </el-button>
+                <el-button @click="$router.go(-1)">取消</el-button>
+            </el-form-item>
+        </el-form>
+    </div>
+</template>
+<script>
+export default {
+    name: 'ConstructionProcessLogEdit',
+    created() {
+        if (this.$route.query.id) {
+            this.$http
+                .get('constructionProcessLog/get/' + this.$route.query.id)
+                .then(res => {
+                    this.formData = res;
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.$message.error(e.error);
+                });
+        }
+    },
+    data() {
+        return {
+            saving: false,
+            formData: {},
+            rules: {}
+        };
+    },
+    methods: {
+        onSave() {
+            this.$refs.form.validate(valid => {
+                if (valid) {
+                    this.submit();
+                } else {
+                    return false;
+                }
+            });
+        },
+        submit() {
+            let data = { ...this.formData };
+
+            this.$store.commit('updateFetchingData', true);
+            this.$http
+                .post('/constructionProcessLog/save', data, { body: 'json' })
+                .then(res => {
+                    this.$store.commit('updateFetchingData', false);
+                    this.$message.success('成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    this.$store.commit('updateFetchingData', false);
+                    console.log(e);
+                    this.$message.error(e.error);
+                });
+        },
+        onDelete() {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/constructionProcessLog/del/${this.formData.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        console.log(e);
+                        this.$message.error(e.error);
+                    }
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped></style>

+ 169 - 0
src/main/vue/src/views/ConstructionProcessLogList.vue

@@ -0,0 +1,169 @@
+<template>
+    <div class="list-view">
+        <div class="filters-container">
+            <el-input placeholder="输入关键字" v-model="search" clearable class="filter-item"></el-input>
+            <el-button @click="getData" type="primary" icon="el-icon-search" class="filter-item">搜索 </el-button>
+            <el-button @click="addRow" v-if="canEdit" 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>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+            :height="tableHeight"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column prop="constructionId" label="项目"> </el-table-column>
+            <el-table-column prop="constructionProcessId" label="工序名称"> </el-table-column>
+            <el-table-column prop="remark" label="备注"> </el-table-column>
+            <el-table-column prop="time" label="操作时间"> </el-table-column>
+            <el-table-column prop="operator" label="操作人"> </el-table-column>
+            <el-table-column label="操作" v-if="canEdit" align="center" fixed="right" min-width="150">
+                <template slot-scope="{ row }">
+                    <el-button @click="editRow(row)" type="primary" size="mini" plain>编辑</el-button>
+                    <el-button @click="deleteRow(row)" type="danger" size="mini" plain>删除</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <!-- <div class="multiple-mode-wrapper">
+                <el-button v-if="!multipleMode" @click="toggleMultipleMode(true)">批量编辑</el-button>
+                <el-button-group v-else>
+                    <el-button @click="operation1">批量操作1</el-button>
+                    <el-button @click="operation2">批量操作2</el-button>
+                    <el-button @click="toggleMultipleMode(false)">取消</el-button>
+                </el-button-group>
+            </div> -->
+            <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="totalElements"
+            >
+            </el-pagination>
+        </div>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+
+export default {
+    name: 'ConstructionProcessLogList',
+    mixins: [pageableTable],
+    created() {
+        this.getData();
+    },
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/constructionProcessLog/all',
+            downloading: false
+        };
+    },
+    computed: {
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        }
+    },
+    methods: {
+        beforeGetData() {
+            if (this.search) {
+                return { search: this.search };
+            }
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        addRow() {
+            this.$router.push({
+                path: '/constructionProcessLogEdit',
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: '/constructionProcessLogEdit',
+                query: {
+                    id: row.id
+                }
+            });
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/constructionProcessLog/excel', {
+                    responseType: 'blob',
+                    params: { size: 10000 }
+                })
+                .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: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        deleteRow(row) {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/constructionProcessLog/del/${row.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.getData();
+                })
+                .catch(action => {
+                    if (action === 'cancel') {
+                        this.$message.info('删除取消');
+                    } else {
+                        this.$message.error('删除失败');
+                    }
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped></style>

+ 2 - 2
src/main/vue/src/views/Operation/RoomStatus.vue

@@ -152,7 +152,7 @@
                     </div>
                 </div>
 
-                <Legend></Legend>
+                <legned-of-room></legned-of-room>
             </div>
 
             <div style="padding:20px">
@@ -1090,7 +1090,7 @@ export default {
         RoomDrawer,
         StatItem,
         CheckinDialog,
-        Legend
+        'legned-of-room': Legend
     }
 };
 </script>

+ 1 - 2
src/main/vue/src/views/customer/ContractListForCustomer.vue

@@ -236,8 +236,7 @@ export default {
     },
     methods: {
         getData() {
-            let data = {
-            };
+            let data = {};
             this.$http
                 .get('/contract/findByCustomerId', data)
                 .then(res => {

+ 67 - 0
src/main/vue/yarn.lock

@@ -2967,6 +2967,11 @@ date-now@^0.1.4:
   resolved "https://registry.npm.taobao.org/date-now/download/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
   integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=
 
+dayjs@^1.10.1, dayjs@^1.8.16, dayjs@^1.8.25:
+  version "1.10.1"
+  resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.1.tgz#114f678624842035396667a24eb1436814bc16fd"
+  integrity sha512-2xg7JrHQeLBQFkvTumLoy62x1siyeocc98QwjtURgvRqOPYmAkMUdmSjrOA+MlmL6QMQn5MUhDf6rNZNuPc1LQ==
+
 de-indent@^1.0.2:
   version "1.0.2"
   resolved "https://registry.npm.taobao.org/de-indent/download/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
@@ -3360,6 +3365,18 @@ element-ui@^2.13.1:
     resize-observer-polyfill "^1.5.0"
     throttle-debounce "^1.0.1"
 
+element-ui@^2.13.2:
+  version "2.14.1"
+  resolved "https://registry.yarnpkg.com/element-ui/-/element-ui-2.14.1.tgz#8b5745c7366c1c1a603bb6c021286ea7187e2aa2"
+  integrity sha512-Uje0J12dBaXdyvt/EtuDA8diFbYTdO7uI4QCfl7zmEJmE1WxgCSVKhlRRoL8MDonO8pyNVhB4n0AFAR14g56nw==
+  dependencies:
+    async-validator "~1.8.1"
+    babel-helper-vue-jsx-merge-props "^2.0.0"
+    deepmerge "^1.2.0"
+    normalize-wheel "^1.0.1"
+    resize-observer-polyfill "^1.5.0"
+    throttle-debounce "^1.0.1"
+
 elliptic@^6.0.0:
   version "6.5.1"
   resolved "https://registry.npm.taobao.org/elliptic/download/elliptic-6.5.1.tgz#c380f5f909bf1b9b4428d028cd18d3b0efd6b52b"
@@ -7740,6 +7757,11 @@ schema-utils@^2.6.1:
     ajv "^6.10.2"
     ajv-keywords "^3.4.1"
 
+scrollparent@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/scrollparent/-/scrollparent-2.0.1.tgz#715d5b9cc57760fb22bdccc3befb5bfe06b1a317"
+  integrity sha1-cV1bnMV3YPsivczDvvtb/gaxoxc=
+
 seed-random@^2.2.0:
   version "2.2.0"
   resolved "https://registry.npm.taobao.org/seed-random/download/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54"
@@ -8851,6 +8873,11 @@ uuid@^3.0.1, uuid@^3.3.2:
   resolved "https://registry.npm.taobao.org/uuid/download/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
   integrity sha1-RWjwIW54dg7h2/Ok0s9T4iQRKGY=
 
+uuid@^3.3.3:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
+  integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
+
 validate-npm-package-license@^3.0.1:
   version "3.0.4"
   resolved "https://registry.npm.taobao.org/validate-npm-package-license/download/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@@ -8951,6 +8978,16 @@ vue-loader@^15.7.0:
     vue-hot-reload-api "^2.3.0"
     vue-style-loader "^4.1.0"
 
+vue-observe-visibility@^0.4.4:
+  version "0.4.6"
+  resolved "https://registry.yarnpkg.com/vue-observe-visibility/-/vue-observe-visibility-0.4.6.tgz#878cb8ebcf3078e40807af29774e97105ebd519e"
+  integrity sha512-xo0CEVdkjSjhJoDdLSvoZoQrw/H2BlzB5jrCBKGZNXN2zdZgMuZ9BKrxXDjNP2AxlcCoKc8OahI3F3r3JGLv2Q==
+
+vue-resize@^0.4.5:
+  version "0.4.5"
+  resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-0.4.5.tgz#4777a23042e3c05620d9cbda01c0b3cc5e32dcea"
+  integrity sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg==
+
 vue-router@^3.0.3:
   version "3.1.3"
   resolved "https://registry.npm.taobao.org/vue-router/download/vue-router-3.1.3.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-router%2Fdownload%2Fvue-router-3.1.3.tgz#e6b14fabc0c0ee9fda0e2cbbda74b350e28e412b"
@@ -8977,6 +9014,15 @@ vue-template-es2015-compiler@^1.9.0:
   resolved "https://registry.npm.taobao.org/vue-template-es2015-compiler/download/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
   integrity sha1-HuO8mhbsv1EYvjNLsV+cRvgvWCU=
 
+vue-virtual-scroller@^1.0.0-rc.2:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/vue-virtual-scroller/-/vue-virtual-scroller-1.0.10.tgz#fdf243240001f05bd79aa77f2e2e60403760e2fb"
+  integrity sha512-Hn4qSBDhRY4XdngPioYy/ykDjrLX/NMm1fQXm/4UQQ/Xv1x8JbHGFZNftQowTcfICgN7yc31AKnUk1UGLJ2ndA==
+  dependencies:
+    scrollparent "^2.0.1"
+    vue-observe-visibility "^0.4.4"
+    vue-resize "^0.4.5"
+
 vue@^2.6.10:
   version "2.6.10"
   resolved "https://registry.npm.taobao.org/vue/download/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"
@@ -9183,6 +9229,27 @@ wide-align@^1.1.0:
   dependencies:
     string-width "^1.0.2 || 2"
 
+wl-core@^1.1.8:
+  version "1.1.8"
+  resolved "https://registry.yarnpkg.com/wl-core/-/wl-core-1.1.8.tgz#a009899a505497bbafc1f9caea7117b4def03b45"
+  integrity sha512-ZzDtSWY0dTPBaEr3M892nRTwG4mMiU5xXJG6cnadQAuH6wzs5yRjsdQiatQt36qfUMwgi4fbU3BOQFN0CH5lCQ==
+  dependencies:
+    big.js "^5.2.2"
+    dayjs "^1.8.25"
+
+wl-gantt@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/wl-gantt/-/wl-gantt-1.0.5.tgz#773382cef640bd5e2f899a2fa93e7e3442e7a0a6"
+  integrity sha512-FzRcRC29UDozR1me6tpmitP8+HX6s+/pANAEAuJ3v134k0CjX8tCoRgyRmwK4j51cCtB6fan6o2eke5MMzsWyA==
+  dependencies:
+    core-js "^2.6.5"
+    dayjs "^1.8.16"
+    element-ui "^2.13.2"
+    uuid "^3.3.3"
+    vue "^2.6.10"
+    vue-virtual-scroller "^1.0.0-rc.2"
+    wl-core "^1.1.8"
+
 word-wrap@~1.2.3:
   version "1.2.3"
   resolved "https://registry.npm.taobao.org/word-wrap/download/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"

+ 2 - 6
src/test/java/com/izouma/zhumj/CommonTest.java

@@ -41,6 +41,7 @@ import java.math.BigDecimal;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Paths;
+import java.text.MessageFormat;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
@@ -347,11 +348,6 @@ public class CommonTest {
 
     @Test
     public void testRegex() {
-        Pattern pattern = Pattern.compile("(?<storeName>[\\u4e00-\\u9fa5]+?)(?<roomName>\\d{1,9}).*");
-        Matcher matcher = pattern.matcher("武宁公园220主表");
-        if (matcher.matches()) {
-            System.out.println(matcher.group("storeName"));
-            System.out.println(matcher.group("roomName"));
-        }
+        System.out.println(MessageFormat.format("{a} {b}", "1", "2"));
     }
 }

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff