index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  1. <template>
  2. <view class="container">
  3. <view class="example">
  4. <!-- 基础表单校验 -->
  5. <uni-forms ref="valiForm" label-align="right" :rules="rules" :modelValue="valiFormData">
  6. <uni-forms-item label="单号" :required="true" name="orderNum">
  7. <uni-easyinput v-model="valiFormData.orderNum" placeholder="请输入单号" suffixIcon="scan" :focus="focusType" @iconClick="scan" />
  8. </uni-forms-item>
  9. <uni-forms-item label="仓位编码" name="space_code">
  10. <uni-data-select v-model="valiFormData.space_code" :localdata="spaces" placeholder="请选择仓位编码"></uni-data-select>
  11. </uni-forms-item>
  12. <uni-forms-item label="图片" name="images">
  13. <view class="upload-container">
  14. <view class="preview">
  15. <view v-for="(img, index) in images" :key="index" class="image-container">
  16. <image :src="img.path" class="preview-image" mode="aspectFill" @click="openPreview(img.path)" />
  17. <progress :percent="img.progress || 0" :activeColor="Number(img.progress || 0) === 100 ? '#00ff00' : '#10AEFF'" stroke-width="3" />
  18. <text v-if="img.status === 'success'">1</text>
  19. <uni-icons class="delete-icon" @click="deleteImage(index)" type="clear" size="20" color="red" />
  20. </view>
  21. <view v-if="images.length < maxImages" class="choose-image-container" @click="chooseImage">
  22. <uni-icons type="plusempty" size="40" color="#ccc" />
  23. </view>
  24. </view>
  25. <text style="padding-top: 8px">最多选择6张图片</text>
  26. <!-- 放大预览 -->
  27. <view v-if="previewImage" class="preview-modal" @click="closePreview">
  28. <image :src="previewImage" class="preview-large" mode="aspectFit" @click.stop="closePreview" />
  29. </view>
  30. </view>
  31. </uni-forms-item>
  32. </uni-forms>
  33. <view class="button-group">
  34. <button type="info" @click="reset">重置</button>
  35. <button type="primary" @click="onsubmit" :loading="loading">
  36. <uni-icons v-if="!loading" type="checkmarkempty" size="18" color="white"></uni-icons>
  37. 提交
  38. </button>
  39. </view>
  40. </view>
  41. <view class="history">
  42. <view class="item" v-for="(item, i) in historyList.slice(0, 5)" :key="i">
  43. <text class="code" :style="{ color: item.status ? 'green' : '#666' }">
  44. {{ item.orderNum }}
  45. <text v-if="item.batch_text">批次号: {{ item.batch_text }}</text>
  46. <text v-if="item.space">仓位编码: {{ item.space }}</text>
  47. {{ item.type }}
  48. </text>
  49. <uni-icons v-if="item.status" type="checkmarkempty" class="status" size="16" color="green"></uni-icons>
  50. <text class="status fail" v-else>F</text>
  51. <text style="margin-left: 10rpx; font-weight: 300">
  52. {{ '\r\n' + item.createTime }}
  53. </text>
  54. </view>
  55. </view>
  56. <uni-popup ref="message" type="message">
  57. <uni-popup-message :type="messageType" :message="messageText" :duration="2000"></uni-popup-message>
  58. </uni-popup>
  59. <uni-popup ref="printerDialog" type="dialog" :is-mask-click="false">
  60. <view style="width: 90%; margin: 0 auto; min-height: 250px; background-color: #fff; border-radius: 5px">
  61. <view class="" style="font-size: 20px; border-bottom: 1px solid #e1e1e1; padding: 15px 10px">
  62. <view>
  63. <view style="margin-bottom: 20px">
  64. <text>打印尾程面单</text>
  65. </view>
  66. <view>
  67. <view v-if="printerList">
  68. <view>
  69. <uni-data-checkbox
  70. multiple
  71. v-model="sendToPeinter"
  72. :localdata="[
  73. {
  74. text: '发送到标签打印机打印',
  75. value: 1
  76. }
  77. ]"
  78. @change="sendToPeinterFun"
  79. >
  80. >
  81. </uni-data-checkbox>
  82. <uni-data-checkbox :disabled="sendToPeinter.length === 0" v-model="selectPrinter" :localdata="printers"></uni-data-checkbox>
  83. </view>
  84. </view>
  85. </view>
  86. </view>
  87. <view style="text-align: center; position: absolute; bottom: 10px; width: 90%; display: flex; margin: 0 auto; left: 0; right: 0">
  88. <button @click="close" style="width: 35%">关闭</button>
  89. <button
  90. @click="printConfirm"
  91. :hover-stay-time="500"
  92. :loading="subLoading"
  93. :disabled="sendToPeinter.length === 0 || subLoading"
  94. class="my-bt-bg"
  95. style="width: 35%"
  96. >
  97. 打印
  98. </button>
  99. </view>
  100. </view>
  101. </view>
  102. </uni-popup>
  103. </view>
  104. </template>
  105. <script setup lang="ts">
  106. import dayjs from 'dayjs';
  107. import permision from '@/common/permission.js';
  108. import { ref, reactive, nextTick, computed } from 'vue';
  109. import { onShow, onHide, onNavigationBarButtonTap } from '@dcloudio/uni-app';
  110. import { getAliyunOssSignatureUrl, instockScanURL, printWaybillLabelURL, getWarehouseSpaceURL, getBindParamsURL, getPrinterListURL } from '@/utils/api.js';
  111. import { uuid } from '@/utils/random.ts';
  112. const message = ref();
  113. const valiForm = ref();
  114. const printerDialog = ref();
  115. const token = ref(null);
  116. const user = ref(null);
  117. const loading = ref(false);
  118. const images = ref([]);
  119. const messageType = ref('');
  120. const messageText = ref('');
  121. const batchOptions = ref([] as any);
  122. const spaces = ref([] as any);
  123. const previewImage = ref(null);
  124. const maxImages = ref(6); // 最大上传图片数量
  125. const printerList = ref({} as any);
  126. const historyList = ref([] as any);
  127. const printers = ref([] as any);
  128. const sendToPeinter = ref([1]);
  129. const selectPrinter = ref(0);
  130. const subLoading = ref(false);
  131. const focusType = ref(true);
  132. const result = ref();
  133. // 校验表单数据
  134. const valiFormData = reactive({
  135. images: [],
  136. orderNum: '',
  137. batch_number: '',
  138. space_code: '',
  139. weight: '',
  140. search_pkg: true,
  141. typing: true,
  142. search_order: true,
  143. search_order_choice: 1 as any
  144. });
  145. const rules = computed(() => {
  146. return {
  147. orderNum: {
  148. rules: [
  149. {
  150. required: true,
  151. errorMessage: '单号不能为空'
  152. }
  153. ]
  154. },
  155. weight: {
  156. rules: [
  157. {
  158. required: true,
  159. errorMessage: '重量不能为空'
  160. },
  161. {
  162. format: 'number',
  163. errorMessage: '重量只能输入数字'
  164. }
  165. ]
  166. }
  167. };
  168. });
  169. const spaceRes = computed(() => {
  170. try {
  171. const res = spaces.value.find((item: any) => item.value === valiFormData.space_code) as any;
  172. // console.log(res);
  173. return res?.text;
  174. } catch (e) {
  175. console.log(e);
  176. return '';
  177. }
  178. });
  179. onShow(() => {
  180. focusType.value = true;
  181. loading.value = false;
  182. token.value = uni.getStorageSync('token');
  183. user.value = uni.getStorageSync('user');
  184. getWarehouseSpace();
  185. getBindParams();
  186. getPrinterList();
  187. });
  188. onHide(() => {
  189. focusType.value = false;
  190. loading.value = false;
  191. });
  192. onNavigationBarButtonTap((event: any) => {
  193. if (event.index === 0) {
  194. uni.navigateTo({
  195. url: '/pages/inbound/instockLog'
  196. });
  197. }
  198. });
  199. const getWarehouseSpace = () => {
  200. uni.request({
  201. url: getWarehouseSpaceURL,
  202. method: 'POST',
  203. header: {
  204. batoken: token.value
  205. },
  206. data: {
  207. code: ''
  208. },
  209. success: (res: any) => {
  210. if (res.data.code === 1) {
  211. spaces.value = res.data.data.spaces.map((item) => {
  212. return {
  213. text: item.name,
  214. value: item.code
  215. };
  216. });
  217. }
  218. }
  219. });
  220. };
  221. const getBindParams = () => {
  222. uni.request({
  223. url: getBindParamsURL,
  224. method: 'GET',
  225. header: {
  226. batoken: token.value
  227. },
  228. success: (res: any) => {
  229. if (res.data.code === 1) {
  230. batchOptions.value = res.data.data.batch_number.map((item) => {
  231. return {
  232. text: item.name,
  233. value: item.id
  234. };
  235. });
  236. }
  237. },
  238. fail(e) {
  239. console.log('fail--', e);
  240. }
  241. });
  242. };
  243. const getPrinterList = () => {
  244. uni.request({
  245. url: getPrinterListURL,
  246. method: 'GET',
  247. header: {
  248. batoken: token.value
  249. },
  250. success: (res: any) => {
  251. if (res.data.code === 1) {
  252. printerList.value = res.data.data.printers;
  253. printers.value = Object.values(printerList.value).map((item: any) => {
  254. return {
  255. text: item.name,
  256. value: item.value
  257. };
  258. });
  259. }
  260. },
  261. fail(e) {
  262. console.log('fail--', e);
  263. }
  264. });
  265. };
  266. const reset = () => {
  267. loading.value = false;
  268. focusType.value = false;
  269. images.value = [];
  270. valiFormData.orderNum = '';
  271. // space_code: '',
  272. valiFormData.batch_number = '';
  273. valiFormData.weight = '';
  274. valiFormData.search_pkg = true;
  275. valiFormData.typing = true;
  276. valiFormData.search_order = true;
  277. nextTick(() => {
  278. focusType.value = true;
  279. });
  280. };
  281. const scan = async () => {
  282. // #ifdef APP-PLUS
  283. let status = await checkPermission();
  284. if (status !== 1) {
  285. return;
  286. }
  287. // #endif
  288. uni.scanCode({
  289. success: (res: any) => {
  290. result.value = res.result;
  291. valiFormData.orderNum = res.result;
  292. // this.onsubmit();
  293. },
  294. fail: (err) => {
  295. // 需要注意的是小程序扫码不需要申请相机权限
  296. }
  297. });
  298. };
  299. // #ifdef APP-PLUS
  300. const checkPermission = async () => {
  301. let status = permision.isIOS ? await permision.requestIOS('camera') : await permision.requestAndroid('android.permission.CAMERA');
  302. if (status === null || status === 1) {
  303. status = 1;
  304. } else {
  305. uni.showModal({
  306. content: 'Camera permission required',
  307. confirmText: 'Setting',
  308. success: function (res) {
  309. if (res.confirm) {
  310. permision.gotoAppSetting();
  311. }
  312. }
  313. });
  314. }
  315. return status;
  316. };
  317. // #endif
  318. let st;
  319. const warehouseScan = async () => {
  320. st && clearTimeout(st);
  321. instockScan();
  322. };
  323. const instockScan = () => {
  324. const images = getImages();
  325. console.log('images--', images);
  326. let allImgVerify = true;
  327. for (var i = 0; i < images.length; i++) {
  328. if (!images[i].savePath) {
  329. allImgVerify = false;
  330. }
  331. }
  332. if (!allImgVerify) {
  333. messageType.value = 'error';
  334. messageText.value = '图片还没上传完毕,请稍后...';
  335. message.value.open();
  336. loading.value = false;
  337. return;
  338. }
  339. uni.request({
  340. url: instockScanURL,
  341. method: 'POST',
  342. header: {
  343. batoken: token.value
  344. },
  345. data: {
  346. order_no: valiFormData.orderNum,
  347. space_code: valiFormData.space_code,
  348. images
  349. },
  350. success: (res: any) => {
  351. loading.value = false;
  352. if (res.data.code == 1) {
  353. messageType.value = 'success';
  354. messageText.value = res.data.msg;
  355. message.value.open();
  356. console.log('res.data.data---', res.data.data);
  357. const historyItem = {
  358. orderNum: valiFormData.orderNum,
  359. createTime: new Date(),
  360. space: spaceRes.value,
  361. type: '入库',
  362. status: true
  363. };
  364. historyList.value.unshift(historyItem);
  365. uni.setStorageSync('inboundHistory', historyList.value);
  366. getHistory();
  367. if (res.data.data.express_label) {
  368. console.log('有打印面单');
  369. // selectPrinter.value = sendToPeinter.value.length > 0 ? printers.value[0].value : 0;
  370. // printerDialog.value.open();
  371. } else {
  372. st = setTimeout(() => {
  373. reset();
  374. st && clearTimeout(st);
  375. }, 1000);
  376. }
  377. } else {
  378. messageType.value = 'error';
  379. messageText.value = res.data.msg;
  380. message.value.open();
  381. const historyItem = {
  382. orderNum: valiFormData.orderNum,
  383. createTime: new Date(),
  384. space: spaceRes.value,
  385. type: '入库',
  386. status: false
  387. };
  388. historyList.value.unshift(historyItem);
  389. uni.setStorageSync('inboundHistory', historyList.value);
  390. getHistory();
  391. st = setTimeout(() => {
  392. reset();
  393. st && clearTimeout(st);
  394. }, 1000);
  395. }
  396. }
  397. });
  398. };
  399. const close = () => {
  400. printerDialog.value.close();
  401. st = setTimeout(() => {
  402. reset();
  403. st && clearTimeout(st);
  404. }, 700);
  405. };
  406. const sendToPeinterFun = (res: any) => {
  407. if (!res.detail.value.length > 0) {
  408. selectPrinter.value = 0;
  409. } else {
  410. selectPrinter.value = printers.value[0].value;
  411. }
  412. };
  413. const printConfirm = () => {
  414. subLoading.value = true;
  415. uni.request({
  416. url: printWaybillLabelURL,
  417. method: 'POST',
  418. header: {
  419. batoken: token.value
  420. },
  421. data: {
  422. order_no: valiFormData.orderNum,
  423. printer_code: selectPrinter
  424. },
  425. success: (res) => {
  426. close();
  427. subLoading.value = false;
  428. console.log('打印成功', res);
  429. messageType.value = 'success';
  430. messageText.value = '打印成功';
  431. message.value.open();
  432. }
  433. });
  434. };
  435. const onsubmit = () => {
  436. valiForm.value
  437. .validate()
  438. .then((res) => {
  439. warehouseScan();
  440. })
  441. .catch((err) => {
  442. console.log('err', err);
  443. });
  444. };
  445. const getHistory = () => {
  446. historyList.value = uni.getStorageSync('inboundHistory')
  447. };
  448. const getImages = () => {
  449. const res = images.value.map((item: any) => {
  450. return {
  451. name: item.name,
  452. savePath: item.serverUrl,
  453. fileSize: item.size,
  454. mimeType: item.type
  455. };
  456. });
  457. console.log('res22 ', res);
  458. return res;
  459. };
  460. // 获取阿里云oss签名
  461. const getAliyunOssSignature = (rawFiles) => {
  462. uni.request({
  463. url: getAliyunOssSignatureUrl,
  464. method: 'GET',
  465. header: {
  466. batoken: token.value
  467. },
  468. success: ({ data }: any) => {
  469. const signature = data.data.signature;
  470. const uploadPromises = rawFiles.map((image) => {
  471. return upLoadFile(signature, image);
  472. });
  473. Promise.all(uploadPromises)
  474. .then((results) => {
  475. console.log('所有图片加载成功:', results);
  476. uni.showToast({
  477. title: '加载成功',
  478. icon: 'success'
  479. });
  480. })
  481. .catch((error) => {
  482. console.error('加载失败:', error);
  483. uni.showToast({
  484. title: '加载失败',
  485. icon: 'none'
  486. });
  487. });
  488. },
  489. fail: (err) => {
  490. console.log(err);
  491. }
  492. });
  493. };
  494. const upLoadFile = (signature, image) => {
  495. const fileData = {
  496. policy: signature.policy,
  497. signature: signature.signature,
  498. ossaccessKeyId: signature.ossAccessKeyId,
  499. key: signature.dir + dayjs().format('YYYYMMDD') + '/' + uuid() + '_' + image.name,
  500. dir: signature.dir,
  501. host: signature.host,
  502. file: image.file
  503. };
  504. return new Promise((resolve, reject) => {
  505. let name = image.name;
  506. // #ifdef APP-PLUS
  507. name = 'file';
  508. // #endif
  509. const uploadTask = uni.uploadFile({
  510. url: signature.host, // 你的上传接口地址
  511. filePath: image.path,
  512. name: name, // 这里根据后端需要的字段来定义
  513. formData: fileData,
  514. success: (uploadFileRes) => {
  515. if (uploadFileRes.statusCode === 204 || uploadFileRes.statusCode === 200) {
  516. image!.serverUrl = fileData.key;
  517. resolve(uploadFileRes);
  518. } else {
  519. reject(uploadFileRes);
  520. }
  521. },
  522. fail: (error) => {
  523. console.log('error++', error);
  524. reject(error);
  525. },
  526. // 更新上传进度
  527. complete: () => {
  528. console.log('complete---');
  529. image.progress = 100;
  530. }
  531. });
  532. // 可选:监听上传进度变化
  533. uploadTask.onProgressUpdate((progressEvent) => {
  534. image.progress = progressEvent.progress; // 更新进度
  535. });
  536. });
  537. };
  538. const chooseImage = () => {
  539. uni.chooseImage({
  540. count: maxImages.value - images.value.length,
  541. success: (res) => {
  542. images.value = images.value.concat(
  543. res.tempFiles.map((item) => {
  544. const res = {
  545. size: item.size,
  546. path: item.path,
  547. name: item.name,
  548. type: item.type,
  549. progress: 0,
  550. file: item
  551. };
  552. // #ifdef APP-PLUS
  553. //文件名操作
  554. //获取文件后缀
  555. const suffix = item.path.substring(item.path.lastIndexOf('.') + 1);
  556. //获取文件名
  557. const fileName = item.path.substring(item.path.lastIndexOf('/') + 1);
  558. res.name = fileName;
  559. res.type = suffix;
  560. // #endif
  561. return res;
  562. })
  563. );
  564. const paddingImages = images.value.filter((image) => image.progress === 0);
  565. nextTick(() => {
  566. getAliyunOssSignature(paddingImages);
  567. });
  568. },
  569. fail: (err) => {
  570. console.error(err);
  571. }
  572. });
  573. };
  574. const deleteImage = (index) => {
  575. images.value.splice(index, 1);
  576. };
  577. const openPreview = (image) => {
  578. previewImage.value = image; // 设置放大预览的图片
  579. };
  580. const closePreview = () => {
  581. previewImage.value = null; // 关闭预览
  582. };
  583. </script>
  584. <style lang="scss">
  585. .example {
  586. padding: 15px;
  587. background-color: #fff;
  588. }
  589. .checkbox-cum {
  590. margin-bottom: 20rpx;
  591. font-size: 14rpx;
  592. }
  593. .sub-choice {
  594. margin-bottom: 20rpx;
  595. margin-left: 20rpx;
  596. font-size: 14rpx;
  597. }
  598. .button-group {
  599. margin-top: 15px;
  600. display: flex;
  601. flex-direction: row;
  602. justify-content: space-around;
  603. button {
  604. display: flex;
  605. align-items: center;
  606. justify-content: center;
  607. height: 35px;
  608. width: 50%;
  609. margin-left: 10px;
  610. font-size: 16rpx;
  611. }
  612. .uni-icons {
  613. margin-right: 10px;
  614. }
  615. }
  616. .weight-right {
  617. padding-right: 10rpx;
  618. font-size: 14rpx;
  619. }
  620. .history {
  621. display: flex;
  622. width: 100%;
  623. flex-direction: column;
  624. justify-items: start;
  625. .title {
  626. padding: 20rpx;
  627. font-size: 24rpx;
  628. font-weight: 600;
  629. }
  630. .code {
  631. font-weight: 600;
  632. }
  633. .item {
  634. padding: 20rpx;
  635. font-size: 20rpx;
  636. color: #666;
  637. .status {
  638. padding-left: 20rpx;
  639. }
  640. .fail {
  641. font-weight: 600;
  642. color: #f00;
  643. }
  644. }
  645. }
  646. </style>
  647. <style scoped>
  648. .upload-container {
  649. padding: 16px;
  650. }
  651. .preview {
  652. display: flex;
  653. flex-wrap: wrap;
  654. }
  655. .image-container {
  656. position: relative;
  657. margin-right: 10px;
  658. margin-bottom: 10px;
  659. }
  660. .preview-image {
  661. width: 130rpx;
  662. height: 130rpx;
  663. }
  664. .delete-icon {
  665. position: absolute;
  666. top: 0;
  667. right: 0;
  668. cursor: pointer;
  669. }
  670. .choose-image-container {
  671. width: 130rpx;
  672. /* 与图片大小一致 */
  673. height: 130rpx;
  674. /* 与图片大小一致 */
  675. display: flex;
  676. align-items: center;
  677. justify-content: center;
  678. background-color: #fff;
  679. /* 背景颜色为白色 */
  680. border: 1px dashed #cccccc;
  681. /* 虚线边框 */
  682. border-radius: 5px;
  683. /* 圆角 */
  684. cursor: pointer;
  685. }
  686. .preview-modal {
  687. position: fixed;
  688. top: 0;
  689. left: 0;
  690. right: 0;
  691. bottom: 0;
  692. background-color: rgba(0, 0, 0, 0.8);
  693. /* 半透明背景 */
  694. display: flex;
  695. align-items: center;
  696. justify-content: center;
  697. z-index: 999;
  698. }
  699. .preview-large {
  700. max-width: 90%;
  701. /* 最大宽度 */
  702. max-height: 90%;
  703. /* 最大高度 */
  704. }
  705. .progress-bar {
  706. position: absolute;
  707. bottom: 0;
  708. left: 0;
  709. width: 100%;
  710. }
  711. </style>