From 5784a0a5d4b8a15efcb53101cdcb42e8c57390ac Mon Sep 17 00:00:00 2001 From: zhutao <1812073942@qq.com> Date: Fri, 21 Nov 2025 18:21:47 +0800 Subject: [PATCH] 1 --- lib/pages/common/auth/login_page.dart | 10 +- lib/pages/common/splash/splash_page.dart | 9 +- lib/pages/student/home/s_home_page.dart | 23 +-- lib/pages/student/home/today/banner_info.dart | 63 ++++--- .../student/home/today/s_today_card.dart | 165 +++++++++++++----- .../student/home/today/teacher_info.dart | 54 ------ .../student/home/viewmodel/s_home_vm.dart | 34 ++++ .../student/home/widgets/user_header.dart | 4 +- .../student/room/controls/bottom_bar.dart | 62 ++++--- lib/pages/student/room/controls/top_bar.dart | 70 ++++++-- lib/pages/student/room/s_room_page.dart | 106 ++++++----- .../room/video/student_video_list.dart | 78 ++++----- .../student/room/viewmodel/stu_room_vm.dart | 133 ++++++++++++++ .../home/viewmodel/home_view_model.dart | 10 +- .../teacher/home/widgets/today_card.dart | 8 +- lib/pages/teacher/room/t_room_page.dart | 16 +- ...dents_view_model.dart => tch_room_vm.dart} | 56 +++--- lib/pages/teacher/room/viewmodel/type.dart | 10 +- .../teacher/room/widgets/content_view.dart | 33 ++-- .../teacher/room/widgets/status_view.dart | 16 +- .../teacher/room/widgets/student_item.dart | 46 ++++- lib/providers/user_store.dart | 23 ++- lib/request/dto/room/room_info_dto.dart | 6 +- lib/request/dto/room/room_type_dto.dart | 4 +- lib/request/dto/room/room_user_dto.dart | 15 +- lib/request/dto/room/rtc_token_dto.dart | 2 +- lib/request/network/interceptor.dart | 1 + lib/request/websocket/room_protocol.dart | 14 +- lib/request/websocket/room_websocket.dart | 22 ++- lib/router/modules/student_routes.dart | 7 +- lib/router/modules/teacher_routes.dart | 7 +- lib/widgets/room/video_surface.dart | 68 ++------ 32 files changed, 734 insertions(+), 441 deletions(-) delete mode 100644 lib/pages/student/home/today/teacher_info.dart create mode 100644 lib/pages/student/home/viewmodel/s_home_vm.dart create mode 100644 lib/pages/student/room/viewmodel/stu_room_vm.dart rename lib/pages/teacher/room/viewmodel/{students_view_model.dart => tch_room_vm.dart} (73%) diff --git a/lib/pages/common/auth/login_page.dart b/lib/pages/common/auth/login_page.dart index e2e9702..4e1aedd 100644 --- a/lib/pages/common/auth/login_page.dart +++ b/lib/pages/common/auth/login_page.dart @@ -79,12 +79,16 @@ class _LoginPageState extends State { //设置登录信息l await userStore.setToken(loginRes.accessToken); - await userStore.asyncUserInfo(); + await userStore.setUserInfo(); + if (!mounted) return; if (userStore.userInfo?.accountType == 1) { context.go(RoutePaths.sHome); - } else { - context.go(RoutePaths.sHome); + } else if(userStore.userInfo?.accountType == 2){ + context.go(RoutePaths.tHome); + }else{ + EasyLoading.showError("账号类型错误"); + } } } finally { diff --git a/lib/pages/common/splash/splash_page.dart b/lib/pages/common/splash/splash_page.dart index fc7c125..576bf0e 100644 --- a/lib/pages/common/splash/splash_page.dart +++ b/lib/pages/common/splash/splash_page.dart @@ -1,6 +1,7 @@ import 'package:app/providers/user_store.dart'; import 'package:app/router/route_paths.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; @@ -28,15 +29,15 @@ class _SplashPageState extends State { context.go(RoutePaths.login); } else { UserStore userStore = context.read(); - userStore.setUserInfo(); + await userStore.init(); //去学生主页 if (userStore.userInfo?.accountType == 1) { context.go(RoutePaths.sHome); - } else { + } else if(userStore.userInfo?.accountType == 2){ context.go(RoutePaths.tHome); + }else{ + EasyLoading.showError("无法找到首页"); } - print("执行用户数据同步了"); - userStore.asyncUserInfo(); } } }); diff --git a/lib/pages/student/home/s_home_page.dart b/lib/pages/student/home/s_home_page.dart index 44002e9..3075fb7 100644 --- a/lib/pages/student/home/s_home_page.dart +++ b/lib/pages/student/home/s_home_page.dart @@ -1,33 +1,36 @@ import 'package:app/config/theme/base/app_theme_ext.dart'; +import 'package:app/pages/student/home/viewmodel/s_home_vm.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 'package:provider/provider.dart'; import 'today/s_today_card.dart'; import 'widgets/user_header.dart'; -class SHomePage extends StatefulWidget { +class SHomePage extends StatelessWidget { const SHomePage({super.key}); @override - State createState() => _SHomePageState(); + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => SHomeVm(), + child: _HomeView(), + ); + } } -class _SHomePageState extends State { - - - ///刷新状态 - Future _refresh() async { - await Future.delayed(Duration(seconds: 1)); - } +class _HomeView extends StatelessWidget { + const _HomeView({super.key}); @override Widget build(BuildContext context) { + final vm = context.read(); return Scaffold( backgroundColor: Theme.of(context).colorScheme.surfaceContainer, appBar: UserHeader(), body: RefreshIndicator( - onRefresh: _refresh, + onRefresh: vm.loadData, child: ListView( padding: EdgeInsets.all(context.pagePadding), children: [ diff --git a/lib/pages/student/home/today/banner_info.dart b/lib/pages/student/home/today/banner_info.dart index 339b558..210295c 100644 --- a/lib/pages/student/home/today/banner_info.dart +++ b/lib/pages/student/home/today/banner_info.dart @@ -1,5 +1,9 @@ import 'package:app/config/theme/base/app_theme_ext.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../viewmodel/s_home_vm.dart'; ///banner class BannerInfo extends StatelessWidget { @@ -7,11 +11,12 @@ class BannerInfo extends StatelessWidget { @override Widget build(BuildContext context) { + final vm = context.read(); return Stack( children: [ Positioned.fill( - child: Image.network( - "https://images.unsplash.com/photo-1505209487757-5114235191e5?w=800", + child: CachedNetworkImage( + imageUrl: "https://www.gxgif.com/pic/fj/2025115155717.jpg", fit: BoxFit.cover, ), ), @@ -35,34 +40,38 @@ class BannerInfo extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), - margin: EdgeInsets.only(bottom: 30), - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: BorderRadius.circular(30), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 5, - children: [ - Container( - width: 15, - height: 15, - decoration: BoxDecoration( - color: context.success, - shape: BoxShape.circle, + Visibility( + visible: false, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), + margin: EdgeInsets.only(bottom: 30), + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.circular(30), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 5, + children: [ + Container( + width: 15, + height: 15, + decoration: BoxDecoration( + color: context.success, + shape: BoxShape.circle, + ), ), - ), - Text( - "进行中", - style: TextStyle(color: Colors.white, fontSize: 14), - ), - ], + Text( + "进行中", + style: TextStyle(color: Colors.white, fontSize: 14), + ), + ], + ), ), ), + SizedBox(height: 50), Text( - "高中数学专场", + vm.roomInfo?.roomName ?? "", style: TextStyle( color: Colors.white, fontSize: 20, @@ -82,4 +91,4 @@ class BannerInfo extends StatelessWidget { ], ); } -} \ No newline at end of file +} diff --git a/lib/pages/student/home/today/s_today_card.dart b/lib/pages/student/home/today/s_today_card.dart index 86613bf..7b86f55 100644 --- a/lib/pages/student/home/today/s_today_card.dart +++ b/lib/pages/student/home/today/s_today_card.dart @@ -1,12 +1,19 @@ import 'package:app/config/theme/base/app_theme_ext.dart'; +import 'package:app/pages/student/home/viewmodel/s_home_vm.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/empty/index.dart'; +import 'package:cached_network_image/cached_network_image.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'; import 'banner_info.dart'; -import 'teacher_info.dart'; class STodayCard extends StatefulWidget { const STodayCard({super.key}); @@ -16,60 +23,124 @@ class STodayCard extends StatefulWidget { } class _STodayCardState extends State { - ///进入自习室 - void _handleEnterRoom() { - context.push(RoutePaths.sRoom); + ///前往会议室 + void _goToRoom() { + checkPermission( + permissions: [Permission.microphone, Permission.camera], + onGranted: () { + final vm = context.read(); + context.push(RoutePaths.sRoom,extra: vm.roomInfo); + }, + onDenied: () { + EasyLoading.showError("请开启权限"); + }, + onPermanentlyDenied: () { + EasyLoading.showError("请手动开启麦克风和摄像头权限"); + }, + ); } + @override Widget build(BuildContext context) { - return Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BannerInfo(), - Container( - width: double.infinity, - padding: EdgeInsets.symmetric(horizontal: context.pagePadding, vertical: 20), - decoration: BoxDecoration( - color: Colors.white, + final vm = context.watch(); + if (!vm.loading && vm.roomInfo == null) { + return Empty(text: "没有自习室"); + } + return Skeletonizer( + enabled: vm.loading, + child: Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Skeleton.unite( + child: BannerInfo(), ), - child: Column( - spacing: 30, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TeacherInfo(), - Row( - spacing: 20, - children: [ - InfoItem( - label: "自习时间", - value: "19:00-21:00", - icon: RemixIcons.time_line, - color: Theme.of(context).primaryColor, + Container( + width: double.infinity, + padding: EdgeInsets.symmetric(horizontal: context.pagePadding, vertical: 20), + decoration: BoxDecoration( + color: Colors.white, + ), + child: Column( + spacing: 30, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.all(context.pagePadding), + decoration: BoxDecoration( + color: Color(0xffeef2ff), + borderRadius: BorderRadius.circular(10), ), - InfoItem( - label: "在线人数", - value: "8/12 人", - icon: RemixIcons.time_line, - color: context.success, + child: Row( + spacing: 15, + children: [ + Container( + width: 60, + height: 60, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 3), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: Skeleton.replace( + replacement: Bone.circle(), + child: CachedNetworkImage( + imageUrl: vm.roomInfo?.teacherAvatar ?? "", + fit: BoxFit.cover, + ), + ), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(vm.roomInfo?.teacherName ?? ""), + Text( + vm.roomInfo?.teacherBackground ?? "", + style: Theme.of(context).textTheme.labelLarge, + ), + ], + ), + ], ), - ], - ), - SizedBox( - height: 50, - child: Button( - text: "进入自习室", - onPressed: _handleEnterRoom, ), - ), - ], + Row( + spacing: 20, + children: [ + InfoItem( + label: "自习时间", + value: "${vm.roomInfo?.startTime}-${vm.roomInfo?.endTime}", + icon: RemixIcons.time_line, + color: Theme.of(context).primaryColor, + ), + InfoItem( + label: "自习时长", + value: "${vm.roomMinutes} 分钟", + icon: RemixIcons.timer_line, + color: context.success, + ), + ], + ), + Skeleton.unite( + child: SizedBox( + height: 50, + child: Button( + text: "进入自习室", + onPressed: _goToRoom, + ), + ), + ), + ], + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/pages/student/home/today/teacher_info.dart b/lib/pages/student/home/today/teacher_info.dart deleted file mode 100644 index fe53e86..0000000 --- a/lib/pages/student/home/today/teacher_info.dart +++ /dev/null @@ -1,54 +0,0 @@ - -//老师信息 -import 'package:app/config/theme/base/app_theme_ext.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; - -class TeacherInfo extends StatelessWidget { - const TeacherInfo({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.all(context.pagePadding), - decoration: BoxDecoration( - color: Color(0xffeef2ff), - borderRadius: BorderRadius.circular(10), - ), - child: Row( - spacing: 15, - children: [ - Container( - width: 60, - height: 60, - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: Colors.white, - width: 3, - ), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(50), - child: CachedNetworkImage( - imageUrl: 'https://doaf.asia/api/assets/1/图/62865798_p0.jpg', - fit: BoxFit.cover, - ), - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("张老师"), - Text( - "资深数学教师 · 10年教学经验", - style: Theme.of(context).textTheme.labelLarge, - ), - ], - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/pages/student/home/viewmodel/s_home_vm.dart b/lib/pages/student/home/viewmodel/s_home_vm.dart new file mode 100644 index 0000000..f195ab3 --- /dev/null +++ b/lib/pages/student/home/viewmodel/s_home_vm.dart @@ -0,0 +1,34 @@ +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/cupertino.dart'; + +class SHomeVm extends ChangeNotifier { + RoomInfoDto? roomInfo; + bool loading = true; + + SHomeVm() { + loadData(); + } + + //加载数据 + Future 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 = parseTime(roomInfo!.startTime); + final end = parseTime(roomInfo!.endTime); + + return end.difference(start).inMinutes; + } +} diff --git a/lib/pages/student/home/widgets/user_header.dart b/lib/pages/student/home/widgets/user_header.dart index 6543b4f..eefcf70 100644 --- a/lib/pages/student/home/widgets/user_header.dart +++ b/lib/pages/student/home/widgets/user_header.dart @@ -1,5 +1,6 @@ import 'package:app/providers/user_store.dart'; import 'package:app/router/route_paths.dart'; +import 'package:app/utils/time.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; @@ -10,6 +11,7 @@ class UserHeader extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { + final userStore = context.read(); return AppBar( title: const Text('学光自习室'), actions: [ @@ -32,7 +34,7 @@ class UserHeader extends StatelessWidget implements PreferredSizeWidget { size: 18, ), Text( - "会员至 2025-03-12", + "会员至 ${formatDate(userStore.userInfo?.extraInfo.vipEndTime,'YYYY-MM-DD')}", style: TextStyle(color: Colors.white, fontSize: 14), ), ], diff --git a/lib/pages/student/room/controls/bottom_bar.dart b/lib/pages/student/room/controls/bottom_bar.dart index e763df2..1c7e64d 100644 --- a/lib/pages/student/room/controls/bottom_bar.dart +++ b/lib/pages/student/room/controls/bottom_bar.dart @@ -1,5 +1,7 @@ +import 'package:app/pages/student/room/viewmodel/stu_room_vm.dart'; import 'package:app/widgets/room/file_drawer.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:remixicon/remixicon.dart'; @@ -23,31 +25,41 @@ class _BottomBarState extends State { color: Color(0xff232426), ), height: 70, - child: Row( - children: [ - BarItem( - title: "摄像头", - icon: RemixIcons.video_on_fill, - ), - BarItem( - title: "麦克风", - icon: RemixIcons.mic_off_fill, - ), - BarItem( - title: "已静音", - icon: RemixIcons.volume_mute_fill, - isOff: true, - ), - BarItem( - title: "举手", - icon: RemixIcons.hand, - ), - BarItem( - title: "拍照", - icon: RemixIcons.upload_2_fill, - onTap: _handShowFile, - ), - ], + child: Consumer( + builder: (context,vm,_) { + //摄像头开关 + return Row( + children: [ + BarItem( + title: "摄像头", + icon: vm.cameraOpen ? RemixIcons.video_on_fill : RemixIcons.video_off_fill, + isOff: !vm.cameraOpen, + onTap: vm.changeCameraSwitch, + ), + BarItem( + title: "麦克风", + icon: vm.micOpen ? RemixIcons.mic_fill : RemixIcons.mic_off_fill, + isOff: !vm.micOpen, + onTap: vm.changeMicSwitch, + ), + BarItem( + title: "声音", + icon: vm.speakerOpen ? RemixIcons.volume_up_fill : RemixIcons.volume_mute_fill, + isOff: !vm.speakerOpen, + onTap: vm.changeSpeakerSwitch, + ), + BarItem( + title: "举手", + icon: RemixIcons.hand, + ), + BarItem( + title: "拍照", + icon: RemixIcons.upload_2_fill, + onTap: _handShowFile, + ), + ], + ); + } ), ); } diff --git a/lib/pages/student/room/controls/top_bar.dart b/lib/pages/student/room/controls/top_bar.dart index 76933b5..08bf12c 100644 --- a/lib/pages/student/room/controls/top_bar.dart +++ b/lib/pages/student/room/controls/top_bar.dart @@ -1,37 +1,83 @@ +import 'dart:async'; + +import 'package:app/utils/time.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:remixicon/remixicon.dart'; -class TopBar extends StatelessWidget implements PreferredSizeWidget { +import '../viewmodel/stu_room_vm.dart'; + +class TopBar extends StatefulWidget implements PreferredSizeWidget { final bool showOther; final void Function()? onOther; - const TopBar({super.key, this.showOther = false, this.onOther}); + const TopBar({ + super.key, + this.showOther = false, + this.onOther, + }); + + @override + State createState() => _TopBarState(); + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} + +class _TopBarState extends State { + Timer? _timer; + int seconds = 0; + late DateTime startTime; + + @override + void initState() { + super.initState(); + final vm = context.read(); + startTime = parseTime(vm.roomInfo.startTime); + + _timer = Timer.periodic(const Duration(seconds: 1), (_) { + final diff = DateTime.now().difference(startTime).inSeconds; + setState(() { + seconds = diff < 0 ? 0 : diff; + }); + }); + } + + /// 你若想外面主动停,可以暴露这个方法 + void stopTimer() { + _timer?.cancel(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } @override Widget build(BuildContext context) { + final vm = context.read(); + return AppBar( foregroundColor: Colors.white, - titleTextStyle: TextStyle(color: Colors.white, fontSize: 18), - backgroundColor: Color(0xff232426), + titleTextStyle: const TextStyle(color: Colors.white, fontSize: 18), + backgroundColor: const Color(0xff232426), centerTitle: true, title: Column( children: [ - Text("会议"), + Text(vm.roomInfo.roomName), Text( - "01:12", - style: TextStyle(fontSize: 12, color: Colors.white24), + formatSeconds(seconds), + style: const TextStyle(fontSize: 12, color: Colors.white24), ), ], ), actions: [ IconButton( - onPressed: onOther, - icon: Icon(showOther ? RemixIcons.team_fill : RemixIcons.team_line), + onPressed: widget.onOther, + icon: Icon(widget.showOther ? RemixIcons.team_fill : RemixIcons.team_line), ), ], ); } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); } diff --git a/lib/pages/student/room/s_room_page.dart b/lib/pages/student/room/s_room_page.dart index b8bc4a5..868f64e 100644 --- a/lib/pages/student/room/s_room_page.dart +++ b/lib/pages/student/room/s_room_page.dart @@ -1,5 +1,9 @@ +import 'package:app/pages/student/room/viewmodel/stu_room_vm.dart'; +import 'package:app/providers/user_store.dart'; +import 'package:app/request/dto/room/room_info_dto.dart'; import 'package:app/widgets/base/transition/slide_hide.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'controls/bottom_bar.dart'; import 'controls/top_bar.dart'; @@ -7,7 +11,9 @@ import 'video/student_video_list.dart'; import 'video/teacher_video.dart'; class SRoomPage extends StatefulWidget { - const SRoomPage({super.key}); + final RoomInfoDto roomInfo; + + const SRoomPage({super.key, required this.roomInfo}); @override State createState() => _SRoomPageState(); @@ -29,57 +35,63 @@ class _SRoomPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: Stack( - children: [ - //底部控制显示 - GestureDetector( - onTap: _toggleOverlay, - child: Container(color: Color(0xff2c3032)), - ), - - //老师视频画面 - TeacherVideo(), - - Positioned( - right: 0, - top: 0, - bottom: 0, - child: Visibility( - visible: _showOtherStudent, - child: StudentVideoList(), + UserStore userStore = context.read(); + return ChangeNotifierProvider( + create: (_) => StuRoomVM( + roomInfo: widget.roomInfo, + uid: userStore.userInfo!.id, + ), + child: Scaffold( + body: Stack( + children: [ + //底部控制显示 + GestureDetector( + onTap: _toggleOverlay, + child: Container(color: Color(0xff2c3032)), ), - ), + //老师视频画面 + TeacherVideo(), - ///控制栏 - Positioned( - top: 0, - left: 0, - right: 0, - child: SlideHide( - direction: SlideDirection.up, - hide: !_controlsVisible, - child: TopBar( - showOther: _showOtherStudent, - onOther: () { - setState(() { - _showOtherStudent = !_showOtherStudent; - }); - }, + Positioned( + right: 0, + top: 0, + bottom: 0, + child: Visibility( + visible: _showOtherStudent, + child: StudentVideoList(), ), ), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: SlideHide( - direction: SlideDirection.down, - hide: !_controlsVisible, - child: BottomBar(), + + ///控制栏 + Positioned( + top: 0, + left: 0, + right: 0, + child: SlideHide( + direction: SlideDirection.up, + hide: !_controlsVisible, + child: TopBar( + showOther: _showOtherStudent, + onOther: () { + setState(() { + _showOtherStudent = !_showOtherStudent; + }); + }, + ), + ), ), - ), - ], + Positioned( + bottom: 0, + left: 0, + right: 0, + child: SlideHide( + direction: SlideDirection.down, + hide: !_controlsVisible, + child: BottomBar(), + ), + ), + ], + ), ), ); } diff --git a/lib/pages/student/room/video/student_video_list.dart b/lib/pages/student/room/video/student_video_list.dart index 808cc6a..23f085e 100644 --- a/lib/pages/student/room/video/student_video_list.dart +++ b/lib/pages/student/room/video/student_video_list.dart @@ -1,24 +1,50 @@ +import 'package:app/pages/student/room/viewmodel/stu_room_vm.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; -class StudentVideoList extends StatefulWidget { +class StudentVideoList extends StatelessWidget { const StudentVideoList({super.key}); - @override - State createState() => _StudentVideoListState(); -} - -class _StudentVideoListState extends State { @override Widget build(BuildContext context) { + final vm = context.watch(); return SafeArea( child: Container( width: 250, padding: EdgeInsets.only(bottom: 30), child: ListView.separated( padding: EdgeInsets.all(10), - itemCount: 8, + itemCount: vm.otherStuList.length, itemBuilder: (context, index) { - return VideoItem(); + final item = vm.otherStuList[index]; + return Stack( + children: [ + AspectRatio( + aspectRatio: 1.5 / 1, + child: Container( + decoration: BoxDecoration( + color: Color(0xff373c3e), + borderRadius: BorderRadius.circular(10), + ), + ), + ), + Positioned( + bottom: 5, + left: 5, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 7, vertical: 3), + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.circular(5), + ), + child: Text( + item.userName, + style: TextStyle(fontSize: 12, color: Colors.white), + ), + ), + ), + ], + ); }, separatorBuilder: (context, index) => SizedBox(height: 15), ), @@ -26,39 +52,3 @@ class _StudentVideoListState extends State { ); } } - -class VideoItem extends StatelessWidget { - const VideoItem({super.key}); - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - AspectRatio( - aspectRatio: 1.5 / 1, - child: Container( - decoration: BoxDecoration( - color: Color(0xff373c3e), - borderRadius: BorderRadius.circular(10), - ), - ), - ), - Positioned( - bottom: 5, - left: 5, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 7, vertical: 3), - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: BorderRadius.circular(5), - ), - child: Text( - "小红", - style: TextStyle(fontSize: 12, color: Colors.white), - ), - ), - ), - ], - ); - } -} diff --git a/lib/pages/student/room/viewmodel/stu_room_vm.dart b/lib/pages/student/room/viewmodel/stu_room_vm.dart new file mode 100644 index 0000000..3ca0d9e --- /dev/null +++ b/lib/pages/student/room/viewmodel/stu_room_vm.dart @@ -0,0 +1,133 @@ +import 'dart:async'; + +import 'package:app/providers/user_store.dart'; +import 'package:app/request/dto/room/room_info_dto.dart'; +import 'package:app/request/dto/room/room_type_dto.dart'; +import 'package:app/request/dto/room/room_user_dto.dart'; +import 'package:app/request/dto/room/rtc_token_dto.dart'; +import 'package:app/request/websocket/room_protocol.dart'; +import 'package:app/request/websocket/room_websocket.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:logger/logger.dart'; + +Logger log = Logger(); + +class StuRoomVM extends ChangeNotifier { + ///房间信息 + final RoomInfoDto roomInfo; + + ///房间开启状态,0没开始,1进行中,2已结束 + int roomStatus = 0; + + ///其他学生列表,老师信息,自己信息 + int uid; + List otherStuList = []; + RoomUserDto? teacherInfo; + RoomUserDto? selfInfo; + + ///本人的摄像头、麦克风、扬声器状态是否打开了 + bool get cameraOpen => selfInfo?.cameraStatus == 1; + + bool get micOpen => selfInfo?.microphoneStatus == 1; + + bool get speakerOpen => selfInfo?.speekerStatus == 1; + + ///ws管理 + final RoomWebSocket _ws = RoomWebSocket(); + StreamSubscription? _sub; + + RtcTokenDto? get rtcToken => _ws.rtcToken; + + StuRoomVM({required this.roomInfo, required this.uid}) { + _startRoom(); + } + + ///开始链接房间 + Future _startRoom() async { + //如果socket的token没有,先初始化 + if (_ws.wsToken.isEmpty) { + await _ws.initToken(roomInfo.id); + } + //启动连接 + await _ws.connect(); + // + _sub = _ws.stream.listen((msg) { + //自习室人员变化,同时也设置房间是否开了 + if (msg.event == RoomEvent.changeUser) { + final list = RoomUserDto.listFromJson(msg.data['user_list']); + onStudentChange(list); + onRoomStartStatus(RoomTypeDto.fromJson(msg.data['room_info'])); + } + }); + } + + ///学生人员变化事件,(如加入、退出、掉线) + void onStudentChange(List list) { + List newList = []; + for (var t in list) { + //设置老师 + if (t.userType == 2) { + teacherInfo = t; + } else { + //要过滤自己,只要其他学生 + if (t.userId != uid) { + newList.add(t); + } else { + selfInfo = t; + } + } + } + otherStuList = newList; + notifyListeners(); + } + + ///设置房间开启状态 + void onRoomStartStatus(RoomTypeDto roomInfo) { + roomStatus = roomInfo.roomStatus; + notifyListeners(); + } + + ///控制摄像头开关 + void changeCameraSwitch() { + bool isOpen = selfInfo!.cameraStatus == 1; + selfInfo!.cameraStatus = isOpen ? 0 : 1; + //发送指令 + _ws.send(RoomCommand.studentActon, { + "mute_type": "camera", + "is_mute": isOpen ? 1 : 0, + }); + notifyListeners(); + } + + ///控制麦克风开关 + void changeMicSwitch() { + bool isOpen = selfInfo!.microphoneStatus == 1; + selfInfo!.microphoneStatus = isOpen ? 0 : 1; + print(selfInfo!.microphoneStatus); + //发送指令 + _ws.send(RoomCommand.studentActon, { + "mute_type": "microphone", + "is_mute": isOpen ? 1 : 0, + }); + notifyListeners(); + } + + /// 控制扬声器开关 + void changeSpeakerSwitch() { + bool isOpen = selfInfo!.speekerStatus == 1; + selfInfo!.speekerStatus = isOpen ? 0 : 1; + //发送指令 + _ws.send(RoomCommand.studentActon, { + "mute_type": "speeker", + "is_mute": isOpen ? 1 : 0, + }); + notifyListeners(); + } + + @override + void dispose() { + super.dispose(); + _sub?.cancel(); + _ws.dispose(); + } +} diff --git a/lib/pages/teacher/home/viewmodel/home_view_model.dart b/lib/pages/teacher/home/viewmodel/home_view_model.dart index 1f06b5d..bd9eb00 100644 --- a/lib/pages/teacher/home/viewmodel/home_view_model.dart +++ b/lib/pages/teacher/home/viewmodel/home_view_model.dart @@ -26,14 +26,10 @@ class HomeViewModel extends ChangeNotifier { ///计算会议时间 int get roomMinutes { if (roomInfo == null) return 0; + final start = parseTime(roomInfo!.startTime); + final end = parseTime(roomInfo!.endTime); - 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; + return end.difference(start).inMinutes; } ///能否进入房间 diff --git a/lib/pages/teacher/home/widgets/today_card.dart b/lib/pages/teacher/home/widgets/today_card.dart index 8cf15ff..4c217c1 100644 --- a/lib/pages/teacher/home/widgets/today_card.dart +++ b/lib/pages/teacher/home/widgets/today_card.dart @@ -28,13 +28,7 @@ class _TodayCardState extends State { permissions: [Permission.microphone, Permission.camera], onGranted: () { final vm = context.read(); - context.push( - RoutePaths.tRoom, - extra: { - "roomId": vm.roomInfo!.id, - "startTime": vm.roomInfo!.startTime, - }, - ); + context.push(RoutePaths.tRoom,extra: vm.roomInfo); }, onDenied: () { EasyLoading.showError("请开启权限"); diff --git a/lib/pages/teacher/room/t_room_page.dart b/lib/pages/teacher/room/t_room_page.dart index ee5e23d..2f8fa4e 100644 --- a/lib/pages/teacher/room/t_room_page.dart +++ b/lib/pages/teacher/room/t_room_page.dart @@ -1,17 +1,16 @@ +import 'package:app/request/dto/room/room_info_dto.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'controls/top_bar.dart'; import 'widgets/status_view.dart'; -import 'viewmodel/students_view_model.dart'; +import 'viewmodel/tch_room_vm.dart'; class TRoomPage extends StatefulWidget { - final int roomId; - final String startTime; + final RoomInfoDto roomInfo; const TRoomPage({ super.key, - required this.roomId, - required this.startTime, + required this.roomInfo, }); @override @@ -21,11 +20,10 @@ class TRoomPage extends StatefulWidget { class _TRoomPageState extends State { @override Widget build(BuildContext context) { - return ChangeNotifierProvider( + return ChangeNotifierProvider( create: (BuildContext context) { - return StudentsViewModel( - roomId: widget.roomId, - start: widget.startTime, + return TchRoomVM( + roomInfo: widget.roomInfo, ); }, child: Scaffold( diff --git a/lib/pages/teacher/room/viewmodel/students_view_model.dart b/lib/pages/teacher/room/viewmodel/tch_room_vm.dart similarity index 73% rename from lib/pages/teacher/room/viewmodel/students_view_model.dart rename to lib/pages/teacher/room/viewmodel/tch_room_vm.dart index 30daec0..ee9c78d 100644 --- a/lib/pages/teacher/room/viewmodel/students_view_model.dart +++ b/lib/pages/teacher/room/viewmodel/tch_room_vm.dart @@ -1,3 +1,6 @@ +import 'dart:async'; + +import 'package:app/request/dto/room/room_info_dto.dart'; import 'package:app/request/dto/room/room_user_dto.dart'; import 'package:app/request/dto/room/rtc_token_dto.dart'; import 'package:app/request/websocket/room_protocol.dart'; @@ -7,18 +10,16 @@ import 'package:flutter/cupertino.dart'; import 'type.dart'; -class StudentsViewModel extends ChangeNotifier { - StudentsViewModel({required this.roomId, String? start}) { - startTime = parseTime(start!); +class TchRoomVM extends ChangeNotifier { + TchRoomVM({required this.roomInfo, String? start}) { _startRoom(); } ///学生摄像头列表 List _students = []; - ///房间的基础信息,房间id、房间开始时间 - final int roomId; - late final DateTime startTime; + ///房间的基础信息 + final RoomInfoDto roomInfo; ///老师选中的学生id int activeSId = 0; @@ -28,7 +29,7 @@ class StudentsViewModel extends ChangeNotifier { ///是否能开始自习室 bool get canEnterRoom { final now = DateTime.now(); - if (now.isAfter(startTime)) { + if (now.isAfter(parseTime(roomInfo.startTime))) { return true; } return false; @@ -36,6 +37,8 @@ class StudentsViewModel extends ChangeNotifier { ///websocket管理 final RoomWebSocket _ws = RoomWebSocket(); + bool wsConnected = false; // socket连接状态 + StreamSubscription? _sub; RtcTokenDto? get rtcToken => _ws.rtcToken; @@ -43,18 +46,16 @@ class StudentsViewModel extends ChangeNotifier { void _startRoom() async { //如果socket的token没有,先初始化 if (_ws.wsToken.isEmpty) { - await _ws.initToken(roomId); + await _ws.initToken(roomInfo.id); } //启动连接 await _ws.connect(); - //进入房间命令 - _ws.send(RoomCommand.joinRoom); - + wsConnected = true; //监听各种ws事件 - _ws.stream.listen((msg) { + _sub = _ws.stream.listen((msg) { // 自习室人员变化 if (msg.event == RoomEvent.changeUser) { - final list = msg.data['user_list'].map((x) => RoomUserDto.fromJson(x)).toList(); + final list = RoomUserDto.listFromJson(msg.data['user_list']); onStudentChange(list); } else if ([ RoomEvent.openSpeaker, @@ -65,8 +66,7 @@ class StudentsViewModel extends ChangeNotifier { RoomEvent.closeCamera, RoomEvent.handUp, ].contains(msg.event)) { - onSyncStudentStatus(); - //TODO 直接同步服务器最新的数组覆盖,或者覆盖这一条学生的 + onSyncStudentItem(RoomUserDto.fromJson(msg.data)); } }); notifyListeners(); @@ -91,29 +91,31 @@ class StudentsViewModel extends ChangeNotifier { ///手动关闭学生扬声器、摄像头、麦克风等操作 /// - [uId]: 学生id /// - [action]: 操作类型 - void closeStudentSpeaker({ + void closeStudentAction({ required int uId, required StudentAction action, }) { final student = _students.firstWhere((t) => t.userId == uId); - Map data = { + Map data = { 'target_user_id': uId, + "mute_type": action.value, }; //如果是控制扬声器 if (action == StudentAction.speaker) { - student.speekerStatus = student.speekerStatus == 0 ? 1 : 0; - data['speeker'] = student.speekerStatus; + bool isOpen = student.speekerStatus == 1; + student.speekerStatus = isOpen ? 0 : 1; + data['is_mute'] = isOpen ? 1 : 0; } else if (action == StudentAction.camera) { //如果是摄像头,只能关 if (student.cameraStatus == 0) return; student.cameraStatus = 0; - data['camera'] = 0; + data['is_mute'] = 1; } else if (action == StudentAction.microphone) { //如果是麦克风,只能关 if (student.microphoneStatus == 0) return; student.microphoneStatus = 0; - data['microphone'] = 0; + data['is_mute'] = 1; } notifyListeners(); _ws.send(RoomCommand.switchStudentCamera, data); @@ -143,13 +145,21 @@ class StudentsViewModel extends ChangeNotifier { notifyListeners(); } - //TODO 同步学生的最新状态 - void onSyncStudentStatus() {} + /// 同步单个学生的最新状态 + void onSyncStudentItem(RoomUserDto userInfo) { + final index = _students.indexWhere((t) => t.userId == userInfo.userId); + print(userInfo.toString()); + if (index != -1) { + _students[index] = userInfo; + notifyListeners(); + } + } //销毁 @override void dispose() { super.dispose(); + _sub?.cancel(); _ws.dispose(); } } diff --git a/lib/pages/teacher/room/viewmodel/type.dart b/lib/pages/teacher/room/viewmodel/type.dart index 7e9cccc..790fc9d 100644 --- a/lib/pages/teacher/room/viewmodel/type.dart +++ b/lib/pages/teacher/room/viewmodel/type.dart @@ -1,11 +1,15 @@ ///老师操作学生的状态、摄像头、扬声器、麦克风 enum StudentAction { ///摄像头 - camera, + camera("camera"), ///麦克风 - microphone, + microphone("microphone"), ///扬声器 - speaker, + speaker("speeker"); + + final String value; + + const StudentAction(this.value); } diff --git a/lib/pages/teacher/room/widgets/content_view.dart b/lib/pages/teacher/room/widgets/content_view.dart index 8d57342..f3ae86f 100644 --- a/lib/pages/teacher/room/widgets/content_view.dart +++ b/lib/pages/teacher/room/widgets/content_view.dart @@ -3,7 +3,7 @@ import 'package:app/config/config.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../viewmodel/students_view_model.dart'; +import '../viewmodel/tch_room_vm.dart'; import 'student_item.dart'; class ContentView extends StatefulWidget { @@ -14,19 +14,24 @@ class ContentView extends StatefulWidget { } class _ContentViewState extends State { - // bool isLoading = true; //声网数据 RtcEngine? _engine; + @override + void initState() { + super.initState(); + // _initRtc(); + } + void _initRtc() async { - final vm = context.read(); + final vm = context.read(); _engine = createAgoraRtcEngine(); //初始化 RtcEngine,设置频道场景为 channelProfileLiveBroadcasting(直播场景) await _engine!.initialize( RtcEngineContext( appId: Config.swAppId, - channelProfile: ChannelProfileType.channelProfileLiveBroadcasting, + channelProfile: ChannelProfileType.channelProfileCommunication, ), ); //添加回调 @@ -39,7 +44,8 @@ class _ContentViewState extends State { // 远端用户或主播加入当前频道回调 onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {}, // 远端用户或主播离开当前频道回调 - onUserOffline: (RtcConnection connection, int remoteUid, UserOfflineReasonType reason) {}, + onUserOffline: + (RtcConnection connection, int remoteUid, UserOfflineReasonType reason) {}, ), ); //启动视频模块 @@ -48,7 +54,7 @@ class _ContentViewState extends State { await _engine!.joinChannel( token: vm.rtcToken!.token, channelId: vm.rtcToken!.channel, - uid: int.parse(vm.rtcToken!.uid), + uid: vm.rtcToken!.uid, options: ChannelMediaOptions( // 自动订阅所有视频流 autoSubscribeVideo: true, @@ -64,22 +70,13 @@ class _ContentViewState extends State { ); } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final vm = context.read(); - if (_engine == null && vm.students.isNotEmpty) { - _initRtc(); - } - } - @override Widget build(BuildContext context) { - return Consumer( + return Consumer( builder: (context, vm, _) { if (vm.students.isEmpty) { return Center( - child: Text('无学生在场,请通知学生入场'), + child: Text('准备中'), ); } //选中的学生 @@ -96,6 +93,7 @@ class _ContentViewState extends State { Expanded( child: StudentItem( user: activeStudent, + engine: _engine, ), ), SizedBox( @@ -107,6 +105,7 @@ class _ContentViewState extends State { height: 250, child: StudentItem( user: item, + engine: _engine, ), ); }, diff --git a/lib/pages/teacher/room/widgets/status_view.dart b/lib/pages/teacher/room/widgets/status_view.dart index 05b64b7..632b982 100644 --- a/lib/pages/teacher/room/widgets/status_view.dart +++ b/lib/pages/teacher/room/widgets/status_view.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'content_view.dart'; -import '../viewmodel/students_view_model.dart'; +import '../viewmodel/tch_room_vm.dart'; class StatusView extends StatefulWidget { const StatusView({super.key}); @@ -26,6 +26,8 @@ class _StatusViewState extends State { void initState() { super.initState(); _init(); + final vm = context.read(); + vm.addListener(openRoom); } @override @@ -36,10 +38,11 @@ class _StatusViewState extends State { } void _init() { - final vm = context.read(); - //如果房间可以开始 + final vm = context.read(); + //如果房间到点可以开始 if (vm.canEnterRoom) { status = RoomStatus.start; + // openRoom(); } else { status = RoomStatus.waiting; startCountDown(); @@ -48,12 +51,12 @@ class _StatusViewState extends State { ///开始倒计时 void startCountDown() { - final vm = context.read(); + final vm = context.read(); //当前时间 DateTime now = DateTime.now(); //远端时间 setState(() { - _seconds = vm.startTime.difference(now).inSeconds; + _seconds = parseTime(vm.roomInfo.startTime).difference(now).inSeconds; }); _timer = Timer.periodic(Duration(seconds: 1), (timer) { setState(() { @@ -71,8 +74,9 @@ class _StatusViewState extends State { ///开启自习室 void openRoom() { - final vm = context.read(); + final vm = context.read(); vm.toggleRoom(isOpen: true); + vm.removeListener(openRoom); } @override diff --git a/lib/pages/teacher/room/widgets/student_item.dart b/lib/pages/teacher/room/widgets/student_item.dart index e8bf1b2..85d0c85 100644 --- a/lib/pages/teacher/room/widgets/student_item.dart +++ b/lib/pages/teacher/room/widgets/student_item.dart @@ -1,3 +1,5 @@ +import 'package:agora_rtc_engine/agora_rtc_engine.dart'; +import 'package:app/pages/teacher/room/viewmodel/type.dart'; import 'package:app/request/dto/room/room_user_dto.dart'; import 'package:app/widgets/room/file_drawer.dart'; import 'package:app/widgets/room/video_surface.dart'; @@ -5,14 +7,16 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:remixicon/remixicon.dart'; -import '../viewmodel/students_view_model.dart'; +import '../viewmodel/tch_room_vm.dart'; class StudentItem extends StatefulWidget { final RoomUserDto user; + final RtcEngine? engine; const StudentItem({ super.key, required this.user, + this.engine, }); @override @@ -27,12 +31,16 @@ class _StudentItemState extends State { @override Widget build(BuildContext context) { - final vm = context.read(); + final vm = context.read(); //摄像头是否开启 bool isCameraOpen = widget.user.cameraStatus == 1; ///麦克风是否开启 bool isMicOpen = widget.user.microphoneStatus == 1; + + ///声音是否开启 + bool isSpeakerOpen = widget.user.speekerStatus == 1; + return ClipRRect( borderRadius: BorderRadius.circular(10), child: Container( @@ -44,6 +52,13 @@ class _StudentItemState extends State { width: double.infinity, child: Stack( children: [ + if (widget.engine != null) + AgoraVideoView( + controller: VideoViewController( + rtcEngine: widget.engine!, + canvas: VideoCanvas(uid: widget.user.rtcUid), + ), + ), // VideoSurface(), Positioned( bottom: 0, @@ -95,14 +110,35 @@ class _StudentItemState extends State { _actionItem( icon: isCameraOpen ? RemixIcons.video_on_fill : RemixIcons.video_off_fill, isActive: isCameraOpen, + onTap: () { + vm.closeStudentAction( + uId: widget.user.userId, + action: StudentAction.camera, + ); + }, ), _actionItem( icon: isMicOpen ? RemixIcons.mic_fill : RemixIcons.mic_off_fill, isActive: isMicOpen, + onTap: () { + vm.closeStudentAction( + uId: widget.user.userId, + action: StudentAction.microphone, + ); + }, + ), + _actionItem( + icon: isSpeakerOpen + ? RemixIcons.volume_up_fill + : RemixIcons.volume_mute_fill, + isActive: isSpeakerOpen, + onTap: () { + vm.closeStudentAction( + uId: widget.user.userId, + action: StudentAction.speaker, + ); + }, ), - // _actionItem( - // icon: RemixIcons.volume_mute_fill, - // ), _actionItem(icon: RemixIcons.file_list_3_fill, onTap: _openFileList), ], ), diff --git a/lib/providers/user_store.dart b/lib/providers/user_store.dart index 881ebe9..912bfa1 100644 --- a/lib/providers/user_store.dart +++ b/lib/providers/user_store.dart @@ -7,21 +7,17 @@ class UserStore extends ChangeNotifier { UserInfoDto? userInfo; String token = ""; - ///设置用户数据 - Future asyncUserInfo() async { - if (token.isNotEmpty) { - var res = await getUserInfoApi(); - await Storage.set("user_info", res.toJson()); - setUserInfo(); - notifyListeners(); - } + Future init() async{ + token = await getToken(); + await setUserInfo(); + notifyListeners(); } ///获取用户数据 Future setUserInfo() async { - var info = await Storage.get("user_info"); - if (info != null) { - userInfo = UserInfoDto.fromJson(info); + if (token.isNotEmpty) { + userInfo = await getUserInfoApi(); + await Storage.set("user_info", userInfo!.toJson()); } } @@ -32,15 +28,16 @@ class UserStore extends ChangeNotifier { } ///获取token - static Future getToken() async { + static Future getToken() async { return await Storage.get("token") ?? ''; } ///退出登录 Future logout() async { - logoutApi(); + await logoutApi(); await Storage.remove('token'); await Storage.remove('user_info'); + userInfo = null; token = ''; notifyListeners(); } diff --git a/lib/request/dto/room/room_info_dto.dart b/lib/request/dto/room/room_info_dto.dart index 8866bfd..16655ef 100644 --- a/lib/request/dto/room/room_info_dto.dart +++ b/lib/request/dto/room/room_info_dto.dart @@ -1,8 +1,7 @@ class RoomInfoDto { - - RoomInfoDto({ required this.teacherBackground, + required this.teacherAvatar, required this.roomName, required this.startTime, required this.teacherName, @@ -11,6 +10,7 @@ class RoomInfoDto { }); String teacherBackground; + String teacherAvatar; String roomName; String startTime; String teacherName; @@ -20,6 +20,7 @@ class RoomInfoDto { factory RoomInfoDto.fromJson(Map json) => RoomInfoDto( teacherBackground: json["teacher_background"], + teacherAvatar: json["teacher_avatar"], roomName: json["room_name"], startTime: json["start_time"], teacherName: json["teacher_name"], @@ -30,6 +31,7 @@ class RoomInfoDto { Map toJson() => { "teacher_background": teacherBackground, + "teacher_avatar": teacherAvatar, "room_name": roomName, "start_time": startTime, "teacher_name": teacherName, diff --git a/lib/request/dto/room/room_type_dto.dart b/lib/request/dto/room/room_type_dto.dart index 381cd18..e67f422 100644 --- a/lib/request/dto/room/room_type_dto.dart +++ b/lib/request/dto/room/room_type_dto.dart @@ -1,7 +1,7 @@ class RoomTypeDto { final int studyRoomId; final int teacherId; - final String teacherRtcUid; + final int teacherRtcUid; final String teacherWsClientId; final int roomStatus; final String dataType; @@ -30,7 +30,7 @@ class RoomTypeDto { return RoomTypeDto( studyRoomId: json["study_room_id"] ?? 0, teacherId: json["teacher_id"] ?? 0, - teacherRtcUid: json["teacher_rtc_uid"] ?? "", + teacherRtcUid: json["teacher_rtc_uid"] ?? 0, teacherWsClientId: json["teacher_ws_client_id"] ?? "", roomStatus: json["room_status"] ?? 0, dataType: json["data_type"] ?? "", diff --git a/lib/request/dto/room/room_user_dto.dart b/lib/request/dto/room/room_user_dto.dart index 983f1e0..dd08d36 100644 --- a/lib/request/dto/room/room_user_dto.dart +++ b/lib/request/dto/room/room_user_dto.dart @@ -1,20 +1,21 @@ class RoomUserDto { final int userId; - final String rtcUid; + final int rtcUid; int microphoneStatus; int cameraStatus; int speekerStatus; final String wsClientId; final String userName; final String avatar; + /// 1是学生,2是老师 final int userType; final List filesList; final String dataType; - int handup; + int handup; int online; //0离线,1在线 - RoomUserDto({ + RoomUserDto({ required this.userId, required this.rtcUid, required this.microphoneStatus, @@ -65,4 +66,12 @@ class RoomUserDto { "online": online, }; } + + static List listFromJson(List data) => + data.map((e) => RoomUserDto.fromJson(e)).toList(); + + @override + String toString() { + return 'RoomUserDto{userId: $userId, rtcUid: $rtcUid, microphoneStatus: $microphoneStatus, cameraStatus: $cameraStatus, speekerStatus: $speekerStatus, wsClientId: $wsClientId, userName: $userName, avatar: $avatar, userType: $userType, filesList: $filesList, dataType: $dataType, handup: $handup, online: $online,}'; + } } diff --git a/lib/request/dto/room/rtc_token_dto.dart b/lib/request/dto/room/rtc_token_dto.dart index ee5dfcf..349d351 100644 --- a/lib/request/dto/room/rtc_token_dto.dart +++ b/lib/request/dto/room/rtc_token_dto.dart @@ -6,7 +6,7 @@ class RtcTokenDto { required this.token, }); - String uid; + int uid; DateTime expiresAt; String channel; String token; diff --git a/lib/request/network/interceptor.dart b/lib/request/network/interceptor.dart index fae793d..7230020 100644 --- a/lib/request/network/interceptor.dart +++ b/lib/request/network/interceptor.dart @@ -31,6 +31,7 @@ void onResponse( error: {'code': 0, 'message': apiResponse.message}, ), ); + showError(apiResponse.message); } } diff --git a/lib/request/websocket/room_protocol.dart b/lib/request/websocket/room_protocol.dart index 34578c9..089c64f 100644 --- a/lib/request/websocket/room_protocol.dart +++ b/lib/request/websocket/room_protocol.dart @@ -9,7 +9,7 @@ enum RoomCommand { getRoomInfo("room_data"), ///学生开关扬声器、摄像头、麦克风 - switchCamera("mute_self"), + studentActon("mute_self"), ///学生上传文件 uploadFile("upload_file"), @@ -66,7 +66,7 @@ enum RoomEvent { handUp("sys_user_handup"), ///自习室以开启,进入自习室(学生用) - openRoom("sys_start_study_room"), + // openRoom("sys_start_study_room"), ///自习室以关闭,退出自习室(学生用) closeRoom("sys_close_study_room"), @@ -94,10 +94,10 @@ enum RoomEvent { const RoomEvent(this.value); /// 根据 值获取枚举 - static RoomEvent fromStr(String value) { - return RoomEvent.values.firstWhere( - (e) => e.value == value, - orElse: () => throw ArgumentError('Invalid weather type value: $value'), - ); + static RoomEvent? fromStr(String value) { + for (final e in RoomEvent.values) { + if (e.value == value) return e; + } + return null; // 找不到就返回 null } } diff --git a/lib/request/websocket/room_websocket.dart b/lib/request/websocket/room_websocket.dart index 13c38f9..8a9b7df 100644 --- a/lib/request/websocket/room_websocket.dart +++ b/lib/request/websocket/room_websocket.dart @@ -60,7 +60,15 @@ class RoomWebSocket { (data) { //监听事件 final jsonMap = jsonDecode(data); - RoomMessage msg = RoomMessage(RoomEvent.fromStr(jsonMap['action']), jsonMap['data']); + + final event = RoomEvent.fromStr(jsonMap['action']); + if (event == null) { + print("未识别的 action: ${jsonMap['action']},消息已忽略"); + return; // 直接跳过 + } else { + logger.i("接收到事件: ${event.value}"); + } + final msg = RoomMessage(event, jsonMap['data']); _msgController.add(msg); }, onDone: () {}, @@ -68,10 +76,12 @@ class RoomWebSocket { logger.e("连接异常断开"); }, ); + //自动加入房间 + send(RoomCommand.joinRoom); + //心跳 _heartbeatTimer?.cancel(); _heartbeatTimer = Timer.periodic(Duration(seconds: 15), (_) { - logger.i("发送心跳"); send(RoomCommand.ping); }); } catch (e) { @@ -84,8 +94,12 @@ class RoomWebSocket { void send(RoomCommand action, [Map? params]) { final msg = { "action": action.value, - "data": params, + if (params != null) ...params, }; + if(action != RoomCommand.ping){ + logger.i("发送指令:$msg"); + } + _socket!.add(jsonEncode(msg)); } @@ -105,7 +119,7 @@ class RoomWebSocket { //socket取消 _socket?.close(); // 销毁事件流 - _msgController.close(); + // _msgController.close(); // 错误重连取消 _reconnectTimer?.cancel(); _reconnectTimer = null; diff --git a/lib/router/modules/student_routes.dart b/lib/router/modules/student_routes.dart index 3c5715d..df829a6 100644 --- a/lib/router/modules/student_routes.dart +++ b/lib/router/modules/student_routes.dart @@ -14,7 +14,10 @@ List studentRoutes = [ RouterConfig( path: RoutePaths.sRoom, child: (state) { - return SRoomPage(); + final extra = state.extra as dynamic; + return SRoomPage( + roomInfo: extra, + ); }, ), -]; \ No newline at end of file +]; diff --git a/lib/router/modules/teacher_routes.dart b/lib/router/modules/teacher_routes.dart index a8de2c7..ddcf3d3 100644 --- a/lib/router/modules/teacher_routes.dart +++ b/lib/router/modules/teacher_routes.dart @@ -14,12 +14,9 @@ List teacherRoutes = [ RouterConfig( path: RoutePaths.tRoom, child: (state) { - final extra = state.extra as Map?; - final roomId = extra?['roomId'] as int?; - final startTime = extra?['startTime'] as String?; + final extra = state.extra as dynamic; return TRoomPage( - roomId: roomId!, - startTime: startTime!, + roomInfo: extra, ); }, ), diff --git a/lib/widgets/room/video_surface.dart b/lib/widgets/room/video_surface.dart index d492203..2900951 100644 --- a/lib/widgets/room/video_surface.dart +++ b/lib/widgets/room/video_surface.dart @@ -21,60 +21,26 @@ enum VideoState { error, } -// class VideoSurface extends StatelessWidget { -// final VideoState state; -// -// const VideoSurface({super.key, this.state = VideoState.normal}); -// -// @override -// Widget build(BuildContext context) { -// String stateText = switch (state) { -// VideoState.closed => "摄像头已关闭", -// VideoState.offline => "掉线", -// VideoState.loading => "加载中", -// VideoState.error => "错误", -// _ => "未知", -// }; -// //如果不是正常 -// if (state != VideoState.normal) { -// return Align( -// child: Text(stateText, style: TextStyle(color: Colors.white70)), -// ); -// } -// return Container(); -// } -// } +class VideoSurface extends StatelessWidget { + final VideoState state; -class VideoSurface extends StatefulWidget { - final RtcTokenDto rtcToken; - final String remoteUid; - - const VideoSurface({ - super.key, - required this.rtcToken, - required this.remoteUid, - }); - - @override - State createState() => _VideoSurfaceState(); -} - -class _VideoSurfaceState extends State { - RtcEngine? _engine; - - void _init() async { - _engine = createAgoraRtcEngine(); - //初始化 RtcEngine,设置频道场景为 channelProfileLiveBroadcasting(直播场景) - await _engine!.initialize( - RtcEngineContext( - appId: Config.swAppId, - channelProfile: ChannelProfileType.channelProfileLiveBroadcasting, - ), - ); - } + const VideoSurface({super.key, this.state = VideoState.normal}); @override Widget build(BuildContext context) { - return const Placeholder(); + String stateText = switch (state) { + VideoState.closed => "摄像头已关闭", + VideoState.offline => "掉线", + VideoState.loading => "加载中", + VideoState.error => "错误", + _ => "未知", + }; + //如果不是正常 + if (state != VideoState.normal) { + return Align( + child: Text(stateText, style: TextStyle(color: Colors.white70)), + ); + } + return Container(); } }