feat: 重置密码

This commit is contained in:
jinyaqiu 2025-08-04 11:49:57 +08:00
parent 3fa9acc15d
commit c406312e0a
3 changed files with 130 additions and 47 deletions

View File

@ -1,4 +1,5 @@
import { HapticTab } from '@/components/HapticTab'; import { HapticTab } from '@/components/HapticTab';
import AskNavbar from '@/components/layout/ask';
import { TabBarIcon } from '@/components/navigation/TabBarIcon'; import { TabBarIcon } from '@/components/navigation/TabBarIcon';
import { requestNotificationPermission } from '@/components/owner/utils'; import { requestNotificationPermission } from '@/components/owner/utils';
import TabBarBackground from '@/components/ui/TabBarBackground'; import TabBarBackground from '@/components/ui/TabBarBackground';
@ -7,14 +8,13 @@ import { useColorScheme } from '@/hooks/useColorScheme';
import { prefetchChats } from '@/lib/prefetch'; import { prefetchChats } from '@/lib/prefetch';
import { fetchApi } from '@/lib/server-api-util'; import { fetchApi } from '@/lib/server-api-util';
import { webSocketManager, WebSocketStatus } from '@/lib/websocket-util'; import { webSocketManager, WebSocketStatus } from '@/lib/websocket-util';
import { TransitionPresets } from '@react-navigation/bottom-tabs';
import * as Notifications from 'expo-notifications'; import * as Notifications from 'expo-notifications';
import { Tabs } from 'expo-router'; import { Tabs } from 'expo-router';
import * as SecureStore from 'expo-secure-store'; import * as SecureStore from 'expo-secure-store';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import { TransitionPresets } from '@react-navigation/bottom-tabs';
import AskNavbar from '@/components/layout/ask';
interface PollingData { interface PollingData {
title: string; title: string;

View File

@ -7,19 +7,19 @@ import { Ionicons } from '@expo/vector-icons';
import { useLocalSearchParams, useRouter } from 'expo-router'; import { useLocalSearchParams, useRouter } from 'expo-router';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ActivityIndicator, KeyboardAvoidingView, Platform, ScrollView, TextInput, TouchableOpacity, View } from 'react-native'; import { ActivityIndicator, KeyboardAvoidingView, Platform, ScrollView, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
const resetPassword = () => { const ResetPassword = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const { session_id: resetPasswordSessionId, token } = useLocalSearchParams<{ session_id: string; token: string }>(); const { session_id: resetPasswordSessionId, token } = useLocalSearchParams<{ session_id: string; token: string }>();
// 使用 auth context 登录
const { login } = useAuth(); const { login } = useAuth();
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [showSecondPassword, setShowSecondPassword] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const validatePassword = (pwd: string) => { const validatePassword = (pwd: string) => {
@ -38,8 +38,8 @@ const resetPassword = () => {
return; return;
} }
if (!validatePassword(password)) { if (password?.length < 6) {
setError(t('auth.signup.passwordAuth', { ns: 'login' })); setError(t('auth.signup.pwdLengthError', { ns: 'login' }));
return; return;
} }
@ -64,6 +64,7 @@ const resetPassword = () => {
if (login) { if (login) {
login(response, response.access_token || ''); login(response, response.access_token || '');
} }
router.push('/ask');
} catch (error) { } catch (error) {
console.error('Reset password error:', error); console.error('Reset password error:', error);
setError(t('auth.resetPwd.error', { ns: 'login' }) || 'Failed to reset password'); setError(t('auth.resetPwd.error', { ns: 'login' }) || 'Failed to reset password');
@ -75,80 +76,75 @@ const resetPassword = () => {
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
className="flex-1 bg-white" style={styles.container}
> >
<ScrollView contentContainerClassName="flex-grow justify-center p-5"> <ScrollView contentContainerStyle={styles.scrollContainer}>
<ThemedView className="w-full max-w-[400px] self-center p-5 rounded-xl bg-white"> <ThemedView style={styles.formContainer}>
<ThemedText className="text-2xl font-bold mb-6 text-center text-gray-800"> <ThemedText style={styles.title}>
{t('auth.resetPwd.title', { ns: 'login' })} {t('auth.resetPwd.title', { ns: 'login' })}
</ThemedText> </ThemedText>
{error ? ( {error ? (
<ThemedText className="text-red-500 mb-4 text-center"> <ThemedText style={styles.errorText}>
{error} {error}
</ThemedText> </ThemedText>
) : null} ) : null}
<View className="mb-6"> <View style={styles.inputContainer}>
<View className="flex-row items-center border border-gray-200 rounded-lg px-3"> <View style={styles.passwordInputContainer}>
<TextInput <TextInput
className="flex-1 h-12 text-gray-800" style={[styles.input, { flex: 1 }]}
placeholder={t('auth.login.passwordPlaceholder', { ns: 'login' })} placeholder={t('auth.signup.confirmPasswordPlaceholder', { ns: 'login' })}
placeholderTextColor="#999" placeholderTextColor="#ccc"
value={password} value={password}
onChangeText={setPassword} onChangeText={(value) => {
setPassword(value)
}}
secureTextEntry={!showPassword} secureTextEntry={!showPassword}
autoCapitalize="none"
autoCorrect={false}
/> />
<TouchableOpacity <TouchableOpacity
onPress={() => setShowPassword(!showPassword)} onPress={() => setShowPassword(!showPassword)}
className="p-2" style={styles.eyeIcon}
> >
<Ionicons <Ionicons
name={showPassword ? 'eye-off' : 'eye'} name={showPassword ? 'eye' : 'eye-off'}
size={20} size={20}
color="#666" color="#666"
/> />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={styles.passwordInputContainer}>
<View className="flex-row items-center border border-gray-200 rounded-lg px-3 mt-4">
<TextInput <TextInput
className="flex-1 h-12 text-gray-800" style={[styles.input, { flex: 1 }]}
placeholder={t('auth.signup.confirmPasswordPlaceholder', { ns: 'login' })} placeholder={t('auth.signup.confirmPasswordPlaceholder', { ns: 'login' })}
placeholderTextColor="#999" placeholderTextColor="#ccc"
value={confirmPassword} value={confirmPassword}
onChangeText={setConfirmPassword} onChangeText={(value) => {
secureTextEntry={!showPassword} setConfirmPassword(value)
autoCapitalize="none" }}
autoCorrect={false} secureTextEntry={!showSecondPassword}
returnKeyType="done"
onSubmitEditing={handleSubmit}
/> />
<TouchableOpacity <TouchableOpacity
onPress={() => setShowPassword(!showPassword)} onPress={() => setShowSecondPassword(!showSecondPassword)}
className="p-2" style={styles.eyeIcon}
> >
<Ionicons <Ionicons
name={showPassword ? 'eye-off' : 'eye'} name={showSecondPassword ? 'eye' : 'eye-off'}
size={20} size={20}
color="#666" color="#666"
/> />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
<TouchableOpacity <TouchableOpacity
className={`w-full py-4 rounded-lg items-center justify-center ${loading ? 'bg-orange-400' : 'bg-[#E2793F]'}`} style={[styles.submitButton, loading && styles.submitButtonDisabled]}
onPress={handleSubmit} onPress={handleSubmit}
disabled={loading} disabled={loading}
> >
{loading ? ( {loading ? (
<ActivityIndicator color="#fff" /> <ActivityIndicator color="#fff" />
) : ( ) : (
<ThemedText className="text-white text-base font-semibold"> <ThemedText style={styles.submitButtonText}>
{t('auth.resetPwd.resetButton', { ns: 'login' })} {t('auth.resetPwd.resetButton', { ns: 'login' })}
</ThemedText> </ThemedText>
)} )}
@ -157,6 +153,87 @@ const resetPassword = () => {
</ScrollView> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
); );
} };
export default resetPassword const styles = StyleSheet.create({
passwordInputContainer: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 12,
backgroundColor: '#FFF8DE',
overflow: 'hidden',
},
container: {
flex: 1,
backgroundColor: '#fff',
},
scrollContainer: {
flexGrow: 1,
justifyContent: 'center',
padding: 20,
},
formContainer: {
width: '100%',
maxWidth: 400,
alignSelf: 'center',
padding: 20,
borderRadius: 12,
backgroundColor: '#fff',
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 24,
textAlign: 'center',
color: '#1f2937',
},
errorText: {
color: '#ef4444',
marginBottom: 16,
textAlign: 'center',
},
inputContainer: {
marginBottom: 24,
gap: 16
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: '#e5e7eb',
borderRadius: 8,
paddingHorizontal: 12,
},
confirmInput: {
marginTop: 16,
},
input: {
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 12,
fontSize: 16,
textAlignVertical: 'center',
backgroundColor: '#FFF8DE'
},
eyeIcon: {
padding: 8,
},
submitButton: {
width: '100%',
paddingVertical: 12,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#E2793F',
},
submitButtonDisabled: {
backgroundColor: '#f59e0b',
},
submitButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
});
export default ResetPassword;

View File

@ -12,12 +12,11 @@ import {
View View
} from 'react-native'; } from 'react-native';
import { webSocketManager, WsMessage } from '@/lib/websocket-util';
import { Message } from '@/types/ask'; import { Message } from '@/types/ask';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ThemedText } from '../ThemedText'; import { ThemedText } from '../ThemedText';
import { createNewConversation } from './utils'; import { createNewConversation } from './utils';
import { WsMessage } from '@/lib/websocket-util';
import { webSocketManager } from '@/lib/websocket-util';
interface Props { interface Props {
setIsHello: Dispatch<SetStateAction<boolean>>, setIsHello: Dispatch<SetStateAction<boolean>>,
@ -38,12 +37,12 @@ export default function SendMessage(props: Props) {
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
// 添加一个ref来跟踪键盘状态 // 添加一个ref来跟踪键盘状态
const isKeyboardVisible = useRef(false); const isKeyboardVisible = useRef(false);
const chunkQueue = useRef<string[]>([]); const chunkQueue = useRef<string[]>([]);
const renderInterval = useRef<ReturnType<typeof setInterval> | null>(null); const renderInterval = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => { useEffect(() => {
const handleChatStream = (message: WsMessage) => { const handleChatStream = (message: WsMessage) => {
if (message.type !== 'ChatStream' || !message.chunk) return; if (message.type !== 'ChatStream' || !message.chunk) return;
chunkQueue.current.push(message.chunk); chunkQueue.current.push(message.chunk);
@ -76,7 +75,7 @@ export default function SendMessage(props: Props) {
} }
}; };
const handleChatStreamEnd = (message: WsMessage) => { const handleChatStreamEnd = (message: WsMessage) => {
if (message.type !== 'ChatStreamEnd') return; if (message.type !== 'ChatStreamEnd') return;
// Stop the timer and process any remaining chunks // Stop the timer and process any remaining chunks
@ -129,7 +128,7 @@ export default function SendMessage(props: Props) {
webSocketManager.subscribe('ChatStreamEnd', typedHandleChatStreamEnd); webSocketManager.subscribe('ChatStreamEnd', typedHandleChatStreamEnd);
webSocketManager.subscribe('ChatResponse', typedHandleChatResponse); webSocketManager.subscribe('ChatResponse', typedHandleChatResponse);
return () => { return () => {
webSocketManager.unsubscribe('ChatStream', typedHandleChatStream); webSocketManager.unsubscribe('ChatStream', typedHandleChatStream);
webSocketManager.unsubscribe('ChatStreamEnd', typedHandleChatStreamEnd); webSocketManager.unsubscribe('ChatStreamEnd', typedHandleChatStreamEnd);
webSocketManager.unsubscribe('ChatResponse', typedHandleChatResponse); webSocketManager.unsubscribe('ChatResponse', typedHandleChatResponse);
@ -194,6 +193,13 @@ export default function SendMessage(props: Props) {
if (!currentSessionId) { if (!currentSessionId) {
currentSessionId = await createNewConversation(text); currentSessionId = await createNewConversation(text);
setConversationId(currentSessionId); setConversationId(currentSessionId);
webSocketManager.send({
type: 'Chat',
session_id: currentSessionId,
message: text,
image_material_ids: selectedImages.length > 0 ? selectedImages : undefined,
});
setSelectedImages([]);
} }
// 通过 WebSocket 发送消息 // 通过 WebSocket 发送消息