add: implementar escáner de códigos QR y agregar soporte en la interfaz de usuario
This commit is contained in:
parent
62cf6afc42
commit
58a0e5b89e
64
package-lock.json
generated
64
package-lock.json
generated
@ -24,6 +24,7 @@
|
|||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-i18n": "^11.1.1",
|
"vue-i18n": "^11.1.1",
|
||||||
"vue-multiselect": "^3.2.0",
|
"vue-multiselect": "^3.2.0",
|
||||||
|
"vue-qrcode-reader": "^5.7.3",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
"ziggy-js": "^2.5.2"
|
"ziggy-js": "^2.5.2"
|
||||||
},
|
},
|
||||||
@ -1234,6 +1235,18 @@
|
|||||||
"vite": "^5.2.0 || ^6"
|
"vite": "^5.2.0 || ^6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/dom-webcodecs": {
|
||||||
|
"version": "0.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.18.tgz",
|
||||||
|
"integrity": "sha512-vAvE8C9DGWR+tkb19xyjk1TSUlJ7RUzzp4a9Anu7mwBT+fpyePWK1UxmH14tMO5zHmrnrRIMg5NutnnDztLxgg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/emscripten": {
|
||||||
|
"version": "1.41.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz",
|
||||||
|
"integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||||
@ -1522,6 +1535,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/barcode-detector": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/barcode-detector/-/barcode-detector-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-JcSekql+EV93evfzF9zBr+Y6aRfkR+QFvgyzbwQ0dbymZXoAI9+WgT7H1E429f+3RKNncHz2CW98VQtaaKpmfQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/dom-webcodecs": "^0.1.11",
|
||||||
|
"zxing-wasm": "1.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/base64-arraybuffer": {
|
"node_modules/base64-arraybuffer": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
@ -3416,6 +3439,12 @@
|
|||||||
"queue-microtask": "^1.2.2"
|
"queue-microtask": "^1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sdp": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/socket.io-client": {
|
"node_modules/socket.io-client": {
|
||||||
"version": "4.8.1",
|
"version": "4.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||||
@ -3867,6 +3896,19 @@
|
|||||||
"npm": ">= 6.14.15"
|
"npm": ">= 6.14.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-qrcode-reader": {
|
||||||
|
"version": "5.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-qrcode-reader/-/vue-qrcode-reader-5.7.3.tgz",
|
||||||
|
"integrity": "sha512-iSGko42FsEvdHyizBMBs/X+HMO9Z5ONDxjW+mQdoraOR5emRNedmjC5SEJdYzGz8ZP5ME3lwB4iHy3S7MOt5Qw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"barcode-detector": "2.2.2",
|
||||||
|
"webrtc-adapter": "8.2.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "4.5.1",
|
"version": "4.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
||||||
@ -3888,6 +3930,19 @@
|
|||||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/webrtc-adapter": {
|
||||||
|
"version": "8.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-8.2.3.tgz",
|
||||||
|
"integrity": "sha512-gnmRz++suzmvxtp3ehQts6s2JtAGPuDPjA1F3a9ckNpG1kYdYuHWYpazoAnL9FS5/B21tKlhkorbdCXat0+4xQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"sdp": "^3.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0",
|
||||||
|
"npm": ">=3.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.17.1",
|
"version": "8.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
@ -3937,6 +3992,15 @@
|
|||||||
"@types/qs": "^6.9.17",
|
"@types/qs": "^6.9.17",
|
||||||
"qs": "~6.9.7"
|
"qs": "~6.9.7"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zxing-wasm": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/zxing-wasm/-/zxing-wasm-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-MYm9k/5YVs4ZOTIFwlRjfFKD0crhefgbnt1+6TEpmKUDFp3E2uwqGSKwQOd2hOIsta/7Usq4hnpNRYTLoljnfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/emscripten": "^1.39.10"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-i18n": "^11.1.1",
|
"vue-i18n": "^11.1.1",
|
||||||
"vue-multiselect": "^3.2.0",
|
"vue-multiselect": "^3.2.0",
|
||||||
|
"vue-qrcode-reader": "^5.7.3",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
"ziggy-js": "^2.5.2"
|
"ziggy-js": "^2.5.2"
|
||||||
},
|
},
|
||||||
|
|||||||
130
src/components/POS/QRscan.vue
Normal file
130
src/components/POS/QRscan.vue
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { QrcodeStream } from 'vue-qrcode-reader';
|
||||||
|
|
||||||
|
/** Props y Emits */
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'qr-detected']);
|
||||||
|
|
||||||
|
/** Refs */
|
||||||
|
const error = ref('');
|
||||||
|
const isScanning = ref(true);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function paintBoundingBox(detectedCodes, ctx) {
|
||||||
|
for (const detectedCode of detectedCodes) {
|
||||||
|
const {
|
||||||
|
boundingBox: { x, y, width, height }
|
||||||
|
} = detectedCode;
|
||||||
|
|
||||||
|
ctx.lineWidth = 4;
|
||||||
|
ctx.strokeStyle = '#10b981';
|
||||||
|
ctx.strokeRect(x, y, width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDetect(detectedCodes) {
|
||||||
|
if (detectedCodes.length > 0) {
|
||||||
|
const code = detectedCodes[0].rawValue;
|
||||||
|
|
||||||
|
// Emitir el código escaneado al componente padre
|
||||||
|
emit('update:modelValue', code);
|
||||||
|
|
||||||
|
// Pausar el escaneo después de detectar
|
||||||
|
isScanning.value = false;
|
||||||
|
|
||||||
|
// Emitir evento con el código detectado
|
||||||
|
emit('qr-detected', code);
|
||||||
|
|
||||||
|
// Reactivar el escaneo después de 2 segundos
|
||||||
|
setTimeout(() => {
|
||||||
|
isScanning.value = true;
|
||||||
|
error.value = '';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(err) {
|
||||||
|
error.value = `[${err.name}]: `;
|
||||||
|
|
||||||
|
if (err.name === 'NotAllowedError') {
|
||||||
|
error.value += 'Necesitas otorgar permiso de acceso a la cámara';
|
||||||
|
} else if (err.name === 'NotFoundError') {
|
||||||
|
error.value += 'No se encontró cámara en este dispositivo';
|
||||||
|
} else if (err.name === 'NotSupportedError') {
|
||||||
|
error.value += 'Se requiere contexto seguro';
|
||||||
|
} else if (err.name === 'NotReadableError') {
|
||||||
|
error.value += '¿La cámara ya está en uso?';
|
||||||
|
} else if (err.name === 'OverconstrainedError') {
|
||||||
|
error.value += 'Las cámaras instaladas no son adecuadas';
|
||||||
|
} else if (err.name === 'StreamApiNotSupportedError') {
|
||||||
|
error.value += 'No es compatible con este navegador';
|
||||||
|
} else if (err.name === 'InsecureContextError') {
|
||||||
|
error.value += 'El acceso a la cámara solo se permite en contexto seguro.';
|
||||||
|
} else {
|
||||||
|
error.value += err.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('Error de cámara:', err);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative w-full h-full">
|
||||||
|
<!-- Componente de escaneo QR -->
|
||||||
|
<QrcodeStream
|
||||||
|
v-if="isScanning"
|
||||||
|
@detect="onDetect"
|
||||||
|
@error="onError"
|
||||||
|
:track="paintBoundingBox"
|
||||||
|
class="w-full h-full rounded-lg overflow-hidden"
|
||||||
|
>
|
||||||
|
</QrcodeStream>
|
||||||
|
|
||||||
|
<!-- Mensaje de éxito -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="w-full h-full rounded-lg bg-green-600 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<div class="text-center text-white">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-16 w-16 mx-auto mb-2"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p class="text-lg font-semibold">¡Código detectado!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mensaje de error -->
|
||||||
|
<div
|
||||||
|
v-if="error"
|
||||||
|
class="absolute bottom-0 left-0 right-0 bg-red-500 text-white p-3 text-sm"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(video) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -451,6 +451,7 @@ export default {
|
|||||||
},
|
},
|
||||||
pos: {
|
pos: {
|
||||||
title: 'Punto de Venta',
|
title: 'Punto de Venta',
|
||||||
|
subtitle: 'Gestión de ventas y caja',
|
||||||
category: 'Categorías',
|
category: 'Categorías',
|
||||||
inventory: 'Inventario',
|
inventory: 'Inventario',
|
||||||
prices: 'Precios',
|
prices: 'Precios',
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import GoogleIcon from '@Shared/GoogleIcon.vue';
|
|||||||
import ProductCard from '@Components/POS/ProductCard.vue';
|
import ProductCard from '@Components/POS/ProductCard.vue';
|
||||||
import CartItem from '@Components/POS/CartItem.vue';
|
import CartItem from '@Components/POS/CartItem.vue';
|
||||||
import CheckoutModal from '@Components/POS/CheckoutModal.vue';
|
import CheckoutModal from '@Components/POS/CheckoutModal.vue';
|
||||||
|
import QRscan from '@Components/POS/QRscan.vue';
|
||||||
|
|
||||||
/** i18n */
|
/** i18n */
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@ -23,6 +24,7 @@ const products = ref([]);
|
|||||||
const showCheckoutModal = ref(false);
|
const showCheckoutModal = ref(false);
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
const processingPayment = ref(false);
|
const processingPayment = ref(false);
|
||||||
|
const scanMode = ref(false);
|
||||||
|
|
||||||
/** Buscador de productos */
|
/** Buscador de productos */
|
||||||
const searcher = useSearcher({
|
const searcher = useSearcher({
|
||||||
@ -79,6 +81,15 @@ const closeCheckout = () => {
|
|||||||
showCheckoutModal.value = false;
|
showCheckoutModal.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleScanMode = () => {
|
||||||
|
scanMode.value = !scanMode.value;
|
||||||
|
if(scanMode.value) {
|
||||||
|
window.Notify.info('Modo escaneo activado');
|
||||||
|
} else {
|
||||||
|
window.Notify.info('Modo escaneo desactivado');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleConfirmSale = async (paymentData) => {
|
const handleConfirmSale = async (paymentData) => {
|
||||||
processingPayment.value = true;
|
processingPayment.value = true;
|
||||||
|
|
||||||
@ -187,6 +198,19 @@ onMounted(() => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
@click="toggleScanMode"
|
||||||
|
:class="[
|
||||||
|
'flex items-center gap-2 px-4 py-3 rounded-lg font-semibold transition-all',
|
||||||
|
scanMode
|
||||||
|
? 'bg-red-500 hover:bg-red-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'
|
||||||
|
]"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<GoogleIcon :name="scanMode ? 'close' : 'qr_code_scanner'" class="text-xl" />
|
||||||
|
{{ scanMode ? 'Cerrar escáner' : 'Escanear código' }}
|
||||||
|
</button >
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -194,6 +218,28 @@ onMounted(() => {
|
|||||||
<div class="flex gap-6 h-[calc(100%-120px)] px-6">
|
<div class="flex gap-6 h-[calc(100%-120px)] px-6">
|
||||||
<!-- Columna de Productos -->
|
<!-- Columna de Productos -->
|
||||||
<div class="flex-1 flex flex-col overflow-hidden">
|
<div class="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<div v-if="scanMode" class="h-full">
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 p-6 h-full">
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100 mb-4 flex items-center gap-2">
|
||||||
|
<GoogleIcon name="qr_code_scanner" class="text-2xl text-indigo-600" />
|
||||||
|
Escáner de Códigos de Barras
|
||||||
|
</h2>
|
||||||
|
<div class="flex-1 relative bg-gray-100 dark:bg-gray-900 rounded-lg overflow-hidden">
|
||||||
|
<QRscan
|
||||||
|
@qr-detected="handleCodeDetected"
|
||||||
|
class="w-full h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||||
|
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||||
|
<strong>Instrucciones:</strong> Coloca el código de barras frente a la cámara.
|
||||||
|
El producto se agregará automáticamente al carrito cuando se detecte.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Buscador -->
|
<!-- Buscador -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user