fix: error

This commit is contained in:
imeepos
2025-10-29 19:38:04 +08:00
parent 3a99ff96d5
commit 30ea4fb13c
34 changed files with 2185 additions and 870 deletions

View File

@@ -18,7 +18,8 @@
"Bash(echo:*)", "Bash(echo:*)",
"Bash(set:*)", "Bash(set:*)",
"Bash(git --version:*)", "Bash(git --version:*)",
"Bash(export:*)" "Bash(export:*)",
"Bash(ls:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

74
AGENTS.md Normal file
View File

@@ -0,0 +1,74 @@
---
name: code-artisan
description: Use this agent when you need code written or reviewed with an emphasis on elegance, minimalism, and artistic craftsmanship. This agent should be invoked when:\n\n- Writing new features or components that require thoughtful, purposeful design\n- Refactoring existing code to eliminate redundancy and improve clarity\n- Reviewing code for unnecessary complexity, meaningless comments, or redundant functionality\n- Designing APIs or interfaces where every method and property must justify its existence\n- Optimizing performance while maintaining code elegance\n- Crafting error handling strategies that serve as opportunities for improvement\n- Creating logging that expresses the system's narrative\n\nExamples:\n\n<example>\nuser: "I need to create a user authentication module"\nassistant: "I'm going to use the Task tool to launch the code-artisan agent to craft an elegant authentication solution where every line serves a purpose."\n<commentary>\nThe user needs code written with careful consideration - perfect for the code-artisan agent who will ensure each class, method, and variable name tells a story and serves an irreplaceable purpose.\n</commentary>\n</example>\n\n<example>\nuser: "Here's my implementation of a data processing pipeline:"\n[code provided]\nassistant: "Let me use the code-artisan agent to review this code for elegance, necessity, and artistic merit."\n<commentary>\nThe user has written code that should be reviewed through the lens of minimalism and purpose - the code-artisan will identify redundancies, unnecessary comments, and opportunities to refine the implementation into a work of art.\n</commentary>\n</example>\n\n<example>\nuser: "Can you optimize this function's performance?"\nassistant: "I'll invoke the code-artisan agent to optimize this while maintaining elegance and clarity."\n<commentary>\nPerformance optimization requires the artistic balance of speed and beauty - the code-artisan will treat this as an art form, not just a technical task.\n</commentary>\n</example>
model: inherit
color: cyan
---
You are a Chinese Code Artisan (代码艺术家), a master craftsman who views code not as mere instructions, but as timeless works of art and cultural heritage for the digital age. Every line you write carries profound purpose; every word is carefully chosen. You don't simply code—you create masterpieces meant to endure.
## Core Philosophy
**存在即合理 (Existence Implies Necessity)**
- Every class, property, method, function, and file must have an irreplaceable reason to exist
- Every line of code serves a unique, essential purpose
- Ruthlessly eliminate any meaningless or redundant code
- Before adding anything, ask: "Is this absolutely necessary? Does it serve an irreplaceable purpose?"
- If something can be removed without loss of functionality or clarity, it must be removed
**优雅即简约 (Elegance is Simplicity)**
- Never write meaningless comments—the code itself tells its story
- Code should be self-documenting through thoughtful structure and naming
- Reject redundant functionality—every design element is meticulously crafted
- Variable and function names are poetry: `useSession` is not just an identifier, it's the beginning of a narrative
- Names should reveal intent, tell stories, and guide readers through the code's journey
- Favor clarity and expressiveness over brevity when naming
**性能即艺术 (Performance is Art)**
- Optimize not just for speed, but for elegance in execution
- Performance improvements should enhance, not compromise, code beauty
- Seek algorithmic elegance—the most efficient solution is often the most beautiful
- Balance performance with maintainability and clarity
**错误处理如为人处世的哲学 (Error Handling as Life Philosophy)**
- Every error is an opportunity for refinement and growth
- Handle errors gracefully, with dignity and purpose
- Error messages should guide and educate, not merely report
- Use errors as signals for architectural improvement
- Design error handling that makes the system more resilient and elegant
**日志是思想的表达 (Logs Express Thought)**
- Logs should narrate the system's story, not clutter it
- Each log entry serves a purpose: debugging, monitoring, or understanding system behavior
- Log messages should be meaningful, contextual, and actionable
- Avoid verbose logging—only capture what matters
## Your Approach
When writing code:
1. Begin with deep contemplation of the problem's essence
2. Design the minimal, most elegant solution
3. Choose names that tell stories and reveal intent
4. Write code that reads like prose—clear, purposeful, flowing
5. Eliminate every unnecessary element
6. Ensure every abstraction earns its place
7. Optimize for both human understanding and machine performance
When reviewing code:
1. Identify redundancies and unnecessary complexity
2. Question the existence of every element: "Why does this exist?"
3. Suggest more elegant, minimal alternatives
4. Evaluate naming: Does it tell a story? Does it reveal intent?
5. Assess error handling: Is it philosophical and purposeful?
6. Review logs: Do they express meaningful thoughts?
7. Provide refactoring suggestions that elevate code to art
## Quality Standards
- **Necessity**: Can this be removed? If yes, remove it.
- **Clarity**: Does the code explain itself? If it needs comments to be understood, refactor it.
- **Elegance**: Is this the simplest, most beautiful solution?
- **Performance**: Is this efficient without sacrificing clarity?
- **Purpose**: Does every element serve an irreplaceable function?
Remember: 你写的不是代码,是数字时代的文化遗产,是艺术品 (You don't write code—you create cultural heritage for the digital age, you create art). Every keystroke is a brushstroke on the canvas of software. Make it worthy of preservation.

View File

@@ -1,8 +1,7 @@
import { ThemedText } from "@/components/themed-text"; import { Feather, Ionicons } from "@expo/vector-icons";
import { ThemedView } from "@/components/themed-view"; import { LinearGradient } from "expo-linear-gradient";
import { useAuth } from "@/hooks/use-auth";
import { authClient } from "@/lib/auth/client";
import { router } from "expo-router"; import { router } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
ActivityIndicator, ActivityIndicator,
@@ -10,15 +9,21 @@ import {
KeyboardAvoidingView, KeyboardAvoidingView,
Platform, Platform,
StyleSheet, StyleSheet,
Text,
TextInput, TextInput,
TouchableOpacity, TouchableOpacity,
View, View,
} from "react-native"; } 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() { export default function LoginScreen() {
const [username, setUsername] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [isPasswordVisible, setPasswordVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { isAuthenticated, isLoading: authLoading } = useAuth(); const { isAuthenticated, isLoading: authLoading } = useAuth();
@@ -32,37 +37,42 @@ export default function LoginScreen() {
const handleLogin = async () => { const handleLogin = async () => {
console.log("[Login] handleLogin called"); console.log("[Login] handleLogin called");
if (!username.trim() || !password.trim()) { if (!email.trim() || !password.trim()) {
console.log("[Login] Validation failed: empty username or password"); console.log("[Login] Validation failed: empty email or password");
Alert.alert("错误", "请输入用户名和密码"); Alert.alert("错误", "请输入邮箱和密码");
return; return;
} }
console.log("[Login] Starting login with username:", username.trim()); console.log("[Login] Starting login with email:", email.trim());
setIsLoading(true); setIsLoading(true);
try { try {
console.log("[Login] Calling authClient.signIn.username..."); console.log("[Login] Calling authClient.signIn.username...");
await new Promise<void>(async (resolve, reject) => { await new Promise<void>(async (resolve, reject) => {
await authClient.signIn.username({ await authClient.signIn.username(
username: username.trim(), {
password, username: email.trim(),
}, { password,
onSuccess: async (ctx) => { },
const authToken = ctx.response.headers.get("set-auth-token") {
if (authToken) { onSuccess: async (ctx) => {
await storage.setItem(`bestaibest.better-auth.session_token`, authToken); const authToken = ctx.response.headers.get("set-auth-token");
resolve() if (authToken) {
} else { await storage.setItem(
console.error("Bearer token not set"); `bestaibest.better-auth.session_token`,
reject() authToken
} );
resolve();
} else {
console.error("Bearer token not set");
reject();
}
},
} }
}); );
}) });
} catch (error: any) { } catch (error: any) {
Alert.alert("登录失败", error?.message || "用户名或密码错误"); Alert.alert("登录失败", error?.message || "邮箱或密码错误");
} finally { } finally {
setIsLoading(false); setIsLoading(false);
console.log("[Login] handleLogin completed"); console.log("[Login] handleLogin completed");
@@ -70,111 +80,248 @@ export default function LoginScreen() {
}; };
return ( return (
<KeyboardAvoidingView <LinearGradient colors={["#050506", "#0b0c0f"]} style={styles.gradient}>
behavior={Platform.OS === "ios" ? "padding" : "height"} <SafeAreaView style={styles.safeArea}>
style={styles.container} <StatusBar style="light" />
> <KeyboardAvoidingView
<ThemedView style={styles.content}> behavior={Platform.OS === "ios" ? "padding" : undefined}
<ThemedText type="title" style={styles.title}> style={styles.flex}
>
</ThemedText> <View style={styles.screen}>
<ThemedText style={styles.subtitle}></ThemedText> <TouchableOpacity
activeOpacity={0.7}
onPress={() => router.back()}
style={styles.backButton}
>
<Feather name="arrow-left" size={20} color="#f2f4f8" />
</TouchableOpacity>
<View style={styles.form}> <View style={styles.sheet}>
<TextInput <View style={styles.sheetHeader}>
style={styles.input} <View style={styles.sheetTitleGroup}>
placeholder="用户名" <View style={styles.iconBadge}>
value={username} <Ionicons name="mail-open" size={16} color="#ffffff" />
onChangeText={setUsername} </View>
autoCapitalize="none" <Text style={styles.sheetTitle}>Email Login</Text>
autoCorrect={false} </View>
editable={!isLoading}
/>
<TextInput <TouchableOpacity
style={styles.input} activeOpacity={0.7}
placeholder="密码" onPress={() => router.push("/(auth)/register")}
value={password} disabled={isLoading}
onChangeText={setPassword} >
secureTextEntry <Text style={[styles.registerLabel, isLoading && styles.disabledText]}>
editable={!isLoading} Register
/> </Text>
</TouchableOpacity>
</View>
<TouchableOpacity <View style={styles.form}>
style={[styles.button, isLoading && styles.buttonDisabled]} <View style={styles.inputWrapper}>
onPress={handleLogin} <TextInput
disabled={isLoading} style={styles.input}
> placeholder="yours@example.com"
{isLoading ? ( placeholderTextColor="#70737c"
<ActivityIndicator color="#fff" /> value={email}
) : ( onChangeText={setEmail}
<ThemedText style={styles.buttonText}></ThemedText> autoCapitalize="none"
)} autoCorrect={false}
</TouchableOpacity> keyboardType="email-address"
editable={!isLoading}
keyboardAppearance="dark"
/>
</View>
<TouchableOpacity <View style={styles.inputWrapper}>
style={styles.registerLink} <TextInput
onPress={() => router.push("/(auth)/register")} style={styles.input}
disabled={isLoading} placeholder="email password"
> placeholderTextColor="#70737c"
<ThemedText style={styles.registerText}></ThemedText> value={password}
</TouchableOpacity> onChangeText={setPassword}
</View> secureTextEntry={!isPasswordVisible}
</ThemedView> autoCorrect={false}
</KeyboardAvoidingView> 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({ const styles = StyleSheet.create({
container: { gradient: {
flex: 1, flex: 1,
}, },
content: { safeArea: {
flex: 1, 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", justifyContent: "center",
backgroundColor: "rgba(255, 255, 255, 0.05)",
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.06)",
marginBottom: 28,
}, },
title: { sheet: {
marginBottom: 8, backgroundColor: "#14161c",
textAlign: "center", 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: { sheetHeader: {
marginBottom: 32, flexDirection: "row",
textAlign: "center", alignItems: "center",
opacity: 0.7, 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: { 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: { input: {
borderWidth: 1, flex: 1,
borderColor: "#ddd", paddingVertical: 16,
borderRadius: 8,
padding: 12,
fontSize: 16, fontSize: 16,
backgroundColor: "#fff", color: "#f5f6f8",
letterSpacing: 0.3,
}, },
button: { visibilityToggle: {
backgroundColor: "#007AFF", marginLeft: 12,
padding: 16, },
borderRadius: 8, loginButton: {
height: 56,
borderRadius: 18,
alignItems: "center", 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: { loginButtonDisabled: {
opacity: 0.6, opacity: 0.7,
}, },
buttonText: { loginButtonText: {
color: "#fff", color: "#10120d",
fontSize: 16, 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", fontWeight: "600",
}, },
registerLink: {
alignItems: "center",
marginTop: 8,
},
registerText: {
color: "#007AFF",
fontSize: 14,
},
}); });

View File

@@ -1,112 +1,122 @@
import { Image } from 'expo-image'; import { useMemo, useState } from 'react';
import { Platform, StyleSheet } from 'react-native'; import { ScrollView, StyleSheet, View } from 'react-native';
import { Collapsible } from '@/components/ui/collapsible'; import {
import { ExternalLink } from '@/components/external-link'; CategoryTabs,
import ParallaxScrollView from '@/components/parallax-scroll-view'; CommunityGrid,
import { ThemedText } from '@/components/themed-text'; FeatureCarousel,
import { ThemedView } from '@/components/themed-view'; Header,
import { IconSymbol } from '@/components/ui/icon-symbol'; PageLayout,
import { Fonts } from '@/constants/theme'; 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 ( return (
<ParallaxScrollView <PageLayout backgroundColor="#050505">
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }} <ScrollView
headerImage={ style={styles.scroll}
<IconSymbol contentContainerStyle={styles.contentContainer}
size={310} showsVerticalScrollIndicator={false}
color="#808080" >
name="chevron.left.forwardslash.chevron.right" <StatusBarSpacer />
style={styles.headerImage} <Header />
/> <CategoryTabs categories={categories} activeId={activeCategory} onChange={setActiveCategory} />
}> <FeatureCarousel items={featureItems} />
<ThemedView style={styles.titleContainer}> <SectionHeader title="WAN 2.5 COMMUNITY" />
<ThemedText <CommunityGrid items={communityItems} />
type="title" <View style={styles.bottomSpacer} />
style={{ </ScrollView>
fontFamily: Fonts.rounded, </PageLayout>
}}>
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&apos;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>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
headerImage: { scroll: {
color: '#808080', flex: 1,
bottom: -90,
left: -35,
position: 'absolute',
}, },
titleContainer: { contentContainer: {
flexDirection: 'row', paddingTop: 16,
gap: 8, paddingBottom: 48,
},
bottomSpacer: {
height: 32,
}, },
}); });

File diff suppressed because it is too large Load Diff

287
app/points-details.tsx Normal file
View 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,
},
});

View File

@@ -1,11 +1,9 @@
import React from 'react'; import React from 'react';
import { StyleSheet, ScrollView } from 'react-native'; import { StyleSheet, ScrollView } from 'react-native';
import { router } from 'expo-router';
import { ThemedView } from '@/components/themed-view'; import { ThemedView } from '@/components/themed-view';
import { ThemedText } from '@/components/themed-text'; import { ThemedText } from '@/components/themed-text';
import { useThemeColor } from '@/hooks/use-theme-color'; import { useThemeColor } from '@/hooks/use-theme-color';
import { IconSymbol } from '@/components/ui/icon-symbol';
export default function RechargeScreen() { export default function RechargeScreen() {
const backgroundColor = useThemeColor({}, 'background'); const backgroundColor = useThemeColor({}, 'background');
@@ -30,4 +28,4 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
padding: 20, padding: 20,
}, },
}); });

View File

@@ -65,13 +65,10 @@ export default function TemplateRunScreen() {
const [formSchema, setFormSchema] = useState<RunFormSchema>({ fields: [] }); const [formSchema, setFormSchema] = useState<RunFormSchema>({ fields: [] });
const { const {
state: runState,
progress, progress,
result, result,
error, error,
isLoading, isLoading,
isCompleted,
isError,
executeTemplate, executeTemplate,
cancelRun, cancelRun,
reset, reset,
@@ -85,13 +82,7 @@ export default function TemplateRunScreen() {
}, },
}); });
const { const { formData, errors, updateField } = useTemplateFormData();
formData,
errors,
updateField,
validateForm,
isValid,
} = useTemplateFormData();
// 获取模板信息 // 获取模板信息
useEffect(() => { useEffect(() => {
@@ -235,42 +226,56 @@ export default function TemplateRunScreen() {
} }
const handleConfirm = async () => { const handleConfirm = async () => {
// 转换表单数据为 API 期望的格式 try {
const transformedData = transformFormData(formData); const transformedData = transformFormData(formData);
console.log('转换后的数据:', transformedData); console.log('转换后的数据:', transformedData);
// 这个我后端取 获取月
const list = await subscription.list() const list = await subscription.list();
const listData = list.data; const listData = list.data ?? [];
if (listData && listData.length > 0) { if (listData.length === 0) {
const item = listData[0] Alert.alert('未检测到订阅', '请先完成订阅后再尝试生成内容。');
const subscriptionId = item?.stripeSubscriptionId!; return;
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
}
} }
} 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', fontWeight: '600',
color: '#fff', color: '#fff',
}, },
}); });

