一、微信小程序中
微信小程序中可以直接使用camera标签,这个标签不兼容app,官方文档
<cameradevice-position="back"flash="off":style="{ height: lheight + 'px', width: lwidth + 'px' }"class="w-full"></camera>
拍照方法:
const takePhoto = () => {const ctx = uni.createCameraContext();ctx.takePhoto({quality: "high",success: (res) => {console.log(res.tempImagePath);},});
};
二、uni-app混合开发app中
在app中需要使用live-pusher直播推流组件来实现,官方文档
有两种方法实现,
1、使用live-pusher标签,但是页面需要是nvue平台【推荐使用这种】
2、使用h5plus提供的plus.video.LivePusher来实现,可以使用于vue平台,如果需要在相机上覆盖样式,比如边框之类的比较复杂,实践过后发现获取的图片宽度比相机宽度大,获取快照时间过长,大概需要2秒
1、live-pusher标签
1.1、使用标签形式需要把页面设置为nvue后缀,如果是vue后缀的可以显示相机但是无法使用uni.createLivePusherContext来获取 live-pusher 上下文对象,在拍照的时候无法进入回调函数,所以无法获取拍照的图片
1.2、需要自定义样式可以使用cover-view等来实现
1.3、如果是vue平台添加了一个nvue页面导致运行项目是报警告[plugin:vite:nvue-css],如下:
解决办法:在app.vue引入公共css文件外添加#ifndef APP-PLUS-NVUE条件
// #ifndef APP-PLUS-NVUE
@import "uview-plus/index.scss";
/*每个页面公共css */
@import "colorui/main.css";
//#endif
实例代码【仅供参考,根据实际需求修改】:
<template><view style="position: relative;height: 100vh; width:100%;display: flex;flex-direction: column;"><u-navbar :fixed="false" title="拍照" :autoBack="true"></u-navbar><view :style="{height:lheight +'px',width:lwidth+'px'}" class="carema_css"><image v-if="imgsrc" mode="aspectFit" :src="imgsrc" :style="{height:lheight +'px',width:lwidth+'px'}" ></image><live-pusher :style="{height:lheight+'px'}" id='livePusher' ref="livePusher" class="livePusher" url=""mode="FHD" :muted="true" :enable-camera="true" :auto-focus="true" :beauty="1" whiteness="2"aspect="9:16" @statechange="statechange" @netstatus="netstatus" @error = "error"></live-pusher><cover-view v-if="!imgsrc" class="cover_view" :style="{height:lheight+'px',width:lwidth+'px'}"><cover-view class="cover_css flec-center":style="{width:(lwidth - borderSize.left - borderSize.right - 4)+'px',height:(lheight-borderSize.top-borderSize.bottom -4)+'px',marginTop:borderSize.top+'px',marginLeft:borderSize.left+'px',marginRight:borderSize.right+'px',marginBottom:borderSize.bottom+'px',}"><text class="covertext">请把单据放在边框内拍照{{imgsrc}}</text></cover-view></cover-view></view><view class="btn-css" :style="{width:lwidth+'px'}"><view class="has_imgsrc" :style="{width:lwidth+'px'}" v-if="!imgsrc"><view style="width:52px;"><u-iconname="photo-fill"color="white"size="28"@click="openimage"></u-icon></view><view style="width:52px;"><viewclass="btn-takePhoto"type="primary"@click="snapshot"><u-icon name="camera" color="#067FFF" size="54rpx"></u-icon></view></view><view style="width:52px;"></view></view><view v-else style="padding: 0 66rpx;display: flex;justify-content: space-between;flex-direction: row;"><view@click="snapshotAgainPusher"><text class="comfirmimg_text">重新拍照</text></view><view><text class="comfirmimg_text" @click="submit">确认上传</text></view></view></view></view>
</template>
<script>import { uploadImg } from "../../config/request.js";export default {data() {return {imgsrc:'',borderSize: {top: null,bottom: null,right: null,left: null,},proportion: null,lheight: null,lwidth: null,navbarHeight: null,border_size_init: 10, // 默认边框宽度}},onLoad(option) {uni.setStorageSync("from_uploadimg_page", true);// W_H_proportion:宽和高的比例值,宽300,高400,传0.75,不传显示默认边框宽度this.proportion = Number(option?.W_H_proportion) || null;this.init_W_h()},onReady() {this.liveInit()},beforeUnmount() {// 页面退出时销毁scanWinconsole.log("beforeUnmount");this.close();},methods: {liveInit(){// 注意:需要在onReady中 或 onLoad 延时this.context = uni.createLivePusherContext("livePusher", this);this.switchCamera()this.startPreview()},// TODO 后期需要加上ocr识别后才能上传async submit() {const img_code = await this.imgUpload();if (img_code) {uni.$emit("img_upload_img", {img_code,temporary_img: this.imgsrc,});uni.navigateBack(-1);}// uni.$on("img_upload_img", function (data) {});获取图片code},async imgUpload() {uni.showLoading({title: "图片正在上传",mask: true,});const { data, code } = await uploadImg(this.imgsrc);uni.hideLoading();if (code === 200) {return data;} else {return false;}},openimage() {const this_ = this;uni.chooseImage({count: 1,sizeType: ["compressed"], // original 原图,compressed 压缩图,默认二者都有,compressed手机端选照片会压缩图片sizesourceType: ["album"], // album 从相册选图,camera 使用相机,默认二者都有success: function (res) {this_.imgsrc = res.tempFilePaths[0];this_.stopPreview();},fail: function (e) {console.log(e);},complete: function () {},});},init_W_h() {const this_ = this;uni.getSystemInfo({success(res) {this_.navbarHeight = res.model.indexOf("iPhone") !== -1 ? 44 : 48;console.log(res);const canUseHeight =res.screenHeight - res.statusBarHeight - this_.navbarHeight - 86; // 相机总的可用高度const canUseWidth = res.screenWidth; // 相机总的可用宽度const calculatesize = this_.calculateDimensions(// 减去默认的两个边框长度(保证边框不会跟手机边框重叠)canUseWidth - this_.border_size_init * 2,canUseHeight - this_.border_size_init * 2,this_.proportion); // 计算出边框的宽高this_.lheight = canUseHeight;this_.lwidth = canUseWidth;this_.borderSize = {top: (canUseHeight - calculatesize.height) / 2,bottom: (canUseHeight - calculatesize.height) / 2,right: (canUseWidth - calculatesize.width) / 2,left: (canUseWidth - calculatesize.width) / 2,};console.log(canUseHeight);},});},calculateDimensions(maxWidth, maxHeight, aspectRatio) {if (!aspectRatio) {return {width: maxWidth,height: maxHeight,};}// 根据比例计算可能的宽度和高度let possibleWidth = maxWidth;let possibleHeight = maxWidth / aspectRatio;// 检查高度是否超过了最大高度if (possibleHeight > maxHeight) {// 如果超过了,则以最大高度为基准,重新计算宽度possibleHeight = maxHeight;possibleWidth = maxHeight * aspectRatio;}// 返回计算后的宽度和高度return {width: possibleWidth,height: possibleHeight,};},snapshotAgainPusher(){this.imgsrc = ''this.startPreview()},start: function() {this.context.start({success: (a) => {console.log("livePusher.start:" + JSON.stringify(a));}});},close: function() {this.context.close({success: (a) => {console.log("livePusher.close:" + JSON.stringify(a));}});},snapshot: function() {const this_ = thisthis.context.snapshot({success: (e) => {this_.imgsrc = e.message.tempImagePath}});},stop: function() {this.context.stop({success: (a) => {console.log(JSON.stringify(a));}});},switchCamera: function() {this.context.switchCamera({success: (a) => {console.log("livePusher.switchCamera:" + JSON.stringify(a));}});},startPreview: function() {this.context.startPreview({success: (a) => {console.log("livePusher.startPreview:" + JSON.stringify(a));}});},stopPreview: function() {this.context.stopPreview({success: (a) => {console.log("livePusher.stopPreview:" + JSON.stringify(a));}});}}}
</script>
<style scoped lang="scss">.has_imgsrc{display: flex;flex-direction: row;justify-content: space-around;align-items: center;}.covertext {background: rgba(51, 51, 51, 0.4);color: #f9f9f9;padding: 3px;font-size: 14px;}.flec-center{display: flex;align-items: center;justify-content: center;}.carema_css{// border:3px solid red;position: relative;}.cover_view{width:100%;height: 100%;position: absolute;top: 0px;left: 0px;z-index:99999;box-shadow: inset 0 0 0 2px #000;}.cover_css{text-align: center;color:white;// border:2px solid #fefefe;border-right:1.5px dashed #fefefe;border-top:1.49999px dashed #fefefe;border-bottom:1.49999px dashed #fefefe;border-left:1.5px dashed #fefefe;border-radius: 4px;}
.flex-ctr-full {height: 100vh;display: flex;flex-direction: column;
}
.border-corner {position: absolute;width: 40rpx;height: 40rpx;border-top: 3px solid #fff;border-left: 3px solid #fff;// border-top-left-radius: 14rpx;
}
.borderlt {top: 0px;left: 0px;}
.borderrt {top: 0px;right: 0px;transform: rotate(90deg);}
.borderlb {bottom: 0px;left: 0px;transform: rotate(270deg);}
.borderrb {bottom: 0px;right: 0px;transform: rotate(180deg);}
.covertext {background: rgba(51, 51, 51, 0.4);color: #f9f9f9;padding-left: 5px;font-size: 14px;
}.btn-css {height: 86px;width: 100%;background: rgba(36, 36, 36, 0.75);// padding: 0 66rpx;justify-content: center;
}
.btn-icon {color: #fff;font-size: 56rpx;margin-right: 50rpx;}
.comfirmimg_text {color: #ffffff;font-size: 16px;height:51px;line-height: 51px;;}.btn-takePhoto {z-index: 250;width: 96rpx;height: 96rpx;padding: 22rpx;border-radius: 100%;background-color: #ffffff;// transform: translateY(-50%);box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 12%);}.level-left {display: flex;justify-content: flex-start;
}
.level-right {display: flex;justify-content: flex-end;
}
.level {display: flex;flex-direction: row;justify-content: flex-start;align-items: center;
}
</style>
2、使用plus.video.LivePusher
2.1、这种方法可以在vue页面中使用,自定义样式需要plus.webview.create添加一个html页面来实现,在项目根目录下添加一个hybrid目录,在目录下添加html页面
this.scanWin = plus.webview.create("/hybrid/html/faceTip.html?" + params,"",{top: that.navbarHeight + 44 + "px",background: "transparent",height: that.lheight + "px",width: that.lwidth + "px",});
2.2、拍照获取页面的时候获取的图片是反转的,需要使用plus.zip.compressImage来翻转图片
plus.zip.compressImage({src: imgPath,dst: imgPath,overwrite: true,quality: 40,rotate: 270,},(zipRes) => {//获取到正确的图片}})
2.3、获取的图片宽度会比相机的大,如果有解决办法欢迎分享。
2.4、使用快照snapshot获取图片时间很长,如果有解决办法欢迎分享。
示例【仅供参考,根据实际需求修改】:
相机组件:
<template><view class="flex-ctr-full"><u-navbar :fixed="false" title="拍照" :autoBack="true"></u-navbar><view class="flex-1 relative"><imagev-if="imgsrc"class="select-img w-full h-full"mode="aspectFit":src="imgsrc"></image></view><view class="btn-css"><view class="level h-full"><view v-if="!imgsrc" class="level-left w-65px"><u-iconname="photo-fill"color="white"size="28"@click="openimage"></u-icon></view><viewv-elseclass="level-left w-80px color-white comfirmimg_text":class="!imgsrc ? 'visibility-hidden' : ''"@click="snapshotAgainPusher">重新拍照</view><view:class="imgsrc ? 'visibility-hidden' : ''"class="btn-takePhoto level-item mt-12px"type="primary"@click="takePhoto"><u-icon name="camera" color="#067FFF" size="54rpx"></u-icon></view><view class="level-right" :class="!imgsrc ? 'visibility-hidden' : ''"><text class="comfirmimg_text btn-collection" @click="submit">确认上传</text></view></view></view></view>
</template>
<script>
import { uploadImg } from "../../config/request.js";
export default {data() {return {imgsrc: "",pusher: null,scanWin: null,snapshotTimeoutNumber: 3000,faceInitTimeout: null,snapshTimeout: null,screenHeight: 0,topStatusHeight: 0,borderSize: {top: null,bottom: null,right: null,left: null,},proportion: null,lheight: null,lwidth: null,navbarHeight: null,border_size_init: 20, // 默认边框宽度imgurl: "",};},onLoad(option) {uni.setStorageSync("from_uploadimg_page", true);// W_H_proportion:宽和高的比例值,宽300,高400,传0.75,不传显示默认边框宽度this.proportion = Number(option?.W_H_proportion) || null;this.init_W_h();let that = this;uni.getSystemInfo({success: function (e) {console.log(e);that.screenHeight = e.windowHeight;that.topStatusHeight = e.screenHeight - e.windowHeight + "px";},});//#ifdef APP-PLUSthis.faceInit();//#endif},onHide() {// this.scanWin.close();this.faceInitTimeout && clearTimeout(this.faceInitTimeout);this.snapshTimeout && clearTimeout(this.snapshTimeout);},methods: {openimage() {const this_ = this;uni.chooseImage({count: 1,sizeType: ["compressed"], // original 原图,compressed 压缩图,默认二者都有,compressed手机端选照片会压缩图片sizesourceType: ["album"], // album 从相册选图,camera 使用相机,默认二者都有success: function (res) {this_.imgsrc = res.tempFilePaths[0];this_.imgurl = res.tempFilePaths[0];this_.scanWin.close();this_.pusher.close();},fail: function (e) {console.log(e);},complete: function () {},});},init_W_h() {const this_ = this;uni.getSystemInfo({success(res) {this_.navbarHeight = res.statusBarHeight;console.log(res);const canUseHeight =res.screenHeight - res.statusBarHeight - this_.navbarHeight - 86; // 相机总的可用高度const canUseWidth = res.screenWidth; // 相机总的可用宽度const calculatesize = this_.calculateDimensions(// 减去默认的两个边框长度(保证边框不会跟手机边框重叠)canUseWidth - this_.border_size_init * 2,canUseHeight - this_.border_size_init * 2,this_.proportion); // 计算出边框的宽高this_.lheight = canUseHeight;this_.lwidth = canUseWidth;this_.borderSize = {top: (canUseHeight - calculatesize.height) / 2,bottom: (canUseHeight - calculatesize.height) / 2,right: (canUseWidth - calculatesize.width) / 2,left: (canUseWidth - calculatesize.width) / 2,};},});},calculateDimensions(maxWidth, maxHeight, aspectRatio) {if (!aspectRatio) {return {width: maxWidth,height: maxHeight,};}// 根据比例计算可能的宽度和高度let possibleWidth = maxWidth;let possibleHeight = maxWidth / aspectRatio;// 检查高度是否超过了最大高度if (possibleHeight > maxHeight) {// 如果超过了,则以最大高度为基准,重新计算宽度possibleHeight = maxHeight;possibleWidth = maxHeight * aspectRatio;}// 返回计算后的宽度和高度return {width: possibleWidth,height: possibleHeight,};},faceInit() {let that = this;uni.showLoading({title: "加载中",mask: true,});this.faceInitTimeout = setTimeout(() => {this.pusherInit();const params = `height=${that.lheight}&width=${that.lwidth}&top=${this.borderSize.top}&left=${this.borderSize.left}&right=${this.borderSize.right}&bottom=${this.borderSize.bottom}`;this.scanWin = plus.webview.create("/hybrid/html/faceTip.html?" + params,"",{top: that.navbarHeight + 44 + "px",background: "transparent",height: that.lheight + "px",width: that.lwidth + "px",});setTimeout(() => {this.scanWin.show();}, 200);}, 200);uni.hideLoading();},pusherInit() {let that = this;const pages = getCurrentPages(); // 获取当前页面栈const page = pages[pages.length - 1]; // 获取当前页面的对象const currentWebview = page.$getAppWebview();console.log(that.screenHeight - 50 + "px",that.lheight + "px",that.navbarHeight,that.topStatusHeight);this.pusher = plus.video.createLivePusher("livepusher", {url: "",top: that.navbarHeight + 44 + "px",left: "0px",width: that.lwhite + "px",height: that.lheight + "px",position: "absolute",aspect: "3:4","z-index": 999,});currentWebview.append(this.pusher);// this.pusher.switchCamera(); //换为前置摄像头this.pusher.preview();uni.hideLoading();},//拍照takePhoto() {let that = this;uni.showLoading({title: "照片生成中",mask: true,});// this.snapshTimeout = setTimeout(() => {console.log(this.pusher);this.pusher.snapshot((res) => {console.log("走到这啦2", res);const src = res.tempImagePath;that.imgurl = res.tempImagePath;that.getImage(src);},(err) => {console.log("拍照失败", err);uni.showToast({title: "拍照失败",});});// }, 3000);},// 重拍snapshotAgainPusher() {this.faceInit(); //全部重新加载this.imgsrc = "";},getImage(imgPath) {let that = this;plus.zip.compressImage({src: imgPath,dst: imgPath,overwrite: true,quality: 40,rotate: 270,},(zipRes) => {that.imgsrc = zipRes.target;that.scanWin.close();uni.hideLoading();uni.showToast({title: "照片已生成",duration: 1000,success() {},});that.pusher.close();},function (error) {uni.showToast({title: "照片生成失败",});});},// TODO 后期需要加上ocr识别后才能上传async submit() {const img_code = await this.imgUpload();if (img_code) {uni.$emit("img_upload_img", {img_code,temporary_img: this.imgurl,});uni.navigateBack(-1);}// uni.$on("img_upload_img", function (data) {});获取图片code},async imgUpload() {uni.showLoading({title: "图片正在上传",mask: true,});const { data, code } = await uploadImg(this.imgurl);uni.hideLoading();console.log(data, code);if (code === 200) {return data;} else {return false;}},},beforeUnmount() {// 页面退出时销毁scanWinconsole.log("beforeUnmount");this.scanWin.close();},
};
</script><style scoped lang="scss">
.cover {position: absolute;z-index: 200;top: 0;left: 0;right: 0;bottom: 0;
}
.flex-ctr-full {height: 100vh;display: flex;flex-direction: column;
}
.border-corner {position: absolute;width: 40rpx;height: 40rpx;border-top: 3px solid #fff;border-left: 3px solid #fff;// border-top-left-radius: 14rpx;&.borderlt {top: 0px;left: 0px;}&.borderrt {top: 0px;right: 0px;transform: rotate(90deg);}&.borderlb {bottom: 0px;left: 0px;transform: rotate(270deg);}&.borderrb {bottom: 0px;right: 0px;transform: rotate(180deg);}
}.covertext {background: rgba(51, 51, 51, 0.4);color: #f9f9f9;padding-left: 5px;font-size: 14px;
}.btn-css {height: 86px;line-height: 86px;width: 100%;background: rgba(36, 36, 36, 0.75);padding: 0 66rpx;.btn-icon {color: #fff;font-size: 56rpx;margin-right: 50rpx;&.btn-collection {margin-left: 50rpx;margin-right: 0;}}.comfirmimg_text {padding: 14rpx;color: #f9f9f9;font-size: 16px;}.btn-takePhoto {z-index: 250;width: 96rpx;height: 96rpx;padding: 22rpx;border-radius: 100%;background-color: #ffffff;// transform: translateY(-50%);box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 12%);}.level-left,.level-right {flex-basis: auto;flex-grow: 0;flex-shrink: 0;display: flex;}
}
.level-left {justify-content: flex-start;
}
.level-right {justify-content: flex-end;
}
.level {display: flex;justify-content: space-between;
}
</style>
自定义样式的覆盖文件faceTip.html:
头部需要添加页面根据设备进行缩放
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"><title></title><script>function getURLParameter(name) {// 使用window.location.search获取URL中的查询字符串var query = window.location.search.substring(1);// 使用new URLSearchParams创建一个查询字符串参数的实例var vars = query.split("&");for (var i = 0; i < vars.length; i++) {var pair = vars[i].split("=");if (pair[0] == name) {return pair[1];}}return (false);}// 调用函数获取特定参数// var paramsValue = {}// paramsValue.top = getURLParameter('top');<style>.facecontent {height: 100%;position: absolute;width: 100%;text-align: center;}.cover {height: calc(100% - 100px);width: calc(100% - 100px);text-align: center;padding: 40px;display: flex;justify-content: center;align-items: center;border: 3px dashed #f9f9f9;border-radius: 20px;}.covertext {background: rgba(51, 51, 51, 0.4);color: #f9f9f9;font-size: 15px;padding: 4px;}</style></head><body><div class="facecontent" id="facecontent_id"><div class="cover" id="cover_border"><div class="covertext">请把单据放在边框内拍照</div></div></div></body>
</html>