From 0f72af91175afb87bccafa53867400b277030306 Mon Sep 17 00:00:00 2001 From: qiin <414382190@qq.com> Date: Mon, 22 Jun 2026 10:27:40 +0800 Subject: [PATCH] fix(scan): guard QR scanning on unsupported devices --- .../main/ets/components/CustomKeyOverlay.ets | 40 ++++++++++++++----- .../main/ets/components/PcListTitleBar.ets | 37 +++++++++-------- entry/src/main/ets/pages/PcListPageV2.ets | 12 ++++++ .../ets/service/network/QrShareService.ets | 26 ++++++++++++ 4 files changed, 89 insertions(+), 26 deletions(-) diff --git a/entry/src/main/ets/components/CustomKeyOverlay.ets b/entry/src/main/ets/components/CustomKeyOverlay.ets index c4c5273..72f2765 100644 --- a/entry/src/main/ets/components/CustomKeyOverlay.ets +++ b/entry/src/main/ets/components/CustomKeyOverlay.ets @@ -154,6 +154,7 @@ export struct CustomKeyOverlay { @State private qrError: string = ''; @State private shareJson: string = ''; @State private qrDataLen: number = 0; + @State private canScanQrCode: boolean = true; /** 扫码导入预览 */ @State private importPreviewName: string = ''; @State private importPreviewCount: number = 0; @@ -237,6 +238,7 @@ export struct CustomKeyOverlay { await CustomKeyStore.init(); this.keys = await CustomKeyStore.getKeys(); this.activeProfileName = await CustomKeyStore.getActiveProfileName(); + this.canScanQrCode = QrShareService.isScanQrCodeSupported(); // 延迟刷新命中测试区域(与 VirtualControllerOverlay 同理) setTimeout(() => { @@ -2112,6 +2114,15 @@ export struct CustomKeyOverlay { /** 调用系统相机扫码导入 */ private async scanAndImport(): Promise { + if (!QrShareService.isScanQrCodeSupported()) { + ToastQueue.show({ + message: QrShareService.getScanUnsupportedMessage() + ',请使用剪贴板导入', + duration: 2500 + }); + this.canScanQrCode = false; + return; + } + const result = await QrShareService.scanQrCode(); if (!result) { ToastQueue.show({ message: '扫码取消或失败', duration: 1500 }); @@ -2453,12 +2464,14 @@ export struct CustomKeyOverlay { Text('导入配置') .fontSize(12).fontColor(OV_TEXT_MEDIUM) - Text('扫码导入') - .fontSize(13).fontColor(OV_TEXT_WHITE).fontWeight(FontWeight.Medium) - .width(130).height(36).textAlign(TextAlign.Center) - .borderRadius(18).backgroundColor('#6098C0') - .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.9 }) - .onClick(() => { this.scanAndImport(); }) + if (this.canScanQrCode) { + Text('扫码导入') + .fontSize(13).fontColor(OV_TEXT_WHITE).fontWeight(FontWeight.Medium) + .width(130).height(36).textAlign(TextAlign.Center) + .borderRadius(18).backgroundColor('#6098C0') + .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.9 }) + .onClick(() => { this.scanAndImport(); }) + } Text('从剪贴板导入') .fontSize(13).fontColor(OV_TEXT_WHITE).fontWeight(FontWeight.Medium) @@ -2469,10 +2482,17 @@ export struct CustomKeyOverlay { Divider().color('#20FFFFFF').width(100).margin({ top: 4, bottom: 4 }) - Text('扫描其他设备上的') - .fontSize(10).fontColor(OV_TEXT_DIM) - Text('配置二维码即可导入') - .fontSize(10).fontColor(OV_TEXT_DIM) + if (this.canScanQrCode) { + Text('扫描其他设备上的') + .fontSize(10).fontColor(OV_TEXT_DIM) + Text('配置二维码即可导入') + .fontSize(10).fontColor(OV_TEXT_DIM) + } else { + Text('当前设备可使用') + .fontSize(10).fontColor(OV_TEXT_DIM) + Text('剪贴板导入配置') + .fontSize(10).fontColor(OV_TEXT_DIM) + } } } .padding({ left: 16, right: 16, top: 8, bottom: 16 }) diff --git a/entry/src/main/ets/components/PcListTitleBar.ets b/entry/src/main/ets/components/PcListTitleBar.ets index 83925fe..9c6d0ab 100644 --- a/entry/src/main/ets/components/PcListTitleBar.ets +++ b/entry/src/main/ets/components/PcListTitleBar.ets @@ -22,6 +22,8 @@ export interface TitleBarConfig { onlineCount: number; /** 总设备数量 */ totalCount: number; + /** 是否显示扫码导入入口 */ + showScanImport?: boolean; /** 关于按钮点击回调 */ onAboutClick: () => void; /** 扫码导入按钮点击回调 */ @@ -39,6 +41,7 @@ export struct PcListTitleBar { @Prop config: TitleBarConfig = { onlineCount: 0, totalCount: 0, + showScanImport: true, onAboutClick: () => {}, onScanImport: () => {}, onSettingsClick: () => {} @@ -111,23 +114,25 @@ export struct PcListTitleBar { this.config.onAboutClick(); }) - // 扫码导入按钮 - Button({ type: ButtonType.Circle }) { - Image($r('app.media.ic_scan')) - .width(AppSizes.IconMedium) - .height(AppSizes.IconMedium) - .fillColor(AppColors.Primary) + if (this.config.showScanImport !== false) { + // 扫码导入按钮 + Button({ type: ButtonType.Circle }) { + Image($r('app.media.ic_scan')) + .width(AppSizes.IconMedium) + .height(AppSizes.IconMedium) + .fillColor(AppColors.Primary) + } + .width(44) + .height(44) + .backgroundColor(AppColors.Surface) + .borderWidth(1) + .borderColor(AppColors.CardBorder) + .flexShrink(0) + .margin({ right: AppSpacing.Small }) + .onClick(() => { + this.config.onScanImport(); + }) } - .width(44) - .height(44) - .backgroundColor(AppColors.Surface) - .borderWidth(1) - .borderColor(AppColors.CardBorder) - .flexShrink(0) - .margin({ right: AppSpacing.Small }) - .onClick(() => { - this.config.onScanImport(); - }) // 设置按钮 Button({ type: ButtonType.Circle }) { diff --git a/entry/src/main/ets/pages/PcListPageV2.ets b/entry/src/main/ets/pages/PcListPageV2.ets index b2024d4..46c21a2 100644 --- a/entry/src/main/ets/pages/PcListPageV2.ets +++ b/entry/src/main/ets/pages/PcListPageV2.ets @@ -59,6 +59,7 @@ struct PcListPageV2 { @State showAboutDialog: boolean = false; @State isLandscape: boolean = false; // 横屏检测 @State screenWidth: number = 0; // 屏幕宽度 + @State canScanQrCode: boolean = true; // 当前设备是否支持系统扫码 @StorageProp('topSafeHeight') topSafeHeight: number = 56; // 动态状态栏安全区高度 // LazyForEach 数据源 @@ -130,6 +131,7 @@ struct PcListPageV2 { // 先设置 context 以便后续使用本地缓存 const context = getContext(this) as common.UIAbilityContext; this.backgroundUtil.setContext(context); + this.canScanQrCode = QrShareService.isScanQrCodeSupported(); // 初始化业务逻辑处理类 this.actions = new PcListActions({ @@ -461,6 +463,7 @@ struct PcListPageV2 { config: { onlineCount: this.viewModel.onlineCount, totalCount: this.viewModel.computerCount, + showScanImport: this.canScanQrCode, onAboutClick: () => { this.showAboutDialog = true; }, onScanImport: () => { this.scanAndImportKeys(); }, onSettingsClick: () => { router.pushUrl({ url: 'pages/SettingsPageV2' }); } @@ -840,6 +843,15 @@ struct PcListPageV2 { /** 扫码:自动识别配对 URL 或按键配置 */ private async scanAndImportKeys(): Promise { + if (!QrShareService.isScanQrCodeSupported()) { + ToastQueue.show({ + message: QrShareService.getScanUnsupportedMessage() + ',请使用扫描网络或手动添加', + duration: 2500 + }); + this.canScanQrCode = false; + return; + } + const result = await QrShareService.scanQrCode(); if (!result) { ToastQueue.show({ message: '扫码取消或失败', duration: 1500 }); diff --git a/entry/src/main/ets/service/network/QrShareService.ets b/entry/src/main/ets/service/network/QrShareService.ets index 6a06673..340ee3d 100644 --- a/entry/src/main/ets/service/network/QrShareService.ets +++ b/entry/src/main/ets/service/network/QrShareService.ets @@ -93,6 +93,11 @@ const QR_MAX_CHARS = 4000; /** 压缩数据前缀 — Moonlight Compressed */ const COMPRESSED_PREFIX = 'MLC:'; +/** 系统扫码 UI 能力。HarmonyOS PC/2in1 只提供生成二维码能力,不提供 ScanBarcode。 */ +const SCAN_BARCODE_SYSCAP = 'SystemCapability.Multimedia.Scan.ScanBarcode'; + +const SCAN_UNSUPPORTED_MESSAGE = '当前设备不支持扫码'; + // ============================================================================= // zlib 压缩 / 解压 // ============================================================================= @@ -548,6 +553,22 @@ function fromCompactV1(raw: Record): CustomKeyDef { export class QrShareService { + /** + * 当前设备是否支持拉起系统扫码 UI。 + * 例如 MateBook/2in1 设备缺少 ScanBarcode syscap,直接调用会返回取消或失败。 + */ + static isScanQrCodeSupported(): boolean { + try { + return canIUse(SCAN_BARCODE_SYSCAP); + } catch (_e) { + return false; + } + } + + static getScanUnsupportedMessage(): string { + return SCAN_UNSUPPORTED_MESSAGE; + } + /** * 将配置序列化为 v3 管道格式(QR/压缩用,最紧凑) */ @@ -697,6 +718,11 @@ export class QrShareService { * @returns 扫码内容字符串,失败返回 null */ static async scanQrCode(): Promise { + if (!QrShareService.isScanQrCodeSupported()) { + console.warn('[QrShareService] 当前设备不支持 ScanBarcode 系统能力'); + return null; + } + try { const options: scanBarcode.ScanOptions = { scanTypes: [scanCore.ScanType.QR_CODE],