071faa482c
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口 完成项目 TypeScript/Prettier 与 Rust 全量格式化 修复 Pingora expected executable 门禁的空白敏感误报 同步开发运维文档与 AGC skill pack 格式化忽略规则
147 lines
3.2 KiB
TypeScript
147 lines
3.2 KiB
TypeScript
import { type BarcodeScanningResult, Camera, CameraView } from 'expo-camera';
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
|
|
import {
|
|
cancelQrCodeScan,
|
|
completeQrCodeScan,
|
|
failQrCodeScan,
|
|
subscribeQrScannerState,
|
|
} from '../host-bridge/scanner';
|
|
|
|
function logQrScannerPermissionFailure(_error: unknown) {
|
|
console.warn('mobile QR scanner permission request failed');
|
|
}
|
|
|
|
export function QrScannerOverlay() {
|
|
const [isActive, setIsActive] = useState(false);
|
|
const [requestKey, setRequestKey] = useState(0);
|
|
const [hasPermission, setHasPermission] = useState(false);
|
|
const [scanCompleted, setScanCompleted] = useState(false);
|
|
|
|
useEffect(
|
|
() =>
|
|
subscribeQrScannerState((state) => {
|
|
setIsActive(state.active);
|
|
setRequestKey(state.requestKey);
|
|
}),
|
|
[],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!isActive) {
|
|
setHasPermission(false);
|
|
setScanCompleted(false);
|
|
return;
|
|
}
|
|
|
|
let disposed = false;
|
|
setHasPermission(false);
|
|
setScanCompleted(false);
|
|
void Camera.requestCameraPermissionsAsync()
|
|
.then((permission) => {
|
|
if (disposed) {
|
|
return;
|
|
}
|
|
if (!permission.granted) {
|
|
failQrCodeScan();
|
|
return;
|
|
}
|
|
setHasPermission(true);
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (!disposed) {
|
|
logQrScannerPermissionFailure(error);
|
|
failQrCodeScan();
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
disposed = true;
|
|
};
|
|
}, [isActive, requestKey]);
|
|
|
|
const handleBarcodeScanned = useCallback(
|
|
(result: BarcodeScanningResult) => {
|
|
if (scanCompleted || result.type !== 'qr') {
|
|
return;
|
|
}
|
|
|
|
if (completeQrCodeScan(result.data)) {
|
|
setScanCompleted(true);
|
|
}
|
|
},
|
|
[scanCompleted],
|
|
);
|
|
|
|
if (!isActive) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<View style={styles.root}>
|
|
{hasPermission ? (
|
|
<CameraView
|
|
style={styles.camera}
|
|
facing="back"
|
|
onBarcodeScanned={scanCompleted ? undefined : handleBarcodeScanned}
|
|
barcodeScannerSettings={{
|
|
barcodeTypes: ['qr'],
|
|
}}
|
|
/>
|
|
) : null}
|
|
<View pointerEvents="none" style={styles.frame} />
|
|
<Pressable
|
|
accessibilityRole="button"
|
|
accessibilityLabel="关闭扫码"
|
|
onPress={cancelQrCodeScan}
|
|
style={styles.closeButton}
|
|
>
|
|
<Text style={styles.closeButtonText}>关闭</Text>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
root: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
backgroundColor: '#0c0a09',
|
|
},
|
|
camera: {
|
|
flex: 1,
|
|
},
|
|
frame: {
|
|
position: 'absolute',
|
|
top: '22%',
|
|
right: 42,
|
|
bottom: '22%',
|
|
left: 42,
|
|
borderWidth: 2,
|
|
borderColor: '#fffdf9',
|
|
borderRadius: 8,
|
|
},
|
|
closeButton: {
|
|
position: 'absolute',
|
|
top: 20,
|
|
right: 20,
|
|
minWidth: 76,
|
|
minHeight: 40,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
borderRadius: 8,
|
|
backgroundColor: '#fffdf9',
|
|
paddingHorizontal: 16,
|
|
},
|
|
closeButtonText: {
|
|
color: '#211a16',
|
|
fontSize: 15,
|
|
fontWeight: '700',
|
|
lineHeight: 20,
|
|
},
|
|
});
|