drew 5 gadi atpakaļ
vecāks
revīzija
dd0bf4bc5f

+ 5 - 0
db/migrate005.sql

@@ -5,3 +5,8 @@ rename table order_free_fee to contract_free_fee;
 alter table order_deduction_fee change order_id contract_store_id bigint not null;
 rename table order_deduction_fee to contract_fixed_fee;
 
+alter table order_room_info change contract_room contract_room_type_id bigint not null;
+alter table order_room_info change room_info_id room_id bigint not null;
+alter table order_room_info drop column order_id;
+rename table order_room_info to contract_reserve_room;
+

+ 4 - 4
src/main/java/com/izouma/zhumj/domain/sale/ContractRoomType.java

@@ -68,7 +68,7 @@ public class ContractRoomType extends BaseEntity implements Serializable {
     private String roomTypeName;
 
     @ApiModelProperty("几间")
-    private String roomNum ;
+    private String roomNum;
 
     @OneToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "roomTypeId", insertable = false, updatable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
@@ -86,9 +86,9 @@ public class ContractRoomType extends BaseEntity implements Serializable {
 
     @ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.DETACH})
     @JoinTable(
-            name = "order_room_info",
-            joinColumns = {@JoinColumn(name = "contract_room", referencedColumnName = "id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))},
-            inverseJoinColumns = {@JoinColumn(name = "room_info_id", referencedColumnName = "id", foreignKey = @ForeignKey(name = "none", value = ConstraintMode.NO_CONSTRAINT))})
+            name = "contract_reserve_room",
+            joinColumns = @JoinColumn(name = "contract_room_type_id", referencedColumnName = "id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)),
+            inverseJoinColumns = {@JoinColumn(name = "room_id", referencedColumnName = "id", foreignKey = @ForeignKey(name = "none", value = ConstraintMode.NO_CONSTRAINT))})
     @ExcelIgnore
     @ApiModelProperty(value = "预定房型", name = "contractRoomInfoList")
     List<RoomInfo> contractRoomInfoList = new ArrayList<>();

+ 30 - 4
src/main/java/com/izouma/zhumj/domain/ContractViolation.java → src/main/java/com/izouma/zhumj/domain/sale/ContractViolation.java

@@ -1,5 +1,7 @@
-package com.izouma.zhumj.domain;
+package com.izouma.zhumj.domain.sale;
 
+import com.alibaba.excel.annotation.ExcelIgnore;
+import com.izouma.zhumj.domain.BaseEntity;
 import com.izouma.zhumj.enums.PenaltyType;
 import com.izouma.zhumj.enums.ViolationType;
 import io.swagger.annotations.ApiModel;
@@ -9,10 +11,10 @@ import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
-import javax.persistence.Entity;
-import javax.persistence.EnumType;
-import javax.persistence.Enumerated;
+import javax.persistence.*;
 import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
 
 @Data
 @Entity
@@ -32,6 +34,12 @@ public class ContractViolation extends BaseEntity {
     @ApiModelProperty("违约类型")
     private ViolationType violationType;
 
+    @ApiModelProperty("时间")
+    private LocalDateTime violationTime;
+
+    @ApiModelProperty("剩余天数")
+    private int days;
+
     @ApiModelProperty("剩余房费")
     private BigDecimal restRent;
 
@@ -41,4 +49,22 @@ public class ContractViolation extends BaseEntity {
     @Enumerated(EnumType.STRING)
     @ApiModelProperty("违约金方式")
     private PenaltyType penaltyType;
+
+    @ApiModelProperty("补充协议编号/告知函编号")
+    private String attachNumber;
+
+    @ApiModelProperty("附件")
+    private String attach;
+
+    @ApiModelProperty("退宿原因")
+    private String reason;
+
+    @ApiModelProperty("是否流失")
+    private boolean customerLost;
+
+    @OneToMany(fetch = FetchType.LAZY)
+    @JoinColumn(name = "residenceId", insertable = false, updatable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
+    @ExcelIgnore
+    private List<ViolationRoomType> violationRoomTypes;
+
 }

+ 5 - 5
src/main/java/com/izouma/zhumj/domain/sale/PhaseRoomType.java

@@ -73,12 +73,12 @@ public class PhaseRoomType extends BaseEntity implements Serializable {
 
     @ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.DETACH})
     @JoinTable(
-            name = "order_room_info",
-            joinColumns = {@JoinColumn(name = "contract_room", referencedColumnName = "id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))},
-            inverseJoinColumns = {@JoinColumn(name = "room_info_id", referencedColumnName = "id", foreignKey = @ForeignKey(name = "none", value = ConstraintMode.NO_CONSTRAINT))})
+            name = "phase_reserve_room",
+            joinColumns = {@JoinColumn(name = "phase_room_type_id", referencedColumnName = "id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))},
+            inverseJoinColumns = {@JoinColumn(name = "room_id", referencedColumnName = "id", foreignKey = @ForeignKey(name = "none", value = ConstraintMode.NO_CONSTRAINT))})
     @ExcelIgnore
-    @ApiModelProperty(value = "预定房型", name = "contractRoomInfoList")
-    List<RoomInfo> contractRoomInfoList = new ArrayList<>();
+    @ApiModelProperty("预定房型")
+    List<RoomInfo> reserveRooms = new ArrayList<>();
 
 }
 

+ 38 - 0
src/main/java/com/izouma/zhumj/domain/sale/ViolationRoomType.java

@@ -0,0 +1,38 @@
+package com.izouma.zhumj.domain.sale;
+
+import com.izouma.zhumj.domain.BaseEntity;
+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 java.math.BigDecimal;
+
+@Data
+@Entity
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@ApiModel("退宿房型")
+public class ViolationRoomType extends BaseEntity {
+    @ApiModelProperty("退宿ID")
+    private Long violationId;
+
+    @ApiModelProperty("门店ID")
+    private Long storeId;
+
+    @ApiModelProperty("门店名称")
+    private String storeName;
+
+    @ApiModelProperty("房型ID")
+    private Long roomTypeId;
+
+    @ApiModelProperty("房型名称")
+    private String roomTypeName;
+
+    @ApiModelProperty("床位数")
+    private int beds;
+}

+ 4 - 1
src/main/java/com/izouma/zhumj/dto/ContractPhase.java

@@ -34,8 +34,11 @@ public class ContractPhase extends BaseEntity {
     @ApiModelProperty("结束时间")
     private LocalDateTime endTime;
 
+    @ApiModelProperty("结束时间")
+    private int months;
+
     @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.DETACH)
     @JoinColumn(name = "contractId")
     @ApiModelProperty("绑定门店")
-    private List<PhaseStore> contractStoreList;
+    private List<PhaseStore> stores;
 }

+ 1 - 1
src/main/java/com/izouma/zhumj/enums/ViolationType.java

@@ -2,7 +2,7 @@ package com.izouma.zhumj.enums;
 
 public enum ViolationType {
     PART("部分"),
-    TOTAL("全部");
+    ALL("全部");
 
     private final String description;
 

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

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

+ 13 - 0
src/main/java/com/izouma/zhumj/service/ContractViolationService.java

@@ -0,0 +1,13 @@
+package com.izouma.zhumj.service;
+
+import com.izouma.zhumj.repo.ContractViolationRepo;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@AllArgsConstructor
+public class ContractViolationService {
+
+    private ContractViolationRepo contractViolationRepo;
+
+}

+ 57 - 0
src/main/java/com/izouma/zhumj/web/ContractViolationController.java

@@ -0,0 +1,57 @@
+package com.izouma.zhumj.web;
+import com.izouma.zhumj.domain.sale.ContractViolation;
+import com.izouma.zhumj.service.ContractViolationService;
+import com.izouma.zhumj.dto.PageQuery;
+import com.izouma.zhumj.exception.BusinessException;
+import com.izouma.zhumj.repo.ContractViolationRepo;
+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.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+
+@RestController
+@RequestMapping("/contractViolation")
+@AllArgsConstructor
+public class ContractViolationController extends BaseController {
+    private ContractViolationService contractViolationService;
+    private ContractViolationRepo contractViolationRepo;
+
+    //@PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/save")
+    public ContractViolation save(@RequestBody ContractViolation record) {
+        if (record.getId() != null) {
+            ContractViolation orig = contractViolationRepo.findById(record.getId()).orElseThrow(new BusinessException("无记录"));
+            ObjUtils.merge(orig, record);
+            return contractViolationRepo.save(orig);
+        }
+        return contractViolationRepo.save(record);
+    }
+
+    @GetMapping("/all")
+    public Page<ContractViolation> all(PageQuery pageQuery) {
+        return contractViolationRepo.findAll(toSpecification(pageQuery,ContractViolation.class), toPageRequest(pageQuery));
+    }
+
+    @GetMapping("/get/{id}")
+    public ContractViolation get(@PathVariable Long id) {
+        return contractViolationRepo.findById(id).orElseThrow(new BusinessException("无记录"));
+    }
+
+    @PostMapping("/del/{id}")
+    public void del(@PathVariable Long id) {
+        contractViolationRepo.deleteById(id);
+    }
+
+    @GetMapping("/excel")
+    @ResponseBody
+    public void excel(HttpServletResponse response, PageQuery pageQuery) throws IOException {
+        List<ContractViolation> data = all(pageQuery).getContent();
+        ExcelUtils.export(response, data);
+    }
+}
+

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

@@ -0,0 +1 @@
+{"tableName":"ContractViolation","className":"ContractViolation","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":"contractId","modelName":"contractId","remark":"合同ID","showInList":true,"showInForm":true,"formType":"number","required":true},{"name":"contractNumber","modelName":"contractNumber","remark":"合同编号","showInList":true,"showInForm":true,"formType":"singleLineText"},{"name":"violationType","modelName":"violationType","remark":"违约类型","showInList":true,"showInForm":true,"formType":"select","required":true,"apiFlag":"1","optionsValue":"[{\"label\":\"部分\",\"value\":\"PART\"},{\"label\":\"全部\",\"value\":\"TOTAL\"}]"},{"name":"restRent","modelName":"restRent","remark":"剩余房费","showInList":true,"showInForm":true,"formType":"number","required":true},{"name":"penalty","modelName":"penalty","remark":"违约金","showInList":true,"showInForm":true,"formType":"number","required":true},{"name":"penaltyType","modelName":"penaltyType","remark":"违约金方式","showInList":true,"showInForm":true,"formType":"select","required":true,"apiFlag":"1","optionsValue":"[{\"label\":\"房费\",\"value\":\"RENT\"},{\"label\":\"押金\",\"value\":\"DEPOSIT\"}]"}],"readTable":false,"dataSourceCode":"dataSource","genJson":"","subtables":[],"update":false,"basePackage":"com.izouma.zhumj","tablePackage":"com.izouma.zhumj.domain.ContractViolation"}

+ 72 - 4
src/main/vue/src/components/FileUpload.vue

@@ -1,11 +1,13 @@
 <template>
     <el-upload
         class="upload-demo"
-        :action="$baseUrl + '/assets/uploadFile'"
+        :action="$baseUrl + '/upload/file'"
         :on-success="onSuccess"
         :on-remove="onRemove"
+        :headers="headers"
+        :file-list="fileList"
     >
-        <el-button type="primary" v-if="!single || fileList.length === 0">点击上传 </el-button>
+        <el-button type="primary" v-if="!single || fileList.length === 0" plain>点击上传 </el-button>
     </el-upload>
 </template>
 
@@ -18,20 +20,86 @@ export default {
             default() {
                 return false;
             }
+        },
+        value: {
+            type: [String, Array]
         }
     },
     data() {
         return {
-            fileList: []
+            fileList: [],
+            emitting: false
         };
     },
+    created() {
+        this.updateFileList();
+    },
+    computed: {
+        headers() {
+            return {
+                Authorization: 'Bearer ' + localStorage.getItem('token')
+            };
+        }
+    },
     methods: {
         onSuccess(res, file, fileList) {
+            file.url = res;
             this.fileList = fileList;
-            this.$emit('input', res.data[0]);
+            console.log(fileList);
+            this.emit();
         },
         onRemove(file, fileList) {
             this.fileList = fileList;
+            this.emit();
+        },
+        updateFileList() {
+            if (this.emitting) return;
+            if (this.value) {
+                if (this.single) {
+                    this.fileList = [
+                        {
+                            name: this.value.substring(this.value.lastIndexOf('/') + 1),
+                            url: this.value
+                        }
+                    ];
+                } else {
+                    let fileList = [];
+                    if (typeof value === 'string') {
+                        for (let url of this.value.split(',')) {
+                            fileList.push({
+                                name: url.substring(url.lastIndexOf('/') + 1),
+                                url: url
+                            });
+                        }
+                    } else {
+                        console.log(this.value);
+                        for (let url of this.value) {
+                            fileList.push({
+                                name: url.substring(url.lastIndexOf('/') + 1),
+                                url: url
+                            });
+                        }
+                    }
+                    this.fileList = fileList;
+                }
+            }
+        },
+        emit() {
+            this.emitting = true;
+            this.this.single
+                ? this.$emit('input', this.fileList.map(i => i.url).join())
+                : this.$emit(
+                      'input',
+                      this.fileList.map(i => i.url)
+                  );
+            this.$nextTick(() => {
+                this.emitting = false;
+            });
+        }
+    },
+    watch: {
+        value() {
+            this.updateFileList();
         }
     }
 };

+ 2 - 2
src/main/vue/src/components/operation/CheckinInfoEdit.vue

@@ -385,8 +385,8 @@ export default {
                                 contractId: res.orderId
                             })
                             .then(res => {
-                                this.personalFeeTypesFree = res.personalFeeTypes || [];
-                                this.personalFeeTypesRequired = res.deductions || [];
+                                this.personalFeeTypesFree = res.freeFeeTypes || [];
+                                this.personalFeeTypesRequired = res.fixedFeeTypes || [];
                                 this.$nextTick(_ => {
                                     this.computedPersonalFeeTypes
                                         .filter(i => i.disabled)

+ 8 - 8
src/main/vue/src/components/sale/ContractStoreChoose.vue

@@ -17,8 +17,8 @@
                     ></el-option>
                 </el-select>
             </el-form-item>
-            <el-form-item label="减免费用" prop="personalFeeTypes">
-                <el-select v-model="formData.personalFeeTypes" value-key="id" multiple placeholder="请选择费用">
+            <el-form-item label="减免费用" prop="freeFeeTypes">
+                <el-select v-model="formData.freeFeeTypes" value-key="id" multiple placeholder="请选择费用">
                     <el-option
                         v-for="item in personalFeeTypeOptions"
                         :label="item.name"
@@ -27,8 +27,8 @@
                     ></el-option>
                 </el-select>
             </el-form-item>
-            <el-form-item label="综合套餐" prop="deductions">
-                <el-select v-model="formData.deductions" value-key="id" multiple placeholder="请选择费用">
+            <el-form-item label="综合套餐" prop="fixedFeeTypes">
+                <el-select v-model="formData.fixedFeeTypes" value-key="id" multiple placeholder="请选择费用">
                     <el-option
                         v-for="item in personalFeeTypeOptions"
                         :label="item.name"
@@ -105,8 +105,8 @@ export default {
                 storeId: null,
                 storeName: null,
                 roomTypeDesc: null,
-                personalFeeTypes: [],
-                deductions: [],
+                freeFeeTypes: [],
+                fixedFeeTypes: [],
                 roomTypes: []
             },
             disableWatch: true,
@@ -181,8 +181,8 @@ export default {
         'formData.storeId'(storeId) {
             if (!this.disableWatch) {
                 this.personalFeeTypeOptions = [];
-                this.formData.personalFeeTypes = [];
-                this.formData.deductions = [];
+                this.formData.freeFeeTypes = [];
+                this.formData.fixedFeeTypes = [];
                 this.formData.roomTypes = [];
                 this.getPersonalFeeTypes();
                 this.updateStoreName();

+ 13 - 4
src/main/vue/src/router.js

@@ -1195,12 +1195,21 @@ const router = new Router({
                     }
                 },
                 {
-                    path: '/saleStoreNoticeList',
-                    name: 'SaleStoreNoticeList',
+                    path: '/contractViolationEdit',
+                    name: 'ContractViolationEdit',
                     component: () =>
-                        import(/* webpackChunkName: "saleStoreNoticeList" */ '@/views/sale/StoreNoticeList.vue'),
+                        import(/* webpackChunkName: "contractViolationEdit" */ '@/views/ContractViolationEdit.vue'),
                     meta: {
-                        title: '门店待办'
+                        title: '退宿编辑'
+                    }
+                },
+                {
+                    path: '/contractViolationList',
+                    name: 'ContractViolationList',
+                    component: () =>
+                        import(/* webpackChunkName: "contractViolationList" */ '@/views/ContractViolationList.vue'),
+                    meta: {
+                        title: '退宿'
                     }
                 }
                 /**INSERT_LOCATION**/

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

@@ -109,7 +109,7 @@
 }
 
 .el-table th.is-leaf {
-    border-bottom: none !important;
+    // border-bottom: none !important;
 }
 
 .menu-tabs .el-tabs__header {

+ 375 - 0
src/main/vue/src/views/ContractViolationEdit.vue

@@ -0,0 +1,375 @@
+<template>
+    <div class="edit-view contract-violation-edit">
+        <el-form :model="formData" :rules="rules" ref="form" label-position="top" size="small" inline>
+            <el-card shadow="never">
+                <span slot="header">退宿信息</span>
+                <el-form-item prop="contractId" label="合同ID">
+                    <el-input type="number" v-model="formData.contractId" disabled></el-input>
+                </el-form-item>
+                <el-form-item prop="contractNumber" label="合同编号">
+                    <el-input v-model="formData.contractNumber" disabled></el-input>
+                </el-form-item>
+                <el-form-item prop="violationType" label="违约退宿类型">
+                    <el-select v-model="formData.violationType" clearable filterable placeholder="请选择">
+                        <el-option
+                            v-for="item in violationTypeOptions"
+                            :key="item.value"
+                            :label="item.label"
+                            :value="item.value"
+                        >
+                        </el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item
+                    prop="attachNumber"
+                    :label="formData.violationType === 'PART' ? '补充协议编号' : '告知函编号'"
+                >
+                    <el-input v-model="formData.attachNumber"></el-input>
+                </el-form-item>
+                <el-form-item prop="violationTime" label="退宿时间">
+                    <el-date-picker
+                        type="datetime"
+                        value-format="yyyy-MM-dd HH:mm:ss"
+                        placeholder="请选择"
+                        v-model="formData.violationTime"
+                    ></el-date-picker>
+                </el-form-item>
+                <el-form-item label="剩余天数">
+                    <el-input-number type="number" :value="formData.days" disabled></el-input-number>
+                </el-form-item>
+                <el-form-item prop="restRent" label="剩余房费">
+                    <el-input-number type="number" :value="formData.restRent" disabled></el-input-number>
+                </el-form-item>
+                <el-form-item prop="penalty" label="违约金">
+                    <el-input-number type="number" v-model="formData.penalty"></el-input-number>
+                </el-form-item>
+                <el-form-item prop="penaltyType" label="违约金方式">
+                    <el-select v-model="formData.penaltyType" clearable filterable placeholder="请选择">
+                        <el-option
+                            v-for="item in penaltyTypeOptions"
+                            :key="item.value"
+                            :label="item.label"
+                            :value="item.value"
+                        >
+                        </el-option>
+                    </el-select>
+                </el-form-item>
+            </el-card>
+
+            <el-card shadow="never" class="room-type" v-if="formData.violationType === 'PART'">
+                <span slot="header">退宿房型</span>
+                <el-form-item prop="contractStoreList" class="form-item-choose-contract">
+                    <el-table :data="formData.violationRoomTypes" stripe>
+                        <el-table-column prop="storeName" label="门店"></el-table-column>
+                        <el-table-column prop="roomTypeName" label="房型"></el-table-column>
+                        <el-table-column prop="price" label="单价"></el-table-column>
+                        <el-table-column prop="beds" label="床位数">
+                            <template v-slot="{ row }">
+                                <el-input-number
+                                    v-model="row.beds"
+                                    controls-position="right"
+                                    :min="0"
+                                    :max="row.maxBeds"
+                                ></el-input-number>
+                            </template>
+                        </el-table-column>
+                    </el-table>
+                </el-form-item>
+            </el-card>
+
+            <el-card shadow="never">
+                <span slot="header">其他</span>
+                <el-form-item prop="reason" label="退宿原因" style="width:570px">
+                    <el-input v-model="formData.reason" type="textarea" rows="5" style="width:505px"></el-input>
+                </el-form-item>
+                <el-form-item prop="attach" label="附件">
+                    <file-upload v-model="formData.attach" single></file-upload>
+                </el-form-item>
+            </el-card>
+
+            <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>
+import { parse, format, differenceInDays, startOfDay, isAfter, isBefore, isEqual } from 'date-fns';
+const parseTime = time => {
+    return parse(time, 'yyyy-MM-dd HH:mm:ss', new Date());
+};
+export default {
+    name: 'ContractViolationEdit',
+    created() {
+        this.$http.get(`/contract/get/${this.$route.query.contractId}`).then(res => {
+            this.contract = res;
+            this.$set(this.formData, 'contractId', res.id);
+            this.$set(this.formData, 'contractNumber', res.contractNumber);
+            this.$set(
+                this.formData,
+                'violationRoomTypes',
+                res.contractStoreList
+                    .map(i => {
+                        i.roomTypes.forEach(e => {
+                            e.storeId = i.storeId;
+                        });
+                        return i.roomTypes;
+                    })
+                    .reduce((a, b) => a.concat(b), [])
+                    .map(i => {
+                        return {
+                            storeId: i.storeId,
+                            roomTypeId: i.roomTypeId,
+                            price: i.price,
+                            beds: i.beds,
+                            maxBeds: i.beds
+                        };
+                    })
+            );
+            this.formData.violationRoomTypes.forEach(item => {
+                this.$http.get(`/storeInfo/get/${item.storeId}`).then(res => {
+                    this.$set(item, 'storeName', res.storeName);
+                });
+                this.$http.get(`/roomTypeInfo/get/${item.roomTypeId}`).then(res => {
+                    this.$set(item, 'roomTypeName', res.typeName);
+                });
+            });
+        });
+        this.$http
+            .get('/contractBill/all', {
+                contractId: this.$route.query.contractId,
+                size: 10000
+            })
+            .then(res => {
+                this.bills = res.content;
+            });
+    },
+    data() {
+        return {
+            saving: false,
+            formData: {
+                violationType: 'ALL',
+                violationRoomTypes: []
+            },
+            rules: {
+                contractId: [{ required: true, message: '请输入合同ID', trigger: 'blur' }],
+                contractNumber: [{ required: true, message: '请输入合同编号', trigger: 'blur' }],
+                attachNumber: [
+                    {
+                        required: true,
+                        validator: (rule, value, callback) => {
+                            if (!value) {
+                                if (this.formData.violationType === 'PART') {
+                                    callback(new Error('请填写补充协议编号'));
+                                } else {
+                                    callback(new Error('请填写告知函编号'));
+                                }
+                                return;
+                            }
+                            callback();
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                violationType: [{ required: true, message: '请输入违约类型', trigger: 'blur' }],
+                restRent: [{ required: true, message: '请输入剩余房费', trigger: 'blur' }],
+                penalty: [{ required: true, message: '请输入违约金', trigger: 'blur' }],
+                penaltyType: [{ required: true, message: '请输入违约金方式', trigger: 'blur' }],
+                violationTime: [{ required: true, message: '请选择退宿时间', trigger: 'blur' }]
+            },
+            violationTypeOptions: [
+                { label: '全部退宿', value: 'ALL' },
+                { label: '部分退宿', value: 'PART' }
+            ],
+            penaltyTypeOptions: [
+                { label: '房费', value: 'RENT' },
+                { label: '押金', value: 'DEPOSIT' }
+            ],
+            contract: {},
+            bills: []
+        };
+    },
+    computed: {
+        currentBill() {
+            if (this.formData.violationTime && this.bills.length) {
+                const violationTime = startOfDay(parseTime(this.formData.violationTime));
+                for (let bill of this.bills) {
+                    const start = parseTime(bill.startTime);
+                    const end = parseTime(bill.endTime);
+                    if (
+                        (isAfter(violationTime, start) && isBefore(violationTime, end)) ||
+                        isEqual(violationTime, start) ||
+                        isEqual(violationTime, end)
+                    ) {
+                        return bill;
+                    }
+                }
+            }
+            return null;
+        },
+        days() {
+            if (this.formData.violationTime && this.currentBill) {
+                const now = startOfDay(parseTime(this.currentBill.endTime));
+                const violationTime = startOfDay(parseTime(this.formData.violationTime));
+                return differenceInDays(now, violationTime);
+            }
+            return 0;
+        },
+        billDays() {
+            if (this.currentBill) {
+                return (
+                    differenceInDays(
+                        startOfDay(parseTime(this.currentBill.endTime)),
+                        startOfDay(parseTime(this.currentBill.startTime))
+                    ) + 1
+                );
+            }
+            return 0;
+        },
+        restRent() {
+            if (this.days) {
+                let restRent = this.$math.bignumber(0);
+                let ratio = this.$math.divide(this.$math.bignumber(this.days), this.$math.bignumber(this.billDays));
+                for (let roomType of this.formData.violationRoomTypes) {
+                    restRent = this.$math.add(
+                        restRent,
+                        this.$math.multiply(
+                            this.$math.bignumber(roomType.beds),
+                            this.$math.multiply(ratio, this.$math.bignumber(roomType.price))
+                        )
+                    );
+                }
+                return this.$math.round(restRent, 2).toString();
+            }
+            return 0;
+        }
+    },
+    methods: {
+        onSave() {
+            this.$refs.form.validate(valid => {
+                if (valid) {
+                    this.submit();
+                } else {
+                    return false;
+                }
+            });
+        },
+        submit() {
+            let data = { ...this.formData };
+            data.violationRoomTypes = data.violationRoomTypes.filter(i => i.beds > 0);
+            this.$store.commit('updateFetchingData', true);
+            this.$http
+                .post('/contractViolation/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(`/contractViolation/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);
+                    }
+                });
+        }
+    },
+    watch: {
+        days(val) {
+            this.$nextTick(() => {
+                this.$set(this.formData, 'days', val);
+            });
+        },
+        restRent(val) {
+            this.$nextTick(() => {
+                this.$set(this.formData, 'restRent', val);
+            });
+        },
+        'formData.violationType'(val) {
+            if (val === 'ALL') {
+                this.formData.violationRoomTypes.forEach(i => {
+                    this.$set(i, 'beds', i.maxBeds);
+                });
+            }
+        }
+    }
+};
+</script>
+<style lang="less" scoped></style>
+<style lang="less">
+.contract-violation-edit {
+    margin: 0;
+    padding: 15px;
+    background: none;
+    .el-card {
+        margin-bottom: 20px;
+        .el-card__header {
+            padding: 12px;
+            font-size: 14px;
+            font-weight: 500;
+        }
+        .el-card__body {
+            padding: 15px 15px 0 15px;
+            .el-form-item {
+                .el-form-item__label {
+                    padding-bottom: 0;
+                    font-size: 13px;
+                    line-height: 24px;
+                }
+            }
+            .el-input {
+                width: 215px;
+            }
+            .el-input-number {
+                width: 215px;
+            }
+            .el-form-item {
+                min-width: 280px;
+            }
+        }
+        &.phases {
+            .el-card__body {
+                padding: 0 15px;
+            }
+        }
+        &.room-type {
+            .el-card__body {
+                padding-top: 0;
+            }
+        }
+    }
+    .form-item-choose-contract {
+        width: 100%;
+        margin: 0 !important;
+        > .el-form-item__content {
+            white-space: nowrap;
+            overflow-x: scroll;
+            > .el-form-item__error {
+                top: inherit;
+                bottom: 0;
+            }
+        }
+    }
+    .btn-add-phase {
+        margin: 10px 0 10px 0;
+    }
+}
+</style>

+ 198 - 0
src/main/vue/src/views/ContractViolationList.vue

@@ -0,0 +1,198 @@
+<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="contractId" label="合同ID"
+>
+                    </el-table-column>
+                    <el-table-column prop="contractNumber" label="合同编号"
+>
+                    </el-table-column>
+                    <el-table-column prop="violationType" label="违约类型"
+                            :formatter="violationTypeFormatter"
+                        >
+                    </el-table-column>
+                    <el-table-column prop="restRent" label="剩余房费"
+>
+                    </el-table-column>
+                    <el-table-column prop="penalty" label="违约金"
+>
+                    </el-table-column>
+                    <el-table-column prop="penaltyType" label="违约金方式"
+                            :formatter="penaltyTypeFormatter"
+                        >
+                    </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: 'ContractViolationList',
+        mixins: [pageableTable],
+        created() {
+            this.getData();
+        },
+        data() {
+            return {
+                multipleMode: false,
+                search: "",
+                url: "/contractViolation/all",
+                downloading: false,
+                        violationTypeOptions:[{"label":"部分","value":"PART"},{"label":"全部","value":"TOTAL"}],
+                        penaltyTypeOptions:[{"label":"房费","value":"RENT"},{"label":"押金","value":"DEPOSIT"}],
+            }
+        },
+        computed: {
+            selection() {
+                return this.$refs.table.selection.map(i => i.id);
+            }
+        },
+        methods: {
+                    violationTypeFormatter(row, column, cellValue, index) {
+                        let selectedOption = this.violationTypeOptions.find(i => i.value === cellValue);
+                        if (selectedOption) {
+                            return selectedOption.label;
+                        }
+                        return '';
+                    },
+                    penaltyTypeFormatter(row, column, cellValue, index) {
+                        let selectedOption = this.penaltyTypeOptions.find(i => i.value === cellValue);
+                        if (selectedOption) {
+                            return selectedOption.label;
+                        }
+                        return '';
+                    },
+            beforeGetData() {
+                if (this.search) {
+                    return { search: this.search };
+                }
+            },
+            toggleMultipleMode(multipleMode) {
+                this.multipleMode = multipleMode;
+                if (!multipleMode) {
+                    this.$refs.table.clearSelection();
+                }
+            },
+            addRow() {
+                this.$router.push({
+                    path: "/contractViolationEdit",
+                    query: {
+                    ...this.$route.query
+                    }
+                });
+            },
+            editRow(row) {
+                this.$router.push({
+                    path: "/contractViolationEdit",
+                    query: {
+                    id: row.id
+                    }
+                });
+            },
+            download() {
+                this.downloading = true;
+                this.$axios
+                    .get("/contractViolation/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(`/contractViolation/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>

+ 7 - 8
src/main/vue/src/views/Operation/RoomStatus.vue

@@ -2097,15 +2097,14 @@ export default {
                     contractNumber: info.contractNumber
                 })
                 .then(res => {
-                    var list = [];
-                    var personalFeeTypes = this.personalFeeTypes;
-                    personalFeeTypes.map(i => {
-                        if (res.personalFeeTypes.findIndex(j => i.id === j.id) > -1) {
-                            list.push(i);
+                    var freeFeeTypes = [];
+                    this.personalFeeTypes.forEach(i => {
+                        if (res.freeFeeTypes.findIndex(j => i.id === j.id) > -1) {
+                            freeFeeTypes.push(i);
                         }
                     });
-                    this.personalFeeTypesFree = list || [];
-                    this.personalFeeTypesRequired = res.deductions || [];
+                    this.personalFeeTypesFree = freeFeeTypes || [];
+                    this.personalFeeTypesRequired = res.fixedFeeTypes || [];
                 });
             if (this.nowMethods != 'filiter' && this.nowMethods != 'change') {
                 this.getContractStaff();
@@ -2315,7 +2314,7 @@ export default {
                         if (action == 'cancel') {
                             this.showCheckForm = true;
                             this.personalFeeTypesFree = [];
-                            this.deductions = [];
+                            this.fixedFeeTypes = [];
                             this.checkInForm = {
                                 checkInType: 'INDIVIDUAL',
                                 bedId: this.chooseBed,

+ 234 - 127
src/main/vue/src/views/sale/ContractEdit.vue

@@ -82,37 +82,36 @@
             </el-card>
 
             <el-card shadow="never" class="phases">
-                <el-collapse v-model="activeNames" accordion>
-                    <el-collapse-item title="1期绑定门店" :name="1">
-                        <el-form-item prop="contractStoreList" class="form-item-choose-contract">
-                            <contract-store-choose
-                                v-for="(item, index) in formData.contractStoreList"
-                                v-model="formData.contractStoreList[index]"
-                                :index="index"
-                                :key="index"
-                                @remove="removeContractStore"
-                                ref="contractStoreChoose"
-                                :checkInType="formData.checkInType"
-                                :storeOptions="storeOptions"
-                            ></contract-store-choose>
-                        </el-form-item>
-                    </el-collapse-item>
-                    <el-collapse-item title="反馈 Feedback" name="2">
-                        <div>控制反馈:通过界面样式和交互动效让用户可以清晰的感知自己的操作;</div>
-                        <div>页面反馈:操作后,通过页面元素的变化清晰地展现当前状态。</div>
-                    </el-collapse-item>
-                    <el-collapse-item title="效率 Efficiency" name="3">
-                        <div>简化流程:设计简洁直观的操作流程;</div>
-                        <div>清晰明确:语言表达清晰且表意明确,让用户快速理解进而作出决策;</div>
-                        <div>帮助用户识别:界面简单直白,让用户快速识别而非回忆,减少用户记忆负担。</div>
-                    </el-collapse-item>
-                    <el-collapse-item title="可控 Controllability" name="4">
-                        <div>用户决策:根据场景可给予用户操作建议或安全提示,但不能代替用户进行决策;</div>
-                        <div>结果可控:用户可以自由的进行操作,包括撤销、回退和终止当前操作等。</div>
+                <el-collapse accordion v-model="activeTab">
+                    <el-collapse-item :name="index" v-for="(item, index) in formData.phases" :key="index">
+                        <template slot="title">
+                            {{ toChinesNum(index + 1) }}期绑定门店
+                            <el-button
+                                v-if="activeTab === index"
+                                @click.stop="addPhaseStore(index)"
+                                type="primary"
+                                plain
+                                size="mini"
+                                style="margin-left:10px"
+                            >
+                                新增门店
+                            </el-button>
+                            <span class="phase-time">{{ item.startTime }} 至 {{ item.endTime }}</span>
+                        </template>
+                        <contract-store-choose
+                            v-for="(item, storeIdx) in item.stores"
+                            v-model="formData.phases[index].stores[storeIdx]"
+                            :index="storeIdx"
+                            :key="storeIdx"
+                            @remove="removePhaseStore(index, $event)"
+                            ref="contractStoreChoose"
+                            :checkInType="formData.checkInType"
+                            :storeOptions="storeOptions"
+                        ></contract-store-choose>
                     </el-collapse-item>
                 </el-collapse>
 
-                <el-button class="btn-add-phase">添加合同期</el-button>
+                <el-button class="btn-add-phase" @click="onAddPhase">添加合同期</el-button>
             </el-card>
 
             <el-card shadow="never">
@@ -429,6 +428,42 @@
                 <el-button @click="submit" type="primary">确认</el-button>
             </span>
         </el-dialog>
+        <el-dialog :visible.sync="showPhaseDialog" title="新增周期" width="400px">
+            <el-form
+                :model="phaseForm"
+                ref="phaseForm"
+                label-width="80px"
+                label-position="right"
+                :rules="phaseFormRules"
+                size="small"
+            >
+                <el-form-item prop="startTime" label="开始时间">
+                    <el-date-picker
+                        v-model="phaseForm.startTime"
+                        type="datetime"
+                        value-format="yyyy-MM-dd HH:mm:ss"
+                        placeholder="请选择"
+                        :disabled="phaseForm.disableStart"
+                    ></el-date-picker>
+                </el-form-item>
+                <el-form-item prop="months" label="月份">
+                    <el-input-number v-model="phaseForm.months" controls-position="right" :min="1"></el-input-number>
+                </el-form-item>
+                <el-form-item prop="endTime" label="结束时间">
+                    <el-date-picker
+                        v-model="phaseForm.endTime"
+                        type="datetime"
+                        value-format="yyyy-MM-dd HH:mm:ss"
+                        placeholder="请选择"
+                        disabled
+                    ></el-date-picker>
+                </el-form-item>
+            </el-form>
+            <span slot="footer">
+                <el-button @click="showPhaseDialog = false">取 消</el-button>
+                <el-button @click="addPhase" type="primary">确 定</el-button>
+            </span>
+        </el-dialog>
     </div>
 </template>
 <script>
@@ -526,7 +561,7 @@ export default {
                                 this.lastFlowBet = res.content[0].flowBet;
                             });
                     }
-                    this.formData = { ...res };
+                    this.formData = { ...res, phases: res.phases || [] };
                     this.$nextTick(_ => {
                         console.log(res.flowBet);
                         this.formData.flowBet = res.flowBet;
@@ -579,7 +614,8 @@ export default {
                 contractEndTime: '',
                 isInvoice: false,
                 brandId: null,
-                status: 'STAY_IN'
+                status: 'STAY_IN',
+                phases: []
             },
             lastFlowBet: 0,
             rules: {
@@ -673,51 +709,20 @@ export default {
             },
             storeOptions: [],
             invoiceType: [
-                {
-                    value: 'SPECIAL',
-                    label: '专票'
-                },
-                {
-                    value: 'GENERAL',
-                    label: '普票'
-                },
-                {
-                    value: 'RECEIPT',
-                    label: '收据'
-                }
+                { value: 'SPECIAL', label: '专票' },
+                { value: 'GENERAL', label: '普票' },
+                { value: 'RECEIPT', label: '收据' }
             ],
             CheckinTypeOptions: [
-                {
-                    value: 'TEAM',
-                    label: '团队包房'
-                },
-                {
-                    value: 'SCATTERED_BEDS',
-                    label: '团队床位'
-                },
-                {
-                    value: 'TEAM_POST_PAID',
-                    label: '团散'
-                }
-                // ,
-                // {
-                //     value: 'INDIVIDUAL',
-                //     label: '散客'
-                // }
+                { value: 'TEAM', label: '团队包房' },
+                { value: 'SCATTERED_BEDS', label: '团队床位' },
+                { value: 'TEAM_POST_PAID', label: '团散' }
+                // ,{  value: 'INDIVIDUAL', label: '散客' }
             ],
             ContractSourceType: [
-                {
-                    value: 'NEW_SIGN',
-                    label: '新签'
-                },
-                {
-                    value: 'RENEWAL',
-                    label: '续签'
-                },
-                {
-                    value: 'ADD_ROOM',
-                    label: '加房'
-                }
+                { value: 'NEW_SIGN', label: '新签' },
+                { value: 'RENEWAL', label: '续签' },
+                { value: 'ADD_ROOM', label: '加房' }
             ],
             departmentGroup: [],
             saleUser: [],
@@ -725,20 +730,11 @@ export default {
             searchingCustomer: false,
             brandOptions: [],
             sourceData: [
-                {
-                    value: 'NETWORK',
-                    label: '网络推广(58同城、赶集网、百姓网、贝壳租房、搜房网,其它)'
-                },
-                {
-                    value: 'PLATFORM',
-                    label: '平台推广(嗨住、巴乐兔、平安好房、蘑菇租房、其它)'
-                },
+                { value: 'NETWORK', label: '网络推广(58同城、赶集网、百姓网、贝壳租房、搜房网,其它)' },
+                { value: 'PLATFORM', label: '平台推广(嗨住、巴乐兔、平安好房、蘑菇租房、其它)' },
                 { value: 'TELEPHONE', label: '电话陌拜' },
                 { value: 'GROUND_PUSH', label: '地推陌拜' },
-                {
-                    value: 'WORD_MOUTH',
-                    label: '口碑推荐(老客户推荐、朋友推荐)'
-                },
+                { value: 'WORD_MOUTH', label: '口碑推荐(老客户推荐、朋友推荐)' },
                 { value: 'PEE', label: '中介同行' },
                 { value: 'DOOR', label: '上门咨询' },
                 { value: 'DOWN', label: '公司下发' },
@@ -751,7 +747,15 @@ export default {
                 { value: 'RETREAT', label: '已退' }
             ],
             renewal: false,
-            curentPhaseId: 1
+            curentPhaseId: 1,
+            showPhaseDialog: false,
+            phaseForm: {},
+            phaseFormRules: {
+                startTime: [{ required: true, message: '请选择开始时间', trigger: 'blur' }],
+                endTime: [{ required: true, message: '请选择结束时间', trigger: 'blur' }],
+                months: [{ required: true, message: '请输入月份', trigger: 'blur' }]
+            },
+            activeTab: 0
         };
     },
     computed: {
@@ -803,6 +807,24 @@ export default {
             if (!isNaN(Number(this.formData.monthlyRent)) && !isNaN(Number(this.formData.bet))) {
                 this.$set(this.formData, 'flowBet', Number(this.formData.monthlyRent) * Number(this.formData.bet));
             }
+        },
+        'phaseForm.startTime'() {
+            if (this.phaseForm.startTime && this.phaseForm.months) {
+                this.$set(
+                    this.phaseForm,
+                    'endTime',
+                    formatDate(addMonths(parseDate(this.phaseForm.startTime), this.phaseForm.months))
+                );
+            }
+        },
+        'phaseForm.months'() {
+            if (this.phaseForm.startTime && this.phaseForm.months) {
+                this.$set(
+                    this.phaseForm,
+                    'endTime',
+                    formatDate(addSeconds(addMonths(parseDate(this.phaseForm.startTime), this.phaseForm.months), -1))
+                );
+            }
         }
     },
     methods: {
@@ -1036,9 +1058,9 @@ export default {
                 .then(res => {
                     this.formData.contractStoreList.push({
                         storeId: '',
-                        personalFeeTypes: [],
+                        freeFeeTypes: [],
                         roomTypeInfo: [],
-                        deductions: [],
+                        fixedFeeTypes: [],
                         roomTypes: []
                     });
                     this.$nextTick(() => {
@@ -1057,9 +1079,43 @@ export default {
                     }
                 });
         },
+        addPhaseStore(index) {
+            Promise.all([
+                ...(this.$refs.contractStoreChoose || []).map(ref => {
+                    return ref.valid();
+                })
+            ])
+                .then(res => {
+                    this.formData.phases[index].stores.push({
+                        storeId: '',
+                        freeFeeTypes: [],
+                        roomTypeInfo: [],
+                        fixedFeeTypes: [],
+                        roomTypes: []
+                    });
+                    this.$nextTick(() => {
+                        document
+                            .querySelector('.phases .el-collapse-item__wrap')
+                            .scrollTo({ left: 100000, behavior: 'smooth' });
+                    });
+                })
+                .catch(e => {
+                    console.log(11111, e);
+                    for (let vm of this.$refs.contractStoreChoose || []) {
+                        if (vm.hasError) {
+                            document
+                                .querySelector('.phases .el-collapse-item__wrap')
+                                .scrollTo({ left: 100000, behavior: 'smooth' });
+                        }
+                    }
+                });
+        },
         removeContractStore(index) {
             this.formData.contractStoreList.splice(index, 1);
         },
+        removePhaseStore(idx0, idx1) {
+            this.formData.phases[idx0].stores.splice(idx1, 1);
+        },
         // changeSaleInfo(dId) {
         //   let lable = this.departmentGroup.filter(i => {
         //     return i.id === dId;
@@ -1115,18 +1171,6 @@ export default {
             })[0];
             return label;
         },
-
-        getSaleUser() {
-            this.$http
-                .get(`/contract/get/${this.$route.query.id}`)
-                .then(res => {
-                    this.formData = res;
-                })
-                .catch(e => {
-                    console.log(e);
-                    this.$message.error(e.error);
-                });
-        },
         onSave() {
             this.$refs.form.validate(valid => {
                 if (valid) {
@@ -1227,6 +1271,53 @@ export default {
             m = (r1.split('.')[1] ? r1.split('.')[1].length : 0) + (r2.split('.')[1] ? r2.split('.')[1].length : 0);
             resultVal = (Number(r1.replace('.', '')) * Number(r2.replace('.', ''))) / Math.pow(10, m);
             return typeof d !== 'number' ? Number(resultVal) : Number(resultVal.toFixed(parseInt(d)));
+        },
+        onAddPhase() {
+            if (this.formData.phases.length === 0) {
+                this.phaseForm = {};
+            } else {
+                this.phaseForm = {
+                    startTime: formatDate(
+                        addSeconds(parseDate(this.formData.phases[this.formData.phases.length - 1].endTime), 1)
+                    ),
+                    disableStart: true
+                };
+            }
+            this.showPhaseDialog = true;
+        },
+        addPhase() {
+            this.$refs.phaseForm.validate(valid => {
+                if (valid) {
+                    this.formData.phases.push({ ...this.phaseForm, stores: [] });
+                    this.showPhaseDialog = false;
+                    this.activeTab = this.formData.phases.length - 1;
+                }
+            });
+        },
+        toChinesNum(num) {
+            let changeNum = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']; //changeNum[0] = "零"
+            let unit = ['', '十', '百', '千', '万'];
+            num = parseInt(num);
+            let getWan = temp => {
+                let strArr = temp
+                    .toString()
+                    .split('')
+                    .reverse();
+                let newNum = '';
+                for (var i = 0; i < strArr.length; i++) {
+                    newNum =
+                        (i == 0 && strArr[i] == 0
+                            ? ''
+                            : i > 0 && strArr[i] == 0 && strArr[i - 1] == 0
+                            ? ''
+                            : changeNum[strArr[i]] + (strArr[i] == 0 ? unit[0] : unit[i])) + newNum;
+                }
+                return newNum;
+            };
+            let overWan = Math.floor(num / 10000);
+            let noWan = num % 10000;
+            if (noWan.toString().length < 4) noWan = '0' + noWan;
+            return overWan ? getWan(overWan) + '万' + getWan(noWan) : getWan(num);
         }
     },
     components: {
@@ -1335,37 +1426,14 @@ export default {
         margin-left: 20px;
     }
 }
+.phase-time {
+    color: @text3;
+    flex-grow: 1;
+    text-align: right;
+    padding-right: 20px;
+}
 </style>
 <style lang="less">
-.stepInfo {
-    .el-table {
-        tr,
-        td,
-        th {
-            background: transparent;
-        }
-    }
-    .bill-money {
-        .el-input__inner {
-            padding: 0 10px;
-        }
-    }
-}
-.el-input__inner[type='number'] {
-    padding-right: 0;
-}
-.form-item-choose-contract {
-    width: 100%;
-    margin: 0 !important;
-    > .el-form-item__content {
-        white-space: nowrap;
-        overflow-x: scroll;
-        > .el-form-item__error {
-            top: inherit;
-            bottom: 0;
-        }
-    }
-}
 .contract-edit {
     margin: 0;
     padding: 15px;
@@ -1439,5 +1507,44 @@ export default {
     .btn-add-phase {
         margin: 10px 0 10px 0;
     }
+    .stepInfo {
+        .el-table {
+            tr,
+            td,
+            th {
+                background: transparent;
+            }
+        }
+        .bill-money {
+            .el-input__inner {
+                padding: 0 10px;
+            }
+        }
+    }
+    .el-input__inner[type='number'] {
+        padding-right: 0;
+    }
+    .form-item-choose-contract {
+        width: 100%;
+        margin: 0 !important;
+        > .el-form-item__content {
+            white-space: nowrap;
+            overflow-x: scroll;
+            > .el-form-item__error {
+                top: inherit;
+                bottom: 0;
+            }
+        }
+    }
+    .phases {
+        .el-collapse-item__wrap {
+            white-space: nowrap;
+            overflow-x: scroll;
+            > .el-form-item__error {
+                top: inherit;
+                bottom: 0;
+            }
+        }
+    }
 }
 </style>

+ 2 - 2
src/main/vue/src/views/sale/ContractHistoryEdit.vue

@@ -940,9 +940,9 @@ export default {
                 .then(res => {
                     this.formData.contractStoreList.push({
                         storeId: '',
-                        personalFeeTypes: [],
+                        freeFeeTypes: [],
                         roomTypeInfo: [],
-                        deductions: [],
+                        fixedFeeTypes: [],
                         roomTypes: []
                     });
                     this.$nextTick(() => {