update for Warranty plugin & barcode scanner
This commit is contained in:
parent
35add500ca
commit
6cbd431edb
|
@ -1,65 +1,83 @@
|
||||||
<template>
|
<template>
|
||||||
<v-dialog v-model="internalShow" max-width="500" persistent>
|
<v-dialog v-model="internalShow" max-width="500" persistent>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-toolbar color="primary" dark>
|
<v-toolbar color="primary" dark>
|
||||||
<v-toolbar-title>اسکن بارکد</v-toolbar-title>
|
<v-toolbar-title>اسکن بارکد</v-toolbar-title>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn icon @click="close"><v-icon>mdi-close</v-icon></v-btn>
|
<v-btn icon @click="close"><v-icon>mdi-close</v-icon></v-btn>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
<v-card-text class="pa-0">
|
|
||||||
<div class="scanner-container">
|
<v-card-text class="pa-0">
|
||||||
<div v-if="overlay" class="scanner-overlay">
|
<div v-if="errorMessage" class="error-box">
|
||||||
<div class="scanner-line"></div>
|
<v-icon color="red" class="mr-2">mdi-alert-circle</v-icon>
|
||||||
<div class="scanner-corner top-left"></div>
|
{{ errorMessage }}
|
||||||
<div class="scanner-corner top-right"></div>
|
<div v-if="canRetry" class="mt-2">
|
||||||
<div class="scanner-corner bottom-left"></div>
|
<v-btn size="small" color="primary" @click="requestPermission">تلاش دوباره</v-btn>
|
||||||
<div class="scanner-corner bottom-right"></div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<qrcode-stream :camera="camera" :torch="isFlashOn" :formats="formats"
|
|
||||||
:track="overlay ? paintOutline : undefined" @detect="onDetect" @init="onInit" />
|
<div v-else class="scanner-container">
|
||||||
</div>
|
<div v-if="overlay" class="scanner-overlay">
|
||||||
</v-card-text>
|
<div class="scanner-line"></div>
|
||||||
<v-card-actions class="pa-2">
|
<div class="scanner-corner top-left"></div>
|
||||||
<v-btn @click="toggleFlash" :color="isFlashOn ? 'primary' : 'grey'">
|
<div class="scanner-corner top-right"></div>
|
||||||
<v-icon start>mdi-flash</v-icon> فلش
|
<div class="scanner-corner bottom-left"></div>
|
||||||
</v-btn>
|
<div class="scanner-corner bottom-right"></div>
|
||||||
<v-btn @click="toggleCamera" :disabled="availableCameras.length < 2">
|
</div>
|
||||||
<v-icon start>mdi-camera-flip</v-icon>
|
<qrcode-stream
|
||||||
{{ isBackCamera ? 'عقب' : 'جلو' }}
|
:camera="camera"
|
||||||
</v-btn>
|
:torch="isFlashOn"
|
||||||
<v-spacer />
|
:formats="formats"
|
||||||
<v-btn color="primary" variant="tonal" @click="close">بستن</v-btn>
|
:track="overlay ? paintOutline : undefined"
|
||||||
</v-card-actions>
|
@detect="onDetect"
|
||||||
</v-card>
|
@init="onInit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions class="pa-2" v-if="!errorMessage">
|
||||||
|
<v-btn @click="toggleFlash" :color="isFlashOn ? 'primary' : 'grey'">
|
||||||
|
<v-icon start>mdi-flash</v-icon> فلش
|
||||||
|
</v-btn>
|
||||||
|
<v-btn @click="toggleCamera" :disabled="availableCameras.length < 2">
|
||||||
|
<v-icon start>mdi-camera-flip</v-icon>
|
||||||
|
{{ isBackCamera ? 'عقب' : 'جلو' }}
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn color="primary" variant="tonal" @click="close">بستن</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, onMounted } from 'vue'
|
import { ref, watch, onMounted } from 'vue'
|
||||||
import { QrcodeStream } from 'vue3-qrcode-reader'
|
import { QrcodeStream } from 'vue3-qrcode-reader'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: Boolean,
|
modelValue: Boolean,
|
||||||
formats: { type: Array as () => string[], default: () => ['QR_CODE', 'EAN_13', 'CODE_128'] },
|
formats: { type: Array as () => string[], default: () => ['QR_CODE', 'EAN_13', 'CODE_128'] },
|
||||||
overlay: { type: Boolean, default: true }
|
overlay: { type: Boolean, default: true }
|
||||||
})
|
})
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: boolean): void
|
(e: 'update:modelValue', value: boolean): void
|
||||||
(e: 'detected', barcode: string): void
|
(e: 'detected', barcode: string): void
|
||||||
(e: 'error', error: Error): void
|
(e: 'error', error: Error): void
|
||||||
(e: 'close'): void
|
(e: 'close'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const internalShow = ref(props.modelValue)
|
const internalShow = ref(props.modelValue)
|
||||||
watch(() => props.modelValue, val => internalShow.value = val)
|
watch(() => props.modelValue, val => internalShow.value = val)
|
||||||
watch(internalShow, val => emit('update:modelValue', val))
|
watch(internalShow, val => emit('update:modelValue', val))
|
||||||
|
|
||||||
const camera = ref<'auto' | 'off' | 'user' | 'environment'>('environment')
|
const camera = ref<'auto' | 'off' | 'user' | 'environment'>('environment')
|
||||||
const isFlashOn = ref(false)
|
const isFlashOn = ref(false)
|
||||||
const availableCameras = ref<MediaDeviceInfo[]>([])
|
const availableCameras = ref<MediaDeviceInfo[]>([])
|
||||||
const isBackCamera = ref(true)
|
const isBackCamera = ref(true)
|
||||||
|
const errorMessage = ref('')
|
||||||
const paintOutline = (location: any, ctx: CanvasRenderingContext2D) => {
|
const canRetry = ref(false)
|
||||||
|
|
||||||
|
const paintOutline = (location: any, ctx: CanvasRenderingContext2D) => {
|
||||||
if (!location) return
|
if (!location) return
|
||||||
const { topLeftCorner, topRightCorner, bottomLeftCorner, bottomRightCorner } = location
|
const { topLeftCorner, topRightCorner, bottomLeftCorner, bottomRightCorner } = location
|
||||||
ctx.strokeStyle = '#00ff00'
|
ctx.strokeStyle = '#00ff00'
|
||||||
|
@ -71,44 +89,82 @@ const paintOutline = (location: any, ctx: CanvasRenderingContext2D) => {
|
||||||
ctx.lineTo(bottomLeftCorner.x, bottomLeftCorner.y)
|
ctx.lineTo(bottomLeftCorner.x, bottomLeftCorner.y)
|
||||||
ctx.closePath()
|
ctx.closePath()
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onInit = (promise: Promise<void>) => {
|
const onInit = async (promise: Promise<void>) => {
|
||||||
promise.catch((err: Error) => {
|
try {
|
||||||
emit('error', err)
|
await promise
|
||||||
close()
|
errorMessage.value = ''
|
||||||
})
|
canRetry.value = false
|
||||||
}
|
} catch (err: any) {
|
||||||
|
if (err.name === 'NotAllowedError' || err.message?.includes('permission')) {
|
||||||
const onDetect = (promise: Promise<any>) => {
|
errorMessage.value = 'برای استفاده از اسکنر لطفاً اجازه دسترسی به دوربین را بدهید.'
|
||||||
promise.then(result => {
|
canRetry.value = true
|
||||||
if (result?.content) {
|
emit('error', new Error(errorMessage.value))
|
||||||
emit('detected', result.content)
|
return
|
||||||
close()
|
}
|
||||||
|
if (err.name === 'NotFoundError' || err.message?.includes('camera') || err.message?.includes('device')) {
|
||||||
|
errorMessage.value = 'هیچ دوربینی در دستگاه شما یافت نشد.'
|
||||||
|
canRetry.value = false
|
||||||
|
emit('error', new Error(errorMessage.value))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errorMessage.value = 'خطا در راهاندازی دوربین: ' + (err.message || 'خطای نامشخص')
|
||||||
|
canRetry.value = false
|
||||||
|
emit('error', new Error(errorMessage.value))
|
||||||
}
|
}
|
||||||
}).catch(err => console.error('Error decoding QR:', err))
|
}
|
||||||
}
|
|
||||||
|
const onDetect = (promise: Promise<any>) => {
|
||||||
const toggleFlash = () => isFlashOn.value = !isFlashOn.value
|
promise.then(result => {
|
||||||
|
if (result?.content) {
|
||||||
const toggleCamera = () => {
|
emit('detected', result.content)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Error decoding QR:', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleFlash = () => isFlashOn.value = !isFlashOn.value
|
||||||
|
const toggleCamera = () => {
|
||||||
camera.value = camera.value === 'environment' ? 'user' : 'environment'
|
camera.value = camera.value === 'environment' ? 'user' : 'environment'
|
||||||
isBackCamera.value = camera.value === 'environment'
|
isBackCamera.value = camera.value === 'environment'
|
||||||
}
|
}
|
||||||
|
const requestPermission = async () => {
|
||||||
const close = () => {
|
try {
|
||||||
|
await navigator.mediaDevices.getUserMedia({ video: true })
|
||||||
|
errorMessage.value = ''
|
||||||
|
canRetry.value = false
|
||||||
|
} catch {
|
||||||
|
errorMessage.value = 'دسترسی به دوربین رد شد.'
|
||||||
|
canRetry.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const close = () => {
|
||||||
internalShow.value = false
|
internalShow.value = false
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
try {
|
||||||
availableCameras.value = devices.filter(device => device.kind === 'videoinput')
|
const devices = await navigator.mediaDevices.enumerateDevices()
|
||||||
})
|
availableCameras.value = devices.filter(device => device.kind === 'videoinput')
|
||||||
</script>
|
if (availableCameras.value.length === 0) {
|
||||||
|
errorMessage.value = 'هیچ دوربینی در دستگاه شما یافت نشد.'
|
||||||
<style scoped>
|
canRetry.value = false
|
||||||
.scanner-container {
|
emit('error', new Error(errorMessage.value))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
errorMessage.value = 'خطا در بررسی دوربینهای موجود.'
|
||||||
|
canRetry.value = false
|
||||||
|
emit('error', new Error(errorMessage.value))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.scanner-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
background: #000;
|
background: #000;
|
||||||
|
@ -118,15 +174,13 @@ onMounted(async () => {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
.scanner-container video {
|
||||||
.scanner-container video {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
.scanner-overlay {
|
||||||
.scanner-overlay {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -134,9 +188,8 @@ onMounted(async () => {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
.scanner-line {
|
||||||
.scanner-line {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -144,56 +197,27 @@ onMounted(async () => {
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: #00ff00;
|
background: #00ff00;
|
||||||
animation: scan 2s linear infinite;
|
animation: scan 2s linear infinite;
|
||||||
}
|
}
|
||||||
|
.scanner-corner {
|
||||||
.scanner-corner {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-color: #00ff00;
|
border-color: #00ff00;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
}
|
}
|
||||||
|
.scanner-corner.top-left { top: 20%; left: 20%; border-top-width: 4px; border-left-width: 4px; }
|
||||||
.scanner-corner.top-left {
|
.scanner-corner.top-right { top: 20%; right: 20%; border-top-width: 4px; border-right-width: 4px; }
|
||||||
top: 20%;
|
.scanner-corner.bottom-left { bottom: 20%; left: 20%; border-bottom-width: 4px; border-left-width: 4px; }
|
||||||
left: 20%;
|
.scanner-corner.bottom-right { bottom: 20%; right: 20%; border-bottom-width: 4px; border-right-width: 4px; }
|
||||||
border-top-width: 4px;
|
@keyframes scan {
|
||||||
border-left-width: 4px;
|
0% { transform: translateY(-50px); }
|
||||||
}
|
50% { transform: translateY(50px); }
|
||||||
|
100% { transform: translateY(-50px); }
|
||||||
.scanner-corner.top-right {
|
}
|
||||||
top: 20%;
|
.error-box {
|
||||||
right: 20%;
|
padding: 16px;
|
||||||
border-top-width: 4px;
|
color: red;
|
||||||
border-right-width: 4px;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
.scanner-corner.bottom-left {
|
|
||||||
bottom: 20%;
|
|
||||||
left: 20%;
|
|
||||||
border-bottom-width: 4px;
|
|
||||||
border-left-width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scanner-corner.bottom-right {
|
|
||||||
bottom: 20%;
|
|
||||||
right: 20%;
|
|
||||||
border-bottom-width: 4px;
|
|
||||||
border-right-width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scan {
|
|
||||||
0% {
|
|
||||||
transform: translateY(-50px);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
transform: translateY(50px);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: translateY(-50px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -109,10 +109,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-actions">
|
<div class="col-actions">
|
||||||
<v-btn icon size="small" color="primary" variant="text"
|
<!-- <v-btn icon size="small" color="primary" variant="text"
|
||||||
@click="scanner.open = true, scanner.mode = 'serial', scanner.itemIndex = index, scanner.lineIndex = lidx">
|
@click="scanner.open = true, scanner.mode = 'serial', scanner.itemIndex = index, scanner.lineIndex = lidx">
|
||||||
<v-icon>mdi-barcode-scan</v-icon>
|
<v-icon>mdi-barcode-scan</v-icon>
|
||||||
</v-btn>
|
</v-btn> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-warranty">
|
<div class="col-warranty">
|
||||||
|
@ -129,7 +129,7 @@
|
||||||
|
|
||||||
<div class="col-actions">
|
<div class="col-actions">
|
||||||
<v-btn icon size="small" color="secondary" variant="text"
|
<v-btn icon size="small" color="secondary" variant="text"
|
||||||
@click="scanner.open = true, scanner.mode = 'warranty', scanner.itemIndex = index, scanner.lineIndex = lidx">
|
@click="scanner.open = true, scanner.mode = 'serial', scanner.itemIndex = index, scanner.lineIndex = lidx">
|
||||||
<v-icon>mdi-barcode-scan</v-icon>
|
<v-icon>mdi-barcode-scan</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue