二维码保存

This commit is contained in:
tx 2024-12-26 16:17:19 +08:00
parent e184055bee
commit a0f361a9ff
4 changed files with 389 additions and 18089 deletions

17487
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,9 @@
import React, { useEffect, useState, useCallback } from 'react'; import React, { useEffect, useState, useCallback } from 'react';
import { View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native'; import { View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native';
import { useNavigation ,useFocusEffect} from '@react-navigation/native'; import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { apiService } from '../../utils/api'; import { apiService } from '../../utils/api';
import { rpx } from '../../utils/rpx'; import { rpx } from '../../utils/rpx';
interface KeyItem { interface KeyItem {
id: string; id: string;
avatarUrl: string; avatarUrl: string;
@ -18,23 +19,20 @@ const DeviceShare = () => {
const getKeyList = async () => { const getKeyList = async () => {
const response = await apiService.getKeyListByOwnerId(); const response = await apiService.getKeyListByOwnerId();
// console.log(response);
if (response.code == 200) { if (response.code == 200) {
setKeyList(response.data); setKeyList(response.data);
} }
}; };
const calculateRemainingTime = (expirationTime: string): string => { const calculateRemainingTime = (expirationTime: string): string => {
const now = new Date(); const now = new Date();
const expiration = new Date(expirationTime); const expiration = new Date(expirationTime);
const diffTime = expiration.getTime() - now.getTime(); const diffTime = expiration.getTime() - now.getTime();
// 如果已过期
if (diffTime <= 0) { if (diffTime <= 0) {
return '已过期'; return '已过期';
} }
// 计算剩余天数、小时、分钟
const days = Math.floor(diffTime / (1000 * 60 * 60 * 24)); const days = Math.floor(diffTime / (1000 * 60 * 60 * 24));
const hours = Math.floor((diffTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const hours = Math.floor((diffTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diffTime % (1000 * 60 * 60)) / (1000 * 60)); const minutes = Math.floor((diffTime % (1000 * 60 * 60)) / (1000 * 60));
@ -47,6 +45,7 @@ const DeviceShare = () => {
return `${minutes}分钟`; return `${minutes}分钟`;
} }
}; };
const getStatusText = (status: string | number) => { const getStatusText = (status: string | number) => {
switch (status) { switch (status) {
case '0': case '0':
@ -59,6 +58,7 @@ const DeviceShare = () => {
return '未知状态'; return '未知状态';
} }
}; };
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
getKeyList(); getKeyList();
@ -72,14 +72,13 @@ const DeviceShare = () => {
<Text style={styles.shareTxt}>使</Text> <Text style={styles.shareTxt}>使</Text>
{keyList.map((key) => ( {keyList.map((key) => (
<TouchableOpacity <TouchableOpacity
key={key.id} key={key.id}
onPress={() => { onPress={() => {
navigation.navigate('ShareDetailScreen', { keyItem: key }); navigation.navigate('ShareDetailScreen', { keyItem: key });
}} }}
> >
<View style={styles.card}>
<View key={key.id} style={styles.card}>
<View style={styles.cardTop}> <View style={styles.cardTop}>
<Image <Image
source={{ uri: key.avatarUrl || 'https://lxnapi.ccttiot.com/bike/img/static/uVnIDwcwQP7oo12PeYVJ' }} source={{ uri: key.avatarUrl || 'https://lxnapi.ccttiot.com/bike/img/static/uVnIDwcwQP7oo12PeYVJ' }}
@ -107,8 +106,8 @@ const DeviceShare = () => {
</View> </View>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
))} ))}
{keyList.length < 3 && ( {keyList.length < 3 && (
<TouchableOpacity onPress={() => { <TouchableOpacity onPress={() => {
navigation.navigate('AddShare' as never); navigation.navigate('AddShare' as never);
@ -124,21 +123,20 @@ const DeviceShare = () => {
)} )}
</View> </View>
<TouchableOpacity onPress={() => { <TouchableOpacity onPress={() => {
navigation.navigate('ExpiredKeysScreen' as never ); navigation.navigate('ExpiredKeysScreen' as never);
}}> }}>
<View style={styles.shareTip}> <View style={styles.shareTip}>
<Text style={styles.shareBtnTxt}></Text> <Text style={styles.shareBtnTxt}></Text>
<Image <Image
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uGq4yJlU1ZZRkwiJ8Y74' }} source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/uGq4yJlU1ZZRkwiJ8Y74' }}
style={styles.lasttimeImg} style={styles.lasttimeImg}
/> />
</View> </View>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
@ -205,7 +203,6 @@ const styles = StyleSheet.create({
}, },
cardTopTxt: { cardTopTxt: {
marginLeft: rpx(28), marginLeft: rpx(28),
}, },
cardTopImg: { cardTopImg: {
width: rpx(96), width: rpx(96),
@ -213,7 +210,6 @@ const styles = StyleSheet.create({
borderRadius: rpx(48), borderRadius: rpx(48),
}, },
cardTopName: { cardTopName: {
fontSize: rpx(44), fontSize: rpx(44),
color: '#3D3D3D', color: '#3D3D3D',
}, },
@ -221,7 +217,6 @@ const styles = StyleSheet.create({
fontSize: rpx(36), fontSize: rpx(36),
color: '#808080', color: '#808080',
}, },
shareBox: { shareBox: {
width: rpx(688), width: rpx(688),
borderRadius: rpx(30), borderRadius: rpx(30),
@ -245,8 +240,8 @@ const styles = StyleSheet.create({
marginTop: rpx(32), marginTop: rpx(32),
borderRadius: rpx(16), borderRadius: rpx(16),
borderWidth: rpx(2), borderWidth: rpx(2),
borderColor: '#808080 ', borderColor: '#808080',
}, },
}); });
export default DeviceShare;
export default DeviceShare;

View File

@ -1,88 +1,105 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { View, StyleSheet, Image, ImageBackground, TouchableOpacity, Text, Alert, Platform, NativeModules } from 'react-native'; import { View, StyleSheet, Image, ImageBackground, TouchableOpacity, Text, Alert, Platform, NativeModules } from 'react-native';
import { rpx } from '../../utils/rpx'; import { rpx } from '../../utils/rpx';
import { RouteProp, useRoute } from '@react-navigation/native'; import { RouteProp, useRoute } from '@react-navigation/native';
import RNFS from 'react-native-fs'; import RNFS from 'react-native-fs';
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions'; import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { Spinner, Layout } from '@ui-kitten/components'; import { Spinner, Layout } from '@ui-kitten/components';
import QRCode from 'react-native-qrcode-svg';
import { captureRef } from 'react-native-view-shot';
const ShareQrcode = () => { const ShareQrcode = () => {
const route = useRoute<RouteProp<RootStackParamList, 'ShareQrcode'>>(); const route = useRoute<RouteProp<RootStackParamList, 'ShareQrcode'>>();
const { QrId } = route.params; const { QrId } = route.params;
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const qrCodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${rpx(400)}x${rpx(400)}&data=https://testlu.chuangtewl.com/prod-api?keyId=${QrId}`; const qrCodeUrl = `https://testlu.chuangtewl.com/prod-api?keyId=${QrId}`;
const qrRef = useRef();
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
setLoading(false); setLoading(false);
}, 2000); }, 1000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, []); }, []);
const saveToGallery = async () => { const saveToGallery = async () => {
try { try {
const writePermission = await request(PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE); // 修正权限请求
const readPermission = await request(PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE); const writePermission = await request(
Platform.select({
android: PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE,
default: PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE,
})
);
if (writePermission === RESULTS.GRANTED && readPermission === RESULTS.GRANTED) { const readPermission = await request(
try { Platform.select({
let directoryPath = `${RNFS.DownloadDirectoryPath}`; android: PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE,
const fileName = `qrcode_${Date.now()}.jpg`; default: PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE,
let downloadDest = `${directoryPath}/${fileName}`; })
);
// 检查目录是否存在 if (writePermission !== RESULTS.GRANTED || readPermission !== RESULTS.GRANTED) {
let exists = await RNFS.exists(directoryPath); Alert.alert(
if (!exists) { '权限不足',
try { '请在设置中授予存储权限',
await RNFS.mkdir(directoryPath); [
} catch (mkdirError) { { text: '取消' },
// 使用备用路径 {
directoryPath = `${RNFS.ExternalStorageDirectoryPath}`; text: '去设置',
downloadDest = `${directoryPath}/${fileName}`; onPress: () => {
} if (Platform.OS === 'android') {
} Linking.openSettings();
// 下载文件
const options = {
fromUrl: qrCodeUrl,
toFile: downloadDest,
};
const response = await RNFS.downloadFile(options).promise;
if (response.statusCode === 200) {
const fileExists = await RNFS.exists(downloadDest);
if (fileExists) {
// 刷新媒体库
NativeModules.MediaScanner.scanFile(downloadDest, 'image/jpeg', (err) => {
if (err) {
Alert.alert('错误', '无法刷新媒体库');
} else {
Alert.alert('成功', '二维码已保存到相册');
} }
}); }
} else {
throw new Error('文件未能成功创建');
} }
} else { ]
Alert.alert('错误', '下载失败,请检查网络连接'); );
} return;
} catch (err) {
Alert.alert('错误', '保存失败,无法访问存储空间');
}
} else {
Alert.alert('权限不足', '请在设置中授予存储权限');
} }
// 使用应用专用目录
const fileName = `qrcode_${Date.now()}.jpg`;
const downloadDest = `${RNFS.ExternalDirectoryPath}/${fileName}`;
// 截取QR码视图
const uri = await captureRef(qrRef, {
format: 'jpg',
quality: 1,
});
// 复制文件到目标位置
await RNFS.copyFile(uri, downloadDest);
// 使用 MediaScanner 使图片在相册中可见
NativeModules.MediaScanner.scanFile(downloadDest, 'image/jpeg', (err) => {
if (err) {
console.error('MediaScanner error:', err);
Alert.alert('错误', '保存失败,请重试');
} else {
Alert.alert('成功', '二维码已保存到相册');
}
});
// 清理临时文件
try {
await RNFS.unlink(uri);
} catch (e) {
console.log('清理临时文件失败:', e);
}
} catch (error) { } catch (error) {
console.error('Save error:', error);
Alert.alert('错误', '保存失败,请重试'); Alert.alert('错误', '保存失败,请重试');
} }
}; };
if (loading) { if (loading) {
return ( return (
<Layout style={styles.loadingContainer}> <Layout style={styles.loadingContainer}>
<Spinner size='giant' /> <Spinner size='giant' />
<Text style={styles.loadingText}>...</Text> <Text style={styles.loadingText}>...</Text>
</Layout> </Layout>
); );
} }
@ -97,10 +114,12 @@ const ShareQrcode = () => {
source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/u8yqVl9y6fcU3fTDDTpK' }} source={{ uri: 'https://lxnapi.ccttiot.com/bike/img/static/u8yqVl9y6fcU3fTDDTpK' }}
style={styles.qrBack} style={styles.qrBack}
> >
<Layout style={styles.qrContainer}> <Layout style={styles.qrContainer} ref={qrRef}>
<Image <QRCode
source={{ uri: qrCodeUrl }} value={qrCodeUrl}
style={styles.qrCode} size={rpx(438)}
color="black"
backgroundColor="white"
/> />
</Layout> </Layout>
</ImageBackground> </ImageBackground>
@ -147,19 +166,20 @@ const styles = StyleSheet.create({
width: rpx(614), width: rpx(614),
height: rpx(368), height: rpx(368),
}, },
qrCode:{
width: rpx(438),
height: rpx(438),
},
qrContainer: { qrContainer: {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: 'white',
padding: rpx(20),
borderRadius: rpx(10),
}, },
qrBack: { qrBack: {
marginTop: rpx(-20), marginTop: rpx(-20),
width: rpx(614), width: rpx(614),
height: rpx(614), height: rpx(614),
padding: rpx(86), padding: rpx(86),
alignItems: 'center',
justifyContent: 'center',
}, },
backimg: { backimg: {
width: rpx(688), width: rpx(688),

806
yarn.lock

File diff suppressed because it is too large Load Diff