add ios app
This commit is contained in:
73
app-ios/app/(tabs)/_layout.tsx
Normal file
73
app-ios/app/(tabs)/_layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
126
app-ios/app/(tabs)/index.tsx
Normal file
126
app-ios/app/(tabs)/index.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
372
app-ios/app/(tabs)/memoir.tsx
Normal file
372
app-ios/app/(tabs)/memoir.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
179
app-ios/app/(tabs)/profile.tsx
Normal file
179
app-ios/app/(tabs)/profile.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
68
app-ios/app/_layout.tsx
Normal file
68
app-ios/app/_layout.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { GluestackUIProvider } from '@gluestack-ui/themed';
|
||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||
import { Stack } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import 'react-native-reanimated';
|
||||
|
||||
import { useColorScheme } from '@/hooks/use-color-scheme';
|
||||
import { gluestackConfig, AppColors } from '@/constants/theme';
|
||||
|
||||
// Custom light theme with our colors
|
||||
const CustomLightTheme = {
|
||||
...DefaultTheme,
|
||||
colors: {
|
||||
...DefaultTheme.colors,
|
||||
primary: AppColors.mediumPurple,
|
||||
background: AppColors.cream,
|
||||
card: AppColors.white,
|
||||
text: AppColors.deepPurple,
|
||||
border: AppColors.lavender,
|
||||
notification: AppColors.mediumPurple,
|
||||
},
|
||||
};
|
||||
|
||||
// Custom dark theme with our colors
|
||||
const CustomDarkTheme = {
|
||||
...DarkTheme,
|
||||
colors: {
|
||||
...DarkTheme.colors,
|
||||
primary: AppColors.lavender,
|
||||
background: AppColors.deepPurple,
|
||||
card: '#2D0036',
|
||||
text: AppColors.white,
|
||||
border: AppColors.slatePurple,
|
||||
notification: AppColors.mediumPurple,
|
||||
},
|
||||
};
|
||||
|
||||
export const unstable_settings = {
|
||||
anchor: '(tabs)',
|
||||
};
|
||||
|
||||
export default function RootLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<GluestackUIProvider config={gluestackConfig}>
|
||||
<ThemeProvider value={colorScheme === 'dark' ? CustomDarkTheme : CustomLightTheme}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen
|
||||
name="chat/[id]"
|
||||
options={{
|
||||
headerShown: false,
|
||||
animation: 'slide_from_right',
|
||||
gestureEnabled: true,
|
||||
gestureDirection: 'horizontal',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
|
||||
</Stack>
|
||||
<StatusBar style={colorScheme === 'dark' ? 'light' : 'dark'} />
|
||||
</ThemeProvider>
|
||||
</GluestackUIProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
186
app-ios/app/chat/[id].tsx
Normal file
186
app-ios/app/chat/[id].tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { ChatBubble, TimeSeparator, TypingIndicator } from '@/components/chat/ChatBubble';
|
||||
import { ChatInput } from '@/components/chat/ChatInput';
|
||||
import { AppColors } from '@/constants/theme';
|
||||
import {
|
||||
getCurrentTimeString,
|
||||
getRandomAIResponse,
|
||||
Message,
|
||||
mockConversations,
|
||||
mockMessages,
|
||||
} from '@/data/mockData';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { router, useLocalSearchParams } from 'expo-router';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
export default function ChatDetailScreen() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
|
||||
const [messages, setMessages] = useState<Message[]>(mockMessages);
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
|
||||
// Find conversation info
|
||||
const conversation = mockConversations.find(c => c.id === id) || mockConversations[0];
|
||||
|
||||
// Scroll to bottom when messages change
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
scrollViewRef.current?.scrollToEnd({ animated: true });
|
||||
}, 100);
|
||||
}, [messages, isTyping]);
|
||||
|
||||
const handleSendMessage = (text: string) => {
|
||||
// Add user message
|
||||
const userMessage: Message = {
|
||||
id: `m${Date.now()}`,
|
||||
conversationId: id || '1',
|
||||
senderType: 'user',
|
||||
contentType: 'text',
|
||||
content: text,
|
||||
timestamp: getCurrentTimeString(),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
|
||||
// Simulate AI typing
|
||||
setTimeout(() => {
|
||||
setIsTyping(true);
|
||||
}, 500);
|
||||
|
||||
// Add AI response after delay
|
||||
setTimeout(() => {
|
||||
setIsTyping(false);
|
||||
const aiMessage: Message = {
|
||||
id: `m${Date.now() + 1}`,
|
||||
conversationId: id || '1',
|
||||
senderType: 'ai',
|
||||
contentType: 'text',
|
||||
content: getRandomAIResponse(),
|
||||
timestamp: getCurrentTimeString(),
|
||||
};
|
||||
setMessages(prev => [...prev, aiMessage]);
|
||||
}, 1500 + Math.random() * 1000);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['top']}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={handleBack}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={AppColors.white} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.headerTitleArea}>
|
||||
<Text style={styles.headerTitle}>{conversation.title}</Text>
|
||||
<Text style={styles.headerStatus}>在线</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Messages */}
|
||||
<KeyboardAvoidingView
|
||||
style={styles.messageContainer}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
keyboardVerticalOffset={0}
|
||||
>
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={styles.messageList}
|
||||
contentContainerStyle={styles.messageListContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<TimeSeparator time="今天 14:30" />
|
||||
|
||||
{messages.map((message) => (
|
||||
<ChatBubble key={message.id} message={message} />
|
||||
))}
|
||||
|
||||
{isTyping && <TypingIndicator />}
|
||||
</ScrollView>
|
||||
|
||||
{/* Input Area */}
|
||||
<ChatInput
|
||||
onSendMessage={handleSendMessage}
|
||||
onVoicePress={() => {
|
||||
// Voice input placeholder
|
||||
console.log('Voice input pressed');
|
||||
}}
|
||||
onEmojiPress={() => {
|
||||
// Emoji picker placeholder
|
||||
console.log('Emoji picker pressed');
|
||||
}}
|
||||
onMorePress={() => {
|
||||
// More options placeholder
|
||||
console.log('More options pressed');
|
||||
}}
|
||||
/>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: AppColors.mediumPurple,
|
||||
},
|
||||
header: {
|
||||
height: 62,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: AppColors.mediumPurple,
|
||||
},
|
||||
backButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 12,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
headerTitleArea: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
marginRight: 40, // Balance the back button
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: AppColors.white,
|
||||
},
|
||||
headerStatus: {
|
||||
fontSize: 12,
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
marginTop: 2,
|
||||
},
|
||||
messageContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: AppColors.cream,
|
||||
},
|
||||
messageList: {
|
||||
flex: 1,
|
||||
backgroundColor: AppColors.cream,
|
||||
},
|
||||
messageListContent: {
|
||||
paddingVertical: 16,
|
||||
},
|
||||
});
|
||||
29
app-ios/app/modal.tsx
Normal file
29
app-ios/app/modal.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Link } from 'expo-router';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { ThemedText } from '@/components/themed-text';
|
||||
import { ThemedView } from '@/components/themed-view';
|
||||
|
||||
export default function ModalScreen() {
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<ThemedText type="title">This is a modal</ThemedText>
|
||||
<Link href="/" dismissTo style={styles.link}>
|
||||
<ThemedText type="link">Go to home screen</ThemedText>
|
||||
</Link>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
link: {
|
||||
marginTop: 15,
|
||||
paddingVertical: 15,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user