code scanner

This commit is contained in:
Julian Krauser 2025-03-24 16:18:23 +01:00
parent 2b3231e26c
commit 2a77a950f5
9 changed files with 312 additions and 91 deletions

View file

@ -0,0 +1,73 @@
<template>
<div class="w-full md:max-w-md">
<XMarkIcon class="ml-auto mb-2 w-5 h-5 cursor-pointer" @click="closeModal" />
<qrcode-stream
:constraints="selectedCamera.constraints"
:track="trackFunctionOptions[4].value"
:formats="barcodeFormats"
:paused="paused"
@error="onError"
@detect="onDetect"
@camera-on="onCameraReady"
/>
<br />
<select v-model="selectedCamera">
<option v-for="c in selecteableCameras" :value="c">{{ c.label }}</option>
</select>
<div class="flex flex-row justify-end gap-4 py-2">
<button primary-outline @click="paused = false" :disabled="!paused">weiter scannen</button>
<button primary-outline @click="commit">bestätigen</button>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "../stores/modal";
import {
barcodeFormats,
defaultConstraintOptions,
getAvailableCameras,
handleScannerError,
trackFunctionOptions,
} from "../helpers/codeScanner";
import { QrcodeStream, type DetectedBarcode } from "vue-qrcode-reader";
import { XMarkIcon } from "@heroicons/vue/24/outline";
</script>
<script lang="ts">
export default defineComponent({
props: {
callback: {
type: Function,
default: (result: string) => {},
},
},
data() {
return {
selecteableCameras: defaultConstraintOptions,
selectedCamera: defaultConstraintOptions[0],
paused: false,
detected: "",
};
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
async onCameraReady() {
this.selecteableCameras = await getAvailableCameras();
},
onDetect(result: Array<DetectedBarcode>) {
this.paused = true;
this.detected = result.map((r) => r.rawValue)[0];
},
onError(err: Error) {
console.log(handleScannerError(err));
},
commit() {
this.callback(this.detected);
this.closeModal();
},
},
});
</script>

View file

@ -9,6 +9,7 @@
<component
:is="component_ref"
:data="data"
:callback="callback"
@click.stop
class="p-4 bg-white rounded-lg max-h-[95%] overflow-y-auto"
/>
@ -23,7 +24,7 @@ import { useModalStore } from "@/stores/modal";
<script lang="ts">
export default {
computed: {
...mapState(useModalStore, ["show", "component_ref", "data"]),
...mapState(useModalStore, ["show", "component_ref", "data", "callback"]),
},
methods: {
...mapActions(useModalStore, ["closeModal"]),

View file

@ -2,6 +2,7 @@
<div class="grow flex flex-col gap-2 overflow-hidden">
<div v-if="useSearch" class="relative self-end flex flex-row items-center gap-2">
<Spinner v-if="deferingSearch" />
<QrCodeIcon v-if="useScanner" class="h-7 cursor-pointer" @click="scanCode" />
<input
type="text"
class="!max-w-64 !w-64 rounded-md shadow-sm relative block px-3 py-2 pr-5 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
@ -66,10 +67,12 @@
</template>
<script setup lang="ts" generic="T extends { id: FieldType }">
import { computed, ref, watch } from "vue";
import { computed, defineAsyncComponent, markRaw, ref, watch } from "vue";
import { ChevronRightIcon, ChevronLeftIcon, XMarkIcon } from "@heroicons/vue/20/solid";
import Spinner from "./Spinner.vue";
import type { FieldType } from "@/types/dynamicQueries";
import { QrCodeIcon } from "@heroicons/vue/24/outline";
import { useModalStore } from "../stores/modal";
const props = defineProps({
items: { type: Array<T>, default: [] },
@ -79,6 +82,7 @@ const props = defineProps({
useSearch: { type: Boolean, default: false },
enablePreSearch: { type: Boolean, default: false },
indicateLoading: { type: Boolean, default: false },
useScanner: { type: Boolean, default: false },
});
const slots = defineSlots<{
@ -183,84 +187,14 @@ const filterData = (array: Array<any>, searchString: string, start: number, end:
)
.filter((elem, index) => (elem?.tab_pos ?? index) >= start && (elem?.tab_pos ?? index) < end);
};
function scanCode() {
useModalStore().openModal(
markRaw(defineAsyncComponent(() => import("@/components/CodeDetector.vue"))),
"pagination",
(result: string) => {
searchString.value = result;
}
);
}
</script>
<!--
<script lang="ts">
export default defineComponent({
computed: {
entryCount() {
return this.totalCount ?? this.items.length;
},
showingStart() {
return this.currentPage * this.maxEntriesPerPage;
},
showingEnd() {
let max = this.currentPage * this.maxEntriesPerPage + this.maxEntriesPerPage;
if (max > this.entryCount) max = this.entryCount;
return max;
},
showingText() {
return `${this.entryCount != 0 ? this.showingStart + 1 : 0} - ${this.showingEnd}`;
},
countOfPages() {
return Math.ceil(this.entryCount / this.maxEntriesPerPage);
},
displayedPagesNumbers(): Array<number | "."> {
//indicate if "." or page number gets pushed
let stateOfPush = false;
return [...new Array(this.countOfPages)].reduce((acc, curr, index) => {
if (
// always display first 2 pages
index <= 1 ||
// always display last 2 pages
index >= this.countOfPages - 2 ||
// always display 1 pages around current page
(this.currentPage - 1 <= index && index <= this.currentPage + 1)
) {
acc.push(index);
stateOfPush = false;
return acc;
}
// abort if placeholder already added to array
if (stateOfPush == true) return acc;
// show placeholder if pagenumber is not actively rendered
acc.push(".");
stateOfPush = true;
return acc;
}, []);
},
visibleRows() {
return this.filterData(this.items, this.searchString, this.showingStart, this.showingEnd);
},
},
methods: {
loadPage(newPage: number | ".") {
if (newPage == ".") return;
if (newPage < 0 || newPage >= this.countOfPages) return;
let pageStart = newPage * this.maxEntriesPerPage;
let pageEnd = newPage * this.maxEntriesPerPage + this.maxEntriesPerPage;
if (pageEnd > this.entryCount) pageEnd = this.entryCount;
let loadedElementCount = this.filterData(this.items, this.searchString, pageStart, pageEnd).length;
if (loadedElementCount < this.maxEntriesPerPage)
this.$emit("loadData", { offset: pageStart, count: this.maxEntriesPerPage, search: this.searchString });
this.currentPage = newPage;
},
filterData(array: Array<any>, searchString: string, start: number, end: number): Array<any> {
return array
.filter(
(elem) =>
!this.enablePreSearch ||
searchString.trim() == "" ||
this.config.some((col) => typeof elem?.[col.key] == "string" && elem[col.key].includes(searchString.trim()))
)
.filter((elem, index) => (elem?.tab_pos ?? index) >= start && (elem?.tab_pos ?? index) < end);
},
},
});
</script> -->