1
This commit is contained in:
@@ -79,12 +79,16 @@ class _LoginPageState extends State<LoginPage> {
|
||||
|
||||
//设置登录信息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 {
|
||||
|
||||
@@ -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<SplashPage> {
|
||||
context.go(RoutePaths.login);
|
||||
} else {
|
||||
UserStore userStore = context.read<UserStore>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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<SHomePage> createState() => _SHomePageState();
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (_) => SHomeVm(),
|
||||
child: _HomeView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SHomePageState extends State<SHomePage> {
|
||||
|
||||
|
||||
///刷新状态
|
||||
Future<void> _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<SHomeVm>();
|
||||
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: [
|
||||
|
||||
@@ -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<SHomeVm>();
|
||||
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 {
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<STodayCard> {
|
||||
///进入自习室
|
||||
void _handleEnterRoom() {
|
||||
context.push(RoutePaths.sRoom);
|
||||
///前往会议室
|
||||
void _goToRoom() {
|
||||
checkPermission(
|
||||
permissions: [Permission.microphone, Permission.camera],
|
||||
onGranted: () {
|
||||
final vm = context.read<SHomeVm>();
|
||||
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<SHomeVm>();
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
34
lib/pages/student/home/viewmodel/s_home_vm.dart
Normal file
34
lib/pages/student/home/viewmodel/s_home_vm.dart
Normal file
@@ -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<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 = parseTime(roomInfo!.startTime);
|
||||
final end = parseTime(roomInfo!.endTime);
|
||||
|
||||
return end.difference(start).inMinutes;
|
||||
}
|
||||
}
|
||||
@@ -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<UserStore>();
|
||||
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),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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<BottomBar> {
|
||||
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<StuRoomVM>(
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<TopBar> createState() => _TopBarState();
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
|
||||
class _TopBarState extends State<TopBar> {
|
||||
Timer? _timer;
|
||||
int seconds = 0;
|
||||
late DateTime startTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final vm = context.read<StuRoomVM>();
|
||||
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<StuRoomVM>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<SRoomPage> createState() => _SRoomPageState();
|
||||
@@ -29,57 +35,63 @@ class _SRoomPageState extends State<SRoomPage> {
|
||||
|
||||
@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<UserStore>();
|
||||
return ChangeNotifierProvider<StuRoomVM>(
|
||||
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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<StudentVideoList> createState() => _StudentVideoListState();
|
||||
}
|
||||
|
||||
class _StudentVideoListState extends State<StudentVideoList> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.watch<StuRoomVM>();
|
||||
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<StudentVideoList> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
133
lib/pages/student/room/viewmodel/stu_room_vm.dart
Normal file
133
lib/pages/student/room/viewmodel/stu_room_vm.dart
Normal file
@@ -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<RoomUserDto> 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<RoomMessage>? _sub;
|
||||
|
||||
RtcTokenDto? get rtcToken => _ws.rtcToken;
|
||||
|
||||
StuRoomVM({required this.roomInfo, required this.uid}) {
|
||||
_startRoom();
|
||||
}
|
||||
|
||||
///开始链接房间
|
||||
Future<void> _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<RoomUserDto> list) {
|
||||
List<RoomUserDto> 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
///能否进入房间
|
||||
|
||||
@@ -28,13 +28,7 @@ class _TodayCardState extends State<TodayCard> {
|
||||
permissions: [Permission.microphone, Permission.camera],
|
||||
onGranted: () {
|
||||
final vm = context.read<HomeViewModel>();
|
||||
context.push(
|
||||
RoutePaths.tRoom,
|
||||
extra: {
|
||||
"roomId": vm.roomInfo!.id,
|
||||
"startTime": vm.roomInfo!.startTime,
|
||||
},
|
||||
);
|
||||
context.push(RoutePaths.tRoom,extra: vm.roomInfo);
|
||||
},
|
||||
onDenied: () {
|
||||
EasyLoading.showError("请开启权限");
|
||||
|
||||
@@ -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<TRoomPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider<StudentsViewModel>(
|
||||
return ChangeNotifierProvider<TchRoomVM>(
|
||||
create: (BuildContext context) {
|
||||
return StudentsViewModel(
|
||||
roomId: widget.roomId,
|
||||
start: widget.startTime,
|
||||
return TchRoomVM(
|
||||
roomInfo: widget.roomInfo,
|
||||
);
|
||||
},
|
||||
child: Scaffold(
|
||||
|
||||
@@ -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<RoomUserDto> _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<RoomMessage>? _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<String, int> data = {
|
||||
Map<String, dynamic> 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();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
///老师操作学生的状态、摄像头、扬声器、麦克风
|
||||
enum StudentAction {
|
||||
///摄像头
|
||||
camera,
|
||||
camera("camera"),
|
||||
|
||||
///麦克风
|
||||
microphone,
|
||||
microphone("microphone"),
|
||||
|
||||
///扬声器
|
||||
speaker,
|
||||
speaker("speeker");
|
||||
|
||||
final String value;
|
||||
|
||||
const StudentAction(this.value);
|
||||
}
|
||||
|
||||
@@ -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<ContentView> {
|
||||
// bool isLoading = true;
|
||||
|
||||
//声网数据
|
||||
RtcEngine? _engine;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// _initRtc();
|
||||
}
|
||||
|
||||
void _initRtc() async {
|
||||
final vm = context.read<StudentsViewModel>();
|
||||
final vm = context.read<TchRoomVM>();
|
||||
_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<ContentView> {
|
||||
// 远端用户或主播加入当前频道回调
|
||||
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<ContentView> {
|
||||
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<ContentView> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final vm = context.read<StudentsViewModel>();
|
||||
if (_engine == null && vm.students.isNotEmpty) {
|
||||
_initRtc();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<StudentsViewModel>(
|
||||
return Consumer<TchRoomVM>(
|
||||
builder: (context, vm, _) {
|
||||
if (vm.students.isEmpty) {
|
||||
return Center(
|
||||
child: Text('无学生在场,请通知学生入场'),
|
||||
child: Text('准备中'),
|
||||
);
|
||||
}
|
||||
//选中的学生
|
||||
@@ -96,6 +93,7 @@ class _ContentViewState extends State<ContentView> {
|
||||
Expanded(
|
||||
child: StudentItem(
|
||||
user: activeStudent,
|
||||
engine: _engine,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
@@ -107,6 +105,7 @@ class _ContentViewState extends State<ContentView> {
|
||||
height: 250,
|
||||
child: StudentItem(
|
||||
user: item,
|
||||
engine: _engine,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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<StatusView> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_init();
|
||||
final vm = context.read<TchRoomVM>();
|
||||
vm.addListener(openRoom);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -36,10 +38,11 @@ class _StatusViewState extends State<StatusView> {
|
||||
}
|
||||
|
||||
void _init() {
|
||||
final vm = context.read<StudentsViewModel>();
|
||||
//如果房间可以开始
|
||||
final vm = context.read<TchRoomVM>();
|
||||
//如果房间到点可以开始
|
||||
if (vm.canEnterRoom) {
|
||||
status = RoomStatus.start;
|
||||
// openRoom();
|
||||
} else {
|
||||
status = RoomStatus.waiting;
|
||||
startCountDown();
|
||||
@@ -48,12 +51,12 @@ class _StatusViewState extends State<StatusView> {
|
||||
|
||||
///开始倒计时
|
||||
void startCountDown() {
|
||||
final vm = context.read<StudentsViewModel>();
|
||||
final vm = context.read<TchRoomVM>();
|
||||
//当前时间
|
||||
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<StatusView> {
|
||||
|
||||
///开启自习室
|
||||
void openRoom() {
|
||||
final vm = context.read<StudentsViewModel>();
|
||||
final vm = context.read<TchRoomVM>();
|
||||
vm.toggleRoom(isOpen: true);
|
||||
vm.removeListener(openRoom);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -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<StudentItem> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.read<StudentsViewModel>();
|
||||
final vm = context.read<TchRoomVM>();
|
||||
//摄像头是否开启
|
||||
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<StudentItem> {
|
||||
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<StudentItem> {
|
||||
_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),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -7,21 +7,17 @@ class UserStore extends ChangeNotifier {
|
||||
UserInfoDto? userInfo;
|
||||
String token = "";
|
||||
|
||||
///设置用户数据
|
||||
Future<void> asyncUserInfo() async {
|
||||
if (token.isNotEmpty) {
|
||||
var res = await getUserInfoApi();
|
||||
await Storage.set("user_info", res.toJson());
|
||||
setUserInfo();
|
||||
notifyListeners();
|
||||
}
|
||||
Future<void> init() async{
|
||||
token = await getToken();
|
||||
await setUserInfo();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
///获取用户数据
|
||||
Future<void> 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<String> getToken() async {
|
||||
static Future<String> getToken() async {
|
||||
return await Storage.get("token") ?? '';
|
||||
}
|
||||
|
||||
///退出登录
|
||||
Future<void> logout() async {
|
||||
logoutApi();
|
||||
await logoutApi();
|
||||
await Storage.remove('token');
|
||||
await Storage.remove('user_info');
|
||||
userInfo = null;
|
||||
token = '';
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -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<dynamic, dynamic> 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<dynamic, dynamic> toJson() =>
|
||||
{
|
||||
"teacher_background": teacherBackground,
|
||||
"teacher_avatar": teacherAvatar,
|
||||
"room_name": roomName,
|
||||
"start_time": startTime,
|
||||
"teacher_name": teacherName,
|
||||
|
||||
@@ -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"] ?? "",
|
||||
|
||||
@@ -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<String> 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<RoomUserDto> listFromJson(List<dynamic> 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,}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ class RtcTokenDto {
|
||||
required this.token,
|
||||
});
|
||||
|
||||
String uid;
|
||||
int uid;
|
||||
DateTime expiresAt;
|
||||
String channel;
|
||||
String token;
|
||||
|
||||
@@ -31,6 +31,7 @@ void onResponse(
|
||||
error: {'code': 0, 'message': apiResponse.message},
|
||||
),
|
||||
);
|
||||
showError(apiResponse.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, dynamic>? 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;
|
||||
|
||||
@@ -14,7 +14,10 @@ List<RouterConfig> studentRoutes = [
|
||||
RouterConfig(
|
||||
path: RoutePaths.sRoom,
|
||||
child: (state) {
|
||||
return SRoomPage();
|
||||
final extra = state.extra as dynamic;
|
||||
return SRoomPage(
|
||||
roomInfo: extra,
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
];
|
||||
|
||||
@@ -14,12 +14,9 @@ List<RouterConfig> teacherRoutes = [
|
||||
RouterConfig(
|
||||
path: RoutePaths.tRoom,
|
||||
child: (state) {
|
||||
final extra = state.extra as Map<String, dynamic>?;
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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<VideoSurface> createState() => _VideoSurfaceState();
|
||||
}
|
||||
|
||||
class _VideoSurfaceState extends State<VideoSurface> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user