【uniapp】uniapp小程序中实现拍照同时打开闪光灯的功能,拍照闪光灯实现
一、需求前提
特殊场景中,需要拍照的同时打开闪光灯,(例如黑暗场景下的设备维护巡检功能)。
起初我是用的uviewui中的u-upload组件自带的拍照功能,但是这个不支持拍照时打开闪光灯,也不支持从通知栏中打开闪光灯。
二、解决方案
采用组合形式解决:
- 使用uniapp官方内置组件中的 媒体组件:camera 实现闪光灯拍照,
uni.createCameraContext()获取返回图片结果 - 结合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, torchuserLocationRefreshing: 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 dataformData: {"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结果中的图片urlurls: 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>
相关文章:
【uniapp】uniapp小程序中实现拍照同时打开闪光灯的功能,拍照闪光灯实现
一、需求前提 特殊场景中,需要拍照的同时打开闪光灯,(例如黑暗场景下的设备维护巡检功能)。 起初我是用的uviewui中的u-upload组件自带的拍照功能,但是这个不支持拍照时打开闪光灯,也不支持从通知栏中打开…...
在python model train里如何驯服野生log?
关键词:python 、epoch、loss、log 🤖: 记录模型的训练过程的步骤如下: 导入logging模块。配置日志记录器,设置日志文件名、日志级别、日志格式等。在每个epoch结束时,使用logging模块记录性能指标、损失值、准确率等信…...
产品推荐 - Xilinx FPGA下载器 XQ-HS/STM2
1 FPGA下载器简介 1.性能优良 FPGA下载器XQ-HS/STM2采用Xilinx下载模块设计而成(JTAG-SMT2NC模块,该模块与Xilinx官方开发板KC705,KCU105,ZC702,ZC706,Zedboard等板载下载器一样,下载速度快…...
STM32 SDRAM知识点
1.SDRAM和SRAM的区别 SRAM不需要刷新电路即能保存它内部存储的数据。而SDRAM(Dynamic Random Access Memory)每隔一段时间,要刷新充电一次,否则内部的数据即会消失,因此SRAM具有较高的性能,但是SRAM也有它…...
手写分布式配置中心(六)整合springboot(自动刷新)
对于springboot配置自动刷新,原理也很简单,就是在启动过程中用一个BeanPostProcessor去收集需要自动刷新的字段,然后在springboot启动后开启轮询任务即可。 不过需要对之前的代码再次做修改,因为springboot的配置注入value("…...
记录一次排查负载均衡不能创建的排查过程
故障现象,某云上,运维同事在创建负载均衡的时候,发现可以创建资源,但是创建完之后,不显示对应的负载均衡。 创建负载均衡时候,按f12发现console有如下报错 后来请后端网络同事排查日志发现,是后…...
数据推送解决方案调研
需求 文档编辑类型的需求,左侧是菜单栏,右侧是内容块,现在的需求时,如果多人同时编辑这个方案,当添加章节/调整章节顺序/删除章节时,其他用户能够及时感知到。 解决方案调研 前端轮询 最简单的方案&…...
二、NLP中的序列标注(分词、主体识别)
一般来说,一个序列指的是一个句子,而一个元素指的是句子中的一个词。在序列标注中,我们想对一个序列的每一个元素标注一个分类标签。比如信息提取问题可以认为是一个序列标注问题,如提取出会议时间、地点等。 常见的应用场景&…...
seq2seq翻译实战-Pytorch复现
🍨 本文为[🔗365天深度学习训练营学习记录博客 🍦 参考文章:365天深度学习训练营 🍖 原作者:[K同学啊 | 接辅导、项目定制]\n🚀 文章来源:[K同学的学习圈子](https://www.yuque.com/…...
软考69-上午题-【面向对象技术2-UML】-关系
一、关系 UML中有4种关系: 依赖;关联;泛化;实现。 1-1、依赖 行为(参数),参数就是被依赖的事物,即:独立事物。 当独立事物发生变化时,依赖事务行为的语义也…...
智慧文旅|AI数字人导览:让旅游体验不再局限于传统
AI数字人导览作为一种创新的展示方式,已经逐渐成为了VR全景领域的一大亮点,不仅可以很好的嵌入在VR全景中,更是能够随时随地为观众提供一种声情并茂的讲解介绍,结合VR场景的沉浸式体验,让观众仿佛置身于真实场景之中&a…...
spring boot 集成 mysql ,mybatisplus多数据源
1、需要的依赖,版本自行控制 <dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId> </dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java<…...
CLion中常用快捷键(仍适用其他编译软件)
基本编辑操作: 复制:Ctrl C粘贴:Ctrl V剪切:Ctrl X撤销:Ctrl Z重做:Ctrl Shift Z (不小心撤销了 需要返回之前的操作 相当于下一步)全选:Ctrl A 导航࿱…...
考研复习c语言初阶(1)
本人准备考研,现在开始每天更新408的内容,目标这个月结束C语言和数据结构,每天更新~ 一.再次认识c语言 C语言是一门通用计算机编程语言,广泛应用于底层开发。C语言的设计目标是提供一种能以简易 的方式编译、处理低级存储器、产生…...
HTML—常用标签
常用标签: 标题标签:<h1></h1>......<h6></h6>段落标签:<p></p>换行标签:<br/>列表:无序列表<ul><li></li></ul> 有序列表<ol>&…...
Midjourney绘图欣赏系列(七)
Midjourney介绍 Midjourney 是生成式人工智能的一个很好的例子,它根据文本提示创建图像。它与 Dall-E 和 Stable Diffusion 一起成为最流行的 AI 艺术创作工具之一。与竞争对手不同,Midjourney 是自筹资金且闭源的,因此确切了解其幕后内容尚不…...
深度学习应该如何入门?
深度学习是一门令人着迷的领域,但初学者可能会感到有些困惑。让我们从头开始,用通俗易懂的语言来探讨深度学习的基础知识。 1. 基础知识 深度学习需要一些数学和编程基础。首先,我们要掌握一些数学知识,如线性代数、微积分和概率…...
FreeRtos Queue(五)
本篇主要分析在中断中向队列里发消息xQueueGenericSendFromISR和在中断里从队列中读取消息xQueueReceiveFromISR。 前言: xQueueGenericSendFromISR 和 xQueueReceiveFromISR都是在中断里调用的而不是任务里调用的,所以队列满了或者是队列为空的时候自然就没有把当…...
解决虚拟机静态网址设置后还是变动的的问题
源头就是我的虚拟机静态网址设置好了以后但是网址还是会变动 这是我虚拟机的配置 vi /etc/sysconfig/network-scripts/ifcfg-ens33 这是出现的问题 进入这里 cd /etc/sysconfig/network-scripts/ 然后我去把多余的ens33的文件都删了 然后还不行 后来按照这个图片进行了下 然后…...
【教程】Github环境配置新手指南(超详细)
写在前面: 如果文章对你有帮助,记得点赞关注加收藏一波,利于以后需要的时候复习,多谢支持! 文章目录 一、Github初始设置(一)登入Github(二)新建仓库 二、本地Git配置&am…...
Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...
HTML 列表、表格、表单
1 列表标签 作用:布局内容排列整齐的区域 列表分类:无序列表、有序列表、定义列表。 例如: 1.1 无序列表 标签:ul 嵌套 li,ul是无序列表,li是列表条目。 注意事项: ul 标签里面只能包裹 li…...
Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级
在互联网的快速发展中,高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司,近期做出了一个重大技术决策:弃用长期使用的 Nginx,转而采用其内部开发…...
如何在网页里填写 PDF 表格?
有时候,你可能希望用户能在你的网站上填写 PDF 表单。然而,这件事并不简单,因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件,但原生并不支持编辑或填写它们。更糟的是,如果你想收集表单数据ÿ…...
《C++ 模板》
目录 函数模板 类模板 非类型模板参数 模板特化 函数模板特化 类模板的特化 模板,就像一个模具,里面可以将不同类型的材料做成一个形状,其分为函数模板和类模板。 函数模板 函数模板可以简化函数重载的代码。格式:templa…...
Java毕业设计:WML信息查询与后端信息发布系统开发
JAVAWML信息查询与后端信息发布系统实现 一、系统概述 本系统基于Java和WML(无线标记语言)技术开发,实现了移动设备上的信息查询与后端信息发布功能。系统采用B/S架构,服务器端使用Java Servlet处理请求,数据库采用MySQL存储信息࿰…...
vulnyx Blogger writeup
信息收集 arp-scan nmap 获取userFlag 上web看看 一个默认的页面,gobuster扫一下目录 可以看到扫出的目录中得到了一个有价值的目录/wordpress,说明目标所使用的cms是wordpress,访问http://192.168.43.213/wordpress/然后查看源码能看到 这…...
GitHub 趋势日报 (2025年06月06日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...
Linux部署私有文件管理系统MinIO
最近需要用到一个文件管理服务,但是又不想花钱,所以就想着自己搭建一个,刚好我们用的一个开源框架已经集成了MinIO,所以就选了这个 我这边对文件服务性能要求不是太高,单机版就可以 安装非常简单,几个命令就…...
恶补电源:1.电桥
一、元器件的选择 搜索并选择电桥,再multisim中选择FWB,就有各种型号的电桥: 电桥是用来干嘛的呢? 它是一个由四个二极管搭成的“桥梁”形状的电路,用来把交流电(AC)变成直流电(DC)。…...
