feat: add two-tab timetable UI and glass dock

This commit is contained in:
Codex
2026-10-12 23:33:33 +08:00
parent 808af007a2
commit 8a98d49d12
17 changed files with 1912 additions and 9 deletions
@@ -0,0 +1,135 @@
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../app/app_theme.dart';
import '../../data/json_schedule_import.dart';
import '../../domain/course.dart';
class ImportPreview extends StatefulWidget {
const ImportPreview({
super.key,
required this.draft,
required this.controller,
});
final ImportDraft draft;
final AppController controller;
@override
State<ImportPreview> createState() => _ImportPreviewState();
}
class _ImportPreviewState extends State<ImportPreview> {
bool _clearConfirmed = false, _busy = false;
@override
Widget build(BuildContext context) {
final draft = widget.draft, empty = draft.courses.isEmpty;
final allowed =
!_busy && (!empty || (draft.explicitlyEmpty && _clearConfirmed));
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(24, 8, 24, 28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'确认你的课表',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
Text(
'${draft.courses.length} 条课程安排 · ${draft.source == CourseSource.demo ? '示例体验' : '导入预览'}',
style: const TextStyle(color: river),
),
const SizedBox(height: 18),
if (empty)
Text(draft.explicitlyEmpty ? '此操作会清空当前课表。' : '没有识别到课程,当前课表保持不变。'),
if (empty && draft.explicitlyEmpty)
CheckboxListTile(
value: _clearConfirmed,
onChanged: (v) => setState(() => _clearConfirmed = v ?? false),
title: const Text('确认清空当前课表'),
contentPadding: EdgeInsets.zero,
),
for (final c in draft.courses)
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: SurfaceCard(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
c.name,
style: const TextStyle(fontWeight: FontWeight.w700),
),
const SizedBox(height: 5),
Text(
'${c.teacher} · ${c.room}',
style: const TextStyle(fontSize: 12),
),
const SizedBox(height: 4),
Text(
'周${const ['一', '二', '三', '四', '五', '六', '日'][c.weekday - 1]} · 第 ${c.startPeriod}–${c.endPeriod} 节',
style: TextStyle(
fontSize: 12,
color: ink.withValues(alpha: 0.5),
),
),
],
),
),
),
if (widget.controller.snapshot.settings.semester == null &&
draft.source != CourseSource.demo)
const Padding(
padding: EdgeInsets.only(bottom: 16),
child: Text(
'导入后请在设置中选择真实学期开始日,课表与提醒才会显示。',
style: TextStyle(fontSize: 12, height: 1.6),
),
),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: allowed
? () async {
setState(() => _busy = true);
await widget.controller.applyImport(
draft,
confirmedEmpty: _clearConfirmed,
);
if (!context.mounted) return;
if (widget.controller.lastError == null) {
Navigator.pop(context, true);
} else {
setState(() => _busy = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(widget.controller.lastError!),
),
);
}
}
: null,
child: Text(
_busy
? '正在保存…'
: draft.source == CourseSource.demo
? '开始体验'
: '确认导入',
),
),
),
const SizedBox(height: 8),
Center(
child: TextButton(
onPressed: _busy ? null : () => Navigator.pop(context, false),
child: const Text('取消'),
),
),
],
),
),
);
}
}