excel-view.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844
  1. <template>
  2. <div id="excel-view">
  3. <el-row>
  4. <el-col :span="4">
  5. <el-button size="mini" @click="getExcelData">获取excel数据</el-button>
  6. </el-col>
  7. <el-col :span="4">
  8. <el-button @click="toAddCol" size="mini" type="primary">新增日期安排</el-button>
  9. </el-col>
  10. </el-row>
  11. <div class="sheetContainerbox" ref="sheetContainer" id="x-spreadsheet-demo"></div>
  12. <el-dialog :visible.sync="dialog" title="计划变更" @close="toClose" :destroy-on-close="true">
  13. <term-add :key="new Date().getTime()" :data="form" :classTypeList="classTypeList" :placeList="placeList" v-bind="$attrs" v-on="$listeners"></term-add>
  14. </el-dialog>
  15. <el-dialog :visible.sync="dialog2" title="培训安排" @close="toClose" :destroy-on-close="true">
  16. <arrange-edit :key="new Date().getTime()" :data="form" :planData="planData" :schoolData="schoolData"></arrange-edit>
  17. </el-dialog>
  18. </div>
  19. </template>
  20. <script>
  21. const _ = require('lodash');
  22. const moment = require('moment');
  23. import termAdd from './term-add.vue';
  24. import arrangeEdit from './arrange-edit.vue';
  25. import XLSX from 'xlsx';
  26. //引入依赖包
  27. import zhCN from 'x-data-spreadsheet/src/locale/zh-cn';
  28. import Spreadsheet from 'x-data-spreadsheet';
  29. //设置中文
  30. Spreadsheet.locale('zh-cn', zhCN);
  31. import { mapState, createNamespacedHelpers } from 'vuex';
  32. export default {
  33. name: 'excel-view',
  34. props: {
  35. year: { type: String }, //当前计划年份
  36. plan: { type: Object }, //计划
  37. classTypeList: { type: Array }, // 班级类型列表
  38. schStuList: { type: Array }, // 学校上传学生的结果 [{schid:学校编码, sum:1}]
  39. schoolList: { type: Array }, //需要设置的学校列表
  40. placeList: { type: Array }, //场地列表
  41. schPlan: { type: Array }, // 培训计划安排
  42. },
  43. components: { termAdd, arrangeEdit },
  44. data: function() {
  45. return {
  46. dialog: false, // 计划dialog
  47. dialog2: false, // 培训计划dialog
  48. form: {},
  49. planData: {}, // 为修改培训计划整理的学校数据
  50. schoolData: {}, // 为修改培训计划整理的期批数据
  51. /**
  52. * merge:合并单元格,实际上是 [sri,sci,eri,eci]
  53. * sri:起始行数,在rows中的合并,已经确定了 起始行这个参数
  54. * sci:起始列数,在rows中的合并,r.cells.${n} n即为起始列数,也可以确定
  55. * eri:结束行数,需要自己确定
  56. * eci:结束列数,需要自己确定
  57. * 由此可知: 在rows中合并单元格, 我们只需要填写 eri和eci这两个参数.
  58. * 即由当前单元格出发,计算要合并几行几列,[行,列]
  59. */
  60. sheetDefaultConfig: {
  61. // name: '普通班', // 表单名(sheet1)
  62. // 样式
  63. styles: {
  64. valign: 'middle',
  65. align: 'center',
  66. },
  67. //行数据
  68. rows: {
  69. 1: {
  70. cells: {
  71. 0: {
  72. text: '序号',
  73. merge: [3, 0],
  74. },
  75. 1: {
  76. text: '学校名称',
  77. merge: [3, 0],
  78. },
  79. 2: {
  80. text: '计划数',
  81. merge: [3, 0],
  82. },
  83. 3: {
  84. text: '地区',
  85. merge: [3, 0],
  86. },
  87. 4: {
  88. text: '期数',
  89. },
  90. },
  91. },
  92. 2: {
  93. cells: {
  94. 4: {
  95. text: '场地',
  96. },
  97. },
  98. },
  99. 3: {
  100. cells: {
  101. 4: {
  102. text: '班级数',
  103. },
  104. },
  105. },
  106. 4: {
  107. cells: {
  108. 4: {
  109. text: '时间',
  110. },
  111. },
  112. },
  113. 5: {
  114. cells: {
  115. 2: {
  116. text: '批次人数',
  117. merge: [0, 2],
  118. },
  119. },
  120. },
  121. 6: {
  122. cells: {
  123. 2: { text: '督导', merge: [1, 1] },
  124. 4: { text: '天数' },
  125. },
  126. },
  127. 7: {
  128. cells: {
  129. 4: { text: '人数' },
  130. },
  131. },
  132. 8: {
  133. cells: {
  134. 2: {
  135. text: '长春高校用车数',
  136. merge: [0, 2],
  137. },
  138. },
  139. },
  140. },
  141. },
  142. // 表格事件设置 如果在 x,y其中一个在无操作中, 则本次事件不操作
  143. /**无操作行: 点击事件,行在这里面,则直接当无事发生 */
  144. ignoreRows: [0],
  145. /**无操作列: 点击事件,列在这里面,则直接当无视发生 */
  146. /**计划行: 点击事件,行在这里面,且 列不在无操作列中, 则进入确定具体修改那期,并进入修改*/
  147. ignoreCols: [0, 1, 2, 3, 4],
  148. planRows: [1, 2, 3, 4],
  149. /**学校计划安排起始行 */
  150. arrangeStartRow: 8,
  151. };
  152. },
  153. computed: {
  154. ...mapState(['user']),
  155. },
  156. mounted() {
  157. this.init();
  158. },
  159. methods: {
  160. init() {
  161. this.xs = new Spreadsheet('#x-spreadsheet-demo', {
  162. mode: 'read',
  163. view: {
  164. height: () => this.$refs.sheetContainer?.offsetHeight,
  165. width: () => this.$refs.sheetContainer?.offsetWidth,
  166. },
  167. col: { len: 45 },
  168. style: { align: 'center' },
  169. showToolbar: false,
  170. showGrid: true,
  171. showContextmenu: false,
  172. }).loadData([]);
  173. this.xs
  174. .on('cell-selected', (cell, ri, ci) => {
  175. console.log('cell:', cell, ', ri:', ri, ', ci:', ci);
  176. this.cellClick(ri, ci, cell);
  177. })
  178. .on('cell-edited', (text, ri, ci) => {
  179. console.log('text:', text, ', ri: ', ri, ', ci:', ci);
  180. });
  181. // 整理表头数据
  182. const sheetDatas = this.organizeLine3456();
  183. // 检查场地,督导的合并,计算督导的人数
  184. this.organizeLine378(sheetDatas);
  185. // 输出学校列表
  186. this.organizeLineFrom9(sheetDatas);
  187. this.xs.loadData(sheetDatas);
  188. // 学校安排人数设置
  189. this.setSchPlan();
  190. // TODO:计算高校用车数
  191. },
  192. // #region 单元格事件
  193. /**
  194. * 单元格点击事件
  195. * @param {Number} ri 点击的行位置
  196. * @param {Number} ci 点击的列位置
  197. * @param {Object} cell 单元格
  198. */
  199. cellClick(ri, ci, cell) {
  200. const inIngnoreRow = this.ignoreRows.includes(ri);
  201. // 在不操作行中,直接返回
  202. if (inIngnoreRow) return;
  203. const inIngnoreCol = this.ignoreCols.includes(ci);
  204. // 在不操作列中,直接返回
  205. if (inIngnoreCol) return;
  206. // 查看是否在计划行中
  207. const inPlanRows = this.planRows.includes(ri);
  208. if (inPlanRows) {
  209. // 确定点击位置是计划的哪个位置,然后进行计划修改
  210. console.log('in plan');
  211. // 使用列,确定是哪一批次即可
  212. const batch = this.toConfirmPlan(ri, ci);
  213. if (batch) {
  214. // 没有延迟会又选择好几个
  215. _.delay(() => {
  216. this.form = batch;
  217. this.dialog = true;
  218. }, 100);
  219. }
  220. } else if (ri > this.arrangeStartRow) {
  221. // 学校的安排,打开学校安排界面
  222. console.log('in arrange');
  223. // 确定是哪个学校,哪个批次
  224. let number = _.get(cell, 'text', 0);
  225. if (number) number = parseInt(number);
  226. const res = this.toConfirmArrange(ri, ci);
  227. if (!res) {
  228. this.$message.error('培训计划安排整理数据发生错误');
  229. return;
  230. }
  231. const { schoolData, planData } = res;
  232. console.log(planData)
  233. _.delay(() => {
  234. this.form = { number };
  235. this.schoolData = schoolData;
  236. this.planData = planData;
  237. this.dialog2 = true;
  238. }, 100);
  239. } else {
  240. console.log('无操作');
  241. }
  242. },
  243. /**
  244. * 根据坐标确定培训安排取出数据
  245. * 收集学校信息:学校名(name);层次(level);需不需要派车(hascar);总名额;剩余名额
  246. * 期批信息;期数;批次;班级类型;开始时间;结束时间;批次总人数;批次剩余名额
  247. * 然后提供给修改组件取设置数值
  248. * @param {Number} ri 行位置
  249. * @param {Number} ci 列位置
  250. */
  251. toConfirmArrange(ri, ci) {
  252. // ri确定学校, ci确定期-批次
  253. const sheetName = this.getSheetName();
  254. const sheetDatas = this.xs.getData();
  255. const sheetData = sheetDatas.find(f => f.name === sheetName);
  256. if (!sheetName) return;
  257. const ct = this.classTypeList.find(f => f.name === sheetName);
  258. // 班级类型
  259. const classType = ct.name;
  260. // 先找行: 学校名 => 学校, 获取分配限制
  261. const schoolName = _.get(sheetData, `rows.${ri}.cells.1.text`);
  262. const school = this.schoolList.find(f => f.name === schoolName);
  263. if (!school) return;
  264. /**
  265. * 丰富:
  266. * 学校总名额: 根据当前班级类型,取出分配的总量即可
  267. * 剩余名额: 计算当前行所有安排的名额作为减数
  268. */
  269. const classnum = _.get(school, 'classnum', []);
  270. const thisTypeClass = classnum.find(f => f.code === ct.code);
  271. if (!thisTypeClass) return;
  272. // 总名额
  273. school.total = _.get(thisTypeClass, 'number');
  274. // 剩余名额
  275. const thisRow = _.get(sheetData, `rows.${ri}.cells`);
  276. if (!thisRow) return;
  277. const arrColsDataObject = _.omit(thisRow, this.ignoreCols);
  278. const arrColsDatas = [];
  279. for (const key in arrColsDataObject) {
  280. const e = arrColsDataObject[key];
  281. let number = _.get(e, 'text', 0);
  282. if (number) number = parseInt(number);
  283. arrColsDatas.push(number);
  284. }
  285. const et = arrColsDatas.reduce((p, n) => p + n, 0);
  286. school.elseNumber = school.total - et;
  287. /**
  288. * 确定批次,因为之后修改了向3,4,5行都注入了 term和batch,方便快速定位期批.
  289. * 所以固定取时间行(因为时间行完全不涉及合并问题,就是跟着批次走)
  290. */
  291. const row5SameColCell = this.xs.cell(4, ci, 0);
  292. const { term, batch } = row5SameColCell;
  293. const termnum = this.plan.termnum.find(f => f.term === term);
  294. if (!termnum) return;
  295. const batchnum = _.get(termnum, 'batchnum');
  296. if (!batchnum) return;
  297. const batchData = batchnum.find(f => f.batch === batch);
  298. const planData = {
  299. term,
  300. termid: _.get(termnum, '_id'),
  301. batch,
  302. batchid: _.get(batchData, '_id'),
  303. startdate: _.get(batchData, 'startdate'),
  304. enddate: _.get(batchData, 'enddate'),
  305. type: classType,
  306. };
  307. /**
  308. * 丰富:
  309. * 总名额: 通过行数据可以直接获取
  310. * 剩余名额: 需要算列总和
  311. */
  312. /**总名额 */
  313. const total = _.get(sheetData, `rows.5.cells.${ci}.text`);
  314. planData.total = total;
  315. /**计算列和 */
  316. let keys = Object.keys(sheetData.rows).map(i => parseInt(i));
  317. keys = keys.filter(f => f > this.arrangeStartRow);
  318. const schoolRowObject = _.pick(sheetData.rows, keys);
  319. const arr = [];
  320. for (const key in schoolRowObject) {
  321. const e = schoolRowObject[key];
  322. const obj = _.get(e, 'cells', {});
  323. if (!obj || Object.keys(obj).length < 0) continue;
  324. const tc = _.get(obj, `${ci}.text`, 0);
  325. arr.push(parseInt(tc));
  326. }
  327. const inNumber = arr.reduce((p, n) => p + n, 0);
  328. const elseNumber = total - inNumber;
  329. planData.elseNumber = elseNumber;
  330. return { schoolData: school, planData };
  331. },
  332. /**
  333. * 根据坐标确定计划期并取出数据
  334. * @param {Number} ri 行位置
  335. * @param {Number} ci 列位置
  336. */
  337. toConfirmPlan(ri, ci) {
  338. const excelDatas = this.xs.getData();
  339. const sheetName = this.getSheetName();
  340. const allData = excelDatas.find(f => f.name === sheetName);
  341. if (!allData) return;
  342. /**返回的结果变量 */
  343. let result;
  344. let batchIndex = 0;
  345. let term;
  346. // 不需要看行,只看列在哪,因为已经冻结了所有表格,无法修改.固定走第二行拿到期数,然后去计划中换数据进行修改
  347. const rowDataObj = allData.rows[1].cells;
  348. const keys = Object.keys(rowDataObj).map(i => parseInt(i));
  349. /**定位列分几种情况:
  350. * 1.该列没有合并单元格,那就能在key中直接找到
  351. * 2.该列有合并单元格,那就不一定能在里面直接找到:只有合并的起始单元格才能被直接找到,需要查看 点击的单元格 是否在 被合并的单元格中.
  352. */
  353. const hasKey = keys.includes(ci);
  354. if (hasKey) {
  355. // 直接找到的一定都是第一个单元格
  356. const cellData = rowDataObj[ci];
  357. term = _.get(cellData, 'text');
  358. } else {
  359. // 将所有列展开,检查是否在被合并的单元格中
  360. for (const key in rowDataObj) {
  361. // 无操作列不要
  362. if (this.ignoreCols.includes(parseInt(key))) continue;
  363. const cols = [parseInt(key)];
  364. const { merge = [], text } = rowDataObj[key];
  365. // 没有两行直接返回
  366. if (merge.length === 2) {
  367. const last = _.last(merge);
  368. if (last) {
  369. for (let i = 1; i <= last; i++) {
  370. cols.push(parseInt(i) + parseInt(key));
  371. }
  372. }
  373. }
  374. // 看下点击的列是否在这里面
  375. const res = cols.find(f => f === ci);
  376. if (res) {
  377. batchIndex = cols.findIndex(f => f === ci);
  378. term = text;
  379. break;
  380. }
  381. }
  382. }
  383. if (term && batchIndex >= 0) {
  384. const termnum = this.plan.termnum.find(f => f.term === term);
  385. if (termnum) {
  386. const batch = _.get(termnum, `batchnum.${batchIndex}`, {});
  387. result = { ...batch, term: _.get(termnum, 'term') };
  388. }
  389. }
  390. return result;
  391. },
  392. /**获取当前浏览的表格名 */
  393. getSheetName() {
  394. const sheetName = _.get(this.xs, 'sheet.data.name');
  395. return sheetName;
  396. },
  397. // #endregion
  398. // #region excel初始化数据整理
  399. /**
  400. * 设置excel中学校安排的数据
  401. * 根据arrange的type:班级类型,确定是普通班还是特殊班
  402. * 然后计算出该数据所在的横纵坐标,使用 this.xs.cellText(ri,ci,text,sheetIndex)
  403. */
  404. setSchPlan() {
  405. const list = this.schPlan.filter(f => f.arrange.length > 0);
  406. const datas = this.xs.getData();
  407. for (let si = 0; si < datas.length; si++) {
  408. const sheetData = datas[si];
  409. const name = _.get(sheetData, 'name');
  410. const ct = this.classTypeList.find(f => f.name === name);
  411. // 这里不能找到不到,如果找不到,那就中断别找了
  412. if (!ct) continue;
  413. const termRowObject = sheetData.rows[1].cells;
  414. const arrangeCols = _.omit(termRowObject, this.ignoreCols);
  415. const termRowKeys = Object.keys(arrangeCols);
  416. const termRowValues = Object.values(arrangeCols);
  417. for (const i of list) {
  418. const { arrange, schid } = i;
  419. const schIndex = this.schoolList.findIndex(f => f.code === schid);
  420. // 小于零就是没有该学校,那就不要处理
  421. if (schIndex < 0) continue;
  422. // 查看有没有当前要处理的班级类型的安排
  423. const thisTypeClassArrange = arrange.filter(f => f.type === ct.code);
  424. if (thisTypeClassArrange.length <= 0) continue;
  425. // 计算行位置
  426. const ri = this.arrangeStartRow + schIndex + 1;
  427. // 计算列位置: 先确定是普通班还是特殊班,然后
  428. for (const a of arrange) {
  429. const { term, batchid, number } = a;
  430. let ci;
  431. // term 确定列范围, batchid确定索引, 两个值之和就是列的位置
  432. const valIndex = termRowValues.findIndex(f => f.text === term);
  433. if (valIndex < 0) continue;
  434. const termStartPos = termRowKeys[valIndex];
  435. // 再确定批次索引
  436. const termnum = this.plan.termnum.find(f => f.term === term);
  437. if (!termnum) continue;
  438. const batchnum = _.get(termnum, 'batchnum');
  439. if (!batchnum) continue;
  440. const batchIndex = batchnum.findIndex(f => f._id === batchid);
  441. if (batchIndex >= 0) ci = batchIndex + parseInt(termStartPos);
  442. if (ci) {
  443. this.xs.cellText(ri, ci, number, si);
  444. }
  445. }
  446. }
  447. }
  448. this.xs.reRender();
  449. },
  450. /**
  451. * 整理学校数据
  452. * 从3456或378后开始都行,反正不影响计划
  453. * @param {Object} sheetData organizeLine3456返回的数据
  454. */
  455. organizeLineFrom9(sheetDatas) {
  456. for (const sheetData of sheetDatas) {
  457. const name = _.get(sheetData, 'name');
  458. const ct = this.classTypeList.find(f => f.name === name);
  459. // 这里不能找到不到,如果找不到,那就中断别找了
  460. if (!ct) continue;
  461. let rowKey = this.arrangeStartRow + 1;
  462. let dataIndex = 0;
  463. for (let i = 0; i < this.schoolList.length; i++) {
  464. const sch = this.schoolList[i];
  465. const clas = _.get(sch, 'classnum', []);
  466. const typeCla = clas.find(f => f.code === ct.code);
  467. if (!typeCla) continue;
  468. const num = _.get(typeCla, 'number', 0);
  469. if (num <= 0) continue;
  470. dataIndex = dataIndex + 1;
  471. const obj = {
  472. 0: { text: dataIndex },
  473. 1: { text: _.get(sch, 'name', '') },
  474. 2: { text: num },
  475. 3: { text: _.get(sch, 'address', '') },
  476. };
  477. sheetData.rows[rowKey] = { cells: obj };
  478. rowKey = rowKey + 1;
  479. }
  480. }
  481. },
  482. /**
  483. * 处理场地的合并及督导天数,人数行合并及内容
  484. * 场地合并: 相邻且相同场地合并;
  485. * 督导天数合并: 与场地合并一致;
  486. * 督导天数内容: 多个批次的 最晚结束时间 - 最早开始时间;
  487. * 督导人数合并: 和期数合并一致;
  488. * 督导人数内容: 几个场地几个人;
  489. * @param {Array<Object>} sheetDatas organizeLine3456返回的数据
  490. */
  491. organizeLine378(sheetDatas) {
  492. // 场地合并
  493. // 取出期数行
  494. for (const sheetData of sheetDatas) {
  495. const name = _.get(sheetData, 'name');
  496. const ct = this.classTypeList.find(f => f.name === name);
  497. // 这里不能找到不到,如果找不到,那就中断别找了
  498. if (!ct) continue;
  499. const termRowObject = sheetData.rows[1].cells;
  500. const termColsMappings = [];
  501. // 整理出期范围
  502. for (const key in termRowObject) {
  503. // 无操作列直接跳过
  504. if (this.ignoreCols.includes(parseInt(key))) continue;
  505. const termObject = termRowObject[key];
  506. const cols = [parseInt(key)];
  507. const { merge = [], text } = termObject;
  508. // 没有两行直接返回
  509. if (merge.length === 2) {
  510. const last = _.last(merge);
  511. if (last) {
  512. for (let i = 1; i <= last; i++) {
  513. cols.push(parseInt(i) + parseInt(key));
  514. }
  515. }
  516. }
  517. termColsMappings.push({ term: text, cols, merge, col: parseInt(key) });
  518. }
  519. // 先把督导人数行处理了
  520. const ddpersonNumRowObject = {};
  521. for (const i of termColsMappings) {
  522. const { term, merge = [], col } = i;
  523. const obj = { text: 0, merge };
  524. const r = this.plan.termnum.find(f => f.term === term);
  525. if (r) {
  526. const bm = _.get(r, 'batchnum', []);
  527. let batchnum = this.batchFilterByClassType(bm, ct.code);
  528. if (batchnum.length <= 0) continue;
  529. const placeList = _.uniq(_.compact(batchnum.map(i => _.get(i, 'place')).filter(f => f !== '')));
  530. obj.text = placeList.length;
  531. }
  532. ddpersonNumRowObject[col] = obj;
  533. }
  534. const old7Cells = _.get(sheetData, `rows.7.cells`, {});
  535. sheetData.rows[7] = { cells: { ...old7Cells, ...ddpersonNumRowObject } };
  536. const placeRowObject = sheetData.rows[2].cells;
  537. // 处理督导天数,再查相邻场地是否相同
  538. for (const tcm of termColsMappings) {
  539. const { cols } = tcm;
  540. const inSameTermDatas = _.pick(placeRowObject, cols);
  541. const keys = Object.keys(inSameTermDatas);
  542. for (const key in inSameTermDatas) {
  543. const cell = inSameTermDatas[key];
  544. const isMerged = _.get(cell, 'isMerged', false);
  545. if (isMerged) continue;
  546. const text = _.get(cell, 'text');
  547. const keyIndex = keys.findIndex(f => f === key);
  548. let nextKeyIndex = keyIndex + 1;
  549. // 需要确定当前场地和下一个场地是否一致.所以需要确定当前列是否是最后一列,如果是最后一列,就不需要处理了,因为没有后面.
  550. while (nextKeyIndex < keys.length) {
  551. const nextKey = keys[nextKeyIndex];
  552. const nextCell = inSameTermDatas[nextKey];
  553. const nextCellText = _.get(nextCell, 'text');
  554. if (text === nextCellText) {
  555. // 一致,主单元格写入合并参数
  556. const merge = _.get(inSameTermDatas[key], 'merge', [0, 0]);
  557. const newLast = _.last(merge) + 1;
  558. merge[merge.length - 1] = newLast;
  559. inSameTermDatas[key].merge = merge;
  560. // 当前单元格写入isMerged:true
  561. inSameTermDatas[nextKey].isMerged = true;
  562. nextKeyIndex = nextKeyIndex + 1;
  563. continue;
  564. }
  565. // 下一个单元格的场地与当前单元格场地不一致,不处理,直接跳过
  566. break;
  567. }
  568. }
  569. }
  570. // 删掉被合并的单元格
  571. for (const key in placeRowObject) {
  572. const value = placeRowObject[key];
  573. if (_.get(value, 'isMerged', false)) delete placeRowObject[key];
  574. }
  575. // 督导天数的合并同 场地行一致,内容由 时间行计算而来
  576. // 时间行数据
  577. const timeRowObject = sheetData.rows[4].cells;
  578. // 督导天数行
  579. let ddDaysRowObject = sheetData.rows[6].cells;
  580. const timeColsMappings = [];
  581. // 根据场地范围,再计算天数,整理出督导天数行
  582. for (const key in placeRowObject) {
  583. // 无操作列直接跳过
  584. if (this.ignoreCols.includes(parseInt(key))) continue;
  585. const object = placeRowObject[key];
  586. const cols = [parseInt(key)];
  587. const { merge = [] } = object;
  588. // 没有两行直接返回
  589. if (merge.length === 2) {
  590. const last = _.last(merge);
  591. if (last) {
  592. for (let i = 1; i <= last; i++) {
  593. cols.push(parseInt(i) + parseInt(key));
  594. }
  595. }
  596. }
  597. timeColsMappings.push({ cols, merge, col: parseInt(key) });
  598. const timeDataInPlaceRange = _.pick(timeRowObject, cols);
  599. let days = [];
  600. for (const key in timeDataInPlaceRange) {
  601. const cell = timeDataInPlaceRange[key];
  602. const text = _.get(cell, 'text');
  603. if (text === '') continue;
  604. let arr = text.split('-');
  605. if (arr.length <= 0) continue;
  606. arr = arr.map(i => this.excelDateToStringDate(i));
  607. days.push(...arr);
  608. }
  609. // 升序排序
  610. days = days.sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
  611. const start = _.head(days);
  612. const last = _.last(days);
  613. const diff = moment(last).diff(start, 'd');
  614. const m = _.get(object, 'merge');
  615. const cell = { text: diff };
  616. if (m) cell.merge = m;
  617. ddDaysRowObject[key] = cell;
  618. }
  619. }
  620. },
  621. /**组织表头,3&4&5&6行的数据:场地,班级,时间,批次人数
  622. */
  623. organizeLine3456() {
  624. /**
  625. * 这几行主要都是由批次信息得来的
  626. * 场地(3),班级数(4),时间(5),批次人数(6)都是可以在批次中获取的.
  627. * 期数可以直接获取,但是合并单元格的长度是根据批次来的
  628. */
  629. const sheetDataTemplate = _.cloneDeep(this.sheetDefaultConfig);
  630. const result = [];
  631. // 根据班级类型开始生成对应excel的数据
  632. for (const ct of this.classTypeList) {
  633. const sheetData = _.cloneDeep(sheetDataTemplate);
  634. // 先给名字
  635. sheetData.name = ct.name;
  636. // 获取本excel的预设数据,之后的操作都在这上面搞
  637. let rows = sheetData.rows;
  638. // 对期进行循环
  639. const termnum = _.get(this.plan, 'termnum', []);
  640. for (const t of termnum) {
  641. const { batchnum: bm = [], term } = t;
  642. /**
  643. * 因为根据班级来分sheet,所以从最开始就把班级都分开
  644. * 根据batchnum下的classnum来决定,如果有1个班级符合当前的班级类型.
  645. * 那就留下这个班,这个批次,这个期
  646. * 如果1个符合条件的班级都没有,那就直接爆了这期
  647. */
  648. let batchnum = this.batchFilterByClassType(bm, ct.code);
  649. if (batchnum.length <= 0) continue;
  650. const { pos, merge } = this.termLineDeal(rows, term, batchnum);
  651. for (const b of batchnum) {
  652. const { place, class: cla = [], startdate, enddate } = b;
  653. // 场地行处理
  654. const p = this.placeList.find(f => f._id === place);
  655. let pstr = place;
  656. if (p) pstr = _.get(p, 'name');
  657. const params = { term, batch: b.batch };
  658. this.batchLineDeal(rows, 2, pstr, params);
  659. // 班级数处理
  660. const clanum = cla.length;
  661. this.batchLineDeal(rows, 3, clanum, params);
  662. // 时间处理
  663. const sStr = moment(startdate).format('M.D');
  664. const eStr = moment(enddate).format('M.D');
  665. const timeStr = `${sStr}-${eStr}`;
  666. this.batchLineDeal(rows, 4, timeStr, params);
  667. // 批次人数
  668. const batchPersonNumber = cla.reduce((p, n) => parseInt(p) + parseInt(_.get(n, 'number', 0)), 0);
  669. this.batchLineDeal(rows, 5, batchPersonNumber, params);
  670. }
  671. }
  672. result.unshift(sheetData);
  673. }
  674. return result;
  675. },
  676. /**
  677. * 针对批次的统一处理方式
  678. * @param {Object} rows excel的所有行数据
  679. * @param {String} linePos 行位置
  680. * @param {String} text 显示内容
  681. * @param {Object} params 额外参数{term,batch}
  682. */
  683. batchLineDeal(rows, linePos, text, params = {}) {
  684. // 获取行数据
  685. const row = _.get(rows, `${linePos}.cells`, {});
  686. // 计算单元格的起始位置
  687. const pos = this.computedPosition(row);
  688. const cell = { text, ...params };
  689. // 不需要计算合并
  690. row[pos] = cell;
  691. },
  692. /**
  693. * 设置期数行
  694. * @param {Object} rows excel的所有行数据
  695. * @param {String} term 期数
  696. * @param {Array} batchnum 批次
  697. */
  698. termLineDeal(rows, term, batchnum) {
  699. // 获取行数据
  700. const row = _.get(rows, `1.cells`, {});
  701. // 计算单元格的起始位置
  702. const pos = this.computedPosition(row);
  703. // 设置单元格的具体内容(显示内容和单元格合并的设置)
  704. const cell = { text: term };
  705. const blen = batchnum.length;
  706. if (blen > 0) cell.merge = [0, blen - 1];
  707. row[pos] = cell;
  708. return { pos, merge: cell.merge };
  709. },
  710. /**
  711. * 计算该单元格所在列的位置.
  712. * @param {Object} row 当前行
  713. */
  714. computedPosition(row) {
  715. // 获取最后单元格的位置
  716. const keys = Object.keys(row);
  717. const lastKey = _.last(keys);
  718. const last = row[lastKey];
  719. // 设置单元格的位置
  720. let pos = parseInt(lastKey) + 1;
  721. if (!_.isObject(last)) {
  722. console.error(`${pos}_${lastKey}解析错误-单元格设置不是object类型`);
  723. return;
  724. }
  725. const lastCellMerge = _.get(last, 'merge', []);
  726. if (lastCellMerge.length > 0) {
  727. // 说明有合并操作,需要将被合并的单元格留下来后,再继续添加
  728. // [r,c]: r是行数合并; c:列合并主要看c,需要往后多窜c个位置, pos + c
  729. const ec = _.last(lastCellMerge);
  730. pos = parseInt(pos) + parseInt(ec);
  731. }
  732. return pos;
  733. },
  734. /**
  735. * 根据班级类型过滤批次
  736. * @param {Array} batch 批次
  737. * @param {String} code 班级类型编码
  738. */
  739. batchFilterByClassType(batch, code) {
  740. let batchnum = [];
  741. for (const bnum of batch) {
  742. const { class: clas = [], ...others } = bnum;
  743. const r = clas.find(f => f.type === code);
  744. if (!r) continue;
  745. const thisTypeClass = clas.filter(f => f.type === code);
  746. const obj = { ...others, class: thisTypeClass };
  747. batchnum.push(obj);
  748. }
  749. return batchnum;
  750. },
  751. // #endregion
  752. /**关闭对话框 */
  753. toClose() {
  754. this.form = {};
  755. this.planData = {};
  756. this.schoolData = {};
  757. },
  758. /**打开新增日期安排的对话框 */
  759. toAddCol() {
  760. this.dialog = true;
  761. },
  762. /**
  763. * 拼接成正常时间: YYYY-MM-DD
  764. * @param {String} pointDate 带点的缩短时间 7.1
  765. */
  766. excelDateToStringDate(pointDate) {
  767. return moment(`${this.year}.${pointDate}`).format('YYYY-MM-DD');
  768. },
  769. /**获取excel数据 */
  770. getExcelData() {
  771. const data = this.xs.getData();
  772. console.log(data);
  773. const copy = this.xs.copy();
  774. console.log(copy);
  775. /**
  776. * 由期行 的位置与合并数量,可以确定一期有几批次,批次的场地,班级,
  777. * 批次还原的原则: 场地,时间; 如果场地和时间都一致还存在2条以上的数据,那就说明分配有问题.在一个场地一个时间只能有个1个批次
  778. * 所以根据场地和时间判断数据是否新添加的(其实判断是不是新数据并没什么用,要做的是把数据整理出来)
  779. * 根据时间和场地,与plan中的数据对比:存在-修改数据;不存在:创建数据
  780. */
  781. },
  782. /** 导出excel */
  783. exportExcel() {
  784. var new_wb = this.xtos(this.xs.getData());
  785. /* generate download */
  786. XLSX.writeFile(new_wb, `培训计划${new Date().getTime()}.xlsx`);
  787. },
  788. xtos(sdata) {
  789. console.log(sdata);
  790. var out = XLSX.utils.book_new();
  791. sdata.forEach(function(xws) {
  792. var aoa = [[]];
  793. var rowobj = xws.rows;
  794. for (var ri = 0; ri < rowobj.len; ++ri) {
  795. var row = rowobj[ri];
  796. if (!row) continue;
  797. aoa[ri] = [];
  798. Object.keys(row.cells).forEach(function(k) {
  799. var idx = +k;
  800. if (isNaN(idx)) return;
  801. aoa[ri][idx] = row.cells[k].text;
  802. });
  803. }
  804. var ws = XLSX.utils.aoa_to_sheet(aoa);
  805. /** 读取在线中的合并单元格,并写入导出的数据中
  806. * merges: Array(19)
  807. 0: "A16:P16"
  808. 1: "A17:P17"
  809. 2: "O2:P2"
  810. 3: "F2:G2"
  811. */
  812. ws['!merges'] = [];
  813. xws.merges.forEach(merge => {
  814. ws['!merges'].push(XLSX.utils.decode_range(merge));
  815. });
  816. XLSX.utils.book_append_sheet(out, ws, xws.name);
  817. });
  818. return out;
  819. },
  820. },
  821. metaInfo() {
  822. return { title: this.$route.meta.title };
  823. },
  824. };
  825. </script>
  826. <style lang="less" scoped>
  827. .sheetContainerbox {
  828. width: 100%;
  829. height: 80vh;
  830. }
  831. </style>