fix: error
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import { ThemedText } from "@/components/themed-text";
|
||||
import { ThemedView } from "@/components/themed-view";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { authClient } from "@/lib/auth/client";
|
||||
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { router } from "expo-router";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
@@ -10,15 +9,21 @@ import {
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { storage } from '../../lib/storage';
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { authClient } from "@/lib/auth/client";
|
||||
import { storage } from "@/lib/storage";
|
||||
|
||||
export default function LoginScreen() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isPasswordVisible, setPasswordVisible] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { isAuthenticated, isLoading: authLoading } = useAuth();
|
||||
|
||||
@@ -32,37 +37,42 @@ export default function LoginScreen() {
|
||||
const handleLogin = async () => {
|
||||
console.log("[Login] handleLogin called");
|
||||
|
||||
if (!username.trim() || !password.trim()) {
|
||||
console.log("[Login] Validation failed: empty username or password");
|
||||
Alert.alert("错误", "请输入用户名和密码");
|
||||
if (!email.trim() || !password.trim()) {
|
||||
console.log("[Login] Validation failed: empty email or password");
|
||||
Alert.alert("错误", "请输入邮箱和密码");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[Login] Starting login with username:", username.trim());
|
||||
console.log("[Login] Starting login with email:", email.trim());
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
console.log("[Login] Calling authClient.signIn.username...");
|
||||
await new Promise<void>(async (resolve, reject) => {
|
||||
await authClient.signIn.username({
|
||||
username: username.trim(),
|
||||
password,
|
||||
}, {
|
||||
onSuccess: async (ctx) => {
|
||||
const authToken = ctx.response.headers.get("set-auth-token")
|
||||
if (authToken) {
|
||||
await storage.setItem(`bestaibest.better-auth.session_token`, authToken);
|
||||
resolve()
|
||||
} else {
|
||||
console.error("Bearer token not set");
|
||||
reject()
|
||||
}
|
||||
await authClient.signIn.username(
|
||||
{
|
||||
username: email.trim(),
|
||||
password,
|
||||
},
|
||||
{
|
||||
onSuccess: async (ctx) => {
|
||||
const authToken = ctx.response.headers.get("set-auth-token");
|
||||
if (authToken) {
|
||||
await storage.setItem(
|
||||
`bestaibest.better-auth.session_token`,
|
||||
authToken
|
||||
);
|
||||
resolve();
|
||||
} else {
|
||||
console.error("Bearer token not set");
|
||||
reject();
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
);
|
||||
});
|
||||
} catch (error: any) {
|
||||
Alert.alert("登录失败", error?.message || "用户名或密码错误");
|
||||
Alert.alert("登录失败", error?.message || "邮箱或密码错误");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
console.log("[Login] handleLogin completed");
|
||||
@@ -70,111 +80,248 @@ export default function LoginScreen() {
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
style={styles.container}
|
||||
>
|
||||
<ThemedView style={styles.content}>
|
||||
<ThemedText type="title" style={styles.title}>
|
||||
欢迎登录
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.subtitle}>请输入您的账号信息</ThemedText>
|
||||
<LinearGradient colors={["#050506", "#0b0c0f"]} style={styles.gradient}>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<StatusBar style="light" />
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||
style={styles.flex}
|
||||
>
|
||||
<View style={styles.screen}>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={() => router.back()}
|
||||
style={styles.backButton}
|
||||
>
|
||||
<Feather name="arrow-left" size={20} color="#f2f4f8" />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.form}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="用户名"
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
editable={!isLoading}
|
||||
/>
|
||||
<View style={styles.sheet}>
|
||||
<View style={styles.sheetHeader}>
|
||||
<View style={styles.sheetTitleGroup}>
|
||||
<View style={styles.iconBadge}>
|
||||
<Ionicons name="mail-open" size={16} color="#ffffff" />
|
||||
</View>
|
||||
<Text style={styles.sheetTitle}>Email Login</Text>
|
||||
</View>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="密码"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
editable={!isLoading}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={() => router.push("/(auth)/register")}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Text style={[styles.registerLabel, isLoading && styles.disabledText]}>
|
||||
Register
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, isLoading && styles.buttonDisabled]}
|
||||
onPress={handleLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<ThemedText style={styles.buttonText}>登录</ThemedText>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputWrapper}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="yours@example.com"
|
||||
placeholderTextColor="#70737c"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardType="email-address"
|
||||
editable={!isLoading}
|
||||
keyboardAppearance="dark"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.registerLink}
|
||||
onPress={() => router.push("/(auth)/register")}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<ThemedText style={styles.registerText}>还没有账号?立即注册</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ThemedView>
|
||||
</KeyboardAvoidingView>
|
||||
<View style={styles.inputWrapper}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="email password"
|
||||
placeholderTextColor="#70737c"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry={!isPasswordVisible}
|
||||
autoCorrect={false}
|
||||
editable={!isLoading}
|
||||
keyboardAppearance="dark"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={() => setPasswordVisible((visible) => !visible)}
|
||||
style={styles.visibilityToggle}
|
||||
>
|
||||
<Feather
|
||||
name={isPasswordVisible ? "eye-off" : "eye"}
|
||||
size={18}
|
||||
color="#8a8e97"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.loginButton, isLoading && styles.loginButtonDisabled]}
|
||||
onPress={handleLogin}
|
||||
disabled={isLoading}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color="#0f100f" />
|
||||
) : (
|
||||
<Text style={styles.loginButtonText}>Log in</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.termsRow}>
|
||||
<Ionicons name="checkmark-circle" size={16} color="#d7ff1f" />
|
||||
<Text style={styles.termsText}>
|
||||
已阅读并同意
|
||||
<Text style={styles.linkText}> 用户协议 </Text>
|
||||
和
|
||||
<Text style={styles.linkText}> 隐私政策</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</LinearGradient>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
gradient: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
padding: 24,
|
||||
},
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
screen: {
|
||||
flex: 1,
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 24,
|
||||
},
|
||||
backButton: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.05)",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255, 255, 255, 0.06)",
|
||||
marginBottom: 28,
|
||||
},
|
||||
title: {
|
||||
marginBottom: 8,
|
||||
textAlign: "center",
|
||||
sheet: {
|
||||
backgroundColor: "#14161c",
|
||||
borderRadius: 28,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 28,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255, 255, 255, 0.08)",
|
||||
shadowColor: "#000",
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 18,
|
||||
shadowOffset: { width: 0, height: -8 },
|
||||
elevation: 24,
|
||||
},
|
||||
subtitle: {
|
||||
marginBottom: 32,
|
||||
textAlign: "center",
|
||||
opacity: 0.7,
|
||||
sheetHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 28,
|
||||
},
|
||||
sheetTitleGroup: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
iconBadge: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 16,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#f6474d",
|
||||
marginRight: 12,
|
||||
},
|
||||
sheetTitle: {
|
||||
color: "#f5f6f8",
|
||||
fontSize: 22,
|
||||
fontWeight: "700",
|
||||
letterSpacing: 0.2,
|
||||
},
|
||||
registerLabel: {
|
||||
color: "#b7bac2",
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
disabledText: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
form: {
|
||||
gap: 16,
|
||||
marginTop: 4,
|
||||
},
|
||||
inputWrapper: {
|
||||
position: "relative",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 18,
|
||||
backgroundColor: "#1a1d23",
|
||||
borderRadius: 18,
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255, 255, 255, 0.05)",
|
||||
marginBottom: 18,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#ddd",
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
flex: 1,
|
||||
paddingVertical: 16,
|
||||
fontSize: 16,
|
||||
backgroundColor: "#fff",
|
||||
color: "#f5f6f8",
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: "#007AFF",
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
visibilityToggle: {
|
||||
marginLeft: 12,
|
||||
},
|
||||
loginButton: {
|
||||
height: 56,
|
||||
borderRadius: 18,
|
||||
alignItems: "center",
|
||||
marginTop: 8,
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#d7ff1f",
|
||||
shadowColor: "#d7ff1f",
|
||||
shadowOpacity: 0.35,
|
||||
shadowRadius: 16,
|
||||
shadowOffset: { width: 0, height: 10 },
|
||||
elevation: 10,
|
||||
marginTop: 4,
|
||||
},
|
||||
buttonDisabled: {
|
||||
opacity: 0.6,
|
||||
loginButtonDisabled: {
|
||||
opacity: 0.7,
|
||||
},
|
||||
buttonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
loginButtonText: {
|
||||
color: "#10120d",
|
||||
fontSize: 17,
|
||||
fontWeight: "700",
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
termsRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginTop: 18,
|
||||
},
|
||||
termsText: {
|
||||
color: "#a7abb5",
|
||||
fontSize: 13,
|
||||
lineHeight: 18,
|
||||
marginLeft: 8,
|
||||
},
|
||||
linkText: {
|
||||
color: "#f5f6f8",
|
||||
fontWeight: "600",
|
||||
},
|
||||
registerLink: {
|
||||
alignItems: "center",
|
||||
marginTop: 8,
|
||||
},
|
||||
registerText: {
|
||||
color: "#007AFF",
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,112 +1,122 @@
|
||||
import { Image } from 'expo-image';
|
||||
import { Platform, StyleSheet } from 'react-native';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||
|
||||
import { Collapsible } from '@/components/ui/collapsible';
|
||||
import { ExternalLink } from '@/components/external-link';
|
||||
import ParallaxScrollView from '@/components/parallax-scroll-view';
|
||||
import { ThemedText } from '@/components/themed-text';
|
||||
import { ThemedView } from '@/components/themed-view';
|
||||
import { IconSymbol } from '@/components/ui/icon-symbol';
|
||||
import { Fonts } from '@/constants/theme';
|
||||
import {
|
||||
CategoryTabs,
|
||||
CommunityGrid,
|
||||
FeatureCarousel,
|
||||
Header,
|
||||
PageLayout,
|
||||
SectionHeader,
|
||||
StatusBarSpacer,
|
||||
type CommunityItem,
|
||||
type FeatureItem,
|
||||
} from '@/components/bestai';
|
||||
|
||||
const categories = [
|
||||
{ id: 'visual-effects', label: 'Visual Effects' },
|
||||
{ id: 'higgsfield-soul', label: 'Higgsfield Soul' },
|
||||
{ id: 'higgsfield-apps', label: 'Higgsfield Apps' },
|
||||
{ id: 'king', label: 'King' },
|
||||
];
|
||||
|
||||
const baseFeatureItems: FeatureItem[] = [
|
||||
{
|
||||
id: 'sketch-video',
|
||||
title: 'UNLIMITED SKETCH TO VIDEO',
|
||||
subtitle: 'Bring your imagination to life with Sora 2',
|
||||
image: 'https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=800&q=80',
|
||||
},
|
||||
{
|
||||
id: 'portrait-video',
|
||||
title: 'UNLIMITED PORTRAIT TO VIDEO',
|
||||
subtitle: 'Transform portraits into motion-driven stories',
|
||||
image: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=800&q=80',
|
||||
},
|
||||
{
|
||||
id: 'water-simulation',
|
||||
title: 'FLUID SIMULATION PACK',
|
||||
subtitle: 'Realistic water physics for cinematic scenes',
|
||||
image: 'https://images.unsplash.com/photo-1505744386214-51dba16a26fc?auto=format&fit=crop&w=800&q=80',
|
||||
},
|
||||
];
|
||||
|
||||
const baseCommunityItems: CommunityItem[] = [
|
||||
{
|
||||
id: 'community-1',
|
||||
chip: '64/10',
|
||||
title: 'OBJECTS AROUND',
|
||||
image: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=800&q=80',
|
||||
actionLabel: 'Generate',
|
||||
},
|
||||
{
|
||||
id: 'community-2',
|
||||
chip: '64/10',
|
||||
title: 'OBJECTS AROUND',
|
||||
image: 'https://images.unsplash.com/photo-1544723795-3fb6469f5b39?auto=format&fit=crop&w=800&q=80',
|
||||
actionLabel: 'Generate',
|
||||
},
|
||||
{
|
||||
id: 'community-3',
|
||||
chip: '64/10',
|
||||
title: 'OBJECTS AROUND',
|
||||
image: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=800&q=80',
|
||||
actionLabel: 'Generate',
|
||||
},
|
||||
{
|
||||
id: 'community-4',
|
||||
chip: '64/10',
|
||||
title: 'OBJECTS AROUND',
|
||||
image: 'https://images.unsplash.com/photo-1531259683007-016a7b628fc4?auto=format&fit=crop&w=800&q=80',
|
||||
actionLabel: 'Generate',
|
||||
},
|
||||
];
|
||||
|
||||
export default function ExploreScreen() {
|
||||
const [activeCategory, setActiveCategory] = useState(categories[0]?.id ?? 'visual-effects');
|
||||
|
||||
const featureItems = useMemo(() => {
|
||||
return baseFeatureItems.map((item, index) => ({
|
||||
...item,
|
||||
id: `${activeCategory}-${index}`,
|
||||
}));
|
||||
}, [activeCategory]);
|
||||
|
||||
const communityItems = useMemo(() => {
|
||||
return baseCommunityItems.map((item, index) => ({
|
||||
...item,
|
||||
id: `${activeCategory}-${index}`,
|
||||
}));
|
||||
}, [activeCategory]);
|
||||
|
||||
export default function TabTwoScreen() {
|
||||
return (
|
||||
<ParallaxScrollView
|
||||
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
|
||||
headerImage={
|
||||
<IconSymbol
|
||||
size={310}
|
||||
color="#808080"
|
||||
name="chevron.left.forwardslash.chevron.right"
|
||||
style={styles.headerImage}
|
||||
/>
|
||||
}>
|
||||
<ThemedView style={styles.titleContainer}>
|
||||
<ThemedText
|
||||
type="title"
|
||||
style={{
|
||||
fontFamily: Fonts.rounded,
|
||||
}}>
|
||||
Explore 222
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
<ThemedText>This app includes example code to help you get started.</ThemedText>
|
||||
<Collapsible title="File-based routing">
|
||||
<ThemedText>
|
||||
This app has two screens:{' '}
|
||||
<ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '}
|
||||
<ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText>
|
||||
</ThemedText>
|
||||
<ThemedText>
|
||||
The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '}
|
||||
sets up the tab navigator.
|
||||
</ThemedText>
|
||||
<ExternalLink href="https://docs.expo.dev/router/introduction">
|
||||
<ThemedText type="link">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
<Collapsible title="Android, iOS, and web support">
|
||||
<ThemedText>
|
||||
You can open this project on Android, iOS, and the web. To open the web version, press{' '}
|
||||
<ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project.
|
||||
</ThemedText>
|
||||
</Collapsible>
|
||||
<Collapsible title="Images">
|
||||
<ThemedText>
|
||||
For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '}
|
||||
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
|
||||
different screen densities
|
||||
</ThemedText>
|
||||
<Image
|
||||
source={require('@/assets/images/react-logo.png')}
|
||||
style={{ width: 100, height: 100, alignSelf: 'center' }}
|
||||
/>
|
||||
<ExternalLink href="https://reactnative.dev/docs/images">
|
||||
<ThemedText type="link">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
<Collapsible title="Light and dark mode components">
|
||||
<ThemedText>
|
||||
This template has light and dark mode support. The{' '}
|
||||
<ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect
|
||||
what the user's current color scheme is, and so you can adjust UI colors accordingly.
|
||||
</ThemedText>
|
||||
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
|
||||
<ThemedText type="link">Learn more</ThemedText>
|
||||
</ExternalLink>
|
||||
</Collapsible>
|
||||
<Collapsible title="Animations">
|
||||
<ThemedText>
|
||||
This template includes an example of an animated component. The{' '}
|
||||
<ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses
|
||||
the powerful{' '}
|
||||
<ThemedText type="defaultSemiBold" style={{ fontFamily: Fonts.mono }}>
|
||||
react-native-reanimated
|
||||
</ThemedText>{' '}
|
||||
library to create a waving hand animation.
|
||||
</ThemedText>
|
||||
{Platform.select({
|
||||
ios: (
|
||||
<ThemedText>
|
||||
The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '}
|
||||
component provides a parallax effect for the header image.
|
||||
</ThemedText>
|
||||
),
|
||||
})}
|
||||
</Collapsible>
|
||||
</ParallaxScrollView>
|
||||
<PageLayout backgroundColor="#050505">
|
||||
<ScrollView
|
||||
style={styles.scroll}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<StatusBarSpacer />
|
||||
<Header />
|
||||
<CategoryTabs categories={categories} activeId={activeCategory} onChange={setActiveCategory} />
|
||||
<FeatureCarousel items={featureItems} />
|
||||
<SectionHeader title="WAN 2.5 COMMUNITY" />
|
||||
<CommunityGrid items={communityItems} />
|
||||
<View style={styles.bottomSpacer} />
|
||||
</ScrollView>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
headerImage: {
|
||||
color: '#808080',
|
||||
bottom: -90,
|
||||
left: -35,
|
||||
position: 'absolute',
|
||||
scroll: {
|
||||
flex: 1,
|
||||
},
|
||||
titleContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
contentContainer: {
|
||||
paddingTop: 16,
|
||||
paddingBottom: 48,
|
||||
},
|
||||
bottomSpacer: {
|
||||
height: 32,
|
||||
},
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
287
app/points-details.tsx
Normal file
287
app/points-details.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
import Ionicons from '@expo/vector-icons/Ionicons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
FlatList,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
type LedgerKind = 'obtained' | 'consumed';
|
||||
|
||||
type LedgerEntry = {
|
||||
id: string;
|
||||
title: string;
|
||||
happenedAt: string;
|
||||
amount: number;
|
||||
kind: LedgerKind;
|
||||
};
|
||||
|
||||
type FilterKey = 'all' | LedgerKind;
|
||||
|
||||
const screenPalette = {
|
||||
background: '#080808',
|
||||
surface: '#111111',
|
||||
surfaceActive: '#1C1C1C',
|
||||
divider: '#1A1A1A',
|
||||
primaryText: '#F5F5F5',
|
||||
secondaryText: '#6C6C6C',
|
||||
accent: '#FEB840',
|
||||
accentHalo: 'rgba(254, 184, 64, 0.18)',
|
||||
negative: '#B3B3B3',
|
||||
};
|
||||
|
||||
const FILTERS: { key: FilterKey; label: string }[] = [
|
||||
{ key: 'all', label: 'All records' },
|
||||
{ key: 'consumed', label: 'Consumed' },
|
||||
{ key: 'obtained', label: 'Obtained' },
|
||||
];
|
||||
|
||||
const LEDGER: LedgerEntry[] = [
|
||||
{
|
||||
id: 'obtained-1',
|
||||
title: 'Income Name',
|
||||
happenedAt: '2025-10-22 11:11',
|
||||
amount: 60,
|
||||
kind: 'obtained',
|
||||
},
|
||||
{
|
||||
id: 'consumed-1',
|
||||
title: 'Expenditure Name',
|
||||
happenedAt: '2025-10-22 11:11',
|
||||
amount: -60,
|
||||
kind: 'consumed',
|
||||
},
|
||||
{
|
||||
id: 'consumed-2',
|
||||
title: 'Expenditure Name',
|
||||
happenedAt: '2025-10-22 11:11',
|
||||
amount: -60,
|
||||
kind: 'consumed',
|
||||
},
|
||||
{
|
||||
id: 'obtained-2',
|
||||
title: 'Income Name',
|
||||
happenedAt: '2025-10-22 11:11',
|
||||
amount: 60,
|
||||
kind: 'obtained',
|
||||
},
|
||||
{
|
||||
id: 'obtained-3',
|
||||
title: 'Income Name',
|
||||
happenedAt: '2025-10-22 11:11',
|
||||
amount: 60,
|
||||
kind: 'obtained',
|
||||
},
|
||||
{
|
||||
id: 'obtained-4',
|
||||
title: 'Income Name',
|
||||
happenedAt: '2025-10-22 11:11',
|
||||
amount: 60,
|
||||
kind: 'obtained',
|
||||
},
|
||||
{
|
||||
id: 'consumed-3',
|
||||
title: 'Expenditure Name',
|
||||
happenedAt: '2025-10-22 11:11',
|
||||
amount: -60,
|
||||
kind: 'consumed',
|
||||
},
|
||||
];
|
||||
|
||||
export default function PointsDetailsScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const router = useRouter();
|
||||
const [activeFilter, setActiveFilter] = useState<FilterKey>('all');
|
||||
|
||||
const filteredEntries = useMemo(() => {
|
||||
if (activeFilter === 'all') {
|
||||
return LEDGER;
|
||||
}
|
||||
|
||||
return LEDGER.filter(entry => entry.kind === activeFilter);
|
||||
}, [activeFilter]);
|
||||
|
||||
return (
|
||||
<View style={[styles.screen, { paddingTop: insets.top + 12 }]}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
<View style={styles.headerRow}>
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
onPress={() => router.back()}
|
||||
style={styles.iconButton}
|
||||
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={screenPalette.primaryText} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>Points Details</Text>
|
||||
<View style={styles.iconButton} />
|
||||
</View>
|
||||
|
||||
<View style={styles.balanceCluster}>
|
||||
<View style={styles.energyOrb}>
|
||||
<Ionicons name="flash" size={20} color={screenPalette.accent} />
|
||||
</View>
|
||||
<Text style={styles.balanceValue}>60</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.segmentRail}>
|
||||
{FILTERS.map(filter => {
|
||||
const isActive = filter.key === activeFilter;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={filter.key}
|
||||
style={[styles.segmentChip, isActive && styles.segmentChipActive]}
|
||||
onPress={() => setActiveFilter(filter.key)}
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Text style={[styles.segmentLabel, isActive && styles.segmentLabelActive]}>
|
||||
{filter.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
data={filteredEntries}
|
||||
keyExtractor={item => item.id}
|
||||
style={styles.list}
|
||||
contentContainerStyle={{ paddingBottom: Math.max(insets.bottom + 24, 64) }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ItemSeparatorComponent={() => <View style={styles.listDivider} />}
|
||||
renderItem={({ item }) => {
|
||||
const amount = Math.abs(item.amount);
|
||||
const isPositive = item.amount > 0;
|
||||
|
||||
return (
|
||||
<View style={styles.recordRow}>
|
||||
<View>
|
||||
<Text style={styles.recordTitle}>{item.title}</Text>
|
||||
<Text style={styles.recordTimestamp}>{item.happenedAt}</Text>
|
||||
</View>
|
||||
<Text
|
||||
style={[
|
||||
styles.recordValue,
|
||||
isPositive ? styles.valuePositive : styles.valueNegative,
|
||||
]}
|
||||
>
|
||||
{isPositive ? `+ ${amount}` : `- ${amount}`}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
screen: {
|
||||
flex: 1,
|
||||
backgroundColor: screenPalette.background,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
iconButton: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: screenPalette.primaryText,
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
balanceCluster: {
|
||||
alignItems: 'center',
|
||||
marginTop: 28,
|
||||
marginBottom: 32,
|
||||
gap: 12,
|
||||
},
|
||||
energyOrb: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
backgroundColor: screenPalette.accentHalo,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
balanceValue: {
|
||||
fontSize: 44,
|
||||
fontWeight: '700',
|
||||
color: screenPalette.primaryText,
|
||||
letterSpacing: 1,
|
||||
},
|
||||
segmentRail: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: screenPalette.surface,
|
||||
padding: 6,
|
||||
borderRadius: 28,
|
||||
},
|
||||
segmentChip: {
|
||||
flex: 1,
|
||||
borderRadius: 22,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
segmentChipActive: {
|
||||
backgroundColor: screenPalette.surfaceActive,
|
||||
},
|
||||
segmentLabel: {
|
||||
fontSize: 14,
|
||||
color: screenPalette.secondaryText,
|
||||
fontWeight: '500',
|
||||
},
|
||||
segmentLabelActive: {
|
||||
color: screenPalette.primaryText,
|
||||
},
|
||||
list: {
|
||||
flex: 1,
|
||||
marginTop: 32,
|
||||
},
|
||||
listDivider: {
|
||||
height: 1,
|
||||
backgroundColor: screenPalette.divider,
|
||||
marginVertical: 6,
|
||||
},
|
||||
recordRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
recordTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: screenPalette.primaryText,
|
||||
},
|
||||
recordTimestamp: {
|
||||
marginTop: 4,
|
||||
fontSize: 13,
|
||||
color: screenPalette.secondaryText,
|
||||
},
|
||||
recordValue: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
valuePositive: {
|
||||
color: screenPalette.accent,
|
||||
},
|
||||
valueNegative: {
|
||||
color: screenPalette.negative,
|
||||
},
|
||||
});
|
||||
@@ -1,11 +1,9 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, ScrollView } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
|
||||
import { ThemedView } from '@/components/themed-view';
|
||||
import { ThemedText } from '@/components/themed-text';
|
||||
import { useThemeColor } from '@/hooks/use-theme-color';
|
||||
import { IconSymbol } from '@/components/ui/icon-symbol';
|
||||
|
||||
export default function RechargeScreen() {
|
||||
const backgroundColor = useThemeColor({}, 'background');
|
||||
@@ -30,4 +28,4 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,13 +65,10 @@ export default function TemplateRunScreen() {
|
||||
const [formSchema, setFormSchema] = useState<RunFormSchema>({ fields: [] });
|
||||
|
||||
const {
|
||||
state: runState,
|
||||
progress,
|
||||
result,
|
||||
error,
|
||||
isLoading,
|
||||
isCompleted,
|
||||
isError,
|
||||
executeTemplate,
|
||||
cancelRun,
|
||||
reset,
|
||||
@@ -85,13 +82,7 @@ export default function TemplateRunScreen() {
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
formData,
|
||||
errors,
|
||||
updateField,
|
||||
validateForm,
|
||||
isValid,
|
||||
} = useTemplateFormData();
|
||||
const { formData, errors, updateField } = useTemplateFormData();
|
||||
|
||||
// 获取模板信息
|
||||
useEffect(() => {
|
||||
@@ -235,42 +226,56 @@ export default function TemplateRunScreen() {
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
// 转换表单数据为 API 期望的格式
|
||||
const transformedData = transformFormData(formData);
|
||||
console.log('转换后的数据:', transformedData);
|
||||
// 这个我后端取 获取月
|
||||
const list = await subscription.list()
|
||||
const listData = list.data;
|
||||
if (listData && listData.length > 0) {
|
||||
const item = listData[0]
|
||||
const subscriptionId = item?.stripeSubscriptionId!;
|
||||
const summary = await subscription.credit.summary({
|
||||
subscriptionId: subscriptionId,
|
||||
filter: {
|
||||
type: `applicability_scope`,
|
||||
applicability_scope: {
|
||||
price_type: `metered`
|
||||
}
|
||||
}
|
||||
});
|
||||
const identify = await subscription.meterEvent({
|
||||
event_name: `token_usage`,
|
||||
payload: {
|
||||
value: `100`
|
||||
}
|
||||
})
|
||||
// error
|
||||
const orid = identify.data?.identifier
|
||||
if (orid) {
|
||||
try {
|
||||
setCurrentStep('progress');
|
||||
executeTemplate(template!.id, transformedData);
|
||||
} catch (e) {
|
||||
// 退款orid
|
||||
}
|
||||
try {
|
||||
const transformedData = transformFormData(formData);
|
||||
console.log('转换后的数据:', transformedData);
|
||||
|
||||
const list = await subscription.list();
|
||||
const listData = list.data ?? [];
|
||||
if (listData.length === 0) {
|
||||
Alert.alert('未检测到订阅', '请先完成订阅后再尝试生成内容。');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 跳转到订阅
|
||||
|
||||
const subscriptionId = listData[0]?.stripeSubscriptionId;
|
||||
if (!subscriptionId) {
|
||||
Alert.alert('订阅信息缺失', '当前订阅缺少 Stripe 订阅标识,无法记录用量。');
|
||||
return;
|
||||
}
|
||||
|
||||
await subscription.credit.summary({
|
||||
subscriptionId,
|
||||
filter: {
|
||||
type: 'applicability_scope',
|
||||
applicability_scope: {
|
||||
price_type: 'metered',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const identify = await subscription.meterEvent({
|
||||
event_name: 'token_usage',
|
||||
payload: {
|
||||
value: '100',
|
||||
},
|
||||
});
|
||||
|
||||
const identifier = identify.data?.identifier;
|
||||
if (!identifier) {
|
||||
Alert.alert('用量记录失败', '无法生成用量凭证,请稍后重试。');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setCurrentStep('progress');
|
||||
await executeTemplate(template!.id, transformedData);
|
||||
} catch (error) {
|
||||
console.error('执行模板失败:', error);
|
||||
Alert.alert('执行失败', '运行模板时出现异常,请稍后重试。');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交表单失败:', error);
|
||||
Alert.alert('提交失败', '提交生成请求时出现问题,请稍后重试。');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -542,4 +547,4 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user