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

一、需求前提

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

起初我是用的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>

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JAVA实战开源项目:大学兼职教师管理系统(Vue+SpringBoot)

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容三、界面展示3.1 登录注册3.2 学生教师管理3.3 课程管理模块3.4 授课管理模块3.5 课程考勤模块3.6 课程评价模块3.7 课程成绩模块3.8 可视化图表 四、免责说明 一、摘要 1.1 项目介绍 大学兼职教师管理系统&#xff0c;旨…

JAVA中YML:几个用法

项目有一些配置文件&#xff0c;ini、prop类型的配置文件都考虑过后&#xff0c;还是选择yml文件&#xff0c;如上图&#xff1a;xxconfig.yml。 要求&#xff1a; 1、允许实施人员手动配置 2、配置文件要能轻便的转化为一个JAVA对象 3、程序启动后&#xff0c;打印这些配置项&…

qt带后缀单位的QLineEdit

QLineEditUnit.h #pragma once #include <QLineEdit> #include <QPushButton>class QLineEditUnit : public QLineEdit {Q_OBJECT public:QLineEditUnit(QWidget* parent Q_NULLPTR);~QLineEditUnit();//获取编辑框单位QString UnitText()const;//设置编辑框单位…

Java开发与配置用到的各类中间件官网

开发配置时用到了一些官网地址&#xff0c;记录一下。 activemq 官网&#xff1a;ActiveMQ elk 官网&#xff1a;Elasticsearch 平台 — 大规模查找实时答案 | Elastic nginx 官网&#xff1a;nginx maven 官网&#xff1a;Maven – Welcome to Apache Maven nexus 官网&a…

C语言之练手题

题目1&#xff1a; 思路&#xff1a;我们定义两个变量left和right分别为数组的左端下标和右端下标。 左端下标的元素为奇数时&#xff0c;left继续往前走&#xff0c;为偶数时就停下 右端下标的元素为偶数时&#xff0c;right- -往回走&#xff0c;为奇数时停下 停下后对应的元…

springboot252基于Springboot和vue的餐饮管理系统的设计与实现

餐饮管理系统的设计与实现 摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对信息管理混乱&#xff0c;出错率高&…

RHCE——一、OpenEuler22.03安装部署及例行性任务

RHCE 一、OpenEuler22.03安装部署及例行性任务 一、网络服务1.准备工作2、RHEL9操作系统的安装部署3、配置并优化OpenEuler22.034、网络配置实验&#xff1a;修改网络配置 二、例行性工作1、 单一执行的例行性任务&#xff1a;at&#xff08;一次性&#xff09;at命令详解 2、循…

基于springboot的水果购物商城管理系统(程序+文档+数据库)

** &#x1f345;点赞收藏关注 → 私信领取本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345;** 一、研究背景…

基于redis实现互斥锁

利用setnx命令实现类似获取锁和释放锁。 获取锁&#xff0c;setnx lock 1&#xff0c;返回值为1视为获取成功&#xff0c;为0视为获取失败 释放锁&#xff0c;del lock 特殊情况&#xff1a; 如果获取锁之后&#xff0c;锁来还来不及释放&#xff0c;redis宕机了&#xff0c;这…

我的NPI项目之Android Camera (三)-- 核心部件 Camera的Lens (待修改)

Lens在选择Camera的模组的时候&#xff0c;算是除了Sensor之后的最重要的一个参数了。那么&#xff0c;我们来了解一下消费类电子产品中的camera 模组中的Lens有哪些&#xff0c;又有哪些讲究。 Lens是Camera模组中的一个小模组&#xff1b; 通常Lens有Plastic 和Glass的区别…

Draco点云压缩测试

ref&#xff1a;https://github.com/google/dracohttps://codelabs.developers.google.com/codelabs/draco-3d/index.html#6 Draco Draco 是一个用于编码压缩和解压缩 3D 几何网格和点云的库&#xff0c;从而改进 3D 图形的存储和传输该代码支持压缩点、连接信息、纹理坐标、颜…

如何修复SFC错误“Windows资源保护无法执行请求的操作”?

SFC是Windows中的一个实用程序&#xff0c;它可以扫描和修复Windows系统文件。该命令虽然便捷&#xff0c;但也会因为各种原因而出现错误&#xff0c;比如“Windows资源保护无法执行请求的操作”。如果您也遇到此错误提示&#xff0c;不妨阅读下面的这篇文章了解相应的解决方法…

音视频按照时长分类小工具

应某用户的需求&#xff0c;编写了这款根据音视频时长分类小工具。 实际效果如下&#xff1a; 显示的是时分秒&#xff1a; 核心代码&#xff1a; MediaInfo MI; if (MI.Open(strPathInput.c_str()) 0){return -1;}_tstring stDuration MI.Get(stream_t::Stream_Audio,0,_T…

13:大数据与Hadoop|分布式文件系统|分布式Hadoop集群

大数据与Hadoop&#xff5c;分布式文件系统&#xff5c;分布式Hadoop集群 Hadoop部署Hadoop HDFS分布式文件系统HDFS部署步骤一&#xff1a;环境准备HDFS配置文件 查官方手册配置Hadoop集群 日志与排错 mapreduce 分布式离线计算框架YARN集群资源管理系统步骤一&#xff1a;安装…

遥感生态指数(RSEI)——四个指数的计算

遥感生态指数RSEI&#xff08;Risk-Screening Environmental Indicators&#xff09;分布数据是一种基于卫星遥感影像反演计算得到的数据产品。生态环境质量评价在一定程度上反映一个地区生态环境系统的好坏,也可以在一定程度上反映人类社会活动和环境质量的关系,其对可持续发展…

将python编写的网站制作成docker镜像并上传到Github Packages上

文章目录 前言Docker安装docker注意事项 创建Dockerfile注意事项 构建 Docker 镜像运行 Docker 镜像 发布到Github Packages坑坑到位申请token的坑docker登录的坑给镜像添加标签的坑docker推送的坑 在Github Packages上查看总结 前言 还记得上一篇《借助ChatGPT使用Python搭建…

《Graphis》杂志报道,凯毅文化斩获两项国际金奖

一、凯毅文化获美国Graphis 2024年度奖金奖   近日&#xff0c;收到美国《Graphis》团队邮件约稿&#xff0c;将对深圳凯毅文化获得Graphis年度金奖的作品《城市与自然》进行案例报道。在Graphis 2024年度奖项评选中&#xff0c;凯毅文化作品获得一项金奖&#xff0c;二项银奖…

原生JavaScript,根据后端返回JSON动态【动态列头、动态数据】生成表格数据

前期准备&#xff1a; JQ下载地址&#xff1a; https://jquery.com/ <!DOCTYPE html> <html><head><meta charset"utf-8"><title>JSON动态生成表格数据,动态列头拼接</title><style>table {width: 800px;text-align: cen…

如何用一台电脑主机连两个显示器同步?

本文介绍如何使用一台电脑主机连接两台显示器并同步两台显示器&#xff01; 有两种方法。 方法一&#xff1a;使用两根信号线将两台显示器连接至显卡上的两个视频输出接口。 计算机显卡必须支持双输出&#xff0c;具有两个视频接口&#xff0c;用于连接不同的显示器。 设置方…

学习与学习理论 - 2024教招 - test

一 方向 方向性很重要&#xff0c;像学投篮一样关注发力顺序才是关键出发点&#xff0c;如果这个出发点没确定下来&#xff0c;会走许多弯路。所有学习理论大的观点&#xff0c;到某个人物个人的观点。被干掉之前&#xff08;不能被干掉&#xff09;&#xff0c;掌握所需的知识…