fix: 拍照提问摄像头画面倒置、下拉菜单换成前后切换按钮

真机拍照实测画面上下颠倒(Mac mini 的苹果标志倒着)。CameraCapture.vue
的 <video> 加 transform: rotate(180deg) 转正预览;capture() 画到 canvas
时同步做相同的 180° 旋转(translate 到右下角再 rotate(Math.PI)),
不然预览是正的、实际拍下来的照片还是倒的。

摄像头选择从下拉菜单换成一个悬浮在预览右上角的圆形切换按钮
(switchCamera(),在设备列表里循环切换,只有 >1 个摄像头时才显示)。
默认摄像头沿用之前"取设备列表第一个"的逻辑不变——真机实测这个正好是
朝外拍摄的那颗(另一颗是朝向操作者的),语义上更接近用户要的默认
"后置摄像头",暂不改动。

真机验证:预览转正、切换按钮能真实切到另一颗摄像头且画面同样是正的、
拍照后的照片方向跟预览一致(不会出现预览正但存图倒的情况)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ssurui 2026-09-03 16:02:31 +08:00
parent 8054fcdad1
commit 0d19be58c3

View File

@ -72,10 +72,23 @@ function capture(): void {
canvas.height = video.videoHeight
const ctx = canvas.getContext('2d')
if (!ctx) return
// 这台设备的摄像头画面是倒置的(见下方 .cam-preview-video 的 CSS 旋转),
// 拍出来的照片也要同步转正,不然预览是正的、存下来的图是倒的
ctx.save()
ctx.translate(canvas.width, canvas.height)
ctx.rotate(Math.PI)
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
ctx.restore()
capturedDataUrl.value = canvas.toDataURL('image/jpeg', 0.85)
}
function switchCamera(): void {
if (devices.value.length < 2) return
const idx = devices.value.findIndex((d) => d.deviceId === selectedDeviceId.value)
const nextIdx = (idx + 1) % devices.value.length
switchDevice(devices.value[nextIdx].deviceId)
}
function retake(): void {
capturedDataUrl.value = ''
}
@ -114,22 +127,23 @@ onUnmounted(() => stopStream())
<div v-if="errorMessage" class="cam-error">{{ errorMessage }}</div>
<div v-show="!capturedDataUrl" class="cam-preview-wrap">
<video ref="videoRef" class="cam-preview" autoplay muted playsinline />
<video ref="videoRef" class="cam-preview cam-preview-video" autoplay muted playsinline />
<div v-if="starting" class="cam-loading">正在打开摄像头...</div>
<button
v-if="devices.length > 1"
class="cam-switch-btn"
title="切换前后摄像头"
@click="switchCamera"
>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/>
</svg>
</button>
</div>
<img v-if="capturedDataUrl" :src="capturedDataUrl" class="cam-preview" alt="拍摄预览" />
<canvas ref="canvasRef" style="display:none" />
<div v-if="devices.length > 1 && !capturedDataUrl" class="cam-device-row">
<label class="fl">摄像头</label>
<select class="s-inp" :value="selectedDeviceId" @change="switchDevice(($event.target as HTMLSelectElement).value)">
<option v-for="d in devices" :key="d.deviceId" :value="d.deviceId">
{{ d.label || `摄像头 ${d.deviceId.slice(0, 6)}` }}
</option>
</select>
</div>
</div>
<template #footer>
@ -154,17 +168,25 @@ onUnmounted(() => stopStream())
width: 100%; max-height: 60vh; object-fit: contain;
background: #000; border-radius: var(--radius-sm);
}
/* 这台设备的摄像头画面是倒置的,转正预览;capture() 里对拍照结果做了同样的旋转 */
.cam-preview-video { transform: rotate(180deg); }
.cam-loading {
position: absolute; inset: 0; display: flex;
align-items: center; justify-content: center;
color: #fff; font-size: 15px; background: rgba(0,0,0,.35);
border-radius: var(--radius-sm);
}
.cam-switch-btn {
position: absolute; top: 14px; right: 14px;
width: 44px; height: 44px; border-radius: 50%;
border: none; background: rgba(0,0,0,.45); color: #fff;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
}
.cam-switch-btn:hover { background: rgba(0,0,0,.65); }
.cam-error {
color: var(--red); background: #FBEAE8;
border-radius: var(--radius-sm); padding: 12px 14px; font-size: 15px;
}
.cam-device-row { display: flex; align-items: center; gap: 12px; }
.cam-device-row .fl { flex-shrink: 0; font-size: 15px; color: var(--ink-2); }
.cam-actions { display: flex; justify-content: flex-end; gap: 12px; }
</style>