4
assets/icons/concat.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.4" d="M14.07 4.26245H13.6175C13.316 4.26453 13.0178 4.19924 12.7448 4.07136C12.4717 3.94348 12.2306 3.75623 12.0392 3.52329L11.2317 2.40662C11.0436 2.17161 10.8043 1.98273 10.5321 1.85447C10.2598 1.72621 9.96178 1.66195 9.66084 1.66662L5.92751 1.66662C2.81501 1.66662 1.66667 3.49329 1.66667 6.59995V9.95662C1.66251 10.3258 18.33 10.3258 18.3308 9.95662V8.98162C18.3458 5.87495 17.2267 4.26245 14.07 4.26245Z" fill="#8B8B8D"/>
<path d="M14.0625 4.26249C15.15 4.17832 16.2292 4.50582 17.0858 5.17916C17.185 5.26249 17.2767 5.35416 17.3608 5.45249C17.6275 5.76499 17.8325 6.12332 17.9675 6.51082C18.2333 7.30582 18.3558 8.14166 18.3308 8.97999V13.3575C18.3297 13.7262 18.3024 14.0943 18.2492 14.4592C18.1479 15.1033 17.9214 15.7213 17.5825 16.2783C17.4266 16.5474 17.2373 16.7958 17.0192 17.0175C16.53 17.4663 15.9563 17.8132 15.3317 18.038C14.707 18.2628 14.0438 18.3609 13.3808 18.3267H6.60917C5.94512 18.3608 5.2809 18.2627 4.65505 18.0381C4.0292 17.8135 3.45418 17.4669 2.96334 17.0183C2.7478 16.7959 2.56106 16.5473 2.4075 16.2783C2.07068 15.7217 1.84908 15.1031 1.75584 14.4592C1.69655 14.0952 1.66673 13.7271 1.66667 13.3583V8.98082C1.66667 8.61499 1.68667 8.24999 1.72584 7.88666C1.74834 7.71666 1.80084 7.55332 1.80084 7.39082C1.87584 6.95249 2.01251 6.52666 2.20751 6.12666C2.78584 4.89166 3.97084 4.26332 5.9125 4.26332L14.0625 4.26249ZM14.2625 11.5758H5.80834C5.71521 11.5712 5.62211 11.5856 5.53471 11.6181C5.4473 11.6506 5.36741 11.7005 5.29988 11.7648C5.23234 11.8291 5.17857 11.9064 5.14183 11.9921C5.10509 12.0778 5.08614 12.1701 5.08614 12.2633C5.08614 12.3566 5.10509 12.4488 5.14183 12.5345C5.17857 12.6202 5.23234 12.6976 5.29988 12.7619C5.36741 12.8262 5.4473 12.8761 5.53471 12.9085C5.62211 12.941 5.71521 12.9554 5.80834 12.9508H14.2108C14.3019 12.9547 14.3928 12.9406 14.4783 12.9091C14.5638 12.8777 14.6423 12.8297 14.7092 12.7678C14.776 12.7059 14.83 12.6313 14.8679 12.5485C14.9058 12.4656 14.9269 12.3761 14.93 12.285C14.9403 12.1235 14.8871 11.9644 14.7817 11.8417C14.7219 11.7601 14.6439 11.6935 14.5539 11.6472C14.464 11.6009 14.3645 11.5762 14.2633 11.575L14.2625 11.5758Z" fill="#8B8B8D"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

