Compare commits

...

15 Commits

Author SHA1 Message Date
665ccc6132 enhance: ws util健壮性优化
All checks were successful
Dev Deploy / Explore-Gitea-Actions (push) Successful in 31s
2025-08-09 15:17:22 +08:00
c89b1df10a fix: 从ask页面返回闪退的问题 2025-08-09 15:09:06 +08:00
30d22715fd feat: 注释掉地区搜索
All checks were successful
Dev Deploy / Explore-Gitea-Actions (push) Successful in 31s
2025-08-08 19:16:51 +08:00
4afbc7fc8e feat: 优化 2025-08-08 19:05:43 +08:00
928d603f32 feat: 冲突合并
All checks were successful
Dev Deploy / Explore-Gitea-Actions (push) Successful in 30s
2025-08-06 19:19:44 +08:00
5befe1ab8f Merge branch 'v_1.0.1'
Some checks failed
Dev Deploy / Explore-Gitea-Actions (push) Failing after 1m27s
2025-08-06 19:15:07 +08:00
53efa37fb6 feat: ask页面 2025-08-06 18:57:18 +08:00
142c8e44b6 feat: ask hello页面 2025-08-06 16:33:34 +08:00
7c4d1529d4 feat: 个人信息优化 2025-08-06 15:45:54 +08:00
11ceca9753 feat: 邮箱登录 2025-08-06 14:01:52 +08:00
a8c5117cb7 feat: 隐私协议 (#10)
All checks were successful
Dev Deploy / Explore-Gitea-Actions (push) Successful in 28s
Reviewed-on: #10
2025-07-21 20:35:10 +08:00
a764210a61 fix/v0.5.0_bug (#9)
All checks were successful
Dev Deploy / Explore-Gitea-Actions (push) Successful in 27s
Co-authored-by: Junhui Chen <chenjunhui@fairclip.cn>
Reviewed-on: #9
2025-07-21 19:49:41 +08:00
cff3516aa2 fix: db for web
All checks were successful
Dev Deploy / Explore-Gitea-Actions (push) Successful in 27s
2025-07-21 17:20:34 +08:00
08571c543d feat: 支持页面
Some checks failed
Dev Deploy / Explore-Gitea-Actions (push) Failing after 22s
2025-07-21 17:19:02 +08:00
d2ec5d7bce feat: app icon 2025-07-21 17:18:48 +08:00
70 changed files with 1959 additions and 1292 deletions

View File

@ -46,7 +46,17 @@
"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

View File

@ -9,13 +9,16 @@ 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;
@ -55,6 +58,19 @@ export default function TabLayout() {
});
};
// 加载字体
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 => {

View File

@ -57,6 +57,7 @@ export default function AskScreen() {
if (translationX > threshold) {
// 从左向右滑动,跳转页面
runOnJS(router.replace)("/memo-list");
runOnJS(setConversationId)("")
}
})
.minPointers(1)
@ -244,18 +245,19 @@ export default function AskScreen() {
useFocusEffect(
useCallback(() => {
Keyboard.dismiss();
if (!sessionId) {
setIsHello(true);
setUserMessages([])
}
}, [sessionId])
}, [sessionId, Keyboard])
);
return (
<GestureDetector gesture={gesture}>
<View style={[styles.container, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
{/* 导航栏 */}
<View style={[styles.navbar, isHello && styles.hiddenNavbar]}>
<View style={[styles.navbar, { borderBottomWidth: isHello ? 0 : 1 }]}>
<TouchableOpacity
style={styles.backButton}
onPress={() => {
@ -273,11 +275,11 @@ export default function AskScreen() {
>
<ReturnArrow />
</TouchableOpacity>
<ThemedText style={styles.title} onPress={() => { router.push('/owner') }}>MemoWake</ThemedText>
<ThemedText style={[styles.title, { opacity: isHello ? 0 : 1 }]} onPress={() => { router.push('/owner') }}>MemoWake</ThemedText>
<View style={styles.placeholder} />
</View>
<View style={styles.contentContainer}>
<View style={[styles.contentContainer, { marginTop: isHello ? -24 : 0 }]}>
{/* 欢迎页面 */}
<Animated.View
style={[
@ -329,6 +331,7 @@ export default function AskScreen() {
setUserMessages={setUserMessages}
selectedImages={selectedImages}
setSelectedImages={setSelectedImages}
isHello={isHello}
/>
</View>
</KeyboardAvoidingView>
@ -346,7 +349,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 16,
paddingVertical: 8,
paddingHorizontal: 16,
borderBottomWidth: 1,
borderBottomColor: 'rgba(0,0,0,0.1)',
@ -355,11 +358,7 @@ const styles = StyleSheet.create({
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 1,
},
hiddenNavbar: {
shadowOpacity: 0,
elevation: 0,
opacity: 0
zIndex: 10
},
backButton: {
padding: 8,

View File

@ -1,291 +1,183 @@
import { Fonts } from '@/constants/Fonts';
import { checkAuthStatus } from '@/lib/auth';
import { useRouter } from 'expo-router';
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Animated, Dimensions, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
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] = useState(false);
// 获取屏幕宽度
const [isLoading, setIsLoading] = React.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;
// 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),
};
// 文本行动画值
const [textAnimations] = useState(() => ({
line1: new Animated.Value(0), // 第一行文本动画
line2: new Animated.Value(0), // 第二行文本动画
line3: new Animated.Value(0), // 第三行文本动画
subtitle: new Animated.Value(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 waveAnim = useRef(new Animated.Value(0)).current;
const waveAnimatedStyle = useAnimatedStyle(() => ({
transform: [
{ rotate: `${interpolate(waveAnim.value, [-1, 0, 1], [-15, 0, 15])}deg` },
],
}));
// 启动IP图标摇晃动画
const startShaking = () => {
// 停止任何正在进行的动画
if (animationRef.current) {
animationRef.current.stop();
}
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 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),
]);
const welcomeStyle = useAnimatedStyle(() => ({
opacity: fadeInAnim.value,
transform: [{ translateY: interpolate(fadeInAnim.value, [0, 1], [20, 0]) }]
}));
// 循环播放动画序列
animationRef.current = Animated.loop(sequence);
animationRef.current.start();
};
const descriptionStyle = useAnimatedStyle(() => ({
opacity: descriptionAnim.value,
transform: [{ translateY: interpolate(descriptionAnim.value, [0, 1], [20, 0]) }]
}));
// 启动文本动画
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 textLine1Style = useAnimatedStyle(() => ({
opacity: textAnimations.line1.value,
transform: [{ translateY: interpolate(textAnimations.line1.value, [0, 1], [10, 0]) }]
}));
// 启动描述文本动画
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 textLine2Style = useAnimatedStyle(() => ({
opacity: textAnimations.line2.value,
transform: [{ translateY: interpolate(textAnimations.line2.value, [0, 1], [10, 0]) }]
}));
// 启动按钮动画
const startButtonAnimation = () => {
// 首先淡入按钮
Animated.sequence([
Animated.timing(buttonAnim, {
toValue: 1,
duration: 800,
useNativeDriver: true,
})
]).start(() => {
// 淡入完成后开始循环摇晃动画
startButtonShakeLoop();
});
};
const textLine3Style = useAnimatedStyle(() => ({
opacity: textAnimations.line3.value,
transform: [{ translateY: interpolate(textAnimations.line3.value, [0, 1], [10, 0]) }]
}));
// 启动按钮循环摇晃动画
const startButtonShakeLoop = () => {
// 停止任何正在进行的动画
if (buttonLoopAnim.current) {
buttonLoopAnim.current.stop();
}
const subtitleStyle = useAnimatedStyle(() => ({
opacity: textAnimations.subtitle.value,
transform: [{ translateY: interpolate(textAnimations.subtitle.value, [0, 1], [10, 0]) }]
}));
// 创建摇晃动画序列
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();
};
// 组件挂载时启动动画
// Start animations
useEffect(() => {
setIsLoading(true);
checkAuthStatus(router, () => {
router.replace('/ask')
router.replace('/ask');
}, false).then(() => {
setIsLoading(false);
}).catch(() => {
setIsLoading(false);
});
// IP图标的淡入动画
Animated.timing(fadeAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}).start(() => {
// 淡入完成后开始摇晃动画
startShaking();
// IP显示后开始文本动画
startTextAnimations()
.then(() => startWelcomeAnimation())
.then(() => startDescriptionAnimation())
.then(() => startButtonAnimation())
.catch(console.error);
// 启动挥手动画
startWaveAnimation();
// 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 () => {
if (buttonLoopAnim.current) {
buttonLoopAnim.current.stop();
}
if (animationRef.current) {
animationRef.current.stop();
}
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);
};
}, []);
// 动画样式
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}>
@ -297,86 +189,27 @@ export default function HomeScreen() {
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]
})
}]
}
]}
>
<Animated.Text style={[styles.titleText, textLine1Style]}>
{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]
})
}]
}
]}
>
<Animated.Text style={[styles.titleText, textLine2Style]}>
{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]
})
}]
}
]}
>
<Animated.Text style={[styles.titleText, textLine3Style]}>
{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]
})
}]
}
]}
>
<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,
opacity: fadeInAnim,
transform: [{
translateY: fadeInAnim.interpolate({
inputRange: [0, 1],
outputRange: [20, 0]
})
}]
}]}
>
<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={{
@ -388,62 +221,25 @@ export default function HomeScreen() {
</Animated.View>
</View>
{/* Animated IP */}
<View style={styles.ipContainer}>
<Animated.View style={[styles.ipWrapper, { transform: [{ rotate }] }]}>
<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 }}
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]
})
}]
}
]}
>
<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",
opacity: buttonAnim,
transform: [
{
translateY: buttonAnim.interpolate({
inputRange: [0, 1],
outputRange: [20, 0]
})
},
{
translateX: buttonShakeAnim.interpolate({
inputRange: [-1, 0, 1],
outputRange: [-5, 0, 5]
})
}
]
}}
>
<Animated.View style={[{ alignItems: "center" }, buttonStyle]}>
<TouchableOpacity
style={styles.awakenButton}
onPress={async () => {
router.push('/login');
}}
onPress={() => router.push('/login')}
activeOpacity={0.8}
>
<Text style={styles.buttonText}>
@ -460,6 +256,7 @@ const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFB645',
fontFamily: 'english'
},
loadingContainer: {
flex: 1,
@ -484,17 +281,19 @@ const styles = StyleSheet.create({
},
titleText: {
color: '#FFFFFF',
fontSize: 30,
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',
@ -513,11 +312,12 @@ const styles = StyleSheet.create({
opacity: 0.9,
paddingHorizontal: 40,
marginTop: -16,
fontFamily: Fonts['inter']
},
awakenButton: {
backgroundColor: '#FFFFFF',
borderRadius: 28,
paddingVertical: 16,
paddingVertical: 20,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
@ -531,5 +331,6 @@ const styles = StyleSheet.create({
color: '#4C320C',
fontWeight: 'bold',
fontSize: 18,
fontFamily: Fonts['quicksand']
},
});

View File

@ -3,6 +3,7 @@ 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';
@ -27,6 +28,9 @@ 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',
@ -70,12 +74,15 @@ const LoginScreen = () => {
keyboardShouldPersistTaps="handled"
bounces={false}
>
<ThemedView className="flex-1 bg-bgPrimary justify-end">
<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>
<View className="flex-1">
<View
className="absolute left-1/2 z-10"
style={{
top: containerHeight > 0 ? windowHeight - containerHeight - 210 + statusBarHeight : 0,
top: containerHeight > 0 ? windowHeight - containerHeight - 210 + statusBarHeight - insets.top - 28 : 0,
transform: [{ translateX: -200 }, { translateY: keyboardOffset > 0 ? -keyboardOffset + statusBarHeight : -keyboardOffset }]
}}
>
@ -90,7 +97,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 : 0,
top: containerHeight > 0 ? windowHeight - containerHeight - 1 + statusBarHeight - insets.top - 30 : 0,
transform: [{ translateX: -39.5 }, { translateY: keyboardOffset > 0 ? -4 - keyboardOffset + statusBarHeight : -4 - keyboardOffset }]
}}
>
@ -98,10 +105,11 @@ const LoginScreen = () => {
</View>
</View>
<ThemedView
className="w-full bg-white pt-4 px-6 relative z-20 shadow-lg pb-5"
className="w-full pt-4 px-6 relative z-20 shadow-lg pb-5"
bgColor="textWhite"
style={{
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
borderTopLeftRadius: 50,
borderTopRightRadius: 50,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
@ -113,7 +121,7 @@ const LoginScreen = () => {
>
{/* 错误提示 */}
<View className={`${error !== "123" ? 'opacity-100' : 'opacity-0'} w-full flex justify-center items-center text-primary-500 text-sm`}>
<ThemedText className="text-sm !text-textPrimary">
<ThemedText size='xxs' color='bgSecondary' type='inter'>
{error}
</ThemedText>
</View>
@ -154,26 +162,29 @@ const LoginScreen = () => {
return components[status as keyof typeof components] || components.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' })}
<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' })}
</ThemedText>
</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' })}
<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' })}
</ThemedText>
</TouchableOpacity>
</View>
}
<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} />
</ThemedView>
</ThemedView>
</ScrollView>

View File

@ -13,6 +13,7 @@ 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';
@ -149,13 +150,7 @@ const MemoList = () => {
onPress={() => handleMemoPress(item)}
activeOpacity={0.7}
>
<View className="w-[3rem] h-[3rem] z-1">
<ChatSvg
width="100%"
height="100%"
preserveAspectRatio="xMidYMid meet"
/>
</View>
<ChatSvg />
<View style={styles.memoContent}>
<Text
style={styles.memoTitle}
@ -233,7 +228,7 @@ const MemoList = () => {
return (
<ErrorBoundary>
<View style={[styles.container, { paddingTop: insets.top }]}>
<View style={[styles.container, { paddingTop: insets.top + 8 }]}>
<FlatList
ref={flatListRef}
data={historyList}
@ -279,7 +274,7 @@ const styles = StyleSheet.create({
backgroundColor: '#fff',
},
headerContainer: {
paddingBottom: 16,
paddingBottom: 8,
backgroundColor: '#fff',
},
title: {
@ -287,7 +282,8 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
color: '#4C320C',
textAlign: 'center',
marginBottom: 16,
marginBottom: 8,
fontFamily: Fonts["quicksand"]
},
listContent: {
paddingBottom: Platform.select({
@ -316,16 +312,19 @@ const styles = StyleSheet.create({
flex: 1,
marginLeft: 12,
justifyContent: 'center',
gap: 2
},
memoTitle: {
fontSize: 16,
fontWeight: '500',
fontWeight: 'bold',
color: '#4C320C',
marginBottom: 4,
fontFamily: Fonts['sfPro']
},
memoSubtitle: {
fontSize: 14,
color: '#AC7E35',
fontFamily: Fonts['inter']
},
separator: {
height: 1 / PixelRatio.get(),

View File

@ -13,13 +13,14 @@ import { CountData, UserInfoDetails } from '@/types/user';
import { useFocusEffect, useRouter } from 'expo-router';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FlatList, StyleSheet, View } from 'react-native';
import { Dimensions, 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);
@ -119,17 +120,17 @@ export default function OwnerPage() {
<UserInfo userInfo={userInfoDetails} />
{/* 会员卡 */}
<MemberCard pro={userInfoDetails?.membership_level} />
<MemberCard pro={userInfoDetails?.membership_level} points={userInfoDetails?.remain_points} />
{/* 分类 */}
<View style={{ marginHorizontal: -16, marginBottom: -16 }}>
<View style={{ marginHorizontal: -16, marginBottom: -width * 0.26 }}>
<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={30} height={30} />} number={userInfoDetails.stories_count} />
<CreateCountComponent title={t("generalSetting.conversationsWithMemo", { ns: "personal" })} icon={<ConversationsSvg width={30} height={30} />} number={userInfoDetails.conversations_count} />
<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} />
</View>
{/* 排行榜 */}

View File

@ -37,9 +37,10 @@ export default function Rights() {
requestProducts,
ErrorCode
} = useIAP();
const { pro } = useLocalSearchParams<{
const { points, pro } = useLocalSearchParams<{
credit: string;
pro: string;
points: string;
pro: string
}>();
// 用户勾选协议
const [agree, setAgree] = useState<boolean>(false);
@ -215,9 +216,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%' }} />
<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/pro.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 }} />
)}
<View style={styles.cardContent}>
<View style={styles.cardinfo}>
@ -226,7 +227,7 @@ export default function Rights() {
</ThemedText>
<View style={styles.cardPoints}>
<StarSvg />
<ThemedText style={styles.cardPointsText}>{pro}</ThemedText>
<ThemedText style={styles.cardPointsText}>{points}</ThemedText>
</View>
</View>
</View>
@ -266,8 +267,6 @@ export default function Rights() {
padding: 16,
paddingBottom: 32,
backgroundColor: '#fff',
borderTopWidth: 1,
borderTopColor: '#eee',
position: 'absolute',
bottom: 0,
left: 0,
@ -303,8 +302,8 @@ export default function Rights() {
}}
activeOpacity={0.8}
>
<ThemedText style={{ color: '#fff', fontWeight: '700', fontSize: 14 }}>
{t('rights.subscribe', { ns: 'personal' })}
<ThemedText style={{ color: '#fff', fontWeight: '700', fontSize: 18 }}>
{t('rights.subscribe', { ns: 'personal' })} {payType?.split('_')[payType?.split('_')?.length - 1]}
</ThemedText>
</TouchableOpacity>
@ -365,8 +364,8 @@ const styles = StyleSheet.create({
},
goPay: {
backgroundColor: '#E2793F',
borderRadius: 24,
paddingVertical: 10,
borderRadius: 32,
paddingVertical: 16,
display: "flex",
alignItems: "center",
width: "100%",
@ -379,9 +378,9 @@ const styles = StyleSheet.create({
marginBottom: 16
},
switchButtonItem: {
width: "48%",
width: "47%",
borderRadius: 24,
paddingVertical: 6,
paddingVertical: 8,
display: "flex",
alignItems: "center",
borderWidth: 1
@ -390,7 +389,7 @@ const styles = StyleSheet.create({
marginHorizontal: 16,
marginVertical: 16,
padding: 16,
borderRadius: 12,
borderRadius: 32,
backgroundColor: '#fff',
shadowColor: '#000',
shadowOffset: {
@ -421,7 +420,7 @@ const styles = StyleSheet.create({
marginHorizontal: 16,
marginVertical: 16,
backgroundColor: '#FFB645',
borderRadius: 12,
borderRadius: 32,
},
cardContent: {
position: 'absolute',
@ -440,7 +439,7 @@ const styles = StyleSheet.create({
fontWeight: '700',
color: '#E2793F',
backgroundColor: '#fff',
paddingHorizontal: 8,
paddingHorizontal: 16,
paddingVertical: 2,
borderRadius: 20,
textAlign: 'center',
@ -450,12 +449,12 @@ const styles = StyleSheet.create({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: 4
gap: 8
},
cardPointsText: {
fontSize: 32,
fontWeight: '700',
color: '#4C320C',
lineHeight: 32
lineHeight: 36
}
});

View File

@ -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, UserInfoDetails } from '@/types/user';
import { Address, User } 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 = (props: { userInfo: UserInfoDetails }) => {
const Setting = () => {
const [userInfo, setUserInfo] = useState<User | null>(null);
const getUserInfo = async () => {
@ -210,7 +210,7 @@ const Setting = (props: { userInfo: UserInfoDetails }) => {
}, [])
return (
<View style={{ flex: 1, paddingTop: insets.top, marginBottom: insets.bottom }}>
<View style={{ flex: 1, paddingTop: insets.top, paddingBottom: insets.bottom, backgroundColor: '#fff' }}>
<Pressable
style={styles.centeredView}
>
@ -221,7 +221,7 @@ const Setting = (props: { userInfo: UserInfoDetails }) => {
<TouchableOpacity onPress={() => { router.push('/owner') }}>
<ReturnArrowSvg />
</TouchableOpacity>
<Text style={styles.modalTitle}>{t('generalSetting.allTitle', { ns: 'personal' })}</Text>
<ThemedText style={styles.modalTitle} >{t('generalSetting.allTitle', { ns: 'personal' })}</ThemedText>
<Text style={{ opacity: 0 }}>×</Text>
</View>
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
@ -269,11 +269,11 @@ const Setting = (props: { userInfo: UserInfoDetails }) => {
</View> */}
{/* 权限信息 */}
<View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('permission.permissionManagement', { ns: 'personal' })}</ThemedText>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }} type="sfPro">{t('permission.permissionManagement', { ns: 'personal' })}</ThemedText>
<View style={styles.content}>
{/* 相册权限 */}
<View style={styles.item}>
<ThemedText style={styles.itemText}>{t('permission.galleryAccess', { ns: 'personal' })}</ThemedText>
<ThemedText style={styles.itemText} type="sfPro">{t('permission.galleryAccess', { ns: 'personal' })}</ThemedText>
<CustomSwitch
isEnabled={albumEnabled}
toggleSwitch={toggleAlbum}
@ -284,7 +284,7 @@ const Setting = (props: { userInfo: UserInfoDetails }) => {
{/* 位置权限 */}
<View style={styles.item}>
<View>
<ThemedText style={styles.itemText}>{t('permission.locationPermission', { ns: 'personal' })}</ThemedText>
<ThemedText style={styles.itemText} type="sfPro">{t('permission.locationPermission', { ns: 'personal' })}</ThemedText>
</View>
<CustomSwitch
isEnabled={locationEnabled}
@ -294,7 +294,7 @@ const Setting = (props: { userInfo: UserInfoDetails }) => {
<Divider />
<View style={styles.item}>
<View>
<ThemedText style={styles.itemText}>{t('permission.pushNotification', { ns: 'personal' })}</ThemedText>
<ThemedText style={styles.itemText} type="sfPro">{t('permission.pushNotification', { ns: 'personal' })}</ThemedText>
</View>
<CustomSwitch
isEnabled={notificationsEnabled}
@ -335,62 +335,62 @@ const Setting = (props: { userInfo: UserInfoDetails }) => {
</View> */}
{/* 协议 */}
<View style={{ marginTop: 16 }}>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }}>{t('lcenses.title', { ns: 'personal' })}</ThemedText>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }} type="sfPro">{t('lcenses.title', { ns: 'personal' })}</ThemedText>
<View style={styles.content}>
<TouchableOpacity style={styles.item} onPress={() => { setModalType('privacy'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.privacyPolicy', { ns: 'personal' })}</ThemedText>
<ThemedText style={styles.itemText} type="sfPro">{t('lcenses.privacyPolicy', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => { setModalType('terms'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.applyPermission', { ns: 'personal' })}</ThemedText>
<ThemedText style={styles.itemText} type="sfPro">{t('lcenses.applyPermission', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => { setModalType('user'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.userAgreement', { ns: 'personal' })}</ThemedText>
<ThemedText style={styles.itemText} type="sfPro">{t('lcenses.userAgreement', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => { setModalType('ai'); setPrivacyModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.aiPolicy', { ns: 'personal' })}</ThemedText>
<ThemedText style={styles.itemText} type="sfPro">{t('lcenses.aiPolicy', { ns: 'personal' })}</ThemedText>
<RightArrowSvg />
</TouchableOpacity>
<Divider />
<TouchableOpacity style={styles.item} onPress={() => { setLcensesModalVisible(true) }} >
<ThemedText style={styles.itemText}>{t('lcenses.qualification', { ns: 'personal' })}</ThemedText>
<ThemedText style={styles.itemText} type="sfPro">{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}>{t('lcenses.ICP', { ns: 'personal' })}ICP备2025133004号-2A</ThemedText>
<ThemedText style={styles.itemText} type="sfPro">{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' }}>{t('generalSetting.otherInformation', { ns: 'personal' })}</ThemedText>
<ThemedText style={{ marginLeft: 16, marginVertical: 8, color: '#AC7E35', fontSize: 14, fontWeight: '600' }} type="sfPro">{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}>{t('generalSetting.contactUs', { ns: 'personal' })}</ThemedText>
<ThemedText style={styles.itemText} type="sfPro">{t('generalSetting.contactUs', { ns: 'personal' })}</ThemedText>
{/* <RightArrowSvg /> */}
</TouchableOpacity>
<Divider />
<View style={styles.item}>
<ThemedText style={styles.itemText}>{t('generalSetting.version', { ns: 'personal' })}</ThemedText>
<ThemedText style={styles.itemText} type="sfPro">{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' }}>{t('generalSetting.logout', { ns: 'personal' })}</ThemedText>
<ThemedText style={{ color: '#E2793F', fontSize: 14, fontWeight: '600' }} type="sfPro">{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' }}>{t('generalSetting.deleteAccount', { ns: 'personal' })}</ThemedText>
<ThemedText style={{ color: '#E2793F', fontSize: 14, fontWeight: '600' }} type="sfPro">{t('generalSetting.deleteAccount', { ns: 'personal' })}</ThemedText>
<DeleteSvg />
</TouchableOpacity>
</ScrollView>
@ -405,14 +405,11 @@ const Setting = (props: { userInfo: UserInfoDetails }) => {
const styles = StyleSheet.create({
centeredView: {
flex: 1,
justifyContent: 'flex-end',
backgroundColor: 'rgba(0,0,0,0.5)',
flex: 1
},
modalView: {
width: '100%',
height: '100%',
backgroundColor: 'white',
paddingHorizontal: 16,
},
modalHeader: {
@ -440,7 +437,8 @@ const styles = StyleSheet.create({
},
premium: {
backgroundColor: "#FAF9F6",
padding: 16,
paddingHorizontal: 16,
paddingVertical: 20,
borderRadius: 24,
flexDirection: 'row',
justifyContent: 'space-between',
@ -452,11 +450,11 @@ const styles = StyleSheet.create({
gap: 4,
backgroundColor: '#FAF9F6',
borderRadius: 24,
paddingVertical: 8
padding: 8
},
item: {
paddingHorizontal: 16,
paddingVertical: 8,
paddingVertical: 12,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',

View File

@ -1,4 +1,5 @@
import ArrowSvg from '@/assets/icons/svg/arrow.svg';
import DownSvg from '@/assets/icons/svg/down.svg';
import PlaceSvg from '@/assets/icons/svg/place.svg';
import ReturnArrowSvg from '@/assets/icons/svg/returnArrow.svg';
import { CascaderItem } from '@/components/cascader';
import ClassifyModal from '@/components/owner/classify';
@ -14,7 +15,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 { LayoutChangeEvent, Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
import { Keyboard, LayoutChangeEvent, Platform, StyleSheet, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native';
import { useSafeAreaInsets } from "react-native-safe-area-context";
interface LocationData {
id: number;
@ -25,6 +26,14 @@ interface LocationData {
export default function OwnerPage() {
const insets = useSafeAreaInsets();
const router = useRouter();
const HOT_CITIES = [
['北京', '上海', '广州', '深圳'],
['杭州', '成都', '乌鲁木齐', '武汉'],
['西安', '重庆', '西宁', '哈尔滨'],
['长沙', '南宁', '贵阳', '昆明']
];
// 位置搜索数据
const [locationSearch, setLocationSearch] = useState('');
// 位置弹窗
const [locationModalVisible, setLocationModalVisible] = useState(false);
// 分类弹窗
@ -161,36 +170,62 @@ export default function OwnerPage() {
};
}, [fetchLocationData]);
useEffect(() => {
// console.log(locationData);
}, [locationSearch])
return (
<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 : "地区"}
<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 : "分类"}
</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>
<TouchableOpacity onPress={() => { setClassifyModalVisible(true) }} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8 }}>
} */}
</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 }}>
<ThemedText style={{ color: selectedClassify?.length > 0 ? '#FFB645' : '#4C320C' }}>
{selectedClassify?.length > 0 ? selectedClassify[selectedClassify?.length - 1].name : "分类"}
</ThemedText>
@ -200,30 +235,56 @@ export default function OwnerPage() {
:
<ReturnArrowSvg style={{ transform: [{ rotate: '270deg' }], width: 12, height: 12 }} />
}
</TouchableOpacity>
</View>
{/* 颁奖台 */}
<PodiumComponent data={ranking} />
{/* 排名区域 */}
<RankList data={ranking} />
</TouchableOpacity> */}
{/* 地区选择弹窗 */}
<LocationModal
modalVisible={locationModalVisible}
setModalVisible={setLocationModalVisible}
podiumPosition={podiumPosition}
handleChange={handleLocationChange}
data={locationData}
/>
{/* 分类选择弹窗 */}
<ClassifyModal
data={classify}
modalVisible={classifyModalVisible}
setModalVisible={setClassifyModalVisible}
podiumPosition={podiumPosition}
handleChange={handleClassifyChange}
/>
</View>
</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}
/>
</View>
</TouchableWithoutFeedback>
);
}
@ -243,5 +304,46 @@ 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
}
});

Binary file not shown.

BIN
assets/fonts/Quicksand.otf Normal file

Binary file not shown.

BIN
assets/fonts/SF-Pro.otf Normal file

Binary file not shown.

View File

@ -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="chat_mask0_555_1115" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="60" height="60">
<mask id="mask0_106_1225" 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(#chat_mask0_555_1115)">
<g mask="url(#mask0_106_1225)">
<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.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"/>
<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"/>
</g>
<g filter="url(#filter1_i_555_1115)">
<ellipse cx="3.07711" cy="30.5" rx="31.3462" ry="22.5" fill="#FFF8DE"/>
<g filter="url(#filter1_i_106_1225)">
<ellipse cx="3.07662" cy="30.5001" rx="31.3462" ry="22.5" fill="#FFF8DE"/>
</g>
<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"/>
<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"/>
</g>
<defs>
<filter id="chat_filter0_i_555_1115" x="21.7313" y="8" width="66.5384" height="46.1538" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter0_i_106_1225" x="21.7318" y="8.00012" width="66.5387" 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_555_1115"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_106_1225"/>
</filter>
<filter id="chat_filter1_i_555_1115" x="-28.269" y="8" width="66.5385" height="45" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter1_i_106_1225" x="-28.2695" y="8.00012" 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_555_1115"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_106_1225"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 331 B

View File

@ -0,0 +1,19 @@
<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>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,4 @@
<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>

After

Width:  |  Height:  |  Size: 995 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 262 KiB

View File

@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,3 +1,4 @@
<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 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>

Before

Width:  |  Height:  |  Size: 296 B

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,31 +1,79 @@
import { StyleSheet, Text, type TextProps } from 'react-native';
import { StyleProp, StyleSheet, Text, TextStyle, 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';
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link' | 'sfPro' | 'inter';
weight?: FontWeight;
size?: FontSize;
radius?: FontSize
color?: ThemeColor | FontColor | ColorValue;
};
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 color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
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'],
};
return (
<Text
style={[
{ color },
baseStyle,
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}
@ -35,26 +83,41 @@ export function ThemedText({
const styles = StyleSheet.create({
default: {
fontSize: 16,
fontSize: Number(Fonts.base),
lineHeight: 24,
fontFamily: Fonts.quicksand,
},
defaultSemiBold: {
fontSize: 16,
fontSize: Number(Fonts.base),
lineHeight: 24,
fontWeight: '600',
},
title: {
fontSize: 32,
fontWeight: 'bold',
fontSize: Fonts['2xl'],
fontWeight: '700',
lineHeight: 32,
},
subtitle: {
fontSize: 20,
fontWeight: 'bold',
fontSize: Fonts.lg,
fontWeight: '600',
lineHeight: 28,
},
link: {
lineHeight: 30,
fontSize: 16,
fontSize: Fonts.sm,
lineHeight: 20,
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,
},
});

View File

@ -1,9 +1,32 @@
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, ...props }: ThemedViewProps) {
return <View className={className} style={style} {...props} />;
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} />;
}

View File

@ -1,9 +1,11 @@
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';
@ -52,6 +54,13 @@ 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 (

View File

@ -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 { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
import { Dimensions, Image, ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
import { createNewConversation } from "./utils";
interface AskHelloProps {
@ -14,6 +14,8 @@ 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);
@ -50,22 +52,20 @@ export default function AskHello({ setUserMessages, setConversationId, setIsHell
<ScrollView
contentContainerStyle={{
flexGrow: 1,
paddingHorizontal: 16,
paddingHorizontal: 8,
paddingBottom: 20
}}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
>
<View className="items-center">
<ThemedText style={{ fontSize: 32, fontWeight: 'bold', textAlign: 'center', lineHeight: 40, }}>
<ThemedText style={{ textAlign: 'center', lineHeight: 40, }} size="title" weight="bold">
{t('ask.hi', { ns: 'ask' })}
{"\n"}
{t('ask.iAmMemo', { ns: 'ask' })}
</ThemedText>
<View className="-mt-10">
<IP />
</View>
<ThemedText className="!text-textPrimary text-center -mt-20">
<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">
{t('ask.ready', { ns: 'ask' })}
{"\n"}
{t('ask.justAsk', { ns: 'ask' })}
@ -110,10 +110,15 @@ const styles = StyleSheet.create({
marginTop: 16
},
case: {
borderWidth: 2,
borderColor: "#FFB645",
borderRadius: 24,
borderWidth: 1,
borderColor: Fonts["textPrimary"],
borderRadius: 10,
paddingHorizontal: 8,
width: 'auto'
paddingVertical: 2,
width: 'auto',
fontSize: Fonts["sm"],
color: Fonts["textSecondary"],
fontFamily: Fonts["sfPro"]
}
})

View File

@ -12,6 +12,7 @@ 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';
@ -25,11 +26,12 @@ 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 } = props;
const { setIsHello, conversationId, setUserMessages, setConversationId, selectedImages, setSelectedImages, isHello } = props;
const { t } = useTranslation()
@ -189,32 +191,33 @@ export default function SendMessage(props: Props) {
}
]));
let currentSessionId = conversationId;
console.log("currentSessionIdcurrentSessionId", currentSessionId);
// 如果没有对话ID先创建一个新对话
if (!currentSessionId) {
currentSessionId = await createNewConversation(text);
setConversationId(currentSessionId);
webSocketManager.send({
type: 'Chat',
session_id: currentSessionId,
message: text,
image_material_ids: selectedImages.length > 0 ? selectedImages : undefined,
});
setSelectedImages([]);
const newCurrentSessionId = await createNewConversation(text);
if (newCurrentSessionId) {
setConversationId(newCurrentSessionId);
} else {
console.error("无法获取 session_id消息发送失败1。");
setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng'));
}
}
// 通过 WebSocket 发送消息
if (currentSessionId) {
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'));
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'));
}
}
// 将输入框清空
setInputValue('');
@ -243,19 +246,20 @@ export default function SendMessage(props: Props) {
return (
<View style={styles.container}>
<View className="relative w-full">
<ScrollView horizontal={true}>
<ScrollView horizontal={true} style={{ display: isHello ? 'flex' : 'none' }}>
<TouchableOpacity style={[styles.button, { borderColor: '#FFB645' }]} onPress={() => handleQuitly('search')}>
<SunSvg width={18} height={18} />
<ThemedText>{t("ask:ask.search")}</ThemedText>
</TouchableOpacity><TouchableOpacity style={[styles.button, { borderColor: '#E2793F' }]} onPress={() => handleQuitly('video')}>
<ThemedText type="sfPro" size="sm" weight='regular' color='textSecondary'>{t("ask:ask.search")}</ThemedText>
</TouchableOpacity>
<TouchableOpacity style={[styles.button, { borderColor: '#E2793F' }]} onPress={() => handleQuitly('video')}>
<VideoSvg width={18} height={18} />
<ThemedText>{t("ask:ask.video")}</ThemedText>
<ThemedText type="sfPro" size="sm" weight='regular' color='textSecondary'>{t("ask:ask.video")}</ThemedText>
</TouchableOpacity>
</ScrollView>
<TextInput
style={styles.input}
placeholder="Ask MeMo Anything..."
placeholderTextColor="#999"
placeholderTextColor={Fonts["textPrimary"]}
value={inputValue}
onChangeText={(text: string) => {
setInputValue(text);
@ -265,13 +269,14 @@ export default function SendMessage(props: Props) {
returnKeyType="send"
/>
<TouchableOpacity
style={[styles.voiceButton, { bottom: -10 }]}
onPress={handleSubmit}
className="absolute right-2"
style={{
position: 'absolute',
right: 6,
bottom: 9
}}
>
<View>
<SendSvg color={'white'} width={24} height={24} />
</View>
<SendSvg />
</TouchableOpacity>
</View>
</View>
@ -285,24 +290,35 @@ const styles = StyleSheet.create({
margin: 5,
borderRadius: 25,
alignItems: 'center',
borderWidth: 2,
borderWidth: 1,
display: 'flex',
flexDirection: 'row',
gap: 5
gap: 5,
},
container: {
justifyContent: 'center',
backgroundColor: '#transparent',
},
input: {
borderColor: '#FF9500',
color: Fonts["textPrimary"],
borderColor: '#AC7E35',
borderWidth: 1,
borderRadius: 25,
borderRadius: 28,
paddingHorizontal: 20,
paddingVertical: 13,
paddingVertical: 16,
lineHeight: 20,
fontSize: 16,
width: '100%', // 确保输入框宽度撑满
paddingRight: 50
width: '100%',
paddingRight: 50,
backgroundColor: '#fff',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.15,
shadowRadius: 3.84,
elevation: 5,
},
voiceButton: {
padding: 8,
@ -310,7 +326,6 @@ const styles = StyleSheet.create({
backgroundColor: '#FF9500',
justifyContent: 'center',
alignItems: 'center',
position: 'absolute',
transform: [{ translateY: -12 }],
marginRight: 8, // 添加一点右边距
},
});

View File

@ -3,7 +3,6 @@ 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';
// 实现一个函数,从两个数组中轮流插入新数组
@ -19,12 +18,12 @@ export const mergeArrays = (arr1: any[], arr2: any[]) => {
// 创建新对话并获取消息
export const createNewConversation = useCallback(async (user_text: string) => {
export const createNewConversation = async (user_text: string) => {
const data = await fetchApi<string>("/chat/new", {
method: "POST",
});
return data
}, []);
};
// 获取对话信息
export const getConversation = async ({

View File

@ -72,7 +72,7 @@ const CascaderComponent: React.FC<CascaderProps> = ({
// 渲染某一级选项
const renderLevel = (items: CascaderItem[], level: number) => {
return (
<View style={[styles.levelContainer, { width: columnWidth }]}>
<View style={[styles.levelContainer]}>
{items.map((item, index) => {
const isActive = selectedItems[level]?.name === item.name;
return (
@ -95,6 +95,7 @@ const CascaderComponent: React.FC<CascaderProps> = ({
textStyle,
isActive && [styles.activeText, activeTextStyle]
]}
type='sfPro'
>
{item.name}
</ThemedText>
@ -112,27 +113,46 @@ const CascaderComponent: React.FC<CascaderProps> = ({
// 渲染所有级联列
const renderColumns = () => {
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 }}
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 }
]
]}
>
{renderLevel(items, level)}
</ScrollView>
</View>
));
<ScrollView
style={{ flex: 1 }}
showsVerticalScrollIndicator={true}
contentContainerStyle={{ paddingBottom: 20 }}
>
{renderLevel(items, level)}
</ScrollView>
</View>
);
});
};
// 自定义显示内容
@ -166,15 +186,17 @@ const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
height: 300, // Set a fixed height for the container
height: 300,
},
scrollContent: {
flexGrow: 1,
height: '100%',
flexDirection: 'row',
},
column: {
height: '100%',
maxHeight: '100%',
flexShrink: 0,
},
columnWithDivider: {
borderRightWidth: 1,

View File

@ -1,11 +1,11 @@
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 DownloadSvg from "@/assets/icons/svg/download.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,18 +16,32 @@ interface MediaGridProps {
}
const MediaGrid = ({ mediaItems, setModalVisible, setCancel, cancel, t }: MediaGridProps) => {
// 只取前6个元素2行每行3个
// Only show up to 6 images (2 rows of 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 className="flex-row flex-wrap justify-between">
{displayItems.map((media) => (
<View style={[styles.container, getContainerStyle()]}>
{displayItems.map((media, index) => (
<Pressable
key={media.id}
onPress={() => {
setModalVisible({ visible: true, data: media });
}}
className="mb-2 w-[32%] aspect-square"
style={[styles.imageContainer, { width: getItemWidth() }]}
>
<ContextMenu
items={[
@ -51,24 +65,19 @@ const MediaGrid = ({ mediaItems, setModalVisible, setCancel, cancel, t }: MediaG
}
]}
cancel={cancel}
menuStyle={{
backgroundColor: 'white',
borderRadius: 8,
padding: 8,
minWidth: 150,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 5,
}}
menuStyle={styles.contextMenu}
>
<Image
source={{ uri: media?.url }}
className="w-full h-full rounded-xl"
style={styles.image}
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>
))}
@ -76,6 +85,50 @@ 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;

View File

@ -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-t-3xl !rounded-b-2xl' : '!rounded-3xl'} px-3`}
className={`${isUser ? '!bg-bgPrimary ml-10 rounded-full' : '!bg-aiBubble rounded-2xl'} border-0 ${!isUser && isMessageContainMedia(item) ? '!rounded-3xl' : '!rounded-3xl'} px-3`}
style={{ marginRight: getMessageText(item) == "keepSearchIng" ? 0 : isUser ? 0 : 10 }}
>
<MessageContent

View File

@ -3,9 +3,11 @@ 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 {
@ -40,23 +42,40 @@ const MessageItem = ({ setCancel, cancel = true, t, insets, item, sessionId, set
setSelectedImages={setSelectedImages}
setModalDetailsVisible={setModalDetailsVisible}
/>
{/* {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>
))}
{/* {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>
</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);

View File

@ -98,13 +98,7 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'white',
shadowColor: '#000',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 10,
right: 0
},
backgroundImage: {
width,
@ -122,18 +116,18 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
bottom: 0,
left: 0,
right: 0,
height: 80, // Set a fixed height for the navbar
height: 80,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 32,
backgroundColor: 'transparent', // Make sure it's transparent
backgroundColor: 'transparent',
},
centerButton: {
position: 'absolute',
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)
left: '50%',
top: -30,
transform: [{ translateX: -17 }],
width: 85,
height: 85,
justifyContent: 'center',
@ -149,8 +143,8 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
},
statusIndicator: {
position: 'absolute',
top: 15,
right: 15,
top: 3,
right: 20,
width: 10,
height: 10,
borderRadius: 5,
@ -168,7 +162,7 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
return (
<View style={styles.container}>
<Image source={require('@/assets/images/png/owner/ask.png')} style={{ width: width, height: 80, resizeMode: 'cover' }} />
<Image source={require('@/assets/images/png/owner/ask.png')} style={{ width: "100%" }} />
<View style={styles.navContainer}>
<TouchableOpacity
onPress={() => navigateTo('/memo-list')}
@ -186,7 +180,7 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
style={styles.centerButton}
>
<View style={styles.statusIndicator} />
<CenterButtonSvg />
<Image source={require('@/assets/images/png/owner/askIP.png')} />
</TouchableOpacity>
<TouchableOpacity

View File

@ -115,13 +115,13 @@ const Code = ({ phone }: CodeProps) => {
<View style={styles.container}>
<View style={styles.contentContainer}>
<View style={styles.headerContainer}>
<ThemedText style={styles.title}>
<ThemedText style={styles.title} color="textSecondary" size="xl" weight="bold">
{t("auth.telLogin.codeTitle", { ns: 'login' })}
</ThemedText>
<ThemedText style={styles.subtitle}>
<ThemedText style={styles.subtitle} type="sfPro" color="textPrimary" size="sm">
{t("auth.telLogin.secondTitle", { ns: 'login' })}
</ThemedText>
<ThemedText style={styles.phoneNumber}>
<ThemedText color="bgSecondary" size="sm" weight="bold">
{phone}
</ThemedText>
</View>
@ -144,13 +144,13 @@ const Code = ({ phone }: CodeProps) => {
/>
<View style={[styles.errorContainer, { opacity: error ? 1 : 0 }]}>
<Error />
<ThemedText style={styles.errorText}>
<ThemedText style={styles.errorText} size="xxs" color="bgSecondary" type="inter">
{error}
</ThemedText>
</View>
<View style={styles.footerContainer}>
<ThemedText style={styles.footerText}>
<ThemedText size="sm" color="textPrimary" type="sfPro">
{t("auth.telLogin.sendAgain", { ns: 'login' })}
</ThemedText>
<TouchableOpacity onPress={() => {
@ -158,10 +158,16 @@ const Code = ({ phone }: CodeProps) => {
sendVerificationCode()
}
}}>
<ThemedText style={[
styles.resendText,
countdown > 0 && styles.disabledResendText
]}>
<ThemedText
style={[
styles.resendText,
countdown > 0 && styles.disabledResendText
]}
size="sm"
color="bgSecondary"
type="inter"
weight="bold"
>
{countdown > 0 ? `${countdown}s${t("auth.telLogin.resend", { ns: 'login' })}` : t("auth.telLogin.resend", { ns: 'login' })}
</ThemedText>
</TouchableOpacity>
@ -185,23 +191,13 @@ 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,
@ -228,9 +224,6 @@ const styles = StyleSheet.create({
alignItems: 'center',
},
errorText: {
fontSize: 16,
fontWeight: '500',
color: '#E2793F',
marginLeft: 8,
},
footerContainer: {
@ -238,12 +231,7 @@ const styles = StyleSheet.create({
justifyContent: 'center',
marginTop: 8,
},
footerText: {
color: '#6B7280',
},
resendText: {
color: '#E2793F',
fontWeight: '500',
marginLeft: 4,
},
disabledResendText: {

View File

@ -1,9 +1,12 @@
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 { ActivityIndicator, StyleSheet, TextInput, TouchableOpacity, View } from "react-native";
import { StyleSheet, 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;
@ -69,45 +72,29 @@ const ForgetPwd = ({ setIsSignUp, updateUrlParam, setError }: LoginProps) => {
return (
<View style={styles.container}>
<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>
{/* 邮箱 */}
<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" })}
/>
{/* 返回登录 */}
<TouchableOpacity
style={styles.backButton}
onPress={handleBackToLogin}
>
<ThemedText style={styles.backButtonText}>
<ThemedText type='inter' color="bgSecondary" size="sm">
{t('auth.forgetPwd.goback', { ns: 'login' })}
</ThemedText>
</TouchableOpacity>
@ -123,16 +110,24 @@ const styles = StyleSheet.create({
marginBottom: 20,
},
inputLabel: {
fontSize: 16,
color: '#1F2937',
fontSize: Fonts['base'],
color: Fonts['textPrimary'],
fontWeight: Fonts['bold'],
fontFamily: Fonts['sfPro'],
marginBottom: 8,
marginLeft: 8,
},
textInput: {
borderRadius: 12,
padding: 12,
fontSize: 16,
backgroundColor: '#FFF8DE',
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'],
},
submitButton: {
width: '100%',
@ -151,11 +146,7 @@ const styles = StyleSheet.create({
backButton: {
alignSelf: 'center',
marginTop: 24,
},
backButtonText: {
color: '#1F2937',
fontSize: 14,
},
}
});
export default ForgetPwd;

View File

@ -1,14 +1,15 @@
import { Ionicons } from "@expo/vector-icons";
import { Fonts } from "@/constants/Fonts";
import { router } from "expo-router";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, StyleSheet, TextInput, TouchableOpacity, View } from "react-native";
import { StyleSheet, 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;
@ -22,7 +23,6 @@ const Login = ({ updateUrlParam, setError, setShowPassword, showPassword }: Logi
const [isLoading, setIsLoading] = useState(false);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [rememberMe, setRememberMe] = useState(false);
const handleLogin = async () => {
if (!email) {
@ -69,85 +69,73 @@ 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>
<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.email', { ns: 'login' })}
placeholder={t('auth.login.accountPlaceholder', { ns: 'login' })}
onChangeText={(text) => {
setEmail(text);
setError('123');
}}
autoCapitalize="none"
value={email}
/>
{/* 密码 */}
<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}>
<ThemedText style={styles.forgotPasswordText} color="textPrimary" type="inter">
{t('auth.login.forgotPassword', { ns: 'login' })}
</ThemedText>
</TouchableOpacity>
<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>
{/* 登录按钮 */}
<Button isLoading={isLoading} handleLogin={handleLogin} text={t('auth.login.loginButton', { ns: 'login' })} />
{/* 注册 */}
<View style={styles.signupContainer}>
<ThemedText style={styles.signupText}>
<ThemedText style={styles.signupText} type="sfPro">
{t('auth.login.signUpMessage', { ns: 'login' })}
</ThemedText>
<TouchableOpacity onPress={handleSignUp}>
<ThemedText style={styles.signupLink}>
<ThemedText style={styles.signupLink} type="sfPro">
{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>
);
};
@ -156,22 +144,43 @@ 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: 16,
color: '#1F2937',
fontSize: Fonts['base'],
color: Fonts['textPrimary'],
fontWeight: Fonts['bold'],
fontFamily: Fonts['sfPro'],
marginBottom: 8,
marginLeft: 8,
},
textInput: {
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 12,
fontSize: 16,
borderRadius: Fonts['xs'],
paddingHorizontal: Fonts['base'],
paddingVertical: Fonts['xs'],
fontSize: Fonts['sm'],
lineHeight: Fonts['base'],
textAlignVertical: 'center',
backgroundColor: '#FFF8DE'
backgroundColor: Fonts['bgInput'],
color: Fonts['textSecondary'],
fontFamily: Fonts['inter'],
paddingRight: Fonts['5xl'],
},
passwordInputContainer: {
position: 'relative',
@ -186,8 +195,8 @@ const styles = StyleSheet.create({
marginBottom: 24,
},
forgotPasswordText: {
color: '#1F2937',
fontSize: 14,
color: '#AC7E35',
fontSize: 11,
},
loginButton: {
width: '100%',
@ -200,6 +209,7 @@ const styles = StyleSheet.create({
loginButtonText: {
color: '#FFFFFF',
fontWeight: '600',
fontSize: 18,
},
signupContainer: {
flexDirection: 'row',
@ -207,14 +217,15 @@ const styles = StyleSheet.create({
marginTop: 8,
},
signupText: {
color: '#1F2937',
fontSize: 14,
color: '#AC7E35',
fontSize: Fonts['sm'],
},
signupLink: {
color: '#E2793F',
fontSize: 14,
fontSize: Fonts['sm'],
fontWeight: '600',
marginLeft: 4,
textDecorationLine: 'underline',
},
});

View File

@ -1,9 +1,9 @@
import { fetchApi } from "@/lib/server-api-util";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, TextInput, TouchableOpacity, View } from "react-native";
import { ThemedText } from "../ThemedText";
import { View } from "react-native";
import { Steps } from "./phoneLogin";
import Button from "./ui/Button";
import TextInput from "./ui/TextInput";
interface LoginProps {
setSteps: (steps: Steps) => void;
@ -18,67 +18,30 @@ const Phone = ({ setSteps, setPhone, phone, updateUrlParam }: LoginProps) => {
const [error, setError] = useState<string>('');
const sendVerificationCode = async () => {
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);
}
setSteps('code')
updateUrlParam("status", "code");
return
};
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
className="border border-gray-300 rounded-2xl p-3 text-base bg-inputBackground"
label={t('auth.telLogin.title', { ns: 'login' })}
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>
{/* 发送验证码 */}
<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>
<Button isLoading={isLoading} handleLogin={sendVerificationCode} text={t('auth.telLogin.sendCode', { ns: 'login' })} />
</View>
}

View File

@ -15,7 +15,9 @@ 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>
}

View File

@ -1,13 +1,16 @@
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 { ActivityIndicator, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
import { StyleSheet, 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;
@ -146,100 +149,53 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword, setSh
return (
<View style={styles.container}>
{/* 邮箱输入 */}
<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.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.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.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, { 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>
<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}
/>
{/* 注册按钮 */}
<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>
<Button isLoading={loading} handleLogin={handleSubmit} text={t("auth.signup.signupButton", { ns: 'login' })} />
<View style={styles.termsContainer}>
<TouchableOpacity
onPress={() => {
@ -259,68 +215,68 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword, setSh
]}
>
{checked && (
<Ionicons name="checkmark" size={14} color="white" />
<Ionicons name="checkmark" size={14} color={Fonts['textSecondary']} />
)}
</TouchableOpacity>
<View style={styles.termsTextContainer}>
<ThemedText style={styles.termsText}>
<ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.agree", { ns: 'login' })}
</ThemedText>
<TouchableOpacity onPress={() => {
setModalType('terms');
setPrivacyModalVisible(true);
}}>
<ThemedText style={styles.termsLink}>
<ThemedText style={styles.termsLink} type="sfPro">
{t("auth.telLogin.terms", { ns: 'login' })}
</ThemedText>
</TouchableOpacity>
<ThemedText style={styles.termsText}>
<ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.and", { ns: 'login' })}
</ThemedText>
<TouchableOpacity onPress={() => {
setModalType('privacy');
setPrivacyModalVisible(true);
}}>
<ThemedText style={styles.termsLink}>
<ThemedText style={styles.termsLink} type="sfPro">
{t("auth.telLogin.privacyPolicy", { ns: 'login' })}
</ThemedText>
</TouchableOpacity>
<ThemedText style={styles.termsText}>
<ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.and", { ns: 'login' })}
</ThemedText>
<TouchableOpacity onPress={() => {
setModalType('user');
setPrivacyModalVisible(true);
}}>
<ThemedText style={styles.termsLink}>
<ThemedText style={styles.termsLink} type="sfPro">
{t("auth.telLogin.userAgreement", { ns: 'login' })}
</ThemedText>
</TouchableOpacity>
<ThemedText style={styles.termsText}>
<ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.and", { ns: 'login' })}
</ThemedText>
<TouchableOpacity onPress={() => {
setModalType('ai');
setPrivacyModalVisible(true);
}}>
<ThemedText style={styles.termsLink}>
<ThemedText style={styles.termsLink} type="sfPro">
{t("auth.telLogin.aiAgreement", { ns: 'login' })}
</ThemedText>
</TouchableOpacity>
<ThemedText style={styles.termsText}>
<ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.agreement", { ns: 'login' })}
</ThemedText>
<ThemedText style={styles.termsLink}>
<ThemedText style={styles.termsLink} type="sfPro">
{t("common.name")}
</ThemedText>
<ThemedText style={styles.termsText}>
<ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.getPhone", { ns: 'login' })}
</ThemedText>
</View>
</View>
{/* 已有账号 */}
<View style={styles.loginContainer}>
<ThemedText style={styles.loginText}>
<View style={styles.loginContainer} >
<ThemedText type="sfPro" color="textPrimary" size="sm">
{t("auth.signup.haveAccount", { ns: 'login' })}
</ThemedText>
<TouchableOpacity
@ -328,7 +284,7 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword, setSh
updateUrlParam("status", "login");
}}
>
<ThemedText style={styles.loginLink}>
<ThemedText type="sfPro" color="bgSecondary" weight="bold" size="sm">
{t("auth.signup.login", { ns: 'login' })}
</ThemedText>
</TouchableOpacity>
@ -353,8 +309,9 @@ const styles = StyleSheet.create({
overflow: 'hidden',
},
inputLabel: {
fontSize: 16,
color: '#1F2937',
fontSize: 14,
color: '#AC7E35',
fontWeight: '600',
marginBottom: 8,
marginLeft: 8,
},
@ -394,17 +351,17 @@ const styles = StyleSheet.create({
checkbox: {
width: 20,
height: 20,
borderRadius: 10,
borderWidth: 2,
borderColor: '#E5E7EB',
borderRadius: 6,
borderWidth: 1,
borderColor: Fonts['textPrimary'],
justifyContent: 'center',
alignItems: 'center',
marginRight: 8,
marginTop: 2,
},
checkboxChecked: {
backgroundColor: '#E2793F',
borderColor: '#E2793F',
backgroundColor: Fonts["bgCheck"],
borderColor: Fonts['bgCheck'],
},
termsTextContainer: {
flexDirection: 'row',
@ -413,29 +370,20 @@ const styles = StyleSheet.create({
},
termsText: {
fontSize: 14,
color: '#1F2937',
color: Fonts["textPrimary"],
lineHeight: 20,
},
termsLink: {
fontSize: 14,
color: '#E2793F',
lineHeight: 20,
color: Fonts['bgSecondary'],
lineHeight: 20
},
loginContainer: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: 24,
},
loginText: {
fontSize: 14,
color: '#1F2937',
},
loginLink: {
color: '#E2793F',
fontSize: 14,
fontWeight: '600',
marginLeft: 4,
},
gap: 4,
}
});
export default SignUp;

View File

@ -0,0 +1,37 @@
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

View File

@ -0,0 +1,122 @@
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;

View File

@ -13,10 +13,10 @@ const AlbumComponent = ({ style }: CategoryProps) => {
return (
<View style={[styles.container, style]}>
<TouchableOpacity style={{ flex: 3 }}>
<TouchableOpacity style={{ flex: 3, opacity: 0 }}>
<ThemedText style={styles.text}>{t('generalSetting.album', { ns: 'personal' })}</ThemedText>
</TouchableOpacity>
<TouchableOpacity style={{ flex: 3 }}>
<TouchableOpacity style={{ flex: 3, opacity: 0 }}>
<ThemedText style={styles.text}>{t('generalSetting.shareProfile', { ns: 'personal' })}</ThemedText>
</TouchableOpacity>
<TouchableOpacity

View File

@ -22,6 +22,7 @@ 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 }
@ -44,7 +45,7 @@ function CarouselComponent(props: Props) {
}
const totleItem = (data: UserCountData) => {
return <View style={styles.container}>
return <View style={[styles.container, { width: width * 0.7 }]}>
{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' }}>
@ -68,37 +69,41 @@ function CarouselComponent(props: Props) {
}, [data]);
return (
<View style={{ flex: 1 }}>
<View
style={{
flex: 1
}}>
<Carousel
width={width}
height={width * 0.75}
height={width}
data={carouselDataValue || []}
mode="parallax"
// defaultIndex={
// carouselDataValue?.length
// ? Math.max(0, Math.min(
// carouselDataValue.length - 1,
// carouselDataValue.findIndex((item) => item?.key === 'total_count') - 1
// ))
// : 0
// }
onSnapToItem={(index) => setCurrentIndex(index)}
defaultIndex={
carouselDataValue?.length
? Math.max(0, Math.min(
carouselDataValue.length - 1,
carouselDataValue.findIndex((item) => item?.key === 'total_count') - 1
))
: 0
}
modeConfig={{
parallaxScrollingScale: 1,
parallaxScrollingOffset: 150,
parallaxScrollingOffset: 130,
parallaxAdjacentItemScale: 0.7
}}
renderItem={({ item, index }) => {
const style: ViewStyle = {
width: width,
height: width * 0.8,
alignItems: "center",
height: width * 0.7,
alignItems: "center"
};
return (
<View key={index} style={style}>
<View key={index} style={[style]}>
{item?.key === 'total_count' ? (
totleItem(item.value)
) : (
<View style={{ flex: 1, width: width * 0.65 }}>
<View>
{CategoryComponent({
title: item?.key,
data: [
@ -107,6 +112,7 @@ function CarouselComponent(props: Props) {
{ title: 'Length', number: formatDuration(item?.value?.video_length || 0) }
],
bgSvg: item?.value?.cover_url,
width: width
})}
</View>
)}
@ -124,13 +130,13 @@ const styles = StyleSheet.create({
justifyContent: 'center',
backgroundColor: '#fff',
borderRadius: 32,
padding: 4
padding: 6
},
container: {
backgroundColor: "#FFB645",
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 16,
paddingLeft: 32,
borderRadius: 32,
display: "flex",
flexDirection: "column",
position: 'relative',
@ -157,10 +163,10 @@ const styles = StyleSheet.create({
number: {
color: "#fff",
fontWeight: "700",
fontSize: 26,
fontSize: 28,
lineHeight: 30,
textAlign: 'left',
flex: 1,
paddingTop: 8
flex: 1
}
})

View File

@ -6,16 +6,45 @@ 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: string | number }[];
data: { title: string, number: { s: number, m: number, h: number } | number }[];
bgSvg: string | null;
style?: StyleProp<ViewStyle>;
width: number;
}
const CategoryComponent = ({ title, data, bgSvg, style }: CategoryProps) => {
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>
);
};
return (
<View style={[styles.container, style]}>
<View style={[styles.container, style, { width: width * 0.73 }]}>
<View style={styles.backgroundContainer}>
<Image
source={bgSvg !== "" && bgSvg !== null ? { uri: bgSvg } : require('@/assets/images/png/owner/people.png')}
@ -38,7 +67,19 @@ const CategoryComponent = ({ title, data, bgSvg, style }: CategoryProps) => {
</View>
<ThemedText style={styles.itemTitle}>{item.title}</ThemedText>
</View>
<ThemedText style={styles.itemNumber}>{item.number}</ThemedText>
<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>
</View>
))}
<View style={styles.titleContent}>
@ -68,7 +109,8 @@ const styles = StyleSheet.create({
backdropFilter: 'blur(5px)',
},
content: {
padding: 16,
padding: 32,
paddingRight: 16,
justifyContent: "space-between",
flex: 1
},
@ -78,6 +120,7 @@ const styles = StyleSheet.create({
justifyContent: 'center',
position: 'relative',
width: '100%',
paddingVertical: 4
},
title: {
color: 'white',
@ -95,7 +138,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 8,
paddingVertical: 16,
width: '100%',
},
itemTitle: {
@ -108,11 +151,10 @@ const styles = StyleSheet.create({
itemNumber: {
color: 'white',
fontSize: 28,
lineHeight: 30,
fontWeight: '700',
textAlign: 'left',
marginLeft: 8,
flex: 1,
paddingTop: 8
}
});

View File

@ -4,6 +4,7 @@ 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;
@ -39,9 +40,9 @@ const ClassifyModal = (props: ClassifyModalProps) => {
<View style={styles.modalView}>
<View style={styles.modalHeader}>
<Text style={{ opacity: 0 }}>Settings</Text>
<Text style={styles.modalTitle}>{t('generalSetting.classify', { ns: 'personal' })}</Text>
<ThemedText style={styles.modalTitle}>{t('generalSetting.classify', { ns: 'personal' })}</ThemedText>
<TouchableOpacity onPress={() => setModalVisible(false)}>
<Text style={styles.closeButton}>×</Text>
<ThemedText style={styles.closeButton}>×</ThemedText>
</TouchableOpacity>
</View>
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>

View File

@ -20,12 +20,10 @@ const CreateCountComponent = React.memo((props: CreateCountProps) => {
return (
<View style={containerStyle}>
<View style={styles.header}>
<View className="mt-1">
{props.icon}
</View>
<ThemedText style={styles.title} className="!text-textSecondary">{props.title}</ThemedText>
<ThemedText type="sfPro" weight="bold" color="textSecondary" size="xxs" style={{ lineHeight: 16 }}>{props.title}</ThemedText>
{props.icon}
</View>
<ThemedText style={styles.number} className="!text-textSecondary">{props.number}</ThemedText>
<ThemedText weight="bold" color="textSecondary" size="title" style={{ lineHeight: 36, textAlign: "right" }}>{props.number}</ThemedText>
</View>
);
});
@ -35,11 +33,12 @@ const styles = StyleSheet.create({
flex: 1,
display: "flex",
flexDirection: "column",
alignItems: "center",
// alignItems: "center",
justifyContent: "center",
gap: 8,
backgroundColor: "#FAF9F6",
padding: 16,
paddingVertical: 16,
paddingRight: 16,
borderRadius: 12,
shadowColor: "#000",
shadowOffset: {
@ -51,12 +50,12 @@ const styles = StyleSheet.create({
// elevation: 1,
},
header: {
width: "100%",
width: "90%",
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 8,
paddingHorizontal: 16,
alignItems: "flex-start",
gap: 4,
},
title: {
fontSize: 20,

View File

@ -36,6 +36,7 @@ const PodiumComponent = ({ data }: IPodium) => {
numberOfLines={1}
ellipsizeMode="tail"
style={styles.title}
type="inter"
>
{data[1]?.user_nick_name}
</ThemedText>
@ -65,6 +66,7 @@ const PodiumComponent = ({ data }: IPodium) => {
numberOfLines={2}
ellipsizeMode="tail"
style={styles.title}
type="inter"
>
{data[0]?.user_nick_name}
</ThemedText>
@ -94,6 +96,7 @@ const PodiumComponent = ({ data }: IPodium) => {
numberOfLines={1}
ellipsizeMode="tail"
style={styles.title}
type="inter"
>
{data[2]?.user_nick_name}
</ThemedText>

View File

@ -43,10 +43,9 @@ const styles = StyleSheet.create({
},
modalView: {
width: '100%',
height: '40%',
height: '50%',
backgroundColor: 'white',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
borderRadius: 26,
},
modalHeader: {
flexDirection: 'row',

View File

@ -126,8 +126,8 @@ const styles = StyleSheet.create({
width: '100%',
height: '80%',
backgroundColor: 'white',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
borderTopLeftRadius: 26,
borderTopRightRadius: 26,
paddingHorizontal: 16,
},
modalHeader: {

View File

@ -15,9 +15,9 @@ const RankList = (props: IRankList) => {
contentContainerStyle={{ flexGrow: 1, paddingHorizontal: 16, marginTop: 16 }}
>
<View style={styles.item}>
<ThemedText style={styles.headerText}>Rank</ThemedText>
<ThemedText style={styles.headerText}>Username</ThemedText>
<ThemedText style={styles.headerText}>Profile</ThemedText>
<ThemedText style={styles.headerText} type="sfPro">Rank</ThemedText>
<ThemedText style={styles.headerText} type="sfPro">Username</ThemedText>
<ThemedText style={styles.headerText} type="sfPro">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}>{index + 1}</ThemedText>
<ThemedText style={styles.itemName}>{item.user_nick_name}</ThemedText>
<ThemedText style={styles.itemRank} type="sfPro">{index + 1}</ThemedText>
<ThemedText style={styles.itemName} type="inter">{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: 14,
fontSize: 12,
color: "#4C320C",
fontWeight: "600"
fontWeight: "500"
},
item: {
flexDirection: 'row',
@ -85,7 +85,7 @@ const styles = StyleSheet.create({
fontWeight: "700"
},
itemName: {
fontSize: 14,
fontSize: 12,
color: "#AC7E35",
},
self: {

View File

@ -1,21 +1,24 @@
import MemberBgSvg from '@/assets/icons/svg/memberBg.svg';
import ProTextSvg from '@/assets/icons/svg/proText.svg';
import GradientText from '@/components/textLinear';
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';
const MemberCard = ({ pro }: { pro: string }) => {
const MemberCard = ({ pro, points }: { pro: string, points: number }) => {
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("/rights")}>
<TouchableOpacity style={[styles.container]} onPress={() => router.push({
pathname: '/rights',
params: {
points: points,
pro: pro
}
})}>
{/* 背景图 */}
<View style={[styles.cardBg, { opacity: pro === "pro" ? 1 : 0.5 }]}>
<CardBg pro={pro} date={"2025-09-05"} />
@ -29,20 +32,20 @@ const MemberCard = ({ pro }: { pro: string }) => {
<IpSvg pro={pro} />
</View>
{/* 会员标识 */}
<View style={[styles.memberContainer, { left: width * 0.25, top: width * 0.1, opacity: 1 }]}>
{/* <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> */}
{/* 解锁更多魔法 */}
<View style={{ position: "absolute", bottom: width * 0.02, left: -width * 0.01, opacity: pro === "pro" ? 1 : 0.5, width: width * 0.1, flexWrap: "wrap" }}>
<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")}
width={width * 0.4}
fontSize={16}
lineHeight={1.5}
color={[
{ offset: "0%", color: "#FF512F" },
{ offset: "100%", color: "#F09819" }
{ offset: "0%", color: "#D0BFB0" },
{ offset: "32.89%", color: "#FFE57D" },
{ offset: "81.1%", color: "#FFFFFF" }
]}
/>
</View>

View File

@ -50,7 +50,7 @@ const styles = StyleSheet.create({
goPro: {
backgroundColor: '#E2793F',
borderRadius: 24,
paddingVertical: 6,
paddingVertical: 12,
display: "flex",
alignItems: "center",
width: "100%",

View File

@ -64,10 +64,10 @@ const Premium = (props: Props) => {
</ThemedText>
<BlackStarSvg />
</View>
<ThemedText style={[styles.titleText, { fontSize: 16 }]}>
<ThemedText style={[styles.titleText, { fontSize: 16, marginTop: item?.product_code === bestValue ? 0 : -10 }]}>
{item.product_code?.split('_')[item.product_code?.split('_')?.length - 1]}
</ThemedText>
<ThemedText style={[styles.titleText, { fontSize: 32, lineHeight: 32 }]}>
<ThemedText style={[styles.titleText, { fontSize: 32, lineHeight: 32, paddingVertical: item?.product_code === bestValue ? 0 : 5 }]}>
$ {(Number(item.unit_price.amount) - Number(item.discount_amount.amount)).toFixed(2)}
</ThemedText>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>

View File

@ -79,11 +79,11 @@ const UserInfo = (props: UserInfoProps) => {
return (
<View style={styles.container}>
<View style={styles.info}>
<ThemedText style={styles.nickname}>{userInfo?.nickname}</ThemedText>
<ThemedText style={styles.userId}>{t('generalSetting.userId', { ns: 'personal' })}{userInfo?.user_id}</ThemedText>
<ThemedText style={styles.nickname} type="sfPro">{userInfo?.nickname}</ThemedText>
<ThemedText style={styles.userId} type="inter">{t('generalSetting.userId', { ns: 'personal' })}{userInfo?.user_id}</ThemedText>
<View style={styles.location}>
<LocationSvg />
<ThemedText style={styles.userId}>
<ThemedText style={styles.userId} type="inter">
{currentLocation?.country}-{currentLocation?.city}-{currentLocation?.district}
</ThemedText>
<TouchableOpacity

View File

@ -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, ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
import { Image, 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 className='flex flex-row justify-between items-center mt-[1rem] gap-[1rem] w-full'>
<View style={styles.container}>
{/* 头像 */}
<View className='w-auto'>
{userInfo?.user_info?.avatar_file_url && !imageError ? (
@ -30,45 +30,34 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
)}
</View>
{/* 用户名 */}
<View className='flex flex-col w-[75%] gap-1'>
<View className='flex flex-col w-[60%]'>
<View className='flex flex-row items-center justify-between w-full'>
<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',
}}
>
<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>
{
userInfo?.medal_infos?.map((item, index) => (
userInfo?.membership_level && (
<Image
key={index}
source={{ uri: item.url }}
source={require('@/assets/images/png/owner/proIcon.png')}
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: '80%' }}>
<View style={{ flex: 1, flexDirection: "row", alignItems: "center", gap: 2, maxWidth: '100%' }}>
<ThemedText
style={{
color: '#AC7E35',
@ -77,6 +66,7 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
flexShrink: 1,
flexGrow: 0,
}}
type="inter"
numberOfLines={1}
ellipsizeMode="tail"
>
@ -84,23 +74,57 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
</ThemedText>
<CopyButton textToCopy={userInfo?.user_info?.user_id || ""} />
</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>
<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
}}
type="inter"
numberOfLines={1}
ellipsizeMode="tail"
>
{userInfo?.remain_points}
</ThemedText>
</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',

View File

@ -0,0 +1,49 @@
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

View File

@ -1,89 +1,216 @@
import DoneSvg from '@/assets/icons/svg/done.svg';
import { router } from 'expo-router';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Platform, TouchableOpacity, View } from 'react-native';
import { Dimensions, Image, PixelRatio, Platform, StyleSheet, View } from 'react-native';
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withTiming
} from 'react-native-reanimated';
import { ThemedText } from '../ThemedText';
import { Fireworks } from '../firework';
import Lottie from '../lottie/lottie';
import StepButton from '../ui/button/stepButton';
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')
};
return (
<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>
{/* 前景动画 - 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>
}
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>
);
}
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
},
});

View File

@ -4,8 +4,10 @@ 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 { ActivityIndicator, Alert, Image, TouchableOpacity, View } from 'react-native';
import { Alert, Image, StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import FilesUploader from '../file-upload/files-uploader';
import StepButton from '../ui/button/stepButton';
interface Props {
setSteps?: (steps: Steps) => void;
@ -19,57 +21,51 @@ interface Props {
export default function Look(props: Props) {
const { fileData, setFileData, isLoading, handleUser, avatar } = props;
const { t } = useTranslation();
const insets = useSafeAreaInsets();
return (
<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]">
<View style={[styles.container, { paddingBottom: insets.bottom, paddingTop: insets.top + 28 }]}>
<View style={styles.contentContainer}>
<ThemedText style={styles.title}>
{t('auth.userMessage.look', { ns: 'login' })}
</ThemedText>
<ThemedText className="text-base !text-white/80 text-center mb-[2rem]">
<ThemedText style={styles.subtitle} type="inter" size="sm" weight="regular">
{t('auth.userMessage.avatarText', { ns: 'login' })}
{"\n"}
{t('auth.userMessage.avatorText2', { ns: 'login' })}
</ThemedText>
{
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 />
}
{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 />
)}
<FilesUploader
onUploadComplete={(fileData) => {
setFileData(fileData as FileUploadItem[]);
}}
showPreview={false}
children={
<View className="w-full rounded-full px-4 py-2 mt-4 items-center bg-inputBackground flex-row flex gap-2">
<View style={styles.uploadButton}>
<ChoicePhoto />
<ThemedText className="text-textTertiary text-lg font-semibold">
<ThemedText style={styles.uploadButtonText} type="sfPro" size="sm" weight="semiBold">
{t('auth.userMessage.choosePhoto', { ns: 'login' })}
</ThemedText>
</View>
}
/>
{/* <AutoUploadScreen /> */}
{/* <MediaStatsScreen /> */}
</View>
<View className="w-full">
<TouchableOpacity
className={`w-full bg-white rounded-full p-4 items-center ${isLoading ? 'opacity-70' : ''}`}
<View style={styles.footer}>
<StepButton
text={t('auth.userMessage.next', { ns: 'login' })}
onPress={() => {
if (fileData[0]?.preview || fileData[0]?.previewUrl || avatar) {
handleUser()
@ -77,17 +73,59 @@ export default function Look(props: Props) {
Alert.alert(t('auth.userMessage.avatarRequired', { ns: 'login' }))
}
}}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color="#000" />
) : (
<ThemedText className="text-textTertiary text-lg font-semibold">
{t('auth.userMessage.next', { ns: 'login' })}
</ThemedText>
)}
</TouchableOpacity>
isLoading={isLoading}
bg="#FFFFFF"
color="#4C320C"
/>
</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%',
}
});

View File

@ -2,7 +2,9 @@ import { Steps } from '@/app/(tabs)/user-message';
import { ThemedText } from '@/components/ThemedText';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ActivityIndicator, KeyboardAvoidingView, Platform, TextInput, TouchableOpacity, View } from 'react-native';
import { Dimensions, KeyboardAvoidingView, Platform, StyleSheet, TextInput, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import StepButton from '../ui/button/stepButton';
interface Props {
setSteps: (steps: Steps) => void;
@ -14,6 +16,8 @@ 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) {
@ -28,45 +32,98 @@ export default function UserName(props: Props) {
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
style={styles.keyboardAvoidingView}
>
<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 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>
<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 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>
<TextInput
className="bg-inputBackground rounded-2xl p-4 w-full"
style={[styles.textInput, { marginBottom: height * 0.2 }]}
placeholder={t('auth.userMessage.usernamePlaceholder', { ns: 'login' })}
placeholderTextColor="#9CA3AF"
value={username}
onChangeText={setUsername}
/>
</View>
<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>
<StepButton text={t('auth.userMessage.next', { ns: 'login' })} onPress={handleUserName} isLoading={isLoading} />
</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%',
}
});

View File

@ -3,9 +3,9 @@
* @param seconds
* @returns
*/
export function formatDuration(seconds: number): string {
export function formatDuration(seconds: number): { s: number, m: number, h: number } {
if (seconds < 60) {
return `${seconds}s`;
return { s: seconds, m: 0, h: 0 };
}
const minutes = Math.floor(seconds / 60);
@ -13,16 +13,16 @@ export function formatDuration(seconds: number): string {
if (minutes < 60) {
return remainingSeconds > 0
? `${minutes}min${remainingSeconds}s`
: `${minutes}min`;
? { s: remainingSeconds, m: minutes, h: 0 }
: { s: 0, m: minutes, h: 0 };
}
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
if (remainingMinutes === 0) {
return `${hours}h`;
return { s: 0, m: 0, h: hours };
}
return `${hours}h${remainingMinutes}min`;
return { s: seconds, m: minutes, h: hours };
}

43
constants/Fonts.ts Normal file
View File

@ -0,0 +1,43 @@
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';

View File

@ -14,9 +14,9 @@
"refresh": "Refresh",
"error": "have some error",
"issue": "have some issue",
"case1": "Find last year's baby/pet material",
"case2": "Find last year's food",
"case3": "Find recent travel material",
"case1": "Find last years baby moments",
"case2": "Pet moments",
"case3": "Show me my food memories in France with family",
"mediaAuth": "need album permission",
"mediaAuthDesc": "allow app to access album to save media files",
"saveSuccess": "save success",

View File

@ -67,13 +67,14 @@
"accountPlaceholder": "Enter your account or email",
"signUpMessage": "Dont have an account?",
"signUp": "Sign up",
"phoneLogin": "Phone Login"
"phoneLogin": "Phone Login",
"titleText": "Awake your Memo"
},
"agree": {
"logintext": "By logging in, you agree to our",
"singupText": "By signing up, you agree to our",
"terms": " Terms",
"join": "&",
"join": " and have read our",
"privacyPolicy": " Privacy Policy."
},
"welcome": {

View File

@ -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",
@ -128,6 +128,6 @@
},
"member": {
"goPremium": "Go Premium",
"unlock": "Unlock more memory magic"
"unlock": "解锁更多记忆魔法"
}
}

View File

@ -67,7 +67,8 @@
"accountPlaceholder": "请输入您的账号或邮箱",
"signUpMessage": "还没有账号?",
"signUp": "注册",
"phoneLogin": "手机号登录"
"phoneLogin": "手机号登录",
"titleText": "Awake your Memo"
},
"agree": {
"logintext": "登录即表示您同意我们的",

View File

@ -25,7 +25,7 @@ export interface PagedResult<T> {
// 获取.env文件中的变量
export const API_ENDPOINT = Constants.expoConfig?.extra?.API_ENDPOINT || "http://192.168.31.16:31646/api";
export const API_ENDPOINT = process.env.EXPO_PUBLIC_API_ENDPOINT || Constants.expoConfig?.extra?.API_ENDPOINT;
// 更新 access_token 的逻辑 - 用于React组件中

View File

@ -4,7 +4,7 @@ import { TFunction } from 'i18next';
import { Platform } from 'react-native';
// 从环境变量或默认值中定义 WebSocket 端点
export const WEBSOCKET_ENDPOINT = Constants.expoConfig?.extra?.WEBSOCKET_ENDPOINT || "ws://192.168.31.16:31646/ws/chat";
export const WEBSOCKET_ENDPOINT = process.env.EXPO_PUBLIC_WEBSOCKET_ENDPOINT || Constants.expoConfig?.extra?.WEBSOCKET_ENDPOINT;
export type WebSocketStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';
@ -34,6 +34,8 @@ 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() 方法来启动
@ -51,66 +53,91 @@ class WebSocketManager {
* 使 token
*/
public async connect() {
if (this.ws && (this.status === 'connected' || this.status === 'connecting')) {
if (this.status === 'connected' || this.status === 'connecting') {
return;
}
// 已连或正在连,直接返回(基于状态的幂等)
if (this.status === 'connected' || this.status === 'connecting' || this.isConnecting) {
return;
}
this.isConnecting = true;
this.setStatus('connecting');
let token = "";
if (Platform.OS === 'web') {
token = localStorage.getItem('token') || "";
} else {
token = await SecureStore.getItemAsync('token') || "";
try {
if (Platform.OS === 'web') {
token = localStorage.getItem('token') || "";
} else {
token = await SecureStore.getItemAsync('token') || "";
}
} catch (e) {
console.error('WebSocket: 获取 token 失败:', e);
}
if (!token) {
console.error('WebSocket: 未找到认证 token无法连接。');
this.isConnecting = false;
this.setStatus('disconnected');
return;
} else {
console.log('WebSocket: 认证 token:', token);
}
const url = `${WEBSOCKET_ENDPOINT}?token=${token}`;
console.log('WebSocket: 连接 URL:', url);
this.ws = new WebSocket(url);
const url = `${WEBSOCKET_ENDPOINT}?token=${encodeURIComponent(token)}`;
console.log('WebSocket: 开始连接到服务器');
const ws = new WebSocket(url);
this.ws = ws;
this.ws.onopen = () => {
// 连接超时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;
}
console.log('WebSocket connected');
this.setStatus('connected');
this.reconnectAttempts = 0; // 重置重连尝试次数
this.isConnecting = false;
this.startPing();
};
this.ws.onmessage = (event) => {
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);
}
};
this.ws.onerror = (error) => {
ws.onerror = (error) => {
if (this.connectTimeoutId) {
clearTimeout(this.connectTimeoutId);
this.connectTimeoutId = null;
}
console.error('WebSocket 发生错误:', error);
this.stopPing();
this.isConnecting = false;
};
this.ws.onclose = () => {
ws.onclose = () => {
if (this.connectTimeoutId) {
clearTimeout(this.connectTimeoutId);
this.connectTimeoutId = null;
}
console.log('WebSocket disconnected');
this.ws = null;
this.stopPing();
this.isConnecting = false;
// 只有在不是手动断开连接时才重连
if (this.status !== 'disconnected') {
this.setStatus('reconnecting');
@ -141,8 +168,8 @@ class WebSocketManager {
* @param message type
*/
public send(message: WsMessage) {
if (this.status !== 'connected' || !this.ws) {
console.error('WebSocket 未连接,无法发送消息。');
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
console.error('WebSocket 未连接或未就绪,无法发送消息。');
return;
}
this.ws.send(JSON.stringify(message));