stats-quota.js 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254
  1. class DataHandler {
  2. constructor() {
  3. this[CONSTANTS.KEY_QUOTA_COMPANY] = [];
  4. this[CONSTANTS.KEY_QUOTA_MERCHANT] = [];
  5. this[CONSTANTS.KEY_QUOTA_CHANNEL] = [];
  6. this[CONSTANTS.KEY_QUOTA_SUMMARY] = [];
  7. this[CONSTANTS.KEY_QUOTA_CONDITION] = [];
  8. this.structureMapping = {
  9. // company结构: 排序(sort_order), 主体名称(name), 总额度(total_quota), 已用额度(used_quota), 备注(remark)
  10. [CONSTANTS.KEY_QUOTA_COMPANY]: ['sort', 'name', 'total_quota', 'used_quota', 'remark'],
  11. // merchant结构: 对应主体(subject), 机构id(mch_id), 已开票总金额(total_invoiced), 上次开票日期(last_invoice_date)
  12. [CONSTANTS.KEY_QUOTA_MERCHANT]: ['sort','subject', 'mch_id', 'total_invoiced', 'last_invoice_date', 'remark', 'month_opened'],
  13. // channel结构: 对应主体(subject), 通道id(store_id), 已开票总金额(total_invoiced), 上次开票日期(last_invoice_date)
  14. [CONSTANTS.KEY_QUOTA_CHANNEL]: ['sort', 'subject', 'store_id', 'total_invoiced', 'last_invoice_date', 'remark', 'month_opened'],
  15. //summary结构: 主体(subject)
  16. [CONSTANTS.KEY_QUOTA_SUMMARY]: ['sort', 'subject', 'invoice_remaining_amount', 'remaining_tickets', 'invoice_needed_amount', 'ticket_difference', 'adjustment_value'],
  17. //condition结构: 月份(month)
  18. [CONSTANTS.KEY_QUOTA_CONDITION]: ['month']
  19. };
  20. }
  21. translateToIndex(structure, data) {
  22. return this.structureMapping[structure].map((key, index) => data[key]);
  23. }
  24. translateToField(structure, data) {
  25. let translatedData = {};
  26. this.structureMapping[structure].forEach((key, index) => {
  27. translatedData[key] = data[index];
  28. });
  29. return translatedData;
  30. }
  31. getCompanyList() {
  32. const list = [], that = this;
  33. this[CONSTANTS.KEY_QUOTA_COMPANY].forEach(v => {
  34. list.push(that.translateToField(CONSTANTS.KEY_QUOTA_COMPANY, v));
  35. });
  36. return list;
  37. }
  38. getMerchantList() {
  39. const list = [], that = this;
  40. this[CONSTANTS.KEY_QUOTA_MERCHANT].forEach((v) => {
  41. list.push(that.translateToField(CONSTANTS.KEY_QUOTA_MERCHANT, v));
  42. });
  43. return list;
  44. }
  45. getChannelList() {
  46. const list = [], that = this;
  47. this[CONSTANTS.KEY_QUOTA_CHANNEL].forEach((v) => {
  48. list.push(that.translateToField(CONSTANTS.KEY_QUOTA_CHANNEL, v));
  49. });
  50. return list;
  51. }
  52. getSummaryList() {
  53. const list = [], that = this;
  54. this[CONSTANTS.KEY_QUOTA_SUMMARY].forEach((v) => {
  55. list.push(that.translateToField(CONSTANTS.KEY_QUOTA_SUMMARY, v));
  56. });
  57. return list;
  58. }
  59. getConditionList() { //条件设置只会有一条数据
  60. const list = [], that = this;
  61. this[CONSTANTS.KEY_QUOTA_CONDITION].forEach((v) => {
  62. list.push(that.translateToField(CONSTANTS.KEY_QUOTA_CONDITION, v));
  63. });
  64. return list;
  65. }
  66. async addOrUpdateData(structure, newData, uni_key) {
  67. let list = this[structure];
  68. let translatedData = this.translateToIndex(structure, newData);
  69. let index = list.findIndex(item => item[uni_key] === translatedData[uni_key]);
  70. if (index !== -1) {
  71. list[index] = translatedData;
  72. } else {
  73. list.push(translatedData);
  74. }
  75. await this.saveDataToBackend(structure);
  76. }
  77. async updateAllData(structure, dataList) {
  78. let list = this[structure];
  79. list.length = 0;
  80. dataList.forEach(data => {
  81. let translatedData = this.translateToIndex(structure, data);
  82. list.push(translatedData);
  83. });
  84. await this.saveDataToBackend(structure);
  85. }
  86. async removeData(structure, index) {
  87. let list = this[structure];
  88. if (index >= 0 && index < list.length) {
  89. list.splice(index, 1);
  90. await this.saveDataToBackend(structure);
  91. }
  92. }
  93. async saveDataToBackend(structure) {
  94. this.checkAndFixData(structure);
  95. const compressedData = this.compressData(structure);
  96. await statsApi.setStatsSettings(structure, compressedData);
  97. }
  98. checkAndFixData(structure) {
  99. let list = this[structure];
  100. let structureLength = this.structureMapping[structure].length;
  101. list.forEach(item => {
  102. if (item.length < structureLength) {
  103. for (let i = item.length; i < structureLength; i++) {
  104. item.push('');
  105. }
  106. }
  107. });
  108. }
  109. compressData(structure) {
  110. return this[structure];
  111. }
  112. decompressData(structure, data) {
  113. this[structure] = data[structure] || [];
  114. this.checkAndFixData(structure);
  115. }
  116. async loadComapny() {
  117. const data = await statsApi.getStatsSettings([CONSTANTS.KEY_QUOTA_COMPANY]);
  118. if (data.state !== true) {
  119. showErr('网络异常,请刷新页面重试');
  120. return;
  121. }
  122. this.decompressData(CONSTANTS.KEY_QUOTA_COMPANY, data);
  123. }
  124. async loadMerchant() {
  125. const data = await statsApi.getStatsSettings([CONSTANTS.KEY_QUOTA_MERCHANT]);
  126. if (data.state !== true) {
  127. showErr('网络异常,请刷新页面重试');
  128. return;
  129. }
  130. this.decompressData(CONSTANTS.KEY_QUOTA_MERCHANT, data);
  131. }
  132. async loadChannel() {
  133. const data = await statsApi.getStatsSettings([CONSTANTS.KEY_QUOTA_CHANNEL]);
  134. if (data.state !== true) {
  135. showErr('网络异常,请刷新页面重试');
  136. return;
  137. }
  138. this.decompressData(CONSTANTS.KEY_QUOTA_CHANNEL, data);
  139. }
  140. async loadSummary() {
  141. const data = await statsApi.getStatsSettings([CONSTANTS.KEY_QUOTA_SUMMARY]);
  142. if (data.state !== true) {
  143. showErr('网络异常,请刷新页面重试');
  144. return;
  145. }
  146. this.decompressData(CONSTANTS.KEY_QUOTA_SUMMARY, data);
  147. }
  148. async loadCondition() {
  149. const data = await statsApi.getStatsSettings([CONSTANTS.KEY_QUOTA_CONDITION]);
  150. if (data.state !== true) {
  151. showErr('网络异常,请刷新页面重试');
  152. return;
  153. }
  154. this.decompressData(CONSTANTS.KEY_QUOTA_CONDITION, data);
  155. }
  156. }
  157. class QuotaApiService extends BaseService {
  158. getStatsSettings(keys = []) {
  159. const endpoint = '?act=refill_tax_quota_stats&op=stats_settings&key=' + keys.join(',');
  160. return this.request(endpoint, 'GET', {});
  161. };
  162. setStatsSettings(key, value) {
  163. const endpoint = '?act=refill_tax_quota_stats&op=stats_settings_save';
  164. let data = {
  165. key: key,
  166. value: value
  167. };
  168. return this.request(endpoint, 'POST', data);
  169. };
  170. getSysMchs() {
  171. const endpoint = '?act=refill_tax_quota_stats&op=sys_mchs';
  172. return this.request(endpoint, 'GET', {});
  173. };
  174. getSysMchAmounts(mchids, month) {
  175. const endpoint = `?act=refill_tax_quota_stats&op=sys_mchs_amounts&mchids=${mchids}&month=${month}` ;
  176. return this.request(endpoint, 'GET', {});
  177. };
  178. getSysChans() {
  179. const endpoint = '?act=refill_tax_quota_stats&op=sys_chans';
  180. return this.request(endpoint, 'GET', {});
  181. };
  182. getSysChanAmounts(storeIds, month) {
  183. const endpoint = `?act=refill_tax_quota_stats&op=sys_chans_amounts&store_ids=${storeIds}&month=${month}`;
  184. return this.request(endpoint, 'GET', {});
  185. };
  186. }
  187. const statsApi = new QuotaApiService('');
  188. const dataHelper = new DataHandler();
  189. class CompanyComponent extends Component {
  190. constructor(options) {
  191. super(Object.assign(options, {
  192. template: `
  193. <div id="{{comp-id}}">
  194. <div class="quota-tablebar">
  195. <button v-on:click="open" type="button" class="layui-btn layui-bg-green">新增</button>
  196. <span>1、主体额度说明:(按月统计)</span>
  197. </div>
  198. <div id="company-model-{{comp-id}}" class="company-model" style="display: none;">
  199. <div class="leftmenu-manager-container selected-items" id="selected-items-{{comp-id}}"></div>
  200. <div class="model-form">
  201. <div class="form-group">
  202. <label for="name-{{comp-id}}"><span class="req-flag">*</span>主体名称:</label>
  203. <input type="text" id="name-{{comp-id}}" />
  204. </div>
  205. <div class="form-group">
  206. <label for="total_quota-{{comp-id}}">总额度(元):</label>
  207. <input type="text" id="total_quota-{{comp-id}}" />
  208. <span style="display: inline-block;margin-left: 10px;color: red;" id="total_quota_cn-{{comp-id}}"></span>
  209. </div>
  210. <div class="form-group">
  211. <label for="used_quota-{{comp-id}}">已用额度(元):</label>
  212. <input type="text" id="used_quota-{{comp-id}}" />
  213. <span style="display: inline-block;margin-left: 10px;color: red;" id="used_quota-{{comp-id}}"></span>
  214. </div>
  215. <div class="form-group">
  216. <label for="remark-{{comp-id}}">备注:</label>
  217. <textarea id="remark-{{comp-id}}"></textarea>
  218. </div>
  219. <button v-on:click="save" class="layui-btn layui-btn-sm">保存</button>
  220. </div>
  221. </div>
  222. </div>
  223. `
  224. }));
  225. this.parent = options.parent;
  226. this.cacheKey = this.props['type'];
  227. this.lastData = undefined;
  228. this.sortChange = false;
  229. }
  230. async mounted() {
  231. const that = this;
  232. $('#selected-items-' + this.id).sortable({
  233. containment: "parent",
  234. cursor: "move",
  235. update: function(event, ui) {
  236. that.sortChange = true;
  237. }
  238. }).disableSelection();
  239. this.inputEvent($('#total_quota-' + this.id));
  240. this.inputEvent($('#used_quota-' + this.id));
  241. }
  242. async open() {
  243. this.companyLeftList();
  244. const that = this;
  245. layer.open({
  246. type: 1,
  247. title: '新增主体',
  248. content: $('#company-model-' + this.props['comp-id']),
  249. area: ['800px', '500px'],
  250. success: function () {
  251. },
  252. end: async function () {
  253. if (that.sortChange === true) {
  254. const loadIndex = layer.load(2, {shade: [0.2, '#000']});
  255. await that.updateSort();
  256. layer.close(loadIndex);
  257. }
  258. if (that.isEdit() === true || that.sortChange === true) {
  259. if (that.parent && typeof that.parent.emit === 'function') {
  260. that.parent.emit(that.props['emit'], that.cacheKey, that.cacheData);
  261. }
  262. }
  263. }
  264. });
  265. }
  266. isEdit() {
  267. const companyList = dataHelper.getCompanyList();
  268. if (this.lastData.length !== companyList.length) {
  269. return true;
  270. }
  271. for (let i = 0; i < this.lastData.length; i++) {
  272. if (this.lastData[i].name !== companyList[i].name) {
  273. return true;
  274. }
  275. }
  276. return false;
  277. }
  278. companyLeftList() {
  279. const content = this.createModelContent();
  280. $('#company-model-' + this.id + ' .leftmenu-manager-container').html(content);
  281. this.bindEvents(); //左列表刷新后需要重新绑定v-on事件
  282. }
  283. createModelContent() {
  284. const companyList = dataHelper.getCompanyList();
  285. if (this.lastData === undefined) {
  286. this.lastData = deepCloneData(companyList);
  287. }
  288. let html = '';
  289. for (let i = 0; i < companyList.length; i++) {
  290. html += '<label class="ch-tags sortable-item" data-name="' + companyList[i].name + '">' +
  291. companyList[i].name +
  292. '<span class="delete-btn" v-on:click="delete" data-index="' + i + '">&times;</span>' +
  293. '</label>';
  294. }
  295. return html;
  296. }
  297. async save() {
  298. const that = this;
  299. const name = $('#name-' + this.id).val().trim();
  300. let total_quota = $('#total_quota-' + this.id).val().trim();
  301. let used_quota = $('#used_quota-' + this.id).val().trim();
  302. let remark = $('#remark-' + this.id).val().trim();
  303. if (name === '') {
  304. showErr('主体名称 未填写');
  305. return;
  306. }
  307. const companyList = dataHelper.getCompanyList();
  308. const used = companyList.find(item => item.name === name);
  309. if (used !== undefined) {
  310. showErr('不能重复设置主体');
  311. return;
  312. }
  313. total_quota = total_quota === '' ? 0 : parseFloat(total_quota);
  314. used_quota = used_quota === '' ? 0 : parseFloat(used_quota);
  315. const data = {
  316. sort: 0,
  317. name: name,
  318. total_quota: formatDecimals(total_quota, 2),
  319. used_quota: formatDecimals(used_quota, 2),
  320. remark: remark
  321. }
  322. await dataHelper.addOrUpdateData(CONSTANTS.KEY_QUOTA_COMPANY, data, 1);
  323. this.companyLeftList();
  324. }
  325. delete(event) {
  326. const that = this;
  327. const index = event.currentTarget.getAttribute('data-index');
  328. const list = dataHelper.getCompanyList();
  329. const company = list[index].name;
  330. const merchantList = dataHelper.getMerchantList();
  331. const usedMch = merchantList.find(item => item.subject === company);
  332. if (usedMch !== undefined) {
  333. showErr('机构统计中有该主体关联,不能删除');
  334. return;
  335. }
  336. const channelList = dataHelper.getChannelList();
  337. const usedChan = channelList.find(item => item.subject === company);
  338. if (usedChan !== undefined) {
  339. showErr('上游统计中有该主体关联,不能删除');
  340. return;
  341. }
  342. const summaryList = dataHelper.getSummaryList();
  343. const usedSummary = summaryList.find(item => item.subject === company);
  344. if (usedSummary !== undefined) {
  345. showErr('综合统计中有该主体关联,不能删除');
  346. return;
  347. }
  348. layer.confirm('确定要删除 "' + company + '" 吗?', {
  349. title: '删除确认',
  350. icon: 3,
  351. btn: ['删除', '取消'],
  352. btn1: async function (btn_i) {
  353. const loadIndex = layer.load(2, {shade: [0.2, '#000']});
  354. await dataHelper.removeData(CONSTANTS.KEY_QUOTA_COMPANY, index);
  355. that.companyLeftList();
  356. layer.close(loadIndex);
  357. layer.close(btn_i);
  358. }
  359. });
  360. }
  361. async updateSort() {
  362. const selectedItems = $('#' + this.id).find('#selected-items-' + this.id + ' .sortable-item');
  363. const that = this;
  364. const companyList = dataHelper.getCompanyList();
  365. selectedItems.each(function (index) {
  366. const value = $(this).data('name');
  367. const currIndex = companyList.findIndex(item => item.name === value);
  368. if (currIndex !== -1) {
  369. companyList[currIndex].sort = index;
  370. }
  371. });
  372. companyList.sort((a, b) => a.sort - b.sort);
  373. await dataHelper.updateAllData(CONSTANTS.KEY_QUOTA_COMPANY, companyList);
  374. }
  375. inputEvent(jqObj) {
  376. jqObj.off('input', handleInputNormal2)
  377. .off('keypress')
  378. .off('blur')
  379. .on('input', handleInputNormal2)
  380. .on('keypress', function (e) {
  381. if (e.keyCode === 13) {
  382. e.preventDefault();
  383. jqObj.blur();
  384. }
  385. })
  386. .on('blur', function () {
  387. let val = jqObj.val();
  388. if (!isNumeric(val)) {
  389. jqObj.val('0');
  390. return;
  391. }
  392. val = normalizeAmount(val);//过滤,比如前导 0
  393. jqObj.val(formatDecimals(val));
  394. const $span = jqObj.next('span');
  395. if ($span.length > 0) {
  396. $span.text('(' + cnMoneyFormat(val) + ')');
  397. }
  398. });
  399. }
  400. }
  401. class MerchantComponent extends Component {
  402. constructor(options) {
  403. super(Object.assign(options, {
  404. template: `
  405. <div id="{{comp-id}}">
  406. <div class="quota-tablebar">
  407. <button v-on:click="open" type="button" class="layui-btn layui-bg-green">新增</button>
  408. <span>2、下游(机构)</span>
  409. </div>
  410. <div id="merchant-model-{{comp-id}}" class="merchant-model" style="display: none;">
  411. <div class="leftmenu-manager-container selected-items" id="selected-items-{{comp-id}}"></div>
  412. <div class="model-form">
  413. <div class="form-group">
  414. <label for="company-{{comp-id}}"><span class="req-flag">*</span>主体:</label>
  415. <select id="company-{{comp-id}}">
  416. </select>
  417. </div>
  418. <div class="form-group">
  419. <label for="merchant-{{comp-id}}"><span class="req-flag">*</span>机构:</label>
  420. <div class="layui-form" lay-filter="mch_select_{{comp-id}}" style="width:220px;">
  421. <select id="merchant-{{comp-id}}" lay-search style="width:220px;">
  422. </select>
  423. </div>
  424. </div>
  425. <div class="form-group">
  426. <label for="total_invoiced-{{comp-id}}">已开票金额(元):</label>
  427. <input type="text" id="total_invoiced-{{comp-id}}" />
  428. </div>
  429. <div class="form-group">
  430. <label for="last_invoice_date-{{comp-id}}">上次开票日期:</label>
  431. <input type="text" id="last_invoice_date-{{comp-id}}" class="date" />
  432. </div>
  433. <div class="form-group">
  434. <label for="remark-{{comp-id}}">备注:</label>
  435. <textarea id="remark-{{comp-id}}"></textarea>
  436. </div>
  437. <button v-on:click="save" class="layui-btn layui-btn-sm">保存</button>
  438. </div>
  439. </div>
  440. </div>
  441. `
  442. }));
  443. this.parent = options.parent;
  444. this.cacheKey = this.props['type'];
  445. this.lastData = undefined;
  446. this.sortChange = false;
  447. this.sysMchs = [];
  448. }
  449. async mounted() {
  450. const that = this;
  451. $('#selected-items-' + this.id).sortable({
  452. containment: "parent",
  453. cursor: "move",
  454. update: function(event, ui) {
  455. that.sortChange = true;
  456. }
  457. }).disableSelection();
  458. this.inputEvent($('#total_invoiced-' + this.id));
  459. laydate.render({
  460. elem: '#last_invoice_date-' + this.id,
  461. type: 'date',
  462. trigger: 'click',
  463. max: new Date().setDate(new Date().getDate() - 1)
  464. });
  465. }
  466. async open() {
  467. this.companySelector();
  468. await this.mchSelector();
  469. this.merchantLeftList();
  470. const that = this;
  471. layer.open({
  472. type: 1,
  473. title: '新增下游',
  474. content: $('#merchant-model-' + this.props['comp-id']),
  475. area: ['800px', '580px'],
  476. success: function () {
  477. },
  478. end: async function () {
  479. if (that.sortChange === true) {
  480. const loadIndex = layer.load(2, {shade: [0.2, '#000']});
  481. await that.updateSort();
  482. layer.close(loadIndex);
  483. }
  484. if (that.isEdit() === true) {
  485. if (that.parent && typeof that.parent.emit === 'function') {
  486. that.parent.emit(that.props['emit'], that.cacheKey, that.cacheData);
  487. }
  488. }
  489. }
  490. });
  491. }
  492. isEdit() {
  493. const merchantList = dataHelper.getMerchantList();
  494. if (this.lastData.length !== merchantList.length) {
  495. return true;
  496. }
  497. for (let i = 0; i < this.lastData.length; i++) {
  498. if (this.lastData[i].mch_id !== merchantList[i].mch_id) {
  499. return true;
  500. }
  501. }
  502. return false;
  503. }
  504. merchantLeftList() {
  505. const content = this.createModelContent();
  506. $('#merchant-model-' + this.id + ' .leftmenu-manager-container').html(content);
  507. this.bindEvents();
  508. }
  509. createModelContent() {
  510. const merchantList = dataHelper.getMerchantList();
  511. if (this.lastData === undefined) {
  512. this.lastData = deepCloneData(merchantList);
  513. }
  514. let html = '';
  515. for (let i = 0; i < merchantList.length; i++) {
  516. const sysItem = this.sysMchs.find(sItem => sItem.mch_id === merchantList[i].mch_id);
  517. let mchName = '';
  518. if (sysItem !== undefined) {
  519. mchName = sysItem.mch_name;
  520. } else {
  521. mchName = merchantList[i].mch_id + '(无效项)';
  522. }
  523. html += '<label class="ch-tags sortable-item" data-name="' + mchName + '">' +
  524. mchName +
  525. '<span class="delete-btn" v-on:click="delete" data-index="' + i + '">&times;</span>' +
  526. '</label>';
  527. }
  528. return html;
  529. }
  530. companySelector() {
  531. let content = '';
  532. const companyList = dataHelper.getCompanyList();
  533. companyList.forEach(item=> {
  534. content += '<option>' + item.name + '</option>';
  535. });
  536. $('#merchant-model-' + this.id + ' #company-' + this.id).html(content);
  537. }
  538. async mchSelector() {
  539. let content = '';
  540. const mchData = await statsApi.getSysMchs();
  541. if (mchData.state === true) {
  542. mchData.list.forEach(item => {
  543. content += '<option value="' + item.mch_id + '">' + item.mch_name + '</option>';
  544. });
  545. $('#merchant-model-' + this.id + ' #merchant-' + this.id).html(content);
  546. this.sysMchs = mchData.list;
  547. }
  548. //只能在渲染option之后才可以设置select搜索组件
  549. const componentId = this.id;
  550. layui.use('form', function() {
  551. const form = layui.form;
  552. form.render('select', 'mch_select_' + componentId);
  553. });
  554. }
  555. async save() {
  556. const subject = $('#company-' + this.id).val();
  557. let mch_id = $('#merchant-' + this.id).val();
  558. let total_invoiced = $('#total_invoiced-' + this.id).val().trim();
  559. let last_invoice_date = $('#last_invoice_date-' + this.id).val();
  560. let remark = $('#remark-' + this.id).val().trim();
  561. if (subject === '') {
  562. showErr('主体未选择');
  563. return;
  564. }
  565. if (mch_id === '') {
  566. showErr('机构未选择');
  567. return;
  568. }
  569. if (last_invoice_date !== '' && !isValidDate(last_invoice_date)) {
  570. showErr('开票日期格式错误');
  571. return;
  572. }
  573. const merchantList = dataHelper.getMerchantList();
  574. const used = merchantList.find(item => item.mch_id === mch_id);
  575. if (used !== undefined) {
  576. showErr('不能重复设置机构');
  577. return;
  578. }
  579. total_invoiced = total_invoiced === '' ? 0 : parseFloat(total_invoiced);
  580. const data = {
  581. sort: 0,
  582. subject: subject,
  583. mch_id: mch_id,
  584. total_invoiced: formatDecimals(total_invoiced, 2),
  585. last_invoice_date: last_invoice_date,
  586. remark: remark
  587. }
  588. await dataHelper.addOrUpdateData(CONSTANTS.KEY_QUOTA_MERCHANT, data, 2);
  589. this.merchantLeftList();
  590. }
  591. delete(event) {
  592. const that = this;
  593. const index = event.currentTarget.getAttribute('data-index');
  594. const list = dataHelper.getMerchantList();
  595. const mch_id = list[index].mch_id;
  596. let mch_name = '';
  597. const sysItem = this.sysMchs.find(sItem => sItem.mch_id === mch_id);
  598. if (sysItem !== undefined) {
  599. mch_name = sysItem.mch_name;
  600. } else {
  601. mch_name = `${mch_id}(无效项)`;
  602. }
  603. layer.confirm('确定要删除 "' + mch_name + '" 吗?', {
  604. title: '删除确认',
  605. icon: 3,
  606. btn: ['删除', '取消'],
  607. btn1: async function (btn_i) {
  608. const loadIndex = layer.load(2, {shade: [0.2, '#000']});
  609. await dataHelper.removeData(CONSTANTS.KEY_QUOTA_MERCHANT, index);
  610. that.merchantLeftList();
  611. layer.close(loadIndex);
  612. layer.close(btn_i);
  613. }
  614. });
  615. }
  616. async updateSort() {
  617. const selectedItems = $('#' + this.id).find('#selected-items-' + this.id + ' .sortable-item');
  618. const that = this;
  619. const merchantList = dataHelper.getMerchantList();
  620. selectedItems.each(function (index) {
  621. const value = $(this).data('name');
  622. const sysItem = that.sysMchs.find(sItem => sItem.mch_name === value);
  623. if (sysItem === undefined) {
  624. return;
  625. }
  626. const currIndex = merchantList.findIndex(item => item.mch_id === sysItem.mch_id);
  627. if (currIndex !== -1) {
  628. merchantList[currIndex].sort = index;
  629. }
  630. });
  631. merchantList.sort((a, b) => a.sort - b.sort);
  632. await dataHelper.updateAllData(CONSTANTS.KEY_QUOTA_MERCHANT, merchantList);
  633. }
  634. inputEvent(jqObj) {
  635. jqObj.off('input', handleInputNormal2)
  636. .off('keypress')
  637. .off('blur')
  638. .on('input', handleInputNormal2)
  639. .on('keypress', function (e) {
  640. if (e.keyCode === 13) {
  641. e.preventDefault();
  642. jqObj.blur();
  643. }
  644. })
  645. .on('blur', function () {
  646. let val = jqObj.val();
  647. if (!isNumeric(val)) {
  648. jqObj.val('0');
  649. return;
  650. }
  651. val = normalizeAmount(val);//过滤,比如前导 0
  652. jqObj.val(formatDecimals(val));
  653. });
  654. }
  655. }
  656. class ChannelComponent extends Component {
  657. constructor(options) {
  658. super(Object.assign(options, {
  659. template: `
  660. <div id="{{comp-id}}">
  661. <div class="quota-tablebar">
  662. <button v-on:click="open" type="button" class="layui-btn layui-bg-green">新增</button>
  663. <span>3、上游</span>
  664. </div>
  665. <div id="channel-model-{{comp-id}}" class="channel-model" style="display: none;">
  666. <div class="leftmenu-manager-container selected-items" id="selected-items-{{comp-id}}"></div>
  667. <div class="model-form">
  668. <div class="form-group">
  669. <label for="company-{{comp-id}}"><span class="req-flag">*</span>主体:</label>
  670. <select id="company-{{comp-id}}">
  671. </select>
  672. </div>
  673. <div class="form-group">
  674. <label for="channel-{{comp-id}}"><span class="req-flag">*</span>通道:</label>
  675. <div class="layui-form" lay-filter="chan_select_{{comp-id}}" style="width:220px;">
  676. <select id="channel-{{comp-id}}" lay-search style="width:220px;">
  677. </select>
  678. </div>
  679. </div>
  680. <div class="form-group">
  681. <label for="total_invoiced-{{comp-id}}">已开票金额(元):</label>
  682. <input type="text" id="total_invoiced-{{comp-id}}" />
  683. </div>
  684. <div class="form-group">
  685. <label for="last_invoice_date-{{comp-id}}">上次开票日期:</label>
  686. <input type="text" id="last_invoice_date-{{comp-id}}" class="date" />
  687. </div>
  688. <div class="form-group">
  689. <label for="remark-{{comp-id}}">备注:</label>
  690. <textarea id="remark-{{comp-id}}"></textarea>
  691. </div>
  692. <button v-on:click="save" class="layui-btn layui-btn-sm">保存</button>
  693. </div>
  694. </div>
  695. </div>
  696. `
  697. }));
  698. this.parent = options.parent;
  699. this.cacheKey = this.props['type'];
  700. this.lastData = undefined;
  701. this.sortChange = false;
  702. this.sysChans = [];
  703. }
  704. async mounted() {
  705. const that = this;
  706. $('#selected-items-' + this.id).sortable({
  707. containment: "parent",
  708. cursor: "move",
  709. update: function(event, ui) {
  710. that.sortChange = true;
  711. }
  712. }).disableSelection();
  713. this.inputEvent($('#total_invoiced-' + this.id));
  714. laydate.render({
  715. elem: '#last_invoice_date-' + this.id,
  716. type: 'date',
  717. trigger: 'click',
  718. max: new Date().setDate(new Date().getDate() - 1)
  719. });
  720. }
  721. async open() {
  722. this.companySelector();
  723. await this.chanSelector();
  724. this.channelLeftList();
  725. const that = this;
  726. layer.open({
  727. type: 1,
  728. title: '新增上游',
  729. content: $('#channel-model-' + this.props['comp-id']),
  730. area: ['800px', '580px'],
  731. success: function () {
  732. },
  733. end: async function () {
  734. if (that.sortChange === true) {
  735. const loadIndex = layer.load(2, {shade: [0.2, '#000']});
  736. await that.updateSort();
  737. layer.close(loadIndex);
  738. }
  739. if (that.isEdit() === true) {
  740. if (that.parent && typeof that.parent.emit === 'function') {
  741. that.parent.emit(that.props['emit'], that.cacheKey, that.cacheData);
  742. }
  743. }
  744. }
  745. });
  746. }
  747. isEdit() {
  748. const channelList = dataHelper.getChannelList();
  749. if (this.lastData.length !== channelList.length) {
  750. return true;
  751. }
  752. for (let i = 0; i < this.lastData.length; i++) {
  753. if (this.lastData[i].store_id !== channelList[i].store_id) {
  754. return true;
  755. }
  756. }
  757. return false;
  758. }
  759. channelLeftList() {
  760. const content = this.createModelContent();
  761. $('#channel-model-' + this.id + ' .leftmenu-manager-container').html(content);
  762. this.bindEvents();
  763. }
  764. createModelContent() {
  765. const channelList = dataHelper.getChannelList();
  766. if (this.lastData === undefined) {
  767. this.lastData = deepCloneData(channelList);
  768. }
  769. let html = '';
  770. for (let i = 0; i < channelList.length; i++) {
  771. const sysItem = this.sysChans.find(sItem => sItem.store_id === channelList[i].store_id);
  772. let chanName = '';
  773. if (sysItem !== undefined) {
  774. chanName = sysItem.store_name;
  775. } else {
  776. chanName = channelList[i].store_id + '(无效项)';
  777. }
  778. html += '<label class="ch-tags sortable-item" data-name="' + chanName + '">' +
  779. chanName +
  780. '<span class="delete-btn" v-on:click="delete" data-index="' + i + '">&times;</span>' +
  781. '</label>';
  782. }
  783. return html;
  784. }
  785. companySelector() {
  786. let content = '';
  787. const companyList = dataHelper.getCompanyList();
  788. companyList.forEach(item=> {
  789. content += '<option>' + item.name + '</option>';
  790. });
  791. $('#channel-model-' + this.id + ' #company-' + this.id).html(content);
  792. }
  793. async chanSelector() {
  794. let content = '';
  795. const chanData = await statsApi.getSysChans();
  796. if (chanData.state === true) {
  797. chanData.list.forEach(item => {
  798. content += '<option value="' + item.store_id + '">' + item.store_name + '</option>';
  799. });
  800. $('#channel-model-' + this.id + ' #channel-' + this.id).html(content);
  801. this.sysChans = chanData.list;
  802. }
  803. //只能在渲染option之后才可以设置select搜索组件
  804. const componentId = this.id;
  805. layui.use('form', function() {
  806. const form = layui.form;
  807. form.render('select', 'chan_select_' + componentId);
  808. });
  809. }
  810. async save() {
  811. const subject = $('#company-' + this.id).val();
  812. let store_id = $('#channel-' + this.id).val();
  813. let total_invoiced = $('#total_invoiced-' + this.id).val().trim();
  814. let last_invoice_date = $('#last_invoice_date-' + this.id).val();
  815. let remark = $('#remark-' + this.id).val().trim();
  816. if (subject === '') {
  817. showErr('主体未选择');
  818. return;
  819. }
  820. if (store_id === '') {
  821. showErr('通道未选择');
  822. return;
  823. }
  824. if (last_invoice_date !== '' && !isValidDate(last_invoice_date)) {
  825. showErr('开票日期格式错误');
  826. return;
  827. }
  828. const channelList = dataHelper.getChannelList();
  829. const used = channelList.find(item => item.store_id === store_id);
  830. if (used !== undefined) {
  831. showErr('不能重复设置通道')
  832. }
  833. total_invoiced = total_invoiced === '' ? 0 : parseFloat(total_invoiced);
  834. const data = {
  835. sort: 0,
  836. subject: subject,
  837. store_id: store_id,
  838. total_invoiced: formatDecimals(total_invoiced, 2),
  839. last_invoice_date: last_invoice_date,
  840. remark: remark
  841. }
  842. await dataHelper.addOrUpdateData(CONSTANTS.KEY_QUOTA_CHANNEL, data, 2);
  843. this.channelLeftList();
  844. }
  845. delete(event) {
  846. const that = this;
  847. const index = event.currentTarget.getAttribute('data-index');
  848. const list = dataHelper.getChannelList();
  849. const store_id = list[index].store_id;
  850. let store_name = '';
  851. const sysItem = this.sysChans.find(sItem => sItem.store_id === store_id);
  852. if (sysItem !== undefined) {
  853. store_name = sysItem.store_name;
  854. } else {
  855. store_name = `${store_id}(无效项)`;
  856. }
  857. layer.confirm('确定要删除 "' + store_name + '" 吗?', {
  858. title: '删除确认',
  859. icon: 3,
  860. btn: ['删除', '取消'],
  861. btn1: async function (btn_i) {
  862. const loadIndex = layer.load(2, {shade: [0.2, '#000']});
  863. await dataHelper.removeData(CONSTANTS.KEY_QUOTA_CHANNEL, index);
  864. that.channelLeftList();
  865. layer.close(loadIndex);
  866. layer.close(btn_i);
  867. }
  868. });
  869. }
  870. async updateSort() {
  871. const selectedItems = $('#' + this.id).find('#selected-items-' + this.id + ' .sortable-item');
  872. const that = this;
  873. const channelList = dataHelper.getChannelList();
  874. selectedItems.each(function (index) {
  875. const value = $(this).data('name');
  876. const sysItem = that.sysChans.find(sItem => sItem.store_name === value);
  877. if (sysItem === undefined) {
  878. return;
  879. }
  880. const currIndex = channelList.findIndex(item => item.store_id === sysItem.store_id);
  881. if (currIndex !== -1) {
  882. channelList[currIndex].sort = index;
  883. }
  884. });
  885. channelList.sort((a, b) => a.sort - b.sort);
  886. await dataHelper.updateAllData(CONSTANTS.KEY_QUOTA_CHANNEL, channelList);
  887. }
  888. inputEvent(jqObj) {
  889. jqObj.off('input', handleInputNormal2)
  890. .off('keypress')
  891. .off('blur')
  892. .on('input', handleInputNormal2)
  893. .on('keypress', function (e) {
  894. if (e.keyCode === 13) {
  895. e.preventDefault();
  896. jqObj.blur();
  897. }
  898. })
  899. .on('blur', function () {
  900. let val = jqObj.val();
  901. if (!isNumeric(val)) {
  902. jqObj.val('0');
  903. return;
  904. }
  905. val = normalizeAmount(val);//过滤,比如前导 0
  906. jqObj.val(formatDecimals(val));
  907. });
  908. }
  909. }
  910. class SummaryComponent extends Component {
  911. constructor(options) {
  912. super(Object.assign(options, {
  913. template: `
  914. <div id="{{comp-id}}">
  915. <div class="quota-tablebar">
  916. <button v-on:click="open" type="button" class="layui-btn layui-bg-green">新增</button>
  917. <span>4、综合统计</span>
  918. </div>
  919. <div id="summary-model-{{comp-id}}" class="summary-model" style="display: none;">
  920. <div class="leftmenu-manager-container selected-items" id="selected-items-{{comp-id}}"></div>
  921. <div class="model-form" style="height: 400px">
  922. <div class="form-group">
  923. <label for="company-{{comp-id}}"><span class="req-flag">*</span>主体:</label>
  924. <div id="company-{{comp-id}}">
  925. </div>
  926. </div>
  927. <button v-on:click="save" class="layui-btn layui-btn-sm">保存</button>
  928. </div>
  929. </div>
  930. </div>
  931. `
  932. }));
  933. this.parent = options.parent;
  934. this.cacheKey = this.props['type'];
  935. this.lastData = undefined;
  936. this.sortChange = false;
  937. }
  938. async mounted() {
  939. const that = this;
  940. $('#selected-items-' + this.id).sortable({
  941. containment: "parent",
  942. cursor: "move",
  943. update: function(event, ui) {
  944. that.sortChange = true;
  945. }
  946. }).disableSelection();
  947. }
  948. async open() {
  949. this.companySelector();
  950. this.summaryLeftList();
  951. const that = this;
  952. layer.open({
  953. type: 1,
  954. title: '设置综合统计主体',
  955. content: $('#summary-model-' + this.props['comp-id']),
  956. area: ['800px', '580px'],
  957. success: function () {
  958. },
  959. end: async function () {
  960. if (that.sortChange === true) {
  961. const loadIndex = layer.load(2, {shade: [0.2, '#000']});
  962. await that.updateSort();
  963. layer.close(loadIndex);
  964. }
  965. if (that.isEdit() === true) {
  966. if (that.parent && typeof that.parent.emit === 'function') {
  967. that.parent.emit(that.props['emit'], that.cacheKey, that.cacheData);
  968. }
  969. }
  970. }
  971. });
  972. }
  973. isEdit() {
  974. const companyList = dataHelper.getCompanyList();
  975. if (this.lastData.length !== companyList.length) {
  976. return true;
  977. }
  978. for (let i = 0; i < this.lastData.length; i++) {
  979. if (this.lastData[i].store_id !== companyList[i].store_id) {
  980. return true;
  981. }
  982. }
  983. return false;
  984. }
  985. summaryLeftList() {
  986. const content = this.createModelContent();
  987. $('#summary-model-' + this.id + ' .leftmenu-manager-container').html(content);
  988. this.bindEvents();
  989. }
  990. createModelContent() {
  991. const summaryList = dataHelper.getSummaryList();
  992. if (this.lastData === undefined) {
  993. this.lastData = deepCloneData(summaryList);
  994. }
  995. let html = '';
  996. for (let i = 0; i < summaryList.length; i++) {
  997. html += '<label class="ch-tags sortable-item" data-name="' + summaryList[i].subject + '">' +
  998. summaryList[i].subject +
  999. '<span class="delete-btn" v-on:click="delete" data-index="' + i + '">&times;</span>' +
  1000. '</label>';
  1001. }
  1002. return html;
  1003. }
  1004. companySelector() {
  1005. let content = '';
  1006. const companyList = dataHelper.getCompanyList();
  1007. const summaryList = dataHelper.getSummaryList();
  1008. const selectedSubjects = summaryList.map(item => item.subject);
  1009. companyList.forEach(item=> {
  1010. const isChecked = selectedSubjects.includes(item.name) ? 'checked' : '';
  1011. content += `
  1012. <label class="company-checkbox">
  1013. <input type="checkbox" name="summary_company" value="${item.name}" ${isChecked}>
  1014. <span>${item.name}</span>
  1015. </label>
  1016. `;
  1017. });
  1018. $('#summary-model-' + this.id + ' #company-' + this.id).html(content);
  1019. }
  1020. async save() {
  1021. const selectedValues = [];
  1022. $('#summary-model-' + this.id + ' #company-' + this.id + ' input[type="checkbox"]:checked').each(function() {
  1023. selectedValues.push($(this).val());
  1024. });
  1025. const summaryList = dataHelper.getSummaryList();
  1026. selectedValues.forEach(item => {
  1027. const activeItem = summaryList.find(sItem => sItem.subject === item);
  1028. if (activeItem === undefined) {
  1029. summaryList.push({
  1030. sort: 0,
  1031. subject: item
  1032. });
  1033. }
  1034. });
  1035. await dataHelper.updateAllData(CONSTANTS.KEY_QUOTA_SUMMARY, summaryList);
  1036. this.summaryLeftList();
  1037. }
  1038. delete(event) {
  1039. const that = this;
  1040. const index = event.currentTarget.getAttribute('data-index');
  1041. const list = dataHelper.getSummaryList();
  1042. const subject = list[index].subject;
  1043. layer.confirm('确定要删除 "' + subject + '" 吗?', {
  1044. title: '删除确认',
  1045. icon: 3,
  1046. btn: ['删除', '取消'],
  1047. btn1: async function (btn_i) {
  1048. const loadIndex = layer.load(2, {shade: [0.2, '#000']});
  1049. await dataHelper.removeData(CONSTANTS.KEY_QUOTA_SUMMARY, index);
  1050. that.summaryLeftList();
  1051. layer.close(loadIndex);
  1052. layer.close(btn_i);
  1053. }
  1054. });
  1055. }
  1056. async updateSort() {
  1057. const selectedItems = $('#' + this.id).find('#selected-items-' + this.id + ' .sortable-item');
  1058. const that = this;
  1059. const summaryList = dataHelper.getSummaryList();
  1060. selectedItems.each(function (index) {
  1061. const value = $(this).data('name');
  1062. const currIndex = summaryList.findIndex(item => item.subject === value);
  1063. if (currIndex !== -1) {
  1064. summaryList[currIndex].sort = index;
  1065. }
  1066. });
  1067. summaryList.sort((a, b) => a.sort - b.sort);
  1068. await dataHelper.updateAllData(CONSTANTS.KEY_QUOTA_SUMMARY, summaryList);
  1069. }
  1070. }
  1071. function filterSubjectPrefix(str) {
  1072. return str.replace(/^\d+-/, '');
  1073. }
  1074. function sortBySubjectContinuity(data) {
  1075. const groups = {};
  1076. const ordered = [];
  1077. data.forEach(item => {
  1078. if (!groups[item.subject]) {
  1079. groups[item.subject] = [];
  1080. }
  1081. groups[item.subject].push(item);
  1082. });
  1083. const added = new Set();
  1084. data.forEach(item => {
  1085. if (!added.has(item)) {
  1086. ordered.push(item);
  1087. added.add(item);
  1088. groups[item.subject].forEach(other => {
  1089. if (!added.has(other)) {
  1090. ordered.push(other);
  1091. added.add(other);
  1092. }
  1093. });
  1094. }
  1095. });
  1096. return ordered;
  1097. }