Skip to content
# t-camera

Camera 相机

t-camera 组件介绍 ​

t-camera 是一个跨平台相机封装组件,统一承接 Web 和原生端的相机实现,支持拍照、录像、切换前后摄像头和闪光灯控制。适合图片采集、扫码辅助、视频录制和本地媒体预览等场景。

t-camera 平台兼容 ​

HarmonyH5AndroidiOS小程序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是否启用点击效果booleanfalse
type组件主题类型,可选 info、primary、error、warning、successstring'primary'
disabled是否禁用booleanfalse
effect组件显示主题,可选 normal、dark、light、plainstring'normal'
size组件尺寸,可选 large、medium、small、ministring'medium'
flash闪光灯模式,可选 auto、on、offstring'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>