@ramathibodi/nuxt-commons 0.1.34 → 0.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/runtime/components/form/Dialog.vue +1 -2
- package/dist/runtime/components/form/File.vue +123 -89
- package/dist/runtime/components/form/Iterator.vue +7 -1
- package/dist/runtime/components/form/SignPad.vue +160 -121
- package/dist/runtime/components/master/label.vue +37 -0
- package/dist/runtime/components/model/Pad.vue +35 -29
- package/dist/runtime/components/model/label.vue +41 -0
- package/dist/runtime/components/pdf/View.vue +32 -12
- package/dist/runtime/composables/graphqlOperation.js +1 -1
- package/package.json +2 -2
package/dist/module.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import { uniqWith
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import {isEqual, uniqWith} from 'lodash-es'
|
|
3
|
+
import {ref, watch} from 'vue'
|
|
4
|
+
import {VTextField} from 'vuetify/components/VTextField'
|
|
5
|
+
import {useAlert} from '../../composables/alert'
|
|
6
6
|
|
|
7
7
|
const alert = useAlert()
|
|
8
8
|
|
|
9
9
|
interface Base64String {
|
|
10
10
|
base64String?: string
|
|
11
11
|
fileName: string
|
|
12
|
+
originalFileName?: string
|
|
12
13
|
id?: number
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -16,22 +17,24 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VTextField['$props
|
|
|
16
17
|
accept?: string
|
|
17
18
|
multiple?: boolean
|
|
18
19
|
maxSize?: number
|
|
19
|
-
modelValue?: Base64String[]
|
|
20
|
+
modelValue?: Base64String | Base64String[]
|
|
21
|
+
downloadable?: boolean
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
const props = withDefaults(defineProps<Props>(), {
|
|
23
25
|
accept: '*',
|
|
24
26
|
multiple: false,
|
|
25
27
|
maxSize: 5,
|
|
28
|
+
downloadable: false
|
|
26
29
|
})
|
|
27
30
|
|
|
28
31
|
const emit = defineEmits<{
|
|
29
|
-
(e: 'update:modelValue', value: Base64String[]): void
|
|
32
|
+
(e: 'update:modelValue', value: Base64String | Base64String[]): void
|
|
30
33
|
}>()
|
|
31
34
|
|
|
32
35
|
const allFiles = ref<File[]>([])
|
|
33
36
|
const allAssets = ref<Base64String[]>([])
|
|
34
|
-
const combinedBase64String = ref<Base64String[]>([])
|
|
37
|
+
const combinedBase64String = ref<Base64String[] | Base64String>([])
|
|
35
38
|
const fileInput = ref()
|
|
36
39
|
|
|
37
40
|
function openWindowUpload() {
|
|
@@ -47,141 +50,172 @@ function addFiles(files: File | File[]) {
|
|
|
47
50
|
|
|
48
51
|
function removeFileByIndex(i: number | string) {
|
|
49
52
|
const index = Number(i)
|
|
50
|
-
if (
|
|
51
|
-
if (index >= 0 && index < allFiles.value.length) allFiles.value.splice(index, 1)
|
|
52
|
-
}
|
|
53
|
+
if (index >= 0 && index < allFiles.value.length) allFiles.value.splice(index, 1)
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
function removeAssetByIndex(i: number | string) {
|
|
56
57
|
const index = Number(i)
|
|
57
|
-
if (
|
|
58
|
-
if (index >= 0 && index < allAssets.value.length) allAssets.value.splice(index, 1)
|
|
59
|
-
}
|
|
58
|
+
if (index >= 0 && index < allAssets.value.length) allAssets.value.splice(index, 1)
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
function fileToBase64(file: File) {
|
|
63
62
|
const maxSize = props.maxSize * 1048576
|
|
64
63
|
|
|
65
64
|
return new Promise<Base64String>((resolve, reject) => {
|
|
66
|
-
if (file.size > maxSize) reject
|
|
65
|
+
if (file.size > maxSize) reject(`File (${file.name}) size exceeds the ${props.maxSize} MB limit.`)
|
|
67
66
|
|
|
68
67
|
const reader = new FileReader()
|
|
69
|
-
reader.onload =
|
|
68
|
+
reader.onload = (event) => {
|
|
70
69
|
resolve({ fileName: file.name, base64String: event.target?.result as string })
|
|
71
70
|
}
|
|
72
|
-
reader.onerror =
|
|
73
|
-
reject(error)
|
|
74
|
-
}
|
|
71
|
+
reader.onerror = reject
|
|
75
72
|
reader.readAsDataURL(file)
|
|
76
73
|
})
|
|
77
74
|
}
|
|
78
75
|
|
|
79
|
-
function base64ToFile(base64Data: string, filename: string) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
function base64ToFile(base64Data: string, filename: string, defaultContentType: string = "application/octet-stream") {
|
|
77
|
+
const matchResult = base64Data.match(/data:([^;]*);base64,(.*)/);
|
|
78
|
+
let contentType: string;
|
|
79
|
+
let base64Payload: string;
|
|
80
|
+
|
|
81
|
+
if (matchResult) {
|
|
82
|
+
[contentType, base64Payload] = matchResult.slice(1)
|
|
83
|
+
} else {
|
|
84
|
+
contentType = defaultContentType
|
|
85
|
+
base64Payload = base64Data
|
|
84
86
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
bytes
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const binaryStr = atob(base64Payload)
|
|
90
|
+
const bytes = new Uint8Array(binaryStr.length)
|
|
91
|
+
for (let i = 0; i < binaryStr.length; i++) {
|
|
92
|
+
bytes[i] = binaryStr.charCodeAt(i)
|
|
93
|
+
}
|
|
94
|
+
return new File([bytes], filename, { type: contentType });
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error("Invalid base64 data", error);
|
|
97
|
+
return undefined;
|
|
93
98
|
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function downloadBase64File(base64Data: string, filename: string): void {
|
|
102
|
+
const file = base64ToFile(base64Data,filename)
|
|
94
103
|
|
|
95
|
-
|
|
104
|
+
if (file) {
|
|
105
|
+
const link = document.createElement("a");
|
|
106
|
+
link.href = URL.createObjectURL(file);
|
|
107
|
+
link.download = filename;
|
|
108
|
+
|
|
109
|
+
// Append the link to the body temporarily and trigger the download
|
|
110
|
+
document.body.appendChild(link);
|
|
111
|
+
link.click();
|
|
112
|
+
|
|
113
|
+
// Cleanup
|
|
114
|
+
document.body.removeChild(link);
|
|
115
|
+
URL.revokeObjectURL(link.href);
|
|
116
|
+
}
|
|
96
117
|
}
|
|
97
118
|
|
|
119
|
+
|
|
98
120
|
watch(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
121
|
+
() => props.modelValue,
|
|
122
|
+
() => {
|
|
123
|
+
if (Array.isArray(props.modelValue)) {
|
|
124
|
+
allAssets.value = props.modelValue.filter((item) => item.id !== undefined)
|
|
125
|
+
allFiles.value = props.modelValue
|
|
126
|
+
.filter((item) => item.id === undefined && item.base64String !== undefined)
|
|
127
|
+
.map((base64) => base64ToFile(base64.base64String as string, base64.fileName))
|
|
128
|
+
.filter((item) => item !== undefined) as File[]
|
|
129
|
+
} else if (props.modelValue) {
|
|
130
|
+
allAssets.value = props.modelValue.id !== undefined ? [props.modelValue] : []
|
|
131
|
+
allFiles.value =
|
|
132
|
+
props.modelValue.id === undefined && props.modelValue.base64String !== undefined
|
|
133
|
+
? [base64ToFile(props.modelValue.base64String, props.modelValue.fileName)].filter(
|
|
134
|
+
(item) => item !== undefined
|
|
135
|
+
) as File[]
|
|
136
|
+
: []
|
|
137
|
+
} else {
|
|
138
|
+
allAssets.value = []
|
|
139
|
+
allFiles.value = []
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
{ deep: true, immediate: true }
|
|
111
143
|
)
|
|
112
144
|
|
|
113
145
|
watch([allAssets, allFiles], () => {
|
|
114
|
-
if (allFiles.value
|
|
115
|
-
const base64Promises = allFiles.value
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
146
|
+
if (allFiles.value.length) {
|
|
147
|
+
const base64Promises = allFiles.value.map(fileToBase64)
|
|
148
|
+
Promise.all(base64Promises)
|
|
149
|
+
.then((base64Strings) => {
|
|
150
|
+
combinedBase64String.value = props.multiple
|
|
151
|
+
? [...allAssets.value, ...base64Strings]
|
|
152
|
+
: base64Strings[0] || allAssets.value[0] || null
|
|
153
|
+
})
|
|
154
|
+
.catch((error) => {
|
|
155
|
+
alert?.addAlert({ message: error, alertType: 'error' })
|
|
156
|
+
allFiles.value = []
|
|
157
|
+
})
|
|
158
|
+
} else {
|
|
159
|
+
combinedBase64String.value = props.multiple
|
|
160
|
+
? [...allAssets.value]
|
|
161
|
+
: allAssets.value[0] || null
|
|
126
162
|
}
|
|
127
163
|
}, { deep: true, immediate: true })
|
|
128
164
|
|
|
129
165
|
watch(combinedBase64String, (newValue, oldValue) => {
|
|
130
166
|
if (!isEqual(newValue, oldValue)) {
|
|
131
|
-
emit('update:modelValue', uniqWith(newValue, isEqual))
|
|
167
|
+
emit('update:modelValue', props.multiple ? uniqWith(newValue as Base64String[], isEqual) : newValue)
|
|
132
168
|
}
|
|
133
169
|
}, { deep: true })
|
|
134
170
|
</script>
|
|
135
171
|
|
|
136
172
|
<template>
|
|
137
173
|
<v-text-field
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
174
|
+
v-bind="$attrs"
|
|
175
|
+
label="Upload files"
|
|
176
|
+
readonly
|
|
177
|
+
:dirty="Array.isArray(combinedBase64String) ? combinedBase64String.length > 0 : !!combinedBase64String"
|
|
178
|
+
v-on="Array.isArray(combinedBase64String) && combinedBase64String.length > 0 ? {} : { click: openWindowUpload }"
|
|
143
179
|
>
|
|
144
180
|
<template #default>
|
|
145
181
|
<v-chip
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
182
|
+
v-for="(asset, index) in allAssets"
|
|
183
|
+
:key="asset.fileName"
|
|
184
|
+
color="green"
|
|
185
|
+
variant="flat"
|
|
186
|
+
closable
|
|
187
|
+
@click:close="removeAssetByIndex(index)"
|
|
152
188
|
>
|
|
153
|
-
{{ asset.fileName }}
|
|
189
|
+
{{ asset.originalFileName || asset.fileName }}
|
|
190
|
+
<template #append v-if="downloadable">
|
|
191
|
+
<slot name="download" :item="asset">
|
|
192
|
+
<v-icon @click="downloadBase64File(asset.base64String || '',asset.originalFileName || asset.fileName)" v-if="asset.base64String">mdi mdi-download</v-icon>
|
|
193
|
+
</slot>
|
|
194
|
+
</template>
|
|
154
195
|
</v-chip>
|
|
155
196
|
<v-chip
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
197
|
+
v-for="(file, index) in allFiles"
|
|
198
|
+
:key="file.name"
|
|
199
|
+
color="primary"
|
|
200
|
+
variant="flat"
|
|
201
|
+
closable
|
|
202
|
+
@click:close="removeFileByIndex(index)"
|
|
162
203
|
>
|
|
163
204
|
{{ file.name }}
|
|
164
205
|
</v-chip>
|
|
165
206
|
</template>
|
|
166
207
|
|
|
167
|
-
<template
|
|
168
|
-
|
|
169
|
-
#append-inner
|
|
170
|
-
>
|
|
171
|
-
<VBtn
|
|
172
|
-
variant="text"
|
|
173
|
-
:icon="true"
|
|
174
|
-
@click="openWindowUpload"
|
|
175
|
-
>
|
|
208
|
+
<template v-if="props.multiple && Array.isArray(combinedBase64String) && combinedBase64String.length > 0" #append-inner>
|
|
209
|
+
<VBtn variant="text" :icon="true" @click="openWindowUpload">
|
|
176
210
|
<v-icon>mdi mdi-plus</v-icon>
|
|
177
211
|
</VBtn>
|
|
178
212
|
</template>
|
|
179
213
|
</v-text-field>
|
|
180
214
|
<v-file-input
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
215
|
+
ref="fileInput"
|
|
216
|
+
:accept="props.accept"
|
|
217
|
+
:multiple="props.multiple"
|
|
218
|
+
style="display: none"
|
|
219
|
+
@update:model-value="addFiles"
|
|
186
220
|
/>
|
|
187
221
|
</template>
|
|
@@ -22,6 +22,8 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataIterator['$pr
|
|
|
22
22
|
insertable?: boolean
|
|
23
23
|
searchable?: boolean
|
|
24
24
|
|
|
25
|
+
loading?: boolean
|
|
26
|
+
|
|
25
27
|
viewSwitch?: boolean
|
|
26
28
|
viewSwitchMultiple?: boolean
|
|
27
29
|
|
|
@@ -44,6 +46,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
44
46
|
insertable: true,
|
|
45
47
|
searchable: true,
|
|
46
48
|
|
|
49
|
+
loading: false,
|
|
50
|
+
|
|
47
51
|
viewSwitch: false,
|
|
48
52
|
viewSwitchMultiple:false,
|
|
49
53
|
|
|
@@ -208,6 +212,7 @@ defineExpose({operation})
|
|
|
208
212
|
:items="items"
|
|
209
213
|
:item-value="modelKey"
|
|
210
214
|
:search="search"
|
|
215
|
+
:loading="loading"
|
|
211
216
|
>
|
|
212
217
|
<template #default="defaultProps" v-if="viewType.includes('iterator')">
|
|
213
218
|
<slot
|
|
@@ -244,7 +249,7 @@ defineExpose({operation})
|
|
|
244
249
|
<v-container fluid>
|
|
245
250
|
<v-row>
|
|
246
251
|
<v-col
|
|
247
|
-
v-for="key in
|
|
252
|
+
v-for="key in itemsPerPage"
|
|
248
253
|
:key="key"
|
|
249
254
|
:cols="cols"
|
|
250
255
|
:sm="sm"
|
|
@@ -340,6 +345,7 @@ defineExpose({operation})
|
|
|
340
345
|
color="primary"
|
|
341
346
|
:items="items"
|
|
342
347
|
:search="search"
|
|
348
|
+
:loading="loading"
|
|
343
349
|
v-if="viewType.includes('table')"
|
|
344
350
|
>
|
|
345
351
|
<!-- @ts-ignore -->
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import { VueSignaturePad } from 'vue-signature-pad';
|
|
3
|
-
import {
|
|
3
|
+
import { VInput } from 'vuetify/components/VInput'
|
|
4
|
+
import { type Ref, ref, onMounted, onBeforeUnmount, nextTick,watch,defineExpose,useTemplateRef } from 'vue'
|
|
4
5
|
|
|
5
6
|
interface SignatureOptions {
|
|
6
7
|
penColor: string
|
|
@@ -8,7 +9,7 @@ interface SignatureOptions {
|
|
|
8
9
|
maxWidth?: number
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
interface SignatureProps {
|
|
12
|
+
interface SignatureProps extends /* @vue-ignore */ InstanceType<typeof VInput['$props']> {
|
|
12
13
|
title?: string
|
|
13
14
|
btnName?: string
|
|
14
15
|
titleConfirm?: string
|
|
@@ -21,8 +22,10 @@ const props = withDefaults(defineProps<SignatureProps>(), {
|
|
|
21
22
|
titleConfirm: 'I Accept My Signature',
|
|
22
23
|
})
|
|
23
24
|
|
|
24
|
-
const
|
|
25
|
-
const
|
|
25
|
+
const inputRef = useTemplateRef<VInput>("inputRef")
|
|
26
|
+
const signaturePadRef = useTemplateRef<typeof VueSignaturePad>("signaturePadRef")
|
|
27
|
+
|
|
28
|
+
const signatureData: Ref<string|null> = ref(null)
|
|
26
29
|
const colorOptions: string[] = ['#303F9F', '#1A2023', '#2E7D32', '#AC04BF']
|
|
27
30
|
const defaultColor: string = colorOptions[0]
|
|
28
31
|
const selectedColor: Ref<string> = ref(defaultColor)
|
|
@@ -30,28 +33,27 @@ const signatureOptions: Ref<SignatureOptions> = ref({ penColor: defaultColor, mi
|
|
|
30
33
|
const isDialogOpen: Ref<boolean> = ref(false)
|
|
31
34
|
|
|
32
35
|
const undoSignature = (): void => {
|
|
33
|
-
signaturePadRef.value
|
|
36
|
+
signaturePadRef.value?.undoSignature()
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
const clearSignature = (): void => {
|
|
37
|
-
signaturePadRef.value
|
|
40
|
+
signaturePadRef.value?.clearSignature()
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
const closeDialog = (): void => {
|
|
41
44
|
isDialogOpen.value = false
|
|
42
|
-
signaturePadRef.value
|
|
43
|
-
signaturePadRef.value
|
|
45
|
+
signaturePadRef.value?.clearSignature()
|
|
46
|
+
signaturePadRef.value?.clearCacheImages()
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
const emit = defineEmits<{
|
|
47
|
-
(event: 'update:modelValue', value: string): void
|
|
50
|
+
(event: 'update:modelValue', value: string|null): void
|
|
48
51
|
}>()
|
|
49
52
|
|
|
50
53
|
const saveSignature = (): void => {
|
|
51
54
|
isDialogOpen.value = false
|
|
52
|
-
const { isEmpty, data } = signaturePadRef.value
|
|
53
|
-
signatureData.value = isEmpty ?
|
|
54
|
-
emit('update:modelValue', signatureData.value)
|
|
55
|
+
const { isEmpty, data } = signaturePadRef.value?.saveSignature()
|
|
56
|
+
signatureData.value = isEmpty ? null : data
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
const changePenColor = (color: string): void => {
|
|
@@ -65,130 +67,167 @@ const openSignatureDialog = (): void => {
|
|
|
65
67
|
isDialogOpen.value = true
|
|
66
68
|
nextTick(() =>{
|
|
67
69
|
if (props.modelValue) {
|
|
68
|
-
signaturePadRef.value
|
|
70
|
+
signaturePadRef.value?.fromDataURL(props.modelValue)
|
|
69
71
|
}
|
|
70
72
|
});
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
const signaturePadHeight: Ref<string> = ref('')
|
|
74
76
|
const updateSignaturePadHeight = () => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
const screenHeight = window.innerHeight
|
|
78
|
+
signaturePadHeight.value = `${screenHeight * 0.4}px`
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
watch(signatureData,()=>{
|
|
82
|
+
emit('update:modelValue', signatureData.value)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
watch(()=>props.modelValue, (newValue) => {
|
|
86
|
+
signatureData.value = newValue || null
|
|
87
|
+
},{immediate: true})
|
|
88
|
+
|
|
79
89
|
onMounted(() => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (props.modelValue) {
|
|
83
|
-
signatureData.value = props.modelValue
|
|
84
|
-
}
|
|
90
|
+
updateSignaturePadHeight()
|
|
91
|
+
window.addEventListener('resize', updateSignaturePadHeight)
|
|
85
92
|
})
|
|
86
93
|
|
|
87
94
|
onBeforeUnmount(() => {
|
|
88
|
-
|
|
95
|
+
window.removeEventListener('resize', updateSignaturePadHeight)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const isValid = computed(()=>{
|
|
99
|
+
return inputRef.value?.isValid
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const errorMessages = computed(()=>{
|
|
103
|
+
return inputRef.value?.errorMessages
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
defineExpose({
|
|
107
|
+
errorMessages,
|
|
108
|
+
isValid,
|
|
109
|
+
reset: ()=>inputRef.value?.reset(),
|
|
110
|
+
resetValidation : ()=>inputRef.value?.resetValidation(),
|
|
111
|
+
validate : ()=>inputRef.value?.validate(),
|
|
89
112
|
})
|
|
90
113
|
</script>
|
|
91
114
|
|
|
92
115
|
<template>
|
|
93
|
-
<v-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
icon
|
|
133
|
-
@click="clearSignature"
|
|
134
|
-
>
|
|
135
|
-
<v-icon>fa-solid fa-trash</v-icon>
|
|
136
|
-
</v-btn>
|
|
137
|
-
<v-menu>
|
|
138
|
-
<template #activator="{ props: activatorProps }">
|
|
139
|
-
<v-btn
|
|
140
|
-
:color="selectedColor"
|
|
141
|
-
:icon="true"
|
|
142
|
-
v-bind="activatorProps"
|
|
143
|
-
>
|
|
144
|
-
<v-icon>fa-solid fa-paintbrush</v-icon>
|
|
145
|
-
</v-btn>
|
|
146
|
-
</template>
|
|
147
|
-
<v-list>
|
|
148
|
-
<v-row>
|
|
149
|
-
<v-col class="text-center">
|
|
150
|
-
<v-avatar
|
|
151
|
-
v-for="(color, index) in colorOptions"
|
|
152
|
-
:key="index"
|
|
153
|
-
:color="color"
|
|
154
|
-
:value="color"
|
|
155
|
-
class="mr-1"
|
|
156
|
-
@click="changePenColor(color)"
|
|
157
|
-
>
|
|
158
|
-
<v-icon color="white">
|
|
159
|
-
{{ selectedColor === color ? 'fa-solid fa-check' : '' }}
|
|
160
|
-
</v-icon>
|
|
161
|
-
</v-avatar>
|
|
162
|
-
</v-col>
|
|
163
|
-
</v-row>
|
|
164
|
-
</v-list>
|
|
165
|
-
</v-menu>
|
|
166
|
-
<v-btn
|
|
167
|
-
icon
|
|
168
|
-
@click="closeDialog"
|
|
169
|
-
>
|
|
170
|
-
<v-icon>fa-solid fa-xmark</v-icon>
|
|
171
|
-
</v-btn>
|
|
172
|
-
</v-toolbar>
|
|
173
|
-
<v-card-text>
|
|
174
|
-
<VueSignaturePad
|
|
175
|
-
ref="signaturePadRef"
|
|
176
|
-
:options="signatureOptions"
|
|
177
|
-
:height="signaturePadHeight"
|
|
178
|
-
/>
|
|
179
|
-
</v-card-text>
|
|
180
|
-
<v-divider />
|
|
181
|
-
<v-card-actions class="justify-center">
|
|
182
|
-
<v-btn
|
|
183
|
-
class="text-none"
|
|
184
|
-
color="success"
|
|
185
|
-
prepend-icon="fa-solid fa-check"
|
|
186
|
-
variant="flat"
|
|
187
|
-
@click="saveSignature"
|
|
116
|
+
<v-input v-model="signatureData" v-bind="$attrs" ref="inputRef">
|
|
117
|
+
<template #default="{isReadonly,isDisabled}">
|
|
118
|
+
<v-card
|
|
119
|
+
class="w-100"
|
|
120
|
+
flat
|
|
121
|
+
>
|
|
122
|
+
<v-card-text v-if="signatureData">
|
|
123
|
+
<v-img
|
|
124
|
+
:src="signatureData"
|
|
125
|
+
cover
|
|
126
|
+
/>
|
|
127
|
+
<v-icon
|
|
128
|
+
class="position-absolute"
|
|
129
|
+
style="top: 8px; right: 8px; z-index: 10;"
|
|
130
|
+
@click="signatureData=null"
|
|
131
|
+
v-if="signatureData && !isReadonly.value"
|
|
132
|
+
>
|
|
133
|
+
mdi mdi-close-circle
|
|
134
|
+
</v-icon>
|
|
135
|
+
</v-card-text>
|
|
136
|
+
<v-card-actions>
|
|
137
|
+
<v-btn
|
|
138
|
+
append-icon="mdi mdi-draw-pen"
|
|
139
|
+
block
|
|
140
|
+
class="text-none"
|
|
141
|
+
color="primary"
|
|
142
|
+
variant="flat"
|
|
143
|
+
@click="openSignatureDialog"
|
|
144
|
+
:readonly="isReadonly.value"
|
|
145
|
+
:disabled="isDisabled.value"
|
|
146
|
+
>
|
|
147
|
+
{{ props.btnName }}
|
|
148
|
+
</v-btn>
|
|
149
|
+
</v-card-actions>
|
|
150
|
+
<v-dialog
|
|
151
|
+
v-model="isDialogOpen"
|
|
152
|
+
height="auto"
|
|
153
|
+
persistent
|
|
154
|
+
width="100%"
|
|
188
155
|
>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
156
|
+
<v-card>
|
|
157
|
+
<v-toolbar>
|
|
158
|
+
<v-toolbar-title class="text-no-wrap">
|
|
159
|
+
{{ props.title }}
|
|
160
|
+
</v-toolbar-title>
|
|
161
|
+
<v-btn
|
|
162
|
+
icon
|
|
163
|
+
@click="undoSignature"
|
|
164
|
+
>
|
|
165
|
+
<v-icon>fa-solid fa-arrow-rotate-left</v-icon>
|
|
166
|
+
</v-btn>
|
|
167
|
+
<v-btn
|
|
168
|
+
icon
|
|
169
|
+
@click="clearSignature"
|
|
170
|
+
>
|
|
171
|
+
<v-icon>fa-solid fa-trash</v-icon>
|
|
172
|
+
</v-btn>
|
|
173
|
+
<v-menu>
|
|
174
|
+
<template #activator="{ props: activatorProps }">
|
|
175
|
+
<v-btn
|
|
176
|
+
:color="selectedColor"
|
|
177
|
+
:icon="true"
|
|
178
|
+
v-bind="activatorProps"
|
|
179
|
+
>
|
|
180
|
+
<v-icon>fa-solid fa-paintbrush</v-icon>
|
|
181
|
+
</v-btn>
|
|
182
|
+
</template>
|
|
183
|
+
<v-list>
|
|
184
|
+
<v-row>
|
|
185
|
+
<v-col class="text-center">
|
|
186
|
+
<v-avatar
|
|
187
|
+
v-for="(color, index) in colorOptions"
|
|
188
|
+
:key="index"
|
|
189
|
+
:color="color"
|
|
190
|
+
:value="color"
|
|
191
|
+
class="mr-1"
|
|
192
|
+
@click="changePenColor(color)"
|
|
193
|
+
>
|
|
194
|
+
<v-icon color="white">
|
|
195
|
+
{{ selectedColor === color ? 'fa-solid fa-check' : '' }}
|
|
196
|
+
</v-icon>
|
|
197
|
+
</v-avatar>
|
|
198
|
+
</v-col>
|
|
199
|
+
</v-row>
|
|
200
|
+
</v-list>
|
|
201
|
+
</v-menu>
|
|
202
|
+
<v-btn
|
|
203
|
+
icon
|
|
204
|
+
@click="closeDialog"
|
|
205
|
+
>
|
|
206
|
+
<v-icon>fa-solid fa-xmark</v-icon>
|
|
207
|
+
</v-btn>
|
|
208
|
+
</v-toolbar>
|
|
209
|
+
<v-card-text>
|
|
210
|
+
<VueSignaturePad
|
|
211
|
+
ref="signaturePadRef"
|
|
212
|
+
:options="signatureOptions"
|
|
213
|
+
:height="signaturePadHeight"
|
|
214
|
+
/>
|
|
215
|
+
</v-card-text>
|
|
216
|
+
<v-divider />
|
|
217
|
+
<v-card-actions class="justify-center">
|
|
218
|
+
<v-btn
|
|
219
|
+
class="text-none"
|
|
220
|
+
color="success"
|
|
221
|
+
prepend-icon="fa-solid fa-check"
|
|
222
|
+
variant="flat"
|
|
223
|
+
@click="saveSignature"
|
|
224
|
+
>
|
|
225
|
+
{{ props.titleConfirm }}
|
|
226
|
+
</v-btn>
|
|
227
|
+
</v-card-actions>
|
|
228
|
+
</v-card>
|
|
229
|
+
</v-dialog>
|
|
230
|
+
</v-card>
|
|
231
|
+
</template>
|
|
232
|
+
</v-input>
|
|
194
233
|
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { computedAsync } from '@vueuse/core'
|
|
3
|
+
import { useGraphQl } from '../../composables/graphql'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
groupKey?: string | null
|
|
7
|
+
itemCode?: string | null
|
|
8
|
+
locale?: string
|
|
9
|
+
notFoundText?: string
|
|
10
|
+
placeholder?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
14
|
+
locale: 'TH',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const masterItemValue = computedAsync<string>(async () => {
|
|
18
|
+
if (props.groupKey && props.itemCode) {
|
|
19
|
+
try {
|
|
20
|
+
const result = await useGraphQl().queryPromise<any>("masterItemByGroupKeyAndItemCode",['itemValue', 'itemValueAlternative'],{ groupKey: props.groupKey, itemCode: props.itemCode })
|
|
21
|
+
|
|
22
|
+
if (result) {
|
|
23
|
+
return props.locale === 'TH'
|
|
24
|
+
? result.itemValue
|
|
25
|
+
: result.itemValueAlternative || result.itemValue
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.error(e)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return props.notFoundText || `${props.itemCode}`
|
|
32
|
+
}, props.placeholder || `${props.groupKey}(${props.itemCode})`)
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
{{ masterItemValue }}
|
|
37
|
+
</template>
|
|
@@ -65,23 +65,27 @@ function cancel() {
|
|
|
65
65
|
loadFormData()
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
const operation = ref({save, cancel, reload, item, canSave, isLoading,isDataChange,isCreating})
|
|
69
|
+
|
|
68
70
|
defineExpose({ save, cancel, reload, item, isLoading })
|
|
69
71
|
</script>
|
|
70
72
|
|
|
71
73
|
<template>
|
|
72
74
|
<VCard>
|
|
73
75
|
<VToolbar>
|
|
74
|
-
<
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
<slot name="titleToolbar" :operation="operation">
|
|
77
|
+
<VToolbarTitle>
|
|
78
|
+
<slot name="title" :operation="operation">
|
|
79
|
+
{{ title }}
|
|
80
|
+
<v-icon
|
|
81
|
+
size="small"
|
|
82
|
+
@click="reload"
|
|
83
|
+
>
|
|
84
|
+
mdi mdi-refresh
|
|
85
|
+
</v-icon>
|
|
86
|
+
</slot>
|
|
87
|
+
</VToolbarTitle>
|
|
88
|
+
</slot>
|
|
85
89
|
</VToolbar>
|
|
86
90
|
<VCardText>
|
|
87
91
|
<form-pad
|
|
@@ -99,24 +103,26 @@ defineExpose({ save, cancel, reload, item, isLoading })
|
|
|
99
103
|
</form-pad>
|
|
100
104
|
</VCardText>
|
|
101
105
|
<VCardActions>
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
106
|
+
<slot name="action" :operation="operation">
|
|
107
|
+
<VSpacer />
|
|
108
|
+
<VBtn
|
|
109
|
+
color="primary"
|
|
110
|
+
variant="flat"
|
|
111
|
+
:loading="isLoading"
|
|
112
|
+
:disabled="!isDataChange || !canSave"
|
|
113
|
+
@click="save"
|
|
114
|
+
>
|
|
115
|
+
{{ saveCaption }}
|
|
116
|
+
</VBtn>
|
|
117
|
+
<VBtn
|
|
118
|
+
color="error"
|
|
119
|
+
variant="flat"
|
|
120
|
+
:disabled="isLoading"
|
|
121
|
+
@click="cancel"
|
|
122
|
+
>
|
|
123
|
+
{{ cancelCaption }}
|
|
124
|
+
</VBtn>
|
|
125
|
+
</slot>
|
|
120
126
|
</VCardActions>
|
|
121
127
|
</VCard>
|
|
122
128
|
</template>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { computedAsync } from '@vueuse/core'
|
|
3
|
+
import { useGraphQlOperation } from '../../composables/graphqlOperation'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
modelName: string
|
|
7
|
+
modelBy?: object
|
|
8
|
+
itemTitle: string
|
|
9
|
+
fields?: Array<string | object>
|
|
10
|
+
cache?: boolean
|
|
11
|
+
|
|
12
|
+
notFoundText?: string
|
|
13
|
+
placeholder?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
17
|
+
cache: false,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const modelItemValue = computedAsync<string>(async () => {
|
|
21
|
+
if (props.modelName && props.itemTitle) {
|
|
22
|
+
let fields: any[] = [props.itemTitle]
|
|
23
|
+
const variables: Record<string, any> = Object.assign({},props.modelBy)
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const result : Record<string, any> = await useGraphQlOperation('Query',props.modelName,fields,variables,props.cache)
|
|
27
|
+
|
|
28
|
+
if (result) {
|
|
29
|
+
return result[props.itemTitle]
|
|
30
|
+
}
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error(e)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return props.notFoundText
|
|
36
|
+
}, props.placeholder )
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
{{ modelItemValue }}
|
|
41
|
+
</template>
|
|
@@ -5,12 +5,23 @@ import printJS from 'print-js'
|
|
|
5
5
|
import { ref, computed } from 'vue'
|
|
6
6
|
import { useAlert } from '../../composables/alert'
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
interface Props extends /* @vue-ignore */ InstanceType<typeof PDF['$props']> {
|
|
9
9
|
base64String?: string
|
|
10
10
|
title?: string
|
|
11
11
|
fileName?: string
|
|
12
12
|
disabled?: boolean
|
|
13
|
-
|
|
13
|
+
isPrint?: boolean
|
|
14
|
+
showBackToTopBtn?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
18
|
+
base64String: "",
|
|
19
|
+
title: "",
|
|
20
|
+
fileName: "",
|
|
21
|
+
disabled: false,
|
|
22
|
+
isPrint: false,
|
|
23
|
+
showBackToTopBtn: false
|
|
24
|
+
})
|
|
14
25
|
|
|
15
26
|
const emit = defineEmits(['closeDialog'])
|
|
16
27
|
const base64 = ref(props.base64String)
|
|
@@ -61,9 +72,19 @@ const endLoadPdf = () => {
|
|
|
61
72
|
base64.value = ''
|
|
62
73
|
}
|
|
63
74
|
|
|
64
|
-
const isMobile =
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
const isMobile = () => {
|
|
76
|
+
return /Android|Mobi|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Macintosh/i.test(navigator.userAgent);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const checkMobileAndPrint = computed(() => {
|
|
80
|
+
return !isMobile() && props.isPrint;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const setWidthPdf = computed(() => {
|
|
84
|
+
if (isMobile()) {
|
|
85
|
+
return "100%"
|
|
86
|
+
} else {
|
|
87
|
+
return "100dvh"
|
|
67
88
|
}
|
|
68
89
|
})
|
|
69
90
|
</script>
|
|
@@ -74,7 +95,7 @@ const isMobile = computed(() => {
|
|
|
74
95
|
<v-toolbar-title>{{ props.title }}</v-toolbar-title>
|
|
75
96
|
<v-spacer />
|
|
76
97
|
<v-btn
|
|
77
|
-
v-if="
|
|
98
|
+
v-if="checkMobileAndPrint"
|
|
78
99
|
icon="mdi mdi-printer"
|
|
79
100
|
variant="plain"
|
|
80
101
|
@click="printPdf"
|
|
@@ -91,13 +112,12 @@ const isMobile = computed(() => {
|
|
|
91
112
|
@click="endLoadPdf"
|
|
92
113
|
/>
|
|
93
114
|
</v-toolbar>
|
|
94
|
-
<v-card-text class="justify-center">
|
|
115
|
+
<v-card-text class="justify-center h-screen">
|
|
95
116
|
<PDF
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
:show-back-to-top-btn="false"
|
|
117
|
+
v-bind="$attrs"
|
|
118
|
+
:pdf-width="setWidthPdf"
|
|
119
|
+
:src="base64"
|
|
120
|
+
:show-back-to-top-btn="props.showBackToTopBtn"
|
|
101
121
|
/>
|
|
102
122
|
</v-card-text>
|
|
103
123
|
</v-card>
|
|
@@ -4,7 +4,7 @@ import { graphqlInputType, graphqlOperation, graphqlType, scalarType } from "#im
|
|
|
4
4
|
export function buildFields(operationFields, fields, depth = 0) {
|
|
5
5
|
if (!operationFields) return [];
|
|
6
6
|
if (isClassConstructor(fields)) fields = classAttributes(fields);
|
|
7
|
-
if (!fields || fields.length == 0) fields = [
|
|
7
|
+
if (!fields || fields.length == 0) fields = ["*"];
|
|
8
8
|
if (fields.includes("*")) {
|
|
9
9
|
operationFields.fields.forEach((field) => {
|
|
10
10
|
if (!fields.includes(field) && !field.toLowerCase().endsWith("base64") && !field.toLowerCase().endsWith("base64string")) fields.push(field);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramathibodi/nuxt-commons",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.35",
|
|
4
4
|
"description": "Ramathibodi Nuxt modules for common components",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -115,5 +115,5 @@
|
|
|
115
115
|
"vitest": "^1.6.0",
|
|
116
116
|
"vue-tsc": "2.0.29"
|
|
117
117
|
},
|
|
118
|
-
"packageManager": "pnpm@9.
|
|
118
|
+
"packageManager": "pnpm@9.14.4+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab"
|
|
119
119
|
}
|