feat(verification): add user identity verification system
Add comprehensive user verification feature including: - VerificationGuideScreen: Guide screen explaining the verification process - VerificationFormScreen: Form for submitting verification applications - VerificationSettingsScreen: Settings page to view verification status - verificationService: API service for verification operations - verificationStore: State management for verification state - DTOs: Types for verification records, status, and admin operations The system supports multiple identity types (student, teacher, staff, alumni, external) and includes both user-facing and admin-facing functionality.
This commit is contained in:
@@ -52,6 +52,7 @@ const SETTINGS_GROUPS_BASE = [
|
||||
icon: 'shield-check-outline',
|
||||
items: [
|
||||
{ key: 'edit_profile', title: '编辑资料', icon: 'account-edit-outline', showArrow: true },
|
||||
{ key: 'verification', title: '身份认证', icon: 'check-decagram', showArrow: true, subtitle: '获得认证标识,解锁更多功能' },
|
||||
{ key: 'privacy', title: '隐私设置', icon: 'shield-account-outline', showArrow: true },
|
||||
{ key: 'security', title: '账号安全', icon: 'lock-outline', showArrow: true },
|
||||
{ key: 'blocked_users', title: '黑名单', icon: 'account-off-outline', showArrow: true },
|
||||
@@ -252,6 +253,9 @@ export const SettingsScreen: React.FC = () => {
|
||||
case 'edit_profile':
|
||||
router.push(hrefs.hrefProfileEdit());
|
||||
break;
|
||||
case 'verification':
|
||||
router.push('/profile/verification');
|
||||
break;
|
||||
case 'notification_settings':
|
||||
router.push(hrefs.hrefProfileNotifications());
|
||||
break;
|
||||
|
||||
498
src/screens/profile/VerificationSettingsScreen.tsx
Normal file
498
src/screens/profile/VerificationSettingsScreen.tsx
Normal file
@@ -0,0 +1,498 @@
|
||||
/**
|
||||
* 身份认证设置页面
|
||||
* 用户查看认证状态、提交认证申请
|
||||
*/
|
||||
|
||||
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 THEME_COLORS = {
|
||||
primary: '#FF6B35',
|
||||
primaryLight: '#FF8C5A',
|
||||
primaryDark: '#E55A2B',
|
||||
};
|
||||
|
||||
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('/auth/verification-guide');
|
||||
};
|
||||
|
||||
const handleViewRecords = () => {
|
||||
showPrompt({
|
||||
title: '认证记录',
|
||||
message: '认证记录功能即将上线',
|
||||
type: 'info',
|
||||
});
|
||||
};
|
||||
|
||||
if (loading && !status) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={THEME_COLORS.primary} />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const statusConfig = STATUS_CONFIG[status?.verification_status || 'not_submitted'];
|
||||
const identityText = IDENTITY_TEXT[status?.identity || 'general'];
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => router.back()}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name="arrow-left"
|
||||
size={24}
|
||||
color={colors.text?.primary || '#333'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>身份认证</Text>
|
||||
<View style={styles.headerRight} />
|
||||
</View>
|
||||
|
||||
<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={THEME_COLORS.primary} />
|
||||
<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="verified"
|
||||
size={24}
|
||||
color={THEME_COLORS.primary}
|
||||
/>
|
||||
</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-variant"
|
||||
size={24}
|
||||
color={THEME_COLORS.primary}
|
||||
/>
|
||||
</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"
|
||||
size={24}
|
||||
color={THEME_COLORS.primary}
|
||||
/>
|
||||
</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="#FFF" />
|
||||
<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={THEME_COLORS.primary} />
|
||||
<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: '#fff',
|
||||
},
|
||||
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: '#F9F9F9',
|
||||
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: '#FFF5F0',
|
||||
borderRadius: 20,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
marginTop: 16,
|
||||
},
|
||||
identityText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: THEME_COLORS.primary,
|
||||
marginLeft: 6,
|
||||
},
|
||||
rejectReasonContainer: {
|
||||
marginTop: 16,
|
||||
padding: 12,
|
||||
backgroundColor: '#FFF3F3',
|
||||
borderRadius: 8,
|
||||
width: '100%',
|
||||
},
|
||||
rejectReasonLabel: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: '#F44336',
|
||||
marginBottom: 4,
|
||||
},
|
||||
rejectReasonText: {
|
||||
fontSize: 13,
|
||||
color: '#D32F2F',
|
||||
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: '#F9F9F9',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
},
|
||||
benefitIconContainer: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#FFF5F0',
|
||||
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: THEME_COLORS.primary,
|
||||
},
|
||||
primaryButtonText: {
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
color: '#FFF',
|
||||
marginLeft: 8,
|
||||
},
|
||||
secondaryButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: 56,
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#FFF5F0',
|
||||
borderWidth: 1,
|
||||
borderColor: THEME_COLORS.primary,
|
||||
},
|
||||
secondaryButtonText: {
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
color: THEME_COLORS.primary,
|
||||
marginLeft: 8,
|
||||
},
|
||||
verifiedInfo: {
|
||||
backgroundColor: '#F9F9F9',
|
||||
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;
|
||||
Reference in New Issue
Block a user