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:
425
src/screens/auth/VerificationFormScreen.tsx
Normal file
425
src/screens/auth/VerificationFormScreen.tsx
Normal file
@@ -0,0 +1,425 @@
|
||||
/**
|
||||
* 身份认证申请表单页面
|
||||
* 用户填写认证信息并提交申请
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Image,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { useAppColors, type AppColors } from '../../theme';
|
||||
import { useVerificationStore } from '../../stores/verificationStore';
|
||||
import { showPrompt } from '../../services/promptService';
|
||||
import type { UserIdentity } from '../../types/dto';
|
||||
|
||||
const THEME_COLORS = {
|
||||
primary: '#FF6B35',
|
||||
primaryLight: '#FF8C5A',
|
||||
primaryDark: '#E55A2B',
|
||||
};
|
||||
|
||||
const IDENTITY_CONFIG: Record<string, { title: string; idField: string; idLabel: string }> = {
|
||||
student: { title: '在校学生', idField: 'student_id', idLabel: '学号' },
|
||||
teacher: { title: '教职工', idField: 'employee_id', idLabel: '工号' },
|
||||
staff: { title: '工作人员', idField: 'employee_id', idLabel: '工号' },
|
||||
alumni: { title: '校友', idField: 'student_id', idLabel: '学号(原)' },
|
||||
};
|
||||
|
||||
export const VerificationFormScreen: React.FC = () => {
|
||||
const colors = useAppColors();
|
||||
const styles = createStyles(colors);
|
||||
const router = useRouter();
|
||||
const params = useLocalSearchParams<{ identity?: string }>();
|
||||
const { submitVerification, isLoading } = useVerificationStore();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
real_name: '',
|
||||
student_id: '',
|
||||
employee_id: '',
|
||||
department: '',
|
||||
proof_materials: '',
|
||||
});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const identity = params.identity as UserIdentity || 'student';
|
||||
const config = IDENTITY_CONFIG[identity] || IDENTITY_CONFIG.student;
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!formData.real_name.trim()) {
|
||||
newErrors.real_name = '请输入真实姓名';
|
||||
}
|
||||
|
||||
if (config.idField === 'student_id' && !formData.student_id.trim()) {
|
||||
newErrors.student_id = `请输入${config.idLabel}`;
|
||||
}
|
||||
|
||||
if (config.idField === 'employee_id' && !formData.employee_id.trim()) {
|
||||
newErrors.employee_id = `请输入${config.idLabel}`;
|
||||
}
|
||||
|
||||
if (!formData.proof_materials) {
|
||||
newErrors.proof_materials = '请上传证明材料';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handlePickImage = async () => {
|
||||
try {
|
||||
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
if (!permissionResult.granted) {
|
||||
Alert.alert('提示', '需要相册权限才能上传图片');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||
allowsEditing: true,
|
||||
aspect: [4, 3],
|
||||
quality: 0.8,
|
||||
});
|
||||
|
||||
if (!result.canceled && result.assets[0]) {
|
||||
setFormData({ ...formData, proof_materials: result.assets[0].uri });
|
||||
if (errors.proof_materials) {
|
||||
setErrors({ ...errors, proof_materials: '' });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to pick image:', error);
|
||||
Alert.alert('错误', '选择图片失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) return;
|
||||
|
||||
const success = await submitVerification({
|
||||
identity,
|
||||
real_name: formData.real_name.trim(),
|
||||
student_id: formData.student_id.trim() || undefined,
|
||||
employee_id: formData.employee_id.trim() || undefined,
|
||||
department: formData.department.trim() || undefined,
|
||||
proof_materials: formData.proof_materials,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
showPrompt({
|
||||
title: '提交成功',
|
||||
message: '您的认证申请已提交,请等待审核',
|
||||
type: 'success',
|
||||
});
|
||||
router.back();
|
||||
} else {
|
||||
const error = useVerificationStore.getState().error;
|
||||
showPrompt({
|
||||
title: '提交失败',
|
||||
message: error || '请稍后重试',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderInput = (
|
||||
field: keyof typeof formData,
|
||||
label: string,
|
||||
placeholder: string,
|
||||
options: {
|
||||
required?: boolean;
|
||||
keyboardType?: 'default' | 'numeric' | 'email-address';
|
||||
maxLength?: number;
|
||||
} = {}
|
||||
) => {
|
||||
const { required, keyboardType = 'default', maxLength } = options;
|
||||
const error = errors[field];
|
||||
|
||||
return (
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.inputLabel}>
|
||||
{label}
|
||||
{required && <Text style={styles.required}> *</Text>}
|
||||
</Text>
|
||||
<View style={[styles.inputWrapper, error && styles.inputWrapperError]}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={colors.text?.hint || '#999'}
|
||||
value={formData[field]}
|
||||
onChangeText={(text) => {
|
||||
setFormData({ ...formData, [field]: text });
|
||||
if (errors[field]) {
|
||||
setErrors({ ...errors, [field]: '' });
|
||||
}
|
||||
}}
|
||||
keyboardType={keyboardType}
|
||||
maxLength={maxLength}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
</View>
|
||||
{error && <Text style={styles.errorText}>{error}</Text>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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}>{config.title}认证</Text>
|
||||
<View style={styles.headerRight} />
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View style={styles.infoCard}>
|
||||
<MaterialCommunityIcons
|
||||
name="information-outline"
|
||||
size={20}
|
||||
color={THEME_COLORS.primary}
|
||||
/>
|
||||
<Text style={styles.infoText}>
|
||||
请填写真实信息,上传清晰的证明材料照片,审核通过后将获得认证标识
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.formSection}>
|
||||
{renderInput('real_name', '真实姓名', '请输入身份证上的姓名', { required: true })}
|
||||
|
||||
{config.idField === 'student_id' && (
|
||||
renderInput('student_id', config.idLabel, `请输入${config.idLabel}`, {
|
||||
required: true,
|
||||
keyboardType: 'numeric',
|
||||
maxLength: 20,
|
||||
})
|
||||
)}
|
||||
|
||||
{config.idField === 'employee_id' && (
|
||||
renderInput('employee_id', config.idLabel, `请输入${config.idLabel}`, {
|
||||
required: true,
|
||||
keyboardType: 'numeric',
|
||||
maxLength: 20,
|
||||
})
|
||||
)}
|
||||
|
||||
{renderInput('department', '院系/部门', '选填,如:计算机学院', {
|
||||
maxLength: 50,
|
||||
})}
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.inputLabel}>
|
||||
证明材料<Text style={styles.required}> *</Text>
|
||||
</Text>
|
||||
<Text style={styles.hintText}>
|
||||
请上传学生证、工作证、教师资格证等证明材料照片
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.uploadButton, errors.proof_materials && styles.uploadButtonError]}
|
||||
onPress={handlePickImage}
|
||||
activeOpacity={0.8}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{formData.proof_materials ? (
|
||||
<Image
|
||||
source={{ uri: formData.proof_materials }}
|
||||
style={styles.previewImage}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.uploadPlaceholder}>
|
||||
<MaterialCommunityIcons
|
||||
name="camera-plus-outline"
|
||||
size={40}
|
||||
color={colors.text?.hint || '#999'}
|
||||
/>
|
||||
<Text style={styles.uploadText}>点击上传图片</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
{errors.proof_materials && (
|
||||
<Text style={styles.errorText}>{errors.proof_materials}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.submitButton, isLoading && styles.submitButtonDisabled]}
|
||||
onPress={handleSubmit}
|
||||
activeOpacity={0.9}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator size="small" color="#FFF" />
|
||||
) : (
|
||||
<Text style={styles.submitButtonText}>提交认证申请</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
function createStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
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,
|
||||
},
|
||||
infoCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: '#FFF5F0',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 24,
|
||||
},
|
||||
infoText: {
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
color: colors.text?.secondary || '#666',
|
||||
marginLeft: 8,
|
||||
lineHeight: 20,
|
||||
},
|
||||
formSection: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
color: colors.text?.primary || '#333',
|
||||
marginBottom: 8,
|
||||
},
|
||||
required: {
|
||||
color: THEME_COLORS.primary,
|
||||
},
|
||||
hintText: {
|
||||
fontSize: 13,
|
||||
color: colors.text?.hint || '#999',
|
||||
marginBottom: 8,
|
||||
},
|
||||
inputWrapper: {
|
||||
backgroundColor: '#F5F5F7',
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
height: 50,
|
||||
borderWidth: 1,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
inputWrapperError: {
|
||||
borderColor: colors.error?.main || '#FF4444',
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
color: colors.text?.primary || '#333',
|
||||
height: 50,
|
||||
},
|
||||
errorText: {
|
||||
fontSize: 12,
|
||||
color: colors.error?.main || '#FF4444',
|
||||
marginTop: 6,
|
||||
},
|
||||
uploadButton: {
|
||||
backgroundColor: '#F5F5F7',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
minHeight: 200,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
uploadButtonError: {
|
||||
borderColor: colors.error?.main || '#FF4444',
|
||||
},
|
||||
uploadPlaceholder: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
},
|
||||
uploadText: {
|
||||
fontSize: 14,
|
||||
color: colors.text?.hint || '#999',
|
||||
marginTop: 12,
|
||||
},
|
||||
previewImage: {
|
||||
width: '100%',
|
||||
height: 200,
|
||||
},
|
||||
submitButton: {
|
||||
height: 56,
|
||||
borderRadius: 14,
|
||||
backgroundColor: THEME_COLORS.primary,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
submitButtonDisabled: {
|
||||
opacity: 0.7,
|
||||
},
|
||||
submitButtonText: {
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default VerificationFormScreen;
|
||||
338
src/screens/auth/VerificationGuideScreen.tsx
Normal file
338
src/screens/auth/VerificationGuideScreen.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* 注册后身份认证引导页面
|
||||
* 可跳过的认证界面,引导用户完成身份认证
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Image,
|
||||
Alert,
|
||||
} 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 * as hrefs from '../../navigation/hrefs';
|
||||
|
||||
const THEME_COLORS = {
|
||||
primary: '#FF6B35',
|
||||
primaryLight: '#FF8C5A',
|
||||
primaryDark: '#E55A2B',
|
||||
};
|
||||
|
||||
// 身份类型选项
|
||||
const IDENTITY_OPTIONS = [
|
||||
{
|
||||
key: 'student',
|
||||
title: '在校学生',
|
||||
description: '需要提交学生证或校园卡照片',
|
||||
icon: 'school-outline',
|
||||
},
|
||||
{
|
||||
key: 'teacher',
|
||||
title: '教职工',
|
||||
description: '需要提交工作证或教师资格证',
|
||||
icon: 'chalkboard-teacher',
|
||||
},
|
||||
{
|
||||
key: 'staff',
|
||||
title: '工作人员',
|
||||
description: '需要提交工作证或在职证明',
|
||||
icon: 'badge-account-outline',
|
||||
},
|
||||
{
|
||||
key: 'alumni',
|
||||
title: '校友',
|
||||
description: '需要提交毕业证或校友卡',
|
||||
icon: 'account-school-outline',
|
||||
},
|
||||
];
|
||||
|
||||
export const VerificationGuideScreen: React.FC = () => {
|
||||
const colors = useAppColors();
|
||||
const styles = createStyles(colors);
|
||||
const router = useRouter();
|
||||
const [selectedIdentity, setSelectedIdentity] = useState<string | null>(null);
|
||||
|
||||
const handleSkip = () => {
|
||||
Alert.alert(
|
||||
'跳过认证',
|
||||
'跳过后您将以普通用户身份使用,部分功能可能受限。您可以在设置中随时完成认证。',
|
||||
[
|
||||
{ text: '继续认证', style: 'cancel' },
|
||||
{
|
||||
text: '跳过',
|
||||
style: 'default',
|
||||
onPress: () => {
|
||||
router.replace(hrefs.hrefHome());
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const handleContinue = () => {
|
||||
if (!selectedIdentity) {
|
||||
Alert.alert('提示', '请选择您的身份类型');
|
||||
return;
|
||||
}
|
||||
// 导航到认证表单页面
|
||||
router.push({
|
||||
pathname: '/auth/verification-form',
|
||||
params: { identity: selectedIdentity },
|
||||
});
|
||||
};
|
||||
|
||||
const renderIdentityCard = (option: typeof IDENTITY_OPTIONS[0]) => {
|
||||
const isSelected = selectedIdentity === option.key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={option.key}
|
||||
style={[styles.identityCard, isSelected && styles.identityCardSelected]}
|
||||
onPress={() => setSelectedIdentity(option.key)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<View style={styles.identityIconContainer}>
|
||||
<MaterialCommunityIcons
|
||||
name={option.icon as any}
|
||||
size={32}
|
||||
color={isSelected ? THEME_COLORS.primary : colors.text?.secondary || '#666'}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.identityContent}>
|
||||
<Text style={[styles.identityTitle, isSelected && styles.identityTitleSelected]}>
|
||||
{option.title}
|
||||
</Text>
|
||||
<Text style={styles.identityDescription}>{option.description}</Text>
|
||||
</View>
|
||||
<View style={styles.checkContainer}>
|
||||
{isSelected && (
|
||||
<MaterialCommunityIcons
|
||||
name="check-circle"
|
||||
size={24}
|
||||
color={THEME_COLORS.primary}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* 头部图标 */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.iconCircle}>
|
||||
<MaterialCommunityIcons
|
||||
name="shield-check-outline"
|
||||
size={48}
|
||||
color={THEME_COLORS.primary}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.title}>身份认证</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
完成身份认证,解锁更多专属功能
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 认证优势 */}
|
||||
<View style={styles.benefitsContainer}>
|
||||
<View style={styles.benefitItem}>
|
||||
<MaterialCommunityIcons
|
||||
name="verified"
|
||||
size={20}
|
||||
color={THEME_COLORS.primary}
|
||||
/>
|
||||
<Text style={styles.benefitText}>获得认证标识</Text>
|
||||
</View>
|
||||
<View style={styles.benefitItem}>
|
||||
<MaterialCommunityIcons
|
||||
name="lock-open-outline"
|
||||
size={20}
|
||||
color={THEME_COLORS.primary}
|
||||
/>
|
||||
<Text style={styles.benefitText}>解锁专属频道</Text>
|
||||
</View>
|
||||
<View style={styles.benefitItem}>
|
||||
<MaterialCommunityIcons
|
||||
name="account-group"
|
||||
size={20}
|
||||
color={THEME_COLORS.primary}
|
||||
/>
|
||||
<Text style={styles.benefitText}>加入社群圈子</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 身份选择 */}
|
||||
<View style={styles.sectionContainer}>
|
||||
<Text style={styles.sectionTitle}>选择您的身份</Text>
|
||||
{IDENTITY_OPTIONS.map(renderIdentityCard)}
|
||||
</View>
|
||||
|
||||
{/* 按钮区域 */}
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.continueButton, !selectedIdentity && styles.continueButtonDisabled]}
|
||||
onPress={handleContinue}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<Text style={styles.continueButtonText}>继续认证</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles.skipButton} onPress={handleSkip} activeOpacity={0.8}>
|
||||
<Text style={styles.skipButtonText}>暂不认证</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
function createStyles(colors: AppColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: 24,
|
||||
paddingTop: 40,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
header: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 32,
|
||||
},
|
||||
iconCircle: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 50,
|
||||
backgroundColor: '#FFF5F0',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: '700',
|
||||
color: colors.text?.primary || '#333',
|
||||
marginBottom: 8,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 15,
|
||||
color: colors.text?.secondary || '#666',
|
||||
textAlign: 'center',
|
||||
},
|
||||
benefitsContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
gap: 16,
|
||||
marginBottom: 40,
|
||||
},
|
||||
benefitItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
benefitText: {
|
||||
fontSize: 13,
|
||||
color: colors.text?.secondary || '#666',
|
||||
},
|
||||
sectionContainer: {
|
||||
marginBottom: 32,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: colors.text?.primary || '#333',
|
||||
marginBottom: 16,
|
||||
},
|
||||
identityCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
backgroundColor: '#F5F5F7',
|
||||
borderRadius: 14,
|
||||
marginBottom: 12,
|
||||
borderWidth: 2,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
identityCardSelected: {
|
||||
backgroundColor: '#FFF5F0',
|
||||
borderColor: THEME_COLORS.primary,
|
||||
},
|
||||
identityIconContainer: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 16,
|
||||
},
|
||||
identityContent: {
|
||||
flex: 1,
|
||||
},
|
||||
identityTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: colors.text?.primary || '#333',
|
||||
marginBottom: 4,
|
||||
},
|
||||
identityTitleSelected: {
|
||||
color: THEME_COLORS.primary,
|
||||
},
|
||||
identityDescription: {
|
||||
fontSize: 13,
|
||||
color: colors.text?.secondary || '#666',
|
||||
},
|
||||
checkContainer: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonContainer: {
|
||||
gap: 12,
|
||||
},
|
||||
continueButton: {
|
||||
height: 56,
|
||||
borderRadius: 14,
|
||||
backgroundColor: THEME_COLORS.primary,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
continueButtonDisabled: {
|
||||
backgroundColor: '#ccc',
|
||||
},
|
||||
continueButtonText: {
|
||||
fontSize: 17,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
},
|
||||
skipButton: {
|
||||
height: 56,
|
||||
borderRadius: 14,
|
||||
backgroundColor: 'transparent',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.divider || '#E0E0E0',
|
||||
},
|
||||
skipButtonText: {
|
||||
fontSize: 17,
|
||||
fontWeight: '500',
|
||||
color: colors.text?.secondary || '#666',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default VerificationGuideScreen;
|
||||
@@ -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