feat:接入ima-camera-view插件,camera_capture改为nvue实现实时相机预览+连拍

This commit is contained in:
ShenQiLun 2026-06-25 16:23:39 +08:00
parent 448f1b1040
commit e9d146ac74
53 changed files with 6041 additions and 269 deletions

View File

@ -0,0 +1,640 @@
<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>

View File

@ -0,0 +1,594 @@
<template>
<view class="apex-camera">
<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 class="photo-mask">
<image class="photo-mask-image" src="../../../../assets/img/mb.png" mode="aspectFill" />
</view>
<view class="preview-top-actions">
<view class="icon-action flash-toggle-btn" @click="cycleFlashMode">
<text class="icon-action-symbol">闪</text>
<text class="icon-action-state">{{ flashModeShortLabel }}</text>
</view>
</view>
<view v-if="!initialized && !hasReadyOnce" class="preview-mask">
<text class="main-copy">{{ props.subTitle }}</text>
<text class="minor-copy">相机正在准备中,请确认系统相机权限已允许</text>
</view>
</view>
<view class="bottom-panel">
<view class="action-row">
<view class="thumb-card">
<image v-if="lastPhotoPath != ''" class="thumb-image" :src="lastPhotoPath" mode="aspectFill" />
<text v-else class="thumb-placeholder">图</text>
</view>
<view class="capture-btn" @click="takePhoto">
<view class="capture-btn-inner">
<text class="capture-btn-text"></text>
</view>
</view>
<view class="ghost-btn" @click="toggleCamera">
<text class="ghost-btn-text">转</text>
</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 hasReadyOnce = 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 flashModeShortLabel = 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
hasReadyOnce.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 cycleFlashMode = () => {
if (flashMode.value == 'off') {
changeFlashMode('auto')
return
}
if (flashMode.value == 'auto') {
changeFlashMode('on')
return
}
changeFlashMode('off')
}
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,
cycleFlashMode
})
</script>
<style scoped>
.apex-camera {
flex: 1;
width: 100%;
height: 100%;
flex-direction: column;
background-color: #000;
}
.preview-area {
flex: 1;
min-height: 240px;
position: relative;
background-color: #0b0b0b;
overflow: hidden;
}
.preview-camera {
width: 100%;
height: 100%;
}
.photo-mask {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 1;
}
.photo-mask-image {
width: 100%;
height: 100%;
margin-top: -80px;
}
.preview-top-actions {
position: absolute;
left: 16px;
top: 18px;
z-index: 3;
}
.icon-action {
width: 48px;
height: 48px;
border-radius: 24px;
background-color: rgba(18, 18, 20, 0.68);
align-items: center;
justify-content: center;
}
.flash-toggle-btn {
border-width: 1px;
border-style: solid;
border-color: rgba(255, 255, 255, 0.16);
}
.icon-action-symbol {
color: #fff;
font-size: 16px;
font-weight: 700;
}
.icon-action-state {
color: #f5d667;
font-size: 11px;
margin-top: 2px;
}
.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;
}
.bottom-panel {
background-color: rgba(15, 15, 17, 0.92);
padding-top: 16px;
padding-bottom: 16px;
padding-left: 16px;
padding-right: 16px;
}
.action-row {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.thumb-card,
.ghost-btn {
width: 74px;
height: 74px;
border-radius: 22px;
align-items: center;
justify-content: center;
}
.thumb-card {
background-color: #1b1b1d;
overflow: hidden;
}
.thumb-image {
width: 100%;
height: 100%;
}
.thumb-placeholder {
color: #fff;
font-size: 18px;
font-weight: 700;
}
.ghost-btn {
background-color: #2c2c2e;
}
.ghost-btn-text {
color: #fff;
font-size: 22px;
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;
}
</style>

View File

@ -0,0 +1 @@
export * from './type.uts'

View File

@ -0,0 +1,29 @@
export type ApexCameraMode = 'photo' | 'video'
export type ApexCameraPosition = 'front' | 'back'
export type ApexCameraFlashMode = 'off' | 'on' | 'auto'
export type ApexCameraPhotoResult = {
path : string
width : number
height : number
savedToAlbum : boolean
}
export type ApexCameraVideoResult = {
path : string
duration : number
size : number
savedToAlbum : boolean
}
export type ApexCameraProps = {
title : string
subTitle : string
tipLines : string[]
autoInit : boolean
autoBack : boolean
maxDuration : number
defaultPosition : ApexCameraPosition
defaultFlashMode : ApexCameraFlashMode
defaultSaveToAlbum : boolean
}

287
components/使用说明.md Normal file
View File

@ -0,0 +1,287 @@
# apex-camera 相机组件使用文档
## 概述
`apex-camera` 是一个基于 UTS 的跨平台相机组件,支持 **Android**、**iOS** 和**鸿蒙**三端。组件封装了拍照、录像、闪光灯控制、前后摄像头切换、保存到相册等常见相机功能,开箱即用。
## 文件结构
```
components/apex-camera/
├── apex-camera.uvue # 简洁版风格(带照片遮罩层)
├── apex-camera copy.uvue # 完整版风格(带顶栏、状态面板、工具面板)
├── index.uts # 导出入口
└── type.uts # 类型定义
```
> 两个 `.uvue` 文件提供了不同的 UI 风格,功能逻辑完全一致。可按需选用其中一个。
## 类型定义
### ApexCameraProps组件 Props
```ts
type ApexCameraProps = {
title: string // 标题文本,默认 "UTS 相机组件"
subTitle: string // 副标题/提示文本,默认 "支持 Android + iOS + 鸿蒙"
tipLines: string[] // 提示信息行
autoInit: boolean // 是否自动初始化相机,默认 true
autoBack: boolean // 点击返回时是否自动返回上一页,默认 true
maxDuration: number // 最大录像时长(秒),默认 60
defaultPosition: 'front' | 'back' // 默认摄像头方向,默认 'back'
defaultFlashMode: 'off' | 'on' | 'auto' // 默认闪光灯模式,默认 'off'
defaultSaveToAlbum: boolean // 默认是否保存到相册,默认 false
}
```
### ApexCameraPhotoResult拍照结果
```ts
type ApexCameraPhotoResult = {
path: string // 照片临时路径
width: number // 照片宽度
height: number // 照片高度
savedToAlbum: boolean // 是否已保存到系统相册
}
```
### ApexCameraVideoResult录像结果
```ts
type ApexCameraVideoResult = {
path: string // 视频临时路径
duration: number // 视频时长
size: number // 视频文件大小
savedToAlbum: boolean // 是否已保存到系统相册
}
```
## Props属性
| 属性名 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `title` | `string` | `"UTS 相机组件"` | 顶栏标题(仅完整版) |
| `subTitle` | `string` | `"支持 Android + iOS + 鸿蒙"` | 初始化遮罩层的主提示文字 |
| `tipLines` | `string[]` | `[]` | 额外的提示信息行 |
| `autoInit` | `boolean` | `true` | 是否在 `onMounted` 时自动启动相机 |
| `autoBack` | `boolean` | `true` | 点击返回按钮是否自动 `navigateBack` |
| `maxDuration` | `number` | `60` | 录像最大时长(秒) |
| `defaultPosition` | `'front' \| 'back'` | `'back'` | 默认摄像头朝向 |
| `defaultFlashMode` | `'off' \| 'on' \| 'auto'` | `'off'` | 默认闪光灯模式 |
| `defaultSaveToAlbum` | `boolean` | `false` | 默认是否将结果保存到系统相册 |
## Events事件
| 事件名 | 参数类型 | 说明 |
|--------|----------|------|
| `ready` | 无 | 相机初始化完成 |
| `photo` | `ApexCameraPhotoResult` | 拍照完成 |
| `video` | `ApexCameraVideoResult` | 录像完成 |
| `error` | `message: string` | 发生错误(含错误信息) |
## Exposed Methods暴露方法
组件通过 `defineExpose` 暴露以下方法,父组件可通过 ref 调用:
| 方法名 | 签名 | 说明 |
|--------|------|------|
| `bootCamera` | `() => Promise<void>` | 手动启动/重启相机 |
| `takePhoto` | `() => void` | 触发拍照 |
| `recordVideo` | `() => void` | 触发录像(再次调用停止) |
| `toggleCamera` | `() => Promise<void>` | 切换前后摄像头 |
| `changeFlashMode` | `(mode: 'off' \| 'on' \| 'auto') => void` | 设置闪光灯模式 |
| `cycleFlashMode` | `() => void` | 循环切换闪光灯模式off→auto→on→off |
> 注意:`cycleFlashMode` 仅在简洁版(`apex-camera.uvue`)中暴露。
## 基本用法
### 1. 引入组件
`pages.json` 中注册组件:
```json
{
"easycom": {
"autoscan": true
}
}
```
或者直接 import
```vue
<script setup>
import ApexCamera from '@/uni_modules/apex-gu-cheng-uts/components/apex-camera/apex-camera.uvue'
</script>
```
### 2. 最简单的使用
```vue
<template>
<apex-camera
@photo="onPhoto"
@video="onVideo"
@error="onError"
/>
</template>
<script setup>
const onPhoto = (res) => {
console.log('拍照完成:', res.path)
// res.path - 照片临时路径
// res.savedToAlbum - 是否已保存到相册
}
const onVideo = (res) => {
console.log('录像完成:', res.path)
// res.path - 视频临时路径
// res.duration - 视频时长
}
const onError = (message) => {
uni.showToast({ title: message, icon: 'none' })
}
</script>
```
### 3. 完整配置示例
```vue
<template>
<apex-camera
ref="cameraRef"
title="智能拍照"
sub-title="请将物品对准框内"
:auto-init="true"
:auto-back="false"
:max-duration="30"
default-position="back"
default-flash-mode="auto"
:default-save-to-album="true"
@ready="onReady"
@photo="onPhoto"
@video="onVideo"
@error="onError"
/>
</template>
<script setup>
import { ref } from 'vue'
const cameraRef = ref(null)
const onReady = () => {
console.log('相机已就绪')
}
const onPhoto = (res) => {
// 拍照成功,处理照片
uni.previewImage({
urls: [res.path]
})
}
const onVideo = (res) => {
console.log('视频路径:', res.path)
}
const onError = (msg) => {
uni.showToast({ title: msg, icon: 'none' })
}
// 可通过 ref 手动调用方法
const manualTakePhoto = () => {
cameraRef.value?.takePhoto()
}
const switchCamera = () => {
cameraRef.value?.toggleCamera()
}
const setFlashAuto = () => {
cameraRef.value?.changeFlashMode('auto')
}
</script>
```
## 两种 UI 风格对比
### 简洁版(`apex-camera.uvue`
- 全屏预览 + 照片遮罩层(可自定义遮罩图)
- 底部一栏:缩略图 + 拍照按钮 + 切换摄像头
- 左上角闪光灯切换按钮
- 适合需要自定义遮罩/引导框的场景
### 完整版(`apex-camera copy.uvue`
- 顶部导航栏:返回按钮 + 标题 + 状态徽章
- 全屏预览 + 初始化遮罩
- 状态面板:显示摄像头方向、闪光灯、保存状态等
- 底部操作栏:切换 + 拍照 + 录像 + 状态文本
- 底部工具面板:保存到相册开关 + 闪光灯三态切换
- 适合需要完整调试信息和状态展示的场景
## 注意事项
1. **相机权限**使用前请确保已在各平台配置相机权限。Android 需在 `AndroidManifest.xml` 中声明iOS 需在 `Info.plist` 中添加 `NSCameraUsageDescription`
2. **相册保存**如需保存到系统相册Android 还需 `WRITE_EXTERNAL_STORAGE` 权限iOS 需 `NSPhotoLibraryAddUsageDescription`
3. **临时文件**:拍照/录像返回的路径为临时文件路径,如需持久保存需自行移动或上传。
4. **录像限制**:通过 `maxDuration` 控制最大录像时长,超出后会自动停止。
5. **组件引用**:通过 `easycom` 自动扫描引入即可,无需手动 import。
```vue
<template>
<view class="shoot-page">
<ApexCamera
class="shoot-camera"
title="UTS 相机组件"
sub-title="支持 Android + iOS + 鸿蒙"
:auto-init="true"
:auto-back="false"
:default-save-to-album="true"
@ready="handleReady"
@photo="handlePhoto"
@video="handleVideo"
@error="handleError"
/>
</view>
</template>
<script setup>
import ApexCamera from '@/uni_modules/apex-gu-cheng-uts/components/apex-camera/apex-camera.uvue'
import { ApexCameraPhotoResult, ApexCameraVideoResult } from '@/uni_modules/apex-gu-cheng-uts/components/apex-camera/type.uts'
const handleReady = () => {
console.log('ApexCamera ready')
}
const handlePhoto = (res : ApexCameraPhotoResult) => {
console.log('photo result', res.path, res.width, res.height, res.savedToAlbum)
}
const handleVideo = (res : ApexCameraVideoResult) => {
console.log('video result', res.path, res.duration, res.size, res.savedToAlbum)
}
const handleError = (message : string) => {
console.error('camera error', message)
}
</script>
<style>
.shoot-page {
flex: 1;
background-color: #000;
}
.shoot-camera {
flex: 1;
width: 100%;
height: 100%;
}
</style>
```

View File

@ -1 +0,0 @@
提交下git描述为 调整相机前指定备份

View File

@ -40,7 +40,10 @@
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\"/>",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>"
]
},
/* ios */

View File

@ -1,6 +1,29 @@
{
"dependencies": {
"blueimp-md5": "^2.19.0",
"js-sha256": "^0.11.1"
}
"id": "apex-gu-cheng-uts",
"name": "安卓鸿蒙IOS调用相机摄像头",
"displayName": "安卓鸿蒙IOS调用相机摄像头",
"version": "1.0.0",
"description": "可以在APP中实时显示摄像头预览画面同时进行拍照录像等功能",
"keywords": [
"鸿蒙",
"安卓",
"IOS",
"摄像头",
"相机"
],
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
}
},
"uni_modules": {
"dependencies": [],
"encrypt": []
}
}

View File