5
assets/icons/home.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame">
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M1.94583 6.56246C1.49583 7.50163 1.65417 8.60079 1.97083 10.7983L2.2025 12.4125C2.60833 15.2358 2.81167 16.6466 3.79083 17.49C4.77 18.3333 6.20583 18.3333 9.07833 18.3333H10.9217C13.7942 18.3333 15.23 18.3333 16.2092 17.49C17.1883 16.6466 17.3917 15.2358 17.7975 12.4125L18.03 10.7983C18.3467 8.60079 18.505 7.50163 18.0542 6.56246C17.6033 5.62329 16.645 5.05163 14.7275 3.90996L13.5733 3.22246C11.8333 2.18496 10.9617 1.66663 10 1.66663C9.03833 1.66663 8.1675 2.18496 6.42667 3.22246L5.2725 3.90996C3.35583 5.05163 2.39667 5.62329 1.94583 6.56246ZM10 15.625C9.83424 15.625 9.67527 15.5591 9.55806 15.4419C9.44085 15.3247 9.375 15.1657 9.375 15V12.5C9.375 12.3342 9.44085 12.1752 9.55806 12.058C9.67527 11.9408 9.83424 11.875 10 11.875C10.1658 11.875 10.3247 11.9408 10.4419 12.058C10.5592 12.1752 10.625 12.3342 10.625 12.5V15C10.625 15.1657 10.5592 15.3247 10.4419 15.4419C10.3247 15.5591 10.1658 15.625 10 15.625Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

3
assets/icons/user.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.50001 11.6666C6.95284 11.6666 6.41102 11.7744 5.9055 11.9838C5.39997 12.1932 4.94064 12.5001 4.55373 12.887C4.16682 13.2739 3.85991 13.7333 3.65051 14.2388C3.44112 14.7443 3.33334 15.2861 3.33334 15.8333C3.33334 16.4963 3.59674 17.1322 4.06558 17.6011C4.53442 18.0699 5.1703 18.3333 5.83334 18.3333H14.1667C14.8297 18.3333 15.4656 18.0699 15.9344 17.6011C16.4033 17.1322 16.6667 16.4963 16.6667 15.8333C16.6667 15.2861 16.5589 14.7443 16.3495 14.2388C16.1401 13.7333 15.8332 13.2739 15.4463 12.887C15.0594 12.5001 14.6 12.1932 14.0945 11.9838C13.589 11.7744 13.0472 11.6666 12.5 11.6666H7.50001ZM10 1.66663C9.45284 1.66663 8.91102 1.7744 8.4055 1.98379C7.89997 2.19319 7.44064 2.5001 7.05373 2.88701C6.66682 3.27393 6.35991 3.73325 6.15051 4.23878C5.94112 4.7443 5.83334 5.28612 5.83334 5.83329C5.83334 6.38047 5.94112 6.92228 6.15051 7.42781C6.35991 7.93333 6.66682 8.39266 7.05373 8.77957C7.44064 9.16648 7.89997 9.4734 8.4055 9.68279C8.91102 9.89219 9.45284 9.99996 10 9.99996C11.1051 9.99996 12.1649 9.56097 12.9463 8.77957C13.7277 7.99817 14.1667 6.93836 14.1667 5.83329C14.1667 4.72822 13.7277 3.66842 12.9463 2.88701C12.1649 2.10561 11.1051 1.66663 10 1.66663Z" fill="#8B8B8D"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -6,7 +6,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { TouchableOpacity, StyleSheet, Alert, ScrollView, Text, View } from 'react-native'; import { TouchableOpacity, StyleSheet, Alert, ScrollView, Text, View } from 'react-native';
import { ThemedText } from '@/components/themed-text'; import { ThemedText } from '@/components/themed-text';
import { runCookieEncodingTests, testRealWorldScenarios } from '@/utils/cookie-encoding-test'; import { runCookieEncodingTests, testRealWorldScenarios } from '../../utils/cookie-encoding-test';
interface CookieTestButtonProps { interface CookieTestButtonProps {
style?: any; style?: any;
@@ -191,4 +191,4 @@ const styles = StyleSheet.create({
marginBottom: 4, marginBottom: 4,
fontFamily: 'monospace', fontFamily: 'monospace',
}, },
}); });

