249 lines
7.0 KiB
TypeScript
249 lines
7.0 KiB
TypeScript
|
|
/**
|
|||
|
|
* 应用中心:聚合站内轻应用入口
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import React, { useCallback, useMemo, useState } from 'react';
|
|||
|
|
import { View, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
|
|||
|
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
|||
|
|
import { StatusBar } from 'expo-status-bar';
|
|||
|
|
import { useRouter } from 'expo-router';
|
|||
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|||
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|||
|
|
|
|||
|
|
import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
|
|||
|
|
import * as hrefs from '../../navigation/hrefs';
|
|||
|
|
import { Text, ResponsiveContainer } from '../../components/common';
|
|||
|
|
import { useResponsive } from '../../hooks/useResponsive';
|
|||
|
|
type AppItem = {
|
|||
|
|
id: string;
|
|||
|
|
title: string;
|
|||
|
|
subtitle: string;
|
|||
|
|
icon: React.ComponentProps<typeof MaterialCommunityIcons>['name'];
|
|||
|
|
gradient: readonly [string, string, ...string[]];
|
|||
|
|
href: string;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const APP_ENTRIES: AppItem[] = [
|
|||
|
|
{
|
|||
|
|
id: 'schedule',
|
|||
|
|
title: '课表',
|
|||
|
|
subtitle: '周视图 · 教务同步',
|
|||
|
|
icon: 'calendar-week',
|
|||
|
|
gradient: [colors.primary.main, colors.primary.light],
|
|||
|
|
href: hrefs.hrefSchedule(),
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
export const AppsScreen: React.FC = () => {
|
|||
|
|
const router = useRouter();
|
|||
|
|
const insets = useSafeAreaInsets();
|
|||
|
|
const { isMobile, isTablet, width } = useResponsive();
|
|||
|
|
|
|||
|
|
const columns = useMemo(() => {
|
|||
|
|
if (width >= 900) return 4;
|
|||
|
|
if (isTablet || width >= 600) return 3;
|
|||
|
|
return 2;
|
|||
|
|
}, [isTablet, width]);
|
|||
|
|
|
|||
|
|
const gap = spacing.md;
|
|||
|
|
const [gridWidth, setGridWidth] = useState(0);
|
|||
|
|
const cardWidth =
|
|||
|
|
gridWidth > 0 ? (gridWidth - gap * (columns - 1)) / columns : undefined;
|
|||
|
|
|
|||
|
|
const scrollBottomInset = isMobile ? 88 + insets.bottom + spacing.md : spacing['3xl'];
|
|||
|
|
|
|||
|
|
const onOpenApp = useCallback(
|
|||
|
|
(href: string) => {
|
|||
|
|
router.push(href);
|
|||
|
|
},
|
|||
|
|
[router]
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<SafeAreaView style={styles.safe} edges={['top']}>
|
|||
|
|
<StatusBar style="dark" />
|
|||
|
|
<ScrollView
|
|||
|
|
style={styles.scroll}
|
|||
|
|
contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset }]}
|
|||
|
|
showsVerticalScrollIndicator={false}
|
|||
|
|
>
|
|||
|
|
<ResponsiveContainer maxWidth={960}>
|
|||
|
|
<LinearGradient
|
|||
|
|
colors={[`${colors.primary.main}18`, `${colors.primary.light}10`, 'transparent']}
|
|||
|
|
start={{ x: 0, y: 0 }}
|
|||
|
|
end={{ x: 1, y: 1 }}
|
|||
|
|
style={styles.hero}
|
|||
|
|
>
|
|||
|
|
<View style={styles.heroIconWrap}>
|
|||
|
|
<LinearGradient
|
|||
|
|
colors={[colors.primary.main, colors.primary.light]}
|
|||
|
|
style={styles.heroIconGradient}
|
|||
|
|
start={{ x: 0, y: 0 }}
|
|||
|
|
end={{ x: 1, y: 1 }}
|
|||
|
|
>
|
|||
|
|
<MaterialCommunityIcons name="apps" size={28} color={colors.primary.contrast} />
|
|||
|
|
</LinearGradient>
|
|||
|
|
</View>
|
|||
|
|
<Text style={styles.heroTitle}>应用</Text>
|
|||
|
|
<Text style={styles.heroSubtitle}>学习与生活常用工具,持续扩充中</Text>
|
|||
|
|
</LinearGradient>
|
|||
|
|
|
|||
|
|
<Text style={styles.sectionLabel}>全部应用</Text>
|
|||
|
|
<View
|
|||
|
|
style={[styles.grid, { gap }]}
|
|||
|
|
onLayout={e => setGridWidth(e.nativeEvent.layout.width)}
|
|||
|
|
>
|
|||
|
|
{APP_ENTRIES.map(item => (
|
|||
|
|
<TouchableOpacity
|
|||
|
|
key={item.id}
|
|||
|
|
style={[styles.card, cardWidth != null ? { width: cardWidth } : styles.cardFlex, shadows.md]}
|
|||
|
|
onPress={() => onOpenApp(item.href)}
|
|||
|
|
activeOpacity={0.88}
|
|||
|
|
>
|
|||
|
|
<LinearGradient
|
|||
|
|
colors={item.gradient}
|
|||
|
|
start={{ x: 0, y: 0 }}
|
|||
|
|
end={{ x: 1, y: 1 }}
|
|||
|
|
style={styles.cardIconRing}
|
|||
|
|
>
|
|||
|
|
<View style={styles.cardIconInner}>
|
|||
|
|
<MaterialCommunityIcons name={item.icon} size={26} color={colors.primary.main} />
|
|||
|
|
</View>
|
|||
|
|
</LinearGradient>
|
|||
|
|
<Text style={styles.cardTitle} numberOfLines={1}>
|
|||
|
|
{item.title}
|
|||
|
|
</Text>
|
|||
|
|
<Text style={styles.cardSubtitle} numberOfLines={2}>
|
|||
|
|
{item.subtitle}
|
|||
|
|
</Text>
|
|||
|
|
<View style={styles.cardFooter}>
|
|||
|
|
<Text style={styles.cardAction}>打开</Text>
|
|||
|
|
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.primary.main} />
|
|||
|
|
</View>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
))}
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
<Text style={styles.hint}>更多应用敬请期待</Text>
|
|||
|
|
</ResponsiveContainer>
|
|||
|
|
</ScrollView>
|
|||
|
|
</SafeAreaView>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default AppsScreen;
|
|||
|
|
|
|||
|
|
const styles = StyleSheet.create({
|
|||
|
|
safe: {
|
|||
|
|
flex: 1,
|
|||
|
|
backgroundColor: colors.background.default,
|
|||
|
|
},
|
|||
|
|
scroll: {
|
|||
|
|
flex: 1,
|
|||
|
|
},
|
|||
|
|
scrollContent: {
|
|||
|
|
paddingTop: spacing.md,
|
|||
|
|
paddingHorizontal: spacing.lg,
|
|||
|
|
},
|
|||
|
|
hero: {
|
|||
|
|
borderRadius: borderRadius['2xl'],
|
|||
|
|
paddingVertical: spacing['2xl'],
|
|||
|
|
paddingHorizontal: spacing.xl,
|
|||
|
|
marginBottom: spacing['2xl'],
|
|||
|
|
overflow: 'hidden',
|
|||
|
|
},
|
|||
|
|
heroIconWrap: {
|
|||
|
|
marginBottom: spacing.md,
|
|||
|
|
},
|
|||
|
|
heroIconGradient: {
|
|||
|
|
width: 52,
|
|||
|
|
height: 52,
|
|||
|
|
borderRadius: borderRadius.xl,
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
...shadows.md,
|
|||
|
|
},
|
|||
|
|
heroTitle: {
|
|||
|
|
fontSize: fontSizes['3xl'],
|
|||
|
|
fontWeight: '800',
|
|||
|
|
color: colors.text.primary,
|
|||
|
|
letterSpacing: -0.5,
|
|||
|
|
},
|
|||
|
|
heroSubtitle: {
|
|||
|
|
marginTop: spacing.xs,
|
|||
|
|
fontSize: fontSizes.sm,
|
|||
|
|
color: colors.text.secondary,
|
|||
|
|
lineHeight: 20,
|
|||
|
|
maxWidth: 320,
|
|||
|
|
},
|
|||
|
|
sectionLabel: {
|
|||
|
|
fontSize: fontSizes.xs,
|
|||
|
|
fontWeight: '700',
|
|||
|
|
color: colors.text.secondary,
|
|||
|
|
textTransform: 'uppercase',
|
|||
|
|
letterSpacing: 1.2,
|
|||
|
|
marginBottom: spacing.md,
|
|||
|
|
marginLeft: 2,
|
|||
|
|
},
|
|||
|
|
grid: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
flexWrap: 'wrap',
|
|||
|
|
width: '100%',
|
|||
|
|
},
|
|||
|
|
cardFlex: {
|
|||
|
|
flex: 1,
|
|||
|
|
minWidth: 140,
|
|||
|
|
maxWidth: '100%',
|
|||
|
|
},
|
|||
|
|
card: {
|
|||
|
|
backgroundColor: colors.background.paper,
|
|||
|
|
borderRadius: borderRadius.xl,
|
|||
|
|
padding: spacing.lg,
|
|||
|
|
borderWidth: 1,
|
|||
|
|
borderColor: `${colors.divider}99`,
|
|||
|
|
},
|
|||
|
|
cardIconRing: {
|
|||
|
|
width: 52,
|
|||
|
|
height: 52,
|
|||
|
|
borderRadius: borderRadius.lg,
|
|||
|
|
padding: 2,
|
|||
|
|
marginBottom: spacing.md,
|
|||
|
|
alignSelf: 'flex-start',
|
|||
|
|
},
|
|||
|
|
cardIconInner: {
|
|||
|
|
flex: 1,
|
|||
|
|
borderRadius: borderRadius.md,
|
|||
|
|
backgroundColor: colors.background.paper,
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
},
|
|||
|
|
cardTitle: {
|
|||
|
|
fontSize: fontSizes.lg,
|
|||
|
|
fontWeight: '700',
|
|||
|
|
color: colors.text.primary,
|
|||
|
|
},
|
|||
|
|
cardSubtitle: {
|
|||
|
|
marginTop: spacing.xs,
|
|||
|
|
fontSize: fontSizes.sm,
|
|||
|
|
color: colors.text.secondary,
|
|||
|
|
lineHeight: 18,
|
|||
|
|
minHeight: 36,
|
|||
|
|
},
|
|||
|
|
cardFooter: {
|
|||
|
|
flexDirection: 'row',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
marginTop: spacing.md,
|
|||
|
|
},
|
|||
|
|
cardAction: {
|
|||
|
|
fontSize: fontSizes.sm,
|
|||
|
|
fontWeight: '600',
|
|||
|
|
color: colors.primary.main,
|
|||
|
|
},
|
|||
|
|
hint: {
|
|||
|
|
marginTop: spacing['3xl'],
|
|||
|
|
textAlign: 'center',
|
|||
|
|
fontSize: fontSizes.sm,
|
|||
|
|
color: colors.text.hint,
|
|||
|
|
},
|
|||
|
|
});
|