From cedb8284babc56beec158a4a0849840f5d9e2c72 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Wed, 25 Mar 2026 01:29:41 +0800 Subject: [PATCH] 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. --- app/(app)/(tabs)/_layout.tsx | 22 +- app/(app)/(tabs)/apps/_layout.tsx | 5 + app/(app)/(tabs)/apps/index.tsx | 5 + .../(tabs)/{ => apps}/schedule/_layout.tsx | 0 .../(tabs)/{ => apps}/schedule/course.tsx | 2 +- .../(tabs)/{ => apps}/schedule/index.tsx | 2 +- src/app-navigation/AppDesktopShell.tsx | 6 +- src/components/business/CommentItem.tsx | 108 ++++---- src/components/business/PostCard/PostCard.tsx | 55 ++++ src/navigation/hrefs.ts | 8 +- src/screens/apps/AppsScreen.tsx | 248 ++++++++++++++++++ src/screens/apps/index.ts | 2 + src/screens/home/HomeScreen.tsx | 36 ++- src/screens/home/PostDetailScreen.tsx | 25 +- src/screens/schedule/ScheduleScreen.tsx | 17 ++ src/types/dto.ts | 8 + 16 files changed, 463 insertions(+), 86 deletions(-) create mode 100644 app/(app)/(tabs)/apps/_layout.tsx create mode 100644 app/(app)/(tabs)/apps/index.tsx rename app/(app)/(tabs)/{ => apps}/schedule/_layout.tsx (100%) rename app/(app)/(tabs)/{ => apps}/schedule/course.tsx (52%) rename app/(app)/(tabs)/{ => apps}/schedule/index.tsx (51%) create mode 100644 src/screens/apps/AppsScreen.tsx create mode 100644 src/screens/apps/index.ts diff --git a/app/(app)/(tabs)/_layout.tsx b/app/(app)/(tabs)/_layout.tsx index 758445d..79cb809 100644 --- a/app/(app)/(tabs)/_layout.tsx +++ b/app/(app)/(tabs)/_layout.tsx @@ -61,15 +61,6 @@ export default function TabsLayout() { ), }} /> - ( - - ), - }} - /> + ( + + ), + }} + /> ; +} diff --git a/app/(app)/(tabs)/apps/index.tsx b/app/(app)/(tabs)/apps/index.tsx new file mode 100644 index 0000000..8259a61 --- /dev/null +++ b/app/(app)/(tabs)/apps/index.tsx @@ -0,0 +1,5 @@ +import { AppsScreen } from '../../../../src/screens/apps'; + +export default function AppsRoute() { + return ; +} diff --git a/app/(app)/(tabs)/schedule/_layout.tsx b/app/(app)/(tabs)/apps/schedule/_layout.tsx similarity index 100% rename from app/(app)/(tabs)/schedule/_layout.tsx rename to app/(app)/(tabs)/apps/schedule/_layout.tsx diff --git a/app/(app)/(tabs)/schedule/course.tsx b/app/(app)/(tabs)/apps/schedule/course.tsx similarity index 52% rename from app/(app)/(tabs)/schedule/course.tsx rename to app/(app)/(tabs)/apps/schedule/course.tsx index a03e316..d290558 100644 --- a/app/(app)/(tabs)/schedule/course.tsx +++ b/app/(app)/(tabs)/apps/schedule/course.tsx @@ -1,4 +1,4 @@ -import { CourseDetailScreen } from '../../../../src/screens/schedule'; +import { CourseDetailScreen } from '../../../../../src/screens/schedule'; export default function CourseDetailRoute() { return ; diff --git a/app/(app)/(tabs)/schedule/index.tsx b/app/(app)/(tabs)/apps/schedule/index.tsx similarity index 51% rename from app/(app)/(tabs)/schedule/index.tsx rename to app/(app)/(tabs)/apps/schedule/index.tsx index 120f50f..bf05928 100644 --- a/app/(app)/(tabs)/schedule/index.tsx +++ b/app/(app)/(tabs)/apps/schedule/index.tsx @@ -1,4 +1,4 @@ -import { ScheduleScreen } from '../../../../src/screens/schedule'; +import { ScheduleScreen } from '../../../../../src/screens/schedule'; export default function ScheduleRoute() { return ; diff --git a/src/app-navigation/AppDesktopShell.tsx b/src/app-navigation/AppDesktopShell.tsx index a807f98..8b7fff2 100644 --- a/src/app-navigation/AppDesktopShell.tsx +++ b/src/app-navigation/AppDesktopShell.tsx @@ -15,7 +15,7 @@ import { colors, shadows } from '../theme'; import { useTotalUnreadCount } from '../stores'; import { AppRouteStack } from './AppRouteStack'; -type TabName = 'HomeTab' | 'MessageTab' | 'ScheduleTab' | 'ProfileTab'; +type TabName = 'HomeTab' | 'MessageTab' | 'AppsTab' | 'ProfileTab'; const SIDEBAR_WIDTH_DESKTOP = 240; const SIDEBAR_WIDTH_TABLET = 200; @@ -24,13 +24,13 @@ const SIDEBAR_COLLAPSED_WIDTH = 72; const NAV_ITEMS: { name: TabName; label: string; href: string; icon: string; iconOutline: string }[] = [ { name: 'HomeTab', label: '首页', href: '/home', icon: 'home', iconOutline: 'home-outline' }, { name: 'MessageTab', label: '消息', href: '/messages', icon: 'message-text', iconOutline: 'message-text-outline' }, - { name: 'ScheduleTab', label: '课表', href: '/schedule', icon: 'calendar-today', iconOutline: 'calendar-today' }, + { name: 'AppsTab', label: '应用', href: '/apps', icon: 'view-grid', iconOutline: 'view-grid-outline' }, { name: 'ProfileTab', label: '我的', href: '/profile', icon: 'account', iconOutline: 'account-outline' }, ]; function pathToTab(pathname: string): TabName { if (pathname.startsWith('/messages')) return 'MessageTab'; - if (pathname.startsWith('/schedule')) return 'ScheduleTab'; + if (pathname.startsWith('/apps')) return 'AppsTab'; if (pathname.startsWith('/profile')) return 'ProfileTab'; if (pathname.startsWith('/home')) return 'HomeTab'; return 'HomeTab'; diff --git a/src/components/business/CommentItem.tsx b/src/components/business/CommentItem.tsx index c42ac2b..555605c 100644 --- a/src/components/business/CommentItem.tsx +++ b/src/components/business/CommentItem.tsx @@ -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 = ({ }; return ( - - - {getFloorText(floorNumber)} - - + + {getFloorText(floorNumber)} + ); }; @@ -276,8 +272,9 @@ const CommentItem: React.FC = ({ return ( - {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 = ({ return ( onReplyPress?.(reply)} activeOpacity={0.7} > @@ -320,7 +317,7 @@ const CommentItem: React.FC = ({ {/* 显示回复内容(如果有文字) */} {reply.content ? ( - + {reply.content} ) : null} @@ -375,11 +372,11 @@ const CommentItem: React.FC = ({ return ( - {/* 用户头像 */} - + {/* 用户头像 - 略大便于点击,与正文左对齐 */} + @@ -389,12 +386,13 @@ const CommentItem: React.FC = ({ {/* 用户信息行 - QQ频道风格 */} - - - {comment.author?.nickname} + + + {comment.author?.nickname || '用户'} {renderBadges()} + · {formatTime(comment.created_at || '')} @@ -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, diff --git a/src/components/business/PostCard/PostCard.tsx b/src/components/business/PostCard/PostCard.tsx index ec77ca5..97ca349 100644 --- a/src/components/business/PostCard/PostCard.tsx +++ b/src/components/business/PostCard/PostCard.tsx @@ -63,6 +63,11 @@ function topCommentSignature(tc: PostCardProps['post']['top_comment']): string { return [tc.id, tc.content ?? '', tc.author?.id ?? ''].join('\u001f'); } +function channelSignature(ch: PostCardProps['post']['channel']): string { + if (!ch) return ''; + return [ch.id, ch.name ?? ''].join('\u001f'); +} + function featuresComparable(f: PostCardProps['features']): string { if (f == null) return ''; if (typeof f === 'string') return f; @@ -103,6 +108,7 @@ function arePostCardPropsEqual(prev: PostCardProps, next: PostCardProps): boolea if (authorSignature(a.author) !== authorSignature(b.author)) return false; if (imagesSignature(a.images) !== imagesSignature(b.images)) return false; if (topCommentSignature(a.top_comment) !== topCommentSignature(b.top_comment)) return false; + if (channelSignature(a.channel) !== channelSignature(b.channel)) return false; return true; } @@ -302,6 +308,15 @@ const PostCardInner: React.FC = (normalizedProps) => { )} + {!!post.channel?.name && ( + + + + {post.channel.name} + + + )} + @@ -331,6 +346,14 @@ const PostCardInner: React.FC = (normalizedProps) => { {!showGrid && ( + {!!post.channel?.name && ( + + + + {post.channel.name} + + + )} {post.is_pinned && ( @@ -459,6 +482,23 @@ const styles = StyleSheet.create({ alignItems: 'center', gap: 6, marginTop: 2, + flexWrap: 'wrap', + }, + channelTag: { + flexDirection: 'row', + alignItems: 'center', + maxWidth: 140, + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: borderRadius.sm, + backgroundColor: `${colors.primary.main}12`, + gap: 4, + }, + channelTagText: { + fontSize: fontSizes.xs, + color: colors.primary.main, + fontWeight: '600', + flexShrink: 1, }, pinnedTag: { flexDirection: 'row', @@ -606,6 +646,21 @@ const styles = StyleSheet.create({ paddingTop: 8, minHeight: 44, }, + gridChannelRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + paddingHorizontal: 8, + paddingTop: 4, + paddingBottom: 2, + }, + gridChannelText: { + fontSize: fontSizes.xs, + color: colors.text.secondary, + fontWeight: '600', + flex: 1, + minWidth: 0, + }, gridFooter: { flexDirection: 'row', alignItems: 'center', diff --git a/src/navigation/hrefs.ts b/src/navigation/hrefs.ts index 55b5000..93e2c82 100644 --- a/src/navigation/hrefs.ts +++ b/src/navigation/hrefs.ts @@ -29,12 +29,16 @@ export function hrefNotifications(): string { return '/messages/notifications'; } +export function hrefApps(): string { + return '/apps'; +} + export function hrefSchedule(): string { - return '/schedule'; + return '/apps/schedule'; } export function hrefScheduleCourse(courseId: string): string { - return `/schedule/course?courseId=${encodeURIComponent(courseId)}`; + return `/apps/schedule/course?courseId=${encodeURIComponent(courseId)}`; } export function hrefProfileSettings(): string { diff --git a/src/screens/apps/AppsScreen.tsx b/src/screens/apps/AppsScreen.tsx new file mode 100644 index 0000000..e8a59ac --- /dev/null +++ b/src/screens/apps/AppsScreen.tsx @@ -0,0 +1,248 @@ +/** + * 应用中心:聚合站内轻应用入口 + */ + +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['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 ( + + + + + + + + + + + 应用 + 学习与生活常用工具,持续扩充中 + + + 全部应用 + setGridWidth(e.nativeEvent.layout.width)} + > + {APP_ENTRIES.map(item => ( + onOpenApp(item.href)} + activeOpacity={0.88} + > + + + + + + + {item.title} + + + {item.subtitle} + + + 打开 + + + + ))} + + + 更多应用敬请期待 + + + + ); +}; + +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, + }, +}); diff --git a/src/screens/apps/index.ts b/src/screens/apps/index.ts new file mode 100644 index 0000000..4aa5ef8 --- /dev/null +++ b/src/screens/apps/index.ts @@ -0,0 +1,2 @@ +export { AppsScreen } from './AppsScreen'; +export { default } from './AppsScreen'; diff --git a/src/screens/home/HomeScreen.tsx b/src/screens/home/HomeScreen.tsx index da6795c..aa5701b 100644 --- a/src/screens/home/HomeScreen.tsx +++ b/src/screens/home/HomeScreen.tsx @@ -4,7 +4,7 @@ * 支持列表和多列网格模式(响应式布局) */ -import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import React, { useState, useEffect, useLayoutEffect, useCallback, useMemo, useRef } from 'react'; import { View, FlatList, @@ -15,6 +15,7 @@ import { StatusBar, TouchableOpacity, NativeSyntheticEvent, + NativeScrollEvent, Alert, Clipboard, Modal, @@ -92,6 +93,24 @@ export const HomeScreen: React.FC = () => { const isLoadingMoreRef = useRef(false); + /** 横向胶囊条滚动位置:切换频道刷新列表时不重置 */ + const capsuleHScrollRef = useRef(null); + const capsuleScrollXRef = useRef(0); + + const restoreCapsuleStripScroll = useCallback(() => { + const x = capsuleScrollXRef.current; + if (x <= 0) return; + const scroll = () => capsuleHScrollRef.current?.scrollTo({ x, animated: false }); + scroll(); + requestAnimationFrame(() => { + requestAnimationFrame(scroll); + }); + }, []); + + const onCapsuleHorizontalScroll = useCallback((e: NativeSyntheticEvent) => { + capsuleScrollXRef.current = e.nativeEvent.contentOffset.x; + }, []); + // 构建一个以 postId 为 key 的 map,用于快速查找 const postsMap = useMemo(() => { const map = new Map(); @@ -114,6 +133,11 @@ export const HomeScreen: React.FC = () => { const isLatestTab = activeIndex === 1; const currentChannelId = isLatestTab && activeCapsuleId ? activeCapsuleId : undefined; + useLayoutEffect(() => { + if (!isLatestTab) return; + restoreCapsuleStripScroll(); + }, [isLatestTab, activeCapsuleId, latestCapsules.length, restoreCapsuleStripScroll]); + // 使用差异更新 Hook 获取帖子列表 const listKey = useMemo( () => `home_${getPostType()}_${currentChannelId || 'all'}`, @@ -453,7 +477,15 @@ export const HomeScreen: React.FC = () => { return ( - + {latestCapsules.map((item) => { const isActive = item.id === activeCapsuleId; return ( diff --git a/src/screens/home/PostDetailScreen.tsx b/src/screens/home/PostDetailScreen.tsx index 5ae432b..2db3174 100644 --- a/src/screens/home/PostDetailScreen.tsx +++ b/src/screens/home/PostDetailScreen.tsx @@ -2054,30 +2054,23 @@ const styles = StyleSheet.create({ paddingVertical: spacing.xs, borderRadius: borderRadius.md, }, - // 空评论状态样式 + // 空评论状态样式(与平铺评论区一致,无卡片气泡) emptyCommentsContainer: { alignItems: 'center', justifyContent: 'center', - marginHorizontal: spacing.lg, - marginTop: spacing.lg, - marginBottom: spacing.md, - paddingVertical: spacing.xl + spacing.md, - paddingHorizontal: spacing.lg, - borderRadius: borderRadius.lg, - backgroundColor: colors.background.paper, - borderWidth: StyleSheet.hairlineWidth, - borderColor: colors.divider, + marginHorizontal: spacing.md, + marginTop: spacing.md, + marginBottom: spacing.lg, + paddingVertical: spacing.xl, + paddingHorizontal: spacing.md, }, emptyCommentsIconWrapper: { - width: 44, - height: 44, - borderRadius: 22, - backgroundColor: colors.background.default, - borderWidth: StyleSheet.hairlineWidth, - borderColor: colors.divider, + width: 40, + height: 40, alignItems: 'center', justifyContent: 'center', marginBottom: spacing.sm, + opacity: 0.85, }, emptyCommentsTitle: { fontSize: fontSizes.md, diff --git a/src/screens/schedule/ScheduleScreen.tsx b/src/screens/schedule/ScheduleScreen.tsx index 50d3d6b..ea71683 100644 --- a/src/screens/schedule/ScheduleScreen.tsx +++ b/src/screens/schedule/ScheduleScreen.tsx @@ -379,6 +379,16 @@ export const ScheduleScreen: React.FC = () => { // 渲染周选择器 const renderWeekSelector = () => ( + {router.canGoBack() ? ( + router.back()} + accessibilityRole="button" + accessibilityLabel="返回" + > + + + ) : null} setIsSettingsModalVisible(true)} @@ -1066,6 +1076,13 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', }, + weekBarIconButton: { + width: 40, + height: 40, + justifyContent: 'center', + alignItems: 'center', + marginLeft: spacing.sm, + }, settingsButton: { width: 40, height: 40, diff --git a/src/types/dto.ts b/src/types/dto.ts index 0cfb1a1..65ff8fc 100644 --- a/src/types/dto.ts +++ b/src/types/dto.ts @@ -57,6 +57,12 @@ export interface PostImageDTO { height: number; } +/** 帖子所属频道(列表卡片展示) */ +export interface PostChannelBriefDTO { + id: string; + name: string; +} + export interface PostDTO { id: string; user_id: string; @@ -81,6 +87,8 @@ export interface PostDTO { is_favorited: boolean; // 额外字段 channel_id?: string; + /** 频道摘要(与 channel_id 对应,服务端批量填充) */ + channel?: PostChannelBriefDTO | null; top_comment?: CommentDTO | null; }