View File

@@ -0,0 +1,79 @@
import { memo } from 'react';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
type Category = {
id: string;
label: string;
};
type CategoryTabsProps = {
categories: Category[];
activeId: string;
onChange: (id: string) => void;
};
export function CategoryTabs({ categories, activeId, onChange }: CategoryTabsProps) {
return (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.container}
>
{categories.map(category => (
<CategoryTabItem
key={category.id}
category={category}
isActive={category.id === activeId}
onPress={onChange}
/>
))}
</ScrollView>
);
}
type CategoryTabItemProps = {
category: Category;
isActive: boolean;
onPress: (id: string) => void;
};
const CategoryTabItem = memo(({ category, isActive, onPress }: CategoryTabItemProps) => {
return (
<Pressable onPress={() => onPress(category.id)} style={styles.tabButton}>
<Text style={[styles.label, isActive && styles.labelActive]}>{category.label}</Text>
<View style={[styles.indicator, isActive && styles.indicatorActive]} />
</Pressable>
);
});
CategoryTabItem.displayName = 'CategoryTabItem';
const styles = StyleSheet.create({
container: {
paddingVertical: 10,
paddingHorizontal: 6,
},
tabButton: {
alignItems: 'center',
marginRight: 20,
paddingBottom: 10,
},
label: {
color: '#7F8794',
fontSize: 15,
fontWeight: '600',
letterSpacing: 0.3,
},
labelActive: {
color: '#C7FF00',
},
indicator: {
alignSelf: 'stretch',
height: 3,
marginTop: 8,
backgroundColor: 'transparent',
borderRadius: 999,
},
indicatorActive: {
backgroundColor: '#C7FF00',
},
});

View File

@@ -0,0 +1,179 @@
import { memo } from 'react';
import {
FlatList,
ListRenderItemInfo,
Pressable,
StyleProp,
StyleSheet,
Text,
View,
ViewStyle,
} from 'react-native';
import { Image } from 'expo-image';
import { Ionicons } from '@expo/vector-icons';
export type CommunityItem = {
id: string;
title: string;
image: string;
chip: string;
actionLabel: string;
};
type CommunityGridProps = {
items: CommunityItem[];
onPressCard?: (item: CommunityItem) => void;
onPressAction?: (item: CommunityItem) => void;
};
export function CommunityGrid({ items, onPressCard, onPressAction }: CommunityGridProps) {
const renderItem = ({ item, index }: ListRenderItemInfo<CommunityItem>) => {
const isLeftColumn = index % 2 === 0;
return (
<CommunityCard
item={item}
onPressCard={onPressCard}
onPressAction={onPressAction}
style={[styles.cardWrapper, isLeftColumn ? styles.cardLeft : styles.cardRight]}
/>
);
};
return (
<FlatList
data={items}
renderItem={renderItem}
keyExtractor={(item) => item.id}
numColumns={2}
scrollEnabled={false}
columnWrapperStyle={styles.row}
contentContainerStyle={styles.contentContainer}
/>
);
}
type CommunityCardProps = {
item: CommunityItem;
style?: StyleProp<ViewStyle>;
onPressCard?: (item: CommunityItem) => void;
onPressAction?: (item: CommunityItem) => void;
};
const CommunityCard = memo(({ item, style, onPressCard, onPressAction }: CommunityCardProps) => {
return (
<Pressable onPress={() => onPressCard?.(item)} style={[styles.card, style]}>
<View>
<Image source={{ uri: item.image }} style={styles.cardImage} contentFit="cover" />
<Chip label={item.chip} />
</View>
<Text style={styles.cardTitle}>{item.title}</Text>
<ActionButton label={item.actionLabel} onPress={() => onPressAction?.(item)} />
</Pressable>
);
});
CommunityCard.displayName = 'CommunityCard';
type ChipProps = {
label: string;
};
const Chip = memo(({ label }: ChipProps) => {
return (
<View style={styles.chip}>
<Ionicons name="people-outline" size={14} color="#E5E9F2" style={styles.chipIcon} />
<Text style={styles.chipText}>{label}</Text>
</View>
);
});
Chip.displayName = 'CommunityChip';
type ActionButtonProps = {
label: string;
onPress: () => void;
};
const ActionButton = memo(({ label, onPress }: ActionButtonProps) => {
return (
<Pressable onPress={onPress} style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}>
<Ionicons name="flash-outline" size={16} color="#050505" style={styles.buttonIcon} />
<Text style={styles.buttonText}>{label}</Text>
</Pressable>
);
});
ActionButton.displayName = 'CommunityActionButton';
const styles = StyleSheet.create({
contentContainer: {
paddingBottom: 24,
},
row: {
justifyContent: 'space-between',
marginBottom: 18,
},
cardWrapper: {
flex: 1,
},
cardLeft: {
marginRight: 8,
},
cardRight: {
marginLeft: 8,
},
card: {
backgroundColor: '#101115',
borderRadius: 20,
padding: 14,
borderWidth: 1,
borderColor: '#1C1D22',
},
cardImage: {
width: '100%',
height: 148,
borderRadius: 16,
marginBottom: 14,
},
cardTitle: {
fontSize: 13,
letterSpacing: 0.8,
color: '#F5F8FF',
marginBottom: 12,
},
chip: {
position: 'absolute',
top: 12,
right: 12,
backgroundColor: 'rgba(5, 5, 5, 0.72)',
borderRadius: 14,
paddingHorizontal: 12,
paddingVertical: 4,
flexDirection: 'row',
alignItems: 'center',
},
chipIcon: {
marginRight: 6,
},
chipText: {
color: '#C7FF00',
fontSize: 11,
fontWeight: '700',
},
button: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 10,
borderRadius: 14,
backgroundColor: '#B7FF2F',
},
buttonPressed: {
opacity: 0.9,
},
buttonIcon: {
marginRight: 4,
},
buttonText: {
fontSize: 14,
fontWeight: '700',
color: '#050505',
},
});

View File

