scannedMaterials.vue 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196
  1. <template>
  2. <gui-page :custom-header="true" :header-class="['gui-theme-background-color']">
  3. <template #gHeader>
  4. <view style="height:44px;" class="gui-flex gui-nowrap gui-rows gui-align-items-center">
  5. <!-- 使用组件实现返回按钮及返回首页按钮 -->
  6. <text style="font-size:44rpx;" class="gui-header-leader-btns gui-color-white font-icons"
  7. @tap="goBack">&#xe6c5;</text>
  8. <!-- 导航文本此处也可以是其他自定义内容 -->
  9. <text
  10. class="gui-h4 gui-blod gui-flex1 gui-text-center gui-ellipsis gui-color-white gui-primary-text">扫描出库明细</text>
  11. <!-- 此处加一个右侧展位元素与左侧同宽,实现标题居中 -->
  12. <!-- 实际宽度请根据自己情况设置 -->
  13. <view style="width:40px;" />
  14. <!-- 如果右侧有其他内容可以利用条件编译和定位来实现-->
  15. </view>
  16. </template>
  17. <template #gBody>
  18. <view class="list-content">
  19. <view class="scan">
  20. <view class="scan-card">
  21. <!-- 禁用软键盘 -->
  22. <!-- <uni-easyinput ref="easyinput" v-model="scanBatchNumber" :input-border="false"
  23. :clearable="false" type="text" focus @focus="handleInputFocus" placeholder="请扫描物料二维码"
  24. @confirm="handleKeydown" /> -->
  25. <!-- 不禁用软键盘 -->
  26. <uni-easyinput ref="easyinput" v-model="scanBatchNumber" :input-border="false"
  27. :clearable="false" type="text" focus placeholder="请扫描物料二维码"
  28. @confirm="handleKeydown" />
  29. <text class="font-icons" @click="handleScanMaterial">&#xe6b7;</text>
  30. </view>
  31. </view>
  32. <view class="tabs">
  33. <div class="tabs-list">
  34. <text :class="!isBefore?'tabs-item-active':'tabs-item'" @click="isBefore = false">物料需求</text>
  35. <text :class="isBefore?'tabs-item-active':'tabs-item'" @click="isBefore = true">已扫物料</text>
  36. </div>
  37. </view>
  38. <view v-if="!isBefore" class="custom-table">
  39. <uni-table border stripe empty-text="暂无更多数据">
  40. <!-- 表头行 -->
  41. <uni-tr class="custom-table-head">
  42. <uni-th align="center" width="180px">物料</uni-th>
  43. <uni-th align="center" width="120px">应备数</uni-th>
  44. <uni-th align="center" width="120px">已备数</uni-th>
  45. </uni-tr>
  46. <!-- 表格数据行 -->
  47. <!-- 可点击行,方法存在 -->
  48. <uni-tr v-for="(item, key) in tableData" :key="key" @click="handleToDetails(item)">
  49. <!-- 不可点击行 -->
  50. <!-- <uni-tr v-for="(item, key) in tableData" :key="key"> -->
  51. <uni-td align="center">{{ item.materialNo }}({{ item.materialName }})</uni-td>
  52. <uni-td align="center">{{ item.nowDeliveredQty }}</uni-td>
  53. <uni-td align="center"
  54. style="color: orange;font-weight: bold;">{{ item.completedQty }}</uni-td>
  55. </uni-tr>
  56. </uni-table>
  57. </view>
  58. <view v-else class="custom-table">
  59. <scroll-view scroll-y class="scroll-box" style="height: 300px;">
  60. <view class="cell-row">
  61. <view class="cell1">物料</view>
  62. <view class="cell2">数量</view>
  63. <view class="cell3">批号</view>
  64. </view>
  65. <view
  66. v-for="(item, idx) in beforeTableData"
  67. :key="item.uniqueId"
  68. class="swipe-row"
  69. @touchstart="touchStart"
  70. @touchmove="touchMove"
  71. @touchend="touchEnd"
  72. :data-index="idx"
  73. >
  74. <view class="content-area" :style="`transform: translateX(${item.slideX}px)`">
  75. <view class="cell1">{{ item.materialNo }}({{ item.materialName }})</view>
  76. <view class="cell2">{{ item.batchQty }}</view>
  77. <view class="cell3">{{ item.batchNumber }}</view>
  78. </view>
  79. <view class="btn-area" @click="deleteItem(idx)">删除</view>
  80. </view>
  81. </scroll-view>
  82. </view>
  83. <view class="card-list-item"
  84. style="margin: 12px 0;display: grid;grid-template-columns: 1fr 1fr;grid-template-rows: 1fr;gap: 10px;">
  85. <view class="sign-btn" :class="{ disabled: saveDisabled }" @click="handleSave">
  86. <text :style="saveBtnTextStyle">保存</text>
  87. </view>
  88. <view class="sign-btn" :class="{ disabled: submitDisabled }" @click="handleSubmit">
  89. <text :style="submitBtnTextStyle">提交</text>
  90. </view>
  91. <view style="width: 0px;">
  92. <!-- <uni-easyinput ref="signInput" v-model="signText" @focus="handleInputFocus"
  93. :input-border="false" :clearable="false" type="text" @confirm="handleComplete" /> -->
  94. <uni-easyinput ref="signInput" v-model="signText"
  95. :input-border="false" :clearable="false" type="text" @confirm="handleComplete" />
  96. </view>
  97. </view>
  98. <gui-modal ref="modalForm" :custom-class="['gui-bg-white', 'gui-dark-bg-level-3', 'gui-border-radius']"
  99. title="提示">
  100. <template #content>
  101. <view class="gui-padding gui-bg-gray gui-dark-bg-level-2">
  102. <text class="gui-block gui-text-center gui-text gui-color-gray"
  103. style="line-height:100rpx; padding:10rpx;">备料超出,是否拆分?</text>
  104. </view>
  105. </template>
  106. <!-- 利用 flex 布局 可以放置多个自定义按钮哦 -->
  107. <template #btns>
  108. <view class="gui-flex gui-row gui-space-between operation-flex">
  109. <view hover-class="gui-tap" class="modal-btns gui-flex1" @tap="modalForm.close()">
  110. <text class="modal-btns gui-color-gray">取消</text>
  111. </view>
  112. <view class="line" />
  113. <view hover-class="gui-tap" class="modal-btns gui-flex1" @tap="handleSplitMaterial">
  114. <text class="modal-btns gui-primary-color">确认</text>
  115. </view>
  116. </view>
  117. </template>
  118. </gui-modal>
  119. <uni-popup ref="errorTip" type="dialog">
  120. <uni-popup-dialog type="error" cancel-text="关闭" confirm-text="确认" title="提示"
  121. :content="errorTipMessage" @confirm="handleCloseErrorTipsModal"
  122. @close="handleCloseErrorTipsModal" />
  123. </uni-popup>
  124. <!-- FIFO 二次确认弹窗 -->
  125. <uni-popup ref="fifoPopup" type="dialog">
  126. <uni-popup-dialog
  127. title="FIFO 提示"
  128. :content="fifoMsg"
  129. confirm-text="继续"
  130. cancel-text="取消"
  131. @confirm="fifoContinue"
  132. @close="fifoCancel" />
  133. </uni-popup>
  134. </view>
  135. </template>
  136. </gui-page>
  137. </template>
  138. <script>
  139. import {
  140. onReachBottom
  141. } from '@dcloudio/uni-app'
  142. import {
  143. ref,
  144. onMounted,
  145. defineComponent,
  146. onBeforeMount,
  147. computed,
  148. watch,
  149. nextTick
  150. } from 'vue'
  151. export default defineComponent({
  152. setup(options) {
  153. const popup = ref()
  154. const queryParams = ref({
  155. pageSize: 20,
  156. pageNo: 1,
  157. masterId: options?.id ?? '',
  158. wmsProductionWorkOrderBomId: options?.id ?? ''
  159. })
  160. const errorTip = ref('')
  161. const errorTipMessage = ref('')
  162. const easyinput = ref('')
  163. const errorState = ref(0)
  164. const modalForm = ref()
  165. const parentRow = uni.getStorageSync('masterId') ?? {}
  166. const masterId = ref('')
  167. const businessType = ref('')
  168. // const saveId = ref('')
  169. const signInput = ref()
  170. const signText = ref('')
  171. const isLightText = ref('')
  172. const isSubmitLight = ref('')
  173. const scanBatchNumber = ref('')
  174. const isBefore = ref(false)
  175. const tableData = ref([])
  176. const beforeTableData = ref([])
  177. const scanMaterialList = ref([])
  178. const receiveList = ref([])
  179. const currentWarehouseId = ref('')
  180. // 当前处理的数据行
  181. const fdIndex = ref(-1)
  182. // 当前扫描的物料
  183. const currentScanMaterial = ref([])
  184. // 前端临时缓存:已扫物料
  185. const localScannedList = ref([])
  186. // 控制保存按钮能否点击
  187. const saveDisabled = ref(false)
  188. // 控制提交按钮能否点击
  189. const submitDisabled = ref(false)
  190. /* -------------------- 下面 4 行是新增变量 -------------------- */
  191. const fifoPopup = ref(null) // 弹窗实例
  192. const fifoMsg = ref('') // 提示语
  193. let fifoQrCode = '' // 缓存本次二维码
  194. const fifoGo = ref(false) // 用户是否点了“继续”
  195. // 存储fifo_check_order_type字典数据
  196. const fifoCheckOrderTypeDict = ref([])
  197. // 标记businessType是否在fifo_check_order_type字典内
  198. const isBusinessTypeInFifoDict = ref(false)
  199. onBeforeMount(() => {
  200. const parsedData = JSON.parse(parentRow)
  201. masterId.value = parsedData?.id
  202. businessType.value = parsedData?.businessType || '0'
  203. console.log('获取到的businessType:', businessType.value)
  204. // 获取fifo_check_order_type字典数据
  205. getFifoCheckOrderTypeDict()
  206. })
  207. // 获取fifo_check_order_type字典数据
  208. const getFifoCheckOrderTypeDict = function() {
  209. uni.$reqGet('getDictDataPage', {
  210. dictType: 'fifo_check_order_type'
  211. })
  212. .then(({ code, data, msg }) => {
  213. if (code === 0) {
  214. fifoCheckOrderTypeDict.value = data?.list || []
  215. console.log('fifo_check_order_type字典数据:', fifoCheckOrderTypeDict.value)
  216. console.log('fifo_check_order_type字典数据长度:', fifoCheckOrderTypeDict.value.length)
  217. // 判断businessType是否在字典内
  218. checkBusinessTypeInFifoDict()
  219. } else {
  220. console.error('获取fifo_check_order_type字典失败:', msg)
  221. }
  222. })
  223. .catch(error => {
  224. console.error('获取fifo_check_order_type字典异常:', error)
  225. })
  226. }
  227. // 判断businessType是否在fifo_check_order_type字典内
  228. const checkBusinessTypeInFifoDict = function() {
  229. // 使用some方法检查businessType是否存在于字典数据中
  230. isBusinessTypeInFifoDict.value = fifoCheckOrderTypeDict.value.some(item =>
  231. String(item?.value).trim() === String(businessType.value).trim()
  232. )
  233. console.log('businessType是否在fifo_check_order_type字典内:', isBusinessTypeInFifoDict.value)
  234. }
  235. const search = function() {
  236. uni.$reqGet('getScannedOutMatersDetails', { id: masterId.value })
  237. .then(({ code, data, msg }) => {
  238. if (code === 0) {
  239. uni.setStorageSync('ids', JSON.stringify(data?.inoutRequestDetailPDARespVOList?.[0] || {}))
  240. // saveId.value = data?.id
  241. receiveList.value = data
  242. currentWarehouseId.value = data?.warehouseId
  243. tableData.value = data?.inoutRequestDetailPDARespVOList || []
  244. // 1. 先清空本地已扫列表,准备重新填充
  245. localScannedList.value = []
  246. // 2. 将后端返回的已扫物料数据塞进localScannedList
  247. tableData.value.forEach(item => {
  248. if (item.inoutRequestSubdetailList?.length) {
  249. localScannedList.value.push(...item.inoutRequestSubdetailList)
  250. }
  251. })
  252. // 3. 调用flushBeforeTableData(),以localScannedList为准重新生成beforeTableData
  253. flushBeforeTableData()
  254. } else {
  255. uni.showToast({
  256. title: msg,
  257. icon: 'none',
  258. duration: 2000
  259. })
  260. }
  261. })
  262. }
  263. onMounted(() => {
  264. search()
  265. })
  266. const goBack = function() {
  267. if (uni.getStorageSync('masterId')) {
  268. uni.removeStorageSync('masterId')
  269. }
  270. if (uni.getStorageSync('ids')) {
  271. uni.removeStorageSync('ids')
  272. }
  273. uni.$goBack('/pages/workbranch/warehouse/scanInOut/Out/scanOutPage')
  274. }
  275. const handleScanMaterial = async function() {
  276. // #ifdef APP-PLUS
  277. /* 0. 基本校验 */
  278. /* 1. 先调摄像头 */
  279. const mpaasScanModule = uni.requireNativePlugin('Mpaas-Scan-Module');
  280. const ret = await new Promise(resolve =>
  281. mpaasScanModule.mpaasScan(
  282. { scanType: ['qrCode', 'barCode'], hideAlbum: false },
  283. resolve
  284. )
  285. );
  286. if (ret.resp_code !== 1000) return;
  287. const qrCode = ret.resp_result;
  288. /* 2. FIFO 校验 */
  289. const ok = await checkFifo(qrCode);
  290. if (!ok && !fifoGo.value) return;
  291. /* 3. 业务接口 */
  292. uni.$reqGet('scanPrepareMaterial', { qrCode })
  293. .then(({ code, data, msg }) => {
  294. fifoGo.value = false;
  295. if (code !== 0) {
  296. // #ifdef APP-PLUS
  297. plus.device.beep(2);
  298. // #endif
  299. errorTipMessage.value = msg;
  300. errorTip.value.open();
  301. errorState.value = 0;
  302. return;
  303. }
  304. /* ---------- 新增仓库ID校验 ---------- */
  305. if (!validateWarehouseId(data)) {
  306. return; // 仓库ID不一致,终止后续逻辑
  307. }
  308. /* ------------------------------------ */
  309. /* ---------- 新增批号重复校验 ---------- */
  310. const cur = Array.isArray(data) ? data[0] : data;
  311. if (cur && isBatchExist(cur.batchNumber)) {
  312. showBatchRepeatTip();
  313. return;// 直接终止
  314. }
  315. /* ------------------------------------ */
  316. scanBatchNumber.value = qrCode;
  317. // 以下保持你原有逻辑不变
  318. if (Object.prototype.toString.call(data) === '[object Array]') {
  319. fdIndex.value = tableData.value.findIndex(
  320. (item) => item?.materialNo === data[0]?.materialNo
  321. );
  322. }
  323. if (fdIndex.value === -1) {
  324. // #ifdef APP-PLUS
  325. plus.device.beep(2);
  326. // #endif
  327. errorTipMessage.value = '请扫描所需物料的物料条码';
  328. errorTip.value.open();
  329. errorState.value = 0;
  330. return;
  331. }
  332. currentScanMaterial.value = data[0] ?? [];
  333. const row = tableData.value[fdIndex.value];
  334. if (row.nowDeliveredQty > row.completedQty) {
  335. /* ===== 前端暂存逻辑 ===== */
  336. row.completedQty += currentScanMaterial.value.receiptQty || 1;
  337. localScannedList.value.push({
  338. ...currentScanMaterial.value,
  339. batchQty: currentScanMaterial.value.receiptQty || 1,
  340. supplierName: currentScanMaterial.value.supplierName || '',
  341. });
  342. flushBeforeTableData();
  343. setInputFocus();
  344. } else {
  345. // #ifdef APP-PLUS
  346. plus.device.beep(2);
  347. // #endif
  348. errorTipMessage.value = '已备数量已满,无需再扫';
  349. errorTip.value.open();
  350. errorState.value = 0;
  351. }
  352. }).catch(() => {
  353. // 异常也要复位
  354. fifoGo.value = false;
  355. });
  356. // #endif
  357. };
  358. const handleToNavigate = function() {
  359. uni.navigateTo({
  360. url: '/pages/workbranch/warehouse/production/materialIssuance'
  361. })
  362. }
  363. const handleComplete = function(e) {
  364. // #ifdef APP-PLUS
  365. // 扫描员工工号
  366. uni.$reqPost('scanPrepareMaterialSign', {
  367. encodedEmployeeId: e,
  368. id: masterId.value
  369. })
  370. .then(({
  371. code,
  372. data,
  373. msg
  374. }) => {
  375. if (code === 0) {
  376. uni.showToast({
  377. title: '扫码成功',
  378. icon: 'none',
  379. duration: 2000
  380. })
  381. setTimeout(() => {
  382. goBack();
  383. }, 500)
  384. } else {
  385. // #ifdef APP-PLUS
  386. plus.device.beep(2)
  387. // #endif
  388. errorTipMessage.value = msg
  389. errorTip.value.open()
  390. errorState.value = -1
  391. }
  392. isLightText.value = false
  393. signText.value = ''
  394. })
  395. // #endif
  396. }
  397. // 物料拆分
  398. const handleSplitMaterial = function() {
  399. uni.$reqPost('prepareSplit', {
  400. prepareId: masterId.value,
  401. id: scanBatchNumber.value,
  402. inQty: currentScanMaterial.value?.receiptQty,
  403. splitQty: tableData.value[fdIndex.value].completedQty - currentScanMaterial.value?.receiptQty
  404. })
  405. .then(res => {
  406. search()
  407. modalForm.value.close()
  408. if (res.code === 0) {
  409. // 调取打印机拆分物料后打印标签
  410. uni.showToast({
  411. title: '拆分完毕',
  412. icon: 'none',
  413. duration: 2000
  414. })
  415. } else {
  416. // #ifdef APP-PLUS
  417. plus.device.beep(2)
  418. // #endif
  419. errorTipMessage.value = res.msg
  420. errorTip.value.open()
  421. errorState.value = 0
  422. }
  423. })
  424. }
  425. const handleKeydown = async function (e) {
  426. const qrCode = e;
  427. /* ===== 新增 FIFO 校验 ===== */
  428. const ok = await checkFifo(qrCode);
  429. if (!ok && !fifoGo.value) return;
  430. /* ========================= */
  431. uni.$reqGet('scanPrepareMaterial', { qrCode })
  432. .then(async ({ code, data, msg }) => {
  433. scanBatchNumber.value = qrCode;
  434. fifoGo.value = false;
  435. if (code !== 0) {
  436. // #ifdef APP-PLUS
  437. plus.device.beep(2);
  438. // #endif
  439. errorTipMessage.value = msg;
  440. errorTip.value.open();
  441. errorState.value = 0;
  442. return;
  443. }
  444. /* ---------- 新增仓库ID校验 ---------- */
  445. if (!validateWarehouseId(data)) {
  446. return; // 仓库ID不一致,终止后续逻辑
  447. }
  448. /* ------------------------------------ */
  449. /* ---------- 新增批号重复校验 ---------- */
  450. const cur = Array.isArray(data) ? data[0] : data;
  451. if (cur && isBatchExist(cur.batchNumber)) {
  452. showBatchRepeatTip();
  453. return;// 直接终止
  454. }
  455. /* ------------------------------------ */
  456. if (Object.prototype.toString.call(data) === '[object Array]') {
  457. fdIndex.value = tableData.value.findIndex(
  458. (item) => item?.materialNo === data[0]?.materialNo
  459. );
  460. }
  461. if (fdIndex.value === -1) {
  462. // #ifdef APP-PLUS
  463. plus.device.beep(2);
  464. // #endif
  465. errorTipMessage.value = '请扫描所需物料的物料条码';
  466. errorTip.value.open();
  467. errorState.value = 0;
  468. return;
  469. }
  470. currentScanMaterial.value = data[0] ?? [];
  471. const row = tableData.value[fdIndex.value];
  472. if (row.nowDeliveredQty > row.completedQty) {
  473. /* ===== 前端暂存逻辑 ===== */
  474. row.completedQty += currentScanMaterial.value.receiptQty || 1;
  475. localScannedList.value.push({
  476. ...currentScanMaterial.value,
  477. batchQty: currentScanMaterial.value.receiptQty || 1,
  478. supplierName: currentScanMaterial.value.supplierName || '',
  479. });
  480. flushBeforeTableData();
  481. setInputFocus();
  482. } else {
  483. // #ifdef APP-PLUS
  484. plus.device.beep(2);
  485. // #endif
  486. errorTipMessage.value = '已备数量已满,无需再扫';
  487. errorTip.value.open();
  488. errorState.value = 0;
  489. }
  490. }).catch(() => {
  491. // 异常也要复位
  492. fifoGo.value = false
  493. });
  494. };
  495. const handleToDetails = function(ret) {
  496. uni.navigateTo({
  497. url: '/pages/workbranch/warehouse/scanInOut/Out/materialsDetail'
  498. })
  499. uni.setStorageSync('mixMaterialDetail', JSON.stringify(ret))
  500. }
  501. // 设置高亮
  502. const handleBtnLight = function() {
  503. // #ifdef APP-PLUS
  504. signInput.value.onBlur()
  505. isLightText.value = true
  506. signInput.value.onFocus()
  507. // #endif
  508. }
  509. const setInputFocus = function() {
  510. scanBatchNumber.value = ''
  511. easyinput.value.onBlur()
  512. easyinput.value.onFocus()
  513. }
  514. // 相同物料+相同批号才合并数量,否则新增一行
  515. const flushBeforeTableData = function() {
  516. const map = new Map()
  517. localScannedList.value.forEach(it => {
  518. // 用“物料编码+批号”当唯一键
  519. const key = `${it.materialNo}__${it.batchNumber}`
  520. if (map.has(key)) {
  521. map.get(key).batchQty += it.batchQty // 同批号累加
  522. } else {
  523. map.set(key, { ...it })// 新批号新开一行
  524. }
  525. })
  526. // 为每个项目添加唯一ID和滑动属性
  527. beforeTableData.value = Array.from(map.values()).map((v, i) => ({
  528. ...v,
  529. uniqueId: `${v.materialNo}_${v.batchNumber}_${i}`,
  530. slideX: 0
  531. }))
  532. }
  533. /* ========= 左滑删除 ========= */
  534. const touchStart = (e) => {
  535. const idx = e.currentTarget.dataset.index
  536. beforeTableData.value.forEach((v, i) => {
  537. if (i !== idx) v.slideX = 0 // 其余归位
  538. })
  539. beforeTableData.value[idx].startX = e.touches[0].pageX
  540. beforeTableData.value[idx].slideX = beforeTableData.value[idx].slideX || 0
  541. }
  542. const touchMove = (e) => {
  543. const idx = e.currentTarget.dataset.index
  544. const row = beforeTableData.value[idx]
  545. const delta = e.touches[0].pageX - row.startX
  546. if (delta < 0) { // 只允许左滑
  547. row.slideX = Math.max(delta, -70) // 70 = 按钮宽度
  548. } else {
  549. row.slideX = Math.min(delta, 0)
  550. }
  551. }
  552. const touchEnd = (e) => {
  553. const idx = e.currentTarget.dataset.index
  554. const row = beforeTableData.value[idx]
  555. row.slideX = row.slideX <= -35 ? -70 : 0 // 过半则露出,否则收回
  556. }
  557. /**
  558. * 删除一条已扫物料
  559. * @param {number} idx 在 beforeTableData 中的下标
  560. */
  561. const deleteItem = async (idx) => {
  562. const target = beforeTableData.value[idx];
  563. /* 1. 有 id 就先调接口删后端 */
  564. if (target.id) {
  565. try {
  566. const { code, msg } = await uni.$reqDelete('deleteScanOutMaterial', { id: target.id });
  567. if (code !== 0) {
  568. // 接口明确返回失败,提示用户并终止后续逻辑
  569. errorTipMessage.value = msg || '后端删除失败';
  570. errorTip.value.open();
  571. return;
  572. }
  573. } catch (err) {
  574. // 网络或其它异常
  575. errorTipMessage.value = err.errMsg || '网络异常,删除失败';
  576. errorTip.value.open();
  577. return;
  578. }
  579. }
  580. /* 2. 无论有没有 id、接口成不成功,只要走到这里就删本地 */
  581. // 2.1 从 localScannedList 里删掉对应项(同物料+同批号)
  582. const key = `${target.materialNo}__${target.batchNumber}`;
  583. const listIdx = localScannedList.value.findIndex(
  584. v => `${v.materialNo}__${v.batchNumber}` === key
  585. );
  586. if (listIdx > -1) localScannedList.value.splice(listIdx, 1);
  587. // 2.2 重新合并生成 beforeTableData
  588. flushBeforeTableData();
  589. // 2.3 把对应物料的“已备数”回退
  590. const row = tableData.value.find(r => r.materialNo === target.materialNo);
  591. if (row) row.completedQty -= target.batchQty;
  592. }
  593. // 关闭错误信息弹窗
  594. const handleCloseErrorTipsModal = async function() {
  595. errorTip.value.close()
  596. if (errorState.value === 0) {
  597. await setInputFocus()
  598. }
  599. }
  600. /* ===================== 公共校验函数 ===================== */
  601. // 仓库ID校验函数
  602. const validateWarehouseId = (scanData) => {
  603. const cur = Array.isArray(scanData) ? scanData[0] : scanData;
  604. if (cur && cur.warehouseId && currentWarehouseId.value && cur.warehouseId !== currentWarehouseId.value) {
  605. // #ifdef APP-PLUS
  606. plus.device.beep(2);
  607. // #endif
  608. errorTipMessage.value = `物料仓库(${cur.warehouseId})与当前仓库(${currentWarehouseId.value})不一致`;
  609. errorTip.value.open();
  610. errorState.value = 0;
  611. return false;
  612. }
  613. return true;
  614. };
  615. // 仅扫描本地缓存池,不再读 beforeTableData
  616. const isBatchExist = (batchNo) =>
  617. beforeTableData.value.some((it) => it.batchNumber === batchNo);
  618. const showBatchRepeatTip = () => {
  619. // #ifdef APP-PLUS
  620. plus.device.beep(2);
  621. // #endif
  622. errorTipMessage.value = '该批号已存在,请勿重复扫描';
  623. errorTip.value.open();
  624. errorState.value = 0;
  625. };
  626. // 禁用软键盘
  627. const handleInputFocus = function() {
  628. setTimeout(() => {
  629. uni.hideKeyboard()
  630. }, 100)
  631. }
  632. // 把 beforeTableData 追加到 tableData 对应物料的 inoutRequestSubdetailList
  633. const mergeBeforeIntoTable = () => {
  634. // 创建新数组存储合并结果
  635. const mergedTableData = JSON.parse(JSON.stringify(tableData.value))
  636. // 先建索引
  637. const map = {}
  638. mergedTableData.forEach(row => {
  639. if (!row.inoutRequestSubdetailList) row.inoutRequestSubdetailList = []
  640. map[row.materialNo] = row
  641. })
  642. // 只追加,不新增
  643. beforeTableData.value.forEach(item => {
  644. const target = map[item.materialNo]
  645. if (target) {
  646. target.inoutRequestSubdetailList.push({ ...item }) // 整条记录丢进去
  647. }
  648. // 不存在就跳过
  649. })
  650. return mergedTableData
  651. }
  652. // 保存按钮点击事件
  653. const handleSave = async function() {
  654. if (saveDisabled.value) return
  655. saveDisabled.value = true
  656. try {
  657. // 保存逻辑实现
  658. isLightText.value = true
  659. // 先合并数据,获取合并后的新数组
  660. const mergedData = mergeBeforeIntoTable()
  661. // 调用保存API,使用合并后的新数组作为参数
  662. await uni.$reqPost('saveScannedOutMaterials', {
  663. id: receiveList.value?.id,
  664. requestNo: receiveList.value?.requestNo,
  665. requestType: receiveList.value?.requestType,
  666. businessType: receiveList.value?.businessType,
  667. businessSubType: receiveList.value?.businessSubType,
  668. status: receiveList.value?.status,
  669. priority: receiveList.value?.priority,
  670. inoutRequestDetailPDASaveReqVOList: mergedData
  671. })
  672. .then(({ code, data, msg }) => {
  673. if (code === 0) {
  674. uni.showModal({
  675. title: '提示',
  676. content: '保存成功',
  677. showCancel: false
  678. })
  679. search();
  680. } else {
  681. // #ifdef APP-PLUS
  682. plus.device.beep(2)
  683. // #endif
  684. errorTipMessage.value = msg
  685. errorTip.value.open()
  686. errorState.value = 0
  687. }
  688. })
  689. } finally {
  690. saveDisabled.value = false
  691. }
  692. }
  693. // 提交按钮点击事件
  694. const handleSubmit = async function() {
  695. if (submitDisabled.value) return
  696. submitDisabled.value = true
  697. try {
  698. // 提交逻辑实现
  699. isSubmitLight.value = true
  700. // 先合并数据,获取合并后的新数组
  701. const mergedData = mergeBeforeIntoTable()
  702. // 调用提交API,使用合并后的新数组作为参数
  703. await uni.$reqPut('submitScannedOutMaterials', {
  704. id: receiveList.value?.id,
  705. requestNo: receiveList.value?.requestNo,
  706. requestType: receiveList.value?.requestType,
  707. businessType: receiveList.value?.businessType,
  708. businessSubType: receiveList.value?.businessSubType,
  709. status: receiveList.value?.status,
  710. priority: receiveList.value?.priority,
  711. inoutRequestDetailPDASaveReqVOList: mergedData
  712. })
  713. .then(({ code, data, msg }) => {
  714. if (code === 0) {
  715. uni.showModal({
  716. title: '提示',
  717. content: '提交成功',
  718. showCancel: false
  719. })
  720. search();
  721. // 保存成功后可以跳转到其他页面或执行其他操作
  722. // goBack()
  723. } else {
  724. // #ifdef APP-PLUS
  725. plus.device.beep(2)
  726. // #endif
  727. errorTipMessage.value = msg
  728. errorTip.value.open()
  729. errorState.value = 0
  730. }
  731. })
  732. } finally {
  733. submitDisabled.value = false
  734. }
  735. }
  736. // uniapp移动端触底事件
  737. onReachBottom(() => {
  738. queryParams.value.pageNo += 1
  739. uni.$reqGet('getPrepareMaterialList', queryParams.value)
  740. .then(({
  741. data
  742. }) => {
  743. Array.prototype.push.call(scanMaterialList.value, ...data?.list ?? [])
  744. })
  745. })
  746. // 保存按钮文字样式
  747. const saveBtnTextStyle = computed(() => ({
  748. fontSize: '14px',
  749. fontWeight: 'bold',
  750. color: saveDisabled.value ? '#ccc' : (isLightText.value ? 'rgba(0,160,233,1)' : '')
  751. }))
  752. // 提交按钮文字样式
  753. const submitBtnTextStyle = computed(() => ({
  754. fontSize: '14px',
  755. fontWeight: 'bold',
  756. color: submitDisabled.value ? '#ccc' : (isSubmitLight.value ? 'rgba(0,160,233,1)' : '')
  757. }))
  758. // 移除status监听,提交按钮和保存按钮均始终可用
  759. // watch(() => receiveList.value?.status, (newStatus) => {
  760. // submitDisabled.value = newStatus !== 0
  761. // })
  762. /* -------------------- 新增 3 个方法 -------------------- */
  763. // 真正继续扫码(关闭弹窗并置标志)
  764. const fifoContinue = () => {
  765. fifoPopup.value.close()
  766. fifoGo.value = true
  767. // 用下一个 tick 把老流程续起来
  768. nextTick(() => {
  769. if (fifoQrCode) handleKeydown(fifoQrCode)
  770. })
  771. }
  772. const fifoCancel = () => { fifoPopup.value.close() } // 什么都不做
  773. // 统一 FIFO 校验入口
  774. const checkFifo = (qrCode) => {
  775. // 用户已点“继续”,直接放行
  776. if (fifoGo.value) return Promise.resolve(true)
  777. if (!isBusinessTypeInFifoDict.value) return Promise.resolve(true)
  778. return uni.$reqPost('fifoCheck', { qrCode }).then(({ code, data, msg }) => {
  779. if (code !== 0) {
  780. errorTipMessage.value = msg || 'FIFO 校验异常'
  781. errorTip.value.open()
  782. return false
  783. }
  784. if (data === null) return true
  785. fifoMsg.value = `当前条码(${qrCode})不是库存中生产日期最早的物料条码(${data}),是否继续发料?`
  786. fifoQrCode = qrCode
  787. fifoPopup.value.open()
  788. return false
  789. })
  790. }
  791. return {
  792. goBack,
  793. popup,
  794. signText,
  795. isBefore,
  796. tableData,
  797. beforeTableData,
  798. signInput,
  799. modalForm,
  800. easyinput,
  801. errorTip,
  802. errorTipMessage,
  803. handleInputFocus,
  804. isLightText,
  805. isSubmitLight,
  806. handleBtnLight,
  807. handleKeydown,
  808. scanBatchNumber,
  809. handleScanMaterial,
  810. handleToNavigate,
  811. saveDisabled,
  812. submitDisabled,
  813. saveBtnTextStyle,
  814. submitBtnTextStyle,
  815. scanMaterialList,
  816. handleComplete,
  817. handleSplitMaterial,
  818. handleToDetails,
  819. handleCloseErrorTipsModal,
  820. handleSubmit,
  821. handleSave,
  822. // 左滑删除相关
  823. touchStart,
  824. touchMove,
  825. touchEnd,
  826. deleteItem,
  827. // FIFO相关
  828. fifoPopup,
  829. fifoMsg,
  830. fifoContinue,
  831. fifoCancel
  832. }
  833. }
  834. })
  835. </script>
  836. <style lang="scss" scoped>
  837. .gui-header-leader-btns {
  838. color: black;
  839. font-size: 24px !important;
  840. margin-left: 24rpx;
  841. }
  842. .list-content {
  843. margin-top: 80px;
  844. background-color: #edeeee;
  845. }
  846. .card-list-flexbox {
  847. display: flex;
  848. flex-direction: row;
  849. align-items: center;
  850. flex-wrap: wrap;
  851. margin: 3px 2px;
  852. .card-list-item {
  853. width: 750rpx;
  854. height: 40px;
  855. margin: 2rpx 0;
  856. display: flex;
  857. flex-direction: row;
  858. align-items: center;
  859. justify-content: space-between;
  860. background-color: #fff;
  861. uni-text {
  862. font-size: 14px;
  863. height: 50rpx;
  864. text-align: left;
  865. padding: 0 12px;
  866. display: flex;
  867. flex-direction: row;
  868. align-items: center;
  869. }
  870. .text-1 {
  871. flex: 1;
  872. height: 40px;
  873. justify-content: flex-start;
  874. }
  875. .text-2 {
  876. flex: 3;
  877. height: 40px;
  878. justify-content: flex-end;
  879. margin-right: 4px;
  880. padding: 2px 6px;
  881. }
  882. }
  883. }
  884. .card-list-flexbox:nth-of-type(2) {
  885. margin-top: 48px;
  886. }
  887. .fixedTop {
  888. bottom: 0 !important;
  889. top: 3.25rem !important;
  890. }
  891. .popup-content {
  892. height: 75vh;
  893. overflow-y: scroll;
  894. background-color: #edeeee;
  895. }
  896. .font-icons {
  897. width: 40px;
  898. font-size: 20px;
  899. }
  900. .scan {
  901. height: 45px;
  902. width: calc(100% - 48px);
  903. margin: 12px;
  904. padding: 0 12px;
  905. display: flex;
  906. justify-content: space-between;
  907. align-items: center;
  908. border-radius: 6px;
  909. background-color: white;
  910. .scan-card {
  911. width: 100%;
  912. display: grid;
  913. grid-template-rows: 1fr;
  914. grid-template-columns: 7fr 2fr;
  915. align-items: center;
  916. input {
  917. height: 35px;
  918. line-height: 35px;
  919. }
  920. text {
  921. width: 100%;
  922. text-align: right;
  923. }
  924. }
  925. }
  926. .custom-table {
  927. height: calc(100vh - 265px);
  928. min-height: 230px;
  929. margin: 5px 0;
  930. // min-height: 600px;
  931. overflow-y: scroll;
  932. }
  933. .modal-btns {
  934. height: 100rpx;
  935. line-height: 100rpx;
  936. display: flex;
  937. justify-content: center;
  938. align-items: center;
  939. }
  940. .line {
  941. margin-top: 10rpx;
  942. height: 80rpx;
  943. width: 1rpx;
  944. background-color: #dcdcdc;
  945. }
  946. .tabs {
  947. width: 100%;
  948. height: 45px;
  949. display: flex;
  950. align-items: flex-end;
  951. padding: 0 2px;
  952. background-color: white;
  953. .tabs-list {
  954. border-radius: 3px;
  955. overflow: hidden;
  956. }
  957. .tabs-item {
  958. display: inline-block;
  959. width: 72px;
  960. height: 30px;
  961. line-height: 30px;
  962. padding: 0 8px;
  963. font-size: 14px;
  964. font-weight: bold;
  965. text-align: center;
  966. color: black;
  967. border-bottom: 1.5px dashed #00a0e9;
  968. transition: all .5s ease-in-out;
  969. }
  970. .tabs-item-active {
  971. position: relative;
  972. display: inline-block;
  973. width: 72px;
  974. height: 30px;
  975. line-height: 30px;
  976. padding: 0 8px;
  977. font-size: 14px;
  978. font-weight: bold;
  979. text-align: center;
  980. color: white;
  981. border-left: 1px solid #00a0e9;
  982. border-top: 1px solid #00a0e9;
  983. border-right: 1px solid #00a0e9;
  984. border-bottom: 1.5px solid #00a0e9;
  985. border-radius: 5px 5px 0 0;
  986. animation: .3s linear show;
  987. background-color: #00a0e9;
  988. }
  989. .tabs-item-active::before {
  990. content: '';
  991. position: absolute;
  992. left: -10px;
  993. bottom: 0;
  994. width: 10px;
  995. height: 10px;
  996. background: radial-gradient(circle at 0% 25%, transparent 10px, #00a0e9 0)
  997. }
  998. .tabs-item-active::after {
  999. content: '';
  1000. position: absolute;
  1001. right: -10px;
  1002. bottom: 0;
  1003. width: 10px;
  1004. height: 10px;
  1005. background: radial-gradient(circle at 100% 25%, transparent 10px, #00a0e9 0)
  1006. }
  1007. }
  1008. @keyframes show {
  1009. from {
  1010. transform: translateY(5%);
  1011. }
  1012. to {
  1013. transform: translateY(0%);
  1014. }
  1015. }
  1016. .sign-btn {
  1017. display: flex;
  1018. align-items: center;
  1019. justify-content: center;
  1020. margin: 0 8px;
  1021. border: 1px solid #999999;
  1022. background-color: white;
  1023. border-radius: 6px;
  1024. }
  1025. .sign-btn.disabled {
  1026. border-color: #ccc;
  1027. background-color: #f5f5f5;
  1028. pointer-events: none;
  1029. }
  1030. /* 左滑删除样式 */
  1031. .scroll-box{
  1032. height:100%; /* 继承父级高度 */
  1033. }
  1034. .cell-row {
  1035. display: flex;
  1036. background-color: #f5f7fa;
  1037. font-weight: bold;
  1038. border-radius: 4px;
  1039. margin-bottom: 5px;
  1040. }
  1041. .swipe-row{
  1042. position:relative;
  1043. width:100%;
  1044. height:44px;
  1045. overflow:hidden;
  1046. border-bottom:1px solid #eee;
  1047. margin-bottom: 5px;
  1048. border-radius: 4px;
  1049. }
  1050. .content-area{
  1051. position:absolute;
  1052. left:0;
  1053. top:0;
  1054. right:0;
  1055. bottom:0;
  1056. z-index:2;
  1057. background:#fff;
  1058. display:flex;
  1059. align-items:center;
  1060. transition: transform .25s;
  1061. border-radius: 4px;
  1062. }
  1063. .cell1{
  1064. flex:2;
  1065. text-align:center;
  1066. font-size:14px;
  1067. color:#333;
  1068. height: 44px;
  1069. line-height: 44px;
  1070. }
  1071. .cell2{
  1072. flex:1;
  1073. text-align:center;
  1074. font-size:14px;
  1075. color:#333;
  1076. height: 44px;
  1077. line-height: 44px;
  1078. }
  1079. .cell3{
  1080. flex:2;
  1081. text-align:center;
  1082. font-size:14px;
  1083. color:#333;
  1084. height: 44px;
  1085. line-height: 44px;
  1086. }
  1087. .btn-area{
  1088. position:absolute;
  1089. right:0;
  1090. top:0;
  1091. width:70px;
  1092. height:100%;
  1093. background:#e54d42;
  1094. color:#fff;
  1095. display:flex;
  1096. align-items:center;
  1097. justify-content:center;
  1098. font-size:14px;
  1099. z-index:1;
  1100. border-radius: 4px;
  1101. }
  1102. </style>