From 542d385d08bd12daffa761471e94c5f25cc7c9de Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Sun, 5 Apr 2026 20:26:51 +0800 Subject: [PATCH] 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. --- src/screens/auth/VerificationFormScreen.tsx | 425 +++++++++++++++ src/screens/auth/VerificationGuideScreen.tsx | 338 ++++++++++++ src/screens/profile/SettingsScreen.tsx | 4 + .../profile/VerificationSettingsScreen.tsx | 498 ++++++++++++++++++ src/services/index.ts | 4 + src/services/verificationService.ts | 68 +++ src/stores/verificationStore.ts | 114 ++++ src/types/dto.ts | 75 ++- 8 files changed, 1525 insertions(+), 1 deletion(-) create mode 100644 src/screens/auth/VerificationFormScreen.tsx create mode 100644 src/screens/auth/VerificationGuideScreen.tsx create mode 100644 src/screens/profile/VerificationSettingsScreen.tsx create mode 100644 src/services/verificationService.ts create mode 100644 src/stores/verificationStore.ts diff --git a/src/screens/auth/VerificationFormScreen.tsx b/src/screens/auth/VerificationFormScreen.tsx new file mode 100644 index 0000000..21fb50a --- /dev/null +++ b/src/screens/auth/VerificationFormScreen.tsx @@ -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 = { + 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>({}); + + const identity = params.identity as UserIdentity || 'student'; + const config = IDENTITY_CONFIG[identity] || IDENTITY_CONFIG.student; + + const validateForm = (): boolean => { + const newErrors: Record = {}; + + 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 ( + + + {label} + {required && *} + + + { + setFormData({ ...formData, [field]: text }); + if (errors[field]) { + setErrors({ ...errors, [field]: '' }); + } + }} + keyboardType={keyboardType} + maxLength={maxLength} + editable={!isLoading} + /> + + {error && {error}} + + ); + }; + + return ( + + + router.back()} + activeOpacity={0.8} + > + + + {config.title}认证 + + + + + + + + 请填写真实信息,上传清晰的证明材料照片,审核通过后将获得认证标识 + + + + + {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, + })} + + + + 证明材料 * + + + 请上传学生证、工作证、教师资格证等证明材料照片 + + + {formData.proof_materials ? ( + + ) : ( + + + 点击上传图片 + + )} + + {errors.proof_materials && ( + {errors.proof_materials} + )} + + + + + {isLoading ? ( + + ) : ( + 提交认证申请 + )} + + + + ); +}; + +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; \ No newline at end of file diff --git a/src/screens/auth/VerificationGuideScreen.tsx b/src/screens/auth/VerificationGuideScreen.tsx new file mode 100644 index 0000000..7ac84da --- /dev/null +++ b/src/screens/auth/VerificationGuideScreen.tsx @@ -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(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 ( + setSelectedIdentity(option.key)} + activeOpacity={0.8} + > + + + + + + {option.title} + + {option.description} + + + {isSelected && ( + + )} + + + ); + }; + + return ( + + + {/* 头部图标 */} + + + + + 身份认证 + + 完成身份认证,解锁更多专属功能 + + + + {/* 认证优势 */} + + + + 获得认证标识 + + + + 解锁专属频道 + + + + 加入社群圈子 + + + + {/* 身份选择 */} + + 选择您的身份 + {IDENTITY_OPTIONS.map(renderIdentityCard)} + + + {/* 按钮区域 */} + + + 继续认证 + + + + 暂不认证 + + + + + ); +}; + +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; diff --git a/src/screens/profile/SettingsScreen.tsx b/src/screens/profile/SettingsScreen.tsx index 07825cc..c9a7511 100644 --- a/src/screens/profile/SettingsScreen.tsx +++ b/src/screens/profile/SettingsScreen.tsx @@ -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; diff --git a/src/screens/profile/VerificationSettingsScreen.tsx b/src/screens/profile/VerificationSettingsScreen.tsx new file mode 100644 index 0000000..bf1ea14 --- /dev/null +++ b/src/screens/profile/VerificationSettingsScreen.tsx @@ -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 = { + 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 ( + + + + + + ); + } + + const statusConfig = STATUS_CONFIG[status?.verification_status || 'not_submitted']; + const identityText = IDENTITY_TEXT[status?.identity || 'general']; + + return ( + + + router.back()} + activeOpacity={0.8} + > + + + 身份认证 + + + + + {/* 状态卡片 */} + + + + + {statusConfig.title} + {statusConfig.description} + + {status?.verification_status === 'approved' && ( + + + {identityText} + + )} + + {status?.latest_record?.reject_reason && ( + + 拒绝原因: + + {status.latest_record.reject_reason} + + + )} + + + {/* 认证优势 */} + {status?.verification_status !== 'approved' && ( + + 认证优势 + + + + + + + 专属认证标识 + + 获得身份认证徽章,提升账号可信度 + + + + + + + + + + 解锁专属频道 + + 访问仅限认证用户的专属内容和频道 + + + + + + + + + + 加入社群圈子 + + 加入需要身份认证的校友群、班级群等 + + + + + + )} + + {/* 操作按钮 */} + + {(status?.verification_status === 'not_submitted' || + status?.verification_status === 'rejected') && ( + + + + {status?.verification_status === 'rejected' ? '重新认证' : '开始认证'} + + + )} + + {status?.verification_status === 'pending' && ( + + + 查看认证记录 + + )} + + {status?.verification_status === 'approved' && ( + <> + + + 认证身份 + {identityText} + + {status.latest_record?.real_name && ( + + 真实姓名 + + {status.latest_record.real_name} + + + )} + {status.latest_record?.department && ( + + 院系/部门 + + {status.latest_record.department} + + + )} + + + )} + + + {/* 帮助信息 */} + + 遇到问题? + + 如果在认证过程中遇到问题,请联系客服或在设置中提交反馈 + + + + + ); +}; + +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; \ No newline at end of file diff --git a/src/services/index.ts b/src/services/index.ts index 0982e3c..aaa9224 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -114,3 +114,7 @@ export { showConfirm } from './dialogService'; // APK 更新检查服务 export { apkUpdateService, checkForAPKUpdate, manualCheckForUpdate } from './apkUpdateService'; export type { APKVersionInfo, VersionCheckResult } from './apkUpdateService'; + +// 身份认证服务 +export { verificationService } from './verificationService'; +export type { SubmitVerificationResponse } from './verificationService'; diff --git a/src/services/verificationService.ts b/src/services/verificationService.ts new file mode 100644 index 0000000..d1a1ee4 --- /dev/null +++ b/src/services/verificationService.ts @@ -0,0 +1,68 @@ +/** + * 身份认证服务 + * 处理用户身份认证申请、状态查询等功能 + */ + +import { api } from './api'; +import { + VerificationStatusDTO, + VerificationRecordDTO, + SubmitVerificationRequest, + PaginatedData, +} from '../types/dto'; + +// 提交认证响应 +export interface SubmitVerificationResponse { + id: string; + user_id: string; + identity: string; + status: string; + real_name: string; + student_id?: string; + employee_id?: string; + department?: string; + proof_materials: string; + created_at: string; + updated_at: string; +} + +class VerificationService { + // 获取当前用户的认证状态 + async getVerificationStatus(): Promise { + try { + const response = await api.get('/verification/status'); + return response.data; + } catch (error) { + console.error('[VerificationService] 获取认证状态失败:', error); + return null; + } + } + + // 提交认证申请 + async submitVerification(data: SubmitVerificationRequest): Promise { + try { + const response = await api.post('/verification/submit', data); + return response.data; + } catch (error: any) { + console.error('[VerificationService] 提交认证申请失败:', error); + throw error; + } + } + + // 获取认证记录列表 + async getVerificationRecords(page = 1, pageSize = 10): Promise | null> { + try { + const response = await api.get>('/verification/records', { + page, + page_size: pageSize, + }); + return response.data; + } catch (error) { + console.error('[VerificationService] 获取认证记录失败:', error); + return null; + } + } +} + +// 导出服务实例 +export const verificationService = new VerificationService(); diff --git a/src/stores/verificationStore.ts b/src/stores/verificationStore.ts new file mode 100644 index 0000000..67f0580 --- /dev/null +++ b/src/stores/verificationStore.ts @@ -0,0 +1,114 @@ +/** + * 身份认证状态管理 + * 处理用户身份认证申请、状态查询等功能 + */ + +import { create } from 'zustand'; +import { verificationService } from '../services/verificationService'; +import { + VerificationStatusDTO, + VerificationRecordDTO, + SubmitVerificationRequest, + UserIdentity, + VerificationStatus, +} from '../types/dto'; + +interface VerificationState { + // 状态 + status: VerificationStatusDTO | null; + records: VerificationRecordDTO[]; + isLoading: boolean; + error: string | null; + hasFetchedStatus: boolean; + + // 操作 + fetchStatus: () => Promise; + fetchRecords: (page?: number, pageSize?: number) => Promise; + submitVerification: (data: SubmitVerificationRequest) => Promise; + clearError: () => void; + reset: () => void; +} + +export const useVerificationStore = create((set, get) => ({ + status: null, + records: [], + isLoading: false, + error: null, + hasFetchedStatus: false, + + // 获取认证状态 + fetchStatus: async () => { + set({ isLoading: true, error: null }); + try { + const status = await verificationService.getVerificationStatus(); + set({ status, isLoading: false, hasFetchedStatus: true }); + } catch (error: any) { + set({ + isLoading: false, + error: error?.message || '获取认证状态失败', + hasFetchedStatus: true, + }); + } + }, + + // 获取认证记录 + fetchRecords: async (page = 1, pageSize = 10) => { + set({ isLoading: true, error: null }); + try { + const result = await verificationService.getVerificationRecords(page, pageSize); + if (result) { + set({ records: result.list, isLoading: false }); + } + } catch (error: any) { + set({ + isLoading: false, + error: error?.message || '获取认证记录失败', + }); + } + }, + + // 提交认证申请 + submitVerification: async (data: SubmitVerificationRequest) => { + set({ isLoading: true, error: null }); + try { + const result = await verificationService.submitVerification(data); + if (result) { + // 提交成功后刷新状态 + await get().fetchStatus(); + set({ isLoading: false }); + return true; + } + return false; + } catch (error: any) { + set({ + isLoading: false, + error: error?.message || '提交认证申请失败', + }); + return false; + } + }, + + // 清除错误 + clearError: () => set({ error: null }), + + // 重置状态 + reset: () => set({ + status: null, + records: [], + isLoading: false, + error: null, + hasFetchedStatus: false, + }), +})); + +// 导出便捷 hooks +export const useVerificationStatus = () => useVerificationStore((state) => state.status); +export const useIsVerified = () => { + const status = useVerificationStore((state) => state.status); + return status?.verification_status === 'approved'; +}; +export const useHasPendingVerification = () => { + const status = useVerificationStore((state) => state.status); + return status?.has_pending_request || status?.verification_status === 'pending'; +}; +export const useVerificationLoading = () => useVerificationStore((state) => state.isLoading); diff --git a/src/types/dto.ts b/src/types/dto.ts index e91851a..4f4d593 100644 --- a/src/types/dto.ts +++ b/src/types/dto.ts @@ -773,7 +773,80 @@ export function extractTextFromSegments(segments?: MessageSegment[]): string { return result.trim(); } -// ==================== Vote DTOs ==================== +// ==================== Verification DTOs ==================== + +// 用户身份类型 +export type UserIdentity = 'general' | 'student' | 'teacher' | 'staff' | 'alumni' | 'external'; + +// 认证状态 +export type VerificationStatus = 'not_submitted' | 'pending' | 'approved' | 'rejected'; + +// 认证记录 +export interface VerificationRecordDTO { + id: string; + user_id: string; + identity: UserIdentity; + status: VerificationStatus; + real_name: string; + student_id?: string; + employee_id?: string; + department?: string; + proof_materials: string; + reviewed_at?: string; + reviewed_by?: string; + reject_reason?: string; + created_at: string; + updated_at: string; +} + +// 认证状态响应 +export interface VerificationStatusDTO { + identity: UserIdentity; + verification_status: VerificationStatus; + has_pending_request: boolean; + latest_record?: VerificationRecordDTO; +} + +// 提交认证请求 +export interface SubmitVerificationRequest { + identity: UserIdentity; + real_name: string; + student_id?: string; + employee_id?: string; + department?: string; + proof_materials: string; +} + +// 管理端认证列表项 +export interface AdminVerificationListItemDTO { + id: string; + user_id: string; + username: string; + nickname: string; + avatar: string; + identity: UserIdentity; + status: VerificationStatus; + real_name: string; + student_id?: string; + employee_id?: string; + department?: string; + created_at: string; + reviewed_at?: string; + reviewer_name?: string; +} + +// 管理端认证详情 +export interface AdminVerificationDetailDTO extends AdminVerificationListItemDTO { + proof_materials: string; + reviewed_by?: string; + reject_reason?: string; +} + +// 审核认证请求 +export interface ReviewVerificationRequest { + approve: boolean; + reject_reason?: string; +} // 投票选项 export interface VoteOptionDTO {