daShangDao_scanBook/components/apex-camera/apex-camera copy.uvue

641 lines
14 KiB
Plaintext

<template>
<view class="apex-camera">
<view class="top-bar">
<text class="nav-btn" @click="handleBack">&lt;</text>
<text class="title">{{ props.title }}</text>
<text class="badge" :class="initialized ? 'badge-ready' : 'badge-pending'">
{{ initialized ? '就绪' : '初始化中' }}
</text>
</view>
<view class="preview-area">
<camera id="apex-camera-preview" class="preview-camera" mode="normal" resolution="high"
:device-position="cameraPosition" :flash="flashMode" @initdone="handleCameraReady"
@ready="handleCameraReady" @stop="handleCameraStop" @error="handleCameraNativeError" />
<view v-if="!initialized" class="preview-mask">
<text class="main-copy">{{ props.subTitle }}</text>
<text class="minor-copy">相机正在准备中,请确认系统相机权限已允许</text>
</view>
</view>
<view class="status-panel">
<text class="status-line">摄像头:{{ cameraPosition == 'front' ? '前置' : '后置' }}</text>
<text class="status-line">闪光灯:{{ flashModeLabel }}</text>
<text class="status-line">保存到相册:{{ saveToAlbum ? '是' : '否' }}</text>
<text class="status-line">{{ statusText }}</text>
<text v-if="lastPhotoPath != ''" class="status-line">最后照片:{{ lastPhotoPath }}</text>
<text v-if="lastVideoPath != ''" class="status-line">最后视频:{{ lastVideoPath }}</text>
</view>
<view class="bottom-panel">
<view class="action-row">
<view class="ghost-btn" @click="toggleCamera">
<text class="ghost-btn-text">切换</text>
</view>
<view class="capture-btn" @click="takePhoto">
<view class="capture-btn-inner">
<text class="capture-btn-text">拍照</text>
</view>
</view>
<view class="record-btn" @click="recordVideo">
<text class="record-btn-text">{{ isRecording ? '停止' : '录像' }}</text>
</view>
</view>
<view class="tool-row">
<view class="tool-card">
<text class="tool-title">保存到相册</text>
<switch :checked="saveToAlbum" color="#1677ff" @change="handleSaveToAlbumChange" />
</view>
<view class="tool-card flash-card">
<text class="tool-title">闪光灯</text>
<view class="flash-options">
<text class="flash-option" :class="flashMode == 'off' ? 'flash-option-active' : ''"
@click="changeFlashMode('off')">关</text>
<text class="flash-option" :class="flashMode == 'on' ? 'flash-option-active' : ''"
@click="changeFlashMode('on')">开</text>
<text class="flash-option" :class="flashMode == 'auto' ? 'flash-option-active' : ''"
@click="changeFlashMode('auto')">自动</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed, nextTick, onMounted, ref } from 'vue'
import {
ApexCameraFlashMode,
ApexCameraPhotoResult,
ApexCameraPosition,
ApexCameraProps,
ApexCameraVideoResult
} from './type.uts'
defineOptions({
name: 'apex-camera',
styleIsolation: 'app'
})
const emit = defineEmits<{
ready : []
photo : [res: ApexCameraPhotoResult]
video : [res: ApexCameraVideoResult]
error : [message: string]
}>()
const props = withDefaults(defineProps<ApexCameraProps>(), {
title: 'UTS 相机组件',
subTitle: '支持 Android + iOS + 鸿蒙',
autoInit: true,
autoBack: true,
maxDuration: 60,
defaultPosition: 'back',
defaultFlashMode: 'off',
defaultSaveToAlbum: false,
tipLines: () : string[] => ([])
})
type ApexCameraErrorDetail = {
errMsg ?: string
message ?: string
msg ?: string
}
type ApexCameraErrorEvent = {
detail ?: ApexCameraErrorDetail
errMsg ?: string
}
const initialized = ref(false)
const busy = ref(false)
const isRecording = ref(false)
const cameraPosition = ref<ApexCameraPosition>(props.defaultPosition)
const flashMode = ref<ApexCameraFlashMode>(props.defaultFlashMode)
const saveToAlbum = ref(props.defaultSaveToAlbum)
const statusText = ref('等待相机初始化')
const lastPhotoPath = ref('')
const lastVideoPath = ref('')
const flashModeLabel = computed(() : string => {
if (flashMode.value == 'on') {
return '开'
}
if (flashMode.value == 'auto') {
return '自动'
}
return '关'
})
const showToast = (title : string) => {
uni.showToast({
title,
icon: 'none'
})
}
const getErrorMessage = (event : any, fallback : string) : string => {
const errorEvent = event as ApexCameraErrorEvent | null
if (errorEvent != null) {
const detail = errorEvent.detail
if (detail != null) {
if (detail.errMsg != null && detail.errMsg != '') {
return detail.errMsg
}
if (detail.message != null && detail.message != '') {
return detail.message
}
if (detail.msg != null && detail.msg != '') {
return detail.msg
}
}
if (errorEvent.errMsg != null && errorEvent.errMsg != '') {
return errorEvent.errMsg
}
}
return fallback
}
const ensureCameraContext = () => {
try {
return uni.createCameraContext()
} catch (e) {
return null
}
}
const saveImageIfNeeded = (path : string) : Promise<boolean> => {
if (!saveToAlbum.value) {
return Promise.resolve(false)
}
return new Promise((resolve) => {
uni.saveImageToPhotosAlbum({
filePath: path,
success: () => resolve(true),
fail: () => resolve(false)
})
})
}
const saveVideoIfNeeded = (path : string) : Promise<boolean> => {
if (!saveToAlbum.value) {
return Promise.resolve(false)
}
return new Promise((resolve) => {
uni.saveVideoToPhotosAlbum({
filePath: path,
success: () => resolve(true),
fail: () => resolve(false)
})
})
}
const bootCamera = async () => {
await nextTick()
const context = ensureCameraContext()
if (context == null) {
const message = '创建相机上下文失败'
statusText.value = message
emit('error', message)
return
}
initialized.value = false
statusText.value = '相机视图已创建,等待初始化完成'
}
const handleCameraReady = () => {
initialized.value = true
statusText.value = isRecording.value ? '录像中...' : '相机已就绪'
emit('ready')
}
const handleCameraStop = () => {
initialized.value = false
isRecording.value = false
statusText.value = '相机预览已停止'
}
const handleCameraNativeError = (event : any) => {
const message = getErrorMessage(event, 'camera error')
initialized.value = false
isRecording.value = false
statusText.value = message
emit('error', message)
showToast(message)
}
const changeFlashMode = (mode : ApexCameraFlashMode) => {
if (busy.value || flashMode.value == mode) {
return
}
flashMode.value = mode
statusText.value = `闪光灯已切换为${flashModeLabel.value}`
}
const toggleCamera = async () => {
if (busy.value || isRecording.value) {
return
}
cameraPosition.value = cameraPosition.value == 'front' ? 'back' : 'front'
initialized.value = false
statusText.value = `正在切换到${cameraPosition.value == 'front' ? '前置' : '后置'}摄像头...`
await bootCamera()
}
const takePhoto = () => {
if (busy.value) {
return
}
const context = ensureCameraContext()
if (context == null) {
const message = '相机上下文不可用'
statusText.value = message
emit('error', message)
return
}
busy.value = true
statusText.value = '正在拍照...'
context.takePhoto({
quality: 'high',
success: (res) => {
let imagePath = ''
if (res.tempImagePath != null && res.tempImagePath != '') {
imagePath = res.tempImagePath
}
if (imagePath == '') {
const message = '拍照失败,未返回图片路径'
statusText.value = message
emit('error', message)
showToast(message)
busy.value = false
return
}
saveImageIfNeeded(imagePath).then((saved : boolean) => {
lastPhotoPath.value = imagePath
lastVideoPath.value = ''
statusText.value = saved ? '拍照成功,已保存到相册' : '拍照成功'
emit('photo', {
path: imagePath,
width: 0,
height: 0,
savedToAlbum: saved
} as ApexCameraPhotoResult)
showToast(saved ? '拍照成功并已保存' : '拍照成功')
busy.value = false
}).catch(() => {
lastPhotoPath.value = imagePath
lastVideoPath.value = ''
statusText.value = '拍照成功'
emit('photo', {
path: imagePath,
width: 0,
height: 0,
savedToAlbum: false
} as ApexCameraPhotoResult)
busy.value = false
})
},
fail: (err) => {
const message = getErrorMessage(err, '拍照失败')
statusText.value = message
emit('error', message)
showToast(message)
busy.value = false
}
})
}
const recordVideo = () => {
if (busy.value) {
return
}
const context = ensureCameraContext()
if (context == null) {
const message = '相机上下文不可用'
statusText.value = message
emit('error', message)
return
}
busy.value = true
if (isRecording.value) {
statusText.value = '正在结束录像...'
context.stopRecord({
success: (res) => {
let videoPath = ''
if (res.tempVideoPath != null && res.tempVideoPath != '') {
videoPath = res.tempVideoPath
}
const duration = 0
const size = 0
saveVideoIfNeeded(videoPath).then((saved : boolean) => {
isRecording.value = false
lastVideoPath.value = videoPath
lastPhotoPath.value = ''
statusText.value = saved ? '录像完成,已保存到相册' : '录像完成'
emit('video', {
path: videoPath,
duration: duration,
size: size,
savedToAlbum: saved
} as ApexCameraVideoResult)
busy.value = false
}).catch(() => {
isRecording.value = false
lastVideoPath.value = videoPath
lastPhotoPath.value = ''
statusText.value = '录像完成'
emit('video', {
path: videoPath,
duration: duration,
size: size,
savedToAlbum: false
} as ApexCameraVideoResult)
busy.value = false
})
},
fail: (err) => {
const message = getErrorMessage(err, '结束录像失败')
statusText.value = message
emit('error', message)
showToast(message)
busy.value = false
}
})
return
}
statusText.value = '开始录像...'
context.startRecord({
timeout: props.maxDuration,
success: () => {
isRecording.value = true
statusText.value = '录像中,再点一次停止'
busy.value = false
},
fail: (err) => {
const message = getErrorMessage(err, '开始录像失败')
statusText.value = message
emit('error', message)
showToast(message)
busy.value = false
}
})
}
const handleSaveToAlbumChange = (event : UniSwitchChangeEvent) => {
saveToAlbum.value = event.detail.value
statusText.value = saveToAlbum.value ? '已开启保存到相册' : '已关闭保存到相册'
}
const handleBack = () => {
if (!props.autoBack) {
return
}
uni.navigateBack({
fail: () => { }
})
}
onMounted(() => {
if (props.autoInit) {
bootCamera()
}
})
defineExpose({
bootCamera,
takePhoto,
recordVideo,
toggleCamera,
changeFlashMode
})
</script>
<style scoped>
.apex-camera {
flex: 1;
width: 100%;
height: 100%;
flex-direction: column;
background-color: #000;
}
.top-bar {
height: 88px;
padding-top: 32px;
padding-left: 16px;
padding-right: 16px;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: rgba(0, 0, 0, 0.95);
}
.preview-area {
flex: 1;
min-height: 240px;
position: relative;
background-color: #0b0b0b;
overflow: hidden;
}
.preview-camera {
width: 100%;
height: 100%;
}
.preview-mask {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
align-items: center;
justify-content: center;
padding-left: 28px;
padding-right: 28px;
background-color: rgba(0, 0, 0, 0.22);
}
.main-copy {
color: #fff;
font-size: 24px;
font-weight: 700;
text-align: center;
}
.minor-copy {
color: #f8e53d;
font-size: 14px;
margin-top: 12px;
text-align: center;
}
.nav-btn {
color: #fff;
font-size: 28px;
width: 32px;
text-align: center;
}
.title {
color: #fff;
font-size: 20px;
font-weight: 600;
}
.badge {
min-width: 54px;
padding-top: 5px;
padding-bottom: 5px;
padding-left: 10px;
padding-right: 10px;
border-radius: 999px;
color: #fff;
font-size: 13px;
text-align: center;
}
.badge-ready {
background-color: #3cc064;
}
.badge-pending {
background-color: #ffb020;
}
.status-panel {
padding: 12px 14px;
background-color: #111214;
}
.status-line {
color: #fff;
font-size: 13px;
line-height: 20px;
}
.bottom-panel {
background-color: #0f0f11;
padding-top: 20px;
padding-bottom: 18px;
padding-left: 16px;
padding-right: 16px;
}
.action-row {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.ghost-btn,
.record-btn {
width: 92px;
height: 50px;
border-radius: 25px;
align-items: center;
justify-content: center;
}
.ghost-btn {
background-color: #2c2c2e;
}
.record-btn {
background-color: #ff6b73;
}
.ghost-btn-text,
.record-btn-text {
color: #fff;
font-size: 18px;
font-weight: 600;
}
.capture-btn {
width: 98px;
height: 98px;
border-radius: 49px;
background-color: #e8e8e8;
align-items: center;
justify-content: center;
}
.capture-btn-inner {
width: 82px;
height: 82px;
border-radius: 41px;
background-color: #fff;
align-items: center;
justify-content: center;
border-width: 1px;
border-style: solid;
border-color: #d9d9d9;
}
.capture-btn-text {
color: #222;
font-size: 18px;
font-weight: 700;
}
.tool-row {
flex-direction: row;
justify-content: space-between;
margin-top: 18px;
}
.tool-card {
width: 48%;
min-height: 74px;
border-radius: 14px;
padding: 12px 14px;
background-color: #1b1b1d;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.flash-card {
flex-direction: column;
align-items: flex-start;
justify-content: center;
}
.tool-title {
color: #fff;
font-size: 15px;
}
.flash-options {
flex-direction: row;
margin-top: 12px;
}
.flash-option {
min-width: 38px;
height: 30px;
padding-left: 10px;
padding-right: 10px;
border-radius: 15px;
color: #fff;
font-size: 14px;
text-align: center;
line-height: 30px;
background-color: #2e2e32;
margin-right: 8px;
}
.flash-option-active {
color: #111;
background-color: #f5d667;
}
</style>