@ -0,0 +1,413 @@
<template>
<view class="cc-page">
<!-- 相机预览 -->
<ima-camera-view
ref="cameraRef"
class="cc-camera"
flash="off"
facing="back"
@onPictureTaken="onPictureTaken"
@onCameraOpened="onCameraOpened"
></ima-camera-view>
<!-- 连拍模式提示遮罩 -->
<view class="cc-burst-overlay" v-if="isBurstMode">
<text class="cc-burst-tip">连拍中⋯</text>
<text class="cc-burst-count">{{ capturedList.length }} 张</text>
<text class="cc-burst-hint">点击红色按钮停止</text>
</view>
<!-- 顶部:已拍数量 & 确认 -->
<view class="cc-topbar">
<text class="cc-topbar-title" v-if="capturedList.length > 0">已拍 {{ capturedList.length }}/9</text>
<text class="cc-topbar-title" v-else>相机</text>
<text class="cc-topbar-confirm" @click="confirmCapture" :class="{ disabled: capturedList.length === 0 }">
确认 ({{ capturedList.length }})
</text>
</view>
<!-- 底部操作区 -->
<view class="cc-footer">
<!-- 返回 -->
<view class="cc-footer-left" @click="goBack">
<text class="cc-footer-text">取消</text>
</view>
<!-- 中央拍照/停止 -->
<view class="cc-footer-center">
<view class="cc-stop-btn" @click="stopBurst" v-if="isBurstMode">
<view class="cc-stop-inner">■</view>
</view>
<view class="cc-capture-btn" @click="capturePhoto" v-else>
<view class="cc-capture-inner"></view>
</view>
</view>
<!-- 右侧操作 -->
<view class="cc-footer-right">
<view class="cc-burst-toggle" @click="toggleBurst" v-if="!isBurstMode && capturedList.length < 9">
<text class="cc-burst-toggle-text">⚡连拍</text>
</view>
<view class="cc-flip-btn" @click="flipCamera" v-if="!isBurstMode">
<text class="cc-flip-icon">↺</text>
</view>
</view>
</view>
<!-- 已拍照缩略图横条 -->
<view class="cc-thumb-bar" v-if="capturedList.length > 0">
<scroll-view class="cc-thumb-scroll" scroll-x="true" show-scrollbar="false">
<view class="cc-thumb-list">
<view class="cc-thumb-item" v-for="(img, idx) in capturedList" :key="idx">
<image class="cc-thumb-img" :src="img" mode="aspectFill"></image>
<view class="cc-thumb-del" @click.stop="deletePhoto(idx)">
<text class="cc-del-icon">✕</text>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
capturedList: [],
cameraReady: false,
facing: 'back',
isBurstMode: false,
burstTimer: null
}
},
onUnload() {
this.clearBurstTimer()
},
methods: {
// 相机初始化完成
onCameraOpened() {
this.cameraReady = true
console.log('相机已就绪')
},
// 拍照
takePhotoAction() {
if (!this.cameraReady) {
uni.showToast({ title: '相机未就绪', icon: 'none' })
return
}
this.$refs.cameraRef.takePhoto()
},
// 拍照完成回调
onPictureTaken(e) {
var path = e.detail?.path || ''
if (path) {
this.capturedList.push(path)
}
},
// 单拍
capturePhoto() {
if (this.capturedList.length >= 9) {
uni.showToast({ title: '最多拍9张', icon: 'none' })
return
}
this.takePhotoAction()
},
// 连拍开关
toggleBurst() {
if (this.capturedList.length >= 9) {
uni.showToast({ title: '最多拍9张', icon: 'none' })
return
}
this.isBurstMode = true
this.doBurstCapture()
},
doBurstCapture() {
if (!this.isBurstMode || this.capturedList.length >= 9) {
this.isBurstMode = false
return
}
this.takePhotoAction()
// 间隔 1.5 秒后继续
var that = this
this.burstTimer = setTimeout(function() {
that.doBurstCapture()
}, 1500)
},
stopBurst() {
this.isBurstMode = false
this.clearBurstTimer()
},
clearBurstTimer() {
if (this.burstTimer) {
clearTimeout(this.burstTimer)
this.burstTimer = null
}
},
// 切换摄像头
flipCamera() {
this.facing = this.facing === 'back' ? 'front' : 'back'
this.$refs.cameraRef.changeFacing(this.facing)
},
// 删除照片
deletePhoto(idx) {
this.capturedList.splice(idx, 1)
},
// 返回
goBack() {
if (this.capturedList.length > 0) {
uni.showModal({
title: '提示',
content: '确定取消拍照吗?已拍照片将丢失',
success: function(res) {
if (res.confirm) {
uni.navigateBack()
}
}
})
} else {
uni.navigateBack()
}
},
// 确认
confirmCapture() {
if (this.capturedList.length === 0) {
uni.showToast({ title: '请先拍照', icon: 'none' })
return
}
var pages = getCurrentPages()
var prevPage = pages[pages.length - 2]
if (prevPage) {
prevPage.$vm.capturedPhotoList = this.capturedList
}
uni.navigateBack()
}
}
}
</script>
<style>
.cc-page {
flex: 1;
background-color: #000;
position: relative;
}
.cc-camera {
flex: 1;
width: 750rpx;
}
/* 连拍遮罩 */
.cc-burst-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(0,0,0,0.25);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
pointer-events: none;
}
.cc-burst-tip {
color: #fff;
font-size: 40rpx;
font-weight: bold;
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.6);
}
.cc-burst-count {
color: rgba(255,255,255,0.9);
font-size: 32rpx;
margin-top: 16rpx;
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.6);
}
.cc-burst-hint {
color: rgba(255,255,255,0.6);
font-size: 24rpx;
margin-top: 20rpx;
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.6);
}
/* 顶部栏 */
.cc-topbar {
position: absolute;
top: 0; left: 0; right: 0;
height: 100rpx;
padding-top: var(--status-bar-height);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding-left: 30rpx;
padding-right: 30rpx;
z-index: 8;
background: linear-gradient(to bottom, rgba(0,0,0,0.4), transparent);
}
.cc-topbar-title {
color: #fff;
font-size: 32rpx;
font-weight: bold;
}
.cc-topbar-confirm {
color: #409eff;
font-size: 28rpx;
font-weight: bold;
padding: 10rpx 24rpx;
border-radius: 32rpx;
background: rgba(64, 158, 255, 0.15);
}
.cc-topbar-confirm.disabled {
color: #666;
background: transparent;
}
/* 底部操作区 */
.cc-footer {
position: absolute;
bottom: 0; left: 0; right: 0;
height: 200rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding-left: 40rpx;
padding-right: 40rpx;
z-index: 8;
background: linear-gradient(to top, rgba(0,0,0,0.5), transparent);
}
.cc-footer-left {
width: 120rpx;
}
.cc-footer-text {
color: #fff;
font-size: 28rpx;
}
.cc-footer-center {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.cc-footer-right {
width: 200rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 20rpx;
}
/* 拍照按钮 */
.cc-capture-btn {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
border: 6rpx solid #fff;
display: flex;
align-items: center;
justify-content: center;
}
.cc-capture-inner {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: #fff;
}
/* 停止按钮 */
.cc-stop-btn {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
border: 6rpx solid #ff4444;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255,68,68,0.15);
}
.cc-stop-inner {
color: #ff4444;
font-size: 32rpx;
font-weight: bold;
}
/* 连拍按钮 */
.cc-burst-toggle {
padding: 12rpx 18rpx;
border-radius: 28rpx;
border: 2rpx solid #409eff;
background: rgba(64,158,255,0.1);
}
.cc-burst-toggle-text {
color: #409eff;
font-size: 22rpx;
font-weight: bold;
}
/* 翻转按钮 */
.cc-flip-btn {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
border: 2rpx solid rgba(255,255,255,0.4);
display: flex;
align-items: center;
justify-content: center;
}
.cc-flip-icon {
color: #fff;
font-size: 32rpx;
}
/* 缩略图横条 */
.cc-thumb-bar {
position: absolute;
bottom: 200rpx;
left: 0;
right: 0;
padding: 10rpx 20rpx;
z-index: 8;
}
.cc-thumb-scroll {
white-space: nowrap;
}
.cc-thumb-list {
display: flex;
flex-direction: row;
gap: 12rpx;
}
.cc-thumb-item {
width: 100rpx;
height: 100rpx;
border-radius: 8rpx;
overflow: hidden;
position: relative;
border: 2rpx solid rgba(255,255,255,0.3);
flex-shrink: 0;
background: #333;
}
.cc-thumb-img {
width: 100rpx;
height: 100rpx;
}
.cc-thumb-del {
position: absolute;
top: -6rpx;
right: -6rpx;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background: rgba(0,0,0,0.6);
display: flex;
align-items: center;
justify-content: center;
}
.cc-del-icon {
color: #fff;
font-size: 18rpx;
}
</style>

View File

@ -1,263 +0,0 @@
<template>
<view class="cc-page">
<!-- H5 环境WebRTC 摄像头预览 -->
<view class="cc-camera-wrap" v-if="useWebRTC">
<video id="ccVideo" ref="ccVideo" class="cc-video" autoplay playsinline muted></video>
<canvas id="ccCanvas" ref="ccCanvas" class="cc-canvas" style="display:none;"></canvas>
<!-- 连拍模式提示遮罩 -->
<view class="cc-burst-overlay" v-if="isBurstMode">
<text class="cc-burst-tip">连拍中</text>
<text class="cc-burst-count">{{ capturedList.length }} </text>
<text class="cc-burst-dots">
<text class="cc-dot" v-for="n in 3" :key="n">.</text>
</text>
<text class="cc-burst-hint">点击红色按钮停止</text>
</view>
</view>
<!-- APP 环境提示信息 -->
<view class="cc-camera-wrap" v-else>
<view class="cc-camera-hint">
<text class="cc-hint-icon">📷</text>
<text class="cc-hint-text">{{ isBurstMode ? '连拍中… 系统相机将自动打开' : '点击下方按钮打开相机拍照' }}</text>
<text class="cc-hint-sub">已拍 {{ capturedList.length }} </text>
</view>
<canvas id="ccCanvas" ref="ccCanvas" class="cc-canvas" style="display:none;"></canvas>
</view>
<!-- 已拍照九宫格 -->
<view class="cc-thumb-bar">
<view class="cc-thumb-count" v-if="capturedList.length > 0">已拍 {{ capturedList.length }} </view>
<view class="cc-thumb-list">
<view class="cc-thumb-item" v-for="(img, idx) in capturedList" :key="idx">
<image class="cc-thumb-img" :src="img" mode="aspectFill"></image>
<view class="cc-thumb-del" @click.stop="deletePhoto(idx)">
<text class="cc-del-icon"></text>
</view>
</view>
<view class="cc-thumb-item cc-thumb-placeholder" v-for="n in (9 - capturedList.length)" :key="'p'+n"></view>
</view>
</view>
<!-- 底部操作区 -->
<view class="cc-footer">
<!-- 连拍模式 - 停止按钮 -->
<view class="cc-stop-btn" @click="stopBurst" v-if="isBurstMode">
<view class="cc-stop-inner"></view>
</view>
<!-- 普通模式 - 拍照按钮 -->
<view class="cc-capture-btn" @click="capturePhoto" v-else :class="{ disabled: capturedList.length >= 9 }">
<view class="cc-capture-inner"></view>
</view>
<view class="cc-right-actions">
<view class="cc-burst-toggle" @click="toggleBurst" v-if="!isBurstMode && capturedList.length < 9">
<text class="cc-burst-toggle-text">连拍</text>
</view>
<view class="cc-confirm-btn" @click="confirmCapture" :class="{ disabled: capturedList.length === 0 }">
<text class="cc-confirm-text">确认 ({{ capturedList.length }})</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
capturedList: [],
mediaStream: null,
useWebRTC: false,
ctxReady: false,
isBurstMode: false,
burstTimer: null
}
},
onReady() {
this.checkEnvironment()
},
onUnload() {
this.stopCamera()
this.clearBurstTimer()
},
methods: {
//
checkEnvironment() {
try {
if (typeof document !== 'undefined' && typeof navigator !== 'undefined' && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
this.useWebRTC = true
this.startWebRTC()
return
}
} catch (e) {}
this.useWebRTC = false
this.ctxReady = true
console.log('APP环境:使用系统相机拍照')
},
// H5: WebRTC
startWebRTC() {
var that = this
try {
if (typeof document === 'undefined') { this.ctxReady = true; return }
var video = document.getElementById('ccVideo')
if (!video) { this.ctxReady = true; return }
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment', width: 1280, height: 720 } })
.then(function(stream) {
that.mediaStream = stream
video.srcObject = stream
video.play()
that.ctxReady = true
})
.catch(function() { that.ctxReady = true })
} catch (e) { this.ctxReady = true }
},
stopCamera() {
this.clearBurstTimer()
if (this.mediaStream) {
var tracks = this.mediaStream.getTracks()
for (var i = 0; i < tracks.length; i++) { tracks[i].stop() }
this.mediaStream = null
}
},
//
toggleBurst() {
if (this.capturedList.length >= 9) {
uni.showToast({ title: '最多拍9张', icon: 'none' })
return
}
this.isBurstMode = true
this.doBurstCapture()
},
doBurstCapture() {
if (!this.isBurstMode || this.capturedList.length >= 9) {
this.isBurstMode = false
return
}
var that = this
this.captureOne(function() {
if (that.isBurstMode) {
that.burstTimer = setTimeout(function() { that.doBurstCapture() }, 1500)
}
})
},
stopBurst() {
this.isBurstMode = false
this.clearBurstTimer()
},
clearBurstTimer() {
if (this.burstTimer) { clearTimeout(this.burstTimer); this.burstTimer = null }
},
//
capturePhoto() {
if (this.capturedList.length >= 9) { uni.showToast({ title: '最多拍9张', icon: 'none' }); return }
this.captureOne()
},
//
captureOne(callback) {
if (this.useWebRTC && this.ctxReady && this.mediaStream) {
this.webRTCCapture(callback)
} else {
this.systemCapture(callback)
}
},
// H5: WebRTC canvas
webRTCCapture(callback) {
if (typeof document === 'undefined') { this.systemCapture(callback); return }
var canvas = document.getElementById('ccCanvas')
var video = document.getElementById('ccVideo')
if (!canvas || !video) { this.systemCapture(callback); return }
canvas.width = video.videoWidth || 1280
canvas.height = video.videoHeight || 720
var ctx = canvas.getContext('2d')
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
this.capturedList.push(canvas.toDataURL('image/jpeg', 0.85))
if (callback) callback()
},
// APP: Android/iOS APP
systemCapture(callback) {
var that = this
uni.chooseImage({
count: 1,
sourceType: ['camera'],
sizeType: ['original'],
success: function(res) {
if (res.tempFilePaths && res.tempFilePaths.length > 0) {
that.capturedList.push(res.tempFilePaths[0])
}
if (callback) callback()
},
fail: function() {
// APP
that.isBurstMode = false
that.clearBurstTimer()
if (callback) callback()
}
})
},
deletePhoto(idx) {
this.capturedList.splice(idx, 1)
},
confirmCapture() {
if (this.capturedList.length === 0) { uni.showToast({ title: '请先拍照', icon: 'none' }); return }
this.stopCamera()
var pages = getCurrentPages()
var prevPage = pages[pages.length - 2]
if (prevPage) { prevPage.$vm.capturedPhotoList = this.capturedList }
uni.navigateBack()
}
}
}
</script>
<style>
.cc-page { display:flex; flex-direction:column; height:100vh; background:#000; overflow:hidden; }
.cc-camera-wrap { flex:1; position:relative; overflow:hidden; background:#111; display:flex; align-items:center; justify-content:center; }
.cc-video { width:100%; height:100%; object-fit:cover; }
.cc-canvas { display:none; }
.cc-camera-hint { text-align:center; padding:40rpx; }
.cc-hint-icon { font-size:80rpx; display:block; margin-bottom:20rpx; }
.cc-hint-text { color:#999; font-size:28rpx; }
.cc-hint-sub { color:#666; font-size:24rpx; margin-top:10rpx; }
.cc-burst-overlay { position:absolute; top:0;left:0;right:0;bottom:0; background:rgba(0,0,0,0.25); display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:5; pointer-events:none; }
.cc-burst-tip { color:#fff; font-size:40rpx; font-weight:bold; text-shadow:0 2rpx 8rpx rgba(0,0,0,0.6); }
.cc-burst-count { color:rgba(255,255,255,0.9); font-size:32rpx; margin-top:16rpx; text-shadow:0 2rpx 8rpx rgba(0,0,0,0.6); }
.cc-burst-dots { color:#409eff; font-size:60rpx; margin-top:10rpx; animation:burstBlink 0.8s ease-in-out infinite; }
@keyframes burstBlink { 0%,100%{opacity:0.3} 50%{opacity:1} }
.cc-burst-hint { color:rgba(255,255,255,0.6); font-size:24rpx; margin-top:20rpx; text-shadow:0 2rpx 8rpx rgba(0,0,0,0.6); }
.cc-dot { margin:0 4rpx; }
.cc-thumb-bar { background:#1a1a1a; padding:16rpx 20rpx; }
.cc-thumb-count { color:#999; font-size:22rpx; margin-bottom:12rpx; }
.cc-thumb-list { display:flex; flex-wrap:wrap; gap:10rpx; }
.cc-thumb-item { width:100rpx; height:100rpx; border-radius:10rpx; overflow:hidden; position:relative; background:#333; }
.cc-thumb-img { width:100%; height:100%; }
.cc-thumb-del { position:absolute; top:-6rpx;right:-6rpx; width:32rpx;height:32rpx; border-radius:50%; background:rgba(0,0,0,0.6); display:flex; align-items:center; justify-content:center; }
.cc-del-icon { color:#fff; font-size:18rpx; }
.cc-thumb-placeholder { border:2rpx dashed #444; box-sizing:border-box; }
.cc-footer { display:flex; justify-content:space-between; align-items:center; padding:30rpx 40rpx; padding-bottom:calc(30rpx + constant(safe-area-inset-bottom)); padding-bottom:calc(30rpx + env(safe-area-inset-bottom)); background:#1a1a1a; }
.cc-capture-btn { width:80rpx;height:80rpx; border-radius:50%; border:6rpx solid #fff; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.cc-capture-inner { width:64rpx;height:64rpx; border-radius:50%; background:#fff; }
.cc-capture-btn:active .cc-capture-inner { background:#ccc; }
.cc-capture-btn.disabled { opacity:0.4; }
.cc-stop-btn { width:80rpx;height:80rpx; border-radius:50%; border:6rpx solid #ff4444; display:flex; align-items:center; justify-content:center; flex-shrink:0; background:rgba(255,68,68,0.15); }
.cc-stop-inner { color:#ff4444; font-size:32rpx; font-weight:bold; }
.cc-right-actions { display:flex; align-items:center; gap:20rpx; }
.cc-burst-toggle { padding:16rpx 24rpx; border-radius:32rpx; border:2rpx solid #409eff; background:transparent; }
.cc-burst-toggle-text { color:#409eff; font-size:24rpx; font-weight:bold; }
.cc-confirm-btn { padding:18rpx 40rpx; border-radius:40rpx; background:#409eff; }
.cc-confirm-btn.disabled { background:#555; }
.cc-confirm-text { color:#fff; font-size:28rpx; font-weight:bold; }
</style>

View File

@ -0,0 +1,438 @@
## 1.2.22026-06-11
> 新增功能
- 1、**iOS 端补齐 `previewRotation` 支持**`index.vue` 新增 `previewRotation` prop 与 `changePreviewRotation` 方法,`index.uts` 导出 `setPreviewRotation``ImaCameraManager.swift` 通过 `CGAffineTransform(rotationAngle:)` 实现预览层旋转(与 Android 行为对齐)。
> 问题修复
- 1、**修复首次进入时 `previewRotation` 初始值 90° 无效**`initCameraView()` 现在始终使用 `TextureView` 预览模式(不再等 `previewRotationDegrees != 0f` 才切换),避免初始化时用 `SurfaceView`、后续 prop 到达时 `setPreview(TEXTURE)` 触发相机重启导致旋转丢失。
- 2、**修复 `setPreviewRotation(0)` 不生效**`applyPreviewRotation()` 移除 `previewRotationDegrees == 0f` 提前返回0° 时显式重置 `cv.rotation = 0f``scaleX/Y = 1f`
- 3、**修复旋转切换按钮点击无效**`NVLoad()` 中在 `initCameraView()` 之前调用 `setPreviewRotation(this.previewRotation)`,确保原生 CameraView 创建时已读取旋转角度;`NVLayouted()` 增加安全网再次应用旋转。
- 4、**修复首次进入时 `onCameraOpened``$refs.cameraRef` 可能未就绪**:使用 `$nextTick` 延迟调用 `applyPreviewRotation()`picture→video 模式切换延迟从 300ms 延长至 800ms并在切换前同步调用一次旋转。
## 1.2.12026-06-10
> 新增功能
- 1、**外接摄像头预览旋转修正Android**:新增 `previewRotation` 属性及 `changePreviewRotation` 方法,用于修正无内置摄像头设备(如平板)外接 USB 摄像头时预览画面方向错误的问题;支持 `0` / `90` / `180` / `270`(或 `-90` 等等效角度)。
- 2、设备旋转时自动保持预览修正方向不变`onCameraOpened`、`onOrientationChanged` 时重新应用)。
> 问题修复
- 1、修复设置 `previewRotation` 后预览已正常、但**拍照输出图片方向仍不正确**的问题:拍照时会将 `previewRotation` 叠加到原生 `result.rotation` 后一并修正。
> 功能优化
- 1、启用 `previewRotation` 时自动切换为 `TextureView` 预览模式,并通过 `CameraView` 视图层旋转 + 缩放填充实现稳定预览(不依赖易被原生覆盖的 `TextureView.setTransform`)。
> 平台说明
- 本能力目前仅在 **`android`** 端实现;`ios` / `harmony` 暂未提供同等 API。
> 由于 **`harmony端`** 不支持兼容性组件,`harmony端`(指的是`harmony 5.0+`)不再本组件中更新,插件正在更新中
## 1.2.02026-05-24
> 新增功能
- 无
> 问题修复
- 无
> 功能优化
- 无
> 由于 **`harmony端`** 不支持兼容性组件,`harmony端`(指的是`harmony 5.0+`)不再本组件中更新,插件正在更新中
## 1.1.162026-05-20
> 新增功能
- 1、**预览圆角**:支持 `previewCornerRadius`、`previewCornerRadiusRate` 及 `changePreviewCorner`(圆形/多档圆角预览以原生实现为准)。
> 问题修复
- 1、修复「同页先预热 `video` 再轻触拍照」时,若在原生 `opened` 回调中无条件按 prop 再次 `changeMode`,会与原生「为拍照切换到 PICTURE 再在下一帧 `takePicture`」的逻辑冲突,导致预览反复关闭/打开、拍照异常的问题(已移除该盲目同步)。
- 2、`nvue` / 部分运行时下 `$refs` 实例上 **`changeMode` 等方法不可用** 的问题:通过 **`mode` prop + `watch`** 同步原生模式,页面侧推荐使用 `:mode` 切换 `picture` / `video`
> 功能优化
- 1、**`mode` prop** 增加监听:变更时调用原生 `setMode`,与 `changeMode` 行为对齐。
- 2、录像相关停止录像、开始时间戳等逻辑在主线程/时序上做了加固。
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
## 1.1.152026-02-02
> 新增功能(临时更新)
- 无
> 问题修复
- 1、部分手机无法录像的问题或者录像中没有返回结果的问题可能解决`{"errorCode"=>5,"reason"=>"录像失败","message"=>"java.lang.RuntimeException:startfailed."}`的问题)
- 1.1、增加了待录像(切换 VIDEO 模式后延后执行)
- 1.2、待拍照(切换 PICTURE 模式后延后执行)
> 功能优化
- 1、增加拍照、录像、结束录像时异常抛出
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
## 1.1.142026-01-30
> 新增功能(临时更新)
- 无
> 问题修复
- 1、部分手机无法录像的问题或者录像中没有返回结果的问题
- 2、修改代码结构准备改成兼容组件模式为`Harmony Next`做准备
> 功能优化
- 无
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
## 1.1.132026-01-22
> 新增功能(临时更新)
- 无
> 问题修复
- 1、修复获取Activity实例、Application上下文上下文出现的问题
- 2、修复相机按键、蓝牙自拍杆监听事件的问题
> 功能优化
- 1、相机震动做了兼容处理
- 2、相机的权限申请做了兼容处理
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
## 1.1.122026-01-19
> 新增功能(临时更新)
- 无
> 问题修复
- 1、修复安卓8.1事打开相机报错“相机初始失败或者当前设备不支持创建失败When targetSdkVersion >= 33
should use amdroid.permission.xxx,...”
> 功能优化
- 无
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
## 1.1.112026-01-07
> 新增功能(临时更新)
- 1、增加`take-error相机拍摄监测`、`orientation-change相机角度转换`、、`camera-change相机设置监听`
> 问题修复
- 1、在`setSizeSelectors`方法增加错误回调,便于排查一些手机调取无反应的问题
> 功能优化
- 无
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
## 1.1.102026-01-07
> 新增功能(临时更新)
- 1、增加`take-error相机拍摄监测`、`orientation-change相机角度转换`、、`camera-change相机设置监听`
> 问题修复
- 1、在`setSizeSelectors`方法增加错误回调,便于排查一些手机调取无反应的问题
> 功能优化
- 无
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
## 1.1.92026-01-06
> 新增功能(临时更新)
- 1、新增加载相机基础参数方法`loadCameraView`
> 问题修复
- 1、修复在 `HBuilderX 4.76` 及以下版本打包包括自定义基座在kotlin文件中无法获取当前
`UTSAndroid.getUniActivity()``UTSAndroid.getAppContext()`的问题
- 2、修复在 `HBuilderX 4.76` 及以下版本打包(包括自定义基座)时白屏后闪退的问题
- 3、修复高版本`安卓15+`及以上获取的SdkVersion值与当前应用实际的SdkVersion值不一致导致相机获取权限崩溃后闪退的问题
> 功能优化
- 无
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
## 1.1.82026-01-05
> 新增功能(临时更新)
- 1、是否将拍摄文件保存到本地可见媒体即相册的方法`changeGallery`
> 问题修复
- 1、相机权限的问题如用户拒绝后可直接跳转到当前 App 的系统设置页
> 功能优化
- 无
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
## 1.1.72026-01-04
> 新增功能(临时更新)
- 无
> 问题修复
- 1.1.6 版本推包后上传不成功的问题下载和导入还是1.1.5的代码)
> 功能优化
- 无
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
## 1.1.62026-01-04
> 新增功能(临时更新)
- 无
> 问题修复
- 1、安卓高版本安卓16出现拍照的问题
- 2、在一些其他终端设备非手机中出现相机预览画面与实际不符合的问题`【待复测】`
- 3、一些配置不生效的问题如changeAspectRatio、changeOrientation由于相机加载顺序问题导致的
- 4、一些其他终端设备非手机进入卡顿或者黑屏几秒才显示相机预览页面`【待复测】`
- 5、一些手机设备由于权限问题导致第一次进入是黑屏或者后几次进入偶尔出现黑屏闪现的问题`【待复测】`
> 功能优化
- 1、将之前的uts改成了kotlin的写法
- 2、将组件是的抛出调整成`CameraManager.setCameraCallback`,方便后期增加参数输出、控制
- 3、优化了相机加载的顺序问题、以及设置相机时的问题
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
## 1.1.52025-12-29
> 新增功能(临时更新)
- 新增设置相机使用设备方向的方法`changeOrientation`
- 新增设置相机网格及颜色的方法`changeGrid`
> 问题修复
- 无
> 功能优化
- 无
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
- 完善鸿蒙`Harmony`版本的并进行更新(已完成开发,测试中)
## 1.1.42025-12-28
> 新增功能(临时更新)
- 新增设置曝光值的方法`changeExposure`
> 问题修复
- 修复动态权限申请的问题即Android 12 及以下、Android 13+
- 修复 Android 16(即API级别为36)时摄像回调的问题
> 功能优化
- 无
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
- 完善鸿蒙`Harmony`版本的并进行更新
## 1.1.32025-12-02
> 新增功能(临时更新)
- 无
> 问题修复
- 打包报错问题修复4.85版本有问题4.76以下的版本未复现)
- 报错内容:
`ima-camera-view/utssdk/app-android/src/index.kt:723:55 Argument type mismatch: actual type is 'Any', but 'Number' was expected`
> 功能优化
- 无
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
- 完善鸿蒙`Harmony`版本的并进行更新
## 1.1.22025-11-06
> 新增功能(临时更新)
- 无
> 问题修复
- `快捷键拍照(蓝牙自拍杆、手机音量键)`拍照时,返回后按钮无法操作的问题修复
- 页面在没启用`shortcut快捷键拍照`时,也会进入快捷键的问题修复
- 修复在低版本`HubuildX`时打包出现找不到类型的问题
> 功能优化
- 优化了相机资源在进入时卡顿的问题
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
- 完善鸿蒙`Harmony`版本的并进行更新
## 1.1.12025-11-03
> 新增功能(临时更新)
- 新增设置照片输出格式的方法`changeSuffix`
> 问题修复
- 修复照片格式无法设置的问题【原因是在uni给出的生命周期内无法取到`props`的参数,只能在`watch`中处理】
> 功能优化
- 无
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
- 完善鸿蒙`Harmony`版本的并进行更新
## 1.1.02025-10-31
> 新增功能
- 快捷键拍照: 如:按两下音量键拍照、按音量键拍照等等(可自定义)
- 蓝牙自拍杆: 提供可以连接蓝牙自拍杆拍照、对焦等(可自定义)
- 相机`是否开启蓝牙自拍杆、手机快捷键拍照`,可自定义快捷键,具体参考参数`shortcut`
-
> 问题修复
- 无
> 功能优化
- 无
> 计划新增功能(大版本更新【即 `1.2.0` 开始】)
- 完善鸿蒙`Harmony`版本的并进行更新
## 1.0.52025-10-30
> 新增功能(临时更新)
- 相机`录像`方法增加设置`视频录制时长限制`
- 相机`录像`声音默认系统录像声音,也可自定义,具体参考参数`recorder`、`sound2`
> 问题修复
- 无
> 功能优化
- 无
## 1.0.42025-10-18
> 新增功能(临时更新)
- 增加设置相机`白平衡`方法:`changeWhiteBalance`
- 增加设置相机`HDR`方法:`changeHdr`
- 增加设置相机`特定比例`方法:`changeAspectRatio`
- 新增了`widthRatio`、`heightRatio`、`tolerance`、`whiteBalance`、`hdr`、`shutter`、`sound`、`vibrate`、
`duration`等参数具体参考API
> 问题修复
- 无
> 功能优化
- 照片文件的尺寸和手机原相机的尺寸不对的问题,可以通过参数`duration`去控制,其中`全屏`、`1:1`
基本是和原相机尺寸一致的测试了大部分手机型号都OK个别手机`3:4`、`4:3`、`9:16`时会有少许容差,可以通过参数
`duration`去控制)
## 1.0.32025-10-16
> 新增功能
- 增加拍照声音,可以自定义声音文件(默认手机原相机声音)
- 增加拍照震动可以自定义震动时长默认200毫秒
> 问题修复
> 无
> 功能优化
- 照片文件的分辨率取手机原相机的,提高照片的清晰度
## 1.0.22025-09-09
> 新增功能
> 无
> 问题修复
> 无
> 功能优化
- 支持拍照预览或从相册选择后,返回当前相机页面时,如果出现黑屏状态,可以重新自己手动拉起相机
## 1.0.12025-09-05
> 新增功能
> 无
> 问题修复
- 修复高版本(`4.66`时)打包报错:`*(项目路径)*//index.kt:44:12 Unresolved reference: _uA`、
`*(项目路径)*/index.kt:118:48 Unresolved reference: _uO`的问题
- 修复前置拍出来的图片是镜像的问题
- 修复照片拍出来尺寸不对、照片过小的问题
> 功能优化
> 无
## 1.0.02025-05-27
> 新增功能
- 新增方法:`close`、 `open``takePhoto``takePhotoSnapshot``takeVideo`、`takeVideoSnapshot`、
`stopVideo``changeZoom``changeFacing`、`changeFlash`、`changeAudio`
- 新增事件:`onPictureTaken`、 `onVideoTakenStart``onVideoTakenEnd``onFocusStart``onFocusEnd`
- `android端`的所有功能已完成开发、测试
- `harmony端`计划开发中
- 初始版
> 问题修复
> 无
> 功能优化
> 无

Binary file not shown.

View File

@ -0,0 +1,126 @@
{
"id": "ima-camera-view",
"displayName": "原生Camera自定义相机拍照、视频录制",
"version": "1.2.2",
"description": "原生Camera相机开发的UTS插件支持相机拍照、视频录制、可实现点击聚焦、手势缩放、自定义布局、自定义蒙版用于人脸拍照身份证拍照等同时支持蓝牙自拍杆可自定义、手机快捷键可自定义",
"keywords": [
"原生Camera",
"相机拍照",
"视频录制",
"自定义布局相机",
"自定义蒙版相机"
],
"repository": "",
"engines": {
"HBuilderX": "^4.8.1",
"uni-app": "^4.81",
"uni-app-x": "^4.81"
},
"dcloudext": {
"type": "component-uts",
"sale": {
"regular": {
"price": "19.99"
},
"sourcecode": {
"price": "299.00"
}
},
"contact": {
"qq": "488266488"
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "摄像头、音频、文件读取、文件写入\n <uses-permission android:name=\"android.permission.CAMERA\" />\n <uses-permission android:name=\"android.permission.RECORD_AUDIO\" />\n <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n <uses-permission android:name=\"android.permission.VIBRATE\" />"
},
"npmurl": "",
"darkmode": "x",
"i18n": "x",
"widescreen": "√"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "x",
"aliyun": "x",
"alipay": "x"
},
"client": {
"uni-app": {
"vue": {
"vue2": {
"extVersion": "1.1.13",
"minVersion": ""
},
"vue3": {
"extVersion": "1.1.13",
"minVersion": ""
}
},
"web": {
"safari": "x",
"chrome": "x"
},
"app": {
"vue": {
"extVersion": "1.1.13",
"minVersion": ""
},
"nvue": {
"extVersion": "1.1.13",
"minVersion": ""
},
"android": {
"extVersion": "1.1.13",
"minVersion": "21"
},
"ios": "x",
"harmony": {
"extVersion": "1.1.13",
"minVersion": "5.0以下(不包括5.0)"
}
},
"mp": {
"weixin": "x",
"alipay": "x",
"toutiao": "x",
"baidu": "x",
"kuaishou": "x",
"jd": "x",
"harmony": "x",
"qq": "x",
"lark": "x",
"xhs": "-"
},
"quickapp": {
"huawei": "x",
"union": "x"
}
},
"uni-app-x": {
"web": {
"safari": "x",
"chrome": "x"
},
"app": {
"android": {
"extVersion": "1.1.13",
"minVersion": "21"
},
"ios": "x",
"harmony": {
"extVersion": "1.1.13",
"minVersion": "5.0以下(不包括5.0)"
}
},
"mp": {
"weixin": "x"
}
}
}
}
}
}

View File

@ -0,0 +1,373 @@
# 原生自定义相机拍照、视频录制 ima-camera-view
`原生自定义相机拍照、视频录制 ima-camera-view`是基于原生相机开发的UTS插件支持`相机拍照`、`视频录制`
、可实现`点击聚焦`、`手势缩放`、
`自定义布局`、`自定义蒙版`(用于人脸拍照,身份证拍照等)。
## ⚠️注意️
由于之前技术选型不支持继续扩展相机的新特性,`本插件ima-camera-view继续维护只修复现存的一些bug不再新增新功能`
建议使用ima-camerax-view这个相机组件插件[ima-camerax-view 地址](https://ext.dcloud.net.cn/plugin?name=ima-camerax-view)
如果不追求如:**超广角镜头切换0.5x`changeWideAngle`**、**按倍率缩放(`setCameraZoom`,如 0.5 / 1.0 / 2.0**、**多摄逻辑镜头切换**、
**Camera2 白平衡 / HDR / 曝光补偿**、**录像进度(`onVideoTakenProgress`)与设备方向变化(`onOrientationChanged`)回调**等新特性的话,扔建议继续使用本插件。
- `ima-camerax-view【新插件】`[插件地址](https://ext.dcloud.net.cn/plugin?name=ima-camerax-view)
- `ima-camera-view【本插件】`[插件地址](https://ext.dcloud.net.cn/plugin?name=ima-camera-view)
- `ima-camerax-view【新插件】``ima-camera-view【本插件】`组件 API 保持基本一致,便于从 CameraView 版平滑迁移
## 支持功能
- 打开、关闭摄像头预览
- 拍照、快照拍照
- 录制视频、快照录制视频
- 设置摄像头缩放级别
- 设置相机白平衡
- 设置相机HDR
- 设置相机曝光
- 设置摄像头方向
- 设置闪光灯模式
- 设置相机使用设备方向
- 设置相机网格及颜色
- 设置音频(录制视频时)
- 设置圆角、圆预览(可自定义)
- 外接摄像头预览方向修正(`previewRotation`Android
- 设置拍照、录制视频的声音(可自定义)
- 蓝牙自拍杆(可自定义)
- 手机快捷键(可自定义)
## 自定义调整
- 自定义调整页面地址:`uni_modules/ima-camera-view/utssdk/app-android/index.vue`
- 蓝牙自拍杆、手机快捷键的自定义,可以参考文件中的`shortcutListener`方法
- 设置拍照、录制视频的声音,可以参考文件中的`photoSound`、`videoSound`方法
## 需要权限
- 摄像头、音频、文件读取、文件写入、震动
```text
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO",
"android.permission.VIBRATE"
"android.permission.READ_EXTERNAL_STORAGE"
"android.permission.WRITE_EXTERNAL_STORAGE"
```
- 即:在`manifest.json`中的`distribute.android.permissions`加入
```text
// 拍摄照片和视频时需要
<uses-permission android:name="android.permission.CAMERA" />
// 拍摄视频时需要Audio.ON默认
<uses-permission android:name="android.permission.RECORD_AUDIO" />
// 读取拍照、录像文件文件时需要
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
// 报错拍照、录像文件文件时需要(默认保存到沙盒缓存)
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
// 震动权限
<uses-permission android:name="android.permission.VIBRATE" />
```
## 快门声音素材
- 将需要的快门声音放在`uni_modules/ima-camera-view/utssdk/app-android/assets`下即可
- [熊猫办公](https://www.tukuppt.com/yinxiaomuban/kuaimenshengyin.html)
- [站长素材](https://sc.chinaz.com/tag_yinxiao/kuaimen.html)
## 使用示例【此示例的代码只实现了`相机拍照`的逻辑,更多示例请导入项目】
- 新建一个`camera.nvue`的文件
- ⚠️注意️:只能在`.nvue`、`.uvue`的文件后缀下才生效,不支持`.vue`
```nvue
<template>
<view class="ima-camera" :style="{ width: windowWidth, height: windowHeight }">
<ima-camera-view
ref="cameraRef"
class="camera-view"
:style="{ width: windowWidth + 'px', height: windowHeight + 'px' }"
flash="on"
@onPictureTaken="onPictureTaken"
onFocusStart="onFocusStart"
/>
<view class="camera-menu">
<!--返回键-->
<cover-image @tap="back" class="camera-menu-button back" src="/static/camera/back.png" />
<!--快门键-->
<cover-image
@tap="takePhoto"
class="camera-menu-button shutter"
src="/static/camera/shutter.png"
/>
<!--反转键-->
<cover-image @tap="flip" class="camera-menu-button flip" src="/static/camera/flip.png" />
</view>
</view>
</template>
<script>
let _this = null
export default {
data() {
return {
windowWidth: '', //屏幕可用宽度
windowHeight: '', //屏幕可用高度
facing: 'back'
}
},
onLoad() {
_this = this
this.initCamera()
},
methods: {
//初始化相机
initCamera() {
console.log('初始化相机')
uni.getSystemInfo({
success: (res) => {
_this.windowWidth = res.windowWidth
_this.windowHeight = res.windowHeight
}
})
},
onFocusStart(e) {
console.log('聚焦', e)
},
takePhoto() {
console.log('拍照', this.facing)
this.$refs.cameraRef.takePhoto()
},
//返回
back() {
console.log('返回上一页', this.facing)
uni.navigateBack()
},
//反转
flip() {
console.log('镜头反转', this.facing)
this.facing = this.facing === 'back' ? 'front' : 'back'
this.$refs.cameraRef.changeFacing(this.facing)
},
onPictureTaken(e) {
console.log('拍照结果', e.detail)
_this.snapshotsrc = e.detail?.path || ''
_this.getTakenRes()
uni.navigateBack()
},
//设置
getTakenRes() {
console.log('返回结果给上一页')
let pages = getCurrentPages()
let prevPage = pages[pages.length - 2] //上一个页面
//直接调用上一个页面的setImage()方法,把数据存到上一个页面中去
prevPage.$vm.setImage({ path: _this.snapshotsrc })
}
}
}
</script>
<style lang="scss">
.ima-camera {
justify-content: center;
align-items: center;
.camera-view {
width: 100%;
background: #111;
}
.camera-menu {
position: absolute;
left: 0;
bottom: 0;
width: 750rpx;
height: 180rpx;
z-index: 98;
align-items: center;
justify-content: center;
&-button {
width: 80rpx;
height: 80rpx;
position: absolute;
bottom: 50rpx;
z-index: 99;
align-items: center;
justify-content: center;
}
.back {
left: 30rpx;
}
.shutter {
width: 130rpx;
height: 130rpx;
left: 310rpx;
bottom: 25rpx;
}
.flip {
right: 30rpx;
}
}
}
</style>
```
## 外接摄像头方向修正Android
适用于**无内置摄像头**或外接 USB 摄像头的设备(如横屏平板、工控一体机)。当物理安装的摄像头与屏幕方向不一致,导致预览画面旋转 90° 等情况时,可通过 `previewRotation` 固定修正预览与拍照方向。
### 使用方式
**方式一:属性(推荐)**
```nvue
<ima-camera-view
ref="cameraRef"
:previewRotation="90"
:previewCornerRadiusRate="0.5"
facing="front"
/>
```
**方式二:方法动态调整**
```javascript
this.$refs.cameraRef.changePreviewRotation(90) // 顺时针 90°
this.$refs.cameraRef.changePreviewRotation(-90) // 逆时针 90°等价 270°
```
### 参数说明
| 取值 | 说明 |
|------|------|
| `0` | 不修正(默认) |
| `90` / `-270` | 顺时针旋转 90° |
| `180` / `-180` | 旋转 180° |
| `270` / `-90` | 顺时针旋转 270°逆时针 90° |
### 注意事项
- 仅 **`android`** 端生效;与 `orientation`(设备方向跟随)不同,`previewRotation` 用于**固定补偿外接摄像头安装角度**,不随手机旋转而改变。
- 启用非 `0``previewRotation` 后,组件会自动使用 `TextureView` 预览;拍照结果会同步叠加该角度修正。
- 若预览方向仍不对,请依次尝试 `90`、`-90`、`180`,以实际安装方向为准。
- 录像方向暂未做同等叠加修正;若录像也有方向问题,可在业务层二次处理或反馈 issue。
## 常见的比例的定义widthRatioheightRatio
```typescript
// 正方形
AspectRatio.of(1, 1) // 1:1
// 竖屏比例
AspectRatio.of(9, 16) // 9:16 (手机竖屏)
AspectRatio.of(3, 4) // 3:4
AspectRatio.of(2, 3) // 2:3
AspectRatio.of(10, 16) // 10:16 (5:8)
// 横屏比例
AspectRatio.of(16, 9) // 16:9 (宽屏)
AspectRatio.of(4, 3) // 4:3 (传统)
AspectRatio.of(3, 2) // 3:2 (照片)
AspectRatio.of(16, 10) // 16:10 (8:5)
AspectRatio.of(21, 9) // 21:9 (超宽屏)
// 建议比例
const AspectRatios = {
// 1:1 正方形
SQUARE: AspectRatio.of(1, 1),
// 9:16 竖屏(手机默认)
PORTRAIT: AspectRatio.of(9, 16),
// 16:9 横屏
LANDSCAPE: AspectRatio.of(16, 9),
// 3:4 传统照片比例
THREE_FOUR: AspectRatio.of(3, 4),
// 4:3 传统相机比例
FOUR_THREE: AspectRatio.of(4, 3)
}
```
## 不同场景的推荐值tolerance
```typescript
const TOLERANCE = {
STRICT: 0.01.toFloat(), // 非常严格,几乎精确匹配
STANDARD: 0.05.toFloat(), // 标准,推荐使用
FLEXIBLE: 0.1.toFloat(), // 灵活,兼容更多设备
LOOSE: 0.2.toFloat() // 宽松,可能匹配到意外比例
}
```
## Api
| 属性 | 类型 | 默认值 | 说明 | 平台 |
|---------------|-----------------|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
| widthRatio | Number | 0 | 照片尺寸比率(宽度): 默认全屏若widthRatio为9heightRatio为16则为9:16、widthRatio为3heightRatio为4则为3:4...不建议值过大常用的比例有1:1、3:4、4:3、9:16... | `android` |
| heightRatio | Number | 0 | 照片尺寸比率(高度): 默认全屏若widthRatio为9heightRatio为16则为9:16、widthRatio为3heightRatio为4则为3:4...不建议值过大常用的比例有1:1、3:4、4:3、9:16... | `android` |
| tolerance | Number | 0.1 | 照片尺寸容差值: 建议设为 0.05~0.15,以便稍微兼容不同设备相机实际比例差异 ,值为:01 | `android` |
| whiteBalance | iWhiteBalance | "auto" | 白平衡模式: auto自动、incandescent白炽、fluorescent荧光、daylight日光、cloudy多云 、loudy多云【慎用`兼容1.1.3版本前单词拼错的问题将在1.2.0大版本更新后删除建议1.1.3后的版本使用cloudy`】) | `android` |
| hdr | iHdr | "off" | HDR模式: off关闭、on开启 | `android` |
| facing | iFacing | "back" | 后置、前置摄像头: back(后置摄像头)、front(前置摄像头) | `android` |
| flash | iFlash | "off" | 闪光灯: off(关闭)、on(开启)、auto(自动)、torch(常开) | `android` |
| audio | iAudio | "on" | 音频: on(开启)、off(关闭)、mono(单声道)、stereo(立体声) | `android` |
| orientation | iOrientation | "auto" | 方向: auto(自动)、portrait(竖屏)、landscape(横屏) 【注意:`目前竖屏、横屏拍出来都为竖屏方式,这两个参数的使用效果一致`,为预留参数,为后期做准备】 | `android` |
| grid | iGrid | "off" | 网格: off(关闭)、draw_3X3(3x3)、draw_4x4(4x4)、draw_phi(phi) | `android` |
| gridColor | String | "#808080" | 颜色值: 只支持如:#fff、#ffffff、#......等类型的颜色值 | `android` |
| photoSuffix | iPhotoSuffix | "jpeg" | 照片格式: jpeg、jpg | `android` |
| mode | String | 'picture' | 相机模式: picture(拍照)、video(录视频) | `android` |
| previewCornerRadius | Number | 0 | 预览圆角半径px`previewCornerRadiusRate` 二选一或组合使用 | `android` |
| previewCornerRadiusRate | Number | 0 | 预览圆角比例(相对短边);`0.5` 且预览区域为正方形时为圆形预览 | `android` |
| previewRotation | Number | 0 | 外接摄像头预览/拍照方向修正角度:`0`、`90`、`180`、`270`(支持负值如 `-90`);详见上文「外接摄像头方向修正」 | `android` |
| gallery | Boolean | false | 是否将拍照、录像文件保存到系统相册(可见媒体库) | `android` |
| shutter | Boolean | true | 是否打开拍照声音: true(开启,此时配置`sound`才起作用)、false(关闭) | `android` |
| sound | String | '' | 相机拍照声音文件: 将mp3音频文件放在`uni_modules/ima-camera-view/utssdk/app-android/assets`下即可,为音频文件的名称,如`xxx.mp3`,默认手机原声 | `android` |
| recorder | Boolean | true | 是否打开录像声音: true(开启,此时配置`sound2`才起作用)、false(关闭) | `android` |
| sound2 | String | '' | 相机录像声音文件: 将mp3音频文件放在`uni_modules/ima-camera-view/utssdk/app-android/assets`下即可,为音频文件的名称,如`xxx.mp3`,默认手机原声 | `android` |
| vibrate | Boolean | false | 是否打开拍照震动: true(开启,此时配置`duration`才起作用)、false(关闭) | `android` |
| duration | Number | 300 | 是否打开拍照震动时长单位毫秒ms | `android` |
| shortcut | Boolean | false | 是否开启蓝牙自拍杆、手机快捷键拍照: false(关闭)、true(开启) | `android` |
## 方法
### 共同 方法/* */
| 方法名称 | 说明 | 方法参数 | 平台 |
|-------------------------------------------------------|---------------------------|-----------------------------------------------------------------------------------------------------------------------|-------------|
| open | 打开摄像头预览 | 无 | `android` |
| close | 关闭摄像头预览 | 无 | `android` |
| takePhoto | 拍照(标准拍照流程) | 无 | `android` |
| takeVideo(duration) | 开始录制视频 | duration:拍摄时长单位毫秒ms【0 表示不限制】 | `android` |
| stopVideo | 停止视频录制 | 无 | `android` |
| changeZoom(zoom) | 设置摄像头缩放级别 | zoom:缩放倍数(浮点数) | `android` |
| changeExposure(exposure) | 设置曝光值 | exposure:曝光值(浮点数)【值为:-22默认为0】 | `android` |
| changeWhiteBalance(whiteBalance) | 设置相机白平衡 | whiteBalance:参考`api`中的`whiteBalance`参数 | `android` |
| changeHdr(hdr) | 设置相机HDR | hdr:参考`api`中的`hdr`参数 | `android` |
| changeFacing((facing)) | 设置摄像头方向 | facing:参考`api`中的`facing`参数 | `android` |
| changeFlash(flash) | 设置闪光灯模式 | flash:参考`api`中的`flash`s参数 | `android` |
| changeOrientation(orientation) | 设置相机使用设备方向 | orientation:参考`api`中的`orientation`s参数 | `android` |
| changeGrid(grid,color) | 设置相机网格及颜色 | grid:参考`api`中的`grid`参数, color: 参考`api`中的`gridColor`参数 | `android` |
| changeAudio(audio) | 设置音频 | audio:参考`api`中的`audio`参数 | `android` |
| changeSuffix(suffix) | 设置照片输出格式 | suffix:参考`api`中的`photoSuffix`参数 | `android` |
| changeSizeSelectors(width,height,tolerance) | 设置相机特定比例/分辨率 | width、height、tolerance 参考 `api` 中同名属性;`0` 表示使用屏幕宽高 | `android` |
| changePreviewCorner(radius,radiusRate) | 设置预览圆角 | radius: 圆角 pxradiusRate: 圆角比例(正方形下 `0.5` 为圆形) | `android` |
| changePreviewRotation(degrees) | 设置外接摄像头预览/拍照方向修正 | degrees: `0` / `90` / `180` / `270`(支持负值);仅 Android | `android` |
| changeGallery(gallery) | 是否保存到系统相册 | gallery: `true` / `false` | `android` |
| changeMode(mode) | 设置相机模式 | mode: `picture` / `video`;也可直接使用 `:mode` prop | `android` |
| takePhotoSnapshot | 快照拍照(适用于快速拍照场景) | 无 | `android` |
| takeVideoSnapshot(duration) | 快照方式录制视频 | duration:拍摄时长单位毫秒ms【0 表示不限制】 | `android` |
## 事件
| 事件名称 | 说明 | 回调参数 | 平台 |
|---------------------|---------------|-----------------------------------|-------------|
| onPictureTaken | 拍照返回数据 | ({path,width,height}: any) => {} | `android` |
| onVideoTakenStart | 录制视频开始事件 | () => {} | `android` |
| onVideoTakenEnd | 录制视频结束事件 | ({path,size}: any) => {} | `android` |
| onFocusStart | 自动对焦开始 | ({x,y}: any) => {} | `android` |
| onFocusEnd | 自动对焦结束 | ({x,y,focus}: any) => {} | `android` |
| onOrientationChange | 设备方向变化 | ({angle,orientation,isPortrait,isLandscape}: any) => {} | `android` |
| onCameraOpened | 相机已打开 | (data: any) => {} | `android` |
| onCameraClosed | 相机已关闭 | (data: any) => {} | `android` |
| onCameraError | 相机错误 | ({errorCode,reason,message}: any) => {} | `android` |

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
package="uts.sdk.modules.imaCameraView">
<!-- 相机权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 震动权限 -->
<uses-permission android:name="android.permission.VIBRATE" />
<!-- 录音权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Android 13+ -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- Android 12 及以下 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 功能声明 -->
<uses-feature android:name="android.hardware.camera" android:required="true"/>
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:name="android.hardware.camera.flash" />
</manifest>

View File

@ -0,0 +1,9 @@
{
"minSdkVersion": 21,
"dependencies": [
{
"id": "com.otaliastudios:cameraview",
"source": "implementation('com.otaliastudios:cameraview:2.7.2') { exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7'; exclude group: 'androidx.core', module: 'core' }"
}
]
}

View File

@ -0,0 +1,651 @@
<template>
<view>
<slot />
</view>
</template>
<script lang="uts">
import FrameLayout from 'android.widget.FrameLayout';
import { ListenerOptions } from 'com.maxiaoqu.camera'
import {
initCameraView, setCameraCallback,
applyPermission, openAppSettings, vibrate, shutterSound, recordSound, keyDestroyer, keyListener,
open, reopen, close, destroyCamera, takePhoto, takePhotoSnapshot, takeVideo, takeVideoSnapshot, stopVideo,
setZoom, setExposure, setWhiteBalance, setHdr, setFacing, setGrid, setFlash, setAudio, setSuffix, setGallery, setOrientation, setMode, setSizeSelectors,
setPreviewCorner, setPreviewRotation, isTakingVideo, isTakingPicture, isOpened
} from './index.uts'
export default {
name: "ima-camera-view",
props: {
mode: {
type: String,
default: 'picture' // video
},
widthRatio: {
type: Number,
default: 0
},
heightRatio: {
type: Number,
default: 0
},
tolerance: {
type: Number,
default: 0.1
},
previewCornerRadius: {
type: Number,
default: 0
},
previewCornerRadiusRate: {
type: Number,
default: 0
},
previewRotation: {
type: Number,
default: 0
},
whiteBalance: {
type: String,
default: 'auto'
},
hdr: {
type: String,
default: 'off'
},
facing: {
type: String,
default: 'back'
},
flash: {
type: String,
default: 'off'
},
audio: {
type: String,
default: 'on'
},
photoSuffix: {
type: String,
default: 'jpeg'
},
orientation: {
type: String,
default: 'auto'
},
grid: {
type: String,
default: 'off'
},
gridColor: {
type: String,
default: '#808080'
},
shutter: {
type: Boolean,
default: true
},
sound: {
type: String,
default: ''
},
recorder: {
type: Boolean,
default: true
},
sound2: {
type: String,
default: ''
},
vibrate: {
type: Boolean,
default: false
},
duration: {
type: Number,
default: 300
},
gallery: {
type: Boolean,
default: false
},
shortcut: {
type: Boolean,
default: false
},
},
data() {
return {
cameraZoom: 0,
cameraFacing: 'back',
cameraViewActivity: null,
cameraViewContext: null,
}
},
watch: {
mode: {
handler(newValue : String) {
this.changeMode(newValue)
},
immediate: false
},
whiteBalance: {
handler(newValue : String) {
this.changeWhiteBalance(newValue)
},
immediate: true
},
hdr: {
handler(newValue : String) {
this.changeHdr(newValue)
},
immediate: true
},
facing: {
handler(newValue : String) {
this.cameraFacing = newValue;
this.changeFacing(newValue)
},
immediate: false
},
flash: {
handler(newValue : String) {
this.changeFlash(newValue)
},
immediate: false
},
audio: {
handler(newValue : String) {
this.changeAudio(newValue)
},
immediate: false
},
photoSuffix: {
handler(newValue : String) {
this.changeSuffix(newValue)
},
immediate: true
},
orientation: {
handler(newValue : String) {
this.changeOrientation(newValue)
},
immediate: true
},
grid: {
handler(newValue : String) {
this.changeGrid(newValue, '#808080')
},
immediate: true
},
gallery: {
handler(newValue : Boolean) {
this.changeGallery(newValue)
},
immediate: true
},
previewCornerRadius: {
handler(newValue ?: Number) {
this.changePreviewCorner(newValue, this.previewCornerRadiusRate)
},
immediate: true
},
previewCornerRadiusRate: {
handler(newValue ?: Number) {
this.changePreviewCorner(this.previewCornerRadius, newValue)
},
immediate: true
},
previewRotation: {
handler(newValue : Number) {
this.changePreviewRotation(newValue)
},
immediate: true
},
shortcut: {
handler(newValue : Boolean) {
this.shortcutListener()
},
immediate: true
}
},
/**
* 组件涉及的事件声明只有声明过的事件才能被正常发送
*/
emits: [
'onCameraOpened',
'onCameraClosed',
'onPictureTaken',
'onVideoTakenStart',
'onVideoTakenProgress',
'onVideoTakenEnd',
'onFocusStart',
'onFocusEnd',
'onZoomChanged',
'onCameraChange',
'onOrientationChange',
'onCameraTakenError',
'onCameraError',
],
/**
* 规则如果没有配置expose则methods中的方法均对外暴露如果配置了expose则以expose的配置为准向外暴露
* ['publicMethod'] 含义为只有 `publicMethod` 在实例上可用
*/
expose: [
'open',
'reopen',
'close',
'destroyCamera',
'takePhoto',
'takePhotoSnapshot',
'takeVideo',
'takeVideoSnapshot',
'stopVideo',
'changeZoom',
'changeExposure',
'changeWhiteBalance',
'changeHdr',
'changeFacing',
'changeMode',
'changeOrientation',
'changeGrid',
'changeFlash',
'changeAudio',
'changeSuffix',
'changeSizeSelectors',
'changePreviewCorner',
'changePreviewRotation',
'changeGallery',
'openAppSettings'
],
methods: {
//
initCameraView(): FrameLayout{
return initCameraView((success : boolean, message : string) => {
if (success) {
this.changePreviewCorner(this.previewCornerRadius, this.previewCornerRadiusRate);
this.initCamera();
} else {
console.warn(`相机初始失败或者当前设备不支持:${message}`);
}
})
},
//
initCameraPermission() {
applyPermission((allRight : boolean, grantedList : Array<String>) => {
//
if (allRight) {
this.initCameraView();
console.log(`用户同意了全部权限: ${grantedList}`);
}
// grantedList
else {
console.warn(`部分权限被拒绝: ${grantedList}`);
}
}, (doNotAskAgain : boolean, grantedList : Array<String>) => {
//
if (doNotAskAgain) {
// App
openAppSettings()
console.warn(`权限被永久拒绝: ${grantedList}`);
}
})
},
openAppSettings(){
openAppSettings()
},
//
initCamera() {
setCameraCallback((event : string, data : any) => {
switch (event) {
case "opened":
console.log("相机已打开", data)
this.$emit('onCameraOpened', data)
break
case "closed":
console.log("相机已关闭", data)
this.$emit('onCameraClosed', data)
break
case "picture":
console.log("照片", data)
this.$emit('onPictureTaken', data)
break
case "video-start":
console.log("开始录制", data)
this.$emit('onVideoTakenStart', data)
break
case "video-progress":
console.log("录制中", data)
this.$emit('onVideoTakenProgress', data)
break
case "video-end":
console.log("结束视频", data)
// this.$emit('onVideoTakenEnd', data)
break
case "video":
console.log("视频资源", data)
this.$emit('onVideoTakenEnd', data)
this.$emit('onVideoTaken', data)
break
case "focus-start":
console.log("对焦开始", data)
this.$emit('onFocusStart', data)
break
case "focus-end":
console.log("对焦结束", data)
this.$emit('onFocusEnd', data)
break
case "zoom-change":
console.log("缩放级别", data)
this.$emit('onZoomChanged', data)
break
case "camera-change":
console.log("相机设置", data)
this.$emit('onCameraChange', data)
break
case "orientation-change":
console.log("相机角度转换", data)
this.$emit('onOrientationChange', data)
break
case "take-error":
console.log("相机拍摄监测", data)
this.$emit('onCameraTakenError', data)
break
case "error":
console.log("相机出错", data)
this.$emit('onCameraError', data)
break
}
})
open();
},
//
open() {
open();
},
//
reopen() {
reopen();
},
//
close() {
close();
},
//
destroyCamera() {
if (this.$el != null) {
destroyCamera()
}
},
//
takePhoto() {
this.photoSound()
takePhoto()
},
//
takePhotoSnapshot() {
this.photoSound()
takePhotoSnapshot()
},
//
takeVideo(duration ?: Number) {
const videoDuration : Number = duration ?? 0;
this.videoSound(true)
takeVideo(videoDuration)
},
//
takeVideoSnapshot(duration ?: Number) {
const videoDuration : Number = duration ?? 0;
this.videoSound(true)
takeVideoSnapshot(videoDuration)
},
//
stopVideo() {
stopVideo();
},
// zoom
changeZoom(zoom : Number) {
setZoom(zoom)
},
// exposure
changeExposure(exposure : Number) {
setExposure(exposure)
},
//
changeWhiteBalance(whiteBalance : String) {
setWhiteBalance(whiteBalance)
},
// HDR
changeHdr(hdr : String) {
setHdr(hdr)
},
//
changeFacing(facing : String) {
setFacing(facing)
},
//
changeMode(mode : String) {
setMode(mode)
},
//
changeGrid(grid : String, color : String) {
setGrid(grid, color)
},
//
changeFlash(flash : String) {
setFlash(flash)
},
//
changeAudio(audio : String) {
setAudio(audio)
},
//
changeSuffix(suffix : String) {
setSuffix(suffix)
},
//
changeGallery(gallery : Boolean) {
setGallery(gallery)
},
// 使
changeOrientation(orientation : String) {
setOrientation(orientation)
},
//
changeSizeSelectors(width : Number, height : Number, tolerance : Number) {
setSizeSelectors(width, height, tolerance)
},
// radiusRate=0.5
changePreviewCorner(radius ?: Number, radiusRate ?: Number) {
let safeRadius : Number = 0
let safeRadiusRate : Number = 0
if (radius != null) {
safeRadius = radius
}
if (radiusRate != null) {
safeRadiusRate = radiusRate
}
setPreviewCorner(safeRadius, safeRadiusRate)
},
// 0/90/180/270
changePreviewRotation(degrees : Number) {
setPreviewRotation(degrees)
},
//
isTakingVideo() : Boolean {
return isTakingVideo();
},
//
isTakingPicture() : Boolean {
return isTakingPicture();
},
//
isOpened() : Boolean {
return isOpened();
},
//
photoSound() {
if (this.vibrate) {
vibrate(this.duration ?? 300)
}
if (this.shutter) {
shutterSound(this.sound ?? '')
}
},
//
videoSound(isStart : Boolean) {
if (this.recorder) {
recordSound(isStart, this.sound2 ?? '')
}
},
//
shortcutListener() {
//
if (!this.shortcut) {
return
}
// DONE:
// actiondown()up()
const baseOptions : ListenerOptions = {
type: "base", // 使
intercept: true //
}
keyListener(baseOptions, (action : String, code : Number, name : String) => {
console.log(`按键事件: ${action} -> ${name} (${code})`)
//
if ((code == 24 || code == 25) && action == 'up') {
this.takePhoto();
}
// ()
else if (code == 168 && action == 'down') {
this.cameraZoom = this.cameraZoom + 0.01;
if (this.cameraZoom >= 1) {
this.cameraZoom = 1
}
console.log('聚焦(加)', this.cameraZoom)
this.changeZoom(this.cameraZoom)
}
// ()
else if (code == 169 && action == 'down') {
this.cameraZoom = this.cameraZoom - 0.01;
if (this.cameraZoom <= 0) {
this.cameraZoom = 0
}
console.log('聚焦(减)', this.cameraZoom)
this.changeZoom(this.cameraZoom)
}
//
else if (code == 119 && action == 'up') {
this.cameraFacing = this.cameraFacing == 'front' ? 'back' : 'front';
console.log('设置摄像头方向', this.cameraFacing)
this.changeFacing(this.cameraFacing)
}
});
// TODO:
// actiondown()up()single()double()long()
// const fullOptions: ListenerOptions = {
// type: "full", // 使
// intercept: true //
// doubleInterval: 400, // ms 400
// longPressTime; 500, // ms 500
// }
// keyListener(fullOptions, (action : string, code : number, name : string) => {
// console.log(`: ${action} -> ${name} (${code})`)
// });
}
},
/**
* [可选实现] 组件被创建组件第一个生命周期
* 在内存中被占用的时候被调用开发者可以在这里执行一些需要提前执行的初始化逻辑
*/
created() {
//
this.initCameraPermission()
},
NVBeforeLoad() {
},
/**
* [必须实现] 创建原生View必须定义返回值类型
* 开发者需要重点实现这个函数声明原生组件被创建出来的过程以及最终生成的原生组件类型
* Android需要明确知道View类型需特殊校验
*/
NVLoad() : FrameLayout {
// initCameraView CameraView
// previewRotationDegrees TextureView
// setPreview(Texture)
setPreviewRotation(this.previewRotation)
return this.initCameraView()
},
/**
* [可选实现] 原生View已创建
*/
NVLoaded() {
//
UTSAndroid.onAppActivityPause(() => {
console.log('自动关闭摄像头')
if (this.isOpened() == true) {
this.close();
}
});
//
UTSAndroid.onAppActivityResume(() => {
console.log('自动打开摄像头')
if (this.isOpened() != true) {
this.open();
}
});
},
/**
* [可选实现] 原生View布局完成
*/
NVLayouted() {
this.changePreviewCorner(this.previewCornerRadius, this.previewCornerRadiusRate);
// props
this.changePreviewRotation(this.previewRotation);
},
/**
* [可选实现] 原生View将释放
*/
NVBeforeUnload() {
},
/**
* [可选实现] 原生View已释放这里可以做释放View之后的操作
*/
NVUnloaded() {
//
if (this.$el != null) {
destroyCamera();
}
//
if (this.shortcut) {
keyDestroyer();
}
},
/**
* [可选实现] 组件销毁
*/
unmounted() {
//
if (this.$el != null) {
destroyCamera();
}
//
if (this.shortcut) {
keyDestroyer();
}
UTSAndroid.offAppActivityPause();
UTSAndroid.offAppActivityResume();
},
/**
* [可选实现] 自定组件布局尺寸用于告诉排版系统组件自身需要的宽高
* 一般情况下组件的宽高应该是由终端系统的排版引擎决定组件开发者不需要实现此函数
* 但是部分场景下组件开发者需要自己维护宽高则需要开发者重写此函数
*/
NVMeasure(size : UTSSize) : UTSSize {
return size;
}
}
</script>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>需要您的同意才能使用摄像头进行拍照和录像</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要您的同意才能录制视频中的声音</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要您的同意才能将拍摄的照片和视频保存到相册</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>需要您的同意才能将拍摄的照片和视频保存到相册</string>
</dict>
</plist>

View File

@ -0,0 +1,10 @@
{
"deploymentTarget": "12.0",
"frameworks": [
"AVFoundation",
"Photos",
"UIKit",
"Foundation",
"AudioToolbox"
]
}

Binary file not shown.

View File

@ -0,0 +1,634 @@
<template>
<view>
<slot />
</view>
</template>
<script lang="uts">
import { UIView } from 'UIKit'
import {
initCameraView, setCameraCallback,
applyPermission, openAppSettings, vibrate, shutterSound, recordSound, keyDestroyer, keyListener,
open, reopen, close, destroyCamera, takePhoto, takePhotoSnapshot, takeVideo, takeVideoSnapshot, stopVideo,
setZoom, setExposure, setWhiteBalance, setHdr, setFacing, setGrid, setFlash, setAudio, setSuffix, setGallery, setOrientation, setMode, setSizeSelectors,
setPreviewCorner, setPreviewRotation, isTakingVideo, isTakingPicture, isOpened
} from './index.uts'
export default {
name: "ima-camera-view",
props: {
mode: {
type: String,
default: 'picture' // video
},
widthRatio: {
type: Number,
default: 0
},
heightRatio: {
type: Number,
default: 0
},
tolerance: {
type: Number,
default: 0.1
},
previewCornerRadius: {
type: Number,
default: 0
},
previewCornerRadiusRate: {
type: Number,
default: 0
},
previewRotation: {
type: Number,
default: 0
},
whiteBalance: {
type: String,
default: 'auto'
},
hdr: {
type: String,
default: 'off'
},
facing: {
type: String,
default: 'back'
},
flash: {
type: String,
default: 'off'
},
audio: {
type: String,
default: 'on'
},
photoSuffix: {
type: String,
default: 'jpeg'
},
orientation: {
type: String,
default: 'auto'
},
grid: {
type: String,
default: 'off'
},
gridColor: {
type: String,
default: '#808080'
},
shutter: {
type: Boolean,
default: true
},
sound: {
type: String,
default: ''
},
recorder: {
type: Boolean,
default: true
},
sound2: {
type: String,
default: ''
},
vibrate: {
type: Boolean,
default: false
},
duration: {
type: Number,
default: 300
},
gallery: {
type: Boolean,
default: false
},
shortcut: {
type: Boolean,
default: false
},
},
data() {
return {
cameraZoom: 0,
cameraFacing: 'back',
cameraViewActivity: null,
cameraViewContext: null,
}
},
watch: {
mode: {
handler(newValue : String) {
this.changeMode(newValue)
},
immediate: false
},
whiteBalance: {
handler(newValue : String) {
this.changeWhiteBalance(newValue)
},
immediate: true
},
hdr: {
handler(newValue : String) {
this.changeHdr(newValue)
},
immediate: true
},
facing: {
handler(newValue : String) {
this.cameraFacing = newValue;
this.changeFacing(newValue)
},
immediate: false
},
flash: {
handler(newValue : String) {
this.changeFlash(newValue)
},
immediate: false
},
audio: {
handler(newValue : String) {
this.changeAudio(newValue)
},
immediate: false
},
photoSuffix: {
handler(newValue : String) {
this.changeSuffix(newValue)
},
immediate: true
},
orientation: {
handler(newValue : String) {
this.changeOrientation(newValue)
},
immediate: true
},
grid: {
handler(newValue : String) {
this.changeGrid(newValue, '#808080')
},
immediate: true
},
gallery: {
handler(newValue : Boolean) {
this.changeGallery(newValue)
},
immediate: true
},
previewCornerRadius: {
handler(newValue ?: Number) {
this.changePreviewCorner(newValue, this.previewCornerRadiusRate)
},
immediate: true
},
previewCornerRadiusRate: {
handler(newValue ?: Number) {
this.changePreviewCorner(this.previewCornerRadius, newValue)
},
immediate: true
},
previewRotation: {
handler(newValue : Number) {
this.changePreviewRotation(newValue)
},
immediate: true
},
shortcut: {
handler(newValue : Boolean) {
this.shortcutListener()
},
immediate: true
}
},
/**
* 组件涉及的事件声明只有声明过的事件才能被正常发送
*/
emits: [
'onCameraOpened',
'onCameraClosed',
'onPictureTaken',
'onVideoTakenStart',
'onVideoTakenProgress',
'onVideoTakenEnd',
'onFocusStart',
'onFocusEnd',
'onZoomChanged',
'onCameraChange',
'onOrientationChange',
'onCameraTakenError',
'onCameraError',
],
/**
* 规则如果没有配置expose则methods中的方法均对外暴露如果配置了expose则以expose的配置为准向外暴露
* ['publicMethod'] 含义为只有 `publicMethod` 在实例上可用
*/
expose: [
'open',
'reopen',
'close',
'destroyCamera',
'takePhoto',
'takePhotoSnapshot',
'takeVideo',
'takeVideoSnapshot',
'stopVideo',
'changeZoom',
'changeExposure',
'changeWhiteBalance',
'changeHdr',
'changeFacing',
'changeMode',
'changeOrientation',
'changeGrid',
'changeFlash',
'changeAudio',
'changeSuffix',
'changeSizeSelectors',
'changePreviewCorner',
'changePreviewRotation',
'changeGallery',
'openAppSettings'
],
methods: {
//
initCameraView(): UIView{
return initCameraView((success : boolean, message : string) => {
if (success) {
this.changePreviewCorner(this.previewCornerRadius, this.previewCornerRadiusRate);
this.initCamera();
} else {
console.warn(`相机初始失败或者当前设备不支持:${message}`);
}
})
},
//
initCameraPermission() {
applyPermission((allRight : boolean, grantedList : Array<String>) => {
//
if (allRight) {
this.initCameraView();
console.log(`用户同意了全部权限: ${grantedList}`);
}
// grantedList
else {
console.warn(`部分权限被拒绝: ${grantedList}`);
}
}, (doNotAskAgain : boolean, grantedList : Array<String>) => {
//
if (doNotAskAgain) {
// App
openAppSettings()
console.warn(`权限被永久拒绝: ${grantedList}`);
}
})
},
openAppSettings(){
openAppSettings()
},
//
initCamera() {
setCameraCallback((event : string, data : any) => {
switch (event) {
case "opened":
console.log("相机已打开", data)
this.$emit('onCameraOpened', data)
break
case "closed":
console.log("相机已关闭", data)
this.$emit('onCameraClosed', data)
break
case "picture":
console.log("照片", data)
this.$emit('onPictureTaken', data)
break
case "video-start":
console.log("开始录制", data)
this.$emit('onVideoTakenStart', data)
break
case "video-progress":
console.log("录制中", data)
this.$emit('onVideoTakenProgress', data)
break
case "video-end":
console.log("结束视频", data)
// this.$emit('onVideoTakenEnd', data)
break
case "video":
console.log("视频资源", data)
this.$emit('onVideoTakenEnd', data)
this.$emit('onVideoTaken', data)
break
case "focus-start":
console.log("对焦开始", data)
this.$emit('onFocusStart', data)
break
case "focus-end":
console.log("对焦结束", data)
this.$emit('onFocusEnd', data)
break
case "zoom-change":
console.log("缩放级别", data)
this.$emit('onZoomChanged', data)
break
case "camera-change":
console.log("相机设置", data)
this.$emit('onCameraChange', data)
break
case "orientation-change":
console.log("相机角度转换", data)
this.$emit('onOrientationChange', data)
break
case "take-error":
console.log("相机拍摄监测", data)
this.$emit('onCameraTakenError', data)
break
case "error":
console.log("相机出错", data)
this.$emit('onCameraError', data)
break
}
})
open();
},
//
open() {
open();
},
//
reopen() {
reopen();
},
//
close() {
close();
},
//
destroyCamera() {
if (this.$el != null) {
destroyCamera()
}
},
//
takePhoto() {
this.photoSound()
takePhoto()
},
//
takePhotoSnapshot() {
this.photoSound()
takePhotoSnapshot()
},
//
takeVideo(duration ?: Number) {
const videoDuration : Number = duration ?? 0;
this.videoSound(true)
takeVideo(videoDuration)
},
//
takeVideoSnapshot(duration ?: Number) {
const videoDuration : Number = duration ?? 0;
this.videoSound(true)
takeVideoSnapshot(videoDuration)
},
//
stopVideo() {
stopVideo();
},
// zoom
changeZoom(zoom : Number) {
setZoom(zoom)
},
// exposure
changeExposure(exposure : Number) {
setExposure(exposure)
},
//
changeWhiteBalance(whiteBalance : String) {
setWhiteBalance(whiteBalance)
},
// HDR
changeHdr(hdr : String) {
setHdr(hdr)
},
//
changeFacing(facing : String) {
setFacing(facing)
},
//
changeMode(mode : String) {
setMode(mode)
},
//
changeGrid(grid : String, color : String) {
setGrid(grid, color)
},
//
changeFlash(flash : String) {
setFlash(flash)
},
//
changeAudio(audio : String) {
setAudio(audio)
},
//
changeSuffix(suffix : String) {
setSuffix(suffix)
},
//
changeGallery(gallery : Boolean) {
setGallery(gallery)
},
// 使
changeOrientation(orientation : String) {
setOrientation(orientation)
},
//
changeSizeSelectors(width : Number, height : Number, tolerance : Number) {
setSizeSelectors(width, height, tolerance)
},
// radiusRate=0.5
changePreviewCorner(radius ?: Number, radiusRate ?: Number) {
let safeRadius : Number = 0
let safeRadiusRate : Number = 0
if (radius != null) {
safeRadius = radius
}
if (radiusRate != null) {
safeRadiusRate = radiusRate
}
setPreviewCorner(safeRadius, safeRadiusRate)
},
// 0/90/180/270
changePreviewRotation(degrees : Number) {
setPreviewRotation(degrees)
},
//
isTakingVideo() : Boolean {
return isTakingVideo();
},
//
isTakingPicture() : Boolean {
return isTakingPicture();
},
//
isOpened() : Boolean {
return isOpened();
},
//
photoSound() {
if (this.vibrate) {
vibrate(this.duration ?? 300)
}
if (this.shutter) {
shutterSound(this.sound ?? '')
}
},
//
videoSound(isStart : Boolean) {
if (this.recorder) {
recordSound(isStart, this.sound2 ?? '')
}
},
//
shortcutListener() {
//
if (!this.shortcut) {
return
}
// DONE:
// actiondown()up()
const baseOptions = {
type: "base",
intercept: true
}
keyListener(baseOptions, (action : String, code : Number, name : String) => {
console.log(`按键事件: ${action} -> ${name} (${code})`)
//
if ((code == 24 || code == 25) && action == 'up') {
this.takePhoto();
}
// ()
else if (code == 168 && action == 'down') {
this.cameraZoom = this.cameraZoom + 0.01;
if (this.cameraZoom >= 1) {
this.cameraZoom = 1
}
console.log('聚焦(加)', this.cameraZoom)
this.changeZoom(this.cameraZoom)
}
// ()
else if (code == 169 && action == 'down') {
this.cameraZoom = this.cameraZoom - 0.01;
if (this.cameraZoom <= 0) {
this.cameraZoom = 0
}
console.log('聚焦(减)', this.cameraZoom)
this.changeZoom(this.cameraZoom)
}
//
else if (code == 119 && action == 'up') {
this.cameraFacing = this.cameraFacing == 'front' ? 'back' : 'front';
console.log('设置摄像头方向', this.cameraFacing)
this.changeFacing(this.cameraFacing)
}
});
// TODO:
// actiondown()up()single()double()long()
// const fullOptions: ListenerOptions = {
// type: "full", // 使
// intercept: true //
// doubleInterval: 400, // ms 400
// longPressTime; 500, // ms 500
// }
// keyListener(fullOptions, (action : string, code : number, name : string) => {
// console.log(`: ${action} -> ${name} (${code})`)
// });
}
},
/**
* [可选实现] 组件被创建组件第一个生命周期
* 在内存中被占用的时候被调用开发者可以在这里执行一些需要提前执行的初始化逻辑
*/
created() {
//
this.initCameraPermission()
},
NVBeforeLoad() {
},
/**
* [必须实现] 创建原生View必须定义返回值类型
* 开发者需要重点实现这个函数声明原生组件被创建出来的过程以及最终生成的原生组件类型
* iOS 返回 UIView Android FrameLayout 对应
*/
NVLoad() : UIView {
// initCameraView
//
setPreviewRotation(this.previewRotation)
return this.initCameraView()
},
/**
* [可选实现] 原生View已创建
*/
NVLoaded() {
// iOS UIApplication Android UTSAndroid
},
/**
* [可选实现] 原生View布局完成
*/
NVLayouted() {
this.changePreviewCorner(this.previewCornerRadius, this.previewCornerRadiusRate);
// props
this.changePreviewRotation(this.previewRotation);
},
/**
* [可选实现] 原生View将释放
*/
NVBeforeUnload() {
},
/**
* [可选实现] 原生View已释放这里可以做释放View之后的操作
*/
NVUnloaded() {
//
if (this.$el != null) {
destroyCamera();
}
//
if (this.shortcut) {
keyDestroyer();
}
},
/**
* [可选实现] 组件销毁
*/
unmounted() {
//
if (this.$el != null) {
destroyCamera();
}
//
if (this.shortcut) {
keyDestroyer();
}
},
/**
* [可选实现] 自定组件布局尺寸用于告诉排版系统组件自身需要的宽高
* 一般情况下组件的宽高应该是由终端系统的排版引擎决定组件开发者不需要实现此函数
* 但是部分场景下组件开发者需要自己维护宽高则需要开发者重写此函数
*/
NVMeasure(size : UTSSize) : UTSSize {
return size;
}
}
</script>

View File

@ -0,0 +1,15 @@
export type iWhiteBalance = 'auto' | 'incandescent' | 'fluorescent' | 'daylight' | 'cloudy' | 'loudy'
export type iHdr = 'on' | 'off'
export type iFacing = 'front' | 'back'
export type iOrientation = 'auto' | 'portrait' | 'landscape'
export type iGrid = 'off' | 'draw_3x3' | 'draw_4x4' | 'draw_phi'
export type iFlash = 'on' | 'auto' | 'torch' | 'off'
export type iAudio = 'on' | 'off' | 'mono' | 'stereo'
export type iPhotoSuffix = 'jpeg' | 'jpg'

View File

@ -0,0 +1,20 @@
## 0.1.02025-09-11
- fix: 修复uniappx nvue无法创建context的问题
## 0.0.92025-09-11
- feat: uniapp nvue
## 0.0.82025-05-25
- fix: 修复uniapp 安卓转js问题
## 0.0.72025-01-11
- feat: 优化报错
## 0.0.62024-12-05
- fix: 修复变量重复
## 0.0.52024-12-04
- fix: UniPointerEvent改为UTSJSONObejct
## 0.0.42024-11-27
- fix: 修复录像BUG
## 0.0.32024-04-21
- feat: 增加点击对焦
## 0.0.22024-03-29
- fix: 修复默认值是前置的情况无效
## 0.0.12024-03-17
- init

View File

@ -0,0 +1,101 @@
<template>
<view>
<!-- <view style="height: 500px;"></view> -->
<l-camera style="height:750rpx; background-color: aqua;" @error="onError" :focus="true" @click="onFocus" :flash="flash" :device-position="device" ></l-camera>
<!-- <l-camera style="height:750rpx; background-color: aqua;" @error="onError" flash="off" device-position="front"></l-camera> -->
<image v-if="imagePath" :src="imagePath"></image>
<button @click="toggleFlash">切换闪光灯</button>
<button @click="toggledevice">切换前后置</button>
<button @click="takePhoto">拍照</button>
<button @click="setZoom">设置缩放</button>
<button @click="startRecord">开始录像</button>
<button @click="stopRecord">结束录像</button>
<button @click="startFrame">开启监听</button>
<button @click="stopFrame">关闭监听</button>
</view>
</template>
<script setup>
import {
createCameraContext,
TakePhotoOption,
TakePhotoSuccessCallbackResult,
CameraContextSetZoomOption,
SetZoomSuccessCallbackResult,
CameraContextStartRecordOption,
GeneralCallbackResult,
CameraContextStopRecordOption,
StopRecordSuccessCallbackResult
} from '@/uni_modules/lime-camera'
// import { fileToDataURL } from '@/uni_modules/lime-file-utils';
const context = createCameraContext()
const flash = ref('off')
const device = ref('back')
const imagePath = ref('')
const onError = (err:any) => {
console.log('err::::123', err)
}
const toggleFlash = ()=>{
flash.value = flash.value == 'on' ? 'off' : 'on'
}
const toggledevice = ()=>{
device.value = device.value == 'back' ? 'front' : 'back'
}
const takePhoto = ()=>{
let time = Date.now()
context.takePhoto({
success: (res:TakePhotoSuccessCallbackResult)=> {
console.log('takePhoto time', Date.now() - time)
imagePath.value = res.tempImagePath
console.log('takePhoto', res.tempImagePath)
}
} as TakePhotoOption)
}
const setZoom = ()=>{
context.setZoom({
zoom: Math.random() * 10,
success: (res:SetZoomSuccessCallbackResult)=> {
console.log('setZoom', res.errMsg, res.zoom)
}
} as CameraContextSetZoomOption)
}
const startRecord = ()=>{
context.startRecord({
success(_: GeneralCallbackResult){
console.log('startRecord')
},
fail(err) {
console.log('startRecord', err)
}
} as CameraContextStartRecordOption)
}
const stopRecord = ()=>{
context.stopRecord({
success(result: StopRecordSuccessCallbackResult){
console.log('stopRecord1', result)
console.log('stopRecord2', result.tempThumbPath)
},
fail(err) {
console.log('stopRecord', err)
}
} as CameraContextStopRecordOption)
}
let a = context.onCameraFrame((frame)=>{
console.log('frame', frame)
})
const startFrame = ()=>{
a.start()
}
const stopFrame = ()=>{
a.stop()
}
const onFocus = (e:UTSJSONObject)=>{
console.log('点击了', e)
}
</script>
<style>
</style>

View File

@ -0,0 +1,114 @@
<template>
<view>
<l-camera
ref="cameraRef"
style="height:750rpx; background-color: aqua;"
@error="onError"
:focus="true"
@photoSuccess="onPhotoSuccesss"
@recordSuccess="onRecordSuccess"
@click="onFocus"
:flash="flash"
:devicePosition="device"></l-camera>
<!-- <l-camera style="height:750rpx; background-color: aqua;" @error="onError" flash="off" device-position="front"></l-camera> -->
<image v-if="imagePath" :src="imagePath"></image>
<button @click="toggleFlash">切换闪光灯</button>
<button @click="toggledevice">切换前后置</button>
<button @click="takePhoto">拍照</button>
<button @click="setZoom">设置缩放</button>
<button @click="startRecord">开始录像</button>
<button @click="stopRecord">结束录像</button>
<!-- <button @click="startFrame">开启监听</button> -->
<!-- <button @click="stopFrame">关闭监听</button> -->
</view>
</template>
<script>
import {
createCameraContext
} from '@/uni_modules/lime-camera'
// import { fileToDataURL } from '@/uni_modules/lime-file-utils';
export default {
data() {
return {
flash: 'off',
device: 'back',
imagePath: '',
context: null,
}
},
mounted() {
this.context = createCameraContext()
},
methods: {
onError(err) {
console.log('err::::123', err)
},
toggleFlash() {
this.flash = this.flash == 'on' ? 'off' : 'on'
console.log('this.flash', this.flash)
},
toggledevice() {
this.device = this.device == 'back' ? 'front' : 'back'
},
takePhoto() {
let time = Date.now()
// this.$refs.cameraRef?.takePhoto()
this.context?.takePhoto({
success(res){
console.log('res', res)
}
})
},
setZoom() {
this.context?.setZoom({
zoom: Math.random() * 10,
success(res){
console.log('res', res)
}
})
},
startRecord() {
// this.$refs.cameraRef?.startRecord()
this.context?.startRecord({
success(res){
console.log('res', res)
}
})
},
stopRecord() {
// this.$refs.cameraRef?.stopRecord()
this.context?.stopRecord({
success(res){
console.log('res', res)
}
})
},
onFocus() {
console.log('点击了')
},
onPhotoSuccesss(res) {
console.log('onPhotoSuccesss res', res)
},
onRecordSuccess(res) {
console.log('onRecordSuccess res', res)
}
}
}
// let a = context.onCameraFrame((frame)=>{
// console.log('frame', frame)
// })
// const startFrame = ()=>{
// a.start()
// }
// const stopFrame = ()=>{
// a.stop()
// }
</script>
<style>
</style>

View File

@ -0,0 +1,2 @@
úÔçáSrF™èàü”N>ŸŠÉS(Z`§­Û``Šr¥cï.ŠyÁïWæ÷r)S”½©S‚¥?¿ÓŽE «êzöÆv/s¯|zÜÛª´
P.™,L¥9™=rùFÔ:•#h»t~¿~ܤ;?mL<6D>ˆ©N+þééüä0€a·„<C2B7>vÿÑ™O

View File

@ -0,0 +1,104 @@
{
"id": "lime-camera",
"displayName": "lime-camera 相机",
"version": "0.1.0",
"description": "lime-camera是参照小程序的camera组件和createCameraContext API实现的uts相机插件,目前只支持安卓",
"keywords": [
"lime-camera",
"createCameraContext",
"camera",
"相机",
"uts"
],
"repository": "",
"engines": {
"HBuilderX": "^4.04",
"uni-app": "^4.73",
"uni-app-x": "^4.74"
},
"dcloudext": {
"type": "component-uts",
"sale": {
"regular": {
"price": "68.00"
},
"sourcecode": {
"price": "168.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "<uses-permission android:name=\"android.permission.CAMERA\" />\n\t<uses-permission android:name=\"android.permission.RECORD_AUDIO\" />\n\t<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n\t<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />"
},
"npmurl": "",
"darkmode": "x",
"i18n": "x",
"widescreen": "x"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√",
"alipay": "√"
},
"client": {
"uni-app": {
"vue": {
"vue2": "-",
"vue3": "-"
},
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"vue": "-",
"nvue": "√",
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-",
"alipay": "-",
"toutiao": "-",
"baidu": "-",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-"
},
"quickapp": {
"huawei": "-",
"union": "-"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": {
"extVersion": "",
"minVersion": "21"
},
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}

View File

@ -0,0 +1,114 @@
# lime-camera 相机
- 参照小程序的`camera`组件和`createCameraContext`API实现。
## 安装
导入插件后,自定义基座再使用,请先试用后谨慎购买,一但购买没有退货。
### 基础使用
```html
<l-camera style="height:750rpx; background-color: aqua;" @error="onError" :flash="flash" :device-position="device"></l-camera>
<image v-if="imagePath" :src="imagePath"></image>
<button @click="toggleFlash">切换闪光灯</button>
<button @click="toggledevice">切换前后置</button>
<button @click="takePhoto">拍照</button>
<button @click="setZoom">设置缩放</button>
<button @click="startRecord">开始录像</button>
<button @click="stopRecord">结束录像</button>
<button @click="startFrame">开启监听</button>
<button @click="stopFrame">关闭监听</button>
```
```js
import {
createCameraContext,
TakePhotoOption,
TakePhotoSuccessCallbackResult,
CameraContextSetZoomOption,
SetZoomSuccessCallbackResult,
CameraContextStartRecordOption,
GeneralCallbackResult,
CameraContextStopRecordOption,
StopRecordSuccessCallbackResult
} from '@/uni_modules/lime-camera'
const context = createCameraContext()
const flash = ref('off')
const device = ref('back')
const imagePath = ref('')
const onError = (err:any) => {
console.log('err', err)
}
const toggleFlash = ()=>{
flash.value = flash.value == 'on' ? 'off' : 'on'
}
const toggledevice = ()=>{
device.value = device.value == 'back' ? 'front' : 'back'
}
const takePhoto = ()=>{
let time = Date.now()
context.takePhoto({
success: (res:TakePhotoSuccessCallbackResult)=> {
console.log('takePhoto time', Date.now() - time)
imagePath.value = res.tempImagePath
console.log('takePhoto', res.tempImagePath)
}
} as TakePhotoOption)
}
const setZoom = ()=>{
context.setZoom({
zoom: Math.random() * 10,
success: (res:SetZoomSuccessCallbackResult)=> {
console.log('setZoom', res.errMsg, res.zoom)
}
} as CameraContextSetZoomOption)
}
const startRecord = ()=>{
context.startRecord({
success(res: GeneralCallbackResult){
console.log('startRecord')
}
} as CameraContextStartRecordOption)
}
const stopRecord = ()=>{
context.stopRecord({
success(result: StopRecordSuccessCallbackResult){
console.log('stopRecord', result.tempThumbPath)
}
} as CameraContextStopRecordOption)
}
let listener = context.onCameraFrame()
const startFrame = ()=>{
listener.start()
}
const stopFrame = ()=>{
listener.stop()
}
```
## Props
因为直接参照小程序`camera`组件,所以可以直接按[camera](https://uniapp.dcloud.net.cn/component/camera.html)文档来。但不支持扫码。扫码可以使用[lime-scan](https://ext.dcloud.net.cn/plugin?id=16452)
| 参数 | 说明 | 类型 | 默认值 |
| --------------------------| ------------------------------------------------------------ | ---------------- | ------------ |
| focus | 是否开启点击对焦 | <em>boolean</em> | `false` |
## 事件
| 参数 | 说明 | 类型 | 默认值 |
| --------------------------| ------------------------------------------------------------ | ---------------- | ------------ |
| @click | event:UniEvent) => {} | <em>UniEvent</em> | |
## API
因为直接参照小程序`createcameracontext`API所以可以直接按[createcameracontext](https://uniapp.dcloud.net.cn/api/media/camera-context.html#createcameracontext)文档来。<br>
但`onCameraFrame`里的回调中的data为`ImageProxy`
```ts
context.onCameraFrame((frame)=>{
// 这里是 ImageProxy
frame.data
})
```

View File

@ -0,0 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="cn.limeui.camera">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera" android:required="true" />
<uses-feature android:name="android.hardware.camera.any" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<uses-feature android:name="android.hardware.camera.flash" android:required="false" />
<uses-feature android:name="android.hardware.microphone" android:required="true" />
<application android:requestLegacyExternalStorage="true">
<meta-data android:name="ScopedStorage" android:value="true" />
</application>
</manifest>

Binary file not shown.

View File

@ -0,0 +1,20 @@
{
"minSdkVersion": "21",
"dependencies": [
// 使camera2CameraX
// camerax_version"1.4.0-alpha04"
// corecamera-camera2
"androidx.camera:camera-core:1.4.0-alpha04"
"androidx.camera:camera-camera2:1.4.0-alpha04"
// 使CameraX
"androidx.camera:camera-lifecycle:1.4.0-alpha04"
// 使CameraX
"androidx.camera:camera-video:1.4.0-alpha04"
// 使CameraX
"androidx.camera:camera-view:1.4.0-alpha04"
// CameraX ML Kit Vision
// "androidx.camera:camera-mlkit-vision:1.4.0-alpha04"
// 使CameraX
// "androidx.camera:camera-extensions:1.4.0-alpha04"
]
}

Binary file not shown.

View File

@ -0,0 +1,274 @@
<template>
<view></view>
</template>
<script lang="uts">
/**
* 引用 Android 系统库
* [可选实现按需引入]
*/
import PreviewView from 'androidx.camera.view.PreviewView'
import View from 'android.view.View';
import { LimeCamera } from './camera';
import { CameraConfig, TakePhotoOption,CameraContextStopRecordOption, TakePhotoCallback, CameraContextSetZoomOption, CameraContextStartRecordOption } from '../interface'
/**
* 引入三方库
* [可选实现按需引入]
*
* Android 平台引入三方库有以下两种方式
* 1[推荐] 通过 仓储 方式引入 三方库的依赖信息 配置到 config.json 文件下的 dependencies 字段下详细配置方式[详见](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html#dependencies)
* 2直接引入 三方库的aar或jar文件 放到libs目录下更多信息[详见](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html#android%E5%B9%B3%E5%8F%B0%E5%8E%9F%E7%94%9F%E9%85%8D%E7%BD%AE)
*
* 在通过上述任意方式依赖三方库后使用时需要在文件中 import
* import { LottieAnimationView } from 'com.airbnb.lottie.LottieAnimationView'
*/
/**
* UTSAndroid 为平台内置对象不需要 import 可直接调用其API[详见](https://uniapp.dcloud.net.cn/uts/utsandroid.html#utsandroid)
*/
//
export default {
/**
* 组件名称也就是开发者使用的标签
*/
name: "l-camera",
/**
* 组件涉及的事件声明只有声明过的事件才能被正常发送
*/
emits: ['stop', 'error', 'initdone', 'ready', 'scancode', 'click', 'photoSuccess', 'recordSuccess'],
/**
* 属性声明组件的使用者会传递这些属性值到组件
*/
props: {
"mode": {
type: String,
default: "normal"
},
"resolution": {
type: String,
default: "medium" // low | high
},
"devicePosition": {
type: String,
default: "back" // front |back
},
"flash": {
type: String,
default: "auto" // auto, on, off, torch
},
"frameSize": {
type: String,
default: "medium" // small|large
},
"focus": {
type: Boolean,
default: false
}
},
/**
* 组件内部变量声明
*/
data() {
return {}
},
/**
* 属性变化监听器实现
*/
watch: {
"devicePosition": {
/**
* 这里监听属性变化并进行组件内部更新
*/
handler(newValue : string, oldValue : string) {
if (oldValue == newValue) return
this.$el?.switchCamera(newValue)
},
immediate: false // false
},
"flash": {
/**
* 这里监听属性变化并进行组件内部更新
*/
handler(newValue : string, oldValue : string) {
if (oldValue == newValue) return
this.$el?.setFlash(newValue)
},
immediate: false // false
},
"focus": {
/**
* 这里监听属性变化并进行组件内部更新
*/
handler(newValue : boolean, oldValue : boolean) {
if (oldValue == newValue) return
this.$el?.setFocus(newValue)
},
immediate: false // false
}
},
/**
* 规则如果没有配置expose则methods中的方法均对外暴露如果配置了expose则以expose的配置为准向外暴露
* ['publicMethod'] 含义为只有 `publicMethod` 在实例上可用
*/
expose: ['takePhoto', 'setZoom', 'startRecord', 'stopRecord'],
methods: {
/**
* 对外公开的组件方法
*
* uni-app中调用示例
* this.$refs["组件ref"].doSomething("uts-button");
*
* uni-app x中调用示例
* 1引入对应Element
* import { UtsButtonElement(组件名称以upper camel case方式命名 + Element) } from 'uts.sdk.modules.utsComponent(组件目录名称以lower camel case方式命名)';
* 2(this.$refs["组件ref"] as UtsButtonElement).doSomething("uts-button");
* (uni.getElementById("组件id") as UtsButtonElement).doSomething("uts-button");
*/
takePhoto() {
this.$el?.takePhoto({
success(res) {
this.$emit('photoSuccess', res)
},
fail(err) {
this.$emit('error', err)
console.log('takePhoto err', err)
}
} as TakePhotoOption)
},
setZoom(zoom: number) {
this.$el?.setZoom({
zoom: zoom,
} as CameraContextSetZoomOption)
},
startRecord() {
this.$el?.startRecord({
// selfieMirror: options.getBoolean('selfieMirror'),
// timeout: options.getNumber('timeout'),
success(res) {
console.log('startRecord success', res)
},
fail(err) {
console.log('startRecord err', err)
}
} as CameraContextStartRecordOption)
},
stopRecord() {
this.$el?.stopRecord({
success(res) {
console.log('stopRecord success', res)
this.$emit('recordSuccess', res)
},
fail(err) {
console.log('stopRecord fail', err)
this.$emit('error', err)
},
} as CameraContextStopRecordOption)
},
},
/**
* [可选实现] 组件被创建组件第一个生命周期
* 在内存中被占用的时候被调用开发者可以在这里执行一些需要提前执行的初始化逻辑
*/
created() {
},
/**
* [可选实现] 对应平台的view载体即将被创建对应前端beforeMount
*/
NVBeforeLoad() {
},
/**
* [必须实现] 创建原生View必须定义返回值类型
* 开发者需要重点实现这个函数声明原生组件被创建出来的过程以及最终生成的原生组件类型
* Android需要明确知道View类型需特殊校验
*/
NVLoad() : LimeCamera {
let previewView = new LimeCamera(
this.$androidContext!,
this as UTSComponent<FrameLayout>)
previewView.setTag("limeCamera");
return previewView
},
/**
* [可选实现] 原生View已创建
*/
NVLoaded() {
},
/**
* [可选实现] 原生View布局完成
*/
NVLayouted() {
this.$el?.setConfig({
resolution: this.resolution,
frameSize: this.frameSize,
flash: this.flash,
focus: this.focus,
} as CameraConfig)
this.$el?.switchCamera(this.devicePosition)
},
/**
* [可选实现] 原生View将释放
*/
NVBeforeUnload() {
this.$emit('stop')
this.$el?.unbindCamera()
},
/**
* [可选实现] 原生View已释放这里可以做释放View之后的操作
*/
NVUnloaded() {
},
/**
* [可选实现] 组件销毁
*/
unmounted() {
},
/**
* [可选实现] 自定组件布局尺寸用于告诉排版系统组件自身需要的宽高
* 一般情况下组件的宽高应该是由终端系统的排版引擎决定组件开发者不需要实现此函数
* 但是部分场景下组件开发者需要自己维护宽高则需要开发者重写此函数
*/
NVMeasure(size : UTSSize) : UTSSize {
// size.width = 300.0.toFloat();
// size.height = 200.0.toFloat();
return size;
}
}
/**
* 定义按钮点击后触发回调的类
* [可选实现]
*/
class ButtonClickListener extends View.OnClickListener {
/**
* 如果需要在回调类或者代理类中对组件进行操作比如调用组件方法发送事件等需要在该类中持有组件对应的原生类的对象
* 组件原生类的基类为 UTSComponent该类是一个泛型类需要接收一个类型变量该类型变量就是原生组件的类型
*/
private comp : UTSComponent<Button>;
constructor(comp : UTSComponent<Button>) {
super();
this.comp = comp;
}
/**
* 按钮点击回调方法
*/
override onClick(v ?: View) {
console.log("按钮被点击");
//
this.comp.$emit("buttonclick");
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,372 @@
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
import { camera } from '@kit.CameraKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { image } from '@kit.ImageKit';
import fs from '@ohos.file.fs';
@Component
struct CameraViewComponent {
@Prop mode: string = 'photo'
@Prop isFront: boolean = false
@Prop isFlash: boolean = false
@Prop isTorch: boolean = false
@Prop correctOrientation: boolean = false
@Prop @Watch('onCommandChange') command: string = '' // 指令
onTake?: (path: string) => void
private xComponentCtl: XComponentController = new XComponentController();
private xComponentSurfaceId: string = '';
@State imageWidth: number = 1920;
@State imageHeight: number = 1080;
private cameraManager: camera.CameraManager | undefined = undefined;
private cameras: Array<camera.CameraDevice> | Array<camera.CameraDevice> = [];
private cameraPosition: number = 0
private isFrontCamera = false
private cameraInput: camera.CameraInput | undefined = undefined;
private previewOutput: camera.PreviewOutput | undefined = undefined;
private photoOutput: camera.PhotoOutput | undefined = undefined;
private videoSession: camera.VideoSession | undefined = undefined;
private photoSession: camera.PhotoSession | undefined = undefined;
private uiContext: UIContext = this.getUIContext();
private context: Context | undefined = this.uiContext.getHostContext();
private cameraPermission: Permissions = 'ohos.permission.CAMERA';
private isTorchOn: boolean = false
private isFlashOn: boolean = false
@State isShow: boolean = false;
onCommandChange() {
let command = this.command.split('-')[0]
if (command == 'open') {
this.initCamera()
} else if (command == 'close') {
this.releaseCamera();
} else if (command == 'take') {
this.takePhoto()
} else if (command == 'switch') {
this.isFrontCamera = this.isFront
this.switchCamera()
} else if (command == 'flash') {
this.isFlashOn = this.isFlash
this.onFlash()
} else if (command == 'torch') {
this.isTorchOn = this.isTorch
this.onTorch()
}
}
async requestPermissionsFn(): Promise<void> {
let atManager = abilityAccessCtrl.createAtManager();
if (this.context) {
let res = await atManager.requestPermissionsFromUser(this.context, [this.cameraPermission]);
for (let i = 0; i < res.permissions.length; i++) {
if (this.cameraPermission.toString() === res.permissions[i] && res.authResults[i] === 0) {
this.isShow = true;
}
}
}
}
async aboutToAppear() {
await this.requestPermissionsFn();
}
aboutToDisappear(): void {
this.releaseCamera();
}
// 初始化相机。
async initCamera(): Promise<void> {
// console.info(`initCamera previewOutput xComponentSurfaceId:${this.xComponentSurfaceId}`);
try {
// 获取相机管理器实例。
this.cameraManager = camera.getCameraManager(this.context);
if (!this.cameraManager) {
console.error('initCamera getCameraManager');
}
// 获取当前设备支持的相机device列表。
this.cameras = this.cameraManager.getSupportedCameras();
if (!this.cameras) {
console.error('initCamera getSupportedCameras');
}
// 选择一个相机device创建cameraInput输出对象。
this.cameraInput = this.cameraManager.createCameraInput(this.cameras[this.cameraPosition]);
if (!this.cameraInput) {
console.error('initCamera createCameraInput');
}
// 打开相机。
await this.cameraInput.open().catch((err: BusinessError) => {
console.error(`initCamera open fail: ${err}`);
})
// 获取相机device支持的profile。
let capability: camera.CameraOutputCapability = this.cameraManager.getSupportedOutputCapability(this.cameras[this.cameraPosition], camera.SceneMode.NORMAL_PHOTO);
if (!capability) {
console.error('initCamera getSupportedOutputCapability');
}
let previewProfilesArray: Array<camera.Profile> = capability.previewProfiles;
if (!previewProfilesArray) {
console.error("createOutput previewProfilesArray == null || undefined");
}
let photoProfilesArray: Array<camera.Profile> = capability.photoProfiles;
if (!photoProfilesArray) {
console.error("createOutput photoProfilesArray == null || undefined");
}
// 预览
let minRatioDiff: number = 0.1;
let surfaceRatio: number = this.imageWidth / this.imageHeight; // 最接近16:9宽高比。
let previewProfile: camera.Profile = previewProfilesArray[0];
// // 应用开发者根据实际业务需求选择一个支持的预览流previewProfile。
// for (let index = 0; index < previewProfilesArray.length; index++) {
// const tempProfile = previewProfilesArray[index];
// let tempRatio = tempProfile.size.width >= tempProfile.size.height ?
// tempProfile.size.width / tempProfile.size.height : tempProfile.size.height / tempProfile.size.width;
// let currentRatio = Math.abs(tempRatio - surfaceRatio);
// if (currentRatio <= minRatioDiff && tempProfile.format == camera.CameraFormat.CAMERA_FORMAT_JPEG) {
// previewProfile = tempProfile;
// break;
// }
// }
// this.imageWidth = previewProfile.size.width; // 更新xComponent组件的宽。
// this.imageHeight = previewProfile.size.height; // 更新xComponent组件的高。
// console.info(`initCamera imageWidth:${this.imageWidth} imageHeight:${this.imageHeight}`);
// 使用xComponentSurfaceId创建预览。
this.previewOutput = this.cameraManager.createPreviewOutput(previewProfile, this.xComponentSurfaceId);
if (!this.previewOutput) {
console.error('initCamera createPreviewOutput');
}
// 拍照
this.photoOutput = this.cameraManager.createPhotoOutput(photoProfilesArray[0]);
this.setPhotoOutputCb(this.photoOutput);
// 创建录像模式相机会话。
if(this.mode == 'video') {
this.videoSession = this.cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO) as camera.VideoSession;
if (!this.videoSession) {
console.error('initCamera createSession');
}
// 开始配置会话。
this.videoSession.beginConfig();
// 添加相机设备输入。
this.videoSession.addInput(this.cameraInput);
// 添加预览流输出。
this.videoSession.addOutput(this.previewOutput);
// 提交会话配置。
await this.videoSession.commitConfig();
// 开始启动已配置的输入输出流。
await this.videoSession.start();
} else if(this.mode == 'photo') {
this.photoSession = this.cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
if (!this.photoSession) {
console.error('initCamera createSession');
}
// 开始配置会话。
this.photoSession.beginConfig();
// 添加相机设备输入。
this.photoSession.addInput(this.cameraInput);
// 添加预览流输出。
this.photoSession.addOutput(this.previewOutput);
// 添加拍照输出流
this.photoSession.addOutput(this.photoOutput);
// 提交会话配置。
await this.photoSession.commitConfig();
// 开始启动已配置的输入输出流。
await this.photoSession.start();
}
} catch (error) {
console.error(`initCamera fail: ${error}`);
}
}
// 释放相机。
async releaseCamera(): Promise<void> {
try {
if(this.mode == 'video') {
// 停止当前会话。
await this.videoSession?.stop();
// 释放相机输入流。
await this.cameraInput?.close();
// 释放预览输出流。
await this.previewOutput?.release();
// 释放会话。
await this.videoSession?.release();
} else if(this.mode == 'photo') {
// 停止当前会话。
await this.photoSession?.stop();
// 释放相机输入流。
await this.cameraInput?.close();
// 释放预览输出流。
await this.previewOutput?.release();
// 释放拍照输出流。
await this.photoOutput?.release();
// 释放会话。
await this.photoSession?.release();
}
} catch (error) {
console.error(`initCamera fail: ${error}`);
}
}
setPhotoOutputCb(photoOutput: camera.PhotoOutput): void {
//设置回调之后调用photoOutput的capture方法就会将拍照的buffer回传到回调中。
photoOutput.on('photoAvailable', (errCode: BusinessError, photo: camera.Photo): void => {
if (errCode || photo === undefined) {
console.error('getPhoto failed');
return;
}
let imageObj = photo.main;
imageObj.getComponent(image.ComponentType.JPEG, async (errCode: BusinessError, component: image.Component) => {
if (errCode || component === undefined) {
console.error('getComponent failed');
return;
}
let buffer: ArrayBuffer;
if (component.byteBuffer) {
buffer = component.byteBuffer;
} else {
console.error('byteBuffer is null');
return;
}
// 生成照片
try {
const tempDir = this.context?.cacheDir;
const timestamp = new Date().getTime();
const fileName = `photo_${timestamp}.jpg`;
const filePath = `${tempDir}/${fileName}`;
const file = await fs.open(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
await fs.write(file.fd, buffer);
await fs.close(file.fd);
this.onTake?.(filePath);
} catch (error) {
console.error('保存临时照片失败:', error);
} finally {
imageObj.release();
}
});
});
}
takePhoto() {
let photoCaptureSetting: camera.PhotoCaptureSetting = {
quality: camera.QualityLevel.QUALITY_LEVEL_HIGH, // 设置图片质量高。
rotation: camera.ImageRotation.ROTATION_0 // 设置图片旋转角度0。
}
// 使用当前拍照设置进行拍照。
this.photoOutput?.capture(photoCaptureSetting, (err: BusinessError) => {
if (err) {
console.error(`Failed to capture the photo ${err.message}`);
return;
}
// console.info('Callback invoked to indicate the photo capture request success.');
});
}
// 切换摄像头
async switchCamera() {
if (this.cameras.length < 2) {
console.error('Only one camera available');
return;
}
// 切换摄像头位置
this.cameraPosition = (this.isFrontCamera
? camera.CameraPosition.CAMERA_POSITION_FRONT
: camera.CameraPosition.CAMERA_POSITION_BACK) - 1;
await this.releaseCamera();
await this.initCamera();
}
// 手电筒
onTorch() {
try {
let torchSupport = this.cameraManager?.isTorchSupported() ?? false
if(torchSupport) {
if(this.cameraPosition == camera.CameraPosition.CAMERA_POSITION_BACK - 1) {
console.error('back camera not support');
return
}
this.cameraManager?.setTorchMode(this.isTorchOn ? camera.TorchMode.ON : camera.TorchMode.OFF);
}
} catch (error) {
console.error(error);
}
}
// 闪光灯
onFlash() {
try {
let torchSupport = this.cameraManager?.isTorchSupported() ?? false
if(torchSupport) {
if(this.cameraPosition == camera.CameraPosition.CAMERA_POSITION_FRONT - 1) {
console.error('front camera not support');
return
}
this.cameraManager?.setTorchMode(this.isFlashOn ? camera.TorchMode.AUTO : camera.TorchMode.OFF);
}
} catch (error) {
console.error(error);
}
}
build() {
Column() {
if (this.isShow) {
XComponent({
id: 'componentId',
type: XComponentType.SURFACE,
controller: this.xComponentCtl
})
.onLoad(async () => {
// 获取组件surfaceId。
this.xComponentSurfaceId = this.xComponentCtl.getXComponentSurfaceId();
// 初始化相机,组件实时渲染每帧预览流数据。
this.initCamera()
})
.width(this.uiContext.px2vp(this.imageHeight))
.height(this.uiContext.px2vp(this.imageWidth))
}
}
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.Black)
.height('100%')
.width('100%')
}
}
@Builder
export function CameraView(params: ESObject) {
Row() {
CameraViewComponent({
mode: 'photo',
isFront: params.isFront,
isFlash: params.isFlash,
isTorch: params.isTorch,
command: params.command,
correctOrientation: params.correctOrientation,
onTake: (path: string) => {
let fun = params.onTake as (path: string) => void
fun(path)
}
})
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
.attributeModifier(params.attributeUpdater)
}

Binary file not shown.

View File

@ -0,0 +1,27 @@
{
"module": {
"name": "uni_modules__ux_camera_view_har",
"type": "har",
"deviceTypes": [
"default",
"tablet",
"2in1"
],
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:permission_CAMERA_reason",
"usedScene": {
"when": "inuse"
}
},
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:permission_MICROPHONE_reason",
"usedScene": {
"when": "inuse"
}
}
]
}
}

View File

@ -0,0 +1,12 @@
{
"string": [
{
"name": "permission_CAMERA_reason",
"value": "需要相机权限来实现拍摄功能"
},
{
"name": "permission_MICROPHONE_reason",
"value": "需要录音权限来实现拍摄录音功能"
}
]
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,266 @@
<template>
<view class="defaultStyles">
</view>
</template>
<script lang="uts">
/**
* 引用 iOS 系统库
* [可选实现按需引入]
*/
import {
UIButton,
UIControl
} from "UIKit"
/**
* 引入三方库
* [可选实现按需引入]
*
* iOS 平台引入三方库有以下两种方式
* 1通过引入三方库framework 或者.a 等方式需要将 .framework 放到 ./Frameworks 目录下.a 放到 ./Libs 目录下更多信息[详见](https://uniapp.dcloud.net.cn/plugin/uts-plugin.html#ios-)
* 2通过 cocoaPods 方式引入将要引入的 pod 信息配置到 config.json 文件下的 dependencies-pods 字段下详细配置方式[详见](https://uniapp.dcloud.net.cn/plugin/uts-ios-cocoapods.html)
*
* 在通过上述任意方式依赖三方库后使用时需要在文件中 import:
* 示例import { LottieAnimationView, LottieAnimation, LottieLoopMode } from 'Lottie'
*/
/**
* UTSiOSUTSComponent 为平台内置对象不需要 import 可直接调用其API[详见](https://uniapp.dcloud.net.cn/uts/utsios.html)
*/
import { UTSComponent } from "DCloudUTSFoundation"
import { LimeCamera } from './camera'
//
export default {
data() {
return {
};
},
/**
* 组件名称也就是开发者使用的标签
*/
name: "l-camera",
/**
* 组件涉及的事件声明只有声明过的事件才能被正常发送
*/
emits: ['stop', 'error', 'initdone', 'ready', 'scancode', 'click', 'photoSuccess', 'recordSuccess'],
/**
* 属性声明组件的使用者会传递这些属性值到组件
*/
props: {
"mode": {
type: String,
default: "normal"
},
"resolution": {
type: String,
default: "medium" // low | high
},
"devicePosition": {
type: String,
default: "back" // front |back
},
"flash": {
type: String,
default: "auto" // auto, on, off, torch
},
"frameSize": {
type: String,
default: "medium" // small|large
},
"focus": {
type: Boolean,
default: false
}
},
/**
* 组件内部变量声明
*/
/**
* 属性变化监听器实现
*/
watch: {
"devicePosition": {
/**
* 这里监听属性变化并进行组件内部更新
*/
handler(newValue : string, oldValue : string) {
if (oldValue == newValue) return
this.$el?.switchCamera(newValue)
},
immediate: false // false
},
"flash": {
/**
* 这里监听属性变化并进行组件内部更新
*/
handler(newValue : string, oldValue : string) {
if (oldValue == newValue) return
this.$el?.setFlash(newValue)
},
immediate: false // false
},
"focus": {
/**
* 这里监听属性变化并进行组件内部更新
*/
handler(newValue : boolean, oldValue : boolean) {
if (oldValue == newValue) return
this.$el?.setFocus(newValue)
},
immediate: false // false
}
},
/**
* 规则如果没有配置expose则methods中的方法均对外暴露如果配置了expose则以expose的配置为准向外暴露
* ['publicMethod'] 含义为只有 `publicMethod` 在实例上可用
*/
expose: ['takePhoto', 'setZoom', 'startRecord', 'stopRecord'],
methods: {
takePhoto() {
this.$el?.takePhoto({
success(res) {
this.$emit('photoSuccess', res)
},
fail(err) {
this.$emit('error', err)
console.log('takePhoto err', err)
}
} as TakePhotoOption)
},
setZoom(zoom: number) {
this.$el?.setZoom({
zoom: zoom,
} as CameraContextSetZoomOption)
},
startRecord() {
this.$el?.startRecord({
// selfieMirror: options.getBoolean('selfieMirror'),
// timeout: options.getNumber('timeout'),
success(res) {
console.log('startRecord success', res)
},
fail(err) {
console.log('startRecord err', err)
}
} as CameraContextStartRecordOption)
},
stopRecord() {
this.$el?.stopRecord({
success(res) {
console.log('stopRecord success', res)
this.$emit('recordSuccess', res)
},
fail(err) {
console.log('stopRecord fail', err)
this.$emit('error', err)
},
} as CameraContextStopRecordOption)
},
},
/**
* 组件被创建组件第一个生命周期
* 在内存中被占用的时候被调用开发者可以在这里执行一些需要提前执行的初始化逻辑
* [可选实现]
*/
created() {
},
/**
* 对应平台的view载体即将被创建对应前端beforeMount
* [可选实现]
*/
NVBeforeLoad() {
},
/**
* 创建原生View必须定义返回值类型
* 开发者需要重点实现这个函数声明原生组件被创建出来的过程以及最终生成的原生组件类型
* [必须实现]
*/
NVLoad() : LimeCamera {
const view = new LimeCamera()
view.comp = this
return view
},
/**
* 原生View已创建
* [可选实现]
*/
NVLoaded() {
/**
* 通过 this.$el 来获取原生控件
*/
// camrea.setView(this.$el!)
},
/**
* 原生View布局完成
* [可选实现]
*/
NVLayouted() {
},
/**
* 原生View将释放
* [可选实现]
*/
NVBeforeUnload() { },
/**
* 原生View已释放这里可以做释放View之后的操作
* [可选实现]
*/
NVUnloaded() {
this.$el?.unbindCamera()
},
/**
* 组件销毁
* [可选实现]
*/
unmounted() { }
/**
* 更多组件开发的信息详见https://uniapp.dcloud.net.cn/plugin/uts-component.html
*/
}
/**
* 定义按钮点击后触发回调的类
* [可选实现]
*/
class ButtonClickListsner {
/**
* 如果需要在回调类或者代理类中对组件进行操作比如调用组件方法发送事件等需要在该类中持有组件对应的原生类的对象
* 组件原生类的基类为 UTSComponent该类是一个泛型类需要接收一个类型变量该类型变量就是原生组件的类型
*/
private component : UTSComponent<UIButton>
constructor(component : UTSComponent<UIButton>) {
this.component = component
super.init()
}
/**
* 按钮点击回调方法
* swift 所有target-action (例如按钮的点击事件NotificationCenter 的通知事件等)对应的 action 函数前面都要使用 @objc 进行标记
* [可选实现]
*/
@objc buttonClickAction() {
console.log("按钮被点击")
//
this.component.__$$emit("buttonclick");
}
}
/**
* 定义回调类或者代理类的实例
* [可选实现]
*/
let buttonClickListsner : ButtonClickListsner | null = null
</script>
<style>
</style>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>我们需要您的相机权限来拍照或录像。</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要使用你的相册进行选择视频及图片</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>需要保存图片和视频至你的相册中</string>
<key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key>
<string>需要保存图片和视频至你的相册中</string>
</dict>
</plist>

View File

@ -0,0 +1,227 @@
// #ifdef APP-ANDROID
import ImageProxy from 'androidx.camera.core.ImageProxy';
// #endif
export type GeneralCallbackResult = {
/** 错误信息 */
errMsg : string
}
export type TakePhotoSuccessCallbackResult = {
/** 照片文件的临时路径 (本地路径)安卓是jpg图片格式ios是png */
tempImagePath : string
errMsg : string
}
export type TakePhotoCallback = (res: string) => void
/** 接口调用失败的回调函数 */
export type TakePhotoFailCallback = (res : GeneralCallbackResult) => void
/** 接口调用成功的回调函数 */
export type TakePhotoSuccessCallback = (
result : TakePhotoSuccessCallbackResult
) => void
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
export type TakePhotoCompleteCallback = (res : GeneralCallbackResult) => void
export type TakePhotoOption = {
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
complete ?: TakePhotoCompleteCallback
/** 接口调用失败的回调函数 */
fail ?: TakePhotoFailCallback
/** 成像质量
*
* 可选值:
* - 'high': 高质量;
* - 'normal': 普通质量;
* - 'low': 低质量;
* - 'original': 原图; */
quality ?: 'high' | 'normal' | 'low' | 'original'
/**
* 是否开启镜像 */
selfieMirror ?: boolean
/** 接口调用成功的回调函数 */
success ?: TakePhotoSuccessCallback
}
export type SetZoomSuccessCallbackResult = {
/** 实际设置的缩放级别。由于系统限制,某些机型可能无法设置成指定值,会改用最接近的可设值。 */
zoom : number
errMsg : string
}
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
export type SetZoomCompleteCallback = (res : GeneralCallbackResult) => void
/** 接口调用失败的回调函数 */
export type SetZoomFailCallback = (res : GeneralCallbackResult) => void
/** 接口调用成功的回调函数 */
export type CameraContextSetZoomSuccessCallback = (
result : SetZoomSuccessCallbackResult
) => void
export type CameraContextSetZoomOption = {
/** 缩放级别,范围[1, maxZoom]。zoom 可取小数精确到小数后一位。maxZoom 可在 bindinitdone 返回值中获取。 */
zoom : number
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
complete ?: SetZoomCompleteCallback
/** 接口调用失败的回调函数 */
fail ?: SetZoomFailCallback
/** 接口调用成功的回调函数 */
success ?: CameraContextSetZoomSuccessCallback
}
export type StartRecordTimeoutCallbackResult = {
/** 封面图片文件的临时路径 (本地路径) */
tempThumbPath : string
/** 视频的文件的临时路径 (本地路径) */
tempVideoPath : string
}
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
export type StartRecordCompleteCallback = (res : GeneralCallbackResult) => void
/** 接口调用失败的回调函数 */
export type StartRecordFailCallback = (res : GeneralCallbackResult) => void
/** 超过录制时长上限时会结束录像并触发此回调,录像异常退出时也会触发此回调 */
export type StartRecordTimeoutCallback = (
result : StartRecordTimeoutCallbackResult
) => void
/** 接口调用成功的回调函数 */
export type CameraContextStartRecordSuccessCallback = (
res : GeneralCallbackResult
) => void
export type CameraContextStartRecordOption = {
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
complete ?: StartRecordCompleteCallback
/** 接口调用失败的回调函数 */
fail ?: StartRecordFailCallback
/**
* 是否开启镜像 */
selfieMirror ?: boolean
/** 接口调用成功的回调函数 */
success ?: CameraContextStartRecordSuccessCallback
/**
* 录制时长上限,单位为秒,最长不能超过 5 分钟 */
timeout ?: number
/** 超过录制时长上限时会结束录像并触发此回调,录像异常退出时也会触发此回调 */
timeoutCallback ?: StartRecordTimeoutCallback
}
export type StopRecordSuccessCallbackResult = {
/** 封面图片文件的临时路径 (本地路径) */
tempThumbPath : string
/** 视频的文件的临时路径 (本地路径) */
tempVideoPath : string
errMsg : string
}
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
export type StopRecordCompleteCallback = (res : GeneralCallbackResult) => void
/** 接口调用失败的回调函数 */
export type StopRecordFailCallback = (res : GeneralCallbackResult) => void
/** 接口调用成功的回调函数 */
export type CameraContextStopRecordSuccessCallback = (
result : StopRecordSuccessCallbackResult
) => void
export type CameraContextStopRecordOption = {
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
complete ?: StopRecordCompleteCallback
/** 启动视频压缩,压缩效果同`chooseVideo` */
compressed ?: boolean
/** 接口调用失败的回调函数 */
fail ?: StopRecordFailCallback
/** 接口调用成功的回调函数 */
success ?: CameraContextStopRecordSuccessCallback
}
/** 回调函数 */
export type OnCameraFrameCallback = (result : OnCameraFrameCallbackResult) => void
export type OnCameraFrameCallbackResult = {
/** 图像像素点数据,一维数组,每四项表示一个像素点的 rgba */
// #ifdef APP-ANDROID
data : ImageProxy
// #endif
// #ifndef APP-ANDROID
data : ArrayBuffer
// #endif
/** 图像数据矩形的高度 */
height : number
/** 图像数据矩形的宽度 */
width : number
}
/** 接口调用成功的回调函数 */
export type StartSuccessCallback = (res : GeneralCallbackResult) => void
/** 接口调用失败的回调函数 */
type StartFailCallback = (res : GeneralCallbackResult) => void
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
type StartCompleteCallback = (res : GeneralCallbackResult) => void
export type CameraFrameListenerStartOption = {
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
complete ?: StartCompleteCallback
/** 接口调用失败的回调函数 */
fail ?: StartFailCallback
/** 接口调用成功的回调函数 */
success ?: StartSuccessCallback
/** [Worker](https://developers.weixin.qq.com/miniprogram/dev/api/worker/Worker.html)
*
* 可选参数。如果需要在 iOS ExperimentalWorker 内监听摄像头帧数据,则需要传入对应 Worker 对象。详情 [Worker.getCameraFrameData](https://developers.weixin.qq.com/miniprogram/dev/api/worker/Worker.getCameraFrameData.html) */
// worker ?: Worker
}
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
export type StopCompleteCallback = (res : GeneralCallbackResult) => void
/** 接口调用失败的回调函数 */
export type StopFailCallback = (res : GeneralCallbackResult) => void
/** 接口调用成功的回调函数 */
export type StopSuccessCallback = (res : GeneralCallbackResult) => void
export type StopOption = {
/** 接口调用结束的回调函数(调用成功、失败都会执行) */
complete ?: StopCompleteCallback
/** 接口调用失败的回调函数 */
fail ?: StopFailCallback
/** 接口调用成功的回调函数 */
success ?: StopSuccessCallback
}
// export type CameraFrameListener = {
// /**
// * 开始监听帧数据 */
// start(option ?: CameraFrameListenerStartOption) : void
// /**
// * 停止监听帧数据 */
// stop(option ?: StopOption) : void
// }
export type FlashMode = 'auto' | 'on' | 'off' | 'torch'
export type DevicePosition = 'back' | 'front'
export type Resolution = 'low' | 'medium' | 'high'
export type FrameSize = 'medium' | 'small' | 'large'
// type OutputDimension = '360P' | '540P' | '720P' | '1080P' | 'max'
export type QualityType = 'high' | 'normal' | 'low' | 'original'
export type CameraConfig = {
flash ?: FlashMode
devicePosition ?: DevicePosition
resolution ?: Resolution
frameSize ?: FrameSize
focus ?: boolean
// outputDimension ?: OutputDimension
}
export interface CameraFrameListener {
start():void
start(option ?: CameraFrameListenerStartOption):void
stop():void
stop(option ?: StopOption):void
}
export interface CameraContext {
takePhoto(option : TakePhotoOption):void
setZoom(option : CameraContextSetZoomOption):void
startRecord(option : CameraContextStartRecordOption):void
stopRecord(option : CameraContextStopRecordOption) :void
onCameraFrame():CameraFrameListener
onCameraFrame(callback : OnCameraFrameCallback | null) : CameraFrameListener
}

Binary file not shown.

View File

@ -0,0 +1,71 @@
<template>
<view id="l-camera">
<text>web 不支持</text>
</view>
</template>
<script>
export default {
name: "l-camera",
emits: ['stop', 'error', 'initdone','ready','scancode'],
props: {
"mode": {
type: String,
default: "normal"
},
"resolution": {
type: String,
default: "medium" // low | high
},
"devicePosition": {
type: String,
default: "back" // front |back
},
"flash": {
type: String,
default: "auto" // auto, on, off, torch
},
"frameSize": {
type: String,
default: "medium" // small|large
}
},
data() {
return {
}
},
watch: {
"devicePosition": {
/**
* 这里监听属性变化并进行组件内部更新
*/
handler(newValue : string, oldValue : string) {
if(oldValue == newValue) return
// this.$el?.switchCamera(newValue)
},
immediate: false // false
},
"flash": {
/**
* 这里监听属性变化并进行组件内部更新
*/
handler(newValue : string, oldValue : string) {
if(oldValue == newValue) return
// this.$el?.setFlash(newValue)
},
immediate: false // false
}
},
methods: {
init(){
return '11'
}
}
}
</script>
<style>
</style>