This commit is contained in:
zhutao
2025-11-20 18:00:34 +08:00
parent 701b99b138
commit b7239292d1
45 changed files with 1499 additions and 354 deletions

View File

@@ -1,14 +1,16 @@
import 'dart:async';
import 'package:app/providers/user_store.dart';
import 'package:app/request/api/user_api.dart';
import 'package:app/router/route_paths.dart';
import 'package:app/widgets/base/button/index.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:remixicon/remixicon.dart';
import 'widgets/login_agree.dart';
import 'widgets/login_input.dart';
class LoginPage extends StatefulWidget {
@@ -23,8 +25,11 @@ class _LoginPageState extends State<LoginPage> {
bool _agree = false;
///输入框
final TextEditingController _telController = TextEditingController();
final TextEditingController _codeController = TextEditingController();
final TextEditingController _telController = TextEditingController(text: "13343214321");
final TextEditingController _codeController = TextEditingController(text: "1111");
///登录中
bool _loading = false;
///验证码倒计时
var _countDown = 0;
@@ -42,6 +47,7 @@ class _LoginPageState extends State<LoginPage> {
EasyLoading.showToast("请填写正确的手机号");
return;
}
sendCodeApi(_telController.text);
setState(() {
_countDown = 60;
});
@@ -63,7 +69,29 @@ class _LoginPageState extends State<LoginPage> {
EasyLoading.showToast("请填写完整手机号或验证码");
return;
}
context.go(RoutePaths.sHome);
try {
setState(() {
_loading = true;
});
var loginRes = await loginApi(_telController.text, _codeController.text);
if (mounted) {
UserStore userStore = context.read<UserStore>();
//设置登录信息l
await userStore.setToken(loginRes.accessToken);
await userStore.asyncUserInfo();
if (!mounted) return;
if (userStore.userInfo?.accountType == 1) {
context.go(RoutePaths.sHome);
} else {
context.go(RoutePaths.sHome);
}
}
} finally {
setState(() {
_loading = false;
});
}
}
@override
@@ -130,21 +158,25 @@ class _LoginPageState extends State<LoginPage> {
Container(
margin: EdgeInsets.only(top: 40),
height: 50,
child: Button(text: "登 录", onPressed: _handSubmit),
),
Container(
width: double.infinity,
margin: EdgeInsets.only(top: 20),
alignment: Alignment.center,
child: LoginAgree(
value: _agree,
onChanged: (value) {
setState(() {
_agree = value!;
});
},
child: Button(
text: "登 录",
loading: _loading,
onPressed: _handSubmit,
),
),
// Container(
// width: double.infinity,
// margin: EdgeInsets.only(top: 20),
// alignment: Alignment.center,
// child: LoginAgree(
// value: _agree,
// onChanged: (value) {
// setState(() {
// _agree = value!;
// });
// },
// ),
// ),
],
),
),

View File

@@ -1,6 +1,8 @@
import 'package:app/providers/user_store.dart';
import 'package:app/router/route_paths.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
class SplashPage extends StatefulWidget {
const SplashPage({super.key});
@@ -19,7 +21,24 @@ class _SplashPageState extends State<SplashPage> {
///权限效验初始化
void initPermission() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
context.go(RoutePaths.login);
String token = await UserStore.getToken();
if (mounted) {
// 未登录
if (token.isEmpty) {
context.go(RoutePaths.login);
} else {
UserStore userStore = context.read<UserStore>();
userStore.setUserInfo();
//去学生主页
if (userStore.userInfo?.accountType == 1) {
context.go(RoutePaths.sHome);
} else {
context.go(RoutePaths.tHome);
}
print("执行用户数据同步了");
userStore.asyncUserInfo();
}
}
});
}

View File

