jws-frontend/src/components/CanvasComponent.vue
Methapon Metanipat 0e685a99f7
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
feat: signature (#194)
* refactor: enable profile signature option in ProfileMenu

* feat: add signature api function

* refactor: add new translation keys for 'Draw' and 'New Upload' in English and Thai

* refactor: update image URL variable and improve translation keys in CanvasComponent and MainLayout

* refactor: get function

* feat: add delete signature function

* feat: add canvas manipulation functions and integrate signature submission in MainLayout (unfinished)

* chore(deps): update

---------

Co-authored-by: puriphatt <puriphat@frappet.com>
Co-authored-by: Methapon2001 <61303214+Methapon2001@users.noreply.github.com>
2025-03-27 09:01:42 +07:00

232 lines
5.5 KiB
Vue

<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue';
import { useQuasar } from 'quasar';
import SignaturePad from 'signature_pad';
import Cropper from 'cropperjs';
defineExpose({ setCanvas, getCanvas, clearCanvas, clearUpload });
const $q = useQuasar();
const isDarkActive = computed(() => $q.dark.isActive);
const canvasRef = ref<HTMLCanvasElement>();
const signaturePad = ref();
const currentColor = ref('blue');
const imageRef = ref<HTMLImageElement>();
const cropper = ref();
const tab = ref('draw');
const uploadFile = ref<File | undefined>(undefined);
const imgUrl = ref<string | null>('');
const inputFile = (() => {
const element = document.createElement('input');
element.type = 'file';
element.accept = 'image/*';
const reader = new FileReader();
reader.addEventListener('load', () => {
if (typeof reader.result === 'string') imgUrl.value = reader.result;
});
element.addEventListener('change', () => {
uploadFile.value = element.files?.[0];
if (uploadFile.value) {
reader.readAsDataURL(uploadFile.value);
}
});
return element;
})();
async function initializeSignaturePad() {
const canvas = canvasRef.value;
if (canvas) {
signaturePad.value = new SignaturePad(canvas, {
penColor: 'blue',
});
} else {
console.warn('Canvas reference not found. SignaturePad not initialized.');
}
}
async function initializeCropper(image?: HTMLImageElement) {
console.log(image);
if (image) {
cropper.value = new Cropper(image, {
aspectRatio: 16 / 9,
crop(event) {
console.log(event.detail.x);
console.log(event.detail.y);
console.log(event.detail.width);
console.log(event.detail.height);
console.log(event.detail.rotate);
console.log(event.detail.scaleX);
console.log(event.detail.scaleY);
},
});
} else {
console.warn('Canvas reference not found. Cropper not initialized.');
}
}
function changeColor(color: string) {
signaturePad.value.penColor = color;
currentColor.value = color;
}
function setCanvas() {
const data = signaturePad.value.toDataURL('image/png');
return data;
}
function getCanvas(signature: string) {
signaturePad.value.fromDataURL(signature);
}
function clearCanvas() {
signaturePad.value.clear();
}
function clearUpload() {
imgUrl.value = '';
}
watch(
() => tab.value,
async () => {
await initializeSignaturePad();
},
);
onMounted(async () => {
await initializeSignaturePad();
});
</script>
<template>
<div class="surface-1 column full-width full-height">
<q-tabs
v-model="tab"
dense
align="left"
class="text-grey surface-2"
active-color="primary"
indicator-color="primary"
>
<div class="row justify-between full-width items-center">
<div class="row">
<q-tab
name="draw"
:label="$t('general.draw')"
style="border-top-left-radius: var(--radius-2)"
/>
<q-tab name="upload" :label="$t('general.upload')" />
</div>
<div class="q-pr-md">
<q-btn
v-if="tab === 'upload'"
dense
flat
:label="$t('general.newUpload')"
color="info"
@click="inputFile.click()"
/>
</div>
</div>
</q-tabs>
<q-separator />
<section v-show="tab === 'draw'" class="q-pa-md col">
<div class="column relative-position">
<article
class="absolute-top-right q-ma-md q-gutter-x-md row items-center"
>
<span
v-for="color in ['black', 'red', 'blue']"
:key="color"
:class="{ active: currentColor === color }"
class="dot"
:style="`background-color: ${color}`"
@click="changeColor(color)"
>
<q-icon
v-if="currentColor === color"
name="mdi-check"
color="white"
size="sm"
/>
</span>
</article>
<canvas
class="signature-canvas"
ref="canvasRef"
id="signature-pad"
width="766"
height="364"
></canvas>
</div>
</section>
<section v-show="tab === 'upload'" class="q-pa-md col">
<div
class="bordered upload-border rounded column items-center justify-center full-height"
>
<q-img
v-show="imgUrl"
ref="imageRef"
:src="imgUrl ?? ''"
style="object-fit: cover; width: 100%; height: 100%"
/>
<div v-if="!imgUrl">
<q-icon
name="mdi-cloud-upload"
size="10rem"
style="color: hsla(var(--text-mute) / 0.2)"
/>
<div class="text-center">
<q-btn
unelevated
color="info"
:label="$t('general.upload')"
icon="mdi-plus"
@click="inputFile.click()"
/>
</div>
</div>
</div>
</section>
</div>
</template>
<style scoped lang="scss">
.signature-canvas {
border: 1px solid var(--border-color);
border-radius: var(--radius-2);
}
.dot {
height: 25px;
width: 25px;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
&.active {
height: 35px;
width: 35px;
}
}
.color-palette {
position: absolute;
display: inline-block;
}
.upload-border {
border-style: dashed;
border-color: hsl(var(--info-bg));
}
</style>