From 94f12f0c95d9c206000dcddcefc5c10cb3ce514d Mon Sep 17 00:00:00 2001 From: kdletters Date: Fri, 19 Jun 2026 16:39:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5=E4=B8=AA=E4=BA=BA=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E5=8E=9F=E7=94=9F=E6=89=AB=E7=A0=81=E4=BC=98=E5=85=88?= =?UTF-8?q?=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 让个人中心扫码弹层优先调用 scanner.scanQrCode HostBridge 能力 保留宿主不支持或异常时的浏览器摄像头扫码回退 取消原生扫码时关闭弹层且不连带打开 H5 摄像头 增加扫码弹层 HostBridge 优先测试并纳入原生壳总门禁 --- scripts/check-native-shells.mjs | 2 + .../PlatformProfileQrScannerModal.test.tsx | 56 +++++++++++++++++++ .../PlatformProfileQrScannerModal.tsx | 26 ++++++++- 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/scripts/check-native-shells.mjs b/scripts/check-native-shells.mjs index 8a8b3c357..83e40a637 100644 --- a/scripts/check-native-shells.mjs +++ b/scripts/check-native-shells.mjs @@ -77,6 +77,7 @@ const h5HostBridgeRequiredCallChainFiles = [ 'src/components/platform-entry/PlatformEntryFlowShellImpl.tsx', 'src/components/platform-entry/PlatformFeedbackView.tsx', 'src/components/platform-entry/PlatformProfilePrimitives.tsx', + 'src/components/platform-entry/PlatformProfileQrScannerModal.tsx', 'src/components/platform-entry/PlatformProfileReferralModal.tsx', 'src/components/platform-entry/PlatformProfileRewardCodeRedeemModal.tsx', 'src/components/platform-entry/usePlatformProfileCenterController.ts', @@ -430,6 +431,7 @@ const h5HostBridgeTests = [ 'src/components/bark-battle-creation/BarkBattleResultView.test.tsx', 'src/components/common/CreativeAudioInputPanel.test.tsx', 'src/components/common/PublishShareModal.test.tsx', + 'src/components/platform-entry/PlatformProfileQrScannerModal.test.tsx', 'src/components/creation-agent/CreationAgentWorkspace.test.tsx', 'src/components/visual-novel-result/VisualNovelResultView.test.tsx', 'src/components/platform-entry/platformDraftGenerationShelfModel.test.ts', diff --git a/src/components/platform-entry/PlatformProfileQrScannerModal.test.tsx b/src/components/platform-entry/PlatformProfileQrScannerModal.test.tsx index 12f6ecbf2..0a0373640 100644 --- a/src/components/platform-entry/PlatformProfileQrScannerModal.test.tsx +++ b/src/components/platform-entry/PlatformProfileQrScannerModal.test.tsx @@ -3,6 +3,7 @@ import { act, render, screen } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import * as hostBridgeServices from '../../services/host-bridge/hostBridge'; import { PlatformProfileQrScannerModal } from './PlatformProfileQrScannerModal'; type MockTrack = { @@ -49,6 +50,7 @@ describe('PlatformProfileQrScannerModal', () => { }); test('detects qr result and stops camera tracks', async () => { + vi.spyOn(hostBridgeServices, 'scanHostQrCode').mockResolvedValue(false); const stop = vi.fn(); const stream = buildStream([{ stop }]); const getUserMedia = vi.fn().mockResolvedValue(stream); @@ -84,6 +86,7 @@ describe('PlatformProfileQrScannerModal', () => { }); test('releases camera resource when modal unmounts before recognition', async () => { + vi.spyOn(hostBridgeServices, 'scanHostQrCode').mockResolvedValue(false); const stop = vi.fn(); const stream = buildStream([{ stop }]); const getUserMedia = vi.fn().mockResolvedValue(stream); @@ -112,6 +115,59 @@ describe('PlatformProfileQrScannerModal', () => { expect(stop).toHaveBeenCalledTimes(1); expect(screen.queryByRole('dialog', { name: '扫码' })).toBeNull(); }); + + test('uses native QR scanner before browser camera', async () => { + const getUserMedia = vi.fn(); + const onResult = vi.fn(); + vi.spyOn(hostBridgeServices, 'scanHostQrCode').mockResolvedValue({ + value: 'https://app.genarrative.world/works/detail?work=PZ-1', + format: 'qr_code', + }); + installMediaDevices(getUserMedia); + + render( + , + ); + + await act(async () => { + await flushPromises(); + }); + + expect(onResult).toHaveBeenCalledWith( + 'https://app.genarrative.world/works/detail?work=PZ-1', + ); + expect(getUserMedia).not.toHaveBeenCalled(); + }); + + test('closes scanner without browser fallback when native scan is cancelled', async () => { + const getUserMedia = vi.fn(); + const onClose = vi.fn(); + vi.spyOn(hostBridgeServices, 'scanHostQrCode').mockResolvedValue(null); + installMediaDevices(getUserMedia); + + render( + , + ); + + await act(async () => { + await flushPromises(); + }); + + expect(onClose).toHaveBeenCalledTimes(1); + expect(getUserMedia).not.toHaveBeenCalled(); + }); }); function buildStream(tracks: MockTrack[]): MockStream { diff --git a/src/components/platform-entry/PlatformProfileQrScannerModal.tsx b/src/components/platform-entry/PlatformProfileQrScannerModal.tsx index a36d813f7..a59c587ab 100644 --- a/src/components/platform-entry/PlatformProfileQrScannerModal.tsx +++ b/src/components/platform-entry/PlatformProfileQrScannerModal.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react'; import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; +import { scanHostQrCode } from '../../services/host-bridge/hostBridge'; import { PlatformProfileModalShell } from './PlatformProfileModalShell'; const PROFILE_QR_SCAN_INTERVAL_MS = 360; @@ -102,7 +103,7 @@ export function PlatformProfileQrScannerModal({ } }; - const startCamera = async () => { + const startBrowserCamera = async () => { if ( typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia @@ -136,7 +137,28 @@ export function PlatformProfileQrScannerModal({ } }; - void startCamera(); + 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;