提交修改
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react'
|
||||
import { useColorScheme } from 'react-native'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import type { ThemeMode, IThemeColors } from '../constants/theme'
|
||||
import { THEME } from '../constants/theme'
|
||||
|
||||
const STORAGE_KEY = 'inchstep-theme'
|
||||
|
||||
interface IThemeContext {
|
||||
mode: ThemeMode
|
||||
colors: IThemeColors
|
||||
toggle: () => void
|
||||
isDark: boolean
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<IThemeContext>({
|
||||
mode: 'light',
|
||||
colors: THEME.light,
|
||||
toggle: () => {},
|
||||
isDark: false,
|
||||
})
|
||||
|
||||
export function useTheme() {
|
||||
return useContext(ThemeContext)
|
||||
}
|
||||
|
||||
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const systemScheme = useColorScheme()
|
||||
const [mode, setModeState] = useState<ThemeMode>('light')
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
AsyncStorage.getItem(STORAGE_KEY).then((stored) => {
|
||||
if (stored === 'light' || stored === 'dark') {
|
||||
setModeState(stored)
|
||||
} else if (systemScheme === 'dark') {
|
||||
setModeState('dark')
|
||||
}
|
||||
setLoaded(true)
|
||||
})
|
||||
}, [systemScheme])
|
||||
|
||||
useEffect(() => {
|
||||
if (loaded) {
|
||||
AsyncStorage.setItem(STORAGE_KEY, mode)
|
||||
}
|
||||
}, [mode, loaded])
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setModeState(prev => (prev === 'light' ? 'dark' : 'light'))
|
||||
}, [])
|
||||
|
||||
if (!loaded) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ mode, colors: THEME[mode], toggle, isDark: mode === 'dark' }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useColorScheme } from 'react-native'
|
||||
|
||||
export type ThemeMode = 'light' | 'dark'
|
||||
|
||||
export interface IThemeColors {
|
||||
primary: string
|
||||
primaryLight: string
|
||||
primaryGradient: string
|
||||
accent: string
|
||||
bg: string
|
||||
card: string
|
||||
text: string
|
||||
textSecondary: string
|
||||
border: string
|
||||
navBg: string
|
||||
success: string
|
||||
warning: string
|
||||
error: string
|
||||
}
|
||||
|
||||
export const THEME: Record<ThemeMode, IThemeColors> = {
|
||||
light: {
|
||||
primary: '#FF6B35',
|
||||
primaryLight: '#FF8C5A',
|
||||
primaryGradient: ['#FF6B35', '#FF4D6D'] as unknown as string,
|
||||
accent: '#7209B7',
|
||||
bg: '#F5F5F7',
|
||||
card: '#FFFFFF',
|
||||
text: '#1D1D1F',
|
||||
textSecondary: '#86868B',
|
||||
border: '#E5E5EA',
|
||||
navBg: 'rgba(245,245,247,0.95)',
|
||||
success: '#34C759',
|
||||
warning: '#FF9500',
|
||||
error: '#FF3B30',
|
||||
},
|
||||
dark: {
|
||||
primary: '#FF6B35',
|
||||
primaryLight: '#FF8C5A',
|
||||
primaryGradient: ['#FF6B35', '#FF4D6D'] as unknown as string,
|
||||
accent: '#BB86FC',
|
||||
bg: '#1C1C1E',
|
||||
card: '#2C2C2E',
|
||||
text: '#F5F5F7',
|
||||
textSecondary: '#98989D',
|
||||
border: '#38383A',
|
||||
navBg: 'rgba(28,28,30,0.95)',
|
||||
success: '#34C759',
|
||||
warning: '#FF9500',
|
||||
error: '#FF453A',
|
||||
},
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
|
||||
import Ionicons from '@expo/vector-icons/Ionicons'
|
||||
import { useTheme } from '../components/ThemeProvider'
|
||||
import type { MainTabParamList } from './types'
|
||||
import HomeStack from './HomeStack'
|
||||
import FeedStack from './FeedStack'
|
||||
@@ -9,15 +10,17 @@ import ProfileStack from './ProfileStack'
|
||||
const Tab = createBottomTabNavigator<MainTabParamList>()
|
||||
|
||||
export default function MainTabs() {
|
||||
const { colors, isDark } = useTheme()
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
tabBarActiveTintColor: '#FF6B35',
|
||||
tabBarInactiveTintColor: '#86868B',
|
||||
tabBarActiveTintColor: colors.primary,
|
||||
tabBarInactiveTintColor: colors.textSecondary,
|
||||
tabBarStyle: {
|
||||
backgroundColor: 'rgba(245,245,247,0.95)',
|
||||
borderTopColor: '#E5E5EA',
|
||||
backgroundColor: colors.navBg,
|
||||
borderTopColor: colors.border,
|
||||
borderTopWidth: 0.5,
|
||||
paddingTop: 6,
|
||||
paddingBottom: 20,
|
||||
|
||||
@@ -4,11 +4,14 @@ import { useNavigation } from '@react-navigation/native'
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { api } from '../services/api'
|
||||
import { useAuth } from '../components/AuthProvider'
|
||||
import { useTheme } from '../components/ThemeProvider'
|
||||
import type { HomeStackParamList } from '../navigation/types'
|
||||
import type { IThemeColors } from '../constants/theme'
|
||||
|
||||
export default function HomeScreen() {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<HomeStackParamList, 'HomeMain'>>()
|
||||
const { user } = useAuth()
|
||||
const { colors } = useTheme()
|
||||
const [items, setItems] = useState<any[]>([])
|
||||
const [streaks, setStreaks] = useState<Record<string, number>>({})
|
||||
const [todayStatus, setTodayStatus] = useState<Record<string, boolean>>({})
|
||||
@@ -34,37 +37,39 @@ export default function HomeScreen() {
|
||||
|
||||
const activeItems = items.filter(i => i.status === 'active')
|
||||
|
||||
const styles = createStyles(colors)
|
||||
|
||||
return (
|
||||
<ScrollView style={s.container} refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); loadItems().then(() => setRefreshing(false)) }} />}>
|
||||
<View style={s.header}>
|
||||
<View style={s.avatar}><Text style={s.avatarText}>{user?.nickname?.[0] || user?.phone?.slice(-4) || '?'}</Text></View>
|
||||
<ScrollView style={styles.container} refreshControl={<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); loadItems().then(() => setRefreshing(false)) }} />}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.avatar}><Text style={styles.avatarText}>{user?.nickname?.[0] || user?.phone?.slice(-4) || '?'}</Text></View>
|
||||
<View style={{ flex: 1, marginLeft: 12 }}>
|
||||
<Text style={s.headerName}>{user?.nickname || '用户'}</Text>
|
||||
<Text style={s.headerSub}>积分 {user?.points} · {activeItems.length} 个活跃项</Text>
|
||||
<Text style={styles.headerName}>{user?.nickname || '用户'}</Text>
|
||||
<Text style={styles.headerSub}>积分 {user?.points} · {activeItems.length} 个活跃项</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={s.sectionHeader}>
|
||||
<Text style={s.sectionTitle}>今日打卡</Text>
|
||||
<TouchableOpacity onPress={() => navigation.navigate('CreateCheckIn')}><Text style={s.createBtn}>+ 创建</Text></TouchableOpacity>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Text style={styles.sectionTitle}>今日打卡</Text>
|
||||
<TouchableOpacity onPress={() => navigation.navigate('CreateCheckIn')}><Text style={styles.createBtn}>+ 创建</Text></TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{activeItems.length === 0 ? (
|
||||
<View style={s.empty}>
|
||||
<View style={styles.empty}>
|
||||
<Text style={{ fontSize: 40 }}>📋</Text>
|
||||
<Text style={s.emptyText}>还没有打卡项</Text>
|
||||
<Text style={{ fontSize: 13, color: '#86868B', marginTop: 4 }}>点击上方"创建"开始</Text>
|
||||
<Text style={styles.emptyText}>还没有打卡项</Text>
|
||||
<Text style={{ fontSize: 13, color: colors.textSecondary, marginTop: 4 }}>点击上方"创建"开始</Text>
|
||||
</View>
|
||||
) : activeItems.map((item: any) => (
|
||||
<TouchableOpacity key={item.id} onPress={() => navigation.navigate('CheckInDetail', { itemId: item.id })}>
|
||||
<View style={s.card}>
|
||||
<View style={s.cardIcon}><Text style={{ fontSize: 22 }}>{item.icon || '📋'}</Text></View>
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardIcon}><Text style={{ fontSize: 22 }}>{item.icon || '📋'}</Text></View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={s.cardName}>{item.name}</Text>
|
||||
<Text style={s.cardMeta}>{item.category}</Text>
|
||||
{todayStatus[item.id] && <Text style={s.doneText}>✓ 已打卡</Text>}
|
||||
<Text style={styles.cardName}>{item.name}</Text>
|
||||
<Text style={styles.cardMeta}>{item.category}</Text>
|
||||
{todayStatus[item.id] && <Text style={styles.doneText}>✓ 已打卡</Text>}
|
||||
</View>
|
||||
{streaks[item.id] !== undefined && <Text style={s.streak}>🔥 {streaks[item.id]}天</Text>}
|
||||
{streaks[item.id] !== undefined && <Text style={styles.streak}>🔥 {streaks[item.id]}天</Text>}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -72,22 +77,24 @@ export default function HomeScreen() {
|
||||
)
|
||||
}
|
||||
|
||||
const s = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#F5F5F7' },
|
||||
header: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'white', borderRadius: 12, margin: 12, padding: 14, shadowOpacity: 0.04, shadowRadius: 3, elevation: 1 },
|
||||
avatar: { width: 44, height: 44, borderRadius: 22, backgroundColor: '#FF6B35', justifyContent: 'center', alignItems: 'center' },
|
||||
avatarText: { fontSize: 18, color: 'white', fontWeight: '600' },
|
||||
headerName: { fontSize: 16, fontWeight: '600' },
|
||||
headerSub: { fontSize: 13, color: '#86868B' },
|
||||
sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 16 },
|
||||
sectionTitle: { fontSize: 20, fontWeight: '700' },
|
||||
createBtn: { color: '#FF6B35', fontSize: 14, fontWeight: '600' },
|
||||
card: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'white', borderRadius: 12, marginHorizontal: 12, marginBottom: 8, padding: 14, shadowOpacity: 0.04, elevation: 1 },
|
||||
cardIcon: { width: 44, height: 44, borderRadius: 12, backgroundColor: '#FFF0EB', justifyContent: 'center', alignItems: 'center', marginRight: 14 },
|
||||
cardName: { fontSize: 16, fontWeight: '600' },
|
||||
cardMeta: { fontSize: 13, color: '#86868B', marginTop: 2 },
|
||||
doneText: { fontSize: 12, color: '#34C759', fontWeight: '600', marginTop: 2 },
|
||||
streak: { fontSize: 14, color: '#FF6B35', fontWeight: '600' },
|
||||
empty: { alignItems: 'center', padding: 60, marginTop: 20 },
|
||||
emptyText: { fontSize: 15, color: '#86868B', marginTop: 8 },
|
||||
})
|
||||
function createStyles(c: IThemeColors) {
|
||||
return StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: c.bg },
|
||||
header: { flexDirection: 'row', alignItems: 'center', backgroundColor: c.card, borderRadius: 12, margin: 12, padding: 14, shadowOpacity: 0.04, shadowRadius: 3, elevation: 1 },
|
||||
avatar: { width: 44, height: 44, borderRadius: 22, backgroundColor: c.primary, justifyContent: 'center', alignItems: 'center' },
|
||||
avatarText: { fontSize: 18, color: 'white', fontWeight: '600' },
|
||||
headerName: { fontSize: 16, fontWeight: '600', color: c.text },
|
||||
headerSub: { fontSize: 13, color: c.textSecondary },
|
||||
sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 16 },
|
||||
sectionTitle: { fontSize: 20, fontWeight: '700', color: c.text },
|
||||
createBtn: { color: c.primary, fontSize: 14, fontWeight: '600' },
|
||||
card: { flexDirection: 'row', alignItems: 'center', backgroundColor: c.card, borderRadius: 12, marginHorizontal: 12, marginBottom: 8, padding: 14, shadowOpacity: 0.04, elevation: 1 },
|
||||
cardIcon: { width: 44, height: 44, borderRadius: 12, backgroundColor: c.primaryLight + '20', justifyContent: 'center', alignItems: 'center', marginRight: 14 },
|
||||
cardName: { fontSize: 16, fontWeight: '600', color: c.text },
|
||||
cardMeta: { fontSize: 13, color: c.textSecondary, marginTop: 2 },
|
||||
doneText: { fontSize: 12, color: c.success, fontWeight: '600', marginTop: 2 },
|
||||
streak: { fontSize: 14, color: c.primary, fontWeight: '600' },
|
||||
empty: { alignItems: 'center', padding: 60, marginTop: 20 },
|
||||
emptyText: { fontSize: 15, color: c.textSecondary, marginTop: 8 },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React from 'react'
|
||||
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, Alert } from 'react-native'
|
||||
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, Alert, Switch } from 'react-native'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { useAuth } from '../components/AuthProvider'
|
||||
import { useTheme } from '../components/ThemeProvider'
|
||||
import type { ProfileStackParamList } from '../navigation/types'
|
||||
|
||||
export default function ProfileScreen() {
|
||||
const { user, logout } = useAuth()
|
||||
const { colors, isDark, toggle } = useTheme()
|
||||
const navigation = useNavigation<NativeStackNavigationProp<ProfileStackParamList, 'ProfileMain'>>()
|
||||
|
||||
const handleLogout = () => {
|
||||
@@ -17,23 +19,29 @@ export default function ProfileScreen() {
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView style={{ flex: 1, backgroundColor: '#F5F5F7' }}>
|
||||
<View style={styles.profileCard}>
|
||||
<ScrollView style={{ flex: 1, backgroundColor: colors.bg }}>
|
||||
<View style={[styles.profileCard, { backgroundColor: colors.card }]}>
|
||||
<View style={styles.avatar}><Text style={{ fontSize: 28, color: 'white', fontWeight: '700' }}>{user?.nickname?.[0] || user?.phone?.slice(-4) || '?'}</Text></View>
|
||||
<Text style={styles.name}>{user?.nickname || '未设置昵称'}</Text>
|
||||
<Text style={styles.bio}>{user?.bio || '还没有个性签名'}</Text>
|
||||
<Text style={[styles.name, { color: colors.text }]}>{user?.nickname || '未设置昵称'}</Text>
|
||||
<Text style={[styles.bio, { color: colors.textSecondary }]}>{user?.bio || '还没有个性签名'}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.statsRow}>
|
||||
<View style={styles.stat}><Text style={styles.statVal}>{user?.points || 0}</Text><Text style={styles.statLbl}>积分</Text></View>
|
||||
<View style={styles.stat}><Text style={styles.statVal}>--</Text><Text style={styles.statLbl}>连续</Text></View>
|
||||
<View style={styles.stat}><Text style={styles.statVal}>--</Text><Text style={styles.statLbl}>徽章</Text></View>
|
||||
<View style={[styles.statsRow, { backgroundColor: colors.card }]}>
|
||||
<View style={styles.stat}><Text style={[styles.statVal, { color: colors.primary }]}>{user?.points || 0}</Text><Text style={[styles.statLbl, { color: colors.textSecondary }]}>积分</Text></View>
|
||||
<View style={styles.stat}><Text style={[styles.statVal, { color: colors.primary }]}>--</Text><Text style={[styles.statLbl, { color: colors.textSecondary }]}>连续</Text></View>
|
||||
<View style={styles.stat}><Text style={[styles.statVal, { color: colors.primary }]}>--</Text><Text style={[styles.statLbl, { color: colors.textSecondary }]}>徽章</Text></View>
|
||||
</View>
|
||||
|
||||
<View style={styles.menu}>
|
||||
<TouchableOpacity style={styles.menuItem} onPress={() => navigation.navigate('Achievements')}><Text>成就徽章</Text><Text style={styles.arrow}>→</Text></TouchableOpacity>
|
||||
<View style={[styles.menu, { backgroundColor: colors.card }]}>
|
||||
<TouchableOpacity style={[styles.menuItem, { borderBottomColor: colors.border }]} onPress={() => navigation.navigate('Achievements')}>
|
||||
<Text style={{ color: colors.text }}>成就徽章</Text><Text style={[styles.arrow, { color: colors.textSecondary }]}>→</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={[styles.menuItem, { borderBottomColor: colors.border }]}>
|
||||
<Text style={{ color: colors.text }}>深色模式</Text>
|
||||
<Switch value={isDark} onValueChange={toggle} trackColor={{ false: colors.border, true: colors.primary }} thumbColor="white" />
|
||||
</View>
|
||||
<TouchableOpacity style={[styles.menuItem, { borderBottomWidth: 0 }]} onPress={handleLogout}>
|
||||
<Text style={{ color: '#FF3B30' }}>退出登录</Text><Text style={styles.arrow}>→</Text>
|
||||
<Text style={{ color: colors.error }}>退出登录</Text><Text style={[styles.arrow, { color: colors.textSecondary }]}>→</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
@@ -41,15 +49,15 @@ export default function ProfileScreen() {
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
profileCard: { backgroundColor: 'white', borderRadius: 12, margin: 12, padding: 32, alignItems: 'center', shadowOpacity: 0.04, elevation: 1 },
|
||||
profileCard: { borderRadius: 12, margin: 12, padding: 32, alignItems: 'center', shadowOpacity: 0.04, elevation: 1 },
|
||||
avatar: { width: 72, height: 72, borderRadius: 36, backgroundColor: '#FF6B35', justifyContent: 'center', alignItems: 'center', marginBottom: 12 },
|
||||
name: { fontSize: 22, fontWeight: '700' },
|
||||
bio: { fontSize: 14, color: '#86868B', marginTop: 4 },
|
||||
statsRow: { flexDirection: 'row', justifyContent: 'space-around', backgroundColor: 'white', borderRadius: 12, margin: 8, padding: 16, shadowOpacity: 0.04, elevation: 1 },
|
||||
bio: { fontSize: 14, marginTop: 4 },
|
||||
statsRow: { flexDirection: 'row', justifyContent: 'space-around', borderRadius: 12, margin: 8, padding: 16, shadowOpacity: 0.04, elevation: 1 },
|
||||
stat: { alignItems: 'center' },
|
||||
statVal: { fontSize: 24, fontWeight: '700', color: '#FF6B35' },
|
||||
statLbl: { fontSize: 13, color: '#86868B' },
|
||||
menu: { backgroundColor: 'white', borderRadius: 12, margin: 8, overflow: 'hidden', shadowOpacity: 0.04, elevation: 1 },
|
||||
menuItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 14, borderBottomWidth: 0.5, borderBottomColor: '#E5E5EA' },
|
||||
arrow: { fontSize: 15, color: '#86868B' },
|
||||
statVal: { fontSize: 24, fontWeight: '700' },
|
||||
statLbl: { fontSize: 13 },
|
||||
menu: { borderRadius: 12, margin: 8, overflow: 'hidden', shadowOpacity: 0.04, elevation: 1 },
|
||||
menuItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 14, borderBottomWidth: 0.5 },
|
||||
arrow: { fontSize: 15 },
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user