add ios app

This commit is contained in:
penghanyuan
2026-01-31 21:20:50 +01:00
parent a170632270
commit 748f252c2f
63 changed files with 38507 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
import { Tabs } from 'expo-router';
import React from 'react';
import { Platform } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { HapticTab } from '@/components/haptic-tab';
import { AppColors } from '@/constants/theme';
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: AppColors.mediumPurple,
tabBarInactiveTintColor: AppColors.slatePurple,
headerShown: false,
tabBarButton: HapticTab,
tabBarStyle: {
backgroundColor: AppColors.white,
borderTopColor: 'rgba(32, 0, 40, 0.08)',
borderTopWidth: 1,
height: Platform.OS === 'ios' ? 90 : 70,
paddingTop: 8,
paddingBottom: Platform.OS === 'ios' ? 30 : 10,
},
tabBarLabelStyle: {
fontSize: 11,
fontWeight: '500',
marginTop: 2,
},
}}>
<Tabs.Screen
name="index"
options={{
title: '聊天',
tabBarIcon: ({ color, focused }) => (
<Ionicons
name={focused ? 'chatbubble' : 'chatbubble-outline'}
size={24}
color={color}
/>
),
}}
/>
<Tabs.Screen
name="memoir"
options={{
title: '回忆录',
tabBarIcon: ({ color, focused }) => (
<Ionicons
name={focused ? 'book' : 'book-outline'}
size={24}
color={color}
/>
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: '我的',
tabBarIcon: ({ color, focused }) => (
<Ionicons
name={focused ? 'person' : 'person-outline'}
size={24}
color={color}
/>
),
}}
/>
</Tabs>
);
}

View File

@@ -0,0 +1,126 @@
import { ConversationItem } from '@/components/chat/ConversationItem';
import { AppColors } from '@/constants/theme';
import { mockConversations } from '@/data/mockData';
import { Ionicons } from '@expo/vector-icons';
import { router } from 'expo-router';
import React from 'react';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
export default function ChatListScreen() {
const handleConversationPress = (conversationId: string) => {
router.push(`/chat/${conversationId}`);
};
return (
<SafeAreaView style={styles.container} edges={['top']}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.headerTitle}></Text>
</View>
{/* Content */}
<ScrollView
style={styles.content}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
>
{/* Section Title */}
<Text style={styles.sectionTitle}></Text>
{/* Conversation List */}
{mockConversations.map((conversation) => (
<React.Fragment key={conversation.id}>
<ConversationItem
conversation={conversation}
onPress={() => handleConversationPress(conversation.id)}
/>
<View style={styles.divider} />
</React.Fragment>
))}
{/* Tip Card */}
<View style={styles.tipCard}>
<View style={styles.tipHeader}>
<Ionicons
name="information-circle-outline"
size={18}
color={AppColors.mediumPurple}
/>
<Text style={styles.tipTitle}></Text>
</View>
<Text style={styles.tipContent}>
AI
</Text>
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.mediumPurple,
},
header: {
height: 62,
paddingHorizontal: 20,
justifyContent: 'center',
backgroundColor: AppColors.mediumPurple,
},
headerTitle: {
fontSize: 32,
fontWeight: '600',
color: AppColors.white,
},
content: {
flex: 1,
backgroundColor: AppColors.cream,
},
contentContainer: {
paddingBottom: 20,
},
sectionTitle: {
fontSize: 13,
color: AppColors.slatePurple,
fontWeight: '500',
letterSpacing: 0.5,
paddingHorizontal: 20,
paddingTop: 16,
paddingBottom: 10,
},
divider: {
height: 1,
backgroundColor: 'rgba(32, 0, 40, 0.06)',
marginHorizontal: 20,
},
tipCard: {
marginHorizontal: 20,
marginTop: 20,
padding: 16,
backgroundColor: AppColors.white,
borderRadius: 16,
shadowColor: AppColors.deepPurple,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 12,
elevation: 2,
},
tipHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
tipTitle: {
fontSize: 14,
fontWeight: '500',
color: AppColors.deepPurple,
marginLeft: 8,
},
tipContent: {
fontSize: 13,
color: AppColors.slatePurple,
lineHeight: 21,
},
});

