feat: signature (#194)
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
Some checks failed
Spell Check / Spell Check with Typos (push) Failing after 8s
* 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>
This commit is contained in:
parent
3646956038
commit
0e685a99f7
9 changed files with 378 additions and 149 deletions
|
|
@ -4,7 +4,7 @@ import { useQuasar } from 'quasar';
|
|||
import SignaturePad from 'signature_pad';
|
||||
import Cropper from 'cropperjs';
|
||||
|
||||
defineExpose({ clearCanvas, clearUpload });
|
||||
defineExpose({ setCanvas, getCanvas, clearCanvas, clearUpload });
|
||||
|
||||
const $q = useQuasar();
|
||||
const isDarkActive = computed(() => $q.dark.isActive);
|
||||
|
|
@ -18,7 +18,7 @@ const cropper = ref();
|
|||
|
||||
const tab = ref('draw');
|
||||
const uploadFile = ref<File | undefined>(undefined);
|
||||
const profileUrl = ref<string | null>('');
|
||||
const imgUrl = ref<string | null>('');
|
||||
const inputFile = (() => {
|
||||
const element = document.createElement('input');
|
||||
element.type = 'file';
|
||||
|
|
@ -26,7 +26,7 @@ const inputFile = (() => {
|
|||
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => {
|
||||
if (typeof reader.result === 'string') profileUrl.value = reader.result;
|
||||
if (typeof reader.result === 'string') imgUrl.value = reader.result;
|
||||
});
|
||||
|
||||
element.addEventListener('change', () => {
|
||||
|
|
@ -39,12 +39,11 @@ const inputFile = (() => {
|
|||
return element;
|
||||
})();
|
||||
|
||||
async function initializeSignaturePad(canva?: HTMLCanvasElement) {
|
||||
if (canva) {
|
||||
signaturePad.value = new SignaturePad(canva, {
|
||||
backgroundColor: isDarkActive.value
|
||||
? 'rgb(21,25,29)'
|
||||
: 'rgb(248,249,250)',
|
||||
async function initializeSignaturePad() {
|
||||
const canvas = canvasRef.value;
|
||||
|
||||
if (canvas) {
|
||||
signaturePad.value = new SignaturePad(canvas, {
|
||||
penColor: 'blue',
|
||||
});
|
||||
} else {
|
||||
|
|
@ -77,34 +76,41 @@ function changeColor(color: string) {
|
|||
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() {
|
||||
profileUrl.value = '';
|
||||
imgUrl.value = '';
|
||||
}
|
||||
|
||||
watch(
|
||||
() => tab.value,
|
||||
async () => {
|
||||
await initializeSignaturePad(canvasRef.value);
|
||||
await initializeCropper(imageRef.value);
|
||||
await initializeSignaturePad();
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await initializeSignaturePad(canvasRef.value);
|
||||
await initializeCropper(imageRef.value);
|
||||
await initializeSignaturePad();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="surface-1 bordered rounded full-width">
|
||||
<div class="surface-1 column full-width full-height">
|
||||
<q-tabs
|
||||
v-model="tab"
|
||||
dense
|
||||
align="left"
|
||||
class="text-grey"
|
||||
class="text-grey surface-2"
|
||||
active-color="primary"
|
||||
indicator-color="primary"
|
||||
>
|
||||
|
|
@ -112,18 +118,18 @@ onMounted(async () => {
|
|||
<div class="row">
|
||||
<q-tab
|
||||
name="draw"
|
||||
label="Draw"
|
||||
:label="$t('general.draw')"
|
||||
style="border-top-left-radius: var(--radius-2)"
|
||||
/>
|
||||
<q-tab name="upload" label="Upload" />
|
||||
<q-tab name="upload" :label="$t('general.upload')" />
|
||||
</div>
|
||||
|
||||
<div class="q-pr-md">
|
||||
<q-btn
|
||||
v-if="tab === 'upload'"
|
||||
dense
|
||||
flat
|
||||
v-if="tab === 'upload'"
|
||||
:label="$t('newUpload')"
|
||||
:label="$t('general.newUpload')"
|
||||
color="info"
|
||||
@click="inputFile.click()"
|
||||
/>
|
||||
|
|
@ -132,89 +138,66 @@ onMounted(async () => {
|
|||
</q-tabs>
|
||||
<q-separator />
|
||||
|
||||
<div v-show="tab === 'draw'" class="q-pa-md">
|
||||
<section v-show="tab === 'draw'" class="q-pa-md col">
|
||||
<div class="column relative-position">
|
||||
<div class="absolute-top-right q-ma-md q-gutter-x-md row items-center">
|
||||
<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"
|
||||
:class="{ active: currentColor === 'black' }"
|
||||
style="background-color: black"
|
||||
@click="changeColor('black')"
|
||||
:style="`background-color: ${color}`"
|
||||
@click="changeColor(color)"
|
||||
>
|
||||
<q-icon
|
||||
v-if="currentColor === 'black'"
|
||||
v-if="currentColor === color"
|
||||
name="mdi-check"
|
||||
color="white"
|
||||
size="sm"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
:class="{ active: currentColor === 'red' }"
|
||||
class="dot"
|
||||
style="background-color: red"
|
||||
@click="changeColor('red')"
|
||||
>
|
||||
<q-icon
|
||||
v-if="currentColor === 'red'"
|
||||
name="mdi-check"
|
||||
color="white"
|
||||
size="sm"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
:class="{ active: currentColor === 'blue' }"
|
||||
class="dot"
|
||||
style="background-color: blue"
|
||||
@click="changeColor('blue')"
|
||||
>
|
||||
<q-icon
|
||||
v-if="currentColor === 'blue'"
|
||||
name="mdi-check"
|
||||
color="white"
|
||||
size="sm"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<canvas
|
||||
class="signature-canvas"
|
||||
ref="canvasRef"
|
||||
id="signature-pad"
|
||||
width="700"
|
||||
height="310"
|
||||
width="766"
|
||||
height="364"
|
||||
></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-show="tab === 'upload'" class="q-pa-md">
|
||||
<section v-show="tab === 'upload'" class="q-pa-md col">
|
||||
<div
|
||||
class="bordered upload-border rounded column items-center justify-center"
|
||||
style="height: 312px"
|
||||
class="bordered upload-border rounded column items-center justify-center full-height"
|
||||
>
|
||||
<q-img
|
||||
v-show="profileUrl"
|
||||
v-show="imgUrl"
|
||||
ref="imageRef"
|
||||
:src="profileUrl ?? ''"
|
||||
:src="imgUrl ?? ''"
|
||||
style="object-fit: cover; width: 100%; height: 100%"
|
||||
/>
|
||||
<div v-if="!profileUrl">
|
||||
<div v-if="!imgUrl">
|
||||
<q-icon
|
||||
name="mdi-cloud-upload"
|
||||
size="10rem"
|
||||
style="color: hsla(var(--text-mute) / 0.2)"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="info"
|
||||
:label="$t('uploadFile')"
|
||||
:label="$t('general.upload')"
|
||||
icon="mdi-plus"
|
||||
@click="inputFile.click()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
|
|
|
|||
|
|
@ -151,6 +151,8 @@ export default {
|
|||
dueDate: 'Due date',
|
||||
year: 'year',
|
||||
tableOfContent: 'Table of Contents',
|
||||
draw: 'Draw',
|
||||
newUpload: 'New Upload',
|
||||
},
|
||||
|
||||
menu: {
|
||||
|
|
|
|||
|
|
@ -151,6 +151,8 @@ export default {
|
|||
dueDate: 'วันครบกำหนด',
|
||||
year: 'ปี',
|
||||
tableOfContent: 'สารบัญ',
|
||||
draw: 'วาด',
|
||||
newUpload: 'อัปโหลดใหม่',
|
||||
},
|
||||
|
||||
menu: {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ const leftDrawerMini = ref(false);
|
|||
const unread = computed<number>(
|
||||
() => notificationData.value.filter((v) => !v.read).length || 0,
|
||||
);
|
||||
// const filterRole = ref<string[]>();
|
||||
const userImage = ref<string>();
|
||||
const userGender = ref('');
|
||||
const canvasRef = ref();
|
||||
|
|
@ -124,6 +123,17 @@ function readNoti(id: string) {
|
|||
}
|
||||
}
|
||||
|
||||
function signatureSubmit() {
|
||||
const signature = canvasRef.value.setCanvas();
|
||||
userStore.setSignature(signature);
|
||||
canvasModal.value = false;
|
||||
}
|
||||
|
||||
async function signatureFetch() {
|
||||
const ret = await userStore.getSignature();
|
||||
if (ret) canvasRef.value.getCanvas(ret);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
initTheme();
|
||||
initLang();
|
||||
|
|
@ -502,13 +512,15 @@ onMounted(async () => {
|
|||
no-app-box
|
||||
:title="$t('menu.profile.addSignature')"
|
||||
:close="() => (canvasModal = false)"
|
||||
:submit="signatureSubmit"
|
||||
:show="signatureFetch"
|
||||
>
|
||||
<CanvasComponent ref="canvasRef" v-model:modal="canvasModal" />
|
||||
<template #footer>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
:label="$t('clear')"
|
||||
:label="$t('general.clear')"
|
||||
@click="
|
||||
() => {
|
||||
canvasRef.clearCanvas(), canvasRef.clearUpload();
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ const options = [
|
|||
label: 'menu.profile.signature',
|
||||
value: 'signature',
|
||||
color: 'grey',
|
||||
disabled: true,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: 'mdi-brightness-6',
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
import axios from 'axios';
|
||||
import useBranchStore from '../branch';
|
||||
import { Branch } from '../branch/types';
|
||||
import { getSignature, setSignature } from './signature';
|
||||
|
||||
const branchStore = useBranchStore();
|
||||
|
||||
|
|
@ -327,6 +328,9 @@ const useUserStore = defineStore('api-user', () => {
|
|||
addAttachment,
|
||||
deleteAttachment,
|
||||
|
||||
getSignature,
|
||||
setSignature,
|
||||
|
||||
typeStats,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
50
src/stores/user/signature.ts
Normal file
50
src/stores/user/signature.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import axios from 'axios';
|
||||
import { api } from 'src/boot/axios';
|
||||
import { getUserId } from 'src/services/keycloak';
|
||||
|
||||
export async function getSignature() {
|
||||
const userId = getUserId();
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
const responseSignature = await api.get<string>(
|
||||
'/user/' + userId + '/signature',
|
||||
);
|
||||
|
||||
if (!responseSignature.data) return '';
|
||||
|
||||
const responseBlob = await axios.get<Blob>(responseSignature.data, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
if (responseBlob.status < 400) {
|
||||
return await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('error', reject);
|
||||
reader.addEventListener('load', () => resolve(reader.result as string));
|
||||
reader.readAsDataURL(responseBlob.data);
|
||||
});
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export async function setSignature(image: string) {
|
||||
const userId = getUserId();
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
if (image === '') {
|
||||
return await deleteSignature();
|
||||
} else {
|
||||
return await api.put('/user/' + userId + '/signature', { data: image });
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteSignature() {
|
||||
const userId = getUserId();
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
return await api.delete('/user/' + userId + '/signature');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue