【uniapp】uniapp小程序中实现拍照同时打开闪光灯的功能,拍照闪光灯实现

news2024/11/15 8:38:32

一、需求前提

特殊场景中,需要拍照的同时打开闪光灯,(例如黑暗场景下的设备维护巡检功能)。

起初我是用的uviewui中的u-upload组件自带的拍照功能,但是这个不支持拍照时打开闪光灯,也不支持从通知栏中打开闪光灯。

二、解决方案

采用组合形式解决:

  1. 使用uniapp官方内置组件中的 媒体组件:camera 实现闪光灯拍照,uni.createCameraContext()获取返回图片结果
  2. 结合uniapp官方内置组件中的 视图容器:cover-view 做定制化布局

1. 媒体组件:camera

camera 是页面内嵌的区域相机组件。注意这不是点击后全屏打开的相机。
其中flash属性可以动态实现拍照闪光灯的功能,值为auto, on, off, torch

拍照动作可以使用uni.createCameraContext()获取拍照的图片结果,再做后续操作。

注意

  • camera 组件是由客户端创建的原生组件,它的层级是最高的,不能通过 z-index 控制层级。可使用 cover-view 、cover-image 覆盖在上面。
  • 同一页面只能插入一个 camera 组件。(多次打开自定义的拍照界面可以使用v-if做销毁)

2. 视图容器:cover-view

cover-view是覆盖在原生组件上的文本视图。
app-vue和小程序框架,渲染引擎是webview的。但为了优化体验,部分组件如map、video、textarea、canvas通过原生控件实现,原生组件层级高于前端组件(类似flash层级高于div)。为了能正常覆盖原生组件,设计了cover-view。

注意

  • 容器内的每一个元素最好都用cover-view标签包裹(包括文字内容),否则会出现渲染异常问题。

三、 示例

在这里插入图片描述

<!--
 * @Description: 自定义文件上传组件,支持拍照、闪光灯、本地图片选择
 * @Doc: 双向绑定使用 <customUpload :modelValue.sync="test"></customUpload>
 * @Author: y
 * @Date: 2024-03-07 09:51:25
-->
<template>
	<view class="custom-upload">
		<!-- 预览图片 -->
		<template v-if="previewImage">
			<view class="file-item" v-for="(item,index) in fileList" :key="index" :style="[{width,height}]">
				<view v-if="item.status ==='uploading'" class="file-uploading">
					<u-loading-icon color="#19be6b"></u-loading-icon>
				</view>
				<u--image v-else :showLoading="true" :src="item.thumb || item.url" :width="width" :height="height"
					@tap="onPreviewImage(item)">
					<template v-slot:loading>
						<!-- 此处后期需要优化为本地文件地址,避免走两次加载 -->
						<u-loading-icon text="加载中" textSize="18"></u-loading-icon>
					</template>
				</u--image>

				<!-- 删除按钮角标 -->
				<view class="upload-deletable" @tap.stop="deleteItem(index)">
					<view class="upload-deletable-icon">
						<u-icon name="close" color="#ffffff" size="10"></u-icon>
					</view>
				</view>
				<!-- 文件状态角标 -->
				<view class="upload-success" v-if="item.status === 'success'">
					<view class="upload-success-icon">
						<u-icon name="checkmark" color="#ffffff" size="12"></u-icon>
					</view>
				</view>
			</view>
		</template>

		<!-- 如果图片数量在设定范围内 -->
		<template v-if="isInCount">
			<view class="upload-button" @tap="chooseOperationType" :style="[{width,height}]">
				<u-icon name="plus" size="26" color="#2979ff"></u-icon>
				<text v-if="uploadText" class="upload-button-text">{{ uploadText }}</text>
				<text v-else class="upload-button-text">上传</text>
			</view>
		</template>

		<!-- 选项弹出层 -->
		<u-popup :show="showOptionsPopup" :round="10" mode="bottom" :closeable="true" @close="this.showOptionsPopup=false">
			<view class="option-list">
				<view v-if="showTakePhoto" class="option-btn" @tap="onTakePhoto">拍照</view>
				<view v-if="showChoosePhoto" class="option-btn" @tap="onChoosePhoto">从相册选择</view>
				<view class="option-btn-close" @tap="this.showOptionsPopup=false">取消</view>
			</view>
		</u-popup>

		<!-- 相机弹出层 -->
		<u-overlay v-if="showCameraPopup" :show="showCameraPopup" mask-click-able="false">
			<!-- 添加v-if避免缓存相机,每次打开都需要重新创建 -->
			<view class="camera-container">
				<camera device-position="back" :flash="flashStatus" style="width: 100%; height: calc(100% - 200rpx);">
					<cover-view class="user-location">
						<!-- 此处只可以使用cover-image插入图片(待开发) -->
						<cover-view v-if="!userLocationRefreshing" class="icon-location"></cover-view>
						<cover-view v-else class="icon-location-refreshing"></cover-view>
						<cover-view v-if="userLocationRefreshing" style="color: #ff9900;">
							加载中...
						</cover-view>
						<cover-view>{{userLocation||'---'}}</cover-view>
					</cover-view>
				</camera>
				<view class="camera-option-list">
					<view class="option-btn" @tap.stop="$u.throttle(refreshLocation, 1000)">刷新定位</view>
					<view class="option-btn" @tap.stop="takePhoto">拍照</view>
					<view class="option-btn" @tap.stop="openFlash">{{flashStatus==='auto'?'闪光灯长亮':'闪光灯自动'}}</view>
				</view>
			</view>
		</u-overlay>
	</view>
</template>

<script>
	import { mapState, mapActions } from 'vuex';
	import { apiUrl } from '@/utils/env.js'; // 全局项目地址
	export default {
		name: "customUpload",
		props: {
			// 对外:上传的文件列表 {status:success|uploading|fail, url:''}
			modelValue: {
				type: Array,
				default: () => []
			},

			showTakePhoto: {
				type: Boolean,
				default: true
			},
			showChoosePhoto: {
				type: Boolean,
				default: true
			},
			// 上传组件的宽度
			width: {
				type: String,
				default: '180rpx'
			},
			// 上传组件的高度
			height: {
				type: String,
				default: '180rpx'
			},
			// 上传图标的文字
			uploadText: {
				type: String,
				default: ''
			},
			// 上传文件的存储位置
			fileStorageLocation: {
				type: String,
				default: 'yhtest'
			},
		},
		data() {
			return {
				fileList: [], // 对内:上传的文件列表 {status:success|uploading|fail, url:''}
				isFileError: false, // 文件列表出现故障(待开发)

				previewImage: false, // 预览图片
				isInCount: true, // 是在限制的文件数量范围内
				showOptionsPopup: false, // 选项弹出层
				showCameraPopup: false, // 相机弹出层
				flashStatus: 'auto', // 闪光灯,值为auto, on, off, torch

				userLocationRefreshing: false, // 用户位置刷新中
				userLocation: '', // 用户位置
			};
		},
		watch: {
			// 监听文件列表数据长度变化,存在数据则显示预览
			fileList(newData, oldData) {
				this.$emit('update:modelValue', newData);
				this.previewImage = newData.length ? true : false;
			},

			modelValue: {
				handler: function(newData, oldData) {
					this.fileList = newData;
				},
				immediate: true,
				deep: true
			}
		},
		computed: {
			...mapState(['userInfo']),
		},
		async created() {
			this.flashStatus = 'auto';
		},
		methods: {
			// 引入vuex中方法
			...mapActions(['getUserLocation']),
			// 选择操作类型
			chooseOperationType() {
				this.showOptionsPopup = true;
				this.refreshLocation(); // 获取定位
			},
			// 拍照
			onTakePhoto() {
				this.flashStatus = 'auto';
				this.showOptionsPopup = false;
				this.showCameraPopup = true;
			},
			//从文件夹选择
			onChoosePhoto() {
				this.showOptionsPopup = false;
				uni.chooseMedia({
					count: 9,
					mediaType: ['image', 'video'], // 文件类型
					sourceType: ['album'], // 指定从相册获取
					maxDuration: 30,
					success: async (res) => {
						// 按顺序执行异步操作,异步迭代
						for (let item of res.tempFiles) {
							const tempUrl = item.tempFilePath;
							console.log('拍照的临时图片地址:', tempUrl);
							this.fileList.push({
								status: 'uploading', // 状态为上传中
								url: tempUrl, // 文件的临时地址
								thumb: tempUrl, // 文件的临时地址
							});

							const realUrl = await this.uploadFilePromise(item.tempFilePath); // 上传图片
							console.log('上传返回的真实图片地址:', realUrl);
							this.fileList.pop();
							this.fileList.push({
								status: 'success', // 状态为上传中
								url: realUrl, // 文件的真实地址
								thumb: tempUrl, // 文件的临时地址
							});
						}
					},
					fail: (err) => {
						console.log('文件夹选择报错:', err);
					},
				})
			},

			// 手动拍照
			async takePhoto() {
				console.log('拍照按钮点击---------', new Date());
				// 创建并返回 camera 组件的上下文 cameraContext 对象
				const ctx = uni.createCameraContext();
				setTimeout(() => {
					this.showCameraPopup = false; // 关闭弹出层
				}, 200);
				await ctx.takePhoto({
					quality: 'high',
					success: async (res) => {
						uni.$u.toast('拍摄成功');
						// 返回照片文件的临时路径
						const tempUrl = res.tempImagePath;
						console.log('拍照的临时图片地址:', tempUrl);
						this.fileList.push({
							status: 'uploading', // 状态为上传中
							url: tempUrl, // 文件的临时地址
							thumb: tempUrl, // 文件的临时地址
						});

						const realUrl = await this.uploadFilePromise(res.tempImagePath); // 上传图片
						console.log('上传返回的真实图片地址:', realUrl);
						this.fileList.pop();
						this.fileList.push({
							status: 'success', // 状态为上传中
							url: realUrl, // 文件的真实地址
							thumb: tempUrl, // 文件的临时地址
						});
					},
					fail: (err) => {
						console.log('手动拍照报错:', err);
					},
				});
			},

			// 打开闪光灯
			openFlash() {
				if (this.flashStatus === 'auto') {
					this.flashStatus = 'torch'; // 闪光灯长亮
				} else {
					this.flashStatus = 'auto'; // 闪光灯长亮
				}
			},

			// 刷新定位
			async refreshLocation() {
				this.userLocationRefreshing = true;
				this.userLocation = await this.getUserLocation(); // 获取用户位置信息
				setTimeout(() => {
					this.userLocationRefreshing = false;
				}, 1000)
			},

			// 上传图片
			async uploadFilePromise(filePath) {
				return new Promise((resolve, reject) => {
					let token = "Bearer ";
					token += uni.getStorageSync('token');
					let a = uni.uploadFile({
						url: `${apiUrl}/wx/wxfile/upload`, // 接口地址
						filePath: filePath,
						name: 'multipartFile', // 此处默认值是file,实际需要根据后端接口做更改
						header: {
							'Content-Type': 'multipart/form-data',
							'Authorization': token
						},
						// HTTP 请求中其他额外的 form data
						formData: {
							"cameraMan": this.userInfo.nickName || '---', // 拍摄人
							"cameraSite": this.userLocation || '---', // 拍摄位置
							"customPath": this.fileStorageLocation, // 自定义文件存放路径
						},
						success: (res) => {
							let parseData = JSON.parse(res.data);
							console.log("上传成功的地址", parseData);
							resolve(parseData.data);
						}
					});
				})
			},

			// 按下标删除图片
			deleteItem(index) {
				this.fileList.splice(index, 1);
			},

			// 预览图片
			onPreviewImage(item) {
				if (item.status !== 'success') return;
				uni.previewImage({
					// 先filter找出为图片的item,再返回filter结果中的图片url
					urls: this.fileList.filter((item) => item.status === 'success' && item.url).map((item) => item.url || item
						.thumb),
					current: item.url || item.thumb,
					fail() {
						uni.$u.toast('预览图片失败')
					},
				});
			},

		}
	}
</script>

<style lang="scss">
	.custom-upload {
		// border: 1px dashed red;
		display: flex;
		flex-direction: row;
		flex-wrap: wrap;

		.file-item {
			position: relative;
			display: flex;
			flex-direction: column;
			justify-content: center;
			align-items: center;

			border-radius: 2px;
			margin: 0 8px 8px 0;
			box-sizing: border-box;

			.upload-deletable {
				position: absolute;
				top: 0;
				right: 0;
				background-color: #373737;
				height: 14px;
				width: 14px;
				display: flex;
				flex-direction: row;
				border-bottom-left-radius: 100px;
				align-items: center;
				justify-content: center;
				z-index: 3;

				.upload-deletable-icon {
					position: absolute;
					-webkit-transform: scale(0.7);
					transform: scale(0.7);
					top: 0px;
					right: 0px;
				}
			}

			.upload-success {
				position: absolute;
				bottom: 0;
				right: 0;
				display: flex;
				flex-direction: row;
				border-style: solid;
				border-top-color: transparent;
				border-left-color: transparent;
				border-bottom-color: #5ac725;
				border-right-color: #5ac725;
				border-width: 9px;
				align-items: center;
				justify-content: center;

				.upload-success-icon {
					position: absolute;
					-webkit-transform: scale(0.7);
					transform: scale(0.7);
					bottom: -10px;
					right: -10px;
				}
			}
		}

		.upload-button {
			padding: 10rpx;
			display: flex;
			flex-direction: column;
			justify-content: center;
			align-items: center;

			background-color: #f4f5f7;
			border-radius: 2px;
			margin: 0 8px 8px 0;
			box-sizing: border-box;

			.upload-button-text {
				margin-top: 8rpx;
				color: #ccc;
				text-align: center;
			}
		}

		.option-list {
			display: flex;
			flex-direction: column;
			justify-content: center;
			align-items: center;
			padding: 40rpx 40rpx 20rpx 40rpx;

			.option-btn {
				border-bottom: 1px solid #ccc6;
				padding: 30rpx;
				width: 100%;
				text-align: center;
				font-size: 16px;
			}

			.option-btn-close {
				padding: 30rpx;
				width: 100%;
				text-align: center;
				font-size: 16px;
			}
		}


		.camera-container {
			position: relative;
			width: 100%;
			height: 100%;

			.user-location {
				position: absolute;
				bottom: 20rpx;
				left: 20rpx;
				padding: 20rpx;
				background-color: #cccccc9c;
				color: #fff;
				border-radius: 10rpx;
				display: flex;
				flex-direction: row;
				justify-content: center;
				align-items: center;

				.icon-location {
					width: 30rpx;
					height: 30rpx;
					border-radius: 50%;
					background-color: #19be6b;
					margin: 6rpx;
					border: 2px solid #ecddd5;
				}

				.icon-location-refreshing {
					width: 30rpx;
					height: 30rpx;
					border-radius: 50%;
					background-color: #ff9900;
					margin: 6rpx;
					border: 2px solid #ecddd5;
				}
			}

			.camera-option-list {
				width: 100%;
				height: 200rpx;
				background-color: #f4f5f7;
				display: flex;
				flex-direction: row;

				.option-btn {
					display: flex;
					flex-direction: column;
					justify-content: center;
					border: 2px solid #2979ff;
					box-sizing: border-box;
					height: 100%;
					width: 33.33%;
					text-align: center;
					font-size: 18px;
				}
			}
		}

	}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1502710.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

哈希表应用

例题 在这里使用一个简化版的问题进行分析&#xff1a;给定N个自然数&#xff0c;值域是&#xff0c;求出这N个自然数中共有多少个不同的自然数。 分析 如果值域是&#xff0c;那么可以利用之前介绍过的计数排序算法解决问题。定义一个的大数组a&#xff0c;每个位置a[x]所对…

vue 使用 PrintJs 实现打印pdf效果

一、print.js介绍 Print.js主要是为了帮助我们直接在应用程序中打印PDF文件&#xff0c;而无需离开界面&#xff0c;并且不使用嵌入。对于用户不需要打开或下载PDF文件的特殊情况&#xff0c;他们只需要打印它们。 例如&#xff0c;当用户请求打印在服务器端生成的报告时&…

Spring web MVC(入门)

1、什么是MVC&#xff08;一种思想&#xff09; Model View Controller &#xff1a; Model—模型 View—视图 Controller—控制器 2、Spring MVC是一种实现&#xff08;我们现在学的是Spring web,Spring mvc过时了&#xff09; View属于前端问题我们后端人员不必太过于关注…

Java基础 - 8 - 算法、正则表达式、异常

一. 算法 什么是算法&#xff1f; 解决某个实际问题的过程和方法 学习算法的技巧&#xff1f; 先搞清楚算法的流程&#xff0c;再直接去推敲如何写算法 1.1 排序算法 1.1.1 冒泡排序 每次从数组中找出最大值放在数组的后面去 public class demo {public static void main(S…

JVM——执行引擎

文章目录 1、概述2、计算机语言的发展史2.1、机器码2.2、汇编语言2.3、高级语言2.4、字节码 3、Java代码编译和执行过程4、解释器5、JIT编译器5.1、为什么HotSpot VM同时存在JIT编译器和解释器5.2、热点代码探测确定何时JIT5.3、设置执行模式5.4、C1编译器和C2编译器 6、AOT编译…

python实现桶排序

桶排序&#xff08;Bucket Sort&#xff09;是一种排序算法&#xff0c;它将待排序的元素分到有限数量的桶&#xff08;buckets&#xff09;中&#xff0c;然后分别对每个桶中的元素进行排序&#xff0c;最后按照顺序将所有的桶中的元素依次取出&#xff0c;即可得到有序序列。…

深入了解Kafka中Topic的神奇之处

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 深入了解Kafka中Topic的神奇之处 前言Topic的基本概念Kafka Topic 的定义&#xff1a;Kafka Topic 的基本原理&#xff1a;为何 Topic 是 Kafka 消息传递的核心组成部分&#xff1a; 创建和配置Topic创…

FairScale 库测试实验(一)-- 大模型训练基础之模型并行

DDP的分布式训练方法采用数据并行方式&#xff0c;相当于通过增大数据的batch来加快训练。但对于大模型&#xff08;LLM&#xff09;来说&#xff0c;DDP已经不适用了。因为LLMs的模型本身太大&#xff0c;一块GPU都放不下怎么可能去复制从而实现数据并行呢。所以LLM的训练采用…

Ansible常用模块详解

Ansible常用模块详解一、Ansible简介1、Ansible是什么&#xff1f;2、Ansible是怎么工作的&#xff1f;3、Ansible的特性 二、Ansible 环境安装部署三、Ansible 命令行模块1、command 模块2、shell 模块3、cron 模块4、user 模块5、group 模块6、copy 模块7、file 模块8、hostn…

阿里云服务器没有国外地域吗?

阿里云地域没有国外节点&#xff1f;有&#xff0c;阿里云服务器国外地域美国、日本、新加坡、韩国、英国及德国等&#xff0c;阿里云服务器地域遍布全球&#xff0c;共29个地域可选。如果您在购买阿里云服务器时&#xff0c;没有国外地域可选&#xff0c;那是因为活动上提供的…

