Appearance
BizCameraCapture 摄像头采集
基于 getUserMedia 的现场拍照组件:拍照 / 连拍(会话缩略图管理)、前后摄像头切换、镜像、分辨率与压缩输出、取景引导框(人脸圆框 / 证件人像框 / 三分线)、倒计时自拍;权限拒绝 / 无设备 / 非 HTTPS 等环境均有明确降级提示(error 事件可接管)。
适用于人脸 / 头像现场采集、证照翻拍归档、设备铭牌上报、门店巡检、售后举证。与 BizAvatarCropper 组成「拍照 → 裁剪」流水线。
浏览器要求:HTTPS 或 localhost;首次使用需授权摄像头权限。
基础拍照
(未采集)需要浏览器授权摄像头
弹窗采集单张:
v-model:visible + confirm 事件回传 BizCameraShot[](含 dataURL / Blob / 时间戳)。<script setup lang="ts">
/** 场景:单张现场拍照——弹窗采集,确认后回显 */
import { ref } from 'vue'
import { ElButton, ElMessage } from 'element-plus'
import { BizCameraCapture } from 'vue3-ep-biz-lib'
import type { BizCameraShot } from 'vue3-ep-biz-lib'
const visible = ref(false)
const photo = ref<BizCameraShot | null>(null)
const onConfirm = (shots: BizCameraShot[]): void => {
photo.value = shots[0] ?? null
ElMessage.success(`采集完成(${(photo.value.blob.size / 1024).toFixed(0)} KB)`)
}
</script>
<template>
<div style="display: flex; align-items: center; gap: 16px">
<ElButton type="primary" @click="visible = true">现场拍照</ElButton>
<img v-if="photo" :src="photo.dataUrl" alt="现场照片" style="height: 96px; border-radius: 6px; border: var(--el-border)" />
<span v-else style="font-size: 13px; color: var(--el-text-color-secondary)">(未采集)需要浏览器授权摄像头</span>
<BizCameraCapture v-model:visible="visible" :max-shots="1" title="设备铭牌拍照" @confirm="onConfirm" />
</div>
</template>
显示代码
拍照 → 头像裁剪流水线
拍摄 → 圆形裁剪 → 72px 头像
guide="face" 人脸圆框引导采集,原图直接交给 BizAvatarCropper 裁出圆形头像。<script setup lang="ts">
/** 场景:拍照 → 裁剪流水线——采集原图进 BizAvatarCropper 裁出圆形头像 */
import { ref } from 'vue'
import { ElAvatar, ElButton } from 'element-plus'
import { BizAvatarCropper, BizCameraCapture } from 'vue3-ep-biz-lib'
import type { BizCameraShot } from 'vue3-ep-biz-lib'
const cameraVisible = ref(false)
const cropperVisible = ref(false)
const rawSrc = ref('')
const avatar = ref('')
const onConfirm = (shots: BizCameraShot[]): void => {
rawSrc.value = shots[0]?.dataUrl ?? ''
if (rawSrc.value !== '') cropperVisible.value = true
}
</script>
<template>
<div style="display: flex; align-items: center; gap: 16px">
<ElButton type="primary" @click="cameraVisible = true">拍摄头像</ElButton>
<ElAvatar v-if="avatar" :size="72" :src="avatar" />
<span v-else style="font-size: 13px; color: var(--el-text-color-secondary)">拍摄 → 圆形裁剪 → 72px 头像</span>
<BizCameraCapture v-model:visible="cameraVisible" guide="face" :max-shots="1" title="拍摄头像" @confirm="onConfirm" />
<BizAvatarCropper
v-model:visible="cropperVisible"
:src="rawSrc"
:ratio="1"
preview-shape="circle"
:output-width="144"
output-type="base64"
@cropped="(payload) => (avatar = payload.base64)"
/>
</div>
</template>
显示代码
巡检连拍
已上报 0 张
max-shots 连拍上限;面板内缩略图可单删 / 清空,shots-change 实时回传清单。<script setup lang="ts">
/** 场景:巡检连拍——多张采集(上限 6)、会话内缩略图单删 / 清空 */
import { ref } from 'vue'
import { ElButton, ElMessage } from 'element-plus'
import { BizCameraCapture } from 'vue3-ep-biz-lib'
import type { BizCameraShot } from 'vue3-ep-biz-lib'
const visible = ref(false)
const shots = ref<BizCameraShot[]>([])
const onConfirm = (list: BizCameraShot[]): void => {
shots.value = list
ElMessage.success(`共采集 ${list.length} 张,准备随工单上报`)
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<div style="display: flex; align-items: center; gap: 12px">
<ElButton type="primary" @click="visible = true">巡检拍照(最多 6 张)</ElButton>
<span style="font-size: 13px; color: var(--el-text-color-secondary)">已上报 {{ shots.length }} 张</span>
</div>
<div v-if="shots.length > 0" style="display: flex; gap: 8px; flex-wrap: wrap">
<img v-for="shot in shots" :key="shot.seq" :src="shot.dataUrl" alt="巡检照片" style="height: 64px; border-radius: 6px; border: var(--el-border)" />
</div>
<BizCameraCapture v-model:visible="visible" :max-shots="6" title="巡检采集" @confirm="onConfirm" />
</div>
</template>
显示代码
前后摄像头与镜像
镜像
面板内「翻转摄像头」切换前后置(移动端生效)面板内「翻转摄像头」切换前置 / 后置(移动端);
mirror 控制自拍镜像(默认开,所见即所得输出)。<script setup lang="ts">
/** 场景:前后摄像头切换 + 镜像开关(自拍镜像 / 翻拍不镜像) */
import { ref } from 'vue'
import { ElButton, ElSwitch } from 'element-plus'
import { BizCameraCapture } from 'vue3-ep-biz-lib'
const visible = ref(false)
const mirror = ref(true)
</script>
<template>
<div style="display: flex; align-items: center; gap: 16px">
<ElButton type="primary" @click="visible = true">证照翻拍</ElButton>
<ElSwitch v-model="mirror" active-text="镜像" />
<span style="font-size: 13px; color: var(--el-text-color-secondary)">面板内「翻转摄像头」切换前后置(移动端生效)</span>
<BizCameraCapture v-model:visible="visible" :mirror="mirror" guide="id-card" :max-shots="2" title="证照翻拍" />
</div>
</template>
显示代码
压缩输出
output-width 等比缩放(1280 采 → 640 出)+ quality JPEG 质量——省流上报场景。<script setup lang="ts">
/** 场景:压缩输出——outputWidth 等比缩放 + JPEG 质量(省流上报) */
import { ref } from 'vue'
import { ElButton, ElMessage } from 'element-plus'
import { BizCameraCapture } from 'vue3-ep-biz-lib'
import type { BizCameraShot } from 'vue3-ep-biz-lib'
const visible = ref(false)
const shot = ref<BizCameraShot | null>(null)
const onConfirm = (shots: BizCameraShot[]): void => {
shot.value = shots[0] ?? null
if (shot.value) ElMessage.success(`压缩后 ${(shot.value.blob.size / 1024).toFixed(0)} KB(原始 1280 宽 → 640 宽)`)
}
</script>
<template>
<div style="display: flex; align-items: center; gap: 16px">
<ElButton type="primary" @click="visible = true">压缩拍摄(640px / 质量 0.7)</ElButton>
<img v-if="shot" :src="shot.dataUrl" alt="压缩照片" style="height: 96px; border-radius: 6px; border: var(--el-border)" />
<BizCameraCapture v-model:visible="visible" :output-width="640" :quality="0.7" :max-shots="1" @confirm="onConfirm" />
</div>
</template>
显示代码
倒计时拍摄
面板内点「倒计时 3s 拍摄」,取景区中央倒数
countdown=3:面板出现「倒计时 3s 拍摄」按钮,取景区中央倒数后自动拍摄——双手持证留拍。<script setup lang="ts">
/** 场景:倒计时自拍——3 秒倒拍,适合双手持证 / 全景留证 */
import { ref } from 'vue'
import { ElButton } from 'element-plus'
import { BizCameraCapture } from 'vue3-ep-biz-lib'
const visible = ref(false)
</script>
<template>
<div style="display: flex; align-items: center; gap: 16px">
<ElButton type="primary" @click="visible = true">倒计时拍摄(3s)</ElButton>
<span style="font-size: 13px; color: var(--el-text-color-secondary)">面板内点「倒计时 3s 拍摄」,取景区中央倒数</span>
<BizCameraCapture v-model:visible="visible" :countdown="3" :max-shots="1" title="留证拍摄" />
</div>
</template>
显示代码
取景引导框
三种引导:
face 人脸圆框(遮罩聚焦)/ id-card 证件人像框 / third 三分构图线,均带场景提示文案。<script setup lang="ts">
/** 场景:三种引导框——人脸圆框 / 证件人像框 / 三分构图线 */
import { ref } from 'vue'
import { ElButton, ElRadioButton, ElRadioGroup } from 'element-plus'
import { BizCameraCapture } from 'vue3-ep-biz-lib'
import type { BizCameraGuide } from 'vue3-ep-biz-lib'
const visible = ref(false)
const guide = ref<BizCameraGuide>('face')
</script>
<template>
<div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap">
<ElRadioGroup v-model="guide">
<ElRadioButton value="face">人脸框</ElRadioButton>
<ElRadioButton value="id-card">证件框</ElRadioButton>
<ElRadioButton value="third">三分线</ElRadioButton>
</ElRadioGroup>
<ElButton type="primary" @click="visible = true">开启取景</ElButton>
<BizCameraCapture v-model:visible="visible" :guide="guide" :max-shots="1" title="取景引导" />
</div>
</template>
显示代码
工单表单内嵌
「现场照片」表单项:多张采集(上限 4)+ 缩略图预览,随工单提交。
<script setup lang="ts">
/** 场景:工单表单内嵌——「现场照片」字段多张采集,随表单一起提交 */
import { reactive, ref } from 'vue'
import { ElButton, ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus'
import { BizCameraCapture } from 'vue3-ep-biz-lib'
import type { BizCameraShot } from 'vue3-ep-biz-lib'
const visible = ref(false)
const photos = ref<BizCameraShot[]>([])
const form = reactive({ title: '', location: '' })
const onConfirm = (shots: BizCameraShot[]): void => {
photos.value = shots
ElMessage.success(`已附 ${shots.length} 张现场照片`)
}
</script>
<template>
<ElForm label-width="90px" style="max-width: 560px">
<ElFormItem label="工单标题">
<ElInput v-model="form.title" placeholder="如:3 号机组异响" />
</ElFormItem>
<ElFormItem label="位置">
<ElInput v-model="form.location" placeholder="车间 / 线体" />
</ElFormItem>
<ElFormItem label="现场照片">
<div style="display: flex; flex-direction: column; gap: 10px">
<ElButton @click="visible = true">{{ photos.length > 0 ? `重新采集(已 ${photos.length} 张)` : '拍照采集' }}</ElButton>
<div v-if="photos.length > 0" style="display: flex; gap: 8px; flex-wrap: wrap">
<img v-for="shot in photos" :key="shot.seq" :src="shot.dataUrl" alt="现场照片" style="height: 60px; border-radius: 6px; border: var(--el-border)" />
</div>
</div>
</ElFormItem>
<ElFormItem>
<ElButton type="primary" @click="ElMessage.success('工单已提交')">提交工单</ElButton>
</ElFormItem>
</ElForm>
<BizCameraCapture v-model:visible="visible" :max-shots="4" title="现场照片采集" @confirm="onConfirm" />
</template>
显示代码
环境降级监听
拒绝权限提示后观察降级文案与 error 事件
error 事件:(未发生错误)error 事件回传错误码(denied / unsupported / notfound / insecure / error)与文案——业务侧可上报或引导。<script setup lang="ts">
/** 场景:环境降级——权限拒绝 / 无设备 / 非 https 等错误事件监听 */
import { ref } from 'vue'
import { ElButton, ElMessage } from 'element-plus'
import { BizCameraCapture } from 'vue3-ep-biz-lib'
const visible = ref(false)
const lastError = ref('(未发生错误)')
const onError = (code: string, message: string): void => {
lastError.value = `${code}:${message}`
ElMessage.warning(message)
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px">
<div style="display: flex; align-items: center; gap: 16px">
<ElButton type="primary" @click="visible = true">开启摄像头</ElButton>
<span style="font-size: 13px; color: var(--el-text-color-secondary)">拒绝权限提示后观察降级文案与 error 事件</span>
</div>
<span style="font-size: 12px; color: var(--el-text-color-secondary)">error 事件:{{ lastError }}</span>
<BizCameraCapture v-model:visible="visible" :max-shots="1" @error="onError" />
</div>
</template>
显示代码
属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| visible / v-model:visible | boolean | false | 弹窗显隐(开启自动拉流) |
| guide | 'face' | 'id-card' | 'third' | 'none' | 'none' | 取景引导框 |
| mirror | boolean | true | 镜像(自拍语义,输出一致) |
| width / height | number | 1280 / 720 | 请求的采集分辨率(ideal) |
| output-width | number | 0 | 输出宽(等比压缩;0 原生) |
| quality | number | 0.9 | JPEG 质量 0~1 |
| max-shots | number | 9 | 连拍上限(0 不限) |
| countdown | number | 0 | 倒计时秒数(0 关闭) |
| title | string | '拍照采集' | 弹窗标题 |
| stop-on-close | boolean | true | 关闭时自动停止流 |
事件
| 事件名 | 参数 | 说明 |
|---|---|---|
| update:visible | (visible: boolean) | 弹窗显隐 |
| open | — | 流开启成功 |
| close | — | 弹窗关闭 |
| shot | (shot: BizCameraShot) | 单张拍成 |
| shots-change | (shots: BizCameraShot[]) | 连拍清单变化 |
| confirm | (shots: BizCameraShot[]) | 点「完成」回传全部并关窗 |
| error | (code, message) | denied / unsupported / notfound / insecure / error |
ts
interface BizCameraShot {
seq: number // 会话内递增序号
dataUrl: string // JPEG dataURL
blob: Blob // Blob 对象(可直接 FormData 上传)
takenAt: number // 拍摄时间戳
}1
2
3
4
5
6
2
3
4
5
6
插槽
| 插槽名 | 说明 |
|---|---|
| — | 无插槽 |
Expose
| 名称 | 类型 | 说明 |
|---|---|---|
| start | () => Promise<void> | 手动开启摄像头 |
| stop | () => void | 停止流 |
| capture | () => void | 程序化拍照 |
| switchDevice | () => Promise<void> | 切换前后摄像头 |
| getShots | () => BizCameraShot[] | 当前连拍清单副本 |