双端适配,web端的修改父容器和接口

This commit is contained in:
2026-03-29 02:34:13 +08:00
parent 3c071957ce
commit ebb0c003e3
16 changed files with 1419 additions and 226 deletions

View File

@@ -7,8 +7,8 @@ import {
Alert,
Modal,
Dimensions,
Platform,
} from 'react-native';
import { CameraView, useCameraPermissions } from 'expo-camera';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as hrefs from '../../navigation/hrefs';
@@ -133,22 +133,74 @@ function createQrScannerStyles(colors: AppColors) {
fontSize: fontSizes.md,
fontWeight: '600',
},
webNotSupportedContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.xl,
backgroundColor: '#000',
},
webNotSupportedText: {
marginTop: spacing.lg,
fontSize: fontSizes.md,
color: 'rgba(255,255,255,0.72)',
textAlign: 'center',
lineHeight: 24,
},
});
}
export const QRCodeScanner: React.FC<QRCodeScannerProps> = ({ visible, onClose }) => {
// Web 端不支持扫码组件
const QRCodeScannerWeb: React.FC<QRCodeScannerProps> = ({ visible, onClose }) => {
const themeColors = useAppColors();
const styles = useMemo(() => createQrScannerStyles(themeColors), [themeColors]);
return (
<Modal visible={visible} animationType="slide" onRequestClose={onClose}>
<View style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={onClose} style={styles.closeButton}>
<MaterialCommunityIcons name="close" size={24} color="#fff" />
</TouchableOpacity>
<Text style={styles.headerTitle}></Text>
<View style={styles.placeholder} />
</View>
<View style={styles.webNotSupportedContainer}>
<MaterialCommunityIcons name="web-off" size={64} color="rgba(255,255,255,0.5)" />
<Text style={styles.webNotSupportedText}>
使{'\n'}
App
</Text>
<TouchableOpacity style={styles.permissionButton} onPress={onClose}>
<Text style={styles.permissionButtonText}></Text>
</TouchableOpacity>
</View>
</View>
</Modal>
);
};
// 原生端扫码组件
const QRCodeScannerNative: React.FC<QRCodeScannerProps> = ({ visible, onClose }) => {
const router = useRouter();
const themeColors = useAppColors();
const styles = useMemo(() => createQrScannerStyles(themeColors), [themeColors]);
const [permission, requestPermission] = useCameraPermissions();
// 动态导入 expo-camera避免 Web 端加载
const [cameraModule, setCameraModule] = useState<typeof import('expo-camera') | null>(null);
const [permission, setPermission] = useState<any>(null);
const [scanned, setScanned] = useState(false);
useEffect(() => {
if (visible) {
setScanned(false);
if (!permission?.granted) {
requestPermission();
}
if (visible && Platform.OS !== 'web') {
import('expo-camera').then((module) => {
setCameraModule(module);
const [perm, requestPerm] = module.useCameraPermissions();
setPermission(perm);
if (!perm?.granted) {
requestPerm();
}
});
}
}, [visible]);
@@ -178,7 +230,7 @@ export const QRCodeScanner: React.FC<QRCodeScannerProps> = ({ visible, onClose }
}
};
if (!permission?.granted) {
if (!cameraModule || !permission?.granted) {
return (
<Modal visible={visible} animationType="slide" onRequestClose={onClose}>
<View style={styles.container}>
@@ -192,7 +244,10 @@ export const QRCodeScanner: React.FC<QRCodeScannerProps> = ({ visible, onClose }
<View style={styles.permissionContainer}>
<MaterialCommunityIcons name="camera-off" size={64} color="rgba(255,255,255,0.5)" />
<Text style={styles.permissionText}></Text>
<TouchableOpacity style={styles.permissionButton} onPress={requestPermission}>
<TouchableOpacity
style={styles.permissionButton}
onPress={() => cameraModule?.useCameraPermissions()[1]()}
>
<Text style={styles.permissionButtonText}></Text>
</TouchableOpacity>
</View>
@@ -201,6 +256,8 @@ export const QRCodeScanner: React.FC<QRCodeScannerProps> = ({ visible, onClose }
);
}
const { CameraView } = cameraModule;
return (
<Modal visible={visible} animationType="slide" onRequestClose={onClose}>
<View style={styles.container}>
@@ -243,4 +300,9 @@ export const QRCodeScanner: React.FC<QRCodeScannerProps> = ({ visible, onClose }
);
};
// 根据平台导出不同组件
export const QRCodeScanner: React.FC<QRCodeScannerProps> = Platform.OS === 'web'
? QRCodeScannerWeb
: QRCodeScannerNative;
export default QRCodeScanner;

View File

@@ -0,0 +1,160 @@
import React from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
StatusBar,
} from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { callStore } from '../../stores/callStore';
const formatDuration = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
const CallScreen: React.FC = () => {
const currentCall = callStore((s) => s.currentCall);
const callDuration = callStore((s) => s.callDuration);
const endCall = callStore((s) => s.endCall);
const isMinimized = callStore((s) => s.isMinimized);
// Don't render full screen when minimized or no call
if (!currentCall || isMinimized) return null;
const getStatusText = (): string => {
switch (currentCall.status) {
case 'ringing':
return '通话功能暂不支持网页端';
case 'connecting':
return '通话功能暂不支持网页端';
case 'connected':
return '通话功能暂不支持网页端';
case 'ending':
return '通话已结束';
default:
return '';
}
};
const handleEndCall = () => {
endCall('hangup');
};
return (
<View style={styles.container}>
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
{/* Background - single consistent color */}
<View style={styles.background} />
{/* Center: Peer info */}
<View style={styles.centerArea}>
<View style={styles.avatarOuter}>
<View style={[styles.avatar, styles.avatarPlaceholder]}>
<MaterialCommunityIcons name="web-off" size={50} color="#fff" />
</View>
</View>
<Text style={styles.peerName} numberOfLines={1}>
{currentCall.peerName || '未知用户'}
</Text>
<Text style={styles.status}>{getStatusText()}</Text>
</View>
{/* Bottom controls */}
<View style={styles.controls}>
{/* End call - prominent red button */}
<TouchableOpacity
style={styles.endCallButton}
onPress={handleEndCall}
activeOpacity={0.8}
>
<View style={styles.endCallCircle}>
<MaterialCommunityIcons name="phone-hangup" size={32} color="#fff" />
</View>
<Text style={styles.endCallLabel}></Text>
</TouchableOpacity>
</View>
</View>
);
};
const END_CALL_SIZE = 64;
const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
backgroundColor: '#1A1A2E',
zIndex: 9999,
},
background: {
...StyleSheet.absoluteFillObject,
backgroundColor: '#1A1A2E',
},
centerArea: {
position: 'absolute',
top: '28%',
left: 0,
right: 0,
alignItems: 'center',
paddingHorizontal: 30,
},
avatarOuter: {
marginBottom: 16,
},
avatar: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: '#3A3A5C',
},
avatarPlaceholder: {
justifyContent: 'center',
alignItems: 'center',
},
avatarText: {
fontSize: 40,
color: '#fff',
fontWeight: '600',
},
peerName: {
fontSize: 22,
fontWeight: '600',
color: '#FFFFFF',
marginBottom: 6,
textAlign: 'center',
maxWidth: 280,
},
status: {
fontSize: 14,
color: 'rgba(255, 255, 255, 0.5)',
letterSpacing: 0.3,
},
controls: {
position: 'absolute',
bottom: 60,
left: 0,
right: 0,
alignItems: 'center',
},
endCallButton: {
alignItems: 'center',
},
endCallCircle: {
width: END_CALL_SIZE,
height: END_CALL_SIZE,
borderRadius: END_CALL_SIZE / 2,
backgroundColor: '#E54D42',
justifyContent: 'center',
alignItems: 'center',
},
endCallLabel: {
fontSize: 11,
color: 'rgba(255, 255, 255, 0.55)',
marginTop: 8,
},
});
export default CallScreen;