# t-camera
Camera 相机
t-camera 组件介绍
t-camera 是一个跨平台相机封装组件,统一承接 Web 和原生端的相机实现,支持拍照、录像、切换前后摄像头和闪光灯控制。适合图片采集、扫码辅助、视频录制和本地媒体预览等场景。
t-camera 平台兼容
| Harmony | H5 | Android | iOS | 小程序 | UTS |
|---|---|---|---|---|---|
| ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
t-camera 基础用法
vue
<template>
<view>
<view class="page-content">
<view class="hero-block">
<text class="hero-title">tui-camera</text>
<text class="hero-desc">通过 ref 调用组件方法控制拍照、录像、缩放等。</text>
</view>
<t-camera ref="cameraRef"
class="demo-camera"
:device-position="devicePosition"
:flash="flash"
:mode="mode"
:resolution="resolution"
:photo-resolution="photoResolution"
@ready="onCameraReady"
@initdone="onCameraInitDone"
@error="onCameraError"
@stop="onCameraStop"
@capture="onCameraCapture"
@scancode="onCameraScanCode">
</t-camera>
<view class="panel-block">
<view class="button-row">
<button class="action-button"
:class="isCameraOpen ? 'warn-button' : 'primary-button'"
@click="handleToggleCamera">{{ isCameraOpen ? '关闭相机' : '打开相机' }}</button>
</view>
</view>
<view class="panel-block">
<text class="panel-title">组件控制</text>
<view class="button-row">
<button class="action-button primary-button"
@click="handleTakePhoto">拍照</button>
<button class="action-button"
:class="isRecording ? 'warn-button' : ''"
@click="handleToggleRecord">{{ isRecording ? (remainText) : '开始录像' }}</button>
</view>
<view class="button-row">
<button class="action-button"
@click="handleSwitchCamera">切换镜头</button>
<button class="action-button"
@click="toggleFlash">切换闪光灯</button>
</view>
<view class="button-row">
<button class="action-button"
:class="mode == 'scanCode' ? 'primary-button' : ''"
@click="toggleMode">{{ mode == 'scanCode' ? '扫码模式' : '普通模式' }}</button>
</view>
</view>
<view class="panel-block">
<text class="panel-title">当前配置</text>
<view class="badge-row">
<text class="info-badge">device-position: {{ devicePosition }}</text>
<text class="info-badge">flash: {{ flash }}</text>
<text class="info-badge">mode: {{ mode }}</text>
<text class="info-badge">resolution: {{ resolution }}</text>
<text class="info-badge">photo-resolution: {{ photoResolution }}</text>
<text class="info-badge">recording: {{ isRecording ? 'yes' : 'no' }}</text>
</view>
<view class="button-row">
<button class="action-button"
@click="cycleResolution">切换预览分辨率</button>
<button class="action-button"
@click="cyclePhotoResolution">切换拍照分辨率</button>
</view>
</view>
<view v-if="scanCodeResult.length > 0 || scanCodeType.length > 0"
class="panel-block scan-result-block">
<text class="panel-title">扫码结果</text>
<text v-if="scanCodeType.length > 0"
class="status-line">类型:{{ scanCodeType }}</text>
<text v-if="scanCodeResult.length > 0"
class="status-line scan-result-text">内容:{{ scanCodeResult }}</text>
</view>
<view class="panel-block">
<text class="panel-title">缩放控制</text>
<text class="status-line">maxZoom: {{ maxZoomText }}</text>
<text class="status-line">currentZoom: {{ currentZoomText }}</text>
<view class="button-row">
<button class="action-button"
@click="zoomOut">缩小</button>
<button class="action-button"
@click="zoomIn">放大</button>
</view>
</view>
<view class="status-card">
<text class="status-label">状态</text>
<text class="status-value">{{ statusText }}</text>
<text class="status-sub">最后事件:{{ lastEventText }}</text>
<text v-if="videoPath.length > 0"
class="status-sub">tempVideoPath:{{ videoPath }}</text>
<text class="status-sub">tempThumbPath:{{ thumbPath.length > 0 ? thumbPath : 'null' }}</text>
</view>
<image v-if="photoPath.length > 0"
class="capture-image"
:src="photoPath"
mode="aspectFill"></image>
<view v-show="videoPath.length > 0"
class="panel-block">
<text class="panel-title">录像回放</text>
<text v-if="videoFileSizeText.length > 0"
class="status-line">{{ videoFileSizeText }}</text>
<text v-if="videoMetaText.length > 0"
class="status-line">{{ videoMetaText }}</text>
<!-- #ifdef APP -->
<text>{{videoPath}}</text>
<video class="capture-video"
:src="videoPath"
:poster="thumbPath.length > 0 ? thumbPath : ''"
controls></video>
<!-- #endif -->
<!-- #ifndef APP -->
<text>{{videoPath}}</text>
<video class="capture-video"
:src="videoPath"
:poster="thumbPath.length > 0 ? thumbPath : ''"
controls></video>
<!-- #endif -->
<button class="action-button primary-button video-save-button"
@click="saveVideoToAlbum">保存到系统相册</button>
</view>
</view>
</view>
</template>
<script setup
lang="uts">
import {
TuiCameraCaptureDetail,
TuiCameraComponentErrorDetail,
TuiCameraDevicePosition,
TuiCameraFlashMode,
TuiCameraInitDoneDetail,
TuiCameraMode,
TuiCameraPhotoResolution,
TuiCameraRecordResult,
TuiCameraReadyDetail,
TuiCameraResolution,
TuiCameraScanCodeDetail,
TuiCameraStopDetail,
TuiCameraTakePhotoOptions,
TuiCameraStartRecordOptions,
TuiCameraStopRecordOptions
} from '@/uni_modules/tui-camera'
const cameraRef = ref<ComponentPublicInstance | null>(null)
const statusText = ref('相机已关闭')
const lastEventText = ref('none')
const photoPath = ref('')
const isCameraOpen = ref(false)
const isRecording = ref(false)
const devicePosition = ref<TuiCameraDevicePosition>('back')
const flash = ref<TuiCameraFlashMode>('off')
const resolution = ref<TuiCameraResolution>('medium')
const photoResolution = ref<TuiCameraPhotoResolution>('high')
const currentZoom = ref(1.0)
const maxZoom = ref(1.0)
const videoPath = ref('')
const thumbPath = ref('')
const videoFileSizeText = ref('')
const videoMetaText = ref('')
const mode = ref<TuiCameraMode>('normal')
const scanCodeResult = ref('')
const scanCodeType = ref('')
function goToScanDemo() {
uni.navigateTo({
url: '/pages/scan-demo/scan-demo'
})
}
function goToCameraTest() {
uni.navigateTo({
url: '/pages/camera-test/camera-test'
})
}
function goToScanTest() {
uni.navigateTo({
url: '/pages/scan-test/scan-test'
})
}
function goToOffscreenCanvasTest() {
uni.navigateTo({
url: '/pages/offscreen-canvas-test/offscreen-canvas-test'
})
}
function applyRecordResult(result : TuiCameraRecordResult | null) {
if (result == null) {
return
}
videoPath.value = result.tempVideoPath == null ? '' : result.tempVideoPath
thumbPath.value = result.tempThumbPath == null ? '' : result.tempThumbPath
videoFileSizeText.value = ''
videoMetaText.value = ''
}
function getSafeCurrentZoom() : number {
if (cameraRef.value == null) {
return 1
}
return cameraRef.value!.$callMethod('getCurrentZoomValue') as number
}
function onCameraReady(detail : TuiCameraReadyDetail | null) {
isCameraOpen.value = true
lastEventText.value = 'ready'
if (detail == null) {
statusText.value = '相机已就绪'
return
}
statusText.value = '相机已就绪:' + detail.devicePosition
currentZoom.value = getSafeCurrentZoom()
}
function onCameraInitDone(detail : TuiCameraInitDoneDetail | null) {
lastEventText.value = 'initdone'
if (detail != null) {
maxZoom.value = detail.maxZoom
currentZoom.value = getSafeCurrentZoom()
statusText.value = '初始化完成,最大缩放:' + detail.maxZoom.toFixed(1)
}
}
function onCameraError(detail : TuiCameraComponentErrorDetail | null) {
isCameraOpen.value = false
lastEventText.value = 'error'
const errorText = detail == null || detail.errMsg == null || detail.errMsg.length == 0 ? '未获取到错误详情' : detail.errMsg
statusText.value = '相机异常:' + errorText
uni.showToast({
title: errorText,
icon: 'none'
})
}
function onCameraStop(detail : TuiCameraStopDetail | null) {
isCameraOpen.value = false
lastEventText.value = 'stop'
isRecording.value = false
statusText.value = '相机已停止'
}
function onCameraCapture(detail : TuiCameraCaptureDetail | null) {
lastEventText.value = 'capture'
if (detail == null) {
statusText.value = '拍照完成,但未获取到图片详情'
return
}
photoPath.value = detail.path
statusText.value = '拍照完成'
}
function onCameraScanCode(detail : TuiCameraScanCodeDetail | null) {
lastEventText.value = 'scancode'
if (detail == null) {
return
}
scanCodeType.value = detail.type == null ? '' : detail.type
scanCodeResult.value = detail.result == null ? '' : detail.result
statusText.value = '扫码识别:' + scanCodeType.value + ' | ' + scanCodeResult.value
}
function toggleMode() {
if (mode.value == 'scanCode') {
mode.value = 'normal'
} else {
mode.value = 'scanCode'
}
}
function handleToggleCamera() : void {
if (cameraRef.value == null) {
return
}
if (isCameraOpen.value) {
cameraRef.value?.$callMethod('close')
isCameraOpen.value = false
statusText.value = '相机已关闭'
lastEventText.value = 'close'
photoPath.value = ''
videoPath.value = ''
thumbPath.value = ''
videoFileSizeText.value = ''
videoMetaText.value = ''
isRecording.value = false
} else {
cameraRef.value?.$callMethod('open')
}
return
}
function handleTakePhoto() : void {
if (cameraRef.value == null || !isCameraOpen.value) {
statusText.value = '相机未打开,请先打开相机'
return
}
const options : TuiCameraTakePhotoOptions = {
quality: 'high',
selfieMirror: false,
success: (result) : void => {
if (result.tempImagePath != null) {
photoPath.value = result.tempImagePath
}
return
},
fail: (error) : void => {
statusText.value = '拍照失败:' + error.errMsg
return
},
complete: (result : any) : void => {
return
}
}
cameraRef.value?.$callMethod('takePhoto', options)
}
function handleToggleRecord() : void {
if (cameraRef.value == null || !isCameraOpen.value) {
statusText.value = '相机未打开,请先打开相机'
return
}
if (!isRecording.value) {
const options : TuiCameraStartRecordOptions = {
selfieMirror: true,
timeoutCallback: (result) : void => {
isRecording.value = false
applyRecordResult(result)
statusText.value = result.tempVideoPath == null ? '录像到达时长上限' : '录像到达时长上限'
lastEventText.value = 'record-timeout'
return
},
success: () : void => {
isRecording.value = true
photoPath.value = ''
videoPath.value = ''
thumbPath.value = ''
videoFileSizeText.value = ''
videoMetaText.value = ''
statusText.value = '开始录像'
lastEventText.value = 'record-start'
return
},
fail: (error) : void => {
isRecording.value = false
statusText.value = '开始录像失败:' + error.errMsg
return
},
complete: (result : any) : void => {
return
}
}
cameraRef.value?.$callMethod('startRecord', options)
return
}
const stopOptions : TuiCameraStopRecordOptions = {
compressed: true,
success: (result) : void => {
isRecording.value = false
applyRecordResult(result)
statusText.value = result.tempVideoPath == null ? '录像已停止' : '录像已停止'
lastEventText.value = 'record-stop'
return
},
fail: (error) : void => {
isRecording.value = false
statusText.value = '停止录像失败:' + error.errMsg
return
},
complete: (result : any) : void => {
return
}
}
cameraRef.value?.$callMethod('stopRecord', stopOptions)
return
}
function handleSwitchCamera() : void {
if (!isCameraOpen.value) {
statusText.value = '相机未打开,请先打开相机'
return
}
const next : TuiCameraDevicePosition = devicePosition.value == 'back' ? 'front' : 'back'
devicePosition.value = next
return
}
function toggleFlash() {
if (flash.value == 'off') {
flash.value = 'torch'
return
}
if (flash.value == 'torch') {
flash.value = 'auto'
return
}
if (flash.value == 'auto') {
flash.value = 'on'
return
}
flash.value = 'off'
}
function cycleResolution() {
if (resolution.value == 'low') {
resolution.value = 'medium'
return
}
if (resolution.value == 'medium') {
resolution.value = 'high'
return
}
resolution.value = 'low'
}
function cyclePhotoResolution() {
if (photoResolution.value == 'low') {
photoResolution.value = 'medium'
return
}
if (photoResolution.value == 'medium') {
photoResolution.value = 'high'
return
}
if (photoResolution.value == 'high') {
photoResolution.value = 'original'
return
}
photoResolution.value = 'low'
}
function setZoom(value : number) : void {
cameraRef.value?.$callMethod('setZoom', value)
currentZoom.value = getSafeCurrentZoom()
return
}
function zoomIn() : void {
const target = getSafeCurrentZoom() + 0.5
setZoom(target)
return
}
function zoomOut() : void {
const target = getSafeCurrentZoom() - 0.5
setZoom(target)
return
}
function saveVideoToAlbum() {
if (videoPath.value.length == 0) {
uni.showToast({
title: '暂无可保存的视频',
icon: 'none'
})
return
}
// #ifdef APP
uni.saveVideoToPhotosAlbum({
filePath: videoPath.value,
success: () => {
statusText.value = '视频已保存到系统相册'
uni.showToast({
title: '保存成功',
icon: 'none'
})
},
fail: (error : IUniError) => {
statusText.value = '保存视频失败:' + error.errMsg
uni.showModal({
title: '保存视频到相册失败',
content: error.errMsg,
showCancel: false
})
}
})
// #endif
// #ifndef APP
uni.showToast({
title: 'Web 端暂不支持保存到相册',
icon: 'none'
})
// #endif
}
const currentZoomText = computed(() : string => currentZoom.value.toFixed(1))
const maxZoomText = computed(() : string => maxZoom.value.toFixed(1))
const remainText = computed(() : string => {
const r : number = (cameraRef.value?.$callMethod('getRemainValue') ?? 0) as number
return r > 0 ? ('停止录像 ' + r.toString() + 's') : '停止录像'
})
</script>
<style>
.page-root {
display: flex;
flex-direction: column;
flex: 1;
background-color: #eef2f5;
}
.page-scroll {
flex: 1;
}
.page-content {
display: flex;
flex-direction: column;
padding: 24rpx;
background-color: #eef2f5;
}
.hero-block {
display: flex;
flex-direction: column;
padding: 24rpx;
border-radius: 20px;
background-color: #ffffff;
}
.hero-title {
font-size: 24px;
font-weight: 700;
color: #1b2430;
}
.hero-desc {
margin-top: 8rpx;
font-size: 14px;
color: #5a6b7b;
}
.demo-camera {
margin-top: 20rpx;
height: 420rpx;
}
.panel-block {
display: flex;
flex-direction: column;
margin-top: 20rpx;
padding: 20rpx;
border-radius: 16px;
background-color: #ffffff;
}
.panel-title {
font-size: 15px;
font-weight: 700;
color: #1b2430;
}
.button-row {
display: flex;
flex-direction: row;
margin-top: 14rpx;
}
.action-button {
flex: 1;
margin-right: 12rpx;
}
.primary-button {
background-color: #1f7aff;
color: #ffffff;
}
.warn-button {
background-color: #dc5b42;
color: #ffffff;
}
.badge-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-top: 12rpx;
}
.info-badge {
margin-right: 10rpx;
margin-bottom: 10rpx;
padding: 8rpx 14rpx;
border-radius: 999px;
background-color: #eef4ff;
font-size: 12px;
color: #35547a;
}
.status-card {
display: flex;
flex-direction: column;
margin-top: 20rpx;
padding: 16rpx;
border-radius: 16px;
background-color: #ffffff;
}
.status-label {
font-size: 12px;
color: #7c8794;
}
.status-value {
margin-top: 6rpx;
font-size: 14px;
color: #1b2430;
}
.status-sub {
margin-top: 6rpx;
font-size: 12px;
color: #6f7c8c;
}
.status-line {
margin-top: 10rpx;
font-size: 13px;
color: #405063;
}
.capture-image {
margin-top: 20rpx;
width: 100%;
height: 420rpx;
border-radius: 20px;
}
.capture-video {
margin-top: 16rpx;
width: 100%;
height: 420rpx;
border-radius: 20px;
background-color: #000000;
}
.video-save-button {
margin-top: 16rpx;
margin-right: 0;
}
.scan-result-block {
background-color: #e8f5e9;
border: 2px solid #4caf50;
}
.scan-result-text {
font-weight: 700;
color: #1b5e20;
}
</style>t-camera Props
| 属性名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
path | 点击组件后跳转的页面路径;为空时仅响应事件 | string | '' |
hover | 是否启用点击效果 | boolean | false |
type | 组件主题类型,可选 info、primary、error、warning、success | string | 'primary' |
disabled | 是否禁用 | boolean | false |
effect | 组件显示主题,可选 normal、dark、light、plain | string | 'normal' |
size | 组件尺寸,可选 large、medium、small、mini | string | 'medium' |
flash | 闪光灯模式,可选 auto、on、off | string | 'auto' |
t-camera Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
take | 拍照完成时触发 | string,图片地址 |
recoder | 录像结束时触发 | string,视频地址或 blob url |
t-camera Slots
当前组件未声明插槽。
Methods
| 方法名 | 说明 |
|---|---|
recoder | 手动转发录像完成事件给外部 |
flashOn | 打开闪光灯 |
flashOff | 关闭闪光灯 |
getPhoto | 手动转发拍照结果给外部 |
front | 切换到前置摄像头 |
back | 切换到后置摄像头 |
open | 打开相机 |
close | 关闭相机 |
start | 开始录像 |
stops | 停止录像 |
take | 执行拍照 |
vue
<script setup lang="ts">
import { ref, ComponentPublicInstance } from 'vue'
const camreaRef = ref<ComponentPublicInstance | null>(null)
const callCameraMethod = (method: string) => {
camreaRef.value?.$callMethod(method)
}
const front = () => callCameraMethod('front')
const back = () => callCameraMethod('back')
const open = () => callCameraMethod('open')
const close = () => callCameraMethod('close')
const take = () => callCameraMethod('take')
const flashOn = () => callCameraMethod('flashOn')
const flashOff = () => callCameraMethod('flashOff')
const start = () => callCameraMethod('start')
const stops = () => callCameraMethod('stops')
</script>