|
@@ -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>
|