Files
frontend/src/screens/profile/VerificationSettingsScreen.tsx
lafay 6f84e17772 feat(service): add foreground service for background message sync
Implement Android foreground service to keep app alive in background for real-time message sync:
- Add withForegroundService expo config plugin for notification channel setup
- Add BackgroundSyncManager with REALTIME, BATTERY_SAVER, and DISABLED modes
- Add ForegroundServiceModule for native Android foreground service binding
- Refactor backgroundService to integrate foreground service and expo-background-task

Also refactor auth and profile screens to use dynamic theme colors:
- Replace hardcoded THEME_COLORS with colors.primary.main, colors.background.paper, etc.
- Add useResolvedColorScheme for dynamic StatusBar styling
- Update NotificationSettingsScreen with sync mode selection UI

BREAKING CHANGE: Switched from expo-background-fetch to expo-background-task, minimum background sync interval changed from 900s to 15min in config
2026-04-11 04:27:22 +08:00

477 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 身份认证设置页面
* 用户查看认证状态、提交认证申请
*/
import React, { useEffect, useState } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ScrollView,
Image,
Alert,
ActivityIndicator,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useAppColors, type AppColors } from '../../theme';
import {
useVerificationStore,
useVerificationStatus,
useVerificationLoading,
} from '../../stores/verificationStore';
import { showPrompt } from '../../services/promptService';
import * as hrefs from '../../navigation/hrefs';
const STATUS_CONFIG = {
not_submitted: {
title: '未认证',
description: '完成身份认证,解锁更多专属功能',
icon: 'account-question-outline',
color: '#999',
},
pending: {
title: '审核中',
description: '您的认证申请正在审核中,请耐心等待',
icon: 'clock-outline',
color: '#FFA500',
},
approved: {
title: '已认证',
description: '您已完成身份认证',
icon: 'check-decagram',
color: '#4CAF50',
},
rejected: {
title: '认证失败',
description: '您的认证申请被拒绝,可以重新提交',
icon: 'close-circle-outline',
color: '#F44336',
},
};
const IDENTITY_TEXT: Record<string, string> = {
general: '普通用户',
student: '在校学生',
teacher: '教职工',
staff: '工作人员',
alumni: '校友',
external: '外部人员',
};
export const VerificationSettingsScreen: React.FC = () => {
const colors = useAppColors();
const styles = createStyles(colors);
const router = useRouter();
const status = useVerificationStatus();
const loading = useVerificationLoading();
const { fetchStatus, reset } = useVerificationStore();
useEffect(() => {
fetchStatus();
return () => {
reset();
};
}, []);
const handleStartVerification = () => {
router.push(hrefs.hrefVerificationGuide());
};
const handleViewRecords = () => {
showPrompt({
title: '认证记录',
message: '认证记录功能即将上线',
type: 'info',
});
};
if (loading && !status) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
</SafeAreaView>
);
}
const statusKey = (status?.verification_status || 'not_submitted') as keyof typeof STATUS_CONFIG;
const statusConfig = STATUS_CONFIG[statusKey];
const identityText = IDENTITY_TEXT[status?.identity || 'general'];
return (
<SafeAreaView style={styles.container}>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* 状态卡片 */}
<View style={styles.statusCard}>
<View style={[styles.statusIconContainer, { backgroundColor: statusConfig.color + '20' }]}>
<MaterialCommunityIcons
name={statusConfig.icon as any}
size={48}
color={statusConfig.color}
/>
</View>
<Text style={styles.statusTitle}>{statusConfig.title}</Text>
<Text style={styles.statusDescription}>{statusConfig.description}</Text>
{status?.verification_status === 'approved' && (
<View style={styles.identityBadge}>
<MaterialCommunityIcons name="check-circle" size={16} color={colors.primary.main} />
<Text style={styles.identityText}>{identityText}</Text>
</View>
)}
{status?.latest_record?.reject_reason && (
<View style={styles.rejectReasonContainer}>
<Text style={styles.rejectReasonLabel}></Text>
<Text style={styles.rejectReasonText}>
{status.latest_record.reject_reason}
</Text>
</View>
)}
</View>
{/* 认证优势 */}
{status?.verification_status !== 'approved' && (
<View style={styles.benefitsSection}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.benefitsList}>
<View style={styles.benefitItem}>
<View style={styles.benefitIconContainer}>
<MaterialCommunityIcons
name="check-decagram"
size={24}
color={colors.primary.main}
/>
</View>
<View style={styles.benefitContent}>
<Text style={styles.benefitTitle}></Text>
<Text style={styles.benefitDescription}>
</Text>
</View>
</View>
<View style={styles.benefitItem}>
<View style={styles.benefitIconContainer}>
<MaterialCommunityIcons
name="lock-open-outline"
size={24}
color={colors.primary.main}
/>
</View>
<View style={styles.benefitContent}>
<Text style={styles.benefitTitle}></Text>
<Text style={styles.benefitDescription}>
访
</Text>
</View>
</View>
<View style={styles.benefitItem}>
<View style={styles.benefitIconContainer}>
<MaterialCommunityIcons
name="account-group-outline"
size={24}
color={colors.primary.main}
/>
</View>
<View style={styles.benefitContent}>
<Text style={styles.benefitTitle}></Text>
<Text style={styles.benefitDescription}>
</Text>
</View>
</View>
</View>
</View>
)}
{/* 操作按钮 */}
<View style={styles.actionSection}>
{(status?.verification_status === 'not_submitted' ||
status?.verification_status === 'rejected') && (
<TouchableOpacity
style={styles.primaryButton}
onPress={handleStartVerification}
activeOpacity={0.9}
>
<MaterialCommunityIcons name="shield-check-outline" size={20} color={colors.primary.contrast} />
<Text style={styles.primaryButtonText}>
{status?.verification_status === 'rejected' ? '重新认证' : '开始认证'}
</Text>
</TouchableOpacity>
)}
{status?.verification_status === 'pending' && (
<TouchableOpacity
style={styles.secondaryButton}
onPress={handleViewRecords}
activeOpacity={0.8}
>
<MaterialCommunityIcons name="file-document-outline" size={20} color={colors.primary.main} />
<Text style={styles.secondaryButtonText}></Text>
</TouchableOpacity>
)}
{status?.verification_status === 'approved' && (
<>
<View style={styles.verifiedInfo}>
<View style={styles.verifiedInfoRow}>
<Text style={styles.verifiedInfoLabel}></Text>
<Text style={styles.verifiedInfoValue}>{identityText}</Text>
</View>
{status.latest_record?.real_name && (
<View style={styles.verifiedInfoRow}>
<Text style={styles.verifiedInfoLabel}></Text>
<Text style={styles.verifiedInfoValue}>
{status.latest_record.real_name}
</Text>
</View>
)}
{status.latest_record?.department && (
<View style={styles.verifiedInfoRow}>
<Text style={styles.verifiedInfoLabel}>/</Text>
<Text style={styles.verifiedInfoValue}>
{status.latest_record.department}
</Text>
</View>
)}
</View>
</>
)}
</View>
{/* 帮助信息 */}
<View style={styles.helpSection}>
<Text style={styles.helpTitle}></Text>
<Text style={styles.helpText}>
</Text>
</View>
</ScrollView>
</SafeAreaView>
);
};
function createStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.paper,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.divider || '#E0E0E0',
},
backButton: {
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'center',
},
headerTitle: {
fontSize: 18,
fontWeight: '600',
color: colors.text?.primary || '#333',
},
headerRight: {
width: 40,
},
scrollContent: {
flexGrow: 1,
padding: 24,
},
statusCard: {
alignItems: 'center',
paddingVertical: 32,
paddingHorizontal: 24,
backgroundColor: colors.background.default,
borderRadius: 16,
marginBottom: 24,
},
statusIconContainer: {
width: 96,
height: 96,
borderRadius: 48,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 16,
},
statusTitle: {
fontSize: 24,
fontWeight: '700',
color: colors.text?.primary || '#333',
marginBottom: 8,
},
statusDescription: {
fontSize: 15,
color: colors.text?.secondary || '#666',
textAlign: 'center',
lineHeight: 22,
},
identityBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primary.main + '15',
borderRadius: 20,
paddingVertical: 8,
paddingHorizontal: 16,
marginTop: 16,
},
identityText: {
fontSize: 14,
fontWeight: '600',
color: colors.primary.main,
marginLeft: 6,
},
rejectReasonContainer: {
marginTop: 16,
padding: 12,
backgroundColor: colors.error.light + '20',
borderRadius: 8,
width: '100%',
},
rejectReasonLabel: {
fontSize: 13,
fontWeight: '600',
color: colors.error.main,
marginBottom: 4,
},
rejectReasonText: {
fontSize: 13,
color: colors.error.dark,
lineHeight: 18,
},
benefitsSection: {
marginBottom: 24,
},
sectionTitle: {
fontSize: 17,
fontWeight: '600',
color: colors.text?.primary || '#333',
marginBottom: 16,
},
benefitsList: {
gap: 12,
},
benefitItem: {
flexDirection: 'row',
alignItems: 'flex-start',
backgroundColor: colors.background.default,
borderRadius: 12,
padding: 16,
},
benefitIconContainer: {
width: 48,
height: 48,
borderRadius: 12,
backgroundColor: colors.primary.main + '15',
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
benefitContent: {
flex: 1,
},
benefitTitle: {
fontSize: 15,
fontWeight: '600',
color: colors.text?.primary || '#333',
marginBottom: 4,
},
benefitDescription: {
fontSize: 13,
color: colors.text?.secondary || '#666',
lineHeight: 18,
},
actionSection: {
marginBottom: 24,
},
primaryButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
height: 56,
borderRadius: 14,
backgroundColor: colors.primary.main,
},
primaryButtonText: {
fontSize: 17,
fontWeight: '600',
color: colors.text.inverse,
marginLeft: 8,
},
secondaryButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
height: 56,
borderRadius: 14,
backgroundColor: colors.primary.main + '15',
borderWidth: 1,
borderColor: colors.primary.main,
},
secondaryButtonText: {
fontSize: 17,
fontWeight: '600',
color: colors.primary.main,
marginLeft: 8,
},
verifiedInfo: {
backgroundColor: colors.background.default,
borderRadius: 12,
padding: 16,
},
verifiedInfoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 8,
},
verifiedInfoLabel: {
fontSize: 15,
color: colors.text?.secondary || '#666',
},
verifiedInfoValue: {
fontSize: 15,
fontWeight: '500',
color: colors.text?.primary || '#333',
},
helpSection: {
alignItems: 'center',
paddingVertical: 16,
},
helpTitle: {
fontSize: 15,
fontWeight: '600',
color: colors.text?.primary || '#333',
marginBottom: 8,
},
helpText: {
fontSize: 13,
color: colors.text?.hint || '#999',
textAlign: 'center',
lineHeight: 18,
},
});
}
export default VerificationSettingsScreen;