View File

@@ -0,0 +1,372 @@
import { ChapterContent } from '@/components/memoir/ChapterContent';
import { ChapterItem } from '@/components/memoir/ChapterItem';
import { FullMemoirContent } from '@/components/memoir/FullMemoirContent';
import { AppColors } from '@/constants/theme';
import { Chapter, mockMemoir } from '@/data/mockData';
import { Ionicons } from '@expo/vector-icons';
import React, { useEffect, useState } from 'react';
import {
Dimensions,
Modal,
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { SafeAreaView } from 'react-native-safe-area-context';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
type ViewType = 'toc' | 'chapter' | 'fullRead';
export default function MemoirScreen() {
const [currentView, setCurrentView] = useState<ViewType>('toc');
const [selectedChapter, setSelectedChapter] = useState<Chapter | null>(null);
const [showMoreMenu, setShowMoreMenu] = useState(false);
// Animation values
const tocTranslateX = useSharedValue(0);
const readingTranslateX = useSharedValue(SCREEN_WIDTH);
const isReading = currentView === 'chapter' || currentView === 'fullRead';
useEffect(() => {
if (isReading) {
// Slide reading view in from right
tocTranslateX.value = withTiming(-SCREEN_WIDTH * 0.3, {
duration: 300,
easing: Easing.out(Easing.cubic),
});
readingTranslateX.value = withTiming(0, {
duration: 300,
easing: Easing.out(Easing.cubic),
});
} else {
// Slide reading view out to right
tocTranslateX.value = withTiming(0, {
duration: 300,
easing: Easing.out(Easing.cubic),
});
readingTranslateX.value = withTiming(SCREEN_WIDTH, {
duration: 300,
easing: Easing.out(Easing.cubic),
});
}
}, [isReading]);
const tocAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: tocTranslateX.value }],
}));
const readingAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: readingTranslateX.value }],
}));
const handleChapterPress = (chapter: Chapter) => {
setSelectedChapter(chapter);
setCurrentView('chapter');
};
const handleBackToToc = () => {
setCurrentView('toc');
setTimeout(() => {
setSelectedChapter(null);
}, 300);
};
const handleReadAll = () => {
setCurrentView('fullRead');
setSelectedChapter(null);
};
const handleExportPdf = () => {
setShowMoreMenu(false);
console.log('Export PDF pressed');
};
const handleShare = () => {
setShowMoreMenu(false);
console.log('Share pressed');
};
return (
<SafeAreaView style={styles.container} edges={['top']}>
<View style={styles.contentWrapper}>
{/* TOC View */}
<Animated.View style={[styles.tocView, tocAnimatedStyle]}>
<View style={styles.purpleHeader}>
<Text style={styles.purpleHeaderTitle}></Text>
</View>
<ScrollView
style={styles.tocContainer}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.tocContent}
>
{/* Book Info */}
<View style={styles.bookHeader}>
<Text style={styles.bookTitle}>{mockMemoir.title}</Text>
<Text style={styles.bookSubtitle}>{mockMemoir.subtitle}</Text>
<Text style={styles.bookUpdate}> {mockMemoir.updatedAt}</Text>
</View>
{/* Chapter List */}
{mockMemoir.chapters.map((chapter) => (
<ChapterItem
key={chapter.id}
chapter={chapter}
onPress={() => handleChapterPress(chapter)}
/>
))}
</ScrollView>
{/* Read All Button */}
{mockMemoir.chapters.length > 0 && (
<View style={styles.floatingActions}>
<TouchableOpacity
style={styles.readAllButton}
onPress={handleReadAll}
activeOpacity={0.8}
>
<Ionicons name="book" size={18} color={AppColors.white} />
<Text style={styles.readAllButtonText}></Text>
</TouchableOpacity>
</View>
)}
</Animated.View>
{/* Reading View */}
<Animated.View style={[styles.readingView, readingAnimatedStyle]}>
<View style={styles.readingHeader}>
<TouchableOpacity
style={styles.backButton}
onPress={handleBackToToc}
activeOpacity={0.7}
>
<Ionicons name="chevron-back" size={24} color={AppColors.white} />
<Text style={styles.backButtonText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.moreButton}
onPress={() => setShowMoreMenu(true)}
activeOpacity={0.7}
>
<Ionicons name="ellipsis-horizontal" size={22} color={AppColors.white} />
</TouchableOpacity>
</View>
<View style={styles.readingContainer}>
{currentView === 'fullRead' ? (
<FullMemoirContent memoir={mockMemoir} />
) : selectedChapter ? (
<ChapterContent chapter={selectedChapter} />
) : null}
</View>
</Animated.View>
</View>
{/* More Menu Modal */}
<Modal
visible={showMoreMenu}
transparent
animationType="fade"
onRequestClose={() => setShowMoreMenu(false)}
>
<Pressable
style={styles.modalOverlay}
onPress={() => setShowMoreMenu(false)}
>
<View style={styles.menuContainer}>
<TouchableOpacity
style={styles.menuItem}
onPress={handleExportPdf}
activeOpacity={0.7}
>
<Ionicons name="download-outline" size={20} color={AppColors.deepPurple} />
<Text style={styles.menuItemText}> PDF</Text>
</TouchableOpacity>
<View style={styles.menuDivider} />
<TouchableOpacity
style={styles.menuItem}
onPress={handleShare}
activeOpacity={0.7}
>
<Ionicons name="share-social-outline" size={20} color={AppColors.deepPurple} />
<Text style={styles.menuItemText}></Text>
</TouchableOpacity>
</View>
</Pressable>
</Modal>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.mediumPurple,
},
contentWrapper: {
flex: 1,
position: 'relative',
overflow: 'hidden',
},
tocView: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
},
readingView: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: AppColors.mediumPurple,
},
purpleHeader: {
height: 62,
paddingHorizontal: 20,
justifyContent: 'center',
backgroundColor: AppColors.mediumPurple,
},
purpleHeaderTitle: {
fontSize: 32,
fontWeight: '600',
color: AppColors.white,
},
readingHeader: {
height: 62,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: AppColors.mediumPurple,
paddingHorizontal: 16,
},
backButton: {
flexDirection: 'row',
alignItems: 'center',
},
backButtonText: {
fontSize: 16,
color: AppColors.white,
marginLeft: 4,
},
moreButton: {
width: 40,
height: 40,
borderRadius: 20,
alignItems: 'center',
justifyContent: 'center',
},
tocContainer: {
flex: 1,
backgroundColor: AppColors.cream,
},
tocContent: {
padding: 20,
paddingBottom: 120,
},
bookHeader: {
alignItems: 'center',
marginBottom: 24,
paddingVertical: 20,
backgroundColor: AppColors.white,
borderRadius: 16,
shadowColor: AppColors.deepPurple,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 12,
elevation: 2,
},
bookTitle: {
fontSize: 24,
fontWeight: '600',
color: AppColors.deepPurple,
marginBottom: 6,
},
bookSubtitle: {
fontSize: 13,
color: AppColors.slatePurple,
},
bookUpdate: {
fontSize: 12,
color: AppColors.mediumPurple,
marginTop: 6,
},
readingContainer: {
flex: 1,
backgroundColor: AppColors.cream,
},
floatingActions: {
position: 'absolute',
bottom: Platform.OS === 'ios' ? 100 : 80,
left: 0,
right: 0,
alignItems: 'center',
paddingHorizontal: 20,
},
readAllButton: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 14,
paddingHorizontal: 32,
borderRadius: 25,
backgroundColor: AppColors.mediumPurple,
gap: 8,
shadowColor: AppColors.deepPurple,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 12,
elevation: 4,
},
readAllButtonText: {
fontSize: 16,
fontWeight: '600',
color: AppColors.white,
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
justifyContent: 'flex-start',
alignItems: 'flex-end',
paddingTop: Platform.OS === 'ios' ? 110 : 90,
paddingRight: 20,
},
menuContainer: {
backgroundColor: AppColors.white,
borderRadius: 12,
paddingVertical: 8,
minWidth: 160,
shadowColor: AppColors.deepPurple,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 16,
elevation: 8,
},
menuItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16,
gap: 12,
},
menuItemText: {
fontSize: 15,
color: AppColors.deepPurple,
},
menuDivider: {
height: 1,
backgroundColor: 'rgba(32, 0, 40, 0.08)',
marginHorizontal: 16,
},
});