@@ -1,4 +1,6 @@
import 'package:app/config/theme/base/app_theme_ext.dart';
import 'package:app/request/api/room_api.dart';
import 'package:app/request/dto/room/room_type_dto.dart';
import 'package:flutter/material.dart';
import 'today/s_today_card.dart';
@@ -12,6 +14,8 @@ class SHomePage extends StatefulWidget {
}
class _SHomePageState extends State<SHomePage> {
///刷新状态
Future<void> _refresh() async {
await Future.delayed(Duration(seconds: 1));

View File

@@ -16,7 +16,6 @@ class STodayCard extends StatefulWidget {
}
class _STodayCardState extends State<STodayCard> {
///进入自习室
void _handleEnterRoom() {
context.push(RoutePaths.sRoom);

View File

@@ -1,4 +1,8 @@
import 'package:app/providers/user_store.dart';
import 'package:app/router/route_paths.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:remixicon/remixicon.dart';
class UserHeader extends StatelessWidget implements PreferredSizeWidget {
@@ -34,7 +38,28 @@ class UserHeader extends StatelessWidget implements PreferredSizeWidget {
],
),
),
const SizedBox(width: 15),
PopupMenuButton(
color: Colors.white,
padding: EdgeInsets.zero,
position: PopupMenuPosition.under,
onSelected: (value) {
if (value == 1) {
UserStore userStore = context.read<UserStore>();
userStore.logout();
context.go(RoutePaths.login);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 1,
child: Text("退出登录", textAlign: TextAlign.center),
),
],
child: IconButton(
onPressed: null,
icon: Icon(RemixIcons.user_line),
),
),
],
);
}

View File

