Xinon mobile/improve
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* SignaturePad Component
|
||||
*
|
||||
* Full-screen signature capture for shipping notes.
|
||||
* Features:
|
||||
* - Canvas-based signature drawing
|
||||
* - Customer name input
|
||||
* - Clear/retry functionality
|
||||
* - Base64 PNG export
|
||||
*/
|
||||
|
||||
import { shippingNoteApi } from '/mobile/modules/lager/shippingnote/ShippingNoteModule.js';
|
||||
|
||||
export default {
|
||||
name: 'SignaturePad',
|
||||
emits: ['close', 'signed', 'toast'],
|
||||
props: {
|
||||
shippingNoteId: [Number, String],
|
||||
shippingNote: Object
|
||||
},
|
||||
|
||||
setup(props, { emit }) {
|
||||
const { ref, onMounted, onUnmounted, nextTick } = Vue;
|
||||
|
||||
// Refs
|
||||
const canvasRef = ref(null);
|
||||
const signatureName = ref('');
|
||||
const loading = ref(false);
|
||||
const hasSignature = ref(false);
|
||||
|
||||
// Canvas context
|
||||
let ctx = null;
|
||||
let isDrawing = false;
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
|
||||
// Initialize canvas
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
initCanvas();
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
const initCanvas = () => {
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
|
||||
// Set canvas size to container
|
||||
const container = canvas.parentElement;
|
||||
canvas.width = container.clientWidth;
|
||||
canvas.height = container.clientHeight;
|
||||
|
||||
ctx = canvas.getContext('2d');
|
||||
ctx.strokeStyle = '#000000';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
// Fill with white background
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
// Save current signature
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
|
||||
const imageData = canvas.toDataURL();
|
||||
|
||||
// Resize canvas
|
||||
initCanvas();
|
||||
|
||||
// Restore signature
|
||||
if (hasSignature.value) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
};
|
||||
img.src = imageData;
|
||||
}
|
||||
};
|
||||
|
||||
// Get position from touch/mouse event
|
||||
const getPosition = (e) => {
|
||||
const canvas = canvasRef.value;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
if (e.touches && e.touches.length > 0) {
|
||||
return {
|
||||
x: e.touches[0].clientX - rect.left,
|
||||
y: e.touches[0].clientY - rect.top
|
||||
};
|
||||
}
|
||||
return {
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top
|
||||
};
|
||||
};
|
||||
|
||||
// Start drawing
|
||||
const startDrawing = (e) => {
|
||||
e.preventDefault();
|
||||
isDrawing = true;
|
||||
const pos = getPosition(e);
|
||||
lastX = pos.x;
|
||||
lastY = pos.y;
|
||||
};
|
||||
|
||||
// Draw
|
||||
const draw = (e) => {
|
||||
if (!isDrawing) return;
|
||||
e.preventDefault();
|
||||
|
||||
const pos = getPosition(e);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(lastX, lastY);
|
||||
ctx.lineTo(pos.x, pos.y);
|
||||
ctx.stroke();
|
||||
|
||||
lastX = pos.x;
|
||||
lastY = pos.y;
|
||||
hasSignature.value = true;
|
||||
};
|
||||
|
||||
// Stop drawing
|
||||
const stopDrawing = () => {
|
||||
isDrawing = false;
|
||||
};
|
||||
|
||||
// Clear canvas
|
||||
const clearCanvas = () => {
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas || !ctx) return;
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
hasSignature.value = false;
|
||||
navigator.vibrate?.([30]);
|
||||
};
|
||||
|
||||
// Submit signature
|
||||
const submitSignature = async () => {
|
||||
if (!hasSignature.value) {
|
||||
emit('toast', 'Bitte unterschreiben', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!signatureName.value.trim()) {
|
||||
emit('toast', 'Bitte Namen eingeben', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const canvas = canvasRef.value;
|
||||
const signatureData = canvas.toDataURL('image/png');
|
||||
|
||||
const data = await shippingNoteApi.post(`sign?id=${props.shippingNoteId}`, {
|
||||
signature: signatureData,
|
||||
signatureName: signatureName.value.trim()
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
emit('signed', data);
|
||||
} else {
|
||||
emit('toast', data.error || 'Fehler beim Speichern', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Signature submit failed:', e);
|
||||
emit('toast', 'Netzwerkfehler', 'error');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Close handler
|
||||
const handleClose = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
return {
|
||||
canvasRef,
|
||||
signatureName,
|
||||
loading,
|
||||
hasSignature,
|
||||
startDrawing,
|
||||
draw,
|
||||
stopDrawing,
|
||||
clearCanvas,
|
||||
submitSignature,
|
||||
handleClose
|
||||
};
|
||||
},
|
||||
|
||||
template: `
|
||||
<div class="flex flex-col h-full bg-white dark:bg-slate-900">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800">
|
||||
<button
|
||||
@click="handleClose"
|
||||
class="p-2 -ml-2 text-slate-500"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<h2 class="text-lg font-semibold text-slate-800 dark:text-white">Unterschrift</h2>
|
||||
<button
|
||||
@click="clearCanvas"
|
||||
class="p-2 -mr-2 text-red-500"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Shipping Note Info -->
|
||||
<div v-if="shippingNote" class="px-4 py-3 bg-slate-100 dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700">
|
||||
<div class="text-sm text-slate-500 dark:text-slate-400">Lieferschein Nr.</div>
|
||||
<div class="font-medium text-slate-800 dark:text-white">{{ shippingNote.number || shippingNote.id }}</div>
|
||||
<div v-if="shippingNote.customerName" class="text-sm text-slate-600 dark:text-slate-300 mt-1">
|
||||
{{ shippingNote.customerName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signature Canvas -->
|
||||
<div class="flex-1 relative bg-white overflow-hidden">
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
class="absolute inset-0 touch-none"
|
||||
@mousedown="startDrawing"
|
||||
@mousemove="draw"
|
||||
@mouseup="stopDrawing"
|
||||
@mouseleave="stopDrawing"
|
||||
@touchstart="startDrawing"
|
||||
@touchmove="draw"
|
||||
@touchend="stopDrawing"
|
||||
@touchcancel="stopDrawing"
|
||||
></canvas>
|
||||
|
||||
<!-- Signature line hint -->
|
||||
<div class="absolute bottom-16 left-8 right-8 border-b-2 border-dashed border-slate-300 pointer-events-none"></div>
|
||||
<div class="absolute bottom-12 left-8 text-xs text-slate-400 pointer-events-none">Hier unterschreiben</div>
|
||||
|
||||
<!-- Empty state hint -->
|
||||
<div v-if="!hasSignature" class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div class="text-center text-slate-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-2 opacity-50" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</svg>
|
||||
<p class="text-sm">Mit Finger unterschreiben</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="p-4 bg-slate-50 dark:bg-slate-800 border-t border-slate-200 dark:border-slate-700 space-y-3">
|
||||
<!-- Name input -->
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500 dark:text-slate-400 mb-1">Name des Unterzeichners *</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="signatureName"
|
||||
class="w-full px-4 py-3 bg-white dark:bg-slate-700 border border-slate-200 dark:border-slate-600 rounded-xl text-base"
|
||||
placeholder="Max Mustermann"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Submit button -->
|
||||
<button
|
||||
@click="submitSignature"
|
||||
:disabled="!hasSignature || loading"
|
||||
:class="[
|
||||
'w-full py-5 rounded-xl text-base font-semibold transition flex items-center justify-center gap-2',
|
||||
hasSignature && !loading
|
||||
? 'bg-gradient-to-r from-green-500 to-emerald-600 text-white shadow-lg active:scale-[0.98]'
|
||||
: 'bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-500'
|
||||
]"
|
||||
>
|
||||
<svg v-if="loading" class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" 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>
|
||||
{{ loading ? 'Wird gespeichert...' : 'Unterschrift speichern' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
Reference in New Issue
Block a user