Files
frontend/src/screens/profile/VerificationSettingsScreen.tsx
lan 2e2f6e3467
Some checks failed
Frontend CI / ota-android (push) Successful in 1m29s
Frontend CI / build-and-push-web (push) Failing after 1m31s
Frontend CI / ota-ios (push) Successful in 4m4s
Frontend CI / build-android-apk (push) Successful in 29m5s
refactor(message): improve message synchronization and hook reliability
Refactor the message management system to address synchronization issues and improve performance through request deduplication and enhanced lifecycle management.

- Implement in-flight promise tracking in `MessageSyncService` to prevent redundant network requests for conversations and messages.
- Enhance `useMessages` hook with `useFocusEffect` to trigger automatic synchronization when a chat screen regains focus.
- Add `forceSync` capability to `MessageManager` and `MessageSyncService` to allow manual overrides of loading states during re-entry.
- Consolidate WebSocket synchronization logic in `WSMessageHandler` using a throttled and deduplicated trigger mechanism.
- Clean up unused styles and deprecated hooks (`baseHooks.ts`, `bubbleStyles.ts`, `inputStyles.ts`).
- Update `metro.config.js` and `package.json` to include `@expo/ui` and optimize resolver settings.
2026-06-03 10:31:46 +08:00

483 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 { StatusBar } from 'expo-status-bar';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { SimpleHeader } from '../../components/common';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useAppColors, type AppColors } from '../../theme';
import {
useVerificationStore,
useVerificationStatus,
useVerificationLoading,
} from '../../stores/auth';
import { showPrompt } from '@/services/ui';
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} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="身份认证" onBack={() => router.back()} />
<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} edges={['top', 'bottom']}>
<StatusBar style="auto" />
<SimpleHeader title="身份认证" onBack={() => router.back()} />
<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;