import 'dart:math' as math; import 'package:flutter/material.dart'; import '../../domain/schedule_engine.dart'; import '../../domain/app_settings.dart'; import '../../app/app_theme.dart'; import 'course_details.dart'; class WeekGrid extends StatelessWidget { const WeekGrid({ super.key, required this.events, required this.monday, required this.today, required this.settings, }); final List events; final DateTime monday, today; final AppSettings settings; @override Widget build(BuildContext context) { final scale = MediaQuery.textScalerOf(context).scale(12) / 12; final rowHeight = 78.0 * scale.clamp(1, 2.5), gap = 20.0, header = 56.0 * scale.clamp(1, 2); double top(int period) => (period - 1) * rowHeight + (period > 4 ? gap : 0) + (period > 8 ? gap : 0); final height = 11 * rowHeight + 2 * gap; return LayoutBuilder( builder: (context, constraints) { final columnWidth = math.max( 90.0 * scale.clamp(1, 1.4), (constraints.maxWidth - 52) / 7, ); final gridWidth = columnWidth * 7; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 52, height: header + height, child: Stack( children: [ for (final slot in lessonSlots) Positioned( top: header + top(slot.period), left: 0, right: 0, height: rowHeight, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '${slot.period}', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 5), Text( minuteLabel(slot.startMinute), style: TextStyle( fontSize: 9, color: ink.withValues(alpha: 0.5), ), ), Text( minuteLabel(slot.endMinute), style: TextStyle( fontSize: 9, color: ink.withValues(alpha: 0.35), ), ), ], ), ), ], ), ), Expanded( child: SingleChildScrollView( key: const Key('week-horizontal'), scrollDirection: Axis.horizontal, child: SizedBox( width: gridWidth, child: Column( children: [ SizedBox( height: header, child: Row( children: List.generate(7, (i) { final date = monday.add(Duration(days: i)); final active = date.year == today.year && date.month == today.month && date.day == today.day; return SizedBox( width: columnWidth, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( const [ '周一', '周二', '周三', '周四', '周五', '周六', '周日', ][i], style: TextStyle( fontSize: 12, color: active ? river : ink.withValues(alpha: 0.55), ), ), const SizedBox(height: 4), Container( padding: const EdgeInsets.symmetric( horizontal: 9, vertical: 3, ), decoration: BoxDecoration( color: active ? river : Colors.transparent, borderRadius: BorderRadius.circular(10), ), child: Text( '${date.day}', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: active ? Colors.white : ink, ), ), ), ], ), ); }), ), ), SizedBox( height: height, child: Stack( children: [ for (var i = 0; i < 7; i++) Positioned( left: i * columnWidth, top: 0, width: columnWidth, height: height, child: DecoratedBox( decoration: BoxDecoration( color: i < 5 ? Colors.white.withValues(alpha: 0.45) : river.withValues(alpha: 0.025), border: Border( left: BorderSide( color: ink.withValues(alpha: 0.035), ), ), ), ), ), for (var p = 1; p <= 11; p++) Positioned( left: 0, right: 0, top: top(p), child: Divider( height: 1, color: ink.withValues(alpha: 0.05), ), ), for (var day = 1; day <= 7; day++) ..._cards( context, day, columnWidth, rowHeight, top, ), ], ), ), ], ), ), ), ), ], ); }, ); } List _cards( BuildContext context, int day, double width, double rowHeight, double Function(int) top, ) { final values = events.where((e) => e.course.weekday == day).toList(); return [ for (final e in values) _position(context, e, values, width, rowHeight, top), ]; } Widget _position( BuildContext context, CourseOccurrence e, List values, double width, double rowHeight, double Function(int) top, ) { final connected = {e}; var grew = true; while (grew) { grew = false; for (final other in values) { if (!connected.contains(other) && connected.any((known) => ScheduleEngine.overlaps(known, other))) { connected.add(other); grew = true; } } } final group = values.where(connected.contains).toList(); final lane = group.indexOf(e), laneWidth = width / group.length; return Positioned( left: (e.course.weekday - 1) * width + lane * laneWidth + 3, top: top(e.course.startPeriod) + 4, width: math.max(12, laneWidth - 6), height: top(e.course.endPeriod) + rowHeight - top(e.course.startPeriod) - 8, child: Semantics( label: '${e.course.name},${e.course.teacher},${e.course.room}', button: true, child: GestureDetector( onTap: () => showCourseDetails(context, e), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: courseColor(e.course.id, settings), borderRadius: BorderRadius.circular(14), border: Border.all(color: Colors.white.withValues(alpha: 0.8)), ), child: SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (group.length > 1) const Text( '时间冲突', style: TextStyle(fontSize: 9, color: Color(0xFFB45D54)), ), Text( e.course.name, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w700, height: 1.25, ), ), const SizedBox(height: 7), Text( e.course.teacher, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 10, color: ink.withValues(alpha: 0.7), ), ), const SizedBox(height: 3), Text( e.course.room, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 10, color: ink.withValues(alpha: 0.6), height: 1.2, ), ), ], ), ), ), ), ), ); } }