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-i18n": "^11.1.1",
|
||||
"vue-multiselect": "^3.2.0",
|
||||
"vue-qrcode-reader": "^5.7.3",
|
||||
"vue-router": "^4.5.0",
|
||||
"ziggy-js": "^2.5.2"
|
||||
},
|
||||
@ -1234,6 +1235,18 @@
|
||||
"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": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
@ -1522,6 +1535,16 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
@ -3416,6 +3439,12 @@
|
||||
"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": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||
@ -3867,6 +3896,19 @@
|
||||
"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": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
|
||||
@ -3888,6 +3930,19 @@
|
||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||
"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": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
@ -3937,6 +3992,15 @@
|
||||
"@types/qs": "^6.9.17",
|
||||
"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-i18n": "^11.1.1",
|
||||
"vue-multiselect": "^3.2.0",
|
||||
"vue-qrcode-reader": "^5.7.3",
|
||||
"vue-router": "^4.5.0",
|
||||
"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: {
|
||||
title: 'Punto de Venta',
|
||||
subtitle: 'Gestión de ventas y caja',
|
||||
category: 'Categorías',
|
||||
inventory: 'Inventario',
|
||||
prices: 'Precios',
|
||||
|
||||
@ -11,6 +11,7 @@ import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||
import ProductCard from '@Components/POS/ProductCard.vue';
|
||||
import CartItem from '@Components/POS/CartItem.vue';
|
||||
import CheckoutModal from '@Components/POS/CheckoutModal.vue';
|
||||
import QRscan from '@Components/POS/QRscan.vue';
|
||||
|
||||
/** i18n */
|
||||
const { t } = useI18n();
|
||||
@ -23,6 +24,7 @@ const products = ref([]);
|
||||
const showCheckoutModal = ref(false);
|
||||
const searchQuery = ref('');
|
||||
const processingPayment = ref(false);
|
||||
const scanMode = ref(false);
|
||||
|
||||
/** Buscador de productos */
|
||||
const searcher = useSearcher({
|
||||
@ -79,6 +81,15 @@ const closeCheckout = () => {
|
||||
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) => {
|
||||
processingPayment.value = true;
|
||||
|
||||
@ -187,6 +198,19 @@ onMounted(() => {
|
||||
</p>
|
||||
</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>
|
||||
|
||||
@ -194,6 +218,28 @@ onMounted(() => {
|
||||
<div class="flex gap-6 h-[calc(100%-120px)] px-6">
|
||||
<!-- Columna de Productos -->
|
||||
<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 -->
|
||||
<div class="mb-4">
|
||||
<div class="relative">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user