From 7f84bbca795b60e58817416cbb05971652a695cf Mon Sep 17 00:00:00 2001 From: yuan Date: Wed, 13 May 2026 19:48:21 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A2=81=E4=BC=9F=E6=9D=B0:=20V2.0.002=20?= =?UTF-8?q?=E5=85=B6=E4=BB=96=E5=85=A5=E5=BA=93=E5=85=B6=E4=BB=96=E5=87=BA?= =?UTF-8?q?=E5=BA=93=E6=89=AB=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + pnpm-lock.yaml | 279 ++++++++++++++- src/api/erp/customer/index.ts | 12 + src/api/erp/product/index.ts | 16 + src/components/erp-scan/item-list.vue | 157 +++++++++ src/components/erp-scan/scan-input.vue | 117 +++++++ src/components/erp-scan/submit-bar.vue | 49 +++ src/pages-erp/stock-in/scan/index.vue | 403 ++++++++++++++++++++++ src/pages-erp/stock-out/scan/index.vue | 455 +++++++++++++++++++++++++ src/pages/index/index.ts | 16 + src/utils/__tests__/scan.spec.ts | 81 +++++ src/utils/scan.ts | 85 +++++ vitest.config.ts | 20 ++ 13 files changed, 1686 insertions(+), 6 deletions(-) create mode 100644 src/api/erp/customer/index.ts create mode 100644 src/components/erp-scan/item-list.vue create mode 100644 src/components/erp-scan/scan-input.vue create mode 100644 src/components/erp-scan/submit-bar.vue create mode 100644 src/pages-erp/stock-in/scan/index.vue create mode 100644 src/pages-erp/stock-out/scan/index.vue create mode 100644 src/utils/__tests__/scan.spec.ts create mode 100644 src/utils/scan.ts create mode 100644 vitest.config.ts diff --git a/package.json b/package.json index f38f150..97fcc76 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "build:quickapp-webview": "uni build -p quickapp-webview", "build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei", "build:quickapp-webview-union": "uni build -p quickapp-webview-union", + "test": "vitest run --config vitest.config.ts", "type-check": "vue-tsc --noEmit", "init-husky": "git init && husky", "init-baseFiles": "node ./scripts/create-base-files.js", @@ -173,6 +174,7 @@ "unplugin-auto-import": "^20.0.0", "vite": "5.2.8", "vite-plugin-restart": "^1.0.0", + "vitest": "^3.2.4", "vue-tsc": "^3.0.6" }, "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4fbc68..565b131 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,7 +135,7 @@ importers: version: 20.19.11 '@uni-helper/eslint-config': specifier: 0.5.0 - version: 0.5.0(@antfu/eslint-config@5.2.1(@unocss/eslint-plugin@66.5.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.22)(eslint-plugin-format@1.0.1(eslint@9.34.0(jiti@2.6.1)))(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.34.0(jiti@2.6.1)) + version: 0.5.0(@antfu/eslint-config@5.2.1(@unocss/eslint-plugin@66.5.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.22)(eslint-plugin-format@1.0.1(eslint@9.34.0(jiti@2.6.1)))(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.11)(jsdom@16.7.0)(sass@1.77.8)(terser@5.43.1)))(eslint@9.34.0(jiti@2.6.1)) '@uni-helper/plugin-uni': specifier: 0.1.0 version: 0.1.0(@dcloudio/vite-plugin-uni@3.0.0-4070620250821001(postcss@8.5.6)(rollup@4.50.0)(vite@5.2.8(@types/node@20.19.11)(sass@1.77.8)(terser@5.43.1))(vue@3.4.21(typescript@5.8.3))) @@ -235,6 +235,9 @@ importers: vite-plugin-restart: specifier: ^1.0.0 version: 1.0.0(vite@5.2.8(@types/node@20.19.11)(sass@1.77.8)(terser@5.43.1)) + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.11)(jsdom@16.7.0)(sass@1.77.8)(terser@5.43.1) vue-tsc: specifier: ^3.0.6 version: 3.0.6(typescript@5.8.3) @@ -1937,12 +1940,18 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/conventional-commits-parser@5.0.1': resolution: {integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==} '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -2345,6 +2354,35 @@ packages: vitest: optional: true + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@volar/language-core@2.4.23': resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} @@ -2565,6 +2603,10 @@ packages: array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-kit@0.11.3: resolution: {integrity: sha512-qdwwKEhckRk0XE22/xDdmU3v/60E8Edu4qFhgTLIhGGDs/PAJwLw9pQn8Rj99PitlbBZbYpx0k/lbir4kg0SuA==} engines: {node: '>=16.14.0'} @@ -2758,6 +2800,10 @@ packages: centra@2.7.0: resolution: {integrity: sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2776,6 +2822,10 @@ packages: character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -3045,6 +3095,10 @@ packages: dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -3478,6 +3532,10 @@ packages: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + expect@27.5.1: resolution: {integrity: sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -4299,6 +4357,9 @@ packages: longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -4782,6 +4843,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -5210,6 +5275,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -5271,6 +5339,9 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -5399,6 +5470,9 @@ packages: timm@1.7.1: resolution: {integrity: sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} @@ -5416,6 +5490,18 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -5652,6 +5738,11 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite-plugin-restart@1.0.0: resolution: {integrity: sha512-t2ktkTOa+DQX05TEZm/3FE0DyrYEyFXdhG5gLcta5p71zOpg9yG3DeRcHWJVLJgWNoaVtOr4fUlr1kKu+WfXyQ==} peerDependencies: @@ -5685,6 +5776,34 @@ packages: terser: optional: true + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} @@ -5772,6 +5891,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -5916,7 +6040,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.30 - '@antfu/eslint-config@5.2.1(@unocss/eslint-plugin@66.5.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.22)(eslint-plugin-format@1.0.1(eslint@9.34.0(jiti@2.6.1)))(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3)': + '@antfu/eslint-config@5.2.1(@unocss/eslint-plugin@66.5.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.22)(eslint-plugin-format@1.0.1(eslint@9.34.0(jiti@2.6.1)))(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.11)(jsdom@16.7.0)(sass@1.77.8)(terser@5.43.1))': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 0.11.0 @@ -5925,7 +6049,7 @@ snapshots: '@stylistic/eslint-plugin': 5.4.0(eslint@9.34.0(jiti@2.6.1)) '@typescript-eslint/eslint-plugin': 8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3) '@typescript-eslint/parser': 8.46.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3) - '@vitest/eslint-plugin': 1.3.16(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3) + '@vitest/eslint-plugin': 1.3.16(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.11)(jsdom@16.7.0)(sass@1.77.8)(terser@5.43.1)) ansis: 4.2.0 cac: 6.7.14 eslint: 9.34.0(jiti@2.6.1) @@ -8508,6 +8632,11 @@ snapshots: dependencies: '@babel/types': 7.28.4 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/conventional-commits-parser@5.0.1': dependencies: '@types/node': 20.19.11 @@ -8516,6 +8645,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} '@types/graceful-fs@4.1.9': @@ -8711,9 +8842,9 @@ snapshots: '@typescript-eslint/types': 8.46.0 eslint-visitor-keys: 4.2.1 - '@uni-helper/eslint-config@0.5.0(@antfu/eslint-config@5.2.1(@unocss/eslint-plugin@66.5.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.22)(eslint-plugin-format@1.0.1(eslint@9.34.0(jiti@2.6.1)))(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3))(eslint@9.34.0(jiti@2.6.1))': + '@uni-helper/eslint-config@0.5.0(@antfu/eslint-config@5.2.1(@unocss/eslint-plugin@66.5.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.22)(eslint-plugin-format@1.0.1(eslint@9.34.0(jiti@2.6.1)))(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.11)(jsdom@16.7.0)(sass@1.77.8)(terser@5.43.1)))(eslint@9.34.0(jiti@2.6.1))': dependencies: - '@antfu/eslint-config': 5.2.1(@unocss/eslint-plugin@66.5.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.22)(eslint-plugin-format@1.0.1(eslint@9.34.0(jiti@2.6.1)))(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3) + '@antfu/eslint-config': 5.2.1(@unocss/eslint-plugin@66.5.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3))(@vue/compiler-sfc@3.5.22)(eslint-plugin-format@1.0.1(eslint@9.34.0(jiti@2.6.1)))(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.11)(jsdom@16.7.0)(sass@1.77.8)(terser@5.43.1)) '@eslint/eslintrc': 3.3.1 eslint: 9.34.0(jiti@2.6.1) eslint-flat-config-utils: 2.1.1 @@ -9108,16 +9239,59 @@ snapshots: vite: 5.2.8(@types/node@20.19.11)(sass@1.77.8)(terser@5.43.1) vue: 3.4.21(typescript@5.8.3) - '@vitest/eslint-plugin@1.3.16(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3)': + '@vitest/eslint-plugin@1.3.16(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.11)(jsdom@16.7.0)(sass@1.77.8)(terser@5.43.1))': dependencies: '@typescript-eslint/scope-manager': 8.46.0 '@typescript-eslint/utils': 8.46.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3) eslint: 9.34.0(jiti@2.6.1) optionalDependencies: typescript: 5.8.3 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.11)(jsdom@16.7.0)(sass@1.77.8)(terser@5.43.1) transitivePeerDependencies: - supports-color + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@5.2.8(@types/node@20.19.11)(sass@1.77.8)(terser@5.43.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 5.2.8(@types/node@20.19.11)(sass@1.77.8)(terser@5.43.1) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.0.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.19 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@volar/language-core@2.4.23': dependencies: '@volar/source-map': 2.4.23 @@ -9383,6 +9557,8 @@ snapshots: array-timsort@1.0.3: {} + assertion-error@2.0.1: {} + ast-kit@0.11.3(rollup@4.50.0): dependencies: '@babel/parser': 7.28.3 @@ -9633,6 +9809,14 @@ snapshots: transitivePeerDependencies: - debug + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -9646,6 +9830,8 @@ snapshots: character-entities@2.0.2: {} + check-error@2.1.3: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -9877,6 +10063,8 @@ snapshots: dedent@0.7.0: {} + deep-eql@5.0.2: {} + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -10381,6 +10569,8 @@ snapshots: exit@0.1.2: {} + expect-type@1.3.0: {} + expect@27.5.1: dependencies: '@jest/types': 27.5.1 @@ -11427,6 +11617,8 @@ snapshots: longest-streak@3.1.0: {} + loupe@3.2.1: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -12045,6 +12237,8 @@ snapshots: pathe@2.0.3: {} + pathval@2.0.1: {} + perfect-debounce@1.0.0: {} phin@2.9.3: {} @@ -12475,6 +12669,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -12527,6 +12723,8 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackback@0.0.2: {} + statuses@2.0.1: {} std-env@3.9.0: {} @@ -12645,6 +12843,8 @@ snapshots: timm@1.7.1: {} + tinybench@2.9.0: {} + tinycolor2@1.6.0: {} tinyexec@0.3.2: {} @@ -12661,6 +12861,12 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -12946,6 +13152,23 @@ snapshots: vary@1.1.2: {} + vite-node@3.2.4(@types/node@20.19.11)(sass@1.77.8)(terser@5.43.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 5.2.8(@types/node@20.19.11)(sass@1.77.8)(terser@5.43.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + vite-plugin-restart@1.0.0(vite@5.2.8(@types/node@20.19.11)(sass@1.77.8)(terser@5.43.1)): dependencies: micromatch: 4.0.8 @@ -12962,6 +13185,45 @@ snapshots: sass: 1.77.8 terser: 5.43.1 + vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.11)(jsdom@16.7.0)(sass@1.77.8)(terser@5.43.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@5.2.8(@types/node@20.19.11)(sass@1.77.8)(terser@5.43.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.19 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 5.2.8(@types/node@20.19.11)(sass@1.77.8)(terser@5.43.1) + vite-node: 3.2.4(@types/node@20.19.11)(sass@1.77.8)(terser@5.43.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 20.19.11 + jsdom: 16.7.0 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - stylus + - sugarss + - supports-color + - terser + vscode-uri@3.1.0: {} vue-demi@0.14.10(vue@3.4.21(typescript@5.8.3)): @@ -13047,6 +13309,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wot-design-uni@1.13.0(vue@3.4.21(typescript@5.8.3)): diff --git a/src/api/erp/customer/index.ts b/src/api/erp/customer/index.ts new file mode 100644 index 0000000..63ffabc --- /dev/null +++ b/src/api/erp/customer/index.ts @@ -0,0 +1,12 @@ +import { http } from '@/http/http' + +/** 客户精简信息 */ +export interface CustomerSimple { + id: number + name: string +} + +/** 获取客户精简列表 */ +export function getCustomerSimpleList() { + return http.get('/erp/customer/simple-list') +} diff --git a/src/api/erp/product/index.ts b/src/api/erp/product/index.ts index f785570..f09fd61 100644 --- a/src/api/erp/product/index.ts +++ b/src/api/erp/product/index.ts @@ -16,7 +16,23 @@ export interface ProductSimple { weight?: number } +/** 按条码查询的产品信息 */ +export interface ProductByBarCode { + id: number + name: string + barCode?: string + unitId?: number + unitName?: string + standard?: string + minPrice?: number +} + /** 获取产品精简列表 */ export function getProductSimpleList() { return http.get('/erp/product/simple-list') } + +/** 按条码查询启用中的产品 */ +export function getProductByBarCode(barCode: string) { + return http.get('/erp/product/get-by-barcode', { barCode }) +} diff --git a/src/components/erp-scan/item-list.vue b/src/components/erp-scan/item-list.vue new file mode 100644 index 0000000..7af0295 --- /dev/null +++ b/src/components/erp-scan/item-list.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/src/components/erp-scan/scan-input.vue b/src/components/erp-scan/scan-input.vue new file mode 100644 index 0000000..c467fbc --- /dev/null +++ b/src/components/erp-scan/scan-input.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/src/components/erp-scan/submit-bar.vue b/src/components/erp-scan/submit-bar.vue new file mode 100644 index 0000000..e756941 --- /dev/null +++ b/src/components/erp-scan/submit-bar.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/pages-erp/stock-in/scan/index.vue b/src/pages-erp/stock-in/scan/index.vue new file mode 100644 index 0000000..ff0bdf6 --- /dev/null +++ b/src/pages-erp/stock-in/scan/index.vue @@ -0,0 +1,403 @@ + + + + + diff --git a/src/pages-erp/stock-out/scan/index.vue b/src/pages-erp/stock-out/scan/index.vue new file mode 100644 index 0000000..db25f6e --- /dev/null +++ b/src/pages-erp/stock-out/scan/index.vue @@ -0,0 +1,455 @@ + + + + + diff --git a/src/pages/index/index.ts b/src/pages/index/index.ts index 3955bf4..acb3a7c 100644 --- a/src/pages/index/index.ts +++ b/src/pages/index/index.ts @@ -146,6 +146,14 @@ const menuGroupsData: MenuGroup[] = [ iconColor: '#13c2c2', permission: 'erp:stock-in:query', }, + { + key: 'stockInScan', + name: '扫码其它入库', + icon: 'scan', + url: '/pages-erp/stock-in/scan/index', + iconColor: '#36cfc9', + permission: 'erp:stock-in:query', + }, { key: 'stockOut', name: '其他出库', @@ -154,6 +162,14 @@ const menuGroupsData: MenuGroup[] = [ iconColor: '#fa8c16', permission: 'erp:stock-out:query', }, + { + key: 'stockOutScan', + name: '扫码其它出库', + icon: 'scan', + url: '/pages-erp/stock-out/scan/index', + iconColor: '#ff7a45', + permission: 'erp:stock-out:query', + }, { key: 'stockGain', name: '库存报溢', diff --git a/src/utils/__tests__/scan.spec.ts b/src/utils/__tests__/scan.spec.ts new file mode 100644 index 0000000..2000ccd --- /dev/null +++ b/src/utils/__tests__/scan.spec.ts @@ -0,0 +1,81 @@ +import type { ScanItem } from '../scan' +import { describe, expect, it } from 'vitest' + +import { + buildScanMergeKey, + mergeScannedItem, + normalizeCameraScanResult, + normalizeScanCode, +} from '../scan' + +/** + * 功能说明:构造测试明细数据,避免每个用例重复拼装字段。 + * 适用场景:扫码合并工具函数单元测试。 + * @param overrides 需要覆盖的字段 + * @return 测试用明细对象 + * 注意事项:测试数据默认模拟“自由扫码”场景,订单约束字段按需覆盖。 + */ +function createItem(overrides: Partial = {}): ScanItem { + return { + warehouseId: 1, + productId: 1001, + count: 1, + orderItemId: undefined, + ...overrides, + } +} + +describe('normalizeScanCode', () => { + it('应去掉回车换行和前后空格', () => { + expect(normalizeScanCode(' TEST-001\r\n')).toBe('TEST-001') + }) + + it('空值应返回空字符串', () => { + expect(normalizeScanCode('')).toBe('') + }) +}) + +describe('normalizeCameraScanResult', () => { + it('应从手机扫码结果对象中提取并清洗条码', () => { + expect(normalizeCameraScanResult({ result: ' MOBILE-001\r\n' })).toBe('MOBILE-001') + }) + + it('扫码取消或结果为空时应返回空字符串', () => { + expect(normalizeCameraScanResult({ result: '' })).toBe('') + }) +}) + +describe('buildScanMergeKey', () => { + it('自由扫码模式应按仓库和产品生成合并键', () => { + expect(buildScanMergeKey(createItem())).toBe('1:1001') + }) + + it('订单约束模式应额外带上订单明细编号', () => { + expect(buildScanMergeKey(createItem({ orderItemId: 5001 }), true)).toBe('1:1001:5001') + }) +}) + +describe('mergeScannedItem', () => { + it('同仓库同产品重复扫码应累计数量', () => { + const merged = mergeScannedItem([createItem({ count: 2 })], createItem(), false) + + expect(merged).toHaveLength(1) + expect(merged[0].count).toBe(3) + }) + + it('跨仓库扫码不应错误合并', () => { + const merged = mergeScannedItem([createItem({ warehouseId: 2 })], createItem(), false) + + expect(merged).toHaveLength(2) + }) + + it('订单约束模式下不同订单行不应合并', () => { + const merged = mergeScannedItem( + [createItem({ orderItemId: 9001 })], + createItem({ orderItemId: 9002 }), + true, + ) + + expect(merged).toHaveLength(2) + }) +}) diff --git a/src/utils/scan.ts b/src/utils/scan.ts new file mode 100644 index 0000000..c1e592a --- /dev/null +++ b/src/utils/scan.ts @@ -0,0 +1,85 @@ +/** + * 功能说明:定义扫码明细的最小结构,统一扫码页里的重复合并计算输入。 + * 适用场景:扫码其它入库、扫码其它出库的条码累计逻辑。 + */ +export interface ScanItem { + warehouseId: number + productId: number + count: number + orderItemId?: number +} + +/** + * 功能说明:清洗扫码枪输入的条码文本,去掉回车、换行和前后空白。 + * 适用场景:扫码枪通过键盘模拟输入后,页面提交前统一清洗条码值。 + * @param raw 原始扫码内容 + * @return 清洗后的条码字符串 + * 注意事项:扫码枪通常会追加回车,不先清洗会导致条码查产品接口无法命中。 + */ +export function normalizeScanCode(raw: string): string { + return (raw || '').replace(/[\r\n]/g, '').trim() +} + +/** + * 功能说明:把相机扫码结果统一转换成业务可用条码。 + * 适用场景:`uni.scanCode` 返回对象后,与扫码枪输入共用同一套处理流程。 + * @param result uni-app 扫码返回结果 + * @return 清洗后的条码字符串 + * 注意事项:扫码取消、空返回或结构变化时,需要安全降级为空字符串,避免页面直接抛错。 + */ +export function normalizeCameraScanResult(result?: { result?: string } | null): string { + return normalizeScanCode(result?.result || '') +} + +/** + * 功能说明:生成扫码明细的合并键,决定重复扫码时应该累加到哪一行。 + * 适用场景:自由扫码模式按“仓库 + 产品”合并,订单约束模式再附加订单行编号。 + * @param item 当前明细 + * @param withOrderConstraint 是否启用订单约束模式 + * @return 合并键字符串 + * 注意事项:订单约束场景若不带订单行编号,会把不同订单行的同一产品错误合并。 + */ +export function buildScanMergeKey(item: ScanItem, withOrderConstraint = false): string { + const segments = [item.warehouseId, item.productId] + + // 订单约束扫码时需要把订单明细编号带入合并键,避免不同订单行误合并。 + if (withOrderConstraint) { + segments.push(item.orderItemId || 0) + } + + return segments.join(':') +} + +/** + * 功能说明:将一次新的扫码结果合并到现有明细列表中。 + * 适用场景:连续扫到同一产品时自动累计数量,减少仓库人员手工修改次数。 + * @param items 当前明细列表 + * @param scannedItem 新扫码得到的明细 + * @param withOrderConstraint 是否启用订单约束模式 + * @return 合并后的新明细列表 + * 注意事项:这里返回新数组而不是原地修改,避免页面状态更新不稳定。 + */ +export function mergeScannedItem( + items: ScanItem[], + scannedItem: ScanItem, + withOrderConstraint = false, +): ScanItem[] { + const targetKey = buildScanMergeKey(scannedItem, withOrderConstraint) + const existedIndex = items.findIndex(item => buildScanMergeKey(item, withOrderConstraint) === targetKey) + + if (existedIndex === -1) { + return [...items, scannedItem] + } + + return items.map((item, index) => { + if (index !== existedIndex) { + return item + } + + // 命中同一合并键时只累计数量,避免把已录入明细拆成多行,影响 PDA 连续扫码效率。 + return { + ...item, + count: Number(item.count || 0) + Number(scannedItem.count || 0), + } + }) +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..c81e931 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,20 @@ +import path from 'node:path' +import { defineConfig } from 'vitest/config' + +/** + * 功能说明:为纯工具函数测试提供独立的 Vitest 配置,避免 uni-app 构建插件影响测试执行。 + * 适用场景:运行扫码工具、数据清洗、明细合并等不依赖页面渲染的单元测试。 + * @return Vitest 配置对象 + * 注意事项:当前仅解析 `@/` 别名并执行 `src/utils/__tests__` 下的测试,避免把页面编译链路引入测试环境。 + */ +export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, + }, + test: { + environment: 'node', + include: ['src/utils/__tests__/**/*.spec.ts'], + }, +})