View File

@@ -0,0 +1,179 @@
import { SectionCard } from '@/components/profile/SectionCard';
import { SettingItem } from '@/components/profile/SettingItem';
import { AppColors } from '@/constants/theme';
import { mockUserProfile, settingsSections } from '@/data/mockData';
import { Ionicons } from '@expo/vector-icons';
import React, { useState } from 'react';
import { Alert, ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
export default function ProfileScreen() {
const [settings, setSettings] = useState({
largeFont: false,
darkMode: false,
reminder: true,
});
const handleToggle = (key: keyof typeof settings) => (value: boolean) => {
setSettings(prev => ({ ...prev, [key]: value }));
};
const showToast = (message: string) => {
Alert.alert('提示', message);
};
return (
<SafeAreaView style={styles.container} edges={['top']}>
<ScrollView
style={styles.content}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.contentContainer}
>
{/* Profile Header */}
<View style={styles.profileHeader}>
<View style={styles.avatar}>
<Ionicons
name="person-outline"
size={36}
color={AppColors.deepPurple}
/>
</View>
<Text style={styles.profileName}>{mockUserProfile.nickname}</Text>
<View style={styles.planBadge}>
<View style={styles.planDot} />
<Text style={styles.planText}>{mockUserProfile.planLabel}</Text>
</View>
</View>
{/* Payment Section */}
<SectionCard title={settingsSections.payment.title}>
{settingsSections.payment.items.map((item, index) => (
<SettingItem
key={item.id}
icon={item.icon}
label={item.label}
description={item.description}
type={item.type}
isLast={index === settingsSections.payment.items.length - 1}
onPress={() => showToast('功能开发中...')}
/>
))}
</SectionCard>
{/* Privacy Section */}
{/* <SectionCard title={settingsSections.privacy.title}>
{settingsSections.privacy.items.map((item, index) => (
<SettingItem
key={item.id}
icon={item.icon}
label={item.label}
description={item.description}
type={item.type}
isLast={index === settingsSections.privacy.items.length - 1}
onPress={() => showToast('数据导出中...')}
/>
))}
</SectionCard> */}
{/* Settings Section */}
{/* <SectionCard title={settingsSections.settings.title}>
<SettingItem
icon="time-outline"
label="语速"
description="标准"
type="arrow"
onPress={() => showToast('功能开发中...')}
/>
<SettingItem
icon="text-outline"
label="大字模式"
type="toggle"
value={settings.largeFont}
onToggle={handleToggle('largeFont')}
/>
<SettingItem
icon="moon-outline"
label="夜间模式"
type="toggle"
value={settings.darkMode}
onToggle={handleToggle('darkMode')}
isLast
/>
</SectionCard> */}
{/* Help Section */}
<SectionCard title={settingsSections.help.title}>
{settingsSections.help.items.map((item, index) => (
<SettingItem
key={item.id}
icon={item.icon}
label={item.label}
type={item.type}
isLast={index === settingsSections.help.items.length - 1}
onPress={() => showToast('功能开发中...')}
/>
))}
</SectionCard>
{/* Version Info */}
<Text style={styles.version}> 1.0.0</Text>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.cream,
},
content: {
flex: 1,
},
contentContainer: {
padding: 20,
paddingTop: 28,
paddingBottom: 40,
},
profileHeader: {
alignItems: 'center',
paddingVertical: 20,
marginBottom: 4,
},
avatar: {
width: 72,
height: 72,
borderRadius: 36,
backgroundColor: AppColors.lavender,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 12,
},
profileName: {
fontSize: 18,
fontWeight: '600',
color: AppColors.deepPurple,
marginBottom: 4,
},
planBadge: {
flexDirection: 'row',
alignItems: 'center',
},
planDot: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: AppColors.mediumPurple,
marginRight: 4,
},
planText: {
fontSize: 13,
color: AppColors.mediumPurple,
},
version: {
textAlign: 'center',
fontSize: 12,
color: AppColors.slatePurple,
marginTop: 20,
},
});