2024-10-11 02:29:29 +02:00
|
|
|
import {
|
|
|
|
|
computed,
|
2025-12-17 10:07:42 +01:00
|
|
|
defineComponent,
|
|
|
|
|
onMounted,
|
|
|
|
|
ref,
|
|
|
|
|
toRefs,
|
|
|
|
|
reactive,
|
|
|
|
|
nextTick,
|
|
|
|
|
} from 'vue';
|
2024-10-11 02:29:29 +02:00
|
|
|
|
2025-12-17 10:07:42 +01:00
|
|
|
import { tools } from '@tools';
|
|
|
|
|
import { costanti } from '@costanti';
|
|
|
|
|
import { useGlobalStore } from '@store/globalStore';
|
|
|
|
|
import { useUserStore } from '@store/UserStore';
|
|
|
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
|
import { toolsext } from '@store/Modules/toolsext';
|
|
|
|
|
import { useQuasar } from 'quasar';
|
|
|
|
|
import { useRouter } from 'vue-router';
|
2024-10-11 02:29:29 +02:00
|
|
|
|
2025-12-17 10:07:42 +01:00
|
|
|
// Import per lettura QR
|
|
|
|
|
import { QrStream, QrCapture, QrDropzone } from 'vue3-qr-reader';
|
2024-10-11 02:29:29 +02:00
|
|
|
|
2025-12-17 10:07:42 +01:00
|
|
|
// Import per generazione QR
|
|
|
|
|
import QRCodeVue3 from 'qrcode-vue3';
|
2024-10-11 02:29:29 +02:00
|
|
|
|
|
|
|
|
export default defineComponent({
|
|
|
|
|
name: 'CQRCode',
|
2025-12-17 10:07:42 +01:00
|
|
|
|
|
|
|
|
components: {
|
|
|
|
|
QrStream,
|
|
|
|
|
QrCapture,
|
|
|
|
|
QrDropzone,
|
|
|
|
|
QRCodeVue3,
|
|
|
|
|
},
|
|
|
|
|
|
2024-10-11 02:29:29 +02:00
|
|
|
props: {
|
|
|
|
|
link: {
|
|
|
|
|
type: String,
|
|
|
|
|
required: false,
|
|
|
|
|
default: '',
|
|
|
|
|
},
|
|
|
|
|
textlink: {
|
|
|
|
|
type: String,
|
|
|
|
|
required: false,
|
|
|
|
|
default: '',
|
|
|
|
|
},
|
2024-10-26 17:12:05 +02:00
|
|
|
imglogo: {
|
|
|
|
|
type: String,
|
|
|
|
|
required: false,
|
|
|
|
|
default: '',
|
|
|
|
|
},
|
2024-10-11 02:29:29 +02:00
|
|
|
read: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
required: false,
|
|
|
|
|
default: false,
|
|
|
|
|
},
|
2025-12-17 10:07:42 +01:00
|
|
|
size: {
|
|
|
|
|
type: Number,
|
|
|
|
|
required: false,
|
|
|
|
|
default: 250,
|
|
|
|
|
},
|
|
|
|
|
primaryColor: {
|
|
|
|
|
type: String,
|
|
|
|
|
required: false,
|
|
|
|
|
default: '#26249a',
|
|
|
|
|
},
|
2024-10-11 02:29:29 +02:00
|
|
|
},
|
|
|
|
|
|
2025-12-17 10:07:42 +01:00
|
|
|
emits: ['decoded', 'error'],
|
|
|
|
|
|
|
|
|
|
setup(props, { emit }) {
|
|
|
|
|
const { t } = useI18n();
|
|
|
|
|
const $q = useQuasar();
|
|
|
|
|
const globalStore = useGlobalStore();
|
|
|
|
|
const userStore = useUserStore();
|
|
|
|
|
const $router = useRouter();
|
|
|
|
|
|
|
|
|
|
const qrDisplayRef = ref<HTMLElement | null>(null);
|
|
|
|
|
|
|
|
|
|
// State
|
2024-10-11 02:29:29 +02:00
|
|
|
const state = reactive({
|
2025-12-17 10:07:42 +01:00
|
|
|
data: null as string | null,
|
|
|
|
|
isDownloading: false,
|
|
|
|
|
uploadedFile: null as File | null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Computed
|
|
|
|
|
const qrSize = computed(() => {
|
|
|
|
|
// Responsive size
|
|
|
|
|
if ($q.screen.lt.sm) {
|
|
|
|
|
return Math.min(props.size, window.innerWidth - 80);
|
|
|
|
|
}
|
|
|
|
|
return props.size;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const logoImage = computed(() => {
|
|
|
|
|
return props.imglogo || tools.getimglogo();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const dotsOptions = computed(() => ({
|
|
|
|
|
type: 'rounded' as const,
|
|
|
|
|
color: props.primaryColor,
|
|
|
|
|
gradient: {
|
|
|
|
|
type: 'linear' as const,
|
|
|
|
|
rotation: 0,
|
|
|
|
|
colorStops: [
|
|
|
|
|
{ offset: 0, color: props.primaryColor },
|
|
|
|
|
{ offset: 1, color: lightenColor(props.primaryColor, 20) },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const canShare = computed(() => {
|
|
|
|
|
return !!navigator.share;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const downloadFilename = computed(() => {
|
|
|
|
|
const username = userStore.my?.username || 'user';
|
|
|
|
|
const timestamp = Date.now();
|
|
|
|
|
return `qrcode-${username}-${timestamp}`;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Methods
|
|
|
|
|
function onDecode(data: string) {
|
|
|
|
|
if (data) {
|
|
|
|
|
state.data = data;
|
|
|
|
|
emit('decoded', data);
|
|
|
|
|
|
|
|
|
|
// Vibrazione feedback su mobile
|
|
|
|
|
if (navigator.vibrate) {
|
|
|
|
|
navigator.vibrate(100);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$q.notify({
|
|
|
|
|
type: 'positive',
|
|
|
|
|
message: 'QR Code rilevato!',
|
|
|
|
|
position: 'top',
|
|
|
|
|
timeout: 2000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleFileUpload(file: File | null) {
|
|
|
|
|
if (!file) return;
|
|
|
|
|
|
|
|
|
|
// Qui potresti usare una libreria per decodificare QR da immagine
|
|
|
|
|
// Per esempio: jsQR
|
|
|
|
|
$q.notify({
|
|
|
|
|
type: 'info',
|
|
|
|
|
message: 'Analisi immagine in corso...',
|
|
|
|
|
position: 'top',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function findCanvas(maxAttempts = 10): Promise<HTMLCanvasElement | null> {
|
|
|
|
|
for (let i = 0; i < maxAttempts; i++) {
|
|
|
|
|
// Cerca in vari modi
|
|
|
|
|
let canvas: HTMLCanvasElement | null = null;
|
|
|
|
|
|
|
|
|
|
// Metodo 1: cerca nel ref
|
|
|
|
|
if (qrDisplayRef.value) {
|
|
|
|
|
canvas = qrDisplayRef.value.querySelector('canvas');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Metodo 2: cerca con classe specifica
|
|
|
|
|
if (!canvas) {
|
|
|
|
|
const qrCode = document.querySelector('.qr-code');
|
|
|
|
|
canvas = qrCode?.querySelector('canvas') || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Metodo 3: cerca nel container .qr-display
|
|
|
|
|
if (!canvas) {
|
|
|
|
|
const container = document.querySelector('.qr-display');
|
|
|
|
|
canvas = container?.querySelector('canvas') || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (canvas && canvas.width > 0 && canvas.height > 0) {
|
|
|
|
|
return canvas;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Aspetta prima del prossimo tentativo
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
|
}
|
2024-10-11 02:29:29 +02:00
|
|
|
|
2025-12-17 10:07:42 +01:00
|
|
|
return null;
|
2024-10-11 02:29:29 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-17 10:07:42 +01:00
|
|
|
// Nuova funzione downloadQR
|
|
|
|
|
async function downloadQR() {
|
|
|
|
|
state.isDownloading = true;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await nextTick();
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
|
|
|
|
|
|
|
|
let dataUrl: string | null = null;
|
|
|
|
|
|
|
|
|
|
// Metodo 1: Cerca l'immagine (QRCodeVue3 genera un <img> con base64)
|
|
|
|
|
const qrDisplay = qrDisplayRef.value || document.querySelector('.qr-display');
|
|
|
|
|
|
|
|
|
|
if (qrDisplay) {
|
|
|
|
|
const img = qrDisplay.querySelector('img') as HTMLImageElement;
|
|
|
|
|
|
|
|
|
|
if (img && img.src && img.src.startsWith('data:image')) {
|
|
|
|
|
console.log('✅ Immagine base64 trovata!');
|
|
|
|
|
dataUrl = img.src;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Metodo 2: Se non trova img, cerca canvas (fallback)
|
|
|
|
|
if (!dataUrl) {
|
|
|
|
|
const canvas = document.querySelector(
|
|
|
|
|
'.qr-display canvas'
|
|
|
|
|
) as HTMLCanvasElement;
|
|
|
|
|
if (canvas && canvas.width > 0) {
|
|
|
|
|
console.log('✅ Canvas trovato come fallback');
|
|
|
|
|
dataUrl = canvas.toDataURL('image/png');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!dataUrl) {
|
|
|
|
|
debugCanvas();
|
|
|
|
|
throw new Error('QR Code non trovato. Riprova.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Download
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
link.href = dataUrl;
|
|
|
|
|
link.download = `${downloadFilename.value}.png`;
|
|
|
|
|
document.body.appendChild(link);
|
|
|
|
|
link.click();
|
|
|
|
|
document.body.removeChild(link);
|
|
|
|
|
|
|
|
|
|
$q.notify({
|
|
|
|
|
type: 'positive',
|
|
|
|
|
message: 'QR Code scaricato!',
|
|
|
|
|
icon: 'download_done',
|
|
|
|
|
position: 'top',
|
|
|
|
|
});
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
console.error('Errore download QR:', error);
|
|
|
|
|
$q.notify({
|
|
|
|
|
type: 'negative',
|
|
|
|
|
message: error.message || 'Errore durante il download',
|
|
|
|
|
position: 'top',
|
|
|
|
|
});
|
|
|
|
|
emit('error', error);
|
|
|
|
|
} finally {
|
|
|
|
|
state.isDownloading = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
async function shareQR() {
|
|
|
|
|
try {
|
|
|
|
|
// Cerca l'immagine base64
|
|
|
|
|
const qrDisplay = qrDisplayRef.value || document.querySelector('.qr-display');
|
|
|
|
|
const img = qrDisplay?.querySelector('img') as HTMLImageElement;
|
|
|
|
|
|
|
|
|
|
if (img && img.src && img.src.startsWith('data:image')) {
|
|
|
|
|
// Converti base64 in blob
|
|
|
|
|
const response = await fetch(img.src);
|
|
|
|
|
const blob = await response.blob();
|
|
|
|
|
|
|
|
|
|
const file = new File([blob], `${downloadFilename.value}.png`, {
|
|
|
|
|
type: 'image/png',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (navigator.canShare && navigator.canShare({ files: [file] })) {
|
|
|
|
|
await navigator.share({
|
|
|
|
|
title: props.textlink || 'QR Code',
|
|
|
|
|
files: [file],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$q.notify({
|
|
|
|
|
type: 'positive',
|
|
|
|
|
message: 'Condivisione avviata!',
|
|
|
|
|
position: 'top',
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback: condividi solo il link
|
|
|
|
|
await navigator.share({
|
|
|
|
|
title: props.textlink || 'QR Code',
|
|
|
|
|
url: props.link,
|
|
|
|
|
});
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.name !== 'AbortError') {
|
|
|
|
|
console.error('Errore condivisione:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
function copyToClipboard(text: string) {
|
|
|
|
|
navigator.clipboard
|
|
|
|
|
.writeText(text)
|
|
|
|
|
.then(() => {
|
|
|
|
|
$q.notify({
|
|
|
|
|
type: 'positive',
|
|
|
|
|
message: 'Copiato negli appunti!',
|
|
|
|
|
icon: 'content_copy',
|
|
|
|
|
position: 'top',
|
|
|
|
|
timeout: 1500,
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
console.error('Errore copia:', err);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isValidUrl(text: string): boolean {
|
|
|
|
|
try {
|
|
|
|
|
new URL(text);
|
|
|
|
|
return true;
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function truncateUrl(url: string, maxLength: number = 400): string {
|
|
|
|
|
if (!url || url.length <= maxLength) return url;
|
|
|
|
|
return url.substring(0, maxLength) + '...';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function lightenColor(color: string, percent: number): string {
|
|
|
|
|
const num = parseInt(color.replace('#', ''), 16);
|
|
|
|
|
const amt = Math.round(2.55 * percent);
|
|
|
|
|
const R = (num >> 16) + amt;
|
|
|
|
|
const G = ((num >> 8) & 0x00ff) + amt;
|
|
|
|
|
const B = (num & 0x0000ff) + amt;
|
|
|
|
|
return (
|
|
|
|
|
'#' +
|
|
|
|
|
(
|
|
|
|
|
0x1000000 +
|
|
|
|
|
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
|
|
|
|
|
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
|
|
|
|
|
(B < 255 ? (B < 1 ? 0 : B) : 255)
|
|
|
|
|
)
|
|
|
|
|
.toString(16)
|
|
|
|
|
.slice(1)
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-10-11 02:29:29 +02:00
|
|
|
|
|
|
|
|
function naviga(path: string) {
|
2025-12-17 10:07:42 +01:00
|
|
|
$router.push(path);
|
2024-10-11 02:29:29 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-17 10:07:42 +01:00
|
|
|
function debugCanvas() {
|
|
|
|
|
console.log('=== DEBUG QR ===');
|
|
|
|
|
console.log('1. qrDisplayRef.value:', qrDisplayRef.value);
|
2024-10-11 02:29:29 +02:00
|
|
|
|
2025-12-17 10:07:42 +01:00
|
|
|
const qrDisplay = qrDisplayRef.value || document.querySelector('.qr-display');
|
|
|
|
|
|
|
|
|
|
if (qrDisplay) {
|
|
|
|
|
// Cerca immagini
|
|
|
|
|
const imgs = qrDisplay.querySelectorAll('img');
|
|
|
|
|
console.log('2. Immagini trovate:', imgs.length);
|
|
|
|
|
imgs.forEach((img, i) => {
|
|
|
|
|
const imgEl = img as HTMLImageElement;
|
|
|
|
|
console.log(
|
|
|
|
|
` Img ${i}: src starts with data:image = ${imgEl.src?.startsWith('data:image')}`
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Cerca canvas
|
|
|
|
|
const canvases = qrDisplay.querySelectorAll('canvas');
|
|
|
|
|
console.log('3. Canvas trovati:', canvases.length);
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
'4. innerHTML (primi 200 char):',
|
|
|
|
|
qrDisplay.innerHTML.substring(0, 200)
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-10-11 02:29:29 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-17 10:07:42 +01:00
|
|
|
// Lifecycle
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
// Inizializzazione se necessaria
|
|
|
|
|
});
|
|
|
|
|
|
2024-10-11 02:29:29 +02:00
|
|
|
return {
|
2025-12-17 10:07:42 +01:00
|
|
|
// Stores
|
|
|
|
|
globalStore,
|
|
|
|
|
userStore,
|
|
|
|
|
|
|
|
|
|
// Utils
|
2024-10-11 02:29:29 +02:00
|
|
|
t,
|
|
|
|
|
tools,
|
|
|
|
|
costanti,
|
|
|
|
|
toolsext,
|
2025-12-17 10:07:42 +01:00
|
|
|
|
|
|
|
|
// State
|
2024-10-11 02:29:29 +02:00
|
|
|
...toRefs(state),
|
2025-12-17 10:07:42 +01:00
|
|
|
|
|
|
|
|
// Computed
|
|
|
|
|
qrSize,
|
|
|
|
|
logoImage,
|
|
|
|
|
dotsOptions,
|
|
|
|
|
canShare,
|
|
|
|
|
downloadFilename,
|
|
|
|
|
primaryColor: props.primaryColor,
|
|
|
|
|
|
|
|
|
|
// Methods
|
2024-10-11 02:29:29 +02:00
|
|
|
onDecode,
|
2025-12-17 10:07:42 +01:00
|
|
|
handleFileUpload,
|
|
|
|
|
qrDisplayRef,
|
|
|
|
|
downloadQR,
|
|
|
|
|
shareQR,
|
|
|
|
|
copyToClipboard,
|
|
|
|
|
isValidUrl,
|
|
|
|
|
truncateUrl,
|
2024-10-11 02:29:29 +02:00
|
|
|
naviga,
|
2025-12-17 10:07:42 +01:00
|
|
|
};
|
2024-10-11 02:29:29 +02:00
|
|
|
},
|
2025-12-17 10:07:42 +01:00
|
|
|
});
|