cost_tmpl.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. $(document).ready(() => {
  2. autoFlashHeight();
  3. class MultiHeaderObj {
  4. constructor () {
  5. const self = this;
  6. this.firstShowDone = false;
  7. this.spread = SpreadJsObj.createNewSpread($('#multi-spread')[0]);
  8. this.sheet = this.spread.getActiveSheet();
  9. const vStyle = new spreadNS.Style();
  10. vStyle.font = '12px 微软雅黑';
  11. vStyle.hAlign = 1;
  12. vStyle.vAlign = 1;
  13. vStyle.locked = false;
  14. this.sheet.setDefaultStyle(vStyle);
  15. this.spread.bind(spreadNS.Events.EditStarting, function(e, info) {
  16. if (info.row === info.sheet.getRowCount() - 1) {
  17. info.cancel = true;
  18. }
  19. });
  20. this.spread.bind(spreadNS.Events.ColumnWidthChanging, function(e, info) {
  21. info.cancel = true;
  22. });
  23. this.spread.bind(spreadNS.Events.RowHeightChanged, function(e, info) {
  24. self.multiHeader.headRowHeight[info.row] = info.sheet.getRowHeight(info.row);
  25. });
  26. $('#multi-header').on('shown.bs.modal', function() {
  27. if (!self.firstShowDone) {
  28. self.spread.refresh();
  29. self.firstShowDone = true;
  30. }
  31. });
  32. $('#header-row-count').change(function() {
  33. let count = parseInt($('#header-row-count').val());
  34. if (count > 4 || count < 1) {
  35. toastr.warning('仅支持1-4层表头');
  36. return;
  37. }
  38. self.setHeaderRows(count);
  39. });
  40. $.contextMenu({
  41. selector: '#multi-spread',
  42. build: function ($trigger, e) {
  43. const target = SpreadJsObj.safeRightClickSelection($trigger, e, self.spread);
  44. return (target.hitTestType === GC.Spread.Sheets.SheetArea.viewport || target.hitTestType === GC.Spread.Sheets.SheetArea.rowHeader);
  45. },
  46. items: {
  47. 'merge': {
  48. name: '合并单元格',
  49. callback: function (key, opt) {
  50. const sel = self.sheet.getSelections()[0];
  51. self.sheet.addSpan(sel.row, sel.col, sel.rowCount, sel.colCount);
  52. },
  53. disabled: function(key, opt) {
  54. const sel = self.sheet.getSelections()[0];
  55. return sel.row + sel.rowCount >= self.sheet.getRowCount();
  56. }
  57. },
  58. 'mergeCancel': {
  59. name: '取消合并',
  60. callback: function (key, opt) {
  61. const sel = self.sheet.getSelections()[0];
  62. console.log(sel);
  63. self.sheet.removeSpan(sel.row, sel.col);
  64. },
  65. disabled: function(key, opt) {
  66. const sel = self.sheet.getSelections()[0];
  67. return sel.row + sel.rowCount === self.sheet.getRowCount();
  68. }
  69. },
  70. }
  71. });
  72. $('#multi-header-ok').click(function() {
  73. if (self.afterSet) self.afterSet(self.getMultiHeader());
  74. $('#multi-header').modal('hide');
  75. });
  76. }
  77. setHeaderRows(count) {
  78. if (count === this.multiHeader.headRows) return;
  79. this.multiHeader = { headRows: count, headRowHeight: new Array(count).fill(32) };
  80. this.reloadRowHeaderData();
  81. }
  82. reloadRowHeaderData() {
  83. this.sheet.setRowCount(0);
  84. this.sheet.setRowCount(this.multiHeader.headRows);
  85. this.sheet.setColumnCount(this.colSet.length);
  86. for (const [i, height] of this.multiHeader.headRowHeight.entries()) {
  87. this.sheet.setRowHeight(i, height);
  88. }
  89. for (const [i, col] of this.colSet.entries()) {
  90. this.sheet.getCell(this.multiHeader.headRows - 1, i).text(col.title).hAlign(1).vAlign(1);
  91. this.sheet.setColumnWidth(i, col.width);
  92. }
  93. if (this.multiHeader.headSpan) {
  94. for (const span of this.multiHeader.headSpan) {
  95. this.sheet.addSpan(span.row, span.col, span.rowCount, span.colCount);
  96. this.sheet.getCell(span.row, span.col).text(span.title);
  97. }
  98. }
  99. }
  100. show(colSet, multiHeader, fun) {
  101. this.afterSet = fun;
  102. this.colSet = colSet;
  103. this.multiHeader = multiHeader || { headRows: 1, headRowHeight: [32] };
  104. $('#header-row-count').val(this.multiHeader.headRows);
  105. this.reloadRowHeaderData();
  106. $('#multi-header').modal('show');
  107. }
  108. getMultiHeader() {
  109. const result = JSON.parse(JSON.stringify(this.multiHeader));
  110. result.headSpan = [];
  111. const spans = this.sheet.getSpans();
  112. for (const s of spans) {
  113. result.headSpan.push({col: s.col, row: s.row, colCount: s.colCount, rowCount: s.rowCount, title: this.sheet.getText(s.row, s.col) });
  114. }
  115. return result;
  116. }
  117. }
  118. const multiHeaderObj = new MultiHeaderObj();
  119. class TemplateDetailObj {
  120. constructor() {
  121. const self = this;
  122. this.firstShowDone = false;
  123. this.spread = SpreadJsObj.createNewSpread($('#col-set-spread')[0]);
  124. this.sheet = this.spread.getActiveSheet();
  125. const getTypeValue = function(data) {
  126. const typeInfo = validColInfo.find(x => { return x.key === data.type; });
  127. return typeInfo.name || '';
  128. };
  129. this.spreadSetting = {
  130. cols: [
  131. { title: '类型', colSpan: '1', rowSpan: '1', field: 'type', hAlign: 1, width: 80, formatter: '@', readOnly: true, getValue: getTypeValue },
  132. { title: '列名', colSpan: '1', rowSpan: '1', field: 'title', hAlign: 0, width: 130, formatter: '@' },
  133. { title: '列宽', colSpan: '1', rowSpan: '1', field: 'width', hAlign: 1, width: 70, type: 'Number' },
  134. // { title: '单位', colSpan: '1', rowSpan: '1', field: 'unit', hAlign: 1, width: 60, cellType: 'unit' },
  135. {
  136. title: '计算代号', colSpan: '1', rowSpan: '1', field: 'calc_code', hAlign: 1, width: 80, cellType: 'customizeCombo',
  137. comboItems: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P'],
  138. },
  139. { title: '小数位数', colSpan: '1', rowSpan: '1', field: 'decimal', hAlign: 1, width: 60, type: 'Number' },
  140. { title: '计算公式', colSpan: '1', rowSpan: '1', field: 'expr', hAlign: 0, width: 250 },
  141. ],
  142. emptyRows: 0,
  143. headRows: 1,
  144. headRowHeight: [32],
  145. defaultRowHeight: 21,
  146. headerFont: '12px 微软雅黑',
  147. font: '12px 微软雅黑',
  148. frozenLineColor: '#93b5e4',
  149. getColor: function(sheet, data, row, col, defaultColor) {
  150. if (!data) return defaultColor;
  151. const typeInfo = validColInfo.find(x => { return x.key === data.type; });
  152. if (!typeInfo) return defaultColor;
  153. return col && typeInfo.valid.indexOf(col.field) >= 0 ? defaultColor : '#f2f2f2';
  154. }
  155. };
  156. SpreadJsObj.initSheet(this.sheet, this.spreadSetting);
  157. this.spread.bind(spreadNS.Events.EditStarting, function(e, info) {
  158. if (self.readOnly) {
  159. info.cancel = true;
  160. return;
  161. }
  162. const col = info.sheet.zh_setting.cols[info.col];
  163. const select = SpreadJsObj.getSelectObject(info.sheet);
  164. const typeInfo = validColInfo.find(x => { return x.key === select.type; });
  165. if (typeInfo) {
  166. info.cancel = col && typeInfo.valid.indexOf(col.field) >= 0 ? false : true;
  167. } else {
  168. info.cancel = true;
  169. }
  170. });
  171. this.spread.bind(spreadNS.Events.EditEnded, function(e, info){
  172. if (!info.sheet.zh_setting) return;
  173. const select = SpreadJsObj.getSelectObject(info.sheet);
  174. const col = info.sheet.zh_setting.cols[info.col];
  175. const validText = col.field === 'spec_set' ? info.editingText : (info.editingText ? info.editingText.replace('\n', '') : '');
  176. if (col.field === 'spec_set') {
  177. select.spec_set = validText;
  178. } else if (col.field === 'calc_code') {
  179. const exist = self.colSetData.find(x => { return x.type === 'num' && x.calc_code === validText && x.field !== select.field; });
  180. if (exist) {
  181. toastr.warning('请勿输入重复的计算代号');
  182. } else {
  183. select.calc_code = validText;
  184. select.field = 'num_' + select.calc_code.toLowerCase();
  185. }
  186. } else if (col.field === 'width') {
  187. select.width = parseInt(validText);
  188. } else if (col.field === 'decimal') {
  189. const num = parseInt(validText);
  190. if (num < 0 || num > 6) {
  191. toastr.warning('小数位数仅可保留0-6位');
  192. } else {
  193. select.decimal = num;
  194. }
  195. } else {
  196. select[col.field] = validText;
  197. }
  198. SpreadJsObj.reLoadRowData(info.sheet, info.row);
  199. });
  200. SpreadJsObj.addDeleteBind(this.spread, function(sheet){
  201. if (!sheet.zh_setting) return;
  202. const sel = sheet.getSelections()[0];
  203. if (!sel) return;
  204. const col = sheet.zh_setting.cols[sel.col];
  205. if (col.readOnly) return;
  206. if (sel.colCount > 1) toastr.warning('请勿同时删除多列数据');
  207. for (let iRow = sel.row; iRow < sel.row + sel.rowCount; iRow ++) {
  208. const node = sheet.zh_data[iRow];
  209. if (!node) continue;
  210. if (col.type === 'Number') {
  211. node[col.field] = 0
  212. } else {
  213. node[col.field] = '';
  214. }
  215. }
  216. SpreadJsObj.reLoadRowData(sheet, sel.row, sel.rowCount);
  217. });
  218. // 右键菜单
  219. $.contextMenu({
  220. selector: '#col-set-spread',
  221. build: function ($trigger, e) {
  222. const target = SpreadJsObj.safeRightClickSelection($trigger, e, self.spread);
  223. return (target.hitTestType === GC.Spread.Sheets.SheetArea.viewport || target.hitTestType === GC.Spread.Sheets.SheetArea.rowHeader) && !self.readOnly && self.template;
  224. },
  225. items: {
  226. 'add_str': {
  227. name: '新增文本列',
  228. icon: 'fa-plus',
  229. callback: function (key, opt) {
  230. const select = SpreadJsObj.getSelectObject(self.sheet);
  231. self.addCol('str', select);
  232. },
  233. },
  234. 'add_num': {
  235. name: '新增数值/计算列',
  236. icon: 'fa-plus',
  237. callback: function (key, opt) {
  238. const select = SpreadJsObj.getSelectObject(self.sheet);
  239. self.addCol('num', select);
  240. },
  241. },
  242. 'remove': {
  243. name: '删除',
  244. icon: 'fa-remove',
  245. callback: function (key, opt) {
  246. self.remove();
  247. },
  248. },
  249. addSpr: '----',
  250. upMove: {
  251. name: '上移',
  252. icon: 'fa-arrow-up',
  253. callback: function (key, opt) {
  254. const select = SpreadJsObj.getSelectObject(self.sheet);
  255. self.move('upMove', select);
  256. },
  257. },
  258. downMove: {
  259. name: '下移',
  260. icon: 'fa-arrow-down',
  261. callback: function (key, opt) {
  262. const select = SpreadJsObj.getSelectObject(self.sheet);
  263. self.move('downMove', select);
  264. },
  265. },
  266. moveSpr: '----',
  267. multiHeader: {
  268. name: '多行表头设置',
  269. callback: function(key, opt) {
  270. multiHeaderObj.show(self.getCurrentColSet(), self.multi_header, function(multiHeader) {
  271. self.multi_header = multiHeader;
  272. });
  273. }
  274. }
  275. }
  276. });
  277. $('#reset').click(function() {
  278. self.reset();
  279. });
  280. $('#save').click(function() {
  281. self.save();
  282. });
  283. }
  284. addCol(type, select) {
  285. const colInfo = validColInfo.find(x => { return x.key === type; });
  286. if (!colInfo) {
  287. toastr.error('未知类型');
  288. return;
  289. }
  290. const existCount = this.colSetData.filter(x => { return x.type === type; }).length;
  291. if (existCount >= colInfo.count) {
  292. toastr.error(`${colInfo.name}列仅支持${colInfo.count}个`);
  293. return;
  294. }
  295. const nData = JSON.parse(JSON.stringify(colInfo.def));
  296. for (const f of colInfo.fields) {
  297. if (!this.colSetData.find(x => { return x.field === f; })) {
  298. nData.field = f;
  299. break;
  300. }
  301. }
  302. if (select) {
  303. const index = this.colSetData.findIndex(x => { return x.field === select.field; });
  304. if (index < 0) {
  305. toastr.error('选择的列配置不存在');
  306. return;
  307. }
  308. this.colSetData.splice(index, 0, nData);
  309. } else {
  310. this.colSetData.push(nData);
  311. }
  312. SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Data, this.colSetData);
  313. }
  314. remove() {
  315. const sel = this.sheet.getSelections()[0];
  316. if (!sel) return;
  317. this.colSetData.splice(sel.row, sel.rowCount);
  318. SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Data, this.colSetData);
  319. }
  320. move(type, select) {
  321. const index = this.colSetData.findIndex(x => { return x.field === select.field; });
  322. if (type === 'upMove' && index === 0) {
  323. toastr.error('不可上移');
  324. return;
  325. }
  326. if (type === 'downMove' && index === this.colSetData.length - 1) {
  327. toastr.error('不可下移');
  328. return;
  329. }
  330. this.colSetData.splice(index, 1);
  331. this.colSetData.splice(type === 'upMove' ? index - 1 : index + 1, 0, select);
  332. SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Data, this.colSetData);
  333. }
  334. reset() {
  335. this.colSetData = JSON.parse(JSON.stringify(this.template.col_set));
  336. SpreadJsObj.loadSheetData(this.sheet, SpreadJsObj.DataType.Data, this.colSetData);
  337. this.multi_header = this.template.multi_header ? JSON.parse(JSON.stringify(this.template.multi_header)) : null;
  338. }
  339. loadDetail(template) {
  340. this.template = template;
  341. this.readOnly = this.template.used_count > 0;
  342. if (!this.firstShowDone) {
  343. this.spread.refresh();
  344. this.firstShowDone = true;
  345. }
  346. if (this.readOnly) {
  347. $('#detail-ctrl').hide();
  348. } else {
  349. $('#detail-ctrl').show();
  350. }
  351. this.reset();
  352. }
  353. save(){
  354. const self = this;
  355. const update = { id: this.template.id, col_set: this.getCurrentColSet(), multi_header: this.multi_header || this.template.multi_header };
  356. if (!update.col_set) return;
  357. postData('save', { update }, function(result) {
  358. self.template.col_set = result.update.col_set;
  359. });
  360. }
  361. getCurrentColSet() {
  362. for (const col of this.colSetData) {
  363. if (col.type === 'num') {
  364. if (!col.calc_code) {
  365. toastr.error(`【${col.title}】未定义计算代号`);
  366. return null;
  367. }
  368. }
  369. }
  370. return this.colSetData;
  371. }
  372. export() {
  373. window.open(`/sp/${spid}/template/ctd?tid=${this.template.id}`);
  374. }
  375. import() {
  376. const self = this;
  377. BaseImportFile.show({
  378. validList: ['.ctd'],
  379. url: `/sp/${spid}/template/ctd/load?tid=${this.template.id}`,
  380. afterImport: function (result) {
  381. self.template.col_set = result.update.col_set;
  382. self.template.multi_header = result.update.multi_header;
  383. self.reset();
  384. }
  385. });
  386. }
  387. }
  388. const detailObj = new TemplateDetailObj();
  389. $('#preview').click(function() {
  390. const data = { type: 'cost', col_set: detailObj.getCurrentColSet(), multi_header: detailObj.multi_header || detailObj.template.multi_header };
  391. if (!data.col_set) return;
  392. calcTemplatePreview.preview(data);
  393. });
  394. const templateObj = (function(list){
  395. const templates = list;
  396. let curTemplate;
  397. const loadTemplateDetail = async function(template) {
  398. const result = await postDataAsync('load', { filter: 'detail', id: template.id, type: 'cost' });
  399. if (result && result.detail) {
  400. template.col_set = result.detail.col_set;
  401. template.multi_header = result.detail.multi_header;
  402. }
  403. };
  404. const refreshTemplate = async function() {
  405. if (!curTemplate) {
  406. // todo 隐藏模板详细界面
  407. } else {
  408. $('dd[templateId]').removeClass('bg-warning');
  409. $(`dd[templateId=${curTemplate.id}]`).addClass('bg-warning');
  410. if (!curTemplate.col_set) await loadTemplateDetail(curTemplate);
  411. detailObj.loadDetail(curTemplate);
  412. }
  413. };
  414. const setCurTemplate = function(template) {
  415. curTemplate = template;
  416. refreshTemplate();
  417. };
  418. const getCurTemplate = function() {
  419. return curTemplate;
  420. };
  421. const getTemplateCaptionHtml = function(template) {
  422. const usedHtml = template.used_count > 0 ? '<i class="ml-1 fa fa-lock text-danger"></i>' : '';
  423. return `<div class="d-flex justify-content-between align-items-center table-file" templateId="${template.id}"><div>${template.name}${usedHtml}</div>` +
  424. ' <div class="btn-group-table" style="display: none;">\n' +
  425. ' <a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="编辑" name="renameTemplate"><i class="fa fa-pencil fa-fw"></i></a>\n' +
  426. ' <a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="删除" name="delTemplate"><i class="fa fa-trash-o fa-fw text-danger"></i></a>\n' +
  427. '</div></div>';
  428. };
  429. const getTemplateHtml = function(template) {
  430. const html = [];
  431. html.push(`<dd class="list-group-item" templateId="${template.id}">`, getTemplateCaptionHtml(template), '</dd>');
  432. return html.join('');
  433. };
  434. const addTemplate = function() {
  435. postData('save', {add: { name: '' }, type: 'cost'}, function(result) {
  436. templates.push(result.add);
  437. $('#template-list').append(getTemplateHtml(result.add));
  438. });
  439. };
  440. const renameTemplate = function(id, name) {
  441. postData('save', { update: { id, name }, type: 'cost'}, function(result){
  442. const template = templates.find(x => { return x.id === result.update.id; });
  443. template.name = result.update.name;
  444. $(`dd[templateId=${template.id}]`).html(getTemplateCaptionHtml(template));
  445. });
  446. };
  447. const delTemplate = function(id){
  448. postData('save', {del: id, type: 'cost'}, function(result) {
  449. $(`dd[templateId=${id}]`).remove();
  450. const tIndex = templates.findIndex(x => { return x.id === id; });
  451. templates.splice(tIndex, 1);
  452. if (curTemplate.id === id) {
  453. curTemplate = null;
  454. refreshTemplate();
  455. }
  456. });
  457. };
  458. if (templates.length > 0) setCurTemplate(templates[0]);
  459. return { setCurTemplate, getCurTemplate, addTemplate, delTemplate, renameTemplate, getTemplateCaptionHtml }
  460. })(templateList);
  461. $('body').on('click', '.table-file', function(e) {
  462. if (this.getAttribute('renaming') === '1') return;
  463. if (e.target.tagName === 'A' || e.target.tagName === 'I' || e.target.tagName === 'INPUT') return;
  464. const templateId = this.getAttribute('templateId');
  465. const template = templateList.find(x => { return x.id === templateId; });
  466. templateObj.setCurTemplate(template);
  467. });
  468. $('body').on('mouseenter', ".table-file", function(){
  469. $(this).children(".btn-group-table").css("display","block");
  470. });
  471. $('body').on('mouseleave', ".table-file", function(){
  472. $(this).children(".btn-group-table").css("display","none");
  473. });
  474. $('body').on('click', 'a[name=renameTemplate]', function(e){
  475. $(this).parents('.table-file').attr('renaming', '1');
  476. $(`#${this.getAttribute('aria-describedby')}`).remove();
  477. const templateId = $(this).parents('.table-file').attr('templateId');
  478. const template = templateList.find(x => { return x.id === templateId; });
  479. if (!template) return;
  480. const html = [];
  481. html.push(`<div><input type="text" class="form-control form-control-sm" style="width: 160px" value="${template.name}"/></div>`);
  482. html.push('<div class="btn-group-table" style="display: none;">',
  483. `<a href="javascript: void(0)" name="renameOk" class="mr-1"><i class="fa fa-check fa-fw"></i></a>`,
  484. `<a href="javascript: void(0)" class="mr-1" name="renameCancel"><i class="fa fa-remove fa-fw text-danger"></i></a>`, '</div>');
  485. $(`.table-file[templateId=${templateId}]`).html(html.join(''));
  486. e.stopPropagation();
  487. });
  488. $('body').on('click', 'a[name=renameOk]', function(){
  489. const templateId = $(this).parents('.table-file').attr('templateId');
  490. const newName = $(this).parents('.table-file').find('input').val();
  491. templateObj.renameTemplate(templateId, newName);
  492. $(this).parents('.table-file').attr('renaming', '0');
  493. });
  494. $('body').on('click', 'a[name=renameCancel]', function() {
  495. $(this).parents('.table-file').attr('renaming', '0');
  496. const templateId = $(this).parents('.table-file').attr('templateId');
  497. const template = templateList.find(x => { return x.id === templateId; });
  498. if (!template) return;
  499. $(`.table-file[templateId=${templateId}]`).html(templateObj.getTemplateCaptionHtml(template));
  500. });
  501. $('body').on('click', 'a[name=delTemplate]', function(e) {
  502. e.stopPropagation();
  503. const templateId = $(this).parents('.table-file').attr('templateId');
  504. templateObj.delTemplate(templateId);
  505. });
  506. $('#export').click(function() {
  507. detailObj.export();
  508. });
  509. $('#import').click(function() {
  510. detailObj.import();
  511. });
  512. $('#addTemplate').click(function() {
  513. templateObj.addTemplate();
  514. });
  515. });