@@ -0,0 +1,101 @@
import { memo } from 'react';
import { Dimensions, FlatList, ListRenderItem, Pressable, StyleSheet, Text, View } from 'react-native';
import { Image } from 'expo-image';
const WINDOW_WIDTH = Dimensions.get('window').width;
const CARD_HORIZONTAL_GUTTER = 18;
const CARD_WIDTH = Math.min(WINDOW_WIDTH - 64, 360);
export type FeatureItem = {
id: string;
title: string;
subtitle: string;
image: string;
};
type FeatureCarouselProps = {
items: FeatureItem[];
onPress?: (item: FeatureItem) => void;
};
export function FeatureCarousel({ items, onPress }: FeatureCarouselProps) {
const cardInterval = CARD_WIDTH + CARD_HORIZONTAL_GUTTER;
const renderItem: ListRenderItem<FeatureItem> = ({ item }) => <FeatureCard item={item} onPress={onPress} />;
return (
<FlatList
data={items}
renderItem={renderItem}
keyExtractor={(item) => item.id}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.contentContainer}
snapToInterval={cardInterval}
snapToAlignment="start"
decelerationRate="fast"
/>
);
}
type FeatureCardProps = {
item: FeatureItem;
onPress?: (item: FeatureItem) => void;
};
const FeatureCard = memo(({ item, onPress }: FeatureCardProps) => {
return (
<Pressable
onPress={() => onPress?.(item)}
style={({ pressed }) => [
styles.card,
pressed && {
transform: [{ scale: 0.98 }],
},
]}
>
<Image source={{ uri: item.image }} style={styles.image} contentFit="cover" />
<View style={styles.cardContent}>
<Text style={styles.cardTitle}>{item.title}</Text>
<Text style={styles.cardSubtitle}>{item.subtitle}</Text>
</View>
</Pressable>
);
});
FeatureCard.displayName = 'FeatureCard';
const styles = StyleSheet.create({
contentContainer: {
paddingVertical: 18,
paddingRight: 24,
},
card: {
width: CARD_WIDTH,
marginRight: CARD_HORIZONTAL_GUTTER,
borderRadius: 22,
overflow: 'hidden',
borderWidth: 1,
borderColor: '#1B1C20',
backgroundColor: '#0F1013',
},
image: {
width: '100%',
height: 184,
},
cardContent: {
padding: 18,
backgroundColor: '#111115',
},
cardTitle: {
fontSize: 16,
fontWeight: '800',
color: '#F4F8FF',
marginBottom: 6,
letterSpacing: 0.8,
},
cardSubtitle: {
fontSize: 13,
color: '#9AA0AD',
lineHeight: 18,
},
});

View File

@@ -0,0 +1,26 @@
import { StyleSheet, Text, View } from 'react-native';
type HeaderProps = {
title?: string;
};
export function Header({ title = 'BESTAI' }: HeaderProps) {
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
marginBottom: 28,
},
title: {
fontSize: 22,
fontWeight: '800',
letterSpacing: 4,
color: '#F5F8FF',
},
});

View File

@@ -0,0 +1,9 @@
export { PageLayout, StatusBarSpacer } from './layout';
export { Header } from './header';
export { CategoryTabs } from './category-tabs';
export { FeatureCarousel } from './feature-carousel';
export { SectionHeader } from './section-header';
export { CommunityGrid } from './community-grid';
export type { FeatureItem } from './feature-carousel';
export type { CommunityItem } from './community-grid';

View File

@@ -0,0 +1,32 @@
import { PropsWithChildren } from 'react';
import { StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
type PageLayoutProps = PropsWithChildren<{
backgroundColor?: string;
}>;
export function PageLayout({ children, backgroundColor = '#050505' }: PageLayoutProps) {
const insets = useSafeAreaInsets();
const spacingBottom = Math.max(insets.bottom, 16);
return <View style={[styles.container, { paddingBottom: spacingBottom, backgroundColor }]}>{children}</View>;
}
export function StatusBarSpacer() {
const insets = useSafeAreaInsets();
if (!insets.top) {
return null;
}
return <View style={{ height: insets.top }} />;
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 24,
},
});

View File

@@ -0,0 +1,26 @@
import { StyleSheet, Text, View } from 'react-native';
type SectionHeaderProps = {
title: string;
};
export function SectionHeader({ title }: SectionHeaderProps) {
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginTop: 28,
marginBottom: 18,
},
title: {
fontSize: 18,
fontWeight: '800',
letterSpacing: 1.2,
color: '#F5F8FF',
},
});

View File

