statistics.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. <template>
  2. <div>
  3. <ele-pro-table
  4. v-if="
  5. statisticsType == 1 &&
  6. localDetails[0] &&
  7. localDetails[0].statisticsValue
  8. "
  9. ref="table"
  10. :columns="typeOneColumns"
  11. :datasource="localDetails[0].statisticsValue"
  12. cacheKey="mes-statistics-type-1-2511041946"
  13. :needPage="false"
  14. >
  15. <template
  16. v-for="(item, index) in localDetails[0].statisticsValue[0]"
  17. v-slot:[`column_${index}`]="{ row }"
  18. >
  19. <div :key="`column_${index}`" class="column_content">
  20. <div v-if="item.isOnlyShow">
  21. {{ item.value }}
  22. </div>
  23. <el-input
  24. v-else
  25. v-model.number="item.value"
  26. :placeholder="
  27. item.formula
  28. ? item.formula.replace(/[\[\]]/g, '')
  29. : `请输入${item.title}`
  30. "
  31. :disabled="Boolean(item.formula)"
  32. @input="handleDataChange(item, row)"
  33. >
  34. <template slot="append">
  35. <div style="width: 80px; height: 100%">
  36. <DictSelection
  37. v-model="item.unit"
  38. dictName="工艺参数单位"
  39. placeholder="单位"
  40. >
  41. </DictSelection>
  42. </div>
  43. </template>
  44. </el-input>
  45. </div>
  46. </template>
  47. </ele-pro-table>
  48. <ele-pro-table
  49. v-if="
  50. statisticsType == 2 &&
  51. localDetails[1] &&
  52. localDetails[1].statisticsValue
  53. "
  54. ref="table"
  55. :columns="typeTwoColumns"
  56. :datasource="localDetails[1].statisticsValue"
  57. cacheKey="mes-statistics-type-2-2511041946"
  58. :needPage="false"
  59. >
  60. <template
  61. v-for="(item, index) in localDetails[1].statisticsValue[0]"
  62. v-slot:[`column_${index}`]="{ row }"
  63. >
  64. <div :key="`column_${index}`" class="column_content">
  65. <div v-if="row[index].isOnlyShow">
  66. {{ row[index].value }}
  67. </div>
  68. <el-input
  69. v-else
  70. v-model.number="row[index].value"
  71. :placeholder="
  72. row[index].formula
  73. ? row[index].formula.replace(/[\[\]]/g, '')
  74. : `请输入${row[index].title}`
  75. "
  76. :disabled="Boolean(row[index].formula)"
  77. @input="handleDataChange(row[index], row)"
  78. >
  79. <template v-if="!row[index].isOnlyShow" slot="append">
  80. <div style="width: 80px; height: 100%">
  81. <DictSelection
  82. v-model="row[index].unit"
  83. dictName="工艺参数单位"
  84. placeholder="单位"
  85. >
  86. </DictSelection>
  87. </div>
  88. </template>
  89. </el-input>
  90. </div>
  91. </template>
  92. </ele-pro-table>
  93. <ele-pro-table
  94. v-if="
  95. statisticsType == 3 &&
  96. localDetails[2] &&
  97. localDetails[2].statisticsValue
  98. "
  99. ref="table"
  100. :columns="typeThreeColumns"
  101. :datasource="localDetails[2].statisticsValue"
  102. cacheKey="mes-statistics-type-3-2511051037"
  103. :needPage="false"
  104. >
  105. <template
  106. v-for="(item, index) in localDetails[2].statisticsValue[0]"
  107. v-slot:[`column_${index}`]="{ row }"
  108. >
  109. <div :key="`column_${index}`" class="column_content">
  110. <div v-if="row[index].isOnlyShow">
  111. {{ row[index].value }}
  112. </div>
  113. <el-input
  114. v-else
  115. v-model.number="row[index].value"
  116. :placeholder="
  117. row[index].formula
  118. ? row[index].formula.replace(/[\[\]]/g, '')
  119. : `请输入${row[index].title}`
  120. "
  121. :disabled="Boolean(row[index].formula)"
  122. @input="handleDataChange(row[index], row)"
  123. >
  124. <template v-if="!row[index].isOnlyShow" slot="append">
  125. <div style="width: 80px; height: 100%">
  126. <DictSelection
  127. v-model="row[index].unit"
  128. dictName="工艺参数单位"
  129. placeholder="单位"
  130. >
  131. </DictSelection>
  132. </div>
  133. </template>
  134. </el-input>
  135. </div>
  136. </template>
  137. </ele-pro-table>
  138. </div>
  139. </template>
  140. <script>
  141. import {
  142. getAllRoutingTaskList,
  143. getPickAndFeed
  144. } from '@/api/producetaskrecordrulesrecord/index';
  145. export default {
  146. props: {
  147. details: {
  148. type: Array,
  149. default: () => []
  150. },
  151. statisticsType: {
  152. type: String,
  153. default: '1' // 1-成品统计,2-物料统计,3-工序统计
  154. },
  155. // 工艺路线id 必填
  156. routingId: {
  157. type: [String, Number],
  158. required: true
  159. },
  160. // workOrderId 工单id 必填
  161. workOrderId: {
  162. type: [String, Number],
  163. required: true
  164. },
  165. // produceTaskId 工序id
  166. produceTaskId: {
  167. type: [String, Number],
  168. required: true
  169. },
  170. //ruleId
  171. ruleId: {
  172. type: [String, Number],
  173. required: true
  174. }
  175. },
  176. watch: {
  177. details: {
  178. deep: true,
  179. immediate: true,
  180. handler(val) {
  181. // const details = JSON.parse(JSON.stringify(val));
  182. this.buildDetials(val);
  183. }
  184. },
  185. // ruleId变化时修改
  186. ruleId: {
  187. handler(val) {
  188. this.localDetails.forEach((item) => {
  189. item.ruleId = val;
  190. });
  191. this.emitChange();
  192. },
  193. immediate: true
  194. }
  195. },
  196. computed: {
  197. // 成品统计表头
  198. typeOneColumns() {
  199. if (
  200. this.localDetails.length > 0 &&
  201. this.localDetails[0].statisticsValue &&
  202. this.localDetails[0].statisticsType == 1
  203. ) {
  204. return this.localDetails[0].statisticsValue[0].map((item, index) => {
  205. return {
  206. label: item.title,
  207. slot: `column_${index}`,
  208. align: 'center'
  209. };
  210. });
  211. } else {
  212. return [];
  213. }
  214. },
  215. // 物料统计表头
  216. typeTwoColumns() {
  217. if (
  218. this.localDetails.length > 1 &&
  219. this.localDetails[1].statisticsValue &&
  220. this.localDetails[1].statisticsType == 2
  221. ) {
  222. return this.localDetails[1].statisticsValue[0].map((item, index) => {
  223. return {
  224. label: item.title,
  225. slot: `column_${index}`,
  226. align: 'center'
  227. };
  228. });
  229. } else {
  230. return [];
  231. }
  232. },
  233. // 工序统计表头
  234. typeThreeColumns() {
  235. if (
  236. this.localDetails.length > 2 &&
  237. this.localDetails[2].statisticsValue &&
  238. this.localDetails[2].statisticsType == 3
  239. ) {
  240. return this.localDetails[2].statisticsValue[0].map((item, index) => {
  241. return {
  242. label: item.title,
  243. slot: `column_${index}`,
  244. align: 'center'
  245. };
  246. });
  247. } else {
  248. return [];
  249. }
  250. }
  251. },
  252. data() {
  253. return {
  254. localDetails: []
  255. };
  256. },
  257. methods: {
  258. // 通知父组件数据变更
  259. emitChange() {
  260. console.log('this.localDetails', this.localDetails);
  261. this.$emit('update:details', this.localDetails);
  262. },
  263. // 构建统计数据
  264. async buildDetials(detials) {
  265. if (detials.length === 0) {
  266. this.localDetails = [];
  267. return;
  268. }
  269. // 已构建过统计数据
  270. if (detials[0].statisticsValue) {
  271. return (this.localDetails = JSON.parse(JSON.stringify(detials)));
  272. }
  273. // 构建统计数据
  274. const list = JSON.parse(JSON.stringify(detials));
  275. // 并发请求工序任务列表与物料数据
  276. const [routingTaskList, pickAndFeed] = await Promise.all([
  277. getAllRoutingTaskList({ routingId: this.routingId }),
  278. getPickAndFeed({
  279. workOrderId: this.workOrderId,
  280. produceTaskId: this.produceTaskId
  281. })
  282. ]);
  283. console.log('routingTaskList 工序数据', routingTaskList);
  284. console.log('物料数据 pickAndFeed', pickAndFeed);
  285. this.localDetails = [
  286. {
  287. ruleId: this.ruleId,
  288. statisticsType: 1,
  289. statisticsValue: [
  290. list
  291. .filter((i) => i.statisticsType == 1)
  292. .map((i) => {
  293. return {
  294. title: i.paramValue,
  295. value: i.defaultValue,
  296. formula: i.formula,
  297. unit: i.unitName
  298. };
  299. })
  300. ]
  301. },
  302. // 物料统计
  303. {
  304. ruleId: this.ruleId,
  305. statisticsType: 2,
  306. statisticsValue: pickAndFeed.map((pick) => {
  307. const data = list
  308. .filter((i) => i.statisticsType == 2)
  309. .map((i) => {
  310. return {
  311. title: i.paramValue,
  312. value: i.defaultValue,
  313. formula: i.formula,
  314. unit: i.unitName
  315. };
  316. });
  317. data.unshift({
  318. title: '领用量',
  319. value: pick.pickQuantity,
  320. formula: '',
  321. unit: pick.pickUnit
  322. });
  323. data.unshift({
  324. title: '使用量',
  325. value: pick.feedQuantity,
  326. formula: '',
  327. unit: pick.feedUnit
  328. });
  329. data.unshift({
  330. title: '物料编码',
  331. value: pick.categoryCode,
  332. formula: '',
  333. unit: '',
  334. // 仅展示不计算和输入
  335. isOnlyShow: true
  336. });
  337. data.unshift({
  338. title: '物料名称',
  339. value: pick.categoryName,
  340. formula: '',
  341. unit: '',
  342. // 仅展示不计算和输入
  343. isOnlyShow: true
  344. });
  345. return data;
  346. })
  347. },
  348. // 工序统计
  349. {
  350. ruleId: this.ruleId,
  351. statisticsType: 3,
  352. statisticsValue: routingTaskList.list.map((task) => {
  353. const data = list
  354. .filter((i) => i.statisticsType == 3)
  355. .map((i) => {
  356. return {
  357. title: i.paramValue,
  358. value: i.defaultValue,
  359. formula: i.formula,
  360. unit: i.unitName
  361. };
  362. });
  363. data.unshift({
  364. title: '工序名称',
  365. value: task.taskInstanceName,
  366. formula: '',
  367. unit: '',
  368. // 仅展示不计算和输入
  369. isOnlyShow: true
  370. });
  371. return data;
  372. })
  373. }
  374. ];
  375. console.log('this.localDetails', this.localDetails);
  376. // 通知父组件数据变更
  377. this.emitChange();
  378. },
  379. // 当数据变化时计算公式
  380. handleDataChange(item, row) {
  381. // todo 计算公式
  382. console.log('item', item);
  383. console.log('row', row);
  384. // 寻找当前行的所有可计算项,进行计算更新
  385. const formulaItmes = row.filter((i) => i.formula);
  386. formulaItmes.forEach((formulaItem) => {
  387. // 简单示例 公式为 [(][合格品量][+][取样量][)][/][接收量] 其中合格品量、取样量、接收量为标题在row中寻找对应value进行计算
  388. const tokens = formulaItem.formula
  389. ? formulaItem.formula.match(/\[([^\]]+)\]/g) || []
  390. : [];
  391. let expr = '';
  392. let invalid = false;
  393. for (const token of tokens) {
  394. if (invalid) break;
  395. const content = token.slice(1, -1).trim();
  396. if (['+', '-', '*', '/', '(', ')', '%'].includes(content)) {
  397. expr += content;
  398. } else {
  399. const target = row.find((r) => r.title === content);
  400. // 若对应的项不存在 或 value为空 则取消计算
  401. if (
  402. !target ||
  403. target.value === '' ||
  404. target.value === null ||
  405. target.value === undefined
  406. ) {
  407. invalid = true;
  408. expr = ''; // 置空表达式,后续将不计算
  409. break;
  410. }
  411. const num = Number(target.value);
  412. if (!Number.isFinite(num)) {
  413. invalid = true;
  414. expr = '';
  415. break;
  416. }
  417. expr += num;
  418. }
  419. }
  420. // 若无效则直接跳过后续计算 (expr 已被置空)
  421. if (/^[0-9+\-*/().\s]+$/.test(expr)) {
  422. try {
  423. console.log('expr', expr);
  424. const result = Function(`"use strict";return (${expr})`)();
  425. let value =
  426. Number.isFinite(result) && !Number.isNaN(result) ? result : '';
  427. if (value) {
  428. // 如果超出4位小数 则保留4位小数
  429. if (typeof value === 'number') {
  430. const dec = value.toString().split('.')[1];
  431. if (dec && dec.length > 4) {
  432. const parts = value.toString().split('.');
  433. if (parts[1] && parts[1].length > 4) {
  434. value = Number(parts[0] + '.' + parts[1].slice(0, 4));
  435. }
  436. }
  437. }
  438. }
  439. formulaItem.value = value;
  440. } catch (e) {
  441. formulaItem.value = '';
  442. }
  443. } else {
  444. formulaItem.value = '';
  445. }
  446. });
  447. // 通知父组件数据变更
  448. this.emitChange();
  449. },
  450. // 验证所有行/列(排除仅展示项),收集缺失项并提示;返回布尔值
  451. validateStatisticsFilled() {
  452. if (
  453. !Array.isArray(this.localDetails) ||
  454. this.localDetails.length === 0
  455. ) {
  456. this.$message.error('统计数据为空');
  457. return false;
  458. }
  459. const typeNameMap = {
  460. 1: '成品统计',
  461. 2: '物料统计',
  462. 3: '工序统计'
  463. };
  464. const missing = [];
  465. this.localDetails.forEach((detail, detailIndex) => {
  466. if (!detail || !detail.statisticsValue) return;
  467. // 统一转成多行结构
  468. const rows =
  469. detail.statisticsType === 1
  470. ? [detail.statisticsValue[0] || []]
  471. : detail.statisticsValue || [];
  472. rows.forEach((row, rowIndex) => {
  473. // 记录行的标识(物料名称 / 工序名称)
  474. let rowLabel = '';
  475. if (detail.statisticsType === 2) {
  476. const nameItem = row.find((i) => i && i.title === '物料名称');
  477. if (nameItem)
  478. rowLabel = `(${nameItem.value || '物料行' + (rowIndex + 1)})`;
  479. } else if (detail.statisticsType === 3) {
  480. const procItem = row.find((i) => i && i.title === '工序名称');
  481. if (procItem)
  482. rowLabel = `(${procItem.value || '工序行' + (rowIndex + 1)})`;
  483. }
  484. row.forEach((item, colIndex) => {
  485. if (!item || item.isOnlyShow) return;
  486. const val = item.value;
  487. if (val === '' || val === null || val === undefined) {
  488. missing.push(
  489. `${typeNameMap[detail.statisticsType]}${rowLabel} - 第${
  490. colIndex + 1
  491. }列【${item.title}】未填写`
  492. );
  493. }
  494. });
  495. });
  496. });
  497. if (missing.length > 0) {
  498. // 过多时截断
  499. const displayList =
  500. missing.length > 1
  501. ? missing.slice(0, 1).concat(`, 共有 ${missing.length} 项未填写`)
  502. : missing;
  503. this.$message.warning(`请完善统计数据:\n${displayList.join('\n')}`);
  504. return false;
  505. }
  506. return true;
  507. }
  508. }
  509. };
  510. </script>
  511. <style scoped lang="scss">
  512. .column_content ::v-deep .el-input-group__append {
  513. padding: 0;
  514. }
  515. </style>