diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/frontend.iml b/.idea/frontend.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/frontend.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..6752ffb --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,448 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..ade1ecd --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f3d93d7 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/components/business/SystemMessageItem.tsx b/src/components/business/SystemMessageItem.tsx index d8f0b9b..64894ec 100644 --- a/src/components/business/SystemMessageItem.tsx +++ b/src/components/business/SystemMessageItem.tsx @@ -13,7 +13,6 @@ import { colors, spacing, borderRadius } from '../../theme'; import { SystemMessageResponse, SystemMessageType } from '../../types/dto'; import Text from '../common/Text'; import Avatar from '../common/Avatar'; -import { useResponsive, useResponsiveValue } from '../../hooks/useResponsive'; interface SystemMessageItemProps { message: SystemMessageResponse; @@ -165,14 +164,6 @@ const SystemMessageItem: React.FC = ({ onRequestAction, requestActionLoading = false, }) => { - // 响应式适配 - const { isWideScreen, isDesktop } = useResponsive(); - const containerPadding = useResponsiveValue({ xs: spacing.md, lg: spacing.lg, xl: spacing.xl }); - const avatarSize = useResponsiveValue({ xs: 40, lg: 48, xl: 52 }); - const iconSize = useResponsiveValue({ xs: 20, lg: 22, xl: 24 }); - const contentFontSize = useResponsiveValue({ xs: 14, lg: 15, xl: 16 }); - const titleFontSize = useResponsiveValue({ xs: 14, lg: 15, xl: 16 }); - const timeFontSize = useResponsiveValue({ xs: 12, lg: 13, xl: 14 }); // 格式化时间 const formatTime = (dateString: string): string => { try { @@ -198,7 +189,15 @@ const SystemMessageItem: React.FC = ({ requestStatus === 'pending' && !!onRequestAction; - const styles = React.useMemo(() => StyleSheet.create({ + // 使用固定的响应式值,兼容移动端和Web端 + const containerPadding = spacing.md; + const avatarSize = 44; + const iconSize = 22; + const contentFontSize = 14; + const titleFontSize = 14; + const timeFontSize = 12; + + const styles = StyleSheet.create({ container: { flexDirection: 'row', alignItems: 'flex-start', @@ -208,7 +207,7 @@ const SystemMessageItem: React.FC = ({ borderBottomColor: colors.divider, }, iconContainer: { - marginRight: isWideScreen ? spacing.lg : spacing.md, + marginRight: spacing.md, }, iconWrapper: { width: avatarSize, @@ -220,6 +219,7 @@ const SystemMessageItem: React.FC = ({ content: { flex: 1, justifyContent: 'center', + minWidth: 0, }, titleRow: { flexDirection: 'row', @@ -237,7 +237,7 @@ const SystemMessageItem: React.FC = ({ fontSize: timeFontSize, }, messageContent: { - lineHeight: isWideScreen ? 22 : 18, + lineHeight: 20, fontSize: contentFontSize, }, extraInfo: { @@ -245,7 +245,7 @@ const SystemMessageItem: React.FC = ({ alignItems: 'center', marginTop: spacing.xs, backgroundColor: colors.background.default, - paddingHorizontal: isWideScreen ? spacing.md : spacing.sm, + paddingHorizontal: spacing.sm, paddingVertical: spacing.xs, borderRadius: borderRadius.sm, alignSelf: 'flex-start', @@ -259,8 +259,8 @@ const SystemMessageItem: React.FC = ({ marginTop: spacing.sm, }, actionBtn: { - paddingVertical: isWideScreen ? spacing.sm : spacing.xs, - paddingHorizontal: isWideScreen ? spacing.lg : spacing.md, + paddingVertical: spacing.xs, + paddingHorizontal: spacing.md, borderRadius: borderRadius.sm, borderWidth: 1, marginRight: spacing.sm, @@ -278,8 +278,9 @@ const SystemMessageItem: React.FC = ({ justifyContent: 'center', alignItems: 'center', height: 20, + width: 24, }, - }), [containerPadding, isWideScreen, avatarSize, titleFontSize, timeFontSize, contentFontSize]); + }); // 判断是否显示操作者头像 const showOperatorAvatar = ['follow', 'like_post', 'like_comment', 'like_reply', 'favorite_post', 'comment', 'reply', 'mention', 'group_join_apply'].includes( diff --git a/src/screens/message/NotificationsScreen.tsx b/src/screens/message/NotificationsScreen.tsx index c3ca10a..6a05791 100644 --- a/src/screens/message/NotificationsScreen.tsx +++ b/src/screens/message/NotificationsScreen.tsx @@ -13,6 +13,7 @@ import { TouchableOpacity, RefreshControl, ActivityIndicator, + Dimensions, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useIsFocused } from '@react-navigation/native'; @@ -25,7 +26,6 @@ import { messageService } from '../../services/messageService'; import { commentService } from '../../services/commentService'; import { SystemMessageItem } from '../../components/business'; import { Text, EmptyState, ResponsiveContainer } from '../../components/common'; -import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive'; import { RootStackParamList } from '../../navigation/types'; import { useMessageManagerSystemUnreadCount, useUserStore } from '../../stores'; @@ -48,9 +48,27 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount(); const navigation = useNavigation>(); - // 响应式布局 - const { isDesktop, isTablet } = useResponsive(); - const isWideScreen = useBreakpointGTE('lg'); + // 修复竞态条件:使用本地状态管理窗口尺寸,避免 hydration 问题 + const [windowSize, setWindowSize] = useState({ width: 0, height: 0 }); + const [isHydrated, setIsHydrated] = useState(false); + + useEffect(() => { + // 延迟设置窗口尺寸,确保在客户端完全加载后再获取 + const timer = setTimeout(() => { + const { width, height } = Dimensions.get('window'); + setWindowSize({ width, height }); + setIsHydrated(true); + }, 100); + return () => clearTimeout(timer); + }, []); + + // 使用本地尺寸状态计算断点 (lg = 768) + const isWideScreen = windowSize.width >= 768; + const isDesktop = windowSize.width >= 1024; + const isTablet = windowSize.width >= 768 && windowSize.width < 1024; + + // Web端使用更大的容器宽度 + const containerMaxWidth = isDesktop ? 1200 : isTablet ? 1000 : 900; const [messages, setMessages] = useState([]); const [activeType, setActiveType] = useState('all'); @@ -320,7 +338,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack const shouldShowBackButton = onBack && !isWideScreen; return ( - + {shouldShowBackButton ? ( void }> = ({ onBack // 渲染空状态 const renderEmpty = () => ( - + + + ); + // 如果还未hydrated,显示加载占位符,避免竞态条件导致的渲染问题 + if (!isHydrated) { + return ( + + + + + + ); + } + return ( {isWideScreen ? ( - + {/* 头部 - 大屏模式下隐藏返回按钮 */} {renderHeader()} {/* 分类筛选 */} - + {MESSAGE_TYPES.map(type => { const count = type.key === 'all' ? displayMessages.length @@ -518,10 +549,11 @@ const styles = StyleSheet.create({ borderBottomWidth: 1, borderBottomColor: colors.divider, }, - filterContainerWide: { + filterContainerWideWeb: { paddingHorizontal: spacing.xl, paddingVertical: spacing.md, justifyContent: 'center', + backgroundColor: colors.background.paper, }, filterTag: { flexDirection: 'row', @@ -596,4 +628,14 @@ const styles = StyleSheet.create({ loadingMoreText: { marginLeft: spacing.sm, }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + emptyContainerWeb: { + minHeight: 500, + justifyContent: 'center', + paddingTop: 100, + }, }); diff --git a/src/screens/schedule/ScheduleScreen.tsx b/src/screens/schedule/ScheduleScreen.tsx index f73a915..b9e44b0 100644 --- a/src/screens/schedule/ScheduleScreen.tsx +++ b/src/screens/schedule/ScheduleScreen.tsx @@ -30,6 +30,7 @@ import { TimeSlot, getCourseColor, hasCourseInWeek, + getCurrentWeek, } from '../../types/schedule'; import type { ScheduleStackParamList } from '../../navigation/types'; import { scheduleService } from '../../services/scheduleService'; @@ -130,9 +131,17 @@ const getWeekDates = (weekOffset: number = 0) => { return getWeekInfo(weekOffset).dates; }; +// 学期起始日期字符串(用于 getCurrentWeek 函数) +const SEMESTER_START_DATE = '2026-03-09'; + +// 计算当前实际周数 +const getInitialWeek = (): number => { + return getCurrentWeek(SEMESTER_START_DATE, TOTAL_WEEKS); +}; + export const ScheduleScreen: React.FC = () => { const navigation = useNavigation>(); - const [currentWeek, setCurrentWeek] = useState(1); + const [currentWeek, setCurrentWeek] = useState(getInitialWeek); const [courses, setCourses] = useState([]); const [isAddModalVisible, setIsAddModalVisible] = useState(false); const [pendingDay, setPendingDay] = useState(0); @@ -163,8 +172,10 @@ export const ScheduleScreen: React.FC = () => { }; }, []); const todayColumnIndex = getTodayColumnIndex(); - // currentWeek === 1 对应今天所在的真实周(offset 0),其他周不高亮今日列 - const isViewingCurrentWeek = currentWeek === 1; + // 获取当前实际周数,用于判断是否在查看当前周 + const actualCurrentWeek = getInitialWeek(); + // 只有在查看当前实际周时才高亮今日列 + const isViewingCurrentWeek = currentWeek === actualCurrentWeek; const activeTodayColumn = isViewingCurrentWeek ? todayColumnIndex : -1; const dayColumnWidth = getDayColumnWidth(); diff --git a/src/services/scheduleService.ts b/src/services/scheduleService.ts index 875d20c..1b6c551 100644 --- a/src/services/scheduleService.ts +++ b/src/services/scheduleService.ts @@ -45,22 +45,42 @@ interface SyncScheduleResponse { error_message?: string; } -const toCourse = (dto: ScheduleCourseDTO): Course => ({ - id: dto.id, - name: dto.name, - teacher: dto.teacher, - location: dto.location, - dayOfWeek: dto.day_of_week, - startSection: dto.start_section, - endSection: dto.end_section, - weeks: dto.weeks ?? [], - color: dto.color, -}); +const toCourse = (dto: ScheduleCourseDTO): Course => { + // 调试日志:检查后端返回的 weeks 字段 + console.log('[ScheduleService] 转换课程 DTO:', { + id: dto.id, + name: dto.name, + day_of_week: dto.day_of_week, + weeks: dto.weeks, + weeksType: typeof dto.weeks, + weeksLength: dto.weeks?.length, + }); + return { + id: dto.id, + name: dto.name, + teacher: dto.teacher, + location: dto.location, + dayOfWeek: dto.day_of_week, + startSection: dto.start_section, + endSection: dto.end_section, + weeks: dto.weeks ?? [], + color: dto.color, + }; +}; class ScheduleService { async getCourses(week?: number): Promise { const params = week ? { week } : undefined; + console.log('[ScheduleService] 获取课程列表, params:', params); const response = await api.get('/schedule/courses', params); + console.log('[ScheduleService] 课程列表响应, 数量:', response.data.list.length); + // 打印原始响应数据中的 weeks 字段 + response.data.list.forEach((item, index) => { + console.log(`[ScheduleService] 课程[${index}] ${item.name}:`, { + day_of_week: item.day_of_week, + weeks: item.weeks, + }); + }); return response.data.list.map(toCourse); } @@ -79,7 +99,9 @@ class ScheduleService { } async syncSchedule(req: SyncScheduleRequest): Promise { + console.log('[ScheduleService] 开始同步教务系统, username:', req.username); const response = await api.post('/schedule/sync', req); + console.log('[ScheduleService] 同步响应:', response.data); return response.data; } }