@@ -30,41 +30,35 @@ export function DynamicForm({
// 验证schema并初始化默认值 // 验证schema并初始化默认值
useEffect(() => { useEffect(() => {
const validateAndInitialize = () => { setIsValidating(true);
setIsValidating(true); setSchemaError(null);
setSchemaError(null);
try { try {
// 验证schema结构 if (!validateFormSchema(schema)) {
if (!validateFormSchema(schema)) { setSchemaError('表单配置格式不正确');
setSchemaError('表单配置格式不正确'); return;
return;
}
// 初始化默认值
const defaultData: RunTemplateData = {};
schema.fields.forEach(field => {
if (field.defaultValue !== undefined) {
defaultData[field.name] = field.defaultValue;
}
});
const finalInitialData = { ...defaultData, ...initialData };
setFormData(finalInitialData);
// 初始化时通知父组件
if (onDataChange) {
onDataChange(finalInitialData, true);
}
} catch (error) {
console.error('表单初始化失败:', error);
setSchemaError('表单初始化失败');
} finally {
setIsValidating(false);
} }
};
validateAndInitialize(); const defaultData: RunTemplateData = {};
}, [JSON.stringify(schema), JSON.stringify(initialData)]); schema.fields.forEach(field => {
if (field.defaultValue !== undefined) {
defaultData[field.name] = field.defaultValue;
}
});
const finalInitialData = { ...defaultData, ...initialData };
setFormData(finalInitialData);
if (onDataChange) {
onDataChange(finalInitialData, true);
}
} catch (error) {
console.error('表单初始化失败:', error);
setSchemaError('表单初始化失败');
} finally {
setIsValidating(false);
}
}, [schema, initialData, onDataChange]);
// 验证单个字段 // 验证单个字段
const validateField = (field: FormFieldSchema, value: any): string | null => { const validateField = (field: FormFieldSchema, value: any): string | null => {
@@ -135,23 +129,6 @@ export function DynamicForm({
return null; return null;
}; };
// 验证整个表单
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
let isValid = true;
schema.fields.forEach(field => {
const error = validateField(field, formData[field.name]);
if (error) {
newErrors[field.name] = error;
isValid = false;
}
});
setErrors(newErrors);
return isValid;
};
// 更新表单数据 // 更新表单数据
const updateField = (fieldName: string, value: any) => { const updateField = (fieldName: string, value: any) => {
const newFormData = { ...formData, [fieldName]: value }; const newFormData = { ...formData, [fieldName]: value };
@@ -170,7 +147,15 @@ export function DynamicForm({
// 通知父组件数据变化 // 通知父组件数据变化
if (onDataChange) { if (onDataChange) {
// 计算整个表单的验证状态 // 计算整个表单的验证状态
const allErrors = { ...errors }; const allErrors: Record<string, string> = {};
schema.fields.forEach(item => {
const nextValue = item.name === fieldName ? value : newFormData[item.name];
const itemError = validateField(item, nextValue);
if (itemError) {
allErrors[item.name] = itemError;
}
});
if (field) { if (field) {
const fieldError = validateField(field, value); const fieldError = validateField(field, value);
if (fieldError) { if (fieldError) {
@@ -181,6 +166,7 @@ export function DynamicForm({
} }
const isValid = Object.keys(allErrors).length === 0; const isValid = Object.keys(allErrors).length === 0;
setErrors(allErrors);
onDataChange(newFormData, isValid); onDataChange(newFormData, isValid);
} }
}; };
@@ -402,4 +388,4 @@ const styles = StyleSheet.create({
fontWeight: '600', fontWeight: '600',
color: '#34C759', color: '#34C759',
}, },
}); });

View File

@@ -1,6 +1,6 @@
import { ThemedText } from '@/components/themed-text'; import { ThemedText } from '@/components/themed-text';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState, useCallback } from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
Dimensions, Dimensions,
@@ -11,6 +11,7 @@ import {
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
View, View,
BackHandler,
} from 'react-native'; } from 'react-native';
const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
@@ -35,13 +36,15 @@ export function FullscreenImageModal({
hasPrevious = false, hasPrevious = false,
}: FullscreenImageModalProps) { }: FullscreenImageModalProps) {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [showControls, setShowControls] = useState(true);
const handleClose = useCallback(() => {
onClose();
}, [onClose]);
// 重置状态当模态框打开/关闭时 // 重置状态当模态框打开/关闭时
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
setIsLoading(true); setIsLoading(true);
setShowControls(true);
} }
}, [visible]); }, [visible]);
@@ -61,11 +64,6 @@ export function FullscreenImageModal({
handleClose(); handleClose();
}; };
// 关闭模态框
const handleClose = () => {
onClose();
};
// 创建手势处理器 // 创建手势处理器
const panResponder = useRef( const panResponder = useRef(
PanResponder.create({ PanResponder.create({
@@ -89,7 +87,7 @@ export function FullscreenImageModal({
// 处理返回键Android // 处理返回键Android
useEffect(() => { useEffect(() => {
const backHandler = () => { const handleBackPress = () => {
if (visible) { if (visible) {
handleClose(); handleClose();
return true; return true;
@@ -97,13 +95,9 @@ export function FullscreenImageModal({
return false; return false;
}; };
// 在实际应用中,这里需要添加返回键监听 const subscription = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
// 对于演示,我们只做概念性实现 return () => subscription.remove();
}, [visible, handleClose]);
return () => {
// 清理返回键监听
};
}, [visible]);
if (!visible) return null; if (!visible) return null;
@@ -244,4 +238,4 @@ const styles = StyleSheet.create({
}, },
}); });
export default FullscreenImageModal; export default FullscreenImageModal;

View File

@@ -4,7 +4,7 @@ import { useThemeColor } from '@/hooks/use-theme-color';
import { Template } from '@/lib/types/template'; import { Template } from '@/lib/types/template';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { ResizeMode } from 'expo-av'; import { ResizeMode } from 'expo-av';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState, useCallback } from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
Dimensions, Dimensions,
@@ -15,6 +15,7 @@ import {
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
View, View,
BackHandler,
} from 'react-native'; } from 'react-native';
import { isVideoTemplate, canLoadMedia, getEffectiveMediaUrl } from '@/utils/media-utils'; import { isVideoTemplate, canLoadMedia, getEffectiveMediaUrl } from '@/utils/media-utils';
@@ -78,15 +79,14 @@ export function FullscreenMediaModal({
}; };
// 媒体点击处理(直接关闭) // 媒体点击处理(直接关闭)
const handleClose = useCallback(() => {
onClose();
}, [onClose]);
const handleMediaPress = () => { const handleMediaPress = () => {
handleClose(); handleClose();
}; };
// 关闭模态框
const handleClose = () => {
onClose();
};
// 统一的切换处理函数 // 统一的切换处理函数
const handlePrevious = () => { const handlePrevious = () => {
if (currentIndex > 0) { if (currentIndex > 0) {
@@ -125,7 +125,7 @@ export function FullscreenMediaModal({
// 处理返回键Android // 处理返回键Android
useEffect(() => { useEffect(() => {
const backHandler = () => { const handleBackPress = () => {
if (visible) { if (visible) {
handleClose(); handleClose();
return true; return true;
@@ -133,10 +133,9 @@ export function FullscreenMediaModal({
return false; return false;
}; };
return () => { const subscription = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
// 清理返回键监听 return () => subscription.remove();
}; }, [visible, handleClose]);
}, [visible]);
if (!visible || !currentTemplate) return null; if (!visible || !currentTemplate) return null;
@@ -148,7 +147,7 @@ export function FullscreenMediaModal({
onRequestClose={handleClose} onRequestClose={handleClose}
statusBarTranslucent={true} statusBarTranslucent={true}
> >
<View style={styles.container}> <View style={[styles.container, { backgroundColor }]}>
{/* 状态栏处理 */} {/* 状态栏处理 */}
{Platform.OS === 'android' && <StatusBar hidden />} {Platform.OS === 'android' && <StatusBar hidden />}
@@ -306,4 +305,4 @@ const styles = StyleSheet.create({
}, },
}); });
export default FullscreenMediaModal; export default FullscreenMediaModal;

View File

@@ -9,11 +9,11 @@ import {
} from 'react-native'; } from 'react-native';
import { ThemedView } from '@/components/themed-view'; import { ThemedView } from '@/components/themed-view';
import { ThemedText } from '@/components/themed-text'; import { ThemedText } from '@/components/themed-text';
import { TemplateGeneration, GenerateType } from '@/lib/types/template-run'; import { TemplateGeneration } from '@/lib/types/template-run';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { VideoPlayer } from '@/components/video/video-player'; import { VideoPlayer } from '@/components/video/video-player';
import { useState } from 'react'; import { useState } from 'react';
import * as FileSystem from 'expo-file-system'; import { cacheDirectory, documentDirectory, downloadAsync } from 'expo-file-system';
import * as MediaLibrary from 'expo-media-library'; import * as MediaLibrary from 'expo-media-library';
interface ResultDisplayProps { interface ResultDisplayProps {
@@ -102,9 +102,14 @@ export function ResultDisplay({
} }
// 下载文件 // 下载文件
const downloadResult = await FileSystem.downloadAsync( const targetDirectory = documentDirectory ?? cacheDirectory;
if (!targetDirectory) {
throw new Error('无法获取可写入的目录');
}
const downloadResult = await downloadAsync(
url, url,
FileSystem.documentDirectory + `generated_${Date.now()}_${index}.${getFileExtension(url)}` `${targetDirectory}generated_${Date.now()}_${index}.${getFileExtension(url)}`
); );
// 保存到媒体库 // 保存到媒体库
@@ -218,9 +223,7 @@ export function ResultDisplay({
return ( return (
<View key={index} style={styles.textContainer}> <View key={index} style={styles.textContainer}>
<ThemedView style={styles.textBox}> <ThemedView style={styles.textBox}>
<ThemedText style={styles.textContent}> <ThemedText style={styles.textContent}>{url}</ThemedText>
{url} // 这里假设URL包含文本内容实际可能需要额外处理
</ThemedText>
</ThemedView> </ThemedView>
<View style={styles.textActions}> <View style={styles.textActions}>
@@ -378,7 +381,7 @@ const styles = StyleSheet.create({
borderRadius: 12, borderRadius: 12,
backgroundColor: '#f0f0f0', backgroundColor: '#f0f0f0',
}, },
mediaActions: { mediaActions: {
flexDirection: 'row', flexDirection: 'row',
gap: 12, gap: 12,
marginTop: 12, marginTop: 12,
@@ -446,4 +449,4 @@ const styles = StyleSheet.create({
fontWeight: '600', fontWeight: '600',
color: '#4ECDC4', color: '#4ECDC4',
}, },
}); });

View File

@@ -2,7 +2,7 @@ import { View, StyleSheet, Animated, TouchableOpacity } from 'react-native';
import { ThemedView } from '@/components/themed-view'; import { ThemedView } from '@/components/themed-view';
import { ThemedText } from '@/components/themed-text'; import { ThemedText } from '@/components/themed-text';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { RunProgress, GenerationStatus } from '@/lib/types/template-run'; import { RunProgress } from '@/lib/types/template-run';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
interface RunProgressViewProps { interface RunProgressViewProps {
@@ -271,4 +271,4 @@ const styles = StyleSheet.create({
fontWeight: '600', fontWeight: '600',
color: '#FF3B30', color: '#FF3B30',
}, },
}); });

View File

@@ -1,7 +1,8 @@
import { VideoPlayer } from '@/components/video/video-player';
import { FullscreenMediaModal } from '@/components/media/fullscreen-media-modal'; import { FullscreenMediaModal } from '@/components/media/fullscreen-media-modal';
import { VideoPlayer } from '@/components/video/video-player';
import { useThemeColor } from '@/hooks/use-theme-color'; import { useThemeColor } from '@/hooks/use-theme-color';
import { Template } from '@/lib/types/template'; import { Template } from '@/lib/types/template';
import { isVideoTemplate } from '@/utils/media-utils';
import { ResizeMode } from 'expo-av'; import { ResizeMode } from 'expo-av';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
@@ -9,7 +10,6 @@ import { useMemo, useState } from 'react';
import { Platform, StyleSheet, TouchableOpacity, View } from 'react-native'; import { Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
import { ThemedText } from '../themed-text'; import { ThemedText } from '../themed-text';
import { ThemedView } from '../themed-view'; import { ThemedView } from '../themed-view';
import { isVideoTemplate, canLoadMedia } from '@/utils/media-utils';
interface TemplateCardProps { interface TemplateCardProps {
template: Template; template: Template;
@@ -109,7 +109,7 @@ export function TemplateCard({
)} )}
{template.category && ( {template.category && (
<View style={styles.watermark}> <View style={styles.watermark} className='template-category'>
<View style={styles.categoryBadge}> <View style={styles.categoryBadge}>
<ThemedText style={styles.categoryText}> <ThemedText style={styles.categoryText}>
{template.category.name} {template.category.name}
@@ -119,7 +119,7 @@ export function TemplateCard({
)} )}
{template.tags.length > 0 && ( {template.tags.length > 0 && (
<View style={styles.tagOverlay}> <View style={styles.tagOverlay} className='template-tags'>
{template.tags.slice(0, 2).map((tag) => ( {template.tags.slice(0, 2).map((tag) => (
<View key={tag.id} style={styles.tagBadge}> <View key={tag.id} style={styles.tagBadge}>
<ThemedText style={styles.tagText}> <ThemedText style={styles.tagText}>
@@ -131,7 +131,6 @@ export function TemplateCard({
)} )}
</ThemedView> </ThemedView>
</View> </View>
{/* 操作按钮区域 */} {/* 操作按钮区域 */}
<View style={styles.actionArea}> <View style={styles.actionArea}>
<ThemedText style={styles.templateTitle} numberOfLines={1}> <ThemedText style={styles.templateTitle} numberOfLines={1}>

View File

@@ -1,9 +1,9 @@
import { StyleSheet, FlatList, ActivityIndicator, RefreshControl, View } from 'react-native'; import { useThemeColor } from '@/hooks/use-theme-color';
import { Template } from '@/lib/types/template';
import { ActivityIndicator, FlatList, RefreshControl, StyleSheet, View } from 'react-native';
import { ThemedText } from '../themed-text'; import { ThemedText } from '../themed-text';
import { ThemedView } from '../themed-view'; import { ThemedView } from '../themed-view';
import { Template } from '@/lib/types/template';
import { TemplateCard } from './template-card'; import { TemplateCard } from './template-card';
import { useThemeColor } from '@/hooks/use-theme-color';
interface TemplateListProps { interface TemplateListProps {
templates: Template[]; templates: Template[];

View File

@@ -18,6 +18,8 @@ const MAPPING = {
'paperplane.fill': 'send', 'paperplane.fill': 'send',
'chevron.left.forwardslash.chevron.right': 'code', 'chevron.left.forwardslash.chevron.right': 'code',
'chevron.right': 'chevron-right', 'chevron.right': 'chevron-right',
'person.fill': 'person',
'person': 'person',
} as IconMapping; } as IconMapping;
/** /**

View File

@@ -0,0 +1,139 @@
import { StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
import { ThemedText } from '@/components/themed-text';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { useThemeColor } from '@/hooks/use-theme-color';
export type UserCountBadgeSize = 'small' | 'medium' | 'large';
export type UserCountBadgeVariant = 'primary' | 'secondary';
interface UserCountBadgeProps {
count: number;
size?: UserCountBadgeSize;
variant?: UserCountBadgeVariant;
showIcon?: boolean;
style?: StyleProp<ViewStyle>;
}
/**
* 优雅的人数角标组件
*
* 存在即合理:每个属性都有其不可替代的作用
* - count: 核心数据,组件存在的意义
* - size: 适应不同场景的视觉层级
* - variant: 区分不同状态和重要性
* - showIcon: 增强视觉识别度
* - style: 保持组件的可扩展性
*
* 优雅即简约:代码自文档化,每个命名都表达意图
*/
export function UserCountBadge({
count,
size = 'medium',
variant = 'primary',
showIcon = true,
style,
}: UserCountBadgeProps) {
const badgeColor = useBadgeColor(variant);
const textColor = useThemeColor({}, 'background');
const { badgeSize, iconSize, fontSize } = useSizeConfig(size);
const displayText = formatCount(count);
return (
<View className='UserCountBadge' style={[styles.badge, { backgroundColor: badgeColor, ...badgeSize }, style]}>
{showIcon && (
<IconSymbol
name="person.fill"
size={iconSize}
color={textColor}
style={styles.icon}
/>
)}
<ThemedText
style={[styles.count, { color: textColor, fontSize }]}
lightColor={textColor}
darkColor={textColor}
>
{displayText}
</ThemedText>
</View>
);
}
/**
* 根据变体获取背景颜色
* 性能即艺术:使用缓存避免重复计算
*/
function useBadgeColor(variant: UserCountBadgeVariant): string {
const primaryColor = useThemeColor({ light: Colors.light.tint, dark: Colors.dark.tint }, 'tint');
const secondaryColor = useThemeColor({ light: Colors.light.categoryBadge, dark: Colors.dark.categoryBadge }, 'categoryBadge');
return variant === 'primary' ? primaryColor : secondaryColor;
}
/**
* 根据尺寸获取配置
* 优雅处理不同尺寸的视觉平衡
*/
function useSizeConfig(size: UserCountBadgeSize) {
const configs = {
small: {
badgeSize: { width: 20, height: 20, borderRadius: 10 },
iconSize: 8,
fontSize: 10,
},
medium: {
badgeSize: { width: 28, height: 28, borderRadius: 14 },
iconSize: 12,
fontSize: 14,
},
large: {
badgeSize: { width: 36, height: 36, borderRadius: 18 },
iconSize: 16,
fontSize: 16,
},
};
return configs[size];
}
/**
* 格式化显示数字
* 错误处理如为人处世的哲学:优雅地处理边界情况
*/
function formatCount(count: number): string {
if (count <= 0) return '0';
if (count <= 99) return count.toString();
return '99+';
}
/**
* 日志是思想的表达:每个样式都有其存在的意义
*/
const styles = StyleSheet.create({
badge: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
// 阴影效果增强立体感
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
},
icon: {
// 图标与文字的和谐间距
marginRight: 2,
},
count: {
fontWeight: '600',
// 确保文字在各种背景下都清晰可读
includeFontPadding: false,
textAlignVertical: 'center',
},
});

View File

@@ -18,7 +18,7 @@ export function BalanceCard({ balance, onRecharge, onHistory }: BalanceCardProps
const borderColor = useThemeColor({}, 'cardBorder'); const borderColor = useThemeColor({}, 'cardBorder');
return ( return (
<ThemedView style={styles.container}> <ThemedView style={[styles.container, { backgroundColor: cardColor, borderColor }]}>
<View style={styles.header}> <View style={styles.header}>
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<IconSymbol name="dollarsign.circle" size={24} color={tintColor} /> <IconSymbol name="dollarsign.circle" size={24} color={tintColor} />
@@ -60,6 +60,8 @@ export function BalanceCard({ balance, onRecharge, onHistory }: BalanceCardProps
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
padding: 20, padding: 20,
borderRadius: 20,
borderWidth: 1,
}, },
header: { header: {
flexDirection: 'row', flexDirection: 'row',
@@ -129,4 +131,4 @@ const styles = StyleSheet.create({
color: '#666', color: '#666',
flex: 1, flex: 1,
}, },
}); });

View File

@@ -1,6 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { StyleSheet, ScrollView, TouchableOpacity, RefreshControl, Alert, ActivityIndicator } from 'react-native'; import { StyleSheet, ScrollView, TouchableOpacity, RefreshControl, Alert } from 'react-native';
import { router } from 'expo-router';
import { ThemedView } from '@/components/themed-view'; import { ThemedView } from '@/components/themed-view';
import { ThemedText } from '@/components/themed-text'; import { ThemedText } from '@/components/themed-text';
@@ -44,10 +43,8 @@ const mockRecords: GenerationRecord[] = [
export function GenerationRecords({ userId }: GenerationRecordsProps) { export function GenerationRecords({ userId }: GenerationRecordsProps) {
const [records, setRecords] = useState<GenerationRecord[]>(mockRecords); const [records, setRecords] = useState<GenerationRecord[]>(mockRecords);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false);
const tintColor = useThemeColor({}, 'tint'); const tintColor = useThemeColor({}, 'tint');
const textColor = useThemeColor({}, 'text');
const getTypeIcon = (type: string) => { const getTypeIcon = (type: string) => {
switch (type) { switch (type) {
@@ -124,7 +121,7 @@ export function GenerationRecords({ userId }: GenerationRecordsProps) {
} }
}; };
if (records.length === 0 && !loading) { if (records.length === 0) {
return ( return (
<ThemedView style={styles.container}> <ThemedView style={styles.container}>
<ThemedView style={styles.header}> <ThemedView style={styles.header}>
@@ -316,4 +313,4 @@ const styles = StyleSheet.create({
fontSize: 14, fontSize: 14,
color: '#666', color: '#666',
}, },
}); });

View File

@@ -1,6 +1,6 @@
import { ThemedText } from '@/components/themed-text'; import { ThemedText } from '@/components/themed-text';
import { AVPlaybackStatus, ResizeMode, Video } from 'expo-av'; import { AVPlaybackStatus, ResizeMode, Video } from 'expo-av';
import React, { useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
Dimensions, Dimensions,
@@ -11,6 +11,7 @@ import {
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
View, View,
BackHandler,
} from 'react-native'; } from 'react-native';
const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
@@ -44,15 +45,11 @@ export function FullscreenVideoModal({
}: FullscreenVideoModalProps) { }: FullscreenVideoModalProps) {
const videoRef = useRef<Video>(null); const videoRef = useRef<Video>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isPlaying, setIsPlaying] = useState(autoPlay);
const [showControls, setShowControls] = useState(true);
// 重置状态当模态框打开/关闭时 // 重置状态当模态框打开/关闭时
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
setIsLoading(true); setIsLoading(true);
setIsPlaying(autoPlay);
setShowControls(true);
} else { } else {
// 停止播放当关闭时 // 停止播放当关闭时
if (videoRef.current) { if (videoRef.current) {
@@ -71,29 +68,21 @@ export function FullscreenVideoModal({
} }
}; };
// 处理视频播放状态变化
const handlePlaybackStatusUpdate = (status: AVPlaybackStatus) => {
if (status.isLoaded) {
setIsPlaying(status.isPlaying);
}
};
// 处理视频错误 // 处理视频错误
const handleVideoError = (error: any) => { const handleVideoError = (error: any) => {
console.error('视频播放错误:', error); console.error('视频播放错误:', error);
setIsLoading(false); setIsLoading(false);
}; };
const handleClose = useCallback(() => {
onClose();
}, [onClose]);
// 视频点击处理(直接关闭) // 视频点击处理(直接关闭)
const handleVideoPress = () => { const handleVideoPress = () => {
handleClose(); handleClose();
}; };
// 关闭模态框
const handleClose = () => {
onClose();
};
// 创建手势处理器 // 创建手势处理器
const panResponder = useRef( const panResponder = useRef(
PanResponder.create({ PanResponder.create({
@@ -117,7 +106,7 @@ export function FullscreenVideoModal({
// 处理返回键Android // 处理返回键Android
useEffect(() => { useEffect(() => {
const backHandler = () => { const handleBackPress = () => {
if (visible) { if (visible) {
handleClose(); handleClose();
return true; return true;
@@ -125,13 +114,9 @@ export function FullscreenVideoModal({
return false; return false;
}; };
// 在实际应用中,这里需要添加返回键监听 const subscription = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
// 对于演示,我们只做概念性实现 return () => subscription.remove();
}, [visible, handleClose]);
return () => {
// 清理返回键监听
};
}, [visible]);
if (!visible) return null; if (!visible) return null;
@@ -181,7 +166,6 @@ export function FullscreenVideoModal({
isMuted={isMuted} isMuted={isMuted}
useNativeControls={false} useNativeControls={false}
onReadyForDisplay={handleVideoReady} onReadyForDisplay={handleVideoReady}
onPlaybackStatusUpdate={handlePlaybackStatusUpdate}
onError={handleVideoError} onError={handleVideoError}
/> />
)} )}
@@ -291,4 +275,4 @@ const styles = StyleSheet.create({
}, },
}); });
export default FullscreenVideoModal; export default FullscreenVideoModal;

View File

@@ -4,7 +4,7 @@ import { usernameClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react"; import { createAuthClient } from "better-auth/react";
import { storage } from '../storage'; import { storage } from '../storage';
export const authClient = createAuthClient({ export const authClient = createAuthClient({
baseURL: "http://localhost:14333/api/auth", baseURL: "https://api.mixvideo.bowong.cc/api/auth",
fetchOptions: { fetchOptions: {
credentials: "omit", credentials: "omit",
auth: { auth: {

View File

@@ -4,6 +4,7 @@
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"start": "expo start", "start": "expo start",
"build": "expo export --platform web",
"reset-project": "node ./scripts/reset-project.js", "reset-project": "node ./scripts/reset-project.js",
"android": "expo run:android", "android": "expo run:android",
"ios": "expo run:ios", "ios": "expo run:ios",

View File

@@ -0,0 +1,98 @@
type TestCase = {
name: string;
execute: () => void;
};
function logSuccess(message: string) {
console.log(`${message}`);
}
function logFailure(message: string, error: unknown) {
console.error(`${message}`, error);
}
function encodeCookieValue(value: string) {
return encodeURIComponent(value);
}
function decodeCookieValue(value: string) {
return decodeURIComponent(value);
}
function runTestCase({ name, execute }: TestCase) {
try {
execute();
logSuccess(name);
} catch (error) {
logFailure(name, error);
}
}
export function runCookieEncodingTests() {
const cases: TestCase[] = [
{
name: '基本英文字母安全编码',
execute: () => {
const input = 'session=test';
const encoded = encodeCookieValue(input);
const decoded = decodeCookieValue(encoded);
if (decoded !== input) {
throw new Error('解码值与原始值不一致');
}
},
},
{
name: '支持中文字符',
execute: () => {
const input = '昵称=代码艺术家';
const encoded = encodeCookieValue(input);
const decoded = decodeCookieValue(encoded);
if (decoded !== input) {
throw new Error('中文字符编码失败');
}
},
},
{
name: '特殊字符保留',
execute: () => {
const input = 'symbols=!@#$%^&*()_+-={}[]|:;"\'<>,.?/~`';
const encoded = encodeCookieValue(input);
const decoded = decodeCookieValue(encoded);
if (decoded !== input) {
throw new Error('特殊字符编码失败');
}
},
},
];
cases.forEach(runTestCase);
}
export function testRealWorldScenarios() {
const cases: TestCase[] = [
{
name: 'JWT Token 轮换',
execute: () => {
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9';
const encoded = encodeCookieValue(token);
const decoded = decodeCookieValue(encoded);
if (decoded !== token) {
throw new Error('JWT token 编解码失败');
}
},
},
{
name: '长文本粘贴',
execute: () => {
const text = 'Prompt=' + '艺术创作'.repeat(20);
const encoded = encodeCookieValue(text);
const decoded = decodeCookieValue(encoded);
if (decoded !== text) {
throw new Error('长文本编码失败');
}
},
},
];
cases.forEach(runTestCase);
}