lrf 9 months ago
parent
commit
930f6e83e0

+ 1 - 1
.eslintrc.js

@@ -17,7 +17,7 @@ module.exports = {
     'no-unused-vars': 'off',
     'no-console': 'off',
     'prettier/prettier': [
-      'warn',
+      'none',
       {
         singleQuote: true,
         trailingComma: 'es5',

+ 218 - 200
src/views/new-plan/arrange/school-arrange.vue

@@ -1,224 +1,235 @@
 <template>
-  <div>
+  <div id="school-arrange">
     <div class="tool">
-      <div class="left">
-        <input type="file" @change="getWorkbook" />
-      </div>
+      <div class="left"></div>
       <div class="right">
-        <button class="button" @click="exportJson">导出JSON</button>
-        <button class="button" @click="exportExcel">导出xlsx</button>
-        <!-- <button class="button" @click="uploadExcel">上传xlsx</button> -->
-        <!-- <button class="button" @click="downloadExcel">下载xlsx</button> -->
+        <el-button type="primary" size="mini" @click="downloadExcel">导出xlsx</el-button>
       </div>
     </div>
-    <!--web spreadsheet组件-->
-    <div class="sheetContainerbox" ref="sheetContainer" id="x-spreadsheet-demo"></div>
+    <excel-view ref="excelView" v-if="!loading" :plan="plan" :classTypeList="classTypeList" :schStuList="schStuList"
+      :schoolList="schoolList" :placeList="placeList" :vacation="vacation" :year="plan.year"
+      @planUpdate="planUpdate" :schPlan="schPlan"></excel-view>
+    <div v-else v-loading="loading" style="height:85vh;width:100%" element-loading-text="加载中,请稍后..."
+      element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.8)"></div>
   </div>
 </template>
 
 <script>
+import _ from 'lodash'
+import excelView from './school-arrange/excel-view.vue'
 import { mapState, createNamespacedHelpers } from 'vuex';
-//引入依赖包
-import zhCN from 'x-data-spreadsheet/src/locale/zh-cn';
-import Spreadsheet from 'x-data-spreadsheet';
-import XLSX from 'xlsx';
-import axios from 'axios';
-import jsondata from './jsondata.js';
-//设置中文
-Spreadsheet.locale('zh-cn', zhCN);
+const { mapActions: trainPlan } = createNamespacedHelpers('trainplan');
+const { mapActions: classtype } = createNamespacedHelpers('classtype');
+const { mapActions: student } = createNamespacedHelpers('student');
+const { mapActions: util } = createNamespacedHelpers('util');
+const { mapActions: location } = createNamespacedHelpers('location');
+const { mapActions: school } = createNamespacedHelpers('school');
+const { mapActions: schPlan } = createNamespacedHelpers('schPlan');
 export default {
-  name: 'xspreadsheet-demo',
-  data() {
+  name: 'school-arrange',
+  props: {},
+  components: { excelView },
+  data: function () {
     return {
-      xs: null,
-      jsondata: jsondata.getJson(),
+      loading: true,
+      // 当前默认设置
+      options: undefined,
+      /**班级类型列表 */
+      classTypeList: [],
+      /**学校上传学生的结果 [{schid:学校编码, sum:1}] 存在哪些学校,学校就是已经上传并入库的*/
+      schStuList: [],
+      /**年度计划 */
+      plan: {},
+      /**计划模板 */
+      template: {},
+      /**需要设置的学校列表 */
+      schoolList: [],
+      /**场地列表 */
+      placeList: [],
+      /**假期列表(计划中的festivals整理出来的) */
+      vacation: [],
+      /**培训计划安排 */
+      schPlan: []
     };
   },
-  mounted() {
-    this.init();
+  computed: {
+    ...mapState(['user', 'defaultOption']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  async created() {
+    await this.getOtherList();
+    await this.loadData();
   },
   methods: {
-    init() {
-      this.xs = new Spreadsheet('#x-spreadsheet-demo', {
-        view: {
-          height: () => this.$refs.sheetContainer?.offsetHeight,
-          width: () => this.$refs.sheetContainer?.offsetWidth,
-        },
-        col: { len: 45 },
-        style: { align: 'center' },
-        showToolbar: true,
-        showGrid: true,
-      })
-        .loadData([])
-        .change(cdata => {
-          // console.log(cdata);
-          console.log('>>>', this.xs.getData());
-        });
-      this.xs
-        .on('cell-selected', (cell, ri, ci) => {
-          console.log('cell:', cell, ', ri:', ri, ', ci:', ci);
-        })
-        .on('cell-edited', (text, ri, ci) => {
-          console.log('text:', text, ', ri: ', ri, ', ci:', ci);
-        });
-      setTimeout(() => {
-        // xs.loadData([{ rows }]);
-        // xs.cellText(14, 3, 'cell-text').reRender();
-        // console.log('cell(8, 8):', this.xs.cell(8, 8));
-        // console.log('cellStyle(8, 8):', this.xs.cellStyle(8, 8));
-      }, 5000);
-      this.xs.loadData(this.jsondata);
-    },
-    loadExcelFile(fileSelected) {
-      var workbook_object = this.getWorkbook(fileSelected);
-      this.xs.loadData(this.stox(workbook_object));
-    },
-    /** 导出excel */
-    exportExcel() {
-      var new_wb = this.xtos(this.xs.getData());
-      /* generate download */
-      XLSX.writeFile(new_wb, `培训计划${new Date().getTime()}.xlsx`);
-    },
-    // 导出JSON
-    exportJson() {
-      console.log(this.xs.getData());
-    },
-    /** 下载并读取excel */
-    downloadExcel() {
-      axios.get('http://localhost:8088/api/v1/test/test1', { responseType: 'arraybuffer' }).then(res => {
-        if (res.status == 200) {
-          var data = res.data;
-          console.log('data', data);
-          data = new Uint8Array(data);
-          var workbook = XLSX.read(data, { type: 'array' });
-          this.xs.loadData(this.stox(workbook));
-        }
-      });
-    },
-    /** 上传excel */
-    uploadExcel() {
-      if (this.xs.getData().length == 0) return;
-      var new_wb = this.xtos(this.xs.getData());
-      var wbout = XLSX.write(new_wb, { type: 'binary' });
-      console.log('new_wb', new_wb);
-      var file = new Blob([this.s2ab(wbout)]);
-      var forms = new FormData();
-      var configs = {
-        headers: { 'Content-Type': 'multipart/form-data' },
-      };
-      forms.append('file', file);
-      forms.append('token', '231231');
+    ...util({ modelFetch: 'fetch' }),
+    ...school(['findByCodes']),
+    ...trainPlan({ getTrainPlan: 'fetch', exportPlan: 'exportSchoolPlan', tranPlanUpdate: 'update' }),
+    ...student({ getSchoolStudent: 'schoolStudent' }),
+    ...classtype({ getClassType: 'query' }),
+    ...location({ getLocation: 'query' }),
+    ...schPlan({ schPlanQuery: 'query', createSchPlan: 'create', updateSchPlan: 'update', setSchPlan: 'schArrange' }),
 
-      axios.post('http://localhost:8088/api/v1/test/test', forms, configs).then(res => {
-        console.log(res);
-      });
-    },
-    s2ab(s) {
-      var buf = new ArrayBuffer(s.length);
-      var view = new Uint8Array(buf);
-      for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xff;
-      return buf;
-    },
-    /** 将x-data-spreadsheet中的数据格式转为xlsx中的workbook */
-    xtos(sdata) {
-      console.log(sdata);
-      var out = XLSX.utils.book_new();
-      sdata.forEach(function(xws) {
-        var aoa = [[]];
-        var rowobj = xws.rows;
-        for (var ri = 0; ri < rowobj.len; ++ri) {
-          var row = rowobj[ri];
-          if (!row) continue;
-          aoa[ri] = [];
-          Object.keys(row.cells).forEach(function(k) {
-            var idx = +k;
-            if (isNaN(idx)) return;
-            aoa[ri][idx] = row.cells[k].text;
-          });
+    async planUpdate({ data }) {
+      // 使用期数 查询是否有数据
+      const { term, ...others } = data;
+      const plan = _.cloneDeep(this.plan)
+      const termData = plan.termnum.find(f => f.term === term)
+      if (termData) {
+        // 有期数
+        const batchnum = _.get(termData, 'batchnum', [])
+        const classnum = _.get(termData, 'classnum', 0)
+        const batchData = batchnum.find(f => f.batch === others.batch)
+        if (batchData) {
+          // 有批次,修改
+          for (const key in others) {
+            const val = others[key]
+            batchData[key] = val;
+          }
+        } else {
+          // 没有批次
+          batchnum.push(others)
         }
-        var ws = XLSX.utils.aoa_to_sheet(aoa);
+        // 计算班级数
+        termData.classnum = batchnum.reduce((p, n) => p + _.get(n, 'class', []).length, 0)
+      } else {
+        // 没有该期数
+        const object = {
+          term,
+          classnum: _.get(others, 'class', []).length,
+          batchnum: [others]
+        }
+        plan.termnum.push(object);
+      }
+      const res = await this.tranPlanUpdate(plan)
+      if (this.$checkRes(res, '计划修改成功', res.errmsg)) {
+        this.loadData();
+      }
+    },
 
-        /** 读取在线中的合并单元格,并写入导出的数据中
-               * merges: Array(19)
-                  0: "A16:P16"
-                  1: "A17:P17"
-                  2: "O2:P2"
-                  3: "F2:G2"
-               */
-        ws['!merges'] = [];
-        xws.merges.forEach(merge => {
-          ws['!merges'].push(XLSX.utils.decode_range(merge));
-        });
 
-        XLSX.utils.book_append_sheet(out, ws, xws.name);
-      });
-      return out;
-    },
-    stox(wb) {
-      var out = [];
-      wb.SheetNames.forEach(function(name) {
-        var o = { name: name, rows: {}, merges: [] };
-        var ws = wb.Sheets[name];
-        var aoa = XLSX.utils.sheet_to_json(ws, { raw: false, header: 1 });
-        aoa.forEach(function(r, i) {
-          var cells = {};
-          r.forEach(function(c, j) {
-            cells[j] = { text: c };
+    /**查询需要根据默认值重新加载的数据 */
+    async needResearchData() {
+      let planyearid = _.get(this.defaultOption, 'planyearid');
+      let planid = _.get(this.defaultOption, 'planid');
+      // #region查询上传学生名单的学校 
+      let res = await this.getSchoolStudent({ planid });
+      if (this.$checkRes(res)) {
+        const { data } = res;
+        console.group('schStuList')
+        console.log(data)
+        console.groupEnd()
+        this.$set(this, `schStuList`, data);
+      }
+      // #endregion
+      // #region 获取年度计划,主要用termnum,计划日历设置部分
+      res = await this.getTrainPlan(planid);
+      if (this.$checkRes(res)) {
+        const data = _.get(res, 'data');
+        if (data) {
+          let termnum = _.get(data, 'termnum', [])
+          termnum = _.orderBy(termnum, ['term'], ['asc']);
+          data.termnum = termnum;
+          console.group('plan')
+          console.log(data)
+          console.groupEnd();
+          this.$set(this, 'plan', data)
+          // 假期
+          let fest = _.get(res.data, 'festivals', []);
+          let vac = fest.map(i => {
+            let object = {};
+            object.id = i._id;
+            object.start = i.begindate;
+            object.end = i.finishdate;
+            object.title = i.name;
+            object.rendering = 'background';
+            object.color = 'red';
+            object.editable = false;
+            return object;
           });
-          o.rows[i] = { cells: cells };
-        });
-        // 设置合并单元格
-        if (ws['!merges']) {
-          ws['!merges'].forEach(merge => {
-            // 修改 cell 中 merge [合并行数,合并列数]
-            let cell = o.rows[merge.s.r].cells[merge.s.c];
-            //无内容单元格处理
-            if (!cell) {
-              cell = { text: '' };
+          this.$set(this, `vacation`, vac);
+          // TODO:原逻辑需要处理批次列表
+          // 可以处理学校,分配人数的学校也在年度计划中存着,可以拿出来换学校名称
+          const needStudentSchool = _.get(data, 'school', [])
+          let schoolList = [];
+          if (needStudentSchool.length > 0) {
+            const schoolCodes = needStudentSchool.map(i => i.code)
+            const schoolListResult = await this.findByCodes({ code: schoolCodes })
+            if (this.$checkRes(schoolListResult)) {
+              // 组成数据,将查询出来的学校 和 需要学校各类型班级的人数带上
+              schoolList = _.get(schoolListResult, 'data', [])
+              schoolList = schoolList.map(i => {
+                const r = needStudentSchool.find(f => f.code === i.code)
+                if (r) {
+                  i.classnum = _.get(r, 'classnum', [])
+                }
+                return i;
+              })
             }
-            cell.merge = [merge.e.r - merge.s.r, merge.e.c - merge.s.c];
-            o.rows[merge.s.r].cells[merge.s.c] = cell;
-            // 修改 merges
-            o.merges.push(XLSX.utils.encode_range(merge));
-          });
+            // TODO:查询出学校不能参培的时间,要禁止该时间段的操作
+
+            console.group('schoolList')
+            console.log(schoolList)
+            console.groupEnd()
+            this.$set(this, 'schoolList', schoolList)
+          }
         }
-        out.push(o);
-      });
-      return out;
+      }
+      // #endregion
+      // #region 获取计划模板
+      res = await this.modelFetch({ model: 'trainmodel', planyearid, planid });
+      if (this.$checkRes(res)) {
+        console.group('template')
+        console.log(res.data)
+        console.groupEnd()
+        this.$set(this, `template`, _.get(res, 'data', {}));
+      }
+      // #endregion
+      res = await this.schPlanQuery({ planid });
+      if (this.$checkRes(res)) {
+        console.group('arrange')
+        console.log(res.data)
+        console.groupEnd()
+        this.$set(this, 'schPlan', res.data)
+      }
+
     },
-    /**
-     * 获取文件
-     * @param fileSelected
-     */
-    getWorkbook(fileSelected) {
-      console.log('fileSelected', fileSelected);
-      let file = fileSelected.target.files[0];
-      let reader = new FileReader();
-      reader.onload = e => {
-        let data = e.target.result,
-          fixedData = this.fixData(data),
-          workbook = XLSX.read(btoa(fixedData), { type: 'base64' });
-        this.xs.loadData(this.stox(workbook));
-        // console.log('workbook', workbook);
-        // console.log('fixedData', fixedData);
-        // console.log('this.stox(workbook)', this.stox(workbook));
-      };
-      reader.readAsArrayBuffer(file);
-      // return workbook
+    /**查询不需要重载的字典数据 */
+    async getOtherList() {
+      let res = await this.getClassType();
+      if (this.$checkRes(res)) {
+        console.group('classTypeList')
+        console.log(res.data)
+        console.groupEnd()
+        this.$set(this, `classTypeList`, res.data);
+      }
+      res = await this.getLocation({ type: '4' })
+      if (this.$checkRes(res)) {
+        // 为每个地点随机生成颜色
+        let list = res.data;
+        if (res.data && res.data.length > 0) {
+          // list = list.map(i => {
+          //   const color = '#' + (parseInt(Math.random() * 0xffffff)).toString(16)
+          //   return { ...i, color }
+          // })
+          this.$set(this, `placeList`, list);
+        }
+      }
     },
-    fixData(data) {
-      var o = '',
-        l = 0,
-        w = 10240;
-      for (; l < data.byteLength / w; ++l) o += String.fromCharCode.apply(null, new Uint8Array(data.slice(l * w, l * w + w)));
-      o += String.fromCharCode.apply(null, new Uint8Array(data.slice(l * w)));
-      return o;
+    async loadData() {
+      try {
+        this.loading = true
+        await this.needResearchData()
+      } catch (error) {
+        console.error(error)
+      } finally {
+        this.loading = false;
+      }
     },
-  },
-  computed: {
-    ...mapState(['user', 'defaultOption']),
-    pageTitle() {
-      return `${this.$route.meta.title}`;
+    /**导出excel */
+    downloadExcel() {
+      this.$refs.excelView.exportExcel();
     },
   },
   watch: {
@@ -226,7 +237,16 @@ export default {
       immediate: true,
       deep: true,
       handler(val) {
-        console.log(val);
+        if (!_.get(this, 'options')) {
+          this.$set(this, `options`, _.cloneDeep(val));
+        } else {
+          let nplanid = _.get(val, 'planid');
+          let oplanid = _.get(this.options, 'planid');
+          if (nplanid && !_.isEqual(nplanid, oplanid)) {
+            this.$set(this, `options`, _.cloneDeep(val));
+            this.needResearchData();
+          }
+        }
       },
     },
   },
@@ -235,19 +255,17 @@ export default {
   },
 };
 </script>
+
 <style lang="less" scoped>
 .tool {
   display: flex;
   justify-content: space-between;
   padding: 10px 0;
+
   .right {
     .button {
       margin: 0 5px 0 0;
     }
   }
 }
-.sheetContainerbox {
-  width: 100%;
-  height: 87vh;
-}
 </style>

+ 54 - 0
src/views/new-plan/arrange/school-arrange/arrange-edit.vue

@@ -0,0 +1,54 @@
+<template>
+  <div id="arrange-edit">
+    <el-form>
+      <el-row type="flex" :gutter="10">
+        <el-col :span="12">
+          <el-form-item label="学校">{{ schoolData.name }}</el-form-item>
+          <el-form-item label="学校层次">{{ schoolData.level }}</el-form-item>
+          <el-form-item label="需要派车">{{ schoolData.hascar === '1' ? '需要' : '不需要' }}</el-form-item>
+          <el-form-item label="总名额">{{ schoolData.total }}</el-form-item>
+          <el-form-item label="剩余名额">{{ schoolData.elseNumber }}</el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="期数">{{ planData.term }}</el-form-item>
+          <el-form-item label="批次">{{ planData.batch }}</el-form-item>
+          <el-form-item label="班级类型">{{ planData.type }}</el-form-item>
+          <el-form-item label="开始时间">{{ planData.startdate }}</el-form-item>
+          <el-form-item label="结束时间">{{ planData.enddate }}</el-form-item>
+          <el-form-item label="总名额">{{ planData.total }}</el-form-item>
+          <el-form-item label="剩余名额">{{ planData.elseNumber }}</el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'arrange-edit',
+  props: {
+    data: { type: Object },
+    planData: { type: Object },
+    schoolData: { type: Object },
+  },
+  components: {},
+  data: function() {
+    return {
+      form: {},
+    };
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+  created() {
+    this.$set(this, 'form', this.data);
+  },
+  methods: {},
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 844 - 0
src/views/new-plan/arrange/school-arrange/excel-view.vue

@@ -0,0 +1,844 @@
+<template>
+  <div id="excel-view">
+    <el-row>
+      <el-col :span="4">
+        <el-button size="mini" @click="getExcelData">获取excel数据</el-button>
+      </el-col>
+      <el-col :span="4">
+        <el-button @click="toAddCol" size="mini" type="primary">新增日期安排</el-button>
+      </el-col>
+    </el-row>
+    <div class="sheetContainerbox" ref="sheetContainer" id="x-spreadsheet-demo"></div>
+
+    <el-dialog :visible.sync="dialog" title="计划变更" @close="toClose" :destroy-on-close="true">
+      <term-add :key="new Date().getTime()" :data="form" :classTypeList="classTypeList" :placeList="placeList" v-bind="$attrs" v-on="$listeners"></term-add>
+    </el-dialog>
+    <el-dialog :visible.sync="dialog2" title="培训安排" @close="toClose" :destroy-on-close="true">
+      <arrange-edit :key="new Date().getTime()" :data="form" :planData="planData" :schoolData="schoolData"></arrange-edit>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+const _ = require('lodash');
+const moment = require('moment');
+import termAdd from './term-add.vue';
+import arrangeEdit from './arrange-edit.vue';
+import XLSX from 'xlsx';
+//引入依赖包
+import zhCN from 'x-data-spreadsheet/src/locale/zh-cn';
+import Spreadsheet from 'x-data-spreadsheet';
+//设置中文
+Spreadsheet.locale('zh-cn', zhCN);
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'excel-view',
+  props: {
+    year: { type: String }, //当前计划年份
+    plan: { type: Object }, //计划
+    classTypeList: { type: Array }, // 班级类型列表
+    schStuList: { type: Array }, // 学校上传学生的结果 [{schid:学校编码, sum:1}]
+    schoolList: { type: Array }, //需要设置的学校列表
+    placeList: { type: Array }, //场地列表
+    schPlan: { type: Array }, // 培训计划安排
+  },
+  components: { termAdd, arrangeEdit },
+  data: function() {
+    return {
+      dialog: false, // 计划dialog
+      dialog2: false, // 培训计划dialog
+      form: {},
+      planData: {}, // 为修改培训计划整理的学校数据
+      schoolData: {}, // 为修改培训计划整理的期批数据
+      /**
+       * merge:合并单元格,实际上是 [sri,sci,eri,eci]
+       * sri:起始行数,在rows中的合并,已经确定了 起始行这个参数
+       * sci:起始列数,在rows中的合并,r.cells.${n} n即为起始列数,也可以确定
+       * eri:结束行数,需要自己确定
+       * eci:结束列数,需要自己确定
+       * 由此可知: 在rows中合并单元格, 我们只需要填写 eri和eci这两个参数.
+       * 即由当前单元格出发,计算要合并几行几列,[行,列]
+       */
+      sheetDefaultConfig: {
+        // name: '普通班', // 表单名(sheet1)
+        // 样式
+        styles: {
+          valign: 'middle',
+          align: 'center',
+        },
+        //行数据
+        rows: {
+          1: {
+            cells: {
+              0: {
+                text: '序号',
+                merge: [3, 0],
+              },
+              1: {
+                text: '学校名称',
+                merge: [3, 0],
+              },
+              2: {
+                text: '计划数',
+                merge: [3, 0],
+              },
+              3: {
+                text: '地区',
+                merge: [3, 0],
+              },
+              4: {
+                text: '期数',
+              },
+            },
+          },
+          2: {
+            cells: {
+              4: {
+                text: '场地',
+              },
+            },
+          },
+          3: {
+            cells: {
+              4: {
+                text: '班级数',
+              },
+            },
+          },
+          4: {
+            cells: {
+              4: {
+                text: '时间',
+              },
+            },
+          },
+          5: {
+            cells: {
+              2: {
+                text: '批次人数',
+                merge: [0, 2],
+              },
+            },
+          },
+          6: {
+            cells: {
+              2: { text: '督导', merge: [1, 1] },
+              4: { text: '天数' },
+            },
+          },
+          7: {
+            cells: {
+              4: { text: '人数' },
+            },
+          },
+          8: {
+            cells: {
+              2: {
+                text: '长春高校用车数',
+                merge: [0, 2],
+              },
+            },
+          },
+        },
+      },
+      // 表格事件设置 如果在 x,y其中一个在无操作中, 则本次事件不操作
+      /**无操作行: 点击事件,行在这里面,则直接当无事发生 */
+      ignoreRows: [0],
+      /**无操作列: 点击事件,列在这里面,则直接当无视发生 */
+      /**计划行: 点击事件,行在这里面,且 列不在无操作列中, 则进入确定具体修改那期,并进入修改*/
+      ignoreCols: [0, 1, 2, 3, 4],
+      planRows: [1, 2, 3, 4],
+      /**学校计划安排起始行 */
+      arrangeStartRow: 8,
+    };
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+  mounted() {
+    this.init();
+  },
+  methods: {
+    init() {
+      this.xs = new Spreadsheet('#x-spreadsheet-demo', {
+        mode: 'read',
+        view: {
+          height: () => this.$refs.sheetContainer?.offsetHeight,
+          width: () => this.$refs.sheetContainer?.offsetWidth,
+        },
+        col: { len: 45 },
+        style: { align: 'center' },
+        showToolbar: false,
+        showGrid: true,
+        showContextmenu: false,
+      }).loadData([]);
+      this.xs
+        .on('cell-selected', (cell, ri, ci) => {
+          console.log('cell:', cell, ', ri:', ri, ', ci:', ci);
+          this.cellClick(ri, ci, cell);
+        })
+        .on('cell-edited', (text, ri, ci) => {
+          console.log('text:', text, ', ri: ', ri, ', ci:', ci);
+        });
+      // 整理表头数据
+      const sheetDatas = this.organizeLine3456();
+      // 检查场地,督导的合并,计算督导的人数
+      this.organizeLine378(sheetDatas);
+      // 输出学校列表
+      this.organizeLineFrom9(sheetDatas);
+      this.xs.loadData(sheetDatas);
+      // 学校安排人数设置
+      this.setSchPlan();
+      // TODO:计算高校用车数
+    },
+
+    // #region 单元格事件
+    /**
+     * 单元格点击事件
+     * @param {Number} ri 点击的行位置
+     * @param {Number} ci 点击的列位置
+     * @param {Object} cell 单元格
+     */
+    cellClick(ri, ci, cell) {
+      const inIngnoreRow = this.ignoreRows.includes(ri);
+      // 在不操作行中,直接返回
+      if (inIngnoreRow) return;
+      const inIngnoreCol = this.ignoreCols.includes(ci);
+      // 在不操作列中,直接返回
+      if (inIngnoreCol) return;
+      // 查看是否在计划行中
+      const inPlanRows = this.planRows.includes(ri);
+      if (inPlanRows) {
+        // 确定点击位置是计划的哪个位置,然后进行计划修改
+        console.log('in plan');
+        // 使用列,确定是哪一批次即可
+        const batch = this.toConfirmPlan(ri, ci);
+        if (batch) {
+          // 没有延迟会又选择好几个
+          _.delay(() => {
+            this.form = batch;
+            this.dialog = true;
+          }, 100);
+        }
+      } else if (ri > this.arrangeStartRow) {
+        // 学校的安排,打开学校安排界面
+        console.log('in arrange');
+        // 确定是哪个学校,哪个批次
+        let number = _.get(cell, 'text', 0);
+        if (number) number = parseInt(number);
+        const res = this.toConfirmArrange(ri, ci);
+        if (!res) {
+          this.$message.error('培训计划安排整理数据发生错误');
+          return;
+        }
+        const { schoolData, planData } = res;
+        console.log(planData)
+        _.delay(() => {
+          this.form = { number };
+          this.schoolData = schoolData;
+          this.planData = planData;
+          this.dialog2 = true;
+        }, 100);
+      } else {
+        console.log('无操作');
+      }
+    },
+    /**
+     * 根据坐标确定培训安排取出数据
+     * 收集学校信息:学校名(name);层次(level);需不需要派车(hascar);总名额;剩余名额
+     * 期批信息;期数;批次;班级类型;开始时间;结束时间;批次总人数;批次剩余名额
+     * 然后提供给修改组件取设置数值
+     * @param {Number} ri 行位置
+     * @param {Number} ci 列位置
+     */
+    toConfirmArrange(ri, ci) {
+      // ri确定学校, ci确定期-批次
+      const sheetName = this.getSheetName();
+      const sheetDatas = this.xs.getData();
+      const sheetData = sheetDatas.find(f => f.name === sheetName);
+      if (!sheetName) return;
+      const ct = this.classTypeList.find(f => f.name === sheetName);
+      // 班级类型
+      const classType = ct.name;
+      // 先找行: 学校名 => 学校, 获取分配限制
+      const schoolName = _.get(sheetData, `rows.${ri}.cells.1.text`);
+      const school = this.schoolList.find(f => f.name === schoolName);
+      if (!school) return;
+      /**
+       * 丰富:
+       * 学校总名额: 根据当前班级类型,取出分配的总量即可
+       * 剩余名额: 计算当前行所有安排的名额作为减数
+       */
+      const classnum = _.get(school, 'classnum', []);
+      const thisTypeClass = classnum.find(f => f.code === ct.code);
+      if (!thisTypeClass) return;
+      // 总名额
+      school.total = _.get(thisTypeClass, 'number');
+      // 剩余名额
+      const thisRow = _.get(sheetData, `rows.${ri}.cells`);
+      if (!thisRow) return;
+      const arrColsDataObject = _.omit(thisRow, this.ignoreCols);
+      const arrColsDatas = [];
+      for (const key in arrColsDataObject) {
+        const e = arrColsDataObject[key];
+        let number = _.get(e, 'text', 0);
+        if (number) number = parseInt(number);
+        arrColsDatas.push(number);
+      }
+      const et = arrColsDatas.reduce((p, n) => p + n, 0);
+      school.elseNumber = school.total - et;
+      /**
+       * 确定批次,因为之后修改了向3,4,5行都注入了 term和batch,方便快速定位期批.
+       * 所以固定取时间行(因为时间行完全不涉及合并问题,就是跟着批次走)
+       */
+      const row5SameColCell = this.xs.cell(4, ci, 0);
+      const { term, batch } = row5SameColCell;
+      const termnum = this.plan.termnum.find(f => f.term === term);
+      if (!termnum) return;
+      const batchnum = _.get(termnum, 'batchnum');
+      if (!batchnum) return;
+      const batchData = batchnum.find(f => f.batch === batch);
+      const planData = {
+        term,
+        termid: _.get(termnum, '_id'),
+        batch,
+        batchid: _.get(batchData, '_id'),
+        startdate: _.get(batchData, 'startdate'),
+        enddate: _.get(batchData, 'enddate'),
+        type: classType,
+      };
+      /**
+       * 丰富:
+       * 总名额: 通过行数据可以直接获取
+       * 剩余名额: 需要算列总和
+       */
+      /**总名额 */
+      const total = _.get(sheetData, `rows.5.cells.${ci}.text`);
+      planData.total = total;
+      /**计算列和 */
+      let keys = Object.keys(sheetData.rows).map(i => parseInt(i));
+      keys = keys.filter(f => f > this.arrangeStartRow);
+      const schoolRowObject = _.pick(sheetData.rows, keys);
+      const arr = [];
+      for (const key in schoolRowObject) {
+        const e = schoolRowObject[key];
+        const obj = _.get(e, 'cells', {});
+        if (!obj || Object.keys(obj).length < 0) continue;
+        const tc = _.get(obj, `${ci}.text`, 0);
+        arr.push(parseInt(tc));
+      }
+      const inNumber = arr.reduce((p, n) => p + n, 0);
+      const elseNumber = total - inNumber;
+      planData.elseNumber = elseNumber;
+      return { schoolData: school, planData };
+    },
+
+    /**
+     * 根据坐标确定计划期并取出数据
+     * @param {Number} ri 行位置
+     * @param {Number} ci 列位置
+     */
+    toConfirmPlan(ri, ci) {
+      const excelDatas = this.xs.getData();
+      const sheetName = this.getSheetName();
+      const allData = excelDatas.find(f => f.name === sheetName);
+      if (!allData) return;
+      /**返回的结果变量 */
+      let result;
+      let batchIndex = 0;
+      let term;
+      // 不需要看行,只看列在哪,因为已经冻结了所有表格,无法修改.固定走第二行拿到期数,然后去计划中换数据进行修改
+      const rowDataObj = allData.rows[1].cells;
+      const keys = Object.keys(rowDataObj).map(i => parseInt(i));
+      /**定位列分几种情况:
+       * 1.该列没有合并单元格,那就能在key中直接找到
+       * 2.该列有合并单元格,那就不一定能在里面直接找到:只有合并的起始单元格才能被直接找到,需要查看 点击的单元格 是否在 被合并的单元格中.
+       */
+      const hasKey = keys.includes(ci);
+      if (hasKey) {
+        // 直接找到的一定都是第一个单元格
+        const cellData = rowDataObj[ci];
+        term = _.get(cellData, 'text');
+      } else {
+        // 将所有列展开,检查是否在被合并的单元格中
+        for (const key in rowDataObj) {
+          // 无操作列不要
+          if (this.ignoreCols.includes(parseInt(key))) continue;
+          const cols = [parseInt(key)];
+          const { merge = [], text } = rowDataObj[key];
+          // 没有两行直接返回
+          if (merge.length === 2) {
+            const last = _.last(merge);
+            if (last) {
+              for (let i = 1; i <= last; i++) {
+                cols.push(parseInt(i) + parseInt(key));
+              }
+            }
+          }
+          // 看下点击的列是否在这里面
+          const res = cols.find(f => f === ci);
+          if (res) {
+            batchIndex = cols.findIndex(f => f === ci);
+            term = text;
+            break;
+          }
+        }
+      }
+      if (term && batchIndex >= 0) {
+        const termnum = this.plan.termnum.find(f => f.term === term);
+        if (termnum) {
+          const batch = _.get(termnum, `batchnum.${batchIndex}`, {});
+          result = { ...batch, term: _.get(termnum, 'term') };
+        }
+      }
+      return result;
+    },
+    /**获取当前浏览的表格名 */
+    getSheetName() {
+      const sheetName = _.get(this.xs, 'sheet.data.name');
+      return sheetName;
+    },
+    // #endregion
+
+    // #region excel初始化数据整理
+    /**
+     * 设置excel中学校安排的数据
+     * 根据arrange的type:班级类型,确定是普通班还是特殊班
+     * 然后计算出该数据所在的横纵坐标,使用 this.xs.cellText(ri,ci,text,sheetIndex)
+     */
+    setSchPlan() {
+      const list = this.schPlan.filter(f => f.arrange.length > 0);
+      const datas = this.xs.getData();
+      for (let si = 0; si < datas.length; si++) {
+        const sheetData = datas[si];
+        const name = _.get(sheetData, 'name');
+        const ct = this.classTypeList.find(f => f.name === name);
+        // 这里不能找到不到,如果找不到,那就中断别找了
+        if (!ct) continue;
+        const termRowObject = sheetData.rows[1].cells;
+        const arrangeCols = _.omit(termRowObject, this.ignoreCols);
+        const termRowKeys = Object.keys(arrangeCols);
+        const termRowValues = Object.values(arrangeCols);
+        for (const i of list) {
+          const { arrange, schid } = i;
+          const schIndex = this.schoolList.findIndex(f => f.code === schid);
+          // 小于零就是没有该学校,那就不要处理
+          if (schIndex < 0) continue;
+          // 查看有没有当前要处理的班级类型的安排
+          const thisTypeClassArrange = arrange.filter(f => f.type === ct.code);
+          if (thisTypeClassArrange.length <= 0) continue;
+          // 计算行位置
+          const ri = this.arrangeStartRow + schIndex + 1;
+          // 计算列位置: 先确定是普通班还是特殊班,然后
+          for (const a of arrange) {
+            const { term, batchid, number } = a;
+            let ci;
+            // term 确定列范围, batchid确定索引, 两个值之和就是列的位置
+            const valIndex = termRowValues.findIndex(f => f.text === term);
+            if (valIndex < 0) continue;
+            const termStartPos = termRowKeys[valIndex];
+            // 再确定批次索引
+            const termnum = this.plan.termnum.find(f => f.term === term);
+            if (!termnum) continue;
+            const batchnum = _.get(termnum, 'batchnum');
+            if (!batchnum) continue;
+            const batchIndex = batchnum.findIndex(f => f._id === batchid);
+            if (batchIndex >= 0) ci = batchIndex + parseInt(termStartPos);
+            if (ci) {
+              this.xs.cellText(ri, ci, number, si);
+            }
+          }
+        }
+      }
+      this.xs.reRender();
+    },
+
+    /**
+     * 整理学校数据
+     * 从3456或378后开始都行,反正不影响计划
+     * @param {Object} sheetData organizeLine3456返回的数据
+     */
+    organizeLineFrom9(sheetDatas) {
+      for (const sheetData of sheetDatas) {
+        const name = _.get(sheetData, 'name');
+        const ct = this.classTypeList.find(f => f.name === name);
+        // 这里不能找到不到,如果找不到,那就中断别找了
+        if (!ct) continue;
+        let rowKey = this.arrangeStartRow + 1;
+        let dataIndex = 0;
+        for (let i = 0; i < this.schoolList.length; i++) {
+          const sch = this.schoolList[i];
+          const clas = _.get(sch, 'classnum', []);
+          const typeCla = clas.find(f => f.code === ct.code);
+          if (!typeCla) continue;
+          const num = _.get(typeCla, 'number', 0);
+          if (num <= 0) continue;
+          dataIndex = dataIndex + 1;
+          const obj = {
+            0: { text: dataIndex },
+            1: { text: _.get(sch, 'name', '') },
+            2: { text: num },
+            3: { text: _.get(sch, 'address', '') },
+          };
+          sheetData.rows[rowKey] = { cells: obj };
+          rowKey = rowKey + 1;
+        }
+      }
+    },
+    /**
+     * 处理场地的合并及督导天数,人数行合并及内容
+     * 场地合并: 相邻且相同场地合并;
+     * 督导天数合并: 与场地合并一致;
+     * 督导天数内容: 多个批次的 最晚结束时间 - 最早开始时间;
+     * 督导人数合并: 和期数合并一致;
+     * 督导人数内容: 几个场地几个人;
+     * @param {Array<Object>} sheetDatas organizeLine3456返回的数据
+     */
+    organizeLine378(sheetDatas) {
+      // 场地合并
+      // 取出期数行
+      for (const sheetData of sheetDatas) {
+        const name = _.get(sheetData, 'name');
+        const ct = this.classTypeList.find(f => f.name === name);
+        // 这里不能找到不到,如果找不到,那就中断别找了
+        if (!ct) continue;
+        const termRowObject = sheetData.rows[1].cells;
+        const termColsMappings = [];
+        // 整理出期范围
+        for (const key in termRowObject) {
+          // 无操作列直接跳过
+          if (this.ignoreCols.includes(parseInt(key))) continue;
+          const termObject = termRowObject[key];
+          const cols = [parseInt(key)];
+          const { merge = [], text } = termObject;
+          // 没有两行直接返回
+          if (merge.length === 2) {
+            const last = _.last(merge);
+            if (last) {
+              for (let i = 1; i <= last; i++) {
+                cols.push(parseInt(i) + parseInt(key));
+              }
+            }
+          }
+          termColsMappings.push({ term: text, cols, merge, col: parseInt(key) });
+        }
+
+        // 先把督导人数行处理了
+        const ddpersonNumRowObject = {};
+        for (const i of termColsMappings) {
+          const { term, merge = [], col } = i;
+          const obj = { text: 0, merge };
+          const r = this.plan.termnum.find(f => f.term === term);
+          if (r) {
+            const bm = _.get(r, 'batchnum', []);
+            let batchnum = this.batchFilterByClassType(bm, ct.code);
+            if (batchnum.length <= 0) continue;
+            const placeList = _.uniq(_.compact(batchnum.map(i => _.get(i, 'place')).filter(f => f !== '')));
+            obj.text = placeList.length;
+          }
+          ddpersonNumRowObject[col] = obj;
+        }
+        const old7Cells = _.get(sheetData, `rows.7.cells`, {});
+        sheetData.rows[7] = { cells: { ...old7Cells, ...ddpersonNumRowObject } };
+        const placeRowObject = sheetData.rows[2].cells;
+        // 处理督导天数,再查相邻场地是否相同
+        for (const tcm of termColsMappings) {
+          const { cols } = tcm;
+          const inSameTermDatas = _.pick(placeRowObject, cols);
+          const keys = Object.keys(inSameTermDatas);
+          for (const key in inSameTermDatas) {
+            const cell = inSameTermDatas[key];
+            const isMerged = _.get(cell, 'isMerged', false);
+            if (isMerged) continue;
+            const text = _.get(cell, 'text');
+            const keyIndex = keys.findIndex(f => f === key);
+            let nextKeyIndex = keyIndex + 1;
+            //  需要确定当前场地和下一个场地是否一致.所以需要确定当前列是否是最后一列,如果是最后一列,就不需要处理了,因为没有后面.
+            while (nextKeyIndex < keys.length) {
+              const nextKey = keys[nextKeyIndex];
+              const nextCell = inSameTermDatas[nextKey];
+              const nextCellText = _.get(nextCell, 'text');
+              if (text === nextCellText) {
+                // 一致,主单元格写入合并参数
+                const merge = _.get(inSameTermDatas[key], 'merge', [0, 0]);
+                const newLast = _.last(merge) + 1;
+                merge[merge.length - 1] = newLast;
+                inSameTermDatas[key].merge = merge;
+                // 当前单元格写入isMerged:true
+                inSameTermDatas[nextKey].isMerged = true;
+                nextKeyIndex = nextKeyIndex + 1;
+                continue;
+              }
+              // 下一个单元格的场地与当前单元格场地不一致,不处理,直接跳过
+              break;
+            }
+          }
+        }
+        // 删掉被合并的单元格
+        for (const key in placeRowObject) {
+          const value = placeRowObject[key];
+          if (_.get(value, 'isMerged', false)) delete placeRowObject[key];
+        }
+        // 督导天数的合并同 场地行一致,内容由 时间行计算而来
+        // 时间行数据
+        const timeRowObject = sheetData.rows[4].cells;
+        // 督导天数行
+        let ddDaysRowObject = sheetData.rows[6].cells;
+        const timeColsMappings = [];
+        // 根据场地范围,再计算天数,整理出督导天数行
+        for (const key in placeRowObject) {
+          // 无操作列直接跳过
+          if (this.ignoreCols.includes(parseInt(key))) continue;
+          const object = placeRowObject[key];
+          const cols = [parseInt(key)];
+          const { merge = [] } = object;
+          // 没有两行直接返回
+          if (merge.length === 2) {
+            const last = _.last(merge);
+            if (last) {
+              for (let i = 1; i <= last; i++) {
+                cols.push(parseInt(i) + parseInt(key));
+              }
+            }
+          }
+          timeColsMappings.push({ cols, merge, col: parseInt(key) });
+          const timeDataInPlaceRange = _.pick(timeRowObject, cols);
+          let days = [];
+          for (const key in timeDataInPlaceRange) {
+            const cell = timeDataInPlaceRange[key];
+            const text = _.get(cell, 'text');
+            if (text === '') continue;
+            let arr = text.split('-');
+            if (arr.length <= 0) continue;
+            arr = arr.map(i => this.excelDateToStringDate(i));
+            days.push(...arr);
+          }
+          // 升序排序
+          days = days.sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
+          const start = _.head(days);
+          const last = _.last(days);
+          const diff = moment(last).diff(start, 'd');
+          const m = _.get(object, 'merge');
+          const cell = { text: diff };
+          if (m) cell.merge = m;
+          ddDaysRowObject[key] = cell;
+        }
+      }
+    },
+    /**组织表头,3&4&5&6行的数据:场地,班级,时间,批次人数
+     */
+    organizeLine3456() {
+      /**
+       * 这几行主要都是由批次信息得来的
+       * 场地(3),班级数(4),时间(5),批次人数(6)都是可以在批次中获取的.
+       * 期数可以直接获取,但是合并单元格的长度是根据批次来的
+       */
+      const sheetDataTemplate = _.cloneDeep(this.sheetDefaultConfig);
+      const result = [];
+      // 根据班级类型开始生成对应excel的数据
+      for (const ct of this.classTypeList) {
+        const sheetData = _.cloneDeep(sheetDataTemplate);
+        // 先给名字
+        sheetData.name = ct.name;
+        // 获取本excel的预设数据,之后的操作都在这上面搞
+        let rows = sheetData.rows;
+        // 对期进行循环
+        const termnum = _.get(this.plan, 'termnum', []);
+        for (const t of termnum) {
+          const { batchnum: bm = [], term } = t;
+          /**
+           * 因为根据班级来分sheet,所以从最开始就把班级都分开
+           * 根据batchnum下的classnum来决定,如果有1个班级符合当前的班级类型.
+           * 那就留下这个班,这个批次,这个期
+           * 如果1个符合条件的班级都没有,那就直接爆了这期
+           */
+          let batchnum = this.batchFilterByClassType(bm, ct.code);
+          if (batchnum.length <= 0) continue;
+          const { pos, merge } = this.termLineDeal(rows, term, batchnum);
+          for (const b of batchnum) {
+            const { place, class: cla = [], startdate, enddate } = b;
+            // 场地行处理
+            const p = this.placeList.find(f => f._id === place);
+            let pstr = place;
+            if (p) pstr = _.get(p, 'name');
+            const params = { term, batch: b.batch };
+            this.batchLineDeal(rows, 2, pstr, params);
+            // 班级数处理
+            const clanum = cla.length;
+            this.batchLineDeal(rows, 3, clanum, params);
+            // 时间处理
+            const sStr = moment(startdate).format('M.D');
+            const eStr = moment(enddate).format('M.D');
+            const timeStr = `${sStr}-${eStr}`;
+            this.batchLineDeal(rows, 4, timeStr, params);
+            // 批次人数
+            const batchPersonNumber = cla.reduce((p, n) => parseInt(p) + parseInt(_.get(n, 'number', 0)), 0);
+            this.batchLineDeal(rows, 5, batchPersonNumber, params);
+          }
+        }
+        result.unshift(sheetData);
+      }
+
+      return result;
+    },
+    /**
+     * 针对批次的统一处理方式
+     * @param {Object} rows excel的所有行数据
+     * @param {String} linePos 行位置
+     * @param {String} text 显示内容
+     * @param {Object} params 额外参数{term,batch}
+     */
+    batchLineDeal(rows, linePos, text, params = {}) {
+      // 获取行数据
+      const row = _.get(rows, `${linePos}.cells`, {});
+      // 计算单元格的起始位置
+      const pos = this.computedPosition(row);
+      const cell = { text, ...params };
+      // 不需要计算合并
+      row[pos] = cell;
+    },
+    /**
+     * 设置期数行
+     * @param {Object} rows excel的所有行数据
+     * @param {String} term 期数
+     * @param {Array} batchnum 批次
+     */
+    termLineDeal(rows, term, batchnum) {
+      // 获取行数据
+      const row = _.get(rows, `1.cells`, {});
+      // 计算单元格的起始位置
+      const pos = this.computedPosition(row);
+      // 设置单元格的具体内容(显示内容和单元格合并的设置)
+      const cell = { text: term };
+      const blen = batchnum.length;
+      if (blen > 0) cell.merge = [0, blen - 1];
+      row[pos] = cell;
+      return { pos, merge: cell.merge };
+    },
+    /**
+     * 计算该单元格所在列的位置.
+     * @param {Object} row 当前行
+     */
+    computedPosition(row) {
+      // 获取最后单元格的位置
+      const keys = Object.keys(row);
+      const lastKey = _.last(keys);
+      const last = row[lastKey];
+      // 设置单元格的位置
+      let pos = parseInt(lastKey) + 1;
+      if (!_.isObject(last)) {
+        console.error(`${pos}_${lastKey}解析错误-单元格设置不是object类型`);
+        return;
+      }
+      const lastCellMerge = _.get(last, 'merge', []);
+      if (lastCellMerge.length > 0) {
+        // 说明有合并操作,需要将被合并的单元格留下来后,再继续添加
+        // [r,c]: r是行数合并; c:列合并主要看c,需要往后多窜c个位置, pos + c
+        const ec = _.last(lastCellMerge);
+        pos = parseInt(pos) + parseInt(ec);
+      }
+      return pos;
+    },
+    /**
+     * 根据班级类型过滤批次
+     * @param {Array} batch 批次
+     * @param {String} code 班级类型编码
+     */
+    batchFilterByClassType(batch, code) {
+      let batchnum = [];
+      for (const bnum of batch) {
+        const { class: clas = [], ...others } = bnum;
+        const r = clas.find(f => f.type === code);
+        if (!r) continue;
+        const thisTypeClass = clas.filter(f => f.type === code);
+        const obj = { ...others, class: thisTypeClass };
+        batchnum.push(obj);
+      }
+      return batchnum;
+    },
+    // #endregion
+
+    /**关闭对话框 */
+    toClose() {
+      this.form = {};
+      this.planData = {};
+      this.schoolData = {};
+    },
+    /**打开新增日期安排的对话框 */
+    toAddCol() {
+      this.dialog = true;
+    },
+    /**
+     * 拼接成正常时间: YYYY-MM-DD
+     * @param {String} pointDate 带点的缩短时间 7.1
+     */
+    excelDateToStringDate(pointDate) {
+      return moment(`${this.year}.${pointDate}`).format('YYYY-MM-DD');
+    },
+
+    /**获取excel数据 */
+    getExcelData() {
+      const data = this.xs.getData();
+      console.log(data);
+      const copy = this.xs.copy();
+      console.log(copy);
+      /**
+       * 由期行 的位置与合并数量,可以确定一期有几批次,批次的场地,班级,
+       * 批次还原的原则: 场地,时间; 如果场地和时间都一致还存在2条以上的数据,那就说明分配有问题.在一个场地一个时间只能有个1个批次
+       * 所以根据场地和时间判断数据是否新添加的(其实判断是不是新数据并没什么用,要做的是把数据整理出来)
+       * 根据时间和场地,与plan中的数据对比:存在-修改数据;不存在:创建数据
+       */
+    },
+    /** 导出excel */
+    exportExcel() {
+      var new_wb = this.xtos(this.xs.getData());
+      /* generate download */
+      XLSX.writeFile(new_wb, `培训计划${new Date().getTime()}.xlsx`);
+    },
+    xtos(sdata) {
+      console.log(sdata);
+      var out = XLSX.utils.book_new();
+      sdata.forEach(function(xws) {
+        var aoa = [[]];
+        var rowobj = xws.rows;
+        for (var ri = 0; ri < rowobj.len; ++ri) {
+          var row = rowobj[ri];
+          if (!row) continue;
+          aoa[ri] = [];
+          Object.keys(row.cells).forEach(function(k) {
+            var idx = +k;
+            if (isNaN(idx)) return;
+            aoa[ri][idx] = row.cells[k].text;
+          });
+        }
+        var ws = XLSX.utils.aoa_to_sheet(aoa);
+
+        /** 读取在线中的合并单元格,并写入导出的数据中
+               * merges: Array(19)
+                  0: "A16:P16"
+                  1: "A17:P17"
+                  2: "O2:P2"
+                  3: "F2:G2"
+               */
+        ws['!merges'] = [];
+        xws.merges.forEach(merge => {
+          ws['!merges'].push(XLSX.utils.decode_range(merge));
+        });
+
+        XLSX.utils.book_append_sheet(out, ws, xws.name);
+      });
+      return out;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.sheetContainerbox {
+  width: 100%;
+  height: 80vh;
+}
+</style>

+ 173 - 0
src/views/new-plan/arrange/school-arrange/term-add.vue

@@ -0,0 +1,173 @@
+<template>
+  <div id="term-editor">
+    <el-form :model="form" ref="form" :rules="formRules" label-width="80px" size="small" @submit.native.prevent
+      style="padding: 15px;">
+      <el-form-item label="开始时间" prop="startdate">
+        <el-date-picker v-model="form.startdate" type="date" format="yyyy-MM-dd" value-format="yyyy-MM-dd"
+          :picker-options="startPickerOptions">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item label="结束时间" prop="enddate">
+        <el-date-picker v-model="form.enddate" type="date" format="yyyy-MM-dd" value-format="yyyy-MM-dd"
+          :picker-options="pickerOptions" :default-value="getEndTimeDefault()">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item label="期数" prop="term" required> <el-input v-model="form.term"></el-input>
+      </el-form-item>
+      <el-form-item label="批次" prop="batch" required> <el-input v-model="form.batch"></el-input></el-form-item>
+      <el-form-item label="培训场地" prop="place" required>
+        <el-select v-model="form.place" placeholder="请选择本批次的培训场地">
+          <el-option v-for="i in placeList" :key="i._id" :label="i.name" :value="i._id"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="班级">
+        <el-row>
+          <el-col :span="24">
+            <el-button type="primary" icon="el-icon-plus" @click="addClass()">添加班级</el-button>
+          </el-col>
+          <el-alert :closable="false" title="班级名称在同一期不可重复;(不论特殊班级,还是正常班级)" type="warning" show-icon center></el-alert>
+          <el-table size="mini" :data="form.class">
+            <el-table-column align="center" label="班级">
+              <template v-slot="{ row }">
+                <el-input v-model="row.name"></el-input>
+              </template>
+            </el-table-column>
+            <el-table-column align="center" label="人数">
+              <template v-slot="{ row }">
+                <el-input v-model="row.number" type="number"></el-input>
+              </template>
+            </el-table-column>
+            <el-table-column align="center" label="类型">
+              <template v-slot="{ row }">
+                <el-select v-model="row.type">
+                  <el-option v-for="(i, index) in classTypeList" :key="index" :label="i.name"
+                    :value="i.code"></el-option>
+                </el-select>
+              </template>
+            </el-table-column>
+            <el-table-column align="center" label="操作" width="50px">
+              <template v-slot="{ row, $index }">
+                <el-button type="text" icon="el-icon-delete" @click="toDeleteClass($index)"></el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-row>
+      </el-form-item>
+      <el-form-item>
+        <el-row type="flex" align="middle" justify="space-around">
+          <el-col :span="6">
+            <el-button type="primary" @click="saveForm">保存</el-button>
+          </el-col>
+        </el-row>
+      </el-form-item>
+    </el-form>
+
+  </div>
+</template>
+
+<script>
+import _ from 'lodash';
+const moment = require('moment');
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'term-editor',
+  props: {
+    data: { type: Object },
+    year: { type: null },
+    vacation: { type: Array, default: () => [] },
+    classTypeList: { type: Array, default: () => [] },
+    placeList: { type: Array, default: () => [] },// 培训场地
+
+  },
+  components: {},
+  data: function () {
+    return {
+      form: { class: [] },
+      formRules: {
+        startdate: [{ required: true, message: '请选择开始时间', trigger: 'blur' }],
+        enddate: [{ required: true, message: '请选择结束时间', trigger: 'blur' }],
+        term: [{ required: true, message: '请输入期数', trigger: 'blur' }],
+        batch: [{ required: true, message: '请输入批次', trigger: 'blur' }],
+        place: [{ required: true, message: '请选择培训场地', trigger: 'blur' }],
+      },
+      pickerOptions: {
+        disabledDate: time => this.setDisabledDate(time),
+      },
+      startPickerOptions: {
+        disabledDate: time => this.setDisabledDate(time, true),
+      },
+    };
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+  created() {
+    if (this.data) this.$set(this, 'form', this.data)
+  },
+  methods: {
+    //保存表单函数
+    saveForm() {
+      this.$refs['form'].validate(valid => {
+        if (valid) {
+          let data = JSON.parse(JSON.stringify(this.form));
+          this.resetForm();
+          this.$emit('planUpdate', { data });
+        } else {
+          console.warn('form validate error!!!');
+        }
+      });
+    },
+    //重置表单函数
+    resetForm() {
+      this.$refs.form.resetFields();
+      this.form.class = [];
+    },
+    addClass() {
+      this.form.class.push({});
+    },
+    //删除班级
+    toDeleteClass(index) {
+      let duplicate = _.cloneDeep(this.form.class);
+      //删除指定行
+      duplicate.splice(index, 1);
+      this.$set(this.form, `class`, duplicate);
+    },
+    // 设置结束时间默认时间
+    getEndTimeDefault() {
+      let result = new Date();
+      const startTime = _.get(this.form, 'startdate')
+      if (!startTime) result = moment().toDate()
+      result = moment(startTime).add(1, 'd').toDate()
+      return result
+    },
+    //禁用时间
+    setDisabledDate(time, isStart) {
+      let thisTime = time.getTime();
+      if (!isStart) {
+        let startTime = _.get(this.form, 'startdate')
+        if (startTime) {
+          // 开始时间之前的全都不能选
+          let sb = moment(thisTime).isSameOrBefore(startTime)
+          if (sb) return true;
+        }
+      }
+
+      // 限制在今年范围内
+      let start = new Date(`${this.year}-01-01`).getTime();
+      let end = new Date(`${this.year}-12-31`).getTime();
+      if (thisTime < start) return true;
+      else if (thisTime > end) return true;
+      else {
+        // 假期不能选
+        const res = this.vacation.find(f => moment(thisTime).isBetween(f.start, f.end, null, '[]'))
+        return res;
+      }
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>