feat(Apps): introduce Apps tab and related screens for enhanced navigation

- Added a new "Apps" tab in the TabsLayout, providing users with access to various applications.
- Created AppsScreen to display app entries, including a schedule feature with a calendar icon.
- Implemented routing for the schedule and course screens under the new Apps tab structure.
- Updated navigation hrefs to reflect the new Apps section, improving overall user experience.
- Refactored HomeScreen to manage bottom tab visibility based on scroll events, enhancing usability.
This commit is contained in:
lafay
2026-03-25 01:29:41 +08:00
parent c12b98e293
commit cedb8284ba
16 changed files with 463 additions and 86 deletions

View File

@@ -1,10 +1,10 @@
/**
* CommentItem 评论项组件 - QQ频道风格
* CommentItem 评论项组件 - 小红书 / 贴吧式平铺列表
* 支持嵌套回复显示、楼层号、身份标识、删除评论、图片显示
*/
import React, { useState } from 'react';
import { View, TouchableOpacity, StyleSheet, Alert, Dimensions } from 'react-native';
import { View, TouchableOpacity, StyleSheet, Alert } from 'react-native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { formatDistanceToNow } from 'date-fns';
import { zhCN } from 'date-fns/locale';
@@ -14,8 +14,6 @@ import Text from '../common/Text';
import Avatar from '../common/Avatar';
import { CompactImageGrid, ImageGridItem } from '../common';
const { width: screenWidth } = Dimensions.get('window');
interface CommentItemProps {
comment: Comment;
onUserPress: () => void;
@@ -181,11 +179,9 @@ const CommentItem: React.FC<CommentItemProps> = ({
};
return (
<View style={styles.floorTag}>
<Text variant="caption" color={colors.text.hint} style={styles.floorText}>
{getFloorText(floorNumber)}
</Text>
</View>
<Text variant="caption" color={colors.text.hint} style={styles.floorPlain}>
{getFloorText(floorNumber)}
</Text>
);
};
@@ -276,8 +272,9 @@ const CommentItem: React.FC<CommentItemProps> = ({
return (
<View style={styles.subRepliesContainer}>
{comment.replies.map((reply) => {
{comment.replies.map((reply, replyIndex) => {
const replyAuthorId = reply.author?.id || '';
const isLastReply = replyIndex === comment.replies!.length - 1;
// 根据 target_id 获取被回复的用户昵称
const targetId = reply.target_id;
const targetNickname = targetId ? getTargetUserNickname(targetId) : null;
@@ -287,7 +284,7 @@ const CommentItem: React.FC<CommentItemProps> = ({
return (
<TouchableOpacity
key={reply.id}
style={styles.subReplyItem}
style={[styles.subReplyItem, isLastReply && styles.subReplyItemLast]}
onPress={() => onReplyPress?.(reply)}
activeOpacity={0.7}
>
@@ -320,7 +317,7 @@ const CommentItem: React.FC<CommentItemProps> = ({
</View>
{/* 显示回复内容(如果有文字) */}
{reply.content ? (
<Text variant="caption" color={colors.text.secondary} numberOfLines={2}>
<Text variant="body" color={colors.text.primary} style={styles.subReplyBody}>
{reply.content}
</Text>
) : null}
@@ -375,11 +372,11 @@ const CommentItem: React.FC<CommentItemProps> = ({
return (
<View style={styles.container}>
{/* 用户头像 */}
<TouchableOpacity onPress={onUserPress}>
{/* 用户头像 - 略大便于点击,与正文左对齐 */}
<TouchableOpacity onPress={onUserPress} activeOpacity={0.7}>
<Avatar
source={comment.author?.avatar}
size={36}
size={40}
name={comment.author?.nickname}
/>
</TouchableOpacity>
@@ -389,12 +386,13 @@ const CommentItem: React.FC<CommentItemProps> = ({
{/* 用户信息行 - QQ频道风格 */}
<View style={styles.header}>
<View style={styles.userInfo}>
<TouchableOpacity onPress={onUserPress}>
<Text variant="body" style={styles.username}>
{comment.author?.nickname}
<TouchableOpacity onPress={onUserPress} activeOpacity={0.7}>
<Text variant="body" style={styles.username} numberOfLines={1}>
{comment.author?.nickname || '用户'}
</Text>
</TouchableOpacity>
{renderBadges()}
<Text style={styles.metaDot}>·</Text>
<Text variant="caption" color={colors.text.hint} style={styles.timeText}>
{formatTime(comment.created_at || '')}
</Text>
@@ -477,31 +475,35 @@ const styles = StyleSheet.create({
container: {
flexDirection: 'row',
paddingTop: spacing.md,
paddingBottom: spacing.xs,
paddingHorizontal: spacing.lg,
backgroundColor: 'transparent',
paddingBottom: spacing.md,
paddingHorizontal: spacing.md,
backgroundColor: colors.background.paper,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.divider,
},
content: {
flex: 1,
marginLeft: spacing.sm,
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.divider,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
minWidth: 0,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: spacing.sm,
alignItems: 'center',
marginBottom: spacing.xs,
gap: spacing.xs,
},
userInfo: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
flex: 1,
minWidth: 0,
},
metaDot: {
fontSize: fontSizes.xs,
color: colors.text.hint,
marginHorizontal: 2,
},
username: {
fontWeight: '600',
@@ -537,42 +539,35 @@ const styles = StyleSheet.create({
},
timeText: {
fontSize: fontSizes.xs,
flexShrink: 0,
},
floorTag: {
backgroundColor: colors.background.default,
paddingHorizontal: spacing.xs,
paddingVertical: 2,
borderRadius: 999,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.divider,
},
floorText: {
floorPlain: {
fontSize: fontSizes.xs,
flexShrink: 0,
},
replyReference: {
marginBottom: spacing.xs,
},
commentContent: {
marginBottom: spacing.sm,
marginBottom: spacing.xs,
},
text: {
lineHeight: 20,
lineHeight: 22,
fontSize: fontSizes.md,
color: colors.text.primary,
},
actions: {
flexDirection: 'row',
alignItems: 'center',
marginTop: spacing.xs,
flexWrap: 'wrap',
},
actionButton: {
flexDirection: 'row',
alignItems: 'center',
marginRight: spacing.sm,
paddingHorizontal: spacing.sm,
paddingVertical: 5,
borderRadius: 999,
backgroundColor: colors.background.default,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.divider,
marginRight: spacing.lg,
paddingVertical: 4,
paddingRight: spacing.xs,
},
actionText: {
marginLeft: 4,
@@ -580,16 +575,25 @@ const styles = StyleSheet.create({
},
subRepliesContainer: {
marginTop: spacing.sm,
marginLeft: 0,
paddingLeft: spacing.sm,
paddingVertical: spacing.xs,
borderLeftWidth: 2,
borderLeftColor: colors.divider,
backgroundColor: colors.background.default,
borderRadius: borderRadius.lg,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.divider,
padding: spacing.sm,
},
subReplyItem: {
flexDirection: 'row',
alignItems: 'flex-start',
marginBottom: spacing.sm,
marginBottom: spacing.md,
},
subReplyItemLast: {
marginBottom: 0,
},
subReplyBody: {
fontSize: fontSizes.sm,
lineHeight: 20,
marginTop: 2,
},
subReplyContent: {
flex: 1,