Compare commits
6 Commits
main
...
fix/screen
| Author | SHA1 | Date | |
|---|---|---|---|
| 01bc0588b8 | |||
| 003235d732 | |||
| ecada3e279 | |||
| 3904f8da66 | |||
| ec83f9ce34 | |||
| 3bc8dda46f |
3
.gitignore
vendored
@ -42,4 +42,5 @@ app-example
|
||||
android/
|
||||
ios/
|
||||
|
||||
build*
|
||||
build*
|
||||
.env
|
||||
3
.qwen/settings.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"theme": "Qwen Light"
|
||||
}
|
||||
12
app.json
@ -46,17 +46,7 @@
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
"expo-secure-store",
|
||||
[
|
||||
"expo-font",
|
||||
{
|
||||
"fonts": [
|
||||
"./assets/fonts/Quicksand.otf",
|
||||
"./assets/fonts/SF-Pro.otf",
|
||||
"./assets/fonts/Inter-Regular.otf"
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
[
|
||||
"expo-background-task",
|
||||
{
|
||||
"minimumInterval": 15
|
||||
|
||||
23
app/(auth)/_layout.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import React from 'react';
|
||||
|
||||
export default function AuthLayout() {
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Screen
|
||||
name="login"
|
||||
options={{
|
||||
headerShown: false,
|
||||
animation: 'fade'
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="reset-password"
|
||||
options={{
|
||||
headerShown: false,
|
||||
animation: 'fade'
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@ -3,7 +3,6 @@ import ForgetPwd from '@/components/login/forgetPwd';
|
||||
import Login from '@/components/login/login';
|
||||
import PhoneLogin from '@/components/login/phoneLogin';
|
||||
import SignUp from '@/components/login/signUp';
|
||||
import PrivacyModal from '@/components/owner/qualification/privacy';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { ThemedView } from '@/components/ThemedView';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
@ -28,9 +27,6 @@ const LoginScreen = () => {
|
||||
// 判断是否有白边
|
||||
const statusBarHeight = StatusBar?.currentHeight ?? 0;
|
||||
|
||||
// 协议弹窗
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [modalType, setModalType] = useState<'ai' | 'terms' | 'privacy' | 'user' | 'membership'>('privacy');
|
||||
useEffect(() => {
|
||||
const keyboardWillShowListener = Keyboard.addListener(
|
||||
Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow',
|
||||
@ -74,15 +70,12 @@ const LoginScreen = () => {
|
||||
keyboardShouldPersistTaps="handled"
|
||||
bounces={false}
|
||||
>
|
||||
<ThemedView className="flex-1 justify-end" bgColor="bgPrimary">
|
||||
<View style={{ width: "100%", alignItems: "center", marginTop: insets.top + 8, opacity: keyboardOffset === 0 ? 1 : 0 }}>
|
||||
<ThemedText size="xl" weight="bold" color="textWhite">{t('login:auth.login.titleText')}</ThemedText>
|
||||
</View>
|
||||
<ThemedView className="flex-1 bg-bgPrimary justify-end">
|
||||
<View className="flex-1">
|
||||
<View
|
||||
className="absolute left-1/2 z-10"
|
||||
style={{
|
||||
top: containerHeight > 0 ? windowHeight - containerHeight - 210 + statusBarHeight - insets.top - 28 : 0,
|
||||
top: containerHeight > 0 ? windowHeight - containerHeight - 210 + statusBarHeight : 0,
|
||||
transform: [{ translateX: -200 }, { translateY: keyboardOffset > 0 ? -keyboardOffset + statusBarHeight : -keyboardOffset }]
|
||||
}}
|
||||
>
|
||||
@ -97,7 +90,7 @@ const LoginScreen = () => {
|
||||
<View
|
||||
className="absolute left-1/2 z-[1000] -translate-x-[39.5px] -translate-y-[4px]"
|
||||
style={{
|
||||
top: containerHeight > 0 ? windowHeight - containerHeight - 1 + statusBarHeight - insets.top - 30 : 0,
|
||||
top: containerHeight > 0 ? windowHeight - containerHeight - 1 + statusBarHeight : 0,
|
||||
transform: [{ translateX: -39.5 }, { translateY: keyboardOffset > 0 ? -4 - keyboardOffset + statusBarHeight : -4 - keyboardOffset }]
|
||||
}}
|
||||
>
|
||||
@ -105,11 +98,10 @@ const LoginScreen = () => {
|
||||
</View>
|
||||
</View>
|
||||
<ThemedView
|
||||
className="w-full pt-4 px-6 relative z-20 shadow-lg pb-5"
|
||||
bgColor="textWhite"
|
||||
className="w-full bg-white pt-4 px-6 relative z-20 shadow-lg pb-5"
|
||||
style={{
|
||||
borderTopLeftRadius: 50,
|
||||
borderTopRightRadius: 50,
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
@ -121,7 +113,7 @@ const LoginScreen = () => {
|
||||
>
|
||||
{/* 错误提示 */}
|
||||
<View className={`${error !== "123" ? 'opacity-100' : 'opacity-0'} w-full flex justify-center items-center text-primary-500 text-sm`}>
|
||||
<ThemedText size='xxs' color='bgSecondary' type='inter'>
|
||||
<ThemedText className="text-sm !text-textPrimary">
|
||||
{error}
|
||||
</ThemedText>
|
||||
</View>
|
||||
@ -162,29 +154,26 @@ const LoginScreen = () => {
|
||||
return components[status as keyof typeof components] || components.login;
|
||||
})()}
|
||||
|
||||
<View style={{ width: "100%", alignItems: "center", }}>
|
||||
{status == 'login' || !status &&
|
||||
<View className="flex-row justify-center mt-2 flex-wrap w-[85%] items-center">
|
||||
<ThemedText color='bgPrimary' size='xxs' type='inter'>
|
||||
{status === 'login' || !status ? t('auth.agree.logintext', { ns: 'login' }) : t('auth.agree.singupText', { ns: 'login' })}
|
||||
{status == 'login' || !status &&
|
||||
<View className="flex-row justify-center mt-2">
|
||||
<ThemedText className="text-sm !text-textPrimary">
|
||||
{status === 'login' || !status ? t('auth.agree.logintext', { ns: 'login' }) : t('auth.agree.singupText', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={() => { }}>
|
||||
<ThemedText className="text-sm font-semibold ml-1 !text-textPrimary underline">
|
||||
{t('auth.agree.terms', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={() => { setModalVisible(true); setModalType('terms') }}>
|
||||
<ThemedText color='bgPrimary' size='xxs' type='inter' style={{ textDecorationLine: 'underline' }}>
|
||||
{t('auth.agree.terms', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<ThemedText color='bgPrimary' size='xxs' type='inter' className='flex-wrap'>
|
||||
{t('auth.agree.join', { ns: 'login' })}
|
||||
</TouchableOpacity>
|
||||
<ThemedText className="text-sm !text-textPrimary">
|
||||
{t('auth.agree.join', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={() => { }}>
|
||||
<ThemedText className="!text-textPrimary underline text-sm font-semibold ml-1">
|
||||
{t('auth.agree.privacyPolicy', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={() => { setModalVisible(true); setModalType('privacy') }}>
|
||||
<ThemedText color='bgPrimary' size='xxs' type='inter' className='flex-wrap' style={{ textDecorationLine: 'underline' }}>
|
||||
{t('auth.agree.privacyPolicy', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
<PrivacyModal modalVisible={modalVisible} setModalVisible={setModalVisible} type={modalType} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
</ThemedView>
|
||||
</ThemedView>
|
||||
</ScrollView>
|
||||
76
app/(main)/(tabs)/_layout.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { Tabs } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { HapticTab } from '@/components/HapticTab';
|
||||
import { TransitionPresets } from '@react-navigation/bottom-tabs';
|
||||
import { Platform } from 'react-native';
|
||||
import AskNavbar from '@/components/layout/ask';
|
||||
import { webSocketManager, WebSocketStatus } from '@/lib/websocket-util';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// 只在iOS平台上导入TabBarBackground组件
|
||||
const TabBarBackground = Platform.OS === 'ios' ? require('@/components/ui/TabBarBackground').default : null;
|
||||
|
||||
export default function TabLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
const [wsStatus, setWsStatus] = useState<WebSocketStatus>('disconnected');
|
||||
|
||||
useEffect(() => {
|
||||
const handleStatusChange = (status: WebSocketStatus) => {
|
||||
setWsStatus(status);
|
||||
};
|
||||
webSocketManager.subscribeStatus(handleStatusChange);
|
||||
return () => {
|
||||
webSocketManager.unsubscribeStatus(handleStatusChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 只在iOS平台上使用TabBarBackground
|
||||
const renderTabBarBackground = () => {
|
||||
if (Platform.OS === 'ios' && TabBarBackground) {
|
||||
return <TabBarBackground />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
|
||||
headerShown: false,
|
||||
tabBarBackground: renderTabBarBackground, // 使用自定义背景
|
||||
tabBarButton: HapticTab, // 添加触觉反馈
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Home',
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />,
|
||||
...TransitionPresets.ShiftTransition,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="memo-list"
|
||||
options={{
|
||||
title: 'Memos',
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="document-text" color={color} />,
|
||||
...TransitionPresets.ShiftTransition,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="owner"
|
||||
options={{
|
||||
title: 'Profile',
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="person" color={color} />,
|
||||
...TransitionPresets.ShiftTransition,
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
<AskNavbar wsStatus={wsStatus} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
572
app/(main)/(tabs)/index.tsx
Normal file
@ -0,0 +1,572 @@
|
||||
import { checkAuthStatus } from '@/lib/auth';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Animated, Dimensions, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
export default function HomeScreen() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// 获取屏幕宽度
|
||||
const screenWidth = Dimensions.get('window').width;
|
||||
|
||||
// 动画值
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current; // IP图标的淡入动画
|
||||
const shakeAnim = useRef(new Animated.Value(0)).current; // IP图标的摇晃动画
|
||||
const animationRef = useRef<Animated.CompositeAnimation | null>(null); // 动画引用
|
||||
const descriptionAnim = useRef(new Animated.Value(0)).current; // 描述文本的淡入动画
|
||||
const buttonAnim = useRef(new Animated.Value(0)).current; // 按钮的淡入动画
|
||||
const buttonShakeAnim = useRef(new Animated.Value(0)).current; // 按钮的摇晃动画
|
||||
const buttonLoopAnim = useRef<Animated.CompositeAnimation | null>(null); // 按钮循环动画引用
|
||||
const fadeInAnim = useRef(new Animated.Value(0)).current;
|
||||
|
||||
// 文本行动画值
|
||||
const [textAnimations] = useState(() => ({
|
||||
line1: new Animated.Value(0), // 第一行文本动画
|
||||
line2: new Animated.Value(0), // 第二行文本动画
|
||||
line3: new Animated.Value(0), // 第三行文本动画
|
||||
subtitle: new Animated.Value(0), // 副标题动画
|
||||
}));
|
||||
|
||||
// 添加挥手动画值
|
||||
const waveAnim = useRef(new Animated.Value(0)).current;
|
||||
|
||||
// 启动IP图标摇晃动画
|
||||
const startShaking = () => {
|
||||
// 停止任何正在进行的动画
|
||||
if (animationRef.current) {
|
||||
animationRef.current.stop();
|
||||
}
|
||||
|
||||
// 创建动画序列
|
||||
const sequence = Animated.sequence([
|
||||
// 第一次左右摇晃
|
||||
Animated.timing(shakeAnim, {
|
||||
toValue: 1,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(shakeAnim, {
|
||||
toValue: -1,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
// 第二次左右摇晃
|
||||
Animated.timing(shakeAnim, {
|
||||
toValue: 1,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(shakeAnim, {
|
||||
toValue: -1,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
// 回到中心位置
|
||||
Animated.timing(shakeAnim, {
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
// 1秒延迟
|
||||
Animated.delay(1000),
|
||||
]);
|
||||
|
||||
// 循环播放动画序列
|
||||
animationRef.current = Animated.loop(sequence);
|
||||
animationRef.current.start();
|
||||
};
|
||||
|
||||
// 启动文本动画
|
||||
const startTextAnimations = () => {
|
||||
// 按顺序延迟启动每行文本动画
|
||||
return new Promise<void>((resolve) => {
|
||||
Animated.stagger(300, [
|
||||
Animated.timing(textAnimations.line1, {
|
||||
toValue: 1,
|
||||
duration: 500,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(textAnimations.line2, {
|
||||
toValue: 1,
|
||||
duration: 500,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(textAnimations.line3, {
|
||||
toValue: 1,
|
||||
duration: 500,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(textAnimations.subtitle, {
|
||||
toValue: 1,
|
||||
duration: 500,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start(() => resolve());
|
||||
});
|
||||
};
|
||||
|
||||
// 启动描述文本动画
|
||||
const startDescriptionAnimation = () => {
|
||||
// IP图标显示后淡入描述文本
|
||||
return new Promise<void>((resolve) => {
|
||||
Animated.sequence([
|
||||
Animated.delay(200), // IP图标显示后延迟200ms
|
||||
Animated.timing(descriptionAnim, {
|
||||
toValue: 1,
|
||||
duration: 800,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
]).start(() => resolve());
|
||||
});
|
||||
};
|
||||
// 启动欢迎语动画
|
||||
const startWelcomeAnimation = () => {
|
||||
// IP图标显示后淡入描述文本
|
||||
return new Promise<void>((resolve) => {
|
||||
Animated.sequence([
|
||||
Animated.delay(200), // IP图标显示后延迟200ms
|
||||
Animated.timing(fadeInAnim, {
|
||||
toValue: 1,
|
||||
duration: 800,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
]).start(() => resolve());
|
||||
});
|
||||
};
|
||||
|
||||
// 启动按钮动画
|
||||
const startButtonAnimation = () => {
|
||||
// 首先淡入按钮
|
||||
Animated.sequence([
|
||||
Animated.timing(buttonAnim, {
|
||||
toValue: 1,
|
||||
duration: 800,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
]).start(() => {
|
||||
// 淡入完成后开始循环摇晃动画
|
||||
startButtonShakeLoop();
|
||||
});
|
||||
};
|
||||
|
||||
// 启动按钮循环摇晃动画
|
||||
const startButtonShakeLoop = () => {
|
||||
// 停止任何正在进行的动画
|
||||
if (buttonLoopAnim.current) {
|
||||
buttonLoopAnim.current.stop();
|
||||
}
|
||||
|
||||
// 创建摇晃动画序列
|
||||
const shakeSequence = Animated.sequence([
|
||||
// 向右摇晃
|
||||
Animated.timing(buttonShakeAnim, {
|
||||
toValue: 1,
|
||||
duration: 100,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
// 向左摇晃
|
||||
Animated.timing(buttonShakeAnim, {
|
||||
toValue: -1,
|
||||
duration: 100,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
// 再次向右摇晃
|
||||
Animated.timing(buttonShakeAnim, {
|
||||
toValue: 1,
|
||||
duration: 100,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
// 回到中心位置
|
||||
Animated.timing(buttonShakeAnim, {
|
||||
toValue: 0,
|
||||
duration: 100,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
// 暂停3秒
|
||||
Animated.delay(3000)
|
||||
]);
|
||||
|
||||
// 循环播放动画序列
|
||||
buttonLoopAnim.current = Animated.loop(shakeSequence);
|
||||
buttonLoopAnim.current.start();
|
||||
};
|
||||
|
||||
// 启动挥手动画
|
||||
const startWaveAnimation = () => {
|
||||
// 创建循环动画:左右摇摆
|
||||
Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(waveAnim, {
|
||||
toValue: 1,
|
||||
duration: 500,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(waveAnim, {
|
||||
toValue: -1,
|
||||
duration: 500,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(waveAnim, {
|
||||
toValue: 0,
|
||||
duration: 500,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.delay(1000), // 暂停1秒
|
||||
])
|
||||
).start();
|
||||
};
|
||||
|
||||
// 组件挂载时启动动画
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const initializeComponent = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// 添加延迟确保 AuthProvider 已经初始化
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
await checkAuthStatus(router, () => {
|
||||
if (isMounted) {
|
||||
router.replace('/ask');
|
||||
}
|
||||
}, false);
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.error('初始化组件时出错:', error);
|
||||
if (isMounted) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
initializeComponent();
|
||||
|
||||
// IP图标的淡入动画
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 1,
|
||||
duration: 1000,
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
// 淡入完成后开始摇晃动画
|
||||
startShaking();
|
||||
// IP显示后开始文本动画
|
||||
startTextAnimations()
|
||||
.then(() => startWelcomeAnimation())
|
||||
.then(() => startDescriptionAnimation())
|
||||
.then(() => startButtonAnimation())
|
||||
.catch(console.error);
|
||||
// 启动挥手动画
|
||||
startWaveAnimation();
|
||||
});
|
||||
|
||||
// 组件卸载时清理动画和状态
|
||||
return () => {
|
||||
isMounted = false;
|
||||
|
||||
// 清理所有动画
|
||||
if (buttonLoopAnim.current) {
|
||||
buttonLoopAnim.current.stop();
|
||||
}
|
||||
if (animationRef.current) {
|
||||
animationRef.current.stop();
|
||||
}
|
||||
|
||||
// 重置所有动画值
|
||||
fadeAnim.setValue(0);
|
||||
shakeAnim.setValue(0);
|
||||
descriptionAnim.setValue(0);
|
||||
buttonAnim.setValue(0);
|
||||
buttonShakeAnim.setValue(0);
|
||||
fadeInAnim.setValue(0);
|
||||
waveAnim.setValue(0);
|
||||
|
||||
// 重置文本动画
|
||||
Object.values(textAnimations).forEach(anim => anim.setValue(0));
|
||||
};
|
||||
|
||||
}, []);
|
||||
|
||||
// 动画样式
|
||||
const animatedStyle = {
|
||||
opacity: fadeAnim,
|
||||
transform: [
|
||||
{
|
||||
translateX: shakeAnim.interpolate({
|
||||
inputRange: [-1, 1],
|
||||
outputRange: [-2, 2],
|
||||
})
|
||||
},
|
||||
{
|
||||
rotate: shakeAnim.interpolate({
|
||||
inputRange: [-1, 1],
|
||||
outputRange: ['-2deg', '2deg'],
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 旋转动画插值
|
||||
const rotate = waveAnim.interpolate({
|
||||
inputRange: [-1, 0, 1],
|
||||
outputRange: ['-15deg', '0deg', '15deg'],
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={styles.loadingText}>{t('common.loading')}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.contentContainer, { paddingTop: insets.top + 16 }]}>
|
||||
{/* 标题区域 */}
|
||||
<View style={styles.headerContainer}>
|
||||
<Animated.Text
|
||||
style={[
|
||||
styles.titleText,
|
||||
{
|
||||
opacity: textAnimations.line1, transform: [{
|
||||
translateY: textAnimations.line1.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [10, 0]
|
||||
})
|
||||
}]
|
||||
}
|
||||
]}
|
||||
>
|
||||
{t('auth.welcomeAwaken.awaken', { ns: 'login' })}
|
||||
</Animated.Text>
|
||||
<Animated.Text
|
||||
style={[
|
||||
styles.titleText,
|
||||
{
|
||||
opacity: textAnimations.line2, transform: [{
|
||||
translateY: textAnimations.line2.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [10, 0]
|
||||
})
|
||||
}]
|
||||
}
|
||||
]}
|
||||
>
|
||||
{t('auth.welcomeAwaken.your', { ns: 'login' })}
|
||||
</Animated.Text>
|
||||
<Animated.Text
|
||||
style={[
|
||||
styles.titleText,
|
||||
{
|
||||
opacity: textAnimations.line3, transform: [{
|
||||
translateY: textAnimations.line3.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [10, 0]
|
||||
})
|
||||
}]
|
||||
}
|
||||
]}
|
||||
>
|
||||
{t('auth.welcomeAwaken.pm', { ns: 'login' })}
|
||||
</Animated.Text>
|
||||
<Animated.Text
|
||||
style={[
|
||||
styles.subtitleText,
|
||||
{
|
||||
opacity: textAnimations.subtitle,
|
||||
transform: [{
|
||||
translateY: textAnimations.subtitle.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [10, 0]
|
||||
})
|
||||
}]
|
||||
}
|
||||
]}
|
||||
>
|
||||
{t('auth.welcomeAwaken.slogan', { ns: 'login' })}
|
||||
</Animated.Text>
|
||||
</View>
|
||||
{/* 欢迎语 */}
|
||||
<View style={{ alignItems: 'flex-end' }}>
|
||||
<Animated.View
|
||||
style={[{
|
||||
height: screenWidth * 0.3,
|
||||
width: screenWidth * 0.3,
|
||||
marginTop: -screenWidth * 0.08,
|
||||
opacity: fadeInAnim,
|
||||
transform: [{
|
||||
translateY: fadeInAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [20, 0]
|
||||
})
|
||||
}]
|
||||
}]}
|
||||
>
|
||||
<Image
|
||||
source={require('@/assets/images/png/icon/think.png')}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
resizeMode: 'contain'
|
||||
}}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
{/* Animated IP */}
|
||||
<View style={styles.ipContainer}>
|
||||
<Animated.View style={[styles.ipWrapper, { transform: [{ rotate }] }]}>
|
||||
<Image
|
||||
source={require('@/assets/images/png/icon/ip.png')}
|
||||
style={{ width: screenWidth * 0.9, marginBottom: - screenWidth * 0.18, marginTop: -screenWidth * 0.22 }}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
{/* 介绍文本 */}
|
||||
<Animated.Text
|
||||
style={[
|
||||
styles.descriptionText,
|
||||
{
|
||||
opacity: descriptionAnim,
|
||||
transform: [{
|
||||
translateY: descriptionAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [20, 0]
|
||||
})
|
||||
}]
|
||||
}
|
||||
]}
|
||||
>
|
||||
{t('auth.welcomeAwaken.gallery', { ns: 'login' })}
|
||||
{"\n"}
|
||||
{t('auth.welcomeAwaken.back', { ns: 'login' })}
|
||||
</Animated.Text>
|
||||
|
||||
{/* 唤醒按钮 */}
|
||||
<Animated.View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
opacity: buttonAnim,
|
||||
transform: [
|
||||
{
|
||||
translateY: buttonAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [20, 0]
|
||||
})
|
||||
},
|
||||
{
|
||||
translateX: buttonShakeAnim.interpolate({
|
||||
inputRange: [-1, 0, 1],
|
||||
outputRange: [-5, 0, 5]
|
||||
})
|
||||
}
|
||||
]
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.awakenButton}
|
||||
onPress={async () => {
|
||||
router.push('/login');
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={styles.buttonText}>
|
||||
{t('auth.welcomeAwaken.awake', { ns: 'login' })}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFB645',
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFB645',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFB645',
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
headerContainer: {
|
||||
marginBottom: 40,
|
||||
width: '100%',
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
titleText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 30,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 12,
|
||||
textAlign: 'left',
|
||||
lineHeight: 36,
|
||||
},
|
||||
subtitleText: {
|
||||
color: 'rgba(255, 255, 255, 0.85)',
|
||||
fontSize: 16,
|
||||
textAlign: 'left',
|
||||
lineHeight: 24,
|
||||
},
|
||||
ipContainer: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
minHeight: 200,
|
||||
},
|
||||
ipWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
descriptionText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
lineHeight: 24,
|
||||
opacity: 0.9,
|
||||
paddingHorizontal: 40,
|
||||
marginTop: -16,
|
||||
},
|
||||
awakenButton: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 28,
|
||||
paddingVertical: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
width: '86%',
|
||||
alignItems: 'center',
|
||||
marginTop: 24,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#4C320C',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 18,
|
||||
},
|
||||
});
|
||||
@ -13,7 +13,6 @@ import UploaderProgress from '@/components/file-upload/upload-progress/uploader-
|
||||
import SkeletonItem from '@/components/memo/SkeletonItem';
|
||||
|
||||
// 类型定义
|
||||
import { Fonts } from '@/constants/Fonts';
|
||||
import { useUploadManager } from '@/hooks/useUploadManager';
|
||||
import { getCachedData, prefetchChatDetail, prefetchChats } from '@/lib/prefetch';
|
||||
import { fetchApi } from '@/lib/server-api-util';
|
||||
@ -150,7 +149,13 @@ const MemoList = () => {
|
||||
onPress={() => handleMemoPress(item)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<ChatSvg />
|
||||
<View className="w-[3rem] h-[3rem] z-1">
|
||||
<ChatSvg
|
||||
width="100%"
|
||||
height="100%"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.memoContent}>
|
||||
<Text
|
||||
style={styles.memoTitle}
|
||||
@ -228,7 +233,7 @@ const MemoList = () => {
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<View style={[styles.container, { paddingTop: insets.top + 8 }]}>
|
||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||
<FlatList
|
||||
ref={flatListRef}
|
||||
data={historyList}
|
||||
@ -274,7 +279,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
headerContainer: {
|
||||
paddingBottom: 8,
|
||||
paddingBottom: 16,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
title: {
|
||||
@ -282,8 +287,7 @@ const styles = StyleSheet.create({
|
||||
fontWeight: 'bold',
|
||||
color: '#4C320C',
|
||||
textAlign: 'center',
|
||||
marginBottom: 8,
|
||||
fontFamily: Fonts["quicksand"]
|
||||
marginBottom: 16,
|
||||
},
|
||||
listContent: {
|
||||
paddingBottom: Platform.select({
|
||||
@ -312,19 +316,16 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
marginLeft: 12,
|
||||
justifyContent: 'center',
|
||||
gap: 2
|
||||
},
|
||||
memoTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
fontWeight: '500',
|
||||
color: '#4C320C',
|
||||
marginBottom: 4,
|
||||
fontFamily: Fonts['sfPro']
|
||||
},
|
||||
memoSubtitle: {
|
||||
fontSize: 14,
|
||||
color: '#AC7E35',
|
||||
fontFamily: Fonts['inter']
|
||||
},
|
||||
separator: {
|
||||
height: 1 / PixelRatio.get(),
|
||||
@ -13,14 +13,13 @@ import { CountData, UserInfoDetails } from '@/types/user';
|
||||
import { useFocusEffect, useRouter } from 'expo-router';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Dimensions, FlatList, StyleSheet, View } from 'react-native';
|
||||
import { FlatList, StyleSheet, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
export default function OwnerPage() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const width = Dimensions.get("window").width;
|
||||
|
||||
// 添加页面挂载状态
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
@ -120,17 +119,17 @@ export default function OwnerPage() {
|
||||
<UserInfo userInfo={userInfoDetails} />
|
||||
|
||||
{/* 会员卡 */}
|
||||
<MemberCard pro={userInfoDetails?.membership_level} points={userInfoDetails?.remain_points} />
|
||||
<MemberCard pro={userInfoDetails?.membership_level} />
|
||||
|
||||
{/* 分类 */}
|
||||
<View style={{ marginHorizontal: -16, marginBottom: -width * 0.26 }}>
|
||||
<View style={{ marginHorizontal: -16, marginBottom: -16 }}>
|
||||
<CarouselComponent data={userInfoDetails?.material_counter} />
|
||||
</View>
|
||||
|
||||
{/* 作品数据 */}
|
||||
<View className='flex flex-row justify-between gap-[1rem]'>
|
||||
<CreateCountComponent title={t("generalSetting.storiesCreated", { ns: "personal" })} icon={<StoriesSvg width={16} height={16} />} number={userInfoDetails.stories_count} />
|
||||
<CreateCountComponent title={t("generalSetting.conversationsWithMemo", { ns: "personal" })} icon={<ConversationsSvg width={16} height={16} />} number={userInfoDetails.conversations_count} />
|
||||
<CreateCountComponent title={t("generalSetting.storiesCreated", { ns: "personal" })} icon={<StoriesSvg width={30} height={30} />} number={userInfoDetails.stories_count} />
|
||||
<CreateCountComponent title={t("generalSetting.conversationsWithMemo", { ns: "personal" })} icon={<ConversationsSvg width={30} height={30} />} number={userInfoDetails.conversations_count} />
|
||||
</View>
|
||||
|
||||
{/* 排行榜 */}
|
||||
57
app/(main)/_layout.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { Tabs } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { HapticTab } from '@/components/HapticTab';
|
||||
import { webSocketManager, WebSocketStatus } from '@/lib/websocket-util';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
// 只在iOS平台上导入TabBarBackground组件
|
||||
const TabBarBackground = Platform.OS === 'ios' ? require('@/components/ui/TabBarBackground').default : null;
|
||||
|
||||
export default function MainLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
const [wsStatus, setWsStatus] = useState<WebSocketStatus>('disconnected');
|
||||
|
||||
useEffect(() => {
|
||||
const handleStatusChange = (status: WebSocketStatus) => {
|
||||
setWsStatus(status);
|
||||
};
|
||||
webSocketManager.subscribeStatus(handleStatusChange);
|
||||
return () => {
|
||||
webSocketManager.unsubscribeStatus(handleStatusChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 只在iOS平台上使用TabBarBackground
|
||||
const renderTabBarBackground = () => {
|
||||
if (Platform.OS === 'ios' && TabBarBackground) {
|
||||
return <TabBarBackground />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
|
||||
headerShown: false,
|
||||
tabBarBackground: renderTabBarBackground, // 使用自定义背景
|
||||
tabBarButton: HapticTab, // 添加触觉反馈
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="(tabs)" // 这将渲染 (tabs) 目录下的 _layout.tsx
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: 'Home',
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />,
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,12 +1,10 @@
|
||||
import ReturnArrow from "@/assets/icons/svg/returnArrow.svg";
|
||||
import Chat from "@/components/ask/chat";
|
||||
import AskHello from "@/components/ask/hello";
|
||||
import SendMessage from "@/components/ask/send";
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { fetchApi } from "@/lib/server-api-util";
|
||||
import { getWebSocketErrorMessage, webSocketManager, WsMessage } from "@/lib/websocket-util";
|
||||
import { Assistant, Message } from "@/types/ask";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useFocusEffect, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
@ -16,8 +14,6 @@ import {
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
@ -25,6 +21,7 @@ import { runOnJS } from 'react-native-reanimated';
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
export default function AskScreen() {
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const chatListRef = useRef<FlatList>(null);
|
||||
@ -36,9 +33,10 @@ export default function AskScreen() {
|
||||
const fadeAnimChat = useRef(new Animated.Value(0)).current;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { sessionId, newSession } = useLocalSearchParams<{
|
||||
const { sessionId, newSession, extra } = useLocalSearchParams<{
|
||||
sessionId: string;
|
||||
newSession: string;
|
||||
extra: string;
|
||||
}>();
|
||||
|
||||
// 创建一个可复用的滚动函数
|
||||
@ -57,11 +55,11 @@ export default function AskScreen() {
|
||||
if (translationX > threshold) {
|
||||
// 从左向右滑动,跳转页面
|
||||
runOnJS(router.replace)("/memo-list");
|
||||
runOnJS(setConversationId)("")
|
||||
}
|
||||
})
|
||||
.minPointers(1)
|
||||
.activeOffsetX([-10, 10]); // 在 X 方向触发的范围
|
||||
.activeOffsetX([-20, 20]) // 扩大触发范围,避免与ScrollView冲突
|
||||
.failOffsetY([-10, 10]); // 限制Y轴的偏移,避免垂直滚动时触发
|
||||
|
||||
useEffect(() => {
|
||||
if (!isHello && userMessages.length > 0) {
|
||||
@ -243,43 +241,29 @@ export default function AskScreen() {
|
||||
}
|
||||
}, [isHello]);
|
||||
|
||||
// 组件卸载时清理动画
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// 停止所有可能正在运行的动画
|
||||
fadeAnim.stopAnimation();
|
||||
fadeAnimChat.stopAnimation();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
Keyboard.dismiss();
|
||||
if (!sessionId) {
|
||||
setIsHello(true);
|
||||
setUserMessages([])
|
||||
setUserMessages([]);
|
||||
}
|
||||
}, [sessionId, Keyboard])
|
||||
}, [sessionId])
|
||||
);
|
||||
|
||||
return (
|
||||
<GestureDetector gesture={gesture}>
|
||||
<View style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
|
||||
{/* 导航栏 */}
|
||||
<View style={[styles.navbar, { borderBottomWidth: isHello ? 0 : 1 }]}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => {
|
||||
try {
|
||||
if (TextInput.State?.currentlyFocusedInput) {
|
||||
const input = TextInput.State.currentlyFocusedInput();
|
||||
if (input) input.blur();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('失去焦点失败:', error);
|
||||
}
|
||||
Keyboard.dismiss();
|
||||
router.push('/memo-list');
|
||||
}}
|
||||
>
|
||||
<ReturnArrow />
|
||||
</TouchableOpacity>
|
||||
<ThemedText style={[styles.title, { opacity: isHello ? 0 : 1 }]} onPress={() => { router.push('/owner') }}>MemoWake</ThemedText>
|
||||
<View style={styles.placeholder} />
|
||||
</View>
|
||||
|
||||
<View style={[styles.contentContainer, { marginTop: isHello ? -24 : 0 }]}>
|
||||
<View style={styles.contentContainer}>
|
||||
{/* 欢迎页面 */}
|
||||
<Animated.View
|
||||
style={[
|
||||
@ -322,7 +306,7 @@ export default function AskScreen() {
|
||||
{/* 输入框区域 */}
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
keyboardVerticalOffset={0} >
|
||||
keyboardVerticalOffset={Platform.OS === "ios" ? 90 : 0}>
|
||||
<View style={styles.inputContainer} key={conversationId}>
|
||||
<SendMessage
|
||||
setIsHello={setIsHello}
|
||||
@ -331,7 +315,6 @@ export default function AskScreen() {
|
||||
setUserMessages={setUserMessages}
|
||||
selectedImages={selectedImages}
|
||||
setSelectedImages={setSelectedImages}
|
||||
isHello={isHello}
|
||||
/>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
@ -349,7 +332,7 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 8,
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'rgba(0,0,0,0.1)',
|
||||
@ -358,7 +341,11 @@ const styles = StyleSheet.create({
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 1,
|
||||
zIndex: 10
|
||||
},
|
||||
hiddenNavbar: {
|
||||
shadowOpacity: 0,
|
||||
elevation: 0,
|
||||
opacity: 0
|
||||
},
|
||||
backButton: {
|
||||
padding: 8,
|
||||
@ -1,5 +1,4 @@
|
||||
import DownSvg from '@/assets/icons/svg/down.svg';
|
||||
import PlaceSvg from '@/assets/icons/svg/place.svg';
|
||||
import ArrowSvg from '@/assets/icons/svg/arrow.svg';
|
||||
import ReturnArrowSvg from '@/assets/icons/svg/returnArrow.svg';
|
||||
import { CascaderItem } from '@/components/cascader';
|
||||
import ClassifyModal from '@/components/owner/classify';
|
||||
@ -15,7 +14,7 @@ import { GroupedData, RankingItem, TargetItem } from '@/types/user';
|
||||
import { useRouter } from "expo-router";
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Keyboard, LayoutChangeEvent, Platform, StyleSheet, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native';
|
||||
import { LayoutChangeEvent, Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
interface LocationData {
|
||||
id: number;
|
||||
@ -26,14 +25,6 @@ interface LocationData {
|
||||
export default function OwnerPage() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const router = useRouter();
|
||||
const HOT_CITIES = [
|
||||
['北京', '上海', '广州', '深圳'],
|
||||
['杭州', '成都', '乌鲁木齐', '武汉'],
|
||||
['西安', '重庆', '西宁', '哈尔滨'],
|
||||
['长沙', '南宁', '贵阳', '昆明']
|
||||
];
|
||||
// 位置搜索数据
|
||||
const [locationSearch, setLocationSearch] = useState('');
|
||||
// 位置弹窗
|
||||
const [locationModalVisible, setLocationModalVisible] = useState(false);
|
||||
// 分类弹窗
|
||||
@ -170,62 +161,36 @@ export default function OwnerPage() {
|
||||
};
|
||||
}, [fetchLocationData]);
|
||||
|
||||
useEffect(() => {
|
||||
// console.log(locationData);
|
||||
|
||||
}, [locationSearch])
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress={() => {
|
||||
Keyboard.dismiss();
|
||||
}}>
|
||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||
{/* 导航栏 */}
|
||||
<View
|
||||
style={styles.header}
|
||||
ref={podiumRef}
|
||||
onLayout={onPodiumLayout}
|
||||
>
|
||||
<TouchableOpacity onPress={() => { router.push('/owner') }} style={{ padding: 16 }}>
|
||||
<ReturnArrowSvg />
|
||||
</TouchableOpacity>
|
||||
<ThemedText style={styles.headerTitle} onPress={() => { setClassifyModalVisible(true) }}>
|
||||
{selectedClassify?.length > 0 ? selectedClassify[selectedClassify?.length - 1].name : "分类"}
|
||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||
{/* 导航栏 */}
|
||||
<View
|
||||
style={styles.header}
|
||||
ref={podiumRef}
|
||||
onLayout={onPodiumLayout}
|
||||
>
|
||||
<TouchableOpacity onPress={() => { router.push('/owner') }} style={{ padding: 16 }}>
|
||||
<ReturnArrowSvg />
|
||||
</TouchableOpacity>
|
||||
<ThemedText style={styles.headerTitle}>
|
||||
Top Memory Makers
|
||||
</ThemedText>
|
||||
<ThemedText className='opacity-0'>123</ThemedText>
|
||||
</View>
|
||||
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 16, marginHorizontal: 16 }}>
|
||||
<TouchableOpacity onPress={() => { setLocationModalVisible(true) }} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||
<ThemedText style={{ color: selectedLocation?.length > 0 ? '#FFB645' : '#4C320C' }}>
|
||||
{selectedLocation?.length > 0 ? selectedLocation[selectedLocation?.length - 1].name : "地区"}
|
||||
</ThemedText>
|
||||
<ThemedText className='opacity-0'>123</ThemedText>
|
||||
</View>
|
||||
<View style={{ display: 'flex', flexDirection: 'column', gap: 16, marginHorizontal: 16, paddingHorizontal: 32 }}>
|
||||
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: "space-between", gap: 16 }}>
|
||||
<TouchableOpacity onPress={() => { setLocationModalVisible(true) }} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||
<PlaceSvg />
|
||||
<ThemedText style={{ color: selectedLocation?.length > 0 ? '#FFB645' : '#4C320C' }}>
|
||||
{selectedLocation?.length > 0 ? selectedLocation[selectedLocation?.length - 1].name : "地区"}
|
||||
</ThemedText>
|
||||
<DownSvg />
|
||||
{/* {
|
||||
{
|
||||
selectedLocation?.length > 0
|
||||
?
|
||||
<ArrowSvg style={{ transform: [{ rotate: '90deg' }], width: 12, height: 12 }} />
|
||||
:
|
||||
<ReturnArrowSvg style={{ transform: [{ rotate: '270deg' }], width: 12, height: 12 }} />
|
||||
} */}
|
||||
|
||||
</TouchableOpacity>
|
||||
{/* <View style={styles.searchContainer}>
|
||||
<View style={styles.searchIcon}>
|
||||
<SearchSvg width={12} height={12} />
|
||||
</View>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
onChangeText={setLocationSearch}
|
||||
value={locationSearch}
|
||||
placeholder="输入城市名进行搜索"
|
||||
onEndEditing={(text) => {
|
||||
setLocationSearch(text.nativeEvent.text)
|
||||
}}
|
||||
/>
|
||||
</View> */}
|
||||
{/* <TouchableOpacity onPress={() => { setClassifyModalVisible(true) }} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||
}
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => { setClassifyModalVisible(true) }} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||
<ThemedText style={{ color: selectedClassify?.length > 0 ? '#FFB645' : '#4C320C' }}>
|
||||
{selectedClassify?.length > 0 ? selectedClassify[selectedClassify?.length - 1].name : "分类"}
|
||||
</ThemedText>
|
||||
@ -235,56 +200,30 @@ export default function OwnerPage() {
|
||||
:
|
||||
<ReturnArrowSvg style={{ transform: [{ rotate: '270deg' }], width: 12, height: 12 }} />
|
||||
}
|
||||
</TouchableOpacity> */}
|
||||
|
||||
</View>
|
||||
{/* 热门城市 */}
|
||||
{/* <View style={styles.hotCity}>
|
||||
<ThemedText size="base" color="textSecondary" style={{ marginBottom: 16 }}>热门城市</ThemedText>
|
||||
{HOT_CITIES.map((row, rowIndex) => (
|
||||
<View
|
||||
key={`row-${rowIndex}`}
|
||||
style={[styles.cityRow, rowIndex === HOT_CITIES.length - 1 && { marginBottom: 0 }]}
|
||||
>
|
||||
{row.map((city, cityIndex) => (
|
||||
<ThemedText
|
||||
key={`${city}-${cityIndex}`}
|
||||
style={styles.item}
|
||||
onPress={() => {
|
||||
setLocationSearch(city);
|
||||
}}
|
||||
>
|
||||
{city}
|
||||
</ThemedText>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
</View> */}
|
||||
</View>
|
||||
|
||||
{/* 颁奖台 */}
|
||||
<PodiumComponent data={ranking} />
|
||||
{/* 排名区域 */}
|
||||
<RankList data={ranking} />
|
||||
|
||||
{/* 地区选择弹窗 */}
|
||||
<LocationModal
|
||||
modalVisible={locationModalVisible}
|
||||
setModalVisible={setLocationModalVisible}
|
||||
podiumPosition={podiumPosition}
|
||||
handleChange={handleLocationChange}
|
||||
data={locationData}
|
||||
/>
|
||||
{/* 分类选择弹窗 */}
|
||||
<ClassifyModal
|
||||
data={classify}
|
||||
modalVisible={classifyModalVisible}
|
||||
setModalVisible={setClassifyModalVisible}
|
||||
podiumPosition={podiumPosition}
|
||||
handleChange={handleClassifyChange}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
{/* 颁奖台 */}
|
||||
<PodiumComponent data={ranking} />
|
||||
{/* 排名区域 */}
|
||||
<RankList data={ranking} />
|
||||
|
||||
{/* 地区选择弹窗 */}
|
||||
<LocationModal
|
||||
modalVisible={locationModalVisible}
|
||||
setModalVisible={setLocationModalVisible}
|
||||
podiumPosition={podiumPosition}
|
||||
handleChange={handleLocationChange}
|
||||
data={locationData}
|
||||
/>
|
||||
{/* 分类选择弹窗 */}
|
||||
<ClassifyModal
|
||||
data={classify}
|
||||
modalVisible={classifyModalVisible}
|
||||
setModalVisible={setClassifyModalVisible}
|
||||
podiumPosition={podiumPosition}
|
||||
handleChange={handleClassifyChange}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@ -304,46 +243,5 @@ const styles = StyleSheet.create({
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: '#4C320C',
|
||||
},
|
||||
searchContainer: {
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: '#D9D9D9',
|
||||
borderRadius: 24,
|
||||
paddingLeft: 40, // 给图标留出空间
|
||||
paddingVertical: 8,
|
||||
paddingRight: 16,
|
||||
fontSize: 14,
|
||||
lineHeight: 16,
|
||||
},
|
||||
searchIcon: {
|
||||
position: 'absolute',
|
||||
left: 20,
|
||||
top: '50%',
|
||||
zIndex: 10,
|
||||
transform: [{ translateY: -6 }]
|
||||
},
|
||||
item: {
|
||||
paddingVertical: 8,
|
||||
borderRadius: 12,
|
||||
fontSize: 14,
|
||||
backgroundColor: '#D9D9D9',
|
||||
width: '23%',
|
||||
textAlign: 'center',
|
||||
color: "#4C320C"
|
||||
},
|
||||
hotCity: {
|
||||
backgroundColor: "#FBFBFB",
|
||||
padding: 16,
|
||||
borderRadius: 24
|
||||
},
|
||||
cityRow: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 16
|
||||
}
|
||||
});
|
||||
39
app/(settings)/_layout.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import React from 'react';
|
||||
|
||||
export default function SettingsLayout() {
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Screen
|
||||
name="setting"
|
||||
options={{
|
||||
headerShown: false
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="privacy-policy"
|
||||
options={{
|
||||
headerShown: false
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="support"
|
||||
options={{
|
||||
headerShown: false
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="rights"
|
||||
options={{
|
||||
headerShown: false
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="user-message"
|
||||
options={{
|
||||
headerShown: false
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@ -37,10 +37,9 @@ export default function Rights() {
|
||||
requestProducts,
|
||||
ErrorCode
|
||||
} = useIAP();
|
||||
const { points, pro } = useLocalSearchParams<{
|
||||
const { pro } = useLocalSearchParams<{
|
||||
credit: string;
|
||||
points: string;
|
||||
pro: string
|
||||
pro: string;
|
||||
}>();
|
||||
// 用户勾选协议
|
||||
const [agree, setAgree] = useState<boolean>(false);
|
||||
@ -216,9 +215,9 @@ export default function Rights() {
|
||||
{/* 会员卡 */}
|
||||
<View style={styles.card}>
|
||||
{userType === 'normal' ? (
|
||||
<Image source={require('@/assets/images/png/owner/normal.png')} style={{ height: 150, objectFit: 'cover', width: '100%', borderRadius: 32 }} />
|
||||
<Image source={require('@/assets/images/png/owner/normal.png')} style={{ height: 150, objectFit: 'cover', width: '100%' }} />
|
||||
) : (
|
||||
<Image source={require('@/assets/images/png/owner/pro.png')} style={{ height: 150, objectFit: 'cover', width: '100%', borderRadius: 32 }} />
|
||||
<Image source={require('@/assets/images/png/owner/pro.png')} style={{ height: 150, objectFit: 'cover', width: '100%' }} />
|
||||
)}
|
||||
<View style={styles.cardContent}>
|
||||
<View style={styles.cardinfo}>
|
||||
@ -227,7 +226,7 @@ export default function Rights() {
|
||||
</ThemedText>
|
||||
<View style={styles.cardPoints}>
|
||||
<StarSvg />
|
||||
<ThemedText style={styles.cardPointsText}>{points}</ThemedText>
|
||||
<ThemedText style={styles.cardPointsText}>{pro}</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@ -267,6 +266,8 @@ export default function Rights() {
|
||||
padding: 16,
|
||||
paddingBottom: 32,
|
||||
backgroundColor: '#fff',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#eee',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
@ -302,8 +303,8 @@ export default function Rights() {
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<ThemedText style={{ color: '#fff', fontWeight: '700', fontSize: 18 }}>
|
||||
{t('rights.subscribe', { ns: 'personal' })} {payType?.split('_')[payType?.split('_')?.length - 1]}
|
||||
<ThemedText style={{ color: '#fff', fontWeight: '700', fontSize: 14 }}>
|
||||
{t('rights.subscribe', { ns: 'personal' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
|
||||
@ -364,8 +365,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
goPay: {
|
||||
backgroundColor: '#E2793F',
|
||||
borderRadius: 32,
|
||||
paddingVertical: 16,
|
||||
borderRadius: 24,
|
||||
paddingVertical: 10,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
@ -378,9 +379,9 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 16
|
||||
},
|
||||
switchButtonItem: {
|
||||
width: "47%",
|
||||
width: "48%",
|
||||
borderRadius: 24,
|
||||
paddingVertical: 8,
|
||||
paddingVertical: 6,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderWidth: 1
|
||||
@ -389,7 +390,7 @@ const styles = StyleSheet.create({
|
||||
marginHorizontal: 16,
|
||||
marginVertical: 16,
|
||||
padding: 16,
|
||||
borderRadius: 32,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#fff',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
@ -420,7 +421,7 @@ const styles = StyleSheet.create({
|
||||
marginHorizontal: 16,
|
||||
marginVertical: 16,
|
||||
backgroundColor: '#FFB645',
|
||||
borderRadius: 32,
|
||||
borderRadius: 12,
|
||||
},
|
||||
cardContent: {
|
||||
position: 'absolute',
|
||||
@ -439,7 +440,7 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '700',
|
||||
color: '#E2793F',
|
||||
backgroundColor: '#fff',
|
||||
paddingHorizontal: 16,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 20,
|
||||
textAlign: 'center',
|
||||
@ -449,12 +450,12 @@ const styles = StyleSheet.create({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8
|
||||
gap: 4
|
||||
},
|
||||
cardPointsText: {
|
||||
fontSize: 32,
|
||||
fontWeight: '700',
|
||||
color: '#4C320C',
|
||||
lineHeight: 36
|
||||
lineHeight: 32
|
||||
}
|
||||
});
|
||||
@ -11,7 +11,7 @@ import { checkNotificationPermission, getLocationPermission, getPermissions, req
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { useAuth } from '@/contexts/auth-context';
|
||||
import { fetchApi } from '@/lib/server-api-util';
|
||||
import { Address, User } from '@/types/user';
|
||||
import { Address, User, UserInfoDetails } from '@/types/user';
|
||||
import * as Location from 'expo-location';
|
||||
import { useFocusEffect, useRouter } from 'expo-router';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
@ -20,7 +20,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Linking, Platform, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
const Setting = () => {
|
||||
const Setting = (props: { userInfo: UserInfoDetails }) => {
|
||||
const [userInfo, setUserInfo] = useState<User | null>(null);
|
||||
|
||||
const getUserInfo = async () => {
|
||||
@ -210,7 +210,7 @@ const Setting = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, paddingTop: insets.top, paddingBottom: insets.bottom, backgroundColor: '#fff' }}>
|
||||
<View style={{ flex: 1, paddingTop: insets.top, marginBottom: insets.bottom }}>
|
||||
<Pressable
|
||||
style={styles.centeredView}
|
||||
>
|
||||
@ -221,7 +221,7 @@ const Setting = () => {
|
||||
<TouchableOpacity onPress={() => { router.push('/owner') }}>
|
||||
<ReturnArrowSvg />
|
||||
</TouchableOpacity>
|
||||
<ThemedText style={styles.modalTitle} >{t('generalSetting.allTitle', { ns: 'personal' })}</ThemedText>
|
||||
<Text style={styles.modalTitle}>{t('generalSetting.allTitle', { ns: 'personal' })}</Text>
|
||||
<Text style={{ opacity: 0 }}>×</Text>
|
||||
</View>
|
||||
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
|
||||
@ -269,11 +269,11 @@ const Setting = () => {
|
||||
</View> */}
|
||||
{/* 权限信息 */}
|
||||
<View style={{ marginTop: 16 }}>
|
||||
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }} type="sfPro">{t('permission.permissionManagement', { ns: 'personal' })}</ThemedText>
|
||||
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('permission.permissionManagement', { ns: 'personal' })}</ThemedText>
|
||||
<View style={styles.content}>
|
||||
{/* 相册权限 */}
|
||||
<View style={styles.item}>
|
||||
<ThemedText style={styles.itemText} type="sfPro">{t('permission.galleryAccess', { ns: 'personal' })}</ThemedText>
|
||||
<ThemedText style={styles.itemText}>{t('permission.galleryAccess', { ns: 'personal' })}</ThemedText>
|
||||
<CustomSwitch
|
||||
isEnabled={albumEnabled}
|
||||
toggleSwitch={toggleAlbum}
|
||||
@ -284,7 +284,7 @@ const Setting = () => {
|
||||
{/* 位置权限 */}
|
||||
<View style={styles.item}>
|
||||
<View>
|
||||
<ThemedText style={styles.itemText} type="sfPro">{t('permission.locationPermission', { ns: 'personal' })}</ThemedText>
|
||||
<ThemedText style={styles.itemText}>{t('permission.locationPermission', { ns: 'personal' })}</ThemedText>
|
||||
</View>
|
||||
<CustomSwitch
|
||||
isEnabled={locationEnabled}
|
||||
@ -294,7 +294,7 @@ const Setting = () => {
|
||||
<Divider />
|
||||
<View style={styles.item}>
|
||||
<View>
|
||||
<ThemedText style={styles.itemText} type="sfPro">{t('permission.pushNotification', { ns: 'personal' })}</ThemedText>
|
||||
<ThemedText style={styles.itemText}>{t('permission.pushNotification', { ns: 'personal' })}</ThemedText>
|
||||
</View>
|
||||
<CustomSwitch
|
||||
isEnabled={notificationsEnabled}
|
||||
@ -335,62 +335,62 @@ const Setting = () => {
|
||||
</View> */}
|
||||
{/* 协议 */}
|
||||
<View style={{ marginTop: 16 }}>
|
||||
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }} type="sfPro">{t('lcenses.title', { ns: 'personal' })}</ThemedText>
|
||||
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('lcenses.title', { ns: 'personal' })}</ThemedText>
|
||||
<View style={styles.content}>
|
||||
<TouchableOpacity style={styles.item} onPress={() => { setModalType('privacy'); setPrivacyModalVisible(true) }} >
|
||||
<ThemedText style={styles.itemText} type="sfPro">{t('lcenses.privacyPolicy', { ns: 'personal' })}</ThemedText>
|
||||
<ThemedText style={styles.itemText}>{t('lcenses.privacyPolicy', { ns: 'personal' })}</ThemedText>
|
||||
<RightArrowSvg />
|
||||
</TouchableOpacity>
|
||||
<Divider />
|
||||
<TouchableOpacity style={styles.item} onPress={() => { setModalType('terms'); setPrivacyModalVisible(true) }} >
|
||||
<ThemedText style={styles.itemText} type="sfPro">{t('lcenses.applyPermission', { ns: 'personal' })}</ThemedText>
|
||||
<ThemedText style={styles.itemText}>{t('lcenses.applyPermission', { ns: 'personal' })}</ThemedText>
|
||||
<RightArrowSvg />
|
||||
</TouchableOpacity>
|
||||
<Divider />
|
||||
<TouchableOpacity style={styles.item} onPress={() => { setModalType('user'); setPrivacyModalVisible(true) }} >
|
||||
<ThemedText style={styles.itemText} type="sfPro">{t('lcenses.userAgreement', { ns: 'personal' })}</ThemedText>
|
||||
<ThemedText style={styles.itemText}>{t('lcenses.userAgreement', { ns: 'personal' })}</ThemedText>
|
||||
<RightArrowSvg />
|
||||
</TouchableOpacity>
|
||||
<Divider />
|
||||
<TouchableOpacity style={styles.item} onPress={() => { setModalType('ai'); setPrivacyModalVisible(true) }} >
|
||||
<ThemedText style={styles.itemText} type="sfPro">{t('lcenses.aiPolicy', { ns: 'personal' })}</ThemedText>
|
||||
<ThemedText style={styles.itemText}>{t('lcenses.aiPolicy', { ns: 'personal' })}</ThemedText>
|
||||
<RightArrowSvg />
|
||||
</TouchableOpacity>
|
||||
<Divider />
|
||||
<TouchableOpacity style={styles.item} onPress={() => { setLcensesModalVisible(true) }} >
|
||||
<ThemedText style={styles.itemText} type="sfPro">{t('lcenses.qualification', { ns: 'personal' })}</ThemedText>
|
||||
<ThemedText style={styles.itemText}>{t('lcenses.qualification', { ns: 'personal' })}</ThemedText>
|
||||
<RightArrowSvg />
|
||||
</TouchableOpacity>
|
||||
<Divider />
|
||||
<TouchableOpacity style={[styles.item, { display: language == "en" ? 'none' : 'flex' }]} onPress={() => Linking.openURL("https://beian.miit.gov.cn/")} >
|
||||
<ThemedText style={styles.itemText} type="sfPro">{t('lcenses.ICP', { ns: 'personal' })}沪ICP备2025133004号-2A</ThemedText>
|
||||
<ThemedText style={styles.itemText}>{t('lcenses.ICP', { ns: 'personal' })}沪ICP备2025133004号-2A</ThemedText>
|
||||
<RightArrowSvg />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
{/* 其他信息 */}
|
||||
<View style={{ marginTop: 16 }}>
|
||||
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }} type="sfPro">{t('generalSetting.otherInformation', { ns: 'personal' })}</ThemedText>
|
||||
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('generalSetting.otherInformation', { ns: 'personal' })}</ThemedText>
|
||||
<View style={styles.content}>
|
||||
<TouchableOpacity style={styles.item} onPress={() => Linking.openURL("https://work.weixin.qq.com/kfid/kfca0ac87f4e05e8bfd")} >
|
||||
<ThemedText style={styles.itemText} type="sfPro">{t('generalSetting.contactUs', { ns: 'personal' })}</ThemedText>
|
||||
<ThemedText style={styles.itemText}>{t('generalSetting.contactUs', { ns: 'personal' })}</ThemedText>
|
||||
{/* <RightArrowSvg /> */}
|
||||
</TouchableOpacity>
|
||||
<Divider />
|
||||
<View style={styles.item}>
|
||||
<ThemedText style={styles.itemText} type="sfPro">{t('generalSetting.version', { ns: 'personal' })}</ThemedText>
|
||||
<ThemedText style={styles.itemText}>{t('generalSetting.version', { ns: 'personal' })}</ThemedText>
|
||||
<ThemedText style={styles.itemText}>{"0.5.0"}</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
{/* 退出 */}
|
||||
<TouchableOpacity style={[styles.premium, { marginVertical: 8 }]} onPress={handleLogout}>
|
||||
<ThemedText style={{ color: '#E2793F', fontSize: 14, fontWeight: '600' }} type="sfPro">{t('generalSetting.logout', { ns: 'personal' })}</ThemedText>
|
||||
<ThemedText style={{ color: '#E2793F', fontSize: 14, fontWeight: '600' }}>{t('generalSetting.logout', { ns: 'personal' })}</ThemedText>
|
||||
<LogoutSvg />
|
||||
</TouchableOpacity>
|
||||
{/* 注销账号 */}
|
||||
<TouchableOpacity style={[styles.premium, { marginVertical: 8 }]} onPress={() => setDeleteModalVisible(true)}>
|
||||
<ThemedText style={{ color: '#E2793F', fontSize: 14, fontWeight: '600' }} type="sfPro">{t('generalSetting.deleteAccount', { ns: 'personal' })}</ThemedText>
|
||||
<ThemedText style={{ color: '#E2793F', fontSize: 14, fontWeight: '600' }}>{t('generalSetting.deleteAccount', { ns: 'personal' })}</ThemedText>
|
||||
<DeleteSvg />
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
@ -405,11 +405,14 @@ const Setting = () => {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
centeredView: {
|
||||
flex: 1
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
modalView: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'white',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
modalHeader: {
|
||||
@ -437,8 +440,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
premium: {
|
||||
backgroundColor: "#FAF9F6",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 20,
|
||||
padding: 16,
|
||||
borderRadius: 24,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
@ -450,11 +452,11 @@ const styles = StyleSheet.create({
|
||||
gap: 4,
|
||||
backgroundColor: '#FAF9F6',
|
||||
borderRadius: 24,
|
||||
padding: 8
|
||||
paddingVertical: 8
|
||||
},
|
||||
item: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
paddingVertical: 8,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
@ -1,399 +0,0 @@
|
||||
import { HapticTab } from '@/components/HapticTab';
|
||||
import AskNavbar from '@/components/layout/ask';
|
||||
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
|
||||
import { requestNotificationPermission } from '@/components/owner/utils';
|
||||
import TabBarBackground from '@/components/ui/TabBarBackground';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { prefetchChats } from '@/lib/prefetch';
|
||||
import { fetchApi } from '@/lib/server-api-util';
|
||||
import { webSocketManager, WebSocketStatus } from '@/lib/websocket-util';
|
||||
import { TransitionPresets } from '@react-navigation/bottom-tabs';
|
||||
import { useFonts } from 'expo-font';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import { Tabs } from 'expo-router';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
interface PollingData {
|
||||
title: string;
|
||||
id: string;
|
||||
content: string;
|
||||
extra: any;
|
||||
}
|
||||
|
||||
export default function TabLayout() {
|
||||
const { t } = useTranslation();
|
||||
const colorScheme = useColorScheme();
|
||||
const [pollingData, setPollingData] = useState<PollingData[]>([]);
|
||||
const pollingInterval = useRef<NodeJS.Timeout | number>(null);
|
||||
const tokenInterval = useRef<NodeJS.Timeout | number>(null);
|
||||
const isMounted = useRef(true);
|
||||
const [token, setToken] = useState('');
|
||||
const [wsStatus, setWsStatus] = useState<WebSocketStatus>('disconnected');
|
||||
const sendNotification = async (item: PollingData) => {
|
||||
// 请求通知权限
|
||||
const granted = await requestNotificationPermission();
|
||||
if (!granted) {
|
||||
console.log('用户拒绝了通知权限');
|
||||
return;
|
||||
}
|
||||
|
||||
// 调度本地通知
|
||||
await Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: item.title,
|
||||
body: item.content,
|
||||
data: { screen: 'ask', extra: item.extra, id: item.id },
|
||||
priority: 'high', // 关键:设置 high 或 max
|
||||
},
|
||||
trigger: {
|
||||
seconds: 2, // 延迟2秒显示
|
||||
type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL // 添加 type 字段
|
||||
}, // 延迟2秒显示
|
||||
});
|
||||
};
|
||||
|
||||
// 加载字体
|
||||
const [loaded, error] = useFonts({
|
||||
quicksand: require('@/assets/fonts/Quicksand.otf'),
|
||||
sfPro: require('@/assets/fonts/SF-Pro.otf'),
|
||||
inter: require('@/assets/fonts/Inter-Regular.otf'),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded || error) {
|
||||
SplashScreen.hideAsync();
|
||||
}
|
||||
}, [loaded, error]);
|
||||
|
||||
// 监听通知点击事件
|
||||
useEffect(() => {
|
||||
const notificationListener = Notifications.addNotificationResponseReceivedListener(response => {
|
||||
const data = response.notification.request.content.data;
|
||||
console.log('通知被点击,数据:', data);
|
||||
pollingData?.filter((item) => item.id !== data.id);
|
||||
});
|
||||
|
||||
// 清理监听器
|
||||
return () => {
|
||||
Notifications.removeNotificationSubscription(notificationListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleStatusChange = (status: WebSocketStatus) => {
|
||||
setWsStatus(status);
|
||||
};
|
||||
webSocketManager.subscribeStatus(handleStatusChange);
|
||||
return () => {
|
||||
webSocketManager.unsubscribeStatus(handleStatusChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 轮询获取推送消息
|
||||
const startPolling = useCallback(async (interval: number = 5000) => {
|
||||
|
||||
// 设置轮询
|
||||
pollingInterval.current = setInterval(async () => {
|
||||
if (isMounted.current) {
|
||||
await getMessageData();
|
||||
}
|
||||
}, interval);
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
if (pollingInterval.current) {
|
||||
clearInterval(pollingInterval.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 获取推送消息
|
||||
const getMessageData = async () => {
|
||||
try {
|
||||
const response = await fetchApi<PollingData[]>("/notice/push/message", {
|
||||
method: "POST"
|
||||
});
|
||||
setPollingData((prev) => ([...prev, ...response]));
|
||||
} catch (error) {
|
||||
console.error('获取轮询数据时出错:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取认证token
|
||||
const getAuthToken = async (): Promise<string> => {
|
||||
let tokenValue = '';
|
||||
if (Platform.OS === 'web') {
|
||||
tokenValue = localStorage.getItem('token') || '';
|
||||
} else {
|
||||
tokenValue = (await SecureStore.getItemAsync('token')) || '';
|
||||
}
|
||||
setToken(tokenValue); // 只在获取到新token时更新状态
|
||||
return tokenValue;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuthStatus = async () => {
|
||||
try {
|
||||
if (token) {
|
||||
// 启动轮询
|
||||
startPolling(5000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取推送消息出错:', error);
|
||||
}
|
||||
};
|
||||
checkAuthStatus();
|
||||
|
||||
return () => {
|
||||
// 清理函数
|
||||
if (pollingInterval.current) {
|
||||
clearInterval(pollingInterval.current);
|
||||
}
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, [token]);
|
||||
|
||||
// 本地推送
|
||||
useEffect(() => {
|
||||
pollingData?.map((item) => {
|
||||
sendNotification(item)
|
||||
})
|
||||
}, [pollingData])
|
||||
|
||||
// 轮询获取token
|
||||
useEffect(() => {
|
||||
// 如果已经有token,直接返回
|
||||
if (token) {
|
||||
if (tokenInterval.current) {
|
||||
clearInterval(tokenInterval.current);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!tokenInterval.current) return;
|
||||
// 设置轮询
|
||||
tokenInterval.current = setInterval(async () => {
|
||||
if (isMounted.current) {
|
||||
const currentToken = await getAuthToken();
|
||||
// 如果获取到token,清除定时器
|
||||
if (currentToken && tokenInterval.current) {
|
||||
clearInterval(tokenInterval.current);
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
if (tokenInterval.current) {
|
||||
clearInterval(tokenInterval.current);
|
||||
}
|
||||
};
|
||||
}, [token]); // 添加token作为依赖
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
prefetchChats().catch(console.error);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
|
||||
headerShown: false,
|
||||
tabBarButton: HapticTab,
|
||||
tabBarBackground: TabBarBackground,
|
||||
tabBarStyle: Platform.select({
|
||||
ios: {
|
||||
// Use a transparent background on iOS to show the blur effect
|
||||
position: 'absolute',
|
||||
},
|
||||
default: {},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{/* 落地页 */}
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Memo',
|
||||
tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
{/* 登录 */}
|
||||
<Tabs.Screen
|
||||
name="login"
|
||||
options={{
|
||||
title: 'Login',
|
||||
href: '/login',
|
||||
// tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
{/* 重置密码 */}
|
||||
<Tabs.Screen
|
||||
name="reset-password"
|
||||
options={{
|
||||
title: 'reset-password',
|
||||
href: '/reset-password',
|
||||
// tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
{/* loading页面 */}
|
||||
<Tabs.Screen
|
||||
name="loading"
|
||||
options={{
|
||||
title: 'loading',
|
||||
href: '/loading',
|
||||
// tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
{/* 用户信息收集 */}
|
||||
<Tabs.Screen
|
||||
name="user-message"
|
||||
options={{
|
||||
title: 'user-message',
|
||||
href: '/user-message',
|
||||
// tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
{/* ask页面 */}
|
||||
<Tabs.Screen
|
||||
name="ask"
|
||||
options={{
|
||||
title: 'ask',
|
||||
tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' }, // 确保在标签栏中不显示
|
||||
...TransitionPresets.ShiftTransition,
|
||||
}}
|
||||
/>
|
||||
{/* memo list */}
|
||||
<Tabs.Screen
|
||||
name="memo-list"
|
||||
options={{
|
||||
title: 'memo-list',
|
||||
tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' }, // 确保在标签栏中不显示
|
||||
...TransitionPresets.ShiftTransition,
|
||||
}}
|
||||
/>
|
||||
{/* owner */}
|
||||
<Tabs.Screen
|
||||
name="owner"
|
||||
options={{
|
||||
title: 'owner',
|
||||
tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' }, // 确保在标签栏中不显示
|
||||
...TransitionPresets.ShiftTransition,
|
||||
}}
|
||||
/>
|
||||
{/* 排行榜 */}
|
||||
<Tabs.Screen
|
||||
name="top"
|
||||
options={{
|
||||
title: 'top',
|
||||
tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
{/* 对话详情页 */}
|
||||
<Tabs.Screen
|
||||
name="chat-details"
|
||||
options={{
|
||||
title: 'chat-details',
|
||||
tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
{/* 隐私协议 */}
|
||||
<Tabs.Screen
|
||||
name="privacy-policy"
|
||||
options={{
|
||||
title: 'privacy-policy',
|
||||
tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Support Screen */}
|
||||
<Tabs.Screen
|
||||
name="support"
|
||||
options={{
|
||||
title: t('tabTitle', { ns: 'support' }),
|
||||
tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Debug Screen - only in development */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<Tabs.Screen
|
||||
name="debug"
|
||||
options={{
|
||||
title: 'Debug',
|
||||
tabBarIcon: ({ color, focused }) => (
|
||||
<TabBarIcon name={focused ? 'bug' : 'bug-outline'} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 下载页面 */}
|
||||
<Tabs.Screen
|
||||
name="download"
|
||||
options={{
|
||||
title: 'download',
|
||||
tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 购买权益页面 */}
|
||||
<Tabs.Screen
|
||||
name="rights"
|
||||
options={{
|
||||
title: 'rights',
|
||||
tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 设置页面 */}
|
||||
<Tabs.Screen
|
||||
name="setting"
|
||||
options={{
|
||||
title: 'setting',
|
||||
tabBarButton: () => null, // 隐藏底部标签栏
|
||||
headerShown: false, // 隐藏导航栏
|
||||
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
|
||||
}}
|
||||
/>
|
||||
</Tabs >
|
||||
<AskNavbar wsStatus={wsStatus} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,336 +0,0 @@
|
||||
import { Fonts } from '@/constants/Fonts';
|
||||
import { checkAuthStatus } from '@/lib/auth';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Dimensions, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import Animated, {
|
||||
interpolate,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withDelay,
|
||||
withRepeat,
|
||||
withSequence,
|
||||
withTiming
|
||||
} from 'react-native-reanimated';
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
// Worklet function for animations
|
||||
const runShakeAnimation = (value: Animated.SharedValue<number>) => {
|
||||
'worklet';
|
||||
return withRepeat(
|
||||
withSequence(
|
||||
withTiming(1, { duration: 300 }),
|
||||
withTiming(-1, { duration: 300 }),
|
||||
withTiming(1, { duration: 300 }),
|
||||
withTiming(-1, { duration: 300 }),
|
||||
withTiming(0, { duration: 200 }),
|
||||
withDelay(1000, withTiming(0, { duration: 0 }))
|
||||
),
|
||||
-1
|
||||
);
|
||||
};
|
||||
|
||||
const runWaveAnimation = (value: Animated.SharedValue<number>) => {
|
||||
'worklet';
|
||||
return withRepeat(
|
||||
withSequence(
|
||||
withTiming(1, { duration: 500 }),
|
||||
withTiming(-1, { duration: 500 }),
|
||||
withTiming(0, { duration: 500 }),
|
||||
withDelay(1000, withTiming(0, { duration: 0 }))
|
||||
),
|
||||
-1
|
||||
);
|
||||
};
|
||||
|
||||
export default function HomeScreen() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const screenWidth = Dimensions.get('window').width;
|
||||
|
||||
// Animation values
|
||||
const fadeAnim = useSharedValue(0);
|
||||
const shakeAnim = useSharedValue(0);
|
||||
const waveAnim = useSharedValue(0);
|
||||
const buttonShakeAnim = useSharedValue(0);
|
||||
const fadeInAnim = useSharedValue(0);
|
||||
const descriptionAnim = useSharedValue(0);
|
||||
const textAnimations = {
|
||||
line1: useSharedValue(0),
|
||||
line2: useSharedValue(0),
|
||||
line3: useSharedValue(0),
|
||||
subtitle: useSharedValue(0),
|
||||
};
|
||||
|
||||
// Animation styles
|
||||
const ipAnimatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: fadeAnim.value,
|
||||
transform: [
|
||||
{ translateX: interpolate(shakeAnim.value, [-1, 1], [-2, 2]) },
|
||||
{ rotate: `${interpolate(shakeAnim.value, [-1, 1], [-2, 2])}deg` },
|
||||
],
|
||||
}));
|
||||
|
||||
const waveAnimatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{ rotate: `${interpolate(waveAnim.value, [-1, 0, 1], [-15, 0, 15])}deg` },
|
||||
],
|
||||
}));
|
||||
|
||||
const buttonStyle = useAnimatedStyle(() => ({
|
||||
opacity: fadeInAnim.value,
|
||||
transform: [
|
||||
{ translateY: interpolate(fadeInAnim.value, [0, 1], [20, 0]) },
|
||||
{ translateX: interpolate(buttonShakeAnim.value, [-1, 0, 1], [-5, 0, 5]) }
|
||||
]
|
||||
}));
|
||||
|
||||
const welcomeStyle = useAnimatedStyle(() => ({
|
||||
opacity: fadeInAnim.value,
|
||||
transform: [{ translateY: interpolate(fadeInAnim.value, [0, 1], [20, 0]) }]
|
||||
}));
|
||||
|
||||
const descriptionStyle = useAnimatedStyle(() => ({
|
||||
opacity: descriptionAnim.value,
|
||||
transform: [{ translateY: interpolate(descriptionAnim.value, [0, 1], [20, 0]) }]
|
||||
}));
|
||||
|
||||
const textLine1Style = useAnimatedStyle(() => ({
|
||||
opacity: textAnimations.line1.value,
|
||||
transform: [{ translateY: interpolate(textAnimations.line1.value, [0, 1], [10, 0]) }]
|
||||
}));
|
||||
|
||||
const textLine2Style = useAnimatedStyle(() => ({
|
||||
opacity: textAnimations.line2.value,
|
||||
transform: [{ translateY: interpolate(textAnimations.line2.value, [0, 1], [10, 0]) }]
|
||||
}));
|
||||
|
||||
const textLine3Style = useAnimatedStyle(() => ({
|
||||
opacity: textAnimations.line3.value,
|
||||
transform: [{ translateY: interpolate(textAnimations.line3.value, [0, 1], [10, 0]) }]
|
||||
}));
|
||||
|
||||
const subtitleStyle = useAnimatedStyle(() => ({
|
||||
opacity: textAnimations.subtitle.value,
|
||||
transform: [{ translateY: interpolate(textAnimations.subtitle.value, [0, 1], [10, 0]) }]
|
||||
}));
|
||||
|
||||
// Start animations
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
checkAuthStatus(router, () => {
|
||||
router.replace('/ask');
|
||||
}, false).then(() => {
|
||||
setIsLoading(false);
|
||||
}).catch(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
// Start fade in animation
|
||||
fadeAnim.value = withTiming(1, { duration: 1000 }, () => {
|
||||
// Start shake animation
|
||||
shakeAnim.value = runShakeAnimation(shakeAnim);
|
||||
|
||||
// Start text animations with delays
|
||||
textAnimations.line1.value = withDelay(0, withTiming(1, { duration: 500 }));
|
||||
textAnimations.line2.value = withDelay(300, withTiming(1, { duration: 500 }));
|
||||
textAnimations.line3.value = withDelay(600, withTiming(1, { duration: 500 }));
|
||||
textAnimations.subtitle.value = withDelay(900, withTiming(1, { duration: 500 }));
|
||||
|
||||
// Start welcome animation
|
||||
fadeInAnim.value = withDelay(200, withTiming(1, { duration: 800 }));
|
||||
|
||||
// Start description animation
|
||||
descriptionAnim.value = withDelay(200, withTiming(1, { duration: 800 }));
|
||||
|
||||
// Start button animation
|
||||
fadeInAnim.value = withDelay(200, withTiming(1, { duration: 800 }, () => {
|
||||
// Start button shake animation
|
||||
buttonShakeAnim.value = withRepeat(
|
||||
withSequence(
|
||||
withTiming(1, { duration: 100 }),
|
||||
withTiming(-1, { duration: 100 }),
|
||||
withTiming(1, { duration: 100 }),
|
||||
withTiming(0, { duration: 100 }),
|
||||
withDelay(3000, withTiming(0, { duration: 0 }))
|
||||
),
|
||||
-1
|
||||
);
|
||||
}));
|
||||
|
||||
// Start wave animation
|
||||
waveAnim.value = runWaveAnimation(waveAnim);
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
fadeAnim.value = 0;
|
||||
shakeAnim.value = 0;
|
||||
waveAnim.value = 0;
|
||||
buttonShakeAnim.value = 0;
|
||||
fadeInAnim.value = 0;
|
||||
descriptionAnim.value = 0;
|
||||
Object.values(textAnimations).forEach(anim => anim.value = 0);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={styles.loadingText}>{t('common.loading')}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.contentContainer, { paddingTop: insets.top + 16 }]}>
|
||||
<View style={styles.headerContainer}>
|
||||
<Animated.Text style={[styles.titleText, textLine1Style]}>
|
||||
{t('auth.welcomeAwaken.awaken', { ns: 'login' })}
|
||||
</Animated.Text>
|
||||
<Animated.Text style={[styles.titleText, textLine2Style]}>
|
||||
{t('auth.welcomeAwaken.your', { ns: 'login' })}
|
||||
</Animated.Text>
|
||||
<Animated.Text style={[styles.titleText, textLine3Style]}>
|
||||
{t('auth.welcomeAwaken.pm', { ns: 'login' })}
|
||||
</Animated.Text>
|
||||
<Animated.Text style={[styles.subtitleText, subtitleStyle]}>
|
||||
{t('auth.welcomeAwaken.slogan', { ns: 'login' })}
|
||||
</Animated.Text>
|
||||
</View>
|
||||
|
||||
<View style={{ alignItems: 'flex-end' }}>
|
||||
<Animated.View style={[{
|
||||
height: screenWidth * 0.3,
|
||||
width: screenWidth * 0.3,
|
||||
marginTop: -screenWidth * 0.08,
|
||||
}, welcomeStyle]}>
|
||||
<Image
|
||||
source={require('@/assets/images/png/icon/think.png')}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
resizeMode: 'contain'
|
||||
}}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
<View style={styles.ipContainer}>
|
||||
<Animated.View style={[styles.ipWrapper, waveAnimatedStyle]}>
|
||||
<Image
|
||||
source={require('@/assets/images/png/icon/ip.png')}
|
||||
style={{ width: screenWidth * 0.9, marginBottom: -screenWidth * 0.18, marginTop: -screenWidth * 0.22 }}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
<Animated.Text style={[styles.descriptionText, descriptionStyle]}>
|
||||
{t('auth.welcomeAwaken.gallery', { ns: 'login' })}
|
||||
{"\n"}
|
||||
{t('auth.welcomeAwaken.back', { ns: 'login' })}
|
||||
</Animated.Text>
|
||||
|
||||
<Animated.View style={[{ alignItems: "center" }, buttonStyle]}>
|
||||
<TouchableOpacity
|
||||
style={styles.awakenButton}
|
||||
onPress={() => router.push('/login')}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={styles.buttonText}>
|
||||
{t('auth.welcomeAwaken.awake', { ns: 'login' })}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFB645',
|
||||
fontFamily: 'english'
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFB645',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFB645',
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
headerContainer: {
|
||||
marginBottom: 40,
|
||||
width: '100%',
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
titleText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 12,
|
||||
textAlign: 'left',
|
||||
lineHeight: 36,
|
||||
fontFamily: Fonts['quicksand']
|
||||
},
|
||||
subtitleText: {
|
||||
color: 'rgba(255, 255, 255, 0.85)',
|
||||
fontSize: 16,
|
||||
textAlign: 'left',
|
||||
lineHeight: 24,
|
||||
fontFamily: Fonts['inter']
|
||||
},
|
||||
ipContainer: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
minHeight: 200,
|
||||
},
|
||||
ipWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
descriptionText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
lineHeight: 24,
|
||||
opacity: 0.9,
|
||||
paddingHorizontal: 40,
|
||||
marginTop: -16,
|
||||
fontFamily: Fonts['inter']
|
||||
},
|
||||
awakenButton: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 28,
|
||||
paddingVertical: 20,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
width: '86%',
|
||||
alignItems: 'center',
|
||||
marginTop: 24,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#4C320C',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 18,
|
||||
fontFamily: Fonts['quicksand']
|
||||
},
|
||||
});
|
||||
@ -1,11 +1,13 @@
|
||||
import { PermissionProvider } from '@/context/PermissionContext';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { registerBackgroundUploadTask } from '@/lib/background-uploader/automatic';
|
||||
import { webSocketManager } from '@/lib/websocket-util';
|
||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||
import * as MediaLibrary from 'expo-media-library';
|
||||
import { Stack } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { useEffect } from 'react';
|
||||
import { AppState } from 'react-native';
|
||||
import 'react-native-reanimated';
|
||||
import '../global.css';
|
||||
import { Provider } from "../provider";
|
||||
@ -29,19 +31,32 @@ export default function RootLayout() {
|
||||
setupBackgroundUpload();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleAppStateChange = (nextAppState: string) => {
|
||||
if (nextAppState === 'background') {
|
||||
// 应用进入后台时断开WebSocket连接
|
||||
webSocketManager.disconnect();
|
||||
} else if (nextAppState === 'active') {
|
||||
// 应用回到前台时重新连接WebSocket
|
||||
webSocketManager.connect();
|
||||
}
|
||||
};
|
||||
|
||||
const subscription = AppState.addEventListener('change', handleAppStateChange);
|
||||
|
||||
return () => {
|
||||
subscription?.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||
<PermissionProvider>
|
||||
<Provider>
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen
|
||||
name="login"
|
||||
options={{
|
||||
headerShown: false,
|
||||
animation: 'fade'
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(main)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(settings)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
</Provider>
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
<svg width="376" height="124" viewBox="0 44.9 376 124.1" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- 原始所有 path 和 defs 保持不变 -->
|
||||
<path d="M286.349 53.467L292.529 44.9033H320.021L311.337 51.2785L284.133 56.0518L286.349 53.467Z" fill="#E0A241" />
|
||||
<path d="M367.546 122.188L363.914 116.368C363.798 116.183 363.714 115.98 363.664 115.768L362.749 111.887C362.722 111.77 362.684 111.657 362.636 111.547L362.079 110.266C361.211 108.273 363.698 106.522 365.282 108.01L375.138 117.274L370.767 122.424C369.891 123.457 368.263 123.338 367.546 122.188Z" fill="#E0A241" />
|
||||
<rect x="1.17188" y="48.8311" width="370" height="120" rx="34" fill="#4C320C" />
|
||||
<path opacity="0.24" d="M1 118.031C1.66058 99.3464 16.3302 58.9993 68.3782 57.5584C116.841 56.2168 170.195 82.5409 173.692 84.0733C186.432 89.6553 254.376 122.217 310.713 107.564C355.783 95.8421 366.107 67.6372 365.635 55M1 128.238C31.3602 124.142 111.645 120.135 189.904 136.872C268.164 153.61 343.243 153.145 371 150.821" stroke="white" stroke-width="1.5" />
|
||||
<path d="M175.398 56.0518C175.398 56.0518 175.916 62.7711 177.936 64.7908C179.956 66.8105 184.795 67.329 184.795 67.329C184.795 67.329 179.956 67.8475 177.936 69.8672C175.916 71.8869 175.398 74.8472 175.398 74.8472C175.398 74.8472 174.879 71.8869 172.859 69.8672C170.84 67.8475 166 67.329 166 67.329C166 67.329 170.84 66.8105 172.859 64.7908C174.879 62.7711 175.398 56.0518 175.398 56.0518Z" fill="#FFB645" />
|
||||
<path d="M149.929 54.0527C149.929 54.0527 150.816 65.5433 154.27 68.9971C157.724 72.451 166 73.3377 166 73.3377C166 73.3377 157.724 74.2244 154.27 77.6783C150.816 81.1321 149.929 86.1943 149.929 86.1943C149.929 86.1943 149.042 81.1321 145.589 77.6783C142.135 74.2244 133.858 73.3377 133.858 73.3377C133.858 73.3377 142.135 72.451 145.589 68.9971C149.042 65.5433 149.929 54.0527 149.929 54.0527Z" fill="#FFB645" />
|
||||
<path d="M181.214 75.7812C181.214 75.7812 181.92 84.9229 184.668 87.6707C187.415 90.4186 194 91.124 194 91.124C194 91.124 187.415 91.8295 184.668 94.5773C181.92 97.3251 181.214 101.353 181.214 101.353C181.214 101.353 180.509 97.3251 177.761 94.5773C175.013 91.8295 168.429 91.124 168.429 91.124C168.429 91.124 175.013 90.4186 177.761 87.6707C180.509 84.9229 181.214 75.7812 181.214 75.7812Z" fill="#FFB645" />
|
||||
<path d="M292.495 44.8398H319.859L375.141 93.3921V117.295L292.495 44.8398Z" fill="#FFC56A" />
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_0_1" x1="26" y1="72.5" x2="106.717" y2="129.81" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" />
|
||||
<stop offset="1" stop-color="#FFE064" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_0_1" x1="30.4844" y1="134.396" x2="141.612" y2="134.396" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#D0BFB0" />
|
||||
<stop offset="0.405582" stop-color="#FFE57D" />
|
||||
<stop offset="1" stop-color="white" />
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_0_1" x1="100.511" y1="78.9371" x2="164.522" y2="104.117" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFF3E8" />
|
||||
<stop offset="1" stop-color="#FFFAB9" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
@ -1,26 +1,26 @@
|
||||
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="30" cy="30" r="30" fill="white"/>
|
||||
<mask id="mask0_106_1225" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="60" height="60">
|
||||
<mask id="chat_mask0_555_1115" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="60" height="60">
|
||||
<circle cx="30" cy="30" r="30" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_106_1225)">
|
||||
<g mask="url(#chat_mask0_555_1115)">
|
||||
<path d="M-12.302 16.2692C6.49892 -16.2949 53.5011 -16.2949 72.302 16.2692L85.1259 38.4808C103.927 71.0449 80.4256 111.75 42.8238 111.75H17.1762C-20.4256 111.75 -43.9267 71.0449 -25.1258 38.4808L-12.302 16.2692Z" fill="#FFD18A"/>
|
||||
<rect x="27.1162" y="14.1539" width="1.92308" height="2.69231" rx="0.961539" transform="rotate(-180 27.1162 14.1539)" fill="#4C320C"/>
|
||||
<rect x="34.0381" y="14.1539" width="1.92308" height="2.69231" rx="0.961539" transform="rotate(-180 34.0381 14.1539)" fill="#4C320C"/>
|
||||
<path d="M2.43174 33.8474C15.7285 14.5173 44.2728 14.5173 57.5695 33.8474L72.7134 55.8629C87.985 78.064 72.0909 108.288 45.1445 108.288H14.8567C-12.0897 108.288 -27.9838 78.064 -12.7122 55.8629L2.43174 33.8474Z" fill="#FFF8DE"/>
|
||||
<g filter="url(#filter0_i_106_1225)">
|
||||
<ellipse cx="56.7318" cy="30.5001" rx="31.5385" ry="22.5" fill="#FFF8DE"/>
|
||||
<rect x="27.116" y="14.1538" width="1.92308" height="2.69231" rx="0.961539" transform="rotate(-180 27.116 14.1538)" fill="#4C320C"/>
|
||||
<rect x="34.0383" y="14.1538" width="1.92308" height="2.69231" rx="0.961539" transform="rotate(-180 34.0383 14.1538)" fill="#4C320C"/>
|
||||
<path d="M2.43174 33.8473C15.7285 14.5172 44.2728 14.5172 57.5695 33.8473L72.7134 55.8628C87.985 78.0639 72.0909 108.288 45.1445 108.288H14.8567C-12.0897 108.288 -27.9838 78.0639 -12.7122 55.8628L2.43174 33.8473Z" fill="#FFF8DE"/>
|
||||
<g filter="url(#filter0_i_555_1115)">
|
||||
<ellipse cx="56.7313" cy="30.5" rx="31.5385" ry="22.5" fill="#FFF8DE"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_i_106_1225)">
|
||||
<ellipse cx="3.07662" cy="30.5001" rx="31.3462" ry="22.5" fill="#FFF8DE"/>
|
||||
<g filter="url(#filter1_i_555_1115)">
|
||||
<ellipse cx="3.07711" cy="30.5" rx="31.3462" ry="22.5" fill="#FFF8DE"/>
|
||||
</g>
|
||||
<ellipse cx="29.8075" cy="19.3463" rx="2.30769" ry="1.73077" transform="rotate(180 29.8075 19.3463)" fill="#FFB8B9"/>
|
||||
<ellipse cx="5.49476" cy="2.19199" rx="5.49476" ry="2.19199" transform="matrix(0.912659 0.408721 0.408721 -0.912659 41.5713 57.0012)" fill="#FFD38D"/>
|
||||
<ellipse cx="12.2572" cy="57.2465" rx="5.49476" ry="2.19199" transform="rotate(155.875 12.2572 57.2465)" fill="#FFD38D"/>
|
||||
<path d="M29.4741 21.2693C29.6221 21.0129 29.9922 21.0129 30.1403 21.2693L30.4733 21.8462C30.6214 22.1026 30.4363 22.4232 30.1403 22.4232H29.4741C29.178 22.4232 28.993 22.1026 29.141 21.8462L29.4741 21.2693Z" fill="#4C320C"/>
|
||||
<ellipse cx="29.8075" cy="19.3464" rx="2.30769" ry="1.73077" transform="rotate(180 29.8075 19.3464)" fill="#FFB8B9"/>
|
||||
<ellipse cx="5.49476" cy="2.19199" rx="5.49476" ry="2.19199" transform="matrix(0.912659 0.408721 0.408721 -0.912659 41.571 57.001)" fill="#FFD38D"/>
|
||||
<ellipse cx="12.2567" cy="57.2463" rx="5.49476" ry="2.19199" transform="rotate(155.875 12.2567 57.2463)" fill="#FFD38D"/>
|
||||
<path d="M29.4743 21.2693C29.6224 21.0129 29.9925 21.0129 30.1405 21.2693L30.4736 21.8462C30.6216 22.1026 30.4366 22.4232 30.1405 22.4232H29.4743C29.1782 22.4232 28.9932 22.1026 29.1412 21.8462L29.4743 21.2693Z" fill="#4C320C"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_106_1225" x="21.7318" y="8.00012" width="66.5387" height="46.1538" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<filter id="chat_filter0_i_555_1115" x="21.7313" y="8" width="66.5384" height="46.1538" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
@ -28,9 +28,9 @@
|
||||
<feGaussianBlur stdDeviation="3.17308"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.713726 0 0 0 0 0.270588 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_106_1225"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_555_1115"/>
|
||||
</filter>
|
||||
<filter id="filter1_i_106_1225" x="-28.2695" y="8.00012" width="66.5385" height="45" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<filter id="chat_filter1_i_555_1115" x="-28.269" y="8" width="66.5385" height="45" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
@ -38,7 +38,7 @@
|
||||
<feGaussianBlur stdDeviation="2.11538"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.713974 0 0 0 0 0.272498 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_106_1225"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_555_1115"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
@ -1,3 +0,0 @@
|
||||
<svg width="11" height="10" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.67965 8.83066C5.21851 9.764 6.56567 9.764 7.10453 8.83066L10.7418 2.53066C11.2807 1.59733 10.6071 0.430664 9.5294 0.430664H2.25478C1.17706 0.430664 0.503487 1.59733 1.04235 2.53066L4.67965 8.83066Z" fill="#635848"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 331 B |
@ -1,9 +0,0 @@
|
||||
<svg width="70" height="24" viewBox="0 0 70 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="0.0673828" width="70" height="23.4326" rx="11.7163" fill="url(#paint0_linear_3450_1422)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3450_1422" x1="6.51052" y1="-2.06292" x2="70.5223" y2="23.1173" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFF3E8"/>
|
||||
<stop offset="1" stop-color="#FFFAB9"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 435 B |
@ -1,19 +0,0 @@
|
||||
<svg width="402" height="154" viewBox="0 0 402 154" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="18.1921" cy="109.346" rx="5.76923" ry="2.5" fill="#AC7E35"/>
|
||||
<ellipse cx="80.4997" cy="108.192" rx="5.76923" ry="2.5" fill="#AC7E35"/>
|
||||
<g filter="url(#filter0_d_0_1)">
|
||||
<path d="M434.5 115C434.5 136.539 417.039 154 395.5 154H9C-12.5391 154 -30 136.539 -30 115V108C-30 86.4609 -12.5391 69 9 69H128.878C135.302 69 140 75.0761 140 81.5C140 110.495 163.505 134 192.5 134C221.495 134 245 110.495 245 81.5C245 75.0761 249.698 69 256.122 69H395.5C417.039 69 434.5 86.4609 434.5 108V115Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_0_1" x="-39.1" y="57.9" width="482.7" height="103.2" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-2"/>
|
||||
<feGaussianBlur stdDeviation="4.55"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_1"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_1" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1,4 +0,0 @@
|
||||
<svg width="14" height="19" viewBox="0 0 14 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.69653 1.76888C5.6629 0.626328 8.07989 0.646298 10.0278 1.82119C11.9566 3.02 13.1288 5.15954 13.1179 7.46108C13.073 9.74752 11.816 11.8968 10.2448 13.5583C9.33792 14.5215 8.32342 15.3733 7.22203 16.0962C7.10859 16.1618 6.98434 16.2057 6.8554 16.2258C6.73131 16.2205 6.61045 16.1838 6.50374 16.1191C4.82225 15.0329 3.34707 13.6464 2.14917 12.0263C1.1468 10.674 0.577319 9.04019 0.518067 7.34676C0.516766 5.0408 1.73016 2.91142 3.69653 1.76888ZM4.83313 8.30119C5.1639 9.11664 5.94465 9.64854 6.81083 9.64855C7.37828 9.65262 7.92375 9.42533 8.32571 9.01731C8.72767 8.6093 8.95272 8.05446 8.95071 7.47643C8.95374 6.59412 8.4343 5.79697 7.63492 5.45718C6.83555 5.1174 5.91392 5.302 5.30037 5.9248C4.68682 6.5476 4.50236 7.48574 4.83313 8.30119Z" fill="#E2793F"/>
|
||||
<ellipse opacity="0.4" cx="6.81738" cy="18.026" rx="4.5" ry="0.9" fill="#E2793F"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 995 B |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@ -1,3 +0,0 @@
|
||||
<svg width="12" height="13" viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.61478 9.40119C6.08057 9.40119 6.5418 9.30411 6.97213 9.1155C7.40246 8.92689 7.79347 8.65043 8.12283 8.30192C8.45219 7.95341 8.71346 7.53967 8.89171 7.08432C9.06996 6.62897 9.1617 6.14092 9.1617 5.64805C9.1617 5.15518 9.06996 4.66714 8.89171 4.21179C8.71346 3.75644 8.45219 3.34269 8.12283 2.99418C7.79347 2.64567 7.40246 2.36922 6.97213 2.18061C6.5418 1.99199 6.08057 1.89492 5.61478 1.89492C4.67408 1.89492 3.7719 2.29033 3.10673 2.99418C2.44155 3.69803 2.06786 4.65266 2.06786 5.64805C2.06786 6.64345 2.44155 7.59807 3.10673 8.30192C3.7719 9.00577 4.67408 9.40119 5.61478 9.40119ZM9.35087 8.71687L11.4672 10.9562C11.5236 11.014 11.5686 11.083 11.5995 11.1594C11.6305 11.2357 11.6467 11.3178 11.6473 11.4009C11.648 11.4839 11.633 11.5663 11.6032 11.6431C11.5734 11.72 11.5295 11.7898 11.4739 11.8485C11.4184 11.9072 11.3523 11.9536 11.2797 11.985C11.207 12.0164 11.1292 12.0321 11.0507 12.0314C10.9722 12.0306 10.8946 12.0133 10.8225 11.9804C10.7504 11.9476 10.6852 11.8999 10.6307 11.8401L8.51439 9.60073C7.56409 10.3813 6.36838 10.7493 5.17068 10.6298C3.97298 10.5104 2.86332 9.91244 2.06759 8.95773C1.27187 8.00303 0.849896 6.76333 0.887577 5.491C0.925257 4.21867 1.41976 3.00936 2.27042 2.10925C3.12107 1.20914 4.26394 0.685884 5.46636 0.646012C6.66877 0.606141 7.84036 1.05265 8.74261 1.89463C9.64486 2.73662 10.2099 3.9108 10.3228 5.17813C10.4357 6.44547 10.0879 7.71069 9.35028 8.71624L9.35087 8.71687Z" fill="#7C7C7C"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,4 +1,3 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="18" cy="18" r="18" fill="#FFB645"/>
|
||||
<path d="M15.6281 20.1601L20.4742 15.314M25.4316 12.3564L21.341 25.651C20.9744 26.8425 20.7909 27.4385 20.4748 27.636C20.2005 27.8074 19.8609 27.836 19.5623 27.7121C19.2178 27.5692 18.9383 27.0111 18.3807 25.8958L15.7897 20.7139C15.7012 20.5369 15.6569 20.4488 15.5978 20.3721C15.5453 20.304 15.4848 20.2427 15.4168 20.1903C15.3418 20.1325 15.2552 20.0892 15.0861 20.0046L9.89224 17.4077C8.77693 16.8501 8.21923 16.571 8.07632 16.2266C7.95238 15.9279 7.98064 15.588 8.152 15.3137C8.34959 14.9975 8.94555 14.8138 10.1374 14.4471L23.4319 10.3564C24.3689 10.0682 24.8376 9.92412 25.154 10.0403C25.4297 10.1415 25.647 10.3586 25.7482 10.6343C25.8644 10.9506 25.7202 11.419 25.4322 12.3551L25.4316 12.3564Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 935 B After Width: | Height: | Size: 296 B |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1,79 +1,31 @@
|
||||
import { StyleProp, StyleSheet, Text, TextStyle, type TextProps } from 'react-native';
|
||||
import { StyleSheet, Text, type TextProps } from 'react-native';
|
||||
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { FontColor, Fonts, FontSize, FontWeight } from '@/constants/Fonts';
|
||||
import { useThemeColor } from '@/hooks/useThemeColor';
|
||||
|
||||
export type ThemeColor = keyof typeof Colors.light & keyof typeof Colors.dark;
|
||||
export type ColorValue = `#${string}` | `rgb(${string})` | `rgba(${string})` | string;
|
||||
|
||||
export type ThemedTextProps = TextProps & {
|
||||
lightColor?: string;
|
||||
darkColor?: string;
|
||||
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link' | 'sfPro' | 'inter';
|
||||
weight?: FontWeight;
|
||||
size?: FontSize;
|
||||
radius?: FontSize
|
||||
color?: ThemeColor | FontColor | ColorValue;
|
||||
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
|
||||
};
|
||||
|
||||
export function isFontColorKey(key: string): key is FontColor {
|
||||
return ['bgPrimary', 'bgSecondary', 'textPrimary', 'textSecondary', 'textThird', 'textWhite'].includes(key);
|
||||
}
|
||||
|
||||
export function ThemedText({
|
||||
style,
|
||||
lightColor,
|
||||
darkColor,
|
||||
type = 'default',
|
||||
weight = 'regular',
|
||||
size,
|
||||
radius,
|
||||
color,
|
||||
...rest
|
||||
}: ThemedTextProps) {
|
||||
|
||||
const themeColor = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
|
||||
|
||||
const textColor = (() => {
|
||||
if (!color) return themeColor;
|
||||
|
||||
// 检查是否是主题颜色
|
||||
const themeColors = Object.keys(Colors.light) as ThemeColor[];
|
||||
if (themeColors.includes(color as ThemeColor)) {
|
||||
return useThemeColor({ light: lightColor, dark: darkColor }, color as ThemeColor);
|
||||
}
|
||||
|
||||
// 检查是否是 Fonts 中定义的颜色
|
||||
if (isFontColorKey(color)) {
|
||||
return Fonts[color as FontColor];
|
||||
}
|
||||
|
||||
// 返回自定义颜色值
|
||||
return color;
|
||||
})();
|
||||
|
||||
|
||||
const baseStyle: StyleProp<TextStyle> = {
|
||||
fontFamily: Fonts.quicksand,
|
||||
color: textColor,
|
||||
fontWeight: Number(Fonts[weight as keyof typeof Fonts]) as TextStyle['fontWeight'],
|
||||
};
|
||||
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={[
|
||||
baseStyle,
|
||||
{ color },
|
||||
type === 'default' ? styles.default : undefined,
|
||||
type === 'title' ? styles.title : undefined,
|
||||
type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
|
||||
type === 'subtitle' ? styles.subtitle : undefined,
|
||||
type === 'link' ? styles.link : undefined,
|
||||
type === 'sfPro' ? styles.sfPro : undefined,
|
||||
type === 'inter' ? styles.inter : undefined,
|
||||
size && { fontSize: Number(Fonts[size as keyof typeof Fonts]) },
|
||||
weight && { fontWeight: Number(Fonts[weight as keyof typeof Fonts]) as TextStyle['fontWeight'] },
|
||||
color && { color: textColor },
|
||||
style,
|
||||
]}
|
||||
{...rest}
|
||||
@ -83,41 +35,26 @@ export function ThemedText({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
default: {
|
||||
fontSize: Number(Fonts.base),
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
fontFamily: Fonts.quicksand,
|
||||
},
|
||||
defaultSemiBold: {
|
||||
fontSize: Number(Fonts.base),
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
fontWeight: '600',
|
||||
},
|
||||
title: {
|
||||
fontSize: Fonts['2xl'],
|
||||
fontWeight: '700',
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
lineHeight: 32,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: Fonts.lg,
|
||||
fontWeight: '600',
|
||||
lineHeight: 28,
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
link: {
|
||||
fontSize: Fonts.sm,
|
||||
lineHeight: 20,
|
||||
lineHeight: 30,
|
||||
fontSize: 16,
|
||||
color: '#0a7ea4',
|
||||
textDecorationLine: 'underline',
|
||||
},
|
||||
sfPro: {
|
||||
fontSize: Number(Fonts.base),
|
||||
lineHeight: 24,
|
||||
fontWeight: '600',
|
||||
fontFamily: Fonts.sfPro,
|
||||
},
|
||||
inter: {
|
||||
fontSize: Number(Fonts.base),
|
||||
lineHeight: 24,
|
||||
fontWeight: '600',
|
||||
fontFamily: Fonts.inter,
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,32 +1,9 @@
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { FontColor, Fonts } from '@/constants/Fonts';
|
||||
import { useThemeColor } from '@/hooks/useThemeColor';
|
||||
import { View, type ViewProps } from 'react-native';
|
||||
import { ColorValue, isFontColorKey, ThemeColor } from './ThemedText';
|
||||
|
||||
type ThemedViewProps = ViewProps & {
|
||||
className?: string;
|
||||
bgColor?: FontColor | ColorValue | string;
|
||||
};
|
||||
|
||||
export function ThemedView({ className, style, bgColor, ...props }: ThemedViewProps) {
|
||||
const themeColor = useThemeColor({ light: bgColor, dark: bgColor }, 'background');
|
||||
|
||||
const bgColorValue = (() => {
|
||||
if (!bgColor) return themeColor;
|
||||
|
||||
// 检查是否是主题颜色
|
||||
const themeColors = Object.keys(Colors.light) as ThemeColor[];
|
||||
if (themeColors.includes(bgColor as ThemeColor)) {
|
||||
return useThemeColor({ light: bgColor, dark: bgColor }, bgColor as ThemeColor);
|
||||
}
|
||||
// 检查是否是 Fonts 中定义的颜色
|
||||
if (isFontColorKey(bgColor)) {
|
||||
return Fonts[bgColor];
|
||||
}
|
||||
// 返回自定义颜色值
|
||||
return bgColor;
|
||||
})();
|
||||
|
||||
return <View className={className} style={[{ backgroundColor: bgColorValue }, style]} {...props} />;
|
||||
export function ThemedView({ className, style, ...props }: ThemedViewProps) {
|
||||
return <View className={className} style={style} {...props} />;
|
||||
}
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { ContentPart, Message } from '@/types/ask';
|
||||
import { useFocusEffect } from 'expo-router';
|
||||
import React, { Dispatch, ForwardedRef, forwardRef, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
FlatList,
|
||||
FlatListProps,
|
||||
Keyboard,
|
||||
SafeAreaView,
|
||||
View
|
||||
} from 'react-native';
|
||||
@ -54,13 +52,6 @@ function ChatComponent(
|
||||
}
|
||||
}, [userMessages.length]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
Keyboard.dismiss();
|
||||
}, [Keyboard, sessionId])
|
||||
);
|
||||
|
||||
|
||||
const renderMessageItem = useCallback(({ item, index }: { item: Message, index: number }) => {
|
||||
const itemStyle = index === 0 ? { marginTop: 16, marginHorizontal: 16 } : { marginHorizontal: 16 };
|
||||
return (
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import IP from "@/assets/icons/svg/ip.svg";
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { Fonts } from "@/constants/Fonts";
|
||||
import { webSocketManager } from "@/lib/websocket-util";
|
||||
import { Message } from "@/types/ask";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Dimensions, Image, ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { createNewConversation } from "./utils";
|
||||
|
||||
interface AskHelloProps {
|
||||
@ -14,8 +14,6 @@ interface AskHelloProps {
|
||||
}
|
||||
export default function AskHello({ setUserMessages, setConversationId, setIsHello }: AskHelloProps) {
|
||||
const { t } = useTranslation();
|
||||
const width = Dimensions.get('window').width;
|
||||
const height = Dimensions.get('window').height;
|
||||
|
||||
const handleCase = async (text: string) => {
|
||||
setIsHello(false);
|
||||
@ -52,20 +50,22 @@ export default function AskHello({ setUserMessages, setConversationId, setIsHell
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: 8,
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 20
|
||||
}}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View className="items-center">
|
||||
<ThemedText style={{ textAlign: 'center', lineHeight: 40, }} size="title" weight="bold">
|
||||
<ThemedText style={{ fontSize: 32, fontWeight: 'bold', textAlign: 'center', lineHeight: 40, }}>
|
||||
{t('ask.hi', { ns: 'ask' })}
|
||||
{"\n"}
|
||||
{t('ask.iAmMemo', { ns: 'ask' })}
|
||||
</ThemedText>
|
||||
<Image source={require('@/assets/images/png/icon/ip.png')} style={{ width: width * 0.4, height: height * 0.25 }} />
|
||||
<ThemedText className="text-center -mt-10" size='base' color="textPrimary" type="sfPro" weight="medium">
|
||||
<View className="-mt-10">
|
||||
<IP />
|
||||
</View>
|
||||
<ThemedText className="!text-textPrimary text-center -mt-20">
|
||||
{t('ask.ready', { ns: 'ask' })}
|
||||
{"\n"}
|
||||
{t('ask.justAsk', { ns: 'ask' })}
|
||||
@ -110,15 +110,10 @@ const styles = StyleSheet.create({
|
||||
marginTop: 16
|
||||
},
|
||||
case: {
|
||||
borderWidth: 1,
|
||||
borderColor: Fonts["textPrimary"],
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
borderColor: "#FFB645",
|
||||
borderRadius: 24,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
width: 'auto',
|
||||
fontSize: Fonts["sm"],
|
||||
color: Fonts["textSecondary"],
|
||||
fontFamily: Fonts["sfPro"]
|
||||
|
||||
width: 'auto'
|
||||
}
|
||||
})
|
||||
@ -12,7 +12,6 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import { Fonts } from '@/constants/Fonts';
|
||||
import { webSocketManager, WsMessage } from '@/lib/websocket-util';
|
||||
import { Message } from '@/types/ask';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -26,12 +25,11 @@ interface Props {
|
||||
setConversationId: (conversationId: string) => void,
|
||||
selectedImages: string[];
|
||||
setSelectedImages: Dispatch<SetStateAction<string[]>>;
|
||||
isHello: boolean;
|
||||
}
|
||||
const RENDER_INTERVAL = 50; // 渲染间隔,单位毫秒
|
||||
|
||||
export default function SendMessage(props: Props) {
|
||||
const { setIsHello, conversationId, setUserMessages, setConversationId, selectedImages, setSelectedImages, isHello } = props;
|
||||
const { setIsHello, conversationId, setUserMessages, setConversationId, selectedImages, setSelectedImages } = props;
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -191,33 +189,32 @@ export default function SendMessage(props: Props) {
|
||||
}
|
||||
]));
|
||||
let currentSessionId = conversationId;
|
||||
console.log("currentSessionIdcurrentSessionId", currentSessionId);
|
||||
|
||||
// 如果没有对话ID,先创建一个新对话
|
||||
if (!currentSessionId) {
|
||||
const newCurrentSessionId = await createNewConversation(text);
|
||||
if (newCurrentSessionId) {
|
||||
setConversationId(newCurrentSessionId);
|
||||
} else {
|
||||
console.error("无法获取 session_id,消息发送失败1。");
|
||||
setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng'));
|
||||
}
|
||||
currentSessionId = await createNewConversation(text);
|
||||
setConversationId(currentSessionId);
|
||||
webSocketManager.send({
|
||||
type: 'Chat',
|
||||
session_id: currentSessionId,
|
||||
message: text,
|
||||
image_material_ids: selectedImages.length > 0 ? selectedImages : undefined,
|
||||
});
|
||||
setSelectedImages([]);
|
||||
}
|
||||
|
||||
// 通过 WebSocket 发送消息
|
||||
if (currentSessionId) {
|
||||
try {
|
||||
webSocketManager.send({
|
||||
type: 'Chat',
|
||||
session_id: currentSessionId,
|
||||
message: text,
|
||||
image_material_ids: selectedImages.length > 0 ? selectedImages : undefined,
|
||||
});
|
||||
setSelectedImages([]);
|
||||
} catch (error) {
|
||||
console.error("无法获取 session_id,消息发送失败2。", error);
|
||||
setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng'));
|
||||
}
|
||||
webSocketManager.send({
|
||||
type: 'Chat',
|
||||
session_id: currentSessionId,
|
||||
message: text,
|
||||
image_material_ids: selectedImages.length > 0 ? selectedImages : undefined,
|
||||
});
|
||||
setSelectedImages([]);
|
||||
} else {
|
||||
console.error("无法获取 session_id,消息发送失败。");
|
||||
// 可以在这里处理错误,例如显示一个提示
|
||||
setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng'));
|
||||
}
|
||||
// 将输入框清空
|
||||
setInputValue('');
|
||||
@ -246,20 +243,19 @@ export default function SendMessage(props: Props) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View className="relative w-full">
|
||||
<ScrollView horizontal={true} style={{ display: isHello ? 'flex' : 'none' }}>
|
||||
<ScrollView horizontal={true}>
|
||||
<TouchableOpacity style={[styles.button, { borderColor: '#FFB645' }]} onPress={() => handleQuitly('search')}>
|
||||
<SunSvg width={18} height={18} />
|
||||
<ThemedText type="sfPro" size="sm" weight='regular' color='textSecondary'>{t("ask:ask.search")}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.button, { borderColor: '#E2793F' }]} onPress={() => handleQuitly('video')}>
|
||||
<ThemedText>{t("ask:ask.search")}</ThemedText>
|
||||
</TouchableOpacity><TouchableOpacity style={[styles.button, { borderColor: '#E2793F' }]} onPress={() => handleQuitly('video')}>
|
||||
<VideoSvg width={18} height={18} />
|
||||
<ThemedText type="sfPro" size="sm" weight='regular' color='textSecondary'>{t("ask:ask.video")}</ThemedText>
|
||||
<ThemedText>{t("ask:ask.video")}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Ask MeMo Anything..."
|
||||
placeholderTextColor={Fonts["textPrimary"]}
|
||||
placeholderTextColor="#999"
|
||||
value={inputValue}
|
||||
onChangeText={(text: string) => {
|
||||
setInputValue(text);
|
||||
@ -269,14 +265,13 @@ export default function SendMessage(props: Props) {
|
||||
returnKeyType="send"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={[styles.voiceButton, { bottom: -10 }]}
|
||||
onPress={handleSubmit}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 6,
|
||||
bottom: 9
|
||||
}}
|
||||
className="absolute right-2"
|
||||
>
|
||||
<SendSvg />
|
||||
<View>
|
||||
<SendSvg color={'white'} width={24} height={24} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@ -290,35 +285,24 @@ const styles = StyleSheet.create({
|
||||
margin: 5,
|
||||
borderRadius: 25,
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderWidth: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: 5,
|
||||
gap: 5
|
||||
},
|
||||
container: {
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#transparent',
|
||||
},
|
||||
input: {
|
||||
color: Fonts["textPrimary"],
|
||||
borderColor: '#AC7E35',
|
||||
borderColor: '#FF9500',
|
||||
borderWidth: 1,
|
||||
borderRadius: 28,
|
||||
borderRadius: 25,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
lineHeight: 20,
|
||||
paddingVertical: 13,
|
||||
fontSize: 16,
|
||||
width: '100%',
|
||||
paddingRight: 50,
|
||||
backgroundColor: '#fff',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
width: '100%', // 确保输入框宽度撑满
|
||||
paddingRight: 50
|
||||
},
|
||||
voiceButton: {
|
||||
padding: 8,
|
||||
@ -326,6 +310,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: '#FF9500',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 8, // 添加一点右边距
|
||||
position: 'absolute',
|
||||
transform: [{ translateY: -12 }],
|
||||
},
|
||||
});
|
||||
@ -3,6 +3,7 @@ import { Message } from "@/types/ask";
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import * as MediaLibrary from 'expo-media-library';
|
||||
import { TFunction } from "i18next";
|
||||
import { useCallback } from "react";
|
||||
import { Alert } from 'react-native';
|
||||
|
||||
// 实现一个函数,从两个数组中轮流插入新数组
|
||||
@ -18,12 +19,12 @@ export const mergeArrays = (arr1: any[], arr2: any[]) => {
|
||||
|
||||
|
||||
// 创建新对话并获取消息
|
||||
export const createNewConversation = async (user_text: string) => {
|
||||
export const createNewConversation = useCallback(async (user_text: string) => {
|
||||
const data = await fetchApi<string>("/chat/new", {
|
||||
method: "POST",
|
||||
});
|
||||
return data
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 获取对话信息
|
||||
export const getConversation = async ({
|
||||
|
||||
@ -72,7 +72,7 @@ const CascaderComponent: React.FC<CascaderProps> = ({
|
||||
// 渲染某一级选项
|
||||
const renderLevel = (items: CascaderItem[], level: number) => {
|
||||
return (
|
||||
<View style={[styles.levelContainer]}>
|
||||
<View style={[styles.levelContainer, { width: columnWidth }]}>
|
||||
{items.map((item, index) => {
|
||||
const isActive = selectedItems[level]?.name === item.name;
|
||||
return (
|
||||
@ -95,7 +95,6 @@ const CascaderComponent: React.FC<CascaderProps> = ({
|
||||
textStyle,
|
||||
isActive && [styles.activeText, activeTextStyle]
|
||||
]}
|
||||
type='sfPro'
|
||||
>
|
||||
{item.name}
|
||||
</ThemedText>
|
||||
@ -113,46 +112,27 @@ const CascaderComponent: React.FC<CascaderProps> = ({
|
||||
|
||||
// 渲染所有级联列
|
||||
const renderColumns = () => {
|
||||
const totalLevels = allLevelsData.length;
|
||||
return allLevelsData.map((items, level) => {
|
||||
// 计算每列的宽度
|
||||
let width;
|
||||
if (totalLevels === 1) {
|
||||
width = '100%'; // 只有一级时占满全部宽度
|
||||
} else if (totalLevels === 2) {
|
||||
width = level === 0 ? '40%' : '60%'; // 两级时第一级40%,第二级60%
|
||||
} else {
|
||||
// 三级或以上时,前两级各占30%,其余级别平分剩余40%
|
||||
if (level < 2) {
|
||||
width = '30%';
|
||||
} else {
|
||||
const remainingLevels = totalLevels - 2;
|
||||
width = remainingLevels > 0 ? `${40 / remainingLevels}%` : '40%';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
key={`column-${level}`}
|
||||
style={[
|
||||
styles.column,
|
||||
{ width },
|
||||
showDivider && level < totalLevels - 1 && [
|
||||
styles.columnWithDivider,
|
||||
{ borderRightColor: dividerColor }
|
||||
]
|
||||
]}
|
||||
return allLevelsData.map((items, level) => (
|
||||
<View
|
||||
key={`column-${level}`}
|
||||
style={[
|
||||
styles.column,
|
||||
{ width: columnWidth },
|
||||
showDivider && level < allLevelsData.length - 1 && [
|
||||
styles.columnWithDivider,
|
||||
{ borderRightColor: dividerColor }
|
||||
]
|
||||
]}
|
||||
>
|
||||
<ScrollView
|
||||
style={{ flex: 1 }}
|
||||
showsVerticalScrollIndicator={true}
|
||||
contentContainerStyle={{ paddingBottom: 20 }}
|
||||
>
|
||||
<ScrollView
|
||||
style={{ flex: 1 }}
|
||||
showsVerticalScrollIndicator={true}
|
||||
contentContainerStyle={{ paddingBottom: 20 }}
|
||||
>
|
||||
{renderLevel(items, level)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
{renderLevel(items, level)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
));
|
||||
};
|
||||
|
||||
// 自定义显示内容
|
||||
@ -186,17 +166,15 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
height: 300,
|
||||
height: 300, // Set a fixed height for the container
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
column: {
|
||||
height: '100%',
|
||||
maxHeight: '100%',
|
||||
flexShrink: 0,
|
||||
},
|
||||
columnWithDivider: {
|
||||
borderRightWidth: 1,
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import CancelSvg from "@/assets/icons/svg/cancel.svg";
|
||||
import React from 'react';
|
||||
import { View, Pressable, Image, StyleSheet } from 'react-native';
|
||||
import ContextMenu from "../../gusture/contextMenu";
|
||||
import DownloadSvg from "@/assets/icons/svg/download.svg";
|
||||
import CancelSvg from "@/assets/icons/svg/cancel.svg";
|
||||
import { ContentPart } from "@/types/ask";
|
||||
import { TFunction } from 'i18next';
|
||||
import React from 'react';
|
||||
import { Image, Pressable, StyleSheet, Text as ThemedText, View } from 'react-native';
|
||||
import { saveMediaToGallery } from "../../ask/utils";
|
||||
import ContextMenu from "../../gusture/contextMenu";
|
||||
|
||||
interface MediaGridProps {
|
||||
mediaItems: ContentPart[];
|
||||
@ -16,32 +16,18 @@ interface MediaGridProps {
|
||||
}
|
||||
|
||||
const MediaGrid = ({ mediaItems, setModalVisible, setCancel, cancel, t }: MediaGridProps) => {
|
||||
// Only show up to 6 images (2 rows of 3)
|
||||
// 只取前6个元素(2行,每行3个)
|
||||
const displayItems = mediaItems.slice(0, 6);
|
||||
const itemCount = displayItems.length;
|
||||
|
||||
// Calculate item width based on number of items
|
||||
const getItemWidth = () => {
|
||||
if (itemCount === 1) return '100%';
|
||||
if (itemCount === 2) return '49%';
|
||||
return '32%'; // For 3+ items
|
||||
};
|
||||
|
||||
// Calculate container style based on number of items
|
||||
const getContainerStyle = () => {
|
||||
if (itemCount === 1) return styles.singleItemContainer;
|
||||
return styles.multiItemContainer;
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, getContainerStyle()]}>
|
||||
{displayItems.map((media, index) => (
|
||||
<View className="flex-row flex-wrap justify-between">
|
||||
{displayItems.map((media) => (
|
||||
<Pressable
|
||||
key={media.id}
|
||||
onPress={() => {
|
||||
setModalVisible({ visible: true, data: media });
|
||||
}}
|
||||
style={[styles.imageContainer, { width: getItemWidth() }]}
|
||||
className="mb-2 w-[32%] aspect-square"
|
||||
>
|
||||
<ContextMenu
|
||||
items={[
|
||||
@ -65,19 +51,24 @@ const MediaGrid = ({ mediaItems, setModalVisible, setCancel, cancel, t }: MediaG
|
||||
}
|
||||
]}
|
||||
cancel={cancel}
|
||||
menuStyle={styles.contextMenu}
|
||||
menuStyle={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 8,
|
||||
padding: 8,
|
||||
minWidth: 150,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 4,
|
||||
elevation: 5,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: media?.url }}
|
||||
style={styles.image}
|
||||
className="w-full h-full rounded-xl"
|
||||
resizeMode="cover"
|
||||
loadingIndicatorSource={require('@/assets/images/png/placeholder.png')}
|
||||
/>
|
||||
{itemCount > 3 && index === 5 && mediaItems.length > 6 && (
|
||||
<View style={styles.overlay}>
|
||||
<ThemedText style={styles.overlayText}>+{mediaItems.length - 5}</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
</ContextMenu>
|
||||
</Pressable>
|
||||
))}
|
||||
@ -85,50 +76,6 @@ const MediaGrid = ({ mediaItems, setModalVisible, setCancel, cancel, t }: MediaG
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
singleItemContainer: {
|
||||
width: '100%',
|
||||
},
|
||||
multiItemContainer: {
|
||||
width: '100%',
|
||||
},
|
||||
imageContainer: {
|
||||
marginBottom: 8,
|
||||
aspectRatio: 1,
|
||||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 8,
|
||||
},
|
||||
contextMenu: {
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 8,
|
||||
padding: 8,
|
||||
minWidth: 150,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 4,
|
||||
elevation: 5,
|
||||
},
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
borderRadius: 8,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
overlayText: {
|
||||
color: 'white',
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export default MediaGrid;
|
||||
|
||||
@ -26,7 +26,7 @@ const MessageBubble = ({
|
||||
}: MessageBubbleProps) => {
|
||||
return (
|
||||
<View
|
||||
className={`${isUser ? '!bg-bgPrimary ml-10 rounded-full' : '!bg-aiBubble rounded-2xl'} border-0 ${!isUser && isMessageContainMedia(item) ? '!rounded-3xl' : '!rounded-3xl'} px-3`}
|
||||
className={`${isUser ? '!bg-bgPrimary ml-10 rounded-full' : '!bg-aiBubble rounded-2xl'} border-0 ${!isUser && isMessageContainMedia(item) ? '!rounded-t-3xl !rounded-b-2xl' : '!rounded-3xl'} px-3`}
|
||||
style={{ marginRight: getMessageText(item) == "keepSearchIng" ? 0 : isUser ? 0 : 10 }}
|
||||
>
|
||||
<MessageContent
|
||||
|
||||
@ -3,11 +3,9 @@ import { ContentPart, Message, User } from "@/types/ask";
|
||||
import { TFunction } from "i18next";
|
||||
import React from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import { Fonts } from "@/constants/Fonts";
|
||||
import MessageRow from './MessageRow';
|
||||
|
||||
interface RenderMessageProps {
|
||||
@ -42,40 +40,23 @@ const MessageItem = ({ setCancel, cancel = true, t, insets, item, sessionId, set
|
||||
setSelectedImages={setSelectedImages}
|
||||
setModalDetailsVisible={setModalDetailsVisible}
|
||||
/>
|
||||
{/* {item.content instanceof Array && item.content.filter((media: ContentPart) => media.type !== 'text').length > 0 && (
|
||||
<View style={styles.tips}>
|
||||
<TouchableOpacity style={[styles.tip, { borderRadius: 16 }]} onPress={() => {
|
||||
|
||||
}}>
|
||||
<ThemedText style={styles.tipText}>Help me create a warm, cozy video.</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.tip, { borderTopLeftRadius: 16, borderTopRightRadius: 16, borderBottomLeftRadius: 24, borderBottomRightRadius: 24 }]}>
|
||||
<ThemedText style={styles.tipText}>Help me find materials for subsequent operations.</ThemedText>
|
||||
</TouchableOpacity>
|
||||
{/* {item.askAgain && item.askAgain.length > 0 && (
|
||||
<View className={`mr-10`}>
|
||||
{item.askAgain.map((suggestion, index, array) => (
|
||||
<TouchableOpacity
|
||||
key={suggestion.id}
|
||||
className={`bg-yellow-50 rounded-xl px-4 py-2 border border-yellow-200 border-0 mb-2 ${index === array.length - 1 ? 'mb-0 rounded-b-3xl rounded-t-2xl' : 'rounded-2xl'}`}
|
||||
>
|
||||
<Text className="text-gray-700">{suggestion.text}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)} */}
|
||||
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tips: {
|
||||
flexDirection: 'column',
|
||||
gap: 5,
|
||||
marginRight: 10,
|
||||
},
|
||||
tip: {
|
||||
backgroundColor: '#FFF8DE',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
tipText: {
|
||||
color: '#4C320C',
|
||||
fontSize: 14,
|
||||
fontFamily: Fonts['inter']
|
||||
}
|
||||
});
|
||||
|
||||
export default React.memo(MessageItem);
|
||||
|
||||
|
||||
@ -98,7 +98,13 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0
|
||||
right: 0,
|
||||
backgroundColor: 'white',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: -4 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
elevation: 10,
|
||||
},
|
||||
backgroundImage: {
|
||||
width,
|
||||
@ -116,18 +122,18 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 80,
|
||||
height: 80, // Set a fixed height for the navbar
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 32,
|
||||
backgroundColor: 'transparent',
|
||||
backgroundColor: 'transparent', // Make sure it's transparent
|
||||
},
|
||||
centerButton: {
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: -30,
|
||||
transform: [{ translateX: -17 }],
|
||||
left: width / 2,
|
||||
top: -42.5, // Adjust this value to move the button up or down
|
||||
marginLeft: -42.5, // Half of the button width (85/2)
|
||||
width: 85,
|
||||
height: 85,
|
||||
justifyContent: 'center',
|
||||
@ -143,8 +149,8 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
|
||||
},
|
||||
statusIndicator: {
|
||||
position: 'absolute',
|
||||
top: 3,
|
||||
right: 20,
|
||||
top: 15,
|
||||
right: 15,
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
@ -162,7 +168,7 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Image source={require('@/assets/images/png/owner/ask.png')} style={{ width: "100%" }} />
|
||||
<Image source={require('@/assets/images/png/owner/ask.png')} style={{ width: width, height: 80, resizeMode: 'cover' }} />
|
||||
<View style={styles.navContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => navigateTo('/memo-list')}
|
||||
@ -180,7 +186,7 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
|
||||
style={styles.centerButton}
|
||||
>
|
||||
<View style={styles.statusIndicator} />
|
||||
<Image source={require('@/assets/images/png/owner/askIP.png')} />
|
||||
<CenterButtonSvg />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
|
||||
@ -115,13 +115,13 @@ const Code = ({ phone }: CodeProps) => {
|
||||
<View style={styles.container}>
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={styles.headerContainer}>
|
||||
<ThemedText style={styles.title} color="textSecondary" size="xl" weight="bold">
|
||||
<ThemedText style={styles.title}>
|
||||
{t("auth.telLogin.codeTitle", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.subtitle} type="sfPro" color="textPrimary" size="sm">
|
||||
<ThemedText style={styles.subtitle}>
|
||||
{t("auth.telLogin.secondTitle", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<ThemedText color="bgSecondary" size="sm" weight="bold">
|
||||
<ThemedText style={styles.phoneNumber}>
|
||||
{phone}
|
||||
</ThemedText>
|
||||
</View>
|
||||
@ -144,13 +144,13 @@ const Code = ({ phone }: CodeProps) => {
|
||||
/>
|
||||
<View style={[styles.errorContainer, { opacity: error ? 1 : 0 }]}>
|
||||
<Error />
|
||||
<ThemedText style={styles.errorText} size="xxs" color="bgSecondary" type="inter">
|
||||
<ThemedText style={styles.errorText}>
|
||||
{error}
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
<View style={styles.footerContainer}>
|
||||
<ThemedText size="sm" color="textPrimary" type="sfPro">
|
||||
<ThemedText style={styles.footerText}>
|
||||
{t("auth.telLogin.sendAgain", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={() => {
|
||||
@ -158,16 +158,10 @@ const Code = ({ phone }: CodeProps) => {
|
||||
sendVerificationCode()
|
||||
}
|
||||
}}>
|
||||
<ThemedText
|
||||
style={[
|
||||
styles.resendText,
|
||||
countdown > 0 && styles.disabledResendText
|
||||
]}
|
||||
size="sm"
|
||||
color="bgSecondary"
|
||||
type="inter"
|
||||
weight="bold"
|
||||
>
|
||||
<ThemedText style={[
|
||||
styles.resendText,
|
||||
countdown > 0 && styles.disabledResendText
|
||||
]}>
|
||||
{countdown > 0 ? `${countdown}s${t("auth.telLogin.resend", { ns: 'login' })}` : t("auth.telLogin.resend", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
@ -191,13 +185,23 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: '600',
|
||||
marginBottom: 8,
|
||||
paddingTop: 4,
|
||||
color: '#111827',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: '#4B5563',
|
||||
textAlign: 'center',
|
||||
marginBottom: 4,
|
||||
},
|
||||
phoneNumber: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: '#E2793F',
|
||||
},
|
||||
otpContainer: {
|
||||
width: '100%',
|
||||
height: 80,
|
||||
@ -224,6 +228,9 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
},
|
||||
errorText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
color: '#E2793F',
|
||||
marginLeft: 8,
|
||||
},
|
||||
footerContainer: {
|
||||
@ -231,7 +238,12 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
marginTop: 8,
|
||||
},
|
||||
footerText: {
|
||||
color: '#6B7280',
|
||||
},
|
||||
resendText: {
|
||||
color: '#E2793F',
|
||||
fontWeight: '500',
|
||||
marginLeft: 4,
|
||||
},
|
||||
disabledResendText: {
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
import { Fonts } from "@/constants/Fonts";
|
||||
import { fetchApi } from "@/lib/server-api-util";
|
||||
import { User } from "@/types/user";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StyleSheet, TouchableOpacity, View } from "react-native";
|
||||
import { ActivityIndicator, StyleSheet, TextInput, TouchableOpacity, View } from "react-native";
|
||||
import { ThemedText } from "../ThemedText";
|
||||
import Button from "./ui/Button";
|
||||
import TextInput from "./ui/TextInput";
|
||||
|
||||
interface LoginProps {
|
||||
setIsSignUp?: (isSignUp: string) => void;
|
||||
@ -72,29 +69,45 @@ const ForgetPwd = ({ setIsSignUp, updateUrlParam, setError }: LoginProps) => {
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* 邮箱 */}
|
||||
<TextInput
|
||||
label={t('auth.forgetPwd.title', { ns: 'login' })}
|
||||
placeholder={t('auth.forgetPwd.emailPlaceholder', { ns: 'login' })}
|
||||
onChangeText={setEmail}
|
||||
autoCapitalize="none"
|
||||
value={email}
|
||||
/>
|
||||
{/* 发送邮箱 */}
|
||||
<Button
|
||||
isLoading={isDisabled || loading}
|
||||
handleLogin={handleSubmit}
|
||||
text={isDisabled
|
||||
? `${t("auth.forgetPwd.sendEmailBtnDisabled", { ns: "login" })} (${countdown}s)`
|
||||
: t("auth.forgetPwd.sendEmailBtn", { ns: "login" })}
|
||||
/>
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.inputLabel}>
|
||||
{t('auth.forgetPwd.title', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
placeholder={t('auth.forgetPwd.emailPlaceholder', { ns: 'login' })}
|
||||
placeholderTextColor="#ccc"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.submitButton,
|
||||
(isDisabled || loading) && styles.disabledButton
|
||||
]}
|
||||
onPress={handleSubmit}
|
||||
disabled={isDisabled || loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<ThemedText style={styles.buttonText}>
|
||||
{isDisabled
|
||||
? `${t("auth.forgetPwd.sendEmailBtnDisabled", { ns: "login" })} (${countdown}s)`
|
||||
: t("auth.forgetPwd.sendEmailBtn", { ns: "login" })}
|
||||
</ThemedText>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 返回登录 */}
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={handleBackToLogin}
|
||||
>
|
||||
<ThemedText type='inter' color="bgSecondary" size="sm">
|
||||
<ThemedText style={styles.backButtonText}>
|
||||
{t('auth.forgetPwd.goback', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
@ -110,24 +123,16 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 20,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: Fonts['base'],
|
||||
color: Fonts['textPrimary'],
|
||||
fontWeight: Fonts['bold'],
|
||||
fontFamily: Fonts['sfPro'],
|
||||
fontSize: 16,
|
||||
color: '#1F2937',
|
||||
marginBottom: 8,
|
||||
marginLeft: 8,
|
||||
},
|
||||
textInput: {
|
||||
borderRadius: Fonts['xs'],
|
||||
paddingHorizontal: Fonts['base'],
|
||||
paddingVertical: Fonts['xs'],
|
||||
fontSize: Fonts['sm'],
|
||||
lineHeight: Fonts['base'],
|
||||
textAlignVertical: 'center',
|
||||
backgroundColor: Fonts['bgInput'],
|
||||
color: Fonts['textSecondary'],
|
||||
fontFamily: Fonts['inter'],
|
||||
paddingRight: Fonts['5xl'],
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
fontSize: 16,
|
||||
backgroundColor: '#FFF8DE',
|
||||
},
|
||||
submitButton: {
|
||||
width: '100%',
|
||||
@ -146,7 +151,11 @@ const styles = StyleSheet.create({
|
||||
backButton: {
|
||||
alignSelf: 'center',
|
||||
marginTop: 24,
|
||||
}
|
||||
},
|
||||
backButtonText: {
|
||||
color: '#1F2937',
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
|
||||
export default ForgetPwd;
|
||||
@ -1,15 +1,14 @@
|
||||
import { Fonts } from "@/constants/Fonts";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StyleSheet, TouchableOpacity, View } from "react-native";
|
||||
import { ActivityIndicator, StyleSheet, TextInput, TouchableOpacity, View } from "react-native";
|
||||
import { useAuth } from "../../contexts/auth-context";
|
||||
import { fetchApi } from "../../lib/server-api-util";
|
||||
import { User } from "../../types/user";
|
||||
import { ThemedText } from "../ThemedText";
|
||||
import Button from "./ui/Button";
|
||||
import TextInput from "./ui/TextInput";
|
||||
|
||||
const REMEMBER_ACCOUNT_KEY = 'fairclip_remembered_account';
|
||||
interface LoginProps {
|
||||
updateUrlParam: (status: string, value: string) => void;
|
||||
setError: (error: string) => void;
|
||||
@ -44,12 +43,18 @@ const Login = ({ updateUrlParam, setError, setShowPassword, showPassword }: Logi
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
}, true, false);
|
||||
login({ ...res, email: res?.account }, res.access_token || '');
|
||||
const userInfo = await fetchApi<User>("/iam/user-info");
|
||||
if (userInfo?.nickname) {
|
||||
router.replace('/ask');
|
||||
|
||||
// 确保用户数据和令牌存在
|
||||
if (res && res.access_token) {
|
||||
login({ ...res, email: res?.account || email }, res.access_token);
|
||||
const userInfo = await fetchApi<User>("/iam/user-info");
|
||||
if (userInfo?.nickname) {
|
||||
router.replace('/ask');
|
||||
} else {
|
||||
router.replace('/user-message');
|
||||
}
|
||||
} else {
|
||||
router.replace('/user-message');
|
||||
throw new Error(t('auth.login.loginError', { ns: 'login' }));
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : t('auth.login.loginError', { ns: 'login' });
|
||||
@ -69,73 +74,85 @@ const Login = ({ updateUrlParam, setError, setShowPassword, showPassword }: Logi
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.inputContainer, { marginBottom: 20 }]}>
|
||||
<ThemedText style={styles.inputLabel}>
|
||||
{t('auth.login.email', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
placeholder={t('auth.login.accountPlaceholder', { ns: 'login' })}
|
||||
placeholderTextColor="#ccc"
|
||||
value={email}
|
||||
onChangeText={(text) => {
|
||||
setEmail(text);
|
||||
setError('123');
|
||||
}}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 邮箱 */}
|
||||
<TextInput
|
||||
label={t('auth.login.email', { ns: 'login' })}
|
||||
placeholder={t('auth.login.accountPlaceholder', { ns: 'login' })}
|
||||
onChangeText={(text) => {
|
||||
setEmail(text);
|
||||
setError('123');
|
||||
}}
|
||||
autoCapitalize="none"
|
||||
value={email}
|
||||
/>
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.inputLabel}>
|
||||
{t('auth.login.password', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<View style={styles.passwordInputContainer}>
|
||||
<TextInput
|
||||
style={[styles.textInput, { paddingRight: 48 }]}
|
||||
placeholder={t('auth.login.passwordPlaceholder', { ns: 'login' })}
|
||||
placeholderTextColor="#ccc"
|
||||
value={password}
|
||||
onChangeText={(text) => {
|
||||
setPassword(text);
|
||||
setError('123');
|
||||
}}
|
||||
secureTextEntry={!showPassword}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.eyeIcon}
|
||||
onPress={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
<Ionicons
|
||||
name={showPassword ? 'eye' : 'eye-off'}
|
||||
size={20}
|
||||
color="#666"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 密码 */}
|
||||
<TextInput
|
||||
label={t('auth.login.password', { ns: 'login' })}
|
||||
placeholder={t('auth.login.passwordPlaceholder', { ns: 'login' })}
|
||||
onChangeText={(text) => {
|
||||
setPassword(text);
|
||||
setError('123');
|
||||
}}
|
||||
autoCapitalize="none"
|
||||
value={password}
|
||||
type="password"
|
||||
setShowPassword={setShowPassword}
|
||||
showPassword={showPassword}
|
||||
containerStyle={{ marginBottom: 0 }}
|
||||
/>
|
||||
|
||||
{/* 忘记密码 */}
|
||||
<TouchableOpacity
|
||||
style={styles.forgotPassword}
|
||||
onPress={handleForgotPassword}
|
||||
>
|
||||
<ThemedText style={styles.forgotPasswordText} color="textPrimary" type="inter">
|
||||
<ThemedText style={styles.forgotPasswordText}>
|
||||
{t('auth.login.forgotPassword', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 登录按钮 */}
|
||||
<Button isLoading={isLoading} handleLogin={handleLogin} text={t('auth.login.loginButton', { ns: 'login' })} />
|
||||
<TouchableOpacity
|
||||
style={[styles.loginButton, isLoading && { opacity: 0.7 }]}
|
||||
onPress={handleLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<ThemedText style={styles.loginButtonText}>
|
||||
{t('auth.login.loginButton', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 注册 */}
|
||||
<View style={styles.signupContainer}>
|
||||
<ThemedText style={styles.signupText} type="sfPro">
|
||||
<ThemedText style={styles.signupText}>
|
||||
{t('auth.login.signUpMessage', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={handleSignUp}>
|
||||
<ThemedText style={styles.signupLink} type="sfPro">
|
||||
<ThemedText style={styles.signupLink}>
|
||||
{t('auth.login.signUp', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 第三方登录 */}
|
||||
<View style={{ width: "100%", alignItems: "center", opacity: 0 }}>
|
||||
<View style={styles.loginTypeContainer}>
|
||||
<ThemedText>
|
||||
OR
|
||||
</ThemedText>
|
||||
<View style={{ flexDirection: 'row', justifyContent: "space-between", width: "100%" }}>
|
||||
<ThemedText style={styles.loginType} />
|
||||
<ThemedText style={styles.loginType} />
|
||||
<ThemedText style={styles.loginType} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@ -144,43 +161,22 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
loginTypeContainer: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
width: "70%"
|
||||
},
|
||||
loginType: {
|
||||
borderRadius: 12,
|
||||
width: 42,
|
||||
height: 42,
|
||||
textAlign: 'center',
|
||||
backgroundColor: '#FADBA1'
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: Fonts['base'],
|
||||
color: Fonts['textPrimary'],
|
||||
fontWeight: Fonts['bold'],
|
||||
fontFamily: Fonts['sfPro'],
|
||||
fontSize: 16,
|
||||
color: '#1F2937',
|
||||
marginBottom: 8,
|
||||
marginLeft: 8,
|
||||
},
|
||||
textInput: {
|
||||
borderRadius: Fonts['xs'],
|
||||
paddingHorizontal: Fonts['base'],
|
||||
paddingVertical: Fonts['xs'],
|
||||
fontSize: Fonts['sm'],
|
||||
lineHeight: Fonts['base'],
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
textAlignVertical: 'center',
|
||||
backgroundColor: Fonts['bgInput'],
|
||||
color: Fonts['textSecondary'],
|
||||
fontFamily: Fonts['inter'],
|
||||
paddingRight: Fonts['5xl'],
|
||||
backgroundColor: '#FFF8DE'
|
||||
},
|
||||
passwordInputContainer: {
|
||||
position: 'relative',
|
||||
@ -195,8 +191,8 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 24,
|
||||
},
|
||||
forgotPasswordText: {
|
||||
color: '#AC7E35',
|
||||
fontSize: 11,
|
||||
color: '#1F2937',
|
||||
fontSize: 14,
|
||||
},
|
||||
loginButton: {
|
||||
width: '100%',
|
||||
@ -209,7 +205,6 @@ const styles = StyleSheet.create({
|
||||
loginButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '600',
|
||||
fontSize: 18,
|
||||
},
|
||||
signupContainer: {
|
||||
flexDirection: 'row',
|
||||
@ -217,15 +212,14 @@ const styles = StyleSheet.create({
|
||||
marginTop: 8,
|
||||
},
|
||||
signupText: {
|
||||
color: '#AC7E35',
|
||||
fontSize: Fonts['sm'],
|
||||
color: '#1F2937',
|
||||
fontSize: 14,
|
||||
},
|
||||
signupLink: {
|
||||
color: '#E2793F',
|
||||
fontSize: Fonts['sm'],
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginLeft: 4,
|
||||
textDecorationLine: 'underline',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { fetchApi } from "@/lib/server-api-util";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { ActivityIndicator, TextInput, TouchableOpacity, View } from "react-native";
|
||||
import { ThemedText } from "../ThemedText";
|
||||
import { Steps } from "./phoneLogin";
|
||||
import Button from "./ui/Button";
|
||||
import TextInput from "./ui/TextInput";
|
||||
|
||||
interface LoginProps {
|
||||
setSteps: (steps: Steps) => void;
|
||||
@ -18,30 +18,67 @@ const Phone = ({ setSteps, setPhone, phone, updateUrlParam }: LoginProps) => {
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const sendVerificationCode = async () => {
|
||||
setSteps('code')
|
||||
updateUrlParam("status", "code");
|
||||
return
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
setError(t("auth.telLogin.phoneInvalid", { ns: 'login' }));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
await fetchApi(`/iam/veritification-code`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ phone: phone }),
|
||||
})
|
||||
setSteps('code')
|
||||
updateUrlParam("status", "code");
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setPhone("")
|
||||
setIsLoading(false);
|
||||
// console.error(t("auth.telLogin.sendCodeError", { ns: 'login' }), error);
|
||||
}
|
||||
};
|
||||
|
||||
return <View>
|
||||
{/* 手机号输入框 */}
|
||||
<View className="mb-5">
|
||||
<View className="w-full flex flex-row justify-between">
|
||||
<ThemedText className="text-base !text-textPrimary mb-2 ml-2">
|
||||
{t('auth.telLogin.title', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<ThemedText className="text-sm !text-textPrimary mb-2 ml-2">
|
||||
{error}
|
||||
</ThemedText>
|
||||
</View>
|
||||
<TextInput
|
||||
label={t('auth.telLogin.title', { ns: 'login' })}
|
||||
className="border border-gray-300 rounded-2xl p-3 text-base bg-inputBackground"
|
||||
placeholder={t('auth.telLogin.phoneRequired', { ns: 'login' })}
|
||||
placeholderTextColor="#ccc"
|
||||
value={phone}
|
||||
onChangeText={(text) => {
|
||||
setPhone(text);
|
||||
setError('');
|
||||
}}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
value={phone}
|
||||
error={error}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 发送验证码 */}
|
||||
<Button isLoading={isLoading} handleLogin={sendVerificationCode} text={t('auth.telLogin.sendCode', { ns: 'login' })} />
|
||||
<TouchableOpacity
|
||||
className={`w-full bg-[#E2793F] rounded-full text-[#fff] p-4 items-center mb-6 ${isLoading ? 'opacity-70' : ''}`}
|
||||
onPress={sendVerificationCode}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<ThemedText className="!text-white font-semibold">
|
||||
{t('auth.telLogin.sendCode', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
|
||||
|
||||
@ -15,9 +15,7 @@ const PhoneLogin = ({ updateUrlParam }: LoginProps) => {
|
||||
|
||||
return <View>
|
||||
{
|
||||
steps === "phone"
|
||||
? <Phone setSteps={setSteps} setPhone={setPhone} phone={phone} updateUrlParam={updateUrlParam} />
|
||||
: <Code phone={phone} />
|
||||
steps === "phone" ? <Phone setSteps={setSteps} setPhone={setPhone} phone={phone} updateUrlParam={updateUrlParam} /> : <Code phone={phone} />
|
||||
}
|
||||
</View>
|
||||
}
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
import { Fonts } from "@/constants/Fonts";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { ActivityIndicator, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import { useAuth } from "../../contexts/auth-context";
|
||||
import { fetchApi } from "../../lib/server-api-util";
|
||||
import { User } from "../../types/user";
|
||||
import { ThemedText } from "../ThemedText";
|
||||
import PrivacyModal from "../owner/qualification/privacy";
|
||||
import Button from "./ui/Button";
|
||||
import TextInput from "./ui/TextInput";
|
||||
|
||||
interface LoginProps {
|
||||
updateUrlParam: (status: string, value: string) => void;
|
||||
@ -149,53 +146,100 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword, setSh
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* 邮箱输入 */}
|
||||
<TextInput
|
||||
label={t('auth.login.email', { ns: 'login' })}
|
||||
placeholder={t('auth.login.accountPlaceholder', { ns: 'login' })}
|
||||
onChangeText={(text) => {
|
||||
setEmail(text);
|
||||
setError('123');
|
||||
}}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
value={email}
|
||||
/>
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.inputLabel}>
|
||||
{t('auth.login.email', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<View style={styles.inputWrapper}>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
placeholder={t('auth.login.accountPlaceholder', { ns: 'login' })}
|
||||
placeholderTextColor="#ccc"
|
||||
value={email}
|
||||
onChangeText={(value) => {
|
||||
setEmail(value)
|
||||
setError('123')
|
||||
}}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 密码输入 */}
|
||||
<TextInput
|
||||
label={t('auth.login.password', { ns: 'login' })}
|
||||
placeholder={t('auth.login.passwordPlaceholder', { ns: 'login' })}
|
||||
autoCapitalize="none"
|
||||
value={password}
|
||||
onChangeText={(value) => {
|
||||
handlePasswordChange(value)
|
||||
setError('123')
|
||||
}}
|
||||
secureTextEntry={!showPassword}
|
||||
type="password"
|
||||
setShowPassword={setShowPassword}
|
||||
showPassword={showPassword}
|
||||
/>
|
||||
<View style={styles.inputContainer}>
|
||||
<ThemedText style={styles.inputLabel}>
|
||||
{t('auth.login.password', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<View style={styles.passwordInputContainer}>
|
||||
<TextInput
|
||||
style={[styles.textInput, { flex: 1 }]}
|
||||
placeholder={t('auth.login.passwordPlaceholder', { ns: 'login' })}
|
||||
placeholderTextColor="#ccc"
|
||||
value={password}
|
||||
onChangeText={(value) => {
|
||||
handlePasswordChange(value)
|
||||
setError('123')
|
||||
}}
|
||||
secureTextEntry={!showPassword}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowPassword(!showPassword)}
|
||||
style={styles.eyeIcon}
|
||||
>
|
||||
<Ionicons
|
||||
name={showPassword ? 'eye' : 'eye-off'}
|
||||
size={20}
|
||||
color="#666"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 确认密码 */}
|
||||
<TextInput
|
||||
label={t('auth.signup.confirmPassword', { ns: 'login' })}
|
||||
placeholder={t('auth.signup.confirmPasswordPlaceholder', { ns: 'login' })}
|
||||
autoCapitalize="none"
|
||||
value={confirmPassword}
|
||||
onChangeText={(value) => {
|
||||
handleConfirmPasswordChange(value)
|
||||
setError('123')
|
||||
}}
|
||||
secureTextEntry={!showSecondPassword}
|
||||
type="password"
|
||||
setShowPassword={setShowSecondPassword}
|
||||
showPassword={showSecondPassword}
|
||||
/>
|
||||
<View style={[styles.inputContainer, { marginBottom: 24 }]}>
|
||||
<ThemedText style={styles.inputLabel}>
|
||||
{t('auth.signup.confirmPassword', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<View style={styles.passwordInputContainer}>
|
||||
<TextInput
|
||||
style={[styles.textInput, { flex: 1 }]}
|
||||
placeholder={t('auth.signup.confirmPasswordPlaceholder', { ns: 'login' })}
|
||||
placeholderTextColor="#ccc"
|
||||
value={confirmPassword}
|
||||
onChangeText={(value) => {
|
||||
handleConfirmPasswordChange(value)
|
||||
setError('123')
|
||||
}}
|
||||
secureTextEntry={!showSecondPassword}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowSecondPassword(!showSecondPassword)}
|
||||
style={styles.eyeIcon}
|
||||
>
|
||||
<Ionicons
|
||||
name={showSecondPassword ? 'eye' : 'eye-off'}
|
||||
size={20}
|
||||
color="#666"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 注册按钮 */}
|
||||
<Button isLoading={loading} handleLogin={handleSubmit} text={t("auth.signup.signupButton", { ns: 'login' })} />
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.signupButton, loading && { opacity: 0.7 }]}
|
||||
onPress={handleSubmit}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<ThemedText style={styles.signupButtonText}>
|
||||
{t("auth.signup.signupButton", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<View style={styles.termsContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
@ -215,68 +259,68 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword, setSh
|
||||
]}
|
||||
>
|
||||
{checked && (
|
||||
<Ionicons name="checkmark" size={14} color={Fonts['textSecondary']} />
|
||||
<Ionicons name="checkmark" size={14} color="white" />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<View style={styles.termsTextContainer}>
|
||||
<ThemedText style={styles.termsText} type="sfPro">
|
||||
<ThemedText style={styles.termsText}>
|
||||
{t("auth.telLogin.agree", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={() => {
|
||||
setModalType('terms');
|
||||
setPrivacyModalVisible(true);
|
||||
}}>
|
||||
<ThemedText style={styles.termsLink} type="sfPro">
|
||||
<ThemedText style={styles.termsLink}>
|
||||
{t("auth.telLogin.terms", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<ThemedText style={styles.termsText} type="sfPro">
|
||||
<ThemedText style={styles.termsText}>
|
||||
{t("auth.telLogin.and", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={() => {
|
||||
setModalType('privacy');
|
||||
setPrivacyModalVisible(true);
|
||||
}}>
|
||||
<ThemedText style={styles.termsLink} type="sfPro">
|
||||
<ThemedText style={styles.termsLink}>
|
||||
{t("auth.telLogin.privacyPolicy", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<ThemedText style={styles.termsText} type="sfPro">
|
||||
<ThemedText style={styles.termsText}>
|
||||
{t("auth.telLogin.and", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={() => {
|
||||
setModalType('user');
|
||||
setPrivacyModalVisible(true);
|
||||
}}>
|
||||
<ThemedText style={styles.termsLink} type="sfPro">
|
||||
<ThemedText style={styles.termsLink}>
|
||||
{t("auth.telLogin.userAgreement", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<ThemedText style={styles.termsText} type="sfPro">
|
||||
<ThemedText style={styles.termsText}>
|
||||
{t("auth.telLogin.and", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity onPress={() => {
|
||||
setModalType('ai');
|
||||
setPrivacyModalVisible(true);
|
||||
}}>
|
||||
<ThemedText style={styles.termsLink} type="sfPro">
|
||||
<ThemedText style={styles.termsLink}>
|
||||
{t("auth.telLogin.aiAgreement", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<ThemedText style={styles.termsText} type="sfPro">
|
||||
<ThemedText style={styles.termsText}>
|
||||
{t("auth.telLogin.agreement", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.termsLink} type="sfPro">
|
||||
<ThemedText style={styles.termsLink}>
|
||||
{t("common.name")}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.termsText} type="sfPro">
|
||||
<ThemedText style={styles.termsText}>
|
||||
{t("auth.telLogin.getPhone", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
{/* 已有账号 */}
|
||||
<View style={styles.loginContainer} >
|
||||
<ThemedText type="sfPro" color="textPrimary" size="sm">
|
||||
<View style={styles.loginContainer}>
|
||||
<ThemedText style={styles.loginText}>
|
||||
{t("auth.signup.haveAccount", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<TouchableOpacity
|
||||
@ -284,7 +328,7 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword, setSh
|
||||
updateUrlParam("status", "login");
|
||||
}}
|
||||
>
|
||||
<ThemedText type="sfPro" color="bgSecondary" weight="bold" size="sm">
|
||||
<ThemedText style={styles.loginLink}>
|
||||
{t("auth.signup.login", { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
@ -309,9 +353,8 @@ const styles = StyleSheet.create({
|
||||
overflow: 'hidden',
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: 14,
|
||||
color: '#AC7E35',
|
||||
fontWeight: '600',
|
||||
fontSize: 16,
|
||||
color: '#1F2937',
|
||||
marginBottom: 8,
|
||||
marginLeft: 8,
|
||||
},
|
||||
@ -351,17 +394,17 @@ const styles = StyleSheet.create({
|
||||
checkbox: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 6,
|
||||
borderWidth: 1,
|
||||
borderColor: Fonts['textPrimary'],
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
borderColor: '#E5E7EB',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 8,
|
||||
marginTop: 2,
|
||||
},
|
||||
checkboxChecked: {
|
||||
backgroundColor: Fonts["bgCheck"],
|
||||
borderColor: Fonts['bgCheck'],
|
||||
backgroundColor: '#E2793F',
|
||||
borderColor: '#E2793F',
|
||||
},
|
||||
termsTextContainer: {
|
||||
flexDirection: 'row',
|
||||
@ -370,20 +413,29 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
termsText: {
|
||||
fontSize: 14,
|
||||
color: Fonts["textPrimary"],
|
||||
color: '#1F2937',
|
||||
lineHeight: 20,
|
||||
},
|
||||
termsLink: {
|
||||
fontSize: 14,
|
||||
color: Fonts['bgSecondary'],
|
||||
lineHeight: 20
|
||||
color: '#E2793F',
|
||||
lineHeight: 20,
|
||||
},
|
||||
loginContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
marginTop: 24,
|
||||
gap: 4,
|
||||
}
|
||||
},
|
||||
loginText: {
|
||||
fontSize: 14,
|
||||
color: '#1F2937',
|
||||
},
|
||||
loginLink: {
|
||||
color: '#E2793F',
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginLeft: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export default SignUp;
|
||||
@ -1,37 +0,0 @@
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { ActivityIndicator, StyleSheet, TouchableOpacity, ViewStyle } from "react-native";
|
||||
|
||||
interface ButtonProps {
|
||||
isLoading?: boolean;
|
||||
handleLogin?: () => void;
|
||||
text: string;
|
||||
containerStyle?: ViewStyle;
|
||||
}
|
||||
const Button = ({ isLoading, handleLogin, text, containerStyle }: ButtonProps) => {
|
||||
return <TouchableOpacity
|
||||
style={[styles.loginButton, isLoading && { opacity: 0.7 }, containerStyle]}
|
||||
onPress={handleLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<ThemedText type="sfPro" size="lg" weight="bold" color="textWhite">
|
||||
{text}
|
||||
</ThemedText>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
loginButton: {
|
||||
width: '100%',
|
||||
backgroundColor: '#E2793F',
|
||||
borderRadius: 28,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
marginBottom: 24,
|
||||
},
|
||||
})
|
||||
|
||||
export default Button
|
||||
@ -1,122 +0,0 @@
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { Fonts } from "@/constants/Fonts";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { TextInput as RNTextInput, TextInputProps as RNTextInputProps, StyleProp, StyleSheet, TextStyle, TouchableOpacity, View, ViewStyle } from "react-native";
|
||||
|
||||
interface CustomTextInputProps {
|
||||
label: string;
|
||||
placeholder: string;
|
||||
value: string;
|
||||
onChangeText: (text: string) => void;
|
||||
showPassword?: boolean;
|
||||
setShowPassword?: (showPassword: boolean) => void;
|
||||
setError?: (error: string) => void;
|
||||
type?: 'default' | 'password';
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
style?: StyleProp<TextStyle>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
type TextInputProps = RNTextInputProps & CustomTextInputProps;
|
||||
|
||||
const TextInput = ({
|
||||
type = 'default',
|
||||
label,
|
||||
placeholder,
|
||||
value,
|
||||
onChangeText,
|
||||
setError,
|
||||
showPassword,
|
||||
setShowPassword,
|
||||
error,
|
||||
style,
|
||||
containerStyle,
|
||||
...props
|
||||
}: TextInputProps) => {
|
||||
|
||||
return (
|
||||
<View style={[styles.inputContainer, containerStyle]}>
|
||||
<View className="w-full flex flex-row justify-between">
|
||||
<ThemedText style={styles.inputLabel}>
|
||||
{label}
|
||||
</ThemedText>
|
||||
{
|
||||
error &&
|
||||
<ThemedText color="bgSecondary" size="xxs">
|
||||
{error}
|
||||
</ThemedText>
|
||||
}
|
||||
</View>
|
||||
|
||||
<View style={styles.inputTextContainer}>
|
||||
<RNTextInput
|
||||
style={[styles.textInput, style]}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={Fonts['placeholderTextColor']}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
secureTextEntry={type === 'password' ? !showPassword : undefined}
|
||||
{...props}
|
||||
/>
|
||||
{
|
||||
type === 'password' &&
|
||||
<TouchableOpacity
|
||||
style={styles.eyeIcon}
|
||||
onPress={() => {
|
||||
if (setShowPassword) {
|
||||
setShowPassword(!showPassword);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={showPassword ? 'eye' : 'eye-off'}
|
||||
size={20}
|
||||
color="#666"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
inputContainer: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: Fonts['sm'],
|
||||
color: Fonts['textPrimary'],
|
||||
fontWeight: Fonts['bold'],
|
||||
fontFamily: Fonts['sfPro'],
|
||||
marginBottom: 8,
|
||||
marginLeft: 8,
|
||||
},
|
||||
textInput: {
|
||||
borderRadius: Fonts['xs'],
|
||||
paddingHorizontal: Fonts['base'],
|
||||
paddingVertical: Fonts['sm'],
|
||||
fontSize: Fonts['sm'],
|
||||
lineHeight: Fonts['base'],
|
||||
textAlignVertical: 'center',
|
||||
backgroundColor: Fonts['bgInput'],
|
||||
color: Fonts['textSecondary'],
|
||||
fontFamily: Fonts['inter'],
|
||||
paddingRight: 48, // Make space for the eye icon
|
||||
},
|
||||
inputTextContainer: {
|
||||
position: 'relative',
|
||||
},
|
||||
eyeIcon: {
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
top: '50%',
|
||||
transform: [{ translateY: -10 }], // Half of the icon's height (20/2 = 10)
|
||||
height: 20,
|
||||
width: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
export default TextInput;
|
||||
@ -13,10 +13,10 @@ const AlbumComponent = ({ style }: CategoryProps) => {
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
<TouchableOpacity style={{ flex: 3, opacity: 0 }}>
|
||||
<TouchableOpacity style={{ flex: 3 }}>
|
||||
<ThemedText style={styles.text}>{t('generalSetting.album', { ns: 'personal' })}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={{ flex: 3, opacity: 0 }}>
|
||||
<TouchableOpacity style={{ flex: 3 }}>
|
||||
<ThemedText style={styles.text}>{t('generalSetting.shareProfile', { ns: 'personal' })}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
|
||||
@ -22,7 +22,6 @@ const width = Dimensions.get("window").width;
|
||||
|
||||
function CarouselComponent(props: Props) {
|
||||
const { data } = props;
|
||||
const [currentIndex, setCurrentIndex] = React.useState(0);
|
||||
const [carouselDataValue, setCarouselDataValue] = React.useState<CarouselData[]>([]);
|
||||
const dataHandle = () => {
|
||||
const carouselData = { ...data?.category_count, total_count: data?.total_count }
|
||||
@ -45,7 +44,7 @@ function CarouselComponent(props: Props) {
|
||||
}
|
||||
|
||||
const totleItem = (data: UserCountData) => {
|
||||
return <View style={[styles.container, { width: width * 0.7 }]}>
|
||||
return <View style={styles.container}>
|
||||
{Object?.entries(data)?.filter(([key]) => key !== 'cover_url')?.map((item, index) => (
|
||||
<View style={styles.item} key={index}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8, width: "75%", overflow: 'hidden' }}>
|
||||
@ -69,41 +68,37 @@ function CarouselComponent(props: Props) {
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1
|
||||
}}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Carousel
|
||||
width={width}
|
||||
height={width}
|
||||
height={width * 0.75}
|
||||
data={carouselDataValue || []}
|
||||
mode="parallax"
|
||||
onSnapToItem={(index) => setCurrentIndex(index)}
|
||||
defaultIndex={
|
||||
carouselDataValue?.length
|
||||
? Math.max(0, Math.min(
|
||||
carouselDataValue.length - 1,
|
||||
carouselDataValue.findIndex((item) => item?.key === 'total_count') - 1
|
||||
))
|
||||
: 0
|
||||
}
|
||||
// defaultIndex={
|
||||
// carouselDataValue?.length
|
||||
// ? Math.max(0, Math.min(
|
||||
// carouselDataValue.length - 1,
|
||||
// carouselDataValue.findIndex((item) => item?.key === 'total_count') - 1
|
||||
// ))
|
||||
// : 0
|
||||
// }
|
||||
modeConfig={{
|
||||
parallaxScrollingScale: 1,
|
||||
parallaxScrollingOffset: 130,
|
||||
parallaxScrollingOffset: 150,
|
||||
parallaxAdjacentItemScale: 0.7
|
||||
}}
|
||||
renderItem={({ item, index }) => {
|
||||
const style: ViewStyle = {
|
||||
width: width,
|
||||
height: width * 0.7,
|
||||
alignItems: "center"
|
||||
height: width * 0.8,
|
||||
alignItems: "center",
|
||||
};
|
||||
return (
|
||||
<View key={index} style={[style]}>
|
||||
<View key={index} style={style}>
|
||||
{item?.key === 'total_count' ? (
|
||||
totleItem(item.value)
|
||||
) : (
|
||||
<View>
|
||||
<View style={{ flex: 1, width: width * 0.65 }}>
|
||||
{CategoryComponent({
|
||||
title: item?.key,
|
||||
data: [
|
||||
@ -112,7 +107,6 @@ function CarouselComponent(props: Props) {
|
||||
{ title: 'Length', number: formatDuration(item?.value?.video_length || 0) }
|
||||
],
|
||||
bgSvg: item?.value?.cover_url,
|
||||
width: width
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
@ -130,13 +124,13 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 32,
|
||||
padding: 6
|
||||
padding: 4
|
||||
},
|
||||
container: {
|
||||
backgroundColor: "#FFB645",
|
||||
paddingVertical: 8,
|
||||
paddingLeft: 32,
|
||||
borderRadius: 32,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 16,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: 'relative',
|
||||
@ -163,10 +157,10 @@ const styles = StyleSheet.create({
|
||||
number: {
|
||||
color: "#fff",
|
||||
fontWeight: "700",
|
||||
fontSize: 28,
|
||||
lineHeight: 30,
|
||||
fontSize: 26,
|
||||
textAlign: 'left',
|
||||
flex: 1
|
||||
flex: 1,
|
||||
paddingTop: 8
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -6,45 +6,16 @@ import VideoTotalSvg from "@/assets/icons/svg/videoTotalWhite.svg";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { Image, StyleProp, StyleSheet, View, ViewStyle } from "react-native";
|
||||
import { ThemedText } from "../ThemedText";
|
||||
|
||||
interface CategoryProps {
|
||||
title: string;
|
||||
data: { title: string, number: { s: number, m: number, h: number } | number }[];
|
||||
data: { title: string, number: string | number }[];
|
||||
bgSvg: string | null;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
width: number;
|
||||
}
|
||||
|
||||
const TimeUnit = ({ value, unit }: { value: number; unit: string }) => (
|
||||
value > 0 && (
|
||||
<>
|
||||
<ThemedText style={styles.itemNumber}>{value}</ThemedText>
|
||||
<ThemedText style={[styles.itemNumber, { fontSize: 10 }]}>{unit}</ThemedText>
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
||||
const CategoryComponent = ({ title, data, bgSvg, style, width }: CategoryProps) => {
|
||||
const renderTimeDisplay = (time: { s: number; m: number; h: number }) => {
|
||||
const { h, m, s } = time;
|
||||
const showSeconds = s > 0 || (s === 0 && m === 0 && h === 0);
|
||||
|
||||
return (
|
||||
<ThemedText style={{ flexDirection: 'row', alignItems: 'flex-end', gap: 2 }}>
|
||||
<TimeUnit value={h} unit="h" />
|
||||
<TimeUnit value={m} unit="m" />
|
||||
{showSeconds && (
|
||||
<>
|
||||
<ThemedText style={styles.itemNumber}>{s}</ThemedText>
|
||||
<ThemedText style={[styles.itemNumber, { fontSize: 10 }]}>s</ThemedText>
|
||||
</>
|
||||
)}
|
||||
</ThemedText>
|
||||
);
|
||||
};
|
||||
|
||||
const CategoryComponent = ({ title, data, bgSvg, style }: CategoryProps) => {
|
||||
return (
|
||||
<View style={[styles.container, style, { width: width * 0.73 }]}>
|
||||
<View style={[styles.container, style]}>
|
||||
<View style={styles.backgroundContainer}>
|
||||
<Image
|
||||
source={bgSvg !== "" && bgSvg !== null ? { uri: bgSvg } : require('@/assets/images/png/owner/people.png')}
|
||||
@ -67,19 +38,7 @@ const CategoryComponent = ({ title, data, bgSvg, style, width }: CategoryProps)
|
||||
</View>
|
||||
<ThemedText style={styles.itemTitle}>{item.title}</ThemedText>
|
||||
</View>
|
||||
<View style={{ alignSelf: 'flex-start', flex: 1 }}>
|
||||
{item?.title === "Length" ? (
|
||||
typeof item.number === 'object' ? (
|
||||
renderTimeDisplay(item.number)
|
||||
) : (
|
||||
<ThemedText style={[styles.itemNumber]}>{item.number}</ThemedText>
|
||||
)
|
||||
) : (
|
||||
<ThemedText style={[styles.itemNumber]}>
|
||||
{typeof item.number === 'number' ? item.number : 0}
|
||||
</ThemedText>
|
||||
)}
|
||||
</View>
|
||||
<ThemedText style={styles.itemNumber}>{item.number}</ThemedText>
|
||||
</View>
|
||||
))}
|
||||
<View style={styles.titleContent}>
|
||||
@ -109,8 +68,7 @@ const styles = StyleSheet.create({
|
||||
backdropFilter: 'blur(5px)',
|
||||
},
|
||||
content: {
|
||||
padding: 32,
|
||||
paddingRight: 16,
|
||||
padding: 16,
|
||||
justifyContent: "space-between",
|
||||
flex: 1
|
||||
},
|
||||
@ -120,7 +78,6 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
paddingVertical: 4
|
||||
},
|
||||
title: {
|
||||
color: 'white',
|
||||
@ -138,7 +95,7 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 16,
|
||||
paddingVertical: 8,
|
||||
width: '100%',
|
||||
},
|
||||
itemTitle: {
|
||||
@ -151,10 +108,11 @@ const styles = StyleSheet.create({
|
||||
itemNumber: {
|
||||
color: 'white',
|
||||
fontSize: 28,
|
||||
lineHeight: 30,
|
||||
fontWeight: '700',
|
||||
textAlign: 'left',
|
||||
marginLeft: 8,
|
||||
flex: 1,
|
||||
paddingTop: 8
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import Modal from 'react-native-modal';
|
||||
import Cascader, { CascaderItem } from '../cascader';
|
||||
import { ThemedText } from '../ThemedText';
|
||||
|
||||
interface ClassifyModalProps {
|
||||
modalVisible: boolean;
|
||||
@ -40,9 +39,9 @@ const ClassifyModal = (props: ClassifyModalProps) => {
|
||||
<View style={styles.modalView}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={{ opacity: 0 }}>Settings</Text>
|
||||
<ThemedText style={styles.modalTitle}>{t('generalSetting.classify', { ns: 'personal' })}</ThemedText>
|
||||
<Text style={styles.modalTitle}>{t('generalSetting.classify', { ns: 'personal' })}</Text>
|
||||
<TouchableOpacity onPress={() => setModalVisible(false)}>
|
||||
<ThemedText style={styles.closeButton}>×</ThemedText>
|
||||
<Text style={styles.closeButton}>×</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
|
||||
|
||||
@ -19,11 +19,13 @@ const CreateCountComponent = React.memo((props: CreateCountProps) => {
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<ThemedText style={styles.number} className="!text-textSecondary">{props.number}</ThemedText>
|
||||
<View style={styles.header}>
|
||||
<ThemedText type="sfPro" weight="bold" color="textSecondary" size="xxs" style={{ lineHeight: 16 }}>{props.title}</ThemedText>
|
||||
{props.icon}
|
||||
<View className="mt-1">
|
||||
{props.icon}
|
||||
</View>
|
||||
<ThemedText style={styles.title} className="!text-textSecondary">{props.title}</ThemedText>
|
||||
</View>
|
||||
<ThemedText weight="bold" color="textSecondary" size="title" style={{ lineHeight: 36, textAlign: "right" }}>{props.number}</ThemedText>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
@ -33,12 +35,11 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
// alignItems: "center",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
backgroundColor: "#FAF9F6",
|
||||
paddingVertical: 16,
|
||||
paddingRight: 16,
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
@ -50,12 +51,12 @@ const styles = StyleSheet.create({
|
||||
// elevation: 1,
|
||||
},
|
||||
header: {
|
||||
width: "90%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: 16,
|
||||
alignItems: "flex-start",
|
||||
gap: 4,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 8,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
|
||||
@ -36,7 +36,6 @@ const PodiumComponent = ({ data }: IPodium) => {
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={styles.title}
|
||||
type="inter"
|
||||
>
|
||||
{data[1]?.user_nick_name}
|
||||
</ThemedText>
|
||||
@ -66,7 +65,6 @@ const PodiumComponent = ({ data }: IPodium) => {
|
||||
numberOfLines={2}
|
||||
ellipsizeMode="tail"
|
||||
style={styles.title}
|
||||
type="inter"
|
||||
>
|
||||
{data[0]?.user_nick_name}
|
||||
</ThemedText>
|
||||
@ -96,7 +94,6 @@ const PodiumComponent = ({ data }: IPodium) => {
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
style={styles.title}
|
||||
type="inter"
|
||||
>
|
||||
{data[2]?.user_nick_name}
|
||||
</ThemedText>
|
||||
|
||||
@ -43,9 +43,10 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
modalView: {
|
||||
width: '100%',
|
||||
height: '50%',
|
||||
height: '40%',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 26,
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
|
||||
@ -126,8 +126,8 @@ const styles = StyleSheet.create({
|
||||
width: '100%',
|
||||
height: '80%',
|
||||
backgroundColor: 'white',
|
||||
borderTopLeftRadius: 26,
|
||||
borderTopRightRadius: 26,
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
modalHeader: {
|
||||
|
||||
@ -15,9 +15,9 @@ const RankList = (props: IRankList) => {
|
||||
contentContainerStyle={{ flexGrow: 1, paddingHorizontal: 16, marginTop: 16 }}
|
||||
>
|
||||
<View style={styles.item}>
|
||||
<ThemedText style={styles.headerText} type="sfPro">Rank</ThemedText>
|
||||
<ThemedText style={styles.headerText} type="sfPro">Username</ThemedText>
|
||||
<ThemedText style={styles.headerText} type="sfPro">Profile</ThemedText>
|
||||
<ThemedText style={styles.headerText}>Rank</ThemedText>
|
||||
<ThemedText style={styles.headerText}>Username</ThemedText>
|
||||
<ThemedText style={styles.headerText}>Profile</ThemedText>
|
||||
</View>
|
||||
{props.data?.filter((item, index) => index > 2).map((item, index) => (
|
||||
<View
|
||||
@ -35,8 +35,8 @@ const RankList = (props: IRankList) => {
|
||||
<OwnerSvg width="100%" height="100%" />
|
||||
</View>
|
||||
)}
|
||||
<ThemedText style={styles.itemRank} type="sfPro">{index + 1}</ThemedText>
|
||||
<ThemedText style={styles.itemName} type="inter">{item.user_nick_name}</ThemedText>
|
||||
<ThemedText style={styles.itemRank}>{index + 1}</ThemedText>
|
||||
<ThemedText style={styles.itemName}>{item.user_nick_name}</ThemedText>
|
||||
<View style={{ opacity: index == 1 ? 0 : 1 }}>
|
||||
{(() => {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
@ -66,9 +66,9 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
headerText: {
|
||||
fontSize: 12,
|
||||
fontSize: 14,
|
||||
color: "#4C320C",
|
||||
fontWeight: "500"
|
||||
fontWeight: "600"
|
||||
},
|
||||
item: {
|
||||
flexDirection: 'row',
|
||||
@ -85,7 +85,7 @@ const styles = StyleSheet.create({
|
||||
fontWeight: "700"
|
||||
},
|
||||
itemName: {
|
||||
fontSize: 12,
|
||||
fontSize: 14,
|
||||
color: "#AC7E35",
|
||||
},
|
||||
self: {
|
||||
|
||||
@ -1,54 +0,0 @@
|
||||
import Svg, { Defs, LinearGradient, Path, Rect, Stop, Text } from 'react-native-svg';
|
||||
|
||||
const CardBg = (prop: { pro: string, date: string }) => {
|
||||
const { pro, date } = prop;
|
||||
return (
|
||||
<Svg width="376" height="124" viewBox="0 44.9 376 124.1" fill="none">
|
||||
{pro === "pro" && <Path d="M286.349 53.467L292.529 44.9033H320.021L311.337 51.2785L284.133 56.0518L286.349 53.467Z" fill="#E0A241" />}
|
||||
{pro === "pro" && <Path d="M367.546 122.188L363.914 116.368C363.798 116.183 363.714 115.98 363.664 115.768L362.749 111.887C362.722 111.77 362.684 111.657 362.636 111.547L362.079 110.266C361.211 108.273 363.698 106.522 365.282 108.01L375.138 117.274L370.767 122.424C369.891 123.457 368.263 123.338 367.546 122.188Z" fill="#E0A241" />}
|
||||
<Rect x="1.17188" y="48.8311" width="370" height="120" rx="34" fill="#4C320C" />
|
||||
<Path opacity="0.24" d="M1 118.031C1.66058 99.3464 16.3302 58.9993 68.3782 57.5584C116.841 56.2168 170.195 82.5409 173.692 84.0733C186.432 89.6553 254.376 122.217 310.713 107.564C355.783 95.8421 366.107 67.6372 365.635 55M1 128.238C31.3602 124.142 111.645 120.135 189.904 136.872C268.164 153.61 343.243 153.145 371 150.821" stroke="white" stroke-width="1.5" />
|
||||
<Path d="M175.398 56.0518C175.398 56.0518 175.916 62.7711 177.936 64.7908C179.956 66.8105 184.795 67.329 184.795 67.329C184.795 67.329 179.956 67.8475 177.936 69.8672C175.916 71.8869 175.398 74.8472 175.398 74.8472C175.398 74.8472 174.879 71.8869 172.859 69.8672C170.84 67.8475 166 67.329 166 67.329C166 67.329 170.84 66.8105 172.859 64.7908C174.879 62.7711 175.398 56.0518 175.398 56.0518Z" fill="#FFB645" />
|
||||
<Path d="M149.929 54.0527C149.929 54.0527 150.816 65.5433 154.27 68.9971C157.724 72.451 166 73.3377 166 73.3377C166 73.3377 157.724 74.2244 154.27 77.6783C150.816 81.1321 149.929 86.1943 149.929 86.1943C149.929 86.1943 149.042 81.1321 145.589 77.6783C142.135 74.2244 133.858 73.3377 133.858 73.3377C133.858 73.3377 142.135 72.451 145.589 68.9971C149.042 65.5433 149.929 54.0527 149.929 54.0527Z" fill="#FFB645" />
|
||||
<Path d="M181.214 75.7812C181.214 75.7812 181.92 84.9229 184.668 87.6707C187.415 90.4186 194 91.124 194 91.124C194 91.124 187.415 91.8295 184.668 94.5773C181.92 97.3251 181.214 101.353 181.214 101.353C181.214 101.353 180.509 97.3251 177.761 94.5773C175.013 91.8295 168.429 91.124 168.429 91.124C168.429 91.124 175.013 90.4186 177.761 87.6707C180.509 84.9229 181.214 75.7812 181.214 75.7812Z" fill="#FFB645" />
|
||||
{/* 背景色块 */}
|
||||
{pro === "pro" && <Path d="M292.495 44.8398H319.859L375.141 93.3921V117.295L292.495 44.8398Z" fill="#FFC56A" />}
|
||||
|
||||
{/* 叠加文字 */}
|
||||
{
|
||||
pro === "pro" && <Text
|
||||
x={340}
|
||||
y={75}
|
||||
dy={3.5}
|
||||
textAnchor="middle"
|
||||
fontSize="11"
|
||||
fontWeight="600"
|
||||
fill="#4C320C"
|
||||
fontFamily="sans-serif"
|
||||
transform={`rotate(40.4 ${340} ${75})`}
|
||||
>
|
||||
{date}
|
||||
</Text>
|
||||
}
|
||||
|
||||
{/* 渐变定义(放在最后,避免覆盖) */}
|
||||
<Defs>
|
||||
<LinearGradient id="paint0_linear_0_1" x1="26" y1="72.5" x2="106.717" y2="129.81" gradientUnits="userSpaceOnUse">
|
||||
<Stop stop-color="white" />
|
||||
<Stop offset="1" stop-color="#FFE064" />
|
||||
</LinearGradient>
|
||||
<LinearGradient id="paint1_linear_0_1" x1="30.4844" y1="134.396" x2="141.612" y2="134.396" gradientUnits="userSpaceOnUse">
|
||||
<Stop stop-color="#D0BFB0" />
|
||||
<Stop offset="0.405582" stop-color="#FFE57D" />
|
||||
<Stop offset="1" stop-color="white" />
|
||||
</LinearGradient>
|
||||
<LinearGradient id="paint2_linear_0_1" x1="100.511" y1="78.9371" x2="164.522" y2="104.117" gradientUnits="userSpaceOnUse">
|
||||
<Stop stop-color="#FFF3E8" />
|
||||
<Stop offset="1" stop-color="#FFFAB9" />
|
||||
</LinearGradient>
|
||||
</Defs>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default CardBg
|
||||
@ -1,68 +0,0 @@
|
||||
import Svg, { Circle, Defs, Ellipse, FeBlend, FeColorMatrix, FeComposite, FeFlood, FeGaussianBlur, FeOffset, Filter, G, Path, Rect } from 'react-native-svg';
|
||||
|
||||
const IpSvg = (prop: { pro: string }) => {
|
||||
const { pro } = prop;
|
||||
return (
|
||||
<Svg width="180" height="110" viewBox="0 0 200 140" fill="none">
|
||||
<G mask="url(#mask0_3223_1170)">
|
||||
{pro === "pro" && <G filter="url(#filter0_i_3223_1170)">
|
||||
<Path d="M61.4295 56.6599C60.8472 52.3476 66.5965 50.3378 68.8277 54.0737L71.3125 58.2342C73.0132 61.082 77.2417 60.7415 78.4648 57.6582L88.4108 32.5851C89.7463 29.2183 94.5115 29.2183 95.8471 32.5851L105.793 57.6582C107.016 60.7415 111.245 61.082 112.945 58.2342L115.43 54.0737C117.661 50.3378 123.411 52.3476 122.828 56.6599L120.386 74.7507C120.118 76.735 118.424 78.2155 116.422 78.2155H67.8363C65.8341 78.2155 64.1402 76.735 63.8723 74.7507L61.4295 56.6599Z" fill="#FFE88A" />
|
||||
</G>}
|
||||
{pro === "pro" && <Circle cx="59.1272" cy="39.4803" r="9.12722" fill="#FFB645" stroke="#4C320C" stroke-width="4" />}
|
||||
{pro === "pro" && <Circle cx="92.2844" cy="22.1272" r="9.12722" fill="#E2793F" stroke="#4C320C" stroke-width="4" />}
|
||||
{pro === "pro" && <Circle cx="125.441" cy="39.4803" r="9.12722" fill="#FFB645" stroke="#4C320C" stroke-width="4" />}
|
||||
<Path d="M49.5755 72.4144C50.4475 69.5036 57.1744 72.1692 60.4288 73.8658L52.5625 77.8182C50.6468 77.7604 48.7034 75.3251 49.5755 72.4144Z" fill="#FFDBA3" />
|
||||
<Path d="M51.0981 72.8437C51.5796 70.3845 56.005 72.8494 58.1575 74.3893L54.2174 76.2282C52.977 76.1247 50.6165 75.3029 51.0981 72.8437Z" fill="#AC7E35" />
|
||||
<Path d="M133.568 71.0186C132.303 68.2559 126.008 71.8237 123.019 73.9529L131.355 76.7827C133.244 76.4612 134.833 73.7813 133.568 71.0186Z" fill="#FFDBA3" />
|
||||
<Path d="M132.118 71.6536C131.302 69.2843 127.259 72.3359 125.339 74.1579L129.495 75.436C130.709 75.1624 132.934 74.0229 132.118 71.6536Z" fill="#AC7E35" />
|
||||
<Path d="M27.1969 103.974C56.4427 53.319 129.557 53.3191 158.803 103.974L178.751 138.526C207.997 189.181 171.44 252.5 112.948 252.5H73.0518C14.5601 252.5 -21.9971 189.181 7.24869 138.526L27.1969 103.974Z" fill="#FFD18A" />
|
||||
<Rect x="88.5132" y="100.684" width="2.99145" height="4.18803" rx="1.49573" transform="rotate(-180 88.5132 100.684)" fill="#4C320C" />
|
||||
<Rect x="99.2822" y="100.684" width="2.99145" height="4.18803" rx="1.49573" transform="rotate(-180 99.2822 100.684)" fill="#4C320C" />
|
||||
<Path d="M50.1157 131.318C70.7994 101.249 115.202 101.249 135.886 131.318L159.443 165.565C183.198 200.1 158.474 247.115 116.558 247.115H69.4435C27.5268 247.115 2.8027 200.1 26.5585 165.565L50.1157 131.318Z" fill="#FFF8DE" />
|
||||
<G filter="url(#filter1_i_3223_1170)">
|
||||
<Ellipse cx="134.581" cy="126.111" rx="49.0598" ry="35" fill="#FFF8DE" />
|
||||
</G>
|
||||
<G filter="url(#filter2_i_3223_1170)">
|
||||
<Ellipse cx="51.1196" cy="126.111" rx="48.7607" ry="35" fill="#FFF8DE" />
|
||||
</G>
|
||||
<Ellipse cx="92.7008" cy="108.761" rx="3.58974" ry="2.69231" transform="rotate(180 92.7008 108.761)" fill="#FFB8B9" />
|
||||
<Ellipse cx="8.5474" cy="3.40976" rx="8.5474" ry="3.40976" transform="matrix(1 0 0 -1 108.647 143)" fill="#FFD38D" />
|
||||
<Ellipse cx="65.5473" cy="139.59" rx="8.5474" ry="3.40976" transform="rotate(-180 65.5473 139.59)" fill="#FFD38D" />
|
||||
<Path d="M91.9591 112.026C92.2223 111.57 92.8803 111.57 93.1434 112.026L93.7356 113.051C93.9988 113.507 93.6698 114.077 93.1434 114.077H91.9591C91.4328 114.077 91.1038 113.507 91.367 113.051L91.9591 112.026Z" fill="#4C320C" />
|
||||
</G>
|
||||
<Defs>
|
||||
<Filter id="filter0_i_3223_1170" x="61.3882" y="30.0601" width="61.4814" height="48.1555" filterUnits="userSpaceOnUse">
|
||||
<FeFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<FeBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<FeColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<FeOffset dy="21" />
|
||||
<FeComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<FeColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.713726 0 0 0 0 0.270588 0 0 0 1 0" />
|
||||
<FeBlend mode="normal" in2="shape" result="effect1_innerShadow_3223_1170" />
|
||||
</Filter>
|
||||
<Filter id="filter1_i_3223_1170" x="80.1369" y="91.1111" width="103.504" height="71.7949" filterUnits="userSpaceOnUse">
|
||||
<FeFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<FeBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<FeColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<FeOffset dx="-5.38462" dy="1.79487" />
|
||||
<FeGaussianBlur stdDeviation="4.9359" />
|
||||
<FeComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<FeColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.713726 0 0 0 0 0.270588 0 0 0 1 0" />
|
||||
<FeBlend mode="normal" in2="shape" result="effect1_innerShadow_3223_1170" />
|
||||
</Filter>
|
||||
<Filter id="filter2_i_3223_1170" x="2.35889" y="91.1111" width="103.504" height="70" filterUnits="userSpaceOnUse">
|
||||
<FeFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<FeBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
|
||||
<FeColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
|
||||
<FeOffset dx="5.98291" />
|
||||
<FeGaussianBlur stdDeviation="3.2906" />
|
||||
<FeComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" />
|
||||
<FeColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.713974 0 0 0 0 0.272498 0 0 0 1 0" />
|
||||
<FeBlend mode="normal" in2="shape" result="effect1_innerShadow_3223_1170" />
|
||||
</Filter>
|
||||
</Defs>
|
||||
</Svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default IpSvg
|
||||
@ -1,91 +1,76 @@
|
||||
import ProTextSvg from '@/assets/icons/svg/proText.svg';
|
||||
import GradientText from '@/components/textLinear';
|
||||
import HandersSvg from '@/assets/icons/svg/handers.svg';
|
||||
import ProSvg from '@/assets/icons/svg/pro.svg';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Dimensions, StyleSheet, TouchableOpacity, View } from "react-native";
|
||||
import CardBg from './cardBg';
|
||||
import IpSvg from './ipSvg';
|
||||
import { Dimensions, Image, StyleSheet, TouchableOpacity, View } from "react-native";
|
||||
|
||||
const MemberCard = ({ pro, points }: { pro: string, points: number }) => {
|
||||
const MemberCard = ({ pro }: { pro: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const proPng = require("@/assets/images/png/owner/pro.png");
|
||||
const width = Dimensions.get("window").width;
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={[styles.container]} onPress={() => router.push({
|
||||
pathname: '/rights',
|
||||
params: {
|
||||
points: points,
|
||||
pro: pro
|
||||
<View style={styles.container}>
|
||||
<Image source={proPng} style={{ opacity: pro === "Pro" ? 1 : 0.5, width: 330, height: 110, position: "absolute", left: width * 0.5 - 40, bottom: 0, zIndex: 9 }} />
|
||||
<ProSvg width={"100%"} height={"100%"} style={{ opacity: pro === "Pro" ? 1 : 0.5, backgroundColor: '#4C320C', borderRadius: 32 }} />
|
||||
<HandersSvg style={{ opacity: pro === "Pro" ? 1 : 0.5, position: "absolute", left: width * 0.5 + 5, bottom: -3, zIndex: 10 }} />
|
||||
<View style={[styles.dateContainer, { opacity: pro === "Pro" ? 1 : 0.5 }]}>
|
||||
<ThemedText style={styles.date}>2025-09-05截止</ThemedText>
|
||||
</View>
|
||||
|
||||
{
|
||||
pro === "Pro"
|
||||
?
|
||||
null :
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
router.push('/rights');
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
style={styles.proText}
|
||||
>
|
||||
<ThemedText >{t("personal:generalSetting.goPremium")}</ThemedText>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
})}>
|
||||
{/* 背景图 */}
|
||||
<View style={[styles.cardBg, { opacity: pro === "pro" ? 1 : 0.5 }]}>
|
||||
<CardBg pro={pro} date={"2025-09-05"} />
|
||||
</View>
|
||||
{/* pro标志 */}
|
||||
<View style={[styles.proTextContainer, { left: width * 0.08, top: width * 0.07 }]}>
|
||||
<ProTextSvg />
|
||||
</View>
|
||||
{/* 背景板ip */}
|
||||
<View style={[styles.ipContainer, { opacity: pro === "pro" ? 1 : 0.5 }]}>
|
||||
<IpSvg pro={pro} />
|
||||
</View>
|
||||
{/* 会员标识 */}
|
||||
{/* <View style={[styles.memberContainer, { left: width * 0.25, top: width * 0.1, opacity: 1 }]}>
|
||||
<MemberBgSvg />
|
||||
<ThemedText style={{ fontSize: 12, color: "#2D3D60", position: "absolute", left: 0, top: 0, bottom: 0, right: 0, textAlign: "center", textAlignVertical: "center" }}>{t("personal:member.goPremium")}</ThemedText>
|
||||
</View> */}
|
||||
{/* 解锁更多魔法 */}
|
||||
<View style={{ position: "absolute", bottom: width * 0.05, left: -width * 0.12, opacity: pro === "pro" ? 1 : 0.5, flexWrap: "wrap" }}>
|
||||
<GradientText
|
||||
text={t("personal:member.unlock")}
|
||||
fontSize={16}
|
||||
lineHeight={1.5}
|
||||
color={[
|
||||
{ offset: "0%", color: "#D0BFB0" },
|
||||
{ offset: "32.89%", color: "#FFE57D" },
|
||||
{ offset: "81.1%", color: "#FFFFFF" }
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: "relative"
|
||||
},
|
||||
memberContainer: {
|
||||
position: "absolute",
|
||||
backgroundColor: "linear-gradient(97.5deg, #FFF3E8 7.16%, #FFFAB9 100.47%)",
|
||||
borderRadius: 13.1348,
|
||||
},
|
||||
proTextContainer: {
|
||||
position: "absolute",
|
||||
zIndex: 9,
|
||||
},
|
||||
ipContainer: {
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
zIndex: 9
|
||||
},
|
||||
cardBg: {
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
position: "relative",
|
||||
marginRight: 10,
|
||||
zIndex: -1,
|
||||
proText: {
|
||||
fontSize: 10,
|
||||
fontWeight: 'bold',
|
||||
color: '#4C320C',
|
||||
padding: 4,
|
||||
paddingHorizontal: 8,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#fff',
|
||||
position: 'absolute',
|
||||
top: 30,
|
||||
left: 90,
|
||||
opacity: 0.7
|
||||
},
|
||||
dateContainer: {
|
||||
position: 'absolute',
|
||||
zIndex: 10,
|
||||
alignItems: 'flex-end',
|
||||
transform: [
|
||||
{ rotate: '400deg' }
|
||||
],
|
||||
top: 16,
|
||||
right: -15,
|
||||
zIndex: 1,
|
||||
transform: [{ rotate: '40deg' }],
|
||||
},
|
||||
date: {
|
||||
fontSize: 11,
|
||||
fontWeight: 'bold',
|
||||
color: '#4C320C',
|
||||
padding: 4,
|
||||
},
|
||||
container: {
|
||||
position: "relative",
|
||||
height: 120
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -50,7 +50,7 @@ const styles = StyleSheet.create({
|
||||
goPro: {
|
||||
backgroundColor: '#E2793F',
|
||||
borderRadius: 24,
|
||||
paddingVertical: 12,
|
||||
paddingVertical: 6,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
|
||||
@ -64,10 +64,10 @@ const Premium = (props: Props) => {
|
||||
</ThemedText>
|
||||
<BlackStarSvg />
|
||||
</View>
|
||||
<ThemedText style={[styles.titleText, { fontSize: 16, marginTop: item?.product_code === bestValue ? 0 : -10 }]}>
|
||||
<ThemedText style={[styles.titleText, { fontSize: 16 }]}>
|
||||
{item.product_code?.split('_')[item.product_code?.split('_')?.length - 1]}
|
||||
</ThemedText>
|
||||
<ThemedText style={[styles.titleText, { fontSize: 32, lineHeight: 32, paddingVertical: item?.product_code === bestValue ? 0 : 5 }]}>
|
||||
<ThemedText style={[styles.titleText, { fontSize: 32, lineHeight: 32 }]}>
|
||||
$ {(Number(item.unit_price.amount) - Number(item.discount_amount.amount)).toFixed(2)}
|
||||
</ThemedText>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
|
||||
@ -79,11 +79,11 @@ const UserInfo = (props: UserInfoProps) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.info}>
|
||||
<ThemedText style={styles.nickname} type="sfPro">{userInfo?.nickname}</ThemedText>
|
||||
<ThemedText style={styles.userId} type="inter">{t('generalSetting.userId', { ns: 'personal' })}{userInfo?.user_id}</ThemedText>
|
||||
<ThemedText style={styles.nickname}>{userInfo?.nickname}</ThemedText>
|
||||
<ThemedText style={styles.userId}>{t('generalSetting.userId', { ns: 'personal' })}{userInfo?.user_id}</ThemedText>
|
||||
<View style={styles.location}>
|
||||
<LocationSvg />
|
||||
<ThemedText style={styles.userId} type="inter">
|
||||
<ThemedText style={styles.userId}>
|
||||
{currentLocation?.country}-{currentLocation?.city}-{currentLocation?.district}
|
||||
</ThemedText>
|
||||
<TouchableOpacity
|
||||
|
||||
@ -5,7 +5,7 @@ import { ThemedText } from '@/components/ThemedText';
|
||||
import { UserInfoDetails } from '@/types/user';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useState } from 'react';
|
||||
import { Image, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import { Image, ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
import CopyButton from '../copy';
|
||||
|
||||
export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
|
||||
@ -14,7 +14,7 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View className='flex flex-row justify-between items-center mt-[1rem] gap-[1rem] w-full'>
|
||||
{/* 头像 */}
|
||||
<View className='w-auto'>
|
||||
{userInfo?.user_info?.avatar_file_url && !imageError ? (
|
||||
@ -30,34 +30,45 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
|
||||
)}
|
||||
</View>
|
||||
{/* 用户名 */}
|
||||
<View className='flex flex-col w-[60%]'>
|
||||
<View className='flex flex-col w-[75%] gap-1'>
|
||||
<View className='flex flex-row items-center justify-between w-full'>
|
||||
<View className='flex flex-row items-center gap-2 w-full justify-between'>
|
||||
<View style={{ width: "100%", flexDirection: "row", alignItems: "center", gap: 2 }}>
|
||||
<ThemedText
|
||||
style={{ maxWidth: "90%", lineHeight: 30 }}
|
||||
weight='bold'
|
||||
color='textSecondary'
|
||||
size='3xl'
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
|
||||
>
|
||||
{userInfo?.user_info?.nickname}
|
||||
</ThemedText>
|
||||
<View className='flex flex-row items-center gap-2 w-full'>
|
||||
<ThemedText
|
||||
className='max-w-[80%] !text-textSecondary !font-semibold !text-2xl'
|
||||
numberOfLines={1} // 限制为1行
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{userInfo?.user_info?.nickname}
|
||||
</ThemedText>
|
||||
<ScrollView
|
||||
className='max-w-[20%]'
|
||||
horizontal // 水平滚动
|
||||
showsHorizontalScrollIndicator={false} // 隐藏滚动条
|
||||
contentContainerStyle={{
|
||||
flexDirection: 'row',
|
||||
gap: 8, // 间距,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{
|
||||
userInfo?.membership_level && (
|
||||
userInfo?.medal_infos?.map((item, index) => (
|
||||
<Image
|
||||
source={require('@/assets/images/png/owner/proIcon.png')}
|
||||
key={index}
|
||||
source={{ uri: item.url }}
|
||||
style={{ width: 24, height: 24 }}
|
||||
/>
|
||||
)
|
||||
))
|
||||
}
|
||||
</ScrollView>
|
||||
|
||||
<View className='flex flex-row items-center gap-2 border border-bgPrimary px-2 py-1 rounded-full'>
|
||||
<StarSvg />
|
||||
<ThemedText style={{ color: 'bgPrimary', fontSize: 14, fontWeight: '700' }}>{userInfo?.remain_points}</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className='flex flex-row items-center justify-between w-full'>
|
||||
<View style={{ flex: 1, flexDirection: "row", alignItems: "center", gap: 2, maxWidth: '100%' }}>
|
||||
<View style={{ flex: 1, flexDirection: "row", alignItems: "center", gap: 2, maxWidth: '80%' }}>
|
||||
<ThemedText
|
||||
style={{
|
||||
color: '#AC7E35',
|
||||
@ -66,7 +77,6 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
|
||||
flexShrink: 1,
|
||||
flexGrow: 0,
|
||||
}}
|
||||
type="inter"
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
@ -74,57 +84,23 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
|
||||
</ThemedText>
|
||||
<CopyButton textToCopy={userInfo?.user_info?.user_id || ""} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flexDirection: "column", alignItems: "flex-end", gap: 4 }}>
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#FFB645',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 16,
|
||||
}}>
|
||||
<StarSvg />
|
||||
<ThemedText
|
||||
style={{
|
||||
color: '#4C320C',
|
||||
fontSize: 14,
|
||||
fontWeight: '700',
|
||||
maxWidth: 40,
|
||||
lineHeight: 20
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
router.push('/setting');
|
||||
}}
|
||||
type="inter"
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
activeOpacity={0.7}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
style={styles.text}
|
||||
>
|
||||
{userInfo?.remain_points}
|
||||
</ThemedText>
|
||||
<SettingSvg />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
router.push('/setting');
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
style={styles.text}
|
||||
>
|
||||
<SettingSvg />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View >
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-end",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
},
|
||||
text: {
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import Svg, { Defs, LinearGradient, Stop, Text as SvgText, TSpan } from 'react-native-svg';
|
||||
|
||||
interface GradientTextProps {
|
||||
text: string;
|
||||
color?: { offset: string, color: string }[];
|
||||
fontSize?: number;
|
||||
fontWeight?: string;
|
||||
width?: number;
|
||||
lineHeight?: number;
|
||||
}
|
||||
|
||||
export default function GradientText(props: GradientTextProps) {
|
||||
const { text, color, fontSize = 48, fontWeight = "700", width = 300, lineHeight = 1.2 } = props;
|
||||
|
||||
// Split text into words and create lines that fit within the specified width
|
||||
const createLines = (text: string, maxWidth: number) => {
|
||||
const words = text.split(' ');
|
||||
const lines: string[] = [];
|
||||
let currentLine = '';
|
||||
|
||||
words.forEach(word => {
|
||||
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
||||
// Approximate text width (this is a simple estimation)
|
||||
const testWidth = testLine.length * (fontSize * 0.6);
|
||||
|
||||
if (testWidth > maxWidth && currentLine) {
|
||||
lines.push(currentLine);
|
||||
currentLine = word;
|
||||
} else {
|
||||
currentLine = testLine;
|
||||
}
|
||||
});
|
||||
|
||||
if (currentLine) {
|
||||
lines.push(currentLine);
|
||||
}
|
||||
|
||||
return lines;
|
||||
};
|
||||
|
||||
const lines = createLines(text, width - 40); // 40px padding
|
||||
const lineHeightPx = fontSize * lineHeight;
|
||||
const totalHeight = lines.length * lineHeightPx;
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { width }]}>
|
||||
<Svg height={totalHeight} width={width}>
|
||||
<Defs>
|
||||
<LinearGradient id="textGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
{color?.map((item, index) => (
|
||||
<Stop key={index} offset={item.offset} stopColor={item.color} />
|
||||
))}
|
||||
</LinearGradient>
|
||||
</Defs>
|
||||
|
||||
<SvgText
|
||||
x="50%"
|
||||
y={fontSize}
|
||||
fontFamily="System"
|
||||
fontSize={fontSize}
|
||||
fontWeight={fontWeight}
|
||||
textAnchor="middle"
|
||||
fill="url(#textGradient)"
|
||||
>
|
||||
{lines.map((line, index) => (
|
||||
<TSpan
|
||||
key={index}
|
||||
x="50%"
|
||||
dy={index === 0 ? 0 : lineHeightPx}
|
||||
textAnchor="middle"
|
||||
>
|
||||
{line}
|
||||
</TSpan>
|
||||
))}
|
||||
</SvgText>
|
||||
</Svg>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
});
|
||||
@ -1,49 +0,0 @@
|
||||
import { ThemedText } from "@/components/ThemedText";
|
||||
import { ActivityIndicator, StyleSheet, TouchableOpacity } from "react-native";
|
||||
|
||||
interface Props {
|
||||
isLoading?: boolean;
|
||||
onPress?: () => void;
|
||||
text: string
|
||||
bg?: string
|
||||
color?: string
|
||||
}
|
||||
const StepButton = (props: Props) => {
|
||||
const { isLoading, onPress, text, bg, color } = props
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.button, isLoading && styles.disabledButton, { backgroundColor: bg ? bg : '#E2793F' }]}
|
||||
onPress={onPress}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<ThemedText style={[styles.buttonText, { color: color ? color : '#FFFFFF' }]} size='lg' weight='bold'>
|
||||
{text}
|
||||
</ThemedText>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
width: '100%',
|
||||
backgroundColor: '#E2793F',
|
||||
borderRadius: 32,
|
||||
padding: 18,
|
||||
alignItems: 'center'
|
||||
},
|
||||
disabledButton: {
|
||||
opacity: 0.7,
|
||||
},
|
||||
buttonText: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '600',
|
||||
fontSize: 18,
|
||||
},
|
||||
});
|
||||
|
||||
export default StepButton
|
||||
@ -1,216 +1,89 @@
|
||||
import DoneSvg from '@/assets/icons/svg/done.svg';
|
||||
import { router } from 'expo-router';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Dimensions, Image, PixelRatio, Platform, StyleSheet, View } from 'react-native';
|
||||
import Animated, {
|
||||
Easing,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming
|
||||
} from 'react-native-reanimated';
|
||||
import { Platform, TouchableOpacity, View } from 'react-native';
|
||||
import { ThemedText } from '../ThemedText';
|
||||
import { Fireworks } from '../firework';
|
||||
import StepButton from '../ui/button/stepButton';
|
||||
import Lottie from '../lottie/lottie';
|
||||
|
||||
export default function Done() {
|
||||
const { t } = useTranslation();
|
||||
const height = Dimensions.get('window').height;
|
||||
const fontSize = (size: number) => {
|
||||
const scale = PixelRatio.getFontScale();
|
||||
return size / scale;
|
||||
};
|
||||
// Animation values
|
||||
const translateX = useSharedValue(300);
|
||||
const translateY = useSharedValue(300);
|
||||
const opacity = useSharedValue(0);
|
||||
|
||||
// Animation style
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{ translateX: translateX.value },
|
||||
{ translateY: translateY.value }
|
||||
],
|
||||
opacity: opacity.value
|
||||
}));
|
||||
|
||||
// Start animation when component mounts
|
||||
useEffect(() => {
|
||||
translateX.value = withTiming(0, {
|
||||
duration: 800,
|
||||
easing: Easing.out(Easing.cubic)
|
||||
});
|
||||
translateY.value = withTiming(0, {
|
||||
duration: 800,
|
||||
easing: Easing.out(Easing.cubic)
|
||||
});
|
||||
opacity.value = withTiming(1, {
|
||||
duration: 1000,
|
||||
easing: Easing.out(Easing.cubic)
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleContinue = () => {
|
||||
router.replace('/ask')
|
||||
};
|
||||
|
||||
const renderWebView = () => (
|
||||
<View style={styles.webContainer}>
|
||||
<View style={styles.webContent}>
|
||||
<ThemedText style={styles.title}>
|
||||
{t('auth.userMessage.allDone', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</View>
|
||||
<View style={styles.flex1} />
|
||||
<View style={styles.lottieContainer}>
|
||||
<Animated.View style={animatedStyle}>
|
||||
<Image
|
||||
source={require('@/assets/images/png/icon/doneIP.png')}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
<View style={styles.webButtonContainer}>
|
||||
<StepButton
|
||||
text={t('auth.userMessage.next', { ns: 'login' })}
|
||||
onPress={handleContinue}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderMobileView = () => (
|
||||
<View style={styles.mobileContainer}>
|
||||
<View style={styles.mobileContent}>
|
||||
<View style={[styles.mobileTextContainer, { marginTop: -height * 0.15 }]}>
|
||||
<ThemedText style={[styles.title, { fontSize: fontSize(36) }]}>
|
||||
{t('auth.userMessage.allDone', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</View>
|
||||
<View style={styles.mobileButtonContainer}>
|
||||
<StepButton
|
||||
text={t('auth.userMessage.next', { ns: 'login' })}
|
||||
onPress={handleContinue}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.fireworksContainer}>
|
||||
<Fireworks
|
||||
autoPlay={true}
|
||||
loop={false}
|
||||
interval={1500}
|
||||
particleCount={90}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.lottieContainer}>
|
||||
<Animated.View style={animatedStyle}>
|
||||
<Image
|
||||
source={require('@/assets/images/png/icon/doneIP.png')}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{Platform.OS === 'web' ? renderWebView() : renderMobileView()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
<View className="flex-1">
|
||||
{
|
||||
Platform.OS === 'web'
|
||||
?
|
||||
<View className="flex-1 bg-bgPrimary absolute top-0 left-0 right-0 bottom-0 h-full">
|
||||
<View className="absolute top-[2rem] left-0 right-0 bottom-[10rem] justify-center items-center">
|
||||
<ThemedText className="!text-4xl !text-white text-center">
|
||||
{t('auth.userMessage.allDone', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</View>
|
||||
<View className='flex-1' />
|
||||
<View className="flex-row justify-end">
|
||||
<DoneSvg />
|
||||
</View>
|
||||
{/* Next Button */}
|
||||
<View className="absolute bottom-[1rem] left-0 right-0 p-[1rem] z-99">
|
||||
<TouchableOpacity
|
||||
className={`w-full bg-buttonFill rounded-full p-4 items-center`}
|
||||
onPress={handleContinue}
|
||||
>
|
||||
<ThemedText className="!text-white text-lg font-semibold">
|
||||
{t('auth.userMessage.next', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
:
|
||||
<View className="flex-1 bg-transparent">
|
||||
{/* 文字 */}
|
||||
<View className="absolute top-0 left-0 right-0 bottom-0 z-30">
|
||||
<View className="flex-1 justify-center items-center">
|
||||
<ThemedText className="!text-4xl !text-white text-center">
|
||||
{t('auth.userMessage.allDone', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</View>
|
||||
{/* Next Button */}
|
||||
<View className="absolute bottom-[1rem] left-0 right-0 p-[1rem] z-99">
|
||||
<TouchableOpacity
|
||||
className={`w-full bg-buttonFill rounded-full p-4 items-center`}
|
||||
onPress={handleContinue}
|
||||
>
|
||||
<ThemedText className="!text-white text-lg font-semibold">
|
||||
{t('auth.userMessage.next', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
{/* 背景动画 - 烟花 */}
|
||||
<View className="absolute top-0 left-0 right-0 bottom-0 z-10">
|
||||
<Fireworks
|
||||
autoPlay={true}
|
||||
loop={false}
|
||||
interval={1500}
|
||||
particleCount={90}
|
||||
/>
|
||||
</View>
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
flex1: {
|
||||
flex: 1,
|
||||
},
|
||||
webContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFB645',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: '100%',
|
||||
},
|
||||
webContent: {
|
||||
position: 'absolute',
|
||||
top: 32,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 160,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
doneSvgContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
webButtonContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 16,
|
||||
left: 0,
|
||||
right: 0,
|
||||
padding: 16,
|
||||
zIndex: 99,
|
||||
},
|
||||
mobileContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
mobileContent: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 30,
|
||||
},
|
||||
mobileTextContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
mobileButtonContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 16,
|
||||
left: 0,
|
||||
right: 0,
|
||||
padding: 16,
|
||||
zIndex: 99,
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
lineHeight: 40,
|
||||
color: '#FFFFFF',
|
||||
textAlign: 'center',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
nextButton: {
|
||||
width: '100%',
|
||||
backgroundColor: '#3B82F6',
|
||||
borderRadius: 999,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
fireworksContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 10,
|
||||
},
|
||||
lottieContainer: {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 20
|
||||
},
|
||||
});
|
||||
{/* 前景动画 - Lottie */}
|
||||
<View className="absolute top-0 left-0 right-0 bottom-0 z-20">
|
||||
<Lottie
|
||||
source={'allDone'}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: 'transparent'
|
||||
}}
|
||||
loop={false}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@ -4,10 +4,8 @@ import LookSvg from '@/assets/icons/svg/look.svg';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { FileUploadItem } from '@/lib/background-uploader/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, Image, StyleSheet, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { ActivityIndicator, Alert, Image, TouchableOpacity, View } from 'react-native';
|
||||
import FilesUploader from '../file-upload/files-uploader';
|
||||
import StepButton from '../ui/button/stepButton';
|
||||
|
||||
interface Props {
|
||||
setSteps?: (steps: Steps) => void;
|
||||
@ -21,51 +19,57 @@ interface Props {
|
||||
export default function Look(props: Props) {
|
||||
const { fileData, setFileData, isLoading, handleUser, avatar } = props;
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { paddingBottom: insets.bottom, paddingTop: insets.top + 28 }]}>
|
||||
<View style={styles.contentContainer}>
|
||||
<ThemedText style={styles.title}>
|
||||
<View className="flex-1 bg-textPrimary justify-between p-[2rem]">
|
||||
<View className="flex-1 justify-center items-center">
|
||||
<ThemedText className="text-4xl font-bold !text-white mb-[2rem]">
|
||||
{t('auth.userMessage.look', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.subtitle} type="inter" size="sm" weight="regular">
|
||||
<ThemedText className="text-base !text-white/80 text-center mb-[2rem]">
|
||||
{t('auth.userMessage.avatarText', { ns: 'login' })}
|
||||
{"\n"}
|
||||
{t('auth.userMessage.avatorText2', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
{fileData[0]?.preview || fileData[0]?.previewUrl ? (
|
||||
<Image
|
||||
style={styles.avatarImage}
|
||||
source={{ uri: fileData[0].preview || fileData[0].previewUrl }}
|
||||
/>
|
||||
) : avatar ? (
|
||||
<Image
|
||||
style={styles.avatarImage}
|
||||
source={{ uri: avatar }}
|
||||
/>
|
||||
) : (
|
||||
<LookSvg />
|
||||
)}
|
||||
{
|
||||
fileData[0]?.preview || fileData[0]?.previewUrl
|
||||
?
|
||||
<Image
|
||||
className='rounded-full w-[10rem] h-[10rem]'
|
||||
source={{ uri: fileData[0].preview || fileData[0].previewUrl }}
|
||||
/>
|
||||
:
|
||||
avatar
|
||||
?
|
||||
<Image
|
||||
className='rounded-full w-[10rem] h-[10rem]'
|
||||
source={{ uri: avatar }}
|
||||
/>
|
||||
:
|
||||
<LookSvg />
|
||||
}
|
||||
<FilesUploader
|
||||
onUploadComplete={(fileData) => {
|
||||
setFileData(fileData as FileUploadItem[]);
|
||||
}}
|
||||
showPreview={false}
|
||||
children={
|
||||
<View style={styles.uploadButton}>
|
||||
<View className="w-full rounded-full px-4 py-2 mt-4 items-center bg-inputBackground flex-row flex gap-2">
|
||||
<ChoicePhoto />
|
||||
<ThemedText style={styles.uploadButtonText} type="sfPro" size="sm" weight="semiBold">
|
||||
<ThemedText className="text-textTertiary text-lg font-semibold">
|
||||
{t('auth.userMessage.choosePhoto', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
{/* <AutoUploadScreen /> */}
|
||||
{/* <MediaStatsScreen /> */}
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<StepButton
|
||||
text={t('auth.userMessage.next', { ns: 'login' })}
|
||||
<View className="w-full">
|
||||
<TouchableOpacity
|
||||
className={`w-full bg-white rounded-full p-4 items-center ${isLoading ? 'opacity-70' : ''}`}
|
||||
onPress={() => {
|
||||
if (fileData[0]?.preview || fileData[0]?.previewUrl || avatar) {
|
||||
handleUser()
|
||||
@ -73,59 +77,17 @@ export default function Look(props: Props) {
|
||||
Alert.alert(t('auth.userMessage.avatarRequired', { ns: 'login' }))
|
||||
}
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
bg="#FFFFFF"
|
||||
color="#4C320C"
|
||||
/>
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color="#000" />
|
||||
) : (
|
||||
<ThemedText className="text-textTertiary text-lg font-semibold">
|
||||
{t('auth.userMessage.next', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View >
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#AC7E35',
|
||||
paddingHorizontal: 24,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
gap: 28
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
lineHeight: 36,
|
||||
fontWeight: 'bold',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
color: "#fff",
|
||||
textAlign: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
avatarImage: {
|
||||
borderRadius: 150,
|
||||
width: 215,
|
||||
height: 215,
|
||||
marginBottom: 16,
|
||||
},
|
||||
uploadButton: {
|
||||
width: '100%',
|
||||
borderRadius: 999,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 13,
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#FFF8DE',
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
},
|
||||
uploadButtonText: {
|
||||
color: '#4C320C'
|
||||
},
|
||||
footer: {
|
||||
width: '100%',
|
||||
}
|
||||
});
|
||||
|
||||
@ -2,9 +2,7 @@ import { Steps } from '@/app/(tabs)/user-message';
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Dimensions, KeyboardAvoidingView, Platform, StyleSheet, TextInput, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import StepButton from '../ui/button/stepButton';
|
||||
import { ActivityIndicator, KeyboardAvoidingView, Platform, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
interface Props {
|
||||
setSteps: (steps: Steps) => void;
|
||||
@ -16,8 +14,6 @@ export default function UserName(props: Props) {
|
||||
const { setSteps, username, setUsername } = props
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const height = Dimensions.get('window').height;
|
||||
const insets = useSafeAreaInsets();
|
||||
const [error, setError] = useState('')
|
||||
const handleUserName = () => {
|
||||
if (!username) {
|
||||
@ -32,98 +28,45 @@ export default function UserName(props: Props) {
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.keyboardAvoidingView}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<View style={[styles.container]}>
|
||||
<View style={styles.flex1} />
|
||||
<View style={[styles.inputContainer, { paddingBottom: insets.bottom }]}>
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={styles.titleContainer}>
|
||||
<ThemedText style={styles.titleText} color="textSecondary" size='xl' weight='bold'>{t('auth.userMessage.title', { ns: 'login' })}</ThemedText>
|
||||
<View className='bg-bgPrimary flex-1 h-full'>
|
||||
<View className="flex-1" />
|
||||
{/* Input container fixed at bottom */}
|
||||
<View className="w-full bg-white p-4 border-t border-gray-200 rounded-t-3xl">
|
||||
<View className="flex-col items-center justify-center w-full gap-[1rem]">
|
||||
<View className='w-full flex flex-row items-center justify-center'>
|
||||
<ThemedText className="text-textSecondary font-semibold">{t('auth.userMessage.title', { ns: 'login' })}</ThemedText>
|
||||
</View>
|
||||
<View style={styles.inputWrapper}>
|
||||
<View style={styles.labelContainer}>
|
||||
<ThemedText style={styles.labelText} color="textPrimary" type='sfPro' size='sm' weight='bold'>{t('auth.userMessage.username', { ns: 'login' })}</ThemedText>
|
||||
<ThemedText style={styles.errorText}>{error}</ThemedText>
|
||||
<View className='w-full mb-[1rem]'>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
|
||||
<ThemedText className="!text-textPrimary ml-2 mb-2 font-semibold">{t('auth.userMessage.username', { ns: 'login' })}</ThemedText>
|
||||
<ThemedText style={{ color: "#E2793F", fontSize: 14 }}>{error}</ThemedText>
|
||||
</View>
|
||||
<TextInput
|
||||
style={[styles.textInput, { marginBottom: height * 0.2 }]}
|
||||
className="bg-inputBackground rounded-2xl p-4 w-full"
|
||||
placeholder={t('auth.userMessage.usernamePlaceholder', { ns: 'login' })}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
/>
|
||||
</View>
|
||||
<StepButton text={t('auth.userMessage.next', { ns: 'login' })} onPress={handleUserName} isLoading={isLoading} />
|
||||
<TouchableOpacity
|
||||
className={`w-full bg-[#E2793F] rounded-full text-[#fff] p-4 items-center mb-6 ${isLoading ? 'opacity-70' : ''} rounded-2xl`}
|
||||
onPress={handleUserName}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<ThemedText className="!text-white font-semibold">
|
||||
{t('auth.userMessage.next', { ns: 'login' })}
|
||||
</ThemedText>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
keyboardAvoidingView: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFB645',
|
||||
height: '100%',
|
||||
},
|
||||
flex1: {
|
||||
flex: 1,
|
||||
},
|
||||
inputContainer: {
|
||||
width: '100%',
|
||||
backgroundColor: '#FFFFFF',
|
||||
padding: 16,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#E5E7EB',
|
||||
borderTopLeftRadius: 50,
|
||||
borderTopRightRadius: 50,
|
||||
},
|
||||
contentContainer: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
gap: 16,
|
||||
},
|
||||
titleContainer: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 20,
|
||||
},
|
||||
titleText: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
inputWrapper: {
|
||||
width: '100%',
|
||||
marginBottom: 16,
|
||||
},
|
||||
labelContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 10,
|
||||
},
|
||||
labelText: {
|
||||
color: '#AC7E35',
|
||||
marginLeft: 8,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
errorText: {
|
||||
color: '#E2793F',
|
||||
fontSize: 14,
|
||||
},
|
||||
textInput: {
|
||||
backgroundColor: '#FFF8DE',
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
width: '100%',
|
||||
}
|
||||
});
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
* @param seconds 总秒数
|
||||
* @returns 格式化后的时间字符串
|
||||
*/
|
||||
export function formatDuration(seconds: number): { s: number, m: number, h: number } {
|
||||
export function formatDuration(seconds: number): string {
|
||||
if (seconds < 60) {
|
||||
return { s: seconds, m: 0, h: 0 };
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
@ -13,16 +13,16 @@ export function formatDuration(seconds: number): { s: number, m: number, h: numb
|
||||
|
||||
if (minutes < 60) {
|
||||
return remainingSeconds > 0
|
||||
? { s: remainingSeconds, m: minutes, h: 0 }
|
||||
: { s: 0, m: minutes, h: 0 };
|
||||
? `${minutes}min${remainingSeconds}s`
|
||||
: `${minutes}min`;
|
||||
}
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
|
||||
if (remainingMinutes === 0) {
|
||||
return { s: 0, m: 0, h: hours };
|
||||
return `${hours}h`;
|
||||
}
|
||||
|
||||
return { s: seconds, m: minutes, h: hours };
|
||||
return `${hours}h${remainingMinutes}min`;
|
||||
}
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
export const Fonts = {
|
||||
// Font family
|
||||
quicksand: 'quicksand',
|
||||
sfPro: 'sfPro',
|
||||
inter: 'inter',
|
||||
|
||||
// Font weights
|
||||
regular: '400',
|
||||
medium: '500',
|
||||
semiBold: '600',
|
||||
bold: '700',
|
||||
extraBold: '800',
|
||||
|
||||
// Font sizes
|
||||
xxs: 11,
|
||||
xs: 12,
|
||||
sm: 14,
|
||||
base: 16,
|
||||
lg: 18,
|
||||
xl: 20,
|
||||
'2xl': 24,
|
||||
'3xl': 30,
|
||||
'4xl': 36,
|
||||
'5xl': 48,
|
||||
"title": 32,
|
||||
|
||||
// color
|
||||
bgPrimary: '#FFB645',
|
||||
bgSecondary: '#E2793F',
|
||||
bgCheck: "#FADBA1",
|
||||
bgInput: '#FFF8DE',
|
||||
textPrimary: '#AC7E35',
|
||||
textSecondary: '#4C320C',
|
||||
textThird: '#7F786F',
|
||||
textWhite: "#FFFFFF",
|
||||
placeholderTextColor: "#ccc",
|
||||
|
||||
} as const;
|
||||
|
||||
export type FontWeight = keyof Omit<typeof Fonts, 'quicksand' | 'sfPro' | 'inter' | 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl'>;
|
||||
export type FontSize = "xxs" | "xs" | "sm" | "base" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "title";
|
||||
export type FontColor = 'bgPrimary' | 'bgSecondary' | 'textPrimary' | 'textSecondary' | 'textThird' | 'textWhite';
|
||||
|
||||
@ -31,9 +31,8 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
if (Platform.OS === 'web') {
|
||||
token = localStorage.getItem('token') || "";
|
||||
} else {
|
||||
await SecureStore.getItemAsync('token').then((token) => {
|
||||
token = token || "";
|
||||
})
|
||||
const storedToken = await SecureStore.getItemAsync('token');
|
||||
token = storedToken || "";
|
||||
}
|
||||
if (token) {
|
||||
// 验证当前 token 是否有效
|
||||
@ -61,7 +60,8 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
} catch (refreshError) {
|
||||
// 刷新 token 失败,才进行登出操作
|
||||
// console.error("Token refresh failed, logging out", refreshError);
|
||||
logout();
|
||||
// 只有在确实需要登出时才调用logout()
|
||||
// 如果是首次启动应用,没有token是正常的,不需要登出
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ i18n
|
||||
defaultNS: 'common',
|
||||
|
||||
// 设置默认语言为中文
|
||||
lng: 'en',
|
||||
lng: 'zh',
|
||||
fallbackLng: 'en',
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
|
||||
|
||||
@ -14,9 +14,9 @@
|
||||
"refresh": "Refresh",
|
||||
"error": "have some error",
|
||||
"issue": "have some issue",
|
||||
"case1": "Find last year’s baby moments",
|
||||
"case2": "Pet moments",
|
||||
"case3": "Show me my food memories in France with family",
|
||||
"case1": "Find last year's baby/pet material",
|
||||
"case2": "Find last year's food",
|
||||
"case3": "Find recent travel material",
|
||||
"mediaAuth": "need album permission",
|
||||
"mediaAuthDesc": "allow app to access album to save media files",
|
||||
"saveSuccess": "save success",
|
||||
|
||||
@ -67,14 +67,13 @@
|
||||
"accountPlaceholder": "Enter your account or email",
|
||||
"signUpMessage": "Don’t have an account?",
|
||||
"signUp": "Sign up",
|
||||
"phoneLogin": "Phone Login",
|
||||
"titleText": "Awake your Memo"
|
||||
"phoneLogin": "Phone Login"
|
||||
},
|
||||
"agree": {
|
||||
"logintext": "By logging in, you agree to our",
|
||||
"singupText": "By signing up, you agree to our",
|
||||
"terms": " Terms",
|
||||
"join": " and have read our",
|
||||
"join": "&",
|
||||
"privacyPolicy": " Privacy Policy."
|
||||
},
|
||||
"welcome": {
|
||||
|
||||
@ -74,7 +74,7 @@
|
||||
"confirm": "Confirm",
|
||||
"location": "Location",
|
||||
"rank": "Top Memory Makers",
|
||||
"userId": "User ID : ",
|
||||
"userId": "User ID",
|
||||
"usedStorage": "Storage Used",
|
||||
"remainingPoints": "Points Remaining",
|
||||
"totalVideo": "Total Videos",
|
||||
@ -125,9 +125,5 @@
|
||||
"agreement": "I have read and agree to",
|
||||
"membership": "《Membership Agreement》",
|
||||
"agreementError": "Please read and agree to the agreement"
|
||||
},
|
||||
"member": {
|
||||
"goPremium": "Go Premium",
|
||||
"unlock": "解锁更多记忆魔法"
|
||||
}
|
||||
}
|
||||
@ -67,8 +67,7 @@
|
||||
"accountPlaceholder": "请输入您的账号或邮箱",
|
||||
"signUpMessage": "还没有账号?",
|
||||
"signUp": "注册",
|
||||
"phoneLogin": "手机号登录",
|
||||
"titleText": "Awake your Memo"
|
||||
"phoneLogin": "手机号登录"
|
||||
},
|
||||
"agree": {
|
||||
"logintext": "登录即表示您同意我们的",
|
||||
|
||||
@ -125,9 +125,5 @@
|
||||
"agreement": "我已阅读并同意",
|
||||
"membership": "《会员协议》",
|
||||
"agreementError": "请先阅读并同意协议"
|
||||
},
|
||||
"member": {
|
||||
"goPremium": "开通会员",
|
||||
"unlock": "解锁更多记忆魔法"
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@ import Constants from 'expo-constants';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { Platform } from 'react-native';
|
||||
import Toast from 'react-native-toast-message';
|
||||
import { useAuth } from '../contexts/auth-context';
|
||||
import { store } from '../store';
|
||||
import { User } from '../types/user';
|
||||
|
||||
@ -25,13 +24,12 @@ export interface PagedResult<T> {
|
||||
// 获取.env文件中的变量
|
||||
|
||||
|
||||
export const API_ENDPOINT = process.env.EXPO_PUBLIC_API_ENDPOINT || Constants.expoConfig?.extra?.API_ENDPOINT;
|
||||
export const API_ENDPOINT = Constants.expoConfig?.extra?.API_ENDPOINT || "http://192.168.31.16:31646/api";
|
||||
|
||||
|
||||
// 更新 access_token 的逻辑 - 用于React组件中
|
||||
export const useAuthToken = async<T>(message: string | null) => {
|
||||
export const useAuthToken = async<T>(message: string | null, login: (user: User, jwt: string) => void) => {
|
||||
try {
|
||||
const { login } = useAuth();
|
||||
const response = await fetch(`${API_ENDPOINT}/v1/iam/access-token-refresh`);
|
||||
const apiResponse: ApiResponse<T> = await response.json();
|
||||
|
||||
@ -57,18 +55,37 @@ export const useAuthToken = async<T>(message: string | null) => {
|
||||
}
|
||||
|
||||
// 使用Redux存储token的刷新token函数
|
||||
export const refreshAuthToken = async<T>(message: string | null): Promise<User> => {
|
||||
export const refreshAuthToken = async<T>(message: string | null): Promise<User | null> => {
|
||||
try {
|
||||
let cookie = "";
|
||||
let userId = "";
|
||||
if (Platform.OS === 'web') {
|
||||
cookie = localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user') || "")?.refresh_token || "" : "";
|
||||
userId = localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user') || "")?.user_id || "" : "";
|
||||
const userStr = localStorage.getItem('user');
|
||||
if (userStr) {
|
||||
try {
|
||||
const userObj = JSON.parse(userStr);
|
||||
cookie = userObj?.refresh_token || "";
|
||||
userId = userObj?.user_id || "";
|
||||
} catch (e) {
|
||||
console.error("Failed to parse user data from localStorage", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await SecureStore.getItemAsync('user').then((user: string | null) => {
|
||||
cookie = JSON.parse(user || "")?.refresh_token || "";
|
||||
userId = JSON.parse(user || "")?.user_id || "";
|
||||
})
|
||||
const userStr = await SecureStore.getItemAsync('user');
|
||||
if (userStr) {
|
||||
try {
|
||||
const userObj = JSON.parse(userStr);
|
||||
cookie = userObj?.refresh_token || "";
|
||||
userId = userObj?.user_id || "";
|
||||
} catch (e) {
|
||||
console.error("Failed to parse user data from SecureStore", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有必要的数据,抛出错误
|
||||
if (!cookie || !userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 退出刷新会重新填充数据
|
||||
|
||||
@ -4,7 +4,7 @@ import { TFunction } from 'i18next';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
// 从环境变量或默认值中定义 WebSocket 端点
|
||||
export const WEBSOCKET_ENDPOINT = process.env.EXPO_PUBLIC_WEBSOCKET_ENDPOINT || Constants.expoConfig?.extra?.WEBSOCKET_ENDPOINT;
|
||||
export const WEBSOCKET_ENDPOINT = Constants.expoConfig?.extra?.WEBSOCKET_ENDPOINT || "ws://192.168.31.16:31646/ws/chat";
|
||||
|
||||
export type WebSocketStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
|
||||
|
||||
@ -34,8 +34,6 @@ class WebSocketManager {
|
||||
private readonly reconnectInterval = 1000; // 初始重连间隔为1秒
|
||||
private pingIntervalId: ReturnType<typeof setInterval> | null = null;
|
||||
private readonly pingInterval = 30000; // 30秒发送一次心跳
|
||||
private isConnecting = false; // 防止并发重复连接
|
||||
private connectTimeoutId: ReturnType<typeof setTimeout> | null = null; // 连接超时
|
||||
|
||||
constructor() {
|
||||
// 这是一个单例类,连接通过调用 connect() 方法来启动
|
||||
@ -53,91 +51,66 @@ class WebSocketManager {
|
||||
* 会自动获取并使用存储的认证 token。
|
||||
*/
|
||||
public async connect() {
|
||||
// 已连或正在连,直接返回(基于状态的幂等)
|
||||
if (this.status === 'connected' || this.status === 'connecting' || this.isConnecting) {
|
||||
return;
|
||||
if (this.ws && (this.status === 'connected' || this.status === 'connecting')) {
|
||||
if (this.status === 'connected' || this.status === 'connecting') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.isConnecting = true;
|
||||
this.setStatus('connecting');
|
||||
|
||||
let token = "";
|
||||
try {
|
||||
if (Platform.OS === 'web') {
|
||||
token = localStorage.getItem('token') || "";
|
||||
} else {
|
||||
token = await SecureStore.getItemAsync('token') || "";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('WebSocket: 获取 token 失败:', e);
|
||||
if (Platform.OS === 'web') {
|
||||
token = localStorage.getItem('token') || "";
|
||||
} else {
|
||||
token = await SecureStore.getItemAsync('token') || "";
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
console.error('WebSocket: 未找到认证 token,无法连接。');
|
||||
this.isConnecting = false;
|
||||
this.setStatus('disconnected');
|
||||
return;
|
||||
} else {
|
||||
console.log('WebSocket: 认证 token:', token);
|
||||
}
|
||||
|
||||
const url = `${WEBSOCKET_ENDPOINT}?token=${encodeURIComponent(token)}`;
|
||||
console.log('WebSocket: 开始连接到服务器');
|
||||
const ws = new WebSocket(url);
|
||||
this.ws = ws;
|
||||
const url = `${WEBSOCKET_ENDPOINT}?token=${token}`;
|
||||
console.log('WebSocket: 连接 URL:', url);
|
||||
this.ws = new WebSocket(url);
|
||||
|
||||
// 连接超时(15s)
|
||||
if (this.connectTimeoutId) {
|
||||
clearTimeout(this.connectTimeoutId);
|
||||
this.connectTimeoutId = null;
|
||||
}
|
||||
this.connectTimeoutId = setTimeout(() => {
|
||||
if (this.ws === ws && ws.readyState !== WebSocket.OPEN) {
|
||||
try { ws.close(); } catch { /* noop */ }
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
ws.onopen = () => {
|
||||
if (this.connectTimeoutId) {
|
||||
clearTimeout(this.connectTimeoutId);
|
||||
this.connectTimeoutId = null;
|
||||
}
|
||||
this.ws.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
this.setStatus('connected');
|
||||
this.reconnectAttempts = 0; // 重置重连尝试次数
|
||||
this.isConnecting = false;
|
||||
this.startPing();
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const message: WsMessage = JSON.parse(event.data);
|
||||
// console.log('WebSocket received message:', message)
|
||||
// 根据消息类型分发
|
||||
const eventListeners = this.messageListeners.get(message.type);
|
||||
if (eventListeners) {
|
||||
eventListeners.forEach(callback => callback(message));
|
||||
}
|
||||
// 可以在这里处理通用的消息,比如 Pong
|
||||
if (message.type === 'Pong') {
|
||||
// console.log('Received Pong');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理 WebSocket 消息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
if (this.connectTimeoutId) {
|
||||
clearTimeout(this.connectTimeoutId);
|
||||
this.connectTimeoutId = null;
|
||||
}
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('WebSocket 发生错误:', error);
|
||||
this.stopPing();
|
||||
this.isConnecting = false;
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (this.connectTimeoutId) {
|
||||
clearTimeout(this.connectTimeoutId);
|
||||
this.connectTimeoutId = null;
|
||||
}
|
||||
this.ws.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
this.ws = null;
|
||||
this.stopPing();
|
||||
this.isConnecting = false;
|
||||
// 只有在不是手动断开连接时才重连
|
||||
if (this.status !== 'disconnected') {
|
||||
this.setStatus('reconnecting');
|
||||
@ -168,8 +141,8 @@ class WebSocketManager {
|
||||
* @param message 要发送的消息对象,必须包含 type 字段。
|
||||
*/
|
||||
public send(message: WsMessage) {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
console.error('WebSocket 未连接或未就绪,无法发送消息。');
|
||||
if (this.status !== 'connected' || !this.ws) {
|
||||
console.error('WebSocket 未连接,无法发送消息。');
|
||||
return;
|
||||
}
|
||||
this.ws.send(JSON.stringify(message));
|
||||
|
||||
2
package-lock.json
generated
@ -74,7 +74,7 @@
|
||||
"react-native-render-html": "^6.3.4",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "~4.11.1",
|
||||
"react-native-svg": "15.11.2",
|
||||
"react-native-svg": "^15.11.2",
|
||||
"react-native-toast-message": "^2.3.0",
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-view-shot": "4.0.3",
|
||||
|
||||
@ -80,7 +80,7 @@
|
||||
"react-native-render-html": "^6.3.4",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "~4.11.1",
|
||||
"react-native-svg": "15.11.2",
|
||||
"react-native-svg": "^15.11.2",
|
||||
"react-native-toast-message": "^2.3.0",
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-view-shot": "4.0.3",
|
||||
|
||||