✨ feat: 应用界面重构 - 从模板中心到AI内容生成平台
- 重构首页:从模板列表展示改为AI功能展示界面 - 更新底部导航:将explore改为content,index改为home - 新增多个页面:积分、兑换、历史记录、结果展示、设置等 - 优化UI组件:新增通用按钮、分类标签、加载状态等组件 - 清理冗余文件:删除旧的认证文档和积分详情页面 - 更新主题配置:调整配色方案和视觉风格 - 修复导入路径:优化组件导入结构 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
387
app/exchange.tsx
Normal file
387
app/exchange.tsx
Normal file
@@ -0,0 +1,387 @@
|
||||
import Ionicons from '@expo/vector-icons/Ionicons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
type PurchaseTab = 'subscription' | 'pack';
|
||||
|
||||
type PointsBundle = {
|
||||
id: string;
|
||||
points: number;
|
||||
price: number;
|
||||
};
|
||||
|
||||
const screenPalette = {
|
||||
background: '#050505',
|
||||
surface: '#101010',
|
||||
surfaceRaised: '#1A1B1E',
|
||||
primaryText: '#F5F5F5',
|
||||
secondaryText: '#7F7F7F',
|
||||
mutedText: '#4C4C4C',
|
||||
accent: '#FEB840',
|
||||
energyHalo: 'rgba(254, 184, 64, 0.22)',
|
||||
divider: '#1C1C1C',
|
||||
button: '#D1FE17',
|
||||
buttonText: '#101010',
|
||||
};
|
||||
|
||||
const CURRENT_POINTS = 60;
|
||||
|
||||
const SUBSCRIPTION_TAGLINE = 'No active subscription plans';
|
||||
|
||||
const TABS: { key: PurchaseTab; label: string }[] = [
|
||||
{ key: 'subscription', label: 'Subscription' },
|
||||
{ key: 'pack', label: 'points pack' },
|
||||
];
|
||||
|
||||
const POINT_BUNDLES: PointsBundle[] = [
|
||||
{ id: 'bundle-500', points: 500, price: 35 },
|
||||
{ id: 'bundle-2000', points: 2000, price: 140 },
|
||||
{ id: 'bundle-5000', points: 5000, price: 350 },
|
||||
{ id: 'bundle-10000', points: 10000, price: 700 },
|
||||
];
|
||||
|
||||
export default function PointsExchangeScreen() {
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const [activeTab, setActiveTab] = useState<PurchaseTab>('pack');
|
||||
const [selectedBundleId, setSelectedBundleId] = useState<string | null>(null);
|
||||
|
||||
const visibleBundles = useMemo(
|
||||
() => (activeTab === 'pack' ? POINT_BUNDLES : []),
|
||||
[activeTab],
|
||||
);
|
||||
|
||||
const changeTab = useCallback((nextTab: PurchaseTab) => {
|
||||
setActiveTab(nextTab);
|
||||
if (nextTab !== 'pack') {
|
||||
setSelectedBundleId(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
router.back();
|
||||
}, [router]);
|
||||
|
||||
const openPointsDetails = useCallback(() => {
|
||||
router.push('/points-details');
|
||||
}, [router]);
|
||||
|
||||
const handleBundleSelect = useCallback((bundleId: string) => {
|
||||
setSelectedBundleId(bundleId);
|
||||
}, []);
|
||||
|
||||
const handlePurchase = useCallback(() => {
|
||||
const bundle = POINT_BUNDLES.find(item => item.id === selectedBundleId);
|
||||
|
||||
if (!bundle) {
|
||||
Alert.alert('Select a bundle', 'Please pick the bundle you wish to purchase.');
|
||||
return;
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
'Confirm purchase',
|
||||
`You are purchasing ${bundle.points} points for ¥ ${bundle.price}.`,
|
||||
);
|
||||
}, [selectedBundleId]);
|
||||
|
||||
return (
|
||||
<View style={[styles.screen, { paddingTop: insets.top + 12 }]}>
|
||||
|
||||
<View style={styles.headerRow}>
|
||||
<TouchableOpacity
|
||||
style={styles.iconButton}
|
||||
accessibilityRole="button"
|
||||
onPress={handleClose}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons name="close" size={22} color={screenPalette.primaryText} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.secondaryPill}
|
||||
accessibilityRole="button"
|
||||
onPress={openPointsDetails}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={styles.secondaryPillLabel}>Points Details</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
style={styles.content}
|
||||
contentContainerStyle={[
|
||||
styles.contentContainer,
|
||||
{ paddingBottom: Math.max(insets.bottom + 96, 160) },
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.balanceCluster}>
|
||||
<View style={styles.energyOrb}>
|
||||
<Ionicons name="flash" size={22} color={screenPalette.accent} />
|
||||
</View>
|
||||
<Text style={styles.balanceValue}>{CURRENT_POINTS}</Text>
|
||||
<Text style={styles.balanceSubtitle}>{SUBSCRIPTION_TAGLINE}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.tabBar}>
|
||||
{TABS.map(tab => {
|
||||
const isActive = tab.key === activeTab;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={tab.key}
|
||||
style={styles.tabButton}
|
||||
onPress={() => changeTab(tab.key)}
|
||||
accessibilityRole="tab"
|
||||
accessibilityState={{ selected: isActive }}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={[styles.tabLabel, isActive && styles.tabLabelActive]}>
|
||||
{tab.label}
|
||||
</Text>
|
||||
<View
|
||||
style={[styles.tabIndicator, isActive && styles.tabIndicatorActive]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
<View style={styles.tabDivider} />
|
||||
|
||||
{activeTab === 'pack' ? (
|
||||
<View style={styles.packGrid}>
|
||||
{visibleBundles.map(bundle => {
|
||||
const isSelected = bundle.id === selectedBundleId;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={bundle.id}
|
||||
style={[
|
||||
styles.packCard,
|
||||
isSelected && styles.packCardSelected,
|
||||
]}
|
||||
onPress={() => handleBundleSelect(bundle.id)}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ selected: isSelected }}
|
||||
activeOpacity={0.88}
|
||||
>
|
||||
<View style={styles.packHeader}>
|
||||
<Ionicons name="flash" size={18} color={screenPalette.accent} />
|
||||
<Text style={styles.packPoints}>{bundle.points}</Text>
|
||||
</View>
|
||||
<Text style={styles.packPrice}>¥ {bundle.price}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.subscriptionEmpty}>
|
||||
<Text style={styles.emptyTitle}>Subscription</Text>
|
||||
<Text style={styles.emptySubtitle}>
|
||||
No subscription tiers are available at the moment.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
<View style={[styles.bottomBar, { paddingBottom: Math.max(insets.bottom, 16) }]}>
|
||||
<TouchableOpacity
|
||||
style={styles.primaryButton}
|
||||
onPress={handlePurchase}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={styles.primaryButtonLabel}>Purchase points</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
screen: {
|
||||
flex: 1,
|
||||
backgroundColor: screenPalette.background,
|
||||
},
|
||||
headerRow: {
|
||||
paddingHorizontal: 24,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
iconButton: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
secondaryPill: {
|
||||
paddingHorizontal: 16,
|
||||
height: 34,
|
||||
borderRadius: 17,
|
||||
backgroundColor: screenPalette.surface,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: screenPalette.divider,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
secondaryPillLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: screenPalette.primaryText,
|
||||
letterSpacing: 0.2,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
balanceCluster: {
|
||||
alignItems: 'center',
|
||||
marginTop: 32,
|
||||
marginBottom: 36,
|
||||
gap: 10,
|
||||
},
|
||||
energyOrb: {
|
||||
width: 58,
|
||||
height: 58,
|
||||
borderRadius: 29,
|
||||
backgroundColor: screenPalette.energyHalo,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
shadowColor: screenPalette.accent,
|
||||
shadowOpacity: 0.28,
|
||||
shadowRadius: 14,
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
elevation: 6,
|
||||
},
|
||||
balanceValue: {
|
||||
fontSize: 44,
|
||||
fontWeight: '700',
|
||||
color: screenPalette.primaryText,
|
||||
letterSpacing: 0.5,
|
||||
fontVariant: ['tabular-nums'],
|
||||
},
|
||||
balanceSubtitle: {
|
||||
fontSize: 13,
|
||||
color: screenPalette.secondaryText,
|
||||
letterSpacing: 0.2,
|
||||
},
|
||||
tabBar: {
|
||||
flexDirection: 'row',
|
||||
gap: 32,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
tabButton: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
paddingBottom: 12,
|
||||
},
|
||||
tabLabel: {
|
||||
fontSize: 14,
|
||||
color: screenPalette.mutedText,
|
||||
fontWeight: '500',
|
||||
letterSpacing: 0.2,
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
tabLabelActive: {
|
||||
color: screenPalette.primaryText,
|
||||
},
|
||||
tabIndicator: {
|
||||
marginTop: 10,
|
||||
height: 2,
|
||||
width: '100%',
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 1,
|
||||
},
|
||||
tabIndicatorActive: {
|
||||
backgroundColor: screenPalette.accent,
|
||||
},
|
||||
tabDivider: {
|
||||
marginTop: 12,
|
||||
height: StyleSheet.hairlineWidth,
|
||||
backgroundColor: screenPalette.divider,
|
||||
},
|
||||
packGrid: {
|
||||
marginTop: 28,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 18,
|
||||
},
|
||||
packCard: {
|
||||
width: '47%',
|
||||
backgroundColor: screenPalette.surfaceRaised,
|
||||
borderRadius: 24,
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 24,
|
||||
justifyContent: 'space-between',
|
||||
borderWidth: 1,
|
||||
borderColor: screenPalette.surfaceRaised,
|
||||
},
|
||||
packCardSelected: {
|
||||
borderColor: screenPalette.accent,
|
||||
},
|
||||
packHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
packPoints: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: screenPalette.primaryText,
|
||||
fontVariant: ['tabular-nums'],
|
||||
},
|
||||
packPrice: {
|
||||
marginTop: 18,
|
||||
fontSize: 14,
|
||||
color: screenPalette.secondaryText,
|
||||
},
|
||||
subscriptionEmpty: {
|
||||
marginTop: 64,
|
||||
backgroundColor: screenPalette.surface,
|
||||
borderRadius: 22,
|
||||
paddingVertical: 40,
|
||||
paddingHorizontal: 32,
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
emptyTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: screenPalette.primaryText,
|
||||
},
|
||||
emptySubtitle: {
|
||||
fontSize: 13,
|
||||
color: screenPalette.secondaryText,
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
},
|
||||
bottomBar: {
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
primaryButton: {
|
||||
height: 54,
|
||||
borderRadius: 27,
|
||||
backgroundColor: screenPalette.button,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
primaryButtonLabel: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
color: screenPalette.buttonText,
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user