@ramathibodi/nuxt-commons 0.1.33 → 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/TabsGroup.vue +1 -2
- 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 +19 -5
- package/dist/runtime/components/form/SignPad.vue +160 -121
- package/dist/runtime/components/form/Table.vue +73 -59
- package/dist/runtime/components/master/label.vue +37 -0
- package/dist/runtime/components/model/Pad.vue +35 -29
- package/dist/runtime/components/model/Table.vue +6 -6
- package/dist/runtime/components/model/iterator.vue +4 -4
- 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,5 +1,4 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import {ref} from 'vue'
|
|
3
2
|
import {VTabs} from 'vuetify/components'
|
|
4
3
|
|
|
5
4
|
interface Props extends /* @vue-ignore */ InstanceType<typeof VTabs['$props']> {
|
|
@@ -11,7 +10,7 @@ defineOptions({
|
|
|
11
10
|
})
|
|
12
11
|
|
|
13
12
|
const props = defineProps<Props>()
|
|
14
|
-
const currentTab =
|
|
13
|
+
const currentTab = defineModel<string|number>()
|
|
15
14
|
</script>
|
|
16
15
|
|
|
17
16
|
<template>
|
|
@@ -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>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import {computed, defineOptions, nextTick, ref, useAttrs, useSlots, watch} from 'vue'
|
|
2
|
+
import {computed, defineExpose, defineOptions, nextTick, ref, useAttrs, useSlots, watch} from 'vue'
|
|
3
3
|
import {omit} from 'lodash-es'
|
|
4
4
|
import type {FormDialogCallback} from '../../types/formDialog'
|
|
5
5
|
import {VDataIterator} from "vuetify/components/VDataIterator";
|
|
@@ -20,6 +20,9 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataIterator['$pr
|
|
|
20
20
|
importable?: boolean
|
|
21
21
|
exportable?: boolean
|
|
22
22
|
insertable?: boolean
|
|
23
|
+
searchable?: boolean
|
|
24
|
+
|
|
25
|
+
loading?: boolean
|
|
23
26
|
|
|
24
27
|
viewSwitch?: boolean
|
|
25
28
|
viewSwitchMultiple?: boolean
|
|
@@ -41,6 +44,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
41
44
|
importable: true,
|
|
42
45
|
exportable: true,
|
|
43
46
|
insertable: true,
|
|
47
|
+
searchable: true,
|
|
48
|
+
|
|
49
|
+
loading: false,
|
|
44
50
|
|
|
45
51
|
viewSwitch: false,
|
|
46
52
|
viewSwitchMultiple:false,
|
|
@@ -72,6 +78,10 @@ const items = ref<Record<string, any>[]>([])
|
|
|
72
78
|
const search = ref<string>()
|
|
73
79
|
const currentItem = ref<Record<string, any> | undefined>(undefined)
|
|
74
80
|
|
|
81
|
+
function setSearch(keyword: string) {
|
|
82
|
+
search.value = keyword
|
|
83
|
+
}
|
|
84
|
+
|
|
75
85
|
const isDialogOpen = ref<boolean>(false)
|
|
76
86
|
|
|
77
87
|
watch(() => props.modelValue, (newValue) => {
|
|
@@ -189,7 +199,9 @@ function openDialog(item?: object) {
|
|
|
189
199
|
})
|
|
190
200
|
}
|
|
191
201
|
|
|
192
|
-
const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpItem, moveDownItem,moveToItem })
|
|
202
|
+
const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpItem, moveDownItem,moveToItem,setSearch })
|
|
203
|
+
|
|
204
|
+
defineExpose({operation})
|
|
193
205
|
</script>
|
|
194
206
|
|
|
195
207
|
<template>
|
|
@@ -200,6 +212,7 @@ const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpIt
|
|
|
200
212
|
:items="items"
|
|
201
213
|
:item-value="modelKey"
|
|
202
214
|
:search="search"
|
|
215
|
+
:loading="loading"
|
|
203
216
|
>
|
|
204
217
|
<template #default="defaultProps" v-if="viewType.includes('iterator')">
|
|
205
218
|
<slot
|
|
@@ -236,7 +249,7 @@ const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpIt
|
|
|
236
249
|
<v-container fluid>
|
|
237
250
|
<v-row>
|
|
238
251
|
<v-col
|
|
239
|
-
v-for="key in
|
|
252
|
+
v-for="key in itemsPerPage"
|
|
240
253
|
:key="key"
|
|
241
254
|
:cols="cols"
|
|
242
255
|
:sm="sm"
|
|
@@ -278,7 +291,7 @@ const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpIt
|
|
|
278
291
|
</VToolbarTitle>
|
|
279
292
|
</v-col>
|
|
280
293
|
<v-col cols="5">
|
|
281
|
-
<slot name="search">
|
|
294
|
+
<slot name="search" :items="items" :operation="operation" v-if="props.searchable">
|
|
282
295
|
<VTextField
|
|
283
296
|
v-model="search"
|
|
284
297
|
class="justify-end w-100"
|
|
@@ -293,7 +306,7 @@ const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpIt
|
|
|
293
306
|
</v-row>
|
|
294
307
|
|
|
295
308
|
<VToolbarItems>
|
|
296
|
-
<slot name="toolbarItems" />
|
|
309
|
+
<slot name="toolbarItems" :items="items" :operation="operation"/>
|
|
297
310
|
<ImportCSV
|
|
298
311
|
v-if="props.importable"
|
|
299
312
|
icon="mdi:mdi-file-upload"
|
|
@@ -332,6 +345,7 @@ const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpIt
|
|
|
332
345
|
color="primary"
|
|
333
346
|
:items="items"
|
|
334
347
|
:search="search"
|
|
348
|
+
:loading="loading"
|
|
335
349
|
v-if="viewType.includes('table')"
|
|
336
350
|
>
|
|
337
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>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import {VDataTable} from 'vuetify/components/VDataTable'
|
|
3
|
-
import {computed, defineOptions, nextTick, ref, useAttrs, watch} from 'vue'
|
|
3
|
+
import {computed, defineOptions,defineExpose, nextTick, ref, useAttrs, watch} from 'vue'
|
|
4
4
|
import {omit} from 'lodash-es'
|
|
5
5
|
import type {FormDialogCallback} from '../../types/formDialog'
|
|
6
6
|
|
|
@@ -19,6 +19,7 @@ interface Props extends /* @vue-ignore */ InstanceType<typeof VDataTable['$props
|
|
|
19
19
|
importable?: boolean
|
|
20
20
|
exportable?: boolean
|
|
21
21
|
insertable?: boolean
|
|
22
|
+
searchable?: boolean
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -29,6 +30,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
29
30
|
importable: true,
|
|
30
31
|
exportable: true,
|
|
31
32
|
insertable: true,
|
|
33
|
+
searchable: true,
|
|
32
34
|
})
|
|
33
35
|
|
|
34
36
|
const emit = defineEmits(['update:modelValue'])
|
|
@@ -42,6 +44,10 @@ const items = ref<Record<string, any>[]>([])
|
|
|
42
44
|
const search = ref<string>()
|
|
43
45
|
const currentItem = ref<Record<string, any> | undefined>(undefined)
|
|
44
46
|
|
|
47
|
+
function setSearch(keyword: string) {
|
|
48
|
+
search.value = keyword
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
const isDialogOpen = ref<boolean>(false)
|
|
46
52
|
|
|
47
53
|
watch(() => props.modelValue, (newValue) => {
|
|
@@ -153,69 +159,77 @@ function openDialog(item?: object) {
|
|
|
153
159
|
})
|
|
154
160
|
}
|
|
155
161
|
|
|
156
|
-
const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpItem, moveDownItem,moveToItem })
|
|
162
|
+
const operation = ref({ openDialog, createItem, updateItem, deleteItem, moveUpItem, moveDownItem,moveToItem,setSearch })
|
|
163
|
+
|
|
164
|
+
defineExpose({operation})
|
|
157
165
|
</script>
|
|
158
166
|
|
|
159
167
|
<template>
|
|
160
168
|
<v-card>
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
</slot>
|
|
174
|
-
</VToolbarTitle>
|
|
175
|
-
</v-col>
|
|
176
|
-
<v-col cols="5">
|
|
177
|
-
<slot name="search">
|
|
178
|
-
<VTextField
|
|
179
|
-
v-model="search"
|
|
180
|
-
class="justify-end w-100"
|
|
181
|
-
density="compact"
|
|
182
|
-
hide-details
|
|
183
|
-
placeholder="ค้นหา"
|
|
184
|
-
clearable
|
|
185
|
-
variant="solo"
|
|
186
|
-
/>
|
|
187
|
-
</slot>
|
|
188
|
-
</v-col>
|
|
189
|
-
</v-row>
|
|
190
|
-
|
|
191
|
-
<VToolbarItems>
|
|
192
|
-
<slot name="toolbarItems" />
|
|
193
|
-
<ImportCSV
|
|
194
|
-
v-if="props.importable"
|
|
195
|
-
icon="mdi:mdi-file-upload"
|
|
196
|
-
variant="flat"
|
|
197
|
-
@import="importItems"
|
|
198
|
-
:color="toolbarColor"
|
|
199
|
-
/>
|
|
200
|
-
<ExportCSV
|
|
201
|
-
v-if="props.exportable && items.length"
|
|
202
|
-
icon="mdi:mdi-file-download"
|
|
203
|
-
variant="flat"
|
|
204
|
-
:file-name="title"
|
|
205
|
-
:model-value="items"
|
|
206
|
-
:color="toolbarColor"
|
|
207
|
-
/>
|
|
208
|
-
<VBtn
|
|
209
|
-
v-if="props.insertable"
|
|
210
|
-
:color="toolbarColor"
|
|
211
|
-
prepend-icon="mdi:mdi-plus"
|
|
212
|
-
variant="flat"
|
|
213
|
-
@click="openDialog()"
|
|
169
|
+
<slot
|
|
170
|
+
name="header"
|
|
171
|
+
:items="items"
|
|
172
|
+
:operation="operation"
|
|
173
|
+
>
|
|
174
|
+
<VToolbar :color="toolbarColor">
|
|
175
|
+
<v-row
|
|
176
|
+
justify="end"
|
|
177
|
+
class="ma-1"
|
|
178
|
+
dense
|
|
179
|
+
no-gutters
|
|
180
|
+
align="center"
|
|
214
181
|
>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
182
|
+
<v-col cols="7">
|
|
183
|
+
<VToolbarTitle class="pl-3">
|
|
184
|
+
<slot name="title">
|
|
185
|
+
{{ title }}
|
|
186
|
+
</slot>
|
|
187
|
+
</VToolbarTitle>
|
|
188
|
+
</v-col>
|
|
189
|
+
<v-col cols="5">
|
|
190
|
+
<slot name="search" :items="items" :operation="operation" v-if="props.searchable">
|
|
191
|
+
<VTextField
|
|
192
|
+
v-model="search"
|
|
193
|
+
class="justify-end w-100"
|
|
194
|
+
density="compact"
|
|
195
|
+
hide-details
|
|
196
|
+
placeholder="ค้นหา"
|
|
197
|
+
clearable
|
|
198
|
+
variant="solo"
|
|
199
|
+
/>
|
|
200
|
+
</slot>
|
|
201
|
+
</v-col>
|
|
202
|
+
</v-row>
|
|
203
|
+
|
|
204
|
+
<VToolbarItems>
|
|
205
|
+
<slot name="toolbarItems" :items="items" :operation="operation"/>
|
|
206
|
+
<ImportCSV
|
|
207
|
+
v-if="props.importable"
|
|
208
|
+
icon="mdi:mdi-file-upload"
|
|
209
|
+
variant="flat"
|
|
210
|
+
@import="importItems"
|
|
211
|
+
:color="toolbarColor"
|
|
212
|
+
/>
|
|
213
|
+
<ExportCSV
|
|
214
|
+
v-if="props.exportable && items.length"
|
|
215
|
+
icon="mdi:mdi-file-download"
|
|
216
|
+
variant="flat"
|
|
217
|
+
:file-name="title"
|
|
218
|
+
:model-value="items"
|
|
219
|
+
:color="toolbarColor"
|
|
220
|
+
/>
|
|
221
|
+
<VBtn
|
|
222
|
+
v-if="props.insertable"
|
|
223
|
+
:color="toolbarColor"
|
|
224
|
+
prepend-icon="mdi:mdi-plus"
|
|
225
|
+
variant="flat"
|
|
226
|
+
@click="openDialog()"
|
|
227
|
+
>
|
|
228
|
+
add
|
|
229
|
+
</VBtn>
|
|
230
|
+
</VToolbarItems>
|
|
231
|
+
</VToolbar>
|
|
232
|
+
</slot>
|
|
219
233
|
<v-data-table
|
|
220
234
|
v-bind="plainAttrs"
|
|
221
235
|
color="primary"
|
|
@@ -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>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import { computed,watch, nextTick, ref, useAttrs } from 'vue'
|
|
3
3
|
import { VDataTable } from 'vuetify/components/VDataTable'
|
|
4
|
-
import {
|
|
4
|
+
import { clone } from 'lodash-es'
|
|
5
5
|
import type { GraphqlModelProps } from '../../composables/graphqlModel'
|
|
6
6
|
import { useGraphqlModel } from '../../composables/graphqlModel'
|
|
7
7
|
|
|
@@ -37,7 +37,7 @@ const props = withDefaults(defineProps<Props & GraphqlModelProps>(), {
|
|
|
37
37
|
|
|
38
38
|
const attrs = useAttrs()
|
|
39
39
|
const plainAttrs = computed(() => {
|
|
40
|
-
const returnAttrs =
|
|
40
|
+
const returnAttrs = clone(attrs)
|
|
41
41
|
if (props.headers) returnAttrs['headers'] = props.headers
|
|
42
42
|
return returnAttrs
|
|
43
43
|
})
|
|
@@ -68,7 +68,7 @@ watch(()=>props.search,()=>{
|
|
|
68
68
|
search.value = props.search
|
|
69
69
|
},{immediate:true})
|
|
70
70
|
|
|
71
|
-
defineExpose({ reload })
|
|
71
|
+
defineExpose({ reload,operation })
|
|
72
72
|
</script>
|
|
73
73
|
|
|
74
74
|
<template>
|
|
@@ -102,8 +102,8 @@ defineExpose({ reload })
|
|
|
102
102
|
</slot>
|
|
103
103
|
</VToolbarTitle>
|
|
104
104
|
</v-col>
|
|
105
|
-
<v-col cols="5"
|
|
106
|
-
<slot name="search">
|
|
105
|
+
<v-col cols="5">
|
|
106
|
+
<slot name="search" :items="items" :operation="operation" v-if="props.searchable">
|
|
107
107
|
<VTextField
|
|
108
108
|
v-model="search"
|
|
109
109
|
class="justify-end w-100"
|
|
@@ -118,7 +118,7 @@ defineExpose({ reload })
|
|
|
118
118
|
</v-row>
|
|
119
119
|
|
|
120
120
|
<VToolbarItems>
|
|
121
|
-
<slot name="toolbarItems" />
|
|
121
|
+
<slot name="toolbarItems" :items="items" :operation="operation"/>
|
|
122
122
|
<ImportCSV
|
|
123
123
|
v-if="props.importable && canCreate && props.insertable"
|
|
124
124
|
icon="mdi mdi-file-upload"
|
|
@@ -128,7 +128,7 @@ watch(()=>props.search,()=>{
|
|
|
128
128
|
search.value = props.search
|
|
129
129
|
},{immediate:true})
|
|
130
130
|
|
|
131
|
-
defineExpose({ reload })
|
|
131
|
+
defineExpose({ reload,operation })
|
|
132
132
|
</script>
|
|
133
133
|
|
|
134
134
|
<template>
|
|
@@ -227,8 +227,8 @@ defineExpose({ reload })
|
|
|
227
227
|
</slot>
|
|
228
228
|
</VToolbarTitle>
|
|
229
229
|
</v-col>
|
|
230
|
-
<v-col cols="5"
|
|
231
|
-
<slot name="search">
|
|
230
|
+
<v-col cols="5">
|
|
231
|
+
<slot name="search" :items="items" :operation="operation" v-if="props.searchable">
|
|
232
232
|
<VTextField
|
|
233
233
|
v-model="search"
|
|
234
234
|
class="justify-end w-100"
|
|
@@ -243,7 +243,7 @@ defineExpose({ reload })
|
|
|
243
243
|
</v-row>
|
|
244
244
|
|
|
245
245
|
<VToolbarItems>
|
|
246
|
-
<slot name="toolbarItems" />
|
|
246
|
+
<slot name="toolbarItems" :items="items" :operation="operation"/>
|
|
247
247
|
<ImportCSV
|
|
248
248
|
v-if="props.importable && canCreate && props.insertable"
|
|
249
249
|
icon="mdi mdi-file-upload"
|
|
@@ -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
|
}
|