一、微信小程序中
微信小程序中可以直接使用camera标签,这个标签不兼容app,官方文档
cpp
<camera
device-position="back"
flash="off"
:style="{ height: lheight + 'px', width: lwidth + 'px' }"
class="w-full"
></camera>
拍照方法:
cpp
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条件
cpp
// #ifndef APP-PLUS-NVUE
@import "uview-plus/index.scss";
/*每个页面公共css */
@import "colorui/main.css";
//#endif
实例代码【仅供参考,根据实际需求修改】:
javascript
<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-icon
name="photo-fill"
color="white"
size="28"
@click="openimage"
></u-icon>
</view>
<view style="width:52px;">
<view
class="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() {
// 页面退出时销毁scanWin
console.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手机端选照片会压缩图片size
sourceType: ["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_ = this
this.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页面
javascript
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来翻转图片
javascript
plus.zip.compressImage(
{
src: imgPath,
dst: imgPath,
overwrite: true,
quality: 40,
rotate: 270,
},
(zipRes) => {
//获取到正确的图片
}
})
2.3、获取的图片宽度会比相机的大,如果有解决办法欢迎分享。
2.4、使用快照snapshot获取图片时间很长,如果有解决办法欢迎分享。
示例【仅供参考,根据实际需求修改】:
相机组件:
javascript
<template>
<view class="flex-ctr-full">
<u-navbar :fixed="false" title="拍照" :autoBack="true"></u-navbar>
<view class="flex-1 relative">
<image
v-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-icon
name="photo-fill"
color="white"
size="28"
@click="openimage"
></u-icon>
</view>
<view
v-else
class="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-PLUS
this.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手机端选照片会压缩图片size
sourceType: ["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() {
// 页面退出时销毁scanWin
console.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:
头部需要添加页面根据设备进行缩放
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>