@@ -1,27 +1,43 @@
import 'package:app/config/theme/base/app_theme_ext.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'viewmodel/home_view_model.dart';
import 'widgets/header.dart';
import 'widgets/today_card.dart';
class THomePage extends StatefulWidget {
class THomePage extends StatelessWidget {
const THomePage({super.key});
@override
State<THomePage> createState() => _THomePageState();
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => HomeViewModel(),
child: const _HomeView(),
);
}
}
class _THomePageState extends State<THomePage> {
class _HomeView extends StatelessWidget {
const _HomeView();
@override
Widget build(BuildContext context) {
final vm = context.read<HomeViewModel>();
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
appBar: Header(),
body: ListView(
padding: EdgeInsets.symmetric(vertical: 20, horizontal: context.pagePadding),
children: [
TodayCard(),
],
body: RefreshIndicator(
onRefresh: vm.loadData,
child: ListView(
padding: EdgeInsets.symmetric(
vertical: 20,
horizontal: context.pagePadding,
),
children: [
TodayCard(),
],
),
),
);
}

View File

@@ -0,0 +1,56 @@
import 'package:app/request/api/room_api.dart';
import 'package:app/request/dto/room/room_info_dto.dart';
import 'package:app/utils/time.dart';
import 'package:flutter/material.dart';
class HomeViewModel extends ChangeNotifier {
RoomInfoDto? roomInfo;
bool loading = true;
HomeViewModel() {
loadData();
}
//加载数据
Future<void> loadData() async {
final list = await getRoomListApi();
loading = false;
if (list.isNotEmpty) {
roomInfo = list.first;
}
notifyListeners();
}
///计算会议时间
int get roomMinutes {
if (roomInfo == null) return 0;
final start = roomInfo!.startTime;
final end = roomInfo!.endTime;
final s = DateTime.parse('2000-01-01 $start:00');
final e = DateTime.parse('2000-01-01 $end:00');
return e.difference(s).inMinutes;
}
///能否进入房间
bool get canEnterRoom {
final info = roomInfo;
if (info == null) return false;
final now = DateTime.now();
//开始时间
final startTime = parseTime(info.startTime);
// 当前时间距离开始时间是否超过 5 分钟
if (now.isBefore(startTime) && startTime.difference(now).inMinutes > 5) {
return false;
}
return true;
}
}

View File

@@ -1,5 +1,9 @@
import 'package:app/config/theme/base/app_theme_ext.dart';
import 'package:app/providers/user_store.dart';
import 'package:app/router/route_paths.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:remixicon/remixicon.dart';
class Header extends StatelessWidget implements PreferredSizeWidget {
@@ -18,6 +22,7 @@ class Header extends StatelessWidget implements PreferredSizeWidget {
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"学光自习室",
@@ -31,9 +36,27 @@ class Header extends StatelessWidget implements PreferredSizeWidget {
),
Row(
children: [
IconButton(
onPressed: () {},
icon: Icon(RemixIcons.user_line),
PopupMenuButton(
color: Colors.white,
padding: EdgeInsets.zero,
position: PopupMenuPosition.under,
onSelected: (value) {
if (value == 1) {
UserStore userStore = context.read<UserStore>();
userStore.logout();
context.go(RoutePaths.login);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 1,
child: Text("退出登录", textAlign: TextAlign.center),
),
],
child: IconButton(
onPressed: null,
icon: Icon(RemixIcons.user_line),
),
),
],
),

View File

@@ -1,127 +1,174 @@
import 'package:app/pages/teacher/home/viewmodel/home_view_model.dart';
import 'package:app/request/dto/room/room_info_dto.dart';
import 'package:app/router/route_paths.dart';
import 'package:app/utils/permission.dart';
import 'package:app/widgets/base/button/index.dart';
import 'package:app/widgets/base/card/g_card.dart';
import 'package:app/widgets/base/config/config.dart';
import 'package:app/widgets/base/tag/index.dart';
import 'package:app/widgets/base/empty/index.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:go_router/go_router.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:remixicon/remixicon.dart';
import 'package:skeletonizer/skeletonizer.dart';
class TodayCard extends StatelessWidget {
class TodayCard extends StatefulWidget {
const TodayCard({super.key});
@override
State<TodayCard> createState() => _TodayCardState();
}
class _TodayCardState extends State<TodayCard> {
///前往会议室
void _goToRoom() {
checkPermission(
permissions: [Permission.microphone, Permission.camera],
onGranted: () {
final vm = context.read<HomeViewModel>();
context.push(
RoutePaths.tRoom,
extra: {
"roomId": vm.roomInfo!.id,
"startTime": vm.roomInfo!.startTime,
},
);
},
onDenied: () {
EasyLoading.showError("请开启权限");
},
onPermanentlyDenied: () {
EasyLoading.showError("请手动开启麦克风和摄像头权限");
},
);
}
@override
Widget build(BuildContext context) {
/// item
Widget item({
required String title,
required String value,
required IconData icon,
required Color color,
}) {
return Expanded(
child: Container(
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(10),
),
child: Row(
spacing: 10,
children: [
Container(
width: 45,
height: 45,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
color: Colors.white,
),
),
Column(
return Consumer<HomeViewModel>(
builder: (context, vm, _) {
return GCard(
child: Visibility(
visible: !vm.loading && vm.roomInfo == null,
replacement: Skeletonizer(
enabled: vm.loading,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.labelLarge),
Text(value),
],
),
],
),
),
);
}
return GCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 10,
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
spacing: 10,
children: [
Skeleton.replace(
replacement: Bone.text(),
child: Text(vm.roomInfo?.roomName ?? ""),
),
],
),
Container(
margin: EdgeInsets.only(top: 5),
child: Text(
"和学生们一起专注学习、共同进步",
style: Theme.of(context).textTheme.labelMedium,
),
),
],
),
Container(
margin: EdgeInsets.only(top: 30),
child: Row(
spacing: 15,
children: [
Text("高三数学冲刺班"),
Tag(text: "开始"),
_item(
title: "开始时间",
value: vm.roomInfo?.startTime ?? "",
icon: RemixIcons.time_line,
color: Color(0xff2b7efd),
),
_item(
title: "结束时间",
value: vm.roomInfo?.endTime ?? "",
icon: RemixIcons.group_line,
color: Color(0xff00c74f),
),
_item(
title: "时长",
value: "${vm.roomMinutes} 分钟",
icon: RemixIcons.book_open_line,
color: Color(0xffac45fd),
),
],
),
Container(
margin: EdgeInsets.only(top: 5),
child: Text(
"和学生们一起专注学习、共同进步",
style: Theme.of(context).textTheme.labelMedium,
),
),
Container(
margin: EdgeInsets.only(top: 30),
height: 45,
child: Button(
text: vm.canEnterRoom ? "开始自习室" : "未到开始时间",
type: ThemeType.success,
// disabled: !vm.canEnterRoom,
onPressed: _goToRoom,
),
],
),
),
],
),
Icon(RemixIcons.arrow_right_s_line, size: 30),
],
),
child: SizedBox(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Empty(text: "未分配自习室"),
],
),
),
),
Container(
margin: EdgeInsets.only(top: 30),
child: Row(
spacing: 15,
);
},
);
}
Widget _item({
required String title,
required String value,
required IconData icon,
required Color color,
}) {
return Expanded(
child: Container(
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(10),
),
child: Row(
spacing: 10,
children: [
Container(
width: 45,
height: 45,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
color: Colors.white,
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
item(
title: "开始时间",
value: "14:00",
icon: RemixIcons.time_line,
color: Color(0xff2b7efd),
),
item(
title: "学生人数",
value: "8 名",
icon: RemixIcons.group_line,
color: Color(0xff00c74f),
),
item(
title: "时长",
value: "120 分钟",
icon: RemixIcons.book_open_line,
color: Color(0xffac45fd),
),
Text(title, style: Theme.of(context).textTheme.labelLarge),
Text(value),
],
),
),
Container(
margin: EdgeInsets.only(top: 30),
height: 45,
child: Button(
text: "开始自习室",
type: ThemeType.success,
onPressed: () {
context.push(RoutePaths.tRoom);
},
),
),
],
],
),
),
);
}

