@ramathibodi/nuxt-commons 0.0.1
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/README.md +81 -0
- package/dist/module.cjs +5 -0
- package/dist/module.d.mts +7 -0
- package/dist/module.d.ts +7 -0
- package/dist/module.json +8 -0
- package/dist/module.mjs +34 -0
- package/dist/runtime/components/Alert.vue +52 -0
- package/dist/runtime/components/BarcodeReader.vue +98 -0
- package/dist/runtime/components/Calendar.vue +99 -0
- package/dist/runtime/components/Camera.vue +116 -0
- package/dist/runtime/components/ExportCSV.vue +55 -0
- package/dist/runtime/components/FileBtn.vue +56 -0
- package/dist/runtime/components/ImportCSV.vue +64 -0
- package/dist/runtime/components/Pdf/Print.vue +63 -0
- package/dist/runtime/components/Pdf/View.vue +70 -0
- package/dist/runtime/components/TabsGroup.vue +28 -0
- package/dist/runtime/components/TextBarcode.vue +52 -0
- package/dist/runtime/components/dialog/Confirm.vue +100 -0
- package/dist/runtime/components/dialog/Index.vue +72 -0
- package/dist/runtime/components/dialog/Loading.vue +34 -0
- package/dist/runtime/components/form/Date.vue +163 -0
- package/dist/runtime/components/form/DateTime.vue +107 -0
- package/dist/runtime/components/form/File.vue +187 -0
- package/dist/runtime/components/form/Login.vue +131 -0
- package/dist/runtime/components/form/Pad.vue +179 -0
- package/dist/runtime/components/form/SignPad.vue +186 -0
- package/dist/runtime/components/form/Time.vue +158 -0
- package/dist/runtime/components/form/images/CameraCrop.vue +58 -0
- package/dist/runtime/components/form/images/Edit.vue +143 -0
- package/dist/runtime/components/form/images/Preview.vue +48 -0
- package/dist/runtime/components/label/Date.vue +29 -0
- package/dist/runtime/components/label/FormatMoney.vue +29 -0
- package/dist/runtime/composables/alert.d.ts +13 -0
- package/dist/runtime/composables/alert.mjs +44 -0
- package/dist/runtime/composables/utils/validation.d.ts +32 -0
- package/dist/runtime/composables/utils/validation.mjs +36 -0
- package/dist/runtime/labs/form/EditMobile.vue +153 -0
- package/dist/runtime/labs/form/TextFieldMask.vue +43 -0
- package/dist/runtime/plugins/vueSignaturePad.d.ts +2 -0
- package/dist/runtime/plugins/vueSignaturePad.mjs +5 -0
- package/dist/runtime/types/alert.d.ts +11 -0
- package/dist/runtime/types/modules.d.ts +5 -0
- package/dist/runtime/utils/datetime.d.ts +25 -0
- package/dist/runtime/utils/datetime.mjs +166 -0
- package/dist/runtime/utils/object.d.ts +8 -0
- package/dist/runtime/utils/object.mjs +28 -0
- package/dist/types.d.mts +16 -0
- package/dist/types.d.ts +16 -0
- package/package.json +90 -0
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Get your module up and running quickly.
|
|
3
|
+
|
|
4
|
+
Find and replace all on all files (CMD+SHIFT+F):
|
|
5
|
+
- Name: Ramahis Common Component
|
|
6
|
+
- Package name: @ramahis/common-components
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# Ramahis Common Component
|
|
10
|
+
|
|
11
|
+
[![npm version][npm-version-src]][npm-version-href]
|
|
12
|
+
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
13
|
+
[![License][license-src]][license-href]
|
|
14
|
+
[![Nuxt][nuxt-src]][nuxt-href]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Quick Setup
|
|
18
|
+
|
|
19
|
+
1. Add `@deverloprama/common-components` dependency to your project
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Using pnpm
|
|
23
|
+
pnpm add -D @deverloprama/common-components
|
|
24
|
+
|
|
25
|
+
# Using yarn
|
|
26
|
+
yarn add --dev @deverloprama/common-components
|
|
27
|
+
|
|
28
|
+
# Using npm
|
|
29
|
+
npm install --save-dev @deverloprama/common-components
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
2. Add `my-module` to the `modules` section of `nuxt.config.ts`
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
export default defineNuxtConfig({
|
|
36
|
+
modules: [
|
|
37
|
+
'@deverloprama/common-components'
|
|
38
|
+
]
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
That's it! You can now use My Module in your Nuxt app ✨
|
|
43
|
+
|
|
44
|
+
## Development
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Install dependencies
|
|
48
|
+
npm install
|
|
49
|
+
|
|
50
|
+
# Generate type stubs
|
|
51
|
+
npm run dev:prepare
|
|
52
|
+
|
|
53
|
+
# Develop with the playground
|
|
54
|
+
npm run dev
|
|
55
|
+
|
|
56
|
+
# Build the playground
|
|
57
|
+
npm run dev:build
|
|
58
|
+
|
|
59
|
+
# Run ESLint
|
|
60
|
+
npm run lint
|
|
61
|
+
|
|
62
|
+
# Run Vitest
|
|
63
|
+
npm run test
|
|
64
|
+
npm run test:watch
|
|
65
|
+
|
|
66
|
+
# Release new version
|
|
67
|
+
npm run release
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
<!-- Badges -->
|
|
71
|
+
[npm-version-src]: https://img.shields.io/npm/v/my-module/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
|
|
72
|
+
[npm-version-href]: https://npmjs.com/package/my-module
|
|
73
|
+
|
|
74
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/my-module.svg?style=flat&colorA=18181B&colorB=28CF8D
|
|
75
|
+
[npm-downloads-href]: https://npmjs.com/package/my-module
|
|
76
|
+
|
|
77
|
+
[license-src]: https://img.shields.io/npm/l/my-module.svg?style=flat&colorA=18181B&colorB=28CF8D
|
|
78
|
+
[license-href]: https://npmjs.com/package/my-module
|
|
79
|
+
|
|
80
|
+
[nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js
|
|
81
|
+
[nuxt-href]: https://nuxt.com
|
package/dist/module.cjs
ADDED
package/dist/module.d.ts
ADDED
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, addComponentsDir, addImportsDir, addTypeTemplate } from '@nuxt/kit';
|
|
2
|
+
|
|
3
|
+
const module = defineNuxtModule({
|
|
4
|
+
meta: {
|
|
5
|
+
name: "@ramathibodi/nuxt-commons",
|
|
6
|
+
configKey: "nuxt-commons",
|
|
7
|
+
compatibility: {
|
|
8
|
+
nuxt: "^3.0.0"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
// Default configuration options of the Nuxt module
|
|
12
|
+
defaults: {},
|
|
13
|
+
async setup(_options, _nuxt) {
|
|
14
|
+
const resolver = createResolver(import.meta.url);
|
|
15
|
+
await addComponentsDir({
|
|
16
|
+
path: resolver.resolve("runtime/components"),
|
|
17
|
+
pathPrefix: true,
|
|
18
|
+
global: true
|
|
19
|
+
});
|
|
20
|
+
addImportsDir(resolver.resolve("runtime/composables"));
|
|
21
|
+
addTypeTemplate({
|
|
22
|
+
filename: "types/modules.d.ts",
|
|
23
|
+
getContents: () => `
|
|
24
|
+
declare module 'painterro';
|
|
25
|
+
declare module "@zxing/browser"
|
|
26
|
+
declare module "vue-signature-pad"
|
|
27
|
+
declare module "accounting"
|
|
28
|
+
declare module "pdf-vue3"
|
|
29
|
+
`
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export { module as default };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, watch } from 'vue'
|
|
3
|
+
import { useAlert } from '../composables/alert'
|
|
4
|
+
import type { AlertItem } from '../types/alert'
|
|
5
|
+
|
|
6
|
+
const isAlertOpen = ref(false)
|
|
7
|
+
const timeout = ref(3000)
|
|
8
|
+
const alert = useAlert()
|
|
9
|
+
const currentItem = ref<AlertItem | undefined>()
|
|
10
|
+
|
|
11
|
+
const renewAlert = () => {
|
|
12
|
+
if (alert?.hasAlert()) {
|
|
13
|
+
currentItem.value = alert?.takeAlert()
|
|
14
|
+
isAlertOpen.value = true
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
currentItem.value = undefined
|
|
18
|
+
isAlertOpen.value = false
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
watch(() => alert?.hasAlert(), (hasAlert) => {
|
|
23
|
+
if (hasAlert) {
|
|
24
|
+
renewAlert()
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<VSnackbar
|
|
31
|
+
v-if="currentItem"
|
|
32
|
+
v-model="isAlertOpen"
|
|
33
|
+
:timeout="timeout"
|
|
34
|
+
location="center"
|
|
35
|
+
multi-line
|
|
36
|
+
variant="text"
|
|
37
|
+
@update:model-value="renewAlert()"
|
|
38
|
+
>
|
|
39
|
+
<!-- @vue-expected-error Type conversion problem -->
|
|
40
|
+
<VAlert
|
|
41
|
+
v-model="isAlertOpen"
|
|
42
|
+
closable
|
|
43
|
+
:type="currentItem.alertType"
|
|
44
|
+
elevation="2"
|
|
45
|
+
theme="dark"
|
|
46
|
+
variant="flat"
|
|
47
|
+
@click:close="renewAlert()"
|
|
48
|
+
>
|
|
49
|
+
{{ currentItem.statusCode ? currentItem.statusCode + ' ' : '' }}{{ currentItem.message }}{{ currentItem.data ? ' ' + currentItem.data : '' }}
|
|
50
|
+
</VAlert>
|
|
51
|
+
</VSnackbar>
|
|
52
|
+
</template>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { BrowserMultiFormatReader } from '@zxing/browser'
|
|
3
|
+
import { type IScannerControls } from '@zxing/browser/esm'
|
|
4
|
+
import type { Exception, Result } from '@zxing/library'
|
|
5
|
+
import { ref, onMounted } from 'vue'
|
|
6
|
+
import { useAlert } from '../composables/alert'
|
|
7
|
+
|
|
8
|
+
const videoElementRef = ref<HTMLVideoElement | null>(null)
|
|
9
|
+
const barcodeReader = new BrowserMultiFormatReader()
|
|
10
|
+
const alert = useAlert()
|
|
11
|
+
const hasCameraAvailable = ref(false)
|
|
12
|
+
|
|
13
|
+
const emit = defineEmits<{
|
|
14
|
+
(event: 'decode', barcodeValue: string): void
|
|
15
|
+
(event: 'error', error: string | unknown): void
|
|
16
|
+
}>()
|
|
17
|
+
|
|
18
|
+
async function checkCameraAvailability() {
|
|
19
|
+
const devices = await navigator.mediaDevices.enumerateDevices()
|
|
20
|
+
hasCameraAvailable.value = devices.some(device => device.kind === 'videoinput')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function startCamera() {
|
|
24
|
+
try {
|
|
25
|
+
const videoInputDevices = await BrowserMultiFormatReader.listVideoInputDevices()
|
|
26
|
+
if (videoInputDevices.length === 0) {
|
|
27
|
+
alert?.addAlert({ message: 'No camera devices found.', alertType: 'error' })
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const selectedDeviceId = videoInputDevices[0].deviceId
|
|
32
|
+
barcodeReader.decodeFromVideoDevice(selectedDeviceId, videoElementRef.value, (result: Result | undefined, error: Exception | undefined, controls: IScannerControls) => {
|
|
33
|
+
if (result) {
|
|
34
|
+
emit('decode', result.getText())
|
|
35
|
+
controls.stop()
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
emit('error', error)
|
|
41
|
+
alert?.addAlert({ message: 'Error starting camera.', alertType: 'error' })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function scanImageFile(selectedFile: File | File[] | undefined) {
|
|
46
|
+
if (!selectedFile) {
|
|
47
|
+
alert?.addAlert({ message: 'No file selected.', alertType: 'error' })
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const reader = new FileReader()
|
|
52
|
+
reader.onload = async (event) => {
|
|
53
|
+
try {
|
|
54
|
+
const result = await barcodeReader.decodeFromImageUrl(event.target?.result as string)
|
|
55
|
+
emit('decode', result.getText())
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
alert?.addAlert({ message: 'Unable to read barcode from image.', alertType: 'error' })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const scanImageSingleFile: File = Array.isArray(selectedFile) ? selectedFile[0] : selectedFile
|
|
62
|
+
reader.readAsDataURL(scanImageSingleFile)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
onMounted(async () => {
|
|
66
|
+
await checkCameraAvailability()
|
|
67
|
+
if (hasCameraAvailable.value) await startCamera()
|
|
68
|
+
})
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<template>
|
|
72
|
+
<v-card flat>
|
|
73
|
+
<v-card-item>
|
|
74
|
+
<v-col v-if="hasCameraAvailable">
|
|
75
|
+
<video
|
|
76
|
+
ref="videoElementRef"
|
|
77
|
+
autoplay
|
|
78
|
+
style="max-width: 1024px"
|
|
79
|
+
/>
|
|
80
|
+
<div style="z-index: 2000; position: relative; bottom: 84px; left: 550px">
|
|
81
|
+
<FileBtn
|
|
82
|
+
accept="image/*"
|
|
83
|
+
icon="mdi mdi-image-plus"
|
|
84
|
+
icon-only
|
|
85
|
+
@update:model-value="scanImageFile"
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
</v-col>
|
|
89
|
+
<v-col v-else>
|
|
90
|
+
<FileBtn
|
|
91
|
+
accept="image/*"
|
|
92
|
+
text="Upload Image"
|
|
93
|
+
@update:model-value="scanImageFile"
|
|
94
|
+
/>
|
|
95
|
+
</v-col>
|
|
96
|
+
</v-card-item>
|
|
97
|
+
</v-card>
|
|
98
|
+
</template>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import FullCalendar from '@fullcalendar/vue3'
|
|
3
|
+
import dayGridPlugin from '@fullcalendar/daygrid'
|
|
4
|
+
import timeGridPlugin from '@fullcalendar/timegrid'
|
|
5
|
+
import listPlugin from '@fullcalendar/list'
|
|
6
|
+
import interactionPlugin from '@fullcalendar/interaction'
|
|
7
|
+
import { ref, computed, withDefaults } from 'vue'
|
|
8
|
+
import { type CalendarOptions } from '@fullcalendar/core'
|
|
9
|
+
|
|
10
|
+
type Event = {
|
|
11
|
+
id: string | undefined
|
|
12
|
+
title: string
|
|
13
|
+
start: string // YYYY-MM-DD | YYYY-MM-DD HH:mm:ss
|
|
14
|
+
end: string // YYYY-MM-DD
|
|
15
|
+
color: string
|
|
16
|
+
detail?: string
|
|
17
|
+
props: Record<string, any> | any[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface CalendarProps {
|
|
21
|
+
modelValue: Event[]
|
|
22
|
+
locale?: 'th' | 'en'
|
|
23
|
+
toolBarLeft?: string
|
|
24
|
+
toolBarCenter?: string
|
|
25
|
+
toolBarRight?: string
|
|
26
|
+
height?: string | number
|
|
27
|
+
selectViewDay?: string
|
|
28
|
+
mode: 'dayGridMonth' | 'timeGridDay'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const props = withDefaults(defineProps<CalendarProps>(), {
|
|
32
|
+
locale: 'th',
|
|
33
|
+
height: 1200,
|
|
34
|
+
toolBarLeft: 'today prev,next',
|
|
35
|
+
toolBarCenter: 'title',
|
|
36
|
+
toolBarRight: 'timeGridDay,timeGridWeek,dayGridMonth',
|
|
37
|
+
mode: 'dayGridMonth',
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const emit = defineEmits(['select', 'dialog'])
|
|
41
|
+
|
|
42
|
+
const buttonText = computed(() => {
|
|
43
|
+
return {
|
|
44
|
+
today: props.locale === 'th' ? 'วันนี้' : 'today',
|
|
45
|
+
month: props.locale === 'th' ? 'เดือน' : 'month',
|
|
46
|
+
week: props.locale === 'th' ? 'สัปดาห์' : 'week',
|
|
47
|
+
day: props.locale === 'th' ? 'วัน' : 'day',
|
|
48
|
+
list: props.locale === 'th' ? 'รายการ' : 'list',
|
|
49
|
+
year: props.locale === 'th' ? 'ปี' : 'year',
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const calendarRef = ref<InstanceType<typeof FullCalendar>>()
|
|
54
|
+
const dateViewDay = ref({ date: '', props: {} })
|
|
55
|
+
|
|
56
|
+
const handleDateClick = (info: any) => {
|
|
57
|
+
emit('select', info.event)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const handleEventClick = (info: any) => {
|
|
61
|
+
emit('dialog', true)
|
|
62
|
+
emit('select', info.event)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const eventContentDay = (arg: any) => {
|
|
66
|
+
const list = document.createElement('div')
|
|
67
|
+
list.innerHTML = document.getElementById(arg.event.id.toString())?.innerHTML || ''
|
|
68
|
+
return { domNodes: [list] }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// @ts-ignore
|
|
72
|
+
const calendarOptions = ref<CalendarOptions>({
|
|
73
|
+
plugins: [dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin],
|
|
74
|
+
initialView: props.mode,
|
|
75
|
+
locale: props.locale,
|
|
76
|
+
dayMaxEvents: true,
|
|
77
|
+
timeZone: 'UTC',
|
|
78
|
+
headerToolbar: {
|
|
79
|
+
left: props.toolBarLeft,
|
|
80
|
+
center: props.toolBarCenter,
|
|
81
|
+
right: props.toolBarRight,
|
|
82
|
+
},
|
|
83
|
+
buttonText: buttonText.value,
|
|
84
|
+
events: props.modelValue,
|
|
85
|
+
initialDate: props.selectViewDay,
|
|
86
|
+
dateClick: handleDateClick,
|
|
87
|
+
eventClick: handleEventClick,
|
|
88
|
+
nowIndicator: true,
|
|
89
|
+
height: props.height,
|
|
90
|
+
eventColor: '#378006',
|
|
91
|
+
})
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<template>
|
|
95
|
+
<FullCalendar
|
|
96
|
+
ref="calendarRef"
|
|
97
|
+
:options="calendarOptions as CalendarOptions"
|
|
98
|
+
/>
|
|
99
|
+
</template>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, onMounted, watchEffect } from 'vue'
|
|
3
|
+
import { useDevicesList, useUserMedia } from '@vueuse/core'
|
|
4
|
+
import { useAlert } from '../composables/alert'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
modelValue?: string
|
|
8
|
+
}
|
|
9
|
+
const props = defineProps<Props>()
|
|
10
|
+
const emit = defineEmits(['update:modelValue', 'closeDialog'])
|
|
11
|
+
|
|
12
|
+
const alert = useAlert()
|
|
13
|
+
const videoRef = ref<HTMLVideoElement>()
|
|
14
|
+
const capturedPhoto = ref<string | null>(null)
|
|
15
|
+
const currentCameraId = ref<ConstrainDOMString | undefined>()
|
|
16
|
+
|
|
17
|
+
const { videoInputs: cameras } = useDevicesList({
|
|
18
|
+
requestPermissions: true,
|
|
19
|
+
constraints: { audio: false, video: true },
|
|
20
|
+
onUpdated() {
|
|
21
|
+
if (!cameras.value.find(camera => camera.deviceId === currentCameraId.value))
|
|
22
|
+
currentCameraId.value = cameras.value[0]?.deviceId
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const { stream, enabled } = useUserMedia({
|
|
27
|
+
constraints: { video: { deviceId: currentCameraId.value } },
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
watchEffect(() => {
|
|
31
|
+
if (videoRef.value) (videoRef.value as HTMLVideoElement).srcObject = stream.value!
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const captureImage = () => {
|
|
35
|
+
if (videoRef.value) {
|
|
36
|
+
const canvas = document.createElement('canvas')
|
|
37
|
+
canvas.width = (videoRef.value as HTMLVideoElement).videoWidth
|
|
38
|
+
canvas.height = (videoRef.value as HTMLVideoElement).videoHeight
|
|
39
|
+
const context = canvas.getContext('2d')
|
|
40
|
+
if (context) {
|
|
41
|
+
context.drawImage(videoRef.value as HTMLVideoElement, 0, 0, canvas.width, canvas.height)
|
|
42
|
+
capturedPhoto.value = canvas.toDataURL('image/jpeg')
|
|
43
|
+
|
|
44
|
+
enabled.value = false
|
|
45
|
+
emit('update:modelValue', capturedPhoto.value)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function loadImageFile(selectedFile: File | File[] | undefined) {
|
|
51
|
+
if (!selectedFile) {
|
|
52
|
+
alert?.addAlert({ message: 'No file selected.', alertType: 'error' })
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const reader = new FileReader()
|
|
57
|
+
reader.onload = (event) => {
|
|
58
|
+
capturedPhoto.value = event.target?.result as string
|
|
59
|
+
|
|
60
|
+
enabled.value = false
|
|
61
|
+
emit('update:modelValue', capturedPhoto.value)
|
|
62
|
+
}
|
|
63
|
+
const scanImageSingleFile: File = Array.isArray(selectedFile) ? selectedFile[0] : selectedFile
|
|
64
|
+
reader.readAsDataURL(scanImageSingleFile)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const openCamera = () => {
|
|
68
|
+
enabled.value = true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const closeDialog = () => {
|
|
72
|
+
enabled.value = false
|
|
73
|
+
emit('closeDialog')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
defineExpose({
|
|
77
|
+
openCamera,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
onMounted(() => {
|
|
81
|
+
enabled.value = true
|
|
82
|
+
})
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<template>
|
|
86
|
+
<v-card>
|
|
87
|
+
<v-card-title class="d-flex justify-end">
|
|
88
|
+
<v-btn
|
|
89
|
+
icon="fa:fa-solid fa-xmark"
|
|
90
|
+
variant="text"
|
|
91
|
+
@click="closeDialog"
|
|
92
|
+
/>
|
|
93
|
+
</v-card-title>
|
|
94
|
+
<v-card-text class="d-flex justify-center">
|
|
95
|
+
<video
|
|
96
|
+
ref="videoRef"
|
|
97
|
+
autoplay
|
|
98
|
+
style="max-width: 1024px"
|
|
99
|
+
/>
|
|
100
|
+
<div style="z-index: 2000; position: relative; bottom: -410px; left: -80px; height: 50px">
|
|
101
|
+
<FileBtn
|
|
102
|
+
accept="image/*"
|
|
103
|
+
icon="mdi mdi-image-plus"
|
|
104
|
+
icon-only
|
|
105
|
+
@update:model-value="loadImageFile"
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
</v-card-text>
|
|
109
|
+
<v-card-actions class="d-flex justify-center">
|
|
110
|
+
<v-btn icon size="x-large" variant="tonal" @click="captureImage()">
|
|
111
|
+
<v-icon icon="fa:fa-solid fa-circle" size="x-large"></v-icon>
|
|
112
|
+
<v-tooltip activator="parent" location="top">ถ่ายภาพ</v-tooltip>
|
|
113
|
+
</v-btn>
|
|
114
|
+
</v-card-actions>
|
|
115
|
+
</v-card>
|
|
116
|
+
</template>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, withDefaults } from 'vue'
|
|
3
|
+
import * as XLSX from 'xlsx'
|
|
4
|
+
import { VBtn } from 'vuetify/components/VBtn'
|
|
5
|
+
import { useAlert } from '../composables/alert'
|
|
6
|
+
|
|
7
|
+
interface ExportButtonProps extends /* @vue-ignore */ InstanceType<typeof VBtn['$props']> {
|
|
8
|
+
fileName?: string
|
|
9
|
+
modelValue?: object[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<ExportButtonProps>(), {
|
|
13
|
+
fileName: 'download',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const alert = useAlert()
|
|
17
|
+
const loading = ref(false)
|
|
18
|
+
|
|
19
|
+
function exportFile() {
|
|
20
|
+
if (props.modelValue && Array.isArray(props.modelValue)) {
|
|
21
|
+
loading.value = true
|
|
22
|
+
const workbook = XLSX.utils.book_new()
|
|
23
|
+
const worksheet = XLSX.utils.json_to_sheet(props.modelValue)
|
|
24
|
+
const fileName = `${props.fileName}.xlsx`
|
|
25
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
|
|
26
|
+
XLSX.writeFile(workbook, fileName)
|
|
27
|
+
loading.value = false
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
alert?.addAlert({ message: 'Invalid or no data to export', alertType: 'error' })
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<VBtn
|
|
37
|
+
v-bind="$attrs"
|
|
38
|
+
color="primary"
|
|
39
|
+
:loading="loading"
|
|
40
|
+
:disabled="loading"
|
|
41
|
+
text="Export CSV"
|
|
42
|
+
@click="exportFile"
|
|
43
|
+
>
|
|
44
|
+
<template
|
|
45
|
+
v-for="(_, slot, index) in ($slots as {})"
|
|
46
|
+
:key="index"
|
|
47
|
+
#[slot]="scope"
|
|
48
|
+
>
|
|
49
|
+
<slot
|
|
50
|
+
:name="slot"
|
|
51
|
+
v-bind="{ scope }"
|
|
52
|
+
/>
|
|
53
|
+
</template>
|
|
54
|
+
</VBtn>
|
|
55
|
+
</template>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, watch } from 'vue'
|
|
3
|
+
import { VBtn } from 'vuetify/components/VBtn'
|
|
4
|
+
|
|
5
|
+
interface Props extends /* @vue-ignore */ InstanceType<typeof VBtn['$props']> {
|
|
6
|
+
accept?: string
|
|
7
|
+
multiple?: boolean
|
|
8
|
+
iconOnly?: boolean
|
|
9
|
+
modelValue?: File | File[] | undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
13
|
+
multiple: false,
|
|
14
|
+
accept: '*',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const emit = defineEmits<{
|
|
18
|
+
(event: 'update:modelValue', value: File | File[] | undefined): void
|
|
19
|
+
}>()
|
|
20
|
+
|
|
21
|
+
const fileInput = ref<HTMLInputElement>()
|
|
22
|
+
const files = ref<File | File[]>()
|
|
23
|
+
|
|
24
|
+
const openFileInput = () => {
|
|
25
|
+
fileInput.value?.click()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
watch(files, () => {
|
|
29
|
+
emit('update:modelValue', files.value)
|
|
30
|
+
}, { deep: true })
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<v-btn
|
|
35
|
+
v-bind="$attrs"
|
|
36
|
+
@click="openFileInput"
|
|
37
|
+
>
|
|
38
|
+
<template
|
|
39
|
+
v-for="(_, slot, index) in ($slots as {})"
|
|
40
|
+
:key="index"
|
|
41
|
+
#[slot]="scope"
|
|
42
|
+
>
|
|
43
|
+
<slot
|
|
44
|
+
:name="slot"
|
|
45
|
+
v-bind="{ scope }"
|
|
46
|
+
/>
|
|
47
|
+
</template>
|
|
48
|
+
</v-btn>
|
|
49
|
+
<v-file-input
|
|
50
|
+
ref="fileInput"
|
|
51
|
+
v-model="files"
|
|
52
|
+
:accept="props.accept"
|
|
53
|
+
:multiple="props.multiple"
|
|
54
|
+
style="display: none"
|
|
55
|
+
/>
|
|
56
|
+
</template>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import * as XLSX from 'xlsx'
|
|
3
|
+
import { ref } from 'vue'
|
|
4
|
+
import { useAlert } from '../composables/alert'
|
|
5
|
+
|
|
6
|
+
const alert = useAlert()
|
|
7
|
+
const emit = defineEmits<{
|
|
8
|
+
(e: 'import', value: object[]): void
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const loading = ref(false)
|
|
12
|
+
|
|
13
|
+
function uploadedFile(files: File[] | File | undefined) {
|
|
14
|
+
if (!files) {
|
|
15
|
+
alert?.addAlert({ message: 'Please upload a file again', alertType: 'error' })
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (Array.isArray(files) && files.length != 1) {
|
|
20
|
+
alert?.addAlert({ message: 'Please select a single file for import', alertType: 'error' })
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (Array.isArray(files)) files = files[0]
|
|
25
|
+
|
|
26
|
+
const fileExtension = files.name.slice(files.name.lastIndexOf('.')).toLowerCase()
|
|
27
|
+
if (!['.xlsx', '.csv'].includes(fileExtension)) {
|
|
28
|
+
alert?.addAlert({ message: 'Please upload a file with .csv or .xlsx extension only (' + files.name + ')', alertType: 'error' })
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const reader = new FileReader()
|
|
33
|
+
reader.onload = (e: ProgressEvent<FileReader>) => {
|
|
34
|
+
const workbook = XLSX.read(e.target?.result)
|
|
35
|
+
emit('import', XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]]))
|
|
36
|
+
loading.value = false
|
|
37
|
+
}
|
|
38
|
+
loading.value = true
|
|
39
|
+
reader.readAsArrayBuffer(files)
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<FileBtn
|
|
45
|
+
v-bind="$attrs"
|
|
46
|
+
color="primary"
|
|
47
|
+
:loading="loading"
|
|
48
|
+
text="Import CSV"
|
|
49
|
+
accept=".csv, .xlsx"
|
|
50
|
+
:multiple="false"
|
|
51
|
+
@update:model-value="uploadedFile"
|
|
52
|
+
>
|
|
53
|
+
<template
|
|
54
|
+
v-for="(_, slot, index) in ($slots as {})"
|
|
55
|
+
:key="index"
|
|
56
|
+
#[slot]="scope"
|
|
57
|
+
>
|
|
58
|
+
<slot
|
|
59
|
+
:name="slot"
|
|
60
|
+
v-bind="{ scope }"
|
|
61
|
+
/>
|
|
62
|
+
</template>
|
|
63
|
+
</FileBtn>
|
|
64
|
+
</template>
|