feat: agregar funcionalidad de QR codes con impresión y descarga PNG
- Click derecho en UUIDs muestra menú contextual con opción de imprimir QR - Modal de vista previa con diseño portrait optimizado para móviles - Generación de QR codes que redirigen a la URL específica del UUID - Funcionalidad de impresión directa con estilos embebidos - Descarga como PNG de alta resolución con todos los estilos preservados - Diseño de tarjeta con gradiente, información del jugador e instrucciones - Librerías: qrcode para generación QR, html2canvas para captura PNG
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
class="uuid-card"
|
||||
:class="{ 'has-name': uuidInfo.hasName }"
|
||||
@click="selectUuid(uuidInfo.uuid)"
|
||||
@contextmenu.prevent="showContextMenu($event, uuidInfo)"
|
||||
:style="{ animationDelay: `${index * 0.02}s` }"
|
||||
>
|
||||
<div class="uuid-number">{{ getUuidIndex(uuidInfo.uuid) }}</div>
|
||||
@@ -31,6 +32,48 @@
|
||||
<div class="uuid-text">{{ formatUuid(uuidInfo.uuid) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Context Menu -->
|
||||
<div
|
||||
v-if="contextMenu.visible"
|
||||
class="context-menu"
|
||||
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
|
||||
@click="contextMenu.visible = false"
|
||||
>
|
||||
<div class="context-menu-item" @click="printQR(contextMenu.uuid)">
|
||||
🖨️ Imprimir QR
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Print Modal -->
|
||||
<div v-if="printModal.visible" class="print-modal-overlay" @click="closePrintModal">
|
||||
<div class="print-modal-content" @click.stop>
|
||||
<button class="close-button" @click="closePrintModal">✕</button>
|
||||
<div class="print-container" ref="printContainer">
|
||||
<div class="qr-print-page">
|
||||
<div class="qr-header">
|
||||
<h2>🎮 Snatch Game</h2>
|
||||
<p class="player-info">{{ printModal.name || 'Jugador' }}</p>
|
||||
</div>
|
||||
<div class="qr-code-container">
|
||||
<canvas ref="qrCanvas"></canvas>
|
||||
</div>
|
||||
<div class="qr-footer">
|
||||
<p class="uuid-display">UUID: {{ printModal.uuidShort }}</p>
|
||||
<p class="url-display">{{ printModal.url }}</p>
|
||||
<div class="instructions">
|
||||
<p>📱 Escanea este código QR</p>
|
||||
<p>para acceder al juego</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-buttons">
|
||||
<button class="btn-download" @click="downloadPNG">📥 Descargar PNG</button>
|
||||
<button class="btn-print" @click="executePrint">🖨️ Imprimir</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="loading">
|
||||
<div class="spinner"></div>
|
||||
@@ -50,8 +93,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, onMounted, nextTick } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import QRCode from 'qrcode';
|
||||
import html2canvas from 'html2canvas';
|
||||
|
||||
interface UuidInfo {
|
||||
uuid: string;
|
||||
@@ -64,6 +109,25 @@ const loading = ref(true);
|
||||
const allUuids = ref<UuidInfo[]>([]);
|
||||
const searchQuery = ref('');
|
||||
const filteredUuids = ref<UuidInfo[]>([]);
|
||||
const qrCanvas = ref<HTMLCanvasElement>();
|
||||
const printContainer = ref<HTMLElement>();
|
||||
|
||||
// Context menu state
|
||||
const contextMenu = ref({
|
||||
visible: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
uuid: null as UuidInfo | null
|
||||
});
|
||||
|
||||
// Print modal state
|
||||
const printModal = ref({
|
||||
visible: false,
|
||||
uuid: '',
|
||||
name: '',
|
||||
uuidShort: '',
|
||||
url: ''
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
@@ -116,6 +180,280 @@ function selectRandom() {
|
||||
function goToDashboard() {
|
||||
router.push('/dashboard');
|
||||
}
|
||||
|
||||
// Context menu functions
|
||||
function showContextMenu(event: MouseEvent, uuidInfo: UuidInfo) {
|
||||
contextMenu.value = {
|
||||
visible: true,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
uuid: uuidInfo
|
||||
};
|
||||
}
|
||||
|
||||
// Hide context menu when clicking elsewhere
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', () => {
|
||||
contextMenu.value.visible = false;
|
||||
});
|
||||
});
|
||||
|
||||
// QR Print functions
|
||||
async function printQR(uuidInfo: UuidInfo | null) {
|
||||
if (!uuidInfo) return;
|
||||
|
||||
contextMenu.value.visible = false;
|
||||
|
||||
// Get the base URL (production or development)
|
||||
const baseUrl = window.location.origin;
|
||||
const fullUrl = `${baseUrl}/${uuidInfo.uuid}`;
|
||||
|
||||
printModal.value = {
|
||||
visible: true,
|
||||
uuid: uuidInfo.uuid,
|
||||
name: uuidInfo.name || '',
|
||||
uuidShort: uuidInfo.uuid.substring(0, 8),
|
||||
url: fullUrl
|
||||
};
|
||||
|
||||
// Wait for modal to render
|
||||
await nextTick();
|
||||
|
||||
// Generate QR code
|
||||
if (qrCanvas.value) {
|
||||
await QRCode.toCanvas(qrCanvas.value, fullUrl, {
|
||||
width: 256,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
light: '#FFFFFF'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function closePrintModal() {
|
||||
printModal.value.visible = false;
|
||||
}
|
||||
|
||||
function executePrint() {
|
||||
if (!printContainer.value) return;
|
||||
|
||||
const printContent = printContainer.value.innerHTML;
|
||||
const printWindow = window.open('', '_blank', 'width=400,height=600');
|
||||
|
||||
if (!printWindow) return;
|
||||
|
||||
printWindow.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>QR Code - ${printModal.value.name || printModal.value.uuidShort}</title>
|
||||
<style>
|
||||
@page {
|
||||
size: portrait;
|
||||
margin: 0;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
.qr-print-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 40px 20px;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
.qr-header {
|
||||
text-align: center;
|
||||
}
|
||||
.qr-header h2 {
|
||||
font-size: 36px;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
.player-info {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
.qr-code-container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
}
|
||||
.qr-code-container canvas {
|
||||
display: block;
|
||||
}
|
||||
.qr-footer {
|
||||
text-align: center;
|
||||
}
|
||||
.uuid-display {
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
margin: 10px 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.url-display {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
margin: 5px 0;
|
||||
}
|
||||
.instructions {
|
||||
margin-top: 20px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.instructions p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${printContent}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
|
||||
printWindow.document.close();
|
||||
printWindow.focus();
|
||||
|
||||
// Add canvas to print window
|
||||
const printCanvas = printWindow.document.querySelector('canvas');
|
||||
if (printCanvas && qrCanvas.value) {
|
||||
const ctx = printCanvas.getContext('2d');
|
||||
if (ctx) {
|
||||
printCanvas.width = qrCanvas.value.width;
|
||||
printCanvas.height = qrCanvas.value.height;
|
||||
ctx.drawImage(qrCanvas.value, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
printWindow.print();
|
||||
printWindow.close();
|
||||
}, 250);
|
||||
}
|
||||
|
||||
async function downloadPNG() {
|
||||
if (!printContainer.value || !qrCanvas.value) return;
|
||||
|
||||
try {
|
||||
// Create a dedicated container for PNG generation
|
||||
const pngContainer = document.createElement('div');
|
||||
pngContainer.style.position = 'absolute';
|
||||
pngContainer.style.left = '-9999px';
|
||||
pngContainer.style.top = '0';
|
||||
pngContainer.style.width = '400px';
|
||||
pngContainer.style.height = '600px';
|
||||
|
||||
// Create the QR card HTML with inline styles
|
||||
const qrDataURL = qrCanvas.value.toDataURL('image/png');
|
||||
|
||||
pngContainer.innerHTML = `
|
||||
<div style="
|
||||
width: 400px;
|
||||
height: 600px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 40px 20px;
|
||||
box-sizing: border-box;
|
||||
color: white;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
">
|
||||
<div style="text-align: center;">
|
||||
<h2 style="
|
||||
font-size: 36px;
|
||||
margin: 0 0 10px 0;
|
||||
font-weight: bold;
|
||||
">🎮 Snatch Game</h2>
|
||||
<p style="
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
">${printModal.value.name || 'Jugador'}</p>
|
||||
</div>
|
||||
|
||||
<div style="
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
">
|
||||
<img src="${qrDataURL}" style="
|
||||
display: block;
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
" />
|
||||
</div>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<p style="
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
margin: 10px 0;
|
||||
opacity: 0.9;
|
||||
">UUID: ${printModal.value.uuidShort}</p>
|
||||
<p style="
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
margin: 5px 0;
|
||||
">${printModal.value.url}</p>
|
||||
<div style="margin-top: 20px;">
|
||||
<p style="
|
||||
margin: 5px 0;
|
||||
font-size: 18px;
|
||||
">📱 Escanea este código QR</p>
|
||||
<p style="
|
||||
margin: 5px 0;
|
||||
font-size: 18px;
|
||||
">para acceder al juego</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(pngContainer);
|
||||
|
||||
// Capture with html2canvas
|
||||
const canvas = await html2canvas(pngContainer, {
|
||||
width: 400,
|
||||
height: 600,
|
||||
scale: 2,
|
||||
backgroundColor: null,
|
||||
useCORS: true,
|
||||
allowTaint: false,
|
||||
logging: false,
|
||||
removeContainer: false
|
||||
});
|
||||
|
||||
// Remove the temporary container
|
||||
document.body.removeChild(pngContainer);
|
||||
|
||||
// Create download link
|
||||
const link = document.createElement('a');
|
||||
link.download = `qr-code-${printModal.value.name || printModal.value.uuidShort}.png`;
|
||||
link.href = canvas.toDataURL('image/png', 1.0);
|
||||
|
||||
// Trigger download
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating PNG:', error);
|
||||
alert('Error al generar la imagen PNG');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -355,6 +693,175 @@ function goToDashboard() {
|
||||
box-shadow: 0 5px 15px rgba(79, 172, 254, 0.4);
|
||||
}
|
||||
|
||||
/* Context Menu */
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
padding: 8px 0;
|
||||
z-index: 1000;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.context-menu-item {
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.context-menu-item:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Print Modal */
|
||||
.print-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.print-modal-content {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.print-container {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.qr-print-page {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-header h2 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.player-info {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.qr-code-container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 15px;
|
||||
display: inline-block;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.qr-code-container canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.qr-footer {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.uuid-display {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
opacity: 0.9;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.url-display {
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.instructions p {
|
||||
margin: 5px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn-download,
|
||||
.btn-print {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-download {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-download:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(79, 172, 254, 0.4);
|
||||
}
|
||||
|
||||
.btn-print {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-print:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.selector-card {
|
||||
@@ -377,5 +884,14 @@ function goToDashboard() {
|
||||
.btn-dashboard {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.print-modal-content {
|
||||
width: 95%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user