View File

@@ -1,12 +1,18 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'controls/top_bar.dart';
import 'view/student_item.dart';
import 'view/waiting_start.dart';
import 'widgets/status_view.dart';
import 'viewmodel/students_view_model.dart';
class TRoomPage extends StatefulWidget {
const TRoomPage({super.key});
final int roomId;
final String startTime;
const TRoomPage({
super.key,
required this.roomId,
required this.startTime,
});
@override
State<TRoomPage> createState() => _TRoomPageState();
@@ -17,37 +23,15 @@ class _TRoomPageState extends State<TRoomPage> {
Widget build(BuildContext context) {
return ChangeNotifierProvider<StudentsViewModel>(
create: (BuildContext context) {
return StudentsViewModel();
return StudentsViewModel(
roomId: widget.roomId,
start: widget.startTime,
);
},
child: Scaffold(
backgroundColor: Color(0xff2c3032),
appBar: TopBar(),
body: true
? WaitingStart()
: Padding(
padding: const EdgeInsets.all(10),
child: Row(
spacing: 15,
children: [
Expanded(
child: StudentItem(),
),
SizedBox(
width: 300,
child: ListView.separated(
itemBuilder: (_, index) {
return SizedBox(
height: 250,
child: StudentItem(),
);
},
separatorBuilder: (_, __) => SizedBox(height: 15),
itemCount: 7,
),
),
],
),
),
body: StatusView(),
),
);
}

View File

@@ -1,80 +0,0 @@
import 'dart:async';
import 'package:app/utils/time.dart';
import 'package:flutter/material.dart';
class WaitingStart extends StatefulWidget {
const WaitingStart({super.key});
@override
State<WaitingStart> createState() => _WaitingStartState();
}
class _WaitingStartState extends State<WaitingStart> {
///剩余秒
int _seconds = 0;
Timer? _timer;
@override
void initState() {
super.initState();
startCountDown();
}
@override
void dispose() {
super.dispose();
_timer?.cancel();
_timer = null;
}
///开始倒计时
void startCountDown() {
//当前时间
DateTime now = DateTime.now();
//远端时间
DateTime remote = DateTime.parse("2025-11-19 17:10:00".replaceFirst(' ', 'T'));
setState(() {
_seconds = remote.difference(now).inSeconds;
});
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
_seconds--;
});
if (_seconds <= 0) {
_timer?.cancel();
_timer = null;
_start();
}
});
}
///倒计时结束开始
void _start() {}
@override
Widget build(BuildContext context) {
return Align(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"未到开播时间,到点后自动开播",
style: TextStyle(color: Colors.white),
),
Container(
margin: EdgeInsets.symmetric(vertical: 10),
child: Text(
formatSeconds(_seconds),
style: TextStyle(
color: Colors.white,
fontSize: 26,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}
}

View File

@@ -1,33 +1,78 @@
import 'package:app/data/models/student.dart';
import 'package:app/websocket/room_websocket.dart';
import 'package:app/request/dto/room/room_user_dto.dart';
import 'package:app/request/websocket/room_protocol.dart';
import 'package:app/request/websocket/room_websocket.dart';
import 'package:app/utils/time.dart';
import 'package:flutter/cupertino.dart';
class StudentsViewModel extends ChangeNotifier {
///学生摄像头列表
List<Student> _students = [];
///房间的基础信息房间id、房间开始时间
final int roomId;
late final DateTime startTime;
StudentsViewModel({required this.roomId, String? start}) {
startTime = parseTime(start!);
_startRoom();
}
List<Student> get students => _students;
///是否能开始自习室
bool get canEnterRoom {
final now = DateTime.now();
// 如果到了开始时间,则可以进入房间
if (now.isAfter(startTime)) {
return true;
}
return false;
}
///websocket管理
late RoomWebSocket _ws;
StudentsViewModel() {
_startRoom();
}
///开始链接房间
void _startRoom() {
void _startRoom() async {
_ws = RoomWebSocket();
_ws.connect();
//如果socket的token没有先初始化
if (_ws.wsToken.isEmpty) {
await _ws.initToken(roomId);
}
//启动连接
await _ws.connect();
//进入房间命令
_ws.send(RoomCommand.joinRoom);
//监听各种ws事件
_ws.stream.listen((msg) {
_handleMessage();
if (msg.event == RoomEvent.changeUser) {
final list = msg.data['user_list'].map((x) => RoomUserDto.fromJson(x)).toList();
onStudentChange(list);
}
});
notifyListeners();
}
///发送命令
void _handleMessage() {
print("监听webscoket传来的事件");
///自习室的开关
/// - [isOpen]: 是否开启
void toggleRoom({required bool isOpen}) {
if (isOpen) {
_ws.send(RoomCommand.openRoom);
} else {
_ws.send(RoomCommand.closeRoom);
}
}
///学生人员变化事件,(如加入、退出、掉线)
void onStudentChange(List<RoomUserDto> list) {}
//销毁
@override
void dispose() {
super.dispose();
_ws.dispose();
}
}

View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'student_item.dart';
class ContentView extends StatelessWidget {
const ContentView({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(10),
child: Row(
spacing: 15,
children: [
Expanded(
child: StudentItem(),
),
SizedBox(
width: 300,
child: ListView.separated(
itemBuilder: (_, index) {
return SizedBox(
height: 250,
child: StudentItem(),
);
},
separatorBuilder: (_, __) => SizedBox(height: 15),
itemCount: 7,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,114 @@
import 'dart:async';
import 'package:app/utils/time.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'content_view.dart';
import '../viewmodel/students_view_model.dart';
class StatusView extends StatefulWidget {
const StatusView({super.key});
@override
State<StatusView> createState() => _StatusViewState();
}
class _StatusViewState extends State<StatusView> {
///房间状态
RoomStatus status = RoomStatus.loading;
///剩余秒
int _seconds = 0;
Timer? _timer;
@override
void initState() {
super.initState();
_init();
}
@override
void dispose() {
super.dispose();
_timer?.cancel();
_timer = null;
}
void _init() {
final vm = context.read<StudentsViewModel>();
//如果房间可以开始
if (vm.canEnterRoom) {
status = RoomStatus.start;
} else {
status = RoomStatus.waiting;
startCountDown();
}
}
///开始倒计时
void startCountDown() {
final vm = context.read<StudentsViewModel>();
//当前时间
DateTime now = DateTime.now();
//远端时间
setState(() {
_seconds = vm.startTime.difference(now).inSeconds;
});
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
_seconds--;
});
if (_seconds <= 0) {
_timer?.cancel();
_timer = null;
setState(() {
status = RoomStatus.start;
});
}
});
}
///开启自习室
void openRoom() {
final vm = context.read<StudentsViewModel>();
vm.toggleRoom(isOpen: true);
}
@override
Widget build(BuildContext context) {
if (status == RoomStatus.waiting) {
return Align(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"未到开播时间,到点后自动开播",
style: TextStyle(color: Colors.white),
),
Container(
margin: EdgeInsets.symmetric(vertical: 10),
child: Text(
formatSeconds(_seconds),
style: TextStyle(
color: Colors.white,
fontSize: 26,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
} else if (status == RoomStatus.start) {
return ContentView();
}
return SizedBox();
}
}
enum RoomStatus {
loading, // 加载中
waiting, //房间倒计时等待中
start, //房间开始中
}