背景
由于业务需求,需要进行拍照上传,百度了一遍组件都不太合适。自己结合已有案例封装了一下,可以把这个组件嵌套到el-dialog里面就可以使用。
实现功能
实时加载预览画面
点击拍照截取照片
不满意可以重拍,不会中断之前的视频流
加载当前设备的所有摄像头,可以进行选择切换
依赖
ElementPlus
@iconify/vue
tailwindCSS
使用方式
width="90%"
title="拍照"
v-model="visiable.camera"
@open="handlePhotoPreviewOpen"
@close="handlePhotoPreviewClose"
>
import { ref } from "vue";
import TakePhoto from "./take-photo.vue";
const visiable = ref({ camera: false });
const takePhotoRef = ref(null);
/** 拍照弹窗打开时 */
function handlePhotoPreviewOpen() {
takePhotoRef.value?.openCamera();
}
/** 拍照弹窗关闭时 */
function handlePhotoPreviewClose() {
takePhotoRef.value?.stop();
visiable.value.camera = false;
}
组件代码
// take-photo.vue
show-icon
type="warning"
v-if="isTipsVisiable"
@close="handleTipsClose"
class="!mt-4 border border-gray-200 border-solid shadow-md !rounded-lg"
>
拍照前,请确认当前设备是否有摄像头
由于浏览器限制,需要进行以下操作才能开启摄像头:
1. 复制以下地址到浏览器地址栏:
>chrome://flags/#unsafely-treat-insecure-origin-as-secure
>
2. 在弹出的页面中,将最上方的 禁用/Disabled 改成 启用/Enabled
style="--el-font-size-base: 18px"
v-for="(camera, index) in cameraList"
:key="camera.deviceId"
:label="index"
:disabled="!isCameraAvailable"
@change="handleCameraSwitchChange"
>
{{ camera.label }}
v-loading="isLoadingCamera"
element-loading-text="正在加载摄像头..."
class="preview-area mt-4 flex rounded-lg overflow-hidden relative justify-center items-center"
>
@click="takePhoto"
:disabled="!isCameraAvailable"
class="w-full !h-[65px] !rounded-lg"
:type="imageURL ? 'plain' : 'primary'"
>
{{ imageURL ? "重拍" : "拍照" }}
type="primary"
v-if="imageURL"
@click="handleImageUpload"
:disabled="!isCameraAvailable"
class="w-full !h-[65px] !rounded-lg"
>
确认上传
import { ElMessage } from "element-plus";
import { ref, onBeforeUnmount } from "vue";
import { Icon as IconifyIcon } from "@iconify/vue";
const emits = defineEmits(["upload"]);
const canvasRef = ref(null); // canvas控件对象
const videoRef = ref(null); // video 控件对象
const imageURL = ref(null); // 照片路径
const imageBlob = ref(null); // 二进制图片数据
const isCameraAvailable = ref(false); // 摄像头是否可用
const isLoadingCamera = ref(false); // 是否正在加载摄像头
const isTipsVisiable = ref(JSON.parse(localStorage.getItem("isTipsVisiable")) ?? true); // 是否显示提示
const Stream = ref(null); // 播放流
const cameraList = ref([]); // 摄像头列表
const selectedCamera = ref(0); // 当前选中的摄像头
const cameraStatus = ref("摄像头未连接或权限被拒绝");
/** 刷新摄像头列表 */
function handleCameraListRefresh() {
if (Stream.value) {
stop();
}
imageURL.value = null;
imageBlob.value = null;
cameraList.value = [];
selectedCamera.value = 0;
isLoadingCamera.value = true;
isCameraAvailable.value = false;
setTimeout(() => {
getCameraList();
}, 1000);
}
/** 获取当前连接到设备的摄像头数量 */
function getCameraList(init = false) {
if (navigator.mediaDevices) {
navigator.mediaDevices.enumerateDevices().then((devices) => {
cameraList.value = devices.filter(
(device) => device.kind === "videoinput" && device.deviceId
);
if (!init) {
openCameraStream();
}
});
} else {
ElMessage.error("该浏览器或所处环境不支持开启摄像头,请更换最新版Chrome浏览器");
}
}
/** 外面可以通过ref拿到这个方法来打开摄像头 */
function openCamera() {
if (cameraList.value.length) {
openCameraStream();
return;
}
getCameraList();
}
/** 切换摄像头 */
function handleCameraSwitchChange() {
stop();
openCameraStream();
}
/** 打开摄像头流 */
function openCameraStream() {
imageURL.value = null;
// 如果已经开启摄像头就不再开启
if (Stream.value) {
isCameraAvailable.value = true;
return;
}
isLoadingCamera.value = true;
isCameraAvailable.value = false;
// 检测浏览器是否支持mediaDevices
if (navigator.mediaDevices) {
navigator.mediaDevices
// 开启视频,关闭音频
.getUserMedia({
audio: false,
video: {
deviceId:
selectedCamera.value !== null
? (cameraList.value[selectedCamera.value]?.deviceId ?? undefined)
: undefined
}
})
.then((stream) => {
isCameraAvailable.value = true;
// 将视频流传入viedo控件
videoRef.value.srcObject = stream;
Stream.value = stream;
// 播放
videoRef.value.play();
})
.catch((err) => {
isCameraAvailable.value = false;
console.error("摄像头开启失败:" + err.message);
if (err.message.includes("Requested device not found")) {
cameraStatus.value = "未找到摄像头,请检查摄像头连接状态";
} else if (err.message.includes("Permission denied")) {
cameraStatus.value = "用户拒绝了摄像头权限,请允许浏览器使用摄像头";
} else {
cameraStatus.value = err.message;
}
ElMessage.warning({
message: `摄像头开启失败:${cameraStatus.value}`,
duration: 3000
});
})
.finally(() => {
isLoadingCamera.value = false;
});
} else {
ElMessage.error({
message: "该浏览器或所处环境不支持开启摄像头,请更换最新版Chrome浏览器",
duration: 3000
});
isLoadingCamera.value = false;
}
}
/** 点击拍照 */
function takePhoto() {
// 如果已经拍照了就重新启动摄像头
if (imageURL.value) {
openCameraStream();
return;
}
// 设置画布大小与摄像大小一致
canvasRef.value.width = videoRef.value.videoWidth;
canvasRef.value.height = videoRef.value.videoHeight;
// 执行画的操作
canvasRef.value.getContext("2d").drawImage(videoRef.value, 0, 0);
// 将结果转换为可展示的格式
imageURL.value = canvasRef.value.toDataURL("image/png");
// 将结果转换为二进制数据
canvasRef.value.toBlob((blob) => {
imageBlob.value = blob;
});
// 关闭摄像头
// stop();
}
/** 关闭摄像头 */
function stop() {
let stream = videoRef.value?.srcObject ?? Stream.value ?? null;
Stream.value = null;
if (!stream) return;
stream.getTracks().forEach((x) => x.stop());
}
/** 点击上传图片 */
function handleImageUpload() {
emits("upload", imageBlob.value);
}
/** 切换提示显示或隐藏 */
function handleTipsBtnClick() {
isTipsVisiable.value = !isTipsVisiable.value;
localStorage.setItem("isTipsVisiable", isTipsVisiable.value);
}
/** 关闭提示 */
function handleTipsClose() {
isTipsVisiable.value = false;
localStorage.setItem("isTipsVisiable", "false");
}
/** 初始化时获取摄像头列表 */
// onMounted(() => {
// getCameraList(true);
// });
/** 组件销毁时关闭摄像头 */
onBeforeUnmount(() => {
stop();
});
/** 向组件外面暴露的方法 */
defineExpose({ openCamera, stop });
.main {
width: 100%;
min-height: 570px;
}
.preview-area {
height: 800px;
border-radius: 12px;
box-shadow:
rgba(14, 30, 37, 0.12) 0px 2px 4px 0px,
rgba(14, 30, 37, 0.32) 0px 2px 16px 0px;
:deep(.el-loading-mask) {
left: -1px;
.el-loading-text {
font-size: 20px;
}
}
}