Files
Genarrative/src/components/platform-entry/PlatformProfileQrScannerModal.tsx
T
kdletters 387a1c26e3 完成全量检查整改
清理已跟踪原始日志与个人本地配置
修复前后端有效门禁、测试和构建问题
补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束
收紧图片编辑器状态、附件与媒体引用测试
排除已下线旧创作入口测试并清理 warning
2026-07-17 16:56:46 +08:00

223 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef } from 'react';
import { scanHostQrCode } from '../../services/host-bridge/hostBridge';
import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformProfileModalShell } from './PlatformProfileModalShell';
const PROFILE_QR_SCAN_INTERVAL_MS = 360;
type BarcodeDetectorLike = {
detect: (source: CanvasImageSource) => Promise<Array<{ rawValue?: string }>>;
};
type BarcodeDetectorConstructorLike = new (options?: {
formats?: string[];
}) => BarcodeDetectorLike;
export type PlatformProfileQrScannerModalProps = {
error: string | null;
mode?: 'camera' | 'resultOnly';
result: string | null;
onClose: () => void;
onError: (message: string) => void;
onResult: (value: string) => void;
};
function getBarcodeDetectorConstructor(): BarcodeDetectorConstructorLike | null {
const maybeDetector = (
globalThis as unknown as {
BarcodeDetector?: BarcodeDetectorConstructorLike;
}
).BarcodeDetector;
return typeof maybeDetector === 'function' ? maybeDetector : null;
}
/**
* 个人中心共享扫码弹层。
* 保持首页现有扫码语义:申请摄像头、轮询识别、关闭时释放视频流。
*/
export function PlatformProfileQrScannerModal({
error,
mode = 'camera',
result,
onClose,
onError,
onResult,
}: PlatformProfileQrScannerModalProps) {
const videoRef = useRef<HTMLVideoElement | null>(null);
const streamRef = useRef<MediaStream | null>(null);
useEffect(() => {
const videoElement = videoRef.current;
if (!videoElement || mode === 'resultOnly') {
return;
}
let isMounted = true;
let scanTimer: number | null = null;
const detectorCtor = getBarcodeDetectorConstructor();
const detector = detectorCtor
? new detectorCtor({ formats: ['qr_code'] })
: null;
const clearScanTimer = () => {
if (scanTimer !== null) {
window.clearTimeout(scanTimer);
scanTimer = null;
}
};
const stopCamera = () => {
const stream = streamRef.current;
streamRef.current = null;
if (stream) {
stream.getTracks().forEach((track) => track.stop());
}
videoElement.srcObject = null;
};
const scanVideo = async () => {
if (!isMounted || !detector || videoElement.readyState < 2) {
if (isMounted && detector) {
scanTimer = window.setTimeout(scanVideo, PROFILE_QR_SCAN_INTERVAL_MS);
}
return;
}
try {
const codes = await detector.detect(videoElement);
const rawValue = codes[0]?.rawValue?.trim();
if (rawValue) {
clearScanTimer();
stopCamera();
onResult(rawValue);
return;
}
} catch {
onError('扫码识别失败,请调整二维码位置');
}
if (isMounted) {
scanTimer = window.setTimeout(scanVideo, PROFILE_QR_SCAN_INTERVAL_MS);
}
};
const startBrowserCamera = async () => {
if (
typeof navigator === 'undefined' ||
!navigator.mediaDevices?.getUserMedia
) {
onError('当前浏览器不支持摄像头扫码');
return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: { facingMode: { ideal: 'environment' } },
});
if (!isMounted) {
stream.getTracks().forEach((track) => track.stop());
return;
}
streamRef.current?.getTracks().forEach((track) => track.stop());
streamRef.current = stream;
videoElement.srcObject = stream;
await videoElement.play();
if (!detector) {
onError('当前浏览器暂不支持二维码识别');
return;
}
scanTimer = window.setTimeout(scanVideo, PROFILE_QR_SCAN_INTERVAL_MS);
} catch {
onError('无法打开摄像头,请检查权限');
}
};
const startScanner = async () => {
try {
const nativeResult = await scanHostQrCode();
if (!isMounted) {
return;
}
if (nativeResult) {
onResult(nativeResult.value);
return;
}
if (nativeResult === null) {
onClose();
return;
}
} catch {
// 浏览器摄像头路径继续承接不支持或异常宿主。
}
await startBrowserCamera();
};
void startScanner();
return () => {
isMounted = false;
clearScanTimer();
stopCamera();
};
}, [mode, onClose, onError, onResult]);
return (
<PlatformProfileModalShell
title="扫码"
onClose={onClose}
showHeader={false}
showCloseButton={false}
size="sm"
panelClassName="platform-qr-scanner-modal !max-w-sm rounded-[1.4rem]"
bodyClassName="!p-0"
>
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
<div className="text-base font-black">扫码</div>
<PlatformModalCloseButton
label="关闭扫码"
onClick={onClose}
icon="×"
/>
</div>
<div className="space-y-3 px-5 py-5">
{mode === 'camera' ? (
<div className="platform-qr-scanner-modal__viewport">
<video
ref={videoRef}
className="h-full w-full object-cover"
playsInline
muted
/>
<span className="platform-qr-scanner-modal__frame" />
</div>
) : null}
{result ? (
<PlatformStatusMessage
tone="success"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
已识别:{result}
</PlatformStatusMessage>
) : error ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
{error}
</PlatformStatusMessage>
) : null}
</div>
</PlatformProfileModalShell>
);
}