Compare commits
2 Commits
928d603f32
...
30d22715fd
| Author | SHA1 | Date | |
|---|---|---|---|
| 30d22715fd | |||
| 4afbc7fc8e |
10
app.json
@ -46,6 +46,16 @@
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
"expo-router",
|
"expo-router",
|
||||||
"expo-secure-store",
|
"expo-secure-store",
|
||||||
|
[
|
||||||
|
"expo-font",
|
||||||
|
{
|
||||||
|
"fonts": [
|
||||||
|
"./assets/fonts/Quicksand.otf",
|
||||||
|
"./assets/fonts/SF-Pro.otf",
|
||||||
|
"./assets/fonts/Inter-Regular.otf"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"expo-background-task",
|
"expo-background-task",
|
||||||
{
|
{
|
||||||
|
|||||||
@ -9,13 +9,16 @@ import { prefetchChats } from '@/lib/prefetch';
|
|||||||
import { fetchApi } from '@/lib/server-api-util';
|
import { fetchApi } from '@/lib/server-api-util';
|
||||||
import { webSocketManager, WebSocketStatus } from '@/lib/websocket-util';
|
import { webSocketManager, WebSocketStatus } from '@/lib/websocket-util';
|
||||||
import { TransitionPresets } from '@react-navigation/bottom-tabs';
|
import { TransitionPresets } from '@react-navigation/bottom-tabs';
|
||||||
|
import { useFonts } from 'expo-font';
|
||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
import { Tabs } from 'expo-router';
|
import { Tabs } from 'expo-router';
|
||||||
import * as SecureStore from 'expo-secure-store';
|
import * as SecureStore from 'expo-secure-store';
|
||||||
|
import * as SplashScreen from 'expo-splash-screen';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
|
SplashScreen.preventAutoHideAsync();
|
||||||
interface PollingData {
|
interface PollingData {
|
||||||
title: string;
|
title: string;
|
||||||
id: 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(() => {
|
useEffect(() => {
|
||||||
const notificationListener = Notifications.addNotificationResponseReceivedListener(response => {
|
const notificationListener = Notifications.addNotificationResponseReceivedListener(response => {
|
||||||
|
|||||||
@ -57,6 +57,7 @@ export default function AskScreen() {
|
|||||||
if (translationX > threshold) {
|
if (translationX > threshold) {
|
||||||
// 从左向右滑动,跳转页面
|
// 从左向右滑动,跳转页面
|
||||||
runOnJS(router.replace)("/memo-list");
|
runOnJS(router.replace)("/memo-list");
|
||||||
|
runOnJS(setConversationId)("")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.minPointers(1)
|
.minPointers(1)
|
||||||
@ -244,11 +245,12 @@ export default function AskScreen() {
|
|||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
|
Keyboard.dismiss();
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
setIsHello(true);
|
setIsHello(true);
|
||||||
setUserMessages([])
|
setUserMessages([])
|
||||||
}
|
}
|
||||||
}, [sessionId])
|
}, [sessionId, Keyboard])
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -329,6 +331,7 @@ export default function AskScreen() {
|
|||||||
setUserMessages={setUserMessages}
|
setUserMessages={setUserMessages}
|
||||||
selectedImages={selectedImages}
|
selectedImages={selectedImages}
|
||||||
setSelectedImages={setSelectedImages}
|
setSelectedImages={setSelectedImages}
|
||||||
|
isHello={isHello}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
|||||||
@ -1,291 +1,183 @@
|
|||||||
|
import { Fonts } from '@/constants/Fonts';
|
||||||
import { checkAuthStatus } from '@/lib/auth';
|
import { checkAuthStatus } from '@/lib/auth';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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";
|
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() {
|
export default function HomeScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
|
||||||
// 获取屏幕宽度
|
|
||||||
const screenWidth = Dimensions.get('window').width;
|
const screenWidth = Dimensions.get('window').width;
|
||||||
|
|
||||||
// 动画值
|
// Animation values
|
||||||
const fadeAnim = useRef(new Animated.Value(0)).current; // IP图标的淡入动画
|
const fadeAnim = useSharedValue(0);
|
||||||
const shakeAnim = useRef(new Animated.Value(0)).current; // IP图标的摇晃动画
|
const shakeAnim = useSharedValue(0);
|
||||||
const animationRef = useRef<Animated.CompositeAnimation | null>(null); // 动画引用
|
const waveAnim = useSharedValue(0);
|
||||||
const descriptionAnim = useRef(new Animated.Value(0)).current; // 描述文本的淡入动画
|
const buttonShakeAnim = useSharedValue(0);
|
||||||
const buttonAnim = useRef(new Animated.Value(0)).current; // 按钮的淡入动画
|
const fadeInAnim = useSharedValue(0);
|
||||||
const buttonShakeAnim = useRef(new Animated.Value(0)).current; // 按钮的摇晃动画
|
const descriptionAnim = useSharedValue(0);
|
||||||
const buttonLoopAnim = useRef<Animated.CompositeAnimation | null>(null); // 按钮循环动画引用
|
const textAnimations = {
|
||||||
const fadeInAnim = useRef(new Animated.Value(0)).current;
|
line1: useSharedValue(0),
|
||||||
|
line2: useSharedValue(0),
|
||||||
|
line3: useSharedValue(0),
|
||||||
|
subtitle: useSharedValue(0),
|
||||||
|
};
|
||||||
|
|
||||||
// 文本行动画值
|
// Animation styles
|
||||||
const [textAnimations] = useState(() => ({
|
const ipAnimatedStyle = useAnimatedStyle(() => ({
|
||||||
line1: new Animated.Value(0), // 第一行文本动画
|
opacity: fadeAnim.value,
|
||||||
line2: new Animated.Value(0), // 第二行文本动画
|
transform: [
|
||||||
line3: new Animated.Value(0), // 第三行文本动画
|
{ translateX: interpolate(shakeAnim.value, [-1, 1], [-2, 2]) },
|
||||||
subtitle: new Animated.Value(0), // 副标题动画
|
{ rotate: `${interpolate(shakeAnim.value, [-1, 1], [-2, 2])}deg` },
|
||||||
|
],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 添加挥手动画值
|
const waveAnimatedStyle = useAnimatedStyle(() => ({
|
||||||
const waveAnim = useRef(new Animated.Value(0)).current;
|
transform: [
|
||||||
|
{ rotate: `${interpolate(waveAnim.value, [-1, 0, 1], [-15, 0, 15])}deg` },
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
// 启动IP图标摇晃动画
|
const buttonStyle = useAnimatedStyle(() => ({
|
||||||
const startShaking = () => {
|
opacity: fadeInAnim.value,
|
||||||
// 停止任何正在进行的动画
|
transform: [
|
||||||
if (animationRef.current) {
|
{ translateY: interpolate(fadeInAnim.value, [0, 1], [20, 0]) },
|
||||||
animationRef.current.stop();
|
{ translateX: interpolate(buttonShakeAnim.value, [-1, 0, 1], [-5, 0, 5]) }
|
||||||
}
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
// 创建动画序列
|
const welcomeStyle = useAnimatedStyle(() => ({
|
||||||
const sequence = Animated.sequence([
|
opacity: fadeInAnim.value,
|
||||||
// 第一次左右摇晃
|
transform: [{ translateY: interpolate(fadeInAnim.value, [0, 1], [20, 0]) }]
|
||||||
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 descriptionStyle = useAnimatedStyle(() => ({
|
||||||
animationRef.current = Animated.loop(sequence);
|
opacity: descriptionAnim.value,
|
||||||
animationRef.current.start();
|
transform: [{ translateY: interpolate(descriptionAnim.value, [0, 1], [20, 0]) }]
|
||||||
};
|
}));
|
||||||
|
|
||||||
// 启动文本动画
|
const textLine1Style = useAnimatedStyle(() => ({
|
||||||
const startTextAnimations = () => {
|
opacity: textAnimations.line1.value,
|
||||||
// 按顺序延迟启动每行文本动画
|
transform: [{ translateY: interpolate(textAnimations.line1.value, [0, 1], [10, 0]) }]
|
||||||
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 textLine2Style = useAnimatedStyle(() => ({
|
||||||
const startDescriptionAnimation = () => {
|
opacity: textAnimations.line2.value,
|
||||||
// IP图标显示后淡入描述文本
|
transform: [{ translateY: interpolate(textAnimations.line2.value, [0, 1], [10, 0]) }]
|
||||||
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 textLine3Style = useAnimatedStyle(() => ({
|
||||||
const startButtonAnimation = () => {
|
opacity: textAnimations.line3.value,
|
||||||
// 首先淡入按钮
|
transform: [{ translateY: interpolate(textAnimations.line3.value, [0, 1], [10, 0]) }]
|
||||||
Animated.sequence([
|
}));
|
||||||
Animated.timing(buttonAnim, {
|
|
||||||
toValue: 1,
|
|
||||||
duration: 800,
|
|
||||||
useNativeDriver: true,
|
|
||||||
})
|
|
||||||
]).start(() => {
|
|
||||||
// 淡入完成后开始循环摇晃动画
|
|
||||||
startButtonShakeLoop();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 启动按钮循环摇晃动画
|
const subtitleStyle = useAnimatedStyle(() => ({
|
||||||
const startButtonShakeLoop = () => {
|
opacity: textAnimations.subtitle.value,
|
||||||
// 停止任何正在进行的动画
|
transform: [{ translateY: interpolate(textAnimations.subtitle.value, [0, 1], [10, 0]) }]
|
||||||
if (buttonLoopAnim.current) {
|
}));
|
||||||
buttonLoopAnim.current.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建摇晃动画序列
|
// Start animations
|
||||||
const shakeSequence = Animated.sequence([
|
|
||||||
// 向右摇晃
|
|
||||||
Animated.timing(buttonShakeAnim, {
|
|
||||||
toValue: 1,
|
|
||||||
duration: 100,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
// 向左摇晃
|
|
||||||
Animated.timing(buttonShakeAnim, {
|
|
||||||
toValue: -1,
|
|
||||||
duration: 100,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
// 再次向右摇晃
|
|
||||||
Animated.timing(buttonShakeAnim, {
|
|
||||||
toValue: 1,
|
|
||||||
duration: 100,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
// 回到中心位置
|
|
||||||
Animated.timing(buttonShakeAnim, {
|
|
||||||
toValue: 0,
|
|
||||||
duration: 100,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
// 暂停3秒
|
|
||||||
Animated.delay(3000)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 循环播放动画序列
|
|
||||||
buttonLoopAnim.current = Animated.loop(shakeSequence);
|
|
||||||
buttonLoopAnim.current.start();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 启动挥手动画
|
|
||||||
const startWaveAnimation = () => {
|
|
||||||
// 创建循环动画:左右摇摆
|
|
||||||
Animated.loop(
|
|
||||||
Animated.sequence([
|
|
||||||
Animated.timing(waveAnim, {
|
|
||||||
toValue: 1,
|
|
||||||
duration: 500,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
Animated.timing(waveAnim, {
|
|
||||||
toValue: -1,
|
|
||||||
duration: 500,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
Animated.timing(waveAnim, {
|
|
||||||
toValue: 0,
|
|
||||||
duration: 500,
|
|
||||||
useNativeDriver: true,
|
|
||||||
}),
|
|
||||||
Animated.delay(1000), // 暂停1秒
|
|
||||||
])
|
|
||||||
).start();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 组件挂载时启动动画
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
checkAuthStatus(router, () => {
|
checkAuthStatus(router, () => {
|
||||||
router.replace('/ask')
|
router.replace('/ask');
|
||||||
}, false).then(() => {
|
}, false).then(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
// IP图标的淡入动画
|
|
||||||
Animated.timing(fadeAnim, {
|
// Start fade in animation
|
||||||
toValue: 1,
|
fadeAnim.value = withTiming(1, { duration: 1000 }, () => {
|
||||||
duration: 1000,
|
// Start shake animation
|
||||||
useNativeDriver: true,
|
shakeAnim.value = runShakeAnimation(shakeAnim);
|
||||||
}).start(() => {
|
|
||||||
// 淡入完成后开始摇晃动画
|
// Start text animations with delays
|
||||||
startShaking();
|
textAnimations.line1.value = withDelay(0, withTiming(1, { duration: 500 }));
|
||||||
// IP显示后开始文本动画
|
textAnimations.line2.value = withDelay(300, withTiming(1, { duration: 500 }));
|
||||||
startTextAnimations()
|
textAnimations.line3.value = withDelay(600, withTiming(1, { duration: 500 }));
|
||||||
.then(() => startWelcomeAnimation())
|
textAnimations.subtitle.value = withDelay(900, withTiming(1, { duration: 500 }));
|
||||||
.then(() => startDescriptionAnimation())
|
|
||||||
.then(() => startButtonAnimation())
|
// Start welcome animation
|
||||||
.catch(console.error);
|
fadeInAnim.value = withDelay(200, withTiming(1, { duration: 800 }));
|
||||||
// 启动挥手动画
|
|
||||||
startWaveAnimation();
|
// 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 () => {
|
return () => {
|
||||||
if (buttonLoopAnim.current) {
|
fadeAnim.value = 0;
|
||||||
buttonLoopAnim.current.stop();
|
shakeAnim.value = 0;
|
||||||
}
|
waveAnim.value = 0;
|
||||||
if (animationRef.current) {
|
buttonShakeAnim.value = 0;
|
||||||
animationRef.current.stop();
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
@ -297,86 +189,27 @@ export default function HomeScreen() {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={[styles.contentContainer, { paddingTop: insets.top + 16 }]}>
|
<View style={[styles.contentContainer, { paddingTop: insets.top + 16 }]}>
|
||||||
{/* 标题区域 */}
|
|
||||||
<View style={styles.headerContainer}>
|
<View style={styles.headerContainer}>
|
||||||
<Animated.Text
|
<Animated.Text style={[styles.titleText, textLine1Style]}>
|
||||||
style={[
|
|
||||||
styles.titleText,
|
|
||||||
{
|
|
||||||
opacity: textAnimations.line1, transform: [{
|
|
||||||
translateY: textAnimations.line1.interpolate({
|
|
||||||
inputRange: [0, 1],
|
|
||||||
outputRange: [10, 0]
|
|
||||||
})
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{t('auth.welcomeAwaken.awaken', { ns: 'login' })}
|
{t('auth.welcomeAwaken.awaken', { ns: 'login' })}
|
||||||
</Animated.Text>
|
</Animated.Text>
|
||||||
<Animated.Text
|
<Animated.Text style={[styles.titleText, textLine2Style]}>
|
||||||
style={[
|
|
||||||
styles.titleText,
|
|
||||||
{
|
|
||||||
opacity: textAnimations.line2, transform: [{
|
|
||||||
translateY: textAnimations.line2.interpolate({
|
|
||||||
inputRange: [0, 1],
|
|
||||||
outputRange: [10, 0]
|
|
||||||
})
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{t('auth.welcomeAwaken.your', { ns: 'login' })}
|
{t('auth.welcomeAwaken.your', { ns: 'login' })}
|
||||||
</Animated.Text>
|
</Animated.Text>
|
||||||
<Animated.Text
|
<Animated.Text style={[styles.titleText, textLine3Style]}>
|
||||||
style={[
|
|
||||||
styles.titleText,
|
|
||||||
{
|
|
||||||
opacity: textAnimations.line3, transform: [{
|
|
||||||
translateY: textAnimations.line3.interpolate({
|
|
||||||
inputRange: [0, 1],
|
|
||||||
outputRange: [10, 0]
|
|
||||||
})
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{t('auth.welcomeAwaken.pm', { ns: 'login' })}
|
{t('auth.welcomeAwaken.pm', { ns: 'login' })}
|
||||||
</Animated.Text>
|
</Animated.Text>
|
||||||
<Animated.Text
|
<Animated.Text style={[styles.subtitleText, subtitleStyle]}>
|
||||||
style={[
|
|
||||||
styles.subtitleText,
|
|
||||||
{
|
|
||||||
opacity: textAnimations.subtitle,
|
|
||||||
transform: [{
|
|
||||||
translateY: textAnimations.subtitle.interpolate({
|
|
||||||
inputRange: [0, 1],
|
|
||||||
outputRange: [10, 0]
|
|
||||||
})
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{t('auth.welcomeAwaken.slogan', { ns: 'login' })}
|
{t('auth.welcomeAwaken.slogan', { ns: 'login' })}
|
||||||
</Animated.Text>
|
</Animated.Text>
|
||||||
</View>
|
</View>
|
||||||
{/* 欢迎语 */}
|
|
||||||
<View style={{ alignItems: 'flex-end' }}>
|
<View style={{ alignItems: 'flex-end' }}>
|
||||||
<Animated.View
|
<Animated.View style={[{
|
||||||
style={[{
|
|
||||||
height: screenWidth * 0.3,
|
height: screenWidth * 0.3,
|
||||||
width: screenWidth * 0.3,
|
width: screenWidth * 0.3,
|
||||||
marginTop: -screenWidth * 0.08,
|
marginTop: -screenWidth * 0.08,
|
||||||
opacity: fadeInAnim,
|
}, welcomeStyle]}>
|
||||||
transform: [{
|
|
||||||
translateY: fadeInAnim.interpolate({
|
|
||||||
inputRange: [0, 1],
|
|
||||||
outputRange: [20, 0]
|
|
||||||
})
|
|
||||||
}]
|
|
||||||
}]}
|
|
||||||
>
|
|
||||||
<Image
|
<Image
|
||||||
source={require('@/assets/images/png/icon/think.png')}
|
source={require('@/assets/images/png/icon/think.png')}
|
||||||
style={{
|
style={{
|
||||||
@ -388,9 +221,8 @@ export default function HomeScreen() {
|
|||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Animated IP */}
|
|
||||||
<View style={styles.ipContainer}>
|
<View style={styles.ipContainer}>
|
||||||
<Animated.View style={[styles.ipWrapper, { transform: [{ rotate }] }]}>
|
<Animated.View style={[styles.ipWrapper, waveAnimatedStyle]}>
|
||||||
<Image
|
<Image
|
||||||
source={require('@/assets/images/png/icon/ip.png')}
|
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 }}
|
||||||
@ -398,52 +230,16 @@ export default function HomeScreen() {
|
|||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 介绍文本 */}
|
<Animated.Text style={[styles.descriptionText, descriptionStyle]}>
|
||||||
<Animated.Text
|
|
||||||
style={[
|
|
||||||
styles.descriptionText,
|
|
||||||
{
|
|
||||||
opacity: descriptionAnim,
|
|
||||||
transform: [{
|
|
||||||
translateY: descriptionAnim.interpolate({
|
|
||||||
inputRange: [0, 1],
|
|
||||||
outputRange: [20, 0]
|
|
||||||
})
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{t('auth.welcomeAwaken.gallery', { ns: 'login' })}
|
{t('auth.welcomeAwaken.gallery', { ns: 'login' })}
|
||||||
{"\n"}
|
{"\n"}
|
||||||
{t('auth.welcomeAwaken.back', { ns: 'login' })}
|
{t('auth.welcomeAwaken.back', { ns: 'login' })}
|
||||||
</Animated.Text>
|
</Animated.Text>
|
||||||
|
|
||||||
{/* 唤醒按钮 */}
|
<Animated.View style={[{ alignItems: "center" }, buttonStyle]}>
|
||||||
<Animated.View
|
|
||||||
style={{
|
|
||||||
alignItems: "center",
|
|
||||||
opacity: buttonAnim,
|
|
||||||
transform: [
|
|
||||||
{
|
|
||||||
translateY: buttonAnim.interpolate({
|
|
||||||
inputRange: [0, 1],
|
|
||||||
outputRange: [20, 0]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
translateX: buttonShakeAnim.interpolate({
|
|
||||||
inputRange: [-1, 0, 1],
|
|
||||||
outputRange: [-5, 0, 5]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.awakenButton}
|
style={styles.awakenButton}
|
||||||
onPress={async () => {
|
onPress={() => router.push('/login')}
|
||||||
router.push('/login');
|
|
||||||
}}
|
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<Text style={styles.buttonText}>
|
<Text style={styles.buttonText}>
|
||||||
@ -460,6 +256,7 @@ const styles = StyleSheet.create({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#FFB645',
|
backgroundColor: '#FFB645',
|
||||||
|
fontFamily: 'english'
|
||||||
},
|
},
|
||||||
loadingContainer: {
|
loadingContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@ -484,17 +281,19 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
titleText: {
|
titleText: {
|
||||||
color: '#FFFFFF',
|
color: '#FFFFFF',
|
||||||
fontSize: 30,
|
fontSize: 32,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
lineHeight: 36,
|
lineHeight: 36,
|
||||||
|
fontFamily: Fonts['quicksand']
|
||||||
},
|
},
|
||||||
subtitleText: {
|
subtitleText: {
|
||||||
color: 'rgba(255, 255, 255, 0.85)',
|
color: 'rgba(255, 255, 255, 0.85)',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
lineHeight: 24,
|
lineHeight: 24,
|
||||||
|
fontFamily: Fonts['inter']
|
||||||
},
|
},
|
||||||
ipContainer: {
|
ipContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -513,11 +312,12 @@ const styles = StyleSheet.create({
|
|||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
paddingHorizontal: 40,
|
paddingHorizontal: 40,
|
||||||
marginTop: -16,
|
marginTop: -16,
|
||||||
|
fontFamily: Fonts['inter']
|
||||||
},
|
},
|
||||||
awakenButton: {
|
awakenButton: {
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: '#FFFFFF',
|
||||||
borderRadius: 28,
|
borderRadius: 28,
|
||||||
paddingVertical: 16,
|
paddingVertical: 20,
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.1,
|
||||||
@ -531,5 +331,6 @@ const styles = StyleSheet.create({
|
|||||||
color: '#4C320C',
|
color: '#4C320C',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
|
fontFamily: Fonts['quicksand']
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -74,16 +74,16 @@ const LoginScreen = () => {
|
|||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
bounces={false}
|
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 }}>
|
<View style={{ width: "100%", alignItems: "center", marginTop: insets.top + 8, opacity: keyboardOffset === 0 ? 1 : 0 }}>
|
||||||
<ThemedText style={{ fontSize: 20, fontWeight: 'bold', color: "#fff" }}>Awake your Memo</ThemedText>
|
<ThemedText size="xl" weight="bold" color="textWhite">{t('login:auth.login.titleText')}</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
<View
|
<View
|
||||||
className="absolute left-1/2 z-10"
|
className="absolute left-1/2 z-10"
|
||||||
style={{
|
style={{
|
||||||
top: containerHeight > 0 ? windowHeight - containerHeight - 210 + statusBarHeight - insets.top - 28 : 0,
|
top: containerHeight > 0 ? windowHeight - containerHeight - 210 + statusBarHeight - insets.top - 28 : 0,
|
||||||
transform: [{ translateX: -200 }, { translateY: keyboardOffset > 0 ? -keyboardOffset + statusBarHeight - insets.top - 28 : -keyboardOffset }]
|
transform: [{ translateX: -200 }, { translateY: keyboardOffset > 0 ? -keyboardOffset + statusBarHeight : -keyboardOffset }]
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
@ -98,14 +98,15 @@ const LoginScreen = () => {
|
|||||||
className="absolute left-1/2 z-[1000] -translate-x-[39.5px] -translate-y-[4px]"
|
className="absolute left-1/2 z-[1000] -translate-x-[39.5px] -translate-y-[4px]"
|
||||||
style={{
|
style={{
|
||||||
top: containerHeight > 0 ? windowHeight - containerHeight - 1 + statusBarHeight - insets.top - 30 : 0,
|
top: containerHeight > 0 ? windowHeight - containerHeight - 1 + statusBarHeight - insets.top - 30 : 0,
|
||||||
transform: [{ translateX: -39.5 }, { translateY: keyboardOffset > 0 ? -4 - keyboardOffset + statusBarHeight - insets.top - 30 : -4 - keyboardOffset }]
|
transform: [{ translateX: -39.5 }, { translateY: keyboardOffset > 0 ? -4 - keyboardOffset + statusBarHeight : -4 - keyboardOffset }]
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Handers />
|
<Handers />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<ThemedView
|
<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={{
|
style={{
|
||||||
borderTopLeftRadius: 50,
|
borderTopLeftRadius: 50,
|
||||||
borderTopRightRadius: 50,
|
borderTopRightRadius: 50,
|
||||||
@ -120,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`}>
|
<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}
|
{error}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
@ -161,22 +162,22 @@ const LoginScreen = () => {
|
|||||||
return components[status as keyof typeof components] || components.login;
|
return components[status as keyof typeof components] || components.login;
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
<View style={{ width: "100%", alignItems: "center", marginTop: 16 }}>
|
<View style={{ width: "100%", alignItems: "center", }}>
|
||||||
{status == 'login' || !status &&
|
{status == 'login' || !status &&
|
||||||
<View className="flex-row justify-center mt-2 flex-wrap w-[85%] items-center">
|
<View className="flex-row justify-center mt-2 flex-wrap w-[85%] items-center">
|
||||||
<ThemedText style={{ fontSize: 11, color: "#FFB645" }}>
|
<ThemedText color='bgPrimary' size='xxs' type='inter'>
|
||||||
{status === 'login' || !status ? t('auth.agree.logintext', { ns: 'login' }) : t('auth.agree.singupText', { ns: 'login' })}
|
{status === 'login' || !status ? t('auth.agree.logintext', { ns: 'login' }) : t('auth.agree.singupText', { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<TouchableOpacity onPress={() => { setModalVisible(true); setModalType('terms') }}>
|
<TouchableOpacity onPress={() => { setModalVisible(true); setModalType('terms') }}>
|
||||||
<ThemedText style={{ fontSize: 11, color: "#FFB645", textDecorationLine: 'underline' }}>
|
<ThemedText color='bgPrimary' size='xxs' type='inter' style={{ textDecorationLine: 'underline' }}>
|
||||||
{t('auth.agree.terms', { ns: 'login' })}
|
{t('auth.agree.terms', { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<ThemedText style={{ fontSize: 11, color: "#FFB645", flexWrap: 'wrap' }}>
|
<ThemedText color='bgPrimary' size='xxs' type='inter' className='flex-wrap'>
|
||||||
{t('auth.agree.join', { ns: 'login' })}
|
{t('auth.agree.join', { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<TouchableOpacity onPress={() => { setModalVisible(true); setModalType('privacy') }}>
|
<TouchableOpacity onPress={() => { setModalVisible(true); setModalType('privacy') }}>
|
||||||
<ThemedText style={{ fontSize: 11, color: "#FFB645", textDecorationLine: 'underline' }}>
|
<ThemedText color='bgPrimary' size='xxs' type='inter' className='flex-wrap' style={{ textDecorationLine: 'underline' }}>
|
||||||
{t('auth.agree.privacyPolicy', { ns: 'login' })}
|
{t('auth.agree.privacyPolicy', { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import UploaderProgress from '@/components/file-upload/upload-progress/uploader-
|
|||||||
import SkeletonItem from '@/components/memo/SkeletonItem';
|
import SkeletonItem from '@/components/memo/SkeletonItem';
|
||||||
|
|
||||||
// 类型定义
|
// 类型定义
|
||||||
|
import { Fonts } from '@/constants/Fonts';
|
||||||
import { useUploadManager } from '@/hooks/useUploadManager';
|
import { useUploadManager } from '@/hooks/useUploadManager';
|
||||||
import { getCachedData, prefetchChatDetail, prefetchChats } from '@/lib/prefetch';
|
import { getCachedData, prefetchChatDetail, prefetchChats } from '@/lib/prefetch';
|
||||||
import { fetchApi } from '@/lib/server-api-util';
|
import { fetchApi } from '@/lib/server-api-util';
|
||||||
@ -227,7 +228,7 @@ const MemoList = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
<View style={[styles.container, { paddingTop: insets.top + 8 }]}>
|
||||||
<FlatList
|
<FlatList
|
||||||
ref={flatListRef}
|
ref={flatListRef}
|
||||||
data={historyList}
|
data={historyList}
|
||||||
@ -273,7 +274,7 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
},
|
},
|
||||||
headerContainer: {
|
headerContainer: {
|
||||||
paddingBottom: 16,
|
paddingBottom: 8,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
@ -281,7 +282,8 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: '#4C320C',
|
color: '#4C320C',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
marginBottom: 16,
|
marginBottom: 8,
|
||||||
|
fontFamily: Fonts["quicksand"]
|
||||||
},
|
},
|
||||||
listContent: {
|
listContent: {
|
||||||
paddingBottom: Platform.select({
|
paddingBottom: Platform.select({
|
||||||
@ -314,13 +316,15 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
memoTitle: {
|
memoTitle: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '500',
|
fontWeight: 'bold',
|
||||||
color: '#4C320C',
|
color: '#4C320C',
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
|
fontFamily: Fonts['sfPro']
|
||||||
},
|
},
|
||||||
memoSubtitle: {
|
memoSubtitle: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#AC7E35',
|
color: '#AC7E35',
|
||||||
|
fontFamily: Fonts['inter']
|
||||||
},
|
},
|
||||||
separator: {
|
separator: {
|
||||||
height: 1 / PixelRatio.get(),
|
height: 1 / PixelRatio.get(),
|
||||||
|
|||||||
@ -13,13 +13,14 @@ import { CountData, UserInfoDetails } from '@/types/user';
|
|||||||
import { useFocusEffect, useRouter } from 'expo-router';
|
import { useFocusEffect, useRouter } from 'expo-router';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
|
||||||
export default function OwnerPage() {
|
export default function OwnerPage() {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const width = Dimensions.get("window").width;
|
||||||
|
|
||||||
// 添加页面挂载状态
|
// 添加页面挂载状态
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
@ -119,17 +120,17 @@ export default function OwnerPage() {
|
|||||||
<UserInfo userInfo={userInfoDetails} />
|
<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} />
|
<CarouselComponent data={userInfoDetails?.material_counter} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 作品数据 */}
|
{/* 作品数据 */}
|
||||||
<View className='flex flex-row justify-between gap-[1rem]'>
|
<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.storiesCreated", { ns: "personal" })} icon={<StoriesSvg width={16} height={16} />} 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.conversationsWithMemo", { ns: "personal" })} icon={<ConversationsSvg width={16} height={16} />} number={userInfoDetails.conversations_count} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 排行榜 */}
|
{/* 排行榜 */}
|
||||||
|
|||||||
@ -37,9 +37,10 @@ export default function Rights() {
|
|||||||
requestProducts,
|
requestProducts,
|
||||||
ErrorCode
|
ErrorCode
|
||||||
} = useIAP();
|
} = useIAP();
|
||||||
const { pro } = useLocalSearchParams<{
|
const { points, pro } = useLocalSearchParams<{
|
||||||
credit: string;
|
credit: string;
|
||||||
pro: string;
|
points: string;
|
||||||
|
pro: string
|
||||||
}>();
|
}>();
|
||||||
// 用户勾选协议
|
// 用户勾选协议
|
||||||
const [agree, setAgree] = useState<boolean>(false);
|
const [agree, setAgree] = useState<boolean>(false);
|
||||||
@ -215,9 +216,9 @@ export default function Rights() {
|
|||||||
{/* 会员卡 */}
|
{/* 会员卡 */}
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
{userType === 'normal' ? (
|
{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.cardContent}>
|
||||||
<View style={styles.cardinfo}>
|
<View style={styles.cardinfo}>
|
||||||
@ -226,7 +227,7 @@ export default function Rights() {
|
|||||||
</ThemedText>
|
</ThemedText>
|
||||||
<View style={styles.cardPoints}>
|
<View style={styles.cardPoints}>
|
||||||
<StarSvg />
|
<StarSvg />
|
||||||
<ThemedText style={styles.cardPointsText}>{pro}</ThemedText>
|
<ThemedText style={styles.cardPointsText}>{points}</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -266,8 +267,6 @@ export default function Rights() {
|
|||||||
padding: 16,
|
padding: 16,
|
||||||
paddingBottom: 32,
|
paddingBottom: 32,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: '#eee',
|
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
@ -303,8 +302,8 @@ export default function Rights() {
|
|||||||
}}
|
}}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<ThemedText style={{ color: '#fff', fontWeight: '700', fontSize: 14 }}>
|
<ThemedText style={{ color: '#fff', fontWeight: '700', fontSize: 18 }}>
|
||||||
{t('rights.subscribe', { ns: 'personal' })}
|
{t('rights.subscribe', { ns: 'personal' })} {payType?.split('_')[payType?.split('_')?.length - 1]}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
@ -365,8 +364,8 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
goPay: {
|
goPay: {
|
||||||
backgroundColor: '#E2793F',
|
backgroundColor: '#E2793F',
|
||||||
borderRadius: 24,
|
borderRadius: 32,
|
||||||
paddingVertical: 10,
|
paddingVertical: 16,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -379,9 +378,9 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 16
|
marginBottom: 16
|
||||||
},
|
},
|
||||||
switchButtonItem: {
|
switchButtonItem: {
|
||||||
width: "48%",
|
width: "47%",
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
paddingVertical: 6,
|
paddingVertical: 8,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
@ -390,7 +389,7 @@ const styles = StyleSheet.create({
|
|||||||
marginHorizontal: 16,
|
marginHorizontal: 16,
|
||||||
marginVertical: 16,
|
marginVertical: 16,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
borderRadius: 12,
|
borderRadius: 32,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: {
|
shadowOffset: {
|
||||||
@ -421,7 +420,7 @@ const styles = StyleSheet.create({
|
|||||||
marginHorizontal: 16,
|
marginHorizontal: 16,
|
||||||
marginVertical: 16,
|
marginVertical: 16,
|
||||||
backgroundColor: '#FFB645',
|
backgroundColor: '#FFB645',
|
||||||
borderRadius: 12,
|
borderRadius: 32,
|
||||||
},
|
},
|
||||||
cardContent: {
|
cardContent: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@ -440,7 +439,7 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: '#E2793F',
|
color: '#E2793F',
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 2,
|
paddingVertical: 2,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
@ -450,12 +449,12 @@ const styles = StyleSheet.create({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4
|
gap: 8
|
||||||
},
|
},
|
||||||
cardPointsText: {
|
cardPointsText: {
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: '#4C320C',
|
color: '#4C320C',
|
||||||
lineHeight: 32
|
lineHeight: 36
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -11,7 +11,7 @@ import { checkNotificationPermission, getLocationPermission, getPermissions, req
|
|||||||
import { ThemedText } from '@/components/ThemedText';
|
import { ThemedText } from '@/components/ThemedText';
|
||||||
import { useAuth } from '@/contexts/auth-context';
|
import { useAuth } from '@/contexts/auth-context';
|
||||||
import { fetchApi } from '@/lib/server-api-util';
|
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 * as Location from 'expo-location';
|
||||||
import { useFocusEffect, useRouter } from 'expo-router';
|
import { useFocusEffect, useRouter } from 'expo-router';
|
||||||
import * as SecureStore from 'expo-secure-store';
|
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 { Linking, Platform, Pressable, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
|
||||||
const Setting = (props: { userInfo: UserInfoDetails }) => {
|
const Setting = () => {
|
||||||
const [userInfo, setUserInfo] = useState<User | null>(null);
|
const [userInfo, setUserInfo] = useState<User | null>(null);
|
||||||
|
|
||||||
const getUserInfo = async () => {
|
const getUserInfo = async () => {
|
||||||
@ -210,7 +210,7 @@ const Setting = (props: { userInfo: UserInfoDetails }) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, paddingTop: insets.top, marginBottom: insets.bottom }}>
|
<View style={{ flex: 1, paddingTop: insets.top, paddingBottom: insets.bottom, backgroundColor: '#fff' }}>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.centeredView}
|
style={styles.centeredView}
|
||||||
>
|
>
|
||||||
@ -221,7 +221,7 @@ const Setting = (props: { userInfo: UserInfoDetails }) => {
|
|||||||
<TouchableOpacity onPress={() => { router.push('/owner') }}>
|
<TouchableOpacity onPress={() => { router.push('/owner') }}>
|
||||||
<ReturnArrowSvg />
|
<ReturnArrowSvg />
|
||||||
</TouchableOpacity>
|
</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>
|
<Text style={{ opacity: 0 }}>×</Text>
|
||||||
</View>
|
</View>
|
||||||
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
|
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
|
||||||
@ -269,11 +269,11 @@ const Setting = (props: { userInfo: UserInfoDetails }) => {
|
|||||||
</View> */}
|
</View> */}
|
||||||
{/* 权限信息 */}
|
{/* 权限信息 */}
|
||||||
<View style={{ marginTop: 16 }}>
|
<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.content}>
|
||||||
{/* 相册权限 */}
|
{/* 相册权限 */}
|
||||||
<View style={styles.item}>
|
<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
|
<CustomSwitch
|
||||||
isEnabled={albumEnabled}
|
isEnabled={albumEnabled}
|
||||||
toggleSwitch={toggleAlbum}
|
toggleSwitch={toggleAlbum}
|
||||||
@ -284,7 +284,7 @@ const Setting = (props: { userInfo: UserInfoDetails }) => {
|
|||||||
{/* 位置权限 */}
|
{/* 位置权限 */}
|
||||||
<View style={styles.item}>
|
<View style={styles.item}>
|
||||||
<View>
|
<View>
|
||||||
<ThemedText style={styles.itemText}>{t('permission.locationPermission', { ns: 'personal' })}</ThemedText>
|
<ThemedText style={styles.itemText} type="sfPro">{t('permission.locationPermission', { ns: 'personal' })}</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
<CustomSwitch
|
<CustomSwitch
|
||||||
isEnabled={locationEnabled}
|
isEnabled={locationEnabled}
|
||||||
@ -294,7 +294,7 @@ const Setting = (props: { userInfo: UserInfoDetails }) => {
|
|||||||
<Divider />
|
<Divider />
|
||||||
<View style={styles.item}>
|
<View style={styles.item}>
|
||||||
<View>
|
<View>
|
||||||
<ThemedText style={styles.itemText}>{t('permission.pushNotification', { ns: 'personal' })}</ThemedText>
|
<ThemedText style={styles.itemText} type="sfPro">{t('permission.pushNotification', { ns: 'personal' })}</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
<CustomSwitch
|
<CustomSwitch
|
||||||
isEnabled={notificationsEnabled}
|
isEnabled={notificationsEnabled}
|
||||||
@ -335,62 +335,62 @@ const Setting = (props: { userInfo: UserInfoDetails }) => {
|
|||||||
</View> */}
|
</View> */}
|
||||||
{/* 协议 */}
|
{/* 协议 */}
|
||||||
<View style={{ marginTop: 16 }}>
|
<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}>
|
<View style={styles.content}>
|
||||||
<TouchableOpacity style={styles.item} onPress={() => { setModalType('privacy'); setPrivacyModalVisible(true) }} >
|
<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 />
|
<RightArrowSvg />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Divider />
|
<Divider />
|
||||||
<TouchableOpacity style={styles.item} onPress={() => { setModalType('terms'); setPrivacyModalVisible(true) }} >
|
<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 />
|
<RightArrowSvg />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Divider />
|
<Divider />
|
||||||
<TouchableOpacity style={styles.item} onPress={() => { setModalType('user'); setPrivacyModalVisible(true) }} >
|
<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 />
|
<RightArrowSvg />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Divider />
|
<Divider />
|
||||||
<TouchableOpacity style={styles.item} onPress={() => { setModalType('ai'); setPrivacyModalVisible(true) }} >
|
<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 />
|
<RightArrowSvg />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Divider />
|
<Divider />
|
||||||
<TouchableOpacity style={styles.item} onPress={() => { setLcensesModalVisible(true) }} >
|
<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 />
|
<RightArrowSvg />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Divider />
|
<Divider />
|
||||||
<TouchableOpacity style={[styles.item, { display: language == "en" ? 'none' : 'flex' }]} onPress={() => Linking.openURL("https://beian.miit.gov.cn/")} >
|
<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 />
|
<RightArrowSvg />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{/* 其他信息 */}
|
{/* 其他信息 */}
|
||||||
<View style={{ marginTop: 16 }}>
|
<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}>
|
<View style={styles.content}>
|
||||||
<TouchableOpacity style={styles.item} onPress={() => Linking.openURL("https://work.weixin.qq.com/kfid/kfca0ac87f4e05e8bfd")} >
|
<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 /> */}
|
{/* <RightArrowSvg /> */}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Divider />
|
<Divider />
|
||||||
<View style={styles.item}>
|
<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>
|
<ThemedText style={styles.itemText}>{"0.5.0"}</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{/* 退出 */}
|
{/* 退出 */}
|
||||||
<TouchableOpacity style={[styles.premium, { marginVertical: 8 }]} onPress={handleLogout}>
|
<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 />
|
<LogoutSvg />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{/* 注销账号 */}
|
{/* 注销账号 */}
|
||||||
<TouchableOpacity style={[styles.premium, { marginVertical: 8 }]} onPress={() => setDeleteModalVisible(true)}>
|
<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 />
|
<DeleteSvg />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@ -405,14 +405,11 @@ const Setting = (props: { userInfo: UserInfoDetails }) => {
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
centeredView: {
|
centeredView: {
|
||||||
flex: 1,
|
flex: 1
|
||||||
justifyContent: 'flex-end',
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
||||||
},
|
},
|
||||||
modalView: {
|
modalView: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
backgroundColor: 'white',
|
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
},
|
},
|
||||||
modalHeader: {
|
modalHeader: {
|
||||||
@ -440,7 +437,8 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
premium: {
|
premium: {
|
||||||
backgroundColor: "#FAF9F6",
|
backgroundColor: "#FAF9F6",
|
||||||
padding: 16,
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 20,
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
@ -452,11 +450,11 @@ const styles = StyleSheet.create({
|
|||||||
gap: 4,
|
gap: 4,
|
||||||
backgroundColor: '#FAF9F6',
|
backgroundColor: '#FAF9F6',
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
paddingVertical: 8
|
padding: 8
|
||||||
},
|
},
|
||||||
item: {
|
item: {
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 8,
|
paddingVertical: 12,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@ -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 ReturnArrowSvg from '@/assets/icons/svg/returnArrow.svg';
|
||||||
import { CascaderItem } from '@/components/cascader';
|
import { CascaderItem } from '@/components/cascader';
|
||||||
import ClassifyModal from '@/components/owner/classify';
|
import ClassifyModal from '@/components/owner/classify';
|
||||||
@ -14,7 +15,7 @@ import { GroupedData, RankingItem, TargetItem } from '@/types/user';
|
|||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import * as SecureStore from 'expo-secure-store';
|
import * as SecureStore from 'expo-secure-store';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
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";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
interface LocationData {
|
interface LocationData {
|
||||||
id: number;
|
id: number;
|
||||||
@ -25,6 +26,14 @@ interface LocationData {
|
|||||||
export default function OwnerPage() {
|
export default function OwnerPage() {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const HOT_CITIES = [
|
||||||
|
['北京', '上海', '广州', '深圳'],
|
||||||
|
['杭州', '成都', '乌鲁木齐', '武汉'],
|
||||||
|
['西安', '重庆', '西宁', '哈尔滨'],
|
||||||
|
['长沙', '南宁', '贵阳', '昆明']
|
||||||
|
];
|
||||||
|
// 位置搜索数据
|
||||||
|
const [locationSearch, setLocationSearch] = useState('');
|
||||||
// 位置弹窗
|
// 位置弹窗
|
||||||
const [locationModalVisible, setLocationModalVisible] = useState(false);
|
const [locationModalVisible, setLocationModalVisible] = useState(false);
|
||||||
// 分类弹窗
|
// 分类弹窗
|
||||||
@ -161,7 +170,15 @@ export default function OwnerPage() {
|
|||||||
};
|
};
|
||||||
}, [fetchLocationData]);
|
}, [fetchLocationData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// console.log(locationData);
|
||||||
|
|
||||||
|
}, [locationSearch])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<TouchableWithoutFeedback onPress={() => {
|
||||||
|
Keyboard.dismiss();
|
||||||
|
}}>
|
||||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||||
{/* 导航栏 */}
|
{/* 导航栏 */}
|
||||||
<View
|
<View
|
||||||
@ -172,25 +189,43 @@ export default function OwnerPage() {
|
|||||||
<TouchableOpacity onPress={() => { router.push('/owner') }} style={{ padding: 16 }}>
|
<TouchableOpacity onPress={() => { router.push('/owner') }} style={{ padding: 16 }}>
|
||||||
<ReturnArrowSvg />
|
<ReturnArrowSvg />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<ThemedText style={styles.headerTitle}>
|
<ThemedText style={styles.headerTitle} onPress={() => { setClassifyModalVisible(true) }}>
|
||||||
Top Memory Makers
|
{selectedClassify?.length > 0 ? selectedClassify[selectedClassify?.length - 1].name : "分类"}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<ThemedText className='opacity-0'>123</ThemedText>
|
<ThemedText className='opacity-0'>123</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 16, marginHorizontal: 16 }}>
|
<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 }}>
|
<TouchableOpacity onPress={() => { setLocationModalVisible(true) }} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||||
|
<PlaceSvg />
|
||||||
<ThemedText style={{ color: selectedLocation?.length > 0 ? '#FFB645' : '#4C320C' }}>
|
<ThemedText style={{ color: selectedLocation?.length > 0 ? '#FFB645' : '#4C320C' }}>
|
||||||
{selectedLocation?.length > 0 ? selectedLocation[selectedLocation?.length - 1].name : "地区"}
|
{selectedLocation?.length > 0 ? selectedLocation[selectedLocation?.length - 1].name : "地区"}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
{
|
<DownSvg />
|
||||||
|
{/* {
|
||||||
selectedLocation?.length > 0
|
selectedLocation?.length > 0
|
||||||
?
|
?
|
||||||
<ArrowSvg style={{ transform: [{ rotate: '90deg' }], width: 12, height: 12 }} />
|
<ArrowSvg style={{ transform: [{ rotate: '90deg' }], width: 12, height: 12 }} />
|
||||||
:
|
:
|
||||||
<ReturnArrowSvg style={{ transform: [{ rotate: '270deg' }], width: 12, height: 12 }} />
|
<ReturnArrowSvg style={{ transform: [{ rotate: '270deg' }], width: 12, height: 12 }} />
|
||||||
}
|
} */}
|
||||||
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity onPress={() => { setClassifyModalVisible(true) }} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
{/* <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' }}>
|
<ThemedText style={{ color: selectedClassify?.length > 0 ? '#FFB645' : '#4C320C' }}>
|
||||||
{selectedClassify?.length > 0 ? selectedClassify[selectedClassify?.length - 1].name : "分类"}
|
{selectedClassify?.length > 0 ? selectedClassify[selectedClassify?.length - 1].name : "分类"}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
@ -200,8 +235,33 @@ export default function OwnerPage() {
|
|||||||
:
|
:
|
||||||
<ReturnArrowSvg style={{ transform: [{ rotate: '270deg' }], width: 12, height: 12 }} />
|
<ReturnArrowSvg style={{ transform: [{ rotate: '270deg' }], width: 12, height: 12 }} />
|
||||||
}
|
}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity> */}
|
||||||
|
|
||||||
</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} />
|
<PodiumComponent data={ranking} />
|
||||||
{/* 排名区域 */}
|
{/* 排名区域 */}
|
||||||
@ -224,6 +284,7 @@ export default function OwnerPage() {
|
|||||||
handleChange={handleClassifyChange}
|
handleChange={handleClassifyChange}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,5 +304,46 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: '#4C320C',
|
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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
BIN
assets/fonts/Inter-Regular.otf
Normal file
BIN
assets/fonts/Quicksand.otf
Normal file
BIN
assets/fonts/SF-Pro.otf
Normal file
3
assets/icons/svg/down.svg
Normal 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 |
4
assets/icons/svg/place.svg
Normal 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 |
17
assets/icons/svg/proIcon.svg
Normal file
|
After Width: | Height: | Size: 262 KiB |
3
assets/icons/svg/search.svg
Normal 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 |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.4 KiB |
BIN
assets/images/png/owner/proIcon.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
@ -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';
|
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 & {
|
export type ThemedTextProps = TextProps & {
|
||||||
lightColor?: string;
|
lightColor?: string;
|
||||||
darkColor?: 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({
|
export function ThemedText({
|
||||||
style,
|
style,
|
||||||
lightColor,
|
lightColor,
|
||||||
darkColor,
|
darkColor,
|
||||||
type = 'default',
|
type = 'default',
|
||||||
|
weight = 'regular',
|
||||||
|
size,
|
||||||
|
radius,
|
||||||
|
color,
|
||||||
...rest
|
...rest
|
||||||
}: ThemedTextProps) {
|
}: 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 (
|
return (
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
{ color },
|
baseStyle,
|
||||||
type === 'default' ? styles.default : undefined,
|
type === 'default' ? styles.default : undefined,
|
||||||
type === 'title' ? styles.title : undefined,
|
type === 'title' ? styles.title : undefined,
|
||||||
type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
|
type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
|
||||||
type === 'subtitle' ? styles.subtitle : undefined,
|
type === 'subtitle' ? styles.subtitle : undefined,
|
||||||
type === 'link' ? styles.link : 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,
|
style,
|
||||||
]}
|
]}
|
||||||
{...rest}
|
{...rest}
|
||||||
@ -35,26 +83,41 @@ export function ThemedText({
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
default: {
|
default: {
|
||||||
fontSize: 16,
|
fontSize: Number(Fonts.base),
|
||||||
lineHeight: 24,
|
lineHeight: 24,
|
||||||
|
fontFamily: Fonts.quicksand,
|
||||||
},
|
},
|
||||||
defaultSemiBold: {
|
defaultSemiBold: {
|
||||||
fontSize: 16,
|
fontSize: Number(Fonts.base),
|
||||||
lineHeight: 24,
|
lineHeight: 24,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 32,
|
fontSize: Fonts['2xl'],
|
||||||
fontWeight: 'bold',
|
fontWeight: '700',
|
||||||
lineHeight: 32,
|
lineHeight: 32,
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
fontSize: 20,
|
fontSize: Fonts.lg,
|
||||||
fontWeight: 'bold',
|
fontWeight: '600',
|
||||||
|
lineHeight: 28,
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
lineHeight: 30,
|
fontSize: Fonts.sm,
|
||||||
fontSize: 16,
|
lineHeight: 20,
|
||||||
color: '#0a7ea4',
|
color: '#0a7ea4',
|
||||||
|
textDecorationLine: 'underline',
|
||||||
|
},
|
||||||
|
sfPro: {
|
||||||
|
fontSize: Number(Fonts.base),
|
||||||
|
lineHeight: 24,
|
||||||
|
fontWeight: '600',
|
||||||
|
fontFamily: Fonts.sfPro,
|
||||||
|
},
|
||||||
|
inter: {
|
||||||
|
fontSize: Number(Fonts.base),
|
||||||
|
lineHeight: 24,
|
||||||
|
fontWeight: '600',
|
||||||
|
fontFamily: Fonts.inter,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,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 { View, type ViewProps } from 'react-native';
|
||||||
|
import { ColorValue, isFontColorKey, ThemeColor } from './ThemedText';
|
||||||
|
|
||||||
type ThemedViewProps = ViewProps & {
|
type ThemedViewProps = ViewProps & {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
bgColor?: FontColor | ColorValue | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ThemedView({ className, style, ...props }: ThemedViewProps) {
|
export function ThemedView({ className, style, bgColor, ...props }: ThemedViewProps) {
|
||||||
return <View className={className} style={style} {...props} />;
|
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} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { ContentPart, Message } from '@/types/ask';
|
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 React, { Dispatch, ForwardedRef, forwardRef, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
FlatList,
|
FlatList,
|
||||||
FlatListProps,
|
FlatListProps,
|
||||||
|
Keyboard,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
View
|
View
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
@ -52,6 +54,13 @@ function ChatComponent(
|
|||||||
}
|
}
|
||||||
}, [userMessages.length]);
|
}, [userMessages.length]);
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
Keyboard.dismiss();
|
||||||
|
}, [Keyboard, sessionId])
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
const renderMessageItem = useCallback(({ item, index }: { item: Message, index: number }) => {
|
const renderMessageItem = useCallback(({ item, index }: { item: Message, index: number }) => {
|
||||||
const itemStyle = index === 0 ? { marginTop: 16, marginHorizontal: 16 } : { marginHorizontal: 16 };
|
const itemStyle = index === 0 ? { marginTop: 16, marginHorizontal: 16 } : { marginHorizontal: 16 };
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { ThemedText } from "@/components/ThemedText";
|
import { ThemedText } from "@/components/ThemedText";
|
||||||
|
import { Fonts } from "@/constants/Fonts";
|
||||||
import { webSocketManager } from "@/lib/websocket-util";
|
import { webSocketManager } from "@/lib/websocket-util";
|
||||||
import { Message } from "@/types/ask";
|
import { Message } from "@/types/ask";
|
||||||
import { Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction } from "react";
|
||||||
@ -58,15 +59,13 @@ export default function AskHello({ setUserMessages, setConversationId, setIsHell
|
|||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
>
|
>
|
||||||
<View className="items-center">
|
<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' })}
|
{t('ask.hi', { ns: 'ask' })}
|
||||||
{"\n"}
|
{"\n"}
|
||||||
{t('ask.iAmMemo', { ns: 'ask' })}
|
{t('ask.iAmMemo', { ns: 'ask' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<View>
|
<Image source={require('@/assets/images/png/icon/ip.png')} style={{ width: width * 0.4, height: height * 0.25 }} />
|
||||||
<Image source={require('@/assets/images/png/icon/ip.png')} style={{ width: width * 0.5, height: height * 0.3 }} />
|
<ThemedText className="text-center -mt-10" size='base' color="textPrimary" type="sfPro" weight="medium">
|
||||||
</View>
|
|
||||||
<ThemedText className="!text-textPrimary text-center -mt-10" style={{ fontSize: 16 }}>
|
|
||||||
{t('ask.ready', { ns: 'ask' })}
|
{t('ask.ready', { ns: 'ask' })}
|
||||||
{"\n"}
|
{"\n"}
|
||||||
{t('ask.justAsk', { ns: 'ask' })}
|
{t('ask.justAsk', { ns: 'ask' })}
|
||||||
@ -112,11 +111,14 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
case: {
|
case: {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: "#AC7E35",
|
borderColor: Fonts["textPrimary"],
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 2,
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
fontSize: 14,
|
fontSize: Fonts["sm"],
|
||||||
color: "#4C320C"
|
color: Fonts["textSecondary"],
|
||||||
|
fontFamily: Fonts["sfPro"]
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -12,6 +12,7 @@ import {
|
|||||||
View
|
View
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
||||||
|
import { Fonts } from '@/constants/Fonts';
|
||||||
import { webSocketManager, WsMessage } from '@/lib/websocket-util';
|
import { webSocketManager, WsMessage } from '@/lib/websocket-util';
|
||||||
import { Message } from '@/types/ask';
|
import { Message } from '@/types/ask';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -25,11 +26,12 @@ interface Props {
|
|||||||
setConversationId: (conversationId: string) => void,
|
setConversationId: (conversationId: string) => void,
|
||||||
selectedImages: string[];
|
selectedImages: string[];
|
||||||
setSelectedImages: Dispatch<SetStateAction<string[]>>;
|
setSelectedImages: Dispatch<SetStateAction<string[]>>;
|
||||||
|
isHello: boolean;
|
||||||
}
|
}
|
||||||
const RENDER_INTERVAL = 50; // 渲染间隔,单位毫秒
|
const RENDER_INTERVAL = 50; // 渲染间隔,单位毫秒
|
||||||
|
|
||||||
export default function SendMessage(props: Props) {
|
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()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@ -189,21 +191,22 @@ export default function SendMessage(props: Props) {
|
|||||||
}
|
}
|
||||||
]));
|
]));
|
||||||
let currentSessionId = conversationId;
|
let currentSessionId = conversationId;
|
||||||
|
console.log("currentSessionIdcurrentSessionId", currentSessionId);
|
||||||
|
|
||||||
// 如果没有对话ID,先创建一个新对话
|
// 如果没有对话ID,先创建一个新对话
|
||||||
if (!currentSessionId) {
|
if (!currentSessionId) {
|
||||||
currentSessionId = await createNewConversation(text);
|
const newCurrentSessionId = await createNewConversation(text);
|
||||||
setConversationId(currentSessionId);
|
if (newCurrentSessionId) {
|
||||||
webSocketManager.send({
|
setConversationId(newCurrentSessionId);
|
||||||
type: 'Chat',
|
} else {
|
||||||
session_id: currentSessionId,
|
console.error("无法获取 session_id,消息发送失败1。");
|
||||||
message: text,
|
setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng'));
|
||||||
image_material_ids: selectedImages.length > 0 ? selectedImages : undefined,
|
}
|
||||||
});
|
|
||||||
setSelectedImages([]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通过 WebSocket 发送消息
|
// 通过 WebSocket 发送消息
|
||||||
if (currentSessionId) {
|
if (currentSessionId) {
|
||||||
|
try {
|
||||||
webSocketManager.send({
|
webSocketManager.send({
|
||||||
type: 'Chat',
|
type: 'Chat',
|
||||||
session_id: currentSessionId,
|
session_id: currentSessionId,
|
||||||
@ -211,11 +214,11 @@ export default function SendMessage(props: Props) {
|
|||||||
image_material_ids: selectedImages.length > 0 ? selectedImages : undefined,
|
image_material_ids: selectedImages.length > 0 ? selectedImages : undefined,
|
||||||
});
|
});
|
||||||
setSelectedImages([]);
|
setSelectedImages([]);
|
||||||
} else {
|
} catch (error) {
|
||||||
console.error("无法获取 session_id,消息发送失败。");
|
console.error("无法获取 session_id,消息发送失败2。", error);
|
||||||
// 可以在这里处理错误,例如显示一个提示
|
|
||||||
setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng'));
|
setUserMessages(prev => prev.filter(item => item.content !== 'keepSearchIng'));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 将输入框清空
|
// 将输入框清空
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
// 只有在键盘可见时才关闭键盘
|
// 只有在键盘可见时才关闭键盘
|
||||||
@ -243,19 +246,20 @@ export default function SendMessage(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View className="relative w-full">
|
<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')}>
|
<TouchableOpacity style={[styles.button, { borderColor: '#FFB645' }]} onPress={() => handleQuitly('search')}>
|
||||||
<SunSvg width={18} height={18} />
|
<SunSvg width={18} height={18} />
|
||||||
<ThemedText>{t("ask:ask.search")}</ThemedText>
|
<ThemedText type="sfPro" size="sm" weight='regular' color='textSecondary'>{t("ask:ask.search")}</ThemedText>
|
||||||
</TouchableOpacity><TouchableOpacity style={[styles.button, { borderColor: '#E2793F' }]} onPress={() => handleQuitly('video')}>
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={[styles.button, { borderColor: '#E2793F' }]} onPress={() => handleQuitly('video')}>
|
||||||
<VideoSvg width={18} height={18} />
|
<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>
|
</TouchableOpacity>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholder="Ask MeMo Anything..."
|
placeholder="Ask MeMo Anything..."
|
||||||
placeholderTextColor="#999"
|
placeholderTextColor={Fonts["textPrimary"]}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChangeText={(text: string) => {
|
onChangeText={(text: string) => {
|
||||||
setInputValue(text);
|
setInputValue(text);
|
||||||
@ -269,7 +273,7 @@ export default function SendMessage(props: Props) {
|
|||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: 6,
|
right: 6,
|
||||||
bottom: 6
|
bottom: 9
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SendSvg />
|
<SendSvg />
|
||||||
@ -286,28 +290,35 @@ const styles = StyleSheet.create({
|
|||||||
margin: 5,
|
margin: 5,
|
||||||
borderRadius: 25,
|
borderRadius: 25,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderWidth: 2,
|
borderWidth: 1,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: 5,
|
gap: 5,
|
||||||
// backgroundColor: '#F8F8F8'
|
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: '#transparent',
|
backgroundColor: '#transparent',
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
// borderColor: '#d9d9d9',
|
color: Fonts["textPrimary"],
|
||||||
borderColor: '#AC7E35',
|
borderColor: '#AC7E35',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
// borderRadius: 18,
|
borderRadius: 28,
|
||||||
borderRadius: 25,
|
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
paddingVertical: 13,
|
paddingVertical: 16,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
width: '100%', // 确保输入框宽度撑满
|
width: '100%',
|
||||||
paddingRight: 50
|
paddingRight: 50,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 2,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.15,
|
||||||
|
shadowRadius: 3.84,
|
||||||
|
elevation: 5,
|
||||||
},
|
},
|
||||||
voiceButton: {
|
voiceButton: {
|
||||||
padding: 8,
|
padding: 8,
|
||||||
|
|||||||
@ -72,7 +72,7 @@ const CascaderComponent: React.FC<CascaderProps> = ({
|
|||||||
// 渲染某一级选项
|
// 渲染某一级选项
|
||||||
const renderLevel = (items: CascaderItem[], level: number) => {
|
const renderLevel = (items: CascaderItem[], level: number) => {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.levelContainer, { width: columnWidth }]}>
|
<View style={[styles.levelContainer]}>
|
||||||
{items.map((item, index) => {
|
{items.map((item, index) => {
|
||||||
const isActive = selectedItems[level]?.name === item.name;
|
const isActive = selectedItems[level]?.name === item.name;
|
||||||
return (
|
return (
|
||||||
@ -95,6 +95,7 @@ const CascaderComponent: React.FC<CascaderProps> = ({
|
|||||||
textStyle,
|
textStyle,
|
||||||
isActive && [styles.activeText, activeTextStyle]
|
isActive && [styles.activeText, activeTextStyle]
|
||||||
]}
|
]}
|
||||||
|
type='sfPro'
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
@ -112,13 +113,31 @@ const CascaderComponent: React.FC<CascaderProps> = ({
|
|||||||
|
|
||||||
// 渲染所有级联列
|
// 渲染所有级联列
|
||||||
const renderColumns = () => {
|
const renderColumns = () => {
|
||||||
return allLevelsData.map((items, level) => (
|
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
|
<View
|
||||||
key={`column-${level}`}
|
key={`column-${level}`}
|
||||||
style={[
|
style={[
|
||||||
styles.column,
|
styles.column,
|
||||||
{ width: columnWidth },
|
{ width },
|
||||||
showDivider && level < allLevelsData.length - 1 && [
|
showDivider && level < totalLevels - 1 && [
|
||||||
styles.columnWithDivider,
|
styles.columnWithDivider,
|
||||||
{ borderRightColor: dividerColor }
|
{ borderRightColor: dividerColor }
|
||||||
]
|
]
|
||||||
@ -132,7 +151,8 @@ const CascaderComponent: React.FC<CascaderProps> = ({
|
|||||||
{renderLevel(items, level)}
|
{renderLevel(items, level)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
));
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 自定义显示内容
|
// 自定义显示内容
|
||||||
@ -166,15 +186,17 @@ const styles = StyleSheet.create({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
height: 300, // Set a fixed height for the container
|
height: 300,
|
||||||
},
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
flexDirection: 'row',
|
||||||
},
|
},
|
||||||
column: {
|
column: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
maxHeight: '100%',
|
maxHeight: '100%',
|
||||||
|
flexShrink: 0,
|
||||||
},
|
},
|
||||||
columnWithDivider: {
|
columnWithDivider: {
|
||||||
borderRightWidth: 1,
|
borderRightWidth: 1,
|
||||||
|
|||||||
@ -26,7 +26,7 @@ const MessageBubble = ({
|
|||||||
}: MessageBubbleProps) => {
|
}: MessageBubbleProps) => {
|
||||||
return (
|
return (
|
||||||
<View
|
<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 }}
|
style={{ marginRight: getMessageText(item) == "keepSearchIng" ? 0 : isUser ? 0 : 10 }}
|
||||||
>
|
>
|
||||||
<MessageContent
|
<MessageContent
|
||||||
|
|||||||
@ -3,9 +3,11 @@ import { ContentPart, Message, User } from "@/types/ask";
|
|||||||
import { TFunction } from "i18next";
|
import { TFunction } from "i18next";
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
|
StyleSheet,
|
||||||
View
|
View
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
||||||
|
import { Fonts } from "@/constants/Fonts";
|
||||||
import MessageRow from './MessageRow';
|
import MessageRow from './MessageRow';
|
||||||
|
|
||||||
interface RenderMessageProps {
|
interface RenderMessageProps {
|
||||||
@ -40,23 +42,40 @@ const MessageItem = ({ setCancel, cancel = true, t, insets, item, sessionId, set
|
|||||||
setSelectedImages={setSelectedImages}
|
setSelectedImages={setSelectedImages}
|
||||||
setModalDetailsVisible={setModalDetailsVisible}
|
setModalDetailsVisible={setModalDetailsVisible}
|
||||||
/>
|
/>
|
||||||
{/* {item.askAgain && item.askAgain.length > 0 && (
|
{/* {item.content instanceof Array && item.content.filter((media: ContentPart) => media.type !== 'text').length > 0 && (
|
||||||
<View className={`mr-10`}>
|
<View style={styles.tips}>
|
||||||
{item.askAgain.map((suggestion, index, array) => (
|
<TouchableOpacity style={[styles.tip, { borderRadius: 16 }]} onPress={() => {
|
||||||
<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'}`}
|
<ThemedText style={styles.tipText}>Help me create a warm, cozy video.</ThemedText>
|
||||||
>
|
</TouchableOpacity>
|
||||||
<Text className="text-gray-700">{suggestion.text}</Text>
|
<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>
|
</TouchableOpacity>
|
||||||
))}
|
|
||||||
</View>
|
</View>
|
||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
</View>
|
</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);
|
export default React.memo(MessageItem);
|
||||||
|
|
||||||
|
|||||||
@ -116,18 +116,18 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
height: 80, // Set a fixed height for the navbar
|
height: 80,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 32,
|
paddingHorizontal: 32,
|
||||||
backgroundColor: 'transparent', // Make sure it's transparent
|
backgroundColor: 'transparent',
|
||||||
},
|
},
|
||||||
centerButton: {
|
centerButton: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: width / 2,
|
left: '50%',
|
||||||
top: -30, // Adjust this value to move the button up or down
|
top: -30,
|
||||||
marginLeft: -42.5, // Half of the button width (85/2)
|
transform: [{ translateX: -17 }],
|
||||||
width: 85,
|
width: 85,
|
||||||
height: 85,
|
height: 85,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@ -143,8 +143,8 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
|
|||||||
},
|
},
|
||||||
statusIndicator: {
|
statusIndicator: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 15,
|
top: 3,
|
||||||
right: 15,
|
right: 20,
|
||||||
width: 10,
|
width: 10,
|
||||||
height: 10,
|
height: 10,
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
@ -162,7 +162,7 @@ const AskNavbar = ({ wsStatus }: AskNavbarProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Image source={require('@/assets/images/png/owner/ask.png')} style={{ width: width * 1.18, height: 100, resizeMode: 'cover', marginLeft: -width * 0.07 }} />
|
<Image source={require('@/assets/images/png/owner/ask.png')} style={{ width: "100%" }} />
|
||||||
<View style={styles.navContainer}>
|
<View style={styles.navContainer}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => navigateTo('/memo-list')}
|
onPress={() => navigateTo('/memo-list')}
|
||||||
|
|||||||
@ -115,13 +115,13 @@ const Code = ({ phone }: CodeProps) => {
|
|||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.contentContainer}>
|
<View style={styles.contentContainer}>
|
||||||
<View style={styles.headerContainer}>
|
<View style={styles.headerContainer}>
|
||||||
<ThemedText style={styles.title}>
|
<ThemedText style={styles.title} color="textSecondary" size="xl" weight="bold">
|
||||||
{t("auth.telLogin.codeTitle", { ns: 'login' })}
|
{t("auth.telLogin.codeTitle", { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<ThemedText style={styles.subtitle}>
|
<ThemedText style={styles.subtitle} type="sfPro" color="textPrimary" size="sm">
|
||||||
{t("auth.telLogin.secondTitle", { ns: 'login' })}
|
{t("auth.telLogin.secondTitle", { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<ThemedText style={styles.phoneNumber}>
|
<ThemedText color="bgSecondary" size="sm" weight="bold">
|
||||||
{phone}
|
{phone}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
@ -144,13 +144,13 @@ const Code = ({ phone }: CodeProps) => {
|
|||||||
/>
|
/>
|
||||||
<View style={[styles.errorContainer, { opacity: error ? 1 : 0 }]}>
|
<View style={[styles.errorContainer, { opacity: error ? 1 : 0 }]}>
|
||||||
<Error />
|
<Error />
|
||||||
<ThemedText style={styles.errorText}>
|
<ThemedText style={styles.errorText} size="xxs" color="bgSecondary" type="inter">
|
||||||
{error}
|
{error}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.footerContainer}>
|
<View style={styles.footerContainer}>
|
||||||
<ThemedText style={styles.footerText}>
|
<ThemedText size="sm" color="textPrimary" type="sfPro">
|
||||||
{t("auth.telLogin.sendAgain", { ns: 'login' })}
|
{t("auth.telLogin.sendAgain", { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<TouchableOpacity onPress={() => {
|
<TouchableOpacity onPress={() => {
|
||||||
@ -158,10 +158,16 @@ const Code = ({ phone }: CodeProps) => {
|
|||||||
sendVerificationCode()
|
sendVerificationCode()
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<ThemedText style={[
|
<ThemedText
|
||||||
|
style={[
|
||||||
styles.resendText,
|
styles.resendText,
|
||||||
countdown > 0 && styles.disabledResendText
|
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' })}
|
{countdown > 0 ? `${countdown}s${t("auth.telLogin.resend", { ns: 'login' })}` : t("auth.telLogin.resend", { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@ -185,23 +191,13 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: '600',
|
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
paddingTop: 4,
|
paddingTop: 4,
|
||||||
color: '#111827',
|
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
fontSize: 16,
|
|
||||||
color: '#4B5563',
|
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
phoneNumber: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: '500',
|
|
||||||
color: '#E2793F',
|
|
||||||
},
|
|
||||||
otpContainer: {
|
otpContainer: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 80,
|
height: 80,
|
||||||
@ -228,9 +224,6 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
errorText: {
|
errorText: {
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: '500',
|
|
||||||
color: '#E2793F',
|
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
},
|
},
|
||||||
footerContainer: {
|
footerContainer: {
|
||||||
@ -238,12 +231,7 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
},
|
},
|
||||||
footerText: {
|
|
||||||
color: '#6B7280',
|
|
||||||
},
|
|
||||||
resendText: {
|
resendText: {
|
||||||
color: '#E2793F',
|
|
||||||
fontWeight: '500',
|
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
},
|
},
|
||||||
disabledResendText: {
|
disabledResendText: {
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
|
import { Fonts } from "@/constants/Fonts";
|
||||||
import { fetchApi } from "@/lib/server-api-util";
|
import { fetchApi } from "@/lib/server-api-util";
|
||||||
import { User } from "@/types/user";
|
import { User } from "@/types/user";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { ThemedText } from "../ThemedText";
|
||||||
|
import Button from "./ui/Button";
|
||||||
|
import TextInput from "./ui/TextInput";
|
||||||
|
|
||||||
interface LoginProps {
|
interface LoginProps {
|
||||||
setIsSignUp?: (isSignUp: string) => void;
|
setIsSignUp?: (isSignUp: string) => void;
|
||||||
@ -69,45 +72,29 @@ const ForgetPwd = ({ setIsSignUp, updateUrlParam, setError }: LoginProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.inputContainer}>
|
{/* 邮箱 */}
|
||||||
<ThemedText style={styles.inputLabel}>
|
|
||||||
{t('auth.forgetPwd.title', { ns: 'login' })}
|
|
||||||
</ThemedText>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.textInput}
|
label={t('auth.forgetPwd.title', { ns: 'login' })}
|
||||||
placeholder={t('auth.forgetPwd.emailPlaceholder', { ns: 'login' })}
|
placeholder={t('auth.forgetPwd.emailPlaceholder', { ns: 'login' })}
|
||||||
placeholderTextColor="#ccc"
|
|
||||||
value={email}
|
|
||||||
onChangeText={setEmail}
|
onChangeText={setEmail}
|
||||||
keyboardType="email-address"
|
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
|
value={email}
|
||||||
/>
|
/>
|
||||||
</View>
|
{/* 发送邮箱 */}
|
||||||
|
<Button
|
||||||
<TouchableOpacity
|
isLoading={isDisabled || loading}
|
||||||
style={[
|
handleLogin={handleSubmit}
|
||||||
styles.submitButton,
|
text={isDisabled
|
||||||
(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.sendEmailBtnDisabled", { ns: "login" })} (${countdown}s)`
|
||||||
: t("auth.forgetPwd.sendEmailBtn", { ns: "login" })}
|
: t("auth.forgetPwd.sendEmailBtn", { ns: "login" })}
|
||||||
</ThemedText>
|
/>
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
|
{/* 返回登录 */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={handleBackToLogin}
|
onPress={handleBackToLogin}
|
||||||
>
|
>
|
||||||
<ThemedText style={styles.backButtonText}>
|
<ThemedText type='inter' color="bgSecondary" size="sm">
|
||||||
{t('auth.forgetPwd.goback', { ns: 'login' })}
|
{t('auth.forgetPwd.goback', { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@ -123,16 +110,24 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
inputLabel: {
|
inputLabel: {
|
||||||
fontSize: 16,
|
fontSize: Fonts['base'],
|
||||||
color: '#1F2937',
|
color: Fonts['textPrimary'],
|
||||||
|
fontWeight: Fonts['bold'],
|
||||||
|
fontFamily: Fonts['sfPro'],
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
},
|
},
|
||||||
textInput: {
|
textInput: {
|
||||||
borderRadius: 12,
|
borderRadius: Fonts['xs'],
|
||||||
padding: 12,
|
paddingHorizontal: Fonts['base'],
|
||||||
fontSize: 16,
|
paddingVertical: Fonts['xs'],
|
||||||
backgroundColor: '#FFF8DE',
|
fontSize: Fonts['sm'],
|
||||||
|
lineHeight: Fonts['base'],
|
||||||
|
textAlignVertical: 'center',
|
||||||
|
backgroundColor: Fonts['bgInput'],
|
||||||
|
color: Fonts['textSecondary'],
|
||||||
|
fontFamily: Fonts['inter'],
|
||||||
|
paddingRight: Fonts['5xl'],
|
||||||
},
|
},
|
||||||
submitButton: {
|
submitButton: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -151,11 +146,7 @@ const styles = StyleSheet.create({
|
|||||||
backButton: {
|
backButton: {
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
marginTop: 24,
|
marginTop: 24,
|
||||||
},
|
}
|
||||||
backButtonText: {
|
|
||||||
color: '#1F2937',
|
|
||||||
fontSize: 14,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ForgetPwd;
|
export default ForgetPwd;
|
||||||
@ -1,14 +1,15 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Fonts } from "@/constants/Fonts";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { useAuth } from "../../contexts/auth-context";
|
||||||
import { fetchApi } from "../../lib/server-api-util";
|
import { fetchApi } from "../../lib/server-api-util";
|
||||||
import { User } from "../../types/user";
|
import { User } from "../../types/user";
|
||||||
import { ThemedText } from "../ThemedText";
|
import { ThemedText } from "../ThemedText";
|
||||||
|
import Button from "./ui/Button";
|
||||||
|
import TextInput from "./ui/TextInput";
|
||||||
|
|
||||||
const REMEMBER_ACCOUNT_KEY = 'fairclip_remembered_account';
|
|
||||||
interface LoginProps {
|
interface LoginProps {
|
||||||
updateUrlParam: (status: string, value: string) => void;
|
updateUrlParam: (status: string, value: string) => void;
|
||||||
setError: (error: string) => void;
|
setError: (error: string) => void;
|
||||||
@ -22,7 +23,6 @@ const Login = ({ updateUrlParam, setError, setShowPassword, showPassword }: Logi
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [rememberMe, setRememberMe] = useState(false);
|
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
if (!email) {
|
if (!email) {
|
||||||
@ -69,75 +69,61 @@ const Login = ({ updateUrlParam, setError, setShowPassword, showPassword }: Logi
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={[styles.inputContainer, { marginBottom: 20 }]}>
|
|
||||||
<ThemedText style={styles.inputLabel}>
|
{/* 邮箱 */}
|
||||||
{t('auth.login.email', { ns: 'login' })}
|
|
||||||
</ThemedText>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.textInput}
|
label={t('auth.login.email', { ns: 'login' })}
|
||||||
placeholder={t('auth.login.accountPlaceholder', { ns: 'login' })}
|
placeholder={t('auth.login.accountPlaceholder', { ns: 'login' })}
|
||||||
placeholderTextColor="#ccc"
|
|
||||||
value={email}
|
|
||||||
onChangeText={(text) => {
|
onChangeText={(text) => {
|
||||||
setEmail(text);
|
setEmail(text);
|
||||||
setError('123');
|
setError('123');
|
||||||
}}
|
}}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
|
value={email}
|
||||||
/>
|
/>
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.inputContainer}>
|
{/* 密码 */}
|
||||||
<ThemedText style={styles.inputLabel}>
|
|
||||||
{t('auth.login.password', { ns: 'login' })}
|
|
||||||
</ThemedText>
|
|
||||||
<View style={styles.passwordInputContainer}>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.textInput, { paddingRight: 48 }]}
|
label={t('auth.login.password', { ns: 'login' })}
|
||||||
placeholder={t('auth.login.passwordPlaceholder', { ns: 'login' })}
|
placeholder={t('auth.login.passwordPlaceholder', { ns: 'login' })}
|
||||||
placeholderTextColor="#ccc"
|
|
||||||
value={password}
|
|
||||||
onChangeText={(text) => {
|
onChangeText={(text) => {
|
||||||
setPassword(text);
|
setPassword(text);
|
||||||
setError('123');
|
setError('123');
|
||||||
}}
|
}}
|
||||||
secureTextEntry={!showPassword}
|
autoCapitalize="none"
|
||||||
|
value={password}
|
||||||
|
type="password"
|
||||||
|
setShowPassword={setShowPassword}
|
||||||
|
showPassword={showPassword}
|
||||||
|
containerStyle={{ marginBottom: 0 }}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.eyeIcon}
|
|
||||||
onPress={() => setShowPassword(!showPassword)}
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name={showPassword ? 'eye' : 'eye-off'}
|
|
||||||
size={20}
|
|
||||||
color="#666"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
|
{/* 忘记密码 */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.forgotPassword}
|
style={styles.forgotPassword}
|
||||||
onPress={handleForgotPassword}
|
onPress={handleForgotPassword}
|
||||||
>
|
>
|
||||||
<ThemedText style={styles.forgotPasswordText}>
|
<ThemedText style={styles.forgotPasswordText} color="textPrimary" type="inter">
|
||||||
{t('auth.login.forgotPassword', { ns: 'login' })}
|
{t('auth.login.forgotPassword', { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
{/* 登录按钮 */}
|
||||||
style={[styles.loginButton, isLoading && { opacity: 0.7 }]}
|
<Button isLoading={isLoading} handleLogin={handleLogin} text={t('auth.login.loginButton', { ns: 'login' })} />
|
||||||
onPress={handleLogin}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<ActivityIndicator color="#fff" />
|
|
||||||
) : (
|
|
||||||
<ThemedText style={styles.loginButtonText}>
|
|
||||||
{t('auth.login.loginButton', { ns: 'login' })}
|
|
||||||
</ThemedText>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
|
{/* 注册 */}
|
||||||
|
<View style={styles.signupContainer}>
|
||||||
|
<ThemedText style={styles.signupText} type="sfPro">
|
||||||
|
{t('auth.login.signUpMessage', { ns: 'login' })}
|
||||||
|
</ThemedText>
|
||||||
|
<TouchableOpacity onPress={handleSignUp}>
|
||||||
|
<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={{ width: "100%", alignItems: "center", opacity: 0 }}>
|
||||||
<View style={styles.loginTypeContainer}>
|
<View style={styles.loginTypeContainer}>
|
||||||
<ThemedText>
|
<ThemedText>
|
||||||
@ -150,17 +136,6 @@ const Login = ({ updateUrlParam, setError, setShowPassword, showPassword }: Logi
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.signupContainer}>
|
|
||||||
<ThemedText style={styles.signupText}>
|
|
||||||
{t('auth.login.signUpMessage', { ns: 'login' })}
|
|
||||||
</ThemedText>
|
|
||||||
<TouchableOpacity onPress={handleSignUp}>
|
|
||||||
<ThemedText style={styles.signupLink}>
|
|
||||||
{t('auth.login.signUp', { ns: 'login' })}
|
|
||||||
</ThemedText>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -179,8 +154,8 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
loginType: {
|
loginType: {
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
width: 54,
|
width: 42,
|
||||||
height: 54,
|
height: 42,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
backgroundColor: '#FADBA1'
|
backgroundColor: '#FADBA1'
|
||||||
},
|
},
|
||||||
@ -188,19 +163,24 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
inputLabel: {
|
inputLabel: {
|
||||||
fontSize: 16,
|
fontSize: Fonts['base'],
|
||||||
color: '#AC7E35',
|
color: Fonts['textPrimary'],
|
||||||
fontWeight: '600',
|
fontWeight: Fonts['bold'],
|
||||||
|
fontFamily: Fonts['sfPro'],
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
},
|
},
|
||||||
textInput: {
|
textInput: {
|
||||||
borderRadius: 12,
|
borderRadius: Fonts['xs'],
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: Fonts['base'],
|
||||||
paddingVertical: 12,
|
paddingVertical: Fonts['xs'],
|
||||||
fontSize: 14,
|
fontSize: Fonts['sm'],
|
||||||
|
lineHeight: Fonts['base'],
|
||||||
textAlignVertical: 'center',
|
textAlignVertical: 'center',
|
||||||
backgroundColor: '#FFF8DE'
|
backgroundColor: Fonts['bgInput'],
|
||||||
|
color: Fonts['textSecondary'],
|
||||||
|
fontFamily: Fonts['inter'],
|
||||||
|
paddingRight: Fonts['5xl'],
|
||||||
},
|
},
|
||||||
passwordInputContainer: {
|
passwordInputContainer: {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@ -238,11 +218,11 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
signupText: {
|
signupText: {
|
||||||
color: '#AC7E35',
|
color: '#AC7E35',
|
||||||
fontSize: 17,
|
fontSize: Fonts['sm'],
|
||||||
},
|
},
|
||||||
signupLink: {
|
signupLink: {
|
||||||
color: '#E2793F',
|
color: '#E2793F',
|
||||||
fontSize: 17,
|
fontSize: Fonts['sm'],
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
textDecorationLine: 'underline',
|
textDecorationLine: 'underline',
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { fetchApi } from "@/lib/server-api-util";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ActivityIndicator, TextInput, TouchableOpacity, View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { ThemedText } from "../ThemedText";
|
|
||||||
import { Steps } from "./phoneLogin";
|
import { Steps } from "./phoneLogin";
|
||||||
|
import Button from "./ui/Button";
|
||||||
|
import TextInput from "./ui/TextInput";
|
||||||
|
|
||||||
interface LoginProps {
|
interface LoginProps {
|
||||||
setSteps: (steps: Steps) => void;
|
setSteps: (steps: Steps) => void;
|
||||||
@ -18,67 +18,30 @@ const Phone = ({ setSteps, setPhone, phone, updateUrlParam }: LoginProps) => {
|
|||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
const sendVerificationCode = async () => {
|
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')
|
setSteps('code')
|
||||||
updateUrlParam("status", "code");
|
updateUrlParam("status", "code");
|
||||||
setIsLoading(false);
|
return
|
||||||
} catch (error) {
|
|
||||||
setPhone("")
|
|
||||||
setIsLoading(false);
|
|
||||||
// console.error(t("auth.telLogin.sendCodeError", { ns: 'login' }), error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return <View>
|
return <View>
|
||||||
{/* 手机号输入框 */}
|
{/* 手机号输入框 */}
|
||||||
<View className="mb-5">
|
<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
|
<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' })}
|
placeholder={t('auth.telLogin.phoneRequired', { ns: 'login' })}
|
||||||
placeholderTextColor="#ccc"
|
|
||||||
value={phone}
|
|
||||||
onChangeText={(text) => {
|
onChangeText={(text) => {
|
||||||
setPhone(text);
|
setPhone(text);
|
||||||
setError('');
|
setError('');
|
||||||
}}
|
}}
|
||||||
keyboardType="email-address"
|
keyboardType="email-address"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
|
value={phone}
|
||||||
|
error={error}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 发送验证码 */}
|
{/* 发送验证码 */}
|
||||||
<TouchableOpacity
|
<Button isLoading={isLoading} handleLogin={sendVerificationCode} text={t('auth.telLogin.sendCode', { ns: 'login' })} />
|
||||||
className={`w-full bg-[#E2793F] rounded-full text-[#fff] p-4 items-center mb-6 ${isLoading ? 'opacity-70' : ''}`}
|
|
||||||
onPress={sendVerificationCode}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<ActivityIndicator color="#fff" />
|
|
||||||
) : (
|
|
||||||
<ThemedText className="!text-white font-semibold">
|
|
||||||
{t('auth.telLogin.sendCode', { ns: 'login' })}
|
|
||||||
</ThemedText>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,9 @@ const PhoneLogin = ({ updateUrlParam }: LoginProps) => {
|
|||||||
|
|
||||||
return <View>
|
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>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
|
import { Fonts } from "@/constants/Fonts";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from "react-i18next";
|
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 { useAuth } from "../../contexts/auth-context";
|
||||||
import { fetchApi } from "../../lib/server-api-util";
|
import { fetchApi } from "../../lib/server-api-util";
|
||||||
import { User } from "../../types/user";
|
import { User } from "../../types/user";
|
||||||
import { ThemedText } from "../ThemedText";
|
import { ThemedText } from "../ThemedText";
|
||||||
import PrivacyModal from "../owner/qualification/privacy";
|
import PrivacyModal from "../owner/qualification/privacy";
|
||||||
|
import Button from "./ui/Button";
|
||||||
|
import TextInput from "./ui/TextInput";
|
||||||
|
|
||||||
interface LoginProps {
|
interface LoginProps {
|
||||||
updateUrlParam: (status: string, value: string) => void;
|
updateUrlParam: (status: string, value: string) => void;
|
||||||
@ -146,100 +149,53 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword, setSh
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{/* 邮箱输入 */}
|
{/* 邮箱输入 */}
|
||||||
<View style={styles.inputContainer}>
|
|
||||||
<ThemedText style={styles.inputLabel}>
|
|
||||||
{t('auth.login.email', { ns: 'login' })}
|
|
||||||
</ThemedText>
|
|
||||||
<View style={styles.inputWrapper}>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.textInput}
|
label={t('auth.login.email', { ns: 'login' })}
|
||||||
placeholder={t('auth.login.accountPlaceholder', { ns: 'login' })}
|
placeholder={t('auth.login.accountPlaceholder', { ns: 'login' })}
|
||||||
placeholderTextColor="#ccc"
|
onChangeText={(text) => {
|
||||||
value={email}
|
setEmail(text);
|
||||||
onChangeText={(value) => {
|
setError('123');
|
||||||
setEmail(value)
|
|
||||||
setError('123')
|
|
||||||
}}
|
}}
|
||||||
keyboardType="email-address"
|
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
|
keyboardType="email-address"
|
||||||
|
value={email}
|
||||||
/>
|
/>
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 密码输入 */}
|
{/* 密码输入 */}
|
||||||
<View style={styles.inputContainer}>
|
|
||||||
<ThemedText style={styles.inputLabel}>
|
|
||||||
{t('auth.login.password', { ns: 'login' })}
|
|
||||||
</ThemedText>
|
|
||||||
<View style={styles.passwordInputContainer}>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.textInput, { flex: 1 }]}
|
label={t('auth.login.password', { ns: 'login' })}
|
||||||
placeholder={t('auth.login.passwordPlaceholder', { ns: 'login' })}
|
placeholder={t('auth.login.passwordPlaceholder', { ns: 'login' })}
|
||||||
placeholderTextColor="#ccc"
|
autoCapitalize="none"
|
||||||
value={password}
|
value={password}
|
||||||
onChangeText={(value) => {
|
onChangeText={(value) => {
|
||||||
handlePasswordChange(value)
|
handlePasswordChange(value)
|
||||||
setError('123')
|
setError('123')
|
||||||
}}
|
}}
|
||||||
secureTextEntry={!showPassword}
|
secureTextEntry={!showPassword}
|
||||||
|
type="password"
|
||||||
|
setShowPassword={setShowPassword}
|
||||||
|
showPassword={showPassword}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => setShowPassword(!showPassword)}
|
|
||||||
style={styles.eyeIcon}
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name={showPassword ? 'eye' : 'eye-off'}
|
|
||||||
size={20}
|
|
||||||
color="#666"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 确认密码 */}
|
{/* 确认密码 */}
|
||||||
<View style={[styles.inputContainer, { marginBottom: 24 }]}>
|
|
||||||
<ThemedText style={styles.inputLabel}>
|
|
||||||
{t('auth.signup.confirmPassword', { ns: 'login' })}
|
|
||||||
</ThemedText>
|
|
||||||
<View style={styles.passwordInputContainer}>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.textInput, { flex: 1 }]}
|
label={t('auth.signup.confirmPassword', { ns: 'login' })}
|
||||||
placeholder={t('auth.signup.confirmPasswordPlaceholder', { ns: 'login' })}
|
placeholder={t('auth.signup.confirmPasswordPlaceholder', { ns: 'login' })}
|
||||||
placeholderTextColor="#ccc"
|
autoCapitalize="none"
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChangeText={(value) => {
|
onChangeText={(value) => {
|
||||||
handleConfirmPasswordChange(value)
|
handleConfirmPasswordChange(value)
|
||||||
setError('123')
|
setError('123')
|
||||||
}}
|
}}
|
||||||
secureTextEntry={!showSecondPassword}
|
secureTextEntry={!showSecondPassword}
|
||||||
|
type="password"
|
||||||
|
setShowPassword={setShowSecondPassword}
|
||||||
|
showPassword={showSecondPassword}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => setShowSecondPassword(!showSecondPassword)}
|
|
||||||
style={styles.eyeIcon}
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name={showSecondPassword ? 'eye' : 'eye-off'}
|
|
||||||
size={20}
|
|
||||||
color="#666"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 注册按钮 */}
|
{/* 注册按钮 */}
|
||||||
<TouchableOpacity
|
<Button isLoading={loading} handleLogin={handleSubmit} text={t("auth.signup.signupButton", { ns: 'login' })} />
|
||||||
style={[styles.signupButton, loading && { opacity: 0.7 }]}
|
|
||||||
onPress={handleSubmit}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<ActivityIndicator color="#fff" />
|
|
||||||
) : (
|
|
||||||
<ThemedText style={styles.signupButtonText}>
|
|
||||||
{t("auth.signup.signupButton", { ns: 'login' })}
|
|
||||||
</ThemedText>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={styles.termsContainer}>
|
<View style={styles.termsContainer}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@ -259,68 +215,68 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword, setSh
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{checked && (
|
{checked && (
|
||||||
<Ionicons name="checkmark" size={14} color="white" />
|
<Ionicons name="checkmark" size={14} color={Fonts['textSecondary']} />
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={styles.termsTextContainer}>
|
<View style={styles.termsTextContainer}>
|
||||||
<ThemedText style={styles.termsText}>
|
<ThemedText style={styles.termsText} type="sfPro">
|
||||||
{t("auth.telLogin.agree", { ns: 'login' })}
|
{t("auth.telLogin.agree", { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<TouchableOpacity onPress={() => {
|
<TouchableOpacity onPress={() => {
|
||||||
setModalType('terms');
|
setModalType('terms');
|
||||||
setPrivacyModalVisible(true);
|
setPrivacyModalVisible(true);
|
||||||
}}>
|
}}>
|
||||||
<ThemedText style={styles.termsLink}>
|
<ThemedText style={styles.termsLink} type="sfPro">
|
||||||
{t("auth.telLogin.terms", { ns: 'login' })}
|
{t("auth.telLogin.terms", { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<ThemedText style={styles.termsText}>
|
<ThemedText style={styles.termsText} type="sfPro">
|
||||||
{t("auth.telLogin.and", { ns: 'login' })}
|
{t("auth.telLogin.and", { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<TouchableOpacity onPress={() => {
|
<TouchableOpacity onPress={() => {
|
||||||
setModalType('privacy');
|
setModalType('privacy');
|
||||||
setPrivacyModalVisible(true);
|
setPrivacyModalVisible(true);
|
||||||
}}>
|
}}>
|
||||||
<ThemedText style={styles.termsLink}>
|
<ThemedText style={styles.termsLink} type="sfPro">
|
||||||
{t("auth.telLogin.privacyPolicy", { ns: 'login' })}
|
{t("auth.telLogin.privacyPolicy", { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<ThemedText style={styles.termsText}>
|
<ThemedText style={styles.termsText} type="sfPro">
|
||||||
{t("auth.telLogin.and", { ns: 'login' })}
|
{t("auth.telLogin.and", { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<TouchableOpacity onPress={() => {
|
<TouchableOpacity onPress={() => {
|
||||||
setModalType('user');
|
setModalType('user');
|
||||||
setPrivacyModalVisible(true);
|
setPrivacyModalVisible(true);
|
||||||
}}>
|
}}>
|
||||||
<ThemedText style={styles.termsLink}>
|
<ThemedText style={styles.termsLink} type="sfPro">
|
||||||
{t("auth.telLogin.userAgreement", { ns: 'login' })}
|
{t("auth.telLogin.userAgreement", { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<ThemedText style={styles.termsText}>
|
<ThemedText style={styles.termsText} type="sfPro">
|
||||||
{t("auth.telLogin.and", { ns: 'login' })}
|
{t("auth.telLogin.and", { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<TouchableOpacity onPress={() => {
|
<TouchableOpacity onPress={() => {
|
||||||
setModalType('ai');
|
setModalType('ai');
|
||||||
setPrivacyModalVisible(true);
|
setPrivacyModalVisible(true);
|
||||||
}}>
|
}}>
|
||||||
<ThemedText style={styles.termsLink}>
|
<ThemedText style={styles.termsLink} type="sfPro">
|
||||||
{t("auth.telLogin.aiAgreement", { ns: 'login' })}
|
{t("auth.telLogin.aiAgreement", { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<ThemedText style={styles.termsText}>
|
<ThemedText style={styles.termsText} type="sfPro">
|
||||||
{t("auth.telLogin.agreement", { ns: 'login' })}
|
{t("auth.telLogin.agreement", { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<ThemedText style={styles.termsLink}>
|
<ThemedText style={styles.termsLink} type="sfPro">
|
||||||
{t("common.name")}
|
{t("common.name")}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<ThemedText style={styles.termsText}>
|
<ThemedText style={styles.termsText} type="sfPro">
|
||||||
{t("auth.telLogin.getPhone", { ns: 'login' })}
|
{t("auth.telLogin.getPhone", { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{/* 已有账号 */}
|
{/* 已有账号 */}
|
||||||
<View style={styles.loginContainer} >
|
<View style={styles.loginContainer} >
|
||||||
<ThemedText style={styles.loginText}>
|
<ThemedText type="sfPro" color="textPrimary" size="sm">
|
||||||
{t("auth.signup.haveAccount", { ns: 'login' })}
|
{t("auth.signup.haveAccount", { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@ -328,7 +284,7 @@ const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword, setSh
|
|||||||
updateUrlParam("status", "login");
|
updateUrlParam("status", "login");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ThemedText style={styles.loginLink}>
|
<ThemedText type="sfPro" color="bgSecondary" weight="bold" size="sm">
|
||||||
{t("auth.signup.login", { ns: 'login' })}
|
{t("auth.signup.login", { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@ -395,17 +351,17 @@ const styles = StyleSheet.create({
|
|||||||
checkbox: {
|
checkbox: {
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
borderRadius: 10,
|
borderRadius: 6,
|
||||||
borderWidth: 2,
|
borderWidth: 1,
|
||||||
borderColor: '#E5E7EB',
|
borderColor: Fonts['textPrimary'],
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
checkboxChecked: {
|
checkboxChecked: {
|
||||||
backgroundColor: '#E2793F',
|
backgroundColor: Fonts["bgCheck"],
|
||||||
borderColor: '#E2793F',
|
borderColor: Fonts['bgCheck'],
|
||||||
},
|
},
|
||||||
termsTextContainer: {
|
termsTextContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@ -414,29 +370,20 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
termsText: {
|
termsText: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#1F2937',
|
color: Fonts["textPrimary"],
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
termsLink: {
|
termsLink: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#E2793F',
|
color: Fonts['bgSecondary'],
|
||||||
lineHeight: 20,
|
lineHeight: 20
|
||||||
},
|
},
|
||||||
loginContainer: {
|
loginContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginTop: 24,
|
marginTop: 24,
|
||||||
},
|
gap: 4,
|
||||||
loginText: {
|
}
|
||||||
fontSize: 14,
|
|
||||||
color: '#1F2937',
|
|
||||||
},
|
|
||||||
loginLink: {
|
|
||||||
color: '#E2793F',
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '600',
|
|
||||||
marginLeft: 4,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default SignUp;
|
export default SignUp;
|
||||||
37
components/login/ui/Button.tsx
Normal 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
|
||||||
122
components/login/ui/TextInput.tsx
Normal 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;
|
||||||
@ -22,6 +22,7 @@ const width = Dimensions.get("window").width;
|
|||||||
|
|
||||||
function CarouselComponent(props: Props) {
|
function CarouselComponent(props: Props) {
|
||||||
const { data } = props;
|
const { data } = props;
|
||||||
|
const [currentIndex, setCurrentIndex] = React.useState(0);
|
||||||
const [carouselDataValue, setCarouselDataValue] = React.useState<CarouselData[]>([]);
|
const [carouselDataValue, setCarouselDataValue] = React.useState<CarouselData[]>([]);
|
||||||
const dataHandle = () => {
|
const dataHandle = () => {
|
||||||
const carouselData = { ...data?.category_count, total_count: data?.total_count }
|
const carouselData = { ...data?.category_count, total_count: data?.total_count }
|
||||||
@ -44,7 +45,7 @@ function CarouselComponent(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const totleItem = (data: UserCountData) => {
|
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) => (
|
{Object?.entries(data)?.filter(([key]) => key !== 'cover_url')?.map((item, index) => (
|
||||||
<View style={styles.item} key={index}>
|
<View style={styles.item} key={index}>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8, width: "75%", overflow: 'hidden' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8, width: "75%", overflow: 'hidden' }}>
|
||||||
@ -68,37 +69,41 @@ function CarouselComponent(props: Props) {
|
|||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1
|
||||||
|
}}>
|
||||||
<Carousel
|
<Carousel
|
||||||
width={width}
|
width={width}
|
||||||
height={width * 0.75}
|
height={width}
|
||||||
data={carouselDataValue || []}
|
data={carouselDataValue || []}
|
||||||
mode="parallax"
|
mode="parallax"
|
||||||
// defaultIndex={
|
onSnapToItem={(index) => setCurrentIndex(index)}
|
||||||
// carouselDataValue?.length
|
defaultIndex={
|
||||||
// ? Math.max(0, Math.min(
|
carouselDataValue?.length
|
||||||
// carouselDataValue.length - 1,
|
? Math.max(0, Math.min(
|
||||||
// carouselDataValue.findIndex((item) => item?.key === 'total_count') - 1
|
carouselDataValue.length - 1,
|
||||||
// ))
|
carouselDataValue.findIndex((item) => item?.key === 'total_count') - 1
|
||||||
// : 0
|
))
|
||||||
// }
|
: 0
|
||||||
|
}
|
||||||
modeConfig={{
|
modeConfig={{
|
||||||
parallaxScrollingScale: 1,
|
parallaxScrollingScale: 1,
|
||||||
parallaxScrollingOffset: 150,
|
parallaxScrollingOffset: 130,
|
||||||
parallaxAdjacentItemScale: 0.7
|
parallaxAdjacentItemScale: 0.7
|
||||||
}}
|
}}
|
||||||
renderItem={({ item, index }) => {
|
renderItem={({ item, index }) => {
|
||||||
const style: ViewStyle = {
|
const style: ViewStyle = {
|
||||||
width: width,
|
width: width,
|
||||||
height: width * 0.8,
|
height: width * 0.7,
|
||||||
alignItems: "center",
|
alignItems: "center"
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<View key={index} style={style}>
|
<View key={index} style={[style]}>
|
||||||
{item?.key === 'total_count' ? (
|
{item?.key === 'total_count' ? (
|
||||||
totleItem(item.value)
|
totleItem(item.value)
|
||||||
) : (
|
) : (
|
||||||
<View style={{ flex: 1, width: width * 0.65 }}>
|
<View>
|
||||||
{CategoryComponent({
|
{CategoryComponent({
|
||||||
title: item?.key,
|
title: item?.key,
|
||||||
data: [
|
data: [
|
||||||
@ -107,6 +112,7 @@ function CarouselComponent(props: Props) {
|
|||||||
{ title: 'Length', number: formatDuration(item?.value?.video_length || 0) }
|
{ title: 'Length', number: formatDuration(item?.value?.video_length || 0) }
|
||||||
],
|
],
|
||||||
bgSvg: item?.value?.cover_url,
|
bgSvg: item?.value?.cover_url,
|
||||||
|
width: width
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@ -124,13 +130,13 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
borderRadius: 32,
|
borderRadius: 32,
|
||||||
padding: 4
|
padding: 6
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
backgroundColor: "#FFB645",
|
backgroundColor: "#FFB645",
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
paddingHorizontal: 16,
|
paddingLeft: 32,
|
||||||
borderRadius: 16,
|
borderRadius: 32,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@ -157,10 +163,10 @@ const styles = StyleSheet.create({
|
|||||||
number: {
|
number: {
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
fontSize: 26,
|
fontSize: 28,
|
||||||
|
lineHeight: 30,
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
flex: 1,
|
flex: 1
|
||||||
paddingTop: 8
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -6,16 +6,45 @@ import VideoTotalSvg from "@/assets/icons/svg/videoTotalWhite.svg";
|
|||||||
import { BlurView } from "expo-blur";
|
import { BlurView } from "expo-blur";
|
||||||
import { Image, StyleProp, StyleSheet, View, ViewStyle } from "react-native";
|
import { Image, StyleProp, StyleSheet, View, ViewStyle } from "react-native";
|
||||||
import { ThemedText } from "../ThemedText";
|
import { ThemedText } from "../ThemedText";
|
||||||
|
|
||||||
interface CategoryProps {
|
interface CategoryProps {
|
||||||
title: string;
|
title: string;
|
||||||
data: { title: string, number: string | number }[];
|
data: { title: string, number: { s: number, m: number, h: number } | number }[];
|
||||||
bgSvg: string | null;
|
bgSvg: string | null;
|
||||||
style?: StyleProp<ViewStyle>;
|
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 (
|
return (
|
||||||
<View style={[styles.container, style]}>
|
<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, { width: width * 0.73 }]}>
|
||||||
<View style={styles.backgroundContainer}>
|
<View style={styles.backgroundContainer}>
|
||||||
<Image
|
<Image
|
||||||
source={bgSvg !== "" && bgSvg !== null ? { uri: bgSvg } : require('@/assets/images/png/owner/people.png')}
|
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>
|
</View>
|
||||||
<ThemedText style={styles.itemTitle}>{item.title}</ThemedText>
|
<ThemedText style={styles.itemTitle}>{item.title}</ThemedText>
|
||||||
</View>
|
</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>
|
||||||
))}
|
))}
|
||||||
<View style={styles.titleContent}>
|
<View style={styles.titleContent}>
|
||||||
@ -68,7 +109,8 @@ const styles = StyleSheet.create({
|
|||||||
backdropFilter: 'blur(5px)',
|
backdropFilter: 'blur(5px)',
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
padding: 16,
|
padding: 32,
|
||||||
|
paddingRight: 16,
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
flex: 1
|
flex: 1
|
||||||
},
|
},
|
||||||
@ -78,6 +120,7 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
paddingVertical: 4
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
color: 'white',
|
color: 'white',
|
||||||
@ -95,7 +138,7 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingVertical: 8,
|
paddingVertical: 16,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
itemTitle: {
|
itemTitle: {
|
||||||
@ -108,11 +151,10 @@ const styles = StyleSheet.create({
|
|||||||
itemNumber: {
|
itemNumber: {
|
||||||
color: 'white',
|
color: 'white',
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
|
lineHeight: 30,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
marginLeft: 8,
|
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingTop: 8
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import Modal from 'react-native-modal';
|
import Modal from 'react-native-modal';
|
||||||
import Cascader, { CascaderItem } from '../cascader';
|
import Cascader, { CascaderItem } from '../cascader';
|
||||||
|
import { ThemedText } from '../ThemedText';
|
||||||
|
|
||||||
interface ClassifyModalProps {
|
interface ClassifyModalProps {
|
||||||
modalVisible: boolean;
|
modalVisible: boolean;
|
||||||
@ -39,9 +40,9 @@ const ClassifyModal = (props: ClassifyModalProps) => {
|
|||||||
<View style={styles.modalView}>
|
<View style={styles.modalView}>
|
||||||
<View style={styles.modalHeader}>
|
<View style={styles.modalHeader}>
|
||||||
<Text style={{ opacity: 0 }}>Settings</Text>
|
<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)}>
|
<TouchableOpacity onPress={() => setModalVisible(false)}>
|
||||||
<Text style={styles.closeButton}>×</Text>
|
<ThemedText style={styles.closeButton}>×</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
|
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false}>
|
||||||
|
|||||||
@ -20,12 +20,10 @@ const CreateCountComponent = React.memo((props: CreateCountProps) => {
|
|||||||
return (
|
return (
|
||||||
<View style={containerStyle}>
|
<View style={containerStyle}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View className="mt-1">
|
<ThemedText type="sfPro" weight="bold" color="textSecondary" size="xxs" style={{ lineHeight: 16 }}>{props.title}</ThemedText>
|
||||||
{props.icon}
|
{props.icon}
|
||||||
</View>
|
</View>
|
||||||
<ThemedText style={styles.title} className="!text-textSecondary">{props.title}</ThemedText>
|
<ThemedText weight="bold" color="textSecondary" size="title" style={{ lineHeight: 36, textAlign: "right" }}>{props.number}</ThemedText>
|
||||||
</View>
|
|
||||||
<ThemedText style={styles.number} className="!text-textSecondary">{props.number}</ThemedText>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -35,11 +33,12 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
alignItems: "center",
|
// alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
gap: 8,
|
gap: 8,
|
||||||
backgroundColor: "#FAF9F6",
|
backgroundColor: "#FAF9F6",
|
||||||
padding: 16,
|
paddingVertical: 16,
|
||||||
|
paddingRight: 16,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
shadowColor: "#000",
|
shadowColor: "#000",
|
||||||
shadowOffset: {
|
shadowOffset: {
|
||||||
@ -51,12 +50,12 @@ const styles = StyleSheet.create({
|
|||||||
// elevation: 1,
|
// elevation: 1,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
width: "100%",
|
width: "90%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
paddingHorizontal: 16,
|
||||||
justifyContent: "center",
|
alignItems: "flex-start",
|
||||||
gap: 8,
|
gap: 4,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
|
|||||||
@ -36,6 +36,7 @@ const PodiumComponent = ({ data }: IPodium) => {
|
|||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
ellipsizeMode="tail"
|
ellipsizeMode="tail"
|
||||||
style={styles.title}
|
style={styles.title}
|
||||||
|
type="inter"
|
||||||
>
|
>
|
||||||
{data[1]?.user_nick_name}
|
{data[1]?.user_nick_name}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
@ -65,6 +66,7 @@ const PodiumComponent = ({ data }: IPodium) => {
|
|||||||
numberOfLines={2}
|
numberOfLines={2}
|
||||||
ellipsizeMode="tail"
|
ellipsizeMode="tail"
|
||||||
style={styles.title}
|
style={styles.title}
|
||||||
|
type="inter"
|
||||||
>
|
>
|
||||||
{data[0]?.user_nick_name}
|
{data[0]?.user_nick_name}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
@ -94,6 +96,7 @@ const PodiumComponent = ({ data }: IPodium) => {
|
|||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
ellipsizeMode="tail"
|
ellipsizeMode="tail"
|
||||||
style={styles.title}
|
style={styles.title}
|
||||||
|
type="inter"
|
||||||
>
|
>
|
||||||
{data[2]?.user_nick_name}
|
{data[2]?.user_nick_name}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
|
|||||||
@ -43,10 +43,9 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
modalView: {
|
modalView: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '40%',
|
height: '50%',
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
borderTopLeftRadius: 20,
|
borderRadius: 26,
|
||||||
borderTopRightRadius: 20,
|
|
||||||
},
|
},
|
||||||
modalHeader: {
|
modalHeader: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@ -126,8 +126,8 @@ const styles = StyleSheet.create({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
height: '80%',
|
height: '80%',
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
borderTopLeftRadius: 20,
|
borderTopLeftRadius: 26,
|
||||||
borderTopRightRadius: 20,
|
borderTopRightRadius: 26,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
},
|
},
|
||||||
modalHeader: {
|
modalHeader: {
|
||||||
|
|||||||
@ -15,9 +15,9 @@ const RankList = (props: IRankList) => {
|
|||||||
contentContainerStyle={{ flexGrow: 1, paddingHorizontal: 16, marginTop: 16 }}
|
contentContainerStyle={{ flexGrow: 1, paddingHorizontal: 16, marginTop: 16 }}
|
||||||
>
|
>
|
||||||
<View style={styles.item}>
|
<View style={styles.item}>
|
||||||
<ThemedText style={styles.headerText}>Rank</ThemedText>
|
<ThemedText style={styles.headerText} type="sfPro">Rank</ThemedText>
|
||||||
<ThemedText style={styles.headerText}>Username</ThemedText>
|
<ThemedText style={styles.headerText} type="sfPro">Username</ThemedText>
|
||||||
<ThemedText style={styles.headerText}>Profile</ThemedText>
|
<ThemedText style={styles.headerText} type="sfPro">Profile</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
{props.data?.filter((item, index) => index > 2).map((item, index) => (
|
{props.data?.filter((item, index) => index > 2).map((item, index) => (
|
||||||
<View
|
<View
|
||||||
@ -35,8 +35,8 @@ const RankList = (props: IRankList) => {
|
|||||||
<OwnerSvg width="100%" height="100%" />
|
<OwnerSvg width="100%" height="100%" />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<ThemedText style={styles.itemRank}>{index + 1}</ThemedText>
|
<ThemedText style={styles.itemRank} type="sfPro">{index + 1}</ThemedText>
|
||||||
<ThemedText style={styles.itemName}>{item.user_nick_name}</ThemedText>
|
<ThemedText style={styles.itemName} type="inter">{item.user_nick_name}</ThemedText>
|
||||||
<View style={{ opacity: index == 1 ? 0 : 1 }}>
|
<View style={{ opacity: index == 1 ? 0 : 1 }}>
|
||||||
{(() => {
|
{(() => {
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
@ -66,9 +66,9 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
},
|
},
|
||||||
headerText: {
|
headerText: {
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
color: "#4C320C",
|
color: "#4C320C",
|
||||||
fontWeight: "600"
|
fontWeight: "500"
|
||||||
},
|
},
|
||||||
item: {
|
item: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@ -85,7 +85,7 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: "700"
|
fontWeight: "700"
|
||||||
},
|
},
|
||||||
itemName: {
|
itemName: {
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
color: "#AC7E35",
|
color: "#AC7E35",
|
||||||
},
|
},
|
||||||
self: {
|
self: {
|
||||||
|
|||||||
@ -1,21 +1,24 @@
|
|||||||
import MemberBgSvg from '@/assets/icons/svg/memberBg.svg';
|
|
||||||
import ProTextSvg from '@/assets/icons/svg/proText.svg';
|
import ProTextSvg from '@/assets/icons/svg/proText.svg';
|
||||||
import GradientText from '@/components/textLinear';
|
import GradientText from '@/components/textLinear';
|
||||||
import { ThemedText } from '@/components/ThemedText';
|
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Dimensions, StyleSheet, TouchableOpacity, View } from "react-native";
|
import { Dimensions, StyleSheet, TouchableOpacity, View } from "react-native";
|
||||||
import CardBg from './cardBg';
|
import CardBg from './cardBg';
|
||||||
import IpSvg from './ipSvg';
|
import IpSvg from './ipSvg';
|
||||||
|
|
||||||
const MemberCard = ({ pro }: { pro: string }) => {
|
const MemberCard = ({ pro, points }: { pro: string, points: number }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const proPng = require("@/assets/images/png/owner/pro.png");
|
|
||||||
const width = Dimensions.get("window").width;
|
const width = Dimensions.get("window").width;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
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 }]}>
|
<View style={[styles.cardBg, { opacity: pro === "pro" ? 1 : 0.5 }]}>
|
||||||
<CardBg pro={pro} date={"2025-09-05"} />
|
<CardBg pro={pro} date={"2025-09-05"} />
|
||||||
@ -29,20 +32,20 @@ const MemberCard = ({ pro }: { pro: string }) => {
|
|||||||
<IpSvg pro={pro} />
|
<IpSvg pro={pro} />
|
||||||
</View>
|
</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 />
|
<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>
|
<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
|
<GradientText
|
||||||
text={t("personal:member.unlock")}
|
text={t("personal:member.unlock")}
|
||||||
width={width * 0.4}
|
|
||||||
fontSize={16}
|
fontSize={16}
|
||||||
lineHeight={1.5}
|
lineHeight={1.5}
|
||||||
color={[
|
color={[
|
||||||
{ offset: "0%", color: "#FF512F" },
|
{ offset: "0%", color: "#D0BFB0" },
|
||||||
{ offset: "100%", color: "#F09819" }
|
{ offset: "32.89%", color: "#FFE57D" },
|
||||||
|
{ offset: "81.1%", color: "#FFFFFF" }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -50,7 +50,7 @@ const styles = StyleSheet.create({
|
|||||||
goPro: {
|
goPro: {
|
||||||
backgroundColor: '#E2793F',
|
backgroundColor: '#E2793F',
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
paddingVertical: 6,
|
paddingVertical: 12,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
|||||||
@ -64,10 +64,10 @@ const Premium = (props: Props) => {
|
|||||||
</ThemedText>
|
</ThemedText>
|
||||||
<BlackStarSvg />
|
<BlackStarSvg />
|
||||||
</View>
|
</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]}
|
{item.product_code?.split('_')[item.product_code?.split('_')?.length - 1]}
|
||||||
</ThemedText>
|
</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)}
|
$ {(Number(item.unit_price.amount) - Number(item.discount_amount.amount)).toFixed(2)}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||||
|
|||||||
@ -79,11 +79,11 @@ const UserInfo = (props: UserInfoProps) => {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.info}>
|
<View style={styles.info}>
|
||||||
<ThemedText style={styles.nickname}>{userInfo?.nickname}</ThemedText>
|
<ThemedText style={styles.nickname} type="sfPro">{userInfo?.nickname}</ThemedText>
|
||||||
<ThemedText style={styles.userId}>{t('generalSetting.userId', { ns: 'personal' })}{userInfo?.user_id}</ThemedText>
|
<ThemedText style={styles.userId} type="inter">{t('generalSetting.userId', { ns: 'personal' })}{userInfo?.user_id}</ThemedText>
|
||||||
<View style={styles.location}>
|
<View style={styles.location}>
|
||||||
<LocationSvg />
|
<LocationSvg />
|
||||||
<ThemedText style={styles.userId}>
|
<ThemedText style={styles.userId} type="inter">
|
||||||
{currentLocation?.country}-{currentLocation?.city}-{currentLocation?.district}
|
{currentLocation?.country}-{currentLocation?.city}-{currentLocation?.district}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { ThemedText } from '@/components/ThemedText';
|
|||||||
import { UserInfoDetails } from '@/types/user';
|
import { UserInfoDetails } from '@/types/user';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { useState } from 'react';
|
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';
|
import CopyButton from '../copy';
|
||||||
|
|
||||||
export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
|
export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
|
||||||
@ -14,7 +14,7 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className='flex flex-row justify-between items-center mt-[1rem] gap-[1rem] w-full'>
|
<View style={styles.container}>
|
||||||
{/* 头像 */}
|
{/* 头像 */}
|
||||||
<View className='w-auto'>
|
<View className='w-auto'>
|
||||||
{userInfo?.user_info?.avatar_file_url && !imageError ? (
|
{userInfo?.user_info?.avatar_file_url && !imageError ? (
|
||||||
@ -30,45 +30,34 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</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 justify-between w-full'>
|
||||||
<View className='flex flex-row items-center gap-2 w-full'>
|
<View className='flex flex-row items-center gap-2 w-full justify-between'>
|
||||||
|
<View style={{ width: "100%", flexDirection: "row", alignItems: "center", gap: 2 }}>
|
||||||
<ThemedText
|
<ThemedText
|
||||||
className='max-w-[80%] !text-textSecondary !font-semibold !text-2xl'
|
style={{ maxWidth: "90%", lineHeight: 30 }}
|
||||||
numberOfLines={1} // 限制为1行
|
weight='bold'
|
||||||
|
color='textSecondary'
|
||||||
|
size='3xl'
|
||||||
|
numberOfLines={1}
|
||||||
ellipsizeMode="tail"
|
ellipsizeMode="tail"
|
||||||
|
|
||||||
>
|
>
|
||||||
{userInfo?.user_info?.nickname}
|
{userInfo?.user_info?.nickname}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<ScrollView
|
|
||||||
className='max-w-[20%]'
|
|
||||||
horizontal // 水平滚动
|
|
||||||
showsHorizontalScrollIndicator={false} // 隐藏滚动条
|
|
||||||
contentContainerStyle={{
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: 8, // 间距,
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{
|
{
|
||||||
userInfo?.medal_infos?.map((item, index) => (
|
userInfo?.membership_level && (
|
||||||
<Image
|
<Image
|
||||||
key={index}
|
source={require('@/assets/images/png/owner/proIcon.png')}
|
||||||
source={{ uri: item.url }}
|
|
||||||
style={{ width: 24, height: 24 }}
|
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>
|
||||||
</View>
|
</View>
|
||||||
<View className='flex flex-row items-center justify-between w-full'>
|
<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
|
<ThemedText
|
||||||
style={{
|
style={{
|
||||||
color: '#AC7E35',
|
color: '#AC7E35',
|
||||||
@ -77,6 +66,7 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
|
|||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
flexGrow: 0,
|
flexGrow: 0,
|
||||||
}}
|
}}
|
||||||
|
type="inter"
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
ellipsizeMode="tail"
|
ellipsizeMode="tail"
|
||||||
>
|
>
|
||||||
@ -84,6 +74,35 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
|
|||||||
</ThemedText>
|
</ThemedText>
|
||||||
<CopyButton textToCopy={userInfo?.user_info?.user_id || ""} />
|
<CopyButton textToCopy={userInfo?.user_info?.user_id || ""} />
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={{ flexDirection: "column", alignItems: "flex-end", gap: 4 }}>
|
||||||
|
<View style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#FFB645',
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 16,
|
||||||
|
}}>
|
||||||
|
<StarSvg />
|
||||||
|
<ThemedText
|
||||||
|
style={{
|
||||||
|
color: '#4C320C',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '700',
|
||||||
|
maxWidth: 40,
|
||||||
|
lineHeight: 20
|
||||||
|
}}
|
||||||
|
type="inter"
|
||||||
|
numberOfLines={1}
|
||||||
|
ellipsizeMode="tail"
|
||||||
|
>
|
||||||
|
{userInfo?.remain_points}
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
router.push('/setting');
|
router.push('/setting');
|
||||||
@ -96,11 +115,16 @@ export default function UserInfo({ userInfo }: { userInfo: UserInfoDetails }) {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View >
|
</View >
|
||||||
</View >
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
text: {
|
text: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
|
|||||||
@ -20,7 +20,7 @@ const StepButton = (props: Props) => {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<ActivityIndicator color="#fff" />
|
<ActivityIndicator color="#fff" />
|
||||||
) : (
|
) : (
|
||||||
<ThemedText style={[styles.buttonText, { color: color ? color : '#FFFFFF' }]}>
|
<ThemedText style={[styles.buttonText, { color: color ? color : '#FFFFFF' }]} size='lg' weight='bold'>
|
||||||
{text}
|
{text}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -181,7 +181,7 @@ const styles = StyleSheet.create({
|
|||||||
zIndex: 99,
|
zIndex: 99,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 36,
|
fontSize: 32,
|
||||||
lineHeight: 40,
|
lineHeight: 40,
|
||||||
color: '#FFFFFF',
|
color: '#FFFFFF',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export default function Look(props: Props) {
|
|||||||
<ThemedText style={styles.title}>
|
<ThemedText style={styles.title}>
|
||||||
{t('auth.userMessage.look', { ns: 'login' })}
|
{t('auth.userMessage.look', { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<ThemedText style={styles.subtitle}>
|
<ThemedText style={styles.subtitle} type="inter" size="sm" weight="regular">
|
||||||
{t('auth.userMessage.avatarText', { ns: 'login' })}
|
{t('auth.userMessage.avatarText', { ns: 'login' })}
|
||||||
{"\n"}
|
{"\n"}
|
||||||
{t('auth.userMessage.avatorText2', { ns: 'login' })}
|
{t('auth.userMessage.avatorText2', { ns: 'login' })}
|
||||||
@ -55,7 +55,7 @@ export default function Look(props: Props) {
|
|||||||
children={
|
children={
|
||||||
<View style={styles.uploadButton}>
|
<View style={styles.uploadButton}>
|
||||||
<ChoicePhoto />
|
<ChoicePhoto />
|
||||||
<ThemedText style={styles.uploadButtonText}>
|
<ThemedText style={styles.uploadButtonText} type="sfPro" size="sm" weight="semiBold">
|
||||||
{t('auth.userMessage.choosePhoto', { ns: 'login' })}
|
{t('auth.userMessage.choosePhoto', { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
@ -123,9 +123,7 @@ const styles = StyleSheet.create({
|
|||||||
gap: 8,
|
gap: 8,
|
||||||
},
|
},
|
||||||
uploadButtonText: {
|
uploadButtonText: {
|
||||||
color: '#4C320C',
|
color: '#4C320C'
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|||||||
@ -39,11 +39,11 @@ export default function UserName(props: Props) {
|
|||||||
<View style={[styles.inputContainer, { paddingBottom: insets.bottom }]}>
|
<View style={[styles.inputContainer, { paddingBottom: insets.bottom }]}>
|
||||||
<View style={styles.contentContainer}>
|
<View style={styles.contentContainer}>
|
||||||
<View style={styles.titleContainer}>
|
<View style={styles.titleContainer}>
|
||||||
<ThemedText style={styles.titleText}>{t('auth.userMessage.title', { ns: 'login' })}</ThemedText>
|
<ThemedText style={styles.titleText} color="textSecondary" size='xl' weight='bold'>{t('auth.userMessage.title', { ns: 'login' })}</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.inputWrapper}>
|
<View style={styles.inputWrapper}>
|
||||||
<View style={styles.labelContainer}>
|
<View style={styles.labelContainer}>
|
||||||
<ThemedText style={styles.labelText}>{t('auth.userMessage.username', { ns: 'login' })}</ThemedText>
|
<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>
|
<ThemedText style={styles.errorText}>{error}</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
<TextInput
|
<TextInput
|
||||||
@ -98,9 +98,6 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
},
|
},
|
||||||
titleText: {
|
titleText: {
|
||||||
color: '#4C320C',
|
|
||||||
fontWeight: '600',
|
|
||||||
fontSize: 20,
|
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
},
|
},
|
||||||
inputWrapper: {
|
inputWrapper: {
|
||||||
|
|||||||
@ -3,9 +3,9 @@
|
|||||||
* @param seconds 总秒数
|
* @param seconds 总秒数
|
||||||
* @returns 格式化后的时间字符串
|
* @returns 格式化后的时间字符串
|
||||||
*/
|
*/
|
||||||
export function formatDuration(seconds: number): string {
|
export function formatDuration(seconds: number): { s: number, m: number, h: number } {
|
||||||
if (seconds < 60) {
|
if (seconds < 60) {
|
||||||
return `${seconds}s`;
|
return { s: seconds, m: 0, h: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60);
|
||||||
@ -13,16 +13,16 @@ export function formatDuration(seconds: number): string {
|
|||||||
|
|
||||||
if (minutes < 60) {
|
if (minutes < 60) {
|
||||||
return remainingSeconds > 0
|
return remainingSeconds > 0
|
||||||
? `${minutes}min${remainingSeconds}s`
|
? { s: remainingSeconds, m: minutes, h: 0 }
|
||||||
: `${minutes}min`;
|
: { s: 0, m: minutes, h: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const hours = Math.floor(minutes / 60);
|
const hours = Math.floor(minutes / 60);
|
||||||
const remainingMinutes = minutes % 60;
|
const remainingMinutes = minutes % 60;
|
||||||
|
|
||||||
if (remainingMinutes === 0) {
|
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
@ -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';
|
||||||
|
|
||||||
@ -67,7 +67,8 @@
|
|||||||
"accountPlaceholder": "Enter your account or email",
|
"accountPlaceholder": "Enter your account or email",
|
||||||
"signUpMessage": "Don’t have an account?",
|
"signUpMessage": "Don’t have an account?",
|
||||||
"signUp": "Sign up",
|
"signUp": "Sign up",
|
||||||
"phoneLogin": "Phone Login"
|
"phoneLogin": "Phone Login",
|
||||||
|
"titleText": "Awake your Memo"
|
||||||
},
|
},
|
||||||
"agree": {
|
"agree": {
|
||||||
"logintext": "By logging in, you agree to our",
|
"logintext": "By logging in, you agree to our",
|
||||||
|
|||||||
@ -74,7 +74,7 @@
|
|||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"location": "Location",
|
"location": "Location",
|
||||||
"rank": "Top Memory Makers",
|
"rank": "Top Memory Makers",
|
||||||
"userId": "User ID",
|
"userId": "User ID : ",
|
||||||
"usedStorage": "Storage Used",
|
"usedStorage": "Storage Used",
|
||||||
"remainingPoints": "Points Remaining",
|
"remainingPoints": "Points Remaining",
|
||||||
"totalVideo": "Total Videos",
|
"totalVideo": "Total Videos",
|
||||||
@ -128,6 +128,6 @@
|
|||||||
},
|
},
|
||||||
"member": {
|
"member": {
|
||||||
"goPremium": "Go Premium",
|
"goPremium": "Go Premium",
|
||||||
"unlock": "Unlock more memory magic"
|
"unlock": "解锁更多记忆魔法"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,7 +67,8 @@
|
|||||||
"accountPlaceholder": "请输入您的账号或邮箱",
|
"accountPlaceholder": "请输入您的账号或邮箱",
|
||||||
"signUpMessage": "还没有账号?",
|
"signUpMessage": "还没有账号?",
|
||||||
"signUp": "注册",
|
"signUp": "注册",
|
||||||
"phoneLogin": "手机号登录"
|
"phoneLogin": "手机号登录",
|
||||||
|
"titleText": "Awake your Memo"
|
||||||
},
|
},
|
||||||
"agree": {
|
"agree": {
|
||||||
"logintext": "登录即表示您同意我们的",
|
"logintext": "登录即表示您同意我们的",
|
||||||
|
|||||||