index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. <template>
  2. <div class="ele-body">
  3. <el-card shadow="never">
  4. <!-- <print /> -->
  5. <ele-split-layout
  6. width="500px"
  7. allow-collapse
  8. :right-style="{ overflow: 'hidden' }"
  9. >
  10. <div class="left-box">
  11. <div class="left-item">
  12. <div class="title">产品</div>
  13. <el-popover placement="right" width="400" trigger="click">
  14. <div>
  15. <el-form
  16. ref="productFormRef"
  17. :model="productBody"
  18. label-width="80px"
  19. class="productForm"
  20. >
  21. <el-form-item label="关键字:">
  22. <el-input
  23. v-model="productBody.param"
  24. placeholder="产品编码、产品名称"
  25. ></el-input>
  26. </el-form-item>
  27. <el-form-item label="查询范围:">
  28. <el-date-picker
  29. style="width: 100%"
  30. v-model="productBody.createTimeStart"
  31. type="date"
  32. placeholder="选择日期"
  33. format="yyyy-MM-dd HH:mm:ss"
  34. value-format="yyyy-MM-dd HH:mm:ss"
  35. :picker-options="{
  36. disabledDate: (date) => {
  37. const end = productBody.createTimeEnd
  38. ? new Date(productBody.createTimeEnd)
  39. : new Date();
  40. const start = new Date(end);
  41. start.setMonth(start.getMonth() - 3);
  42. return (
  43. date.getTime() < start.getTime() ||
  44. date.getTime() > end.getTime()
  45. );
  46. }
  47. }"
  48. >
  49. </el-date-picker>
  50. <div style="margin-top: 10px">
  51. <el-date-picker
  52. style="width: 100%"
  53. v-model="productBody.createTimeEnd"
  54. type="date"
  55. placeholder="选择日期"
  56. format="yyyy-MM-dd HH:mm:ss"
  57. value-format="yyyy-MM-dd HH:mm:ss"
  58. @change="createTimeEndDateChange"
  59. :picker-options="{
  60. disabledDate: (date) => {
  61. const now = new Date();
  62. const start = new Date(now);
  63. start.setMonth(start.getMonth() - 3);
  64. return (
  65. date.getTime() < start.getTime() ||
  66. date.getTime() > now.getTime()
  67. );
  68. }
  69. }"
  70. >
  71. </el-date-picker>
  72. </div>
  73. </el-form-item>
  74. <el-form-item>
  75. <el-button
  76. type="primary"
  77. @click="getAllProductInWorkOrder"
  78. icon="el-icon-search"
  79. :loading="productButLoading"
  80. >查询</el-button
  81. >
  82. <el-button
  83. type="primary"
  84. @click="resetSubmit"
  85. icon="el-icon-refresh-left"
  86. :loading="productButLoading"
  87. >重置</el-button
  88. >
  89. </el-form-item>
  90. </el-form>
  91. </div>
  92. <el-button slot="reference" type="primary" style="width: 100%">
  93. 搜索
  94. </el-button>
  95. </el-popover>
  96. <div class="list-box" style="padding-bottom: 80px">
  97. <div
  98. class="list-item"
  99. v-for="i in productList"
  100. :key="i.productName + i.productCode"
  101. :class="{ active: i.productCode == tableQuery.productCode }"
  102. @click="changeProductName(i)"
  103. >
  104. <!-- <el-popover
  105. placement="top-start"
  106. width="200"
  107. trigger="hover"
  108. :content="`产品编码:${i.productCode},产品名称:${i.productName}`"
  109. >
  110. <template slot="reference">
  111. <div class="ele-elip" style="width: 100%">{{
  112. i.productCode
  113. }}</div>
  114. <div class="ele-elip" style="width: 100%">{{
  115. i.productName
  116. }}</div>
  117. </template>
  118. </el-popover> -->
  119. <div class="ele-elip" style="width: 100%">
  120. {{ i.productCode }}
  121. </div>
  122. <div class="ele-elip" style="width: 100%">
  123. {{ i.productName }}
  124. </div>
  125. </div>
  126. </div>
  127. <div class="footer-box">
  128. <el-pagination
  129. @size-change="handleSizeChange"
  130. @current-change="handleCurrentChange"
  131. :current-page.sync="productBody.pageNum"
  132. :page-size="productBody.size"
  133. layout="sizes,pager"
  134. :total="total"
  135. :pager-count="5"
  136. small
  137. >
  138. </el-pagination>
  139. </div>
  140. </div>
  141. <div class="left-item">
  142. <div class="title">批次号</div>
  143. <div v-if="batchNos.length > 0" class="list-box">
  144. <div
  145. class="list-item"
  146. v-for="i in batchNos"
  147. :key="i"
  148. :class="{ active: i == tableQuery.batchNo }"
  149. @click="changeBatchNo(i)"
  150. >
  151. <div> {{ i ? i : '暂无批次号' }}</div>
  152. </div>
  153. </div>
  154. <div v-else class="list-box">
  155. <div class="list-item" style="text-align: center; color: #888">
  156. 请先选择产品
  157. </div>
  158. </div>
  159. </div>
  160. <div class="left-item">
  161. <div class="title">批记录类型</div>
  162. <div class="list-box">
  163. <div
  164. class="list-item"
  165. v-for="i in types"
  166. :key="i.name"
  167. :class="{ active: activeType == i.name }"
  168. @click="changeActiveType(i)"
  169. >
  170. {{ i.name }}
  171. </div>
  172. </div>
  173. </div>
  174. </div>
  175. <template v-slot:content>
  176. <div class="table-box">
  177. <component
  178. :is="activeComponentName"
  179. ref="tableRef"
  180. :tableQuery="tableQuery"
  181. ></component>
  182. </div>
  183. </template>
  184. </ele-split-layout>
  185. </el-card>
  186. </div>
  187. </template>
  188. <script>
  189. import dictMixins from '@/mixins/dictMixins';
  190. import tableColumnsMixin from '@/mixins/tableColumnsMixin';
  191. import seekPage from '@/components/common/seekPage.vue';
  192. import { getAllProductInWorkOrder } from '@/api/produce/workOrder';
  193. import workOrderTable from './components/tables/workOrderTable.vue';
  194. import materialTable from './components/tables/materialTable.vue';
  195. import materialReturnTable from './components/tables/materialReturnTable.vue';
  196. import workReportTable from './components/tables/workReportTable.vue';
  197. import qualityWorkOrderTable from './components/tables/qualityWorkOrderTable.vue';
  198. import wmsOutInt from './components/tables/wmsOutInt.vue';
  199. import batchRecordTable from './components/tables/batchRecordTable.vue';
  200. import deviceBatchRecordTable from './components/tables/deviceBatchRecordTable.vue';
  201. import craftFilesTable from './components/tables/craftFilesTable.vue';
  202. import qualityInspection from './components/tables/qualityInspection.vue';
  203. import certificate from './components/tables/certificate.vue';
  204. import qualityReportApproval from './components/tables/qualityReportApproval.vue';
  205. import print from './print/index.vue';
  206. export default {
  207. components: {
  208. seekPage,
  209. workOrderTable,
  210. materialTable,
  211. materialReturnTable,
  212. workReportTable,
  213. qualityWorkOrderTable,
  214. wmsOutInt,
  215. batchRecordTable,
  216. deviceBatchRecordTable,
  217. craftFilesTable,
  218. qualityInspection,
  219. certificate,
  220. qualityReportApproval,
  221. print
  222. },
  223. name: 'batchRecord',
  224. mixins: [dictMixins, tableColumnsMixin],
  225. data() {
  226. return {
  227. types: [
  228. {
  229. name: '生产工单',
  230. componentName: 'workOrderTable'
  231. },
  232. {
  233. name: '领料单',
  234. componentName: 'materialTable'
  235. },
  236. {
  237. name: '退料单',
  238. componentName: 'materialReturnTable'
  239. },
  240. {
  241. name: '报工',
  242. componentName: 'workReportTable'
  243. },
  244. {
  245. name: '质检工单',
  246. componentName: 'qualityWorkOrderTable'
  247. },
  248. {
  249. name: '入库单',
  250. componentName: 'wmsOutInt'
  251. },
  252. {
  253. name: '生产记录',
  254. componentName: 'batchRecordTable'
  255. },
  256. {
  257. name: '设备工单',
  258. componentName: 'deviceBatchRecordTable'
  259. },
  260. {
  261. name: '工艺文件',
  262. componentName: 'craftFilesTable'
  263. },
  264. {
  265. name: '首件两检',
  266. componentName: 'qualityInspection'
  267. },
  268. {
  269. name: '质检报告',
  270. componentName: 'qualityReportApproval'
  271. },
  272. {
  273. name: '合格证',
  274. componentName: 'certificate'
  275. }
  276. ],
  277. activeType: '生产工单',
  278. activeComponentName: 'workOrderTable',
  279. // 产品搜索条件
  280. productBody: {
  281. param: '',
  282. pageNum: 1,
  283. size: 10,
  284. // 创建开始时间和结束时间
  285. createTimeStart: '',
  286. createTimeEnd: ''
  287. },
  288. productButLoading: false,
  289. total: 0,
  290. // 批次号
  291. batchNos: [],
  292. // 产品列表
  293. productList: [],
  294. tableQuery: {
  295. productCode: '',
  296. batchNo: ''
  297. },
  298. // 扫码枪缓冲
  299. scanBuffer: '',
  300. scanLastTime: 0,
  301. /** keep-alive 下避免与 created 重复请求;第二次 activated 再复位 */
  302. _pageHasActivated: false
  303. };
  304. },
  305. computed: {},
  306. mounted() {
  307. window.addEventListener('keydown', this.handleScanKey, true);
  308. },
  309. beforeDestroy() {
  310. window.removeEventListener('keydown', this.handleScanKey, true);
  311. },
  312. activated() {
  313. if (this._pageHasActivated) {
  314. this.initPage();
  315. }
  316. this._pageHasActivated = true;
  317. },
  318. created() {
  319. this.initPage();
  320. },
  321. methods: {
  322. // 初始化页面:复位筛选条件并拉取产品列表
  323. initPage() {
  324. // 时间范围复位为最近一个月(含今天)
  325. const endDate = new Date();
  326. const startDate = new Date();
  327. startDate.setMonth(startDate.getMonth() - 1);
  328. startDate.setHours(0, 0, 0, 0);
  329. this.productBody.param = '';
  330. this.productBody.pageNum = 1;
  331. this.productBody.size = 10;
  332. this.productBody.createTimeStart = this.$util.toDateString(
  333. startDate,
  334. 'yyyy-MM-dd HH:mm:ss'
  335. );
  336. this.productBody.createTimeEnd = this.$util.toDateString(
  337. endDate,
  338. 'yyyy-MM-dd HH:mm:ss'
  339. );
  340. // 复位选中状态
  341. this.tableQuery.productCode = '';
  342. this.tableQuery.batchNo = '';
  343. this.batchNos = [];
  344. this.activeType = '生产工单';
  345. this.activeComponentName = 'workOrderTable';
  346. this.getAllProductInWorkOrder();
  347. },
  348. // 刷新表格数据
  349. reload() {
  350. this.$refs.tableRef?.reload();
  351. },
  352. // 获取产品和批次号
  353. async getAllProductInWorkOrder() {
  354. try {
  355. this.productButLoading = true;
  356. const { list, count } = await getAllProductInWorkOrder(
  357. this.productBody
  358. );
  359. this.productList = list;
  360. console.log('this.productList', this.productList);
  361. this.productButLoading = false;
  362. this.total = count;
  363. } catch (error) {
  364. this.productButLoading = false;
  365. }
  366. },
  367. // 选择产品
  368. changeProductName(i) {
  369. this.tableQuery.productCode = i.productCode;
  370. // 设置批次号
  371. this.batchNos = i.batchNos;
  372. this.tableQuery.batchNo = '';
  373. // 刷新表格
  374. this.reload();
  375. },
  376. // 选择批次号
  377. changeBatchNo(i) {
  378. this.tableQuery.batchNo = i;
  379. this.reload();
  380. },
  381. // 选择批记录类型
  382. changeActiveType(i) {
  383. this.activeType = i.name;
  384. this.activeComponentName = i.componentName;
  385. this.reload();
  386. },
  387. resetSubmit() {
  388. this.productBody.param = '';
  389. this.productBody.createTimeStart = null;
  390. this.productBody.createTimeEnd = null;
  391. this.getAllProductInWorkOrder();
  392. },
  393. handleSizeChange(size) {
  394. this.productBody.size = size;
  395. this.getAllProductInWorkOrder();
  396. },
  397. handleCurrentChange(pageNum) {
  398. this.productBody.pageNum = pageNum;
  399. this.getAllProductInWorkOrder();
  400. },
  401. createTimeEndDateChange() {
  402. // 格式化 productBody.createTimeEnd 的时间为 23:59:59
  403. if (this.productBody.createTimeEnd) {
  404. const date = new Date(this.productBody.createTimeEnd);
  405. date.setHours(23, 59, 59);
  406. this.productBody.createTimeEnd = this.$util.toDateString(
  407. date,
  408. 'yyyy-MM-dd HH:mm:ss'
  409. );
  410. }
  411. },
  412. // 扫码枪监听:根据按键间隔判断是否为扫码输入
  413. handleScanKey(e) {
  414. const now = Date.now();
  415. const interval = now - this.scanLastTime;
  416. this.scanLastTime = now;
  417. if (e.key === 'Enter') {
  418. if (this.scanBuffer.length >= 3 && interval < 100) {
  419. e.preventDefault();
  420. e.stopPropagation();
  421. const code = this.scanBuffer;
  422. const target = e.target;
  423. this.scanBuffer = '';
  424. // 清掉首字符泄漏到输入框中的内容
  425. if (
  426. target &&
  427. (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') &&
  428. typeof target.value === 'string' &&
  429. code.endsWith(target.value) === false &&
  430. target.value.length > 0 &&
  431. code.startsWith(target.value)
  432. ) {
  433. target.value = '';
  434. target.dispatchEvent(new Event('input', { bubbles: true }));
  435. }
  436. this.processScanResult(code);
  437. } else {
  438. this.scanBuffer = '';
  439. }
  440. return;
  441. }
  442. if (e.key.length !== 1) return;
  443. // 间隔过长视为人工输入,重置缓冲
  444. if (interval > 50) {
  445. this.scanBuffer = e.key;
  446. return;
  447. }
  448. // 已检测到扫码连续输入,阻止字符进入输入框
  449. e.preventDefault();
  450. e.stopPropagation();
  451. this.scanBuffer += e.key;
  452. },
  453. // 处理扫码结果:产品编码-批次号(按第一个 - 拆分,批次号可能包含 -)
  454. async processScanResult(code) {
  455. const idx = code.indexOf('-');
  456. if (idx <= 0 || idx === code.length - 1) {
  457. this.$message.warning(`扫码结果格式不正确:${code}`);
  458. return;
  459. }
  460. const productCode = code.substring(0, idx);
  461. const batchNo = code.substring(idx + 1);
  462. let product = this.productList.find(
  463. (p) => p.productCode === productCode
  464. );
  465. // 当前列表未找到则按产品编码搜索一次
  466. if (!product) {
  467. this.productBody.param = productCode;
  468. this.productBody.pageNum = 1;
  469. await this.getAllProductInWorkOrder();
  470. product = this.productList.find(
  471. (p) => p.productCode === productCode
  472. );
  473. }
  474. if (!product) {
  475. this.$message.warning(`未找到产品:${productCode}`);
  476. return;
  477. }
  478. // 选中产品
  479. this.tableQuery.productCode = product.productCode;
  480. // 选中批次号:扫码命中时仅展示该批次号,未命中则展示该产品全部批次号
  481. const allBatchNos = product.batchNos || [];
  482. if (allBatchNos.includes(batchNo)) {
  483. this.batchNos = [batchNo];
  484. this.tableQuery.batchNo = batchNo;
  485. this.$message.success(`扫码成功:${productCode} / ${batchNo}`);
  486. } else {
  487. this.batchNos = allBatchNos;
  488. this.tableQuery.batchNo = '';
  489. this.$message.warning(
  490. `产品 ${productCode} 下未找到批次号:${batchNo}`
  491. );
  492. }
  493. this.reload();
  494. }
  495. }
  496. };
  497. </script>
  498. <style lang="scss" scoped>
  499. .left-box {
  500. height: 100%;
  501. display: flex;
  502. .left-item {
  503. flex: 1;
  504. border-right: 1px solid #ededed;
  505. padding: 0 5px;
  506. width: 33.3%;
  507. position: relative;
  508. box-sizing: border-box;
  509. .title {
  510. text-align: center;
  511. font-size: 16px;
  512. padding: 10px 0;
  513. border-bottom: 1px solid #ededed;
  514. margin-bottom: 15px;
  515. }
  516. .list-box {
  517. margin-top: 10px;
  518. max-height: 78vh;
  519. overflow-y: auto;
  520. box-sizing: border-box;
  521. .list-item {
  522. padding: 10px 10px;
  523. border-bottom: 1px solid #ededed;
  524. font-size: 14px;
  525. cursor: pointer;
  526. word-break: break-all;
  527. }
  528. .active {
  529. background: #1890ff;
  530. color: #fff;
  531. }
  532. }
  533. .footer-box {
  534. width: 100%;
  535. height: 70px;
  536. position: absolute;
  537. left: 0;
  538. bottom: 0;
  539. z-index: 1;
  540. background: #fff;
  541. padding: 10px 0;
  542. :deep(.el-pagination__sizes) {
  543. display: block;
  544. margin-bottom: 10px;
  545. }
  546. }
  547. }
  548. }
  549. .productForm {
  550. .el-form-item {
  551. margin-bottom: 10px;
  552. }
  553. }
  554. .table-box {
  555. height: 85vh;
  556. overflow-y: auto;
  557. }
  558. </style>