基于SSM框架的婚庆平台设计与实现

目 录 摘 要 I Abstract II 引 言 1 1相关技术 3 1.1 SSM框架简介 3 1.2 MySQL数据库简介 3 1.3 MVC设计模式简介 3 1.4 本章小结 4 2系统分析 5 2.1功能需求 5 2.1.1 用户功能需求 5 2.1.2 管理员功能需求 6 2.2非功能需求 10 2.3本章小结 10 3系统设计 11 3.1总体设计 11 3.…

spark 实验二 RDD编程初级实践

目录 一. pyspark交互式编程示例&#xff08;学生选课成绩统计&#xff09; 该系总共有多少学生&#xff1b; 该系DataBase课程共有多少人选修&#xff1b; 各门课程的平均分是多少&#xff1b; 使用累加器计算共有多少人选了DataBase这门课。 二.编写独立应用程序实现数…

java Spring boot简述jetcache 并叙述后续文章安排

我们之前 讲了 Spring boot 整合 cache 使用 simple(默认) redis Ehcache memcached的几种方式 但是 始终有人觉得不够完善 提出了一些问题 例如 觉得 当前spring boot 对缓存过期的控制过于松散 不严谨 比较明显的体现就是 memcached过期时间在逻辑代码中控制 Ehcache的过期时…

spring boot 集成 mysql ,mybatisplus多数据源

1、需要的依赖&#xff0c;版本自行控制 <dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId> </dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java<…

神经网络 梯度与神经元参数w、b关系;梯度与导数关系

参考&#xff1a;https://blog.csdn.net/weixin_44259490/article/details/90295146 概念 梯度与w的关系可以用梯度下降公式来表示&#xff1a;ww−α ∂ c o s t ∂ w \frac{\partial cost}{\partial w} ∂w∂cost​&#xff0c;其中w表示网络的权重&#xff0c; ∂ c o s t…

练习3-softmax分类(李沐函数简要解析)与d2l.train_ch3缺失的简单解决方式

环境为:练习1的环境 网址为:https://www.bilibili.com/video/BV1K64y1Q7wu/?spm_id_from333.1007.top_right_bar_window_history.content.click 代码简要解析 导入模块 导入PyTorch 导入Torch中的nn模块 导入d2l中torch模块 并命名为d2l import torch from torch import nn…

实现鼠标移动el-select下拉框的label上面显示出table悬浮窗

首先是对vue代码 实现思路就是在el-option里面放一个span来包裹el-popover&#xff0c;里面在放tabe实现悬浮表格 <el-form-item label"原理图编号"><el-select v-model"data.number" placeholder"请选择" clearable multiple collaps…

C语言第三十七弹---文件操作(下)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】 文件操作 1、文件的随机读写 1.1、fseek 1.2、ftell 1.3、rewind 2、文件读取结束的判定 2.1、被错误使用的 feof 3、文件缓冲区 总结 1、文件的随机读写…

阿里云主机地域怎么选择?哪个好?

阿里云服务器地域选择方法&#xff0c;如何选择速度更快、网络延迟更低的地域节点&#xff0c;地域指云服务器所在的地理位置区域&#xff0c;地域以城市划分&#xff0c;如北京、杭州、深圳及上海等&#xff0c;如何选择地域&#xff1f;建议根据用户所在地区就近选择地域&…

同等学力申硕专业介绍——管理学硕士

同等学力申硕的专业很多。 目前有十三大门类&#xff0c;分别是医学、法学、管理学、工学、教育学、经济学、艺术学、文学、历史学、理学、哲学、农学、军事学等&#xff0c;每个大门类中都有很多的细分专业。 今天为大家介绍同等学力申硕专业——管理学。 专业介绍 管理学是…