Merge branch 'develop' of https://github.com/Frappet/bma-ehr-checkin into develop
All checks were successful
Build & Deploy on Dev / build (push) Successful in 4m32s
All checks were successful
Build & Deploy on Dev / build (push) Successful in 4m32s
This commit is contained in:
commit
d4f53653d2
9 changed files with 899 additions and 124 deletions
|
|
@ -1,52 +1,171 @@
|
|||
import { useQuasar } from 'quasar'
|
||||
import { onBeforeUnmount, ref } from 'vue'
|
||||
import { usePrivacyStore } from '@/stores/privacy'
|
||||
|
||||
type BrowserPermissionState = PermissionState | 'unsupported'
|
||||
|
||||
type PermissionQueryName = 'camera' | 'geolocation'
|
||||
|
||||
export function usePermissions() {
|
||||
const $q = useQuasar()
|
||||
const privacyStore = usePrivacyStore()
|
||||
const cameraPermissionState = ref<BrowserPermissionState>('prompt')
|
||||
const locationPermissionState = ref<BrowserPermissionState>('prompt')
|
||||
|
||||
// const checkCameraPermission = (): boolean => {
|
||||
// if (!privacyStore.isAccepted) {
|
||||
// privacyStore.modalPrivacy = true
|
||||
// $q.notify({
|
||||
// type: 'warning',
|
||||
// message: 'กรุณายอมรับนโยบายคุ้มครองข้อมูลส่วนบุคคลก่อนใช้งานกล้อง',
|
||||
// position: 'top',
|
||||
// })
|
||||
// return false
|
||||
// }
|
||||
// return true
|
||||
// }
|
||||
let cameraPermissionStatus: PermissionStatus | null = null
|
||||
let locationPermissionStatus: PermissionStatus | null = null
|
||||
|
||||
// const checkLocationPermission = (): boolean => {
|
||||
// if (!privacyStore.isAccepted) {
|
||||
// privacyStore.modalPrivacy = true
|
||||
// $q.notify({
|
||||
// type: 'warning',
|
||||
// message: 'กรุณายอมรับนโยบายคุ้มครองข้อมูลส่วนบุคคลก่อนใช้งานแผนที่',
|
||||
// position: 'top',
|
||||
// })
|
||||
// return false
|
||||
// }
|
||||
// return true
|
||||
// }
|
||||
const isPermissionsApiSupported = () =>
|
||||
typeof navigator !== 'undefined' && 'permissions' in navigator
|
||||
|
||||
const setPermissionState = (
|
||||
target: typeof cameraPermissionState | typeof locationPermissionState,
|
||||
state: BrowserPermissionState
|
||||
) => {
|
||||
target.value = state
|
||||
}
|
||||
|
||||
const setPermissionChangeListener = (
|
||||
name: PermissionQueryName,
|
||||
status: PermissionStatus
|
||||
) => {
|
||||
status.onchange = () => {
|
||||
const target =
|
||||
name === 'camera' ? cameraPermissionState : locationPermissionState
|
||||
setPermissionState(target, status.state)
|
||||
}
|
||||
}
|
||||
|
||||
async function queryPermissionState(name: PermissionQueryName) {
|
||||
if (!isPermissionsApiSupported()) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return await navigator.permissions.query({ name } as PermissionDescriptor)
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function syncPermissionState(name: PermissionQueryName) {
|
||||
const target =
|
||||
name === 'camera' ? cameraPermissionState : locationPermissionState
|
||||
const previousStatus =
|
||||
name === 'camera' ? cameraPermissionStatus : locationPermissionStatus
|
||||
|
||||
if (previousStatus) {
|
||||
previousStatus.onchange = null
|
||||
}
|
||||
|
||||
const status = await queryPermissionState(name)
|
||||
if (!status) {
|
||||
if (target.value === 'prompt') {
|
||||
setPermissionState(target, 'unsupported')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (name === 'camera') {
|
||||
cameraPermissionStatus = status
|
||||
} else {
|
||||
locationPermissionStatus = status
|
||||
}
|
||||
|
||||
setPermissionState(target, status.state)
|
||||
setPermissionChangeListener(name, status)
|
||||
}
|
||||
|
||||
async function syncPermissionStates() {
|
||||
await Promise.all([
|
||||
syncPermissionState('camera'),
|
||||
syncPermissionState('geolocation'),
|
||||
])
|
||||
}
|
||||
|
||||
const checkPrivacyAccepted = (): boolean => {
|
||||
if (!privacyStore.isAccepted) {
|
||||
privacyStore.modalPrivacy = true
|
||||
// $q.notify({
|
||||
// type: 'warning',
|
||||
// message: 'กรุณายอมรับนโยบายคุ้มครองข้อมูลส่วนบุคคลก่อนใช้งาน',
|
||||
// position: 'center',
|
||||
// })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function requestCameraPermission() {
|
||||
if (!checkPrivacyAccepted()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (cameraPermissionState.value === 'granted') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!navigator.mediaDevices?.getUserMedia) {
|
||||
setPermissionState(cameraPermissionState, 'unsupported')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: 'user' },
|
||||
audio: false,
|
||||
})
|
||||
|
||||
stream.getTracks().forEach((track) => track.stop())
|
||||
setPermissionState(cameraPermissionState, 'granted')
|
||||
return true
|
||||
} catch (error) {
|
||||
setPermissionState(cameraPermissionState, 'denied')
|
||||
return false
|
||||
} finally {
|
||||
await syncPermissionState('camera')
|
||||
}
|
||||
}
|
||||
|
||||
async function requestLocationPermission() {
|
||||
if (!checkPrivacyAccepted()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (locationPermissionState.value === 'granted') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!navigator.geolocation) {
|
||||
setPermissionState(locationPermissionState, 'unsupported')
|
||||
return false
|
||||
}
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
async () => {
|
||||
setPermissionState(locationPermissionState, 'granted')
|
||||
await syncPermissionState('geolocation')
|
||||
resolve(true)
|
||||
},
|
||||
async () => {
|
||||
setPermissionState(locationPermissionState, 'denied')
|
||||
await syncPermissionState('geolocation')
|
||||
resolve(false)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (cameraPermissionStatus) {
|
||||
cameraPermissionStatus.onchange = null
|
||||
}
|
||||
|
||||
if (locationPermissionStatus) {
|
||||
locationPermissionStatus.onchange = null
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
// checkCameraPermission,
|
||||
// checkLocationPermission,
|
||||
cameraPermissionState,
|
||||
locationPermissionState,
|
||||
checkPrivacyAccepted,
|
||||
syncPermissionStates,
|
||||
requestCameraPermission,
|
||||
requestLocationPermission,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch, onBeforeUnmount, computed } from 'vue'
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
onMounted,
|
||||
watch,
|
||||
onBeforeUnmount,
|
||||
computed,
|
||||
nextTick,
|
||||
} from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { format } from 'date-fns'
|
||||
import Camera from 'simple-vue-camera'
|
||||
|
|
@ -18,7 +26,12 @@ import MapCheck from '@/components/AscGISMap.vue'
|
|||
const mixin = useCounterMixin()
|
||||
const { date2Thai, showLoader, hideLoader, messageError, dialogConfirm } = mixin
|
||||
const $q = useQuasar()
|
||||
const { checkPrivacyAccepted } = usePermissions()
|
||||
const {
|
||||
cameraPermissionState,
|
||||
checkPrivacyAccepted,
|
||||
syncPermissionStates,
|
||||
requestCameraPermission,
|
||||
} = usePermissions()
|
||||
const privacyStore = usePrivacyStore()
|
||||
const positionKeycloakStore = usePositionKeycloakStore()
|
||||
const MOCK_CHECK_DELAY_MS = 800
|
||||
|
|
@ -50,10 +63,40 @@ const msgCheckTime = ref<string>('') // ข้อความแจ้งเต
|
|||
const isDisabledCheckTime = ref<boolean>(true) // ข้อความแจ้งเตือน
|
||||
const isErr = ref<boolean | null>(null) // ข้อความแจ้งเตือน
|
||||
const endTimeAfternoon = ref<string>('12:00:00') //เวลาเช็คเอาท์ตามรอบ
|
||||
const crossDayCheckoutWarning = ref<string>('')
|
||||
|
||||
const isLoadingCheckTime = ref<boolean>(false) // ตัวแปรสำหรับการโหลด
|
||||
const disabledBtn = ref<boolean>(false)
|
||||
|
||||
function parseApiDateTime(value?: string | null) {
|
||||
if (!value) return null
|
||||
|
||||
const normalizedValue = value.includes('T') ? value : value.replace(' ', 'T')
|
||||
const parsedDate = new Date(normalizedValue)
|
||||
return Number.isNaN(parsedDate.getTime()) ? null : parsedDate
|
||||
}
|
||||
|
||||
function isSameCalendarDay(firstDate: Date, secondDate: Date) {
|
||||
return (
|
||||
firstDate.getFullYear() === secondDate.getFullYear() &&
|
||||
firstDate.getMonth() === secondDate.getMonth() &&
|
||||
firstDate.getDate() === secondDate.getDate()
|
||||
)
|
||||
}
|
||||
|
||||
function updateCrossDayCheckoutWarning(checkInTime?: string | null) {
|
||||
crossDayCheckoutWarning.value = ''
|
||||
|
||||
const checkInDate = parseApiDateTime(checkInTime)
|
||||
if (!checkInDate) return
|
||||
|
||||
if (!isSameCalendarDay(checkInDate, new Date())) {
|
||||
crossDayCheckoutWarning.value = `คุณยังไม่ได้ลงเวลาออกของวันที่ ${date2Thai(
|
||||
checkInDate
|
||||
)} กรุณาลงเวลาออกก่อน จึงจะลงเวลาเข้าของวันนี้ได้`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch เช็คเวลาต้องลงเวลาเข้าหรือออกงาน
|
||||
*/
|
||||
|
|
@ -66,6 +109,7 @@ async function fetchCheckTime(load: any = true) {
|
|||
|
||||
statusCheckin.value = data.checkInId ? false : true
|
||||
checkInId.value = data.checkInId ? data.checkInId : ''
|
||||
updateCrossDayCheckoutWarning(data.checkInTime)
|
||||
endTimeAfternoon.value = data.endTimeAfternoon
|
||||
isDisabledCheckTime.value = isDisabledCheckTime.value ? true : false
|
||||
})
|
||||
|
|
@ -89,7 +133,7 @@ const formattedM = ref()
|
|||
const formattedH = ref()
|
||||
const formattedHH = ref()
|
||||
const formattedA = ref()
|
||||
const clockInterval = ref<number | undefined>()
|
||||
const clockInterval = ref<ReturnType<typeof setInterval> | undefined>()
|
||||
|
||||
/** function อัพเดทเวลา*/
|
||||
function updateClock() {
|
||||
|
|
@ -118,6 +162,7 @@ const useLocation = ref<string>('') //กรณีเลือกนอกสถ
|
|||
const fileImg = ref<any>() //รูปถ่ายสถานที่
|
||||
const remark = ref<string>('') //ข้อความหมายเหตุที่ต้องการระบุเพิ่ม
|
||||
const checkInId = ref<string>('') //Id ลงเวลา check-in ล่าสุดที่ยังไม่ลงเวลาออก
|
||||
const MIN_CAPTURE_FILE_SIZE_BYTES = 5 * 1024
|
||||
|
||||
/**
|
||||
* funciton เรียกพิกัดละติจูด พิกัดลองติจูด
|
||||
|
|
@ -150,6 +195,131 @@ function wait(ms: number) {
|
|||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function getCameraVideoElement() {
|
||||
return camera.value?.video
|
||||
}
|
||||
|
||||
function prepareInlineCameraVideo(videoElement: HTMLVideoElement) {
|
||||
videoElement.autoplay = true
|
||||
videoElement.muted = true
|
||||
videoElement.setAttribute('autoplay', '')
|
||||
videoElement.setAttribute('muted', '')
|
||||
videoElement.setAttribute('playsinline', '')
|
||||
videoElement.setAttribute('webkit-playsinline', 'true')
|
||||
}
|
||||
|
||||
function isInlineCameraPreviewReady(videoElement: HTMLVideoElement) {
|
||||
return (
|
||||
Boolean(videoElement.srcObject) &&
|
||||
videoElement.videoWidth > 0 &&
|
||||
videoElement.videoHeight > 0 &&
|
||||
videoElement.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA
|
||||
)
|
||||
}
|
||||
|
||||
async function waitForCameraInstance() {
|
||||
for (let attempt = 0; attempt < 10; attempt += 1) {
|
||||
await nextTick()
|
||||
|
||||
if (camera.value) {
|
||||
return camera.value
|
||||
}
|
||||
|
||||
await wait(50)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async function stopInlineCamera() {
|
||||
camera.value?.stop()
|
||||
cameraIsOn.value = false
|
||||
}
|
||||
|
||||
async function remountInlineCameraComponent() {
|
||||
await stopInlineCamera()
|
||||
cameraMountKey.value += 1
|
||||
await nextTick()
|
||||
await wait(INLINE_CAMERA_REMOUNT_DELAY_MS)
|
||||
}
|
||||
|
||||
async function ensureInlineCameraPreviewReady() {
|
||||
const deadline = Date.now() + INLINE_CAMERA_PREVIEW_TIMEOUT_MS
|
||||
let lastPlaybackError: unknown = null
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const videoElement = getCameraVideoElement()
|
||||
|
||||
if (videoElement) {
|
||||
prepareInlineCameraVideo(videoElement)
|
||||
|
||||
if (isInlineCameraPreviewReady(videoElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (videoElement.srcObject) {
|
||||
try {
|
||||
await videoElement.play()
|
||||
} catch (error) {
|
||||
lastPlaybackError = error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await wait(INLINE_CAMERA_PREVIEW_POLL_MS)
|
||||
}
|
||||
|
||||
if (lastPlaybackError instanceof Error) {
|
||||
throw lastPlaybackError
|
||||
}
|
||||
|
||||
throw new Error('Inline camera preview did not become ready in time')
|
||||
}
|
||||
|
||||
async function syncActiveCameraDevices() {
|
||||
const devices: any = await camera.value?.devices(['videoinput'])
|
||||
if (!devices) {
|
||||
return
|
||||
}
|
||||
|
||||
availableCameras.value = devices
|
||||
|
||||
const activeId = camera.value?.currentDeviceID()
|
||||
const activeIdx = activeId
|
||||
? devices.findIndex((device: any) => device.deviceId === activeId)
|
||||
: -1
|
||||
currentCameraIndex.value = activeIdx >= 0 ? activeIdx : 0
|
||||
currentCameraType.value = resolveCameraType(
|
||||
devices[currentCameraIndex.value]?.label || '',
|
||||
'front'
|
||||
)
|
||||
}
|
||||
|
||||
async function startInlineCamera(recoveryAttempt = 0): Promise<void> {
|
||||
const cameraInstance = await waitForCameraInstance()
|
||||
|
||||
if (!cameraInstance) {
|
||||
throw new Error('Camera component is unavailable')
|
||||
}
|
||||
|
||||
try {
|
||||
currentCameraType.value = 'front'
|
||||
await cameraInstance.start()
|
||||
await ensureInlineCameraPreviewReady()
|
||||
await syncActiveCameraDevices()
|
||||
cameraIsOn.value = true
|
||||
} catch (error) {
|
||||
await stopInlineCamera()
|
||||
|
||||
if (recoveryAttempt < MAX_INLINE_CAMERA_RECOVERY_ATTEMPTS) {
|
||||
await remountInlineCameraComponent()
|
||||
return startInlineCamera(recoveryAttempt + 1)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function getDelayedFreshPosition() {
|
||||
const firstPosition = await getCurrentPositionAsync({
|
||||
enableHighAccuracy: true,
|
||||
|
|
@ -215,11 +385,227 @@ const cameraIsOn = ref<boolean>(false)
|
|||
const img = ref<any>(undefined)
|
||||
const photoWidth = ref<number>(350)
|
||||
const photoHeight = ref<number>(350)
|
||||
const cameraMountKey = ref<number>(0)
|
||||
const availableCameras = ref<any[]>([])
|
||||
const currentCameraIndex = ref<number>(0)
|
||||
const currentCameraType = ref<'front' | 'back' | 'unknown'>('unknown')
|
||||
|
||||
const intervalId = ref<number | undefined>(undefined) // ต้องใช้ตัวแปรเก็บค่า interval
|
||||
/**
|
||||
* Detect real iOS/iPadOS devices. Desktop mobile emulation can spoof the
|
||||
* iPhone user-agent, so rely on platform + touch capability instead.
|
||||
*/
|
||||
const isIOSDevice: boolean = (() => {
|
||||
const platform = navigator.platform || ''
|
||||
const hasTouchPoints = navigator.maxTouchPoints > 1
|
||||
|
||||
return (
|
||||
/iPhone|iPad|iPod/.test(platform) ||
|
||||
(platform === 'MacIntel' && hasTouchPoints)
|
||||
)
|
||||
})()
|
||||
|
||||
const isAndroidDevice: boolean = /Android/i.test(navigator.userAgent)
|
||||
/**
|
||||
* `preferNativePhotoCapture` starts false on every device — inline camera is
|
||||
* always attempted first. It is set to true only by
|
||||
* `switchToNativePhotoCapture()` / `enableNativePhotoCaptureFallback()` when
|
||||
* inline capture is proven to be unsupported or has failed. This covers iOS
|
||||
* Safari too: getUserMedia is supported there since iOS 14.3, so we try
|
||||
* inline first and fall back gracefully on first failure.
|
||||
*/
|
||||
const preferNativePhotoCapture = ref<boolean>(false)
|
||||
const useNativePhotoCapture = computed(() => preferNativePhotoCapture.value)
|
||||
const centeredPreviewImageStyle = Object.freeze({
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center center',
|
||||
})
|
||||
const INLINE_CAMERA_PREVIEW_TIMEOUT_MS = isIOSDevice ? 2500 : 1800
|
||||
const INLINE_CAMERA_PREVIEW_POLL_MS = 120
|
||||
const INLINE_CAMERA_REMOUNT_DELAY_MS = 120
|
||||
const MAX_INLINE_CAMERA_RECOVERY_ATTEMPTS = 1
|
||||
|
||||
/** Ref for the hidden file input used as native camera fallback */
|
||||
const nativePhotoInput = ref<HTMLInputElement | null>(null)
|
||||
const DEFAULT_NORMALIZED_IMAGE_QUALITY = 0.92
|
||||
|
||||
interface NormalizePhotoOptions {
|
||||
fileName?: string
|
||||
mirrorHorizontally?: boolean
|
||||
quality?: number
|
||||
}
|
||||
|
||||
function clearSelectedPhoto() {
|
||||
if (img.value) {
|
||||
URL.revokeObjectURL(img.value)
|
||||
img.value = undefined
|
||||
}
|
||||
fileImg.value = undefined
|
||||
}
|
||||
|
||||
async function canDecodeImageBlob(blob: Blob) {
|
||||
if (!blob.type.startsWith('image/')) {
|
||||
return false
|
||||
}
|
||||
|
||||
const imageUrl = URL.createObjectURL(blob)
|
||||
|
||||
try {
|
||||
const image = await new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const element = new Image()
|
||||
element.onload = () => {
|
||||
if (element.naturalWidth > 0 && element.naturalHeight > 0) {
|
||||
resolve(element)
|
||||
} else {
|
||||
reject(new Error('Image has invalid dimensions'))
|
||||
}
|
||||
}
|
||||
element.onerror = () => reject(new Error('Image decode failed'))
|
||||
element.src = imageUrl
|
||||
})
|
||||
|
||||
return image.naturalWidth > 0 && image.naturalHeight > 0
|
||||
} catch (error) {
|
||||
console.error('Error decoding captured image:', error)
|
||||
return false
|
||||
} finally {
|
||||
URL.revokeObjectURL(imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
async function isValidCapturedImage(blob: Blob) {
|
||||
if (blob.size < MIN_CAPTURE_FILE_SIZE_BYTES) {
|
||||
return false
|
||||
}
|
||||
|
||||
return canDecodeImageBlob(blob)
|
||||
}
|
||||
|
||||
async function loadImageFromBlob(blob: Blob) {
|
||||
const imageUrl = URL.createObjectURL(blob)
|
||||
|
||||
try {
|
||||
return await new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const image = new Image()
|
||||
image.onload = () => resolve(image)
|
||||
image.onerror = () => reject(new Error('Image load failed'))
|
||||
image.src = imageUrl
|
||||
})
|
||||
} finally {
|
||||
URL.revokeObjectURL(imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
async function normalizePhotoFile(
|
||||
source: Blob,
|
||||
options: NormalizePhotoOptions = {}
|
||||
) {
|
||||
const fileName =
|
||||
options.fileName ??
|
||||
(source instanceof File && source.name
|
||||
? source.name
|
||||
: `photo_${Date.now()}.jpg`)
|
||||
const imageType =
|
||||
source.type && source.type.startsWith('image/') ? source.type : 'image/jpeg'
|
||||
|
||||
if (!options.mirrorHorizontally) {
|
||||
if (source instanceof File && source.name === fileName) {
|
||||
return source
|
||||
}
|
||||
|
||||
return new File([source], fileName, { type: imageType })
|
||||
}
|
||||
|
||||
const image = await loadImageFromBlob(source)
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = image.naturalWidth
|
||||
canvas.height = image.naturalHeight
|
||||
|
||||
const context = canvas.getContext('2d')
|
||||
if (!context) {
|
||||
throw new Error('Canvas context unavailable')
|
||||
}
|
||||
|
||||
context.translate(canvas.width, 0)
|
||||
context.scale(-1, 1)
|
||||
context.drawImage(image, 0, 0, canvas.width, canvas.height)
|
||||
|
||||
const normalizedBlob = await new Promise<Blob | null>((resolve) => {
|
||||
canvas.toBlob(
|
||||
resolve,
|
||||
imageType,
|
||||
options.quality ?? DEFAULT_NORMALIZED_IMAGE_QUALITY
|
||||
)
|
||||
})
|
||||
|
||||
if (!normalizedBlob) {
|
||||
throw new Error('Failed to normalize image')
|
||||
}
|
||||
|
||||
return new File([normalizedBlob], fileName, { type: imageType })
|
||||
}
|
||||
|
||||
async function assignSelectedPhoto(file: File) {
|
||||
clearSelectedPhoto()
|
||||
fileImg.value = file
|
||||
img.value = URL.createObjectURL(file)
|
||||
}
|
||||
|
||||
function openNativePhotoCapture() {
|
||||
void stopInlineCamera()
|
||||
|
||||
if (nativePhotoInput.value) {
|
||||
nativePhotoInput.value.value = ''
|
||||
nativePhotoInput.value.click()
|
||||
}
|
||||
}
|
||||
|
||||
function enableNativePhotoCaptureFallback() {
|
||||
preferNativePhotoCapture.value = true
|
||||
void stopInlineCamera()
|
||||
clearSelectedPhoto()
|
||||
}
|
||||
|
||||
function switchToNativePhotoCapture() {
|
||||
enableNativePhotoCaptureFallback()
|
||||
openNativePhotoCapture()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user selects / captures a photo via the native file-input
|
||||
* fallback. Keeps the same `img` + `fileImg` state that capturePhoto()
|
||||
* sets so the upload flow is identical.
|
||||
*/
|
||||
async function onNativePhotoSelected(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
input.value = ''
|
||||
if (!file) return
|
||||
|
||||
if (!(await isValidCapturedImage(file))) {
|
||||
clearSelectedPhoto()
|
||||
messageError($q, '', 'ไม่สามารถอ่านไฟล์รูปภาพได้ กรุณาถ่ายรูปใหม่อีกครั้ง')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const normalizedFile = await normalizePhotoFile(file, {
|
||||
fileName: file.name,
|
||||
mirrorHorizontally: !isIOSDevice,
|
||||
})
|
||||
|
||||
if (!(await isValidCapturedImage(normalizedFile))) {
|
||||
throw new Error('Normalized image is invalid')
|
||||
}
|
||||
|
||||
await assignSelectedPhoto(normalizedFile)
|
||||
} catch (error) {
|
||||
console.error('Error normalizing native photo:', error)
|
||||
clearSelectedPhoto()
|
||||
messageError($q, error, 'ไม่สามารถเตรียมรูปภาพได้ กรุณาถ่ายรูปใหม่อีกครั้ง')
|
||||
}
|
||||
}
|
||||
|
||||
const intervalId = ref<ReturnType<typeof setInterval> | undefined>(undefined)
|
||||
|
||||
/**
|
||||
* เริ่มจาก onMounted #1 เช็ค status คิว
|
||||
|
|
@ -339,6 +725,14 @@ function identifyCameraType(label: string): 'front' | 'back' | 'unknown' {
|
|||
return 'unknown'
|
||||
}
|
||||
|
||||
function resolveCameraType(
|
||||
label: string,
|
||||
fallbackType: 'front' | 'back'
|
||||
): 'front' | 'back' {
|
||||
const detectedType = identifyCameraType(label)
|
||||
return detectedType === 'unknown' ? fallbackType : detectedType
|
||||
}
|
||||
|
||||
/** function เปิดกล้อง*/
|
||||
async function openCamera() {
|
||||
// เช็คสิทธิ์ privacy ก่อนเปิดกล้อง
|
||||
|
|
@ -346,20 +740,35 @@ async function openCamera() {
|
|||
return
|
||||
}
|
||||
|
||||
if (!isPermissionCameraDenied.value) {
|
||||
// change camera device
|
||||
if (cameraIsOn.value) {
|
||||
camera.value?.stop()
|
||||
} else {
|
||||
await camera.value?.start()
|
||||
const devices: any = await camera.value?.devices(['videoinput'])
|
||||
if (devices) {
|
||||
availableCameras.value = devices
|
||||
await changeCamera()
|
||||
}
|
||||
// If inline capture previously failed on this device, fall back to the
|
||||
// native still-photo picker instead of reopening the live camera.
|
||||
if (useNativePhotoCapture.value) {
|
||||
openNativePhotoCapture()
|
||||
return
|
||||
}
|
||||
|
||||
if (cameraIsOn.value) {
|
||||
await stopInlineCamera()
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
(isIOSDevice || isAndroidDevice) &&
|
||||
!useNativePhotoCapture.value &&
|
||||
(cameraPermissionState.value === 'denied' ||
|
||||
cameraPermissionState.value === 'unsupported')
|
||||
) {
|
||||
switchToNativePhotoCapture()
|
||||
return
|
||||
}
|
||||
|
||||
const hasCameraPermission = await requestCameraPermission()
|
||||
if (!hasCameraPermission) {
|
||||
if ((isIOSDevice || isAndroidDevice) && !useNativePhotoCapture.value) {
|
||||
switchToNativePhotoCapture()
|
||||
return
|
||||
}
|
||||
cameraIsOn.value = !cameraIsOn.value
|
||||
} else {
|
||||
|
||||
messageError(
|
||||
$q,
|
||||
'',
|
||||
|
|
@ -367,6 +776,19 @@ async function openCamera() {
|
|||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await startInlineCamera()
|
||||
} catch (error) {
|
||||
console.error('Error opening camera:', error)
|
||||
|
||||
if ((isIOSDevice || isAndroidDevice) && !useNativePhotoCapture.value) {
|
||||
switchToNativePhotoCapture()
|
||||
return
|
||||
}
|
||||
|
||||
messageError($q, error, 'ไม่สามารถเปิดกล้องได้ กรุณาลองใหม่อีกครั้ง')
|
||||
}
|
||||
}
|
||||
|
||||
/** change camera device*/
|
||||
|
|
@ -383,9 +805,16 @@ async function changeCamera(targetCameraType?: 'front' | 'back') {
|
|||
|
||||
if (devices.length === 1 || !targetCameraType) {
|
||||
const device = devices[0]
|
||||
const fallbackType =
|
||||
targetCameraType ??
|
||||
(currentCameraType.value === 'back' ? 'back' : 'front')
|
||||
await camera.value?.changeCamera(device.deviceId)
|
||||
await ensureInlineCameraPreviewReady()
|
||||
currentCameraIndex.value = 0
|
||||
currentCameraType.value = identifyCameraType(device.label || '')
|
||||
currentCameraType.value = resolveCameraType(
|
||||
device.label || '',
|
||||
fallbackType
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -397,14 +826,22 @@ async function changeCamera(targetCameraType?: 'front' | 'back') {
|
|||
if (matchingCameras.length > 0) {
|
||||
const targetDevice = matchingCameras[0]
|
||||
await camera.value?.changeCamera(targetDevice.deviceId)
|
||||
await ensureInlineCameraPreviewReady()
|
||||
currentCameraIndex.value = devices.indexOf(targetDevice)
|
||||
currentCameraType.value = targetCameraType
|
||||
currentCameraType.value = resolveCameraType(
|
||||
targetDevice.label || '',
|
||||
targetCameraType
|
||||
)
|
||||
} else {
|
||||
const nextIndex = (currentCameraIndex.value + 1) % devices.length
|
||||
const nextDevice = devices[nextIndex]
|
||||
await camera.value?.changeCamera(nextDevice.deviceId)
|
||||
await ensureInlineCameraPreviewReady()
|
||||
currentCameraIndex.value = nextIndex
|
||||
currentCameraType.value = identifyCameraType(nextDevice.label || '')
|
||||
currentCameraType.value = resolveCameraType(
|
||||
nextDevice.label || '',
|
||||
targetCameraType
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error switching camera:', error)
|
||||
|
|
@ -420,34 +857,75 @@ async function switchCamera() {
|
|||
// สลับแค่ระหว่างกล้อง 2 ตัวแรก (กล้องหน้าและหลังหลัก)
|
||||
const targetIndex = currentCameraIndex.value === 0 ? 1 : 0
|
||||
const targetDevice = availableCameras.value[targetIndex]
|
||||
const fallbackType = currentCameraType.value === 'back' ? 'front' : 'back'
|
||||
|
||||
await camera.value?.changeCamera(targetDevice.deviceId)
|
||||
await ensureInlineCameraPreviewReady()
|
||||
currentCameraIndex.value = targetIndex
|
||||
currentCameraType.value = identifyCameraType(targetDevice.label || '')
|
||||
currentCameraType.value = resolveCameraType(
|
||||
targetDevice.label || '',
|
||||
fallbackType
|
||||
)
|
||||
}
|
||||
|
||||
/** function ถ่ายรูป*/
|
||||
async function capturePhoto() {
|
||||
const imageBlob: any = await camera.value?.snapshot(
|
||||
{ width: photoWidth.value, height: photoHeight.value },
|
||||
'image/png',
|
||||
0.5
|
||||
)
|
||||
if (!imageBlob) return
|
||||
const fileName = 'photo.jpg'
|
||||
//ไฟล์รูป
|
||||
const file = new File([imageBlob], fileName, { type: 'image/png' })
|
||||
fileImg.value = file
|
||||
//แสดงรูป
|
||||
camera.value?.stop()
|
||||
const url = URL.createObjectURL(imageBlob)
|
||||
img.value = url
|
||||
try {
|
||||
const imageBlob: any = await camera.value?.snapshot(
|
||||
{ width: photoWidth.value, height: photoHeight.value },
|
||||
'image/jpeg',
|
||||
0.8
|
||||
)
|
||||
if (!imageBlob || !(await isValidCapturedImage(imageBlob))) {
|
||||
if (isIOSDevice || isAndroidDevice) {
|
||||
switchToNativePhotoCapture()
|
||||
return
|
||||
}
|
||||
|
||||
messageError($q, '', 'ไม่สามารถถ่ายรูปได้ กรุณาลองใหม่อีกครั้ง')
|
||||
return
|
||||
}
|
||||
const fileName = `photo_${Date.now()}.jpg`
|
||||
const normalizedFile = await normalizePhotoFile(imageBlob, {
|
||||
fileName,
|
||||
mirrorHorizontally: false,
|
||||
quality: 0.8,
|
||||
})
|
||||
|
||||
if (!(await isValidCapturedImage(normalizedFile))) {
|
||||
throw new Error('Normalized image is invalid')
|
||||
}
|
||||
|
||||
await assignSelectedPhoto(normalizedFile)
|
||||
|
||||
//แสดงรูป
|
||||
await stopInlineCamera()
|
||||
} catch (error) {
|
||||
console.error('Error capturing photo:', error)
|
||||
|
||||
if ((isIOSDevice || isAndroidDevice) && !useNativePhotoCapture.value) {
|
||||
switchToNativePhotoCapture()
|
||||
return
|
||||
}
|
||||
|
||||
messageError($q, error, 'ไม่สามารถถ่ายรูปได้ กรุณาลองใหม่อีกครั้ง')
|
||||
}
|
||||
}
|
||||
|
||||
/** function เปลี่ยนรูปภาพ*/
|
||||
function refreshPhoto() {
|
||||
img.value = undefined
|
||||
camera.value?.start()
|
||||
async function refreshPhoto() {
|
||||
try {
|
||||
clearSelectedPhoto()
|
||||
|
||||
if (useNativePhotoCapture.value) {
|
||||
openNativePhotoCapture()
|
||||
return
|
||||
}
|
||||
await startInlineCamera()
|
||||
} catch (error) {
|
||||
console.error('Error refreshing photo:', error)
|
||||
messageError($q, error, 'ไม่สามารถเปิดกล้องได้ กรุณาลองใหม่อีกครั้ง')
|
||||
}
|
||||
}
|
||||
|
||||
/** ref validate*/
|
||||
|
|
@ -509,6 +987,7 @@ const mapRef = computed(() =>
|
|||
)
|
||||
|
||||
const timeChickin = ref<string>('') //เวลาเข้างาน,เวลาออกงาน
|
||||
const displayedCheckDate = ref<Date>(new Date())
|
||||
|
||||
/** function ยืนยันการลงเวลาเข้า - ออก*/
|
||||
async function confirm() {
|
||||
|
|
@ -524,6 +1003,16 @@ async function confirm() {
|
|||
return
|
||||
}
|
||||
|
||||
if (!fileImg.value || !(await isValidCapturedImage(fileImg.value))) {
|
||||
if ((isIOSDevice || isAndroidDevice) && !useNativePhotoCapture.value) {
|
||||
enableNativePhotoCaptureFallback()
|
||||
}
|
||||
clearSelectedPhoto()
|
||||
disabledBtn.value = false
|
||||
messageError($q, '', 'รูปภาพไม่สมบูรณ์ กรุณาถ่ายรูปใหม่อีกครั้งก่อนลงเวลา')
|
||||
return
|
||||
}
|
||||
|
||||
showLoader()
|
||||
const isLocation = workplace.value === 'in-place' //*true คือ ณ สถานที่ตั้ง, false คือ นอกสถานที่ตั้ง
|
||||
const locationName = workplace.value === 'in-place' ? '' : useLocation.value
|
||||
|
|
@ -541,6 +1030,7 @@ async function confirm() {
|
|||
.then(async (res) => {
|
||||
const data = await res.data.result
|
||||
const dateObject = new Date(data.date)
|
||||
displayedCheckDate.value = dateObject
|
||||
checkDate.value = data.date ? true : false
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
hour12: false,
|
||||
|
|
@ -627,7 +1117,7 @@ async function onClickConfirm() {
|
|||
try {
|
||||
showLoader()
|
||||
cameraIsOn.value = false
|
||||
img.value = undefined
|
||||
clearSelectedPhoto()
|
||||
modalTime.value = false
|
||||
if (!statusCheckin.value) {
|
||||
statusCheckin.value = true
|
||||
|
|
@ -665,13 +1155,8 @@ const inQueue = ref<boolean>(false)
|
|||
|
||||
// ฟังก์ชันสำหรับรีเซ็ตรูปและหยุดกล้อง
|
||||
function resetCameraAndImage() {
|
||||
if (img.value) {
|
||||
img.value = undefined
|
||||
}
|
||||
if (cameraIsOn.value && camera.value) {
|
||||
camera.value.stop()
|
||||
cameraIsOn.value = false
|
||||
}
|
||||
clearSelectedPhoto()
|
||||
void stopInlineCamera()
|
||||
}
|
||||
|
||||
// เพิ่มฟังก์ชันสำหรับจัดการการปิดแอพในมือถือ
|
||||
|
|
@ -697,16 +1182,6 @@ function handleVisibilityChange() {
|
|||
}
|
||||
}
|
||||
|
||||
const isPermissionCameraDenied = ref<boolean>(false) // ตัวแปรสำหรับตรวจสอบการปฏิเสธสิทธิ์กล้อง
|
||||
|
||||
async function requestCamera() {
|
||||
try {
|
||||
await navigator.mediaDevices.getUserMedia({ video: true })
|
||||
} catch (err) {
|
||||
isPermissionCameraDenied.value = true
|
||||
}
|
||||
}
|
||||
|
||||
/** Hook*/
|
||||
onMounted(async () => {
|
||||
// เริ่มต้น clock เสมอ
|
||||
|
|
@ -723,10 +1198,11 @@ onMounted(async () => {
|
|||
startChecking()
|
||||
}
|
||||
|
||||
await syncPermissionStates()
|
||||
|
||||
// เรียกแผนที่เฉพาะเมื่อยอมรับ privacy แล้ว
|
||||
if (privacyStore.isAccepted) {
|
||||
mapRef.value?.requestLocationPermission()
|
||||
requestCamera()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -765,10 +1241,10 @@ onBeforeUnmount(() => {
|
|||
|
||||
watch(
|
||||
() => privacyStore.isAccepted,
|
||||
(newVal) => {
|
||||
async (newVal) => {
|
||||
if (newVal) {
|
||||
await syncPermissionStates()
|
||||
mapRef.value?.requestLocationPermission()
|
||||
requestCamera()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -854,6 +1330,16 @@ watch(
|
|||
class="col-xs-12 col-md-11 q-pa-md q-col-gutter-md row q-pt-lg"
|
||||
v-if="!isDisabledCheckTime"
|
||||
>
|
||||
<div v-if="crossDayCheckoutWarning" class="col-12">
|
||||
<q-banner
|
||||
rounded
|
||||
class="bg-white text-orange-10 warning-banner"
|
||||
>
|
||||
<q-icon name="warning" color="warning" size="20px" />
|
||||
|
||||
{{ crossDayCheckoutWarning }}
|
||||
</q-banner>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-8 gt-xs">
|
||||
<div class="col-12">
|
||||
<MapCheck
|
||||
|
|
@ -863,7 +1349,7 @@ watch(
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-4 gt-xs">
|
||||
<div v-if="$q.screen.gt.xs" class="col-xs-12 col-sm-4">
|
||||
<q-card
|
||||
:class="
|
||||
$q.screen.xs ? 'card-container-xs' : 'card-container'
|
||||
|
|
@ -885,17 +1371,38 @@ watch(
|
|||
</div>
|
||||
<div class="col-12 row items-center">
|
||||
<!-- แสดงกล้องตอนกดถ่ายภาพ -->
|
||||
|
||||
<Camera
|
||||
:key="`desktop-${cameraMountKey}`"
|
||||
:resolution="{ width: photoWidth, height: photoHeight }"
|
||||
ref="camera"
|
||||
:class="['camera-preview']"
|
||||
:autoplay="false"
|
||||
:style="!img ? 'display: block' : 'display: none'"
|
||||
:playsinline="true"
|
||||
:facingMode="'user'"
|
||||
/>
|
||||
|
||||
<!-- แสดงรูปเมื่อกด capture -->
|
||||
<div v-if="img" class="image-container">
|
||||
<q-img :src="img" class="image-element"></q-img>
|
||||
<q-img
|
||||
:src="img"
|
||||
class="image-element"
|
||||
:img-style="centeredPreviewImageStyle"
|
||||
></q-img>
|
||||
<!-- Native capture: cameraIsOn stays false so show retake here
|
||||
the refresh button here instead of inside v-if="cameraIsOn" -->
|
||||
<div
|
||||
v-if="useNativePhotoCapture"
|
||||
class="absolute-bottom-right q-ma-md"
|
||||
>
|
||||
<q-btn
|
||||
round
|
||||
push
|
||||
icon="refresh"
|
||||
size="md"
|
||||
color="negative"
|
||||
@click="refreshPhoto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="cameraIsOn">
|
||||
|
|
@ -996,20 +1503,42 @@ watch(
|
|||
</div>
|
||||
<div class="col-12 row items-center">
|
||||
<!-- แสดงกล้องตอนกดถ่ายภาพ -->
|
||||
|
||||
<Camera
|
||||
:key="`mobile-${cameraMountKey}`"
|
||||
:resolution="{
|
||||
width: photoWidth,
|
||||
height: photoHeight,
|
||||
}"
|
||||
ref="camera"
|
||||
:class="['camera-preview']"
|
||||
:autoplay="false"
|
||||
:style="!img ? 'display: block' : 'display: none'"
|
||||
:playsinline="true"
|
||||
:facingMode="'user'"
|
||||
/>
|
||||
|
||||
<!-- แสดงรูปเมื่อกด capture -->
|
||||
<div v-if="img" class="image-container">
|
||||
<q-img :src="img" class="image-element"></q-img>
|
||||
<q-img
|
||||
:src="img"
|
||||
class="image-element"
|
||||
:img-style="centeredPreviewImageStyle"
|
||||
></q-img>
|
||||
<!-- Native capture: cameraIsOn stays false so show retake here
|
||||
the refresh button here instead of inside v-if="cameraIsOn" -->
|
||||
<div
|
||||
v-if="useNativePhotoCapture"
|
||||
class="absolute-bottom text-subtitle2 text-center q-py-sm"
|
||||
style="background: #00000021"
|
||||
>
|
||||
<q-btn
|
||||
round
|
||||
icon="refresh"
|
||||
size="18px"
|
||||
style="background: #263238; color: white"
|
||||
@click="refreshPhoto"
|
||||
unelevated
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="cameraIsOn">
|
||||
|
|
@ -1429,7 +1958,7 @@ watch(
|
|||
<div
|
||||
class="col-12 text-subtitle1 text-center text-white text-weight-medium"
|
||||
>
|
||||
{{ date2Thai(Thai) }}
|
||||
{{ date2Thai(displayedCheckDate) }}
|
||||
</div>
|
||||
<div class="row col-12 justify-center q-pt-sm">
|
||||
<!-- <div class="text-h3 text-white text-weight-bold"></div> -->
|
||||
|
|
@ -1463,6 +1992,20 @@ watch(
|
|||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!--
|
||||
Native still-image fallback: hidden file input.
|
||||
Activated only after inline camera is proven unsupported or fails — on any
|
||||
platform including iOS. Never used as the first-choice path.
|
||||
-->
|
||||
<input
|
||||
ref="nativePhotoInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="user"
|
||||
style="display: none"
|
||||
@change="onNativePhotoSelected"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -1499,6 +2042,8 @@ watch(
|
|||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.image-element {
|
||||
|
|
@ -1513,6 +2058,14 @@ watch(
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
/* Keep the live camera preview visually aligned with the normalized saved photo. */
|
||||
.camera-preview :deep(video) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
-webkit-object-fit: cover;
|
||||
}
|
||||
|
||||
.bg-topOut {
|
||||
background: rgb(39, 50, 56);
|
||||
background: linear-gradient(
|
||||
|
|
@ -1535,6 +2088,11 @@ watch(
|
|||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
.warning-banner {
|
||||
border: 1px solid #ffcc80;
|
||||
border-left: 4px solid #fb8c00;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
100% {
|
||||
|
|
|
|||
|
|
@ -133,14 +133,19 @@ const photoHeight = ref<number>(350)
|
|||
|
||||
/** function เปิดกล้อง */
|
||||
async function openCamera() {
|
||||
// change camera device
|
||||
if (cameraIsOn.value) {
|
||||
camera.value?.stop()
|
||||
} else {
|
||||
await camera.value?.start()
|
||||
changeCamera()
|
||||
try {
|
||||
// change camera device
|
||||
if (cameraIsOn.value) {
|
||||
camera.value?.stop()
|
||||
} else {
|
||||
await camera.value?.start()
|
||||
changeCamera()
|
||||
}
|
||||
cameraIsOn.value = !cameraIsOn.value
|
||||
} catch (error) {
|
||||
console.error('Error opening camera:', error)
|
||||
messageError($q, error, 'ไม่สามารถเปิดกล้องได้ กรุณาลองใหม่อีกครั้ง')
|
||||
}
|
||||
cameraIsOn.value = !cameraIsOn.value
|
||||
}
|
||||
|
||||
/** change camera device */
|
||||
|
|
@ -152,25 +157,49 @@ async function changeCamera() {
|
|||
|
||||
/** function ถ่ายรูป*/
|
||||
async function capturePhoto() {
|
||||
const imageBlob: any = await camera.value?.snapshot(
|
||||
{ width: photoWidth.value, height: photoHeight.value },
|
||||
'image/png',
|
||||
0.5
|
||||
)
|
||||
const fileName = 'photo.png'
|
||||
//ไฟล์รูป
|
||||
const file = new File([imageBlob], fileName, { type: 'image/png' })
|
||||
fileImg.value = file
|
||||
//แสดงรูป
|
||||
camera.value?.stop()
|
||||
const url = URL.createObjectURL(imageBlob)
|
||||
img.value = url
|
||||
try {
|
||||
const imageBlob: any = await camera.value?.snapshot(
|
||||
{ width: photoWidth.value, height: photoHeight.value },
|
||||
'image/jpeg',
|
||||
0.8
|
||||
)
|
||||
if (!imageBlob) {
|
||||
messageError($q, '', 'ไม่สามารถถ่ายรูปได้ กรุณาลองใหม่อีกครั้ง')
|
||||
return
|
||||
}
|
||||
const fileName = `photo_${Date.now()}.jpg`
|
||||
//ไฟล์รูป
|
||||
const file = new File([imageBlob], fileName, { type: 'image/jpeg' })
|
||||
fileImg.value = file
|
||||
|
||||
// ยกเลิก URL เก่าก่อนสร้างใหม่ (ป้องกัน memory leak)
|
||||
if (img.value) {
|
||||
URL.revokeObjectURL(img.value)
|
||||
}
|
||||
|
||||
//แสดงรูป
|
||||
camera.value?.stop()
|
||||
const url = URL.createObjectURL(imageBlob)
|
||||
img.value = url
|
||||
} catch (error) {
|
||||
console.error('Error capturing photo:', error)
|
||||
messageError($q, error, 'ไม่สามารถถ่ายรูปได้ กรุณาลองใหม่อีกครั้ง')
|
||||
}
|
||||
}
|
||||
|
||||
/** function เปลี่ยนรูปภาพ*/
|
||||
function refreshPhoto() {
|
||||
img.value = undefined
|
||||
camera.value?.start()
|
||||
async function refreshPhoto() {
|
||||
try {
|
||||
// ยกเลิก URL เก่า (ป้องกัน memory leak)
|
||||
if (img.value) {
|
||||
URL.revokeObjectURL(img.value)
|
||||
img.value = undefined
|
||||
}
|
||||
await camera.value?.start()
|
||||
} catch (error) {
|
||||
console.error('Error refreshing photo:', error)
|
||||
messageError($q, error, 'ไม่สามารถเปิดกล้องได้ กรุณาลองใหม่อีกครั้ง')
|
||||
}
|
||||
}
|
||||
|
||||
/** ref validate*/
|
||||
|
|
@ -335,7 +364,8 @@ onMounted(async () => {
|
|||
:resolution="{ width: photoWidth, height: photoHeight }"
|
||||
ref="camera"
|
||||
:autoplay="false"
|
||||
:style="!img ? 'display: block' : 'display: none'"
|
||||
:playsinline="true"
|
||||
:facingMode="'user'"
|
||||
/>
|
||||
|
||||
<!-- แสดงรูปเมื่อกด capture -->
|
||||
|
|
@ -569,4 +599,12 @@ onMounted(async () => {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* iOS-specific video fixes */
|
||||
.card-container video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
-webkit-object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue